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

# 数据订阅：异步

## 概述

本篇实践介绍如何通过 `async/await` 订阅 Wuji Glove 数据流，并在协程中读取数据帧。应用场景包括接入基于 `asyncio` 的应用、并发读取多条数据流和协调其他异步 I/O 任务。

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

<Info>
  C SDK 的手套示例只提供回调式订阅，没有对应的异步版本。本页介绍 Python 用法。
</Info>

## 前置条件

### 硬件

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

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

### 软件

| 项目 | 要求 |
| - | - |
| 操作系统 | Ubuntu 22.04 LTS 及以上版本 |
| Wuji SDK | v2026.8.31 或更高版本 |
| Python | 3.10 及以上版本 |

```bash theme={null}
pip install wuji-sdk
```

### 前序步骤

先看懂[数据订阅：回调](/docs/wuji-glove/sdk-practice/subscribe-callback)。六条流的帧结构、`-1.0` 无效通道、744/572/526 三个数字的关系都在那里，本页不重复这些内容。

## 数据流

```mermaid theme={null}
flowchart LR
    FW["Wuji Glove 固件"] --> HUB["SDK 订阅中枢"]
    subgraph TASKS[asyncio.gather 并发任务]
        T1["tactile().subscribe()<br/>while: await recv_async()"]
        T2["hand_skeleton().subscribe()<br/>while: await recv_async()"]
        T3["其余数据流各自订阅并消费"]
    end
    HUB --> T1
    HUB --> T2
    HUB --> T3
    T1 --> OUT["处理或打印最新帧"]
    T2 --> OUT
    T3 --> OUT
    STOP["KeyboardInterrupt / finally"] --> CLEAN["manager.disconnect_all()"]
```

队列在 SDK 内部，由固件推送填充，与消费协程解耦。这就是丢帧风险的来源。

## 订阅队列实测

### 队列容量是 1024 帧

订阅建立后即使一帧不取，队列也在填。实测在 132 Hz 的 `tactile` 流上：

| 订阅后静置时长 | 期间产生帧数 | 首次 `recv_async()` 拿到的帧落后实时多少 |
| - | - | - |
| 2 秒 | 约 264 | 300 帧 |
| 5 秒 | 约 655 | 654 帧 |
| 15 秒 | 约 1965 | 1023 帧 |

前两行落后量等于产生量，一帧没丢。第三行卡在 1023，**超出的部分已经被丢弃**。队列容量是 1024 帧，与 SDK 内部广播通道的配置一致。

### 丢的是最老的帧，不抽帧

三组实测里，首帧之后连续收 400 帧，`header.seq` 的步长集合恒为 `[1]`。队列溢出时丢弃队首最老的一批，保留的部分仍然连续，不会中间挖洞。

### 丢帧不抛异常，只有一条 WARN 日志

积压 12 秒后开始取帧，SDK 打出：

```text theme={null}
[<timestamp> WARN  wuji_sdk] Subscription for 'tactile' lagged 549 messages
```

`recv_async()` 本身正常返回下一帧，不抛异常，返回值里也没有任何丢帧标记。**应用代码对这 549 帧的丢失完全无感知。** 要发现它，只能靠日志。

跑对数据完整性有要求的任务时，把 SDK 日志级别开到 WARN 以上并接进自己的日志系统，不要直接输出到终端。

### 快消费不丢帧

反过来，只要消费跟得上就没有任何损失。实测连续 `await recv_async()` 取 200 帧用时 1.52 秒，等效 131 Hz，与流本身的输出率一致，`seq` 步长全是 1。

## 集成步骤

完整示例见 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/1.subscribe_async.py)。以下片段按接入顺序展示关键操作，参数解析、信号处理和完整错误分支见示例。

### 1. 连接目标手套

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

```python theme={null}
from wuji_sdk import SdkManager
manager = SdkManager.instance()
glove = manager.connect(sn=serial_number, device_name="glove_0")
```

### 2. 在协程中创建订阅并取帧

把订阅创建放在消费循环之前。每次 `recv_async()` 等待下一帧，任务结束或取消时通过 `finally` 关闭订阅。

```python theme={null}
async def consume_tactile(glove):
    sub = glove.tactile().subscribe()
    try:
        while True:
            frame = await sub.recv_async()
            valid = [v for v in frame.data if v >= 0.0]
            print(frame.header.seq, max(valid, default=0.0))
    finally:
        sub.close()
```

### 3. 启动消费任务并清理连接

单条流直接等待消费协程。需要并发消费时，按 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/1.subscribe_async.py) 为各条流建立协程，再交给 `asyncio.gather()`。

```python theme={null}
import asyncio
async def main():
    try:
        await consume_tactile(glove)
    finally:
        manager.disconnect_all()

asyncio.run(main())
```

## 运行与输出

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

```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][EmfPoses] thumb=[+0.067, +0.000, -0.059]
[glove_0][Tactile] max=0.3
[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][Zones] palm=-0.1 thumb=-0.1
[glove_0][PointCloud] points=526 frame=r_wrist
```

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

5 秒实测输出 3031 行，各流帧数分别是 506、506、505、502、502、501，六条流彼此相差不超过 5 帧。事件循环没有偏袒任何一条，`gather` 的调度是公平的。

`Zones` 那行的 `palm=-0.1` 是示例 `avg()` 把 `-1.0` 无效通道算进去的结果，不是手套受了反向的力。原因与修法见[数据订阅：回调](/docs/wuji-glove/sdk-practice/subscribe-callback)。

## 注意事项

| 分支点 | 表现 | 处理 |
| - | - | - |
| 消费慢于产出 | 队列堆积，延迟持续增长直到 1024 帧封顶 | 降低输出率，或回调式订阅里只做最小拷贝 |
| 队列溢出 | 静默丢最老的帧，只打 WARN 日志 | 监听 SDK 日志，或改用 `subscribe_with_callback` |
| 协程里做阻塞调用 | 整个事件循环卡住，六条流一起堆积 | 阻塞操作放 `run_in_executor`，不要直接调 |
| 设备被占用 | 连接时抛 `Session already exists on device` | 等上一个会话心跳超时后重试 |
| 设备断连 | `recv_async()` 抛 `Disconnected` | 捕获后重连重订阅 |
| 多只手套在网 | 示例遍历全部手套逐只连接 | 用 `--sn` 指定 |

示例的 `while True` 循环没有退出条件，靠 `KeyboardInterrupt` 停。`finally` 里只调了 `manager.disconnect_all()`，订阅句柄随进程退出一起回收。写长期运行的服务要自己补 `sub.close()`。

## 扩展用法

### 回调还是异步

| | 回调 | 异步 |
| - | - | - |
| 帧的流向 | SDK 推送 | 应用侧主动获取 |
| 执行线程 | SDK 工作线程，每条订阅一个 | 应用侧事件循环，单线程 |
| 跟不上时 | 同样在队列里堆积 | 同样在队列里堆积 |
| 适合 | 低延迟转发、写入队列 | 需要和别的 `await` 协作的逻辑 |

两者队列语义相同，1024 帧的边界对回调一样成立。回调不会占用应用侧事件循环，异步订阅可以与 HTTP 请求、数据库写入等协程组合使用。

### 避免在协程中执行阻塞调用

事件循环是单线程的。协程里调 `time.sleep()`、同步写文件、同步 HTTP 请求，六条流会一起堆积。阻塞操作交给线程池：

```python theme={null}
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, write_frame_to_disk, frame)
```

### 只订阅需要的流

六条流全开，静置状态下每秒约 640 次 `recv_async()` 唤醒。做触觉分析只留 `tactile` 或 `tactile_point_cloud`，事件循环压力立刻降下来。

### 降低输出率

130 Hz 对多数应用偏高。订阅句柄支持降频，见[输出率调节](/docs/wuji-glove/sdk-practice/stream-rate)。降频之后队列里 1024 帧能覆盖的时间窗口随之变长，抗抖动能力更好。

## 相关内容

* [数据订阅：回调](/docs/wuji-glove/sdk-practice/subscribe-callback)：六条流的帧结构与无效通道处理
* [数据录制](/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.