Hand Model Calibration
SDK Reference
This page covers the SDK interface for hand model calibration. To run the same calibration with Wuji CLI, see Hand Model Calibration. The other calibration—Tactile Calibration—trains a tactile contact detection model, runs through Wuji CLI or the SDK calibration API, and is independent of hand model calibration.
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.
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:
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.
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.
Synchronous Blocking Call
Best for scripts and command-line tools:
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:
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 |
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.
Real-Time Feedback
Use the on_feedback callback to track calibration state:
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).
Exceptions raised inside the callback are logged by the SDK and do not interrupt calibration.
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:
# 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:
- External custom path:
calibration.hand_model_pathpointing to a file outside the SDK-managed directory - Current user’s calibrated model: the stable per-side files
left_hand.urdf/right_hand.urdffor that SDK user - Built-in default URDF: the SDK’s own model per hand side
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.
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.
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
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.
- The
calibration.hand_profileandcalibration.hand_model_paths.*parameters (including thehand_1/hand_2aliases) were removed. Setting these paths fails withPathNotFound. 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, orcalibrated_urdfs. Usehandednessandcalibrated_urdfinstead.
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 |
Every failure path keeps the last successful calibration. A failed run never corrupts the existing model, so you can retry directly.
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()) |
Python
C
Python: See the Python API examples earlier on this page.
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.
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:
// 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:
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:
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.
Documentation