From a7724b9ec6dc67db7fcbae4579dc8e32a347eecf Mon Sep 17 00:00:00 2001 From: root Date: Fri, 14 Aug 2026 20:16:20 +0200 Subject: [PATCH] Add flow control to the companion USB interface The contact sync streams up to MAX_CONTACTS frames back to back, but ArduinoSerialInterface never checked whether the stream could take them: isWriteBusy() returned false unconditionally, so MyMesh's pacing gate had no effect, and writeFrame() called write() without looking at availableForWrite(). On ESP32 (HWCDC, 256 byte TX ring) a host that stalls for a moment makes the driver drop queued bytes silently, which tears a frame in half - and since the framing is length prefixed with no checksum and no resync marker, the client stays desynchronised for the rest of the session. Reported as 'the app disconnects while syncing contacts' on devices with a large contact list. Add opt-in flow control: report busy until a whole frame fits, and drop frames as a unit instead of tearing them. Enable it for the companion USB interface and give HWCDC a bigger TX buffer with a short write timeout, mirroring what kiss_modem already does. Co-Authored-By: Claude Fable 5 --- examples/companion_radio/main.cpp | 7 +++++++ src/helpers/ArduinoSerialInterface.cpp | 16 ++++++++++++++++ src/helpers/ArduinoSerialInterface.h | 9 ++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6b421550..d6da46f4 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -218,7 +218,14 @@ void setup() { // add usb interface #if defined(ENABLE_USB_INTERFACE) usb_serial_interface.begin(Serial); + // keep frames intact and pace the contact stream when the host is slow + usb_serial_interface.enableFlowControl(true); #if defined(ESP32) && defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 1 + // a 256 byte TX buffer overflows during a contact sync, and write() blocks + // up to tx_timeout_ms per call against a stalled host (same reasoning as + // the kiss_modem tuning) + Serial.setTxBufferSize(4096); + Serial.setTxTimeoutMs(5); // The ESP32 USB-Serial-JTAG peripheral (HWCDC) has no DTR concept at all: // (bool)Serial only tells us the host has enumerated the device, which is // already true when the cable is plugged into a powered port. Fall back to diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index 317e8395..079508df 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -19,6 +19,11 @@ bool ArduinoSerialInterface::isConnected() const { } bool ArduinoSerialInterface::isWriteBusy() const { + if (_flow_ctl && isConnected()) { + return const_cast(_serial)->availableForWrite() < (int)(MAX_FRAME_SIZE + 3); + } + // while nobody drains the port the TX buffer stays full, so never report + // busy in that case: it would stall the paced streams on all interfaces return false; } @@ -27,6 +32,17 @@ size_t ArduinoSerialInterface::writeFrame(const uint8_t src[], size_t len) { // frame is too big! return 0; } + if (_flow_ctl) { + if (!isConnected()) { + return len; // nobody is listening, drop instead of filling the TX buffer + } + if (_serial->availableForWrite() < (int)(len + 3)) { + // a short write would tear the length prefixed framing, and as there is + // neither a checksum nor a resync marker the receiver would stay out of + // sync forever - so drop the whole frame instead + return 0; + } + } uint8_t hdr[3]; hdr[0] = '>'; diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index ecc05976..ca6d9f69 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -10,6 +10,7 @@ public: private: bool _isEnabled; + bool _flow_ctl; uint8_t _state; uint16_t _frame_len; uint16_t rx_len; @@ -19,7 +20,7 @@ private: uint8_t rx_buf[MAX_FRAME_SIZE]; public: - ArduinoSerialInterface() { _isEnabled = false; _state = 0; _last_frame_ms = 0; _conn_check = NULL; } + ArduinoSerialInterface() { _isEnabled = false; _flow_ctl = false; _state = 0; _last_frame_ms = 0; _conn_check = NULL; } void begin(Stream& serial) { _serial = &serial; @@ -36,6 +37,12 @@ public: // Useful as an activity-based connection check where no DTR state exists. uint32_t getLastFrameMillis() const { return _last_frame_ms; } + // Optional: only hand a frame to the stream when it fits into the TX buffer + // as a whole, and report busy while it does not, so bulk streams get paced. + // Only enable this for streams which really implement availableForWrite() + // (USB-CDC does, the Print default returns 0). + void enableFlowControl(bool enable) { _flow_ctl = enable; } + // BaseSerialInterface methods void enable() override; void disable() override;