2026年8月25日 星期二

[水井村USR] 看得見538 Bytes,卻讀不到HTTP:一次珍貴的Ameba SSL封包除錯紀錄

水井三寶故事機|V1.0-13 R3.3

看得見538 Bytes,卻讀不到HTTP:一次珍貴的Ameba SSL封包除錯紀錄

本次測試完成了HUB 8735 Ultra故事機的安全遠端命令、執行結果回報與重新啟動,也找到一個非常隱密的封包問題:SSL回應中只要混入一個NUL(0x00),Arduino的字串解析就可能看似收到資料,實際上卻找不到HTTP狀態列。

最終結果:六種遠端命令全部成功,Django後台均顯示「成功」,每筆命令只領取一次;設備重新啟動後,開機語音也正常播放。

一、這一版要完成什麼?

水井三寶故事機先前已具備實體按鈕、手機Web控制、JQ6500播放、STA主要連線、AP故障備援、NTP校時、Flash離線事件佇列與設備心跳。V1.0-13再向前一步:讓管理者可以從Django雲端安全地下達維護命令,設備執行後還必須回報結果。

Django建立命令
心跳領取命令
8735執行命令
結果ACK後結案
安全領取命令包含Device ID、API Key、UUID與逾期時間。
只執行一次透過命令UUID避免同一指令被重複執行。
結果可追蹤Django必須收到SUCCESS或FAILED,才能將命令結案。

二、R3.1已經成功,為何後面又失敗?

R3.1第一次證明「心跳夾帶命令」的架構可行。設備成功收到並執行狀態回報:

[HEARTBEAT] Accepted HTTP 200
[REMOTE] Heartbeat received REPORT_STATUS | bba846e1-...
[REMOTE] Executed REPORT_STATUS | result queued SUCCESS

但是下一次心跳要把執行結果送回Django時,卻出現:

[HEARTBEAT] POST | state=IDLE | queue=0
[HEARTBEAT] Failed code=0

這不是命令沒有執行,也不是Django拒絕請求,而是RTL8735B端沒有正確解析伺服器回傳的HTTP狀態。

三、最關鍵的線索:538 Bytes與空白Preview

R3.2先改成收完整HTTP Header,並加入原始資料長度診斷。結果出現非常矛盾的訊息:

[HEARTBEAT HTTP] Unparsed bytes=538 | preview=
[HEARTBEAT] Failed code=0
問題焦點:如果完全沒有收到資料,長度應該是0;現在明明收到538 bytes,為何預覽是空白,而且找不到HTTP/1.1 200 OK

答案是回應內容最前方混入了NUL,也就是數值為0的位元組:

第1 byte00 NUL控制字元
後續文字HTTP/1.1 200 OK
自訂HeaderX-Ameba-Result-Ack: 1
JSON Body{"ok":true,...}

Arduino的String仍可能把這個0x00算入長度,所以看到538 bytes;但許多字串函式會把NUL視為C字串結尾。於是:

  • raw.length()仍顯示收到資料。
  • raw.indexOf("HTTP/")可能找不到後面的HTTP狀態列。
  • Serial.println(raw)從第一個NUL就停止,所以Preview看起來完全空白。
  • HTTP狀態碼最後被判定為0。
這次最重要的除錯觀念:「收到幾個bytes」不等於「收到可直接當成文字處理的bytes」。網路除錯不能只看字串,也要保留位元組、控制字元與十六進位的觀點。

四、R3.3如何修正?

R3.3不再把讀到的0x00直接加入HTTP文字緩衝區,而是在SSL讀取階段排除NUL,同時計算排除數量:

int value = client.read();
if (value == 0) {
    nulCount++;
} else if (value > 0) {
    raw += (char)value;
}

若解析仍失敗,程式還會輸出前32 bytes的HEX資料。這使除錯從「猜測HTTP為何不見」變成「直接觀察真正收到的位元組」。

[HEARTBEAT HTTP] Unparsed bytes=... | NUL filtered=... | preview=...
[HEARTBEAT HTTP] HEX: 48 54 54 50 2F 31 2E 31 ...

其中48 54 54 50就是ASCII的HTTP。這種診斷方式未來也適合用於UART、MQTT、Modbus、WebSocket及其他嵌入式通訊問題。

五、R3.3實機測試結果

更新R3.3後,日誌第一次直接證實問題來源:

[HEARTBEAT HTTP] NUL filtered=1
[HEARTBEAT] Accepted HTTP 200
[REMOTE] Result ACK SUCCESS | d7d05b25-...

只是一個NUL,就足以讓前一版完全讀不到HTTP;排除後,命令領取、執行與ACK全部恢復正常。

六種遠端命令測試

遠端命令設備動作結果
REPORT_STATUS立即回報設備狀態成功
SYNC_EVENTS立即補送Flash離線事件成功
SYNC_TIMENTP校時;失敗時切換HTTP時間成功
SET_VOLUME音量20調整為16並延遲保存成功
PLAYER_RESET重新初始化JQ6500播放器成功
DEVICE_RESTART回報成功並保存資料後重新啟動成功

六、安全重新啟動不是「收到就重開」

DEVICE_RESTART尤其值得記錄。設備不是一收到命令便立刻重新啟動,而是依序完成:

  1. 領取具有UUID的重新啟動命令。
  2. 將執行結果排入下一次心跳。
  3. 等待Django回傳X-Ameba-Result-Ack: 1
  4. 保存Flash中的統計、音量與佇列資料。
  5. 最後才執行系統重新啟動。
[REMOTE] Executed DEVICE_RESTART | result queued SUCCESS
[HEARTBEAT] Accepted HTTP 200
[REMOTE] Result ACK SUCCESS | 0a815d53-...
[FLASH] Batch saved: before remote restart
[SYSTEM] Restart scheduled: heartbeat command restart
[SYSTEM] Restarting: heartbeat command restart

重新開機後,音量16成功保留,開機提示語音正常播放,STA重新連上Wi-Fi,證明「命令回報、Flash保存、重啟復原」三個環節都通過。

七、Django後台驗證

後台六筆命令全部顯示「成功」,Delivery Attempts均為1,Completed At也有完成時間。這代表:

  • 命令沒有因輪詢或網路延遲而重複執行。
  • 每筆命令都由相同UUID貫穿領取、執行與結果確認。
  • 設備重新啟動前,雲端已收到成功結果。
  • 心跳既是健康監控,也是低負擔的安全命令通道。
命令類型狀態領取次數完成確認
重新啟動設備成功1已記錄
重設播放器成功1已記錄
設定音量成功1已記錄
立即網路校時成功1已記錄
立即補送事件成功1已記錄
立即回報狀態成功1已記錄

八、這次經驗教會我們什麼?

1. HTTP仍是Bytes即使最終看到的是文字協定,底層傳輸仍可能包含控制字元。
2. 長度不能代表內容538 bytes可能以NUL開頭,字串函式看到的卻是空字串。
3. 要有HEX診斷文字預覽失效時,十六進位輸出是最可靠的證據。
4. ACK後才能破壞性操作重啟前先回報與保存,才能避免雲端永遠停在「已領取」。
5. UUID保證冪等同一命令不因重送而執行兩次,對展場設備非常重要。
6. 現場功能優先網路同步與心跳都不能阻塞實體按鈕及故事播放。

九、版本結論

V1.0-13 R3.3已完成從「會播放故事的單機」到「可被雲端安全維護的展場設備」的重要跨越。它不只會執行遠端命令,還能辨識命令、避免重複、回報結果、等待ACK、保存狀態,並在必要時安全重啟。

本次最珍貴的發現:真正讓系統卡住的,不是538 bytes的大問題,而是藏在最前面、肉眼看不見的1 byte——NUL(0x00)。物聯網系統的可靠性,常常就建立在願不願意把「看似空白」繼續追查到底。

沒有留言:

張貼留言