- 新增 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 用例(冷启动/历史滑动/索引映射/帧源等)
156 lines
6.0 KiB
Python
156 lines
6.0 KiB
Python
"""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())
|