Skip to content

🐛 fix(voice): 增强 SparkBot 唤醒恢复与 Linx 诊断 - #349

Open
ZhaoXingPeng wants to merge 20 commits into
mainfrom
fix/sparkbot-wake-recovery-20260822
Open

🐛 fix(voice): 增强 SparkBot 唤醒恢复与 Linx 诊断#349
ZhaoXingPeng wants to merge 20 commits into
mainfrom
fix/sparkbot-wake-recovery-20260822

Conversation

@ZhaoXingPeng

@ZhaoXingPeng ZhaoXingPeng commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

结论:本 PR 在固定百炼测试入口的基础上,提交 SparkBot 语音唤醒恢复、Linx 传输边界和重连诊断修复。当前主机侧和提交前门禁已通过;真实 SparkBot 的短链路证据已固定,但 Linx 在长播报期间的 RST 仍未闭合,因此本 PR 不宣称完整多轮验收通过。

Fixes #348

Lifecycle: Ready for review; merge after required checks and human approval.

Scope

  • 固化本地唤醒后的协议顺序:先发送 listen.detect,再发送 listen.start,问候 TTS 结束或有界超时后才打开物理麦克风。
  • 在唤醒切换边界清除旧 PCM,并丢弃迟到帧,避免唤醒词或问候音频混入新一轮上行。
  • 为 VoiceSession 增加 Provider capture、唤醒确认租约、代次隔离和旧事件证据,避免旧回合改变当前屏幕/会话状态。
  • 为 Linx WebSocket 增加控制/媒体队列边界、音频 generation/sequence gap 诊断、RST 归类、hello/收发明文状态日志和重连清理。
  • 修正 Linx 架构门禁,使 pthread 私有依赖与实际 CMake 配置一致。
  • 更新百炼 SparkBot 唤醒/多轮脚本和固定测试模板;证据统一保存在 /tmp/voicelife-bailian-tests

Verification

  • ./scripts/check_format.sh:45 个文件通过。
  • ./scripts/run_checks.sh:公共 API、主机测试、架构边界、双端契约、Profile 校验通过;Python 188 项通过(跳过 1 项)。
  • ./scripts/run_host_tests.sh:CTest 83/83 通过。
  • 百炼 TTS preflight 实测通过:qwen-audio-3.0-tts-flash + longanlingxi,生成音频成功,未将 Key 写入证据。
  • SparkBot 实板唤醒证据覆盖 standby_ready -> wake_detected -> local_wake_ack_requested -> tts_started -> tts_first_audio -> capture_started,屏幕状态与语音阶段同步。
  • 短多轮测试能生成完整 JSON/串口证据,并能记录 Linx RST,而不是把异常判成通过。
  • Key 负例和敏感信息扫描通过,证据目录未发现 sk-AuthorizationBearer

Known limits

  • Linx 在长播报/多轮过程中仍可能出现 Connection reset by peer;这是当前剩余阻断项,需后续在实板继续定位,不能以本 PR 的 Host 通过替代。
  • IM readiness 本次本地检查的最新终端信号仍为 degraded,公众号通知闭环未在本 PR 中宣称完成。
  • 日程持久化修复已有硬复位后查询仍存在的实板证据;当前分支未重新宣称完整 CRUD 压力验收。

AI assistance

Codex 协助整理实现、测试脚本和证据记录;作者已核对代码差异、主机测试和真实 SparkBot/百炼日志。

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

Reviewed the new Bailian wrapper, wake-injection harness, and the documented evidence gates. The key parsing and wrapper syntax are sound, but the wake workflow has two correctness gaps that can either report a false pass or make the documented rerun/reconnect flow fail.

Validation performed: bash -n scripts/run_bailian_sparkbot_test.sh, Python AST parsing, invalid-key exit contract, and git diff --check.

Additional findings

  • scripts/voice_linx_wake_injection_test.py:?: [P1] Do not wait for a one-shot boot marker on every wake run: SERIAL_VOICE_TEST_READY=1 is emitted once when the firmware serial task starts, before this process opens the port; it is not re-emitted on each connection or while the board is already running. With the default wake command (which does not pass --reset-before-run), a second invocation or a reconnect therefore waits for a marker that has already been consumed and times out before injecting any audio. The documented rerun/reconnect workflow needs a readiness check that works after attach, or the wrapper must make/reset-and-wait an explicit prerequisite rather than relying on this one-shot log line.

Comment on lines +111 to +112
cursor = wait_for(log, "SERIAL_VOICE_EVIDENCE event=tts_started ", cursor, args.timeout)
cursor = wait_for(log, "SERIAL_VOICE_EVIDENCE event=capture_started ", cursor, args.timeout)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Require first TTS audio before declaring wake success

This success gate skips tts_first_audio even though the template requires it. The runtime intentionally opens capture after ACK_FIRST_AUDIO_TIMEOUT when the acknowledgment has started but no first PCM arrived, and that path still emits capture_started; therefore a provider/audio-output failure can satisfy the two waits below and print wake_injection_success. Wait for SERIAL_VOICE_EVIDENCE event=tts_first_audio (and reject provider/abort errors) before accepting capture_started so this harness cannot report a false wake pass.

@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

百炼注入与唤醒重跑验证(2026-08-22)

本阶段提交:7ae4f5a 🐛 fix(test): 提升百炼 SparkBot 唤醒注入重跑稳定性

注入方式核验

  • 百炼预检:qwen-audio-3.0-tts-flash + longanlingxi,真实 SDK 调用成功,successful=1failed=0、音频 58187 bytes。
  • 独立格式核验:SDK 返回单声道 MP3;ffmpeg 强制转换为 16 kHz / mono / S16LE,2.586 秒音频得到 130 个严格 640-byte PCM 帧。
  • 串口协议核验:每帧为 VLVT-v1kind=PCM、little-endian length=640;固件再次校验格式后才进入现有有界输入队列。注入路径不直接伪造或调用 Linx WebSocket 帧。
  • 主机检查:四项 C++ 契约测试、Python 编译、Shell 语法、git diff --check 均通过。

SparkBot 实板结果

命令:scripts/run_bailian_sparkbot_test.sh wake --timeout 45

  • 日志:/tmp/voicelife-bailian-tests/wake-20260822-163708.log
  • SERIAL_VOICE_TEST_READY=1 后只出现一次待机拒绝 code=4,等待 standby_ready 后一次握手成功 SERIAL_VOICE_WAKE_BEGIN=ok;不再在启动期堆积重复握手帧。
  • test_in_frames=58test_in_bytes=37120SERIAL_VOICE_PCM=reject 无,in_drop=0out_reject=0short_write=0,I2S 错误为 0。
  • WAKE_DETECTED word=你好牛牛tts_startedtts_first_audiocapture_started 全部出现;日志中没有 Connection reset by peerprovider_error

结论与边界

百炼注入格式和注入路径已证明不会制造 Linx 非法音频帧;本阶段此前多轮测试中的 Linx RST 仍需按 WebSocket 生命周期/TX 时序单独处理,不能把本次唤醒通过扩大为多轮链路通过。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

阶段进度更新(2026-08-22,先暂停 CI,继续实板链路)

本阶段已提交并推送:156d4e7 🐛 fix(linx): 完善 SparkBot 传输边界与重连诊断

已完成

  • 百炼固定入口预检通过:qwen-audio-3.0-tts-flash + longanlingxi,真实 TTS 成功,successful=1failed=0、音频 58187 bytes。
  • SparkBot 实板唤醒通过:standby_ready -> WAKE_DETECTED(你好牛牛) -> local_wake_ack_requested -> tts_started -> tts_first_audio -> capture_started
  • 唤醒注入严格转换为 16 kHz、单声道、S16LE、20 ms、640-byte PCM;SERIAL_VOICE_PCM=rejectin_dropout_rejectshort_write、I2S 错误均为 0。
  • 固件补充了 Linx hello 参数、TX 文本/音频顺序、MCP method/id、WebSocket error 分类和重连日志;listen.start/stop 与 PCM 共用有序媒体队列,断线后清理旧代际队列。
  • 主机验证通过:4 项 C++ 契约测试、Python 编译、Shell 语法、git diff --check

当前实板普通对话结果

测试日志:/tmp/voicelife-bailian-tests/multiturn-20260822-165203.log
结果:/tmp/voicelife-bailian-tests/multiturn-20260822-165203.json

  • ASR 精确匹配输入:请简单介绍一下你自己。
  • 已收到两段 TTS 文本并开始播放,屏幕状态依次覆盖“聆听中/处理中/说话中”,字幕滚动指标正常。
  • 播放第二段 TTS 约 2 秒时,Linx 对端 TCP RST:esp_tls_conn_read ... Connection reset by peer;随后设备自动重连并再次完成 hello。
  • 该轮被测试门禁正确判定为失败(缺少 tts_stopped),没有把“播出部分内容”误报为完整对话通过。
  • 这次失败不符合百炼 PCM 注入错误特征,当前证据仍指向 Linx WebSocket 会话生命周期/服务端策略或协议时序,需要独立对照实验确认。

下一步对照实验

将使用官方 78/xiaozhi-esp32 的 SparkBot 实现,按 Linx 的 xiaozhi-firmware/xiaozhi-hardware 文档配置同一网络和凭据,再用同一套百炼 TTS 自动注入与串口证据检查:

  1. 官方固件唤醒与一句普通对话;
  2. 官方固件多段 TTS/多轮上下文;
  3. 对比 WebSocket hello、listen.start/PCM/listen.stop、RST 时间点;
  4. 若官方固件同样 RST,优先归因 Linx 服务/服务端会话策略;若官方固件稳定,再回到 VoiceLife TX 队列和生命周期实现;
  5. 对照结果、完整日志路径和结论继续追加到本 PR。

原始串口日志只保存在本机 /tmp/voicelife-bailian-tests,不提交密钥、Authorization 或私密音频。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

官方 SparkBot 对照实验阶段进度(2026-08-22)

本阶段目标是用 78/xiaozhi-esp32 官方 SparkBot 固件连接同一 Linx OTA/WebSocket 服务,再用固定百炼 TTS 注入模板对比 VoiceLife 的链路行为。

已完成

  • 克隆官方仓库并固定 commit:bb9122ab08c3083eeb4f67b3974b7afe771723b8
  • 确认官方 SparkBot 硬件配置与实板一致:ESP32-S3、ES8311、16 kHz 单声道音频、同一 GPIO 配置。
  • 临时构建配置使用 Linx OTA:https://xrobo.qiniuapi.com/v1/ota/;官方固件构建成功,生成 xiaozhi.bin(2.8 MiB)和资源镜像。
  • 刷写前完整读取实板 16 MiB Flash,备份:/tmp/voicelife-sparkbot-backup-20260822/flash-16mb.bin
  • 跳过 0x9000 NVS,仅刷入官方启动程序、分区表、OTA 数据、应用和资源;官方固件已在 SparkBot 实板启动。
  • 官方启动日志:/tmp/voicelife-official-boot-20260822.log。硬件初始化、LCD、ES8311、I2S 和本地唤醒模型均正常。

当前阻塞与证据

官方固件启动后明确打印 SsidManager: NVS namespace wifi doesn't exist,随后进入 Xiaozhi-81B1 配网模式。原因是 VoiceLife 使用加密的独立 linx_secrets 分区保存 Wi-Fi/Linx 凭据,而官方固件使用未加密默认 NVS;保留原 NVS 不能跨固件复用凭据,不是 Linx WebSocket 失败。

因此目前还不能把“官方固件未连接 Linx”误判为服务端问题。下一步需要在官方配网页面录入同一 Wi-Fi 和 Linx 凭据,然后用相同百炼 preflight/wake/multiturn 模板完成正式对照。

风险控制

  • 原 VoiceLife 整片 Flash 已备份,可恢复;官方临时固件和构建目录均位于 /tmp,没有混入仓库。
  • 未提交 Wi-Fi 密码、Linx token、Authorization、原始语音或完整敏感日志。
  • 本阶段没有新增仓库代码或 commit;现有分支仍保持干净,CI 按当前要求暂缓。

Refs #348

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

实板对照测试进度(2026-08-22)

对照基线

  • 固件:78/xiaozhi-esp32,commit bb9122ab08c3083eeb4f67b3974b7afe771723b8
  • 板卡:SparkBot(esp-sparkbot);当前唤醒词为“你好小智”。
  • Linx:已成功连接 xrobo.qiniuapi.com:443,OTA 检查通过,设备进入 activating -> idle
  • Wi-Fi:已连接 zxp,地址 10.42.159.73
  • 串口明文日志:持续记录至 /tmp/voicelife-human-serial-monitor-20260822.log

与 VoiceLife 症状的对照

VoiceLife 之前表现为首轮可以唤醒并开始回复,后续 TTS 阶段出现 esp_tls_conn_read ... Connection reset by peer。官方干净固件目前已稳定待机,尚未出现连接重置、看门狗或异常重启;真人语音对照仍待执行。

真人测试用例(严格顺序)

  1. 你好小智
  2. 你能听见我吗?
  3. 我叫新鹏,请记住这个名字。
  4. 我刚才叫什么名字?
  5. 请用两句话介绍一下你能做什么。
  6. 在第 5 步播报期间说:你好小智,停一下。
  7. 回到聆听后说:今天天气怎么样?
  8. 再见。

判定标准

检查唤醒事件、WebSocket 建连、ASR/TTS 明文、打断后状态回收;全程不应出现 Connection reset by peeresp_tls_conn_readGuru Meditationtask_wdt 或异常复位。

本阶段只做官方固件对照测试,没有新增 VoiceLife 源码,因此没有额外 commit;测试完成后会在此评论补充逐步结果。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

真人实板对照结果(2026-08-22)

已通过

  • 唤醒词“你好小智”识别成功,Linx WebSocket 握手成功并建立 Session。
  • 普通对话、连续上下文记忆、长句 TTS、结束语均完成;设备在每轮正确回到 listening
  • ASR/TTS 明文持续可见,Wi-Fi 与连接稳定。
  • 全程未出现 Connection reset by peeresp_tls_conn_readGuru Meditationtask_wdt 或异常复位。

未完成项

  • 播报中打断尚未形成有效证据:两次“你好小智,停一下”均发生在设备已回到 listening 后;其中一次完整 ASR 已识别,但不是 barge-in。
  • 需要在设备仍处于 speaking 时立即插话,确认是否提前停止播放并回到 listening

日程边界

官方 78/xiaozhi-esp32 对照固件注册的是底盘、屏幕、相机等 MCP 工具,没有 VoiceLife 日程持久化工具;本轮“新增日程”仅验证了服务端自然语言引导,不能替代 VoiceLife 日程增删改查与持久化验收。

串口完整记录:/tmp/voicelife-human-serial-monitor-20260822.log。本轮没有新增 VoiceLife 源码或 commit。

@ZhaoXingPeng ZhaoXingPeng changed the title ✅ test(voice): 固化百炼 SparkBot 实板测试模板 🐛 fix(voice): 增强 SparkBot 唤醒恢复与 Linx 诊断 Aug 23, 2026
@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

阶段性进度:SparkBot 唤醒恢复与 Linx 传输诊断

本阶段已提交并推送:

这次不是只加日志,核心是把“本地唤醒、Linx 会话、物理麦克风、TTS 播放、重连恢复”之间的边界重新固定下来,并把失败时能回答“哪一帧、哪一代、哪一个协议阶段出错”的证据补齐。

1. 语音会话和唤醒顺序

修改了 VoiceSessionVoiceInteractionControllerWakeGateAudioInput

  • 本地识别到“你好牛牛”后,先进入 acknowledging,不再立即打开物理麦克风。
  • Provider 顺序固定为 listen.detect -> listen.start
  • 问候 TTS 播放完成,或在有界超时后,才打开真实麦克风。
  • 新增 BeginProviderCapture(),用于只发送 Provider 侧 listen.start,避免重复打开 I2S。
  • 唤醒切换时清空物理输入端的待处理 PCM,并丢弃边界窗口内的迟到帧,防止唤醒词、问候音或旧回合数据混入新一轮上行。
  • 每一轮会话使用独立 generation/sequence;重连、打断、停止和新唤醒都会推进代次,旧回合事件和旧 PCM 不能再修改当前状态。
  • 服务端回传本地唤醒词时只做一次 echo 抑制,避免把“你好牛牛”误当成用户的新问题。
  • TTS 的 started、首个下行 PCM、stopped 和物理播放排空分别记录,屏幕的“收到/说话中/聆听中”只在对应的真实事件确认后切换。

2. Linx WebSocket 和 PCM 传输

修改了 Linx Provider、ESP WebSocket Transport、JSON codec 和运行时装配:

  • listen.stop 不再携带文档未定义的 mode 字段,保持和参考 SparkBot 客户端一致。
  • listen.start/stop 与 PCM 共用有序媒体 FIFO,控制消息仍走控制 lane,避免文本和音频跨队列重排。
  • TX item 携带 generation/sequence;发送前做 generation gate,断线或重连后不会把旧连接的队列内容写入新连接。
  • 记录每轮 PCM 的序号,出现跳帧时输出 LINX_TX_AUDIO_GAP,并区分新一轮的 sequence=0 和上一轮的缺帧。
  • WebSocket 的 ERROR_TYPE_NONE、TLS read failure、TCP FIN/RST 现在统一归类到可恢复 disconnect,保留原始 error type、close code、TLS/errno 字段。
  • hello 请求/响应、收到的 Linx 消息类型、TTS 状态、session 长度和音频参数均有明文诊断,但不会打印 token。
  • 重连时清理旧的 TX 队列和 session 状态,重新 hello 后建立新的 generation。
  • 架构门禁同步声明 voicelife_linx 的实际 pthread 私有依赖,避免代码和依赖图不一致。

3. 百炼和实板验证入口

更新了百炼串口注入脚本、唤醒脚本、多轮脚本和固定测试模板:

  • Key 只读取配置文件中完整的 sk-... 行,不再把整个 key.txt 误当作 API Key。
  • 测试先做真实 TTS preflight,再执行唤醒或多轮;失败会根据串口 evidence 判定,不会把“脚本发出了音频”当作链路通过。
  • 固定等待 standby_readywake_detectedlocal_wake_ack_requestedtts_startedtts_first_audiocapture_started 等 marker。
  • JSON、元数据和串口日志写入 /tmp/voicelife-bailian-tests;本阶段没有把 Key、Authorization、Bearer 或原始敏感配置提交到仓库。

4. 已取得的结果

实板日志已经能稳定留下完整的唤醒阶段证据。例如:

  • /tmp/voicelife-bailian-tests/wake-20260823-065806.log
  • /tmp/voicelife-bailian-tests/wake-followup-1000.log

其中可以看到:

standby_ready -> wake_detected -> local_wake_ack_requested -> provider_capture_started -> tts_started -> tts_first_audio -> capture_started

唤醒确认 TTS 的首阶段延迟也有记录,示例约为 411ms/431ms;屏幕状态跟随这些事件切换,没有再把“收到”或“聆听中”提前显示。

主机和提交前检查结果:

  • CTest:83/83 通过。
  • C/C++ 与 Python 格式检查:45 个文件通过。
  • 公共 API 文档检查:98 个头文件通过。
  • 架构依赖、双端契约和全部 Firmware Profile 校验通过。
  • Python unittest:188 项通过,1 项按环境跳过。
  • 百炼 TTS preflight:qwen-audio-3.0-tts-flash + longanlingxi 成功。
  • Key 负例和敏感字段扫描通过。

5. 目前仍未闭合的问题

这次没有把失败伪装成成功,剩余问题仍明确保留:

  • 在短多轮/长播报期间,Linx 仍可能出现 Connection reset by peer。日志能确认设备收到 RST,进入 disconnect 恢复,generation 从 1 推进到 2,并重新完成 hello;但“长对话不被 RST 打断”尚未通过。
  • 本地 IM readiness 检查最新终端信号仍是 degraded,因此公众号通知闭环暂不宣称完成。
  • 日程 FATFS/SQLite 的硬复位持久化已有实板证据,但本阶段没有把完整 CRUD 压力验收重新标记为通过。
  • 当前改动证明了本地协议顺序、迟到 PCM、旧代次和 RST 诊断链路已经可测、可复现;下一阶段应继续针对 Linx RST 的服务端/音频模式/发送节奏做对照实验。

本评论记录的是当前阶段真实结果:本地语音状态和重连诊断已明显可观测、主机门禁已通过,但完整长对话和 IM 闭环仍需要后续实板验证。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

CI 补充:修复严格编译器下的测试 fixture 初始化

推送后的第一轮 CI 中,主机测试、Codecov 和 CodeQL C++ 同时在同一处失败:

tests/host/linx_provider_contract_test.cc:104

原因是 LinxConnectionConfig 新增了 preferred_audio 聚合字段,但测试 fixture 没有显式初始化;GitHub Runner 使用 -Werror=missing-field-initializers,因此本地默认编译通过并不代表 CI 通过。

已在新提交中修复:

  • Commit:5bdcc85 🐛 fix(test): 补齐 Linx 音频配置测试夹具
  • Connection() fixture 显式设置 .preferred_audio = std::nullopt
  • clang-format 检查通过。
  • 本地 ./scripts/run_host_tests.sh:83/83 通过。
  • git diff --check:通过。

该提交已推送到本 PR,等待新一轮 CI 结果。这个修复只补齐测试数据初始化,不改变固件运行逻辑。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

同步主分支与 PR #342 展示代码

已将当前开发分支同步到最新 origin/main

本次实际带入的展示内容:

  • components/voicelife_display_esp/assets/esp-sparkbot/mascot/gifs/idle_eyes.gif:新的连贯眼睛动画资源。
  • components/voicelife_display_esp/assets/esp-sparkbot/manifest.json:资源清单更新。
  • components/voicelife_display_sparkbot/src/sparkbot_lvgl_renderer.cc:新的动画渲染处理。
  • 对应的 SparkBot 资源和渲染测试。

验证结果:

刷写注意:#342 不只是应用代码变化,还更新了 idle_eyes.gif,它会进入 sparkbot_assets.bin 资源分区。首次验证必须重新构建并刷写应用分区和 SparkBot 资源分区;只刷 voicelife.bin 可能仍然保留旧的屏保动画。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

实板刷写验证:已使用最新 main + PR #342 展示资源

在同步最新主分支后,已使用 ESP-IDF 6.0.2 重新构建并刷写 SparkBot:

  • Profile:esp32s3-esp-sparkbot-serial-voice
  • 构建分支基线:当前 PR 分支,已包含 origin/main=b83061c
  • 应用镜像:build/esp32s3-esp-sparkbot-serial-voice/voicelife.bin
  • 资源镜像:build/esp32s3-esp-sparkbot-serial-voice/sparkbot_assets.bin
  • 资源 SHA-256:7bc16f4712b7750bbbb04ca60cea69947c2e00ae7898387c84ef7cbe1b4badcb

刷写范围:

  • 0x10000:应用分区
  • 0x300000:SparkBot assets 分区
  • 0x400000:MultiNet 模型分区

明确没有写入:

  • 分区表
  • NVS/Wi‑Fi 凭据
  • linx_secrets

esptool 对三个镜像均报告 Hash of data verified。芯片识别为 ESP32-S3,MAC 为本机已授权设备,分区表内容与构建配置一致。

复位后的串口验证已经看到:

SPARKBOT_GIF_LOADED asset=idle_eyes.gif
GIF_OPEN width=96 height=96
SPARKBOT_GIF_FIRST_FRAME_OK width=96 height=96
SPARKBOT_IDLE_SCREENSAVER_ENTERED=1 asset=idle_eyes
SPARKBOT_GIF_LOOP asset=idle_eyes frames=14/28/42/56

新资源实际为 68 帧循环,说明 #342 的新眼睛 GIF 已经被 assets 分区加载,不再是旧资源。当前工作树保持干净。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

重要阶段结论:SparkBot 无 AEC,生产监听模式固定为 auto

这次修改已经在实板上完成 12 轮连续对话验收。结论很明确:SparkBot 没有播放回采通道,也没有 AEC,生产路径应沿用小智 SparkBot 的 auto-stop 机制;强制 realtime 与这块板子的硬件能力不匹配。该结论和 auto 默认值作为本项目 SparkBot 生产基线冻结,后续不得为追求“实时”而随意改回 realtime。如需调整,必须重新做同等实板长对话对照并在 PR 中说明原因。

本次代码修改

  • Commit:363655d 🐛 fix(voice): 对齐 SparkBot auto 监听模式
  • components/voicelife_runtime/src/runtime.cc
  • 生产 VoiceSessionConfigVoiceMode::kRealtime 改为 VoiceMode::kAuto
  • 唤醒协议日志改为显式输出 manualautorealtime,避免非 auto 模式被误报成 realtime
  • 通用 VoiceMode::kRealtime、Linx 三种模式编码和契约测试保留;它们是通用兼容能力,不代表 SparkBot 生产路径继续使用 realtime。

实板环境和固件

  • 设备:ESP32-S3 SparkBot
  • 串口:/dev/cu.usbmodem14401
  • Wi-Fi:zxp
  • ESP-IDF:6.0.2
  • 固件:build/esp32s3-esp-sparkbot-serial-voice/voicelife.bin
  • 固件 SHA-256:de946dbb9dfebbd8418ca1786b1d694652210f543704dfbf63b5950687939bed
  • 只刷写应用分区 0x10000,未改动 NVS、Wi-Fi 凭据、分区表、assets、model 或 SQLite 数据分区。

百炼 12 轮连续对话

测试方式:阿里云百炼真实 TTS PCM 串口注入,模型 qwen-audio-3.0-tts-flash,音色 longanlingxi;每轮完成后等待设备自动回到下一轮 聆听中,没有使用 macOS 扬声器,也没有在轮次之间重新建 WebSocket。

日志:/private/tmp/voicelife-bailian-auto-20260823-12turn/multiturn-20260823-103513.log

结果:requested_turns=12completed_turns=12asr_exact_matches=12。下表中的“设备回应”按串口 SERIAL_VOICE_EVIDENCE event=tts_sentence_started 原文记录,竖线表示同一轮的连续 TTS 句段;每一轮均无错误。

# 注入文本 ASR 设备回应 错误
1 你是谁? 你是谁? 😲哇!我是牛牛,是你身边的小伙伴!专门帮你记事情、安排日程,还会陪你聊天哦!你叫什么名字呀?
2 你叫什么? 你叫什么? 😶我叫牛牛呀!就是你的小帮手,帮你记日程、陪你聊天的小伙伴!你刚才说你的名字是新鹏对吗?
3 你能做什么? 你能做什么? 😶我可以帮你做三件事哦:1. 记事情——比如你说“明天下午三点有画画课”,我就能帮你记下来,还会提醒你。2. 查日程——想知道今天或明天有什么安排,问我就可以。3. 改或删日程——比如想取消某节课、改时间,我都能帮你处理。不过我不会放音乐,也不会自己编故事,只做跟日程有关的事情!你现在有什么需要我帮忙的吗?
4 你会记住前面的话吗? 你会记住前面的话吗? 😶我会记住一些和你有关系的事情,比如你的名字、你告诉我的日程安排。但是其他不重要的信息,我聊完就会忘记啦,这样每次跟你聊天都更专注!你刚才说你是新鹏,这个名字我记住啦~
5 我喜欢散步。 我喜欢散步。 😌散步真棒!晚上吹吹风、走走逛逛,特别放松。你平时喜欢在哪里散步呀?
6 我刚才喜欢什么? 我刚才喜欢什么? 😶你刚才说喜欢晚上散步,我记得呢!是喜欢在小区里走走,还是去公园呀?
7 请说说春天。 请说说春天 😌春天呀,天气暖暖的,花儿都开了,最适合出去散步啦!新鹏,春天的时候你是不是更喜欢在公园里散步呀?
8 春天有什么特点? 春天有什么特点? 😶春天呀,天气变暖和了,小草变绿了,花儿也开了,还会有小蝴蝶飞来飞去。不过这些我都是听说的,我平时主要帮你记日程,春天的事情我知道得不多哦~
9 我们聊了几个话题? 我们聊了几个话题。 😶我们聊了四个话题哦:1. 我是谁2. 我叫什么名字3. 我能做什么4. 你喜欢散步和春天
10 请总结一下。 请总结一下。 😌好呀!我帮你总结一下:你叫新鹏,喜欢晚上散步,还跟我聊了春天的特点。我们聊了四个话题,都很开心!现在有什么需要我帮忙的吗?比如记个日程,或者查一下明天的安排?
11 再短一点。 再短一点。 😶你是新鹏,喜欢晚上散步,我是帮你记日程的牛牛。
12 请用一句话结束这次聊天。 请用一句话结束这次聊天。 😶好的新鹏,今天和你聊天很开心,下次想记日程或聊天随时找我哦!

串口和屏幕验收指标

  • 每轮均观察到 listen.start mode=auto,并完成 准备中 -> 聆听中 -> 处理中 -> 说话中 -> 下一轮聆听中
  • in_drop=0out_reject=0short_write=0in_i2s_err=0out_i2s_err=0
  • SERIAL_VOICE_PCM=reject:0;INTERACTION_REJECTED:0;交互队列 control_dropped/best_effort_dropped/board_dropped:0。
  • Connection reset by peer:0;LINX_WS_ERROR:0;provider_error:0;transport_disconnected:0。
  • 音频统计:输入 test_in_frames=922,设备下行 out_frames=7232in_frames=9737out_q_hi=36,无丢失或短写。
  • 屏幕渲染快照 111 条、含正文 68 条;长文本观察到 overflow_width 横向滚动,准备中/聆听中/处理中/说话中 与语音事件同步。
  • 最低空闲堆:min_heap=4573856;未观察到异常复位、Guru Meditation、看门狗或 TLS/WSS 错误。

回归门禁

  • ./scripts/run_host_tests.sh:83/83 通过。
  • git diff --check:通过。
  • 实板补跑使用的日志和 JSON 均保留在上述临时目录,未提交 API Key、Authorization、Wi-Fi 密码或原始音频。

这条评论记录的是当前阶段的真实验收结果。后续仍需单独完成唤醒、日程 CRUD/持久化、提醒和 IM 闭环,但无 AEC SparkBot 的语音长对话模式已经有实板 12 轮通过证据,不应再被随意改回 realtime。

@ZhaoXingPeng

ZhaoXingPeng commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

V0-V3 阶段索引更正(2026-08-23)

上一条 V0-V3 汇总是过程草稿,包含尚未闭合的门禁,不再作为验收依据。最终通过的详细记录已拆成独立阶段评论:

后续阶段只引用对应的成功实验评论,不复用本条旧汇总。

@ZhaoXingPeng

ZhaoXingPeng commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

旧计数勘误作废

本条计数勘误属于旧过程草稿,已由 V1/V2 的最终实板成功记录取代。请以后续独立阶段评论为准:

串口测试任务在有限时间内等待 PCM 租约归还,避免 TLS 短暂背压制造输入丢帧;MCP tools/list 的工具项补齐 Linx 文档要求的 type=0。

Host 格式检查、mcp_server_test、mcp_json_writer_coverage_test、linx_mcp_bridge_test 已通过。

Refs #349
@ZhaoXingPeng

ZhaoXingPeng commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

V1 实板唤醒验收(2026-08-23,详细记录更正)

结论:当前固件在真实 SparkBot 上完成“你好牛牛”唤醒、确认音和自动监听闭环。本轮只验证唤醒阶段,没有发送普通聊天问题;因此“用户说了什么”和“设备回答什么”分别只有唤醒词与确认音,不能把这轮误写成普通对话验收。日志中的 stale_event_dropped(asr_outside_capture) 是确认音阶段的旧代际 ASR 被门禁丢弃,属于预期保护,不是失败。

固件、环境和修复

  • 固件提交:8e20852(串口 PCM 背压有界等待 + Linx MCP 工具 type=0);实板 ELF 日志 SHA 前缀 bc7806b0b
  • 生产模式基线:363655d 将 SparkBot 监听模式固定为 auto81faa3c 恢复唤醒确认音。
  • 设备:ESP32-S3 SparkBot,串口 /dev/cu.usbmodem14401,Wi-Fi 已连接 zxp;本轮未改写 Wi-Fi/NVS/SQLite 分区。
  • 输入:阿里云百炼真实 TTS qwen-audio-3.0-tts-flash / longanlingxi,随后转换为 16 kHz、单声道、S16LE、20 ms、640-byte PCM,经 VLVT 串口注入;没有使用 Mac 扬声器。
  • 百炼预检:/private/tmp/voicelife-pr349-v1v2-20260823/preflight-20260823-113413.json,1/1 成功,音频 58,187 bytes,无 provider error。
  • 实板日志:/private/tmp/voicelife-pr349-v1v2-20260823/v1-wake.log

逐步输入、识别、播报和屏幕证据

步骤 我说/注入的原文 设备识别/回答原文 屏幕、协议和串口证据
1 你好牛牛(百炼 TTS,58 帧、37,120 bytes) WAKE_DETECTED word=你好牛牛(日志 257) 先有 standby_ready(日志 250),屏幕仍为待机时间;随后进入确认阶段
2 没有追加第二句用户问题 Linx 收到 listen.detect(日志 265),随后收到 listen.start mode=auto(日志 272);provider_capture_started detail=auto(日志 278) 屏幕 revision 6 显示 status=收到 content=收到!(日志 279),没有提前显示“聆听中”
3 设备确认音不是用户输入 设备实际唯一 TTS 原文:收到!;顺序为 tts_started(日志 299)→ tts_sentence_started detail=收到!(日志 308)→ tts_first_audio(日志 313)→ tts_stopped(日志 331) 播放阶段屏幕为 status=说话中 content=收到!(日志 324),确认音播完后清空正文并回到准备阶段
4 无额外注入 自动重新打开采集:capture_started(日志 348) 屏幕 revision 11 显示 status=聆听中(日志 352),唤醒闭环结束

这轮没有发生的事情

  • 没有注入“你是谁”“记一个日程”等普通聊天或 MCP 日程指令;所以 V1 不宣称普通对话、CRUD 或提醒通过。
  • 设备没有自我介绍;确认播报严格为 收到! 一句。
  • 日志 283 的 LINX_RX kind=stt text=收到! 没有被当成用户问题,日志 288 明确记录为 stale_event_dropped detail=asr_outside_capture,这是确认音回传的旧事件隔离。

错误门禁

  • in_drop=0out_reject=0short_write=0in_i2s_err=0out_i2s_err=0
  • SERIAL_VOICE_PCM=reject:0;Connection reset by peer:0;LINX_WS_ERROR:0;provider_error:0。
  • Linx hello、MCP initialize/tools/list 均完成;连接未在唤醒确认音后断开。

修复路径

修复前,唤醒确认音、物理麦克风和旧 ASR 事件可能交错,表现为确认音后状态提前变化或测试误报。对应修复如下:

  1. 363655d:SparkBot 无 AEC 时固定使用 auto,不再把 realtime 当成已验证前提。
  2. 81faa3c:恢复本地唤醒确认音,且只播报 收到!
  3. 04c3d8d / 8e20852:固定 detect → start(mode=auto) → 确认音 → capture_started;用 generation 隔离旧回合 ASR/PCM;串口注入在短暂 TX 背压时等待 payload 租约,不人为制造丢帧。
  4. 498fa4afa3af12d8832e5:测试门禁按实际 PCM 长度等待回合结束,并接受 TURN_END 或真实 capture_stopped 作为结束证据;这些提交只修测试证据边界,不改变设备业务逻辑。

本轮日志中的协议顺序、字幕、GIF/状态和音频事件均按上述顺序出现,证明修复后的唤醒闭环没有提前切屏或把确认音误当用户输入。

验收状态:PASS

@ZhaoXingPeng

ZhaoXingPeng commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

V2 实板儿童视角多轮对话验收(2026-08-23)

结论:修复串口背压和回合结束门禁后,SparkBot 在同一条 Linx WebSocket 会话内完成 8 轮儿童视角对话。8/8 回合完成、8/8 ASR 精确匹配、每句设备 TTS 原文均有串口证据,未发生 PCM reject、RST、provider、I2S 或交互队列错误。

固件、环境和修复

  • 固件提交:8e20852;只刷写应用分区,实板仍保留 Wi-Fi、NVS、SQLite、assets 和 model。
  • 测试工具提交:498fa4a,将 SERIAL_VOICE_TURN_END 的等待从 5 秒扩大为有界 15 秒,覆盖已观察到的 Linx/TX 队列排空延迟,避免把有效回合误判为测试失败。
  • 生产监听模式:auto363655d),适配无 AEC 的 SparkBot。
  • 输入:阿里云百炼真实 TTS qwen-audio-3.0-tts-flash / longanlingxi;每句预先转为 16 kHz、单声道、S16LE、20 ms PCM,经 VLVT 串口注入,未使用 Mac 扬声器。
  • 实板日志:/private/tmp/voicelife-pr349-v1v2-20260823/v2-multiturn-final.log
  • 结构化结果:/private/tmp/voicelife-pr349-v1v2-20260823/v2-multiturn-final.json

逐轮完整记录

轮次 注入话术 ASR 原文 设备实际播报(串口 tts_sentence_started,按顺序)
1 我叫小新,我喜欢画画和踢足球。 我叫小新,我喜欢画画和踢足球。 😆哈哈,小新你好呀!你喜欢画画和踢足球,听起来真棒!画画可以画出好多有趣的东西,踢足球又能跑又好玩,真厉害!现在有什么需要牛牛帮忙的吗?比如记个日程、查个事情之类的?
2 请记住我的名字和爱好。 请记住我的名字和爱好。 😆好的小新,我已经记住啦!你叫小新,喜欢画画和踢足球,牛牛会一直记得的!现在有什么需要我帮忙的吗?比如记个日程或者查个事情?
3 我最喜欢听小熊探险的故事。 我最喜欢听小熊探险的故事。 😆哇,小熊探险的故事听起来好有趣!小新喜欢听勇敢的小熊去冒险,一定特别精彩!现在有什么需要牛牛帮忙的吗?比如记个日程或者查个事情?
4 你还记得我喜欢什么吗? 你还记得我喜欢什么吗? 😆当然记得啦!小新喜欢画画、踢足球,还喜欢听小熊探险的故事!牛牛都牢牢记住啦!
5 如果我今天有点不开心,你会怎么安慰我? 如果我今天有点不开心,你会怎么安慰我? 😶小新不开心呀,牛牛先抱抱你!不开心的时候,可以试试做喜欢的事,比如画一幅画,或者踢一会儿足球,说不定心情就好起来啦!要不要牛牛陪你聊聊天,或者帮你记个好玩的事情?
6 请把刚才的安慰缩短。 请把刚才的安慰缩短。 😶小新不开心就抱抱你!画画、踢足球,或者听小熊探险的故事,心情就会好起来啦!
7 你能用小朋友听得懂的话讲讲为什么要按时睡觉吗? 你能用小朋友听得懂的话讲讲为什么要按时睡觉吗? 😉小新,按时睡觉就像给手机充电一样!白天玩累了,晚上睡觉的时候,身体就会“充电”,第二天才能有力气踢足球、画画、听小熊探险的故事哦!如果睡太晚,第二天就会像没电的玩具一样,没力气玩啦~
8 请说一句鼓励我学习的话。 请说一句鼓励我学习的话。 😎小新,学习就像在搭积木,每学会一个新知识,就多了一块积木,慢慢地就能搭出又高又漂亮的城堡啦!

上表不是模型摘要,而是串口明文逐句记录。结构化结果中的每轮边界为:R1 log_start=259, log_end=435、R2 435-588、R3 588-738、R4 738-862、R5 862-1017、R6 1017-1136、R7 1136-1277、R8 1277-1390;原始日志 /private/tmp/voicelife-pr349-v1v2-20260823/v2-multiturn-final.log 中每轮都能按 LINX_RX kind=sttSERIAL_VOICE_EVIDENCE event=stt_text_received、连续的 event=tts_sentence_startedevent=tts_stopped 复核。例如:R1 用户原文在日志 329,五段设备原文在 353/376/388/401/412;R5 用户原文在 914,四段设备原文在 938/961/972/987;R8 用户原文在 1319,两段设备原文在 1343/1367。

每轮状态与屏幕证据

每轮均出现 SERIAL_VOICE_TURN_BEGIN=ok → capture_started → capture_stopped → stt_text_received → tts_started → tts_first_audio → tts_stopped → 下一轮 capture_started,状态/屏幕顺序为 准备中 → 聆听中 → 处理中 → 说话中 → 下一轮准备中/聆听中。字幕逐句覆盖用户输入和设备回复;长句出现 overflow_width>0 横向滚动。日志统计:rendered_text_snapshots=75content_snapshots=44scroll_observed=true

错误门禁和 Linx 证据

  • 结果:requested_turns=8completed_turns=8asr_exact_matches=8
  • 音频:test_in_frames=1121in_frames=7292out_frames=5086in_drop=0out_reject=0short_write=0in_i2s_err=0out_i2s_err=0
  • 串口 PCM:SERIAL_VOICE_PCM=reject 0;交互队列 control_dropped=0best_effort_dropped=0board_dropped=0INTERACTION_REJECTED 0。
  • Linx/服务:Connection reset by peer 0、LINX_WS_ERROR 0、provider_error 0;没有异常复位、Guru Meditation 或 watchdog。
  • 启动阶段 hello、MCP initialize/tools/list 成功,6 个工具注册;本轮是普通聊天,不应触发 tools/call,因此不以日程工具证据作为 V2 判定条件。

修复路径

旧 V2 的 67 条 SERIAL_VOICE_PCM=reject code=5 来自测试注入在 Linx 短暂背压时立即放弃 payload。8e20852 在串口测试任务中加入最多 2 秒的 payload 租约等待;498fa4a 又把显式回合结束确认改为 15 秒有界等待;fa3af12 按 PCM 帧数动态延长等待上限,d8832e5 接受 TURN_END 或真实 capture_stopped 作为有效结束证据。最终重跑在相同实板、相同百炼模型和相同 PCM 协议下将两项门禁全部闭合,证明本阶段的失败来自测试/传输边界而非对话内容或 Linx RST。上述修复只调整串口测试的背压/证据边界,未修改儿童对话内容或日程业务逻辑。

验收状态:PASS

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

v3 实板多轮语音阶段记录(2026-08-23)

结论:在 SparkBot 实板、auto 模式和百炼真实 TTS 注入下,12 轮儿童视角上下文对话完整通过。测试脚本门禁修正已提交为 5f90e15🐛 fix(test): 修正 auto 多轮状态门禁边界)。本次固件证据对应 c98a5af;脚本修正只影响证据边界判定,不改变设备语音行为。

固定环境

  • 板卡:SparkBot;Wi-Fi:zxp;串口:/dev/cu.usbmodem14401
  • 百炼输入:qwen-audio-3.0-tts-flash / longanlingxi
  • 百炼预检:1/1 成功,音频 58187 bytes,无 provider error
  • 命令入口:scripts/run_bailian_sparkbot_test.sh multiturn
  • 证据:/tmp/voicelife-bailian-tests/multiturn-20260823-143844.json、同名 .log.meta.txt

逐轮记录

轮次 注入原文 板端 STT 设备第一段播报 PCM 结果
1 我是小朋友,你是谁? 我是小朋友,你是谁? 😆我是牛牛, 102/102 PASS
2 你喜欢什么颜色? 你喜欢什么颜色? 😉我最喜欢蓝色啦! 87/87 PASS
3 我喜欢蓝色。 我喜欢蓝色 😆哇,我们都喜欢蓝色, 66/66 PASS
4 蓝色像什么? 蓝色像什么 🤔蓝色像大大的天空, 78/78 PASS
5 大海里有鱼吗? 大海里有鱼吗? 😲当然有啦! 83/83 PASS
6 海豚会游泳吗? 海豚会游泳吗? 😆海豚最会游泳啦! 87/87 PASS
7 月亮会发光吗? 月亮会发光吗。 🤔月亮自己不会发光哦, 83/83 PASS
8 星星在哪里? 星星在哪里 😲星星在好远好远的天空上! 70/70 PASS
9 讲一个小故事。 讲一个小故事。 😌好啊,牛牛给你讲一个小熊探险的故事。 70/70 PASS
10 故事里的小朋友叫什么? 故事里的小朋友叫什么? 😶故事里的小熊叫蓝蓝, 95/95 PASS
11 他找到宝藏了吗? 他找到宝藏了吗? 😜蓝蓝还没有找到宝藏呢, 70/70 PASS
12 和我说晚安。 和我说晚安 😴晚安,小新! 83/83 PASS

第 3、4、7、8、12 轮仅有中文标点差异,归一化后与输入一致;没有语义丢失或端点截断。

每轮链路与屏幕证据

12 轮均观察到:

tts_stopped -> phase/state 3 准备中 -> phase/state 4 聆听中 -> phase/state 5 处理中(或 auto 服务端 STT 直达 phase/state 6) -> phase/state 6 处理中 -> phase/state 7 说话中 -> tts_first_audio -> tts_stopped

SPARKBOT_DISPLAY_SNAPSHOT_RENDERED 共 108 次,文本快照 108 次,内容快照 65 次,并观察到 overflow_width>0 的横向字幕滚动。状态/屏幕/语音阶段均有串口明文证据。

音频与异常统计

completed_turns=12/12
asr_exact_matches=12/12
in_drop=0
out_reject=0
short_write=0
in_i2s_err=0
out_i2s_err=0
in_pool_fail=0
control_dropped=0
best_effort_dropped=0
board_dropped=0
serial_pcm_rejections=0
provider_error=0
INTERACTION_REJECTED=0
Connection reset/RST=0

最终统计:test_in_frames=974out_frames=7434in_frames=9849out_q_hi=72/96min_heap=4509668。设备保持 Linx 会话连续,没有重连或 WebSocket RST。

修复内容

5f90e15 修正测试证据而非放宽产品行为:

  1. 等待下一轮 SERIAL_VOICE_CAPTURE_READY 后增加一个调度窗口,确保异步 LVGL phase=4 快照落入本轮日志边界。
  2. auto 模式下若 Linx 的 server-VAD 先返回 stt_text_received,允许本地 capture_stopped/phase=5 缺席;仍强制检查 phase/state 3、4、6、7、TTS 首帧和停止证据。
  3. 保持音频零丢失、PCM reject、provider error、交互队列丢弃等硬门禁不变。

仍未验收项

本阶段只证明语音/屏幕/音频长对话链路。该日志没有 MCP_RX method=tools/callMCP_TOOL_EXECUTED tool_call=1,因此日程 CRUD 仍不能标记为通过;前面 Linx 的“已经创建”只能作为自然语言伪成功,不能替代真实工具执行和 SQLite 持久化证据。下一阶段继续针对 Linx 智能体未下发 tools/call 的原因做实验。

主机回归:./scripts/run_host_tests.sh,83/83 通过。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

v4 实板唤醒阶段记录(2026-08-23)

结论:在 v3 多轮结束后重新打开串口,SparkBot 仍能稳定识别唤醒词“你好牛牛”,完成“收到!”确认音,并回到 auto 聆听状态。该阶段通过。

环境与证据

  • SparkBot 实板,Wi-Fi zxp,串口 /dev/cu.usbmodem14401
  • 百炼 TTS:qwen-audio-3.0-tts-flash / longanlingxi
  • 证据:/tmp/voicelife-bailian-tests/wake-20260823-144525.log、同名 .meta.txt
  • 设备应用镜像 SHA 与 v3 相同(本次未重新刷写固件);测试脚本提交为 5f90e15

实验步骤与结果

  1. 串口打开后第一次 WAKE_BEGIN 返回 reject code=4。这是设备尚未完成本次启动的待机切换,不是唤醒词失败;脚本按协议等待 SERIAL_VOICE_EVIDENCE event=standby_ready 后只重试一次。
  2. 第二次握手:SERIAL_VOICE_WAKE_BEGIN=ok
  3. 注入百炼真实语音“你好牛牛”(58 个 20 ms PCM 帧),收到 SERIAL_VOICE_WAKE_END=ok
  4. 本地唤醒:WAKE_DETECTED word=你好牛牛
  5. 协议顺序:listen.detect -> local_wake_ack_requested -> listen.start mode=auto
  6. 屏幕先显示 收到,随后进入 说话中 并渲染 收到!;串口收到 tts_started -> tts_sentence_started(收到!) -> tts_first_audio -> tts_stopped
  7. 确认音结束后:capture_startedSERIAL_VOICE_CAPTURE_READY,屏幕进入 聆听中,可继续接收下一句。

关键日志证据:

SERIAL_VOICE_WAKE_BEGIN=ok
WAKE_DETECTED word=你好牛牛
SERIAL_VOICE_EVIDENCE event=local_wake_ack_requested
LINX_SEND listen state=detect mode=-
LINX_SEND listen state=start mode=auto
SERIAL_VOICE_EVIDENCE event=tts_started
SERIAL_VOICE_EVIDENCE event=tts_first_audio
SERIAL_VOICE_EVIDENCE event=tts_stopped
SERIAL_VOICE_EVIDENCE event=capture_started
wake_injection_success text=你好牛牛 frames=58

异常统计

SERIAL_VOICE_PCM=reject: 0
in_drop=0
out_reject=0
short_write=0
in_i2s_err=0
out_i2s_err=0
provider_error=0
INTERACTION_REJECTED=0
Connection reset/RST=0

本阶段补充确认:唤醒确认音不是自我介绍,播报内容严格为“收到!”。初次 code=4 被固定握手逻辑吸收,后续不再重复发送请求;这也是脚本修复后的预期行为。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

v5 服务器 IM 运行面核对(2026-08-23)

结论:SSH 核对 /root/XE6-15 后确认服务器只运行 IM 网关和 PostgreSQL,没有 Linx/WebSocket/MCP 服务进程;因此服务器端不会解释设备侧 Linx tools/call 缺失,也没有在服务器上发现 Linx RST 日志。

服务器事实

voicelife-im-gateway   xe6-15-gateway       Up 2 days (healthy)   127.0.0.1:3000->3000/tcp
voicelife-postgres     postgres:16-alpine   Up 3 days (healthy)   127.0.0.1:5432->5432/tcp

/root/XE6-15/docker-compose.yml 仅声明 gatewaypostgres 两个服务;网关健康检查持续返回 200。

日志证据

网关历史路由计数:

device.pairing.create       11
device.pairing.get          428
device.schedule-query-result.create 6
schedule-query-page         5
wechat.webhook              8
action-ui                   1
healthz                     24465

已看到 delivery.worker.started 和多次 delivery.worker.dispatched,随后对应 wechat.webhook 返回 HTTP 200;最近一次有效微信回调为 2026-08-22T06:46:48Z,耗时 53 ms。服务器日志中没有 linxMCP_RXtools/call 或 WebSocket RST 进程/路由。

对当前实验的影响

  • IM 服务健康、配对轮询和微信 webhook 到达过;历史通知投递 worker 有 dispatched 证据。
  • 本次没有通过服务器主动发送真人验证码或修改服务配置,未改变线上状态。
  • Linx 语音/MCP 仍是设备直接连接 Linx 平台;日程 CRUD 的阻塞点仍在“平台未下发真实 tools/call”,不是 /root/XE6-15 上的 IM 服务。

@ZhaoXingPeng

ZhaoXingPeng commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

MCP 协议对照与回退结果(74c2a25)

结论:PR338 与当前分支的 MCP 调用分发链没有结构性差异;本次确认的回归点是后续提交加入的 tools/list 扩展字段,而不是 tools/call 参数转换或日程工具 handler。

与 PR338 基线的逐项对照

  • initialize:保持 JSON-RPC 版本 2024-11-05capabilities.toolsserverInfo,无变化。
  • tools/list:PR338 返回单页 {"tools":[...]}。当前已移除后续加入的 nextCursor: null、分页 cursor 处理和工具项 type: 0,恢复为同一单页结构。
  • 工具注册:生产运行时仍是 6 个工具:schedule.createschedule.queryschedule.updateschedule.deleteschedule.operation_queryim.binding.start。PR338 的生产注册数量和名称相同;Host 基础夹具只注册前 4 个日程工具,因此测试断言为稳定的四项顺序。
  • 注册边界:当前在 operation_service == nullptr 时只注册基础四个日程工具,避免精简装配误注册 schedule.operation_query;真实 SparkBot 运行时传入该服务,因此生产工具集合不变。
  • tools/call:仍按“解析 JSON-RPC -> 校验 params.name/arguments -> JSON 值转换 -> McpServer::call -> 返回 MCP text content/isError”执行。没有新增 nextCursor、额外调用参数或新的工具调用入口。
  • 异步 worker、session_id 回传、队列满/超时错误回包和调用结果语义提取均保留;这些是 PR338 的并发隔离与诊断改动,不属于工具发现字段。

本次修复

  • 删除 mcp_json_writer.cc 中的工具项 type: 0
  • 删除 mcp_json_writer.cc 中的 nextCursor 分页字段。
  • 删除 MCP 单元测试中对上述字段的过时断言,恢复 PR338 单页发现契约。

验证

  • mcp_server_test:通过。
  • mcp_json_writer_coverage_test:通过。
  • linx_mcp_bridge_test:通过,覆盖 initialize、单页 tools/list、schedule.create tools/call、session_id、通知和错误回包。
  • linx_mcp_bridge_coverage_test:通过。
  • 聚焦 CTest:4/4 通过。
  • git diff --check:通过。

本阶段没有修改 Wi-Fi、NVS、SQLite 分区,也没有带入工作树中的音频/日程/语音实验改动。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

阶段记录:MCP 内部状态探测与屏幕终态修正

提交:b13ef52

已完成

  • self.get_device_status 现在被标记为 Linx 规划阶段的内部探测。它成功返回时不再投递 mcp_tool_result 用户终态,不会再把屏幕覆盖成“操作已完成”。
  • self.audio_speaker.set_volumeschedule.create/query/update/delete 仍保留用户可见结果;新增更新、删除的固定摘要。
  • 状态工具回包补齐为与原生 Xiaozhi SparkBot 一致的稳定顶层结构:audio_speakerscreennetwork.type=wifi。未伪造电池或芯片数据。
  • tools/list 分页、self.* 工具名和 MCP 外层信封保持不变。

已通过验证

  1. 主机 linx_mcp_bridge_test:通过。覆盖分页目录、self.* 调用、音量 true 文本回包、状态工具隐藏用户终态。
  2. 主机 linx_mcp_bridge_coverage_test:通过。
  3. SparkBot serial-voice 固件重新编译:通过,应用镜像 0x282c90,工厂应用分区余量约 11%。
  4. 只刷应用分区 0x10000,保留 Wi-Fi/NVS/分区表/资源;刷写校验通过。
  5. 实板启动、Wi-Fi zxp、Linx hello、MCP 工具发现:通过。
  6. 实板连续两次状态探测:日志为 MCP_TOOL_RESULT_INTERNAL tool=self.get_device_status success=1,回包 227 字节;未出现 SPARKBOT_TEXT_RENDER ... 操作已完成

实板日志:/tmp/voicelife-bailian-tests/post-fix-status-20260823.log

当前边界

本阶段没有把最后一次百炼音量语音夹具计为通过:串口夹具打开时将板子带入 USB 下载模式,已终止夹具并用 esptool run 恢复正常启动。此前同类百炼上行回合仍能复现 transport_poll_write(0),这是 WebSocket 连接在 PCM 上行阶段断开,和 MCP 工具注册/屏幕误终态是两个独立问题;后续应单独处理传输层,不能把它包装成 MCP 已通过。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

MCP 工具广告与 Linx 选择层验证(重要阶段记录)

固件与证据绑定

  • 诊断提交:edf3ba5🧪 test(mcp): 记录工具广告与固件镜像指纹
  • 构建环境:ESP-IDF v6.0.2
  • 刷写方式:仅刷 0x10000 应用分区,未修改 Wi-Fi、NVS、SQLite、资源和模型分区
  • 测试 meta:/tmp/voicelife-bailian-tests/wake-20260824-000152.meta.txt
  • 实板串口日志:/tmp/voicelife-bailian-tests/wake-20260824-000152.log
  • 镜像 SHA-256:a2e84a58a855765bfd5a899efc3ad59c7f4f4871c127c1998062917bdbc39b3f
  • ELF SHA-256:6d6eb0aa21f9e1fe6cb14ca2a83ef1282b96e2a1b517ff7eb5493b699c61256d

设备侧实际广告内容

设备收到 Linx 的一次 tools/list 后记录:

MCP_TX method=tools/list id=2 bytes=7560 result=1
MCP_TOOL_ADVERTISEMENT count=8
MCP_TOOL_ADVERTISED name=self.get_device_status
MCP_TOOL_ADVERTISED name=self.audio_speaker.set_volume
MCP_TOOL_ADVERTISED name=self.im.binding.start
MCP_TOOL_ADVERTISED name=self.schedule.create
MCP_TOOL_ADVERTISED name=self.schedule.query
MCP_TOOL_ADVERTISED name=self.schedule.update
MCP_TOOL_ADVERTISED name=self.schedule.delete
MCP_TOOL_ADVERTISED name=self.schedule.operation_query
LINX_TX_TEXT_SENT bytes=7560

因此本次固件在线路上确实发送了包含 self.audio_speaker.set_volume 的完整目录,且响应长度与 WebSocket 发送长度一致。此前 512 字节正文截断造成的“第一页是否包含音量工具”疑问已排除。

百炼真实语音单轮测试

输入配置:qwen-audio-3.0-tts-flash + longanlingxi

  1. 唤醒:你好牛牛
    • 唤醒成功,设备播放“收到!”并进入聆听状态。
  2. 音量请求:请把音量调到30
    • ASR:请把音量调到30。
    • Linx 只发送了两次:tools/call self.get_device_status
    • 两次状态调用均成功返回。
    • 没有出现:tools/call self.audio_speaker.set_volume
    • 没有出现:MCP_VOLUME_SET value=30
    • 没有出现:ES8311_OUTPUT_VOLUME_SET value=30
    • 设备最终播报“目前没有找到可以调节系统音量的工具”。
  3. 本轮没有出现 LINX_WS_ERROR_EVENTMCP_REQUEST_REJECTED、队列满、MCP 超时或 Connection reset by peer

结论

本阶段进一步排除了设备侧“工具没有注册/没有序列化/发送不完整”的可能性。调用链实际走到:

VoiceLife tools/list(完整 8 工具)
  -> Linx tools/call self.get_device_status
  -> VoiceLife 成功回包
  -> 未产生 self.audio_speaker.set_volume

失败仍发生在 Linx 服务端工具目录重建、模型上下文工具暴露或模型下一步工具选择阶段;本地音量 handler 没有进入执行路径。/root/XE6-15 仅运行 IM gateway 和 PostgreSQL,没有 Linx Agent 审计日志,因此还不能区分“服务端目录过滤”和“模型候选选择失败”。

自动验证

以下主机测试通过:

  • linx_mcp_bridge_test
  • linx_mcp_bridge_coverage_test
  • mcp_server_test
  • mcp_json_writer_coverage_test
  • bash -n scripts/run_bailian_sparkbot_test.sh

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

PR319 实板 MCP 验证(2026-08-24)

结论:PR319 固件可以完成百炼语音交互,但本次没有完成任何业务 MCP tools/call。设备收到工具目录后,Linx/模型只生成了自然语言回复,未把 schedule.create/query/delete 请求下发到设备,因此“已成功创建”不能视为日程已写入。

固件与环境

  • PR:🐛 fix(voice): 修正 SparkBot 实时语音可靠性与日程边界 #319(已合并)
  • 固件 commit:9c3732807ed763f2a6fe75e4c24d7b7a5279f063
  • 构建目标:SparkBot ESP32-S3,ESP-IDF v6.0.2
  • 刷写方式:仅刷应用分区 0x10000,未擦除/覆盖 Wi-Fi、NVS、Linx secrets 或资源分区
  • 固件镜像 SHA-256:0acc3ca51a769110c150db1f34d4d10eddcafad063d350fd7044839fd0d7d3b0
  • ELF SHA-256:53230c749ccadfc29e62db17079d42b4863a782012747679faf0b02a963710b2
  • 串口:/dev/cu.usbmodem14401,Wi-Fi:zxp
  • 百炼 TTS:qwen-audio-3.0-tts-flash / longanlingxi
  • TTS 预检:PASS,返回音频 58187 bytes

实验输入

通过仓库固定脚本和 PR319 固件的 VLVT-v1 串口注入协议执行:

  1. 请创建一个明天下午三点到四点的画画日程,标题是彩笔实验319,备注是带上红色彩笔。
  2. 请查询标题为彩笔实验319的日程。
  3. 请删除标题为彩笔实验319的日程。

日志:/tmp/voicelife-bailian-tests/pr319/multiturn-20260824-002016.log
结果汇总:/tmp/voicelife-bailian-tests/pr319/multiturn-20260824-002016.json

串口证据

  • 启动广告:MCP_TOOLS_READY count=6 names=schedule.create,schedule.query,schedule.update,schedule.delete,schedule.operation_query,im.binding.start
  • 启动阶段只有两次非工具 MCP 请求:MCP_REQUEST_QUEUED bytes=518bytes=46,对应日志均为 MCP_TOOL_EXECUTED tool_call=0 result=1
  • 三轮业务语音期间:tools/call=0mcp_tool_started=0MCP_TOOL_EXECUTED tool_call=1=0
  • 没有 MCP_REQUEST_REJECTED、串口 PCM 拒绝、Provider 连接错误或 Connection reset
  • 因此本地 schedule handler、MCP worker 和响应路径没有被业务请求触发,不能把模型播报当作工具执行成功

交互结果

  • 语音状态/屏幕状态链路:3/3 轮完成,聆听中 -> 处理中 -> 说话中 -> 聆听中 正常
  • 第 1 轮模型播报“已经成功创建”,但随后查询返回“没有找到”;这是无工具调用时的模型幻觉,不是持久化成功
  • 查询和删除的 ASR 还分别出现了 彩笔实验三一九 / 彩笔实验319 差异,不能作为可靠 CRUD 验收
  • 音频输出队列出现 OUTPUT_REJECT reason=queue_full 共 102 次;最终状态流仍完成,但该版本播报存在音频丢帧风险
  • 启动日志:STORAGE_MEMORY_READY=1 persistence=volatile,PR319 不是 SQLite 持久化固件

额外协议观察

PR319 的串口协议只接受 TURN_BEGIN/PCM/END。使用当前唤醒脚本的 WAKE_BEGIN 会得到 SERIAL_VOICE_FRAME_REJECT kind=4,因此该次唤醒脚本结果不用于 MCP 判断;多轮实验改用 PR319 原生 VLVT-v1 协议完成。

最终判断

PR319 实板复现了“工具已广告但没有业务 tools/call”的问题。根因边界仍在 Linx 服务端工具目录重建/模型工具选择阶段,设备侧没有收到调用,不能归因于 schedule handler 或 SQLite 执行失败。后续修复应在当前 PR349 基线继续,修复后必须以串口出现 tools/callMCP_TOOL_EXECUTED tool_call=1 和明确工具结果为通过条件。

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🧪 test(voice): 固化百炼 SparkBot 实板测试模板

1 participant