プラレールのカメラ付きドクターイエロー。運転席にカメラが見える

こちらの画像は2016年10月にタカラトミーより発売された、スマホで運転!ダブルカメラドクターイエローという商品で、 名前の通りカメラが付いていてスマホから操作できるプラレールの電車です。 この記事ではこのドクターイエローをスマホ以外の端末から操作、およびカメラ映像の受信をするために通信プロトコルを解析した過程を書いています。

発端

少し前に押し入れの片付けをしていたところ、このドクターイエローの車両を見つけました。 私の手元にあるこのドクターイエローものは風穴さん (@windhole) から頂いたもので、 私がプラレール計算を始めたての頃に面白いものがあるからと譲っていただいた記憶があります。

さて、昔のことを思い出しながらiPadに入っていた専用アプリで久々に遊んでみます。 アプリから電車の速度を変えられたり、車両に付いているカメラの映像を表示できたりとなかなか高機能でよくできています。 カメラは先頭車両の運転席および左側面の車窓の2箇所に搭載されており、 アプリの操作でリアルタイムに切り替えられるという凝った作りになっています。

ドクターイエローの専用アプリには、速度の調節やカメラの切り替えができるボタンが搭載されていることを表す写真

しかし残念ながらアプリの配信自体はすでに2022年までで終了しており、 中古などで手に入れた人はもう遊べないという状況になっていました。 そこで制御プロトコルを解析して、専用アプリ以外からもこのドクターイエローを制御できないか?と思い試すことにしました。

本記事ではその記録と得られた結果、および試行錯誤に使ったツールや情報をまとめます。 ツールに関してはMacの使用が前提になっていることをあらかじめご了承ください。 また、試行錯誤の過程を思い出しながら書いているためぐだぐだかもしれませんがそこもご容赦ください。

通信の仕組み

専用アプリからどうやってドクターイエローに繋いでいるのでしょうか? 一言で言えば、ドクターイエローがWi-Fiの母艦になっており、 スマホなどの端末からそこに接続することで専用アプリとドクターイエローの通信ができる、という仕組みになっています。

ドクターイエローのシステム構成図。モバイル端末上の専用アプリとドクターイエローが通信をする

というわけで、まずはWi-Fiの通信を傍受すれば制御プロトコルがわかるのではないか?と考えて先に進むことにしました。

パケットキャプチャしてみる

真っ先に思いつくことはWi-Fiのパケットキャプチャですよね。 Macではワイヤレス診断.app というアプリケーションの機能としてWi-Fiスニファがあるのでそれを利用することにしました。 これを使うことでMacが直接関わらないWi-Fiの通信も傍受することができます。

PCがモバイル端末上の専用アプリとドクターイエローの通信を傍受している図

まず、前もってプラレールのWi-FiにMacから接続し、Wi-Fiアクセスポイントとしての情報を確かめてみます。 メインメニューのWi-FiのアイコンをOptionキーを押しながら開くと以下のような情報が得られました。

ドクターイエローのWi-Fiの情報。詳細は続く本文に記載

モバイル端末からだけではなく、Macからも接続できました。どうやら2.4GHz帯のチャネル1、チャネル幅は20MHzのようです。 この情報を元にWi-Fiスニファを使用します。 ワイヤレス診断.appのウィンドウ > スニファを選択するとチャネルとチャネル幅を選ぶダイアログが表示されるので、上記の情報を入力しスニファを実行します。 (その間、MacのWi-Fi機能は使えなくなります)

結果は /var/tmp ディレクトリに保存されるのですが…… なんとサイズが0バイトとなり、中身がありませんでした1(滝汗)。

Linuxなどの別の端末で行うことも考えましたが、モニターモードが使えるWi-Fiドングルが必要と聞いたことから、 ちょっと大変そうだったためパケットの空気録音はあきらめることにしました。

PCから接続した際の挙動

PCからドクターイエローのWi-Fiに繋いだ際にはDHCPが有効になっていることを確認できました。 降ってきたIPアドレスは 192.168.123.2 でゲートウェイは 192.168.123.1 でした。 試しにこのIPアドレスに対して ping を飛ばしたところ予想通り返ってきました。 複雑なことをしているとも思えないので 192.168.123.1 に何かしらを行えばドクターイエローの制御ができるのではないでしょうか。

次にやることといったらポートスキャンでしょうか。 とりあえず開いているTCPポートを探すことにしました。 nmap コマンドを試してみましたが、反応はなかったので次はどうしようかという感じになりました。

専用アプリを騙してPCにつなげる

今回捕捉したい通信は専用アプリからドクターイエローへの命令なのですが、Wi-Fiの傍受がうまくいかないことから別の手段を講じる必要がありました。 少々考えたところ、 PCのIPアドレスを 192.168.123.1 にして 同じネットワークに接続すればアプリから繋ぎに来てくれるのでは?と予想しました。

PCがドクターイエローのフリをして、モバイル端末上の専用アプリからの通信を受け取っている図

自宅で使っているのがTP-Linkの普通のルーターでしたが、DHCPのアドレス予約機能を使うことでPCに対して 192.168.123.1 を割り当てることができました。 その際にサブネットマスクを 255.255.255.0 から 255.255.0.0 に変更する必要がありました。

この状況でWiresharkでパケットキャプチャを開始し、アプリでの操作を行うと… 出ました、 192.168.123.1 へのUDPの通信です。

専用アプリからのパケットキャプチャ結果。ペイロード長が2のUDPパケットが192.168.123.1あてに送られており、それらに対してICMPのDestination unreachable (Port unreachable) の返答がある

UDPというのは自分にとって盲点でした。ポートスキャンがうまくいかなかったのも合点が行きます。

さて、これで通信の中身は見放題です。パケットキャプチャ中にいろいろな操作をしたところ、 専用アプリは以下の表のような体系でコマンドを送信することが分かりました。

操作 宛先ポート ペイロード ASCII 備考
接続確認・設定の取得 30863 42 02 B\x02 SSID・MACアドレス等
を含むレスポンスが返ってくる
映像配信要求 30863 42 76 Bv 詳しくは後述
映像停止要求? 30863 42 77 Bw あまり確証ないです
カメラを車窓に切り替え 30864 42 70 Bp  
カメラを運転席に切り替え 30864 42 71 Bq  
高速走行 30864 42 72 Br  
バック走行 30864 42 73 Bs  
低速走行 30864 42 74 Bt  
電車停止 30864 42 75 Bu  

ひとつ何か実行してみましょう。UDPパケットを送る方法はncコマンドなどいくつかありますが、 一番手軽だと筆者が考えるは bash の擬似デバイスを使う方法です。 ターミナルを開き、電車を走らせるコマンドを送ってみます。

printf 'Bt' > /dev/udp/192.168.123.1/30864

PCでbashのコマンドを実行するとドクターイエローが発車するGIF動画

やりました、成功です!バック走行や電車の停止などがうまくいくことも確認できました。

さて、次の課題は映像の取得です。

映像は謎

カメラが2つもついてるのに運転席からの風景が見れないのはさみしいです。 ここからはどうにかしてドクターイエローからの映像を取得して表示できないかを調査していきました。

パケットキャプチャの結果から、ポート30863番に 42 76 を送ると ドクターイエローはポート30865番に映像データを送り始めることは分かっていましたが、肝心の中身はさっぱり分かりません。 ただ、わからないなりにパケットを観察していくうちに以下のような規則性が読み取れました。

  • ペイロードは固定長で1028バイト
  • 末尾4バイトには規則性がある(これをトレーラーと呼ぶことにします)
  • トレーラーの後ろ2バイトはパケット間で連番になっており、短い周期でリセットされる
    • 周期や送信時刻を見るに、この数字は映像の1フレーム中のパケット番号を表していると推測
  • トレーラーの先頭2バイトは以下のパターンになっていた
    • ff da … フレームの最初のパケット
    • ff dd … フレームの中間のパケット
    • 06 d9 … フレームの最後のパケット
  • 1フレームあたりのパケット数は可変だった

トレーラーを除くとパケット1つの長さが1024バイトというキリのいい数字であることから、 データ全体を1024バイトずつに分割したあとトレーラーを付与し、固定長のUDPパケットとして送信していると推測しました。 そのため、1フレームあたりの映像データは連番になっているパケットのデータ内容を結合したものだと仮定して進めることにしました。

真っ黒な映像をキャプチャしてみる

私自身、映像のコーデックに関する知識は人並みにしかありません。 そのような状況でなんらかの法則を見つけるためには、システムに対して極端な入力を与えた時の挙動を見ることが手掛かりになることが経験上多いです。 極端な入力としてふと思いついたのが、カメラを手で覆って真っ黒な映像を受信することでした。 するとこんな感じのパケットがやってきました。

Wiresharkでキャプチャした真っ黒の映像フレームのキャプチャの先頭部分。規則的なパターンが見つかる。詳細は続いて説明している

画像は最初のパケットの冒頭部分です。 UDPのペイロードは e5 から始まり 28 a0 02 8a 00 を繰り返しています。 続くパケットでも途切れることなくこのパターンが繰り返されており、全体で1200回繰り返していました。

また、各フレームに対しては全く同じデータを含むパケット群がやってきました。 つまり、1フレーム目も2フレーム目もその先も、このようなデータがドクターイエローからやってきました。

先頭の e5 はどういった意味を持つのでしょうか?いまはまだ分かりません。 ただ、ここに来て興味深い規則性を見つけることはができました。

Motion JPEGという説

この話を会社でしたところ、後輩のぱくとまさん (@pakutoma) から、 カメラモジュールはMotion JPEGで出力するものが多いという話を聞きました。 なるほど、Motion JPEG。調べたところ各フレームをJPEGファイル形式で保存するフォーマットのようです。 比較的エンコードの負荷が低いため組み込み用途で使われることが多く、 それぞれのフレームが独立してエンコードされるという特徴があるようでした。

MPEG-4など他のコーデックはフレーム間にデータの依存関係があるものが多いらしいので、 キャプチャした別々のフレームのデータが全く同じになるということはやはりMotion JPEGなのではないか?という目星がつきました。

パターンでググる

次にやることは何でしょうか?数少ない手掛かりであるところの “28 a0 02 8a 00” でとりあえずググってみました。 完全一致を狙うために引用符で囲って検索したところ、次のようなページがヒットしました。

どちらもJPEGファイルのダンプを掲載したページのようです。 これによってドクターイエローからの映像がJPEG形式で圧縮されているという確証がかなり高まりました。 ここから先はデータがJPEG圧縮されていると仮定して進めていきました。

JPEGファイルの構造

本当はこんなに大変なことまでやるはずではなかったのですが、JPEGファイルのフォーマットについて調査する必要を感じたのでこちらのサイト・文献を参考にしながら調べていきました。

その中でも、ドクターイエローからの映像の復元に必要だった要素だけに絞って説明していきます。

JPEGファイルのデコードには色空間の変換や離散コサイン変換などたくさんの工程が関わります。 その中でも、量子化(の復元)およびハフマン符号のデコードには、 画像データそのものの他にそれぞれの処理のために量子化テーブルおよびハフマンテーブルというものが必要です。

これらのテーブルはひとつの画像ファイル内では共通ですが、通常は画像ごとに異なるものが埋め込まれています。 非常に雑な説明ですが、JPEGファイルの中には画像データ内から参照される2種類のテーブルも埋め込まれているとここでは思ってもらえればOKです2

JPEGファイルフォーマットでは、画像データがハフマンテーブルと量子化テーブルの2つに依存していることを視覚的に説明する画像

JPEGファイルは先頭のマーカーから始まり、いくつかのセグメントと画像データの本体が続き、最後に終端となるマーカーで終わります。 マーカーは固定のバイト列であり、JPEGファイルの先頭にはSOI(Start Of Image, 16進数で ff d8)マーカーがあります。 同様にJPEGファイルの末尾にはEOI(End Of Image, 16進数で ff 09)マーカーがあります。 画像データはさまざまな工程を経てエンコードされたデータで、スキャンヘッダーセグメントから始まりますが詳細は省きます。 そしてセグメントの中には量子化テーブルの定義やハフマンテーブルの定義をはじめ、様々な種類のものがありますが、 ここではこの2つに注目します。

セグメントは2バイトのマーカーで始まります。例えば量子化テーブル (DQT) は ff db から始まり、 ハフマンテーブル (DHT) は ff c4 から始まります。

本当にJPEGなのか?

さて、この知識を踏まえて真っ黒な映像フレームのパケットのデータを見てみましょう。

真っ黒な映像フレームのキャプチャ。同じパターンが連続している様子

あー、そういえば同じパターンの繰り返ししかなかったんでした。 しかしわずかな希望があります。データの中には ff 09 が存在しました。 これはJPEGファイルの終わりを表すEOIマーカーに他なりません。

ちなみに真っ黒な映像以外のフレームについてもパケットを覗いてみましたが、 フレームの最後のパケットのどこかにはEOIが含まれていました (それ以降のデータはパケット一つを固定長にするための不要な情報でしょうか)。 なのでJPEGという読みは間違っていないような気がします。

真っ黒な映像のフレームでは同じパターンが最初から最後まで連続するため、EOIの直前までは画像データが続いているように思えます。 ここに至って最後の仮説が出てきます。 すなわち、パケットのデータは量子化テーブルやハフマンテーブルなどを除いた画像データのみなのではないか?というやつです。 つまり、ドクターイエローにも専用アプリにも同じテーブルを使ってエンコード・デコードをするけれど、 映像のフレームを転送する際にはそれらだけを省いて送信しているのではないかと予想します。

ハフマンテーブルと量子化テーブルがないことを視覚的に説明する画像

さて、この仮説を検証していきましょう。

そもそもどんなテーブルを使っているのか?

調べていくうちに、ハフマンテーブルが存在しないJPEGファイルでも一部のブラウザでは開くことができるという話を耳にしました。 その場合にはJPEGの標準が定めるハフマンテーブルを利用してデコードを行うとのことです。 もしかすると、ドクターイエローの映像もこういった類のものかもしれません。

JPEGの仕様であるITU-T 勧告T.81には、 付録として標準的な量子化テーブルとハフマンテーブルが記載されています。 とりあえずこれを使ってデコードができるか試してみることにします。

やることは比較的単純で、SOIマーカーから始まるJPEGに必要な量子化テーブルやハフマンテーブル、それにフレームヘッダーやスキャンヘッダーの先頭部分だけを作成し、ドクターイエローからやってきた映像のフレームと結合すればJPEGファイルとして認識されるのではないか?と考えました。

というわけでそのJPEGファイルの “頭” の部分を作るわけですが、各種テーブルを自前で作成するのは骨が折れます。 ここはオープンソースで公開されているソフトウェアを利用させてもらいます。

先ほどの参考文献のうちのひとつにおけるJPEGの実装として、libKPEGというものが公開されています。 中を覗いてみると、勧告T.81と同じテーブルを用いてJPEGファイルを作成するコードがありました。 このプログラムがエンコードしたJPEGの画像データ以外の頭の部分を利用させてもらうことにしました。

そうして生成したJPEGファイルを見てみると…

生成に失敗して壊れたJPEGの画像

なんかおかしいですね… JPEGっぽく認識されてはいるけれど壊れてしまいました (本当に壊れてるので環境によって見え方が変わるかもしれません)。 ここから試行錯誤して適切なテーブルを作ることも考えましたがこの時点では諦めています。 というのも量子化テーブルは内容が変わったら画像の見え方が変わってしまいますし、 ハフマンテーブルは性質上、圧縮されたデータの切り出し方を定義するため手当たり次第にいじるのは無謀だと考えたためです。

バックアップから録画を抜き出す

万策尽きた… と思いましたがあることを思い出しました。 専用アプリには映像の録画機能があり、iPadにいくつか映像が残されていたことです。 映像の中にJPEGとしてエンコードされたフレームが残っており、そこからテーブルの情報を抜き出せるかもしれません。

さっそくやってみましょう。ただ映像をアプリから取り出す機能が見当たらなかったので、 以前行ったことのある、iOSデバイスのバックアップからファイルを抜き出すことを試みました。 以下、専用アプリが入っている端末はiPadで、バックアップの作成先はMacです。

まずiPadとMacをケーブルで接続し、端末のバックアップを取ります。 すると ~/Library/Application Support/MobileSync/Backup/(長い識別子)/ にバックアップが保存されます。 このフォルダの中にはiPadに含まれるファイルが保存されており、 Manifest.db にiPadのファイルパスとバックアップ内の置き場所の対応が保存されています。 このファイルはSQLite3 のデータベースです。 sqlite3 コマンドでこのファイルを開き、テーブル一覧を .schema コマンドで表示すると以下のように出力されます。

% sqlite3 Manifest.db
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> .schema
CREATE TABLE Files (fileID TEXT PRIMARY KEY, domain TEXT, relativePath TEXT, flags INTEGER, file BLOB);
CREATE INDEX FilesDomainIdx ON Files(domain);
CREATE INDEX FilesRelativePathIdx ON Files(relativePath);
CREATE INDEX FilesFlagsIdx ON Files(flags);
CREATE TABLE Properties (key TEXT PRIMARY KEY, value BLOB);

Files テーブルに対応付けが保存されており、domain 列にiOSのアプリケーション識別子が含まれています。 その中からダブルカメラドクターイエローの専用アプリに対応する識別子を探します。

sqlite> select distinct domain from Files;
...
AppDomain-jp.co.taito.groovecoasterzero
AppDomain-jp.co.takaratomy.plaraildoublecamera
AppDomain-jp.klab.lovelive
...

どうやら AppDomain-jp.co.takaratomy.plaraildoublecamera がそれのようです (今となっては配信終了してしまったゲームもちらほら見えます…)。 Files テーブルの fileID がMac上のバックアップにおけるファイルIDで、 relativePath がiOSでのファイルパスに対応します。 それでは専用アプリのファイルを全て表示してみます。

sqlite> select fileID, relativePath from Files where domain = "AppDomain-jp.co.takaratomy.plaraildoublecamera";
...
1147223af8e41e77416a77a3125761a872ad9d2d|Documents/Unity/local.2f6b486243079466eb83276e4b7c04f0/Analytics/ArchivedEvents/170687570900002.7afb5f51
83cd78fbdc6f53c56e363a745df05a733bd7e110|Documents/20190623154834.mov
ef0069fe7467333fe10cd25855e75482f87ceee7|Library/Preferences/jp.co.takaratomy.plaraildoublecamera.plist

動画ファイル20190623154834.movが見当たりました。昔に撮影した映像のようです。 バックアップフォルダの中にこのIDに対応するファイルが存在したので、拡張子 .mov を付与して開いてみます。

バックアップから抜き出したファイルをQuickTime Playerで開いた様子。コントロールが出てますがただの画像です

お、Macの標準プレーヤーで開けました。

動画からデコードに必要な情報を取り出す

気になる中身はどうなってるでしょうか?

バイナリエディタで開いたらJPEGのような感じだった

開いたらすぐJPEGのSOIマーカー (ff d8) が見つかりました。 ここからJPEGファイルが始まるのでしょう。 さらに、量子化テーブル (ff db) やハフマンテーブル (ff c4) も見当たりました。

SOIセグメント(画像データの開始)の開始までをコピーし、 パケットキャプチャした映像フレームのデータと結合してみると…?

パケットから復元した画像。真っ黒

お、期待通り真っ黒な画像が得られました。しかしこれだけでは確証が得られなかったので、キャプチャした中から適当なフレームのパケットを抜き出して試してみると…

パケットから復元した画像。真っ黒ではない

やりました!ドクターイエローで撮影した画像が得られました。ついにドクターイエローから送られてくる映像フレームのデコード方法を突き止めることに成功です! ちなみに抜き出したJPEGのヘッダに相当するデータの長さは1024バイトでした。キリがいいですね。

なお e5 から始まって 28 a0 02 8a 00 を繰り返す、真っ黒なフレームにおける先頭の e5 はどうやら必要な映像データの一部だったようです。 JPEGなんもわからん。

映像を受信する

あとは実装するだけです。最近仕事でGoを書いているのでGoのライブラリを使って映像を受信して表示するプログラムを作ってみます。 UDPサーバ自体はとても手軽に書けたのと、GUIライブラリは pixel というものを使いました。

ドクターイエローから受信した映像を表示するプログラムがあり、それをさらにドクターイエローで撮影している様子を写真に撮った

ここまで長かったですが、受信した映像を動画として出力することができました。 機能は全く充実していませんが、映像受信用のプログラムはこちらで公開しています。

あとがき

振り返ってみると、電車の制御(止まるとか走るとか)のプロトコルについてはパケットキャプチャができた途端に把握することができましたが、 映像についてはかなり時間がかかりましたがとても達成感がありました。 途中でうまくパケットの規則性を見つけたり、特に真っ黒な映像からはいいヒントが得られたのが面白いポイントなのと、 JPEGファイルそのものについて学ぶいい経験になりました。

それと、本文には書ききれませんでしたが、jpdump というJPEGファイルの中身をセグメントごとに分けて表示するツールがJPEGファイルの理解にとても役に立ちました。

告知

2024年3月10日(日)のOpen Source Conference 2024 Tokyo/Spring にて、プラレール計算機の展示をさせていただく予定です。 場所は浅草、入場は無料のオープンソースソフトウェアのコミュニティが集まるイベントです。 昨年から再び活発になったオープンソースの祭典にぜひお越しください!


  1. 試しにチャネル2に対してスニファを実行したらうまく保存できたので、チャネル1に対してはなんらかの制約があるのかもしれません。なにかご存知の方がいらっしゃったら教えていただけると嬉しいです。 

  2. 実際にはSOF(フレームヘッダー)セグメントにどの量子化テーブルを使うかが、SOS(スキャンヘッダー)にどのハフマンテーブルを使うか、の指定があります。