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

# 运行 MIT 关节扫描

## 概述

本篇实践介绍如何使能 Wuji Hand 2 并向关节发送固定的余弦位置序列。应用场景包括验证控制指令链路和检查 MIT 参数配置。

对应的 Python 示例为 [7.mit\_sweep.py](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_hand_2/7.mit_sweep.py)，C 示例为 [7\_mit\_sweep.c](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_hand_2/7_mit_sweep.c)。 指令字段见 [JointCommand](/docs/wuji-hand/latest/sdk-reference/joint-control#jointcommand)，控制参数见 [MitParam](/docs/wuji-hand/latest/sdk-reference/effort-and-mit#mitparam)。

<Info>
  本示例会使能电机。开始前清空手部周围的人员、线缆与台面物品，全程有人监护，并保持随时断电的条件。

  手部原本不在零位时，第一帧就会把 20 个关节一起拉向零位，没有插值缓冲。需要柔和归零请先运行[发布控制指令](/docs/wuji-hand/latest/sdk-practice/publish-command)的平滑归零流程。
</Info>

## 前置条件

### 硬件

| 项目 | 要求 |
| - | - |
| Wuji Hand 2 | 1 只，**20 个关节必须全部在线**，否则示例直接报错退出 |
| 连接方式 | 以太网 |
| 网络 | **网络上只能有这一台设备**，原因见下方运行时分支 |

### 软件

| 项目 | 要求 |
| - | - |
| 操作系统 | Ubuntu 22.04 LTS 及以上版本 |
| Wuji SDK | v2026.8.31 或更高版本 |
| Python | 3.10 及以上版本 |
| C 工具链 | CMake 3.16 及以上版本，以及解压后的 SDK 包 |

本示例不接受任何命令行参数。

### 前序步骤

确认设备的 MIT 参数与力矩上限处于出厂值。实测值：

```text theme={null}
mit_params:    kp=3, kd=0.05   （20 个关节相同）
effort_limit:  1.5             （20 个关节相同）
```

`kp` 被改小会让本就微弱的位移进一步消失，改大则超出本示例的安全设定。

## 数据流

```mermaid theme={null}
flowchart TD
    CONNECT["连接 Wuji Hand 2<br/>Python: SdkManager.auto_connect()<br/>C: wuji_scan() → wuji_connect()"]
    CHECK["确认 20 个关节在线<br/>Python: hand.online_joints_count().get()<br/>C: wuji_hand_2_online_joints_count()"]
    ENABLE["使能设备并创建命令发布器<br/>Python: hand.enable()、hand.joint_command().publish()<br/>C: wuji_hand_2_enable()、wuji_hand_2_joint_command_publish()"]
    CAPTURE["读取完整 20 关节起始姿态<br/>Python: hand.joint_states().subscribe()<br/>C: wuji_hand_2_subscribe_joint_states()"]
    RAMP["示例逻辑：缓动 1 秒到扫掠起点<br/>Python: publisher.send()<br/>C: wuji_joint_command_publisher_send()"]
    SWEEP["示例逻辑：发送 101 帧扫掠，间隔 20 ms<br/>Python: publisher.send()<br/>C: wuji_joint_command_publisher_send()<br/>仅关节 0 按余弦变化"]
    CLEAN["释放设备资源<br/>Python: hand.disable()、publisher.close()、manager.disconnect_all()<br/>C: wuji_hand_2_disable()、wuji_joint_command_publisher_close()、wuji_dev_disconnect()"]

    CONNECT --> CHECK --> ENABLE --> CAPTURE --> RAMP --> SWEEP --> CLEAN
```

序列在本地一次生成完毕再逐帧发送，不做插值也不做重规划。每条 `JointCommand` 是「位置 + 速度 + 力矩」三元组，本示例只填位置，速度与力矩都是 0。

注意每帧都是完整的 20 维指令。关节 0 之外的 19 个位置写死为 0，等于持续命令它们回到零位，这部分运动不受 0.02 rad 这个幅度约束。

## 集成步骤

### 1. 连接并检查关节

扫描并连接 Wuji Hand 2 后，读取在线关节数量。示例要求 20 个关节全部在线，检查失败时不使能设备。

```python theme={null}
hand = manager.auto_connect(device_name="hand_2_mit_sweep")
if int(hand.online_joints_count().get()) != 20:
    raise RuntimeError("expected 20/20 online joints")
```

C 使用 `wuji_scan()`、`wuji_connect()` 和 `wuji_hand_2_online_joints_count()` 完成同样的检查。

### 2. 使能并取得起始姿态

调用 `enable()` 后创建 `joint_command` 发布器。示例同时订阅 `joint_states`，通过 `nid_to_joint_index` 将完整帧整理为固定的 20 关节顺序，再从当前姿态平滑过渡到扫掠起点。

```python theme={null}
hand.enable()
publisher = hand.joint_command().publish()
start = capture_start_positions(hand)
sweep = generate_sweep()
send_sweep(ramp_frames(start, sweep[0], 1000), publisher,
           wuji_sdk, interval=0.001)
```

C 使用 `wuji_hand_2_enable(hand, NULL)`、`wuji_hand_2_joint_command_publish()`，并通过回调复制一帧完整状态。

### 3. 按固定周期发送扫掠指令

扫掠只改变关节 0，幅度为 0.02 弧度，共发送 101 帧，帧间隔 20 ms。每帧创建 20 个 `JointCommand`，其余关节保持零位。发送循环应响应 Ctrl+C。

```python theme={null}
send_sweep(sweep, publisher, wuji_sdk)
```

`capture_start_positions()`、`ramp_frames()`、`generate_sweep()` 和 `send_sweep()` 是 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_hand_2/7.mit_sweep.py) 中的辅助函数。保留完整帧校验、1 秒起始过渡和停止检查，不能直接从未知姿态发送扫掠首帧。

C 使用对应的 `capture_start_positions()`、`send_ramp()` 和 `send_sweep()` 辅助函数，发送最终调用 `wuji_joint_command_publisher_send(publisher, commands)`。

### 4. 停止运动并清理

结束或中断时先失能，再关闭发布器，最后断开并释放设备。C 示例使用 `wuji_hand_2_disable(hand, NULL)` 和 `wuji_joint_command_publisher_close(publisher)`，并在所有错误路径执行相同清理顺序。

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

[Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_hand_2/7.mit_sweep.py) 和 [C 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_hand_2/7_mit_sweep.c) 逐项捕获清理错误，避免失能失败后跳过发布器关闭和断开连接。

## 运行与输出

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

  <Tab title="C">
    ```bash theme={null}
    ./build/hand2/7_mit_sweep
    ```
  </Tab>
</Tabs>

**正常结束时没有任何标准输出**，退出码 0。只有出错才打印到标准错误。安静跑完两秒就是成功。

### 实测数据

在 20 关节全在线的右手 Wuji Hand 2 上实跑，同步订阅 `joint_states` 记录关节 0 的响应：

| 指令位置 | 实际位置 | 实际力矩 |
| - | - | - |
| +0.0000 | +0.0050 | −0.0030 |
| +0.0100 | +0.0046 | +0.0245 |
| +0.0200 | +0.0045 | **+0.0803** |
| +0.0100 | +0.0046 | +0.0392 |
| +0.0000 | +0.0045 | −0.0219 |

| 指标 | 实测 |
| - | - |
| 指令条数与耗时 | 101 条，2.016 秒 |
| 实际发送频率 | 49.9 Hz |
| 指令峰值 | 0.0200 rad |
| **关节实际位移范围** | **0.0004 rad** |
| 力矩变化范围 | 0.1384 |
| `enable()` 耗时 | 0.001 秒 |

**关节 0 几乎不动，这是正常的。** 力矩严格跟随指令上升与回落，说明指令已经下发、电机已按 MIT 阻抗响应。但 0.02 rad 的目标偏差在 `kp=3` 下只产生约 0.06 的力矩，克服不了关节静摩擦，实际位移只有 0.0004 rad，约合 0.023 度，肉眼不可见。

上表是手已处于零位附近时的数据。**另一次运行的对照**：手部原本弯曲时跑同一个示例，其余 19 个关节被拉向零位，实测位移如下。

| nid | 运行前 | 运行后 | 位移 |
| - | - | - | - |
| 7 | +0.5018 | −0.0018 | −0.5036 |
| 9 | +0.6070 | +0.0382 | −0.5688 |
| 19 | +0.6498 | +0.0143 | **−0.6355** |
| 22 | +0.5375 | +0.0652 | −0.4723 |

nid 19 动了 0.6355 rad，约合 36 度。所以「幅度最小」只在手已经归零时成立。

判断本示例成功的依据是**进程安静退出且退出码为 0**，以及订阅 `joint_states` 能看到力矩响应，而不是看到手指摆动。要观察真实运动请看[回放拇指对指动作](/docs/wuji-hand/latest/sdk-practice/opposition-replay)。

## 注意事项

| 分支点 | 表现 | 处理 |
| - | - | - |
| **网络上有多台设备** | 示例用 `auto_connect()`，实测抛 `Found 6 devices, please specify SN to connect`，**直接退出，不会误连** | 共用网络必须改成指定 SN，见调整与适配 |
| 在线关节不足 20 | 抛 `expected 20/20 online joints, got N/20`，不使能 | 先排查离线关节 |
| 使能失败 | 抛 `enable failed: ...`，不发任何指令 | 查故障码，必要时 `clear_fault()` |
| Ctrl+C | 退出码 130，停止后续指令，**不做回位动作** | 手停在当前位置，随后被失能 |
| 运行时异常 | 退出码 1 | 同样进入清理流程 |
| 正常结束 | 退出码 0 | 发完最后一条即失能 |

无论哪条路径，清理流程都会执行失能、关闭发布器、断开设备。异常与中断都**不会**添加恢复动作，手不会自动回到初始姿态。

## 扩展用法

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

`auto_connect()` 的语义是「连接总线上发现的唯一设备」。网络上不止一台时它拒绝连接并列出全部 SN，不会自动选择。实测报错：

```text theme={null}
Internal error: Found 6 devices, please specify SN to connect.
Available: WG1KA00XXXXXXXXX, WH2KA00XXXXXXXXX, DF1XE00XXXXXXXXX, ...
```

这个设计避免了误动他人设备，代价是共用网络里示例无法直接运行。替换第 1 步：

```python theme={null}
hand = manager.connect(sn="WH2KA00XXXXXXXXX", device_name="hand_2_mit_sweep")
```

### 想看到关节真的动

本示例不适合观察运动。要看幅度，用[回放拇指对指动作](/docs/wuji-hand/latest/sdk-practice/opposition-replay)的完整运动序列。自行加大本示例幅度会越过其安全设定，不建议。

### 读取关节状态的方式

`joint_states` 是订阅类资源，**没有 `get()` 方法**。想在发指令的同时读实际位置，要先 `subscribe_with_callback()` 持续接收，再从最新帧取值。直接调用 `hand.joint_states().get()` 会抛 `AttributeError`。

### 改变指令频率

命令间隔写死 20 ms。缩短间隔会提高指令率，但本示例的固定序列共 101 帧，缩短只是让整段更快走完，不改变轨迹形状。

## 相关内容

* [拇指对掌回放](/docs/wuji-hand/latest/sdk-practice/opposition-replay)：幅度更大的完整四指运动
* [发布控制指令](/docs/wuji-hand/latest/sdk-practice/publish-command)：`joint_command` 的通用用法
* [数据订阅：回调](/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.