Subscribing With Callbacks
SDK Practice
Overview
Subscribe to the Wuji Hand 2 joint_states broadcast to read position, velocity, and an effort proxy for every online joint. Callbacks fire on SDK background worker threads, so your main thread never blocks.
This is the shortest path to live state from the hand. State monitoring, data logging, and feeding a higher-level controller all start here.
Matching examples: examples/python/wuji_hand_2/0.subscribe_callback.py and examples/c/wuji_hand_2/0_subscribe_joint_states.c.
For API and field definitions, see joint_states Field Definitions.
Prerequisites
Hardware
| Item | Requirement |
|---|---|
| Wuji Hand 2 | 1 |
| Connection | Ethernet |
| Network | The device uses a static IP. Configure your network adapter on the same subnet. Firmware v2.7.0 and later accepts SDK connections on port 50001, while earlier firmware uses port 7447 |
This example only reads. It doesn’t enable the motors, and the fingers don’t move.
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, plus the extracted SDK package |
Python
C
pip install wuji-sdkLIB="libwuji_sdk_c.so"
if [ "$(uname -s)" = "Darwin" ]; then LIB="libwuji_sdk_c.dylib"; fi
cmake -S examples/c/wuji_hand_2 -B build/hand2 \
-DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
-DWUJI_SDK_LIB="${SDK}/lib/${LIB}"
cmake --build build/hand2 --target 0_subscribe_joint_states Before You Start
Nothing. Power the device and connect the network.
Data Flow
Each frame is a self-describing variable-length aggregate: header carries seq, timestamp_us, and frame_id, while joints holds only the joints the firmware reported this cycle.
Frame Structure
On a device with all 20 joints online, a single frame measures as follows:
| Field | Measured |
|---|---|
header.seq | A monotonically increasing device-side sequence number |
header.timestamp_us | Firmware timestamp in microseconds |
header.frame_id | r_wrist for a right hand, l_wrist for a left |
num_joints | 20 |
joints length | 20 |
Each joints entry | nid, position in radians, velocity in radians per second, effort as a torque proxy |
nid isn’t contiguous, and it’s a different numbering from the array index. The 20 online joints report nids 1 2 3 4, 6 7 8 9, 11 12 13 14, 16 17 18 19, 21 22 23 24, skipping 5, 10, 15, and 20. Each finger occupies a block of 5 nids but uses only the first 4.
| Numbering | Values | Appears in |
|---|---|---|
| Array index | Contiguous 0 through 19 | Array APIs such as effort_limit, mit_params, and joint_command |
nid | 1–4, 6–9, 11–14, 16–19, 21–24 | joint_states and joint_diagnostics |
The conversion holds across all 20 joints:
nid = (index // 4) * 5 + (index % 4) + 1 Index 0 is thumb_S1 (nid 1), index 4 is index_S1 (nid 6), and index 19 is pinky_S4 (nid 24). Using nid directly as an array subscript shifts everything, and the full joint label mapping lives in Reading Device Info.
Runtime Handling
| Branch point | Behavior | What to do |
|---|---|---|
| Device already in use | Raises Session already exists on device (another session is active) | Wait for the previous session’s heartbeat to time out, or confirm no other client holds the device |
| Several devices on the network | The Python example connects to every Hand 2 it finds, while the C example connects only to the first | See Adapting It below. With several devices around, name a serial number yourself |
| Some joints offline | joints is shorter than 20 and missing nids simply don’t appear | Match by nid rather than assuming a fixed length |
| Subscription backlog | The C callback receives WUJI_FRAME_KIND_LAG | Keep callbacks light, since a 1 kHz stream leaves no room for blocking work |
| Stream ends or errors | The C side receives WUJI_FRAME_KIND_END or WUJI_FRAME_KIND_ERROR | On ERROR, call wuji_last_error() for the reason |
C callbacks fire on dedicated worker threads, and the frame pointer is valid only for the duration of the callback. Its heap joints array is freed as soon as it returns, so never retain the pointer.
Integration Steps
Python
C
import time
from functools import partial
from wuji_sdk import SdkManager, DeviceType, JointStateFrame
def on_joint_states(device_name: str, frame: JointStateFrame):
h = frame.header
thumb = frame.joints[:4]
positions = [f"{j.position:+.3f}" for j in thumb]
print(f"[{device_name}][JointState] seq={h.seq} t={h.timestamp_us}us "
f"frame={h.frame_id!r} thumb_pos={positions}")
manager = SdkManager.instance()
# 1. Discover devices and filter by type
devices = [d for d in manager.scan() if d.device_type == DeviceType.WujiHand2]
# 2. Connect. With several devices around, name an sn instead of iterating
hand = manager.connect(sn=devices[0].sn, device_name="hand_0")
# 3. Register a callback. It fires on a background thread
sub = hand.joint_states().subscribe_with_callback(
partial(on_joint_states, hand.device_name)
)
time.sleep(10)
# 4. Close the subscription first, then disconnect
sub.close()
manager.disconnect_all()#include "wuji_sdk.h"
/* Callbacks fire on an SDK worker thread, not main.
f is valid only for the duration of the callback */
static void on_joint_state(WujiFrameKind kind, const WujiJointStateFrame *f, void *ud) {
if (kind == WUJI_FRAME_KIND_LAG) { fprintf(stderr, "lagged\n"); return; }
if (kind == WUJI_FRAME_KIND_END) { fprintf(stderr, "stream ended\n"); return; }
if (kind == WUJI_FRAME_KIND_ERROR) { fprintf(stderr, "error: %s\n", wuji_last_error()); return; }
if (kind != WUJI_FRAME_KIND_OK || !f) return;
printf("seq=%u frame_id=%s num_joints=%u\n",
f->header.seq, f->header.frame_id, f->num_joints);
for (size_t i = 0; i < f->joints_len; i++) {
const WujiJointStateEntry *e = &f->joints[i];
printf(" nid=%u pos=%.4f vel=%.4f eff=%.4f\n",
e->nid, e->position, e->velocity, e->effort);
}
}
/* 1. Initialize */
WujiInitOptions opts = { .log_level = 3 };
wuji_init(&opts);
/* 2. Discover and connect */
WujiDiscovered *list = NULL; size_t n = 0;
wuji_scan(&list, &n);
WujiConnectTarget tgt = { .kind = WUJI_CONNECT_TARGET_KIND_SN, .value = list[idx].serial_number };
WujiConnectOptions copts = wuji_connect_options_default();
struct WujiDevice *dev = NULL;
wuji_connect(&tgt, "hand2", &copts, &dev);
wuji_discovered_free(list, n);
/* 3. Subscribe */
struct WujiSub *sub = NULL;
wuji_hand_2_subscribe_joint_states(dev, on_joint_state, &ctx, &sub);
/* 4. Close the subscription to stop the worker thread, then release */
wuji_sub_close(sub);
wuji_dev_disconnect(dev);
wuji_dev_release(dev);
wuji_shutdown(); Always close subscriptions before disconnecting. The other way around, a worker thread may still touch a device you’ve already released.
Running It
Python
C
python examples/python/wuji_hand_2/0.subscribe_callback.py./build/hand2/0_subscribe_joint_states The Python side prints one line per frame, showing only the thumb’s 4 joints by default:
Connected: WH2KA00XXXXXXXXX (20 joints online)
Subscribed to 1 streams. Ctrl+C to stop.
[hand_0][JointState] seq=34166 t=1789649383485956us frame='r_wrist' thumb_pos=['+0.239', '-0.037', '-0.023', '+0.006']
[hand_0][JointState] seq=34167 t=1789649383486834us frame='r_wrist' thumb_pos=['+0.239', '-0.037', '-0.023', '+0.006'] The C side lays out the whole frame as a table and throttles printing to about 1 Hz so it doesn’t flood the terminal:
Connecting to WH2KA00XXXXXXXXX (192.168.1.100:7447)
Subscribed to joint_states. Ctrl+C to stop.
seq=76845 frame_id=r_wrist ts=1789649958304782us num_joints=20 (frame #1)
nid position velocity effort
1 0.2365 0.0000 0.0043
2 -0.0322 0.0002 0.0051
3 -0.0226 0.0002 0.0022
4 0.0055 -0.0001 0.0060
6 0.0845 -0.0000 -0.0052
7 0.5018 -0.0001 0.0037
...
24 0.1562 -0.0002 -0.0019 Measured on a right-hand Wuji Hand 2 with all 20 joints online. At rest, position holds steady and velocity jitters around zero.
The rate is about 1 kHz. Five seconds received 5555 frames, working out to roughly 1000 frames per second from the device-side seq difference. Doing anything heavier than printing inside the callback causes a backlog.
Adapting It
Name a Device When Several Share a Network
This is the easiest mistake to make with this example, and the two bindings behave differently:
| How it picks a device | With several on the network | |
|---|---|---|
| Python | Iterates all of hand_devices and connects to each | Connects one by one, and a single failure raises, aborting the whole run and terminating subscriptions already open |
| C | if (idx == n && ...) idx = i keeps only the first | Connects only to the first device found, which changes as scan order changes |
The Python example takes no --sn argument, unlike its glove counterpart. Before running on a shared network, change the connect call to name a serial number:
hand = manager.connect(sn="WH2KA00XXXXXXXXX", device_name="hand_0") A counterexample came up in testing: with three Hand 2 devices on the network and one held by another client, the example raised Session already exists when it reached that device, subscriptions already open terminated with it, and the whole run received a single frame.
Subscribe to More Streams
Alongside joint_states, subscribe to joint_diagnostics and imu with the matching resource methods.
Lower the Rate
About 1 kHz is higher than most applications need. Use set_rate() on the subscription handle, described in Adjusting Stream Rate.
Keep Callbacks Light
Callbacks fire per frame on an SDK worker thread. File writes, algorithms, and lock waits all compete for that thread and cause a backlog. Copy the minimum inside the callback, push it onto a queue, and consume it on another thread.
Related
- Subscribing Asynchronously: the async/await form of the same stream
- Adjusting Stream Rate: bring roughly 1 kHz down to what your application needs
- Data Subscription: subscription model and lifecycle
- Common Data Types:
FrameHeaderand joint-state structures
Documentation