2026年11月14日 星期六

Reverse-Engineering a Feiyu Gimbal's BLE Protocol and Driving It from a Raspberry Pi | 反編譯 Feiyu 雲台的藍牙協定,用樹莓派自己控制它

手邊有一支 Feiyu(飛宇)的 Vimble 3 手持雲台,平常是用手機上的官方 App「Feiyu ON」透過藍牙控制。這次想試試不經官方 App、自己寫程式直接控制雲台——結果官方 App 在測試機上一開就閃退,連「側錄藍牙封包」這條路都走不了,只好改成直接反編譯 App 把控制協定挖出來。最後在一台樹莓派 4 上用 Python 把雲台轉了起來,而且能靠雲台自己回報的角度確認動作,不用拿相機對著看。這篇記錄整個過程、協定細節,和一路踩到的坑。

起點:官方 App 一開就閃退

原本的計畫很單純:手機開官方 App 連雲台,同時打開 Android 開發者選項的「藍牙 HCI 封包記錄」,動一動雲台,再把側錄到的封包反推出指令。沒想到 App 一打開就退回桌面,點回來只剩白畫面。抓 crash log 一看,原因很明確:

java.lang.Throwable: MobSDK渠道版本配置错误,Google Play渠道未配置GPP版本
  at MyApplication.initMobSdk(MyApplication.kt:120)
  at MyApplication.initAfterPolicyAgreed(MyApplication.kt:62)
  at MyApplication.onCreate(MyApplication.kt:35)

這是這支 Google Play 版的 App 在 Application.onCreate 初始化一個第三方 SDK(MobSDK)時,偵測到「Google Play 通道沒配置 GPP 版本」就直接未捕捉例外閃退——而且它是在「同意隱私政策之後」才初始化,所以政策一旦同意過,之後每次都是畫面都還沒出來就崩掉。這是 App 本身的建置問題,不是我設定能解的。App 跑不起來,就沒有封包可以側錄。

既然側錄走不通,乾脆換另一條路:反編譯。App 的 APK 就在手邊,用 jadx 把它解回 Java 原始碼,直接讀控制邏輯,不需要 App 跑起來、也不需要雲台在旁邊。

反編譯挖出協定

用 jadx 反編譯後,控制核心在 com.feiyutech.lib.gimbal 這個套件。幾個關鍵類別:組封包的 AkCmdWriter、搖桿控制的 JoystickController、協定常數 Protocol/RequestId,以及校驗用的工具函式。把 generateWholeCmd 讀懂後,整個封包格式就清楚了:

重點整理:

  • 前導碼:A5 5A 03 固定,第 4 位元組是 target(搖桿即時控制用 0)。
  • 指令欄位:高 3 bit 是 cmdType(push=0、set=1、get=2、respSet=3、respGet=4),低 13 bit 是 cmdId,兩者組成一個小端 16-bit。
  • 校驗:CRC-16/XMODEM(多項式 0x1021、初始 0x0000、不反射、不 xorout),範圍從第 3 位元組算到資料末端,不含兩個 header 與 CRC 本身。
  • 即時搖桿:cmdId = 14,資料 5 bytes = [enableRoll, LE16(course 平移), LE16(pitch 俯仰)],皆為帶號 int16,0 代表停。要持續運鏡就每 20~50ms 重送一次,送 0,0 停止。

藍牙這端,APK 裡的 gimbal-ble-properties.json 依機型分類了服務與特徵。AK 系列(含 Vimble 3)用的是標準的 Nordic UART Service:

用途UUID
Service6e400001-b5a3-f393-e0a9-e50e24dcca9e
Write(送指令)6e400002-b5a3-f393-e0a9-e50e24dcca9e
Notify(收狀態)6e400003-b5a3-f393-e0a9-e50e24dcca9e

把這些湊起來,就能用 Python 重現封包編碼器。CRC 與組框的核心大概長這樣:

def crc16_xmodem(data: bytes) -> int:
    crc = 0
    for b in data:
        for i in range(8):
            bit = (b >> (7 - i)) & 1
            msb = (crc >> 15) & 1
            crc = (crc << 1) & 0xFFFF
            if bit ^ msb:
                crc ^= 0x1021
    return crc & 0xFFFF

def build_frame(target, cmd_type, cmd_id, msg_id, data=b""):
    body = bytes([0x03, target & 0xFF]) \
         + ((cmd_type << 13 | cmd_id) & 0xFFFF).to_bytes(2, "little") \
         + bytes([msg_id & 0xFF]) + len(data).to_bytes(2, "little") + data
    return bytes([0xA5, 0x5A]) + body + crc16_xmodem(body).to_bytes(2, "little")

def joystick(course, pitch, roll=False):
    data = bytes([1 if roll else 0]) \
         + (course & 0xFFFF).to_bytes(2, "little") \
         + (pitch & 0xFFFF).to_bytes(2, "little")
    return build_frame(0, 0, 14, 0, data)   # target=0, push, cmdId=14

在樹莓派 4 上實測

控制主機要「實體靠近雲台、有藍牙、能跑 Python」,手邊的樹莓派 4 正好符合,用 bleak 這個跨平台 BLE 函式庫。先掃描,果然看到雲台在廣播:

$ python3 send_gimbal.py --scan
EE:xx:xx:xx:xx:xx  FY_Vimble3_xx
...

連上去、往 write 特徵送 joystick 封包——雲台真的往右轉了。反編譯還原的協定,第一次就驅動了實體雲台。

踩到的坑

  • 一直往同方向送會頂到極限:持續送同向的 joystick,yaw 會一路轉到機械極限頂住,雲台「噹噹」叫警告。正常操作要用短脈衝或回中,不要一直壓著。
  • 重連會卡「半連線」:BLE 斷線後雲台會卡在半連線狀態,害下次連線逾時。解法是重連前先讓它斷乾淨、重新廣播,再掃描連線。
  • 開 notify 直接被拒:想訂閱 notify 特徵讀回報時,CCCD 寫入回了 UNLIKELY_ERROR 並且連線馬上斷。這個錯誤指向一個關鍵——

關鍵:一定要先「配對(bond)」

重開雲台之後,同樣的程式卻怎麼送都不動了,而且雲台一直閃藍燈(藍牙搜尋/等待連線狀態)。反覆試下來,結論是:這支雲台必須先完成 BLE 配對(bond、加密連線),才會進入「受 App 控制」模式,也才允許開 notify。第一次沒配對就能動,只是雲台還殘留著先前被手機 App 連過的受控狀態,是巧合。

用 bluetoothctl 做一次 Just Works 配對(無 PIN)後,一切就通了:

bluetoothctl
  power on
  agent NoInputNoOutput
  default-agent
  scan on
  pair EE:xx:xx:xx:xx:xx        # 出現 Bonded: yes 即成功

配對之後:notify 正常開啟、雲台開始持續回報姿態、joystick 立刻生效。(小提醒:bond 後不要把裝置 remove,會連 bond 一起刪掉;重連只要先 disconnect 讓它重廣播再掃描即可。)

不用相機,靠回報角度自己驗證

配對+開 notify 之後,雲台會持續推送姿態幀(含即時的各軸角度)。這帶來一個很方便的驗證方式:送移動指令前後各讀一次角度,比對差值,就能判斷雲台有沒有照指令轉動,完全不必拿相機對著拍。實際跑一段右轉:

$ python3 send_gimbal.py --addr EE:xx:xx:xx:xx:xx --course 300 --hold 1.5 --verify
已連線 FY_Vimble3_xx
notify 已開(需 bond 才會成功)
  [前] 軸1=-101 軸3=-2750
  [後] 軸1=-130 軸3=-3383
角度變化:軸1 -101→-130 (Δ-29), 軸3 -2750→-3383 (Δ-633)
→ 有明顯變化=雲台有動

送右轉後,回報的其中一軸角度變化了 633(移動前只在 ±1 抖動),遠超過漂移,確認雲台確實轉動了。有了這個,之後要自動化測試或遠端驗證動作,都不用人在現場盯著。

小結

官方 App 崩潰反而逼出一條更乾淨的路:直接反編譯把協定挖出來,用 Python 重現,在樹莓派上就能控制雲台。過程中最花時間的不是協定本身(格式很規矩),而是 BLE 的狀態細節——尤其「要先配對才受控」這點,沒有回報角度幫忙判斷的話很難定位。整理成幾句給之後的自己:封包是 A5 5A 03 開頭加 CRC16-XMODEM、搖桿是 cmdId 14、BLE 走 Nordic UART、而且務必先 bond。

※ 本文為個人對自有裝置的互通性研究與學習紀錄。Feiyu、Vimble 等為飛宇科技的商標,與原廠無關;相關協定資訊請勿用於侵權或商業用途。

沒有留言:

張貼留言