Files
wlpro-robot-ctl/docs/PROTOCOL.md
T
deepseek Bot 6dd1fa7e24 release: v1.0 - WL-PRO 轮组机器人遥控脚本
反向实现设备 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/方向键无效
- 网页与脚本不可同时连接,会互相覆盖帧
2026-10-25 17:53:11 +08:00

5.2 KiB
Raw Blame History

协议逆向笔记

记录 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 说明数据根本没到达设备。