AI 语音与工具 / 语音项目发布 内容更新

做一个有屏幕的语音桌面助手:状态、按键、对话和故障反馈

完整N16R8/OLED语音设备,主循环管理联网、采音、等待、播放和工具故障,软确认与GPIO维修分离;30轮实物对话待测。

读完能做什么
得到有明确状态、错误分层和恢复动作的完整可构建设备,并给出30轮观察方法和空表。
开始之前
完成真实前置;能断电追线、核桥接UART与完整镜像/分区;服务端账号和观察权限由读者自行取得。
所需器材
官方ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N16R8、桥接UART USB线、万用表与可靠短线、PID6049 ICS43434匹配停产存量,排针已可靠焊接;PID3006单声道MAX98357A、PID1313 8Ω/1W、功放独立稳压3.3V≥1A、SD低压SPST/1k/10k电阻、共地;BTL不接地、PID4440 128×32自动复位revB OLED,GPIO47/39/40按钮及外10k上拉;WP7113ID红LED/1k电阻、自己的匹配语音服务/账号和获授权日志;无实体/云端结果预填
版本组合
SDK / Python:ESP-IDF5.5.5 / Python3.12.13应用原树:xiaozhi v2.4.0 / 5540258abcbfa62518d09959308200be1c5b1b2b;完整1590来源+两兼容补丁+本站项目锁与音频:实际标准Registry74项锁;esp_audio_effects1.3.0~1;输入/输出16k,独立I2S,无AEC串口 / 字体:可选pyserial3.5;78/xiaozhi-fonts2.0.0 / basic_14_1 + default common_14_1,14px1bpp,组件元数据Apache-2.0
适用范围

官方DevKitC-1 v1.1 / WROOM-1-N16R8;小智v2.4.0/5540258…+两处板局部兼容补丁/本站项目;IDF5.5.5;PID6049/PID3006/PID1313/PID4440,实体与云端待测。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/audio-projects-sources.json及audio-projects.md;实际文件/命令/原日志/输入和产物SHA分阶段留存。验证日期:
构建 / 配置已通过
docs/esp32-evidence/audio-projects-builds.json及audio-projects.md;实际文件/命令/原日志/输入和产物SHA分阶段留存。对象:ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N16R8版本:ESP-IDF5.5.5 / xiaozhi v2.4.0+显式16k/I2C兼容补丁+本站项目验证日期:
仿真已通过
docs/esp32-evidence/audio-projects-host.json及audio-projects.md;实际文件/命令/原日志/输入和产物SHA分阶段留存。验证日期:
本站实板待验证
30轮实体会话、指定板/时钟/显示/麦克风/功放/声学/光学/云端未执行;所有物理CSV仅表头。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 语音项目

这篇要完成什么,尚未验证什么

这是一份完整有屏幕的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:解析、调度、回包/异常

02 / 语音项目

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 单声道MAX98357ADIN7/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自动复位revBVIN→主板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约束

03 / 语音项目

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/匹配实际服务流程
LISTENApplication为Listening且无更高错误;GPIO47释放结束。不是麦克风质量检测
THINK手动StopListening后等待本地回应观察;不是后端LLM内部状态
PLAYApplication Speaking或播放队列未空;不是声学播放确认
WIFI ERR观察到扫描/断线回调,且不在主动CONFIG;检查AP/供电/配置,重连后按新状态继续
BACK ERR协议OnNetworkError或观察到等待≥60秒;先物理静音,诊断后端/decoder/内存与日志后重试
TOOL ERR两个实验灯工具的原始参数拒绝、id冲突等可重试错误;修正参数/新id,成功工具或本地确认清软错误
GPIO ERRLED策略检测到本地初始化/写/数字回读故障并锁存;断电修接线,再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打包

04 / 语音项目

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位开机单调微秒

05 / 语音项目

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规格表

06 / 语音项目

5. 对话、工具和恢复的逐项操作

按以下步骤核对并保留实际记录。

  1. CONFIG时只完成配网;ACTIVATE按UART实际提示,用自己的账号,不将验证码/token写进公开表。得到Idle/READY并等队列排空后开始。
  2. 按住GPIO47提出短问题,确认LISTEN;释放后观察THINK、字幕JSON、PLAY与最终READY。每项分别记,不只写“成功”。BOOT原toggle、39/40音量与长按39软确认按表使用。
  3. 让支持设备侧MCP的实际后端枚举工具,核self.lab_led.set/get_state schema;发送power布尔与整数request_id,不提供公开默认口令,也不冒称工具返回等于灯已亮。
  4. 用同id同power重试核不重复写;同id不同power应TOOL ERR/JSON-RPC error,不改变GPIO。改用新id和正确布尔重试;digital_pad与实际肉眼光分别记。
  5. 主动断开AP,观察WIFI ERR、原日志和回应是否仍在飞;恢复网络后等实际状态,不宣称回调投递后瞬间停止音频。
  6. 使用自己可控后端的可恢复拒绝/不可达场景观察BACK ERR;先断开SD,再查日志/服务/heap/decoder并确认软错误或RESET。不能用损坏密钥、反复猜密码代替实验。
  7. GPIO ERR的实体注入只用安全低压接线检查:断电改线、核无短路后再上电,不把GPIO直接短到GND/3V3造故障。数字LED开路可能仍pad一致而不报警,此局限必须记。写/读API失败仅在明确host桩中注入,非实板结果。
  8. 不拔插带电电源/信号做试错。维修后RESET清硬件锁,原id缓存随重启丢失;先query新boot_id与当前pad,不能把上次缓存当持久exactly-once。

参考:[13] 固定Application:事件、手动采音、协议回调与主循环[23] 固定McpServer:解析、调度、回包/异常[24] 固定McpTool/PropertyList与callback契约[10] 固定本板config:I2S/按钮/OLED/未接GPIO48

07 / 语音项目

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位开机单调微秒

08 / 语音项目

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:合法状态转换与监听

09 / 语音项目

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最低约束

10 / 语音项目

本次实际做过的检查与空白物理记录

作者验证按来源、构建、主机和实物分别记录。正常报告、成功返回和编译通过都不是对话/声音/光学成功;下面的样本标签保留其真实来源。

检查实际结果边界
真实固件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发布包:字体/许可元数据

11 / 语音项目

完整状态优先级与恢复模型

以下与下载包当前文件逐字对应。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契约

12 / 语音项目

完整主循环运行适配

以下与下载包当前文件逐字对应。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契约

13 / 语音项目

完整显示事务适配

以下与下载包当前文件逐字对应。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契约

14 / 语音项目

完整板级硬件/按键/所有权注册

以下与下载包当前文件逐字对应。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契约

15 / 语音项目

完整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契约

BACK TO THE SOURCE

资料来源与版本说明

固定上游、SDK/Registry实际文件及具体厂家范围已核;主机合成记录、文件/字体算术与构建不替代指定硬件、后端、声学、显示或光学实验。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  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槽/右移限幅/音量
  12. 固定AudioService:任务、队列、解码/重采样与输出
  13. 固定Application:事件、手动采音、协议回调与主循环
  14. 固定Protocol内存packet与wire边界
  15. 固定WebSocket SendAudio三协议版本
  16. 固定MQTT/UDP SendAudio加密nonce与输出
  17. 固定AfeAudioEngine:raw Feed/OutputRawAudio同步路径
  18. ESP-IDF5.5.5 esp_timer:64位开机单调微秒
  19. 固定CMake:本板工厂、14px字体与lang_header
  20. 固定DeviceStateMachine:合法状态转换与监听
  21. 固定OledDisplay:128×32布局/字幕滚动
  22. 固定LvglDisplay:状态/通知/锁与字形
  23. 固定McpServer:解析、调度、回包/异常
  24. 固定McpTool/PropertyList与callback契约
  25. 实际Registry 78/xiaozhi-fonts2.0.0发布包:字体/许可元数据
  26. 实际esp_audio_effects1.3.0~1来源:rate converter格式/采样率/API约束
  27. IDF5.5.5 LCD I2C真实字段顺序
  28. 固定v2.4.0应用manifest:依赖与IDF最低约束
  29. 固定Assets::Apply:common字体资源替换与字形能力
  30. 固定default assets:basic→common CBIN打包