創刊号に「インストールなしのアプリの時代」を載せた。十分の修正がユーザーの画面に届くまでに何が起きるか — ビルド、審査、配布、そしてユーザーが更新を押すまでの待ち — その鎖を描いた記事だった。
次号に「なぜ我々はコンパイルを捨てたか」を載せた。その鎖の最初の環を物性として読み直した記事だった。コンパイルとは、その瞬間のすべての決定をひとつの硬い塊に凍らせて入れる仕事だという一文が、この雑誌が六か月で書いたもののうち最も良い概念の圧縮だった。
二本は同じ尾根の表と裏だった。ひとつは鎖が消えれば何が変わるか、もうひとつはなぜそもそもそうだったか。だから系譜も、迂回の試み三種も、反論の問答も二度ずつ立てた。今回ひとつにまとめる。
そしてまとめるにあたり、直すものがもうひとつある。二本とも数字なしに数字の話をした。
「画面定義は小さい。重いコードバンドルではなく、『ボタンひとつ、テキストひとつ』という軽い仕様だ。」
小さいと書いたが、何バイトかを測らなかった。「一方は三日かかり一方は十秒」と書いたが、十秒を測らなかった。あのときは測るものがなかった。今はある。
氷 — コンパイルがやること
まず生かすものから。コンパイルを物性として読んだあの箇所はそのまま行く。
アプリを作るということは、その瞬間のすべての決定をひとつの塊に凍らせる仕事だった。ボタンがどこにあるか、何色か、押したら何が起きるか — それらの決定がコンパイルを経て端末の中で固まる。固まった後は一文字も変えられない。変えるには再び凍らせ、再び審査を受け、再び配布して、ユーザーが受け取ってくれるのを待たなければならない。
配布の痛みは怠慢や古い道具のせいではなかった。画面という決定が端末の中に凍っているという構造の必然だった。
ここで問いがひとつ立つ。元の記事が定式化したあの問いだ。能力と氷は本当に分離できないのか? ネイティブの能力を使うには必ず凍らせなければならないのか、それとも画面の層だけを溶かし、能力の層は凍ったまま置けるのか。
六か月かけてそれを実際に作ってみた。答えから書く。分離できる。そして凍らせたままにしておくべきものが思ったより多い。
定義はどれだけ小さいか — 実測
元の記事の最も弱い一文から返す。「定義は小さい」に数字がなかった。
今は実際に測れる。以下はすべて動いているものから受け取った大きさだ。
| 何 | 大きさ |
|---|---|
STM32H723ボードが渡した ui://app(ページひとつ) | 656 B |
ESP32が渡した ui://app(ルートを持つアプリケーション) | 158 B |
そのアプリケーションの最初の画面 ui://page/main | 1,894 B |
| 店舗サーバーのキオスク画面 | 1,241 B |
| 同じサーバーのPOS画面 | 1,170 B |
| 同じサーバーの厨房画面 | 1,144 B |
| 無人店舗バンドル全体(JSON 4つ) | 約 6 KB |
一画面が1〜2キロバイトだ。 小さいという言葉は当たっていた。ただしあのときはそれが当たっているか知らずに書き、今は分かっている。
数字ができると、元の記事ができなかった話がひとつ付く。店舗サーバーが三画面をそれぞれ渡すが、その三つの合計が3,555バイトだ。旧来のやり方ならタブレットアプリ・POSアプリ・厨房アプリ三つを作り、それぞれ審査を受け、それぞれ配布したはずの場所だ。三つのアプリが3.5キロバイトに置き換わったのではなく、三つのアプリがあるべき理由がなくなったのだ。
(測定の出典: チップひとつに繋げば、繋いだ端末が端末になる · ボードが自分の画面を渡す · フォルダひとつがアプリだ)
十分の修正はどれだけかかるか — 実測
元の記事が描いた鎖の反対側だ。凍らせなければ本当に即座に届くのか。
無人店舗アプリの画面JSONを直して回し直した。ログだ。
[+ 1415ms] edited ui/pages/main.json — 2940 B -> 2934 B, no compiler ran
[+ 1419ms] route "/" -> ui://pages/main ("Unmanned Store")
[+ 1498ms] captured 04_edited_json.png
編集から画面反映まで83ミリ秒、コンパイラは走らなかった。 ルート移動は4ミリ秒だった。画面遷移がファイル参照だからだ。
「十秒」と書いた場所に、いま実際の数字が入る。そしてここで元の記事が見えていなかったことがひとつ露わになる — 83ミリ秒はビルドがないからではなく、配布がないからだ。 ビルドが0.1秒に縮んだとしても、審査と配布とユーザーの更新受諾が残っていれば鎖はそのままだ。溶けるのはコンパイルではなく鎖全体だ。
何が今も凍っていなければならないか
ここが二本のうち「コンパイルを捨てたか」が持つ最良の節であり、実際に作ってみてその節は正しかった。ただし元の記事はそれも記述だけで書いた。
リアルタイムループは凍ったまま残る。 LCDパネルのサンプルで温度制御ループはCファームウェアの中にある。
/* First-order thermal: dT/dt = k·(target − T). Converges to target
* with a little sensor noise. */
static void thermal_update(void) {
/* ... */
double alpha = 1.0 - exp(-k * dt);
g_temp += (g_target - g_temp) * alpha;
}
これは画面定義に降りてこられない。ミリ秒単位で回るものがネットワークの向こうにあってはならないからだ。
安全範囲も凍ったまま残る。 そしてこちらはもっと強い — 上から何を命じられても、ハードウェアが自分を超えない。
/* The relay clamps its own range. Whatever the rule upstream
* decided, the hardware still refuses to exceed itself. */
if (v < 0) v = 0;
if (v > 100) v = 100;
元の記事は「リアルタイム・ドライバ・セキュリティは凍ったまま、画面の層だけを液体に」と書いた。作ってみるとその一覧に一行足す必要があった。自分の限界を自分で守ること。 規則が溶けているということは規則が間違いうるということであり、そのとき最後に掴むものは凍っている側でなければならない。
決済承認の経路も凍ったまま残る。 カード端末の保安領域は開けない。開けられないからではなく開けない方が良いからで、その理由は出さないものがアプリを守るに別途書いた。
(ファームウェアの境界はLCDメーカーがHMIを30分で、リレーのクランプはセンサーを差し替えても制御はそのまま)
規約の名前 — 二本が最後まで呼ばなかったもの
二本ともこの規約を最後まで無名のままにした。「約束された言語」「開かれた共通言語」。開かれていなければ成り立たないと主張する記事が、その規約の指示対象をぼかせば、読者は検証しに行く先がない。
名前はMCP(Model Context Protocol)だ。そしてこのシリーズのすべてのサンプルがそれで話す。実際のバイトはこう見える。
=> {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18", ...}}
<= {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26",
"capabilities":{"tools":{},"resources":{}},
"serverInfo":{"name":"WeAct H723 MCP Node","version":"0.1.0"}}}
これは実際のSTM32ボードがUSBシリアルで答えた行だ。そして開かれているという主張にも今は根拠がある — 同じクライアントコードでSTM32(UART)とESP32(Wi-Fi TCP)に繋がった。 二つのボードは互いを知らず、別会社のチップで、転送が違う。それでも同じ規約を使うから通じた。
元の記事が「ブラウザがどの会社のサーバーのHTMLでも描き出す原理と正確に同じだ」と書いたあの一文は、いまや比喩ではなく二つのボードのログだ。
反論にもう一度答える — 今回は数字で
二本がそれぞれ五つの問答を立て、四つが重なった。一度だけ立て、答えられるようになったものから直して答える。
「画面ごとにサーバーから受け取れば遅くないか?」— 画面定義は1〜2 KBだ(上の表)。そして往復はリンク次第だ。ある実行でUART側の道具の往復が1〜11 ms、Wi-Fi TCP側が21〜69 ms、接続から最初の画面までが46 ms対189 msだった。だから正確な答えはこうだ — 遅くない、ではなく、リンクによって桁が違い、その差がUI設計を分ける。 一桁の側は「押せばすぐ」を期待してよく、数十ミリ秒の側は待ち状態を画面に描かなければならない。元の記事はこの分岐を知らなかった。
ここでひとつ付け加える。時間の値は再現されない。 大きさはファイルなので何度測っても同じだが、遅延は実行ごとに揺れ、ある実行ではWi-Fi側が上の値の倍を超えた。だからこの記事は正確なミリ秒ではなく桁だけを主張する。
「オフラインでは?」— これはまだ返せない。元の記事は「定義はキャッシュされる」と書いたが、このシリーズのどのサンプルもキャッシュとオフライン動作を試していない。 再接続の動作も測っていない。未検証として残す。
「セキュリティは?」— ランタイムが定義を信用せず、定められた組み合わせだけを許すという元の答えは有効だ。そこに作ってみて分かったことを足す。境界はひとつではなく三つで(受ける側のランタイム・出す側の設計・自分の限界を守るハードウェア)、道具一覧にない能力は人が押してもモデルが選んでも届かない。実証は別の記事にある。ただし「だから安全だ」はこのシリーズが証明していない — 認証・権限・転送のセキュリティはすべて外だ。
「アプリストア方針は?」— 元の記事の答えをそのまま置く(境界は任意のネイティブコードを植えるかどうかにある)。そして元の記事が自ら付けた但し書きもそのまま置く — これは方針の解釈であって法律助言ではない。 ここにひとつ足す。二本ともに根拠となるガイドラインを一行も引用しなかった。引用のない解釈は解釈ではなく推測なので、この項目は根拠を付けるまで未解決と表示する。
「サーバー費用は?」— 元の答えを維持。ただしこのシリーズが作ったサーバーはすべてローカルプロセスなので、運用コストを測る位置になかった。
このサンプルがやっていないこと
元の二本の誠実の節は維持する。コンパイルは消えるのではなく場所を移し、既存システムは一晩で変身せず、デザインという労働は残る。作ってみて三つとも当たっていた。
ここに今回分かったことを足す。
二本の思考実験は思考実験だった。 店舗機器の場面も、創刊号の場面も「〜としよう」で始まる想像なのに、具体的に描かれて観測のように読めた。この記事が引用したものはすべて実行ログと実測であり、それぞれどの記事から来たかをリンクで付けた。想像と観測を同じ書体で書かない。
測定は狭い。 上の数字は一台のMac、一台のルーター、ひと部屋の中で、大半が一度ずつ測った値だ。標本が少なく環境がひとつだ。大きさの値は決定的だが(ファイルがそれだけある)遅延の値は実行ごとに変わる — この記事を書いている間にも、先に引用した遅延の数値が再実行で変わり、掲載したログと合わせるために一度直した。時間を引用する記事は、そのログとともに老いる。
三つはまだ返せていない。 オフライン・キャッシュ動作、アプリストア方針の根拠引用、そして運用コスト。元の記事が答えた場所に今も実測がない。
だから、もう一度疑え
元の記事はそう閉じた。疑え、次の記事たちが実物で見せると。
六か月が過ぎ、その次の記事たちが出た。この記事が引用した数字はすべてそれらから出たものであり、各編にはビルドログと実行ログとヘッドレスで撮った画面が付いている。再現したければフォルダを持って行って回せばいい。
そして今も疑う場所を残しておく。オフラインは測っていないし、方針は根拠を付けていないし、費用は測る位置になかった。 その三つはまだあのときと同じ状態だ。
画面の層は溶けた。それは今や656バイトと83ミリ秒で言える。何が凍っていなければならないかも分かり、その一覧は最初に思ったより長かった。残ったのはまだ測っていないものであり、それは測ってから書く。
makemind.dev 「探索」— あのとき測らずに書いた数字を実測に置き換えた。