Fix USB radio stalls and add durable replay timestamp recovery

This commit is contained in:
mikecarper committed 2026-09-03 23:58:32 -07:00
1 parent f2c15225cc
commit a8cf4fd12f
44 files changed
+4339 -148

No files matched your search

+306 -12
View File
@@ -3,6 +3,7 @@
#if defined(ARDUINO)
#include <Arduino.h>
#include <atomic>
#include <stdio.h>
#if defined(ESP32) && defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 1 \
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
@@ -14,9 +15,13 @@
#define MESH_ESP32_HWCDC_SESSION_GUARD 0
#endif
#if defined(NRF52_PLATFORM) || MESH_ESP32_HWCDC_SESSION_GUARD
#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
@@ -72,6 +77,208 @@ class NullUsbLoggingStream : public Stream {
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
@@ -317,7 +524,13 @@ static uint32_t primary_usb_terminal_taken_reset_generation = 0;
#endif
static void setPlatformDebugOutputEnabled(bool enabled) {
#if defined(ESP32_PLATFORM) && defined(ENABLE_USB_INTERFACE)
#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)
// 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
@@ -549,9 +762,18 @@ bool isUsbLoggingEnabled() {
}
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) {
@@ -573,6 +795,12 @@ void beginUsbLoggingPort() {
// 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 (!esp32_hwcdc_event_handler_registered) {
Serial.onEvent(ARDUINO_HW_CDC_ANY_EVENT, handleEsp32HwcdcEvent);
@@ -603,6 +831,9 @@ void beginUsbLoggingPort() {
}
void serviceUsbLoggingPort() {
#if MESH_ESP32_TINYUSB_NONBLOCKING
serviceEsp32TinyUsbPorts();
#endif
#if defined(MESH_DUAL_CDC_LOGGING)
const bool connected = dedicated_usb_logging_port_started
&& dedicated_usb_logging_port.dtr();
@@ -702,7 +933,12 @@ static void purgeEsp32HwcdcQueues(void* opaque) {
#endif
bool resetUsbCompanionTransport() {
#if MESH_ESP32_HWCDC_SESSION_GUARD
#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
@@ -776,7 +1012,9 @@ Stream& usbLoggingPort() {
return null_usb_logging_stream;
#else
if (!isUsbLoggingEnabled()) return null_usb_logging_stream;
#if MESH_ESP32_HWCDC_SESSION_GUARD
#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;
@@ -787,7 +1025,9 @@ Stream& usbLoggingPort() {
}
Stream& usbCompanionPort() {
#if MESH_ESP32_HWCDC_SESSION_GUARD
#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;
@@ -797,7 +1037,9 @@ Stream& usbCompanionPort() {
}
Stream& usbMotaPort() {
#if MESH_ESP32_HWCDC_SESSION_GUARD
#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;
@@ -807,15 +1049,36 @@ Stream& usbMotaPort() {
}
Stream& usbTerminalPort() {
#if defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
#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;
#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);
#else
return true;
#endif
}
void serviceUsbTerminalPort() {
#if defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
#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) {
@@ -829,21 +1092,48 @@ void serviceUsbTerminalPort() {
}
void discardUsbTerminalOutput() {
#if defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
#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 defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
#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 defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
#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) {
@@ -881,7 +1171,11 @@ static void completePrimaryUsbSessionReset(void*) {
#endif
bool tryCompleteUsbTerminalSessionReset() {
#if defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
#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;