HumansXDocumentation Center 日本語Website ↗

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

ItemRequirement
Wuji Hand 21
ConnectionEthernet
NetworkThe 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

ItemRequirement
Operating systemLinux or a Mac with Apple silicon, meeting the system requirements
Wuji SDKv2026.8.31 or later
Python3.10 or later
C toolchainCMake 3.16 or later, plus the extracted SDK package
  • Python

  • C

pip install wuji-sdk
LIB="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:

FieldMeasured
header.seqA monotonically increasing device-side sequence number
header.timestamp_usFirmware timestamp in microseconds
header.frame_idr_wrist for a right hand, l_wrist for a left
num_joints20
joints length20
Each joints entrynid, 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.

NumberingValuesAppears in
Array indexContiguous 0 through 19Array APIs such as effort_limit, mit_params, and joint_command
nid1–4, 6–9, 11–14, 16–19, 21–24joint_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 pointBehaviorWhat to do
Device already in useRaises 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 networkThe Python example connects to every Hand 2 it finds, while the C example connects only to the firstSee Adapting It below. With several devices around, name a serial number yourself
Some joints offlinejoints is shorter than 20 and missing nids simply don’t appearMatch by nid rather than assuming a fixed length
Subscription backlogThe C callback receives WUJI_FRAME_KIND_LAGKeep callbacks light, since a 1 kHz stream leaves no room for blocking work
Stream ends or errorsThe C side receives WUJI_FRAME_KIND_END or WUJI_FRAME_KIND_ERROROn 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 deviceWith several on the network
PythonIterates all of hand_devices and connects to eachConnects one by one, and a single failure raises, aborting the whole run and terminating subscriptions already open
Cif (idx == n && ...) idx = i keeps only the firstConnects 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.