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
41 changes: 25 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -151,7 +151,8 @@ 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
```

### Parameters (`arm.params.*`)
Expand Down Expand Up @@ -187,10 +188,17 @@ 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.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
Expand Down Expand Up @@ -293,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
Expand Down
33 changes: 21 additions & 12 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -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,急停独立入口,不可逆命令逐个标注

## 安装

Expand All @@ -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__)" # 验证
Expand Down Expand Up @@ -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() # 失能后不再被位置环托住
```

### 前馈 / 动力学调参
Expand All @@ -146,7 +148,8 @@ 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,前馈由固件算
```

### 参数(`arm.params.*`)
Expand Down Expand Up @@ -182,10 +185,16 @@ 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.15](docs/DEVELOPER_GUIDE.zh-CN.md#515-授权与激活)。

### 参数持久化

```python
arm.save_params() # ⚠ 写入 flash,不可逆
arm.save_params() # 写入 flash,不可逆
```

### 只读属性
Expand Down Expand Up @@ -278,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
Expand Down
52 changes: 25 additions & 27 deletions TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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".

Expand All @@ -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.

Expand All @@ -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.
Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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.

Expand All @@ -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).

---
Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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**:
Expand All @@ -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.
Loading
Loading