棄用 MQTT 改採 HTTPS Event-Driven + WSS + Offline-First SD Queue
Context
既有設計
YiBi MVP 既有 codebase 已實作 MQTT 架構:
- 五個 MQTT topic:
heartbeat(30s 週期)、status、command、manifest、ota - EMQX Cloud(Serverless)作為 managed broker,TLS over port 8883
- 後端
backend/src/infrastructure/providers/mqtt/含 client、各類 publisher、heartbeat handler、credential manager、encryption - Go simulator(
simulator/mqtt/)扮演真實 ODM 裝置角色連到 broker - 設計依據:
docs/odm/01-mqtt-protocol-spec.md、docs/features/008-device-control/contracts/mqtt-topics.yaml - 最新 MQTT 協議擴展設計:
docs/openspec/changes/archive/2026-04-23-e05-mqtt-protocol-extension/(含 OTA、content preloading、child lock、diagnostics)
觸發
Editorial Note(2026-06-09):本節提及的「ESP32-S3 N16R8」為 2026-05 早期 ODM 洽談時的候選晶片型號,2026-06-09 確認晶片最終型號尚未決定。後續文件應使用晶片中立術語「device MCU」;本 ADR 原文依 archive immutable 原則不修改,但所有引用本段的新 spec / change 均應替換為抽象需求描述,由 ODM 確認晶片時負責達成具體規格。
2026-05 硬體規格確認(ESP32-S3 N16R8 電池供電、無螢幕)後,做精確的電池壽命估算與架構適配性審查,發現兩項根本性衝突:
-
電池硬體限制:維持 MQTT broker 連線需 WiFi 持續 associated(modem-sleep ~20 mA),2000 mAh 電池僅撐約 4 天——與 YiBi 兒童故事機「數週至月」的目標電池壽命差兩個數量級。
-
Offline-first 架構與 MQTT 設計前提根本不相容:YiBi 裝置大部分時間在 deep sleep(~10 μA),多數 NFC 觸發應盡量不接觸雲端(見新架構 wake decision tree)。MQTT 的核心價值假設「裝置持續在線」,在 offline-first 裝置上,broker 的所有保障都無效——可靠性必須做在 device 端 SD queue。
同期,產品團隊確認(2026-05-25):家長 app 與內部管理介面都不需要即時在線狀態,進一步讓 heartbeat 與 LWT 的存在失去業務意義。
既有 MQTT 四項賣點評估
| MQTT 賣點 | 在 YiBi 是否成立 | 替代方案 |
|---|---|---|
| 低功耗長連接 | ❌ ESP32-S3 電池上不可達 | Event-driven HTTPS + offline-first + daily safety-net |
| 雙向非同步 push(idle) | ❌ MQTT 也做不到(硬體限制:deep sleep 中 WiFi 關閉) | POST /events response 中的 pending_actions(下次 wake 時送達) |
| 雙向非同步 push(active session) | ✓ MQTT 可做 | WSS during live-chat session(對等) |
| QoS 1(device→cloud) | ✓ MQTT 可做 | SD queue + fsync + client_event_id idempotency(更可靠:離線方就是 device) |
| QoS 1(cloud→device) | ✓ MQTT 可做,但語義錯位 | config_version 比對 + GET /config state-sync(正確語義) |
| LWT 離線偵測 | ❌ 30s heartbeat 在電池上不可達,產品也不需要 | last_event_at 推導 + daily safety-net |
整體判定:MQTT 在 YiBi 場景沒有獨佔的能力,且 heartbeat 與 LWT 在電池硬體限制下從來就無法達成既有規格宣稱的目標。
Decision
結論:棄用 MQTT,改採 HTTPS Event-Driven + WSS(僅 live-chat session)+ Offline-First SD Queue,不部署任何 MQTT broker。
新通訊架構
ESP32-S3 (C firmware)
│
├── HTTPS Client (mbedTLS) ─────────────────→ Cloud Run yibi-api (Python)
│ ├── POST /events ← 上傳 SD queue 事件批次;response 含 pending_actions
│ ├── GET /config ← 版本不一致時取完整 config snapshot
│ ├── GET /manifest ← 每次接觸雲端的 wake 檢查 OTA 與 content pack
│ └── GET /safety_net/slot ← 取得每日喚醒 slot(通常 piggybacked on /events)
│
├── HTTPS Range GET ─────────────────────→ GCS(音訊檔,signed URL)
│
└── WSS Client (mbedTLS,僅 live-chat NFC 觸發時開啟)
└── /live_chat ─────────────────────→ Cloud Run yibi-live-chat
└──────── WSS ──────────────────→ Gemini Live API關鍵設計決策
Offline-first SD Queue
- 所有事件在發生當下寫入 SD card
/queue/pending.ndjson並 fsync(斷電存活) - 每筆事件含
client_event_id(UUIDv4),後端以此為 Firestore doc ID 做 idempotent 寫入 - flush trigger 分層:pre-sleep 強制同步、高優先事件立即背景、size/時長/電量門檻觸發背景
- 離線期間 queue 保留;下次自然 wake 重試
Wake Decision Tree(NFC persona 觸發)
- 音訊已快取 AND last_checkin < 24h AND config 未過期(7 天)AND 無 pending actions → 不接觸雲端,直接播放,事件寫本地 queue
- 否則 → 接觸雲端(POST /events)
- 重度使用場景:30 次 NFC 觸發/day,僅 ~3 次有雲端流量(~3 mAh/day vs MQTT 持續連線 ~480 mAh/day)
Wake Sources(裝置喚醒來源)
後續補充(2026-06-09,E19 spectra-discuss):本 ADR 原始設計只涵蓋兩種 wake source;E19 Sleep & Rise Routine 引入第三種。完整三種 wake source:
# Wake Source 觸發條件 說明 1 NFC scan 孩子放置 persona(公仔/卡片/硬幣) Wake Decision Tree 的主要 trigger 2 Daily safety-net Server hash(device_id) % slot_count 排程,02:00–04:00 裝置本地時間 取代 heartbeat;每日最少一次完整 sync 3 Scheduled wake(RTC alarm) E19 起床時間整點,device 本地 RTC alarm 觸發 與雲端無關;觸發 Wake Flow;保障 ±1 分鐘精度
Cloud→Device Push(pending_actions)
- POST /events response 下發,共 14 種類型(config 同步、content 管理、session、OTA、訂閱/配額、診斷/維運、電源管理)
- priority:HIGH(中斷其他動作)/ NORMAL(本 wake cycle 結束前)/ LOW(下次 wake 重試)
- Idle 裝置的「指令」改用 state convergence 語義:最新 pending action 為準,broker 暫存概念不適用
Daily Safety-net Wake(取代 heartbeat + LWT)
- Server 以 hash(device_id) % slot_count 將 02:00–04:00 裝置本地時間均勻分散,每日輪換
- 保證每天至少一次完整 sync:events upload + config check + manifest check
- fleet 健康監控改為 server-side daily job 掃描 last_event_at 異常裝置
Live-chat(WSS,唯一長連線)
- 獨立 NFC 卡觸發,session 邊界明確且家長可控
- 強制 flush 既有 queue → 取得 open_wss_live_chat pending action(含 session_token)→ 開啟 WSS
- Inactivity timeout 300s / Hard cap 1800s(30 分鐘)
- Session 期間雲端可隨時 push;session 結束即關閉 WSS,回 sleep
認證
- Phase 1:JWT(15 分鐘),HMAC-signed request 取得
- Phase 2 考量:device certificate + mTLS(不在本次範圍)
Broker 決策
不部署任何 MQTT broker:
- 沒有沉沒成本(broker 部署方式尚未決定)
- GCP IoT Core 已於 2023-08 終止服務;自管 EMQX / HiveMQ Cloud 都增加 ops 成本(3 人技術團隊 net negative)
- Cloud Run + Firestore + GCS 既有 stack 直接擴用
Alternatives Considered
-
方案 A:維持 MQTT + EMQX Cloud — ESP32-S3 持續連線電池僅撐 ~4 天;LWT 30s/90s 在電池上不可達;offline-first 下 broker 提供零附加價值。否決。
-
方案 B:MQTT + HTTPS 雙協議過渡並存 — 韌體需同時維護兩套 TLS 連線路徑,增加 bundle size 與複雜度;MQTT 路徑最終必須移除,過渡期無中間業務價值。否決。
-
方案 C:純 HTTP 短輪詢 — 不需要 WSS 的 live-chat 雙向 streaming;輪詢頻率若高到有用則耗電,若低則延遲不可接受。否決(live-chat 使用 WSS 才能支援 Gemini Live streaming)。
-
方案 D:自管 MQTT broker(EMQX on GCE/GKE 或 HiveMQ Cloud) — 3 人技術團隊需自行維護 HA、監控、憑證輪換;GCP IoT Core 2023-08 終止後托管選項受限;即使解決 broker ops,電池硬體限制仍使 broker 對 YiBi 場景零附加價值(問題在裝置端,不在雲端)。否決。
Consequences
好:
- 電池壽命估算從 ~4 天(MQTT 持續連線)提升至 ~3 mAh/day(HTTPS offline-first),可達成數週至月目標
- 不需自管 broker,移除整個 broker infrastructure 層的 ops 負擔(no HA、no monitoring、no cert rotation for broker)
- 韌體只用一套 mbedTLS(HTTPS + WSS 共用),無額外 TLS cost
- Cloud Run + Firestore + GCS 既有 stack 直接擴用,auth/log/observability 共用基礎設施
- Stateless REST 後端完美 fit Cloud Run 的 request model(可水平擴展)
- client_event_id idempotency 讓所有 retry 天然安全
壞 / 代償:
- Idle 期間失去「雲端主動推送」:產品團隊已確認不需要,但未來若需求出現需要評估 revisit 條件
- OTA mandatory 場景最壞延遲 = daily safety-net slot(當日凌晨):非即時,但 mandatory urgency 有「下次 idle wake 即安裝」的保障
- 韌體工程量增加:SD queue + fsync + 背景 flush task + wake decision tree,比直接用 MQTT client lib 複雜;但這是「正確解決了電池問題」的必要成本
- 8 個既有 spec 需更新:E09-device-control / E11-device-token / ota-update-protocol / enhanced-diagnostics / content-preloading / child-lock-control / voice-session-lifecycle / odm-delivery-docs 中的 MQTT-only requirements,留待 spectra change 0022 標 REMOVED/MODIFIED/ADDED
中性:
- 業務語義完整保留:heartbeat / status / command / manifest / ota 五項功能全部有對等替代,只是傳輸層改變
- FSM 狀態定義不變;ACK/NACK 概念不變(換成 HTTP status + ingested_event_ids + action_acks)
- Firestore canonical state 設計不變
詞彙漂移:
本 ADR 引入新 canonical 詞彙(offline-first、safety-net wake、pending action、wake decision tree、flush trigger、client_event_id、session summary、engagement_score_local),並讓既有詞退場(在線狀態、heartbeat 30s 週期、LWT、MQTT topic、broker)。完整詞彙清單見 docs/openspec/LANGUAGE.md(與本 ADR 同 PR 建立)。
Revisit 條件(需 product/business case 驅動,不接受「以防萬一」):
- (a) YiBi 之外的硬體產品線需要共用通訊基礎設施
- (b) 單裝置出貨 > 100K 且雲端→裝置即時推送成為核心 UX
- (c) 裝置硬體升級到支援 always-on 連線(如加入 NB-IoT modem)
- (d) 加入家長即時遠端控制 UX 且裝置必須 idle 時也接收推送
Revisit (d) 評估紀錄(E19 場景,2026-06-09):E19 Sleep & Rise Routine 要求「家長修改 sleep schedule 後裝置盡快生效」,表面上符合 Revisit 條件 (d)。評估後決定不觸發 revisit,理由:
- E19 PRD 接受「設定已儲存,下次 YiBi 連網使用時生效」文案,PM 預期為 best-effort,非 ≤30 秒強制
- 「立即同步」UX 採教育引導路徑(路徑 A):App 引導家長手動觸發 NFC 或按鍵;home WiFi keep-alive(路徑 C)或 low-power push(路徑 D)均需引入持續連線機制,與 offline-first 模型根本衝突
- E19 睡眠時段(Deep Sleep with Locked-Sleep)最壞延遲為當日 daily safety-net wake(02:00–04:00),PM 評估可接受
- 結論:offline-first 模型不需修改;config sync 補強方向為 device→cloud ack 回流(
current_config_version),讓 App 顯示三狀態 badge(synced / pending / stale_>24h),見 LANGUAGE.md §Config Sync 狀態