feat(factory): support USB CDC alongside UART CLI
This commit is contained in:
@@ -4,8 +4,8 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*
|
*
|
||||||
* Factory provisioning serial CLI. Owns command line parsing, provisioning
|
* Factory provisioning serial CLI. Owns command line parsing, provisioning
|
||||||
* command handlers, and UART replies in the control-plane layer. Reads
|
* command handlers, and per-port replies in the control-plane layer. Reads
|
||||||
* `\r\n`-delimited commands from the console UART through the domain console
|
* `\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.
|
* bridge and replies with `OK`, `ERROR <reason>`, or one JSON line.
|
||||||
*
|
*
|
||||||
* 工厂串口 CLI 拥有行缓冲、命令解析和应答格式,但设备身份/Wi-Fi 的持久化仍经 domain 完成。
|
* 工厂串口 CLI 拥有行缓冲、命令解析和应答格式,但设备身份/Wi-Fi 的持久化仍经 domain 完成。
|
||||||
@@ -36,7 +36,8 @@
|
|||||||
#define FACTORY_CLI_SERIAL_MAX_LEN 64
|
#define FACTORY_CLI_SERIAL_MAX_LEN 64
|
||||||
#define FACTORY_CLI_JSON_MAX_LEN 640
|
#define FACTORY_CLI_JSON_MAX_LEN 640
|
||||||
#define FACTORY_CLI_WIFI_JSON_MAX_LEN 3072
|
#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
|
#define FACTORY_CLI_RESET_REPLY_DELAY_MS 100
|
||||||
#ifndef CONFIG_TQ_FACTORY_CLI_TASK_STACK_BYTES
|
#ifndef CONFIG_TQ_FACTORY_CLI_TASK_STACK_BYTES
|
||||||
#define CONFIG_TQ_FACTORY_CLI_TASK_STACK_BYTES (7U * 1024U)
|
#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;
|
static portMUX_TYPE s_worker_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||||
|
|
||||||
/* Touched only by the single worker task; no extra lock required. */
|
/* Touched only by the single worker task; no extra lock required. */
|
||||||
static bool s_factory_mode_enabled;
|
typedef struct {
|
||||||
static char s_pending_device_id[FACTORY_CLI_DEVICE_ID_MAX_LEN];
|
char line[FACTORY_CLI_MAX_LINE_LEN];
|
||||||
static char s_pending_serial_number[FACTORY_CLI_SERIAL_MAX_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)
|
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)
|
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)
|
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};
|
char json[FACTORY_CLI_JSON_MAX_LEN] = {0};
|
||||||
snprintf(json, sizeof(json),
|
snprintf(json, sizeof(json),
|
||||||
"{\"factory_mode\":%s,\"device_id\":\"%s\",\"serial_number\":\"%s\"}",
|
"{\"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,
|
device_id,
|
||||||
serial_number);
|
serial_number);
|
||||||
cli_write_line(json);
|
cli_write_line(json);
|
||||||
@@ -457,8 +475,8 @@ static void handle_save_config(void)
|
|||||||
domain_runtime_read_device_identity(current_id, sizeof(current_id),
|
domain_runtime_read_device_identity(current_id, sizeof(current_id),
|
||||||
current_sn, sizeof(current_sn));
|
current_sn, sizeof(current_sn));
|
||||||
|
|
||||||
const char *id_source = s_pending_device_id[0] != '\0' ? s_pending_device_id : current_id;
|
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_pending_serial_number[0] != '\0' ? s_pending_serial_number : current_sn;
|
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)) {
|
if (is_text_empty(id_source) || is_text_empty(sn_source)) {
|
||||||
cli_write_error("identity_not_ready");
|
cli_write_error("identity_not_ready");
|
||||||
return;
|
return;
|
||||||
@@ -481,8 +499,8 @@ static void handle_save_config(void)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_pending_device_id[0] = '\0';
|
s_current_session->pending_device_id[0] = '\0';
|
||||||
s_pending_serial_number[0] = '\0';
|
s_current_session->pending_serial_number[0] = '\0';
|
||||||
/* Force a fresh challenge/response against the new identity on next use. */
|
/* Force a fresh challenge/response against the new identity on next use. */
|
||||||
control_plane_multimodal_device_auth_invalidate(0);
|
control_plane_multimodal_device_auth_invalidate(0);
|
||||||
/* 仅发布配置更新;Wi-Fi worker 在自己的上下文恢复配网,不阻塞串口应答。 */
|
/* 仅发布配置更新;Wi-Fi worker 在自己的上下文恢复配网,不阻塞串口应答。 */
|
||||||
@@ -597,7 +615,7 @@ static void handle_command_line(char *line)
|
|||||||
const char *cmd = argv[0];
|
const char *cmd = argv[0];
|
||||||
|
|
||||||
if (strcmp(cmd, "factory_mode") == 0) {
|
if (strcmp(cmd, "factory_mode") == 0) {
|
||||||
s_factory_mode_enabled = true;
|
s_current_session->factory_mode_enabled = true;
|
||||||
cli_write_ok();
|
cli_write_ok();
|
||||||
} else if (strcmp(cmd, "get_device_info") == 0) {
|
} else if (strcmp(cmd, "get_device_info") == 0) {
|
||||||
handle_get_device_info();
|
handle_get_device_info();
|
||||||
@@ -606,9 +624,9 @@ static void handle_command_line(char *line)
|
|||||||
cli_write_error("missing_device_id");
|
cli_write_error("missing_device_id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
strlcpy(s_pending_device_id, argv[1], sizeof(s_pending_device_id));
|
strlcpy(s_current_session->pending_device_id, argv[1], sizeof(s_current_session->pending_device_id));
|
||||||
trim_text(s_pending_device_id);
|
trim_text(s_current_session->pending_device_id);
|
||||||
if (s_pending_device_id[0] == '\0') {
|
if (s_current_session->pending_device_id[0] == '\0') {
|
||||||
cli_write_error("invalid_device_id");
|
cli_write_error("invalid_device_id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -618,9 +636,9 @@ static void handle_command_line(char *line)
|
|||||||
cli_write_error("missing_serial_number");
|
cli_write_error("missing_serial_number");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
strlcpy(s_pending_serial_number, argv[1], sizeof(s_pending_serial_number));
|
strlcpy(s_current_session->pending_serial_number, argv[1], sizeof(s_current_session->pending_serial_number));
|
||||||
trim_text(s_pending_serial_number);
|
trim_text(s_current_session->pending_serial_number);
|
||||||
if (s_pending_serial_number[0] == '\0') {
|
if (s_current_session->pending_serial_number[0] == '\0') {
|
||||||
cli_write_error("invalid_serial_number");
|
cli_write_error("invalid_serial_number");
|
||||||
return;
|
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)
|
static void factory_cli_worker(void *arg)
|
||||||
{
|
{
|
||||||
(void)arg;
|
(void)arg;
|
||||||
@@ -696,34 +761,20 @@ static void factory_cli_worker(void *arg)
|
|||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "Factory serial CLI ready on console UART");
|
ESP_LOGI(TAG, "Factory serial CLI ready (UART0 and enabled USB Serial/JTAG CDC)");
|
||||||
|
memset(&s_uart_session, 0, sizeof(s_uart_session));
|
||||||
char line[FACTORY_CLI_MAX_LINE_LEN];
|
memset(&s_cdc_session, 0, sizeof(s_cdc_session));
|
||||||
size_t line_len = 0;
|
s_current_session = &s_uart_session;
|
||||||
|
s_reply_over_cdc = false;
|
||||||
|
|
||||||
while (!factory_cli_get_stop_requested()) {
|
while (!factory_cli_get_stop_requested()) {
|
||||||
uint8_t ch = 0;
|
factory_cli_poll_input(&s_uart_session, false);
|
||||||
int got = domain_runtime_console_read_bytes(&ch, 1, FACTORY_CLI_READ_TIMEOUT_MS);
|
if (factory_cli_get_stop_requested()) {
|
||||||
if (got <= 0) {
|
break;
|
||||||
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_cdc_session, true);
|
||||||
|
/* 驱动读取均为非阻塞;空闲时至少让出一个tick,不创建第二个CLI任务。 */
|
||||||
|
vTaskDelay(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory_cli_finish_worker_exit();
|
factory_cli_finish_worker_exit();
|
||||||
|
|||||||
@@ -613,12 +613,20 @@ void domain_runtime_restart(void);
|
|||||||
*
|
*
|
||||||
* `read_bytes` returns bytes read (0 on timeout, <0 on error). A zero timeout
|
* `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
|
* 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);
|
esp_err_t domain_runtime_console_install(void);
|
||||||
int domain_runtime_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms);
|
int domain_runtime_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms);
|
||||||
void domain_runtime_console_write_line(const char *line);
|
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);
|
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(domain_wifi_prompt_id_t prompt);
|
||||||
esp_err_t domain_runtime_behavior_play_prompt_file(const char *path);
|
esp_err_t domain_runtime_behavior_play_prompt_file(const char *path);
|
||||||
|
|||||||
@@ -1213,6 +1213,16 @@ void domain_runtime_console_write_line(const char *line)
|
|||||||
platform_console_write_line(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,
|
esp_err_t domain_runtime_stepper_rotate_angle_with_accel_timeout(float angle,
|
||||||
int target_delay_us,
|
int target_delay_us,
|
||||||
int timeout_ms)
|
int timeout_ms)
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ set(platform_priv_requires
|
|||||||
nvs_flash
|
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)
|
if(CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE)
|
||||||
list(APPEND platform_priv_requires lwip)
|
list(APPEND platform_priv_requires lwip)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -243,7 +243,8 @@ esp_err_t platform_write_device_identity(const char *device_id, const char *seri
|
|||||||
* @brief 工厂 CLI 使用的串口行传输原语。
|
* @brief 工厂 CLI 使用的串口行传输原语。
|
||||||
* Console (UART) line-transport primitives for the factory 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
|
* Stateless primitives over the console UART. `platform_console_install()` is
|
||||||
* idempotent (a pre-existing console driver is treated as success).
|
* 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);
|
int platform_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms);
|
||||||
void platform_console_write_line(const char *line);
|
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 的通用持久化键值存储原语。
|
* @brief 基于 NVS 的通用持久化键值存储原语。
|
||||||
* Generic persistent key/value blob storage (NVS-backed).
|
* Generic persistent key/value blob storage (NVS-backed).
|
||||||
|
|||||||
@@ -3,15 +3,24 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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.
|
* accumulation and the worker loop live in the control-plane CLI owner.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
#include "driver/uart.h"
|
#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 "esp_log.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
@@ -22,30 +31,67 @@
|
|||||||
|
|
||||||
static const char *TAG = "platform_console";
|
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 platform_console_install(void)
|
||||||
{
|
{
|
||||||
esp_err_t err = uart_driver_install(PLATFORM_CONSOLE_UART_PORT,
|
portENTER_CRITICAL(&s_install_lock_guard);
|
||||||
PLATFORM_CONSOLE_RX_BUFFER,
|
if (s_install_lock == NULL) {
|
||||||
0,
|
s_install_lock = xSemaphoreCreateMutexStatic(&s_install_lock_buffer);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
if (err != ESP_OK) {
|
portEXIT_CRITICAL(&s_install_lock_guard);
|
||||||
ESP_LOGE(TAG, "Failed to install UART%d driver: %s",
|
if (s_install_lock == NULL ||
|
||||||
(int)PLATFORM_CONSOLE_UART_PORT, esp_err_to_name(err));
|
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)
|
int platform_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms)
|
||||||
{
|
{
|
||||||
/* 0 表示单次轮询,负数是无效参数;正数转换为 FreeRTOS 有界等待。 */
|
/* 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;
|
return -1;
|
||||||
}
|
}
|
||||||
TickType_t ticks = pdMS_TO_TICKS(timeout_ms);
|
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)
|
void platform_console_write_line(const char *line)
|
||||||
{
|
{
|
||||||
/* 工厂协议按行发送,统一补充 CRLF,避免调用方重复处理行结束符。 */
|
/* 工厂协议按行发送,统一补充 CRLF,避免调用方重复处理行结束符。 */
|
||||||
if (line == NULL) {
|
if (line == NULL || !uart_is_driver_installed(PLATFORM_CONSOLE_UART_PORT)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
uart_write_bytes(PLATFORM_CONSOLE_UART_PORT, line, strlen(line));
|
uart_write_bytes(PLATFORM_CONSOLE_UART_PORT, line, strlen(line));
|
||||||
uart_write_bytes(PLATFORM_CONSOLE_UART_PORT, "\r\n", 2);
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
Config/default consistency and enabled-pin uniqueness checks passed.
|
||||||
No device was flashed; touch polarity, ADC2 sampling with Wi-Fi, charger/wake,
|
No device was flashed; touch polarity, ADC2 sampling with Wi-Fi, charger/wake,
|
||||||
motor direction and concurrent operation still require hardware validation.
|
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).
|
||||||
|
|||||||
@@ -130,7 +130,10 @@ CONFIG_TQ_FACTORY_CLI_ENABLE=y
|
|||||||
control_plane_factory_cli_start();
|
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 |
|
| 最大命令行长度 | 512 B |
|
||||||
| 最大参数数量 | 8 |
|
| 最大参数数量 | 8 |
|
||||||
| 串口读取超时 | 100 ms |
|
| 接收调度 | 两路非阻塞读取,每路每轮最多64字节,每轮让出1 tick |
|
||||||
| 任务栈 | 12 KiB |
|
| 任务栈 | 默认7 KiB(`CONFIG_TQ_FACTORY_CLI_TASK_STACK_BYTES`) |
|
||||||
| 任务优先级 | 4 |
|
| 任务优先级 | 4 |
|
||||||
| 运行核心 | `CONFIG_TQ_LOCAL_TASK_CORE` |
|
| 运行核心 | `CONFIG_TQ_LOCAL_TASK_CORE` |
|
||||||
|
|
||||||
@@ -186,8 +189,8 @@ get_device_info
|
|||||||
`set_device_id` 和 `set_serial_number` 只将值暂存在 RAM:
|
`set_device_id` 和 `set_serial_number` 只将值暂存在 RAM:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
s_pending_device_id
|
s_current_session->pending_device_id
|
||||||
s_pending_serial_number
|
s_current_session->pending_serial_number
|
||||||
```
|
```
|
||||||
|
|
||||||
只有执行 `save_config` 后,才会调用:
|
只有执行 `save_config` 后,才会调用:
|
||||||
|
|||||||
@@ -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 <reason>`。
|
||||||
|
`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枚举及实机收发测试。
|
||||||
@@ -1380,6 +1380,16 @@ menu "TQ Cat Configuration"
|
|||||||
device identity provisioning, public-key retrieval, and
|
device identity provisioning, public-key retrieval, and
|
||||||
on-device challenge signing.
|
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
|
config TQ_FACTORY_CLI_TASK_STACK_BYTES
|
||||||
int "Factory CLI task stack (bytes)"
|
int "Factory CLI task stack (bytes)"
|
||||||
default 7168
|
default 7168
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ CONFIG_TQ_MULTIMODAL_WATCHDOG_TASK_STACK_BYTES=6144
|
|||||||
# CONFIG_MULTIMODAL_AUTH_BYPASS_TOKEN_SERVICE is not set
|
# CONFIG_MULTIMODAL_AUTH_BYPASS_TOKEN_SERVICE is not set
|
||||||
CONFIG_AUTH_V2_ENABLE=y
|
CONFIG_AUTH_V2_ENABLE=y
|
||||||
CONFIG_TQ_FACTORY_CLI_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_TQ_FACTORY_CLI_TASK_STACK_BYTES=7168
|
||||||
CONFIG_MULTIMODAL_UPSTREAM_MODE="push2talk"
|
CONFIG_MULTIMODAL_UPSTREAM_MODE="push2talk"
|
||||||
CONFIG_MULTIMODAL_PTT_BUTTON_GPIO=13
|
CONFIG_MULTIMODAL_PTT_BUTTON_GPIO=13
|
||||||
|
|||||||
@@ -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 <assert.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#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+len<sizeof(uart_tx));
|
||||||
|
memcpy(uart_tx+uart_tx_len,buf,len);uart_tx_len+=len;uart_tx[uart_tx_len]=0;return (int)len;
|
||||||
|
}
|
||||||
|
#if CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE
|
||||||
|
static void usb_serial_jtag_vfs_use_driver(void) {}
|
||||||
|
static bool usb_serial_jtag_is_driver_installed(void) {return usb_ready;}
|
||||||
|
static int usb_serial_jtag_driver_install(usb_serial_jtag_driver_config_t *p) {
|
||||||
|
assert(p->rx_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<sizeof(usb_tx));memcpy(usb_tx+usb_tx_len,buf,len);
|
||||||
|
usb_tx_len+=len;usb_tx[usb_tx_len]=0;return (int)len;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define strlcpy test_strlcpy
|
||||||
|
static size_t test_strlcpy(char *dst,const char *src,size_t cap) {
|
||||||
|
size_t n=strlen(src);if(cap){size_t c=n<cap-1?n:cap-1;memcpy(dst,src,c);dst[c]=0;}return n;
|
||||||
|
}
|
||||||
|
static void domain_runtime_read_device_identity(char *id,size_t ilen,char *sn,size_t slen) {
|
||||||
|
strlcpy(id,saved_id,ilen);strlcpy(sn,saved_sn,slen);
|
||||||
|
}
|
||||||
|
static int domain_runtime_write_device_identity(const char *id,const char *sn) {
|
||||||
|
++saves;strlcpy(saved_id,id,sizeof(saved_id));strlcpy(saved_sn,sn,sizeof(saved_sn));return ESP_OK;
|
||||||
|
}
|
||||||
|
static void control_plane_provisioning_configuration_changed(void) {++notifies;}
|
||||||
|
static void control_plane_multimodal_device_auth_invalidate(int reason) {(void)reason;}
|
||||||
|
static int domain_runtime_request_expression(const domain_expression_request_t *p) {(void)p;return ESP_OK;}
|
||||||
|
static void handle_get_wifi(void) {}
|
||||||
|
static void handle_set_wifi(size_t n,char **a) {(void)n;(void)a;}
|
||||||
|
static void handle_reset_wifi(void) {}
|
||||||
|
static void handle_get_auth_material(void) {}
|
||||||
|
static void handle_sign_challenge(const char *a,const char *b) {(void)a;(void)b;}
|
||||||
|
static void handle_prepare_rotate(uint32_t v) {(void)v;}
|
||||||
|
static void handle_lcd_test(void) {}
|
||||||
|
'''
|
||||||
|
|
||||||
|
BRIDGE = r'''
|
||||||
|
static int domain_runtime_console_read_bytes(uint8_t *buf,size_t len,int ms) {return platform_console_read_bytes(buf,len,ms);}
|
||||||
|
static int domain_runtime_console_read_cdc_bytes(uint8_t *buf,size_t len,int ms) {return platform_console_read_cdc_bytes(buf,len,ms);}
|
||||||
|
static void domain_runtime_console_write_line(const char *p) {platform_console_write_line(p);}
|
||||||
|
static void domain_runtime_console_write_cdc_line(const char *p) {platform_console_write_cdc_line(p);}
|
||||||
|
'''
|
||||||
|
|
||||||
|
TESTS = r'''
|
||||||
|
static void pump(void) {
|
||||||
|
for(int i=0;i<100;++i) {factory_cli_poll_input(&s_uart_session,false);factory_cli_poll_input(&s_cdc_session,true);}
|
||||||
|
}
|
||||||
|
static void clear_outputs(void) {uart_tx_len=usb_tx_len=0;uart_tx[0]=usb_tx[0]=0;}
|
||||||
|
int main(void) {
|
||||||
|
assert(platform_console_install()==ESP_OK);
|
||||||
|
assert(platform_console_install()==ESP_OK && uart_installs==1);
|
||||||
|
#if CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE
|
||||||
|
assert(usb_installs==1);
|
||||||
|
usb_ready=false;usb_error=ESP_FAIL;
|
||||||
|
assert(platform_console_install()==ESP_OK && uart_ready); // UART fallback
|
||||||
|
usb_error=0;uart_ready=false;uart_error=ESP_FAIL;
|
||||||
|
assert(platform_console_install()==ESP_OK && usb_ready); // CDC fallback
|
||||||
|
uart_error=0;assert(platform_console_install()==ESP_OK);
|
||||||
|
usb_ready=uart_ready=false;usb_error=uart_error=ESP_FAIL;
|
||||||
|
assert(platform_console_install()!=ESP_OK);
|
||||||
|
usb_error=uart_error=0;assert(platform_console_install()==ESP_OK);
|
||||||
|
#endif
|
||||||
|
lock_fail=true;assert(platform_console_install()==ESP_ERR_TIMEOUT);lock_fail=false;
|
||||||
|
uint8_t ch;
|
||||||
|
assert(platform_console_read_bytes(NULL,1,0)<0);
|
||||||
|
assert(platform_console_read_bytes(&ch,0,0)<0);
|
||||||
|
assert(platform_console_read_bytes(&ch,1,-1)<0);
|
||||||
|
uart_rx="get_device_info\r\n";pump();
|
||||||
|
assert(strstr(uart_tx,"device_id") && strstr(uart_tx,"OK\r\n"));
|
||||||
|
assert(usb_tx_len==0);
|
||||||
|
#if CONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE
|
||||||
|
clear_outputs();usb_rx="get_device_info\n";pump();
|
||||||
|
assert(strstr(usb_tx,"device_id") && strstr(usb_tx,"OK\r\n") && uart_tx_len==0);
|
||||||
|
assert(platform_console_read_cdc_bytes(NULL,1,0)<0);
|
||||||
|
assert(platform_console_read_cdc_bytes(&ch,1,-1)<0);
|
||||||
|
// Partial lines do not cross ports, and pending identities are isolated too.
|
||||||
|
clear_outputs();uart_rx="set_device_id uart-id";pump();
|
||||||
|
usb_rx="set_serial_number cdc-sn\nsave_config\n";pump();
|
||||||
|
assert(saves==0 && strstr(usb_tx,"ERROR"));
|
||||||
|
uart_rx="\nset_serial_number uart-sn\nsave_config\n";pump();
|
||||||
|
assert(saves==1 && notifies==1 && !strcmp(saved_id,"uart-id") && !strcmp(saved_sn,"uart-sn"));
|
||||||
|
// A saturated/disconnected USB writer has a fixed budget; UART remains usable.
|
||||||
|
clear_outputs();usb_block=true;TickType_t start=ticks;
|
||||||
|
platform_console_write_cdc_line("reply");
|
||||||
|
assert(ticks-start==250 && !usb_tx_len);
|
||||||
|
usb_block=false;uart_rx="get_device_info\n";pump();assert(strstr(uart_tx,"OK"));
|
||||||
|
#else
|
||||||
|
assert(platform_console_read_cdc_bytes(&ch,1,0)<0);
|
||||||
|
platform_console_write_cdc_line("disabled");assert(!usb_tx_len);
|
||||||
|
#endif
|
||||||
|
// Oversize input must discard the whole line, including any valid-looking tail.
|
||||||
|
char oversized[700];memset(oversized,'x',600);strcpy(oversized+600,"save_config\n");
|
||||||
|
int before=saves;uart_rx=oversized;pump();assert(saves==before);
|
||||||
|
// A delayed partial command is discarded, not joined to a later command.
|
||||||
|
uart_rx="save_";pump();ticks+=2000;pump();uart_rx="config\n";pump();assert(saves==before);
|
||||||
|
clear_outputs();uart_rx="get_device_info\n";pump();assert(strstr(uart_tx,"OK"));
|
||||||
|
puts("PASS: actual driver/CLI init, UART/CDC routing, session isolation, bounded USB writes, overflow and partial-line recovery");
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
driver = (ROOT / 'components/platform/src/platform_console_driver.c').read_text()
|
||||||
|
driver = re.sub(r'^#include .*$', '', driver, flags=re.M)
|
||||||
|
cli = (ROOT / 'components/control_plane/src/runtime/control_plane_factory_cli.c').read_text()
|
||||||
|
session = re.search(r'typedef struct \{[^}]+\} factory_cli_session_t;', cli).group()
|
||||||
|
state = session + '\nstatic factory_cli_session_t s_uart_session, s_cdc_session;\n' \
|
||||||
|
'static factory_cli_session_t *s_current_session=&s_uart_session;\nstatic bool s_reply_over_cdc;\n'
|
||||||
|
names = ['is_text_empty', 'trim_text', 'split_args', 'cli_write_line', 'cli_write_ok',
|
||||||
|
'cli_write_error', 'handle_get_device_info', 'handle_save_config',
|
||||||
|
'handle_command_line', 'factory_cli_poll_input']
|
||||||
|
actual = '\n'.join(extract(cli, n) for n in names)
|
||||||
|
with tempfile.TemporaryDirectory(prefix='factory-cdc-') as tmp:
|
||||||
|
path = Path(tmp)
|
||||||
|
cfile = path / 'test.c'
|
||||||
|
cfile.write_text(STUBS + driver + BRIDGE + state + actual + TESTS)
|
||||||
|
for enabled in (0, 1):
|
||||||
|
binary = path / f'test-{enabled}'
|
||||||
|
subprocess.run(['clang', '-std=c11', '-Wall', '-Wextra', '-Werror',
|
||||||
|
'-Wno-unused-variable', '-fsanitize=address,undefined',
|
||||||
|
f'-DCONFIG_TQ_FACTORY_CLI_USB_CDC_ENABLE={enabled}',
|
||||||
|
str(cfile), '-o', str(binary)], check=True)
|
||||||
|
subprocess.run([str(binary)], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -103,7 +103,10 @@ static QueueHandle_t s_provisioning_queue;
|
|||||||
static EventGroupHandle_t s_provisioning_events;
|
static EventGroupHandle_t s_provisioning_events;
|
||||||
static domain_wifi_provisioning_event_callback_t s_event_callback;
|
static domain_wifi_provisioning_event_callback_t s_event_callback;
|
||||||
static void *s_event_user_data;
|
static void *s_event_user_data;
|
||||||
static char s_pending_device_id[64], s_pending_serial_number[64];
|
static struct { char pending_device_id[64], pending_serial_number[64]; } test_session;
|
||||||
|
static __typeof__(test_session) *s_current_session = &test_session;
|
||||||
|
#define s_pending_device_id test_session.pending_device_id
|
||||||
|
#define s_pending_serial_number test_session.pending_serial_number
|
||||||
static uint64_t esp_timer_get_time(void) {return now_ms*1000;}
|
static uint64_t esp_timer_get_time(void) {return now_ms*1000;}
|
||||||
static void vTaskDelay(unsigned ms) {now_ms+=ms;}
|
static void vTaskDelay(unsigned ms) {now_ms+=ms;}
|
||||||
static void domain_runtime_restart(void) {++restarts;}
|
static void domain_runtime_restart(void) {++restarts;}
|
||||||
|
|||||||
Reference in New Issue
Block a user