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
@@ -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;
}