这篇要完成什么,尚未验证什么
本篇产出可构建的客户端计时固件、完整串口采集/分析工具和单次时间线。记录来自客户端同一个64位esp_timer单调时钟;“首字”在这里明确操作化为首次非空TTS sentence_start字幕JSON被客户端回调观察到,不是LLM首token、屏幕首像素或声学起音。服务端内部STT/LLM/TTS没有共享时钟和trace_id时,不能靠两台机器的日志相减拆解。
硬件、云端与20次真实会话尚未执行;hardware-observations.csv/attempts.csv/server-stages.csv只有表头。主机20份数据全部来自标记synthetic的生产记录器夹具;它们验证工具,不是性能报告。
参考:[13] 固定Application:事件、手动采音、协议回调与主循环[12] 固定AudioService:任务、队列、解码/重采样与输出[18] ESP-IDF5.5.5 esp_timer:64位开机单调微秒
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,不做边听边播实验。
| 具体器材 | 断电接线与核对 |
|---|---|
| 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. 先定义每个标记,不把调用返回叫送达
只用GPIO47手动按住说话/释放结束;BOOT自动会话、唤醒与AFE不在本测量范围。固件仅在Application处理新StartListening、状态Idle且播放队列空时尝试BEGIN。已有报告没取走、正在播放、重入或复位造成的未记录尝试必须写进attempts.csv;不能只挑有完整报告的轮次。
每个阶段只记一次,最多20行RAM,锁内不做IO/分配;音频任务只写标记,主循环在窗口完成后才批量写A09v1 UART行。字段包含boot观察标签、全局seq、turn、event、us/detail。探针仍有时间戳/短锁/额外8字节packet元数据开销,本次未量化;不能拿探针版数据直接当零开销原机数据。
| 标记 | 精确定义与边界 |
|---|---|
| BEGIN | 主循环接受本次计时窗口;不等于按键物理边沿 |
| LISTEN_CMD_RETURN | 上游SendStartListening返回;不是服务器收到命令 |
| MIC_CHUNK_RETURN | 窗口内发起的首次10ms ReadAudioData成功返回;不等于人开始说话/ADC首样本 |
| FIRST_SEND_RETURN | 带本地epoch的首个音频packet经上游SendAudio返回true;不是无线ACK、TCP对端或云处理成功 |
| STOP_LISTEN_RETURN | 释放后主循环调用SendStopListening返回;不等于服务端收尾时间 |
| STT_JSON | 首次非空stt.text回调观察;可能为中间/最终文本,未区分服务端算法完成 |
| FIRST_TTS_SENTENCE_JSON | 首次非空tts.sentence_start.text回调,字幕可观察点;不是LLM首token |
| FIRST_TTS_AUDIO_CALLBACK | Speaking状态收到首个音频packet;不表示解码成功 |
| FIRST_PCM_WRITE_ENTER / RETURN | 对同epoch的首段PCM调用codec OutputData前/后;返回不证明DMA播完、功放出声或声压 |
| END / failure | 首次PCM调用返回后结束报告,或网络错误/发送失败/解码队列拒绝/重入/观察到60秒超时结束 |
参考:[13] 固定Application:事件、手动采音、协议回调与主循环[12] 固定AudioService:任务、队列、解码/重采样与输出[18] ESP-IDF5.5.5 esp_timer:64位开机单调微秒
3. 本地窗口怎样穿过音频队列
lab_trace_epoch只在AudioStreamPacket/AudioTask内存对象中携带,传给发送、解码、PCM队列;完整WebSocket三种版本与MQTT/UDP SendAudio体的桌面字节夹具核对它没有进wire格式。AES/socket只是明确桩,非加密、无线或远端证明。
raw/manual固定路径下,AfeAudioEngine::Feed调用OutputRawAudio并同步回调OnOutput,lab_feed_epoch只包住此调用,防旧编码包标成新窗口;启用AFE/WakeNet会触发编译范围保护,不能未经重审复用。旧epoch的发送失败不能终止新的记录,完整64位计时避免49天窗口复活。64位ID没有加到云协议:服务端晚到旧音频仍可能落入当前本地观察窗口,所以本报告不是端到端因果trace。逐轮隔离、等播放排空,异常记ambiguous/失败,不编造跨端关联。
解码失败、分配失败、队列清空可能使某段缺失或最终观察超时,不会补0。60秒由主循环下一次poll观察;阻塞网络调用、调度停顿会延后记录,不能称硬实时截止。一次END只表示首段调用或失败报告结束,整句回答可能还在队列;下一轮等完整播放结束和Idle,不能把END当声学结束。
参考:[14] 固定Protocol内存packet与wire边界[15] 固定WebSocket SendAudio三协议版本[16] 固定MQTT/UDP SendAudio加密nonce与输出[17] 固定AfeAudioEngine:raw Feed/OutputRawAudio同步路径[12] 固定AudioService:任务、队列、解码/重采样与输出[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密码与现实对话只留本地私有日志;本次没有账号、激活或真实音频上传/播放实验。
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[27] IDF5.5.5 LCD I2C真实字段顺序[8] ESP32-S3-WROOM-1/1U v1.8:N16R8规格表
5. 采集至少20次的方法与空表
先固定房间/背景噪声、麦克风和喇叭位置、路由器/频段、服务端STT/LLM/TTS版本、同一句短问题、音量/SD位置、串口波特率和固件SHA。使用你获授权观察的后端;若只能看云对话而无服务端阶段日志,只报告客户端区间,不宣称定位到LLM内部。完整server-stages.csv给出可控后端的trace_id、stage及同台服务端start/end单调ns;没有真实ID映射时不得与本地turn硬拼,更不能跨时钟相减。
实际参数以--help为准。工具origin只是调用者标签,不能自动认证这就是物理记录。不要上传包含现实对话的日志;本次提供的CSV无数据行,不给平均延迟或“优化百分比”。
- 先保持SD断开完成供电/录音路径检查,全部按钮释放,桥接UART连接;此阶段记录在hardware-observations.csv,未核通过不开始性能试验。
- 根据真实串口名采集,SD仅在检查完成后按低音量方法启用。固定时长至少覆盖20次完整尝试和失败;另存私有原始UART日志用于诊断。
- 每次只按GPIO47,问题/说话节奏固定,释放后等整句排空、Idle再开下一轮;每次尝试无论成功/失败/重启/无报告都填attempts.csv。
- 每改变一项条件就新建一组捕获;同一boot文件分析,复位后另开文件,并保留重启/无报告分母。
- 至少20个有效样本才能给每项median/p90/p95。completeReports只表示窗口正常END且有首次PCM调用返回,并不保证六项阶段齐全或整句对话成功。不足、缺段、失败不补零;失败率、未报告尝试与各项有效样本数一起呈现。
python -m pip install -r python-requirements.lock
python tools/capture_trace.py --port YOUR_UART_BRIDGE_PORT --baud 115200 --seconds 1200 --output trace-private.txt
python tools/analyze_trace.py trace-private.txt --origin serial-unverified --attempts attempts.csv --min-complete 20 --output local-report.json参考:[13] 固定Application:事件、手动采音、协议回调与主循环[12] 固定AudioService:任务、队列、解码/重采样与输出[14] 固定Protocol内存packet与wire边界[18] ESP-IDF5.5.5 esp_timer:64位开机单调微秒
6. 读报告,按证据提出下一步
工具给六个本地区间:BEGIN→MIC返回、MIC→发送调用返回、STOP→首字幕JSON、STOP→首音频回调、首音频回调→首PCM调用返回、BEGIN→首PCM调用返回。只在同一64位客户端时钟内相减。网络等待、服务端计算、返回传输可能混在STOP后长段,不能直接全算成LLM时间。
分析器拒绝序列缺口/重复、跨boot混文件、格式错误、溢出和不完整窗口;缺少特定阶段保留null/缺失项。分位数用排序后(n−1)p线性插值;仅统计该项完整报告,显示分母和失败/ambiguous数量,不能从20次尝试偷换成20次有效值。
- MIC→send长,先查是否采集积累/编码队列/主循环调度;需要更多本地探针才能继续拆,不据一个数字就换模型。
- STOP→字幕/音频都长,查看获授权的后端阶段区间、服务器负载和网络,保持同问题/配置;没有后端日志则写“未区分”。
- audio回调→PCM返回长,查解码/重采样/队列/调度与原错误日志;PCM返回仍不是声学起音,仪器记录表保持独立。
- 首字幕比音频早或晚都可能;字幕JSON不是LLM首token,客户端也没有证明屏幕已刷新。
参考:[13] 固定Application:事件、手动采音、协议回调与主循环[12] 固定AudioService:任务、队列、解码/重采样与输出[18] ESP-IDF5.5.5 esp_timer:64位开机单调微秒
7. 可复现主机工具检查
主机夹具在macOS/POSIX桌面、clang与ASan/UBSan上执行;PTY依赖os.openpty,测试不是在ESP32上跑。生产Recorder头8组ASan/UBSan涵盖重复标记、重入、旧epoch/旧失败、60秒/完整32位毫秒周期、时钟回退和线程互斥。生产UART格式产生20个明确synthetic窗口;真实文件夹具检查20/19样本门限与6种拒绝与失败标记降级,真实OS PTY分片240行核采集器。不初始化I2S、codec、GPIO或服务器,不把synthetic样本叫麦克风实录。相同完整源、生成体、原log和输入SHA在作者证据归档。
python tests/run_trace_host.py --output /tmp/a09-recorder-contract
python tests/check_files_pty.py --producer-dir /tmp/a09-recorder-contract --output /tmp/a09-file-pty-contract
python tests/check_transport_metadata.py --output /tmp/a09-wire-contract
python tests/check_project_tool.py --output /tmp/a09-project-tool-contract参考:[13] 固定Application:事件、手动采音、协议回调与主循环[12] 固定AudioService:任务、队列、解码/重采样与输出[14] 固定Protocol内存packet与wire边界[15] 固定WebSocket SendAudio三协议版本[16] 固定MQTT/UDP SendAudio加密nonce与输出
8. 故障、恢复和许可
原小智MIT和组件原LICENSE/NOTICE保留,本站探针/工具/概念图MIT,PySerial BSD-3-Clause。source manifest列出原1590、本站改动和新头,不把制造商手册转授为MIT。完整实体20轮、时钟/声学和后端运行仍pending。
| 现象 | 可执行定位与恢复 |
|---|---|
| 没有A09v1或报告少 | 核bridge UART0/115200、手动GPIO47、Idle/队列空、实际ELF标记;等先前报告刷完。保持无报告attempt,不从分母删除 |
| 多boot/seq缺口/OVERFLOW | 保原文件和attempt记录,检查串口拥塞/复位/供电;分boot重新采,不修剪坏行伪造完整 |
| TIMEOUT/缺PCM | 查首音频是否收到、解码/重采样/队列日志;先SD静音,再查服务/堆/PSRAM,恢复同版本完整镜像,不把超时赋0ms |
| 字符串存在却没新编译 | 比source/输入/obj编译日志与ELF专属标记;本工具刷新变化输入mtime。仅Exit0和bin文件不能证明当前源已进入镜像 |
| 换回旧版本 | 关闭SD/功放外供,保存本次原log/镜像和配置,使用完整原下载包+verify_project+新空build,按该次地址表恢复;不要改SDK或删managed锁 |
参考:[10] 固定本板config:I2S/按钮/OLED/未接GPIO48[19] 固定CMake:本板工厂、14px字体与lang_header[28] 固定v2.4.0应用manifest:依赖与IDF最低约束[26] 实际esp_audio_effects1.3.0~1来源:rate converter格式/采样率/API约束
本次实际做过的检查与空白物理记录
作者验证按来源、构建、主机和实物分别记录。正常报告、成功返回和编译通过都不是对话/声音/光学成功;下面的样本标签保留其真实来源。
| 检查 | 实际结果 | 边界 |
|---|---|---|
| 真实固件 | IDF5.5.5实际增量build Exit0/输入稳定 | 2631520B bin;28产品/原日志与全部输入按阶段COW留存 |
| 同源主机 | 生产Recorder8组、完整双transport体4组;真实文件20/19门限/6拒绝+失败降级、OS PTY240行 | 全部使用明确桩/合成时间,非音频/无线/屏幕实测 |
| 实际文件/数据 | 五项防stale工具检查使用真实桌面Ninja/文件;分位数只在各项≥20样本输出 | 字体/ELF/文件计算不能替代实体结果 |
| 实体会话/声音/显示/光 | 未执行,物理表只有表头 | 20次实体尝试/后端/声学待测 |
参考:[13] 固定Application:事件、手动采音、协议回调与主循环[12] 固定AudioService:任务、队列、解码/重采样与输出[19] 固定CMake:本板工厂、14px字体与lang_header[18] ESP-IDF5.5.5 esp_timer:64位开机单调微秒
完整固定RAM记录器
以下与下载包当前文件逐字对应。Application/音频/协议的完整原文件、所有新增头、完整差异、配置/依赖锁、README和许可均随包;CURRENT-SOURCE.json核完整来源树,不靠截断片段重建工程。
// ---- firmware/main/lab_latency_policy.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <mutex>
namespace a09 {
enum class Event : uint8_t {
BEGIN,
LISTEN_CMD_RETURN,
MIC_CHUNK_RETURN,
FIRST_SEND_RETURN,
STOP_LISTEN_RETURN,
STT_JSON,
TTS_START_JSON,
FIRST_TTS_SENTENCE_JSON,
FIRST_TTS_AUDIO_CALLBACK,
FIRST_PCM_WRITE_ENTER,
FIRST_PCM_WRITE_RETURN,
NETWORK_FAILURE,
SEND_FAILURE,
DECODE_QUEUE_REJECT,
TIMEOUT,
REENTRY,
CLOCK_INVALID,
END,
COUNT
};
inline const char* name(Event e) {
constexpr const char* names[] = {"BEGIN",
"LISTEN_CMD_RETURN",
"MIC_CHUNK_RETURN",
"FIRST_SEND_RETURN",
"STOP_LISTEN_RETURN",
"STT_JSON",
"TTS_START_JSON",
"FIRST_TTS_SENTENCE_JSON",
"FIRST_TTS_AUDIO_CALLBACK",
"FIRST_PCM_WRITE_ENTER",
"FIRST_PCM_WRITE_RETURN",
"NETWORK_FAILURE",
"SEND_FAILURE",
"DECODE_QUEUE_REJECT",
"TIMEOUT",
"REENTRY",
"CLOCK_INVALID",
"END"};
return e < Event::COUNT ? names[static_cast<unsigned>(e)] : "INVALID";
}
struct Row {
uint64_t seq = 0, turn = 0, us = 0;
Event event = Event::BEGIN;
int32_t detail = 0;
};
struct Report {
std::array<Row, 20> rows{};
std::size_t count = 0;
bool overflow = false;
};
// No I/O/allocation while locked. Only a completed report may be taken/flushed.
// Does not change the upstream conversation, audio, or network state.
class Recorder {
public:
uint64_t begin(uint64_t now) {
std::lock_guard<std::mutex> lock(mutex_);
if (active_) {
terminal(Event::REENTRY, now);
return 0;
}
if (ready_ || turn_ == std::numeric_limits<uint64_t>::max())
return 0;
++turn_;
start_ = now;
mask_ = 0;
count_ = 0;
overflow_ = false;
active_ = true;
put(Event::BEGIN, now, 0);
return turn_;
}
uint64_t epoch() {
std::lock_guard<std::mutex> lock(mutex_);
return active_ ? turn_ : 0;
}
bool mark(uint64_t epoch, Event event, uint64_t now, int32_t detail = 0) {
std::lock_guard<std::mutex> lock(mutex_);
if (!active_ || !epoch || epoch != turn_ || event >= Event::COUNT ||
event == Event::BEGIN || event == Event::END)
return false;
if (now < start_) {
terminal(Event::CLOCK_INVALID, start_);
return false;
}
const auto bit = uint32_t(1) << static_cast<unsigned>(event);
if (mask_ & bit)
return false;
put(event, now, detail);
if (event == Event::FIRST_PCM_WRITE_RETURN)
finish(now, 0);
return true;
}
void fail_epoch(uint64_t epoch, Event reason, uint64_t now) {
std::lock_guard<std::mutex> lock(mutex_);
if (active_ && epoch && epoch == turn_)
terminal(now < start_ ? Event::CLOCK_INVALID : reason, now < start_ ? start_ : now);
}
void fail(Event reason, uint64_t now) {
std::lock_guard<std::mutex> lock(mutex_);
if (active_)
terminal(now < start_ ? Event::CLOCK_INVALID : reason, now < start_ ? start_ : now);
}
void poll(uint64_t now) {
std::lock_guard<std::mutex> lock(mutex_);
if (active_ && now < start_)
terminal(Event::CLOCK_INVALID, start_);
else if (active_ && now - start_ >= 60000000ULL)
terminal(Event::TIMEOUT, now);
}
bool take(Report& out) {
std::lock_guard<std::mutex> lock(mutex_);
if (!ready_)
return false;
out.rows = rows_;
out.count = count_;
out.overflow = overflow_;
ready_ = false;
count_ = 0;
return true;
}
private:
void put(Event e, uint64_t now, int32_t detail) {
if (count_ == rows_.size()) {
overflow_ = true;
active_ = false;
ready_ = true;
return;
}
rows_[count_++] = {++sequence_, turn_, now, e, detail};
mask_ |= uint32_t(1) << static_cast<unsigned>(e);
}
void finish(uint64_t now, int32_t reason) {
put(Event::END, now, reason);
active_ = false;
ready_ = true;
}
void terminal(Event e, uint64_t now) {
put(e, now, 0);
finish(now, static_cast<int32_t>(e));
}
std::mutex mutex_;
std::array<Row, 20> rows_{};
uint64_t turn_ = 0, sequence_ = 0, start_ = 0;
uint32_t mask_ = 0;
std::size_t count_ = 0;
bool active_ = false, ready_ = false, overflow_ = false;
};
} // namespace a09参考:[13] 固定Application:事件、手动采音、协议回调与主循环[12] 固定AudioService:任务、队列、解码/重采样与输出[14] 固定Protocol内存packet与wire边界[18] ESP-IDF5.5.5 esp_timer:64位开机单调微秒
完整生产时钟与完成后UART输出
以下与下载包当前文件逐字对应。Application/音频/协议的完整原文件、所有新增头、完整差异、配置/依赖锁、README和许可均随包;CURRENT-SOURCE.json核完整来源树,不靠截断片段重建工程。
// ---- firmware/main/lab_latency.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <esp_log.h>
#include <esp_random.h>
#include <esp_timer.h>
#include "lab_latency_policy.h"
#if CONFIG_USE_AUDIO_PROCESSOR || !CONFIG_WAKE_WORD_DISABLED
#error \
"A09 trace scope is the locked raw/manual configuration; re-evaluate epoch propagation for AFE/wake modes"
#endif
namespace a09 {
inline uint64_t now() { return static_cast<uint64_t>(esp_timer_get_time()); }
inline Recorder& recorder() {
static Recorder r;
return r;
}
inline uint64_t epoch() { return recorder().epoch(); }
inline void mark(uint64_t turn, Event event) { recorder().mark(turn, event, now()); }
inline void fail(Event event) { recorder().fail(event, now()); }
inline void fail_epoch(uint64_t turn, Event event) { recorder().fail_epoch(turn, event, now()); }
// Called by Application main-loop owner only, after a report has completed.
// UART is never written from an audio task, or while this measurement is active.
inline void flush() {
recorder().poll(now());
Report report;
if (!recorder().take(report))
return;
static const uint32_t boot = esp_random(); // observation label, not authentication
for (std::size_t i = 0; i < report.count; ++i) {
const auto& r = report.rows[i];
ESP_LOGI("a09", "A09v1 boot=%08lx seq=%llu turn=%llu event=%s us=%llu detail=%ld",
static_cast<unsigned long>(boot), static_cast<unsigned long long>(r.seq),
static_cast<unsigned long long>(r.turn), name(r.event),
static_cast<unsigned long long>(r.us), static_cast<long>(r.detail));
}
if (report.overflow)
ESP_LOGE("a09", "A09v1_OVERFLOW boot=%08lx", static_cast<unsigned long>(boot));
}
} // namespace a09参考:[13] 固定Application:事件、手动采音、协议回调与主循环[12] 固定AudioService:任务、队列、解码/重采样与输出[14] 固定Protocol内存packet与wire边界[18] ESP-IDF5.5.5 esp_timer:64位开机单调微秒
完整本地时间线分析器
以下与下载包当前文件逐字对应。Application/音频/协议的完整原文件、所有新增头、完整差异、配置/依赖锁、README和许可均随包;CURRENT-SOURCE.json核完整来源树,不靠截断片段重建工程。
# ---- tools/analyze_trace.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Analyze locally observed A09 stages; never derives server first-token/acoustic time."""
from pathlib import Path
import argparse,csv,hashlib,json,math,re,statistics
PATTERN=re.compile(r'A09v1 boot=([0-9a-f]{8}) seq=(\d+) turn=(\d+) event=([A-Z_]+) us=(\d+) detail=(-?\d+)(?:\s|$)')
EVENTS={'BEGIN','LISTEN_CMD_RETURN','MIC_CHUNK_RETURN','FIRST_SEND_RETURN','STOP_LISTEN_RETURN','STT_JSON','TTS_START_JSON','FIRST_TTS_SENTENCE_JSON','FIRST_TTS_AUDIO_CALLBACK','FIRST_PCM_WRITE_ENTER','FIRST_PCM_WRITE_RETURN','NETWORK_FAILURE','SEND_FAILURE','DECODE_QUEUE_REJECT','TIMEOUT','REENTRY','CLOCK_INVALID','END'}
METRICS={'setup_to_mic_ms':('BEGIN','MIC_CHUNK_RETURN'),'mic_to_send_call_return_ms':('MIC_CHUNK_RETURN','FIRST_SEND_RETURN'),'stop_to_subtitle_ms':('STOP_LISTEN_RETURN','FIRST_TTS_SENTENCE_JSON'),'stop_to_audio_callback_ms':('STOP_LISTEN_RETURN','FIRST_TTS_AUDIO_CALLBACK'),'audio_callback_to_pcm_call_return_ms':('FIRST_TTS_AUDIO_CALLBACK','FIRST_PCM_WRITE_RETURN'),'local_begin_to_pcm_call_return_ms':('BEGIN','FIRST_PCM_WRITE_RETURN')}
def load(path):
data=path.read_text();turns={};seq={};boots=set()
for number,line in enumerate(data.splitlines(),1):
if 'A09v1_OVERFLOW' in line:raise ValueError(f'recorder overflow at line{number}; reject capture')
if 'A09v1 'not in line:continue
m=PATTERN.search(line)
if not m:raise ValueError(f'malformed trace line{number}')
boot,sequence,turn,event,us,detail=m.groups();sequence,turn,us,detail=map(int,(sequence,turn,us,detail));boots.add(boot)
if event not in EVENTS or not 0<sequence<=0xffffffffffffffff or not 0<turn<=0xffffffffffffffff or us>0x7fffffffffffffff or not -0x80000000<=detail<=0x7fffffff:raise ValueError(f'event/range error line{number}')
if boot in seq and sequence!=seq[boot]+1:raise ValueError(f'missing/duplicate/reordered serial sequence line{number}')
seq[boot]=sequence;row=turns.setdefault((boot,turn),{})
if event in row:raise ValueError(f'duplicate stage line{number}')
row[event]=(us,detail)
if len(boots)>1:raise ValueError('multiple boot labels: split captures and record resets in attempts.csv')
rows=[]
for (boot,turn),stages in turns.items():
if 'BEGIN'not in stages or 'END'not in stages:raise ValueError('truncated turn: preserve raw file and recapture')
begin=stages['BEGIN'][0];end,reason=stages['END'];failures={'NETWORK_FAILURE','SEND_FAILURE','DECODE_QUEUE_REJECT','TIMEOUT','REENTRY','CLOCK_INVALID'};status='complete'if reason==0 and'FIRST_PCM_WRITE_RETURN'in stages and not failures.intersection(stages) else'failed'
if end<begin or any(us<begin or us>end for us,_ in stages.values()):raise ValueError('stage outside local window')
values={};missing=[]
for name,(a,b)in METRICS.items():
if a not in stages or b not in stages:values[name]=None;missing.append(name)
elif stages[b][0]<stages[a][0]:values[name]=None;missing.append(name+':ordering');status='ambiguous'
else:values[name]=(stages[b][0]-stages[a][0])/1000
rows.append({'boot':boot,'turn':turn,'status':status,'end_reason':reason,'missing':missing,**values})
return rows
def percentile(values,p):
values=sorted(values);position=(len(values)-1)*p;lo=math.floor(position);hi=math.ceil(position)
return values[lo]+(values[hi]-values[lo])*(position-lo)
def main():
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('capture',type=Path);parser.add_argument('--origin',choices=['serial-unverified','synthetic-fixture'],required=True);parser.add_argument('--attempts',type=Path);parser.add_argument('--min-complete',type=int,default=20);parser.add_argument('--output',type=Path,required=True);args=parser.parse_args()
if args.min_complete<20:parser.error('min-complete must be at least20')
if args.origin=='serial-unverified'and not args.attempts:parser.error('physical method requires the complete manual attempts.csv including reset/no-report rows')
try:
rows=load(args.capture);attempts=list(csv.DictReader(args.attempts.open(newline='')))if args.attempts else[]
if args.attempts and len(attempts)<len(rows):raise ValueError('manual attempts cannot be fewer than reports')
except (ValueError,OSError)as error:parser.exit(2,str(error)+'\n')
complete=[r for r in rows if r['status']=='complete'];metrics={}
for name in METRICS:
values=[r[name]for r in complete if r[name]is not None]
metrics[name]={'validSamples':len(values),'median':statistics.median(values)if len(values)>=args.min_complete else None,'p90':percentile(values,.9)if len(values)>=args.min_complete else None,'p95':percentile(values,.95)if len(values)>=args.min_complete else None}
summary={'origin':args.origin,'physicalValidation':'not established by this tool; origin is a caller label','captureSha256':hashlib.sha256(args.capture.read_bytes()).hexdigest(),'reportedTurns':len(rows),'completeReports':len(complete),'completeDefinition':'normal END+firstPCMcallreturn withoutfailureevent; not allmetricstages, entireaudio, or successfuldialogue','failedOrAmbiguousReports':len(rows)-len(complete),'manualAttemptRows':len(attempts),'unreportedAttempts':len(attempts)-len(rows)if args.attempts else None,'minimumSamplesPerMetric':args.min_complete,'quantileMethod':'linear interpolation at (n-1)*p; failures/missing stages counted, not assigned zero','metrics':metrics,'turns':rows,'notMeasured':['server STT/LLM/TTS internals','server first token','acoustic onset','wireless ACK or remote receipt','trace probe overhead']}
args.output.write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n');print(json.dumps({k:summary[k]for k in ['origin','reportedTurns','completeReports','failedOrAmbiguousReports','manualAttemptRows','unreportedAttempts']},ensure_ascii=False));print(args.output)
if __name__=='__main__':main()参考:[13] 固定Application:事件、手动采音、协议回调与主循环[12] 固定AudioService:任务、队列、解码/重采样与输出[14] 固定Protocol内存packet与wire边界[18] ESP-IDF5.5.5 esp_timer:64位开机单调微秒
资料来源与版本说明
固定上游、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打包