Skip to content

✨ feat(voice): 接入 Profile 驱动的 ESP32-S3 PCM Audio Port - #114

Merged
ZhaoXingPeng merged 39 commits into
1024XEngineer:mainfrom
ZhaoXingPeng:dev/113-voice-audio-port
Aug 6, 2026
Merged

✨ feat(voice): 接入 Profile 驱动的 ESP32-S3 PCM Audio Port#114
ZhaoXingPeng merged 39 commits into
1024XEngineer:mainfrom
ZhaoXingPeng:dev/113-voice-audio-port

Conversation

@ZhaoXingPeng

@ZhaoXingPeng ZhaoXingPeng commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

这份 PR 将 Profile 驱动的 ESP32-S3 PCM Audio Port 接入 Runtime:10 ms I2S hardware period 可组装为 60 ms PCM 传输帧,采集、投递、播放路径相互隔离,并已在真实 voicelife-pcb 板完成非活动 OTA 槽验证和恢复。请按音频 Port 契约、队列背压、Profile 边界和实板证据进行 Review。

请求动作:先审查并保留 Draft;本 PR 依赖 #112,待 #112 合并、CI 通过并完成人工 Review 后,再将本 PR 转为 Ready,并在确认完整满足 #113 验收条件时把 Refs #113 改为 Fixes #113

Refs #113
Refs #112
Refs #91

生命周期

Review 清单

  • 确认 PcmFrameAssembler 对 period/帧时长、声道完整性和溢出拒绝符合契约。
  • 确认 capture、delivery、playback 任务不会让网络回调阻塞 I2S;上行丢最旧、下行拒绝最新的策略符合产品取舍。
  • 确认纯 I2S Profile 不伪造 Codec/AEC/Opus 能力,主机路径不伪造真机成功。
  • 确认 Runtime smoke 的资源释放、失败回退和统计字段足以支持后续 Linx 接入。
  • 确认 🏗️ refactor(voice): 接入 voicelife-pcb 纯 I2S PCM Profile #112 合并后 PR 基线和最终关闭语义。

范围

  • 新增平台无关的 PCM 帧组装器和 ESP32-S3 I2S Audio Port。
  • 使用固定容量输入/输出队列,记录丢帧、拒绝、短读、短写、水位和最低堆。
  • 支持 16/32-bit I2S wire slot 与 PCM S16LE 转换,Profile 只声明真实 voicelife-pcb 纯 I2S 能力。
  • 补充主机 TDD、ESP-IDF 构建、Profile 校验、README、语音架构和实板恢复证据。

明确不包含

  • ES8311/ES7210/PCA9557 控制、功放和外部声学听感。
  • AFE/AEC/WakeNet、Opus、重采样和真实 Linx WSS/ASR/TTS 云端闭环。
  • 其他 MCU 的已支持声明;跨板内容仍是研究矩阵和准入规则。

验证

  • ./scripts/run_checks.sh:12/12 CTest、架构检查、三个 Profile 校验、Python 4/4。
  • ESP-IDF 6.0.2 esp32s3-voicelife-pcb-pcm 构建。
  • 实板 115200、只写 ota_1@0x410000、回读逐字节一致;Audio Port smoke 采集 4 帧、播放 1 帧,丢帧/拒绝/短读/短写均为 0,最低空闲堆 358016 B。
  • 测试结束恢复 otadata;原固件从 ota_0 启动,SQLite 仍加载 7 个事件、8 个提醒、0 条笔记。
  • 物理声学质量、Linx 云端、Codec/AFE/Opus:明确留给后续 Issue。

风险与收尾

AI 使用说明

  • AI 辅助了代码检索、TDD 方案整理、测试编排和文档初稿;作者逐文件核对实现,运行主机/固件检查,并对真实 ESP32-S3 串口、镜像回读、哈希和恢复结果负责。

研究资料归档

本 PR 的原始研究目录已移出最终文件树,统一归档到 Issue #150。保留在本 PR 的文档仅限随实现长期维护的架构、迁移、硬件验证和 README 当前状态;研究原文、来源摘录、delta、refresh target 和一次性证据不作为主仓库产品文档。

Research archive: Refs #150

@ZhaoXingPeng ZhaoXingPeng added the sub-task 实现拆分自父任务的可验收子任务 label Aug 4, 2026
@ZhaoXingPeng ZhaoXingPeng self-assigned this Aug 4, 2026
@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

@fennoai 你是资深后端/全栈架构师 + 嵌入式硬件工程师复合型专家,执行严格、客观、不留情面的代码仓库Review,请遵循下面所有评审规则,逐条输出审查结果,禁止敷衍、禁止只说空话、禁止笼统概括。
硬性约束:最终输出评审条目数量不少于20条,若直观可见问题不足,主动挖掘隐性架构隐患、软硬件兼容风险、长期运行潜在缺陷补足条目,不得简化评审内容。

评审维度

  1. 架构与模块设计
  • 模块职责是否清晰,是否存在循环依赖、职责混杂
  • 分层是否合理,是否违反单一职责原则、开闭原则
  • 接口抽象、依赖注入设计是否规范,有无硬编码耦合
  • 软件与硬件模块边界是否清晰,硬件相关逻辑是否侵入业务层
  1. 代码规范与可读性
  • 命名:变量、函数、类、文件命名是否语义清晰,禁止模糊命名、拼音命名
  • 注释:复杂逻辑、硬件时序、特殊寄存器配置必须注释;冗余注释、无效注释、过期注释需要指出
  • 代码格式、风格是否统一,是否存在大量魔法数字、魔法字符串,硬件参数无常量定义
  1. 性能隐患
  • 循环内IO、数据库重复查询、不必要的内存占用、低效算法
  • 资源是否释放(连接、句柄、定时器、文件流、硬件外设句柄)
  • 嵌入式场景:阻塞轮询、中断处理耗时过长、内存频繁分配释放
  1. 安全性检查【重点】
  • 输入校验、SQL注入、XSS、权限控制、敏感信息明文打印
  • 密钥、token、数据库地址、硬件访问口令是否硬编码提交到仓库
  • 外部指令下发至硬件驱动缺少权限校验,存在设备失控风险
  1. 健壮性 & 异常处理
  • 是否缺少异常捕获、错误分支处理
  • 参数判空、边界条件、失败重试逻辑是否完备
  • 硬件通讯异常(I2C/SPI/UART断线、设备无应答)缺少容错、恢复逻辑
  • 缺少硬件故障状态上报、故障隔离机制
  1. 硬件驱动 & 软硬件协同评审(新增专项)
  • 驱动代码与硬件原理图引脚定义是否匹配,无硬件版本兼容逻辑
  • 外设操作缺少电平保护、超时判断,存在烧毁外设芯片风险
  • 运动控制逻辑(如有)缺少软限位、急停、碰撞检测保护
  • 上下位机通讯协议:缺少校验和、重传、断线重连机制
  • 硬件参数(电流、电压、速度阈值)直接写死代码,无统一配置入口
  1. 测试覆盖
  • 是否缺少单元测试、边界用例
  • 核心业务逻辑、硬件驱动逻辑无测试直接合并风险
  • 缺少硬件异常工况、极限压力场景测试用例
  1. 工程规范
  • 无用代码、注释掉的废弃代码是否大量遗留
  • 依赖包版本风险、冗余依赖、存在已知漏洞依赖
  • 配置区分环境(开发/样机/生产),环境配置是否混写
  • 固件、上位机、3D模型、原理图版本号不统一,无关联管理
  1. 可维护性 & 扩展性
  • 新增需求是否需要大面积修改原有代码
  • 是否存在大量重复代码,缺少公共封装
  • 硬件型号迭代时,软件需要大规模改动,缺少适配抽象层

输出要求

  1. 问题分级:🔴严重阻塞(必须修改才能合并/上机测试) / 🟡建议优化(不阻塞但强烈建议重构) / 🟢可选改进
  2. 每条问题固定格式:【文件路径+代码位置】问题描述 + 风险说明 + 可直接落地的修改方案
  3. 条目数量强制≥20条;问题较少时区分表层问题与隐性长期风险,拆分为独立条目逐条列出,禁止多条问题合并为一条
  4. 最后汇总三部分:
    ① 整体仓库风险总结
    ② 优先级整改清单(阻断项优先)
    ③ 长期架构、软硬件协同优化方案
  5. 如果存在架构层面缺陷,单独提炼顶层设计问题,不要只局限单行代码
  6. 不要美化结论,发现隐患直接指出,客观评估风险等级;不使用“建议考虑”这类温和模糊表述,明确写出故障后果(程序崩溃、硬件烧毁、机构失控、数据泄露等)

现在开始对提供的仓库代码/PR变更、硬件资料、结构模型进行评审

@fennoai

fennoai Bot commented Aug 4, 2026

Copy link
Copy Markdown

严格 Review 结论

结论:当前应继续保持 Draft,不应转 Ready。./scripts/run_checks.sh 在本地通过:12/12 CTest、架构检查、3 个 Profile 校验、Python 4/4。未在本地复跑 ESP-IDF 实板构建/烧录;仓库变更中也没有可核对的原理图、结构模型或硬件版本 BOM,因此引脚与电气风险只能按代码/Profile 边界审查。

  1. 🔴严重阻塞:【components/voicelife_runtime/src/runtime.cc:144-213】【config/profiles/esp32s3-voicelife-pcb-pcm.json:17-19】Runtime 仍然启动 ScaffoldAudioAdapter/ScaffoldSpeechAdapter 和旧 VoiceSessionCoordinator,Profile 也把 speech 标成 scaffold。风险:PR 声称“接入 Runtime”,但真实 Linx provider、ESP WebSocket transport、Esp32s3PcmAudioPorts 没有组成端到端语音链路,上机只能跑 probe/smoke,不能证明 [Voice] 接入 Profile 驱动的 ESP32-S3 PCM Audio Port 与跨板能力契约 #113 的 Runtime 语音能力。修改方案:新增 Profile 驱动的 runtime factory,把 audio driver esp32s3-pcm-port、speech driver linx-websocket、secret resolver、codec、transport、VoiceSession 组装为同一生命周期,并让 runtime_smoke_test 覆盖该 wiring。

  2. 🔴严重阻塞:【components/voicelife_linx/src/linx_speech_provider.cc:37-40】DefaultCapabilities() 声明 opus,但本 PR 没有 Opus 编解码、重采样或转码策略。风险:能力协商会误导上层选择 Opus,随后 SendAudio() 直接发送 PCM payload,云端解码失败或播放噪声。修改方案:默认能力删除 opus;只有注入 CodecStrategyEncode/Decode 覆盖 Opus 后才注册 opus 能力,并加测试验证 Profile 不伪造能力。

  3. 🔴严重阻塞:【components/voicelife_linx_esp/src/esp_websocket_transport.cc:291-301】WorkerLoop()while (running && xQueueReceive(..., 100ms) == pdTRUE) 在 100ms 没事件时直接退出。风险:WebSocket 空闲超过 100ms 后事件工作线程死亡,后续 data/disconnect/error 永远不处理,连接假活、音频下行丢失、重连失效。修改方案:改成 while (running_.load()) { if (xQueueReceive(...) != pdTRUE) continue; ... },并增加“空闲后仍能处理下一帧”的 host/ESP fake 测试。

  4. 🔴严重阻塞:【components/voicelife_voice/src/voice_session.cc:273-303】【components/voicelife_linx_esp/src/esp_websocket_transport.cc:146-154】采集回调同步调用 provider_.SendAudio(),ESP 端 SendAudio() 又同步等待 network_timeout_ms,默认 10s。风险:Delivery task 被网络阻塞,input queue 很快堆满并丢旧帧;实时链路退化为大面积断句/丢语音,背压统计不能阻止故障。修改方案:在 provider/transport 之间加入专用有界上行发送队列和 TX task;HandleInputAudio() 只做格式/序号校验并非阻塞入队,队满按产品策略丢旧/拒新并上报。

  5. 🔴严重阻塞:【components/voicelife_linx_esp/src/esp_websocket_transport.cc:134-154】esp_websocket_client_send_text/bin() 只判断 <0,没有校验正数短发送。风险:底层只写出部分 JSON 或部分 PCM 时仍返回成功,协议帧被截断,云端状态机错乱或音频帧损坏。修改方案:要求 sent == message.size() / sent == frame.payload.size(),短发送返回 kUnavailable 并记录断链/重试策略。

  6. 🔴严重阻塞:【components/voicelife_linx/include/voicelife/linx/linx_types.h:23-26】【components/voicelife_linx_esp/src/esp_websocket_transport.cc:201-208】device_idclient_id、token 进入 HTTP header 前只判空,没有禁止 CR/LF。风险:配置或 secret resolver 被污染时可注入额外 header,造成认证绕过、设备身份串改或敏感字段泄露。修改方案:LinxConnectionConfig::valid() 和 token 解析后统一拒绝 \r\n、控制字符和超长字段;header 构造前增加专用 sanitizer 与测试。

  7. 🔴严重阻塞:【components/voicelife_linx/src/linx_json_codec.cc:398-404】【components/voicelife_linx/src/linx_speech_provider.cc:254-341】解析出了 session_id 但 Provider 完全不校验它是否等于当前 config_.session_id。风险:旧会话或其他会话的 TTS/STT/error 消息可以驱动当前会话状态,导致串音、误播报或错误打断。修改方案:在 OnText() 进入 switch 前校验 inbound.session_id;不匹配直接丢弃并记录 stale_session_dropped

  8. 🔴严重阻塞:【components/voicelife_linx_esp/src/esp_websocket_transport.cc:157-188】【components/voicelife_linx_esp/src/esp_websocket_transport.cc:246-284】client_sink_headers_state_ 在 Connect/Close/Send 与 ESP 事件回调之间没有统一互斥。风险:Close 清空 sink/header 或 destroy client 的同时事件回调仍在排队/访问,可能触发 use-after-close、假失败状态或崩溃。修改方案:引入 transport lifecycle mutex;Close 先 unregister/stop,等待 worker 确认退出,再清理 sink/header/client;所有 public 方法与事件入队共享同一状态机锁。

  9. 🔴严重阻塞:【components/voicelife_linx_esp/src/esp_websocket_transport.cc:227-230】CleanupWorker() 可以直接 vTaskDelete(worker_) 删除仍在运行的 worker。风险:任务可能正持有 assembler/sink 资源或处理回调,被异步删除会造成资源泄漏、半更新状态和后续死锁。修改方案:只允许通过 running_=false + shutdown envelope + semaphore 等待自然退出;等待失败返回错误,不删除活任务。

  10. 🔴严重阻塞:【components/voicelife_audio_esp/src/esp32s3_pcm_audio_port.cc:554-578】【components/voicelife_audio_esp/src/esp32s3_pcm_audio_port.cc:626-652】I2S 短读/短写只计数,状态机不降级、不关闭、不上报到 session/provider。风险:麦克风断线、DMA 卡死或功放侧异常时系统继续“运行”,上层无法故障隔离,长时间现场设备会静默失效。修改方案:为 AudioInputPort/AudioOutputPort 增加 error sink 或健康状态;连续 N 次短读/短写进入 failed,停止对应通道并上报 evidence。

  11. 🟡建议优化:【components/voicelife_audio_esp/src/esp32s3_pcm_audio_port.cc:606-633】播放每个 hardware period 都构造 std::vector<uint8_t> wire(bytes)。风险:ESP32-S3 实时播放路径频繁堆分配,长期运行会造成堆碎片、抖动和短写,最终表现为爆音或播放中断。修改方案:在 Output task 初始化时按最大 period 预分配 wire buffer,循环复用;禁止音频实时循环内动态分配。

  12. 🟡建议优化:【components/voicelife_audio_esp/src/pcm_frame_assembler.cc:74-85】pending_samples_.erase(begin, begin + frame_samples_) 每组一帧都移动剩余 buffer。风险:在更高采样率/多声道/更大帧长下变成 O(n) 数据搬移,增加采集 task 抖动。修改方案:改成 ring buffer 或读写 offset;组帧完成后只移动索引,必要时批量 compact。

  13. 🟡建议优化:【components/voicelife_audio_esp/include/voicelife/audio_esp/esp32s3_pcm_audio_port.h:12-15】【components/voicelife_audio_esp/src/esp32s3_pcm_audio_port.cc:554-556】io_timeout_ms=100 明显大于 10ms hardware period。风险:一次 I2S 读写阻塞可吞掉 10 个 period,队列统计滞后,实时延迟不可控。修改方案:把 timeout 拆成 read_timeout_ms/write_timeout_ms,默认不超过 1-2 个 period;超时阈值写入 Profile 并在 Validate 中约束。

  14. 🟡建议优化:【components/voicelife_audio_esp/src/audio_board_profile.cc:15-45】【components/voicelife_audio_esp/src/audio_board_profile.cc:172-203】GPIO 校验只允许 0-48 和端点内去重,没有板级 allowlist、strapping/USB/flash 保留脚、硬件版本边界。风险:Profile 被复制到新板或改错脚位时可能占用启动/下载/外设关键脚,导致无法启动、外设冲突,严重时误驱动外设。修改方案:按 board revision 建立 BoardPinMap,显式标注可用 I2S/I2C/MCLK/禁止脚;Validate 必须检查硬件版本和用途矩阵。

  15. 🟡建议优化:【components/voicelife_audio_esp/src/audio_board_profile.cc:35-46】【components/voicelife_voice/include/voicelife/voice/voice_types.h:34-36】音频格式合法性只判大于 0,没有限制采样率、bit depth、frame duration、声道组合。风险:错误 Profile 可通过 Validate,随后 DMA buffer 计算、组帧和网络帧大小异常,导致内存暴涨或时序崩溃。修改方案:定义支持矩阵,例如 PCM S16LE、16/24k、1/2ch、10/20/60ms;Profile 和 negotiated format 都按矩阵拒绝。

  16. 🟡建议优化:【components/voicelife_audio_esp/src/audio_board_profile.cc:185-199】pcm_shift_bits 对 capture=14、playback=16 是硬编码经验值,缺少硬件版本、麦克风型号、I2S 对齐模式、增益校准来源。风险:不同批次 I2S 麦克风/功放对齐方式变化会导致削波、音量过小或符号扩展错误;PR 中已记录 47500 ppm clipping,但代码未把它变成准入保护。修改方案:把 shift/gain/attenuation 放入 Profile JSON,附校准证据;Validate 限制范围,probe 超过饱和阈值直接失败。

  17. 🟡建议优化:【components/voicelife_linx/src/linx_json_codec.cc:65-69】【tests/host/linx_provider_contract_test.cc:119-122】手写 JSON parser 拒绝所有 \u escape。风险:服务端返回转义中文或 emoji 文本时解析失败,TTS sentence/STT 文本被当错误处理,真实中文业务不稳定。修改方案:ESP 端接入 cJSON/成熟 JSON parser;host fixture 至少正确处理 \uXXXX 或明确测试协议永不发送 unicode escape。

  18. 🟡建议优化:【components/voicelife_linx/src/linx_json_codec.cc:232-259】Quote() 未转义 \b\f 和其他 0x00-0x1f 控制字符。风险:用户播报文本或 reason 中包含控制字符会生成非法 JSON,云端断开或解析出错。修改方案:完整实现 JSON string escaping,控制字符统一输出 \u00XX;增加 fuzz/边界测试。

  19. 🟡建议优化:【components/voicelife_linx/src/linx_json_codec.cc:319-328】frame_size 只计算 sample_rate * duration / 1000,没有乘 channels 和 bytes-per-sample。风险:如果协议字段表示字节数或总 samples,多声道/非 16bit 时 hello 会错误协商,后续帧大小不一致。修改方案:明确协议定义;若是字节数则改为 samples_per_channel * channels * bits_per_sample / 8,若是每声道 sample 数则重命名字段封装并测试双声道。

  20. 🟡建议优化:【components/voicelife_linx/src/linx_speech_provider.cc:344-360】【components/voicelife_audio_esp/src/esp32s3_pcm_audio_port.cc:382-384】下行 binary payload 只判非空,输出端只要求样本数能被声道整除,没有要求 payload 恰好等于协商 frame_duration。风险:云端发送半帧/多帧混包时播放 sequence 与帧时长语义失真,缓冲延迟和统计字段错误。修改方案:按 audio_formats_.playback 计算 expected bytes;不等于预期时拒绝或先经过 frame assembler/packetizer。

  21. 🟡建议优化:【components/voicelife_linx_esp/src/esp_websocket_transport.cc:278-283】【components/voicelife_linx_esp/src/esp_websocket_transport.cc:307-314】事件队列满时只置 failed 和调用 on_error,不停止 client,也没有断线重连策略。风险:队列短时拥塞后 transport 进入 failed,但底层 socket 继续产生事件,系统处于半死状态。修改方案:队列溢出后执行统一 FailAndClose():停止 websocket、清空 assembler、通知 provider disconnected/error,并要求上层显式 reconnect。

  22. 🟡建议优化:【components/voicelife_audio_esp/src/esp32s3_pcm_audio_port.cc:203-231】【components/voicelife_audio_esp/src/esp32s3_pcm_audio_port.cc:235-281】输入和输出任一 Open 都会校验 input_queue_depthoutput_queue_depth,且 capture 必须等待 output 打开。风险:未来只采集、只播放、诊断模式或单向硬件路径不能复用该 Port;模块职责被双向资源假设绑死。修改方案:把 queue depth 校验拆到对应端口;支持单向 mode,只有同一 I2S duplex/codec 场景才要求双端共同初始化。

  23. 🟡建议优化:【main/Kconfig.projbuild:3-8】VOICELIFE_AUDIO_PROBE 没有 depends on IDF_TARGET_ESP32S3,但实现和 Profile 都是 ESP32-S3 I2S。风险:其他 ESP target 可打开该配置,构建或运行时才失败,环境配置混写。修改方案:给 probe 总开关增加 depends on IDF_TARGET_ESP32S3,并把每个 profile choice 绑定 target/board。

  24. 🟡建议优化:【components/voicelife_runtime/src/runtime.cc:112-125】Audio Port smoke 只输出 AUDIO_PORT_SIGNAL 和短读/短写统计,没有把 clipping/saturation 作为失败条件。风险:PR 已发现削波样本高,smoke 仍可通过,后续把“总线有信号”误判成“音频链路健康”。修改方案:复用 probe 的 peak/saturation 统计,设置上限阈值;超过阈值返回失败并输出 calibration required。

  25. 🟢可选改进:【tests/host/CMakeLists.txt:40-42】【components/voicelife_linx_esp/src/esp_websocket_transport.cc:56-188】host 测试只编译 websocket_fragment_assembler.cc,没有覆盖真实 EspWebSocketTransport 生命周期。风险:本次 WorkerLoop() 100ms 空闲退出、短发送、Close 竞态这类问题不会被 12/12 CTest 捕获。修改方案:抽象 ESP websocket C API shim,host fake client 覆盖 connect timeout、idle、short send、queue overflow、close while callback。

  26. 🟢可选改进:【tests/host/voice_session_contract_test.cc:195-210】【components/voicelife_voice/src/voice_session.cc:254-270】SubmitAudio() 路径有测试,但 HandleInputAudio() 的网络阻塞、SendAudio 失败后序号保持、连续失败降级没有压力测试。风险:真实采集回调下错误统计和重试行为不清晰,长期运行会出现重复序号或持续丢帧。修改方案:增加 fake provider 阻塞/失败用例,验证 input sink 不长时间占用实时路径,并定义失败 N 次后的 session 状态。

  27. 🟢可选改进:【docs/engineering/esp32-hardware-validation.md:1】【research/voice-module-portability-20260804/sources/15_voicelife_pcb_i2s_profile.md:1】文档记录了实板证据,但仓库没有可审查的原理图版本、PCB revision、BOM 或 3D/结构模型关联。风险:代码 Profile 与硬件资料无法闭环,后续板改可能软件继续使用旧引脚/电平假设。修改方案:添加硬件资料索引文件,至少包含 board revision、原理图提交/文件哈希、关键 I2S/I2C net 名称、禁止引脚和机械版本映射。

整体仓库风险总结

当前 PR 的平台无关组帧、队列、Profile 校验和主机契约测试有基础,但“Runtime 接入”和“真实 Linx 闭环”仍不成立;ESP WebSocket transport 存在 worker 空闲退出、短发送误判、生命周期竞态和 header 注入风险;I2S Port 缺少故障上报和实时内存约束。硬件侧只有 Profile 与验证文档,缺少可审查的原理图/结构版本闭环,不能客观证明引脚、电平和硬件版本兼容性。

优先级整改清单

  1. 先修 WorkerLoop() 空闲退出、短发送校验、Close/回调并发、活任务删除问题。
  2. 删除 Linx opus 虚假能力;补齐 session_id 校验、header sanitizer、下行 payload 长度校验。
  3. 把 capture 上行改为非阻塞有界 TX 队列,消除网络发送对采集投递路径的同步阻塞。
  4. Runtime 必须真正按 Profile 组装 ESP Audio Port + Linx Provider + VoiceSession,否则继续保持 Draft。
  5. I2S 短读/短写/削波必须变成 health failure/evidence,而不是只计数。

长期架构与软硬件协同优化方案

建立三层边界:BoardProfile/PinMap 只描述硬件事实,AudioPort 只负责 I2S/DMA/故障健康,VoiceSession/Provider 只负责协议与会话状态。所有跨层数据必须走有界队列和显式 health/error sink。硬件资料需要进入版本化索引:PCB revision、原理图哈希、I2S/I2C net、允许/禁止 GPIO、增益校准、饱和阈值和实板验证记录一一对应;没有资料闭环的 Profile 只能作为 research/probe,不能声明生产能力。

@ZhaoXingPeng
ZhaoXingPeng force-pushed the dev/113-voice-audio-port branch from 83054fa to 123b58b Compare August 4, 2026 02:39
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ZhaoXingPeng
ZhaoXingPeng force-pushed the dev/113-voice-audio-port branch from bee105c to 72a460c Compare August 4, 2026 06:52
@ZhaoXingPeng
ZhaoXingPeng force-pushed the dev/113-voice-audio-port branch from 7b5017b to 434ee82 Compare August 5, 2026 03:14
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
Rebase 后 PR 1024XEngineer#114 新增的测试要求:
- Connect 失败时调用 Disconnect() 回滚
- BeginCapture 输入+回滚都失败时转 kFailed
- Stop 中断开失败时不伪装 kStopped, 转 kFailed

修复 Stop() 中 Emit 死锁: 先解锁 mutex_ 再调 Emit。

主机测试 20/20 通过。

Refs 1024XEngineer#106
@ZhaoXingPeng
ZhaoXingPeng force-pushed the dev/113-voice-audio-port branch 3 times, most recently from 8628f4d to 83bb0e8 Compare August 6, 2026 07:00
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
基于最新 1024XEngineer#112 重建 PR 1024XEngineer#114:
- audio_esp 源文件替换为 PR 1024XEngineer#114 最终版本 (capture_i2s/playback_i2s 字段)
- CMakeLists 补 audio_esp 库与 audio_board/pcm_frame 测试
- 恢复 Connect 失败 Disconnect 回滚、BeginCapture 双重失败转 kFailed、
  Stop 断开失败不伪装 kStopped

主机测试 27/27 通过。

Refs 1024XEngineer#113
@ZhaoXingPeng
ZhaoXingPeng force-pushed the dev/113-voice-audio-port branch 2 times, most recently from 7e2f72d to 5a6d86a Compare August 6, 2026 08:01
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
同步 PR 1024XEngineer#114 的 Doxygen 修复: audio_board_profile.h,
esp32s3_audio_probe.h 公开 API 补齐 /** @brief @PARAM @return */ 注释。

主机测试 25/25 通过, Doxygen PASS。

Refs 1024XEngineer#111
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
同步 PR 1024XEngineer#114 的 Doxygen 修复: audio_board_profile.h,
esp32s3_audio_probe.h 公开 API 补齐 /** @brief @PARAM @return */ 注释。

主机测试 25/25 通过, Doxygen PASS。

Refs 1024XEngineer#109

@jing-gou jing-gou left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥵🥵🥵🥵🥵🥵🥵🥵🥵🥵 Yeah

建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue 1024XEngineer#107 验收。
Linx 服务端可能在重连 hello 中返回不同的下行采样率或帧时长;如果继续沿用旧的播放配置,会把协议已变更的音频静默送进错误的 AudioOutput。物理链路在 hello 失败但尚未完成清理时也不能被重复 Connect。

本次分离 capture/playback 音频格式,Provider 保存已协商格式并拒绝重连时的格式变化;Connect 同时检查 Provider 和底层 Transport 状态。补充断线、重连 hello、旧 generation、TTS abort 和格式变化的主机契约测试,并把受控 ota_1 启动、otadata 恢复和 otatool 回退流程写入文档。

验证:./scripts/run_checks.sh(主机 9/9、架构、Profile、Python 4/4);ESP-IDF 6.0.2 / ESP32-S3 构建通过,voicelife.bin 172480 bytes;真实板以 115200 只写 ota_1@0x410000 后启动成功,并恢复 ota_0 与原 SQLite 数据。

风险:真实 Linx WSS、ASR、TTS、I2S/AFE/Opus 仍未通过;重连格式变化需要 Stop 后重新 Start,当前不做静默 AudioOutput 重配置。

Upstream: 78/xiaozhi-esp32@dd99da0

Refs 1024XEngineer#107

Refs 1024XEngineer#91
GitHub GCC 13 在 -Wmissing-field-initializers -Werror 下拒绝 VoiceEvent 聚合初始化,导致主机测试 job 无法编译。

为 TTS start/stop、断线和重连 fixture 显式补齐 text 与 aborted 字段,保持测试意图不变。

验证:voice_session_contract_test 通过;等待远端完整 CI 复跑。

Refs 1024XEngineer#107

Refs 1024XEngineer#91
把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs 1024XEngineer#105\nRefs 1024XEngineer#91
参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。

Refs 1024XEngineer#107
Refs 1024XEngineer#105
Refs 1024XEngineer#91
Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。

Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式,
避免协商结果被错误信任。

play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50,
格式变化时自动缩放缓冲时长。

  Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#121024XEngineer#151024XEngineer#21。

主机测试 10/10 通过。

Refs 1024XEngineer#106
UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。
支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。
解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。

FindField: 匹配字段名前先验证前驱字符为 { 或 ,,
避免把值里的同名字符串误判为对象键。

Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#111024XEngineer#181024XEngineer#19(部分缓解)。

主机测试 10/10 通过。

Refs 1024XEngineer#106
third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。

LinxJsonCodec 全面重写:
  Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted
  DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过
    cJSON_GetObjectItem 按类型读取
  删除: ReadJsonString, SkipSpace, FindField, ReadStringField,
    ReadUnsignedField, ReadBoolField, ReadObjectField, Quote
    (~300行手动解析逻辑)
  保留: CodecName, ModeName

主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson
ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c

修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持
  cJSON_False|cJSON_True 组合查询。

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#181024XEngineer#19。

主机测试 10/10 通过。
ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。

Refs 1024XEngineer#106
Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少
channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。

主机测试 17/17 通过。

Refs 1024XEngineer#106
格式化 voice_ports.h, voice_provider_registry.cc,
runtime.cc, voice_session.cc, linx_json_codec.cc,
linx_speech_provider.cc 以通过 CI clang-format 门禁。

Refs 1024XEngineer#108
AudioInputPort, AudioOutputPort, VoiceTransportPort,
CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter,
SpeechProviderAdapter, SpeechProviderRegistry 全部公开
类型和函数补上 /// Doxygen 注释。

通过 CI 公共 API 文档门禁。

Refs 1024XEngineer#108
AudioInputPort, AudioOutputPort, VoiceTransportPort,
SpeechProviderPort, CodecStrategy, ASRAdapter, TTSAdapter,
RealtimeAdapter 所有公开析构函数和方法补上 /// 注释。

通过 CI 公共 API 文档门禁。

Refs 1024XEngineer#108
全部 33 个公开头文件通过 check_public_api_docs.py 校验。

Refs 1024XEngineer#108
修复与 main 合并后丢失的注释:
- voice_types.h, voice_session.h, audio_frame_queue.h 补中文 Doxygen
- linx_types.h, websocket_fragment_assembler.h 恢复 Doxygen 版本
- CapabilityProfile::Has 改为 std::find, 避免公共 API 检查器误报

tests/host/CMakeLists.txt 清理重复的 linx_esp 定义。

主机测试 25/25 通过, Doxygen 33 头文件 PASS。

Refs 1024XEngineer#108
将板级 GPIO、I2C 地址、设备 PCM 格式和 DMA 预算收敛到独立 Profile,并把探针编排放回 Runtime 组装根。探针只验证 I2C ACK、I2S 通道生命周期和有限静音读写,不把 Codec 录放或云端闭环伪装成已完成。

主机契约测试 11/11、Python 测试 4/4、架构检查和 ESP-IDF 6.0.2 的 ESP32-S3 构建均通过。实板以 115200 只写非活动 ota_1,最终镜像 222320 B 回读逐字节一致;当前连接板为 SKU=voicelife-pcb 的 NoAudioCodec 纯 I2S 板,I2S smoke 通过但 ES8311/ES7210/PCA9557 未 ACK,已恢复 otadata 并确认原固件和 SQLite 数据正常。后续必须为纯 I2S 板与 Lichuang Codec 板分别建立 Profile。

Refs 1024XEngineer#109
audio_board_profile.h, esp32s3_audio_probe.h 公开 API
补齐 /** @brief @PARAM @return */ 注释。

Doxygen 35 头文件 PASS。

Refs 1024XEngineer#109
当前实板原固件报告 SKU=voicelife-pcb/NoAudioCodec,不能复用 Lichuang Codec Profile。将 AudioBoardProfile 扩展为 external-codec-duplex 与 direct-i2s-simplex,独立描述 RX/TX controller、GPIO、采样率、wire slot 和 PCM 对齐,并把探针结果接入硬件/PCM 信号失败路径。

迁移旧 voicelife-pcb-native-mvp 的 I2S0 TX + I2S1 RX 拓扑和有界回放边界;同一块板对照 >>12 与 >>14,削波从 79791 ppm 降到 208 ppm,当前 Profile 采用 >>14。主机 11/11、Python 测试、Profile validate、架构检查和 ESP-IDF 6.0.2 构建通过;最终镜像 229488 B,ota_1 回读逐字节一致,恢复 otadata 后原固件从 ota_0 启动且 SQLite 仍加载 7 events/8 reminders/0 notes。

仍未宣称 Codec、AFE、AEC、WakeNet、Opus、WSS、ASR、TTS 或声学播放闭环。

Upstream: 78/xiaozhi-esp32@dd99da0

Refs 1024XEngineer#111
audio_board_profile.h, esp32s3_audio_probe.h 公开 API
补齐 /** @brief @PARAM @return */ 注释与 Impl 说明。

Doxygen 35 头文件 PASS。

Refs 1024XEngineer#111
新增硬件 period 到传输帧的组装器、独立采集/投递/播放任务和有界队列,并将 Profile 接入 Runtime。补充主机 TDD、ESP-IDF 构建与真实 voicelife-pcb 实板回退证据;物理声学与 Linx 云端闭环继续留在后续 Issue。

Refs 1024XEngineer#113

Refs 1024XEngineer#91
- esp32s3_pcm_audio_port.cc 670 行拆分为平台无关入口
  + esp32s3_pcm_i2s_runtime.cc(I2S 通道与任务循环)
- linx_json_codec.cc 拆出 linx_json_reader.{h,cc}
- esp_websocket_transport.cc 拆出 impl 与事件处理
- 同步 host 测试 CMakeLists 补齐新源文件
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
基于新 1024XEngineer#112 重建 PR 1024XEngineer#114:
- voice_session.cc 恢复 Connect 失败 Disconnect 回滚、
  BeginCapture 双重失败转 kFailed、Stop 断开失败不伪装 kStopped
- pcm_frame_assembler.h, esp32s3_pcm_audio_port.h 补中文 Doxygen

主机测试 27/27 通过, Doxygen PASS, 代码规模 PASS。

Refs 1024XEngineer#113
@ZhaoXingPeng
ZhaoXingPeng force-pushed the dev/113-voice-audio-port branch from c111ed2 to 55e4b1c Compare August 6, 2026 09:11
基于新 1024XEngineer#112 重建 PR 1024XEngineer#114:
- voice_session.cc 恢复 Connect 失败 Disconnect 回滚、
  BeginCapture 双重失败转 kFailed、Stop 断开失败不伪装 kStopped
- pcm_frame_assembler.h, esp32s3_pcm_audio_port.h 补中文 Doxygen

主机测试 27/27 通过, Doxygen PASS, 代码规模 PASS。

Refs 1024XEngineer#113
@ZhaoXingPeng
ZhaoXingPeng force-pushed the dev/113-voice-audio-port branch from 55e4b1c to 644b9d0 Compare August 6, 2026 09:12
@ZhaoXingPeng
ZhaoXingPeng marked this pull request as ready for review August 6, 2026 12:48
@ZhaoXingPeng
ZhaoXingPeng merged commit c618b30 into 1024XEngineer:main Aug 6, 2026
12 of 13 checks passed

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 3 concrete issues in the PR diff. I also ran ./scripts/run_checks.sh; it passed locally, including 30 host CTest tests and profile validation.

Findings without inline locations

  • components/voicelife_linx/src/linx_speech_provider.cc:360: Downlink binary audio is only checked for non-empty payload before being assigned a sequence and forwarded using the negotiated playback format. That allows half frames or arbitrary byte counts to enter the playback path; for example the host contract currently accepts a 3-byte TTS payload for a negotiated 24 kHz/60 ms PCM frame, even though the expected S16LE mono frame is 2880 bytes. This can create truncated playback, sequence advancement on invalid packets, and queue pressure from malformed server data. Compute the expected byte count from audio_formats_.playback and reject anything that is not exactly one negotiated frame, or explicitly packetize/reassemble before emitting AudioFrames.

return init_status;
}
output_running_ = true;
if (i2s_channel_enable(tx_channel_) != ESP_OK) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenOutput() can be called before OpenInput() because the class exposes independent platform output() and input() ports. In that order, TryInitializeChannelsLocked() returns Ok() while input_open_ is still false, leaving tx_channel_ == nullptr; this line then calls i2s_channel_enable(tx_channel_). That makes output-only diagnostics or any caller that opens playback first fail at the ESP I2S boundary, and likely turns into an invalid-handle error or crash instead of a clear contract failure. Either reject playback-open until the required capture side is open, or initialize a TX-only channel before enabling it.


Status Esp32s3PcmAudioPorts::Impl::WriteFrame(const voice::AudioFrame& frame) {
const auto& endpoint = profile_.playback_i2s;
const std::size_t sample_count = frame.payload.size() / (sizeof(int16_t) * endpoint.format.channels);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This computes frames-per-channel, but the loop below treats sample_count and offset as indexes into the interleaved int16_t sample array. For stereo playback, a full frame has channels times more int16_t samples than this value, so only half of the PCM payload is converted/written; with 32-bit wire slots the wire buffer is also half the required size. Use total interleaved samples (payload.size() / sizeof(int16_t)) for loop bounds, and keep period_samples as the total interleaved samples per period.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sub-task 实现拆分自父任务的可验收子任务

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants