diff --git a/src/litearm/_protocol.py b/src/litearm/_protocol.py index e36f2d7..0c4ee17 100644 --- a/src/litearm/_protocol.py +++ b/src/litearm/_protocol.py @@ -218,8 +218,16 @@ #: (usb_cmd.c CMD_GET_IK: "seed 恒为模型 7 轴 (KIN_N), 与 LITEARM_NUM_JOINTS 解耦")。 #: 台架 1J 版把台架那台电机映射到模型第 `BENCH_MODEL_AXIS` 轴。 KIN_N = 7 -#: 台架电机对应的整臂模型轴 (`joint_cfg.h` 的 `LITEARM_BENCH_MODEL_AXIS`, 台架=5)。 -#: ⚠ 跨仓常量 —— 由 tests/test_protocol_sync.py 直接解析固件 joint_cfg.h 断言其一致。 +#: 台架电机对应的整臂模型轴 (固件 `joint_cfg.h` 的 `LITEARM_BENCH_MODEL_AXIS`, 台架=5)。 +#: +#: ⚠ **LEGACY —— 固件 2026-09-28 已移除 `LITEARM_BENCH_1J` 台架单电机模式** +#: (joint_cfg.h 顶部三条理由, 其中 ② 是 flash 布局随开关变却不 bump +#: `FLASH_STORE_VERSION` ⇒ **静默丢现场标定**; 另有防"再打开"的 `#error` 硬守卫)。 +#: ⇒ 当前固件**不再定义**这个宏, 本常量只服务**仍要支持的 1J 老固件** +#: (`Litearm1.7.0-1J`, 见 `tests/test_ik_bench.py` / `conftest.offline_arm_1j`)。 +#: **不要当成死代码删掉** —— 删了 1J 会话的 IK 种子就会填错轴。 +#: 跨仓一致性由 tests/test_protocol_sync.py::test_bench_model_axis_matches_sdk_constant +#: 守: 固件仍有该宏则逐值比对, 固件已移除则断言守卫在 + 本常量仍在。 BENCH_MODEL_AXIS = 5 MODE_ZERO_G = 7 diff --git a/src/litearm/errors.py b/src/litearm/errors.py index 02a6541..905dfe2 100644 --- a/src/litearm/errors.py +++ b/src/litearm/errors.py @@ -206,6 +206,9 @@ class ArmIsInDfuError(LiteArmError): "只能按「这条 move_p 没成立」处置 (不要据此判失能)", (0x02, 0x04): "零重力(拖动示教)进行中", (0x02, 0x05): "IK 后台通道忙 (已有一次后台求解在途)", + (0x02, 0x06): "掉线刚性持位锁存 (drop_hold) 中 —— 受理过的 move_p 的**迟到终态应答** " + "(kin_runner_flush_move: IK 结果登记之后才发生锁存时补发), " + "须先 reset/clear_faults", # ---- 0x3A / 0x3B MOVE_L / MOVE_C(固件原生笛卡尔, #if LITEARM_CART_PLAN)---- # 门禁三档由 cart_gate_ok() 统一发: !enabled(0x03) -> drop_hold(0x06) -> 零重力(0x04); @@ -316,6 +319,8 @@ class ArmIsInDfuError(LiteArmError): # ---- 0x20 / 0x21 运行期旋钮(都只判长度)---- (0x20, 0x01): "载荷长度不足 (需 1 字节: mode)", (0x21, 0x01): "载荷长度不足 (需 1 字节: 百分比)", + (0x21, 0x02): "百分比越界 (>100) —— 协议范围 0..100; 0 合法 " + "(usb_cmd.c [L-1 fix 2026-10-09]: 原实现静默钳到 100% 并照回 ACK)", # ---- 0x42 GET_IK / 0x49 KIN_BENCH(后台求解通道)---- (0x42, 0x01): "载荷长度不足 (需 52 字节: pose[6] + seed[7])", @@ -387,7 +392,13 @@ class ArmIsInDfuError(LiteArmError): (0x2B, 0x02): "item 越界 (须 1..15)", (0x2C, 0x01): "载荷长度不足 (需 2 字节: item + sub)", (0x2C, 0x02): "item 越界 (须 1..18)", - (0x31, 0x02): "preset 非法 (须 0..2), **或**载荷长度不足 (两者共用本码)", + (0x31, 0x01): "载荷长度不足 (需 1 字节: preset)", + (0x31, 0x02): "preset 非法 (须 0..2)", + # ⚠ [2026-10-10 同步] `0x31` 的这两个码**原是合并的** —— 固件 `usb_cmd.c` 的 + # [2026-09-24 fix] 把"长度不足"与"item 非法"拆开 (长度不足统一 = 0x01), + # 真机实测过: 发 len=0 原回 {0x31,0x02}、应为 {0x31,0x01}。 + # 上面这条曾写作"preset 非法 (须 0..2), **或**载荷长度不足 (两者共用本码)", + # 那个"或"自 2026-09-24 起已作废 —— 保留会让上位机把长度错当成 preset 错。 # ---- 0x30 / 0x32 / 0x33 / 0x34 / 0x37 / 0x39 HYY 动力学模型在线导入 ---- (0x30, 0x01): "载荷长度不足 (需 1 + 4×BODY_PARAMS 字节)", @@ -402,6 +413,9 @@ class ArmIsInDfuError(LiteArmError): (0x34, 0x02): "body_idx 越界", (0x37, 0x04): "武装中 —— 须先失能", (0x39, 0x01): "载荷长度不足 (需 4×DOF 字节: q[7])", + (0x39, 0x02): "q 含 NaN/Inf —— [2026-09-24 fix] 补的有限性校验; 本条是全仓唯一" + "不做 f32buf_finite 的**读载荷**命令, 而它正是产线" + "\"静态重力核查\"的入口 (NaN 参与比较恒为 false ⇒ 核查会静默通过)", # ---- 0x2D / 0x2E 300Hz 控制拍采集 ---- (0x2D, 0x01): "载荷长度不足 (需 4 字节: n_ticks)", diff --git a/tests/test_error_codes.py b/tests/test_error_codes.py index 8d3734d..ac143f2 100644 --- a/tests/test_error_codes.py +++ b/tests/test_error_codes.py @@ -36,9 +36,13 @@ `errors.ERR_TEXT` 里 (唯一例外: `code == 0x00`, 那是 `default` 分支, 见 `test_protocol_sync.py` 的 `test_err_code_zero_only_comes_from_default_branch`)。 - · **变量码 (清单, 13 条)**: `SYMBOLIC_ERR_SITES` 逐条列举, 每条声明它的 + · **变量码 (清单, 15 条)**: `SYMBOLIC_ERR_SITES` 逐条列举, 每条声明它的 `(命令集合 × 码集合)`; **清单条数被断言钉死** (见 `test_symbolic_site_manifest_matches_firmware`), 清单自己漂了也红。 + ⚠ 组成 = `usb_cmd.c` 13 条 + `kin_runner.c` 1 条 + `default` 哨兵 1 条。 + (2026-10-10 之前只有 14 条 —— 下面那条 `usb_cmd.c` 的分解是**单文件**口径, + 而 `_firmware_err_sites` 走的是**整棵固件树**, 于是 `kin_runner.c` 那条 + 一直没被清单看见; 是 SDK 对着固件跑 `test_protocol_sync`/本文件时抓出来的。) · **反向闭合 (这是"双向差集"唯一写得出来的形态)**: `ERR_TEXT` 里**不在**上两类 覆盖范围内的条目 = 必须是**显式列举且计数**的 `ERR_TEXT_OUT_OF_TREE` (当前 2 条, 出处是另一条固件分支)。于是"表里凭空多一条固件发不出的码" @@ -92,20 +96,35 @@ def test_table_is_not_empty_and_covers_the_cartesian_range(): assert (0x3E, 0x01) not in E.ERR_TEXT, "CART_RUN 载荷为空, 不存在长度不足档" -#: 会因 drop_hold 而回 `0x06` 的命令。⚠ **`0x02` MOVE_P 不在其中** —— 它的 dispatch -#: 直接问 `kin_runner_request_move_p` (后台 IK), 三档只有 `!enabled`(0x03) / -#: 零重力(0x04) / 忙(0x05), **没有** drop_hold 门禁 (固件 `usb_cmd.c` 的 -#: `case CMD_MOVE_P`)。它是运动类命令里唯一的例外 —— 别按"运动类都判"去推。 -DROP_HOLD_CMD_CODES = (0x01, 0x03, 0x04, 0x05, 0x07, 0x2A, +#: 会因 drop_hold 而回 `0x06` 的命令。 +#: +#: ⚠ **`0x02` MOVE_P 走的是另一条路, 但同样会回 `0x06`** —— 它的 **dispatch** +#: (`usb_cmd.c` 的 `case CMD_MOVE_P` → `kin_runner_request_move_p`) 确实**没有** +#: drop_hold 门禁 (三档只有 `!enabled`(0x03) / 零重力(0x04) / 忙(0x05)); +#: 但**受理之后**后台 IK 出解时走 `kin_runner_flush_move` → `ctrl_accept_move_j`, +#: 那里面**有** `if (g_arm.drop_hold) return 0x06`, 并以 `{0x02, rc}` 补一条 +#: **迟到终态应答** (固件 `kin_runner.c` 的 [2026-09-24 fix]; 其注释点名 +#: "0x06 掉线锁存")。⇒ **登记 IK 与 flush 之间**发生锁存, 就会看到 `{0x02,0x06}`。 +#: +#: ⚠ [2026-10-10 修正] 本条原先写"`0x02` 不在其中 … 它是运动类命令里唯一的例外", +#: 并据此断言 `(0x02, 0x06)` **不得**在表里。那只覆盖了 **dispatch** 一路, 漏了 +#: 迟到终态应答一路 —— 是 SDK 对着固件跑 `test_protocol_sync`/本文件时抓出来的 +#: (同一批里清单新增了 `kin_runner.c` 那个变量码位点)。**别按"dispatch 没有"去推 +#: 整条命令没有** —— 收口函数与 dispatch 不是同一层。 +DROP_HOLD_CMD_CODES = (0x01, 0x02, 0x03, 0x04, 0x05, 0x07, 0x2A, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E) def test_the_two_previously_untabulated_entries_are_present(): - """S4 fix 的两条"今天不在任何表里"的码 (计划 Step 5 点名)。""" + """S4 fix 的两条"今天不在任何表里"的码 (计划 Step 5 点名)。 + + ⚠ `(0x02, 0x06)` 的**负向**断言已于 2026-10-10 撤掉并改为由上面的循环**正向** + 断言 —— 见 `DROP_HOLD_CMD_CODES` 那段: MOVE_P 的 0x06 走的是迟到终态应答, + 不是 dispatch。撤负向不是放宽, 是**改对了方向**。 + """ assert (0x25, 0x03) in E.ERR_TEXT, "异步保存失败码 {0x25,0x03} 未登记" for cmd in DROP_HOLD_CMD_CODES: assert (cmd, 0x06) in E.ERR_TEXT, f"{{0x{cmd:02X},0x06}} (drop_hold 门禁) 未登记" - assert (0x02, 0x06) not in E.ERR_TEXT, "MOVE_P 无 drop_hold 门禁, 不该有 0x06" @pytest.mark.parametrize("cmd", DROP_HOLD_CMD_CODES) @@ -333,7 +352,7 @@ def test_doc_warns_that_inference_is_one_sided(): _ERR_CALL_HEAD = re.compile(r"usb_cmd_reply\s*\(\s*RSP_ERR") _HEX = re.compile(r"^0x[0-9A-Fa-f]+$") -#: 13 条**变量码**符号位点 (清单, 见模块 docstring 的口径说明)。 +#: 15 条**变量码**符号位点 (清单, 见模块 docstring 的口径说明)。 #: key = `(相对固件仓的路径, cmd 表达式, code 表达式)`; #: value = `(可达的 (cmd, code) 对集合, 说明)` —— 命令位写 `None` 表示"任意命令" #: (只有 `default` 分支的 `{cmd, 0x00}` 是这样)。 @@ -352,6 +371,7 @@ def test_doc_warns_that_inference_is_one_sided(): #: ⚠ 这份清单**必须与固件逐条对齐**: 条数与内容都被 #: `test_symbolic_site_manifest_matches_firmware` 钉住。 _USB_CMD_C = os.path.join(_FW_SUBDIR, "hal", "usb_cmd.c") +_KIN_RUNNER_C = os.path.join(_FW_SUBDIR, "kinematics", "kin_runner.c") SYMBOLIC_ERR_SITES = { (_USB_CMD_C, "cmd", "0x03"): (frozenset({(0x3A, 0x03), (0x3B, 0x03), (0x3E, 0x03)}), @@ -399,13 +419,21 @@ def test_doc_warns_that_inference_is_one_sided(): (_USB_CMD_C, "0x15", "r"): (frozenset({(0x15, 0x02), (0x15, 0x03)}), "ctrl_request_enter_dfu 的返回码 (0x02 ROM 向量表无效 / 0x03 使能中)"), + (_KIN_RUNNER_C, "0x02", "rc"): + (frozenset({(0x02, 0x03), (0x02, 0x04), (0x02, 0x06)}), + "kin_runner_flush_move: 受理过的 move_p 的**迟到终态应答** —— IK 结果登记之后" + "才发生失能/掉线锁存/进零重力时补发, rc = ctrl_accept_move_j 的返回码直接透传" + " (固件注释点名: 0x03 未使能 / 0x04 零重力中 / 0x06 掉线锁存, **不新增码**)。" + "⚠ 本仓 2026-10-10 才发现: 上面那 14 条全部按 `usb_cmd.c` 计数, 而 " + "`_firmware_err_sites` 走的是**整棵固件树**, 故这一条在 `kin_runner.c` 里" + "一直没进清单 —— 哨兵此前对它是瞎的"), (_USB_CMD_C, "cmd", "0x00"): (frozenset({(None, 0x00)}), "default 分支: 固件没有这条命令 (唯一一处 0x00, 能力探测的哨兵)"), } #: 清单自身的**条数** —— 断言它, 免得清单被"顺手"改小而不自知。 -SYMBOLIC_ERR_SITES_COUNT = 14 +SYMBOLIC_ERR_SITES_COUNT = 15 #: `ERR_TEXT` 里**不在本固件树产出范围内**、但刻意保留的条目 (spec R5): #: 出处是 `origin/feat/hyy-model-import` (1.5.3 `cdb744a` 不是 master 祖先), diff --git a/tests/test_protocol_sync.py b/tests/test_protocol_sync.py index 65f652c..902e793 100644 --- a/tests/test_protocol_sync.py +++ b/tests/test_protocol_sync.py @@ -20,7 +20,9 @@ `getattr` 得到 (覆盖"无死角"这件事本身); 3. 固件标注「未实现」的命令仍然标注着 (固件一旦实现, 这里立刻红, 提醒补入口); 4. 状态帧布局表达式未被改动; - 5. `joint_cfg.h` 的 `LITEARM_BENCH_MODEL_AXIS` 与 SDK 常量一致; + 5. `joint_cfg.h` 的台架常量与 SDK `BENCH_MODEL_AXIS` 一致 —— 固件 2026-09-28 移除 + 台架模式后, 该条**退化为**"防再打开的 `#error` 守卫仍在 + SDK 的 legacy 常量仍在" + (见用例内注释; SDK 仍要服务 1J 老固件, 故常量不能删); 6. `ERR{cmd,0x00}` 在固件里仍**只**由 `default` 分支产生 —— 这条守的是 「用 ERR 码判定固件能力」这个核心假设 (见 test_capability.py); 7. 固件自报版本不低于 SDK 要求的 `MIN_FW`。 @@ -262,14 +264,38 @@ def test_status_frame_joint_stride_matches_sdk_constant(): # ---------------------------------------------------------------- 4. 台架常量 def test_bench_model_axis_matches_sdk_constant(): + """`BENCH_MODEL_AXIS` 与固件 `joint_cfg.h` 的台架映射一致。 + + ⚠ [2026-10-10] 固件 **2026-09-28 已移除** `LITEARM_BENCH_1J` 台架单电机模式 + (joint_cfg.h 顶部三条理由, 其中 ② 是**静默丢 flash 标定**), 并留了防"再打开"的 + `#error` 硬守卫。故本用例分两支: + + · 固件**仍**定义 `LITEARM_BENCH_MODEL_AXIS` ⇒ 逐值比对 (旧固件树); + · 固件**已移除** ⇒ 断言守卫仍在, 且 SDK 常量仍在 —— SDK 仍要服务 1J 老固件 + (`Litearm1.7.0-1J`, 见 `test_ik_bench.py` / `conftest.offline_arm_1j`), + 故这里 assert 的是"**这条 legacy 通路没被人当死代码删掉**", 不是放行。 + + ⚠ 判据**只认 `#define` 行**, 不走 `#if\\s+LITEARM_BENCH_1J(.*?)#else`: + 旧写法带 `re.S`, 而 joint_cfg.h 的**注释里**同时出现 `#if LITEARM_BENCH_1J` + 与 `#else` 字样 (逐字讨论 litearm.h 的两支结构) ⇒ 正则**从注释匹配到注释**, + 于是这条断言被一个**假匹配**触发、报"找不到台架分支", 归因指向错误的地方 + (实测 2026-10-10)。 + """ src = _read(JOINT_CFG_H) - m = re.search(r"#if\s+LITEARM_BENCH_1J(.*?)#else", src, re.S) - assert m, "joint_cfg.h 结构变了 (找不到 #if LITEARM_BENCH_1J ... #else)" - ax = re.search(r"#define\s+LITEARM_BENCH_MODEL_AXIS\s+(\d+)", m.group(1)) - assert ax, "找不到台架分支的 LITEARM_BENCH_MODEL_AXIS" - assert int(ax.group(1)) == P.BENCH_MODEL_AXIS, ( - f"固件台架模型轴 = {ax.group(1)}, SDK BENCH_MODEL_AXIS = {P.BENCH_MODEL_AXIS} " - f"—— IK 种子会填错轴") + m = re.search(r"^[ \t]*#define[ \t]+LITEARM_BENCH_MODEL_AXIS[ \t]+(\d+)[ \t]*$", + src, re.M) + if m: + assert int(m.group(1)) == P.BENCH_MODEL_AXIS, ( + f"固件台架模型轴 = {m.group(1)}, SDK BENCH_MODEL_AXIS = {P.BENCH_MODEL_AXIS} " + f"—— IK 种子会填错轴") + return + assert "#error" in src and "LITEARM_BENCH_1J" in src, ( + "joint_cfg.h 既没有 LITEARM_BENCH_MODEL_AXIS, 也没有防再打开的 `#error` 守卫 " + "—— 台架模式是被**移除**了还是被**改坏**了? 先弄清再改本用例, 别顺手删断言") + assert P.BENCH_MODEL_AXIS == 5, ( + f"固件已移除台架模式, SDK 的 BENCH_MODEL_AXIS 现在只服务 1J 老固件 " + f"(Litearm1.7.0-1J); 当前值 = {P.BENCH_MODEL_AXIS}。真要改它, 请同时更新" + f"本断言与 _protocol.py 上那条 legacy 标注") # ---------------------------------------------------------------- 5. 能力判定假设