> ## 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

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 joint state and field definitions, see [SDK Reference](/en/docs/wuji-hand/latest/sdk-reference). This page covers only how to run it and what the measurements show.

## 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 | 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_hand_2 -B build/hand2 \
      -DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
      -DWUJI_SDK_LIB="${SDK}/lib/libwuji_sdk_c.so"
    cmake --build build/hand2 --target 0_subscribe_joint_states
    ```
  </Tab>
</Tabs>

### Before You Start

Nothing. Power the device and connect the network.

## Data Flow

```mermaid theme={null}
flowchart TD
    FW["Wuji Hand 2 firmware<br/>broadcasts joint state periodically"]
    SCAN["scan()<br/>find devices on the network"]
    CONN["connect(sn=...)<br/>open a session"]
    SUB["joint_states().subscribe_with_callback()<br/>register a callback"]
    WORKER["SDK worker thread<br/>not the main thread"]
    CB["callback<br/>fires per frame"]

    FW -->|"about 1 kHz"| SCAN
    SCAN -->|"serial number and address"| CONN
    CONN -->|"device handle"| SUB
    SUB --> WORKER
    WORKER -->|"JointStateFrame"| CB
```

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:

```text theme={null}
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](/en/docs/wuji-hand/latest/sdk-practice/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

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    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()
    ```
  </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_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();
    ```
  </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_hand_2/0.subscribe_callback.py
    ```
  </Tab>

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

The Python side prints one line per frame, showing only the thumb's 4 joints by default:

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

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

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

Beyond `joint_states`, the device offers `joint_diagnostics`, `imu`, and fingertip tactile. Subscribing works the same way with the matching resource method. For decoding fingertip tactile, see [Wuji Hand 2 Fingertip Tactile](/en/docs/wuji-sdk/sdk-practice/hand2-fingertip/).

### Lower the Rate

About 1 kHz is higher than most applications need. Use `set_rate()` on the subscription handle, described in [Adjusting Stream Rate](/en/docs/wuji-hand/latest/sdk-practice/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](/en/docs/wuji-hand/latest/sdk-practice/subscribe-async): the async/await form of the same stream
* [Adjusting Stream Rate](/en/docs/wuji-hand/latest/sdk-practice/stream-rate): bring roughly 1 kHz 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` and joint-state structures


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