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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 测试门禁。
#
# 跑在 PR 与默认分支的 push 上。⚠ CI 里**绝不**设 PYLITEARM_LIVE ——
# 跑在 PR 与默认分支的 push 上。⚠ CI 里**绝不**设 LITEARM_LIVE ——
# 那会去开 /dev/ttyACM0 连真机。
# 测试单独成 workflow: 它跑在 PR 上, 而 release workflow 跑在默认分支的 push 上。
# 两者混在一起会让「发布坏了」和「PR 坏了」互相挡住。
Expand Down Expand Up @@ -38,7 +38,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

# tests/conftest.py 只在 PYLITEARM_LIVE=1 时才连真机; 不设这个变量即纯离线桩。
# tests/conftest.py 只在 LITEARM_LIVE=1 时才连真机; 不设这个变量即纯离线桩。
# CI 里**绝不**设它 —— 那会去开 /dev/ttyACM0。
- name: Test
run: |
Expand Down
456 changes: 271 additions & 185 deletions README.md

Large diffs are not rendered by default.

359 changes: 233 additions & 126 deletions README.zh-CN.md

Large diffs are not rendered by default.

428 changes: 215 additions & 213 deletions TROUBLESHOOTING.md

Large diffs are not rendered by default.

327 changes: 168 additions & 159 deletions TROUBLESHOOTING.zh-CN.md

Large diffs are not rendered by default.

929 changes: 389 additions & 540 deletions docs/DEVELOPER_GUIDE.md

Large diffs are not rendered by default.

635 changes: 273 additions & 362 deletions docs/DEVELOPER_GUIDE.zh-CN.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions env.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ rem call env.cmd
rem python examples\01_hello.py
rem set LITEARM_PORT=COM5 & call env.cmd & python examples\01_hello.py
rem ===========================================================================
set "PYLITEARM_REPO=%~dp0"
set "PYTHONPATH=%PYLITEARM_REPO%src;%PYTHONPATH%"
set "LITEARM_REPO=%~dp0"
set "PYTHONPATH=%LITEARM_REPO%src;%PYTHONPATH%"
if not defined PYTHON_BIN set "PYTHON_BIN=python"
if not defined LITEARM_PORT set "LITEARM_PORT="
echo [litearm-python env] repo=%PYLITEARM_REPO%
echo [litearm-python env] repo=%LITEARM_REPO%
echo PYTHON_BIN = %PYTHON_BIN%
echo LITEARM_PORT = '%LITEARM_PORT%' ^(空=自动发现 1d50:606f^)
echo PYTHONPATH = %PYTHONPATH%
6 changes: 3 additions & 3 deletions env.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
# ============================================================================
$ErrorActionPreference = "Stop"

$env:PYLITEARM_REPO = Split-Path -Parent $MyInvocation.MyCommand.Path
$env:LITEARM_REPO = Split-Path -Parent $MyInvocation.MyCommand.Path

# Windows 分隔符 ';' 合并已有的 PYTHONPATH
$src = Join-Path $env:PYLITEARM_REPO "src"
$src = Join-Path $env:LITEARM_REPO "src"
if (-not $env:PYTHONPATH) {
$env:PYTHONPATH = $src
} else {
Expand All @@ -29,7 +29,7 @@ if (-not $env:PYTHON_BIN) { $env:PYTHON_BIN = "python" }
# CDC 端口: 留空 = 自动发现
if (-not $env:LITEARM_PORT) { $env:LITEARM_PORT = "" }

Write-Host "[litearm-python env] repo=$($env:PYLITEARM_REPO)"
Write-Host "[litearm-python env] repo=$($env:LITEARM_REPO)"
Write-Host " PYTHON_BIN = $($env:PYTHON_BIN)"
Write-Host " LITEARM_PORT = '$($env:LITEARM_PORT)' (空=自动发现 1d50:606f)"
Write-Host " PYTHONPATH = $($env:PYTHONPATH)"
9 changes: 4 additions & 5 deletions env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
# ./run_example.sh 01_hello.py # 更省事
#
# 作用:
# 1) PYTHONPATH 指向 src —— 即使未 `pip install -e .` 也能 import litearm
# (仅依赖 pyserial, 无需 pylitearm/pinocchio/numpy);
# 1) PYTHONPATH 指向 src —— 即使未 `pip install -e .` 也能 import litearm;
# 2) 选一个装了 pyserial 的解释器 (见下, 无需手改);
# 3) LITEARM_PORT 可覆盖 CDC 端口; 不设则自动发现 (VID:PID 1d50:606f)。
# ============================================================================
set -euo pipefail

export PYLITEARM_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PYTHONPATH="${PYLITEARM_REPO}/src${PYTHONPATH:+:${PYTHONPATH}}"
export LITEARM_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PYTHONPATH="${LITEARM_REPO}/src${PYTHONPATH:+:${PYTHONPATH}}"

# 解释器: 默认 python3; 已设 PYTHON_BIN 则尊重之。否则若默认解释器 import 不到
# pyserial, 就在常见的 conda 位置里找一个能 import 的 —— 这样你不必手改本文件。
Expand All @@ -35,7 +34,7 @@ fi
# CDC 端口: 留空=自动发现; 想锁定就 `LITEARM_PORT=/dev/ttyACM1 ./run_example.sh 01_hello.py`
export LITEARM_PORT="${LITEARM_PORT:-}"

echo "[litearm-python env] repo=${PYLITEARM_REPO}"
echo "[litearm-python env] repo=${LITEARM_REPO}"
echo " PYTHON_BIN = ${PYTHON_BIN}"
echo " LITEARM_PORT = '${LITEARM_PORT}' (空=自动发现 1d50:606f)"
echo " PYTHONPATH = ${PYTHONPATH}"
86 changes: 36 additions & 50 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
# litearm-python examples

Each script runs standalone. **Read it before you point it at real hardware.**
Each script runs on its own. **Read it before you run it on real hardware.**

**Read-only by default** — anything that enables, moves, or writes parameters
requires an explicit `--go`, so a stray run cannot move the arm.
**Read-only by default** — any example that enables, moves or retunes parameters requires an
explicit `--go`, so nothing moves by accident.

## Prerequisites

1. This package is installed (or `PYTHONPATH=src`).
2. The arm is connected, firmware `Litearm1.5.0+`.
3. The serial port is free — close anything else holding `/dev/ttyACM*`.
1. This package installed (or `PYTHONPATH` pointing at `src`);
2. The arm connected, firmware `Litearm1.5.0` or later;
3. The serial port available for exclusive use — shut down anything else holding
`/dev/ttyACM*`.

Port priority: `--port` > `LITEARM_PORT` env var > auto-discovery (`1d50:606f`).

```bash
source env.sh # exports PYTHONPATH/PYTHON_BIN/LITEARM_PORT
```

On Windows use `env.ps1` / `env.cmd`; `LITEARM_PORT` can pin e.g. `COM5`.
Port priority: `--port` > the `LITEARM_PORT` environment variable > auto-discovery
(`1d50:606f`).

## Running

```bash
source env.sh # exports PYTHONPATH / PYTHON_BIN / LITEARM_PORT

python3 examples/01_hello.py # read-only, no --go needed
python3 examples/02_movej.py --go # moves the arm
./run_example.sh 02_movej.py --go # or one-shot wrapper
./run_example.sh 02_movej.py --go # or one-shot via the wrapper
LITEARM_PORT=/dev/ttyACM0 ./run_example.sh 03_move_p.py --go
```

Expand All @@ -41,52 +39,40 @@ call env.cmd
python examples\01_hello.py
```

## The examples
On Windows, `LITEARM_PORT` pins a port such as `COM5`.

| Example | Shows | Needs `--go` |
|---|---|---|
| [01_hello.py](01_hello.py) | connect handshake + firmware convention + state / TCP pose | no |
| [02_movej.py](02_movej.py) | `movej` single-shot → firmware S-curve + hold at target | yes |
| [03_move_p.py](03_move_p.py) | `move_p` single pose → firmware IK + S-curve (TCP arrival) | yes |
| [04_ik_tcp.py](04_ik_tcp.py) | `ik(pose)` solve + `get_tcp()` current pose (self-consistency) | no |
| [05_ff_tune.py](05_ff_tune.py) | dynamics / control-law tuning (`ff_preset`, gravity, inertia, payload, save) | yes |
| [06_cartesian.py](06_cartesian.py) | Cartesian paths `move_l` / `move_c` / `move_path` (firmware-planned, `CartPlan`) | yes |
| [07_vel_jitter_trace.py](07_vel_jitter_trace.py) | per-tick capture of a slow `movej` (300 Hz firmware log + 100 Hz live stream) | yes |

Each script's docstring carries its own run and safety notes.

## ⚠ Safety
## The examples

The moving examples (02 / 03 / 05 / 06 / 07) **drive the real arm**:
| Example | What it shows | Needs `--go` |
| --- | --- | --- |
| [01_hello.py](01_hello.py) | Handshake + firmware version + reading state and tool pose | No |
| [02_movej.py](02_movej.py) | `movej` as a single shot: firmware plans and completes it, then holds position | Yes |
| [03_move_p.py](03_move_p.py) | `move_p` with one pose: firmware IK + S-curve, arrival judged by TCP | Yes |
| [04_ik_tcp.py](04_ik_tcp.py) | `ik(pose)` plus `get_tcp()` for a self-consistency check | No |
| [05_ff_tune.py](05_ff_tune.py) | Dynamics / control-law tuning (`ff_preset` / gravity / inertia / payload / persist) | Yes |
| [06_cartesian.py](06_cartesian.py) | Cartesian paths: `move_l` / `move_c` / `move_path` | Yes |
| [07_vel_jitter_trace.py](07_vel_jitter_trace.py) | Per-tick capture of a slow `movej` (300 Hz firmware log + 100 Hz live stream) | Yes |

- keep `speed` at 0.1~0.3 the first time
- stand at the e-stop, keep the workspace clear
- read [TROUBLESHOOTING.md](../TROUBLESHOOTING.md) first
- ⚠ **`movej` does not currently check joint limits** — an out-of-range target
is driven the full way
Every script's docstring states how to run it and what to watch out for.

## About 06_cartesian.py
## Safety notes

This is the **Cartesian path** entry point: `move_l` goes straight, `move_c` goes
around an arc, `move_path` visits waypoints in turn (**sharp corners** — the
protocol has no blend field). Planning (sampling / per-point IK / playback) all
happens **in the firmware**: the PC sends points and collects the `0x4E` result
frame, returned as a `CartPlan`.
The examples that move the arm (02 / 03 / 05 / 06 / 07) **really drive it**:

Known degradations (no corner blending / no pre-send preview / speed pre-check
delegated to firmware) are in the script docstring and in the
[Developer Guide](../docs/DEVELOPER_GUIDE.md#53-cartesian-firmware-planned).
For kinematics and impedance identification, see `pylitearm`'s own `examples/`.
- 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.

## Pose format

A pose is a plain Python list of **6 numbers**: position 3 + RPY 3. No numpy.
A pose is **6 numbers**: 3 of position (m) plus 3 of orientation (rad, RPY). Lists and tuples
both work, and no numpy is needed.

```python
pose = [px, py, pz, rx, ry, rz]

m = arm.get_tcp() # Msg envelope
p = m.value # 6 numbers (or None)
p[:3] # position, m
p[3:6] # RPY, rad
p = m.value # 6 numbers, or None if no frame could be obtained

p[:3] # position, in metres
p[3:6] # orientation RPY, in radians
```
71 changes: 29 additions & 42 deletions examples/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,24 @@

每个脚本可独立运行,**先读懂再上真机**。

**默认只读** —— 会 `enable` / 运动 / 改参的样例必须显式加 `--go`,防误动。
**默认只读**——会 `enable` / 运动 / 改参的样例必须显式加 `--go`,防误动。

## 前提

1. 已装本包(或 `PYTHONPATH=src`)。
2. 机械臂已连接,固件 `Litearm1.5.0+`。
3. 串口可被独占 —— 关掉其它占着 `/dev/ttyACM*` 的进程。
1. 已装本包(或让 `PYTHONPATH` 指向 `src`);
2. 机械臂已连接,固件 `Litearm1.5.0` 及以上;
3. 串口可被独占——关掉其它占着 `/dev/ttyACM*` 的进程。

端口优先级:`--port` > 环境变量 `LITEARM_PORT` > 自动发现(`1d50:606f`)。

```bash
source env.sh # 导出 PYTHONPATH/PYTHON_BIN/LITEARM_PORT
```

Windows 用 `env.ps1` / `env.cmd`;`LITEARM_PORT` 可锁 `COM5` 等。

## 运行

```bash
source env.sh # 导出 PYTHONPATH / PYTHON_BIN / LITEARM_PORT

python3 examples/01_hello.py # 只读,不需要 --go
python3 examples/02_movej.py --go # 会运动
./run_example.sh 02_movej.py --go # 或包装脚本一键跑
./run_example.sh 02_movej.py --go # 或用包装脚本一键跑
LITEARM_PORT=/dev/ttyACM0 ./run_example.sh 03_move_p.py --go
```

Expand All @@ -40,48 +36,39 @@ call env.cmd
python examples\01_hello.py
```

Windows 上 `LITEARM_PORT` 可以锁定 `COM5` 这类端口。

## 样例列表

| 样例 | 演示 | 需 `--go` |
|---|---|---|
| [01_hello.py](01_hello.py) | 连接握手 + 固件版本约定 + 状态/末端位姿 | 否 |
| [02_movej.py](02_movej.py) | `movej` 单发 → 固件 S 曲线自完成 + 静止保持 | 是 |
| [03_move_p.py](03_move_p.py) | `move_p` 单 pose → 固件内置 IK + S 曲线(TCP 到位判定) | 是 |
| [04_ik_tcp.py](04_ik_tcp.py) | `ik(pose)` 反解 + `get_tcp()` 当前位姿(自洽校验) | 否 |
| [05_ff_tune.py](05_ff_tune.py) | 内置动力学/控制律调参(`ff_preset` / 重力 / 惯量 / payload / save) | 是 |
| [06_cartesian.py](06_cartesian.py) | 笛卡尔路径 `move_l` / `move_c` / `move_path`(固件规划 + `CartPlan`) | 是 |
| [07_vel_jitter_trace.py](07_vel_jitter_trace.py) | 慢速 `movej` 的逐拍采集(300 Hz 固件日志 + 100 Hz 实测流双通道) | 是 |
| 样例 | 演示什么 | 需要 `--go` |
| --- | --- | --- |
| [01_hello.py](01_hello.py) | 连接握手 + 固件版本 + 读状态与末端位姿 | 否 |
| [02_movej.py](02_movej.py) | `movej` 单发:固件规划并走完,到位后静止保持 | 是 |
| [03_move_p.py](03_move_p.py) | `move_p` 单个位姿:固件内置逆解 + S 曲线,按 TCP 判到位 | 是 |
| [04_ik_tcp.py](04_ik_tcp.py) | `ik(pose)` 反解 + `get_tcp()` 读当前位姿(自洽校验) | 否 |
| [05_ff_tune.py](05_ff_tune.py) | 动力学 / 控制律调参(`ff_preset` / 重力 / 惯量 / 负载 / 持久化) | 是 |
| [06_cartesian.py](06_cartesian.py) | 笛卡尔路径:`move_l` / `move_c` / `move_path` | 是 |
| [07_vel_jitter_trace.py](07_vel_jitter_trace.py) | 慢速 `movej` 的逐拍采集(300 Hz 固件日志 + 100 Hz 实测流) | 是 |

每个脚本的 docstring 含运行与安全说明。
每个脚本的 docstring 都写明了运行方式与安全注意事项。

## ⚠ 安全提示
## 安全提示

会运动的样例(02 / 03 / 05 / 06 / 07)**真实驱动机械臂**:

- 首次运行 `speed` 保持 0.1~0.3
- 人站在急停旁,确保周围无人无障碍
- 跑之前先读 [TROUBLESHOOTING.zh-CN.md](../TROUBLESHOOTING.zh-CN.md)
- ⚠ **`movej` 目前不校验关节限位** —— 越限目标会被走满行程

## 关于 06_cartesian.py

它是**笛卡尔路径**入口:`move_l` 走直线、`move_c` 走圆弧、`move_path` 依次经过多路点
(**尖角**,协议无倒角字段)。规划(采样 / 逐点 IK / 播放)全在**固件**里 ——
PC 只发点、收 `0x4E` 结果帧,返回 `CartPlan`。

已知降级(无倒角 / 无下发前预览 / 速度预检改由固件做)见脚本 docstring 与
[开发者指南](../docs/DEVELOPER_GUIDE.zh-CN.md#53-笛卡尔固件规划)。
运动学 / 阻抗辨识等其余高级场景仍参考 `pylitearm` 本体 `examples/`。
- 首次运行把 `--speed` 保持在 0.1~0.3;
- 人站在急停旁,确保周围无人无障碍;
- 跑之前先读 [排障指南](../TROUBLESHOOTING.zh-CN.md);
- ⚠ **`movej` 不校验关节限位**——越限目标会被走满行程。

## 位姿格式

位姿是 **6 个数**的纯 Python list:位置 3 + RPY 3。不需要 numpy。
位姿是 **6 个数**:位置 3(m)+ 姿态 3(rad,RPY)。list 和 tuple 都收,不需要 numpy。

```python
pose = [px, py, pz, rx, ry, rz]

m = arm.get_tcp() # Msg 信封
p = m.value # 6 个数(或 None)
p[:3] # 位置,m
p[3:6] # RPY,rad
p = m.value # 6 个数,取不到帧时为 None

p[:3] # 位置,单位 m
p[3:6] # 姿态 RPY,单位 rad
```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
dependencies = ["pyserial>=3.4"]
# 刻意不依赖 pylitearm/pinocchio/numpy (唯一例外仍是 pyserial):
# 唯一依赖是 pyserial —— 刻意不引 numpy:
# 运动学(FK/JAC/IK)全问固件 (`CMD_GET_TCP 0x43` / `CMD_GET_IK 0x42`), 笛卡尔规划
# (`move_l`/`move_c`/`move_path`) 也全在固件里 —— 不引模型, 也就不会和固件模型漂移。
# PC 侧只用 `math` (`_rot.py` 的位姿形态归一化与到位姿态判据)。
Expand Down
4 changes: 2 additions & 2 deletions run_example.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ $ErrorActionPreference = "Stop"

if (-not $Name) {
Write-Host "用法: $PSCommandPath <example.py> [args...]"
Get-ChildItem (Join-Path $env:PYLITEARM_REPO "examples") -Filter *.py |
Get-ChildItem (Join-Path $env:LITEARM_REPO "examples") -Filter *.py |
ForEach-Object { Write-Host " $($_.Name)" }
exit 1
}

$example = Join-Path $env:PYLITEARM_REPO ("examples\" + $Name)
$example = Join-Path $env:LITEARM_REPO ("examples\" + $Name)
if (-not (Test-Path $example)) {
Write-Error "找不到样例: $example"
exit 1
Expand Down
8 changes: 4 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

用法:
pytest # 只跑离线 (桩 transport), 不碰真机
PYLITEARM_LIVE=1 pytest # 额外跑真机 live (需接 Litearm1.5.0+ 整臂/台架)
LITEARM_LIVE=1 pytest # 额外跑真机 live (需接 Litearm1.5.0+ 整臂/台架)
"""
from __future__ import annotations

Expand All @@ -18,7 +18,7 @@

from fake_serial import FakeTransport # noqa: E402

LIVE = bool(os.environ.get("PYLITEARM_LIVE"))
LIVE = bool(os.environ.get("LITEARM_LIVE"))


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -172,6 +172,6 @@ def offline_arm_1j(fake_transport_factory):

@pytest.fixture
def live_only():
"""真机用例: 未设 PYLITEARM_LIVE=1 时跳过。"""
"""真机用例: 未设 LITEARM_LIVE=1 时跳过。"""
if not LIVE:
pytest.skip("真机用例需 PYLITEARM_LIVE=1 (并接好 Litearm1.5.0+ 固件)")
pytest.skip("真机用例需 LITEARM_LIVE=1 (并接好 Litearm1.5.0+ 固件)")
Loading
Loading