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

# API Reference

> Complete Wuji Hand SDK (wujihandpy) API reference, including field lists and method descriptions for Hand, Finger, and Joint classes.

## Field List

This section introduces all available data fields in wujihandpy.

### Hand Layer

This layer contains device-unique general fields, such as handedness, firmware version, etc.

#### handedness

This field indicates the handedness of the dexterous hand (left/right hand, 0 for right, 1 for left).

* **Type**: `uint8`
* **Read/Write**: Read-only

#### firmware\_version

This field indicates the version number of the dexterous hand firmware.

* **Type**: `uint32`
* **Read/Write**: Read-only

#### firmware\_date

This field indicates the compilation date of the dexterous hand firmware.

* **Type**: `uint32`
* **Read/Write**: Read-only

#### system\_time

This field indicates the system running time of the dexterous hand, in milliseconds.

* **Type**: `uint32`
* **Read/Write**: Read-only

#### temperature

This field indicates the current temperature near the wrist of the dexterous hand, in degrees Celsius.

* **Type**: `float32`
* **Read/Write**: Read-only

#### input\_voltage

This field indicates the measured input voltage of the dexterous hand, in volts.

* **Type**: `float32`
* **Read/Write**: Read-only

### Joint Layer

This layer contains fields where each joint holds an independent copy, such as enabled status, target position, etc.

#### joint\_firmware\_version

This field records the version number of the joint driver board firmware.

* **Type**: `uint32`
* **Read/Write**: Read-only

#### joint\_firmware\_date

This field records the compilation date of the joint driver board firmware.

* **Type**: `uint32`
* **Read/Write**: Read-only

#### joint\_control\_mode

This field is used to set the control mode of the joint.

* **Type**: `uint16`
* **Read/Write**: Write-only

> Note: The mode switches automatically, and this field should not be modified under normal circumstances.

#### joint\_sin\_level

This field is used to set the sine wave excitation level of the joint drive.

* **Type**: `uint16`
* **Read/Write**: Write-only

> Note: This field is used for internal debugging and should not be modified under normal circumstances.

#### joint\_effort\_limit

This field is used to set the upper effort limit allowed by the joint.

* **Type**: `float64`
* **Unit**: Ampere (A)
* **Default**: `1.5`
* **Valid Range**: `0.0 ~ 3.5` (values exceeding 3.5 will be clamped to 3.5)
* **Read/Write**: Read-Write

<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, useful for load monitoring and collision detection. See [joint\_effort](#joint_effort) for details.
</Info>

<Warning>
  **Thermal Protection: Effort Limit is dynamically adjusted based on temperature**

  To protect the device, the firmware automatically reduces the effective effort limit based on joint temperature:

  | Temperature Status | Trigger Condition | Effective Effort Limit |
  | - | - | - |
  | Normal | Below 85°C | User-set value (default 1.5A) |
  | Temperature Warning (Tier 1) | Greater than or equal to 85°C | User-set value × 70% |
  | Temperature Alert (Tier 2) | Greater than or equal to 93°C | User-set value × 40% |

  Recovery uses a 4°C hysteresis threshold:

  * Tier 2 returns to Tier 1 when temperature drops below 89°C
  * Tier 1 returns to Normal when temperature drops below 81°C

  When the temperature returns to a safe range, the effort limit automatically recovers.
</Warning>

<Warning>
  **Warning: Modifying Effort Limit may cause device damage**

  Modifying this parameter changes the maximum output capability of the joint. Users are responsible for any damage caused by adjusting the effort limit.

  * Default value of 1.5 is suitable for most applications
  * Maximum value is 3.5, values exceeding this will be automatically clamped
  * Increasing this value increases joint load capacity but also increases the risk of damage
  * Affects joint lifespan and accelerates component wear with unpredictable impact
  * May cause irreversible damage to joints under certain operating conditions
  * May accelerate thermal protection triggering, reducing continuous operation time
  * May cause other serious consequences beyond those listed above
</Warning>

#### joint\_bus\_voltage

This field records the current measured bus voltage of the joint.

* **Type**: `float32`
* **Read/Write**: Read-only

#### joint\_temperature

This field records the current temperature of the joint driver board, in degrees Celsius.

* **Type**: `float32`
* **Read/Write**: Read-only

#### joint\_reset\_error

Writing `1` to this field can clear the error status reported by the joint.

* **Type**: `uint16`
* **Read/Write**: Write-only

<Warning>
  This field can only clear error codes and cannot resolve issues with the dexterous hand.
  Do not abuse this field, especially do not use it in a loop, as it may cause damage to the dexterous hand.
</Warning>

#### joint\_error\_code

This field indicates the current error code of each joint, used for diagnosing operational status.
A value of `0` indicates no abnormality in the joint.

* **Type**: `uint32`
* **Read/Write**: Read-only

#### joint\_effort

This field provides real-time effort feedback of the joint.

* **Type**: `float32`
* **Unit**: Ampere (A)
* **Read/Write**: Read-only

<Info>
  **Understanding Effort**

  Effort is the actuation effort in current space, filtered before output.

  * It is **not** the actual measured current
  * It is **not** the direct output of the current loop
  * Should be interpreted as relative actuation strength, suitable for load monitoring and collision detection

  Typical use cases:

  * Collision detection: sudden increase in effort indicates joint obstruction
  * Output monitoring: calculate output percentage via `effort / effort_limit`

  This field is only accessible through `IController.get_joint_actual_effort()` in real-time control mode.
</Info>

#### joint\_enabled

This field controls the enabling and disabling of the joint.

* **Type**: `bool`
* **Default**: `False`
* **Valid Range**: `True / False`
* **Read/Write**: Write-only

#### joint\_actual\_position

This field provides the actual position of the joint.

* **Type**: `float64`
* **Read/Write**: Read-only

#### joint\_target\_position

Writing to this field can issue the target control angle of the joint. If the value exceeds the valid range, it will be automatically clamped to the allowed interval.

* **Type**: `float64`
* **Valid Range**: `joint_lower_limit ~ joint_upper_limit`
* **Read/Write**: Write-only

#### joint\_upper\_limit

This field records the upper position limit of the joint.

* **Type**: `float64`
* **Read/Write**: Read-only

<Info>
  **Difference from URDF Joint Range**

  There are slight precision errors during joint processing, causing the actual physical limits of each joint to vary slightly.
  The dexterous hand calibrates the actual limits of each joint at the factory, and the results are written to the `joint_upper_limit` and `joint_lower_limit` fields.
  Therefore, these two fields record the calibrated actual physical limit values, which are usually slightly larger than the range defined in the URDF file.

  The joint range in the URDF file is a theoretical design value, conservatively scaled to account for processing errors, and can be used for simulation or neural network training.
  While `joint_upper_limit` and `joint_lower_limit` represent the actual measured range of motion.
</Info>

#### joint\_lower\_limit

This field records the lower position limit of the joint.

* **Type**: `float64`
* **Read/Write**: Read-only

## API List

### Hand Class

#### Constructor

* `__init__(serial_number=None, *, side=None, usb_pid=0x2000, usb_vid=0x0483, mask=None) -> None`
  *Initialize connection to the dexterous hand. Supports specifying the device by serial number, handedness, USB ID, or device mask.*

  * `serial_number`: USB serial number string to select one device exactly
  * `side`: `"left"` or `"right"`. Selects the device by handedness. Mutually exclusive with `serial_number` (see [Tutorial - Filtering Devices](/en/docs/wujihandpy/tutorial#filtering-devices))

#### Finger Access

* `finger(index) -> Finger`
  *Get the finger object at the specified index, index range 0-4*

#### Thread Safety Control

* `disable_thread_safe_check() -> None`
  *Disable thread safety check to allow concurrent multi-threaded access to the Hand object*

<Warning>
  **Note**: After disabling the thread safety check, you must use synchronization mechanisms such as `threading.Lock` to ensure thread-safe operations.
</Warning>

#### System Information

##### Firmware Information

* `read_firmware_version(timeout=0.5) -> uint32`
  *Synchronously read firmware version*

* `read_firmware_version_async(timeout=0.5) -> Awaitable[uint32]`
  *Asynchronously read firmware version*

* `read_firmware_version_unchecked(timeout=0.5) -> None`
  *Non-blocking read firmware version*

* `get_firmware_version() -> uint32`
  *Get cached firmware version*

* `read_firmware_date(timeout=0.5) -> uint32`
  *Synchronously read firmware compilation date*

* `read_firmware_date_async(timeout=0.5) -> Awaitable[uint32]`
  *Asynchronously read firmware compilation date*

* `read_firmware_date_unchecked(timeout=0.5) -> None`
  *Non-blocking read firmware compilation date*

* `get_firmware_date() -> uint32`
  *Get cached firmware compilation date*

* `read_handedness(timeout=0.5) -> uint8`
  *Synchronously read left/right hand identifier (0=right, 1=left)*

* `read_handedness_async(timeout=0.5) -> Awaitable[uint8]`
  *Asynchronously read left/right hand identifier*

* `read_handedness_unchecked(timeout=0.5) -> None`
  *Non-blocking read left/right hand identifier*

* `get_handedness() -> uint8`
  *Get cached left/right hand identifier*

##### System Status

* `read_system_time(timeout=0.5) -> uint32`
  *Synchronously read system running time (milliseconds)*

* `read_system_time_async(timeout=0.5) -> Awaitable[uint32]`
  *Asynchronously read system running time*

* `read_system_time_unchecked(timeout=0.5) -> None`
  *Non-blocking read system running time*

* `get_system_time() -> uint32`
  *Get cached system running time*

* `read_temperature(timeout=0.5) -> float32`
  *Synchronously read palm temperature (Celsius)*

* `read_temperature_async(timeout=0.5) -> Awaitable[float32]`
  *Asynchronously read palm temperature*

* `read_temperature_unchecked(timeout=0.5) -> None`
  *Non-blocking read palm temperature*

* `get_temperature() -> float32`
  *Get cached palm temperature*

* `read_input_voltage(timeout=0.5) -> float32`
  *Synchronously read input voltage (volts)*

* `read_input_voltage_async(timeout=0.5) -> Awaitable[float32]`
  *Asynchronously read input voltage*

* `read_input_voltage_unchecked(timeout=0.5) -> None`
  *Non-blocking read input voltage*

* `get_input_voltage() -> float32`
  *Get cached input voltage*

#### Joint Data (Batch Read, Returns {5,4} Array)

##### Position Data

* `read_joint_actual_position(timeout=0.5) -> NDArray[float64]`
  *Synchronously read all joint actual positions (radians)*

* `read_joint_actual_position_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *Asynchronously read all joint actual positions*

* `read_joint_actual_position_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint actual positions*

* `get_joint_actual_position() -> NDArray[float64]`
  *Get cached all joint actual positions*

* `read_joint_upper_limit(timeout=0.5) -> NDArray[float64]`
  *Synchronously read all joint upper limit positions (radians)*

* `read_joint_upper_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *Asynchronously read all joint upper limit positions*

* `read_joint_upper_limit_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint upper limit positions*

* `get_joint_upper_limit() -> NDArray[float64]`
  *Get cached all joint upper limit positions*

* `read_joint_lower_limit(timeout=0.5) -> NDArray[float64]`
  *Synchronously read all joint lower limit positions (radians)*

* `read_joint_lower_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *Asynchronously read all joint lower limit positions*

* `read_joint_lower_limit_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint lower limit positions*

* `get_joint_lower_limit() -> NDArray[float64]`
  *Get cached all joint lower limit positions*

##### Status Monitoring

* `read_joint_bus_voltage(timeout=0.5) -> NDArray[float32]`
  *Synchronously read all joint bus voltages (volts)*

* `read_joint_bus_voltage_async(timeout=0.5) -> Awaitable[NDArray[float32]]`
  *Asynchronously read all joint bus voltages*

* `read_joint_bus_voltage_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint bus voltages*

* `get_joint_bus_voltage() -> NDArray[float32]`
  *Get cached all joint bus voltages*

* `read_joint_temperature(timeout=0.5) -> NDArray[float32]`
  *Synchronously read all joint temperatures (Celsius)*

* `read_joint_temperature_async(timeout=0.5) -> Awaitable[NDArray[float32]]`
  *Asynchronously read all joint temperatures*

* `read_joint_temperature_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint temperatures*

* `get_joint_temperature() -> NDArray[float32]`
  *Get cached all joint temperatures*

* `read_joint_error_code(timeout=0.5) -> NDArray[uint32]`
  *Synchronously read all joint error codes*

* `read_joint_error_code_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *Asynchronously read all joint error codes*

* `read_joint_error_code_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint error codes*

* `get_joint_error_code() -> NDArray[uint32]`
  *Get cached all joint error codes*

##### Effort Limit Data

* `read_joint_effort_limit(timeout=0.5) -> NDArray[float64]`
  *Synchronously read all joint effort limits (Ampere)*
* `read_joint_effort_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *Asynchronously read all joint effort limits*
* `read_joint_effort_limit_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint effort limits*
* `get_joint_effort_limit() -> NDArray[float64]`
  *Get cached all joint effort limits*

##### Firmware Information

* `read_joint_firmware_version(timeout=0.5) -> NDArray[uint32]`
  *Synchronously read all joint firmware versions*

* `read_joint_firmware_version_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *Asynchronously read all joint firmware versions*

* `read_joint_firmware_version_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint firmware versions*

* `get_joint_firmware_version() -> NDArray[uint32]`
  *Get cached all joint firmware versions*

* `read_joint_firmware_date(timeout=0.5) -> NDArray[uint32]`
  *Synchronously read all joint firmware build dates*

* `read_joint_firmware_date_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *Asynchronously read all joint firmware build dates*

* `read_joint_firmware_date_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint firmware build dates*

* `get_joint_firmware_date() -> NDArray[uint32]`
  *Get cached all joint firmware build dates*

#### Joint Control (Supports Single Value or {5,4} Array)

##### Enable Control

* `write_joint_enabled(value, timeout=0.5) -> None`
  *Synchronously set all joint enable states*
* `write_joint_enabled(value_array, timeout=0.5) -> None`
  *Synchronously batch set each joint enable state*
* `write_joint_enabled_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set all joint enable states*
* `write_joint_enabled_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch set each joint enable state*
* `write_joint_enabled_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set all joint enable states*
* `write_joint_enabled_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch set each joint enable state*

##### Position Control

* `write_joint_target_position(value, timeout=0.5) -> None`
  *Synchronously set all joint target positions (radians)*
* `write_joint_target_position(value_array, timeout=0.5) -> None`
  *Synchronously batch set each joint target position*
* `write_joint_target_position_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set all joint target positions*
* `write_joint_target_position_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch set each joint target position*
* `write_joint_target_position_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set all joint target positions*
* `write_joint_target_position_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch set each joint target position*

##### Mode Control

* `write_joint_control_mode(value, timeout=0.5) -> None`
  *Synchronously set all joint control modes*
* `write_joint_control_mode(value_array, timeout=0.5) -> None`
  *Synchronously batch set each joint control mode*
* `write_joint_control_mode_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set all joint control modes*
* `write_joint_control_mode_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch set each joint control mode*
* `write_joint_control_mode_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set all joint control modes*
* `write_joint_control_mode_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch set each joint control mode*

##### Effort Limit Configuration

<Warning>
  **Warning**: Modifying effort limit may cause device damage, see field description for details.
</Warning>

* `write_joint_effort_limit(value, timeout=0.5) -> None`
  *Synchronously set all joint effort limits (Ampere)*
* `write_joint_effort_limit(value_array, timeout=0.5) -> None`
  *Synchronously batch set each joint effort limit*
* `write_joint_effort_limit_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set all joint effort limits*
* `write_joint_effort_limit_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch set each joint effort limit*
* `write_joint_effort_limit_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set all joint effort limits*
* `write_joint_effort_limit_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch set each joint effort limit*

##### Other Parameter Configuration

* `write_joint_sin_level(value, timeout=0.5) -> None`
  *Synchronously set all joint sine wave excitation levels*

* `write_joint_sin_level(value_array, timeout=0.5) -> None`
  *Synchronously batch set each joint sine wave excitation level*

* `write_joint_sin_level_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set all joint sine wave excitation levels*

* `write_joint_sin_level_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch set each joint sine wave excitation level*

* `write_joint_sin_level_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set all joint sine wave excitation levels*

* `write_joint_sin_level_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch set each joint sine wave excitation level*

* `write_joint_reset_error(value, timeout=0.5) -> None`
  *Synchronously reset all joint errors*

* `write_joint_reset_error(value_array, timeout=0.5) -> None`
  *Synchronously batch reset each joint error*

* `write_joint_reset_error_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously reset all joint errors*

* `write_joint_reset_error_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch reset each joint error*

* `write_joint_reset_error_unchecked(value, timeout=0.5) -> None`
  *Non-blocking reset all joint errors*

* `write_joint_reset_error_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch reset each joint error*

#### Real-time Controller

* `realtime_controller(enable_upstream, filter) -> IController`
  *Create a real-time controller for high-frequency control scenarios*

### Finger Class

#### Joint Access

* `joint(index) -> Joint`
  *Get the joint object at the specified index, index range 0-3*

#### Joint Data (Returns 4-Element Array)

##### Position Data

* `read_joint_actual_position(timeout=0.5) -> NDArray[float64]`
  *Synchronously read all joint actual positions of the finger (radians)*

* `read_joint_actual_position_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *Asynchronously read all joint actual positions of the finger*

* `read_joint_actual_position_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint actual positions of the finger*

* `get_joint_actual_position() -> NDArray[float64]`
  *Get cached all joint actual positions of the finger*

* `read_joint_upper_limit(timeout=0.5) -> NDArray[float64]`
  *Synchronously read all joint upper limit positions of the finger (radians)*

* `read_joint_upper_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *Asynchronously read all joint upper limit positions of the finger*

* `read_joint_upper_limit_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint upper limit positions of the finger*

* `get_joint_upper_limit() -> NDArray[float64]`
  *Get cached all joint upper limit positions of the finger*

* `read_joint_lower_limit(timeout=0.5) -> NDArray[float64]`
  *Synchronously read all joint lower limit positions of the finger (radians)*

* `read_joint_lower_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *Asynchronously read all joint lower limit positions of the finger*

* `read_joint_lower_limit_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint lower limit positions of the finger*

* `get_joint_lower_limit() -> NDArray[float64]`
  *Get cached all joint lower limit positions of the finger*

##### Status Monitoring

* `read_joint_bus_voltage(timeout=0.5) -> NDArray[float32]`
  *Synchronously read all joint bus voltages of the finger (volts)*

* `read_joint_bus_voltage_async(timeout=0.5) -> Awaitable[NDArray[float32]]`
  *Asynchronously read all joint bus voltages of the finger*

* `read_joint_bus_voltage_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint bus voltages of the finger*

* `get_joint_bus_voltage() -> NDArray[float32]`
  *Get cached all joint bus voltages of the finger*

* `read_joint_temperature(timeout=0.5) -> NDArray[float32]`
  *Synchronously read all joint temperatures of the finger (Celsius)*

* `read_joint_temperature_async(timeout=0.5) -> Awaitable[NDArray[float32]]`
  *Asynchronously read all joint temperatures of the finger*

* `read_joint_temperature_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint temperatures of the finger*

* `get_joint_temperature() -> NDArray[float32]`
  *Get cached all joint temperatures of the finger*

* `read_joint_error_code(timeout=0.5) -> NDArray[uint32]`
  *Synchronously read all joint error codes of the finger*

* `read_joint_error_code_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *Asynchronously read all joint error codes of the finger*

* `read_joint_error_code_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint error codes of the finger*

* `get_joint_error_code() -> NDArray[uint32]`
  *Get cached all joint error codes of the finger*

##### Effort Limit Data

* `read_joint_effort_limit(timeout=0.5) -> NDArray[float64]`
  *Synchronously read all joint effort limits of the finger (Ampere)*
* `read_joint_effort_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *Asynchronously read all joint effort limits of the finger*
* `read_joint_effort_limit_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint effort limits of the finger*
* `get_joint_effort_limit() -> NDArray[float64]`
  *Get cached all joint effort limits of the finger*

##### Firmware Information

* `read_joint_firmware_version(timeout=0.5) -> NDArray[uint32]`
  *Synchronously read all joint firmware versions of the finger*

* `read_joint_firmware_version_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *Asynchronously read all joint firmware versions of the finger*

* `read_joint_firmware_version_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint firmware versions of the finger*

* `get_joint_firmware_version() -> NDArray[uint32]`
  *Get cached all joint firmware versions of the finger*

* `read_joint_firmware_date(timeout=0.5) -> NDArray[uint32]`
  *Synchronously read all joint firmware build dates of the finger*

* `read_joint_firmware_date_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *Asynchronously read all joint firmware build dates of the finger*

* `read_joint_firmware_date_unchecked(timeout=0.5) -> None`
  *Non-blocking read all joint firmware build dates of the finger*

* `get_joint_firmware_date() -> NDArray[uint32]`
  *Get cached all joint firmware build dates of the finger*

#### Joint Control (Supports Single Value or 4-Element Array)

##### Enable Control

* `write_joint_enabled(value, timeout=0.5) -> None`
  *Synchronously set all joint enable states of the finger*
* `write_joint_enabled(value_array, timeout=0.5) -> None`
  *Synchronously batch set each joint enable state of the finger*
* `write_joint_enabled_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set all joint enable states of the finger*
* `write_joint_enabled_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch set each joint enable state of the finger*
* `write_joint_enabled_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set all joint enable states of the finger*
* `write_joint_enabled_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch set each joint enable state of the finger*

##### Position Control

* `write_joint_target_position(value, timeout=0.5) -> None`
  *Synchronously set all joint target positions of the finger (radians)*
* `write_joint_target_position(value_array, timeout=0.5) -> None`
  *Synchronously batch set each joint target position of the finger*
* `write_joint_target_position_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set all joint target positions of the finger*
* `write_joint_target_position_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch set each joint target position of the finger*
* `write_joint_target_position_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set all joint target positions of the finger*
* `write_joint_target_position_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch set each joint target position of the finger*

##### Mode Control

* `write_joint_control_mode(value, timeout=0.5) -> None`
  *Synchronously set all joint control modes of the finger*
* `write_joint_control_mode(value_array, timeout=0.5) -> None`
  *Synchronously batch set each joint control mode of the finger*
* `write_joint_control_mode_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set all joint control modes of the finger*
* `write_joint_control_mode_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch set each joint control mode of the finger*
* `write_joint_control_mode_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set all joint control modes of the finger*
* `write_joint_control_mode_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch set each joint control mode of the finger*

##### Effort Limit Configuration

<Warning>
  **Warning**: Modifying effort limit may cause device damage, see field description for details.
</Warning>

* `write_joint_effort_limit(value, timeout=0.5) -> None`
  *Synchronously set all joint effort limits of the finger (Ampere)*
* `write_joint_effort_limit(value_array, timeout=0.5) -> None`
  *Synchronously batch set each joint effort limit of the finger*
* `write_joint_effort_limit_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set all joint effort limits of the finger*
* `write_joint_effort_limit_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch set each joint effort limit of the finger*
* `write_joint_effort_limit_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set all joint effort limits of the finger*
* `write_joint_effort_limit_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch set each joint effort limit of the finger*

##### Other Parameter Configuration

* `write_joint_sin_level(value, timeout=0.5) -> None`
  *Synchronously set all joint sine wave excitation levels of the finger*

* `write_joint_sin_level(value_array, timeout=0.5) -> None`
  *Synchronously batch set each joint sine wave excitation level of the finger*

* `write_joint_sin_level_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set all joint sine wave excitation levels of the finger*

* `write_joint_sin_level_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch set each joint sine wave excitation level of the finger*

* `write_joint_sin_level_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set all joint sine wave excitation levels of the finger*

* `write_joint_sin_level_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch set each joint sine wave excitation level of the finger*

* `write_joint_reset_error(value, timeout=0.5) -> None`
  *Synchronously reset all joint errors of the finger*

* `write_joint_reset_error(value_array, timeout=0.5) -> None`
  *Synchronously batch reset each joint error of the finger*

* `write_joint_reset_error_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously reset all joint errors of the finger*

* `write_joint_reset_error_async(value_array, timeout=0.5) -> Awaitable[None]`
  *Asynchronously batch reset each joint error of the finger*

* `write_joint_reset_error_unchecked(value, timeout=0.5) -> None`
  *Non-blocking reset all joint errors of the finger*

* `write_joint_reset_error_unchecked(value_array, timeout=0.5) -> None`
  *Non-blocking batch reset each joint error of the finger*

### Joint Class

#### Joint Data (Returns Single Value)

##### Position Data

* `read_joint_actual_position(timeout=0.5) -> float64`
  *Synchronously read joint actual position (radians)*

* `read_joint_actual_position_async(timeout=0.5) -> Awaitable[float64]`
  *Asynchronously read joint actual position*

* `read_joint_actual_position_unchecked(timeout=0.5) -> None`
  *Non-blocking read joint actual position*

* `get_joint_actual_position() -> float64`
  *Get cached joint actual position*

* `read_joint_upper_limit(timeout=0.5) -> float64`
  *Synchronously read joint upper limit position (radians)*

* `read_joint_upper_limit_async(timeout=0.5) -> Awaitable[float64]`
  *Asynchronously read joint upper limit position*

* `read_joint_upper_limit_unchecked(timeout=0.5) -> None`
  *Non-blocking read joint upper limit position*

* `get_joint_upper_limit() -> float64`
  *Get cached joint upper limit position*

* `read_joint_lower_limit(timeout=0.5) -> float64`
  *Synchronously read joint lower limit position (radians)*

* `read_joint_lower_limit_async(timeout=0.5) -> Awaitable[float64]`
  *Asynchronously read joint lower limit position*

* `read_joint_lower_limit_unchecked(timeout=0.5) -> None`
  *Non-blocking read joint lower limit position*

* `get_joint_lower_limit() -> float64`
  *Get cached joint lower limit position*

##### Status Monitoring

* `read_joint_bus_voltage(timeout=0.5) -> float32`
  *Synchronously read joint bus voltage (volts)*

* `read_joint_bus_voltage_async(timeout=0.5) -> Awaitable[float32]`
  *Asynchronously read joint bus voltage*

* `read_joint_bus_voltage_unchecked(timeout=0.5) -> None`
  *Non-blocking read joint bus voltage*

* `get_joint_bus_voltage() -> float32`
  *Get cached joint bus voltage*

* `read_joint_temperature(timeout=0.5) -> float32`
  *Synchronously read joint temperature (Celsius)*

* `read_joint_temperature_async(timeout=0.5) -> Awaitable[float32]`
  *Asynchronously read joint temperature*

* `read_joint_temperature_unchecked(timeout=0.5) -> None`
  *Non-blocking read joint temperature*

* `get_joint_temperature() -> float32`
  *Get cached joint temperature*

* `read_joint_error_code(timeout=0.5) -> uint32`
  *Synchronously read joint error code*

* `read_joint_error_code_async(timeout=0.5) -> Awaitable[uint32]`
  *Asynchronously read joint error code*

* `read_joint_error_code_unchecked(timeout=0.5) -> None`
  *Non-blocking read joint error code*

* `get_joint_error_code() -> uint32`
  *Get cached joint error code*

##### Effort Limit Data

* `read_joint_effort_limit(timeout=0.5) -> float64`
  *Synchronously read joint effort limit (Ampere)*
* `read_joint_effort_limit_async(timeout=0.5) -> Awaitable[float64]`
  *Asynchronously read joint effort limit*
* `read_joint_effort_limit_unchecked(timeout=0.5) -> None`
  *Non-blocking read joint effort limit*
* `get_joint_effort_limit() -> float64`
  *Get cached joint effort limit*

##### Firmware Information

* `read_joint_firmware_version(timeout=0.5) -> uint32`
  *Synchronously read joint firmware version*

* `read_joint_firmware_version_async(timeout=0.5) -> Awaitable[uint32]`
  *Asynchronously read joint firmware version*

* `read_joint_firmware_version_unchecked(timeout=0.5) -> None`
  *Non-blocking read joint firmware version*

* `get_joint_firmware_version() -> uint32`
  *Get cached joint firmware version*

* `read_joint_firmware_date(timeout=0.5) -> uint32`
  *Synchronously read joint firmware build date*

* `read_joint_firmware_date_async(timeout=0.5) -> Awaitable[uint32]`
  *Asynchronously read joint firmware build date*

* `read_joint_firmware_date_unchecked(timeout=0.5) -> None`
  *Non-blocking read joint firmware build date*

* `get_joint_firmware_date() -> uint32`
  *Get cached joint firmware build date*

#### Joint Control (Single Value)

##### Enable Control

* `write_joint_enabled(value, timeout=0.5) -> None`
  *Synchronously set joint enable state*
* `write_joint_enabled_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set joint enable state*
* `write_joint_enabled_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set joint enable state*

##### Position Control

* `write_joint_target_position(value, timeout=0.5) -> None`
  *Synchronously set joint target position (radians)*
* `write_joint_target_position_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set joint target position*
* `write_joint_target_position_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set joint target position*

##### Mode Control

* `write_joint_control_mode(value, timeout=0.5) -> None`
  *Synchronously set joint control mode*
* `write_joint_control_mode_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set joint control mode*
* `write_joint_control_mode_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set joint control mode*

##### Effort Limit Configuration

<Warning>
  **Warning**: Modifying effort limit may cause device damage, see field description for details.
</Warning>

* `write_joint_effort_limit(value, timeout=0.5) -> None`
  *Synchronously set joint effort limit (Ampere)*
* `write_joint_effort_limit_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set joint effort limit*
* `write_joint_effort_limit_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set joint effort limit*

##### Other Parameter Configuration

* `write_joint_sin_level(value, timeout=0.5) -> None`
  *Synchronously set joint sine wave excitation level*

* `write_joint_sin_level_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously set joint sine wave excitation level*

* `write_joint_sin_level_unchecked(value, timeout=0.5) -> None`
  *Non-blocking set joint sine wave excitation level*

* `write_joint_reset_error(value, timeout=0.5) -> None`
  *Synchronously reset joint error*

* `write_joint_reset_error_async(value, timeout=0.5) -> Awaitable[None]`
  *Asynchronously reset joint error*

* `write_joint_reset_error_unchecked(value, timeout=0.5) -> None`
  *Non-blocking reset joint error*

### IController Interface

#### Context Management

* `__enter__() -> IController`
  *Context manager entry supporting with statement*
* `__exit__(arg0, arg1, arg2) -> None`
  *Context manager exit supporting with statement*
* `close() -> None`
  *Manually close the real-time controller*

#### Real-time Data Retrieval

* `get_joint_actual_position() -> NDArray[float64]`
  *Real-time get all joint actual positions (high-frequency read, returns {5,4} array)*
* `get_joint_actual_effort() -> NDArray[float64]`
  *Real-time get all joint efforts (high-frequency read, returns {5,4} array, unit: Ampere)*

<Info>
  **Effort Real-time Feedback**

  `get_joint_actual_effort()` returns the actuation effort in current space, filtered before output.

  * Only available in real-time controller mode
  * Suitable for collision detection and load monitoring
  * Calculate output percentage via `effort / effort_limit`
</Info>

#### Real-time Data Setting

* `set_joint_target_position(value_array) -> None`
  *Real-time set all joint target positions (high-frequency write, parameter is {5,4} array)*

***

### Array Shape Description

* **Hand Class Batch Operations**: Returns/receives arrays shaped `{5, 4}` (5 fingers × 4 joints)
* **Finger Class Batch Operations**: Returns/receives arrays shaped `{4}` (4 joints)
* **Joint Class Operations**: Returns/receives scalar values (single joint)
* **IController Operations**: Handles arrays shaped `{5, 4}` for high-frequency real-time control

### Operation Mode Description

* **Synchronous Operations** (`read_*`, `write_*`): Blocking operations, ensure read/write success
* **Asynchronous Operations** (`*_async`): Returns Awaitable objects, supports await syntax
* **Non-blocking Operations** (`*_unchecked`): Returns immediately, does not guarantee operation success, suitable for high-frequency scenarios
* **Cached Retrieval** (`get_*`): Gets the most recently read data, does not block

***

## Tactile Sensing Glove

The Tactile Sensing Glove is an optional tactile perception accessory for Wuji Hand. The SDK provides device connection and data reading through the `TactileGlove` class.

### Core Classes

#### TactileHandedness

Handedness enum:

* `TactileHandedness.LEFT` (0): Left hand
* `TactileHandedness.RIGHT` (1): Right hand

#### TactileFrame

Single-frame tactile data with the following properties:

| Property | Type | Description |
| - | - | - |
| `hand` | `TactileHandedness` | Left or right hand |
| `sequence` | `int` (uint16) | Frame counter, wraps at 65535 |
| `timestamp_ms` | `int` (uint32) | Milliseconds since device boot |
| `pressure` | `numpy.ndarray` (24, 32) float32 | Pressure matrix |

**Pressure value semantics:**

* Range: `[0.0, 1.0]`—`0.0` = no contact, `1.0` = maximum contact
* `NaN` marks an invalid cell (not part of any finger or palm region). Skip such cells with `numpy.isnan()`

CRC validation happens inside the SDK before delivery, so callers never see a bad-CRC frame.

#### TactileGlove

Connection and data reading for the Tactile Sensing Glove.

**Constructor:**

```python theme={null}
TactileGlove(serial_number: str | None = None)
```

<Info>
  `serial_number` is the USB device serial number. Pass `None` to auto-discover the only attached device. Raises if more than one is present.
</Info>

### Device Connection

#### Auto-discovery

```python theme={null}
from wujihandpy import TactileGlove

glove = TactileGlove()
glove.connect()  # Returns True on success
```

#### Specific device

```python theme={null}
glove = TactileGlove(serial_number="ABC123")
glove.connect()
```

#### Context Manager

```python theme={null}
with TactileGlove() as glove:
    frame = glove.read_frame()
```

#### Connection management methods

* `connect() -> bool`: Connect to device. Returns `True` on success
* `disconnect() -> None`: Disconnect and stop streaming
* `is_connected() -> bool`: Query connection status
* `get_handedness() -> TactileHandedness`: Return device handedness

### Data Reading

#### Blocking read

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

frame = glove.read_frame(timeout_ms=100)
print(f"Sequence: {frame.sequence}")
print(f"Pressure shape: {frame.pressure.shape}")  # (24, 32)
print(f"Max pressure: {np.nanmax(frame.pressure)}")
```

* Performs frame synchronization and CRC validation internally
* Raises `RuntimeError` on timeout
* Raises `RuntimeError` on disconnect

#### Streaming callback

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

def on_frame(frame):
    print(f"Seq={frame.sequence}, Max={np.nanmax(frame.pressure)}")

glove.start_streaming(on_frame)
# ... application logic ...
glove.stop_streaming()
```

* Callback invoked on the internal reader thread. Must not block
* Register a disconnect callback with `set_disconnect_callback(fn)` to react to USB drop. Streaming stops automatically afterward
* `start_streaming` and `read_frame` are mutually exclusive
* All blocking operations release the Python GIL

### Configuration and Diagnostics

| Method | Description |
| - | - |
| `set_sample_rate_hz(hz)` | Set sample rate, 1–120 Hz |
| `get_sample_rate_hz() -> int` | Read current sample rate |
| `set_streaming(enable)` | Toggle the device data-frame stream |
| `get_streaming_enabled() -> bool` | Read the streaming-enabled flag |
| `set_disconnect_callback(callback)` | Register a `Callable[[], None]` invoked once when the USB device drops off the bus |
| `get_device_info() -> TactileDeviceInfo` | Read `serial`, `hw_revision`, `fw_version` |
| `get_fw_build() -> TactileFwBuild` | Read firmware build (`git_short_sha`) |
| `get_diagnostics() -> TactileDiagnostics` | Read counters: `uptime_ms`, `frame_count`, `crc_err_count`, `dropout_count`, `usb_reset_count` |
| `reset_counters()` | Zero the diagnostic counters |
| `get_device_time() -> TactileDeviceTime` | Read device monotonic clock (`device_monotonic_ns`) |
| `sync_host_epoch(host_unix_ns) -> TactileSyncResult` | Exchange host UTC nanoseconds for the device clock at the same moment |
| `reset_device()` | Soft reset. The device drops off the bus and re-enumerates—call `connect()` again afterward |
| `enter_bootloader(magic)` | Jump to bootloader for OTA. Pass `TACTILE_BOOTLOADER_MAGIC` |

### Exception Handling

| Exception | Trigger |
| - | - |
| `RuntimeError` | `read_frame` timeout, USB device disconnected during read, `read_frame` called when not connected, or `start_streaming` called while already streaming |
| `TactileError` | Device-reported protocol error from a command (bad length, bad CRC, unknown command, bad payload) |

### Complete Example

Covers the full `TactileGlove` workflow: auto-discovery and device info queries, host/device clock synchronization, sample-rate configuration, blocking reads and streaming callbacks, data saving, and diagnostic counters. `reset_device` and `enter_bootloader` close the current connection (device reset or jump to bootloader, respectively) and appear only as trailing comments.

```python theme={null}
import time
import numpy as np
from wujihandpy import (
    TactileGlove,
    TactileHandedness,
    TactileError,
    TACTILE_BOOTLOADER_MAGIC,
)


def on_disconnect():
    print("[event] USB disconnect detected")


# 1. Connect — context manager calls disconnect() on exit.
#    To target a specific device: TactileGlove(serial_number="ABC123").
with TactileGlove() as glove:
    if not glove.is_connected():
        print("Not connected")
        exit(1)

    glove.set_disconnect_callback(on_disconnect)

    # 2. Device info
    info = glove.get_device_info()
    hw = ".".join(str(x) for x in info.hw_revision[:3])
    fw = ".".join(str(x) for x in info.fw_version[:3])
    build = glove.get_fw_build()
    print(f"Serial: {info.serial}")
    print(f"Hardware: v{hw}, Firmware: v{fw} ({build.git_short_sha})")

    # 3. Handedness
    hand = glove.get_handedness()
    side = "left" if hand == TactileHandedness.LEFT else "right"
    print(f"Handedness: {side}")

    # 4. Clock sync and device time
    sync = glove.sync_host_epoch(time.time_ns())
    skew_ns = sync.device_ns_at_sync - sync.host_ns_echo
    dev_time = glove.get_device_time()
    print(f"Host/device clock skew: {skew_ns} ns")
    print(f"Device monotonic time: {dev_time.device_monotonic_ns} ns")

    # 5. Sample rate
    print(f"Current sample rate: {glove.get_sample_rate_hz()} Hz")
    glove.set_sample_rate_hz(60)
    print(f"New sample rate: {glove.get_sample_rate_hz()} Hz")

    # 6. Streaming toggle + single blocking read
    glove.set_streaming(True)
    print(f"Streaming enabled: {glove.get_streaming_enabled()}")
    try:
        frame = glove.read_frame(timeout_ms=200)
        side_str = "L" if frame.hand == TactileHandedness.LEFT else "R"
        print(
            f"Frame: seq={frame.sequence}, ts={frame.timestamp_ms} ms, "
            f"hand={side_str}, max={np.nanmax(frame.pressure):.3f}"
        )
    except RuntimeError as e:
        print(f"Read failed (timeout or disconnect): {e}")
    except TactileError as e:
        print(f"Device protocol error: {e}")

    # 7. Reset counters, then stream and collect
    glove.reset_counters()
    frames = []

    def collect(frame):
        frames.append(frame.pressure.copy())

    glove.start_streaming(collect)
    time.sleep(5)  # Collect for 5 seconds
    glove.stop_streaming()

    # 8. Diagnostics
    diag = glove.get_diagnostics()
    print(
        f"Collected {diag.frame_count} frames, CRC errors {diag.crc_err_count}, "
        f"dropouts {diag.dropout_count}, USB resets {diag.usb_reset_count}, "
        f"uptime {diag.uptime_ms} ms"
    )

    # 9. Save data
    if frames:
        data = np.stack(frames)  # shape: (N, 24, 32), dtype: float32
        np.save("tactile_data.npy", data)
        print(f"Saved {len(frames)} frames to tactile_data.npy")
    else:
        print("No frames collected")

# Destructive operations (commented by default; both invalidate the handle):
# glove.reset_device()
# glove.enter_bootloader(TACTILE_BOOTLOADER_MAGIC)
```

#### Example Output

```
[xxxx-xx-xx xx:xx:xx.xxx] [wuji] [info] SDK vx.x.x initialized
[xxxx-xx-xx xx:xx:xx.xxx] [wuji] [info] Log files can be found at ~/.wuji/log/<timestamp>_<pid>.log
Serial: WTxxxxxxxxxxxxxx
Hardware: vx.x.x, Firmware: vx.x.x (xxxxxxx)
Handedness: left
Host/device clock skew: -1779537846723254078 ns
Device monotonic time: 142623339080000 ns
Current sample rate: 60 Hz
New sample rate: 60 Hz
Streaming enabled: True
Frame: seq=33917, ts=142623244 ms, hand=L, max=0.820
Collected 326 frames, CRC errors 0, dropouts 0, USB resets 0, uptime 142628802 ms
Saved 326 frames to tactile_data.npy
```

### Use with Wuji Hand

`Hand` and `TactileGlove` use independent USB transports and threads, so the two APIs compose in the same process with no extra coordination. The frame callback fires on the glove's streaming consumer thread, while joint commands run on the thread that calls them. This pattern enables closed-loop scenarios where tactile feedback drives motion—for example, scaling joint amplitude by the current pressure peak.

Reference example: [`example/joint_with_tactile.py`](https://github.com/wuji-technology/wujihandpy/blob/main/example/joint_with_tactile.py).

**Runtime behavior:**

* Prints `Squeeze the glove to dampen the motion. Ctrl-C to exit.` on startup
* After joints are enabled, fingers F2–F5 perform periodic flexion–extension in a 100 Hz realtime loop. F1 (thumb) stays still
* Higher tactile pressure peaks shrink the motion amplitude: a relaxed glove yields the full sweep (about 0.4 rad), while a firm squeeze drives amplitude toward 0
* Releasing the glove restores the full amplitude
* Ctrl-C stops streaming, disables joints, and disconnects the glove cleanly

### Protocol Reference

This section is for advanced developers who need to parse tactile data frames directly. The `TactileGlove` class handles all protocol details automatically.

#### Data frame format

The Tactile Sensing Glove streams fixed 3088-byte frames at up to 120 Hz over USB CDC (virtual serial port). The effective rate is set via `set_sample_rate_hz` (1–120 Hz):

| Offset | Length | Field | Type | Description |
| - | - | - | - | - |
| 0–1 | 2 | Header | u8\[2] | Fixed `0xAA 0x55` |
| 2–3 | 2 | Frame length | u16 LE | Fixed 3088 |
| 4 | 1 | Hand | u8 | `0x00` = left, `0x01` = right |
| 5–7 | 3 | Padding | u8\[3] | Aligns tactile data to a 4-byte boundary |
| 8–3079 | 3072 | Tactile data | f32\[24]\[32] LE | Pressure matrix, row-major, `[0.0, 1.0]`. `NaN` = invalid cell |
| 3080–3081 | 2 | Sequence | u16 LE | Frame counter, wraps at 65535 |
| 3082–3085 | 4 | Timestamp | u32 LE | Milliseconds since device boot |
| 3086–3087 | 2 | CRC16 | u16 LE | CRC16-CCITT checksum |

#### CRC16-CCITT

* Polynomial: `0x1021`, initial value: `0xFFFF`
* Coverage: bytes `[2, 3086)` (frame length through timestamp, 3084 bytes)
* Excludes header and CRC field

#### Frame synchronization algorithm

1. Scan byte-by-byte for header `0xAA 0x55`
2. Read bytes 2–3, verify frame length == 3088
3. Read remaining bytes to complete the frame
4. Compute CRC16 and compare
5. CRC failure: discard, resume scanning from header\_position + 1
6. CRC success: parse and deliver frame

<Info>
  `0xAA 0x55` may appear within pressure data (false headers). CRC validation is the only reliable way to distinguish real headers from false ones.
</Info>

#### Reference Implementation

The Python snippet below parses a single frame straight from a raw byte stream (for example, a CDC serial port opened with `pyserial`) without depending on the SDK. Use it as a starting point for a custom parser.

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

FRAME_LEN = 3088
HEADER = b"\xAA\x55"


def crc16_ccitt(data: bytes, init: int = 0xFFFF) -> int:
    crc = init
    for b in data:
        crc ^= b << 8
        for _ in range(8):
            if crc & 0x8000:
                crc = ((crc << 1) ^ 0x1021) & 0xFFFF
            else:
                crc = (crc << 1) & 0xFFFF
    return crc


def parse_frame(buf: bytes) -> dict:
    """Validate a 3088-byte candidate and decode its fields."""
    if len(buf) != FRAME_LEN or buf[:2] != HEADER:
        raise ValueError("bad header or length")
    if struct.unpack_from("<H", buf, 2)[0] != FRAME_LEN:
        raise ValueError("length field mismatch")
    if crc16_ccitt(buf[2:3086]) != struct.unpack_from("<H", buf, 3086)[0]:
        raise ValueError("CRC mismatch")
    return {
        "hand": "L" if buf[4] == 0 else "R",
        "pressure": np.frombuffer(buf, dtype="<f4", count=24 * 32, offset=8).reshape(24, 32),
        "sequence": struct.unpack_from("<H", buf, 3080)[0],
        "timestamp_ms": struct.unpack_from("<I", buf, 3082)[0],
    }


def read_frame(port) -> dict:
    """Resync on a raw byte stream and return the next valid frame."""
    buf = bytearray()
    while True:
        chunk = port.read(FRAME_LEN)
        if not chunk:
            raise TimeoutError("stream exhausted")
        buf.extend(chunk)
        i = buf.find(HEADER)
        while 0 <= i and i + FRAME_LEN <= len(buf):
            try:
                frame = parse_frame(bytes(buf[i:i + FRAME_LEN]))
                del buf[: i + FRAME_LEN]
                return frame
            except ValueError:
                i = buf.find(HEADER, i + 1)
        # Keep tail so a header spanning chunk boundaries survives.
        if len(buf) > FRAME_LEN - 1:
            del buf[: len(buf) - (FRAME_LEN - 1)]
```

Usage:

```python theme={null}
import serial  # python3 -m pip install pyserial

with serial.Serial("/dev/ttyACM0", timeout=1.0) as port:
    frame = read_frame(port)
    print(f"seq={frame['sequence']}, ts={frame['timestamp_ms']} ms, "
          f"hand={frame['hand']}, max={np.nanmax(frame['pressure']):.3f}")
```

***

## logging Module

The `wujihandpy.logging` submodule configures SDK logging at runtime. For default behavior, environment variables, and troubleshooting tips, see [Getting Started - Get Logs](/en/docs/wujihandpy/introduction#get-logs).

### Level Enum

| Member | Value | Meaning |
| :- | :- | :- |
| `Level.Trace` | 0 | Most verbose, includes raw TX/RX bytes |
| `Level.Debug` | 1 | Debug details |
| `Level.Info` | 2 | Key events (default) |
| `Level.Warn` | 3 | Warnings |
| `Level.Error` | 4 | Errors |
| `Level.Critical` | 5 | Critical errors |
| `Level.Off` | 6 | Disable logging |

### Functions

#### set\_log\_path

Set the log file directory. **Must be called before the logger initializes** (in the common `Hand` flow that means before creating the first `Hand`. `flush()` or any other logging API also triggers logger initialization), otherwise it raises `RuntimeError`.

```python theme={null}
def set_log_path(value: str) -> None
```

#### set\_log\_level

Set the log level. Safe to call at any time and takes effect immediately.

```python theme={null}
def set_log_level(value: Level) -> None
```

#### set\_log\_to\_file

Toggle file output. The log file is opened only if `WUJI_LOG_TO_FILE` is `true` at logger initialization, and after init this function can temporarily disable or restore file output. If file output was disabled at init, calling `set_log_to_file(True)` later won't re-enable it.

```python theme={null}
def set_log_to_file(value: bool) -> None
```

#### set\_log\_to\_console

Toggle terminal output (stdout / stderr). Safe to call at any time and takes effect immediately.

```python theme={null}
def set_log_to_console(value: bool) -> None
```

#### flush

Force pending log entries to file and terminal. The SDK calls this automatically on process exit, so manual calls are rarely needed.

```python theme={null}
def flush() -> None
```


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