# 瓴控 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_HTTP_HOST=0.0.0.0 MOTOR_HTTP_PORT=8000 MOTOR_API_TOKEN= ``` `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` 后再次运行。 服务默认监听 `0.0.0.0:8000`,即全部 IPv4 网络接口。本机可打开 ,其他电脑应访问 `http://<服务器真实局域网IP>:8000/docs`;`0.0.0.0` 只是监听地址,不能作为客户端目标。 当前 `MOTOR_API_TOKEN` 留空,不启用 API Key,任何能访问 TCP 8000 端口的电脑都可调用 电机接口。请只在可信局域网中使用,并用主机或网络防火墙限制可访问范围。 真实硬件上只能运行一个服务进程,不要使用 `--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 SERVER_IP="<服务器真实局域网IP>" curl -X POST "http://${SERVER_IP}:8000/v1/motor/open" # 方向切换会自动执行零输出、Stop 和 20 ms 等待 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" ``` `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`。 `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 设备。 当前配置不需要 API Key。如果以后显式设置了 `MOTOR_API_TOKEN`,请求才需携带: ```bash curl -X POST -H "X-API-Key: your-token" \ "http://${SERVER_IP}:8000/v1/motor/open" ``` ## 7. 验证边界 自动化测试只验证配置、协议编码、HTTP 状态机和持续广播逻辑,不证明电机已经运动、 方向正确、状态回包可信或急停有效。真实 MS3008 验证结果必须单独记录,并包含 电机 ID、回包、raw、脉冲时长、停止结果和 USB/CAN 错误计数。