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

# 数据订阅：回调

## 概述

本篇实践介绍如何订阅 Wuji Hand 2 的 `joint_states` 实时状态流，并在回调中读取在线关节的位置（`position`）、速度（`velocity`）和力矩代理值（`effort`）。应用场景包括实时监控关节状态、记录和分析运动数据，以及为上层控制器提供位置、速度和力矩反馈。

对应的 Python 示例为 [0.subscribe\_callback.py](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_hand_2/0.subscribe_callback.py)，C 示例为 [0\_subscribe\_joint\_states.c](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_hand_2/0_subscribe_joint_states.c)。字段定义见 [joint\_states 字段定义](http://localhost:3000/docs/wuji-hand/latest/sdk-reference/joint-control#joint_statessub)。

## 前置条件

### 硬件

| 项目 | 要求 |
| - | - |
| Wuji Hand 2 | 1 只 |
| 连接方式 | 以太网 |
| 网络 | 设备使用静态 IP，电脑网卡配置到同一网段。固件 v2.7.0 起在 50001 端口接受 SDK 连接，更早固件用 7447 端口 |

本示例只读取，不使能电机，手指不会动。

### 软件

| 项目 | 要求 |
| - | - |
| 操作系统 | Ubuntu 22.04 LTS 及以上版本 |
| Wuji SDK | v2026.8.31 或更高版本 |
| Python | 3.10 及以上版本 |
| C 工具链 | CMake 3.16 及以上版本，以及解压后的 SDK 包 |

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

### 前序步骤

无。设备上电、网络连通即可。

## 数据流

```mermaid theme={null}
flowchart TD
    FW["Wuji Hand 2 固件<br/>按周期广播关节状态"]
    SCAN["scan()<br/>发现网络上的设备"]
    CONN["connect(sn=...)<br/>建立会话"]
    SUB["joint_states().subscribe_with_callback()<br/>注册回调"]
    WORKER["SDK 工作线程<br/>非主线程"]
    CB["回调函数<br/>逐帧触发"]

    FW -->|"约 1 kHz 广播"| SCAN
    SCAN -->|"SN 与地址"| CONN
    CONN -->|"设备句柄"| SUB
    SUB --> WORKER
    WORKER -->|"JointStateFrame"| CB
```

每帧是一个自描述的变长聚合体：`header` 带 `seq`、`timestamp_us` 与 `frame_id`，`joints` 只包含固件本周期上报的关节。

### 帧结构实测

在 20 关节全在线的设备上，单帧实测如下：

| 字段 | 实测值 |
| - | - |
| `header.seq` | 连续递增的设备端序号 |
| `header.timestamp_us` | 固件时间戳，微秒 |
| `header.frame_id` | 右手为 `r_wrist`，左手为 `l_wrist` |
| `num_joints` | 20 |
| `joints` 长度 | 20 |
| 每条 `joints` 条目 | `nid`、`position`（弧度）、`velocity`（弧度每秒）、`effort`（力矩代理值） |

**`nid` 不连续，而且与数组索引是两套编号。** 实测 20 个在线关节的 nid 依次是 `1 2 3 4`、`6 7 8 9`、`11 12 13 14`、`16 17 18 19`、`21 22 23 24`，跳过 5、10、15、20。每根手指占用 5 个 nid 号段但只用前 4 个。

| 编号 | 取值 | 出现在 |
| - | - | - |
| 数组索引 | 连续的 0 到 19 | `effort_limit`、`mit_params`、`joint_command` 等数组类接口 |
| `nid` | 1–4、6–9、11–14、16–19、21–24 | `joint_states`、`joint_diagnostics` |

两者的换算关系实测吻合全部 20 个关节：

```text theme={null}
nid = (索引 // 4) * 5 + (索引 % 4) + 1
```

索引 0 是 `thumb_S1`（nid 1），索引 4 是 `index_S1`（nid 6），索引 19 是 `pinky_S4`（nid 24）。**拿 `nid` 直接当数组下标会整体错位**，关节标签与完整对照见[设备信息](/docs/wuji-hand/latest/sdk-practice/device-info)。

## 运行时处理

| 分支点 | 表现 | 处理 |
| - | - | - |
| 设备被占用 | 抛 `Session already exists on device (another session is active)` | 等上一个会话的心跳超时后重试，或确认没有别的客户端在连同一台设备 |
| 多台设备在网 | **Python 示例连接扫到的每一台 Hand 2，C 示例只连第一台** | 见下方「调整与适配」，多设备环境必须指定 SN |
| 部分关节离线 | `joints` 长度小于 20，缺失的 nid 不出现 | 用 `nid` 匹配，不要假设长度固定 |
| 订阅积压 | C 侧回调收到 `WUJI_FRAME_KIND_LAG` | 回调内避免执行阻塞操作，约 1 kHz 的流容不下阻塞操作 |
| 流结束或出错 | C 侧收到 `WUJI_FRAME_KIND_END` 或 `WUJI_FRAME_KIND_ERROR` | `ERROR` 时用 `wuji_last_error()` 取原因 |

C 侧回调在专用工作线程上触发，`frame` 指针**只在回调期间有效**，其堆上的 `joints` 数组在回调返回后立即释放，不能留存指针。

## 集成步骤

<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. 发现设备，按类型过滤
    devices = [d for d in manager.scan() if d.device_type == DeviceType.WujiHand2]

    # 2. 连接。多设备环境下指定 sn，不要遍历全部
    hand = manager.connect(sn=devices[0].sn, device_name="hand_0")

    # 3. 注册回调，回调在后台线程触发
    sub = hand.joint_states().subscribe_with_callback(
        partial(on_joint_states, hand.device_name)
    )

    time.sleep(10)

    # 4. 先关订阅，再断设备
    sub.close()
    manager.disconnect_all()
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    #include "wuji_sdk.h"

    /* 回调在 SDK 工作线程上触发，不是 main。f 只在回调期间有效 */
    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. 初始化 */
    WujiInitOptions opts = { .log_level = 3 };
    wuji_init(&opts);

    /* 2. 发现并连接 */
    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. 订阅 */
    struct WujiSub *sub = NULL;
    wuji_hand_2_subscribe_joint_states(dev, on_joint_state, &ctx, &sub);

    /* 4. 先关订阅（停掉工作线程），再释放设备 */
    wuji_sub_close(sub);
    wuji_dev_disconnect(dev);
    wuji_dev_release(dev);
    wuji_shutdown();
    ```
  </Tab>
</Tabs>

关订阅必须早于断设备。反过来做，工作线程可能仍在访问已释放的设备。

## 运行与输出

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

Python 侧每帧一行，默认只打印拇指的 4 个关节：

```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']
```

C 侧把整帧铺开成表，并把打印限流到约 1 Hz，避免刷屏：

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

以上为实机运行结果，设备是 20 关节全在线的右手 Wuji Hand 2。静置不动时 `position` 稳定，`velocity` 在零附近抖动。

**输出率约 1 kHz。** 实测 5 秒收到 5555 帧，按设备端 `seq` 差计算为每秒约 1000 帧。回调中执行阻塞操作会导致积压。

## 配置与适配

### 多设备环境必须指定设备

这是本示例最容易踩的坑，两侧行为还不一样：

| | 选设备的方式 | 多台在网时 |
| - | - | - |
| Python | 遍历 `hand_devices` 全部连接 | **逐台连接**，任一台失败会抛异常中断整个流程，已连设备的订阅一并终止 |
| C | `if (idx == n && ...) idx = i` 只记第一个 | **只连扫到的第一台**，扫描顺序变化时连到的设备也会变 |

Python 侧的示例没有 `--sn` 参数（Glove 的同类示例有）。共用网络里跑之前，把连接那段改成指定 SN：

```python theme={null}
hand = manager.connect(sn="WH2KA00XXXXXXXXX", device_name="hand_0")
```

实测过一次反例：网络上有三台 Hand 2，其中一台被别的客户端占用，示例连到它时抛 `Session already exists`，此前已连的设备订阅随之终止，整轮只收到 1 帧数据。

### 订阅更多数据流

`joint_states` 之外，设备还提供 `joint_diagnostics`、`imu`、指尖触觉等。订阅方式相同，换成对应的资源方法即可。指尖触觉的解码方式见 [Wuji Hand 2 指尖触觉](/docs/wuji-sdk/sdk-practice/hand2-fingertip/)。

### 降低输出率

约 1 kHz 对多数应用偏高。用订阅句柄的 `set_rate()` 降频，见[输出率调节](/docs/wuji-hand/latest/sdk-practice/stream-rate)。

### 避免在回调中执行阻塞操作

回调在 SDK 工作线程上按帧触发。写文件、跑算法、加锁等待都会挤占这条线程，导致积压。正确做法是回调里只做最小拷贝，把数据丢进队列，另起线程消费。

## 相关内容

* [数据订阅：异步](/docs/wuji-hand/latest/sdk-practice/subscribe-async)：同一条流的 async/await 写法
* [输出率调节](/docs/wuji-hand/latest/sdk-practice/stream-rate)：把约 1 kHz 降到应用需要的频率
* [数据订阅机制](/docs/wuji-sdk/data-subscription/)：订阅模型与生命周期
* [通用数据类型](/docs/wuji-sdk/data-reference/)：`FrameHeader` 与关节状态相关结构


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