From e11f8d2d25929ad291c2e3cbe4db4b5131060692 Mon Sep 17 00:00:00 2001 From: luochun <56000204@qq.com> Date: Tue, 29 Sep 2026 15:48:41 +0800 Subject: [PATCH 1/2] docs: document joint_follow, license and activate Three public entry points had no line in any of the eight documents: joint_follow (added in 97884f3) and license / activate (present since the direct-CDC rewrite, 3d962bb). Record joint_follow in guide section 5.7 and in both READMEs, with the facts read off the firmware rather than the SDK: the accept path clamps q to the soft limits, the firmware slews at its own joint-follow speed table, and the state frame keeps reporting MOVE_MIT_ALL, so the mode field does not tell the host that following is active. Add guide section 5.12 for the license record and the activation call, list license() among the entry points that return no Msg envelope, and add a short README pointer. Verified: 656 passed, 2 skipped (tests/test_doc_examples.py checks every signature and runs every example block against the offline stub). --- README.md | 8 +++++ README.zh-CN.md | 7 ++++ docs/DEVELOPER_GUIDE.md | 68 +++++++++++++++++++++++++++++++++++ docs/DEVELOPER_GUIDE.zh-CN.md | 56 +++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) diff --git a/README.md b/README.md index fc784d0..3d328ee 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ with arm.zero_g(): ```python arm.send_mit(0, 0.0, 0.0, 30.0, 1.0, 0.0) # ⚠ bypasses planning; keep alive at ≥10 Hz +arm.joint_follow([0.0]*7, [0.0]*7, [30.0]*7, [1.0]*7) # no tau: the firmware computes it ``` ### Parameters (`arm.params.*`) @@ -187,6 +188,13 @@ for s in r.samples()[:3]: print(arm.diag.kin_bench().value) # CAN link diagnostic counters ``` +### License and activation + +An arm that has never been activated rejects `enable()` with `ERR{0x10,0x08}` and answers every +other command normally, so it stays diagnosable. `arm.license()` reads the record — a bare +`LicenseInfo`, not a `Msg`; `arm.activate()` submits a vendor-issued credential and needs +`disable()` first. See [the developer guide §5.12](docs/DEVELOPER_GUIDE.md#512-license-and-activation). + ### Persistence ```python diff --git a/README.zh-CN.md b/README.zh-CN.md index c96c903..04da644 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -147,6 +147,7 @@ with arm.zero_g(): ```python arm.send_mit(0, 0.0, 0.0, 30.0, 1.0, 0.0) # ⚠ 绕过规划,需 ≥10 Hz 自己保活 +arm.joint_follow([0.0]*7, [0.0]*7, [30.0]*7, [1.0]*7) # 帧里没有 tau,前馈由固件算 ``` ### 参数(`arm.params.*`) @@ -182,6 +183,12 @@ for s in r.samples()[:3]: print(arm.diag.kin_bench().value) # CAN 链路诊断计数 ``` +### 授权与激活 + +未激活的臂 `enable()` 回 `ERR{0x10,0x08}`,其余命令一律照常,所以现场仍可诊断。 +`arm.license()` 读授权记录(裸 `LicenseInfo`,不是 `Msg`);`arm.activate()` 提交厂商签发的凭据, +须先 `disable()`。见[开发者指南 §5.12](docs/DEVELOPER_GUIDE.zh-CN.md#512-授权与激活)。 + ### 参数持久化 ```python diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 16d9097..75fe31f 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -165,6 +165,7 @@ link. - `n` / `firmware` / `last_reset_reason` / `zero_g_active` — no frame at all - `ik()` — a computation request - `get_ff_mask()` returns a bare `int`; `params.all_joint_params()` returns `list[JointParam]` +- `license()` returns a bare `LicenseInfo` (see §5.12): no firmware-initiated traffic to measure, so `hz` is your own polling rate ### `RobotState` @@ -369,6 +370,7 @@ with arm.zero_g(): ```python send_mit(idx, q, dq, kp, kd, tau) send_mit_all(q, dq, kp, kd, tau) +joint_follow(q, dq, kp, kd) ``` These bypass motion planning, and **the caller must keep them alive**: resend at ≥10 Hz, or the @@ -379,6 +381,30 @@ The five arrays of `send_mit_all` must all have length `n` (checked locally), an finite** — a `NaN` / `Inf` makes the **firmware** reject the whole frame with `ERR{cmd,0x02}`. `send_mit` and `move_js` get the same finiteness check. +`joint_follow` is `send_mit_all` with the feedforward left out of the frame: the firmware +computes `tau_ff = clamp(G(q_measured) + wall, ±tau_max)` on every control tick, so a follow +loop does not spend one `get_gravity()` round trip per tick. Four arrays travel instead of five. + +| Array | Meaning | +| --- | --- | +| `q` | Position target. The firmware clamps it to the joint's soft limits | +| `dq` | Velocity reference for the firmware's MIT loop, **not** a rate limit | +| `kp` / `kd` | MIT gains, clamped by the firmware to its own `MIT_KP_MAX` / `MIT_KD_MAX` | + +All four arrays must have length `n`, checked locally. Being finite is the firmware's call, as +for the rest of this group. + +The firmware slews the position reference toward `q` at its own joint-follow speed table +(J1..J7 = 2.8 / 3.4 / 5.0 / 5.0 / 10.0 / 8.0 / 13.0 rad/s, `control_loop.c`) and clamps `dq` to +the same table. That table is separate from the one the other passthrough commands use, so a +follow session may run faster than `send_mit_all`. + +- `enable()` is required: otherwise the accept path answers `ERR{0x08,0x03}`, also when EMERGENCY is latched. +- `ERR{0x08,0x04}` while hand-guiding runs; `ERR{0x08,0x06}` while the drop-hold latch is set. +- A firmware that predates this command answers `ERR{0x08,0x00}` — it does not fail silently. +- **Any other motion or stop command ends the session** — the next `movej` or `move_js` takes over. +- The state frame still reports `mode = MOVE_MIT_ALL`, so **do not** detect joint following from `state.mode`. + This group has not been fully verified on hardware, see [troubleshooting §16](../TROUBLESHOOTING.md#16-not-yet-verified). @@ -514,6 +540,48 @@ bench_model_axis # bench calibration axis `last_reset_reason` is `None` in normal use, and that is correct: the boot signature is sent once, only after a real MCU reset, and `reset()` does not make it repeat. +### 5.12 License and activation + +```python +license(timeout=1.0) -> LicenseInfo +activate(*, cust_id, issued, flags=0, mac, timeout=2.0) -> None +``` + +A unit ships locked until it is activated: `enable()` answers `ERR{0x10,0x08}` and every other +command keeps working, so the arm stays diagnosable in the field. The activation record is +written once and is never erased. + +`license()` reads that record: + +| Field | Meaning | +| --- | --- | +| `state` / `state_name` | `0` not activated, `1` activated, `2` activated with a factory code | +| `activated` | `state != 0` | +| `factory_mode` | The factory bit. It does **not** mean "activated"; use `activated` | +| `ver` | Record version | +| `uid` / `uid_hex` | The 12-byte MCU UID in raw register order. `uid_hex` is the form the issuing tool needs | +| `cust_id` / `issued` / `flags` | Customer number, issue date `YYYYMMDD`, flags — all `0` while unactivated | + +**Do not** read `activated` as "this arm is usable": an unactivated arm answers `ACK` to +everything except `enable()`, so a follow loop that skips the enable step looks healthy until it +tries to move. + +`activate()` submits a vendor-issued credential. `mac` is 16 bytes (two SipHash-2-4 tags) +produced by the vendor-side issuing tool; this package neither produces nor needs the signing +key. + +- The arm must be disabled first, otherwise the firmware answers `ERR{0x3F,0x04}`. +- `ERR{0x3F,0x02}` is an **aggregate code**, not a specific failure. +- Verify with `license()`, not with the `ACK`: only the record read back proves the write landed. + +The `0x02` aggregate covers a reserved flag bit, an already-activated unit, a MAC mismatch, a +bad key and a failed write. A resend after a lost `ACK` lands in it too, with the unit already +unlocked, so `activate()` re-reads `license()` on `0x02` and raises only if `state` is still +`0` — which is also what the vendor-side tool does. + +Disabling first is the same rule as `save_params()`: flash must not be written while the motors +hold the arm under supervision. + --- ## 6. Exceptions diff --git a/docs/DEVELOPER_GUIDE.zh-CN.md b/docs/DEVELOPER_GUIDE.zh-CN.md index 7364f31..c2b0689 100644 --- a/docs/DEVELOPER_GUIDE.zh-CN.md +++ b/docs/DEVELOPER_GUIDE.zh-CN.md @@ -157,6 +157,7 @@ class Msg(Generic[T]): - `n` / `firmware` / `last_reset_reason` / `zero_g_active` —— 没有帧 - `ik()` —— 计算请求 - `get_ff_mask()` 返回裸 `int`;`params.all_joint_params()` 返回 `list[JointParam]` +- `license()` 返回裸 `LicenseInfo`(见 §5.12)—— 它是请求/应答式的**设备身份记录**,没有固件发起的流量,逐类 `hz` / `timestamp` 只会度量你自己轮询的频率 ### `RobotState` @@ -354,6 +355,7 @@ with arm.zero_g(): ```python send_mit(idx, q, dq, kp, kd, tau) send_mit_all(q, dq, kp, kd, tau) +joint_follow(q, dq, kp, kd) ``` 绕过运动规划,**调用方必须自己保活**:需 ≥10 Hz 重发,否则 0.1 s 看图门狗进入 fail-soft @@ -362,6 +364,28 @@ send_mit_all(q, dq, kp, kd, tau) `send_mit_all` 的五个数组长度必须都等于 `n`(本地校验),且**必须是有限数** —— `NaN` / `Inf` 会被**固件**整帧拒收,回 `ERR{cmd,0x02}`。`send_mit` / `move_js` 同样校验有限性。 +`joint_follow` 就是**帧里不带前馈**的 `send_mit_all`:前馈由固件每个控制拍自己算 +`tau_ff = clamp(G(q_measured) + wall, ±tau_max)`,跟随环因此省掉每拍一次 `get_gravity()` 往返。 +帧里只有四段(少一段 `tau`)。 + +| 段 | 含义 | +| --- | --- | +| `q` | 位置目标。固件会钳到本关节软限位内 | +| `dq` | 送进固件 MIT 环的**速度参考**,不是速率上限 | +| `kp` / `kd` | MIT 增益,固件按自己的 `MIT_KP_MAX` / `MIT_KD_MAX` 钳制 | + +四段长度都必须等于 `n`(本地校验);有限性同本组其余入口,由固件判。 + +固件用**自己的跟随速度表**(J1..J7 = 2.8 / 3.4 / 5.0 / 5.0 / 10.0 / 8.0 / 13.0 rad/s, +`control_loop.c`)对位置参考做斜率限制,并同样钳 `dq`。那张表与其余透传入口用的表 +**不是同一张**,所以跟随会话允许比 `send_mit_all` 跑得快。 + +- `enable()` 是前提:否则受理路径回 `ERR{0x08,0x03}`,EMERGENCY 锁存时同样是这个码。 +- 拖动示教(零重力)进行中回 `ERR{0x08,0x04}`;掉线刚性持位(drop_hold)锁存中回 `ERR{0x08,0x06}`。 +- 早于本命令的固件回 `ERR{0x08,0x00}` —— 不会静默。 +- **任何其它运动/停止命令都会结束本会话** —— 下一条 `movej` 或 `move_js` 接管。 +- 状态帧仍报 `mode = MOVE_MIT_ALL`,**不要**从 `state.mode` 判断跟随是否在进行。 + 本组入口在真机上未经完整验证,见[排障指南 §16](../TROUBLESHOOTING.zh-CN.md#16-尚未验证的部分)。 ### 5.8 子对象 @@ -488,6 +512,38 @@ bench_model_axis # 台架标定轴 `last_reset_reason` 常态是 `None`,那是正确行为:开机签名只在真 MCU 复位后发一次, `reset()` 不会让它重发。 +### 5.12 授权与激活 + +```python +license(timeout=1.0) -> LicenseInfo +activate(*, cust_id, issued, flags=0, mac, timeout=2.0) -> None +``` + +设备出厂即锁定,直到被激活:`enable()` 回 `ERR{0x10,0x08}`,**其余命令一律照常**, +这样现场仍然可诊断。激活记录**写一次、永不擦除**。 + +`license()` 读这条记录: + +| 字段 | 说明 | +| --- | --- | +| `state` / `state_name` | `0` 未激活、`1` 已激活、`2` 已激活且产线模式 | +| `activated` | `state != 0` | +| `factory_mode` | 产线码位。它**不表示**"已激活",判断请用 `activated` | +| `ver` | 记录版本 | +| `uid` / `uid_hex` | 12 字节 MCU UID(原始寄存器序)。签发工具要的是 `uid_hex` 这个形态 | +| `cust_id` / `issued` / `flags` | 客户号、签发日 `YYYYMMDD`、标志位 —— 未激活时全为 `0` | + +**不要**把 `activated` 当成"这台臂能用":未激活的臂除 `enable()` 外全部回 `ACK`, +所以一个跳过了使能步骤的跟随环看起来一切正常,直到它试图运动。 + +`activate()` 提交厂商签发的凭据。`mac` 是 16 字节(两个 SipHash-2-4 标签),由**厂商侧** +签发工具产出;本包既不产生也不需要签名密钥。 + +- 必须先失能,否则固件回 `ERR{0x3F,0x04}` —— 与 `save_params()` 同一个理由:写 flash 期间电机不得在无监督下保持使能。 +- `ERR{0x3F,0x02}` 是**聚合档**:固件把"flags 保留位非 0 / 已经激活过 / MAC 不符 / 密钥非法 / 写或读回失败"全折进它。 +- 因此 `activate()` 见到 `0x02` 会**回读一次 `license()`**,只有 state 确实仍是 `0` 才抛 —— ACK 丢失后重发不该被当成失败。 +- 判据用 `license()` 而不是 `ACK`:`ACK` 只说明固件收下了帧,读回来的记录才是"已落位"的证据。 + --- ## 6. 异常 From 6a5df64017c5667d6cfc8832ce0450081e4d8421 Mon Sep 17 00:00:00 2001 From: luochun <56000204@qq.com> Date: Tue, 29 Sep 2026 15:53:51 +0800 Subject: [PATCH 2/2] docs: conform all eight documents to the documentation rules Section 4 of AGENTS.md (added in fed097a, after the documents were rewritten in PR #2) had never been applied to the documents themselves. - Strip the decorative emoji from headings, bullets, status cells and code comments; the wording carries the warning, and the marks do not survive a terminal or a diff. - Join the soft-wrapped list items onto a single line each. Where a bullet carried more than a line's worth of detail, move the detail into the paragraph beside it instead of leaving a 380-character line. - Remove the fourth heading level: the four sub-object sections of the developer guide are now 5.8 to 5.11, and DFU, persistence, read-only properties and licensing move to 5.12 to 5.15. The two README links follow. - State what each document is and who it is for in its first sentence: field troubleshooting and the examples index only described their own layout. - State the working directory for the install and example commands. Every list item now fits within the 130-column limit in .markdownlint.json, so the two rules do not conflict. Verified: 656 passed, 2 skipped, and a scan of all eight files for emoji, fourth-level headings, soft-wrapped bullets and over-long lines reports nothing. --- README.md | 35 +++++++++++------------ README.zh-CN.md | 28 ++++++++++--------- TROUBLESHOOTING.md | 52 +++++++++++++++++------------------ TROUBLESHOOTING.zh-CN.md | 36 ++++++++++++------------ docs/DEVELOPER_GUIDE.md | 52 ++++++++++++++++------------------- docs/DEVELOPER_GUIDE.zh-CN.md | 40 +++++++++++++-------------- examples/README.md | 5 ++-- examples/README.zh-CN.md | 4 +-- 8 files changed, 123 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 3d328ee..5e99703 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,19 @@ # litearm-python -Python SDK for the LiteArm robotic arm. Connect over USB serial straight to the arm's firmware -and control it from any machine — no server or middleware in between. Trajectory planning, -kinematics and dynamics are all carried by the firmware. +Python SDK for the LiteArm robotic arm: drive the arm from a PC over one USB serial cable, +straight to its firmware — no server or middleware in between. Trajectory planning, kinematics +and dynamics are all carried by the firmware. -> 📖 Full interface reference: [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md). +> Full interface reference: [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md). ## Features -- 🐍 **Pure Python**: Python 3.9+, poses are plain lists / tuples, no numpy needed -- 📦 **A single dependency**: `pyserial`, nothing else -- 🔌 **Direct over USB**: one cable to the firmware, no server in between -- ⚙️ **The firmware does the heavy lifting**: planning, kinematics and dynamics live there; the - PC side just sends points and decides arrival -- 🦾 **Full motion API**: joint moves, Cartesian lines / arcs / waypoints, hand-guiding -- 🛡️ **Safety built in**: fail-closed in child processes, a dedicated emergency stop, and every - irreversible command flagged +- **Pure Python**: Python 3.9+, poses are plain lists / tuples, no numpy needed +- **A single dependency**: `pyserial`, nothing else +- **Direct over USB**: one cable to the firmware, no server in between +- **The firmware does the planning**: kinematics and dynamics live there; the PC sends points and decides arrival +- **Full motion API**: joint moves, Cartesian lines / arcs / waypoints, hand-guiding +- **Safety built in**: fail-closed in child processes, a dedicated emergency stop, and every irreversible command flagged ## Install @@ -26,6 +24,8 @@ kinematics and dynamics are all carried by the firmware. | Firmware | `Litearm1.5.0` or later | | Connection | USB CDC serial, VID:PID `1d50:606f` | +From the repository root: + ```bash pip install -e . python3 -c "import litearm; print(litearm.__version__)" # verify @@ -129,7 +129,7 @@ print(arm.ik((0.30, 0.0, 0.35, 3.1416, 0.0, 0.0))) # pose → joint angles arm.emergency_stop() # emergency stop: no preconditions at all arm.reset() # clear faults + re-anchor (not an MCU reboot) arm.clear_faults() # clears RAM fault bits only -arm.disable() # ⚠ the arm is no longer held once disabled +arm.disable() # the arm is no longer held once disabled ``` ### Feed-forward / dynamics @@ -151,7 +151,7 @@ with arm.zero_g(): ### Passthrough / servo ```python -arm.send_mit(0, 0.0, 0.0, 30.0, 1.0, 0.0) # ⚠ bypasses planning; keep alive at ≥10 Hz +arm.send_mit(0, 0.0, 0.0, 30.0, 1.0, 0.0) # bypasses planning; keep alive at ≥10 Hz arm.joint_follow([0.0]*7, [0.0]*7, [30.0]*7, [1.0]*7) # no tau: the firmware computes it ``` @@ -193,12 +193,12 @@ print(arm.diag.kin_bench().value) # CAN link diagnostic counters An arm that has never been activated rejects `enable()` with `ERR{0x10,0x08}` and answers every other command normally, so it stays diagnosable. `arm.license()` reads the record — a bare `LicenseInfo`, not a `Msg`; `arm.activate()` submits a vendor-issued credential and needs -`disable()` first. See [the developer guide §5.12](docs/DEVELOPER_GUIDE.md#512-license-and-activation). +`disable()` first. See [the developer guide §5.15](docs/DEVELOPER_GUIDE.md#515-license-and-activation). ### Persistence ```python -arm.save_params() # ⚠ writes flash, irreversible +arm.save_params() # writes flash, irreversible ``` ### Read-only properties @@ -301,7 +301,8 @@ See [examples/README.md](examples/README.md): - `06_cartesian.py` — Cartesian lines / arcs / waypoints - `07_vel_jitter_trace.py` — 300 Hz per-tick capture -The examples are **read-only by default**; anything that moves needs `--go`: +The examples are **read-only by default**; anything that moves needs `--go`. Run them from +the repository root: ```bash source env.sh # exports PYTHONPATH / PYTHON_BIN / LITEARM_PORT diff --git a/README.zh-CN.md b/README.zh-CN.md index 04da644..d1305c0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,18 +1,18 @@ # litearm-python -LiteArm 机械臂的 Python SDK。通过 USB 串口直连机械臂固件,即可从任意机器控制机械臂 —— +LiteArm 机械臂的 Python SDK:从 PC 经一根 USB 串口线直连固件驱动机械臂 —— 不需要服务端或中间件。轨迹规划、运动学、动力学全部由固件承担。 -> 📖 完整接口说明见 [docs/DEVELOPER_GUIDE.zh-CN.md](docs/DEVELOPER_GUIDE.zh-CN.md)。 +> 完整接口说明见 [docs/DEVELOPER_GUIDE.zh-CN.md](docs/DEVELOPER_GUIDE.zh-CN.md)。 ## 特性 -- 🐍 **纯 Python**:Python 3.9+,位姿就是普通的 list / tuple,不需要 numpy -- 📦 **单一依赖**:只有 `pyserial` -- 🔌 **USB 直连**:一根线接到固件,不经过任何服务端 -- ⚙️ **重活交给固件**:规划、运动学、动力学都在固件里,PC 侧只发点、判到位 -- 🦾 **完整运动接口**:关节运动、笛卡尔直线 / 圆弧 / 多路点、拖动示教 -- 🛡️ **安全兜底**:子进程 fail-closed,急停独立入口,不可逆命令逐个标注 +- **纯 Python**:Python 3.9+,位姿就是普通的 list / tuple,不需要 numpy +- **单一依赖**:只有 `pyserial` +- **USB 直连**:一根线接到固件,不经过任何服务端 +- **规划在固件里**:运动学、动力学都在固件里,PC 侧只发点、判到位 +- **完整运动接口**:关节运动、笛卡尔直线 / 圆弧 / 多路点、拖动示教 +- **安全兜底**:子进程 fail-closed,急停独立入口,不可逆命令逐个标注 ## 安装 @@ -23,6 +23,8 @@ LiteArm 机械臂的 Python SDK。通过 USB 串口直连机械臂固件,即 | 固件 | `Litearm1.5.0` 及以上 | | 连接 | USB CDC 串口,VID:PID `1d50:606f` | +在仓库根目录执行: + ```bash pip install -e . python3 -c "import litearm; print(litearm.__version__)" # 验证 @@ -124,7 +126,7 @@ print(arm.ik((0.30, 0.0, 0.35, 3.1416, 0.0, 0.0))) # 位姿 → 关节角 arm.emergency_stop() # 急停:唯一没有前置条件的入口 arm.reset() # 清故障 + 重锚控制环(不是 MCU 重启) arm.clear_faults() # 只清 RAM 故障位 -arm.disable() # ⚠ 失能后不再被位置环托住 +arm.disable() # 失能后不再被位置环托住 ``` ### 前馈 / 动力学调参 @@ -146,7 +148,7 @@ with arm.zero_g(): ### 透传 / 伺服 ```python -arm.send_mit(0, 0.0, 0.0, 30.0, 1.0, 0.0) # ⚠ 绕过规划,需 ≥10 Hz 自己保活 +arm.send_mit(0, 0.0, 0.0, 30.0, 1.0, 0.0) # 绕过规划,需 ≥10 Hz 自己保活 arm.joint_follow([0.0]*7, [0.0]*7, [30.0]*7, [1.0]*7) # 帧里没有 tau,前馈由固件算 ``` @@ -187,12 +189,12 @@ print(arm.diag.kin_bench().value) # CAN 链路诊断计数 未激活的臂 `enable()` 回 `ERR{0x10,0x08}`,其余命令一律照常,所以现场仍可诊断。 `arm.license()` 读授权记录(裸 `LicenseInfo`,不是 `Msg`);`arm.activate()` 提交厂商签发的凭据, -须先 `disable()`。见[开发者指南 §5.12](docs/DEVELOPER_GUIDE.zh-CN.md#512-授权与激活)。 +须先 `disable()`。见[开发者指南 §5.15](docs/DEVELOPER_GUIDE.zh-CN.md#515-授权与激活)。 ### 参数持久化 ```python -arm.save_params() # ⚠ 写入 flash,不可逆 +arm.save_params() # 写入 flash,不可逆 ``` ### 只读属性 @@ -285,7 +287,7 @@ p.start() - `06_cartesian.py` — 笛卡尔直线 / 圆弧 / 多路点 - `07_vel_jitter_trace.py` — 300 Hz 逐拍采集 -样例**默认只读**,会运动的必须加 `--go`: +样例**默认只读**,会运动的必须加 `--go`。都在仓库根目录执行: ```bash source env.sh # 导出 PYTHONPATH / PYTHON_BIN / LITEARM_PORT diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 07e544e..8f6f0ef 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -1,7 +1,8 @@ # litearm-python field troubleshooting -Every entry follows the same three beats: **symptom → cause → what to do**. Start with -the lookup table below. +For anyone debugging a LiteArm driven by this SDK: you have a symptom and no cause. Every +entry follows the same three beats: **symptom → cause → what to do**. Start with the lookup +table below. Suggested order of investigation: check the **error code** first (§4 warns about two code spaces that are easy to confuse), then the **link diagnostics counters** (§11), and @@ -47,7 +48,7 @@ only then suspect the cabling. **What to do**: for `FirmwareMismatchError`, flash `Litearm1.5.0` or later. For a busy port, shut down whatever is holding it. -⚠ Device **re-enumeration** (unplug/replug, after `enter_dfu()`, a real power cycle) makes +Device **re-enumeration** (unplug/replug, after `enter_dfu()`, a real power cycle) makes `/dev/ttyACM*` **change number**. A script pinned to `LITEARM_PORT` now points at a port that does not exist — the most common reason for "it worked a minute ago". @@ -67,7 +68,7 @@ that does not exist — the most common reason for "it worked a minute ago". | `ERR{0x10,0x07}` | Resending is useless | Consult the firmware code table | | `ERR{0x10,0x00}` | **The firmware does not have this command** | Firmware too old — update it | -⚠ A close cousin that often gets mixed in: **with the arm not enabled, `movej` is rejected +A close cousin that often gets mixed in: **with the arm not enabled, `movej` is rejected with `ERR[01,3]`**, and its message points at **two** possibilities at once — "not enabled, **or** EMERGENCY latched". Do not read only the first half. @@ -86,7 +87,7 @@ See [README](README.md#multiprocessing-a-forked-child-must-not-use-an-inherited- | A command "times out", but a retry "sometimes works" | The command did go out; the reply was read by the parent ⇒ the retry is a **duplicate command** | | `get_state()` does not raise, but the numbers **never change** | It silently returns the inherited, **stale** value — the subtlest case | | `connect()` raises in the child | The parent still holds the port ⇒ the parent must `close()` first | -| Any command immediately raises `ForkedSessionError` | ✅ The guard is **working**, not failing | +| Any command immediately raises `ForkedSessionError` | The guard is **working**, not failing | **What to do**: `close()` the parent session to release the port, then `fork`, then **create** a new `Arm` inside the child. @@ -146,10 +147,10 @@ hits this; concurrent use across processes or clients does. **What to do**: pick three points that are not collinear; when starting from a singular pose, leave the singularity first. -⚠ In `move_c(start, via, goal)` the **`start` must match the measured TCP at call time** +In `move_c(start, via, goal)` the **`start` must match the measured TCP at call time** (tolerance 6 mm / 0.03 rad). It is not a free "start from here" parameter — it is **validated against reality**, so a mid-motion TCP as the start point is rejected. -⚠ The **orientation of `via` is ignored**; only its position defines the circle. +The **orientation of `via` is ignored**; only its position defines the circle. --- @@ -205,7 +206,7 @@ is expected. It only has a value when you connect **soon after a real reset** (`"normal"` / `"iwdg-rst"`). -⚠ A related clarification: **`reset()` is a software state reset, not an MCU reboot.** It does +A related clarification: **`reset()` is a software state reset, not an MCU reboot.** It does **not** trigger USB re-enumeration, **the same `Arm` object keeps working afterwards**, and the signature is not resent. @@ -222,7 +223,7 @@ read" — and that failure is **silent**: no error, just a healthy-looking 0. **What to do**: before using it as a link health check, confirm frames are really arriving — look at `Msg.hz` / `Msg.timestamp`. If both are `0.0`, no frame of that kind has ever arrived. -⚠ Among the counters, `crc` is live and exact; `can_tx_fail` can be surprisingly large (a +Among the counters, `crc` is live and exact; `can_tx_fail` can be surprisingly large (a firmware-reported cumulative value whose exact definition has not been verified). --- @@ -250,24 +251,23 @@ slowly under gravity — the measured sag matches a "0.6× stiffness + τ=0" est **What to do**: keep resending at ≥10 Hz for as long as the motion is needed. -⚠ Arrays must be **length `n`** (checked locally) and **finite** — a `NaN` / `Inf` makes the +Arrays must be **length `n`** (checked locally) and **finite** — a `NaN` / `Inf` makes the firmware reject the whole frame (`ERR{cmd,0x02}`). `send_mit`, `send_mit_all` and `move_js` all get this check. -⚠ In `move_js`, `dq` is a **velocity reference**, not a limit. +In `move_js`, `dq` is a **velocity reference**, not a limit. --- ## 14. After `enter_dfu()` -- **You cannot flash immediately**: `ACK{0x15}` only means "registered"; the device has to - **re-enumerate** as `0483:DF11`. Running pyocd right away fails; retry after ten-odd seconds - and it succeeds. -- To tell that the device has really gone, use a **read or write raising an error** — **not** - `is_open`, and **not** "we read 0 bytes". -- After it returns successfully, **this `Arm` is unusable**: every entry point raises - `ArmIsInDfuError` (`close()` excepted). After flashing, **create a new `Arm`**. -- Calling it while enabled is **rejected locally** (the jump stops TIM3 ⇒ the motors release - within 100 ms and sag under load). +- **You cannot flash immediately**: `ACK{0x15}` only means "registered"; the device has to **re-enumerate** as `0483:DF11` first. +- To tell that the device has really gone, use a **read or write raising an error** — not `is_open` and not "we read 0 bytes". +- After it returns successfully, **this `Arm` is unusable**: every entry point raises `ArmIsInDfuError`, `close()` excepted. +- After flashing, **create a new `Arm`**. +- Calling it while enabled is **rejected locally** (the jump stops TIM3 ⇒ the motors release within 100 ms and sag under load). + +Running pyocd right after the `ACK` fails; retrying ten-odd seconds later succeeds. "We read 0 +bytes" is not evidence that the device is gone — only a read or a write that raises is. --- @@ -295,13 +295,11 @@ Do not assume any of the following has been verified: - `reset_factory()` (it wipes tuned parameters); - The **success** path of `move_c()` — no reliably successful arc was constructed; - `move_js` / `send_mit` / `send_mit_all` — never run on hardware; -- The **effective narrowing** of `set_joint_limits()` (only "writing the old value back is - rejected" was verified); -- Whether **`capture()` always records 0 ticks with the arm disabled** — this appears only in - this repo's field notes, with no matching check or test in the code; not re-verified; +- The **effective narrowing** of `set_joint_limits()` (only "writing the old value back is rejected" was verified); +- Whether **`capture()` always records 0 ticks with the arm disabled** — field notes only, no test in the code; - **Windows** — not yet verified. -### ⚠ Irreversible commands: do not run these on a calibrated arm +### Irreversible commands: do not run these on a calibrated arm All four **overwrite or erase that unit's per-arm identified dynamics model**, and **there is no undo**: @@ -315,10 +313,10 @@ no undo**: **The only way to be safe: do it on a board whose calibration has no value.** -⚠ Separately, `0x33` `model.set_jm()` **should never be called** — it rewrites the joint +Separately, `0x33` `model.set_jm()` **should never be called** — it rewrites the joint mapping (including signs), a mistake there can make the arm **flail**, and the only local recovery options (`revert` / `save_params`) are both in the table above ⇒ **there is no reliable way back**. -⚠ When stress-testing the CAN link, run **`candump` (read-only) only — never `cangen`**: +When stress-testing the CAN link, run **`candump` (read-only) only — never `cangen`**: `can0` *is* the motor bus. diff --git a/TROUBLESHOOTING.zh-CN.md b/TROUBLESHOOTING.zh-CN.md index ae58cdf..b94a47d 100644 --- a/TROUBLESHOOTING.zh-CN.md +++ b/TROUBLESHOOTING.zh-CN.md @@ -1,6 +1,7 @@ # litearm-python 现场排障 -每条都是三段式:**现象 → 原因 → 怎么办**。先看下面的速查表定位。 +给调试本 SDK 所驱动机械臂的人:你有现象,但不知道原因。每条都是三段式: +**现象 → 原因 → 怎么办**。先看下面的速查表定位。 排查顺序建议:先看**错误码**(§4 提醒了两个容易混淆的码空间),再看**链路诊断计数**(§11), 最后才怀疑线路。 @@ -43,7 +44,7 @@ **怎么办**:`FirmwareMismatchError` 就升级固件到 `Litearm1.5.0` 及以上。端口被占就关掉占用方。 -⚠ 设备**重新枚举**(拔插、`enter_dfu()` 之后、真断电重启)会让 `/dev/ttyACM*` **换号**。 +设备**重新枚举**(拔插、`enter_dfu()` 之后、真断电重启)会让 `/dev/ttyACM*` **换号**。 被 `LITEARM_PORT` 锁死的脚本这时会指向一个不存在的端口——这是"刚才还好好的"最常见的原因。 --- @@ -62,7 +63,7 @@ | `ERR{0x10,0x07}` | 重发无用 | 查固件码表 | | `ERR{0x10,0x00}` | **固件没有这条命令** | 固件太旧,升级 | -⚠ 一条容易混进来的近亲:**未使能时 `movej` 会被拒 `ERR[01,3]`**, +一条容易混进来的近亲:**未使能时 `movej` 会被拒 `ERR[01,3]`**, 它的文案会**同时**点出两种可能——「未使能,**或** EMERGENCY 锁存」。别只看前半句。 --- @@ -79,7 +80,7 @@ | 命令"超时无应答",但重试"有时又好了" | 命令真的发出去了,应答被父进程读走 ⇒ 重试 = **重复下发** | | `get_state()` 不报错,但读数**永远不动** | 静默返回继承来的**陈旧**值——最隐蔽的一种 | | 子进程里 `connect()` 抛异常 | 父进程还持有端口 ⇒ 父进程必须先 `close()` | -| 任何命令立刻抛 `ForkedSessionError` | ✅ 守卫**正常工作**,不是在报错 | +| 任何命令立刻抛 `ForkedSessionError` | 守卫**正常工作**,不是在报错 | **怎么办**:先 `close()` 父进程的会话释放串口,再 `fork`,然后在子进程里**新建** `Arm`。 @@ -131,9 +132,9 @@ **怎么办**:换一组不共线的三点;从奇异位姿出发时先离开奇异点再走圆弧。 -⚠ `move_c(start, via, goal)` 的 **`start` 必须与调用时的实测 TCP 一致**(容差 6 mm / 0.03 rad)。 +`move_c(start, via, goal)` 的 **`start` 必须与调用时的实测 TCP 一致**(容差 6 mm / 0.03 rad)。 它不是"从哪儿走"的自由参数,是**校验收到的**——拿运动中的 TCP 当起点会被拒。 -⚠ `via` 的**姿态被忽略**,只有位置参与定圆。 +`via` 的**姿态被忽略**,只有位置参与定圆。 --- @@ -180,7 +181,7 @@ 它只在**真复位之后尽快连上**这一种情形下才有值(`"normal"` / `"iwdg-rst"`)。 -⚠ 顺带澄清一个近亲:**`reset()` 是软件状态复位,不是 MCU 重启**—— +顺带澄清一个近亲:**`reset()` 是软件状态复位,不是 MCU 重启**—— 它**不会**触发 USB 重新枚举,**同一个 `Arm` 对象事后照常可用**,签名也不会重发。 --- @@ -196,7 +197,7 @@ **怎么办**:用它做链路体检前,先确认真的读到了帧——看 `Msg.hz` / `Msg.timestamp`, 两者都是 `0.0` 说明这一类帧从没到过。 -⚠ 计数里 `crc` 是活的且精确;`can_tx_fail` 的量级可能异常大(固件自述的累计值,口径未核)。 +计数里 `crc` 是活的且精确;`can_tx_fail` 的量级可能异常大(固件自述的累计值,口径未核)。 --- @@ -221,19 +222,19 @@ **怎么办**:按 ≥10 Hz 的节奏持续重发,直到不再需要这个动作。 -⚠ 数组必须**长度 = `n`**(本地校验)且**是有限数** —— `NaN` / `Inf` 会被固件整帧拒收 +数组必须**长度 = `n`**(本地校验)且**是有限数** —— `NaN` / `Inf` 会被固件整帧拒收 (`ERR{cmd,0x02}`),`send_mit` / `send_mit_all` / `move_js` 都做这个校验。 -⚠ `move_js` 的 `dq` 是**速度参考**,不是限位。 +`move_js` 的 `dq` 是**速度参考**,不是限位。 --- ## 14. `enter_dfu()` 之后 -- **不能立刻刷**:`ACK{0x15}` 只表示"已登记",设备要**重新枚举**成 `0483:DF11`。 - 立刻跑 pyocd 会失败,隔十几秒重跑就成功。 +- **不能立刻刷**:`ACK{0x15}` 只表示"已登记",要等设备**重新枚举**成 `0483:DF11`。 +- 立刻跑 pyocd 会失败,隔十几秒重跑就成功。 - 判定"设备真的消失了"要用**读 / 写抛错**,**不是** `is_open`,**更不是**"读到 0 字节"。 - 成功返回后**本 `Arm` 不可再用**:所有入口抛 `ArmIsInDfuError`(`close()` 例外)。 - 烧完固件**新建一个 `Arm`**。 +- 烧完固件**新建一个 `Arm`**。 - 使能中调用会被**本地拒绝**(跳转会停 TIM3 ⇒ 电机 100 ms 松开、有负载则下垂)。 --- @@ -261,11 +262,10 @@ - `move_c()` 的**成功**路径 —— 未构造出稳定成功的圆弧; - `move_js` / `send_mit` / `send_mit_all` —— 未上真机; - `set_joint_limits()` 的**有效收窄**(只验证了"写回原值被拒"); -- **失能态下 `capture()` 是否恒录到 0 拍** —— 只在本仓库的现场笔记里出现过,代码里没有 - 对应的判据或测试,未复核; +- **失能态下 `capture()` 是否恒录到 0 拍** —— 只在本仓库的现场笔记里出现过,代码里没有 对应的判据或测试,未复核; - **Windows 平台** —— 尚未验证。 -### ⚠ 不可逆命令:不要在标定过的臂上执行 +### 不可逆命令:不要在标定过的臂上执行 下面四条都会**覆盖或抹掉该台设备逐台辨识的动力学模型**,**没有撤销**: @@ -278,8 +278,8 @@ **唯一解锁方式:在一台没有标定价值的板子上做。** -⚠ 另有 `0x33` `model.set_jm()` **建议永不调用**——它改关节映射(含符号), +另有 `0x33` `model.set_jm()` **建议永不调用**——它改关节映射(含符号), 改错有**乱飞**风险,而本机唯一的恢复手段(`revert` / `save_params`)恰好都在上面那张表里 ⇒ **没有可依赖的退路**。 -⚠ 压测 CAN 链路时**只开 `candump`(只读),绝不 `cangen`**——`can0` 就是电机总线。 +压测 CAN 链路时**只开 `candump`(只读),绝不 `cangen`**——`can0` 就是电机总线。 diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 75fe31f..11c88d3 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -144,18 +144,16 @@ class Msg(Generic[T]): `hz` is the average rate since the first frame of that kind arrived in this session, and it is **`0.0` with fewer than 2 samples**. -- Passive streams (`RSP_STATUS`, 100 Hz): converge to about 100 after two or three frames; an - idle link does not make it decay. -- The 4 request/response ones (`params.get_joint_param` / `model.get_body` / `model.get_jm` / - `model.get_gravity`): one call yields one frame, so **the first call is always `hz == 0.0`**, - and from the second call on it equals **your own polling rate**. -- ⚠ `diag.kin_bench()` is **not** in that group: its reply is **two consecutive frames** (a - timing frame plus a LINK frame), so one call delivers 2 frames — `hz` is non-zero on the very - first call, and that number is **meaningless** (the numerator is inflated by the split frame - while the denominator is still the call interval). Use `timestamp` to tell whether a frame - ever arrived. +- Passive streams (`RSP_STATUS`, 100 Hz): converge to about 100 after two or three frames; an idle link does not make it decay. +- The 4 request/response getters: one call yields one frame, so **the first call is always `hz == 0.0`**. +- `diag.kin_bench()` is **not** in that group: its reply is **two consecutive frames**, so `hz` is meaningless on it. - `reconnect()` resets it. +The 4 request/response getters are `params.get_joint_param`, `model.get_body`, `model.get_jm` and +`model.get_gravity`; from the second call on, `hz` reports **your own polling rate**. For +`diag.kin_bench()` the numerator is inflated by the split frame while the denominator is still +the call interval, so read `timestamp` to tell whether a frame ever arrived. + **`hz == 0.0` and `timestamp == 0.0` mean no frame of this kind has ever arrived**, not a slow link. @@ -165,7 +163,7 @@ link. - `n` / `firmware` / `last_reset_reason` / `zero_g_active` — no frame at all - `ik()` — a computation request - `get_ff_mask()` returns a bare `int`; `params.all_joint_params()` returns `list[JointParam]` -- `license()` returns a bare `LicenseInfo` (see §5.12): no firmware-initiated traffic to measure, so `hz` is your own polling rate +- `license()` returns a bare `LicenseInfo` (see §5.15): no firmware-initiated traffic to measure, so `hz` is your own polling rate ### `RobotState` @@ -190,7 +188,7 @@ Derived properties: `n`, `enabled`, `cart_busy`, `q`, `dq`, `tau`, `fault_axes`, `get_status_now()` **actively sends a `GET_STATUS`**, unlike `get_state()` which only consumes the passive stream — that makes it useful for confirming the link is alive. -⚠ `get_status_now(timeout=0.0)` is **not** a non-blocking poll; it means **return the current +`get_status_now(timeout=0.0)` is **not** a non-blocking poll; it means **return the current cached value immediately**. It raises `MotionTimeoutError` if this session has not received a single state frame yet. @@ -359,8 +357,7 @@ with arm.zero_g(): ``` - Keep-alive is resent by a background SDK thread; `period` must be within `[0.005, 0.10)`. -- Other motion commands are rejected while keep-alive runs; queries are unaffected, and - emergency stop / disable are exceptions. +- Other motion commands are rejected while keep-alive runs; queries are unaffected, and emergency stop / disable are exceptions. - The exit is asynchronous; the firmware needs a moment after `zero_g_stop()` returns. - If keep-alive breaks on a write failure, the exit raises rather than failing silently. - The read-only `zero_g_active` / `zero_g_error` report the state. @@ -408,9 +405,7 @@ follow session may run faster than `send_mit_all`. This group has not been fully verified on hardware, see [troubleshooting §16](../TROUBLESHOOTING.md#16-not-yet-verified). -### 5.8 Sub-objects - -#### `arm.params.*` — per-joint parameters +### 5.8 `arm.params.*` — per-joint parameters ```python set_joint_param(idx, kp, kd, tau_max) @@ -426,7 +421,7 @@ reset_factory() widening request and rejected (`ERR[23,2]`), so it is not idempotent. `reset_factory()` requires the disabled state and is irreversible. -#### `arm.model.*` — online dynamics model import +### 5.9 `arm.model.*` — online dynamics model import ```python probe() -> bool @@ -446,7 +441,7 @@ Writes land in a staging layer and do not take effect immediately; judge by `sta `commit(expected_mask)` applies them, `revert()` discards them. **Both require the disabled state**; while enabled they return `ERR{0x32,0x04}` / `ERR{0x37,0x04}` respectively. -⚠ `revert()` **rolls back RAM only — it does not touch flash.** Three consequences you need to +`revert()` **rolls back RAM only — it does not touch flash.** Three consequences you need to know: 1. the imported model in flash is still there, so it **comes back on the next power cycle**; @@ -458,7 +453,7 @@ know: mistake there can make the arm flail, and the only local recovery options are themselves irreversible. -#### `arm.log.*` — 300 Hz control-tick capture +### 5.10 `arm.log.*` — 300 Hz control-tick capture ```python start(n_ticks) @@ -485,7 +480,7 @@ Constants: `LOG_MAX_SAMPLES = 2400` (about 8 s @300 Hz, stops when full), `CTRL_ `reader()` reads back in chunks following the firmware cursor, retrying automatically on a dropped chunk. `dump()` writes the raw byte stream to disk. -#### `arm.diag.*` — firmware self-test +### 5.11 `arm.diag.*` — firmware self-test ```python kin_bench(timeout=8.0) -> Msg[KinBenchResult] @@ -499,7 +494,7 @@ It is the only source of return-link diagnostic counters, but **all-zero counter nothing was actually read** — see [troubleshooting §11](../TROUBLESHOOTING.md#11-every-kin_bench-counter-reads-0). -### 5.9 Firmware update (DFU) +### 5.12 Firmware update (DFU) ```python enter_dfu(timeout=0.3) -> None @@ -507,16 +502,15 @@ enter_dfu(timeout=0.3) -> None The only terminal-state operation: enters the ROM bootloader without a probe. -- Two-stage: `ACK{0x15}` only means registered; you still wait for the device to disappear from - CDC. +- Two-stage: `ACK{0x15}` only means registered; you still wait for the device to disappear from CDC. - Rejected locally while enabled (the jump stops TIM3, motors release within 100 ms). -- Afterwards this `Arm` is unusable (every entry point raises `ArmIsInDfuError`, `close()` - excepted); the device re-enumerates as `0483:DF11`, and after flashing you create a new `Arm`. +- Afterwards this `Arm` is unusable: every entry point raises `ArmIsInDfuError`, `close()` excepted. +- The device re-enumerates as `0483:DF11`; after flashing you create a new `Arm`. - If the device does not disappear before the timeout it raises, and the object stays usable. You cannot flash immediately after entering DFU; wait for USB re-enumeration. -### 5.10 Persistence +### 5.13 Persistence ```python save_params() -> None @@ -524,7 +518,7 @@ save_params() -> None Writes flash, irreversible. -### 5.11 Read-only properties +### 5.14 Read-only properties ```python params / model / log / diag # sub-objects @@ -540,7 +534,7 @@ bench_model_axis # bench calibration axis `last_reset_reason` is `None` in normal use, and that is correct: the boot signature is sent once, only after a real MCU reset, and `reset()` does not make it repeat. -### 5.12 License and activation +### 5.15 License and activation ```python license(timeout=1.0) -> LicenseInfo diff --git a/docs/DEVELOPER_GUIDE.zh-CN.md b/docs/DEVELOPER_GUIDE.zh-CN.md index c2b0689..802f823 100644 --- a/docs/DEVELOPER_GUIDE.zh-CN.md +++ b/docs/DEVELOPER_GUIDE.zh-CN.md @@ -141,14 +141,14 @@ class Msg(Generic[T]): `hz` = 该类帧自本会话首次到达起的平均频率,**样本不足 2 条时为 `0.0`**。 - 被动连续流(`RSP_STATUS`,100 Hz):两三帧后收敛到约 100,链路空闲不会衰减。 -- 单发请求/应答式的 4 个(`params.get_joint_param` / `model.get_body` / `model.get_jm` / - `model.get_gravity`):一次调用只到一帧,**第一次调用必然 `hz == 0.0`**,第二次起等于 - **你自己的轮询频率**。 -- ⚠ `diag.kin_bench()` **不在**上面那一组:它的回执是**连续两帧**(耗时帧 + LINK 帧), - 一次调用到 2 帧,所以第一次调用 `hz` 就非 0,而且那个数**没有意义**(分子被拆帧放大、 - 分母还是调用间隔)。判"有没有读到"要看 `timestamp`。 +- 单发请求/应答式的 4 个:一次调用只到一帧,**第一次调用必然 `hz == 0.0`**。 +- `diag.kin_bench()` **不在**上面那一组:它的回执是**连续两帧**(耗时帧 + LINK 帧),所以 `hz` 在它上面没有意义。 - `reconnect()` 后归零。 +那 4 个请求/应答式入口是 `params.get_joint_param`、`model.get_body`、`model.get_jm` 与 +`model.get_gravity`;第二次起 `hz` 等于**你自己的轮询频率**。`diag.kin_bench()` 则是分子被拆帧放大、 +分母还是调用间隔,判"有没有读到"要看 `timestamp`。 + **`hz == 0.0` 且 `timestamp == 0.0` 表示这类帧从没到过**,不是链路慢。 ### 不返回 `Msg` 的入口 @@ -157,7 +157,7 @@ class Msg(Generic[T]): - `n` / `firmware` / `last_reset_reason` / `zero_g_active` —— 没有帧 - `ik()` —— 计算请求 - `get_ff_mask()` 返回裸 `int`;`params.all_joint_params()` 返回 `list[JointParam]` -- `license()` 返回裸 `LicenseInfo`(见 §5.12)—— 它是请求/应答式的**设备身份记录**,没有固件发起的流量,逐类 `hz` / `timestamp` 只会度量你自己轮询的频率 +- `license()` 返回裸 `LicenseInfo`(见 §5.15)—— 它是请求/应答式的**设备身份记录**,没有固件发起的流量,逐类 `hz` / `timestamp` 只会度量你自己轮询的频率 ### `RobotState` @@ -182,7 +182,7 @@ get_status_now(timeout=0.5) -> Msg[RobotState] `get_status_now()` 会**主动发一条 `GET_STATUS`**,与只消费被动流的 `get_state()` 不同, 可用来确认链路活性。 -⚠ `get_status_now(timeout=0.0)` **不是**"非阻塞探一帧"——它的意思是**立刻返回当前缓存**。 +`get_status_now(timeout=0.0)` **不是**"非阻塞探一帧"——它的意思是**立刻返回当前缓存**。 本会话还**一帧都没收到过**时它抛 `MotionTimeoutError`。 --- @@ -388,9 +388,7 @@ joint_follow(q, dq, kp, kd) 本组入口在真机上未经完整验证,见[排障指南 §16](../TROUBLESHOOTING.zh-CN.md#16-尚未验证的部分)。 -### 5.8 子对象 - -#### `arm.params.*` —— 关节级参数 +### 5.8 `arm.params.*` —— 关节级参数 ```python set_joint_param(idx, kp, kd, tau_max) @@ -405,7 +403,7 @@ reset_factory() `set_joint_limits()` **只许收窄**,写回当前值会被判成放宽请求并拒(`ERR[23,2]`), 所以它不幂等,别拿它做读写回环。`reset_factory()` 要求失能态,且不可逆。 -#### `arm.model.*` —— 动力学模型在线导入 +### 5.9 `arm.model.*` —— 动力学模型在线导入 ```python probe() -> bool @@ -424,7 +422,7 @@ revert() 写入进 staging 层,不立即生效,判据看 `staged_mask`。`commit(expected_mask)` 才应用, `revert()` 丢弃。**两者都要求失能态**,已使能时分别回 `ERR{0x32,0x04}` / `ERR{0x37,0x04}`。 -⚠ `revert()` **只回退 RAM,不动 flash**,三条后果必须知道: +`revert()` **只回退 RAM,不动 flash**,三条后果必须知道: 1. flash 里那份导入模型还在,**重新上电会复活**; 2. 此后任何一次 `save_params()` 会把 flash 里那份**一并抹掉**(整扇区擦除,不可恢复); @@ -433,7 +431,7 @@ revert() **`set_jm()` 建议永不调用**:它改关节映射(含符号),改错有乱飞风险, 而本机唯一的恢复手段本身也不可逆。 -#### `arm.log.*` —— 300 Hz 控制拍采集 +### 5.10 `arm.log.*` —— 300 Hz 控制拍采集 ```python start(n_ticks) @@ -459,7 +457,7 @@ r.total_bytes # 属性 `reader()` 按固件游标分块读回,掉帧自动重试。`dump()` 落盘原始字节流。 -#### `arm.diag.*` —— 固件自检 +### 5.11 `arm.diag.*` —— 固件自检 ```python kin_bench(timeout=8.0) -> Msg[KinBenchResult] @@ -472,7 +470,7 @@ kin_bench(timeout=8.0) -> Msg[KinBenchResult] 它是回链路诊断计数的唯一来源,但**计数器全 0 可能是根本没读到**, 见[排障 §11](../TROUBLESHOOTING.zh-CN.md#11-kin_bench-的计数器全是-0)。 -### 5.9 固件升级(DFU) +### 5.12 固件升级(DFU) ```python enter_dfu(timeout=0.3) -> None @@ -482,13 +480,13 @@ enter_dfu(timeout=0.3) -> None - 两段式:`ACK{0x15}` 只表示已登记,还要等设备真的从 CDC 上消失。 - 使能中本地拒绝(跳转会停 TIM3,电机 100 ms 松开)。 -- 成功后本 `Arm` 不可再用(所有入口抛 `ArmIsInDfuError`,`close()` 例外), - 设备重新枚举成 `0483:DF11`,烧完固件新建一个 `Arm`。 +- 成功后本 `Arm` 不可再用:所有入口抛 `ArmIsInDfuError`,`close()` 例外。 +- 设备重新枚举成 `0483:DF11`,烧完固件新建一个 `Arm`。 - 超时未消失则抛异常,对象照旧可用。 进 DFU 后不能立刻刷,要等 USB 重新枚举。 -### 5.10 参数持久化 +### 5.13 参数持久化 ```python save_params() -> None @@ -496,7 +494,7 @@ save_params() -> None 写 flash,不可逆。 -### 5.11 只读属性 +### 5.14 只读属性 ```python params / model / log / diag # 子对象 @@ -512,7 +510,7 @@ bench_model_axis # 台架标定轴 `last_reset_reason` 常态是 `None`,那是正确行为:开机签名只在真 MCU 复位后发一次, `reset()` 不会让它重发。 -### 5.12 授权与激活 +### 5.15 授权与激活 ```python license(timeout=1.0) -> LicenseInfo diff --git a/examples/README.md b/examples/README.md index 0b7858b..485839e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,7 @@ # litearm-python examples -Each script runs on its own. **Read it before you run it on real hardware.** +Runnable examples for the litearm-python SDK, one script per feature. Each runs on its own; +**read it before you run it on real hardware.** **Read-only by default** — any example that enables, moves or retunes parameters requires an explicit `--go`, so nothing moves by accident. @@ -62,7 +63,7 @@ The examples that move the arm (02 / 03 / 05 / 06 / 07) **really drive it**: - keep `--speed` at 0.1–0.3 the first time; - stand by the emergency stop, and make sure nobody and nothing is in the workspace; - read the [troubleshooting guide](../TROUBLESHOOTING.md) first; -- ⚠ **`movej` does not check joint limits** — an out-of-range target travels the full stroke. +- **`movej` does not check joint limits** — an out-of-range target travels the full stroke. ## Pose format diff --git a/examples/README.zh-CN.md b/examples/README.zh-CN.md index 14c9d26..1aeb76c 100644 --- a/examples/README.zh-CN.md +++ b/examples/README.zh-CN.md @@ -1,6 +1,6 @@ # litearm-python 样例 -每个脚本可独立运行,**先读懂再上真机**。 +litearm-python SDK 的可运行样例,一个脚本一项功能。每个都能独立运行,**先读懂再上真机**。 **默认只读**——会 `enable` / 运动 / 改参的样例必须显式加 `--go`,防误动。 @@ -59,7 +59,7 @@ Windows 上 `LITEARM_PORT` 可以锁定 `COM5` 这类端口。 - 首次运行把 `--speed` 保持在 0.1~0.3; - 人站在急停旁,确保周围无人无障碍; - 跑之前先读 [排障指南](../TROUBLESHOOTING.zh-CN.md); -- ⚠ **`movej` 不校验关节限位**——越限目标会被走满行程。 +- **`movej` 不校验关节限位**——越限目标会被走满行程。 ## 位姿格式