From 3e773debc9616544ccea9147784f37ab79f6d33a Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Fri, 14 Aug 2026 07:52:35 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20pybot=C3=97actpred=20=E5=AE=A1=E8=AE=A1?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E4=B8=8E=20actpred=20=E7=A0=94=E7=A9=B6?= =?UTF-8?q?=E6=96=B9=E5=90=91=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/reviews/pybot_actpred_AUDIT_20260814.md:双模块审计(15 项既有 修复确认 + 新发现 P1×4/P2×6,含 P1-2 闭环帧源语义、P1-1 节拍失效研究) - docs/reports/actpred_research_directions_20260814.md:RNN 思想四切入点 (C 残差/B 多步/A GRU 记忆 token/D 全 recurrent)+ R1~R13 分层演进路线 - 迁移文档 §9 更新为已实现:进程内闭环 --- .../actpred_research_directions_20260814.md | 166 ++++++++++++++++ .../bot4cj_python_migration_dev_doc.md | 21 +- docs/reviews/pybot_actpred_AUDIT_20260814.md | 188 ++++++++++++++++++ 3 files changed, 368 insertions(+), 7 deletions(-) create mode 100644 docs/reports/actpred_research_directions_20260814.md create mode 100644 docs/reviews/pybot_actpred_AUDIT_20260814.md diff --git a/docs/reports/actpred_research_directions_20260814.md b/docs/reports/actpred_research_directions_20260814.md new file mode 100644 index 0000000..7e389c3 --- /dev/null +++ b/docs/reports/actpred_research_directions_20260814.md @@ -0,0 +1,166 @@ +# actpred × pybot 研究方向报告:循环神经网络思想与机器人学习演进路线 + +日期:2026-08-14(Asia/Shanghai) +范围:`actpred/`(动作预测,大脑)与 `pybot/`(机器人运行时,身体) +类型:技术研究方向调研(`advisory`;涉及部署可行性的判断标注 `verify_pending`,需 Hailo 实测确认) + +## 0. 报告说明 + +本报告源于两个递进的调研问题:(1)动作预测能否引入更多循环神经网络(RNN)思想;(2)在此之上的整体研究方向。报告先剖析 actpred 当前建模范式的本质与痛点,再给出 RNN 思想的四个切入点,最后组织成分层的演进路线图。所有判断基于当日代码基线(进程内闭环已落地、Hailo NPU 部署链已实机验证)与项目现实约束。 + +## 1. 现状建模范式与核心痛点 + +### 1.1 当前是"单步窗口预测",模型无跨步隐状态 + +actpred 的 `ActionPredictor`([model.py:89-147](../../actpred/model.py#L89-L147))每步独立地用 +`[F帧图像, H步动作, PRED]` token 序列做一次性 Transformer 注意力融合,取 PRED 位置回归下一动作。 +关键事实:**模型本身没有任何跨步隐状态**,跨步记忆完全由 H 步(默认 5)原始动作向量窗口承载。 + +训练侧是 teacher forcing([dataset.py:112-117](../../actpred/dataset.py#L112-L117)): +`action_history` 取真实历史,`target` 取下一真实动作,单步监督。 + +### 1.2 由此产生的三个真实痛点 + +| 痛点 | 根因 | 后果 | +|------|------|------| +| 长程依赖被截断 | 记忆容量 = H 步原始动作 | 超过 5 步的运动模式(起步→加速→匀速的长惯性)在窗口外丢失 | +| 动作不平滑 | 每步独立回归,无惯性先验 | 预测抖动,对物理舵机是磨损/损坏风险 | +| exposure bias | 训练用真实历史、闭环推理用自预测历史 | 分布失配,误差随步数累积,闭环易发散 | + +### 1.3 决定性约束:Hailo NPU 部署链 + +[model.py:94](../../actpred/model.py#L94) 明确"除 batch 外全部维度静态,保证 ONNX/NPU 部署友好"; +[export_deploy.py](../../actpred/export_deploy.py) 整套 DFC 改造(图像输入拆帧、AvgPool 替换、 +注意力手工展开)都围绕**静态图**。因此**循环算子(GRU/LSTM)是可行性的分水岭**—— +DFC 对循环算子的支持通常弱于静态卷积。这一约束直接决定下文各方案的取舍。 + +## 2. RNN 思想的四个切入点(按部署链冲击排序) + +### 方案 C · 残差/惯性先验:预测 Δaction(零参数,立即见效)⭐ + +把预测目标从绝对动作改为增量:`â_t = a_{t-1} + Δ`,模型只学 `Δ`。 + +- **本质**:等价于对角权重固定为 1 的线性 RNN(积分器),是 RNN 思想的极简形式。 +- **收益**:输出被惯性锚定,天然平滑;训练更易收敛;舵机抖动大减。 +- **部署影响**:零(只改 dataset 的 target 构造 + predict 逆变换时加回 `a_{t-1}`)。 +- **代价**:极小。**强烈建议无论选哪条路线都先做这条**。 + +### 方案 B · 训练目标:多步展开 + scheduled sampling(治 exposure bias) + +不改模型,只改训练: +- dataset 一次取 K 步连续样本而非单步; +- 第 1 步用真实历史,第 2 步起用模型自己的预测回传(scheduled sampling),loss 对 K 步累加。 + +- **收益**:直接消除训练/推理分布失配,闭环稳定性大幅提升。 +- **部署影响**:零(模型结构不变,已部署 Hailo 推理链不动)。 +- **代价**:只改 train.py 训练循环 + dataset 采样逻辑,风险最低。 + +### 方案 A · 混合架构:动作历史用 GRU 压缩(进阶) + +`act_proj` 后不接 H 个独立 token,而是过一层 GRU,取最终隐状态压成 **1 个"动作记忆 token"**, +与 F 个图像 token + PRED 一起进 Transformer: + +``` +现状: H 个动作 token ──────────────────┐ +改进: H 步动作 → GRU → h_final (1 token) ├→ [img×F, mem×1, PRED] → Transformer +``` + +- **收益**:跨步记忆容量与 H 解耦(GRU 隐状态压缩全部历史),长程惯性可建模。 +- **部署影响**:token 序列从 `F+H+1` 变 `F+2`,更短更静态;唯一新增算子是 GRU—— + **关键风险点**,需先验证 Hailo DFC 对 GRU 的支持(可用固定 H 步展开的 cell 堆叠规避真循环)。 +- **代价**:中等改动(model.py + export_deploy.py 的 GRU 展开)。 + +### 方案 D · 全 recurrent 编码器(表达力最强,部署风险最高) + +图像帧也过 ConvGRU,跨步隐状态贯穿视觉与动作。**但这打破"除 batch 外全静态 shape"的 +NPU 部署前提**,需重新设计部署图。不推荐在当前 Hailo 目标下做。 + +### 切入点评审结论 + +``` +第 1 步(零风险): C 残差预测 ──→ 立即改善平滑性,验证收益 +第 2 步(零部署影响): B 多步展开训练 ──→ 治 exposure bias +第 3 步(需验证): A GRU 记忆 token ──→ 先做 Hailo GRU 支持单测 + ├─ 支持 → 展开 GRU 进部署链 + └─ 不支持 → 保持 Transformer,仅用 B+C +``` + +**C+B 组合是性价比最高的第一步**:不改部署链、不增参数,直接解决"抖动 + exposure bias" +两个最痛的问题。A 留作需要更强长程记忆时的进阶项。 + +## 3. 整体研究方向:分层演进路线 + +在 RNN 思想之外,从机器人学习前沿看,两模块的演进空间组织为四层。现实约束: +**纯模仿学习(无奖励信号)、MPU6050 已装未用、数据靠实机 `--record`(稀缺)、Hailo NPU 部署硬约束**。 + +### Tier 0 · 立足现状(零部署风险,先做) + +| 编号 | 方向 | 价值 | 改动 | +|------|------|------|------| +| R1 | 残差预测 + 多步展开训练(C+B) | 治抖动+失配,所有后续工作的地基 | dataset + train | +| R2 | **MPU6050 姿态回流** ⭐ | 给模型"本体感觉",是机器人标准状态量 | pybot 读 IMU + actpred 加输入维 | +| R3 | 数据增强扩充 | 稀缺数据的低成本放大 | dataset 增方法 | +| R4 | **离线评估基准** | 没有基准,后续改进全是盲改 | 新增评估脚本 | + +**R2 被低估**:pybot 已有 [mpu6050.py](../../pybot/pybot/hd/mpu6050.py) Kalman 姿态解算,但完全没进 +actpred 的状态表征——模型对自身俯仰/横滚/角速度是盲的。接入 IMU 6 轴作为额外 token 或并入 +动作向量,既补本体感觉,又为 R9 奖励设计提供信号源。 + +**R4 是基础设施**:现在只有开环 MSE。需要闭环代理指标——留出 episode 上开环滚动预测, +测多步累积误差、动作平滑度(相邻动作差分方差)、物理可行性(越界率)。 + +**R3**:当前仅亮度/对比度/饱和度/色调增强([dataset.py:99-102](../../actpred/dataset.py#L99-L102)), +可加时间扭曲(帧率扰动)、动作镜像(左右对称对)、episode 混合拼接。 + +### Tier 1 · 模型能力(需验证部署兼容) + +| 编号 | 方向 | 说明 | +|------|------|------| +| R5 | GRU 记忆 token(方案 A) | 先验 Hailo DFC 循环算子支持 | +| R6 | 多模态状态融合 | 视觉+IMU+本体感觉统一表征(R2 完整版) | +| R7 | 世界模型雏形 | 学"动作→下一帧"预测头,支撑 MPC 规划与 model-based RL | + +### Tier 2 · 学习范式跃迁(数据/奖励是瓶颈) + +| 编号 | 方向 | 说明 | +|------|------|------| +| R8 | **离线强化学习(CQL/IQL)** | 关键契合点:已有大量录制 episode、无法实机安全探索(舵机会坏),离线 RL 从固定数据集学策略、无需在线交互 | +| R9 | 奖励设计(MPU6050) | 姿态稳定性(姿态角方差小=奖励高)、能耗(动作差分平滑=省能)、任务进度 → 标量奖励;从"模仿"走向"优化"的关键一跃 | +| R10 | 自监督视觉预训练 | MAE/对比学习在相机图像上预训练编码器,缓解数据稀缺(ImageNet 预训练与机器人第一视角差异大) | + +**R8 的洞察**:离线 RL 正好契合"数据已有 + 不能实机探索"的现状。但前提是 R9 的奖励信号。 + +### Tier 3 · 远景(与 aibox 联动) + +- **R11 语言条件策略**:aibox 高层语言指令("向前走""左转")作为条件,actpred 出低层动作—— + 分层控制,打通项目"AI 智能体"与"机器人"两大域。 +- **R12 sim2real / 数字孪生**:pybot 的 dry_run 已具备合成帧 + Mock 总线,可演进为轻量仿真环境, + 先仿真预训练再迁移真机,解决数据稀缺与安全探索。 +- **R13 持续学习**:闭环运行中不断回流新 episode 增量微调,机器人越用越聪明。 + +## 4. 推荐切入点与关键决策点 + +**短期(本月)**:**R1 + R4**。先落地"残差+多步训练",同时建评估基准——没有基准,后续改进无法量化对比。 + +**中期**:**R2(MPU6050 回流)性价比之王**——硬件已装、代码已在、收益明确(本体感觉 + 奖励信号源), +只缺"接一根线"。 + +**关键决策点**:R5 GRU 是否值得做,取决于先做 **"Hailo DFC 循环算子支持验证"** 这个小实验: + +- 支持 → R5/R7 可行,循环路线打开; +- 不支持 → Tier 1 循环类方案全部绕行(用更长静态窗口替代隐状态),精力转向 + **R2 多模态 + R8 离线 RL** 这条不依赖循环算子的路线。 + +## 5. 与本季其他工作的衔接 + +- 进程内闭环已落地(pybot `run_loop`/`--loop`),P1-2 帧源(守护线程队列)与 P1-1 overrun 告警已修复 + ——见 [pybot_actpred_AUDIT_20260814.md](../reviews/pybot_actpred_AUDIT_20260814.md)。 +- 数据飞轮已通:`--record` 录制 → 训练 → 推理 → `--loop` 闭环,为 R8 离线 RL 与 R13 持续学习 + 提供数据回路。 +- `--record` 微动作与录制设计见 [pybot_micro_action_and_recording_20260813.md](pybot_micro_action_and_recording_20260813.md); + Hailo 部署链详见 [actpred_hailo_hat2_inference_research_20260813.md](actpred_hailo_hat2_inference_research_20260813.md)。 + +--- + +*本报告为研究方向调研(advisory),方案 A/R5/R7 的部署可行性标 verify_pending, +落地前需做 Hailo DFC 循环算子支持实测。* diff --git a/docs/reports/bot4cj_python_migration_dev_doc.md b/docs/reports/bot4cj_python_migration_dev_doc.md index 98c08e1..fd3a67b 100644 --- a/docs/reports/bot4cj_python_migration_dev_doc.md +++ b/docs/reports/bot4cj_python_migration_dev_doc.md @@ -320,18 +320,25 @@ class I2CBus: --- -## 9. 与 actpred 的集成展望(非本期范围) +## 9. 与 actpred 的集成(已实现:进程内闭环) -迁移完成后,"大脑-身体"闭环的最短路径: +原规划的"大脑-身体"闭环已在 `run_loop()` 中落地,采用进程内调用替代 FIFO 序列化: ``` -actpred predict.py(图像+历史 → 动作向量 action_dim=18) - → 向量转 @Action 单行(pybot 提供 action_row_from_vector() 工具函数) - → pybot RobotStruct 增量执行(对应仓颉版 TODO:@Action 位置指示器) - → MPU6050 姿态反馈回流 actpred(DRL 奖励信号) +camera 帧 → actpred.predict(F帧, H历史) → 动作向量(18) → pybot 下发执行 + ↑ ↓ +history deque ←────────── 回写实际动作 ←────────────────────┘ ``` -同语言后,上述管线可在单进程内完成,无需序列化协议——这正是本次迁移最大的战略收益。 +**用法**:`python3 -m pybot --loop CKPT --hz 10 scripts/std.mt` + +- checkpoint 后缀 `.hef` → Hailo NPU 后端,否则 → PyTorch 后端 +- `@Action` 表头列数 ≤ `action_dim`(默认 18),列索引即动作向量索引 +- 闭环中剔除 `spk.delay` 阻塞(发声但不延时),节拍由 `hz` 主循环掌控 +- 冷启动:F 帧重复同帧 + H 步零动作历史 +- `--loop-steps N` 限步数调试;`-i/-o` FIFO 保留用于未来跨进程隔离 + +**与录制的衔接**:`--record DIR` 产出 actpred 格式 episode → actpred 训练 → 推理 checkpoint → `--loop` 闭环执行,形成完整数据飞轮。 --- diff --git a/docs/reviews/pybot_actpred_AUDIT_20260814.md b/docs/reviews/pybot_actpred_AUDIT_20260814.md new file mode 100644 index 0000000..3c972bc --- /dev/null +++ b/docs/reviews/pybot_actpred_AUDIT_20260814.md @@ -0,0 +1,188 @@ +# pybot & actpred 闭环功能审计报告(含 P1-2 帧源语义研究) + +> 审查日期:2026-08-14 (Asia/Shanghai) +> 审查人:AI Assistant(SOLO Coder) +> 审查范围:`pybot/`(机器人运行时)与 `actpred/`(动作预测)两模块,重点是当日新增的进程内实时预测闭环(`run_loop()` / `--loop`) +> 代码基线:pybot 新增 `run_loop`/`_execute_action_vec`/`--loop` CLI;actpred 为部署链稳定版 +> +> **审计背景**:当日实现进程内闭环后做专项审计。pybot 此前已有 bot4cj 审计 15 项(S01~S08/Q01~Q07) +> 全部修复的基线;actpred 已有 `actpred_CODE_AUDIT_20260808.md`(27 项已修复)与 +> `actpred_CODE_AUDIT_20260812.md`(架构重写后首轮)。本报告**仅记录新发现问题**, +> 焦点是闭环路径与两模块接口。 + +--- + +## 一、审计结论摘要 + +| 级别 | pybot | actpred | 说明 | +|------|-------|---------|------| +| P0(崩溃/数据损坏) | 0 | 0 | 无 | +| P1(功能风险) | 2 | 2 | 见 §二、§三 | +| P2(代码质量) | 3 | 3 | 见 §四 | + +**无 P0**。两模块既有防护(pybot Mock 总线逐字节等价、actpred 部署链层层数值验证)覆盖了大部分高危路径。 + +最高优先:**P1-2 闭环帧源语义**——闭环的相机读取路径与 camera 守护线程完全解耦,导致 `framerate`/`capacity` 配置在闭环中失效,且闭环周期被同步曝光锁死在 ~33ms+。本报告 §五 给出专项研究与修复方案。 + +--- + +## 二、pybot P1 发现 + +### P1-1 闭环节拍在长执行下静默失效 + +**位置**:[pybot/robot.py:245-258](../../pybot/pybot/robot.py#L245-L258) + +```python +remain = dt - (time.monotonic() - t0) +if remain > 0: + time.sleep(remain) +``` + +**问题**:当某步"推理+执行"耗时超过 `1/hz`(NPU 抖动、首帧预热、系统调度抢占),`remain <= 0` 则不 sleep。连续多步超时会让实际控制频率**静默漂移**低于设定 `hz`,无任何累积补偿、无告警。闭环是硬实时假设,部署者无法察觉节拍已失守。 + +**影响**:`--hz 50` 设定下若实际只能跑 30Hz,动作节奏变慢但无人知晓,可能导致预测模型的"动作→结果"时序与训练分布失配。 + +**修复建议**: +- 超时步 `log.warning("loop overrun: step took %.1fms > %.1fms", elapsed*1000, dt*1000)`; +- 可选:统计 overrun 比例,超过阈值(如 10%)时提示降低 `hz`; +- 不做累积补偿(追赶会引入动作抖动,比降频更危险)。 + +### P1-2 闭环帧源与 camera 守护线程解耦(详见 §五 专项研究) + +**位置**:[pybot/robot.py:359-371](../../pybot/pybot/robot.py#L359-L371) `_grab_frame` + [pybot/robot.py:247](../../pybot/pybot/robot.py#L247) + +**问题**:闭环 `_grab_frame` 走 `camera.capture()`(同步曝光),而非 camera 守护线程维护的有界队列 `latest()`。后果: +1. `framerate`/`capacity` 配置在闭环中**完全不生效**(守护线程空转、队列白填); +2. 闭环每步同步阻塞等相机曝光(30fps ≈ 33ms),`hz` 上限被锁死; +3. `hz` 与相机 `framerate` 两个参数实际耦合却未声明。 + +**修复建议**:闭环改从 `camera.snapshot()` 取最近 F 帧。详见 §五。 + +--- + +## 三、actpred P1 发现 + +### P1-3 mt_parser 对 UTF-8 BOM 与首块表头的边界 + +**位置**:[actpred/mt_parser.py:33-37](../../actpred/mt_parser.py#L33-L37) + +**问题**:`read_text(encoding="utf-8")` 对 UTF-8 with BOM 文件,首字符是 ``。`re.search(r"@Action\s+")` 仍能匹配(BOM 在 `@Action` 前不干扰定位),但**若 `@Action` 恰为文件第一个块**,`start_m.end()` 之后、`split("\n")` 之前的表头首列名可能粘附 BOM(如 `s1.A`)。下游用表头列名做 `.mt` 列对齐时可能失配。 + +**影响**:低概率(BOM 文件少见,且 `@Action` 常非首块),但 pybot 的 FSM 解析器对 BOM 的处理需与 actpred 对齐验证,否则两侧对同一文件的列名解析可能不一致——这正是两侧"语义锚点"测试未覆盖的盲区。 + +**修复建议**:`read_text(encoding="utf-8-sig")` 显式剥离 BOM;在 `test_actpred.py::TestMtParser` 补一个 BOM 文件用例,并在 pybot `test_parser.py` 补对应锚点用例。 + +### P1-4 DataRecorder 缺 output_dir 入口校验 + +**位置**:[actpred/dataset.py:136-156](../../actpred/dataset.py#L136-L156) + +**问题**:`self.output_dir = Path(output_dir)` 后直接 `glob` + `images_dir.mkdir(parents=True)`。父目录靠 `parents=True` 隐式创建,正常路径 OK。但若 `output_dir` 是**已存在的文件**(非目录),`glob` 抛 `NotADirectoryError` 或 `mkdir` 抛出不含路径上下文的底层错误,排查困难。对比 pybot `EpisodeRecorder.__init__` 有显式 `output.mkdir(parents=True, exist_ok=True)` 先行([pybot/recorder.py:36](../../pybot/pybot/recorder.py#L36))。 + +**修复建议**:`__init__` 开头加 `self.output_dir.mkdir(parents=True, exist_ok=True)`,并对 `output_dir` 已存在但非目录的情况抛出含路径的清晰错误。 + +--- + +## 四、P2 代码质量发现(汇总) + +### pybot + +| # | 位置 | 问题 | 建议 | +|---|------|------|------| +| P2-a | [main.py:137](../../pybot/pybot/main.py#L137) | `_load_predictor` 用 `endswith(".hef")` 区分后端,大小写敏感且无 ONNX 分支,误传 `.HEF`/`.onnx` 会走 PyTorch 后端抛出晦涩错误 | 按扩展名白名单(`.hef`/`.onnx`/`.pt`)显式分发,或加 `--backend` 参数 | +| P2-b | [robot.py:281](../../pybot/pybot/robot.py#L281) 等 3 处 | `numpy` 在闭环路径 3 个函数各自 `import`;闭环本就依赖 actpred(必装 numpy) | 闭环路径顶部统一 import;脚本模式保留函数级 import 以守零依赖承诺 | +| P2-c | [tests/test_loop.py](../../pybot/tests/test_loop.py) | 未覆盖闭环 + recorder 集成、predictor 抛异常时的资源清理 | 补 `run_loop`+recorder 落盘验证、predictor 异常时 `with robot` 清理验证 | + +### actpred + +| # | 位置 | 问题 | 建议 | +|---|------|------|------| +| P2-d | [dataset.py:32-36](../../actpred/dataset.py#L32-L36) | 图像 `Resize((H,W))` 不保持宽高比,16:9 相机配 4:3 尺寸会畸变;训练/推理一致故对精度无害,但与真实几何脱节 | 文档注明取舍;换相机/分辨率时需重评估 | +| P2-e | [train.py:135](../../actpred/train.py#L135) | resume 时改 `--epochs` 会按新 T_max 重算 lr,用户可能误以为冻结原调度 | 文档注明 resume 改 epochs 的语义 | +| P2-f | [export_deploy.py:31-81](../../actpred/export_deploy.py#L31-L81) | `ManualAttnBlock` 从 `in_proj_weight` 切 Q/K/V 依赖 PyTorch 内部布局约定(非 API 承诺);已有导出时数值对比兜底 | 风险已缓解;保留导出数值验证作为强制门槛 | + +--- + +## 五、P1-2 专项研究:闭环帧源语义 + +### 5.1 两条帧路径的真实差异 + +camera 提供两种帧访问([hd/camera.py](../../pybot/pybot/hd/camera.py)): + +| 路径 | 实现 | 延迟特性 | 帧新旧 | +|------|------|---------|--------| +| `capture()` (L72-76) | 同步调 `picam.capture_array()` | **阻塞等一帧曝光**(30fps≈33ms) | "调用之后"的新帧 | +| `latest()` (L67-70) | 读守护线程有界队列队尾 | **无阻塞** | 最多落后一个抓帧周期 | + +守护线程(`start_daemon`/`_loop`)按 `framerate` 持续抓帧填入 `deque(maxlen=capacity)`,满丢最旧。 + +### 5.2 闭环当前行为的问题 + +`run_loop` 每步 `frames.append(self._grab_frame(camera_var))`(L247),而 `_grab_frame`(L359-371)走 `camera.capture()`: + +``` +闭环步 → capture() → 同步阻塞 ~33ms(30fps 曝光)→ 拿到新帧 + ↓ +camera 守护线程:空转(抓的帧填入队列,但无人消费) +``` + +**量化后果**(以相机 30fps、闭环目标 50Hz 为例): +- 每步 `capture()` 阻塞 ~33ms → 闭环实际周期 ≥ 33ms → **实际 hz ≤ 30**,设定 50 完全达不到; +- `framerate=30`、`capacity=4` 的配置在闭环里无任何作用(守护线程抓的帧全部过期丢弃); +- `hz` 与 `framerate` 必须手工对齐(hz ≤ framerate),但代码无任何校验或声明。 + +### 5.3 修复方案:闭环改走后台队列 + +闭环应消费守护线程的队列,而非同步抓帧。核心改动在 `run_loop` 取帧处: + +```python +# 改进前:同步曝光,阻塞 ~33ms +frames.append(self._grab_frame(camera_var)) + +# 改进后:从守护线程队列取最近 F 帧,无阻塞 +snap = camera_var.camera.snapshot() # [(ts, frame), ...] 旧→新 +recent = snap[-F:] # 取最近 F 帧 +if len(recent) < F: # 队列未填满:重复最旧帧兜底 + recent = [snap[0]] * (F - len(recent)) + recent if snap else [self._grab_frame(camera_var)] * F +frames.clear(); frames.extend(f for _, f in recent) +``` + +**收益**: +- 闭环不再同步阻塞曝光,周期由 `hz` 主导(受限于推理+执行,~12ms); +- `framerate`/`capacity` 配置真正生效:相机高频抓帧,闭环消费最新 F 帧,**帧延迟从 33ms 降到 1/framerate**; +- `hz` 与 `framerate` 解耦(hz 控制节奏、framerate 控制帧新鲜度),但需文档建议 `framerate ≥ 2×hz`。 + +**代价与对策**: +- 帧可能略旧(最多 1/framerate):对 50Hz 控制,33ms 旧帧可接受(舵机机械惯性 ~100ms 量级); +- `snapshot()` 返回带时间戳,可顺带校验帧新鲜度(队尾 ts 距今 > 阈值时 warning); +- 冷启动 `_loop_cold_start` 同样改从队列取,队列空时回退 `_grab_frame` 合成帧。 + +### 5.4 与录制路径的一致性 + +脚本模式 `run()` 的录制(L180/190)也用 `_grab_frame`。若闭环改走队列,需同步评估: +- 录制用 `capture()`(同步新帧)反而合理——录制追求"动作↔帧"严格对齐,新帧更准确; +- 闭环用队列(最新可得帧)合理——实时控制追求低延迟,容忍轻微旧帧。 + +两者语义本可不同,不必强行统一。但应在文档明确"录制=同步新帧,闭环=队列最新帧"的取舍。 + +### 5.5 验证建议 + +修复后补测试([tests/test_loop.py](../../pybot/tests/test_loop.py)): +- 注入 FakeGrabber 的 Camera,验证闭环从队列取最近 F 帧(而非 capture); +- 队列未满时重复最旧帧兜底; +- 断言闭环周期不再含 capture 阻塞(Mock 计时)。 + +--- + +## 六、修复优先级建议 + +| 优先级 | 项 | 理由 | +|--------|-----|------| +| 1 | P1-2 闭环帧源(§五) | 直接决定 `--loop` 真机延迟特性与 hz/framerate 语义,影响最大 | +| 2 | P1-1 节拍 overrun 告警 | 一行 warning + 统计,低成本高诊断价值 | +| 3 | P1-4 DataRecorder 入口校验 | 一行 mkdir + 校验,与 pybot 对齐 | +| 4 | P1-3 mt_parser BOM | 低概率但涉及两侧语义锚点一致性 | +| 5 | P2 各项 | 文档/测试补强,随下次迭代带入 | + +--- + +*本报告为闭环功能专项审计,修复前建议在树莓派真机验证 P1-2 的实际延迟影响(capture vs 队列两种模式的周期对比)。*