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

Wuji Hand 2 provides fingertip tactile data through five per-finger streams. This page brings together the hardware and version requirements, self-describing format, Python and C APIs, zero-baseline calibration, status queries, communication diagnostics, and complete examples.

## Requirements

Fingertip tactile requires [Wuji Hand 2.2 (Beta)](/en/docs/wuji-hand/latest/version-identification-and-compatibility/) hardware equipped with fingertip tactile sensors, firmware v2.1.0 or later, and Wuji SDK v2026.7.21 or later.

| Finger | Sensing Points | Stream |
| - | -: | - |
| Thumb | 40 | `fingertip/thumb/data` |
| Index | 34 | `fingertip/index/data` |
| Middle | 34 | `fingertip/middle/data` |
| Ring | 34 | `fingertip/ring/data` |
| Pinky | 34 | `fingertip/pinky/data` |

Each stream has a native output rate of 100 Hz. After subscribing, use `Subscription.set_rate()` to lower the output rate, and pass `0` to restore the device default. The C SDK uses `wuji_sub_set_rate()`. Rate control requires Wuji Hand 2 firmware v2.3.0 or later and Wuji SDK v2026.8.17 or later.

## API Overview

| Operation | Python | C |
| - | - | - |
| Read sensor metadata | `hand.fingertip_<finger>_info().get()` | `wuji_hand_2_get_fingertip_<finger>_info()` |
| Subscribe to fingertip data | `hand.fingertip_<finger>_data().subscribe()` | `wuji_hand_2_subscribe_fingertip_<finger>_data()` |
| Recalibrate the whole-hand zero baseline | `hand.tactile_calibrate()` | `wuji_hand_2_tactile_calibrate()` |
| Query one finger's status | `hand.tactile_status(finger)` | `wuji_hand_2_tactile_status()` |
| Lower the stream output rate | `sub.set_rate(frequency_hz)` | `wuji_sub_set_rate()` |

Python's `fingertip_<finger>_info()` and `fingertip_<finger>_data()` name the finger in the method: `thumb`, `index`, `middle`, `ring`, or `pinky`. `tactile_status()` uses `"thumb"`, `"index"`, `"middle"`, `"ring"`, or `"pinky"`. The C `wuji_hand_2_tactile_status()` takes a `finger` argument from `0` through `4` for thumb, index, middle, ring, and pinky.

## Self-Describing Data Format

Read the finger's `FingertipSensorInfo` first, then decode `FingertipSensorData.data` from its `format` JSON. Don't hardcode the payload length, field offsets, scales, or units.

```python theme={null}
import json

from wuji_sdk import WujiException

try:
    info = hand.fingertip_thumb_info().get()
except WujiException as exc:
    # Expected right after power-on, without a sensor fitted, or on older
    # sensor firmware. Skip this finger and keep decoding the others.
    print(f"thumb: no sensor info yet ({exc})")
else:
    fmt = json.loads(info.format)
    sub = hand.fingertip_thumb_data().subscribe()
    frame = await sub.recv_async()
    if frame.info_digest != info.digest:
        info = hand.fingertip_thumb_info().get()
        fmt = json.loads(info.format)
        # Rebuild the decoder from the updated fmt
```

A finger can have no info for a while: no sensor fitted, older sensor firmware, or a read taken right after power-on while the hand is still collecting it. The info read raises `WujiException` in that case. Skip that finger and keep decoding the others. This is an expected state, not a fault.

### Metadata and Data Frames

| Type | Field | Description |
| - | - | - |
| `FingertipSensorInfo` | `header` | Sequence, timestamp, and coordinate frame |
| `FingertipSensorInfo` | `digest` | Metadata content digest that binds data frames |
| `FingertipSensorInfo` | `model` | Sensor model string, which may be empty |
| `FingertipSensorInfo` | `device_type` | Sensor type |
| `FingertipSensorInfo` | `rate_hz` | Native output rate, currently 100 Hz |
| `FingertipSensorInfo` | `format` | JSON that describes the payload layout and mounting pose |
| `FingertipSensorData` | `header` | Data-frame sequence, timestamp, and coordinate frame |
| `FingertipSensorData` | `info_digest` | Metadata digest used to produce the frame |
| `FingertipSensorData` | `data` | Raw payload bytes to decode from `format`. Python delivers them as `list[int]`, so convert with `bytes(frame.data)` before `struct.unpack_from()`. C delivers `uint8_t *data` with `data_len` |

The `v`, `encoding`, `point_count`, `point_stride`, `point_fields`, `aggregate_stride`, `aggregate_fields`, and `unit` keys in `format` describe the payload. Check that `v` is `1` and `encoding` is `point_array` before decoding. The payload length equals `point_count * point_stride + aggregate_stride`, so use it to validate each frame. Each sensing point contains `fx`, `fy`, and `fz`. Aggregate fields provide the three-axis resultant force and temperature.

Starting with firmware v2.4.0, per-point force is normalized against the sensor's full scale. The normal axis ranges from 0 to 1, and the two tangential axes range from -1 to 1, clamped beyond that. Earlier firmware reports per-point force in newtons. Aggregate force stays in newtons, and temperature stays in degrees Celsius. Read `scale` and `unit` from `format` to support both formats. The normalized format requires Wuji SDK v2026.8.17 or later.

### Place Points on the Hand Model

Starting with firmware v2.5.0, `format` includes the mounting-pose fields `point_rpy`, `base_xyz`, and `base_rpy`. The format can include `positions` without the other mounting-pose fields. Treat the mounting pose as available only when `point_rpy`, `base_xyz`, and `base_rpy` are all present. `positions` and `point_rpy` must each contain `point_count` rows of three numbers. `base_xyz` and `base_rpy` must each contain three numbers. Treat a partial set or a shape mismatch as an invalid format instead of placing points with incomplete metadata.

`positions` and `point_rpy` use the sensor module frame. `base_xyz` and `base_rpy` describe that frame relative to the finger's `tip_sensor_frame`. Convert each contact point and its force axes with:

```text theme={null}
point_link = R(base_rpy) * positions[i] + base_xyz
force_link = R(base_rpy) * R(point_rpy[i]) * [fx, fy, fz]
```

`R(rpy)` uses URDF fixed-axis roll, pitch, and yaw: `Rz(yaw) * Ry(pitch) * Rx(roll)`. `format` reports positions in meters and angles in radians. Both formulas transform values from the sensor module frame into the finger's `tip_sensor_frame`.

The device reports the mounting pose that matches its handedness, so the same transform works for left and right hands. When none of the three mounting-pose keys is present, force data remains available, but the points can't be placed on the hand model.

### Locate the Resultant Force

Starting with firmware v2.6.0, `format` includes `aggregate_xyz` and `aggregate_rpy`. `aggregate_xyz` is the fixed geometric centroid of the sensor pad in the sensor module frame, and the resultant force from `aggregate_fields` is applied at that point. The point doesn't follow the center of pressure from frame to frame. Convert it to the finger link with:

```text theme={null}
point_link = R(base_rpy) * aggregate_xyz + base_xyz
```

`aggregate_rpy` describes the resultant-force axes in the same sensor module frame. Convert the force and its moment about the finger link origin with:

```text theme={null}
force_link = R(base_rpy) * R(aggregate_rpy) * [fx, fy, fz]
moment_link = point_link × force_link
```

Because `aggregate_xyz` is fixed, `moment_link` is the moment of the resultant force about a fixed lever arm, not a measured contact moment.

Firmware earlier than v2.6.0 still reports the resultant force, but doesn't provide its point of application or moment. When `aggregate_rpy` is absent, use the sensor mounting orientation `R(base_rpy)` alone for the force axes, whether or not `aggregate_xyz` is present. If `aggregate_xyz` or `aggregate_rpy` is present but doesn't hold exactly three numbers, treat the whole `format` as invalid rather than as a missing field.

## Read and Display Examples

The Python and C examples read metadata for all five fingers and build decoders that match the current `digest`. Python subscribes only to the fingers that report metadata, while C subscribes to all five streams and skips fingers without metadata when displaying. The terminal refreshes the same display area at 100 Hz to show:

* Resultant force, temperature, contact count, and strongest contact for each finger
* Three-axis force for every sensing point, where `fx` and `fy` are tangential components along the local x- and y-axes, and `fz` is the normal component along the local z-axis
* Position and force direction of the strongest contact, starting with firmware v2.5.0
* Position of the aggregate force point and its moment about the origin of the finger link frame, starting with firmware v2.6.0

### Frame Handling and Status

The Python example drains every subscription queue on each refresh and displays only the newest frame for each finger. The C example stores the latest frame for each finger, then refreshes the display from the main thread. When `info_digest` differs from the current metadata, the corresponding status line tells you to read `FingertipSensorInfo` again and rebuild the decoder. When the payload length doesn't match `format`, the example skips that finger's frame while the other fingers keep updating.

The C example reports waiting, lagged, stream ended, stream error, metadata changed, and unexpected payload length on each finger's status line. The Python example also keeps running after a payload-length error for one finger.

### Format Validation

Both examples require `aggregate_fields` to contain `fx`, `fy`, `fz`, and `temperature`. Before subscribing, the C example validates point counts, strides, field offsets, and scales. It rejects nonfinite values, noninteger layout values, out-of-range fields, and per-finger layouts larger than 2048 bytes.

When a force value exceeds the expected column width, the table widens the row instead of truncating it.

The examples select a contact threshold from the unit declared in `format`. They use 0.02 for normalized data and 0.2 N for data in newtons. These values serve the example display and aren't part of the SDK data contract.

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

<Info>
  C subscription callbacks run on SDK worker threads. The frame and its `data` stay valid only for the callback duration. The example copies each payload into mutex-protected storage before the callback returns, then renders a five-finger snapshot on the main thread. Build it with the provided `CMakeLists.txt`, which links the math library and Threads/pthread. Close each subscription with `wuji_sub_close()` when the example exits.
</Info>

## Zero-Baseline Calibration and Status

Keep all five sensor surfaces unloaded before calibration. One call triggers every sensor from thumb through pinky. With firmware v2.6.0 or later and Wuji SDK v2026.8.31 or later, the call raises `NodeOfflineError` on the first offline finger, and later fingers aren't triggered. The C SDK returns `WUJI_STATUS_ERR_NODE_OFFLINE` for that finger. Earlier versions raise `WujiException` instead.

```python theme={null}
import time

from wuji_sdk import NodeOfflineError, TactileState

hand.tactile_calibrate()

pending = ["thumb", "index", "middle", "ring", "pinky"]
deadline = time.monotonic() + 30.0
while pending and time.monotonic() < deadline:
    not_ready = []
    for finger in pending:
        try:
            if hand.tactile_status(finger).state != TactileState.Ready:
                not_ready.append(finger)
        except NodeOfflineError:
            # The finger's tactile module is offline. Keep waiting for it.
            not_ready.append(finger)
    pending = not_ready
    if pending:
        time.sleep(0.5)

if pending:
    raise RuntimeError(f"tactile calibration timed out: {pending}")
print("all fingertip sensors are ready")
```

A successful call means the calibration command reached every finger. It doesn't mean calibration has finished. Poll all five fingers until every state is `TactileState.Ready`, and set an overall timeout for the wait. Each status query blocks for up to about one second, so one round over five fingers can take about five seconds, and the deadline is checked between rounds. `tactile_status()` raises `NodeOfflineError` when that finger's tactile module is offline, under the same version requirement. The example treats that finger as not ready until the deadline.

`TactileType.Thumb` identifies the 40-point thumb sensor, and `TactileType.Standard` identifies a 34-point standard sensor. Runtime states include `TactileState.Calibrating` and `TactileState.Ready`.

## Handle an Offline Sensor

A fingertip data subscription can still be created while its sensor is offline. If the subscription doesn't receive any frames, call `tactile_status` for that finger to check whether the sensor is offline.

With firmware v2.6.0 or later and Wuji SDK v2026.8.31 or later:

* Python `fingertip_<finger>_info().get()`, `tactile_status`, and `tactile_calibrate` raise `NodeOfflineError`.
* The corresponding C calls return `WUJI_STATUS_ERR_NODE_OFFLINE (-12)`.
* Generic GET, SET, and EXEC requests return the same typed result when their target is offline.

Firmware or SDK versions without offline-node error support report a generic internal error instead. `NodeOfflineError` inherits from `WujiException`, not `RuntimeError`. Existing code that catches `WujiException` continues to work. Code that caught `RuntimeError` for these operations must catch `NodeOfflineError` or `WujiException` instead.

```python theme={null}
from wuji_sdk import NodeOfflineError, WujiException

try:
    hand.tactile_status("ring")
except NodeOfflineError:
    print("The ring fingertip sensor is offline")
except WujiException as error:
    print(f"SDK error: {error}")
```

This result means the device can't communicate with the sensor. It doesn't identify the root cause. Confirm the module connection. If the sensor remains offline after the device powers on, contact support because the module may be damaged. Retrying the same request doesn't restore communication.

## Tactile Communication Diagnostics

Subscribe with `hand.joint_diagnostics().subscribe()`, then read `JointDiagnosticsFrame.comm` from each frame that `recv()` or `recv_async()` returns:

```python theme={null}
sub = hand.joint_diagnostics().subscribe()
frame = await sub.recv_async()
comm = frame.comm
```

`comm` includes tactile communication status:

| Field | Description |
| - | - |
| `age_ms` | Age of the device-internal snapshot in milliseconds. `65535` means never sampled, and `65534` means the snapshot is 65.5 s or older |
| `comm_get_failures` | Cumulative count of failed snapshot refreshes |
| `tactile_online_mask` | Online bitmap for the five tactile sensors, with bits 0 through 4 representing thumb through pinky |
| `tactile_response_rate_pct` | Five-element array of per-finger bus response rates from 0 through 100. Index 0 through 4 is thumb through pinky, matching the `tactile_online_mask` bit order |
| `tactile_timeout_total` | Five-element array of per-finger cumulative bus timeout counts in the same index order. Each counter saturates at 4294967295 and doesn't wrap, so read it as a delta between two samples |

Use `tactile_online_mask` to locate an offline finger, then check the response rate and timeout count for continuing communication degradation.

The mask and both per-finger arrays come from a device-internal snapshot. When that snapshot is unavailable, all three read as zeros. Check `age_ms` before reading them as five offline fingers. A growing `age_ms` with a flat `comm_get_failures` means the SDK is skipping the refresh on purpose, which happens during tactile calibration and firmware upgrades. A growing `age_ms` with an increasing `comm_get_failures` means the refresh itself is failing.

For the full `Hand2CommSummary` field list, see [Wuji Hand 2 SDK Reference — Hand2CommSummary](/en/docs/wuji-hand/latest/sdk-reference/diagnostics/#hand2commsummary).

For a measured run, how to detect the capability, and the interface change in SDK v2026.9.14, see [Wuji Hand 2 Fingertip Tactile in practice](/en/docs/wuji-sdk/sdk-practice/hand2-fingertip).


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