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

## Overview

Subscribe to the Wuji Hand 2 `joint_states` stream with `async/await`. Compared with callbacks, frames come out sequentially inside a coroutine with no cross-thread handoff.

When your application already runs on asyncio, this fits more naturally. Concurrency across devices falls out of `asyncio.gather()` rather than thread management.

Matching example: `examples/python/wuji_hand_2/1.subscribe_async.py`.

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.

<Info>
  The C API has no async form. C subscriptions all go through callbacks, described in [Subscribing With Callbacks](/en/docs/wuji-hand/latest/sdk-practice/subscribe-callback).
</Info>

This example only reads. It doesn't enable the motors, and the fingers don't move.

## Prerequisites

### Hardware

| Item | Requirement |
| - | - |
| Wuji Hand 2 | 1 |
| Connection | Ethernet |

### Software

| Item | Requirement |
| - | - |
| Operating system | Ubuntu 22.04 LTS or later |
| Wuji SDK | v2026.8.31 or later |
| Python | 3.10 or later |

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

## Data Flow

```mermaid theme={null}
flowchart TD
    FW["Wuji Hand 2 firmware<br/>broadcasts joint state at about 1 kHz"]
    SUB["joint_states().subscribe()<br/>returns a handle backed by a queue"]
    LOOP["asyncio event loop"]
    AWAIT["await sub.recv_async()<br/>suspends the coroutine when the queue is empty"]
    APP["application coroutine<br/>processes each frame"]

    FW -->|"frames enqueue"| SUB
    SUB --> LOOP
    LOOP --> AWAIT
    AWAIT -->|"JointStateFrame"| APP
    APP -->|"wait for the next frame"| AWAIT
```

The handle from `subscribe()` supports both retrieval styles: `await recv_async()` asynchronously and `recv()` synchronously. Frame structure is identical to the callback form.

## Runtime Handling

| Branch point | Behavior | What to do |
| - | - | - |
| Queue empty | `recv_async()` suspends the coroutine and yields the event loop | Expected behavior, so don't busy-wait |
| Queue empty with synchronous `recv()` | Returns `None` immediately without blocking | Pace the loop yourself when using `recv()` |
| Device disconnects | `recv_async()` raises `WujiException` | Since v2026.8.17 it raises immediately, where earlier versions could return a buffered frame or `None` |
| Several devices | The example connects to each device it finds and runs them concurrently with `asyncio.gather()` | On a shared network, change it to name a serial number |
| Coroutine cancelled | `gather` raises and `finally` disconnects the devices | The example relies on `disconnect_all()` in `finally` |

## Integration Steps

```python theme={null}
import asyncio
from wuji_sdk import SdkManager, DeviceType, JointStateFrame

async def print_joint_state(hand):
    # 1. Get a subscription handle
    sub = hand.joint_states().subscribe()
    while True:
        # 2. Await the next frame. The coroutine suspends when the queue is empty
        frame: JointStateFrame = await sub.recv_async()
        h = frame.header
        positions = [f"{j.position:+.3f}" for j in frame.joints[:4]]
        print(f"[{hand.device_name}][JointState] seq={h.seq} "
              f"t={h.timestamp_us}us frame={h.frame_id!r} thumb_pos={positions}")

async def main():
    manager = SdkManager.instance()
    hands = [d for d in manager.scan() if d.device_type == DeviceType.WujiHand2]
    try:
        # 3. Connect. With several devices around, name an sn
        hand = manager.connect(sn=hands[0].sn, device_name="hand_0")
        # 4. One coroutine per device, running concurrently
        await asyncio.gather(print_joint_state(hand))
    finally:
        manager.disconnect_all()

asyncio.run(main())
```

Subscribing is read-only and needs neither `enable()` nor `disable()`.

## Running It

```bash theme={null}
python examples/python/wuji_hand_2/1.subscribe_async.py
```

```text theme={null}
Connected: WH2KA00XXXXXXXXX (20 joints online)
[hand_0][JointState] seq=147508 t=210677379743us frame='r_wrist' thumb_pos=['+0.003', '-0.018', '+0.021', '+0.056']
```

### Measurements

On a right-hand Wuji Hand 2 with all 20 joints online:

| Metric | Measured |
| - | - |
| `recv_async()` throughput | **1088 Hz** (3265 frames in 3 seconds) |
| Synchronous `recv()` on the same handle | 1091 frames in 1 second |
| `frame_id` | `r_wrist` |
| `joints` length | 20 |

Both retrieval styles reach the same throughput and both keep up with the device's roughly 1 kHz broadcast. Choose based on how your application is structured, not on performance.

## Adapting It

### Async or Callback

| | Async `recv_async()` | Callback `subscribe_with_callback()` |
| - | - | - |
| Runs on | Your event loop | An SDK worker thread |
| Backpressure | Queue growth is visible, and you can drop frames deliberately | A blocking callback directly slows the worker thread |
| Fits | Applications already on asyncio | Simple forwarding, writing to a queue |
| C API | None | Available |

### Concurrency Across Devices

One coroutine per device with `asyncio.gather()` is enough. Devices don't block each other.

### Lower the Rate

About 1 kHz is higher than most applications need. Use `set_rate()` on the same handle, noting the firmware version requirement described in [Adjusting Stream Rate](/en/docs/wuji-hand/latest/sdk-practice/stream-rate).

## Related

* [Subscribing With Callbacks](/en/docs/wuji-hand/latest/sdk-practice/subscribe-callback): the callback form, and the only form the C API offers
* [Adjusting Stream Rate](/en/docs/wuji-hand/latest/sdk-practice/stream-rate): lower the firmware push rate
* [Data Subscription](/en/docs/wuji-sdk/data-subscription/): subscription model and lifecycle


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