當前位置: 華文問答 > 影視

如何「黑入」你的【流浪地球2】笨笨機器人

2023-05-13影視

前一陣子熱映的電影【流浪地球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 );

    簡單來說就是向使用者請求連線帶有指定 UUID 的藍芽器材,並在連線後獲取對應器材的 GATT Service 和 Characteristic。嘗試執行一下,果然能夠發現並連線笨笨的控制器:

    成功連線之後,就要發送實際的控制數據了。在程式碼中繼續尋找,在 BluetoothDevice 類中我們可以發現一個名為 getSendData 的函數和一個可疑的陣列:

    getSendData 函數很明顯是將陣列中的數據相加計算校驗和,並寫入陣列中指定位置的字節,應該就是計算封包的校驗和部份的程式碼了,基本上可以確定這個數據就是要發送的藍芽數據了。那麽剩下就是看哪裏的程式碼會修改這個陣列(呼叫 setSendData 方法,上圖中未列出)了。很快就能找到 BleContext 類中的 engineRun 方法。

    檢視相關程式碼可以得知參數 engineEnum 是電機的編號(ABCD),z10 是電機方向(false 為正轉),i10 是 電機轉速(0~127)。比如下面是 ControllerActivity 類中呼叫 engineRun 方法設定四個電機的方向和轉速的程式碼。

    可以發現官方控制器的程式碼只分了前後左右四個方向的情況,沒有考慮斜向移動,難怪手感會很奇怪。回到上面的 enginRun 程式碼,不難發現 ABCD 四個電機的數據分別對應上述陣列中下標 4~7 的位置。經過後續測試發現:

  • 值為 0 或 128 時電機停轉。
  • 對於 ABC 電機而言:
  • 值為 1~127 時電機逆向旋轉,為 1 時速度為最大值。
  • 值為 129~255 時電機正向旋轉,為 255 時速度為最大值。
  • D 電機的數據與上述描述相反,1~127 為正向旋轉,129~255 為逆向旋轉。
  • 這裏你可能會好奇,前面程式碼裏寫的是特判了電機 A,為什麽實際上是電機 D 相反呢?這是因為笨笨上的四個麥克納姆輪有兩種不同的手性方向,AD 為一組,BC 為一組(可以腦補一下,對角線上的一對輪子是旋轉對稱重合的,而同一前後/左右側的兩個輪子是映像的)。官方控制器的程式碼是按照相對於電機本身的方向來規定正反轉方向的,如果我們統一將電機正轉方向規定為讓機器人向前運動的方向,這樣變換下來就是 D 電機需要將數據反向處理一下了。至於為什麽有一個電機的方向是反的就不得而知了 :)

    另一個呼叫了 setSendData 的地方是在 BluetoothContext 類的 initContext 函數中:

    其中 ExifInterface.MARKER 的值為 (byte)-1,BluetoothActionEnum.ActionId 的值為 1,在這裏並不是其名稱對應的含義,猜測可能是混淆導致的。這段程式碼的含義是首次執行時隨機生成一個 16 位的序列號,存放在 MobileSerail.dat 檔中,並寫入藍芽數據的下標 1~2 的位置。由於這個序列號是隨機生成的,所以實際測試下來在藍芽數據的 1~2 位置上填任意數據都可以正常。

    那麽總結一下,藍芽數據的格式如下表所示:

    字節 描述
    0 固定為 0xCC。
    1~2 官方控制器在首次啟動時會隨機生成並保存一個 16 位整數作為此欄位的值,實際設為任意值均可正常工作。
    3 固定為 0x02。
    4~7 四個電機(A、B、C、D)的轉速值。
    電機 A、B、C: 值為 0 或 128 時停轉,為 1~127 為逆向時旋轉(為 1 時速度最大),為 129~255 時正向旋轉(為 255 時速度最大)。
    電機 D: 值為 0 或 128 時停轉,為 1~127 時正向旋轉(為 1 時速度最大),為 129~255 時逆向旋轉(為 255 時速度最大)。
    8~15 可為任意值。
    16 校驗和,值為 1~15 字節之和模 256 的結果。
    17 固定為 0x33。

    按上述格式填好 18 字節的封包之後,只要以一定頻率向之前獲取到的控制器 Characteristic 發送這個數據,就能開始控制笨笨了。官方控制器程式碼中設定為每過 150ms 發送一次封包,實際測試可以以更短間隔發送,但間隔不能過短。如果只發送一次封包的話,控制器的輸出會有異常,無法正常控制。比如說想讓笨笨前進兩秒後倒退,就不能只發送一次前進的數據,過兩秒才再次發出後退的數據,而是必須要在這兩秒內不斷反復發送前進的數據才能夠正常控制。

    至此我們就能夠完全透過自己發送的數據來控制我們的笨笨了!剩下的工作就是包裝這個控制器的程式碼,寫一個界面來制作我們自己的第三方控制器了。在我自己編寫的控制器中,可以透過觸摸(或者滑鼠拖動螢幕上的操縱桿)、鍵盤或者直接連線遊戲手柄來控制笨笨了!左搖桿控制前後左右或斜線移動,右搖桿控制原地旋轉。下面是透過 XBox 手柄來控制笨笨的演示影片。

    笨笨機器人-XBox 手柄操作演示 https://www.zhihu.com/video/1640744418356428800

    至此,「黑入」笨笨的過程就已經介紹完畢了,實際使用到的程式碼都已經在 GitHub 上開源:https:// github.com/SuperSodaSea /BenbenController ,歡迎 Star!如果使用中遇到了問題,也歡迎提出 Issue 哦。

    題外話

  • 為什麽那個包的名稱是 com.yundongjia,藍芽控制器的名稱叫 YX_000000?
  • 搜尋後可以查到一個叫宇星模王的做樂高 like 積木的公司。YX = 宇星。

    附送一張控制器內部的電路圖:

  • AC21BP09335:藍芽控制器
  • MX620B:單路全橋驅動
  • SA8328S:雙路全橋驅動
  • 有點奇葩的是四個電機還用了兩種驅動芯片,兩個 MX620B 驅動兩個電機,然後剩下兩個電機是把一個 SA8328S 拆開來驅動……可能是板子空間不夠放不下了吧。