要約
ArduSimple simpleRTK3B Compass (UM982) に AS-XBEE-LBAND-NEOD9C-SMA (u-blox NEO-D9C-00B-02) を載せて、QZSS L6 帯の生ナビゲーションメッセージ UBX-RXM-QZSSL6 を Linux ホストに引き出すまで、想定外の罠を 3 つ踏んだ。同じ構成で QZSS L6 を触る人の時間を節約するために記録を残す。
- 罠 1: 「XBee ソケットに刺せば本体 USB-C で取れる」は 嘘。Plugin 本体に付いている micro USB を PC に直結するのが正解
- 罠 2: u-blox D9 QZS の IDD が v1 に上がっていて payload が 262 → 264 byte に。古い前提で書いた parser は version field の時点で reject される
- 罠 3: 屋内窓際だと bytes は届くが
errStatus=Erroneous連発。生バイト dump 目的なら十分、CLAS / QZNMA デコードは屋上アンテナが要る
環境
- 受信機: ArduSimple
simpleRTK3B Compass(UM982 ベース、dual-antenna RTK) - L6 オプション: ArduSimple
AS-XBEE-LBAND-NEOD9C-SMA(u-blox NEO-D9C-00B-02、CLAS / QZNMA) - アンテナ: u-blox
ANN-MB2-00× 1 (L1/L2/L5/L6 全バンド、active、bias-T 内蔵) - ホスト: Ubuntu 24.04 + Rust (serialport crate で UART 直読み)
- 用途: QZSS の CLAS センチ補正と QZNMA 信号認証を ホスト側で公開鍵検証する自社受信機の前段検証
罠 1: USB-C 経由ルートは詰まる、micro USB が最短
事前に procurement メモにこう書いていた:
NEO-D9C Plugin は UART のみ (USB なし)。simpleRTK3B Compass の XBee ソケットに刺せば本体の DIP スイッチで XBee UART を USB-C 側にルーティングできる。
これは ArduSimple の Plugin datasheet と u-blox NEO-D9C のチップ仕様書 (USB ピンは存在するがソケットに引き出されていない、と読める) から導いた結論で、頭の中ではきれいなはずだった。
実機で simpleRTK3B Compass + Plugin を組んで USB-C を 2 本繋いだら、lsusb には FTDI FT231X が一個出てくる。OK と思って:
$ stty -F /dev/ttyUSB0 9600 raw -echo $ timeout 5 cat /dev/ttyUSB0 | wc -c 0
9600 / 38400 / 57600 / 115200 / 230400 / 460800 / 921600 と全 baud で 0 byte。FTDI までは enumerate されているのに、その先の UART に何も流れていない。本体の DIP スイッチが XBee → USB ではなく XBee → UM982 にルーティングしているか、別経路かは結局未解明。
ハマって写真を見直していたら、Plugin 基板の真ん中に micro USB コネクタが付いているのに気付いた。Plugin の datasheet にも procurement メモにも「USB なし」と書いたのに、現物には micro USB が刺さる穴がある。試しに micro USB ケーブル (データ対応) を Plugin に直差しすると:
$ dmesg | tail [...] usb 1-3: New USB device found, idVendor=1546, idProduct=01a9 [...] usb 1-3: Product: u-blox GNSS receiver [...] cdc_acm 1-3:1.0: ttyACM0: USB ACM device
VID 0x1546 は u-blox AG、PID 0x01a9 は u-blox generation 9 (NEO-D9C もここ)。/dev/ttyACM0 として u-blox CDC ACM デバイスが即座に見える。UART よりずっと素直で速い。
教訓: チップ datasheet の “USB なし” は「チップ単体での話」かつ「IDD バージョン依存」かもしれない。Plugin / 評価ボードのリビジョンによっては USB-UART ブリッジを足して micro USB に引き出してくれている。現物の基板写真を眺めて全コネクタを数えるべき。
罠 2: UBX-RXM-QZSSL6 が version 0x00 → 0x01 で payload 構造ごと差し替わってた
parser を当てたら全フレームが “unsupported version: 0x1” で reject。コードを書いた時に参照した古い IDD (version 0x00、payload 262 byte、channelName が ASCII ‘a’ .. ‘d’ で channel を表す) のレイアウトと、実機 firmware が吐く version 0x01 (payload 264 byte、channel 情報が bitfield) が違う。
最新の u-blox D9 QZS 1.01 Interface Description (UBX-21031777-R02) に当たって layout を取り直す:
offset size field 0 1 version (= 0x01) 1 1 svId 2 2 cno (raw × 2^-8 = dBHz) 4 4 timeTag (ms, ローカル時刻タグ) 8 1 groupDelay (ns, L2 に対する L6 群遅延) 9 1 bitErrCorr (Reed-Solomon で訂正したビット数) 10 2 chInfo (bitfield、下記) 12 2 reserved0 14 250 msgBytes (生 L6 メッセージ)
chInfo bitfield (X2、リトルエンディアン):
- bits 9..8:
chn(受信機チャネル 0/1) - bit 10:
msgName(0=L6D, 1=L6E) - bits 13..12:
errStatus(0=unknown, 1=error-free, 2=erroneous) - bits 15..14:
chName(0=channel A, 1=channel B)
旧 v0 では「channel 名 = 1 byte ASCII で ‘a’/’b’ = L6D ch1/ch2, ‘c’/’d’ = L6E ch1/ch2」だったのが、v1 では (msgName, chName) の 2 bit ペアで同じ情報を表す。さらに errStatus と cno が追加されて、パーサで観測品質が直接見えるようになった。
Rust の parser を書き直す:
pub const UBX_RXM_QZSSL6_PAYLOAD_LEN: usize = 264;
pub const L6_RAW_LEN: usize = 250;
impl QzssL6Frame {
pub fn parse(payload: &[u8]) -> Result<Self, QzssL6Error> {
if payload.len() != UBX_RXM_QZSSL6_PAYLOAD_LEN {
return Err(QzssL6Error::BadLength(payload.len()));
}
if payload[0] != 0x01 {
return Err(QzssL6Error::UnsupportedVersion(payload[0]));
}
let sv_id = payload[1];
let cno_dbhz = (u16::from_le_bytes([payload[2], payload[3]]) as f32) / 256.0;
let time_tag_ms = u32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]);
let ch_info = u16::from_le_bytes([payload[10], payload[11]]);
let msg_name = (ch_info >> 10) & 1;
let err_status = (ch_info >> 12) & 0b11;
let ch_name = (ch_info >> 14) & 0b11;
let band = match (msg_name, ch_name) {
(0, 0) => L6Band::L6dCh1,
(0, 1) => L6Band::L6dCh2,
(1, 0) => L6Band::L6eCh1,
(1, 1) => L6Band::L6eCh2,
_ => L6Band::L6dCh1, // reserved
};
let raw = payload[14..14 + L6_RAW_LEN].to_vec();
// ...
}
}
教訓: u-blox の IDD はリビジョン番号が静かに上がる。自分の parser を書く時は version field を厳密に拒否する側に倒して、拒否ログから IDD 追従漏れを検知できるようにしておいた方がいい。今回は厳密 reject にしておいたおかげで 5 秒で気付いた。緩く受けてしまっていたら、後段の CLAS / QZNMA デコーダで意味不明な落ち方をして探すのに半日溶かしていた。
罠 3: 屋内窓際だと bytes は出るが errStatus=Erroneous で CRC が通らない
parser を直して走らせると、60 秒間で:
final stats: L6D ch1=60 ch2=60, L6E ch1=0 ch2=0, total=120
QZS-2 (svId=2) と QZS-7 (svId=7) から L6D の A/B 両チャネルが綺麗な 1Hz で届く。CNO は 32〜34 dBHz、bitErrCorr は 0。L6E (QZNMA) は今回ゼロだが、QZNMA は内閣府側で試験運用中のフェーズなので、観測タイミングや SV 依存で見えないことがある (運用情報を確認する必要あり)。
ただし全フレームで errStatus=2 (Erroneous)。bytes は届いているが L6 フレーム内部の CRC が pass していない、という状態。屋内の窓際 + bias-T 給電のパッチアンテナ机置きでは、CNO が「届くが decode 品質には足りない」レンジに入りやすい。ANN-MB2-00 を屋外の開けた場所に持ち出した別検証では ErrorFree が立つ。
用途別の判断:
- 生バイト dump、parser 検証、UBX フレーミング検証: 屋内窓際で十分。バイト列は正しい構造で届く
- CLAS デコード (raw → SSR/OSR 補正適用): CRC pass が前提なので屋外アンテナ必須
- QZNMA 公開鍵検証: 同上、加えて L6E が実際に放送されているタイミングを CAO 公開資料で確認
教訓: GNSS 評価はとりあえず屋内窓際から始めて「電気は来てる、UART は喋ってる」を確認しつつ、本格的な信号品質評価は早めに屋外に移すこと。「bytes が届く」と「decode が成立する」は別の状態であって、errStatus / CRC フラグはそれを区別してくれる重要な指標。
まとめ
- ArduSimple NEO-D9C Plugin で L6 raw 取り出しは、Plugin 本体の micro USB に直差しが最短。simpleRTK3B Compass の USB-C ルートは DIP スイッチ周りで詰まりやすい
UBX-RXM-QZSSL6は version 0x01、payload 264 byte が現行 (UBX-21031777-R02)。自前 parser を書くなら version 厳密チェックで IDD 追従漏れを検知できるように- 屋内窓際は
errStatus=Erroneousが普通。decode 成立を見たいなら屋外
次は屋外でアンテナを開けた空に向けて errStatus=ErrorFree を取り、その上で CLAS decoder と QZNMA 公開鍵検証層を実装する。「自前で受信機を作る」モチベーションについてはまた別の記事で。
コメントを残す