# L6 灵巧手 API 开发指南

## 1. 指南目标

本文聚焦 `L6/O6` 型号在当前 SDK 中的 API 使用与二次开发方法，回答三个问题：

1. 应该使用哪一层接口
2. 哪些能力属于通用 API，哪些属于 L6 专有 API
3. 做二次封装时应如何避免常见误用

## 2. API 分层结构

L6 在当前仓库中的调用链如下：

```text
应用代码
  -> LinkerHandApi
      -> HandFactory
          -> IHand
              -> L6Hand
```

各层职责：

| 层级 | 主要文件 | 作用 |
|---|---|---|
| 业务入口层 | `include/LinkerHandApi.h` | 对外提供统一接口 |
| 实例工厂层 | `include/HandFactory.h` | 根据型号和通信方式实例化具体手型 |
| 抽象接口层 | `include/IHand.h` | 定义跨型号公共能力与异常行为 |
| 型号实现层 | `include/LinkerHandL6.h` | 暴露 L6/O6 专有接口 |

## 3. 推荐使用策略

### 3.1 优先使用 `LinkerHandApi`

适用场景：

- 快速接入 L6
- 做上层业务逻辑
- 多型号共用一套代码
- 仅使用基础动作与基础状态读取

典型接口：

- `fingerMove`
- `setSpeed`
- `setTorque`
- `getState`
- `getSpeed`
- `getTorque`
- `getTemperature`
- `getFaultCode`
- `getForce`
- `getVersion`

### 3.2 需要 L6 专有能力时直接使用 `L6Hand`

适用场景：

- 需要加速度控制
- 需要重调零
- 需要读取序列号/硬件版本/机械版本
- 需要恢复出厂设置或保存参数

## 4. L6 能力矩阵

### 4.1 通过 `LinkerHandApi` 可直接调用的 L6 能力

| 方法 | 说明 | L6 使用建议 |
|---|---|---|
| `fingerMove(const std::vector<uint8_t>&)` | 位置控制 | 数组长度固定为 `6` |
| `setSpeed(const std::vector<uint8_t>&)` | 速度设置 | 建议长度 `6` |
| `setTorque(const std::vector<uint8_t>&)` | 扭矩设置 | 建议长度 `6` |
| `getState()` | 当前位置 | 返回当前 6 关节状态 |
| `getSpeed()` | 当前速度 | 用于回读配置 |
| `getTorque()` | 当前扭矩 | 用于回读配置 |
| `getTemperature()` | 温度信息 | 适合做热保护监控 |
| `getFaultCode()` | 故障码 | 每轮任务前后都应检查 |
| `clearFaultCode()` | 清故障 | 对跨型号代码建议包装异常处理 |
| `getForce()` | 触觉/压力矩阵 | 返回三维向量 |
| `getVersion()` | 版本信息 | 用于联机确认 |

### 4.2 仅在 `L6Hand` 中暴露的能力

| 方法 | 说明 |
|---|---|
| `setAcceleration(const std::vector<uint8_t>&)` | 设置 6 关节加速度 |
| `getAcceleration()` | 读取加速度 |
| `reHome()` | 重新调零 |
| `getSerialNumber()` | 获取设备序列号 |
| `getHardwareVersion()` | 获取硬件版本 |
| `getMechanicalVersion()` | 获取机械结构版本 |
| `factoryReset()` | 恢复出厂设置 |
| `saveParameters()` | 保存参数 |

如果你的项目需要这些能力，不建议只停留在 `LinkerHandApi` 层。

## 5. 构造方式与通信选择

### 5.1 推荐构造方式

```cpp
LinkerHandApi hand(LINKER_HAND::L6, HAND_TYPE::RIGHT, COMM_CAN_0);
```

或者显式指定通道：

```cpp
LinkerHandApi hand(LINKER_HAND::L6, HAND_TYPE::RIGHT, "can0", 1000000);
```

### 5.2 工厂逻辑中的通信差异

从 `include/HandFactory.h` 当前实现可见：

- 枚举构造器 `LinkerHandApi(..., COMM_TYPE)` 会把
  - `COMM_CAN_0` 映射为 `can0`
  - `COMM_CAN_1` 映射为 `can1`
  - `COMM_ETHERCAT` 映射为 `ethercat`
- 对 L6 来说，上述三者都会走到 `L6Hand` 创建逻辑

但字符串构造器 `LinkerHandApi(..., std::string canChannel, int baudrate)` 的工厂判断是：

- `canChannel` 包含 `"can"` 时才走 L6/L7/L10/L20/L25 的 CAN 分支
- `canChannel == "modbus"` 只支持 `L10`

这意味着：

- 对 L6，`"can0"`、`"can1"` 是明确可用路径
- 如果你想走 `COMM_ETHERCAT`，优先使用枚举构造器，不要假设字符串 `"ethercat"` 在当前重载中一定可用

这是当前头文件逻辑直接能确认的限制，二次开发时应显式规避。

## 6. 基础 API 示例

### 6.1 使用 `LinkerHandApi`

```cpp
#include "LinkerHandApi.h"
#include <chrono>
#include <thread>

int main() {
    LinkerHandApi hand(LINKER_HAND::L6, HAND_TYPE::LEFT, COMM_CAN_0);

    std::vector<uint8_t> speed(6, 180);
    std::vector<uint8_t> torque(6, 220);
    std::vector<uint8_t> open_pose = {255, 255, 255, 255, 255, 255};
    std::vector<uint8_t> close_pose = {120, 160, 0, 0, 0, 0};

    hand.setSpeed(speed);
    hand.setTorque(torque);

    hand.fingerMove(open_pose);
    std::this_thread::sleep_for(std::chrono::milliseconds(600));

    hand.fingerMove(close_pose);
    std::this_thread::sleep_for(std::chrono::milliseconds(600));

    auto state = hand.getState();
    auto force = hand.getForce();
    auto fault = hand.getFaultCode();
}
```

### 6.2 使用 `L6Hand`

```cpp
#include "LinkerHandL6.h"
#include <iostream>

int main() {
    linkerhand::hand::L6Hand hand(static_cast<uint32_t>(HAND_TYPE::RIGHT), "can0", 1000000);

    hand.setSpeed(std::vector<uint8_t>(6, 150));
    hand.setTorque(std::vector<uint8_t>(6, 180));
    hand.setAcceleration(std::vector<uint8_t>(6, 90));

    std::cout << hand.getVersion() << std::endl;
    std::cout << hand.getSerialNumber() << std::endl;

    return 0;
}
```

## 7. `getForce()` 数据使用建议

`LinkerHandApi::getForce()` 返回：

```cpp
std::vector<std::vector<std::vector<uint8_t>>>
```

现有文档与示例中，通常把第一维按五指处理：

- `0`：拇指
- `1`：食指
- `2`：中指
- `3`：无名指
- `4`：小指

建议做法：

- 先打印原始结构，确认维度
- 再做语义命名和阈值封装
- 将触觉解析与动作控制拆开，避免强耦合

## 8. 异常与兼容性策略

### 8.1 未实现能力会抛异常

`IHand` 中大量默认方法会抛出 `UnsupportedFeatureException`，因此跨型号开发时建议统一做保护：

```cpp
try {
    auto value = hand.getCurrent();
} catch (const linkerhand::UnsupportedFeatureException& e) {
    std::cerr << e.what() << std::endl;
}
```

### 8.2 参数错误要在业务层先拦截

建议所有二次封装先检查：

- 位置数组长度是否为 `6`
- 速度/扭矩/加速度长度是否为 `6`
- 数值是否在 `0-255`

例如：

```cpp
void ensureL6Vector(const std::vector<uint8_t>& data) {
    if (data.size() != 6) {
        throw std::invalid_argument("L6 expects exactly 6 joint values");
    }
}
```

## 9. 二次封装建议

### 9.1 业务层包装器

建议封装一个 `L6Controller`，只暴露业务语义接口，例如：

- `openHand()`
- `prepareGrip()`
- `pinch()`
- `release()`
- `readStatus()`

不要让上层任务代码直接散落大量 `std::vector<uint8_t>` 常量。

### 9.2 型号能力分层

如果项目同时支持 `L6/L7/L10/L20`，建议分成：

- 公共接口层：只调用 `LinkerHandApi`
- 型号扩展层：只在确有必要时下钻到 `L6Hand`

这样既能复用业务逻辑，也能保留 L6 的特性能力。

### 9.3 基于当前仓库的事实约束

当前仓库中很多实现位于预编译动态库中，因此在做深度扩展时要区分两类问题：

- 头文件已经声明，但库内部行为未公开
- 头文件本身就没有声明

前者可以通过小样例验证；后者则需要新增头文件接口与实现。

## 10. 推荐阅读顺序

1. `include/LinkerHandApi.h`
2. `include/HandFactory.h`
3. `include/IHand.h`
4. `include/LinkerHandL6.h`
5. `src/main.cpp`
6. `examples/toolset_example.cpp`
7. `docs/API-Reference.md`
8. `docs/TROUBLESHOOTING.md`
