iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
自我挑戰組

一塊板子,兩個腦袋:Arduino UNO Q 30 天實戰系列 第 29

Day 29|解決現場與網頁各說各話

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260828/20058745TgbhhnHQDR.jpg

六條狀態規則

最終系統固定遵守以下規則。

  1. ledState 只由 MCU 保存。
  2. D9 與 Web 都呼叫 toggle_led()
  3. Python 不建立可自行修改的 LED 狀態。
  4. JavaScript 點擊後不先切換畫面。
  5. MCU 完成輸出後才回報 led_status
  6. Web 只顯示 MCU 回報的 led_update

這些規則的目的,是將操作入口與真實狀態分開,入口可以有兩個,但答案只能有一個。

若 Web 點擊時先反轉畫面,或 Python 自己保存一份狀態,看似能讓介面反應更快,實際上只要 Bridge 呼叫失敗、現場按鈕插入操作或訊息延遲,上層答案就可能與 LED 分離,因此最終版本寧可等待回報,也不預測結果。

MCU 最終責任

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 最終責任

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 的事實,若將兩者混在同一個函式中自行改狀態,會讓命令與結果的責任再次模糊。

Web 最終責任

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 只提供 statustoggle-btn 元素,CSS 只定義 on 與 off 的外觀,最後由 JavaScript 根據回報選擇顯示,三個前端檔案的責任因此保持分離。

快速交替操作測試

先用 Web 切換一次,等待畫面更新,再按 D9 一次,接著以正常速度交替操作十次,LED、Python Console 與所有瀏覽器應始終顯示相同狀態。

若畫面偶爾相反,先搜尋是否在 JavaScript click 中直接修改文字,或 Python 是否另外保存狀態,若一次操作產生多筆回報,則檢查 Bridge.call("led_status") 是否只存在共同切換入口。

事件名稱 toggle_ledled_statusled_update 與資料欄位 status 也必須固定,名稱不一致會讓某一段資料流中斷。

測試時可刻意觀察每次切換的順序,先出現實體 LED 變化,再看到 Python 狀態,最後更新 Web,若畫面先變或 Python 出現沒有對應硬體動作的訊息,就表示系統中仍存在繞過 MCU 的路徑。

最終版本還要保留離線能力

完成 Web 功能後,停止 Python 再按下 D9,D2 仍必須正常切換,這項測試確認 Bridge 與回報只是附加能力,沒有讓 MCU 依賴上層服務,重新啟動完整 App 後,後續操作則再次同步到頁面。

最終檢查還要搜尋專案中所有 digitalWrite()ledState 與狀態文字修改位置,除了 MCU 初始化與共同切換函式外,不應再有其他地方直接決定燈號,JavaScript 也只能在 led_update listener 中更新畫面,這項程式碼檢視能補足操作測試不容易發現的隱藏重複來源。

最終版本不得再有任何旁路控制。

Edge Box 3.0 完成

  • 現場與 Web 共用 toggle_led()
  • MCU 保存唯一 ledState
  • Python 只負責命令與狀態轉送。
  • Web 等待 MCU 回報後更新。
  • 現場與遠端交替操作仍保持一致。

現在智慧路口 Edge Box 3.0 的功能已完成,最後一天不再加程式,下一篇要用現場、遠端、交替操作與檔案責任做最終驗收。

下一篇|最終驗收 Edge Box 3.0


上一篇
Day 28|從現場串起遠端控制完整路徑
下一篇
Day 30|最終驗收 Edge Box 3.0
系列文
一塊板子,兩個腦袋:Arduino UNO Q 30 天實戰30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言