入力した7が、1秒後に88へ戻りました
第2回では、Ajaxポーリングの応答が送信順に返るとは限らないことを扱いました。要求番号を使い、古い応答を画面へ出さない仕組みです。
順序を直したあとにも、画面には別の問題が残っていました。
装置の現在値が88のとき、入力欄へ7を打つ。まだ適用ボタンを押していないのに、次の周期取得で入力欄が88へ戻ります。通信は成功しています。装置の値も正しい。それでも、利用者が編集中の値を画面が勝手に消しています。
原因は、「装置が返した値」と「これから送る値」を同じ場所へ置いたことでした。
この記事で分かること
- 装置が返した値と、ユーザーがこれから送る値を分ける理由
- 周期取得で入力途中の値を上書きしない方法
- 要求値、適用中、適用済みを画面で区別する考え方
- LCDとWebから操作される装置で、排他をどこへ置くか
- 操作タイムアウト後に装置状態を取り直す理由
この記事の前提
- 対象読者:第1回・第2回の構成を読んだ方、小型装置のWeb UIを設計する方
- 前提知識:JavaScript、jQuery、Ajaxの基礎
- 対象範囲:公開サンプルの画面状態と、実機へ展開するときの設計境界
- 扱わない範囲:LCD入力処理そのもの、認証、TLS、ハードリアルタイム制御
- 公開サンプル:embedded-web-control-demo。LCD入力は含まず、状態ファイルを模擬装置として使います
同じ数値でも、意味は同じではありません
画面には、要求値、適用中の値、装置が返した適用済み値があります。数値が同じでも、出どころと確定度を混ぜないことが、この回の設計の芯です。
「これから送る値」と「装置が返した値」を混ぜない
ここが、この記事で一番書きたかったところです。
実は公開サンプルの最初の実装には、この記事の主張と真っ向から矛盾するバグがありました。せっかくなので、そのまま残して原因から追います。
最初に書いたコード
function renderStatus(data) {
$('#running').text(data.running ? 'RUNNING' : 'STOPPED');
$('#level').text(data.level);
$('#levelInput').val(data.level); // ← これが問題でした
$('#updatedAt').text(new Date(data.updated_at * 1000).toLocaleString());
}
一見すると親切な処理です。画面を開いた直後に、入力欄へ現在値が入っていてほしい。装置側で値が変わったら、入力欄もそれに追従してほしい。どちらももっともな要求で、この1行はそれを満たします。
何が起きるか
renderStatus()は周期取得のたびに呼ばれます。つまりこの1行は、1秒ごとにユーザーの入力欄を装置の現在値で上書きします。
装置のレベルが88のときに「7」と入力しようとすると、こうなります。
- ユーザーが
7を打つ。入力欄は7 - 1秒以内に周期取得が返る
-
renderStatus()が走り、入力欄が88へ戻る
実際に確認しました。ヘッドレスブラウザで入力欄へ7を入れて2.5秒待つと、値は88に戻っています。
【修正前】2.5秒後の入力欄 = 88
二桁の値を入れようとすると、もっとひどいことになります。「45」の4を打った瞬間にポーリングが走れば、入力欄は88へ戻り、続けて打った5が付いて885になることもあります。打鍵と周期取得のどちらが先かで結果は変わりますが、どのみち利用者の意図した値にはなりません。
なぜ混ざったのか
原因は1行のコードではなく、入力欄が2つの役割を兼ねていたことです。
- 装置が返した値を表示する場所(=出力)
- これから送る値を入力する場所(=入力)
この2つを同じ<input>に載せると、どちらの都合で書き換えるべきかが決まりません。表示として見れば装置の値へ追従すべきですし、入力として見れば人が打った内容を守るべきです。両立しないので、どちらかが壊れます。
第1回で「PHPはHTTPの語彙を装置の語彙へ翻訳する」と書きました。同じことが画面の中でも起きています。値の出どころが違うものを、同じ場所に置いてはいけません。
直したコード
役割を分けます。表示専用の場所を別に用意し、入力欄は「編集されていない間だけ」追従させます。
var levelDirty = false; // 入力欄をユーザーが編集中か
function renderStatus(data) {
$('#running').text(data.running ? 'RUNNING' : 'STOPPED');
$('#level').text(data.level);
$('#updatedAt').text(new Date(data.updated_at * 1000).toLocaleString());
// 入力欄は「これから送る値」です。
// ユーザーが編集を始めたら、周期取得の結果で上書きしません。
if (!levelDirty) {
$('#levelInput').val(data.level);
}
}
$('#levelInput').on('input', function () {
levelDirty = true;
});
levelDirtyを落とすのは、操作要求が成功したときだけです。
}).done(function (data) {
if (!acceptResponse(seq)) {
return; // 応答の採否判定は第2回のとおり
}
// 要求が通ったので、入力欄の編集状態を解除します。
// 以降は装置が返した値で入力欄を追従させます。
levelDirty = false;
renderStatus(data);
/* ... */
});
これで、次の4つが同時に成立します。
- 画面を開いた直後、入力欄には現在値が入っている
- 入力中は、何秒待っても打った内容が消えない
- 適用が成功したら、また装置の値へ追従に戻る
- LCD側など別の経路で値が変わっても、編集していなければ入力欄も追いつく
裏を返すと、levelDirtyが落ちるのは適用が成功したときだけです。打ちかけてやめても、入力欄はひとりでに追従へ戻りません。戻すには、そのまま適用するか、画面を読み込み直すことになります。編集の取り消しを用意するなら、入力欄の横に「現在値へ戻す」を置いて、そこでlevelDirtyを落とすのが素直です。
HTMLのラベルも、役割が読めるように変えました。
<span class="label">出力レベル(装置が返した値)</span>
...
<label for="levelInput">出力レベル(0~100・これから送る値)</label>
コードを直したら、画面の言葉も直します。片方だけでは、次に触る人がまた混ぜます。
LCDとWebから同時に操作されたら
小型装置では、Web UIを開いたまま誰かが装置の前でLCDを操作する、ということが普通に起きます。
公開サンプルにはLCD入力処理を含めていません。ただし、実機のLCD操作も同じC言語の装置制御境界へ通す設計を想定しています。公開サンプルでは、その境界に置く排他をCプログラムの状態ファイルで再現しています。
flock(fd, LOCK_EX);
状態ファイルを開いた後に排他ロックを取ります。実機でWebから来たset-levelとLCDから来たstartを同じ境界へ通せば、操作が重なっても片方が待って順に適用されます。PHPは排他を持ちません。Web UIだけを守っても、LCD経由の操作は素通りしてしまうためです。排他は、すべての操作が必ず通る場所へ置きます。
UI側の設計はここから決まります。同時操作がありうる以上、ブラウザは自分が送った値を正としてはいけません。
- 送った値を画面へ即座に反映して終わりにしない
- 操作の応答を受け取ったあとも、もう一度
statusを取り直す - 表示するのは、あくまで装置が返した値
サンプルのsendCommand()がalways()でrefreshStatus(true)を呼んでいるのは、このためです。自分の操作が最後とは限りません。
操作要求がタイムアウトしたときも、同じように状態を取り直します。HTTP応答を受け取れなかっただけでは、装置へ適用されなかったのか、適用された後の応答だけを受け取れなかったのかを区別できないためです。そのため、画面には「操作に失敗しました」ではなく、「操作結果を確認できませんでした。装置状態を再取得します」と表示します。
確認したこと
修正前後をヘッドレスブラウザで確認しました。
| 確認内容 | 結果 |
|---|---|
修正前に入力欄へ7を入れて2.5秒待つ |
周期取得によって88へ戻る |
修正後に入力欄へ7を入れて2.5秒待つ |
7のまま。周期取得で上書きされない |
| そのまま適用する | 表示値・入力欄とも7へそろう |
別経路でset-level 88を実行する |
編集していなければ、表示値・入力欄とも88へ追従する |
C側は-std=c11 -Wall -Wextra -Wpedantic -O2で警告0、make testがstatus/set-level/start/stopの順に期待どおりのJSONを返すことを確認しています。
確認日は2026-08-06です。環境はLinux(Ubuntu 24.04ベース)、PHP 8.4.21の内蔵サーバ、Chromium 141.0.7390.37のヘッドレスモード、gcc 13.3.0です。
lighttpd+php-cgi構成、組み込み実機(BusyBox中心のrootfs)、jQuery 3.7.1本体は未確認です。ブラウザ側の確認には最小のjQuery互換シムを使っています。
まとめ
- 装置が返した値と、ユーザーがこれから送る値を同じ入力欄へ載せません。
- 入力中は
levelDirtyで周期取得による上書きを止め、適用成功後に追従へ戻します。 - 画面では要求値、適用中、適用済みを、表示場所と文言で区別します。
- LCDとWebの操作を両立させるなら、両方が必ず通るC言語の装置制御境界へ排他を置きます。
- 操作要求のHTTP応答を受け取れなくても、未適用とは断定せず、装置状態を取り直します。
「一貫して見せる」とは、すべてを同じ値へ即座にそろえることではありません。いま見えている値が、誰が入力したものか、装置へ適用済みかを説明できることです。
次回は、PHPからC製コマンドを呼ぶ境界そのものを扱います。操作名のホワイトリスト、HTTPメソッド、値の検証、実行対象の固定、終了コード、都度起動と常駐デーモンの分かれ目へ進みます。
