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 <noreply@anthropic.com>
This commit is contained in:
root
2026-08-14 20:16:20 +02:00
co-authored by Claude Fable 5
parent ab492ba187
commit a7724b9ec6
3 changed files with 31 additions and 1 deletions
+7
View File
@@ -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
+16
View File
@@ -19,6 +19,11 @@ bool ArduinoSerialInterface::isConnected() const {
}
bool ArduinoSerialInterface::isWriteBusy() const {
if (_flow_ctl && isConnected()) {
return const_cast<Stream*>(_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] = '>';
+8 -1
View File
@@ -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;