diff --git a/components/control_plane/src/runtime/control_plane_factory_cli.c b/components/control_plane/src/runtime/control_plane_factory_cli.c index 6e77b3e..e467891 100644 --- a/components/control_plane/src/runtime/control_plane_factory_cli.c +++ b/components/control_plane/src/runtime/control_plane_factory_cli.c @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Factory provisioning serial CLI. Owns command line parsing, provisioning - * command handlers, and UART replies in the control-plane layer. Reads - * `\r\n`-delimited commands from the console UART through the domain console + * command handlers, and per-port replies in the control-plane layer. Reads + * `\r\n`-delimited commands from UART0 and USB Serial/JTAG CDC through the domain console * bridge and replies with `OK`, `ERROR `, or one JSON line. * * 工厂串口 CLI 拥有行缓冲、命令解析和应答格式,但设备身份/Wi-Fi 的持久化仍经 domain 完成。 @@ -36,7 +36,8 @@ #define FACTORY_CLI_SERIAL_MAX_LEN 64 #define FACTORY_CLI_JSON_MAX_LEN 640 #define FACTORY_CLI_WIFI_JSON_MAX_LEN 3072 -#define FACTORY_CLI_READ_TIMEOUT_MS 100 +#define FACTORY_CLI_INPUT_BURST_BYTES 64 +#define FACTORY_CLI_PARTIAL_TIMEOUT_MS 2000 #define FACTORY_CLI_RESET_REPLY_DELAY_MS 100 #ifndef CONFIG_TQ_FACTORY_CLI_TASK_STACK_BYTES #define CONFIG_TQ_FACTORY_CLI_TASK_STACK_BYTES (7U * 1024U) @@ -71,9 +72,22 @@ static bool s_worker_starting; static portMUX_TYPE s_worker_lock = portMUX_INITIALIZER_UNLOCKED; /* Touched only by the single worker task; no extra lock required. */ -static bool s_factory_mode_enabled; -static char s_pending_device_id[FACTORY_CLI_DEVICE_ID_MAX_LEN]; -static char s_pending_serial_number[FACTORY_CLI_SERIAL_MAX_LEN]; +typedef struct { + char line[FACTORY_CLI_MAX_LINE_LEN]; + size_t line_len; + bool discard_until_eol; + TickType_t last_rx_tick; + bool factory_mode_enabled; + char pending_device_id[FACTORY_CLI_DEVICE_ID_MAX_LEN]; + char pending_serial_number[FACTORY_CLI_SERIAL_MAX_LEN]; +} factory_cli_session_t; + +/* 两路行缓冲及身份暂存也隔离,不能把UART的ID和CDC的序列号拼成一次保存。 + * 只有单个CLI worker访问;回复路由只在同步执行命令期间有效。 */ +static factory_cli_session_t s_uart_session; +static factory_cli_session_t s_cdc_session; +static factory_cli_session_t *s_current_session = &s_uart_session; +static bool s_reply_over_cdc; static TaskHandle_t factory_cli_get_worker_task(void) { @@ -250,7 +264,11 @@ static size_t split_args(char *line, char *argv[], size_t max_args) static void cli_write_line(const char *line) { - domain_runtime_console_write_line(line); + if (s_reply_over_cdc) { + domain_runtime_console_write_cdc_line(line); + } else { + domain_runtime_console_write_line(line); + } } static void cli_write_ok(void) @@ -443,7 +461,7 @@ static void handle_get_device_info(void) char json[FACTORY_CLI_JSON_MAX_LEN] = {0}; snprintf(json, sizeof(json), "{\"factory_mode\":%s,\"device_id\":\"%s\",\"serial_number\":\"%s\"}", - s_factory_mode_enabled ? "true" : "false", + s_current_session->factory_mode_enabled ? "true" : "false", device_id, serial_number); cli_write_line(json); @@ -457,8 +475,8 @@ static void handle_save_config(void) domain_runtime_read_device_identity(current_id, sizeof(current_id), current_sn, sizeof(current_sn)); - const char *id_source = s_pending_device_id[0] != '\0' ? s_pending_device_id : current_id; - const char *sn_source = s_pending_serial_number[0] != '\0' ? s_pending_serial_number : current_sn; + const char *id_source = s_current_session->pending_device_id[0] != '\0' ? s_current_session->pending_device_id : current_id; + const char *sn_source = s_current_session->pending_serial_number[0] != '\0' ? s_current_session->pending_serial_number : current_sn; if (is_text_empty(id_source) || is_text_empty(sn_source)) { cli_write_error("identity_not_ready"); return; @@ -481,8 +499,8 @@ static void handle_save_config(void) return; } - s_pending_device_id[0] = '\0'; - s_pending_serial_number[0] = '\0'; + s_current_session->pending_device_id[0] = '\0'; + s_current_session->pending_serial_number[0] = '\0'; /* Force a fresh challenge/response against the new identity on next use. */ control_plane_multimodal_device_auth_invalidate(0); /* 仅发布配置更新;Wi-Fi worker 在自己的上下文恢复配网,不阻塞串口应答。 */ @@ -597,7 +615,7 @@ static void handle_command_line(char *line) const char *cmd = argv[0]; if (strcmp(cmd, "factory_mode") == 0) { - s_factory_mode_enabled = true; + s_current_session->factory_mode_enabled = true; cli_write_ok(); } else if (strcmp(cmd, "get_device_info") == 0) { handle_get_device_info(); @@ -606,9 +624,9 @@ static void handle_command_line(char *line) cli_write_error("missing_device_id"); return; } - strlcpy(s_pending_device_id, argv[1], sizeof(s_pending_device_id)); - trim_text(s_pending_device_id); - if (s_pending_device_id[0] == '\0') { + strlcpy(s_current_session->pending_device_id, argv[1], sizeof(s_current_session->pending_device_id)); + trim_text(s_current_session->pending_device_id); + if (s_current_session->pending_device_id[0] == '\0') { cli_write_error("invalid_device_id"); return; } @@ -618,9 +636,9 @@ static void handle_command_line(char *line) cli_write_error("missing_serial_number"); return; } - strlcpy(s_pending_serial_number, argv[1], sizeof(s_pending_serial_number)); - trim_text(s_pending_serial_number); - if (s_pending_serial_number[0] == '\0') { + strlcpy(s_current_session->pending_serial_number, argv[1], sizeof(s_current_session->pending_serial_number)); + trim_text(s_current_session->pending_serial_number); + if (s_current_session->pending_serial_number[0] == '\0') { cli_write_error("invalid_serial_number"); return; } @@ -684,6 +702,53 @@ static void handle_command_line(char *line) } } +static void factory_cli_poll_input(factory_cli_session_t *session, bool over_cdc) +{ + /* 每轮每路最多64字节,避免一端持续发送饿死另一端;所有命令仍串行执行。 */ + if (session->line_len > 0U && + (TickType_t)(xTaskGetTickCount() - session->last_rx_tick) >= + pdMS_TO_TICKS(FACTORY_CLI_PARTIAL_TIMEOUT_MS)) { + session->line_len = 0U; + session->discard_until_eol = true; + } + for (unsigned i = 0; i < FACTORY_CLI_INPUT_BURST_BYTES; ++i) { + uint8_t ch = 0; + int got = over_cdc ? domain_runtime_console_read_cdc_bytes(&ch, 1U, 0) + : domain_runtime_console_read_bytes(&ch, 1U, 0); + if (got <= 0) { + if (got < 0) { + session->line_len = 0U; + session->discard_until_eol = true; + } + break; + } + session->last_rx_tick = xTaskGetTickCount(); + if (ch == '\r' || ch == '\n') { + if (session->discard_until_eol) { + session->discard_until_eol = false; + session->line_len = 0U; + } else if (session->line_len > 0U) { + session->line[session->line_len] = '\0'; + s_current_session = session; + s_reply_over_cdc = over_cdc; + handle_command_line(session->line); + session->line_len = 0U; + } + continue; + } + if (session->discard_until_eol || ch < 0x20 || ch == 0x7F) { + continue; + } + if (session->line_len < sizeof(session->line) - 1U) { + session->line[session->line_len++] = (char)ch; + } else { + /* 整行丢弃到换行,不能把超长命令尾部误当成save_config等新命令。 */ + session->line_len = 0U; + session->discard_until_eol = true; + } + } +} + static void factory_cli_worker(void *arg) { (void)arg; @@ -696,34 +761,20 @@ static void factory_cli_worker(void *arg) vTaskDelete(NULL); return; } - ESP_LOGI(TAG, "Factory serial CLI ready on console UART"); - - char line[FACTORY_CLI_MAX_LINE_LEN]; - size_t line_len = 0; + ESP_LOGI(TAG, "Factory serial CLI ready (UART0 and enabled USB Serial/JTAG CDC)"); + memset(&s_uart_session, 0, sizeof(s_uart_session)); + memset(&s_cdc_session, 0, sizeof(s_cdc_session)); + s_current_session = &s_uart_session; + s_reply_over_cdc = false; while (!factory_cli_get_stop_requested()) { - uint8_t ch = 0; - int got = domain_runtime_console_read_bytes(&ch, 1, FACTORY_CLI_READ_TIMEOUT_MS); - if (got <= 0) { - continue; - } - - if (ch == '\r' || ch == '\n') { - if (line_len > 0) { - line[line_len] = '\0'; - handle_command_line(line); - line_len = 0; - } - continue; - } - if (ch < 0x20 || ch == 0x7F) { - continue; - } - if (line_len < sizeof(line) - 1U) { - line[line_len++] = (char)ch; - } else { - line_len = 0; /* overlong line: drop and resync */ + factory_cli_poll_input(&s_uart_session, false); + if (factory_cli_get_stop_requested()) { + break; } + factory_cli_poll_input(&s_cdc_session, true); + /* 驱动读取均为非阻塞;空闲时至少让出一个tick,不创建第二个CLI任务。 */ + vTaskDelay(1); } factory_cli_finish_worker_exit(); diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index 219f2a4..eeae5af 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -613,12 +613,20 @@ void domain_runtime_restart(void); * * `read_bytes` returns bytes read (0 on timeout, <0 on error). A zero timeout * polls once; negative timeouts are invalid and return <0. Used by the factory - * provisioning CLI. + * provisioning CLI. install幂等,安装锁最多等待100ms;至少一路可用即成功。 + * 原有read/write_line操作UART0,CDC使用下方独立入口。 */ esp_err_t domain_runtime_console_install(void); int domain_runtime_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms); void domain_runtime_console_write_line(const char *line); +/** 独立USB Serial/JTAG CDC端点:read语义同UART,禁用/不可用返回负数。 + * write为尽力发送(共250ms入队预算,超时可能已发送部分),不切换或广播到UART。 + * 两路输入必须由同一个CLI任务消费,并各自维护行缓冲。驱动常驻,不随CLI stop卸载。 + */ +int domain_runtime_console_read_cdc_bytes(uint8_t *buf, size_t len, int timeout_ms); +void domain_runtime_console_write_cdc_line(const char *line); + void domain_runtime_behavior_set_keep_awake(bool keep_awake); esp_err_t domain_runtime_behavior_play_prompt(domain_wifi_prompt_id_t prompt); esp_err_t domain_runtime_behavior_play_prompt_file(const char *path); diff --git a/components/domain/src/runtime/domain_runtime_adapters.c b/components/domain/src/runtime/domain_runtime_adapters.c index 9a73261..420ea56 100644 --- a/components/domain/src/runtime/domain_runtime_adapters.c +++ b/components/domain/src/runtime/domain_runtime_adapters.c @@ -1213,6 +1213,16 @@ void domain_runtime_console_write_line(const char *line) platform_console_write_line(line); } +int domain_runtime_console_read_cdc_bytes(uint8_t *buf, size_t len, int timeout_ms) +{ + return platform_console_read_cdc_bytes(buf, len, timeout_ms); +} + +void domain_runtime_console_write_cdc_line(const char *line) +{ + platform_console_write_cdc_line(line); +} + esp_err_t domain_runtime_stepper_rotate_angle_with_accel_timeout(float angle, int target_delay_us, int timeout_ms) diff --git a/components/platform/CMakeLists.txt b/components/platform/CMakeLists.txt index d135271..caa2a64 100644 --- a/components/platform/CMakeLists.txt +++ b/components/platform/CMakeLists.txt @@ -77,6 +77,10 @@ set(platform_priv_requires nvs_flash ) +if(CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE) + list(APPEND platform_priv_requires esp_driver_usb_serial_jtag) +endif() + if(CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE) list(APPEND platform_priv_requires lwip) endif() diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index 24ef374..0e41b34 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -243,7 +243,8 @@ esp_err_t platform_write_device_identity(const char *device_id, const char *seri * @brief 工厂 CLI 使用的串口行传输原语。 * Console (UART) line-transport primitives for the factory CLI. * - * 这些接口不保存命令状态。install 可重复调用;read 返回读取字节数,超时返回0, + * 这些接口不保存命令状态。install 可重复调用,安装锁最多等待100ms;至少一路可用即成功。 + * 原有read/write_line操作UART0;read 返回读取字节数,超时返回0, * 错误返回负数。零超时表示轮询一次,负超时无效。 * Stateless primitives over the console UART. `platform_console_install()` is * idempotent (a pre-existing console driver is treated as success). @@ -254,6 +255,13 @@ esp_err_t platform_console_install(void); int platform_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms); void platform_console_write_line(const char *line); +/** 独立USB Serial/JTAG CDC端点:read语义同UART,禁用/不可用返回负数。 + * write为尽力发送(共250ms入队预算,超时可能已发送部分),不切换或广播到UART。 + * 两路输入必须由同一个CLI任务消费,并各自维护行缓冲。驱动常驻,不随CLI stop卸载。 + */ +int platform_console_read_cdc_bytes(uint8_t *buf, size_t len, int timeout_ms); +void platform_console_write_cdc_line(const char *line); + /** * @brief 基于 NVS 的通用持久化键值存储原语。 * Generic persistent key/value blob storage (NVS-backed). diff --git a/components/platform/src/platform_console_driver.c b/components/platform/src/platform_console_driver.c index 5f80a70..a0d7d98 100644 --- a/components/platform/src/platform_console_driver.c +++ b/components/platform/src/platform_console_driver.c @@ -3,15 +3,24 @@ * * SPDX-License-Identifier: Apache-2.0 * - * Console UART line-transport primitives for the factory CLI. Line + * UART and USB Serial/JTAG CDC line-transport primitives for the factory CLI. Line * accumulation and the worker loop live in the control-plane CLI owner. */ #include "platform.h" #include +#include #include "driver/uart.h" +#include "sdkconfig.h" +#if CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE +#include "driver/usb_serial_jtag.h" +#if CONFIG_VFS_SUPPORT_IO +#include "driver/usb_serial_jtag_vfs.h" +#endif +#endif +#include "freertos/semphr.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -22,30 +31,67 @@ static const char *TAG = "platform_console"; -/* UART0 驱动安装具有幂等性,已由其他启动路径安装也视为可用。 */ +/* 两个驱动是平台常驻资源,CLI stop只停止消费者,不卸载共享控制台驱动。 + * 初始化串行化且可重入;USB安装失败仍允许UART工作,下一次install会重试USB。 */ +static StaticSemaphore_t s_install_lock_buffer; +static SemaphoreHandle_t s_install_lock; +static portMUX_TYPE s_install_lock_guard = portMUX_INITIALIZER_UNLOCKED; +#if CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE && CONFIG_VFS_SUPPORT_IO +static bool s_usb_vfs_attached; +#endif + esp_err_t platform_console_install(void) { - esp_err_t err = uart_driver_install(PLATFORM_CONSOLE_UART_PORT, - PLATFORM_CONSOLE_RX_BUFFER, - 0, - 0, - NULL, - 0); - /* The console may have already installed the UART driver; treat that as success. */ - if (err == ESP_ERR_INVALID_STATE) { - return ESP_OK; + portENTER_CRITICAL(&s_install_lock_guard); + if (s_install_lock == NULL) { + s_install_lock = xSemaphoreCreateMutexStatic(&s_install_lock_buffer); } - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to install UART%d driver: %s", - (int)PLATFORM_CONSOLE_UART_PORT, esp_err_to_name(err)); + portEXIT_CRITICAL(&s_install_lock_guard); + if (s_install_lock == NULL || + xSemaphoreTake(s_install_lock, pdMS_TO_TICKS(100)) != pdTRUE) { + return ESP_ERR_TIMEOUT; } - return err; + esp_err_t uart_err = ESP_OK; + if (!uart_is_driver_installed(PLATFORM_CONSOLE_UART_PORT)) { + uart_err = uart_driver_install(PLATFORM_CONSOLE_UART_PORT, + PLATFORM_CONSOLE_RX_BUFFER, 0, 0, NULL, 0); + } + if (uart_err != ESP_OK) { + ESP_LOGW(TAG, "Factory UART0 unavailable: %s", esp_err_to_name(uart_err)); + } + bool ready = uart_err == ESP_OK; +#if CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE + esp_err_t usb_err = ESP_OK; + if (!usb_serial_jtag_is_driver_installed()) { + usb_serial_jtag_driver_config_t config = { + .rx_buffer_size = PLATFORM_CONSOLE_RX_BUFFER, + .tx_buffer_size = 1024, + }; + usb_err = usb_serial_jtag_driver_install(&config); + } + if (usb_err != ESP_OK) { + ESP_LOGW(TAG, "Factory USB Serial/JTAG CDC unavailable: %s", esp_err_to_name(usb_err)); + } else { +#if CONFIG_VFS_SUPPORT_IO + if (!s_usb_vfs_attached) { + /* 日志的USB第二输出也走同一驱动队列,避免VFS直接写FIFO与ISR竞争。 */ + usb_serial_jtag_vfs_use_driver(); + s_usb_vfs_attached = true; + } +#endif + ESP_LOGI(TAG, "Factory USB Serial/JTAG CDC ready (GPIO19 D-, GPIO20 D+)"); + ready = true; + } +#endif + (void)xSemaphoreGive(s_install_lock); + return ready ? ESP_OK : uart_err; } int platform_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms) { /* 0 表示单次轮询,负数是无效参数;正数转换为 FreeRTOS 有界等待。 */ - if (buf == NULL || len == 0U || timeout_ms < 0) { + if (buf == NULL || len == 0U || timeout_ms < 0 || + !uart_is_driver_installed(PLATFORM_CONSOLE_UART_PORT)) { return -1; } TickType_t ticks = pdMS_TO_TICKS(timeout_ms); @@ -55,9 +101,63 @@ int platform_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms) void platform_console_write_line(const char *line) { /* 工厂协议按行发送,统一补充 CRLF,避免调用方重复处理行结束符。 */ - if (line == NULL) { + if (line == NULL || !uart_is_driver_installed(PLATFORM_CONSOLE_UART_PORT)) { return; } uart_write_bytes(PLATFORM_CONSOLE_UART_PORT, line, strlen(line)); uart_write_bytes(PLATFORM_CONSOLE_UART_PORT, "\r\n", 2); } + +int platform_console_read_cdc_bytes(uint8_t *buf, size_t len, int timeout_ms) +{ +#if CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE + if (buf == NULL || len == 0U || len > UINT32_MAX || timeout_ms < 0 || + !usb_serial_jtag_is_driver_installed()) { + return -1; + } + return usb_serial_jtag_read_bytes(buf, (uint32_t)len, pdMS_TO_TICKS(timeout_ms)); +#else + (void)buf; + (void)len; + (void)timeout_ms; + return -1; +#endif +} + +void platform_console_write_cdc_line(const char *line) +{ +#if CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE + if (line == NULL || !usb_serial_jtag_is_driver_installed()) { + return; + } + /* USB未打开/拔出时不能让CLI卡住。使用零等待的小块写入和一条应答共250ms预算; + * 超时停止入队,已经交给驱动的字节不可撤销,主机按完整JSON/OK判定成功。 */ + TickType_t started = xTaskGetTickCount(); + TickType_t budget = pdMS_TO_TICKS(250); + /* 普通ESP_LOG使用stdout;按行有界获取同一锁,避免JSON被并发日志插入。 + * 不使用无界flockfile,未能获得锁也必须让CLI继续处理停止/另一端输入。 */ + while (ftrylockfile(stdout) != 0) { + if ((TickType_t)(xTaskGetTickCount() - started) >= budget) { + return; + } + vTaskDelay(1); + } + size_t length = strlen(line); + size_t sent = 0U; + while (sent < length + 2U && + (TickType_t)(xTaskGetTickCount() - started) < budget) { + const char *data = sent < length ? line + sent : &"\r\n"[sent - length]; + size_t remaining = sent < length ? length - sent : length + 2U - sent; + size_t chunk = remaining > 64U ? 64U : remaining; + int written = usb_serial_jtag_write_bytes(data, chunk, 0); + if (written > 0) { + sent += (size_t)written; + } else { + vTaskDelay(1); + } + } + funlockfile(stdout); +#else + (void)line; +#endif +} diff --git a/docs/firmware-pin-usage.md b/docs/firmware-pin-usage.md index 6512aa6..2f58ee0 100644 --- a/docs/firmware-pin-usage.md +++ b/docs/firmware-pin-usage.md @@ -100,3 +100,11 @@ bounded lock failure, cleanup and retry (`tools/test_battery_adc_init.py`). Config/default consistency and enabled-pin uniqueness checks passed. No device was flashed; touch polarity, ADC2 sampling with Wi-Fi, charger/wake, motor direction and concurrent operation still require hardware validation. + +## Factory CLI UART and CDC + +`CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE=y` enables native USB Serial/JTAG CDC input +on GPIO19/20 alongside UART0 (GPIO43 TX / GPIO44 RX). Logs may already appear on +CDC through the secondary console; this option additionally enables command input +and routes replies back to the requesting port. UART and CDC line buffers and +pending identity values are separate. See [the CLI guide](工厂CLI串口与CDC使用说明.zh-CN.md). diff --git a/docs/knowledge vault/device-auth-v2-flow.zh-CN.md b/docs/knowledge vault/device-auth-v2-flow.zh-CN.md index 7b36a38..5d6ca0c 100644 --- a/docs/knowledge vault/device-auth-v2-flow.zh-CN.md +++ b/docs/knowledge vault/device-auth-v2-flow.zh-CN.md @@ -130,7 +130,10 @@ CONFIG_TQ_FACTORY_CLI_ENABLE=y control_plane_factory_cli_start(); ``` -它创建名为 `factory_cli` 的 FreeRTOS 任务,通过 Console UART 读取以 `\r` 或 `\n` 结束的命令。 +它创建单个名为 `factory_cli` 的 FreeRTOS 任务,同时轮询 UART0 与原生 USB Serial/JTAG CDC, +读取以 `\r` 或 `\n` 结束的命令。新增 `CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE=y` 默认启用CDC, +两路各自维护行缓冲、工厂模式和待保存身份字段,回复只发给命令来源。 +详见 [工厂CLI串口与CDC使用说明](../工厂CLI串口与CDC使用说明.zh-CN.md)。 关键参数: @@ -138,8 +141,8 @@ control_plane_factory_cli_start(); | --- | ---: | | 最大命令行长度 | 512 B | | 最大参数数量 | 8 | -| 串口读取超时 | 100 ms | -| 任务栈 | 12 KiB | +| 接收调度 | 两路非阻塞读取,每路每轮最多64字节,每轮让出1 tick | +| 任务栈 | 默认7 KiB(`CONFIG_TQ_FACTORY_CLI_TASK_STACK_BYTES`) | | 任务优先级 | 4 | | 运行核心 | `CONFIG_TQ_LOCAL_TASK_CORE` | @@ -186,8 +189,8 @@ get_device_info `set_device_id` 和 `set_serial_number` 只将值暂存在 RAM: ```c -s_pending_device_id -s_pending_serial_number +s_current_session->pending_device_id +s_current_session->pending_serial_number ``` 只有执行 `save_config` 后,才会调用: diff --git a/docs/工厂CLI串口与CDC使用说明.zh-CN.md b/docs/工厂CLI串口与CDC使用说明.zh-CN.md new file mode 100644 index 0000000..b17fc0d --- /dev/null +++ b/docs/工厂CLI串口与CDC使用说明.zh-CN.md @@ -0,0 +1,90 @@ +# 工厂 CLI:UART0 与原生 USB CDC + +2026-10-23:工厂CLI新增原生USB Serial/JTAG CDC收发,保留UART0。 +此前CDC可看到第二控制台日志,但工厂CLI只读取UART0;“能看到日志”不代表能输入命令。 + +## 配置与接线 + +`menuconfig`位置:`TQ Cat Configuration -> Voice, Authentication & Multimodal -> +Credentials & Device Authentication`。 + +```text +CONFIG_TQ_FACTORY_CLI_ENABLE=y +CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE=y +``` + +第二项默认开启,要求芯片支持USB Serial/JTAG,且没有选择USB-OTG控制台。 +这里的CDC是ESP32-S3自带的USB Serial/JTAG设备,不是TinyUSB/USB-OTG CDC实现。 +关闭第二项仍能使用UART0。 + +- 原生USB:GPIO19 D-、GPIO20 D+,使用板上的原生USB接口和数据线。 +- UART0:GPIO43 TX、GPIO44 RX,使用3.3V USB转串口,交叉连接TX/RX并共地。 +- 当前日志主控制台仍为UART0,USB Serial/JTAG仍为第二日志输出;不改变刷机方式。 + +## 发送命令 + +刷入新固件后,打开原生USB枚举出来的串口。macOS通常为`/dev/cu.usbmodem…`, +应使用本机实际枚举的端口。UART当前波特率115200;原生USB CDC的速率不代表UART线速。 + +逐行发送,每行以回车或换行结束: + +```text +get_device_info +factory_mode +set_device_id <后台登记的设备UUID> +set_serial_number <同一台设备的实际序列号> +save_config +get_device_info +get_auth_material +``` + +普通设置成功返回`OK`;读取命令返回JSON和`OK`;失败返回`ERROR `。 +`save_config`成功才表示写入Flash。JSON和`OK`应作为独立完整行读取,工具应允许穿插日志行。 +关于注册服务和工位签名,继续按设备认证文档操作;CDC不改变认证协议。 + +日志入口: + +```text +platform_console: Factory USB Serial/JTAG CDC ready (GPIO19 D-, GPIO20 D+) +cp_factory_cli: Factory serial CLI ready (UART0 and enabled USB Serial/JTAG CDC) +``` + +平台日志确认实际CDC驱动安装;CLI通用就绪日志只表示至少有一路可用。 +如果某一路初始化失败,会打印对应警告,另一路仍可工作,后续install可重试失败一路。 + +## 隔离、超时和生命周期 + +只有一个CLI任务。每轮先后读取两路,各最多64字节,非阻塞读取后让出一个tick。 +两路命令串行执行,不新增第二个worker。每路独立保存行缓冲、工厂模式及待提交身份, +回复只发回来源端口。持久化的设备身份仍是整台设备共享,不能把两路当成两台设备操作。 + +超长行丢弃至下一个行尾,不把尾部重新当作命令。半行连续2秒没有新字节也会丢弃至行尾; +USB拔插后建议先发送一个空行,再发完整命令。CDC读取报错也会清理当前半行。 + +USB发送使用小块、零等待驱动入队,一条回复共用250ms预算(含获取stdout锁), +并按整行保护,避免普通ESP_LOG插进JSON。USB未打开或拔出时不会无限等待。 +超时可能已有部分数据入队;250ms是软件入队预算,不保证主机已收到。 +工具必须检查完整应答,超时后可先换行并查询设备状态,不能仅凭部分输出认定保存成功。 +UART沿用原有发送路径。驱动是平台常驻共享资源,CLI停止/重启不卸载驱动, +启动可重复调用;CLI仍按原有协作取消、有界等待和强制兜底策略停止。 + +新增USB RX/TX缓冲请求分别为2048/1024字节,另有驱动/队列/ISR开销; +两路行数组共1024字节,待提交身份数组共256字节,另有会话元数据。 +它们不是图像或DMA缓存;没有新增CLI任务,也未增加CLI任务栈配置。 + +## 验证 + +```sh +python3 tools/test_factory_cli_console.py +python3 tools/test_provisioning_recovery.py +``` + +第一项编译实际平台驱动及CLI组帧、命令解析、身份保存函数,分别验证CDC开/关、 +驱动重复初始化、单端失败与回退、双端来源路由、身份暂存隔离、USB发送超时、超长行 +与半行恢复;使用ASan/UBSan。第二项回归身份保存通知和配网恢复。 + +本次CDC开启/关闭的主机测试和配网恢复测试均通过,ESP-IDF 5.5.4新板配置构建通过; +未烧录,尚未完成本次实现的USB枚举与实机收发验证。 + +实机验收需分别通过UART、CDC发送`get_device_info`,再验证CDC身份保存、公钥读取、 +USB拔插及双端交错命令。主机故障桩和构建不能代替USB枚举及实机收发测试。 diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 06cd03d..2db891e 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -1380,6 +1380,16 @@ menu "TQ Cat Configuration" device identity provisioning, public-key retrieval, and on-device challenge signing. + config TQ_FACTORY_CLI_USB_CDC_ENABLE + bool "Also accept factory CLI commands over USB Serial/JTAG CDC" + depends on TQ_FACTORY_CLI_ENABLE && SOC_USB_SERIAL_JTAG_SUPPORTED && !ESP_CONSOLE_USB_CDC + default y + help + Use the ESP32-S3 built-in USB Serial/JTAG CDC port in addition + to UART0. Each input has a separate command buffer and replies + return to the requesting port. This is not TinyUSB/USB-OTG CDC. + GPIO19/20 must remain connected to the native USB connector. + config TQ_FACTORY_CLI_TASK_STACK_BYTES int "Factory CLI task stack (bytes)" default 7168 diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 4cd8def..fe583ac 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -190,6 +190,7 @@ CONFIG_TQ_MULTIMODAL_WATCHDOG_TASK_STACK_BYTES=6144 # CONFIG_MULTIMODAL_AUTH_BYPASS_TOKEN_SERVICE is not set CONFIG_AUTH_V2_ENABLE=y CONFIG_TQ_FACTORY_CLI_ENABLE=y +CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE=y CONFIG_TQ_FACTORY_CLI_TASK_STACK_BYTES=7168 CONFIG_MULTIMODAL_UPSTREAM_MODE="push2talk" CONFIG_MULTIMODAL_PTT_BUTTON_GPIO=13 diff --git a/tools/test_factory_cli_console.py b/tools/test_factory_cli_console.py new file mode 100644 index 0000000..995e238 --- /dev/null +++ b/tools/test_factory_cli_console.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +"""Compile actual console driver and CLI framing/handlers with UART/CDC stubs.""" +from pathlib import Path +import re +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] + + +def extract(source, name): + match = re.search(r'^(?:static )?[\w *]+\b' + name + r'\([^;]*?\)\s*\{', source, re.M) + assert match, name + end, depth = match.end(), 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[match.start():end] + + +STUBS = r''' +#include +#include +#include +#include +#include +#include +#include +#include +#define CONFIG_VFS_SUPPORT_IO 1 +#define ESP_OK 0 +#define ESP_FAIL 1 +#define ESP_ERR_TIMEOUT 2 +#define UART_NUM_0 0 +#define portMUX_INITIALIZER_UNLOCKED 0 +#define portENTER_CRITICAL(p) ((void)(p)) +#define portEXIT_CRITICAL(p) ((void)(p)) +#define pdTRUE 1 +#define pdMS_TO_TICKS(n) (n) +#define ESP_LOGI(tag, ...) do { (void)(tag); if(0) printf(__VA_ARGS__); } while(0) +#define ESP_LOGW ESP_LOGI +#define ESP_LOGE ESP_LOGI +#define FACTORY_CLI_MAX_LINE_LEN 512 +#define FACTORY_CLI_MAX_ARGC 8 +#define FACTORY_CLI_DEVICE_ID_MAX_LEN 64 +#define FACTORY_CLI_SERIAL_MAX_LEN 64 +#define FACTORY_CLI_INPUT_BURST_BYTES 64 +#define FACTORY_CLI_PARTIAL_TIMEOUT_MS 2000 +#define FACTORY_CLI_JSON_MAX_LEN 640 +#define DOMAIN_EXPRESSION_CHOUQIAN 1 +#define DOMAIN_EXPRESSION_TRIGGER_FACTORY_LCD_TEST 2 +typedef int esp_err_t; +typedef uint32_t TickType_t; +typedef int StaticSemaphore_t; +typedef int *SemaphoreHandle_t; +typedef int portMUX_TYPE; +typedef struct {uint32_t rx_buffer_size,tx_buffer_size;} usb_serial_jtag_driver_config_t; +typedef struct {int id,trigger;bool chouqian_result_index_valid;size_t chouqian_result_index;} domain_expression_request_t; +static TickType_t ticks; +static bool uart_ready,usb_ready,lock_fail,usb_block; +static int uart_error,usb_error,uart_installs,usb_installs; +static const char *uart_rx="", *usb_rx=""; +static char uart_tx[16384],usb_tx[16384]; +static size_t uart_tx_len,usb_tx_len; +static int saves,notifies; +static char saved_id[64],saved_sn[64]; +static TickType_t xTaskGetTickCount(void) {return ticks;} +#if CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE +static void vTaskDelay(TickType_t delay) {ticks+=delay;} +#endif +static SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t *p) {return p;} +static int xSemaphoreTake(SemaphoreHandle_t p, TickType_t wait) {assert(p && wait<=100);return !lock_fail;} +static int xSemaphoreGive(SemaphoreHandle_t p) {assert(p);return 1;} +static const char *esp_err_to_name(int err) {(void)err;return "fake";} +static bool uart_is_driver_installed(int port) {assert(port==0);return uart_ready;} +static int uart_driver_install(int port,int rx,int tx,int q,void *handle,int flags) { + assert(port==0 && rx==2048 && tx==0 && q==0 && !handle && !flags); + ++uart_installs;uart_ready=!uart_error;return uart_error; +} +static int uart_read_bytes(int port,void *buf,size_t len,TickType_t wait) { + assert(port==0 && len>0);(void)wait; + if(!*uart_rx)return 0; *(char *)buf=*uart_rx++;return 1; +} +static int uart_write_bytes(int port,const void *buf,size_t len) { + assert(port==0 && uart_tx_len+lenrx_buffer_size==2048 && p->tx_buffer_size==1024); + ++usb_installs;usb_ready=!usb_error;return usb_error; +} +static int usb_serial_jtag_read_bytes(void *buf,uint32_t len,TickType_t wait) { + assert(len>0);(void)wait; + if(!*usb_rx)return 0;*(char *)buf=*usb_rx++;return 1; +} +static int usb_serial_jtag_write_bytes(const void *buf,size_t len,TickType_t wait) { + assert(wait==0 && len<=64);if(usb_block)return 0; + assert(usb_tx_len+len