> ## 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 参考

> Wuji Hand SDK (wujihandpy) 完整 API 参考，包括 Hand、Finger、Joint 类的字段列表和方法说明。

## 字段列表

本节介绍 wujihandpy 中所有可用的数据字段。

### Hand 层

该层包含整手设备唯一的通用字段，例如手性、固件版本等。

#### handedness

该字段表示灵巧手的手性（左/右手，0 表示右手，1 表示左手）。

* **类型**：`uint8`
* **读写属性**：只读

#### firmware\_version

该字段表示灵巧手固件的版本号。

* **类型**：`uint32`
* **读写属性**：只读

#### firmware\_date

该字段表示灵巧手固件的编译日期。

* **类型**：`uint32`
* **读写属性**：只读

#### system\_time

该字段表示灵巧手系统运行时间，单位为毫秒。

* **类型**：`uint32`
* **读写属性**：只读

#### temperature

该字段表示灵巧手手腕附近的当前温度，单位为摄氏度。

* **类型**：`float32`
* **读写属性**：只读

#### input\_voltage

该字段表示灵巧手母线电压的实测值，单位为伏特。

* **类型**：`float32`
* **读写属性**：只读

### Joint 层

该层包含每关节持有一个独立副本的字段，例如使能、目标位置等。

#### joint\_firmware\_version

该字段记录关节驱动板固件的版本号。

* **类型**：`uint32`
* **读写属性**：只读

#### joint\_firmware\_date

该字段记录关节驱动板固件的编译日期。

* **类型**：`uint32`
* **读写属性**：只读

#### joint\_control\_mode

该字段用于设置关节的控制模式。

* **类型**：`uint16`
* **读写属性**：只写

> 说明：模式会自动切换，一般情况下不应修改该字段。

#### joint\_sin\_level

该字段用于设置关节驱动的正弦波激励档位。

* **类型**：`uint16`
* **读写属性**：只写

> 说明：该字段用于内部调试，一般情况下不应修改该字段。

#### joint\_effort\_limit

该字段用于设置关节允许的 effort 上限。

* **类型**：`float64`
* **单位**：安培 (A)
* **默认值**：`1.5`
* **合法范围**：`0.0 ~ 3.5`（超过 3.5 会被限制为 3.5）
* **读写属性**：读写

<Info>
  **什么是 Effort？**

  Effort 是电流空间的执行器作用量，经过滤波处理后输出。它不是实际测量的电流值，应将其理解为相对驱动强度，适用于负载监控和碰撞检测等场景。详见 [joint\_effort](#joint_effort) 字段。
</Info>

<Warning>
  **过热保护：Effort 限制 会随温度动态调整**

  为保护设备，固件会根据关节温度自动降低实际生效的 effort 限制：

  | 温度状态 | 触发条件 | 实际 Effort 限制 |
  | - | - | - |
  | 正常 | 低于 85°C | 用户设定值 (默认 1.5A) |
  | 温度预警 (Tier 1) | 大于等于 85°C | 用户设定值 × 70% |
  | 温度告警 (Tier 2) | 大于等于 93°C | 用户设定值 × 40% |

  恢复阶段采用 4°C 回退阈值：

  * 从 Tier 2 回退到 Tier 1：温度需降到 89°C 以下
  * 从 Tier 1 恢复到正常：温度需降到 81°C 以下

  当温度恢复到安全范围后，effort 限制会自动恢复。
</Warning>

<Warning>
  **警告：修改 Effort 限制 可能导致设备损坏**

  修改此参数会改变关节的最大输出能力。如因调整 effort limit 导致设备损坏，用户需自行承担责任。

  * 默认值 1.5 适用于大多数应用场景
  * 最大值 3.5，超过此值会被自动限制
  * 增大此值会增加关节负载能力，但也增加损坏风险
  * 影响相关关节运动寿命，加速各零件磨损且无法预估影响大小
  * 可能在某工况下直接造成相关关节出现不可逆的损坏
  * 可能加速关节热保护触发，减少持续运动时间
  * 可能出现除以上情况下的其他严重后果
</Warning>

#### joint\_bus\_voltage

该字段记录关节当前的母线电压实测值。

* **类型**：`float32`
* **读写属性**：只读

#### joint\_temperature

该字段记录关节驱动板当前的温度，单位为摄氏度。

* **类型**：`float32`
* **读写属性**：只读

#### joint\_reset\_error

向该字段写入 `1` 可清除关节上报的错误状态。

* **类型**：`uint16`
* **读写属性**：只写

<Warning>
  该字段仅能清除错误码，不能解决灵巧手出现的问题。\
  切勿滥用此字段，尤其不能在循环中使用此字段，否则可能导致灵巧手损坏。
</Warning>

#### joint\_error\_code

该字段表示各关节的当前错误码，用于诊断运行状态。
值为 `0` 时表示关节无异常。

* **类型**：`uint32`
* **读写属性**：只读

#### joint\_effort

该字段提供关节的实时 effort 反馈。

* **类型**：`float32`
* **单位**：安培 (A)
* **读写属性**：只读

<Info>
  **Effort 的含义**

  Effort 是电流空间的执行器作用量 (actuation effort in current space)，经过滤波处理后输出。

  * **不是**实际测量的电流值
  * **不是**电流环的直接输出
  * 应将其理解为相对驱动强度，适用于负载监控和碰撞检测

  典型应用场景：

  * 碰撞检测：effort 突增表示关节受阻
  * 输出监控：可通过 `effort / effort_limit` 计算当前输出百分比

  此字段仅在实时控制模式下通过 `IController.get_joint_actual_effort()` 访问。
</Info>

#### joint\_enabled

该字段控制关节的使能和失能。

* **类型**：`bool`
* **默认值**：`False`
* **合法范围**：`True / False`
* **读写属性**：只写

#### joint\_actual\_position

该字段提供关节的实际位置。

* **类型**：`float64`
* **读写属性**：只读

#### joint\_target\_position

写入该字段可下发关节的目标控制角度。若数值超出合法范围，会自动限幅至允许区间。

* **类型**：`float64`
* **合法范围**：`joint_lower_limit ~ joint_upper_limit`
* **读写属性**：只写

#### joint\_upper\_limit

该字段记录关节的运行位置上限。

* **类型**：`float64`
* **读写属性**：只读

<Info>
  **与 URDF 关节范围的区别**

  各关节在加工时存在微小的精度误差，使每个关节的实际物理极限略有不同。\
  灵巧手在出厂时会对各关节的实际极限进行标定，结果写入 `joint_upper_limit` 和 `joint_lower_limit` 字段。\
  因此，这两个字段记录的是经标定的实际物理极限值，通常略大于 URDF 文件中定义的范围。

  URDF 文件中的关节范围是理论设计值，经过保守缩放以考虑加工误差，可用于仿真或神经网络训练。\
  而 `joint_upper_limit` 与 `joint_lower_limit` 表示实际测得的可运动范围。
</Info>

#### joint\_lower\_limit

该字段记录关节的运行位置下限。

* **类型**：`float64`
* **读写属性**：只读

## API 列表

### Hand 类

#### 构造函数

* `__init__(serial_number=None, *, side=None, usb_pid=0x2000, usb_vid=0x0483, mask=None) -> None`
  *初始化灵巧手连接，支持通过序列号、左右手、USB ID 或设备掩码指定设备*

  * `serial_number`：USB 序列号字符串，精确指定一台设备
  * `side`：`"left"` 或 `"right"`，按手性选择设备，与 `serial_number` 互斥（详见 [教程 - 筛选设备](/docs/wujihandpy/tutorial#筛选设备)）

#### 手指访问

* `finger(index) -> Finger`
  *获取指定索引的手指对象，索引范围 0-4*

#### 线程安全控制

* `disable_thread_safe_check() -> None`
  *禁用线程安全检查，允许在多线程环境中并发访问 Hand 对象*

<Warning>
  **注意**：禁用线程安全检查后，用户需要自行使用互斥锁（`threading.Lock`）确保多线程操作的正确性。
</Warning>

#### 系统信息

##### 固件信息

* `read_firmware_version(timeout=0.5) -> uint32`
  *同步读取固件版本号*

* `read_firmware_version_async(timeout=0.5) -> Awaitable[uint32]`
  *异步读取固件版本号*

* `read_firmware_version_unchecked(timeout=0.5) -> None`
  *非阻塞读取固件版本号*

* `get_firmware_version() -> uint32`
  *获取缓存的固件版本号*

* `read_firmware_date(timeout=0.5) -> uint32`
  *同步读取固件编译日期*

* `read_firmware_date_async(timeout=0.5) -> Awaitable[uint32]`
  *异步读取固件编译日期*

* `read_firmware_date_unchecked(timeout=0.5) -> None`
  *非阻塞读取固件编译日期*

* `get_firmware_date() -> uint32`
  *获取缓存的固件编译日期*

* `read_handedness(timeout=0.5) -> uint8`
  *同步读取左右手标识（0=右，1=左）*

* `read_handedness_async(timeout=0.5) -> Awaitable[uint8]`
  *异步读取左右手标识*

* `read_handedness_unchecked(timeout=0.5) -> None`
  *非阻塞读取左右手标识*

* `get_handedness() -> uint8`
  *获取缓存的左右手标识*

##### 系统状态

* `read_system_time(timeout=0.5) -> uint32`
  *同步读取系统运行时间（毫秒）*

* `read_system_time_async(timeout=0.5) -> Awaitable[uint32]`
  *异步读取系统运行时间*

* `read_system_time_unchecked(timeout=0.5) -> None`
  *非阻塞读取系统运行时间*

* `get_system_time() -> uint32`
  *获取缓存的系统运行时间*

* `read_temperature(timeout=0.5) -> float32`
  *同步读取手掌温度（摄氏度）*

* `read_temperature_async(timeout=0.5) -> Awaitable[float32]`
  *异步读取手掌温度*

* `read_temperature_unchecked(timeout=0.5) -> None`
  *非阻塞读取手掌温度*

* `get_temperature() -> float32`
  *获取缓存的手掌温度*

* `read_input_voltage(timeout=0.5) -> float32`
  *同步读取输入电压（伏特）*

* `read_input_voltage_async(timeout=0.5) -> Awaitable[float32]`
  *异步读取输入电压*

* `read_input_voltage_unchecked(timeout=0.5) -> None`
  *非阻塞读取输入电压*

* `get_input_voltage() -> float32`
  *获取缓存的输入电压*

#### 关节数据（批量读取，返回 {5,4} 数组）

##### 位置数据

* `read_joint_actual_position(timeout=0.5) -> NDArray[float64]`
  *同步读取所有关节实际位置（弧度）*

* `read_joint_actual_position_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *异步读取所有关节实际位置*

* `read_joint_actual_position_unchecked(timeout=0.5) -> None`
  *非阻塞读取所有关节实际位置*

* `get_joint_actual_position() -> NDArray[float64]`
  *获取缓存的所有关节实际位置*

* `read_joint_upper_limit(timeout=0.5) -> NDArray[float64]`
  *同步读取所有关节上限位置（弧度）*

* `read_joint_upper_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *异步读取所有关节上限位置*

* `read_joint_upper_limit_unchecked(timeout=0.5) -> None`
  *非阻塞读取所有关节上限位置*

* `get_joint_upper_limit() -> NDArray[float64]`
  *获取缓存的所有关节上限位置*

* `read_joint_lower_limit(timeout=0.5) -> NDArray[float64]`
  *同步读取所有关节下限位置（弧度）*

* `read_joint_lower_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *异步读取所有关节下限位置*

* `read_joint_lower_limit_unchecked(timeout=0.5) -> None`
  *非阻塞读取所有关节下限位置*

* `get_joint_lower_limit() -> NDArray[float64]`
  *获取缓存的所有关节下限位置*

##### 状态监控

* `read_joint_bus_voltage(timeout=0.5) -> NDArray[float32]`
  *同步读取所有关节母线电压（伏特）*

* `read_joint_bus_voltage_async(timeout=0.5) -> Awaitable[NDArray[float32]]`
  *异步读取所有关节母线电压*

* `read_joint_bus_voltage_unchecked(timeout=0.5) -> None`
  *非阻塞读取所有关节母线电压*

* `get_joint_bus_voltage() -> NDArray[float32]`
  *获取缓存的所有关节母线电压*

* `read_joint_temperature(timeout=0.5) -> NDArray[float32]`
  *同步读取所有关节温度（摄氏度）*

* `read_joint_temperature_async(timeout=0.5) -> Awaitable[NDArray[float32]]`
  *异步读取所有关节温度*

* `read_joint_temperature_unchecked(timeout=0.5) -> None`
  *非阻塞读取所有关节温度*

* `get_joint_temperature() -> NDArray[float32]`
  *获取缓存的所有关节温度*

* `read_joint_error_code(timeout=0.5) -> NDArray[uint32]`
  *同步读取所有关节错误码*

* `read_joint_error_code_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *异步读取所有关节错误码*

* `read_joint_error_code_unchecked(timeout=0.5) -> None`
  *非阻塞读取所有关节错误码*

* `get_joint_error_code() -> NDArray[uint32]`
  *获取缓存的所有关节错误码*

##### Effort 限制 数据

* `read_joint_effort_limit(timeout=0.5) -> NDArray[float64]`
  *同步读取所有关节 effort limit（安培）*
* `read_joint_effort_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *异步读取所有关节 effort limit*
* `read_joint_effort_limit_unchecked(timeout=0.5) -> None`
  *非阻塞读取所有关节 effort limit*
* `get_joint_effort_limit() -> NDArray[float64]`
  *获取缓存的所有关节 effort limit*

##### 固件信息

* `read_joint_firmware_version(timeout=0.5) -> NDArray[uint32]`
  *同步读取所有关节固件版本*

* `read_joint_firmware_version_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *异步读取所有关节固件版本*

* `read_joint_firmware_version_unchecked(timeout=0.5) -> None`
  *非阻塞读取所有关节固件版本*

* `get_joint_firmware_version() -> NDArray[uint32]`
  *获取缓存的所有关节固件版本*

* `read_joint_firmware_date(timeout=0.5) -> NDArray[uint32]`
  *同步读取所有关节固件编译日期*

* `read_joint_firmware_date_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *异步读取所有关节固件编译日期*

* `read_joint_firmware_date_unchecked(timeout=0.5) -> None`
  *非阻塞读取所有关节固件编译日期*

* `get_joint_firmware_date() -> NDArray[uint32]`
  *获取缓存的所有关节固件编译日期*

#### 关节控制（支持单个值或 {5,4} 数组）

##### 使能控制

* `write_joint_enabled(value, timeout=0.5) -> None`
  *同步设置所有关节使能状态*
* `write_joint_enabled(value_array, timeout=0.5) -> None`
  *同步批量设置各关节使能状态*
* `write_joint_enabled_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置所有关节使能状态*
* `write_joint_enabled_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量设置各关节使能状态*
* `write_joint_enabled_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置所有关节使能状态*
* `write_joint_enabled_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量设置各关节使能状态*

##### 位置控制

* `write_joint_target_position(value, timeout=0.5) -> None`
  *同步设置所有关节目标位置（弧度）*
* `write_joint_target_position(value_array, timeout=0.5) -> None`
  *同步批量设置各关节目标位置*
* `write_joint_target_position_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置所有关节目标位置*
* `write_joint_target_position_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量设置各关节目标位置*
* `write_joint_target_position_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置所有关节目标位置*
* `write_joint_target_position_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量设置各关节目标位置*

##### 模式控制

* `write_joint_control_mode(value, timeout=0.5) -> None`
  *同步设置所有关节控制模式*
* `write_joint_control_mode(value_array, timeout=0.5) -> None`
  *同步批量设置各关节控制模式*
* `write_joint_control_mode_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置所有关节控制模式*
* `write_joint_control_mode_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量设置各关节控制模式*
* `write_joint_control_mode_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置所有关节控制模式*
* `write_joint_control_mode_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量设置各关节控制模式*

##### Effort 限制 配置

<Warning>
  **警告**：修改 effort limit 可能导致设备损坏，详见字段说明。
</Warning>

* `write_joint_effort_limit(value, timeout=0.5) -> None`
  *同步设置所有关节 effort limit（安培）*
* `write_joint_effort_limit(value_array, timeout=0.5) -> None`
  *同步批量设置各关节 effort limit*
* `write_joint_effort_limit_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置所有关节 effort limit*
* `write_joint_effort_limit_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量设置各关节 effort limit*
* `write_joint_effort_limit_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置所有关节 effort limit*
* `write_joint_effort_limit_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量设置各关节 effort limit*

##### 其他参数配置

* `write_joint_sin_level(value, timeout=0.5) -> None`
  *同步设置所有关节正弦波激励档位*

* `write_joint_sin_level(value_array, timeout=0.5) -> None`
  *同步批量设置各关节正弦波激励档位*

* `write_joint_sin_level_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置所有关节正弦波激励档位*

* `write_joint_sin_level_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量设置各关节正弦波激励档位*

* `write_joint_sin_level_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置所有关节正弦波激励档位*

* `write_joint_sin_level_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量设置各关节正弦波激励档位*

* `write_joint_reset_error(value, timeout=0.5) -> None`
  *同步重置所有关节错误*

* `write_joint_reset_error(value_array, timeout=0.5) -> None`
  *同步批量重置各关节错误*

* `write_joint_reset_error_async(value, timeout=0.5) -> Awaitable[None]`
  *异步重置所有关节错误*

* `write_joint_reset_error_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量重置各关节错误*

* `write_joint_reset_error_unchecked(value, timeout=0.5) -> None`
  *非阻塞重置所有关节错误*

* `write_joint_reset_error_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量重置各关节错误*

#### 实时控制器

* `realtime_controller(enable_upstream, filter) -> IController`
  *创建实时控制器，用于高频控制场景*

### Finger 类

#### 关节访问

* `joint(index) -> Joint`
  *获取指定索引的关节对象，索引范围 0-3*

#### 关节数据（返回 4 元素数组）

##### 位置数据

* `read_joint_actual_position(timeout=0.5) -> NDArray[float64]`
  *同步读取手指所有关节实际位置（弧度）*

* `read_joint_actual_position_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *异步读取手指所有关节实际位置*

* `read_joint_actual_position_unchecked(timeout=0.5) -> None`
  *非阻塞读取手指所有关节实际位置*

* `get_joint_actual_position() -> NDArray[float64]`
  *获取缓存的手指所有关节实际位置*

* `read_joint_upper_limit(timeout=0.5) -> NDArray[float64]`
  *同步读取手指所有关节上限位置（弧度）*

* `read_joint_upper_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *异步读取手指所有关节上限位置*

* `read_joint_upper_limit_unchecked(timeout=0.5) -> None`
  *非阻塞读取手指所有关节上限位置*

* `get_joint_upper_limit() -> NDArray[float64]`
  *获取缓存的手指所有关节上限位置*

* `read_joint_lower_limit(timeout=0.5) -> NDArray[float64]`
  *同步读取手指所有关节下限位置（弧度）*

* `read_joint_lower_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *异步读取手指所有关节下限位置*

* `read_joint_lower_limit_unchecked(timeout=0.5) -> None`
  *非阻塞读取手指所有关节下限位置*

* `get_joint_lower_limit() -> NDArray[float64]`
  *获取缓存的手指所有关节下限位置*

##### 状态监控

* `read_joint_bus_voltage(timeout=0.5) -> NDArray[float32]`
  *同步读取手指所有关节母线电压（伏特）*

* `read_joint_bus_voltage_async(timeout=0.5) -> Awaitable[NDArray[float32]]`
  *异步读取手指所有关节母线电压*

* `read_joint_bus_voltage_unchecked(timeout=0.5) -> None`
  *非阻塞读取手指所有关节母线电压*

* `get_joint_bus_voltage() -> NDArray[float32]`
  *获取缓存的手指所有关节母线电压*

* `read_joint_temperature(timeout=0.5) -> NDArray[float32]`
  *同步读取手指所有关节温度（摄氏度）*

* `read_joint_temperature_async(timeout=0.5) -> Awaitable[NDArray[float32]]`
  *异步读取手指所有关节温度*

* `read_joint_temperature_unchecked(timeout=0.5) -> None`
  *非阻塞读取手指所有关节温度*

* `get_joint_temperature() -> NDArray[float32]`
  *获取缓存的手指所有关节温度*

* `read_joint_error_code(timeout=0.5) -> NDArray[uint32]`
  *同步读取手指所有关节错误码*

* `read_joint_error_code_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *异步读取手指所有关节错误码*

* `read_joint_error_code_unchecked(timeout=0.5) -> None`
  *非阻塞读取手指所有关节错误码*

* `get_joint_error_code() -> NDArray[uint32]`
  *获取缓存的手指所有关节错误码*

##### Effort 限制 数据

* `read_joint_effort_limit(timeout=0.5) -> NDArray[float64]`
  *同步读取手指所有关节 effort limit（安培）*
* `read_joint_effort_limit_async(timeout=0.5) -> Awaitable[NDArray[float64]]`
  *异步读取手指所有关节 effort limit*
* `read_joint_effort_limit_unchecked(timeout=0.5) -> None`
  *非阻塞读取手指所有关节 effort limit*
* `get_joint_effort_limit() -> NDArray[float64]`
  *获取缓存的手指所有关节 effort limit*

##### 固件信息

* `read_joint_firmware_version(timeout=0.5) -> NDArray[uint32]`
  *同步读取手指所有关节固件版本*

* `read_joint_firmware_version_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *异步读取手指所有关节固件版本*

* `read_joint_firmware_version_unchecked(timeout=0.5) -> None`
  *非阻塞读取手指所有关节固件版本*

* `get_joint_firmware_version() -> NDArray[uint32]`
  *获取缓存的手指所有关节固件版本*

* `read_joint_firmware_date(timeout=0.5) -> NDArray[uint32]`
  *同步读取手指所有关节固件编译日期*

* `read_joint_firmware_date_async(timeout=0.5) -> Awaitable[NDArray[uint32]]`
  *异步读取手指所有关节固件编译日期*

* `read_joint_firmware_date_unchecked(timeout=0.5) -> None`
  *非阻塞读取手指所有关节固件编译日期*

* `get_joint_firmware_date() -> NDArray[uint32]`
  *获取缓存的手指所有关节固件编译日期*

#### 关节控制（支持单个值或 4 元素数组）

##### 使能控制

* `write_joint_enabled(value, timeout=0.5) -> None`
  *同步设置手指所有关节使能状态*
* `write_joint_enabled(value_array, timeout=0.5) -> None`
  *同步批量设置手指各关节使能状态*
* `write_joint_enabled_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置手指所有关节使能状态*
* `write_joint_enabled_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量设置手指各关节使能状态*
* `write_joint_enabled_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置手指所有关节使能状态*
* `write_joint_enabled_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量设置手指各关节使能状态*

##### 位置控制

* `write_joint_target_position(value, timeout=0.5) -> None`
  *同步设置手指所有关节目标位置（弧度）*
* `write_joint_target_position(value_array, timeout=0.5) -> None`
  *同步批量设置手指各关节目标位置*
* `write_joint_target_position_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置手指所有关节目标位置*
* `write_joint_target_position_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量设置手指各关节目标位置*
* `write_joint_target_position_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置手指所有关节目标位置*
* `write_joint_target_position_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量设置手指各关节目标位置*

##### 模式控制

* `write_joint_control_mode(value, timeout=0.5) -> None`
  *同步设置手指所有关节控制模式*
* `write_joint_control_mode(value_array, timeout=0.5) -> None`
  *同步批量设置手指各关节控制模式*
* `write_joint_control_mode_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置手指所有关节控制模式*
* `write_joint_control_mode_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量设置手指各关节控制模式*
* `write_joint_control_mode_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置手指所有关节控制模式*
* `write_joint_control_mode_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量设置手指各关节控制模式*

##### Effort 限制 配置

<Warning>
  **警告**：修改 effort limit 可能导致设备损坏，详见字段说明。
</Warning>

* `write_joint_effort_limit(value, timeout=0.5) -> None`
  *同步设置手指所有关节 effort limit（安培）*
* `write_joint_effort_limit(value_array, timeout=0.5) -> None`
  *同步批量设置手指各关节 effort limit*
* `write_joint_effort_limit_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置手指所有关节 effort limit*
* `write_joint_effort_limit_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量设置手指各关节 effort limit*
* `write_joint_effort_limit_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置手指所有关节 effort limit*
* `write_joint_effort_limit_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量设置手指各关节 effort limit*

##### 其他参数配置

* `write_joint_sin_level(value, timeout=0.5) -> None`
  *同步设置手指所有关节正弦波激励档位*

* `write_joint_sin_level(value_array, timeout=0.5) -> None`
  *同步批量设置手指各关节正弦波激励档位*

* `write_joint_sin_level_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置手指所有关节正弦波激励档位*

* `write_joint_sin_level_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量设置手指各关节正弦波激励档位*

* `write_joint_sin_level_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置手指所有关节正弦波激励档位*

* `write_joint_sin_level_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量设置手指各关节正弦波激励档位*

* `write_joint_reset_error(value, timeout=0.5) -> None`
  *同步重置手指所有关节错误*

* `write_joint_reset_error(value_array, timeout=0.5) -> None`
  *同步批量重置手指各关节错误*

* `write_joint_reset_error_async(value, timeout=0.5) -> Awaitable[None]`
  *异步重置手指所有关节错误*

* `write_joint_reset_error_async(value_array, timeout=0.5) -> Awaitable[None]`
  *异步批量重置手指各关节错误*

* `write_joint_reset_error_unchecked(value, timeout=0.5) -> None`
  *非阻塞重置手指所有关节错误*

* `write_joint_reset_error_unchecked(value_array, timeout=0.5) -> None`
  *非阻塞批量重置手指各关节错误*

### Joint 类

#### 关节数据（返回单个值）

##### 位置数据

* `read_joint_actual_position(timeout=0.5) -> float64`
  *同步读取关节实际位置（弧度）*

* `read_joint_actual_position_async(timeout=0.5) -> Awaitable[float64]`
  *异步读取关节实际位置*

* `read_joint_actual_position_unchecked(timeout=0.5) -> None`
  *非阻塞读取关节实际位置*

* `get_joint_actual_position() -> float64`
  *获取缓存的关节实际位置*

* `read_joint_upper_limit(timeout=0.5) -> float64`
  *同步读取关节上限位置（弧度）*

* `read_joint_upper_limit_async(timeout=0.5) -> Awaitable[float64]`
  *异步读取关节上限位置*

* `read_joint_upper_limit_unchecked(timeout=0.5) -> None`
  *非阻塞读取关节上限位置*

* `get_joint_upper_limit() -> float64`
  *获取缓存的关节上限位置*

* `read_joint_lower_limit(timeout=0.5) -> float64`
  *同步读取关节下限位置（弧度）*

* `read_joint_lower_limit_async(timeout=0.5) -> Awaitable[float64]`
  *异步读取关节下限位置*

* `read_joint_lower_limit_unchecked(timeout=0.5) -> None`
  *非阻塞读取关节下限位置*

* `get_joint_lower_limit() -> float64`
  *获取缓存的关节下限位置*

##### 状态监控

* `read_joint_bus_voltage(timeout=0.5) -> float32`
  *同步读取关节母线电压（伏特）*

* `read_joint_bus_voltage_async(timeout=0.5) -> Awaitable[float32]`
  *异步读取关节母线电压*

* `read_joint_bus_voltage_unchecked(timeout=0.5) -> None`
  *非阻塞读取关节母线电压*

* `get_joint_bus_voltage() -> float32`
  *获取缓存的关节母线电压*

* `read_joint_temperature(timeout=0.5) -> float32`
  *同步读取关节温度（摄氏度）*

* `read_joint_temperature_async(timeout=0.5) -> Awaitable[float32]`
  *异步读取关节温度*

* `read_joint_temperature_unchecked(timeout=0.5) -> None`
  *非阻塞读取关节温度*

* `get_joint_temperature() -> float32`
  *获取缓存的关节温度*

* `read_joint_error_code(timeout=0.5) -> uint32`
  *同步读取关节错误码*

* `read_joint_error_code_async(timeout=0.5) -> Awaitable[uint32]`
  *异步读取关节错误码*

* `read_joint_error_code_unchecked(timeout=0.5) -> None`
  *非阻塞读取关节错误码*

* `get_joint_error_code() -> uint32`
  *获取缓存的关节错误码*

##### Effort 限制 数据

* `read_joint_effort_limit(timeout=0.5) -> float64`
  *同步读取关节 effort limit（安培）*
* `read_joint_effort_limit_async(timeout=0.5) -> Awaitable[float64]`
  *异步读取关节 effort limit*
* `read_joint_effort_limit_unchecked(timeout=0.5) -> None`
  *非阻塞读取关节 effort limit*
* `get_joint_effort_limit() -> float64`
  *获取缓存的关节 effort limit*

##### 固件信息

* `read_joint_firmware_version(timeout=0.5) -> uint32`
  *同步读取关节固件版本*

* `read_joint_firmware_version_async(timeout=0.5) -> Awaitable[uint32]`
  *异步读取关节固件版本*

* `read_joint_firmware_version_unchecked(timeout=0.5) -> None`
  *非阻塞读取关节固件版本*

* `get_joint_firmware_version() -> uint32`
  *获取缓存的关节固件版本*

* `read_joint_firmware_date(timeout=0.5) -> uint32`
  *同步读取关节固件编译日期*

* `read_joint_firmware_date_async(timeout=0.5) -> Awaitable[uint32]`
  *异步读取关节固件编译日期*

* `read_joint_firmware_date_unchecked(timeout=0.5) -> None`
  *非阻塞读取关节固件编译日期*

* `get_joint_firmware_date() -> uint32`
  *获取缓存的关节固件编译日期*

#### 关节控制（单个值）

##### 使能控制

* `write_joint_enabled(value, timeout=0.5) -> None`
  *同步设置关节使能状态*
* `write_joint_enabled_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置关节使能状态*
* `write_joint_enabled_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置关节使能状态*

##### 位置控制

* `write_joint_target_position(value, timeout=0.5) -> None`
  *同步设置关节目标位置（弧度）*
* `write_joint_target_position_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置关节目标位置*
* `write_joint_target_position_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置关节目标位置*

##### 模式控制

* `write_joint_control_mode(value, timeout=0.5) -> None`
  *同步设置关节控制模式*
* `write_joint_control_mode_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置关节控制模式*
* `write_joint_control_mode_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置关节控制模式*

##### Effort 限制 配置

<Warning>
  **警告**：修改 effort limit 可能导致设备损坏，详见字段说明。
</Warning>

* `write_joint_effort_limit(value, timeout=0.5) -> None`
  *同步设置关节 effort limit（安培）*
* `write_joint_effort_limit_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置关节 effort limit*
* `write_joint_effort_limit_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置关节 effort limit*

##### 其他参数配置

* `write_joint_sin_level(value, timeout=0.5) -> None`
  *同步设置关节正弦波激励档位*

* `write_joint_sin_level_async(value, timeout=0.5) -> Awaitable[None]`
  *异步设置关节正弦波激励档位*

* `write_joint_sin_level_unchecked(value, timeout=0.5) -> None`
  *非阻塞设置关节正弦波激励档位*

* `write_joint_reset_error(value, timeout=0.5) -> None`
  *同步重置关节错误*

* `write_joint_reset_error_async(value, timeout=0.5) -> Awaitable[None]`
  *异步重置关节错误*

* `write_joint_reset_error_unchecked(value, timeout=0.5) -> None`
  *非阻塞重置关节错误*

### IController 接口

#### 上下文管理

* `__enter__() -> IController`
  *支持 with 语句的上下文管理器入口*
* `__exit__(arg0, arg1, arg2) -> None`
  *支持 with 语句的上下文管理器出口*
* `close() -> None`
  *手动关闭实时控制器*

#### 实时数据获取

* `get_joint_actual_position() -> NDArray[float64]`
  *实时获取所有关节实际位置（高频读取，返回 {5,4} 数组）*
* `get_joint_actual_effort() -> NDArray[float64]`
  *实时获取所有关节 effort（高频读取，返回 {5,4} 数组，单位：安培）*

<Info>
  **Effort 实时反馈**

  `get_joint_actual_effort()` 返回电流空间的执行器作用量 (actuation effort in current space)，经过滤波处理。

  * 仅在实时控制器模式下可用
  * 适用于碰撞检测和负载监控
  * 可通过 `effort / effort_limit` 计算输出百分比
</Info>

#### 实时数据设置

* `set_joint_target_position(value_array) -> None`
  *实时设置所有关节目标位置（高频写入，参数为 {5,4} 数组）*

***

### 数组形状说明

* **Hand 类批量操作**：返回/接收形状为 `{5, 4}` 的数组（5个手指 × 4个关节）
* **Finger 类批量操作**：返回/接收形状为 `{4}` 的数组（4个关节）
* **Joint 类操作**：返回/接收标量值（单个关节）
* **IController 操作**：处理形状为 `{5, 4}` 的数组，用于高频实时控制

### 操作模式说明

* **同步操作** (`read_*`, `write_*`)：阻塞操作，确保读/写成功
* **异步操作** (`*_async`)：返回 Awaitable 对象，支持 await 语法
* **非阻塞操作** (`*_unchecked`)：立即返回，不保证操作成功，适用于高频率场景
* **缓存获取** (`get_*`)：获取最近一次读取的数据，不阻塞

***

## 触觉感知手套

触觉感知手套（Tactile Sensing Glove）是 Wuji Hand 的选配触觉感知配件。SDK 通过 `TactileGlove` 类提供设备连接和数据读取接口。

### 核心类

#### TactileHandedness

手型枚举：

* `TactileHandedness.LEFT`（0）：左手
* `TactileHandedness.RIGHT`（1）：右手

#### TactileFrame

单帧触觉数据，包含以下属性：

| 属性 | 类型 | 说明 |
| - | - | - |
| `hand` | `TactileHandedness` | 左手或右手 |
| `sequence` | `int`（uint16） | 帧计数器，0–65535 循环 |
| `timestamp_ms` | `int`（uint32） | 设备启动后的毫秒时间戳 |
| `pressure` | `numpy.ndarray`（24, 32）float32 | 压力矩阵 |

**压力值语义：**

* 范围：`[0.0, 1.0]`——`0.0` 表示无接触，`1.0` 表示最大接触
* `NaN` 标记无效单元（不属于任何手指或手掌区域），处理时用 `numpy.isnan()` 跳过

CRC 校验在 SDK 内部完成，调用方收到的帧均已通过校验。

#### TactileGlove

触觉感知手套设备的连接与数据读取。

**构造函数：**

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

<Info>
  `serial_number` 为 USB 设备序列号。传 `None` 时自动发现总线上唯一的设备，若存在多台会抛出异常。
</Info>

### 设备连接

#### 自动发现

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

glove = TactileGlove()
glove.connect()  # 返回 True 表示连接成功
```

#### 指定设备

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

#### Context Manager

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

#### 连接管理方法

* `connect() -> bool`：连接设备，成功返回 `True`
* `disconnect() -> None`：断开连接并停止流式读取
* `is_connected() -> bool`：查询连接状态
* `get_handedness() -> TactileHandedness`：返回设备手型

### 数据读取

#### 阻塞读取

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

frame = glove.read_frame(timeout_ms=100)
print(f"序号: {frame.sequence}")
print(f"压力矩阵形状: {frame.pressure.shape}")  # (24, 32)
print(f"最大压力: {np.nanmax(frame.pressure)}")
```

* 内部执行帧同步和 CRC 校验
* 超时抛出 `RuntimeError`
* 连接断开抛出 `RuntimeError`

#### 流式回调

```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)
# ... 业务逻辑 ...
glove.stop_streaming()
```

* 回调在内部读取线程中调用，不可阻塞
* 通过 `set_disconnect_callback(fn)` 注册断开回调以响应 USB 拔出，之后流式读取自动停止
* `start_streaming` 与 `read_frame` 互斥，不可同时使用
* 所有阻塞操作释放 Python GIL

### 配置与诊断

| 方法 | 说明 |
| - | - |
| `set_sample_rate_hz(hz)` | 设置采样率，范围 1–120 Hz |
| `get_sample_rate_hz() -> int` | 读取当前采样率 |
| `set_streaming(enable)` | 开关设备数据帧流 |
| `get_streaming_enabled() -> bool` | 读取流式输出开关状态 |
| `set_disconnect_callback(callback)` | 注册 `Callable[[], None]`，USB 设备脱离总线时调用一次 |
| `get_device_info() -> TactileDeviceInfo` | 读取 `serial`、`hw_revision`、`fw_version` |
| `get_fw_build() -> TactileFwBuild` | 读取固件构建信息（`git_short_sha`） |
| `get_diagnostics() -> TactileDiagnostics` | 读取计数器：`uptime_ms`、`frame_count`、`crc_err_count`、`dropout_count`、`usb_reset_count` |
| `reset_counters()` | 清零诊断计数器 |
| `get_device_time() -> TactileDeviceTime` | 读取设备单调时钟（`device_monotonic_ns`） |
| `sync_host_epoch(host_unix_ns) -> TactileSyncResult` | 用主机 UTC 纳秒交换同一时刻的设备时钟 |
| `reset_device()` | 软复位。设备会从总线断开并重新枚举，调用后需重新 `connect()` |
| `enter_bootloader(magic)` | 跳转至 bootloader 以执行 OTA，传入 `TACTILE_BOOTLOADER_MAGIC` |

### 异常处理

| 异常 | 触发场景 |
| - | - |
| `RuntimeError` | `read_frame` 超时、读取期间 USB 断开、未连接时调用 `read_frame`，或在已流式时重复调用 `start_streaming` |
| `TactileError` | 命令返回设备协议错误（长度错误、CRC 错误、未知命令、负载错误） |

### 完整示例

覆盖 `TactileGlove` 的完整工作流：自动发现与设备信息查询、主机/设备时钟同步、采样率配置、阻塞读取与流式回调、数据保存以及诊断计数器读取。`reset_device` / `enter_bootloader` 会断开当前连接（设备复位或进入 bootloader），仅在末尾以注释形式给出。

```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)
```

#### 预期输出示例

```
[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
```

### 与 Wuji Hand 联合使用

`Hand` 与 `TactileGlove` 使用独立的 USB 传输和线程，可在同一进程内并行使用，无需额外协调。帧回调在触觉手套的流式消费线程中触发，关节命令运行在调用它们的线程上。该模式适合用触觉反馈驱动运动控制的闭环场景，例如根据当前压力峰值动态缩放关节运动幅度。

参考示例：[`example/joint_with_tactile.py`](https://github.com/wuji-technology/wujihandpy/blob/main/example/joint_with_tactile.py)。

**运行效果：**

* 启动后终端打印 `Squeeze the glove to dampen the motion. Ctrl-C to exit.`
* 关节使能后，F2–F5 四指以 100 Hz 实时循环做周期性弯曲–伸展，F1（拇指）保持不动
* 触觉手套压力峰值越大，关节运动幅度越小：松开手套时幅度最大（约 0.4 rad），用力按压时幅度趋近于 0
* 松手后幅度自动恢复
* Ctrl-C 退出时自动停止流式、关闭关节使能并断开手套

### 协议参考

本节面向需要自行解析触觉数据帧的高级开发者。使用 SDK 的 `TactileGlove` 类时无需关注协议细节。

#### 数据帧格式

触觉感知手套通过 USB CDC（虚拟串口）以最高 120 Hz 输出固定 3088 字节的数据帧。实际速率由 `set_sample_rate_hz`（1–120 Hz）控制：

| 偏移量 | 长度 | 字段 | 类型 | 说明 |
| - | - | - | - | - |
| 0–1 | 2 | 帧头 | u8\[2] | 固定 `0xAA 0x55` |
| 2–3 | 2 | 帧长度 | u16 LE | 固定 3088 |
| 4 | 1 | 手型 | u8 | `0x00` = 左手，`0x01` = 右手 |
| 5–7 | 3 | 填充 | u8\[3] | 将触觉数据对齐到 4 字节边界 |
| 8–3079 | 3072 | 触觉数据 | f32\[24]\[32] LE | 压力矩阵，行优先，`[0.0, 1.0]`。`NaN` = 无效单元 |
| 3080–3081 | 2 | 序号 | u16 LE | 帧计数器，0–65535 循环 |
| 3082–3085 | 4 | 时间戳 | u32 LE | 设备启动后毫秒数 |
| 3086–3087 | 2 | CRC16 | u16 LE | CRC16-CCITT 校验 |

#### CRC16-CCITT

* 多项式：`0x1021`，初始值：`0xFFFF`
* 校验范围：字节 `[2, 3086)`（帧长度到时间戳，共 3084 字节）
* 不含帧头和 CRC 字段本身

#### 帧同步算法

1. 逐字节扫描，匹配帧头 `0xAA 0x55`
2. 读取字节 2–3，验证帧长度 == 3088
3. 读取剩余字节，补齐完整帧
4. 计算 CRC16 并比对
5. CRC 失败：丢弃，从 header\_position + 1 继续扫描
6. CRC 成功：解析并交付帧

<Info>
  `0xAA 0x55` 可能出现在压力数据中（伪帧头），CRC 校验是区分真伪帧头的唯一可靠手段。
</Info>

#### 参考实现

下面的 Python 片段不依赖 SDK，直接从原始字节流（例如通过 `pyserial` 打开 CDC 串口）解析单帧，可作为自定义解析器的起点：

```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)]
```

调用示例：

```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 模块

`wujihandpy.logging` 子模块用于在运行时配置 SDK 日志行为。默认行为、环境变量与排查建议见 [入门 - 获取日志](/docs/wujihandpy/introduction#获取日志)。

### Level 枚举

| 成员 | 数值 | 含义 |
| :- | :- | :- |
| `Level.Trace` | 0 | 最详细，含原始收发字节 |
| `Level.Debug` | 1 | 调试细节 |
| `Level.Info` | 2 | 关键事件（默认） |
| `Level.Warn` | 3 | 警告 |
| `Level.Error` | 4 | 错误 |
| `Level.Critical` | 5 | 严重错误 |
| `Level.Off` | 6 | 关闭日志 |

### 函数

#### set\_log\_path

设置日志文件目录。**必须在 logger 初始化之前调用**（常见 `Hand` 流程下即创建第一个 `Hand` 之前，`flush()` 或其他日志接口也会触发 logger 初始化），否则抛出 `RuntimeError`。

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

#### set\_log\_level

设置日志级别。可在任意时刻调用，立即生效。

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

#### set\_log\_to\_file

控制是否写入文件。仅在 logger 初始化时 `WUJI_LOG_TO_FILE` 为 `true` 才会打开日志文件，初始化后调用此函数可临时关闭或恢复文件输出。若初始化时已被禁用，运行期调用 `set_log_to_file(True)` 不会重新启用文件输出。

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

#### set\_log\_to\_console

控制是否将日志输出到终端（标准输出 / 标准错误）。可在任意时刻调用，立即生效。

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

#### flush

强制将缓冲区中的日志刷入文件和终端。进程退出时 SDK 已自动调用，正常使用无需手动触发。

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


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