Metadata-Version: 2.1
Name: spi-proto
Version: 0.1.0
Summary: Jetson SPI 电机控制协议库（libspi_proto.so）的 Python 绑定，内置共享库，pip install 后即可使用。
Home-page: UNKNOWN
License: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: POSIX :: Linux
Classifier: Topic :: Software Development :: Embedded Systems
Classifier: Topic :: System :: Hardware
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# spi-proto：SPI 电机控制库的 Python 包

把 C++ 工程编译出的 `libspi_proto.so`（aarch64 / Jetson）封装为可直接
`pip install` 的 Python 库。包内自带共享库，安装后无需再手动管理 `.so`
路径，所有硬件命令、协议造帧/解析、CRC、浮点转换都有对应的 Python API。

对应 C 接口见 `../c_api.h`，协议细节见 `../通讯协议.md`。

## 平台要求

- Linux aarch64（Jetson 等），因为 `libspi_proto.so` 是 aarch64 ELF；
- Python 3.8+；
- 硬件命令需要 `/dev/spidev*` 节点（纯协议接口 / `selftest` 不需要硬件）。

在其他架构上安装会正常完成，但导入时会抛出明确的架构错误提示。

## 安装

### 方式一：从源码目录直接安装

```sh
cd python
pip install .
```

### 方式二：构建 wheel 后安装（推荐分发给其他 Jetson）

```sh
cd python
bash build_wheel.sh
pip install dist/spi_proto-0.1.0-py3-none-linux_aarch64.whl
```

`build_wheel.sh` 会同时生成 sdist 和平台 wheel。wheel 的平台标签必须是
`linux_aarch64`（`.so` 是 aarch64 的 ELF，不能当作 `py3-none-any`
跨平台包发布）。

### 方式三：直接安装 sdist

```sh
pip install python/dist/spi_proto-0.1.0.tar.gz
```

### 验证安装

```sh
python3 -c "import spi_proto; print(spi_proto.__version__)"
spi-demo selftest        # 无硬件自检，全部 [PASS] 即正常
```

## 快速开始

安装后自带 `spi-demo` 命令行（也可 `python -m spi_proto.demo`，源码目录
下还可以直接 `python3 demo_python.py`）：

```sh
spi-demo selftest                    # 不访问硬件，自检协议
spi-demo enable                      # 0x01 使能（默认全选）
spi-demo enable --motor-id 1 2       # 只使能电机 1、2
spi-demo disable --motor-id 3        # 只失能电机 3
spi-demo zero                        # 0x04 设置零位（默认全选）
spi-demo zero --motor-id 3 --motor-id 7
spi-demo setmode --mode 0            # 全部电机设为 MIT 模式
spi-demo pos --motor 1 0.5 0 0       # 控制电机 1：pos=0.5, vel=0, tor=0
spi-demo pos --motor 1 0.5 0 0 --motor 9 -0.2 0 0 --loop 10
spi-demo test                        # 0x0F 回环测试（调试）
```

硬件命令默认打开 `/dev/spidev0.0`（不存在时自动选择第一个
`/dev/spidev*`），20 MHz，SPI 模式 0，可用 `--device` / `--speed` 覆盖。

## Python API 示例

### 硬件控制

```python
from spi_proto import SpiClient

with SpiClient() as client:            # 默认 /dev/spidev0.0, 20MHz, mode 0
    fb = client.enable()               # 整机使能，返回 Feedback
    fb = client.enable_motor(1)        # 只使能电机 1
    fb = client.disable()              # 整机失能
    fb = client.set_zero()             # 全选标零
    fb = client.set_zero_motor(3)      # 只标零电机 3
    fb = client.set_mode(0)            # 全部电机 MIT 模式（0/1/2）
    fb = client.set_mode([1, 2] + [0] * 30)   # 按电机分别设置
    fb = client.position([0.5] * 16, [0.0] * 16, [0.0] * 16)  # 全 16 路
    fb = client.position_ids([1, 9], [0.5, -0.2], [0.0, 0.0], [0.0, 0.0])
    fb = client.stop()                 # 0x03 读电机状态

    print(fb.valid, fb.crc_ok, fb.cmd)          # True True 0x82
    for m in fb.motors:
        print(m.id, m.pos, m.vel, m.tor)        # 位置 rad / 速度 rad/s / 力矩 Nm
```

所有硬件方法成功返回 `Feedback`，失败抛 `SpiError`（可 `client.last_error()`
查看 C 层错误信息）。

### 纯协议（不访问硬件）

```python
from spi_proto import (
    crc16, frame_enable, frame_position_ids, parse_feedback,
)

frame = frame_enable(0xFFFFFFFF)       # bytes，长度 255
frame = frame_position_ids([1], [0.5], [0.0], [0.0])

assert crc16(b"123456789") == 0x4B37

fb = parse_feedback(rx_bytes)          # rx_bytes 必须是 255 字节
```

## API 参考

### 常量

| 常量 | 值 | 说明 |
| --- | --- | --- |
| `MOTOR_NUM` | 16 | 全 16 路控制时的电机数 |
| `MODE_MOTOR_NUM` | 32 | 设置模式帧的槽位数 |
| `MOTOR_ID_MAX` | 32 | 位图类命令的电机 ID 上限 |
| `FRAME_LEN` | 255 | 帧长度 |
| `MASK_ALL` | `0xFFFFFFFF` | 使能/失能/标零/读状态全选位图 |
| `MODE_MIT` / `MODE_POSITION` / `MODE_VELOCITY` | 0 / 1 / 2 | set_mode 取值 |

### `SpiClient`

构造参数：`device=None, speed_hz=20_000_000, mode=0, bits_per_word=8`。
支持 `with` 语句，`close()` / `closed`。

| 方法 | 命令 | 说明 |
| --- | --- | --- |
| `enable(on=True)` | 0x01 | 使能，默认全选 |
| `disable()` | 0x00 | 失能，默认全选 |
| `enable_mask(mask)` / `disable_mask(mask)` | 0x01/0x00 | 按 32 位位图 |
| `enable_motor(id)` / `disable_motor(id)` | 0x01/0x00 | 单电机（1..32） |
| `stop()` / `stop_mask(mask)` | 0x03 | 读电机状态 |
| `set_zero(mask=0xFFFFFFFF)` / `set_zero_motor(id)` | 0x04 | 标零 |
| `set_mode(modes)` | 0x05 | int 或 32 字节序列 |
| `loopback(byte=0x5A)` | 0x0F | 回环测试 |
| `position(pos, vel, tor)` | 0x02 | 全 16 路，数组长度 16，可传 None |
| `position_ids(ids, pos, vel, tor)` | 0x02 | 变长控制，ids 1..16 个 |
| `last_error()` | - | C 层最近错误信息 |

### 协议函数

`crc16(data)`、`float_to_uint(x, min, max, bits)`、
`uint_to_float(x, min, max, bits)`、`parse_feedback(frame)`，以及返回
`bytes`（255 字节）的帧构造：`frame_enable` / `frame_disable` /
`frame_stop` / `frame_set_zero` / `frame_set_mode` / `frame_test` /
`frame_position` / `frame_position_ids`。

### 数据结构

- `Feedback`：`valid`、`crc_ok`、`cmd`、`raw`（bytes）、`motors`
- `MotorFeedback`：`id`、`state`、`pos`、`vel`、`tor`、`mos_temp`、`coil_temp`

## 协议要点

- 使能/失能/读状态/标零：32 位位图写入 `frame[4..7]`（小端），
  `bit(id-1)=1` 对应电机 id（1..32）；
- 设置模式（0x05）：32 字节，每电机一字节（0=MIT、1=位置、2=速度）；
- 运动控制（0x02 / 回传 0x82）为变长帧，长度字节 = 数据长度（9×N）；
- CRC-16/MODBUS 覆盖“长度+命令+数据”，紧随数据存放。

## 注意事项

- SPI 设备节点可能需要 root 或 dialout 组权限，报 `open 失败` 时先检查
  `ls -l /dev/spidev*` 与权限；
- `libspi_proto.so` 是 aarch64 的，wheel 只能装在 Linux aarch64 上；
- MCU 0x0F 回环存在固件特性：回传帧携带上一拍状态、回环字节写在 CRC
  计算之后。`spi-demo test` 已兼容（连发两拍 + 按 `raw[5]=0` 复核 CRC），
  普通命令（stop 等）回传帧 CRC 正常；
- 纯协议接口和 `selftest` 不需要硬件，可在任意 aarch64 机器上跑。

## 打包与分发

```sh
bash build_wheel.sh
```

产物在 `dist/`：

- `spi_proto-0.1.0-py3-none-linux_aarch64.whl`：把 `.so` 打进 wheel，
  目标 Jetson 上 `pip install <wheel>` 即可；
- `spi_proto-0.1.0.tar.gz`：sdist，目标机器上 `pip install <tar.gz>`。

包内模块：`spi_proto`（API 入口）、`spi_proto.client`（SpiClient）、
`spi_proto.protocol`（造帧/解析/CRC）、`spi_proto.selftest`、
`spi_proto.demo`（CLI demo）。


