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

## Overview

How far off are the joint angles the built-in generic model computes for your hand? Swapping models measures up to 58° of difference. Hand model calibration closes that gap: wear the glove through six guided poses and the SDK generates a URDF that matches your hand.

Calibration output is scoped **by SDK user and hand side**, not bound to a particular glove. Switch to another glove of the same side under the same user and it loads the same model, with no need to recalibrate.

Matching examples: `examples/python/wuji_glove/5.calibration.py` and `examples/c/wuji_glove/4_user_calibration.c`.

For when to calibrate, where output is stored, and version requirements, see [Calibration and Configuration](/en/docs/wuji-glove/calibration). This page covers only how to run it and what the measurements show.

<Warning>
  On hardware, a person wearing the glove completed the first four pinch poses, and the fifth pose `four_finger_bend_90` timed out, so no final URDF was produced. The user gate, the six-pose list with each constraint threshold, the feedback structure, the state machine, both timeout error codes, and the real behavior of `skip_constraints` are all measured. The result structure after a successful calibration is described from the API contract.
</Warning>

## Prerequisites

### Hardware

| Item | Requirement |
| - | - |
| Wuji Glove | 1 |
| Connection | Ethernet |
| Wearing | **Someone must wear the glove**, for about 2 to 5 minutes |

Calibration reads sensors and writes an SDK-side model file. It writes no firmware.

### 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 4_user_calibration
    ```
  </Tab>
</Tabs>

### Before You Start

**Switch to a named SDK user.** The default user can't calibrate, as the next section shows. Confirm with `--dry-run` first:

```bash theme={null}
python examples/python/wuji_glove/5.calibration.py --sn <serial number> --dry-run
```

Output:

```text theme={null}
current SDK user:
  user_id:      'u_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
  display_name: alice
  description:  None
  is_default:   False

Calibration results are stored per SDK user and hand side.

Dry run complete: no device connection or calibration was performed.
```

`--dry-run` neither connects nor calibrates. It exists to confirm the user.

## The Default User Can't Calibrate

Running `--dry-run` under the default user, the example warns before any error appears:

```text theme={null}
You are on the default SDK user, which cannot run calibration.
Create and switch to a user profile first, e.g.:
    user = manager.create_user("user1")
    manager.switch_user(user["user_id"])
or run 4.user.py to manage profiles, then re-run this example.
```

Run it for real and the SDK raises with an error code:

```text theme={null}
Error: Execute error [calibrate] (0x4108): Default SDK user cannot run calibrate.
Create a user profile first — e.g. SdkManager.create_user("user1") then
switch_user() — and calibrate again; calibration results are stored per user profile
```

Remember **0x4108**, which means this and only this.

## Data Flow

```mermaid theme={null}
flowchart TD
    USER["named SDK user"]
    CONN["connect(sn=...)"]
    CAL["calibrate(on_feedback=...)"]
    S1["pose 1 pinch_index"]
    S2["poses 2 through 6"]
    W1["waiting_stable<br/>wait for the pose to settle"]
    W2["collecting<br/>capture data"]
    W3["done<br/>this step is finished"]
    W4["waiting_movement<br/>wait for the move to the next pose"]
    URDF["generate URDF<br/>stored per user and hand side"]
    IK["online solver loads it automatically"]

    USER --> CONN
    CONN --> CAL
    CAL --> S1
    S1 --> W1
    W1 --> W2
    W2 --> W3
    W3 --> W4
    W4 --> S2
    S2 --> URDF
    URDF --> IK
```

## Six Guided Poses

`step_total` measures 6, in a fixed order:

| Step | `step_name` | What to do | Criterion |
| - | - | - | - |
| 0 | `pinch_index` | Touch thumb tip to index fingertip | Fingertip distance ≤ 0.05 m |
| 1 | `pinch_middle` | Touch thumb tip to middle fingertip | Same |
| 2 | `pinch_ring` | Touch thumb tip to ring fingertip | Same |
| 3 | `pinch_pinky` | Touch thumb tip to pinky fingertip | Same |
| 4 | `four_finger_bend_90` | Bend four fingers about 90 degrees | Four-finger orientation on three axes, see below |
| 5 | `flat_open` | Straighten all fingers, palm flat, wrist still | Four-finger orientation on three axes, see below |

A person wearing the glove reached `step 4/6`, passing all four pinch poses.

Between poses you have to **actually move your hand**, because the state machine advances on detected motion. See `waiting_movement` below.

### Two Kinds of Criteria

The four pinch poses check exactly **one** metric: the distance between the thumb tip and the matching fingertip, capped at 0.05 m. It's intuitive, and pinching passes.

The last two poses switch to **three-axis orientation constraints on the four fingers**, where roll, pitch, and yaw must all fall inside their ranges:

| Pose | roll | pitch | yaw |
| - | - | - | - |
| `four_finger_bend_90` | −135° to −45° | −38° to +50° | −35° to +40° |
| `flat_open` | −40° to +35° | −30° to +40° | −30° to +50° |

The constraints apply to the **four fingers** and exclude the thumb, so thumb placement doesn't matter.

**Step five is the practical sticking point.** A person wearing the glove burned the full 120 seconds there, receiving `rotate to decrease roll` 43 times, `rotate to increase yaw` 36 times, and `rotate to decrease yaw` 15 times.

`decrease roll` means roll needs to go more negative, which means **bending the fingers further**. Bending to what feels like 40 or 50 degrees falls well short, since the range centers near −90°, so bend until knuckle to fingertip approaches a right angle. Clenching into a fist instead throws yaw off, so keep the four fingers side by side and bend each one without squeezing.

`flat_open` has a much wider range and passes on a natural flat hand.

<Info>
  The source defines 8 poses, the other two being `thumb_to_index_dip` and `index_to_thumb_emf`. The default flow walks 6 of them, with `step_total` always 6.
</Info>

## Feedback Structure

`on_feedback` receives a dict. The full contents at step one:

```json theme={null}
{
  "operation": "calibrate",
  "step_index": 0,
  "step_total": 6,
  "step_name": "pinch_index",
  "state": "waiting_stable",
  "progress": 0.0,
  "hold_elapsed": 0.0,
  "hold_target": 1.0,
  "variance_ok": true,
  "constraints_ok": false,
  "variance": 9.308185333622987e-09,
  "variance_target": 0.00019999999494757503,
  "metrics": [
    {
      "label": "Fingertip distance",
      "finger": "thumb",
      "finger_b": "index",
      "value": 0.0758685,
      "max": 0.05,
      "error": 0.025868502,
      "unit": "m",
      "hint": "move the two fingertips closer"
    }
  ],
  "hints": ["move the two fingertips closer"]
}
```

The fields fall into three groups:

| Group | Fields | Purpose |
| - | - | - |
| Progress | `step_index`, `step_total`, `step_name`, `state`, `progress` | Draw a progress bar |
| Hold criterion | `hold_elapsed`, `hold_target`, `variance`, `variance_target`, `variance_ok` | Is the pose steady enough |
| Constraint criterion | `constraints_ok`, `metrics`, `hints` | Is the pose correct |

Every entry in `metrics` carries its own `label`, `unit`, current `value`, allowed `max`, the `error` by which it exceeds, and a plain-language `hint`. Render them directly rather than writing your own copy.

### The Two Criteria Are Independent

The measurements make this clear. On a glove resting on a desk, unworn:

| Criterion | Measured | Verdict |
| - | - | - |
| Variance | `9.31e-9` against a target of `2.0e-4` | `variance_ok = true`, since resting is as steady as it gets |
| Constraint | Thumb-to-index distance `0.0759 m` against a cap of `0.05 m` | `constraints_ok = false`, since nothing is pinched |

**Steady isn't correct.** Capture begins only when both pass.

## State Machine

Running one round with constraint checking skipped:

```text theme={null}
[   0.4s] step 0/6 pinch_index     state=waiting_stable    progress=0.239
[   1.1s] step 0/6 pinch_index     state=collecting        progress=0.0
[   5.1s] step 0/6 pinch_index     state=done              progress=1.0
[   5.1s] step 1/6 pinch_middle    state=waiting_movement  progress=0.0
```

| `state` | Meaning |
| - | - |
| `waiting_stable` | Waiting for the pose to settle, with `hold_target` measured at 1.0 second |
| `collecting` | Capturing |
| `done` | This step is captured |
| `waiting_movement` | Waiting for you to **move** into the next pose |

A person wearing the glove walked the same flow, with all four pinch poses stepping through `waiting_movement` → `waiting_stable` → `collecting` → `done`, matching the sequence above.

The timing constants in this flow:

| Constant | Value |
| - | - |
| Hold before capture starts | 1.0 second |
| Capture duration per pose | 1.0 second |
| Per-step timeout | 120 seconds |
| Feedback callback interval | 0.3 seconds |

`waiting_movement` is the gate a motionless glove never passes. After `done`, the state machine needs to detect real motion before it accepts that you've started the next pose. A resting glove sits here until it times out.

## Two Separate Timeouts

| Timeout | Trigger | Behavior |
| - | - | - |
| Per-step | One pose doesn't complete within 120 seconds | `WujiException: Execute error [calibrate] (0x4101): Pose 'pinch_middle' timed out after 120s` |
| Overall | Exceeds `timeout_s`, which defaults to 900 seconds | `WujiException: calibrate timed out` |

The 120-second per-step limit is fixed while `timeout_s` sets the overall ceiling. With `timeout_s` at 25 seconds, the overall timeout fires first and raises `WujiException`. At 200 seconds, `pinch_middle` hits its per-step limit first and raises a `WujiException` carrying **0x4101**.

Both timeouts raise `WujiException`. Use the error message to distinguish an overall timeout from a per-step timeout.

## Effects of skip\_constraints

The name sounds harmless, and in practice it turns off pose-correctness checking entirely.

**A glove resting on a desk with nobody wearing it passed all three stages of the first pose under `skip_constraints=True`.** What it captured was a completely wrong "thumb touching index" sample.

That happens because `variance_ok` only measures steadiness, and resting is the steadiest state there is. With constraints off, nothing remains to judge whether the pose is actually correct.

So:

* Use it to debug the flow, verify an integration, or run an automated smoke test
* **Never use it to generate a hand model you intend to keep**, because it quietly produces a garbage model and reports no error at all

## Runtime Handling

| Branch point | Behavior | What to do |
| - | - | - |
| Currently on the default SDK user | `Execute error [calibrate] (0x4108)` | Switch to a named user first |
| Pose is incorrect | `constraints_ok=false` with guidance in `hints` | Show `hints` to the user |
| Step five repeats `decrease roll` | The fingers aren't bent far enough | Bend toward a right angle, since the range centers near −90° |
| Step five repeats `increase/decrease yaw` | The fingers drift sideways, or the hand is clenched | Keep the four fingers side by side and bend each one without squeezing |
| Pose isn't steady | `variance_ok=false` | Hold still until `hold_elapsed` reaches `hold_target` |
| One pose exceeds 120 seconds | `0x4101`, naming the pose | Rerun, or check that the glove is worn properly |
| Exceeds `timeout_s` | `WujiException` | Raise `timeout_s`, which defaults to 900 seconds |
| Using `skip_constraints` | Pose correctness is no longer checked | Debug only |
| `Ctrl+C` mid-calibration | The blocking form defers it until the call returns, the async form cancels promptly | Use `calibrate()` when cancellation matters |
| Calibration doesn't complete | No files are written | After a timeout exit, `~/.wuji/sdk/models/` gained nothing |

The docstring states the cancellation difference between the two forms:

```text theme={null}
Run Wuji Glove hand model calibration from synchronous Python code.

Blocks the calling thread until calibration finishes or times out;
Ctrl+C is deferred until the call returns. Use ``calibrate`` from
asyncio code when responsive cancellation is needed.
```

To let users interrupt at any point, use `await glove.calibrate(...)` rather than `calibrate_blocking`.

## Integration Steps

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

    def on_feedback(fb: dict) -> None:
        step = fb["step_index"] + 1
        # metrics carry label / unit / value / max / hint. Render them directly
        for m in fb.get("metrics", []):
            print(f"  {m['label']}: {m['value']:.4f}{m['unit']} "
                  f"(max {m['max']}{m['unit']}) — {m['hint']}")
        print(f"[{step}/{fb['step_total']}] {fb['step_name']} {fb['state']} "
              f"stable={fb['variance_ok']} pose_ok={fb['constraints_ok']}")

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

        # 1. A named user is required. The default user is rejected with 0x4108
        if manager.current_user()["is_default"]:
            raise RuntimeError("Call switch_user() to move to a named user first")

        # 2. Connect
        devices = [d for d in manager.scan() if d.device_type == DeviceType.WujiGlove]
        glove = manager.connect(sn=devices[0].sn, device_name="my_glove")

        # 3. Calibrate. Use the await form so Ctrl+C cancels promptly.
        #    skip_constraints is for debugging only, so keep it False in production
        result = await glove.calibrate(
            skip_constraints=False,
            timeout_s=900.0,
            on_feedback=on_feedback,
        )
        print(result)

        manager.disconnect_all()

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

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

    /* 1. Initialize */
    WujiInitOptions opts = { .log_level = 2 };
    wuji_init(&opts);

    /* 2. Confirm a named SDK user. The default user is rejected with 0x4108.
     *    The C example calibrates whichever user you're already on and
     *    neither creates nor switches one */
    WujiUserInfo current = {0};
    if (wuji_current_user(&current) == WUJI_STATUS_OK) {
        if (current.is_default) {
            fprintf(stderr, "The default user can't calibrate. Switch with 3_user_management\n");
        }
        wuji_user_info_free(&current);
    }

    /* 3. Connect. Calibration owns one local session and needs no bridge */
    WujiConnectTarget tgt = {
        .kind = WUJI_CONNECT_TARGET_KIND_SN,
        .value = serial_number,
    };
    struct WujiDevice *dev = NULL;
    wuji_connect(&tgt, "my_glove", NULL, &dev);

    /* 4. Calibrate with a pose-prompt callback.
     *    The example's default path is a cooperatively cancellable async flow,
     *    while --blocking runs to completion and ignores Ctrl+C */

    /* 5. Clean up */
    wuji_dev_disconnect(dev);
    wuji_dev_release(dev);
    wuji_shutdown();
    ```
  </Tab>
</Tabs>

## Running It

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    # confirm the user first
    python examples/python/wuji_glove/5.calibration.py --sn <serial number> --dry-run
    # terminal-guided mode
    python examples/python/wuji_glove/5.calibration.py --sn <serial number>
    # callback and result flow only, for wiring into your own UI
    python examples/python/wuji_glove/5.calibration.py --sn <serial number> --mode api
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    ./build/glove/4_user_calibration
    ./build/glove/4_user_calibration --blocking
    ```
  </Tab>
</Tabs>

`--mode terminal`, the default, shows a screen of pose prompts, while `--mode api` prints only the callback and result flow.

The state flow measured unworn with constraints skipped:

```text theme={null}
[   0.4s] step 0/6 pinch_index     state=waiting_stable    progress=0.239
[   1.1s] step 0/6 pinch_index     state=collecting        progress=0.0
[   5.1s] step 0/6 pinch_index     state=done              progress=1.0
[   5.1s] step 1/6 pinch_middle    state=waiting_movement  progress=0.0
calibration ended: WujiException: Execute error [calibrate] (0x4101): Pose 'pinch_middle' timed out after 120s
```

Measured on a right-hand Wuji Glove under a named SDK user. The glove rested unworn, so the flow stopped at motion detection for the second pose.

A successful calibration returns `handedness` and a `calibrated_urdf` path, with the model landing in `~/.wuji/sdk/models/`. After a timeout exit, that directory gained **no new files**, so an incomplete calibration leaves nothing half-written.

## Adapting It

### Confirm the User Before Calibrating

Output is scoped per user, and calibrating under the wrong one overwrites someone else's model without an error. Print one line at the top of your calibration script:

```python theme={null}
print(f"Calibration will write to user: {SdkManager.instance().current_user()['display_name']}")
```

### Switching Gloves Needs No Recalibration

Models are stored by **user and hand side**, not by serial number. Switch to another glove of the same side under the same user and it loads the same model. Switching to the other hand does need a new calibration.

### Display Angles and Hints

`hints` gives a direction (`rotate to decrease roll`) without a magnitude. The person wearing the glove burned 120 seconds on step five precisely because nothing said how far off they were. Render the current value from `metrics` alongside its allowed range and one adjustment gets there:

```text theme={null}
pose✗ steady✓  Roll=-52.3° needs [-135,-45] | Pitch=8.1° needs [-38,50] | Yaw=41.0° needs [-35,40] ✗
```

For a calibration wizard, this is the single most worthwhile piece of effort.

### Show Hints Directly

The `hint` in each `metrics` entry is already plain language (`move the two fingertips closer`), with `label` and `unit` alongside. Render them as they are instead of writing a parallel set of checks.

### Verify the Result

Take a recording made before calibration, run it through the offline pipeline against both the default and the new model, and compare joint angles frame by frame to quantify what changed. See [Replaying Data Offline](/en/docs/wuji-glove/sdk-practice/offline-replay).

### Don't Generate a Real Model With skip\_constraints

Once more: a resting glove passes pose checking under that option. It suits debugging the flow and nothing else.

## Related

* [SDK User Management](/en/docs/wuji-sdk/sdk-practice/user-management/): switch to a named user, the hard requirement for calibration
* [Setting a Custom Hand Model](/en/docs/wuji-glove/sdk-practice/custom-hand-model): quantified impact of swapping models on solver output
* [Replaying Data Offline](/en/docs/wuji-glove/sdk-practice/offline-replay): compare two models over recorded data
* [Tactile Calibration](/en/docs/wuji-glove/sdk-practice/tactile-calibration): the other calibration path, producing a contact model


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