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

# 手部模型标定

<Info>
  本页覆盖手部模型标定的 SDK 接口，用 Wuji CLI 完成同类标定见 [手部模型标定](/docs/wuji-cli/hand-model-calibration/)。另一类标定——[触觉标定](/docs/wuji-cli/tactile-calibration/)——训练触觉接触检测模型，通过 Wuji CLI 或 SDK 标定接口完成，与手部模型标定相互独立。
</Info>

Wuji Glove 用 EMF 解算手部关节角度前，需要先做一次手部模型标定，生成与穿戴者手型匹配的 URDF 模型。标定产物按 SDK 用户与左右手保存，同一用户下任意同侧手套共享，更换手套无需重新标定。

## 何时需要标定

* 某个 SDK 用户首次使用某一侧手套
* 更换穿戴者，为新用户单独标定

左手套和右手套各自独立标定，互不影响。同一 SDK 用户下换用另一只同侧手套时，直接复用已有标定，无需重标。启用 SDK 用户隔离后，每位用户独立持有自己的标定 URDF，见 [SDK 用户管理](/docs/wuji-sdk/device-params#sdk-用户管理)。

## SDK 用户前提

标定产物归属具名 SDK 用户。默认用户既不保存也不加载标定产物，实时 IK 始终使用内置默认模型。标定前先创建并切换到具名用户：

```python theme={null}
from wuji_sdk import SdkManager

manager = SdkManager.instance()
user = manager.create_user("alice")     # "alice" 是显示名
manager.switch_user(user["user_id"])    # 按 user_id 切换，不是显示名
```

在默认用户下标定或设置自定义 URDF 会报错，错误信息提示先执行 `create_user` 与 `switch_user`。

## 标定流程

标定要求穿戴者按提示完成一组规定动作，SDK 采集 EMF 数据并解算出适配的 URDF。

<Info>
  标定采集期间 SDK 自动把 EMF 链路恢复满速，采集结束（成功、失败或取消）后恢复原输出率。预先用 `set_rate()` 降低 `emf_poses` 输出率不会拉长标定时间。详见 [输出率调节](/docs/wuji-glove/sdk-data-reference/hand-tracking#输出率调节)。
</Info>

### 同步阻塞调用

适合脚本与命令行工具：

```python theme={null}
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()` 在标定结束或超时前不返回，Ctrl+C 延迟到方法返回后才响应。

### 异步调用

`asyncio` 程序使用异步 API，支持标准 cancellation：

```python theme={null}
result = await glove.calibrate(timeout_s=900.0)
```

### 参数

| 参数 | 类型 | 默认 | 说明 |
| - | - | - | - |
| `skip_constraints` | `bool` | `False` | 跳过稳定性和约束检查（仅调试用） |
| `timeout_s` | `float` | `900.0` | 整体超时（秒），必须为有限正数 |
| `on_feedback` | `Callable` 或 `None` | `None` | 实时反馈回调 |

<Warning>
  `hand_profile` 参数已废弃并被忽略。标定始终按统一基准为每一侧生成单个手部模型。传入 `"wujihand"` 或 `"wujihand2"` 会触发 `DeprecationWarning`，传入其他值会报错。请移除该参数。
</Warning>

### 实时反馈

通过 `on_feedback` 回调获取标定过程中的状态：

```python theme={null}
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)
```

`fb` 字典包含当前姿势序号、状态、进度、采集帧数、姿势偏差诊断指标 (`metrics`) 与提示文本 (`hints`) 等字段。

<Info>
  回调内抛出的异常会被 SDK 记录到日志，不会中断标定。
</Info>

## 自定义 URDF 覆盖

`glove.hand_model_path().set(path)` 指定用户提供的外部 URDF 文件，是 `calibration.hand_model_path` 资源的规范访问器，在 URDF 查找顺序中**优先级最高**：

```python theme={null}
# 设置自定义手部 URDF（实时 IK 最高优先级）
glove.hand_model_path().set("/path/to/custom_hand.urdf")

# 读回当前覆盖值
print(glove.hand_model_path().get())
```

设置路径后，在线 IK 数据流（`hand_joint_angles` / `tip_poses` / `hand_skeleton`）按自定义 URDF 重载。传入空路径则回退到当前用户的标定模型。仅 SDK 托管目录之外的路径算作外部覆盖，指向托管目录内的路径不生效。

自定义 URDF 覆盖仅具名 SDK 用户可用。默认用户调用 `set()` 会被拒绝，需先切换到具名用户，与标定产物的用户隔离一致。

## URDF 查找顺序

实时 IK、`tf_static` 加载手部 URDF 的优先级：

1. **外部自定义路径**：`calibration.hand_model_path` 指向 SDK 托管目录之外的文件
2. **当前用户的标定模型**：该 SDK 用户对应左右手的稳定文件 `left_hand.urdf` / `right_hand.urdf`
3. **内置默认 URDF**：按左右手返回 SDK 自带模型

<Warning>
  默认 SDK 用户跳过第 1、2 级，始终使用内置默认模型。要使用标定产物或自定义 URDF，先切换到具名用户。
</Warning>

<Info>
  SDK 不读取任何旧格式产物——旧 profile 路径、旧版本写入的托管 `hand_model_path`、按序列号命名的旧标定文件运行时都不再加载，也没有自动迁移。旧数据重新标定即可。离线管线接口另支持显式传入 `urdf_path`，传参时优先于上述所有来源。
</Info>

## 返回字段

`calibrate()` / `calibrate_blocking()` 返回的标定汇总：

| 字段 | 类型 | 说明 |
| - | - | - |
| `handedness` | `str` | 本次标定的手别（`"left"` 或 `"right"`） |
| `calibrated_urdf` | `str` | 标定生成的稳定模型路径（按用户保存） |
| `poses_collected` | `int` | 采集到的姿势数 |
| `frames_per_pose` | `dict[str, int]` | 姿势名 → 该姿势采集帧数 |
| `sdk_user` | `dict` | 标定当时的 SDK 用户信息（`user_id` / `display_name` / `description` / `is_default`） |

## 标定产物存储

标定生成的稳定文件按 SDK 用户与左右手命名，同一用户下所有同侧手套共享同一模型：

| SDK 用户 | 模型目录 |
| - | - |
| 具名用户 | `~/.wuji/sdk/users/<user_id>/models/left_hand.urdf`、`right_hand.urdf` |

唯一性由 `(user_id, 左右手)` 二元组保证，不再绑定设备序列号。切换 SDK 用户时，已连接设备自动重新加载该用户名下对应的模型，无需重连。写入走原子替换，标定失败不会损坏已有产物。

标定 bundle 采用新格式：手部模型提为用户级，触觉数据仍按设备序列号保存。导入旧版本 bundle 时只保留触觉数据，旧的按序列号标定一律跳过（已失效，不做迁移）。本版本导出的 bundle 需用最新 SDK 导入。

## 从旧版本升级

<Warning>
  升级到本版本后，之前生成的标定 URDF **不再被加载**，实时 IK 与 `tf_static` 回落到内置默认模型。磁盘上的旧标定文件仍保留、但运行时不再读取，需在具名 SDK 用户下**重新标定一次**以恢复个人标定效果。同侧更换手套无需重新标定。
</Warning>

* `calibration.hand_profile` 与 `calibration.hand_model_paths.*`（含 `hand_1` / `hand_2` 别名）参数已移除。对这些路径执行 SET 会报 `PathNotFound`，磁盘上的旧值仍可读取但运行时忽略。
* 标定结果不再含 `device_sn`、`hand_profile`、`active_hand_profile`、`generated_hand_profiles`、`calibrated_urdfs` 字段。改用 `handedness` 与 `calibrated_urdf`。

## 错误处理

| 场景 | 行为 |
| - | - |
| 在默认 SDK 用户下标定或设置自定义 URDF | 报错，信息提示先 `create_user` 与 `switch_user` |
| `timeout_s` 非有限正数 | 立即抛 `WujiException` |
| 标定超时 | 抛 `WujiException`，本轮采集结果不持久化 |
| 标定中途切换 SDK 用户 | 抛 `WujiException`（错误信息含 `SDK user changed during calibration`），本轮采集结果丢弃，旧模型保持原值 |
| URDF 写入失败 | 抛 `WujiException`，**自动回滚本轮新文件**，旧模型保持原值 |
| 标定算法收敛失败 | 抛 `WujiException`，旧模型保持原值 |

<Info>
  所有失败路径都保留**上一次成功的标定结果**，失败的本轮不会污染已有模型，可直接重试。
</Info>

## 资源路径速查

| 资源路径 | 访问 | 说明 |
| - | - | - |
| `calibration.hand_model_path` | GET / SET | 用户自定义外部 URDF 覆盖（优先级最高，规范访问器 `glove.hand_model_path()`） |

<Tabs>
  <Tab title="Python">
    Python：本页前文已展示 Python 接口。
  </Tab>

  <Tab title="C">
    C 侧接口与本页 Python 接口语义一致，函数前缀 `wuji_glove_`。初始化约定、内存管理、错误码与工程构建见 [C 接口参考](/docs/wuji-sdk/data-reference/)。

    ### 在线 IK 自定义手部模型

    `wuji_glove_set_hand_model_path` 设置自定义手部 URDF 路径，`wuji_glove_get_hand_model_path` 读回，对齐 Python `glove.hand_model_path().set(path)` / `.get()`。设置路径后，在线 IK 数据流（`hand_joint_angles` / `tip_poses` / `hand_skeleton`）通过 SDK 标定生成机制按自定义 URDF 重载。getter 采用两步 size 查询——先传 `NULL` 获取所需长度，再次调用填充：

    ```c theme={null}
    // 设置在线 IK 的自定义手部 URDF
    wuji_glove_set_hand_model_path(dev, "/path/to/hand.urdf");

    // 读回（两步 size 查询）
    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);
    ```

    仅具名 SDK 用户可设置自定义手部模型：默认 SDK 用户的在线 IK 始终使用内置默认 URDF，此时调用 `wuji_glove_set_hand_model_path` 返回错误，`wuji_last_error` 会提示先切换到具名用户。路径指向不可读文件时同样立即报错，不写入参数存储。

    ### 手部模型标定

    Wuji Glove 的手部模型标定在 C 侧提供同步与异步两种入口，对齐 Python `glove.calibrate()`。标定按用户隔离，需先切换到具名 SDK 用户，默认用户直接标定会返回错误。

    用 `wuji_glove_calibration_options_default` 取默认选项（`skip_constraints` 是否跳过约束校验、`timeout_s` 超时秒数）。反馈回调收到的 `WujiGloveCalibrationFeedback` 携带当前姿势进度。其中只有 `state` 和 `progress` 必然存在。其余标量字段由配套的 `has_*` 标志位门控——`has_step_index` 为 `true` 时才能读 `step_index`，`step_total`、`step_name`、`hold_elapsed`、`frames_collected` 等同理。`metrics` 与 `hints` 是数组，没有 `has_*` 位，用 `metrics_len` / `hints_len` 判断是否为空。`WujiGloveCalibrationMetric` 内部同样带门控：`has_finger` / `has_finger_b` 为 `true` 时才有 `finger` / `finger_b`，所以指标不一定是按手指给出的。

    异步 session：`start` 开始后用 `try_finish` 轮询或 `wait` 阻塞等待完成，`cancel` 请求协作式取消，最后用 `session_free` 释放句柄。

    ```c theme={null}
    void on_feedback(const WujiGloveCalibrationFeedback* fb, void* user) {
        // fb->state / fb->progress / fb->step_index / fb->metrics ...
        // fb 仅回调期间有效，需要保留的字段必须拷贝
    }

    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 */) 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);   // 轮询间隔约 50 ms
    }
    if (session) wuji_glove_calibration_session_free(session);

    // 仅在启动成功、标定正常完成时读取结果；取消或失败时不要读 result 字段
    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);
    }
    ```

    同步入口 `wuji_glove_calibrate` 一次调用阻塞至标定完成，参数与异步一致，直接写 `WujiGloveCalibrationResult`：

    ```c theme={null}
    WujiGloveCalibrationResult result = {0};
    wuji_glove_calibrate(dev, &options, on_feedback, /*user_data=*/NULL, &result);
    wuji_glove_calibration_result_free(&result);
    ```

    `WujiGloveCalibrationResult` 含采集姿势数 `poses_collected`、每姿势帧数 `frames_per_pose`、手别 `handedness`、标定生成的 URDF 路径 `calibrated_urdf`，以及标定所属用户快照 `sdk_user`。`calibrated_urdf` 是 SDK 分配的本地路径，按不透明值处理，不要依赖其父目录结构。结果使用完毕后必须调用 `wuji_glove_calibration_result_free` 释放堆字段。

    取消是协作式的。`wuji_glove_calibration_cancel` 返回 `WUJI_STATUS_OK` 只代表请求已记录，不代表标定已停下。只有取消在结果发布前生效，本次标定才映射为 `WUJI_STATUS_ERR_CANCELLED`。一旦标定进入发布阶段，它仍可能成功完成或返回发布阶段的错误，因此请以 session 实际返回的状态码为准，不要默认取消后一定得到 `ERR_CANCELLED`。设备不支持标定时映射为 `WUJI_STATUS_ERR_UNSUPPORTED`。
  </Tab>
</Tabs>


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