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

# 回放拇指对指动作

## 概述

本篇实践介绍如何按设备手性加载录制文件并回放拇指对指动作。应用场景包括演示灵巧手动作和验证录制轨迹的回放流程。

对应的 Python 示例为 [opposition.py](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_hand_2/6_opposition/opposition.py)，C 示例为 [opposition.c](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_hand_2/6_opposition/opposition.c)。 指令字段见 [JointCommand](/docs/wuji-hand/latest/sdk-reference/joint-control#jointcommand)。

<Info>
  本示例驱动全部 20 个关节做大幅度运动，实测指令峰值 1.43 rad，约合 82 度，持续 30 秒。开始前确认灵巧手已牢固固定，清空周围的人员、线缆与台面物品，全程有人监护，并保持随时断电的条件。
</Info>

## 前置条件

### 硬件

| 项目 | 要求 |
| - | - |
| Wuji Hand 2 | 1 只，**20 个关节必须全部在线** |
| 固定 | 设备需牢固固定。30 秒连续大幅运动会产生明显反作用力 |
| 连接方式 | 以太网 |
| 网络 | **网络上只能有这一台设备**，示例用 `auto_connect()` |

### 软件

| 项目 | 要求 |
| - | - |
| 操作系统 | Ubuntu 22.04 LTS 及以上版本 |
| Wuji SDK | v2026.8.31 或更高版本 |
| Python | 3.10 及以上版本 |
| C 工具链 | CMake 3.16 及以上版本。本示例是独立 CMake 工程 |

### 回放文件

示例目录自带 `data/right.replay` 与 `data/left.replay`。**要完整复制整个 `6_opposition/` 目录**，只拷源文件会因为找不到数据文件而失败。

## 数据流

```mermaid theme={null}
flowchart TD
    CONNECT["自动连接 Wuji Hand 2<br/>Python: auto_connect() / C: wuji_scan() + wuji_connect()"]
    CHECK["检查 20 个关节在线<br/>online_joints_count()"]
    SIDE["读取手性并选择 replay 文件<br/>Python: handedness() / C: wuji_hand_2_get_handedness()"]
    VALIDATE["校验文件头、手性、关节数与文件长度"]
    ENABLE["使能设备并创建发布器<br/>Python: enable()、publish()<br/>C: wuji_hand_2_enable()、wuji_hand_2_joint_command_publish()"]
    CAPTURE["订阅 joint_states<br/>捕获完整 20 关节起始姿态"]
    RAMP["按 1 秒缓动到 replay 首帧"]
    PLAY["逐帧读取 20 个位置并发布<br/>Python: publisher.send() / C: publisher_send()<br/>每帧间隔 1 ms"]
    CLEAN["失能、关闭发布器、关闭文件并断开设备"]

    CONNECT --> CHECK --> SIDE --> VALIDATE --> ENABLE --> CAPTURE --> RAMP --> PLAY --> CLEAN
```

### 文件格式

固定长度容器，头部 16 字节，之后每帧 84 字节。

| 段 | 结构 | 说明 |
| - | - | - |
| 头部 | `<8sHBBI` | magic `WJH2RPL\0`、版本、手性 ID、关节数、帧数 |
| 每帧 | `<I20f` | 4 字节帧序号 + 20 个 `float32` 关节角，单位弧度 |

手性 ID 右手为 1，左手为 2。实测文件：30000 帧、2520016 字节，与 `16 + 30000 × 84` 完全吻合。

**校验在使能之前完成。** magic、版本、手性、关节数、文件长度五项任一不符就报错退出，不会使能电机。

## 集成步骤

### 1. 连接并校验回放文件

连接设备后读取手性，选择对应的 `left.replay` 或 `right.replay`。打开文件后校验 magic、版本、手性标识、关节数量和文件长度。文件校验失败时不使能设备。

```python theme={null}
hand = manager.auto_connect("hand_2_opposition")
if hand.online_joints_count().get() != 20:
    raise RuntimeError("expected 20 online joints")
stream, frame_count = _open_replay(_side_for_hand(hand), data_dir=data_dir)
```

### 2. 检查在线关节并准备发布器

确认 20 个关节全部在线，然后调用 `enable()` 并创建 `joint_command` 发布器。读取一帧当前 `joint_states`，用余弦曲线平滑过渡到回放的第一帧。

```python theme={null}
hand.enable()
publisher = hand.joint_command().publish()
start = _capture_start_positions(hand)
```

C 使用 `wuji_hand_2_online_joints_count()`、`wuji_hand_2_enable()` 和 `wuji_hand_2_joint_command_publish()`，并通过 `wuji_hand_2_get_handedness()` 选择文件。

### 3. 按 1 kHz 发布回放帧

逐帧读取 20 个位置值，为每个值创建 `JointCommand(position, 0, 0)`，调用发布器发送，并保持 1 ms 的帧间隔。回放循环应响应停止请求和文件读取错误。

```python theme={null}
def send(qpos):
    publisher.send([wuji_sdk.JointCommand(float(v), 0.0, 0.0) for v in qpos])
_play_replay(stream, frame_count, send, ramp_start=start,
             stop_requested=lambda: _STOP_REQUESTED)
```

以上带下划线的函数均为 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_hand_2/6_opposition/opposition.py) 中的辅助函数。`_open_replay()` 负责完整容器校验，`_capture_start_positions()` 根据 `nid` 排列关节，`_play_replay()` 完成起始过渡和逐帧回放。

C 对应使用 `open_replay_path()`、`capture_start_positions()`、`send_ramp()` 和 `play_replay()`，底层通过 `wuji_joint_command_publisher_send()` 发送固定的 20 关节数组。

### 4. 失能并释放资源

无论正常结束还是异常退出，都先失能，再关闭发布器和回放文件，最后断开设备。C 版本使用 `wuji_hand_2_disable(device, NULL)`、`wuji_joint_command_publisher_close(publisher)`，并关闭回放文件、断开并释放设备、调用 `wuji_shutdown()`。

```python theme={null}
hand.disable()
publisher.close()
stream.close()
manager.disconnect_all()
```

将清理放在 `finally` 中，并像 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_hand_2/6_opposition/opposition.py) 和 [C 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_hand_2/6_opposition/opposition.c) 中一样逐项收集错误，确保前一步清理失败时仍继续释放其余资源。

## 运行与输出

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    python examples/python/wuji_hand_2/6_opposition/opposition.py
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    ./build/hand2-opposition/6_opposition
    ```
  </Tab>
</Tabs>

### 实测数据

在 20 关节全在线的右手 Wuji Hand 2 上完整跑完一轮：

| 指标 | 实测 |
| - | - |
| 回放帧数 | 30000 帧，无丢帧 |
| 总耗时 | 30.00 秒 |
| **实际发布频率** | **1000.0 Hz** |
| 指令峰值 | 1.4315 rad（82.0 度） |
| 起始姿态最大绝对值 | 0.0668 rad |
| 结束姿态最大绝对值 | 0.0563 rad |

1 ms 的目标间隔在 Python 里稳定达成，30000 帧累计误差可以忽略。

**结束时手自己收住了。** 录制的最后一帧本身就是回到张开位，所以正常跑完不需要额外的归位动作。这是录制内容的性质，不是示例做了回位处理，中断时不享有这个保证。

### 各手指的运动范围

从右手回放文件离线统计每个关节的行程：

| 手指 | 四个关节的行程（rad） | 峰值 |
| - | - | - |
| 拇指 | 1.125 / 0.734 / 1.296 / 0.801 | +1.319 |
| 食指 | 0.799 / 0.477 / 1.428 / 1.257 | **+1.431** |
| 中指 | 0.787 / 0.620 / 1.205 / 0.298 | +1.197 |
| 无名指 | 0.930 / 0.434 / 0.795 / 0.104 | +0.925 |
| 小指 | 0.630 / 0.524 / 1.188 / 0.322 | +1.184 |

食指第三关节行程最大，1.428 rad 约合 81.8 度。

## 注意事项

| 分支点 | 表现 | 处理 |
| - | - | - |
| 手性不匹配 | 右手设备读到 `left.replay` 时 `side_id` 校验失败 | 报错退出，不使能。示例按设备手性自动选文件，一般不会触发 |
| 文件缺失或损坏 | 报 `replay file not found` 或容器校验失败 | 确认复制了完整的 `6_opposition/` 目录 |
| 在线关节不足 20 | 报错退出 | 先排查离线关节 |
| 网络有多台设备 | `auto_connect()` 报 `Found N devices, please specify SN`，直接退出 | 共用网络必须改成指定 SN |
| Ctrl+C | 退出码 130，停止后续帧，**不做回位动作** | 手停在中断时的姿态，随后被失能 |
| 正常结束 | 退出码 0 | 录制的最后一帧本身就是回到张开位 |

中断时手会停在当前姿态而不是自动张开。大幅度中途停止可能停在握拳状态，需要手动处理。

## 扩展用法

### 多设备网络下必须指定 SN

与 [MIT 关节扫描](/docs/wuji-hand/latest/sdk-practice/mit-sweep)相同，`auto_connect()` 在发现多台设备时拒绝连接并列出全部 SN，不会自动选择。改成 `manager.connect(sn="WH2KA00XXXXXXXXX", device_name="hand_2_opposition")` 即可。

### 回放自己录制的轨迹

按同样的容器格式生成文件即可复用整套回放逻辑：16 字节头部加每帧 84 字节。帧序号字段在回放时被跳过，只有 20 个关节角参与控制。注意 `side_id` 要与目标设备手性一致，否则容器校验不通过。

### 改变回放速度

间隔写死 1 ms。改大间隔会整体放慢动作，但因为不做重采样，关节角序列不变，只是每个姿态停留更久。想要真正的变速需要自己对轨迹重采样。

### 中断后的复位

Ctrl+C 不触发回位。中途停止后手停在当前姿态，可以用[发布控制指令](/docs/wuji-hand/latest/sdk-practice/publish-command)的平滑归零流程把它柔和地收回零位。不要直接发 `pos=0`，大偏差在 MIT 阻抗下会导致猛烈回弹。

## 相关内容

* [发布控制指令](/docs/wuji-hand/latest/sdk-practice/publish-command)：`joint_command` 的通用用法与平滑归零
* [MIT 扫描](/docs/wuji-hand/latest/sdk-practice/mit-sweep)：幅度最小的指令链路验证
* [数据订阅：回调](/docs/wuji-hand/latest/sdk-practice/subscribe-callback)：读取回放过程中的实际关节状态
* [数据类型参考](/docs/wuji-sdk/data-reference/)：`JointCommand` 结构


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