Files
MeshCore/src/helpers/bridges/RS232Bridge.cpp
T
mikecarper ca9caece53 Add bounded Morse receive and negotiated RS232 autosleep
Add native narrow-band CW capture, ACK-first remote sessions, retained LoRa-readable pages, checked restoration and transport-safe progress. Add stock-compatible negotiated heartbeat/autosleep and recovery. Include host regressions and identity-bound HIL runners; keep unverified faster tuning HIL-only and document outstanding hardware validation.
2026-10-13 01:24:39 -07:00

475 lines
18 KiB
C++

#include "RS232Bridge.h"
#include "RS232UartUtils.h"
#include <HardwareSerial.h>
#include <stdio.h>
#ifdef WITH_RS232_BRIDGE
#ifdef ESP32
// check_firmware_ram.py reserves this object plus the UART driver's runtime
// allocation even when a combined Full image starts with the UART disabled.
static_assert(sizeof(RS232Bridge) <= 2816, "Update the ESP32 UART heap budget");
#endif
#if defined(NRF52_PLATFORM) && defined(RAK4631_COMBINED_ETHERNET)
// The combined RAK RAM policy reserves this object plus allocator metadata
// and the UART driver's TX semaphore. Its RX/TX arrays are linked globals.
static_assert(sizeof(RS232Bridge) <= 2816, "Update the combined RAK UART heap budget");
#endif
RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, int16_t rx_pin,
int16_t tx_pin, mesh::PacketManager *mgr,
mesh::RTCClock *rtc)
: BridgeBase(prefs, mgr, rtc), _serial(&serial), _rx_pin(rx_pin),
_tx_pin(tx_pin) {}
void RS232Bridge::begin() {
_initialized = false;
resetTransportState();
BRIDGE_DEBUG_PRINTLN("Initializing at %d baud...\n", _prefs->bridge_baud);
#if defined(ESP32)
if (!((HardwareSerial *)_serial)->setPins(_rx_pin, _tx_pin)) return;
#elif defined(NRF52_PLATFORM)
// Tested with RAK_4631 and T114
// The Adafruit Uart object may already be active on its variant defaults.
// Stop it before changing pins or the EasyDMA instance can retain the old
// pin selection and silently receive nothing.
mesh::bridge::prepareNrfUart(*((Uart *)_serial), _rx_pin, _tx_pin);
#elif defined(RP2040_PLATFORM)
((SerialUART *)_serial)->setRX(_rx_pin);
((SerialUART *)_serial)->setTX(_tx_pin);
#elif defined(STM32_PLATFORM)
((HardwareSerial *)_serial)->setRx(_rx_pin);
((HardwareSerial *)_serial)->setTx(_tx_pin);
#else
#error RS232Bridge was not tested on the current platform
#endif
((HardwareSerial *)_serial)->begin(_prefs->bridge_baud);
// Update bridge state
#if defined(ESP32)
_initialized = static_cast<bool>(*static_cast<HardwareSerial *>(_serial));
#else
_initialized = true;
#endif
}
void RS232Bridge::end() {
BRIDGE_DEBUG_PRINTLN("Stopping...\n");
((HardwareSerial *)_serial)->end();
// Update bridge state
_initialized = false;
resetTransportState();
}
void RS232Bridge::resetTransportState() {
_rx_buffer_pos = 0;
_rx_packet_ready = false;
_tx_size = _tx_pos = 0;
_tx_wire_pending = false;
_tx_is_ping = false;
_tx_ping_challenge = 0;
_outstanding_challenge = 0;
_pong_target = 0;
_pong_challenge = 0;
_ping_due = true;
_partial_suspend_grace = false;
}
void RS232Bridge::setHeartbeatSession(uint64_t session) {
if (_session == session) return;
_session = session;
_challenge_counter = static_cast<uint32_t>(session)
^ static_cast<uint32_t>(session >> 32) ^ 0x9E3779B9UL;
resetTransportState();
_peer_session = 0;
_have_ack = false;
_ack_fresh = false;
_standby_armed = false;
_legacy_rx = false;
_legacy_fresh = false;
_power.resetPeer(millis());
}
void RS232Bridge::setHeartbeatEnabled(bool enabled) {
if (enabled && !_heartbeat_enabled) _ping_due = true;
_heartbeat_enabled = enabled;
if (!enabled) _standby_armed = false;
if (!enabled && _tx_is_ping && _tx_pos == 0) {
// An unsent PING can be withdrawn. A partially emitted frame must finish
// to preserve framing of later mesh data; passive PONGs remain supported.
_tx_size = 0;
_tx_is_ping = false;
_tx_ping_challenge = 0;
}
}
uint32_t RS232Bridge::frameAssemblyMs() const {
const uint32_t baud = _prefs->bridge_baud ? _prefs->bridge_baud : 115200;
// Ten wire bits per byte (8N1), plus loop scheduling margin. Start-relative,
// not last-byte-relative: a noise trickle cannot extend an assembly forever.
uint32_t duration = static_cast<uint32_t>(
(static_cast<uint64_t>(MAX_SERIAL_PACKET_SIZE) * 10000 + baud - 1) / baud) + 1000;
if (duration < 1000) duration = 1000;
if (duration > 3000) duration = 3000;
return duration;
}
void RS232Bridge::expirePartialReceive(uint32_t now) {
if (_rx_buffer_pos != 0 && !_rx_packet_ready
&& static_cast<uint32_t>(now - _rx_started) >= frameAssemblyMs()) {
_rx_buffer_pos = 0;
}
}
void RS232Bridge::expireHeartbeatFreshness(uint32_t now) {
// Freshness expires permanently until real RX refreshes it. Historical
// negotiation remains separate, so dead updated peers can still rendezvous
// and an old timestamp cannot appear alive after a full millis() cycle.
if (_ack_fresh && static_cast<uint32_t>(now - _last_ack) >= _idle_ms) {
_ack_fresh = false;
}
if (_legacy_fresh && static_cast<uint32_t>(now - _last_legacy_rx) >= _idle_ms) {
_legacy_fresh = false;
}
}
bool RS232Bridge::txPending(uint32_t now) const {
return _tx_size != 0 || (_tx_wire_pending
&& !mesh::bridge::rs232heartbeat::due(now, _tx_wire_until));
}
RS232Bridge::PowerAction RS232Bridge::servicePowerPolicy(
bool allow_standby, uint32_t idle_ms, uint32_t now, bool force_unconfirmed) {
_idle_ms = idle_ms;
expireHeartbeatFreshness(now);
// Historical successful bidirectional negotiation survives a soft UART stop,
// but a fresh object/session has no such capability. Never stop a stock or
// never-confirmed peer by default; explicit force mode is for dead settings.
const bool armed = allow_standby && _heartbeat_enabled
&& (_have_ack || force_unconfirmed);
_standby_armed = armed;
expirePartialReceive(now);
bool partial_blocks = false;
if (_rx_buffer_pos != 0 && !_rx_packet_ready
&& _power.standbyDue(armed, idle_ms, now)) {
// One bounded grace period at the inactivity deadline; repeatedly starting
// malformed headers cannot keep UART awake indefinitely.
if (!_partial_suspend_grace) {
_partial_suspend_grace = true;
_partial_suspend_until = now + frameAssemblyMs();
}
partial_blocks = !mesh::bridge::rs232heartbeat::due(now, _partial_suspend_until);
if (!partial_blocks) _rx_buffer_pos = 0;
} else if (!_power.standbyDue(armed, idle_ms, now)) {
_partial_suspend_grace = false;
}
return _power.service(armed, idle_ms, now, _initialized,
_rx_packet_ready || txPending(now) || partial_blocks);
}
void RS232Bridge::completePowerAction(PowerAction action, bool success, uint32_t now) {
_power.complete(action, success, now);
}
uint32_t RS232Bridge::powerWakeDelayMs(uint32_t now) const {
return _power.wakeDelay(now);
}
void RS232Bridge::formatHeartbeatStatus(char* out, size_t capacity, uint32_t now) const {
if (!out || capacity == 0) return;
const bool confirmed = _ack_fresh && static_cast<uint32_t>(now - _last_ack) < _idle_ms;
const char* phase = _power.retrying() ? "retrying"
: _power.suspended() ? "standby" : _power.probing() ? "probing"
: _initialized ? "active" : "stopped";
const char* peer = confirmed ? "confirmed" : _legacy_fresh
&& static_cast<uint32_t>(now - _last_legacy_rx) < _idle_ms ? "legacy" : "unknown";
const uint32_t age = _power.peerAge(now);
const uint32_t wake = powerWakeDelayMs(now);
snprintf(out, capacity, "state=%s,peer=%s,age_s=%lu,wake_s=%lu,heartbeat=%s,armed=%s",
phase, peer, static_cast<unsigned long>(age == UINT32_MAX ? 0 : age / 1000),
static_cast<unsigned long>(wake == UINT32_MAX ? 0 : (wake + 999) / 1000),
_heartbeat_enabled ? "on" : "passive", _standby_armed ? "on" : "off");
out[capacity - 1] = 0;
}
void RS232Bridge::drainTransmit(uint32_t now, bool blocking) {
if (!_initialized || _tx_size == 0) return;
size_t amount = static_cast<size_t>(_tx_size - _tx_pos);
if (!blocking) {
const int available = _serial->availableForWrite();
if (available <= 0) return;
if (amount > static_cast<size_t>(available)) amount = static_cast<size_t>(available);
}
size_t sent = _serial->write(_tx_buffer + _tx_pos, amount);
if (sent > amount) sent = amount;
if (sent == 0) return;
_tx_pos = static_cast<uint16_t>(_tx_pos + sent);
const uint32_t baud = _prefs->bridge_baud ? _prefs->bridge_baud : 115200;
uint32_t base = now;
if (_tx_wire_pending && !mesh::bridge::rs232heartbeat::due(now, _tx_wire_until)) {
base = _tx_wire_until;
}
_tx_wire_until = base + static_cast<uint32_t>((sent * 10000UL + baud - 1) / baud) + 1;
_tx_wire_pending = true;
if (_tx_pos == _tx_size) {
if (_tx_is_ping) {
_outstanding_challenge = _tx_ping_challenge;
_outstanding_since = now;
}
_tx_size = _tx_pos = 0;
_tx_is_ping = false;
}
}
bool RS232Bridge::queueControl(char opcode, uint64_t target, uint32_t challenge) {
if (_tx_size != 0 || _session == 0) return false;
mesh::bridge::rs232heartbeat::Control control;
control.opcode = opcode;
control.sender = _session;
control.target = target;
control.challenge = challenge;
if (!mesh::bridge::rs232heartbeat::encode(_tx_buffer + 4, control)) return false;
_tx_buffer[0] = 0xC0; _tx_buffer[1] = 0x3E;
_tx_buffer[2] = _tx_buffer[3] = 0;
_tx_size = mesh::bridge::rs232heartbeat::FRAME_SIZE;
_tx_pos = 0;
_tx_is_ping = opcode == 'P';
_tx_ping_challenge = challenge;
return true;
}
void RS232Bridge::serviceHeartbeat(uint32_t now) {
if (_session == 0 || _tx_size != 0) return;
if (_pong_target != 0) {
if (queueControl('A', _pong_target, _pong_challenge)) {
_pong_target = 0;
_pong_challenge = 0;
}
} else if (_heartbeat_enabled && (_ping_due
|| mesh::bridge::rs232heartbeat::due(now, _next_ping))) {
++_challenge_counter;
if (_challenge_counter == 0) ++_challenge_counter;
if (queueControl('P', 0, _challenge_counter)) {
_ping_due = false;
_next_ping = now + (_power.probing()
? mesh::bridge::rs232heartbeat::PROBE_PING_MS
: mesh::bridge::rs232heartbeat::ACTIVE_PING_MS);
}
}
}
void RS232Bridge::handleControl(uint32_t now) {
mesh::bridge::rs232heartbeat::Control control;
if (_session == 0 || !mesh::bridge::rs232heartbeat::decode(_rx_buffer + 4, control)
|| control.sender == _session) return;
if (control.opcode == 'P') {
// A replayed PING is not peer liveness. Only a fresh challenge response
// proves bidirectional reachability; replying also supports passive peers.
_pong_target = control.sender;
_pong_challenge = control.challenge;
} else if (control.target == _session && _outstanding_challenge != 0
&& control.challenge == _outstanding_challenge
&& static_cast<uint32_t>(now - _outstanding_since)
<= mesh::bridge::rs232heartbeat::ACK_MAX_AGE_MS) {
_outstanding_challenge = 0; // A matching ACK can be consumed only once.
_peer_session = control.sender;
_last_ack = now;
_have_ack = true;
_ack_fresh = true;
_power.notePeer(now);
_partial_suspend_grace = false;
}
}
void RS232Bridge::drainReceivedPackets() {
if (!_rx_packet_ready) return;
mesh::Packet* pkt = _mgr->allocNew();
if (!pkt) return;
const uint16_t len = (_rx_buffer[2] << 8) | _rx_buffer[3];
const bool valid = pkt->readFrom(_rx_buffer + 4, len);
_rx_buffer_pos = 0;
_rx_packet_ready = false;
if (valid) {
// A cable loopback or replay of our own accepted mesh TX is not peer proof.
// Discard here to avoid testing/counting the same duplicate a second time
// inside BridgeBase::handleReceivedPacket().
if (_seen_packets.wasSeen(pkt)) {
_mgr->free(pkt);
return;
}
// Legacy traffic proves inbound reachability, not a bidirectional heartbeat.
// If ordinary data continues after heartbeat replies have stopped, a stock
// peer may have replaced the negotiated one. Unarm the safe default rather
// than eventually sleeping that quiet legacy link. A later fresh ACK can
// immediately restore negotiated capability; force mode remains explicit.
if (_have_ack && (!_ack_fresh
|| static_cast<uint32_t>(_rx_packet_received_ms - _last_ack)
> mesh::bridge::rs232heartbeat::ACTIVE_PING_MS
+ mesh::bridge::rs232heartbeat::ACK_MAX_AGE_MS)) {
_have_ack = false;
_peer_session = 0;
}
_legacy_rx = true;
_legacy_fresh = true;
_last_legacy_rx = _rx_packet_received_ms;
_power.notePeer(_rx_packet_received_ms);
_partial_suspend_grace = false;
onPacketReceived(pkt);
}
else _mgr->free(pkt);
}
void RS232Bridge::loop() {
// Guard against uninitialized state
if (_initialized == false) {
return;
}
const uint32_t now = millis();
expireHeartbeatFreshness(now);
// Let a tail already buffered by the UART complete first. Flash/admin/sensor
// work can pause the loop beyond the assembly deadline even though the peer
// transmitted the complete frame on time; expiry before draining would drop
// valid legacy traffic. A still-incomplete tail expires after bounded work.
if (_serial->available() == 0) expirePartialReceive(now);
drainTransmit(now);
drainReceivedPackets();
// Snapshot bounded work; a noisy UART must not starve mesh/radio service.
int remaining = _serial->available();
if (remaining > 512) remaining = 512;
while (remaining-- > 0 && !_rx_packet_ready) {
const int value = _serial->read();
if (value < 0) break;
const uint8_t b = static_cast<uint8_t>(value);
if (_rx_buffer_pos < 2) {
// Waiting for magic word
if ((_rx_buffer_pos == 0 && b == ((BRIDGE_PACKET_MAGIC >> 8) & 0xFF)) ||
(_rx_buffer_pos == 1 && b == (BRIDGE_PACKET_MAGIC & 0xFF))) {
_rx_buffer[_rx_buffer_pos++] = b;
if (_rx_buffer_pos == 1) _rx_started = now;
} else {
// Invalid magic byte, reset and start over
_rx_buffer_pos = 0;
// Check if this byte could be the start of a new magic word
if (b == ((BRIDGE_PACKET_MAGIC >> 8) & 0xFF)) {
_rx_buffer[_rx_buffer_pos++] = b;
_rx_started = now;
}
}
} else {
// Reading length, payload, and checksum
_rx_buffer[_rx_buffer_pos++] = b;
if (_rx_buffer_pos >= 4) {
uint16_t len = (_rx_buffer[2] << 8) | _rx_buffer[3];
// Validate length field
if (len > (MAX_TRANS_UNIT + 1)) {
BRIDGE_DEBUG_PRINTLN("RX invalid length %d, resetting\n", len);
_rx_buffer_pos = 0; // Invalid length, reset
continue;
}
if (len == 0) {
// Zero length was invalid in the old mesh parser. The extension body
// is fixed-size ASCII, so old receivers cannot find C0 3E within it.
// New receivers resync immediately if a real new frame interrupts it.
if (_rx_buffer_pos > 4 && b >= 0x80) {
_rx_buffer_pos = 0;
if (b == 0xC0) { _rx_buffer[0] = b; _rx_buffer_pos = 1; _rx_started = now; }
} else if (_rx_buffer_pos == mesh::bridge::rs232heartbeat::FRAME_SIZE) {
handleControl(now);
_rx_buffer_pos = 0;
}
continue;
}
if (_rx_buffer_pos == len + SERIAL_OVERHEAD) { // Full packet received
uint16_t received_checksum = (_rx_buffer[4 + len] << 8) | _rx_buffer[5 + len];
if (validateChecksum(_rx_buffer + 4, len, received_checksum)) {
BRIDGE_DEBUG_PRINTLN("RX, len=%d crc=0x%04x\n", len, received_checksum);
_rx_packet_ready = true;
_rx_packet_received_ms = now;
drainReceivedPackets(); // continue this bounded batch when the pool is available
} else {
BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04x\n", received_checksum);
}
if (!_rx_packet_ready) _rx_buffer_pos = 0; // Keep a complete valid frame
}
}
}
}
drainReceivedPackets();
expirePartialReceive(now);
serviceHeartbeat(now);
drainTransmit(now);
}
void RS232Bridge::sendPacket(mesh::Packet *packet) {
// Guard against uninitialized state
if (_initialized == false) {
return;
}
// First validate the packet pointer
if (!packet) {
BRIDGE_DEBUG_PRINTLN("TX invalid packet pointer\n");
return;
}
if (!allowsPacket(packet)) return;
// Preserve the legacy data path's blocking UART acceptance/throughput.
// Finish a partially emitted control/data frame first, never interleave it.
// A genuine failed/short hardware write is retained for later loop service;
// only that failure case cannot accept this caller's additional packet.
drainTransmit(millis(), true);
if (_tx_size != 0) return; // Never interleave mesh traffic with a partial frame.
if (!_seen_packets.wasSeen(packet)) {
const int expected = packet->getRawLength();
if (expected <= 0 || expected > MAX_TRANS_UNIT + 1) return;
uint8_t* buffer = _tx_buffer;
uint16_t len = packet->writeTo(buffer + 4);
// Check if packet fits within our maximum payload size
if (len == 0 || len > (MAX_TRANS_UNIT + 1)) {
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", len, MAX_TRANS_UNIT + 1);
return;
}
// Build packet header
buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF; // Magic high byte
buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF; // Magic low byte
buffer[2] = (len >> 8) & 0xFF; // Length high byte
buffer[3] = len & 0xFF; // Length low byte
// Calculate checksum over the payload
uint16_t checksum = fletcher16(buffer + 4, len);
buffer[4 + len] = (checksum >> 8) & 0xFF; // Checksum high byte
buffer[5 + len] = checksum & 0xFF; // Checksum low byte
// Own the complete frame before returning the shared mesh packet. Data
// uses the original blocking write contract; control-only loop work stays
// nonblocking. Local UART acceptance is never peer liveness.
_tx_size = static_cast<uint16_t>(len + SERIAL_OVERHEAD);
_tx_pos = 0;
_tx_is_ping = false;
_seen_packets.markSeen(packet);
drainTransmit(millis(), true);
BRIDGE_DEBUG_PRINTLN("TX, len=%d crc=0x%04x\n", len, checksum);
}
}
void RS232Bridge::onPacketReceived(mesh::Packet *packet) {
handleReceivedPacket(packet);
}
#endif