aima_phone_catcher/README.md

252 lines
9.8 KiB
Markdown
Raw Normal View History

# 瓴控 MS3008 双电机 HTTP 控制服务
该项目使用 `uv` 管理 Python 环境,通过 `python-can` 的
`canalystii`/PyUSB 后端直连 CANalyst-II,并提供 HTTP 接口控制
广播模式下的两台瓴控 MS3008 电机。
当前现场配置为:
- CAN 波特率:`1 Mbps`
- 电机 ID:`1,2`
- 广播控制帧:标准帧 `0x280`,DLC 8
- 持续广播频率:`10 Hz`
- HTTP `open`:ID 1 = `+400` raw,ID 2 = `-400` raw
- HTTP `close`:ID 1 = `-400` raw,ID 2 = `+400` raw
- 服务启动:自动 Enable;正常退出:自动 Stop 后 Disable
- MS3008 开环 raw 范围:`-850..+850`
同时控制 ID 1、2 时,固定控制帧分别为:
- `open`:`0x280 90 01 70 FE 00 00 00 00`
- `close`:`0x280 70 FE 90 01 00 00 00 00`
> `400` 是 MS3008 的协议原始开环控制量,不是力矩,也不是 N·m。
> 正负号只表示协议方向;机械正反方向必须逐台、低值、短脉冲实测确认。
> 上述映射是方向确认后的目标值,禁止将其用于首次上电试转。
## 1. 硬件和协议边界
- 两台电机必须已用厂商上位机设为广播模式、`1 Mbps`,ID 分别为 1 和 2。
- `0x280` 的四个 16 位小端槽位依次对应 ID 1..4。本现场使用前两个槽位,
ID 3、4 槽位写 0。总线上不要同时运行另一个广播控制程序。
- MS3008 的 `0x280` 是开环控制命令,不是 MF/MG 的力矩电流命令;不得用
“力矩 400”“400 N·m”等方式描述或标定。
- 每次 HTTP Stop 会显式执行两轮“零控制量 + 协议 Stop”,间隔 `20 ms`;
每轮的 Stop 帧最多等待 `100 ms` 让 CANalyst-II TX 队列处理完。
这仍不等于电机 ACK、物理急停、STO 或断电。
- `MOTOR_HARDWARE_ENABLED=true` 时,服务启动自动发送零输出和 `0x88` Enable;
HTTP 不再提供 `enable`/`disable` 接口。
- 服务正常退出会先执行两轮 Stop,再发送 `0x80` Disable。`0x80`
会清除电机多圈计数;`SIGKILL`、进程崩溃或断电时无法保证执行退出清理。
普通暂停应调用 `stop`;`open`/`close` 换向不会 Disable。
- USB 设备打开或 `send()` 返回不代表电机已实际转动;可在 HTTP
服务停止时使用独立手动脚本读取状态回包作为可选诊断。
## 2. 用 uv 创建环境
需要 Python 3.11 或更高版本,并先安装 [uv](https://docs.astral.sh/uv/)。在本目录执行:
```bash
uv sync --locked
uv run --locked pytest
```
`uv sync` 会创建 `.venv` 并按照 `uv.lock` 安装依赖。如果项目位于不支持虚拟环境
软链接的 Samba/NAS 挂载盘,可以把环境放到本地磁盘;后续 `uv run` 也要保留
同一个变量:
```bash
export UV_PROJECT_ENVIRONMENT=/tmp/lingkong-motor-service-venv
uv sync --locked
uv run --locked pytest
```
## 3. CANalyst-II 直连后端
当前设备为 `VID:PID 04d8:0053`。它使用厂商自定义的 USB 协议,
不会自动生成 SocketCAN `canX` 接口。服务固定使用
`python-can[canalystii]`:
```dotenv
MOTOR_CAN_CHANNEL=0
MOTOR_CAN_DEVICE=0
MOTOR_CAN_BITRATE=1000000
```
通道号与面板标识的对应关系为:
- 面板 `CAN1` → `MOTOR_CAN_CHANNEL=0`
- 面板 `CAN2` → `MOTOR_CAN_CHANNEL=1`
- `MOTOR_CAN_DEVICE=0` 表示 PyUSB 枚举到的第一块 CANalyst-II,与通道号无关。
可只读检查 USB 设备:
```bash
lsusb -d 04d8:0053
lsusb -t
```
该 USB interface 不应绑定 `usbserial_generic`,也不应把强制生成的
`ttyUSB0..5` 当成可靠 CAN 通道。如果 PyUSB 报权限错误,应为
`04d8:0053` 设置范围明确的 udev 权限规则,不要以 root 长期运行 HTTP 服务。
### CAN 接线检查
断电后检查:
- 总线两端各有约 `120 Ω` 终端,CAN_H 与 CAN_L 之间实测应约为 `60 Ω`。
- USB-CAN 与电机控制侧信号地可靠共地。
- CAN_H 对 CAN_H、CAN_L 对 CAN_L,不能接反。
- 采用总线型连接并尽量缩短支线;`1 Mbps` 对布线和终端更敏感。
该后端不能可靠报告 CAN ACK、bus-off 或真实发送失败;停止时的
TX 队列等待只证明设备队列已处理,不证明电机已收到或机械已停。因此服务的
`can_connected` 只表示 USB 对象已打开。独立手动状态脚本可读取 ID 1、2 的
`0x9C` 回包作为可选诊断。
## 4. 服务配置与启动
复制配置:
```bash
cp .env.example .env
```
`.env.example` 已对应当前 MS3008 双电机现场,关键项为:
```dotenv
MOTOR_HARDWARE_ENABLED=false
MOTOR_CAN_CHANNEL=0
MOTOR_CAN_DEVICE=0
MOTOR_CAN_BITRATE=1000000
MOTOR_IDS=1,2
MOTOR_OPEN_CONTROL_RAW=400
MOTOR_CLOSE_CONTROL_RAW=400
MOTOR_BROADCAST_FREQUENCY_HZ=10
```
`MOTOR_OPEN_CONTROL_RAW` 和 `MOTOR_CLOSE_CONTROL_RAW` 是幅值;程序按电机 ID
固定方向:`open` 为 `{1: +400, 2: -400}`,`close` 为
`{1: -400, 2: +400}`。
完成接线和物理安全准备后,将 `MOTOR_HARDWARE_ENABLED` 改为 `true`。
此开关是启动安全总闸:服务启动后会立即自动 Enable,无需 HTTP 调用。
程序不会自动读取 `.env`,启动前导入:
```bash
set -a
. ./.env
set +a
uv run --locked lingkong-motor-service
```
也可直接一键启动(自动加载 `.env`、同步锁定依赖并检查端口冲突):
```bash
./script/launch.sh
```
如果 `.env` 不存在,脚本会根据 `.env.example` 创建一份,完成硬件检查并设置
`MOTOR_HARDWARE_ENABLED=true` 后再次运行。
Swagger 界面默认为 <http://127.0.0.1:8000/docs>。真实硬件上只能运行一个服务
进程,不要使用 `--reload` 或多个 worker。
## 5. 首次实机测试顺序
必须先逐台验证,不能直接以 ID `1,2` 和目标 raw 首次试转:
1. 断开负载并架空输出轴,设置电源限流,确认物理急停和断电回路有效。
2. 在 HTTP 服务未运行时,可选执行
`uv run --locked python tests/test_can.py status`,观察 ID 1、2 的 `0x9C`
状态回包作为独立诊断。运行该脚本时不得同时启动 HTTP 服务。
3. 临时设置 `MOTOR_IDS=1`,将两个方向的控制量都改为 `20`。服务启动并自动使能后只调用一次
`open`(服务内部会以 10 Hz 持续广播 ID 1 = `+20`),约 `100 ms` 时必须显式调用 `stop`;
确认完全停稳后,才以同样方式测试 `close` 的 ID 1 = `-20/100 ms`,再显式 `stop`、停稳并
最后用 `Ctrl-C` 正常退出,由服务自动 Stop 并 Disable。
4. 保持 raw `20`,把 `MOTOR_IDS` 改为 `2`,按 ID 2 `open=-20`、
`close=+20` 的预期验证第二台电机。
5. 两台的状态、方向和停机都确认后,才逐级提高控制量。每一级、每个方向之间
都必须 Stop 并确认机械完全停稳,最终映射为 `open={1:+400,2:-400}`、
`close={1:-400,2:+400}`。
6. 首次方向验证仍必须执行“当前方向 → Stop → 确认停稳 → 另一方向”。正常运行时
`open → close` 和 `close → open` 都可直接调用;服务会自动停止当前广播,发送零输出
与 Stop,等待 `20 ms` 后开始另一方向广播。该等待不代表机械轴已经停稳。
`100 ms` 是上限目标,不是普通 shell/HTTP 调度的硬实时保证。旁站人员必须一直
可以操作物理急停;必须由操作者显式调用 HTTP `stop`,且 HTTP `stop` 不能作为唯一保护。
### 手动 CAN 测试脚本
[`tests/test_can.py`](tests/test_can.py) 是一个不依赖项目其他脚本的简单手动测试程序。
它直接使用 `python-can` 的 `canalystii` 后端,默认面板 CAN1、1 Mbps、电机 ID 1和2。
运行前要停止 HTTP 服务。
读取状态2(0x9C):
```bash
uv run --locked python tests/test_can.py status
```
发送 `open` 映射(ID 1 = `+400`、ID 2 = `-400`,持续 10 Hz,
`Ctrl-C` 后自动零输出并 Stop):
```bash
uv run --locked python tests/test_can.py torque 400 -400
```
发送 `close` 映射(ID 1 = `-400`、ID 2 = `+400`,持续 10 Hz,
`Ctrl-C` 后自动零输出并 Stop):
```bash
uv run --locked python tests/test_can.py torque -400 400
```
停止 ID 1、2:
```bash
uv run --locked python tests/test_can.py stop
```
仅测试一台时,另一台的值填 `0`,例如 `torque 20 0`。MS 的该控制值是开环
`powerControl raw`,不是 N·m 力矩。
## 6. HTTP 接口
完成上述低值方向验证后,正常操作顺序如下:
```bash
curl -X POST http://127.0.0.1:8000/v1/motor/open
# 方向切换会自动执行零输出、Stop 和 20 ms 等待
curl -X POST http://127.0.0.1:8000/v1/motor/close
curl -X POST http://127.0.0.1:8000/v1/motor/open
curl -X POST http://127.0.0.1:8000/v1/motor/stop
curl http://127.0.0.1:8000/v1/motor/status
```
`MOTOR_HARDWARE_ENABLED=true` 时,启动过程会自动 Enable。HTTP 只保留
`open`、`close`、`stop` 和 `status`,不提供 `enable`/`disable`。
`open` 固定使用 `{1:+400,2:-400}`,`close` 固定使用
`{1:-400,2:+400}`;HTTP 请求不能临时提高 raw。状态响应中
`control_raw_by_motor` 显示每台电机的实际有符号控制量;保留的 `control_raw`
字段表示 ID 1(单独配置 ID 2 时表示 ID 2)。调用任一运动接口后,服务内部线程都会以
`MOTOR_BROADCAST_FREQUENCY_HZ`
(默认 10 Hz)持续发送完整 `0x280` 四槽帧,不会自动超时,HTTP 客户端断开也不会
停止。`open` 与 `close` 相互切换时会自动执行零输出与 Stop;最终仍必须显式调用
`stop`。单次 `stop` 会自动重复两轮停止序列,不需要客户端再调用两次。
服务正常退出会自动执行两轮 Stop,再 Disable 并关闭 CAN 设备。
如果设置了 `MOTOR_API_TOKEN`,请求需携带:
```bash
curl -X POST -H "X-API-Key: your-token" \
http://127.0.0.1:8000/v1/motor/open
```
## 7. 验证边界
自动化测试只验证配置、协议编码、HTTP 状态机和持续广播逻辑,不证明电机已经运动、
方向正确、状态回包可信或急停有效。真实 MS3008 验证结果必须单独记录,并包含
电机 ID、回包、raw、脉冲时长、停止结果和 USB/CAN 错误计数。