Replaying a Thumb Opposition
SDK Practice
Overview
Read the recording shipped with the example, pick the left- or right-hand file by device handedness, and replay 30000 joint-angle frames straight through at 1 kHz.
This is the larger of the two motion examples, meant for observing the hand’s full range. The example does no interpolation, no resampling, no smoothing, and adds no recovery motion. What the recording holds is what goes out.
Matching examples: examples/python/wuji_hand_2/6_opposition/opposition.py and examples/c/wuji_hand_2/6_opposition/opposition.c.
For API and field definitions, see JointCommand.
This example drives all 20 joints through large motions, with a measured command peak of 1.43 rad (about 82 degrees) sustained over 30 seconds. Before starting, confirm the hand is firmly mounted, clear people, cables, and desk objects from around it, keep someone watching throughout, and stay ready to cut power.
Prerequisites
Hardware
| Item | Requirement |
|---|---|
| Wuji Hand 2 | 1, with all 20 joints online |
| Mounting | The device must be firmly mounted. 30 seconds of continuous large motion produces noticeable reaction forces |
| Connection | Ethernet |
| Network | Only this device may be on the network, since the example uses auto_connect() |
Software
| Item | Requirement |
|---|---|
| Operating system | Linux or a Mac with Apple silicon, meeting the system requirements |
| Wuji SDK | v2026.8.31 or later |
| Python | 3.10 or later |
| C toolchain | CMake 3.16 or later. This example is a standalone CMake project |
Replay Files
The example directory ships data/right.replay and data/left.replay. Copy the entire 6_opposition/ directory, since taking only the source file fails when it can’t find the data.
Data Flow
File Format
A fixed-length container with a 16-byte header followed by 84 bytes per frame.
| Section | Layout | Note |
|---|---|---|
| Header | <8sHBBI | magic WJH2RPL\0, version, handedness ID, joint count, frame count |
| Per frame | <I20f | 4-byte frame index plus 20 float32 joint angles in radians |
Handedness ID is 1 for right and 2 for left. The shipped files measure 30000 frames and 2520016 bytes, matching 16 + 30000 x 84 exactly.
Validation completes before enabling. If any of magic, version, handedness, joint count, or file length fails, the example reports an error and exits without enabling the motors.
Integration Steps
1. Connect and Validate the Replay File
Read handedness after connecting, and select left.replay or right.replay. After opening the file, validate the magic, version, handedness marker, joint count, and file length. Don’t enable the device if validation fails.
hand = manager.auto_connect("hand_2_opposition")
if hand.online_joints_count().get() != 20:
raise RuntimeError("expected 20 online joints")
stream, frame_count = _open_replay(_side_for_hand(hand), data_dir=data_dir) 2. Check Online Joints and Prepare the Publisher
Confirm all 20 joints are online, then call enable() and create a joint_command publisher. Read one current joint_states frame and use a cosine transition to reach the first replay frame smoothly.
hand.enable()
publisher = hand.joint_command().publish()
start = _capture_start_positions(hand) C uses wuji_hand_2_online_joints_count(), wuji_hand_2_enable(), and wuji_hand_2_joint_command_publish(). It selects the replay file with wuji_hand_2_get_handedness().
3. Publish Replay Frames at 1 kHz
Read 20 position values per frame, create JointCommand(position, 0, 0) entries, and send them at 1 ms intervals. The replay loop must handle stop requests and file-read errors.
def send(qpos):
publisher.send([wuji_sdk.JointCommand(float(v), 0.0, 0.0) for v in qpos])
_play_replay(stream, frame_count, send, ramp_start=start,
stop_requested=lambda: _STOP_REQUESTED) The functions with leading underscores are helpers in Python example. _open_replay() validates the complete container, _capture_start_positions() orders joints by nid, and _play_replay() handles the starting transition and frame-by-frame playback.
C uses open_replay_path(), capture_start_positions(), send_ramp(), and play_replay(). They send a fixed 20-joint array through wuji_joint_command_publisher_send().
4. Disable and Release Resources
On normal completion or an error, disable first, close the publisher and replay file, then disconnect. C uses wuji_hand_2_disable(device, NULL) and wuji_joint_command_publisher_close(publisher), closes the replay file, disconnects and releases the device, and calls wuji_shutdown().
hand.disable()
publisher.close()
stream.close()
manager.disconnect_all() Place cleanup in finally. Collect errors individually, as shown in Python example and C example, so a failure in one cleanup operation doesn’t prevent releasing the remaining resources.
Running It
Python
C
python examples/python/wuji_hand_2/6_opposition/opposition.py./build/hand2-opposition/6_opposition Measurements
A complete run on a right-hand Wuji Hand 2 with all 20 joints online:
| Metric | Measured |
|---|---|
| Frames replayed | 30000, none dropped |
| Total duration | 30.00 seconds |
| Actual publish rate | 1000.0 Hz |
| Command peak | 1.4315 rad (82.0 degrees) |
| Largest absolute starting angle | 0.0668 rad |
| Largest absolute ending angle | 0.0563 rad |
Python held the 1 ms target interval reliably, and accumulated error across 30000 frames is negligible.
The hand settles on its own at the end. The recording’s last frame already returns to an open position, so a normal run needs no extra homing. That comes from the recording’s content rather than anything the example does, and an interrupted run gets no such guarantee.
Range of Motion per Finger
Per-joint travel computed offline from the right-hand replay file:
| Finger | Travel across four joints (rad) | Peak |
|---|---|---|
| Thumb | 1.125 / 0.734 / 1.296 / 0.801 | +1.319 |
| Index | 0.799 / 0.477 / 1.428 / 1.257 | +1.431 |
| Middle | 0.787 / 0.620 / 1.205 / 0.298 | +1.197 |
| Ring | 0.930 / 0.434 / 0.795 / 0.104 | +0.925 |
| Pinky | 0.630 / 0.524 / 1.188 / 0.322 | +1.184 |
The index finger’s third joint travels furthest at 1.428 rad, about 81.8 degrees.
Notes
| Branch point | Behavior | What to do |
|---|---|---|
| Handedness mismatch | side_id validation fails when a right hand reads left.replay | Reports an error and exits without enabling. The example picks by device handedness, so this rarely triggers |
| File missing or corrupt | Reports replay file not found or a container validation failure | Confirm you copied the whole 6_opposition/ directory |
| Fewer than 20 joints online | Reports an error and exits | Investigate the offline joints first |
| Several devices on the network | auto_connect() reports Found N devices, please specify SN and exits | On a shared network, change it to name a serial number |
Ctrl+C | Exit code 130, stopping further frames and performing no return motion | The hand holds the posture it stopped in, then is disabled |
| Normal completion | Exit code 0 | The recording’s final frame already returns to an open hand |
On interrupt the hand holds its current posture rather than opening automatically. Stopping mid-motion can leave it clenched, which needs handling by hand.
Adapting It
Name an SN on a Shared Network
As with the MIT joint sweep, auto_connect() refuses to connect when it finds several devices and lists every serial number rather than guessing. Change it to manager.connect(sn="WH2KA00XXXXXXXXX", device_name="hand_2_opposition").
Replay Your Own Recording
Generate a file in the same container format and the whole replay path works unchanged: a 16-byte header plus 84 bytes per frame. The frame index is skipped during replay, and only the 20 joint angles drive control. Make sure side_id matches the target device’s handedness, or container validation fails.
Changing Playback Speed
The interval is hard-coded at 1 ms. Increasing it slows the whole motion, but since nothing is resampled the joint-angle sequence stays the same and each posture simply holds longer. Real speed changes need you to resample the trajectory yourself.
Recovering After an Interrupt
Ctrl+C triggers no return motion. After stopping mid-run, the hand holds its posture, and the smooth homing routine in Publishing Control Commands brings it gently back to zero. Don’t send pos=0 directly, since a large error under MIT impedance snaps the hand back hard.
Related
- Publishing Control Commands: general
joint_commandusage and smooth homing - Running an MIT Joint Sweep: the smallest command-path check
- Subscribing With Callbacks: read actual joint state during replay
- Common Data Types: the
JointCommandstructure
Documentation