From fea549d1e22149af0dc89ecb74603ffa805ff87f Mon Sep 17 00:00:00 2001 From: nick Date: Sun, 26 Jul 2026 18:15:48 -0600 Subject: [PATCH] dashboard: expand RS485 sensor diagnostics --- docs/CHANGELOG.md | 1 + docs/dashboard-esp32s3.md | 2 +- firmware/esp32-s3-dashboard/README.md | 2 +- .../esp32-s3-dashboard/dashboard_config.h | 2 +- .../esp32-s3-dashboard/esp32-s3-dashboard.ino | 7 +++++++ .../sensor_node_receiver.cpp | 21 ++++++++++++++++++- .../esp32-s3-dashboard/sensor_node_receiver.h | 4 +++- tests/test_dashboard_source_contract.py | 6 ++++++ 8 files changed, 40 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 555ddaf..483ce51 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,7 @@ - Expanded telemetry with BNO086 motion/gravity/stability reports and GPS HDOP/data ages. - Hardened RS485 transmission with local-echo draining, frame-size guards, a larger RX buffer, and boot/frame sequence identifiers. - Added dashboard reception of XIAO sensor-node RS485 telemetry and a basic diagnostics-overlay view without changing Cargo `/api/v1` behavior. +- Expanded dashboard sensor-node diagnostics with yaw, GPS altitude, and UTC. ## Unreleased diff --git a/docs/dashboard-esp32s3.md b/docs/dashboard-esp32s3.md index 59a146a..f738d01 100644 --- a/docs/dashboard-esp32s3.md +++ b/docs/dashboard-esp32s3.md @@ -303,7 +303,7 @@ The dashboard enclosure will include an M9N GPS module connected to the ESP32-S3 The dashboard has a hidden diagnostics overlay. Tap the top status card five times within roughly three seconds to open it. Double-tap the diagnostics overlay to return to the main dashboard. -The overlay also provides the initial XIAO sensor-node bring-up view. The dashboard receives newline-delimited JSON through its onboard automatic-direction RS485 transceiver on GPIO43 RX / GPIO44 TX at 115200 baud. It shows link age, received/dropped/error counts, IMU pitch/roll/accuracy/stability, and GPS fix/satellites/HDOP/location/speed. Cargo communication remains WiFi/HTTP through `/api/v1`. +The overlay also provides the initial XIAO sensor-node bring-up view. The dashboard receives newline-delimited JSON through its onboard automatic-direction RS485 transceiver on GPIO43 RX / GPIO44 TX at 115200 baud. It shows link age, received/dropped/error counts, IMU pitch/roll/yaw/accuracy/stability, and GPS fix/satellites/HDOP/location/altitude/speed/UTC. Cargo communication remains WiFi/HTTP through `/api/v1`. The diagnostics overlay shows dashboard version, uptime, heap, WiFi RSSI/IP, Cargo API status, Cargo firmware version, Cargo uptime, Cargo heap, Cargo AP client count, BMS status, relay count, and basic control-loop state. diff --git a/firmware/esp32-s3-dashboard/README.md b/firmware/esp32-s3-dashboard/README.md index ff2d685..075d740 100644 --- a/firmware/esp32-s3-dashboard/README.md +++ b/firmware/esp32-s3-dashboard/README.md @@ -80,7 +80,7 @@ This keeps the endpoint model simple while reducing payload size, JSON parsing, The dashboard listens to the onboard RS485 receiver at 115200 baud using GPIO43 RX and GPIO44 TX. The Waveshare transceiver switches direction automatically, so no DE GPIO is required. The receiver accepts newline-delimited schema-v1 `sensor_status` JSON from the XIAO node and does not change Cargo HTTP/control behavior. -Open the hidden diagnostics overlay to view link state, frame sequence/gaps/errors, IMU accuracy/stability/pitch/roll, and GPS fix/satellites/HDOP/location/speed. This is a temporary bring-up view ahead of a dedicated off-road page. +Open the hidden diagnostics overlay to view link state, frame sequence/gaps/errors, IMU accuracy/stability/pitch/roll/yaw, and GPS fix/satellites/HDOP/location/altitude/speed/UTC. This is a temporary bring-up view ahead of a dedicated off-road page. Arduino **USB CDC On Boot** must remain enabled so dashboard debug `Serial` output uses native USB rather than competing with the GPIO43/GPIO44 RS485 UART. diff --git a/firmware/esp32-s3-dashboard/dashboard_config.h b/firmware/esp32-s3-dashboard/dashboard_config.h index 313cc37..17f21b0 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.130-rs485-diagnostics"; +static const char *DASHBOARD_VERSION = "0.1.131-rs485-sensor-stats"; // 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 976384d..88a5952 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -702,6 +702,9 @@ static void update_diagnostics_overlay() text += " / "; text += String(sensor_node.rollDegrees, 1); text += sensor_node.levelCalibrationValid ? " deg calibrated" : " deg raw"; + text += "\n Yaw: "; + text += String(sensor_node.yawDegrees, 1); + text += " deg"; text += "\n GPS: "; text += sensor_node.gpsOnline ? (sensor_node.gpsFix ? "fix" : "no fix") : "offline"; text += " sats "; @@ -718,6 +721,10 @@ static void update_diagnostics_overlay() } text += "\n Speed: "; text += sensor_node.gpsFix ? String(sensor_node.speedKph, 1) + " km/h" : String("--"); + text += "\n Altitude: "; + text += sensor_node.gpsFix ? String(sensor_node.altitudeMeters, 1) + " m" : String("--"); + text += "\n UTC: "; + text += sensor_node.utcTime.length() ? sensor_node.utcTime : String("--"); text += "\n\nCargo ESP\n"; text += " Version: "; diff --git a/firmware/esp32-s3-dashboard/sensor_node_receiver.cpp b/firmware/esp32-s3-dashboard/sensor_node_receiver.cpp index 3ab6a26..af73057 100644 --- a/firmware/esp32-s3-dashboard/sensor_node_receiver.cpp +++ b/firmware/esp32-s3-dashboard/sensor_node_receiver.cpp @@ -57,6 +57,7 @@ void parseSensorNodeFrame(const String &line) { sensorNodeStatus.imuValid = imu["valid"] | false; sensorNodeStatus.imuAccuracy = imu["accuracy"] | 0; sensorNodeStatus.stability = String((const char *)(imu["stability"] | "unknown")); + sensorNodeStatus.yawDegrees = imu["yaw_degrees"] | 0.0f; JsonObject level = imu["level_calibration"]; sensorNodeStatus.levelCalibrationValid = level["valid"] | false; @@ -74,7 +75,26 @@ void parseSensorNodeFrame(const String &line) { sensorNodeStatus.hdop = gps["hdop"].isNull() ? 0 : (float)gps["hdop"]; sensorNodeStatus.latitude = gps["latitude"] | 0.0; sensorNodeStatus.longitude = gps["longitude"] | 0.0; + sensorNodeStatus.altitudeMeters = gps["altitude_meters"] | 0.0f; sensorNodeStatus.speedKph = gps["speed_kph"] | 0.0f; + JsonObject utc = gps["utc"]; + if (!utc.isNull() && (int)(utc["year"] | 0) > 0) { + char utcBuffer[24]; + snprintf( + utcBuffer, + sizeof(utcBuffer), + "%04d-%02d-%02d %02d:%02d:%02dZ", + (int)(utc["year"] | 0), + (int)(utc["month"] | 0), + (int)(utc["day"] | 0), + (int)(utc["hour"] | 0), + (int)(utc["minute"] | 0), + (int)(utc["second"] | 0) + ); + sensorNodeStatus.utcTime = utcBuffer; + } else { + sensorNodeStatus.utcTime = ""; + } sensorNodeStatus.firmwareVersion = String((const char *)(doc["firmware_version"] | "")); sensorNodeStatus.frameReceived = true; sensorNodeStatus.framesReceived++; @@ -132,4 +152,3 @@ bool sensorNodeRs485Connected() { const SensorNodeDashboardStatus &getSensorNodeStatus() { return sensorNodeStatus; } - diff --git a/firmware/esp32-s3-dashboard/sensor_node_receiver.h b/firmware/esp32-s3-dashboard/sensor_node_receiver.h index feed40c..e6498c2 100644 --- a/firmware/esp32-s3-dashboard/sensor_node_receiver.h +++ b/firmware/esp32-s3-dashboard/sensor_node_receiver.h @@ -20,12 +20,15 @@ struct SensorNodeDashboardStatus { uint32_t satellites = 0; float pitchDegrees = 0; float rollDegrees = 0; + float yawDegrees = 0; float speedKph = 0; float hdop = 0; + float altitudeMeters = 0; double latitude = 0; double longitude = 0; String stability = "unknown"; String firmwareVersion; + String utcTime; unsigned long lastFrameMs = 0; }; @@ -33,4 +36,3 @@ void initSensorNodeRs485(); void processSensorNodeRs485(); bool sensorNodeRs485Connected(); const SensorNodeDashboardStatus &getSensorNodeStatus(); - diff --git a/tests/test_dashboard_source_contract.py b/tests/test_dashboard_source_contract.py index 304682c..ad725d1 100644 --- a/tests/test_dashboard_source_contract.py +++ b/tests/test_dashboard_source_contract.py @@ -44,3 +44,9 @@ 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 'imu["yaw_degrees"]' in receiver + assert 'gps["altitude_meters"]' in receiver + assert 'JsonObject utc = gps["utc"]' in receiver + assert r'text += "\n Yaw: "' in dashboard + assert r'text += "\n Altitude: "' in dashboard + assert r'text += "\n UTC: "' in dashboard