> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wuji.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Subscribing With Callbacks

## 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 field definitions of each stream, see [SDK Reference](/en/docs/wuji-glove/sdk-data-reference). This page covers only how to run it and what the measurements show.

## 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 | Ubuntu 22.04 LTS or later |
| Wuji SDK | v2026.8.31 or later |
| Python | 3.10 or later |
| C toolchain | CMake 3.16 or later, plus the extracted SDK package |

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    pip install wuji-sdk
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    cmake -S examples/c/wuji_glove -B build/glove \
      -DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
      -DWUJI_SDK_LIB="${SDK}/lib/libwuji_sdk_c.so"
    cmake --build build/glove --target 0_subscribe_callback
    ```
  </Tab>
</Tabs>

### 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](/en/docs/wuji-glove/sdk-practice/hand-model-calibration).

## Data Flow

```mermaid theme={null}
flowchart TD
    FW["Wuji Glove firmware<br/>tactile array + EMF tracking"]
    CONN["connect(sn=...)<br/>open a session"]
    RAW["tactile<br/>744 raw z-score channels"]
    ZONE["tactile_zones<br/>regrouped by hand region"]
    PC["tactile_point_cloud<br/>526 valid points"]
    EMF["emf_poses<br/>5 fingertip poses"]
    ANG["hand_joint_angles<br/>5 fingers x 5 angles"]
    SKE["hand_skeleton<br/>21 skeleton joints"]
    TF["tf / tf_static<br/>global transforms"]
    CB["callback<br/>SDK worker thread"]

    FW --> CONN
    CONN --> RAW
    CONN --> ZONE
    CONN --> PC
    CONN --> EMF
    CONN --> ANG
    CONN --> SKE
    CONN --> TF
    RAW --> CB
    ZONE --> CB
    PC --> CB
    EMF --> CB
    ANG --> CB
    SKE --> CB
    TF --> CB
```

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:

```text theme={null}
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.

## Runtime Handling

| 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](/en/docs/wuji-sdk/sdk-practice/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.

## Integration Steps

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import time
    from functools import partial
    from wuji_sdk import SdkManager, DeviceType, TactileFrame, HandSkeleton

    def on_tactile(device_name: str, frame: TactileFrame):
        # -1.0 marks an invalid channel. Filter before computing statistics
        valid = [v for v in frame.data if v >= 0.0]
        print(f"[{device_name}][Tactile] max={max(valid):.3f} valid={len(valid)}/{len(frame.data)}")

    def on_skeleton(device_name: str, skeleton: HandSkeleton):
        tip = next(j for j in skeleton.joints if j.name == "index_finger_tip")
        p = tip.pose.position
        print(f"[{device_name}][Skeleton] index_tip=[{p[0]:+.3f}, {p[1]:+.3f}, {p[2]:+.3f}]")

    manager = SdkManager.instance()

    # 1. Discover devices and filter by type
    devices = [d for d in manager.scan() if d.device_type == DeviceType.WujiGlove]

    # 2. Connect. With several devices around, name an sn instead of iterating
    glove = manager.connect(sn=devices[0].sn, device_name="glove_0")
    print(f"Connected: {glove.serial_number} "
          f"(FW={glove.version().get()}, {glove.hand_side().get()})")

    # 3. Register callbacks. They fire on background threads
    subs = [
        glove.tactile().subscribe_with_callback(partial(on_tactile, glove.device_name)),
        glove.hand_skeleton().subscribe_with_callback(partial(on_skeleton, glove.device_name)),
    ]

    # 4. Global streams take no device handle. Subscribe once per process
    subs.append(manager.tf().subscribe_with_callback(
        lambda f: print(f"[tf] {f.transforms[0].parent_frame_id} -> {f.transforms[0].child_frame_id}")
    ))

    time.sleep(10)

    # 5. Close subscriptions first, then disconnect
    for sub in subs:
        sub.close()
    manager.disconnect_all()
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    #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_tactile(WujiFrameKind kind, const WujiTactileFrame *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;

        /* -1.0 marks an invalid channel. Filter before computing statistics */
        float max_val = 0.0f; size_t valid = 0;
        for (size_t i = 0; i < f->data_len; i++) {
            if (f->data[i] < 0.0f) continue;
            valid++;
            if (f->data[i] > max_val) max_val = f->data[i];
        }
        printf("seq=%u taxels=%zu valid=%zu max=%.3f\n",
               f->header.seq, f->data_len, valid, max_val);
    }

    /* 1. Initialize */
    WujiInitOptions opts = { .log_level = 3 };
    wuji_init(&opts);

    /* 2. Discover and connect, filtering gloves by device_id */
    WujiDiscovered *list = NULL; size_t n = 0;
    wuji_scan(&list, &n);
    /* list[i].device_id == WUJI_DEVICE_TYPE_WUJI_GLOVE */
    WujiConnectTarget tgt = { .kind = WUJI_CONNECT_TARGET_KIND_SN, .value = list[i].serial_number };
    struct WujiDevice *dev = NULL;
    wuji_connect(&tgt, "glove_0", NULL, &dev);
    wuji_discovered_free(list, n);

    /* 3. Subscribe to device streams, one worker thread per stream */
    struct WujiSub *sub = NULL;
    wuji_glove_subscribe_tactile(dev, on_tactile, &ctx, &sub);

    /* 4. Global streams take no device handle. Subscribe once per process */
    struct WujiSub *tf_sub = NULL;
    wuji_subscribe_tf(on_transforms, &tf_ctx, &tf_sub);

    /* 5. Close subscriptions first to stop the worker threads, then release */
    wuji_sub_close(sub);
    wuji_sub_close(tf_sub);
    wuji_dev_disconnect(dev);
    wuji_dev_release(dev);
    wuji_shutdown();
    ```
  </Tab>
</Tabs>

Always close subscriptions before disconnecting. The other way around, a worker thread may still touch a device you've already released.

## Running It

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    python examples/python/wuji_glove/0.subscribe_callback.py --sn WG1KA00XXXXXXXXX
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    ./build/glove/0_subscribe_callback
    ```
  </Tab>
</Tabs>

On the Python side, all six streams interleave without throttling:

```text theme={null}
Found 1 device(s)
  SN=WG1KA00XXXXXXXXX, Type=DeviceType.WujiGlove, Address=192.168.1.100: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:

```text theme={null}
Wuji SDK version: 2026.8.31
Found 6 device(s)
  SN=WG1KA00XXXXXXXXX  Type=WujiGlove  Address=192.168.1.100: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)
```

Measured on a right-hand Wuji Glove resting on a desk, unworn.

## 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()`:

```python theme={null}
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:

```python theme={null}
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`:

```bash theme={null}
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](/en/docs/wuji-glove/sdk-practice/subscribe-async).

## Related

* [Subscribing Asynchronously](/en/docs/wuji-glove/sdk-practice/subscribe-async): the async/await form of the same streams
* [Recording Data](/en/docs/wuji-glove/sdk-practice/data-recording): write these streams to a replayable file
* [Adjusting Stream Rate](/en/docs/wuji-glove/sdk-practice/stream-rate): bring 130 Hz down to what your application needs
* [Data Subscription](/en/docs/wuji-sdk/data-subscription/): subscription model and lifecycle
* [Common Data Types](/en/docs/wuji-sdk/data-reference/): `FrameHeader`, `TactileFrame`, `HandSkeleton`, and related structures


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.