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
22 changes: 15 additions & 7 deletions docs/DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ All planning happens in the firmware; the PC sends points and receives a `0x4E`
| --- | --- |
| `move_p` | Accepts a single pose only; a sequence raises `InvalidCommandError`. Arrival is judged by TCP tolerance |
| `move_l` / `move_c` / `move_path` | Return a `CartPlan`. `wait` decides **only** whether to wait for the arm to *stop*: `wait=True` (default) blocks until it has settled and fills `settled` / `q_final` / `settle_err_rad`; `wait=False` returns as soon as the firmware's plan reply arrives — the arm is still moving and those three fields are left as "not waited for", **not** as "did not arrive". **Both block until that reply**; `wait=False` is not a non-blocking call |
| `move_c` | `start` must match the measured TCP at call time (tolerance 6 mm / 0.03 rad); write `arm.get_tcp().value` |
| `move_c` | `start` must match the measured TCP at call time (tolerance 6 mm / 0.03 rad); write `arm.get_tcp().value`. A mismatch is checked **locally** and raises `InvalidCommandError` — the `CartesianPlanError` above is for the firmware's own rejections (err 1/2/3). ⚠ The collinear check (`err=2`) has a **scale threshold**: three collinear points 20 mm apart are accepted and planned; 100 mm apart is rejected |
| `poll_cart()` | Reads only the collector's unclaimed queue, does not touch the link. ⚠ **It is not how you collect a `wait=False` result** — there is none to collect: both `wait` values consume the reply before returning, so `poll_cart()` returns `None` after them. Its one real use is recovering the result of a call whose **ACK timed out** (`MotionTimeoutError`) and which therefore left its token unclaimed |
| `set_speed(percent)` | A global, **persistent** governor. `percent` is an **integer percentage 0..100** — `set_speed(1)` means **1% speed**, not "full speed"; it is not the same thing as the per-trajectory factor in `movej(speed=0..1)` |

Expand Down Expand Up @@ -397,10 +397,10 @@ the same table. That table is separate from the one the other passthrough comman
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.
- `ERR{0x08,0x04}` while hand-guiding runs; `ERR{0x08,0x06}` while the drop-hold latch is set. ⚠ `0x08,0x04` is **unreachable from this SDK** when the hand-guiding was started by this client: `zero_g()`'s own guard rejects every other downstream command locally with `InvalidCommandError` **before the frame is sent**. Seeing it needs another host or process to have entered zero-g.
- 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`.
- The state frame still reports `mode = MOVE_MIT_ALL` **in firmware terms**; the SDK renders mode 5 as **`MIT_ALL`** (`_protocol.MODE_NAMES`), so `state.mode_name == "MOVE_MIT_ALL"` is never true. `send_mit_all` reports `MIT_ALL` as well — the two are **indistinguishable** from the state frame, which is the point: **do not** detect joint following from `state.mode`. (`send_mit` is a different mode, `MOVE_MIT`.)

This group has not been fully verified on hardware, see
[troubleshooting §16](../TROUBLESHOOTING.md#16-not-yet-verified).
Expand All @@ -417,9 +417,15 @@ reset_factory()

`JointParam`: `idx`, `kp`, `kd`, `tau_max`, `q_min`, `q_max`.

`set_joint_limits()` **only allows narrowing**: writing the current values back is judged a
widening request and rejected (`ERR[23,2]`), so it is not idempotent. `reset_factory()` requires
the disabled state and is irreversible.
`set_joint_limits()` **only narrows**, and it does so **silently**: the firmware intersects the
request with its compile-time default and answers `ACK`. A widening request — including
**writing the current values back** — is therefore accepted with **no error and no change**.
So the call *is* idempotent, but it also **cannot be used to confirm that a write landed**;
read back with `get_joint_param()`. `ERR{0x23,0x02}` is for a NaN, an empty intersection
(`q_min >= q_max`) or an out-of-range `idx` — **not** for widening.
⚠ Narrowing is **irreversible in RAM**: the intersection only ever shrinks, so there is no way
to widen back without a power cycle. `reset_factory()` requires the disabled state and is
irreversible.

### 5.9 `arm.model.*` — online dynamics model import

Expand Down Expand Up @@ -475,7 +481,9 @@ r.total_bytes # property
```

`LogSample`: `tick`, `q_ref`, `dq`, `tau`.
Constants: `LOG_MAX_SAMPLES = 2400` (about 8 s @300 Hz, stops when full), `CTRL_HZ = 300`.
Constants: `LOG_MAX_SAMPLES = 2400` (about 8 s @300 Hz, stops when full) and `CTRL_HZ = 300`,
both in **`litearm.log`** — unlike `LogSample` / `LogReader` / `parse_samples` they are **not**
re-exported at package level, so `litearm.LOG_MAX_SAMPLES` raises `AttributeError`.

`reader()` reads back in chunks following the firmware cursor, retrying automatically on a
dropped chunk. `dump()` writes the raw byte stream to disk.
Expand Down
13 changes: 7 additions & 6 deletions docs/DEVELOPER_GUIDE.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ set_speed(percent)
| --- | --- |
| `move_p` | 只收单个位姿,传序列抛 `InvalidCommandError`。到位判据是 TCP 容差 |
| `move_l` / `move_c` / `move_path` | 返回 `CartPlan`。`wait` **只决定要不要等臂"停稳"**:`wait=True`(默认)阻塞到停稳并填好 `settled` / `q_final` / `settle_err_rad`;`wait=False` 在固件的规划应答一到就返回——此时臂**还在动**,那三个字段是"**没等**",不是"没到位"。**两种取值都会阻塞到那条应答**,`wait=False` 不是非阻塞调用 |
| `move_c` | `start` 必须与调用时的实测 TCP 一致(容差 6 mm / 0.03 rad),写成 `arm.get_tcp().value` |
| `move_c` | `start` 必须与调用时的实测 TCP 一致(容差 6 mm / 0.03 rad),写成 `arm.get_tcp().value`。超差是**本地**校验,抛 `InvalidCommandError` —— 上面那句 `CartesianPlanError` 只管固件自己的拒绝(err 1/2/3)。⚠ 共线判据(`err=2`)有**尺度门槛**:三点相距 20 mm 会被接受并规划出来,相距 100 mm 才拒 |
| `poll_cart()` | 只读收集器的待认领队列,不碰链路。⚠ **它不是"取 `wait=False` 结果"的手段**——压根没有可取的结果:两种 `wait` 取值都在返回前把应答消费掉了,所以调用它只会拿到 `None`。它唯一的真实用途是取回**入口 ACK 超时**(`MotionTimeoutError`)那条命令的结局——那条路径刻意留下了没人认领的 token |
| `set_speed(percent)` | 全局、**持续**的调速器。`percent` 是 **0..100 的整数百分比**——`set_speed(1)` 就是 **1% 速度**,不是"满速";它和 `movej(speed=0..1)` 的单条轨迹倍率不是一回事 |

Expand Down Expand Up @@ -381,10 +381,10 @@ joint_follow(q, dq, kp, kd)
**不是同一张**,所以跟随会话允许比 `send_mit_all` 跑得快。

- `enable()` 是前提:否则受理路径回 `ERR{0x08,0x03}`,EMERGENCY 锁存时同样是这个码。
- 拖动示教(零重力)进行中回 `ERR{0x08,0x04}`;掉线刚性持位(drop_hold)锁存中回 `ERR{0x08,0x06}`。
- 拖动示教(零重力)进行中回 `ERR{0x08,0x04}`;掉线刚性持位(drop_hold)锁存中回 `ERR{0x08,0x06}`。⚠ 若零重力是**本客户端**开的,`0x08,0x04` **从 SDK 不可达** —— `zero_g()` 自己的守卫会在**发帧之前**就在本地抛 `InvalidCommandError` 拒掉其它下行命令。要看到它,得有**另一个主机/进程**进入了零重力。
- 早于本命令的固件回 `ERR{0x08,0x00}` —— 不会静默。
- **任何其它运动/停止命令都会结束本会话** —— 下一条 `movej` 或 `move_js` 接管。
- 状态帧仍报 `mode = MOVE_MIT_ALL`,**不要**从 `state.mode` 判断跟随是否在进行。
- 状态帧仍报 `mode = MOVE_MIT_ALL`,但那是**固件枚举名**;SDK 把 mode 5 渲染成 **`MIT_ALL`**(`_protocol.MODE_NAMES`),所以 `state.mode_name == "MOVE_MIT_ALL"` **永远为假**。`send_mit_all` 报的也是 `MIT_ALL` —— 两者从状态帧**分不开**,这正是本条警告的意义:**不要**从 `state.mode` 判断跟随是否在进行。(`send_mit` 是另一个 mode,`MOVE_MIT`。)

本组入口在真机上未经完整验证,见[排障指南 §16](../TROUBLESHOOTING.zh-CN.md#16-尚未验证的部分)。

Expand All @@ -400,8 +400,9 @@ reset_factory()

`JointParam`:`idx`、`kp`、`kd`、`tau_max`、`q_min`、`q_max`。

`set_joint_limits()` **只许收窄**,写回当前值会被判成放宽请求并拒(`ERR[23,2]`),
所以它不幂等,别拿它做读写回环。`reset_factory()` 要求失能态,且不可逆。
`set_joint_limits()` **只许收窄**,而且是**静默地收窄**:固件把请求**照编译期默认值取交集**后回 `ACK`。所以放宽请求——**包括写回当前值**——会被**接受、不报错、值也不变**。
也就是说它**是幂等的**,但**不能拿它当写口自检**:想确认写没写进去,只能 `get_joint_param()` 回读。`ERR{0x23,0x02}` 的触发条件是 NaN / 交集为空(`q_min >= q_max`)/ `idx` 越界,**不含放宽**。
⚠ **收窄在 RAM 里不可逆**:交集只会越取越小,不重启回不去。`reset_factory()` 要求失能态,且不可逆。

### 5.9 `arm.model.*` —— 动力学模型在线导入

Expand Down Expand Up @@ -453,7 +454,7 @@ r.total_bytes # 属性
```

`LogSample`:`tick`、`q_ref`、`dq`、`tau`。
常量:`LOG_MAX_SAMPLES = 2400`(约 8 s @300 Hz,记满自停)、`CTRL_HZ = 300`。
常量:`LOG_MAX_SAMPLES = 2400`(约 8 s @300 Hz,记满自停)、`CTRL_HZ = 300`——两者都在 **`litearm.log`**。与 `LogSample` / `LogReader` / `parse_samples` 不同,它们**没有**在包级再导出,所以 `litearm.LOG_MAX_SAMPLES` 会抛 `AttributeError`。

`reader()` 按固件游标分块读回,掉帧自动重试。`dump()` 落盘原始字节流。

Expand Down
15 changes: 11 additions & 4 deletions src/litearm/cart.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,10 +734,17 @@ class CartPlan:
① 到位判据 (先见 `bit10=1` 再见 `0` 且 `q` 静止) 收的尾; **且** ② 到位判据满足**之后**
回读的**实际 TCP** 与**本次请求的目标位姿**对得上 (容差 6mm / 0.03rad)。

⚠⚠ **`ok=True` 而 `settled=False` 是一个必须存在的结局** —— 它的含义是"这条笛卡尔
轨迹被**别的运动作废**了" (锁外/进程外的 `movej`/`home`/`zero_g`: 固件一收到就
`cart_invalidate_before_motion()` 把这条轨迹作废, 而收尾照样是 `bit10=0` + `q` 静止),
或者 `get_tcp()` 取不到 (超时/断连 —— 保守回"没到位")。
⚠⚠ **`ok=True` 而 `settled=False` 是一个必须存在的结局**, 有**三条**成因 —— 现场请**先**
排除第 ③ 条, 它最常见:
① 这条笛卡尔轨迹被**别的运动作废** (锁外/进程外的 `movej`/`home`/`zero_g`: 固件一收到就
`cart_invalidate_before_motion()` 把这条轨迹作废, 而收尾照样是 `bit10=0` + `q` 静止);
② `get_tcp()` 取不到 (超时/断连 —— 保守回"没到位");
③ **臂停稳了, 但回读的实际 TCP 落在 6mm / 0.03rad 容差之外** —— 这条**不在异常名单**
里, 是**普通结局**: ① ② 都满足、`settle_err_rad` 也可以很小, 只要 TCP 精度不够就是
`settled=False`。真机实测 (yd 机整臂, `Litearm1.10.0-7J`) 20mm 的 `move_l`:
**5 轮里 4 轮**落在这一条 (回读位置差 7.9~9.9mm, 容差 6mm), 第 5 轮 2.7mm 才 True。
⇒ **`settled=False` 既不是"被作废"也不是"没停稳"** —— 判据是**回读 `get_tcp()` 与目标
比一次**, 别从这两个字段猜。
**`ok` 只说明"固件受理并规划出来了"**, 不说明臂停在目标上。
⇒ 现场**必须回读 `get_tcp()` 看真实落点**, 不要把它读成"只是没停稳"。
(`wait=True` 的**超时/故障**不是这个形状 —— 那两条会抛 `MotionTimeoutError` /
Expand Down
11 changes: 10 additions & 1 deletion src/litearm/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,16 @@ def set_joint_param(self, idx: int, kp: float, kd: float, tau_max: float) -> Non

# ---- 0x23 ----
def set_joint_limits(self, idx: int, q_min: float, q_max: float) -> None:
"""改写单关节软限位 (RAM)。固件要求 `q_min < q_max`。"""
"""改写单关节软限位 (RAM)。固件要求 `q_min < q_max`。

⚠⚠ **写入是"照编译期默认值取交集", 而且静默** —— 固件只收窄: 放宽请求
(**包括写回当前值**) 会被**接受、回 ACK、且值不变**, 不报错。
所以本方法**是幂等的**, 但**不能拿它当写口自检** —— 想确认写没写进去只能
:meth:`get_joint_param` 回读。
`ERR{0x23,0x02}` 的触发条件是 NaN / 交集为空 (`q_min >= q_max`) / idx 越界,
**不含放宽** (固件原文: "固件只许收窄, 照编译期默认值取交集")。
⚠ **收窄在 RAM 里不可逆**: 交集只会越取越小, 不重启回不去。
"""
idx = self._check_idx(idx)
if not float(q_min) < float(q_max):
raise InvalidCommandError(f"软限位需 q_min < q_max (给的是 {q_min}, {q_max})")
Expand Down
Loading