Subscribing With Callbacks
SDK Practice
Overview
The Wuji Glove exposes six device data streams at once: raw tactile, zoned tactile, EMF poses, finger joint angles, hand skeleton, and tactile point cloud. Add the two cross-device coordinate-transform streams tf and tf_static, and a single subscription pass gives you everything the glove produces in real time.
Callbacks fire on SDK background worker threads, so your main thread never blocks. This is the shortest path to glove data.
Matching examples: examples/python/wuji_glove/0.subscribe_callback.py and examples/c/wuji_glove/0_subscribe_callback.c.
For API and field definitions, see Hand Tracking, Tactile, and Coordinate Transforms.
Prerequisites
Hardware
| Item | Requirement |
|---|---|
| Wuji Glove | 1 |
| Connection | Ethernet |
| Network | The device uses a static IP. Configure your network adapter on the same subnet. The device used here listens on port 50001 |
This example only reads. It writes no parameters.
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_glove -B build/glove \
-DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
-DWUJI_SDK_LIB="${SDK}/lib/${LIB}"
cmake --build build/glove --target 0_subscribe_callback Before You Start
Nothing. Power the device and connect the network. Hand model calibration isn’t a prerequisite for subscribing, though it does affect how well hand_joint_angles and hand_skeleton match your hand. See Hand Model Calibration.
Data Flow
Device streams subscribe through a device handle. Global streams take no device handle and are subscribed once per process.
Frame Structures
Measured on a glove resting on a desk, unworn, sampled for 3 seconds.
Six Device Streams
| Stream | Rate | Per frame |
|---|---|---|
tactile | 132.3 Hz | data of length 744, one z-score per taxel |
tactile_zones | 132.3 Hz | palm 308, thumb 50, index 45, middle 60, ring 54, pinky 55, totaling 572 |
emf_poses | 132.3 Hz | poses of length 5, each with a pose (position plus orientation) and confidence |
hand_joint_angles | 131.0 Hz | fingers of length 5, each with angles (length 5, radians) and confidence |
hand_skeleton | 131.0 Hz | joints of length 21, each with name, pose, and confidence. header.frame_id is r_wrist |
tactile_point_cloud | 130.3 Hz | 526 points, point_stride of 16 bytes, field layout x(0) y(4) z(8) pressure(12) |
Two Global Streams
| Stream | Rate | Per frame |
|---|---|---|
tf | 871.7 Hz | 1 transform, waist → r_wrist |
tf_static | 1.0 Hz | 2 transforms, r_wrist → r_hand_emf_tx and r_wrist → r_palm_imu_link |
tf runs an order of magnitude faster than the device streams because several upstream sources drive it. Heavy work inside a callback shows up here first.
Why 744, 572, and 526 Add Up
-1.0 marks a taxel as invalid or masked. It isn’t a pressure reading. On this glove, 218 of the 744 channels report -1.0:
| Array | Channels | Invalid | Valid |
|---|---|---|---|
tactile.data | 744 | 218 | 526 |
tactile_zones, six regions combined | 572 | 46 | 526 |
tactile_point_cloud points | — | — | 526 |
All three converge on 526. tactile_zones includes only channels that map to a hand region, and the 172 unzoned channels happen to be invalid. tactile_point_cloud drops invalid channels entirely and emits only points that have coordinates.
Use the point cloud when you need to know where the force lands, zones when you need to know which finger, and tactile when you need raw channel alignment.
Skeleton Joint Names
The 21 joints in hand_skeleton follow the MediaPipe hand topology in a fixed order:
wrist
thumb_cmc thumb_mcp thumb_ip thumb_tip
index_finger_mcp index_finger_pip index_finger_dip index_finger_tip
middle_finger_mcp middle_finger_pip middle_finger_dip middle_finger_tip
ring_finger_mcp ring_finger_pip ring_finger_dip ring_finger_tip
pinky_mcp pinky_pip pinky_dip pinky_tip joints[0] is wrist, and its pose is always the origin, because the whole skeleton chain uses the wrist as its reference frame. For where the wrist sits in the world, read the waist → r_wrist transform on the tf stream. The example printing wrist=[+0.000, +0.000, +0.000] is expected, not a data fault.
The Fifth Joint Angle
hand_joint_angles.fingers[i].angles always has length 5, but only the thumb uses all of it. Measured at rest:
| Finger | angles | confidence |
|---|---|---|
| Thumb | 0.785, -0.611, -0.028, -0.262, 1.658 | 0.47 |
| Index | -0.102, -0.104, 0.065, 0.369, 0.000 | 0.41 |
| Middle | 0.030, -0.033, 0.048, 0.267, 0.000 | 0.75 |
| Ring | -0.036, 0.167, 0.009, 0.553, 0.000 | 0.74 |
| Pinky | -0.175, 0.235, 0.009, 0.236, 0.000 | 0.47 |
For every finger but the thumb the fifth value stays 0 as alignment padding. confidence reflects solver quality and drifts between 0.4 and 0.75 at rest on an uncalibrated glove.
Integration Steps
These excerpts show the integration order. See the examples for argument parsing, signal handling, and full error handling.
1. Connect to the Target Glove
Replace serial_number with the target glove’s serial number. Reuse the connected device handle in later steps. C excerpts require wuji_sdk.h.
Python
C
from wuji_sdk import SdkManager
manager = SdkManager.instance()
glove = manager.connect(sn=serial_number, device_name="glove_0")WujiInitOptions init = { .log_level = 3 };
if (wuji_init(&init) != WUJI_STATUS_OK) return 1;
WujiConnectTarget target = {
.kind = WUJI_CONNECT_TARGET_KIND_SN, .value = serial_number,
};
struct WujiDevice *dev = NULL;
if (wuji_connect(&target, "glove_0", NULL, &dev) != WUJI_STATUS_OK) {
wuji_shutdown();
return 1;
} 2. Define a Frame Handler
This tactile example reads only the current frame and filters invalid channels represented by negative values. In C, check the frame kind first. The frame pointer is valid only during the callback.
Python
C
def on_tactile(frame):
valid = [v for v in frame.data if v >= 0.0]
print(frame.header.seq, max(valid, default=0.0))static void on_tactile(WujiFrameKind kind, const WujiTactileFrame *f, void *ud) {
if (kind != WUJI_FRAME_KIND_OK || !f) return;
float peak = 0.0f;
for (size_t i = 0; i < f->data_len; i++) {
if (f->data[i] >= 0.0f && f->data[i] > peak) peak = f->data[i];
}
printf("seq=%u peak=%.3f\n", f->header.seq, peak);
} 3. Register the Callback and Keep the Subscription Handle
The SDK calls the frame handler on a background thread. Keep one handle per stream, and close each handle on exit. Subscribe to global streams such as tf through the manager or global C API, once per process.
Python
C
sub = glove.tactile().subscribe_with_callback(on_tactile)struct WujiSub *sub = NULL;
WujiStatus st = wuji_glove_subscribe_tactile(dev, on_tactile, NULL, &sub);
/* On failure, report wuji_last_error(), clean up, and exit. */ 4. Close the Subscription and Disconnect
Keep the main thread running to receive data. On exit or an exception, close the subscription before disconnecting the device so callbacks don’t access released resources.
Python
C
import time
try:
time.sleep(10)
finally:
sub.close()
manager.disconnect_all()wuji_sub_close(sub);
wuji_dev_disconnect(dev);
wuji_dev_release(dev);
wuji_shutdown(); Running It
Python
C
python examples/python/wuji_glove/0.subscribe_callback.py --sn WG1KA00XXXXXXXXX./build/glove/0_subscribe_callback On the Python side, all six streams interleave without throttling:
Found 1 device(s)
SN=WG1KA00XXXXXXXXX, Type=DeviceType.WujiGlove, Address=192.168.1.101:50001
Connected: WG1KA00XXXXXXXXX (FW=0.11.2, right)
Subscribed to 6 streams. Ctrl+C to stop.
[glove_0][Tactile] max=0.3
[glove_0][Zones] palm=-0.1 thumb=-0.1
[glove_0][EmfPoses] thumb=[+0.067, +0.000, -0.059]
[glove_0][JointAngles] conf=['0.47', '0.41', '0.74', '0.71', '0.47']
[glove_0][Skeleton] wrist=[+0.000, +0.000, +0.000] joints=21
[glove_0][PointCloud] points=526 frame=r_wrist Five seconds produced 3201 lines, about 640 per second, consistent with six streams at roughly 130 Hz each.
The C side throttles printing to every 30th frame and adds the two global streams:
Wuji SDK version: 2026.8.31
Found 6 device(s)
SN=WG1KA00XXXXXXXXX Type=WujiGlove Address=192.168.1.101:50001
...
Connected: WG1KA00XXXXXXXXX as glove_0
Subscribed to 8 streams. Ctrl+C to stop.
[glove_0][Tactile] seq=343375 taxels=744 max=0.308 (#1)
[glove_0][Zones] seq=343375 palm=-1.000 (#1)
[glove_0][EmfPoses] seq=343375 pos=[+0.067,+0.000,-0.059] (#1)
[glove_0][JointAngles] seq=343378 conf=[0.47,0.41,0.74,0.71,0.47] (#1)
[glove_0][Skeleton] seq=343378 wrist=[+0.000,+0.000,+0.000] joints=21 (#1)
[glove_0][PointCloud] seq=343379 points=526 frame=r_wrist (#1)
[global][tf] count=1 first=waist->r_wrist xyz=[+0.100,-0.150,+0.050] (#1)
[global][tf_static] count=2 first=r_wrist->r_hand_emf_tx xyz=[+0.002,-0.048,-0.055] (#1) This sample output shows a right-hand Wuji Glove resting on a desk, unworn, using its default IP address, 192.168.1.101. Use the address reported by the scan.
Notes
| Branch point | Behavior | What to do |
|---|---|---|
| Device already in use | Raises Session already exists on device | Wait for the previous session’s heartbeat to time out, or confirm no other client holds the device |
| Several gloves on the network | Both examples iterate every glove and connect to each | Pass --sn, described under Adapting It |
| Non-glove devices on the network | Filtered out by device_type | Already handled. Hand 2 and unknown devices are excluded |
| Subscription backlog | The C callback receives WUJI_FRAME_KIND_LAG | Keep callbacks light |
| 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 |
--hand-model-path without a named user | The setting is rejected | A custom hand model needs an SDK user first. See SDK User Management |
C callbacks fire on dedicated worker threads, and the frame pointer is valid only for the duration of the callback. Heap arrays such as data, joints, and poses are freed as soon as it returns, so never retain the pointer.
Adapting It
The Example’s avg() Goes Negative on Invalid Channels
This is the one thing worth watching in this example. The Python example averages each zone with avg():
def avg(lst: list[float]) -> float:
return sum(lst) / len(lst) if lst else 0.0 -1.0 lands straight in the numerator. With 20 of the palm’s 308 channels invalid, the output reads palm=-0.1 — a negative reading from a glove under no load at all. The C example prints palm[0], so when that first channel happens to be invalid it shows -1.000, which is exactly what the measurement produced.
Filter negatives first on both sides:
def avg(lst: list[float]) -> float:
valid = [v for v in lst if v >= 0.0]
return sum(valid) / len(valid) if valid else 0.0 After filtering, the same frame gives a palm mean of +0.015. The share of invalid channels varies by glove, so don’t hard-code a count.
Name an SN When Several Devices Share a Network
Both examples iterate every glove they find and connect to each one. On a shared network, limit that with --sn:
python examples/python/wuji_glove/0.subscribe_callback.py --sn WG1KA00XXXXXXXXX The C example takes no command-line arguments, so compare list[i].serial_number in the source before connecting.
Subscribe Only to What You Need
All six streams open means about 640 callbacks per second at rest. For tactile analysis keep tactile or tactile_point_cloud, for hand pose keep hand_skeleton, and callback pressure drops immediately.
Keep Callbacks Light
Each subscription gets its own worker thread. Callbacks within one subscription run serially and in order, while different subscriptions may run concurrently, so guard any state you share across them. Move file writes, algorithms, and lock waits to a consumer thread, and copy only the minimum inside the callback.
Switch to Async
If callbacks don’t suit you, use subscribe() with recv() and control the pacing yourself. See Subscribing Asynchronously.
Related
- Subscribing Asynchronously: the async/await form of the same streams
- Recording Data: write these streams to a replayable file
- Adjusting Stream Rate: bring 130 Hz down to what your application needs
- Data Subscription: subscription model and lifecycle
- Common Data Types:
FrameHeader,TactileFrame,HandSkeleton, and related structures
Documentation