这篇要完成什么,尚未验证什么
这是一份完整有屏幕的DIY派生工程,不是给A08换标题:保留按键语音、OLED字幕和MCP实验灯,新增主循环状态面板、请求错误/硬件故障分层、显示写入串行化和明确的恢复动作。LISTEN/THINK/PLAY分别是本地状态、等待标记与协议/队列观察,不证明已采到清晰声音、LLM正在计算或喇叭已出声。
30轮实体对话尚未执行,dialog-rounds.csv/hardware-observations.csv只有表头。生产面板的30轮主机输入是synthetic逻辑夹具,不是连续对话成功率。
参考:[13] 固定Application:事件、手动采音、协议回调与主循环[20] 固定DeviceStateMachine:合法状态转换与监听[21] 固定OledDisplay:128×32布局/字幕滚动[23] 固定McpServer:解析、调度、回包/异常
1. 桌面原型的完整器材和接线
本包是完整小智v2.4.0 / 5540258abcbfa62518d09959308200be1c5b1b2b派生工程,固定ESP-IDF5.5.5/Python3.12.13,采用A07明确的两处板局部兼容补丁:I2C命名初始化顺序,以及MAX98357A输出24k→16k。上游最低>=5.5.2不是本板原样成功保证;v2.5.x要求>=6.0.1,不能替换tag后沿用本锁。原1590跟踪文件、实际dependencies.lock、全部本站生产源、差异与许可均提供;不下载第二SDK,不把补丁版称原样上游。
本组合仅官方ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N16R8,16MB Quad Flash/8MB Octal PSRAM80MHz。A01/A02的N8R8不是本工程目标。保留bread-compact-wifi板标识/工厂与所有既有引脚,原生USB19/20不接,桥接UART0为43/44,35/36/37留给OPI。GPIO48的原SingleLed不是v1.1板载RGB38;这里两者都不外接,不把它们当实验灯。
两套独立I2S主时钟,不短接时钟:输入16kHz、32bit LEFT槽、24有效位;固定Simplex源码右移12再限16bit,不能照抄A01的增益/DC算法。输出16kHz、32bit LEFT槽,64fs标称BCLK1.024MHz;实际时钟未测。MAX98357A Rev7 p6保证LRCLK为7.6–8.4/15.2–16.8/30.4–50.4/83.8–100.8kHz,原版24k不在保证范围。服务端24k Opus经原AudioService/esp_audio_effects1.3.0~1的24→16重采样再送本板codec,不把服务端采样率直接接到功放LRCLK。
软件音量平方缩放,默认70且NVS可恢复,非正值开机会回10;它不是开机静音保证。先保持SD断开、核VIN/共地/短路,再从低音量试验;喇叭1W额定不证明实际输出功率。原重采样open失败只记日志、空handle会旁路,get_max_out/process返回值未完整检查。看到Failed to create output resampler/Opus decoder、异常语速/音调/压降/发热时先断开SD、再停功放外供,查堆/PSRAM/decoder率并RESET;本站没有声称自动静音或DSP质量通过。没有AEC,不做边听边播实验。
额外实验灯固定WP7113ID红LED:GPIO18→1kΩ→LED阳极,阴极→GND。LED排针/杜邦接点必须可靠;由原A08单一GpioLed/16项本地去重策略拥有GPIO18,移除了原LampController的第二写入者。数字pad回读不能检测LED开路/损坏/光是否可见;不能用此实验工具替代真实负载控制。
| 具体器材 | 断电接线与核对 |
|---|---|
| PID6049 ICS43434,匹配停产存量且排针可靠焊接 | 3V→主板3V3,GND共地,SEL→GND,WS4/SCK5/DOUT6;非PDM/模拟麦克风 |
| PID3006 单声道MAX98357A | DIN7/BCLK15/LRC16;GAIN→功放VIN选择6dB;不要换成不同供电/偏置的“兼容”板 |
| PID1313 C2464-001,8Ω/1W | 仅接功放OUT+和OUT−;BTL两端均不接GND、示波器地夹或耳机公共地 |
| 功放外供 | 独立稳压3.3V、额定≥1A,仅与主板共地,正极不并到主板3V3/5V;不是以GPIO带负载 |
| 物理SD静音 | SD经10kΩ到GND;功放VIN经低压SPST和1kΩ到SD。开关断开=下拉静音,闭合才启用;首次上电/改线/诊断先断开 |
| PID4440,128×32 I2C、STEMMA QT自动复位revB | VIN→主板3V3,GND,SDA41/SCL42,0x3C;RST不另接。不是需手接RESET的旧PID931 |
| 手动采音按钮 | GPIO47→按钮→GND,外接10kΩ到主板3V3;39/40分别音量减/加,也各接10k上拉;BOOT0保留板载按钮 |
参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[2] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动[3] PID6049固定PCB:3V、SEL与47k数据下拉[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL[5] PID3006固定单声道PCB:VIN/SD1M/BTL端子[6] PID1313扬声器2017-02-14厂家规格:8Ω额定1W[7] PID6049产品页:具体型号与停产边界[8] ESP32-S3-WROOM-1/1U v1.8:N16R8规格表[9] PID4440固定revB PCB:上拉/电平转换/自动复位/128×32[10] 固定本板config:I2S/按钮/OLED/未接GPIO48[11] 固定Simplex codec:板参数/32bit槽/右移限幅/音量[26] 实际esp_audio_effects1.3.0~1来源:rate converter格式/采样率/API约束
2. 屏幕究竟显示哪一层状态
沿用PID4440的128×32版式:左32像素原表情,右96像素状态/字幕。本站固定短ASCII状态,隐藏右栏原网络/静音/电池图标以留宽度;音量变更等上游notification改留UART,不能临时盖住错误状态。正常时保留用户/回答滚动字幕;故障时固定提示并抑制晚到字幕。小屏激活信息看桥接UART日志,本例没有重新实现激活UI,也没有OCR/实屏可读性结果。固定本板编译起始字体为78/xiaozhi-fonts2.0.0的font_noto_sans_basic_14_1,默认assets还包含font_noto_sans_common_14_1.bin并由Assets::Apply尝试加载(均14px/1bpp,行高16),组件元数据声明Apache-2.0。已从与官方Registry2.0.0包逐字相同的basic生成C与common CBIN glyph/cmap/kerning数据分别计算,且common原字节实际位于本次generated_assets.bin:两套15个ASCII状态最大advance均64px,含左2px均落在94×16标签内;这是字体数据算术,不是LVGL/实屏渲染。长提示沿用循环滚动;外部OTA/custom assets替换字体会使该检查失效。任意中文依赖原basic/common字库及服务端TextGlyphPayload和动态字形机制,不能承诺所有汉字都有字形。
优先级为GPIO锁存→SYS ERR→主动CONFIG→Wi-Fi断线→后端错误→工具错误→正常阶段。错误不会因普通字幕或状态切回Idle自动消失。其范围也有限:GPIO18数字判断、已挂钩两个工具与协议错误/观察超时;没有检测所有MCP工具、I2C电气、麦克风音质、功放负载、STT准确度或云端执行。
| 状态 | 触发与可执行下一步 |
|---|---|
| BOOT / ACTIVATE / CONFIG | 启动/激活/配网状态;CONFIG是主动配置,不误判为断网。先看UART/匹配实际服务流程 |
| LISTEN | Application为Listening且无更高错误;GPIO47释放结束。不是麦克风质量检测 |
| THINK | 手动StopListening后等待本地回应观察;不是后端LLM内部状态 |
| PLAY | Application Speaking或播放队列未空;不是声学播放确认 |
| WIFI ERR | 观察到扫描/断线回调,且不在主动CONFIG;检查AP/供电/配置,重连后按新状态继续 |
| BACK ERR | 协议OnNetworkError或观察到等待≥60秒;先物理静音,诊断后端/decoder/内存与日志后重试 |
| TOOL ERR | 两个实验灯工具的原始参数拒绝、id冲突等可重试错误;修正参数/新id,成功工具或本地确认清软错误 |
| GPIO ERR | LED策略检测到本地初始化/写/数字回读故障并锁存;断电修接线,再RESET。查询/联网恢复/确认不清此锁 |
| SYS ERR / TEST / UPDATE | 原FatalError/本地AudioTesting/Upgrading的观察标签;不是自动恢复保证,更新期间保持供电 |
参考:[13] 固定Application:事件、手动采音、协议回调与主循环[20] 固定DeviceStateMachine:合法状态转换与监听[21] 固定OledDisplay:128×32布局/字幕滚动[22] 固定LvglDisplay:状态/通知/锁与字形[19] 固定CMake:本板工厂、14px字体与lang_header[25] 实际Registry 78/xiaozhi-fonts2.0.0发布包:字体/许可元数据[29] 固定Assets::Apply:common字体资源替换与字形能力[30] 固定default assets:basic→common CBIN打包
3. 用主循环拥有模型,用同一锁保护显示事务
网络/协议回调和raw参数validator可能在其他任务,只调用Application::Schedule投递模型更新;灯工具执行回调本来就在主循环。主循环处理事件后统一render,并查询同一LED owner的snapshot故障;不添加第二GPIO写入者,不改协议/NVS/SDK。
DeskDisplay把“检查是否允许上游状态/字幕”与“调用base UI写入”放在同一ui_mutex内。首次启用面板、错误提示和晚到字幕使用相同锁;只有atomic标志仍有check/use窗口,可能发生错误后旧调用覆盖缓存显示。作者保留两个旧完整class桌面反例(启动状态、晚字幕),修后完整class线程夹具都保持错误,不把它称FreeRTOS/LVGL/硬件排程证明。UI先锁自身再调用现有LVGL锁,不从音频任务写屏;无硬实时或即时网络停机保证。
60秒使用64位esp_timer,由主循环下一次render观察;阻塞调用/调度会延后显示。超时是UI诊断,不会撤回已在飞服务器回应或保证停声。物理SD才是此原型的人工静音入口。长按GPIO39仍执行原音量0,再排队确认软错误;它不取消服务器请求、不清GPIO故障、不能代替关SD。恢复后旧回应可能到来,观察并等待排空后开新轮。
参考:[13] 固定Application:事件、手动采音、协议回调与主循环[20] 固定DeviceStateMachine:合法状态转换与监听[23] 固定McpServer:解析、调度、回包/异常[24] 固定McpTool/PropertyList与callback契约[22] 固定LvglDisplay:状态/通知/锁与字形[18] ESP-IDF5.5.5 esp_timer:64位开机单调微秒
4. 构建、激活和首次低音量检查
先激活你已安装的ESP-IDF5.5.5环境,核idf.py --version确为ESP-IDF v5.5.5。运行工具时用该环境的Python3.12.13;串口采集额外固定pyserial3.5。python-requirements.lock只列本站工具直接依赖,SDK依赖仍按该SDK的原锁;managed组件按完整firmware/dependencies.lock固定74项(含IDF),保留原LICENSE/NOTICE,没有本地override。
首次build先跑上游lang_header,处理固定CMake缺少main→语言生成头显式依赖的原问题,不手改生成头。工具对与上次成功不同的编译输入刷新mtime并核字节不变,防COW/拷贝保旧mtime导致Ninja重用旧对象;实际新build日志、输入、bin/ELF/map/hash一起记录。仅已有bin不够,没有成功记录或源/产品/原log变化,manifest/flash拒绝。verify_project.py验证本交付固定源;你主动改源后它应拒绝,不能悄悄更新原版清单冒充原样。
改板型、Flash/PSRAM或defaults必须保原产物后用新的空build目录重新configure,并检查实际sdkconfig;defaults不是对已有sdkconfig强制覆盖。不要为了省空间复用其他板型build,勿更换SDK/关闭编译错误。当前固定N16R8与128×32/OLED/中文/raw/manual/禁WakeNet与AEC的实际构建结果另列验证表。
只在拥有相同具体实物并完成断电检查后,使用桥接UART USB口。先查local-products.json与该次build/flasher_args.json,再替换你自己的端口:
不把原生USB口当UART桥;BOOT进ROM时释放BOOT再RESET可恢复。必须携带同次bootloader/partition/app/assets及地址表,不能只写app.bin或沿用8MB布局。默认OTA/服务端更新可能替换本站派生固件;使用自己可控的服务配置/版本策略,升级前保完整镜像,不承诺云平台将保留本站补丁。联网/激活按实际服务条款和自己的账号操作,验证码、token、Wi-Fi密码与现实对话只留本地私有日志;本次没有账号、激活或真实音频上传/播放实验。
当前工程在相同标准锁/同一已安装SDK上真实增量构建,生产App/board对象与ELF独占A10v1标记一一对应。没有因为COW保旧mtime只检查Exit0;前置verify_project可核完整当前源码,实际构建表与原log另外列出。没有刷板/激活/对话运行结果。
首次实体流程:全部按钮释放、SD断开,桥接UART供电确认无复位循环;核OLED/I2C、主板与功放电压/共地、正确位宽/16k两时钟,然后按实际服务流程配网/激活。若后台自动更新会替换本站固件,先用可控版本策略/自有匹配后端处理,不能把默认云端更新当本站代码保留承诺。硬件检查通过后低音量、短问题试验;发热/异常音调先断开SD再诊断。
python tools/verify_project.py
python tools/project.py configure
python tools/project.py build
python tools/project.py manifest
python tools/project.py flash --port YOUR_UART_BRIDGE_PORT
python tools/project.py monitor --port YOUR_UART_BRIDGE_PORT参考:[28] 固定v2.4.0应用manifest:依赖与IDF最低约束[19] 固定CMake:本板工厂、14px字体与lang_header[10] 固定本板config:I2S/按钮/OLED/未接GPIO48[27] IDF5.5.5 LCD I2C真实字段顺序[8] ESP32-S3-WROOM-1/1U v1.8:N16R8规格表
5. 对话、工具和恢复的逐项操作
按以下步骤核对并保留实际记录。
- CONFIG时只完成配网;ACTIVATE按UART实际提示,用自己的账号,不将验证码/token写进公开表。得到Idle/READY并等队列排空后开始。
- 按住GPIO47提出短问题,确认LISTEN;释放后观察THINK、字幕JSON、PLAY与最终READY。每项分别记,不只写“成功”。BOOT原toggle、39/40音量与长按39软确认按表使用。
- 让支持设备侧MCP的实际后端枚举工具,核self.lab_led.set/get_state schema;发送power布尔与整数request_id,不提供公开默认口令,也不冒称工具返回等于灯已亮。
- 用同id同power重试核不重复写;同id不同power应TOOL ERR/JSON-RPC error,不改变GPIO。改用新id和正确布尔重试;digital_pad与实际肉眼光分别记。
- 主动断开AP,观察WIFI ERR、原日志和回应是否仍在飞;恢复网络后等实际状态,不宣称回调投递后瞬间停止音频。
- 使用自己可控后端的可恢复拒绝/不可达场景观察BACK ERR;先断开SD,再查日志/服务/heap/decoder并确认软错误或RESET。不能用损坏密钥、反复猜密码代替实验。
- GPIO ERR的实体注入只用安全低压接线检查:断电改线、核无短路后再上电,不把GPIO直接短到GND/3V3造故障。数字LED开路可能仍pad一致而不报警,此局限必须记。写/读API失败仅在明确host桩中注入,非实板结果。
- 不拔插带电电源/信号做试错。维修后RESET清硬件锁,原id缓存随重启丢失;先query新boot_id与当前pad,不能把上次缓存当持久exactly-once。
参考:[13] 固定Application:事件、手动采音、协议回调与主循环[23] 固定McpServer:解析、调度、回包/异常[24] 固定McpTool/PropertyList与callback契约[10] 固定本板config:I2S/按钮/OLED/未接GPIO48
6. 连续30轮的可执行记录方法
固定同一固件/锁、AP、服务模型、房间、麦克风/喇叭位置与低音量;保持半双工,逐轮等排空再提问。dialog-rounds.csv逐行填round、开始/结束/观察到的六状态、字幕/声音是否真实可辨、工具请求id/JSON-RPC结果/pad/肉眼光、网络/服务故障、恢复动作、原日志路径/SHA。至少30次尝试都保留,失败/取消/重启不能删除;重启后另记boot_id,旧去重缓存不延续。
建议1–10普通短问答,11–15带灯开/关/查询,16–20重复/冲突/非法参数,21–25可恢复AP/后端异常,26–30修复后新id与重启边界;各类次数固定不是已发生。未经实际执行,本篇不填成功率、延迟、声音/光学结果或“运行稳定”。30份synthetic面板轨迹只验证状态机能重复收敛。
参考:[13] 固定Application:事件、手动采音、协议回调与主循环[12] 固定AudioService:任务、队列、解码/重采样与输出[23] 固定McpServer:解析、调度、回包/异常[18] ESP-IDF5.5.5 esp_timer:64位开机单调微秒
7. 本机同源检查和证据边界
生产面板/runtime9组与完整DeskDisplay3组ASan/UBSan;后者的baseDisplay、LVGL锁/图形/字体/I2C/时钟是明确桩。完整MCP从当前生产源提取10个方法,使用实际Registry cJSON1.7.19~2和当前绑定,12常规组、最终snapshot-fault单独进程、新面板投递/冲突/锁存组均实际执行;FIFO Application/复制JSON/GPIO HAL是明确适配器。
UI并发夹具用真实桌面std::thread和完整生产class,串行化门禁与显示调用;没有证明FreeRTOS时序、LVGL锁延迟、显示像素、云端RF/账号、麦克风或声学质量。source/host/build/diagram四类证据分别保留,不把其中任何一项替代实体30轮。
python tests/run_panel_host.py --output /tmp/a10-panel-contract
python tests/run_mcp_host.py --output /tmp/a10-mcp-contract
python tests/check_ui_serialization.py --output /tmp/a10-ui-contract参考:[23] 固定McpServer:解析、调度、回包/异常[24] 固定McpTool/PropertyList与callback契约[22] 固定LvglDisplay:状态/通知/锁与字形[20] 固定DeviceStateMachine:合法状态转换与监听
8. 故障恢复和版本保全
原小智及A08复用部分按MIT保留,组件/字体/资源按各原LICENSE/NOTICE;本站面板/工具/概念图MIT,PySerial若用于串口按BSD-3-Clause。全部源/依赖/许可随下载包,实体30轮仍pending。
| 现象 | 核查与恢复 |
|---|---|
| OLED无字/复位循环 | 先SD断开;断电核PID4440自动复位revB、SDA41/SCL42/0x3C、VIN3V3与Flash16MB/OPI8MB;查UART。I2C初始化可能触发原ESP_ERROR_CHECK复位,UI并非电气自检 |
| Wi-Fi已回但仍错误 | status是主循环观察;看后端/工具各独立错误,长按39只确认软错误并静音软件音量;GPIO ERR必须维修+RESET |
| 有工具result却无光 | 核boot_id、pad与请求id,再断电检查WP7113ID方向/1k/连接;数字回读不是光学,勿提高电压试错 |
| READY后晚回应又PLAY | 超时/确认没撤回服务器在飞音频;关物理SD,等队列/连接重建,再开新轮,不伪称取消保证 |
| 源/镜像不一致或OTA替换 | 保存原log/配置/产物,重新解压本完整包,verify_project,新空build后按当前地址表恢复所有镜像;不要混不同版本assets或删锁掩盖差异 |
参考:[10] 固定本板config:I2S/按钮/OLED/未接GPIO48[11] 固定Simplex codec:板参数/32bit槽/右移限幅/音量[21] 固定OledDisplay:128×32布局/字幕滚动[22] 固定LvglDisplay:状态/通知/锁与字形[28] 固定v2.4.0应用manifest:依赖与IDF最低约束
本次实际做过的检查与空白物理记录
作者验证按来源、构建、主机和实物分别记录。正常报告、成功返回和编译通过都不是对话/声音/光学成功;下面的样本标签保留其真实来源。
| 检查 | 实际结果 | 边界 |
|---|---|---|
| 真实固件 | IDF5.5.5实际增量build Exit0/输入稳定 | 2638144B bin;28产品/原日志与全部输入按阶段COW留存 |
| 同源主机 | 生产Panel/runtime9组、完整显示adapter3组、完整MCP12+1+面板投递组;两个旧UI反例与串行化恢复 | 全部使用明确桩/合成时间,非音频/无线/屏幕实测 |
| 实际文件/数据 | 锁定14px/1bpp字体15状态标签按glyph数据均适配94×16;不是LVGL实屏渲染 | 字体/ELF/文件计算不能替代实体结果 |
| 实体会话/声音/显示/光 | 未执行,物理表只有表头 | 连续30轮实体对话/工具/恢复待测 |
参考:[13] 固定Application:事件、手动采音、协议回调与主循环[20] 固定DeviceStateMachine:合法状态转换与监听[22] 固定LvglDisplay:状态/通知/锁与字形[25] 实际Registry 78/xiaozhi-fonts2.0.0发布包:字体/许可元数据
完整状态优先级与恢复模型
以下与下载包当前文件逐字对应。Application/音频/协议的完整原文件、所有新增头、完整差异、配置/依赖锁、README和许可均随包;CURRENT-SOURCE.json核完整来源树,不靠截断片段重建工程。
// ---- firmware/main/lab_desk_policy.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <cstdint>
namespace a10 {
enum class Phase { BOOT, CONFIG, ACTIVATE, CONNECT, READY, LISTEN, PLAY, UPDATE, TEST, FATAL };
struct Frame {
const char* status;
const char* hint;
bool suppress_chat;
};
class Panel {
public:
void network(bool up) {
network_known_ = true;
network_up_ = up;
if (!up)
awaiting_ = false;
}
void backend_error() {
backend_error_ = true;
awaiting_ = false;
}
void backend_connected() { backend_error_ = false; }
void tool_error() { tool_error_ = true; }
void tool_success() { tool_error_ = false; }
void gpio_fault() { gpio_fault_ = true; }
// A UI acknowledgement, not a network cancel or hardware repair.
void acknowledge() {
tool_error_ = false;
if (network_up_)
backend_error_ = false;
awaiting_ = false;
}
void stop_listening(uint64_t now) {
awaiting_ = true;
wait_at_ = now;
}
void tick(uint64_t now) {
if (awaiting_ && (now < wait_at_ || now - wait_at_ >= 60000000ULL))
backend_error();
}
Frame frame(Phase phase, bool playback_busy, uint64_t now) {
if (phase == Phase::LISTEN || phase == Phase::PLAY || playback_busy)
awaiting_ = false;
tick(now);
if (gpio_fault_)
return {"GPIO ERR", "Repair + RESET", true};
if (phase == Phase::FATAL)
return {"SYS ERR", "Check UART; RESET", true};
if (phase == Phase::CONFIG)
return {"CONFIG", "Use setup WiFi", true};
if (network_known_ && !network_up_)
return {"WIFI ERR", "Check AP / USB", true};
if (backend_error_)
return {"BACK ERR", "Mute; retry", true};
if (tool_error_)
return {"TOOL ERR", "Check args/id", true};
if (phase == Phase::ACTIVATE)
return {"ACTIVATE", "Check UART", true};
if (phase == Phase::CONNECT)
return {"CONNECT", "Opening link", true};
if (phase == Phase::UPDATE)
return {"UPDATE", "Keep power", true};
if (phase == Phase::TEST)
return {"TEST", "Local loopback", true};
if (phase == Phase::PLAY || playback_busy)
return {"PLAY", "", false};
if (phase == Phase::LISTEN)
return {"LISTEN", "", false};
if (awaiting_)
return {"THINK", "Waiting server", false};
if (phase == Phase::READY)
return {"READY", "Hold GPIO47", false};
return {"BOOT", "Check UART", true};
}
private:
uint64_t wait_at_ = 0;
bool awaiting_ = false, network_known_ = false, network_up_ = false;
bool backend_error_ = false, tool_error_ = false, gpio_fault_ = false;
};
} // namespace a10参考:[13] 固定Application:事件、手动采音、协议回调与主循环[20] 固定DeviceStateMachine:合法状态转换与监听[22] 固定LvglDisplay:状态/通知/锁与字形[23] 固定McpServer:解析、调度、回包/异常[24] 固定McpTool/PropertyList与callback契约
完整主循环运行适配
以下与下载包当前文件逐字对应。Application/音频/协议的完整原文件、所有新增头、完整差异、配置/依赖锁、README和许可均随包;CURRENT-SOURCE.json核完整来源树,不靠截断片段重建工程。
// ---- firmware/main/lab_desk_runtime.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <esp_log.h>
#include <esp_timer.h>
#include <cstring>
#include "device_state.h"
#include "lab_desk_policy.h"
#if !CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI || !CONFIG_OLED_SSD1306_128X32 || \
CONFIG_USE_AUDIO_PROCESSOR || !CONFIG_WAKE_WORD_DISABLED
#error "A10 scope: bread-compact-wifi128x32 raw/manual configuration only"
#endif
namespace a10 {
// All model mutation/rendering is owned by Application main loop. Register these
// callbacks during Board construction, before that loop starts.
inline Panel& panel() {
static Panel p;
return p;
}
inline uint64_t now() { return static_cast<uint64_t>(esp_timer_get_time()); }
using Present = void (*)(const Frame&);
using FaultReader = bool (*)();
inline Present& presenter() {
static Present p = nullptr;
return p;
}
inline FaultReader& fault_reader() {
static FaultReader p = nullptr;
return p;
}
inline Phase phase(DeviceState s) {
switch (s) {
case kDeviceStateIdle:
return Phase::READY;
case kDeviceStateWifiConfiguring:
return Phase::CONFIG;
case kDeviceStateActivating:
return Phase::ACTIVATE;
case kDeviceStateConnecting:
return Phase::CONNECT;
case kDeviceStateListening:
return Phase::LISTEN;
case kDeviceStateSpeaking:
return Phase::PLAY;
case kDeviceStateFatalError:
return Phase::FATAL;
case kDeviceStateAudioTesting:
return Phase::TEST;
case kDeviceStateUpgrading:
return Phase::UPDATE;
default:
return Phase::BOOT;
}
}
inline void render(DeviceState s, bool playback_busy) {
if (fault_reader() && fault_reader()())
panel().gpio_fault();
const auto f = panel().frame(phase(s), playback_busy, now());
static const char* last = nullptr;
if (!last || std::strcmp(last, f.status)) {
ESP_LOGI("a10", "A10v1 status=%s oled_adapter=%d; queue/protocol/digital evidence only",
f.status, presenter() != nullptr);
last = f.status;
}
if (presenter())
presenter()(f);
}
} // namespace a10参考:[13] 固定Application:事件、手动采音、协议回调与主循环[20] 固定DeviceStateMachine:合法状态转换与监听[22] 固定LvglDisplay:状态/通知/锁与字形[23] 固定McpServer:解析、调度、回包/异常[24] 固定McpTool/PropertyList与callback契约
完整显示事务适配
以下与下载包当前文件逐字对应。Application/音频/协议的完整原文件、所有新增头、完整差异、配置/依赖锁、README和许可均随包;CURRENT-SOURCE.json核完整来源树,不靠截断片段重建工程。
// ---- firmware/main/lab_desk_display.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <cstring>
#include <mutex>
#include "display/oled_display.h"
#include "lab_desk_runtime.h"
namespace a10 {
class DeskDisplay : public OledDisplay {
public:
using OledDisplay::OledDisplay;
void SetupUI() override {
std::lock_guard<std::mutex> owner(ui_mutex_);
OledDisplay::SetupUI();
DisplayLockGuard lock(this);
// Reserve the entire96px right column for a short ASCII state label.
for (auto* p : {network_label_, mute_label_, battery_label_})
if (p)
lv_obj_add_flag(p, LV_OBJ_FLAG_HIDDEN);
if (status_label_) {
lv_obj_set_width(status_label_, 94);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_CLIP);
}
}
void SetStatus(const char* text) override {
std::lock_guard<std::mutex> owner(ui_mutex_);
if (!active_)
OledDisplay::SetStatus(text);
}
void ShowNotification(const char* text, int ms = 3000) override {
{
std::lock_guard<std::mutex> owner(ui_mutex_);
if (!active_) {
OledDisplay::ShowNotification(text, ms);
return;
}
}
ESP_LOGI("a10", "upstream notification retained in UART: %s", text);
}
void ShowNotification(const std::string& text, int ms = 3000) override {
ShowNotification(text.c_str(), ms);
}
void SetChatMessage(const char* role, const char* text) override {
std::lock_guard<std::mutex> owner(ui_mutex_);
if (!suppress_)
OledDisplay::SetChatMessage(role, text);
}
void present(const Frame& f) {
std::lock_guard<std::mutex> owner(ui_mutex_);
if (!IsSetupUICalled())
return;
if (!active_) {
active_ = true;
if (notification_timer_)
esp_timer_stop(notification_timer_);
}
suppress_ = f.suppress_chat;
if (last_status_ != f.status) {
OledDisplay::SetStatus(f.status);
last_status_ = f.status;
}
if (f.suppress_chat || std::strcmp(f.status, "READY") == 0) {
if (last_hint_ != f.hint) {
OledDisplay::SetChatMessage("system", f.hint);
last_hint_ = f.hint;
}
} else
last_hint_.clear();
}
private:
// Serialize gate checks with their qualified base UI call, including the
// first activation and late chat concurrent with an error. Atomic flags
// alone leave a check/use window and can hide a persistent error.
std::mutex ui_mutex_;
bool active_ = false, suppress_ = true;
std::string last_status_, last_hint_;
};
inline DeskDisplay*& display() {
static DeskDisplay* p = nullptr;
return p;
}
inline void present_to_oled(const Frame& f) {
if (display())
display()->present(f);
}
} // namespace a10参考:[13] 固定Application:事件、手动采音、协议回调与主循环[20] 固定DeviceStateMachine:合法状态转换与监听[22] 固定LvglDisplay:状态/通知/锁与字形[23] 固定McpServer:解析、调度、回包/异常[24] 固定McpTool/PropertyList与callback契约
完整板级硬件/按键/所有权注册
以下与下载包当前文件逐字对应。Application/音频/协议的完整原文件、所有新增头、完整差异、配置/依赖锁、README和许可均随包;CURRENT-SOURCE.json核完整来源树,不靠截断片段重建工程。
// ---- firmware/main/boards/bread-compact-wifi/compact_wifi_board.cc ----
#include <esp_random.h>
#include "application.h"
#include "assets/lang_config.h"
#include "audited_led_gpio.h"
#include "audited_led_mcp.h"
#include "button.h"
#include "codecs/no_audio_codec.h"
#include "config.h"
#include "display/oled_display.h"
#include "lab_desk_display.h"
#include "led/single_led.h"
#include "mcp_server.h"
#include "system_reset.h"
#include "wifi_board.h"
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_vendor.h>
#ifdef SH1106
#include <esp_lcd_panel_sh1106.h>
#endif
#define TAG "CompactWifiBoard"
class CompactWifiBoard : public WifiBoard {
private:
i2c_master_bus_handle_t display_i2c_bus_;
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
esp_lcd_panel_handle_t panel_ = nullptr;
Display* display_ = nullptr;
Button boot_button_;
Button touch_button_;
Button volume_up_button_;
Button volume_down_button_;
void InitializeDisplayI2c() {
i2c_master_bus_config_t bus_config = {
.i2c_port = (i2c_port_t)0,
.sda_io_num = DISPLAY_SDA_PIN,
.scl_io_num = DISPLAY_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &display_i2c_bus_));
}
void InitializeSsd1306Display() {
// SSD1306 config
esp_lcd_panel_io_i2c_config_t io_config = {
.dev_addr = 0x3C,
.on_color_trans_done = nullptr,
.user_ctx = nullptr,
.control_phase_bytes = 1,
.dc_bit_offset = 6,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.flags =
{
.dc_low_on_data = 0,
.disable_control_phase = 0,
},
.scl_speed_hz = 400 * 1000,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(display_i2c_bus_, &io_config, &panel_io_));
ESP_LOGI(TAG, "Install SSD1306 driver");
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.bits_per_pixel = 1;
esp_lcd_panel_ssd1306_config_t ssd1306_config = {
.height = static_cast<uint8_t>(DISPLAY_HEIGHT),
};
panel_config.vendor_config = &ssd1306_config;
#ifdef SH1106
ESP_ERROR_CHECK(esp_lcd_new_panel_sh1106(panel_io_, &panel_config, &panel_));
#else
ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_));
#endif
ESP_LOGI(TAG, "SSD1306 driver installed");
// Reset the display
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_));
if (esp_lcd_panel_init(panel_) != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize display");
display_ = new NoDisplay();
return;
}
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_, false));
// Set the display to on
ESP_LOGI(TAG, "Turning display on");
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true));
a10::display() = new a10::DeskDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT,
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display_ = a10::display();
a10::presenter() = &a10::present_to_oled;
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});
touch_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
touch_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
volume_up_button_.OnClick([this]() {
auto codec = GetAudioCodec();
auto volume = codec->output_volume() + 10;
if (volume > 100) {
volume = 100;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
});
volume_up_button_.OnLongPress([this]() {
GetAudioCodec()->SetOutputVolume(100);
GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME);
});
volume_down_button_.OnClick([this]() {
auto codec = GetAudioCodec();
auto volume = codec->output_volume() - 10;
if (volume < 0) {
volume = 0;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
});
volume_down_button_.OnLongPress([this]() {
GetAudioCodec()->SetOutputVolume(0);
GetDisplay()->ShowNotification(Lang::Strings::MUTED);
Application::GetInstance().Schedule([]() { a10::panel().acknowledge(); });
});
}
// 物联网初始化,逐步迁移到 MCP 协议
void InitializeTools() {
static a08::GpioLed led_io(LAMP_GPIO);
static a08::LedPolicy<a08::GpioLed> led(led_io, esp_random());
a10::fault_reader() = +[]() { return led.snapshot().fault; };
static const bool registered = []() {
a08::register_tools(McpServer::GetInstance(), led);
return true;
}();
(void)registered;
}
public:
CompactWifiBoard() :
boot_button_(BOOT_BUTTON_GPIO),
touch_button_(TOUCH_BUTTON_GPIO),
volume_up_button_(VOLUME_UP_BUTTON_GPIO),
volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) {
InitializeDisplayI2c();
InitializeSsd1306Display();
InitializeButtons();
InitializeTools();
}
virtual Led* GetLed() override {
static SingleLed led(BUILTIN_LED_GPIO);
return &led;
}
virtual AudioCodec* GetAudioCodec() override {
#ifdef AUDIO_I2S_METHOD_SIMPLEX
static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN);
#else
static NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN);
#endif
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
};
DECLARE_BOARD(CompactWifiBoard);参考:[13] 固定Application:事件、手动采音、协议回调与主循环[20] 固定DeviceStateMachine:合法状态转换与监听[22] 固定LvglDisplay:状态/通知/锁与字形[23] 固定McpServer:解析、调度、回包/异常[24] 固定McpTool/PropertyList与callback契约
完整MCP实验灯与面板绑定
以下与下载包当前文件逐字对应。Application/音频/协议的完整原文件、所有新增头、完整差异、配置/依赖锁、README和许可均随包;CURRENT-SOURCE.json核完整来源树,不靠截断片段重建工程。
// ---- firmware/main/boards/bread-compact-wifi/audited_led_mcp.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <esp_log.h>
#include "application.h"
#include "audited_led_policy.h"
#include "audited_led_wire.h"
#include "lab_desk_runtime.h"
#include "mcp_server.h"
namespace a08 {
inline cJSON* snapshot_json(const Snapshot& s) {
cJSON* j = cJSON_CreateObject();
if (!j)
throw std::runtime_error("A08 allocation failed");
cJSON_AddNumberToObject(j, "boot_id", s.boot_id);
cJSON_AddBoolToObject(j, "fault", s.fault);
cJSON_AddBoolToObject(j, "last_confirmed_power", s.last_confirmed_power);
if (s.pad_known)
cJSON_AddBoolToObject(j, "digital_pad_level", s.pad_level);
else
cJSON_AddNullToObject(j, "digital_pad_level");
if (s.has_last_request)
cJSON_AddNumberToObject(j, "last_applied_request_id", s.last_request);
else
cJSON_AddNullToObject(j, "last_applied_request_id");
cJSON_AddStringToObject(j, "feedback", "digital_gpio_only_not_optical");
cJSON_AddNumberToObject(j, "dedup_entries", 16);
return j;
}
template <class Io>
void register_tools(McpServer& server, LedPolicy<Io>& policy) {
auto* set_tool = new McpTool(
"self.lab_led.set",
"Set only the external laboratory LED. power is boolean. request_id is an integer "
"0..2147483647. Use a new id for a new command; retry with the same id and power. Cache "
"holds last16 successful ids for this boot only. Never use this example for a safety "
"actuator.",
PropertyList({Property("power", kPropertyTypeBoolean),
Property("request_id", kPropertyTypeInteger, 0, 2147483647)}),
[&policy](const PropertyList& args) -> ReturnValue {
const bool power = args["power"].template value<bool>();
const auto id = static_cast<uint32_t>(args["request_id"].template value<int>());
const Result result = policy.set(id, power);
ESP_LOGI("a08", "SET request=%lu requested=%d outcome=%s",
static_cast<unsigned long>(id), power,
result == Result::APPLIED ? "APPLIED"
: result == Result::DUPLICATE ? "DUPLICATE"
: result == Result::CONFLICT ? "CONFLICT"
: "FAULT");
if (result == Result::CONFLICT) {
a10::panel().tool_error();
throw std::runtime_error("A08 request id conflict");
}
if (result == Result::FAULT) {
a10::panel().gpio_fault();
throw std::runtime_error("A08 GPIO fault; repair and RESET");
}
const Snapshot state = policy.snapshot();
ESP_LOGI("a08", "PAD boot=%lu confirmed=%d known=%d value=%d fault=%d",
static_cast<unsigned long>(state.boot_id), state.last_confirmed_power,
state.pad_known, state.pad_level, state.fault);
if (state.fault) {
a10::panel().gpio_fault();
throw std::runtime_error("A08 GPIO fault; repair and RESET");
}
a10::panel().tool_success();
cJSON* j = snapshot_json(state);
cJSON_AddNumberToObject(j, "request_id", id);
cJSON_AddBoolToObject(j, "requested_power", power);
cJSON_AddBoolToObject(j, "duplicate", result == Result::DUPLICATE);
return j;
});
set_tool->set_validator([](const cJSON* args) {
auto error = validate_arguments("self.lab_led.set", args);
if (error)
Application::GetInstance().Schedule([]() { a10::panel().tool_error(); });
return error;
});
server.AddTool(set_tool);
auto* query_tool = new McpTool(
"self.lab_led.get_state",
"Query the laboratory LED digital pad and latched fault. No arguments. Digital feedback "
"does not prove visible light. A fault needs repair and RESET; a query does not clear it.",
PropertyList(), [&policy](const PropertyList&) -> ReturnValue {
const Snapshot state = policy.snapshot();
ESP_LOGI("a08", "QUERY boot=%lu confirmed=%d known=%d value=%d fault=%d",
static_cast<unsigned long>(state.boot_id), state.last_confirmed_power,
state.pad_known, state.pad_level, state.fault);
return snapshot_json(state);
});
query_tool->set_validator([](const cJSON* args) {
auto error = validate_arguments("self.lab_led.get_state", args);
if (error)
Application::GetInstance().Schedule([]() { a10::panel().tool_error(); });
return error;
});
server.AddTool(query_tool);
}
} // namespace a08参考:[13] 固定Application:事件、手动采音、协议回调与主循环[20] 固定DeviceStateMachine:合法状态转换与监听[22] 固定LvglDisplay:状态/通知/锁与字形[23] 固定McpServer:解析、调度、回包/异常[24] 固定McpTool/PropertyList与callback契约
资料来源与版本说明
固定上游、SDK/Registry实际文件及具体厂家范围已核;主机合成记录、文件/字体算术与构建不替代指定硬件、后端、声学、显示或光学实验。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。
- S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB
- ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动
- PID6049固定PCB:3V、SEL与47k数据下拉
- MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL
- PID3006固定单声道PCB:VIN/SD1M/BTL端子
- PID1313扬声器2017-02-14厂家规格:8Ω额定1W
- PID6049产品页:具体型号与停产边界
- ESP32-S3-WROOM-1/1U v1.8:N16R8规格表
- PID4440固定revB PCB:上拉/电平转换/自动复位/128×32
- 固定本板config:I2S/按钮/OLED/未接GPIO48
- 固定Simplex codec:板参数/32bit槽/右移限幅/音量
- 固定AudioService:任务、队列、解码/重采样与输出
- 固定Application:事件、手动采音、协议回调与主循环
- 固定Protocol内存packet与wire边界
- 固定WebSocket SendAudio三协议版本
- 固定MQTT/UDP SendAudio加密nonce与输出
- 固定AfeAudioEngine:raw Feed/OutputRawAudio同步路径
- ESP-IDF5.5.5 esp_timer:64位开机单调微秒
- 固定CMake:本板工厂、14px字体与lang_header
- 固定DeviceStateMachine:合法状态转换与监听
- 固定OledDisplay:128×32布局/字幕滚动
- 固定LvglDisplay:状态/通知/锁与字形
- 固定McpServer:解析、调度、回包/异常
- 固定McpTool/PropertyList与callback契约
- 实际Registry 78/xiaozhi-fonts2.0.0发布包:字体/许可元数据
- 实际esp_audio_effects1.3.0~1来源:rate converter格式/采样率/API约束
- IDF5.5.5 LCD I2C真实字段顺序
- 固定v2.4.0应用manifest:依赖与IDF最低约束
- 固定Assets::Apply:common字体资源替换与字形能力
- 固定default assets:basic→common CBIN打包
