Skip to content

🎙️ feat: 建立可插拔语音会话与 Provider 契约 - #106

Merged
ZhaoXingPeng merged 8 commits into
1024XEngineer:mainfrom
ZhaoXingPeng:dev/voice-provider-architecture
Aug 6, 2026
Merged

🎙️ feat: 建立可插拔语音会话与 Provider 契约#106
ZhaoXingPeng merged 8 commits into
1024XEngineer:mainfrom
ZhaoXingPeng:dev/voice-provider-architecture

Conversation

@ZhaoXingPeng

@ZhaoXingPeng ZhaoXingPeng commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

关联

Refs #105
Refs #91

结论

建立 ESP32-S3 优先的语音子架构:实时音频数据面与会话控制面分离,Linx/xiaozhi 通过 Provider 防腐层接入。当前已完成核心会话、Provider Registry,以及 Linx 控制协议和二进制音频的主机可测试实现。

代码变化

  • 定义 AudioInputPortAudioOutputPortVoiceTransportPortCodecStrategySpeechProviderAdapter 和固定容量 SpeechProviderRegistry
  • 实现 VoiceSession 的启动回滚、采集/播报状态、generation/sequence 隔离、上下行音频格式校验、打断和幂等停止。
  • 新增 voicelife_linx:编码 hello、listen start/stop/detect、abort;解析 hello、STT、TTS、错误事件;映射二进制下行音频。
  • 同一连接打断后显式重绑定 generation,避免后续有效 TTS 被当成迟到帧丢弃。
  • README、组件依赖检查和语音子架构文档同步当前完成边界。

验证

  • VOICELIFE_HOST_BUILD_DIR=/tmp/voicelife-host-build ./scripts/run_checks.sh:8 个主机测试、架构边界、Profile 校验和 Python 测试通过。
  • 本机 ESP-IDF v6.0.2 / ESP32-S3 完整构建通过;voicelife_linx 已进入固件依赖图,固件大小 0x269c0,最小应用分区剩余 90%。
  • GitHub CI 三项通过:IM Gateway、主机测试与架构边界、ESP-IDF 6.0 / ESP32-S3。
  • 真实板只读探针确认 ESP32-S3 rev0.2、8 MB PSRAM;本 PR 未刷写 Flash。

未完成与风险

  • 尚未实现 ESP-IDF 的真实 WSS/TLS Transport、凭据引用解析、hello 超时和断线重连。
  • Opus、I2S/AFE/Wake 和真实板 ASR/TTS/打断闭环仍需后续 PR 验收。
  • LinxJsonCodec 当前是可移植的严格子集解析器;ESP Transport 接入时应以平台 JSON 库替换并复用同一组协议 fixture。
  • 需要非作者 Review,重点看 generation 重绑定、下行音频所有权、协议错误语义和 ESP32 资源开销。

@ZhaoXingPeng
ZhaoXingPeng requested a review from JunLang-7 August 3, 2026 17:22
@ZhaoXingPeng ZhaoXingPeng added the sub-task 实现拆分自父任务的可验收子任务 label Aug 3, 2026
@ZhaoXingPeng
ZhaoXingPeng force-pushed the dev/voice-provider-architecture branch from ea37d17 to 036fba3 Compare August 3, 2026 17:24

@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.

Review findings are inline. I also verified the fixed-range host build/test suite and architecture check locally: cmake -S tests/host -B /tmp/xe6-15-host-tests && cmake --build /tmp/xe6-15-host-tests && ctest --test-dir /tmp/xe6-15-host-tests --output-on-failure, plus scripts/check_architecture.sh, both passed.

return status;
}
status = provider_.Connect(config_, [this](const VoiceEvent& event) {
if (event.generation != 0 && event.generation != generation_) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

VoiceEvent defaults generation to 0, and this condition treats 0 as a wildcard, so an adapter that omits the field can still mutate the session after Interrupt() or Stop() invalidates the old generation. For example, a late default-constructed kTtsStarted event will pass this guard and line 61 moves the session back to kSpeaking, defeating the generation isolation described for stale provider events. Require provider events to carry the current generation, or otherwise gate these state transitions on the current state/session epoch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复:Provider 事件现在必须携带当前会话 generation,0 不再作为通配值;新增打断后迟到默认事件回归测试,旧 generation 不能把会话改回 speaking。


bool VoiceSession::AcceptFrame(const AudioFrame& frame) const {
return state_ == VoiceSessionState::kCapturing && frame.generation == generation_ &&
frame.format.valid() && !frame.payload.empty();

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 accepts any syntactically valid audio format, so a capturing session opened as 16 kHz PCM can still forward frames with a different codec, sample rate, channel count, bit depth, or frame duration to provider_.SendAudio(). Since VoiceSession owns the configured AudioFormat and the docs make frame format part of the session contract, this should reject frames whose format does not match the active session format before advancing next_sequence_.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复:SubmitAudio 现在逐字段比较 codec、sample_rate_hz、channels、bits_per_sample 和 frame_duration_ms;新增采样率与编码不一致的回归测试,拒绝后不会推进 sequence。

@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 complete

  • 已提交 PR review #4849818659,共 28 条 findings,覆盖 runtime 装配、会话状态机、握手、JSON codec、Registry 竞态和音频边界。
  • 本次只做了固定 base/head SHA 的静态审查,没有修改仓库代码,也没有额外跑构建。

View job run

@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.

Review findings

🔴 严重阻塞

  1. [components/voicelife_runtime/src/runtime.cc:16-45] 运行时仍然实例化 ScaffoldSpeechAdapter,没有把新增的 voicelife_linx Provider 接进启动链。风险:这次 PR 的语音架构在启动路径上是死代码,设备依然只能跑占位实现。修改方案:把 runtime 改成从 SpeechProviderRegistry/Profile 选出真实 Provider,并删除 scaffold 适配器的默认装配。
  2. [components/voicelife_voice/src/voice_session.cc:108-119] EndCapture()provider_.StopCapture() 失败时保留 kCapturing,但本地输入已经停了。风险:会话状态和真实硬件状态分裂,后续 stop/begin/capture 流程会卡死或重复操作。修改方案:把 stop 过程拆成明确的回滚状态,至少在输入已停而 Provider 停止失败时转入 kFailed 并上报清理失败。
  3. [components/voicelife_voice/src/voice_session.cc:161-179] Interrupt() 先发 Abort()/Flush(),后递增 generation。风险:旧音频和旧事件在这段窗口内仍可能进入当前会话,导致迟到帧误播或状态回退。修改方案:先原子地失效旧 generation 和会话状态,再通知 Provider 终止和刷新输出。
  4. [components/voicelife_voice/src/voice_session.cc:182-192] Stop() 先断开 Provider,再关闭输入输出,且在 generation 失效前就允许旧回调继续跑。风险:停止过程中仍可能收到旧事件并污染状态;Disconnect() 失败也会被静默吞掉。修改方案:先让当前 epoch 失效并清空回调,再关闭端口,最后把断开失败显式返回给上层。
  5. [components/voicelife_voice/src/voice_session.cc:93-104] BeginCapture() 先开远端 StartCapture(),再开本地输入;输入失败时调用 provider_.StopCapture() 但忽略其返回值。风险:远端 listen 可能残留为半开状态,麦克风已经不可用但云端仍在采集。修改方案:把本地输入启动放在可回滚事务里,失败时必须校验远端停止结果并向上层返回最坏错误。
  6. [components/voicelife_voice/src/voice_session.cc:63-76] 会话回调只处理 TTS 状态迁移,ASR 文本、错误和工具类事件都被丢弃。风险:语音层的核心语义根本传不到业务层,后续集成会出现“能说话但拿不到结果”的假完成。修改方案:为 VoiceEvent 建立明确的下游分发/观察者接口,至少把 kAsrTextkError 透出。
  7. [components/voicelife_voice/src/voice_session.cc:21-30] AcceptFrame() 只校验格式字段和非空 payload,没有检查 payload 大小是否匹配采样率/位深/声道,也没有硬上限。风险:异常大帧会把内存和音频链路拖垮,嵌入式场景下会直接触发 heap 压力或播放异常。修改方案:按格式计算最大允许字节数并在会话边界拒绝超限帧。
  8. [components/voicelife_voice/src/voice_session.cc:135-146] HandleAudio() 仅检查非空就把二进制下行帧送到 AudioOutputPort。风险:畸形或过大的下行帧会直接打到驱动层,造成播放缓冲污染或外设压力。修改方案:在会话层先做长度/格式双重校验,再允许输出端口接收。
  9. [components/voicelife_voice/src/voice_session.cc:84-86] Start()provider_.Connect() 返回后立刻把会话置为 kReady,没有等服务端 hello/ack。风险:会话被标成就绪,但协议握手可能还没成功,采集和发送会提前打开。修改方案:把启动状态保持在 kStarting,直到 Provider 明确收到服务器 hello 或超时失败。
  10. [components/voicelife_voice/include/voicelife/voice/voice_types.h:45-56; components/voicelife_voice/src/voice_session.cc:32-86] hello_timeout_ms 只是校验非零,从未真正驱动超时控制。风险:Transport/握手卡死时,Connect() 可以无限挂住,阻塞启动链。修改方案:把超时参数传入真实连接层,并在会话层对超时进行可见失败处理。
  11. [components/voicelife_voice/src/voice_provider_registry.cc:7-53] SpeechProviderRegistry 是全局单例,但注册和创建没有任何同步保护。风险:在多任务固件里并发注册/查询会直接产生竞态和脏读。修改方案:把注册表初始化收敛到单线程阶段,或给 Register/Create 加互斥保护。
  12. [components/voicelife_voice/src/voice_provider_registry.cc:22-26] 注册表固定上限只有 8 个 Provider,且没有卸载/回收策略。风险:新增一个实现或测试桩就可能把注册表打满,启动时直接失败。修改方案:改成可配置容量或显式的动态容器,并把“满表”变成可诊断错误而不是静默上限。
  13. [components/voicelife_linx/src/linx_speech_provider.cc:42-69] Connect() 只要 transport 连上并发出 hello 就直接返回成功,没有等待服务端 hello/ack。风险:Provider 会在握手未完成时被上层当成 ready,后续 listen/send 的时序全错。修改方案:增加显式 handshake 状态,只有收到对端 hello 才允许把连接视为建立完成。
  14. [components/voicelife_linx/src/linx_speech_provider.cc:134-175] 入站消息没有校验 session_id,只看当前 generation。风险:同一 socket 上的旧会话或串流错投消息会误改当前会话状态。修改方案:在 OnText() 里把 session_idconfig_.session_id 严格比对,不一致直接丢弃并上报错误。
  15. [components/voicelife_linx/src/linx_speech_provider.cc:143-153] hello 没带 audio_params 时也会被当成成功连接。风险:协商结果没有被真正确认,远端默认值和本地请求可能早已不一致。修改方案:把 audio_params 设为必填,缺失或不匹配都直接失败。
  16. [components/voicelife_linx/src/linx_speech_provider.cc:26-29; 86-94] 默认能力把 opus 写进去了,但这个 Provider 只是在转发原始帧,没有真实的 Opus 编解码策略。风险:Registry 会按虚假能力选择到这个 Provider,运行时才在音频链路上炸掉。修改方案:删掉未实现能力,或者在真正接入 CodecStrategy 后再对外声明。
  17. [components/voicelife_linx/src/linx_json_codec.cc:313-329] EncodeHello() 不管 codec 是什么都写死 sample_format:"signed_int16"。风险:Opus 和 PCM 的 hello 内容互相矛盾,服务端很容易拒绝或误配。修改方案:按 codec 分支输出字段,禁止把 PCM 专属字段塞进 Opus hello。
  18. [components/voicelife_linx/src/linx_json_codec.cc:17-73; 382-463] 这份 JSON codec 明确拒绝 \uXXXX 转义。风险:标准 JSON 编码的中文转义文本会直接解析失败,ASR/TTS 在真实服务端上很容易全部退化成错误事件。修改方案:换成真正的 JSON 解析器,或者至少补齐 Unicode escape 解码。
  19. [components/voicelife_linx/src/linx_json_codec.cc:75-99; 188-230] 字段查找是按原始字符串扫描,而不是按 JSON 结构解析。风险:重复键、嵌套对象或刻意构造的文本会让 codec 取错字段值。修改方案:改用结构化解析并拒绝重复键,别再靠字符串搜索判定字段。
  20. [components/voicelife_linx/src/linx_json_codec.cc:232-259] Quote() 只处理了少数转义字符,没有覆盖所有控制字符。风险:一旦文本里出现其他 ASCII 控制字节,输出 JSON 就是坏的。修改方案:把所有 0x00-0x1f 控制字节都做标准 JSON 转义,或者直接拒绝。
  21. [components/voicelife_linx/src/linx_json_codec.cc:313-329] frame_sizeplay_buffer_duration 是硬编码派生值,没有配置入口也没有和真实声道/位深做一致性约束。风险:缓冲参数和设备实际能力偏离时,会直接拉高时延或造成缓存不足。修改方案:把这类值做成显式配置并和音频格式联动校验。
  22. [components/voicelife_linx/src/linx_speech_provider.cc:178-195; components/voicelife_voice/src/voice_session.cc:135-146] 二进制下行音频从 transport 到 session 再到输出端口,没有任何大小上限或帧边界校验。风险:大包、脏包或攻击性 payload 会压爆嵌入式内存并拖垮播放驱动。修改方案:在 transport 和 session 两层都加最大帧长和格式一致性检查。

🟡 建议优化

  1. [components/voicelife_linx/src/linx_speech_provider.cc:18-24; 110-117] Disconnect() 之后只清空本地句柄,没对关闭失败做恢复或重试。风险:底层 WebSocket 句柄卡住时,Provider 进入“看起来已断开、实际还挂着”的假状态。修改方案:为关闭失败保留诊断信息,并在下次连接前强制清理底层状态。
  2. [components/voicelife_voice/src/voice_session.cc:149-158] Speak() 先把状态切成 kSpeaking,再调用 Provider。风险:如果 Provider 同步返回错误,状态回滚逻辑依赖后续分支,出错路径很容易被未来改动打断。修改方案:把状态迁移和发送结果封装成一个明确的事务式 helper。
  3. [components/voicelife_voice/src/voice_session.cc:32-47] Start() 只验证 provider_id,没有再核一次实际能力集合。风险:绕开 Registry 直接构造 Provider 时,缺能力对象也能进会话。修改方案:把必需能力也作为 Start() 的硬校验项。

🟢 可选改进

  1. [components/voicelife_voice/include/voicelife/voice/voice_types.h:45-56] reconnect_backoff_ms 目前没有任何调用点。风险:配置看起来支持重连,实际没有行为落地,后续排障会被误导。修改方案:要么接进真实重连策略,要么先从公共配置里删掉。
  2. [components/voicelife_linx/src/linx_json_codec.cc:405-423] DecodeText()hellotransport 只做了宽松校验。风险:协议方一旦开始发别的 transport,codec 仍可能安静放行。修改方案:如果当前实现只支持 websocket,就把它收紧成必填且唯一值。
  3. [components/voicelife_voice/src/voice_session.cc:63-76] VoiceEventKind::kToolCall 在会话回调里没有任何处理路径。风险:后续 Provider 真发工具调用时会悄悄丢事件。修改方案:在 VoiceEvent 到业务层的适配里补齐 tool-call 分发。

① 整体仓库风险总结

这次 PR 把语音抽象、LinX 防腐层和主机测试骨架都搭起来了,但真正危险的点不在“有没有类”,而在状态机、握手和 JSON codec 三个地方。当前实现还没达到可上板的闭环标准:启动路径仍然是占位实现,握手没有被严格门控,音频帧边界也没有被硬限制。

② 优先级整改清单

先修 1、2、3、4、9、10、13、14、16、17、18、22 这些阻塞项,再处理 5、6、7、8、11、12、15、19、20、21。最后补 23-28 这种维护性和可扩展性问题。

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

语音层要继续按“会话控制面 / 实时数据面 / Provider 防腐层”分开,但下一步必须把真实 Transport、CodecStrategy、超时、重连、最大帧长和设备资源预算都落成可验证契约。硬件侧要把 I2S/AFE、PSRAM、WSS/TLS、Opus/PCM 和打断后的 generation 失效一起做端到端验证,不要让协议语义只停留在主机测试。

保留语音 Provider Port 的新接口位置与主干公共 API 文档规则,避免协调器重复定义 SpeechProviderPort。\n\nRefs 1024XEngineer#106\nRefs 1024XEngineer#105\nRefs 1024XEngineer#91
@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!

统一 clang-format-18 输出并满足中文 Doxygen 质量门禁,不改变 Provider 行为。
@ZhaoXingPeng
ZhaoXingPeng force-pushed the dev/voice-provider-architecture branch from 3cd8ba7 to b37b9da Compare August 4, 2026 06:45
- 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径
- 覆盖 JSON 转义、\u 拒绝、opus/wav 音频参数解析边界
- 覆盖 Registry 空 ID、重复注册、未找到、缺能力与工厂空实现
语音模块仍处于实板验证阶段,host 覆盖率无法覆盖
ESP32 专属代码与头文件声明行,先忽略以保持门禁可执行
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB
sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB
新增 partitions_voicelife_16mb.csv:
  nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB

实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时
spi_flash 警告并浪费 14MB 空间。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为
新 VoiceSession + SpeechProviderRegistry 架构:

- 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider
  实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter)
- Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry
- Start() 从 Registry 创建 Provider 并构造 VoiceSession
- 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#1: 之前新语音架构在启动路径
上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。
现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。

主机测试 10/10 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

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

主机测试 10/10 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。
之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。

帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。
AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload,
防止畸形帧或攻击性 payload 压爆嵌入式内存。

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#71024XEngineer#81024XEngineer#161024XEngineer#22。

主机测试 10/10 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值,
回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。

Linx OnText: 解码后校验 session_id 与当前会话匹配,
拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。

EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出,
不再把 PCM 专用字段硬编码进 Opus hello。

Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一
输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#51024XEngineer#141024XEngineer#171024XEngineer#20。

主机测试 10/10 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
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
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
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
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
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
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少
channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。

主机测试 17/17 通过。

Refs 1024XEngineer#106
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

@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

@ZhaoXingPeng
ZhaoXingPeng merged commit d09b74c into 1024XEngineer:main Aug 6, 2026
13 checks passed
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB
sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB
新增 partitions_voicelife_16mb.csv:
  nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB

实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时
spi_flash 警告并浪费 14MB 空间。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为
新 VoiceSession + SpeechProviderRegistry 架构:

- 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider
  实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter)
- Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry
- Start() 从 Registry 创建 Provider 并构造 VoiceSession
- 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#1: 之前新语音架构在启动路径
上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。
现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。

主机测试 10/10 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

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

主机测试 10/10 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。
之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。

帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。
AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload,
防止畸形帧或攻击性 payload 压爆嵌入式内存。

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#71024XEngineer#81024XEngineer#161024XEngineer#22。

主机测试 10/10 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值,
回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。

Linx OnText: 解码后校验 session_id 与当前会话匹配,
拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。

EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出,
不再把 PCM 专用字段硬编码进 Opus hello。

Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一
输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#51024XEngineer#141024XEngineer#171024XEngineer#20。

主机测试 10/10 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
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
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
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
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
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
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少
channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。

主机测试 17/17 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit that referenced this pull request Aug 6, 2026
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架

建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。

* 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次

* ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证

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 #107

Refs #91

* 🐛 fix(test): 显式初始化语音事件测试夹具

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

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

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

Refs #107

Refs #91

* ✨ feat(voice): 固化有界音频队列与迁移边界

把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91

* ✨ feat(voice): 接入会话采集音频回调契约

参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。

Refs #107
Refs #105
Refs #91

* 📝 docs(voice): 刷新官方接入与跨板能力边界

记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91

* 🐛 fix(docs): 移除失效的 Zephyr 音频来源

跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91

* 👷 ci(quality): 统一 Linx ESP 传输格式

补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。

* ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试

- 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径
- 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界
- 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径

* 📝 docs(process): 将研究资料迁移到 Issue 归档

* 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表

sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB
sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB
新增 partitions_voicelife_16mb.csv:
  nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB

实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时
spi_flash 警告并浪费 14MB 空间。

Refs #106

* ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动

将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为
新 VoiceSession + SpeechProviderRegistry 架构:

- 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider
  实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter)
- Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry
- Start() 从 Registry 创建 Provider 并构造 VoiceSession
- 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold

这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径
上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。
现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷

Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

这是 fennoai 审查 #106 标记的阻塞项 #2#3。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限

能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。
之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。

帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。
AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload,
防止畸形帧或攻击性 payload 压爆嵌入式内存。

这是 fennoai 审查 #106 标记的阻塞项 #7#8#16#22。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷

BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值,
回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。

Linx OnText: 解码后校验 session_id 与当前会话匹配,
拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。

EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出,
不再把 PCM 专用字段硬编码进 Opus hello。

Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一
输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。

这是 fennoai 审查 #106 标记的阻塞项 #5#14#17#20。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长

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 审查 #106 标记的阻塞项 #12#15#21。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界

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

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

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

这是 fennoai 审查 #106 标记的阻塞项 #11#18#19(部分缓解)。

主机测试 10/10 通过。

Refs #106

* ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析

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 审查 #106 标记的阻塞项 #18#19。

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

Refs #106

* 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验

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

主机测试 17/17 通过。

Refs #106

* 🔧 build(style): clang-format 修复 CI 格式检查

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

Refs #108

* 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释

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

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释

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

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen

* 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式

全部 33 个公开头文件通过 check_public_api_docs.py 校验。

Refs #108

* 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists

修复与 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 #108

* 🔧 build(style): clang-format 修复 voice_session_contract_test.cc

Refs #108
ZhaoXingPeng added a commit that referenced this pull request Aug 6, 2026
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架

建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。

* 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次

* ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证

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 #107

Refs #91

* 🐛 fix(test): 显式初始化语音事件测试夹具

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

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

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

Refs #107

Refs #91

* ✨ feat(voice): 固化有界音频队列与迁移边界

把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91

* ✨ feat(voice): 接入会话采集音频回调契约

参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。

Refs #107
Refs #105
Refs #91

* 📝 docs(voice): 刷新官方接入与跨板能力边界

记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91

* 🐛 fix(docs): 移除失效的 Zephyr 音频来源

跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91

* 👷 ci(quality): 统一 Linx ESP 传输格式

补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。

* ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试

- 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径
- 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界
- 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径

* 📝 docs(process): 将研究资料迁移到 Issue 归档

* 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表

sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB
sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB
新增 partitions_voicelife_16mb.csv:
  nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB

实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时
spi_flash 警告并浪费 14MB 空间。

Refs #106

* ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动

将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为
新 VoiceSession + SpeechProviderRegistry 架构:

- 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider
  实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter)
- Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry
- Start() 从 Registry 创建 Provider 并构造 VoiceSession
- 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold

这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径
上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。
现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷

Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

这是 fennoai 审查 #106 标记的阻塞项 #2#3。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限

能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。
之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。

帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。
AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload,
防止畸形帧或攻击性 payload 压爆嵌入式内存。

这是 fennoai 审查 #106 标记的阻塞项 #7#8#16#22。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷

BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值,
回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。

Linx OnText: 解码后校验 session_id 与当前会话匹配,
拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。

EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出,
不再把 PCM 专用字段硬编码进 Opus hello。

Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一
输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。

这是 fennoai 审查 #106 标记的阻塞项 #5#14#17#20。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长

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 审查 #106 标记的阻塞项 #12#15#21。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界

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

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

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

这是 fennoai 审查 #106 标记的阻塞项 #11#18#19(部分缓解)。

主机测试 10/10 通过。

Refs #106

* ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析

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 审查 #106 标记的阻塞项 #18#19。

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

Refs #106

* 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验

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

主机测试 17/17 通过。

Refs #106

* 🔧 build(style): clang-format 修复 CI 格式检查

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

Refs #108

* 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释

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

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释

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

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen

* 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式

全部 33 个公开头文件通过 check_public_api_docs.py 校验。

Refs #108

* 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists

修复与 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 #108

* ✨ feat(voice): 建立 ESP32-S3 PCM/I2S 板级探针

将板级 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 #109

* 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释

audio_board_profile.h, esp32s3_audio_probe.h 公开 API
补齐 /** @brief @PARAM @return */ 注释。

Doxygen 35 头文件 PASS。

Refs #109

* 🏗️ refactor(voice): 接入 voicelife-pcb 纯 I2S PCM Profile

当前实板原固件报告 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 #111

* 📝 docs(audio): 补 esp32s3_audio_probe.h Impl Doxygen

Refs #111

* 🐛 fix(audio): 同步 audio_esp 源文件与新版头文件字段

audio_board_profile.cc, esp32s3_audio_probe.cc 使用 capture_i2s/
playback_i2s 字段; audio_board_profile.h, esp32s3_audio_probe.h
补齐中文 Doxygen 注释。

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

Refs #109

* 🔧 build(style): clang-format 修复 audio_esp 与测试文件

runtime.cc, audio_board_profile_contract_test.cc,
voice_session_contract_test.cc 等通过 clang-format-18。

Refs #109
ZhaoXingPeng added a commit that referenced this pull request Aug 6, 2026
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架

建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。

* 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次

* ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证

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 #107

Refs #91

* 🐛 fix(test): 显式初始化语音事件测试夹具

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

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

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

Refs #107

Refs #91

* ✨ feat(voice): 固化有界音频队列与迁移边界

把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91

* ✨ feat(voice): 接入会话采集音频回调契约

参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。

Refs #107
Refs #105
Refs #91

* 📝 docs(voice): 刷新官方接入与跨板能力边界

记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91

* 🐛 fix(docs): 移除失效的 Zephyr 音频来源

跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91

* 👷 ci(quality): 统一 Linx ESP 传输格式

补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。

* ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试

- 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径
- 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界
- 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径

* 📝 docs(process): 将研究资料迁移到 Issue 归档

* 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表

sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB
sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB
新增 partitions_voicelife_16mb.csv:
  nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB

实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时
spi_flash 警告并浪费 14MB 空间。

Refs #106

* ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动

将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为
新 VoiceSession + SpeechProviderRegistry 架构:

- 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider
  实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter)
- Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry
- Start() 从 Registry 创建 Provider 并构造 VoiceSession
- 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold

这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径
上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。
现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷

Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

这是 fennoai 审查 #106 标记的阻塞项 #2#3。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限

能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。
之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。

帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。
AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload,
防止畸形帧或攻击性 payload 压爆嵌入式内存。

这是 fennoai 审查 #106 标记的阻塞项 #7#8#16#22。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷

BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值,
回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。

Linx OnText: 解码后校验 session_id 与当前会话匹配,
拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。

EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出,
不再把 PCM 专用字段硬编码进 Opus hello。

Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一
输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。

这是 fennoai 审查 #106 标记的阻塞项 #5#14#17#20。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长

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 审查 #106 标记的阻塞项 #12#15#21。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界

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

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

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

这是 fennoai 审查 #106 标记的阻塞项 #11#18#19(部分缓解)。

主机测试 10/10 通过。

Refs #106

* ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析

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 审查 #106 标记的阻塞项 #18#19。

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

Refs #106

* 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验

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

主机测试 17/17 通过。

Refs #106

* 🔧 build(style): clang-format 修复 CI 格式检查

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

Refs #108

* 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释

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

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释

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

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen

* 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式

全部 33 个公开头文件通过 check_public_api_docs.py 校验。

Refs #108

* 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists

修复与 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 #108

* ✨ feat(voice): 建立 ESP32-S3 PCM/I2S 板级探针

将板级 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 #109

* 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释

audio_board_profile.h, esp32s3_audio_probe.h 公开 API
补齐 /** @brief @PARAM @return */ 注释。

Doxygen 35 头文件 PASS。

Refs #109

* 🏗️ refactor(voice): 接入 voicelife-pcb 纯 I2S PCM Profile

当前实板原固件报告 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 #111

* 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释

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

Doxygen 35 头文件 PASS。

Refs #111

* 🔧 build(style): clang-format 修复 audio_esp 与测试文件

Refs #111
ZhaoXingPeng added a commit that referenced this pull request Aug 6, 2026
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架

建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。

* 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次

* ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证

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 #107

Refs #91

* 🐛 fix(test): 显式初始化语音事件测试夹具

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

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

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

Refs #107

Refs #91

* ✨ feat(voice): 固化有界音频队列与迁移边界

把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91

* ✨ feat(voice): 接入会话采集音频回调契约

参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。

Refs #107
Refs #105
Refs #91

* 📝 docs(voice): 刷新官方接入与跨板能力边界

记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91

* 🐛 fix(docs): 移除失效的 Zephyr 音频来源

跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91

* 👷 ci(quality): 统一 Linx ESP 传输格式

补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。

* ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试

- 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径
- 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界
- 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径

* 📝 docs(process): 将研究资料迁移到 Issue 归档

* 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表

sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB
sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB
新增 partitions_voicelife_16mb.csv:
  nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB

实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时
spi_flash 警告并浪费 14MB 空间。

Refs #106

* ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动

将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为
新 VoiceSession + SpeechProviderRegistry 架构:

- 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider
  实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter)
- Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry
- Start() 从 Registry 创建 Provider 并构造 VoiceSession
- 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold

这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径
上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。
现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷

Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

这是 fennoai 审查 #106 标记的阻塞项 #2#3。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限

能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。
之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。

帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。
AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload,
防止畸形帧或攻击性 payload 压爆嵌入式内存。

这是 fennoai 审查 #106 标记的阻塞项 #7#8#16#22。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷

BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值,
回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。

Linx OnText: 解码后校验 session_id 与当前会话匹配,
拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。

EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出,
不再把 PCM 专用字段硬编码进 Opus hello。

Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一
输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。

这是 fennoai 审查 #106 标记的阻塞项 #5#14#17#20。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长

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 审查 #106 标记的阻塞项 #12#15#21。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界

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

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

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

这是 fennoai 审查 #106 标记的阻塞项 #11#18#19(部分缓解)。

主机测试 10/10 通过。

Refs #106

* ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析

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 审查 #106 标记的阻塞项 #18#19。

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

Refs #106

* 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验

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

主机测试 17/17 通过。

Refs #106

* 🔧 build(style): clang-format 修复 CI 格式检查

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

Refs #108

* 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释

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

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释

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

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen

* 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式

全部 33 个公开头文件通过 check_public_api_docs.py 校验。

Refs #108

* 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists

修复与 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 #108

* ✨ feat(voice): 建立 ESP32-S3 PCM/I2S 板级探针

将板级 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 #109

* 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释

audio_board_profile.h, esp32s3_audio_probe.h 公开 API
补齐 /** @brief @PARAM @return */ 注释。

Doxygen 35 头文件 PASS。

Refs #109

* 🏗️ refactor(voice): 接入 voicelife-pcb 纯 I2S PCM Profile

当前实板原固件报告 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 #111

* 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释

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

Doxygen 35 头文件 PASS。

Refs #111

* ✨ feat(voice): 接入 ESP32-S3 PCM Audio Port

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

Refs #113

Refs #91

* 🐛 fix(voice): 收紧 Linx 传输生命周期与会话失败回滚

* 🏗️ refactor(voice): 拆分超大语音源文件以通过规模门禁

- 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 补齐新源文件

* 🐛 fix(voice): 恢复 Connect/BeginCapture/Stop 失败回滚并补 Doxygen

基于新 #112 重建 PR #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 #113

* 🔧 build(style): clang-format 修复 audio_esp 与测试文件

Refs #113

* 🔧 chore(build): 移除误提交的 ESP-IDF 构建产物

* 🔧 fix(timing): 链接合并后的主机服务实现
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