プロトコル解析のススメ
LEGO SPIKEはLEGO社から出ているプログラミング学習キットシリーズの一つです。
https://education.lego.com/ja-jp/products/-spike-/45678#spike%E3%83%97%E3%83%A9%E3%82%A4%E3%83%A0
専用の開発環境アプリ上でブロック配置による簡易的なものだけでなくPythonによる本格的なプログラミングもでき、モータやセンサの制御方法を楽しく学ぶことができます。
LEGOのSTEM教材シリーズの前モデルであるMindstorm EV3ではDirect CommandというAPIが公開されていてPCから制御できたのですが、SPIKEでは残念ながら非公開のようです。
以前使っていたPCからEV3をコントロールするプログラムをSPIKEでも動かしたかったので、通信内容を調べてみることにしました。
まずは会話を覗いてみる
SPIKEの本体(Hub)とPCは、USBケーブルかBluetoothで接続します。
USB接続した時もBluetooth接続した時もPC上にCOMポートが追加されるので、どうやらUSB接続時にはUSBシリアルデバイス、Bluetooth接続時にはBluetooth SPP(Serial Port Profile)で通信してるようです。
シリアル通信では読みやすいテキストデータでやり取りされることが多いので、解析できる可能性が高くて期待できます。
自作プログラムからコマンド送信するにはシリアルポートの接続設定が必要になるので、とりあえずシリアル接続できるRLoginやTeraTermなどのターミナルソフトで繋いでみましょう。
文字化けしないシリアルポート設定を探していきます。
ボーレート9600bps、パリティなしのストップビット1、フロー制御はDSR/DTRで良さそうです。

最初に内蔵されているMicroPythonのバージョン情報が送られてきた後、SPIKE Hubに内蔵されてるジャイロ等のセンサ情報が高周期で送信されてきてるようです。
どうやら流れてるメッセージはJSONフォーマットで、行終端コードNLではなくCRですね。
知りたいのはこちらから送るコマンドなので、次はターミナルソフトではなく通信内容を記録して中身を覗くことができるパケットキャプチャツールを使いましょう。
ソフトウェアパケットキャプチャの定番ツールであるWireSharkを使います。
WireSharkは追加のドライバUSBCapを入れることで、USB通信やUSB接続のBluetooth通信の内容もキャプチャできるようになります。
*** USBCapドライバはPC環境によってはOSが不安定になることがあるようなので、OSの復元操作などに不安がある人はやめておきましょう。 ***
PCから送るコマンドを調べたいのでPCとSPIKE HubをUSBケーブルで接続し、SPIKEの開発環境アプリでストリーミングモードがあるワードブロックプロジェクトを作成して適当なプログラムを作っておきます。

WireSharkでUSB Serialのキャプチャを開始します。

SPIKEからPCへ向けてテキストデータが流れてますけど、バルク通信で細切れになってて読みにくいですね・・・。
めんどくさそうなのでBluetooth接続に変更して、Bluetooth SPPのほうでキャプチャしてみます。
Bluetooth SPPの通信データはRFCOMM(Radio Frequency Communication)プロトコルで実現されているので、RFCOMMでフィルタします。

PC側から送信してるパケットを検索していくと・・・ありました。
最初にSPIKE Hubの情報取得やモード設定らしきコマンドを送信してます。
{"i":"-dJ0","m":"get_hub_info","p":{}}
{"i":"8NSZ","m":"trigger_current_state","p":{}}
{"i":"U1qJ","m":"program_modechange","p":{"mode":"play"}}
{"i":"WFpW","m":"reset_program_time","p":{}}
モータ制御らしきコマンドはこのあたりですね。
{"i":"kPBK","m":"scratch.motor_set_position","p":{"port":"A","offset":0}}
{"i":"pZaU","m":"scratch.motor_go_direction_to_position","p":{"port":"A","position":0,"speed":75,"direction":"shortest","stall":true,"stop":1}}
{"i":"VMMY","m":"scratch.motor_run_for_degrees","p":{"port":"A","speed":100,"degrees":720,"stall":true,"stop":2}}
PCから送信するコマンドもJSON形式で、mがコマンド名で、pにコマンド固有のパラメタを指定する形式のようです。
iは接続するPCによって変わるようなので、送信されたコマンドを一意に識別するためにランダム振られるIDでしょうか?
とりあえず4文字の適当な文字列でも大丈夫そうです。
自作プログラムから動かしてみよう
試しにシリアルポートを開いてモータコマンドを送るプログラムをチャチャっと書いてみます。
using System.IO.Ports;
using System.Text;
using NLog;
ILogger logger = LogManager.GetLogger("main");
const string comPort = "COM7";
using var port = new SerialPort(comPort)
{
NewLine = "\r",
Handshake = Handshake.None,
Encoding = Encoding.UTF8,
};
port.ErrorReceived += (object sender, SerialErrorReceivedEventArgs e) =>
{
logger.Warn($"ErrorReceived: {{ EventType: {e.EventType} }}");
};
port.DataReceived += (object sender, SerialDataReceivedEventArgs e) =>
{
switch (e.EventType)
{
case SerialData.Chars:
var port = sender as SerialPort;
var s = port.ReadLine();
logger.Debug($"<== {s}");
break;
case SerialData.Eof:
default:
logger.Warn($"DataReceived: {{ EventType: {e.EventType} }}");
break;
}
};
logger.Info("Open serial port.");
port.Open();
port.DtrEnable = true;
port.RtsEnable = true;
logger.Info("Connected");
IEnumerable<string> commands = new List<string>()
{
@"{""m"":""program_modechange"",""p"":{""mode"":""play""}}",
};
var motorPort = new[] { "A", "B" };
var motorCmds = motorPort.Select(p => $@"{{""i"":""DS_n"",""m"":""scratch.motor_stop"",""p"":{{""port"":""{p}"",""stop"":1}}}}");
commands = commands.Concat(motorCmds);
foreach (var cmd in commands)
{
Thread.Sleep(100);
SendCommand(cmd);
}
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s,e)=> cts.Cancel();
while (!cts.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(1));
MotorRunForDegrees("A", 100, 30);
}
return;
void SendCommand(string cmd)
{
if (port != null && port.IsOpen)
{
logger.Info($"SEND: {cmd}");
port.Write(cmd + "\r");
}
}
void MotorRunForDegrees(string motorPort, int speed, uint degree)
{
var cmd = $@"{{""i"":""FYX4"",""m"":""scratch.motor_run_for_degrees"""
+ $@",""p"":{{""port"":""{motorPort}"",""speed"":{speed},""degrees"":{degree},""stall"":true,""stop"":1}}}}";
SendCommand(cmd);
}
Aポートに接続したモータを1秒毎に30度ずつ回転させる.NET6.0のコンソールプログラムです。
ビルドして実行してみると・・・あっさり動きました。(トラブルがないとブログとしての撮れ高が・・・)
他のコマンドも簡単に使えそうですね。
何の役に立つのさ?
製造業向けの仕事をしていると、通信仕様書が紛失している設備やプロトコル非公開の設備との通信データをなんとか取れないかという要望がたまにあります。
通信内容がシンプルなものであれば、同じ手順でプロトコル解析して取りたいデータにアクセスできることもあります。
あなたも身近な機器のデータを取れないか、通信内容を覗いてみては?
Happy Hacking!