Merge PR #3214: companion USB connection and flow control

Preserve the newer terminal and mOTA passthrough paths while adding real USB client detection and whole-frame transmit pacing.
This commit is contained in:
mikecarper committed 2026-08-17 23:55:40 -07:00
commit 68c67f2cd4
3 files changed
+75 -4

No files matched your search

+28
View File
@@ -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
+20 -1
View File
@@ -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<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;
}
@@ -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;
}
}
+27 -3
View File
@@ -4,18 +4,26 @@
#include <Arduino.h>
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;