Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/litearm/_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion src/litearm/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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])",
Expand Down Expand Up @@ -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 字节)",
Expand All @@ -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)",
Expand Down
48 changes: 38 additions & 10 deletions tests/test_error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 条, 出处是另一条固件分支)。于是"表里凭空多一条固件发不出的码"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}` 是这样)。
Expand All @@ -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)}),
Expand Down Expand Up @@ -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 祖先),
Expand Down
42 changes: 34 additions & 8 deletions tests/test_protocol_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`。
Expand Down Expand Up @@ -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. 能力判定假设
Expand Down
Loading