Adjusting Stream Rate
SDK Practice
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. 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 API and field definitions, see Hand Tracking.
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.
Prerequisites
Hardware
| Item | Requirement |
|---|---|
| Wuji Glove | 1, firmware v0.11.4 or later |
| Connection | Ethernet |
Check the firmware version:
print(glove.version().get()) Software
| Item | Requirement |
|---|---|
| Operating system | Linux or a Mac with Apple silicon, meeting the system requirements |
| Wuji SDK | v2026.8.31 or later |
| Python | 3.10 or later |
| C toolchain | CMake 3.16 or later, plus the extracted SDK package |
Python
C
pip install wuji-sdkLIB="libwuji_sdk_c.so"
if [ "$(uname -s)" = "Darwin" ]; then LIB="libwuji_sdk_c.dylib"; fi
cmake -S examples/c/wuji_glove -B build/glove \
-DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
-DWUJI_SDK_LIB="${SDK}/lib/${LIB}"
cmake --build build/glove --target 1_set_stream_rate Before You Start
Read Subscribing With Callbacks first. set_rate is a method on a subscription handle, so a subscription is required before calling it.
Data Flow
The checks run in order, and that order decides which error you see.
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.
Failure Paths
The device runs firmware 0.11.2, below the required v0.11.4.
Rate Entry Streams Stop at the Firmware Gate
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
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
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:
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:
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:
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}") Integration Steps
These excerpts show the integration order. See the examples for argument parsing, signal handling, and full error handling.
1. Connect to the Target Glove
Replace serial_number with the target glove’s serial number. Reuse the connected device handle in later steps. C excerpts require wuji_sdk.h.
Python
C
from wuji_sdk import SdkManager
manager = SdkManager.instance()
glove = manager.connect(sn=serial_number, device_name="glove_0")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;
} 2. Subscribe to a Rate-Control Entry Stream
Create an emf_poses subscription to get the handle used for rate control. See C example for the C on_emf_poses callback.
Python
C
sub = glove.emf_poses().subscribe()struct WujiSub *sub = NULL;
WujiStatus st = wuji_glove_subscribe_emf_poses(dev, on_emf_poses, NULL, &sub);
/* If subscription fails, clean up the device and exit. */ 3. Set the Rate and Read the Applied Value
Keep the subscription handle alive. The device may quantize the request, so use the returned rate. Handle a failed request before continuing, and don’t treat the requested rate as applied.
Python
C
actual = sub.set_rate(30)
print(f"Applied rate: {actual} Hz")uint16_t actual = 0;
if (wuji_sub_set_rate(sub, 30, &actual) == WUJI_STATUS_OK) {
printf("actual=%u Hz\n", actual);
} else {
fprintf(stderr, "%s\n", wuji_last_error());
} 4. Receive Data and Restore the Default Rate
Python awaits frames in an async function. C receives them through a callback. Pass 0 to restore the default rate, then close the subscription and connection.
Python
C
try:
for _ in range(5):
frame = await sub.recv_async()
print(len(frame.poses))
print(sub.set_rate(0))
finally:
sub.close()
manager.disconnect_all()uint16_t restored = 0;
wuji_sub_set_rate(sub, 0, &restored);
wuji_sub_close(sub);
wuji_dev_disconnect(dev);
wuji_dev_release(dev);
wuji_shutdown(); Running It
Python
C
python examples/python/wuji_glove/6.set_stream_rate.py./build/glove/1_set_stream_rate Python output on firmware 0.11.2, where the program stops at set_rate:
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:
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:
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.
Notes
| 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 |
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.
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.
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: measured rates for all six streams
- Subscribing Asynchronously: the subscription queue’s 1024-frame boundary
- Recording Data: how lowering the rate affects file size
- Wuji Hand 2 Adjusting Stream Rate: the same API on the dexterous hand
- Data Subscription: subscription model and lifecycle
Documentation