> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wuji.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK 接口

> [!NOTE]
>
> Wuji Hand 2 的 Python 语义化 API。统一模式：`hand.{resource}().{action}()`。所有操作都针对整只手（20 关节）。

本页聚焦 Wuji Hand 2 专属的语义化 API。SDK 基于通用的 **Wuji SDK**（`wuji_sdk`，`pip install wuji-sdk`）——通用的安装、设备连接与数据订阅模型见 [Wuji SDK 文档](https://docs.wuji.tech/docs/zh/wuji-sdk/latest/index.md)，源码见 [wuji-sdk 仓库](https://github.com/wuji-technology/wuji-sdk)。

## 连接

> [!NOTE]
>
> **网络连接前提**：Wuji Hand 2 使用静态 IP，不使用 DHCP。设备按左右手出厂分配固定地址——左手 `192.168.1.110`，右手 `192.168.1.111`，网关 `192.168.1.1`，子网掩码 `255.255.255.0`。首次连接前将电脑对应网卡设置为同一网段（如 `192.168.1.50`），再按地址连接。出厂 IP 可后续修改，见 [ip（SET/GET）](#ip（set/get）)。

### 通过 SdkManager 连接（推荐）

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

manager = SdkManager.instance()

# 自动发现并连接局域网内首个 Wuji Hand 2
hand = manager.auto_connect(device_name="wuji_hand_2")

# 已知序列号或地址时显式连接
hand = manager.connect(sn=<device_sn>, device_name="wuji_hand_2")
hand = manager.connect(address="192.168.1.110:50001", device_name="wuji_hand_2")

# 或按左右手直接连接，无需序列号
hand = manager.connect(handedness=Handedness.Right, device_name="wuji_hand_2")
```

`auto_connect` 和 `connect` 的 `device_name="wuji_hand_2"` 重载会直接返回 `WujiHand2` 实例（带类型提示）。`handedness` 与 `sn`、`address` 互斥。多只同手性 Hand 2 在网时会抛 `AmbiguousHandedness`，请改用 `sn` 指定。

### ConnectOptions

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

opts = ConnectOptions(
    timeout_ms=1000,
    retry_count=3,
    enable_bridge=True,    # 默认 True：允许多个客户端（Wuji Studio + 录制脚本 + 业务程序）同时连接
)
hand = manager.connect(sn=<device_sn>, device_name="wuji_hand_2", options=opts)
```

设 `enable_bridge=False` 切换成独占单客户端模式。

### 实例属性

| 属性 | 类型 | 说明 |
| - | - | - |
| `serial_number` | `str` | 设备序列号 |
| `device_name` | `str` | 连接时指定的名字（默认 `"wuji_hand_2"`） |
| `info` | `Optional[DeviceInfo]` | 设备信息：`serial_number`、`firmware_version` |
| `is_connected` | `bool` | 连接状态 |

`hand.hw_version().get()` 返回出厂硬件版本 `HwVersion(major, minor, patch)`。`0.0.0` 表示未在工厂端写入。

## Hand 级资源

本节覆盖整手 API（20 关节）。单关节的读取与动作通过 `hand.joint(k)` / `hand.joints()` 返回的 `JointHandle` 进行（见 [关节遍历](#关节遍历)）。设备默认运行在 MIT 控制模式，控制模式不通过 Python API 设置。

### handedness（GET）

获取**左右手识别**结果（left / right）。

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

### online\_joints\_count（GET）

获取**在线关节数**（0–20）。

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

### joint\_diagnostics（SUB）

**关节诊断流**：订阅逐关节的状态字 / 电流 / 母线电压 / 温度 / 错误码。帧变长，仅含在线关节，用每条的 `nid` 定位关节。本流与 `joint_states` 共用同一条设备流，调低其中一个的推送频率，另一个会同步降低，见 [joint\_states（SUB）](#joint_states（sub）)。

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

`error_code_current` 用静态方法 `WujiHand2.describe_error(code)` 解码，返回对象含错误名、说明、`severity`（`Warning` / `DeferredStop` / `ImmediateStop` / `Fatal`）与 `clear_policy`（`AutoClear` / `ManualClear` / `NonClearable`），未知错误码返回 `None`。返回对象的 `code` 字段是整数，与传入的错误码相同。直接打印诊断条目对象时，错误码按 16 进制显示。

> [!NOTE]
>
> 关节角请用 `joint_states` 流的 `position`，诊断流不含关节角。

### comm\_diag（GET，1 Hz）

**通信诊断**：每秒返回 5 根手指的吞吐 / 错误率。

```python theme={null}
diag = hand.comm_diag().get()           # → HandCommunicationDiagnostics
for finger in diag.fingers:             # 5 根手指各自的吞吐 / 错误率
    print(finger.tx_kbps, finger.rx_kbps, finger.error_per_sec)
```

用于排查关节与控制板之间的通信问题。

### effort\_limit（SET/GET）

**力矩限幅**：读写各关节的力矩上限（A）。

```python theme={null}
hand.effort_limit().set(1.5)            # 全部关节设为 1.5 A
limits = hand.effort_limit().get()      # → list[Optional[float]]
```

写入拒绝 NaN / Inf / 负值与超出设备上限的值，整手与单关节写入同样校验。写入被拒时报错，当前生效值保持不变。上限由固件设定，常见异常与处理见 [Wuji SDK 故障排查](https://docs.wuji.tech/docs/zh/wuji-sdk/latest/troubleshooting.md)。

### mit\_params（SET/GET）

**MIT 阻抗参数**：读写各关节的 kp / kd。

```python theme={null}
hand.mit_params().set((1.0, 0.05))           # 全部关节统一 kp / kd
hand.mit_params().set([(1.0, 0.05)] * 20)    # 长度 20 的列表逐关节设置
mp = hand.mit_params().get()                 # → list[Optional[MitParam]]，离线关节为 None
```

写入拒绝 NaN / Inf / 负值。

### clear\_fault（EXEC）

**清除故障**：直接动作，无参数时作用于整手。

```python theme={null}
hand.clear_fault()                  # 清除全部关节故障
hand.clear_fault(joints=mask)       # 可选：长度 20 的 0/1 掩码，只清除掩码位为 1 的关节
```

### 用户零点（origin）

把当前物理位置标定为关节侧零位。关节正在运动时延迟到下次 IDLE 才生效。

```python theme={null}
hand.set_origin()                   # 整手：所有关节刷新用户零点
hand.clear_origin()                 # 整手：清除所有关节的用户零点
hand.set_origin(joints=mask)        # 可选：长度 20 的 0/1 掩码，只作用于掩码位为 1 的关节
```

### 使能 / 失能 / 急停

```python theme={null}
hand.enable()               # 使能全部关节
hand.disable()              # 失能全部关节
hand.enable(joints=mask)    # 可选：长度 20 的 0/1 掩码，作用于掩码位为 1 的关节
hand.emergency_stop()       # 急停：整手动作，不接受掩码
```

### joint\_states（SUB）

**关节状态流**：订阅 20 关节的实时状态帧，原生输出率 1000 Hz。可通过订阅句柄调低推送频率：`sub.set_rate(hz)` 返回实际生效值，传 `0` 恢复原生输出率，完整用法见 [Wuji SDK 数据订阅](https://docs.wuji.tech/docs/zh/wuji-sdk/latest/data-subscription.md#调节订阅流输出率)。本流与 `joint_diagnostics` 共用同一条设备流，调低其中一个的推送频率，另一个会同步降低。

> [!NOTE]
>
> 推送频率调节适用于 `joint_states`、`joint_diagnostics`与 `imu` 流。固件与 Wuji SDK 需同批升级到支持该能力的版本，版本要求见对应发布记录。

```python theme={null}
sub = hand.joint_states().subscribe()   # → Subscription[JointStateFrame]
frame = sub.recv()                      # 同步非阻塞，None = 暂无数据
frame = await sub.recv_async()          # 异步等待
print(frame.header.seq, frame.header.timestamp_us)
for j in frame.joints:
    print(j.nid, j.position, j.velocity, j.effort)
sub.close()
```

回调模式：

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

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

> [!NOTE]
>
> `position` 为关节侧角度（rad），`velocity` 为关节侧角速度（rad/s），这是获取关节角与关节速度的唯一推荐路径。帧变长、仅含在线关节，用每条的 `nid` 定位关节。`joint_diagnostics` 流不包含关节角。

### joint\_command（PUB）

**关节命令**：发布 20 关节的位置 / 速度 / 力矩前馈。每次发送恰好 20 个 `JointCommand`，每个携带该关节的 `position` / `velocity` / `effort`。

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

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

要把人手关键点转换成这里的 20 维关节命令，用 Wuji SDK 的[手部重定向](https://docs.wuji.tech/docs/zh/wuji-sdk/latest/retargeting.md)。

### Flash 日志导出（诊断）

```python theme={null}
# 该调用会阻塞，直至导出完成
result = hand.export_flash_logs("./logs")
print(result["path"])      # str：导出 JSONL 文件的绝对路径
print(result["frames"])    # int：导出的日志记录数
```

SDK 会在 `out_dir` 中写入一个 `flash_<serial>_<YYYY-MM-DD_HHMMSS>.jsonl` 文件。省略 `out_dir` 时，文件保存到 `~/.wuji/logs`。

SDK 先写入临时文件，导出完成后再将其保存为 JSONL 文件。导出失败时，SDK 会自动清理临时文件，输出目录中不会出现残缺的导出结果。

> [!NOTE]
>
> 多个会话同时导出同一设备的日志时，SDK 会自动重试。若重试后仍无法完成，本次导出会报设备忙。SDK 会清除临时文件，不会保留未完成的日志。

> [!WARNING]
>
> 日志导出要求设备固件和 Wuji SDK 使用兼容版本。版本不匹配时，导出会报错且不会生成日志文件。请同步升级设备固件和 Wuji SDK 后重试。

### ip（SET/GET）

**读写设备静态 IP 地址**。`set` 把新 IP 写入设备 flash，固件不会热切换正在运行的以太网栈，新 IP 下次重启才生效。`set` 之后、重启之前，`get` 仍返回当前生效的 IP。

```python theme={null}
hand.ip().get()                    # 读当前 IP，如 "192.168.1.110"
hand.ip().set("192.168.1.201")     # 写入 flash，重启后生效
```

IP 在同网段内必须唯一，避开其他设备与电脑网卡。

修改 IP 分四步：`set` 写入新地址、`reboot` 重启设备、按新 IP 重连、`get` 确认。

```python theme={null}
hand.ip().set("192.168.1.201")
hand.reboot()
manager.disconnect_all()
# 等设备重启且以太网就绪，约 8 秒
hand = manager.connect(address="192.168.1.201:50001", device_name="wuji_hand_2")
hand.ip().get()                    # → "192.168.1.201"
```

> [!WARNING]
>
> 新 IP 重启后才生效。`set` 只写入 flash，不改变正在运行的连接。重启前 `get` 返回的仍是当前 IP。

### reboot（EXEC）

**重启设备**。设备会断连，重启后需重新连接。常与 `ip().set()` 搭配，让写入 flash 的新 IP 生效。

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

### imu（SUB）

**板载 IMU 流**：以 100 Hz 的原生输出率推送加速度与角速度，可通过订阅句柄调低。设备不做板载姿态融合，`orientation` 字段不可用，按 ROS `sensor_msgs/Imu` 约定以 `orientation_covariance[0] = -1` 标记朝向无效。数据按 IMU 传感器自身坐标系输出，各轴与手部结构的对应关系将随手部模型后续更新明确。

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

`ImuData` 是跨设备通用类型，字段定义见 [Wuji SDK 数据类型参考](https://docs.wuji.tech/docs/zh/wuji-sdk/latest/data-reference.md#imudata)。

## 关节遍历

`JointHandle` 提供 `label` / `index`（用于与 `effort_limit()` / `mit_params()` 等 20 元素返回数组做下标对应），以及单关节资源与动作。`FingerHandle` 用于按手指遍历关节。

| 方法 | 返回类型 | 说明 |
| - | - | - |
| `hand.joints()` | `list[JointHandle]` | 全部 20 个关节 |
| `hand.fingers()` | `list[FingerHandle]` | 全部 5 根手指 |

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

for finger in hand.fingers():
    for joint in finger.joints():          # 每根手指 4 个关节，按 S1..S4 顺序
        print(joint.label)                 # "thumb_S1", "thumb_S2", ...
```

### JointHandle

**关节句柄**：提供 label / index、单关节资源与单关节动作。

| 成员 | 类型 | 说明 |
| - | - | - |
| `label` | `str` | 属性。关节标签，格式 `{finger}_S{1..4}`，如 `"thumb_S1"`、`"pinky_S4"` |
| `index` | `int` | 属性。全局索引（0–19） |
| `effort_limit()` | 资源（SET/GET） | 单关节力矩限幅（A） |
| `error_code()` | 资源（GET） | 单关节当前错误码，用 `describe_error()` 解码。返回整数，直接打印时自行按 `f"0x{code:04X}"` 格式化 |
| `status_word()` | 资源（GET） | 单关节状态字 |
| `enable()` / `disable()` | 动作 | 单关节使能 / 失能 |
| `clear_fault()` | 动作 | 清除该关节故障 |
| `set_origin()` / `clear_origin()` | 动作 | 设置 / 清除该关节用户零点 |

### FingerHandle

**手指句柄**：遍历该手指的 4 个关节。

| 方法 | 返回类型 | 说明 |
| - | - | - |
| `joints()` | `list[JointHandle]` | 该手指 4 个关节，按 S1..S4 顺序 |

## 数据类型

### JointDiagnosticsFrame / JointDiagnosticsEntry

**关节诊断帧**：`hand.joint_diagnostics().subscribe()` 流的帧类型。变长，仅含在线关节。

```python theme={null}
class JointDiagnosticsFrame:
    header: FrameHeader                    # seq / timestamp_us / frame_id
    num_joints: int
    joints: list[JointDiagnosticsEntry]
    comm: Hand2CommSummary                 # 帧级通信健康摘要，1 Hz 刷新（见 Hand2CommSummary）

class JointDiagnosticsEntry:
    nid: int                               # 节点编号，跨帧定位关节
    status_word: StatusWord                # 状态字
    current: float                         # 电流 (A)
    vbus_v_fb: float                       # 母线电压 (V)
    mcu_temp_c_fb: float                   # MCU 温度 (°C)
    error_code_current: int                # 当前错误码（整数值，打印条目对象时按 16 进制显示），用 describe_error() 解码
    comm_response_rate_pct: int            # 该关节最近 1 秒的总线响应率，0–100
    comm_timeout_total: int                # 该关节累计总线超时次数
```

### MitParam

**MIT 参数**：`mit_params().get()` 返回元素。

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

`set` 接受单个 `(kp, kd)`（应用到全部关节）或长度 20 的列表（逐关节）。`get` 返回 20 个元素，离线关节为 `None`。

### JointStateFrame

**关节状态帧**：`hand.joint_states().subscribe().recv()` 返回值。变长，仅含在线关节。

| 属性 | 类型 | 说明 |
| - | - | - |
| `header` | `FrameHeader` | `seq` / `timestamp_us` / `frame_id` |
| `num_joints` | `int` | 本帧关节数量 |
| `joints` | `list[JointStateEntry]` | 关节状态数组 |

### JointStateEntry

**单关节状态**。

| 属性 | 类型 | 说明 |
| - | - | - |
| `nid` | `int` | 节点编号，跨帧定位关节 |
| `position` | `float` | 位置（rad，关节侧） |
| `velocity` | `float` | 速度（rad/s） |
| `effort` | `float` | 力矩 (A) |

### JointCommand

**关节命令**：`joint_command().publish().send()` 入参元素。

```python theme={null}
class JointCommand:
    position: float    # 位置 (rad)
    velocity: float    # 速度 (rad/s)
    effort: float      # 力矩前馈 (A)
```

### HandCommunicationDiagnostics

**通信诊断数据**。

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

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

### StatusWord

**状态字**：[JointDiagnosticsEntry](#jointdiagnosticsframe-/-jointdiagnosticsentry) 的 `status_word` 字段类型，SDK 暴露的公开状态视图。

| 字段 | 类型 | 说明 |
| - | - | - |
| `ext_state` | `int` | 扩展状态值（如 Init / Ready / Enabled / Stopped） |
| `ext_state_name` | `str` | `ext_state` 的语义名 |
| `position_limit_active` | `bool` | 位置限幅触发 |
| `velocity_limit_active` | `bool` | 速度限幅触发 |
| `current_limit_active` | `bool` | 电流限幅触发 |

### Hand2CommSummary

**帧级通信健康摘要**：[JointDiagnosticsFrame](#jointdiagnosticsframe-/-jointdiagnosticsentry) 的 `comm` 字段类型，同时覆盖设备内部总线与 SDK 本地端到端（以太网）两段。设备内部快照由 SDK 常驻任务以 1 Hz 自动刷新，无需手动轮询。

| 字段 | 类型 | 说明 |
| - | - | - |
| `age_ms` | `int` | 设备内部快照距今毫秒数。`65535` = 从未成功采样（设备内部字段均为 0）。`65534` = 已饱和，快照距今 ≥ 65.5 秒 |
| `e2e_received` | `int` | 全部订阅流累计收帧数 |
| `e2e_lost` | `int` | 以太网段经序号缺口检测出的累计丢帧数 |
| `e2e_reordered` | `int` | 累计乱序/迟到帧数 |
| `e2e_duplicates` | `int` | 累计重复帧数 |
| `e2e_window_loss_x100` | `int` | 最近 1 秒窗口丢包率，0.01% 单位 |
| `rpc_total` | `int` | 累计请求数。当前连接不提供 RPC 统计时，此字段为 `0`。此时，`rpc_retries` 和 `rpc_timeouts` 不表示实际重试或超时次数 |
| `rpc_retries` | `int` | 累计请求重传次数 |
| `rpc_timeouts` | `int` | 累计最终超时的请求数 |
| `comm_get_failures` | `int` | 累计内部快照刷新失败次数 |
| `sdk_dropped` | `int` | SDK 内部某一跳落后而丢弃的帧数（累计）——可能是应用侧的订阅消费端，也可能是 SDK 内部的流 handler。与网络丢帧 `e2e_lost` 语义不同 |

> [!NOTE]
>
> **设备内部字段的判读**：`age_ms` 以及每关节的 `comm_*` 字段同属一份快照，由 SDK 以 1 Hz 刷新。固件升级期间会**有意跳过**这次刷新，因此短暂的陈旧值属预期状态而非故障。用 `comm_get_failures` 区分两种情况：`age_ms` 增大而 `comm_get_failures` 不变 = 正在有意跳过，`comm_get_failures` 持续增加 = 刷新本身在失败。

> [!WARNING]
>
> **计数器饱和，不回绕**：本表所有计数达到上限后即停在上限（16 位的 `e2e_reordered`、`e2e_duplicates`、`rpc_retries`、`rpc_timeouts`、`comm_get_failures` 停在 65535，32 位的停在 4294967295），不会归零重新计数。1 kHz 流长时间运行时确实可能触顶，因此应按**两次采样之间的增量**判读，而非当作全生命周期的绝对累计值。

## 关节编号

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

标签格式：`{finger}_S{1..4}`（如 `thumb_S1`、`pinky_S4`）。

## 异常处理

所有 SDK 操作错误统一抛 `WujiException`，消息中携带错误类型前缀（`Disconnected`、`Timeout`、`PathNotFound`、`SchemaMismatch`、`SerializeError`、…）。业务侧按需 `try / except` 即可。

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

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


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