Raspberry PiにUPS HATを接続し、バッテリー残量と入力電圧を毎分監視するスクリプトを動かしていた。停電を検知したら安全にシャットダウンする——その意図は正しかった。だがこのスクリプトが、数日にわたりシステムを停止させていた本体だった。
本記事は、保護を目的としたコードがかえって破壊的になった経緯と、そこから得られた教訓の記録である。
症状:理由のない再起動
数日間にわたり、1日に1〜2回の不本意な再起動が発生していた。Bluetoothヘッドホンが突然切断され、オーディオストリームが途切れ、セッションが不自然に再開される。再起動の時刻には規則性がなく、負荷との相関も見られなかった。
まず疑ったのは、カーネルパニック、OOM killer、アンダーボルトの3つ。しかし dmesg にも journalctl にも、いずれの痕跡も残っていなかった。
ログがない診断
最大の障害は、ログそのものが存在しなかったことである。journald の設定が Storage=volatile になっており、再起動のたびにシステムログがメモリから消去されていた。再起動の原因を知るためのログが、再起動そのものによって消されていた——循環的欠陥である。
これを解消するため、Storage=persistent に変更し、/var/log/journal/ にログが残るようにした。この変更が完了した直後、現象が再発した。
システムの最期の記録
永続化されたジャーナルを開くと、前回ブートの終末期が克明に残されていた。
CRON: upsPlus.py 実行 sudo: /usr/bin/sync sudo: /usr/sbin/halt systemd-logind: The system will halt now!
監視スクリプト upsPlus.py が sudo halt を実行していた。停電を検知してシステムを保護するはずのコードが、通常稼働中のシステムを停止させていた。
なぜ halt したのか:I2C通信の罠
upsPlus.py は毎分I2Cバスを通じてUPS HATのステータスを読み取る。ある瞬間、I2C通信が一時的に失敗し、Type-C入力電圧が正しく読めなかった。スクリプトはこれを「電源が接続されていない(停電)」と解釈し、保護シーケンスを起動した。
os.system("sudo sync && sudo halt")
この1行が、守るためのコードの中で最も破壊的な存在だった。I2Cの通信失敗に対するエラーハンドリングは一切なく、1回の誤読が即座にシステム停止に直結する設計だった。
皮肉な連鎖:直したつもりが壊した
さらに皮肉なことに、問題発生前はこのスクリプトが実行されてすらなかった。crontabに登録されたPythonパスが、存在しない仮想環境を指していたためだ。スクリプトは静かに失敗し続けていた。
調査過程でこのパスを修正した。スクリプトが正常に動き始めた——そして2時間後、最初の halt を引き起こした。
それまで「壊れていたから安全だった」コードを、直したことで破壊的になった。修正が新しい問題を生むとは、その時点では予想していなかった。
I2Cは嘘をつく
この問題の根幹には、I2C通信の信頼性がある。I2Cはハードウェアバスであり、信号ノイズ、バスの占有競合、デバイスの一時的な応答不能といった理由で、通信失敗は日常的に発生する。
センサーデータを信用して破壊的なアクション(シャットダウン等)を取る設計は、センサーが嘘をつかないことを前提にしている。現実のハードウェアは、嘘をつく。一時的な読み取り失敗を「停電」と同一視してはならない。
公式サンプルコードの落とし穴
upsPlus.py は、UPS HATメーカーが提供する公式サンプルコードをベースにしていた。「公式だから安全だ」という前提が、ここで崩れた。
サンプルコードは「動くこと」を目的として書かれている。「長期間、毎分実行されて安定して動くこと」は保証しない。エラーハンドリング、通信のリトライ、状態の複数経路検証——本番運用に必要な層は、デモには含まれていないことが多い。
これは特定のメーカーの問題ではない。センサー、HAT、周辺デバイスの公式サンプル全般に言えることだ。「動く」と「安定して動く」の間には、本番環境の洗礼が必要である。
修正と設計の転換
シャットダウンロジックを無効化し、測定・表示機能のみを維持した。バッテリー保護は、UPS MCUファームウェアの自律機能に任せる。
ハードウェアレベルの保護は、ソフトウェアの推測よりも信頼できる。MCUファームウェアは、I2Cの読み取りを介さず、直接電圧と充電状態を監視している。一時的な通信エラーに左右されない。
5つの教訓
- サンプルコードは「動く」ために書かれている。「長く安定して動く」ためではない。本番環境で定期実行するスクリプトに、デモコードをそのまま使ってはならない。
- I2C通信には一時的な失敗がつきもの。読み取り失敗を、シャットダウン等の致命的な判断の根拠にしない。リトライ、タイムアウト、複数回の確認を挟む。
- 破壊的なアクションを取る前に、状態を複数の経路で検証する。1つのセンサー読み取りだけで「停電」と断定しない。入力電圧、バッテリー電圧、充電状態を組み合わせて判断する。
- ログの永続化は、障害対応の前提条件。揮発性ログでは再起動原因すら追跡できない。
Storage=persistentをデフォルトにする。 - 「修正」が新しい問題を露出することがある。特に、それまで動いていなかったコードを動くようにしたときは、そのコードが何をするかを確認してから有効化する。
おわりに
「システムを守る」という目的は、コードを書く人にとって最も納得感のある動機の1つだ。しかし守るためのロジックが、守る対象と同じ信頼性の前提に立っていないとき、それは保護ではなく攻撃になる。
センサーは嘘をつく。通信は途切れる。サンプルコードは本番を知らない。この3つを前提に設計したとき、保護の境界線は初めて機能する。
本記事の事象は2026年8月上旬に発生し、halt問題の原因は特定・修正済みである。なお、haltとは別の経路による再起動が別に存在しており、こちらはログの永続化後に観察を継続している。