1848 lines
77 KiB
C++
1848 lines
77 KiB
C++
#include "UsbLogging.h"
|
|
#include "UsbLoggingClientActivity.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"
|
|
// 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 <Adafruit_TinyUSB.h>
|
|
#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 <Adafruit_TinyUSB.h>
|
|
#include <nrf.h>
|
|
// 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<bool> usb_logging_enabled{false};
|
|
#else
|
|
static std::atomic<bool> usb_logging_enabled{true};
|
|
#endif
|
|
static std::atomic<bool> usb_logging_preference_known{false};
|
|
static std::atomic<bool> usb_debug_enabled{false};
|
|
static std::atomic<uint32_t> usb_logging_tx_progress{0};
|
|
static std::atomic<bool> 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<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 std::atomic<bool> esp32_tinyusb_was_connected{false};
|
|
// 0: ordinary; 1: detached; 2: awaiting fresh mount; 3: preparing quarantine.
|
|
static std::atomic<uint8_t> esp32_tinyusb_reenumeration_state{0};
|
|
static std::atomic<bool> esp32_tinyusb_owner_dtr{false};
|
|
static std::atomic<uint32_t> 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<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;
|
|
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_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);
|
|
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<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 std::atomic<bool> 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<size_t> esp32_hwcdc_tx_buffer_capacity{0};
|
|
static std::atomic<bool> 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<uint32_t> primary_usb_reset_generation{0};
|
|
static std::atomic<uint32_t> primary_usb_allowed_generation{0};
|
|
static std::atomic<bool> primary_usb_terminal_discard_pending{false};
|
|
static std::atomic<bool> primary_usb_line_state_dtr{false};
|
|
static std::atomic<uint32_t> primary_usb_rx_generation{UINT32_MAX};
|
|
static constexpr uint32_t primary_usb_session_settle_millis = 8;
|
|
static std::atomic<uint32_t> primary_usb_reset_settle_until{0};
|
|
static std::atomic<bool> nrf52_usb_reenumeration_pending{false};
|
|
static std::atomic<uint32_t> 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<uint8_t>(
|
|
reinterpret_cast<uintptr_t>(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<void*>(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<int>(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<int>(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<bool> dedicated_usb_logging_port_connected{false};
|
|
static std::atomic<bool> 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<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 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<char>(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<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 && (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<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);
|
|
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<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;
|
|
#elif MESH_NRF52_USB_CONSOLE_COOPERATIVE
|
|
return buffered_primary_usb_terminal_port.availableForWrite()
|
|
>= static_cast<int>(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
|