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

# Recording Data

## Overview

`TopicRecorder` writes subscribed streams straight into an MCAP file. MCAP is Foxglove's open recording format, so you can step through a finished recording in Foxglove Studio, or parse it offline with the `mcap` command-line tool or Python's `mcap` library.

Recording rides on the same subscription machinery: `recorder.record(stream.subscribe())` registers a channel, and everything registered starts hitting disk once you call `recorder.start(path)`. Both device streams and global transform streams can be recorded.

Matching example: `examples/python/wuji_glove/2.recording.py`.

For the full recording API and compression options, see [Data Recording and Offline Processing](/en/docs/wuji-sdk/data-recording/). This page covers only how to run it and what the measurements show.

<Info>
  The C SDK has no matching recording API. This page covers Python.
</Info>

## Prerequisites

### Hardware

| Item | Requirement |
| - | - |
| Wuji Glove | 1 |
| Connection | Ethernet |
| Disk | Size from the measurements below. Six streams with zstd run about 70 MB per minute |

This example only reads. It writes no device parameters.

### 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
# add this to parse mcap locally
pip install mcap
```

### Before You Start

Read [Subscribing With Callbacks](/en/docs/wuji-glove/sdk-practice/subscribe-callback) first. Recording takes the subscription handles returned by `subscribe()`, and the queue semantics are identical.

## Data Flow

```mermaid theme={null}
flowchart TD
    FW["Wuji Glove firmware"]
    SUB["stream.subscribe()<br/>one handle per stream"]
    REC["TopicRecorder.record()<br/>register as a channel"]
    START["recorder.start(path)<br/>returns a handle"]
    WRITE["writer thread<br/>compress by chunk"]
    FILE["MCAP file<br/>named /SN/topic"]
    Q["handle.stop()<br/>returns summary and quality"]

    FW --> SUB
    SUB --> REC
    REC --> START
    START --> WRITE
    WRITE --> FILE
    START --> Q
```

## File Structure

Recording six device streams plus two global streams for 5 seconds produces these channels:

| Channel | Messages | Note |
| - | - | - |
| `/WG1KA00XXXXXXXXX/tactile` | 659 | Device streams are named `/{serial number}/{topic}` |
| `/WG1KA00XXXXXXXXX/tactile_zones` | 659 | |
| `/WG1KA00XXXXXXXXX/emf_poses` | 659 | |
| `/WG1KA00XXXXXXXXX/hand_joint_angles` | 655 | |
| `/WG1KA00XXXXXXXXX/hand_skeleton` | 655 | |
| `/WG1KA00XXXXXXXXX/tactile_point_cloud` | 654 | |
| `/tf` | 4361 | **Global streams carry no serial-number prefix** because they aggregate across devices |
| `/tf_static` | 6 | |

Messages use `json` encoding and each channel carries its own schema, so Foxglove Studio shows field names directly without a separate message-definition file.

The file header's `library` field reads `mcap-rs-0.24.0`, the SDK's built-in Rust writer.

## Compression Modes

Recording the same 3 streams for 5 seconds, about 1970 frames:

| `compression` | File size | Relative to none |
| - | - | - |
| `"none"` | 5.63 MB | 100% |
| `"lz4"` | 1.71 MB | 30% |
| `"zstd"` | 0.92 MB | 16% |

JSON encoding repeats field names, which compresses well. Choose `zstd` for long recordings, `lz4` where write latency matters, and `none` only when another process needs to stream the file while it's still being written.

Looking at a single lz4 chunk, 3101 bytes of raw data compress to 938.

## Quality Metrics

The summary returned by `handle.stop()` carries a `quality` object:

```text theme={null}
QualitySummary(total_frames=8308, dropped=0, drop_rate=0.0000, sync_rate=0.8571)
```

Full fields with measured values:

| Field | Meaning | 3 streams | 6 streams | 8 streams (with tf) |
| - | - | - | - | - |
| `total_frames` | Frames written | 1973 | 3937 | 8308 |
| `dropped_frames` | Frames dropped | 0 | 0 | 0 |
| `frame_drop_rate` | Drop rate over a 5-second sliding window | 0.0000 | 0.0000 | 0.0000 |
| `sync_rate` | Cross-channel sync success rate | 0.998 | 1.000 | 0.857 |
| `avg_sync_offset_ms` | Average sync offset | 7.89 | 5.23 | — |
| `max_sync_offset_ms` | Peak sync offset | 2966.23 | 13.24 | — |
| `spc_alert_count` | Statistical process control alerts | 0 | 0 | — |

Three things stand out:

**`sync_rate` falls to 0.857 once `tf` joins.** The six device streams all sit near 130 Hz and line up with each other. `tf` measures 872 Hz and `tf_static` only 1 Hz, an order of magnitude apart from the rest, so the sync check naturally fails. Mixing rate bands in one recording makes this number low by design, and it doesn't mean the data is bad.

**`max_sync_offset_ms` can show one large outlier.** The 3-stream run measured 2966 ms while its `avg` was only 7.89 ms. That comes from the first frames of each channel not yet being aligned right after recording starts. Judge recording quality by `avg` and `frame_drop_rate`, and don't let a startup spike in `max` mislead you.

**`duration_s` is wall-clock time, not effective recording time.** Pausing breaks the correspondence, as the next section shows.

## Pause and Resume

Besides `stop()`, the handle offers `pause()` and `resume()`, which the example doesn't demonstrate. Recording 2 seconds, pausing 2 seconds, then recording 2 more with 3 streams:

| Metric | Measured |
| - | - |
| `duration_s` | 6.0 (wall clock, pause included) |
| `total_frames` | 1574 |
| Expected from 4 effective seconds | 3 streams x 131 Hz x 4 s ≈ 1572 |

The frame count matches 4 seconds of effective recording, with nothing written while paused. `duration_s` still follows the wall clock, so deriving a rate from `total_frames / duration_s` comes out low.

That run also raised `spc_alert_count` to 1. Channels go silent while paused and statistical process control flags it as an anomaly. That's expected, not a fault.

The handle also exposes `subscribe_alerts()`, `subscribe_metrics()`, and `subscribe_status()`, which deliver alerts, quality metrics, and status during recording rather than making you wait for `stop()`.

## Runtime Handling

| Branch point | Behavior | What to do |
| - | - | - |
| No hand model calibration | The log prints `offline hand_model: using built-in default URDF for Right` | Recording proceeds on the built-in default hand model. For a model that matches your hand, see [Hand Model Calibration](/en/docs/wuji-glove/sdk-practice/hand-model-calibration) |
| `Unknown interest` on stop | One ERROR-level log line | A race in the zenoh transport layer during shutdown. It doesn't affect the file already on disk and can be ignored |
| Target directory missing | `start()` fails | The example calls `os.makedirs(..., exist_ok=True)` first |
| Disk can't keep up | `dropped_frames` climbs | Switch to zstd, or register fewer channels |
| Device disconnects mid-run | That channel stops producing | `quality` records `last_downtime_ms` for the channel |
| `Ctrl+C` during recording | The example guards `stop()` with `asyncio.shield` | The file closes cleanly instead of being left half-written |

This block in the example is deliberate:

```python theme={null}
stop_task = asyncio.ensure_future(handle.stop())
try:
    summary = await asyncio.shield(stop_task)
except asyncio.CancelledError:
    summary = await stop_task
```

`asyncio.shield` keeps `stop()` from being cancelled by an outer interrupt. Without that guard, `Ctrl+C` can kill the task before MCAP finishes writing its index, leaving a file whose summary can't be read.

## Integration Steps

```python theme={null}
import asyncio
import os
from datetime import datetime
from wuji_sdk import SdkManager, DeviceType, TopicRecorder

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

    # 1. Discover devices and filter by type
    devices = [d for d in manager.scan() if d.device_type == DeviceType.WujiGlove]

    # 2. Connect. With several devices around, name an sn instead of iterating
    glove = manager.connect(sn=devices[0].sn, device_name="glove_0")
    print(f"Connected: {glove.serial_number}")

    # 3. Create the recorder and pick a compression mode
    recorder = TopicRecorder(compression="zstd")

    # 4. Register channels. Each subscribe() feeds one stream into the recorder
    recorder.record(glove.tactile().subscribe())
    recorder.record(glove.emf_poses().subscribe())
    recorder.record(glove.hand_skeleton().subscribe())

    # Global streams record too, without a serial-number prefix in the file
    recorder.record(manager.tf().subscribe())

    # 5. Start recording
    os.makedirs("./data", exist_ok=True)
    path = f"./data/{datetime.now().strftime('%Y%m%d_%H%M%S')}.mcap"
    handle = await recorder.start(path)

    try:
        await asyncio.sleep(10)
    finally:
        # 6. Guard the stop, or the file index may end up incomplete
        stop_task = asyncio.ensure_future(handle.stop())
        try:
            summary = await asyncio.shield(stop_task)
        except asyncio.CancelledError:
            summary = await stop_task

    q = summary.quality
    print(f"{summary.total_frames} frames, "
          f"{summary.file_size / 1024 / 1024:.2f} MB, {summary.duration_s:.1f}s")
    print(f"dropped {q.dropped_frames} ({q.frame_drop_rate:.4f}), "
          f"sync rate {q.sync_rate:.3f}, avg offset {q.avg_sync_offset_ms:.2f} ms")

    manager.disconnect_all()

asyncio.run(main())
```

Finish every `record()` call before `start()`. Channels registered afterward never reach the file.

## Running It

```bash theme={null}
python examples/python/wuji_glove/2.recording.py
```

```text theme={null}
  SN=WG1KA00XXXXXXXXX, Type=DeviceType.WujiGlove, Address=192.168.1.100:50001
Connected: WG1KA00XXXXXXXXX
Recording to ./data/<date>_<time>.mcap ...
Done — 3939 frames, 3.43 MB, 10.0s
```

Measured on a right-hand Wuji Glove resting on a desk, unworn, recording 3 streams for 10 seconds with lz4. Those 3939 frames spread across 3 channels at about 1313 each, an effective 131 Hz per channel.

## Reading a Recording

With the `mcap` command-line tool installed:

```bash theme={null}
mcap info ./data/<date>_<time>.mcap
```

Parsing frame by frame in Python:

```python theme={null}
from mcap.reader import make_reader

with open("./data/<date>_<time>.mcap", "rb") as fp:
    reader = make_reader(fp)
    summary = reader.get_summary()
    for cid, channel in summary.channels.items():
        n = summary.statistics.channel_message_counts.get(cid, 0)
        print(f"{channel.topic}: {n} messages")

    for schema, channel, message in reader.iter_messages(topics=["/WG1KA00XXXXXXXXX/tactile"]):
        print(message.log_time, len(message.data))
```

Message bodies are JSON, so `json.loads(message.data)` gives you the same field structure as a live subscription.

## Adapting It

### Register Only the Channels You Need

All eight streams with zstd measured 6.01 MB over 5 seconds, roughly 72 MB per minute. `tf` alone accounts for 4361 messages, more than the six device streams combined. Decide which streams you actually need before a long capture.

### Lower the Rate Before Recording

Subscription handles support rate control. Dropping 130 Hz to 30 Hz cuts file size to under a quarter. See [Adjusting Stream Rate](/en/docs/wuji-glove/sdk-practice/stream-rate).

### Monitor Quality While Recording

Rather than waiting for `stop()` to learn whether frames dropped, read metrics live with `handle.subscribe_metrics()` and alerts with `handle.subscribe_alerts()`. Any long unattended capture should wire up both.

## Related

* [Subscribing With Callbacks](/en/docs/wuji-glove/sdk-practice/subscribe-callback): frame structures for all six streams and invalid-channel handling
* [Subscribing Asynchronously](/en/docs/wuji-glove/sdk-practice/subscribe-async): the subscription queue's buffering boundary
* [Replaying Data Offline](/en/docs/wuji-glove/sdk-practice/offline-replay): rerun algorithms over a finished recording
* [Adjusting Stream Rate](/en/docs/wuji-glove/sdk-practice/stream-rate): lower the rate before recording to control file size
* [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.