YiBi Dev Docs
架構決策 (ADR)

棄用 MQTT 改採 HTTPS Event-Driven + WSS + Offline-First SD Queue

Context

既有設計

YiBi MVP 既有 codebase 已實作 MQTT 架構:

  • 五個 MQTT topic:heartbeat(30s 週期)、statuscommandmanifestota
  • 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.mddocs/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 電池供電、無螢幕)後,做精確的電池壽命估算與架構適配性審查,發現兩項根本性衝突:

  1. 電池硬體限制:維持 MQTT broker 連線需 WiFi 持續 associated(modem-sleep ~20 mA),2000 mAh 電池僅撐約 4 天——與 YiBi 兒童故事機「數週至月」的目標電池壽命差兩個數量級。

  2. 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觸發條件說明
1NFC scan孩子放置 persona(公仔/卡片/硬幣)Wake Decision Tree 的主要 trigger
2Daily safety-netServer hash(device_id) % slot_count 排程,02:00–04:00 裝置本地時間取代 heartbeat;每日最少一次完整 sync
3Scheduled 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 狀態

On this page