Dashboard (web/hand_web.py) - Record/replay panel driving g1_record_replay.py as a pty child: take library (replay/download/duplicate/rename/delete/upload/delete-all), pause & resume, and in-take key buttons that grey out in the --fingers modes the recorder ignores (measured: in touch mode the keys change nothing at all). - /api/restart is container-aware: it kills inspire_g1 and lets the supervisor relaunch it. It used to run manage.sh, which started a SECOND inspire_g1 beside the supervised one - two writers on one RS-485 bus - and never returned. - Shape/combo libraries take a .bak on every write, with an undo button. Both files are rewritten in full, so deleting the last entry was unrecoverable. recorder/ - The recorder lives in this project now: one source of truth for the CLI and the dashboard, with pause/resume added to replay. - record.sh picks a runtime by itself (a python with the SDK, the vendored SDK, or the inspire-hand container). bundle.sh packs a ~340KB portable kit. tools/ - preflight.sh: read-only readiness report for a new robot (hardware, docker, build prerequisites, per-robot settings) ending in an install-path verdict. - fetch_deps.sh: stage build dependencies, verifying the libs are aarch64. - export_ui.py: regenerate an embedding app's vendored copy of the UI. docker/ - build_image.sh resolves its dependencies from several layouts: deps/ inside the project, /usr/local, a source install prefix, or a ROS2 colcon workspace (where the idl headers live when /usr/local has none). - web/ is copied in the last layer, so dashboard edits skip the C++ rebuild. - restart=always, and start.sh always builds so an edit cannot silently run a stale image. deps/unitree_sdk2 is vendored so a robot that has never seen the SDK can build. Docs: README quickstart + embedding notes, SETUP_G1 corrected (that udev rule stopped creating /dev/inspire_* symlinks a while ago), ROBOT_README describing a live install.
245 lines
9.6 KiB
Markdown
245 lines
9.6 KiB
Markdown
# Inspire RH56 hands — what is installed on this robot
|
|
|
|
Everything for the two Inspire RH56 hands: the RS-485 service, the DDS bridge, the web
|
|
dashboard, and the arm+hand record/replay studio. All of it runs in **one Docker container**.
|
|
|
|
Installed 2026-08-26 on the G1's Jetson (PC2), Ubuntu 20.04 / aarch64.
|
|
|
|
| | |
|
|
|---|---|
|
|
| Dashboard | `http://192.168.123.164:8088` (ethernet) · `http://10.255.254.68:8088` (wifi) |
|
|
| Project | `~/DFX_inspire_service` |
|
|
| Manual recorder kit | `~/manual_recorder` |
|
|
| Your data (survives rebuilds) | `~/hand_data` — shapes, combos, `DataG1/` takes |
|
|
| Image | `inspire-hand:latest`, built on this robot |
|
|
| Canonical source | workstation: `Project/G1/DFX_inspire_service` |
|
|
|
|
---
|
|
|
|
## 1. Run it
|
|
|
|
```bash
|
|
cd ~/DFX_inspire_service
|
|
./start.sh # build if needed → run → wait until it answers → print the URL
|
|
./start.sh status # running? serving what? hands alive?
|
|
./start.sh stop
|
|
./start.sh logs # follow the container log
|
|
./start.sh rebuild # force an image rebuild
|
|
```
|
|
|
|
It starts on boot (`--restart unless-stopped`). To stop that: change `--restart no` in
|
|
`docker/run.sh` and re-run `./start.sh`.
|
|
|
|
`./start.sh` always rebuilds first — the layer cache makes that a second or two, and it means
|
|
an edited dashboard can never keep running the old image.
|
|
|
|
---
|
|
|
|
## 2. What runs where
|
|
|
|
```
|
|
inspire_g1 RS-485 → both hands, publishes rt/inspire/state (supervised, auto-restarts)
|
|
hand_bridge TCP 127.0.0.1:7799 → rt/inspire/cmd + force-follow
|
|
hand_web.py the dashboard on :8088, and it spawns the recorder
|
|
```
|
|
|
|
All three inside the `inspire-hand` container, started by `docker/start.sh`. Container paths:
|
|
`/opt/hand` (service + dashboard), `/opt/recorder` (recorder + `DataG1`).
|
|
|
|
Flags that matter: `--network host` (DDS needs it), `--privileged -v /dev:/dev` (the CH340
|
|
adapters re-enumerate), `-v ~/hand_data:/opt/hand/data`, `-v ~/hand_data/DataG1:/opt/recorder/DataG1`.
|
|
|
|
---
|
|
|
|
## 3. The dashboard
|
|
|
|
| card | what it does |
|
|
|---|---|
|
|
| Fingers | 12 sliders, presets, save/load named shapes |
|
|
| Force follow | press a fingertip and it closes under your hand — the RH56 is not back-drivable, so this admittance loop is the only way to shape it by hand |
|
|
| Joint tracker | angle traces + force, current, force limit, temperature, ERROR/STATUS |
|
|
| Diagnostics | live read-back + close→open self-test, per hand |
|
|
| Read back | copy the hand's 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, manage the library ⚠ arm moves |
|
|
| Library | manage saved shapes and combos (with ↺ undo) |
|
|
|
|
### Record / replay
|
|
|
|
A take holds the arms ~3 s then goes **limp** — have hold of the arm before that. Fingers
|
|
cannot be posed by hand, so drive them with the in-take keys or your saved shapes. At the end
|
|
the recorder offers a preview (⚠ replays it — the arm moves) and then a save; both are
|
|
answered in the panel. **Stop** interrupts cleanly (arm goes home, then releases) — it does
|
|
not kill. Replay has **⏸ Pause / ▶ Resume**; a pause holds the frame rather than dropping the
|
|
arm.
|
|
|
|
In-take keys: `o`/`c` both open/close · `[` `]` right · `;` `'` left · `1`-`9` saved shapes ·
|
|
`f` arm/disarm follow. Whether they do anything depends on the mode:
|
|
|
|
| `--fingers` | keys | records |
|
|
|---|---|---|
|
|
| `touch` *(default)* | **ignored** — the hand holds the rest pose | how far a finger closes, from how hard you press |
|
|
| `position` | drive the hand for real | the measured angle |
|
|
| `external` | ignored — no hand commands sent | the angle the sliders reach |
|
|
| `follow` | only `f` and the shape keys | the angle you push a finger into |
|
|
|
|
The panel greys out keys the current mode ignores.
|
|
|
|
Take library: **▶ replay · ⬇ download · ⧉ duplicate · ✎ rename · 🗑 delete**, plus **⬆ Upload**
|
|
and **🗑 Delete all**. `arm_home.jsonl` is hidden and protected — it is the home pose every
|
|
take returns through, not a recording.
|
|
|
|
---
|
|
|
|
## 4. Without the dashboard (the backup plan)
|
|
|
|
The same script, from a terminal. Installed at `~/manual_recorder`:
|
|
|
|
```bash
|
|
cd ~/manual_recorder/recorder
|
|
./record.sh doctor # which runtime it will use
|
|
./record.sh record --output wave --seconds 20
|
|
./record.sh replay --input wave --speed 0.5
|
|
```
|
|
|
|
`record.sh` finds a runtime by itself — a python that can import `unitree_sdk2py`, the
|
|
vendored SDK, or (here) the **container**, which has the SDK baked in. `DataG1` is symlinked
|
|
to `~/hand_data/DataG1`, so CLI takes and dashboard takes are the same library.
|
|
|
|
Raw equivalent:
|
|
|
|
```bash
|
|
docker exec -it -w /opt/recorder inspire-hand python3 g1_record_replay.py eth0 \
|
|
record --output wave --seconds 20
|
|
```
|
|
|
|
`-it` matters — without a TTY the in-take keys and pause do not work.
|
|
|
|
Carry it elsewhere: `./bundle.sh` → `manual_recorder_<date>.tar.gz` (~340 KB), or
|
|
`--with-takes` to include recordings.
|
|
|
|
---
|
|
|
|
## 5. Per-robot state — read this
|
|
|
|
**Hands are identified by USB path, not by id.** Both answer at RS-485 id 1, so
|
|
`INSPIRE_RIGHT_PATH=1-2.2.1.1` in `docker/run.sh` decides which is which:
|
|
|
|
```
|
|
RIGHT = /dev/ttyUSB1 (usb 1-2.2.1.1)
|
|
LEFT = /dev/ttyUSB0 (usb 1-2.2.3)
|
|
```
|
|
|
|
Move an adapter to another socket and left/right silently swap. The permanent fix, once:
|
|
|
|
```bash
|
|
# confirm ttyUSB0 really is the LEFT hand first — this writes a permanent id
|
|
docker exec inspire-hand ./build/hand_setid /dev/ttyUSB0 1 2
|
|
```
|
|
|
|
After that identity is `id 1 = right, id 2 = left`, immune to replugs and hub changes.
|
|
|
|
**Force-sensor zeros have drifted** on both hands. `inspire_g1` measures the resting offset at
|
|
startup and raises that hand's grip limit so the affected fingers do not read as dead — but
|
|
touch-mode recording thresholds off exactly that signal, so light presses read wrong. The real
|
|
fix is re-zeroing with the hands unloaded.
|
|
|
|
**A card reader (`05e3:0749`) shares the hands' USB hub** and power-cycles constantly with no
|
|
card in it, which re-enumerates the branch and shuffles the `ttyUSB` numbers. Stop it for good:
|
|
|
|
```bash
|
|
./start.sh usbfix # installs a udev rule + deauthorizes it (asks for your password)
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Changes made to this robot during install
|
|
|
|
| change | why | revert |
|
|
|---|---|---|
|
|
| `unitree` added to the `docker` group | every docker call was permission-denied | `sudo gpasswd -d unitree docker` |
|
|
| wifi radio unblocked, joined `BusinessPro_TP_5G` (`10.255.254.68`) | robot was ethernet-only | `sudo nmcli radio wifi off` |
|
|
| wifi route metric → 50 | `eth0` held the default route, so internet went out the robot-internal link and died; the in-image `pip` build needs internet | `sudo nmcli con mod BusinessPro_TP_5G ipv4.route-metric -1` |
|
|
|
|
`eth0`'s own configuration was not touched — robot-internal traffic is unaffected.
|
|
|
|
---
|
|
|
|
## 7. Rebuilding / redeploying
|
|
|
|
Workstation is canonical. Push and restart:
|
|
|
|
```bash
|
|
# from the workstation
|
|
rsync -az --exclude build/ Project/G1/DFX_inspire_service/ unitree@192.168.123.164:~/DFX_inspire_service/
|
|
ssh unitree@192.168.123.164 'cd ~/DFX_inspire_service && ./start.sh'
|
|
```
|
|
|
|
`./start.sh` run *from* the workstation does the same thing by itself (it rsyncs and re-runs
|
|
over SSH). `ROBOT=unitree@10.255.254.68 ./start.sh` targets the wifi address.
|
|
|
|
### How the image was built here
|
|
|
|
This robot had no `~/unitree_sdk2` and no `idl` headers in `/usr/local`, so the build used:
|
|
|
|
- `deps/unitree_sdk2` — shipped inside the project
|
|
- `/usr/local/include` — its CycloneDDS C/C++ headers and libs
|
|
- `~/cyclonedds_ws/install/cyclonedds/include/idl` — the idl headers, from its ROS2 colcon
|
|
workspace (a plain C++ install leaves those out, and the in-image `pip cyclonedds` build
|
|
needs them)
|
|
|
|
`./tools/preflight.sh` reports all of this and says which install path is open.
|
|
|
|
For a robot with none of it, carry the image instead:
|
|
|
|
```bash
|
|
docker save inspire-hand:latest | gzip > inspire-hand.tar.gz # here
|
|
gunzip -c inspire-hand.tar.gz | docker load # there
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Troubleshooting
|
|
|
|
**`service=false`, `ports=none`** — no `/dev/ttyUSB*`. Check both CH340 adapters are in and
|
|
the hands have 24 V. `lsusb | grep 1a86` should list two. The supervisor starts `inspire_g1`
|
|
within ~3 s of them appearing.
|
|
|
|
**Fingers do not move (thumb might)** — usually a latched actuator fault from two `inspire_g1`
|
|
running at once. Press **Restart** in the dashboard (re-runs `ClearError`); still stuck,
|
|
power-cycle the hands (24 V).
|
|
|
|
**A finger reads as dead (0 current, no movement)** — its force-sensor zero drifted past the
|
|
grip limit, so the RH56 refuses to drive it. Startup raises the limit; the log names the
|
|
fingers.
|
|
|
|
**"Port drift" warning** — the ttys renumbered. Harmless (hands are pinned by USB path); the
|
|
card reader is the cause — see `usbfix` above.
|
|
|
|
**`rt/inspire/state` reads all zeros** — cosmetic; the CH340s echo their own TX, so 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. Kill with `pkill -x inspire_g1`, never `pkill -f`.
|
|
|
|
**Recorder says "not found"** — the image was built without `recorder/`. `./start.sh rebuild`.
|
|
|
|
---
|
|
|
|
## 9. Safety
|
|
|
|
- A take makes the **arms go limp** after ~3 s. Be holding the arm.
|
|
- **Replay and preview move the arm** through the whole recording.
|
|
- Never run a CLI take while the dashboard has one going — two writers on the same topics.
|
|
- Do not run the native `manage.sh` path while the container is up, for the same reason.
|
|
|
|
---
|
|
|
|
## 10. Removing it
|
|
|
|
```bash
|
|
./start.sh stop
|
|
docker rmi inspire-hand:latest
|
|
rm -rf ~/DFX_inspire_service ~/manual_recorder # ~/hand_data holds your takes — keep it
|
|
```
|