litearm-python 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- litearm/__init__.py +116 -0
- litearm/__main__.py +3 -0
- litearm/_protocol.py +513 -0
- litearm/_rot.py +245 -0
- litearm/arm.py +2656 -0
- litearm/cart.py +1403 -0
- litearm/diagnostics.py +197 -0
- litearm/errors.py +509 -0
- litearm/log.py +233 -0
- litearm/model.py +253 -0
- litearm/params.py +120 -0
- litearm/payload.py +718 -0
- litearm/py.typed +0 -0
- litearm/state.py +171 -0
- litearm/testing.py +959 -0
- litearm/tool.py +171 -0
- litearm/transport.py +375 -0
- litearm_python-1.2.0.dist-info/METADATA +508 -0
- litearm_python-1.2.0.dist-info/RECORD +23 -0
- litearm_python-1.2.0.dist-info/WHEEL +5 -0
- litearm_python-1.2.0.dist-info/entry_points.txt +2 -0
- litearm_python-1.2.0.dist-info/licenses/LICENSE +202 -0
- litearm_python-1.2.0.dist-info/top_level.txt +1 -0
litearm/diagnostics.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""固件自检 —— `CMD_KIN_BENCH(0x49)` -> `RSP_KIN_BENCH(0x4A)` 文本回执的解析。
|
|
2
|
+
|
|
3
|
+
固件侧 (`kin_runner.c` 的 `kin_bench_run`, `:335-564`; LINK 行在 `:514`) 在后台用 DWT
|
|
4
|
+
cycle 测各热点单拍开销 (禁中断测量,
|
|
5
|
+
期间 `g_kin_bench_active` 置位以豁免主循环心跳监督; 最长窗口 5s), 结果拼成多行文本
|
|
6
|
+
经可靠应答通道回。**最有价值的产出是最后一行 LINK** —— 它承载了固件里唯一的链路
|
|
7
|
+
诊断计数 (CRC 坏帧 / RX 环溢出 / 应答 FIFO 丢弃 / CAN TX 失败分类桶 / 控制拍峰值与
|
|
8
|
+
超预算拍数 / 锁存拍与原因), 这些在固件里原本全部静默 (L3/M1 fix)。
|
|
9
|
+
|
|
10
|
+
⚠ `0x49` 是双 ID: 下行 `CMD_KIN_BENCH` 与上行 `RSP_JOINT_PARAM` 同值, 靠收发方向区分。
|
|
11
|
+
⚠ 该命令**不检查 payload 长度**, 发出去必产生一次真实测量 —— 故仅用于显式调用,
|
|
12
|
+
不做连接期探测。
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import TYPE_CHECKING, Dict, Tuple
|
|
19
|
+
|
|
20
|
+
from litearm import _protocol as P
|
|
21
|
+
from litearm.errors import MotionTimeoutError, TransportError
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING: # 避免与 arm 循环导入
|
|
24
|
+
from litearm.arm import Arm, Msg
|
|
25
|
+
|
|
26
|
+
__all__ = ["KinBenchResult", "Diagnostics"]
|
|
27
|
+
|
|
28
|
+
#: 固件 bench 最长窗口 KIN_BENCH_MAX_TICKS = 5s (`control_loop.c`), 留 1.6 倍余量
|
|
29
|
+
KIN_BENCH_TIMEOUT = 8.0
|
|
30
|
+
|
|
31
|
+
#: 固件把 KIN_BENCH 回执拆成**连续两帧**发 (`[H-9 fix 2026-09-13]`) ——
|
|
32
|
+
#: 第 1 帧 = 各项耗时, 第 2 帧 = **LINK 诊断行**。
|
|
33
|
+
#:
|
|
34
|
+
#: 拆帧的理由是单帧载荷上限 255B (`USB_MAX_PAYLOAD`), 而全文实测 ~306B: 原单帧发出会
|
|
35
|
+
#: 在 253B 处硬截断, **截断点恰好落在 LINK 行的 `loop_k=` 之后** ⇒ `loop_k`/`ovr`/
|
|
36
|
+
#: `flt`/`cause`/`st` 一个都读不到 (`kin_runner.c:497-505` 原文)。
|
|
37
|
+
#:
|
|
38
|
+
#: ⚠⚠ **只收一帧必然丢掉全部链路计数, 而丢了它不会报错** —— `KinBenchResult` 的五个
|
|
39
|
+
#: 便捷出口 (`crc_errors` / `reply_dropped` / `can_tx_fail` / `loop_max_kcycle` /
|
|
40
|
+
#: `loop_overruns`) 全是 `link.get(key, 0)`, 拿不到就是**静默的 0**, 与"没有出错"分不开。
|
|
41
|
+
#: 本包迁移时漏了这个双帧契约, 于是把固件刚修好的 H-9 盲区又引入了一次
|
|
42
|
+
#: (真机实证: 回执里没有 LINK 行, `link == {}`, 五个出口全报 0)。
|
|
43
|
+
_KIN_BENCH_FRAMES = 2
|
|
44
|
+
|
|
45
|
+
#: 第 2 帧的等待窗口。**新固件它紧跟第 1 帧到; 旧固件根本不发** ⇒ 靠超时退出,
|
|
46
|
+
#: 与收窄前的行为兼容 (旧工具 `tools/kin_bench.py` 同款口径: "收满两帧再拼接;
|
|
47
|
+
#: 旧固件只会来一帧, 循环靠超时退出")。
|
|
48
|
+
#: ⚠ 代价: 在**只发一帧的旧固件**上, 每次 `kin_bench()` 会多等这半个秒。
|
|
49
|
+
#: 换来的是"新固件的链路计数真的读得到" —— 那五个数正是本命令最实用的产出。
|
|
50
|
+
_KIN_BENCH_MORE_S = 0.5
|
|
51
|
+
|
|
52
|
+
#: 固件 `kin_bench_run` 产出的计时段名 (长名在前, 避免前缀误配)。
|
|
53
|
+
#: ⚠ 固件用 `txt(名字)` + `u32_cat(n)` 拼接, 而 `u32_cat` 只在**数字之后**补空格 ——
|
|
54
|
+
#: 所以名字与第一个数字是**相连**的 (`FK200 108 240 ` / `LOOP14000 `), 不能用
|
|
55
|
+
#: 「名字 + 空白 + 数字」式正则去切。
|
|
56
|
+
_BLOCK_NAMES = ("LOOP1", "RNEA", "JAC", "IK", "FK", "SC", "G", "M", "LAW")
|
|
57
|
+
_TIMING_RE = re.compile(r"^(%s)([\d\s]+)$" % "|".join(_BLOCK_NAMES))
|
|
58
|
+
#: `flt=<锁存拍> /<原因码>` —— 数字与 '/' 之间有一个尾随空格 (u32_cat 语义所致)
|
|
59
|
+
_FLT_RE = re.compile(r"flt=(\d+)\s*/\s*(\d+)")
|
|
60
|
+
#: `key=value` 对。
|
|
61
|
+
#: ⚠⚠ 键名**可以带数字** —— `rxl0` / `rxl1` 就是 (它们正是"RX FIFO 溢出 = 静默丢反馈
|
|
62
|
+
#: = 可能整臂 EMERGENCY"的那两个计数器, 见固件 `kin_runner.c:520-522`)。
|
|
63
|
+
#: 从前写的是 `[a-z_]+`, **匹配不到带数字的键**, 于是那两个静默丢失。
|
|
64
|
+
#: ⚠ 这是**同一个文件里第二次**栽在"名字里带数字"上 —— `_TIMING_RE` 上面那段注释
|
|
65
|
+
#: 早就写过 `LOOP1` 行是 `LOOP14000`, "名字里本身带数字, 故…不能用通用正则";
|
|
66
|
+
#: 而同格式的 `_KV_RE` 还是写窄了。
|
|
67
|
+
_KV_RE = re.compile(r"([a-z_][a-z0-9_]*)=(\d+)")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class KinBenchResult:
|
|
72
|
+
"""KIN_BENCH 回执。`raw` 是固件原文 (逐行), `timings`/`link` 是解析结果。
|
|
73
|
+
|
|
74
|
+
`timings`: 块名 -> 数字元组。`FK/JAC/SC/G/RNEA/M/LAW` 是 `(n, avg_cyc, max_cyc)`,
|
|
75
|
+
`IK` 是 `(n_try, min_cyc, max_cyc)`, `LOOP1` 是 `(cycles,)`。
|
|
76
|
+
`link`: 链路诊断计数 (见模块 docstring)。
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
raw: str
|
|
80
|
+
timings: Dict[str, Tuple[int, ...]] = field(default_factory=dict)
|
|
81
|
+
link: Dict[str, int] = field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
def __str__(self) -> str:
|
|
84
|
+
return self.raw
|
|
85
|
+
|
|
86
|
+
# ---- 常用诊断的便捷出口 ----
|
|
87
|
+
@property
|
|
88
|
+
def crc_errors(self) -> int:
|
|
89
|
+
"""固件累计 CRC 坏帧 (上位机发出的帧被固件丢弃)。"""
|
|
90
|
+
return self.link.get("crc", 0)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def reply_dropped(self) -> int:
|
|
94
|
+
"""固件应答 FIFO 队满丢弃数 (上位机可能等不到某条应答)。"""
|
|
95
|
+
return self.link.get("rfd", 0)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def can_tx_fail(self) -> int:
|
|
99
|
+
"""CAN 发送失败累计。"""
|
|
100
|
+
return self.link.get("txf", 0)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def loop_max_kcycle(self) -> int:
|
|
104
|
+
"""控制拍峰值耗时 (千 cycle)。"""
|
|
105
|
+
return self.link.get("loop_k", 0)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def loop_overruns(self) -> int:
|
|
109
|
+
"""超出周期预算的控制拍数。"""
|
|
110
|
+
return self.link.get("ovr", 0)
|
|
111
|
+
|
|
112
|
+
# ---- 下面三个是固件自己点名的"静默丢反馈"项 (`kin_runner.c:520-524`) ----
|
|
113
|
+
# `[N-12 fix] RX FIFO 溢出 (原为零遥测): 溢出 = 静默丢反馈 = 可能整臂 EMERGENCY`
|
|
114
|
+
# ⚠⚠ 它们的键名**带数字** (`rxl0`/`rxl1`) —— 解析器从前吃不下, 于是这三个出口
|
|
115
|
+
# 恒返回 0。**"读不到"与"没丢过"在本类里长得一模一样**, 见模块里 `_KV_RE` 的注释。
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def rx_fifo_lost_motor(self) -> int:
|
|
119
|
+
"""**电机域** CAN FIFO0 溢出计数 (丢的是电机反馈)。"""
|
|
120
|
+
return self.link.get("rxl0", 0)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def rx_fifo_lost_bridge(self) -> int:
|
|
124
|
+
"""**gs_usb 桥** CAN FIFO1 溢出计数。"""
|
|
125
|
+
return self.link.get("rxl1", 0)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def gsusb_ring_drops(self) -> int:
|
|
129
|
+
"""gs_usb 桥环满丢帧数。"""
|
|
130
|
+
return self.link.get("rbd", 0)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_kin_bench(text: str) -> KinBenchResult:
|
|
134
|
+
"""解析固件 KIN_BENCH 文本; 空文本视为异常回执。"""
|
|
135
|
+
if not text.strip():
|
|
136
|
+
raise TransportError("KIN_BENCH 回执为空 —— 非正常响应, 拒绝当成全 0 返回")
|
|
137
|
+
timings: Dict[str, Tuple[int, ...]] = {}
|
|
138
|
+
link: Dict[str, int] = {}
|
|
139
|
+
for line in text.splitlines():
|
|
140
|
+
line = line.strip()
|
|
141
|
+
if not line:
|
|
142
|
+
continue
|
|
143
|
+
if line.startswith("LINK"):
|
|
144
|
+
body = line[len("LINK"):]
|
|
145
|
+
m = _FLT_RE.search(body)
|
|
146
|
+
if m:
|
|
147
|
+
link["fault_tick"] = int(m.group(1))
|
|
148
|
+
link["fault_cause"] = int(m.group(2))
|
|
149
|
+
body = body[:m.start()] + body[m.end():]
|
|
150
|
+
for k, v in _KV_RE.findall(body):
|
|
151
|
+
if k == "st":
|
|
152
|
+
link["store_from_flash"] = bool(int(v))
|
|
153
|
+
else:
|
|
154
|
+
link[k] = int(v)
|
|
155
|
+
continue
|
|
156
|
+
m = _TIMING_RE.match(line)
|
|
157
|
+
if m:
|
|
158
|
+
timings[m.group(1)] = tuple(int(x) for x in m.group(2).split())
|
|
159
|
+
return KinBenchResult(raw=text, timings=timings, link=link)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class Diagnostics:
|
|
163
|
+
"""`arm.diag` —— 固件自检入口。"""
|
|
164
|
+
|
|
165
|
+
def __init__(self, arm: "Arm"):
|
|
166
|
+
self._arm = arm
|
|
167
|
+
|
|
168
|
+
def kin_bench(self, timeout: float = KIN_BENCH_TIMEOUT) -> "Msg[KinBenchResult]":
|
|
169
|
+
"""触发固件运动学/动力学单拍开销自测, 返回解析后的回执。
|
|
170
|
+
|
|
171
|
+
固件侧最长窗口 5s (期间控制循环心跳监督豁免), 默认超时 8s。
|
|
172
|
+
⚠ 该命令不检查载荷长度, 发出即产生一次真实测量。
|
|
173
|
+
|
|
174
|
+
⚠⚠ **回执是连续两帧, 本方法两帧都收** —— 见 `_KIN_BENCH_FRAMES` 的说明。
|
|
175
|
+
只收第 1 帧会**静默**丢掉全部链路计数 (那五个 `link.get(k, 0)` 出口会全报 0,
|
|
176
|
+
与"没有出错"分不开)。旧固件只发一帧, 靠短窗口超时退出, 行为兼容。
|
|
177
|
+
|
|
178
|
+
返回 :class:`Msg` 信封 (帧 id `RSP_KIN_BENCH`); 单发请求/应答式, 见 `Msg`。
|
|
179
|
+
"""
|
|
180
|
+
arm = self._arm
|
|
181
|
+
arm._write_query(P.CMD_KIN_BENCH) # 查询/自检类: 不受零重力守卫
|
|
182
|
+
# 第 1 帧 (各项耗时) —— 等满 `timeout`
|
|
183
|
+
_, p = arm._require().expect(P.RSP_KIN_BENCH, timeout, "kin_bench",
|
|
184
|
+
echo_cmd=P.CMD_KIN_BENCH)
|
|
185
|
+
parts = [p.decode(errors="replace")]
|
|
186
|
+
# 第 2 帧 (LINK 诊断行) —— 只等一个短窗口: 新固件它紧跟第 1 帧到, 旧固件不发。
|
|
187
|
+
# ⚠ 超时**不是错误** (旧固件只有一帧, 那是兼容情形); 但也**不能**因为
|
|
188
|
+
# "拿不到就算了"而把窗口设成 0 —— 那等于没改, 又回到静默 0。
|
|
189
|
+
while len(parts) < _KIN_BENCH_FRAMES:
|
|
190
|
+
try:
|
|
191
|
+
_, p = arm._require().expect(P.RSP_KIN_BENCH, _KIN_BENCH_MORE_S,
|
|
192
|
+
"kin_bench(LINK 帧)", raise_on_err=False,
|
|
193
|
+
echo_cmd=P.CMD_KIN_BENCH)
|
|
194
|
+
except (MotionTimeoutError, TransportError):
|
|
195
|
+
break
|
|
196
|
+
parts.append(p.decode(errors="replace"))
|
|
197
|
+
return arm._msg(parse_kin_bench("".join(parts)), P.RSP_KIN_BENCH)
|