# TQ Cat 固件整体架构与运行工作流
本文档基于 `feature/st7789p3-new-board` 分支的代码,面向项目接手、问题定位和后续重构。内容以实际函数调用关系为准。
## 1. 系统定位
项目运行在 ESP32-S3 上,主要负责:
- ST7789P3 LCD 表情播放;
- ES8311 音频采集与播放;
- Opus 编解码与 WebSocket 多模态对话;
- TTP223 PTT、SW1、SW2 按键处理;
- ULN2003 步进电机和转盘校准;
- Wi-Fi 连接、重连和配置热点;
- 设备身份、JWT 和会话令牌;
- 用户出生资料、每日运势和财富方向;
- NVS、LittleFS 和工厂 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`