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;