# TQ Cat 固件整体架构与运行工作流 本文档基于 `feature/st7789p3-new-board` 分支的代码,面向项目接手、问题定位和后续重构。内容以实际函数调用关系为准。 ## 1. 系统定位 项目运行在 ESP32-S3 上,主要负责: - ST7789P3 LCD 表情播放; - ES8311 音频采集与播放; - Opus 编解码与 WebSocket 多模态对话; - TTP223 PTT、SW1、SW2 按键处理; - ULN2003 步进电机和转盘校准; - Wi-Fi 连接、重连和配置热点; - 设备身份、JWT 和会话令牌; - 用户出生资料、每日运势和财富方向; - NVS、应用内嵌素材、模型分区和工厂 CLI。 ## 2. 分层架构 ```mermaid flowchart TB Main["main / app_main"] App["app_composition
启动顺序与应用装配"] CP["control_plane
策略、状态机、任务、重试"] Domain["domain
方向、校准、运势、表情语义"] Platform["platform
GPIO、LCD、音频、网络、存储"] HW["ESP32-S3 新板硬件"] Main --> App --> CP --> Domain --> Platform --> HW ``` 各层职责: | 层 | 主要职责 | 典型内容 | | --- | --- | --- | | `main` | 固件入口 | 只调用 composition 入口 | | `app_composition` | 决定启动顺序 | 存储、音频、LCD、校准、联网、workers | | `control_plane` | 产品策略和生命周期 | Wi-Fi、PTT、多模态、任务、重试、异常恢复 | | `domain` | 业务语义和状态 | 方向、角度、校准、运势、LCD 表情映射 | | `platform` | 硬件与传输原语 | GPIO、I2S、I2C、SPI、WebSocket、NVS、HTTP | ## 3. 启动总流程 入口: ```text app_main() └── app_composition_start_default_runtime() └── app_composition_run_plan_a_boot_sequence() ``` 主要文件: - `main/main.c` - `components/app_composition/src/runtime/app_composition_default_runtime.c` 启动阶段: ```mermaid flowchart TD Entry["app_main"] Storage["准备 NVS / 文件系统"] Audio["初始化音频并播放开机音"] LCD["初始化 LCD 和渲染任务"] Core["初始化 platform / domain"] Expression["请求默认 LCD 表情"] Calibration["执行转盘原点校准"] WiFi["连接 Wi-Fi 或启动配置 AP"] Workers["启动运行时 workers"] Entry --> Storage --> Audio --> LCD --> Core --> Expression --> Calibration --> WiFi --> Workers ``` ### 3.1 存储初始化 ```text control_plane_prepare_runtime_storage() └── domain_runtime_prepare_storage() └── platform_prepare_storage() ├── nvs_flash_init() ├── 必要时 nvs_flash_erase() ├── 再次 nvs_flash_init() └── platform_log_nvs_stats() ``` 默认 NVS namespace 包括 `wifi`、`ota`、`auth`、`auth_key` 和 `fortune`。 ### 3.2 音频与开机音 ```text control_plane_play_boot_sound(timeout) ├── domain_runtime_multimodal_audio_init() │ └── platform_multimodal_audio_init() │ └── multimodal_audio_init() └── domain_runtime_behavior_play_prompt_file(path) ``` `multimodal_audio_init()` 的主要步骤: ```text fill_codec_sample_info() → audio_init_heap_guard_passed() → init_opus_codec() → init_i2c_bus() → init_i2s_bus() → create_codec_path() → open_codec_device() → apply_codec_default_controls() → ensure_uplink_worker_tasks() → ensure_downstream_playback_task() ``` 开机音失败只记录 warning,不会终止启动。 ### 3.3 LCD 初始化 ```text control_plane_init_lcd_device() └── domain_runtime_init_lcd_device() └── platform_init_lcd_device() └── lcd_direct_init_device() ├── lcd_direct_gpio_init() └── lcd_direct_panel_init() ``` 渲染任务: ```text control_plane_start_lcd_render_worker() └── control_plane_lcd_render_loop_task() └── control_plane_start_lcd_render_loop() └── domain_runtime_start_lcd_render_loop() └── platform_start_lcd_render_loop() └── lcd_direct_start_render_loop() ``` ### 3.4 platform/domain 初始化 ```text control_plane_init() ├── domain_direction_default_axis_config() ├── domain_direction_default_calibration() └── domain_init(axis_config, calibration) ├── domain_runtime_platform_init() │ └── platform_init() │ ├── platform_power_hold_init() │ ├── platform_io_effects_init() │ ├── platform_stepper_driver_init() │ └── platform_stepper_driver_power_off() ├── 初始化方向状态 └── domain_calibration_store_set() ``` 注意:启动日志将 power hold 描述为已提前拉起,但实际 `platform_power_hold_init()` 位于 `platform_init()` 中。新板应真机验证 GPIO9 拉高时机是否足够早。 ## 4. 新板关键 GPIO | 功能 | GPIO | 电气语义 | | --- | ---: | --- | | Power Hold | 9 | 高有效 | | LCD CLK | 8 | SPI clock | | LCD MOSI | 18 | SPI data | | LCD CS | 15 | 低有效 | | LCD DC | 16 | 数据/命令 | | LCD RESET | 17 | 低有效 reset | | LCD Backlight | 7 | 高有效 | | TTP223 / PTT | 3 | 高有效,无内部上下拉 | | SW1 校准开关 | 12 | 低有效,上拉 | | SW2 功能开关 | 13 | 低有效,上拉 | | Stepper IN1/2/3/4 | 45/48/47/21 | ULN2003 相线 | | Audio PA | 11 | 功放使能 | | ES8311 I2C SDA/SCL | 1/2 | codec 控制 | | I2S MCLK/BCLK/DOUT/WS/DIN | 38/39/40/41/42 | 音频数据 | ## 5. 转盘校准与旋转 启动校准调用链: ```text control_plane_calibrate() └── control_plane_calibrate_with_timeout() └── control_plane_direction_calibrate_internal() └── domain_calibrate_with_timeout() ``` `domain_calibrate_with_timeout()`: ```text 锁定方向状态 → prepare_calibration_switch() → 检查 SW1 是否已有效 → 循环执行 calibration_rotate_and_track() → calibration_settle() → 再次读取 SW1 → 触发后 calibration_return_from_limit() → domain_set_current_angle(home_angle) → domain_runtime_stepper_power_off() ``` 底层旋转: ```text domain_runtime_stepper_rotate_angle_with_accel_timeout() └── platform_stepper_rotate_angle_with_accel_timeout() └── platform_stepper_driver_rotate_angle_with_accel() └── rotate_with_accel() ├── calculate_motion_segments() ├── stepper_advance_phase() └── precise_delay_us() ``` 绝对角度旋转: ```text control_plane_rotate_to_angle() └── domain_rotate_to_angle_with_timeout() ├── domain_angle_normalize() ├── domain_angle_shortest_rotation() ├── 执行电机旋转 └── 更新 s_current_angle ``` ## 6. Wi-Fi 启动与恢复 启动入口: ```text control_plane_bootstrap_connectivity(seed_ssid, seed_password, timeout, "TQ-CAT") ``` 调用链: ```text control_plane_register_wifi_behavior_hooks() → domain_runtime_wifi_portal_init_defaults() → wifi_portal_init_defaults() → 检查已保存凭据 → control_plane_connect_sta_with_feedback() → 最多三次 STA 连接 → 成功则播放提示音 → 失败则按错误类型保留或清除凭据 → 必要时启动 TQ-CAT 配置 AP ``` 运行时由 `control_plane_wifi_state_task()` 负责: - 检测 Wi-Fi 断开; - 等待短暂 disconnect grace; - 停止多模态 client 和用户配置 server; - 请求断网表情; - 按指数退避重连; - 重连后恢复 idle 表情、WebSocket 和配置 server; - 同时驱动 PTT 状态机。 ## 7. 运行时任务拓扑 入口: ```text control_plane_start_runtime_workers() ``` ```mermaid flowchart TB Start["control_plane_start_runtime_workers"] Model["cp_model_cmd
模型设备命令"] WiFi["cp_wifi_state
Wi-Fi、WS、PTT"] LCD["cp_lcd_render
LCD 启动"] Events["cp_system_event
事件分发"] Keys["cp_key_input
GPIO 与消抖"] Fortune["cp_fortune
每日运势"] Monitor["cp_task_monitor
可选诊断"] Start --> Model Start --> WiFi Start --> LCD Start --> Events Start --> Keys Start --> Fortune Start --> Monitor ``` `control_plane_schedule_websocket_wait_task()` 当前只设置 `s_ws_start_requested = true`,实际 WebSocket 启动由 `cp_wifi_state` 执行。 ## 8. 按键和系统事件 按键中断链路: ```text GPIO edge → control_plane_key_gpio_isr_handler() → xQueueSendFromISR(s_gpio_irq_queue) → control_plane_key_input_task() → control_plane_process_key_timers() → control_plane_handle_debounced_transition() → control_plane_emit_key_event() → control_plane_post_system_event() → control_plane_system_event_task() → control_plane_dispatch_system_event() → control_plane_dispatch_key_event() ``` 事件包括: - `PRESSED`; - `RELEASED`; - `SHORT_PRESS`; - `LONG_PRESS_START`; - `LONG_PRESS_RELEASE`; - `DOUBLE_CLICK`。 行为映射: | 输入 | 行为 | | --- | --- | | PTT 短按 | `control_plane_set_ptt_active(true)` | | PTT 在服务端回复时按下 | `control_plane_multimodal_send_request_to_speak()` | | PTT 双击 | 请求 `DOMAIN_EXPRESSION_MUYU` | | SW2 单击 | 请求 `DOMAIN_EXPRESSION_CHOUQIAN` | | SW2 双击 | 请求 `DOMAIN_EXPRESSION_WUXING` | ## 9. PTT 状态机 状态: ```text IDLE WAIT_WIFI WAIT_WS WAIT_START_SENT WAIT_LISTENING WAIT_SPEECH_STARTED STREAMING TEARDOWN_LOCAL TEARDOWN_REMOTE DENIED ``` 状态机由 `control_plane_drive_ptt_state_machine()` 在 `cp_wifi_state` 中周期驱动: ```text 无 Wi-Fi → WAIT_WIFI 无 WebSocket → WAIT_WS 无 active task → WAIT_START_SENT 服务端未 Listening → WAIT_LISTENING 未发送 SendSpeech → WAIT_SPEECH_STARTED uplink 已运行 → STREAMING 释放/超时 → TEARDOWN_LOCAL → TEARDOWN_REMOTE → IDLE ``` 每次推进调用 `control_plane_multimodal_handle_ptt_press_step()`,根据当前会话状态选择: - `control_plane_multimodal_send_start()`; - `control_plane_multimodal_send_request_to_speak()`; - `control_plane_multimodal_send_speech_start()`; - `control_plane_multimodal_start_uplink()`。 ## 10. 设备认证和 WebSocket 启动链: ```text cp_wifi_state → control_plane_try_start_multimodal_from_state_machine() → control_plane_start_multimodal_client() → control_plane_multimodal_start() ``` `control_plane_start_multimodal_client()`: ```text control_plane_prepare_for_multimodal_start() → 注册 event callback → 注册 downstream audio callback → 创建 mm_watchdog → control_plane_multimodal_start() ``` 认证链: ```text control_plane_multimodal_auth_fetch() ├── resolve_device_identity() ├── control_plane_multimodal_device_auth_ensure() │ ├── control_plane_multimodal_device_auth_init() │ ├── 检查已有 JWT 和 scope │ └── refresh_jwt_internal() ├── control_plane_multimodal_device_auth_copy_jwt() ├── HTTP POST token endpoint ├── 401/403 时 invalidate JWT 并重试 └── parse_token_response() ``` WebSocket 链: ```text domain_runtime_multimodal_transport_start() └── platform_multimodal_transport_start() └── multimodal_ws_start() ├── 停止旧 client ├── 构造 Authorization 和 Workspace headers ├── 创建 send mutex ├── create_worker_pipeline() └── start_websocket_client_with_fallback() ``` ## 11. 一次完整语音会话 ```mermaid sequenceDiagram participant User as 用户/PTT participant CP as Control Plane participant Session as Session Service participant Audio as Audio Pipeline participant WS as WebSocket participant Server as 服务端 User->>CP: 短按 CP->>Session: send_start() Session->>WS: run-task / Start Server-->>Session: DialogStateChanged(Listening) CP->>Session: send_speech_start() CP->>Audio: start_uplink() Audio->>WS: Opus binary frames Server-->>Session: SpeechEnded / RespondingStarted Session->>Audio: stop uplink / begin response Server-->>Audio: Opus binary frames Audio-->>User: 扬声器播放 Server-->>Session: RespondingEnded Audio-->>Session: playback completed Session->>WS: LocalRespondingEnded ``` ### 11.1 Start ```text control_plane_multimodal_send_start() → 生成 task UUID → 构造 run-task / Start JSON → 设置 upstream/downstream Opus 参数 → domain_runtime_multimodal_transport_send_text() → 保存 s_task_id ``` ### 11.2 开始采集和上行 ```text control_plane_multimodal_send_speech_start() → send_directive("continue-task", "SendSpeech") → control_plane_multimodal_start_uplink() → domain_runtime_multimodal_audio_start_uplink() → multimodal_audio_start_uplink() ``` 实时音频: ```text I2S capture task → PCM 整理 → Opus encode → uplink send queue → uplink_send_task → control_plane_audio_uplink_send_cb() → control_plane_multimodal_send_audio_realtime() → multimodal_ws_send_binary_realtime() ``` ### 11.3 下行播放 ```text WebSocket binary frame → transport_event_bridge() → control_plane_downstream_audio_bridge() → control_plane_multimodal_play_downstream_audio() → multimodal_audio_handle_downstream_opus() → downstream playback queue → Opus decode → I2S / ES8311 播放 ``` 服务端 `RespondingEnded` 和本地播放完成都满足后,才发送 `LocalRespondingEnded`。 ## 12. 服务端事件解析 所有文本帧进入 `process_text_json()`,主要处理: - `task-failed`; - `task-started`; - `DialogStateChanged`; - `RequestAccepted`; - `RespondingStarted`; - `RespondingContent`; - `RespondingEnded`; - `SpeechStarted`; - `SpeechContent`; - `SpeechEnded`; - `Stopped`; - `Error`。 回复文本还会进入: ```text notify_responding_text() → control_plane_responding_text_bridge() → control_plane_extract_model_command() → control_plane_on_model_command() → model command queue → control_plane_model_command_task() ``` 设备命令包括方向、校准动作和抽签。 ## 13. LCD 表情工作流 ```text domain_runtime_request_expression() ├── domain_resolve_expression_platform_request() └── platform_lcd_request_presentation() └── lcd_direct_request_presentation() ``` domain 负责把业务语义映射成 platform 播放请求: ```text IDLE SERVER_ERROR WIFI_DISCONNECTED WEALTH_SPIN WEALTH_DIRECTION MUYU CHOUQIAN WUXING ``` platform 只处理: - asset directory; - frame index; - frame interval; - priority; - interrupt policy; - repeat count; - return-to-idle。 渲染循环: ```text lcd_direct_take_pending() → lcd_direct_play_asset_sequence() 或 lcd_direct_play_step_sequence() → lcd_direct_signal_completion() → 无 pending 时播放 basic idle face ``` ## 14. 财富方向与运势 财富方向动作: ```text control_plane_apply_wealth_direction() ├── domain_runtime_preheat_expression(reveal) ├── 阻塞播放 WEALTH_SPIN ├── 请求 WEALTH_DIRECTION reveal ├── 记录 start_angle ├── 旋转固定角度 ├── 延时 └── 返回 start_angle ``` 当前 `direction_index` 主要选择 LCD 方向资源,电机仍执行固定角度转出再返回,并非直接旋转到方向目录的绝对角度。 每日运势 worker: ```text control_plane_daily_fortune_task() ├── 等待 birth profile ├── 等待 Wi-Fi ├── control_plane_wait_for_synced_time() ├── 等待 WebSocket TLS 握手稳定 ├── 计算本地日期 ├── control_plane_fortune_fetch() ├── domain_fortune_result_store_set() └── control_plane_apply_wealth_direction() ``` 运势 HTTPS 与 WebSocket TLS 握手被有意串行化,以避免内部 DMA RAM 不足。 ## 15. 局域网用户配置 联网后 `control_plane_wifi_state_task()` 启动用户配置 server: ```text control_plane_ensure_user_config_server() └── domain_user_config_server_start() └── platform_lan_http_server_start() ``` 路由: | 方法 | 路径 | 处理函数 | | --- | --- | --- | | GET | `/` | `handle_index()` | | GET | `/api/profile` | `handle_get_profile()` | | POST | `/api/profile` | `handle_post_profile()` | | GET | `/api/fortune` | `handle_get_fortune()` | 资料更新后: ```text domain_birth_profile_store_set() → birth profile observer → control_plane_request_fortune_refresh() → 唤醒 cp_fortune ``` ## 16. Watchdog 和异常恢复 `multimodal_watchdog_task()` 每秒检查: - WebSocket 是否连接; - 是否存在 active task; - heartbeat 是否到期; - 是否长时间没有下行; - transport 是否空闲超过 30 秒。 处理: ```text heartbeat 到期 → control_plane_multimodal_send_heartbeat() 无下行超时 → control_plane_multimodal_send_stop("NoResponseTimeout") → stop uplink transport 空闲 30 秒 → control_plane_multimodal_stop() ``` WebSocket 断开或错误时,会停止 uplink,并由 `cp_wifi_state` 安排后续重连。 ## 17. 停止流程 ```text control_plane_stop_runtime_workers() ├── 设置 runtime stop flag ├── 停止用户配置 server ├── 注销模型命令和 profile observer ├── 停止 key task 和 system event task ├── 停止 LCD render loop ├── bounded wait 各 worker ├── 必要时强制删除 task ├── 删除 model command queue └── control_plane_stop_multimodal_client() ├── stop_watchdog_task() └── control_plane_multimodal_stop() ├── stop uplink ├── 停止本地 responding worker ├── 注销 callbacks ├── transport_stop(2000) └── reset_session_state_locked() ``` ## 18. 调试索引 遇到问题时先判断属于哪个状态机: 1. `control_plane_wifi_state_task()`:Wi-Fi、WebSocket 启动、PTT; 2. multimodal session:task、Listening、Speech、Responding; 3. audio pipeline:capture、send queue、decode、playback。 推荐排查路径: | 现象 | 优先检查 | | --- | --- | | 上电后掉电 | GPIO9、`platform_power_hold_init()` 调用时机 | | LCD 无显示 | GPIO、`lcd_direct_init_device()`、背光和 SPI | | 电机不动 | `platform_stepper_driver_init()`、相序、限位状态 | | 开机校准超时 | GPIO12、有效电平、最大 sweep、旋转方向 | | PTT 无反应 | ISR queue、消抖、system event、PTT state | | 有按键但没录音 | Listening、SendSpeech、audio init、uplink task | | 有录音但服务端收不到 | Opus queue、uplink callback、WS binary send | | 服务端回复但没声音 | binary callback、decode queue、I2S、PA GPIO11 | | 第二轮对话失败 | `s_task_id`、Listening、speech flags、playback completion | | 断网不恢复 | `cp_wifi_state`、saved credentials、backoff、WS start request | ## 19. 关键源码入口 - `main/main.c` - `components/app_composition/src/runtime/app_composition_default_runtime.c` - `components/control_plane/src/runtime/control_plane_runtime_workers.c` - `components/control_plane/src/runtime/control_plane_connectivity_runtime.c` - `components/control_plane/src/runtime/control_plane_key_input.c` - `components/control_plane/src/runtime/control_plane_system_events.c` - `components/control_plane/src/multimodal/control_plane_multimodal_runtime.c` - `components/control_plane/src/multimodal/control_plane_multimodal_session_service.c` - `components/control_plane/src/multimodal/control_plane_multimodal_auth_service.c` - `components/control_plane/src/multimodal/control_plane_multimodal_device_auth.c` - `components/domain/src/direction/domain_direction_service.c` - `components/domain/src/calibration/domain_calibration_service.c` - `components/domain/src/runtime/domain_runtime_adapters.c` - `components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c` - `components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c` - `components/platform/src/lcd_direct.c` - `components/platform/src/platform_stepper_driver.c`