公式SDKが死んだRoboMaster S1を、プロトコル解析で完全復活させた話(プロトコル解析編)
全3回の1本目です。
①プロトコル解析編(この記事)/②[MCPで会話から操縦し、喋らせる編]/③[ROS 2 VSLAM編]
作りたいのは「フチコマ」
先に動機を書いておきます。私が作りたいのは警備ロボットではなく、フチコマです。
自分で家の中を回って、前と何かが変わっていたらそれに気づいて、勝手に喋ってくる。賢さより「そこに居て勝手に動いている存在感」のほうが本体です。
そのためには最低限、自分で回れて・変化に気づけて・喋れる必要がある。この記事から始まる3本は、その部品を一つずつ作っていく記録です。まずは大前提、「そもそも外部から動かせない」という壁を壊すところから。
先に白状しておくと、コードはほぼ全部Claudeが書いています
この3部作のPythonコードは、私がほとんど書いていません。実装はほぼClaude(Claude Code)任せです。
私がやっているのは、実機のパケットキャプチャ、実機での動作確認、「なんか違う」という体感の報告、そして方針の判断。あと重要な機体の充電w。手を動かす部分はAIに投げています。
なので、この記事は「AIにロボットのリバースエンジニアリングをやらせるとどうなるか」の記録でもあります。結論から言うと、驚くほど進むが、驚くほど嘘もつく。各回の最後に、その回で見えた役割分担と失敗を正直に書きます。
詰んでいた
素体に選んだのは DJI の教育用ロボット RoboMaster S1。メカナムホイールの足回り、3軸ジンバル、カメラ。ロボットの素体として破格に良くできています。ヤフオクで破格でゲット。
安いのにはそれなりの理由がありました。
公式SDKが死んでる
GITも長らくメンテされてない
外から制御しようとすると壁が3枚ありました。調べてから買えばよかった・・・
| 事実 | 意味 |
|---|---|
| 実機は S1(EP ではない) | 公式 Python SDK は上位機種 EP 専用 |
ファーム 00.06.0521 が実質最終 |
かつての解放技 robot_ctrl.enable_sdk(1) が存在しない
|
AP直結で 192.168.2.1:30030 が無応答 |
機体側の SDK サービスがそもそも起動していない |
つまり公式ルートは完全に詰み。ネット上でも「アプリを使うか、Raspberry Pi を積むハード改造しかない」というのが通説でした。
残る道は2つ。機体内蔵の「Lab」(Scratch/Python が動く教育機能)から内側を叩くか、通信プロトコルを自力で解いてアプリのフリをするか。
まずはLab経由で一勝
最初の突破は Lab でした。機体内 Lab に UDP サーバを書いたスクリプトを貼り付けて実行し、PC から UDP を投げて chassis_ctrl / gimbal_ctrl を直接叩く。
これは確実に動きます。ただし毎回アプリを開いて Lab を実行する手間が残る。「勝手に動いている存在」からは程遠い。本丸はプロトコルでした。
最大の障壁は8バイト目だった
パケットをキャプチャして眺めると、ヘッダ構造自体はすぐ見えてきます。長さ、固定値 0x80、マジックナンバー、シーケンス番号、タイプ。問題は byte[7] でした。
DJI の他製品の解析例を読むと、どれも CRC の話をしています。当然「CRC8 か CRC16 のカスタム多項式だろう」と考えました。
そして総当たりを回しました。初期値256通り × 主要多項式 × 反転有無 × 入力範囲の組み合わせ。全滅。
「Ghidra でファームウェアを落としてデコンパイルするしかないか」というところまで追い詰められて、最後にもう一度、2800個以上のキャプチャパケットをただ眺めていた時でした。
「これ、ヘッダを全部XORしただけでは?」
from functools import reduce
from operator import xor
def verify_header(pkt):
# ヘッダ[0:7]の8bit XORが pkt[7] と一致するか
return reduce(xor, pkt[0:7]) == pkt[7]
2852パケット中、2852パケットで一致。
CRCという先入観に囚われていただけでした。Ghidra は要りませんでした。この瞬間に、任意の制御パケットを自由に「合成」して機体に送り込めるようになります。
確定したセッション層の構造
| オフセット | 内容 |
|---|---|
[0] |
長さ(Low Byte) |
[1] |
0x80(固定) |
[2:4] |
マジックナンバー 0x1975
|
[4:6] |
シーケンス番号 |
[6] |
タイプ(0x00=hello / 0x05=信頼ch / 0x04=リアルタイムch / 0x03=信頼応答) |
[7] |
ヘッダ[0:7]の8bit XOR |
[8:] |
内側DUMLペイロード(CRC8初期値 0x77 / CRC16初期値 0x3692) |
外側がXOR、内側のDUMLはちゃんとCRC。これが混乱の元でもありました。
駆動コマンド「vstick」は理論が全部外れた
セッションが張れたら次は動かす番です。操縦は vstick(仮想スティック) ペイロードで行われています。
ここでも遠回りをしました。「スティックのXY値がそのまま入っているはず」「速度成分に分解されているはず」といった理論モデルを立てては、実機が微動だにしない、という往復です。
正解を教えてくれたのは、実機の通信を実際にキャプチャしたログだけでした。
mode バイトがビットマスクになっていて、制御対象を切り替えます。
-
mode = 1:並進(f0= 横移動 /f1= 前後移動) -
mode = 2:車体旋回(f6) -
mode = 4:ジンバル(f4= pitch /f6= yaw)
厄介なのは f6 が「車体旋回」と「ジンバルyaw」で共有されていること。同一ステップでこの2つを同時に動かすことはできず、排他になります。
一方、mode = 3(前進+旋回)や mode = 5(並進+ジンバル)といったビットの組み合わせは実機で動作します。ビットマスクだと気づけば素直な設計でした。
プラント特性(後で効いてくる)
実際に走らせて分かった、ファームウェア側の癖です。第3回の自律走行で全部効いてきます。
- 低スティック(f6=25〜60)は動き出しまで約1秒(ランプフィルタが入っている)
- ランプアップ中に停止指令を出すと惰走が +15〜25° に爆増
- 逆スティックを0.35秒入れると即停止(アプリのキビキビした操作感の正体はこれ)
- 回転レートは
≈ 0.84 × f6 deg/s、f6≥330 で ~169deg/s に飽和
テレメトリ:0x01 を見ていなかった
バッテリー、姿勢、位置、ジンバル角、速度、IMU。テレメトリも取れました。
ここでのハマりどころは1つだけですが致命的で、機体からのプッシュ通知は type = 0x01 のパケットで届きます。応答パケットの 0x02 / 0x03 だけをフィルタしていると、テレメトリを丸ごと取り逃がします。「対応するコマンドを送っているのに何も返ってこない」と悩んでいた時間の正体はこれでした。
映像も同じセッションに流れていた
カメラ映像は当初、EP の資料にある別ポート(40921)を張って待っていました。来ません。
実際には、映像も 10607 の同一セッションに in-band で多重化されています。ポートを増やす必要はなく、hello を送ってセッションを確立すれば、その中を H.264 が流れてきます。
ここは第3回で、帯域が11倍になるバグを踏み抜く舞台になります。
この時点で得たもの
- ハードウェア改造:なし
- 公式アプリ:起動しない
- できること:駆動・ジンバル・映像・テレメトリの全部
「詰み」と言われていた機体が、素のPythonから完全に動くようになりました。
教訓
- 「解けない」の前提を疑う。 CRC総当たりが外れ続けたのは、そもそもCRCではなかったから。前提が間違っていると、正しい努力が全部無駄になります。
- 理論より実測ログ1本。 vstick は理論モデルが全滅し、実キャプチャだけが正解を持っていました。
AI任せでやってみて(第1回ぶん)
この回で分かった、AIに実装を投げた時の癖です。
AIは「与えられた前提の中で」全力で走る。 CRC総当たりのコードは頼めば一瞬で出てきます。初期値256通り、多項式のバリエーション、ビット反転の有無、入力範囲の総当たり。網羅性は人間より上です。
ただし、その前提が間違っていることは指摘してくれませんでした。「CRCだと思うんだけど」と言えば、CRCを全力で探し続ける。「そもそもCRCではないのでは」という問い直しは、こちらから出すまで出てきませんでした。
vstickも同じ構図です。「スティックのXY値がそのまま入っているのでは」と相談すれば、その仮説に沿った実装が即座に出てくる。もっともらしい理論モデルがいくらでも出てくるので、全部外れているのに、外れていることに気づきにくい。結局、実機のキャプチャを取ってきて突きつけるまで前に進みませんでした。
一方で、キャプチャという「動かぬ証拠」を渡した後の速さは圧倒的でした。2852パケットのバイト列を渡して構造を解かせる、ビットマスクの規則性を見つけさせる、といった作業は明らかにAIの土俵です。
役割分担は自然にこうなりました。
- 私:実機を動かす、キャプチャを取る、前提を疑う
- Claude:解析コードを書く、パターンを見つける、実装に落とす
「AIは検索空間を高速に潰す。空間そのものを間違えたら止まらない」というのが第1回の実感です。
次回は、この解析結果を使って LLMとの会話からロボットを操縦し、実際に喋らせ、耳を持たせる話です。MCPサーバ化してチャットから drive を呼び、機体のスピーカーで日本語を喋らせ、機体のマイクで聞き取ります。
→ ②[MCPで会話から操縦し、喋らせる編]
