2026-08-04 16:09:17 +04:00

215 lines
9.2 KiB
Markdown

# Inspire RH56 dual-hand stack for the Unitree G1
Runs the two Inspire RH56DFX hands on the G1's PC2 and puts a web dashboard in front of
them: finger sliders, saved shapes, force-follow, live diagnostics, arm actions, and
**arm + hand record / replay**.
Everything runs in **one Docker container on the robot**. The workstation is only where you
edit the source and push it.
Forked from Unitree's [DFX inspire hand service](https://support.unitree.com/home/en/H1_developer/Dexterous_hand);
the service protocol section at the bottom is theirs.
---
## Run it
```bash
./start.sh
```
That is the whole thing. It builds the image the first time (a few minutes), starts the
container, waits until the dashboard actually answers, and prints the URL:
```
>> up
service=True bridge=True iface=eth0 ports=/dev/ttyUSB1, /dev/ttyUSB2
recorder: ready (record/replay panel enabled)
http://192.168.123.164:8088
http://10.255.254.88:8088
```
| command | does |
|---|---|
| `./start.sh` | start (builds the image if missing) |
| `./start.sh stop` | stop the container |
| `./start.sh restart` | stop + start |
| `./start.sh status` | running? serving what? hands alive? |
| `./start.sh logs` | follow the container log |
| `./start.sh rebuild` | force an image rebuild, then start |
**Run it on the robot.** Run it on the workstation and it rsyncs this folder to the robot
and runs itself there — the hands, the DDS link to the arm and Docker are all on the robot,
so there is nothing to run locally. Override the target with `ROBOT=unitree@10.255.254.88`.
Only `./start.sh` is meant to be run by hand. `docker/start.sh` is the container's internal
entrypoint; running it directly starts the services natively, outside Docker.
---
## What you get at `:8088`
| card | what it does |
|---|---|
| **Fingers** | 12 sliders, presets, save/load named shapes |
| **Force follow** | press a fingertip and that finger closes under your hand — the RH56 cannot be back-driven, so this admittance loop is the only way to shape it by hand |
| **Joint tracker** | angle traces + per-finger force, current, force limit, temperature, ERROR/STATUS |
| **Diagnostics** | live read-back and a close→open self-test, reported per hand |
| **Read back** | copy the hand's current pose into the sliders |
| **Arm + hand combo** | built-in arm actions fused with a hand pose ⚠ arm moves |
| **Record / replay** | record the arm + hand, replay it, manage the library ⚠ arm moves |
| **Library** | manage saved shapes and combos |
---
## Record / replay
Runs [`recorder/g1_record_replay.py`](recorder/g1_record_replay.py) as a child process over a
pty — the same script you would run from a terminal, so a take made in the browser and one
made from the CLI are the same file in `DataG1/`.
**A take:** arms hold for ~3 s, then go **limp** so you pose them by hand. Fingers *cannot*
be posed by hand, so they are driven by the in-take keys or your saved shapes. At the end the
recorder offers a preview (⚠ replays the take — the arm moves) and then a save; both are
answered in the panel.
**Stop** sends SIGINT, so the recorder walks the arm home and releases it. It does not kill.
**Replay** has ⏸ Pause / ▶ Resume. Pause freezes the playback clock but keeps publishing the
frame it stopped on — the arm holds the pose rather than dropping, because `rt/arm_sdk` needs
a continuous command.
### In-take keys — and when they do nothing
`o` both to rest · `c` both closed · `[` `]` right open/close · `;` `'` left open/close ·
`f` arm/disarm follow · `1`-`9` your saved shapes.
Which of them the recorder acts on depends on `--fingers`. Measured against the real recorder
while watching `rt/inspire/cmd`:
| `--fingers` | what the keys do | records |
|---|---|---|
| `touch` *(default)* | **nothing** — the recorder holds the rest pose all take | how far a finger closes, from how hard you press it |
| `position` | drive the hand for real | the finger's measured angle |
| `external` | nothing — the recorder publishes no hand commands at all | the angle reached by the `:8088` sliders |
| `follow` | only `f` and the shape keys reach the hand | the angle you push a finger into |
The panel greys out keys the current mode ignores and says why, so a dead button is never a
mystery. Note `[` and `;` also un-park thumb_rot, while `o` returns it to tucked.
### The take library
Per take: **▶ replay · ⬇ download · ⧉ duplicate · ✎ rename · 🗑 delete**, plus **⬆ Upload**
(validated: the first line must be the recorder's own meta header) and **🗑 Delete all**.
`arm_home.jsonl` is hidden and protected — it is the home pose every take returns through,
not a recording.
Takes live on the **host** at `~/hand_data/DataG1`, bind-mounted into the container.
`docker/run.sh` begins with `docker rm -f`, so anything kept inside the image would be
destroyed on every restart.
---
## Where things are
| | |
|---|---|
| Workstation (canonical source) | `~/Robotics_workspace/yslootahtech/Project/G1/DFX_inspire_service` |
| Robot | `~/DFX_inspire_service` |
| In the container | `/opt/hand` (service + dashboard) · `/opt/recorder` (recorder + `DataG1`) |
| Host data (survives rebuilds) | `~/hand_data``hand_poses.txt`, `hand_combos.json`, `DataG1/` |
```
inspire_g1 RS-485 -> both hands, publishes rt/inspire/state
hand_bridge TCP 127.0.0.1:7799 -> rt/inspire/cmd, plus the force-follow loop at 30Hz
hand_web.py the dashboard on :8088, and it spawns the recorder
```
All three run inside the single `inspire-hand` container (`--network host --privileged
-v /dev:/dev` — DDS needs the host network, the CH340 adapters re-enumerate).
### What is in the image
Self-contained from a clean checkout: `recorder/` (the recorder + home pose),
`vendor/unitree_sdk2_python/`, `web/`, `example/`, `include/`.
The build still reads three things from the **robot's own system install**, because they are
compiled libraries rather than source:
- `~/unitree_sdk2` — the C++ static lib the binaries link against
- `/usr/local/{include,lib}` — CycloneDDS headers and runtime
- `~/cyclonedds/install/include/idl` — IDL headers; the Python `cyclonedds` wheel compiles a
`_idlpy` extension against them, and a normal CycloneDDS install does not put them in
`/usr/local`
So: **runtime is 100% Docker; building the image needs the robot's SDK.** Override the
locations with `UNITREE_SDK2_DIR`, `CYCLONEDDS_SRC`, `RECORDER_SRC`, `UNITREE_SDK2PY_DIR`.
`web/` is copied in the last layer, so editing the dashboard rebuilds in seconds instead of
recompiling every binary.
---
## Troubleshooting
**`recorder: NOT found`** — the image was built without `recorder/` or `vendor/`. Rebuild:
`./start.sh rebuild`.
**Stop does nothing during a take** — the dashboard was started with SIGINT ignored (any
process backgrounded by a non-interactive shell inherits `SIG_IGN`, and it survives `exec`).
`hand_web.py` repairs this at startup; if the repair fails the panel says so. Restart it.
**Fingers don't move (thumb might)** — usually a latched actuator fault from two `inspire_g1`
running at once. `./start.sh restart` re-runs `ClearError`. Still stuck: power-cycle the
hands (24 V).
**"Port drift" warning** — the service opened different ttys than are live now; the CH340
adapters re-enumerate on the hub. Restart.
**A finger reads as dead (0 current, no movement)** — its force-sensor zero has drifted past
the grip-force limit, so the RH56 refuses to drive it. `inspire_g1` measures the resting
offset at startup and raises that hand's limit to clear it; the log names the fingers. The
real fix is re-zeroing the sensor with the hand unloaded.
**`rt/inspire/state` reads all zeros** — cosmetic; the CH340 adapters echo their own TX, so
position read-back is unreliable. Commands are unaffected.
**Two `inspire_g1` instances** — they collide on the RS-485 bus. `docker/run.sh` kills
host-native copies before starting the container. Kill with `pkill -x inspire_g1`, never
`pkill -f`.
---
## Native (non-Docker) path
Kept for bring-up and debugging; `manage.sh` runs the services directly on the robot
(`./manage.sh status|start|restart|dashboard|...`). It needs `./build.sh` first, since the
compiled `build/` is not shipped. Do not run it while the container is up — two writers on
the RS-485 bus corrupt the hands.
Serial setup, udev rules and hand-side wiring: [SETUP_G1.md](SETUP_G1.md).
Docker specifics: [docker/README.md](docker/README.md).
---
## Service protocol (upstream)
Send `unitree_go::msg::dds::MotorCmds_` to `rt/inspire/cmd`; read
`unitree_go::msg::dds::MotorStates_` from `rt/inspire/state`. Only `q` is meaningful —
`1.00` = finger open, `0.00` = closed.
```mermaid
graph LR
A(user) --rt/inspire/cmd--> B(H1 or G1)
B --rt/inspire/state--> A
```
| id | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hand | right | right | right | right | right | right | left | left | left | left | left | left |
| joint | pinky | ring | middle | index | thumb-bend | thumb-rot | pinky | ring | middle | index | thumb-bend | thumb-rot |
`hand_bridge` also serves this over TCP on `127.0.0.1:7799` (`S` = state+force, `R` = state,
`F` = force-follow), which is how the dashboard talks to the hands without a DDS stack of its
own.