Metadata-Version: 2.4
Name: touchglove
Version: 0.1.0
Summary: Touch Glove SDK with PTP synchronization and Python native bindings
Author: ImprintX
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.20.0
Dynamic: author
Dynamic: license-file
Dynamic: requires-python

# Touch Glove SDK

Touch Glove 数据采集 SDK。本项目包含由 Rust 实现的高性能、无锁（lock-free）底层驱动库，并对外提供标准的 C API 接口。

SDK 的核心目标是接收 5 通道（5 只手套）的实时 192x192 灰度图像流，支持手套硬件 RTC 时间同步、数据包丢包检测与吞吐量统计，支持 Python 和 ROS2 调用。
本版本集成了高性能 **ONNX Runtime 推理引擎**，支持在 Rust 底层直接推理 3D 位移场与 3D 力场，并对外提供高效的物理约束标定与预测接口。

## 跨平台 Release 输出

最终发布包固定输出 **4 个系统目录**，命名统一为 `sdk_系统名`：

```text
release/
├── sdk_linux_x86/   # Linux x86_64；当前 sdk/ 就是这个版本应有的形态
├── sdk_linux_arm/   # Linux ARM64 / aarch64
├── sdk_windows/     # Windows x86_64
└── sdk_mac/         # macOS arm64 或 universal2
```

每个目录都是可独立复制使用的 SDK。`sdk_linux_x86/` 会以当前 [sdk/](sdk/) 为模板，并补充 `api/runtimes/` 目录；其他系统版本也保持同样的外形：

```text
sdk_<system>/
├── api/                       # Python API 与 PyO3 原生扩展
│   ├── __init__.py
│   ├── _rust.*
│   ├── _rust.pyi
│   └── runtimes/              # 当前系统所有可用推理包
│       ├── cpu/
│       ├── cuda/
│       ├── npu/
│       ├── coreml/
│       └── directml/
├── include/touch_glove_api.h  # C/C++ API 头文件
├── lib/                       # C ABI 原生库：.so / .dll / .dylib
├── model/                     # ONNX / OpenVINO 模型
├── example.py
├── dense_dashboard.py
├── README.md
└── LICENSE
```

运行时选择通过环境变量控制：

```bash
# 自动选择当前系统可用 runtime，失败时回退 CPU
export TOUCH_GLOVE_RUNTIME=auto

# 指定 CUDA / NPU(OpenVINO) / CoreML / DirectML / CPU
export TOUCH_GLOVE_RUNTIME=cuda
export TOUCH_GLOVE_RUNTIME=npu
export TOUCH_GLOVE_RUNTIME=coreml
export TOUCH_GLOVE_RUNTIME=directml
export TOUCH_GLOVE_RUNTIME=cpu

# 如 runtime 库放在自定义目录，可覆盖搜索路径
export TOUCH_GLOVE_RUNTIME_DIR=/path/to/onnxruntime/libs
```

本机系统可以直接构建；**非本系统的 release 文件夹统一通过 Docker 打包**。Docker 打包入口见 [scripts/package_with_docker.py](scripts/package_with_docker.py)，单系统构建入口见 [scripts/build_release.py](scripts/build_release.py)，ONNX Runtime 下载清单见 [packaging/onnxruntime.toml](packaging/onnxruntime.toml)。

## 1. 项目目录结构

整改后的 SDK 仅保留编译好的 Rust 库文件、C API 头文件和单个 Python 示例文件，删除了所有其他 `.sh` 和 `.py` 脚本：

```
.
├── lib/
│   ├── libtouch_glove.so     # 编译好的 Rust 动态链接库
│   └── libonnxruntime.so     # ONNX Runtime 动态链接库（底层推理依赖）
├── include/
│   └── touch_glove_api.h     # C/C++ 接口头文件（包含推理 API）
├── example.py                # 唯一的 Python 示例文件（包含 GUI 显示、标定、位移/力推理与标准 ROS2 图像发布）
├── README.md                 # SDK 使用及 API 说明文档
└── api/                      # Rust 底层驱动源码项目 (可在此修改并编译库)
    ├── Cargo.toml
    └── src/
```

---

## 2. 编译与构建

### 2.1 生成 `sdk_系统名` release 目录

当前 Linux x86 主机上可直接生成 `sdk_linux_x86`：

```bash
python3 scripts/build_release.py \
  --system linux_x86 \
  --runtime cpu --runtime cuda --runtime npu \
  --copy-existing-linux-x86-runtimes \
  --zip
```

非本系统的包通过 Docker 打包。一次性生成四个 release 目录：

```bash
python3 scripts/package_with_docker.py \
  --system linux_x86 \
  --system linux_arm \
  --system windows \
  --system mac \
  --zip
```

如果 Windows/macOS 原生二进制还没有挂载到 `api/target/<target>/release/` 或通过参数传入，可先生成目录骨架：

```bash
python3 scripts/package_with_docker.py --system windows --system mac --package-only --allow-missing-artifacts --zip
```

单系统构建入口：

```bash
# Linux ARM64
python3 scripts/build_release.py --system linux_arm --runtime cpu --runtime npu --zip

# Windows x86_64
python scripts/build_release.py --system windows --runtime cpu --runtime cuda --runtime npu --runtime directml --zip

# macOS
python3 scripts/build_release.py --system mac --runtime cpu --runtime coreml --zip
```

构建完成后会生成：

```text
release/sdk_linux_x86
release/sdk_linux_arm
release/sdk_windows
release/sdk_mac
```

并在指定 `--zip` 时生成对应的 `sdk_linux_x86.zip`、`sdk_linux_arm.zip`、`sdk_windows.zip`、`sdk_mac.zip`。

### 2.2 编译 Rust 底层库
在系统安装好 Rust (cargo) 之后，运行：
```bash
cd api
cargo build --release
```
编译生成的库位于 `api/target/release/libtouch_glove.so`。将其拷贝到 `lib/` 目录下即可。

---

## 3. 示例程序 `example.py` 的使用

`example.py` 是本项目**唯一的** Python 脚本，它整合了：
1. **Python 调用接口**：通过 `ctypes` 直接加载并调用编译好的 Rust 库 `libtouch_glove.so`。
2. **GUI 图像显示**：使用 OpenCV 将 5 个通道的 192x192 灰度图拼接为 3x2 网格，提供实时高帧率图像界面。
3. **Pcap/Tare 标定**：当模型加载时，按下 OpenCV 窗口中的 **`[B]`** 键可触发 1 秒图像基准值均值采集，并在 Rust 底层自动计算位移场和力场的零点偏移量。
4. **实时物理推理**：将当前 5 通道的原始图像流传给 Rust 底层进行推理，高频输出各通道的位移和力预测（带物理约束，如压应力约束、非接触零点噪声抑制等）。
5. **ROS2 调用接口**：在当前终端 sourcing 了 ROS2 环境时，自动检测并初始化 ROS2 Node，除了发布原始图像外，若开启了模型推理，还会将 5 个通道的 3D 位移场与 3D 力场以标准的 `32FC3`（32位浮点型，3通道）图像格式发布至 `/touch_glove/ch{0..4}_disp` 和 `/touch_glove/ch{0..4}_force` 话题。

### 3.1 运行依赖
运行前请安装依赖（如需 GUI 请确保安装了 `opencv-python`，如需 ROS2 请加载 ROS2 Humble）：
```bash
pip3 install numpy opencv-python
```

### 3.2 运行示例
可以直接在终端启动：
```bash
# 仅查看原始 2D 灰度图（不加载推理模型）
python3 example.py

# 加载多任务 ONNX 模型，并指定手套串口（如 /dev/ttyACM0）
python3 example.py /dev/ttyACM0 --model /path/to/best.onnx
```
启动后，程序会自动：
* 侦测并列出系统中所有的可用串口供您选择（如未指定串口参数）。
* 建立连接并与手套硬件执行握手（停止流、清空缓存、获取手套 UID、同步主机系统时间至硬件 RTC）。
* 如果指定了 `--model`，则会在 OpenCV 窗口左侧空栏提示“**Press 'b' to Calibrate**”。按下键盘 **`b`** 键静置 1 秒即完成标定，终端开始实时输出 Ch 1 的最大位移 (mm)、最大力 (N) 以及合力矢量 (N)。
* 若检测到 ROS2 环境，还会同时作为 ROS2 Publisher 广播对应的话题数据（包括 `ch0-ch4` 的原始图像、位移场和力场）。

---

## 4. SDK C API 文档

底层库通过标准 C 链接（`extern "C"`）导出了如下核心接口，用户可以使用任何支持 C 调用约定的语言自行加载和调用。

### 4.1 数据结构

#### 图像帧结构体 (`ts_frame_t`)
```c
typedef struct ts_frame_t {
    uint8_t addr;                  // 设备地址 (默认 0x01)
    uint8_t type;                  // 帧类型 (0x41)
    uint8_t cmd;                   // 命令/通道号 (0x11-0x15 代表通道 0-4 的图像数据)
    uint16_t param_len;            // 参数长度 (图像为 36864 字节)
    uint32_t seq_id;               // 序列号 (用于检测丢包)
    uint64_t timestamp_us;         // 硬件接收时间戳 (Unix epoch, 微秒)
    uint8_t sensor_id[8];          // 该通道传感器唯一的 8 字节 UID
    uint8_t param[36864];          // 192x192 灰度图像的原始字节数据
} ts_frame_t;
```

### 4.2 核心函数

#### 实例化与销毁
* **`touch_glove_handle_t touch_glove_create(void)`**
  创建并初始化一个 SDK 实例上下文句柄。
* **`void touch_glove_destroy(touch_glove_handle_t h)`**
  断开串口连接并销毁实例释放内存。

#### 串口控制与流控制
* **`int touch_glove_open(touch_glove_handle_t h, const char* port_name)`**
  打开指定串口。成功返回 `0`，失败返回 `-1`。
* **`void touch_glove_close(touch_glove_handle_t h)`**
  关闭串口连接。
* **`int touch_glove_send_start(touch_glove_handle_t h)`**
  发送启动数据流指令。
* **`int touch_glove_send_stop(touch_glove_handle_t h)`**
  发送停止数据流指令。
* **`void touch_glove_poll_read(touch_glove_handle_t h)`**
  非阻塞轮询串口接收缓存，自动解析并将完整的数据帧分配到各通道的环形环缓冲区队列中。

#### 物理推理引擎 API (模型加载、去皮标定与位移/力预测)
* **`int touch_glove_init_model(touch_glove_handle_t h, const char* model_path)`**  
  加载指定的 dense 多任务 ONNX 模型并初始化底层的推理引擎。成功返回 `0`，失败返回 `-1`。
* **`void touch_glove_set_baseline(touch_glove_handle_t h, uint8_t channel, const uint8_t* image_data)`**  
  为指定通道设定 192x192 的背景基准图像（用于去皮和差分计算）。
* **`int touch_glove_calibrate(touch_glove_handle_t h)`**  
  使用当前的各通道基准图像进行一次推理，自动生成力与位移的零点 tare 偏置项。成功返回 `0`，失败返回 `-1`。
* **`int touch_glove_infer(touch_glove_handle_t h, const uint8_t* last_images, float* out_disp, float* out_force)`**  
  传入 5 个通道当前的原始图像字节（大小为 `5 * 192 * 192 = 184320` 字节），推理计算出其 3D 位移场与 3D 力场。  
  * `out_disp` 和 `out_force` 为预先分配的 float32 数组，大小均为 `5 * 32 * 32 * 3 = 15360` 个元素。
  * 输出排布格式为 `[Channel 0..4][Y 0..31][X 0..31][C 0..2 (dx, dy, dz 或 fx, fy, fz)]`。
  * 成功返回 `0`，失败返回 `-1`。

---

## 5. 高级集成：如何调用 SDK

### 5.1 Python 推理调用示例

除了直接运行 `example.py` 之外，您可以在您自己的 Python 代码中通过 `ctypes` 极简地调用推理能力：

```python
import ctypes
import numpy as np
import os

# 1. 设置 ONNX runtime 动态库寻找路径并加载底层驱动
os.environ["ORT_DYLIB_PATH"] = os.path.abspath("./lib/libonnxruntime.so")
lib = ctypes.CDLL("./lib/libtouch_glove.so")

# 2. 声明 API 签名
lib.touch_glove_create.restype = ctypes.c_void_p
lib.touch_glove_init_model.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
lib.touch_glove_set_baseline.argtypes = [ctypes.c_void_p, ctypes.c_uint8, ctypes.POINTER(ctypes.c_uint8)]
lib.touch_glove_calibrate.restype = ctypes.c_int
lib.touch_glove_infer.argtypes = [
    ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), 
    ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float)
]

# 3. 创建句柄与加载模型
handle = lib.touch_glove_create()
lib.touch_glove_init_model(handle, b"./best.onnx")

# 4. 采集一帧作为背景基准并设定 (这里以 channel 0 为例)
baseline_img = np.zeros((192, 192), dtype=np.uint8) # 实际中从手套中 poll 得到
lib.touch_glove_set_baseline(handle, 0, baseline_img.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)))

# 5. 去皮校准
lib.touch_glove_calibrate(handle)

# 6. 推理预测
current_images = np.zeros((5, 192, 192), dtype=np.uint8) # 5通道当前原始图像
out_disp = (ctypes.c_float * 15360)()
out_force = (ctypes.c_float * 15360)()

# 调用推理
if lib.touch_glove_infer(handle, current_images.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)), out_disp, out_force) == 0:
    disp_array = np.ctypeslib.as_array(out_disp).reshape(5, 32, 32, 3)
    force_array = np.ctypeslib.as_array(out_force).reshape(5, 32, 32, 3)
    print("Channel 1 max force:", np.linalg.norm(force_array[1], axis=-1).max())
```

### 5.2 ROS2 C++ 调用示例

在 ROS2 节点中，可以通过 C++ 接口非常高频地推理并发布 PointCloud2 或 Image 消息。核心逻辑片段如下：

```cpp
#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/image.hpp>
#include "touch_glove_api.h"

class TouchGloveInferenceNode : public rclcpp::Node {
public:
    TouchGloveInferenceNode() : Node("touch_glove_inference_node") {
        disp_pub_ = this->create_publisher<sensor_msgs::msg::Image>("/touch_glove/ch1_disp", 10);
        
        // 1. 初始化
        h_ = touch_glove_create();
        touch_glove_open(h_, "/dev/ttyACM0");
        touch_glove_init_model(h_, "./best.onnx");
        
        // 2. 假设已经通过 touch_glove_set_baseline 和 touch_glove_calibrate 完成校准
        
        touch_glove_send_start(h_);
        
        // 3. 定时推理并发布 (30Hz)
        timer_ = this->create_wall_timer(
            std::chrono::milliseconds(33),
            std::bind(&TouchGloveInferenceNode::infer_and_publish, this)
        );
    }
    
private:
    void infer_and_publish() {
        touch_glove_poll_read(h_);
        
        // 获取当前 5 通道的原始图像拼接缓冲区
        uint8_t last_images[5 * 192 * 192];
        // 填充 last_images（从 peek 队列头取最新的帧填入）
        
        // 预分配预测输出数组 (5 * 32 * 32 * 3 = 15360)
        float out_disp[15360];
        float out_force[15360];
        
        if (touch_glove_infer(h_, last_images, out_disp, out_force) == 0) {
            auto msg = sensor_msgs::msg::Image();
            msg.header.stamp = this->now();
            msg.header.frame_id = "glove_ch1_disp";
            msg.height = 32;
            msg.width = 32;
            msg.encoding = "32FC3";  // 3通道 float32 图像格式
            msg.step = 32 * 3 * sizeof(float);
            
            // 提取通道 1 的位移数据 (偏移量为 1 * 32 * 32 * 3 * 4 字节)
            float* ch1_disp_start = out_disp + (1 * 32 * 32 * 3);
            uint8_t* raw_data_ptr = reinterpret_cast<uint8_t*>(ch1_disp_start);
            msg.data.assign(raw_data_ptr, raw_data_ptr + (32 * 32 * 3 * sizeof(float)));
            
            disp_pub_->publish(msg);
        }
    }

    touch_glove_handle_t h_;
    rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr disp_pub_;
    rclcpp::TimerBase::SharedPtr timer_;
};
```

---

## 6. 高级主题

### 6.1 macOS 平台加速与模型说明
在 macOS 平台上运行 `example.py` 开启推理模式时：
* SDK 会自动侦测系统架构并启用 `mac` 推理模式。
* 内部使用 `CoreMLInferenceEngine` 实现，优先利用 ONNX Runtime 的 `CoreMLExecutionProvider` 对 Apple Silicon 芯片（M1/M2/M3 等）进行硬件加速推理，如加速不可用，则自动降级回退至 `CPUExecutionProvider`。
* 默认推荐使用 **FP32** 的 ONNX 模型。如果模型是加密格式（`.enc`），引擎在加载时会自动在内存中执行 AES-GCM 解密，全程无需将解密模型写入磁盘，保护知识产权的同时保证加载效率。

### 6.2 一对手套（多路/10通道）接入方案
由于底层的 C/Rust 驱动采用**面向实例（Context Handle）**的设计模式，每个手套句柄独立封装了串口、数据队列、标定参数与推理资源，无全局静态变量冲突，原生支持同时连接多只手套。开发接入可选择以下三种方式：

#### A. 多进程部署（ROS2 架构下推荐）
在机器人多机或手部协同场景中，为每只手套启动一个独立的 Python 节点实例，配置不同的串口号，并通过 ROS2 的 `namespace` 或 `topic` 前缀进行区分：
```bash
# 启动左手节点，数据发布在 /left_glove/ch{0..4}_disp 话题
python3 example.py /dev/ttyACM0 --mode inference --model ./model/dense_3ch.onnx --ros-namespace left_glove

# 启动右手节点，数据发布在 /right_glove/ch{0..4}_disp 话题
python3 example.py /dev/ttyACM1 --mode inference --model ./model/dense_3ch.onnx --ros-namespace right_glove
```
此种方式下，单侧手套拔插或故障不影响另一侧，高并发和容错性能最佳。

#### B. 单进程多句柄调用
在普通 Python 代码中，可通过创建多个 Context 实例实现单进程聚合多只手套的数据：
```python
# 初始化左/右手套句柄
handle_left = lib.touch_glove_create()
handle_right = lib.touch_glove_create()

# 分别打开对应的串口并开启流
lib.touch_glove_open(handle_left, b"/dev/ttyACM0")
lib.touch_glove_open(handle_right, b"/dev/ttyACM1")
lib.touch_glove_send_start(handle_left)
lib.touch_glove_send_start(handle_right)

# 轮询读取与数据获取
try:
    while True:
        lib.touch_glove_poll_read(handle_left)
        lib.touch_glove_poll_read(handle_right)
        
        # 分别拉取两个句柄的图像队列...
        # left_imgs = get_images(handle_left)
        # right_imgs = get_images(handle_right)
        time.sleep(0.001)
finally:
    lib.touch_glove_destroy(handle_left)
    lib.touch_glove_destroy(handle_right)
```

#### C. 模型 Session 共享（节省内存）
当使用 Python 侧推理引擎时，为了避免为两个手套重复加载两套模型而占用多余显存/内存，可以使用同一个推理引擎实例去分别处理两只手套的图像输入：
```python
# 初始化单个共享的推理引擎 Session
shared_engine = CoreMLInferenceEngine(model_path="./model/dense_3ch.onnx")

# 循环中将获取到的左右手图像分别送入推理
disp_left, force_left = shared_engine.infer(left_images)
disp_right, force_right = shared_engine.infer(right_images)
```
