前一陣子熱映的電影《流浪地球2》,相信裡面出現過的機器狗“笨笨”給大傢留下過深刻的印象。當時商湯科技出瞭一款 1:2.5 比例復刻的笨笨機器人拼裝智能積木。

當時看著感覺挺有趣的就直接入手瞭。簡單來說就是一個可以藍牙遙控的麥克納姆輪小車,機械臂部分有齒輪結構,可以手動轉動,不過沒有連接電機。整個拼裝的過程還是挺愉快的,1800 個零件拼瞭兩個晚上才完工。拼裝過程沒有紙質說明書,依靠的是 APP 中的動態說明書來完成的,過程還算輕松。不過拼完之後使用官方給的 APP 控制笨笨感覺手感比較奇怪,而且這個 APP 竟然要接近 600MB!(雖然知道裡面還有動態說明書,AR 體驗之類的一堆各種各樣的功能,不過估計也很少會用上瞭……)

於是很自然地萌生瞭“黑入”笨笨的主意,解出笨笨的藍牙控制協議,自己寫一個輕量級的第三方控制器,這樣我就可以以自己想要的方式來控制笨笨瞭。說幹就幹,還真寫出來瞭。先放鏈接:

  • 在線體驗鏈接(當然,得先自己買一個笨笨拼好才能用):https://supersodasea.github.io/BenbenController/
  • GitHub 代碼鏈接(歡迎 Star!):https://github.com/SuperSodaSea/BenbenController

你可能會好奇,這是怎麼做到不使用官方的 APP 來控制笨笨的呢?下面來簡單介紹一下我是如何“黑入”笨笨,獲取到控制笨笨的藍牙協議的分析過程。

協議分析

首先想到的是,既然是樂高 like 積木,那說不定和真·樂高的藍牙協議是相似的呢。於是在網上查找瞭一下關於真·樂高的藍牙協議的相關資料,然而並沒有成功。這麼一來就隻能從笨笨的 APP 入手來進行逆向分析瞭。

APK 反編譯

在官網下載一份 Android APK 文件:https://benben.beyondgravity.cn/,下面以版本 1.1.1 的文件為例(beyondgravity_v111.apk)。.apk 文件實際上是改瞭後綴名的 .zip 文件,用你喜歡的解壓軟件進行解壓就可以得到其中的內容。

Android 程序與普通的 .jar 形式的 Java 程序有所區別,它的代碼存放在 classes.dex 中,我們可以用 jadx 對 .dex 文件進行反編譯來查看內部的代碼(當時一開始使用的是 dex2jar + jd-gui 的組合,後來發現 jadx 更好,所以這裡就換成用 jadx 進行介紹瞭)。

可以發現代碼是經過混淆的,不過不用緊張,因為我們是來找藍牙協議的,所以應該有地方會調用安卓的藍牙 API,可以被我們在反編譯後的代碼文本中找到。我們試一下直接嘗試文本搜索一下 Bluetooth 關鍵字:

果然找到瞭一些可疑的類,而且發現 com 包下的一堆類都沒有經過混淆,估計是引用的第三方庫,這倒是幫瞭大忙瞭。研究瞭一下這個 com.yundongjia 包下的代碼,確認瞭它就是藍牙遙控笨笨的代碼。com.yundongjia.tongble 是藍牙協議部分,com.yundongjia.tongui 是遙控界面的 UI 代碼。

藍牙協議

那麼接下來我們要做的就是讀一讀相關的代碼來確認藍牙邏輯瞭。首先我們可以在 com.yundongjia.tongble.BleContext 類中找到三個 UUID:

這些是連接藍牙設備所需要的 UUID。後面一串 -0000-1000-8000-00805F9B34FB 實際上是藍牙公用的 Bluetooth Base UUID,所以我們需要的就是前面的 AE3A、AE3B 和 AF30 這幾個數字。查找一下引用瞭這幾個字段的代碼(在 BleDevice 和 BlueScanThread 這兩個類中),可以發現:

  • AF30 是搜索藍牙設備時所用的 UUID;
  • AE3A 是藍牙設備 GATT Service 所用的 UUID;
  • AE3B 是藍牙設備 GATT Characteristic 所用的 UUID。

(藍牙協議本身很復雜,本人也隻知道其中的很少一部分,這裡就不班門弄斧對其進行詳細介紹瞭這裡我們暫時隻需要知道藍牙 BLE 協議裡有 GATT、Service、Characteristic 這些東西,並且我們可以通過 UUID 找到這些東西就行瞭,詳細的藍牙 BLE 協議介紹可以自行進行搜索。)

到這裡我們就可以嘗試寫一點代碼來測試是否能成功連接控制器瞭。這裡我使用瞭瀏覽器中的 Web Bluetooth API 進行測試,簡單又方便:

const SERVICE_FILTER_UUID = 0xAF30;
const SERVICE_DATA_UUID = 0xAE3A;
const CHARACTERISTIC_UUID = 0xAE3B;

const bluetoothDevice = await navigator.bluetooth.requestDevice({
filters: [
{ services: [SERVICE_FILTER_UUID] },
],
optionalServices: [SERVICE_DATA_UUID],
});
const gattServer = bluetoothDevice.gatt;
if (!gattServer) throw new Error('device.gatt do not exist');
await gattServer.connect();
const service = await gattServer.getPrimaryService(SERVICE_DATA_UUID);
const characteristic = await service.getCharacteristic(CHARACTERISTIC_UUID);