Protect ESP32 HWCDC console sessions

This commit is contained in:
mikecarper committed 2026-09-04 17:18:54 -07:00
1 parent 36435b450f
commit b724d19d6f
18 files changed
+196 -104

No files matched your search

+1 -30
View File
@@ -1978,37 +1978,8 @@ void halt() {
#endif
void setup() {
#if defined(ESP32) && defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 1 \
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
&& defined(ENABLE_USB_INTERFACE)
// Configure HWCDC before begin() allocates its mutex and enables the USB ISR.
// The bundled setTxBufferSize() deletes/recreates its ring without taking the
// TX mutex or masking that ISR, so even an otherwise-quiet post-begin resize
// can race the interrupt handler.
// A failed resize deletes HWCDC's existing ring. Try progressively smaller
// queues while setup still has no producers, and tell reset cleanup the exact
// capacity it must observe before reopening a host session.
static const size_t usb_tx_sizes[] = {
MESH_ESP32_USB_TX_BUFFER_SIZE, 2048, 1024, 512, 256};
size_t usb_tx_capacity = 0;
for (size_t candidate : usb_tx_sizes) {
usb_tx_capacity = Serial.setTxBufferSize(candidate);
if (usb_tx_capacity == candidate) break;
}
Serial.setTxTimeoutMs(5);
#endif
mesh::prepareUsbLoggingPort();
Serial.begin(115200);
#if defined(ESP32) && defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 1 \
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
&& defined(ENABLE_USB_INTERFACE)
// If every pre-begin allocation failed, begin() may have recovered by
// creating its built-in 256-byte ring. Discover that ring without replacing
// it after the ISR is live; a zero result leaves reset cleanup quarantined.
if (usb_tx_capacity == 0) {
usb_tx_capacity = Serial.availableForWrite();
}
mesh::setUsbCompanionTxBufferCapacity(usb_tx_capacity);
#endif
#if MESH_PACKET_LOGGING
mesh::serialLogBegin();
#endif
+6 -6
View File
@@ -2402,7 +2402,7 @@ void MyMesh::printRecentRepeatersSerial() {
return;
}
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
if (hasPendingSerialOutput()) {
mesh::usbConsolePort().printf("Err - USB output busy\r\n");
return;
@@ -4845,7 +4845,7 @@ void MyMesh::updateFloodAdvertTimer() {
}
void MyMesh::dumpLogFile() {
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
if (hasPendingSerialOutput()) {
mesh::usbConsolePort().printf("Err - USB output busy\r\n");
return;
@@ -4869,7 +4869,7 @@ void MyMesh::dumpLogFile() {
#endif
}
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
bool MyMesh::hasPendingSerialOutput() const {
return serial_log_active || serial_log_eof_pending || serial_recent_next >= 0;
}
@@ -11536,7 +11536,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
// Compatibility path for manually defined legacy portable builds. Current
// release builds use FULL and do not enter this branch.
_cli.handleCommand(sender_timestamp, command, reply);
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
if (sender_timestamp == 0 && serial_log_eof_pending
&& strcmp(reply, " EOF") == 0) reply[0] = 0;
#endif
@@ -11942,7 +11942,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
#endif
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
if (sender_timestamp == 0 && serial_log_eof_pending
&& strcmp(reply, " EOF") == 0) reply[0] = 0;
#endif
@@ -11956,7 +11956,7 @@ void MyMesh::loop() {
// Check radio FIRST to ensure we don't miss incoming packets
// MQTT processing runs in a separate FreeRTOS task on Core 0, so we don't call bridge.loop() here
mesh::Mesh::loop();
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
servicePendingSerialOutput();
#endif
_cli.loop();
+2 -2
View File
@@ -288,7 +288,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
};
FILESYSTEM* _fs;
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
File serial_log_dump;
size_t serial_log_remaining = 0;
size_t serial_log_pending_size = 0;
@@ -982,7 +982,7 @@ public:
}
void dumpLogFile() override;
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
// Large local-only replies advance between radio service passes.
bool hasPendingSerialOutput() const;
void servicePendingSerialOutput();
+5 -4
View File
@@ -71,8 +71,9 @@ static unsigned long userBtnDownAt = 0;
#endif
void setup() {
mesh::prepareUsbLoggingPort();
Serial.begin(115200);
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::beginUsbLoggingPort();
#endif
#if MESH_PACKET_LOGGING
@@ -116,7 +117,7 @@ void setup() {
radioinit_attempts = 0;
const uint32_t retry_started = millis();
while (millis() - retry_started < 60000UL) {
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbLoggingPort();
mesh::serviceUsbTerminalPort();
#endif
@@ -224,7 +225,7 @@ void setup() {
static void __attribute__((noinline)) serviceCommandInterfaces() {
bool usb_ready = true;
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbLoggingPort();
mesh::serviceUsbTerminalPort();
if (mesh::takeUsbTerminalSessionReset()) {
@@ -351,7 +352,7 @@ void loop() {
if (display_ready) ui_task.loop();
#endif
rtc_clock.tick();
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbTerminalPort();
#endif
+4 -4
View File
@@ -1577,7 +1577,7 @@ void MyMesh::updateFloodAdvertTimer() {
}
void MyMesh::dumpLogFile() {
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
if (hasPendingSerialOutput()) {
mesh::usbConsolePort().printf("Err - USB output busy\r\n");
return;
@@ -1601,7 +1601,7 @@ void MyMesh::dumpLogFile() {
#endif
}
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
bool MyMesh::hasPendingSerialOutput() const {
return serial_log_active || serial_log_eof_pending;
}
@@ -2415,7 +2415,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
// handled by the role-independent mesh clock synchronizer
} else {
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
if (sender_timestamp == 0 && serial_log_eof_pending
&& strcmp(reply, " EOF") == 0) reply[0] = 0;
#endif
@@ -2433,7 +2433,7 @@ void MyMesh::loop() {
// Check radio FIRST to ensure we don't miss incoming packets
// MQTT processing can take time, so we prioritize radio reception
mesh::Mesh::loop();
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
servicePendingSerialOutput();
#endif
_cli.loop();
+2 -2
View File
@@ -158,7 +158,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks,
#endif
{
FILESYSTEM* _fs;
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
File serial_log_dump;
size_t serial_log_remaining = 0;
size_t serial_log_pending_size = 0;
@@ -463,7 +463,7 @@ public:
}
void dumpLogFile() override;
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
// Large local-only replies advance between radio service passes.
bool hasPendingSerialOutput() const;
void servicePendingSerialOutput();
+4 -3
View File
@@ -43,8 +43,9 @@ static char ethernet_command[MAX_POST_TEXT_LEN+1];
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot
void setup() {
mesh::prepareUsbLoggingPort();
Serial.begin(115200);
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::beginUsbLoggingPort();
#endif
#if MESH_PACKET_LOGGING
@@ -157,7 +158,7 @@ void loop() {
board.feedWatchdog(the_mesh.getNodePrefs()->system_watchdog_enabled != 0);
#endif
bool usb_ready = true;
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbLoggingPort();
mesh::serviceUsbTerminalPort();
if (mesh::takeUsbTerminalSessionReset()) {
@@ -219,7 +220,7 @@ void loop() {
if (display_ready) ui_task.loop();
#endif
rtc_clock.tick();
#if MESH_ESP32_TINYUSB_NONBLOCKING
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbTerminalPort();
#endif
#ifdef TBEAM_1W
+6 -4
View File
@@ -4,6 +4,7 @@
#include <helpers/ClientPathPersistence.h>
#include <helpers/LazyPersistence.h>
#include <helpers/radiolib/RXPowerSaving.h>
#include <helpers/UsbLogging.h>
static uint32_t nextRadioApplyRetryDelay(uint8_t& failure_count) {
uint8_t shift = failure_count < 5 ? failure_count : 5;
@@ -623,14 +624,15 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
}
}
} else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
Serial.println("ACL:");
Stream& console = mesh::usbConsolePort();
console.println("ACL:");
for (int i = 0; i < acl.getNumClients(); i++) {
auto c = acl.getClientByIdx(i);
if (c->permissions == 0) continue; // skip deleted entries
Serial.printf("%02X ", c->permissions);
mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
Serial.printf("\n");
console.printf("%02X ", c->permissions);
mesh::Utils::printHex(console, c->id.pub_key, PUB_KEY_SIZE);
console.print('\n');
}
reply[0] = 0;
} else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read
+26 -6
View File
@@ -1,5 +1,6 @@
#include "SensorMesh.h"
#include <helpers/IdentityGeneration.h>
#include <helpers/UsbLogging.h>
#if defined(ESP32_PLATFORM)
#include <helpers/ESP32TrueRandom.h>
@@ -59,7 +60,11 @@ static char command[160];
static const unsigned long POWERSAVING_FIRST_SLEEP_SECS = 120;
void setup() {
mesh::prepareUsbLoggingPort();
Serial.begin(115200);
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::beginUsbLoggingPort();
#endif
delay(1000);
board.begin();
@@ -124,8 +129,10 @@ void setup() {
return;
}
Serial.print("Sensor ID: ");
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
Stream& console = mesh::usbConsolePort();
console.print("Sensor ID: ");
mesh::Utils::printHex(console, the_mesh.self_id.pub_key, PUB_KEY_SIZE);
console.println();
command[0] = 0;
@@ -149,14 +156,23 @@ void loop() {
#if defined(NRF52_PLATFORM)
board.feedWatchdog(the_mesh.getNodePrefs()->system_watchdog_enabled != 0);
#endif
bool usb_ready = true;
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbLoggingPort();
mesh::serviceUsbTerminalPort();
if (mesh::takeUsbTerminalSessionReset()) command[0] = 0;
usb_ready = mesh::tryCompleteUsbTerminalSessionReset()
&& mesh::canAcceptUsbConsoleCommand();
#endif
Stream& console = mesh::usbConsolePort();
int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read();
while (usb_ready && console.available() && len < sizeof(command)-1) {
char c = console.read();
if (c != '\n') {
command[len++] = c;
command[len] = 0;
}
Serial.print(c);
console.print(c);
}
if (len == sizeof(command)-1) { // command buffer full
command[sizeof(command)-1] = '\r';
@@ -168,7 +184,8 @@ void loop() {
reply[0] = 0;
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
console.print(" -> ");
console.println(reply);
}
command[0] = 0; // reset command buffer
@@ -180,6 +197,9 @@ void loop() {
ui_task.loop();
#endif
rtc_clock.tick();
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbTerminalPort();
#endif
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
+4 -4
View File
@@ -24,7 +24,7 @@
#define MAX_TRANS_UNIT 255
#if defined(ARDUINO) && \
(defined(NRF52_PLATFORM) || MESH_ESP32_TINYUSB_NONBLOCKING) && \
(defined(NRF52_PLATFORM) || MESH_ESP32_USB_CONSOLE_COOPERATIVE) && \
((defined(MESH_DEBUG) && MESH_DEBUG) || \
(defined(BRIDGE_DEBUG) && BRIDGE_DEBUG) || \
(defined(POWERSAVING_DEBUG) && POWERSAVING_DEBUG))
@@ -86,7 +86,7 @@ inline size_t nrf52DebugPrintf(const char* format, ...) {
#if MESH_DEBUG && ARDUINO
#include <Arduino.h>
#if defined(NRF52_PLATFORM) || MESH_ESP32_TINYUSB_NONBLOCKING
#if defined(NRF52_PLATFORM) || MESH_ESP32_USB_CONSOLE_COOPERATIVE
#define MESH_DEBUG_PRINT(F, ...) do { mesh::nrf52DebugPrintf("DEBUG: " F, ##__VA_ARGS__); } while(0)
#define MESH_DEBUG_PRINTLN(F, ...) do { mesh::nrf52DebugPrintf("DEBUG: " F "\n", ##__VA_ARGS__); } while(0)
#else
@@ -99,7 +99,7 @@ inline size_t nrf52DebugPrintf(const char* format, ...) {
#endif
#if BRIDGE_DEBUG && ARDUINO
#if defined(NRF52_PLATFORM) || MESH_ESP32_TINYUSB_NONBLOCKING
#if defined(NRF52_PLATFORM) || MESH_ESP32_USB_CONSOLE_COOPERATIVE
#define BRIDGE_DEBUG_PRINTLN(F, ...) do { mesh::nrf52DebugPrintf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__); } while(0)
#else
#define BRIDGE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__); } } while(0)
@@ -110,7 +110,7 @@ inline size_t nrf52DebugPrintf(const char* format, ...) {
#if POWERSAVING_DEBUG && ARDUINO
#include <Arduino.h>
#if defined(NRF52_PLATFORM) || MESH_ESP32_TINYUSB_NONBLOCKING
#if defined(NRF52_PLATFORM) || MESH_ESP32_USB_CONSOLE_COOPERATIVE
#define POWERSAVING_DEBUG_PRINT(F, ...) do { mesh::nrf52DebugPrintf("POWERSAVING: " F, ##__VA_ARGS__); } while(0)
#define POWERSAVING_DEBUG_PRINTLN(F, ...) do { mesh::nrf52DebugPrintf("POWERSAVING: " F "\n", ##__VA_ARGS__); } while(0)
#else
+3 -3
View File
@@ -1124,9 +1124,9 @@ void ESP32Board::enterDeepSleep(uint32_t secs) {
sensors.getLocationProvider()->stop();
}
// Native TinyUSB must not wait for a host which has stopped reading.
// Keep the original flush behavior for UART and USB-Serial-JTAG targets.
#if MESH_ESP32_TINYUSB_NONBLOCKING
// Native USB must not wait for a host which has stopped reading.
// Keep the original flush behavior for UART targets.
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbTerminalPort();
#else
Serial.flush();
+42 -10
View File
@@ -5,14 +5,9 @@
#include <atomic>
#include <stdio.h>
#if defined(ESP32) && defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 1 \
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
&& defined(ENABLE_USB_INTERFACE)
#define MESH_ESP32_HWCDC_SESSION_GUARD 1
#if MESH_ESP32_HWCDC_SESSION_GUARD
#include "esp_idf_version.h"
#include "hal/usb_serial_jtag_ll.h"
#else
#define MESH_ESP32_HWCDC_SESSION_GUARD 0
#endif
#if defined(NRF52_PLATFORM) || MESH_ESP32_HWCDC_SESSION_GUARD \
@@ -296,8 +291,7 @@ static std::atomic<bool> esp32_hwcdc_tx_kick_pending{false};
static portMUX_TYPE esp32_hwcdc_session_mux = portMUX_INITIALIZER_UNLOCKED;
static uint32_t esp32_hwcdc_taken_bus_reset_generation = 0;
static bool esp32_hwcdc_event_handler_registered = false;
static std::atomic<size_t> esp32_hwcdc_tx_buffer_capacity{
MESH_ESP32_USB_TX_BUFFER_SIZE};
static std::atomic<size_t> esp32_hwcdc_tx_buffer_capacity{0};
// A busy writer or temporary allocation failure may require more than one main
// loop to purge. Keep the original detach state/generation across retries so a
// failed attempt never reattaches stale bytes or turns into a reboot loop.
@@ -530,7 +524,8 @@ static void setPlatformDebugOutputEnabled(bool enabled) {
// bytes from racing a binary mOTA/Companion record's capacity preflight.
(void)enabled;
Serial.setDebugOutput(false);
#elif defined(ESP32_PLATFORM) && defined(ENABLE_USB_INTERFACE)
#elif defined(ESP32_PLATFORM) \
&& (defined(ENABLE_USB_INTERFACE) || MESH_ESP32_HWCDC_SESSION_GUARD)
// Arduino-ESP32 log_e()/ESP-IDF diagnostics otherwise write straight to
// the same UART/CDC stream used by Binary Companion.
#if MESH_ESP32_HWCDC_SESSION_GUARD
@@ -789,6 +784,23 @@ void setUsbCompanionTxBufferCapacity(size_t capacity) {
#endif
}
void prepareUsbLoggingPort() {
#if MESH_ESP32_HWCDC_SESSION_GUARD
// HWCDC::setTxBufferSize() deletes/recreates its ring without taking the TX
// mutex or masking the USB ISR. Resize only during early setup, before
// Serial.begin() creates that mutex and enables the interrupt handler.
static const size_t usb_tx_sizes[] = {
MESH_ESP32_USB_TX_BUFFER_SIZE, 2048, 1024, 512, 256};
size_t capacity = 0;
for (size_t candidate : usb_tx_sizes) {
capacity = Serial.setTxBufferSize(candidate);
if (capacity == candidate) break;
}
Serial.setTxTimeoutMs(5);
setUsbCompanionTxBufferCapacity(capacity);
#endif
}
void beginUsbLoggingPort() {
// setup() calls this once before role preferences are loaded and again
// afterwards. The first call silences framework diagnostics on a protected
@@ -802,6 +814,11 @@ void beginUsbLoggingPort() {
}
#endif
#if MESH_ESP32_HWCDC_SESSION_GUARD
// If every pre-begin resize failed, begin() may have recovered by allocating
// HWCDC's built-in ring. Discover it without replacing a live ISR-owned ring.
if (esp32_hwcdc_tx_buffer_capacity.load(std::memory_order_acquire) == 0) {
setUsbCompanionTxBufferCapacity(Serial.availableForWrite());
}
if (!esp32_hwcdc_event_handler_registered) {
Serial.onEvent(ARDUINO_HW_CDC_ANY_EVENT, handleEsp32HwcdcEvent);
esp32_hwcdc_event_handler_registered = true;
@@ -833,6 +850,8 @@ void beginUsbLoggingPort() {
void serviceUsbLoggingPort() {
#if MESH_ESP32_TINYUSB_NONBLOCKING
serviceEsp32TinyUsbPorts();
#elif MESH_ESP32_HWCDC_SESSION_GUARD
serviceEsp32HwcdcTxKick();
#endif
#if defined(MESH_DUAL_CDC_LOGGING)
const bool connected = dedicated_usb_logging_port_started
@@ -1061,6 +1080,8 @@ Stream& usbTerminalPort() {
Stream& usbConsolePort() {
#if MESH_ESP32_TINYUSB_NONBLOCKING
return buffered_esp32_tinyusb_terminal_port;
#elif MESH_ESP32_HWCDC_SESSION_GUARD
return guarded_esp32_hwcdc_port;
#else
return Serial;
#endif
@@ -1070,6 +1091,10 @@ bool canAcceptUsbConsoleCommand() {
#if MESH_ESP32_TINYUSB_NONBLOCKING
return buffered_esp32_tinyusb_terminal_port.availableForWrite()
>= static_cast<int>(esp32_tinyusb_functional_reserve);
#elif MESH_ESP32_HWCDC_SESSION_GUARD
// Leave enough room for a complete ordinary CLI response. Larger listings
// already retain and retry a short write from their cooperative pump.
return guarded_esp32_hwcdc_port.availableForWrite() >= 256;
#else
return true;
#endif
@@ -1141,7 +1166,7 @@ bool takeUsbTerminalSessionReset() {
}
primary_usb_terminal_taken_reset_generation = reset_generation;
return true;
#elif MESH_ESP32_HWCDC_SESSION_GUARD && defined(ENABLE_USB_INTERFACE)
#elif MESH_ESP32_HWCDC_SESSION_GUARD
const uint32_t reset_generation =
esp32_hwcdc_bus_reset_generation.load(std::memory_order_acquire);
if (reset_generation == esp32_hwcdc_taken_bus_reset_generation) {
@@ -1181,6 +1206,13 @@ bool tryCompleteUsbTerminalSessionReset() {
if ((int32_t)(millis() - settle_until) < 0) return false;
return nonblocking_primary_usb_companion_port.tryRunExclusive(
completePrimaryUsbSessionReset);
#elif MESH_ESP32_HWCDC_SESSION_GUARD
if (!esp32_hwcdc_cleanup_pending
&& esp32_hwcdc_allowed_generation.load(std::memory_order_acquire)
== esp32_hwcdc_access_generation.load(std::memory_order_acquire)) {
return true;
}
return resetUsbCompanionTransport();
#else
return true;
#endif
+25 -3
View File
@@ -24,6 +24,23 @@
#define MESH_ESP32_TINYUSB_NONBLOCKING 0
#endif
// ESP32-S3 USB-Serial/JTAG (HWCDC) has a software TX ring which can retain a
// previous host's bytes across a USB bus reset. This capability is independent
// of the Companion role: room, repeater, and sensor consoles need the same
// session gate and bounded writes.
#if defined(ESP32) && defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 1 \
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
#define MESH_ESP32_HWCDC_SESSION_GUARD 1
#else
#define MESH_ESP32_HWCDC_SESSION_GUARD 0
#endif
#if MESH_ESP32_TINYUSB_NONBLOCKING || MESH_ESP32_HWCDC_SESSION_GUARD
#define MESH_ESP32_USB_CONSOLE_COOPERATIVE 1
#else
#define MESH_ESP32_USB_CONSOLE_COOPERATIVE 0
#endif
namespace mesh {
#ifndef MESH_ESP32_USB_TX_BUFFER_SIZE
@@ -59,8 +76,13 @@ bool saveUsbLoggingBootPreference(bool enabled);
// instead of mistaking an allocation failure for a permanently non-empty ring.
void setUsbCompanionTxBufferCapacity(size_t capacity);
// Configure ESP32 HWCDC's bounded write timeout and TX ring before Serial.begin
// creates its mutex and enables the USB ISR. Safe as a no-op on other ports.
void prepareUsbLoggingPort();
// Start the optional dedicated USB logging interface. Ordinary and single-TTY
// builds use Serial; nRF52 Full Companion uses a second CDC ACM port.
// builds use Serial; nRF52 Full Companion uses a second CDC ACM port. Call this
// after Serial.begin() so native USB roles also register session events.
void beginUsbLoggingPort();
// Enable owner-task service when a host opens the dedicated logging endpoint.
// That task emits the identity marker from the actual TinyUSB write result and
@@ -83,8 +105,8 @@ Stream& usbMotaPort();
// it from the application loop; discard it before changing the CDC protocol or
// after a host disconnect so stale text cannot prefix a later Binary session.
Stream& usbTerminalPort();
// Repeater/room-server console: protect ESP32 TinyUSB while preserving the
// historical raw Serial behavior of other platforms and roles.
// Repeater/room-server/sensor console: protect both ESP32 native USB transports
// while preserving the historical raw Serial behavior of other platforms.
Stream& usbConsolePort();
// Diagnostic backlog alone must not starve console input. Native ESP32 CDC
// admits another command when its reserved functional capacity is available;
+1 -6
View File
@@ -26,12 +26,7 @@
#include "../UsbLogging.h"
#ifndef OTA_FOLDER_SERIAL_STREAM
#if defined(NRF52_PLATFORM) \
|| MESH_ESP32_TINYUSB_NONBLOCKING \
|| (defined(ESP32) && defined(ARDUINO_USB_MODE) \
&& ARDUINO_USB_MODE == 1 \
&& defined(ARDUINO_USB_CDC_ON_BOOT) \
&& ARDUINO_USB_CDC_ON_BOOT \
&& defined(ENABLE_USB_INTERFACE))
|| MESH_ESP32_USB_CONSOLE_COOPERATIVE
// Native USB serial-folder requests share the primary session facade.
// It prevents a host reset from racing a cached mOTA Stream reference;
// dedicated-UART overrides retain their normal stream.
@@ -24,6 +24,7 @@ HARNESS = r'''
#include <cstdint>
#include <helpers/FileRead.h>
#define MESH_ESP32_TINYUSB_NONBLOCKING 1
#define MESH_ESP32_USB_CONSOLE_COOPERATIVE 1
#define MAX_ROUTE_HASH_BYTES 3
#define PACKET_LOG_FILE "/packet_log"
static void require(bool ok, const char* what) {
@@ -248,7 +249,7 @@ class CooperativeOutputTest(unittest.TestCase):
with self.subTest(role=role), tempfile.TemporaryDirectory() as temporary:
text = (ROOT / f"examples/{role}/MyMesh.cpp").read_text()
dump_start = text.index("void MyMesh::dumpLogFile()")
dump_end = text.index("\n#if MESH_ESP32_TINYUSB_NONBLOCKING\nbool MyMesh::hasPendingSerialOutput", dump_start)
dump_end = text.index("\n#if MESH_ESP32_USB_CONSOLE_COOPERATIVE\nbool MyMesh::hasPendingSerialOutput", dump_start)
pump_start = text.index("bool MyMesh::hasPendingSerialOutput()", dump_end)
pump_end = text.index("\n#endif", pump_start)
methods = text[dump_start:dump_end] + "\n" + text[pump_start:pump_end]
+7 -3
View File
@@ -18,13 +18,17 @@ class Esp32TinyUsbNonblockingTest(unittest.TestCase):
if compiler is None:
self.skipTest("a host C++17 compiler is required")
with tempfile.TemporaryDirectory(prefix=".tmp-usb-mode0-", dir=ROOT) as temporary:
for mode, companion in ((0, False), (0, True), (1, False)):
with self.subTest(usb_mode=mode, companion=companion):
for mode, companion, cdc_on_boot in (
(0, False, 1), (0, True, 1), (1, False, 0)
):
with self.subTest(usb_mode=mode, companion=companion,
cdc_on_boot=cdc_on_boot):
binary = Path(temporary) / f"usb-mode-{mode}-{companion}.exe"
command = [
compiler, "-std=c++17", "-DARDUINO=1", "-DESP32=1",
"-DESP32_PLATFORM=1", "-DMESH_DEBUG=1",
"-DARDUINO_USB_CDC_ON_BOOT=1", f"-DARDUINO_USB_MODE={mode}",
f"-DARDUINO_USB_CDC_ON_BOOT={cdc_on_boot}",
f"-DARDUINO_USB_MODE={mode}",
f"-I{FIXTURE / 'mocks'}", f"-I{ROOT / 'src'}",
str(FIXTURE / "test_esp32_tinyusb_nonblocking.cpp"),
str(ROOT / "src/helpers/UsbLogging.cpp"),
+39 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Keep native-TinyUSB repeater/room output away from blocking Arduino writes."""
"""Keep ESP32 native-USB role output away from blocking Arduino writes."""
from pathlib import Path
import re
@@ -22,6 +22,8 @@ class Esp32TinyUsbRoleHygieneTest(unittest.TestCase):
r"Stream& usbConsolePort\(\)\s*\{\s*"
r"#if MESH_ESP32_TINYUSB_NONBLOCKING\s+"
r"return buffered_esp32_tinyusb_terminal_port;\s+"
r"#elif MESH_ESP32_HWCDC_SESSION_GUARD\s+"
r"return guarded_esp32_hwcdc_port;\s+"
r"#else\s+return Serial;\s+#endif",
)
@@ -98,14 +100,49 @@ class Esp32TinyUsbRoleHygieneTest(unittest.TestCase):
text = source(f"examples/{role}/main.cpp")
setup = text[text.index("void setup()") : text.index("void loop()")]
loop = text[text.index("void loop()") :]
self.assertLess(
setup.index("mesh::prepareUsbLoggingPort();"),
setup.index("Serial.begin(115200);"),
)
self.assertIn("mesh::beginUsbLoggingPort();", setup)
self.assertIn("mesh::serviceUsbTerminalPort();", loop)
def test_sensor_uses_the_same_guarded_console_lifecycle(self):
main = source("examples/simple_sensor/main.cpp")
sensor = source("examples/simple_sensor/SensorMesh.cpp")
setup = main[main.index("void setup()") : main.index("void loop()")]
loop = main[main.index("void loop()") :]
self.assertLess(
setup.index("mesh::prepareUsbLoggingPort();"),
setup.index("Serial.begin(115200);"),
)
self.assertIn("mesh::beginUsbLoggingPort();", setup)
self.assertIn("mesh::takeUsbTerminalSessionReset()", loop)
self.assertIn("mesh::tryCompleteUsbTerminalSessionReset()", loop)
self.assertIn("mesh::canAcceptUsbConsoleCommand()", loop)
for text in (main, sensor):
self.assertNotRegex(
text,
r"\bSerial\s*\.\s*(?:print|println|printf|write|flush|available|read|peek)\s*\(",
)
self.assertNotRegex(text, r"\bprintHex\s*\(\s*Serial\b")
self.assertIn("mesh::usbConsolePort()", main)
self.assertIn("mesh::usbConsolePort()", sensor)
def test_hwcdc_guard_is_not_limited_to_companion_builds(self):
header = source("src/helpers/UsbLogging.h")
start = header.index("#if defined(ESP32) && defined(ARDUINO_USB_MODE)",
header.index("MESH_ESP32_HWCDC_SESSION_GUARD") - 300)
end = header.index("#if MESH_ESP32_TINYUSB_NONBLOCKING", start)
guard = header[start:end]
self.assertIn("ARDUINO_USB_MODE == 1", guard)
self.assertNotIn("ENABLE_USB_INTERFACE", guard)
def test_sleep_keeps_raw_flush_only_for_other_esp32_transports(self):
text = source("src/helpers/ESP32Board.cpp")
sleep = text[text.index("void ESP32Board::enterDeepSleep(") :]
guard = re.search(
r"#if MESH_ESP32_TINYUSB_NONBLOCKING\s+"
r"#if MESH_ESP32_USB_CONSOLE_COOPERATIVE\s+"
r"mesh::serviceUsbTerminalPort\(\);\s+"
r"#else\s+Serial\.flush\(\);\s+#endif",
sleep,
+17 -11
View File
@@ -53,9 +53,9 @@ class Esp32UsbSerialHygieneTest(unittest.TestCase):
hwcdc = main[start : main.index("#elif", start)]
serial_begin = main.index("Serial.begin(115200);")
tx_buffer = main.index("Serial.setTxBufferSize(candidate);")
prepare = main.index("mesh::prepareUsbLoggingPort();")
logging_begin = main.index("mesh::beginUsbLoggingPort();")
self.assertLess(tx_buffer, serial_begin)
self.assertLess(prepare, serial_begin)
self.assertLess(serial_begin, logging_begin)
self.assertIn("board.isUsbHostConnected()", hwcdc)
@@ -151,12 +151,20 @@ class Esp32UsbSerialHygieneTest(unittest.TestCase):
self.assertNotIn("Serial.begin(", purge)
setup = main[main.index("void setup()") : main.index("board.begin();")]
self.assertIn("static const size_t usb_tx_sizes[]", setup)
self.assertIn("Serial.setTxBufferSize(candidate)", setup)
self.assertIn("usb_tx_capacity = Serial.availableForWrite();", setup)
self.assertIn(
"mesh::setUsbCompanionTxBufferCapacity(usb_tx_capacity)", setup
)
self.assertIn("mesh::prepareUsbLoggingPort();", setup)
prepare_start = logging.index("void prepareUsbLoggingPort()")
prepare = logging[prepare_start : logging.index(
"void beginUsbLoggingPort()", prepare_start
)]
self.assertIn("static const size_t usb_tx_sizes[]", prepare)
self.assertIn("Serial.setTxBufferSize(candidate)", prepare)
self.assertIn("Serial.setTxTimeoutMs(5);", prepare)
begin_start = logging.index("void beginUsbLoggingPort()")
begin = logging[begin_start : logging.index(
"void serviceUsbLoggingPort()", begin_start
)]
self.assertIn("Serial.availableForWrite()", begin)
self.assertIn("setUsbCompanionTxBufferCapacity(", begin)
debug_start = logging.index(
"static void setPlatformDebugOutputEnabled(bool enabled)"
@@ -189,9 +197,7 @@ class Esp32UsbSerialHygieneTest(unittest.TestCase):
ota_context.index("#ifndef OTA_FOLDER_SERIAL_STREAM") :
ota_context.index("#ifndef OTA_FOLDER_SERIAL_BAUD")
]
self.assertIn("defined(ESP32)", ota_stream)
self.assertIn("ARDUINO_USB_MODE == 1", ota_stream)
self.assertIn("defined(ENABLE_USB_INTERFACE)", ota_stream)
self.assertIn("MESH_ESP32_USB_CONSOLE_COOPERATIVE", ota_stream)
self.assertIn("::mesh::usbMotaPort()", ota_stream)
self.assertIn(