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

## Overview

Wuji Hand 2 fingertip tactile sensors describe their data format through metadata. Read each finger's `info` metadata before subscribing, then use its `format` JSON to decode the data frame. The field describes the contact layout, units, and mounting pose.

The application must read the metadata before decoding data frames. When firmware changes the point count, units, or mounting pose, the decoder can use the updated metadata.

**This capability depends on the hardware configuration.** 2.1 hardware has no fingertip tactile sensors, which arrive with 2.2 hardware. The test device used for this page does not provide the capability. This page documents capability detection, failure behavior, and handling. For supported devices, follow the interface definition and product specification.

Matching examples: `examples/python/wuji_hand_2/3.fingertip_typed.py` and `examples/c/wuji_hand_2/3_fingertip.c`.

For the interface list, the field definitions of the self-describing format, and the calibration interface, see [Wuji Hand 2 Fingertip Tactile](/en/docs/wuji-sdk/wuji-hand-2-tactile). This page covers only how to run it and what the measurements show.

## Prerequisites

### Hardware

| Item | Requirement |
| - | - |
| Wuji Hand 2 | 1, **2.2 hardware or later** |
| Firmware | v2.1.0 or later |
| Connection | Ethernet |

What 2.2 ships is a contact array: 40 contacts on the thumb and 34 on each other finger, each reporting three-axis force plus temperature at 100 Hz.

### Software

| Item | Requirement |
| - | - |
| Operating system | Ubuntu 22.04 LTS or later |
| Wuji SDK | v2026.7.21 or later |
| Python | 3.10 or later |
| C toolchain | CMake 3.16 or later, plus the extracted SDK package |

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

### Before You Start

Confirm the device has this capability. The next section shows how.

## Detect Device Capability

The most reliable runtime check reads the resource list. Don't infer it from a version number:

```python theme={null}
has_fingertip = any("fingertip" in t.path for t in hand.topics())
print("fingertip tactile:", "available" if has_fingertip else "not provided by this device")
```

Measured on a device without it, `topics()` holds **no** fingertip entries at all. A device with it lists five `fingertip/<finger>/data` topics.

This check needs no try/except and doesn't depend on mapping hardware or firmware versions.

## Data Flow

```mermaid theme={null}
flowchart TD
    CHK["Check the resource list"]
    INFO["Read sensor metadata"]
    FMT["Parse format JSON<br/>layout, units, and mounting pose"]
    DEC["Build the data decoder"]
    SUB["Subscribe to fingertip data<br/>100 Hz"]
    PT["Decode contact force components"]
    AGG["Decode aggregate force and temperature"]
    POSE["Transform to hand-model coordinates"]

    CHK -->|"available"| INFO
    INFO --> FMT
    FMT --> DEC
    DEC --> SUB
    SUB --> PT
    SUB --> AGG
    FMT --> POSE
    PT --> POSE
```

Read the sensor metadata before subscribing to the data stream. The data frame does not contain a complete format description, so it cannot be decoded correctly without the metadata.

## The `format` Field

`info.format` is a JSON string that describes the data layout. The decoder is driven by its fields. The key fields:

| Field | Purpose |
| - | - |
| `v` | Contract version. The example accepts only 1 |
| `encoding` | The example accepts only `point_array` |
| `point_count` | Contact count. 40 on the thumb, 34 elsewhere |
| `point_stride` | Bytes per contact |
| `point_fields` | Per-contact field definitions: `name`, `type`, `offset`, `scale` |
| `aggregate_stride` | Bytes in the aggregate section |
| `aggregate_fields` | Aggregate fields. `fx`, `fy`, `fz`, and `temperature` are always present |
| `positions` | Each contact's position in the sensor module's own frame |
| `point_rpy` | Each contact's force-axis orientation |
| `base_xyz` / `base_rpy` | The sensor module's pose relative to the finger's `tip_sensor_frame` |
| `aggregate_xyz` / `aggregate_rpy` | The aggregate application point and force-axis orientation |

The field-type to unpack-format mapping is defined in one table. Unknown types raise an error:

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

Unknown field types raise an error. An incorrect field mapping causes the frame to fail decoding.

### Self-Checking the Data Length

Compute the expected length before decoding and fail outright on a mismatch:

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

This check surfaces an info-to-data version mismatch on the very first frame.

### Units Change With Firmware, So Read Them From format

Starting with firmware v2.4.0, per-contact force is **normalized** to the sensor's full scale: 0 to 1 on the normal axis, −1 to 1 on the tangential axes, clipped beyond that. Earlier firmware reports newtons. The aggregate force and temperature stay in newtons and degrees Celsius on both.

So pick your contact threshold from the declared unit rather than hard-coding it:

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

After a firmware upgrade, re-read info so the decoder picks up the new `scale` and `unit`.

### Placing Contacts on the Hand Model

`positions` and `point_rpy` live in the sensor module's own frame, and `base_xyz` with `base_rpy` give that frame's pose relative to the finger's `tip_sensor_frame`. So contact i sits in finger coordinates at:

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

Its force-axis rotation is `R(base_rpy) @ R(point_rpy[i])`.

The aggregate can be recovered as a true wrench: the force is `R(base_rpy) @ R(aggregate_rpy) @ (fx, fy, fz)`, the application point is `R(base_rpy) @ aggregate_xyz + base_xyz`, and the moment about the link origin is `p × f`.

**These three mounting-pose keys were added to the contract together.** The example branches on that: all three missing means older firmware where position information isn't available, while only some missing means the metadata itself is broken, which raises. Firmware without `aggregate_xyz` reports neither an application point nor a moment. Do not calculate these values when the field is missing.

The device reports poses for its own handedness, so the same code is correct on left and right hands.

## SDK Version Compatibility

This change affects interface compatibility.

| SDK version | How to read info |
| - | - |
| v2026.9.14 and later | `hand.fingertip_thumb_info().get()`, one accessor per finger, no argument |
| v2026.8.31 and earlier | `hand.get_fingertip_info(finger)`, where `finger` is an integer 0 to 4 |

The C side matches, where `wuji_hand_2_get_fingertip_thumb_info(dev, &out)` replaces `wuji_hand_2_get_fingertip_info(dev, finger, &out)`.

**There's no compatibility shim.** Measured, calling the new interface on v2026.8.31:

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

On the C side it fails at link time on the removed symbol. The struct layout didn't change, so an old binary reports the missing symbol during linking.

The old interface takes an integer, and the binding layer rejects a string:

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

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

<Info>
  The examples in the repository follow the newest interface. Running them against an older SDK raises `AttributeError` while reading info. That isn't a device problem.
</Info>

## Measured Behavior on Unsupported Devices

Device details: `firmware_version = 1.3.0` and `hw_version = HwVersion(major=0, minor=2, patch=0)`.

All four paths fail, with errors from different layers:

| Call | Measured error |
| - | - |
| `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` |

All five fingers report the same error from `get_fingertip_info`, and `topics()` holds no fingertip entries either.

Note that the Python object **does** have `fingertip_thumb_data`, `get_fingertip_info`, `tactile_calibrate`, and `tactile_status`. A method existing doesn't mean the device supports it, because the SDK's interface surface is defined per product while what works depends on the device's resource list.

## Runtime Handling

| Branch point | Behavior | What to do |
| - | - | - |
| 2.1 hardware | Every fingertip path reports `Path not found` or `Resource not found` | This hardware doesn't provide the capability |
| Firmware older than v2.1.0 | Same | Upgrade the firmware |
| SDK older than v2026.7.21 | The interface doesn't exist | Upgrade the SDK |
| SDK and example versions mismatched | `AttributeError: ... has no attribute 'fingertip_thumb_info'` | See the version table above |
| One finger's info can't be read | That finger is skipped, the rest continue | No sensor installed, older sensor firmware, or still gathering info after power-on |
| No finger's info can be read | The example exits | Detect the capability with `topics()` first |
| Firmware older than v2.4.0 | Per-contact force is in newtons, not normalized | Read the unit from `format` instead of assuming |
| Firmware missing `aggregate_xyz` | No application point and no moment reported | Do not calculate an application point |
| `format` has only some mounting-pose keys | Raises | The metadata itself is broken, so don't degrade gracefully |

One finger failing to report info is normal, not a fault. The example skips it and decodes the rest, stopping only when all five fail.

## Integration Steps

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

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

    manager = SdkManager.instance()

    # 1. Connect. On a shared network, name a serial number
    devices = [d for d in manager.scan() if d.device_type == DeviceType.WujiHand2]
    hand = manager.connect(sn=devices[0].sn, device_name="hand_0")

    # 2. Detect the capability before going further
    if not any("fingertip" in t.path for t in hand.topics()):
        raise SystemExit("this device has no fingertip tactile, which needs 2.2 hardware or later")

    # 3. Read info. The SDK drives the chunked read internally, so what
    #    arrives here is already decoded.
    #    v2026.9.14 and later use one accessor per finger
    info = hand.fingertip_thumb_info().get()
    fmt = json.loads(info.format)
    if fmt["v"] != 1 or fmt["encoding"] != "point_array":
        raise ValueError(f"unsupported format: {fmt.get('encoding')}")

    # 4. Build the decoder from format
    point_count, stride = fmt["point_count"], fmt["point_stride"]
    expect = point_count * stride + fmt["aggregate_stride"]

    def read(defs, data, base):
        return {d["name"]: struct.unpack_from(
                    FIELD_FMT[d["type"]], data, base + d["offset"]
                )[0] * d.get("scale", 1.0)
                for d in defs}

    def decode(data):
        if len(data) != expect:
            raise ValueError(f"data length {len(data)} != expected {expect}")
        points = [read(fmt["point_fields"], data, k * stride)
                  for k in range(point_count)]
        return points, read(fmt["aggregate_fields"], data, point_count * stride)

    # 5. Subscribe to the stream and decode the data
    sub = hand.fingertip_thumb_data().subscribe()

    # 6. Pick the contact threshold from the declared unit, not a constant
    unit = fmt["point_fields"][0].get("unit", "N")
    threshold = {"normalized": 0.02, "N": 0.2}[unit]

    # 7. Clean up
    sub.close()
    manager.disconnect_all()
    ```
  </Tab>

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

    /* 1. Initialize, discover, connect (see the subscription page) */

    /* 2. Read info. v2026.9.14 and later use one function per finger, no finger argument */
    WujiFingertipSensorInfo info = {0};
    if (wuji_hand_2_get_fingertip_thumb_info(dev, &info) != WUJI_STATUS_OK) {
        /* One finger failing is normal: no sensor installed, older sensor
         * firmware, or still gathering info after power-on. Skip this
         * finger and continue with the other four */
        fprintf(stderr, "thumb info unavailable: %s\n", wuji_last_error());
    }

    /* 3. info.format is a JSON string. Parse it with your own JSON library
     *    and build a decoder from point_count / point_stride / point_fields.
     *    Raise on an unknown field type */

    /* 4. Subscribe to the stream and decode against the contract in the callback */
    struct WujiSub *sub = NULL;
    wuji_hand_2_subscribe_fingertip_thumb_data(dev, on_fingertip, &ctx, &sub);

    /* 5. Close the subscription first, then release the device */
    wuji_sub_close(sub);
    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_hand_2/3.fingertip_typed.py
    ```
  </Tab>

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

With the capability present, the Python example redraws in place at 100 Hz: one status line per finger (aggregate force, temperature, contact count, and the strongest contact's position), followed by a table of fx / fy / fz per contact. Each round drains the subscription queue first so only the newest frame shows. It runs 15 seconds by default.

On a device without the capability, all four paths fail:

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

fingertip entries in topics(): none
```

These are real results, on firmware 1.3.0, hardware version 0.2.0, and SDK v2026.8.31.

## Recalibration and Status Queries

On a device with the capability, `hand.tactile_calibrate()` resets all five fingertips to a fresh zero baseline. The C side is `wuji_hand_2_tactile_calibrate(dev)`.

**The sensor surfaces must be unloaded during calibration.** Nothing touching them, nothing pressing against them. Afterward, `hand.tactile_status(finger)` reports each sensor's model and calibration state, which confirms it finished.

Every frame summary on the `joint_diagnostics` stream carries a fingertip tactile online bitmap, so you can monitor which fingertips are online without polling.

## Configuration and Adaptation

### Lower the Stream Rate

Five contact arrays at 100 Hz add up. `fingertip/*/data` is a rate-control entry point, so a subscription handle's `set_rate()` lowers it, which needs firmware v2.3.0 or later. See [Wuji Hand 2 Adjusting Stream Rate](/en/docs/wuji-hand/latest/sdk-practice/stream-rate/).

### Re-Read info After a Firmware Upgrade

`scale`, `unit`, and the mounting poses in `format` can all change with firmware. Skip the re-read and your decoder parses new data against the old contract, giving wrong numbers with no error. Read info right after every connection and don't cache it to disk.

### When You Only Care Whether Something Is Touched

If you don't need three-axis force per contact, the aggregate section's `fx`/`fy`/`fz` plus a threshold is enough, with no need to decode individual contacts. The aggregate is in newtons on every firmware and isn't affected by normalization.

### Tolerate Per-Finger Failure

All five fingers report info independently. In production code, handle failure per finger so one finger's problem doesn't break the whole pipeline. That's how the example was changed.

## Related

* [Wuji Hand 2 Subscribing With Callbacks](/en/docs/wuji-hand/latest/sdk-practice/subscribe-callback/): the subscription model and callback thread constraints
* [Wuji Hand 2 Adjusting Stream Rate](/en/docs/wuji-hand/latest/sdk-practice/stream-rate/): lower the rate on `fingertip/*/data`
* [Wuji Hand 2 Reading Device Info](/en/docs/wuji-hand/latest/sdk-practice/device-info/): read the firmware version and resource list
* [Common Data Types](/en/docs/wuji-sdk/data-reference/): `FingertipSensorInfo` and `FingertipSensorData`


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