2026-08-04 16:09:17 +04:00
2025-08-18 10:56:30 +08:00
2026-08-04 16:09:17 +04:00
2026-08-04 16:09:17 +04:00
2026-08-04 16:09:17 +04:00
2026-08-04 16:09:17 +04:00
2026-08-04 16:09:17 +04:00
2026-08-04 16:09:17 +04:00
2025-08-18 10:56:30 +08:00
2026-08-04 16:09:17 +04:00
2025-08-18 11:01:56 +08:00
2026-08-04 16:09:17 +04:00
2026-08-04 16:09:17 +04:00
2025-08-18 10:56:30 +08:00
2025-08-18 11:03:37 +08:00
2026-08-04 16:09:17 +04:00
2026-08-04 16:09:17 +04:00
2026-08-04 16:09:17 +04:00
2026-08-04 16:09:17 +04:00
2026-08-04 16:09:17 +04:00

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; the service protocol section at the bottom is theirs.


Run it

./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 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_datahand_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. Docker specifics: 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.

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.

Description
No description provided
Readme
Languages
Python 45%
C++ 38.4%
Shell 15%
Dockerfile 1.2%
CMake 0.4%