> ## 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 Glove 的实时数据流。应用场景包括实时监控手部追踪及触觉数据、记录采集结果和向上层应用提供反馈。

对应的 Python 示例为 [0.subscribe\_callback.py](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/0.subscribe_callback.py)，C 示例为 [0\_subscribe\_callback.c](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_glove/0_subscribe_callback.c)。 字段定义见 [手部追踪](/docs/wuji-glove/sdk-data-reference/hand-tracking)、[触觉](/docs/wuji-glove/sdk-data-reference/tactile) 和 [坐标变换](/docs/wuji-glove/sdk-data-reference/coordinate-transforms)，本页说明集成流程和运行时行为。

## 前置条件

### 硬件

| 项目 | 要求 |
| - | - |
| Wuji Glove | 1 只 |
| 连接方式 | 以太网 |
| 网络 | 设备使用静态 IP，电脑网卡配置到同一网段。本文所用设备在 50001 端口 |

本示例只读取，不写任何参数。

### 软件

| 项目 | 要求 |
| - | - |
| 操作系统 | 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_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>

### 前序步骤

无。设备上电、网络连通即可。手部模型标定不是订阅的前提，但会影响 `hand_joint_angles` 与 `hand_skeleton` 的贴合度，详见[手部模型标定](/docs/wuji-glove/sdk-practice/hand-model-calibration)。

## 数据流

```mermaid theme={null}
flowchart LR
    M["SdkManager.scan()"] --> C["connect(sn=...)"]
    C --> S1["订阅设备流<br/>tactile / zones / point_cloud"]
    C --> S2["订阅跟踪流<br/>emf_poses / joint_angles / skeleton"]
    C --> S3["订阅全局流<br/>tf / tf_static"]
    S1 --> W1["SDK 工作线程回调"]
    S2 --> W1
    S3 --> W1
    W1 --> H["按帧处理数据"]
    H --> STOP["Ctrl+C"]
    STOP --> D["关闭订阅并断开设备"]
```

设备流按设备句柄订阅，全局流不带设备句柄、整个进程订阅一次。

## 帧结构实测

静置手套、无人佩戴、3 秒采样，实测如下。

### 六条设备流

| 流 | 输出率 | 每帧内容 |
| - | - | - |
| `tactile` | 132.3 Hz | `data` 长 744，每个元素是一个 taxel 的 z-score |
| `tactile_zones` | 132.3 Hz | `palm` 308、`thumb` 50、`index` 45、`middle` 60、`ring` 54、`pinky` 55，合计 572 |
| `emf_poses` | 132.3 Hz | `poses` 长 5，每条含 `pose`（position + orientation）与 `confidence` |
| `hand_joint_angles` | 131.0 Hz | `fingers` 长 5，每条含 `angles`（长 5，弧度）与 `confidence` |
| `hand_skeleton` | 131.0 Hz | `joints` 长 21，每条含 `name`、`pose`、`confidence`，`header.frame_id` 为 `r_wrist` |
| `tactile_point_cloud` | 130.3 Hz | 526 个点，`point_stride` 16 字节，字段布局 `x`(0) `y`(4) `z`(8) `pressure`(12) |

### 两条全局流

| 流 | 输出率 | 每帧内容 |
| - | - | - |
| `tf` | 871.7 Hz | 1 条变换，`waist` → `r_wrist` |
| `tf_static` | 1.0 Hz | 2 条变换，`r_wrist` → `r_hand_emf_tx`、`r_wrist` → `r_palm_imu_link` |

`tf` 的输出率比设备流高一个量级，它由多个上游源共同驱动。回调中执行阻塞操作在这条流上最先暴露。

### 通道数量关系

**`-1.0` 表示该 taxel 无效或被屏蔽，不是压力值。** 实测这台手套 744 个通道里有 218 个是 `-1.0`：

| 数组 | 通道数 | 其中无效 | 有效 |
| - | - | - | - |
| `tactile.data` | 744 | 218 | 526 |
| `tactile_zones` 六个分区合计 | 572 | 46 | 526 |
| `tactile_point_cloud` 点数 | — | — | 526 |

三者收敛到同一个 526。`tactile_zones` 只收录能映射到手部分区的通道，剩下 172 个未分区通道恰好全是无效的。`tactile_point_cloud` 则把无效通道整个剔除，只输出有坐标的有效点。

需要「哪些位置在受力」时用点云，需要「哪根手指受力」时用分区，需要原始通道对齐时用 `tactile`。

### 骨骼关节名

`hand_skeleton` 的 21 个关节按 MediaPipe 手部拓扑排列，顺序固定：

```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]` 是 `wrist`，位姿恒为原点——整条骨骼链以腕为参考系。腕本身在世界里的位置要看 `tf` 流的 `waist` → `r_wrist`。示例打印 `wrist=[+0.000, +0.000, +0.000]` 不是数据异常。

### 关节角的第 5 个分量

`hand_joint_angles.fingers[i].angles` 长度固定为 5，但只有拇指用满。实测静置状态：

| 手指 | angles | confidence |
| - | - | - |
| 拇指 | `0.785, -0.611, -0.028, -0.262, 1.658` | 0.47 |
| 食指 | `-0.102, -0.104, 0.065, 0.369, 0.000` | 0.41 |
| 中指 | `0.030, -0.033, 0.048, 0.267, 0.000` | 0.75 |
| 无名指 | `-0.036, 0.167, 0.009, 0.553, 0.000` | 0.74 |
| 小指 | `-0.175, 0.235, 0.009, 0.236, 0.000` | 0.47 |

非拇指的第 5 个分量恒为 0，是对齐用的填充位。`confidence` 反映求解质量，静置未标定时在 0.4 到 0.75 之间浮动。

## 集成步骤

以下片段按接入顺序展示关键操作，参数解析、信号处理和完整错误分支见示例。

### 1. 连接目标手套

将 `serial_number` 替换为目标手套的序列号。连接成功后，后续步骤复用同一个设备句柄。C 片段需包含 `wuji_sdk.h`。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from wuji_sdk import SdkManager
    manager = SdkManager.instance()
    glove = manager.connect(sn=serial_number, device_name="glove_0")
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    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;
    }
    ```
  </Tab>
</Tabs>

### 2. 定义帧处理函数

以触觉流为例，回调只读取本帧数据，并过滤负值表示的无效通道。C 必须先判断帧类型，帧指针只在本次回调内有效。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    def on_tactile(frame):
        valid = [v for v in frame.data if v >= 0.0]
        print(frame.header.seq, max(valid, default=0.0))
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    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);
    }
    ```
  </Tab>
</Tabs>

### 3. 注册回调并保存订阅句柄

注册后 SDK 在后台线程调用帧处理函数。每条流保存一个订阅句柄，退出时逐一关闭。`tf` 等全局流通过 manager 或全局 C 接口订阅，整个进程订阅一次即可。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    sub = glove.tactile().subscribe_with_callback(on_tactile)
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    struct WujiSub *sub = NULL;
    WujiStatus st = wuji_glove_subscribe_tactile(dev, on_tactile, NULL, &sub);
    /* st 非 OK 时记录 wuji_last_error()，清理设备并退出 */
    ```
  </Tab>
</Tabs>

### 4. 结束订阅并断开连接

主线程保持运行以接收数据。退出或发生异常时，先关闭订阅，再断开设备，避免回调继续访问已释放的资源。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import time
    try:
        time.sleep(10)
    finally:
        sub.close()
        manager.disconnect_all()
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    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_glove/0.subscribe_callback.py --sn WG1KA00XXXXXXXXX
    ```
  </Tab>

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

Python 侧六条流交错输出，不限流：

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

5 秒实测输出 3201 行，约 640 行每秒，与六条流各 130 Hz 相符。

C 侧把打印限流到每 30 帧一次，并多出两条全局流：

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

以上为实机运行结果，设备是右手 Wuji Glove，手套静置无人佩戴。

## 注意事项

| 分支点 | 表现 | 处理 |
| - | - | - |
| 设备被占用 | 抛 `Session already exists on device` | 等上一个会话心跳超时后重试，或确认没有别的客户端连着同一台设备 |
| 多只手套在网 | 两侧都遍历全部手套并逐只连接 | 用 `--sn` 指定，见下方「调整与适配」 |
| 网内有非手套设备 | 按 `device_type` 过滤后跳过 | 示例已处理，Hand 2 与未知设备都会被排除 |
| 订阅积压 | C 侧回调收到 `WUJI_FRAME_KIND_LAG` | 回调内避免执行阻塞操作 |
| 流结束或出错 | C 侧收到 `WUJI_FRAME_KIND_END` 或 `WUJI_FRAME_KIND_ERROR` | `ERROR` 时用 `wuji_last_error()` 取原因 |
| 指定了 `--hand-model-path` 但没有具名用户 | 设置被拒 | 自定义手部模型需要先创建 SDK 用户，见 [SDK 本地用户管理](/docs/wuji-sdk/sdk-practice/user-management/) |

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

## 扩展用法

### 示例的 avg() 会被无效通道拉成负数

此处为本示例的关键说明。Python 示例用 `avg()` 算分区均值：

```python theme={null}
def avg(lst: list[float]) -> float:
    return sum(lst) / len(lst) if lst else 0.0
```

`-1.0` 直接进了分子，实测掌心 308 个通道里 20 个无效，输出就成了 `palm=-0.1`——手套根本没受力，读数却是负的。C 示例打 `palm[0]`，如果第一个通道恰好无效就直接显示 `-1.000`，实测正是如此。

两侧都要先滤掉负值：

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

滤掉后同一帧的掌心均值是 `+0.015`。无效通道的比例因手套而异，不要按固定数字写死。

### 多设备环境指定 SN

两侧示例都遍历扫到的每一只手套逐个连接。共用网络里跑之前用 `--sn` 限定：

```bash theme={null}
python examples/python/wuji_glove/0.subscribe_callback.py --sn WG1KA00XXXXXXXXX
```

C 示例没有命令行参数，需要在源码里比对 `list[i].serial_number` 后再连。

### 只订阅需要的流

六条流全开在静置状态下就是每秒 640 帧回调。做触觉分析只留 `tactile` 或 `tactile_point_cloud`，做手部姿态只留 `hand_skeleton`，回调压力立刻降下来。

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

每条订阅有独立的工作线程，同一条订阅内的回调串行、保序，不同订阅之间可能并发。跨回调共享的状态要自己加锁。写文件、跑算法、加锁等待都应该挪到消费线程，回调里只做最小拷贝。

### 换成异步写法

不想用回调可以改 `subscribe()` 加 `recv()`，把取帧的节奏握在自己手里，见[数据订阅：异步](/docs/wuji-glove/sdk-practice/subscribe-async)。

## 相关内容

* [数据订阅：异步](/docs/wuji-glove/sdk-practice/subscribe-async)：同一批流的 async/await 写法
* [数据录制](/docs/wuji-glove/sdk-practice/data-recording)：把这些流落盘成可回放的文件
* [输出率调节](/docs/wuji-glove/sdk-practice/stream-rate)：把 130 Hz 降到应用需要的频率
* [数据订阅机制](/docs/wuji-sdk/data-subscription/)：订阅模型与生命周期
* [通用数据类型](/docs/wuji-sdk/data-reference/)：`FrameHeader`、`TactileFrame`、`HandSkeleton` 等结构


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