Add native USB CDC broker transport

This commit is contained in:
2026-08-23 00:47:56 +02:00
parent 6793f6bcc2
commit aa5fa207b6
11 changed files with 1394 additions and 9 deletions
+3
View File
@@ -9,6 +9,8 @@ idf_component_register(
"serial_console.c"
"session_broker.c"
"session_console.c"
"usb_cdc_transport.c"
"usb_console.c"
INCLUDE_DIRS "."
REQUIRES
console
@@ -16,6 +18,7 @@ idf_component_register(
esp_driver_uart
esp_psram
esp_timer
esp_tinyusb
freertos
led_strip
nvs_flash
+1
View File
@@ -3,3 +3,4 @@ description: ESP32 Serial Swiss Army Knife application
dependencies:
idf: ">=5.3.0"
espressif/led_strip: "^3.0.3"
espressif/esp_tinyusb: "^2.2.1"
+8 -3
View File
@@ -11,6 +11,8 @@
#include "session_broker.h"
#include "session_console.h"
#include "status_led.h"
#include "usb_cdc_transport.h"
#include "usb_console.h"
#define CONSOLE_BAUD_RATE 115200
#define CONSOLE_TX_GPIO 43
@@ -20,7 +22,7 @@ static const char *TAG = "firmware";
void app_main(void)
{
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 2 started");
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife native USB CDC phase started");
if (esp_psram_is_initialized()) {
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size());
@@ -45,9 +47,11 @@ void app_main(void)
}
ESP_ERROR_CHECK(serial_service_init(&serial_config));
ESP_ERROR_CHECK(session_broker_init());
/* Native USB owns GPIO19/20; UART0 logging stays on the USB-to-UART bridge. */
ESP_ERROR_CHECK(usb_cdc_transport_init());
ESP_LOGI(
TAG,
"Using %s serial configuration; UART service remains stopped until 'serial start'",
"Using %s serial configuration; UART service starts on 'serial start' or native USB open",
used_stored_config ? "stored" : "default");
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
@@ -72,8 +76,9 @@ void app_main(void)
ESP_ERROR_CHECK(rs232_hw_test_register_console_commands());
ESP_ERROR_CHECK(serial_console_register_commands());
ESP_ERROR_CHECK(session_console_register_commands());
ESP_ERROR_CHECK(usb_console_register_commands());
ESP_ERROR_CHECK(esp_console_start_repl(repl));
ESP_LOGI(TAG, "Interactive test console ready at %d baud", CONSOLE_BAUD_RATE);
ESP_LOGI(TAG, "Type 'help' for commands; no serial service or test runs automatically");
ESP_LOGI(TAG, "Type 'help' for commands; native USB starts UART1 only when its host port opens");
}
+989
View File
@@ -0,0 +1,989 @@
#include "usb_cdc_transport.h"
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include "esp_mac.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/stream_buffer.h"
#include "freertos/task.h"
#include "serial_config.h"
#include "serial_service.h"
#include "tinyusb.h"
#include "tinyusb_cdc_acm.h"
#include "tinyusb_default_config.h"
#define USB_CDC_HOST_RX_STREAM_SIZE 4096U
#define USB_CDC_IO_CHUNK_SIZE 256U
#define USB_CDC_CONTROL_QUEUE_LENGTH 8U
#define USB_CDC_TASK_STACK_SIZE 5632U
#define USB_CDC_TASK_PRIORITY 8U
#define USB_CDC_POLL_MS 2U
#define USB_CDC_SERVICE_RETRY_MS 250U
#define USB_CDC_CONNECT_RETRY_MS 100U
#define USB_CDC_BROKER_RECONCILE_MS 100U
#define USB_STATE_ATTACHED (1U << 0)
#define USB_STATE_DTR (1U << 1)
#define USB_STATE_RTS (1U << 2)
typedef enum {
USB_CDC_CONTROL_REQUEST_WRITER = 0,
USB_CDC_CONTROL_RELEASE_WRITER,
} usb_cdc_control_t;
typedef struct {
uint8_t data[USB_CDC_IO_CHUNK_SIZE];
size_t size;
size_t offset;
} usb_cdc_pending_buffer_t;
static StreamBufferHandle_t s_host_rx_stream;
static QueueHandle_t s_control_queue;
static atomic_uintptr_t s_transport_task;
static portMUX_TYPE s_state_lock = portMUX_INITIALIZER_UNLOCKED;
static atomic_bool s_initialized;
static atomic_bool s_initializing;
static atomic_uint s_usb_state;
/* Changes on every effective CDC open/close boundary, even during one task poll. */
static atomic_uint s_connection_generation;
static session_broker_client_id_t s_broker_client_id;
static bool s_writer;
static usb_cdc_transport_line_coding_t s_line_coding;
static bool s_line_coding_pending;
static usb_cdc_transport_counters_t s_counters;
static const char s_language_descriptor[] = {0x09, 0x04};
static const char s_manufacturer[] = "ESP32 Serial Tools";
/* esp_tinyusb's default UTF-16 conversion accepts at most 31 characters. */
static const char s_product[] = "ESP32 Serial Swiss Army Knife";
static char s_serial_number[13];
static const char s_cdc_interface[] = "USB CDC";
static const char *s_string_descriptors[] = {
s_language_descriptor,
s_manufacturer,
s_product,
s_serial_number,
s_cdc_interface,
};
static TickType_t milliseconds_to_ticks(uint32_t milliseconds)
{
TickType_t ticks = pdMS_TO_TICKS(milliseconds);
return (milliseconds > 0U && ticks == 0U) ? 1U : ticks;
}
static void add_counter(uint64_t *counter, uint64_t amount)
{
taskENTER_CRITICAL(&s_state_lock);
*counter += amount;
taskEXIT_CRITICAL(&s_state_lock);
}
static void notify_transport_task(void)
{
TaskHandle_t task = (TaskHandle_t)atomic_load(&s_transport_task);
if (task != NULL) {
xTaskNotifyGive(task);
}
}
static bool usb_state_is_open(unsigned int state)
{
return (state & (USB_STATE_ATTACHED | USB_STATE_DTR)) ==
(USB_STATE_ATTACHED | USB_STATE_DTR);
}
static bool usb_host_port_open(void)
{
return usb_state_is_open(atomic_load(&s_usb_state));
}
static void set_broker_state(session_broker_client_id_t client_id, bool writer)
{
taskENTER_CRITICAL(&s_state_lock);
s_broker_client_id = client_id;
s_writer = writer;
taskEXIT_CRITICAL(&s_state_lock);
}
static void set_writer_state(bool writer)
{
taskENTER_CRITICAL(&s_state_lock);
s_writer = writer;
taskEXIT_CRITICAL(&s_state_lock);
}
static void device_event_callback(tinyusb_event_t *event, void *arg)
{
(void)arg;
if (event == NULL) {
add_counter(&s_counters.callback_drops, 1U);
return;
}
switch (event->id) {
case TINYUSB_EVENT_ATTACHED:
atomic_fetch_or(&s_usb_state, USB_STATE_ATTACHED);
notify_transport_task();
break;
case TINYUSB_EVENT_DETACHED: {
/* A new attachment must receive fresh control state and line coding. */
unsigned int old_state = atomic_exchange(&s_usb_state, 0U);
if (usb_state_is_open(old_state)) {
atomic_fetch_add(&s_connection_generation, 1U);
}
taskENTER_CRITICAL(&s_state_lock);
s_line_coding_pending = false;
taskEXIT_CRITICAL(&s_state_lock);
notify_transport_task();
break;
}
default:
break;
}
}
static void cdc_rx_callback(int itf, cdcacm_event_t *event)
{
if (itf != TINYUSB_CDC_ACM_0 || event == NULL || event->type != CDC_EVENT_RX) {
add_counter(&s_counters.callback_drops, 1U);
return;
}
uint8_t data[USB_CDC_IO_CHUNK_SIZE];
for (;;) {
size_t received = 0U;
esp_err_t result = tinyusb_cdcacm_read(TINYUSB_CDC_ACM_0,
data,
sizeof(data),
&received);
if (result != ESP_OK) {
add_counter(&s_counters.callback_drops, 1U);
break;
}
if (received == 0U) {
break;
}
size_t queued = 0U;
if (s_host_rx_stream != NULL) {
/* This callback is the stream's only writer and never waits. */
queued = xStreamBufferSend(s_host_rx_stream, data, received, 0U);
}
taskENTER_CRITICAL(&s_state_lock);
s_counters.host_rx_bytes += received;
s_counters.host_rx_stream_dropped_bytes += received - queued;
taskEXIT_CRITICAL(&s_state_lock);
}
notify_transport_task();
}
static void cdc_wanted_char_callback(int itf, cdcacm_event_t *event)
{
if (itf != TINYUSB_CDC_ACM_0 || event == NULL ||
event->type != CDC_EVENT_RX_WANTED_CHAR) {
add_counter(&s_counters.callback_drops, 1U);
return;
}
/* Wanted-character notifications are only a scheduling hint here. */
notify_transport_task();
}
static void cdc_line_state_callback(int itf, cdcacm_event_t *event)
{
if (itf != TINYUSB_CDC_ACM_0 || event == NULL ||
event->type != CDC_EVENT_LINE_STATE_CHANGED) {
add_counter(&s_counters.callback_drops, 1U);
return;
}
unsigned int old_state = atomic_load(&s_usb_state);
for (;;) {
/* A valid CDC class callback is also proof that this device is attached. */
unsigned int new_state = (old_state | USB_STATE_ATTACHED) &
~(USB_STATE_DTR | USB_STATE_RTS);
if (event->line_state_changed_data.dtr) {
new_state |= USB_STATE_DTR;
}
if (event->line_state_changed_data.rts) {
new_state |= USB_STATE_RTS;
}
if (atomic_compare_exchange_weak(&s_usb_state, &old_state, new_state)) {
if (usb_state_is_open(old_state) != usb_state_is_open(new_state)) {
atomic_fetch_add(&s_connection_generation, 1U);
}
break;
}
}
if (!event->line_state_changed_data.dtr) {
/* Do not apply a closed host session's deferred line coding after reopen. */
taskENTER_CRITICAL(&s_state_lock);
s_line_coding_pending = false;
taskEXIT_CRITICAL(&s_state_lock);
}
notify_transport_task();
}
static void cdc_line_coding_callback(int itf, cdcacm_event_t *event)
{
if (itf != TINYUSB_CDC_ACM_0 || event == NULL ||
event->type != CDC_EVENT_LINE_CODING_CHANGED ||
event->line_coding_changed_data.p_line_coding == NULL) {
add_counter(&s_counters.callback_drops, 1U);
return;
}
/* The TinyUSB value is packed and callback-owned, so copy it immediately. */
cdc_line_coding_t coding;
memcpy(&coding,
event->line_coding_changed_data.p_line_coding,
sizeof(coding));
taskENTER_CRITICAL(&s_state_lock);
if (s_line_coding_pending) {
/* Preserve the latest complete setting and account for the superseded one. */
++s_counters.callback_drops;
}
s_line_coding = (usb_cdc_transport_line_coding_t) {
.baud_rate = coding.bit_rate,
.stop_bits = coding.stop_bits,
.parity = coding.parity,
.data_bits = coding.data_bits,
};
s_line_coding_pending = true;
taskEXIT_CRITICAL(&s_state_lock);
notify_transport_task();
}
static bool take_pending_line_coding(usb_cdc_transport_line_coding_t *coding)
{
bool pending;
taskENTER_CRITICAL(&s_state_lock);
pending = s_line_coding_pending;
if (pending) {
*coding = s_line_coding;
s_line_coding_pending = false;
}
taskEXIT_CRITICAL(&s_state_lock);
return pending;
}
static bool serial_configs_equal(const serial_config_t *left,
const serial_config_t *right)
{
return left->version == right->version &&
left->baud_rate == right->baud_rate &&
left->data_bits == right->data_bits &&
left->parity == right->parity &&
left->stop_bits == right->stop_bits &&
left->flow_control == right->flow_control &&
left->dtr_behavior == right->dtr_behavior &&
left->rts_threshold == right->rts_threshold;
}
static bool map_line_coding(const usb_cdc_transport_line_coding_t *coding,
serial_config_t *config)
{
if (coding->baud_rate < SERIAL_CONFIG_MIN_BAUD_RATE ||
coding->baud_rate > SERIAL_CONFIG_MAX_BAUD_RATE) {
return false;
}
config->baud_rate = coding->baud_rate;
switch (coding->data_bits) {
case 7U:
config->data_bits = SERIAL_CONFIG_DATA_BITS_7;
break;
case 8U:
config->data_bits = SERIAL_CONFIG_DATA_BITS_8;
break;
default:
return false;
}
switch (coding->parity) {
case CDC_LINE_CODING_PARITY_NONE:
config->parity = SERIAL_CONFIG_PARITY_NONE;
break;
case CDC_LINE_CODING_PARITY_ODD:
config->parity = SERIAL_CONFIG_PARITY_ODD;
break;
case CDC_LINE_CODING_PARITY_EVEN:
config->parity = SERIAL_CONFIG_PARITY_EVEN;
break;
default:
/* Mark and space parity are intentionally not representable by UART policy. */
return false;
}
switch (coding->stop_bits) {
case CDC_LINE_CODING_STOP_BITS_1:
config->stop_bits = SERIAL_CONFIG_STOP_BITS_1;
break;
case CDC_LINE_CODING_STOP_BITS_2:
config->stop_bits = SERIAL_CONFIG_STOP_BITS_2;
break;
default:
/* This also rejects USB's 1.5-stop-bit encoding. */
return false;
}
return true;
}
static void apply_pending_line_coding(bool writer)
{
if (!writer || !serial_service_is_running()) {
return;
}
/* Restarting UART1 discards queued TX, so defer framing changes until idle. */
if (serial_service_tx_pending() > 0U) {
return;
}
usb_cdc_transport_line_coding_t coding;
if (!take_pending_line_coding(&coding)) {
return;
}
serial_config_t current;
if (serial_service_get_config(&current) != ESP_OK) {
add_counter(&s_counters.line_coding_failed, 1U);
return;
}
serial_config_t desired = current;
if (!map_line_coding(&coding, &desired)) {
add_counter(&s_counters.line_coding_rejected, 1U);
return;
}
/* Flow control, DTR policy, and RTS threshold remain from current RAM state. */
if (serial_configs_equal(&current, &desired)) {
return;
}
if (serial_service_apply_config(&desired) == ESP_OK) {
add_counter(&s_counters.line_coding_applied, 1U);
} else {
add_counter(&s_counters.line_coding_failed, 1U);
}
}
static bool writer_event_type(session_broker_event_type_t type)
{
return type == SESSION_BROKER_EVENT_WRITER_GRANTED ||
type == SESSION_BROKER_EVENT_WRITER_RELEASED ||
type == SESSION_BROKER_EVENT_WRITER_REVOKED ||
type == SESSION_BROKER_EVENT_WRITER_DENIED;
}
static esp_err_t reconcile_broker_state(session_broker_client_id_t client_id,
bool *writer)
{
session_broker_client_snapshot_t snapshot;
esp_err_t result = session_broker_get_client_snapshot(client_id, &snapshot);
if (result == ESP_OK) {
*writer = snapshot.is_writer;
set_writer_state(*writer);
}
return result;
}
static esp_err_t drain_broker_events(session_broker_client_id_t client_id,
bool *writer)
{
session_broker_event_t event;
for (;;) {
esp_err_t result = session_broker_pop_event(client_id, &event);
if (result == ESP_ERR_TIMEOUT) {
return ESP_OK;
}
if (result != ESP_OK) {
return result;
}
/* writer_id is ownership after this event, including forced changes. */
*writer = event.writer_id == client_id;
set_writer_state(*writer);
if (!writer_event_type(event.type)) {
continue;
}
taskENTER_CRITICAL(&s_state_lock);
++s_counters.writer_events;
if (event.type == SESSION_BROKER_EVENT_WRITER_GRANTED &&
event.client_id == client_id) {
++s_counters.writer_grants;
} else if (event.type == SESSION_BROKER_EVENT_WRITER_DENIED &&
event.client_id == client_id) {
++s_counters.writer_denials;
} else if (event.type == SESSION_BROKER_EVENT_WRITER_REVOKED &&
event.client_id == client_id) {
++s_counters.writer_revocations;
}
taskEXIT_CRITICAL(&s_state_lock);
}
}
static void discard_host_input(usb_cdc_pending_buffer_t *pending)
{
uint64_t discarded = pending->size - pending->offset;
pending->size = 0U;
pending->offset = 0U;
uint8_t data[USB_CDC_IO_CHUNK_SIZE];
size_t drain_budget = USB_CDC_HOST_RX_STREAM_SIZE;
while (drain_budget > 0U) {
size_t request = drain_budget < sizeof(data) ? drain_budget : sizeof(data);
size_t received = xStreamBufferReceive(s_host_rx_stream, data, request, 0U);
if (received == 0U) {
break;
}
discarded += received;
drain_budget -= received;
}
if (discarded > 0U) {
add_counter(&s_counters.broker_rejected_bytes, discarded);
}
}
static void discard_usb_pending(usb_cdc_pending_buffer_t *pending)
{
size_t discarded = pending->size - pending->offset;
pending->size = 0U;
pending->offset = 0U;
if (discarded > 0U) {
add_counter(&s_counters.usb_tx_dropped_bytes, discarded);
}
}
static esp_err_t move_host_data_to_broker(session_broker_client_id_t client_id,
usb_cdc_pending_buffer_t *pending)
{
if (pending->offset == pending->size) {
pending->size = xStreamBufferReceive(s_host_rx_stream,
pending->data,
sizeof(pending->data),
0U);
pending->offset = 0U;
}
if (pending->size == 0U) {
return ESP_OK;
}
size_t accepted = 0U;
esp_err_t result = session_broker_write(client_id,
pending->data + pending->offset,
pending->size - pending->offset,
&accepted);
size_t remaining = pending->size - pending->offset;
if (accepted > remaining) {
accepted = remaining;
}
pending->offset += accepted;
if (accepted > 0U) {
add_counter(&s_counters.broker_accepted_bytes, accepted);
}
if (pending->offset == pending->size) {
pending->size = 0U;
pending->offset = 0U;
}
/* Timeout, zero acceptance, and partial acceptance retain exact byte order. */
return result;
}
static esp_err_t move_broker_data_to_usb(session_broker_client_id_t client_id,
usb_cdc_pending_buffer_t *pending)
{
if (pending->offset == pending->size) {
size_t received = 0U;
esp_err_t result = session_broker_read(client_id,
pending->data,
sizeof(pending->data),
&received);
if (result != ESP_OK) {
return result;
}
pending->size = received;
pending->offset = 0U;
if (received > 0U) {
add_counter(&s_counters.broker_to_usb_bytes, received);
}
}
if (pending->offset < pending->size) {
size_t remaining = pending->size - pending->offset;
size_t queued = tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0,
pending->data + pending->offset,
remaining);
if (queued > remaining) {
queued = remaining;
}
pending->offset += queued;
if (queued > 0U) {
add_counter(&s_counters.usb_tx_queued_bytes, queued);
}
if (pending->offset == pending->size) {
pending->size = 0U;
pending->offset = 0U;
}
}
/* Keep endpoint servicing nonblocking even when the host stops reading. */
(void)tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, 0U);
return ESP_OK;
}
static void discard_control_requests(void)
{
usb_cdc_control_t control;
uint64_t discarded = 0U;
while (xQueueReceive(s_control_queue, &control, 0U) == pdTRUE) {
++discarded;
}
if (discarded > 0U) {
add_counter(&s_counters.control_drops, discarded);
}
}
static esp_err_t process_control_requests(session_broker_client_id_t client_id,
bool *writer)
{
usb_cdc_control_t control;
while (xQueueReceive(s_control_queue, &control, 0U) == pdTRUE) {
esp_err_t result;
if (control == USB_CDC_CONTROL_REQUEST_WRITER) {
result = session_broker_request_writer(client_id);
/* A competing writer is an observed denial, not a lost control. */
if (result != ESP_OK && result != ESP_ERR_INVALID_STATE) {
add_counter(&s_counters.control_drops, 1U);
}
} else {
result = session_broker_release_writer(client_id);
/* Releasing while already an observer is an idempotent no-op. */
if (result != ESP_OK && result != ESP_ERR_INVALID_STATE) {
add_counter(&s_counters.control_drops, 1U);
}
}
if (result == ESP_ERR_NOT_FOUND) {
return result;
}
esp_err_t event_result = drain_broker_events(client_id, writer);
if (event_result != ESP_OK) {
return event_result;
}
}
return ESP_OK;
}
static bool retry_due(TickType_t now,
TickType_t interval,
TickType_t *last_attempt,
bool *attempted)
{
if (!*attempted || (TickType_t)(now - *last_attempt) >= interval) {
*last_attempt = now;
*attempted = true;
return true;
}
return false;
}
static void mark_client_disconnected(session_broker_client_id_t *client_id,
bool *writer)
{
*client_id = SESSION_BROKER_NO_CLIENT;
*writer = false;
set_broker_state(SESSION_BROKER_NO_CLIENT, false);
add_counter(&s_counters.disconnections, 1U);
}
static void transport_task(void *context)
{
(void)context;
session_broker_client_id_t client_id = SESSION_BROKER_NO_CLIENT;
bool writer = false;
unsigned int observed_generation = atomic_load(&s_connection_generation);
TickType_t last_service_attempt = 0U;
TickType_t last_connect_attempt = 0U;
TickType_t last_reconcile = 0U;
bool service_attempted = false;
bool connect_attempted = false;
bool reconciled = false;
usb_cdc_pending_buffer_t host_pending = {0};
usb_cdc_pending_buffer_t usb_pending = {0};
const TickType_t poll_ticks = milliseconds_to_ticks(USB_CDC_POLL_MS);
const TickType_t service_retry_ticks =
milliseconds_to_ticks(USB_CDC_SERVICE_RETRY_MS);
const TickType_t connect_retry_ticks =
milliseconds_to_ticks(USB_CDC_CONNECT_RETRY_MS);
const TickType_t reconcile_ticks =
milliseconds_to_ticks(USB_CDC_BROKER_RECONCILE_MS);
for (;;) {
unsigned int generation = atomic_load(&s_connection_generation);
if (generation != observed_generation) {
/* Never let pending data or a broker identity cross a CDC session. */
service_attempted = false;
connect_attempted = false;
reconciled = false;
discard_control_requests();
discard_host_input(&host_pending);
discard_usb_pending(&usb_pending);
if (client_id != SESSION_BROKER_NO_CLIENT) {
esp_err_t result = session_broker_disconnect(client_id);
if (result == ESP_OK || result == ESP_ERR_NOT_FOUND) {
mark_client_disconnected(&client_id, &writer);
}
if (client_id != SESSION_BROKER_NO_CLIENT) {
/* Leave the generation unmatched so cleanup is retried. */
(void)ulTaskNotifyTake(pdTRUE, poll_ticks);
continue;
}
}
observed_generation = generation;
}
bool open = usb_host_port_open();
if (!open) {
discard_control_requests();
discard_host_input(&host_pending);
discard_usb_pending(&usb_pending);
if (client_id != SESSION_BROKER_NO_CLIENT) {
esp_err_t event_result = drain_broker_events(client_id, &writer);
if (event_result == ESP_ERR_NOT_FOUND) {
mark_client_disconnected(&client_id, &writer);
} else {
esp_err_t result = session_broker_disconnect(client_id);
if (result == ESP_OK || result == ESP_ERR_NOT_FOUND) {
mark_client_disconnected(&client_id, &writer);
}
/* Other failures leave the broker transaction intact for retry. */
}
}
(void)ulTaskNotifyTake(pdTRUE, poll_ticks);
continue;
}
TickType_t now = xTaskGetTickCount();
if (!serial_service_is_running() &&
retry_due(now,
service_retry_ticks,
&last_service_attempt,
&service_attempted)) {
esp_err_t result = serial_service_start();
if (result != ESP_OK && !serial_service_is_running()) {
add_counter(&s_counters.service_start_failures, 1U);
}
}
if (client_id == SESSION_BROKER_NO_CLIENT &&
serial_service_is_running() &&
retry_due(now,
connect_retry_ticks,
&last_connect_attempt,
&connect_attempted)) {
session_broker_client_id_t new_client_id = SESSION_BROKER_NO_CLIENT;
esp_err_t result = session_broker_connect(SESSION_BROKER_CLIENT_USB,
"usb-cdc",
&new_client_id);
if (result == ESP_OK) {
client_id = new_client_id;
writer = false;
set_broker_state(client_id, false);
add_counter(&s_counters.connections, 1U);
/* Initial ownership is opportunistic; denial leaves an observer. */
(void)session_broker_request_writer(client_id);
esp_err_t state_result = drain_broker_events(client_id, &writer);
if (state_result == ESP_OK) {
state_result = reconcile_broker_state(client_id, &writer);
reconciled = state_result == ESP_OK;
last_reconcile = now;
}
if (state_result == ESP_ERR_NOT_FOUND) {
mark_client_disconnected(&client_id, &writer);
}
}
}
if (client_id == SESSION_BROKER_NO_CLIENT) {
discard_control_requests();
discard_host_input(&host_pending);
discard_usb_pending(&usb_pending);
(void)ulTaskNotifyTake(pdTRUE, poll_ticks);
continue;
}
esp_err_t result = drain_broker_events(client_id, &writer);
if (result == ESP_OK) {
result = process_control_requests(client_id, &writer);
}
now = xTaskGetTickCount();
if (result == ESP_OK &&
retry_due(now, reconcile_ticks, &last_reconcile, &reconciled)) {
/* Event queues are bounded; the snapshot is authoritative after gaps. */
result = reconcile_broker_state(client_id, &writer);
}
if (result == ESP_ERR_NOT_FOUND) {
discard_host_input(&host_pending);
discard_usb_pending(&usb_pending);
mark_client_disconnected(&client_id, &writer);
(void)ulTaskNotifyTake(pdTRUE, poll_ticks);
continue;
}
/* Re-check after broker calls that may have waited for another task. */
if (atomic_load(&s_connection_generation) != observed_generation) {
continue;
}
apply_pending_line_coding(writer);
if (atomic_load(&s_connection_generation) != observed_generation) {
continue;
}
if (writer) {
result = move_host_data_to_broker(client_id, &host_pending);
if (result == ESP_ERR_INVALID_STATE) {
/* Ownership may have changed after an event-queue overflow. */
result = reconcile_broker_state(client_id, &writer);
}
if (result == ESP_ERR_NOT_FOUND) {
discard_host_input(&host_pending);
discard_usb_pending(&usb_pending);
mark_client_disconnected(&client_id, &writer);
(void)ulTaskNotifyTake(pdTRUE, poll_ticks);
continue;
}
} else {
/* Observers receive UART data but may not retain host-originated input. */
discard_host_input(&host_pending);
}
result = move_broker_data_to_usb(client_id, &usb_pending);
if (result == ESP_ERR_NOT_FOUND) {
discard_host_input(&host_pending);
discard_usb_pending(&usb_pending);
mark_client_disconnected(&client_id, &writer);
}
(void)ulTaskNotifyTake(pdTRUE, poll_ticks);
}
}
static void reset_uninitialized_state(void)
{
atomic_store(&s_usb_state, 0U);
atomic_store(&s_connection_generation, 0U);
atomic_store(&s_transport_task, (uintptr_t)NULL);
taskENTER_CRITICAL(&s_state_lock);
s_broker_client_id = SESSION_BROKER_NO_CLIENT;
s_writer = false;
s_line_coding = (usb_cdc_transport_line_coding_t) {
.baud_rate = 115200U,
.stop_bits = USB_CDC_TRANSPORT_STOP_BITS_1,
.parity = USB_CDC_TRANSPORT_PARITY_NONE,
.data_bits = 8U,
};
s_line_coding_pending = false;
memset(&s_counters, 0, sizeof(s_counters));
taskEXIT_CRITICAL(&s_state_lock);
}
static void cleanup_init_allocations(bool cdc_initialized, bool driver_installed)
{
if (cdc_initialized) {
(void)tinyusb_cdcacm_deinit(TINYUSB_CDC_ACM_0);
}
if (driver_installed) {
(void)tinyusb_driver_uninstall();
}
if (s_control_queue != NULL) {
vQueueDelete(s_control_queue);
s_control_queue = NULL;
}
if (s_host_rx_stream != NULL) {
vStreamBufferDelete(s_host_rx_stream);
s_host_rx_stream = NULL;
}
reset_uninitialized_state();
}
esp_err_t usb_cdc_transport_init(void)
{
bool expected = false;
if (atomic_load(&s_initialized) ||
!atomic_compare_exchange_strong(&s_initializing, &expected, true)) {
return ESP_ERR_INVALID_STATE;
}
reset_uninitialized_state();
uint8_t mac[6];
esp_err_t result = esp_read_mac(mac, ESP_MAC_WIFI_STA);
if (result != ESP_OK) {
atomic_store(&s_initializing, false);
return result;
}
(void)snprintf(s_serial_number,
sizeof(s_serial_number),
"%02X%02X%02X%02X%02X%02X",
mac[0],
mac[1],
mac[2],
mac[3],
mac[4],
mac[5]);
s_host_rx_stream = xStreamBufferCreate(USB_CDC_HOST_RX_STREAM_SIZE, 1U);
if (s_host_rx_stream == NULL) {
atomic_store(&s_initializing, false);
return ESP_ERR_NO_MEM;
}
s_control_queue = xQueueCreate(USB_CDC_CONTROL_QUEUE_LENGTH,
sizeof(usb_cdc_control_t));
if (s_control_queue == NULL) {
cleanup_init_allocations(false, false);
atomic_store(&s_initializing, false);
return ESP_ERR_NO_MEM;
}
/* ESP32-S3's default full-speed internal PHY is fixed to GPIO19/20. */
tinyusb_config_t usb_config = TINYUSB_DEFAULT_CONFIG(device_event_callback);
usb_config.descriptor.string = s_string_descriptors;
usb_config.descriptor.string_count =
(int)(sizeof(s_string_descriptors) / sizeof(s_string_descriptors[0]));
/* NULL device/config descriptor fields deliberately select class defaults. */
result = tinyusb_driver_install(&usb_config);
if (result != ESP_OK) {
cleanup_init_allocations(false, false);
atomic_store(&s_initializing, false);
return result;
}
const tinyusb_config_cdcacm_t cdc_config = {
.cdc_port = TINYUSB_CDC_ACM_0,
.callback_rx = cdc_rx_callback,
.callback_rx_wanted_char = cdc_wanted_char_callback,
.callback_line_state_changed = cdc_line_state_callback,
.callback_line_coding_changed = cdc_line_coding_callback,
};
result = tinyusb_cdcacm_init(&cdc_config);
if (result != ESP_OK) {
cleanup_init_allocations(false, true);
atomic_store(&s_initializing, false);
return result;
}
TaskHandle_t task = NULL;
if (xTaskCreate(transport_task,
"usb_cdc_transport",
USB_CDC_TASK_STACK_SIZE,
NULL,
USB_CDC_TASK_PRIORITY,
&task) != pdPASS) {
cleanup_init_allocations(true, true);
atomic_store(&s_initializing, false);
return ESP_ERR_NO_MEM;
}
atomic_store(&s_transport_task, (uintptr_t)task);
atomic_store(&s_initialized, true);
atomic_store(&s_initializing, false);
notify_transport_task();
return ESP_OK;
}
esp_err_t usb_cdc_transport_get_snapshot(usb_cdc_transport_snapshot_t *snapshot)
{
if (snapshot == NULL) {
return ESP_ERR_INVALID_ARG;
}
unsigned int usb_state = atomic_load(&s_usb_state);
snapshot->initialized = atomic_load(&s_initialized);
snapshot->attached = (usb_state & USB_STATE_ATTACHED) != 0U;
snapshot->dtr = (usb_state & USB_STATE_DTR) != 0U;
snapshot->rts = (usb_state & USB_STATE_RTS) != 0U;
taskENTER_CRITICAL(&s_state_lock);
snapshot->broker_client_id = s_broker_client_id;
snapshot->writer = s_writer;
snapshot->line_coding = s_line_coding;
snapshot->counters = s_counters;
taskEXIT_CRITICAL(&s_state_lock);
return ESP_OK;
}
static esp_err_t enqueue_control_request(usb_cdc_control_t control)
{
if (!atomic_load(&s_initialized)) {
return ESP_ERR_INVALID_STATE;
}
taskENTER_CRITICAL(&s_state_lock);
bool connected = s_broker_client_id != SESSION_BROKER_NO_CLIENT;
taskEXIT_CRITICAL(&s_state_lock);
if (!connected) {
add_counter(&s_counters.control_drops, 1U);
return ESP_ERR_INVALID_STATE;
}
if (xQueueSend(s_control_queue, &control, 0U) != pdTRUE) {
add_counter(&s_counters.control_drops, 1U);
return ESP_ERR_TIMEOUT;
}
notify_transport_task();
return ESP_OK;
}
esp_err_t usb_cdc_transport_request_writer(void)
{
return enqueue_control_request(USB_CDC_CONTROL_REQUEST_WRITER);
}
esp_err_t usb_cdc_transport_release_writer(void)
{
return enqueue_control_request(USB_CDC_CONTROL_RELEASE_WRITER);
}
esp_err_t usb_cdc_transport_clear_counters(void)
{
if (!atomic_load(&s_initialized)) {
return ESP_ERR_INVALID_STATE;
}
taskENTER_CRITICAL(&s_state_lock);
memset(&s_counters, 0, sizeof(s_counters));
taskEXIT_CRITICAL(&s_state_lock);
return ESP_OK;
}
+82
View File
@@ -0,0 +1,82 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "session_broker.h"
#ifdef __cplusplus
extern "C" {
#endif
/* USB CDC line-coding values are the numeric encodings defined by USB PSTN. */
#define USB_CDC_TRANSPORT_STOP_BITS_1 0U
#define USB_CDC_TRANSPORT_STOP_BITS_1_5 1U
#define USB_CDC_TRANSPORT_STOP_BITS_2 2U
#define USB_CDC_TRANSPORT_PARITY_NONE 0U
#define USB_CDC_TRANSPORT_PARITY_ODD 1U
#define USB_CDC_TRANSPORT_PARITY_EVEN 2U
#define USB_CDC_TRANSPORT_PARITY_MARK 3U
#define USB_CDC_TRANSPORT_PARITY_SPACE 4U
typedef struct {
uint32_t baud_rate;
uint8_t stop_bits;
uint8_t parity;
uint8_t data_bits;
} usb_cdc_transport_line_coding_t;
typedef struct {
uint64_t host_rx_bytes;
uint64_t host_rx_stream_dropped_bytes;
uint64_t broker_accepted_bytes;
uint64_t broker_rejected_bytes;
uint64_t broker_to_usb_bytes;
/* Bytes admitted to TinyUSB's TX FIFO, not a host-delivery acknowledgement. */
uint64_t usb_tx_queued_bytes;
uint64_t usb_tx_dropped_bytes;
uint64_t connections;
uint64_t disconnections;
uint64_t writer_grants;
uint64_t writer_denials;
uint64_t writer_revocations;
uint64_t writer_events;
uint64_t service_start_failures;
uint64_t line_coding_applied;
uint64_t line_coding_rejected;
uint64_t line_coding_failed;
uint64_t callback_drops;
uint64_t control_drops;
} usb_cdc_transport_counters_t;
typedef struct {
bool initialized;
bool attached;
bool dtr;
bool rts;
session_broker_client_id_t broker_client_id;
bool writer;
usb_cdc_transport_line_coding_t line_coding;
usb_cdc_transport_counters_t counters;
} usb_cdc_transport_snapshot_t;
/*
* Installs the native ESP32-S3 USB-OTG device on GPIO19/20 as CDC-ACM 0.
* UART0 console/logging configuration is not changed.
*/
esp_err_t usb_cdc_transport_init(void);
esp_err_t usb_cdc_transport_get_snapshot(usb_cdc_transport_snapshot_t *snapshot);
/* Requests are asynchronous and execute in the transport task. */
esp_err_t usb_cdc_transport_request_writer(void);
esp_err_t usb_cdc_transport_release_writer(void);
/* Clearing counters does not alter connection, ownership, or queued data. */
esp_err_t usb_cdc_transport_clear_counters(void);
#ifdef __cplusplus
}
#endif
+184
View File
@@ -0,0 +1,184 @@
#include "usb_console.h"
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "esp_console.h"
#include "esp_err.h"
#include "session_broker.h"
#include "usb_cdc_transport.h"
static const char *yes_no(bool value)
{
return value ? "yes" : "no";
}
static const char *stop_bits_name(uint8_t stop_bits)
{
switch (stop_bits) {
case USB_CDC_TRANSPORT_STOP_BITS_1:
return "1";
case USB_CDC_TRANSPORT_STOP_BITS_1_5:
return "1.5";
case USB_CDC_TRANSPORT_STOP_BITS_2:
return "2";
default:
return "unknown";
}
}
static const char *parity_name(uint8_t parity)
{
switch (parity) {
case USB_CDC_TRANSPORT_PARITY_NONE:
return "none";
case USB_CDC_TRANSPORT_PARITY_ODD:
return "odd";
case USB_CDC_TRANSPORT_PARITY_EVEN:
return "even";
case USB_CDC_TRANSPORT_PARITY_MARK:
return "mark";
case USB_CDC_TRANSPORT_PARITY_SPACE:
return "space";
default:
return "unknown";
}
}
static int show_status(void)
{
usb_cdc_transport_snapshot_t snapshot;
esp_err_t err = usb_cdc_transport_get_snapshot(&snapshot);
if (err != ESP_OK) {
printf("Could not read USB CDC status: %s\n", esp_err_to_name(err));
return 1;
}
bool host_open = snapshot.attached && snapshot.dtr;
printf("USB CDC: initialized=%s attached=%s host-open=%s DTR=%s RTS=%s\n",
yes_no(snapshot.initialized),
yes_no(snapshot.attached),
yes_no(host_open),
yes_no(snapshot.dtr),
yes_no(snapshot.rts));
if (snapshot.broker_client_id == SESSION_BROKER_NO_CLIENT) {
printf("Broker: disconnected\n");
} else {
printf("Broker: client=%" PRIu32 " role=%s\n",
snapshot.broker_client_id,
snapshot.writer ? "writer" : "observer");
}
printf("Last host line coding: baud=%" PRIu32 " data=%u parity=%s stop=%s\n",
snapshot.line_coding.baud_rate,
(unsigned int)snapshot.line_coding.data_bits,
parity_name(snapshot.line_coding.parity),
stop_bits_name(snapshot.line_coding.stop_bits));
printf("USB line coding changes RAM only; use 'serial save' to persist it.\n");
return 0;
}
static int show_counters(void)
{
usb_cdc_transport_snapshot_t snapshot;
esp_err_t err = usb_cdc_transport_get_snapshot(&snapshot);
if (err != ESP_OK) {
printf("Could not read USB CDC counters: %s\n", esp_err_to_name(err));
return 1;
}
const usb_cdc_transport_counters_t *counter = &snapshot.counters;
printf("Host RX: received=%" PRIu64 " stream-dropped=%" PRIu64
" broker-accepted=%" PRIu64 " discarded=%" PRIu64 "\n",
counter->host_rx_bytes,
counter->host_rx_stream_dropped_bytes,
counter->broker_accepted_bytes,
counter->broker_rejected_bytes);
printf("Host TX: broker-read=%" PRIu64 " USB-queued=%" PRIu64
" local-dropped=%" PRIu64 "\n",
counter->broker_to_usb_bytes,
counter->usb_tx_queued_bytes,
counter->usb_tx_dropped_bytes);
printf("Sessions: connect=%" PRIu64 " disconnect=%" PRIu64
" grants=%" PRIu64 " denials=%" PRIu64
" revocations=%" PRIu64 " writer-events=%" PRIu64 "\n",
counter->connections,
counter->disconnections,
counter->writer_grants,
counter->writer_denials,
counter->writer_revocations,
counter->writer_events);
printf("Line coding: applied=%" PRIu64 " rejected=%" PRIu64
" failed=%" PRIu64 " service-start-failures=%" PRIu64 "\n",
counter->line_coding_applied,
counter->line_coding_rejected,
counter->line_coding_failed,
counter->service_start_failures);
printf("Control/callback drops: control=%" PRIu64 " callback=%" PRIu64 "\n",
counter->control_drops,
counter->callback_drops);
return 0;
}
static int queue_writer_request(bool request)
{
esp_err_t err = request ? usb_cdc_transport_request_writer()
: usb_cdc_transport_release_writer();
if (err != ESP_OK) {
printf("Could not queue USB writer %s: %s\n",
request ? "request" : "release",
esp_err_to_name(err));
return 1;
}
printf("USB writer %s queued; use 'usb status' to observe the result.\n",
request ? "request" : "release");
return 0;
}
static void print_usage(void)
{
printf("Usage: usb status|counters|clear-counters|request-writer|release-writer\n");
}
static int command_usb(int argc, char **argv)
{
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0)) {
return show_status();
}
if (argc == 2 && strcmp(argv[1], "counters") == 0) {
return show_counters();
}
if (argc == 2 && strcmp(argv[1], "clear-counters") == 0) {
esp_err_t err = usb_cdc_transport_clear_counters();
if (err != ESP_OK) {
printf("Could not clear USB CDC counters: %s\n", esp_err_to_name(err));
return 1;
}
printf("USB CDC counters cleared.\n");
return 0;
}
if (argc == 2 && strcmp(argv[1], "request-writer") == 0) {
return queue_writer_request(true);
}
if (argc == 2 && strcmp(argv[1], "release-writer") == 0) {
return queue_writer_request(false);
}
print_usage();
return 1;
}
esp_err_t usb_console_register_commands(void)
{
const esp_console_cmd_t command = {
.command = "usb",
.help = "Inspect native USB CDC and manage its broker writer request",
.hint = NULL,
.func = &command_usb,
.argtable = NULL,
};
return esp_console_cmd_register(&command);
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "esp_err.h"
/* Register UART0 diagnostics and writer controls for native USB CDC-ACM. */
esp_err_t usb_console_register_commands(void);