AI 语音与工具 / 语音实践发布 内容更新

第一次编译小智:固定源码、显式兼容补丁,只改一条日志

完整固定v2.4.0源码与IDF5.5.5显式兼容补丁,原样失败如实留存,三次真实构建验证基线、单日志修改与恢复。

读完能做什么
能检查源码状态,得到三阶段真实构建和可保存/恢复的完整镜像链。
开始之前
完成所列前置,能断电追线和用桥接USB构建/下载;只有已核具体组合在本文范围。
所需器材
指定官方S3 DevKitC1 v1.1 / N16R8、UART桥USB线、短线与万用表、PID6049 ICS43434(匹配停产存量,排针已可靠焊接);PID3006单声道MAX98357A、PID1313 8Ω/1W、功放独立稳压3.3V≥1A、共地、SD电阻与低压SPST物理静音开关、PID4440 128×32自动复位OLED、按钮与10k上拉
版本组合
框架 / Python:ESP-IDF5.5.5 / Python3.12.13应用基线:小智v2.4.0 / 5540258abcbfa62518d09959308200be1c5b1b2b + 显式I2C字段顺序与MAX98357A输出16k两处补丁依赖锁:标准Registry解析74项,cJSON1.7.19~2;无本地override音频:ICS43434 Rev1.2 / MAX98357A Rev7;两组独立I2S,无AEC
适用范围

DevKitC1 v1.1 N16R8新组合;小智v2.4.0/5540258…+本站I2C顺序/功放16k两处板局部兼容补丁。PID6049/PID3006/PID1313;实板与声音待测。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/audio-assistant-source.json及audio-assistant.md;对应实际文件/命令与SHA留存。验证日期:
构建 / 配置已通过
docs/esp32-evidence/audio-assistant-build.json及audio-assistant.md;对应实际文件/命令与SHA留存。对象:ESP32-S3 N16R8版本:小智v2.4.0+明确兼容/本站补丁 / IDF5.5.5验证日期:
仿真已通过
docs/esp32-evidence/audio-assistant-host.json及audio-assistant.md;对应实际文件/命令与SHA留存。验证日期:
本站实板待验证
指定板、模块、HA/云端、录音、声音、电压/时钟/光学均未执行;hardware-observations.csv仅表头。

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

01 / 语音实践

先确定这篇要完成什么

下载包内 firmware/ 来自小智 v2.4.0 的完整1590个跟踪文件,并显式应用两处 本站板局部兼容补丁(IDF5.5.5 I2C顺序 + MAX98357A输出16k)。上游提交为 5540258abcbfa62518d09959308200be1c5b1b2b,原许可和目录保留,不是只放下载脚本。UPSTREAM.json记录1590原文件SHA,COMPATIBILITY.json、AUDIO-COMPATIBILITY.json和两份patch分别记录两处兼容修改,可逆向恢复并逐SHA核对原树。

本篇改变的是SSD1306初始化时的一条串口日志,不会改变屏幕文字、提示音或按键。这样先证明“我的源码→我的配置→我的镜像”这条链,再做功能。本站没有执行这块DIY组合的实板刷写、联网激活或声音测试,各项物理结果保持待测。

参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[2] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL

02 / 语音实践

1. 先把版本路线分开

本包固定 ESP-IDF5.5.5。v2.4.0的manifest声明IDF >=5.5.2,但最低约束不保证每个板型原样可编译。该版部分说明/AGENTS已优先推荐6.x;实际原样构建在本板型遇到I2C结构体字段顺序硬错。本文因此明确为 v2.4.0 + 本站I2C顺序/功放16k两处明确兼容补丁,不称原样上游构建通过。

第一份兼容补丁只把compact_wifi_board.cc中的命名初始化顺序调整为5.5.5真实头文件顺序:回调/上下文前移,scl_speed_hz后移。字段值、I2C API、引脚和板型不变,不改SDK。它与下一步一条日志个性化修改分开保存和验证。

站内浏览器刷机/硬件准备页另外核对了 v2.5.2 发布包;v2.5.x的manifest需要IDF >=6.0.1,不能把它原样放进本教程5.5.5环境。这里不修改上游SDK版本约束,也不下载第二SDK冒充同一构建。切换tag时必须重查manifest、板型配置、分区和依赖锁。

本配置选择 bread-compact-wifi、SSD1306 128×32、简体中文、禁用唤醒词/音频处理器/AEC,用实体按键测试;这不是上游所有默认功能。没有编入WakeNet并不等于端上已经能离线理解任意语音;普通对话还需要所选后端、网络和账号关联。

参考:[8] 小智v2.4.0固定manifest:IDF最低约束[13] IDF5.5.5 LCD I2C真实字段顺序[18] 固定CMake:语言头生成目标与组件选择[19] 小智v2.5.2 manifest:>=6.0.1差异

03 / 语音实践

2. 这是一套新的N16R8组合

仅覆盖 ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N16R8:16MB Quad Flash、8MB Octal PSRAM、Flash80MHz、Octal PSRAM80MHz。按模组屏蔽罩后缀与板修订核对,不凭网店“16M”标题判断。A01/A02使用的是N8R8,不能把其构建或历史检查当成本组合已实测。也不把模块规格表当成当前商家库存/板卡SKU证明。

GPIO47是本源码使用的数字按钮,不能只因变量叫touch就换成电容触摸片。128×64 PID326不是本包显示配置,旧PID931需另核RESET,不作直接替代。PID4440固定制造商revB电路有自动复位,原始仓库README仍提旧PID931,本文按具体电路/产品页分别核对。显示地址0x3C、I2C400kHz,源码镜像X/Y均为true。

v1.1板载RGB在GPIO38,而本版bread配置的 BUILTIN_LED_GPIO=48 用于外接可寻址RGB。本实验不接GPIO48,不把它当板载灯。 GPIO18原版LampController会初始化为低,本篇先不接该LED;下一篇才替换该工具。GPIO35/36/37涉及Octal PSRAM,不用;USB原生19/20不接模块,下载/日志使用桥接UART0的43/44。

先断USB与功放外供,再接线、查极性/短路/共地。MAX98357A的BTL两个输出均不接地,不接耳机地或接地示波器夹。GPIO不带扬声器,功放正电源不与主板正端并联。首次物理SD开关保持断开,任何复位前也先关闭。

模块固定选择与接线
麦克风已可靠焊接排针的Adafruit PID6049 / ICS-43434;3V→主板3V3,GND/SEL→GND,WS4、BCLK5、DOUT6
数字功放已可靠焊接排针的PID3006 / MAX98357A单声道;DIN7、BCLK15、LRC16;GAIN→其VIN选6dB
功放供电独立稳压3.3V、额定至少1A;VIN只接外供正端,外供负端与主板GND共地,不并接主板3V3/5V正端
扬声器PID1313 / C2464-001,8Ω/1W;两线接OUT+/OUT−,任何一端都不接GND
物理静音SD接10kΩ到GND;另用低压SPST开关把SD经1kΩ接到功放VIN。开关断开为关闭、闭合为左声道启用;先测电压
显示Adafruit PID4440、0.91英寸128×32 SSD1306 I2C、自动复位版;VIN→主板3V3,GND→GND,SDA41、SCL42,RST留空
控制板载BOOT GPIO0;另GPIO47常开按钮到GND、GPIO47外10kΩ上拉到主板3V3;可另用39/40各加同样按钮作音量减/增

参考:[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产品页:具体型号与停产边界[14] ESP32-S3-WROOM-1/1U v1.8:N16R8规格表[15] PID4440固定revB PCB:上拉/电平转换/自动复位/128×32[16] Adafruit PID4440:具体尺寸、自动复位版本[17] 固定board config:真实I2S/按钮/GPIO48/18

04 / 语音实践

3. 读懂实际音频,而不是沿用别篇的处理结论

本版 NoAudioCodecSimplex 用两套独立I2S:输入16kHz左槽、32bit;输出16kHz左槽、32bit。ICS-43434的有效24bit数据位于32bit槽中,16kHz属于其低功耗范围。实际输入源码把带符号32bit值右移12位再限到16bit,含额外数值放大/削顶可能,不是A01的WAV转换策略;本包不声称已经校准麦克风增益或直流偏移。

输出把16bit PCM按软件音量平方比例放到32bit槽,不能把音量数字直接当dB、声压或已测输出功率。原版初始音量70,NVS还会保存旧音量;AudioCodec启动会把非正音量恢复为10。因此软件静音/NVS不是复位期间物理静音保证。保持SD开关关闭,先在日志/屏幕核音量、用音量减按钮降低,再小心启用外供和SD;出现啸叫/削波/热/压降立即关SD与外供。本例没有AEC。

本组合为什么必须把原版输出24k改为16k

MAX98357A Rev7 p6 DIGITAL AUDIO INTERFACE保证的LRCLK区间为7.6–8.4、15.2–16.8、30.4–50.4、83.8–100.8kHz。24k不在保证范围;原上游AUDIO_OUTPUT_SAMPLE_RATE=24000不能用来证明本站所选PID3006合规。第二份显式板局部补丁只把config.h此宏改16000,输入16k和所有引脚不变。两套I2S仍独立,不因频率相同就短接时钟。32bit×2slot标称BCLK=1.024MHz,物理时钟未测。

固定AudioService::SetDecodeSampleRate按到来的Opus packet sample_rate打开decoder。若服务端送24k,则以锁定esp_audio_effects1.3.0~1配置src=24000/dst=16000、mono16bit、complexity2;decode后通过get_max_out_sample_num分配容量、process实际输出数量缩放PCM,再入播放队列。组件头声明输入/输出rate为4000或11025倍数,24k和16k均符合。TX codec本身仍配置16k,不直接将服务端24k设成MAX98357A LRCLK。实际组件已链接;主机桩只查完整方法参数/分支,不执行Xtensa DSP,不证明音质或时钟。可在本机执行 python3 tools/check_rate_path.py --output /tmp/a07-rate-contract(输出目录须尚不存在;A08可换目录名),它从下载包生产源提取完整方法和转换条件块,验证24→16配置、容量/实际输出数、重复配置复用、同频旁路和空handle局限;编译器静态断言同时核所选LRCLK落在厂家保证区间。

上游重采样open失败只打印日志,没有自动静音保证;get_max_out_sample_num/process返回值也没有在本版全面处理。看到“Failed to create output resampler”或“Failed to create Opus decoder”、异常语速/音调或内存错误时先关闭物理SD启用(开关断开VIN、由10k下拉静音),再停止功放外供,检查供电/PSRAM/堆、decoder采样率与完整版本,恢复自己的归档或RESET,不继续把错误当正常转换。此处保留并准确说明原实现,未改SDK/managed算法或暗称故障已被自动安全处理。

原24k作者包和全部原产品留在历史,编译通过未曾证明器件频率适配;当前结果只认16k修正版的三阶段/工具版新构建。

参考:[2] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL[10] 固定Simplex codec:使用板配置的采样率与数值转换[11] 固定AudioCodec:默认音量与NVS恢复[20] 固定AudioService:decoder采样率与输出重采样路径[21] 实际esp_audio_effects1.3.0~1来源:rate converter格式/采样率/API约束

05 / 语音实践

4. 激活已有IDF环境,核对原树

先完成站内E02/E07对应的环境/依赖管理。激活你已安装的5.5.5环境,确认 idf.py --version。下面工具使用当前IDF_PATH和当前Python,不替你装/替换SDK。

依赖锁固定当前解析结果;不要先删除锁再称“复现同版本”。首次构建会读取完整源和相应组件,SDK、缓存、managed组件与build都需要空间;先保证可写余量,不要在磁盘满时反复启动多个构建。

本次原样首次并行构建真实遇到 assets/lang_config.h 缺失。固定上游CMake把 lang_header设为ALL目标,却未显式接到main编译依赖,显示源码可能先编译。工具先运行上游 idf.py lang_header,再build,不手工改生成头。生成头后,原样源码又因I2C字段顺序失败,另有一次空间门限主动停止;日志和原输入均保留,不能写成原样成功。下载包已经带明确兼容补丁,当前结果以补丁版构建证据为准。

代码 / 配置
python tools/source_state.py verify-baseline
python tools/project.py configure
python tools/project.py build
python tools/project.py manifest

参考:[8] 小智v2.4.0固定manifest:IDF最低约束[13] IDF5.5.5 LCD I2C真实字段顺序[18] 固定CMake:语言头生成目标与组件选择

06 / 语音实践

5. 保存基线,再只改一条日志

第一次成功后,将 local-products.json 和对应日志另存为baseline,复制其中列出的完整镜像/配置/锁到自己的只读归档,并逐SHA核对。镜像文件需要真正保留,不能只留哈希后又覆盖唯一原件。

唯一变化是 Install SSD1306 driver 改成 A07 OLED driver: SSD1306。验证器重算全部1590来源文件,先核I2C块和16k宏,再核这一行;其他意外修改会停止。不要一边换引脚/模型/屏幕,一边把失败归因于这条日志。个性化构建的镜像与记录另存,不覆盖baseline;构建时间等元数据可能让镜像SHA变化,源码恢复不等于保证每次二进制逐字相同。

代码 / 配置
python tools/source_state.py diff
python tools/source_state.py change
python tools/source_state.py verify-changed
python tools/project.py build
python tools/project.py manifest

参考:[9] 固定bread-compact-wifi:显示、按钮、工厂与硬件[13] IDF5.5.5 LCD I2C真实字段顺序

07 / 语音实践

6. 只按当前构建的地址表刷写

保持SD开关断开、功放外供关闭,用主板UART桥USB连接电脑,确认实际串口,再执行:

此处由IDF读取本构建的 flasher_args.json,不是把某个应用bin一律写到0x0。16MB V2分区包含NVS/OTA元数据/PHY、两个约0x3f0000应用槽,以及0x800000处8MB assets区;按生成表核对,不能与N8R8或旧版分区混刷。若转用浏览器,只能使用另行正确合并并核对的完整镜像,不能把app.bin冒充merged-binary.bin。

未保存Wi-Fi的设备应按本版流程进入配网;启动期点BOOT可进入配网,平常点BOOT切换聊天,GPIO47按下/释放调用开始/停止聆听。普通配网激活步骤回看X04,不在这里虚构已登录控制台截图。观察表记录新串口日志、屏幕、版本/配置和后续恢复;本站未完成这些实板观察。

原版会请求默认OTA服务并可能安装更新,联网后的镜像不一定仍是自编版本。本包保留上游行为,不称已禁用OTA。先在配网/离线阶段捕获初始化日志,再联网;出现版本变化或新日志消失,核对是否被OTA替换,使用自己的归档重新刷入。设备信息/音频去向按所用后端说明确认,普通对话并非完全端上处理。

代码 / 配置
python tools/project.py flash --port /dev/cu.YOUR_BRIDGE_PORT
python tools/project.py monitor --port /dev/cu.YOUR_BRIDGE_PORT

参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[9] 固定bread-compact-wifi:显示、按钮、工厂与硬件[12] 固定OTA:默认服务与版本更新[14] ESP32-S3-WROOM-1/1U v1.8:N16R8规格表

08 / 语音实践

7. 恢复基线与常见失败

restore只把个性化日志恢复到兼容基线,保留两处显式兼容补丁;发现其他编辑不会悄悄抹掉。若需核原样上游,可依次执行remove-compat、verify-upstream;该状态在本板/5.5.5预期会重现字段顺序编译失败,再执行apply-compat同时恢复I2C顺序和功放16k两处兼容基线,不能把这两个阶段混为同一源码。也可使用先前实际保留的完整baseline镜像与对应地址表恢复,二者均须匹配同一配置。只有哈希、没有原文件时,不能声称“恢复原字节”。

失败阶段检查与恢复
组件/SDK约束失败查是否误用v2.5.x、另一个IDF、改过manifest或丢锁;回固定tag和锁,不降低上游要求
I2C designator order硬错查是否移除了本包I2C兼容补丁,核实际5.5.5头文件;按apply-compat恢复,不改SDK或关闭编译错误
缺lang_config.h先运行上游lang_header目标,再build,保留失败日志,不手写生成头
空间不足/被有界停止停自己的构建,保存原日志和已生成产品;释放已确认可重建的自有缓存后从同输入续建,不动共享SDK
烧录/串口失败核桥接口、数据线、占用;按住BOOT点RESET再松BOOT进入下载;不由此判断音频好坏
启动反复复位查N16R8实物、16MB Flash/OCT PSRAM、分区/完整地址表和供电;先关闭功放外供
OLED不对/无日志变化核128×32/0x3C/41/42、RST版本、当前镜像及OTA替换;不要先变动音频引脚
声音/麦克风异常按固定I2S/槽/GAIN/物理SD与外供查,回A01/A02分离底层;本站没有声音成功记录
代码 / 配置
python tools/source_state.py restore
python tools/source_state.py verify-baseline
python tools/project.py build

参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[8] 小智v2.4.0固定manifest:IDF最低约束[9] 固定bread-compact-wifi:显示、按钮、工厂与硬件[12] 固定OTA:默认服务与版本更新[13] IDF5.5.5 LCD I2C真实字段顺序[15] PID4440固定revB PCB:上拉/电平转换/自动复位/128×32

09 / 语音实践

8. 来源、许可和未执行记录

上游完整源保留MIT LICENSE;本包辅助Python/配置说明采用MIT,managed组件和模型依各自原许可,不笼统称全为MIT。主机只核源状态、配置和文件,不能证明Flash物理容量、PSRAM稳定、显示、声音、RF或云端对话。hardware-observations.csv保持表头,读者按实际执行填写;不要用ELF字符串存在替代串口实板日志。

参考:[8] 小智v2.4.0固定manifest:IDF最低约束[10] 固定Simplex codec:使用板配置的采样率与数值转换[11] 固定AudioCodec:默认音量与NVS恢复

10 / 语音实践

本次实际完成的检查与仍空白的物理记录

软件结果来自实际命令、当前源码与原始产物。失败和恢复均保留,四个验证维度分别展示;下表没有把主机夹具替代实体实验。

检查项实际结果边界
原样上游真实缺语言头/I2C字段顺序失败,另有空间门限主动停止不是原样上游构建通过
兼容基线 → 单日志 → 恢复三次实际完整增量构建均Exit0当前三个16k xiaozhi.bin各2629296B;旧24k阶段镜像作为历史保留
ELF / 源状态真实ELF字符串仅在修改版出现新日志;源状态工具实际执行变更与恢复ELF字符串存在不是实板串口日志
实板 / 显示 / 声音 / 后端未执行,空CSV没有刷写、激活或对话成功记录

参考:[8] 小智v2.4.0固定manifest:IDF最低约束[9] 固定bread-compact-wifi:显示、按钮、工厂与硬件

11 / 语音实践

完整源状态工具

以下逐字对应下载包当前文件;完整依赖、构建说明、许可和空物理记录一起提供。

代码 / 配置
# ---- tools/source_state.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""One explicitly bounded log-text change, with whole tracked-tree verification."""
from pathlib import Path
import argparse
import difflib
import hashlib
import json

ROOT = Path(__file__).resolve().parents[1]
REL = "main/boards/bread-compact-wifi/compact_wifi_board.cc"
OLD = 'ESP_LOGI(TAG, "Install SSD1306 driver");'
NEW = 'ESP_LOGI(TAG, "A07 OLED driver: SSD1306");'


def expected_changed(original: str) -> str:
    if original.count(OLD) != 1:
        raise ValueError("Expected exactly one original log line")
    return original.replace(OLD, NEW)


def verify(state: str) -> None:
    meta = json.loads((ROOT / "UPSTREAM.json").read_text())
    compat = json.loads((ROOT / "COMPATIBILITY.json").read_text())
    audio = json.loads((ROOT / "AUDIO-COMPATIBILITY.json").read_text())
    for rel, expected_sha in meta["files"].items():
        data = (ROOT / "firmware" / rel).read_bytes()
        if state == "changed" and rel == REL:
            text = data.decode()
            if text.count(NEW) != 1 or OLD in text:
                raise ValueError("Selected file is not the single known change")
            data = text.replace(NEW, OLD).encode()
        if state != "upstream" and rel == REL:
            text = data.decode()
            if text.count(compat["patchedBlock"]) != 1:
                raise ValueError("Expected explicit IDF5.5.5 compatibility patch")
            data = text.replace(compat["patchedBlock"], compat["originalBlock"]).encode()
        if state != "upstream" and rel == audio["relativePath"]:
            text = data.decode()
            if text.count(audio["patchedLine"]) != 1:
                raise ValueError("Expected explicit MAX98357A supported16k output patch")
            data = text.replace(audio["patchedLine"], audio["originalLine"]).encode()
        if hashlib.sha256(data).hexdigest() != expected_sha:
            raise ValueError(f"Unexpected tracked source modification: {rel}")
    print(f"PASS {len(meta['files'])} original tracked files, state={state}")


def main() -> None:
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument("action", choices=["verify-upstream", "apply-compat", "remove-compat", "verify-baseline", "change", "verify-changed", "restore", "diff"])
    args = p.parse_args()
    target = ROOT / "firmware" / REL
    if args.action in ["verify-upstream", "apply-compat"]:
        verify("upstream")
    elif args.action in ["verify-baseline", "change", "diff", "remove-compat"]:
        verify("baseline")
    else:
        verify("changed")
    if args.action in ["apply-compat", "remove-compat"]:
        compat = json.loads((ROOT / "COMPATIBILITY.json").read_text())
        audio = json.loads((ROOT / "AUDIO-COMPATIBILITY.json").read_text())
        before, after = compat["originalBlock"], compat["patchedBlock"]
        if args.action == "remove-compat":
            before, after = after, before
        text = target.read_text()
        if text.count(before) != 1:
            raise ValueError("Compatibility block differs; no file changed")
        audio_target = ROOT / "firmware" / audio["relativePath"]
        audio_before, audio_after = audio["originalLine"], audio["patchedLine"]
        if args.action == "remove-compat":
            audio_before, audio_after = audio_after, audio_before
        audio_text = audio_target.read_text()
        if audio_text.count(audio_before) != 1:
            raise ValueError("Audio compatibility line differs; no file changed")
        target.write_text(text.replace(before, after))
        audio_target.write_text(audio_text.replace(audio_before, audio_after))
        verify("baseline" if args.action == "apply-compat" else "upstream")
    elif args.action == "change":
        target.write_text(expected_changed(target.read_text()))
        verify("changed")
    elif args.action == "restore":
        target.write_text(target.read_text().replace(NEW, OLD))
        verify("baseline")
    elif args.action == "diff":
        original = target.read_text()
        print("".join(difflib.unified_diff(original.splitlines(True), expected_changed(original).splitlines(True), fromfile="a/"+REL, tofile="b/"+REL)), end="")


if __name__ == "__main__":
    main()

参考:[8] 小智v2.4.0固定manifest:IDF最低约束[13] IDF5.5.5 LCD I2C真实字段顺序

12 / 语音实践

完整构建与当前镜像追踪工具

以下逐字对应下载包当前文件;完整依赖、构建说明、许可和空物理记录一起提供。

代码 / 配置
# ---- tools/project.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Use an already activated IDF5.5.5; record inputs and refuse stale-image manifests."""
from pathlib import Path
import argparse
import datetime
import hashlib
import json
import os
import subprocess
import sys

ROOT = Path(__file__).resolve().parents[1]
BUILD = ROOT / 'build'
SOURCE = ROOT / 'firmware'


def fingerprint(path: Path) -> dict:
    return {'path': str(path.resolve()), 'bytes': path.stat().st_size,
            'sha256': hashlib.sha256(path.read_bytes()).hexdigest()}


def inputs() -> list[dict]:
    # Includes generated language header and the resolver lock when present.
    files = [p for p in SOURCE.rglob('*') if p.is_file() and
             'managed_components' not in p.parts and '__pycache__' not in p.parts]
    files += [ROOT/'configs/lab.defaults', BUILD/'sdkconfig']
    if any(not p.is_file() for p in files):
        raise SystemExit('Configure first; missing configuration input')
    return [fingerprint(p) for p in sorted(set(files))]


def products() -> list[dict]:
    files = [p for p in BUILD.rglob('*') if p.is_file() and
             (p.suffix in ['.bin', '.elf', '.map'] or p.name in
              ['sdkconfig', 'flasher_args.json', 'flash_args', 'project_description.json',
               'compile_commands.json'])]
    return [fingerprint(p) for p in sorted(files)]


def run(command: list[str], label: str) -> dict:
    logs = ROOT/'local-logs'
    logs.mkdir(exist_ok=True)
    stamp = datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S%fZ')
    output = logs/f'{stamp}-{label}.log'
    with output.open('wb') as stream:
        stream.write(('COMMAND '+json.dumps(command)+'\n').encode())
        stream.flush()
        process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        assert process.stdout is not None
        for line in process.stdout:
            stream.write(line)
            stream.flush()
            sys.stdout.buffer.write(line)
            sys.stdout.buffer.flush()
        code = process.wait()
        stream.write(f'\nEXIT {code}\n'.encode())
    record = {'command': command, 'exit': code, 'log': fingerprint(output)}
    output.with_suffix('.json').write_text(json.dumps(record, indent=2))
    if code:
        raise SystemExit(code)
    return record


def current_success() -> dict:
    record = ROOT/'local-last-success.json'
    if not record.is_file():
        raise SystemExit('No successful build recorded by this tool; existing bins are insufficient')
    value = json.loads(record.read_text())
    if value['inputs'] != inputs() or value['products'] != products():
        raise SystemExit('Inputs/products changed since the recorded build; rebuild before manifest/flash')
    for step in value['commands']:
        log = Path(step['log']['path'])
        if fingerprint(log) != step['log']:
            raise SystemExit('Recorded build log missing or changed')
    return value


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('action', choices=['configure', 'build', 'manifest', 'flash', 'monitor'])
    parser.add_argument('--port')
    args = parser.parse_args()
    sdk = os.environ.get('IDF_PATH')
    if not sdk or not (Path(sdk)/'tools/idf.py').is_file():
        raise SystemExit('Activate ESP-IDF5.5.5 first; this tool does not install an SDK')
    idf = [sys.executable, str(Path(sdk)/'tools/idf.py')]
    version = subprocess.check_output(idf+['--version'], text=True).strip()
    if version != 'ESP-IDF v5.5.5':
        raise SystemExit('Wrong SDK: '+version+'; expected ESP-IDF v5.5.5')
    base = idf+['-C', str(SOURCE.resolve()), '-B', str(BUILD.resolve()),
                '-D', 'SDKCONFIG='+str((BUILD/'sdkconfig').resolve())]
    if args.action == 'configure':
        run(base+['-D', 'IDF_TARGET=esp32s3', '-D',
                  'SDKCONFIG_DEFAULTS='+str(SOURCE/'sdkconfig.defaults')+';'+str(ROOT/'configs/lab.defaults'),
                  'reconfigure'], 'configure')
    elif args.action == 'build':
        # Fixed upstream ALL target lacks an explicit main-component dependency.
        language = run(base+['lang_header'], 'lang-header')
        before = inputs()
        build = run(base+['build'], 'build')
        if inputs() != before:
            raise SystemExit('Inputs changed during the build; no success record emitted')
        result = products()
        if not any(Path(p['path']).name == 'xiaozhi.bin' for p in result):
            raise SystemExit('Expected application image missing')
        (ROOT/'local-last-success.json').write_text(json.dumps({
            'version': version, 'inputs': before, 'products': result,
            'commands': [language, build], 'hardware': 'not executed by this tool'}, indent=2))
    elif args.action == 'manifest':
        value = current_success()
        value['upstream'] = json.loads((ROOT/'UPSTREAM.json').read_text())['commit']
        (ROOT/'local-products.json').write_text(json.dumps(value, indent=2))
        print(ROOT/'local-products.json')
    else:
        if not args.port:
            raise SystemExit('Provide the physical USB-UART bridge --port')
        if args.action == 'flash':
            current_success()
        run(base+['-p', args.port, args.action], args.action)


if __name__ == '__main__':
    main()

参考:[8] 小智v2.4.0固定manifest:IDF最低约束[13] IDF5.5.5 LCD I2C真实字段顺序

BACK TO THE SOURCE

资料来源与版本说明

固定上游、实际SDK/managed和厂家具体电路章节已核;主机/构建记录不替代指定硬件、HA/云端、光学或声音实测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  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. 小智v2.4.0固定manifest:IDF最低约束
  9. 固定bread-compact-wifi:显示、按钮、工厂与硬件
  10. 固定Simplex codec:使用板配置的采样率与数值转换
  11. 固定AudioCodec:默认音量与NVS恢复
  12. 固定OTA:默认服务与版本更新
  13. IDF5.5.5 LCD I2C真实字段顺序
  14. ESP32-S3-WROOM-1/1U v1.8:N16R8规格表
  15. PID4440固定revB PCB:上拉/电平转换/自动复位/128×32
  16. Adafruit PID4440:具体尺寸、自动复位版本
  17. 固定board config:真实I2S/按钮/GPIO48/18
  18. 固定CMake:语言头生成目标与组件选择
  19. 小智v2.5.2 manifest:>=6.0.1差异
  20. 固定AudioService:decoder采样率与输出重采样路径
  21. 实际esp_audio_effects1.3.0~1来源:rate converter格式/采样率/API约束