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

# Tactile Calibration

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

## 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 | Ubuntu 22.04 LTS or later |
| 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.

<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 5_tactile_calibration
    ```
  </Tab>
</Tabs>

### Before You Start

Nothing required. Tactile calibration and hand model calibration are independent, and each runs on its own.

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

## Data Flow

```mermaid theme={null}
flowchart TD
    CONN["connect(enable_bridge=False)<br/>calibration owns one local session"]
    P1["on_pose_prompt<br/>kind=pose_ready"]
    COL["state=collect<br/>record for seconds_per_pose"]
    OK["state=pose_ok<br/>reports frames_collected"]
    P2["on_pose_prompt<br/>kind=pose_review"]
    LOOP["loop over four motions"]
    CHK["state=check"]
    TRN["state=train<br/>train by epoch"]
    DONE["state=done"]
    OUT["output written<br/>bags plus model"]
    INST["install=True<br/>copies into the runtime directory"]
    BIN["tactile_binary<br/>and tactile_residual become usable"]

    CONN --> P1
    P1 -->|"proceed"| COL
    COL --> OK
    OK --> P2
    P2 -->|"proceed"| LOOP
    LOOP --> P1
    LOOP --> CHK
    CHK --> TRN
    TRN --> DONE
    DONE --> OUT
    OUT --> INST
    INST --> BIN
```

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

```text theme={null}
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`:

```text theme={null}
[   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:

```json theme={null}
{
  "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:

```text theme={null}
~/.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:

```json theme={null}
{
  "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:

```text theme={null}
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.

## Runtime Handling

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

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

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

    MOTION_TEXT = {
        "four_finger_L_thumb_in": "Curl four fingers into an L, thumb tucked in, varying pressure",
        "claw_curl": "Curl all fingers into a claw and flex repeatedly",
        "abduction": "Spread and close the fingers repeatedly",
        "finger_to_palm": "Press each fingertip to the palm in turn",
    }

    def on_pose_prompt(p: dict):
        kind = p["kind"]
        name = p["step_name"]
        step = p["step_index"] + 1
        if kind == "pose_ready":
            print(f"[{step}/{p['step_total']}] {MOTION_TEXT[name]} "
                  f"(recording {p['seconds_per_pose']:.0f} seconds)")
            input("Press Enter when ready")
            return "proceed"
        # pose_review: this motion is captured, with quality warnings attached
        for w in p["payload"].get("warnings", []):
            print(f"  ! {w}")
        # Only proceed / retry / abort / None are valid. Anything else raises RuntimeError
        return "proceed"

    def on_feedback(fb: dict):
        state = fb["state"]
        if state == "collect":
            print(f"\r  recording {fb.get('collect_elapsed', 0):.1f}/"
                  f"{fb.get('collect_target', 0):.1f}s "
                  f"frames={fb.get('frames_collected', 0)}", end="")
        elif state == "train":
            # train and check carry no step_index. Don't read it unconditionally
            print(f"\r  training epoch {fb.get('epoch')}/{fb.get('epoch_total')}", end="")

    manager = SdkManager.instance()

    # 1. Connect. Calibration owns one local session and needs no multi-process bridge
    devices = [d for d in manager.scan() if d.device_type == DeviceType.WujiGlove]
    glove = manager.connect(sn=devices[0].sn, device_name="my_glove",
                            options=ConnectOptions(enable_bridge=False))

    # 2. Calibrate. Keep the 30-second default for real runs
    summary = glove.calibrate_tactile_blocking(
        seconds_per_pose=30.0,
        epochs=60,
        install=True,
        on_feedback=on_feedback,
        on_pose_prompt=on_pose_prompt,
    )

    # 3. Read dead_pct first. It's the primary quality measure
    print(f"\nalive_taxels={summary['alive_taxels']} "
          f"dead_pct={summary['dead_pct']:.2f}% installed={summary['installed']}")
    if summary["dead_pct"] > 50:
        print("Coverage is insufficient. Check the fit and motion range, then recalibrate")

    manager.disconnect_all()
    ```
  </Tab>

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

    /* 1. Initialize, discover, connect. See the callback subscription page.
     *    Calibration owns one local session, so disable the bridge when connecting */

    /* 2. Run the blocking guided calibration. Pose prompts arrive through a
     *    callback where you tell the user what to do and wait for confirmation */
    WujiTactileCalibrationSummary *summary = NULL;
    if (wuji_glove_calibrate_tactile_blocking(dev, &opts, &summary) != WUJI_STATUS_OK) {
        /* A build without model-export returns WUJI_STATUS_ERR_UNSUPPORTED here */
        fprintf(stderr, "tactile calibration failed: %s\n", wuji_last_error());
    }

    /* 3. Read the summary. The struct carries poses_collected, frames_per_pose,
     *    has_verified_alive_taxels, and verified_alive_taxels */
    if (summary) {
        printf("poses=%u\n", summary->poses_collected);
        for (size_t i = 0; i < summary->frames_per_pose_len; i++) {
            printf("  %s: %u frames\n",
                   summary->frames_per_pose[i].pose_name,
                   summary->frames_per_pose[i].frames);
        }
        /* Return it through the SDK's release function when done */
    }

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

## Running It

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    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
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    ./build/glove/5_tactile_calibration
    ```
  </Tab>
</Tabs>

The example opens by printing this round's parameters:

```text theme={null}
========================================================================
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:

```json theme={null}
{
  "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.

## 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](/en/docs/wuji-glove/sdk-practice/tactile-contact-view).

## Related

* [Tactile Contact Grid View](/en/docs/wuji-glove/sdk-practice/tactile-contact-view): verify the result after calibrating
* [Tactile Residual Grid View](/en/docs/wuji-glove/sdk-practice/tactile-residual-view): the continuous signal behind a contact verdict
* [Subscribing With Callbacks](/en/docs/wuji-glove/sdk-practice/subscribe-callback): where 744, 526, and 218 come from
* [Hand Model Calibration](/en/docs/wuji-glove/sdk-practice/hand-model-calibration): the other calibration path, producing a hand model
* [SDK User Management](/en/docs/wuji-sdk/sdk-practice/user-management/): calibration output is scoped per user


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