diff --git a/CHANGELOG.md b/CHANGELOG.md index eae2949..7959772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.4.3] - 2026-10-10 +_The `v0.4.3` tag was moved on 2026-10-18 to include the EL05 motor OTA +fix and docs below; the version number is unchanged._ + ### Fixed +- **`motor_ota_update.py` mis-read EL05 images.** EL05 versions have five + parts (`1.0.5.0.4`); the example kept four and compared the motor's raw + bytes (`10.5.0.4`), so a good flash was reported as a mismatch. It now + parses five-part versions and compares the vendor spelling. - **Force-position could not close a left-hand RS00 follower to 0.0.** Root cause (0089s, diagnostic firmware, 2026-10-10): a RobStride motor takes an MIT motion frame whose byte 0 equals its own CAN ID for another command and @@ -26,6 +33,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **`motor_ota_update.py` runs as two passes of the same command**: the first + switches the motor to its private protocol, the second (after a 24 V power + cycle) flashes, verifies, records the version in the follower (0x5C) and + switches back to MIT. The version is recorded only after the motor confirmed + it, or blind when the motor returned to MIT by itself (RS00). `--to-mit` + switches back without flashing. +- **The motor OTA sets the motion envelope to the motor's rating** when it + differs (`ensure_envelope()`); travel calibration and open direction are + kept. `--keep-envelope` skips it. +- **`docs/MOTOR_OTA.md`** (and `MOTOR_OTA-zh.md`): customer guide to updating + the motor firmware over USB-C. - **RS00 followers use motor CAN ID 17 (left) / 18 (right).** `detail::expected_motor_can_id(model_id, side)` (Python `expected_motor_can_id`) is the rule; `FollowerGripper` warns on open when the @@ -49,6 +67,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Chinese documents are named `-zh.md`.** `README_CN.md` is now + `README-zh.md` and `docs/USAGE_CN.md` is `docs/USAGE-zh.md`; links inside the + repository are updated. English files stay authoritative. - Shipped follower image is now **1.2.16** (was 1.2.14 in 0.4.2; leader unchanged at 1.2.6). It carries 1.2.15: an RS00's open direction follows the side in its firmware SN, auto-calibration corrects a stored direction that diff --git a/README_CN.md b/README-zh.md similarity index 99% rename from README_CN.md rename to README-zh.md index 584d97c..0e01e75 100644 --- a/README_CN.md +++ b/README-zh.md @@ -344,7 +344,7 @@ python python/examples/follower_status.py left ## 文档 -- [docs/USAGE_CN.md](docs/USAGE_CN.md)([English](docs/USAGE.md))—— 夹爪、腕部相机、视触觉传感器的完整上手流程 +- [docs/USAGE-zh.md](docs/USAGE-zh.md)([English](docs/USAGE.md))—— 夹爪、腕部相机、视触觉传感器的完整上手流程 - [docs/INSTALL.md](docs/INSTALL.md) —— 构建选项、纯 C++ 构建、环境陷阱 - [docs/CALIBRATION.md](docs/CALIBRATION.md) —— 编码器零点、行程、鱼眼内参 - [docs/EXAMPLES.md](docs/EXAMPLES.md) —— 每个示例脚本与控制器参数 diff --git a/README.md b/README.md index b3ebade..4257112 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # taccap-gripper -[中文](README_CN.md) +[中文](README-zh.md) C++17 / Python SDK for the **TacCap-Gripper**, XenseRobotics' multimodal tactile data-collection gripper. One namespace: `xense::taccap::` in C++, @@ -345,7 +345,8 @@ looking healthy while it drops status frames. `FollowerGripper` refuses followers below firmware 1.2.5. Leader and follower firmware are versioned independently. Required motor firmware: EL05 ≥ 1.0.5.0.4, RS00 ≥ 0.0.3.32. Image table, motor model, RS00 setup and motor OTA: -[firmware/README.md](firmware/README.md). +[firmware/README.md](firmware/README.md). Step-by-step motor firmware update: +[docs/MOTOR_OTA.md](docs/MOTOR_OTA.md) ([中文](docs/MOTOR_OTA-zh.md)). ### Checking versions @@ -378,8 +379,9 @@ Logs: every process writes a DEBUG log to `~/.taccaplogs/session_*.log` ## Documentation -- [docs/USAGE.md](docs/USAGE.md) ([中文](docs/USAGE_CN.md)) — end-to-end bring-up of gripper, wrist camera and tactile sensor +- [docs/USAGE.md](docs/USAGE.md) ([中文](docs/USAGE-zh.md)) — end-to-end bring-up of gripper, wrist camera and tactile sensor - [docs/INSTALL.md](docs/INSTALL.md) — build options, C++-only builds, environment traps +- [docs/MOTOR_OTA.md](docs/MOTOR_OTA.md) ([中文](docs/MOTOR_OTA-zh.md)) — updating the follower's motor firmware over USB-C - [docs/CALIBRATION.md](docs/CALIBRATION.md) — encoder zero, travel span, fisheye intrinsics - [docs/EXAMPLES.md](docs/EXAMPLES.md) — every example script and controller parameter - [firmware/README.md](firmware/README.md) — firmware images, flashing, motor setup diff --git a/docs/MOTOR_OTA-zh.md b/docs/MOTOR_OTA-zh.md new file mode 100644 index 0000000..5fd3be4 --- /dev/null +++ b/docs/MOTOR_OTA-zh.md @@ -0,0 +1,152 @@ +# 升级从夹爪的电机固件 + +> 本文是 [MOTOR_OTA.md](MOTOR_OTA.md) 的中文版,以英文版为准。 + +从夹爪里的 RobStride 电机运行着自己的固件,和夹爪主控(MCU)的固件是两套。本文介绍如何用 +`python/examples/motor_ota_update.py`,通过夹爪的 USB-C 线升级电机固件。电机不需要拆下来。 + +| 电机 | 电机固件要求 | +| --- | --- | +| EL05 | ≥ 1.0.5.0.4 | + +## 开始之前 + +- **夹爪固件 1.2.16**(升级电机至少需要 1.2.8)。用 `follower_status.py` 查看,看 `[fw]` + 那一行。如果版本较旧,先用 `ota_update.py` 升级夹爪固件并断电重启。升级夹爪固件会把电机切回 + 常规协议,所以必须先升级夹爪、再升级电机,顺序不能反。 +- **SDK 0.4.3 或更新版本**,并且 `import xense.taccap` 能正常导入。 +- **我们提供的电机固件镜像**,例如 `EL05_1.0.5.0.4.bin`。请保持原文件名:脚本会从文件名读出 + 电机型号和版本,并与镜像内容和夹爪记录核对。 +- **没有别的程序在使用夹爪。** 先停掉你自己的程序。 +- **两指之间清空。** 每次断电重启后,夹爪会自动开合一次做标定。 + +下文的"断电重启"一律指:拔下夹爪的 **24V 电源线**,等约 2 秒再插回。USB 线可以不拔,夹爪主控和 +电机都由 24V 供电。 + +## 为什么同一条命令要运行两遍 + +我们的 SDK 让电机工作在 **MIT 协议**下。电机只在**私有协议**下接受固件升级,而切换协议要到下次 +上电才生效。所以一次升级的过程是: + +1. 运行脚本:脚本把电机切到私有协议; +2. 断电重启; +3. 再运行一遍同样的命令:脚本刷写固件、核对版本、把新版本记进夹爪,如果运动安全包络和电机额定值 + 不一致就顺带改正,最后把电机切回 MIT; +4. 断电重启。 + +私有协议只在升级时使用。全部完成后夹爪回到 MIT,可以正常使用。 + +## 操作步骤 + +下面以左侧从夹爪为例;右侧用 `right`,或者用完整序列号(例如 `TCGU01A25Z0017s`)指定。 + +### 1. 查看当前状态 + +```bash +python python/examples/follower_status.py left +``` + +看 `[fw]`(夹爪固件)和 `[motor fw]`(电机固件)两行。此时电机固件可能显示读不到:电机在 MIT 下、 +且夹爪里还没有记录时就是这样,属于正常现象。 + +### 2. 第一遍:切到私有协议 + +```bash +python python/examples/motor_ota_update.py EL05_1.0.5.0.4.bin left +``` + +```text +=== motor OTA, step 1 of 2 === + the motor is on the MIT protocol and does not answer OTA frames there, + so it has to be switched to its private protocol first. +Switch the motor to the private protocol? type 'yes': yes + +done. Now unplug the gripper's 24 V power cable for ~2 s and plug it back (USB may stay in), +wait ~10 s, then run the same command again to flash. +``` + +**断电重启**,等约 10 秒。私有协议下电机保持不使能,夹爪不会做标定,这是正常的。 + +### 3. 第二遍:刷写 + +```bash +python python/examples/motor_ota_update.py EL05_1.0.5.0.4.bin left +``` + +```text +=== motor OTA, step 2 of 2 === + gripper : TCGU01A25Z0017s firmware FirmwareVersion(1.2.16.0) + motor : EL05 can_id 1 uid ... + motor fw now : 1.0.5.0.2 + image : EL05_1.0.5.0.4.bin + 92,584 B, 11,573 packs, version 1.0.5.0.4 + envelope : at the motor's rating, unchanged +Flash the MOTOR with this image? type 'yes': yes + [########################################] 100.0% 11,573/11,573 +=== done in 28s; waiting for the motor === + motor fw : 1.0.5.0.2 -> 1.0.5.0.4 + recorded : motor fw 1.0.5.0.4 in follower flash + switched the motor back to the MIT protocol. + +Done. Now unplug the gripper's 24 V power cable for ~2 s and plug it back (USB may stay in); +the gripper auto-calibrates (~10 s, keep the jaws clear) and is ready to use. +``` + +刷写大约需要半分钟。开始时出现 `motor OTA start: no reply (attempt 1/6)` 这类警告是正常的: +电机要先重启进 bootloader,之后的某次重试才会应答。 + +**运动安全包络。** 如果夹爪里存的运动安全包络(力矩和过热保护)和电机额定值不一致,脚本会在刷写 +之后把它改成额定值(EL05:cont 1.1 / peak 1.8 N·m),并在你确认刷写前提示: + +```text + envelope : stored cont 1.000 / peak 2.000 N*m, will be set to cont 1.100 / peak 1.800 after flashing +``` + +行程标定和开合方向保持不变。加 `--keep-envelope` 可以不改包络。 + +### 4. 断电重启并确认 + +**断电重启**,等夹爪标定完(约 10 秒),然后运行: + +```bash +python python/examples/follower_status.py left +``` + +`[motor fw]` 应显示新版本,例如 `MotorVersion(1.0.5.0.4, flash: host-written)`。版本已经记在 +夹爪里,之后在 MIT 下也能直接读到。 + +## 出现问题时 + +| 现象 | 处理方法 | +| --- | --- | +| `cannot tell the motor model from ...` | 镜像文件被改了名。恢复原文件名,或加 `--model EL05`。 | +| `this gripper records a ... motor, the image is for ...` | 镜像对应的是另一种电机型号。不要强行刷写,请联系我们。 | +| `follower firmware ... lacks the 0x5B relay` | 先升级夹爪固件(见"开始之前")。 | +| `motor OTA ...: no reply after N attempts` | 断电重启后再运行一遍同样的命令。中断的升级会从头开始,电机不会损坏。 | +| 刷写后提示 `!! expected ...` | 断电重启后再运行一遍同样的命令。 | +| 脚本中途停止,之后夹爪不再标定 | 电机还停在私有协议。运行 `python python/examples/motor_ota_update.py --to-mit left`,再断电重启。 | + +不刷写、只把电机切回 MIT: + +```bash +python python/examples/motor_ota_update.py --to-mit left +``` + +然后断电重启。 + +## 在自己的代码里调用 + +这个脚本只是对 `xense.taccap.MotorOtaSession` 的简单封装: + +```python +import xense.taccap as t + +g = t.FollowerGripper(t.find_left().mcu_device) +m = g.motor +# the motor must already be on the private protocol (see step 2) +session = t.MotorOtaSession(m, m.get_can_id()) +session.preflight("EL05") # private protocol + recorded model +session.update_from_file("EL05_1.0.5.0.4.bin", lambda done, total: None) +m.set_motor_fw_version("1.0.5.0.4") # record it for MIT reads +m.switch_protocol(t.MotorProtocol.Mit) # then power-cycle +``` diff --git a/docs/MOTOR_OTA.md b/docs/MOTOR_OTA.md new file mode 100644 index 0000000..e256b4a --- /dev/null +++ b/docs/MOTOR_OTA.md @@ -0,0 +1,170 @@ +# Updating the follower's motor firmware + +> Chinese version: [MOTOR_OTA-zh.md](MOTOR_OTA-zh.md) + +The follower gripper carries a RobStride motor that runs its own firmware, +separate from the gripper's MCU firmware. This guide updates the motor firmware +over the gripper's USB-C cable with `python/examples/motor_ota_update.py`. The +motor stays mounted. + +| Motor | Required motor firmware | +| --- | --- | +| EL05 | ≥ 1.0.5.0.4 | + +## Before you start + +- **Gripper firmware 1.2.16** (at least 1.2.8 for motor updates). Check it with + `follower_status.py` (the `[fw]` line). If it is older, update the gripper + firmware first with `ota_update.py` and power-cycle; a gripper firmware update + also resets the motor to its normal protocol, so it must come before the motor + update, never after. +- **SDK 0.4.3 or newer**, installed so that `import xense.taccap` works. +- **The motor firmware image** from us, for example `EL05_1.0.5.0.4.bin`. Keep the + file name as delivered: the script reads the motor model and version from it + and checks them against the image and the gripper. +- **Nothing else using the gripper.** Stop your own programs first. +- **Jaws clear.** After each power cycle the gripper opens and closes once to + calibrate. + +"Power-cycle" below always means: unplug the gripper's **24 V power cable** for +about 2 seconds and plug it back. The USB cable can stay connected; the MCU and +the motor both run from 24 V. + +## Why the command runs twice + +Our SDK drives the motor on the **MIT protocol**. The motor accepts firmware +updates only on its **private protocol**, and a protocol change takes effect at +the next power-up. So one update is: + +1. run the script: it switches the motor to the private protocol; +2. power-cycle; +3. run the same command again: it flashes, verifies, records the new version in + the gripper, sets the motion envelope to the motor's rating if it differs, and + switches the motor back to MIT; +4. power-cycle. + +The private protocol is used only for the update. The gripper is back on MIT, +ready for normal use, at the end. + +## Steps + +The examples assume the left follower; use `right` or the full serial number +(for example `TCGU01A25Z0017s`) for another one. + +### 1. Check the current state + +```bash +python python/examples/follower_status.py left +``` + +Look at `[fw]` (gripper firmware) and `[motor fw]` (motor firmware). The motor +firmware may show as unreadable at this point; that is expected while the motor +is on MIT and nothing has been recorded yet. + +### 2. First run: switch to the private protocol + +```bash +python python/examples/motor_ota_update.py EL05_1.0.5.0.4.bin left +``` + +```text +=== motor OTA, step 1 of 2 === + the motor is on the MIT protocol and does not answer OTA frames there, + so it has to be switched to its private protocol first. +Switch the motor to the private protocol? type 'yes': yes + +done. Now unplug the gripper's 24 V power cable for ~2 s and plug it back (USB may stay in), +wait ~10 s, then run the same command again to flash. +``` + +**Power-cycle** and wait about 10 seconds. On the private protocol the motor +stays disabled and the gripper does not calibrate; that is expected. + +### 3. Second run: flash + +```bash +python python/examples/motor_ota_update.py EL05_1.0.5.0.4.bin left +``` + +```text +=== motor OTA, step 2 of 2 === + gripper : TCGU01A25Z0017s firmware FirmwareVersion(1.2.16.0) + motor : EL05 can_id 1 uid ... + motor fw now : 1.0.5.0.2 + image : EL05_1.0.5.0.4.bin + 92,584 B, 11,573 packs, version 1.0.5.0.4 + envelope : at the motor's rating, unchanged +Flash the MOTOR with this image? type 'yes': yes + [########################################] 100.0% 11,573/11,573 +=== done in 28s; waiting for the motor === + motor fw : 1.0.5.0.2 -> 1.0.5.0.4 + recorded : motor fw 1.0.5.0.4 in follower flash + switched the motor back to the MIT protocol. + +Done. Now unplug the gripper's 24 V power cable for ~2 s and plug it back (USB may stay in); +the gripper auto-calibrates (~10 s, keep the jaws clear) and is ready to use. +``` + +Flashing takes about half a minute. Warnings like `motor OTA start: no reply +(attempt 1/6)` at the start are normal: the motor restarts into its bootloader +first and answers on a later attempt. + +**Motion envelope.** If the gripper's stored motion envelope (its torque and +thermal protection) differs from the motor's rating, the script sets it to the +rating after flashing (EL05: cont 1.1 / peak 1.8 N·m) and says so before you +confirm: + +```text + envelope : stored cont 1.000 / peak 2.000 N*m, will be set to cont 1.100 / peak 1.800 after flashing +``` + +The travel calibration and open direction are kept. Pass `--keep-envelope` to +leave the envelope as it is. + +### 4. Power-cycle and check + +**Power-cycle** and let the gripper calibrate (about 10 seconds), then: + +```bash +python python/examples/follower_status.py left +``` + +`[motor fw]` should show the new version, for example +`MotorVersion(1.0.5.0.4, flash: host-written)`. The version is recorded in the +gripper, so it stays readable on MIT from now on. + +## If something goes wrong + +| Symptom | What to do | +| --- | --- | +| `cannot tell the motor model from ...` | The image file was renamed. Restore the original name, or pass `--model EL05`. | +| `this gripper records a ... motor, the image is for ...` | The image is for a different motor model. Do not override this; contact us. | +| `follower firmware ... lacks the 0x5B relay` | Update the gripper firmware first (see *Before you start*). | +| `motor OTA ...: no reply after N attempts` | Power-cycle and run the same command again. An interrupted update restarts from the beginning; the motor is not damaged. | +| `!! expected ...` after flashing | Power-cycle and run the same command again. | +| The script stopped and the gripper does not calibrate any more | The motor is still on the private protocol. Run `python python/examples/motor_ota_update.py --to-mit left`, then power-cycle. | + +To switch the motor back to MIT at any time without flashing: + +```bash +python python/examples/motor_ota_update.py --to-mit left +``` + +then power-cycle. + +## Doing it from your own code + +The script is a thin wrapper around `xense.taccap.MotorOtaSession`: + +```python +import xense.taccap as t + +g = t.FollowerGripper(t.find_left().mcu_device) +m = g.motor +# the motor must already be on the private protocol (see step 2) +session = t.MotorOtaSession(m, m.get_can_id()) +session.preflight("EL05") # private protocol + recorded model +session.update_from_file("EL05_1.0.5.0.4.bin", lambda done, total: None) +m.set_motor_fw_version("1.0.5.0.4") # record it for MIT reads +m.switch_protocol(t.MotorProtocol.Mit) # then power-cycle +``` diff --git a/docs/USAGE_CN.md b/docs/USAGE-zh.md similarity index 99% rename from docs/USAGE_CN.md rename to docs/USAGE-zh.md index 90756da..dbeeb01 100644 --- a/docs/USAGE_CN.md +++ b/docs/USAGE-zh.md @@ -23,7 +23,7 @@ > [EXAMPLES.md](EXAMPLES.md#选择设备统一用-left--right)。 安装见 [docs/INSTALL.md](INSTALL.md);请先完成一次性的 -[主机配置](../README_CN.md#主机配置一次性)(`dialout` 组、ModemManager udev 规则、 +[主机配置](../README-zh.md#主机配置一次性)(`dialout` 组、ModemManager udev 规则、 相机 MJPEG 格式)。下文假定 `taccap` 环境已激活、 `import xense.taccap` 能成功。 @@ -625,7 +625,7 @@ finally: 一路未压缩的 YUYV(640×480 @ 30 fps ≈ 147 Mbit/s)就足以让下一台相机报 `No space left on device` 或丢帧。保持 `use_mjpg=True`,并用 `v4l2-ctl -d <设备> --get-fmt-video` 确认(见 README 的 - [主机配置](../README_CN.md#3-腕部相机必须使用-mjpeg))。即使是 MJPEG,两个 OG + 一个 + [主机配置](../README-zh.md#3-腕部相机必须使用-mjpeg))。即使是 MJPEG,两个 OG + 一个 腕相机也会占掉相当一部分总线,满配采集时先实测帧率,别假设标称值。夹爪本身走串口, 不受影响。 - **谁持有 UVC 设备。** 生产环境里外部相机服务持有 `/dev/video*`,这时 @@ -648,9 +648,9 @@ finally: | 现象 | 多半是 | | --- | --- | -| `scan_grippers()` 返回空,或 `role=Unknown` | 用户不在 `dialout` 组(见 README 的[主机配置](../README_CN.md#主机配置一次性)),或线没插好 | -| 插上后立即打开报 `Device or resource busy` | ModemManager 在探测串口 —— 添加[主机配置](../README_CN.md#2-让-modemmanager-不要占用夹爪串口)里的 udev 规则 | -| 相机报 `No space left on device` 或丢帧 | 没用 MJPEG —— 见[主机配置](../README_CN.md#3-腕部相机必须使用-mjpeg) | +| `scan_grippers()` 返回空,或 `role=Unknown` | 用户不在 `dialout` 组(见 README 的[主机配置](../README-zh.md#主机配置一次性)),或线没插好 | +| 插上后立即打开报 `Device or resource busy` | ModemManager 在探测串口 —— 添加[主机配置](../README-zh.md#2-让-modemmanager-不要占用夹爪串口)里的 udev 规则 | +| 相机报 `No space left on device` 或丢帧 | 没用 MJPEG —— 见[主机配置](../README-zh.md#3-腕部相机必须使用-mjpeg) | | `fw_sn` 为空 / `Side.Unknown` | 固件没烧 SN,或固件早于 V1.6 | | 矫正后画面全黑 | 用了 `read_fisheye()` 的全零记录 —— 改用 `resolve_fisheye()` | | 矫正后画面偏心 | 未必是错的,主点本来就不一定在画面中心 | diff --git a/docs/USAGE.md b/docs/USAGE.md index e1f466c..03b5791 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -1,6 +1,6 @@ # Usage — bringing up the three data paths -> A Chinese version is available at [USAGE_CN.md](USAGE_CN.md). This English file is authoritative. +> A Chinese version is available at [USAGE-zh.md](USAGE-zh.md). This English file is authoritative.