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

# 回放拇指对指动作

## 概述

读取随示例分发的录制文件，按设备手性选择左手或右手那一份，以 1 kHz 把 30000 帧关节角一次性回放完。

这是两个运动示例里幅度最大的一个，用来观察灵巧手的完整动作能力。示例不做插值、不做重采样、不做平滑，也不添加恢复动作，录制里是什么就发什么。

对应的 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)。

关节命令与控制模式见[控制指南](https://docs.wuji.tech/docs/zh/wuji-hand/beta1/control-guide)，本页只讲怎么跑起来与实测结果。

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

## 前置条件

### 硬件

| 项目 | 要求 |
| - | - |
| 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
    HAND["handedness()<br/>读设备手性"]
    PICK["选 right.replay 或 left.replay"]
    VAL["校验容器<br/>magic、version、手性与长度"]
    READ["逐帧解包<br/>序号 + 20 个 float32"]
    PUB["joint_command().publish()<br/>1 ms 间隔"]
    FW["固件 MIT 阻抗控制"]

    HAND --> PICK
    PICK --> VAL
    VAL -->|"校验通过才使能"| READ
    READ -->|"30000 帧"| PUB
    PUB -->|"1 kHz"| FW
```

### 文件格式

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

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

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

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

## 运行时分支

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

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

## 调用顺序

> [tab] Python
>
> ```python theme={null}
> import struct, time, wuji_sdk
> from wuji_sdk import JointCommand
>
> HEADER = struct.Struct("<8sHBBI")
> FRAME  = struct.Struct("<I20f")
>
> manager = wuji_sdk.SdkManager.instance()
> hand = manager.auto_connect("hand_2_opposition")   # 多设备时改 connect(sn=...)
>
> # 1. 按设备手性选回放文件
> side = str(hand.handedness().get()).lower()        # "right" 或 "left"
>
> # 2. 打开并校验容器，这一步在使能之前
> data = open(f"data/{side}.replay", "rb").read()
> magic, version, side_id, joint_count, frame_count = HEADER.unpack(data[:HEADER.size])
> assert magic == b"WJH2RPL\0" and joint_count == 20
> assert len(data) == HEADER.size + frame_count * FRAME.size
>
> # 3. 门控与使能
> assert int(hand.online_joints_count().get()) == 20
> hand.enable()
>
> # 4. 逐帧回放，固定 1 ms 间隔
> publisher = hand.joint_command().publish()
> t0 = time.monotonic()
> for i in range(frame_count):
>     off = HEADER.size + i * FRAME.size
>     qpos = FRAME.unpack(data[off:off + FRAME.size])[1:]   # 跳过帧序号
>     publisher.send([JointCommand(v, 0.0, 0.0) for v in qpos])
>     wait = t0 + (i + 1) * 0.001 - time.monotonic()
>     if wait > 0:
>         time.sleep(wait)
>
> # 5. 清理
> hand.disable()
> publisher.close()
> manager.disconnect_all()
> ```

> [tab] C
>
> ```c theme={null}
> #include "wuji_sdk.h"
>
> /* 1. 读设备手性选文件 */
> WujiHandedness side;
> wuji_hand_2_handedness(dev, &side);
> const char *path = (side == WUJI_HANDEDNESS_RIGHT) ? "data/right.replay"
>                                                    : "data/left.replay";
>
> /* 2. 打开并校验容器（magic / version / side_id / joint_count / 文件长度） */
> FILE *fp = fopen(path, "rb");
> /* 头部 16 字节：char magic[8] + uint16 version + uint8 side_id
>                  + uint8 joint_count + uint32 frame_count */
>
> /* 3. 门控与使能 */
> uint32_t online = 0;
> wuji_hand_2_online_joints_count(dev, &online);
> if (online != 20) { /* 报错退出，不使能 */ }
> wuji_hand_2_enable(dev);
>
> /* 4. 逐帧回放，1 ms 间隔 */
> struct WujiPub *pub = NULL;
> wuji_hand_2_joint_command_publisher(dev, &pub);
> WujiJointCommand cmd[20] = {0};
> for (uint32_t i = 0; i < frame_count; i++) {
>     uint32_t seq; float qpos[20];
>     fread(&seq, 4, 1, fp);
>     fread(qpos, 4, 20, fp);
>     for (int j = 0; j < 20; j++) cmd[j].position = qpos[j];
>     wuji_hand_2_publish_joint_command(pub, cmd, 20);
>     usleep(1000);
> }
>
> /* 5. 清理 */
> wuji_hand_2_disable(dev);
> wuji_pub_close(pub);
> fclose(fp);
> wuji_dev_disconnect(dev);
> wuji_dev_release(dev);
> ```

C 侧是独立 CMake 工程，要单独配置构建：

```bash theme={null}
cmake -S examples/c/wuji_hand_2/6_opposition -B build/hand2-opposition \
  -DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
  -DWUJI_SDK_LIB="${SDK}/lib/libwuji_sdk_c.so"
cmake --build build/hand2-opposition
```

## 运行与输出

> [tab] Python
>
> ```bash theme={null}
> python examples/python/wuji_hand_2/6_opposition/opposition.py
> ```

> [tab] C
>
> ```bash theme={null}
> ./build/hand2-opposition/6_opposition
> ```

### 实测数据

在 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 度。

## 调整与适配

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

与 [MIT 关节扫描](https://docs.wuji.tech/docs/zh/wuji-hand/beta1/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 不触发回位。中途停止后手停在当前姿态，可以用[发布控制指令](https://docs.wuji.tech/docs/zh/wuji-hand/beta1/sdk-practice/publish-command)的平滑归零流程把它柔和地收回零位。不要直接发 `pos=0`，大偏差在 MIT 阻抗下会导致猛烈回弹。

## 相关内容

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


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