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/方向键无效
- 网页与脚本不可同时连接,会互相覆盖帧
This commit is contained in:
commit
6dd1fa7e24
11 files changed
+1971
No files matched your search
@@ -0,0 +1,181 @@
|
||||
# 协议逆向笔记
|
||||
|
||||
记录 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 文本帧,每次发送必须包含全部字段:
|
||||
|
||||
```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` 实现的就是这个模式。
|
||||
|
||||
---
|
||||
|
||||
## 网页抢帧冲突
|
||||
|
||||
网页里这个定时器是隐形杀手:
|
||||
|
||||
```js
|
||||
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` 说明数据根本没到达设备。
|
||||
Reference in new issue
Block a user