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

# Tutorial

> Wuji Hand SDK (wujihandpy) tutorial covering device connection, synchronous/asynchronous read/write, and real-time control.

## Device Connection and Selection

This section describes how to connect to dexterous hand devices in wujihandpy and how to filter devices when multiple devices are present.

### Connection and Filtering Interface Overview

```python theme={null}
class Hand:
    def __init__(
        self,
        serial_number: str | None = None,
        *,
        side: str | None = None,
        ...,
    ) -> None:
        ...
```

### Connecting to a Device

Call `wujihandpy.Hand()` to create a `Hand` object and connect to the dexterous hand:

```python theme={null}
hand = wujihandpy.Hand()
```

If only one dexterous hand is connected to the system, it will automatically connect to that device.

### Filtering Devices

When multiple dexterous hands are connected to the system, using the no-argument constructor will result in an error. For example:

```
[ERROR] 2 devices found with specified vendor id (0x0483)
[ERROR]   Device 1 (0483:2000): Serial Number = 337238793233 <-- Matched #1
[ERROR]   Device 2 (0483:2000): Serial Number = 337238873233 <-- Matched #2
[ERROR] To ensure correct device selection, please specify the Serial Number
Traceback (most recent call last):
  File "/workspaces/quickstart.py", line 5, in <module>
    hand = wujihandpy.Hand()
           ^^^^^^^^^^^^^^^^^
RuntimeError: Failed to init.
```

There are two ways to specify which device to connect to.

#### Select by serial number

Pass the `serial_number` parameter to identify the device explicitly:

```python theme={null}
hand = wujihandpy.Hand(serial_number="337238793233")
```

This ensures that the program always connects to the correct device instance.

#### Select by handedness

When the system has exactly one left hand and one right hand connected, the `side` parameter selects the device directly, without needing to remember any serial number:

```python theme={null}
left = wujihandpy.Hand(side="left")
right = wujihandpy.Hand(side="right")
```

The SDK enumerates all matching devices, reads the handedness identifier from each, and connects to the matching one.

<Info>
  **`side` and `serial_number` are mutually exclusive**

  Pass only one of them. Passing both raises `ValueError`. `side` accepts only the string `"left"` or `"right"`. Any other value raises `ValueError`.

  Common failure cases:

  * No hand of the requested side is connected → raises `ConnectionError` with `No <side> hand found`
  * Two hands of the same side are connected → raises `ConnectionError` suggesting `serial_number` instead
</Info>

### Querying Serial Numbers

The dexterous hand has two types of serial numbers:

* **USB serial number**: An identifier in the USB hardware descriptor, used to distinguish multiple devices during connection. The constructor's `serial_number` parameter uses this number.
* **Product serial number**: A product tracking number stored in firmware, used for after-sales service and device management.

#### Querying the USB serial number

Execute the following command in the terminal to view all connected dexterous hands:

```bash theme={null}
lsusb -v -d 0483:2000 | grep iSerial
```

Example output:

```
  iSerial                 3 337238793233
                            ^^^^^^^^^^^^  <-- USB Serial Number
```

The highlighted number is the USB serial number, which can be passed directly to the constructor's `serial_number` parameter.

#### Querying the product serial number

After connecting to a device, call `get_product_sn()` to retrieve the product serial number:

```python theme={null}
hand = wujihandpy.Hand()
product_sn = hand.get_product_sn()
print(product_sn)  # e.g. "WJ-DH5-R-2501001"
```

## Reading Data

This section describes how to read data in synchronous mode.

### Reading Interface Overview

Calling `read_*` series functions sends a synchronous read request to the dexterous hand and blocks the current thread until the device returns a result. Function return indicates that the read has completed successfully.

<Warning>
  **Latency Characteristics of Non-Real-Time Interfaces**

  Each synchronous request causes approximately 5\~10 ms of blocking and is suitable for non-real-time tasks. Do not use these APIs in real-time control loops or high-frequency loops requiring high real-time performance.

  For real-time control scenarios, consider:

  * Using [Unidirectional/Bidirectional Real-Time Controllers](#real-time-control)
  * Using [Async APIs](#async-read/write) in a low-speed bypass loop
</Warning>

### Single Value Reading

When the read level matches the object level, the function returns a single scalar value.

```python theme={null}
class Hand:
    def read_<dataname>(self) -> datatype: ...

class Finger:
    def read_<dataname>(self) -> datatype: ...

class Joint:
    def read_<dataname>(self) -> datatype: ...
```

**Examples**
Read the dexterous hand's uptime (Hand level) using `hand`:

```python theme={null}
time = hand.read_system_time()
```

Read the current position of the index finger's proximal joint (Joint level) using `joint`:

```python theme={null}
position = hand.finger(1).joint(0).read_joint_actual_position()
```

### Batch Reading

When the read level is lower than the object level, the function returns a matrix containing multiple data points.

```python theme={null}
class Hand:
    def read_<dataname>(self) -> np.NDArray[datatype]: ...

class Finger:
    def read_<dataname>(self) -> np.NDArray[datatype]: ...

class Joint:
    def read_<dataname>(self) -> np.NDArray[datatype]: ...
```

**Examples**
Request the current position data for all joints in the entire hand (20 joints in total) using `hand` (Joint level):

```python theme={null}
positions = hand.read_joint_actual_position()
```

The function returns an `np.NDArray[np.float64]`, where each row corresponds to a finger and each column corresponds to a joint:

```python theme={null}
>>> print(positions)
[[ 0.975  0.523  0.271 -0.450]
 [ 0.382  0.241 -0.003 -0.275]
 [-0.299  0.329  0.067 -0.286]
 [-0.122  0.228  0.315 -0.178]
 [ 0.205  0.087  0.288 -0.149]]
```

Similarly, you can request all joint positions for a single finger using `finger`:

```python theme={null}
>>> print(hand.finger(1).read_joint_actual_position())
[ 0.382  0.241 -0.003 -0.275]
```

### More Reading Examples

* [example/joint/1.read.py](https://github.com/wuji-technology/wujihandpy/blob/main/example/joint/1.read.py)

## Writing Data

This section describes how to write data in synchronous mode.

### Writing Interface Overview

Calling `write_*` series functions sends a synchronous write request to the dexterous hand and blocks the current thread until the device returns a result. Function return indicates that the write has completed successfully.

<Warning>
  **Latency Characteristics of Non-Real-Time Interfaces**

  Each synchronous request causes approximately 5\~10 ms of blocking and is suitable for non-real-time tasks. Do not use these APIs in real-time control loops or high-frequency loops requiring high real-time performance.

  For real-time control scenarios, consider:

  * Using [Unidirectional/Bidirectional Real-Time Controllers](#real-time-control)
  * Using [Async APIs](#async-read/write) in a low-speed bypass loop
</Warning>

### Single Value Writing

When the write level matches the object level, the function uses a single scalar value.

```python theme={null}
class Hand:
    def write_<dataname>(self, datatype) -> None: ...

class Finger:
    def write_<dataname>(self, datatype) -> None: ...

class Joint:
    def write_<dataname>(self, datatype) -> None: ...
```

**Examples**
Enable the index finger's proximal joint (Joint level):

```python theme={null}
hand.finger(1).joint(0).write_joint_enabled(True)
```

### Batch Writing

When the write level is lower than the object level, the function can accept either a single scalar value or a matrix containing multiple data points.

```python theme={null}
class Hand:
    @typing.overload
    def write_<dataname>(self, datatype) -> None: ...
    @typing.overload
    def write_<dataname>(self, np.NDArray[datatype]) -> None: ...

class Finger:
    @typing.overload
    def write_<dataname>(self, datatype) -> None: ...
    @typing.overload
    def write_<dataname>(self, np.NDArray[datatype]) -> None: ...

class Joint:
    @typing.overload
    def write_<dataname>(self, datatype) -> None: ...
    @typing.overload
    def write_<dataname>(self, np.NDArray[datatype]) -> None: ...
```

**Examples**
Enable all joints in the entire hand (Joint level) using `hand`:

```python theme={null}
hand.write_joint_enabled(True)
```

Or enable the index finger while disabling all other fingers:

```python theme={null}
hand.write_joint_enabled(
    np.array(
        [
            #  J1     J2     J3     J4
            [False, False, False, False],  # F1
            [ True,  True,  True,  True],  # F2
            [False, False, False, False],  # F3
            [False, False, False, False],  # F4
            [False, False, False, False],  # F5
        ],
        dtype=bool,
    )
)
```

Similarly, you can write target control angles to all joints in the entire hand:

```python theme={null}
hand.write_joint_target_position(0.0)
# Equals to
hand.write_joint_target_position(
    np.array(
        [
            # J1   J2   J3   J4
            [0.0, 0.0, 0.0, 0.0],  # F1
            [0.0, 0.0, 0.0, 0.0],  # F2
            [0.0, 0.0, 0.0, 0.0],  # F3
            [0.0, 0.0, 0.0, 0.0],  # F4
            [0.0, 0.0, 0.0, 0.0],  # F5
        ],
        dtype=np.float64,
    )
)
```

### Invalid Value Writing

Typically, invalid value writes are handled automatically, such as:

* Angles exceeding the valid range are automatically clamped to upper and lower bounds.

However, this is not absolute. When values have logical errors, such as:

* Setting the effort limit of a joint to an invalid value (too large or too small)

This will cause the operation to fail and throw a TimeoutError.

### More Writing Examples

* [example/joint/2.write.py](https://github.com/wuji-technology/wujihandpy/blob/main/example/joint/2.write.py)

## Real-Time Control

This section describes how to control the dexterous hand in real-time mode in wujihandpy.

### Real-Time Control Interface Overview

```python theme={null}
class Hand:
    def realtime_controller(self, enable_upstream: bool, filter: filter.IFilter) -> IController:
        ...

class IController:
    def set_joint_target_position(self, value_array: np.NDArray[np.float64]) -> None:
        ...
    def get_joint_actual_position(self) -> np.NDArray[np.float64]:
        ...
    def get_joint_actual_effort(self) -> np.NDArray[np.float64]:
        ...
    def close(self) -> None:
        ...
```

Calling `Hand.realtime_controller()` puts the dexterous hand into real-time control mode and returns a real-time controller object `IController`.
After entering real-time control mode, a 16 kHz real-time filter starts on the motor side to smooth control signals into a stable 16 kHz stream.

### Filters

The real-time controller requires a filter object (`filter.IFilter`) to smoothly adjust target values.

<Info>
  The initial value of the filter is automatically set to the actual position of the dexterous hand, so there is no need to worry about initial transition issues.
</Info>

wujihandpy provides the following commonly used filters:

* `filter.LowPass(cutoff_freq: float)`: First-order low-pass filter with adjustable cutoff frequency. When set to a cutoff frequency >= 1 kHz, the filter is disabled.

<Warning>
  Because filter logic runs on the motor driver board, custom filters are not supported.
</Warning>

### Context Manager

`IController` can be used as a context manager:

```python theme={null}
with hand.realtime_controller(
    enable_upstream=False,
    filter=wujihandpy.filter.LowPass(cutoff_freq=2.0),
) as controller:
    ...  # Use controller to perform real-time control
```

When the `with` block ends, `controller.close()` is automatically called, ensuring the dexterous hand correctly exits real-time control mode.

#### Automatic Destruction

If a context manager is not used, `IController` will automatically call `close()` during destruction. Due to Python's garbage collection delay, the dexterous hand may not immediately exit real-time mode. If precise control of exit timing is not needed, you can directly rely on destruction behavior.

### Real-Time Writing

```python theme={null}
def set_joint_target_position(self, value_array: np.NDArray[np.float64]) -> None: ...
```

The `set_joint_target_position()` function sends target control values to the motor driver board as quickly as possible without blocking.

### Real-Time Reading

The motor automatically reports current position and effort at a 1 kHz rate. The following functions are non-blocking and immediately return the latest cached readings.

```python theme={null}
def get_joint_actual_position(self) -> np.NDArray[np.float64]: ...
def get_joint_actual_effort(self) -> np.NDArray[np.float64]: ...
```

<Info>
  **What is Effort?**

  Effort is the actuation value in current space, filtered before output. It is not the actual measured current—think of it as relative actuation strength.

  Typical use cases:

  * Collision detection: a sudden increase in effort indicates joint obstruction
  * Load monitoring: calculate output percentage via `effort / effort_limit`
</Info>

**Example**

```python theme={null}
import wujihandpy
import numpy as np

hand = wujihandpy.Hand()
try:
    hand.write_joint_enabled(True)
    # Enter real-time control mode using a first-order low-pass filter with 5 Hz cutoff frequency
    with hand.realtime_controller(
        enable_upstream=True,
        filter=wujihandpy.filter.LowPass(cutoff_freq=5.0)
    ) as controller:
        while True:
            # Real-time write target position (non-blocking)
            controller.set_joint_target_position(np.zeros((5, 4)))
            # Real-time read position and effort (non-blocking)
            position = controller.get_joint_actual_position()
            effort = controller.get_joint_actual_effort()
            print(f"pos: {position[1, :]}  effort: {effort[1, :]}")
finally:
    hand.write_joint_enabled(False)
```

### Advanced Real-Time Control Examples

* [example/joint/3.realtime.py](https://github.com/wuji-technology/wujihandpy/blob/main/example/joint/3.realtime.py)

## Async Read/Write

This section describes how to read/write data in asynchronous mode in wujihandpy.

### Async Interface Overview

Both read and write interfaces provide asynchronous versions with function names suffixed with `_async` for performing non-blocking I/O operations in coroutine environments.

```python theme={null}
async def read_<dataname>_async(self) -> datatype: ...
async def read_<dataname>_async(self) -> np.NDArray[datatype]: ...  # Batch read

async def write_<dataname>_async(self, value: datatype): ...
async def write_<dataname>_async(self, values: np.NDArray[datatype]): ...  # Batch write
```

When calling asynchronous interfaces, use `await`. The event loop is not blocked during the wait. Function return indicates that the operation has completed successfully.

<Warning>
  **Latency Characteristics of Non-Real-Time Interfaces**

  Asynchronous requests also require 5\~10 ms to complete. They are not inherently faster. They simply do not block the event loop.

  Compared to synchronous interfaces, the only difference with asynchronous interfaces is that they allow other coroutine tasks to execute during the wait.
  Therefore, do not call these APIs in real-time control loops or high-frequency loops requiring high real-time performance.

  For real-time control scenarios, consider:

  * Using [Unidirectional/Bidirectional Real-Time Controllers](#real-time-control)
  * Using [Async APIs](#async-read/write) in a low-speed bypass loop
</Warning>

**Example**

```python theme={null}
import asyncio
import wujihandpy

async def main():
    hand = wujihandpy.Hand()
    try:
        await hand.write_joint_enabled_async(True)
        # Execute the following tasks in parallel:
        # - Move index finger joint 0 to 1.57 rad (approximately 90° downward)
        # - Move index finger joint 2 to 1.57 rad (approximately 90° downward)
        # - Wait for 0.5 seconds simultaneously
        await asyncio.gather(
            hand.finger(1).joint(0).write_joint_target_position_async(1.57),
            hand.finger(1).joint(2).write_joint_target_position_async(1.57),
            asyncio.sleep(0.5),
        )
    finally:
        await hand.write_joint_enabled_async(False)

# Start the event loop and run the main task
asyncio.run(main())
```

### Advanced Async Examples

* [example/joint/4.async.py](https://github.com/wuji-technology/wujihandpy/blob/main/example/joint/4.async.py)

## Multi-Threaded Operations

This section describes how to perform multi-threaded operations in wujihandpy.

### Multi-Threading Interface Overview

By default, wujihandpy performs thread safety checks on the `Hand` object to prevent data race conditions from concurrent multi-threaded access. If you need to use the same `Hand` object in a multi-threaded environment, you must first call `Hand.disable_thread_safe_check()` to disable this check.

```python theme={null}
class Hand:
    def disable_thread_safe_check(self) -> None:
        ...
```

### Multi-Threading Safety Considerations

<Warning>
  **Important**: After disabling the thread safety check, you are responsible for ensuring thread safety.
</Warning>

* You must call `disable_thread_safe_check()` **before** performing multi-threaded operations on the `Hand` object
* You must use synchronization mechanisms such as `threading.Lock` to protect concurrent access to the `Hand` object
* It is recommended to wrap all operations on the `Hand` object and its derived objects (such as `IController`) with `with lock:` blocks

**Example**
The following example demonstrates how to use the real-time controller in a multi-threaded environment:

```python theme={null}
import threading
import time
import wujihandpy
import numpy as np
import math

hand = wujihandpy.Hand()

# Disable thread safety check to allow multi-threaded access
hand.disable_thread_safe_check()
print("Thread safety check disabled, multi-threaded access enabled")

# Create mutex lock to ensure thread safety
lock = threading.Lock()

def reader_thread(ctrl):
    """Periodically read joint positions"""
    while True:
        with lock:
            position = ctrl.get_joint_actual_position()
        print(f"[Reader] Position:\n{position}")
        time.sleep(0.01)

def writer_thread(ctrl):
    """Periodically write target positions (cosine wave motion)"""
    x = 0
    while True:
        y = (1 - math.cos(x)) * 0.8
        target = np.array(
            [
                [0, 0, 0, 0],  # F1
                [y, 0, y, y],  # F2
                [y, 0, y, y],  # F3
                [y, 0, y, y],  # F4
                [y, 0, y, y],  # F5
            ],
            dtype=np.float64,
        )
        with lock:
            ctrl.set_joint_target_position(target)
        print(f"[Writer] Target: {y:.4f} rad")
        x += math.pi / 100.0
        time.sleep(0.01)

hand.write_joint_enabled(True)
with hand.realtime_controller(
    enable_upstream=True,
    filter=wujihandpy.filter.LowPass(cutoff_freq=5.0)
) as ctrl:
    threads = [
        threading.Thread(target=reader_thread, args=(ctrl,)),
        threading.Thread(target=writer_thread, args=(ctrl,)),
    ]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
```

### Expected Output

After running the example, the terminal will display output similar to the following:

```
Thread safety check disabled, multi-threaded access enabled
[Reader] Position:
[[ 0.012  0.003 -0.005  0.008]
 [ 0.015  0.002  0.007  0.011]
 [ 0.009 -0.001  0.004  0.006]
 [ 0.018  0.005  0.012  0.009]
 [ 0.011  0.004  0.008  0.013]]
[Writer] Target: 0.0000 rad
[Reader] Position:
[[ 0.012  0.003 -0.005  0.008]
 [ 0.016  0.002  0.008  0.012]
 [ 0.010 -0.001  0.005  0.007]
 [ 0.019  0.005  0.013  0.010]
 [ 0.012  0.004  0.009  0.014]]
[Writer] Target: 0.0004 rad
[Writer] Target: 0.0016 rad
[Reader] Position:
[[ 0.012  0.003 -0.005  0.008]
 [ 0.018  0.002  0.010  0.014]
 [ 0.012 -0.001  0.007  0.009]
 [ 0.021  0.005  0.015  0.012]
 [ 0.014  0.004  0.011  0.016]]
...
```

* The first line indicates that the thread safety check has been disabled
* `[Reader]` prints the current joint position matrix (5×4, corresponding to 4 joints for each of 5 fingers)
* `[Writer]` prints the current target position (cosine wave, starting from 0 and gradually increasing)
* The two threads output alternately, and the order may be interleaved (this is normal behavior for multi-threading)
* F1 (thumb) remains stationary, while F2 (index), F3 (middle), F4 (ring), and F5 (pinky) perform periodic flexion-extension movements

### Advanced Multi-Threading Examples

* [example/joint/5.multithread.py](https://github.com/wuji-technology/wujihandpy/blob/main/example/joint/5.multithread.py)

## USB Disconnect Handling

During operation, the USB connection between the dexterous hand and the computer can drop—for example, when the data cable is unplugged. This section describes how wujihandpy behaves on disconnect and how to catch disconnect exceptions.

### Disconnect Behavior

On disconnect, wujihandpy raises an exception. Catch it to recover without crashing the process.

An operation started after a disconnect raises Python's built-in `ConnectionError` immediately, without waiting for the 500 ms timeout. Affected interfaces include:

* Synchronous read/write (`read_*` / `write_*`)
* Asynchronous read/write (`read_*_async` / `write_*_async`)

<Info>
  An async request already in flight at the moment of disconnect is an exception: it ends with `TimeoutError` rather than `ConnectionError`. If your code runs concurrent async operations, catch their common base class `OSError` to cover both disconnect and timeout.
</Info>

### Catching Disconnect Exceptions

Wrap device operations in a `try` block and catch `ConnectionError` to handle disconnects:

```python theme={null}
import wujihandpy
import time


def main():
    hand = wujihandpy.Hand()
    print("Reading. Unplug USB to test disconnect handling.\n")

    try:
        while True:
            v = hand.read_input_voltage()
            print(f"input_voltage: {v:.2f}V")
            time.sleep(0.5)
    except ConnectionError as e:
        print(f"\nDisconnect detected: {e}")


if __name__ == "__main__":
    main()
```

Run the script, then unplug the USB cable while it reads. The loop is interrupted immediately by `ConnectionError`, and the program exits normally without crashing.

### Disconnect Detection in Real-Time Control Mode

<Warning>
  The real-time controller's non-blocking interfaces (`set_joint_target_position()`, `get_joint_actual_position()`, `get_joint_actual_effort()`) do **not** raise `ConnectionError` on disconnect.
</Warning>

Real-time control loops rely on high-speed PDO data. On disconnect, these interfaces simply stop updating the cache instead of raising an exception. To detect a disconnect in real-time scenarios, periodically call a synchronous read (such as `read_input_voltage()`) as a probe in a low-speed bypass loop, and catch `ConnectionError`.

When the real-time controller is used as a context manager, the end of the `with` block automatically calls `close()` to clean up. `close()` also returns safely on disconnect, so no extra handling is needed.

### More Disconnect Handling Examples

* [example/joint/6.disconnect.py](https://github.com/wuji-technology/wujihandpy/blob/main/example/joint/6.disconnect.py)


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