From 0aa0710a9e4a971841023ba272b5347e1bda8c1f Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Tue, 11 Aug 2026 16:30:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=20.mt=20=E9=A6=96?= =?UTF-8?q?=E8=A1=8C=20'-'=20=E5=8D=A0=E4=BD=8D=E7=AC=A6=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E2=80=94=E2=80=94actpred=20=E5=AF=B9=E9=BD=90=20pybot/bot4cj?= =?UTF-8?q?=20=E8=BF=90=E8=A1=8C=E6=97=B6=EF=BC=88=E5=A1=AB=200=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E5=BC=83=E7=94=A8=20@Servo=20max=20=E5=88=97=E5=A1=AB?= =?UTF-8?q?=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- actpred/mt_parser.py | 38 +++---------------- actpred/test_actpred.py | 34 +++++++++++++++-- .../bot4cj_python_migration_dev_doc.md | 6 +-- pybot/pybot/parser.py | 6 +-- pybot/tests/test_parser.py | 2 +- 5 files changed, 42 insertions(+), 44 deletions(-) diff --git a/actpred/mt_parser.py b/actpred/mt_parser.py index fc7193f..d75b09a 100644 --- a/actpred/mt_parser.py +++ b/actpred/mt_parser.py @@ -1,7 +1,9 @@ """轻量级 .mt 脚本 @Action 矩阵解析器 从 .mt 文件中提取 @Action 块,解析为 (n_rows, n_cols) 的 float32 矩阵。 -支持 `-` 占位符(沿用上一行同列的值);首行 `-` 从 @Servo 块的初始角度填充。 +支持 `-` 占位符(沿用上一行同列的值);首行 `-` 填 0, +与 pybot/bot4cj 运行时语义一致(2026-08 统一,见 +docs/reports/bot4cj_python_migration_dev_doc.md §2.3)。 """ import re @@ -15,27 +17,6 @@ def _natural_key(p: Path): return [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", p.name)] -def _parse_servo_init(text: str) -> dict: - """从 @Servo 块提取 {servo_name: initial_angle}(max 列)""" - match = re.search(r"@Servo\s+(.*?)\s*@", text, re.DOTALL) - if not match: - return {} - servo_init = {} - for line in match.group(1).split("\n"): - line = re.sub(r"#.*$", "", line).strip() - if line.endswith(";"): - line = line[:-1].strip() - if not line: - continue - cols = re.split(r"\s+", line) - if len(cols) >= 2 and cols[0] != "name": - try: - servo_init[cols[0]] = float(cols[1]) - except ValueError: - pass - return servo_init - - def parse_mt_action(path) -> np.ndarray: """解析 .mt 文件中的 @Action 矩阵 @@ -44,7 +25,7 @@ def parse_mt_action(path) -> np.ndarray: Returns: (n_data_rows, n_cols) float32 矩阵,表头行已跳过,`-` 已前向填充。 - 首行 `-` 从 @Servo 块的初始角度填充。 + 首行 `-` 填 0(与 pybot/bot4cj 运行时语义一致)。 Raises: ValueError: 文件中无 @Action 块、数据行不足、行列不一致或数值非法 @@ -53,9 +34,6 @@ def parse_mt_action(path) -> np.ndarray: # 先去除整行注释(# 开头),避免注释中的 @Action 干扰正则匹配 text = re.sub(r"(?m)^[ \t]*#.*$", "", text) - # 解析 @Servo 块的初始角度,用于首行 `-` 前向填充 - servo_init = _parse_servo_init(text) - match = re.search(r"@Action\s+(.*?)\s*@", text, re.DOTALL) if not match: raise ValueError(f"未找到 @Action 块: {path}") @@ -84,12 +62,6 @@ def parse_mt_action(path) -> np.ndarray: data_rows = rows[1:] n_cols = len(header) - # 为每列计算初始值:s1.A → @Servo 中 s1 的 max 角度,其余为 0.0 - col_init = [] - for col_name in header: - servo_name = col_name.split(".")[0] if "." in col_name else None - col_init.append(servo_init.get(servo_name, 0.0)) - matrix = np.zeros((len(data_rows), n_cols), dtype=np.float32) for i, row in enumerate(data_rows): if len(row) != n_cols: @@ -98,7 +70,7 @@ def parse_mt_action(path) -> np.ndarray: f"不一致: {path}") for j, val in enumerate(row): if val == "-": - matrix[i, j] = matrix[i - 1, j] if i > 0 else col_init[j] + matrix[i, j] = matrix[i - 1, j] if i > 0 else 0.0 else: try: matrix[i, j] = float(val) diff --git a/actpred/test_actpred.py b/actpred/test_actpred.py index c20bf44..e5a66c5 100644 --- a/actpred/test_actpred.py +++ b/actpred/test_actpred.py @@ -321,8 +321,8 @@ class TestMtParser(unittest.TestCase): np.testing.assert_array_equal(matrix[0], [10, 20, 100]) np.testing.assert_array_equal(matrix[2], [11, 22, 101]) - def test_first_row_dash_uses_servo_init(self): - """首行 `-` 从 @Servo 初始角度(max 列)填充,无映射列填 0""" + def test_first_row_dash_fills_zero(self): + """首行 `-` 填 0(与 pybot/bot4cj 运行时语义一致),后续行 `-` 沿用""" content = ( "@Servo\n" " name max min;\n" @@ -339,8 +339,34 @@ class TestMtParser(unittest.TestCase): with tempfile.TemporaryDirectory() as tmpdir: path = _write_mt(tmpdir, "servo.mt", content) matrix = parse_mt_action(path) - np.testing.assert_array_equal(matrix[0], [90, 180, 0]) - np.testing.assert_array_equal(matrix[1], [45, 180, 10]) + np.testing.assert_array_equal(matrix[0], [0, 0, 0]) + np.testing.assert_array_equal(matrix[1], [45, 0, 10]) + + def test_std_mt_first_row_matches_pybot(self): + """跨模块回归锚点:std.mt 首行解析结果与 pybot 运行时执行值一致 + + 与 pybot/tests/test_hardvars.py 的 journal 期望值互为对照: + 首行 `-`(s2/s4/s6/s8)解析为 0,而非 @Servo max 列。 + """ + content = ( + "@Servo\n" + " name max min offset controller channel;\n" + " s1 90 - - pca9685-0 0;\n" + " s2 180 - - - 1;\n" + " s3 -90 - - - 2;\n" + " s4 -180 - - - 3;\n" + "@\n" + "\n" + "@Action\n" + "s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay;\n" + " 45 - 45 - 45 - 45 - 90 90 10 200;\n" + "@\n" + ) + with tempfile.TemporaryDirectory() as tmpdir: + path = _write_mt(tmpdir, "std.mt", content) + matrix = parse_mt_action(path) + np.testing.assert_array_equal( + matrix[0], [45, 0, 45, 0, 45, 0, 45, 0, 90, 90, 10, 200]) def test_no_action_block_raises(self): """无 @Action 块应抛 ValueError""" diff --git a/docs/reports/bot4cj_python_migration_dev_doc.md b/docs/reports/bot4cj_python_migration_dev_doc.md index 39cbefb..296f427 100644 --- a/docs/reports/bot4cj_python_migration_dev_doc.md +++ b/docs/reports/bot4cj_python_migration_dev_doc.md @@ -81,7 +81,7 @@ RobotStruct(r.cj)createHardVars():按 @块名分发到硬件变量工厂 ### 2.3 关键语义(必须逐项对齐) 1. **占位符 `-`**:递归沿用上一行同列值;首行为 `-` 时,`parseMt` 解析为 `0`(数值列),`getMt` 返回字面 `-`。 - ⚠️ 注意与 actpred `mt_parser.py` 的差异:后者首行 `-` 用 `@Servo` 初始角度填充(这是 ML 数据管线的语义)。**pybot 运行时语义以仓颉版为准(填 0)**,因为运行时 Servo 有 max/min 钳位保护;数据管线语义留在 actpred 不变。 + actpred `mt_parser.py` 曾与此不同(首行 `-` 用 `@Servo` max 列填充),2026-08 已统一为本语义(填 0):运行时 Servo 有 max/min 钳位保护,且 max 列是钳位上限而非初始位姿(反装舵机 max 为负值,会填出指令空间 0~180 之外的非法值)。 2. **@Action 列名 `dev.param`**:点号拆分为硬件名 + 参数名;参数名经 `@Alias` 表转换(`A`→`angle`、`F`→`frequency`)后传入硬件。 3. **每行执行**:同一行按硬件聚合参数字典,逐硬件调用一次 `play()`;行间时延由 `spk.delay` 参数承担。 4. **硬件生命周期**:`active()` 按创建顺序 build,`deactive()` 倒序 destroy,且必须在 `try/finally` 中保证执行。 @@ -276,7 +276,7 @@ class I2CBus: | 用例组 | 覆盖 | |--------|------| | test_parser | 全部 `scripts/*.mt` 解析成功;列数不一致抛 DoesNotSync;漏分号跨行;注释/空行/CRLF;roundtrip(解析→save→再解析等价) | -| test_placeholder | `-` 递归沿用;首行 `-` 数值列=0;与 actpred mt_parser 语义差异的显式断言(防止两边被误"统一") | +| test_placeholder | `-` 递归沿用;首行 `-` 数值列=0;actpred mt_parser 已统一为同一语义(2026-08),两侧测试互为语义锚点 | | test_servo | 正装/反装翻转/offset/钳位边界 | | test_hardvars | @Alias 转换;@Action 列名拆分;同行参数聚合;重名覆盖警告 | | test_plugin | 插件三级路径查找;PluginNotFound 报错内容;PCA9685.py 用 Mock I2C 断言寄存器写序列(prescale=121、reg=0x06+4*ch、offval 字节序) | @@ -315,7 +315,7 @@ class I2CBus: | Python GIL + 解释器抖动影响动作时序 | 舵机动作节奏轻微不均 | 动作行间延时 ≥ 20ms,抖动 <1ms 不可感知;MPU6050 采样用 Event.wait 定时,不忙等;量化依据见附录 A | | aplay 子进程首次启动延迟 | 首个音符滞后 | build() 时预热(播放 0 长度静音);或后续可选接 alsaaudio | | 仓颉版行为的未记录细节 | 等价性缺口 | §6.3 写序列逐字节 diff 兜底 | -| .mt 脚本方言分裂(pybot vs actpred 解析器) | 语义漂移 | test_placeholder 显式断言两者差异;长期可评估 actpred 复用 pybot parser 的只读模式 | +| .mt 脚本方言分裂(pybot vs actpred 解析器) | 语义漂移 | 占位符语义已统一(2026-08,首行 `-` 均填 0);长期可评估 actpred 复用 pybot parser 的只读模式 | | 树莓派 Python 版本参差 | 语法兼容 | 目标 Python ≥ 3.9(树莓派 OS Bullseye 起自带 3.9) | --- diff --git a/pybot/pybot/parser.py b/pybot/pybot/parser.py index 39bc066..26c41da 100644 --- a/pybot/pybot/parser.py +++ b/pybot/pybot/parser.py @@ -213,9 +213,9 @@ class MatrixSaver: # ---------------- 占位符语义(与仓颉版 MatrixVars_Hardware.getMt/parseMt 对齐) ---------------- # -# ⚠️ 与 actpred/mt_parser.py 的语义差异(不得误统一): -# actpred 数据管线首行 '-' 用 @Servo 初始角度填充; -# pybot 运行时首行 '-' 数值列解析为 0(Servo 有 max/min 钳位保护)。 +# 首行 '-' 数值列解析为 0(Servo 有 max/min 钳位保护)。 +# actpred/mt_parser.py 曾用 @Servo max 列填充首行 '-',2026-08 已统一 +# 为本语义(填 0),见 docs/reports/bot4cj_python_migration_dev_doc.md §2.3。 def get_mt(obj, row, col): """获得项目值:原始值是占位符且非首行(row>1),递归取上一行同列值""" diff --git a/pybot/tests/test_parser.py b/pybot/tests/test_parser.py index d7b6eeb..aa64a9e 100644 --- a/pybot/tests/test_parser.py +++ b/pybot/tests/test_parser.py @@ -91,7 +91,7 @@ def test_placeholder_recursion(): def test_placeholder_first_row(): """首行 '-':get_mt 返回字面 '-';parse_mt 解析为 typ("0") - ⚠️ 与 actpred/mt_parser.py 不同(后者用 @Servo 初始角度填充),不得误统一""" + actpred/mt_parser.py 已统一为本语义(首行 '-' 填 0),本测试即两侧共同的语义锚点""" m = _matrix("@T\nname v;\na -;\n@\n") assert get_mt(m, 1, 1) == "-" assert parse_mt(m, 1, 1) == 0.0