HumansXDocumentation Center 日本語Website ↗
Knowledge Base › Wuji Glove › Manual

Hand Model Calibration

SDK Practice

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 API and field definitions, see Calibration and Configuration.

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.

Prerequisites

Hardware

ItemRequirement
Wuji Glove1
ConnectionEthernet
WearingSomeone 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

ItemRequirement
Operating systemLinux or a Mac with Apple silicon, meeting the system requirements
Wuji SDKv2026.8.31 or later
Python3.10 or later
C toolchainCMake 3.16 or later, plus the extracted SDK package
  • Python

  • C

pip install wuji-sdk
LIB="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 4_user_calibration

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:

python examples/python/wuji_glove/5.calibration.py --sn <serial number> --dry-run

Output:

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.

Data Flow

The Default User Can’t Calibrate

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

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:

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.

Six Guided Poses

step_total measures 6, in a fixed order:

Stepstep_nameWhat to doCriterion
0pinch_indexTouch thumb tip to index fingertipFingertip distance ≤ 0.05 m
1pinch_middleTouch thumb tip to middle fingertipSame
2pinch_ringTouch thumb tip to ring fingertipSame
3pinch_pinkyTouch thumb tip to pinky fingertipSame
4four_finger_bend_90Bend four fingers about 90 degreesFour-finger orientation on three axes, see below
5flat_openStraighten all fingers, palm flat, wrist stillFour-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:

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

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.

Feedback Structure

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

{
  "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:

GroupFieldsPurpose
Progressstep_index, step_total, step_name, state, progressDraw a progress bar
Hold criterionhold_elapsed, hold_target, variance, variance_target, variance_okIs the pose steady enough
Constraint criterionconstraints_ok, metrics, hintsIs 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:

CriterionMeasuredVerdict
Variance9.31e-9 against a target of 2.0e-4variance_ok = true, since resting is as steady as it gets
ConstraintThumb-to-index distance 0.0759 m against a cap of 0.05 mconstraints_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:

[   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
stateMeaning
waiting_stableWaiting for the pose to settle, with hold_target measured at 1.0 second
collectingCapturing
doneThis step is captured
waiting_movementWaiting 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:

ConstantValue
Hold before capture starts1.0 second
Capture duration per pose1.0 second
Per-step timeout120 seconds
Feedback callback interval0.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

TimeoutTriggerBehavior
Per-stepOne pose doesn’t complete within 120 secondsWujiException: Execute error [calibrate] (0x4101): Pose 'pinch_middle' timed out after 120s
OverallExceeds timeout_s, which defaults to 900 secondsWujiException: 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

Integration Steps

Complete examples: Python hand model calibration example and C hand model calibration example.

Reuse imports, manager setup, C initialization, connection error handling, and callbacks from Python example and C example. Check the status of each C call. On failure, release existing resources and exit as shown in the examples.

1. Confirm a Named User

Calibration uses the current SDK user. The default user is rejected, so switch to a named user first.

  • Python

  • C

manager = SdkManager.instance()
if manager.current_user()["is_default"]:
    raise RuntimeError("Switch to a named user first")
WujiUserInfo current = {0};
wuji_current_user(&current);
bool is_default = current.is_default;
wuji_user_info_free(&current);
if (is_default) { wuji_shutdown(); return 2; }

2. Connect and Start Calibration

Connect by serial number, then use the cancellable async API. Keep skip_constraints=False for calibration, and register cancellation handling for Ctrl+C.

  • Python

  • C

glove = manager.connect(sn=serial_number, device_name="my_glove")
options = dict(skip_constraints=False, timeout_s=900.0,
               on_feedback=on_feedback)
WujiGloveCalibrationOptions options;
wuji_glove_calibration_options_default(&options);
options.skip_constraints = false;
options.timeout_s = 900.0;
WujiGloveCalibrationResult result = {0};
/* run_async_calibration is a helper in the C example:
 * start -> poll try_finish -> cancel on a stop signal -> session_free */
WujiStatus st = run_async_calibration(dev, &options, display, &result);
/* On failure, report the error and clean up without reading result. */

3. Read the Result and Release Resources

Wrap the Python calibration call in try/finally so cancellation or an exception also disconnects the device. The C helper releases the calibration session first. After success, read and free the result. Release the device on every exit path.

  • Python

  • C

try:
    result = await glove.calibrate(**options)
    print(result)
finally:
    manager.disconnect_all()
wuji_glove_calibration_result_free(&result);
wuji_dev_disconnect(dev);
wuji_dev_release(dev);
wuji_shutdown();

Running It

  • Python

  • C

# 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
./build/glove/4_user_calibration
./build/glove/4_user_calibration --blocking

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

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

Notes

Branch pointBehaviorWhat to do
Currently on the default SDK userExecute error [calibrate] (0x4108)Switch to a named user first
Pose is incorrectconstraints_ok=false with guidance in hintsShow hints to the user
Step five repeats decrease rollThe fingers aren’t bent far enoughBend toward a right angle, since the range centers near −90°
Step five repeats increase/decrease yawThe fingers drift sideways, or the hand is clenchedKeep the four fingers side by side and bend each one without squeezing
Pose isn’t steadyvariance_ok=falseHold still until hold_elapsed reaches hold_target
One pose exceeds 120 seconds0x4101, naming the poseRerun, or check that the glove is worn properly
Exceeds timeout_sWujiExceptionRaise timeout_s, which defaults to 900 seconds
Using skip_constraintsPose correctness is no longer checkedDebug only
Ctrl+C mid-calibrationThe blocking form defers it until the call returns, the async form cancels promptlyUse calibrate() when cancellation matters
Calibration doesn’t completeNo files are writtenAfter a timeout exit, ~/.wuji/sdk/models/ gained nothing

The docstring states the cancellation difference between the two forms:

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.

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:

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:

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.

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.