DMR パケット解析 実践ガイド
対象読者: OpenCCVoice for DVSwitch または類似の DVSwitch 系ボットで、症状の原因が「ログや設定を見ても分からない」ところまで来た人。「電波レベルまで降りて確認する」ためのマニュアル。
必要環境: Raspberry Pi (Pi-Star ベースを想定)、Python 3、tcpdump、SoX(既に入っているはず)。追加の外部 Python ライブラリは不要。
セットアップ時間: 約15分(ツール導入と初回キャプチャまで)
目次
- なぜパケットキャプチャなのか
- ツールの導入
- キャプチャの基本操作
- 検証シナリオ集
- 解析器の出力の読み方
- 判定チェックリスト
- よくある落とし穴
- 証拠として残す形式
1. なぜパケットキャプチャなのか
DVSwitch 系のトラブルシューティングでは、以下の観測点が段階的にあります:
| 観測点 | 分かること | 分からないこと |
|---|---|---|
ボットのログ (journalctl) | 何を送ろうとしたか | 実際に何が RF に乗ったか |
| Analog_Bridge のログ | USRP パケット受信の記録 | DMR フレーム化された後の中身 |
| MMDVM_Bridge のログ | 送受信の要約 | フレーム内部の埋め込みデータ |
| Pi-Star ダッシュボード | 表示用の要約情報 | 生ストリームの詳細 |
| パケットキャプチャ | フレーム内の全ビット | ─(ここが最終手段) |
「ダッシュボードでは正常なのに、無線機の挙動がおかしい」という状況では、ダッシュボードは既に加工された情報を見せているため判断材料にできません。RF に乗っているビット列そのものを見るしかありません。
具体例:本デバッグでは、ダッシュボードでは OCV の送信が Callsign=JJ2YYK / BER 0.0% / ロス 0% と完全に見えていましたが、パケットキャプチャで解析すると VoiceLCHeader が全部 TGIF に食われている ことが判明しました。これはダッシュボードでは絶対に分からない情報でした。
2. ツールの導入
2.1 tcpdump のインストール
Pi-Star 環境では標準では入っていないことがあります。
# Pi-Star の場合、まず read-write に
rpi-rw
# インストール
sudo apt update
sudo apt install -y tcpdump
# 確認
tcpdump --version
出力例:
tcpdump version 4.99.1
libpcap version 1.10.1 (with TPACKET_V3)
2.2 解析ツールの配置
Pi の任意のディレクトリに以下の3ファイルを置きます。同じディレクトリに置くのが必須(dmrd_analyze.py は emblc.py を相対 import します)。
mkdir -p ~/dmr-tools
cd ~/dmr-tools
# ファイルを転送(scp / GitHub / USB など任意)
# - dmrd_analyze.py (pcap → ストリーム分析)
# - emblc.py (埋め込み LC / Talker Alias デコーダ)
# - capture_dmrd.sh (キャプチャ支援シェル)
chmod +x capture_dmrd.sh
2.3 動作確認
cd ~/dmr-tools
python3 dmrd_analyze.py --help 2>&1 | head -3
usage: python3 dmrd_analyze.py <capture.pcap> と出れば OK。
もし emblc.py が正しくロードされているかを確認したい場合は、テスト実行時の1行目を見ます:
dmrd_analyze v1.3 (LC/TAデコード: 有効) ← これが出ればOK
dmrd_analyze v1.3 (LC/TAデコード: 無効 (emblc.py が同じディレクトリにありません))
3. キャプチャの基本操作
3.1 最小コマンド
sudo tcpdump -i any -Z root -w /tmp/dmr.pcap udp port 62031
各オプションの意味:
| オプション | 意味 | 省略できるか |
|---|---|---|
-i any | 全インタフェース(loopback含む) | ✕ ローカルの Analog_Bridge↔MMDVM_Bridge 通信を拾うため必須 |
-Z root | 権限を落とさない | ✕ /tmp などへの書き込み権限確保のため |
-w /tmp/dmr.pcap | pcap 形式で保存 | ✕ 解析のために保存が必要 |
udp port 62031 | DMR Homebrew プロトコルのポート | ✕ フィルタしないと巨大化 |
停止方法: Ctrl+C。ターミナルが「N packets captured」を表示して終わります。
3.2 支援シェルスクリプト
前述の tcpdump を毎回打つのが面倒なら、capture_dmrd.sh を使ってください:
sudo ./capture_dmrd.sh test1 60
# ^^^^^^ 出力名 ^^ 秒数
スクリプト本体(capture_dmrd.sh):
#!/bin/bash
# capture_dmrd.sh — DMR Homebrew (UDP/62031) キャプチャ支援スクリプト
# 使い方: sudo ./capture_dmrd.sh [出力名] [秒数]
# 例: sudo ./capture_dmrd.sh test1 60
set -e
OUTNAME="${1:-dmr}"
DURATION="${2:-60}"
OUTDIR="/tmp"
OUTFILE="${OUTDIR}/${OUTNAME}_$(date +%Y%m%d_%H%M%S).pcap"
if ! command -v tcpdump >/dev/null 2>&1; then
echo "❌ tcpdump がインストールされていません"
echo " Pi-Star なら: rpi-rw && sudo apt update && sudo apt install -y tcpdump"
exit 1
fi
FREE_TMP=$(df /tmp | tail -1 | awk '{print $4}')
if [ "$FREE_TMP" -lt 10000 ]; then
echo "⚠️ /tmp の空きが少ないです (${FREE_TMP}KB)"
fi
echo "───────────────────────────────────────────────"
echo " DMRD キャプチャ開始"
echo " 出力: ${OUTFILE}"
echo " 時間: ${DURATION} 秒(Ctrl+C で早期停止)"
echo "───────────────────────────────────────────────"
echo ""
echo "▶ この間に検証したい送信を発生させてください:"
echo " (a) OCV に応答させる (カーチャンクなど)"
echo " (b) 通常局の送信を1回入れる (比較対象)"
echo ""
timeout "${DURATION}s" sudo tcpdump -i any -Z root -w "${OUTFILE}" udp port 62031 2>&1 | tail -5 || true
echo ""
echo "───────────────────────────────────────────────"
echo "✅ キャプチャ完了: ${OUTFILE}"
if [ -f "${OUTFILE}" ]; then
SIZE=$(du -h "${OUTFILE}" | awk '{print $1}')
echo " サイズ: ${SIZE}"
fi
echo "───────────────────────────────────────────────"
echo ""
echo "▶ 解析: python3 dmrd_analyze.py ${OUTFILE}"指定時間で自動停止するので、席を離れて別の作業をしやすくなります。
3.3 キャプチャファイルの命名規則(推奨)
複数の検証を回す場合、後で見返せるよう命名を工夫します:
# 例:状況・版数・日時を入れる
sudo ./capture_dmrd.sh v184_before 120
sudo ./capture_dmrd.sh v184_after 120
sudo ./capture_dmrd.sh v189_hum 120
これで /tmp/v184_before_20260706_143012.pcap のような名前で残ります。
4. 検証シナリオ集
現場のデバッグで実際に有効だったシナリオを、目的別にまとめます。
シナリオA: 「OCV の送信が何かおかしい」の一般調査
目的: OCV と通常局を比較して、OCV 固有の異常があるかを見る。
# キャプチャ開始
sudo ./capture_dmrd.sh general 120
# キャプチャ中に以下を発生させる:
# 1. 通常局から1回カーチャンク or 短い送信を入れる (10秒くらい)
# 2. 通常局から短い QSO (5-10秒の音声送信)
# 3. OCV に応答させる(別の局からカーチャンク or 「マイク開けて JJ2YYK-の応答を誘発)
# 4. 追加で OCV に2〜3回応答させる
# 停止して解析
python3 dmrd_analyze.py /tmp/general_*.pcap
見るポイント:
- 通常局と OCV でヘッダ数・終端数が違わないか
- 埋め込み断片のパターンが違わないか
- Talker Alias が正しく OCV のコールサインになっているか
シナリオB: 「MD-619 が固まる」→ TGIF 無音スキップの検証
目的: OCV の送信フレーム数と、戻ってきた(配送された)フレーム数を比較して、TGIF がどのくらい削っているかを測る。
sudo ./capture_dmrd.sh tgif_gate 90
# キャプチャ中:
# 1. OCV に応答させる(他局からカーチャンク)
# 2. 応答完了を待つ
# 3. もう一度 OCV に応答させる(1回目と条件を揃える)
python3 dmrd_analyze.py /tmp/tgif_gate_*.pcap
期待される出力:
■ #? [TX->net udp XXXXX→62031] src=<自局DMR-ID>
ヘッダ:2 終端:3 総フレーム:281
■ #? [net->RX udp 62031→YYYYY] src=<自局DMR-ID>
ヘッダ:0 終端:3 総フレーム:259 ⚠️ ヘッダ無し!
判定:
- 差 = TX 総フレーム − net→RX 総フレーム
- 差が ~51 フレーム前後 (約3秒) → 対策前 (V1.83以前) の症状。TGIF が先頭無音を食っている
- 差が ~22 フレーム前後 (約1.3秒) → V1.84 適用済み。まだゲートに食われているがヘッダは通り始めているかもしれない
- 差が 数フレーム以内 → V1.89以降の対策が効いている
シナリオC: 「頭の音質がおかしい」→ AMBE 経路の検証
目的: リード音の性質(ノイズかトーンか)と、AMBE の反応を確認する。
このシナリオは、実は tcpdump ではなく 無線機での聴感テストが主で、tcpdump は「送出データが期待どおりか」の確認に使います。
sudo ./capture_dmrd.sh audio_check 60
# OCV に1回だけ応答させる
python3 dmrd_analyze.py /tmp/audio_check_*.pcap
見るのはフレーム数ではなく、send 側の埋め込みパターン数:
埋め込み断片 ユニーク21種 上位: 1100000000e2x46, ...
種類が非常に少ない (2〜3種) 場合は、送出データがほとんど同じ内容の繰り返し(=合成が崩れている、無音が長すぎる等)を示唆します。
シナリオD: 「表示が別の局のコールサインになる」→ Talker Alias の実物確認
目的: OCV が送っている Talker Alias の実際の中身をデコードする。
sudo ./capture_dmrd.sh ta_check 90
# キャプチャ中:
# 1. 他局 (仮に JJ2ZAR とする) から1回カーチャンク
# 2. OCV の応答完了まで待つ
python3 dmrd_analyze.py /tmp/ta_check_*.pcap
期待される出力:
OCV の TX→net セグメント(src=<自局DMR-ID>)に、こう出るはず:
埋め込みLC/TA デコード:
GroupLC dst=44833 src=4402396 x23
TA_Header data=... x6
TA_Block1 data=... x6
TA_Block2 data=... x6
TA_Block3 data=... x5
★ Talker Alias = 'JJ2YYK AichiPref Japan MMDV' (format=ISO8859-1, len=27)
判定:
★ Talker Aliasが 自局コールサイン → 送信は完全に正しい。表示引きずりは受信機側の問題★ Talker Aliasが カーチャンクした局のコールサイン → Analog_Bridge のメタデータが上書きされている。V1.90 の対策で改善する可能性あり★ Talker Aliasの行が 出ない → 埋め込み LC の CRC エラー、または解析器の emblc.py が読めていない
シナリオE: 「sox FAIL が出る」→ 並行制御の確認
目的: これは tcpdump ではなくログの分析ですが、シナリオ集の完全性のために記載。
# journalctl の該当時刻前後を見る
sudo journalctl -u dvswitch-bot --since "10 minutes ago" | grep -E "Precache|SoX|Sending|Generate|Cached"
# キャッシュディレクトリの現状
ls -la /dev/shm/ocv_reply_cache_$(systemctl show dvswitch-bot -p MainPID --value)/
判定:
No such file or directory→ V1.93 未適用、または /dev/shm 上の異常Precache X readyが並走する応答と重なっている → 並行制御バグ(V1.91以前)- 発生している版が V1.93 以上なら別の原因を疑う
5. 解析器の出力の読み方
dmrd_analyze.py の出力を、実際の例で解説します。
5.1 出力例(正常な通常局と、対策前の OCV の比較)
dmrd_analyze v1.3 (LC/TAデコード: 有効)
pcap: /tmp/dmr_20260704.pcap (送信セグメント: 6)
========================================================================
■ #1 t+ 0.0s ( 0.9s間) Stream 4e6dc7a9 [net->RX udp 62031→36677]
src=4402519 dst=TG44833 TS2
ヘッダ:1 終端:1 総フレーム:14
並び: VAbcdefAbcdefT
埋め込み断片 ユニーク9種 上位: 1100000000e2x2, 131803140691x1, ...
埋め込みLC/TA デコード:
GroupLC dst=44833 src=4402519 x1
■ #4 t+ 2.4s ( 16.3s間) Stream 8882a22c [TX->net udp 63032→62031]
src=4402396 dst=TG44833 TS2
ヘッダ:2 終端:3 総フレーム:281
並び: VVAbcdefAbcdefAbcdef...
埋め込み断片 ユニーク21種 上位: ...
埋め込みLC/TA デコード:
GroupLC dst=44833 src=4402396 x46
TA_Header data=... x6
★ Talker Alias = 'JJ2YYK AichiPref Japan MMDV' (format=ISO8859-1, len=27)
■ #5 t+ 3.6s ( 15.2s間) Stream 8882a22c [net->RX udp 62031→36677]
src=4402396 dst=TG44833 TS2
ヘッダ:0 終端:3 総フレーム:259 ⚠️ ヘッダ無し!
並び: AbcdefAbcdefAbcdef...
埋め込み断片 ユニーク21種 上位: ...
埋め込みLC/TA デコード:
GroupLC dst=44833 src=4402396 x42
★ Talker Alias = 'JJ2YYK AichiPref Japan MMDV' (format=ISO8859-1, len=27)
5.2 各項目の意味
| 項目 | 意味 |
|---|---|
Stream 4e6dc7a9 | ストリームID (32bit)。1回の送信を一意に識別 |
[net->RX udp 62031→36677] | 方向とポート。「net→RX」は TGIF から Pi へ、「TX→net」は Pi から TGIF へ |
t+ 2.4s (16.3s間) | キャプチャ開始からの経過時刻と、この送信の継続時間 |
src=4402396 dst=TG44833 TS2 | 発信元 DMR ID、宛先 TG、タイムスロット |
ヘッダ:2 終端:3 | VoiceLCHeader と TerminatorLC の数 |
総フレーム:281 | 全 DMRD パケット数 |
並び: | フレーム種別の時系列。V=VoiceLCHeader, T=Terminator, A=voice sync, b-f=voice B-F |
埋め込み断片 ユニークN種 | 音声フレーム中央48bit の種類数。少なすぎると内容が薄い |
埋め込みLC/TA デコード: | 128bit の埋め込み LC を復元してデコードした結果 |
★ Talker Alias | 発信者のコールサイン文字列(判決材料) |
5.3 警告マーク
- ⚠️ ヘッダ無し! — VoiceLCHeader が 0 個。途中参加ストリームで受信機が困る
- ⚠️ 同一ID N回目! — 同じストリームID で複数の送信が観測された(通常は毎回違うランダムID になる)
6. 判定チェックリスト
キャプチャを取ったら、以下を順にチェックします:
6.1 まず全体の健全性
□ セグメント数は妥当か(送信した数 × 経路数 くらい)
□ TX→net のフレーム数が想定と合うか
(時間 × 50 = フレーム数目安、例: 16秒送信 → 約800フレーム)
注意: Pi-Star + DVSwitch のオールインワン構成では、同じストリームが複数のポート宛に配送されるため、net→RX 側のセグメント数が TX→net より多くなります(普通)。
6.2 対策前後の比較(TGIF ゲート)
□ TX→net の「総フレーム」数を控える (例: 281)
□ net→RX の「総フレーム」数を控える (例: 259)
□ 差 = 281 - 259 = 22 フレーム
□ 差 × 0.02 秒 = 削られた時間 (例: 0.44秒)
対策の段階:
| 削られた時間 | 意味 |
|---|---|
| ~3秒 (~150フレーム相当) | 対策前。V1.83以前 |
| ~1.3秒 (~65フレーム) | V1.84 適用(ノイズ充填のみ) |
| ~0.5秒以下 | V1.89 以降(トーン化+助走) |
6.3 ヘッダの生存
□ net→RX 側のヘッダ数を確認
□ 通常局と比較して同数か
通常局は 1〜3 ヘッダが普通(1回のカーチャンクで最低1つは付く)。OCV の net→RX で ヘッダ:0 が繰り返し出るなら、まだヘッダが食われています。
6.4 Talker Alias の内容
□ ★ Talker Alias 行が出ているか
□ 内容が自局コールサインで始まっているか
□ format と len が MMDVM_Bridge.ini の設定と整合しているか
7. よくある落とし穴
7.1 tcpdump が権限エラーになる
tcpdump: /tmp/dmr.pcap: Permission denied
原因: tcpdump は起動後に権限を落とす仕様で、書き先に書けなくなることがあります。
対策: -Z root を必ず付ける。
7.2 pcap が空
キャプチャ中にトラフィックがなかった or フィルタが間違っている。
# トラフィックの生存確認
sudo tcpdump -i any -c 5 udp port 62031
# 5パケット来れば正常
7.3 解析器が古い挙動をする
出力の1行目にバージョン表示が出ます:
dmrd_analyze v1.3 (LC/TAデコード: 有効)
v1.3 未満、または「無効」の表示が出る場合:
- ファイルの転送に失敗している
- emblc.py が同じディレクトリにない
- 古い版がキャッシュされている(
__pycache__を削除して再実行)
7.4 セグメント分割の誤解
同じストリームID の送信が同一 pcap 内で複数回現れると、v1.1 以降は 2秒のギャップで別セグメントとして分けます。それでも「同じ TX が2セグメントに割れて見える」場合は、実際に2回送っているか、ネットワークの遅延で分かれて見えたか、要注意です。
7.5 pcapng 形式との混同
古い tcpdump は pcap、新しいものは pcapng をデフォルトで出すことがあります。dmrd_analyze.py は旧 pcap のみ対応。エラーメッセージで案内されるので、その場合は変換:
# pcapng → pcap
editcap -F pcap in.pcapng out.pcap
または tcpdump 実行時に明示的に pcap 形式を指定:
sudo tcpdump -i any -Z root -w /tmp/dmr.pcap --time-stamp-precision=micro udp port 62031
8. 証拠として残す形式
パケット解析の結果を、他人(Radioddity のサポート、TGIF 運営、ブログ記事など)に見せる場合の形式です。
8.1 生 pcap ファイル
絶対に消さない。後で追加の解析を要求される可能性があるため、原本を保管します。
# 保管用ディレクトリ
mkdir -p ~/dmr_evidence
cp /tmp/dmr_20260704_*.pcap ~/dmr_evidence/
8.2 解析結果のログ
python3 dmrd_analyze.py /tmp/dmr_20260704.pcap > ~/dmr_evidence/dmr_20260704_analysis.txt 2>&1
このテキストファイルには pcap を持たない相手にも判断材料を渡せる情報が入ります。
8.3 メタデータ
「いつ、どの版で、どういう条件で取ったか」を必ず添える:
cat > ~/dmr_evidence/dmr_20260704_meta.txt << EOF
キャプチャ日時: 2026-07-04 14:30 JST
Pi ホスト名: pi-star-14575
ボット版数: V1.83
DVSwitch 構成: Pi-Star + Analog_Bridge + MMDVM_Bridge
接続先: TGIF network TG44833
シナリオ: OCV に他局からカーチャンクを入れて応答させた
症状: MD-619 で受信スタック発生
EOF
8.4 ブログ・報告用の抜粋
生の出力を全部載せると読みづらいので、要点を抜粋する形式:
■ 通常局 (JJ2ZAR) の net→RX:
ヘッダ:3 終端:1 総フレーム:42
★ Talker Alias = 'JJ2ZAR'
■ OCV の net→RX:
ヘッダ:0 終端:3 総フレーム:259 ⚠️ ヘッダ無し!
★ Talker Alias = 'JJ2YYK AichiPref Japan MMDV'
差の解釈:
- TX 281 → net→RX 259 = 51 フレーム欠落
- 51 × 20ms = 1.02秒
- OCV の先頭には 3秒の無音があり、その一部が TGIF に食われた形跡
付録: 参考コマンドリファレンス
tcpdump 系
# 基本
sudo tcpdump -i any -Z root -w /tmp/dmr.pcap udp port 62031
# 時間制限(60秒で停止)
sudo timeout 60 tcpdump -i any -Z root -w /tmp/dmr.pcap udp port 62031
# 特定の IP からのパケットだけ
sudo tcpdump -i any -Z root -w /tmp/dmr.pcap udp port 62031 and host 192.168.1.66
# キャプチャ中に統計を表示 (verbose)
sudo tcpdump -i any -Z root -w /tmp/dmr.pcap -v udp port 62031
# pcap の中身を人間可読で見る(デバッグ用)
sudo tcpdump -r /tmp/dmr.pcap -X -c 10 udp port 62031
dmrd_analyze 系
# 通常解析
python3 dmrd_analyze.py /tmp/dmr.pcap
# 結果をファイルに保存
python3 dmrd_analyze.py /tmp/dmr.pcap > analysis.txt
# grep で特定の情報だけ抽出
python3 dmrd_analyze.py /tmp/dmr.pcap | grep -E "ヘッダ:|★ Talker|src="
保管系
# 全 pcap を1つの tar.gz に
tar czf dmr_evidence_$(date +%Y%m%d).tar.gz ~/dmr_evidence/
# サイズ確認
du -sh ~/dmr_evidence/*.pcap
このガイドは 2026 年 7 月時点の実務に基づきます。 tcpdump のバージョン、tcpdump 出力形式のデフォルト、Pi-Star の内部構造は将来変わる可能性があります。「7. よくある落とし穴」で扱ったパターンは、ほとんどが「環境の些細な違い」から生まれます。上手くいかないときは、まずこの章から見直してください。