パケットキャプチャで電波の中身を見る ─ DMR フレームの読み方
カテゴリ: ハマりどころ / OpenCCVoice / DVSwitch タグ: DMR, DMRD, tcpdump, パケット解析, Python
前回、MD-619 のハング問題を「TGIF が先頭無音を転送していない」ことで解決しました。この決定的発見は、Pi 上で tcpdump を回して自作の解析器で分析した結果でした。今回はこの解析器 dmrd_analyze.py の作り方と使い方を紹介します。
なぜ自作したか
DMR フレームを解析するツールは既存にもあります。Wireshark にはプラグインがありますし、専用のデコーダも幾つか存在します。しかし、いずれも Windows 環境前提だったり、GUI が必要だったり、目的の情報(ヘッダの有無、埋め込みデータの内容、ストリーム比較)が取り出しにくかったりします。
Pi の Bookworm 上で外部依存なしに動く、pcap を読んで自分の解析軸で集計する Python スクリプトが欲しかった。pcap の形式は単純なので、自前パースの方が結局早いと判断しました。標準ライブラリだけで動くので、Pi にそのまま置いて使えます。
DMRD プロトコルの基本
Homebrew プロトコル(BrandMeister や TGIF が使っているもの)の DMR フレームは、UDP パケットとして送られます。ポートは 62031。1フレームは 53 バイト以上あり、こう組まれています。
"DMRD" (4B) + seq(1B) + src(3B) + dst(3B) + repeaterID(4B) + bits15(1B)
+ streamID(4B) + payload(33B) + reserved(2B)
- src / dst: 発信元と宛先の DMR ID(TG 番号を含む)
- bits15: フレーム種別を表すビット群。上位2bitでスロット、次の2bitでフレームタイプ、下位4bitでデータタイプ
- payload 33B: これが実際の音声+制御データ
フレームタイプには「音声同期(A)」「音声(B/C/D/E/F)」「データ同期(VoiceLCHeader、Terminator など)」の区別があります。正常な DMR ストリームは「VoiceLCHeader → A → B → C → D → E → F → A → B → … → Terminator」の順で流れます。
中央 48bit に何が入っているか
音声フレーム(B〜F)の 264bit ペイロードのうち、中央 48bit に埋め込み情報が入っています。この48bit は「EMB(Embedded Signalling)」と「embedded LC の断片」から成り、4フレームぶんを集めて 128bit の埋め込み LC を復元します。
埋め込み LC には何が入っているか。
- GroupLC / PrivateLC: 発信元 ID と宛先の情報(音声ヘッダが失われても同期用に使える)
- Talker Alias Header / Block1 / Block2 / Block3: 発信者のコールサイン文字列
つまり ヘッダが TGIF に食われても、この埋め込み LC を組み立てられれば受信機は情報を復元できるはずです。しかし復号にはデインタリーブ、Hamming(16,11,4) FEC、5bit CRC の複雑な処理が必要で、これを Python で書き直しました。実装は MMDVMHost(DMR ゲートウェイのオープン実装)から移植しています。
実際の解析結果
前回の記事で紹介した通り、通常局と OCV のストリームを比較すると:
通常局 JJ2ZAR の折り返し: ヘッダ:3 終端:1 総フレーム:42 並び: VVVAAAbbbcccdddeeefff...T OpenCCVoice の折り返し: ヘッダ:0 終端:3 総フレーム:259 ⚠️ ヘッダ無し! 並び: AbcdefAbcdefAbcdef...
ここまでは前回の話。今回はさらに埋め込み LC のデコード結果も見せます。
埋め込みLC/TA デコード:
GroupLC dst=44833 src=4402396 x23
★ Talker Alias = 'JJ2YYK AichiPref Japan MMDV' (format=ISO8859-1, len=27)
ちゃんと JJ2YYK として送っている。 しかも Talker Alias の中身は MMDVM_Bridge.ini の Callsign + Location + Description そのものです。つまり OCV のストリームは電波レベルでは完全に正しい。問題は「途中参加を強要される受信機がどこまで解釈できるか」なのでした。
使い方
Pi に tcpdump が入っていれば、キャプチャは1行です。
sudo tcpdump -i any -Z root -w /tmp/dmr.pcap udp port 62031 # → 気になる送信を1〜2回入れて Ctrl+C python3 dmrd_analyze.py /tmp/dmr.pcap
これだけで、各ストリームの構造、ヘッダ有無、埋め込み LC の中身が全て見えます。
Pi-Star に tcpdump が入っていない場合は、こう入れます。
rpi-rw sudo apt update sudo apt install -y tcpdump
-Z root は tcpdump が起動後に権限を落とす仕様に対応するもので、これが無いと /tmp に書けないことがあります。-i any は全インタフェース対象で、Pi-Star + DVSwitch のオールインワン構成では loopback を含めた通信も拾うために必要です。
何を見るべきか
解析器の出力で真っ先に見るべきは、「送信側のフレーム数」と「配送側のフレーム数」の差です。
■ #4 [TX->net] 総フレーム:281 ヘッダ:2 終端:3 ← 送った量 ■ #5 [net->RX] 総フレーム:259 ヘッダ:0 終端:3 ← 戻ってきた量
差 = 281 – 259 = 22 フレーム = 0.44 秒。これが TGIF に「食われた」時間です。V1.83 以前だと差が 51 フレーム(約3秒)でしたが、V1.89 以降の対策で数フレーム以内まで縮められました。
次に見るのは ⚠️ ヘッダ無し! の警告。これが出ている限り、その配送先の受信機は「途中参加」を強要されます。MD-619 のような一部の機種はここで固まります。
そして最後に ★ Talker Alias。ここに自局のコールサインが正しく載っていれば、送信は電波レベルで完璧です。もし別のコールサインが出るなら、Analog_Bridge のメタデータ処理が疑われます。
🏛 JJ2YYK あいちデジタルコミュニケーションハムクラブ
教訓: DVSwitch 系で「なんで俺の局だけ変な動きをするんだ」と思ったら、tcpdump と自作解析器の出番です。ダッシュボード表示は加工された情報なので、判断材料にできません。RF で流れているビット列そのものを見るしかありません。
dmrd_analyze.py と Talker Alias デコーダの emblc.py は、この一連のデバッグで作った副産物ですが、汎用ツールとしても使えるものになりました。同じ症状に遭遇した方はぜひ使ってみてください。
続きの話(AMBE ボコーダとの相性、Talker Alias 引きずり、並行制御の落とし穴)は次回以降で。