Skip to content
  • Date Created: 2025-12-05
  • Last Modified: 2025-12-05

Progress Log: GNSS Manager Design (GT502MGG Integration)

1. GNSSモジュールについて

GT502MGG 仕様

GT502MGGはマルチコンスステレーション衛星受信機です。 以下の機能を備えています。

受信可能な衛星システム

  • GPS(アメリカ)
  • GLONASS(ロシア)
  • BeiDou(中国)
  • Galileo(ヨーロッパ)
  • SBAS(補強信号)

通信インターフェイス

  • UART: NMEA-0183プロトコル
  • 1PPS: 1秒あたり1パルスの正確な同期信号

電源仕様

  • 供給電圧: 3.3V~5.5V(ESP32互換)

性能

  • コールドスタート: 30~60秒(初期測位時間)
  • ホットスタート: <1秒(キャッシュされた衛星軌道情報を使用)
  • 更新レート: 1Hz(デフォルト)

2. NMEA形式について

NMEA-0183 プロトコル

NMEA(National Marine Electronics Association)は海洋機器の標準フォーマットです。GNSS機器が出力する標準形式として採用されています。

物理層

  • ボーレート: 9600 bps
  • データ形式: 8ビット、パリティなし、ストップビット1(8N1)
  • 行終了: CR + LF(\r\n
  • 初期化: Serial2.begin(9600, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN)

Serial2はUART2(GNSS専用のUART)です。 独立して動作するためUART0(ホストコマンド/USB接続)と同時に処理可能です。

kurikintonsで解析するNMEAセンテンス

$GPRMC - 推奨最小必須情報

  • GPS Recommended Minimum (Specific) Sentence - PositC
  • 時刻、位置、速度、日付を含む最小限のセンテンス

構造:

$GPRMC,hhmmss.ss,status,latitude,N/S,longitude,E/W,speed,heading,ddmmyy,variation,E/W,checksum*xx

フィールド詳細:

  • hhmmss.ss: UTC時刻(時分秒.小数秒
  • status: A(有効)/ V(無効)
  • latitude/longitude: ddmm.mmmm形式(度分)
  • N/S, E/W: 方向指示(北緯/南緯、東経/西経)
  • speed: 速度(ノット単位)
  • heading: 進行方向(0~359度)
  • ddmmyy: 日付(日月年)
  • variation: 磁気偏角(省略可能)
  • checksum: XORチェックサム

実例:

$GPRMC,093426.00,A,3723.2475,N,12158.3416,E,0.0,0.0,231215,,*49
  • 時刻: 09時34分26秒UTC
  • 位置: N37°23.2475' / E121°58.3416'
  • 日付: 2025年12月5日

$GPGGA - 衛星測位システム固定データ

  • GPS Global Positioning System fix Data
  • 3次元位置(高度付き)と衛星数を含むセンテンス

構造:

$GPGGA,hhmmss.ss,latitude,N/S,longitude,E/W,fix_quality,num_satellites,hdop,altitude,M,geoid_height,M,checksum*xx

フィールド詳細:

  • hhmmss.ss: UTC時刻
  • latitude/longitude: ddmm.mmmm形式
  • fix_quality: 固定品質(0~8)
  • 0: 無効
  • 1: GPS通常測位
  • 2: DGPS(差分GPS)
  • 4: RTK固定
  • 5: RTK浮動
  • num_satellites: 測位に使用した衛星数(0~24)
  • hdop: 水平精度(低い値が高精度)
  • altitude: 楕円面上の高度(メートル)
  • geoid_height: ジオイド高度

実例:

$GPGGA,093426.00,3723.2475,N,12158.3416,E,1,08,1.23,45.9,M,-6.2,M,,
  • 固定品質: 1(GPS通常測位)
  • 衛星数: 08個
  • 水平精度: 1.23
  • 高度: 45.9m

NMEA座標形式の変換

NMEA形式の座標(度分)を10進法度に変換する必要があります。

変換公式:

decimal_degrees = floor(nmea_value / 100) + (nmea_value mod 100) / 60.0

:

3723.2475 = 37 + 23.2475 / 60 = 37.3874°N
12158.3416 = 121 + 58.3416 / 60 = 121.9724°E

チェックサム検証

NMEAセンテンスの整合性を確認できます。 チェックサム検証はオプションですが推奨されています。

アルゴリズム:

  • $* の間の全文字のXOR(排他的論理和)
  • 結果を2桁の16進数で表現

:

$GPRMC,093426.00,A,3723.2475,N,...*49
       ↑                           ↑
    開始                      チェックサム

3. GT502MGGの特徴

マルチコンスステレーション対応

1つの受信機で複数の衛星システムから同時に信号を受信できるため

  • より多くの衛星を見通せる
  • 山間部やビル街での測位精度が向上
  • より高速な初期測位(ホットスタート)

正確な時刻同期

1PPS出力(1秒に1回のパルス)により

  • マイクロ秒オーダーの時刻同期が可能
  • 複数デバイス間の時刻合わせ可能
  • 宇宙線検出イベントとGNSS時刻の正確な対応付け

低電力設計

3.3V~5.5Vの広い電圧範囲に対応しており、ESP32から直接給電可能です。


4. GPIO アサイン

kurikintonsでのGT502MGG接続

用途 ESP32 GPIO GT502MGG Pin 説明
UART2 RX GPIO16 TX GNSS データ入力
UART2 TX GPIO17 RX GNSS コマンド出力
1PPS入力 GPIO4 1PPS 秒単位の同期パルス

配置の利点:

  • UART0(GPIO1/3): ホスト通信(コマンド)に専用
  • UART2(GPIO16/17): GNSSデータに専用
  • GPIO4: 汎用入力として利用可能

5. UART2通信の詳細: 送受信内容

受信内容(GNSS → ESP32)

GT502MGG は UART2 (9600 baud) 経由で、NMEA-0183形式のセンテンスを連続送信します。

フォーマット仕様

  • ボーレート: 9600 bps(固定)
  • データフォーマット: 8ビット、パリティなし、ストップビット1(8N1)
  • 行終了: CR + LF(\r\n
  • 更新レート: 1Hz(デフォルト、約100ms間隔)

送信されるNMEAセンテンス

$GPRMC (推奨最小必須情報)

$GPRMC,hhmmss.ss,A,ddmm.mmmm,N/S,dddmm.mmmm,E/W,speed,heading,ddmmyy,variation,E/W,mode,checksum*xx\r\n

例:

$GPRMC,093426.00,A,3723.2475,N,12158.3416,E,0.0,0.0,231215,,*49\r\n

$GPGGA (グローバルポジショニングシステム測位データ)

$GPGGA,hhmmss.ss,ddmm.mmmm,N/S,dddmm.mmmm,E/W,fix_quality,num_sat,hdop,altitude,M,geoid_height,M,age,station_id,checksum*xx\r\n

例:

$GPGGA,093426.00,3723.2475,N,12158.3416,E,1,08,1.23,45.9,M,-6.2,M,,\r\n

フィールド説明

フィールド GPRMC GPGGA 説明
hhmmss.ss UTC時刻(時分秒.小数秒)
ddmm.mmmm 緯度(度分形式)
dddmm.mmmm 経度(度分形式)
N/S, E/W 方向指示
status (A/V) - A=有効, V=無効
speed - 速度(ノット)
heading - 進行方向(0-359度)
ddmmyy - 日付(日月年)
fix_quality - 0=無効, 1=GPS, 2=DGPS, 4=RTK固定, 5=RTK浮動
num_satellites - 測位に使用した衛星数(0-24)
hdop - 水平精度低下係数(低いほど正確)
altitude - 楕円面上の高度(メートル)
geoid_height - ジオイド高度(メートル)

送信内容(ESP32 → GNSS)

基本的には受信のみです。ただし、以下のケースで送信可能:

構成コマンド(proprietaryコマンド)

GT502MGG はマルチコンステレーション受信機なので、以下のような設定コマンドをサポート:

  • 更新レート変更: デフォルト1Hz → 5Hz/10Hzに変更(オプション)
  • 衛星システム選択: GPS/GLONASS/BeiDou/Galileoの有効/無効切り替え
  • 冷間スタート: リセットして衛星捜索を再開
  • シリアル設定: ボーレート変更(ただしESP32とGNSSモジュールの両方で同期必要)

例(proprietary形式):

$PUBX,40,RMC,0,1,0,0,0,0*47\r\n     # RMC出力を有効化
$PUBX,40,GGA,0,1,0,0,0,0*4E\r\n     # GGA出力を有効化

kurikintons での送信戦略

kurikintons では 送信は最小限に設計:

  1. 初期化時の送信: なし(受信のみ)
  2. 実行時の送信: ユーザーコマンド経由のみ
  3. SYNC_TIME - RTC同期時にNMEAデータ読み取り(送信なし)
  4. GET_GNSS_STATUS - 状態クエリ(送信なし、内部状態返却)
  5. 将来拡張: 更新レート変更コマンド

理由:

  • 宇宙線検出がイベント駆動型であり、GNSSの定常的なデータ受信で十分
  • 送受信の複雑化を避け、ファームウェアの安定性を優先

UART2のバッファー管理

  • ハードウェアバッファー: UART2のRXバッファー(ESP32内蔵、深さ128バイト)
  • ソフトウェアアプローチ: gnss_update() を毎ループ呼び出し
  • Serial2.available() で新規データをチェック(ノンブロッキング)
  • Serial2.readStringUntil('\n') で1行読み取り
  • パース後、グローバル状態 g_gnss_data を更新
  • オーバーフロー防止: ポーリングレート100Hz > NMEAレート1Hzなので安全

6. UART通信のフロー

全体アーキテクチャ

graph LR
    subgraph ESP32["kurikintons ESP32"]
        UART0["UART0 Serial"]
        UART1["UART1 Reserved"]
        UART2["UART2 Serial2"]
    end

    UART0 -->|RX: GPIO3| USB["USB serial in"]
    UART0 -->|TX: GPIO1| HOST["Host PC"]
    UART0 -->|Baud: 115200| CMD["Text/binary command protocol"]

    UART1 -->|Status| FLASH["Unavailable flash chip SPI"]

    UART2 -->|RX: GPIO16| GPSRX["GPSRXD data in from GT502MGG"]
    UART2 -->|TX: GPIO17| GPSTX["GPSTXD command out to GT502MGG"]
    UART2 -->|Baud: 9600| GNSS["GNSS NMEA sentence streaming"]
    UART2 -->|Users| GT502["GT502MGG module only"]

    HOST -.->|commands| CMD
    GT502 -.->|GNSS data| GPSRX

Tree表記版:

kurikintons (ESP32)
├── UART0 (Serial)
│   ├── RX: GPIO3 (USB serial in)
│   ├── TX: GPIO1 (USB serial out)
│   ├── Baud: 115200
│   ├── Purpose: Text/binary command protocol
│   └── Users: Host PC, command handlers
│
├── UART1 (Reserved)
│   └── Status: Unavailable (flash chip SPI)
│
└── UART2 (Serial2) ← GNSS Dedicated
    ├── RX: GPIO16 (GPSRXD - data in from GT502MGG)
    ├── TX: GPIO17 (GPSTXD - command out to GT502MGG)
    ├── Baud: 9600
    ├── Purpose: GNSS NMEA sentence streaming
    └── Users: GT502MGG module only

データフロー: 受信側のNMEA処理

graph TD
    GT502["GT502MGG GNSS receiver"]
    UART2BUF["UART2 RX Buffer<br/>Hardware"]
    UPDATE["gnss_manager_update()"]
    AVAILABLE["Serial2.available() check"]
    READ["Serial2.readStringUntil('\n')"]
    PARSE["Parse line in gnss_manager.cpp"]
    RMC["If '$GPRMC'<br/>gnss_parse_rmc()"]
    GGA["If '$GPGGA'<br/>gnss_parse_gga()"]
    RMC_EXTRACT["Extract: latitude<br/>longitude, speed<br/>date/time"]
    GGA_EXTRACT["Extract: fix_quality<br/>satellites, altitude"]
    UPDATE_STATE["Update: g_gnss_data"]
    GLOBAL["Global State: g_gnss_data<br/>updated ~1Hz"]
    READ_API["Available via<br/>gnss_manager_read()"]

    GT502 -->|UART2 @ 9600 bps| UART2BUF
    UART2BUF -->|every ~100ms| UPDATE
    UPDATE --> AVAILABLE
    AVAILABLE --> READ
    READ --> PARSE
    PARSE --> RMC
    PARSE --> GGA
    RMC --> RMC_EXTRACT
    GGA --> GGA_EXTRACT
    RMC_EXTRACT --> UPDATE_STATE
    GGA_EXTRACT --> UPDATE_STATE
    UPDATE_STATE --> GLOBAL
    GLOBAL --> READ_API

    style GT502 fill:#e1f5ff
    style UART2BUF fill:#fff3e0
    style UPDATE fill:#f3e5f5
    style GLOBAL fill:#e8f5e9
    style READ_API fill:#fce4ec

Text表記版:

GT502MGG (GNSS receiver)
  ↓ UART2 @ 9600 bps

UART2 RX Buffer (Hardware)
  ↓ (every ~100ms)

gnss_manager_update()
  ├── Serial2.available() check
  ├── Serial2.readStringUntil('\n')
  └── Parse line in gnss_manager.cpp
      ├── If "$GPRMC" → gnss_parse_rmc()
      │   ├── Extract: latitude, longitude, speed, date/time
      │   └── Update: g_gnss_data
      │
      └── If "$GPGGA" → gnss_parse_gga()
          ├── Extract: fix_quality, satellites, altitude
          └── Update: g_gnss_data

Global State: g_gnss_data (updated ~1Hz)
  └── Available via gnss_manager_read()

検出イベント時のデータフロー

graph TD
    LOOP["main.cpp loop()"]
    COSMIC["cosmic_detection_read()"]
    COSMIC_RESULT["If detection triggered<br/>→ cosmic_data ready"]
    GNSS_UPDATE["gnss_manager_update()"]
    GNSS_RESULT["Parse latest NMEA<br/>→ g_gnss_data updated"]
    SENSOR["sensor_read_all()"]

    BME280["Read BME280<br/>temp, pressure, humidity"]
    ADC["Read ADC<br/>voltage"]
    RTC["Read RTC<br/>timestamp"]
    GNSS_READ["Read GNSS data<br/>data.gnss = gnss_manager_read()"]

    EVENT["Construct event_t<br/>{<br/>hit1, hit2, hit3, adc,<br/>tmp_c, atm_pa, hmd_pct,<br/>unix_timestamp,<br/>gnss: {latitude,<br/>longitude, altitude, ...}<br/>}"]

    FORMATTER["stream_formatter_send event"]
    FORMAT["Format:<br/>SSV/TSV/CSV/JSONL"]
    OUTPUT["Output to Serial UART0<br/>→ host"]

    LOOP --> COSMIC
    COSMIC --> COSMIC_RESULT
    LOOP --> GNSS_UPDATE
    GNSS_UPDATE --> GNSS_RESULT
    LOOP --> SENSOR
    SENSOR --> BME280
    SENSOR --> ADC
    SENSOR --> RTC
    SENSOR --> GNSS_READ
    BME280 --> EVENT
    ADC --> EVENT
    RTC --> EVENT
    GNSS_READ --> EVENT
    EVENT --> FORMATTER
    FORMATTER --> FORMAT
    FORMAT --> OUTPUT

    style LOOP fill:#e3f2fd
    style COSMIC_RESULT fill:#fff3e0
    style GNSS_RESULT fill:#fff3e0
    style EVENT fill:#f3e5f5
    style OUTPUT fill:#e8f5e9

Text表記版:

main.cpp loop()
  ├── cosmic_detection_read()
  │   └── If detection triggered → cosmic_data ready
  │
  ├── gnss_manager_update()
  │   └── Parse latest NMEA → g_gnss_data updated
  │
  └── sensor_read_all()
      ├── Read BME280 (temp, pressure, humidity)
      ├── Read ADC (voltage)
      ├── Read RTC timestamp
      ├── Read GNSS data
      │   └── data.gnss = gnss_manager_read()
      │
      └── Construct event_t
          {
            hit1, hit2, hit3, adc,
            tmp_c, atm_pa, hmd_pct,
            unix_timestamp,
            gnss: {latitude, longitude, altitude, ...}
          }
          ↓
      stream_formatter_send(event)
          ├── Format: SSV/TSV/CSV/JSONL
          └── Output to Serial (UART0) → host

タイミング関係

コンポーネント レート 備考
GNSS NMEA出力 1Hz GT502MGGデフォルト、約100ms間隔
宇宙線検出 イベント駆動 可変、典型的に1~5分に1回
main.cpp loop() 約10ms 100Hz ポーリングレート
UART0 出力 イベント駆動 検出時のみ
UART2 入力 連続 GT502MGG からのストリーミング

6. GNSS_TIMEとRTC_TIMEの同期

SYNC_TIME コマンド

GNSSの正確な絶対時刻をESP32のRTCに同期させるコマンドです。

目的

  • GNSSが時刻のソース(Single Source of Truth)となる
  • 検出イベントに正確なタイムスタンプを付与
  • 複数デバイス間の時刻同期を可能に

実装フロー

ユーザーコマンド: SYNC_TIME
  ↓
text_command_handlers.cpp: handle_sync_time()
  ├── gnss_manager_update() × 2      # UART2から読み取り、NMEA解析
  ├── GNSS 状態の検証
  │   └── satellites < 4 の場合はエラーを返す
  │
  ├── GNSS からタイムスタンプを取出
  │   └── $GPRMC: hhmmss.ss, ddmmyy
  │   └── 変換: ddmmyy hhmmss → unix タイムスタンプ
  │
  ├── rtc_set_time(unix_ts)          # ESP32 RTC を更新
  │   └── ESP32 RTC now = unix_ts
  │
  └── 返す: {"status":"ok","unix_timestamp":1732046789}

応答形式

成功時:

{"status":"ok","unix_timestamp":1732046789,"satellites":8,"fix_quality":1}

エラー時:

{"status":"error","reason":"GNSS_NO_FIX"}
{"status":"error","reason":"GNSS_INSUFFICIENT_SATS"}
{"status":"error","reason":"GNSS_TIMEOUT"}

タイムスタンプ変換アルゴリズム

NMEA形式の日付・時刻をunixタイムスタンプに変換します:

uint32_t gnss_time_to_unixtime(uint32_t nmea_date, float nmea_time) {
  // 日付解析: ddmmyy
  int day = (nmea_date / 10000) % 100;
  int month = (nmea_date / 100) % 100;
  int year = 2000 + (nmea_date % 100);

  // 時刻解析: hhmmss.ss
  int hour = (int)(nmea_time / 10000);
  int minute = (int)((nmea_time - hour*10000) / 100);
  int second = (int)(nmea_time - hour*10000 - minute*100);

  // 標準Cライブラリで unix タイムスタンプに変換
  struct tm tm_info = {0};
  tm_info.tm_year = year - 1900;
  tm_info.tm_mon = month - 1;
  tm_info.tm_mday = day;
  tm_info.tm_hour = hour;
  tm_info.tm_min = minute;
  tm_info.tm_sec = second;
  tm_info.tm_isdst = -1;

  return (uint32_t)mktime(&tm_info);
}

7. kurikintons実装の概要

モジュール名:gnss_manager

  • ファイル:gnss_manager.hgnss_manager.cpp
  • 目的:GNSS受信機の状態管理とNMEAパース
  • パターン:wifi_managerrtc_managerと同じマネージャーパターン

ビルドフラグ設定

// config.h
#ifndef ENABLE_GNSS
#define ENABLE_GNSS 0     // 1=有効, 0=無効(デフォルト)
#endif

GPIO ピン定義

#ifdef ENABLE_GNSS
#ifndef GPS_TX_PIN
#define GPS_TX_PIN 17     // GNSS データ出力
#endif

#ifndef GPS_RX_PIN
#define GPS_RX_PIN 16     // GNSS コマンド入力
#endif

#ifndef GPS_PPS_PIN
#define GPS_PPS_PIN 4     // 1PPS 時刻同期
#endif
#endif

コアデータ構造

typedef enum {
  GNSS_STATE_INITIALIZING,
  GNSS_STATE_SEARCHING,
  GNSS_STATE_FIXING,
  GNSS_STATE_FIXED,
  GNSS_STATE_ERROR
} gnss_state_t;

typedef struct {
  double latitude;       // 十進法度数
  double longitude;      // 十進法度数
  float altitude;        // メートル
  float speed_knots;     // ノット
  uint16_t heading;      // 度(0~359)
  uint32_t date_ddmmyy;  // DDMMYY 形式
  float time_hhmmss;     // HHMMSS.SS 形式
  uint8_t satellites;    // 衛星数
  gnss_fix_quality_t fix_quality;
  bool fix_valid;
  bool rmc_valid;
  bool gga_valid;
} gnss_data_t;

公開 API

  • gnss_init() - UART2、1PPS 割り込み、状態機械を初期化
  • gnss_update() - ポーリングベースの状態機械(NMEAパース)
  • gnss_read() - 現在のGNSS位置/状態を返す
  • gnss_get_state() - 現在の状態を照会
  • gnss_state_name() - 状態の人間が読める名前
  • gnss_fix_quality_name() - 固定品質の名前
  • gnss_time_to_unix() - NMEA日付/時刻をunix timestampに変換
  • gnss_on_1pps() - 1PPS割り込みハンドラ

8. 実装の詳細

初期化シーケンス

setup()
  ├── Serial.begin(115200)           // UART0 - コマンド
  ├── cosmic_detector_init()         // 検出チャネル
  ├── bme280_sensor_init()           // I2C 環境センサ
  ├── adc_sensor_init()              // ADC 入力
  ├── rtc_manager_init()             // ESP32 RTC
  ├── #ifdef ENABLE_GNSS
  │   └── gnss_init()
  │       ├── Serial2.begin(9600)    // UART2 設定
  │       ├── pinMode(GPIO4, INPUT)  // 1PPS 入力
  │       ├── attachInterrupt()      // 1PPS ハンドラ
  │       └── g_gnss_state = INITIALIZING
  └── Ready for loop()

loop() - 連続ポーリング
  ├── serial_protocol_handle()       // UART0 コマンド確認
  ├── gnss_update()                  // UART2 NMEA 読み取り/解析
  ├── cosmic_detection_read()        // 検出チャネルポーリング
  ├── IF 検出発生:
  │   ├── sensor_read_all()          // 全センサデータ読み取り
  │   └── stream_formatter_send()    // UART0 に出力
  └── 約10ms 遅延

状態機械遷移

[INITIALIZING]
  Serial2 オープン、最初の NMEA を待機
  ↓ (最初の "$GPRMC,..." を受信)
[SEARCHING]
  NMEA 受信中だが status='V'(無効)
  衛星 < 4個
  ↓ (status='A' かつ GPGGA 受信)
[FIXING]
  有効な位置データ受信
  3D 測位用に衛星を取得中
  ↓ (衛星 ≥ 4個)
[FIXED]
  安定した 3D 位置
  約1秒ごとに NMEA 更新
  ↓ (信号喪失時)
[ERROR]
  UART 通信エラー
  NMEA パースエラー
  30秒以上無データ

NMEA パース実装パターン

void gnss_update(void) {
  // ノンブロッキングチェック
  if (!Serial2.available()) {
    return;
  }

  // 単一行を読み取り
  String line = Serial2.readStringUntil('\n');

  // 高速パス: 最初の6文字でセンテンス種を判定
  if (line.length() >= 6) {
    if (line.substring(0, 6) == "$GPRMC") {
      gnss_parse_rmc(line.c_str());
    }
    else if (line.substring(0, 6) == "$GPGGA") {
      gnss_parse_gga(line.c_str());
    }
  }

  // 状態機械更新
  gnss_update_state();
}

stream_data.h との統合

#if ENABLE_GNSS
  gnss_data_t gnss;  // latitude, longitude, altitude など
#endif

出力フォーマット例

JSONL形式(GNSSデータ付き):

{"hit1":95,"hit2":87,"hit3":92,"adc":2048,"tmp_c":25.35,"atm_pa":1013.25,"hmd_pct":45.67,"unix_timestamp":1732046789,"gnss":{"latitude":37.3874,"longitude":121.9724,"altitude":45.9,"satellites":8,"fix_quality":1,"fix_valid":true}}

SSV形式(GNSSフィールド付き):

95 87 92 2048 25.35 1013.25 45.67 1732046789 37.3874 121.9724 45.9 8 1

アクセスパターン: 連続ポーリング推奨

理由:

  • wifi_manager と同じポーリングパターン
  • UART2バッファオーバーフロー防止
  • 検出イベント時に常に最新のデータを取得可能
  • ループ処理時間 <10us(無視できる)

実装:

loop() {
  gnss_update();                    // 毎ループ(100Hz)
  if (detection) {
    data.gnss = *gnss_read();       // 常に最新(最大1秒古い)
  }
}

検出イベント時のデータフロー

main.cpp loop()
  ├── cosmic_detection_read()
  │
  ├── gnss_update()
  │   └── 最新 NMEA を解析
  │
  └── sensor_read_all()
      ├── BME280, ADC, RTC 読み取り
      ├── GNSS データ読み取り
      └── event_t 構築
          ↓
      stream_formatter_send(event)
          ├── 形式: SSV/TSV/CSV/JSONL
          └── UART0 → ホスト

9. R&D段階での設計決定

アクセスパターン: 毎ループgnss_update + 検出時GNSS読み出し

設計決定: R&D段階のため、最大限のデータ収集を優先

理由:

  • すべてのGNSSデータをevent_tに含める(位置、衛星数、品質など)
  • RTC時刻とGNSS時刻の両方を記録し、漂移を分析可能に
  • 将来の解析/最適化のための基礎データ収集

実装:

loop() {
  gnss_update();                    // 毎ループ: UART2 NMEA受信/解析
  if (detection) {
    // RTC時刻取得
    event.unix_timestamp = rtc_get_time();

    // GNSS時刻取得(最新NMEA から抽出)
    event.gnss_unix_timestamp = gnss_get_time();

    // GNSS位置データ読み出し
    gnss_data_t gnss = *gnss_read();
    event.gnss_latitude = gnss.latitude;
    event.gnss_longitude = gnss.longitude;
    event.gnss_altitude = gnss.altitude;
    event.gnss_satellites = gnss.satellites;
    event.gnss_fix_quality = gnss.fix_quality;
    event.gnss_hdop = gnss.hdop;
    event.gnss_fix_valid = gnss.fix_valid;
  }
}

event_t 構造: フラット形式(R&D最適化)

設計: GNSS関連フィールドをフラットに個別フィールドで追加

利点:

  • シンプルなJSON出力(ネストなし)
  • 解析ツール・スプレッドシート統合が容易
  • 将来的に不要になれば削除できる柔軟性

構造:

// Optional RTC + GNSS timestamps (when ENABLE_RTC=1)
#if ENABLE_RTC
  uint32_t unix_timestamp;        /**< ESP32 RTC時刻(秒) */
  uint32_t gnss_unix_timestamp;   /**< GNSS導出時刻(秒) */
#endif

// Optional GNSS position and quality (when ENABLE_GNSS=1)
#if ENABLE_GNSS
  double gnss_latitude;           /**< 十進法度数 */
  double gnss_longitude;          /**< 十進法度数 */
  float gnss_altitude;            /**< メートル */
  uint8_t gnss_satellites;        /**< 衛星数(0-24) */
  uint8_t gnss_fix_quality;       /**< 固定品質(0-8) */
  float gnss_hdop;                /**< 水平精度低下因子 */
  bool gnss_fix_valid;            /**< 測位有効フラグ */
#endif

フォーマット例:

JSONL形式(R&D最大データ):

{"hit1":95,"hit2":87,"hit3":92,"adc":2048,"tmp_c":25.35,"atm_pa":1013.25,"hmd_pct":45.67,"unix_timestamp":1732046789,"gnss_unix_timestamp":1732046789,"gnss_latitude":37.3874,"gnss_longitude":121.9724,"gnss_altitude":45.9,"gnss_satellites":8,"gnss_fix_quality":1,"gnss_hdop":1.23,"gnss_fix_valid":true}

SSV形式(スペース区切り):

95 87 92 2048 25.35 1013.25 45.67 1732046789 1732046789 37.3874 121.9724 45.9 8 1 1.23

データ分析用途

タイムスタンプ差分解析:

rtc_drift = gnss_unix_timestamp - unix_timestamp
  • 負値: RTCが進んでいる(高速)
  • 正値: RTCが遅れている(低速)
  • 値が小さい: 同期精度が高い

位置トラッキング(複数検出の空間分布を確認):

  • 同じ場所から複数イベント検出→位置座標でグループ化
  • 宇宙線シャワーの空間的形状分析

GNSS品質トレンド(衛星数vs検出数の相関):

  • satellites >= 4: 3D測位可能
  • fix_quality = 1: GPS通常測位
  • hdop: 測位信頼度(1-2が良好、>5は注意)

10. 外部ライブラリの検討:TinyGPS vs インライン実装

TinyGPS と TinyGPS++ の違い - ライブラリ選択

重要: TinyGPS と TinyGPS++ は異なるライブラリです。kurikintons では TinyGPS++ を推奨します。

両ライブラリの比較

項目 TinyGPS(旧) TinyGPS++(新)
インタフェース 複雑(関数ベース) シンプル(オブジェクトベース)
NMEA対応 \(GPRMC、\)GPGGAのみ 任意のNMEA文に対応
データアクセス 直接アクセス isValid()による安全なアクセス
メモリ使用量 小(~1.5KB) 中(~3.5KB)
精度 32-bit float 64-bit double
保守状況 古い 活発に保守中
推奨対象 Arduino Uno等(RAM制限) ESP32、Arduino Due

kurikintons の選択: TinyGPS++推奨

  • ESP32はRAMに余裕あり(520KB使用可能)
  • TinyGPS++のオブジェクトベースインタフェースが保守性向上
  • 64-bit doubleで座標精度が高い

TinyGPS++ の評価

TinyGPS++(Mikal Hart著、PlatformIOで利用可能)

メリット

項目 内容
開発時間削減 NMEA解析が実装済み(実装時間短縮)
複数衛星システム対応 GPS、GLONASS、Galileo、BeiDou対応
テスト済みコード 長期運用での信頼性
メモリ効率 約240バイト(ライブラリロード時のみ)

デメリット

項目 内容
外部依存性 Vendoringが必要、バージョン管理手間
カスタマイズ困難 GT502MGGの専用フォーマット対応に時間
不要機能 我々が使わない機能が含まれる(bloat)
デバッグの複雑さ ライブラリ内部の問題時の追跡困難
バージョン管理 platformio.ini で依存管理が必要

kurikintons での推奨:インライン実装

採用理由

1. 最小限のパーサ実装で十分
// 必要な解析: $GPRMC と $GPGGA のみ
// 約100行のC++ で実装可能
void gnss_parse_rmc(const char* sentence) {
  // latitude、longitude、speed、date/time 抽出
  // 変換: NMEA度分 → 十進法度
}

void gnss_parse_gga(const char* sentence) {
  // fix_quality、satellites、altitude、hdop 抽出
}
2. kurikintons の実装フィロソフィー
  • Minimal Dependencies: 外部ライブラリ最小化(Adafruit BME280のみ)
  • 透明性: すべてのコードがリポジトリに存在し、即座に検査可能
  • 保守性: モジュール内にNMEA解析ロジックが閉じている
3. パフォーマンス
項目 TinyGPS++ インライン実装
Flash使用量 ~3.5KB ~1.2KB
RAM(動的) ~240B ~100B
解析速度 ~50μs/sentence ~30μs/sentence
バイナリサイズ増加 3500B 1200B(3.3% vs 7%)
4. 将来の拡張可能性
  • GT502MGG固有の拡張フォーマット対応が容易
  • proprietary $PUBX コマンド応答の特殊処理が簡単
  • R&D段階でのデータ収集要件への柔軟対応

デメリット回避策

// インライン実装での品質保証
1. 単体テスト: NMEA文字列の正常系異常系カバー
2. チェックサム検証: データ整合性を常時確認
3. 状態機械: 無効なデータ遷移を防止
4. ドキュメント: NMEA形式仕様をコメント化

比較表

項目 TinyGPS++ インライン実装
開発速度 高(即座に使用可) 中(実装+テスト必要)
導入コスト 低(外部ライブラリ) 低(内部実装)
保守性 中(バグ修正は自動) 高(完全可視化)
パフォーマンス
カスタマイズ性 低(限定的)
バイナリサイズ 中(3.5KB) 小(1.2KB)
メモリ効率
GT502MGG対応 制限的 フル対応可
外部依存 あり なし

結論:保守性とリスク軽減を優先して TinyGPS++ を採用を再検討

TinyGPS++ を選ぶ場合の利点

バグ修正と保守性:

  • 20年以上の運用実績(Mikal Hart作、2008年~)
  • NMEA-0183プロトコル仕様の変更はない(標準化済み)
  • 既知のバグはコミュニティにより報告・修正済み
  • 新しいGNSS受信機の対応も自動的に利用可能

リスク軽減:

インライン実装リスク

  • 実装バグ: NMEA解析のedge case見落とし(カンマ位置、フィールド数、etc)
  • 保守負担: 今後のGNSS仕様変更への対応は自分たちで実施
  • テスト不足: 全NMEA文法パターンのカバレッジが不十分
  • 時間コスト: デバッグ、修正、検証に予期しない時間が必要

TinyGPS++なら

  • 実装済みバグ: 既に多数のedge case対応済み
  • コミュニティ保守: GNSS仕様変更は自動対応
  • テスト済み: 数千ユーザーの実運用で検証済み
  • 時間効率: 99%のユースケースは即座に使用可能

インライン実装を選ぶ場合の検討項目

メンテナンス責任が自分たちにある場合のみ採用:

  1. GNSS形式の頻繁な変更 - GT502MGGが新しい$PUBXコマンド追加時
  2. 完全なカスタマイズ要件 - 独自フォーマット解析が必要
  3. 将来の機能追加 - RTK固定解の導入など

実装コードの比較:パーサーの書き方の違い

インライン実装の例
// gnss_manager.cpp: 自前のNMEA解析(約100行)
void gnss_parse_rmc(const char* sentence) {
  // $GPRMC,093426.00,A,3723.2475,N,12158.3416,E,0.0,0.0,231215,,*49

  // 手動でカンマ区切りを解析
  const char* fields[13];
  int field_count = 0;
  const char* p = sentence;

  for (int i = 0; i < 13; i++) {
    // "$GPRMC," をスキップ
    if (i == 0) {
      while (*p && *p != ',') p++;
      p++;  // スキップ ","
    }

    fields[i] = p;
    while (*p && *p != ',' && *p != '\r' && *p != '\n') p++;

    if (*p == ',') p++;
    else break;
  }

  // 各フィールドを手動で抽出・変換
  float utc_time = atof(fields[0]);  // hhmmss.ss
  char status = fields[1][0];         // A/V

  // 度分形式を十進法度に変換(手動計算)
  float lat = atof(fields[2]);
  int lat_deg = (int)lat / 100;
  float lat_min = fmod(lat, 100);
  g_gnss_data.latitude = lat_deg + lat_min / 60.0;

  // 同様に経度、速度、日付も手動変換...
  // チェックサム検証も自前実装
}

// 状態管理(自前実装)
void gnss_update() {
  if (!Serial2.available()) return;

  String line = Serial2.readStringUntil('\n');

  if (line.startsWith("$GPRMC")) {
    gnss_parse_rmc(line.c_str());
  } else if (line.startsWith("$GPGGA")) {
    gnss_parse_gga(line.c_str());
  }

  // 状態遷移ロジック(自前実装)
  if (g_gnss_data.fix_valid && g_gnss_data.satellites >= 4) {
    g_gnss_state = GNSS_STATE_FIXED;
  }
}

課題:

  • 手動のフィールド解析: バグの温床(カンマ数え誤り、文法エラー処理漏れ)
  • 座標変換の重複実装: \(GPRMCと\)GPGGAで同じロジック
  • エラーハンドリング: atof()失敗、フィールド数不足への対応が面倒
  • テストの複雑さ: 全パターンの手動テストが必要
TinyGPS++を使った実装
// gnss_manager.cpp: TinyGPS++を使用(約30行)
#include <TinyGPS++.h>

TinyGPSPlus gps;

void gnss_update() {
  // Serial2から1文字ずつ読み取り
  // TinyGPS++が自動的にNMEA文を解析して内部状態を更新
  while (Serial2.available()) {
    char c = Serial2.read();
    gps.encode(c);  // 1文字処理、自動的に$GPRMC/$GPGGAを判別+解析
  }
}

void gnss_read(gnss_data_t* out) {
  // TinyGPS++の構造体から直接読み取り
  if (gps.location.isValid()) {
    out->latitude = gps.location.lat();   // 既に十進法度
    out->longitude = gps.location.lng();  // 既に十進法度
    out->altitude = gps.altitude.meters();
    out->fix_valid = true;
  }

  if (gps.satellites.isValid()) {
    out->satellites = gps.satellites.value();
  }

  if (gps.date.isValid() && gps.time.isValid()) {
    // TinyGPS++は既にタイムスタンプ計算済み
    out->date_ddmmyy = gps.date.value();
    out->time_hhmmss = gps.time.value();
  }
}

// 状態管理(シンプル)
gnss_state_t gnss_get_state() {
  if (gps.location.isValid() && gps.satellites.value() >= 4) {
    return GNSS_STATE_FIXED;
  }
  return GNSS_STATE_SEARCHING;
}

メリット:

  • TinyGPS++が自動的にフィールド区切り、データ型変換を処理
  • エラーハンドリング(isValid())が統一インタフェース
  • ユーザーコードは「データの利用」に集中可能
  • テストは「出力結果の検証」のみ(パーサ動作は既検証)
コード量の比較
項目 インライン実装 TinyGPS++
パーサロジック 150~200行 0行(ライブラリ)
エラーハンドリング 50~80行 0行(組み込み)
座標変換 30~50行 0行(自動)
状態管理 40~60行 10~20行
合計ユーザーコード 270~390行 30~50行
メンテナンス視点での比較

インライン実装のメンテナンスシナリオ:

issue: GPS座標がときどきNaNになる

  • 原因追跡: パーサのどこでedge caseが?
  • デバッグ: NMEA文字列の全パターン試行
  • 修正: atof()の前にバリデーション追加?
  • テスト: 新しいテストケース追加→リグレッション確認
  • リスク: 修正が他の部分に影響?

→ 合計対応時間: 2~4時間

TinyGPS++を使う場合:

issue: GPS座標がときどきNaNになる

  • 原因調査: TinyGPS++のissuesで既知か確認
  • 解決: すでに修正版が存在→バージョンアップ
  • テスト: ライブラリ更新+簡易テスト

→ 合計対応時間: 15~30分

推奨戦略:段階的アプローチ

Phase 1(今): TinyGPS++を採用
// platformio.ini に追加
lib_deps =
    Adafruit BME280 Library @ 2.3.0
    TinyGPSPlus @ ^1.0.3  // 安定版、広く使用

メリット:

  • 開発時間短縮: 今週中にGNSS統合完了
  • バグリスク軽減: 既知の問題はすでに解決
  • 運用効率: ライブラリ更新で自動的に改善
Phase 2(将来): 必要に応じてインライン実装へ

条件:

  • 実運用でTinyGPS++の限界が明確になった場合
  • GT502MGGの専用フォーマットへの完全対応が必須の場合
  • パフォーマンスボトルネック(1.2KB vs 3.5KB の差)が検証された場合

最終判断:保守性優先でTinyGPS++を採用

理由:

  1. 運用実績 - 20年以上のコミュニティ検証
  2. メンテナンス不要 - バグ修正がコミュニティに任される
  3. 時間効率 - 実装時間を他の機能に充当可能
  4. リスク軽減 - NMEA解析のバグを自分たちで作らない
  5. 長期投資 - インライン実装の負債を回避

Next Steps

  1. Implementation Phase
  2. Create gnss_manager.h/cpp files with state machine
  3. Implement HardwareSerial2 initialization for UART2 (pins 16/17)
  4. Implement inline NMEA sentence parser within gnss_manager.cpp:
    • gnss_parse_rmc() - Extract latitude, longitude, speed, date/time
    • gnss_parse_gga() - Extract altitude, satellite count, fix quality
    • Streaming parser: Read line-by-line via Serial2.readStringUntil('\n')
  5. Add GPIO4 interrupt handler for 1PPS signal (RISING edge)
  6. State machine transitions: INITIALIZING → SEARCHING → FIXING → FIXED/ERROR

  7. Sensor Data Integration

  8. Add optional GNSS fields to event_t (conditional on ENABLE_GNSS)
  9. Update stream_formatter.h/cpp to output GNSS data in all formats (SSV/TSV/CSV/JSONL)

  10. Text Command Support

  11. GET_GNSS_STATUS - Return current GNSS state and satellite count
  12. GET_GNSS_POSITION - Return lat/lon/altitude
  13. GET_GNSS_TIME - Return GNSS-derived unix timestamp (read on-demand from UART2)
  14. SET_GNSS_UPDATE_RATE - Configure GNSS output interval (1Hz, 5Hz, 10Hz)
  15. SYNC_TIME - Synchronize RTC with GNSS time (set RTC = GNSS timestamp)
  16. GNSS_RESET - Force cold start

  17. Testing & Validation

  18. Verify UART2 communication with GT502MGG module
  19. Validate NMEA sentence parsing
  20. Confirm 1PPS timing accuracy
  21. Integration test with cosmic ray detection timestamps

  22. Documentation

  23. Update CLAUDE.md with GNSS module section
  24. Document NMEA sentence format and satellite ID mapping
  25. Add troubleshooting guide for GPS module initialization