# VirtualPartner **Repository Path**: Wilyu45/virtual-partner ## Basic Information - **Project Name**: VirtualPartner - **Description**: 嵌入式-LLM-硬件辅助 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-28 - **Last Updated**: 2026-10-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # VirtualPartner — AI 感知驱动的具身交互伴侣(嵌入式实现) > 方案定稿见 [solution.md](./solution.md)(**v1.1 分体式双节点**——中枢 ESP32-S3 放于一旁 + 机身 ESP32-C3 随玩具,ESP-NOW 直连,零 PCB/零焊接;**v1.2** 增补主机侧蓝牙耳机语音链路与直线执行器“抽拉”动作)。 > **硬件清单(配置/型号/逐器件规格/接线总表/引脚/通信协议/安全件/采购 BOM/变更记录)的单一权威源见 [hardware.md](./hardware.md)**——后续硬件变动只改该文件并追加其变更记录(solution.md 第九/十三章为选型推导背景)。 > 本仓库按方案第十二章路线推进:**全部工程量在嵌入式端(ESP32 双节点固件)**,推理主机是局域网内提供 OpenAI 兼容接口 + WebSocket 的黑盒(已就绪)。硬件采购前先用主机仿真把逻辑闭环验证。 ## 仓库结构 ``` firmware/ ESP32-S3 固件(ESP-IDF 工程) components/vp_core/ 核心逻辑组件 —— 纯 C,零硬件依赖,主机/目标同码编译: include/ 公共头文件 vp_contract.h 4.1 接口契约(上行摘要帧 / 下行指令,版本化) vp_reflex.h 2.2 反射层安全规则 vp_wave.h 3.7 波形渲染引擎 vp_stats.h 3.6 边缘滑窗统计 + 定点 Goertzel 频率提取 vp_json.h 极简 JSON 解析/构建 + 下行 schema 校验(5.3 输出格式) vp_link.h 4.3 机身链路二进制协议(ESP-NOW 直连:帧/指令/事件/配对/心跳) vp_bridge.h 4.3 中枢网关状态机(配对/保活/PING 门控/掉线判定;PC 仿真与真机中枢同码) vp_stroke.h 3.7 直线执行器(抽拉)位置流渲染(复用波形形状;“该在哪个位置”) vp_stroke_ctrl.h 3.7/2.2 抽拉控制律:归零状态机/去抖/计步积分/P 律/限位与堵转处置(硬件经 vp_stroke_io_t 回调注入,真机与 PC 桩同码) vp_thermal.h 3.6/2.2 加热闭环:下行目标温度(4.1 target_temp)驱动的 bang-bang + 42.0°C 硬顶钳位 + 0.5°C 迟滞 + 探头失效保护(反射层规则 2 下沉,默认关) vp_comm.h 4.2 中枢↔主机链路状态机(BOOT/PROVISION/CONNECTING/ONLINE/DEGRADED/SAFE_IDLE + 指数退避含抖动;硬件经 io 回调注入,PC 与真机同码) src/ 对应实现(定点运算,无 sin/fft 库依赖) sim/ 主机仿真后端:注入虚拟传感器样本,输出摘要帧/电机占空比 NDJSON test/ 黄金向量单元测试(assert 即用例) idf_stub/ ESP-IDF 最小桩声明头:让 PC 上的 gcc/MSVC 能真正解析真机固件源文件(仅语法检查,不参与固件编译) idf_app/ 中枢节点 ESP-IDF 工程(ESP32-S3,USB 常电):BRIDGE(ESP-NOW 网关)/COMM(vp_comm + WSS)/CTRL + 串口配网 CLI(wifi/host/scan/show/pair/unpair/forget/help,凭据落 NVS) node_body/ 机身节点 ESP-IDF 工程(ESP32-C3-DevKitM-1,随玩具;hardware.md §8.1 定案):REFLEX/ACTUATOR/STROKE/SENSOR/LINK + C3 HAL(STROKE 任务仅为 HAL 适配层,判断全在 vp_core/vp_stroke_ctrl)+ 串口 CLI(ssid/chan/scan/show/hub/unpair/forget/reboot/help,频道跟随走 NVS) tools/ mock-host/ Node 模拟主机:4.2 通信状态机、6.2 唤起评分、5.2 事件驱动决策 server.mjs 无头闭环:驱动 sim 进程做端到端验证(直连/wire 各 10 项、hub 12 项验收断言) real-host.mjs **主机侧真机边界**:等中枢固件拨入的 WebSocket 服务端(4.1 握手 + 1 Hz 心跳 + 同端口 /api/voice、/state),决策/LLM/降级复用同一套 lib;开发期用 server.mjs,到货后换成这个 smoke-wss.mjs M4 预演(18 项):HubLink(中枢 comm_task 的 Node 镜像)+ vp_sim --hub 过真传输栈,验握手/心跳/坏下行/静默传导/掉线重绑/语音入口 smoke-stroke.mjs vp_sim --stroke 通道级冒烟(9 项):脚本化时间线喂 stdio,验位移/堵转冻结/看门狗归零 transparency.mjs 4.3 透明性门禁:固定黄金时间线跑直连/--wire/--hub 三流,FNV-1a 逐字节比对 + 基线漂移告警(--diff 定位首分叉行,--update 登记新基线) web.mjs 浏览器调试面板(方案第十章"桌面/伴侣端"):node tools\mock-host\web.mjs [端口] → http://127.0.0.1:8080(端口被占时换一个,如 8085) lib/ engine.mjs(唤起评分+决策引擎,LLM 优先/降级规则桩)、llm.mjs(OpenAI 兼容客户端 + 5.3 prompt + schema 校验)、device.mjs(仿真器 stdio 封装)、ws.mjs(零依赖 RFC6455 服务端)、hub-link.mjs(中枢传输层替身) host.config.json LLM 配置(modelservice 地址/模型 ID/超时/最小间隔——换模型只改这里) voice/ 语音前台(Python;SenseVoice 包与模型都在本项目内,不碰 GPT-SoVITS runtime) voice_host.py 免唤醒 VAD 直进 / --ptt 回车触发 / --text 文本直通 → ASR → 主机 Agent → TTS asr_engine.py SenseVoice(默认,超低延迟,CPU 零显存)| funasr(备选,复用 GPT-SoVITS runtime)| faster-whisper(可选) tts_engine.py engine=auto:Qwen3-TTS 流式优先(:9881,音色常驻预提取)→ GPT-SoVITS 流式(:9880,基模+每次上传参考音频)→ edge-tts 兑底,逐级降级 qwen_tts_server.py Qwen3-TTS 0.6B 音色克隆流式服务(:9881,协议与 GPT-SoVITS 同构;官方包无流式,patch talker.forward 逐帧截 codec 自拼) voice.config.json 设备/VAD 阈值/ASR/TTS/参考音频/Agent 地址(全部配置化) run.ps1 GPT-SoVITS runtime 解释器启动入口 vendor/ sherpa-onnx(pip --target 安装在项目内,零新依赖进 runtime) models/ SenseVoice int8 模型 + Qwen3-TTS-12Hz-0.6B(均项目内自包含,不入库) venv-qwen-tts/ Qwen3-TTS 服务专用虚拟环境(torch cu 系 + qwen-tts,不入库;Windows 需 flash-attn wheel 才能达实用延迟,见进展清单) build.ps1 PC 侧构建脚本(gcc 优先,找不到时自动回退 VS/MSVC;产出 build/host/vp_sim.exe、vp_test.exe;附带真机固件语法门禁,用 idf_stub 桩头解析 4 个固件 .c) ``` ## 快速开始(无需硬件、无需编译器之外的东西) ```powershell # 1. 构建固件核心逻辑的 PC 版(需要任一 C 编译器:gcc 或 VS Build Tools 均可,脚本自动探测;本机以 MSVC 14.51 实测通过) tools\build.ps1 # 2. 跑黄金向量单测(反射层规则、波形/直线渲染、统计、JSON、链路与桥接) build\host\vp_test.exe # 3. 端到端闭环演示:模拟传感器 → 摘要帧 → 唤起评分 → 决策(LLM 可用时真实推理,否则规则桩)→ 下行指令 → 反射层校验 → PWM node tools\mock-host\server.mjs # 3b. v1.1 二进制链路模式(4.3 透明性验证):帧/指令/心跳走 vp_link 打包-解包全路径,验收数值与直连模式完全一致 node tools\mock-host\server.mjs --wire # 3c. v1.1 中枢桥接模式(4.3):帧/事件/指令全走 vp_bridge 状态机(与真机中枢同码) # 含无线中断故障注入(air off@100s → 中枢报 body:offline@~102.8s;air on@110s → body:online) node tools\mock-host\server.mjs --hub # 3d. v1.2 直线执行器(抽拉)冒烟:--stroke 启用位置渲染,可注入 stall/limit 故障验证 2.2 安全语义 # 脚本已封好一条时间线(会话→指令→爬升→堵转→清除→静默看门狗),同时支持 --wire/--hub node tools\mock-host\smoke-stroke.mjs # 9 项冒烟断言(三模式均需绿) # 3e. 手工冒烟(不进脚本时):直接向 vp_sim.exe 的 stdin 写 NDJSON 指令({kind:adv/press/imu/temp/stall/limit/cmd/hb/session…}) # 3f. 三模式透明性门禁(4.3 铁律):直连/--wire/--hub 输出流逐字节一致,且不得漂移基线 # 改 4.1 输出面时故意让它报 WARN:确认无误后 --update 写回新基线并同步 solution.md 4.1 node tools\mock-host\transparency.mjs # --diff 看首个分叉行;--update 登记基线 # 3g. 主机侧真机边界(M4 预演,零硬件):起 real-host → 用 WebSocket 连它,另一端接 vp_sim --hub 当“中枢里的固件” # 验的是 server.mjs 验不到的传输面:4.1 握手、1 Hz 心跳语义、坏下行耐受、主机静默→中枢停 PING→机身看门狗、掉线重绑 node tools\mock-host\smoke-wss.mjs # 18 项(--speed 1..4 调时间倍率,--keep 不拆链路) node tools\mock-host\real-host.mjs --no-llm # 单独跑:监听 :8790/vp 等中枢拨入(交互:say/silent/drop/show/quit) # 4.(可选)真实 LLM:先启动本机 modelservice(OpenAI 兼容 :8000),server/面板自动探测切 LLM 模式(不可达自动降级规则桩) # cd D:\agent\modelservice; python main.py # 另开一个终端 node tools\mock-host\server.mjs --llm # LLM 模式验收(12 项全过;qwen3.5-2b-realtime 流式两阶段:控制落地平均 ~0.32s、含语音总 ~0.49s) # 5. 浏览器面板 + 免唤醒语音前台(语音前台需 GPT-SoVITS runtime;主机 Agent 先起) node tools\mock-host\web.mjs 8085 # 面板(本机 8080 被系统端口代理占用,故用 8085;voice.config.json 已指向 8085) powershell tools\voice\run.ps1 --text "再快点" # 链路验证:文本 → Agent 决策 → 扬声器回应(TTS 自动选 GPT-SoVITS/edge) powershell tools\voice\run.ps1 --list-devices # 枚举音频设备(麦克风/扬声器编号写入 voice.config.json) powershell tools\voice\run.ps1 # 麦克风 VAD 直进(免唤醒);--ptt 为回车触发 # 到货后语音前台不改配置、只换个地址(real-host 与中枢 WS 同端口): # powershell tools\voice\run.ps1 --agent-url http://127.0.0.1:8790/api/voice # 6.(可选)Qwen3-TTS 流式服务(engine=auto 时在线即优先,掉线自动降级 GPT-SoVITS): # 需先在 tools\voice\venv-qwen-tts 装好 qwen-tts + flash-attn(Windows 无高效 sdpa kernel,未装时 RTF≈1.7 不可用) # 模型放 tools\voice\models\qwen3-tts\Qwen3-TTS-12Hz-0.6B-Base(约 2.5GB,不入库) & tools\voice\venv-qwen-tts\Scripts\python.exe tools\voice\qwen_tts_server.py # 另开终端,:9881 ``` 真机编译(硬件到货、装好 ESP-IDF v5.3+ 后): ``` # 工具链安装(推荐 Espressif 官方 EIM 图形安装器): # winget install Espressif.IDF --interactive # 或下载 https://github.com/espressif/idf-im-ui(选 ESP32-S3、v5.3+、Path 长度 C:\Espressif) . C:\Espressif\export.ps1 # 或 EIM 安装后的导出脚本 cd firmware\idf_app # 中枢节点(ESP32-S3) idf.py set-target esp32s3 idf.py -B build flash monitor # N16R8 分区与 PSRAM 已在 sdkconfig.defaults/partitions.csv 预配 cd firmware\node_body # 机身节点(ESP32-C3-DevKitM-1) idf.py set-target esp32c3 # 单槽分区(板载 USB-UART 线刷,无 OTA) idf.py -B build flash monitor ``` ## 当前状态(对齐方案第十二章) - [x] 第 1–2 周(上):**接口契约、反射层、波形引擎、边缘统计、JSON 校验、二进制链路、中枢桥接 —— 纯 C 核心 + 黄金向量单测(570/570)+ PC 仿真端到端闭环(直连 10/10、wire 10/10、hub 12/12 验收项)** - [x] 第 1–2 周(骨架):ESP32-S3 工程基础(vp_core 组件、A/B 双 OTA 分区、N16R8 sdkconfig)+ 浏览器调试面板 —— 待 `idf.py build` 实测(v1.1 前的旧六任务骨架已由中枢网关替代) - [x] 第 1–2 周(中):**v1.1 分体式改造(无需硬件部分已全部完成)**——`vp_link` 二进制 ESP-NOW 协议(含 CRC8/尺寸守卫,+28 断言)+ `vp_bridge` 中枢网关状态机(配对白名单/主机保活门控 PING/机身掉线 body:offline/online;仿真-真机同码,修正了无门控 PING 的传导链缺陷)+ 中枢/机身双 IDF 工程(四任务 + C3 HAL + 频道跟随)——IDF 侧待 `idf.py build` 实测;PC 侧 `--wire`/`--hub` 双模式已验证链路透明性(三模式输出流字节级一致,hash=f6d94c0f) - [x] v1.2 抽拉动作(PC 仿真先行):**`vp_stroke` 直线执行器渲染模块**——复用 `vp_wave_shape` 形状函数生成位置流(Q10:0=回收位/1023=满行程),八波形深度表 + 限斜率(升 1x/降 2x)+ 软限位 + escalate 深度递进(40%→100%/16s);反射联动:ceiling 降额同压抽拉深度、看门狗持续期显式停至回收位、堵转/限位冻结+清除后回位(2.2)——黄金向量 26 断言(含 engine 级看门狗全链路)全过(总 570/570),`vp_sim --stroke` 双通道 trace 冒烟通过(stall/limit 故障注入);真机 STROKE 任务(RMT 脉冲+DIR+TMC2209 DIAG/限位)随硬件到货接入 - [x] 正确性/实时性优化:**`vp_wave` 振动侧 ramp 整数截断修复**——旧式 `113*2*dt/1000` 在 1ms 步进下截断为 0 被 clamp 到 1,实际爬升速率 ~1000 Q10/s(非设计 226/s,违反安全规则 2.2“≤2 档/秒”约 4.4 倍)——与 `vp_stroke` 同源缺陷,现改**蓄放式余数累加**修正;同时新增**纯模块级 ramp 断言**(不经 engine 每 200ms apply 清零,堵住旧 engine 级断言的平凡通过弱点);黄金向量 +4 断言(总 570/570),三模式仍字节一致(hash e5e013b2→f6d94c0f,预期内 duty 上行段修正,行数 3301 不变);`--stroke` 冒烟不受影响 - [x] 正确性/实时性优化第二轮(抗调度抖动 + 时基独立 + 长跑有界 + 真机实时性): - **渲染抗抖动**:`vp_wave`/`vp_stroke` 的强度 ramp 累加器与抽拉位置限斜率改用按 `VP_RENDER_MAX_DT_MS`(20ms) 封顶的 `ramp_dt`——真机执行任务被 WiFi/NVS/故障处理抢占几十毫秒时,旧实现会把停摆时间折算成“补偿步进”一步补齐,瞬间突破 2.2「≤2 档/秒」;包络/载波相位仍用真实 dt(节奏不漂移,位置由限斜率滑过去) - **ceiling 恢复限斜率**:`vp_reflex` 新添 `ceiling_want`(策略期望上限)+ `ceil_up_acc` 蓄放 + `ceiling_slew`——**降即时、升按 500/s**(旧实现 P_JUMP 保持期满与看门狗解除时一步跳回 1000,而 ceiling 直乘占空比 → 绕过强度 ramp 使输出瞬间翻倍);与看门狗降档对称 - **反射层时基独立(安全洞修复)**:新增 `vp_reflex_update_at(..., now_ms)`,时基由调用方注入而非 `f->t_ms`——SENSOR 任务挂死时上行帧时间戳冻结,帧时基下静默时长永不增长 → 看门狗永不触发。同时传感器派生规则(力度急增/频率钳位)改为按 `f->seq` **边沿门控**,故本函数可安全地以 1ms 重复喂同一帧而不重复扣档/刷屏 - **Goertzel 状态有界化**:极点在单位圆上无衰减 → 持续共振(100Hz 振动几分钟)下 `s1` 可致 int64 越界;现每 16 样本做一次全通道一致右移(s1/s2 >>=1、power >>=2),通道间相对谱不变,长跑不溢出 - **链路与引擎**:`vp_link` CRC8 改 256B 查表(69B 帧内层循环 ~1100 → 69 次);`vp_engine` 文件作用域 static(pending/wire_cmd/hb_pending/去抖基准)全部收进结构体实例——旧实现下多实例互相覆写,新增双实例隔离断言;注入队列改 head/pop 环形弹出 O(1)(旧 `run_due` 每次 memmove 整块,批量注入下发 O(n²)) - **真机固件实时性**(`body_main.c`/`app_main.c`/`vp_board_body.c`):REFLEX/ACTUATOR/SENSOR 改 `vTaskDelayUntil` 绝对节拍(旧 `t += 1` + `vTaskDelay` 自由累加会长期漂出 1kHz/50Hz)并喂挂钟时基;下行 PING/CMD 不再直接写 `s_reflex.last_cmd_ms`,改 LINK 单写 `s_link_alive_ms`、REFLEX 折叠进反射层(`s_reflex` 写者唯一化,同时取消 LINK 上下文里的跨任务预钳位);反射输出→执行层改 `vp_safe_pack/unpack` 单字交接(12B 结构体赋值会被拆成多次访问而撕裂出“新 ceiling + 旧 intensity”组合);加热只在翻转时进 HAL;中枢 `bridge_task` 去“receive(1ms)+delay(1ms)”双延时改阻塞驱动 + 突发排空;MPU-6050 读失败 2s 退避 + 单次超时 50→20ms(保 SENSOR 节拍不被阻塞);1-Wire 临界区 ~960µs → ~550µs(尾恢复期开放中断,改善 ESP-NOW 共存) - **工程门禁**:新增 `firmware/test/idf_stub/`(ESP-IDF 最小桩声明)+ `build.ps1` 语法阶段——真机 .c 首次可在 PC 上被 `gcc -fsyntax-only` / `cl /Zs` 真正解析(两条编译路径均已实测零告警);此前固件改动只能靠肉眼审查(本轮即修正了一处重复声明硬错)。仍不等同于 `idf.py build`(不检查头文件版本差异与链接) - **回归**:黄金向量 570 → **597/597**(+27:抖动、ceiling 斜率、挂钟看门狗、Goertzel 有界、CRC 表与参考实现全长度比对、打包穷举回环、引擎双实例);三模式验收仍 10/10、10/10、12/12;新增 `tools/mock-host/smoke-stroke.mjs` 冒烟脚本并在直连/wire/hub 下均 9/9(静默窗口拉到 16s,以容忍中枢保活窗口 2.5s 内继续 PING 带来的等效静默缩短);透明性 hash 与本轮起点持平(**951f4300 / 3301 行不变**)——以上优化在黄金路径上字节级零回归(口径修正:本条之前的历史条目所记 hash=f6d94c0f 与本轮实测起点不一致,属当时未记录的流变更;本轮已验证三个二进制模式之间仍严格字节一致) - [x] 正确性/实时性优化第三轮(**真机 STROKE 落地 + 控制律下沉 vp_core** + 抽拉上报通道 + 透明性门禁脚本化): - **控制律下沉为纯 C 模块 `vp_stroke_ctrl`**(`vp_core/src/vp_stroke_ctrl.c`):归零状态机建立开环计步基准(上电即退向回收位限位,未归零前拒绝任何渲染输出)、`vp_stroke` 位置流→步数目标→**P 律**(误差按 60ms 领先量折频率,20–4600Hz)、限位/堵转/拍间停摆一律停机并作废基准(2.2 三条直线规则逐条对号)、限位/DIAG 三拍去抖、回收位限位在 target=0 时顺带再基准(非故障)。硬件经 `vp_stroke_io_t` 五个回调注入(home_hit/max_hit/stalled/set_rate/halt),方法论同 `vp_bridge`——**真机与 PC 桩跑同一份安全状态机**,故这些规则在硬件到货前已被 34 条黄金断言证伪;下沉当天断言即抓出一处**计步积分方向符号写反**(误差永不收敛、频率钉在上限直到撞满行程硬限位,实机必撞机)与两处回归(故障态抹掉渲染位置→清故障后满频率急退、丢步判据误用命令步数→每次起波形无病停机) - **机身 `stroke_task` 只剩适配层**(`body_main.c` 1ms、优先级 21 低于 ACTUATOR,−158/+46 行):节拍 + 读安全单字 + `vp_stroke_ctrl_tick` + 事件上报 + 发布抽拉单字,不含任何判断;换脚/换传感器只动 5 个 `PIN_ST_*` 宏与 HAL 五个函数,动控制律必须重跑 `vp_test` - **脉冲列驱动改 LEDC 变频方波(不用 RMT)**:一个周期=一步,50% 占空比,启停全靠占空比门控(不调 `ledc_stop`,它会解除通道-定时器绑定)——位带 busy-wait 在 4600 步/s 下单拍吃掉整个 1ms 窗口会挤掉同节拍的 ACTUATOR PWM,RMT 的 `item_num` 单位约定又无法在本仓验证;`ST_RATE_MAX_HZ=4600` 由渲染斜率反推(3072 Q10/s × 750步/1023 × 2 降向),**机械冲击上限钉在 HAL 层不信任调用方** - **独立安全守护**:脉冲列一旦启动就由硬件自由运行,任务死掉滑块会满速撞硬限位 → `esp_timer` 5ms 回调监控拍心跳,超 20ms 未续期直接 `vp_board_stroke_halt()`(esp_timer 上下文不依赖该任务调度) - **vp_core 支撑改动**(PC 可验证):`vp_safe_out_t` 追加 `watchdog_active`/`session` 并进单字位图 `[19]watchdog [23:20]session`(事件位是边沿触发且只降 ceiling,抽拉通道必须按**状态位**持续压制);`vp_safe_unpack` 增加解包侧越界钳位(坏字不得驱动执行器);`vp_link` 开 bit8-11 抽拉事件域 + `vp_link_ev_source()`(STALL/LIMIT/HOME/FAIL 上报**不破 4.1 契约、不 bump 链路版本**),三模式 detail 由同一函数派生使透明性由构造保证 - **去直读/竞态收敛**:SENSOR 不再碰 `s_reflex`(会话态与起点时刻都由 REFLEX 发单字);事件位图改 REFLEX/STROKE **各位域单写者 + 自旋锁原子取走**(旧实现下 LINK 的 `&= ~ev` 与两个写者的 `|=` 同一字读-改-写会丢位,1Hz 上报丢一次故障即漏报) - **工具链**:`tools/mock-host/transparency.mjs` 把只存在于临时命令行的三模式比对固化为带基线门禁(`--diff`/`--update`/基线漂移 exit 2);`idf_stub` 补 `ledc_set_freq`/`esp_timer_*`/`portENTER_CRITICAL` 与 GPIO11–21(换脚改动不再只能到实机构建才暴露) - **回归**:黄金向量 **636/636**(第三轮累计 +39:5 条契约/链路上报字段往返,34 条 `vp_stroke_ctrl` 控制律——首拍只断列、三拍去抖、半程开机归零耗时兼作方向证明、限位断线超时退避自愈、12000 拍满行程往复不碰硬限位且计步与机械对拍漂移 ≤1、频率上限钳位、计步偏乐观由硬限位兜底后重归零、同步带脱开丢步、单次堵转不作废基准、3 次堵转判卡死、看门狗状态位持续压制、拍间隔超票基准作废);透明性基线登记为 **4d75a3ce / 777 行**(与历史条目 951f4300/3301 不同脚本不同时间线,不可直接对比;现由脚本承载固定时间线);`--stroke` 冒烟直连/wire/hub 均 9/9,`server.mjs` 三模式 10/10・10/10・12/12,4 文件真机语法门禁全绿 - **待实机验证的假设**:`ledc_set_freq` 返回值语义(未变化时部分版本返 0,HAL 已按请求值兜底)与 LEDC 频率精度;hardware.md §4.2 已补三条装配约束(GPIO2/8/9 strapping 外拉 10kΩ、满行程限位装在软行程之外、上电自动归零,其中“不碰硬限位”与“计步偏乐观由硬限位兜底”两条已由 PC 黄金向量固定)、并**修正一处步数换算事实错误**(750 步是 200 步/圈 × 30mm÷8mm 导程的**整步**换算,非 1/16 细分之积——按 1/16 接线实行程只剩约 1.9mm,故拨码必须固定 1/1),§8.1 登记机身 GPIO 预算定案(选 D:机身换 ESP32-C3-DevKitM-1,直线五脚重排 STEP=GPIO8/DIR=GPIO9/DIAG=GPIO5/限位A=GPIO2/限位B=GPIO19,详见 hardware.md 变更记录) - [x] v1.2 加热子系统(**下行目标温度接口,改 4.1 契约** + 控制律下沉纯模块):旧实现下 `vp_reflex` 上电即 `heater_on=true`、41.5°C 自动恢复,反射层成了加热唯一的自主决策者、无任何接口能叫它停(放置态也会把内胆烧到 42°C)。本轮改为**主机/LLM 下发意图温度**: - **契约新增 `vp_cmd_t.target_temp_dC`**(`vp_contract.h`):`VP_CMD_TEMP_UNSET(-1)`=本条不改目标 / `0`=关 / `>0`=目标 0.1°C 定点,反射层钳到 ≤42.0°C。经 `vp_json.vp_cmd_from_json` 解析可选 `target_temp`(缺置哨兵、未知字段忽略,向后兼容);vp_link 下行是 `sizeof(vp_cmd_t)` 的 struct memcpy,三模式透明性由构造保证(无需改链路版本号) - **新增纯 C 模块 `vp_thermal`**(`vp_core/src/vp_thermal.c`):把规则 2 的加热自主决策下沉——目标钳位到 ≤42.0°C 硬顶(2.2 不可被下行抬升)+ 0.5°C 迟滞 bang-bang + 探头失效(负读数)保守关断;**加热默认关**(无下行目标不自热)。无 io 回调(heater 仍是 bool,经 `vp_safe_pack` bit18),比 stroke_ctrl 更轻,真机与 PC 同码。`vp_reflex` 规则 2 改为消费:cmd 落地时 `set_target`、每拍 `tick` 产 `heater_on`(42°C 硬顶与 OVERTEMP 事件边沿去重);engine/body 初始安全字 heater 一律改默认 false - **主机侧**:`engine.mjs._finalize` 按会话阶段注入 `target_temp`(预热/交互/高唤起→40.0°C,待机/冷却/后戏→关),LLM 未显式给出时的兵家必争之地;`server.mjs` 过温/降温验收断言在新模型下仍 PASS(heater:off→over temp 断、heater:on→降温恢复) - **回归**:黄金向量 **636 → 655/655**(+19:`test_thermal` 16 断言——目标钳位/迟滞带两侧/硬顶上升沿去重/退出切断区恢复控制/探头失效保护/负目标就近取 0;`test_json` +3——缺省 UNSET/显式解析/越界保留原值交反射钳位);因加热默认关且契约新增字段,透明性基线变更为 **4d75a3ce → 6531c3d5 / 777 → 778 行**(黄金时间线两条 cmd 新增 target_temp 400/0 以覆盖新字段三模式编解码,+1 行=heater:on 事件),三模式仍逐字节一致;`server.mjs --no-llm` 10/10,4 文件真机语法门禁全绿 - [x] v1.2 中枢↔主机链路落地(**本轮:把“等硬件”的那一层先全部写死并验完**): - **`vp_comm` 纯 C 状态机**(4.2):凭据/拨号超时/指数退避含抖动/DEGRADED(1s)/SAFE_IDLE(3s)/host_alive 传导,硬件经 `vp_comm_io_t` 三回调注入;确定性 xorshift32 使退避序列可断言;`set_creds` 是唯一可从任意态拉回拨号的入口,但只记事实、动作仍在 `tick` 里做(改主机地址不作废旧连接、也不白等退避窗口这两条都有断言钉住) - **4.1 补订握手机制**(设计增量,不是实现细节):LLM 是事件驱动的,健康但无事可说的主机若被“有没有指令”当作掉线,就会被中枢判死断链——因此新增主机 1 Hz 心跳帧 `{"v":1,"hb":1}`(中枢先分流、绝不进指令 schema)与连接通告 `hello`(带 hub MAC / 机身配对态 / 心跳周期);`vp_is_heartbeat_json`/`vp_hello_json` 在 vp_core,+14 条黄金断言 - **中枢固件接电**:`vp_board.h/.c` 把 NVS 凭据(`vp_net`:ssid/pass/uri/cacert)+ WiFi STA + `esp_websocket_client`(**`disable_auto_reconnect=true`**,重连唯一归 vp_comm)包成 HAL,异步事件一律经队列由 `COMM` 任务翻译;分片重组 + 行切分 + 超长整条丢弃;`app_main.c` 的 `comm_task` 四段式(排空事件→hello→tick→收包)+ 最低优先级串口配网 CLI(配网不能从反射层/实时预算偷走周期) - **主机侧真机边界**:`lib/ws.mjs`(零 npm 依赖的 RFC6455 服务端:握手/文本帧含分片/ping-pong/close/掩码解异或)+ `real-host.mjs`(hello 绑定会话 + 1 Hz 心跳 + 4.1 白名单下行 + 同端口 `/api/voice`、`/state`,决策全复用 lib/engine+llm)+ `lib/hub-link.mjs`(中枢 comm_task 的 Node 镜像)+ `smoke-wss.mjs`(**18/18**:握手/保活/坏下行/前向兼容/感知驱动决策/静默→停 PING→看门狗三段传导/掉线重绑/语音入口) - **机身 NVS 化**:家庭 SSID 与台架频道存 `vp_net`(原写死的 `VP_HOME_SSID="CHANGE_ME"` 删除),换路由器不再重烧;扫不到 SSID 时回退上次固化频道(路由器断电≠频道错);机身串口 CLI 与中枢同形状 - **门禁与两个提前堵掉的配置坑**:`idf_stub` 补 `esp_websocket_client.h` 与 nvs u8/esp_restart/wifi 扫描声明(4 文件语法门禁仍全绿);中枢 `sdkconfig.defaults` 原本**缺 `CONFIG_PARTITION_TABLE_CUSTOM`**(partitions.csv 写了也不生效,默认 1MB 单 app 分区装不下带 WSS 的固件)、机身原本会默认使能 **USB-Serial-JTAG 次控制台**(把硬限位所在的 GPIO18/19 归给 USB PHY)——两者都只会在到货当天才响,已在配置里封死并记进 hardware.md 变更记录 - **回归**:黄金向量 **655 → 702/702**(+47:`test_comm_link` 14 组 + `test_link_frames` 14 条 + 改配/忘记凭据路径);`transparency.mjs` 三模式仍 **6531c3d5 / 778 行**逐字节一致(本轮未污染 4.1 输出面);`server.mjs` 三模式 10/10·10/10·12/12;`smoke-stroke.mjs` 9/9;`smoke-wss.mjs` 18/18 - [x] 主机侧真实化 + 免唤醒语音链路(并列支线,方案第五/十章落地):**mock-host 接入本机 LLM**——`--llm` 验收全过(LLM 决策成功 9 次/降级 0/平均 5.3 s,含 stale 指令防丢弃端到端验证);不可达自动降级规则桩,规则模式零回归(单测 570/570 + 直连 10/10 + wire 10/10 + hub 12/12)。**`tools/voice/` 语音前台实测通过**——`--text` 与麦克风 VAD 直进(免唤醒)→ funasr(本机 CPU、零显存)→ Agent → GPT-SoVITS(基模+每次上传参考音频)/edge-tts 自动降级;现场复现 LLM 超时→规则降级路径。已知限制:免唤醒直进对环境噪声敏感(阈值见 voice.config.json),LLM 与 TTS 并发时 9B 延迟接近超时上限(换小模型仅改 host.config.json) - [x] 超低延迟主机侧方案(2B 实时模型 + 流式全链路):**LLM 流式两阶段**——qwen3.5-2b-realtime SSE 增量解析,控制字段就绪即下发设备(先动)再补语音文本;`--llm` 验收 12/12(控制落地平均 316 ms、含语音总 489 ms,语义方向正确)。**ASR SenseVoice**(sherpa-onnx int8、CPU 2 线程,5.6s 音频解码 ~260 ms;包与模型都在本项目 `tools/voice/vendor`、`models/` 内,GPT-SoVITS runtime 零污染)。**TTS GPT-SoVITS 流式**(streaming_mode=3 边收边播:首包 ~0.5s 出声 vs 整段合成 8.4s,音色克隆保留)。端到端实测:语音指令到设备动作 ~0.3s、到出声 ~1.0s(旧 9B 非流式方案 ~16s)。实测发现并已修复 modelservice(D:\agent\modelservice,原文件留 .bak)两个缺陷:① response_format=json_object 挂 GBNF grammar 约束采样慢 ~10x(同长度 1985ms vs 246ms)——改为 opt-in 开关 JSON_GRAMMAR_ENFORCE(默认仅提示词约束;本仓库 llm.mjs 亦早已弃用该参数,靠加固 prompt+宽松解析+规则桩兑底),修复后同类请求 424ms;② in-flight 计数在客户端断开时泄漏(async release 在已取消任务上下文里 await 立刻再抛 CancelledError、减计数永远执行不到 → 死锁模型自动淘汰)——release 同步化后断开×3 计数归零已验证。修复后 --llm 验收 12/12(控制落地平均 296ms、含语音总 488ms)。另:2B 与 GPT-SoVITS 同卡并发长音频合成时显存峰值叠加偶发双双崩溃(本日现场一次,重启即恢复;期间 TTS 自动降级 edge / LLM 自动降级规则桩均现场验证零中断)。 ⚠️ **存疑/待复测**(2026-09-29 业主复盘指出):当时两个服务已被手动关闭,现场观察到的“崩溃”很可能是误判(失败模式实为服务不可用触发自动降级);本机仅一张 RTX 4060 8 GB,需按 [s2s-research.md](s2s-research.md) 第七章第 2 项实测 LLM+TTS 并发的真实常驻/峰值后再定结论;调研期间已将本仓库相关决策改为不依赖此记录 - [x] 流式增量 ASR + 判停缩短(延迟优化 A-3 落地):录音期间 sherpa-onnx streaming zipformer-zh int8(CPU 2 线程、零显存,新增 `asr.stream`,模型在 `tools/voice/models/`)边采边解,判停即取增量文本;SenseVoice 退回兜底(增量空结果自动离线补解,`--no-stream`/模型缺失/加载失败均回退旧路径零中断);`silence_ms` 600→450。文件验收 `stream-check.py` 3/3:首个部分结果 0.5–1.5s 音频处、**判停后尾解码 0–150 ms(旧此处完整 260–370 ms)**、背压压测(5x 实时+主线程满载)仍 150 ms 追平;合计净省 **~700 ms**,控制通路零改动(实测数据回填 s2s-research.md 第六章 A-3/第七章第 5 项);待麦克风现场复测端到端出声延迟 - [x] Qwen3-TTS 0.6B 音色克隆流式接入(更低延迟 TTS 调研落地,**结论:不达标,已摘出主链**):**管线已打通**——官方 qwen-tts 包无流式,`qwen_tts_server.py`(:9881)patch talker.forward 逐帧截 12.5Hz codec 帧攒段重 decode(解码器全因果,重解前缀与整段解码逐样本一致),协议与 GPT-SoVITS 同构、ICL 音色启动预提取常驻、code_predictor 手写采样循环替掉 HF generate 开销。**性能实测(已装 torch 2.7.0+cu128 + flash-attn 2.7.4 并确认 attn=flash_attention_2 生效)推翻 flash-attn 前提**:talker 单步微基准 flash≈sdpa(41–85ms,噪声内);真实管线仍 ~262ms/帧、首包 2.5–3.4s、RTF>1——根因不是 attention kernel,而是 code_predictor 每帧 15 次子 forward + 每段重解码整段前缀的超线性成本(微基准 decode L=66→69ms、L=306→244ms 证实),属架构级、非 kernel 级。**处置**:`voice.config.json tts.qwen3tts.enabled=false` 从 auto 链摘出,GPT-SoVITS(~471ms 已达标)回主力;服务代码保留作实验位(要启用改 enabled=true)。**级联 vs 端到端 S2S 已专项调研(结论:主机 RTX 4060 8 GB 下不引入端到端——MiniCPM-o 4.5/Step-Audio 2 mini/VoiceChat 11B 仅凭模型体积就已超预算),实测数据/显存预算表/验收单见 [s2s-research.md](s2s-research.md)** - [x] 场景化延迟压缩(A-3 增补 + A-5 门控版落地):针对“指令要快切、对话防腰斩、等待要有反馈”三种使用场景——①**场景自适应判停**:增量文本尾巴落在助词/连词(“和/但/呢/的…”)上判静音未尽,延长 `silence_ms` 450→`thinking_ms` 900 重新计时;短指令尾巴是实词则照常快断;②**首句短答**(`tts.ack`):检测到人声即后台两段式预合成(`tts_engine` 拆 prepare/write)一句短反馈,决策快(≤`latency_gate_ms` 500)finalize 跳过不打扰主语音,决策慢(LLM/降级)先出声占位——合成窗口整个藏在“说话+判停”里;噪声误触发/ASR 空结果一律 cancel,短答绝不因环境噪声出声(防扬声器→麦克风反馈循环);③**TTS 启动同步预热**:进监听前先合成一次不出声(首次 CUDA 冷启动 ~6s 不算在用户头上;改后台并发实测会挤 GPT-SoVITS 串行队列把短答首包拖到 563–843ms 甚至超时丢失)。验证:Ack 状态机离线单测 6/6(快跳/慢播/等就绪/cancel/合成失败/门限内判快);集成双场景实测——规则 Agent 4ms 决策无短答直接主语音,拖 1.5s 决策则 `[ack] 短答先行出声(首包 755 ms)`+主语音随后;`--no-stream`/`thinking_ms` 缺省/`ack.enabled=false` 均一键回退旧行为(回填 s2s-research.md A-3 增补/A-5 落地);待麦克风现场复测 - [x] 成人亲密场景对齐(隐私 + 安全词时序):业主明确使用场景包含成人亲密接触/发生关系——两个前轮延迟优化项在此场景下翻转为隐私/安全隐患,已按此定位修正:①**隐私红线**(`tts.local_only` 默认 true)——本地引擎(GPT-SoVITS/ Qwen)均不可用时**宁可不出声也绝不降级 edge-tts(微软云端)**,不把 ASR 出的私密文本上云(显式 `engine=edge` 仍可用,为非亲密时段留口);`voice_host` 落盘的原始语音 `_wipe_wav` 识别后即删,不在磁盘残留亲密音频。②**安全词优先**:`engine.mjs` 的 `/停|住手/`→cooldown 是反射级安全词,`handle_text` 中 `act in (cooldown, stop)` 或语音含“停”时**短答 ack 强制 cancel不抢占声道**(停就是停,不先“嗯”一声)。未改动:反射层七条硬规则(42°C 顶/ceiling 缓变斜率/看门狗归零/30min 强制冷却/cooldown 拒 escalate)与判停逻辑(业主选择安全词不短路 `silence_ms`)。业主已确认本场景不会出现耳语/低音量→`rms_threshold` 漏检安全词的风险排除 - [x] 场景化延迟压缩第二批(可枚举回应特化):利用本场景“指令重复、回应可枚举”(`engine.mjs` 规则 persona 仅 ~8 句固定话术反复出现)——①**固定话术内存音缓存**(`tts.cache` 默认开):`TTSEngine` 加 text→PCM(归一 float32) 内存层,流式播完顺手回灌、`prepare` 命中直接 RAM 出声(热句 TTS 首包 471ms→~0ms);仅存设备自己 persona 语音、非用户输入,**纯内存不落盘、进程退出即清**,且缓存句在 TTS 服务宕机时仍能出声(与 local_only 互补);`cache_max` 上限兑住 LLM 动态长句不无限增;②**完整指令早断句**(`vad.command_words`,默认“最大/最小/加热/冷却”):增量文本规范化后**精确相等**命中即结束段、不等满 `silence_ms`(省 ~450ms);仅精确相等命中防“最大一点”误断,**安全词“停/住手”故意不入集**(尊重上轮“不短路判停”),日志标 `早断·指令`。验证:缓存+早断离线单测 6/6(首次合成/二次命中缓存/重复 0 再合成/新文本各自合成/精确命中早断/非精确不误断);stream-check 回归 3/3、尾延迟 0ms。(④ 投机并行下发已在第三批落地,见下条;逐句 TTS 流式 SSE 在 ④ 用 dry 规划把整个 LLM 决策藏进录音窗口后收益趋微,留作 backlog) - [x] 场景化延迟压缩第三批(**④ 投机并行下发,含主机侧契约改造**):解掉第二批挂起的 ④——根因是 `/api/voice` 一被调用就驱动设备,投机即提前误执行(亲密场景不可接受)。本轮在主机侧把“决策”与“下发”解耦:`engine.mjs` 的 `onVoice(text,frame,now,{dry})` 只决策不下发(dry 下连 LLM 两阶段的 `onCtrl` 提前下发也被吞),新增 `commitCmd(res)` 用已规划 cmd **重新盖发送时刻帧号**后补下发一次;`web.mjs`/`real-host.mjs` 的 `/api/voice` 加 `mode=plan`(只决策)/`mode=commit`(补下发)。前台 `SpecPlanner`:`capture_segment` 在收尾静音过 `pre_dispatch_ms`(300) 且增量非思考尾时后台 `agent.plan(partial)`(设备不动),段末 `settle` **仅当末文本与投机文本规范化相等**才 `commit` 落地并复用决策、否则丢弃(dry 从未下发,无误动)回退常规 `voice`;命中则取消短答占位(段末即有答,占位反成多余回声)。把 LLM 决策耗时(~1.5s)与“说话+判停”重叠,规则/LLM 均安全(规则只是多一次 commit 往返、无副作用)。验证:engine 侧 dry 绝不下发/commit 恰好一次/LLM 两阶段 dry 也不误动/ref_seq 重盖(11 项),SpecPlanner 命中·未命中·规范化·plan异常·commit异常·reset·start去重(12 项),HTTP 三模式端到端;`node --check` 全绿。回退:`vad.speculate=false` 一键关(回到调用即执行单段);未触碰 C 层 4.1 输出面,transparency 基线(6531c3d5/778 行)不受影响 - [x] LLM 人设与场景(人格化 `speech`,配置驱动零回归):把 5.3 提示词的“角色段”从写死改为 `host.config.json` 顶层级配置——新增 `persona`(`name`/`identity`/`relationship`/`speaking_style`/`never_say`)与 `scenario`(`setting`/`context`/`goal`),由 `llm.mjs::buildSystemPrompt(persona, scenario)` 拼进系统提示词。**只塑造 `speech` 的语气与内容**(第一人称、昵称、情绪随唤起程度变化),传感器词表/决策规则/输出 schema 三段逐字节不变,`act`/`pattern`/`intensity_delta` 决策语义与安全红线零回归;角色段末尾强制“用户语音‘停/住手’永远是 cooldown”。**零回归**:`persona`/`scenario` 缺省或字段全空时退回中性“感知-回应引擎”,与旧版逐字节一致。三处装配点(`server.mjs`/`web.mjs`/`real-host.mjs`)统一经 `llmConfigFrom(fullCfg)` 组装,换人设只改配置不动代码(默认人设面对齐成人亲密场景、私下使用)。**增强:多套可切换人设 + 分阶段场景**——`persona` 支持 `profiles`(命名集)+`active`(选中),`resolvePersona` 解析(键名兼作 name、缺 active 取首个、扁平写法向后兼容),预置“晚星”温柔/“阿芷”俏皮两套,改一个 `active` 即整体切换;`scenario.stages` 按会话阶段(idle/preheat/active/high/afterglow+default)给氛围,`formatUserMessage(ctx, scenario)` 按 `ctx.sessionStage` 动态追加一行 `[此刻氛围]`(静态基座场景定“人是谁”、阶段氛围定“此刻到哪阶段”,同样只塑 `speech` 不改决策)。验证:临时 prompt 探针两轮共 **30/30**(第一轮 11:默认逐字节一致/人设生效且尾段不变/部分字段不崩/装配合并;第二轮 19:profiles 解析与 active 切换/缺 active 取首/扁平兼容/分阶段氛围命中·回退 default·无 stages 不塞行/单参向后兼容/client 生效);`--no-llm` 基线下五套采购前门禁全绿(build OK、vp_test 702/702、transparency 6531c3d5/778 三模式、smoke-wss 18/18、smoke-stroke 全过) - [ ] 第 1 周(硬件到货后):ESP-NOW 链路验证(M1)→ 传感器接入(M2/M3)→ 执行器与双节点闭环(M4),流程与接线见方案 9.6 / 9.7 与第十二章;到货前的预验见 hardware.md §10.1 - [x] 第 5–6 周(**软件侧完成**):`vp_comm` 状态机 + 中枢 WSS/NVS/串口 CLI + 主机侧 `real-host.mjs` 真机边界均已落地,并由 `smoke-wss.mjs` 在无硬件环境下跨传输栈跑过 18 项(含“主机静默→中枢停 PING→机身看门狗”三段传导);**待实机**:真板替下 HubLink、`idf.py build` 首次编译、路由器频道跟随实测(不再“复用 xiaozhi-esp32 通信框架”,而是直接用托管组件 `espressif/esp_websocket_client`,理由见 solution.md 4.2“重连唯一归属”约束) - [ ] 第 7 周起:中枢本体氛围音(I2S,可选件)、个体校准写 NVS、App;唤醒词已在主机侧软件实现(方案第十一章 v1.2),固件不做 WakeNet ## 安全红线(从第一行代码起生效) 方案 2.2 的七条反射规则固化在 `vp_reflex.c`,**不可被任何下行/远程指令覆盖**; 所有 LLM 下行指令必须先过 `vp_schema_check` + `vp_reflex_sanitize_cmd` 才允许落地执行 (真机:两者都只在 REFLEX 任务内发生,`s_reflex` 写者唯一;LINK 只负责把原始指令入队并记录链路活性)。 同一前提下还兜住三条易被忽略的不变量:**斜率**(强度与 ceiling 都只能缓变,恢复方向同样限斜率)、 **时基**(反射层安全计时用挂钟,不依赖传感器帧是否还在到)、**发布**(跨任务交接必须是单字,不得出现新旧字段混读的撕裂组合)。