タグ: u-blox

  • ArduSimple NEO-D9C Plugin で QZSS L6 raw を Linux から取り出すまでに踏んだ 3 つの罠

    要約

    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 ペアで同じ情報を表す。さらに errStatuscno が追加されて、パーサで観測品質が直接見えるようになった。

    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 公開鍵検証層を実装する。「自前で受信機を作る」モチベーションについてはまた別の記事で。

IP: 取得中...
216.73.217.150216.73.217.150