diff --git a/components/app_composition/include/app_composition.h b/components/app_composition/include/app_composition.h index edfcdd6..05284d2 100644 --- a/components/app_composition/include/app_composition.h +++ b/components/app_composition/include/app_composition.h @@ -12,6 +12,14 @@ extern "C" { #endif +/** + * @brief 启动默认应用组合流程。 / Start the default application composition. + * + * 本接口只负责顶层启动顺序和模块接线,不实现连接、重试或设备策略。首次致命阶段 + * 失败时返回明确错误;可降级阶段记录结果后继续。长期任务归 control_plane 或下层 + * 所有。This entry point owns boot ordering and wiring only; policy and workers + * remain owned by control_plane or lower layers. + */ esp_err_t app_composition_start_default_runtime(void); #ifdef __cplusplus diff --git a/components/app_composition/internal/app_composition_internal.h b/components/app_composition/internal/app_composition_internal.h index 2f8c54a..12e7734 100644 --- a/components/app_composition/internal/app_composition_internal.h +++ b/components/app_composition/internal/app_composition_internal.h @@ -6,6 +6,10 @@ #pragma once +/* + * app_composition 当前没有需要跨源文件共享的私有接口;保留此边界是为了以后拆分启动方案时, + * 不把编排细节泄漏到公共头文件。业务能力应继续通过 control_plane 注入,而非在此直连平台。 + */ #ifdef __cplusplus extern "C" { #endif diff --git a/components/app_composition/src/runtime/app_composition_default_runtime.c b/components/app_composition/src/runtime/app_composition_default_runtime.c index 543ef65..2b1b8dc 100644 --- a/components/app_composition/src/runtime/app_composition_default_runtime.c +++ b/components/app_composition/src/runtime/app_composition_default_runtime.c @@ -14,6 +14,12 @@ #include "freertos/task.h" #include "sdkconfig.h" +/* + * 默认启动编排只负责“按什么顺序组装”,不实现 domain/control_plane 的业务逻辑。 + * 顺序核心是:先建立存储和硬件基础,再恢复配置与网络,最后启动长期工作任务;这样后启动 + * 的任务可以把前序服务视为稳定依赖。失败处理分为致命失败与可降级失败,见主序列各阶段说明。 + */ + #ifndef CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN #define CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN 0 #endif @@ -26,6 +32,11 @@ #define CONFIG_TQ_WIFI_CONNECT_CYCLE_TIMEOUT_MS 20000 #endif +/* + * 3 秒只约束开机提示,不应拖住完整启动;20 秒是一次 Wi-Fi 连接/配网周期的共享预算, + * 而不是每个内部重试都重新获得 20 秒。dry-run 用于验证阶段依赖和日志,不触碰真实外设。 + */ + static const char *TAG = "app_comp_boot"; typedef enum { @@ -71,6 +82,7 @@ static void app_composition_log_stage_skipped(app_composition_boot_stage_t stage static esp_err_t app_composition_run_plan_a_boot_sequence(void) { + /* 每个阶段都在依赖满足后才进入;不要仅为缩短启动时间而随意交换这些调用。 */ app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_POWER_HOLD, "power_hold", "configured_or_disabled_by_platform_core"); @@ -78,6 +90,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_STORAGE_PREPARE, "storage_prepare", "control_plane_prepare_runtime_storage"); + /* 存储是身份、凭据和领域 SSOT 的前置依赖,失败后继续会制造“看似启动但状态无法保存”的假成功。 */ esp_err_t err = control_plane_prepare_runtime_storage(); if (err != ESP_OK) { ESP_LOGE(TAG, "Runtime storage prepare failed: %s", esp_err_to_name(err)); @@ -92,6 +105,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) "wall_clock", wall_clock_err); if (wall_clock_err != ESP_OK) { + /* 时区失败只影响本地日期类功能,联网和设备控制仍可工作,因此按降级处理。 */ ESP_LOGW(TAG, "Local time zone setup failed, continue boot: %s", esp_err_to_name(wall_clock_err)); @@ -109,6 +123,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) control_plane_take_mini_program_recovery_boot_request(); if (provisioning_recovery_boot) { + /* 恢复启动优先给 BLE 配网留出内部 RAM/DMA,暂不初始化音频和屏幕等大资源消费者。 */ app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT, "audio_init", "ble_recovery_boot"); @@ -138,6 +153,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) } #if CONFIG_TQ_SCREEN_ENABLE + /* LCD 是可选呈现通道:设备或渲染任务失败不阻断无屏的核心控制/配网能力。 */ app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_LCD_DEVICE, "lcd_device", "control_plane_init_lcd_device"); @@ -178,6 +194,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_PLATFORM_DOMAIN, "platform_domain", "control_plane_init"); + /* control_plane/domain 初始化失败会使后续所有用例前提不成立,属于致命阶段。 */ err = control_plane_init(); if (err != ESP_OK) { ESP_LOGE(TAG, "control_plane_init failed: %s", esp_err_to_name(err)); @@ -192,6 +209,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) } #if CONFIG_TQ_FACTORY_CLI_ENABLE + /* 工厂构建依赖 CLI 完成身份灌装;启用后无法启动应显式失败,避免产出未配置设备。 */ if (!provisioning_recovery_boot) { err = control_plane_factory_cli_start(); if (err != ESP_OK) { @@ -223,6 +241,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) "turntable_calibration", "control_plane_calibrate"); esp_err_t calibration_err = control_plane_calibrate(); + /* 校准失败仍允许联网和诊断启动,只把方向执行视为降级能力。 */ if (calibration_err != ESP_OK) { ESP_LOGW(TAG, "Boot turntable calibration failed, continue degraded: %s", @@ -242,6 +261,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) CONFIG_MULTIMODAL_WIFI_PASSWORD, CONFIG_TQ_WIFI_CONNECT_CYCLE_TIMEOUT_MS); if (wifi_err != ESP_OK) { + /* 联网失败可能由暂时离线造成;运行时重连策略仍需启动,因此这里只记录而不终止。 */ ESP_LOGW(TAG, "Connectivity bootstrap failed: %s", esp_err_to_name(wifi_err)); } @@ -252,9 +272,11 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) ? "deferred_until_ble_provisioning_completes" : "control_plane_start_runtime_workers"); if (provisioning_active) { + /* BLE 配网 worker 完成后负责恢复完整运行时,当前启动线程不得重复创建同一批任务。 */ ESP_LOGI(TAG, "Full runtime workers deferred while BLE provisioning is active"); } else { + /* 长期 worker 是在线恢复、事件分发等能力的宿主,非配网模式下启动失败不能伪装成功。 */ err = control_plane_start_runtime_workers(); if (err != ESP_OK) { ESP_LOGE(TAG, "Runtime worker start failed: %s", esp_err_to_name(err)); diff --git a/components/control_plane/include/control_plane.h b/components/control_plane/include/control_plane.h index cc25934..62ff37b 100644 --- a/components/control_plane/include/control_plane.h +++ b/components/control_plane/include/control_plane.h @@ -15,6 +15,24 @@ extern "C" { #endif +/* + * Control-plane 调用与所有权概览 / Control-plane call flow and ownership overview + * + * 本层拥有策略、重试/退避、健康状态和长生命周期工作任务;它只通过 domain facade + * 获取业务能力,不直接包含 platform 接口。app_composition 负责决定启动顺序,本层 + * 负责每项策略如何运行、停止和恢复。 + * This layer owns policy, retry/backoff, health and managed workers. It consumes + * only the domain facade; app_composition chooses startup order while this layer + * owns how each policy runs, stops and recovers. + * + * app_composition -> control_plane policy/worker -> domain facade -> platform + * + * 查询 API 返回不可变快照;工作任务必须协作停止并接受有界兜底。回调 payload 的 + * 生命周期由具体契约限定,调用方需要跨任务保留时必须复制。 + * Query APIs return snapshots. Workers use cooperative stop plus bounded fallback. + */ + +/* 命令枚举属于编排策略,解析本身无副作用,执行由显式 API 完成。 */ typedef enum { CONTROL_PLANE_DIRECTION_CMD_UNKNOWN = 0, CONTROL_PLANE_DIRECTION_CMD_HELP, @@ -45,9 +63,12 @@ typedef enum { } control_plane_multimodal_respond_type_t; typedef struct { + /* 响应载荷类型决定 text 被解释为转写文本还是提示词。 */ control_plane_multimodal_respond_type_t type; + /* 调用方拥有的 UTF-8 文本;函数只在调用期间读取。 */ const char *text; /* + * parameters_json 必须是 JSON object 文本,发送函数在调用期间读取,不长期持有。 * Optional JSON object serialized as payload.parameters for * RequestToRespond. Use this for documented fields such as images and * biz_params. NULL or empty means no parameters object is sent. @@ -57,6 +78,7 @@ typedef struct { typedef struct { /* + * parameters_json 是调用方拥有的 JSON object 文本,函数不会保留该指针。 * JSON object serialized as payload.parameters for UpdateInfo. Expected * fields follow docs/websocket.md: images, client_info, and biz_params. */ @@ -78,6 +100,7 @@ typedef enum { } control_plane_ota_state_t; typedef struct { + /* OTA 快照是 control-plane SSOT 的调用方副本,不暴露内部任务状态指针。 */ control_plane_ota_state_t state; bool running; bool forced; @@ -110,6 +133,7 @@ typedef enum { } control_plane_light_standby_activity_t; typedef struct { + /* 快照中的时间均为本次启动后的单调毫秒值,不是 Unix 墙上时钟。 */ control_plane_light_standby_state_t state; control_plane_light_standby_activity_t last_activity; bool running; @@ -122,8 +146,9 @@ typedef struct { esp_err_t control_plane_init(void); /** - * @brief Start battery sensing and charge-status presentation. + * @brief 启动电池采样和充电状态呈现。 / Start battery sensing and presentation. * + * 可重复调用;刻意早于联网启动,使电量和充电指示不依赖 Wi-Fi 或服务端。 * Safe to call repeatedly. This service is intentionally started before * connectivity bootstrap so battery and charger indications do not depend on * Wi-Fi, provisioning, or server readiness. @@ -131,26 +156,30 @@ esp_err_t control_plane_init(void); esp_err_t control_plane_battery_service_start(void); /** + * 低电量唤醒后若仍未恢复且无充电器,在正常启动前重新进入深睡。 + * * Re-enter deep sleep before normal boot when a low-battery wake has not * recovered and no charger is active. */ esp_err_t control_plane_battery_apply_boot_guard(void); /** - * @brief Prepare platform-backed persistent storage before runtime workers start. + * @brief 在工作任务前准备下层持久化存储。 / Prepare persistent storage before workers. * + * 幂等且没有 Wi-Fi/执行器副作用,用于避免非阻塞工作任务与 NVS store 初始化竞态。 * This is idempotent and has no Wi-Fi or actuator side effects. It exists so the * composition layer can start non-blocking workers before connectivity bootstrap * without racing NVS-backed stores. */ esp_err_t control_plane_prepare_runtime_storage(void); -/** Apply the configured local time zone before any local time is rendered. */ +/** 在渲染本地时间前应用时区。 / Apply the local time zone before rendering time. */ esp_err_t control_plane_prepare_wall_clock(void); /** - * @brief Start the control-plane owned light-standby coordinator. + * @brief 启动 control-plane 拥有的轻待机协调器。 / Start the light-standby coordinator. * + * 协调器为完整运行时维护一条空闲截止时间,并应用配置的本地硬件节能策略;重复启动幂等。 * The coordinator tracks one inactivity deadline for the full runtime and * applies configured local hardware power policies. Repeated starts are * idempotent. @@ -158,16 +187,18 @@ esp_err_t control_plane_prepare_wall_clock(void); esp_err_t control_plane_light_standby_start(void); /** - * @brief Stop the light-standby coordinator with bounded teardown. + * @brief 有界停止轻待机协调器。 / Stop the light-standby coordinator with bounded teardown. * + * 零超时只发起异步停止;正值最多等待指定预算,必要时执行确定性的任务清理。 * Zero requests asynchronous stop. Positive values wait up to the supplied * timeout and apply deterministic task teardown if necessary. */ esp_err_t control_plane_light_standby_stop(int timeout_ms); /** - * @brief Record user activity and return the coordinator to ACTIVE state. + * @brief 记录活动并恢复 ACTIVE。 / Record activity and return to ACTIVE. * + * 若硬件已在轻待机,本调用会先恢复配置的硬件再返回;转换串行化有界,禁止从 ISR 直接调用。 * If hardware is in light standby, this call restores configured hardware * before returning. Transition serialization is bounded; the API must not be * called directly from an ISR. @@ -176,13 +207,16 @@ esp_err_t control_plane_light_standby_record_activity( control_plane_light_standby_activity_t activity); /** - * @brief Copy the current light-standby state snapshot. + * @brief 复制当前轻待机状态快照。 / Copy the current light-standby snapshot. */ esp_err_t control_plane_light_standby_get_snapshot( control_plane_light_standby_snapshot_t *out_snapshot); /** - * @brief Parse one direction command without side effects. + * @brief 无副作用解析方向命令。 / Parse one direction command without side effects. + * + * 初始化契约:必须先显式调用 `control_plane_init()`;查询/解析 API 不做惰性初始化; + * 未初始化时返回 `CONTROL_PLANE_DIRECTION_CMD_UNKNOWN`。 * * Initialization contract: * - `control_plane_init()` must be called explicitly first. @@ -195,7 +229,10 @@ esp_err_t control_plane_rotate_to_angle(float target_angle); esp_err_t control_plane_rotate_to_direction(size_t direction_index); /** - * @brief Parse and execute one direction command line. + * @brief 解析并执行一行方向命令。 / Parse and execute one direction command line. + * + * 执行策略:`DIRECTION` 旋转到目标;`CALIBRATE` 使用默认预算校准;其余命令只解析、无执行器 + * 副作用。非动作命令或动作成功返回 `ESP_OK`,动作失败透传执行器错误。 * * Execution policy: * - `DIRECTION`: perform rotation to parsed target direction. @@ -211,7 +248,10 @@ esp_err_t control_plane_execute_direction_command(const char *input, size_t *out_direction_index); /** - * @brief Run calibration with explicit timeout budget. + * @brief 使用明确总预算执行校准。 / Run calibration with an explicit timeout budget. + * + * 非正超时使用内部默认值;超预算返回 `ESP_ERR_TIMEOUT`。若进入时校准开关已触发,实现可先 + * 在预算内释放并重新寻零,再设置校准角度。 * * Semantics: * - `timeout_ms <= 0`: use internal default timeout. @@ -224,7 +264,10 @@ esp_err_t control_plane_calibrate_with_timeout(int timeout_ms); esp_err_t control_plane_calibrate(void); /** - * @brief Bootstrap Wi-Fi connectivity for runtime. + * @brief 为运行时引导 Wi-Fi 连接。 / Bootstrap runtime Wi-Fi connectivity. + * + * 非正超时使用内部默认值;重复调用会复用已有良好状态。STA 已连接或 BLE 配网已运行均视为 + * 成功;超时/失败返回后不会在后台继续隐藏的破坏性重试。 * * Semantics: * - `connect_timeout_ms <= 0`: apply internal default timeout. @@ -238,7 +281,10 @@ esp_err_t control_plane_bootstrap_connectivity(const char *seed_ssid, int connect_timeout_ms); /** - * @brief Consume a one-shot request to use the low-memory BLE recovery boot. + * @brief 消费一次低内存 BLE 恢复启动请求。 / Consume a one-shot BLE recovery request. + * + * 请求借助 RTC no-init 内存跨软重启;本次启动读到 true 后会锁存,使联网流程跳过 STA 重连, + * 在完整运行时任务启动前优先进入 BLE 配网。 * * The request survives a software restart in RTC no-init memory. A true result * is latched for the current boot so connectivity skips STA reconnect and @@ -250,15 +296,18 @@ esp_err_t control_plane_stop_mini_program_provisioning(int timeout_ms); bool control_plane_is_mini_program_provisioning_running(void); /** - * @brief Execute the Plan A boot sound policy with bounded caller semantics. + * @brief 以有界语义执行 Plan A 启动音策略。 / Execute the bounded Plan A boot-sound policy. * + * 当前提示音后端可能返回 `ESP_ERR_NOT_SUPPORTED`;调用方应记录并继续启动,而非视为致命失败。 * The current platform voice-prompt backend may return ESP_ERR_NOT_SUPPORTED; * callers should log and continue boot in that case. */ esp_err_t control_plane_play_boot_sound(int timeout_ms); /** - * @brief Start multimodal client runtime. + * @brief 启动多模态客户端运行时。 / Start the multimodal client runtime. + * + * 已运行时重复调用幂等;所需后台任务采用有界启动;无法完成时返回明确错误。 * * Semantics: * - Idempotent for repeated calls when runtime is already active. @@ -268,7 +317,9 @@ esp_err_t control_plane_play_boot_sound(int timeout_ms); esp_err_t control_plane_start_multimodal_client(void); /** - * @brief Stop multimodal client runtime and owned background tasks. + * @brief 停止多模态运行时及其后台任务。 / Stop multimodal runtime and owned workers. + * + * 重复停止幂等;先请求协作退出,再按有界预算等待资源回收;未能完成时返回明确错误。 * * Semantics: * - Idempotent and safe to call repeatedly. @@ -281,16 +332,19 @@ esp_err_t control_plane_register_model_command_callback(control_plane_model_comm void *user_data); /** - * @brief Cancel an active push2talk speech input. + * @brief 取消当前 push2talk 语音输入。 / Cancel active push-to-talk speech input. * + * 向活动任务发送协议 `CancelSpeech` 指令,并停止本地上行采集。 * Sends the protocol `CancelSpeech` directive for the active multimodal task * and stops local uplink capture. */ esp_err_t control_plane_multimodal_cancel_speech(void); /** - * @brief Ask the active multimodal session to respond to local text. + * @brief 请求当前会话响应本地文本。 / Ask the active session to respond to local text. * + * 仅在活动任务处于 `DialogStateChanged(Listening)` 时有效;请求编码为协议 + * `RequestToRespond` 指令。 * Requires an active task in `DialogStateChanged(Listening)` state. The request * is encoded as the documented `RequestToRespond` directive. */ @@ -298,8 +352,9 @@ esp_err_t control_plane_multimodal_request_to_respond( const control_plane_multimodal_respond_request_t *request); /** - * @brief Update per-session multimodal context. + * @brief 更新当前多模态会话上下文。 / Update per-session multimodal context. * + * `request->parameters_json` 会作为 UpdateInfo 的 `payload.parameters` 对象编码。 * Encodes `request->parameters_json` as the documented UpdateInfo * `payload.parameters` object. */ @@ -307,8 +362,9 @@ esp_err_t control_plane_multimodal_update_info( const control_plane_multimodal_update_info_request_t *request); /** - * @brief Start control-plane owned runtime workers. + * @brief 启动 control-plane 拥有的运行时工作任务。 / Start control-plane runtime workers. * + * 包含模型命令分发任务和受联网状态门禁的多模态启动任务;重复调用安全且幂等。 * This includes model-command dispatch worker and connectivity-gated multimodal startup worker. * Safe to call repeatedly. */ @@ -320,7 +376,10 @@ esp_err_t control_plane_stop_ota_service(int timeout_ms); esp_err_t control_plane_get_ota_snapshot(control_plane_ota_snapshot_t *out); /** - * @brief Start the staged event observer runtime only. + * @brief 仅启动分阶段事件观察运行时。 / Start only the staged event-observer runtime. + * + * 仅启动系统事件分发和按键观察,以便通过日志验证模块事件;刻意不启动 LCD、Wi-Fi、 + * WebSocket、运势、LAN HTTP、电机或音频业务任务。 * * This starts the system event dispatcher and key input observer so module * events can be verified through logs. It intentionally does not start LCD, @@ -330,7 +389,11 @@ esp_err_t control_plane_start_event_observer_runtime(void); void control_plane_stop_event_observer_runtime(void); /** - * @brief Daily fortune policy (control_plane-owned). + * @brief control-plane 拥有的每日运势策略。 / Control-plane-owned daily fortune policy. + * + * 启用 `CONFIG_FORTUNE_ENABLE` 时由运行时 workers 启动:等待 STA 和 SNTP,按本地日期拉取、 + * 持久化并应用幸运色/财位,然后调度至本地次日 00:00。`refresh_now` 请求立即刷新;任务未 + * 运行返回 `ESP_ERR_INVALID_STATE`,构建未启用返回 `ESP_ERR_NOT_SUPPORTED`。 * * The daily fortune worker is started by `control_plane_start_runtime_workers()` * when `CONFIG_FORTUNE_ENABLE` is set. It waits for STA connectivity and SNTP @@ -349,15 +412,19 @@ esp_err_t control_plane_start_lcd_render_worker(void); void control_plane_start_lcd_render_loop(void); esp_err_t control_plane_request_boot_expression(void); /** - * @brief Stop LCD render loop with bounded teardown. + * @brief 有界停止 LCD 渲染循环。 / Stop LCD render loop with bounded teardown. * + * 负超时无效;零只请求异步停止;正值等待到预算结束,再由下层执行确定性兜底。 * `timeout_ms < 0` is invalid, zero requests asynchronous stop, and positive * values wait up to the bound before lower layers apply deterministic fallback. */ esp_err_t control_plane_stop_lcd_render_loop(int timeout_ms); /** - * @brief Start the factory provisioning serial CLI on the console UART. + * @brief 在控制台 UART 启动工厂配置 CLI。 / Start the factory CLI on console UART. + * + * 创建 control-plane 拥有的 worker,读取 `\r\n` 分隔命令并返回 `OK`、`ERROR ` + * 或单行 JSON;已运行时幂等。用于制造阶段灌装设备身份及读取/签名认证密钥材料。 * * Spawns a control-plane-owned worker that reads `\r\n`-delimited commands and * replies with `OK` / `ERROR ` / JSON lines. Idempotent (a no-op if @@ -367,8 +434,9 @@ esp_err_t control_plane_stop_lcd_render_loop(int timeout_ms); esp_err_t control_plane_factory_cli_start(void); /** - * @brief Stop the factory CLI worker (cooperative request + bounded teardown). + * @brief 协作请求并有界停止工厂 CLI。 / Stop factory CLI cooperatively with bounded teardown. * + * 重复调用安全且幂等。 * Idempotent and safe to call repeatedly. */ void control_plane_factory_cli_stop(void); diff --git a/components/control_plane/internal/control_plane_events.h b/components/control_plane/internal/control_plane_events.h index c62106f..e47bae8 100644 --- a/components/control_plane/internal/control_plane_events.h +++ b/components/control_plane/internal/control_plane_events.h @@ -10,6 +10,12 @@ #include "driver/gpio.h" +/* + * 控制面内部事件总线的数据契约:输入驱动只负责生成“发生了什么”,系统事件任务再决定 + * “应该做什么”。这种拆分避免在 GPIO 中断或按键扫描任务里直接执行联网、播放、旋转等 + * 可能阻塞的业务动作。这里的结构体会按值放入 FreeRTOS 队列,因此字段必须保持轻量, + * 不得加入需要跨任务管理生命周期的裸指针。 + */ typedef enum { CONTROL_PLANE_EVENT_FAMILY_KEY = 0, CONTROL_PLANE_EVENT_FAMILY_WIFI, diff --git a/components/control_plane/internal/control_plane_internal.h b/components/control_plane/internal/control_plane_internal.h index c701877..6590d9e 100644 --- a/components/control_plane/internal/control_plane_internal.h +++ b/components/control_plane/internal/control_plane_internal.h @@ -14,6 +14,11 @@ #include "esp_err.h" #include "control_plane_events.h" +/* + * control_plane 组件的私有协作接口,仅供本组件各编译单元使用。 + * 公共入口放在 control_plane.h;这里暴露的是解析器、工作任务和多模态状态机之间的接缝, + * 不承诺跨组件 ABI 稳定性。函数若涉及阻塞等待,其超时和调用上下文由对应实现说明。 + */ control_plane_direction_command_t control_plane_direction_parse_command_internal( const char *input, size_t *out_direction_index); diff --git a/components/control_plane/internal/multimodal/control_plane_multimodal_auth.h b/components/control_plane/internal/multimodal/control_plane_multimodal_auth.h index dcd4f79..3955911 100644 --- a/components/control_plane/internal/multimodal/control_plane_multimodal_auth.h +++ b/components/control_plane/internal/multimodal/control_plane_multimodal_auth.h @@ -14,6 +14,10 @@ extern "C" { #endif +/* + * 旧版多模态令牌服务的内部契约。长度宏同时约束 JSON 解析目标缓冲区和会话配置, + * 修改时必须与服务端字段上限一起核对,不能只为消除截断而盲目放大。 + */ #define CONTROL_PLANE_MULTIMODAL_AUTH_TOKEN_MAX_LEN 256 #define CONTROL_PLANE_MULTIMODAL_AUTH_ID_MAX_LEN 64 #define CONTROL_PLANE_MULTIMODAL_AUTH_VOICE_MAX_LEN 64 diff --git a/components/control_plane/internal/multimodal/control_plane_multimodal_device_auth.h b/components/control_plane/internal/multimodal/control_plane_multimodal_device_auth.h index a08c28d..e75d6fe 100644 --- a/components/control_plane/internal/multimodal/control_plane_multimodal_device_auth.h +++ b/components/control_plane/internal/multimodal/control_plane_multimodal_device_auth.h @@ -16,6 +16,10 @@ extern "C" { #endif +/* + * Device-JWT 认证引擎的组件内契约。调用方只消费按 scope 筛选后的 JWT;密钥生成、 + * challenge/response、刷新退避和持久化均由实现封装,避免会话与 OTA 各自复制认证状态机。 + */ /* * Device-auth (device-JWT) engine, control-plane internal. * diff --git a/components/control_plane/src/direction/control_plane_direction_calibration.c b/components/control_plane/src/direction/control_plane_direction_calibration.c index f437d50..e00ee91 100644 --- a/components/control_plane/src/direction/control_plane_direction_calibration.c +++ b/components/control_plane/src/direction/control_plane_direction_calibration.c @@ -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) diff --git a/components/control_plane/src/direction/control_plane_direction_command_parser.c b/components/control_plane/src/direction/control_plane_direction_command_parser.c index 3dbedee..1dd359b 100644 --- a/components/control_plane/src/direction/control_plane_direction_command_parser.c +++ b/components/control_plane/src/direction/control_plane_direction_command_parser.c @@ -9,6 +9,11 @@ #include #include +/* + * 方向命令的纯解析器:不访问设备状态,也不产生旋转副作用。 + * 先裁掉首尾空白再比较,是为了兼容串口、模型文本和 HTTP 等不同来源的换行习惯; + * 返回值只表达意图,真正的索引校验和执行仍由方向服务完成。 + */ static void trimmed_span(const char *input, const char **start_out, size_t *len_out) { const char *start = input; diff --git a/components/control_plane/src/direction/control_plane_direction_service.c b/components/control_plane/src/direction/control_plane_direction_service.c index f4af84b..600b52d 100644 --- a/components/control_plane/src/direction/control_plane_direction_service.c +++ b/components/control_plane/src/direction/control_plane_direction_service.c @@ -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(); diff --git a/components/control_plane/src/multimodal/control_plane_model_command_extractor.c b/components/control_plane/src/multimodal/control_plane_model_command_extractor.c index eb78538..de90ac4 100644 --- a/components/control_plane/src/multimodal/control_plane_model_command_extractor.c +++ b/components/control_plane/src/multimodal/control_plane_model_command_extractor.c @@ -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) diff --git a/components/control_plane/src/multimodal/control_plane_multimodal_auth_service.c b/components/control_plane/src/multimodal/control_plane_multimodal_auth_service.c index 278cade..130d8be 100644 --- a/components/control_plane/src/multimodal/control_plane_multimodal_auth_service.c +++ b/components/control_plane/src/multimodal/control_plane_multimodal_auth_service.c @@ -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 diff --git a/components/control_plane/src/multimodal/control_plane_multimodal_device_auth.c b/components/control_plane/src/multimodal/control_plane_multimodal_device_auth.c index 07b82cd..bb3708a 100644 --- a/components/control_plane/src/multimodal/control_plane_multimodal_device_auth.c +++ b/components/control_plane/src/multimodal/control_plane_multimodal_device_auth.c @@ -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; } diff --git a/components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c b/components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c index f7f080a..35471fd 100644 --- a/components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c +++ b/components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c @@ -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 diff --git a/components/control_plane/src/multimodal/control_plane_multimodal_protocol.c b/components/control_plane/src/multimodal/control_plane_multimodal_protocol.c index fe3d226..37defe9 100644 --- a/components/control_plane/src/multimodal/control_plane_multimodal_protocol.c +++ b/components/control_plane/src/multimodal/control_plane_multimodal_protocol.c @@ -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) diff --git a/components/control_plane/src/multimodal/control_plane_multimodal_runtime.c b/components/control_plane/src/multimodal/control_plane_multimodal_runtime.c index 9b66798..2f1f864 100644 --- a/components/control_plane/src/multimodal/control_plane_multimodal_runtime.c +++ b/components/control_plane/src/multimodal/control_plane_multimodal_runtime.c @@ -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(); diff --git a/components/control_plane/src/multimodal/control_plane_multimodal_session_service.c b/components/control_plane/src/multimodal/control_plane_multimodal_session_service.c index 1399c02..f0ca12f 100644 --- a/components/control_plane/src/multimodal/control_plane_multimodal_session_service.c +++ b/components/control_plane/src/multimodal/control_plane_multimodal_session_service.c @@ -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; } diff --git a/components/control_plane/src/runtime/control_plane_battery_service.c b/components/control_plane/src/runtime/control_plane_battery_service.c index fe024de..200cdda 100644 --- a/components/control_plane/src/runtime/control_plane_battery_service.c +++ b/components/control_plane/src/runtime/control_plane_battery_service.c @@ -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" diff --git a/components/control_plane/src/runtime/control_plane_connectivity_runtime.c b/components/control_plane/src/runtime/control_plane_connectivity_runtime.c index 1e8b84a..537aa2d 100644 --- a/components/control_plane/src/runtime/control_plane_connectivity_runtime.c +++ b/components/control_plane/src/runtime/control_plane_connectivity_runtime.c @@ -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 : "", diff --git a/components/control_plane/src/runtime/control_plane_factory_cli.c b/components/control_plane/src/runtime/control_plane_factory_cli.c index 316df0f..68b4419 100644 --- a/components/control_plane/src/runtime/control_plane_factory_cli.c +++ b/components/control_plane/src/runtime/control_plane_factory_cli.c @@ -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 `, 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 diff --git a/components/control_plane/src/runtime/control_plane_fortune_service.c b/components/control_plane/src/runtime/control_plane_fortune_service.c index 1eee994..b39e136 100644 --- a/components/control_plane/src/runtime/control_plane_fortune_service.c +++ b/components/control_plane/src/runtime/control_plane_fortune_service.c @@ -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)) { diff --git a/components/control_plane/src/runtime/control_plane_key_input.c b/components/control_plane/src/runtime/control_plane_key_input.c index 4347522..30ea82d 100644 --- a/components/control_plane/src/runtime/control_plane_key_input.c +++ b/components/control_plane/src/runtime/control_plane_key_input.c @@ -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) { diff --git a/components/control_plane/src/runtime/control_plane_light_standby.c b/components/control_plane/src/runtime/control_plane_light_standby.c index d7e3939..ccd738e 100644 --- a/components/control_plane/src/runtime/control_plane_light_standby.c +++ b/components/control_plane/src/runtime/control_plane_light_standby.c @@ -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; diff --git a/components/control_plane/src/runtime/control_plane_network_feedback.c b/components/control_plane/src/runtime/control_plane_network_feedback.c index 184d30c..dc4e6c2 100644 --- a/components/control_plane/src/runtime/control_plane_network_feedback.c +++ b/components/control_plane/src/runtime/control_plane_network_feedback.c @@ -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 \ diff --git a/components/control_plane/src/runtime/control_plane_ota.c b/components/control_plane/src/runtime/control_plane_ota.c index c293c95..17ab305 100644 --- a/components/control_plane/src/runtime/control_plane_ota.c +++ b/components/control_plane/src/runtime/control_plane_ota.c @@ -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; } diff --git a/components/control_plane/src/runtime/control_plane_remote_log_service.c b/components/control_plane/src/runtime/control_plane_remote_log_service.c index 1656b36..3e8d615 100644 --- a/components/control_plane/src/runtime/control_plane_remote_log_service.c +++ b/components/control_plane/src/runtime/control_plane_remote_log_service.c @@ -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; diff --git a/components/control_plane/src/runtime/control_plane_reset_service.c b/components/control_plane/src/runtime/control_plane_reset_service.c index 50cb41b..f41d409 100644 --- a/components/control_plane/src/runtime/control_plane_reset_service.c +++ b/components/control_plane/src/runtime/control_plane_reset_service.c @@ -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"); diff --git a/components/control_plane/src/runtime/control_plane_runtime_workers.c b/components/control_plane/src/runtime/control_plane_runtime_workers.c index 8f8040a..c9a7e29 100644 --- a/components/control_plane/src/runtime/control_plane_runtime_workers.c +++ b/components/control_plane/src/runtime/control_plane_runtime_workers.c @@ -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); diff --git a/components/control_plane/src/runtime/control_plane_system_events.c b/components/control_plane/src/runtime/control_plane_system_events.c index ccaf6a1..d3851f9 100644 --- a/components/control_plane/src/runtime/control_plane_system_events.c +++ b/components/control_plane/src/runtime/control_plane_system_events.c @@ -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 diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index fca8f92..c5db34f 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -15,6 +15,24 @@ extern "C" { #endif +/* + * Domain 层调用与所有权概览 / Domain call flow and ownership overview + * + * domain 是业务语义的唯一所有者:方向、校准、电池等级、配网协议、表情映射、 + * 用户资料、运势和 OTA 响应解析均在此定义。它只通过 platform facade 使用硬件原语, + * 不把 platform 类型暴露给 control_plane。 + * Domain is the single owner of business semantics and consumes hardware only + * through the platform facade without leaking platform types upward. + * + * control_plane -> domain facade -> domain service/adapter -> platform facade + * + * store/service 状态以 domain 内部快照或下层持久化为事实来源;回调携带的临时数据 + * 只在调用期间有效。阻塞接口的 timeout 包含锁等待和下层操作的完整调用方预算。 + * Stores and service snapshots are SSOTs. Callback payloads are temporary, and + * blocking timeouts cover lock acquisition plus all lower-layer work. + */ + +/* 方向轴与校准策略属于 domain 语义,GPIO/步进相位等硬件细节不得进入这些结构。 */ typedef struct { float initial_angle; int rotate_speed_us; @@ -29,6 +47,7 @@ typedef struct { int settle_delay_ms; } domain_direction_calibration_t; +/* 电池原始采样由 platform 提供,domain 负责充电、等级、有效性和抑制语义。 */ typedef enum { DOMAIN_BATTERY_CHARGE_UNKNOWN = 0, DOMAIN_BATTERY_CHARGE_NOT_CHARGING, @@ -58,22 +77,22 @@ typedef struct { int64_t last_valid_sample_time_ms; } domain_battery_status_t; -/** Initialize the battery measurement domain. Idempotent and thread-safe. */ +/** 幂等且线程安全地初始化电池领域。 / Initialize the battery domain idempotently and thread-safely. */ esp_err_t domain_battery_init(void); -/** Acquire and process one battery sample without applying product policy. */ +/** 获取并处理一次电池采样,不执行产品策略。 / Process one battery sample without product policy. */ esp_err_t domain_battery_sample(void); -/** Copy the latest battery SSOT snapshot into `out_status`. */ +/** 复制最新电池 SSOT 快照。 / Copy the latest battery SSOT snapshot into `out_status`. */ esp_err_t domain_battery_get_status(domain_battery_status_t *out_status); -/** Enter the configured board deep-sleep state; a successful call does not return. */ +/** 进入板级深睡,成功时不返回。 / Enter configured deep sleep; success does not return. */ esp_err_t domain_battery_prepare_shutdown(void); -/** Return true when the current boot resumed through a deep-sleep wake source. */ +/** 当前启动来自深睡唤醒源时返回 true。 / Return true after a deep-sleep wake. */ bool domain_battery_woke_from_deep_sleep(void); -/** Release low-power GPIO holds after accepting a deep-sleep wake. */ +/** 接受唤醒后释放低功耗 GPIO 保持。 / Release low-power GPIO holds after wake. */ void domain_battery_complete_wakeup(void); typedef enum { @@ -84,6 +103,7 @@ typedef enum { DOMAIN_WIFI_PROMPT_WIFI_CONFIG, } domain_wifi_prompt_id_t; +/* Wi-Fi 行为钩子使用 domain 提示语义,具体音频文件映射留在适配实现中。 */ typedef struct { void (*set_keep_awake)(bool keep_awake); esp_err_t (*play_prompt)(domain_wifi_prompt_id_t prompt); @@ -98,6 +118,7 @@ typedef enum { DOMAIN_MULTIMODAL_TRANSPORT_EVENT_BINARY_MESSAGE, } domain_multimodal_transport_event_type_t; +/* 回调 payload 仅在回调期间有效;跨任务保存前必须复制。 */ typedef struct { domain_multimodal_transport_event_type_t type; const uint8_t *data; @@ -202,6 +223,7 @@ typedef enum { DOMAIN_EXPRESSION_TRIGGER_FACTORY_LCD_TEST, } domain_expression_trigger_t; +/* 表情请求表达业务意图;资源目录、帧序列和图标位置由 domain 适配器映射。 */ typedef struct { domain_expression_id_t id; domain_expression_trigger_t trigger; @@ -228,20 +250,22 @@ float domain_get_current_angle(void); void domain_set_current_angle(float angle); int domain_get_rotate_speed_us(void); /** - * @brief Rotate to an absolute direction angle. + * @brief 旋转到绝对方向角。 / Rotate to an absolute direction angle. * + * 非正 timeout 使用 domain 默认值;预算覆盖状态锁获取和电机运动全过程。 * `timeout_ms <= 0` uses the default domain timeout. The timeout is the total * caller-visible budget, including state-lock acquisition and motor movement. */ esp_err_t domain_rotate_to_angle_with_timeout(float target_angle, int timeout_ms); esp_err_t domain_apply_rotation_delta(float delta); -/** Release stepper holding current; the next movement re-energizes automatically. */ +/** 释放电机保持电流,下次运动自动重新励磁。 / Release hold; next movement re-energizes. */ esp_err_t domain_direction_release_motor_hold(int timeout_ms); void domain_set_calibration(const domain_direction_calibration_t *config); bool domain_get_calibration(domain_direction_calibration_t *out_config); /** - * @brief Run direction calibration. + * @brief 执行方向校准。 / Run direction calibration. * + * 非正 timeout 使用默认校准预算,包含锁等待、分步运动、开关稳定等待和释放扫描。 * `timeout_ms <= 0` uses the default calibration timeout. The timeout is the * total caller-visible budget, including state-lock acquisition, movement * steps, switch settle delays, and release sweep. @@ -254,8 +278,9 @@ esp_err_t domain_runtime_multimodal_transport_start( domain_multimodal_transport_event_callback_t cb, void *user_data); /** - * @brief Stop the multimodal transport with bounded teardown. + * @brief 有界停止多模态传输。 / Stop multimodal transport with bounded teardown. * + * 负超时无效,零发起异步停止,正数等待到边界后由下层执行确定性兜底。 * `timeout_ms < 0` is invalid, zero requests asynchronous stop, and positive * values wait up to the bound before lower layers apply deterministic fallback. */ @@ -272,7 +297,7 @@ esp_err_t domain_runtime_multimodal_audio_start_uplink( void *user_data); /** - * @brief Stop multimodal uplink capture/stream with bounded teardown. + * @brief 有界停止多模态上行采集和流发送。 / Stop uplink capture/stream with bounded teardown. * * `timeout_ms < 0` is invalid, zero requests asynchronous stop, and positive * values wait up to the bound before lower layers apply deterministic fallback. @@ -280,11 +305,12 @@ esp_err_t domain_runtime_multimodal_audio_start_uplink( esp_err_t domain_runtime_multimodal_audio_stop_uplink(int timeout_ms); bool domain_runtime_multimodal_audio_is_uplink_running(void); bool domain_runtime_multimodal_audio_is_initialized(void); -/** Gate speaker output while preserving the microphone capture path. */ +/** 保留麦克风采集,仅控制扬声器输出。 / Gate speaker output while preserving capture. */ esp_err_t domain_runtime_multimodal_audio_set_speaker_output_enabled(bool enabled); /** - * @brief Start local MultiNet detection for the Chinese phrase "喵掌柜". + * @brief 启动“喵掌柜”本地 MultiNet 检测。 / Start local MultiNet detection for "喵掌柜". * + * 回调运行在 domain 推理任务中,必须有界且不可阻塞;相同回调的重复启动幂等。 * The callback runs in the domain inference task and must be bounded and * non-blocking. Repeated start with the same callback is idempotent. */ @@ -326,8 +352,10 @@ typedef enum { } domain_runtime_auth_record_t; /** - * @brief Domain-owned device-auth persistence port, backed by platform blobs. + * @brief domain 拥有、由 platform blob 支撑的设备认证持久化端口。 + * Domain-owned device-auth persistence port backed by platform blobs. * + * domain 只拥有记录槽、存储键映射和旧版迁移边界;调用方拥有序列化 schema。 * The domain facade owns only the auth record slots, storage key mapping, and * legacy migration boundary. Callers own the serialized record schema and pass * opaque bytes. `load_record` returns `ESP_ERR_NOT_FOUND` when no record exists @@ -342,10 +370,11 @@ esp_err_t domain_runtime_auth_store_save_record(domain_runtime_auth_record_t rec size_t len); /* ------------------------------------------------------------------------- */ -/* Daily fortune: user birth profile + server-computed daily result. */ -/* Owned by the domain layer; see docs/adr/0002-daily-fortune-contracts.md. */ +/* 每日运势:用户出生资料 + 服务端计算结果。 / Daily fortune profile + result. */ +/* 契约属于 domain;详见 docs/adr/0002-daily-fortune-contracts.md。 */ /* ------------------------------------------------------------------------- */ +/* 固定容量使资料可直接序列化进 NVS;长度包含结尾 NUL,修改需考虑旧记录兼容。 */ #define DOMAIN_BIRTH_PLACE_MAX_LEN 64 #define DOMAIN_BIRTH_RESIDENCE_MAX_LEN 64 #define DOMAIN_BIRTH_MBTI_MAX_LEN 8 @@ -364,6 +393,7 @@ typedef struct { bool valid; } domain_birth_profile_t; +/* 运势字段容量同时约束服务端 JSON 校验和持久化结构布局。 */ #define DOMAIN_FORTUNE_BAZI_MAX_LEN 64 #define DOMAIN_FORTUNE_ELEMENT_MAX_LEN 8 #define DOMAIN_FORTUNE_DIRECTION_MAX_LEN 16 @@ -382,8 +412,9 @@ typedef struct { typedef void (*domain_birth_profile_observer_t)(const domain_birth_profile_t *profile, void *user_data); /** - * @brief Persist / query the user birth profile (NVS-backed via the platform store). + * @brief 持久化或查询用户出生资料。 / Persist or query the user birth profile. * + * set 校验并持久化后在锁外通知观察者;get 在无有效记录时返回 false。存储是 SSOT。 * `set` requires a valid profile, persists it, then notifies the registered * observer with a snapshot (invoked outside any lock). `get` returns false when * no valid profile has been stored. Storage is the single source of truth; no @@ -393,21 +424,25 @@ esp_err_t domain_birth_profile_store_set(const domain_birth_profile_t *profile); bool domain_birth_profile_store_get(domain_birth_profile_t *out_profile); /** - * @brief Persist / query the most recent daily fortune result. + * @brief 持久化或查询最近一次每日运势。 / Persist or query the latest daily fortune result. */ esp_err_t domain_fortune_result_store_set(const domain_fortune_result_t *result); bool domain_fortune_result_store_get(domain_fortune_result_t *out_result); esp_err_t domain_fortune_result_store_clear(void); /** - * @brief Register the single observer notified when a new birth profile is - * stored (e.g. submitted over the LAN configuration page). Pass NULL to clear it. The - * callback must be lightweight (it runs in the caller's context). + * @brief 注册出生资料更新的唯一观察者。 / Register the single birth-profile observer. + * + * 新资料持久化后在调用方上下文通知,例如 LAN 配置页面提交;传 NULL 清除。 + * The callback runs in the caller's context after persistence and must be lightweight. */ void domain_runtime_register_birth_profile_observer(domain_birth_profile_observer_t cb, void *user_data); /** - * @brief Wall-clock time synchronization, bridged to the platform SNTP + * @brief 桥接到 platform SNTP 的墙上时钟同步。 / Wall-clock synchronization. + * + * set_timezone 仅设置本地时间转换;start 幂等启动 SNTP 并应用时区;网络就绪后调用。 + * Bridged to the platform SNTP * primitive. `set_timezone` applies local-time conversion without * network access. `start` is idempotent and also applies the process * time zone (`posix_tz`, e.g. "CST-8"); `is_synced` reports whether the @@ -417,6 +452,7 @@ esp_err_t domain_runtime_time_set_timezone(const char *posix_tz); esp_err_t domain_runtime_time_sync_start(const char *sntp_server, const char *posix_tz); bool domain_runtime_time_is_synced(void); +/* mDNS 主机名按值复制到服务状态,容量包含结尾 NUL。 */ #define DOMAIN_USER_CONFIG_HOSTNAME_MAX_LEN 32 typedef struct { @@ -431,8 +467,9 @@ typedef struct { } domain_user_config_server_state_t; /** - * @brief Start the domain-owned user profile and fortune web application. + * @brief 启动 domain 拥有的资料与运势 Web 应用。 / Start the domain-owned user web app. * + * platform 只提供有界 HTTP 传输;路由、JSON、校验和页面属于 domain;重复启动幂等。 * The platform supplies only the bounded raw HTTP transport. This domain * service owns the routes, JSON semantics, validation, and embedded page. * Start is idempotent while the service is starting or running. @@ -441,7 +478,7 @@ esp_err_t domain_user_config_server_start( const domain_user_config_server_config_t *config); /** - * @brief Stop the user configuration server with bounded teardown. + * @brief 有界停止用户配置服务。 / Stop the user configuration server with bounded teardown. * * `timeout_ms < 0` is invalid, zero requests asynchronous stop, and positive * values wait up to the bound before the platform applies deterministic @@ -452,8 +489,9 @@ esp_err_t domain_user_config_server_get_state( domain_user_config_server_state_t *out_state); /** - * @brief POST domain JSON data through the lower-layer HTTPS transport adapter. + * @brief 通过下层 HTTPS 适配器 POST domain JSON。 / POST domain JSON via HTTPS adapter. * + * 可选设备 ID 和 bearer token 被映射为请求头;响应写入调用方缓冲并以 NUL 结尾。 * Optional `device_id` is sent as X-Device-Id and optional `bearer_token` is * sent as Authorization: Bearer . The response body is written to the * caller-owned buffer and NUL-terminated on success. A completed HTTP exchange @@ -475,6 +513,7 @@ esp_err_t domain_runtime_post_json(const char *url, size_t *out_response_body_len, int *out_status); +/* OTA 固定容量是响应校验和任务间快照复制边界;SHA-256 为64位十六进制。 */ #define DOMAIN_OTA_VERSION_MAX_LEN 32 #define DOMAIN_OTA_URL_MAX_LEN 2048 #define DOMAIN_OTA_SHA256_HEX_LEN 64 @@ -534,7 +573,7 @@ esp_err_t domain_runtime_ota_get_memory_headroom(domain_ota_memory_headroom_t *o void domain_runtime_restart(void); /** - * @brief Console (UART) line-transport primitives, bridged to the platform. + * @brief 桥接到 platform 的串口行传输原语。 / Console line transport bridged to platform. * * `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 @@ -551,6 +590,7 @@ void domain_runtime_wifi_portal_set_behavior_hooks(const domain_wifi_behavior_ho esp_err_t domain_runtime_wifi_portal_init_defaults(const char *seed_ssid, const char *seed_password); bool domain_runtime_wifi_portal_has_saved_credentials(void); +/* SSID 容量包含 NUL;最多保存10个候选,返回顺序即后续连接优先级。 */ #define DOMAIN_WIFI_SSID_MAX_LEN 33 #define DOMAIN_WIFI_SAVED_NETWORK_MAX_COUNT 10 @@ -559,7 +599,7 @@ typedef struct { char ssids[DOMAIN_WIFI_SAVED_NETWORK_MAX_COUNT][DOMAIN_WIFI_SSID_MAX_LEN]; } domain_wifi_saved_networks_t; -/** Copy saved SSIDs in persistent priority order. Passwords are not exposed. */ +/** 按持久化优先级复制 SSID,不暴露密码。 / Copy saved SSIDs in priority order. */ esp_err_t domain_runtime_wifi_portal_get_saved_networks( domain_wifi_saved_networks_t *out_networks); bool domain_runtime_wifi_portal_sta_is_connected(void); @@ -567,7 +607,9 @@ esp_err_t domain_runtime_wifi_portal_start_config_ap(const char *ssid_prefix); bool domain_runtime_wifi_connect_error_keeps_credentials(esp_err_t err); /** - * @brief Connect STA using stored credentials with explicit timeout. + * @brief 以明确超时使用已存凭据连接 STA。 / Connect STA using stored credentials. + * + * 非正超时使用默认值;STA 已连接时幂等返回 `ESP_OK`;超时或连接错误会明确返回失败。 * * Semantics: * - `timeout_ms <= 0`: use default timeout. @@ -576,7 +618,9 @@ bool domain_runtime_wifi_connect_error_keeps_credentials(esp_err_t err); */ esp_err_t domain_runtime_wifi_portal_connect_sta(int timeout_ms); /** - * @brief Validate candidate credentials before replacing persisted Wi-Fi state. + * @brief 替换持久化状态前先验证候选凭据。 / Validate credentials before persistence. + * + * 有界连接试验失败时保留原凭据;只有试验成功才提交新凭据。 * * The previous credentials remain available when the bounded connection trial * fails. A successful trial commits only the new credentials. @@ -587,12 +631,13 @@ esp_err_t domain_runtime_wifi_portal_connect_sta_with_credentials( int timeout_ms); esp_err_t domain_runtime_wifi_portal_reconnect_sta(int timeout_ms); esp_err_t domain_runtime_wifi_portal_stop_sta(void); -/** Apply standby Wi-Fi power save, or restore the configured active policy. */ +/** 应用待机 Wi-Fi 省电或恢复活动策略。 / Apply standby Wi-Fi power save or restore policy. */ esp_err_t domain_runtime_wifi_portal_set_light_standby(bool standby); bool domain_runtime_is_network_connect_error(esp_err_t err); esp_err_t domain_runtime_wifi_portal_save_credentials(const char *ssid, const char *password); esp_err_t domain_runtime_wifi_portal_clear_saved_credentials(void); +/* 小程序配网容量包含 NUL,并对应 BLE 写入 payload 的校验边界。 */ #define DOMAIN_WIFI_PROVISIONING_SSID_MAX_LEN DOMAIN_WIFI_SSID_MAX_LEN #define DOMAIN_WIFI_PROVISIONING_PASSWORD_MAX_LEN 65 @@ -627,7 +672,7 @@ typedef enum { DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_CONNECT, } domain_wifi_provisioning_status_t; -/** Start the fixed TalkingQ mini-program BLE provisioning protocol. */ +/** 启动固定 TalkingQ 小程序 BLE 配网协议。 / Start the TalkingQ BLE provisioning protocol. */ esp_err_t domain_runtime_mini_program_provisioning_start( domain_wifi_provisioning_event_callback_t callback, void *user_data); @@ -636,24 +681,28 @@ esp_err_t domain_runtime_mini_program_provisioning_notify( esp_err_t domain_runtime_mini_program_provisioning_stop(int timeout_ms); esp_err_t domain_runtime_init_lcd_device(void); -/** Set LCD backlight PWM brightness from 0 to 100 percent. */ +/** 设置0到100%的 LCD PWM 背光。 / Set LCD PWM backlight from 0 to 100 percent. */ esp_err_t domain_runtime_set_lcd_backlight_brightness(uint8_t brightness_percent); void domain_runtime_start_lcd_render_loop(void); /** - * @brief Stop LCD render loop with bounded teardown. + * @brief 有界停止 LCD 渲染循环。 / Stop LCD render loop with bounded teardown. * + * 负超时无效;零只发起异步停止;正值等待到预算结束,再由下层执行确定性兜底。 * `timeout_ms < 0` is invalid, zero requests asynchronous stop, and positive * values wait up to the bound before lower layers apply deterministic fallback. */ esp_err_t domain_runtime_stop_lcd_render_loop(int timeout_ms); -/** Map the domain battery snapshot to persistent LCD status overlays. */ +/** 将电池快照映射为持久 LCD 状态层。 / Map battery snapshot to persistent LCD overlays. */ esp_err_t domain_runtime_update_battery_overlay(const domain_battery_status_t *status); esp_err_t domain_runtime_request_expression(const domain_expression_request_t *request); /** - * @brief Preload assets for an expression without changing the active display. + * @brief 预热表情资源而不改变当前显示。 / Preload expression assets without display changes. + * + * 用于即将播放的资源型表情。请求会被复制,LCD 渲染任务在帧间执行预热,调用方等待结果; + * 不支持或非资源型表情返回明确错误。 * * Intended for asset-backed expressions that are likely to play soon. The * request is copied, the LCD render task performs the preload between display @@ -663,9 +712,14 @@ esp_err_t domain_runtime_request_expression(const domain_expression_request_t *r esp_err_t domain_runtime_preheat_expression(const domain_expression_request_t *request); /** - * @brief Request an expression and block until its finite asset sequence + * @brief 请求表情并阻塞到有限资源序列结束或超时。 / + * Request an expression and block until its finite asset sequence * completes, or @p timeout_ms elapses. * + * 用于财富旋转开场等有限演示。入队失败返回底层错误;播放未在预算内结束返回 + * `ESP_ERR_TIMEOUT`(调用方可自行继续);LCD 未运行返回 `ESP_ERR_INVALID_STATE`。 + * `repeat_count == 0` 表示无限循环,因此本接口最终必然超时。 + * * Intended for finite presentations such as the wealth spin intro. Returns the * underlying request error if queuing fails, ESP_ERR_TIMEOUT if playback does * not finish within the timeout (the caller may proceed anyway), or diff --git a/components/domain/internal/domain_internal.h b/components/domain/internal/domain_internal.h index f272889..f3b3a2e 100644 --- a/components/domain/internal/domain_internal.h +++ b/components/domain/internal/domain_internal.h @@ -11,6 +11,10 @@ #include "domain.h" #include "esp_err.h" +/* + * domain 私有协作接口:目录、角度数学、校准存储和 platform 适配器只供本组件实现 + * 使用。上层不得包含此文件;所有跨层契约必须经 domain.h 暴露。 + */ typedef struct { const char *name_cn; const char *name_en; @@ -29,6 +33,7 @@ void domain_calibration_store_set(const domain_direction_calibration_t *config); bool domain_calibration_store_get(domain_direction_calibration_t *out_config); bool domain_direction_lock_state(int timeout_ms); +/* 状态锁保护角度/初始化快照,物理运动由 direction service 的另一互斥锁串行化。 */ void domain_direction_unlock_state(void); typedef struct { diff --git a/components/domain/src/battery/domain_battery_service.c b/components/domain/src/battery/domain_battery_service.c index 60a0171..f5fce7b 100644 --- a/components/domain/src/battery/domain_battery_service.c +++ b/components/domain/src/battery/domain_battery_service.c @@ -17,6 +17,11 @@ #if CONFIG_TQ_BATTERY_ENABLE +/* + * 电量等级使用迟滞阈值并连续确认3次,避免临界点抖动。单次电压跳变超过300mV时, + * 必须在100mV窗口内再次出现才接受;滤波采用旧值80%、新值20%。这些规则只生成 + * domain 状态,不决定关机、提示或 LCD 策略。 + */ #if CONFIG_TQ_BATTERY_LOW_EXIT_PERCENT <= CONFIG_TQ_BATTERY_LOW_ENTER_PERCENT #error "Battery low recovery threshold must exceed the entry threshold" #endif @@ -110,6 +115,7 @@ static domain_battery_charge_state_t map_charge_state( static uint8_t voltage_to_percentage(int voltage_mv) { + /* 在实测放电曲线相邻点之间线性插值,而不是用单一线性电压公式。 */ if (voltage_mv <= s_discharge_curve[0].voltage_mv) { return s_discharge_curve[0].percentage; } @@ -131,6 +137,7 @@ static uint8_t voltage_to_percentage(int voltage_mv) static bool accept_voltage_step(int voltage_mv) { + /* 大跳变需要连续候选确认,用于过滤电机/扬声器负载造成的瞬时压降。 */ if (!s_filter_initialized || abs(voltage_mv - s_filtered_mv) <= BATTERY_MAX_ACCEPTED_STEP_MV) { s_jump_candidate_count = 0U; return true; @@ -230,6 +237,7 @@ esp_err_t domain_battery_init(void) esp_err_t domain_battery_sample(void) { + /* platform 样本无效或高负载抑制时保留最后有效状态,只更新质量和失败计数。 */ esp_err_t err = domain_battery_init(); if (err != ESP_OK) { return err; diff --git a/components/domain/src/calibration/domain_calibration_service.c b/components/domain/src/calibration/domain_calibration_service.c index cf0c254..2540829 100644 --- a/components/domain/src/calibration/domain_calibration_service.c +++ b/components/domain/src/calibration/domain_calibration_service.c @@ -14,6 +14,10 @@ static const char *TAG = "domain_cal"; +/* + * 20秒是完整校准预算,单步最多1.5秒。扫描不超过330度;命中限位后分最多90度 + * 的块回到180度机械中心,再把逻辑角设为0。所有阶段共享同一绝对截止时间。 + */ #define DOMAIN_CALIBRATION_TIMEOUT_MS 20000 #define DOMAIN_CALIBRATION_STEP_TIMEOUT_MS 1500 #define DOMAIN_TURNTABLE_LIMIT_SEARCH_MAX_DEG 330.0f @@ -96,6 +100,7 @@ static bool calibration_settle(const domain_direction_calibration_t *calibration static esp_err_t calibration_rotate_and_track(float delta, int rotate_speed_us, int timeout_ms) { + /* 只有物理运动成功才同步更新 domain 角度,避免软件/机械位置分叉。 */ if (timeout_ms <= 0) { return ESP_ERR_TIMEOUT; } @@ -139,6 +144,7 @@ static esp_err_t calibration_return_from_limit(float search_direction, esp_err_t domain_calibrate_with_timeout(const domain_direction_calibration_t *config, int timeout_ms) { + /* 完整扫描期间持有 rotation mutex,普通方向命令不能插入校准序列。 */ int effective_timeout_ms = timeout_ms > 0 ? timeout_ms : DOMAIN_CALIBRATION_TIMEOUT_MS; TickType_t timeout_ticks = pdMS_TO_TICKS(effective_timeout_ms); if (timeout_ticks == 0) { diff --git a/components/domain/src/calibration/domain_calibration_store.c b/components/domain/src/calibration/domain_calibration_store.c index b96f44a..b8cb853 100644 --- a/components/domain/src/calibration/domain_calibration_store.c +++ b/components/domain/src/calibration/domain_calibration_store.c @@ -14,6 +14,7 @@ static domain_direction_calibration_t s_calibration = {0}; static bool s_calibration_configured = false; static portMUX_TYPE s_calibration_lock = portMUX_INITIALIZER_UNLOCKED; +/* 校准配置是受自旋锁保护的内存 SSOT;读写按值复制,不暴露内部指针。 */ void domain_calibration_store_set(const domain_direction_calibration_t *config) { portENTER_CRITICAL(&s_calibration_lock); diff --git a/components/domain/src/direction/domain_angle_math.c b/components/domain/src/direction/domain_angle_math.c index e65f9be..e47c7e1 100644 --- a/components/domain/src/direction/domain_angle_math.c +++ b/components/domain/src/direction/domain_angle_math.c @@ -6,6 +6,7 @@ #include "domain_internal.h" +/* 角度统一归一化到 (-180, 180],为目录匹配和最短旋转提供唯一表示。 */ float domain_angle_normalize(float angle) { while (angle > 180.0f) { @@ -19,6 +20,7 @@ float domain_angle_normalize(float angle) float domain_angle_shortest_rotation(float current, float target) { + /* 结果限制到 [-180, 180],正负号决定方向,避免不必要的整圈运动。 */ float diff = target - current; while (diff > 180.0f) { diff --git a/components/domain/src/direction/domain_direction_catalog.c b/components/domain/src/direction/domain_direction_catalog.c index 2cf9f62..60004ff 100644 --- a/components/domain/src/direction/domain_direction_catalog.c +++ b/components/domain/src/direction/domain_direction_catalog.c @@ -9,6 +9,7 @@ #include #include +/* 方向目录是名称、别名和标准角度的唯一事实来源;解析不修改当前转盘状态。 */ static const domain_direction_entry_t s_default_directions[] = { {"南", "S", 0.0f}, {"西南", "SW", 45.0f}, @@ -61,6 +62,7 @@ static void trimmed_span(const char *input, const char **start_out, size_t *len_ static bool equals_trimmed(const char *input, const char *token, bool case_insensitive) { + /* 忽略输入首尾空白;英文别名大小写不敏感,中文按 UTF-8 字节精确比较。 */ const char *start = NULL; size_t len = 0; size_t token_len = 0; @@ -140,6 +142,7 @@ bool domain_direction_catalog_find_by_name(const char *input, size_t *out_index) bool domain_direction_catalog_find_by_angle(float angle, size_t *out_index) { + /* 先归一化角度,再在浮点容差内匹配目录标准方向。 */ for (size_t i = 0; i < domain_direction_catalog_count(); i++) { const domain_direction_entry_t *entry = domain_direction_catalog_get(i); if (entry == NULL) { diff --git a/components/domain/src/direction/domain_direction_service.c b/components/domain/src/direction/domain_direction_service.c index c116122..76a677c 100644 --- a/components/domain/src/direction/domain_direction_service.c +++ b/components/domain/src/direction/domain_direction_service.c @@ -15,6 +15,10 @@ static const char *TAG = "domain"; +/* + * 默认速度和超时是一次方向动作的产品基线;校准扫描以5度步进、最多330度搜索, + * 避免机械结构在找不到限位时持续整圈运动。修改需同步验证电机和限位开关。 + */ #define DOMAIN_DEFAULT_ROTATE_SPEED_US 1500 #define DOMAIN_ROTATE_TIMEOUT_MS 8000 #define DOMAIN_STATE_LOCK_TIMEOUT_MS 2000 @@ -26,6 +30,7 @@ static const char *TAG = "domain"; #define DOMAIN_DEFAULT_CALIBRATION_SETTLE_DELAY_MS 10 static float s_current_angle = 0.0f; +/* 状态锁保护快照,rotation mutex 串行化整个物理运动;下层电机调用不持有状态锁。 */ static int s_rotate_speed_us = DOMAIN_DEFAULT_ROTATE_SPEED_US; static bool s_initialized; static SemaphoreHandle_t s_state_mutex; @@ -169,6 +174,7 @@ void domain_direction_unlock_state(void) esp_err_t domain_init(const domain_direction_axis_config_t *config, const domain_direction_calibration_t *calibration) { + /* platform 初始化成功后才发布 initialized,避免查询者看到半初始化配置。 */ esp_err_t runtime_err = domain_runtime_platform_init(); if (runtime_err != ESP_OK) { return runtime_err; @@ -301,6 +307,7 @@ int domain_get_rotate_speed_us(void) esp_err_t domain_rotate_to_angle_with_timeout(float target_angle, int timeout_ms) { + /* 计算最短角差并使用剩余总预算驱动电机;成功后才更新角度 SSOT。 */ TickType_t start_tick = xTaskGetTickCount(); int effective_timeout_ms = timeout_ms > 0 ? timeout_ms : DOMAIN_ROTATE_TIMEOUT_MS; @@ -365,6 +372,7 @@ esp_err_t domain_rotate_to_angle_with_timeout(float target_angle, int timeout_ms esp_err_t domain_apply_rotation_delta(float delta) { + /* 相对运动仍走同一互斥路径,完成后把累计角度归一化。 */ if (!domain_direction_lock_state(DOMAIN_STATE_LOCK_TIMEOUT_MS)) { return ESP_ERR_TIMEOUT; } diff --git a/components/domain/src/fortune/domain_fortune_store.c b/components/domain/src/fortune/domain_fortune_store.c index ab1d178..549fe0f 100644 --- a/components/domain/src/fortune/domain_fortune_store.c +++ b/components/domain/src/fortune/domain_fortune_store.c @@ -15,8 +15,7 @@ static const char *TAG = "domain_fortune_store"; -/* NVS namespace + keys (<=15 chars). The platform layer owns the raw storage; - * the domain serializes its typed records as opaque struct blobs. */ +/* NVS 命名空间/键不超过15字符。platform 只存原始 blob,domain 拥有结构 schema。 */ #define DOMAIN_FORTUNE_NVS_NAMESPACE "fortune" #define DOMAIN_FORTUNE_KEY_PROFILE "profile" #define DOMAIN_FORTUNE_KEY_RESULT "result" @@ -33,6 +32,7 @@ static bool s_fortune_result_loaded; static bool s_fortune_result_valid; static portMUX_TYPE s_fortune_result_lock = portMUX_INITIALIZER_UNLOCKED; +/* 内存快照采用惰性加载;loaded 与 valid 分开,避免每次查询重复访问 NVS。 */ static bool domain_birth_profile_is_valid(const domain_birth_profile_t *profile) { return profile != NULL && @@ -67,6 +67,7 @@ void domain_runtime_register_birth_profile_observer(domain_birth_profile_observe esp_err_t domain_birth_profile_store_set(const domain_birth_profile_t *profile) { + /* 先完成持久化,再发布内存快照和锁外观察者通知,确保通知代表已落盘状态。 */ if (!domain_birth_profile_is_valid(profile)) { return ESP_ERR_INVALID_ARG; } @@ -105,6 +106,7 @@ esp_err_t domain_birth_profile_store_set(const domain_birth_profile_t *profile) bool domain_birth_profile_store_get(domain_birth_profile_t *out_profile) { + /* 首次查询从 NVS 加载,缺失或旧记录无效时缓存“无有效资料”结果。 */ if (out_profile == NULL) { return false; } diff --git a/components/domain/src/fortune/domain_user_config_server.c b/components/domain/src/fortune/domain_user_config_server.c index 2eba134..d1903ad 100644 --- a/components/domain/src/fortune/domain_user_config_server.c +++ b/components/domain/src/fortune/domain_user_config_server.c @@ -18,6 +18,10 @@ static const char *TAG = "domain_user_web"; static const char *kFortuneTimezone = "Asia/Shanghai"; +/* + * 本服务拥有路由、JSON schema、日期校验和嵌入页面;platform 只提供有界 HTTP + * request/response 缓冲。handler 不保存 request body 指针,返回前完成全部解析。 + */ extern const uint8_t user_config_html_start[] asm("_binary_user_config_html_start"); extern const uint8_t user_config_html_end[] @@ -30,6 +34,7 @@ static bool is_leap_year(int year) static bool is_valid_date(int year, int month, int day) { + /* 接受1900-2100年并执行完整闰年/月份天数校验,拒绝只满足数值范围的无效日期。 */ static const uint8_t days_per_month[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, }; @@ -200,6 +205,7 @@ static bool parse_profile(const uint8_t *body, domain_birth_profile_t *out_profile, const char **out_error) { + /* 所有必填字段先解析到局部快照,完整校验通过后才交给 store,避免部分更新。 */ if (body == NULL || body_len == 0U) { *out_error = "Request body is empty"; return false; @@ -333,6 +339,7 @@ static esp_err_t user_config_http_handler( platform_lan_http_response_t *response, void *user_data) { + /* 路由匹配与 method 检查集中在此处,未知路径和错误方法返回明确 HTTP 状态。 */ (void)user_data; if (strcmp(request->path, "/") == 0) { diff --git a/components/domain/src/runtime/domain_local_command.c b/components/domain/src/runtime/domain_local_command.c index d62892a..c2a7215 100644 --- a/components/domain/src/runtime/domain_local_command.c +++ b/components/domain/src/runtime/domain_local_command.c @@ -19,6 +19,10 @@ #include "platform.h" #include "sdkconfig.h" +/* + * 本地命令链路:platform PCM observer -> 有界 PCM 队列 -> domain MultiNet 推理任务 + * -> generation 标记的检测事件。观察者从不阻塞采集;队列满时计数并丢帧。 + */ #ifndef CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES #define CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES 8192 #endif @@ -47,6 +51,10 @@ #define DOMAIN_LOCAL_COMMAND_MODEL_NAME "mn7_cn" #define DOMAIN_LOCAL_COMMAND_TEXT "喵掌柜" #define DOMAIN_LOCAL_COMMAND_GRAPH_TEXT "miao zhang gui" +/* + * 8帧队列吸收短期调度抖动;每帧最多512样本。控制/锁等待有界,统计每2秒汇总, + * 任务事件位区分请求停止、已停止、运行和暂停,避免只靠 task handle 猜测状态。 + */ #define DOMAIN_LOCAL_COMMAND_PCM_QUEUE_LEN 8U #define DOMAIN_LOCAL_COMMAND_PCM_MAX_SAMPLES 512U #define DOMAIN_LOCAL_COMMAND_QUEUE_WAIT_MS 100U @@ -203,6 +211,7 @@ static void local_command_pcm_observer(const int16_t *pcm, uint32_t sample_rate_hz, void *user_data) { + /* 只复制可容纳的数据并立即返回;推理、日志和回调都留给 domain 任务。 */ (void)user_data; bool admitted = false; portENTER_CRITICAL(&s_pcm_ingress_lock); @@ -425,6 +434,7 @@ static bool local_command_apply_control(size_t *fill_samples) static void local_command_publish_detection(float probability) { + /* confidence 转为万分比整数,generation 防止暂停/恢复前的迟到结果被误用。 */ domain_local_command_event_callback_t callback = NULL; void *callback_user_data = NULL; domain_local_command_event_t event = { @@ -692,6 +702,7 @@ esp_err_t domain_runtime_local_command_start( domain_local_command_event_callback_t callback, void *user_data) { + /* 模型、队列、静态任务栈全部准备成功后才注册 PCM observer 并发布 started。 */ if (callback == NULL) { return ESP_ERR_INVALID_ARG; } diff --git a/components/domain/src/runtime/domain_mini_program_provisioning.c b/components/domain/src/runtime/domain_mini_program_provisioning.c index e5d9ed9..ca88e07 100644 --- a/components/domain/src/runtime/domain_mini_program_provisioning.c +++ b/components/domain/src/runtime/domain_mini_program_provisioning.c @@ -14,6 +14,10 @@ #include "freertos/FreeRTOS.h" #include "platform.h" +/* + * 设备信息使用固定缓冲构造 JSON;写特征分片必须在3秒内完成,否则清空旧片段, + * 防止两次手机写入拼接。名称前缀和 UUID 属于固定 TalkingQ 小程序协议。 + */ #define DOMAIN_PROVISIONING_ID_MAX_LEN 64 #define DOMAIN_PROVISIONING_SERIAL_MAX_LEN 64 #define DOMAIN_PROVISIONING_VERSION_MAX_LEN 32 @@ -63,6 +67,7 @@ static const platform_ble_uuid128_t s_device_info_uuid = { static void domain_provisioning_dispatch_event( const domain_wifi_provisioning_event_t *event) { + /* 回调对在锁内取快照、锁外调用,允许上层在事件中停止或重启配网。 */ domain_wifi_provisioning_event_callback_t callback; void *user_data; @@ -91,6 +96,7 @@ domain_provisioning_append_write_fragment(const uint8_t *data, size_t data_len, size_t *out_buffered_len) { + /* 以完整 JSON 结束符判断写入完成;越界、超时或格式异常会重置整个缓冲。 */ if (data == NULL || data_len == 0U) { return DOMAIN_PROVISIONING_WRITE_INVALID; } diff --git a/components/domain/src/runtime/domain_ota.c b/components/domain/src/runtime/domain_ota.c index d8527e9..8d954b1 100644 --- a/components/domain/src/runtime/domain_ota.c +++ b/components/domain/src/runtime/domain_ota.c @@ -19,6 +19,10 @@ #include "mbedtls/sha256.h" #include "platform.h" +/* + * 4KB 响应上限约束版本查询 JSON;36字符 nonce 提供请求唯一性;内部认证值包含 + * 时间戳/nonce/HMAC 编码。系统时间早于合理 epoch 时拒绝生成时效认证。 + */ #define DOMAIN_OTA_RESPONSE_CAPACITY 4096 #define DOMAIN_OTA_NONCE_LEN 36 #define DOMAIN_OTA_AUTH_VALUE_MAX_LEN 128 @@ -117,6 +121,7 @@ static esp_err_t domain_ota_build_internal_auth(const char *url, char *signature, size_t signature_len) { + /* 签名输入绑定 HTTP 路径、时间戳和随机 nonce,避免认证值跨接口或重放复用。 */ time_t now = time(NULL); if ((int64_t)now < DOMAIN_OTA_MIN_VALID_EPOCH) { return ESP_ERR_INVALID_STATE; @@ -216,6 +221,7 @@ static esp_err_t domain_ota_build_report_payload(const domain_ota_report_request static esp_err_t domain_ota_parse_report_response(const char *body, domain_ota_report_response_t *response) { + /* 完整验证业务码、版本、URL、SHA-256、大小和强制/确认标志后才发布固件快照。 */ cJSON *root = cJSON_Parse(body); if (root == NULL) { return ESP_ERR_INVALID_RESPONSE; @@ -427,6 +433,7 @@ esp_err_t domain_runtime_ota_install(const domain_ota_firmware_t *firmware, domain_ota_cancel_callback_t cancel_cb, void *user_data) { + /* domain 将十六进制哈希转为32字节并桥接进度/取消回调,下载写入仍由 platform 拥有。 */ if (firmware == NULL || strncmp(firmware->url, "https://", 8) != 0 || firmware->size_bytes == 0U || timeout_ms <= 0) { return ESP_ERR_INVALID_ARG; diff --git a/components/domain/src/runtime/domain_runtime_adapters.c b/components/domain/src/runtime/domain_runtime_adapters.c index b40a5ee..df50945 100644 --- a/components/domain/src/runtime/domain_runtime_adapters.c +++ b/components/domain/src/runtime/domain_runtime_adapters.c @@ -16,6 +16,12 @@ #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/portmacro.h" + +/* + * Domain 运行时适配器:把领域层端口翻译成 platform 调用,并把平台事件映射回领域语义。 + * 本文件不应引入重试、状态机或产品策略;这些属于 control_plane。静态回调及 hook 是注册 + * 信息的唯一副本,分发时先在临界区内取得快照,再在锁外调用,防止回调重入造成死锁。 + */ #include "sdkconfig.h" #ifndef CONFIG_MULTIMODAL_PTT_BUTTON_GPIO @@ -138,8 +144,12 @@ static const char *const s_wealth_direction_asset_dirs[] = { #define DOMAIN_WUXING_REVEAL_FRAME_MS 100U #define DOMAIN_WUXING_HOLD_FRAME_MS 500U +/* 动画节奏属于领域呈现契约:短帧展示运动过程,较长末帧让用户有时间辨认最终结果。 */ + #define DOMAIN_AUTH_KEY_STORE_NAMESPACE "auth_key" #define DOMAIN_AUTH_STATE_STORE_NAMESPACE "auth" + +/* 认证密钥与认证状态分命名空间保存,便于独立轮换密钥而不破坏注册进度。 */ #define DOMAIN_AUTH_KEY_STORE_BLOB_KEY "keymat" #define DOMAIN_AUTH_STATE_STORE_BLOB_KEY "state" #define DOMAIN_AUTH_LEGACY_KEY_PRIV_KEY "sk" @@ -304,6 +314,7 @@ static const domain_expression_catalog_entry_t s_expression_catalog[] = { static void snapshot_domain_wifi_behavior_hooks(domain_wifi_behavior_hooks_t *out_hooks) { + /* 只在临界区复制函数指针和上下文;实际 hook 在锁外调用,允许 hook 反向更新注册。 */ if (out_hooks == NULL) { return; } @@ -392,6 +403,7 @@ static void dispatch_domain_transport_event(domain_multimodal_transport_event_ty const uint8_t *data, size_t data_len) { + /* data 只在本次 platform 回调期间有效;domain 不缓存,跨任务保留由上层自行复制。 */ domain_multimodal_transport_event_callback_t cb = NULL; void *cb_user_data = NULL; snapshot_domain_transport_callback(&cb, &cb_user_data); @@ -576,6 +588,7 @@ esp_err_t domain_runtime_post_json(const char *url, size_t *out_response_body_len, int *out_status) { + /* 本适配只映射 header/缓冲区所有权,不解释业务 HTTP 状态,也不在 domain 内隐式重试。 */ if (out_response_body_len != NULL) { *out_response_body_len = 0U; } @@ -1116,6 +1129,7 @@ static esp_err_t load_legacy_auth_record(domain_runtime_auth_record_t record, size_t out_len, size_t *out_len_actual) { + /* 旧键只用于一次兼容读取;成功后由新格式保存路径接管,迁移不会改变调用方序列化 schema。 */ switch (record) { case DOMAIN_RUNTIME_AUTH_RECORD_KEY_MATERIAL: return load_legacy_auth_key_material(out, out_len, out_len_actual); @@ -1180,6 +1194,7 @@ esp_err_t domain_runtime_auth_store_save_record(domain_runtime_auth_record_t rec const void *data, size_t len) { + /* platform blob 写入成功才更新持久状态;输入缓冲区始终由调用方拥有。 */ if (data == NULL || len == 0U) { return ESP_ERR_INVALID_ARG; } @@ -1430,6 +1445,7 @@ static esp_err_t domain_resolve_expression_platform_request( const domain_expression_request_t *request, platform_lcd_presentation_request_t *out_platform_request) { + /* 这是领域表情 ID 到平台资源序列的唯一映射,避免 control_plane 拼接资源目录和帧参数。 */ if (request == NULL || out_platform_request == NULL) { return ESP_ERR_INVALID_ARG; } @@ -1621,6 +1637,7 @@ esp_err_t domain_runtime_preheat_expression(const domain_expression_request_t *r esp_err_t domain_runtime_request_expression_blocking(const domain_expression_request_t *request, uint32_t timeout_ms) { + /* 仅有限序列适合等待完成;底层仍拥有渲染任务,调用方只是等待该请求的完成通知。 */ /* Snapshot the playback completion counter before queuing so the wait stays * robust to the asynchronous request not having started yet. */ uint32_t since_count = platform_lcd_presentation_completion_count(); diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index a296a4c..1a73b3d 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -15,11 +15,40 @@ extern "C" { #endif +/* + * Platform 层调用与所有权概览 / Platform call flow and ownership overview + * + * 上层只能通过本门面调用硬件与系统原语;domain 适配器负责把业务语义映射为 + * platform 请求,platform 再把请求交给内部驱动或工作任务。典型路径如下: + * Upper layers call hardware/system primitives only through this facade. Domain + * adapters map business semantics to platform requests, which are then handled + * by internal drivers or platform-owned workers: + * + * domain adapter -> platform_* facade -> internal driver/queue -> ESP-IDF/hardware + * + * 长生命周期任务、队列、socket、codec 和面板句柄均由 platform 创建并回收; + * 调用方拥有输入/输出缓冲,除非具体契约明确说明请求数据会在调用期间复制。 + * Platform owns long-lived tasks, queues, sockets, codec and panel handles. + * Callers own I/O buffers unless a contract explicitly states that data is copied. + * + * 状态查询返回快照;回调注册信息在锁内复制、在锁外调用。带 timeout_ms 的停止 + * 接口遵循各自文档中的有界等待语义,不应由上层额外复制生命周期状态。 + * State APIs return snapshots. Callback registrations are copied under lock and + * invoked outside locks. Timeout-bearing stop APIs use the bounded semantics below. + */ + +/* + * 步进速度以“单步延时(微秒)”表示,因此数值越小速度越快;这些值是公共预设, + * 实际安全范围仍受驱动的最大角度、启动延时和加减速曲线约束。 + * Stepper speed is expressed as per-step delay in microseconds, so smaller values + * are faster. Driver-side angle limits and acceleration still apply. + */ #define PLATFORM_STEPPER_SPEED_ULTRA_FAST 600 #define PLATFORM_STEPPER_SPEED_FAST 800 #define PLATFORM_STEPPER_SPEED_NORMAL 1500 #define PLATFORM_STEPPER_SPEED_SLOW 2000 +/* 通用数字输入描述:统一 GPIO 编号、有效电平及上下拉配置。 */ typedef struct { int gpio_num; int active_level; @@ -27,12 +56,14 @@ typedef struct { bool pulldown_enabled; } platform_digital_input_config_t; +/* Wi-Fi 门户只通过这些钩子请求上层行为,不直接依赖具体业务模块。 */ typedef struct { void (*set_keep_awake)(bool keep_awake, void *user_data); esp_err_t (*play_prompt_file)(const char *path, void *user_data); void *user_data; } platform_wifi_behavior_hooks_t; +/* 多模态传输回调的数据只在回调执行期间有效,需要长期使用时必须复制。 */ typedef struct { void (*on_connected)(void *user_data); void (*on_disconnected)(void *user_data); @@ -42,6 +73,7 @@ typedef struct { void *user_data; } platform_multimodal_transport_callbacks_t; +/* generation 用于区分连续的下行播放会话,避免旧事件结束新会话。 */ typedef struct { void (*on_started)(uint32_t playback_generation, void *user_data); void (*on_completed)(uint32_t playback_generation, void *user_data); @@ -56,11 +88,18 @@ typedef void (*platform_multimodal_audio_pcm_observer_t)( uint32_t sample_rate_hz, void *user_data); +/* + * LAN HTTP 固定容量是传输层内存边界,也是调用方结构体的 ABI 上限;增大 body + * 上限会直接增加服务器常驻 I/O 缓冲,修改时需同步评估内部 RAM/PSRAM。 + * LAN HTTP capacities are transport and ABI bounds. Raising body limits increases + * the server's resident I/O buffers and requires a memory-budget review. + */ #define PLATFORM_LAN_HTTP_PATH_MAX_LEN 96 #define PLATFORM_LAN_HTTP_HOSTNAME_MAX_LEN 32 #define PLATFORM_LAN_HTTP_REQUEST_BODY_MAX_LEN 1024 #define PLATFORM_LAN_HTTP_RESPONSE_BODY_MAX_LEN 16384 +/* 局域网 HTTP 层只提供有界传输能力,URL 路由与业务语义由 domain 层负责。 */ typedef enum { PLATFORM_LAN_HTTP_METHOD_GET = 0, PLATFORM_LAN_HTTP_METHOD_POST, @@ -115,6 +154,7 @@ typedef struct { char hostname[PLATFORM_LAN_HTTP_HOSTNAME_MAX_LEN]; } platform_lan_http_server_state_t; +/* HTTPS 请求和响应缓冲区均由调用方持有,平台层不会长期保存这些指针。 */ typedef struct { const char *name; const char *value; @@ -142,10 +182,16 @@ typedef void (*platform_ota_progress_callback_t)(uint64_t bytes_received, void *user_data); typedef bool (*platform_ota_cancel_callback_t)(void *user_data); +/* + * BLE 限制分别来自 UUID128、广播名称容量和单次 GATT 值的本地缓冲边界。 + * BLE limits cover UUID128 size, advertised-name capacity and the local GATT + * value buffer; they do not define higher-level provisioning payload semantics. + */ #define PLATFORM_BLE_UUID128_LEN 16 #define PLATFORM_BLE_GATT_NAME_MAX_LEN 31 #define PLATFORM_BLE_GATT_VALUE_MAX_LEN 512 +/* BLE 层暴露通用单服务 GATT 原语,不在此处定义配网等业务协议。 */ typedef struct { uint8_t bytes[PLATFORM_BLE_UUID128_LEN]; } platform_ble_uuid128_t; @@ -171,6 +217,7 @@ typedef struct { void *user_data; } platform_ble_gatt_server_config_t; +/* 平台初始化与持久化存储准备分离,便于启动阶段明确处理存储失败。 */ esp_err_t platform_init(void); esp_err_t platform_prepare_storage(void); bool platform_storage_is_ready(void); @@ -181,9 +228,13 @@ void platform_read_device_identity(char *device_id, char *serial_number, size_t serial_number_len); +/* 设备身份由工厂写入 NVS;读取接口始终写入调用方提供的缓冲区。 */ /** - * @brief Persist device identity (device_id + serial_number) to NVS. + * @brief 将设备身份(device_id + serial_number)持久化到 NVS。 + * Persist device identity (device_id + serial_number) to NVS. * + * 写入内容与 `platform_read_device_identity()` 的读取来源相同。两个参数都必须 + * 非空;否则或 NVS 写入失败时返回明确错误。主要用于工厂烧录/配置。 * Writes the same storage that `platform_read_device_identity()` reads. Both * arguments must be non-empty; returns an explicit error otherwise or on NVS * failure. Intended for factory provisioning. @@ -191,8 +242,11 @@ void platform_read_device_identity(char *device_id, esp_err_t platform_write_device_identity(const char *device_id, const char *serial_number); /** - * @brief Console (UART) line-transport primitives for the factory CLI. + * @brief 工厂 CLI 使用的串口行传输原语。 + * Console (UART) line-transport primitives for the factory CLI. * + * 这些接口不保存命令状态。install 可重复调用;read 返回读取字节数,超时返回0, + * 错误返回负数。零超时表示轮询一次,负超时无效。 * Stateless primitives over the console UART. `platform_console_install()` is * idempotent (a pre-existing console driver is treated as success). * `platform_console_read_bytes()` returns bytes read (0 on timeout, <0 on error). @@ -203,8 +257,12 @@ int platform_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms); void platform_console_write_line(const char *line); /** - * @brief Generic persistent key/value blob storage (NVS-backed). + * @brief 基于 NVS 的通用持久化键值存储原语。 + * Generic persistent key/value blob storage (NVS-backed). * + * platform 只拥有无业务语义的存储能力,上层负责把类型化记录序列化为字节。 + * `ns`/`key` 遵循 NVS 的15字符限制;写操作提交后才算成功,读取缺失返回 + * `ESP_ERR_NOT_FOUND`,缓冲不足返回 `ESP_ERR_NVS_INVALID_LENGTH`,删除具有幂等性。 * Platform owns the storage primitive and stays semantics-free: upper layers * serialize their own typed records into opaque bytes. `ns`/`key` follow NVS * limits (<=15 chars). Semantics: @@ -234,9 +292,13 @@ esp_err_t platform_storage_get_string(const char *ns, size_t *out_len_actual); esp_err_t platform_storage_erase(const char *ns, const char *key); +/* 时间接口只负责时区和系统时钟同步,不承载业务日期或日历规则。 */ /** - * @brief Wall-clock time synchronization (SNTP) primitives. + * @brief 系统墙上时钟和 SNTP 同步原语。 + * Wall-clock time synchronization (SNTP) primitives. * + * 设置时区不会启动 SNTP;sync_start 应在网络就绪后调用且可重复调用; + * is_synced 通过合理的时间下界判断系统时钟是否至少成功同步过一次。 * - `platform_time_set_timezone()` applies the process-local POSIX time zone * without starting SNTP. Call it during boot before local time is rendered. * - `platform_time_sync_start()` is idempotent: it sets the process time zone @@ -249,9 +311,13 @@ esp_err_t platform_time_set_timezone(const char *posix_tz); esp_err_t platform_time_sync_start(const char *sntp_server, const char *posix_tz); bool platform_time_is_synced(void); +/* HTTPS 层负责 TLS 和连接资源生命周期,HTTP 状态码仍需由调用方判断。 */ /** - * @brief POST a JSON body over HTTPS with bounded caller-owned response storage. + * @brief 使用调用方有界响应缓冲执行 HTTPS JSON POST。 + * POST a JSON body over HTTPS with bounded caller-owned response storage. * + * platform 管理 TLS、连接和客户端资源。一次 HTTP 交换完成即返回 ESP_OK, + * HTTP 状态码仍由调用方检查;传输、分配、响应过大或协议初始化失败返回明确错误。 * Platform owns the outbound HTTPS transport primitive and TLS client resource * lifecycle. Completion of an HTTP exchange returns ESP_OK regardless of status * code; callers inspect `response->status_code` and body semantics. Transport, @@ -261,6 +327,7 @@ bool platform_time_is_synced(void); esp_err_t platform_https_post_json(const platform_https_post_json_request_t *request, platform_https_response_t *response); +/* OTA 全程受 timeout_ms 限制,并在写入完成后校验长度与 SHA-256。 */ esp_err_t platform_ota_install_image(const char *url, const uint8_t expected_sha256[32], uint64_t expected_size, @@ -275,26 +342,34 @@ esp_err_t platform_ota_get_memory_headroom(uint32_t *free_internal, void platform_restart(void); bool platform_https_is_connect_error(esp_err_t err); -/** Start a generic single-service BLE GATT peripheral. */ +/* BLE 服务的启动、通知和停止共同构成平台拥有的 NimBLE 生命周期。 */ +/** 启动通用单服务 BLE GATT 外设。 / Start a generic single-service BLE GATT peripheral. */ esp_err_t platform_ble_gatt_server_start(const platform_ble_gatt_server_config_t *config); -/** Send a payload to a subscribed GATT notification characteristic. */ +/** 向已订阅的 GATT 通知特征发送数据。 / Send a payload to a subscribed GATT notification characteristic. */ esp_err_t platform_ble_gatt_server_notify(platform_ble_gatt_notify_target_t target, const uint8_t *data, size_t data_len); /** + * 停止 platform 拥有的 NimBLE 运行时。正超时限制等待时间;零表示仅请求协作式 + * 清理并立即返回。 + * * Stop the platform-owned NimBLE runtime. A positive timeout bounds the wait; * zero requests cooperative teardown without waiting. */ esp_err_t platform_ble_gatt_server_stop(int timeout_ms); -/** Read the running application version into caller-owned storage. */ +/** 将当前固件版本复制到调用方缓冲区。 / Read the running application version into caller-owned storage. */ esp_err_t platform_read_firmware_version(char *out, size_t out_len); +/* 步进电机旋转为同步调用;超时只限制本次运动,不留下后台运动任务。 */ /** - * @brief Rotate stepper synchronously with a bounded timeout. + * @brief 在有界超时内同步旋转步进电机。 + * Rotate stepper synchronously with a bounded timeout. * + * 语义:非正超时不设置限制;正超时内无法完成则返回 ESP_ERR_TIMEOUT;零角度 + * 直接成功且不操作硬件。函数返回后不会留下后台旋转任务。 * Semantics: * - `timeout_ms <= 0`: no timeout limit is enforced. * - `timeout_ms > 0`: returns `ESP_ERR_TIMEOUT` if the move cannot complete @@ -305,26 +380,31 @@ esp_err_t platform_stepper_rotate_angle_with_accel_timeout(float angle, int target_delay_us, int timeout_ms); /** - * @brief Release stepper holding current with bounded lock acquisition. + * @brief 在有界锁等待内释放步进电机保持电流。 + * Release stepper holding current with bounded lock acquisition. * + * 负超时无效,零表示非阻塞尝试,正数限制等待当前运动结束的时间。 * A negative timeout is invalid, zero is non-blocking, and a positive value * bounds the wait for an in-progress movement to finish. */ esp_err_t platform_stepper_release_hold(int timeout_ms); void platform_stepper_power_off(void); -/** Return true when this boot was caused by a configured deep-sleep wake source. */ +/** 当前启动由已配置深睡唤醒源触发时返回 true。 / Return true for a configured deep-sleep wake. */ bool platform_power_woke_from_deep_sleep(void); -/** Release board-output holds after a deep-sleep wake is accepted. */ +/** 接受深睡唤醒后释放板级输出保持。 / Release board-output holds after deep-sleep wake. */ void platform_power_release_deep_sleep_holds(void); /** + * 配置低电平有效的充电器唤醒源和可选按键唤醒源后进入深睡;成功时不返回。 + * * Enter deep sleep after configuring active-low charger and optional button * wake GPIOs. A successful call does not return. */ esp_err_t platform_power_enter_deep_sleep(int charger_gpio, int button_gpio); +/* platform 只报告原始充电和电压质量,电量等级/SOC 语义由 domain 层计算。 */ typedef enum { PLATFORM_BATTERY_CHARGE_UNKNOWN = 0, PLATFORM_BATTERY_CHARGE_NOT_CHARGING, @@ -340,10 +420,13 @@ typedef struct { bool cooldown_active; } platform_battery_sample_t; -/** Initialize the board battery ADC and TP4057 status input. Idempotent. */ +/** 幂等初始化电池 ADC 和 TP4057 状态输入。 / Idempotently initialize battery sensing. */ esp_err_t platform_battery_init(void); /** + * 读取一次裁剪均值电池电压和负载质量快照。即使返回成功,电气读数不合理时 + * voltage_valid 仍可能为 false;此时不得解释为空电池。 + * * Read one trimmed-mean battery voltage sample and a load-quality snapshot. * A successful call can still return voltage_valid=false for an implausible * electrical reading; callers must not interpret such a sample as an empty @@ -354,12 +437,16 @@ esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample); esp_err_t platform_configure_digital_input(const platform_digital_input_config_t *config); bool platform_is_digital_input_active(const platform_digital_input_config_t *config); +/* 行为钩子用于隔离 Wi-Fi 门户与上层唤醒、语音提示实现。 */ void platform_behavior_set_keep_awake(bool keep_awake); esp_err_t platform_behavior_play_prompt_file(const char *path); void platform_wifi_portal_set_behavior_hooks(const platform_wifi_behavior_hooks_t *hooks); esp_err_t platform_wifi_portal_init_defaults(const char *seed_ssid, const char *seed_password); bool platform_wifi_portal_has_saved_credentials(void); /** + * 按持久化优先级复制已保存 SSID,不暴露密码。out_ssids 是 capacity 行、每行 + * ssid_stride 字节的调用方缓冲区。 + * * Copy saved SSIDs in persistent priority order without exposing passwords. * `out_ssids` contains `capacity` rows of `ssid_stride` bytes. */ @@ -368,13 +455,16 @@ esp_err_t platform_wifi_portal_get_saved_ssids(char *out_ssids, size_t capacity, size_t *out_count); bool platform_wifi_portal_sta_is_connected(void); -/** Apply standby modem power save, or restore the configured active policy. */ +/** 应用待机 modem 省电或恢复活动策略。 / Apply standby modem power save or restore active policy. */ esp_err_t platform_wifi_portal_set_light_standby(bool standby); esp_err_t platform_wifi_portal_start_config_ap(const char *ssid_prefix); /** - * @brief Connect STA using saved credentials with timeout. + * @brief 在超时范围内使用已保存凭据连接 STA。 + * Connect STA using saved credentials with timeout. * + * 非正超时使用默认10秒;已连接时幂等成功;失败返回明确错误,返回后不会继续 + * 隐藏的破坏性连接操作。 * Semantics: * - `timeout_ms <= 0`: use default timeout (10s). * - Returns `ESP_OK` when already connected (idempotent). @@ -391,9 +481,13 @@ esp_err_t platform_wifi_portal_save_credentials(const char *ssid, const char *pa esp_err_t platform_wifi_portal_clear_saved_credentials(void); bool platform_wifi_connect_error_keeps_credentials(esp_err_t err); +/* LAN HTTP 服务采用单工作任务;回调在锁外执行,避免业务处理阻塞状态锁。 */ /** - * @brief Start the LAN HTTP transport on the connected STA interface. + * @brief 在已连接的 STA 接口上启动局域网 HTTP 传输。 + * Start the LAN HTTP transport on the connected STA interface. * + * 传输层接收有界 GET/POST 请求,在锁外调用处理器,并通过 mDNS 发布 + * `_http._tcp`;正在启动或运行时重复 start 具有幂等性。 * The transport accepts bounded GET/POST requests, invokes the snapshotted * handler outside locks, and advertises `_http._tcp` using mDNS. Repeated start * calls are idempotent while the server is starting or running. @@ -401,8 +495,11 @@ bool platform_wifi_connect_error_keeps_credentials(esp_err_t err); esp_err_t platform_lan_http_server_start(const platform_lan_http_server_config_t *config); /** - * @brief Stop the LAN HTTP transport and release its worker/socket resources. + * @brief 停止 LAN HTTP 传输并释放任务和 socket。 + * Stop the LAN HTTP transport and release its worker/socket resources. * + * 负超时无效;零仅发出协作停止请求;正超时等待到边界后强制删除工作任务; + * 重复停止具有幂等性。 * Semantics: * - `timeout_ms < 0`: invalid argument. * - `timeout_ms == 0`: request cooperative stop and return immediately. @@ -412,9 +509,13 @@ esp_err_t platform_lan_http_server_start(const platform_lan_http_server_config_t esp_err_t platform_lan_http_server_stop(int timeout_ms); esp_err_t platform_lan_http_server_get_state(platform_lan_http_server_state_t *out_state); +/* 多模态公开接口是 domain 适配器使用的底层原语,不暴露会话业务语义。 */ /** - * @brief Low-level multimodal transport primitive used by the domain adapter. + * @brief domain 适配器使用的底层多模态传输原语。 + * Low-level multimodal transport primitive used by the domain adapter. * + * 跨层会话契约属于 domain;这些回调保持 platform 形状,并只在 domain 运行时 + * 适配器中映射一次,编排层不会直接看到内部传输类型。 * The domain facade owns the cross-layer multimodal session contract. These * callbacks intentionally stay platform-shaped and are mapped exactly once in * the domain runtime adapter before orchestration code can observe them. @@ -425,8 +526,10 @@ esp_err_t platform_multimodal_transport_start( const platform_multimodal_transport_callbacks_t *callbacks); /** - * @brief Stop multimodal WebSocket transport with bounded teardown. + * @brief 以有界方式停止多模态 WebSocket 并清理资源。 + * Stop multimodal WebSocket transport with bounded teardown. * + * 负超时无效;零只请求协作停止;正超时结束时强制删除所属任务;重复调用幂等。 * Semantics: * - `timeout_ms < 0`: invalid argument. * - `timeout_ms == 0`: request cooperative stop and return immediately. @@ -439,6 +542,7 @@ esp_err_t platform_multimodal_transport_send_text(const char *label, const char esp_err_t platform_multimodal_transport_send_text_realtime(const char *label, const char *text); esp_err_t platform_multimodal_transport_send_binary_realtime(const uint8_t *data, size_t len); +/* 音频上行负责采集与编码,下行负责 Opus 解码和播放;两条链路可独立停止。 */ esp_err_t platform_multimodal_audio_init(void); void platform_multimodal_audio_deinit(void); esp_err_t platform_multimodal_audio_start_uplink( @@ -446,8 +550,10 @@ esp_err_t platform_multimodal_audio_start_uplink( void *user_data); /** - * @brief Stop multimodal uplink capture/stream worker with bounded teardown. + * @brief 以有界方式停止多模态上行采集与流式发送任务。 + * Stop multimodal uplink capture/stream worker with bounded teardown. * + * 负超时无效;零只请求协作停止;正超时结束时强制清理所属任务;重复调用幂等。 * Semantics: * - `timeout_ms < 0`: invalid argument. * - `timeout_ms == 0`: request cooperative stop and return immediately. @@ -458,15 +564,19 @@ esp_err_t platform_multimodal_audio_stop_uplink(int timeout_ms); bool platform_multimodal_audio_is_uplink_running(void); bool platform_multimodal_audio_is_initialized(void); /** - * @brief Gate the speaker power amplifier without stopping microphone capture. + * @brief 不停止麦克风采集,仅控制扬声器功放开关。 + * Gate the speaker power amplifier without stopping microphone capture. * + * 关闭时先静音 codec 再关闭 PA;开启时恢复 PA,但保持静音直到播放真正开始。 * Disabling first mutes the codec output and then powers down the speaker PA. * Enabling restores the PA while leaving output muted until playback resumes. */ esp_err_t platform_multimodal_audio_set_speaker_output_enabled(bool enabled); /** - * @brief Register the single mono PCM observer for the active capture path. + * @brief 为当前采集路径注册唯一的单声道 PCM 观察者。 + * Register the single mono PCM observer for the active capture path. * + * 回调在 platform 采集任务中同步执行,但不持有注册锁;回调必须有界且不可阻塞。 * The callback runs synchronously in the platform capture task, outside the * registration lock, and must remain bounded and non-blocking. */ @@ -474,8 +584,10 @@ esp_err_t platform_multimodal_audio_register_pcm_observer( platform_multimodal_audio_pcm_observer_t observer, void *user_data); /** - * @brief Remove a matching PCM observer and wait for in-flight callbacks. + * @brief 移除匹配的 PCM 观察者并等待正在执行的回调结束。 + * Remove a matching PCM observer and wait for in-flight callbacks. * + * 负超时无效,零只执行非阻塞的排空检查。 * Negative timeout is invalid. Zero performs a non-blocking drain check. */ esp_err_t platform_multimodal_audio_unregister_pcm_observer( @@ -491,26 +603,34 @@ esp_err_t platform_multimodal_audio_register_downstream_playback_callbacks( esp_err_t platform_init_lcd_device(void); +/* LCD 面板访问集中在渲染任务中,其他任务通过请求和状态覆盖层间接更新画面。 */ /** - * @brief Set LCD backlight brightness using PWM. + * @brief 使用 PWM 设置 LCD 背光亮度。 + * Set LCD backlight brightness using PWM. * - * @param brightness_percent Brightness from 0 (off) to 100 (full). - * @return ESP_OK on success, ESP_ERR_INVALID_ARG outside the supported range, + * @param brightness_percent 亮度百分比,0为关闭、100为最亮。 + * Brightness from 0 (off) to 100 (full). + * @return 成功返回 ESP_OK;超出范围返回 ESP_ERR_INVALID_ARG;LCD 未初始化返回 + * ESP_ERR_INVALID_STATE。ESP_OK on success, ESP_ERR_INVALID_ARG outside the supported range, * or ESP_ERR_INVALID_STATE before LCD initialization. */ esp_err_t platform_lcd_set_backlight_brightness(uint8_t brightness_percent); /** - * @brief Start LCD render loop. + * @brief 启动 LCD 渲染循环。 + * Start LCD render loop. * + * 已运行时调用为空操作,具有幂等性;通过 stop 接口请求终止。 * Calling this API while the render loop is already running is a no-op (idempotent). * Use `platform_stop_lcd_render_loop()` to request termination. */ void platform_start_lcd_render_loop(void); /** - * @brief Stop LCD render loop. + * @brief 停止 LCD 渲染循环。 + * Stop LCD render loop. * + * 负超时无效;零只请求协作停止;正超时结束后强制删除渲染任务;重复调用幂等。 * Semantics: * - `timeout_ms < 0`: invalid argument. * - `timeout_ms == 0`: request cooperative stop and return immediately. @@ -519,11 +639,18 @@ void platform_start_lcd_render_loop(void); */ esp_err_t platform_stop_lcd_render_loop(int timeout_ms); +/* + * presentation 的固定容量用于在请求入队时深拷贝调用方数据,避免渲染任务持有 + * 外部指针。提高这些上限会增大每个内部请求对象及任务栈/队列拷贝成本。 + * Fixed presentation capacities allow requests to deep-copy caller data before + * queuing. Raising them increases every internal request object's memory cost. + */ #define PLATFORM_LCD_PRESENTATION_FRAME_ORDER_MAX 16 #define PLATFORM_LCD_PRESENTATION_ASSET_DIR_MAX 32 #define PLATFORM_LCD_PRESENTATION_STEP_MAX 12 #define PLATFORM_LCD_PRESENTATION_NO_ICON 0xFFFFU +/* presentation 仅描述播放方式;具体表情含义和资源映射属于 domain 层。 */ typedef enum { PLATFORM_LCD_PRESENTATION_IDLE = 0, PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, @@ -560,17 +687,24 @@ typedef struct { bool charging_visible; } platform_lcd_status_overlay_t; +/* 状态覆盖层是持久状态,不会随一次 presentation 播放结束而自动清除。 */ /** - * @brief Set persistent LCD status overlays composited over presentation frames. + * @brief 设置持续叠加在 presentation 画面上的 LCD 状态图层。 + * Set persistent LCD status overlays composited over presentation frames. * + * 状态会同步复制并持续有效,直到被下一次调用替换;PNG 解码和面板访问只由 + * 渲染任务执行。 * The state is copied synchronously and remains active until replaced. The * render task owns all PNG decoding and panel access. */ esp_err_t platform_lcd_set_status_overlay(const platform_lcd_status_overlay_t *overlay); /** - * @brief Request a LCD presentation playback. + * @brief 请求播放一个 LCD presentation。 + * Request a LCD presentation playback. * + * 非阻塞;LCD 未初始化或停止中返回状态错误;请求数据在调用期间深拷贝;高优先级 + * 可中断请求会替换当前播放;与当前或待处理请求相同时会合并并返回成功。 * Semantics: * - Non-blocking. * - Returns `ESP_ERR_INVALID_STATE` if the LCD is not initialized or is stopping. @@ -582,8 +716,11 @@ esp_err_t platform_lcd_set_status_overlay(const platform_lcd_status_overlay_t *o esp_err_t platform_lcd_request_presentation(const platform_lcd_presentation_request_t *request); /** - * @brief Preload the decoded assets for a LCD presentation without changing display state. + * @brief 预解码 LCD presentation 资源,但不改变当前显示状态。 + * Preload decoded assets without changing the current display state. * + * 请求在渲染任务中执行,调用方阻塞到完成或内部安全超时;当前仅支持资源序列; + * 内存不足时渲染任务可以淘汰较旧的缓存资源。 * Semantics: * - Queues the preload on the LCD render task and blocks until it completes or * the internal safety timeout elapses. @@ -598,8 +735,10 @@ esp_err_t platform_lcd_request_presentation(const platform_lcd_presentation_requ esp_err_t platform_lcd_preheat_presentation(const platform_lcd_presentation_request_t *request); /** - * @brief Read the LCD presentation completion counter. + * @brief 读取 LCD presentation 完成计数器。 + * Read the LCD presentation completion counter. * + * 每当有限资源序列完成配置轮次,计数递增。提交前读取快照,再把快照传给等待接口。 * The counter increments each time a finite asset-sequence presentation * finishes its configured cycles. Snapshot it before queuing a finite * presentation, then pass the snapshot to @@ -608,8 +747,11 @@ esp_err_t platform_lcd_preheat_presentation(const platform_lcd_presentation_requ uint32_t platform_lcd_presentation_completion_count(void); /** - * @brief Block until a finite asset-sequence presentation completes. + * @brief 阻塞等待有限资源序列 presentation 完成。 + * Block until a finite asset-sequence presentation completes. * + * 等待计数超过 since_count 或 timeout_ms 到期;零超时只轮询一次。无限循环的 + * presentation 不会完成,因此等待必然超时。 * Semantics: * - Blocks the calling task until the completion * counter advances past `since_count`, or `timeout_ms` elapses. diff --git a/components/platform/internal/lcd_direct.h b/components/platform/internal/lcd_direct.h index adba895..c142e20 100644 --- a/components/platform/internal/lcd_direct.h +++ b/components/platform/internal/lcd_direct.h @@ -9,6 +9,11 @@ extern "C" { #endif +/* + * LCD 直接驱动的内部接口。 + * 面板初始化、渲染任务及 SPI 访问均由本模块独占;platform facade 只做参数透传, + * 不复制驱动状态。停止接口采用有界等待,presentation 请求在返回前完成深拷贝。 + */ esp_err_t lcd_direct_init_device(void); esp_err_t lcd_direct_set_backlight_brightness(uint8_t brightness_percent); void lcd_direct_start_render_loop(void); diff --git a/components/platform/internal/lcd_direct_assets.h b/components/platform/internal/lcd_direct_assets.h index 459165f..2537cef 100644 --- a/components/platform/internal/lcd_direct_assets.h +++ b/components/platform/internal/lcd_direct_assets.h @@ -7,14 +7,17 @@ extern "C" { #endif +/* RGB565 图片描述符,实际像素由渲染任务根据路径加载和解码。 */ typedef struct { const char *path; } lcd_direct_image_t; +/* 图标描述符与普通帧分离,便于在主画面之上进行状态叠加。 */ typedef struct { const char *path; } lcd_direct_icon_t; +/* 默认图标序列携带绘制坐标,返回的数组由静态资源表持有。 */ typedef struct { uint16_t icon_id; uint16_t x; @@ -26,6 +29,7 @@ typedef struct { #define LCD_DIRECT_ICON_CHARGING 5U #define LCD_DIRECT_ICON_LOW_BATTERY 6U +/* 查找函数返回只读静态描述符;目录、序号或图标不存在时返回 NULL。 */ const lcd_direct_image_t *lcd_direct_default_image(void); const lcd_direct_image_t *lcd_direct_asset_frame(const char *asset_dir, uint8_t frame_index); const lcd_direct_icon_t *lcd_direct_icon(uint16_t icon_id); diff --git a/components/platform/internal/multimodal/audio_pipeline.h b/components/platform/internal/multimodal/audio_pipeline.h index 1fa691a..6cc9e93 100644 --- a/components/platform/internal/multimodal/audio_pipeline.h +++ b/components/platform/internal/multimodal/audio_pipeline.h @@ -10,7 +10,9 @@ extern "C" { #endif +/* 上行回调接收已经编码的 Opus 包;返回错误会被采集任务记录并按策略处理。 */ typedef esp_err_t (*multimodal_audio_uplink_cb_t)(const uint8_t *data, size_t len, void *ctx); +/* PCM 观察者运行在采集任务上下文,必须有界且不可阻塞。 */ typedef void (*multimodal_audio_pcm_observer_t)(const int16_t *pcm, size_t samples, uint32_t sample_rate_hz, @@ -23,6 +25,7 @@ typedef enum { MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STALLED, } multimodal_audio_downstream_playback_event_type_t; +/* generation 将开始、完成、停止或卡顿事件绑定到唯一一次下行响应。 */ typedef struct { multimodal_audio_downstream_playback_event_type_t type; uint32_t playback_generation; @@ -35,6 +38,7 @@ typedef void (*multimodal_audio_downstream_playback_callback_t)( esp_err_t multimodal_audio_init(void); void multimodal_audio_deinit(void); +/* 上行和下行工作任务共享 codec/I2S 资源,但生命周期和停止请求彼此独立。 */ esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *cb_ctx); esp_err_t multimodal_audio_stop_uplink(int timeout_ms); bool multimodal_audio_is_uplink_running(void); @@ -48,6 +52,7 @@ esp_err_t multimodal_audio_unregister_pcm_observer( void *observer_ctx, int timeout_ms); +/* 下行响应先 begin 建代、持续送入 Opus 包,最后 finish 标记服务端数据结束。 */ esp_err_t multimodal_audio_handle_downstream_opus(const uint8_t *data, size_t len); esp_err_t multimodal_audio_begin_downstream_response(uint32_t *out_playback_generation); esp_err_t multimodal_audio_finish_downstream_response(void); diff --git a/components/platform/internal/multimodal/transport_ws.h b/components/platform/internal/multimodal/transport_ws.h index bf805c3..87eb3e4 100644 --- a/components/platform/internal/multimodal/transport_ws.h +++ b/components/platform/internal/multimodal/transport_ws.h @@ -9,6 +9,7 @@ extern "C" { #endif +/* 仅描述 WebSocket 传输事件,不携带 domain 层的业务会话语义。 */ typedef enum { MULTIMODAL_WS_EVENT_CONNECTED = 0, MULTIMODAL_WS_EVENT_DISCONNECTED, @@ -17,6 +18,7 @@ typedef enum { MULTIMODAL_WS_EVENT_BINARY_MESSAGE, } multimodal_ws_event_type_t; +/* data 指针只在事件回调期间有效,需要异步处理时必须复制。 */ typedef struct { multimodal_ws_event_type_t type; const uint8_t *data; @@ -26,6 +28,7 @@ typedef struct { typedef void (*multimodal_ws_event_callback_t)(const multimodal_ws_event_t *event, void *user_data); +/* start 复制鉴权信息和回调;stop 负责回收 client、队列及全部工作任务。 */ esp_err_t multimodal_ws_start(const char *bearer_token, const char *workspace_id, multimodal_ws_event_callback_t cb, @@ -33,6 +36,7 @@ esp_err_t multimodal_ws_start(const char *bearer_token, esp_err_t multimodal_ws_stop(int timeout_ms); bool multimodal_ws_is_connected(void); +/* 普通文本允许内部重试;realtime 接口使用更短锁等待和发送超时以保护实时性。 */ esp_err_t multimodal_ws_send_text(const char *label, const char *text); esp_err_t multimodal_ws_send_text_realtime(const char *label, const char *text); esp_err_t multimodal_ws_send_binary_realtime(const uint8_t *data, size_t len); diff --git a/components/platform/internal/platform_battery_internal.h b/components/platform/internal/platform_battery_internal.h index ed70499..8c15099 100644 --- a/components/platform/internal/platform_battery_internal.h +++ b/components/platform/internal/platform_battery_internal.h @@ -2,6 +2,10 @@ #include +/* + * 高负载来源使用独立引用计数:同一来源允许嵌套 begin/end,只有全部结束后 + * 才进入冷却窗口。该状态用于标记采样质量,不直接计算电量百分比。 + */ typedef enum { PLATFORM_BATTERY_LOAD_AUDIO = 0, PLATFORM_BATTERY_LOAD_STEPPER, diff --git a/components/platform/internal/platform_internal.h b/components/platform/internal/platform_internal.h index 927af3f..77e30fb 100644 --- a/components/platform/internal/platform_internal.h +++ b/components/platform/internal/platform_internal.h @@ -10,13 +10,16 @@ #include "esp_err.h" #include "platform.h" +/* platform facade 以下的板级驱动入口,不允许被上层组件直接包含。 */ esp_err_t platform_stepper_driver_init(void); +/* 旋转调用同步占有运动互斥锁,返回时不会残留后台运动。 */ esp_err_t platform_stepper_driver_rotate_angle_with_accel(float angle, int target_delay_us, int timeout_ms); esp_err_t platform_stepper_driver_release_hold(int timeout_ms); void platform_stepper_driver_power_off(void); void platform_power_hold_init(void); +/* 初始化板级灯带和脉冲输出任务;重复调用由实现保证幂等。 */ esp_err_t platform_io_effects_init(void); esp_err_t platform_digital_input_driver_configure(const platform_digital_input_config_t *config); bool platform_digital_input_driver_is_active(const platform_digital_input_config_t *config); diff --git a/components/platform/internal/power_manager.h b/components/platform/internal/power_manager.h index fcf1d70..2c9d388 100644 --- a/components/platform/internal/power_manager.h +++ b/components/platform/internal/power_manager.h @@ -6,6 +6,10 @@ extern "C" { #endif +/* + * 更新“保持唤醒”状态。true 阻止电源保持 GPIO 进入关断态,false 允许恢复 + * 低功耗状态;调用方不应直接操作对应 GPIO。 + */ void power_manager_set_keep_awake(bool keep_awake); #ifdef __cplusplus diff --git a/components/platform/internal/voice_prompt.h b/components/platform/internal/voice_prompt.h index 85f4362..181b12c 100644 --- a/components/platform/internal/voice_prompt.h +++ b/components/platform/internal/voice_prompt.h @@ -6,6 +6,10 @@ extern "C" { #endif +/* + * 同步播放 LittleFS 中的 Opus 提示音。函数负责分帧、下行播放会话和完成等待, + * 返回时文件句柄已关闭;路径解析和播放超时均在实现内处理。 + */ esp_err_t voice_prompt_play_file(const char *path); #ifdef __cplusplus diff --git a/components/platform/internal/wifi_portal.h b/components/platform/internal/wifi_portal.h index f810275..e89e56b 100644 --- a/components/platform/internal/wifi_portal.h +++ b/components/platform/internal/wifi_portal.h @@ -8,6 +8,11 @@ extern "C" { #endif +/* + * Wi-Fi 门户内部契约:封装第三方 wifi-manager、凭据优先级和连接等待。 + * 所有连接 API 都在调用者给定的超时内结束,返回后不启动隐藏的破坏性重试。 + * 行为钩子只用于唤醒保持和提示音,具体连接策略仍由 control_plane 拥有。 + */ typedef struct { void (*set_keep_awake)(bool keep_awake); esp_err_t (*play_prompt_file)(const char *path); diff --git a/components/platform/src/lcd_direct.c b/components/platform/src/lcd_direct.c index 6a93e21..ad607dc 100644 --- a/components/platform/src/lcd_direct.c +++ b/components/platform/src/lcd_direct.c @@ -1,8 +1,8 @@ /* - * Direct ST7789P3 LCD backend. + * ST7789P3 LCD 直接驱动后端。 * - * This backend owns the low-level panel initialization, a serialized render - * loop, and a minimal presentation queue. + * 本模块独占面板底层初始化和 SPI 访问,并通过串行渲染循环处理最小化的 + * presentation 队列;调用方不得绕过该任务直接操作面板。 */ #include "lcd_direct.h" @@ -44,14 +44,23 @@ #define LCD_DIRECT_WIDTH LCD_DIRECT_NATIVE_WIDTH #define LCD_DIRECT_HEIGHT LCD_DIRECT_NATIVE_HEIGHT #endif +/* + * 每次只合成并传输8行,用较小 DMA 缓冲换取更多 SPI 提交次数;100ms 是单批传输 + * 故障边界。预热允许5秒是因为首次 PNG 文件读取和解码远慢于缓存命中。 + */ #define LCD_DIRECT_ROWS_PER_TRANSFER 8U #define LCD_DIRECT_PIXELS_PER_TRANSFER (LCD_DIRECT_WIDTH * LCD_DIRECT_ROWS_PER_TRANSFER) #define LCD_DIRECT_PRESENTATION_WAIT_POLL_MS 20U #define LCD_DIRECT_TRANSFER_TIMEOUT_MS 100U #define LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS 5000U +/* + * PNG 缓存固定6项并使用 LRU 淘汰,限制 PSRAM 峰值;边界测试恰好覆盖一次完整 + * 缓存命中周期。路径上限同时约束缓存键和 LittleFS 资源路径复制。 + */ #define LCD_DIRECT_FRAME_CACHE_MAX 6U #define LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES LCD_DIRECT_FRAME_CACHE_MAX #define LCD_DIRECT_PATH_MAX 96U +/* 状态图标坐标基于逻辑屏幕尺寸计算,旋转屏幕时无需复制另一套绝对坐标。 */ #define LCD_DIRECT_CHARGING_ICON_WIDTH 50U #define LCD_DIRECT_CHARGING_ICON_X (LCD_DIRECT_WIDTH - LCD_DIRECT_CHARGING_ICON_WIDTH - 8U) #define LCD_DIRECT_CHARGING_ICON_Y 8U @@ -59,15 +68,21 @@ #define LCD_DIRECT_LOW_BATTERY_ICON_HEIGHT 36U #define LCD_DIRECT_LOW_BATTERY_ICON_X ((LCD_DIRECT_WIDTH - LCD_DIRECT_LOW_BATTERY_ICON_WIDTH) / 2U) #define LCD_DIRECT_LOW_BATTERY_ICON_Y (LCD_DIRECT_HEIGHT - LCD_DIRECT_LOW_BATTERY_ICON_HEIGHT - 8U) +/* 基础表情按100ms节奏播放,有限动画末帧额外停留200ms以避免视觉跳变过快。 */ #define LCD_DIRECT_LAST_FRAME_EXTRA_HOLD_MS 200U #define LCD_DIRECT_BASIC_FACE_FRAME_HOLD_MS 100U #define LCD_DIRECT_BASIC_FACE_LAST_FRAME_HOLD_MS 100U +/* + * 20kHz 背光 PWM 高于常见可听频率;10位占空比分辨率提供平滑亮度变化。 + * 独立背光锁最多等待100ms,防止控制路径因并发更新无限阻塞。 + */ #define LCD_DIRECT_BACKLIGHT_PWM_FREQUENCY_HZ 20000U #define LCD_DIRECT_BACKLIGHT_PWM_DUTY_RESOLUTION LEDC_TIMER_10_BIT #define LCD_DIRECT_BACKLIGHT_PWM_DUTY_MAX ((1U << 10U) - 1U) #define LCD_DIRECT_BACKLIGHT_PWM_TIMER LEDC_TIMER_0 #define LCD_DIRECT_BACKLIGHT_PWM_CHANNEL LEDC_CHANNEL_0 #define LCD_DIRECT_BACKLIGHT_LOCK_TIMEOUT_MS 100U +/* 5x7 点阵放大4倍,24像素步进为字形留出间隔;颜色为 RGB565 白字黑描边。 */ #define LCD_DIRECT_CLOCK_X 12U #define LCD_DIRECT_CLOCK_Y 10U #define LCD_DIRECT_CLOCK_GLYPH_WIDTH 5U @@ -79,6 +94,7 @@ #define LCD_DIRECT_CLOCK_COLOR 0xFFFFU #define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0x0000U +/* ST7789P3 的方向控制(MADCTL)和 RAM 数据格式控制(RAMCTRL)命令号。 */ #define LCD_CMD_MADCTL 0x36 #define LCD_CMD_RAMCTRL 0xB0 @@ -92,6 +108,7 @@ unsigned lodepng_esp_decode32(unsigned char **out, const char *lodepng_esp_error_text(unsigned code); typedef struct { + /* 请求中的外部指针在入队前复制到此结构,调用返回后不再依赖调用方内存。 */ platform_lcd_presentation_kind_t kind; uint8_t priority; bool interrupt_current; @@ -107,6 +124,7 @@ typedef struct { } lcd_direct_request_t; typedef struct lcd_direct_decoded_image_t { + /* 缓存项拥有 pixels/alpha;pinned 项不可被 LRU 淘汰。 */ bool valid; bool alpha_valid; char path[LCD_DIRECT_PATH_MAX]; @@ -119,6 +137,7 @@ typedef struct lcd_direct_decoded_image_t { } lcd_direct_decoded_image_t; typedef struct { + /* 图标优先引用缓存,缓存不足时 owns_transient 标记本次绘制临时所有权。 */ uint32_t completed_frames; uint64_t total_draw_us; uint32_t min_draw_us; @@ -200,6 +219,7 @@ static uint16_t s_line_buffer[LCD_DIRECT_PIXELS_PER_TRANSFER]; static lcd_direct_decoded_image_t s_frame_cache[LCD_DIRECT_FRAME_CACHE_MAX]; static uint32_t s_cache_use_counter; +/* 渲染状态由 s_state_lock 保护,面板和缓存访问则由 s_lock 串行化。 */ static void lcd_direct_signal_completion(void); static void lcd_direct_clock_text(char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]); static void lcd_direct_overlay_clock(uint16_t *pixel_buffer, @@ -211,6 +231,7 @@ static bool IRAM_ATTR lcd_direct_on_color_trans_done(esp_lcd_panel_io_handle_t p esp_lcd_panel_io_event_data_t *event_data, void *user_ctx) { + /* SPI 完成回调位于中断上下文,只释放信号量并按需触发任务切换。 */ (void)panel_io; (void)event_data; (void)user_ctx; @@ -233,6 +254,7 @@ static uint16_t lcd_direct_swap_color(uint16_t color) static esp_err_t lcd_direct_draw_rows(uint16_t row_start, uint16_t row_count) { + /* 每批行发送后等待传输完成,确保静态行缓冲不会在 DMA 使用期间被覆盖。 */ ESP_RETURN_ON_FALSE(s_panel != NULL, ESP_ERR_INVALID_STATE, TAG, "panel is not ready"); ESP_RETURN_ON_FALSE(s_transfer_done_sem != NULL, ESP_ERR_INVALID_STATE, @@ -296,6 +318,7 @@ static uint16_t lcd_direct_blend_rgb565(uint16_t dst, uint16_t src, uint8_t alph static esp_err_t lcd_direct_read_file_psram(const char *path, unsigned char **out_data, size_t *out_size) { + /* 整个 PNG 文件优先读入 PSRAM;任何短读都视为失败并关闭句柄。 */ ESP_RETURN_ON_FALSE(path != NULL && out_data != NULL && out_size != NULL, ESP_ERR_INVALID_ARG, TAG, @@ -356,6 +379,7 @@ static esp_err_t lcd_direct_decode_png(const char *path, bool keep_alpha, lcd_direct_decoded_image_t *out_decoded) { + /* 解码后立即转换为面板原生 RGB565,仅在确有透明像素时保留 alpha 平面。 */ ESP_RETURN_ON_FALSE(path != NULL && out_decoded != NULL, ESP_ERR_INVALID_ARG, TAG, @@ -443,6 +467,7 @@ static esp_err_t lcd_direct_decode_png(const char *path, static lcd_direct_decoded_image_t *lcd_direct_find_cached_image(const char *path, bool require_alpha) { + /* 命中时更新单调递增使用序号,供固定容量缓存执行 LRU 淘汰。 */ if (path == NULL) { return NULL; } @@ -459,6 +484,7 @@ static lcd_direct_decoded_image_t *lcd_direct_find_cached_image(const char *path static lcd_direct_decoded_image_t *lcd_direct_find_cache_slot(void) { + /* 优先空槽,否则淘汰最久未使用且未 pinned 的资源。 */ lcd_direct_decoded_image_t *oldest = NULL; for (size_t i = 0; i < LCD_DIRECT_FRAME_CACHE_MAX; ++i) { if (!s_frame_cache[i].valid) { @@ -481,6 +507,7 @@ static esp_err_t lcd_direct_get_png_image(const char *path, lcd_direct_decoded_image_t **out_image, lcd_direct_decoded_image_t *transient) { + /* 缓存已满且全部固定时,允许调用方提供 transient 承接一次性解码结果。 */ ESP_RETURN_ON_FALSE(path != NULL && out_image != NULL, ESP_ERR_INVALID_ARG, TAG, @@ -579,6 +606,7 @@ static esp_err_t lcd_direct_gpio_init(void) static esp_err_t lcd_direct_panel_init(void) { + /* 面板命令、方向和颜色格式在显示开启前一次性配置完成。 */ spi_bus_config_t bus_config = { .mosi_io_num = CONFIG_TQ_SCREEN_SPI_MOSI_GPIO, .miso_io_num = CONFIG_TQ_SCREEN_SPI_MISO_GPIO, @@ -735,6 +763,7 @@ static size_t lcd_direct_prepare_icons(const lcd_direct_icon_draw_t *icons, lcd_direct_prepared_icon_t *prepared, size_t prepared_capacity) { + /* 绘制前集中解析全部图标,避免逐行合成时反复访问文件系统。 */ if (icons == NULL || icon_count == 0U || prepared == NULL || prepared_capacity == 0U) { return 0; } @@ -836,6 +865,7 @@ static void lcd_direct_overlay_icons(uint16_t *pixel_buffer, const lcd_direct_prepared_icon_t *icons, size_t icon_count) { + /* 只处理当前传输行范围,并依据 alpha 平面与背景执行 RGB565 混合。 */ if (pixel_buffer == NULL || icons == NULL || icon_count == 0U) { return; } @@ -972,6 +1002,7 @@ static void lcd_direct_overlay_clock(uint16_t *pixel_buffer, uint16_t row_count, const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]) { + /* 时钟作为最终覆盖层绘制,保证 presentation 与状态图标不会遮住时间。 */ if (pixel_buffer == NULL || text == NULL) { return; } @@ -996,6 +1027,7 @@ static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image, const lcd_direct_icon_draw_t *icons, size_t icon_count) { + /* 按固定行块合成主图、状态图标和时钟,限制单次 DMA 缓冲占用。 */ ESP_RETURN_ON_FALSE(image != NULL, ESP_ERR_INVALID_ARG, TAG, @@ -1138,6 +1170,7 @@ static bool lcd_direct_preheat_is_pending(void) static bool lcd_direct_take_preheat(lcd_direct_request_t *out_request, uint32_t *out_generation) { + /* 取出请求与标记 in-progress 在同一临界区完成,确保只有渲染任务消费。 */ bool has_request = false; portENTER_CRITICAL(&s_state_lock); if (s_preheat_pending_valid) { @@ -1165,7 +1198,7 @@ static void lcd_direct_complete_preheat(uint32_t generation, esp_err_t result) } } -/* Called only by the LCD render task while it owns s_lock. */ +/* 仅由持有 s_lock 的 LCD 渲染任务调用,预热不会与实际绘制并发访问缓存。 */ static bool lcd_direct_service_preheat(void) { lcd_direct_request_t request = {0}; @@ -1465,6 +1498,7 @@ static esp_err_t lcd_direct_run_boundary_test(const lcd_direct_request_t *reques static esp_err_t lcd_direct_play_asset_sequence(const lcd_direct_request_t *request) { + /* 每帧间隙检查更高优先级请求和停止标志,使长动画可以被及时抢占。 */ ESP_RETURN_ON_FALSE(request != NULL, ESP_ERR_INVALID_ARG, TAG, @@ -1602,6 +1636,7 @@ static esp_err_t lcd_direct_play_basic_face_until_pending(void) static void lcd_direct_signal_completion(void) { + /* 完成计数是事实来源,二值信号量仅用于唤醒等待者,不承担事件计数。 */ portENTER_CRITICAL(&s_state_lock); s_completion_count++; portEXIT_CRITICAL(&s_state_lock); @@ -1612,6 +1647,7 @@ static void lcd_direct_signal_completion(void) static bool lcd_direct_take_pending(lcd_direct_request_t *out) { + /* pending 槽采用移动语义,取出后立即清零,渲染任务独占本地副本。 */ bool has_pending = false; portENTER_CRITICAL(&s_state_lock); if (s_pending_valid) { @@ -1626,6 +1662,7 @@ static bool lcd_direct_take_pending(lcd_direct_request_t *out) esp_err_t lcd_direct_init_device(void) { + /* 同步原语先于 GPIO/SPI 创建,任意异步完成回调到达时所需对象均已存在。 */ if (s_initialized) { return ESP_OK; } @@ -1713,6 +1750,7 @@ esp_err_t lcd_direct_set_backlight_brightness(uint8_t brightness_percent) void lcd_direct_start_render_loop(void) { + /* 当前调用任务本身成为渲染任务,因此此函数在收到停止请求前不会返回。 */ if (!s_initialized) { ESP_LOGW(TAG, "Direct LCD is not initialized, skip render loop"); return; @@ -1810,6 +1848,7 @@ void lcd_direct_start_render_loop(void) esp_err_t lcd_direct_stop_render_loop(int timeout_ms) { + /* 先协作停止;超过边界后删除已记录任务,并取消所有预热等待者。 */ if (timeout_ms < 0) { return ESP_ERR_INVALID_ARG; } @@ -1887,6 +1926,7 @@ esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *ove esp_err_t lcd_direct_request_presentation(const platform_lcd_presentation_request_t *request) { + /* 校验并深拷贝可变数组后再发布 pending,发布完成即不再引用调用方数据。 */ if (!s_initialized || request == NULL) { return ESP_ERR_INVALID_STATE; } @@ -1934,6 +1974,7 @@ esp_err_t lcd_direct_request_presentation(const platform_lcd_presentation_reques esp_err_t lcd_direct_preheat_presentation(const platform_lcd_presentation_request_t *request) { + /* 每次只允许一个同步预热等待者,以 generation 区分迟到的完成信号。 */ if (!s_initialized || request == NULL) { return ESP_ERR_INVALID_STATE; } @@ -2022,6 +2063,7 @@ uint32_t lcd_direct_presentation_completion_count(void) esp_err_t lcd_direct_wait_presentation_completion(uint32_t since_count, uint32_t timeout_ms) { + /* 循环检查单调计数而非相信信号量次数,可正确处理合并唤醒和先完成后等待。 */ if (!s_initialized) { return ESP_ERR_INVALID_STATE; } diff --git a/components/platform/src/lcd_direct_assets.c b/components/platform/src/lcd_direct_assets.c index 25f9497..efd0607 100644 --- a/components/platform/src/lcd_direct_assets.c +++ b/components/platform/src/lcd_direct_assets.c @@ -4,6 +4,10 @@ #define LCD_ASSET_BASE_PATH "/littlefs/lcd/" +/* + * 静态表只保存资源元数据和路径,不在此文件执行 PNG 解码。 + * 资源目录名称是 platform 内部播放原语,表情语义映射仍由 domain 层维护。 + */ static const lcd_direct_image_t s_default_image = { .path = LCD_ASSET_BASE_PATH "buyunqian1.png", }; @@ -254,6 +258,7 @@ static const lcd_direct_image_t *lcd_direct_frame_from_array(const lcd_direct_im size_t frame_count, uint8_t frame_index) { + /* 数组以调用方传入的显式长度为界,避免依赖哨兵项。 */ if (frame_index >= frame_count) { return NULL; } @@ -267,6 +272,7 @@ const lcd_direct_image_t *lcd_direct_default_image(void) const lcd_direct_image_t *lcd_direct_asset_frame(const char *asset_dir, uint8_t frame_index) { + /* 目录名精确匹配;未知目录不回退默认资源,以便暴露配置错误。 */ if (asset_dir == NULL) { return NULL; } @@ -442,6 +448,7 @@ const lcd_direct_image_t *lcd_direct_asset_frame(const char *asset_dir, uint8_t const lcd_direct_icon_t *lcd_direct_icon(uint16_t icon_id) { + /* icon_id 是紧凑表索引;越界时不尝试读取静态数组。 */ if (icon_id >= (sizeof(s_wuxing_icons) / sizeof(s_wuxing_icons[0]))) { return NULL; } diff --git a/components/platform/src/multimodal/audio_pipeline.c b/components/platform/src/multimodal/audio_pipeline.c index cf79852..9371833 100644 --- a/components/platform/src/multimodal/audio_pipeline.c +++ b/components/platform/src/multimodal/audio_pipeline.c @@ -32,6 +32,12 @@ #include "driver/i2s_std.h" #include "driver/i2c_master.h" +/* + * Kconfig 兼容默认值:仅在配置项缺失时生效。后面的编译期约束仍强制语音链路使用 + * 16 kHz、双物理时隙和16位 PCM,因此默认值不等于所有板型都可直接运行。 + * These are compatibility fallbacks for missing Kconfig values; compile-time + * checks below still enforce the audio format required by voice/AEC processing. + */ #ifndef CONFIG_MULTIMODAL_AUDIO_ENABLE_CODEC_PATH #define CONFIG_MULTIMODAL_AUDIO_ENABLE_CODEC_PATH 1 #endif @@ -68,6 +74,7 @@ #define CONFIG_MULTIMODAL_AUDIO_I2S_PORT 0 #endif +/* I2C/I2S GPIO 必须由板级 Kconfig 明确提供,避免错误默认引脚驱动真实硬件。 */ #ifndef CONFIG_MULTIMODAL_AUDIO_I2S_MCLK_GPIO #error "CONFIG_MULTIMODAL_AUDIO_I2S_MCLK_GPIO must be defined in Kconfig" #endif @@ -97,6 +104,10 @@ #endif #ifndef CONFIG_MULTIMODAL_AUDIO_I2S_DMA_DESC_NUM +/* + * I2S DMA 默认4个描述符、每描述符160帧:增加可提高抗调度抖动能力,但会占用更多 + * DMA 内存并增加排队延迟;减少则降低内存占用,但更容易出现采集/播放 underrun。 + */ #define CONFIG_MULTIMODAL_AUDIO_I2S_DMA_DESC_NUM 4 #endif @@ -105,6 +116,7 @@ #endif #ifndef CONFIG_MULTIMODAL_AUDIO_OUT_VOLUME +/* codec 初始增益属于硬件安全基线;调整前需同时检查削波、啸叫和扬声器功率。 */ #define CONFIG_MULTIMODAL_AUDIO_OUT_VOLUME 60 #endif @@ -113,6 +125,10 @@ #endif #ifndef CONFIG_MULTIMODAL_AUDIO_MIC_CHANNEL +/* + * 双时隙输入中,麦克风与 AEC 回采必须使用不同槽位;滤波长度增大可覆盖更长回声, + * 但会增加运算量和工作内存。编译期检查防止两个通道配置为同一槽位。 + */ #define CONFIG_MULTIMODAL_AUDIO_MIC_CHANNEL 0 #endif @@ -129,6 +145,11 @@ #endif #ifndef CONFIG_MULTIMODAL_AUDIO_DOWNSTREAM_IDLE_MUTE_MS +/* + * 下行播放时序权衡:首次积累 PREBUFFER 后开声;断流超过 REBUFFER_GAP 后重新缓冲, + * 最大积累量受 MAX_REBUFFER 限制以避免延迟持续增长;正常排空后等待 IDLE_MUTE + * 再静音以减少爆音,活动响应长时间无数据则按 ACTIVE_STALL 判定卡顿。 + */ #define CONFIG_MULTIMODAL_AUDIO_DOWNSTREAM_IDLE_MUTE_MS 250 #endif @@ -153,6 +174,11 @@ #endif #ifndef CONFIG_MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_TASK_STACK +/* + * 下行任务承担 Opus 解码和 codec 写入,栈下限保护最深调用链。队列长度和单包容量 + * 共同决定最坏内存占用;配置值会被归一化到 MIN/CAP 范围,避免过小不可运行或 + * 过大耗尽 PSRAM。修改时应同时评估端到端延迟和网络突发大小。 + */ #define CONFIG_MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_TASK_STACK 12288 #endif @@ -173,6 +199,10 @@ #define MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_PACKET_CAP_BYTES 8192U #ifndef CONFIG_MULTIMODAL_AUDIO_UPLINK_TASK_STACK +/* + * 上行采集栈包含 I2S、AEC 和 Opus 编码调用链,因此基线大于纯发送任务。发送队列 + * 将网络抖动与实时采集隔离;5ms 入队等待刻意选择“及时丢包”而非阻塞采集。 + */ #define CONFIG_MULTIMODAL_AUDIO_UPLINK_TASK_STACK 32768 #endif @@ -199,6 +229,10 @@ #endif #ifndef CONFIG_MULTIMODAL_AUDIO_INIT_RETRY_COUNT +/* + * 初始化重试前先检查最大连续 DMA/内部内存块,避免在堆碎片严重时构建出半初始化 + * codec。阈值是创建 I2S/codec 控制对象所需的安全水位,不是总空闲内存要求。 + */ #define CONFIG_MULTIMODAL_AUDIO_INIT_RETRY_COUNT 1 #endif @@ -211,6 +245,10 @@ #endif #ifndef CONFIG_MULTIMODAL_OPUS_FRAME_DURATION_MS +/* + * 服务端协议要求 60ms raw-Opus 帧。24kbps、低复杂度和关闭 VBR/DTX/FEC 是当前 + * 实时性与 MCU 负载基线;改变任一项需同步验证服务端解码、包容量和网络带宽。 + */ #define CONFIG_MULTIMODAL_OPUS_FRAME_DURATION_MS 60 #endif @@ -259,6 +297,7 @@ #endif #ifndef CONFIG_MULTIMODAL_AUDIO_TASK_CORE +/* 音频任务默认固定到本地实时核心;负数表示由调度器决定,需重新验证抖动。 */ #define CONFIG_MULTIMODAL_AUDIO_TASK_CORE 1 #endif @@ -268,6 +307,10 @@ #define MULTIMODAL_AUDIO_CORE_ID ((BaseType_t)CONFIG_MULTIMODAL_AUDIO_TASK_CORE) #endif +/* + * 解码缓冲包含最小工作区和保护余量;TX bounce 缓冲用于处理 codec 短写。 + * AEC 缓冲按16字节对齐以满足 DSP/SIMD 访问要求。 + */ #define MULTIMODAL_PCM_SAMPLE_BYTES 2U #define MULTIMODAL_MIN_DECODER_PCM_BUFFER_BYTES 2048U #define MULTIMODAL_DECODER_BUFFER_GUARD_BYTES 512U @@ -280,6 +323,7 @@ static const char *TAG = "multimodal_audio"; static portMUX_TYPE s_audio_runtime_lock = portMUX_INITIALIZER_UNLOCKED; +/* 音频上下行共享 codec/I2S 资源,因此所有生命周期状态集中保存在单一上下文中。 */ typedef struct { const audio_codec_data_if_t *data_if; const audio_codec_ctrl_if_t *ctrl_if; @@ -355,6 +399,7 @@ typedef struct { } multimodal_audio_ctx_t; static multimodal_audio_ctx_t s_audio; +/* 下行播放以 generation 标识会话,终止事件只唤醒等待同一代的调用方。 */ static multimodal_audio_downstream_playback_callback_t s_downstream_playback_cb; static void *s_downstream_playback_cb_ctx; static SemaphoreHandle_t s_downstream_playback_completion_sem; @@ -423,10 +468,17 @@ typedef struct { uint8_t *data; } downstream_packet_t; -/* Split implementation by concern while keeping a single translation unit: - * - helpers: memory, config normalization, utility helpers - * - codec: codec/i2c/i2s/opus setup and teardown - * - runtime: uplink/downlink processing and public APIs +/* + * 实现文件拆分规则: + * - 下列 *_impl.c 不能单独加入 CMake 或独立编译,只能由本文件按当前顺序包含; + * - 它们共享本文件上方定义的 static 类型、状态、宏和前置声明; + * - 拆分仅为了按职责降低阅读长度,不形成新的模块或链接边界; + * - include 顺序有意为 helpers -> codec -> debug -> runtime,后面的实现可以使用 + * 前面已经定义的内部辅助函数,调整顺序前必须检查可见声明和依赖方向。 + * + * Included *_impl.c files are not standalone translation units. They share this + * file's static state and are split only for readability. Their include order is + * dependency-sensitive and must not be changed without reviewing symbol visibility. */ #include "audio_pipeline/audio_pipeline_helpers_impl.c" diff --git a/components/platform/src/multimodal/audio_pipeline/audio_debug_dump_impl.c b/components/platform/src/multimodal/audio_pipeline/audio_debug_dump_impl.c index e3dbfbd..dbc7609 100644 --- a/components/platform/src/multimodal/audio_pipeline/audio_debug_dump_impl.c +++ b/components/platform/src/multimodal/audio_pipeline/audio_debug_dump_impl.c @@ -1,9 +1,10 @@ /* - * Audio debug dump - streams uplink PCM and/or Opus packets over UDP - * to a PC on the same LAN for offline/real-time analysis. - * - * Included by audio_pipeline.c (single translation unit). - * Guarded by CONFIG_MULTIMODAL_AUDIO_DEBUG_DUMP. + * 音频调试转储:通过 UDP 将上行 PCM 和/或 Opus 包发送到同一局域网的电脑, + * 供离线或实时分析。本文件不能单独编译;由 audio_pipeline.c 在 codec 之后、 + * runtime 之前包含,共享 TAG 和音频内部声明。拆分仅为了阅读,include 顺序影响 + * 后续 runtime 对这些静态调试函数的可见性。功能由配置宏完全裁剪。 + * This file is a non-standalone include between codec and runtime. It shares the + * parent translation unit, and its position makes debug helpers visible to runtime. * * PC-side playback: * ffplay -nodisp -f s16le -ar 16000 -ac 1 udp://0.0.0.0:7200 @@ -27,6 +28,7 @@ #define CONFIG_MULTIMODAL_AUDIO_DEBUG_DUMP_OPUS_PORT 7201 #endif +/* 1400字节低于常见以太网 MTU,为 UDP/IP 头留余量以尽量避免 IP 分片。 */ #define AUDIO_DEBUG_DUMP_MAX_UDP_PAYLOAD 1400U static int s_debug_dump_sock = -1; @@ -37,6 +39,7 @@ static struct sockaddr_in s_debug_dump_opus_addr; static esp_err_t audio_debug_dump_init(void) { + /* 使用非阻塞 UDP,调试接收端离线时不得拖慢实时采集链路。 */ if (s_debug_dump_sock >= 0) { return ESP_OK; } @@ -94,6 +97,7 @@ static void audio_debug_dump_deinit(void) static void audio_debug_dump_pcm(const int16_t *pcm, size_t pcm_bytes) { + /* 按保守 UDP payload 大小分片,降低 IP 分片和局域网丢包概率。 */ if (s_debug_dump_sock < 0 || pcm == NULL || pcm_bytes == 0U) { return; } @@ -114,6 +118,7 @@ static void audio_debug_dump_pcm(const int16_t *pcm, size_t pcm_bytes) static void audio_debug_dump_opus(const uint8_t *data, size_t len) { + /* 每个 Opus 包保持单 datagram 边界,便于电脑端恢复编码帧。 */ #if CONFIG_MULTIMODAL_AUDIO_DEBUG_DUMP_OPUS_ENABLE if (s_debug_dump_sock < 0 || data == NULL || len == 0U) { return; @@ -127,7 +132,7 @@ static void audio_debug_dump_opus(const uint8_t *data, size_t len) #endif } -#else /* !CONFIG_MULTIMODAL_AUDIO_DEBUG_DUMP */ +#else /* 未启用调试转储时提供零开销内联空实现,调用点无需条件编译。 */ static inline esp_err_t audio_debug_dump_init(void) { diff --git a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c index bd324fe..87307ab 100644 --- a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c +++ b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c @@ -1,3 +1,11 @@ +/* + * codec/I2C/I2S/Opus 资源构建与逆序清理。 + * 不能单独编译;由 audio_pipeline.c 在 helpers 之后包含,共享 s_audio 和内部类型。 + * 拆分仅为了阅读,runtime 依赖本文件定义的硬件/编解码初始化与清理函数,因此 + * include 顺序不可随意调整。每个 cleanup 都可处理部分初始化状态。 + * This non-standalone implementation shares audio_pipeline.c static state and + * must remain after helpers and before runtime. + */ static void cleanup_codec_path(multimodal_audio_ctx_t *ctx) { if (!ctx) { @@ -36,6 +44,7 @@ static void cleanup_codec_path(multimodal_audio_ctx_t *ctx) static void cleanup_opus_path(multimodal_audio_ctx_t *ctx) { + /* 编解码器和关联缓冲按所有权逐项释放,并将句柄清零以支持重复清理。 */ if (!ctx) { return; } @@ -121,6 +130,7 @@ static void deinit_buses(multimodal_audio_ctx_t *ctx) static esp_err_t init_i2c_bus(multimodal_audio_ctx_t *ctx) { + /* 先建立总线,再探测候选地址;探测地址和 codec 控制地址分别保存。 */ if (!ctx) { return ESP_ERR_INVALID_ARG; } @@ -189,6 +199,7 @@ static esp_err_t init_i2c_bus(multimodal_audio_ctx_t *ctx) static esp_err_t init_i2s_bus(multimodal_audio_ctx_t *ctx) { + /* TX/RX 共用时钟配置,但保留独立通道句柄以支持全双工采集与播放。 */ if (!ctx) { return ESP_ERR_INVALID_ARG; } @@ -264,6 +275,7 @@ static esp_err_t init_i2s_bus(multimodal_audio_ctx_t *ctx) static esp_err_t create_codec_path(multimodal_audio_ctx_t *ctx) { + /* 数据、控制和 GPIO 接口均由 codec device 引用,设备销毁前不得提前释放。 */ if (!ctx) { return ESP_ERR_INVALID_ARG; } @@ -451,6 +463,7 @@ static esp_err_t open_codec_device(multimodal_audio_ctx_t *ctx) static esp_err_t ensure_downstream_buffers(multimodal_audio_ctx_t *ctx, size_t pcm_mono_bytes_needed) { + /* 根据解码帧动态扩容;只有新缓冲分配成功后才替换旧指针。 */ if (!ctx || pcm_mono_bytes_needed == 0) { return ESP_ERR_INVALID_ARG; } @@ -508,6 +521,7 @@ static esp_err_t ensure_downstream_buffers(multimodal_audio_ctx_t *ctx, size_t p static esp_err_t init_opus_codec(multimodal_audio_ctx_t *ctx) { + /* 编码器和解码器必须成对就绪,任一失败都会回滚整个 Opus 路径。 */ if (!ctx) { return ESP_ERR_INVALID_ARG; } @@ -675,6 +689,7 @@ static void expand_mono_to_multichannel_16(const int16_t *input, int16_t *output, size_t frames) { + /* 单声道解码结果复制到 codec 输出的每个声道,不在此处改变采样幅度。 */ if (!input || !output || channels == 0) { return; } diff --git a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_helpers_impl.c b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_helpers_impl.c index ab197f7..bc52dfa 100644 --- a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_helpers_impl.c +++ b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_helpers_impl.c @@ -1,5 +1,14 @@ +/* + * 音频流水线通用辅助实现。 + * 不能单独编译;由 audio_pipeline.c 首先包含,并共享其 static 类型、宏和状态。 + * 拆分仅为了阅读,当前顺序使 codec/runtime 可以使用这里定义的内存、队列、任务 + * 和配置归一化函数;改变 include 顺序可能导致声明不可见或依赖倒置。 + * This file is included first by audio_pipeline.c, shares its static state, and + * must not be compiled independently. Include order is dependency-sensitive. + */ static bool ensure_downstream_playback_queue_mutex(void) { + /* 首次使用时串行创建互斥锁,避免两个调用方各自创建并泄漏一个实例。 */ if (s_downstream_playback_queue_mutex != NULL) { return true; } @@ -57,6 +66,7 @@ static void unlock_downstream_playback_queue(void) static void *audio_psram_first_malloc(size_t size) { + /* 大块音频缓冲优先放 PSRAM,失败后回退内部 8-bit RAM 保证兼容无 PSRAM 板型。 */ if (size == 0U) { return NULL; } @@ -164,6 +174,7 @@ static void log_heap_snapshot(const char *phase) static bool audio_init_heap_guard_passed(void) { + /* 初始化前预留内部 RAM 安全水位,避免 codec 创建成功后网络栈无可用连续块。 */ size_t largest_dma = heap_caps_get_largest_free_block(MALLOC_CAP_DMA); size_t largest_internal = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); @@ -265,6 +276,7 @@ static esp_audio_err_t open_opus_encoder_with_fallback(multimodal_audio_ctx_t *c bool requested_dtx, bool requested_fec) { + /* 按候选采样率/帧长逐一尝试,成功后将实际参数写回统一上下文。 */ if (!ctx) { return ESP_AUDIO_ERR_INVALID_PARAMETER; } @@ -539,6 +551,7 @@ static TickType_t get_uplink_send_queue_timeout_ticks(void) static QueueHandle_t create_uplink_send_queue(UBaseType_t queue_len, bool *out_with_caps) { + /* 队列控制块与存储区尽量放外部内存,并记录分配方式供销毁路径正确释放。 */ if (out_with_caps != NULL) { *out_with_caps = false; } @@ -672,6 +685,7 @@ static BaseType_t create_audio_task_with_caps(TaskFunction_t task, bool prefer_psram_stack, BaseType_t core_id) { + /* 优先使用带内存能力的静态任务创建;不支持时回退标准 FreeRTOS 动态任务。 */ #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) #if (defined(CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) && CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) || \ defined(CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY) @@ -747,6 +761,7 @@ static void get_i2c_addr_candidates(uint8_t raw, uint8_t *probe_b, uint8_t *ctrl_b) { + /* 同时兼容 Kconfig 中填写 7 位地址和包含 R/W 位的 8 位地址。 */ *probe_a = (uint8_t)(raw & 0x7FU); *ctrl_a = i2c_addr_ctrl_from_7bit(*probe_a); *ctrl_b = (uint8_t)(raw & 0xFEU); diff --git a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c index 3b4622d..061c799 100644 --- a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c +++ b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c @@ -5,6 +5,17 @@ typedef struct { #define MULTIMODAL_AUDIO_UPLINK_STATS_LOG_INTERVAL_MS 5000U +/* + * 音频运行时状态机。 + * 不能单独编译;由 audio_pipeline.c 最后包含,共享前述 helpers/codec 定义及全部 + * static 状态。拆分仅为了阅读,include 顺序决定内部函数和类型是否可见。 + * This file is the final non-standalone include of audio_pipeline.c and depends + * on helper/codec definitions that precede it. + * + * 上行链路:I2S 采集 -> 选取/清理麦克风声道 -> Opus 编码 -> 发送队列。 + * 下行链路:Opus 包队列 -> 解码 -> 预缓冲/恢复判断 -> codec 播放 -> 空闲静音。 + * 所有任务退出路径都负责清除自身句柄,停止 API 使用有界等待并提供兜底清理。 + */ static uint32_t abs_i16_to_u32(int16_t sample) { int32_t value = sample; @@ -195,6 +206,7 @@ static bool audio_consume_first_network_packet(multimodal_audio_ctx_t *ctx, static void notify_pcm_observer(const int16_t *pcm, size_t samples) { + /* in_flight 计数让 unregister 能等待正在执行的观察者,同时回调在锁外运行。 */ multimodal_audio_pcm_observer_t observer = NULL; void *observer_ctx = NULL; @@ -438,6 +450,7 @@ static void free_downstream_packet(downstream_packet_t *packet) static void drain_downstream_playback_queue_handle(QueueHandle_t queue) { + /* 队列保存的是拥有 payload 的指针,排空时必须逐包释放而不能只 reset 队列。 */ if (queue == NULL) { return; } @@ -881,6 +894,7 @@ static esp_err_t encode_and_queue_mono_pcm(multimodal_audio_ctx_t *ctx, const int16_t *pcm, size_t pcm_bytes) { + /* 编码成功只表示生成 Opus 包;队列满时按实时策略丢包,绝不阻塞采集任务。 */ if (!ctx || !pcm || pcm_bytes != ctx->uplink_mono_bytes) { return ESP_ERR_INVALID_ARG; } @@ -944,6 +958,7 @@ static esp_err_t append_and_queue_mono_pcm( const int16_t *pcm, size_t pcm_bytes) { + /* I2S 块可能不对齐 Opus 帧,尾部 PCM 暂存在 fill 缓冲并与下一块拼接。 */ if (ctx == NULL || pcm == NULL || pcm_bytes == 0U || ctx->uplink_mono_buf == NULL || ctx->uplink_mono_buf_capacity_bytes != ctx->uplink_mono_bytes) { @@ -982,6 +997,7 @@ static esp_err_t append_and_queue_mono_pcm( static void uplink_send_task(void *arg) { + /* 网络发送与采集解耦,慢连接只消耗有界队列容量,不拖慢 I2S 读取。 */ (void)arg; multimodal_audio_ctx_t *ctx = &s_audio; @@ -1041,6 +1057,7 @@ static void uplink_send_task(void *arg) static void uplink_capture_task(void *arg) { + /* 采集循环先完成声道映射/AEC,再通知 PCM 观察者并编码网络音频。 */ (void)arg; multimodal_audio_ctx_t *ctx = &s_audio; @@ -1143,6 +1160,7 @@ static void uplink_capture_task(void *arg) static esp_err_t ensure_uplink_worker_tasks(multimodal_audio_ctx_t *ctx) { + /* 发送任务先于采集任务启动;若后者创建失败,立即回收前者及队列。 */ if (ctx == NULL) { return ESP_ERR_INVALID_ARG; } @@ -1208,6 +1226,7 @@ static esp_err_t ensure_uplink_worker_tasks(multimodal_audio_ctx_t *ctx) esp_err_t multimodal_audio_init(void) { + /* 初始化按总线、codec、Opus、缓冲顺序推进,失败统一逆序回滚到干净状态。 */ #if !CONFIG_MULTIMODAL_AUDIO_ENABLE_CODEC_PATH ESP_LOGW(TAG, "Codec path disabled by configuration"); return ESP_ERR_NOT_SUPPORTED; @@ -1340,6 +1359,7 @@ esp_err_t multimodal_audio_init(void) void multimodal_audio_deinit(void) { + /* deinit 先阻止新工作进入,再停止上下行任务,最后释放共享硬件资源。 */ if (!s_audio.initialized && !s_audio.opus_ready) { return; } @@ -1496,6 +1516,7 @@ esp_err_t multimodal_audio_unregister_pcm_observer( esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *cb_ctx) { + /* 回调在任务启动前发布;任一启动失败都会撤销回调,避免半启动状态。 */ if (!s_audio.initialized || !s_audio.codec_dev || !s_audio.opus_enc || !s_audio.uplink_codec_buf || !s_audio.uplink_mono_buf || !s_audio.uplink_opus_buf) { return ESP_ERR_INVALID_STATE; @@ -1540,6 +1561,7 @@ esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *c esp_err_t multimodal_audio_stop_uplink(int timeout_ms) { + /* 先协作请求退出并等待两个任务;超时后保留明确错误而不伪装成已停止。 */ if (timeout_ms < 0) { return ESP_ERR_INVALID_ARG; } @@ -1606,6 +1628,7 @@ static void snapshot_downstream_playback_state(uint32_t *out_generation, static uint32_t advance_downstream_playback_generation_locked(void) { + /* generation 跳过 0,使零值可以持续表示“尚未建立播放会话”。 */ s_audio.downstream_playback_generation++; if (s_audio.downstream_playback_generation == 0U) { s_audio.downstream_playback_generation = 1U; @@ -1618,6 +1641,7 @@ static bool claim_downstream_response_terminal(uint32_t expected_generation, bool invalidate_generation, uint32_t *out_terminal_generation) { + /* 终止事件只能被同一 generation 首次领取,屏蔽迟到包触发的重复完成通知。 */ if (out_terminal_generation == NULL) { return false; } @@ -1696,6 +1720,7 @@ static void dispatch_downstream_playback_event( multimodal_audio_downstream_playback_event_type_t type, uint32_t playback_generation) { + /* 先更新等待者所需的终止快照并释放信号量,再在锁外调用外部回调。 */ bool terminal = type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_COMPLETED || type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STOPPED || type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STALLED; @@ -1838,6 +1863,7 @@ static void abort_downstream_response_after_active_stall(multimodal_audio_ctx_t uint32_t idle_ms, uint32_t expected_generation) { + /* 活跃响应长时间无音频视为卡顿:停止本代播放,但不误终止下一代响应。 */ (void)ctx; uint32_t stopped_generation = 0U; if (!claim_downstream_response_terminal(expected_generation, @@ -1883,6 +1909,7 @@ static esp_err_t set_downstream_output_mute_if_needed(multimodal_audio_ctx_t *ct bool mute, const char *reason) { + /* 缓存 codec 静音状态,只有目标值变化时才进行控制总线写入。 */ if (ctx == NULL || ctx->codec_dev == NULL) { return ESP_ERR_INVALID_STATE; } @@ -1983,6 +2010,7 @@ static void restart_downstream_idle_mute_timer(multimodal_audio_ctx_t *ctx, static void downstream_idle_mute_timer_cb(void *arg) { + /* 定时器回调只做状态判断和轻量控制,不销毁播放任务或队列。 */ multimodal_audio_ctx_t *ctx = (multimodal_audio_ctx_t *)arg; if (ctx == NULL) { return; @@ -2114,6 +2142,7 @@ static esp_err_t ensure_downstream_tx_bounce_buffer(multimodal_audio_ctx_t *ctx) static int write_downstream_pcm_to_codec(multimodal_audio_ctx_t *ctx, const void *data, size_t bytes) { + /* codec 短写通过 bounce 缓冲循环补齐,避免丢失一个解码帧的尾部样本。 */ if (ctx == NULL || ctx->codec_dev == NULL || data == NULL || bytes == 0U || bytes > INT_MAX) { return ESP_CODEC_DEV_INVALID_ARG; } @@ -2233,6 +2262,7 @@ static esp_err_t stop_downstream_idle_mute_task(multimodal_audio_ctx_t *ctx, int static void downstream_playback_task(void *arg) { + /* 预缓冲达到阈值后才取消静音;发生队列断流则重新进入缓冲态以减少爆音。 */ multimodal_audio_ctx_t *ctx = (multimodal_audio_ctx_t *)arg; if (ctx == NULL) { delete_audio_task_with_caps(NULL); @@ -2547,6 +2577,7 @@ static esp_err_t stop_downstream_playback_task(multimodal_audio_ctx_t *ctx, int timeout_ms, bool keep_stopping) { + /* 停止请求、等待和强制删除共享同一超时预算,退出后排空所有未播放包。 */ if (ctx == NULL) { return ESP_ERR_INVALID_ARG; } @@ -2615,6 +2646,7 @@ static esp_err_t stop_downstream_playback_task(multimodal_audio_ctx_t *ctx, static esp_err_t reset_downstream_decoder(multimodal_audio_ctx_t *ctx) { + /* 每个新响应重置 Opus 解码历史,防止上一段语音的状态污染下一段。 */ if (ctx == NULL || ctx->opus_sample_rate == 0U || ctx->opus_frame_duration_ms == 0U) { return ESP_OK; } @@ -2678,6 +2710,7 @@ static esp_err_t handle_downstream_opus_sync(const uint8_t *data, size_t len, uint32_t expected_generation) { + /* 同步解码路径校验输出大小并扩展声道,实际 codec 写入仍由播放任务串行完成。 */ multimodal_audio_ctx_t *ctx = &s_audio; if (!ctx->initialized || !ctx->codec_dev || !ctx->opus_dec) { return ESP_ERR_INVALID_STATE; @@ -2892,6 +2925,7 @@ static esp_err_t handle_downstream_opus_sync(const uint8_t *data, esp_err_t multimodal_audio_handle_downstream_opus(const uint8_t *data, size_t len) { + /* 包被深拷贝进有界队列;调用返回后不再引用 WebSocket 接收缓冲区。 */ multimodal_audio_ctx_t *ctx = &s_audio; if (!ctx->initialized || !ctx->codec_dev || !ctx->opus_dec) { return ESP_ERR_INVALID_STATE; @@ -3003,6 +3037,7 @@ esp_err_t multimodal_audio_handle_downstream_opus(const uint8_t *data, size_t le esp_err_t multimodal_audio_begin_downstream_response( uint32_t *out_playback_generation) { + /* 建立新 generation、清除旧终止快照并启动播放任务,之后才能接收该代数据。 */ multimodal_audio_ctx_t *ctx = &s_audio; if (out_playback_generation == NULL) { return ESP_ERR_INVALID_ARG; @@ -3098,6 +3133,7 @@ esp_err_t multimodal_audio_begin_downstream_response( esp_err_t multimodal_audio_finish_downstream_response(void) { + /* finish 仅声明不会再到包;真正完成需等待队列和 codec 输出全部排空。 */ multimodal_audio_ctx_t *ctx = &s_audio; if (!ctx->initialized || ctx->codec_dev == NULL) { return ESP_ERR_INVALID_STATE; @@ -3129,6 +3165,7 @@ esp_err_t multimodal_audio_wait_downstream_playback_complete( uint32_t playback_generation, int timeout_ms) { + /* 等待前先检查终止快照,避免完成事件先于等待者注册造成永久错过。 */ if (playback_generation == 0U || timeout_ms < 0) { return ESP_ERR_INVALID_ARG; } diff --git a/components/platform/src/multimodal/transport_ws.c b/components/platform/src/multimodal/transport_ws.c index 0cec85a..248d5db 100644 --- a/components/platform/src/multimodal/transport_ws.c +++ b/components/platform/src/multimodal/transport_ws.c @@ -18,6 +18,7 @@ #include "esp_websocket_client.h" #include "soc/soc_caps.h" +/* 连接默认值只在 Kconfig 缺失时启用;URI 和鉴权仍由部署环境及 start 参数决定。 */ #ifndef CONFIG_MULTIMODAL_WS_URI #define CONFIG_MULTIMODAL_WS_URI "wss://dashscope.aliyuncs.com/api-ws/v1/inference" #endif @@ -31,6 +32,10 @@ #endif #ifndef CONFIG_MULTIMODAL_WEBSOCKET_TASK_STACK_BYTES +/* + * client、dispatch、文本和 Opus 处理具有不同调用深度,分别配置栈以避免用最大值 + * 覆盖所有任务。栈优先放 PSRAM,内部 RAM 回退受后面的 MIN/CAP 约束。 + */ #define CONFIG_MULTIMODAL_WEBSOCKET_TASK_STACK_BYTES 32768 #endif @@ -51,6 +56,10 @@ #endif #ifndef CONFIG_MULTIMODAL_WS_MSG_QUEUE_LEN +/* + * 接收队列吸收 ESP 事件线程与 dispatch 消费速度的短期差异。512 是突发容忍上限; + * 增大会线性增加队列及 payload 峰值内存,不能作为处理长期拥塞的手段。 + */ #define CONFIG_MULTIMODAL_WS_MSG_QUEUE_LEN 512 #endif @@ -59,6 +68,10 @@ #endif #ifndef CONFIG_MULTIMODAL_WS_PING_INTERVAL_SEC +/* + * WebSocket ping 用于应用层存活检测,TCP keepalive 用于发现半开连接。两者故意并存: + * ping 更快反映服务端会话状态,keepalive 覆盖底层连接失效。减小周期会增加无线唤醒。 + */ #define CONFIG_MULTIMODAL_WS_PING_INTERVAL_SEC 20 #endif @@ -83,6 +96,10 @@ #endif #ifndef CONFIG_MULTIMODAL_WS_REALTIME_LOCK_WAIT_MS +/* + * 实时音频发送只短暂等待发送锁和网络写入:宁可丢弃当前包,也不能反压采集任务。 + * 普通文本使用更长超时和重试,以可靠性优先。 + */ #define CONFIG_MULTIMODAL_WS_REALTIME_LOCK_WAIT_MS 80 #endif @@ -98,6 +115,7 @@ #define CONFIG_TQ_NETWORK_TASK_CORE 0 #endif +/* 网络工作默认固定到网络核心;配置为负数时取消亲和性,修改后需观察音频抖动。 */ #if CONFIG_TQ_NETWORK_TASK_CORE < 0 #define MULTIMODAL_WS_CORE_ID tskNO_AFFINITY #else @@ -107,10 +125,15 @@ #define WS_OPCODE_TEXT 0x1 #define WS_OPCODE_BINARY 0x2 +/* + * 单个事件片段最多4KB;JSON 文本允许重组到8倍以容纳结构化响应,二进制音频保持 + * 4KB 硬上限以控制实时内存。越界或分片不连续会丢弃整条消息。 + */ #define MULTIMODAL_WS_MSG_MAX_BYTES 4096U #define WS_TEXT_REASSEMBLE_MAX_BYTES (MULTIMODAL_WS_MSG_MAX_BYTES * 8U) #define WS_BINARY_REASSEMBLE_MAX_BYTES MULTIMODAL_WS_MSG_MAX_BYTES +/* 配置栈先限制到全局范围,再按任务角色应用更严格的最低值和 PSRAM 栈上限。 */ #define MULTIMODAL_WS_STACK_MIN 8192 #define MULTIMODAL_WS_STACK_MAX 32768 #define MULTIMODAL_WS_DISPATCH_STACK_MIN 4096 @@ -123,6 +146,10 @@ #define MULTIMODAL_WS_MSG_QUEUE_CAP 512U +/* + * 普通发送最多重试4次、间隔80ms,适合控制文本;实时文本/音频使用300ms级边界。 + * 二进制 gap/summary 常量只控制诊断日志节流,不改变传输行为。 + */ #define MULTIMODAL_WS_SEND_TIMEOUT_MS 1200 #define MULTIMODAL_WS_SEND_RETRY_MAX 4 #define MULTIMODAL_WS_SEND_RETRY_DELAY_MS 80 @@ -130,6 +157,10 @@ #define MULTIMODAL_WS_TEXT_REALTIME_SEND_TIMEOUT_MS 300 #define MULTIMODAL_WS_BINARY_GAP_LOG_MS 600 #define MULTIMODAL_WS_BINARY_SUMMARY_INTERVAL 200U +/* + * timeout=0 的 stop 由独立清理任务接管;启动门限防止 start 在旧清理流程尚未登记 + * 任务句柄时抢先创建新 client。工作任务停止等待仍是有界的1200ms。 + */ #define MULTIMODAL_WS_ASYNC_STOP_TASK_STACK_BYTES 4096 #define MULTIMODAL_WS_ASYNC_STOP_START_GATE_WAIT_MS 5000 @@ -137,12 +168,14 @@ #define MULTIMODAL_WS_REALTIME_SEND_TIMEOUT_MS CONFIG_MULTIMODAL_WS_REALTIME_SEND_TIMEOUT_MS #define MULTIMODAL_WS_WORKER_STOP_WAIT_MS 1200 +/* mbedTLS 非阻塞 WANT_WRITE/WANT_READ 错误,用于判定发送是否值得短暂重试。 */ #define MULTIMODAL_TLS_WANT_WRITE_ERR (-0x6880) #define MULTIMODAL_TLS_WANT_READ_ERR (-0x6900) #define MULTIMODAL_WS_TOKEN_MAX_LEN 256 static const char *TAG = "multimodal_ws"; +/* WebSocket 事件数据先复制进队列,避免在 ESP 事件回调线程中执行耗时解析。 */ typedef struct { uint8_t op_code; uint8_t *data; @@ -194,6 +227,7 @@ static bool s_ws_msg_queue_cap_warned; static uint32_t s_ws_binary_event_enqueued; static uint32_t s_ws_binary_worker_processed; +/* client、连接标志和停止标志必须作为一个状态快照读取,避免竞态判断。 */ static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, @@ -273,6 +307,7 @@ static esp_err_t websocket_send_binary_internal(const uint8_t *data, static void set_event_callback(multimodal_ws_event_callback_t cb, void *user_data) { + /* 注册信息在临界区内更新;实际回调始终使用锁外快照。 */ portENTER_CRITICAL(&s_event_cb_lock); s_event_cb = cb; s_event_cb_ctx = user_data; @@ -355,9 +390,16 @@ static void ws_set_stop_requested(bool stop_requested) portEXIT_CRITICAL(&s_client_state_lock); } -/* Split implementation by concern while keeping a single translation unit: - * - workers: queue/task/stack helpers and worker pipeline logic - * - runtime: websocket lifecycle, event handling, send/stop public APIs +/* + * 实现文件拆分规则: + * - 下列 *_impl.c 不能单独加入 CMake 或独立编译,只能由本文件按当前顺序包含; + * - 两个文件共享本文件的 static client、锁、队列、重组缓冲及前置声明; + * - 拆分仅用于区分“工作管线”和“client 生命周期”,不存在链接边界; + * - workers 必须先于 runtime,因为 runtime 会调用前者定义的创建、发送和销毁函数。 + * + * Included *_impl.c files are not standalone translation units. They share the + * static transport state above, and workers must precede runtime because runtime + * depends on worker-pipeline definitions. */ #include "transport_ws/transport_ws_workers_impl.c" diff --git a/components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c b/components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c index 51bdfe4..1188b4b 100644 --- a/components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c +++ b/components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c @@ -1,8 +1,18 @@ +/* + * WebSocket client 生命周期和 ESP 事件桥接。 + * 不能单独编译;由 transport_ws.c 在 workers 之后包含,共享主文件 static 状态并 + * 调用 workers 中定义的辅助函数。拆分仅为了阅读,改变 include 顺序会破坏依赖。 + * This non-standalone file is included after workers, shares transport_ws.c + * static state, and depends on symbols defined by the preceding include. + * + * start/stop 统一拥有 client、工作队列和回调注册;零超时停止转为异步清理任务。 + */ static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { + /* 事件 payload 在 ESP 回调返回后即失效,因此入队前必须完成深拷贝。 */ (void)handler_args; (void)base; @@ -123,6 +133,7 @@ esp_err_t multimodal_ws_start(const char *bearer_token, multimodal_ws_event_callback_t cb, void *user_data) { + /* 新启动前清理上次失败遗留资源,鉴权 token 复制到模块私有定长缓冲区。 */ if (bearer_token == NULL || bearer_token[0] == '\0') { return ESP_ERR_INVALID_ARG; } @@ -263,6 +274,7 @@ static bool ws_has_owned_resources(void) static bool ws_claim_async_stop(void) { + /* starting 标志作为一次性所有权声明,防止并发 stop 创建多个清理任务。 */ bool claimed = false; portENTER_CRITICAL(&s_client_state_lock); @@ -336,6 +348,7 @@ static bool ws_wait_async_stop_done_until(TickType_t deadline_tick) static void ws_async_stop_task(void *arg) { + /* 异步停止仍执行完整同步清理,完成后清除自身句柄供后续 start 使用。 */ (void)arg; TaskHandle_t self = xTaskGetCurrentTaskHandle(); @@ -384,6 +397,7 @@ static esp_err_t ws_start_async_stop(void) static esp_err_t multimodal_ws_stop_sync_until(TickType_t deadline_tick) { + /* 顺序为停止 client、等待工作任务、销毁队列和清空回调,避免释放后仍有事件到达。 */ esp_websocket_client_handle_t client = ws_get_client_handle(); ws_set_stop_requested(true); ws_set_connected(false); @@ -433,6 +447,7 @@ static esp_err_t multimodal_ws_stop_sync_until(TickType_t deadline_tick) esp_err_t multimodal_ws_stop(int timeout_ms) { + /* timeout=0 仅发起清理;正数共享一个绝对截止时间等待已有或新建的停止流程。 */ if (timeout_ms < 0) { return ESP_ERR_INVALID_ARG; } diff --git a/components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c b/components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c index d2d833a..b6517b5 100644 --- a/components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c +++ b/components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c @@ -1,3 +1,12 @@ +/* + * WebSocket 队列、工作任务、分片重组和发送策略实现。 + * 不能单独编译;由 transport_ws.c 先于 runtime 包含,共享主文件的 client、锁、 + * 队列和重组状态。拆分仅为了阅读,runtime 直接依赖本文件中定义的静态函数。 + * This non-standalone file is included before runtime and shares all transport + * static state; include order is dependency-sensitive. + * + * ESP WebSocket 事件线程只负责复制消息;完整消息在 dispatch 任务中重组并回调。 + */ static bool is_retryable_send_error(int transport_ret, int saved_errno) { if (transport_ret == MULTIMODAL_TLS_WANT_WRITE_ERR || @@ -112,6 +121,7 @@ static BaseType_t create_ws_task_with_caps(TaskFunction_t task, bool prefer_psram_stack, BaseType_t core_id) { + /* 优先把工作栈放入 PSRAM,创建失败时由上层候选策略缩小栈并回退内部 RAM。 */ #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) #if (defined(CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) && CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) || \ defined(CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY) @@ -214,6 +224,7 @@ static QueueHandle_t create_ws_queue_with_caps(UBaseType_t queue_len, static void free_ws_msg(ws_msg_t *msg) { + /* 消息结构和 payload 由队列消费者独占,消费或丢弃后必须一起释放。 */ if (msg == NULL) { return; } @@ -284,6 +295,7 @@ static void log_queue_drop(const char *queue_name, QueueHandle_t queue, uint8_t static void emit_event(multimodal_ws_event_type_t type, const uint8_t *data, size_t data_len) { + /* 回调注册先取快照,调用期间不持有注册锁,允许回调安全地重新注册。 */ multimodal_ws_event_callback_t cb = NULL; void *cb_user_data = NULL; snapshot_event_callback(&cb, &cb_user_data); @@ -303,6 +315,7 @@ static void emit_event(multimodal_ws_event_type_t type, const uint8_t *data, siz static void process_text_fragment(const ws_msg_t *msg) { + /* 使用 payload_offset 校验连续性,乱序、越界或总长变化都会丢弃当前重组。 */ if (msg == NULL || msg->data == NULL || msg->data_len == 0U) { return; } @@ -385,6 +398,7 @@ static void process_text_fragment(const ws_msg_t *msg) static void process_binary_fragment(const ws_msg_t *msg) { + /* 二进制与文本分别维护缓冲,避免两类分片交错到达时相互覆盖。 */ if (msg == NULL || msg->data == NULL || msg->data_len == 0U) { return; } @@ -490,6 +504,7 @@ static bool wait_worker_task_exit(TaskHandle_t *task_handle, int timeout_ms) static void ws_dispatch_task(void *arg) { + /* 单消费者串行完成重组和事件分发,退出前清空残留消息并清除任务句柄。 */ (void)arg; ws_msg_t msg = {0}; @@ -557,6 +572,7 @@ static void destroy_queue_with_messages(QueueHandle_t *queue, bool *queue_with_c static bool destroy_worker_pipeline_until(TickType_t deadline_tick) { + /* 先请求协作退出,截止时间内未结束时返回 false 交由上层执行确定性兜底。 */ s_ws_worker_stop_requested = true; bool stopped_cleanly = true; int task_timeout_ms = ws_stop_remaining_ms(deadline_tick); @@ -736,6 +752,7 @@ static bool ws_transport_ready(void) static esp_err_t start_websocket_client_with_fallback(void) { + /* 按可用堆连续块重新排序栈候选值,降低内存碎片导致的启动失败概率。 */ int candidates[5] = {0}; size_t count = unique_stack_candidates(CONFIG_MULTIMODAL_WEBSOCKET_TASK_STACK_BYTES, candidates, @@ -916,6 +933,7 @@ static esp_err_t websocket_send_binary_internal(const uint8_t *data, TickType_t send_timeout, bool with_retry) { + /* 发送锁把同一帧写入串行化;实时路径通过短超时主动丢弃而不是阻塞采集。 */ esp_websocket_client_handle_t client = ws_get_client_handle(); if (client == NULL || data == NULL || len == 0U) { return ESP_ERR_INVALID_ARG; diff --git a/components/platform/src/platform_battery_driver.c b/components/platform/src/platform_battery_driver.c index 6e302b5..b6aa343 100644 --- a/components/platform/src/platform_battery_driver.c +++ b/components/platform/src/platform_battery_driver.c @@ -25,6 +25,11 @@ #if CONFIG_TQ_BATTERY_ENABLE +/* + * 每次采16个样本并裁掉最低/最高各4个,剩余8个取均值;200us 间隔用于跨越短促 + * 开关噪声。2500-4500mV 只是硬件合理性范围,不是 SOC 阈值;越界样本标记无效, + * domain 层不得把它直接解释为没电。 + */ #define BATTERY_ADC_SAMPLE_COUNT 16 #define BATTERY_ADC_TRIM_COUNT 4 #define BATTERY_VALID_MIN_MV 2500 @@ -43,6 +48,7 @@ static portMUX_TYPE s_state_lock = portMUX_INITIALIZER_UNLOCKED; static uint16_t s_load_ref_count[PLATFORM_BATTERY_LOAD_COUNT]; static int64_t s_cooldown_until_us; +/* ADC 读取可能跨任务发生,使用有界互斥等待防止采样路径无限阻塞。 */ static bool lock_adc(void) { if (s_adc_lock == NULL) { @@ -81,6 +87,7 @@ static void sort_ints(int *values, size_t count) static esp_err_t create_calibration(adc_unit_t unit, adc_channel_t channel) { + /* 优先曲线拟合,芯片不支持时回退到线性拟合校准方案。 */ #if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED { adc_cali_curve_fitting_config_t config = { @@ -204,6 +211,7 @@ esp_err_t platform_battery_init(void) static void snapshot_load_state(bool *out_active, bool *out_cooldown) { + /* active 表示当前高负载,cooldown 表示负载刚结束且电压尚未完全恢复。 */ bool active = false; int64_t cooldown_until_us = 0; @@ -223,6 +231,7 @@ static void snapshot_load_state(bool *out_active, bool *out_cooldown) esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample) { + /* 多次采样排序后去除两端异常值,以降低电机和音频负载造成的瞬态噪声。 */ if (out_sample == NULL) { return ESP_ERR_INVALID_ARG; } @@ -262,6 +271,7 @@ esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample) } sort_ints(samples_mv, BATTERY_ADC_SAMPLE_COUNT); + /* 对称裁剪最高/最低样本后取均值,抑制开关噪声但保留缓慢电压变化。 */ int64_t sum_mv = 0; for (size_t i = BATTERY_ADC_TRIM_COUNT; i < BATTERY_ADC_SAMPLE_COUNT - BATTERY_ADC_TRIM_COUNT; @@ -297,6 +307,7 @@ esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample) void platform_battery_load_begin(platform_battery_load_source_t source) { + /* 饱和递增防止异常嵌套调用造成引用计数回绕。 */ if (source < 0 || source >= PLATFORM_BATTERY_LOAD_COUNT) { return; } @@ -309,6 +320,7 @@ void platform_battery_load_begin(platform_battery_load_source_t source) void platform_battery_load_end(platform_battery_load_source_t source) { + /* 最后一个负载结束时启动冷却窗口,给电池端电压留出恢复时间。 */ if (source < 0 || source >= PLATFORM_BATTERY_LOAD_COUNT) { return; } diff --git a/components/platform/src/platform_behavior_stubs.c b/components/platform/src/platform_behavior_stubs.c index 56c866f..625a6d2 100644 --- a/components/platform/src/platform_behavior_stubs.c +++ b/components/platform/src/platform_behavior_stubs.c @@ -30,11 +30,17 @@ #error "CONFIG_TQ_POWER_HOLD_ACTIVE_LEVEL must be defined in Kconfig" #endif +/* 提示音和运行日志使用独立分区/挂载点,避免日志写入影响只读媒体资源。 */ #define VOICE_PROMPT_DEFAULT_BASE_PATH "/littlefs/audio_prompts" #define VOICE_PROMPT_LITTLEFS_BASE_PATH "/littlefs" #define VOICE_PROMPT_LITTLEFS_PARTITION "gifpack" #define PLATFORM_LOGFS_BASE_PATH "/logfs" #define PLATFORM_LOGFS_PARTITION "logfs" +/* + * 提示音文件由4字节小端包长头和对应 raw-Opus payload 重复组成;单包上限2KB + * 防止损坏文件触发异常分配。帧时长按60ms估算播放总时长,并额外留3秒用于队列 + * 排空、codec 尾部播放和调度抖动。 + */ #define VOICE_PROMPT_FRAME_HEADER_BYTES 4U #define VOICE_PROMPT_MAX_OPUS_PACKET_BYTES 2048U #define VOICE_PROMPT_OPUS_FRAME_MS 60U @@ -48,6 +54,7 @@ static esp_err_t s_voice_prompt_littlefs_mount_result = ESP_ERR_INVALID_STATE; static bool s_logfs_mount_attempted; static esp_err_t s_logfs_mount_result = ESP_ERR_INVALID_STATE; +/* GPIO 掩码集中构造,负数配置代表该板型不使用电源保持引脚。 */ static uint64_t power_manager_gpio_bit_mask(int gpio_num) { if (gpio_num < 0) { @@ -58,6 +65,7 @@ static uint64_t power_manager_gpio_bit_mask(int gpio_num) static esp_err_t power_manager_configure_hold_gpio(void) { + /* 配置只执行一次;GPIO 合法性在首次实际使用前统一校验。 */ if (CONFIG_TQ_POWER_HOLD_GPIO < 0) { return ESP_OK; } @@ -96,6 +104,7 @@ static esp_err_t power_manager_configure_hold_gpio(void) void power_manager_set_keep_awake(bool keep_awake) { + /* 当前硬件通过保持电平锁住电源,false 由后续关机流程真正切断电源。 */ if (CONFIG_TQ_POWER_HOLD_GPIO < 0) { (void)keep_awake; return; @@ -117,6 +126,7 @@ void platform_power_hold_init(void) esp_err_t platform_mount_littlefs(void) { + /* 记录首次挂载结果,使重复调用幂等且不会重复注册同一 VFS 路径。 */ if (s_voice_prompt_littlefs_mount_attempted) { return s_voice_prompt_littlefs_mount_result; } @@ -160,6 +170,7 @@ esp_err_t platform_mount_littlefs(void) esp_err_t platform_mount_logfs(void) { + /* 日志分区与提示音分区独立挂载,任一失败不会覆盖另一分区状态。 */ if (s_logfs_mount_attempted) { return s_logfs_mount_result; } @@ -203,6 +214,7 @@ esp_err_t platform_mount_logfs(void) esp_err_t voice_prompt_play_file(const char *path) { + /* 文件采用“长度头 + Opus 数据”帧格式,逐帧送入统一的下行播放管线。 */ if (path == NULL || path[0] == '\0') { return ESP_ERR_INVALID_ARG; } diff --git a/components/platform/src/platform_console_driver.c b/components/platform/src/platform_console_driver.c index 6aeb458..5f80a70 100644 --- a/components/platform/src/platform_console_driver.c +++ b/components/platform/src/platform_console_driver.c @@ -16,11 +16,13 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" +/* 工厂 CLI 复用 UART0;2KB RX 环形缓冲吸收整行命令与短期主机发送突发。 */ #define PLATFORM_CONSOLE_UART_PORT UART_NUM_0 #define PLATFORM_CONSOLE_RX_BUFFER 2048 static const char *TAG = "platform_console"; +/* UART0 驱动安装具有幂等性,已由其他启动路径安装也视为可用。 */ esp_err_t platform_console_install(void) { esp_err_t err = uart_driver_install(PLATFORM_CONSOLE_UART_PORT, @@ -42,6 +44,7 @@ esp_err_t platform_console_install(void) int platform_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms) { + /* 0 表示单次轮询,负数是无效参数;正数转换为 FreeRTOS 有界等待。 */ if (buf == NULL || len == 0U || timeout_ms < 0) { return -1; } @@ -51,6 +54,7 @@ int platform_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms) void platform_console_write_line(const char *line) { + /* 工厂协议按行发送,统一补充 CRLF,避免调用方重复处理行结束符。 */ if (line == NULL) { return; } diff --git a/components/platform/src/platform_digital_input_driver.c b/components/platform/src/platform_digital_input_driver.c index b7f0230..173d21e 100644 --- a/components/platform/src/platform_digital_input_driver.c +++ b/components/platform/src/platform_digital_input_driver.c @@ -8,6 +8,7 @@ #include "driver/gpio.h" +/* 数字输入驱动只解释电气有效电平,不附加按键、充电器等业务含义。 */ esp_err_t platform_digital_input_driver_configure(const platform_digital_input_config_t *config) { if (config == NULL) { @@ -31,6 +32,7 @@ esp_err_t platform_digital_input_driver_configure(const platform_digital_input_c bool platform_digital_input_driver_is_active(const platform_digital_input_config_t *config) { + /* 读取失败或配置无效时返回 false,调用方可通过初始化结果区分硬件错误。 */ if (config == NULL || config->gpio_num < 0 || config->gpio_num >= GPIO_NUM_MAX) { return false; } diff --git a/components/platform/src/platform_io_effects.c b/components/platform/src/platform_io_effects.c index 6e64782..e5a074d 100644 --- a/components/platform/src/platform_io_effects.c +++ b/components/platform/src/platform_io_effects.c @@ -26,12 +26,18 @@ #define PLATFORM_IO_EFFECTS_TASK_CORE ((BaseType_t)CONFIG_TQ_LOCAL_TASK_CORE) #endif +/* GPIO4、GPIO5和22颗灯珠来自当前 PCB 布线/灯带布局,换板时必须同步核对原理图。 */ #define PLATFORM_RMT_GPIO GPIO_NUM_4 #define PLATFORM_PULSE_GPIO GPIO_NUM_5 #define PLATFORM_PULSE_HIGH_MS 500 #define PLATFORM_PULSE_LOW_MS 200 +/* + * WS2812 每像素按 GRB 三字节发送;10MHz RMT 分辨率用于构造亚微秒0/1脉宽。 + * 30ms 帧周期约33FPS,100ms 完成等待是硬件故障边界。亮度上限用于限制峰值电流, + * 下限保证呼吸动画暗部仍可见,调整时需复核电源预算和热量。 + */ #define PLATFORM_RMT_LED_COUNT 22 #define PLATFORM_RMT_LED_BYTES_PER_PIXEL 3 #define PLATFORM_RMT_RESOLUTION_HZ 10000000 @@ -45,6 +51,7 @@ static const char *TAG = "platform_io_effects"; static TaskHandle_t s_pulse_task; static TaskHandle_t s_rmt_task; +/* WS2812 的 0/1/reset 时序预先编码为 RMT symbol,回调只负责逐字节展开。 */ static const rmt_symbol_word_t s_ws2812_zero = { .level0 = 1, .duration0 = PLATFORM_RMT_RESOLUTION_HZ / 10000000 * 3, @@ -74,6 +81,7 @@ static size_t platform_ws2812_encoder_callback(const void *data, bool *done, void *arg) { + /* RMT 每次至少需要八个空 symbol 才能原子编码一个颜色字节。 */ (void)arg; if (symbols_free < 8U) { @@ -103,6 +111,7 @@ static uint8_t scale_u8(uint8_t value, uint8_t scale) static void hsv_to_rgb(uint8_t hue, uint8_t value, uint8_t *red, uint8_t *green, uint8_t *blue) { + /* 使用整数 HSV 转换,避免动画任务引入浮点运算和额外栈占用。 */ uint8_t region = hue / 43U; uint8_t remainder = (uint8_t)((hue - (region * 43U)) * 6U); uint8_t p = 0U; @@ -145,6 +154,7 @@ static void hsv_to_rgb(uint8_t hue, uint8_t value, uint8_t *red, uint8_t *green, static uint8_t breathing_brightness(uint16_t phase) { + /* 三角波亮度不需要查表,且在最暗处保留最低可见亮度。 */ uint16_t wave = phase & 0x01FFU; if (wave > 255U) { wave = 511U - wave; @@ -175,6 +185,7 @@ static void fill_breathing_pixels(uint8_t *pixels, size_t pixel_len, uint8_t hue static void platform_pulse_task(void *arg) { + /* 独立任务产生固定占空节奏,不阻塞灯带 RMT 刷新。 */ (void)arg; while (true) { @@ -187,6 +198,7 @@ static void platform_pulse_task(void *arg) static void platform_rmt_led_task(void *arg) { + /* 每帧等待上一次 RMT 发送完成,防止复用仍被 DMA/RMT 消费的像素缓冲。 */ (void)arg; uint8_t pixels[PLATFORM_RMT_LED_COUNT * PLATFORM_RMT_LED_BYTES_PER_PIXEL] = {0}; @@ -264,6 +276,7 @@ static void platform_rmt_led_task(void *arg) esp_err_t platform_io_effects_init(void) { + /* 只有全部 GPIO/RMT 资源和任务创建成功后才保存任务句柄。 */ gpio_config_t pulse_config = { .pin_bit_mask = BIT64(PLATFORM_PULSE_GPIO), .mode = GPIO_MODE_OUTPUT, diff --git a/components/platform/src/platform_stepper_driver.c b/components/platform/src/platform_stepper_driver.c index 07c57e2..e3362b9 100644 --- a/components/platform/src/platform_stepper_driver.c +++ b/components/platform/src/platform_stepper_driver.c @@ -35,12 +35,22 @@ #define PLATFORM_STEPPER_IN2_PIN CONFIG_TQ_STEPPER_IN2_GPIO #define PLATFORM_STEPPER_IN3_PIN CONFIG_TQ_STEPPER_IN3_GPIO #define PLATFORM_STEPPER_IN4_PIN CONFIG_TQ_STEPPER_IN4_GPIO +/* + * 2200us 是重新励磁和加速起点,较慢起步用于降低失步风险;加速/减速各48个半步, + * 短行程会按比例压缩。4128半步/圈包含电机步距、减速箱和八拍半步序列的综合标定, + * 修改电机或传动结构后必须重新实测标定。 + */ #define PLATFORM_STEPPER_START_DELAY_US 2200 #define PLATFORM_STEPPER_ACCEL_STEPS 48 #define PLATFORM_STEPPER_DECEL_STEPS 48 #define PLATFORM_STEPPER_STEPS_PER_REV 4128.0f +/* + * 释放保持电流最多等待1秒获取运动锁;单次旋转限制100度用于保护当前机械行程, + * 不是电机本体能力。扩大角度前必须确认实体限位和线缆余量。 + */ #define PLATFORM_STEPPER_POWER_OFF_WAIT_MS 1000 #define PLATFORM_STEPPER_MAX_ROTATION_DEG 100.0f +/* ESP32-S3 GPIO0-31 与 GPIO32以上使用不同输出寄存器组。 */ #define PLATFORM_GPIO_LOW_BANK_MAX_PIN 31 #define PLATFORM_GPIO_HIGH_BANK_OFFSET 32 @@ -53,6 +63,7 @@ static StaticSemaphore_t s_stepper_motion_mutex_buf; static int s_stepper_phase_index; static bool s_stepper_energized; +/* 八拍半步序列兼顾转动平滑度和低速扭矩,反转时按相反方向遍历。 */ static const int s_step_sequence[8][4] = { {1, 0, 0, 0}, {1, 1, 0, 0}, @@ -91,6 +102,7 @@ static void add_gpio_level_mask(uint32_t *set0, static void set_motor_pins(int in1, int in2, int in3, int in4) { + /* 先计算寄存器掩码,再批量更新四相输出,缩短相位切换的时间偏差。 */ uint32_t set0 = 0; uint32_t clr0 = 0; uint32_t set1 = 0; @@ -219,6 +231,7 @@ static void calculate_motion_segments(int steps, int *out_constant_steps, int *out_decel_steps) { + /* 短行程按比例缩短加减速段,确保三段步数之和不超过总步数。 */ int accel_steps = PLATFORM_STEPPER_ACCEL_STEPS; int decel_steps = PLATFORM_STEPPER_DECEL_STEPS; @@ -278,6 +291,7 @@ static esp_err_t rotate_with_accel(int steps, int timeout_ms, int64_t start_ms) { + /* 每步动态计算延时形成加速、匀速、减速曲线,并在步间检查总超时。 */ int accel_steps = 0; int constant_steps = 0; int decel_steps = 0; @@ -340,6 +354,7 @@ esp_err_t platform_stepper_driver_rotate_angle_with_accel(float angle, int target_delay_us, int timeout_ms) { + /* 角度先换算为整数半步;方向只决定相位遍历方向,不改变加速曲线。 */ if (!ensure_stepper_motion_mutex()) { return ESP_ERR_INVALID_STATE; } @@ -406,6 +421,7 @@ esp_err_t platform_stepper_driver_rotate_angle_with_accel(float angle, esp_err_t platform_stepper_driver_release_hold(int timeout_ms) { + /* 与运动使用同一互斥锁,保证释放线圈不会发生在相位更新中间。 */ if (timeout_ms < 0) { return ESP_ERR_INVALID_ARG; } @@ -431,5 +447,6 @@ esp_err_t platform_stepper_driver_release_hold(int timeout_ms) void platform_stepper_driver_power_off(void) { + /* 紧急断电直接清零相线;常规并发路径应优先使用 release_hold。 */ (void)platform_stepper_driver_release_hold(PLATFORM_STEPPER_POWER_OFF_WAIT_MS); } diff --git a/components/platform/src/runtime/platform_behavior_runtime.c b/components/platform/src/runtime/platform_behavior_runtime.c index 7b58028..f8368aa 100644 --- a/components/platform/src/runtime/platform_behavior_runtime.c +++ b/components/platform/src/runtime/platform_behavior_runtime.c @@ -26,6 +26,7 @@ static SemaphoreHandle_t s_wifi_behavior_hooks_mutex; static StaticSemaphore_t s_wifi_behavior_hooks_mutex_buf; #endif +/* 钩子注册可能与 Wi-Fi 事件并发发生,因此使用独立互斥锁保护整组快照。 */ static bool ensure_wifi_behavior_hooks_mutex(void) { if (s_wifi_behavior_hooks_mutex != NULL) { @@ -61,6 +62,7 @@ static void unlock_wifi_behavior_hooks(void) static void platform_wifi_behavior_set_keep_awake_adapter(bool keep_awake) { + /* 在锁内复制、锁外调用,防止外部回调反向注册钩子时死锁。 */ platform_wifi_behavior_hooks_t hooks = {0}; if (lock_wifi_behavior_hooks()) { hooks = s_wifi_behavior_hooks; @@ -98,6 +100,7 @@ esp_err_t platform_behavior_play_prompt_file(const char *path) void platform_wifi_portal_set_behavior_hooks(const platform_wifi_behavior_hooks_t *hooks) { + /* NULL 表示清空全部自定义行为并恢复“不支持”的默认结果。 */ if (!lock_wifi_behavior_hooks()) { return; } @@ -146,6 +149,7 @@ bool platform_wifi_portal_sta_is_connected(void) esp_err_t platform_wifi_portal_set_light_standby(bool standby) { + /* 轻待机只调整 modem 省电策略,不停止 STA 或清除已保存凭据。 */ wifi_ps_type_t ps_type = standby ? WIFI_PS_MIN_MODEM : (CONFIG_TQ_WIFI_STA_POWER_SAVE ? WIFI_PS_MIN_MODEM : WIFI_PS_NONE); return esp_wifi_set_ps(ps_type); diff --git a/components/platform/src/runtime/platform_ble_gatt_server.c b/components/platform/src/runtime/platform_ble_gatt_server.c index b9f3454..820c324 100644 --- a/components/platform/src/runtime/platform_ble_gatt_server.c +++ b/components/platform/src/runtime/platform_ble_gatt_server.c @@ -27,6 +27,11 @@ static const char *TAG = "platform_ble_gatt"; #include "services/gap/ble_svc_gap.h" #include "services/gatt/ble_svc_gatt.h" +/* + * 启动最多等待3秒让 NimBLE host 同步;首选 MTU 256在通知吞吐和 mbuf 占用之间折中。 + * 停止由独立任务最多尝试3次、间隔100ms,防止 host 瞬时忙状态导致资源遗留。 + * ESP-IDF 的 FreeRTOS 创建接口按字节解释这里的4096栈配置,名称保留历史叫法。 + */ #define PLATFORM_BLE_SYNC_TIMEOUT_MS 3000 #define PLATFORM_BLE_PREFERRED_MTU 256 #define PLATFORM_BLE_STOP_DONE_BIT BIT0 @@ -35,6 +40,7 @@ static const char *TAG = "platform_ble_gatt"; #define PLATFORM_BLE_STOP_MAX_ATTEMPTS 3 #define PLATFORM_BLE_STOP_RETRY_DELAY_MS 100 +/* BLE 生命周期状态由自旋锁保护;NimBLE 回调只读取必要快照后再调用外部代码。 */ typedef struct { bool started; bool synced; @@ -76,6 +82,7 @@ static int platform_ble_gatt_access(uint16_t connection_handle, static int platform_ble_gap_event(struct ble_gap_event *event, void *arg); static const struct ble_gatt_svc_def s_gatt_services[] = { + /* 单服务包含写入、通知以及可读通知三个通用特征值。 */ { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &s_service_uuid.u, @@ -107,6 +114,7 @@ static const struct ble_gatt_svc_def s_gatt_services[] = { static void platform_ble_set_uuid(ble_uuid128_t *out, const platform_ble_uuid128_t *source) { + /* 写入 payload 在回调返回后失效;业务层需要异步使用时必须自行复制。 */ memset(out, 0, sizeof(*out)); out->u.type = BLE_UUID_TYPE_128; memcpy(out->value, source->bytes, sizeof(out->value)); @@ -129,6 +137,7 @@ static esp_err_t platform_ble_validate_config( static void platform_ble_clear_runtime_state(void) { + /* 停止完成后清除连接句柄和回调,防止下次启动复用旧会话状态。 */ portENTER_CRITICAL(&s_state_lock); s_state.started = false; s_state.synced = false; @@ -145,6 +154,7 @@ static void platform_ble_clear_runtime_state(void) static esp_err_t platform_ble_start_advertising(void) { + /* 主机同步完成后才开始广播;断连后由 GAP 事件路径重新广播。 */ struct ble_hs_adv_fields fields = {0}; struct ble_gap_adv_params parameters = {0}; char advertised_name[PLATFORM_BLE_GATT_NAME_MAX_LEN + 1]; @@ -265,6 +275,7 @@ static int platform_ble_gatt_access(uint16_t connection_handle, static int platform_ble_gap_event(struct ble_gap_event *event, void *arg) { + /* GAP 回调只维护连接/订阅快照,断连后恢复广播以支持重新发现。 */ (void)arg; switch (event->type) { @@ -328,6 +339,7 @@ esp_err_t platform_ble_gatt_server_notify(platform_ble_gatt_notify_target_t targ const uint8_t *data, size_t data_len) { + /* 通知前原子快照连接句柄和订阅状态,未订阅不会分配 mbuf。 */ if (data == NULL || data_len == 0U || (target != PLATFORM_BLE_GATT_NOTIFY_VALUE && target != PLATFORM_BLE_GATT_READ_NOTIFY_VALUE)) { @@ -374,6 +386,7 @@ esp_err_t platform_ble_gatt_server_notify(platform_ble_gatt_notify_target_t targ static void platform_ble_stop_task(void *arg) { + /* NimBLE stop 可能需要多次尝试;后台任务负责重试并发布唯一完成结果。 */ (void)arg; bool host_stopped = false; portENTER_CRITICAL(&s_state_lock); @@ -444,6 +457,7 @@ static void platform_ble_stop_task(void *arg) esp_err_t platform_ble_gatt_server_start(const platform_ble_gatt_server_config_t *config) { + /* 配置数据和回调先复制入内部状态,再启动 NimBLE host 并等待同步完成。 */ esp_err_t err = platform_ble_validate_config(config); if (err != ESP_OK) { return err; @@ -534,6 +548,7 @@ esp_err_t platform_ble_gatt_server_start(const platform_ble_gatt_server_config_t esp_err_t platform_ble_gatt_server_stop(int timeout_ms) { + /* 零超时只触发后台停止;正超时等待事件位,不在调用线程忙等。 */ if (timeout_ms < 0) { return ESP_ERR_INVALID_ARG; } diff --git a/components/platform/src/runtime/platform_core_runtime.c b/components/platform/src/runtime/platform_core_runtime.c index 823e2c3..2b38fd0 100644 --- a/components/platform/src/runtime/platform_core_runtime.c +++ b/components/platform/src/runtime/platform_core_runtime.c @@ -48,6 +48,7 @@ static const char *const s_known_nvs_namespaces[] = { "fortune", }; +/* 深睡唤醒原因在启动早期读取,供组合层决定是否恢复外设保持状态。 */ bool platform_power_woke_from_deep_sleep(void) { return esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_UNDEFINED; @@ -66,6 +67,7 @@ static void platform_power_set_output_low_and_hold(int gpio) static void platform_power_set_board_outputs_held(bool held) { + /* 深睡前统一拉低高耗电输出,避免外设在主 CPU 休眠后继续耗电。 */ const int output_gpios[] = { CONFIG_MULTIMODAL_AUDIO_PA_GPIO, CONFIG_TQ_SCREEN_BACKLIGHT_GPIO, @@ -99,6 +101,7 @@ void platform_power_release_deep_sleep_holds(void) esp_err_t platform_power_enter_deep_sleep(int charger_gpio, int button_gpio) { + /* 两个唤醒源均按低电平触发;无效 GPIO 必须在进入睡眠前被拒绝。 */ uint64_t wake_mask = 0U; const int wake_gpios[] = {charger_gpio, button_gpio}; @@ -150,6 +153,7 @@ esp_err_t platform_power_enter_deep_sleep(int charger_gpio, int button_gpio) static esp_err_t platform_gpio_isr_service_init(void) { + /* 全局 ISR service 只安装一次,ESP_ERR_INVALID_STATE 表示已有合法所有者。 */ if (s_gpio_isr_service_installed) { return ESP_OK; } @@ -184,6 +188,7 @@ esp_err_t platform_read_firmware_version(char *out, size_t out_len) static void platform_log_nvs_stats(void) { + /* 仅输出容量诊断,不读取键值内容,避免日志泄露凭据。 */ nvs_stats_t stats = {0}; esp_err_t err = nvs_get_stats(NULL, &stats); if (err != ESP_OK) { @@ -265,6 +270,7 @@ static bool platform_read_nvs_string(const char *ns, const char *key, char *out, esp_err_t platform_init(void) { + /* 该入口只初始化平台基础设施;存储准备保留为独立、可重试的阶段。 */ if (s_initialized) { return ESP_OK; } @@ -299,6 +305,7 @@ esp_err_t platform_init(void) esp_err_t platform_prepare_storage(void) { + /* NVS 页损坏或版本不匹配时按 ESP-IDF 建议擦除后重新初始化。 */ if (s_storage_ready) { return ESP_OK; } @@ -330,6 +337,7 @@ void platform_read_device_identity(char *device_id, char *serial_number, size_t serial_number_len) { + /* 每个输出缓冲先清空;NVS 缺失时使用芯片派生默认值而非旧数据。 */ if (device_id != NULL && device_id_len > 0U) { device_id[0] = '\0'; (void)platform_read_nvs_string( @@ -351,6 +359,7 @@ void platform_read_device_identity(char *device_id, esp_err_t platform_write_device_identity(const char *device_id, const char *serial_number) { + /* 两个字段在同一 NVS 句柄中写入并一次 commit,避免只持久化一半身份。 */ if (device_id == NULL || serial_number == NULL || device_id[0] == '\0' || serial_number[0] == '\0') { return ESP_ERR_INVALID_ARG; diff --git a/components/platform/src/runtime/platform_https_client.c b/components/platform/src/runtime/platform_https_client.c index bf63968..7d457ae 100644 --- a/components/platform/src/runtime/platform_https_client.c +++ b/components/platform/src/runtime/platform_https_client.c @@ -406,6 +406,7 @@ static const char *dns_error_name(int err) static esp_err_t parse_url_host(const char *url, char *host, size_t host_capacity) { + /* 仅提取 DNS 主机部分,同时兼容带方括号的 IPv6 字面量。 */ if (url == NULL || host == NULL || host_capacity == 0U) { return ESP_ERR_INVALID_ARG; } @@ -446,6 +447,7 @@ static esp_err_t parse_url_host(const char *url, char *host, size_t host_capacit static esp_err_t resolve_host_before_open(const char *url) { + /* 在创建 HTTP client 前显式解析 DNS,便于提供稳定错误码和网络诊断。 */ char host[PLATFORM_HTTPS_HOST_MAX_LEN]; esp_err_t err = parse_url_host(url, host, sizeof(host)); if (err != ESP_OK) { @@ -528,6 +530,7 @@ static esp_err_t resolve_host_before_open(const char *url) static esp_err_t write_all(esp_http_client_handle_t client, const char *payload) { + /* esp_http_client_write 允许短写,循环直到完整 payload 发出或明确失败。 */ size_t payload_len = strlen(payload); if (payload_len > (size_t)INT_MAX) { return ESP_ERR_INVALID_SIZE; @@ -551,6 +554,7 @@ static esp_err_t write_all(esp_http_client_handle_t client, const char *payload) esp_err_t platform_https_post_json(const platform_https_post_json_request_t *request, platform_https_response_t *response) { + /* 先清空调用方响应快照,确保任何提前失败都不会暴露上次请求的旧数据。 */ if (response != NULL) { response->body_len = 0U; response->status_code = -1; diff --git a/components/platform/src/runtime/platform_lan_http_server.c b/components/platform/src/runtime/platform_lan_http_server.c index 9cbf028..3c59b32 100644 --- a/components/platform/src/runtime/platform_lan_http_server.c +++ b/components/platform/src/runtime/platform_lan_http_server.c @@ -24,12 +24,20 @@ #include "mdns.h" #include "soc/soc_caps.h" +/* + * 单任务串行处理 HTTP,8KB 栈覆盖解析及业务 handler 调用。2KB 头部上限和公共 body + * 上限共同决定固定 I/O 缓冲大小;提高上限会增加服务器整个生命周期的常驻内存。 + */ #define LAN_HTTP_TASK_STACK_SIZE (1024 * 8) #define LAN_HTTP_TASK_PRIORITY 4 #define LAN_HTTP_HEADER_MAX_LEN 2048 #define LAN_HTTP_REQUEST_BUFFER_BYTES (LAN_HTTP_HEADER_MAX_LEN + \ PLATFORM_LAN_HTTP_REQUEST_BODY_MAX_LEN + 1U) #define LAN_HTTP_RESPONSE_BUFFER_BYTES PLATFORM_LAN_HTTP_RESPONSE_BODY_MAX_LEN +/* + * 3秒是每个 client socket 的收发超时;select 每200ms醒来检查停止请求;启动最多 + * 等待1.5秒获得 bind/listen 结果;停止轮询20ms用于兼顾响应速度和调度开销。 + */ #define LAN_HTTP_IO_TIMEOUT_MS 3000 #define LAN_HTTP_SELECT_INTERVAL_MS 200 #define LAN_HTTP_START_TIMEOUT_MS 1500 @@ -68,6 +76,7 @@ static uint8_t *s_response_buffer; static esp_err_t s_start_result = ESP_ERR_INVALID_STATE; static portMUX_TYPE s_server_lock = portMUX_INITIALIZER_UNLOCKED; +/* 服务器状态和 socket 句柄由同一自旋锁保护,耗时 I/O 始终在锁外执行。 */ static void server_task(void *arg); static void lan_http_log_heap_snapshot(const char *phase) @@ -98,6 +107,7 @@ static void lan_http_log_heap_snapshot(const char *phase) static void *lan_http_alloc_io_buffer(size_t size) { + /* 大块收发缓冲优先放入外部内存,保留内部 RAM 给网络栈和实时任务。 */ if (size == 0U) { return NULL; } @@ -246,6 +256,7 @@ static const char *http_content_type(platform_lan_http_content_type_t content_ty static esp_err_t send_all(int fd, const void *data, size_t len) { + /* TCP send 可能短写或被信号中断,循环到完整发送或出现确定错误。 */ const uint8_t *cursor = (const uint8_t *)data; size_t sent = 0; @@ -307,6 +318,7 @@ static void send_error_response(int fd, static const char *find_header_value(const char *headers, const char *name) { + /* 只在已终止的头部缓冲内逐行搜索,返回值指向请求缓冲且不单独分配。 */ size_t name_len = strlen(name); const char *line = strstr(headers, "\r\n"); if (line == NULL) { @@ -357,6 +369,7 @@ static esp_err_t read_request(int fd, platform_lan_http_request_t *out_request, platform_lan_http_status_t *out_error_status) { + /* 先在固定上限内读取完整头,再依据 Content-Length 读取有界 body。 */ if (request_buffer == NULL || request_buffer_len < LAN_HTTP_REQUEST_BUFFER_BYTES) { *out_error_status = PLATFORM_LAN_HTTP_STATUS_INTERNAL_ERROR; return ESP_ERR_INVALID_ARG; @@ -427,6 +440,7 @@ static esp_err_t read_request(int fd, strlcpy(out_request->path, target, sizeof(out_request->path)); if (find_header_value((char *)request_buffer, "Transfer-Encoding") != NULL) { + /* 简化服务器不支持 chunked 编码,明确拒绝以避免请求边界歧义。 */ *out_error_status = PLATFORM_LAN_HTTP_STATUS_BAD_REQUEST; return ESP_FAIL; } @@ -465,6 +479,7 @@ static esp_err_t read_request(int fd, static platform_lan_http_handler_t snapshot_handler(void **out_user_data) { + /* handler 与 user_data 作为一组快照读取,业务回调执行期间不持有状态锁。 */ platform_lan_http_handler_t handler; portENTER_CRITICAL(&s_server_lock); handler = s_config.handler; @@ -479,6 +494,7 @@ static void handle_client(int fd, uint8_t *response_buffer, size_t response_buffer_len) { + /* 单连接串行处理,request/response 都借用服务器任务拥有的固定缓冲区。 */ platform_lan_http_request_t request; platform_lan_http_status_t error_status = PLATFORM_LAN_HTTP_STATUS_BAD_REQUEST; if (read_request(fd, @@ -593,6 +609,7 @@ static void log_server_address(uint16_t port, const char *hostname) static void server_task(void *arg) { + /* 工作任务独占 listen/client socket 和 I/O 缓冲,所有退出路径统一释放。 */ (void)arg; platform_lan_http_server_config_t config; @@ -651,6 +668,7 @@ static void server_task(void *arg) log_server_address(config.port, config.hostname); while (!server_stop_requested()) { + /* select 使用短周期轮询停止标志,同时避免永久阻塞在 accept。 */ fd_set read_fds; FD_ZERO(&read_fds); FD_SET(listen_fd, &read_fds); @@ -728,6 +746,7 @@ startup_failed: esp_err_t platform_lan_http_server_start(const platform_lan_http_server_config_t *config) { + /* 配置按值复制;启动调用只等待工作任务报告 bind/listen 结果。 */ if (config == NULL || config->handler == NULL || config->port == 0U || @@ -776,6 +795,7 @@ esp_err_t platform_lan_http_server_start(const platform_lan_http_server_config_t esp_err_t platform_lan_http_server_stop(int timeout_ms) { + /* shutdown 主动唤醒阻塞 I/O,再等待任务自行清理;超时后确定性强制删除。 */ if (timeout_ms < 0) { return ESP_ERR_INVALID_ARG; } diff --git a/components/platform/src/runtime/platform_lcd_runtime.c b/components/platform/src/runtime/platform_lcd_runtime.c index 34b3113..739dc1a 100644 --- a/components/platform/src/runtime/platform_lcd_runtime.c +++ b/components/platform/src/runtime/platform_lcd_runtime.c @@ -7,6 +7,10 @@ #include "platform.h" #include "lcd_direct.h" +/* + * facade 适配层刻意不保存状态:LCD 生命周期、队列和完成计数均以 + * lcd_direct 为唯一事实来源,避免 platform API 与驱动内部状态分叉。 + */ esp_err_t platform_init_lcd_device(void) { return lcd_direct_init_device(); diff --git a/components/platform/src/runtime/platform_multimodal_runtime.c b/components/platform/src/runtime/platform_multimodal_runtime.c index b8e16a9..3ed9325 100644 --- a/components/platform/src/runtime/platform_multimodal_runtime.c +++ b/components/platform/src/runtime/platform_multimodal_runtime.c @@ -21,6 +21,7 @@ static portMUX_TYPE s_multimodal_downstream_playback_cb_lock = portMUX_INITIALIZ static uint32_t s_battery_playback_generation; static bool s_battery_playback_active; +/* 将扬声器播放纳入电池高负载窗口,避免负载压降被误判为真实低电量。 */ static void platform_battery_note_playback_event( multimodal_audio_downstream_playback_event_type_t type, uint32_t generation) @@ -61,11 +62,13 @@ static void snapshot_transport_callbacks( portENTER_CRITICAL(&s_multimodal_transport_cb_lock); *out_callbacks = s_multimodal_transport_callbacks; portEXIT_CRITICAL(&s_multimodal_transport_cb_lock); + /* 回调快照离开临界区后再调用,防止用户代码在自旋锁内运行。 */ } static void platform_multimodal_transport_event_bridge(const multimodal_ws_event_t *event, void *user_data) { + /* 此处只做底层事件形状转换,连接策略和业务语义由上层负责。 */ (void)user_data; if (event == NULL) { diff --git a/components/platform/src/runtime/platform_ota.c b/components/platform/src/runtime/platform_ota.c index 6d1e6f1..080ca30 100644 --- a/components/platform/src/runtime/platform_ota.c +++ b/components/platform/src/runtime/platform_ota.c @@ -19,12 +19,18 @@ #include "esp_timer.h" #include "mbedtls/sha256.h" +/* + * 4KB 下载缓冲在 TLS/HTTP 内存和 Flash 写入吞吐之间折中;最多5次重定向用于兼容 + * CDN,同时限制异常跳转链。10秒仅限制单次网络 I/O,整个 OTA 仍共享调用方提供的 + * 绝对 timeout_ms 预算。 + */ #define PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE 4096 #define PLATFORM_OTA_MAX_REDIRECTS 5 #define PLATFORM_OTA_IO_TIMEOUT_MS 10000 static const char *TAG = "platform_ota"; +/* OTA 使用单一绝对截止时间,DNS、HTTP 和写 Flash 都共享同一超时预算。 */ static bool platform_ota_deadline_expired(int64_t deadline_us) { return esp_timer_get_time() >= deadline_us; @@ -49,6 +55,7 @@ esp_err_t platform_ota_install_image(const char *url, platform_ota_cancel_callback_t cancel_cb, void *user_data) { + /* 仅接受 HTTPS,并在下载前确认目标镜像能够放入非活动分区。 */ if (url == NULL || strncmp(url, "https://", 8) != 0 || expected_sha256 == NULL || expected_size == 0U || timeout_ms <= 0) { return ESP_ERR_INVALID_ARG; @@ -106,6 +113,7 @@ esp_err_t platform_ota_install_image(const char *url, esp_http_client_cleanup(client); return ESP_ERR_INVALID_RESPONSE; } + /* OTA 只接受完整 200 响应,不处理 range 或服务端错误页。 */ int status_code = esp_http_client_get_status_code(client); if (status_code != 200) { ESP_LOGW(TAG, "OTA download returned HTTP %d", status_code); @@ -113,6 +121,7 @@ esp_err_t platform_ota_install_image(const char *url, esp_http_client_cleanup(client); return ESP_ERR_INVALID_RESPONSE; } + /* 服务端提供长度时先行拒绝不匹配镜像,避免无意义地擦写分区。 */ if (content_length >= 0 && (uint64_t)content_length != expected_size) { ESP_LOGE(TAG, "OTA Content-Length mismatch: expected=%" PRIu64 " actual=%" PRIi64, @@ -147,6 +156,7 @@ esp_err_t platform_ota_install_image(const char *url, } uint64_t received = 0U; + /* 每轮重新计算剩余预算,使网络读取、Flash 写入和取消检查共享总超时。 */ while (received < expected_size) { bool cancelled = cancel_cb != NULL && cancel_cb(user_data); if (cancelled || platform_ota_deadline_expired(deadline_us)) { @@ -223,6 +233,7 @@ esp_err_t platform_ota_install_image(const char *url, } mbedtls_sha256_free(&sha_ctx); + /* 哈希不匹配时不切换启动分区,当前固件仍保持可启动。 */ if (memcmp(actual_sha256, expected_sha256, sizeof(actual_sha256)) != 0) { ESP_LOGE(TAG, "OTA SHA-256 mismatch"); err = ESP_ERR_INVALID_CRC; @@ -260,6 +271,7 @@ cleanup_no_abort: esp_err_t platform_ota_confirm_running_image(void) { + /* 仅在镜像处于 pending verify 时取消回滚,重复确认视为成功。 */ const esp_partition_t *running = esp_ota_get_running_partition(); if (running == NULL) { return ESP_ERR_NOT_FOUND; diff --git a/components/platform/src/runtime/platform_storage.c b/components/platform/src/runtime/platform_storage.c index 56aafe4..c781c5f 100644 --- a/components/platform/src/runtime/platform_storage.c +++ b/components/platform/src/runtime/platform_storage.c @@ -12,6 +12,7 @@ static const char *TAG = "platform_storage"; +/* 所有 KV 操作都先检查统一的存储就绪状态,避免各调用方重复维护状态。 */ static esp_err_t ensure_storage_ready(const char *op) { if (platform_storage_is_ready()) { @@ -40,11 +41,13 @@ static esp_err_t open_storage_readonly(const char *ns, nvs_handle_t *out_handle) static esp_err_t normalize_missing_key(esp_err_t err) { + /* 对外统一使用 ESP_ERR_NOT_FOUND,屏蔽 NVS 内部错误码差异。 */ return (err == ESP_ERR_NVS_NOT_FOUND) ? ESP_ERR_NOT_FOUND : err; } esp_err_t platform_storage_set_blob(const char *ns, const char *key, const void *data, size_t len) { + /* set 与 commit 在同一句柄生命周期内完成,只有提交成功才报告写入成功。 */ if (ns == NULL || key == NULL || data == NULL || len == 0U) { return ESP_ERR_INVALID_ARG; } @@ -149,6 +152,7 @@ esp_err_t platform_storage_get_blob(const char *ns, size_t out_len, size_t *out_len_actual) { + /* 先查询实际长度再读取,长度不足时返回标准错误并报告所需字节数。 */ if (out_len_actual != NULL) { *out_len_actual = 0U; } @@ -260,6 +264,7 @@ esp_err_t platform_storage_get_string(const char *ns, size_t out_len, size_t *out_len_actual) { + /* NVS 返回长度包含结尾 NUL;对外 actual 同样保留该约定,便于安全分配。 */ if (out_len_actual != NULL) { *out_len_actual = 0U; } @@ -298,6 +303,7 @@ esp_err_t platform_storage_get_string(const char *ns, esp_err_t platform_storage_erase(const char *ns, const char *key) { + /* 删除不存在的键归一化为成功,使清理和迁移流程保持幂等。 */ if (ns == NULL || key == NULL) { return ESP_ERR_INVALID_ARG; } diff --git a/components/platform/src/runtime/platform_time.c b/components/platform/src/runtime/platform_time.c index 1a5d712..86b8a9b 100644 --- a/components/platform/src/runtime/platform_time.c +++ b/components/platform/src/runtime/platform_time.c @@ -23,6 +23,7 @@ static const char *TAG = "platform_time"; static bool s_sntp_started; static portMUX_TYPE s_time_lock = portMUX_INITIALIZER_UNLOCKED; +/* POSIX TZ 影响整个进程,更新后调用 tzset 立即刷新 libc 的本地时间规则。 */ esp_err_t platform_time_set_timezone(const char *posix_tz) { if (posix_tz == NULL || posix_tz[0] == '\0') { @@ -39,6 +40,7 @@ esp_err_t platform_time_set_timezone(const char *posix_tz) esp_err_t platform_time_sync_start(const char *sntp_server, const char *posix_tz) { + /* 重复调用只更新时区;SNTP 客户端只初始化一次,避免重复注册资源。 */ if (posix_tz != NULL && posix_tz[0] != '\0') { esp_err_t timezone_err = platform_time_set_timezone(posix_tz); if (timezone_err != ESP_OK) { @@ -78,6 +80,7 @@ esp_err_t platform_time_sync_start(const char *sntp_server, const char *posix_tz bool platform_time_is_synced(void) { + /* 不复制 SNTP 内部状态,以系统时钟越过合理下界作为同步成功事实来源。 */ time_t now = 0; time(&now); return (int64_t)now >= PLATFORM_TIME_MIN_VALID_EPOCH_S; diff --git a/components/platform/src/wifi_portal_bridge.cpp b/components/platform/src/wifi_portal_bridge.cpp index 12437d2..e147676 100644 --- a/components/platform/src/wifi_portal_bridge.cpp +++ b/components/platform/src/wifi_portal_bridge.cpp @@ -25,6 +25,10 @@ static const char *TAG = "wifi_portal"; +/* + * 门户锁最多等待2秒,防止第三方 manager 卡住所有调用者;默认连接预算10秒由多个 + * 已保存候选共享,100ms 轮询在连接响应速度与任务唤醒开销之间折中。 + */ #define WIFI_PORTAL_LOCK_TIMEOUT_MS 2000 #define WIFI_PORTAL_DEFAULT_CONNECT_TIMEOUT_MS 10000 #define WIFI_PORTAL_CONNECT_POLL_MS 100 @@ -49,6 +53,7 @@ static wifi_portal_behavior_hooks_t s_behavior_hooks = {}; static esp_err_t apply_sta_stability_policy(void); +/* 将 wifi-manager 的凭据持久化重定向到平台 NVS,避免组件维护第二份存储。 */ static void install_wifi_storage_hooks(void) { static bool s_installed; @@ -92,6 +97,7 @@ static void log_saved_wifi_candidates(const char *context) } struct PortalUnlockGuard { + /* RAII 保证每条提前返回路径都会释放门户互斥锁。 */ ~PortalUnlockGuard() { if (active) { @@ -127,6 +133,7 @@ static esp_err_t wifi_portal_lock(void) static void promote_saved_wifi(const std::string &ssid) { + /* 成功连接的 SSID 移到队首,使下次启动优先尝试最近可用网络。 */ if (ssid.empty()) { return; } @@ -176,6 +183,7 @@ static void notify_keep_awake(bool keep_awake) static void wifi_manager_event_callback(WifiEvent event, const std::string &data) { + /* 事件回调只更新状态并转发通知,不在管理器线程里执行连接流程。 */ switch (event) { case WifiEvent::Connecting: ESP_LOGI(TAG, "Connecting to \"%s\"", data.c_str()); @@ -205,6 +213,7 @@ static void wifi_manager_event_callback(WifiEvent event, const std::string &data static esp_err_t ensure_wifi_manager_initialized(void) { + /* 管理器初始化和事件注册只执行一次,后续 API 共享同一实例。 */ install_wifi_storage_hooks(); if (s_wifi_manager_initialized) { return ESP_OK; @@ -254,6 +263,7 @@ static int normalize_connect_timeout_ms(int timeout_ms) static esp_err_t apply_sta_stability_policy(void) { + /* 关闭省电或调整失活时间,避免实时音频期间 Wi-Fi 周期性休眠。 */ wifi_ps_type_t ps_type = CONFIG_TQ_WIFI_STA_POWER_SAVE ? WIFI_PS_MIN_MODEM : WIFI_PS_NONE; @@ -350,6 +360,7 @@ esp_err_t wifi_portal_start_config_ap(const char *ssid_prefix) esp_err_t wifi_portal_connect_sta(int timeout_ms) { + /* 在统一截止时间内按优先级尝试已保存网络,返回后不继续后台轮询。 */ { ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); PortalUnlockGuard guard; @@ -393,6 +404,7 @@ esp_err_t wifi_portal_connect_sta_with_credentials(const char *ssid, const char *password, int timeout_ms) { + /* 临时凭据仅用于本次连接;成功后再持久化,失败不污染候选列表。 */ ESP_RETURN_ON_FALSE(ssid != NULL && ssid[0] != '\0', ESP_ERR_INVALID_ARG, TAG, @@ -516,6 +528,7 @@ esp_err_t wifi_portal_connect_sta_with_credentials(const char *ssid, esp_err_t wifi_portal_stop_sta(void) { + /* 停止 STA 时同步更新连接快照,确保查询接口不会返回陈旧状态。 */ ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); PortalUnlockGuard guard;