Skip to content

✨ feat(sparkbot): 优化显示与设备控制 - #362

Closed
ZhaoXingPeng wants to merge 46 commits into
1024XEngineer:mainfrom
ZhaoXingPeng:fix/sparkbot-display-and-controls-20260825
Closed

✨ feat(sparkbot): 优化显示与设备控制#362
ZhaoXingPeng wants to merge 46 commits into
1024XEngineer:mainfrom
ZhaoXingPeng:fix/sparkbot-display-and-controls-20260825

Conversation

@ZhaoXingPeng

@ZhaoXingPeng ZhaoXingPeng commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

结论

这个 PR 把 SparkBot 的显示、设备控制和板载 IMU 接入收回到用户能读懂、也能稳定使用的形状。显示改动保持原有语音链路不变;IMU 先完成可靠的 BMI270 采样与摇晃事件上报,摇晃后的产品反馈留给实板确认后再决定。

改动

  • 固化 Linx 文档列出的 21 个常用情感 key,保留现有 Noto emoji/GIF fallback;启动日志额外检查 U+77D2)字形是否可解析。
  • 将正文从 LV_LABEL_LONG_SCROLL_CIRCULAR 改为单向横向滚动。只有正文变化时才重设 LVGL label,并按实际溢出宽度把滚动时长限制在 3000 到 12000 ms,避免重复、加速和一闪而过。
  • Linx llm 消息中的 emotion/action 通过 VoiceSession evidence 传到 SparkBot 表情映射,不改变语音会话状态。
  • MCP 工具的 JSON-RPC、TTS 和串口日志继续保留;“操作已完成”“日程查询完成”等内部摘要不再作为用户屏幕文案。
  • 注册 self.audio_speaker.set_volume,参数为 0..100 的整数,回调复用现有板级输出音量接口,成功返回 true
  • 将 SparkBot 软件视口从 viewport_y=6 下移到 viewport_y=10
  • 接入官方 espressif2022/bmi270 组件(1.1.0~2)。按 SparkBot 硬件资料使用 I2C0 GPIO4/5、默认地址 0x68(同时探测 0x69),加速度计配置为 2G、200 Hz。
  • IMU 复用音频已创建的 I2C master bus,不创建或删除共享总线;独立任务采样并以慢速基线、连续样本和冷却窗口判定摇晃。
  • 摇晃只投递 BoardInputAction::kShakeDetected,Runtime 当前记录 SPARKBOT_IMU_SHAKE detected=1,不伪造语音事件、不改变语音状态机。

IMU 实板验证边界

代码侧已覆盖“传感器可用”和“传感器不存在”两条路径:

  • 成功初始化:SPARKBOT_IMU_READY sensor=BMI270 addr=0x68 sample_hz=200 threshold_mps2=4.5
  • 未找到或初始化失败:SPARKBOT_IMU_UNAVAILABLE ...
  • 摇晃触发:SPARKBOT_IMU_SHAKE detected=1

本次没有访问、读取或刷写实板。实板验证时先确认 READYUNAVAILABLE,再轻微摇晃观察 SHAKE 是否只出现一次且不会连续刷屏。摇晃后的屏幕动画和提示音暂不固化,等硬件事实和阈值由实板测试确认。

聆听时长基线

本 PR 没有改动语音门限。当前实现是:唤醒后未检测到用户说话时最多等待 6000 ms;检测到语音后取消这个门限;VAD 静音 900 ms 触发端点;端点后最终 STT 最多等待 5000 ms。

验证

  • Host CTest:92/92 通过,包含新增 sparkbot_imu_test
  • ESP-IDF 6.0.2:esp32s3-esp-sparkbot 构建通过,voicelife.bin0x2944d0 字节,应用分区剩余约 8%。
  • git diff --check 通过。
  • 提交:0ae06dd✨ feat(imu): 接入 SparkBot BMI270 摇晃检测)。
  • 本 PR 未连接、读取或刷写实板;没有把 HIL 结果冒充为本次验证结论。

Fixes #361

JunLang-7 and others added 25 commits August 24, 2026 15:52
按 reminderTriggerId 执行 IM 动作,避免最近提醒窗口误操作其他提醒链。
持久化 operationId、动作终态和 nextTriggerAt,使设备任务重启后复用已提交结果。

主机测试、Gateway 门禁和 SparkBot 固件构建已通过;acknowledge 多仓储写入仍沿用现有非事务边界。

Closes 1024XEngineer#179
Linx 启动与提醒到点时,TLS、MCP 和音频初始化可能让内部 RAM 不再有连续任务栈空间。将唤醒控制、串口测试及提醒动作任务栈迁移到已启用的 PSRAM,并保持 WithCaps 创建与删除配对。

Refs 1024XEngineer#179
WakeTask 会在加载本地模型时调用关闭 cache 的分区映射,不能使用 PSRAM 栈。保持其内部栈属性,并在 VoiceSession 启动 TLS、MCP 与音频初始化前完成分配。

Refs 1024XEngineer#179
IM 与语音同时启用后,I2S 播放任务创建时内部堆最大连续块可能不足 16 KiB,导致 VoiceSession 启动失败。
将播放任务栈迁移到 PSRAM,并配对使用 WithCaps 创建和删除接口;ESP-IDF 6.0 构建已通过,真机复测继续进行。

Refs 1024XEngineer#179
设备可能在串口任务确认 BEGIN 前完成语音状态切换并发布 CAPTURE_READY。测试脚本从同一发送起点等待两个标记,避免因日志顺序竞争漏发 PCM。

Refs 1024XEngineer#179
Linx 与 IM 的网络初始化会碎片化内部堆,导致 I2S 双工通道偶发无法分配 DMA 描述符。先预留唤醒任务内部栈并预开固定 PCM 端口,再启动 Wi-Fi、TLS 和 Gateway;失败路径按双工设备顺序关闭预开端口。

Refs 1024XEngineer#179
记录通知提交、动作窗口、SSE 命令与结果统计,日志不包含 URL、凭据或 Action token。

Refs: 1024XEngineer#179
撤回 2929553,保留提醒播报后的语音 follow-up 交互。重连问题继续按 Linx 传输链路诊断,不通过关闭自动采集规避。

Refs: 1024XEngineer#179
主干新增提醒事件快照后与 1024XEngineer#179 的 v007 动作状态迁移冲突,导致 PR
处于 DIRTY 状态且 CI 无法启动。

保留事件快照、最终提醒文案和精确 action 幂等字段,将动作迁移顺延为
v008,并维持 Linx 释放共享串口后启动 IM provisioning 的资源顺序。

clang-format 18、完整 host 编译及 92 项 host 测试已通过。

Refs 1024XEngineer#179
ESP-IDF 6.0 将聚合字段遗漏警告提升为错误。为精确提醒动作的首次执行和幂等重放路径显式初始化空事件列表,不改变动作结果协议。

Refs 1024XEngineer#179
提醒任务 ID 是本地 SQLite 行号,数据库重建后可能从 1 重新分配。业务事件键加入稳定的 device_id,保持同一任务重试幂等并避免 Gateway 全局键冲突。

Refs: 1024XEngineer#179
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.05085% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ponents/voicelife_mcp/src/tools/audio_mcp_tools.cc 63.63% 1 Missing and 3 partials ⚠️
components/voicelife_board_esp/src/sparkbot_imu.cc 92.10% 0 Missing and 3 partials ⚠️
...ife_display_sparkbot/src/sparkbot_lvgl_renderer.cc 0.00% 3 Missing ⚠️
@@            Coverage Diff             @@
##             main     #362      +/-   ##
==========================================
- Coverage   88.70%   88.69%   -0.02%     
==========================================
  Files         215      217       +2     
  Lines       27129    27188      +59     
  Branches     8509     8553      +44     
==========================================
+ Hits        24066    24115      +49     
- Misses       1434     1438       +4     
- Partials     1629     1635       +6     
Flag Coverage Δ
cpp 88.69% <83.05%> (-0.02%) ⬇️
typescript 91.18% <86.53%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...oicelife/display_sparkbot/sparkbot_lvgl_renderer.h 100.00% <100.00%> (ø)
...time/include/voicelife/runtime/platform_assembly.h 22.22% <ø> (ø)
components/voicelife_board_esp/src/sparkbot_imu.cc 92.10% <92.10%> (ø)
...ife_display_sparkbot/src/sparkbot_lvgl_renderer.cc 91.89% <0.00%> (-8.11%) ⬇️
...ponents/voicelife_mcp/src/tools/audio_mcp_tools.cc 63.63% <63.63%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

已检查固定范围 3b7b8e751a6871b283616f6dd4fe24cb8eb70bed...290748156d2f8a2934ac28d22edc9315e479fb31,覆盖显示渲染、Linx 情感证据、MCP 音量工具和 Runtime 集成。情感事件链与音量工具 schema 逻辑整体完整;但新增回调存在一个运行时线程归属问题,见内联意见。git diff --check 通过;主机全量构建被仓库现有的 [[maybe_unused]] 数据成员警告在 -Werror 下阻断,未能完成 CTest。

schedule_operation_service_, schedule_reminder_service_.get());
if (!init_status_.ok()) return fail_startup(init_status_);
init_status_ = mcp::RegisterAudioMcpTools(mcp_server_, [this](int volume) {
SetVolume(volume);

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] 将音量修改转回 Runtime 事件循环

该闭包由 StartMcpWorker 的 MCP worker 线程执行,但它直接调用 SetVolume。该函数会同时写入 volume_、调用板级 SetOutputVolume、提交显示事件并读写音量 overlay 定时器状态;这些状态在普通按键路径中由交互事件循环线程维护。MCP 请求与按键/显示事件并发到达时会产生数据竞争,并可能从非显示任务触碰板级输出和 timer 状态,导致音量 overlay 或运行时状态损坏。请在此处只入队一个控制事件,让 SetVolume 在 Runtime 事件循环中执行(或为所有相关状态提供一致的同步机制)。

设备端不再用请求头 API 校验响应 Content-Type,改由 HTTP 状态和 SSE 解码器共同判断动作流。避免合法 SSE 被误关闭,确保提醒确认结果可以继续回传。

Refs: 1024XEngineer#179
回执未被网关受理时立即释放 SSE,让运行时按 operationId 重放未确认命令,避免 Gateway processing 状态与设备等待互相阻塞直至动作过期。

Refs: 1024XEngineer#179
SSE 提前收到 FIN 时不要误判为正常空流,保留提醒动作窗口并触发重连,确保稍后到达的确认命令仍能被设备消费。

Refs: 1024XEngineer#179
@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

阶段更新:BMI270 IMU 接入

这次把 SparkBot 的 IMU 型号先按官方实现定为 BMI270,依据是 SparkBot 资料和对应示例都使用 BMI270;代码不再把 QMA6100P/QMA7981 的旧 capability 注释当成确定硬件结论。

已完成:

  • 加入 espressif2022/bmi270: 1.1.0~2,显式声明 i2c_busesp_timer 等构建依赖。
  • 复用音频已经创建的 I2C0 master bus,使用 GPIO4/5;默认探测 0x68,兼容探测 0x69
  • 加速度计配置为 2G、200 Hz;采样任务每 5 ms 读取一次。
  • 摇晃判断使用加速度模长、慢速 EMA 基线、连续 3 个超阈值样本和 1500 ms 冷却窗口,避免静止姿态和单次噪声误触发。
  • IMU 事件只进入 BoardInputAction::kShakeDetected,当前 Runtime 只记日志 SPARKBOT_IMU_SHAKE detected=1,不会伪造唤醒、打断或改变语音状态。
  • 传感器不可用时只记录 SPARKBOT_IMU_UNAVAILABLE,不会阻断语音启动。

验证结果:

  • Host CTest:92/92 通过,包含新增 sparkbot_imu_test
  • ESP-IDF 6.0.2:esp32s3-esp-sparkbot 构建通过
  • 固件:build/esp32s3-esp-sparkbot/voicelife.bin,大小 0x2944d0,应用分区剩余约 8%。
  • 构建过程中修正了 BMI270 头文件依赖和 ESP-IDF 6 的设备释放 API(i2c_master_bus_rm_device)。

本阶段没有访问、读取或刷写实板。你后续测试时重点看三类明文日志:

SPARKBOT_IMU_READY sensor=BMI270 ...
SPARKBOT_IMU_UNAVAILABLE ...
SPARKBOT_IMU_SHAKE detected=1

摇晃后的“晕眼睛”或提示音暂不固化,等你确认实板型号、地址和阈值后再做产品行为决策。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

资源与干净基线复核

补充复核已完成:

  • 当前分支合并基点与最新 origin/main 一致(3b7b8e7)。
  • SparkBot 资源打包:11 个 GIF + common 14px 文本字体,打包脚本通过。
  • Python 资产校验:11 项运行通过,1 项按环境跳过。
  • 资源清单包含固定 common 字体;固件启动会输出 SPARKBOT_TEXT_GLYPH_CHECK ... xi=1,用来确认“矽”(U+77D2)实际有可解析字形。
  • 当前仍没有实板读写,因此 emoji 的最终物理屏幕效果和 BMI270 地址/摇晃阈值仍由后续 HIL 验证确认。

为每个提醒动作窗口创建独立的动作通道,并将并发 worker 限制为 4 个。空闲 SSE 不再阻塞后续提醒,断线时保留当前窗口并重连。

Refs: 1024XEngineer#179
@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

阶段更新:预合并 PR #359 到 PR #362

已在 PR #362 分支 fix/sparkbot-display-and-controls-20260825 预合并 PR #359

本地验证:

  • ./scripts/run_checks.sh:通过
  • Host CTest:95/95 通过
  • Python 测试:197 通过,1 个按外部凭据条件跳过
  • 公共 API 文档、格式、组件依赖图和固件 Profile 检查:通过

远端检查当前状态:

请先合并 PR #359main,再刷新 PR #362 的基线和检查。

…and-controls-20260825

# Conflicts:
#	components/voicelife_audio_esp/include/voicelife/audio_esp/esp_opus_codec_strategy.h
#	components/voicelife_audio_esp/include/voicelife/audio_esp/sparkbot_audio_budget.h
#	components/voicelife_runtime/src/runtime.cc
#	components/voicelife_schedule/include/voicelife/schedule/schedule_reminder_service.h
#	components/voicelife_schedule/src/service/schedule_reminder_service.cc
#	components/voicelife_voice/include/voicelife/voice/voice_ports.h
#	tests/host/schedule_reminder_action_execution_test.cc
#	tests/host/usb_serial_frame_router_test.cc
#	tests/python/test_im_wifi_credential_isolation.py
@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

PR #362 实验矩阵与验收计划

结论与范围

本评论只验收 PR #362 相对最新 origin/main 的显示、Emoji、SparkBot BMI270 IMU、MCP 音量工具、MCP 用户界面文案、唤醒确认显示和唤醒词改动。PR #359 合并带入的语音稳定性、提醒持久化、IM Gateway 等内容不在本次实验结论中,也不做泛化的长对话回归。

当前基线:

  • base:origin/main=ce064673b46ab76a1b7d6507297d0a2380ed3c69
  • 合并提交:265364ac0889e65dc319afbbb0bee5b7b83e08c1
  • PR head:265364ac0889e65dc319afbbb0bee5b7b83e08c1
  • 工作树:干净
  • 本评论发出时:尚未访问、读取或刷写实板;HIL 项均待后续明确通知后执行

自动化基线(已完成)

编号 实验 操作与输入 验收标准 当前结果
A-01 Host 全量回归 ctest --test-dir build-host --output-on-failure 所有测试退出码为 0 通过:95/95
A-02 变更格式检查 git diff --check origin/main...HEAD 无空白错误 通过
A-03 IMU 摇晃检测契约 静止基线;单次冲击;连续 3 个超阈值样本;冷却窗口内重复冲击;冷却后再次冲击;Reset() 静止/噪声不触发;第 3 个连续样本只触发一次;1500 ms 冷却内不重复;冷却后可再次触发;Reset 后重新建基线 通过:sparkbot_imu_test
A-04 Host 无硬件行为 在非 ESP 构建调用 SparkBotImu::Start()Stop()running() 无硬件时明确返回 kUnavailable,不得伪造运行状态 通过:sparkbot_imu_test
A-05 显示布局与滚动纯函数 检查 viewport_y、槽位边界、Emoji key 白名单、ScrollDurationForOverflow(0/1/1000) viewport_y=10;视口/状态栏/Emoji/正文不重叠;未知 key 被拒绝;不溢出不滚动,溢出时长限制在 3000 到 12000 ms 通过:sparkbot_lvgl_renderer_test
A-06 Linx 情感证据链 注入 emotion=happyllm 消息 Provider 产生独立 kLlmEmotion 事件;VoiceSession 只产生 llm_emotion evidence,不改变会话状态 通过:linx_provider_contract_testvoice_session_contract_test
A-07 MCP 音量工具契约 注册 self.audio_speaker.set_volume;调用 volume=30volume=101 30 触发回调并返回布尔 true;101 被范围校验拒绝 通过:mcp_server_test

实板验收矩阵(待通知后执行)

实板测试时保存完整明文串口日志;不使用本机扬声器播放。串口端口以现场枚举为准,优先使用 SparkBot 原生 USB-Serial/JTAG 的应用日志口,不把 CH340 bootloader 口当作应用日志口。每项均需要在日志和屏幕/硬件现象两侧同时验收。

编号 实验目的 步骤与输入 预期日志/现象 通过标准
H-01 BMI270 初始化与共享 I2C 总线 冷启动固件,保持板子静止,观察启动日志 SPARKBOT_IMU_READY sensor=BMI270 addr=0x68(or 0x69) sample_hz=200 threshold_mps2=4.5;不出现因 IMU 创建/删除 I2C 总线导致的音频初始化错误 出现 READY,且 Wi-Fi/应用正常启动;若硬件实际无传感器,只能记录 SPARKBOT_IMU_UNAVAILABLE,不得把缺件当成代码通过
H-02 静止与噪声抗误触发 上电后静置 10 秒,再轻触桌面一次 不出现 SPARKBOT_IMU_SHAKE detected=1 静止和单次轻触均无 SHAKE
H-03 连续样本摇晃触发 手持板子连续左右摇晃约 0.5~1 秒 出现 SPARKBOT_IMU_SHAKE detected=1;当前版本只记录事件,不伪造语音、不切换聆听状态 一次有效摇晃至少产生一次 SHAKE,且不导致重连、录音或 TTS
H-04 IMU 冷却与去重 触发一次后在 1.5 秒内继续摇晃,再等待超过 1.5 秒后再次摇晃 冷却窗口内不连续刷 SHAKE;冷却结束后新摇晃可再次记录 日志事件数量符合“单次窗口一次、冷却后可再次触发”
H-05 视口下移与槽位边界 冷启动、待机、聆听/播报各观察一次屏幕 所有内容整体比旧版下移;顶部状态栏、Emoji、正文不互相覆盖且不越界 实物无明显贴顶/遮挡;记录屏幕照片或视频
H-06 Linx Emoji 与中文字形 让 Linx 返回 happyconfused、未知 emotion;发送包含“灵矽/矽”的自然语言 常用 key 映射到受控本地资源;未知 key 回退安全表情;“矽”不显示方框/乱码 每个输入均能看到稳定表情;中文字符可辨识;记录对应 emotion_key/渲染日志
H-07 长句单向滚动 发送明显超过一行的儿童视角长句,连续观察完整滚动周期 SPARKBOT_TEXT_RENDER 的同一正文不被重复设置;滚动为单向 LV_LABEL_LONG_SCROLL,速度随溢出宽度变化但不一闪而过 一句话不重复循环、不突然加速;字幕可读且与当前文本一致
H-08 唤醒确认去重 分别说“你好牛牛”和“牛来”,每次只唤醒一次 屏幕顶部只出现一次“收到”;底部正文不再复制“收到!”;不因显示去重而多播报一次 每次唤醒视觉确认恰好一处;不得出现两处“收到”
H-09 唤醒词集合 逐个测试“你好牛牛”“别说了”“牛来”;额外测试单独说“牛牛” 前三个命令进入各自既有处理链;单独“牛牛”不再作为独立唤醒命令 三个有效词可触发;“牛牛”单独不触发;以明文日志和次数统计为准
H-10 MCP 内部摘要不泄漏 通过 Linx 触发一次日程查询/创建等 MCP 调用,观察工具执行期间和结果返回后的屏幕 日志可有 MCP_RESULT_DISPLAY_SUPPRESSED success=...;屏幕不显示“操作已完成”“查询成功”等内部摘要;模型自然语言回复仍按正常字幕/TTS 链路呈现 用户只看到自然语言结果,不看到 JSON-RPC/内部状态短句
H-11 MCP 音量真实调用 让 Linx 执行“把音量调到 30/70”;同时记录完整 MCP 收发日志 工具目录包含 self.audio_speaker.set_volume;收到 tools/call;日志出现 MCP_VOLUME_SET value=30/70;喇叭实际音量变化;不出现内部工具摘要 两个合法值均实际生效;工具名、参数、回包和硬件听感一致
H-12 MCP 音量边界 请求 0、100、-1、101(若 Linx 会发出非法值) 0/100 可执行;非法值被参数校验拒绝并保留错误日志,不崩溃、不重连 边界行为与 Host 契约一致
H-13 Emoji/滚动/唤醒组合回归 一次唤醒后发送含 Emoji 情感和长句的自然语言,观察到 TTS 结束 情感只更新显示,不改变会话状态;长句只滚动一次;“收到”不重复 仅验证 PR #362 的显示和证据链耦合,不把语音网络稳定性作为本项判据

明确不纳入本次验收

实板验收记录格式

每个 HIL 编号在后续测试中补充:时间、固件 commit、板号/串口、原始输入、完整串口日志路径、屏幕/硬件观察、是否通过、失败时最小复现步骤。只有日志、屏幕现象和验收标准同时满足,才将该项从“待执行”改为“通过”。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

本 PR 现关闭,后续实现已迁移到 #377

替代 PR #377 基于最新 main,关联 Issue #376,收敛以下问题:

  • 说话阶段固定显示 speaking.gif,不再被 Linx emotion 覆盖;
  • 移除用户可见的“准备中”,统一使用既有“聆听中”;
  • 长字幕改为显式单向左滚动,避免反向、重复和重启动画导致的加速;
  • 首句字幕等待 tts_first_audio,后续句段按 TTS 事件顺序更新;
  • 增加“矽”字形 fallback、Linx emotion 受控 key 和 ESP-IDF 工具链自动发现;
  • 已完成 SparkBot 实板百炼 3/3 回合测试,日志与详细结果见 🐛 fix(sparkbot): 稳定表情动画与字幕时序 #377

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

实现已由 PR #377 接替,关闭本历史 PR。

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.

[Display] 优化 SparkBot 显示可读性与设备控制

3 participants