第 1 章 项目总览与文件分级总览表
本章解决两个问题:① 这份代码是个什么东西、整体怎么跑起来;② 哪些文件是核心、哪些是辅助、哪些一辈子都不用看。后面所有章节都会基于这张总览表展开。
1.1 项目是什么
xiaozhi-esp32-main 是开源项目「小智 AI 聊天机器人」的 v2 版本固件源码(仓库 78/xiaozhi-esp32)。代码角色:
| 角色 | 说明 |
|---|---|
| 麦克风 | 用 I²S 采集 16 kHz PCM |
| 唤醒词识别 | 板载 ESP-SR("你好小智"等),整段离线运行 |
| 编解码 | Opus(窄带语音压缩),上行下行都是 Opus 帧 |
| 通信 | 选 WebSocket 或 MQTT+UDP 之一,向云端发音频 + 收音频/文本 |
| 云端语音处理 | ASR(语音转文字)→ LLM(Qwen/DeepSeek 等)→ TTS(合成语音),全部在服务器 |
| 表情/状态显示 | OLED / LCD / AMOLED / e-Paper 均可,UI 走 LVGL |
| 设备控制 | 通过 MCP 协议(一种基于 JSON-RPC 2.0 的工具调用协议)暴露音量、灯光、电机、GPIO 等给云端大模型 |
一句话:麦克风 + 网络 + 喇叭 + 一个能被 LLM 调用 GPIO 的协议 = 一个会对话能控制硬件的小盒子。
支持的芯片:ESP32 / ESP32-C3 / ESP32-S3 / ESP32-C5 / ESP32-C6 / ESP32-P4。开发板(boards/)有 110 块。
1.2 顶层目录速览
xiaozhi-esp32-main/
├── CMakeLists.txt # 顶层 CMake,引 ESP-IDF
├── sdkconfig.defaults* # 各芯片默认 menuconfig 配置
├── README*.md # 项目自述(中英日三份)
├── LICENSE # 开源协议
├── main/ # 应用代码(本项目的全部业务逻辑)
├── partitions/ # 分区表 v1 / v2
├── scripts/ # Python 辅助脚本:资源打包、发布、音频调试
├── docs/ # 协议文档(MCP、WebSocket、MQTT+UDP、自定义板子)
└── .github/ # GitHub Actions CI 配置(每个板子自动构建)
只有 main/ 是业务代码。其它都是构建胶水、文档、CI——读不读完全不影响理解。
main/ 下进一步拆:
main/
├── CMakeLists.txt # 列出所有源文件 + 110 个板子的条件分支选择
├── Kconfig.projbuild # 项目级 menuconfig 选项(板子型号、AEC 模式、唤醒词等)
├── idf_component.yml # ESP-IDF 组件依赖清单(esp-sr、opus、lvgl 等)
├── main.cc # app_main(),30 行
├── application.{cc,h} # ★ 全局单例 Application,主事件循环(1055 行)
├── device_state.h # 11 个状态枚举
├── device_state_machine.{cc,h} # ★ 状态机 + 回调观察者
├── ota.{cc,h} # ★ 服务器握手 / 激活 / 固件升级
├── settings.{cc,h} # NVS 键值对封装
├── system_info.{cc,h} # 芯片型号 / MAC / 堆内存信息
├── mcp_server.{cc,h} # ★ 设备端 MCP 工具调用服务器
├── assets.{cc,h} # ★ assets 分区(字体/表情/唤醒词模型)管理
│
├── audio/ # ★ 音频子系统
├── protocols/ # ★ WebSocket / MQTT+UDP 双协议
├── display/ # 显示子系统(LCD / OLED / e-Paper / 表情)
├── led/ # 状态指示灯
├── assets/ # 多语言文字 + 提示音 OGG 文件
└── boards/ # 110 个开发板的硬件适配
├── common/ # ★ 板级共用基类(Board / WifiBoard / Ml307Board / blufi 等)
└── <某板子>/ # 各板子具体的 GPIO 引脚、屏幕驱动、按键映射
带 ★ 的就是后面会逐文件逐函数讲的核心模块。
1.3 全局架构与启动流程
1.3.1 一张图看懂启动顺序
ESP-IDF 启动
│
▼
app_main() ── main.cc 初始化 NVS 闪存
│
▼
Application::GetInstance() ① 单例 → 创建事件组 + 时钟 timer
│
▼
Application::Initialize() ② 一系列同步初始化(在 app_main 上下文)
│ ├─ Board::GetInstance() └─ 板级单例 (board.cc 里 create_board())
│ ├─ display = board.GetDisplay() └─ 拿屏(每个板子构造时已创建)
│ ├─ audio_service_.Initialize(codec) └─ 装好编解码器、起 audio 任务
│ ├─ audio_service_.Start()
│ ├─ audio_service_.SetCallbacks(...) └─ 把"有 Opus 包可发"等回调挂到 event group
│ ├─ state_machine_.AddStateChangeListener(...) └─ 状态变 → 触发 event group
│ ├─ esp_timer_start_periodic(1s) └─ 心跳 timer
│ ├─ McpServer::GetInstance().AddCommonTools() └─ 注册 self.audio_speaker.set_volume 等通用工具
│ ├─ board.SetNetworkEventCallback(...) └─ 网络连/断/扫描等 UI 反馈
│ └─ board.StartNetwork() └─ 异步启动 WiFi / 4G
│
▼
Application::Run() ③ 永不返回的主事件循环
│
│ while (true) {
│ xEventGroupWaitBits(...); ← 13 种事件位
│ 处理:状态变 / 网络变 / 唤醒词 / VAD / 心跳 / 主线程任务 / ...
│ }
│
└── 与此并行运行的后台任务:
├─ ActivationTask(一次性):拉服务器配置、CheckNewVersion、激活、初始化协议
├─ audio_input_task / audio_output_task / opus_codec_task
├─ LVGL 任务(在 display.cc / LvglDisplay 里起)
├─ 网络栈本身的 RTOS 任务(WiFi/MQTT/WS/UDP)
└─ 各板子的辅助任务(按键防抖、电池监控、屏幕背光定时器 等)
1.3.2 谁负责什么——一句话总结
| 模块 | 责任 |
|---|---|
main.cc | 上电跳板,30 行 |
Application | 大调度器,所有事件汇总在它的 event_group_ 上,谁也别绕开 |
Board | 板子抽象,每块板子各自实现 audio_codec / display / network 等接口的具体硬件 |
AudioService | 三个 RTOS 任务管全部音频读写、Opus 编解码、唤醒词馈喂、AFE 处理 |
Protocol(基类)+ WebsocketProtocol / MqttProtocol | 收发两边的 JSON 控制信令 + 二进制 Opus 帧 |
McpServer | 把"音量""灯光""GPIO"等本地能力注册成 MCP 工具,给云端 LLM 调用 |
Ota | 启动后向 xiaozhi.me 发 POST 拿配置(含 MQTT/WS 端点),处理激活码与固件升级 |
Assets | 管理 SPI Flash 上一个名为 assets 的分区(字体、表情、唤醒词模型按文件名 mmap) |
Display | UI 抽象。LcdDisplay / OledDisplay / EmoteDisplay 各自实现 |
Led | 状态指示灯(单 LED 闪烁、WS2812 灯环、RGB) |
Settings | 对 ESP-IDF NVS 的轻量封装,读写小配置 |
1.3.3 数据流速览
上行:你说话 → 服务器
MIC (I²S)
│
▼ AudioCodec::InputData → vector<int16_t>
audio_input_task
│
├─ 喂 → WakeWord::Feed (若处于 idle、需要监听唤醒词)
├─ 喂 → AudioProcessor::Feed (若 listening 状态,AFE 做降噪/VAD/AEC)
└─ 否则丢弃
AudioProcessor 输出
│
▼ PushTaskToEncodeQueue(kEncodeToSendQueue)
opus_codec_task
│
▼ Opus 编码
audio_send_queue_ ──→ MAIN_EVENT_SEND_AUDIO
│
▼ Application::Run() 取包
protocol_->SendAudio(...)
│
▼
WebSocket 二进制帧 / UDP+AES 加密 → 服务端 ASR+LLM+TTS
下行:服务器 → 喇叭
WebSocket/UDP 收到二进制 Opus
│
▼ protocol_ on_incoming_audio 回调
audio_service_.PushPacketToDecodeQueue(...)
opus_codec_task
│
▼ Opus 解码
audio_playback_queue_
│
▼
audio_output_task
│
▼ AudioCodec::OutputData → I²S → 喇叭
控制信令(JSON)走另一条路:
WebSocket/MQTT JSON 文本
│
▼ protocol_ on_incoming_json
Application::InitializeProtocol 中注册的大 switch(按 type 分发)
├─ "tts" / "stt" / "llm" → 改设备状态 + UI
├─ "mcp" → McpServer::ParseMessage(工具调用)
├─ "alert" → 弹提示
└─ "system" → reboot 等
1.4 全文件分级总览表(这是本章最值钱的一张表)
分级说明:
- ★★★ 核心:业务主干,必须逐文件读懂,关键函数要逐段注释
- ★★ 重要:模块的稳定基础,要懂作用 + 主要类/函数 + 关键技术点
- ★ 辅助:偶尔读一眼即可,会按用途归类讲,但不展开
- ○ 板级硬件:110 块板子里挑代表讲,其余看名字知道是什么
- — 构建/资源:CMake / Kconfig / 资源文件 / CI / 文档,不是 C++ 业务代码
1.4.1 main/ 根目录(11 个文件)
| 文件 | 行数 | 分级 | 一句话作用 | 主要技术 |
|---|---|---|---|---|
main.cc | 30 | ★★★ | 入口;NVS 初始化 + Application 单例 + Run() | ESP-IDF app_main |
application.h | 186 | ★★★ | Application 类定义 + 13 种事件位宏 + AEC 模式枚举 | 事件组、单例 |
application.cc | 1055 | ★★★ | 大调度器:初始化、主循环、所有协议回调、激活、升级、状态切换 | FreeRTOS EventGroup、esp_timer、lambda、Schedule(任务)、互斥锁、unique_ptr |
device_state.h | 17 | ★★★ | 11 个状态枚举(idle/listening/speaking 等) | enum |
device_state_machine.h | 83 | ★★★ | 状态机 + 观察者回调 | std::atomic<>、互斥锁 |
device_state_machine.cc | 161 | ★★★ | 状态转换合法性表 + 通知监听者 | 状态机、switch case 转换表、观察者模式 |
ota.h | 58 | ★★ | OTA 类接口(CheckVersion / Activate / Upgrade) | — |
ota.cc | 473 | ★★ | 跟 xiaozhi.me 握手取配置、激活码循环、esp_https_ota 升级 | HTTP POST + JSON、HMAC-SHA256、esp_https_ota |
assets.h | 52 | ★★ | Assets 单例接口 | — |
assets.cc | 532 | ★★ | 管理 SPI Flash 上一个 assets 分区:下载、校验、mmap、按文件名取数据 | esp_partition_*、mmap、流式 CRC、HTTP 下载 |
mcp_server.h | 344 | ★★★ | Property / PropertyList / McpTool / McpServer 完整类型定义 + base64 编码 | std::variant、std::optional、模板、JSON-RPC 2.0 |
mcp_server.cc | 563 | ★★★ | MCP 协议解析 + initialize / tools/list / tools/call 三大方法实现 + 内置工具注册 | JSON-RPC、cJSON、线程池跑工具 |
settings.h | 28 | ★ | Settings 类接口 | — |
settings.cc | 108 | ★ | NVS 命名空间封装 | nvs_flash |
system_info.h | 22 | ★ | 静态方法集合 | — |
system_info.cc | 151 | ★ | 取 MAC / 芯片型号 / 堆内存 / 任务表 | esp_chip_info、uxTaskGetSystemState |
1.4.2 main/audio/ —— 音频子系统(19 个文件)
| 文件 | 行数 | 分级 | 一句话作用 | 主要技术 |
|---|---|---|---|---|
audio_codec.h | 61 | ★★★ | AudioCodec 抽象基类:双工标志、I²S 句柄、Read/Write 纯虚 | I²S、抽象基类 |
audio_codec.cc | 77 | ★★ | 基类默认实现:音量、增益、Start | 默认行为 |
audio_service.h | 160 | ★★★ | AudioService 接口 + 队列上限宏 + 事件位宏 + AudioServiceCallbacks | 任务/队列设计 |
audio_service.cc | 686 | ★★★ | 三任务(input/output/codec)+ 5 个队列 + 唤醒词/AFE 切换 + 节流 + Opus 重采样 | FreeRTOS Task、条件变量、deque 队列、std::chrono 节流、Opus 编解码、采样率重采样 |
audio_processor.h | 26 | ★★ | AudioProcessor 抽象基类(前端处理) | 抽象 |
wake_word.h | 26 | ★★ | WakeWord 抽象基类 | 抽象 |
processors/afe_audio_processor.{cc,h} | 187+ | ★★ | ESP-SR AFE 实现:VAD + AEC + 降噪 | ESP-SR AFE API |
processors/no_audio_processor.{cc,h} | 59+ | ★ | 兜底无处理实现(小芯片用) | 直通 |
processors/audio_debugger.{cc,h} | 67+ | ★ | 录音转发到本机 Python 脚本听效果 | UDP 流 |
wake_words/afe_wake_word.{cc,h} | 208+ | ★★ | 用 ESP-SR AFE 内置唤醒词 | wakenet 模型 |
wake_words/custom_wake_word.{cc,h} | 254+ | ★★ | 自定义 MFCC + 简单分类器 | 信号处理 |
wake_words/esp_wake_word.{cc,h} | 87+ | ★★ | 兼容小芯片(C3)的轻量唤醒 | wakenet9 |
codecs/no_audio_codec.{cc,h} | 359+ | ★★ | 软件 I²S codec(无外置芯片,靠 MCU 自驱 PDM/麦克风) | I²S std、PDM |
codecs/es8311_audio_codec.{cc,h} | 196+ | ○ | ES8311 I²S codec 芯片驱动(最常见) | I²C 配置寄存器 |
codecs/es8374_audio_codec.{cc,h} | 197+ | ○ | ES8374 codec | 同上 |
codecs/es8388_audio_codec.{cc,h} | 221+ | ○ | ES8388 codec(lyrat/某些 box) | 同上 |
codecs/es8389_audio_codec.{cc,h} | 203+ | ○ | ES8389 codec | 同上 |
codecs/box_audio_codec.{cc,h} | 244+ | ○ | 乐鑫 ESP-BOX 自家组合方案 | 双芯片协作 |
codecs/dummy_audio_codec.{cc,h} | 20+ | ○ | 无音频硬件占位(编译过) | 假实现 |
README.md | 几行 | — | 模块说明 | — |
1.4.3 main/protocols/ —— 通信协议(6 个文件)
| 文件 | 行数 | 分级 | 一句话作用 | 主要技术 |
|---|---|---|---|---|
protocol.h | 98 | ★★★ | Protocol 基类 + AudioStreamPacket + 两版二进制帧头 + 几个 enum | 抽象基类、PoD struct + __attribute__((packed)) |
protocol.cc | 90 | ★★★ | 公共回调注册 + 发送 abort/listen/mcp 控制 JSON + 超时判断 | JSON 字符串拼接 |
websocket_protocol.h | 34 | ★★★ | WebsocketProtocol 类声明 | — |
websocket_protocol.cc | 253 | ★★★ | 单条 WSS 连接同时传 JSON 文本 + Opus 二进制帧 | esp_websocket_client、HTTP hello 握手 |
mqtt_protocol.h | 65 | ★★★ | MqttProtocol 类声明 | — |
mqtt_protocol.cc | 382 | ★★★ | MQTT 走控制信令;UDP+AES-128-CTR 走 Opus 媒体流;hello/goodbye 协调 | MQTT 客户端、UDP socket、mbedtls AES-CTR、随机 nonce |
1.4.4 main/display/ —— 显示子系统(13 个文件)
| 文件 | 行数 | 分级 | 一句话作用 | 主要技术 |
|---|---|---|---|---|
display.h | 81 | ★★ | Display 基类接口 + DisplayLockGuard RAII + NoDisplay 占位 | 抽象、RAII |
display.cc | 56 | ★ | 基类默认实现(多数为空、被子类覆盖) | — |
oled_display.{cc,h} | 396+ | ★★ | SSD1306/SSD1315 OLED LVGL 适配 | LVGL、I²C 屏 |
lcd_display.{cc,h} | 1196+ | ★★ | LCD(ST7789、ILI9341、AMOLED、e-Paper)LVGL 适配,含状态栏/聊天气泡布局 | LVGL widget、字体、按需重绘 |
emote_display.{cc,h} | 657+ | ★ | 表情专用显示风格(动画化、整屏铺满) | LVGL Animation |
lvgl_display/lvgl_display.{cc,h} | 258+ | ★★ | LVGL 启动、tick 任务、主题切换 | LVGL Port |
lvgl_display/lvgl_theme.{cc,h} | 30+ | ★ | 主题(深/浅色) | — |
lvgl_display/lvgl_font.{cc,h} | 12+ | ★ | 字体注册(普惠/font_awesome) | — |
lvgl_display/lvgl_image.{cc,h} | 63+ | ★ | 图片资源解码 | — |
lvgl_display/emoji_collection.{cc,h} | 123+ | ★ | 表情包查表(twemoji 32/64) | — |
lvgl_display/gif/{gifdec.c, lvgl_gif.cc} | — | ★ | GIF 解码(emote_display 用) | — |
lvgl_display/jpg/{image_to_jpeg.cpp, jpeg_to_image.c} | — | ★ | JPEG 编解码(带相机的板子用) | — |
1.4.5 main/led/ —— LED 状态指示(4 个文件)
| 文件 | 行数 | 分级 | 一句话作用 | 主要技术 |
|---|---|---|---|---|
led.h | 17 | ★ | Led 基类 + NoLed 占位 | 抽象 |
single_led.{cc,h} | — | ★ | 单 LED(GPIO 闪烁) | esp_timer 定时切换 |
gpio_led.{cc,h} | — | ★ | 多色 LED(多 GPIO 组合) | — |
circular_strip.{cc,h} | — | ★ | WS2812 RGB 灯环(state→颜色映射) | RMT 驱动 |
1.4.6 main/boards/common/ —— 板级共用基类(22 个文件)
| 文件 | 行数 | 分级 | 一句话作用 | 主要技术 |
|---|---|---|---|---|
board.h | 93 | ★★★ | Board 基类 + DECLARE_BOARD 宏 + NetworkEvent enum + PowerSaveLevel | 抽象基类、虚函数、DECLARE_BOARD 单例宏 |
board.cc | 178 | ★★ | 公共方法默认实现(GenerateUuid、GetSystemInfoJson、GetLed/Display 默认占位) | UUID 生成 |
wifi_board.{cc,h} | 359+ | ★★★ | 所有 WiFi 板子的基类:连 WiFi、配网(SmartConfig/AP/BluFi)、事件分发 | esp_wifi、esp_netif、事件 |
ml307_board.{cc,h} | 274+ | ★★ | 4G 模组(中移 ML307)板子的基类 | UART + AT 指令 |
dual_network_board.{cc,h} | — | ★ | 同板支持 WiFi+4G 切换 | 双栈共存 |
button.{cc,h} | — | ★★ | 按键防抖、长短按区分 | iot_button 组件 |
knob.{cc,h} | — | ★ | 旋钮(编码器) | iot_knob |
backlight.{cc,h} | — | ★ | LCD 背光 PWM | ledc |
power_save_timer.{cc,h} | — | ★ | 空闲计时降功耗 | esp_timer |
sleep_timer.{cc,h} | — | ★ | 深度睡眠倒计时 | esp_sleep |
system_reset.{cc,h} | — | ★ | 长按硬件复位、出厂 | nvs erase + reset |
i2c_device.{cc,h} | — | ★ | I²C 设备父类(codec/PMIC 复用) | i2c_master |
adc_battery_monitor.{cc,h} | — | ★ | ADC 读电压估电量 | adc_oneshot |
axp2101.{cc,h} | — | ○ | AXP2101 电源管理 IC 驱动 | I²C |
sy6970.{cc,h} | — | ○ | SY6970 充电 IC | I²C |
camera.h + esp32_camera.{cc,h} | — | ○ | DVP/MIPI 摄像头驱动(带摄像头的板子用) | esp32-camera 组件 |
lamp_controller.h | — | ○ | 灯具控制接口 | — |
afsk_demod.{cc,h} | — | ○ | AFSK 声波配网解调(手机用 web 发 wifi 配置) | DSP |
blufi.{cpp,h} | 777 | ○ | 蓝牙 BLE 配网(BluFi 协议) | nimble BLE |
press_to_talk_mcp_tool.{cc,h} | — | ★ | 把"按住说话"模式注册成 MCP 工具 | MCP tool 复用模板 |
1.4.7 main/boards/<各板子>/ —— 110 块开发板
每块板子目录下基本只有 3 类文件:
<board-name>/
├── config.h 硬件 GPIO 引脚号宏定义
├── config.json 给 CI 用的"这个板子叫什么、追加哪些 sdkconfig"
├── <board>_board.cc 继承 WifiBoard / Ml307Board,实现 GetAudioCodec / GetDisplay 等
└── README.md / 图片 可选
本文档不会把 110 块全部拆开讲——里面 95% 是引脚号差异。后面第 9 章会挑 3 个代表(
bread-compact-wifi面包板、esp-box-3乐鑫官方开发板、xmini-c3-4g带 4G 的硬件)讲清楚"如何看懂一个板子目录"。其余板子你看到目录名xxx-s3-touch-amoled-1.8这种就能猜出大概,遇到具体硬件 GPIO 才去对应目录翻config.h。
1.4.8 main/assets/ —— 资源文件(非代码)
| 子目录 | 内容 | 分级 |
|---|---|---|
common/ | 5 个通用 OGG 提示音(success / exclamation / vibration / popup / low_battery) | — |
locales/<lang>/ | 36 种语言的 language.json(文本翻译表)+ 部分语言独有的 OGG 数字播报音(活码激活时) | — |
lang_config.h | CMake 阶段由 scripts/gen_lang.py 自动从对应 locale 的 JSON 生成的 C++ 头 | — |
讲解时这里只需要知道:"文本翻译表 + 提示音 ogg 文件,编译期被嵌进固件二进制"。
1.4.9 顶层辅助资源
| 路径 | 分级 | 用途 |
|---|---|---|
partitions/v1/、partitions/v2/ | — | 不同 Flash 容量、不同芯片的分区表 CSV |
scripts/gen_lang.py | — | 从 locales/*/language.json 生成 C++ 头 |
scripts/build_default_assets.py | — | 把字体+表情+唤醒词模型打成一个 assets.bin |
scripts/release.py、scripts/versions.py | — | 发版构建脚本 |
scripts/audio_debug_server.py | — | 配合 audio_debugger.cc 监听设备的 UDP 流听音质 |
scripts/ogg_converter/、scripts/mp3_to_ogg.sh | — | 提示音转码工具 |
scripts/Image_Converter/ | — | LVGL 图片资源转换 |
scripts/spiffs_assets/、scripts/p3_tools/、scripts/acoustic_check/ | — | 资源/声学测试小工具 |
scripts/sonic_wifi_config.html | — | AFSK 声波配网用的浏览器页面 |
docs/mcp-protocol.md | — | MCP 协议官方说明 |
docs/mcp-usage.md | — | MCP 用法教程 |
docs/websocket.md | — | WebSocket 通信细节 |
docs/mqtt-udp.md | — | MQTT + UDP 媒体流细节 |
docs/custom-board.md | — | 怎么添加自己的板子 |
docs/blufi.md | — | BluFi 蓝牙配网说明 |
docs/v0/、docs/v1/ | — | 旧版资料 |
.github/workflows/ | — | 每块板子的 CI 构建工作流 |
1.5 本项目核心技术与方法清单
后面章节会随用随讲,这里先列一份"看这份代码会学到什么"的索引:
1.5.1 FreeRTOS / 嵌入式并发原语
| 技术 | 在哪里用到 | 干什么 |
|---|---|---|
| EventGroup(事件组) | application.cc 的 event_group_、audio_service.cc 的 event_group_ | 不同 task 之间用 1 bit 1 个事件来"提醒"主循环干活 |
| xTaskCreate | audio_service.cc(input/output/codec 三个任务)、application.cc 的激活任务、各板子的按键任务 | 创建独立 RTOS 任务 |
| esp_timer 周期定时 | application.cc 的 clock_timer(1 秒滴答更新状态栏)、led/single_led.cc 的闪烁 | 软定时器在 timer 任务上下文回调 |
| std::mutex / std::lock_guard | application.cc 的 main_tasks_ 队列、device_state_machine.cc 的 listeners_、audio_service.cc 的 audio_queue_mutex_ | 跨任务共享数据保护 |
| std::condition_variable | audio_service.cc 的 audio_queue_cv_ | 队列空/满时阻塞/唤醒 codec 任务 |
| std::atomic | device_state_machine.cc 的 current_state_ | 状态字段无锁读写 |
| vTaskDelete | 激活任务做完后自杀 | 一次性任务清理 |
uxTaskPriorityGet/Set + RAII(TaskPriorityReset) | application.h 末尾那个 helper 类 | 临时提权再恢复,常见于关键短任务 |
1.5.2 状态机 + 观察者
- 11 个状态(
device_state.h); - 一张合法转换表(
device_state_machine.cc::IsValidTransition)——非法转换直接拒绝并ESP_LOGW; - 观察者:
AddStateChangeListener注册回调,转换成功后挨个调; Application把"状态变了"翻译成MAIN_EVENT_STATE_CHANGED事件位,再在主循环里执行真正的副作用(点灯/切音频任务/UI),保证副作用都跑在主任务上下文。
1.5.3 单例模式 + DECLARE_BOARD 宏
Application::GetInstance()、McpServer::GetInstance()、Assets::GetInstance()、Board::GetInstance()都用 C++11 magic static(线程安全的局部静态变量);- 但是
Board比较特别:用宏DECLARE_BOARD(BoardClass)由具体板子定义create_board(),Board::GetInstance()内部调用static_cast<Board*>(create_board())完成多态实例化。整个项目只有一处板级实例化,靠 CMake 选板子的源文件来决定create_board()到底返回哪个子类。这就是"110 个板子怎么共用同一份 Application 代码"的关键。
1.5.4 工厂 + 抽象基类
每个子系统都搭一对"抽象基类 + 多个实现 + 工厂选择":
| 基类 | 多个实现 | 工厂决定点 |
|---|---|---|
Board | WifiBoard / Ml307Board / DualNetworkBoard / 110 个具体板子 | DECLARE_BOARD 宏(CMake 选板子) |
AudioCodec | NoAudioCodec / Es8311AudioCodec / Box* / ... | 各板子的 GetAudioCodec() |
Display | OledDisplay / LcdDisplay / EmoteDisplay / NoDisplay | 各板子的构造函数里 new |
WakeWord | AfeWakeWord / CustomWakeWord / EspWakeWord | audio_service.cc::Initialize + Kconfig |
AudioProcessor | AfeAudioProcessor / NoAudioProcessor | audio_service.cc::Initialize + Kconfig |
Protocol | WebsocketProtocol / MqttProtocol | application.cc::InitializeProtocol + 服务器下发的配置 |
Led | SingleLed / GpioLed / CircularStrip / NoLed | 各板子的 GetLed() |
1.5.5 回调式编程(避免上层关心下层细节)
Protocol::OnIncomingJson(...)让Application决定 JSON 来了怎么处理;AudioServiceCallbacks让Application决定唤醒/VAD/可发包了怎么响应;Board::SetNetworkEventCallback(...)让Application在网络变化时改 UI 和状态;McpTool的std::function<ReturnValue(const PropertyList&)>让每个工具的具体实现可以是 lambda、可以是成员函数;Ota::Upgrade(...)接受进度回调,把"进度怎么显示"和"OTA 怎么下载"解耦。
1.5.6 JSON-RPC 2.0(MCP 协议)
mcp_server.cc解析method字段,分发到initialize/notifications/initialized/tools/list/tools/call;- 工具描述(input schema)按 JSON Schema 规范写:type=object, properties={}, required=[…];
- 调用结果统一封装成
{"content":[{"type":"text","text":"..."}],"isError":false}; - 大模型那边可以理解"这台设备暴露了 set_volume / set_brightness / press_to_talk 等工具,调用它们"。
1.5.7 二进制流协议(音频帧)
- WebSocket:帧头
BinaryProtocol2(16 字节)+ Opus payload; - MQTT+UDP:UDP 帧头
BinaryProtocol3(4 字节)+ AES-128-CTR(nonce, opus_payload); - 时间戳字段用于服务端 AEC(回声消除):服务器知道刚才发的是哪一段 TTS,就能在你这一段输入里减掉它。
1.5.8 资源管理与 Flash 分区
- 分区表(
partitions/v2/*.csv)划出assets分区; assets.bin的格式:固定头 + 文件索引表 + 各文件原始数据;- 运行期:
esp_partition_mmap()把分区直接映射进 CPU 地址空间,然后按Asset { offset, size }表去拿数据——字体直接 mmap 给 LVGL、模型直接 mmap 给 ESP-SR、不占内存。 - 升级:HTTP 下载新的
assets.bin→ 写入分区 → 校验 CRC → 重 mmap → 应用。
1.5.9 ESP-IDF 特定 API 速览
nvs_flash_init():键值存储初始化;esp_event_loop_create_default()(在 wifi_board 里):默认事件循环;esp_https_ota_*:固件 OTA;esp_partition_*、esp_partition_mmap():分区表读写、mmap;esp_timer_*:高精度软定时器;esp_chip_info()/esp_efuse_*/esp_mac_addr_*:硬件信息;esp_wifi_*/esp_netif_*:网络栈;i2s_std_*/i2s_pdm_*:音频外设;mbedtls_*:base64 / SHA / AES。
1.5.10 第三方库
- cJSON:所有 JSON 解析/序列化;
- Opus(含 encoder/decoder/resampler 三个包装类,来自管理组件):语音编解码;
- ESP-SR:唤醒词模型 (wakenet) + AFE 前端处理;
- LVGL:UI 框架;
- esp-websocket-client:WebSocket;
- esp-mqtt:MQTT 客户端;
- iot_button、iot_knob:按键/旋钮;
- mbedtls:加解密;
- 4G 模组用 ML307 SDK(espressif/atomic_*)。
1.6 看完本章你应该掌握的
- 知道
main/是业务代码,其它目录只是构建/资源 - 知道一次开机会经过
app_main → Application::Initialize → board.StartNetwork → 等网通了 → ActivationTask → 拉服务器配置 → 选 WS/MQTT → 进 idle 等唤醒 - 能看着分级表估算优先级:核心 7-8 个文件读懂就能讲清整个项目
- 知道这份代码用到的核心技术清单,遇到具体代码段能猜到属于哪个技术
下一章开始进入 main.cc + application.cc/h——主调度器逐段讲解。
参考资料:
- 项目自带
README_zh.md已实现功能列表docs/mcp-protocol.mdMCP 协议docs/websocket.mdWebSocket 帧格式docs/mqtt-udp.mdMQTT + UDP 媒体流docs/custom-board.md添加自定义板子