SDK for the ZBL five-finger dexterous hand, providing ROS 2 interfaces and ROS2-based high-level Python/C++ APIs for secondary development.
Warning
-
Before operation
- Ensure the hand is firmly mounted
- Keep clear of fingers during motion
- Maintain a safe distance before enabling position control
-
Do not launch multiple ROS processes with overlapping parameters at the same time
Running
ros2 launch zbl_dexterous_hand_bringup single_hand.launch.pystarts right-hand control services. Launching multiple overlapping processes may cause abnormal behavior and damage finger motors. -
In dual-hand scenarios, do not connect both hands through the same USB hub (shared upstream port). Two uncompressed YUYV 640x480 @30fps streams require about 36 MB/s and may saturate one USB 2.0 upstream link, causing one camera to fail with
Unable to start stream. If you need both camera streams, connect each hand to a different USB port on the host. -
Do not plug/unplug power or USB cables while powered on
Always power off the hand before plugging or unplugging any cable to avoid hardware damage.
sdk_project/
├── .devcontainer/ # VSCode/Cursor dev container configuration
│ ├── devcontainer.json
│ ├── docker-compose.yml
│ ├── Dockerfile.base # Base image: system + dependency packages
│ ├── Dockerfile # Dev image: user setup + COPY deps/ (~30s)
│ ├── apt.list
│ ├── ros2_apt.list
│ └── pip.list
├── deps/ # Dependencies
├── lib/
│ ├── python/ # Python wrapper API (on top of ROS2)
│ └── cpp/ # C++ wrapper API (on top of ROS2)
├── examples/
│ ├── python/ # Python examples
│ └── cpp/ # C++ examples
├── docs/ # API references
│ ├── API_ROS2_CN.md # ROS2 interface reference (CN)
│ ├── API_ROS2.md # ROS2 interface reference (EN)
│ ├── API_SDK_CN.md # SDK wrapper API reference (CN)
│ └── API_SDK.md # SDK wrapper API reference (EN)
├── setup_serial.sh # Host serial setup script
├── README.md # This file
└── README_CN.md # Chinese version
- Linux (Ubuntu 22.04+ recommended)
- At least 2 USB ports available (USB 2.0+)
- Docker and Docker Compose
- USB Type-C adapter
- Stable DC power supply, at least 36V / 4A
Install Docker: Docker official docs
Install Docker Compose: Docker Compose official docs
To speed up image pulls (especially base images such as ubuntu:24.04), configure registry mirrors.
Tip
- You can configure multiple mirror endpoints for better failover
- Remove/replace unavailable endpoints as needed
- This significantly improves base image build speed
Create or edit /etc/docker/daemon.json:
sudo mkdir -p /etc/docker
sudo vim /etc/docker/daemon.jsonExample full JSON:
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://dockerproxy.com",
"https://docker.mirrors.ustc.edu.cn",
"https://docker.nju.edu.cn"
]
}Apply:
sudo systemctl daemon-reload
sudo systemctl restart dockerVerify:
docker info | grep -A 5 "Registry Mirrors"Common mirror sources:
| Mirror | URL | Notes |
|---|---|---|
| DaoCloud | https://docker.m.daocloud.io |
Recommended, usually stable |
| DockerProxy | https://dockerproxy.com |
Community-maintained |
| USTC | https://docker.mirrors.ustc.edu.cn |
Good for education networks |
| NJU | https://docker.nju.edu.cn |
Good for education networks |
| Aliyun | https://<your_id>.mirror.aliyuncs.com |
Register in Aliyun CR first |
Connect the hand to host via USB cable.

Run on the host machine (not inside container), from open_hand_sdk root.
By default, left hand is d601, right hand is d602.
bash setup_serial.sh --identifyExample:
=== Connected GD32 CDC Devices ===
/dev/ttyACM0 serial=358C657933BB PID=d602 (RIGHT hand — ZBL dexterous hand)
The PID determines which installation mode to use:
| Hardware | PID | Output hint |
|---|---|---|
| Right hand | d602 |
PID=d602 (RIGHT hand ...) |
| Left hand | d601 |
PID=d601 (LEFT hand ...) |
| Same PID on both hands | identical | Use --device to pin each device node |
You can also query directly:
udevadm info /dev/ttyACM0 | grep ID_MODEL_ID
# E: ID_MODEL_ID=d602Choose one of the following modes. Hands shipped with different left/right PIDs (the default) should use Mode A.
Merge semantics.
--install-rulesperforms a per-symlink merge into/etc/udev/rules.d/99-zbl-dexterous-hand.rules: only the symlinks written this run are replaced; rules for other symlinks (e.g. the other hand, the exoskeleton) and any manual edits OUTSIDE the# >>> zbl-managed: <symlink>/# <<< zbl-managed: <symlink>markers are preserved across runs. Use the standalone--resetcommand (see below) to remove all script-managed blocks.
Mode A — by PID (recommended)
When the left/right hands have different PIDs (d601 / d602), udev matches each hand by PID, so plug order doesn't matter. The two single-side commands can be run separately and the second one will not wipe the first.
bash setup_serial.sh --install-rules # right hand only (--side defaults to right, PID d602)
bash setup_serial.sh --install-rules --side left # left hand only (PID d601) — keeps right hand rule
bash setup_serial.sh --install-rules --side dual # both hands at onceMode B — pin by device node
When two devices share the same PID (two same-PID hands, or two same-PID exoskeletons), pin a specific ttyACMx to each one. The script can only pin one hand and at most one exo per call, so run it once per side. Thanks to the merge semantics above, the second call does not wipe what the first call wrote.
Pin only the hands:
bash setup_serial.sh --install-rules --side right --device /dev/ttyACM0
bash setup_serial.sh --install-rules --side left --device /dev/ttyACM1Pin a hand and its exoskeleton in one call (--exo enables exo handling, --exo-device pins the exo node):
bash setup_serial.sh --install-rules --side right --device /dev/ttyACM0 --exo --exo-device /dev/ttyACM2Two hands plus two exoskeletons (four devices, two calls, all preserved across runs):
bash setup_serial.sh --install-rules --side right --device /dev/ttyACM0 --exo --exo-device /dev/ttyACM2
bash setup_serial.sh --install-rules --side left --device /dev/ttyACM1 --exo --exo-device /dev/ttyACM3You can also mix Mode A and Mode B — e.g. install both hands by PID once, then pin only the exoskeletons by node:
bash setup_serial.sh --install-rules --side dual # hands by PID
bash setup_serial.sh --install-rules --side right --exo --exo-device /dev/ttyACM2 # right exo by node
bash setup_serial.sh --install-rules --side left --exo --exo-device /dev/ttyACM3 # left exo by nodeAfter installation the script automatically prints each expected symlink with its real device node and VID/PID/serial:
=== Verify symlinks (rules file: /etc/udev/rules.d/99-zbl-dexterous-hand.rules) ===
[OK] /dev/zbl_right_dexterous_hand -> /dev/ttyACM0 (VID:0483 PID:d602 serial=EDAA6579427F)
[OK] /dev/zbl_left_dexterous_hand -> /dev/ttyACM2 (VID:0483 PID:d601 serial=DB2C657934B9)
All 2 expected symlink(s) present and ready to use.
If any line shows [MISSING], power cycle and replug the USB cable, then re-check:
bash setup_serial.sh --side dual # or --side left / --side right
# fallback: ls -la /dev/zbl_*Mode C — bypass udev: pass raw serial paths directly to the launch file (no udev rules needed):
ros2 launch zbl_dexterous_hand_bringup single_hand.launch.py side:=right serial_port:=/dev/ttyACM0
ros2 launch zbl_dexterous_hand_bringup single_hand.launch.py side:=left serial_port:=/dev/ttyACM1
ros2 launch zbl_dexterous_hand_bringup dual_hand.launch.py left_serial_port:=/dev/ttyACM1 right_serial_port:=/dev/ttyACM0Run the standalone --reset command to remove every script-managed block from the rules file. It cannot be combined with --install-rules or --identify. Manual rules outside the markers are kept; if nothing else remains, the rules file itself is deleted.
bash setup_serial.sh --resetRemoving 4 zbl-managed block(s) from /etc/udev/rules.d/99-zbl-dexterous-hand.rules:
- zbl_right_dexterous_hand
- zbl_left_dexterous_hand
- right_exo_skeleton
- left_exo_skeleton
No manual rules left; removing /etc/udev/rules.d/99-zbl-dexterous-hand.rules entirely.
Replug the USB cable to make any removed /dev/zbl_* and /dev/*_exo_skeleton symlinks disappear immediately.
Docker uses a two-layer image structure:
| Image | Dockerfile | Rebuild when |
|---|---|---|
zbl_hand_sdk:base_latest |
Dockerfile.base |
apt.list / ros2_apt.list / pip.list / Dockerfile.base changed (~10 min) |
zbl_hand_sdk_image:v0.1.1 |
Dockerfile |
deps/ updated (~30s) |
# First time (one-time, ~10 min)
cd /path/to/open_hand_sdk
docker build -f .devcontainer/Dockerfile.base -t zbl_hand_sdk:base_latest .
# Rebuild dev image on existing base (~30s)
cd .devcontainer
docker compose build
docker compose up -dOr use VSCode/Cursor Dev Containers:
- Install Dev Containers
- Open project folder
F1-> Dev Containers: Reopen in Container
docker exec -it zbl_hand_sdk bashsource commands are already written into .bashrc in .devcontainer/Dockerfile, so no extra sourcing is required after entering the container.
The hand camera is mounted on the lower palm area.
Camera device paths are configured in:
/opt/xr/core/install/zbl_dexterous_hand_bringup/share/zbl_dexterous_hand_bringup/config/camera_right.yaml/opt/xr/core/install/zbl_dexterous_hand_bringup/share/zbl_dexterous_hand_bringup/config/camera_left.yaml
How to find the correct /dev/video*:
- Check existing devices before plugging camera USB:
ls /dev/video*- Plug camera USB, check again. Newly appeared devices belong to the camera.
For example, if/dev/video4appears, setvideo_device: /dev/video4.
If two devices appear (for example4and5), usually the even index (4) is the capture stream.
# ── Single hand ─────────────────────────────────────────────────────────────
# Right hand (default)
ros2 launch zbl_dexterous_hand_bringup single_hand.launch.py &
# Left hand
ros2 launch zbl_dexterous_hand_bringup single_hand.launch.py side:=left &
# With eye-in-hand camera (camera:=true is the default, set false to disable)
ros2 launch zbl_dexterous_hand_bringup single_hand.launch.py camera:=true &
ros2 launch zbl_dexterous_hand_bringup single_hand.launch.py side:=left camera:=true &
# With exoskeleton teleoperation
ros2 launch zbl_dexterous_hand_bringup single_hand.launch.py use_exoskeleton:=true &
ros2 launch zbl_dexterous_hand_bringup single_hand.launch.py side:=left use_exoskeleton:=true &
# Custom exo serial port (default: {side}_exo_skeleton udev symlink)
ros2 launch zbl_dexterous_hand_bringup single_hand.launch.py use_exoskeleton:=true exo_serial_port:=/dev/ttyACM0 &
# ── Dual hand ────────────────────────────────────────────────────────────────
ros2 launch zbl_dexterous_hand_bringup dual_hand.launch.py &
# With cameras (both on by default)
ros2 launch zbl_dexterous_hand_bringup dual_hand.launch.py right_camera:=true left_camera:=true &
# With exoskeleton teleoperation (both sides)
ros2 launch zbl_dexterous_hand_bringup dual_hand.launch.py use_exoskeleton:=true &
# Custom exo serial ports for dual hand
ros2 launch zbl_dexterous_hand_bringup dual_hand.launch.py use_exoskeleton:=true \
right_exo_serial_port:=/dev/ttyACM0 left_exo_serial_port:=/dev/ttyACM1 &If launched correctly, the hand performs a homing motion (close then open).
Verify controllers:
ros2 control list_controllersExpected output (dual-hand example):
right_dexterous_hand_controller x2robot_controllers/DexterousHandCommandController inactive
right_hand_tactile_sensor_broadcaster x2robot_controllers/TashanTactileSensorBroadcaster active
left_hand_tactile_sensor_broadcaster x2robot_controllers/TashanTactileSensorBroadcaster active
left_dexterous_hand_controller x2robot_controllers/DexterousHandCommandController inactive
joint_state_broadcaster joint_state_broadcaster/JointStateBroadcaster active
{side}_dexterous_hand_controlleris inactive by default. Activate it before sending position commands.
cd ~/workspace
# Camera viewer
python3 examples/python/ROS2/camera_viewer.py
python3 examples/python/ROS2/camera_viewer.py --side left
python3 examples/python/ROS2/camera_viewer.py --side dual
# Python ROS2-layer examples
python3 examples/python/ROS2/controller_manager.py --list
python3 examples/python/ROS2/controller_manager.py --switch pos --side dual
python3 examples/python/ROS2/state_monitor.py --side dual
python3 examples/python/ROS2/tactile_monitor.py --side left
python3 examples/python/ROS2/finger_control.py --joint right_1_1 --delta 0.1
python3 examples/python/ROS2/hand_demo.py --gesture fist
python3 examples/python/ROS2/hand_demo.py --sequence --side left
# Python WrapperAPI-layer examples
python3 examples/python/WrapperAPI/state_monitor.py --side dual --hz 10
python3 examples/python/WrapperAPI/tactile_monitor.py --side right --hz 10
python3 examples/python/WrapperAPI/finger_control.py --joint right_2_2 --target 1.02 --duration 2.0
python3 examples/python/WrapperAPI/hand_demo.py --sequence --side dual
# C++ examples
source /opt/xr/core/install/setup.bash
colcon build --base-paths lib/cpp lib/python examples/cpp --symlink-install
source install/setup.bash
ros2 run dexterous_hand_examples state_monitor --side right
ros2 run dexterous_hand_examples tactile_monitor --side left
ros2 run dexterous_hand_examples finger_control --joint right_1_1 --delta 0.1
ros2 run dexterous_hand_examples hand_demo --gesture fist --side dualSee the Examples section below for full command references.
ROS Domain ID defaults to 134. Change ROS_DOMAIN_ID in .devcontainer/docker-compose.yml if needed.
| Topic | Type | Description |
|---|---|---|
/{side}_dexterous_hand_controller/commands |
std_msgs/msg/Float64MultiArray |
15-joint position command (rad) |
| Topic | Type | Description |
|---|---|---|
/joint_states |
sensor_msgs/msg/JointState |
Global joint state (pos/vel/effort) |
/diagnostics |
diagnostic_msgs/msg/DiagnosticArray |
Aggregated diagnostics (device/controller health) |
/{side}_hand_tactile |
x2robot_msgs/msg/TashanTactileData |
Fingertip tactile data |
/{side}_eye_in_hand/image_raw/compressed |
sensor_msgs/msg/CompressedImage |
Compressed camera image |
# Publish right-hand open pose command (controller must be active)
ros2 topic pub /right_dexterous_hand_controller/commands \
std_msgs/msg/Float64MultiArray \
"{data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}" --once
# Joint states
ros2 topic echo /joint_states
# Diagnostics
ros2 topic echo /diagnostics
# Right-hand tactile
ros2 topic echo /right_hand_tactile| Service | Type | Description |
|---|---|---|
/controller_manager/list_controllers |
controller_manager_msgs/ListControllers |
Query controller states |
/controller_manager/switch_controller |
controller_manager_msgs/SwitchController |
Switch controllers |
ros2 control switch_controllers --activate right_dexterous_hand_controller| Controller | Plugin | Default | Description |
|---|---|---|---|
joint_state_broadcaster |
joint_state_broadcaster/JointStateBroadcaster |
active | Publishes /joint_states |
{side}_dexterous_hand_controller |
x2robot_controllers/DexterousHandCommandController |
inactive | Position controller |
{side}_hand_tactile_sensor_broadcaster |
x2robot_controllers/TashanTactileSensorBroadcaster |
active | Tactile broadcaster |
The hand has 15 independently actuated joints (5 fingers x 3 joints each), in radians.
| Index | Joint | Finger | Description | Range (rad) |
|---|---|---|---|---|
| 0 | {prefix}1_1_joint |
Thumb | Splay / abduction | [0, 1.8316] |
| 1 | {prefix}1_2_joint |
Thumb | Proximal flexion | [0, 0.872] |
| 2 | {prefix}1_3_joint |
Thumb | Distal flexion | [0, 1.57] |
| 3 | {prefix}2_1_joint |
Index | Splay | [-0.348, 0.348] |
| 4 | {prefix}2_2_joint |
Index | MCP flexion | [0, 1.57] |
| 5 | {prefix}2_3_joint |
Index | PIP flexion | [0, 1.57] |
| 6 | {prefix}3_1_joint |
Middle | Splay | [-0.348, 0.348] |
| 7 | {prefix}3_2_joint |
Middle | MCP flexion | [0, 1.57] |
| 8 | {prefix}3_3_joint |
Middle | PIP flexion | [0, 1.57] |
| 9 | {prefix}4_1_joint |
Ring | Splay | [-0.348, 0.348] |
| 10 | {prefix}4_2_joint |
Ring | MCP flexion | [0, 1.57] |
| 11 | {prefix}4_3_joint |
Ring | PIP flexion | [0, 1.57] |
| 12 | {prefix}5_1_joint |
Pinky | Splay | [-0.348, 0.348] |
| 13 | {prefix}5_2_joint |
Pinky | MCP flexion | [0, 1.57] |
| 14 | {prefix}5_3_joint |
Pinky | PIP flexion | [0, 1.57] |
{prefix} is right_ or left_. {prefix}N_4_joint are mimic joints (DIP = PIP * 1), not independently commanded.
No pip install is required. Add lib/python into PYTHONPATH. Example scripts do this automatically via examples/python/sdk_paths.py.
from dexterous_hand import DexterousHandDexterousHand(
side: str = "right",
node: Optional[rclpy.node.Node] = None,
*,
manage_controller: bool = True,
)| Argument | Type | Description |
|---|---|---|
side |
str |
right or left |
node |
Node | None |
Optional external node |
manage_controller |
bool |
If False, skip activate/deactivate (monitor-only scenarios) |
| Method | Return | Description |
|---|---|---|
activate(timeout=10.0) |
bool |
Activate {side}_dexterous_hand_controller |
deactivate(timeout=10.0) |
bool |
Deactivate position controller |
is_active() |
bool |
Whether controller is active |
| Method | Return | Description |
|---|---|---|
set_positions(positions) |
None |
Publish one 15D target (clipped to limits) |
move_to(positions, duration=2.0, dt=0.02) |
bool |
Smoothstep interpolated motion |
open(duration=1.5) |
bool |
Move to all zeros (URDF rest pose) |
| Method | Return | Description |
|---|---|---|
get_joint_positions(timeout=5.0) |
np.ndarray(15,) | None |
Joint positions (rad) |
get_joint_velocities() |
np.ndarray(15,) | None |
Joint velocities (rad/s) |
get_joint_efforts() |
np.ndarray(15,) | None |
Joint efforts |
get_tactile_data() |
TashanTactileData | None |
Tactile message |
| Method | Description |
|---|---|
spin_background() |
Non-blocking background spin thread |
spin() |
Blocking spin |
destroy() |
Stop and destroy resources |
from dexterous_hand import (
NUM_JOINTS,
JOINT_LOWER,
JOINT_UPPER,
JOINT_SUFFIXES,
FINGER_LABELS,
)Demo poses are defined in examples/python/demo_poses.py: POSE_OPEN, POSE_FIST, POSE_PINCH, POSE_POINT, POSE_THREE, POSE_THUMBS_UP.
dexterous_hand_cpp is header-only:
#include "dexterous_hand/dexterous_hand.hpp"find_package(dexterous_hand_cpp REQUIRED)
ament_target_dependencies(my_node dexterous_hand_cpp rclcpp)DexterousHand(
const std::string& side = "right",
rclcpp::Node::SharedPtr node = nullptr);| Method | Return | Description |
|---|---|---|
activate(double timeout=10.0) |
bool |
Activate position controller |
deactivate(double timeout=10.0) |
bool |
Deactivate position controller |
isActive(double timeout=5.0) |
bool |
Query controller state |
| Method | Return | Description |
|---|---|---|
setPositions(const Positions&) |
void |
Publish one 15D target (clipped to limits) |
moveTo(const Positions&, double duration=2.0, double dt=0.02) |
bool |
Smoothstep interpolated motion |
open(double duration=1.5) |
bool |
Move to all zeros |
| Method | Return | Description |
|---|---|---|
getJointPositions(double timeout=5.0) |
std::optional<Positions> |
Joint positions |
getJointVelocities() |
std::optional<Positions> |
Joint velocities |
getJointEfforts() |
std::optional<Positions> |
Joint efforts |
| Method | Description |
|---|---|
spinBackground() |
Background callback thread |
destroy() |
Stop and destroy resources |
getNode() |
Get internal node |
namespace dexterous_hand {
constexpr int NUM_JOINTS = 15;
using Positions = std::array<double, NUM_JOINTS>;
constexpr Positions JOINT_LOWER = { ... };
constexpr Positions JOINT_UPPER = { ... };
}Demo poses are in examples/cpp/demo_poses.hpp (zbl_hand_demo namespace).
Examples are organized into two layers:
ROS2/: direct topic/service usageWrapperAPI/: based onDexterousHand
| Script | Layer | Description |
|---|---|---|
ROS2/controller_manager.py |
ROS2 | List/switch controllers (--list, --switch pos, --side) |
ROS2/state_monitor.py |
ROS2 | Joint state + diagnostics monitor (--side right/left/dual) |
ROS2/tactile_monitor.py |
ROS2 | Tactile monitor (--side right/left/dual) |
ROS2/finger_control.py |
ROS2 | Single-joint control (--joint + --delta/--target) |
ROS2/hand_demo.py |
ROS2 | Gesture demo (--gesture or --sequence) |
ROS2/camera_viewer.py |
ROS2 | Camera viewer (subscribes to image_raw/compressed only) |
WrapperAPI/state_monitor.py |
Wrapper | Joint + status + tactile + diagnostics terminal UI (--side, --hz, --no-diagnostics) |
WrapperAPI/tactile_monitor.py |
Wrapper | Tactile terminal UI (--side, --hz) |
WrapperAPI/finger_control.py |
Wrapper | Single-joint CLI (same as ROS2 version) |
WrapperAPI/hand_demo.py |
Wrapper | Gesture demo (same CLI as ROS2 version) |
Common commands (run under ~/workspace/examples/python):
cd ~/workspace/examples/python
# ROS2 layer
python3 ROS2/controller_manager.py --list
python3 ROS2/controller_manager.py --switch pos --side dual
python3 ROS2/state_monitor.py --side dual
python3 ROS2/tactile_monitor.py --side left
python3 ROS2/finger_control.py --joint right_1_1 --delta 0.1
python3 ROS2/hand_demo.py --gesture fist
python3 ROS2/hand_demo.py --sequence --side left
python3 ROS2/camera_viewer.py --side dual
# WrapperAPI layer
python3 WrapperAPI/state_monitor.py --side dual --hz 10
python3 WrapperAPI/state_monitor.py --no-diagnostics
python3 WrapperAPI/tactile_monitor.py --side right --hz 10
python3 WrapperAPI/finger_control.py --joint right_2_2 --target 1.02 --duration 2.0
python3 WrapperAPI/hand_demo.py --sequence --side dualGesture targets and sequences are defined in examples/python/demo_poses.py: GESTURE_TARGETS, DEMO_SEQUENCE.
| Program | Layer | Description |
|---|---|---|
ROS2/state_monitor |
ROS2 | Joint state monitor (--side right/left) |
ROS2/tactile_monitor |
ROS2 | Tactile monitor (--side right/left) |
WrapperAPI/finger_control |
Wrapper | Single-joint control (--joint + --delta/--target) |
WrapperAPI/hand_demo |
Wrapper | Gesture demo (--gesture/--sequence --side right/left/dual) |
cd ~/workspace
source /opt/xr/core/install/setup.bash
colcon build --base-paths lib/cpp lib/python examples/cpp --symlink-install
source install/setup.bash
ros2 run dexterous_hand_examples state_monitor --side right
ros2 run dexterous_hand_examples tactile_monitor --side left
ros2 run dexterous_hand_examples finger_control --joint right_1_1 --delta 0.1
ros2 run dexterous_hand_examples hand_demo --gesture fist --side dual
ros2 run dexterous_hand_examples hand_demo --sequence --side left# List controllers
ros2 control list_controllers
# Activate right position controller
ros2 control switch_controllers --activate right_dexterous_hand_controller
# Activate left position controller
ros2 control switch_controllers --activate left_dexterous_hand_controller
# Joint states
ros2 topic echo /joint_states
# Diagnostics
ros2 topic echo /diagnostics
# Publish open command (right hand)
ros2 topic pub /right_dexterous_hand_controller/commands \
std_msgs/msg/Float64MultiArray \
"{data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}" --once
# List topics
ros2 topic list
# Tactile
ros2 topic echo /right_hand_tactile
# List nodes
ros2 node list
# Record bag
ros2 bag record /joint_states \
/diagnostics \
/right_dexterous_hand_controller/commands \
/right_hand_tactileQ1: No module named 'rclpy'
Source workspace first:
source /opt/xr/core/install/setup.bashQ2: Serial device not found
Check USB connection and udev rules:
bash setup_serial.sh --install-rules
# replug USB cable
bash setup_serial.shQ3: Controller switching fails
Check whether launch process is running:
ros2 node list | grep controller_managerIf missing, restart launch process.
Q4: Left hand not recognized in dual-hand mode
Check PID of each hand (plug one hand at a time):
bash setup_serial.sh --identifyIf PIDs differ (d601/d602):
bash setup_serial.sh --install-rules --side dualIf both hands share the same PID, pin them by device node. Run once per side; thanks to the merge semantics described above, the second call does not wipe the first:
bash setup_serial.sh --install-rules --side right --device /dev/ttyACM0
bash setup_serial.sh --install-rules --side left --device /dev/ttyACM1Q5: Joint position out of limits
If you see an error like Joint position is out of bounds for the joint and the control process exits, keep commanded joint targets within limits and restart the process.
Q6: Joint angles become nan after exoskeleton calibration
This is usually caused by improper wearing or loose finger-link cables. If the linkage cannot be tensioned during calibration, the driver may fail to compute valid angles. Re-wear the exoskeleton and check whether each finger-link cable can be properly tightened.
Q7: After exoskeleton calibration, the palm is open but PIP joints are not close to 0
This usually means the spring cannot retract normally after being stretched, leaving the PIP joint in a continuously pulled state. Check whether the spring can extend and retract normally.
Q8: During teleoperation, the exoskeleton finger direction is opposite to the dexterous hand
For example, the exoskeleton thumb abducts outward, but the dexterous hand moves inward:
Adjust the corresponding joint direction in:
vim /opt/xr/core/install/exoskeleton_hand_driver_ros/lib/exoskeleton_hand_driver_ros/config/exoskeleton_hand_retarget.confFor example, change the thumb joint 1 (abduction/adduction) direction from 2.0 to -2.0.
Ensure there are no obstacles around the hand before control.
Startup order: launch control process first, then run application after controllers are loaded.
Use interpolated motion when moving to target positions to avoid abrupt motion and motor stall from finger interference.
Controller default is inactive: you must explicitly activate it before control.
Command rate: recommended 50 Hz (
dt=20ms), while hardware loop runs at 200 Hz.Multi-system isolation: use
export ROS_DOMAIN_ID=<number>when multiple systems share one network.









