Files
blackbird/pybot/pybot/main.py
T
qing fe12dd7b33 feat(pybot): 进程内实时预测闭环(--loop)与帧源/节拍修复
- 新增 run_loop 闭环:F 帧+H 步历史双滑窗,冷启动重复同帧+零动作,
  每步 采样→推理→执行→回写,hz 节拍控制,可选同步录制
- --loop CKPT/--hz/--loop-steps 参数,按 .hef 后缀选 Hailo/PyTorch 后端,
  actpred 软依赖(仅 --loop 时 import),保持 pybot 零依赖
- P1-2 帧源修复:闭环改从 camera 守护线程队列取最近 F 帧(低延迟),
  替代 capture() 同步曝光阻塞,framerate/capacity 配置真正生效
- P1-1 overrun 告警:超时步记录实际耗时,汇总 overrun 比例并给降频建议
- 剔除 Speaker.delay 发声阻塞,speaker 补 delay=0 非阻塞
- 新增 tests/test_loop.py 11 用例(冷启动/历史滑动/索引映射/帧源等)
2026-08-14 07:52:07 +08:00

156 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""CLI 入口 —— 移植自 bot4cj/src/main.cj
用法(与仓颉版管道方式兼容):
cat scripts/std.mt | python3 -m pybot # stdin 管道
python3 -m pybot scripts/std.mt # 位置参数(增强)
python3 -m pybot --dry-run scripts/std.mt # Mock 总线,动作序列到 stdout
python3 -m pybot --test roundtrip in.mt out.mt # 解析↔序列化往返
与仓颉版的差异(见方案 §4.7):
--test nn/cnn/cnn1 移除(NN 由 actpred 承担);
--test matrix_o/matrix_2 合并为 --test roundtrip(路径改为参数);
新增 --dry-run / --verbose;-i / -o 保留参数位(reserved,对接 actpred)。
日志规范(S02 根治):全部诊断走 logging(stderr),stdout 保持纯净。
"""
import argparse
import logging
import sys
from .hd import i2c, spi
from .parser import MatrixParser, MatrixSaver
from .robot import RobotStruct
def _build_argparser():
p = argparse.ArgumentParser(
prog="pybot",
description="机器人 .mt 动作脚本解析器(bot4cj 的 Python 迁移版)")
p.add_argument("script", nargs="?", default=None,
help=".mt 脚本路径(缺省从 stdin 读取)")
p.add_argument("--test", nargs=3, metavar=("MODE", "IN", "OUT"),
help="自检:roundtrip <in.mt> <out.mt> 解析↔序列化往返")
p.add_argument("--dry-run", action="store_true",
help="Mock 总线运行(不碰硬件),动作序列输出到 stdout")
p.add_argument("--verbose", action="store_true",
help="DEBUG 级日志(stderr)")
p.add_argument("-i", metavar="FIFO", default=None,
help="(reserved) 传感器数据输出管道,对接 actpred")
p.add_argument("-o", metavar="FIFO", default=None,
help="(reserved) 动作指令输入管道,对接 actpred")
p.add_argument("--record", metavar="DIR", default=None,
help="录制 actpred 格式 episode 到 DIR(需要 @Camera 块;"
"--dry-run 下用合成帧)")
p.add_argument("--action-dim", type=int, default=18,
help="动作向量维度(默认 18,与 actpred 一致)")
p.add_argument("--loop", metavar="CKPT", default=None,
help="实时预测闭环:加载 actpred checkpoint,"
"采样→推理→执行→回写(不执行脚本 @Action 行,"
"仅用其表头做动作向量索引映射)")
p.add_argument("--hz", type=float, default=10.0,
help="闭环控制频率 Hz(默认 10,须 ≥ 推理+执行耗时)")
p.add_argument("--loop-steps", type=int, default=None, metavar="N",
help="闭环步数上限(默认无限,调试用)")
return p
def _roundtrip(in_path, out_path):
"""解析 in.mt 再序列化到 out.mt(对应仓颉版 --test matrix_o/matrix_2)"""
objects = []
with open(in_path, encoding="utf-8") as f:
parser = MatrixParser(f)
while True:
obj = parser.parse()
if obj is None:
break
objects.append(obj)
with open(out_path, "w", encoding="utf-8") as f:
MatrixSaver(f).save(objects)
logging.info("roundtrip: %d blocks, %s -> %s",
len(objects), in_path, out_path)
return 0
def _dump_journal(*mocks):
"""--dry-run 结束后把 Mock 总线动作序列输出到 stdout(等价性验证工件)"""
for mock in mocks:
for entry in mock.journal:
print(" ".join(str(x) for x in entry))
def main(argv=None):
args = _build_argparser().parse_args(argv)
logging.basicConfig(
stream=sys.stderr,
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)s %(name)s: %(message)s")
if args.i or args.o:
logging.warning("-i/-o are reserved for actpred integration, ignored")
if args.test:
mode, in_path, out_path = args.test
if mode != "roundtrip":
logging.error("unknown --test mode: %s (supported: roundtrip)", mode)
return 2
return _roundtrip(in_path, out_path)
mocks = ()
if args.dry_run:
mocks = (i2c.use_mock(), spi.use_mock())
predictor = None
if args.loop:
predictor = _load_predictor(args.loop)
if predictor is None:
return 2
source = open(args.script, encoding="utf-8") if args.script else sys.stdin
try:
robot = RobotStruct(source, dry_run=args.dry_run)
recorder = None
if args.record:
from .recorder import EpisodeRecorder
recorder = EpisodeRecorder(args.record,
action_dim=args.action_dim)
with robot:
if predictor is not None:
robot.run_loop(predictor, hz=args.hz,
max_steps=args.loop_steps, recorder=recorder)
else:
robot.run(recorder=recorder)
finally:
if args.script:
source.close()
if args.dry_run:
_dump_journal(*mocks)
return 0
def _load_predictor(checkpoint):
"""进程内构建 actpred 推理器;失败返回 None
pybot 不硬依赖 actpred:仅 --loop 时才尝试 import,actpred 不在
sys.path 时给出可操作提示。checkpoint 目录名决定后端(.hef → Hailo
NPU,否则 → PyTorch),推理类由 actpred 提供。
"""
try:
if checkpoint.endswith(".hef"):
from actpred.hailo_infer import HailoPredictor as _P
else:
from actpred.predict import ActionPredictorInference as _P
except ImportError:
logging.error("--loop 需要 actpred 模块(把 blackbird 根目录加入 "
"PYTHONPATH,或在 actpred/ 同级运行)")
return None
try:
return _P(checkpoint)
except Exception:
logging.exception("加载 actpred checkpoint 失败: %s", checkpoint)
return None
if __name__ == "__main__":
sys.exit(main())