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

# 设置自定义手部模型

## 概述

本篇实践介绍如何通过 `hand_model_path` 为当前连接设置自定义手部模型。应用场景包括加载自定义 URDF 和比较不同模型的关节角及骨骼求解结果。

对应的 C 示例为 [2\_set\_hand\_model\_path.c](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_glove/2_set_hand_model_path.c)。Python 侧没有独立示例，但订阅示例的 `--hand-model-path` 参数走的是同一套接口。 接口与参数见 [自定义 URDF 覆盖](/docs/wuji-glove/sdk-data-reference/calibration#自定义-urdf-覆盖)。

## 前置条件

### 硬件

| 项目 | 要求 |
| - | - |
| Wuji Glove | 1 只 |
| 连接方式 | 以太网 |

本示例只设置 SDK 侧参数，不写固件，手套不会动。

### 软件

| 项目 | 要求 |
| - | - |
| 操作系统 | Ubuntu 22.04 LTS 及以上版本 |
| Wuji SDK | v2026.8.31 或更高版本 |
| Python | 3.10 及以上版本 |
| C 工具链 | CMake 3.16 及以上版本，以及解压后的 SDK 包 |
| URDF 文件 | 一份可读的手部模型，手别要与手套一致 |

<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 2_set_hand_model_path
    ```
  </Tab>
</Tabs>

### 前序步骤

**先切到具名 SDK 用户。** 见 [SDK 本地用户管理](/docs/wuji-sdk/sdk-practice/user-management/)。这是前置条件，不是建议。

## 数据流

```mermaid theme={null}
sequenceDiagram
    participant U as 具名 SDK 用户
    participant M as SdkManager
    participant G as WujiGlove
    participant S as 订阅流

    U->>M: current_user()
    M-->>U: 确认不是默认用户
    U->>M: connect(sn=...)
    M-->>U: glove
    U->>G: hand_model_path().set(path)
    G-->>U: 设置成功
    U->>G: hand_model_path().get()
    G-->>U: 读回实际路径
    U->>S: 订阅关节角、骨骼或指端位姿
    S-->>U: 使用该手模计算的帧
    U->>M: disconnect_all()
    Note over G,S: 连接断开后，手模覆盖不再保留
```

## 两条硬约束实测

### 一、默认用户下直接报错

切到默认用户后调用，错误信息把原因和解法都写清楚了：

```text theme={null}
wuji_glove_set_hand_model_path failed: Unsupported operation: the default SDK
user does not support a custom hand_model_path: online IK always uses the
built-in default URDF. Switch to a named SDK user (SdkManager::switch_user)
before setting hand_model_path
```

默认用户下在线求解**永远**用内置通用手模，没有例外。

### 二、设置不跨连接

完整实测序列，全程在同一个具名用户下：

| 步骤 | 结果 |
| - | - |
| 1. 连上手套，读 `hand_model_path` | 抛 `WujiException: Parameter not set: calibration.hand_model_path` |
| 2. `set(path)` 后在同一会话读回 | 返回刚设的路径 |
| 3. `disconnect_all()` 后重新 `connect()` | 再读又是 `Parameter not set` |
| 4. 检查磁盘上的参数文件 | 修改时间完全没变 |

结论是 `hand_model_path` 存在会话级参数里，**不落盘**。每建立一次连接都要重新设一次，写成连接后的固定步骤。

<Warning>
  未设置时 `get()` 抛异常，不是返回空字符串或 `None`。判断「当前是否设置自定义手模」要用 try/except，不能靠真假值判断。
</Warning>

## URDF 来源的三种取值

离线管线的 `urdf_source` 属性实测有这些取值，在线侧同理：

| 取值 | 含义 | `urdf_source_path` |
| - | - | - |
| `builtin_default` | 内置通用手模 | `None` |
| `override` | 显式指定了路径 | 指定的那个路径 |
| 标定产物 | 该序列号标定过，用 `~/.wuji/sdk/models/<序列号>_hand.urdf` | 该文件路径 |

## 换手模的影响有多大

拿同一批 200 帧真实电磁位姿数据，分别用内置默认手模和一份来自另一只手套的标定手模求解，逐元素比较：

| 对比项 | 结果 |
| - | - |
| 关节角最大偏差 | 1.0140 rad = 58.10° |
| 关节角平均偏差 | 0.1395 rad = 7.99° |
| 骨骼关节位置最大偏差 | 39.25 mm |

按手指拆开看最大偏差：

| 手指 | 最大偏差 |
| - | - |
| 拇指 | 58.10° |
| 食指 | 20.78° |
| 中指 | 20.34° |
| 无名指 | 31.26° |
| 小指 | 24.47° |

**拇指最敏感。** 它的自由度最多，手模几何的差异被放大得最明显。

这组数字说明手模不是可有可无的配置项。用错手模，求解结果看上去正常、置信度也不低，但和真实手势差出半个关节的角度。重放别人采的数据时尤其要对上手模。

## C 侧的缓冲区两段式查询

`wuji_glove_get_hand_model_path` 用的是 C 里常见的两段式模式，示例封装得很规整，值得照抄：

```c theme={null}
static int print_hand_model_path(struct WujiDevice *dev) {
    /* 第一段：传 NULL 和 0 问需要多大缓冲区。
     * 注意成功的返回值是 ERR_BUFFER_TOO_SMALL，不是 OK */
    size_t needed = 0;
    WujiStatus st = wuji_glove_get_hand_model_path(dev, NULL, 0, &needed);
    if (st != WUJI_STATUS_ERR_BUFFER_TOO_SMALL || needed == 0) {
        fprintf(stderr, "size query failed: %s\n", wuji_last_error());
        return -1;
    }

    /* 第二段：按需分配后真正取值 */
    char *buf = (char *)malloc(needed);
    if (!buf) return -1;

    st = wuji_glove_get_hand_model_path(dev, buf, needed, &needed);
    if (st != WUJI_STATUS_OK) {
        fprintf(stderr, "get failed: %s\n", wuji_last_error());
        free(buf);
        return -1;
    }

    printf("Read back hand_model_path = %s\n", buf);
    free(buf);
    return 0;
}
```

**第一段的「成功」是 `WUJI_STATUS_ERR_BUFFER_TOO_SMALL`。** 按 `== WUJI_STATUS_OK` 判断会永远走进错误分支。

## 集成步骤

完整示例：[Python 订阅回调](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/0.subscribe_callback.py) 和 [C 手模设置](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_glove/2_set_hand_model_path.c)。以下片段只保留接入顺序。

以下为接入片段。Python 导入与 manager 初始化、C 初始化与连接错误分支、回调定义均沿用 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/0.subscribe_callback.py) 和 [C 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_glove/2_set_hand_model_path.c)。C 每次调用应检查返回状态，失败时按 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/0.subscribe_callback.py) 和 [C 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_glove/2_set_hand_model_path.c) 释放已有资源并退出。

### 1. 确认用户并检查 URDF

自定义手模需要具名 SDK 用户。连接前将路径展开为绝对路径，并确认文件可读。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from pathlib import Path
    user = manager.current_user()
    if user["is_default"]:
        raise RuntimeError("先切换到具名 SDK 用户")
    urdf = Path(path).expanduser().resolve()
    if not urdf.is_file():
        raise FileNotFoundError(urdf)
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    WujiUserInfo user = {0};
    wuji_current_user(&user);
    bool is_default = user.is_default;
    wuji_user_info_free(&user);
    if (is_default) { wuji_shutdown(); return 2; }
    if (access(urdf_path, R_OK) != 0) return 2;
    ```
  </Tab>
</Tabs>

### 2. 连接并设置手模

每次连接后都要重新设置路径。设置成功后读取一次确认实际生效的路径。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    glove = manager.connect(sn=serial_number, device_name="glove_ik")
    glove.hand_model_path().set(str(urdf))
    print(glove.hand_model_path().get())
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    WujiStatus st = wuji_glove_set_hand_model_path(dev, urdf_path);
    if (st != WUJI_STATUS_OK) {
        wuji_dev_disconnect(dev);
        wuji_dev_release(dev);
        wuji_shutdown();
        return 1;
    }
    print_hand_model_path(dev);
    ```
  </Tab>
</Tabs>

### 3. 订阅使用新手模计算的结果

设置完成后再订阅关节角。订阅句柄和设备在退出时按顺序释放。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    sub = glove.hand_joint_angles().subscribe_with_callback(
        lambda frame: print(len(frame.fingers)))
    try:
        time.sleep(10)
    finally:
        sub.close()
        manager.disconnect_all()
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    struct WujiSub *sub = NULL;
    wuji_glove_subscribe_hand_joint_angles(dev, on_hand_joint_angles, &ctx, &sub);
    /* 保持主线程运行以观察回调，结束后执行下面的清理 */
    wuji_sub_close(sub);
    wuji_dev_disconnect(dev);
    wuji_dev_release(dev);
    wuji_shutdown();
    ```
  </Tab>
</Tabs>

## 运行与输出

```bash theme={null}
./build/glove/2_set_hand_model_path /absolute/path/to/hand.urdf WG1KA00XXXXXXXXX
```

第二个参数可以省略，省略时示例连扫到的第一只手套。共用网络里建议显式传序列号。

具名用户下的实测输出：

```text theme={null}
  SN=WG1KA00XXXXXXXXX  Type=WujiGlove  Address=192.168.1.100:50001
  ...
Connecting to WG1KA00XXXXXXXXX (192.168.1.100:50001)
Set hand_model_path = /path/to/custom_hand.urdf
Read back hand_model_path = /path/to/custom_hand.urdf
Subscribed to hand_joint_angles. Move the glove. Ctrl+C to stop.
seq=473345 frame_id=r_hand_emf_tx fingers=5 frame=1
  finger[0] confidence=0.465 angles=[0.0571 -0.4224 -0.2648 -0.0216 0.1521]
  finger[1] confidence=0.407 angles=[-0.1745 -0.1312 0.0087 0.0087 0.0000]
  finger[2] confidence=0.398 angles=[-0.1745 0.3062 0.0087 0.0087 0.0000]
  finger[3] confidence=0.440 angles=[-0.1745 0.5134 0.0087 0.0087 0.0000]
  finger[4] confidence=0.470 angles=[-0.1745 0.5897 0.0087 0.0087 0.0000]
```

以上为实机运行结果，设备是右手 Wuji Glove，手套静置无人佩戴，当前 SDK 用户为具名用户。

`hand_joint_angles` 的 `frame_id` 是 `r_hand_emf_tx`，与 `hand_skeleton` 的 `r_wrist` 不同，两条流的参考系本来就不一样。

非拇指的第 5 个角度恒为 0，是对齐用的填充位，与手模无关。

## 注意事项

| 分支点 | 表现 | 处理 |
| - | - | - |
| 当前是默认 SDK 用户 | `set` 失败，错误信息指明要切用户 | 先 `switch_user()` |
| URDF 路径不可读 | 示例在连接前就检查并退出，返回码 2 | 用绝对路径，注意 `~` 要自己展开 |
| 未设置就 `get` | 抛 `Parameter not set: calibration.hand_model_path` | 用 try/except 包住 |
| 断开重连 | 设置失效，回落到该用户该设备的默认来源 | 连接后重新 `set` |
| URDF 手别与手套不符 | 求解照常进行，不报错，结果系统性偏差 | 检查 URDF 里 `robot name` 是 `left_hand` 还是 `right_hand` |
| 设备被占用 | 连接时抛 `Session already exists on device` | 等上一个会话心跳超时后重试 |

手别不符是最隐蔽的一种。SDK 不做校验，照样算出置信度正常的结果。

## 扩展用法

### 把设置写进连接流程

因为不跨连接，正确的做法是封一个连接函数：

```python theme={null}
def connect_with_model(manager, sn, urdf_path):
    glove = manager.connect(sn=sn, device_name="glove_ik")
    glove.hand_model_path().set(str(urdf_path))
    return glove
```

断线重连的逻辑里也要走这个函数，否则重连后悄悄换回了默认手模。

### 校验手别

设置之前检查 URDF 的根节点：

```bash theme={null}
grep -o 'robot name="[^"]*"' hand.urdf | head -1
```

右手手套要配 `right_hand`。SDK 不校验这个，配错不会报错。

### 想要贴合自己的手就去标定

自定义路径解决的是「用哪份现成模型」，不解决「模型准不准」。要一份贴合本人手型的模型，走手部模型标定，产物自动落到 `~/.wuji/sdk/models/<序列号>_hand.urdf`，标定过之后不设置也会自动用上。

### 离线重放用 urdf\_path 参数

离线管线有等价能力，而且是构造参数，不受会话限制：

```python theme={null}
pipeline = WujiGlove.offline_pipeline(
    sn="WG1KA00XXXXXXXXX", hand_side="right",
    urdf_path="/path/to/hand.urdf",
)
```

路径不存在时立刻抛错，不会静默回落默认手模。

## 相关内容

* [SDK 本地用户管理](/docs/wuji-sdk/sdk-practice/user-management/)：切到具名用户，这是本篇的前置条件
* [手部模型标定](/docs/wuji-glove/sdk-practice/hand-model-calibration)：生成贴合本人手型的 URDF
* [离线数据处理](/docs/wuji-glove/sdk-practice/offline-replay)：重放时按需指定手模，含本篇那组对比数据的来源
* [数据订阅：回调](/docs/wuji-glove/sdk-practice/subscribe-callback)：`hand_joint_angles` 与 `hand_skeleton` 的帧结构


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