diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 96675507..9585aec2 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -74,6 +74,11 @@ MultiSerialInterface interface_manager; static const char USB_MOTA_START_TOKEN[] = "ota folder on"; #endif ArduinoSerialInterface usb_serial_interface; + #ifndef USB_CLIENT_IDLE_TIMEOUT + // how long a USB client is still considered present after its last frame, + // for targets which cannot report DTR (see setConnectedCheck below) + #define USB_CLIENT_IDLE_TIMEOUT (10*60*1000UL) + #endif #endif // include ethernet interface @@ -942,6 +947,29 @@ void setup() { usb_serial_interface.begin(Serial, USB_TERMINAL_START_TOKEN, USB_MOTA_START_TOKEN); #else usb_serial_interface.begin(Serial, USB_TERMINAL_START_TOKEN); +#endif + // 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 + // activity: a real client has to send us frames. + usb_serial_interface.setConnectedCheck([]() { + uint32_t last = usb_serial_interface.getLastFrameMillis(); + return (bool)Serial && last != 0 && (millis() - last) < USB_CLIENT_IDLE_TIMEOUT; + }); +#elif (defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT) \ + || defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) + // native USB-CDC (TinyUSB): (bool)Serial reflects DTR, ie. the host really + // has the port open. A classic ESP32 behind a UART bridge has no such + // signal and keeps the assume-connected default. + usb_serial_interface.setConnectedCheck([]() { return (bool)Serial; }); #endif interface_manager.addInterface(InterfaceType::USB, &usb_serial_interface); #endif diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index afa70084..c4c33d60 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -66,7 +66,8 @@ void ArduinoSerialInterface::disable() { resetReceiveState(); } -bool ArduinoSerialInterface::isConnected() const { +bool ArduinoSerialInterface::isConnected() const { + if (_conn_check) return _conn_check(); return true; // no way of knowing, so assume yes } @@ -75,6 +76,12 @@ bool ArduinoSerialInterface::isReadBusy() const { } bool ArduinoSerialInterface::isWriteBusy() const { + if (_passthroughMode) return false; + 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; } @@ -84,6 +91,17 @@ size_t ArduinoSerialInterface::writeFrame(const uint8_t src[], size_t len) { return 0; } if (_passthroughMode) return len; + 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] = '>'; @@ -133,6 +151,7 @@ size_t ArduinoSerialInterface::checkRecvFrame(uint8_t dest[]) { if (_frame_len > MAX_FRAME_SIZE) _frame_len = MAX_FRAME_SIZE; // truncate memcpy(dest, rx_buf, _frame_len); _state = RECV_STATE_IDLE; // reset state, for next frame + _last_frame_ms = millis(); // a real client is talking to us return _frame_len; } } diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index 6922a161..fe3f5dfe 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -4,18 +4,26 @@ #include class ArduinoSerialInterface : public BaseSerialInterface { +public: + // reports whether a client currently has this stream open (see setConnectedCheck) + typedef bool (*ConnectedCheck)(); + +private: bool _isEnabled; bool _passthroughMode; bool _controlSequenceReceived; bool _secondaryControlSequenceReceived; + bool _flow_ctl; uint8_t _state; size_t _controlSequencePos; size_t _secondaryControlSequencePos; uint16_t _frame_len; uint16_t rx_len; + uint32_t _last_frame_ms; Stream* _serial; const char* _controlSequence; const char* _secondaryControlSequence; + ConnectedCheck _conn_check; uint8_t rx_buf[MAX_FRAME_SIZE]; bool checkControlSequence(uint8_t c, const char* sequence, @@ -26,9 +34,10 @@ public: ArduinoSerialInterface() : _isEnabled(false), _passthroughMode(false), _controlSequenceReceived(false), _secondaryControlSequenceReceived(false), - _state(0), _controlSequencePos(0), _secondaryControlSequencePos(0), - _frame_len(0), rx_len(0), _serial(nullptr), - _controlSequence(nullptr), _secondaryControlSequence(nullptr) {} + _flow_ctl(false), _state(0), _controlSequencePos(0), + _secondaryControlSequencePos(0), _frame_len(0), rx_len(0), + _last_frame_ms(0), _serial(nullptr), _controlSequence(nullptr), + _secondaryControlSequence(nullptr), _conn_check(nullptr) {} void begin(Stream& serial, const char* controlSequence = nullptr, const char* secondaryControlSequence = nullptr) { @@ -38,6 +47,7 @@ public: _passthroughMode = false; _controlSequenceReceived = false; _secondaryControlSequenceReceived = false; + _last_frame_ms = 0; resetReceiveState(); #ifdef RAK_4631 pinMode(WB_IO2, OUTPUT); @@ -54,6 +64,20 @@ public: bool takeControlSequence(); bool takeSecondaryControlSequence(); + // Optional: let the target report the real link state, e.g. USB-CDC DTR. + // Without it isConnected() assumes true, as a plain UART has no way of knowing. + void setConnectedCheck(ConnectedCheck fn) { _conn_check = fn; } + + // millis() of the last completely received frame, 0 if none since boot. + // 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;