dashboard: show sensor node RS485 diagnostics
This commit is contained in:
@@ -76,6 +76,14 @@ The dashboard uses field-filtered `/api/v1/status` calls:
|
||||
|
||||
This keeps the endpoint model simple while reducing payload size, JSON parsing, and UI churn.
|
||||
|
||||
## XIAO sensor-node RS485 diagnostics
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Passive CAN Logger
|
||||
|
||||
The dashboard runs its 500 kbps TWAI controller in listen-only mode and never
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
static const char *DASHBOARD_VERSION = "0.1.129-can-markers";
|
||||
static const char *DASHBOARD_VERSION = "0.1.130-rs485-diagnostics";
|
||||
|
||||
// First-boot defaults. Credentials are stored in Preferences/NVS after setup.
|
||||
static const char *DASHBOARD_DEFAULT_CARGO_WIFI_SSID = "OverlandController";
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
extern const lv_img_dsc_t gas_station;
|
||||
extern const lv_img_dsc_t thermometer_water;
|
||||
#include "dashboard_status_model.h"
|
||||
#include "sensor_node_receiver.h"
|
||||
|
||||
using namespace esp_panel::drivers;
|
||||
using namespace esp_panel::board;
|
||||
@@ -668,6 +669,56 @@ static void update_diagnostics_overlay()
|
||||
text += "\n Cargo API: ";
|
||||
text += status_model.api_ok ? "OK" : "FAIL";
|
||||
|
||||
const SensorNodeDashboardStatus &sensor_node = getSensorNodeStatus();
|
||||
text += "\n\nSensor Node RS485\n";
|
||||
text += " Link: ";
|
||||
text += sensorNodeRs485Connected() ? "connected" : "offline";
|
||||
text += " @ 115200";
|
||||
text += "\n Pins: RX GPIO43 / TX GPIO44";
|
||||
text += "\n Version: ";
|
||||
text += sensor_node.firmwareVersion.length() ? sensor_node.firmwareVersion : String("--");
|
||||
text += "\n Boot/sequence: ";
|
||||
text += sensor_node.frameReceived ? String(sensor_node.bootId, HEX) : String("--");
|
||||
text += "/";
|
||||
text += sensor_node.frameReceived ? String(sensor_node.frameSequence) : String("--");
|
||||
text += "\n Frames/gaps/errors: ";
|
||||
text += String(sensor_node.framesReceived);
|
||||
text += "/";
|
||||
text += String(sensor_node.framesDropped);
|
||||
text += "/";
|
||||
text += String(sensor_node.parseErrors + sensor_node.oversizedFrames);
|
||||
text += "\n Frame age: ";
|
||||
text += sensor_node.frameReceived
|
||||
? String(millis() - sensor_node.lastFrameMs) + " ms"
|
||||
: String("--");
|
||||
text += "\n IMU: ";
|
||||
text += sensor_node.imuOnline && sensor_node.imuValid ? "OK" : "offline";
|
||||
text += " acc ";
|
||||
text += String(sensor_node.imuAccuracy);
|
||||
text += " ";
|
||||
text += sensor_node.stability;
|
||||
text += "\n Pitch/roll: ";
|
||||
text += String(sensor_node.pitchDegrees, 1);
|
||||
text += " / ";
|
||||
text += String(sensor_node.rollDegrees, 1);
|
||||
text += sensor_node.levelCalibrationValid ? " deg calibrated" : " deg raw";
|
||||
text += "\n GPS: ";
|
||||
text += sensor_node.gpsOnline ? (sensor_node.gpsFix ? "fix" : "no fix") : "offline";
|
||||
text += " sats ";
|
||||
text += String(sensor_node.satellites);
|
||||
text += " HDOP ";
|
||||
text += sensor_node.hdop > 0 ? String(sensor_node.hdop, 1) : String("--");
|
||||
text += "\n Position: ";
|
||||
if (sensor_node.gpsFix) {
|
||||
text += String(sensor_node.latitude, 6);
|
||||
text += ", ";
|
||||
text += String(sensor_node.longitude, 6);
|
||||
} else {
|
||||
text += "--";
|
||||
}
|
||||
text += "\n Speed: ";
|
||||
text += sensor_node.gpsFix ? String(sensor_node.speedKph, 1) + " km/h" : String("--");
|
||||
|
||||
text += "\n\nCargo ESP\n";
|
||||
text += " Version: ";
|
||||
text += status_model.firmware_version;
|
||||
@@ -2624,6 +2675,7 @@ void setup()
|
||||
Serial.println("Target: Waveshare ESP32-S3 Touch LCD 5B");
|
||||
Serial.println("Display: 1024x600");
|
||||
Serial.println("API Base: /api/v1");
|
||||
initSensorNodeRs485();
|
||||
|
||||
load_dashboard_connection_config();
|
||||
|
||||
@@ -2659,6 +2711,7 @@ void loop()
|
||||
process_pending_relay_command();
|
||||
poll_status_api();
|
||||
process_obd_can();
|
||||
processSensorNodeRs485();
|
||||
process_dashboard_page_switch();
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#include <ArduinoJson.h>
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include "sensor_node_receiver.h"
|
||||
|
||||
namespace {
|
||||
constexpr int SENSOR_NODE_RS485_RX_PIN = 43;
|
||||
constexpr int SENSOR_NODE_RS485_TX_PIN = 44;
|
||||
constexpr uint32_t SENSOR_NODE_RS485_BAUD = 115200;
|
||||
constexpr size_t SENSOR_NODE_RS485_MAX_LINE = 1900;
|
||||
constexpr size_t SENSOR_NODE_RS485_RX_BUFFER = 4096;
|
||||
constexpr unsigned long SENSOR_NODE_STALE_MS = 1500;
|
||||
|
||||
HardwareSerial sensorNodeSerial(1);
|
||||
SensorNodeDashboardStatus sensorNodeStatus;
|
||||
String receiveLine;
|
||||
bool discardUntilNewline = false;
|
||||
bool sequenceInitialized = false;
|
||||
|
||||
void recordSequence(uint32_t bootId, uint32_t sequence) {
|
||||
if (!sequenceInitialized || bootId != sensorNodeStatus.bootId) {
|
||||
if (sequenceInitialized && bootId != sensorNodeStatus.bootId) {
|
||||
sensorNodeStatus.nodeRestarts++;
|
||||
}
|
||||
sensorNodeStatus.bootId = bootId;
|
||||
sensorNodeStatus.frameSequence = sequence;
|
||||
sequenceInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t delta = sequence - sensorNodeStatus.frameSequence;
|
||||
if (delta > 1 && delta < 0x80000000UL) {
|
||||
sensorNodeStatus.framesDropped += delta - 1;
|
||||
}
|
||||
sensorNodeStatus.frameSequence = sequence;
|
||||
}
|
||||
|
||||
void parseSensorNodeFrame(const String &line) {
|
||||
StaticJsonDocument<3072> doc;
|
||||
DeserializationError error = deserializeJson(doc, line);
|
||||
if (error) {
|
||||
sensorNodeStatus.parseErrors++;
|
||||
return;
|
||||
}
|
||||
|
||||
const char *type = doc["type"] | "";
|
||||
if (strcmp(type, "sensor_status") != 0 || (int)(doc["schema_version"] | 0) != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t bootId = doc["boot_id"] | 0;
|
||||
uint32_t sequence = doc["frame_sequence"] | 0;
|
||||
recordSequence(bootId, sequence);
|
||||
|
||||
JsonObject imu = doc["imu"];
|
||||
sensorNodeStatus.imuOnline = imu["online"] | false;
|
||||
sensorNodeStatus.imuValid = imu["valid"] | false;
|
||||
sensorNodeStatus.imuAccuracy = imu["accuracy"] | 0;
|
||||
sensorNodeStatus.stability = String((const char *)(imu["stability"] | "unknown"));
|
||||
|
||||
JsonObject level = imu["level_calibration"];
|
||||
sensorNodeStatus.levelCalibrationValid = level["valid"] | false;
|
||||
sensorNodeStatus.pitchDegrees = sensorNodeStatus.levelCalibrationValid
|
||||
? (float)(level["pitch_degrees"] | 0.0f)
|
||||
: (float)(imu["pitch_degrees"] | 0.0f);
|
||||
sensorNodeStatus.rollDegrees = sensorNodeStatus.levelCalibrationValid
|
||||
? (float)(level["roll_degrees"] | 0.0f)
|
||||
: (float)(imu["roll_degrees"] | 0.0f);
|
||||
|
||||
JsonObject gps = doc["gps"];
|
||||
sensorNodeStatus.gpsOnline = gps["online"] | false;
|
||||
sensorNodeStatus.gpsFix = gps["fix"] | false;
|
||||
sensorNodeStatus.satellites = gps["satellites"] | 0;
|
||||
sensorNodeStatus.hdop = gps["hdop"].isNull() ? 0 : (float)gps["hdop"];
|
||||
sensorNodeStatus.latitude = gps["latitude"] | 0.0;
|
||||
sensorNodeStatus.longitude = gps["longitude"] | 0.0;
|
||||
sensorNodeStatus.speedKph = gps["speed_kph"] | 0.0f;
|
||||
sensorNodeStatus.firmwareVersion = String((const char *)(doc["firmware_version"] | ""));
|
||||
sensorNodeStatus.frameReceived = true;
|
||||
sensorNodeStatus.framesReceived++;
|
||||
sensorNodeStatus.lastFrameMs = millis();
|
||||
}
|
||||
}
|
||||
|
||||
void initSensorNodeRs485() {
|
||||
receiveLine.reserve(SENSOR_NODE_RS485_MAX_LINE);
|
||||
sensorNodeSerial.setRxBufferSize(SENSOR_NODE_RS485_RX_BUFFER);
|
||||
sensorNodeSerial.begin(
|
||||
SENSOR_NODE_RS485_BAUD,
|
||||
SERIAL_8N1,
|
||||
SENSOR_NODE_RS485_RX_PIN,
|
||||
SENSOR_NODE_RS485_TX_PIN
|
||||
);
|
||||
Serial.println("Sensor node RS485: RX GPIO43 / TX GPIO44 @ 115200");
|
||||
}
|
||||
|
||||
void processSensorNodeRs485() {
|
||||
while (sensorNodeSerial.available() > 0) {
|
||||
char value = (char)sensorNodeSerial.read();
|
||||
if (value == '\r') continue;
|
||||
|
||||
if (value != '\n') {
|
||||
if (discardUntilNewline) continue;
|
||||
if (receiveLine.length() < SENSOR_NODE_RS485_MAX_LINE) {
|
||||
receiveLine += value;
|
||||
} else {
|
||||
receiveLine = "";
|
||||
discardUntilNewline = true;
|
||||
sensorNodeStatus.oversizedFrames++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (discardUntilNewline) {
|
||||
discardUntilNewline = false;
|
||||
receiveLine = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (receiveLine.length() > 0) {
|
||||
parseSensorNodeFrame(receiveLine);
|
||||
}
|
||||
receiveLine = "";
|
||||
}
|
||||
}
|
||||
|
||||
bool sensorNodeRs485Connected() {
|
||||
return sensorNodeStatus.frameReceived &&
|
||||
millis() - sensorNodeStatus.lastFrameMs <= SENSOR_NODE_STALE_MS;
|
||||
}
|
||||
|
||||
const SensorNodeDashboardStatus &getSensorNodeStatus() {
|
||||
return sensorNodeStatus;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
struct SensorNodeDashboardStatus {
|
||||
bool frameReceived = false;
|
||||
bool imuOnline = false;
|
||||
bool imuValid = false;
|
||||
bool levelCalibrationValid = false;
|
||||
bool gpsOnline = false;
|
||||
bool gpsFix = false;
|
||||
uint8_t imuAccuracy = 0;
|
||||
uint32_t bootId = 0;
|
||||
uint32_t frameSequence = 0;
|
||||
uint32_t framesReceived = 0;
|
||||
uint32_t framesDropped = 0;
|
||||
uint32_t parseErrors = 0;
|
||||
uint32_t oversizedFrames = 0;
|
||||
uint32_t nodeRestarts = 0;
|
||||
uint32_t satellites = 0;
|
||||
float pitchDegrees = 0;
|
||||
float rollDegrees = 0;
|
||||
float speedKph = 0;
|
||||
float hdop = 0;
|
||||
double latitude = 0;
|
||||
double longitude = 0;
|
||||
String stability = "unknown";
|
||||
String firmwareVersion;
|
||||
unsigned long lastFrameMs = 0;
|
||||
};
|
||||
|
||||
void initSensorNodeRs485();
|
||||
void processSensorNodeRs485();
|
||||
bool sensorNodeRs485Connected();
|
||||
const SensorNodeDashboardStatus &getSensorNodeStatus();
|
||||
|
||||
Reference in New Issue
Block a user