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) hardware equipped with fingertip tactile sensors, firmware v2.1.0 or later, and Wuji SDK v2026.7.21 or later.

FingerSensing PointsStream
Thumb40fingertip/thumb/data
Index34fingertip/index/data
Middle34fingertip/middle/data
Ring34fingertip/ring/data
Pinky34fingertip/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

OperationPythonC
Read sensor metadatahand.fingertip_<finger>_info().get()wuji_hand_2_get_fingertip_<finger>_info()
Subscribe to fingertip datahand.fingertip_<finger>_data().subscribe()wuji_hand_2_subscribe_fingertip_<finger>_data()
Recalibrate the whole-hand zero baselinehand.tactile_calibrate()wuji_hand_2_tactile_calibrate()
Query one finger's statushand.tactile_status(finger)wuji_hand_2_tactile_status()
Lower the stream output ratesub.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.

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

TypeFieldDescription
FingertipSensorInfoheaderSequence, timestamp, and coordinate frame
FingertipSensorInfodigestMetadata content digest that binds data frames
FingertipSensorInfomodelSensor model string, which may be empty
FingertipSensorInfodevice_typeSensor type
FingertipSensorInforate_hzNative output rate, currently 100 Hz
FingertipSensorInfoformatJSON that describes the payload layout and mounting pose
FingertipSensorDataheaderData-frame sequence, timestamp, and coordinate frame
FingertipSensorDatainfo_digestMetadata digest used to produce the frame
FingertipSensorDatadataRaw 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:

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:

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:

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.

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.

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.

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.

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:

sub = hand.joint_diagnostics().subscribe()
frame = await sub.recv_async()
comm = frame.comm

comm includes tactile communication status:

FieldDescription
age_msAge of the device-internal snapshot in milliseconds. 65535 means never sampled, and 65534 means the snapshot is 65.5 s or older
comm_get_failuresCumulative count of failed snapshot refreshes
tactile_online_maskOnline bitmap for the five tactile sensors, with bits 0 through 4 representing thumb through pinky
tactile_response_rate_pctFive-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_totalFive-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.

Subscribe to Updates