反向实现设备 WebSocket 控制协议,提供单次命令与常驻长连接两种模式。
核心内容:
- robot-ctl.py 单次命令客户端,纯标准库无依赖
- robot-daemon.py 常驻守护进程,维持唯一长连接,
解决断开丢尾帧导致的速度残留
- rc.sh 向守护进程注入动作,内置 stable=1 硬锁
- docs/PROTOCOL.md 协议逆向与踩坑记录
- tests/fake-robot-ws.js 离线验证用假设备
实测标定 (50% 速度):
- 前进/后退 ~8 cm/s
- 左转/右转 ~35 deg/s
已知限制:
- 设备为 Arduino WebSocketsServer,纯单向,不回传状态
- 仅摇杆与跳跃有效,linear/angular/方向键无效
- 网页与脚本不可同时连接,会互相覆盖帧
5.2 KiB
协议逆向笔记
记录 WL-PRO 轮组机器人的通信协议,以及踩过的坑。内容全部来自实测,不是推测。
设备识别
WebSocket 握手响应头泄露了固件身份:
HTTP/1.1 101 Switching Protocols
Server: arduino-WebSocketsServer
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Accept: ...
结论:固件基于 Arduino WebSocketsServer 库(ESP32 / Arduino 平台)。
这个库的关键特性:纯单向。
- 握手完成后不发送任何数据
- 收到客户端帧后不返回任何 ack 或状态
- 因此永远无法确认动作是否执行,只能靠肉眼观察
端点
| 用途 | 地址 |
|---|---|
| 遥控页 | http://<device>/ |
| 控制通道 | ws://<device>:81/ |
| WiFi 设置 | http://<device>/wifi |
控制通道固定 81 端口,路径为 /。
帧格式
完整的 JSON 文本帧,每次发送必须包含全部字段:
{
"dir": "stop",
"mode": "basic",
"roll": 0,
"height": 38,
"linear": 0,
"angular": 0,
"stable": 1,
"joy_x": 0,
"joy_y": 50
}
字段详解
| 字段 | 范围 | 有效 | 说明 |
|---|---|---|---|
joy_y |
-100 ~ 100 | ✅ | 纵向摇杆。正值前进,负值后退 |
joy_x |
-100 ~ 100 | ✅ | 横向摇杆。正值右转,负值左转 |
dir |
字符串 | ✅ 仅 jump |
jump 触发跳跃;其余值(含 forward/back/left/right)无效 |
stable |
0 / 1 | ✅ | 主动保持平衡开关。1 = 开启 |
height |
32 ~ 85 | ✅ | 站高,单位 mm |
roll |
-30 ~ 30 | ❌ | 滑块存在但设备侧无响应 |
linear |
-200 ~ 200 | ❌ | 单位 mm/s,设备侧无响应 |
angular |
-100 ~ 100 | ❌ | 单位 °/s,设备侧无响应 |
mode |
字符串 | — | 固定 basic |
只有摇杆(joy_x/joy_y)、跳跃(dir:"jump")、stable、height 真正生效。
roll / linear / angular 三个滑块在网页上有 UI,但拖动了机器人没反应——用户确认过,不要浪费时间试。
为什么必须常驻连接
这是本项目最重要的一条经验。
现象
早期实现是「每次命令新建连接 → 推几百毫秒帧 → 断开」。结果:
17:32 执行 joy_y=-60 持续 600ms,命令已退出
17:36 用户报告「还在退」
命令结束四分钟后,机器人还在移动。
原因
设备对短连接的处理不可靠:客户端推完动作帧后立刻发 WebSocket close 帧,归零帧可能随连接关闭一起丢失,设备保留最后一次收到的速度值。
设备是纯单向的,不会告诉我们它记住了什么。
解法
保持单条长连接永不主动断开:
- 空闲时每 120ms 推一帧全零(
dir:"stop",joy:0,0),与网页空闲时的行为一致 - 动作通过状态文件注入,带到期时间,到期自动归零
- 断开前先补 12 帧零,再发 close
robot-daemon.py 实现的就是这个模式。
网页抢帧冲突
网页里这个定时器是隐形杀手:
setInterval(function () {
send_data(); // dir 恒为 "stop",joy_x/joy_y 取摇杆当前位置
}, 150);
不动摇杆时摇杆自动回中,所以它每 150ms 发一帧 joy=0,0。
如果网页和脚本同时连着设备,两个独立 WS 客户端的帧会交错:
脚本: joy_y = -60
网页: joy_y = 0 ← 150ms 后
脚本: joy_y = -60
网页: joy_y = 0
设备收到的是来回跳变的矛盾指令,表现就是"几乎没动"或原地抖动。
结论:操作前必须关掉网页标签页(关闭,不是最小化)。
同理,网页的方向键是 mousedown 发一帧 dir、mouseup 发一帧 stop,中间还有 150ms 定时器插一帧 stop——实际效果是 ≤150ms 的瞬间点动,不是持续移动。
早期我把 dir:"forward" 连推 1 秒(7 帧),等于连下 7 次动作指令不给停止,姿态控制被打乱,机器人直接摔倒。
非线性响应
速度与 joy 值不是线性的:
joy 绝对值 |
实测表现 |
|---|---|
| 20 | 几乎不动 |
| 30 | 前进 3 秒变化很小 |
| 50 | ≈ 8 cm/s(可用基准) |
| 80 | 明显更快 |
低于约 50% 推进力不足。标定表只在 50% 附近有效,换速度需重新标定。
远程访问
设备通常藏在局域网里。远程控制需要一条端口隧道:
- frpc 客户端在局域网机器上
- 代理类型必须选 tcp(
http/https是 frps 的虚拟主机模式,需要vhostHTTPPort,通常没开) localIP必须填设备真实 IP,不能填127.0.0.1(那是客户端自己)localPort= 81,remotePort= 云服务器上已放行的端口
云服务器安全组必须放行 remotePort。被 DROP 的端口表现为连接超时(不是拒绝),很容易误判成网络问题。
调试技巧
判断隧道是否真的打通,看 frps 的出向流量:
in=579B / out=0B ← 后端没收到,或连错了对象
in=2155B / out=964B ← 通了
in 是我们发出的字节,out 是设备返回的字节。纯单向设备握手后不再说话,但握手响应本身会让 out 增长。out=0 说明数据根本没到达设备。