嗨大家好,潛水很久第一次發文。
最近我花了大概n天,把小米 M365 電動滑板車的 BLE 通訊流程摸到可用程度,最後自己寫了一個 Android App 來讀取即時數據(速度/電量/溫度/里程),也順便把踩坑過程整理起來,給之後想做 BLE 相關專案的人參考。
先說清楚:這篇是工程心得分享,我只研究自己的設備,不鼓勵拿去做任何破壞或繞過安全機制的用途;另外騎車時也不要邊騎邊操作手機或眼鏡 UI,安全第一。
為什麼要自己寫
我平常用米家 App 連 M365,但有幾點讓我很不爽。
開 App 要等載入一堆我用不到的東西
明明是藍牙連線,有些數據卻要連網才能看
我之後想接到 AR 眼鏡上做 HUD,官方 App 基本不可能配合
所以我一開始想得很天真:BLE 而已,掃到就連、連到就讀,最多加個 notify 就行。
第一個大坑:它不是「連上去就能讀」的 BLE
現實是:你連上只是拿到「握手資格」,後面還要跑一段安全通道建立流程,才能開始交換實際資料。這類裝置常見做法是先透過金鑰交換把雙方拉到同一個 session key,再把後續資料包進認證加密裡。
我第一次遇到時最痛苦的點不是「看不懂加密」,而是它錯了不一定會回錯誤。很多時候你做錯一個 byte,它就直接沉默,你只看到 App 這邊一直等、一直 timeout,debug 起來超像在玩恐怖遊戲。
找資料:文件不完整是常態
我過程中有參考一些協議整理、現成 library 的實作、討論串筆記。心得是:文件可以讓你知道大方向,但細節常常缺;最後能救你的,通常是 source code + 自己的封包 log 對照。
我後來採用的策略是:把流程拆成「連線層」、「握手層」、「加密通道層」、「資料解析層」,每層都留足 log(包含十六進位 dump),才不會全部混在一起瞎猜。
註冊(第一次配對):我以為是密碼學寫錯,結果是「少了一個人類動作」
註冊流程我卡最久,症狀很固定:前面步驟看起來都正常,但到了某一步裝置就已讀不回,最後 timeout。我當下直覺就是「我把公鑰或序列化弄錯了」,於是開始地毯式排查。
這邊我踩過的坑都很工程日常,也真的會讓人越查越懷疑人生:
我懷疑 Kotlin 的 Byte 正負號坑到我,所以把每一段 byte array 都印成 hex 一個個比
我懷疑 characteristic 用錯(寫到不該寫的那個),乾脆把可寫的都試過一輪
我懷疑是沒先開 notify(裝置有回但我沒訂閱到),所以補了 CCCD 設定與回呼狀態檢查
我懷疑是 Android 的 GATT cache 玄學導致服務列表不更新,甚至試了「斷線重連/重掃/延遲」那套
我懷疑是 timing 問題:送太快、裝置還沒準備好,所以加入 retry/backoff,並把 timeout 拉長
結果最後答案很荒謬:註冊流程在某個時間窗內需要使用者按下滑板車的電源鍵,它才會進入「允許註冊/確認配對」的狀態。
協議資料通常只會寫「等待 ready」,但不會把「ready 其實要人按按鈕」講得很明顯。後來我把 timeout 拉長(例如到十幾~三十秒區間)並在 UI 明確提示「請按電源鍵」,註冊就穩定通過了。
那一刻我才懂:很多 IoT 裝置會用「實體互動」當 presence proof,避免路人遠端亂註冊。它不是 bug,是設計,只是文件沒把它寫成人話。
登入(後續連線):明明顯示成功,為什麼後面封包都像丟進黑洞?
註冊成功後我以為結束了,結果登入又撞牆一次,而且是更陰的那種。
登入那段我遇到的狀況是:流程跑完看起來「成功」,也拿到一些 session 相關資料,但後續送讀取命令,裝置要嘛不回、要嘛回了卻解不開。你會開始分不清楚到底是哪裡壞:是其實根本沒登入成功,還是登入成功但我後面包的資料格式不對。
我後來整理出幾種「超常見、而且很像你會不小心踩到」的原因:
保存憑證/配對資訊時資料型別混用(例如把 byte 當字串存),長度看似正常但內容已經變了
派生 session key 時雙方輸入順序不一致(你以為是 (A||B),其實要 (B||A) 之類),結果 key 每次都「差一點點」
我一開始太相信文件,以為某個計數欄位要嚴格遞增;但在我的車/韌體上,實際驗證行為跟想像不同,導致我方產生的加密封包對不上它期待的形式
最容易犯的一個:把「明文通道的外層封包格式」也一起丟去加密,導致加密後的內容裝置根本解析不了
我最後的解法不是神來一筆,而是很笨但有效:找一個「已知可用」的實作當參考,逐步比對每個階段的輸入輸出(不用到一模一樣,但至少要在同一個分層概念上),然後把自己的程式拆成可觀測的小步驟。
一旦能把問題定位成「我的加密 key 派生跟它不一致」或「我加密的 payload 分層搞錯」,debug 就會突然變快,因為你終於知道自己在修哪一層。
讀取資料:分層觀念釐清後就通了
登入之後要讀速度、電量、溫度那些,其實就是一堆命令與回應的解析。但我一開始照著某份封包格式組,裝置完全不理我。
後來才釐清:有些資料講的是「外層傳輸格式」(含 header/CRC),但在加密通道裡你真正要處理的是「內層命令 payload」。外層包裝往往由加密封裝層統一處理,你多包一次或少包一次都會直接被丟棄。
當我把分層釐清、並且把解析端的 little-endian 與倍率(例如速度/里程常有縮放係數)處理好之後,第一次在 log 看到速度、電量穩定更新,真的有種「終於跟它講上話了」的感覺。
App 架構:Kotlin 做狀態機,Rust 負責密碼學
我最後的架構大概是:
Jetpack Compose:UI(顯示連線狀態、提示使用者按鍵、顯示即時數據)
Repository:BLE 連線、狀態機、timeout/retry、封包解析
Rust(JNI/FFI):握手與加解密相關流程(避免在 Kotlin 自己硬寫)
把密碼學那段放 Rust 的好處是:你可以把「協議流程問題」跟「演算法實作問題」分開,不然全寫在 Kotlin 裡,出錯時你會兩邊一起懷疑,會慢很多。
我踩過的坑(簡表)
註冊一直 timeout:其實需要使用者按電源鍵確認,且 timeout 要設得比較像人類反應時間
登入後解密失敗:先懷疑資料保存/輸入順序/分層,再回頭看密碼學參數
命令不回:常見是內外層封包混在一起包錯
數值亂跳:注意 little-endian 與縮放係數
Android 權限:Android 12+ 通常需要 BLUETOOTH_SCAN / BLUETOOTH_CONNECT,不同版本還可能牽涉定位權限與前景/背景限制
目前成果與下一步
目前我已經做到:
掃描並連線 M365
第一次配對與後續快速登入
即時讀取速度、電量、溫度、里程
行程統計(時間/距離)
多語系介面
下一步是接到 Rokid AR 眼鏡做 HUD(顯示速度/電量/警示),但會以安全為前提,原則上不做「需要操作」的互動,頂多顯示資訊。
如果有人也在做 BLE、或正在被「它都不回我」折磨,歡迎留言交流。我也可以分享我怎麼設計狀態機與 log 結構(偏工程面、以除錯為主)。


