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

# Adjusting Stream Rate

## Overview

The glove's six data streams push at around 130 Hz by default. Most applications can't use that much, and `Subscription.set_rate(hz)` lowers the rate at the device itself, saving bandwidth and CPU compared with decimating frames in your application.

One hard requirement: **the device firmware must support rate control**. The Wuji Glove needs v0.11.4 or later, and the Wuji Hand 2 needs v2.3.0 or later. The glove used here runs firmware 0.11.2, so what this page documents in full is **the behavior when that requirement isn't met**, with the success path described from the API contract.

Matching examples: `examples/python/wuji_glove/6.set_stream_rate.py` and `examples/c/wuji_glove/1_set_stream_rate.c`.

For the list of adjustable streams and their API definitions, see [Hand Tracking](/en/docs/wuji-glove/sdk-data-reference/hand-tracking). This page covers only how to run it and what the measurements show.

<Warning>
  The success path on this page isn't verified on hardware, because the available device's firmware doesn't meet the requirement. The failure paths, evaluation order, parameter bounds, and the difference in behavior between the two bindings are all measured.
</Warning>

## Prerequisites

### Hardware

| Item | Requirement |
| - | - |
| Wuji Glove | 1, **firmware v0.11.4 or later** |
| Connection | Ethernet |

Check the firmware version:

```python theme={null}
print(glove.version().get())
```

### 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_glove -B build/glove \
      -DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
      -DWUJI_SDK_LIB="${SDK}/lib/libwuji_sdk_c.so"
    cmake --build build/glove --target 1_set_stream_rate
    ```
  </Tab>
</Tabs>

### Before You Start

Read [Subscribing With Callbacks](/en/docs/wuji-glove/sdk-practice/subscribe-callback) first. `set_rate` is a method on a subscription handle, so a subscription is required before calling it.

## Which Streams Are Adjustable

| Category | Streams | `set_rate` |
| - | - | - |
| Rate entry points | `emf_poses`, `tactile`, `tactile_zones`, `imu_raw/{palm,thumb,index,middle,ring,pinky}` | Supported |
| Derived streams | `hand_joint_angles`, `hand_skeleton`, `tip_poses`, `imu_data/*`, `tactile_point_cloud` | Rejected, they follow their source |
| Global streams | `tf`, `tf_static` | Not supported |

**Lowering a source stream lowers every stream derived from it.** Drop `emf_poses` to 30 Hz and `hand_joint_angles`, `hand_skeleton`, and `tip_poses` all follow, sparing you a proportional share of the solver cost.

## Stream Rate Scope

This is the most commonly misread part of `set_rate`:

* The setting applies to **every subscriber of that stream**, including other processes, Wuji Studio, and recorders
* When the **last subscription to that stream closes**, the rate returns to the device default
* A **device reconnect** resets it as well

So keeping a lower rate in effect means holding that subscription handle open. The example says as much in a comment: `Keep the handle open for as long as you want its rate to stay in effect.`

When several clients share a device, the rate you lower is one everyone feels. Coordinate before changing it in a shared setup.

## Data Flow

```mermaid theme={null}
flowchart TD
    SUB["stream.subscribe()<br/>get a subscription handle"]
    CALL["sub.set_rate(hz)"]
    G1["1. entry-point check<br/>is this a rate entry stream"]
    G2["2. firmware capability gate<br/>rate_capabilities"]
    G3["3. device capability gate<br/>is this stream adjustable"]
    G4["4. active-subscription gate"]
    DEV["device lowers its rate<br/>returns what it applied"]
    DER["derived streams follow"]

    SUB --> CALL
    CALL --> G1
    G1 -->|"pass"| G2
    G2 -->|"pass"| G3
    G3 -->|"pass"| G4
    G4 --> DEV
    DEV --> DER
```

The checks run in order, and that order decides which error you see.

## Failure Paths

The device runs firmware 0.11.2, below the required v0.11.4.

### Rate Entry Streams Stop at the Firmware Gate

```text theme={null}
WujiException: Unsupported operation: device 'WG1KA00XXXXXXXXX' runs firmware without rate control
```

`emf_poses`, `tactile`, and `tactile_zones` all report the same thing. **Passing 0 to restore the default hits the same wall**, because the firmware gate comes before the parameter.

### Derived Streams Stop at the Entry-Point Check, Regardless of Firmware

```text theme={null}
WujiException: Unsupported operation: 'hand_joint_angles' is not a rate-control entry point
WujiException: Unsupported operation: 'hand_skeleton' is not a rate-control entry point
WujiException: Unsupported operation: 'tactile_point_cloud' is not a rate-control entry point
```

This check runs **before** the firmware gate, so newer firmware reports exactly the same error. Seeing this means changing which stream you call, not upgrading firmware.

### Global Streams Report an Internal Error

```text theme={null}
WujiException: Internal error: Device '' not registered
```

Both `tf` and `tf_static` report this. It isn't a friendly message, so recognize it as "I called `set_rate` on a global stream." Global streams don't support rate control at all, since they aggregate across devices and no single device can receive the command.

### Parameter Range: uint16

The C signature reads:

```c theme={null}
WujiStatus wuji_sub_set_rate(struct WujiSub *sub, uint16_t frequency_hz,
                             uint16_t *out_actual_hz);
```

Python uses the same 16-bit unsigned type. Measured bounds:

| Input | Result |
| - | - |
| `-1` | `OverflowError: out of range integral type conversion attempted` |
| `65535` | Reaches the firmware gate, so the parameter is valid |
| `65536` | `OverflowError` |

Negatives and anything above 65535 are stopped in the binding layer and never reach the device.

## The Two Bindings Handle Errors Differently

Given the same firmware-gate error, the two examples react in opposite ways:

| | Behavior | Consequence |
| - | - | - |
| Python example | `set_rate` raises `WujiException` with no try | **The program crashes out**, receiving no frames at all |
| C example | Checks the return code, prints the error, and continues | The subscription keeps working and collects its full 2 seconds |

Measured output from the C example:

```text theme={null}
wuji_sub_set_rate failed: Unsupported operation: device 'WG1KA00XXXXXXXXX' runs firmware without rate control
emf_poses frame: 5 poses
emf_poses frame: 5 poses
emf_poses frame: 5 poses
...
```

**The C approach is the sturdier one.** Rate control is an optional optimization and shouldn't take down a capture pipeline because a device runs older firmware. When adapting the Python example, wrap `set_rate` in a try:

```python theme={null}
try:
    actual = sub.set_rate(30)
    print(f"lowered to {actual} Hz")
except WujiException as e:
    print(f"rate control unavailable, running at the default rate: {e}")
```

## Runtime Handling

| Branch point | Behavior | What to do |
| - | - | - |
| Firmware below v0.11.4 | `runs firmware without rate control` | Upgrade the firmware, or catch the exception and run at the default rate |
| Called on a derived stream | `is not a rate-control entry point` | Call its source stream instead, and derived streams follow |
| Called on a global stream | `Internal error: Device '' not registered` | Global streams don't support rate control |
| Negative or above 65535 | `OverflowError` | The parameter is a uint16 |
| Device doesn't support the requested rate | Returns the **quantized** value it applied | Use the return value, don't assume it equals the request |
| Last subscription to the stream closes | The rate returns to the device default | Hold the handle to keep the lower rate |
| Device reconnects | The rate resets | Call it again after reconnecting |
| Another client subscribes to the same stream | It gets the lowered rate too | Coordinate before changing rates on a shared device |

<Info>
  The older `emf_poses_rate_divider` API (Python `glove.emf_poses_rate_divider()`, C `wuji_glove_get/set_emf_poses_rate_divider`) has been removed. Upgrade to firmware v0.11.4 or later and use `set_rate` on an `emf_poses` subscription. One practical difference: the old divider persisted across reconnects, while `set_rate` doesn't.
</Info>

## Integration Steps

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import asyncio
    from wuji_sdk import SdkManager, DeviceType, WujiException

    REQUESTED_HZ = 30

    async def main():
        manager = SdkManager.instance()

        # 1. Discover and connect
        devices = [d for d in manager.scan() if d.device_type == DeviceType.WujiGlove]
        glove = manager.connect(sn=devices[0].sn, device_name="glove")
        print(f"Firmware version: {glove.version().get()}")

        # 2. Subscribe first. set_rate is a method on the subscription handle,
        #    and closing that handle resets the rate
        sub = glove.emf_poses().subscribe()

        # 3. Lower the rate. Older firmware raises, so wrap it and keep going
        try:
            actual = sub.set_rate(REQUESTED_HZ)
            # The device may quantize to a rate it supports. Use the return value
            print(f"requested {REQUESTED_HZ} Hz, device applied {actual} Hz")
        except WujiException as e:
            print(f"rate control unavailable, running at the default rate: {e}")

        # 4. Read frames as usual. Derived streams already follow the new rate
        for _ in range(5):
            frame = await sub.recv_async()
            print(f"{len(frame.poses)} poses")

        # 5. Pass 0 to restore the device default
        try:
            print(f"restored to the device default: {sub.set_rate(0)} Hz")
        except WujiException:
            pass

        # 6. Close the handle. The rate resets at this moment too
        sub.close()
        manager.disconnect_all()

    asyncio.run(main())
    ```
  </Tab>

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

    #define REQUESTED_HZ 30

    /* 1. Initialize, discover, connect. See the callback subscription page */

    /* 2. Subscribe first */
    struct WujiSub *sub = NULL;
    if (wuji_glove_subscribe_emf_poses(dev, on_emf_poses, &ctx, &sub) != WUJI_STATUS_OK) {
        fprintf(stderr, "subscribe failed: %s\n", wuji_last_error());
        return 1;
    }

    /* 3. Lower the rate. Check the return code without aborting what follows */
    uint16_t actual = 0;
    WujiStatus st = wuji_sub_set_rate(sub, REQUESTED_HZ, &actual);
    if (st != WUJI_STATUS_OK) {
        /* Streams without rate control return WUJI_STATUS_ERR_UNSUPPORTED (-8)
         * and send no rate-control request to the device */
        fprintf(stderr, "wuji_sub_set_rate failed: %s\n", wuji_last_error());
    } else {
        printf("requested %d Hz, device applied %u Hz\n", REQUESTED_HZ, actual);
    }

    /* 4. Receive frames as usual, with callbacks on SDK worker threads */
    sleep(2);

    /* 5. Pass 0 to restore the device default */
    uint16_t restored = 0;
    if (wuji_sub_set_rate(sub, 0, &restored) == WUJI_STATUS_OK) {
        printf("restored to the device default: %u Hz\n", restored);
    }

    /* 6. Close the handle. The rate resets at this moment too */
    wuji_sub_close(sub);
    wuji_dev_disconnect(dev);
    wuji_dev_release(dev);
    wuji_shutdown();
    ```
  </Tab>
</Tabs>

Subscribe before calling `set_rate`. Without an active subscription, the active-subscription gate rejects the call.

## Running It

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    python examples/python/wuji_glove/6.set_stream_rate.py
    ```
  </Tab>

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

Python output on firmware 0.11.2, where the program stops at `set_rate`:

```text theme={null}
Connected: WG1KA00XXXXXXXXX
Traceback (most recent call last):
  ...
  File "6.set_stream_rate.py", line 43, in main
    actual = sub.set_rate(REQUESTED_HZ)
wuji_sdk.WujiException: Unsupported operation: device 'WG1KA00XXXXXXXXX' runs firmware without rate control
```

C output on the same device, which keeps receiving frames after the error:

```text theme={null}
  SN=WG1KA00XXXXXXXXX  Type=WujiGlove  Address=192.168.1.100:50001
wuji_sub_set_rate failed: Unsupported operation: device 'WG1KA00XXXXXXXXX' runs firmware without rate control
emf_poses frame: 5 poses
emf_poses frame: 5 poses
...
```

Measured on a right-hand Wuji Glove running firmware 0.11.2.

With firmware that meets the requirement, Python output looks like this:

```text theme={null}
Connected: <serial number>
emf_poses: requested 30 Hz, device applied 30 Hz
emf_poses frame at 30 Hz: 5 poses
...
restored to the device default: 0 Hz
```

`device applied` may differ from the request, because the device quantizes to a rate it supports.

## Adapting It

### Use the Return Value, Not the Request

`set_rate` returns the rate the device actually applied. Sizing timeouts, buffers, or sampling windows from the requested value goes wrong as soon as quantization kicks in.

### How Much Lowering the Rate Saves

It scales linearly. From 130 Hz to 30 Hz, both network traffic and callback count fall to about 23%. Recording benefits most directly: all eight streams with zstd measured about 72 MB per minute, which becomes roughly 17 MB per minute at 30 Hz.

Lowering `emf_poses` also saves hand-solver CPU, because derived streams follow.

### Lowering the Rate Doesn't Change Queue Capacity

The subscription queue still holds 1024 frames. At a lower rate those same 1024 frames cover a longer window, so a slow consumer actually gains headroom. See [Subscribing Asynchronously](/en/docs/wuji-glove/sdk-practice/subscribe-async).

### Keeping a Low Rate Without Holding a Handle

There's no mechanism that persists across connections today. The old `emf_poses_rate_divider` had that property and has been removed. The current approach is to hold the subscription handle inside your long-running service.

## Related

* [Subscribing With Callbacks](/en/docs/wuji-glove/sdk-practice/subscribe-callback): measured rates for all six streams
* [Subscribing Asynchronously](/en/docs/wuji-glove/sdk-practice/subscribe-async): the subscription queue's 1024-frame boundary
* [Recording Data](/en/docs/wuji-glove/sdk-practice/data-recording): how lowering the rate affects file size
* [Wuji Hand 2 Adjusting Stream Rate](/en/docs/wuji-hand/latest/sdk-practice/stream-rate/): the same API on the dexterous hand
* [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.