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

# SDK Reference

<Info>
  The Python semantic API for Wuji Hand 2. Unified pattern: `hand.{resource}().{action}()`. All operations target the whole hand (20 joints).
</Info>

This page focuses on the Wuji Hand 2–specific semantic API. The SDK builds on the shared **Wuji SDK** (`wuji_sdk`, `pip install wuji-sdk`)—for general installation, device connection, and data subscription, see the [Wuji SDK docs](/en/docs/wuji-sdk/). For source, see the [wuji-sdk repository](https://github.com/wuji-technology/wuji-sdk).

## Connection

<Info>
  **Network prerequisite**: Wuji Hand 2 uses a static IP, not DHCP. Each device ships with a fixed address by handedness — left hand `192.168.1.110`, right hand `192.168.1.111`, gateway `192.168.1.1`, subnet mask `255.255.255.0`. Before the first connection, set your host NIC to the same subnet (for example `192.168.1.50`), then connect by address. The factory IP is changeable later — see [ip (SET/GET)](#ip-set/get).
</Info>

### Connect via SdkManager (Recommended)

```python theme={null}
from wuji_sdk import SdkManager, Handedness

manager = SdkManager.instance()

# Auto-discover and connect to the first Wuji Hand 2 on the LAN
hand = manager.auto_connect(device_name="wuji_hand_2")

# Connect explicitly by serial number or address
hand = manager.connect(sn=<device_sn>, device_name="wuji_hand_2")
hand = manager.connect(address="192.168.1.110:50001", device_name="wuji_hand_2")

# Or connect by handedness directly, without a serial number
hand = manager.connect(handedness=Handedness.Right, device_name="wuji_hand_2")
```

The `device_name="wuji_hand_2"` overloads of `auto_connect` and `connect` return a `WujiHand2` instance directly (with type hints). `handedness` is mutually exclusive with `sn` and `address`. If multiple Hand 2 units of the same handedness are on the network, `AmbiguousHandedness` is raised—specify `sn` instead.

### ConnectOptions

```python theme={null}
from wuji_sdk import SdkManager, ConnectOptions

opts = ConnectOptions(
    timeout_ms=1000,
    retry_count=3,
    enable_bridge=True,    # Default True: lets multiple clients (Wuji Studio + recording scripts + your app) connect at once
)
hand = manager.connect(sn=<device_sn>, device_name="wuji_hand_2", options=opts)
```

Set `enable_bridge=False` for exclusive single-client mode.

### Instance Attributes

| Attribute | Type | Description |
| - | - | - |
| `serial_number` | `str` | Device serial number |
| `device_name` | `str` | Name given at connect (default `"wuji_hand_2"`) |
| `info` | `Optional[DeviceInfo]` | Device info: `serial_number`, `firmware_version` |
| `is_connected` | `bool` | Connection state |

`hand.hw_version().get()` returns the factory hardware version `HwVersion(major, minor, patch)`. `0.0.0` means it wasn't written at the factory.

## Hand-Level Resources

This section covers the whole-hand API (20 joints). Single-joint reads and actions go through the `JointHandle` returned by `hand.joint(k)` / `hand.joints()` (see [Joint Traversal](#joint-traversal)). The device runs in MIT control mode by default — the control mode isn't set from the Python API.

### handedness (GET)

Get the **handedness** result (left / right).

```python theme={null}
side = hand.handedness().get()   # → "left" or "right"
```

### online\_joints\_count (GET)

Get the **online joint count** (0–20).

```python theme={null}
n = hand.online_joints_count().get()   # → int, 0–20
```

### joint\_diagnostics (SUB)

**Joint diagnostics stream**: subscribe to per-joint status word, current, bus voltage, temperature, and error code. Frames are variable-length and contain only online joints — identify each entry by its `nid`. This stream and `joint_states` ride the same device stream, so lowering the push rate of one lowers both — see [joint\_states (SUB)](#joint_states-sub).

```python theme={null}
sub = hand.joint_diagnostics().subscribe()   # → Subscription[JointDiagnosticsFrame]
frame = await sub.recv_async()
for j in frame.joints:
    print(j.nid, j.vbus_v_fb, j.mcu_temp_c_fb, f"0x{j.error_code_current:04X}")
sub.close()
```

Decode `error_code_current` with the static method `WujiHand2.describe_error(code)`. The returned object carries the error name, description, `severity` (`Warning` / `DeferredStop` / `ImmediateStop` / `Fatal`), and `clear_policy` (`AutoClear` / `ManualClear` / `NonClearable`). Unknown codes return `None`. The `code` field of the returned object is an integer, the same value you passed in. Printing a diagnostics entry object shows error codes in hex.

<Info>
  For joint angles, use `position` from the `joint_states` stream — the diagnostics stream doesn't carry joint angles.
</Info>

### comm\_diag (GET, 1 Hz)

**Communication diagnostics**: returns the throughput / error rate of the 5 fingers, once per second.

```python theme={null}
diag = hand.comm_diag().get()           # → HandCommunicationDiagnostics
for finger in diag.fingers:             # throughput / error rate for each of the 5 fingers
    print(finger.tx_kbps, finger.rx_kbps, finger.error_per_sec)
```

Use it to troubleshoot communication between the joints and the control board.

### effort\_limit (SET/GET)

**Effort limit**: read / write the per-joint torque cap (A).

```python theme={null}
hand.effort_limit().set(1.5)            # set all joints to 1.5 A
limits = hand.effort_limit().get()      # → list[Optional[float]]
```

Writes reject NaN, Inf, negative values, and values above the device ceiling — whole-hand and per-joint writes validate the same way. A rejected write fails with an error and leaves the current value unchanged. The firmware sets the ceiling. For common errors and remedies, see [Wuji SDK troubleshooting](/en/docs/wuji-sdk/troubleshooting/).

### mit\_params (SET/GET)

**MIT impedance parameters**: read / write per-joint kp / kd.

```python theme={null}
hand.mit_params().set((1.0, 0.05))           # same kp / kd for all joints
hand.mit_params().set([(1.0, 0.05)] * 20)    # list of 20, per joint
mp = hand.mit_params().get()                 # → list[Optional[MitParam]]; offline joints are None
```

Writes reject NaN, Inf, and negative values.

### clear\_fault (EXEC)

**Clear faults**: a direct action. Without arguments it acts on the whole hand.

```python theme={null}
hand.clear_fault()                  # clear faults on all joints
hand.clear_fault(joints=mask)       # optional: 0/1 mask of length 20 — only joints with a 1 are cleared
```

### User Origin

Calibrate the current physical position as the joint-side zero. If a joint is moving, it takes effect at the next IDLE.

```python theme={null}
hand.set_origin()                   # whole hand: refresh the user origin of all joints
hand.clear_origin()                 # whole hand: clear the user origin of all joints
hand.set_origin(joints=mask)        # optional: 0/1 mask of length 20 — only joints with a 1 are affected
```

### Enable / Disable / Emergency Stop

```python theme={null}
hand.enable()               # enable all joints
hand.disable()              # disable all joints
hand.enable(joints=mask)    # optional: 0/1 mask of length 20 — acts on joints with a 1
hand.emergency_stop()       # emergency stop: whole-hand action, no mask
```

### joint\_states (SUB)

**Joint state stream**: subscribe to the real-time state frames of the 20 joints, at a native output rate of 1000 Hz. Lower the push rate through the subscription handle: `sub.set_rate(hz)` returns the rate in effect, and `0` restores the native rate — see [Wuji SDK data subscription](/en/docs/wuji-sdk/data-subscription/#adjusting-the-stream-output-rate) for full usage. This stream and `joint_diagnostics` ride the same device stream, so lowering the rate of one lowers both.

<Info>
  Push-rate tuning applies to the `joint_states`, `joint_diagnostics`, fingertip tactile, and `imu` streams. Upgrade the firmware and the Wuji SDK together to versions that support it — see the release notes for the version requirements.
</Info>

```python theme={null}
sub = hand.joint_states().subscribe()   # → Subscription[JointStateFrame]
frame = sub.recv()                      # synchronous, non-blocking; None = no data yet
frame = await sub.recv_async()          # asynchronous wait
print(frame.header.seq, frame.header.timestamp_us)
for j in frame.joints:
    print(j.nid, j.position, j.velocity, j.effort)
sub.close()
```

Callback mode:

```python theme={null}
def on_state(frame):
    print(frame.header.seq, [j.position for j in frame.joints[:4]])

cb = hand.joint_states().subscribe_with_callback(on_state)
# ...
cb.close()
```

<Info>
  `position` is the joint-side angle (rad) and `velocity` the joint-side angular velocity (rad/s)—this is the only recommended path for joint angle and speed. Frames are variable-length and contain only online joints — identify each entry by its `nid`. The `joint_diagnostics` stream doesn't carry joint angles.
</Info>

### joint\_command (PUB)

**Joint command**: publish positions / velocities / torque feedforward for the 20 joints. Each send takes exactly 20 `JointCommand` entries, one `position` / `velocity` / `effort` per joint.

```python theme={null}
from wuji_sdk import JointCommand

pub = hand.joint_command().publish()    # → JointCommandPublisher
pub.send([JointCommand(position=p, velocity=0.0, effort=0.0) for p in positions])
pub.close()
```

To turn human hand keypoints into these 20-joint commands, use Wuji SDK [Hand Retargeting](/en/docs/wuji-sdk/retargeting/).

### Fingertip Tactile Streams (SUB)

<Info>
  Fingertip tactile requires Wuji Hand 2.2 (Beta) hardware equipped with the fingertip tactile sensors, plus firmware v2.1.0 and SDK v2026.7.21 or later, upgraded together.
</Info>

**Self-describing per-finger sensor streams**: first fetch the finger's `FingertipSensorInfo` metadata (its `format` JSON describes the data-frame layout), then subscribe to that finger's `FingertipSensorData` stream and decode per the format. The thumb has 40 sensing points, the other fingers 34. The native output rate is 100 Hz, adjustable down through the subscription handle.

<Warning>
  Starting with firmware v2.4.0, per-point force arrives normalized against the sensor's full scale: 0 to 1 on the normal axis and -1 to 1 on the two tangential axes, with readings past full scale clamped to the limit. Firmware before v2.4.0 reports per-point force in newtons. The three-axis resultant force stays in newtons and the temperature in Celsius, unaffected by this change.

  Read `scale` and `unit` from `info.format` when decoding instead of hard-coding either convention. A firmware upgrade changes the `info` digest, so the digest check in the example rebuilds the decoder and the new range takes effect. Normalization needs Wuji SDK v2026.8.17 or later.
</Warning>

```python theme={null}
import json

info = hand.get_fingertip_info(0)               # 0=thumb … 4=pinky → FingertipSensorInfo
fmt = json.loads(info.format)                   # the format fully describes the frame layout — never hardcode it

sub = hand.fingertip_thumb_data().subscribe()   # one resource per finger: fingertip_{thumb,index,middle,ring,pinky}_data
frame = await sub.recv_async()                  # → FingertipSensorData
if frame.info_digest != info.digest:            # a digest mismatch means the info changed
    info = hand.get_fingertip_info(0)           # re-fetch and rebuild the decoder
# decode frame.data per fmt's point_fields / aggregate_fields
sub.close()
```

For a complete consumer-side reference (including decoder construction), see the `3.fingertip_typed.py` example under [`examples/python/wuji_hand_2/`](https://github.com/wuji-technology/wuji-sdk/tree/main/examples/python/wuji_hand_2).

### Fingertip Tactile Calibration (EXEC) and Status (GET)

**Zero-baseline recalibration**: one call recalibrates all 5 fingertips to a fresh zero baseline.

```python theme={null}
hand.tactile_calibrate()      # whole-hand action; fails fast on the first offline finger
```

<Warning>
  Keep all sensor surfaces unloaded (no contact force) during the call — otherwise the calibrated zero baseline is wrong.
</Warning>

A successful call means the calibration command reached every finger, not that calibration finished. Confirm with the status query:

```python theme={null}
from wuji_sdk import TactileState

status = hand.tactile_status("thumb")   # "thumb" / "index" / "middle" / "ring" / "pinky"
print(status.model)                     # TactileType.Thumb (40 points) or TactileType.Standard (34 points)
print(status.state)                     # TactileState.Ready / TactileState.Calibrating
```

`tactile_status` is a blocking query (up to about 1 s). Calibration is complete once polling reads `state == TactileState.Ready`.

### Flash Log Export (Diagnostics)

```python theme={null}
# This call blocks until the export finishes.
result = hand.export_flash_logs("./logs")
print(result["path"])      # str: absolute path to the exported JSONL file
print(result["frames"])    # int: number of exported log records
```

The SDK writes one `flash_<serial>_<YYYY-MM-DD_HHMMSS>.jsonl` file to `out_dir`. Omit `out_dir` to use `~/.wuji/logs`.

The SDK writes to a temporary file and makes the JSONL file available only after the export finishes. If the export fails, the SDK removes the temporary file, so the output directory contains no partial export.

<Info>
  When multiple sessions export logs from the same device, the SDK retries automatically. If the export still can't finish, the call reports that the device is busy. The SDK removes the temporary files and doesn't keep an unfinished log.
</Info>

<Warning>
  Flash log export requires compatible device firmware and Wuji SDK versions. If the versions are incompatible, the export fails without creating log files. Update the device firmware and the Wuji SDK together, then retry.
</Warning>

### ip (SET/GET)

**Read and write the device's static IP address**. `set` writes the new IP to the device's flash. The firmware doesn't hot-swap the running Ethernet stack, so the new IP takes effect only after the next reboot. Between `set` and reboot, `get` still returns the current IP.

```python theme={null}
hand.ip().get()                    # read the current IP, e.g. "192.168.1.110"
hand.ip().set("192.168.1.201")     # write to flash, takes effect after reboot
```

The IP must be unique on the subnet. Avoid addresses already used by other devices or the host network adapter.

Changing the IP takes four steps: `set` the new address, `reboot` the device, reconnect at the new IP, and `get` to confirm.

```python theme={null}
hand.ip().set("192.168.1.201")
hand.reboot()
manager.disconnect_all()
# wait for the device to reboot and Ethernet to come up, about 8 seconds
hand = manager.connect(address="192.168.1.201:50001", device_name="wuji_hand_2")
hand.ip().get()                    # → "192.168.1.201"
```

<Warning>
  The new IP takes effect only after a reboot. `set` writes to flash without changing the live connection. Before the reboot, `get` still returns the current IP.
</Warning>

### reboot (EXEC)

**Reboot the device**. The device disconnects and needs a fresh connection. Pair it with `ip().set()` to apply a new IP written to flash.

```python theme={null}
hand.reboot()
```

### imu (SUB)

**Onboard IMU stream**: pushes acceleration and angular velocity at a native output rate of 100 Hz, adjustable down through the subscription handle. The device does no onboard orientation fusion, so the `orientation` field is unavailable and `orientation_covariance[0] = -1` marks it invalid per the ROS `sensor_msgs/Imu` convention. Data is reported in the IMU sensor's own frame, and the mapping between its axes and the hand structure will be defined in a future hand model update.

```python theme={null}
sub = hand.imu().subscribe()                 # → Subscription[ImuData]
s = await sub.recv_async()
a, g = s.linear_acceleration, s.angular_velocity   # m/s² and rad/s
print(s.header.seq, a.x, a.y, a.z, g.x, g.y, g.z)
sub.close()
```

`ImuData` is a cross-device shared type. See the [Wuji SDK data reference](/en/docs/wuji-sdk/data-reference/#imudata) for field definitions.

## Joint Traversal

`JointHandle` provides `label` / `index` (for indexing into 20-element return arrays such as `effort_limit()` / `mit_params()`), plus single-joint resources and actions. `FingerHandle` traverses joints by finger.

| Method | Return type | Description |
| - | - | - |
| `hand.joints()` | `list[JointHandle]` | All 20 joints |
| `hand.fingers()` | `list[FingerHandle]` | All 5 fingers |

```python theme={null}
for joint in hand.joints():
    print(joint.label, joint.index)        # e.g. "thumb_S1" 0

for finger in hand.fingers():
    for joint in finger.joints():          # 4 joints per finger, in S1..S4 order
        print(joint.label)                 # "thumb_S1", "thumb_S2", ...
```

### JointHandle

**Joint handle**: provides `label` / `index`, single-joint resources, and single-joint actions.

| Member | Type | Description |
| - | - | - |
| `label` | `str` | Property. Joint label, format `{finger}_S{1..4}`, e.g. `"thumb_S1"`, `"pinky_S4"` |
| `index` | `int` | Property. Global index (0–19) |
| `effort_limit()` | Resource (SET/GET) | Single-joint torque cap (A) |
| `error_code()` | Resource (GET) | The joint's current error code — decode with `describe_error()`. Returns an integer, so format it as `f"0x{code:04X}"` if you print it directly |
| `status_word()` | Resource (GET) | The joint's status word |
| `enable()` / `disable()` | Action | Enable / disable this joint |
| `clear_fault()` | Action | Clear this joint's faults |
| `set_origin()` / `clear_origin()` | Action | Set / clear this joint's user origin |

### FingerHandle

**Finger handle**: traverse the 4 joints of this finger.

| Method | Return type | Description |
| - | - | - |
| `joints()` | `list[JointHandle]` | The 4 joints of this finger, in S1..S4 order |

## Data Types

### JointDiagnosticsFrame / JointDiagnosticsEntry

**Joint diagnostics frame**: frame type of the `hand.joint_diagnostics().subscribe()` stream. Variable-length, online joints only.

```python theme={null}
class JointDiagnosticsFrame:
    header: FrameHeader                    # seq / timestamp_us / frame_id
    num_joints: int
    joints: list[JointDiagnosticsEntry]
    comm: Hand2CommSummary                 # frame-level comm health summary, 1 Hz refresh (see Hand2CommSummary)

class JointDiagnosticsEntry:
    nid: int                               # node ID — identifies the joint across frames
    status_word: StatusWord                # status word
    current: float                         # current (A)
    vbus_v_fb: float                       # bus voltage (V)
    mcu_temp_c_fb: float                   # MCU temperature (°C)
    error_code_current: int                # current error code (integer value, shown in hex when the entry is printed) — decode with describe_error()
    comm_response_rate_pct: int            # bus response rate over the last second, 0–100
    comm_timeout_total: int                # cumulative bus timeouts for this joint
```

### MitParam

**MIT parameters**: element of `mit_params().get()`.

```python theme={null}
class MitParam:
    kp: float
    kd: float
```

`set` accepts a single `(kp, kd)` (applied to all joints) or a list of 20 (per joint). `get` returns 20 entries — offline joints are `None`.

### JointStateFrame

**Joint state frame**: return value of `hand.joint_states().subscribe().recv()`. Variable-length, online joints only.

| Attribute | Type | Description |
| - | - | - |
| `header` | `FrameHeader` | `seq` / `timestamp_us` / `frame_id` |
| `num_joints` | `int` | Joint count in this frame |
| `joints` | `list[JointStateEntry]` | Joint state array |

### JointStateEntry

**Single-joint state**.

| Attribute | Type | Description |
| - | - | - |
| `nid` | `int` | Node ID — identifies the joint across frames |
| `position` | `float` | Position (rad, joint-side) |
| `velocity` | `float` | Velocity (rad/s) |
| `effort` | `float` | Torque (A) |

### JointCommand

**Joint command**: element of the `joint_command().publish().send()` argument.

```python theme={null}
class JointCommand:
    position: float    # position (rad)
    velocity: float    # velocity (rad/s)
    effort: float      # torque feedforward (A)
```

### HandCommunicationDiagnostics

**Communication diagnostics** data.

```python theme={null}
class HandCommunicationDiagnostics:
    fingers: list[FingerCommunicationDiagnostics]   # length 5

class FingerCommunicationDiagnostics:
    tx_frame_total: int
    rx_frame_total: int
    tx_kbps: int
    rx_kbps: int
    error_per_sec: int
    crc_error_total: int
    frame_format_error_total: int
    uart_hw_error_total: int
    transfer_stats: list[TransferStats]
    nodes: list[NodeDiagnostics]    # per-node: online / ms_since_last_response / response_rate_pct
```

### FingertipSensorInfo / FingertipSensorData

**Fingertip sensor metadata and data frames**: return value of `get_fingertip_info(finger)` and frame type of the `fingertip_{finger}_data` subscriptions.

```python theme={null}
class FingertipSensorInfo:
    header: FrameHeader    # seq / timestamp_us / frame_id
    digest: int            # CRC32 of the info content — binds data frames to this info
    model: str             # sensor model string (may be empty)
    device_type: int       # sensor type enum
    rate_hz: float         # native output rate of the data stream (Hz, currently 100) — unchanged when the push rate is lowered
    format: str            # JSON: data-frame layout (point_count / point_stride / point_fields / aggregate_fields / encoding)

class FingertipSensorData:
    header: FrameHeader
    info_digest: int       # digest of the info in effect — on mismatch, re-fetch the info
    data: list[int]        # pure value payload, interpreted per info.format
```

### TactileStatus / TactileType / TactileState

**Tactile status**: return value of `hand.tactile_status(finger)`.

```python theme={null}
class TactileStatus:
    model: TactileType     # Standard (34-point fingertip) or Thumb (40-point thumb)
    state: TactileState    # Ready or Calibrating
```

Enum members compare equal to the firmware integer, for example `TactileState.Calibrating == 1`.

### StatusWord

**Status word**: the type of the `status_word` field on [JointDiagnosticsEntry](#jointdiagnosticsframe-/-jointdiagnosticsentry), the public status view exposed by the SDK.

| Field | Type | Description |
| - | - | - |
| `ext_state` | `int` | Extended state value (such as Init / Ready / Enabled / Stopped) |
| `ext_state_name` | `str` | Semantic name of `ext_state` |
| `position_limit_active` | `bool` | Position limit triggered |
| `velocity_limit_active` | `bool` | Velocity limit triggered |
| `current_limit_active` | `bool` | Current limit triggered |

### Hand2CommSummary

**Frame-level communication health summary**: the type of the `comm` field on [JointDiagnosticsFrame](#jointdiagnosticsframe-/-jointdiagnosticsentry), combining the device-internal bus view with the SDK-local end-to-end (Ethernet) view. The device-internal part comes from a snapshot the SDK refreshes automatically at 1 Hz — no polling required.

| Field | Type | Description |
| - | - | - |
| `age_ms` | `int` | Age of the device-internal snapshot in milliseconds. `65535` = never sampled successfully (the device-internal fields are zeros). `65534` = saturated, the snapshot is 65.5 s or older |
| `tactile_online_mask` | `int` | Fingertip tactile online bitmap, bit0 = thumb … bit4 = pinky |
| `e2e_received` | `int` | Frames received across all subscribed streams (cumulative) |
| `e2e_lost` | `int` | Frames lost on the Ethernet segment, detected via sequence gaps (cumulative) |
| `e2e_reordered` | `int` | Late/reordered frames (cumulative) |
| `e2e_duplicates` | `int` | Duplicate frames (cumulative) |
| `e2e_window_loss_x100` | `int` | Loss rate over the last second, in 0.01% units |
| `rpc_total` | `int` | Requests sent (cumulative). This field is `0` when RPC statistics aren't available for the current connection. In that case, `rpc_retries` and `rpc_timeouts` don't represent the actual retry or timeout counts |
| `rpc_retries` | `int` | Request retransmissions (cumulative) |
| `rpc_timeouts` | `int` | Requests that ultimately timed out (cumulative) |
| `comm_get_failures` | `int` | Failed internal snapshot refreshes (cumulative) |
| `tactile_response_rate_pct` | `list[int]` | Per-finger tactile (node 5) bus response rate, 0–100, index 0 = thumb … 4 = pinky |
| `tactile_timeout_total` | `list[int]` | Per-finger tactile cumulative bus timeouts, same index order |
| `sdk_dropped` | `int` | Frames dropped inside the SDK because one of its in-process hops fell behind (cumulative) — either your subscription consumer or the internal stream handler. Distinct from `e2e_lost`, which is network loss |

<Info>
  **Reading the device-internal fields.** `age_ms`, `tactile_*` and the per-joint
  `comm_*` fields all come from one snapshot the SDK refreshes at 1 Hz. The refresh is
  deliberately skipped while a firmware upgrade or a tactile calibration is running, so a
  briefly stale snapshot is expected rather than a fault. Tell the two cases apart with
  `comm_get_failures`: a growing `age_ms` while `comm_get_failures` stays flat means the
  refresh is being skipped on purpose, while `comm_get_failures` increasing means the refresh
  itself is failing.
</Info>

<Warning>
  **Counters saturate, they do not wrap.** Every counter in this table stops at its
  maximum (65535 for the 16-bit ones — `e2e_reordered`, `e2e_duplicates`,
  `rpc_retries`, `rpc_timeouts`, `comm_get_failures` — and 4294967295 for the 32-bit ones)
  and stays there. On a 1 kHz stream a long-running session can reach those ceilings, so
  read these as *deltas between two samples* rather than as absolute lifetime totals.
</Warning>

## Joint Numbering

| Finger | S1 | S2 | S3 | S4 |
| - | - | - | - | - |
| thumb | 0 | 1 | 2 | 3 |
| index | 4 | 5 | 6 | 7 |
| middle | 8 | 9 | 10 | 11 |
| ring | 12 | 13 | 14 | 15 |
| pinky | 16 | 17 | 18 | 19 |

Label format: `{finger}_S{1..4}` (e.g. `thumb_S1`, `pinky_S4`).

## Exception Handling

All SDK operation errors raise a unified `WujiException` whose message carries an error-type prefix (`Disconnected`, `Timeout`, `PathNotFound`, `SchemaMismatch`, `SerializeError`, …). Use `try` / `except` as needed.

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

try:
    hand.joint_states().subscribe()
except WujiException as e:
    print(f"SDK error: {e}")
```


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