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

# Offline Solving

Wuji Glove offline solving turns `emf_poses` into joint angles, fingertip poses, and a hand skeleton, and raw IMU frames into fused orientation. [Data Recording](https://docs.wuji.tech/docs/en/wuji-sdk/latest/data-recording.md) saves input frames to an MCAP file. The [replay code](https://docs.wuji.tech/docs/en/wuji-glove/latest/sdk-practice/offline-replay.md#replay-code) reads that file and rebuilds the frames. Pass the rebuilt frames directly to the offline solving API without connecting to a device.

## Solving Steps

### Create the Pipeline

Create an offline processing pipeline with the capture device’s serial number and hand side. The serial number accepts 1–64 bytes of ASCII letters, digits, `.`, `_`, and `-`, excluding `.` and `..` alone. Use the optional `urdf_path` to pin a hand model when processing the same data again.

The examples use a right hand. Replace these values with those of the recording.

> [tab] Python
>
> `hand_side` accepts `"left"` or `"right"`. An invalid value raises `WujiException`. Set `urdf_path=None` to use the model lookup rules below.
>
> ```python theme={null}
> from wuji_sdk import WujiGlove
>
> pipeline = WujiGlove.offline_pipeline(
>     sn="WujiGlove-12345",
>     hand_side="right",
>     urdf_path=None,
> )
> ```

> [tab] C
>
> Use the header and shared library from the same SDK release. Call `wuji_init` before processing and `wuji_shutdown` afterward. For initialization error handling and where to call shutdown, see [C API Reference: Initialization and conventions](https://docs.wuji.tech/docs/en/wuji-sdk/latest/c-sdk-reference.md#initialization-and-conventions). Place this snippet in an application function returning `int`, with `wuji_sdk.h`, `stdio.h`, and `stdlib.h` included.
>
> ```c theme={null}
> if (wuji_init(NULL) != WUJI_STATUS_OK) {
>     fprintf(stderr, "Init failed: %s\n", wuji_last_error());
>     return 1;
> }
> WujiGloveOfflinePipeline *pipeline = NULL;
> WujiStatus status = wuji_glove_offline_create(
>     "WujiGlove-12345", WUJI_HANDEDNESS_RIGHT, NULL, &pipeline);
> if (status != WUJI_STATUS_OK) {
>     fprintf(stderr, "Create failed: %s\n", wuji_last_error());
>     wuji_shutdown();
>     return 1;
> }
> ```

### Recompute Joint Angles, Fingertip Poses, and Skeleton

When processing recorded data, read input frames from the `/<serial_number>/emf_poses` channel in the MCAP file. The offline API takes `EmfPoseArray` in Python or `WujiEmfPoseArray` in C. See the Python [replay code](https://docs.wuji.tech/docs/en/wuji-glove/latest/sdk-practice/offline-replay.md#replay-code) for MCAP reading and frame reconstruction.

Each frame contains five EMF poses. Request joint angles, fingertip poses, and the skeleton from the same frame to share one solve. These functions process one frame and print the result counts.

> [tab] Python
>
> ```python theme={null}
> def compute_hand_frame(pipeline, frame):
>     angles = pipeline.hand_joint_angles().compute(frame)
>     tips = pipeline.tip_poses().compute(frame)
>     skeleton = pipeline.hand_skeleton().compute(frame)
>     print(f"Fingers: {len(angles.fingers)}, tips: {len(tips.poses)}, "
>           f"skeleton joints: {len(skeleton.joints)}")
> ```

> [tab] C
>
> Zero-initialize the outputs. Free their heap fields after use, including earlier successful results if a later computation fails.
>
> ```c theme={null}
> int compute_hand_frame(WujiGloveOfflinePipeline *pipeline, const WujiEmfPoseArray *frame) {
>     WujiHandJointAngles angles = {0};
>     WujiFingertipPoses tips = {0};
>     WujiHandSkeleton skeleton = {0};
>     WujiStatus status = wuji_glove_offline_compute_hand_joint_angles(pipeline, frame, &angles);
>     if (status == WUJI_STATUS_OK)
>         status = wuji_glove_offline_compute_tip_poses(pipeline, frame, &tips);
>     if (status == WUJI_STATUS_OK)
>         status = wuji_glove_offline_compute_hand_skeleton(pipeline, frame, &skeleton);
>     if (status == WUJI_STATUS_OK)
>         printf("Fingers: %zu, tips: %zu, skeleton joints: %zu\n",
>                angles.fingers_len, tips.poses_len, skeleton.joints_len);
>     else
>         fprintf(stderr, "Compute failed: %s\n", wuji_last_error());
>     wuji_hand_joint_angles_free(&angles);
>     wuji_fingertip_poses_free(&tips);
>     wuji_hand_skeleton_free(&skeleton);
>     return status == WUJI_STATUS_OK ? 0 : 1;
> }
> ```

| Output | Content |
| - | - |
| [HandJointAngles](https://docs.wuji.tech/docs/en/wuji-glove/latest/sdk-data-reference/hand-tracking.md#handjointangles) | 21 DoF hand joint angles |
| [FingertipPoses](https://docs.wuji.tech/docs/en/wuji-glove/latest/sdk-data-reference/hand-tracking.md#fingertipposes) | Five fingertip poses |
| [HandSkeleton](https://docs.wuji.tech/docs/en/wuji-glove/latest/sdk-data-reference/hand-tracking.md#handskeleton) | 21 MediaPipe keypoints |

### Recompute IMU Orientation

When processing recorded data, read input frames from the `/<serial_number>/imu_raw/palm` channel in the MCAP file. The offline API takes raw `ImuData` in Python or raw `WujiImuData` in C. See the Python [replay code](https://docs.wuji.tech/docs/en/wuji-glove/latest/sdk-practice/offline-replay.md#replay-code) for MCAP reading and frame reconstruction.

Pass each raw IMU frame to the matching fusion entry point to compute orientation. The six entry points correspond to `palm`, `thumb`, `index`, `middle`, `ring`, and `pinky`. Each keeps a separate fusion state and doesn’t depend on the calibration URDF. Python methods follow `imu_data_<location>()`, and C functions follow `wuji_glove_offline_compute_imu_data_<location>`.

> [tab] Python
>
> `ImuData.raw()` sets `orientation_covariance[0]` to `-1.0` to mark an unfused input.
>
> ```python theme={null}
> def compute_palm_imu(pipeline, raw):
>     fused = pipeline.imu_data_palm().compute(raw)
>     q = fused.orientation
>     print(f"Orientation: {q.x}, {q.y}, {q.z}, {q.w}")
> ```

> [tab] C
>
> Set the raw `WujiImuData` field `orientation_covariance[0]` to `-1.0`. The fused output sets it to `0.0`. All output fields are inline, with nothing to free.
>
> ```c theme={null}
> int compute_palm_imu(WujiGloveOfflinePipeline *pipeline, const WujiImuData *raw) {
>     WujiImuData fused = {0};
>     WujiStatus status = wuji_glove_offline_compute_imu_data_palm(pipeline, raw, &fused);
>     if (status != WUJI_STATUS_OK) {
>         fprintf(stderr, "IMU compute failed: %s\n", wuji_last_error());
>         return 1;
>     }
>     printf("Orientation: %g, %g, %g, %g\n", fused.orientation.x, fused.orientation.y,
>            fused.orientation.z, fused.orientation.w);
>     return 0;
> }
> ```

Input and output use the same data type. The fused output has `orientation_covariance[0]` set to `0.0`.

| Output | Content |
| - | - |
| [ImuData](https://docs.wuji.tech/docs/en/wuji-glove/latest/sdk-data-reference/imu.md#imudata) (`WujiImuData` in C) | `orientation` contains the fused quaternion alongside the original angular velocity and linear acceleration |

## Example

These programs use a synthetic right-hand EMF frame and a raw palm IMU sample. They create a pipeline, compute hand outputs and IMU orientation, and release resources. No device connection is needed.

For real data, use an MCAP file recorded from a Wuji Glove with both `emf_poses` and `imu_raw/palm` channels. Follow the [replay code](https://docs.wuji.tech/docs/en/wuji-glove/latest/sdk-practice/offline-replay.md#replay-code) to rebuild the raw frames and submit them one by one. Check the serial number against the channel name. Record the hand side and URDF used during capture separately.

> [tab] Python
>
> Save the program as `offline_pipeline.py`, then run `python offline_pipeline.py`.
>
> ```python theme={null}
> from wuji_sdk import (
>     EmfPose, EmfPoseArray, FrameHeader, ImuData,
>     Pose, Quaternion, Vector3F64, WujiGlove,
> )
>
> # Create the pipeline. For replay, use the capture device's serial number and hand side.
> pipeline = WujiGlove.offline_pipeline(sn="WujiGlove-12345", hand_side="right")
>
> # Construct one EMF frame with five finger poses.
> emf = EmfPoseArray(
>     header=FrameHeader(seq=1, timestamp_us=1000000, frame_id="r_hand_emf_tx"),
>     poses=[
>         EmfPose(
>             pose=Pose(position=[x, 0.0, 0.0],
>                       orientation=Quaternion(0.0, 0.0, 0.0, 1.0)),
>             confidence=1.0,
>         )
>         for x in (0.00, 0.03, 0.07, 0.11, 0.15)
>     ],
> )
>
> # Compute joint angles, fingertip poses, and skeleton from the same frame.
> angles = pipeline.hand_joint_angles().compute(emf)
> tips = pipeline.tip_poses().compute(emf)
> skeleton = pipeline.hand_skeleton().compute(emf)
> print(f"Fingers: {len(angles.fingers)}, tips: {len(tips.poses)}, "
>       f"skeleton joints: {len(skeleton.joints)}")
>
> # Construct an unfused palm IMU sample and compute its orientation.
> raw_imu = ImuData.raw(
>     header=FrameHeader(seq=1, timestamp_us=1000000, frame_id="imu_raw/palm"),
>     angular_velocity=Vector3F64(0.1, 0.2, 0.3),
>     linear_acceleration=Vector3F64(0.0, 0.0, 9.8),
> )
> fused = pipeline.imu_data_palm().compute(raw_imu)
> q = fused.orientation
> print(f"Orientation: {q.x}, {q.y}, {q.z}, {q.w}")
> ```

> [tab] C
>
> Save the C code below as `offline_pipeline.c`. Put the extracted C SDK package root from the same release in the program's directory and name it `sdk`. It should contain `include/` and `lib/`.
>
> ```c theme={null}
> #include "wuji_sdk.h"
> #include <stdio.h>
>
> static int check(WujiStatus status) {
>     if (status == WUJI_STATUS_OK) return 1;
>     fprintf(stderr, "SDK error: %s\n", wuji_last_error());
>     return 0;
> }
>
> int main(void) {
>     if (!check(wuji_init(NULL))) return 1;
>     int result = 1;
>     WujiGloveOfflinePipeline *pipeline = NULL;
>     WujiHandJointAngles angles = {0};
>     WujiFingertipPoses tips = {0};
>     WujiHandSkeleton skeleton = {0};
>
>     // Create the pipeline. For replay, use the capture device's serial number and hand side.
>     if (!check(wuji_glove_offline_create(
>             "WujiGlove-12345", WUJI_HANDEDNESS_RIGHT, NULL, &pipeline))) goto cleanup;
>
>     // Construct one EMF frame with five finger poses.
>     WujiEmfPose poses[5] = {0};
>     const float x[5] = {0.00f, 0.03f, 0.07f, 0.11f, 0.15f};
>     for (size_t i = 0; i < 5; ++i) {
>         poses[i].pose.position[0] = x[i];
>         poses[i].pose.orientation.w = 1.0f;
>         poses[i].confidence = 1.0f;
>     }
>     WujiEmfPoseArray emf = {
>         .header = {.seq = 1, .timestamp_us = 1000000, .frame_id = "r_hand_emf_tx"},
>         .poses = poses,
>         .poses_len = 5,
>     };
>
>     // Compute joint angles, fingertip poses, and skeleton from the same frame.
>     if (!check(wuji_glove_offline_compute_hand_joint_angles(pipeline, &emf, &angles)))
>         goto cleanup;
>     if (!check(wuji_glove_offline_compute_tip_poses(pipeline, &emf, &tips))) goto cleanup;
>     if (!check(wuji_glove_offline_compute_hand_skeleton(pipeline, &emf, &skeleton)))
>         goto cleanup;
>     printf("Fingers: %zu, tips: %zu, skeleton joints: %zu\n",
>            angles.fingers_len, tips.poses_len, skeleton.joints_len);
>
>     // Construct an unfused palm IMU sample and compute its orientation.
>     WujiImuData raw_imu = {
>         .header = {.seq = 1, .timestamp_us = 1000000, .frame_id = "imu_raw/palm"},
>         .orientation = {.w = 1.0},
>         .orientation_covariance = {-1.0},
>         .angular_velocity = {.x = 0.1, .y = 0.2, .z = 0.3},
>         .linear_acceleration = {.z = 9.8},
>     };
>     WujiImuData fused = {0};
>     if (!check(wuji_glove_offline_compute_imu_data_palm(pipeline, &raw_imu, &fused)))
>         goto cleanup;
>     printf("Orientation: %g, %g, %g, %g\n", fused.orientation.x,
>            fused.orientation.y, fused.orientation.z, fused.orientation.w);
>     result = 0;
>
> cleanup:
>     // Free the outputs and the pipeline.
>     wuji_hand_joint_angles_free(&angles);
>     wuji_fingertip_poses_free(&tips);
>     wuji_hand_skeleton_free(&skeleton);
>     wuji_glove_offline_free(pipeline);
>     wuji_shutdown();
>     return result;
> }
> ```
>
> The command below compiles `offline_pipeline.c` against the C SDK header and shared library, then runs the program.
>
> ```bash theme={null}
> SDK="$PWD/sdk"
> cc offline_pipeline.c -I "$SDK/include" -L "$SDK/lib" \
>   -Wl,-rpath,"$SDK/lib" -lwuji_sdk_c -o offline_pipeline &&
> ./offline_pipeline
> ```

Both programs should print `Fingers: 5, tips: 5, skeleton joints: 21` and an `Orientation` with four components. IMU input determines the orientation values. Hand output values also depend on the hand model. A warning about the built-in model is expected for the default SDK user. This step confirms that the solving API runs. For MCAP reading, frame-by-frame replay, and a measured comparison with online solving, see [SDK Usage Practices: Replaying Data Offline](https://docs.wuji.tech/docs/en/wuji-glove/latest/sdk-practice/offline-replay.md).

## Usage Notes

### Select and Verify the Hand Model

Hand outputs depend on the calibration model. Both languages select the URDF using the same rules:

1. **Explicit Path**: use `urdf_path` when supplied. A read failure returns an error, without a fallback
2. **Managed Model**: without an explicit path, use the current SDK user’s custom `calibration.hand_model_path` if it points outside the SDK-managed directory. Otherwise, use that user’s `left_hand.urdf` or `right_hand.urdf`. These are alternatives, not fallbacks for each other
3. **Built-In Model**: if the selected managed file is missing, unreadable, or named for the other hand, use the built-in default URDF and log a warning

> [!WARNING]
>
> The default SDK user skips managed models and uses the built-in model unless you specify a path. To match the model used during capture, switch to that named user or specify its URDF. See [Calibration](https://docs.wuji.tech/docs/en/wuji-glove/latest/sdk-data-reference/calibration.md). The built-in model can reduce hand-output accuracy. URDF selection doesn’t affect IMU fusion.

You can solve frames immediately after creating the pipeline. No query is required. The SDK may fall back to the built-in URDF if a managed model is unavailable. To confirm the model it selected, read `urdf_source` and `urdf_source_path`. You can also read back the serial number and hand side.

> [tab] Python
>
> `urdf_source` is `"override"`, `"calibration_file"`, or `"builtin_default"`. The built-in model’s `urdf_source_path` is `None`.
>
> ```python theme={null}
> print(pipeline.sn)
> print(pipeline.hand_side)
> print(pipeline.urdf_source)
> print(pipeline.urdf_source_path)
> ```

> [tab] C
>
> The source is `WUJI_OFFLINE_URDF_SOURCE_OVERRIDE`, `WUJI_OFFLINE_URDF_SOURCE_CALIBRATION_FILE`, or `WUJI_OFFLINE_URDF_SOURCE_BUILTIN_DEFAULT`. Serial numbers and file paths support a two-call size query, with `needed` including NUL. A missing or short file-path buffer returns `WUJI_STATUS_ERR_BUFFER_TOO_SMALL`. For the built-in model, the size query returns `WUJI_STATUS_OK` with `needed=1`, and writes an empty string when space is available. This function reserves 65 bytes for the serial number and allocates the path buffer from its required size. Call `print_pipeline_info(pipeline)` after creating the pipeline.
>
> ```c theme={null}
> int print_pipeline_info(const WujiGloveOfflinePipeline *pipeline) {
>     char sn[65];
>     WujiHandedness hand_side;
>     WujiOfflineUrdfSource source;
>     WujiStatus status = wuji_glove_offline_serial_number(pipeline, sn, sizeof(sn), NULL);
>     if (status == WUJI_STATUS_OK)
>         status = wuji_glove_offline_handedness(pipeline, &hand_side);
>     if (status == WUJI_STATUS_OK)
>         status = wuji_glove_offline_urdf_source(pipeline, &source);
>     if (status != WUJI_STATUS_OK) {
>         fprintf(stderr, "Query failed: %s\n", wuji_last_error());
>         return 1;
>     }
>     size_t needed = 0;
>     status = wuji_glove_offline_urdf_source_path(pipeline, NULL, 0, &needed);
>     if (status != WUJI_STATUS_OK && status != WUJI_STATUS_ERR_BUFFER_TOO_SMALL) {
>         fprintf(stderr, "Path query failed: %s\n", wuji_last_error());
>         return 1;
>     }
>     char *path = malloc(needed);
>     if (path == NULL) return 1;
>     status = wuji_glove_offline_urdf_source_path(pipeline, path, needed, NULL);
>     if (status == WUJI_STATUS_OK)
>         printf("SN: %s, hand: %s, URDF source: %d, path: %s\n", sn,
>                hand_side == WUJI_HANDEDNESS_LEFT ? "left" : "right", (int)source, path);
>     else
>         fprintf(stderr, "Path query failed: %s\n", wuji_last_error());
>     free(path);
>     return status == WUJI_STATUS_OK ? 0 : 1;
> }
> ```

### Process Frames in Capture Order

Submit frames in capture order. Each pipeline retains solver state and caches only the most recent hand frame for hand computations. Consecutive calls with the same hand frame reuse its result. Submitting frames A, B, and A in that order recomputes A.

Use a separate pipeline for each independent stream, and process one pipeline serially. Create a new pipeline to restart playback or change the calibration model.

Offline and online processing share the same solvers. Use the same input frames, calibration model, and processing order when comparing results.

### Check Errors and Release Resources

> [tab] Python
>
> Failed calls raise exceptions. Python manages result objects, so no manual release is needed. Use the pipeline and its resource objects on the thread that created them. Create a new `pipeline` for another replay.
>
> An invalid `hand_side` raises `WujiException`. An unreadable explicit URDF or a failed computation reports its cause through an exception.

> [tab] C
>
> Check `WujiStatus` after each call. On failure, read `wuji_last_error()` before cleanup. Free the pipeline after all computations finish. Don’t free the handle while another call uses it, and don’t free it twice.
>
> ```c theme={null}
> wuji_glove_offline_free(pipeline);
> pipeline = NULL;
> wuji_shutdown();
> ```
>
> * NULL required pointers, an invalid hand side, or a pose count other than five return `WUJI_STATUS_ERR_INVALID_ARG`
> * An invalid serial number, an unreadable explicit URDF, or an internal solver failure returns `WUJI_STATUS_ERR_INTERNAL`. Free and recreate the pipeline after an internal error
>
> Failed hand computations leave outputs unchanged. Before reusing a result, release its old value with the matching `wuji_hand_joint_angles_free`, `wuji_fingertip_poses_free`, or `wuji_hand_skeleton_free`. These functions accept `NULL` and can safely free a cleared structure again. Don’t pass borrowed subscription callback data.


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