diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 1dcb6326..f3888331 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -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 diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 28760f48..1c7551b1 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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(); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 861a03f9..3bae24f6 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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(); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 05496f02..15b93c70 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -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 diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 263524f0..9aadd2c5 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -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(); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index c04b729a..4c53f6c2 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -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(); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index d5b58b77..c28a2634 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -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 diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index d21a6441..992aaf15 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -4,6 +4,7 @@ #include #include #include +#include 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 diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 776b9723..2bba49dd 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -1,5 +1,6 @@ #include "SensorMesh.h" #include +#include #if defined(ESP32_PLATFORM) #include @@ -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 diff --git a/src/MeshCore.h b/src/MeshCore.h index 79819e6d..252a1cc4 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -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 - #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 - #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 diff --git a/src/helpers/ESP32Board.cpp b/src/helpers/ESP32Board.cpp index 2857c425..7f681471 100644 --- a/src/helpers/ESP32Board.cpp +++ b/src/helpers/ESP32Board.cpp @@ -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(); diff --git a/src/helpers/UsbLogging.cpp b/src/helpers/UsbLogging.cpp index 2f79dd79..9c1674e1 100644 --- a/src/helpers/UsbLogging.cpp +++ b/src/helpers/UsbLogging.cpp @@ -5,14 +5,9 @@ #include #include -#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 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 esp32_hwcdc_tx_buffer_capacity{ - MESH_ESP32_USB_TX_BUFFER_SIZE}; +static std::atomic 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(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 diff --git a/src/helpers/UsbLogging.h b/src/helpers/UsbLogging.h index 3de340ae..1a6c96f4 100644 --- a/src/helpers/UsbLogging.h +++ b/src/helpers/UsbLogging.h @@ -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; diff --git a/src/helpers/ota/OtaContext.h b/src/helpers/ota/OtaContext.h index cc13cfad..079df3d8 100644 --- a/src/helpers/ota/OtaContext.h +++ b/src/helpers/ota/OtaContext.h @@ -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. diff --git a/test/test_esp32_tinyusb_cooperative_output.py b/test/test_esp32_tinyusb_cooperative_output.py index 3861c5ca..72067642 100644 --- a/test/test_esp32_tinyusb_cooperative_output.py +++ b/test/test_esp32_tinyusb_cooperative_output.py @@ -24,6 +24,7 @@ HARNESS = r''' #include #include #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] diff --git a/test/test_esp32_tinyusb_nonblocking.py b/test/test_esp32_tinyusb_nonblocking.py index de89598d..9138a12e 100644 --- a/test/test_esp32_tinyusb_nonblocking.py +++ b/test/test_esp32_tinyusb_nonblocking.py @@ -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"), diff --git a/test/test_esp32_tinyusb_role_hygiene.py b/test/test_esp32_tinyusb_role_hygiene.py index 2bc746f0..22278737 100644 --- a/test/test_esp32_tinyusb_role_hygiene.py +++ b/test/test_esp32_tinyusb_role_hygiene.py @@ -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, diff --git a/test/test_esp32_usb_serial_hygiene.py b/test/test_esp32_usb_serial_hygiene.py index 47dc1132..5806d926 100644 --- a/test/test_esp32_usb_serial_hygiene.py +++ b/test/test_esp32_usb_serial_hygiene.py @@ -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(