DMR パケット解析 実践ガイド

対象読者: OpenCCVoice for DVSwitch または類似の DVSwitch 系ボットで、症状の原因が「ログや設定を見ても分からない」ところまで来た人。「電波レベルまで降りて確認する」ためのマニュアル。

必要環境: Raspberry Pi (Pi-Star ベースを想定)、Python 3、tcpdump、SoX(既に入っているはず)。追加の外部 Python ライブラリは不要。

セットアップ時間: 約15分(ツール導入と初回キャプチャまで)


目次

  1. なぜパケットキャプチャなのか
  2. ツールの導入
  3. キャプチャの基本操作
  4. 検証シナリオ集
  5. 解析器の出力の読み方
  6. 判定チェックリスト
  7. よくある落とし穴
  8. 証拠として残す形式

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.pyemblc.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.pcappcap 形式で保存✕ 解析のために保存が必要
udp port 62031DMR 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 終端:3VoiceLCHeader と 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. よくある落とし穴」で扱ったパターンは、ほとんどが「環境の些細な違い」から生まれます。上手くいかないときは、まずこの章から見直してください。

JJ2YYK
  • JJ2YYK

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です