#include "UsbLogging.h" #include "UsbLoggingClientActivity.h" #if defined(ARDUINO) #include #include #include #if MESH_ESP32_HWCDC_SESSION_GUARD #include "esp_idf_version.h" #include "hal/usb_serial_jtag_ll.h" #endif #if defined(NRF52_PLATFORM) || MESH_ESP32_HWCDC_SESSION_GUARD \ || MESH_ESP32_TINYUSB_NONBLOCKING #include "NonBlockingWriteStream.h" #endif #if MESH_ESP32_TINYUSB_NONBLOCKING #include "esp32-hal-tinyusb.h" // Build-local Arduino hook queries its actual CDC descriptor's IN endpoint. extern "C" bool meshEsp32TinyUsbTxPending(); #endif #if defined(NRF52_PLATFORM) && defined(USE_TINYUSB) #include #endif #if MESH_ESP32_HWCDC_SESSION_GUARD #include "UsbAsciiBinarySwitch.h" #endif #if defined(NRF52_PLATFORM) && \ (defined(USE_TINYUSB) || defined(ENABLE_USB_INTERFACE) || defined(OTA_FOLDER_SERIAL)) #define MESH_NRF52_PRIMARY_USB_NONBLOCKING 1 #include #include // Build-local CDC query installed by scripts/nrf52_usb_power_fix.py. It does // not clear/cancel an endpoint or change its USB bulk data-toggle state. extern "C" bool mesh_tud_cdc_n_tx_pending(uint8_t instance); #if !defined(CFG_TUD_CDC) || CFG_TUD_CDC < 1 #error "nRF52 USB Companion/mOTA requires CFG_TUD_CDC >= 1" #endif #else #define MESH_NRF52_PRIMARY_USB_NONBLOCKING 0 #endif #if defined(MESH_DUAL_CDC_LOGGING) #if !defined(COMPANION_FEATURE_DEDICATED_USB_LOGGING) || \ !COMPANION_FEATURE_DEDICATED_USB_LOGGING #error "MESH_DUAL_CDC_LOGGING requires its dedicated Companion capability" #endif #if !defined(NRF52_PLATFORM) #error "MESH_DUAL_CDC_LOGGING is supported only by nRF52 Full Companion" #endif #if !defined(ENABLE_USB_INTERFACE) #error "MESH_DUAL_CDC_LOGGING requires ENABLE_USB_INTERFACE" #endif #if !defined(CFG_TUD_CDC) || CFG_TUD_CDC < 2 #error "MESH_DUAL_CDC_LOGGING requires CFG_TUD_CDC >= 2" #endif #endif namespace mesh { #if defined(ENABLE_USB_INTERFACE) // A USB Companion owns its primary stream for framed traffic until saved // preferences are loaded. Starting disabled prevents early boot diagnostics // from corrupting that stream before single-TTY builds can enter terminal mode. static std::atomic usb_logging_enabled{false}; #else static std::atomic usb_logging_enabled{true}; #endif static std::atomic usb_logging_preference_known{false}; static std::atomic usb_debug_enabled{false}; static std::atomic usb_logging_tx_progress{0}; static std::atomic usb_logging_tx_waiting{false}; #if MESH_ESP32_TINYUSB_NONBLOCKING \ || (defined(NRF52_PLATFORM) && defined(USE_TINYUSB)) static bool usb_logging_watchdog_detached = false; static uint32_t usb_logging_watchdog_attach_at = 0; #endif static void noteUsbLoggingTxAttempt() { if (isUsbLoggingEnabled()) usb_logging_tx_waiting.store(true, std::memory_order_release); } static void noteUsbLoggingTxComplete() { if (!isUsbLoggingEnabled()) return; usb_logging_tx_progress.fetch_add(1, std::memory_order_acq_rel); usb_logging_tx_waiting.store(false, std::memory_order_release); } class NullUsbLoggingStream : public Stream { public: int available() override { return 0; } int read() override { return -1; } int peek() override { return -1; } void flush() override {} size_t write(uint8_t) override { return 1; } size_t write(const uint8_t*, size_t size) override { return size; } }; static NullUsbLoggingStream null_usb_logging_stream; #if MESH_ESP32_TINYUSB_NONBLOCKING // Arduino-ESP32 2.0.17 constructs Serial as USBCDC(0). Keep its RX queue and // existing descriptors/callbacks, but never call its potentially unbounded // write()/flush() or its mutex-taking availableForWrite(). The native CDC // application API makes one FIFO attempt and can safely return short; its // flush starts an available endpoint transfer without waiting for the host. // TinyUSB still takes short RTOS FIFO/endpoint mutexes internally: this is a // no-host-progress-wait contract, not a claim that the USB stack is lock-free. static std::atomic esp32_tinyusb_reset_generation{0}; static std::atomic esp32_tinyusb_clean_generation{0}; static std::atomic_flag esp32_tinyusb_queue_busy = ATOMIC_FLAG_INIT; static std::atomic esp32_tinyusb_terminal_discard_pending{false}; static std::atomic esp32_tinyusb_terminal_dropped_bytes{0}; static uint32_t esp32_tinyusb_terminal_reported_dropped_bytes = 0; static uint32_t esp32_tinyusb_taken_reset_generation = 0; static std::atomic esp32_tinyusb_was_connected{false}; // 0: ordinary; 1: detached; 2: awaiting fresh mount; 3: preparing quarantine. static std::atomic esp32_tinyusb_reenumeration_state{0}; static std::atomic esp32_tinyusb_owner_dtr{false}; static std::atomic esp32_tinyusb_reattach_after{0}; static constexpr uint32_t esp32_tinyusb_detach_millis = 20; static bool canAccessEsp32TinyUsb(void*) { return !xPortInIsrContext() && esp32_tinyusb_reenumeration_state.load(std::memory_order_acquire) == 0 && tud_cdc_n_connected(0) && esp32_tinyusb_clean_generation.load(std::memory_order_acquire) == esp32_tinyusb_reset_generation.load(std::memory_order_acquire); } static void requestEsp32TinyUsbReenumeration(bool force_reconnect = false) { uint8_t expected = 0; if (!esp32_tinyusb_reenumeration_state.compare_exchange_strong( expected, 3, std::memory_order_acq_rel)) { // A later watchdog stage may restart an already reattached device whose // host never delivered a fresh mount. Owner callbacks never force this. if (!force_reconnect || expected != 2 || !esp32_tinyusb_reenumeration_state.compare_exchange_strong( expected, 3, std::memory_order_acq_rel)) return; } // The pinned ESP32 S2/S3 DCD disconnect is one bounded soft-disconnect // register write, not a send into TinyUSB's owner queue. Isolate an armed // old packet immediately; only the application loop may reattach later. (void)tud_disconnect(); esp32_tinyusb_reattach_after.store(millis() + esp32_tinyusb_detach_millis, std::memory_order_release); // Publish only a fully prepared detach. The other core must never connect // before this callback has actually disconnected and started its deadline. esp32_tinyusb_reenumeration_state.store(1, std::memory_order_release); } static void purgeEsp32TinyUsbRx() { // Owner-task session hooks run before new-host RX callbacks. A fixed snapshot // bounds this purge even if an application reader consumes bytes concurrently. int pending = Serial.available(); while (pending-- > 0) (void)Serial.read(); // At unmount the class endpoints have already been reset. read_flush also // rearms OUT, so it must not address an unopened/reset endpoint there. if (tud_mounted()) tud_cdc_n_read_flush(0); } class Esp32TinyUsbFifoStream : public Stream { public: int available() override { const int count = Serial.available(); return count > 0 ? count : 0; } int read() override { return Serial.read(); } int peek() override { return Serial.peek(); } void flush() override {} int availableForWrite() override { return canAccessEsp32TinyUsb(nullptr) ? static_cast(tud_cdc_n_write_available(0)) : 0; } size_t write(uint8_t value) override { return write(&value, 1); } size_t write(const uint8_t* data, size_t size) override { if (data == nullptr || size == 0 || !canAccessEsp32TinyUsb(nullptr)) return 0; const size_t available = tud_cdc_n_write_available(0); const size_t attempt = size < available ? size : available; if (attempt == 0) return 0; noteUsbLoggingTxAttempt(); const size_t written = tud_cdc_n_write(0, data, attempt); (void)tud_cdc_n_write_flush(0); return written; } }; static Esp32TinyUsbFifoStream esp32_tinyusb_fifo_port; static size_t writeEsp32TinyUsbOnce(void*, const uint8_t* data, size_t size) { return esp32_tinyusb_fifo_port.write(data, size); } static SingleAttemptNonBlockingStream nonblocking_esp32_tinyusb_port( esp32_tinyusb_fifo_port, writeEsp32TinyUsbOnce, nullptr, canAccessEsp32TinyUsb); static AtomicWholeRecordNonBlockingStream<11> nonblocking_esp32_tinyusb_mota_port( nonblocking_esp32_tinyusb_port); // The ESP32 CDC FIFO is just 64 bytes. Retain complete producer writes in ONE // chronological queue: independent log/reply queues would interleave their // partial lines as that small FIFO drains. Diagnostics leave most of the bounded // queue reserved for functional replies; congestion drops records, not LoRa. static constexpr size_t esp32_tinyusb_text_capacity = 4096; static constexpr size_t esp32_tinyusb_functional_reserve = 3072; static constexpr size_t esp32_tinyusb_log_record_capacity = 640; // ESP32 Print::printf has no Adafruit 256-byte scratch-length bug. Disable // that nRF52-specific sentinel without changing the existing nRF52 facade. static BufferedNonBlockingWriteStream esp32_tinyusb_text_queue( nonblocking_esp32_tinyusb_port); template class Esp32TinyUsbBufferedStream : public Stream { public: int available() override { return nonblocking_esp32_tinyusb_port.available(); } int read() override { return nonblocking_esp32_tinyusb_port.read(); } int peek() override { return nonblocking_esp32_tinyusb_port.peek(); } void flush() override { serviceUsbTerminalPort(); } int availableForWrite() override { if (esp32_tinyusb_queue_busy.test_and_set(std::memory_order_acquire)) return 0; const int available = canQueue() ? esp32_tinyusb_text_queue.availableForWrite() : 0; esp32_tinyusb_queue_busy.clear(std::memory_order_release); if (!Diagnostic) return available; // SerialLogLine must admit its entire <=640-byte record instead of // splitting it when only a few bytes of diagnostic capacity remain. return available >= static_cast(esp32_tinyusb_functional_reserve + esp32_tinyusb_log_record_capacity) ? available - esp32_tinyusb_functional_reserve : 0; } size_t write(uint8_t value) override { return write(&value, 1); } size_t write(const uint8_t* data, size_t size) override { if (data == nullptr || size == 0) return 0; if (esp32_tinyusb_queue_busy.test_and_set(std::memory_order_acquire)) { noteDropped(size); return 0; } size_t written = 0; if (canQueue()) { const size_t available = esp32_tinyusb_text_queue.availableForWrite(); if (!Diagnostic || (available >= esp32_tinyusb_functional_reserve && size <= available - esp32_tinyusb_functional_reserve)) { written = esp32_tinyusb_text_queue.write(data, size); } } if (written != size) noteDropped(size - written); esp32_tinyusb_queue_busy.clear(std::memory_order_release); return written; } private: bool canQueue() const { return canAccessEsp32TinyUsb(nullptr) && (Diagnostic ? isUsbLoggingEnabled() : !esp32_tinyusb_terminal_discard_pending.load( std::memory_order_acquire)); } void noteDropped(size_t size) { if (!Diagnostic && !xPortInIsrContext() && tud_cdc_n_connected(0)) { esp32_tinyusb_terminal_dropped_bytes.fetch_add( static_cast(size), std::memory_order_relaxed); } } }; static Esp32TinyUsbBufferedStream buffered_esp32_tinyusb_logging_port; static Esp32TinyUsbBufferedStream buffered_esp32_tinyusb_terminal_port; static void clearEsp32TinyUsbTx(void*) { (void)tud_cdc_n_write_clear(0); if (tud_mounted() && meshEsp32TinyUsbTxPending()) { requestEsp32TinyUsbReenumeration(); } } static void closeEsp32TinyUsbSession(bool device_boundary) { clearUsbLoggingClientActivity(); esp32_tinyusb_reset_generation.fetch_add(1, std::memory_order_acq_rel); // Do this synchronously in TinyUSB's owner, never in its delayed Arduino // event handler or the main-loop cleanup (which could purge fresh RX). purgeEsp32TinyUsbRx(); if (!device_boundary && !nonblocking_esp32_tinyusb_port.tryRunExclusive(clearEsp32TinyUsbTx)) { // A writer already in flight can still arm old bytes after the close. // Physically quarantine it instead of racing an endpoint abort/FIFO purge. requestEsp32TinyUsbReenumeration(); } } static void purgeEsp32TinyUsbDetachedQueues(void*) { purgeEsp32TinyUsbRx(); (void)tud_cdc_n_write_clear(0); } static void serviceEsp32TinyUsbPorts() { if (xPortInIsrContext()) return; if (esp32_tinyusb_queue_busy.test_and_set(std::memory_order_acquire)) return; const uint8_t reenumeration = esp32_tinyusb_reenumeration_state.load(std::memory_order_acquire); if (reenumeration != 0) { if (reenumeration == 1 && (int32_t)(millis() - esp32_tinyusb_reattach_after.load( std::memory_order_acquire)) >= 0 && (!usb_logging_watchdog_detached || (int32_t)(millis() - usb_logging_watchdog_attach_at) >= 0) && nonblocking_esp32_tinyusb_port.tryRunExclusive( purgeEsp32TinyUsbDetachedQueues)) { // Publish before connect: a fast owner mount callback can run immediately. esp32_tinyusb_reenumeration_state.store(2, std::memory_order_release); (void)tud_connect(); usb_logging_watchdog_detached = false; } esp32_tinyusb_queue_busy.clear(std::memory_order_release); return; // Only the fresh owner-task mount hook can reopen this gate. } // Polling also catches a physical disconnect without a CDC line-state // event. Events capture quick close/reopen pairs between service calls. const bool connected = tud_cdc_n_connected(0); if (esp32_tinyusb_was_connected.exchange(connected, std::memory_order_acq_rel) && !connected) { clearUsbLoggingClientActivity(); esp32_tinyusb_reset_generation.fetch_add(1, std::memory_order_acq_rel); } const uint32_t generation = esp32_tinyusb_reset_generation.load(std::memory_order_acquire); if (generation != esp32_tinyusb_clean_generation.load(std::memory_order_acquire)) { if (!nonblocking_esp32_tinyusb_port.tryRunExclusive(clearEsp32TinyUsbTx)) { esp32_tinyusb_queue_busy.clear(std::memory_order_release); return; } esp32_tinyusb_text_queue.discardPending(); esp32_tinyusb_terminal_reported_dropped_bytes = esp32_tinyusb_terminal_dropped_bytes.load(std::memory_order_relaxed); // The synchronous owner hook already purged old RX. Keep the new host's // first query, while protocol owners reset their partial parser separately. esp32_tinyusb_clean_generation.store(generation, std::memory_order_release); } if (esp32_tinyusb_terminal_discard_pending.exchange(false, std::memory_order_acq_rel)) { esp32_tinyusb_text_queue.discardPending(); esp32_tinyusb_terminal_reported_dropped_bytes = esp32_tinyusb_terminal_dropped_bytes.load(std::memory_order_relaxed); } if (canAccessEsp32TinyUsb(nullptr)) { esp32_tinyusb_text_queue.service(); const uint32_t dropped = esp32_tinyusb_terminal_dropped_bytes.load(std::memory_order_relaxed); if (dropped != esp32_tinyusb_terminal_reported_dropped_bytes && esp32_tinyusb_text_queue.availableForWrite() >= 96) { char marker[96]; const int length = snprintf(marker, sizeof(marker), "\r\n[USB terminal output dropped %lu bytes]\r\n", static_cast( dropped - esp32_tinyusb_terminal_reported_dropped_bytes)); if (length > 0 && static_cast(length) < sizeof(marker) && esp32_tinyusb_text_queue.write( reinterpret_cast(marker), length) == static_cast(length)) { esp32_tinyusb_terminal_reported_dropped_bytes = dropped; } } } esp32_tinyusb_queue_busy.clear(std::memory_order_release); } #endif #if MESH_ESP32_HWCDC_SESSION_GUARD // Every primary HWCDC role shares one producer gate. In particular, returning // this facade from usbLoggingPort() means a task which cached its Stream& before // a disconnect still checks the current session at the actual write boundary. static std::atomic esp32_hwcdc_access_generation{0}; static std::atomic esp32_hwcdc_allowed_generation{0}; static std::atomic esp32_hwcdc_bus_reset_generation{0}; static UsbSelfResetBurstGuard esp32_hwcdc_self_reset_guard; // HWCDC::write() retains bytes in its software ring when the framework's raw // five-millisecond SOF detector happens to report false. In that path the core // does not leave SERIAL_IN_EMPTY enabled, so a valid Companion response can // remain parked until a later request happens to kick TX. Keep requesting that // interrupt until the framework reports actual TX progress. static std::atomic esp32_hwcdc_tx_kick_pending{false}; static std::atomic esp32_hwcdc_tx_primed{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{0}; static std::atomic esp32_hwcdc_rx_queue_ready{false}; // 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. static bool esp32_hwcdc_cleanup_pending = false; static bool esp32_hwcdc_restore_pad_enabled = false; static uint32_t esp32_hwcdc_cleanup_generation = 0; static bool canAccessEsp32Hwcdc(void*) { return esp32_hwcdc_rx_queue_ready.load(std::memory_order_acquire) && esp32_hwcdc_allowed_generation.load(std::memory_order_acquire) == esp32_hwcdc_access_generation.load(std::memory_order_acquire); } static void handleEsp32HwcdcEvent(void*, esp_event_base_t, int32_t event_id, void*) { if (event_id == ARDUINO_HW_CDC_BUS_RESET_EVENT) { clearUsbLoggingClientActivity(); // A host may issue several reset requests while enumerating the clean // transport. Suppress the complete burst; post-clean traffic below ends // the exemption before any later active-session reset can be ignored. if (esp32_hwcdc_self_reset_guard.shouldIgnoreBusReset()) { return; } // Quarantine reads and writes before publishing the boundary to the main // loop. If this callback lands just after that loop sampled the generation, // no old-session parser or producer can run during the intervening pass. portENTER_CRITICAL(&esp32_hwcdc_session_mux); esp32_hwcdc_access_generation.fetch_add( 1, std::memory_order_acq_rel); esp32_hwcdc_tx_kick_pending.store(false, std::memory_order_release); esp32_hwcdc_tx_primed.store(false, std::memory_order_release); usb_serial_jtag_ll_disable_intr_mask( USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); portEXIT_CRITICAL(&esp32_hwcdc_session_mux); Serial.setDebugOutput(false); esp32_hwcdc_bus_reset_generation.fetch_add( 1, std::memory_order_acq_rel); return; } if (event_id == ARDUINO_HW_CDC_RX_EVENT || event_id == ARDUINO_HW_CDC_TX_EVENT) { // If no self-reset event was delivered, post-clean traffic proves that the // expected enumeration is over. A subsequent reset must not be ignored. esp32_hwcdc_self_reset_guard.notePostCleanActivity(); } if (event_id == ARDUINO_HW_CDC_TX_EVENT && esp32_hwcdc_tx_primed.exchange(true, std::memory_order_acq_rel)) { // The first empty interrupt only stages the first packet. A subsequent // one proves the host picked up the preceding IN transfer. noteUsbLoggingTxComplete(); } } class Esp32HwcdcSessionStream : public Stream { public: int available() override { const int count = Serial.available(); if (count > 0) noteActivity(); return count; } int read() override { const int value = Serial.read(); if (value >= 0) noteActivity(); return value; } int peek() override { const int value = Serial.peek(); if (value >= 0) noteActivity(); return value; } void flush() override { Serial.flush(); } int availableForWrite() override { return Serial.availableForWrite(); } size_t write(uint8_t value) override { return write(&value, 1); } size_t write(const uint8_t* data, size_t size) override { if (data == nullptr || size == 0) return 0; noteUsbLoggingTxAttempt(); esp32_hwcdc_tx_kick_pending.store(true, std::memory_order_release); return Serial.write(data, size); } private: static void noteActivity() { esp32_hwcdc_self_reset_guard.notePostCleanActivity(); } }; static Esp32HwcdcSessionStream esp32_hwcdc_session_stream; static size_t writeEsp32HwcdcOnce(void*, const uint8_t* data, size_t size) { return esp32_hwcdc_session_stream.write(data, size); } static SingleAttemptNonBlockingStream guarded_esp32_hwcdc_port( esp32_hwcdc_session_stream, writeEsp32HwcdcOnce, nullptr, canAccessEsp32Hwcdc); static AtomicWholeRecordNonBlockingStream<11> guarded_esp32_hwcdc_mota_port(guarded_esp32_hwcdc_port); static void serviceEsp32HwcdcTxKickExclusive(void*) { if (!esp32_hwcdc_tx_kick_pending.load(std::memory_order_acquire) || !canAccessEsp32Hwcdc(nullptr)) { return; } // Unlike the asynchronous TX event queue, the ring's free capacity is // direct proof that every tracked byte has left HWCDC's software queue. // Sample it while the shared producer gate excludes every MeshCore writer. const size_t tx_capacity = esp32_hwcdc_tx_buffer_capacity.load( std::memory_order_acquire); if (tx_capacity != 0 && Serial.availableForWrite() >= tx_capacity) { esp32_hwcdc_tx_kick_pending.store(false, std::memory_order_release); return; } if (!Serial.isPlugged()) return; // Match the framework's normal connected-write kick. Serialize the final // generation check and register writes with BUS_RESET quarantine so a stale // service pass cannot re-enable TX after the session has been closed. portENTER_CRITICAL(&esp32_hwcdc_session_mux); if (esp32_hwcdc_tx_kick_pending.load(std::memory_order_acquire) && canAccessEsp32Hwcdc(nullptr) && Serial.isPlugged()) { usb_serial_jtag_ll_txfifo_flush(); usb_serial_jtag_ll_ena_intr_mask( USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); } portEXIT_CRITICAL(&esp32_hwcdc_session_mux); } static void serviceEsp32HwcdcTxKick() { if (!esp32_hwcdc_tx_kick_pending.load(std::memory_order_acquire)) return; (void)guarded_esp32_hwcdc_port.tryRunExclusive( serviceEsp32HwcdcTxKickExclusive); } #endif #if MESH_NRF52_PRIMARY_USB_NONBLOCKING #if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB) // DTR or actual host input proves a CDC0 client. Stock MeshCLI deliberately // deasserts DTR, so it cannot be a prerequisite for receiving its first frame. // Access still requires the application to complete the current close epoch. // Incrementing reset_generation therefore // closes the transport atomically; an older completion can publish only its // older epoch and can never reopen across a newer close. static std::atomic primary_usb_reset_generation{0}; static std::atomic primary_usb_allowed_generation{0}; static std::atomic primary_usb_terminal_discard_pending{false}; static std::atomic primary_usb_line_state_dtr{false}; static std::atomic primary_usb_rx_generation{UINT32_MAX}; static constexpr uint32_t primary_usb_session_settle_millis = 8; static std::atomic primary_usb_reset_settle_until{0}; static std::atomic nrf52_usb_reenumeration_pending{false}; static std::atomic nrf52_usb_reattach_after{0}; // Only the application service loop owns this phase. USB callbacks merely // quarantine the pull-up and publish the atomic request below. static bool nrf52_usb_stack_detach_issued = false; static constexpr uint32_t nrf52_usb_detach_millis = 20; static void requestNrf52UsbSessionReenumeration() { // Only an armed old-session packet needs this recovery. TinyUSB has no safe // nRF52 per-endpoint abort API; FIFO clears cannot retract endpoint RAM. A // physical detach guarantees that packet cannot enter a reopened handle, // and the host's fresh enumeration resets endpoint/data-toggle state. Both // CDC ports briefly re-enumerate whenever a close finds a pending transfer, // including an unread packet or the stack's trailing zero-length packet. if (nrf52_usb_reenumeration_pending.exchange(true, std::memory_order_acq_rel)) return; primary_usb_line_state_dtr.store(false, std::memory_order_release); primary_usb_reset_generation.fetch_add(1, std::memory_order_acq_rel); // Match the Nordic driver's bounded physical-disconnect operation now so // an armed packet cannot reach a rapidly reopened host. Do NOT call // TinyUSBDevice.detach() from this CDC/SOF callback: dcd_disconnect also // queues UNPLUGGED with an infinite FreeRTOS send timeout. A full event // queue would then make its sole consumer wait forever on its own queue. NRF_USBD->USBPULLUP = 0; __ISB(); __DSB(); } static void clearNrf52UsbTxForSession(uint8_t instance) { (void)tud_cdc_n_write_clear(instance); if (mesh_tud_cdc_n_tx_pending(instance)) { requestNrf52UsbSessionReenumeration(); } } static void serviceNrf52UsbSessionReenumeration() { if (!nrf52_usb_reenumeration_pending.load(std::memory_order_acquire)) return; if (!nrf52_usb_stack_detach_issued) { // Called only from the application loop, never a TinyUSB owner callback. // The owner can therefore drain a full event queue while this supported // detach operation enqueues the reset, instead of blocking on itself. TinyUSBDevice.detach(); nrf52_usb_stack_detach_issued = true; // Start the quiet interval at the actual stack detach, not at a possibly // much earlier callback request while the application loop was occupied. nrf52_usb_reattach_after.store(millis() + nrf52_usb_detach_millis, std::memory_order_release); return; } if ((int32_t)(millis() - nrf52_usb_reattach_after.load( std::memory_order_acquire)) < 0 || tud_mounted()) return; #if defined(USE_TINYUSB) // A watchdog recovery can overlap an old-session endpoint quarantine. Both // detach deadlines must expire before either service reattaches the device. if (usb_logging_watchdog_detached && (int32_t)(millis() - usb_logging_watchdog_attach_at) < 0) return; #endif // Waiting for unmount as well as the quiet interval ensures TinyUSB's owner // has consumed the unplug event. Never reattach with its old class state. TinyUSBDevice.attach(); #if defined(USE_TINYUSB) usb_logging_watchdog_detached = false; #endif nrf52_usb_stack_detach_issued = false; nrf52_usb_reenumeration_pending.store(false, std::memory_order_release); } static uint32_t primaryUsbSessionGeneration() { return primary_usb_reset_generation.load(std::memory_order_acquire); } static bool canAccessPrimaryUsbSession(void*) { if (primary_usb_terminal_discard_pending.load(std::memory_order_acquire)) return false; const uint32_t generation = primaryUsbSessionGeneration(); if (nrf52_usb_reenumeration_pending.load(std::memory_order_acquire) || !tud_mounted() || primary_usb_allowed_generation.load(std::memory_order_acquire) != generation) return false; if (!primary_usb_line_state_dtr.load(std::memory_order_acquire) && primary_usb_rx_generation.load(std::memory_order_acquire) != generation) { if (tud_cdc_n_available(0) == 0) return false; // Tag the proof with the epoch sampled before looking at RX. A callback // racing this store can invalidate it without an old reader reopening the // next host's session after cleanup. primary_usb_rx_generation.store(generation, std::memory_order_release); } return primaryUsbSessionGeneration() == generation && tud_mounted(); } static void endPrimaryUsbHostSession(bool clear_cdc_fifos) { const bool previous = primary_usb_line_state_dtr.exchange( false, std::memory_order_acq_rel); const bool received = primary_usb_rx_generation.load(std::memory_order_acquire) == primaryUsbSessionGeneration(); if (!previous && !received && !(clear_cdc_fifos && tud_cdc_n_available(0) != 0)) return; // Publish a short quiescence deadline before closing the generation gate. // FIFO-only cleanup suffices when no old IN packet is armed; otherwise // quarantine and physically re-enumerate rather than leaking that packet. // RX is purged synchronously below; bytes sent by a reopened host // during the settle interval remain queued until the gate reopens. primary_usb_reset_settle_until.store( millis() + primary_usb_session_settle_millis, std::memory_order_release); primary_usb_reset_generation.fetch_add(1, std::memory_order_acq_rel); #if !defined(MESH_DUAL_CDC_LOGGING) clearUsbLoggingClientActivity(); #endif if (clear_cdc_fifos) { // The line-state callback runs before TinyUSB resets the device. A device // unmount/remount boundary has already reset these class FIFOs. tud_cdc_n_read_flush(0); clearNrf52UsbTxForSession(0); } } #endif // Adafruit_USBD_CDC::write() retries from a yield loop until it has queued the // complete request. A CDC host can change DTR or stop draining between the // caller's availableForWrite() sample and that loop. Use TinyUSB's native // single-attempt primitive so both Companion CDCs return short immediately and // let their existing queue/whole-record policy decide what to retry. static size_t writeTinyUsbCdcOnce(void* context, const uint8_t* data, size_t size) { const uint8_t instance = static_cast( reinterpret_cast(context)); if (data == nullptr || size == 0 || instance >= CFG_TUD_CDC) { return 0; } #if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB) if (nrf52_usb_reenumeration_pending.load(std::memory_order_acquire)) return 0; if (instance == 0) { if (!canAccessPrimaryUsbSession(nullptr)) return 0; } else #endif { if (!tud_cdc_n_connected(instance)) return 0; } const size_t available = tud_cdc_n_write_available(instance); const size_t attempt = size < available ? size : available; if (attempt == 0) return 0; #if defined(MESH_DUAL_CDC_LOGGING) if (instance == 1) noteUsbLoggingTxAttempt(); #else if (instance == 0) noteUsbLoggingTxAttempt(); #endif return tud_cdc_n_write(instance, data, attempt); } static SingleAttemptNonBlockingStream nonblocking_primary_usb_companion_port( Serial, writeTinyUsbCdcOnce, reinterpret_cast(uintptr_t{0}) #if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB) , canAccessPrimaryUsbSession #endif ); // SerialMotaSource emits one contiguous request whose largest valid frame is // 11 bytes. Preflight that entire record before the sole TinyUSB write attempt; // a busy FIFO drops the request cleanly and lets the transaction retry later. static AtomicWholeRecordNonBlockingStream<11> nonblocking_primary_usb_mota_port( nonblocking_primary_usb_companion_port); #if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB) // Unlike Binary Companion frames, a terminal reply is produced as many Print // calls whose return values are not consumed by the CLI. Retain one complete // multi-line response while a normal host drains CDC0, but bound the memory and // drop later records instead of ever waiting on an unread endpoint. static BufferedNonBlockingWriteStream<4096> buffered_primary_usb_terminal_port( nonblocking_primary_usb_companion_port); static uint32_t primary_usb_terminal_seen_reset_generation = 0; static uint32_t primary_usb_terminal_taken_reset_generation = 0; // Functional replies and diagnostics on one CDC need one chronological queue // so their unwritten suffixes cannot overtake each other. Diagnostics leave // most capacity available for stats/configuration replies. static constexpr size_t nrf52_usb_functional_reserve = 3072; class Nrf52BufferedLoggingStream : public Stream { public: int available() override { return 0; } int read() override { return -1; } int peek() override { return -1; } void flush() override { serviceUsbTerminalPort(); } int availableForWrite() override { const int free = buffered_primary_usb_terminal_port.availableForWrite(); return free > static_cast(nrf52_usb_functional_reserve) ? free - nrf52_usb_functional_reserve : 0; } size_t write(uint8_t value) override { return write(&value, 1); } size_t write(const uint8_t* data, size_t size) override { if (!data || !size || size > 4096 || !canAccessPrimaryUsbSession(nullptr) || availableForWrite() < static_cast(size)) return 0; return buffered_primary_usb_terminal_port.write(data, size); } }; static Nrf52BufferedLoggingStream buffered_primary_usb_logging_port; #endif #endif static void setPlatformDebugOutputEnabled(bool enabled) { #if MESH_ESP32_TINYUSB_NONBLOCKING // The framework putc hook bypasses the common producer gate. MeshCore // diagnostics remain enabled through usbLoggingPort(); keep raw framework // bytes from racing a binary mOTA/Companion record's capacity preflight. (void)enabled; Serial.setDebugOutput(false); #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 // HWCDC's framework putc hook bypasses the guarded Stream and cannot make // its check-plus-write atomic with a BUS_RESET callback. Keep that raw route // disabled on a shared protocol port; MeshCore diagnostics still use the // guarded usbLoggingPort() below. (void)enabled; Serial.setDebugOutput(false); #else Serial.setDebugOutput(enabled); #endif #else (void)enabled; #endif } #if defined(MESH_DUAL_CDC_LOGGING) static bool dedicated_usb_logging_port_configured = false; static bool dedicated_usb_logging_port_started = false; static std::atomic dedicated_usb_logging_port_connected{false}; static std::atomic dedicated_usb_logging_watchdog_reset_requested{false}; static bool dedicated_usb_logging_sof_enabled = false; static constexpr char dedicated_usb_logging_descriptor[] = "MeshCore Logging"; // Adafruit_USBD_CDC::begin() assigns "TinyUSB Serial" and immediately copies // the interface descriptor into TinyUSBDevice's configuration buffer. Calling // setStringDescriptor() after begin() therefore cannot rename that copied // descriptor. Assign our string while the virtual descriptor builder is // running, immediately before the core copies it instead. class DedicatedUsbLoggingCdc : public Adafruit_USBD_CDC { public: uint16_t getInterfaceDescriptor(uint8_t itfnum_deprecated, uint8_t* buf, uint16_t bufsize) override { if (buf != nullptr) { setStringDescriptor(dedicated_usb_logging_descriptor); } return Adafruit_USBD_CDC::getInterfaceDescriptor( itfnum_deprecated, buf, bufsize); } }; static DedicatedUsbLoggingCdc dedicated_usb_logging_port; static SingleAttemptNonBlockingStream single_attempt_dedicated_usb_logging_port( dedicated_usb_logging_port, writeTinyUsbCdcOnce, reinterpret_cast(uintptr_t{1})); static TaskOwnedWriteStream<> usb_task_dedicated_usb_logging_port( single_attempt_dedicated_usb_logging_port); static WholeRecordNonBlockingStream<> nonblocking_dedicated_usb_logging_port( usb_task_dedicated_usb_logging_port); static constexpr char dedicated_usb_logging_identity[] = "MeshCore USB logging port\r\n" "USB CDC 1; interface 02; Linux stable suffix: -if02\r\n"; static constexpr size_t dedicated_usb_logging_identity_size = sizeof(dedicated_usb_logging_identity) - 1; static_assert(dedicated_usb_logging_identity_size <= 256, "USB identity marker exceeds the nonblocking record limit"); static_assert(dedicated_usb_logging_identity_size <= CFG_TUD_CDC_TX_BUFSIZE, "USB identity marker exceeds the TinyUSB CDC TX FIFO"); static std::atomic dedicated_usb_logging_reset_generation{0}; static uint32_t dedicated_usb_logging_seen_reset_generation = 0; // TinyUSB reports SET_CONTROL_LINE_STATE from its owner task. Keep the last // DTR value independently of main-loop polling so a close/reopen cannot be // collapsed into one continuously connected sample. static std::atomic dedicated_usb_logging_line_state_dtr{false}; static bool dedicated_usb_logging_usb_task_connected = false; static size_t dedicated_usb_logging_identity_offset = 0; // Windows configures line coding and then purges its COM buffers. Keep CDC1 // silent for more than one Windows scheduling quantum so that purge cannot // discard the identity marker and expose a later diagnostic as byte zero. static constexpr uint8_t dedicated_usb_logging_host_settle_sofs = 50; static uint8_t dedicated_usb_logging_quiet_sofs = 0; static UsbLoggingStatsLineParser dedicated_usb_logging_stats_parser; static void resetDedicatedUsbLoggingIdentity() { dedicated_usb_logging_identity_offset = 0; } static void resetDedicatedUsbLoggingUsbTaskState() { dedicated_usb_logging_usb_task_connected = false; resetDedicatedUsbLoggingIdentity(); usb_task_dedicated_usb_logging_port.discardPending(); } static void restartDedicatedUsbLoggingHostSession() { clearUsbLoggingClientActivity(); dedicated_usb_logging_stats_parser.reset(); tud_cdc_n_read_flush(1); // This callback and the SOF drain both run in TinyUSB's task. Gate producers // before clearing both queues, then publish a generation so the next SOF // repeats the reset after any producer which was already in flight. Clearing // here also prevents bytes left in TinyUSB's TX FIFO from preceding the // identity marker after a rapid reopen. dedicated_usb_logging_port_connected.store(false, std::memory_order_release); clearNrf52UsbTxForSession(1); resetDedicatedUsbLoggingUsbTaskState(); dedicated_usb_logging_quiet_sofs = dedicated_usb_logging_host_settle_sofs; dedicated_usb_logging_reset_generation.fetch_add( 1, std::memory_order_acq_rel); } static void handleDedicatedUsbLoggingLineState(bool dtr) { const bool previous = dedicated_usb_logging_line_state_dtr.exchange( dtr, std::memory_order_acq_rel); if (previous == dtr) return; restartDedicatedUsbLoggingHostSession(); // TinyUSB clears its SOF-consumer bits on every USB bus reset. A rapid // reset/re-enumeration can leave the main loop's requested-state cache true // throughout, so it would otherwise never ask TinyUSB to restore this // callback. SET_CONTROL_LINE_STATE runs in TinyUSB's owner task after the // new configuration is active, making the fresh DTR edge the exact place // to rearm (or disable) CDC1's sole drain tick. tud_sof_cb_enable(dtr && isUsbLoggingEnabled()); } static void handleDedicatedUsbLoggingLineCoding() { // Windows can close and immediately reopen a COM handle without exposing a // distinct DTR-low interval to the device. It still configures line coding // for the newly opened handle. Treat that owner-task callback as a fresh // CDC1 host session only when DTR is already asserted; if it is not, the // subsequent DTR-high edge performs the reset instead. if (tud_cdc_n_connected(1)) { restartDedicatedUsbLoggingHostSession(); } } // Called by TinyUSB's own high-priority task through tud_sof_cb(). Application // writers only copy complete records into the queue above, so every CDC1 FIFO // and endpoint operation happens in the same task as control transfers. The // identity marker bypasses that producer queue: its cursor advances from the // actual TinyUSB write result, not from queue acceptance. Sampling DTR here at // 1 kHz also catches a close/reopen that occurs entirely between main-loop // service calls. void serviceDedicatedUsbLoggingFromUsbTask() { if (nrf52_usb_reenumeration_pending.load(std::memory_order_acquire)) { dedicated_usb_logging_port_connected.store(false, std::memory_order_release); resetDedicatedUsbLoggingUsbTaskState(); return; } if (dedicated_usb_logging_watchdog_reset_requested.exchange(false, std::memory_order_acq_rel)) { restartDedicatedUsbLoggingHostSession(); // Clearing an armed old packet can itself request a detach. Do not touch // any endpoint again in this owner tick while that recovery is pending. if (nrf52_usb_reenumeration_pending.load(std::memory_order_acquire)) return; } const uint32_t reset_generation = dedicated_usb_logging_reset_generation.load(std::memory_order_acquire); if (reset_generation != dedicated_usb_logging_seen_reset_generation) { resetDedicatedUsbLoggingUsbTaskState(); dedicated_usb_logging_seen_reset_generation = reset_generation; } const bool physical_connected = tud_cdc_n_connected(1); const bool connected = isUsbLoggingEnabled() && physical_connected; if (!physical_connected) { // A bus reset need not deliver an explicit DTR-low request. Make the next // DTR-high callback an edge even when the previous host vanished abruptly. dedicated_usb_logging_line_state_dtr.store( false, std::memory_order_release); } if (!connected) { if (dedicated_usb_logging_usb_task_connected) clearUsbLoggingClientActivity(); dedicated_usb_logging_stats_parser.reset(); dedicated_usb_logging_port_connected.store( false, std::memory_order_release); resetDedicatedUsbLoggingUsbTaskState(); return; } if (dedicated_usb_logging_quiet_sofs > 0) { dedicated_usb_logging_port_connected.store( false, std::memory_order_release); // Drop racing producer bytes. An armed old IN packet requires a physical // detach, not another FIFO clear or an arbitrary number of quiet SOFs. clearNrf52UsbTxForSession(1); resetDedicatedUsbLoggingUsbTaskState(); --dedicated_usb_logging_quiet_sofs; return; } dedicated_usb_logging_port_connected.store( true, std::memory_order_release); // CDC1 is still logging-only, not a second configuration console. Consume // bounded stats polling input solely to distinguish an application from // a plugged cable/open port. Endpoint access stays in TinyUSB's owner. for (size_t budget = 0; budget < 64 && tud_cdc_n_available(1); ++budget) { uint8_t value; if (tud_cdc_n_read(1, &value, 1) != 1) break; if (dedicated_usb_logging_stats_parser.consume(static_cast(value))) noteUsbLoggingStatsCommand("stats-core"); } if (!dedicated_usb_logging_usb_task_connected) { // Drop diagnostics from the prior host before sending byte zero of the // marker to the new host. usb_task_dedicated_usb_logging_port.discardPending(); resetDedicatedUsbLoggingIdentity(); dedicated_usb_logging_usb_task_connected = true; } if (dedicated_usb_logging_identity_offset < dedicated_usb_logging_identity_size) { // This direct single-attempt call runs in the sole CDC1 endpoint owner. // With the complete marker fitting in one TX FIFO, a short result can only // occur around a connection transition; restart from byte zero for the // next observed host instead of delivering only a suffix. const size_t remaining = dedicated_usb_logging_identity_size - dedicated_usb_logging_identity_offset; if (single_attempt_dedicated_usb_logging_port.availableForWrite() < static_cast(remaining)) { (void)tud_cdc_n_write_flush(1); return; } const size_t written = single_attempt_dedicated_usb_logging_port.write( reinterpret_cast( dedicated_usb_logging_identity + dedicated_usb_logging_identity_offset), remaining); dedicated_usb_logging_identity_offset = detail::nextUsbLoggingIdentityOffset( dedicated_usb_logging_identity_offset, remaining, written); (void)tud_cdc_n_write_flush(1); return; } // If the host stops reading, the TinyUSB FIFO and this queue simply fill and // later diagnostics are dropped without blocking either task. usb_task_dedicated_usb_logging_port.drainOne(); (void)tud_cdc_n_write_flush(1); } #elif defined(NRF52_PLATFORM) // Single-CDC nRF52 roles need the same protection. In particular, BLE debug // callbacks and packet logging write through usbLoggingPort() without going // through MeshCore's formatted-debug helper. #if MESH_NRF52_PRIMARY_USB_NONBLOCKING && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) static WholeRecordNonBlockingStream<640> nonblocking_primary_usb_logging_port( buffered_primary_usb_logging_port); #elif MESH_NRF52_PRIMARY_USB_NONBLOCKING static AtomicWholeRecordNonBlockingStream<> nonblocking_primary_usb_logging_port( nonblocking_primary_usb_companion_port); #else static WholeRecordNonBlockingStream<> nonblocking_primary_usb_logging_port(Serial); #endif #endif bool isUsbLoggingEnabled() { return usb_logging_enabled.load(std::memory_order_relaxed); } bool isUsbDebugEnabled() { return usb_debug_enabled.load(std::memory_order_relaxed); } void setUsbDebugEnabled(bool enabled) { usb_debug_enabled.store(enabled, std::memory_order_relaxed); setPlatformDebugOutputEnabled(isUsbDebugLoggingEnabled()); } void setUsbLoggingEnabled(bool enabled) { usb_logging_enabled.store(enabled, std::memory_order_relaxed); if (!enabled) { clearUsbLoggingClientActivity(); usb_logging_tx_waiting.store(false, std::memory_order_release); } usb_logging_preference_known.store(true, std::memory_order_relaxed); setPlatformDebugOutputEnabled(isUsbDebugLoggingEnabled()); // Turning diagnostics off is not a protocol switch. Retain functional // replies in the shared text queue; actual Binary/mOTA ownership changes // explicitly call discardUsbTerminalOutput() after their bounded barrier. } bool saveUsbLoggingBootPreference(bool enabled) { (void)enabled; return true; } void setUsbCompanionTxBufferCapacity(size_t capacity) { #if MESH_ESP32_HWCDC_SESSION_GUARD esp32_hwcdc_tx_buffer_capacity.store(capacity, std::memory_order_release); #else (void)capacity; #endif } void prepareUsbLoggingPort() { #if MESH_ESP32_HWCDC_SESSION_GUARD // The SDK's 256-byte default can lose the CR of an oversized ASCII setter // before the Companion's line-length check can reject it. Retain an // oversized command and exceed the terminal's 542-byte line buffer so // still larger single lines enter discard mode before this queue fills. // RX events report only admitted bytes, with no reliable overflow signal. // Like the TX ring, this queue must never be replaced under a live USB ISR. static_assert(MESH_ESP32_USB_RX_BUFFER_SIZE >= 1024, "HWCDC RX queue must retain oversized terminal commands"); // On allocation failure, begin() may fall back to the unsafe SDK default. // Keep the application stream quarantined instead of parsing damaged input. esp32_hwcdc_rx_queue_ready.store( Serial.setRxBufferSize(MESH_ESP32_USB_RX_BUFFER_SIZE) == MESH_ESP32_USB_RX_BUFFER_SIZE, std::memory_order_release); // 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 // ESP32 Companion stream; setUsbLoggingEnabled() restores them only when the // saved settings explicitly enable both USB output and debug verbosity. setPlatformDebugOutputEnabled(isUsbDebugLoggingEnabled()); #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; } #endif #if defined(MESH_DUAL_CDC_LOGGING) if (dedicated_usb_logging_port_started || !usb_logging_preference_known.load(std::memory_order_relaxed) || !isUsbLoggingEnabled()) { return; } // The core already started USB before setup. A host can have fetched the // old descriptor even while mounted() is false. Detach BEFORE mutating the // live configuration buffer, allow the owner to consume the unplug event, // then always enumerate the complete descriptor (not just mounted hosts). TinyUSBDevice.detach(); delay(10); dedicated_usb_logging_port.begin(115200); dedicated_usb_logging_port_configured = true; dedicated_usb_logging_port_started = true; TinyUSBDevice.attach(); #endif } void serviceUsbLoggingPort() { #if MESH_ESP32_TINYUSB_NONBLOCKING \ || (defined(NRF52_PLATFORM) && defined(USE_TINYUSB)) // A timed reconnect retains all descriptors and never blocks the radio loop // in delay(), Serial.end(), or a host-progress wait. if (usb_logging_watchdog_detached && int32_t(millis() - usb_logging_watchdog_attach_at) >= 0 #if MESH_ESP32_TINYUSB_NONBLOCKING && esp32_tinyusb_reenumeration_state.load(std::memory_order_acquire) == 0 #endif #if defined(NRF52_PLATFORM) && !nrf52_usb_reenumeration_pending.load(std::memory_order_acquire) && !tud_mounted() #endif ) { (void)tud_connect(); usb_logging_watchdog_detached = false; } #endif #if defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) serviceNrf52UsbSessionReenumeration(); #endif #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 && dedicated_usb_logging_port.dtr(); const bool should_service = connected && isUsbLoggingEnabled(); // Only the TinyUSB owner task publishes a positive producer gate. The main // loop may close it, but must not bypass the post-open quiet window. if (!should_service) { dedicated_usb_logging_port_connected.store( false, std::memory_order_release); } if (!connected) { dedicated_usb_logging_line_state_dtr.store( false, std::memory_order_release); } // SOF callbacks execute in TinyUSB's task and provide the bounded owner-task // drain tick. This cached requested state converges ordinary opens/closes // and runtime logging changes. TinyUSB clears the actual SOF-consumer bit on // bus reset, so the owner-task DTR callback above also rearms it after each // re-enumeration even when this sampled state never changed. Queue cursor // mutation remains in TinyUSB's owner task, and the reset generation stays // pending until a later reopen lets that callback reset before its first // drain. if (should_service != dedicated_usb_logging_sof_enabled) { if (should_service) { // Publish the new generation before the callback can run. dedicated_usb_logging_reset_generation.fetch_add( 1, std::memory_order_acq_rel); tud_sof_cb_enable(true); } else { // Stop the consumer before publishing the next reset generation. The // generation remains pending until a later reopen enables the callback. tud_sof_cb_enable(false); dedicated_usb_logging_reset_generation.fetch_add( 1, std::memory_order_acq_rel); } dedicated_usb_logging_sof_enabled = should_service; } #endif #if MESH_NRF52_USB_CONSOLE_COOPERATIVE && !defined(ENABLE_USB_INTERFACE) serviceUsbTerminalPort(); #endif } struct Esp32HwcdcPurgeResult { bool tx_empty = false; }; #if MESH_ESP32_HWCDC_SESSION_GUARD static bool detachEsp32HwcdcPads() { #if ESP_IDF_VERSION_MAJOR >= 5 // IDF 5.x replaced the C3/S3 state-preserving light-sleep helper with the // unified PHY query/setter pair also used by C6. const bool enabled = usb_serial_jtag_ll_phy_is_pad_enabled(); usb_serial_jtag_ll_phy_enable_pad(false); return enabled; #else // IDF 4.x (Arduino-ESP32 2.x C3/S3) supplies this combined helper. return usb_serial_jtag_ll_pad_backup_and_disable(); #endif } static void restoreEsp32HwcdcPads(bool enabled) { #if ESP_IDF_VERSION_MAJOR >= 5 usb_serial_jtag_ll_phy_enable_pad(enabled); #else usb_serial_jtag_ll_enable_pad(enabled); #endif } static void purgeEsp32HwcdcQueues(void* opaque) { Esp32HwcdcPurgeResult* result = static_cast(opaque); size_t tx_capacity = esp32_hwcdc_tx_buffer_capacity.load( std::memory_order_acquire); if (tx_capacity == 0) { // Never replace HWCDC's ring after begin() has enabled its ISR. Without a // known empty capacity we cannot prove the old session was purged, so keep // the transport quarantined and let the caller retry at a bounded rate. while (Serial.read() >= 0) {} return; } const uint32_t purge_started = millis(); uint8_t flush_attempts = 0; do { // The bundled HWCDC flush removes only one contiguous BYTEBUF item, and a // busy TX mutex makes it return without a result. Require at least two // passes, then verify the configured empty capacity under a deadline. Serial.flush(); ++flush_attempts; result->tx_empty = flush_attempts >= 2 && tx_capacity != 0 && Serial.availableForWrite() >= tx_capacity; if (!result->tx_empty) delay(1); } while (!result->tx_empty && (uint32_t)(millis() - purge_started) < 100U); while (Serial.read() >= 0) {} } #endif bool resetUsbCompanionTransport() { #if MESH_ESP32_TINYUSB_NONBLOCKING esp32_tinyusb_reset_generation.fetch_add(1, std::memory_order_acq_rel); serviceEsp32TinyUsbPorts(); return esp32_tinyusb_clean_generation.load(std::memory_order_acquire) == esp32_tinyusb_reset_generation.load(std::memory_order_acquire); #elif MESH_ESP32_HWCDC_SESSION_GUARD // HWCDC owns RTOS queues, a TX mutex, an ISR, and an event task. Calling // end()/begin() here can delete those objects while a WiFi/MQTT/diagnostic // producer is writing. Close the independent transport gate first. Runtime // logging changes remain preferences while the low-level debug route is // forced off by that gate. if (!esp32_hwcdc_cleanup_pending) { portENTER_CRITICAL(&esp32_hwcdc_session_mux); esp32_hwcdc_cleanup_generation = esp32_hwcdc_access_generation.fetch_add( 1, std::memory_order_acq_rel) + 1U; esp32_hwcdc_tx_kick_pending.store(false, std::memory_order_release); esp32_hwcdc_tx_primed.store(false, std::memory_order_release); usb_serial_jtag_ll_disable_intr_mask( USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); portEXIT_CRITICAL(&esp32_hwcdc_session_mux); setPlatformDebugOutputEnabled(false); esp32_hwcdc_restore_pad_enabled = detachEsp32HwcdcPads(); esp32_hwcdc_cleanup_pending = true; // Always hold a real host-visible detach interval. The SOF tracker may // already be false when a physical-loss edge reaches the main loop. delay(10); const uint32_t detached_at = millis(); while (Serial.isPlugged() && (uint32_t)(millis() - detached_at) < 50U) { delay(1); } } // tryRunExclusive() shares the same writer guard as all primary Stream // facades. Retry briefly if a producer entered Serial.write() just before // the access gate closed; HWCDC writes are capped at five milliseconds. Esp32HwcdcPurgeResult result; const uint32_t writer_deadline = millis(); bool ran_exclusive = false; do { ran_exclusive = guarded_esp32_hwcdc_port.tryRunExclusive( purgeEsp32HwcdcQueues, &result); if (!ran_exclusive) delay(1); } while (!ran_exclusive && (uint32_t)(millis() - writer_deadline) < 50U); if (!ran_exclusive || !result.tx_empty) { // Leave the PHY and producer gate closed. The caller retries from a later // loop; rebooting here can create an enumeration watchdog/restart cycle. return false; } esp32_hwcdc_self_reset_guard.expectSelfResetBurst(); restoreEsp32HwcdcPads(esp32_hwcdc_restore_pad_enabled); // Publish only the epoch actually purged. If another BUS_RESET arrived // during cleanup its newer generation remains quarantined for the next pass. portENTER_CRITICAL(&esp32_hwcdc_session_mux); esp32_hwcdc_tx_kick_pending.store(false, std::memory_order_release); esp32_hwcdc_allowed_generation.store( esp32_hwcdc_cleanup_generation, std::memory_order_release); portEXIT_CRITICAL(&esp32_hwcdc_session_mux); esp32_hwcdc_cleanup_pending = false; // A concurrent runtime preference change is authoritative; never restore a // stale snapshot taken before the purge. setPlatformDebugOutputEnabled(isUsbDebugLoggingEnabled()); #endif return true; } Stream& usbLoggingPort() { #if defined(MESH_DUAL_CDC_LOGGING) if (isUsbLoggingEnabled() && dedicated_usb_logging_port_started && dedicated_usb_logging_port_connected.load(std::memory_order_acquire)) { return nonblocking_dedicated_usb_logging_port; } return null_usb_logging_stream; #else if (!isUsbLoggingEnabled()) return null_usb_logging_stream; #if MESH_ESP32_TINYUSB_NONBLOCKING return buffered_esp32_tinyusb_logging_port; #elif MESH_ESP32_HWCDC_SESSION_GUARD return guarded_esp32_hwcdc_port; #elif defined(NRF52_PLATFORM) return nonblocking_primary_usb_logging_port; #else return Serial; #endif #endif } Stream& usbDebugPort() { if (!isUsbDebugLoggingEnabled()) return null_usb_logging_stream; return usbLoggingPort(); } Stream& usbCompanionPort() { #if MESH_ESP32_TINYUSB_NONBLOCKING return nonblocking_esp32_tinyusb_port; #elif MESH_ESP32_HWCDC_SESSION_GUARD return guarded_esp32_hwcdc_port; #elif MESH_NRF52_PRIMARY_USB_NONBLOCKING return nonblocking_primary_usb_companion_port; #else return Serial; #endif } #if defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) bool isUsbCompanionClientConnected() { return canAccessPrimaryUsbSession(nullptr); } #endif Stream& usbMotaPort() { #if MESH_ESP32_TINYUSB_NONBLOCKING return nonblocking_esp32_tinyusb_mota_port; #elif MESH_ESP32_HWCDC_SESSION_GUARD return guarded_esp32_hwcdc_mota_port; #elif MESH_NRF52_PRIMARY_USB_NONBLOCKING return nonblocking_primary_usb_mota_port; #else return usbCompanionPort(); #endif } Stream& usbTerminalPort(bool enabled) { if (!enabled) return null_usb_logging_stream; #if MESH_ESP32_TINYUSB_NONBLOCKING return buffered_esp32_tinyusb_terminal_port; #elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) return buffered_primary_usb_terminal_port; #else return usbCompanionPort(); #endif } Stream& usbConsolePort() { #if MESH_ESP32_TINYUSB_NONBLOCKING return buffered_esp32_tinyusb_terminal_port; #elif MESH_ESP32_HWCDC_SESSION_GUARD return guarded_esp32_hwcdc_port; #elif MESH_NRF52_USB_CONSOLE_COOPERATIVE return buffered_primary_usb_terminal_port; #else return Serial; #endif } 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; #elif MESH_NRF52_USB_CONSOLE_COOPERATIVE return buffered_primary_usb_terminal_port.availableForWrite() >= static_cast(nrf52_usb_functional_reserve); #else return true; #endif } void serviceUsbTerminalPort() { #if MESH_ESP32_TINYUSB_NONBLOCKING serviceEsp32TinyUsbPorts(); #elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) const uint32_t reset_generation = primaryUsbSessionGeneration(); if (reset_generation != primary_usb_terminal_seen_reset_generation) { if (!buffered_primary_usb_terminal_port.tryDiscardPending()) return; primary_usb_terminal_seen_reset_generation = reset_generation; } if (primary_usb_terminal_discard_pending.exchange(false, std::memory_order_acq_rel) && !buffered_primary_usb_terminal_port.tryDiscardPending()) { primary_usb_terminal_discard_pending.store(true, std::memory_order_release); return; } buffered_primary_usb_terminal_port.service(); #elif MESH_ESP32_HWCDC_SESSION_GUARD serviceEsp32HwcdcTxKick(); #endif } void discardUsbTerminalOutput() { #if MESH_ESP32_TINYUSB_NONBLOCKING esp32_tinyusb_terminal_discard_pending.store(true, std::memory_order_release); if (esp32_tinyusb_queue_busy.test_and_set(std::memory_order_acquire)) return; esp32_tinyusb_text_queue.discardPending(); esp32_tinyusb_terminal_reported_dropped_bytes = esp32_tinyusb_terminal_dropped_bytes.load(std::memory_order_relaxed); esp32_tinyusb_terminal_discard_pending.store(false, std::memory_order_release); esp32_tinyusb_queue_busy.clear(std::memory_order_release); #elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) primary_usb_terminal_discard_pending.store(true, std::memory_order_release); serviceUsbTerminalPort(); #endif } bool hasPendingUsbTerminalOutput() { #if MESH_ESP32_TINYUSB_NONBLOCKING if (esp32_tinyusb_queue_busy.test_and_set(std::memory_order_acquire)) return true; const bool pending = esp32_tinyusb_text_queue.queuedByteCount() != 0; esp32_tinyusb_queue_busy.clear(std::memory_order_release); return pending; #elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) return primary_usb_terminal_discard_pending.load(std::memory_order_acquire) || buffered_primary_usb_terminal_port.queuedByteCount() != 0; #else return false; #endif } uint32_t usbTerminalDroppedBytes() { #if MESH_ESP32_TINYUSB_NONBLOCKING return esp32_tinyusb_terminal_dropped_bytes.load(std::memory_order_relaxed); #else return 0; #endif } bool takeUsbTerminalSessionReset() { #if MESH_ESP32_TINYUSB_NONBLOCKING const uint32_t reset_generation = esp32_tinyusb_reset_generation.load(std::memory_order_acquire); if (reset_generation == esp32_tinyusb_taken_reset_generation) return false; esp32_tinyusb_taken_reset_generation = reset_generation; return true; #elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) const uint32_t reset_generation = primaryUsbSessionGeneration(); if (reset_generation == primary_usb_terminal_taken_reset_generation) { return false; } primary_usb_terminal_taken_reset_generation = reset_generation; return true; #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) { return false; } esp32_hwcdc_taken_bus_reset_generation = reset_generation; return true; #else return false; #endif } #if defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) static void completePrimaryUsbSessionReset(void*) { // This runs under the primary SingleAttempt stream's producer gate. It // removes a write that raced the owner-task close callback before allowing // any producer to address the reopened handle. RX was already purged when // the close/line-coding boundary was captured. Do not purge it again here: // hosts such as meshcli send APP_START immediately after opening the port, // while this generation gate is deliberately settling. clearNrf52UsbTxForSession(0); const uint32_t cleaned_generation = primary_usb_terminal_taken_reset_generation; primary_usb_allowed_generation.store(cleaned_generation, std::memory_order_release); } #endif bool tryCompleteUsbTerminalSessionReset() { #if MESH_ESP32_TINYUSB_NONBLOCKING serviceEsp32TinyUsbPorts(); return esp32_tinyusb_clean_generation.load(std::memory_order_acquire) == esp32_tinyusb_reset_generation.load(std::memory_order_acquire); #elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) serviceNrf52UsbSessionReenumeration(); if (nrf52_usb_reenumeration_pending.load(std::memory_order_acquire)) return false; serviceUsbTerminalPort(); const uint32_t generation = primaryUsbSessionGeneration(); if (primary_usb_terminal_discard_pending.load(std::memory_order_acquire) || primary_usb_terminal_seen_reset_generation != generation) return false; // Ordinary roles call this every loop, not only on a reset. Never purge // an already-open endpoint: that would drop every pending reply prefix. if (primary_usb_allowed_generation.load(std::memory_order_acquire) == generation) return true; const uint32_t settle_until = primary_usb_reset_settle_until.load(std::memory_order_acquire); if ((int32_t)(millis() - settle_until) < 0) return false; if (!nonblocking_primary_usb_companion_port.tryRunExclusive( completePrimaryUsbSessionReset)) return false; return primary_usb_allowed_generation.load(std::memory_order_acquire) == primaryUsbSessionGeneration() && !nrf52_usb_reenumeration_pending.load(std::memory_order_acquire); #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 } bool hasDedicatedUsbLoggingPort() { #if defined(MESH_DUAL_CDC_LOGGING) return true; #else return false; #endif } bool isDedicatedUsbLoggingPortConfigured() { #if defined(MESH_DUAL_CDC_LOGGING) return dedicated_usb_logging_port_configured; #else return false; #endif } bool usbLoggingInterfaceRestartRequired() { #if defined(MESH_DUAL_CDC_LOGGING) return dedicated_usb_logging_port_configured != isUsbLoggingEnabled(); #else return false; #endif } const char* usbLoggingPortDescription() { #if defined(MESH_DUAL_CDC_LOGGING) return "dedicated USB CDC 1, interface 02 (Linux: *-if02; tty/COM name is host-assigned)"; #else return "primary USB serial port (tty/COM name is host-assigned)"; #endif } UsbLoggingObservation observeUsbLoggingTransport() { UsbLoggingObservation result; result.tx_progress = usb_logging_tx_progress.load(std::memory_order_acquire); #if MESH_ESP32_TINYUSB_NONBLOCKING result.supported = true; if (xPortInIsrContext()) return result; result.host_connected = tud_mounted(); result.reader_connected = tud_cdc_n_connected(0); result.pending = hasPendingUsbTerminalOutput() || usb_logging_tx_waiting.load(std::memory_order_acquire); #elif MESH_ESP32_HWCDC_SESSION_GUARD result.supported = true; if (xPortInIsrContext()) return result; result.host_connected = Serial.isPlugged(); result.reader_connected = result.host_connected && bool(Serial); const size_t capacity = esp32_hwcdc_tx_buffer_capacity.load(std::memory_order_acquire); result.pending = usb_logging_tx_waiting.load(std::memory_order_acquire) || (capacity != 0 && Serial.availableForWrite() < int(capacity)); #elif defined(NRF52_PLATFORM) && defined(USE_TINYUSB) result.supported = true; const uint8_t instance = hasDedicatedUsbLoggingPort() ? 1 : 0; result.host_connected = tud_mounted(); result.reader_connected = tud_cdc_n_connected(instance); result.pending = usb_logging_tx_waiting.load(std::memory_order_acquire) || tud_cdc_n_write_available(instance) < CFG_TUD_CDC_TX_BUFSIZE; #if defined(MESH_DUAL_CDC_LOGGING) result.pending = result.pending || usb_task_dedicated_usb_logging_port.queuedRecordCount() != 0; #else result.pending = result.pending || hasPendingUsbTerminalOutput(); #endif #endif return result; } void probeUsbLoggingTransport() { if (!isUsbLoggingEnabled()) return; #if MESH_ESP32_TINYUSB_NONBLOCKING || MESH_ESP32_HWCDC_SESSION_GUARD if (xPortInIsrContext()) return; #endif static const uint8_t record[] = "[USB watchdog] heartbeat\r\n"; #if MESH_ESP32_TINYUSB_NONBLOCKING || MESH_ESP32_HWCDC_SESSION_GUARD \ || defined(MESH_DUAL_CDC_LOGGING) || MESH_NRF52_USB_CONSOLE_COOPERATIVE Stream& port = usbLoggingPort(); if (port.availableForWrite() >= int(sizeof(record) - 1)) (void)port.write(record, sizeof(record) - 1); #elif defined(NRF52_PLATFORM) && defined(USE_TINYUSB) // Ordinary nRF52 roles may still use the framework Serial facade. Never // call its retrying write path from the watchdog: use one native attempt. if (tud_cdc_n_connected(0) && tud_cdc_n_write_available(0) >= sizeof(record) - 1) { noteUsbLoggingTxAttempt(); (void)tud_cdc_n_write(0, record, sizeof(record) - 1); (void)tud_cdc_n_write_flush(0); } #endif } UsbLoggingRecoveryResult recoverUsbLoggingTransport(uint8_t stage) { if (!isUsbLoggingEnabled() || (stage != 1 && stage != 2)) return UsbLoggingRecoveryResult::Unsupported; #if MESH_ESP32_TINYUSB_NONBLOCKING if (xPortInIsrContext()) return UsbLoggingRecoveryResult::Deferred; if (stage == 1) { if (!resetUsbCompanionTransport()) return UsbLoggingRecoveryResult::Deferred; probeUsbLoggingTransport(); } else { if (!usb_logging_watchdog_detached) { requestEsp32TinyUsbReenumeration(true); usb_logging_watchdog_attach_at = millis() + 50; usb_logging_watchdog_detached = true; } } return UsbLoggingRecoveryResult::Attempted; #elif MESH_ESP32_HWCDC_SESSION_GUARD if (xPortInIsrContext()) return UsbLoggingRecoveryResult::Deferred; if (stage == 1) { // First just re-kick the existing ISR. Purge/PHY reconnection is the next // stage, and the bounded existing helper retains the driver/descriptors. serviceEsp32HwcdcTxKick(); probeUsbLoggingTransport(); } else if (!resetUsbCompanionTransport()) { return UsbLoggingRecoveryResult::Deferred; } return UsbLoggingRecoveryResult::Attempted; #elif defined(NRF52_PLATFORM) && defined(USE_TINYUSB) if (stage == 1) { #if defined(MESH_DUAL_CDC_LOGGING) // Only publish a request. FIFO/identity/queue mutation remains in CDC1's // existing TinyUSB-owner SOF service, never in this application call. dedicated_usb_logging_watchdog_reset_requested.store(true, std::memory_order_release); dedicated_usb_logging_port_connected.store(false, std::memory_order_release); if (tud_cdc_n_connected(1)) tud_sof_cb_enable(true); #else endPrimaryUsbHostSession(false); clearNrf52UsbTxForSession(0); probeUsbLoggingTransport(); #endif } else if (!usb_logging_watchdog_detached) { if (!tud_disconnect()) return UsbLoggingRecoveryResult::Deferred; usb_logging_watchdog_attach_at = millis() + 50; usb_logging_watchdog_detached = true; } return UsbLoggingRecoveryResult::Attempted; #else return UsbLoggingRecoveryResult::Unsupported; #endif } bool isUsbLoggingTransportRecoveryPending() { #if defined(NRF52_PLATFORM) && defined(USE_TINYUSB) return usb_logging_watchdog_detached || nrf52_usb_reenumeration_pending.load(std::memory_order_acquire); #elif defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE) return nrf52_usb_reenumeration_pending.load(std::memory_order_acquire); #elif MESH_ESP32_TINYUSB_NONBLOCKING const auto state = esp32_tinyusb_reenumeration_state.load(std::memory_order_acquire); return usb_logging_watchdog_detached || state == 1 || state == 3; #elif MESH_ESP32_HWCDC_SESSION_GUARD return esp32_hwcdc_cleanup_pending; #else return false; #endif } } // namespace mesh #if defined(NRF52_PLATFORM) && defined(USE_TINYUSB) extern "C" void meshTinyUsbLoggingTxComplete(uint8_t instance) { #if defined(MESH_DUAL_CDC_LOGGING) if (instance == 1) mesh::noteUsbLoggingTxComplete(); #else if (instance == 0) mesh::noteUsbLoggingTxComplete(); #endif } #endif #if MESH_ESP32_TINYUSB_NONBLOCKING // These strong bridges are called synchronously by the build-local framework // copy, not by Arduino's delayed event queue. Do not include weak declarations // in this translation unit or these definitions would inherit that attribute. extern "C" void meshEsp32TinyUsbTxComplete() { mesh::noteUsbLoggingTxComplete(); } extern "C" void meshEsp32TinyUsbCdcLineState(bool dtr) { // Never use USBCDC::dtr here: a delayed framework unplug event can overwrite // that member after a new host opens. These samples come from the owner. const bool previous = mesh::esp32_tinyusb_owner_dtr.exchange( dtr, std::memory_order_acq_rel); if (previous != dtr) mesh::closeEsp32TinyUsbSession(dtr); } extern "C" bool meshEsp32TinyUsbAcceptRx() { // A late old OUT completion while DTR is low is not fresh-host input. return mesh::esp32_tinyusb_reenumeration_state.load(std::memory_order_acquire) == 0 && tud_cdc_n_connected(0); } extern "C" void meshEsp32TinyUsbDeviceSessionBoundary(bool mounted) { mesh::closeEsp32TinyUsbSession(true); mesh::esp32_tinyusb_owner_dtr.store(false, std::memory_order_release); mesh::esp32_tinyusb_was_connected.store(false, std::memory_order_release); if (mounted) { uint8_t expected = 2; (void)mesh::esp32_tinyusb_reenumeration_state.compare_exchange_strong( expected, 0, std::memory_order_acq_rel); } } #endif #if defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)) // A protocol-only, weak-declaration-free translation unit supplies strong // versions of TinyUSB's callbacks and forwards here. Preserve Adafruit's CDC0 // 1200-baud touch behavior exactly, capture every CDC0 host-session close and // USB device boundary, and give an optional CDC1 logging endpoint its exact // reconnect edges. extern "C" void meshTinyUsbCdcLineStateChanged(uint8_t instance, bool dtr, bool rts) { (void)rts; #if defined(MESH_DUAL_CDC_LOGGING) if (instance == 1) { mesh::handleDedicatedUsbLoggingLineState(dtr); return; } #endif if (instance == 0 && !dtr) { // Discard raw input/output already admitted to TinyUSB. The generation // change keeps every facade closed until the application resets its // protocol owner and performs the final exclusive TX purge. mesh::endPrimaryUsbHostSession(true); cdc_line_coding_t coding; tud_cdc_get_line_coding(&coding); if (coding.bit_rate == 1200) { TinyUSB_Port_EnterDFU(); } } else if (instance == 0) { const bool was_open = mesh::primary_usb_line_state_dtr.load( std::memory_order_acquire); if (!was_open) { // DTR rising can also reopen a prior DTR-low client's handle. mesh::endPrimaryUsbHostSession(true); // Purge any late bytes from the closed owner before publishing the fresh // session. New-host input sent after SET_CONTROL_LINE_STATE completes is // then retained while the application-side generation gate settles. tud_cdc_n_read_flush(0); mesh::primary_usb_line_state_dtr.store( true, std::memory_order_release); } } } extern "C" void meshTinyUsbDeviceSessionBoundary() { // TinyUSB does not issue SET_CONTROL_LINE_STATE(DTR=0) for a hard unplug or // bus reset. An unmount catches the former; the following mount catches a // reset/reconfigure whose old DTR state survived in this application. The // helper is edge-sensitive, so initial enumeration and the mount following // an already observed unmount do not create a false reset. mesh::endPrimaryUsbHostSession(false); #if defined(MESH_DUAL_CDC_LOGGING) mesh::handleDedicatedUsbLoggingLineState(false); #endif } extern "C" void meshTinyUsbCdcLineCodingChanged(uint8_t instance) { if (instance == 0) { // Windows may close/reopen a COM handle without presenting a distinct // DTR-low interval to the device, but it configures line coding for the // new handle. If CDC0 still appears open, treat this as a protocol-session // boundary. Restore the physical DTR sample after closing the epoch so an // intentional baud change on a continuously open handle recovers as soon // as the application has reset its parsers. mesh::endPrimaryUsbHostSession(true); mesh::primary_usb_line_state_dtr.store( tud_cdc_n_connected(0), std::memory_order_release); return; } #if defined(MESH_DUAL_CDC_LOGGING) if (instance == 1) { mesh::handleDedicatedUsbLoggingLineCoding(); } #else (void)instance; #endif } #if defined(MESH_DUAL_CDC_LOGGING) // The weak-declaration-free bridge lives in UsbLoggingLineStateOverride.cpp. // This forwarding target runs in TinyUSB's task, making CDC1 endpoint // ownership explicit without inheriting TinyUSB's weak callback attribute. extern "C" void meshTinyUsbStartOfFrame(uint32_t frame_count) { (void)frame_count; mesh::serviceDedicatedUsbLoggingFromUsbTask(); } #endif #endif #endif