Tactile Calibration
SDK Practice
Overview
The raw tactile stream gives a z-score per taxel, which doesn’t tell you whether anything touched the glove. Tactile calibration trains a contact model that turns that raw signal into two directly usable streams: tactile_binary for contact or not, and tactile_residual for continuous contact strength.
The model is scoped by SDK user and glove serial number, and once installed it loads automatically at runtime with no path to specify in code.
The flow is: record four guided motions, train, install into the runtime directory. The whole thing takes about 2 to 5 minutes depending on how long you record each motion.
Matching examples: examples/python/wuji_glove/7.tactile_calibration.py and examples/c/wuji_glove/5_tactile_calibration.c.
For API and field definitions, see Calibration and Configuration.
The flow on this page ran end to end, but capture happened on a glove resting on a desk with nobody wearing it, so the trained model is unusable (see the 99.3% dead_pct measurement below). The state machine, prompt protocol, output layout, summary fields, and parameter semantics are all measured. A usable model needs someone wearing the glove through all four motions.
Prerequisites
Hardware
| Item | Requirement |
|---|---|
| Wuji Glove | 1 |
| Connection | Ethernet |
| Wearing | Someone must wear the glove and perform the motions, for the reason below |
Calibration reads sensors and writes an SDK-side model file. It writes no firmware.
Software
| Item | Requirement |
|---|---|
| Operating system | Linux or a Mac with Apple silicon, meeting the system requirements |
| Wuji SDK | v2026.8.31 or later, built with the model-export feature |
| Python | 3.10 or later |
| C toolchain | CMake 3.16 or later, plus the extracted SDK package |
The training step needs an SDK built with model-export. A build without it fails during training.
Python
C
pip install wuji-sdkLIB="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 5_tactile_calibration Before You Start
Nothing required. Tactile calibration and hand model calibration are independent, and each runs on its own.
Data Flow
Four Guided Motions
step_total measures 4, in a fixed order:
| Step | step_name | What to do |
|---|---|---|
| 0 | four_finger_L_thumb_in | Curl four fingers into an L with the thumb tucked into the palm, varying pressure |
| 1 | claw_curl | Curl all five fingers into a claw and flex repeatedly |
| 2 | abduction | Spread and close the fingers repeatedly |
| 3 | finger_to_palm | Press each fingertip to the palm in turn |
Together the four motions must cover the entire tactile array. Any region that never gets pressed is one the trainer treats as dead taxels.
Prompt Protocol
on_pose_prompt is the interaction hub of this flow, firing twice per motion for 8 calls in a full round.
Two Kinds
kind | When | payload fields |
|---|---|---|
pose_ready | Before a motion starts | step_index, step_total, step_name, seconds_per_pose |
pose_review | After a motion is captured | step_index, step_total, step_name, paired_frames, warnings |
pose_ready lets you tell the user to get ready. pose_review reports capture quality for that motion so the user can keep it or redo it.
Only Four Return Values Are Valid
RuntimeError: prompt callback must return 'proceed', 'retry', 'abort', or None | Return | Behavior |
|---|---|
'proceed' or None | Continue |
'retry' | Re-record this motion |
'abort' | Abort the whole calibration |
Anything else raises RuntimeError rather than warning. This is the first thing you’ll hit when integrating.
State Machine
A full round captured 11 state transitions with seconds_per_pose=3.0 and epochs=5:
[ 0.0s] state='collect' step=0/4 four_finger_L_thumb_in
[ 3.0s] state='pose_ok' step=0/4 four_finger_L_thumb_in
[ 3.0s] state='collect' step=1/4 claw_curl
[ 6.1s] state='pose_ok' step=1/4 claw_curl
[ 6.1s] state='collect' step=2/4 abduction
[ 9.1s] state='pose_ok' step=2/4 abduction
[ 9.1s] state='collect' step=3/4 finger_to_palm
[ 12.2s] state='pose_ok' step=3/4 finger_to_palm
[ 12.2s] state='check'
[ 12.2s] state='train'
[ 14.0s] state='done' state | Fields carried | Meaning |
|---|---|---|
collect | collect_target, collect_elapsed, frames_collected | Recording this motion |
pose_ok | frames_collected | This motion is captured |
check | None | Overall validation after capture |
train | epoch, epoch_total, best_val | Training |
done | None | Finished |
check and train carry no step_index, because neither stage has a step number. Handle that when drawing a progress bar rather than reading step_index unconditionally.
Capture time tracks the parameter exactly: four motions at 3.0 seconds each measured 394 frames per motion. 131 Hz x 3 s = 393, a precise match.
What an Unworn Glove Costs: dead_pct 99.3%
Running a full round on a glove resting on a desk with nobody wearing it produced this summary:
{
"alive_taxels": 5,
"dead_pct": 99.32796,
"best_val_loss": -1.5158646,
"epochs": 5,
"frames_per_pose": {
"four_finger_L_thumb_in": 394,
"claw_curl": 394,
"abduction": 394,
"finger_to_palm": 394
},
"installed": false,
"poses_collected": 4,
"verified_alive_taxels": null
} Of 744 taxels only 5 counted as alive, with 99.3% marked dead. The flow ran green all the way to done with no error and wrote its output normally, yet the model is unusable.
The reason is direct: the trainer decides a taxel is alive by whether its signal changed meaningfully during the motions. On a resting glove nothing moves, so everything is marked dead.
The operating rule this gives you: dead_pct is the first measure of calibration quality. Read it before anything else in the summary. A high value means insufficient coverage, whether from not wearing the glove, motions that are too small, too little pressure, or regions never reached.
For a sense of the healthy range, compare against the raw array: 526 of the 744 channels are valid and the remaining 218 are hardware-masked. A good calibration should put alive_taxels near 526.
Output Layout
Calibration data is stored per user and serial number:
~/.wuji/sdk/users/<user_id>/tactile/<serial number>/
├── calibration-data/
│ └── run_<timestamp>/
│ ├── step1_four_finger_L_thumb_in.bag
│ ├── step2_claw_curl.bag
│ ├── step3_abduction.bag
│ ├── step4_finger_to_palm.bag
│ └── trained_model/
│ ├── contact.safetensors
│ ├── contact.npz
│ └── contact.json
└── model/ ← install=True puts the model here The default SDK user uses the shared root ~/.wuji/sdk/tactile/<serial number>/, while named users each get their own space under users/<user_id>/.
contact.json is a manifest carrying contract and checksum information:
{
"format_version": 1,
"contract": { "tactile_pixels": 744, "state_dim": 40 },
"entries": {
"contact.npz": { "sha256": "...", "bytes": 8764 },
"contact.safetensors": { "sha256": "...", "bytes": 1835448 }
},
"provenance": { "device_sn": "WG1KA00XXXXXXXXX", ... }
} tactile_pixels: 744 matches the raw tactile stream’s channel count, so the model trains against the complete array.
With install=False the model stays in the run directory and the runtime directory is never created. The summary reports installed as false and no model/ directory exists. Pass it when you want to evaluate before committing.
One calibration’s raw data measures about 6.6 MB across four bags plus the model. Each run adds another run_<timestamp> directory, and old ones are never cleaned up.
Parameters
Full signature:
calibrate_tactile_blocking(*, seconds_per_pose=30.0, epochs=60, install=True,
sensitivity=None, timeout_s=1800.0,
on_feedback=None, on_pose_prompt=None) | Parameter | Default | Note |
|---|---|---|
seconds_per_pose | 30.0 | How long to record each motion. 3.0 seconds measured 394 frames |
epochs | 60 | Training rounds. 5 epochs measured about 1.8 seconds |
install | True | Whether to install into the runtime directory |
sensitivity | None | Contact sensitivity in [0.5, 3.0], where above 1 is more sensitive |
timeout_s | 1800.0 | Overall timeout |
on_feedback | None | State callback |
on_pose_prompt | None | Interaction callback |
At defaults, a round is four 30-second captures plus 60 training epochs. Shrinking those only suits flow verification, and real calibration should keep the defaults, because capture duration directly determines coverage.
Before and After Calibration
Sampling all three streams at the same moment before calibration:
| Stream | Length | Invalid channels | Non-zero positives |
|---|---|---|---|
tactile | 744 | 218 | 305 |
tactile_binary | 744 | 218 | 0 |
tactile_residual | 744 | 218 | 0 |
The raw stream carries data as usual while both derived streams are exactly zero, not merely close to it. So to tell whether a glove has been calibrated, check whether those two streams are all zeros instead of digging through directories.
Integration Steps
Complete examples: Python tactile calibration example and C tactile 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. Connect to the Target Glove
Connect using the target glove’s serial number. Python example explicitly disables bridging. C example uses the default connection options.
Python
C
options = ConnectOptions(enable_bridge=False)
glove = manager.connect(sn=serial_number, device_name="my_glove", options=options)WujiConnectTarget target = { .kind = WUJI_CONNECT_TARGET_KIND_SN, .value = serial_number };
struct WujiDevice *dev = NULL;
wuji_connect(&target, "glove", NULL, &dev); 2. Run Guided Calibration
Use the default 30 seconds per pose and the training epoch count shown below. The pose prompt callback waits for confirmation, and the feedback callback shows capture and training status.
Python
C
options = dict(seconds_per_pose=30.0, epochs=60, install=True,
on_feedback=on_feedback, on_pose_prompt=on_pose_prompt)WujiTactileCalibrationOptions options = {
.seconds_per_pose = 30.0, .epochs = 60, .install = true,
.timeout_s = 1800.0,
};
WujiTactileCalibrationCallbacks callbacks = {
.on_feedback = on_feedback, .on_pose_prompt = on_pose_prompt,
};
WujiTactileCalibrationSummary summary = {0};
WujiStatus st = wuji_glove_calibrate_tactile_blocking(dev, &options, &callbacks, &summary);
/* On failure, report the error and clean up without reading summary. */ 3. Check Quality and Clean Up
Wrap the previous call in try/finally, read dead_pct and installation status, then disconnect. After a successful C call, free the summary before disconnecting. The pose prompt callback must handle pose_ready and pose_review and return only proceed, retry, abort, or None.
Python
C
try:
result = glove.calibrate_tactile_blocking(**options)
print(result["dead_pct"], result["installed"])
finally:
manager.disconnect_all()printf("dead_pct=%.2f installed=%s\n", summary.dead_pct,
summary.installed ? "true" : "false");
wuji_tactile_calibration_summary_free(&summary);
wuji_dev_disconnect(dev);
wuji_dev_release(dev);
wuji_shutdown(); Running It
Python
C
python examples/python/wuji_glove/7.tactile_calibration.py --sn <serial number>
# train without installing, to evaluate quality first
python examples/python/wuji_glove/7.tactile_calibration.py --sn <serial number> --no-install./build/glove/5_tactile_calibration The example opens by printing this round’s parameters:
========================================================================
Wuji Glove tactile calibration
========================================================================
serial number: <serial number>
seconds per pose: 10.0
epochs: 60
install model: True
You will be asked to confirm before each motion and then keep, redo, or abort after it. Result of a full round with seconds_per_pose=3.0, epochs=5, install=False, glove unworn:
{
"alive_taxels": 5,
"dead_pct": 99.32796,
"best_val_loss": -1.5158646,
"epochs": 5,
"frames_per_pose": {
"abduction": 394,
"claw_curl": 394,
"finger_to_palm": 394,
"four_finger_L_thumb_in": 394
},
"installed": false,
"poses_collected": 4,
"verified_alive_taxels": null
} Measured on a right-hand Wuji Glove under a named SDK user. The glove rested on a desk unworn, which is why dead_pct reached 99.3%. All output from this run was deleted afterward and nothing was installed into the runtime directory.
Notes
| Branch point | Behavior | What to do |
|---|---|---|
| Prompt callback returns an invalid value | RuntimeError: prompt callback must return 'proceed', 'retry', 'abort', or None | Return only one of those four |
| Glove unworn, or motions too small | The flow completes normally with dead_pct near 100% | Judge by dead_pct and recalibrate |
| One motion captured badly | warnings in pose_review says so | Return 'retry' from the callback to re-record it |
SDK built without model-export | Training fails | Use a build that includes the feature |
Exceeds timeout_s | Aborts on timeout | The 1800-second default covers four 30-second motions |
install=False | The model stays in the run directory with installed: false | Decide manually after evaluating |
| Before calibration | tactile_binary and tactile_residual are exactly zero | See the next section |
Adapting It
dead_pct Is the Number to Read First
Finishing without an error doesn’t mean the calibration worked. Read dead_pct, then check whether alive_taxels lands near 526. If either looks wrong, recalibrate before installing.
Use pose_review Warnings to Decide on a Retry
Every motion ends with a pose_review whose warnings apply to that motion alone. Returning 'retry' on the spot costs far less than finishing the round and starting over.
Don’t Shrink seconds_per_pose for Speed
Capture duration directly determines array coverage. The 30-second default exists for a reason, and 3 seconds suits nothing beyond checking that your code runs.
Clean Up Run Directories
Each calibration creates a run_<timestamp> directory of about 6.6 MB and nothing removes them. After several rounds, clear them by hand.
Check the Grid After Calibrating
Once the model is installed, run the contact grid view and touch the glove to confirm it responds. That’s far more direct than reading numbers. See Tactile Contact Grid View.
Related
- Tactile Contact Grid View: verify the result after calibrating
- Tactile Residual Grid View: the continuous signal behind a contact verdict
- Subscribing With Callbacks: where 744, 526, and 218 come from
- Hand Model Calibration: the other calibration path, producing a hand model
- SDK User Management: calibration output is scoped per user
Documentation