diff --git a/docs/SENSOR_NODE_PROTOCOL.md b/docs/SENSOR_NODE_PROTOCOL.md index e56267c..3e0d05e 100644 --- a/docs/SENSOR_NODE_PROTOCOL.md +++ b/docs/SENSOR_NODE_PROTOCOL.md @@ -2,7 +2,7 @@ The XIAO sensor node reads the BNO086 and MAX-M10S and publishes vehicle telemetry over half-duplex RS485 at 115200 8-N-1, one JSON object per line at 4 Hz. It owns no control state. Cargo remains the controller, HTTP server, and `/api/v1` source of truth; JBD/NimBLE behavior is unchanged. -The expansion-board pins are D2 driver enable, D4 TX, and D5 RX. Only physical bus ends should be terminated. Confirm A/B polarity because vendor labels vary. +The expansion-board pins are D2 driver enable, D4 RX, and D5 TX, matching Seeed's XIAO RS485 expansion-board wiring. Only physical bus ends should be terminated. Confirm A/B polarity because vendor labels vary. ## Schema version 1 diff --git a/firmware/esp32-s3-dashboard/dashboard_config.h b/firmware/esp32-s3-dashboard/dashboard_config.h index 17f21b0..9342dfa 100644 --- a/firmware/esp32-s3-dashboard/dashboard_config.h +++ b/firmware/esp32-s3-dashboard/dashboard_config.h @@ -1,6 +1,6 @@ #pragma once -static const char *DASHBOARD_VERSION = "0.1.131-rs485-sensor-stats"; +static const char *DASHBOARD_VERSION = "0.1.132-rs485-raw-diagnostics"; // First-boot defaults. Credentials are stored in Preferences/NVS after setup. static const char *DASHBOARD_DEFAULT_CARGO_WIFI_SSID = "OverlandController"; diff --git a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino index 88a5952..e9af873 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -687,6 +687,12 @@ static void update_diagnostics_overlay() text += String(sensor_node.framesDropped); text += "/"; text += String(sensor_node.parseErrors + sensor_node.oversizedFrames); + text += "\n Raw bytes/age: "; + text += String(sensor_node.bytesReceived); + text += "/"; + text += sensor_node.bytesReceived + ? String(millis() - sensor_node.lastByteMs) + " ms" + : String("--"); text += "\n Frame age: "; text += sensor_node.frameReceived ? String(millis() - sensor_node.lastFrameMs) + " ms" diff --git a/firmware/esp32-s3-dashboard/sensor_node_receiver.cpp b/firmware/esp32-s3-dashboard/sensor_node_receiver.cpp index af73057..fee0de5 100644 --- a/firmware/esp32-s3-dashboard/sensor_node_receiver.cpp +++ b/firmware/esp32-s3-dashboard/sensor_node_receiver.cpp @@ -117,6 +117,8 @@ void initSensorNodeRs485() { void processSensorNodeRs485() { while (sensorNodeSerial.available() > 0) { char value = (char)sensorNodeSerial.read(); + sensorNodeStatus.bytesReceived++; + sensorNodeStatus.lastByteMs = millis(); if (value == '\r') continue; if (value != '\n') { diff --git a/firmware/esp32-s3-dashboard/sensor_node_receiver.h b/firmware/esp32-s3-dashboard/sensor_node_receiver.h index e6498c2..937ed75 100644 --- a/firmware/esp32-s3-dashboard/sensor_node_receiver.h +++ b/firmware/esp32-s3-dashboard/sensor_node_receiver.h @@ -16,6 +16,7 @@ struct SensorNodeDashboardStatus { uint32_t framesDropped = 0; uint32_t parseErrors = 0; uint32_t oversizedFrames = 0; + uint32_t bytesReceived = 0; uint32_t nodeRestarts = 0; uint32_t satellites = 0; float pitchDegrees = 0; @@ -29,6 +30,7 @@ struct SensorNodeDashboardStatus { String stability = "unknown"; String firmwareVersion; String utcTime; + unsigned long lastByteMs = 0; unsigned long lastFrameMs = 0; }; diff --git a/firmware/xiao-esp32c6-sensor-node/README.md b/firmware/xiao-esp32c6-sensor-node/README.md index b4079fe..658caf1 100644 --- a/firmware/xiao-esp32c6-sensor-node/README.md +++ b/firmware/xiao-esp32c6-sensor-node/README.md @@ -4,7 +4,7 @@ This Arduino sketch reads a BNO086 IMU and MAX-M10S GPS and publishes versioned, | Function | XIAO pin | | --- | --- | -| RS485 DE / TX / RX | D2 / D4 / D5 | +| RS485 DE / RX / TX | D2 / D4 / D5 | | BNO086 SDA / SCL | D7 / D8 | | GPS TX to XIAO RX | D9 | | GPS RX from XIAO TX | D10 | diff --git a/firmware/xiao-esp32c6-sensor-node/sensor_node_config.h b/firmware/xiao-esp32c6-sensor-node/sensor_node_config.h index 1a80d8d..80041bc 100644 --- a/firmware/xiao-esp32c6-sensor-node/sensor_node_config.h +++ b/firmware/xiao-esp32c6-sensor-node/sensor_node_config.h @@ -2,11 +2,11 @@ #include #define SENSOR_NODE_NAME "xiao_c6_sensor_node" -#define SENSOR_NODE_FIRMWARE_VERSION "0.2.0" +#define SENSOR_NODE_FIRMWARE_VERSION "0.2.1" #define SENSOR_NODE_SCHEMA_VERSION 1 #define RS485_DE_PIN D2 -#define RS485_TX_PIN D4 -#define RS485_RX_PIN D5 +#define RS485_RX_PIN D4 +#define RS485_TX_PIN D5 #define BNO086_SDA_PIN D7 #define BNO086_SCL_PIN D8 #define GPS_RX_PIN D9 diff --git a/tests/test_dashboard_source_contract.py b/tests/test_dashboard_source_contract.py index ad725d1..7d4955a 100644 --- a/tests/test_dashboard_source_contract.py +++ b/tests/test_dashboard_source_contract.py @@ -44,6 +44,8 @@ def test_dashboard_receives_sensor_node_rs485_for_diagnostics(): assert 'strcmp(type, "sensor_status")' in receiver assert 'doc["frame_sequence"]' in receiver assert "framesDropped" in receiver + assert "bytesReceived++" in receiver + assert r'text += "\n Raw bytes/age: "' in dashboard assert 'imu["yaw_degrees"]' in receiver assert 'gps["altitude_meters"]' in receiver assert 'JsonObject utc = gps["utc"]' in receiver diff --git a/tests/test_xiao_sensor_node_contract.py b/tests/test_xiao_sensor_node_contract.py index d1c226a..a6f27b5 100644 --- a/tests/test_xiao_sensor_node_contract.py +++ b/tests/test_xiao_sensor_node_contract.py @@ -7,7 +7,7 @@ def source(name): return (NODE / name).read_text() def test_wiring_matches_assembly(): config = source("sensor_node_config.h") - expected = {"RS485_DE_PIN":"D2", "RS485_TX_PIN":"D4", "RS485_RX_PIN":"D5", + expected = {"RS485_DE_PIN":"D2", "RS485_RX_PIN":"D4", "RS485_TX_PIN":"D5", "BNO086_SDA_PIN":"D7", "BNO086_SCL_PIN":"D8", "GPS_RX_PIN":"D9", "GPS_TX_PIN":"D10"} for name, pin in expected.items(): assert re.search(rf"#define\s+{name}\s+{pin}\b", config)