From fb2ca464b956182cf07f33873c0d87949a3d1da6 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 17 Jul 2026 02:24:40 -0600 Subject: [PATCH] firmware: auto-detect GPS UART baud --- firmware/xiao-esp32c6-sensor-node/README.md | 2 +- firmware/xiao-esp32c6-sensor-node/gps.cpp | 38 ++++++++++++++++++- .../rs485_transport.cpp | 1 + .../sensor_node_config.h | 3 +- .../xiao-esp32c6-sensor-node/sensor_state.h | 1 + tests/test_xiao_sensor_node_contract.py | 6 +++ 6 files changed, 48 insertions(+), 3 deletions(-) diff --git a/firmware/xiao-esp32c6-sensor-node/README.md b/firmware/xiao-esp32c6-sensor-node/README.md index 09b2a67..c9f2bea 100644 --- a/firmware/xiao-esp32c6-sensor-node/README.md +++ b/firmware/xiao-esp32c6-sensor-node/README.md @@ -9,7 +9,7 @@ This Arduino sketch reads a BNO086 IMU and MAX-M10S GPS and publishes versioned, | GPS TX to XIAO RX | D9 | | GPS RX from XIAO TX | D10 | -Power both sensors from 3.3V with shared ground. Required Arduino libraries are Adafruit BNO08x, TinyGPSPlus, and ArduinoJson 6.x. Enable **USB CDC On Boot** for diagnostics. GPS uses hardware UART0 and RS485 uses hardware UART1. +Power both sensors from 3.3V with shared ground. Required Arduino libraries are Adafruit BNO08x, TinyGPSPlus, and ArduinoJson 6.x. Enable **USB CDC On Boot** for diagnostics. GPS uses hardware UART0 and RS485 uses hardware UART1. Startup probes 9600, 38400, 115200, and 4800 baud and reports the first rate producing checksum-valid NMEA. At startup the firmware scans D7/D8 and accepts either valid BNO08x address, `0x4A` or `0x4B`. If neither appears in the scan, verify 3.3V/GND, swap-check SDA and SCL, and confirm the breakout's PS0/PS1 jumpers are configured for I2C. diff --git a/firmware/xiao-esp32c6-sensor-node/gps.cpp b/firmware/xiao-esp32c6-sensor-node/gps.cpp index b8c8999..e7e921d 100644 --- a/firmware/xiao-esp32c6-sensor-node/gps.cpp +++ b/firmware/xiao-esp32c6-sensor-node/gps.cpp @@ -3,12 +3,48 @@ #include "sensor_node_config.h" namespace { TinyGPSPlus parser; } -void initGps(HardwareSerial& serial) { serial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); } +namespace { +uint32_t activeBaud = GPS_FALLBACK_BAUD; + +bool baudHasValidNmea(HardwareSerial& serial, uint32_t baud) { + TinyGPSPlus probe; + serial.end(); + serial.begin(baud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); + unsigned long started = millis(); + while (millis() - started < GPS_BAUD_DETECT_MS) { + while (serial.available()) probe.encode(serial.read()); + if (probe.passedChecksum() > 0) return true; + delay(1); + } + return false; +} +} + +void initGps(HardwareSerial& serial) { + const uint32_t candidates[] = {9600, 38400, 115200, 4800}; + Serial.println("Detecting GPS UART baud..."); + for (uint32_t baud : candidates) { + Serial.print("Trying GPS baud "); + Serial.println(baud); + if (!baudHasValidNmea(serial, baud)) continue; + activeBaud = baud; + Serial.print("Valid GPS NMEA detected at "); + Serial.println(activeBaud); + return; + } + + activeBaud = GPS_FALLBACK_BAUD; + serial.end(); + serial.begin(activeBaud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); + Serial.println("No valid GPS NMEA detected; falling back to 9600 baud"); +} + void updateGps(HardwareSerial& serial, GpsState& s) { while (serial.available()) { s.lastByteMs = millis(); parser.encode(serial.read()); } s.charactersProcessed = parser.charsProcessed(); s.sentencesPassed = parser.passedChecksum(); s.sentencesFailed = parser.failedChecksum(); + s.baud = activeBaud; s.online = s.lastByteMs && millis() - s.lastByteMs <= SENSOR_STALE_MS; s.fix = parser.location.isValid() && parser.location.age() <= SENSOR_STALE_MS; s.satellites = parser.satellites.isValid() ? parser.satellites.value() : 0; diff --git a/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp b/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp index a0b6f15..3fbb1bb 100644 --- a/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp +++ b/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp @@ -19,6 +19,7 @@ void publishSensorStatus(HardwareSerial& serial, const ImuState& imu, const GpsS i["yaw_degrees"] = imu.yawDegrees; i["pitch_degrees"] = imu.pitchDegrees; i["roll_degrees"] = imu.rollDegrees; JsonObject g = doc.createNestedObject("gps"); g["online"] = gps.online; g["fix"] = gps.fix; g["satellites"] = gps.satellites; + g["baud"] = gps.baud; JsonObject nmea = g.createNestedObject("nmea"); nmea["characters"] = gps.charactersProcessed; nmea["sentences_passed"] = gps.sentencesPassed; diff --git a/firmware/xiao-esp32c6-sensor-node/sensor_node_config.h b/firmware/xiao-esp32c6-sensor-node/sensor_node_config.h index ecd5dfa..e3ce431 100644 --- a/firmware/xiao-esp32c6-sensor-node/sensor_node_config.h +++ b/firmware/xiao-esp32c6-sensor-node/sensor_node_config.h @@ -14,7 +14,8 @@ #define BNO086_I2C_ADDRESS 0x4A #define BNO086_I2C_ADDRESS_ALTERNATE 0x4B #define BNO086_I2C_CLOCK_HZ 100000 -#define GPS_BAUD 9600 +#define GPS_FALLBACK_BAUD 9600 +#define GPS_BAUD_DETECT_MS 2500 #define RS485_BAUD 115200 #define IMU_REPORT_INTERVAL_US 20000 #define TELEMETRY_INTERVAL_MS 200 diff --git a/firmware/xiao-esp32c6-sensor-node/sensor_state.h b/firmware/xiao-esp32c6-sensor-node/sensor_state.h index d0917e0..750022f 100644 --- a/firmware/xiao-esp32c6-sensor-node/sensor_state.h +++ b/firmware/xiao-esp32c6-sensor-node/sensor_state.h @@ -19,5 +19,6 @@ struct GpsState { uint32_t charactersProcessed = 0; uint32_t sentencesPassed = 0; uint32_t sentencesFailed = 0; + uint32_t baud = 0; unsigned long lastByteMs = 0; }; diff --git a/tests/test_xiao_sensor_node_contract.py b/tests/test_xiao_sensor_node_contract.py index 9af1425..7dedb84 100644 --- a/tests/test_xiao_sensor_node_contract.py +++ b/tests/test_xiao_sensor_node_contract.py @@ -38,6 +38,12 @@ def test_imu_scans_both_bno08x_i2c_addresses(): assert "findBnoAddress()" in imu assert "Wire.endTransmission()" in imu +def test_gps_auto_detects_common_nmea_baud_rates(): + gps = source("gps.cpp") + assert "baudHasValidNmea" in gps + assert "{9600, 38400, 115200, 4800}" in gps + assert "passedChecksum() > 0" in gps + def test_node_does_not_take_cargo_or_ble_authority(): combined = "\n".join(p.read_text() for p in NODE.glob("*.*")) assert "NimBLE" not in combined and "JBD" not in combined and "server.on(" not in combined