臨時內容即時下發機制(on-demand content push)— offline-first 下的低延遲下發選項
ACCEPTED(2026-06-23,deciders: howie,經
/spectra-discuss):採方案 C(adaptive pull) 為現行方向(change0034-on-demand-adaptive-pull);B(BLE 旁路)列為後續唯一 push 候選, 啟動前須與晶片廠商討論並完成電池 spike。見 Decision 段。本 ADR 同時更新了 LANGUAGE.md 「雲端不主動 push」立場的適用邊界(見 Context vocabulary note 與下方 Consequences)。
Context
問題:offline-first 下臨時內容的下發延遲
ADR-0010 將裝置通訊改為 HTTPS event-driven + offline-first:裝置長時間閒置時停留於
淺睡眠(Light Sleep,感應模式)——NFC 低工作週期輪詢、WiFi off、主 CPU light-sleep;
這是 NFC 可觸發的待機狀態,其基底電流為待機壽命的最大項(見
docs/odm/06-power-state-machine.html 狀態2)。Deep Sleep(<20 μA)僅由手動長按進入、
NFC 全關、只有電源鍵與 RTC 鬧鐘可喚醒,並非日常閒置狀態。裝置只在 wake cycle
(NFC scan / 充電 / RTC safety-net)接觸雲端。雲端→裝置的唯一指令通道是
POST /events response 帶回的 pending_actions(純 pull,無 push;見
docs/odm/01-device-communication-spec.md §3.1)。
這帶來一個固有延遲缺口:
- 小孩用 NFC 觸發、正在聽已快取的故事時,裝置不會中途回頭 poll。
- 此時爸媽臨時從 App 產生一則新故事,後端只能把它排成一個
content_refresh/content_preloadpending action 靜靜等著。 - 由於後端無任何 push 能力,該 action 要到裝置「下一次接觸雲端」才會被取走。
下一次接觸雲端的時間,取下列最早發生者:
| 觸發路徑 | 何時 | 延遲 |
|---|---|---|
| 每日 safety-net wake(每天固定 UTC slot,±15 分鐘) | 每天 1 次 | 保證上限 ~24h(最差約 24h15m) |
| 下一次符合 wake decision tree 條件的 NFC scan | 不確定 | 通常遠快於 24h,但無保證 |
若小孩接下來放的都是已快取內容、且 last_checkin_at 未滿 24h、config 未過期、
NVS 無 pending action,則每次 NFC scan 都直接放快取、完全不連雲端,新故事
要到隔天 safety-net slot 才會被發現。保證最壞延遲 ≈ 24 小時。
限制:為何「真 push」對這台裝置特別難
裝置是 WiFi-only,且在任一睡眠狀態(淺睡眠 / Deep Sleep / Locked-Sleep)WiFi 射頻 皆完全關閉(見 power-state spec),封包進不來。 手機能收 APNS/FCM 是因為蜂巢式 modem 永遠在低功耗 paging 模式聽基地台; WiFi-only 沉睡裝置沒有 FCM 的等價物。任何 server push 都必須先解決 「如何叫醒一個射頻關閉的裝置」,而這正是 ADR-0010 為了電池壽命刻意排除的方向 (MQTT 常連線 modem-sleep ~20 mA → 2000 mAh 約 4 天,達不到「數週至月」目標)。
這是在重啟一個已固化的立場(vocabulary drift)
「雲端不主動 push」目前已寫進專案共享詞彙,本 ADR 等於回頭挑戰它,必須誠實標註:
docs/openspec/LANGUAGE.md把"config push"列為avoid:「裝置為 Device-initiated,雲端不主動 push」。- 同檔 Scheduled wake 條目亦
avoid 雲端 push:「雲端 push 網路 worst case 無法保證精度;Scheduled wake 完全相容 offline-first 模型」。
Vocabulary note:若本 ADR 最終採納任何形式的 push,須同步更新 LANGUAGE.md 的
"config push" / 雲端 push條目(從「一律 avoid」改為「限定情境下允許」), 否則文件與行為會再次漂移。
Decision
裁決(2026-06-23,deciders: howie,經 /spectra-discuss):採方案 C(adaptive pull)為現行方向;
B(BLE 旁路)列為後續唯一 push 候選;A 不採為後續路徑;D 否決。
-
現在做 C(adaptive pull):純 pull、零架構風險、相容 offline-first,把保證最壞延遲 ~24h 壓到 ~1–2h。已形式化為 change
0034-on-demand-adaptive-pull(proposal/design/specs/ tasks/testplan 齊備、spectra validate通過)。後續以/spectra-apply實作。 -
B(BLE 旁路)為後續唯一 push 候選:當產品確認需要「battery 待機也能近即時下發」時走 B (尤其 B2 裝置常駐 BLE advertising),因為只有 B2 能在不充電、不常開 WiFi 下被觸達——A 的 WSS 僅充電時可開,做不到。A 不作為後續 push 路徑保留。
-
B 啟動的前置條件(go/no-go gate,全部達成才進
/spectra-propose):- 須與晶片廠商討論:RTL8730EAH(或最終選定 SoC)維持 BLE advertising 的實際平均電流、 advertiser-only 角色可行性、BLE stack / 配對的韌體成本——這些須由 ODM / 晶片廠商確認,不能只憑估算。
- 完成電池 spike 實測(淺睡眠基底 + BLE advertising + WiFi 偶發喚醒的合成待機電流)。
- 產品定出可接受的最壞下發延遲與「分層體驗(充電即時 / 待機延遲)」是否可接受。
- B2 要擺脫「手機在場」假設需專用家用閘道——須先決定是否納入硬體路線。
-
D(維持現狀)否決:~24h 體驗對「爸媽臨時做故事」不可接受。
詞彙影響:C 仍是 pull,
config push/雲端 push的avoid立場不變;但 C 重新定義了Safety-net wake(LANGUAGE.md 現為「02:00–04:00 本地單次」→ C 改「日間多 slot」), 0034 apply 落地時須同步更新 LANGUAGE.md 該條目。未來若實際採 B,再翻config push條目。
關鍵守則:不要為了 push 把整台機器拉回常連線——那會直接推翻 ADR-0010 的 電池決策。任何 push 都應綁定充電狀態,或走 deep-sleep 仍醒著的旁路。
Alternatives Considered
方案 C:Adaptive pull(不做 push,把 pull 變聰明)— 建議優先
零架構風險、最快落地,把最壞延遲從 ~24h 壓到分鐘~小時級:
- adaptive safety-net:每天 1 次 → 清醒時段(如本地 7–21 時)每 1–2h 一次, 半夜不醒。延遲降到 ~1–2h,電池僅多花少量。
- 調小
content_expiry_days:讓 NFC wake decision tree 條件 3 更易命中, 放卡時順便連雲端(0= 每次 NFC 都連,最即時但最耗電)。 - 產品流程對齊:讓「臨時故事」對應一張未快取的新內容 → 命中 decision tree 條件 1,放卡即拉,接近即時。
- 代償:仍非真即時;半夜產生的內容隔天才到;adaptive 排程增加平均喚醒次數需量測電池影響。
方案 A:持久連線(WSS)—「真 push」,僅建議充電時開
裝置與後端維持長連線,後端有東西直接寫下來。
- 省電技巧:WiFi modem-sleep + DTIM(每 ~100ms–1s 醒來看 AP buffer), push 延遲 < 1s,平均電流 ~1–5 mA → 電池由「月」掉到「天~週」。 注意:1–5 mA 是樂觀的實驗室基準;家用網路常見 broadcast / multicast 風暴會把 平均電流推到 ~10–20 mA,須在實際家庭網路環境量測驗證。
- 僅在充電/插電時開:晚上放上充電座 → 開一條 WSS → 後端把累積的
content_refresh全部推下來 → 早上即有新故事。 - 部分複用既有基礎設施:裝置端已有 WSS client(live chat 用)可複用,但既有通道是
暫態、使用者觸發;方案 A 需常駐長連線,等於把通道性質從暫態改為常駐,後端的
presence registry / pub-sub 路由 / connection gateway 屬重大升級(見下表),非單純複用。
離線佇列語意已由現有
pending_actions提供。 - 代償:電池上不可在 battery + idle 常開;需後端長連線基礎設施(見下表)。
若採方案 A,後端需補:
| 元件 | 作用 | 常見實作 |
|---|---|---|
| Presence / connection registry | 記錄 device X 連在哪個 gateway 節點 | Redis |
| Pub/Sub 路由 | 「device X 有新內容」送到持有連線的節點 | Redis Pub/Sub / NATS |
| Connection gateway | 撐住 N 條長連線、idle ping/pong | 自寫 WS gateway |
| 離線佇列 + QoS | 裝置離線時存著、重連補送 | 既有 pending_actions |
方案 B:BLE 旁路喚醒 — battery + idle 也能 push,電池代償低–中、主成本在韌體
用「比 WiFi 省電、deep sleep 仍醒著」的 BLE 副通道收「快醒來」訊號。
兩個變體:
- B1(手機中繼):家長 App 永遠可被 FCM/APNS 觸達 → 後端 push 到手機 →
手機用 BLE 叫醒同房間裝置 → 裝置才開 WiFi 拉
pending_actions。 功耗角色須分清:裝置端應僅作 BLE advertiser(被動廣播,低功耗); BLE scanning 是高收信電流操作,不可放在電池裝置常駐——scanning / initiator 角色 交給手機(或閘道)。需手機在家、在附近。 - B2(裝置常駐 BLE advertising):裝置自身常駐低功耗 BLE advertising 作為 「可被觸達通道」,由鄰近手機 / 家用閘道經 BLE 推喚醒提示。相對方案 A 的優勢是 battery + idle 也能近即時 push(A 的 WSS 只能充電時開)。 注意:B2 仍需鄰近有一個聯網中繼(手機或家用閘道)來偵測廣播並送出喚醒—— 裝置睡眠時 WiFi 全關,無法自行收網路 push。目前 MVP 硬體生態不含家用閘道, 故 B2 與 B1 同樣依賴「手機在家、在附近」;唯有導入專用固定閘道後,B2 才能擺脫手機在場假設。
電池代償重估(先前評為「中」,依韌體側估算下修為「低–中」)
殺死 ADR-0010「常連線」的根本原因是電池;但 BLE keep-alive 比 WiFi keep-alive 省約一個數量級。以 RTL8730EAH(候選晶片;ADR-0010 編註晶片型號待定,本估算以此 Realtek AmebaSmart 系列為據,最終以選定型號實測為準)維持 BLE advertising、經優化(大核 A32 沉睡、 廣播間隔設為 1 秒、降低 Tx Power)後,平均電流通常可壓在**數百 µA(甚至更低)**級別。 代入 2000 mAh 電池估算:
| 常駐通道 | 平均電流 | 2000 mAh 估算續航 | 與「數週至月」目標 |
|---|---|---|---|
| Deep sleep(地板參考;非日常 idle,日常 idle 為淺睡眠,電流更高) | ~10 µA | 以年計(受自放電上限) | — |
| BLE advertising(B2 增量,僅 BLE) | ~300 µA | ~9 個月(僅 BLE 上限) | 達標(須加基底) |
| B2 實際待機 = 淺睡眠基底(NFC 輪詢 + CPU light-sleep)+ BLE | 淺睡眠基底 + ~300 µA | 低於 9 個月(待 spike 量測合成電流) | 須實測確認 |
| WiFi modem-sleep keep-alive(方案 A on battery) | 1–5 mA | 天~週 | 不達標 |
| MQTT 常連線(ADR-0010 已否決) | ~20 mA | ~4 天 | 不達標 |
數值為韌體側估算,go/no-go 前須 spike 實測 RTL8730EAH 於目標廣播間隔 / Tx Power 下的合成待機電流 =(淺睡眠基底,含 NFC 輪詢)+ BLE advertising +(WiFi 偶發喚醒), 不可只憑此表的 BLE 增量逕行採納;BLE-only 的 ~9 個月為上限,實際續航更低。
- 代償(重估後):
- 電池:低–中(月級續航落在目標內,不再是否決理由)。
- 主成本在工程而非電池:新增韌體 BLE stack、配對 / 安全(避免任意鄰近裝置觸發 喚醒)。B1 與 B2 皆需「手機在家、在附近」假設(MVP 無家用閘道;B2 僅在導入專用閘道後 才能擺脫此依賴)。對「家用兒童故事機」情境手機在場尚合理,但仍屬重投資。
註:本段下修了先前「電池代償=中、成本最高」的評估。此為記錄性重估,非裁決; 若團隊最終採 B,仍須完成上述 spike 實測並於
/spectra-discuss收斂。
方案 D:維持現狀(不改)— 基準
接受 ~24h 最壞延遲,依賴 NFC scan 自然觸發。
- 好:完全不動架構,電池最佳。
- 壞:爸媽臨時內容體驗差(「我做了故事為什麼機器沒有」),客訴風險。
Consequences
好(採 C,現行方向):
- 大幅縮短臨時內容下發延遲(~24h → ~1–2h),提升「爸媽即時產生故事」的產品體驗。
- 幾乎零架構風險、純 pull、相容 offline-first,不動
config push詞彙立場。
壞 / 代償:
- C 的 adaptive 多喚醒以電池為代價,0034 上線前須 spike 量測;不可推翻 ADR-0010。
- C 重定義
Safety-net wake,0034 apply 時須同步更新 LANGUAGE.md(見 Decision)。 - 後續方案 B 將引入韌體(BLE stack / 配對)複雜度與晶片廠商協作成本;採 B 時須翻 LANGUAGE.md「雲端不主動 push」詞彙。
中性:
- 現有
pending_actions離線佇列在所有方案中都可複用,不需重做下發語意。 - 決策需跨韌體(ODM)、後端、產品三方,屬跨團隊決策,適合先 discuss 收斂再 propose。
待決問題(方案 B go/no-go 前置;C 已裁決採用)
方案 C 已裁決採用(見 Decision),下列為未來啟動方案 B 前必須回答的前置條件:
- 須與晶片廠商 / ODM 討論:RTL8730EAH(或最終選定 SoC)維持 BLE advertising 的實際平均電流、 advertiser-only(裝置不做高耗電 scanning)角色可行性、BLE stack + 配對的韌體開發成本與時程。 此為 B 的硬體可行性根本,須由晶片廠商確認,不得只憑本 ADR 估算。
- RTL8730EAH BLE advertising 的電池 spike 實測(淺睡眠基底 + BLE + WiFi 偶發喚醒的合成待機電流)。
- 產品可接受的最壞下發延遲?以及「充電時即時 / 待機延遲」分層體驗能否被產品接受?
- B2 擺脫「手機在場」假設需專用家用閘道——是否納入硬體路線?
- 採 B 時須同步把 LANGUAGE.md
config push/雲端 push從「一律 avoid」改為「限定情境允許」。