2026-08-07 09:16:59 +08:00
|
|
|
|
# 瓴控 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
|
2026-08-12 11:20:28 +08:00
|
|
|
|
- `open`/`close` 持续 `5 s` 后:保持方向,两台电机幅值降为 `100` raw
|
|
|
|
|
|
- 温度达到 `80 C`:两台电机控制量归零并发送 Stop
|
2026-08-07 09:16:59 +08:00
|
|
|
|
- 服务启动:自动 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
|
2026-08-12 11:20:28 +08:00
|
|
|
|
MOTOR_DERATE_AFTER_S=5
|
|
|
|
|
|
MOTOR_DERATED_CONTROL_RAW=100
|
|
|
|
|
|
MOTOR_TEMPERATURE_LIMIT_C=80
|
|
|
|
|
|
MOTOR_TEMPERATURE_POLL_INTERVAL_S=0.5
|
2026-08-07 09:35:48 +08:00
|
|
|
|
MOTOR_HTTP_HOST=0.0.0.0
|
|
|
|
|
|
MOTOR_HTTP_PORT=8000
|
|
|
|
|
|
MOTOR_API_TOKEN=
|
2026-08-07 09:16:59 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`MOTOR_OPEN_CONTROL_RAW` 和 `MOTOR_CLOSE_CONTROL_RAW` 是幅值;程序按电机 ID
|
|
|
|
|
|
固定方向:`open` 为 `{1: +400, 2: -400}`,`close` 为
|
|
|
|
|
|
`{1: -400, 2: +400}`。
|
|
|
|
|
|
|
2026-08-12 11:20:28 +08:00
|
|
|
|
每次调用 `open` 或 `close` 都会重新开始计时。持续 5 秒后,程序保持当前方向并把
|
|
|
|
|
|
两台电机的开环 raw 幅值降至 100,即 `open={1:+100,2:-100}`、
|
|
|
|
|
|
`close={1:-100,2:+100}`。方向切换也会重新开始 5 秒计时。
|
|
|
|
|
|
|
|
|
|
|
|
运动期间服务每 0.5 秒发送一次 `0x9C` 状态2请求并读取温度。如果任一配置电机回报
|
|
|
|
|
|
温度达到或超过 `80 C`,程序会把两台电机控制量都设为 0、发送 Stop,并锁存温度保护;
|
|
|
|
|
|
电机冷却后需要正常重启服务才能再次运动。保护依赖有效的状态2回包:如果
|
|
|
|
|
|
`temperature_c_by_motor` 为空,表示尚未收到温度,不能认为温度保护已经得到现场验证。
|
|
|
|
|
|
|
2026-08-07 09:16:59 +08:00
|
|
|
|
完成接线和物理安全准备后,将 `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` 后再次运行。
|
|
|
|
|
|
|
2026-08-07 09:35:48 +08:00
|
|
|
|
服务默认监听 `0.0.0.0:8000`,即全部 IPv4 网络接口。本机可打开
|
|
|
|
|
|
<http://127.0.0.1:8000/docs>,其他电脑应访问
|
|
|
|
|
|
`http://<服务器真实局域网IP>:8000/docs`;`0.0.0.0` 只是监听地址,不能作为客户端目标。
|
|
|
|
|
|
当前 `MOTOR_API_TOKEN` 留空,不启用 API Key,任何能访问 TCP 8000 端口的电脑都可调用
|
|
|
|
|
|
电机接口。请只在可信局域网中使用,并用主机或网络防火墙限制可访问范围。
|
|
|
|
|
|
真实硬件上只能运行一个服务进程,不要使用 `--reload` 或多个 worker。
|
2026-08-07 09:16:59 +08:00
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-08-07 09:35:48 +08:00
|
|
|
|
SERVER_IP="<服务器真实局域网IP>"
|
|
|
|
|
|
curl -X POST "http://${SERVER_IP}:8000/v1/motor/open"
|
2026-08-07 09:16:59 +08:00
|
|
|
|
# 方向切换会自动执行零输出、Stop 和 20 ms 等待
|
2026-08-07 09:35:48 +08:00
|
|
|
|
curl -X POST "http://${SERVER_IP}:8000/v1/motor/close"
|
|
|
|
|
|
curl -X POST "http://${SERVER_IP}:8000/v1/motor/open"
|
|
|
|
|
|
curl -X POST "http://${SERVER_IP}:8000/v1/motor/stop"
|
|
|
|
|
|
curl "http://${SERVER_IP}:8000/v1/motor/status"
|
2026-08-07 09:16:59 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-07 09:59:04 +08:00
|
|
|
|
`open`、`close`、`stop` 成功时均返回 HTTP `200 OK`,响应 JSON 包含
|
|
|
|
|
|
`"code": 200`、执行动作和当前电机状态。例如:
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"code": 200,
|
|
|
|
|
|
"accepted": true,
|
|
|
|
|
|
"action": "stop",
|
|
|
|
|
|
"state": "stopped"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
实际响应还包含电机 ID、控制量、广播状态等字段。硬件未启用或状态冲突仍返回
|
|
|
|
|
|
HTTP `409`,CAN/USB 通讯失败仍返回 HTTP `503`。
|
|
|
|
|
|
|
2026-08-12 11:20:28 +08:00
|
|
|
|
状态响应中的 `derated` 表示是否已进入 5 秒后的低功耗阶段,
|
|
|
|
|
|
`temperature_c_by_motor` 是各电机最近一次状态2温度,`temperature_limited` 表示
|
|
|
|
|
|
80 C 归零保护是否已触发。
|
|
|
|
|
|
|
2026-08-07 09:16:59 +08:00
|
|
|
|
`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 设备。
|
|
|
|
|
|
|
2026-08-07 09:35:48 +08:00
|
|
|
|
当前配置不需要 API Key。如果以后显式设置了 `MOTOR_API_TOKEN`,请求才需携带:
|
2026-08-07 09:16:59 +08:00
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
curl -X POST -H "X-API-Key: your-token" \
|
2026-08-07 09:35:48 +08:00
|
|
|
|
"http://${SERVER_IP}:8000/v1/motor/open"
|
2026-08-07 09:16:59 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 验证边界
|
|
|
|
|
|
|
|
|
|
|
|
自动化测试只验证配置、协议编码、HTTP 状态机和持续广播逻辑,不证明电机已经运动、
|
|
|
|
|
|
方向正确、状态回包可信或急停有效。真实 MS3008 验证结果必须单独记录,并包含
|
|
|
|
|
|
电机 ID、回包、raw、脉冲时长、停止结果和 USB/CAN 错误计数。
|