Files
MeshCore/src/helpers/UsbLogging.cpp
T

1342 lines
53 KiB
C++

#include "UsbLogging.h"
#if defined(ARDUINO)
#include <Arduino.h>
#include <atomic>
#include <stdio.h>
#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"
#endif
#if MESH_ESP32_HWCDC_SESSION_GUARD
#include "UsbAsciiBinarySwitch.h"
#endif
#if defined(NRF52_PLATFORM) && \
(defined(ENABLE_USB_INTERFACE) || defined(OTA_FOLDER_SERIAL))
#define MESH_NRF52_PRIMARY_USB_NONBLOCKING 1
#include <Adafruit_TinyUSB.h>
#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<bool> usb_logging_enabled{false};
#else
static std::atomic<bool> usb_logging_enabled{true};
#endif
static std::atomic<bool> usb_logging_preference_known{false};
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<uint32_t> esp32_tinyusb_reset_generation{0};
static std::atomic<uint32_t> esp32_tinyusb_clean_generation{0};
static std::atomic_flag esp32_tinyusb_queue_busy = ATOMIC_FLAG_INIT;
static std::atomic<bool> esp32_tinyusb_terminal_discard_pending{false};
static std::atomic<uint32_t> 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 bool esp32_tinyusb_event_handler_registered = false;
static bool esp32_tinyusb_was_connected = false;
static bool canAccessEsp32TinyUsb(void*) {
return !xPortInIsrContext() && 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 handleEsp32TinyUsbEvent(void*, esp_event_base_t, int32_t event_id,
void* event_data) {
// This is the Arduino event task, not TinyUSB's owner. Only publish an
// epoch: queue cleanup and endpoint access remain in application service.
bool closed = event_id == ARDUINO_USB_CDC_DISCONNECTED_EVENT;
if (event_id == ARDUINO_USB_CDC_LINE_STATE_EVENT && event_data != nullptr) {
const auto* event = static_cast<const arduino_usb_cdc_event_data_t*>(event_data);
closed = !event->line_state.dtr;
}
if (closed) {
esp32_tinyusb_reset_generation.fetch_add(1, std::memory_order_acq_rel);
}
}
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<int>(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;
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_capacity,
esp32_tinyusb_text_capacity + 1> esp32_tinyusb_text_queue(
nonblocking_esp32_tinyusb_port);
template <bool Diagnostic>
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<int>(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<uint32_t>(size), std::memory_order_relaxed);
}
}
};
static Esp32TinyUsbBufferedStream<true> buffered_esp32_tinyusb_logging_port;
static Esp32TinyUsbBufferedStream<false> buffered_esp32_tinyusb_terminal_port;
static void clearEsp32TinyUsbTx(void*) {
(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;
// 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 && !connected) {
esp32_tinyusb_reset_generation.fetch_add(1, std::memory_order_acq_rel);
}
esp32_tinyusb_was_connected = connected;
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);
// Do not purge Serial's RX queue here: the new host may already have sent
// its first query. 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<unsigned long>(
dropped - esp32_tinyusb_terminal_reported_dropped_bytes));
if (length > 0 && static_cast<size_t>(length) < sizeof(marker)
&& esp32_tinyusb_text_queue.write(
reinterpret_cast<const uint8_t*>(marker), length)
== static_cast<size_t>(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<uint32_t> esp32_hwcdc_access_generation{0};
static std::atomic<uint32_t> esp32_hwcdc_allowed_generation{0};
static std::atomic<uint32_t> 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<bool> esp32_hwcdc_tx_kick_pending{false};
static portMUX_TYPE esp32_hwcdc_session_mux = portMUX_INITIALIZER_UNLOCKED;
static uint32_t esp32_hwcdc_taken_bus_reset_generation = 0;
static bool esp32_hwcdc_event_handler_registered = false;
static std::atomic<size_t> esp32_hwcdc_tx_buffer_capacity{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.
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_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) {
// 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);
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();
}
}
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;
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)
// Writers/readers may access CDC0 only when DTR is high and the application has
// completed 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<uint32_t> primary_usb_reset_generation{0};
static std::atomic<uint32_t> primary_usb_allowed_generation{0};
static std::atomic<bool> primary_usb_line_state_dtr{false};
static constexpr uint32_t primary_usb_session_settle_millis = 8;
static std::atomic<uint32_t> primary_usb_reset_settle_until{0};
static uint32_t primaryUsbSessionGeneration() {
return primary_usb_reset_generation.load(std::memory_order_acquire);
}
static bool canAccessPrimaryUsbSession(void*) {
return primary_usb_line_state_dtr.load(std::memory_order_acquire)
&& primary_usb_allowed_generation.load(std::memory_order_acquire)
== primary_usb_reset_generation.load(std::memory_order_acquire);
}
static void endPrimaryUsbHostSession(bool clear_cdc_fifos) {
const bool previous = primary_usb_line_state_dtr.exchange(
false, std::memory_order_acq_rel);
if (!previous) return;
// Publish a short quiescence deadline before closing the generation gate.
// TinyUSB's TX clear does not cancel an endpoint transfer already in its
// class buffer, so the later final TX purge catches a racing old-session
// write. 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 (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);
(void)tud_cdc_n_write_clear(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<uint8_t>(
reinterpret_cast<uintptr_t>(context));
if (data == nullptr || size == 0 || instance >= CFG_TUD_CDC) {
return 0;
}
#if defined(ENABLE_USB_INTERFACE)
if (instance == 0 && !canAccessPrimaryUsbSession(nullptr)) {
return 0;
}
#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;
return tud_cdc_n_write(instance, data, attempt);
}
static SingleAttemptNonBlockingStream nonblocking_primary_usb_companion_port(
Serial, writeTinyUsbCdcOnce, reinterpret_cast<void*>(uintptr_t{0})
#if defined(ENABLE_USB_INTERFACE)
, 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)
// 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;
#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<bool> dedicated_usb_logging_port_connected{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<void*>(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<uint32_t> 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<bool> 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 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() {
// 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);
(void)tud_cdc_n_write_clear(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() {
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) {
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 anything already in flight from the prior session and anything a
// producer raced into the owner queue before the callback gated it.
(void)tud_cdc_n_write_clear(1);
resetDedicatedUsbLoggingUsbTaskState();
--dedicated_usb_logging_quiet_sofs;
return;
}
dedicated_usb_logging_port_connected.store(
true, std::memory_order_release);
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<int>(remaining)) {
(void)tud_cdc_n_write_flush(1);
return;
}
const size_t written = single_attempt_dedicated_usb_logging_port.write(
reinterpret_cast<const uint8_t*>(
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
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);
}
void setUsbLoggingEnabled(bool enabled) {
#if MESH_ESP32_TINYUSB_NONBLOCKING
const bool was_enabled = isUsbLoggingEnabled();
#endif
usb_logging_enabled.store(enabled, std::memory_order_relaxed);
usb_logging_preference_known.store(true, std::memory_order_relaxed);
setPlatformDebugOutputEnabled(enabled);
#if MESH_ESP32_TINYUSB_NONBLOCKING
// Text shares one chronological queue. When leaving logging mode, discard
// its residual application bytes before later Binary/mOTA traffic can start.
// An in-flight producer is gated immediately and cleaned by the next service.
if (was_enabled && !enabled) discardUsbTerminalOutput();
#endif
}
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
// 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 setting explicitly enables logging.
setPlatformDebugOutputEnabled(isUsbLoggingEnabled());
#if MESH_ESP32_TINYUSB_NONBLOCKING
if (!esp32_tinyusb_event_handler_registered) {
Serial.onEvent(ARDUINO_USB_CDC_ANY_EVENT, handleEsp32TinyUsbEvent);
esp32_tinyusb_event_handler_registered = true;
}
#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;
}
#endif
#if defined(MESH_DUAL_CDC_LOGGING)
if (dedicated_usb_logging_port_started
|| !usb_logging_preference_known.load(std::memory_order_relaxed)
|| !isUsbLoggingEnabled()) {
return;
}
dedicated_usb_logging_port.begin(115200);
dedicated_usb_logging_port_configured = true;
dedicated_usb_logging_port_started = true;
// The nRF52 core starts TinyUSB before setup(). If the host completed
// enumeration in that small window, reconnect once so it reads the expanded
// two-CDC descriptor. Usually enumeration has not completed and no reconnect
// is needed.
if (TinyUSBDevice.mounted()) {
TinyUSBDevice.detach();
delay(10);
TinyUSBDevice.attach();
}
#endif
}
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
&& 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
}
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<Esp32HwcdcPurgeResult*>(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);
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(isUsbLoggingEnabled());
#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& 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
}
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() {
#if MESH_ESP32_TINYUSB_NONBLOCKING
return buffered_esp32_tinyusb_terminal_port;
#elif defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
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;
#else
return Serial;
#endif
}
bool canAcceptUsbConsoleCommand() {
#if MESH_ESP32_TINYUSB_NONBLOCKING
return buffered_esp32_tinyusb_terminal_port.availableForWrite()
>= static_cast<int>(esp32_tinyusb_functional_reserve);
#elif MESH_ESP32_HWCDC_SESSION_GUARD
// Leave enough room for a complete ordinary CLI response. Larger listings
// already retain and retry a short write from their cooperative pump.
return guarded_esp32_hwcdc_port.availableForWrite() >= 256;
#else
return true;
#endif
}
void serviceUsbTerminalPort() {
#if MESH_ESP32_TINYUSB_NONBLOCKING
serviceEsp32TinyUsbPorts();
#elif defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
const uint32_t reset_generation =
primaryUsbSessionGeneration();
if (reset_generation != primary_usb_terminal_seen_reset_generation) {
buffered_primary_usb_terminal_port.discardPending();
primary_usb_terminal_seen_reset_generation = reset_generation;
}
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)
buffered_primary_usb_terminal_port.discardPending();
#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)
return 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)
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)
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.
(void)tud_cdc_n_write_clear(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)
const uint32_t settle_until =
primary_usb_reset_settle_until.load(std::memory_order_acquire);
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
}
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
}
} // namespace mesh
#if defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
// 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) {
// 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.
if (mesh::primary_usb_line_state_dtr.load(std::memory_order_acquire)) {
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