家族が触るmandoの照明ボタン、家のOSは3段階で応える
家じゅうの機器はCLIで動かしていますが、家族が触れる入口はそこではありません。スマホから開く1個のWebページ、mando だけです。ボタンは「開ける」「閉める」「点ける」「消す」しかない、最小限のUIです。
この記事では、その最小のボタンを1つ押したときに、家の中で何が起きているかを辿ります。以前の記事では各ツールの役割を上から見下ろす全体構成を書きましたが、今回は逆に、家族が押す1点から下りていく視点です。
ボタンの裏では、2つの返し方が混ざっている
mando はプロトコルを一切喋りません。設定ファイルに書かれたコマンド列をそのまま実行するだけです。電動シャッターはECHONET Lite、照明はMatter——どちらも open や on を叩けば物理はすぐに反応します。ただし「反応した」という確認の返し方が、この2つでまったく違います。
シャッターは操作のたびに enl(ブートストラップ期。いまは casa 経由)へ確認コマンドを投げ、開閉が終わるまで短い間隔でポーリングします。愚直ですが、機器の台数もアクティブな時間帯も限られているので、これで足りています。
照明は勝手が違いました。Matterの照明制御は**groupcast(応答を待たない一斉送信)**です。物理は即座に反応しますが、「本当に点いたか」を確認するには、代表ノードへ個別にunicastで read を投げ直す必要があります。そしてこの確認readが、あるとき突然80秒待たされることが分かりました。
確認readが80秒かかった日
「照明を点けると物理はすぐ点くのに、UIの表示だけが遅い、あるいは応答なしになる」という不具合を調べたところ、原因はMatterのセッション確立(CASE)のcold-startでした。
| 条件 | 実測値 |
|---|---|
| まだ購読していないノードへの初回read | 3.6秒〜80.7秒(応答なしで失敗することもある) |
| 2回目以降のread | 約100ミリ秒 |
| 購読確立後、2時間以上放置してからのread | 159ミリ秒(warmのまま) |
つまり遅いのは「常駐デーモン matd がまだそのノードを購読していない最初の1回」だけです。一度購読が確立すれば、あとは軽い。mando の実行タイムアウト(既定15秒)がこの初回ハンドシェイクを打ち切ることで、UIには「応答なし」として現れていました。
対処療法として確認readのタイムアウトを伸ばすこともできましたが、それでは根治になりません。選んだのは、そもそも確認のためのreadをやめることでした。
選んだこと: push優先の3段階、TTLでは腐らせない
matd に対して常駐で購読している mat listen は、家の中で照明の状態が変わるたびにイベントを流します。これを長寿命のサブプロセスとしてmando自身が張り、ブラウザへはSSE(Server-Sent Events)で配信するようにしました。
ここでの判断は「値の古さをTTLで管理しない」ことです。静止した照明の状態は勝手には変わりません。変わるならイベントが飛んでくるはずなので、信頼できるかどうかはリスナーが生きているかどうかだけで決まります。デバイスごとに次の3状態を持たせました。
| 状態 | 条件 | 応答 |
|---|---|---|
| primed | リスナー接続中・基準値確立済み | push値をそのまま返す。実行コマンドはゼロ・即答 |
| unprimed | 起動直後・再接続直後で基準値が未確立 | 1回だけreadして確定させる(これが購読を誘発する) |
| disconnected | リスナーが断・再起動待ち | readにフォールバック。それも失敗したらstale: trueで正直に返す |
GET /api/devices/{name}/state のレスポンスには source(pushかreadか)と stale が付きます。「いま何を根拠に表示しているか」を隠さない、という設計です。
{"device": "living_lights", "state": "on", "source": "push", "stale": false}
もう1つ、地味だが効いている判断があります。押下してからのイベントの突合キーである node_id は、readの戻り値から自己学習させず、設定ファイルに固定で書かせることにしました。学習させれば設定は自動で追従しますが、学習のために必要なreadこそが80秒かかったり失敗したりする張本人です。直したい不具合が、そのまま学習の失敗モードとして残ってしまいます。configに固定し、再commissionでずれたら黙って自己修復せず警告だけ出す——設定の真実は設定ファイルにある、という一貫性を優先しました。
捨てたこと: 全部を同じ作りにはしない
シャッターの状態はpush化していません。ECHONET Lite側には照明のMatterのような常駐購読の主体がいないので、従来どおり「set後の同期確認+アクティブ窓でのポーリング」のままです。1つの入口だからといって、下の機構まで無理に揃える必要はありませんでした。
Threadメッシュの状態を見る /mesh 画面も同じ発想です。これは家族向けではなく技術者本人向けの画面なので、トップページからはリンクしていません。取得コマンド mat diag mesh は全ノードを逐次probeするため実測1分45秒かかる重い処理で、常時ポーリングはしません。誰かが /mesh を開いている間だけ非同期ジョブが動き、見ている人がいなければ何も走らない仕組みにしました。mando はスケジューラを持たない、という原則をここでも崩していません。
デーモン化そのものも自前ではやっていません。mando は前景プロセスのまま起動し、自動起動・異常時再起動・ログはラズパイの systemd に任せています。self-forkで自分をデーモン化する仕組みはあえて作りませんでした。
住んでみた答え合わせ
前回の記事では、当時のUIが数秒間隔でポーリングしていたことについて「さすがにやりすぎなので、やめる予定」と書きました。実際にpush化に切り替えてから、今日でおよそ2か月が経ちます。
運用に乗せてから見えた穴も1つありました。matd と mando を同時に再起動すると、起動直後の再ベースラインreadと matd の購読確立が同じデバイスで競合し、警告が数件まとまって出ることがありました。対策として、再ベースラインは購読が確立したノードから順にreadする「primeループ」に変え、残りは間隔を空けながら再試行する形に直しています。派手な失敗ではなく、運用してみて初めて踏んだ小さな競合でした。
明るさや色の値表示はまだreadのままです。同じ仕組みで拾えることは実測で確認済みなので、次はここをpushに寄せていくつもりです。