- 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 では 送信は最小限に設計:
- 初期化時の送信: なし(受信のみ)
- 実行時の送信: ユーザーコマンド経由のみ
SYNC_TIME- RTC同期時にNMEAデータ読み取り(送信なし)GET_GNSS_STATUS- 状態クエリ(送信なし、内部状態返却)- 将来拡張: 更新レート変更コマンド
理由:
- 宇宙線検出がイベント駆動型であり、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.h、gnss_manager.cpp - 目的:GNSS受信機の状態管理とNMEAパース
- パターン:
wifi_manager、rtc_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%のユースケースは即座に使用可能
インライン実装を選ぶ場合の検討項目¶
メンテナンス責任が自分たちにある場合のみ採用:
- GNSS形式の頻繁な変更 - GT502MGGが新しい$PUBXコマンド追加時
- 完全なカスタマイズ要件 - 独自フォーマット解析が必要
- 将来の機能追加 - 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++を採用¶
理由:
- 運用実績 - 20年以上のコミュニティ検証
- メンテナンス不要 - バグ修正がコミュニティに任される
- 時間効率 - 実装時間を他の機能に充当可能
- リスク軽減 - NMEA解析のバグを自分たちで作らない
- 長期投資 - インライン実装の負債を回避
Next Steps¶
- Implementation Phase
- Create
gnss_manager.h/cppfiles with state machine - Implement HardwareSerial2 initialization for UART2 (pins 16/17)
- Implement inline NMEA sentence parser within gnss_manager.cpp:
gnss_parse_rmc()- Extract latitude, longitude, speed, date/timegnss_parse_gga()- Extract altitude, satellite count, fix quality- Streaming parser: Read line-by-line via Serial2.readStringUntil('\n')
- Add GPIO4 interrupt handler for 1PPS signal (RISING edge)
-
State machine transitions: INITIALIZING → SEARCHING → FIXING → FIXED/ERROR
-
Sensor Data Integration
- Add optional GNSS fields to
event_t(conditional on ENABLE_GNSS) -
Update
stream_formatter.h/cppto output GNSS data in all formats (SSV/TSV/CSV/JSONL) -
Text Command Support
GET_GNSS_STATUS- Return current GNSS state and satellite countGET_GNSS_POSITION- Return lat/lon/altitudeGET_GNSS_TIME- Return GNSS-derived unix timestamp (read on-demand from UART2)SET_GNSS_UPDATE_RATE- Configure GNSS output interval (1Hz, 5Hz, 10Hz)SYNC_TIME- Synchronize RTC with GNSS time (set RTC = GNSS timestamp)-
GNSS_RESET- Force cold start -
Testing & Validation
- Verify UART2 communication with GT502MGG module
- Validate NMEA sentence parsing
- Confirm 1PPS timing accuracy
-
Integration test with cosmic ray detection timestamps
-
Documentation
- Update CLAUDE.md with GNSS module section
- Document NMEA sentence format and satellite ID mapping
- Add troubleshooting guide for GPS module initialization