Wuji Hand 2 Fingertip Tactile
SDK Practice
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. Read the sensor metadata to confirm that the connected device provides the required interfaces. 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 API and field definitions, see Wuji Hand 2 Fingertip Tactile.
Prerequisites
Hardware
| Item | Requirement |
|---|---|
| Wuji Hand 2 | 1, equipped with fingertip tactile sensors and the corresponding metadata and data interfaces |
| Firmware | v2.1.0 or later |
| Connection | Ethernet |
Read the contact count, fields, units, and output rate from the connected device’s sensor metadata. Don’t infer them from the hardware model.
Software
| Item | Requirement |
|---|---|
| Operating system | Linux or a Mac with Apple silicon, meeting the system requirements |
| Wuji SDK | v2026.7.21 or later |
| Python | 3.8 or later |
| C toolchain | CMake 3.16 or later, plus the extracted SDK package |
Python
C
pip install wuji-sdkLIB="libwuji_sdk_c.so"
if [ "$(uname -s)" = "Darwin" ]; then LIB="libwuji_sdk_c.dylib"; fi
cmake -S examples/c/wuji_hand_2 -B build/hand2 \
-DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
-DWUJI_SDK_LIB="${SDK}/lib/${LIB}"
cmake --build build/hand2 --target 3_fingertip Before You Start
Confirm the device has this capability. The next section shows how.
Data Flow
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.
Detect Device Capability
The most reliable runtime check reads the resource list. Don’t infer it from a version number:
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.
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:
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:
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:
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:
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.22 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:
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:
TypeError: argument 'finger': 'str' object cannot be interpreted as an integer Where 0=thumb, 1=index, 2=middle, 3=ring, 4=pinky.
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.
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.
Integration Steps
The following thumb example connects, reads the format, receives data, and releases resources. See Python example and C example for complete five-finger implementations.
The excerpts reuse helpers and types from Python example and C example: Python’s fetch_format and make_decoder, and C’s finger_ctx_t, parse_format, on_fingertip_data, snapshot, and status_line. These helpers parse formats, validate frames, and display results. They aren’t SDK APIs.
1. Connect to a Specific Device
Follow callback subscription practice for SDK initialization and connection. Select the target SN when several devices share the network. The Python excerpts use the connected hand and manager. C excerpts use the connected dev.
2. Read the Thumb Format and Prepare the Decoder
Read info, save its digest, and build a decoder from the field types, offsets, and scales declared by format. Subscribe to that finger only after this succeeds. On failure, skip the finger, or exit the single-finger program and disconnect.
Python
C
sub = None
try:
fmt, digest = fetch_format(hand, 0)
decode = make_decoder(fmt)
except Exception:
manager.disconnect_all()
raisestruct 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; fetch_format(hand, 0) calls hand.fingertip_thumb_info().get(). C’s parse_format also prepares the length, units, and fields used by later callbacks. Both bindings read the layout from the format instead of hard-coding contact counts or byte offsets.
3. Subscribe and Process Frames
Python reads the subscription queue in the main loop. C receives frames through a background callback. Check info_digest against the saved digest before decoding. Don’t decode a zero digest. If the digest changes, reread info and rebuild the decoder. The frame length must match the format declaration.
Python
C
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()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);
} The C excerpt reuses g_stop, signal handling, and REFRESH_US from C example. on_fingertip_data checks frame status, digest, and length, then copies data under a mutex. The main thread reads it through snapshot. If the digest changes, C example discards the frame and displays status. Close the subscription, reread the format, and subscribe again.
4. Close the Subscription and Disconnect
Close the subscription and wait for its callback worker to finish before freeing the callback context and device. Python handles normal exit, exceptions, and interruption in the previous step’s finally block. C uses the following cleanup block on success and failure.
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(); Running It
Python
C
python examples/python/wuji_hand_2/3.fingertip_typed.py./build/hand2/3_fingertip 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:
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.
Notes
| Branch point | Behavior | What to do |
|---|---|---|
| Device without fingertip tactile sensors | 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.
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.
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: the subscription model and callback thread constraints
- Wuji Hand 2 Adjusting Stream Rate: lower the rate on
fingertip/*/data - Wuji Hand 2 Reading Device Info: read the firmware version and resource list
- Common Data Types:
FingertipSensorInfoandFingertipSensorData
Documentation