Tutorial
1. 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.
1.1 Interface Overview
class Hand:
def __init__(
self,
serial_number: str | None = None,
*,
side: str | None = None,
...,
) -> None:
...1.2 Connecting to a Device
Call wujihandpy.Hand() to create a Hand object and connect to the dexterous hand:
hand = wujihandpy.Hand()If only one dexterous hand is connected to the system, it will automatically connect to that device.
1.3 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:
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:
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.
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
ConnectionErrorwithNo <side> hand found - Two hands of the same side are connected → raises
ConnectionErrorsuggestingserial_numberinstead
1.4 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_numberparameter 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:
lsusb -v -d 0483:2000 | grep iSerialExample output:
iSerial 3 337238793233
^^^^^^^^^^^^ <-- USB Serial NumberThe 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:
hand = wujihandpy.Hand()
product_sn = hand.get_product_sn()
print(product_sn) # e.g. "WJ-DH5-R-2501001"2. Reading Data
This section describes how to read data in synchronous mode.
2.1 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.
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
- Using Async APIs in a low-speed bypass loop
2.2 Single Value Reading
When the read level matches the object level, the function returns a single scalar value.
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:
time = hand.read_system_time()Read the current position of the index finger's proximal joint (Joint level) using joint:
position = hand.finger(1).joint(0).read_joint_actual_position()2.3 Batch Reading
When the read level is lower than the object level, the function returns a matrix containing multiple data points.
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):
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:
>>> 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:
>>> print(hand.finger(1).read_joint_actual_position())
[ 0.382 0.241 -0.003 -0.275]2.4 More Examples
3. Writing Data
This section describes how to write data in synchronous mode.
3.1 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.
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
- Using Async APIs in a low-speed bypass loop
3.2 Single Value Writing
When the write level matches the object level, the function uses a single scalar value.
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):
hand.finger(1).joint(0).write_joint_enabled(True)3.3 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.
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:
hand.write_joint_enabled(True)Or enable the index finger while disabling all other fingers:
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:
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,
)
)3.4 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.
3.5 More Examples
4. Real-Time Control
This section describes how to control the dexterous hand in real-time mode in wujihandpy.
4.1 Interface Overview
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.
4.2 Filters
The real-time controller requires a filter object (filter.IFilter) to smoothly adjust target values.
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.
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.
Because filter logic runs on the motor driver board, custom filters are not supported.
4.3 Context Manager
IController can be used as a context manager:
with hand.realtime_controller(
enable_upstream=False,
filter=wujihandpy.filter.LowPass(cutoff_freq=2.0),
) as controller:
... # Use controller to perform real-time controlWhen 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.
4.4 Real-Time Writing
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.
4.5 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.
def get_joint_actual_position(self) -> np.NDArray[np.float64]: ...
def get_joint_actual_effort(self) -> np.NDArray[np.float64]: ...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
Example
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)4.6 Advanced Examples
5. Async Read/Write
This section describes how to read/write data in asynchronous mode in wujihandpy.
5.1 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.
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 writeWhen calling asynchronous interfaces, use await. The event loop is not blocked during the wait. Function return indicates that the operation has completed successfully.
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
- Using Async APIs in a low-speed bypass loop
Example
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())5.2 Advanced Examples
6. Multi-threaded Operations
This section describes how to perform multi-threaded operations in wujihandpy.
6.1 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.
class Hand:
def disable_thread_safe_check(self) -> None:
...6.2 Multi-threading Safety Considerations
Important: After disabling the thread safety check, you are responsible for ensuring thread safety.
- You must call
disable_thread_safe_check()before performing multi-threaded operations on theHandobject - You must use synchronization mechanisms such as
threading.Lockto protect concurrent access to theHandobject - It is recommended to wrap all operations on the
Handobject and its derived objects (such asIController) withwith lock:blocks
Example The following example demonstrates how to use the real-time controller in a multi-threaded environment:
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()6.3 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
6.4 Advanced Examples
7. 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.
7.1 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)
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.
7.2 Catching Disconnect Exceptions
Wrap device operations in a try block and catch ConnectionError to handle disconnects:
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.
7.3 Disconnect Detection in Real-Time Control Mode
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.
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.