feat(factory): support USB CDC alongside UART CLI

This commit is contained in:
chenjintao
2026-10-27 15:04:14 +08:00
parent 6116a67d5f
commit 002967b21f
13 changed files with 583 additions and 69 deletions
@@ -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 <reason>`, 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();