diff --git a/docs/SENSOR_NODE_PROTOCOL.md b/docs/SENSOR_NODE_PROTOCOL.md index 805f9d9..eb359b5 100644 --- a/docs/SENSOR_NODE_PROTOCOL.md +++ b/docs/SENSOR_NODE_PROTOCOL.md @@ -9,9 +9,10 @@ The expansion-board pins are D2 driver enable, D4 TX, and D5 RX. Only physical b `sensor_status` includes `node_id`, `firmware_version`, `uptime_ms`, and: - `imu`: `online`, `valid`, `accuracy`, quaternion, yaw, pitch, and roll in degrees. -- `gps`: `online`, `fix`, satellites, latitude, longitude, altitude meters, speed kph, course degrees, and structured UTC. +- `gps`: `online`, `fix`, satellites, NMEA character/checksum counters, latitude, longitude, altitude meters, speed kph, course degrees, and structured UTC. + +`online` only means UART bytes arrived recently. In `gps.nmea`, a rising `sentences_passed` count proves valid NMEA at the configured baud rate. A rising character count with no passed sentences indicates wrong-baud noise or malformed data. A consistently rising failed count indicates corrupt UART data or a baud mismatch. Consumers must check `valid` or `fix` before displaying measurements and ignore unknown fields. The native breakout coordinate frame is reported initially; installation-specific axis remapping should follow physical calibration. This link does not replace dashboard-to-Cargo WiFi/HTTP. A future dashboard receiver may display this telemetry without moving relay, BMS, alarm, or configuration authority away from Cargo. - diff --git a/firmware/xiao-esp32c6-sensor-node/gps.cpp b/firmware/xiao-esp32c6-sensor-node/gps.cpp index 6f2d718..b8c8999 100644 --- a/firmware/xiao-esp32c6-sensor-node/gps.cpp +++ b/firmware/xiao-esp32c6-sensor-node/gps.cpp @@ -6,6 +6,9 @@ namespace { TinyGPSPlus parser; } void initGps(HardwareSerial& serial) { serial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); } 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.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; @@ -16,4 +19,3 @@ void updateGps(HardwareSerial& serial, GpsState& s) { if (parser.date.isValid()) { s.year = parser.date.year(); s.month = parser.date.month(); s.day = parser.date.day(); } if (parser.time.isValid()) { s.hour = parser.time.hour(); s.minute = parser.time.minute(); s.second = parser.time.second(); } } - diff --git a/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp b/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp index 3d5c4bb..a0b6f15 100644 --- a/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp +++ b/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp @@ -19,6 +19,10 @@ 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; + JsonObject nmea = g.createNestedObject("nmea"); + nmea["characters"] = gps.charactersProcessed; + nmea["sentences_passed"] = gps.sentencesPassed; + nmea["sentences_failed"] = gps.sentencesFailed; g["latitude"] = gps.latitude; g["longitude"] = gps.longitude; g["altitude_meters"] = gps.altitudeMeters; g["speed_kph"] = gps.speedKph; g["course_degrees"] = gps.courseDegrees; JsonObject utc = g.createNestedObject("utc"); utc["year"] = gps.year; utc["month"] = gps.month; utc["day"] = gps.day; diff --git a/firmware/xiao-esp32c6-sensor-node/sensor_state.h b/firmware/xiao-esp32c6-sensor-node/sensor_state.h index eb3bd56..d0917e0 100644 --- a/firmware/xiao-esp32c6-sensor-node/sensor_state.h +++ b/firmware/xiao-esp32c6-sensor-node/sensor_state.h @@ -16,6 +16,8 @@ struct GpsState { double speedKph = 0, courseDegrees = 0; uint16_t year = 0; uint8_t month = 0, day = 0, hour = 0, minute = 0, second = 0; + uint32_t charactersProcessed = 0; + uint32_t sentencesPassed = 0; + uint32_t sentencesFailed = 0; unsigned long lastByteMs = 0; }; - diff --git a/tests/test_xiao_sensor_node_contract.py b/tests/test_xiao_sensor_node_contract.py index a56867c..9af1425 100644 --- a/tests/test_xiao_sensor_node_contract.py +++ b/tests/test_xiao_sensor_node_contract.py @@ -17,6 +17,8 @@ def test_transport_contract(): assert token in text assert "USB_TELEMETRY_ENABLED" in text assert "serializeJson(doc, Serial)" in text + assert 'createNestedObject("nmea")' in text + assert 'nmea["sentences_passed"]' in text def test_loop_is_non_blocking(): loop = source("xiao-esp32c6-sensor-node.ino").split("void loop()", 1)[1]