守るためのコードが壊す側に回るとき —— Raspberry Pi UPS監視スクリプトが引き起こした再起動

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.pysudo 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つの教訓

  1. サンプルコードは「動く」ために書かれている。「長く安定して動く」ためではない。本番環境で定期実行するスクリプトに、デモコードをそのまま使ってはならない。
  2. I2C通信には一時的な失敗がつきもの。読み取り失敗を、シャットダウン等の致命的な判断の根拠にしない。リトライ、タイムアウト、複数回の確認を挟む。
  3. 破壊的なアクションを取る前に、状態を複数の経路で検証する。1つのセンサー読み取りだけで「停電」と断定しない。入力電圧、バッテリー電圧、充電状態を組み合わせて判断する。
  4. ログの永続化は、障害対応の前提条件。揮発性ログでは再起動原因すら追跡できない。Storage=persistent をデフォルトにする。
  5. 「修正」が新しい問題を露出することがある。特に、それまで動いていなかったコードを動くようにしたときは、そのコードが何をするかを確認してから有効化する。

おわりに

「システムを守る」という目的は、コードを書く人にとって最も納得感のある動機の1つだ。しかし守るためのロジックが、守る対象と同じ信頼性の前提に立っていないとき、それは保護ではなく攻撃になる。

センサーは嘘をつく。通信は途切れる。サンプルコードは本番を知らない。この3つを前提に設計したとき、保護の境界線は初めて機能する。

本記事の事象は2026年8月上旬に発生し、halt問題の原因は特定・修正済みである。なお、haltとは別の経路による再起動が別に存在しており、こちらはログの永続化後に観察を継続している。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール