DFX_inspire_service/ROBOT_README.md
kassam 2a78f1b609 Record/replay studio, manual recorder kit, and new-robot install tooling
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.
2026-08-28 20:27:28 +04:00

9.6 KiB

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

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:

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:

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.shmanual_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:

# 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:

./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:

# 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:

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

./start.sh stop
docker rmi inspire-hand:latest
rm -rf ~/DFX_inspire_service ~/manual_recorder     # ~/hand_data holds your takes — keep it