diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ced5a85..19c2ca3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,7 @@ - Added standalone XIAO ESP32-C6 BNO086/MAX-M10S sensor-node firmware and a versioned RS485 telemetry contract without changing Cargo `/api/v1` or JBD/NimBLE behavior. - Added XIAO-side persistent level calibration with stability checking and RS485 start/clear commands; no dashboard behavior changed. +- Prevented echoed or oversized RS485 frames from starving the XIAO sensor loop. ## Unreleased diff --git a/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp b/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp index 2fd540e..c0b458a 100644 --- a/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp +++ b/firmware/xiao-esp32c6-sensor-node/rs485_transport.cpp @@ -5,6 +5,7 @@ namespace { String commandBuffer; +bool discardCommandUntilNewline = false; void sendJson(HardwareSerial& serial, JsonDocument& doc) { digitalWrite(RS485_DE_PIN, HIGH); delayMicroseconds(20); serializeJson(doc, serial); serial.write('\n'); serial.flush(); @@ -63,12 +64,25 @@ void publishCalibrationResult(HardwareSerial& serial, const String& result) { } void pollRs485Commands(HardwareSerial& serial, const ImuState& imu) { - while (serial.available()) { + // Bound RX work so an electrically echoed or noisy bus cannot starve sensors. + int bytesRemaining = RS485_COMMAND_MAX_LENGTH * 2; + while (serial.available() && bytesRemaining-- > 0) { char c = serial.read(); if (c == '\r') continue; if (c != '\n') { - if (commandBuffer.length() < RS485_COMMAND_MAX_LENGTH) commandBuffer += c; - else commandBuffer = ""; + if (discardCommandUntilNewline) continue; + if (commandBuffer.length() < RS485_COMMAND_MAX_LENGTH) { + commandBuffer += c; + } else { + commandBuffer = ""; + discardCommandUntilNewline = true; + } + continue; + } + + if (discardCommandUntilNewline) { + discardCommandUntilNewline = false; + commandBuffer = ""; continue; } @@ -77,6 +91,8 @@ void pollRs485Commands(HardwareSerial& serial, const ImuState& imu) { commandBuffer = ""; if (error) { publishCalibrationResult(serial, "invalid_json"); continue; } String type = request["type"] | ""; + // Some RS485 boards echo transmitted frames into RX. Ignore our own output. + if (type == "sensor_status" || type == "calibration_response") continue; if (type == "calibrate_level") { publishCalibrationResult(serial, startLevelCalibration(imu) ? "started" : "imu_unavailable"); } else if (type == "clear_level_calibration") { diff --git a/tests/test_xiao_sensor_node_contract.py b/tests/test_xiao_sensor_node_contract.py index 1892e54..503b5f1 100644 --- a/tests/test_xiao_sensor_node_contract.py +++ b/tests/test_xiao_sensor_node_contract.py @@ -56,6 +56,12 @@ def test_level_calibration_is_persistent_and_non_blocking(): assert "updateLevelCalibration" in loop assert "delay(5000)" not in calibration +def test_rs485_echo_cannot_starve_sensor_loop(): + transport = source("rs485_transport.cpp") + assert "bytesRemaining" in transport + assert "discardCommandUntilNewline" in transport + assert 'type == "sensor_status"' in transport + 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