215 lines
9.2 KiB
Markdown
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.
|