在邊緣運算與物聯網 (IoT) 開發中,資料同步往往是最具挑戰性的一環。特別是當我們的設備(DataDock)必須同時處理低頻(20Hz 生命體徵)與高頻(50Hz PPG 綠光與三軸)的混合資料流時,韌體的時序差異與通訊協定的邊界處理,往往會引發意想不到的坑。
今天,我們將深入探討在 DataDock 專案中,如何解決序列埠同步的 「競速危害 (Race Condition)」,以及如何精準處理硬體傳感器的二進位格式與開機雜訊。
在我們的架構中,手環會先將積累的歷史資料切換至下載模式。根據通訊協定,我們會分批拉取資料:
每當一個階段的檔案傳輸完畢時,手環端會發送一個 0x04 0xFF 0x55 0xFF 0x55 的 FinishTransferPacket 作為該階段結束的簽章。
然而,我們遇到了一個棘手的問題:PPG 檔案總是下載失敗,長度異常中斷!
透過排查 Log 我們發現了經典的競速危害(Race Condition):
當我們在第一階段 (20Hz) 算準了預期位元組數 (Expected Bytes),在收到最後一個位元組的瞬間,我們的 SyncEngine 就「提早」宣布第一階段成功並進入第二階段 (50Hz) 下載。
但此時,手環的韌體因為硬體處理延遲,慢了半拍才把第一階段的 FinishTransferPacket 吐出來。
這導致這個「遲到的結束簽章」,直接撞上了我們已經開始的第二階段 50Hz 下載任務。SyncEngine 誤以為這是 50Hz 任務結束的訊號,於是強制中斷了 PPG 下載,導致高頻資料流失。
為了解決這個時序落差,我們在 SyncEngine 中引入了 _ignore_next_finish 旗標 (Flag)。
[!TIP]
攔截幽靈封包
當我們因為「算準 Bytes」而提早結束下載任務時,我們主動豎起防護罩(設定_ignore_next_finish = True)。當接收到下一個FinishTransferPacket時,系統會主動丟棄並印出:[SyncEngine] Ignoring leftover FinishTransferPacket from previous fast-finish.
這個機制成功隔離了前一個任務的殘留訊號,保護了 50Hz 巨量 PPG 資料 (100KB+) 不被無辜腰斬。
解決了下載中斷的問題後,我們成功取得了 50Hz 的原始資料。但在解碼為 CSV 檢查時,卻發現了另一個靈異現象:檔案的第一筆資料,出現了 -1!
50Hz 的 PPG 封包非常緊湊,每一筆採樣 (Sample) 只佔用 9 個 Bytes:Time Diff (1) + Green PPG (2) + Acc X (2) + Acc Y (2) + Acc Z (2)
在 Python 中,我們最初使用 struct.unpack('>4h', ...) 來解碼這後面的 8 個 Bytes。
這裡的 h 代表的是 Signed 16-bit Integer (帶符號整數)。
我們發現這個 -1 其實是感測器的 硬體啟動雜訊。在晶片剛通電、FIFO 暫存器尚未準備好第一筆真實採樣的最初幾毫秒,硬體會直接吐出記憶體的預設空值:0xFFFF。
當 0xFFFF 遇到我們定義的帶符號解碼器 h 時:
根據 二補數 (Two's Complement) 原理,0xFFFF (二進位的 1111111111111111) 會被無情地轉換為整數 -1!
這就是為什麼綠光 PPG 和三軸一開始都是 -1,但到了第四行就恢復成 30816 這種正常生理讀數的原因。
[!NOTE]
在處理 IoT 邊緣設備的 Raw Data 時,開頭幾筆出現極值或預設值 (0x0000或是0xFFFF) 是極為正常的硬體特性,通常在後端資料清洗時 Drop 掉即可。
雖然了解了原理,但對於後端分析系統來說,綠光 PPG 屬於「絕對光強度」,本質上不應該有負數。
同時,為了與團隊舊有的歷史資料處理管線 (Pipeline) 完全相容,客戶要求我們將所有的欄位(包含具有方向性的三軸)全部強制設定為無符號正數。
我們將 Python 的 struct 解碼字元做了一點魔法替換:
從原本的 >4h (4 個帶符號整數)
改成了 >4H (4 個無符號整數,大寫 H 代表 Unsigned Short)
- vals = struct.unpack('>4h', bytes(chunk[1:9]))
+ vals = struct.unpack('>4H', bytes(chunk[1:9]))
green, acc_x, acc_y, acc_z = vals[0], vals[1], vals[2], vals[3]
改動推送到 Raspberry Pi 產線後:
-1 雜訊,現在正確顯示為無符號的最大值 65535,完全消除了光強度變負的邏輯謬誤。65536,就能還原出物理世界的真實負向加速度。在這場高頻資料保衛戰中,我們不僅見證了分散式非同步通訊中 Race Condition 的狡猾,也深刻體驗了二進位協定中位元組解碼 (Signed vs Unsigned) 對於最終資料呈現的巨大影響力。
在開發邊緣運算節點時,我們不僅是在寫程式,更是在擔任硬體與軟體世界之間的最佳翻譯官。
明天,我們將繼續深入 DataDock 的系統穩定性工程,敬請期待!