docs: 补充核心分层中文注释

This commit is contained in:
chenjintao
2026-09-29 18:18:44 +08:00
parent a77471a7b1
commit 9fd01bb61b
81 changed files with 1189 additions and 112 deletions
@@ -5,6 +5,8 @@
*/
#include "control_plane_internal.h"
/* 控制面只转发校准用例;采样、步进电机互斥和校准值落盘由 domain 层统一负责。 */
esp_err_t control_plane_direction_calibrate_internal(
const domain_direction_calibration_t *config,
int timeout_ms)
@@ -9,6 +9,11 @@
#include <ctype.h>
#include <string.h>
/*
* 方向命令的纯解析器:不访问设备状态,也不产生旋转副作用。
* 先裁掉首尾空白再比较,是为了兼容串口、模型文本和 HTTP 等不同来源的换行习惯;
* 返回值只表达意图,真正的索引校验和执行仍由方向服务完成。
*/
static void trimmed_span(const char *input, const char **start_out, size_t *len_out)
{
const char *start = input;
@@ -7,8 +7,15 @@
#include "control_plane.h"
#include "control_plane_internal.h"
/* 人机交互入口使用统一上限,避免调用方遗漏超时而长期占用 domain 的旋转互斥锁。 */
#define CONTROL_PLANE_ROTATE_TIMEOUT_MS 8000
/*
* 方向用例门面:完成初始化门禁、文本命令解释和统一超时,几何换算与硬件动作下沉到 domain。
* 该层不缓存当前角度,domain 才是方向状态的唯一事实来源。
*/
static bool control_plane_is_initialized(void)
{
return domain_is_initialized();
@@ -12,6 +12,10 @@
#include "cJSON.h"
#include "esp_log.h"
/*
* 模型文本到本地命令的防御性解析边界。只识别白名单方向/抽签结果,并允许命令藏在普通文本
* 或 JSON 字段中;解析成功仍只产生命令值,不在网络回调线程里直接驱动电机或 LCD。
*/
static const char *TAG = "cp_model_cmd";
static bool is_supported_model_direction(const char *value)
@@ -18,6 +18,12 @@
#include "esp_log.h"
#include "domain.h"
#include "sdkconfig.h"
/*
* 多模态会话凭据装配器:优先使用 Device-JWT,新协议不可用时才按配置回落到旧令牌或静态凭据。
* HTTP 响应先完整收集并限制为 4 KiB,再解析到定长结构,防止远端响应把堆占满。
* 返回的 auth_info 为调用方值拷贝;本模块不把 cJSON 节点或响应缓冲区借给会话线程。
*/
#include "soc/soc_caps.h"
#ifndef CONFIG_MULTIMODAL_DEVICE_ID
@@ -70,6 +76,8 @@
static const char *TAG = "cp_mm_auth";
#if !CONTROL_PLANE_MULTIMODAL_AUTH_USE_STATIC_CREDENTIALS
static const int kMaxHttpBody = 4096;
/* 认证响应字段很少,4 KiB 足以容纳协议并为异常大响应提供明确拒绝边界。 */
#endif
#if !CONTROL_PLANE_MULTIMODAL_AUTH_USE_STATIC_CREDENTIALS
@@ -7,6 +7,10 @@
* delegating persistence to the domain auth persistence port. HTTPS transport
* uses the domain/platform outbound transport primitive, and randomness uses
* esp_fill_random/esp_random.
*
* 设备认证(Device-JWT)状态机。它独占 challenge/response、JWT 刷新和指数退避,持久化
* 则通过 domain 端口完成。调用方不应自行缓存私钥或复制刷新逻辑;所有 scope 检查也在这里
* 集中完成,避免语音与 OTA 对“令牌是否可用”得出不同结论。
*/
#include "multimodal/control_plane_multimodal_device_auth.h"
@@ -102,6 +106,11 @@ typedef control_plane_device_auth_result_t auth_result_t;
#define AUTH_LOCK_TIMEOUT_MS 2000
#define AUTH_LOCK_POLL_MS 10
/*
* 锁等待采用短轮询并有 2 秒总上限:认证包含网络 I/O,必须允许停止请求被观察到,且不能让
* 第二个消费者永久卡住。退避上限 60 秒用于保护服务端,抖动用于避免批量设备同步重试。
*/
#define AUTH_DEVICE_ID_MAX_LEN 64
#define AUTH_SERIAL_NUMBER_MAX_LEN 64
@@ -224,6 +233,7 @@ static void *auth_psram_first_calloc(size_t count, size_t size)
static bool auth_lock_acquire(const char *owner)
{
/* 不持锁睡满总超时,而是短轮询;这样日志能标明竞争者,停止/看门狗也不会被长临界区饿死。 */
TickType_t start = xTaskGetTickCount();
TickType_t timeout = pdMS_TO_TICKS(AUTH_LOCK_TIMEOUT_MS);
TickType_t poll = pdMS_TO_TICKS(AUTH_LOCK_POLL_MS);
@@ -832,6 +842,7 @@ static esp_err_t key_store_save_locked(void)
static esp_err_t key_store_load_or_create_locked(void)
{
/* 私钥只在首次缺失时生成并立即持久化;后续公钥/JKT 都从同一私钥派生,保证设备身份稳定。 */
if (s_auth.key_loaded) {
return ESP_OK;
}
@@ -911,6 +922,7 @@ static esp_err_t save_state_to_store_locked(void)
static esp_err_t parse_jwt_payload(const char *jwt, uint64_t *out_exp_epoch_s, uint32_t *out_scope_mask)
{
/* 此处只读取受签发流程保护的声明用于本地门禁,不把“解码成功”等同于独立验证任意 JWT。 */
if (!jwt || !out_exp_epoch_s || !out_scope_mask) {
return ESP_ERR_INVALID_ARG;
}
@@ -1062,6 +1074,7 @@ static bool jwt_scope_satisfied(uint32_t required_scope_mask)
static bool jwt_still_valid_locked(uint32_t required_scope_mask, bool force_refresh)
{
/* 在真正过期前预留刷新余量,覆盖一次网络请求和时钟小幅偏差,避免会话中途失效。 */
if (force_refresh) {
return false;
}
@@ -1845,6 +1858,7 @@ static auth_result_t refresh_jwt_internal(uint32_t required_scope_mask)
static void apply_retry_backoff_locked(void)
{
/* 失败计数与下次允许时间一起写入状态,所有消费者共享同一退避窗口。 */
if (s_auth.rt.fail_count < UINT32_MAX) {
s_auth.rt.fail_count++;
}
@@ -1855,6 +1869,7 @@ static void apply_retry_backoff_locked(void)
esp_err_t control_plane_multimodal_device_auth_init(void)
{
/* 初始化只恢复/创建本地材料;是否访问服务端由后续按 scope 获取 JWT 的路径决定。 */
if (!auth_lock_acquire("component_init")) {
return ESP_ERR_TIMEOUT;
}
@@ -9,6 +9,12 @@
#include "freertos/task.h"
#include "sdkconfig.h"
/*
* 本地唤醒后台任务:持续消费 domain 本地识别事件,在“云端前台会话未占用音频”时接管采集。
* capture_owned 明确记录采集资源所有权;暂停/恢复接口供 PTT、OTA 等前台流程让渡资源。
* 检测回调只入队,提示音和会话启动在任务上下文执行,避免阻塞识别器回调线程。
*/
#ifndef CONFIG_TQ_LOCAL_WAKE_ENABLE
#define CONFIG_TQ_LOCAL_WAKE_ENABLE 0
#endif
@@ -48,6 +54,8 @@
#define CONTROL_PLANE_LOCAL_WAKE_QUEUE_LEN 4U
#define CONTROL_PLANE_LOCAL_WAKE_POLL_MS 100U
/* 队列只保留少量瞬时唤醒意图;处理不过来时丢弃比堆积过期命令更符合交互语义。 */
#if CONFIG_TQ_LOCAL_WAKE_TASK_CORE < 0
#define CONTROL_PLANE_LOCAL_WAKE_TASK_CORE tskNO_AFFINITY
#else
@@ -13,6 +13,10 @@
#include "esp_random.h"
#include "sdkconfig.h"
/*
* 多模态协议的无状态辅助层:规范化上行模式、构造协议 JSON,并生成一次性 UUID。
* 这里不持有 WebSocket 或音频生命周期,因此结果要么是常量,要么写入调用方提供的缓冲区。
*/
static const char *TAG = "cp_mm_proto";
const char *control_plane_multimodal_resolve_upstream_mode(void)
@@ -16,6 +16,12 @@
#include "esp_log.h"
#include "sdkconfig.h"
/*
* 多模态运行时监督器:负责心跳、无响应检测、下行播放停滞恢复以及空闲连接回收。
* 它观察 session_service 的状态但不拥有会话对象;所有关闭动作都走统一生命周期 API,
* 避免 watchdog 与用户停止路径同时释放传输/编解码资源。
*/
#ifndef CONFIG_MULTIMODAL_HEARTBEAT_INTERVAL_MS
#define CONFIG_MULTIMODAL_HEARTBEAT_INTERVAL_MS 50000
#endif
@@ -43,6 +49,11 @@
#define CONTROL_PLANE_DOWNSTREAM_PLAYBACK_GUARD_MS 220
#define CONTROL_PLANE_DOWNSTREAM_STALL_STOP_GRACE_MS 2000
#define CONTROL_PLANE_IDLE_TRANSPORT_SHUTDOWN_MS 30000
/*
* 50 秒心跳低于常见代理空闲回收窗口;60 秒无响应用于判定半开连接。220 ms 保护下行尾帧,
* 2 秒则给播放器正常收尾机会,超过后才按停滞恢复,避免网络抖动造成频繁强停。
*/
#define CONTROL_PLANE_WATCHDOG_TASK_STACK_SIZE CONFIG_TQ_MULTIMODAL_WATCHDOG_TASK_STACK_BYTES
#define CONTROL_PLANE_SERVER_SUCCESS_PROMPT_FILE "/littlefs/audio_prompts/server_connect_success.opus"
#define CONTROL_PLANE_CLIENT_LIFECYCLE_WAIT_MS 30000U
@@ -460,6 +471,7 @@ static void control_plane_responding_text_bridge(const char *text,
static esp_err_t control_plane_prepare_for_multimodal_start(void)
{
/* 建链前统一检查联网、OTA/配网资源门禁和音频占用,调用点无需各自复制竞态判断。 */
#if CONFIG_MULTIMODAL_AUDIO_PREWARM_OPUS_BEFORE_WS
if (!domain_runtime_multimodal_audio_is_initialized() &&
!domain_runtime_multimodal_audio_is_uplink_running()) {
@@ -588,6 +600,7 @@ static bool control_plane_is_within_downstream_guard(const control_plane_multimo
static esp_err_t control_plane_start_uplink_if_ready(void)
{
/* 只有协议进入 Listening 后才开麦,防止连接成功但服务端尚未接受任务时提前发送 PCM。 */
control_plane_multimodal_runtime_state_t state = {0};
esp_err_t state_err = control_plane_multimodal_get_runtime_state(&state);
if (state_err != ESP_OK) {
@@ -749,6 +762,7 @@ static void stop_watchdog_task(void)
static void multimodal_watchdog_task(void *arg)
{
/* 看门狗只发出恢复意图,实际 stop/start 仍经生命周期锁串行化。 */
(void)arg;
TaskHandle_t self = xTaskGetCurrentTaskHandle();
@@ -889,6 +903,7 @@ static void control_plane_request_server_error_expression(domain_expression_trig
static void control_plane_multimodal_event_bridge(control_plane_multimodal_event_t event,
void *user_data)
{
/* 事件桥同时维护 UI/网络反馈,但不在回调中执行长时重连;重活交给监督任务。 */
(void)user_data;
switch (event) {
@@ -998,6 +1013,7 @@ static void control_plane_multimodal_event_bridge(control_plane_multimodal_event
static esp_err_t control_plane_start_multimodal_client_locked(void)
{
/* client 是 session 与 watchdog 的组合所有者,部分启动失败必须在返回前恢复到全停状态。 */
control_plane_clear_downstream_stall_recovery();
esp_err_t prepare_err = control_plane_prepare_for_multimodal_start();
@@ -21,6 +21,12 @@
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "sdkconfig.h"
/*
* 多模态会话主状态机,拥有 started/starting/stopping、会话 generation、音频上行和本地响应
* 队列。generation 是异步回调的失效令牌:旧连接迟到的事件不能改变新会话。生命周期锁只
* 保护状态迁移,不在持锁期间做网络或音频阻塞调用;资源按启动逆序释放。
*/
#include "soc/soc_caps.h"
#ifndef CONFIG_ENABLE_MULTIMODAL_INTERACTION
@@ -90,6 +96,11 @@
#define CONTROL_PLANE_AUDIO_SEND_FAILURE_THRESHOLD 2U
#define CONTROL_PLANE_MULTIMODAL_LIFECYCLE_WAIT_MS 30000U
/*
* 本地 responding 队列短而有界,避免服务端连续事件耗尽内存;上行就绪最多等 6 秒。
* 100 ms 窗口内连续两次发送失败才升级故障,可滤掉单个音频包的瞬态背压。
*/
#ifndef CONFIG_TQ_LOCAL_TASK_CORE
#define CONFIG_TQ_LOCAL_TASK_CORE 1
#endif
@@ -425,6 +436,7 @@ static void notify_responding_text(const char *text, bool finished)
static void reset_session_state_locked(void)
{
/* 仅清理当前 generation 的协议状态;底层资源释放由 stop 路径按逆序完成。 */
s_task_id[0] = '\0';
s_pending_start_task_id[0] = '\0';
s_dialog_id[0] = '\0';
@@ -619,6 +631,7 @@ static void stop_local_downstream_playback(void)
static void local_responding_event_worker(void *arg)
{
/* 将“本地播放开始/结束”与传输回调解耦,避免在音频回调栈内同步发送 WebSocket 指令。 */
(void)arg;
local_responding_event_msg_t msg = {0};
@@ -1099,6 +1112,7 @@ static esp_err_t send_directive(const char *action,
bool include_dialog_id,
bool realtime)
{
/* JSON 在本函数内构造和销毁;domain transport 必须在返回前复制或消费文本。 */
char task_id[sizeof(s_task_id)] = {0};
char dialog_id[sizeof(s_dialog_id)] = {0};
@@ -1295,6 +1309,7 @@ static void update_dialog_id_from_json(const cJSON *root)
static void process_text_json(const char *json_text)
{
/* 单一入口按 action 更新会话状态;未知字段向前兼容,状态迁移仍要求 task/dialog 标识匹配。 */
if (json_text == NULL || json_text[0] == '\0') {
return;
}
@@ -1628,6 +1643,7 @@ static void process_text_json(const char *json_text)
static void transport_event_bridge(const domain_multimodal_transport_event_t *event,
void *user_data)
{
/* 平台事件可能晚于 stop 到达,先核对 generation/当前 transport,再通知上层观察者。 */
(void)user_data;
if (event == NULL) {
@@ -1818,6 +1834,7 @@ esp_err_t control_plane_multimodal_start(void)
static esp_err_t control_plane_multimodal_start_locked(void)
{
/* 启动按“认证→传输→本地事件 worker”推进;任一步失败都回滚此前已取得的资源。 */
#if !CONFIG_ENABLE_MULTIMODAL_INTERACTION
ESP_LOGW(TAG, "Multimodal interaction is disabled by config");
return ESP_ERR_NOT_SUPPORTED;
@@ -1947,6 +1964,7 @@ esp_err_t control_plane_multimodal_stop(void)
static esp_err_t control_plane_multimodal_stop_locked(void)
{
/* 先阻止新上行,再停播放/worker/传输;该逆序与 start 对称,减少迟到回调访问已释放状态。 */
bool already_stopping = false;
if (lock_state()) {
already_stopping = s_stopping;
@@ -2483,6 +2501,7 @@ esp_err_t control_plane_multimodal_send_heartbeat(void)
esp_err_t control_plane_multimodal_send_audio_realtime(const uint8_t *data, size_t len)
{
/* 单包失败先计入短窗口,达到阈值才宣告 transport 不可用,避免偶发背压拆掉整条会话。 */
if (data == NULL || len == 0U) {
return ESP_ERR_INVALID_ARG;
}
@@ -16,6 +16,12 @@
#if CONFIG_TQ_BATTERY_ENABLE
/*
* 电池后台策略层:domain 负责把原始电压稳定为状态,本任务负责提示音、充满确认和受控关机。
* GPIO ISR 只唤醒/通知,不读取 ADC 或播放音频;所有慢操作都在任务上下文完成。
* 关机一旦进入独立任务即视为不可逆流程,以免普通采样任务重复触发并争用电机/音频资源。
*/
#define BATTERY_WORKER_STACK_BYTES 4096
#define BATTERY_WORKER_PRIORITY 2
#define BATTERY_SUPPRESSED_RETRY_MS 1000
@@ -31,6 +37,11 @@
#define BATTERY_SHUTDOWN_TASK_STACK_BYTES 6144
#define BATTERY_SHUTDOWN_TASK_PRIORITY 5
#define BATTERY_SHUTDOWN_MOTOR_STOP_MS 2000
/*
* 多次确认用于排除负载突变导致的瞬时低压;紧急阈值只需两次以缩短保护延迟。
* 轻待机下采样间隔显著拉长,充电 GPIO 中断仍可及时唤醒状态更新。
*/
#define BATTERY_LOW_PROMPT_FILE "/littlefs/audio_prompts/battery_low_20.opus"
#define BATTERY_FULL_PROMPT_FILE "/littlefs/audio_prompts/charge_complete.opus"
@@ -17,6 +17,12 @@
#include "freertos/task.h"
#include "sdkconfig.h"
/*
* 联网与配网用例编排:先尝试已保存凭据,失败后按配置切换门户或小程序 BLE 配网。
* 连接周期使用同一个递减超时预算,防止每次重试重置计时;成功提示只播放一次。
* BLE worker 通过单元素队列串行化请求,恢复重启标记放在 RTC 内存以跨软重启传递一次。
*/
#ifndef CONFIG_MULTIMODAL_CONNECT_TIMEOUT_MS
#define CONFIG_MULTIMODAL_CONNECT_TIMEOUT_MS 10000
#endif
@@ -66,6 +72,11 @@
#define CONTROL_PLANE_PROVISIONING_RESTART_DELAY_MS 100
#define CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC 0x5451424cU
/*
* 重试基准 300 ms 兼顾快速恢复与射频稳定;配网切换前先停传输并等待运行时静默,
* 是为了释放内部 RAM/DMA 连续块,而不只是检查总剩余堆。
*/
static const char *TAG = "control_plane_rt";
static bool s_wifi_success_prompt_played;
static bool s_provisioning_recovery_boot;
@@ -108,6 +119,7 @@ static void control_plane_request_provisioning_recovery_restart(
static esp_err_t control_plane_check_provisioning_memory_headroom(void)
{
/* BLE 初始化需要连续内部/DMA 块;总空闲堆达标但最大块不足时仍必须拒绝并走恢复启动。 */
domain_ota_memory_headroom_t headroom = {0};
esp_err_t err = domain_runtime_ota_get_memory_headroom(&headroom);
if (err != ESP_OK) {
@@ -216,6 +228,7 @@ static void control_plane_play_wifi_success_prompt(void)
static esp_err_t control_plane_connect_sta_with_feedback(int timeout_ms,
bool play_connecting_prompt)
{
/* 所有候选网络共享调用方预算,并把失败分类反馈给 UI;此函数不保存试连失败的凭据。 */
esp_err_t err = ESP_ERR_TIMEOUT;
TickType_t start_tick = xTaskGetTickCount();
@@ -320,6 +333,7 @@ static void control_plane_provisioning_event_callback(
const domain_wifi_provisioning_event_t *event,
void *user_data)
{
/* BLE 回调只复制凭据到单元素队列;联网验证与 NVS 提交在 worker 中串行完成。 */
(void)user_data;
if (event == NULL) {
return;
@@ -343,6 +357,7 @@ static bool control_plane_provisioning_stop_requested(void)
static void control_plane_provisioning_worker(void *arg)
{
/* 状态通知顺序也是小程序协议的一部分:connecting→got_ip→saving→ready,失败则给明确原因。 */
(void)arg;
bool provisioning_succeeded = false;
TickType_t last_wait_log_tick = xTaskGetTickCount();
@@ -543,6 +558,7 @@ esp_err_t control_plane_stop_mini_program_provisioning(int timeout_ms)
esp_err_t control_plane_enter_mini_program_reprovisioning(const char *reason)
{
/* 先静默语音/网络运行时释放内存;仍不足时写 RTC 标记并软重启进入最小资源配网路径。 */
#if !CONFIG_TQ_MINI_PROGRAM_PROVISIONING_ENABLE
(void)reason;
return ESP_ERR_NOT_SUPPORTED;
@@ -580,6 +596,7 @@ esp_err_t control_plane_bootstrap_connectivity(const char *seed_ssid,
const char *seed_password,
int connect_timeout_ms)
{
/* 启动只做一轮有界策略:已存凭据优先,失败再配网;持续重连归 runtime worker 所有。 */
ESP_LOGI(TAG,
"Boot Wi-Fi config SSID=%s timeout_ms=%d",
(seed_ssid != NULL && seed_ssid[0] != '\0') ? seed_ssid : "<empty>",
@@ -7,6 +7,10 @@
* command handlers, and UART replies in the control-plane layer. Reads
* `\r\n`-delimited commands from the console UART through the domain console
* bridge and replies with `OK`, `ERROR <reason>`, or one JSON line.
*
* 工厂串口 CLI 拥有行缓冲、命令解析和应答格式,但设备身份/Wi-Fi 的持久化仍经 domain 完成。
* worker 单实例运行;停止通过标志协作完成,不能在别的任务中直接删除它,否则可能留下半条
* UART 应答或未提交的配置。所有输入都有固定上限,超长行会整体拒绝而不是截断后误执行。
*/
#include "control_plane.h"
@@ -46,6 +50,8 @@
#define FACTORY_CLI_START_WAIT_STEP_MS 20
#define FACTORY_CLI_START_GATE_WAIT_MS 5000
/* 512 字节覆盖普通命令,独立 3 KiB Wi-Fi JSON 上限用于凭据列表;二者不可互相替代。 */
#ifndef CONFIG_TQ_LOCAL_TASK_CORE
#define CONFIG_TQ_LOCAL_TASK_CORE 1
#endif
@@ -18,6 +18,12 @@
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "sdkconfig.h"
/*
* 每日运势远端用例:读取 domain 中的出生档案,构造请求并把验证后的结果交回 domain 持久化。
* 网络响应属于临时缓冲区,解析后全部复制到定长领域结构;服务端日期必须与请求目标一致,
* 防止缓存或跨时区响应污染“当天结果”。启用 Device-JWT 时还会校验 fortune 所需 scope。
*/
#include "soc/soc_caps.h"
#ifndef CONFIG_FORTUNE_API_URL
@@ -43,6 +49,8 @@
static const char *TAG = "cp_fortune";
static const int kMaxHttpBody = 4096;
/* 返回字段规模固定,4 KiB 是协议护栏;超限视为服务端异常而不是尝试部分解析。 */
static void *fortune_psram_first_calloc(size_t count, size_t size)
{
if (count == 0U || size == 0U || count > (SIZE_MAX / size)) {
@@ -20,6 +20,12 @@
#include "freertos/task.h"
#include "sdkconfig.h"
/*
* 物理按键输入前端:ISR 只把 GPIO 编号投入队列,任务随后完成去抖、单双击和长按判定,
* 最终发布统一 system event。这样所有产品动作都离开 ISR,且多个按键共享同一时间语义。
* 配置表是 GPIO、有效电平和事件类型的唯一映射;新增按键应先检查引脚冲突与上下拉组合。
*/
#ifndef CONFIG_TQ_KEY_INPUT_ENABLE
#define CONFIG_TQ_KEY_INPUT_ENABLE 1
#endif
@@ -50,6 +56,11 @@
#ifndef CONFIG_TQ_KEY_INPUT_POLL_MS
#define CONFIG_TQ_KEY_INPUT_POLL_MS 20
/*
* 20 ms 与去抖周期一致;800 ms 长按避免普通点击误触,300 ms 双击窗口兼顾可操作性,
* 双击后再抑制 450 ms 是为了不把第二次释放沿解释成新的单击序列。
*/
#endif
#if !defined(CONFIG_TQ_PTT_MODE_HOLD_TO_TALK) && !defined(CONFIG_TQ_PTT_MODE_CLICK_TO_TALK)
@@ -237,6 +248,7 @@ static bool control_plane_key_level_is_pressed(const control_plane_key_config_t
static void IRAM_ATTR control_plane_key_gpio_isr_handler(void *arg)
{
/* ISR 只能使用 ISR-safe 队列 API,不能记录日志、读取复杂状态或直接调用业务服务。 */
const gpio_num_t gpio_num = (gpio_num_t)(intptr_t)arg;
QueueHandle_t queue = s_gpio_irq_queue;
if (queue == NULL) {
@@ -292,6 +304,7 @@ static esp_err_t control_plane_configure_key_gpio(const control_plane_key_config
static esp_err_t control_plane_validate_key_configs(void)
{
/* 启动前一次性拒绝重复 GPIO 和互斥的上下拉设置,避免运行期出现不可解释的幽灵按键。 */
for (size_t i = 0; i < sizeof(s_key_configs) / sizeof(s_key_configs[0]); ++i) {
const control_plane_key_config_t *config = &s_key_configs[i];
if (config->pullup_enabled && config->pulldown_enabled) {
@@ -356,6 +369,7 @@ static void control_plane_handle_debounced_transition(const control_plane_key_co
int level,
int64_t now_ms)
{
/* 物理边沿只更新按键状态机;单击要等双击窗口关闭后才最终发布。 */
if (state->wait_inactive_before_press) {
if (pressed) {
state->debounced_pressed = false;
@@ -435,6 +449,7 @@ static void control_plane_handle_debounced_transition(const control_plane_key_co
static void control_plane_process_key_timers(void)
{
/* 定时扫描补足没有新 GPIO 中断时的长按触发和延迟单击确认。 */
const int64_t now_ms = control_plane_event_timestamp_ms();
for (size_t i = 0; i < sizeof(s_key_configs) / sizeof(s_key_configs[0]); ++i) {
@@ -17,6 +17,12 @@
#include "freertos/task.h"
#include "sdkconfig.h"
/*
* 轻待机协调器:根据最后活动时间和系统忙闲状态进入低功耗策略,但不执行深睡或重启。
* transition_lock 串行化进入/退出,防止按键唤醒与定时任务同时改背光、音频、电机和 Wi-Fi。
* 每项节能动作都可配置,唤醒按相反顺序恢复;活动时间只由明确的用户/业务事件刷新。
*/
#ifndef CONFIG_TQ_LIGHT_STANDBY_ENABLE
#define CONFIG_TQ_LIGHT_STANDBY_ENABLE 0
#endif
@@ -86,6 +92,8 @@
#define CONTROL_PLANE_LIGHT_STANDBY_STOP_POLL_MS 20U
#define CONTROL_PLANE_LIGHT_STANDBY_TRANSITION_WAIT_MS 1000U
/* 每秒复查资格即可满足分钟级空闲阈值,同时避免忙时高频查询多模态和 OTA 状态。 */
#if CONFIG_TQ_LOCAL_TASK_CORE < 0
#define CONTROL_PLANE_LIGHT_STANDBY_TASK_CORE tskNO_AFFINITY
#else
@@ -171,6 +179,7 @@ static void control_plane_light_standby_log_transition(
static bool control_plane_light_standby_runtime_is_eligible(void)
{
/* 有语音轮次、OTA、配网或其他前台交互时禁止进入,空闲时长只是必要条件而非充分条件。 */
if (!control_plane_voice_turn_is_idle() ||
!control_plane_ota_voice_services_allowed() ||
control_plane_is_mini_program_provisioning_running()) {
@@ -243,6 +252,7 @@ static esp_err_t control_plane_light_standby_apply_runtime_policy(bool standby)
static esp_err_t control_plane_light_standby_apply_wake_policy(void)
{
/* 先恢复业务所需资源再宣布 ACTIVE,调用者返回时即可安全发起屏幕、音频或网络动作。 */
esp_err_t first_error = control_plane_light_standby_apply_runtime_policy(false);
#if CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT
@@ -274,6 +284,7 @@ static esp_err_t control_plane_light_standby_apply_wake_policy(void)
static esp_err_t control_plane_light_standby_apply_entry_policy(void)
{
/* 进入顺序尽量先停止高层活动再降低硬件能力;中途失败会由唤醒策略回滚已应用项。 */
#if CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD
esp_err_t motor_err = domain_direction_release_motor_hold(
CONTROL_PLANE_LIGHT_STANDBY_MOTOR_RELEASE_WAIT_MS);
@@ -365,6 +376,7 @@ static bool control_plane_light_standby_entry_is_current(
static void control_plane_light_standby_enter(uint64_t now_ms)
{
/* 二次核对 activity generation,防止准备待机期间刚发生的用户活动被旧定时结果覆盖。 */
if (!control_plane_light_standby_lock_transition()) {
ESP_LOGW(TAG, "Enter transition lock timed out");
return;
@@ -672,6 +684,7 @@ esp_err_t control_plane_light_standby_stop(int timeout_ms)
esp_err_t control_plane_light_standby_record_activity(
control_plane_light_standby_activity_t activity)
{
/* 更新时间与唤醒共用转换锁,确保新的 deadline 不会和正在提交的 STANDBY 状态交叉。 */
if (activity <= CONTROL_PLANE_LIGHT_STANDBY_ACTIVITY_UNSPECIFIED ||
activity > CONTROL_PLANE_LIGHT_STANDBY_ACTIVITY_EXTERNAL) {
return ESP_ERR_INVALID_ARG;
@@ -14,9 +14,16 @@
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
/*
* 网络反馈是一个小型聚合状态机:分别记录 Wi-Fi 与服务端可用性,只在持续故障或恢复边界提示。
* 状态在临界区更新,播放提示音必须在锁外进行;否则音频回调重入会阻塞其他网络事件。
* require_ready() 同时承担动作门禁,让“无法执行”的原因和播报保持一致。
*/
#define CONTROL_PLANE_SERVER_DISCONNECT_GRACE_MS 3000U
#define CONTROL_PLANE_NETWORK_PROMPT_COOLDOWN_MS 5000U
/* 3 秒宽限过滤 WebSocket 短重连,5 秒冷却防止 Wi-Fi 与服务端事件交替造成提示音轰炸。 */
#define CONTROL_PLANE_WIFI_INTERRUPTED_PROMPT_FILE \
"/littlefs/audio_prompts/network_interrupted_repairing.opus"
#define CONTROL_PLANE_WIFI_ACTION_BLOCKED_PROMPT_FILE \
@@ -20,6 +20,12 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
/*
* OTA 策略状态机:周期报告版本、等待服务端指令、检查电量/内存/业务空闲门禁,再独占语音资源
* 执行安装。下载与分区写入由 domain/platform 完成;本层负责“现在是否适合升级”及失败退避。
* snapshot 是面向诊断的值拷贝,任务停止采用协作标志,安装关键区不能被外部强删任务打断。
*/
#include "soc/soc_caps.h"
#ifndef CONFIG_TQ_OTA_ENABLE
@@ -80,6 +86,11 @@
#define CONTROL_PLANE_OTA_TASK_STACK_WORDS \
((CONFIG_TQ_OTA_TASK_STACK_BYTES + sizeof(StackType_t) - 1U) / sizeof(StackType_t))
/*
* 常规轮询与确认均为 6 小时,避免设备群频繁访问服务;低电量以 60 秒重查,因为充电状态可能
* 很快变化。除总内存外还检查 DMA 最大连续块,防止“剩余很多但无法分配音频/网络缓冲”。
*/
typedef struct {
bool acquired;
bool restore_multimodal;
@@ -155,6 +166,7 @@ static void control_plane_ota_set_state(control_plane_ota_state_t state,
esp_err_t error,
int http_status)
{
/* snapshot 更新集中在同一临界区,诊断读取不会观察到 state 与 error/http 状态跨代混合。 */
portENTER_CRITICAL(&s_ota_lock);
s_ota_snapshot.state = state;
s_ota_snapshot.last_error = error;
@@ -203,6 +215,7 @@ static bool control_plane_ota_wait_interruptible(uint32_t wait_ms)
static uint32_t control_plane_ota_retry_delay(uint32_t failures)
{
/* 采用饱和倍增而非直接移位,防止连续失败次数过大产生溢出后的短延迟。 */
uint64_t delay_ms = CONFIG_TQ_OTA_RETRY_BASE_MS;
for (uint32_t i = 1U; i < failures && delay_ms < CONFIG_TQ_OTA_RETRY_MAX_MS; ++i) {
delay_ms *= 2U;
@@ -320,6 +333,7 @@ static bool control_plane_ota_cancel(void *user_data)
static bool control_plane_ota_runtime_is_idle(void)
{
/* “空闲”要求语音轮次和关键业务都可让渡,单看 WebSocket 是否连接不足以判断。 */
if (!control_plane_voice_turn_is_idle()) {
return false;
}
@@ -420,6 +434,7 @@ static esp_err_t control_plane_ota_acquire_voice_window(
bool allow_active_teardown,
control_plane_ota_voice_window_t *window)
{
/* 先设置全局门禁阻止新会话,再停止已存在的语音服务;失败时由 window 记录内容精确回滚。 */
if (window == NULL) {
return ESP_ERR_INVALID_ARG;
}
@@ -505,6 +520,7 @@ static esp_err_t control_plane_ota_acquire_voice_window(
static void control_plane_ota_release_voice_window(control_plane_ota_voice_window_t *window)
{
/* 只恢复本函数实际停止过的服务,避免把调用前就关闭的功能意外启动。 */
if (window == NULL || !window->acquired) {
return;
}
@@ -591,6 +607,7 @@ static esp_err_t control_plane_ota_prepare_report(domain_ota_report_request_t *r
static esp_err_t control_plane_ota_check_once(domain_ota_report_response_t *response,
bool force_initial_jwt_refresh)
{
/* 一次检查包含上报、候选校验和确认判定,但不在资源/电量门禁之前开始下载。 */
if (response == NULL) {
return ESP_ERR_INVALID_ARG;
}
@@ -646,6 +663,7 @@ static esp_err_t control_plane_ota_check_once(domain_ota_report_response_t *resp
static void control_plane_ota_worker(void *arg)
{
/* 所有等待均切片为可中断睡眠,使 stop 不必等完整 6 小时轮询周期。 */
(void)arg;
if (ulTaskNotifyTake(pdTRUE,
pdMS_TO_TICKS(CONTROL_PLANE_OTA_START_GATE_MS)) == 0U) {
@@ -929,6 +947,7 @@ esp_err_t control_plane_start_ota_service(void)
esp_err_t control_plane_stop_ota_service(int timeout_ms)
{
/* 安装临界区优先协作取消;只有超出有界等待后才走确定性清理,避免写分区中途强删任务。 */
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
@@ -26,6 +26,12 @@
#include "freertos/task.h"
#include "sdkconfig.h"
/*
* 远程日志采用“日志 hook -> 内存环形队列 -> JSONL 文件 -> 批量上传”的四段式所有权链。
* hook 只复制有限长度文本,绝不联网或写文件;worker 独占文件轮转和上传,成功后才删除文件。
* 本地控制台输出可保留,递归保护防止本模块自己的上传日志再次进入 hook。
*/
#ifndef CONFIG_TQ_REMOTE_LOG_ENABLE
#define CONFIG_TQ_REMOTE_LOG_ENABLE 0
#endif
@@ -94,6 +100,11 @@
#define REMOTE_LOG_POLL_MS 200
#define REMOTE_LOG_VOICE_STARTUP_GRACE_MS 60000
/*
* 队列和单条长度均有界,压力过大时记录 dropped_count 而非阻塞产生日志的实时任务。
* 语音启动后留 60 秒宽限,避免日志 TLS/上传与首个会话争夺内部 RAM 和网络带宽。
*/
#if CONFIG_TQ_REMOTE_LOG_ENABLE
typedef struct {
@@ -240,6 +251,7 @@ static bool should_capture_level(char level)
static void enqueue_line(const char *line, bool truncated)
{
/* hook 上下文不等待空间:队列满时累计丢弃数,稍后作为合成记录写入,保留压力证据。 */
if (line == NULL || line[0] == '\0' || s_queue == NULL) {
return;
}
@@ -384,6 +396,7 @@ static void sanitize_value(char *text, const char *key)
static void sanitize_log_line(char *text)
{
/* 落盘前脱敏常见认证字段;这是最后一道保护,不替代调用方避免打印秘密。 */
sanitize_value(text, "password");
sanitize_value(text, "Password");
sanitize_value(text, "pwd");
@@ -557,6 +570,7 @@ static size_t count_pending_files(void)
static void trim_old_files(void)
{
/* 超过文件上限时只删最旧已封存批次,当前追加文件和内存队列不受影响。 */
uint32_t sequences[CONFIG_TQ_REMOTE_LOG_MAX_FILES + 8] = {0};
size_t count = collect_files(sequences, sizeof(sequences) / sizeof(sequences[0]));
while (count > CONFIG_TQ_REMOTE_LOG_MAX_FILES) {
@@ -618,6 +632,7 @@ static esp_err_t append_entry_to_file(const remote_log_entry_t *entry)
static void drain_memory_queue_to_flash(void)
{
/* 先从队列取得值拷贝再写 flash,文件 I/O 期间不占用队列临界区。 */
remote_log_entry_t entry = {0};
while (dequeue_entry(&entry)) {
if (append_entry_to_file(&entry) != ESP_OK) {
@@ -631,6 +646,7 @@ static void drain_memory_queue_to_flash(void)
static bool realtime_path_busy(void)
{
/* 音频/OTA 等实时路径繁忙时仍允许落盘,但推迟 TLS 上传以减少带宽和堆竞争。 */
#if CONFIG_TQ_LOCAL_WAKE_ENABLE
if (!control_plane_ota_voice_services_allowed() ||
!control_plane_voice_turn_is_idle()) {
@@ -861,6 +877,7 @@ static esp_err_t build_upload_body(uint32_t sequence,
static bool upload_one_file(uint32_t sequence)
{
/* 只有服务端明确接受后才删除源文件;传输或状态码失败均保留供下次重试。 */
uint32_t dropped = snapshot_and_clear_dropped_count();
char *body = (char *)remote_log_psram_malloc(REMOTE_LOG_UPLOAD_BODY_MAX);
char *response = (char *)remote_log_psram_malloc(REMOTE_LOG_RESPONSE_MAX);
@@ -1106,6 +1123,7 @@ static void uninstall_hook(void)
static void remote_log_task(void *arg)
{
/* worker 独占 flash 文件和上传顺序:先排空内存,再按序上传,停止前尽力完成最后一次落盘。 */
(void)arg;
esp_err_t mount_err = domain_runtime_mount_logfs();
@@ -1215,6 +1233,7 @@ esp_err_t control_plane_remote_log_flush_now(void)
esp_err_t control_plane_remote_log_clear(void)
{
/* 清理仅作用于远程日志目录和内存队列,不触碰系统日志分区或 coredump 本体。 */
portENTER_CRITICAL(&s_lock);
s_queue_read = 0U;
s_queue_count = 0U;
@@ -10,6 +10,10 @@
static const char *TAG = "cp_reset";
/*
* 恢复出厂联网状态的单一路径:先经 domain 清除持久化凭据,再请求平台重启。
* 即使清除失败也会记录错误并继续重启,使设备下次启动仍能进入现有的恢复/配网判定流程。
*/
void control_plane_reset_wifi_provisioning_and_restart(void)
{
ESP_LOGW(TAG, "Wi-Fi reset requested; clearing saved credentials");
@@ -20,6 +20,12 @@
#include "freertos/task.h"
#include "sdkconfig.h"
/*
* 控制面长期任务的集中装配与监督点。它拥有 LCD 渲染、Wi-Fi 状态、模型命令和任务监控 worker,
* 并按依赖顺序启动/逆序停止外围服务。队列只传递值类型命令;任何指针载荷都必须明确复制。
* 每个 task handle 同时承担“已启动”标志,创建/退出更新受生命周期门禁保护,避免重复 worker。
*/
#ifndef CONFIG_TQ_LCD_RENDER_TASK_STACK_BYTES
#define CONFIG_TQ_LCD_RENDER_TASK_STACK_BYTES (8U * 1024U)
#endif
@@ -58,6 +64,12 @@
#define CONTROL_PLANE_MEMORY_INTERNAL_DROP_BYTES 512U
#define CONTROL_PLANE_MEMORY_DMA_LARGEST_DROP_BYTES 256U
/*
* Wi-Fi 重连使用 5~60 秒指数退避,既快速处理偶发断链又避免离线时持续占用射频。
* PTT 的 80 ms 轮询追求按键响应;普通 Wi-Fi 状态每秒采样即可。内存告警看变化量而不仅是
* 绝对值,便于在任务监控日志中发现缓慢泄漏和最大连续块碎片化。
*/
#if (configSUPPORT_STATIC_ALLOCATION == 1) && \
((defined(CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) && CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) || \
(defined(CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY) && CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY))
@@ -325,6 +337,7 @@ static unsigned control_plane_heap_contiguous_percent(const multi_heap_info_t *i
static void control_plane_memory_monitor_sample(TickType_t now_tick, bool force)
{
/* 同时采样内部堆、PSRAM 和最大 DMA 块;后者下降往往比总空闲量更早暴露音频分配风险。 */
bool light_standby;
portENTER_CRITICAL(&s_runtime_lock);
light_standby = s_runtime_diagnostics_light_standby;
@@ -534,6 +547,7 @@ static const char *ptt_state_name(ptt_state_t state)
static void control_plane_ptt_transition_locked(ptt_state_t next_state, const char *reason)
{
/* 所有 PTT 状态迁移集中记录,便于把按键、网络和会话异步事件还原为一条时间线。 */
if (s_ptt_runtime.state == next_state) {
return;
}
@@ -876,6 +890,7 @@ static void control_plane_delete_model_cmd_queue(QueueHandle_t queue, bool queue
static esp_err_t control_plane_ensure_model_cmd_queue(void)
{
/* 先完整创建队列再发布句柄;其他任务永远看不到半初始化对象。 */
QueueHandle_t created_queue = NULL;
bool created_queue_with_caps = false;
@@ -1123,6 +1138,7 @@ static void control_plane_on_model_command(control_plane_model_command_type_t co
bool value_index_valid,
void *user_data)
{
/* 模型回调只复制白名单命令并入队,电机/LCD 等慢操作由 model_command_task 串行执行。 */
(void)user_data;
QueueHandle_t queue = NULL;
@@ -1269,6 +1285,7 @@ static esp_err_t control_plane_show_chouqian_expression(size_t result_index, boo
static void control_plane_model_command_task(void *arg)
{
/* 串行消费保证两个模型动作不会同时争用方向、表情和提示音资源。 */
(void)arg;
QueueHandle_t queue = control_plane_get_model_cmd_queue();
@@ -1633,6 +1650,7 @@ static void control_plane_request_wifi_state_expression(domain_expression_id_t i
static uint32_t control_plane_next_wifi_reconnect_delay_ms(uint32_t current_delay_ms)
{
/* 成功路径在调用方重置为基准;失败按倍增并封顶,避免整数溢出和永久过长等待。 */
if (current_delay_ms == 0U) {
return CONTROL_PLANE_WIFI_RECONNECT_BASE_DELAY_MS;
}
@@ -1897,6 +1915,7 @@ static bool control_plane_service_ptt_lifecycle(void)
static void control_plane_drive_ptt_state_machine(TickType_t *last_ws_start_attempt_tick)
{
/* 这是 PTT 唯一推进器:按键只改变意图,网络/会话条件满足后才真正建链和开麦。 */
if (control_plane_service_ptt_lifecycle()) {
return;
}
@@ -2057,6 +2076,7 @@ static void control_plane_start_wall_clock_sync(void)
static void control_plane_wifi_state_task(void *arg)
{
/* 汇总 STA、服务端、配网和 PTT 状态,负责重连与服务启停;不把策略分散到平台事件回调。 */
(void)arg;
ESP_LOGI(TAG, "Wi-Fi state monitor task started");
@@ -2451,6 +2471,7 @@ static time_t control_plane_next_local_midnight(time_t now)
static esp_err_t control_plane_refresh_daily_fortune(const char *target_date)
{
/* 远端成功后先持久化领域结果,再更新方向/表情,使重启后看到的状态与已呈现结果一致。 */
domain_fortune_result_t result = {0};
esp_err_t err = control_plane_fortune_fetch(target_date, &result);
if (err != ESP_OK) {
@@ -2617,6 +2638,7 @@ esp_err_t control_plane_start_lcd_render_worker(void)
esp_err_t control_plane_start_runtime_workers(void)
{
/* 启动顺序遵循生产者/消费者依赖;失败路径调用统一 stop,避免留下孤儿任务。 */
control_plane_set_runtime_stop_requested(false);
control_plane_network_feedback_reset();
portENTER_CRITICAL(&s_runtime_lock);
@@ -2792,6 +2814,7 @@ void control_plane_stop_event_observer_runtime(void)
void control_plane_stop_runtime_workers(void)
{
/* 先发布全局停止意图,再逆序唤醒并回收任务;最后才销毁队列和回调注册。 */
control_plane_set_runtime_stop_requested(true);
(void)control_plane_light_standby_stop(CONTROL_PLANE_TASK_STOP_WAIT_MS);
(void)control_plane_battery_service_stop(CONTROL_PLANE_TASK_STOP_WAIT_MS);
@@ -16,6 +16,12 @@
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
/*
* 系统事件任务是按键等离散输入的串行化决策点。生产者只投递值拷贝事件,消费者按 family/type
* 调度 PTT、校准、木鱼等用例;因此耗时动作不会出现在 GPIO ISR 中,也不会并发解释同一次输入。
* 队列满代表用户输入已过时,调用方得到明确错误而不是无限阻塞实时任务。
*/
#include "sdkconfig.h"
#ifndef CONFIG_TQ_SYSTEM_EVENT_ENABLE
@@ -35,6 +41,8 @@
#endif
#define CONTROL_PLANE_PTT_HOLD_STEP_MS 80
/* 长按 PTT 以 80 ms 小步等待,既能及时感知释放,也能周期检查会话已失败或被取消。 */
#define CONTROL_PLANE_MUYU_PROMPT_FILE "muyu.opus"
#ifndef CONFIG_TQ_LOCAL_TASK_CORE