
最終系統固定遵守以下規則。
ledState 只由 MCU 保存。toggle_led()。led_status。led_update。這些規則的目的,是將操作入口與真實狀態分開,入口可以有兩個,但答案只能有一個。
若 Web 點擊時先反轉畫面,或 Python 自己保存一份狀態,看似能讓介面反應更快,實際上只要 Bridge 呼叫失敗、現場按鈕插入操作或訊息延遲,上層答案就可能與 LED 分離,因此最終版本寧可等待回報,也不預測結果。
sketch/sketch.ino 負責現場 I/O、去彈跳、狀態與 Bridge,核心程式如下。
// 本日新增
#include <Arduino_RouterBridge.h>
const int LED_PIN = 2;
const int BTN_PIN = 9;
bool ledState = false;
bool lastBtn = HIGH;
void toggle_led() {
ledState = !ledState;
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
Bridge.call("led_status", ledState);
}
void on_toggle_led() {
toggle_led();
}
void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BTN_PIN, INPUT_PULLUP);
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
Bridge.begin();
Bridge.provide("toggle_led", on_toggle_led);
}
void loop() {
bool btn = digitalRead(BTN_PIN);
if (lastBtn == HIGH && btn == LOW) {
toggle_led();
delay(50);
}
lastBtn = btn;
}
所有切換都進入 toggle_led(),更新硬體後才回報,MCU 因此是唯一真實狀態來源。
python/main.py 只接收 Web 命令、呼叫 MCU、接收 MCU 回報並推送畫面。
# 本日新增
from arduino.app_utils import App, Bridge
from arduino.app_bricks.web_ui import WebUI
ui = WebUI()
def on_led_status(state: bool):
label = "ON" if state else "OFF"
print("LED 狀態:", label)
ui.send_message("led_update", {"status": label})
def on_toggle_led(client_id, data):
Bridge.call("toggle_led")
Bridge.provide("led_status", on_led_status)
ui.on_message("toggle_led", on_toggle_led)
App.run()
Python 沒有 ledState,也不在收到 Web 點擊時自行反轉 ON/OFF,它只是讓命令向下、結果向上流動。
on_toggle_led() 與 on_led_status() 分別代表兩個方向,前者把要求送到 MCU,後者接收 MCU 的事實,若將兩者混在同一個函式中自行改狀態,會讓命令與結果的責任再次模糊。
JavaScript 只送出事件與更新 DOM。
// 本日新增
const socket = io();
const statusEl = document.getElementById("status");
const toggleBtn = document.getElementById("toggle-btn");
socket.on("led_update", (data) => {
statusEl.textContent = data.status;
statusEl.className = data.status === "ON" ? "on" : "off";
});
toggleBtn.addEventListener("click", () => {
socket.emit("toggle_led", {});
});
點擊 callback 不修改 statusEl,只有 led_update callback 可以改變文字與 class,因此畫面不會比 MCU 更早宣布結果。
HTML 與 CSS 也沒有保存狀態的能力,HTML 只提供 status 與 toggle-btn 元素,CSS 只定義 on 與 off 的外觀,最後由 JavaScript 根據回報選擇顯示,三個前端檔案的責任因此保持分離。
先用 Web 切換一次,等待畫面更新,再按 D9 一次,接著以正常速度交替操作十次,LED、Python Console 與所有瀏覽器應始終顯示相同狀態。
若畫面偶爾相反,先搜尋是否在 JavaScript click 中直接修改文字,或 Python 是否另外保存狀態,若一次操作產生多筆回報,則檢查 Bridge.call("led_status") 是否只存在共同切換入口。
事件名稱 toggle_led、led_status、led_update 與資料欄位 status 也必須固定,名稱不一致會讓某一段資料流中斷。
測試時可刻意觀察每次切換的順序,先出現實體 LED 變化,再看到 Python 狀態,最後更新 Web,若畫面先變或 Python 出現沒有對應硬體動作的訊息,就表示系統中仍存在繞過 MCU 的路徑。
最終版本還要保留離線能力
完成 Web 功能後,停止 Python 再按下 D9,D2 仍必須正常切換,這項測試確認 Bridge 與回報只是附加能力,沒有讓 MCU 依賴上層服務,重新啟動完整 App 後,後續操作則再次同步到頁面。
最終檢查還要搜尋專案中所有 digitalWrite()、ledState 與狀態文字修改位置,除了 MCU 初始化與共同切換函式外,不應再有其他地方直接決定燈號,JavaScript 也只能在 led_update listener 中更新畫面,這項程式碼檢視能補足操作測試不容易發現的隱藏重複來源。
最終版本不得再有任何旁路控制。
toggle_led()。ledState。現在智慧路口 Edge Box 3.0 的功能已完成,最後一天不再加程式,下一篇要用現場、遠端、交替操作與檔案責任做最終驗收。
下一篇|最終驗收 Edge Box 3.0