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

# Hand Model Calibration

<Info>
  This page covers the SDK interface for hand model calibration. To run the same calibration with Wuji CLI, see [Hand Model Calibration](/en/docs/wuji-cli/hand-model-calibration/). The other calibration—[Tactile Calibration](/en/docs/wuji-cli/tactile-calibration/)—trains a tactile contact detection model, runs through Wuji CLI or the SDK calibration API, and is independent of hand model calibration.
</Info>

Wuji Glove uses EMF data to solve finger-joint angles, which first requires a hand model calibration to generate a URDF model matched to the wearer's hand. Calibration output belongs to the SDK user and hand side. Any glove of the same side under the same user shares it, so swapping gloves needs no recalibration.

## When to Calibrate

* The first time an SDK user uses a glove on a given side
* A new wearer, calibrated under their own user

Left and right gloves calibrate independently. Under the same SDK user, switching to another glove of the same side reuses the existing calibration, no recalibration needed. With SDK user isolation on, each user keeps their own calibration URDF. See [SDK User Management](/en/docs/wuji-sdk/device-params#sdk-user-management).

## SDK User Requirement

Calibration output belongs to a named SDK user. The default user neither stores calibration output nor loads it — real-time IK always uses the built-in default model. Create and switch to a named user before calibrating:

```python theme={null}
from wuji_sdk import SdkManager

manager = SdkManager.instance()
user = manager.create_user("alice")     # "alice" is the display name
manager.switch_user(user["user_id"])    # switch by user_id, not display name
```

Calibrating or setting a custom URDF under the default user raises an error that tells you to run `create_user` and `switch_user` first.

## Calibration Flow

Calibration asks the wearer to follow a set of guided poses while the SDK captures EMF data and solves the matching URDF.

<Info>
  During calibration capture, the SDK runs the EMF chain at full speed and restores the previous output rate when capture ends — whether it succeeds, fails, or is canceled. Lowering the `emf_poses` rate with `set_rate()` beforehand doesn't stretch the calibration time. See [Output Rate Tuning](/en/docs/wuji-glove/sdk-data-reference/hand-tracking#output-rate-tuning).
</Info>

### Synchronous Blocking Call

Best for scripts and command-line tools:

```python theme={null}
from wuji_sdk import SdkManager, Handedness

manager = SdkManager.instance()
glove = manager.connect(handedness=Handedness.Left, device_name="glove")

result = glove.calibrate_blocking(timeout_s=900.0)

print("handedness:", result["handedness"])
print("calibrated urdf:", result["calibrated_urdf"])
print("poses collected:", result["poses_collected"])
print("sdk user:", result["sdk_user"]["display_name"])
```

`calibrate_blocking()` does not return until calibration finishes or times out, and Ctrl+C is deferred until the call returns.

### Asynchronous Call

`asyncio` programs use the async API, which supports standard cancellation:

```python theme={null}
result = await glove.calibrate(timeout_s=900.0)
```

### Parameters

| Parameter | Type | Default | Description |
| - | - | - | - |
| `skip_constraints` | `bool` | `False` | Skip stability and constraint checks (debug only) |
| `timeout_s` | `float` | `900.0` | Overall timeout in seconds, must be a finite positive number |
| `on_feedback` | `Callable` or `None` | `None` | Real-time feedback callback |

<Warning>
  `hand_profile` is deprecated and ignored. Calibration always produces one hand model per side using the canonical baseline. Passing `"wujihand"` or `"wujihand2"` emits a `DeprecationWarning`, and other values raise an error. Remove this argument.
</Warning>

### Real-Time Feedback

Use the `on_feedback` callback to track calibration state:

```python theme={null}
def on_feedback(fb):
    if fb.get("state") == "collecting":
        progress = fb.get("progress", 0.0)
        step = fb.get("step_index", 0)
        total = fb.get("step_total", 0)
        print(f"step {step}/{total}: {progress*100:.0f}%")
    for hint in fb.get("hints", []):
        print(f"hint: {hint}")

result = glove.calibrate_blocking(on_feedback=on_feedback)
```

The `fb` dict carries the current pose index, state, progress, captured frame count, pose-deviation diagnostics (`metrics`), and hint text (`hints`).

<Info>
  Exceptions raised inside the callback are logged by the SDK and do not interrupt calibration.
</Info>

## Custom URDF Override

`glove.hand_model_path().set(path)` points to a user-supplied external URDF file. It's the canonical accessor for the `calibration.hand_model_path` resource and has the **highest priority** in the URDF lookup order:

```python theme={null}
# Set a custom hand URDF (highest real-time IK priority)
glove.hand_model_path().set("/path/to/custom_hand.urdf")

# Read back the current override
print(glove.hand_model_path().get())
```

Setting a path reloads the online-IK streams (`hand_joint_angles`, `tip_poses`, `hand_skeleton`) against the custom URDF. Set an empty path to fall back to the current user's calibrated model. Only paths outside the SDK-managed directory count as an external override — a path inside the managed directory has no effect.

The custom URDF override is available only to a named SDK user. Calling `set()` under the default user is rejected, so switch to a named user first, matching the user isolation of calibration output.

## URDF Lookup Order

Priority for real-time IK and `tf_static` when loading the hand URDF:

1. **External custom path**: `calibration.hand_model_path` pointing to a file outside the SDK-managed directory
2. **Current user's calibrated model**: the stable per-side files `left_hand.urdf` / `right_hand.urdf` for that SDK user
3. **Built-in default URDF**: the SDK's own model per hand side

<Warning>
  The default SDK user skips levels 1 and 2 and always uses the built-in default model. To use a calibrated or custom URDF, switch to a named user first.
</Warning>

<Info>
  The SDK loads no legacy output — old profile paths, managed `hand_model_path` values written by older versions, and per-serial-number calibration files are no longer loaded at runtime, and there's no automatic migration. Recalibrate to regenerate. The offline pipeline API also accepts an explicit `urdf_path`, which takes priority over all of the above when passed.
</Info>

## Return Fields

Summary returned by `calibrate()` / `calibrate_blocking()`:

| Field | Type | Description |
| - | - | - |
| `handedness` | `str` | The calibrated hand side (`"left"` or `"right"`) |
| `calibrated_urdf` | `str` | The stable per-user model path this calibration produced |
| `poses_collected` | `int` | Number of poses captured |
| `frames_per_pose` | `dict[str, int]` | Pose name to captured-frame-count map |
| `sdk_user` | `dict` | SDK user info at calibration time (`user_id` / `display_name` / `description` / `is_default`) |

## Where Calibration Is Stored

The stable files are named by SDK user and hand side. All same-side gloves under one user share the same model:

| SDK User | Model Directory |
| - | - |
| Named user | `~/.wuji/sdk/users/<user_id>/models/left_hand.urdf`, `right_hand.urdf` |

Uniqueness comes from the `(user_id, hand_side)` pair and no longer binds to a device serial number. Switching the SDK user reloads the matching model for connected devices automatically, no reconnect required. Writes use an atomic replace, so a failed calibration never corrupts existing output.

Calibration bundles use a new format: hand models are user-level, tactile data stays per serial number. Importing a bundle from an older SDK version keeps the tactile data but skips the old per-serial-number calibration (no longer loaded, no migration). Bundles exported by this version require an up-to-date SDK to import.

## Upgrading From Older Versions

<Warning>
  After upgrading to this version, previously generated calibration URDFs are **no longer loaded** — real-time IK and `tf_static` fall back to the built-in default model. The old files stay on disk but the runtime ignores them, so recalibrate once under a named SDK user to restore your calibration. Swapping to another glove of the same side needs no recalibration.
</Warning>

* The `calibration.hand_profile` and `calibration.hand_model_paths.*` parameters (including the `hand_1` / `hand_2` aliases) were removed. Setting these paths fails with `PathNotFound`. Values left on disk stay readable but are ignored at runtime.
* The calibration result no longer includes `device_sn`, `hand_profile`, `active_hand_profile`, `generated_hand_profiles`, or `calibrated_urdfs`. Use `handedness` and `calibrated_urdf` instead.

## Error Handling

| Scenario | Behavior |
| - | - |
| Calibrating or setting a custom URDF under the default SDK user | Raises an error telling you to `create_user` and `switch_user` first |
| `timeout_s` is not a finite positive number | Raises `WujiException` immediately |
| Calibration times out | Raises `WujiException`, and this run's capture is not persisted |
| SDK user switches mid-calibration | Raises `WujiException` (message contains `SDK user changed during calibration`), this run's capture is discarded, and the old model is left untouched |
| URDF write fails | Raises `WujiException`, **rolls back this run's new files automatically**, and leaves the previous model untouched |
| Calibration solver fails to converge | Raises `WujiException`, and the previous model is left untouched |

<Info>
  Every failure path keeps the **last successful calibration**. A failed run never corrupts the existing model, so you can retry directly.
</Info>

## Resource Path Reference

| Resource Path | Access | Description |
| - | - | - |
| `calibration.hand_model_path` | GET / SET | User-supplied external URDF override (highest priority, canonical accessor `glove.hand_model_path()`) |

<Tabs>
  <Tab title="Python">
    Python: See the Python API examples earlier on this page.
  </Tab>

  <Tab title="C">
    The C API matches the Python API on this page and uses the `wuji_glove_` prefix. For initialization conventions, memory management, error codes, and build setup, see the [C API Reference](/en/docs/wuji-sdk/data-reference/).

    ### Custom Hand Model for Online IK

    `wuji_glove_set_hand_model_path` sets a custom hand URDF path and `wuji_glove_get_hand_model_path` reads it back, mirroring Python `glove.hand_model_path().set(path)` / `.get()`. Setting a path reloads the online-IK streams (`hand_joint_angles`, `tip_poses`, `hand_skeleton`) against the custom URDF through the SDK's calibration-generation mechanism. The getter uses the two-call size query — pass `NULL` to get the required length, then call again to fill:

    ```c theme={null}
    // Set a custom hand URDF for online IK
    wuji_glove_set_hand_model_path(dev, "/path/to/hand.urdf");

    // Read it back (two-call size query)
    size_t needed = 0;
    wuji_glove_get_hand_model_path(dev, NULL, 0, &needed);
    char *path = malloc(needed);
    wuji_glove_get_hand_model_path(dev, path, needed, NULL);
    printf("hand model: %s\n", path);
    free(path);
    ```

    Only a named SDK user can set a custom hand model: online IK under the default SDK user always uses the built-in default URDF, so calling `wuji_glove_set_hand_model_path` there fails and `wuji_last_error` tells you to switch to a named user first. An unreadable path also fails immediately and is never stored.

    ### Hand Model Calibration

    Wuji Glove hand model calibration offers both synchronous and asynchronous entry points in C, matching Python `glove.calibrate()`. Calibration is per-user, so switch to a named SDK user first — the default user fails calibration outright.

    Get default options with `wuji_glove_calibration_options_default` (`skip_constraints` to skip constraint checks, `timeout_s` for the timeout in seconds). The feedback callback receives a `WujiGloveCalibrationFeedback` with the current pose progress. Only `state` and `progress` are always present. Every other scalar field is guarded by a companion `has_*` flag — read `step_index` only when `has_step_index` is `true`, and the same for `step_total`, `step_name`, `hold_elapsed`, `frames_collected`, and the rest. `metrics` and `hints` are arrays with no `has_*` flag: check `metrics_len` / `hints_len` instead. Each `WujiGloveCalibrationMetric` carries its own guards: a metric has `finger` / `finger_b` only when `has_finger` / `has_finger_b` is `true`, so metrics aren't necessarily per-finger.

    For an async session, `start` begins calibration, then poll with `try_finish` or block with `wait` until it finishes, `cancel` requests cooperative cancellation, and `session_free` releases the handle:

    ```c theme={null}
    void on_feedback(const WujiGloveCalibrationFeedback* fb, void* user) {
        // fb->state / fb->progress / fb->step_index / fb->metrics ...
        // fb is valid only during the callback; copy any fields you need to keep
    }

    WujiGloveCalibrationOptions options;
    wuji_glove_calibration_options_default(&options);

    WujiGloveCalibrationSession* session = NULL;
    WujiStatus st = wuji_glove_calibration_start(dev, &options, on_feedback, /*user_data=*/NULL, &session);

    WujiGloveCalibrationResult result = {0};
    bool done = false;
    while (st == WUJI_STATUS_OK && !done) {
        if (/* Ctrl+C received */) wuji_glove_calibration_cancel(session);
        st = wuji_glove_calibration_try_finish(session, &done, &result);
        if (st != WUJI_STATUS_OK || done) break;
        nanosleep(&(struct timespec){ .tv_nsec = 50 * 1000 * 1000 }, NULL);   // poll interval ~50 ms
    }
    if (session) wuji_glove_calibration_session_free(session);

    // Read the result only when start succeeded and calibration finished cleanly; don't touch result fields on cancel or failure
    if (st == WUJI_STATUS_OK && done) {
        printf("side=%s poses=%u urdf=%s user=%s\n",
               handedness_name(result.handedness), result.poses_collected,
               result.calibrated_urdf, result.sdk_user.display_name);
        wuji_glove_calibration_result_free(&result);
    }
    ```

    The synchronous entry point `wuji_glove_calibrate` blocks in a single call until calibration finishes, takes the same parameters, and writes straight into a `WujiGloveCalibrationResult`:

    ```c theme={null}
    WujiGloveCalibrationResult result = {0};
    wuji_glove_calibrate(dev, &options, on_feedback, /*user_data=*/NULL, &result);
    wuji_glove_calibration_result_free(&result);
    ```

    `WujiGloveCalibrationResult` carries the pose count `poses_collected`, per-pose frame counts `frames_per_pose`, handedness `handedness`, the generated URDF path `calibrated_urdf`, and the owning user snapshot `sdk_user`. Treat `calibrated_urdf` as an opaque SDK-allocated local path and don't depend on its parent-directory layout. Free the result with `wuji_glove_calibration_result_free` when you're done.

    Cancellation is cooperative. `wuji_glove_calibration_cancel` returning `WUJI_STATUS_OK` only means the request was recorded — it doesn't mean calibration stopped. The run maps to `WUJI_STATUS_ERR_CANCELLED` only when the cancellation lands before results are published. Once the run reaches the publishing stage, it can still finish successfully or fail with a publishing error, so always check the status the session actually returns rather than assuming a cancelled run yields `ERR_CANCELLED`. A device that doesn't support calibration maps to `WUJI_STATUS_ERR_UNSUPPORTED`.
  </Tab>
</Tabs>


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