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

# Wuji Hand 2 指尖触觉

## 概述

本篇实践介绍如何读取 Wuji Hand 2 指尖传感器元数据，并按自描述格式订阅和解析触觉数据。应用场景包括接入不同传感器布局、监控指尖触觉和记录接触数据。

对应的 Python 示例为 [3.fingertip\_typed.py](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_hand_2/3.fingertip_typed.py)，C 示例为 [3\_fingertip.c](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_hand_2/3_fingertip.c)。 接口清单、自描述格式的字段定义与标定接口见 [Wuji Hand 2 指尖触觉](/docs/wuji-sdk/wuji-hand-2-tactile)。本页说明集成流程、兼容性要求和运行时行为。

<Warning>
  当前测试设备不具备指尖触觉传感器，因此本文只覆盖能力判定方法、四条失败路径和接口版本分界。支持设备的数据处理流程以接口定义和产品规格为准。
</Warning>

## 前置条件

### 硬件

| 项目 | 要求 |
| - | - |
| Wuji Hand 2 | 1 只，**2.2 及以上硬件** |
| 固件 | v2.1.0 及以上版本 |
| 连接方式 | 以太网 |

2.2 的交付形态是触点阵列：拇指 40 触点，其余手指各 34 触点，每触点三轴力加温度，100 Hz 推送。

### 软件

| 项目 | 要求 |
| - | - |
| 操作系统 | Ubuntu 22.04 LTS 及以上版本 |
| Wuji SDK | v2026.7.21 及以上版本 |
| Python | 3.10 及以上版本 |
| C 工具链 | CMake 3.16 及以上版本，以及解压后的 SDK 包 |

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    pip install wuji-sdk
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    cmake -S examples/c/wuji_hand_2 -B build/hand2 \
      -DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
      -DWUJI_SDK_LIB="${SDK}/lib/libwuji_sdk_c.so"
    cmake --build build/hand2 --target 3_fingertip
    ```
  </Tab>
</Tabs>

### 前序步骤

先确认设备支持该能力，方法见下一节。

## 数据流

```mermaid theme={null}
flowchart TD
    TOPICS["topics()"] -->|"存在 fingertip 数据流"| INFO["逐指读取 fingertip_*_info()"]
    TOPICS -->|"未返回 fingertip"| END["设备不提供该能力"]
    INFO --> FORMAT["解析每指 info.format JSON"]
    FORMAT --> VALID["校验版本、编码、字段和安装位姿"]
    VALID --> DEC["按 format 构建解码器"]
    DEC --> SUB["订阅可用的 fingertip/<finger>/data"]
    SUB --> FRAME["按对应 format 解码每帧"]
    FRAME --> POINT["点级 fx / fy / fz"]
    FRAME --> AGG["合力与温度"]
    POINT --> POSE["按安装位姿变换到手模坐标系"]
    AGG --> OUT["显示或记录触觉数据"]
    POSE --> OUT
```

顺序不能颠倒。没有 info 就无法解码 data，帧里没有自描述头。

## 判定设备是否支持该能力

最可靠的运行时判定是看资源清单，不要靠版本号推断：

```python theme={null}
has_fingertip = any("fingertip" in t.path for t in hand.topics())
print("指尖触觉:", "可用" if has_fingertip else "该设备不提供")
```

实测在不具备的设备上，`topics()` 里**未返回** fingertip 条目。具备的设备会列出五条 `fingertip/<finger>/data`。

这个判定不需要 try/except，也不依赖硬件或固件版本号的映射关系。

## format 契约

`info.format` 是一个 JSON 字符串，示例把它当唯一真相源，一个字段都不硬编码。关键字段：

| 字段 | 作用 |
| - | - |
| `v` | 契约版本，示例只接受 1 |
| `encoding` | 示例只接受 `point_array` |
| `point_count` | 触点数。拇指 40，其余 34 |
| `point_stride` | 每个触点占多少字节 |
| `point_fields` | 每个触点的字段定义：`name`、`type`、`offset`、`scale` |
| `aggregate_stride` | 合力段占多少字节 |
| `aggregate_fields` | 合力段字段。`fx`、`fy`、`fz`、`temperature` 必有 |
| `positions` | 各触点在传感器模块自身坐标系里的位置 |
| `point_rpy` | 各触点的力轴朝向 |
| `base_xyz` / `base_rpy` | 传感器模块相对手指 `tip_sensor_frame` 的位姿 |
| `aggregate_xyz` / `aggregate_rpy` | 合力作用点与力轴朝向 |

字段类型到解包格式的映射，示例写死成一张表并对未知类型直接抛错：

```python theme={null}
FIELD_FMT = {"i8": "<b", "u8": "<B", "i16": "<h", "u16": "<H",
             "i32": "<i", "u32": "<I", "f32": "<f"}
```

**遇到表里没有的类型就抛异常，绝不猜。** 猜错一个类型，整帧数据会被解释成看似合理的垃圾。

### 数据长度自校验

解码前先算期望长度，对不上直接报错：

```python theme={null}
expect = point_count * point_stride + aggregate_stride
if len(data) != expect:
    raise ValueError(f"data length {len(data)} != expected {expect}")
```

这条校验能在第一帧就把 info 与 data 版本不匹配的情况暴露出来。

### 根据 format 读取单位

firmware v2.4.0 及以上版本，每触点的力**归一化**到传感器满量程：法向轴 0 到 1，切向轴 -1 到 1，超出部分截断。更早的固件按牛顿上报。合力与温度在两者上都是牛顿和摄氏度。

所以接触阈值要按声明的单位选，不能写死：

```python theme={null}
CONTACT_BY_UNIT = {"normalized": 0.02, "N": 0.2}
```

固件升级之后要重新读 info，解码器才会拿到新的 `scale` 与 `unit`。

### 将触点坐标转换到手模型

`positions` 与 `point_rpy` 在传感器模块自身坐标系里，`base_xyz` 与 `base_rpy` 给出这个坐标系相对手指 `tip_sensor_frame` 的位姿。所以触点 i 在手指坐标系里的位置是：

```text theme={null}
R(base_rpy) @ positions[i] + base_xyz
```

它的力轴旋转是 `R(base_rpy) @ R(point_rpy[i])`。

合力可以还原成真正的力旋量：力为 `R(base_rpy) @ R(aggregate_rpy) @ (fx, fy, fz)`，作用点为 `R(base_rpy) @ aggregate_xyz + base_xyz`，对连杆原点的力矩是 `p × f`。

**这三个安装位姿键是一起加进契约的。** 示例据此做了一条判定：三个全缺表示固件较早、位置信息不可用，只缺一部分说明元数据本身坏了，直接抛错。缺 `aggregate_xyz` 的固件不报作用点也不报力矩——在那里猜一个点等于凭空编造力臂。

设备会按自己的左右手上报位姿，同一份代码在左手和右手上都对。

## SDK 接口版本兼容性

这是接入时需要确认的兼容性要求。

| SDK 版本 | 读 info 的写法 |
| - | - |
| v2026.9.14 及以上 | `hand.fingertip_thumb_info().get()`，每指一个访问器，不带参数 |
| v2026.8.31 及以下 | `hand.get_fingertip_info(finger)`，`finger` 是整数 0 到 4 |

C 侧同理，`wuji_hand_2_get_fingertip_thumb_info(dev, &out)` 取代了 `wuji_hand_2_get_fingertip_info(dev, finger, &out)`。

**SDK 不提供兼容适配层。** 在 v2026.8.31 上调用新接口：

```text theme={null}
AttributeError: 'builtins.WujiHand2' object has no attribute 'fingertip_thumb_info'
```

C 侧则是链接期就失败，找不到被移除的符号。结构体布局没变，所以旧二进制是响亮地失败，而不是悄悄读错数据。

旧接口的参数是整数，传字符串会被绑定层拦下：

```text theme={null}
TypeError: argument 'finger': 'str' object cannot be interpreted as an integer
```

`0=thumb, 1=index, 2=middle, 3=ring, 4=pinky`。

<Info>
  仓库里的示例跟随最新接口。使用旧版 SDK 运行示例时，会在读取 info 的步骤抛出 `AttributeError`。此错误与设备无关。
</Info>

## 设备不支持该能力时的行为

设备信息：`firmware_version = 1.3.0`，`hw_version = HwVersion(major=0, minor=2, patch=0)`。

四条路径全部失败，错误来自不同层：

| 调用 | 实测错误 |
| - | - |
| `hand.get_fingertip_info(0)` | `RuntimeError: sdk error: Path not found: fingertip.info_req` |
| `hand.fingertip_thumb_data().subscribe()` | `WujiException: Resource not found: fingertip/thumb/data` |
| `hand.tactile_calibrate()` | `RuntimeError: sdk error: Path not found: thumb/tactile/tactile_calibrate` |
| `hand.tactile_status('thumb')` | `RuntimeError: sdk error: Path not found: thumb/tactile/tactile_query_status` |

五根手指的 `get_fingertip_info` 报的是同一条。`topics()` 里也**未返回** fingertip 条目。

注意 Python 对象上**有** `fingertip_thumb_data`、`get_fingertip_info`、`tactile_calibrate`、`tactile_status` 这些方法——方法存在不代表设备支持，SDK 的接口面是按产品定义的，能不能用要看设备的资源清单。

## 集成步骤

以下以拇指为例，依次连接设备、读取格式、接收数据和释放资源。完整五指读取代码见 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_hand_2/3.fingertip_typed.py) 和 [C 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_hand_2/3_fingertip.c)。

下文复用 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_hand_2/3.fingertip_typed.py) 和 [C 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_hand_2/3_fingertip.c) 中的辅助函数和类型：Python 的 `fetch_format`、`make_decoder`，以及 C 的 `finger_ctx_t`、`parse_format`、`on_fingertip_data`、`snapshot`、`status_line`。这些辅助函数负责解析格式、检查数据帧和显示结果，并非 SDK 接口。

### 1. 连接指定设备

先按[回调订阅实践](/docs/wuji-hand/latest/sdk-practice/subscribe-callback#集成步骤)完成 SDK 初始化和设备连接。多设备环境按目标 SN 选择设备。后续 Python 片段使用已连接的 `hand` 和 `manager`，C 片段使用已连接的 `dev`。

### 2. 读取拇指格式并准备解码器

读取 `info`，保存格式摘要，并按 `format` 描述的字段类型、偏移和比例建立解码器。只有这一步成功，才订阅对应手指。读取失败时跳过该手指，或结束单指程序并断开设备。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    sub = None
    try:
        fmt, digest = fetch_format(hand, 0)
        decode = make_decoder(fmt)
    except Exception:
        manager.disconnect_all()
        raise
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    struct WujiSub *sub = NULL;
    finger_ctx_t ctx = {0};
    ctx.name = "thumb";
    pthread_mutex_init(&ctx.lock, NULL);
    ctx.lock_ready = 1;
    WujiFingertipSensorInfo info = {0};
    if (wuji_hand_2_get_fingertip_thumb_info(dev, &info) != WUJI_STATUS_OK) {
        fprintf(stderr, "%s\n", wuji_last_error());
        goto cleanup;
    }
    ctx.digest = info.digest;
    if (parse_format(info.format, &ctx) != 0) goto cleanup;
    ctx.has_info = 1;
    ```
  </Tab>
</Tabs>

`fetch_format(hand, 0)` 内部调用 `hand.fingertip_thumb_info().get()`。C 的 `parse_format` 同时准备后续回调所需的长度、单位和字段信息。两种语言都从格式读取布局，避免写死触点数量或字节偏移。

### 3. 订阅并处理数据帧

Python 在主循环读取订阅队列，C 由后台回调接收帧。先检查 `info_digest` 与保存的摘要一致，再解码。摘要为 0 时不解码，摘要变化时重新读取 `info` 并建立解码器。帧长度必须与格式声明一致。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import time

    try:
        sub = hand.fingertip_thumb_data().subscribe()
        end = time.monotonic() + 15
        while time.monotonic() < end:
            frame = sub.recv()
            if frame is None:
                time.sleep(0.01)
                continue
            if frame.info_digest == 0:
                continue
            if frame.info_digest != digest:
                fmt, digest = fetch_format(hand, 0)
                decode = make_decoder(fmt)
                continue
            try:
                points, aggregate = decode(bytes(frame.data))
            except ValueError:
                continue
            print(aggregate["fx"], aggregate["fy"], aggregate["fz"])
    finally:
        if sub is not None:
            sub.close()
        manager.disconnect_all()
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    if (wuji_hand_2_subscribe_fingertip_thumb_data(
            dev, on_fingertip_data, &ctx, &sub) != WUJI_STATUS_OK) {
        fprintf(stderr, "%s\n", wuji_last_error());
        goto cleanup;
    }
    while (!g_stop) {
        snapshot_t snap;
        snapshot(&ctx, &snap);
        status_line(&ctx, &snap);
        usleep(REFRESH_US);
    }
    ```
  </Tab>
</Tabs>

C 片段沿用 [C 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_hand_2/3_fingertip.c) 的 `g_stop`、信号处理和 `REFRESH_US`。`on_fingertip_data` 检查帧状态、摘要和长度，并在互斥锁保护下复制数据，主线程通过 `snapshot` 读取。摘要变化时，[C 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_hand_2/3_fingertip.c) 丢弃该帧并显示状态，应用需关闭订阅、重新读取格式后再订阅。

### 4. 关闭订阅并断开设备

先关闭订阅，等回调工作线程结束，再释放回调上下文和设备。Python 已在上一步的 `finally` 中处理正常退出、异常和中断。C 的成功路径与失败路径共用以下清理段。

```c theme={null}
cleanup:
if (sub) wuji_sub_close(sub);
if (ctx.lock_ready) pthread_mutex_destroy(&ctx.lock);
wuji_dev_disconnect(dev);
wuji_dev_release(dev);
wuji_shutdown();
```

## 运行与输出

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    python examples/python/wuji_hand_2/3.fingertip_typed.py
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    ./build/hand2/3_fingertip
    ```
  </Tab>
</Tabs>

具备该能力时，Python 示例以 100 Hz 原地重绘：每根手指一行状态（合力、温度、接触点数、最强接触点的位置），下面是每个触点的 fx / fy / fz 表格。每轮先排空订阅队列，只显示最新一帧。默认跑 15 秒。

不具备该能力的设备上，实测四条路径全部失败：

```text theme={null}
get_fingertip_info(0) [thumb]: RuntimeError: sdk error: Path not found: fingertip.info_req
get_fingertip_info(1) [index]: RuntimeError: sdk error: Path not found: fingertip.info_req
get_fingertip_info(2) [middle]: RuntimeError: sdk error: Path not found: fingertip.info_req
get_fingertip_info(3) [ring]: RuntimeError: sdk error: Path not found: fingertip.info_req
get_fingertip_info(4) [pinky]: RuntimeError: sdk error: Path not found: fingertip.info_req

fingertip_thumb_data().subscribe(): WujiException: Resource not found: fingertip/thumb/data
tactile_calibrate(): RuntimeError: sdk error: Path not found: thumb/tactile/tactile_calibrate
tactile_status('thumb'): RuntimeError: sdk error: Path not found: thumb/tactile/tactile_query_status

topics() 里 fingertip 相关: 无
```

以上为实机运行结果，设备固件 1.3.0，硬件版本 0.2.0，SDK v2026.8.31。

## 注意事项

| 分支点 | 表现 | 处理 |
| - | - | - |
| 2.1 硬件 | 全部指尖触觉路径报 `Path not found` 或 `Resource not found` | 该硬件不提供此能力 |
| 固件低于 v2.1.0 | 同上 | 升级固件 |
| SDK 低于 v2026.7.21 | 接口不存在 | 升级 SDK |
| SDK 与示例版本不匹配 | `AttributeError: ... has no attribute 'fingertip_thumb_info'` | 见上一节的版本对照 |
| 某根手指读不到 info | 该指跳过，其余继续 | 没装传感器、传感器固件旧、或刚上电还在收集信息 |
| 五根手指都读不到 info | 示例退出 | 先用 `topics()` 判定能力 |
| 固件低于 v2.4.0 | 每点力的单位是牛顿而非归一化值 | 从 `format` 读取单位，不要预设单位 |
| 固件缺少 `aggregate_xyz` | 不报告合力作用点和力矩 | 不计算作用点和力矩 |
| `format` 只有部分安装位姿键 | 抛错 | 元数据本身有问题，不要降级处理 |

单根手指读不到 info 是正常情况，不是故障。示例的处理是跳过它继续解码其余手指，只有五根全读不到才停。

## 重新标定与状态查询

具备该能力的设备上，`hand.tactile_calibrate()` 把五个指尖重置到全新零基线。C 侧是 `wuji_hand_2_tactile_calibrate(dev)`。

**标定期间传感器表面必须无负载**，不能触碰、不能压在任何东西上。标定完用 `hand.tactile_status(finger)` 查各传感器的型号与标定状态，确认已完成。

`joint_diagnostics` 流的每帧汇总里带指尖触觉在线位图，可以用来持续监控哪些指尖在线，不用轮询。

## 扩展用法

### 降低输出率

100 Hz 五路触点阵列的数据量不小。`fingertip/*/data` 是速率控制的入口点，可以用订阅句柄的 `set_rate()` 降频，需要固件 v2.3.0 及以上版本，见 [Wuji Hand 2 输出率调节](/docs/wuji-hand/latest/sdk-practice/stream-rate/)。

### 固件升级后重读 info

`format` 里的 `scale`、`unit`、安装位姿都可能随固件变。升级之后不重读 info，解码器会按旧契约解析新数据，结果是数值全错但不报错。把读 info 放在每次连接之后，别缓存到磁盘。

### 仅判断是否接触

不需要每个触点的三轴力时，用合力段的 `fx`/`fy`/`fz` 加一个阈值就够了，不用解每个触点。合力在任何固件上都是牛顿，不受归一化影响。

### 逐指容错

五根手指各自独立上报 info。写生产代码时按指处理失败，别让一根手指的问题中断整条流水线——示例就是这么改的。

## 相关内容

* [Wuji Hand 2 数据订阅：回调](/docs/wuji-hand/latest/sdk-practice/subscribe-callback/)：订阅模型与回调线程约束
* [Wuji Hand 2 输出率调节](/docs/wuji-hand/latest/sdk-practice/stream-rate/)：给 `fingertip/*/data` 降频
* [Wuji Hand 2 设备信息](/docs/wuji-hand/latest/sdk-practice/device-info/)：读取固件版本与资源清单
* [通用数据类型](/docs/wuji-sdk/data-reference/)：`FingertipSensorInfo` 与 `FingertipSensorData`


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