文档:小智 AI 硬件开放应用平台 —— 总体架构与开发 Plan
Version: V1.1
Date: 2026-08-30
本文在 V1.0 概念稿上,依据《Xiaozhi AI 开放应用平台 Master Plan》与《Architecture Audit》修订。
只设计,不编码。
本文只定义 Device / Client(仓库 xiaozhi-esp32-2.2.6)如何从「语音对话固件」演进为「可执行多种 InteractionMode 的 Device Runtime」。
不在本文展开、由 Server Plan 负责的内容:
相对 V1.0 的关键修正:
| V1.0 | V1.1(本次) |
|---|---|
| 设备上有完整 Task Runtime | 权威 Task Runtime 在 Server;设备只有 Task Executor |
| Phase 1 做复读 + 跟读 + 口算 + 成语接龙 | Phase 1 只做 CHAT + PLAY_AUDIO(英语复读),不破坏现有 Chat |
| 新造统一 Command Protocol | 扩展现有 JSON type + MCP,不新开总线 |
Skill 列表含 math.calculate / llm.evaluate |
Skill 必须映射到**已有代码**;没有的标为新建 |
| 设备维护 App Manager 全套生命周期 | Phase 1–2 不在固件实现 App 商店;资源同步可复用板级下载 |
当前固件**不是** App 平台,而是:
Application 单例
+ DeviceState 状态机
+ Opus 实时双工
+ MCP 工具面
| 项 | 真实位置 |
|---|---|
| 入口 | main/main.cc → Application::Initialize → Application::Run |
| 主循环 | application.cc:FreeRTOS EventGroup,永不返回 |
| 对话态 | device_state.h:Idle / Connecting / Listening / Speaking … |
| 音频管道 | audio/audio_service.{h,cc}:Mic→Opus→Server;Server→Opus→Speaker |
| 协议 | protocols/protocol.{h,cc} + websocket_protocol / mqtt_protocol |
| Skill 面 | mcp_server.cc + 各 board InitializeTools() |
| 资源 OTA | ota.{h,cc}(固件/配置)、assets.{h,cc}(语音模型等) |
| 板级旁路 | Sening:self.mp3.*、BoardMqtt(snlp/v1 DOWNLOAD / PLAY_MEDIA) |
完整对话链:
Mic → AudioService → Opus 16k/60ms → Protocol.SendAudio
→ Server ASR/LLM/TTS
→ type=tts/stt + binary Opus → AudioService decode → Speaker
没有: InteractionMode 枚举、Task/Step、可归档录音文件、核心 play_audio(仅 Sening 板级)、统一 Event Bus。
ListeningMode(auto/manual/realtime)只是拾音策略,**不是** Master Plan 的 InteractionMode。
目标不是把手机 OS 搬到 ESP32,而是:
Device Runtime ← 现有 Application 事件环升级,不是推翻
├── Chat Runtime ← 现有 Listening ↔ Speaking,降为 Mode=CHAT
└── Task Executor ← 薄执行器:收指令、跑 Skill、回 Event
├── InteractionMode 切换
└── Skill → Device Capability
同一时间只运行一个 InteractionMode(Chat 与 Task 音频互斥)。
设备**不理解**业务:
禁止固件出现:if (app == english) / if (task == idiom)
设备只理解:
mode + skill + resource_ref + 完成条件(超时 / 播放结束 / VAD 静音)
Server(权威)
创建 TaskInstance
推进 Step
指定 InteractionMode
ASR / LLM / Assessment
持久化状态与音频对象
Client(执行)
保持 Chat 热路径
按 mode 使用麦/扬声器/屏
执行 Skill(播、听、显示、下载)
上报 Event(开始/结束/失败)
缓存当前 step(断线 UI),不以本地为权威
对应 Master Plan:Device 负责执行、播放、采集、缓存、通信;Server 负责调度与评估。
业务层 App / Task ← 不下发到固件业务代码;只下发 JSON + 资源
交互层 Interaction Mode ← 新增;挂在 Application,与 DeviceState 正交
能力层 Skill ← 现有 MCP tools 升级为正式 Skill 目录
设备层 Device Capability ← AudioCodec / Display / Button / SD / LED
DeviceState 与 InteractionMode 必须分开:
| DeviceState(已有) | InteractionMode(新增) | |
|---|---|---|
| 回答 | 设备现在能不能听/说/升级 | 这一步用什么交互方式 |
| 例子 | Idle, Listening, Speaking | CHAT, PLAY_AUDIO, FOLLOW_READ |
| 谁改 | 音频通道占用 | Server 指定 / Client 执行 |
不要把 PLAY_AUDIO 做成一个新的 kDeviceStatePlayingEnglishLesson。播放时可保持 Idle 或增加**非对话**占用标记,但 禁止把 Task 塞进 Listening/Speaking 迁移表。
现有 Chat 必须继续作为默认能力。无 App、无 Task 时:
唤醒 / 按键 → Connecting → Listening ↔ Speaking → Idle
路径、协议 hello/listen/abort/tts/stt/mcp、Opus 热路径 Phase 1–2 禁止改语义。
降级方式:
InteractionMode = CHAT
→ 走现有 Application 对话状态机
InteractionMode = PLAY_AUDIO
→ 不进入 LLM
→ 扬声器给 play_audio
→ 麦可关或只用于打断/唤醒退出
任意 Mode 下唤醒词 / 退出
→ 回到 CHAT 或 Idle(可配置)
Chat 不是 Task Runtime 的父容器,而是 Device Runtime 下与 Task Executor 并列的子 Runtime。
V1.0 把 Task Runtime 画在设备上。Audit 结论:编排已在 Server ConnectionHandler。设备侧只增加:
收到 skill.execute / 板级 PLAY_MEDIA / MCP tools/call
→ 执行
→ 上报 completed / failed
→ 等待下一步
设备 不 负责:
设备 可以 本地判断的完成条件仅限硬件可观测:
播放结束
VAD 静音超时
用户按键取消
下载完成 / 失败
状态机(执行器级,缓存用):
IDLE → EXECUTING → WAITING_USER? → COMPLETED | FAILED | CANCELLED
权威状态仍在 Server 的 TaskInstance / StepInstance。
Interaction Mode 不是业务场景(不是「英语课」),只描述硬件怎么与人交互。
| Mode | Phase | 现有代码基础 | 设备要做的 |
|---|---|---|---|
CHAT |
0(已有) | Listening/Speaking | 保持 |
PLAY_AUDIO |
1 | Sening self.mp3.play / PLAY_MEDIA;PauseSdCardMp3 |
抽成可调用 Skill;与 TTS 互斥 |
FOLLOW_READ |
2 | listen 流 + 播放 | 先播后听;听完上报,不进 LLM |
LISTEN |
2 | SendStartListening |
可单独听,不自动 Chat |
QUIZ / TRANSLATION / GAME |
3 | 无独立 Mode | 先用 CHAT+Skill 组合,再固化 Mode |
V1.0 要求第一阶段就支持 QUIZ/TRANSLATION/GAME —— 取消。那些是 Server 编排 + CHAT/LISTEN 即可先做,不值得先改固件状态机。
Server:重复 N 次的编排(权威)
Client:play_audio(resource) → 播完 event → 等下一次 play 或 stop
LLM = false,ASR = false
TTS 来时沿用现有 PauseSdCardMp3 / resume_sdcard_mp3_after_tts_,**不要**另写一套混音。
play 标准音频
→ play.completed
listen(复用上行 Opus)
→ speech.stopped / listen.completed
Server ASR + v0 文本评估
→ speak 反馈(可走现有 TTS)
设备不评分。
Skill = 对 Device Capability 的稳定封装。第三方 App 禁止直接 GPIO/I2S。
| Skill | 当前实现 | 范围 |
|---|---|---|
device.get_status |
self.get_device_status |
全局 MCP |
audio.set_volume |
self.audio_speaker.set_volume |
全局 |
screen.set_brightness / set_theme |
self.screen.* |
有屏 |
camera.take_photo |
self.camera.take_photo |
有摄像头 |
audio.play_sound |
AudioService::PlaySound(OGG 提示音) |
全局;不能当课程音频 |
mp3.play/stop/list |
self.mp3.* |
Sening |
media.play |
BoardMqtt PLAY_MEDIA |
Sening snlp |
resource.download |
DOWNLOAD / Assets |
Sening / Assets |
alarm.* |
self.alarm.* |
Sening;≠ 家长 Schedule |
led / 运动类 |
各 board MCP | 板相关 |
play_audio
输入:SD 路径或已下载 resource_id
实现:优先复用 self.mp3.play / PLAY_MEDIA
目标:从 board 私有工具提升为 Runtime Skill(可仍 ifdef 板型)
不要求所有板第一天都能 play MP3。无 SD 的板 Phase 1 可用 Server 推流(不推荐占 TTS 通道做复读循环)。
| Skill | 现状 | 设计 |
|---|---|---|
record_audio(可归档) |
不存在;只有流式 Opus | 优先复用 listen 流,由 Server 句级落 WAV;设备端文件录音为增强(SD /recordings/pending) |
listen(不进 Chat) |
listen 消息会进入对话 | Mode=FOLLOW_READ/LISTEN 时 Client 仍发 Opus,Server 跳过 LLM |
wait |
无 | 可用 Server 计时;设备只需播完/超时事件 |
display.show |
有屏 MCP / SetChatMessage |
可扩展文本/图片资源 |
asr.transcribe → Server
llm.chat/evaluate → Server
score.evaluate → Server
translation.translate → Server
math.calculate → Server
V1.0 Skill 表里的这些能力改挂 Server Plan。
对话:PCM 16k → Opus 60ms 流 → 不写盘 → Server 可选转 WAV 入库
提示音:OGG PlaySound
Sening 课程:SD MP3 独立播放,TTS 时 Pause
热路径禁止改:MAIN_EVENT_SEND_AUDIO、AudioInputTask、OpusCodecTask、FinishTtsResponse。
同一时刻扬声器只服务一个源:TTS | SD/MP3 | PlaySound
CHAT 的 Listening/Speaking 优先于 PLAY_AUDIO(可暂停课程音频)
Task 需要麦时:要么 Mode 独占 listen,要么明确与 Chat 互斥
不先做设备完整 wav 文件上传。
复用现有上行 Opus
Server 按句 PCM→WAV(已有 chat_history_conf=2)
新增关联:audio_id ↔ step_instance_id
离线缓存 /recordings/pending 放到 Phase 3,除非跟读在弱网成为阻塞。
设备侧 没有 audio_id;只有 session_id(连接级)。audio_id 由 Server 生成后回传(可选)。
原则保留 V1.0 / Master Plan:
OTA = 驱动、协议、Audio Engine、Runtime、Skill 宿主
App Sync = manifest、task JSON、MP3、图片、配置
App 原则上不 OTA。但下列能力必须在 Firmware 里(可一次 OTA 补齐,之后新 App 不再改 C++):
Codec / VAD / 唤醒
WS 或 MQTT+UDP 协议栈
MCP / Skill 宿主
SD 文件系统
HTTP 下载(或等价)
音频互斥
InteractionMode 开关
(Phase 2)非 Chat 的 listen 语义
/sdcard/
apps/<appId>/ # manifest + tasks JSON
resources/<appId>/ # mp3 / 图
recordings/pending/ # Phase 3
cache/
Sening 当前下载路径偏 /data/courses/...(smartlearning)。演进时 映射到统一布局,不要强迫立即改终端已有文件。
当前存在两套:
| 通道 | 用途 |
|---|---|
Xiaozhi Protocol |
对话 JSON + Opus |
Sening snlp/v1 |
心跳、DOWNLOAD、PLAY_MEDIA、ack/progress |
Phase 1: 允许并存;复读优先走已能工作的 MCP/PLAY_MEDIA。
Phase 2–3: 资源同步与播放控制逐步收到 Xiaozhi JSON type 或 MCP,snlp 降为板级实现细节,避免业务云长期分叉。
禁止 Phase 1 合并两套 MQTT。
现有设备→云:hello / listen / abort / iot / mcp / ping / server
现有云→设备:hello / stt / tts / llm / mcp / system / alert / custom
不强制新 type。复用:
type=mcp tools/call self.mp3.play | stop
或 snlp PLAY_MEDIA
下行:
task.start / task.stop / task.pause / task.resume
step.start
skill.execute
mode.set # CHAT | PLAY_AUDIO | FOLLOW_READ | LISTEN
上行:
task.started / task.completed / task.failed
step.started / step.completed / step.failed
skill.completed / skill.failed
audio.play.completed
speech.started / speech.stopped # 可从现有 VAD 事件导出
载荷必须带:session_id;Phase 2 起带 task_instance_id / step_id。
不要新建第四套 EventBus。Sening rpt/event 可作实现,但语义应对齐上表。
插入点:Application 中 protocol_->OnIncomingJson(application.cc 约 647–778 行),unknown type 分支旁增加 task / mode。
现有机制:MAIN_EVENT_* + 协议回调 + MCP JSON-RPC +(Sening)rpt/ack。
Task Executor 挂载顺序(侵入性从低到高):
tools/call — Phase 1 首选,Chat 路径不动Application::Schedule — 线程安全推进 UI / 非音频副作用DeviceStateMachine::IsValidTransition 的 Listening/Speaking 边按键:今日绑定 ToggleChatState / Start/Stop Listening。Phase 2 起允许「取消当前 Task / 跳过」而不拆掉 Chat 按键。
设备只持久化必要项(NVS / 内存):
activeMode
activeTaskInstanceId # 缓存,权威在 Server
activeStepId # 缓存
sdcard play 状态 # 已有
不在设备保存完整 Step 历史、评分、ASR 文本。
App 生命周期 V1.0 的 INSTALL/LAUNCH/ACTIVE 全套 Phase 3 再做。Phase 1 甚至不需要 activeAppId。
与 Server Plan 阶段编号对齐。
目标:英语复读 N 遍。
Client 工作:
self.mp3.play / PLAY_MEDIA + PauseSdCardMp3验收:
mode.set + type=task / skill.execute验收:一次跟读 Step 能在 Server 看到 audio_id + asr_text。
play_audio / 下载进非 Sening 抽象(或明确仅支持 SD 板)EnglishRepeatManager 这种一次性类(可用薄 TaskExecutor + Skill)Application / AudioService / Protocol / mcp_server,再改。Client 保证:
play_audio(至少 Sening 路径)session_id(及后续 instance id)Client 要求 Server:
ConnectionHandler.chat()下一份可执行文档应是 Phase 1 Coding Plan(Client 切片),而不是继续加 Mode。