反向实现设备 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/方向键无效
- 网页与脚本不可同时连接,会互相覆盖帧
94 lines
3.0 KiB
Bash
Executable File
94 lines
3.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# 通过常驻守护进程注入动作(只写状态文件,不自己开连接)
|
|
#
|
|
# 前提:robot-daemon.py 正在运行,保持唯一长连接。
|
|
#
|
|
# 用法:
|
|
# bash rc.sh go -40 800 # 后退 40% 速度 0.8 秒
|
|
# bash rc.sh go 40 800 # 前进
|
|
# bash rc.sh strafe 40 800 # 右转(负值左转)
|
|
# bash rc.sh joy 30 -40 800 # 任意摇杆组合
|
|
# bash rc.sh jump # 跳一下
|
|
# bash rc.sh stop # 立即归零
|
|
# bash rc.sh status # 看当前注入状态 + 守护进程是否活着
|
|
# bash rc.sh hold # 持续注入零帧(确保静止,默认 5 秒)
|
|
|
|
set -u
|
|
D="$(cd "$(dirname "$0")" && pwd)"
|
|
STATE="${RC_STATE:-$D/daemon-state.json}"
|
|
PIDF="$D/daemon.pid"
|
|
|
|
die() { echo "[rc] $*" >&2; exit 1; }
|
|
|
|
alive() {
|
|
[ -f "$PIDF" ] || return 1
|
|
local pid; pid="$(cat "$PIDF" 2>/dev/null || true)"
|
|
[ -n "$pid" ] || return 1
|
|
kill -0 "$pid" 2>/dev/null || return 1
|
|
return 0
|
|
}
|
|
|
|
write_state() { # joy_x joy_y dir seconds
|
|
local jx="$1" jy="$2" dr="$3" secs="$4"
|
|
local until
|
|
if [ "${secs:-0}" = "0" ]; then
|
|
until=0
|
|
else
|
|
until="$(python3 -c "import time;print(f'{time.time()+float($secs):.3f}')")"
|
|
fi
|
|
python3 - "$STATE" "$jx" "$jy" "$dr" "$until" <<'PY'
|
|
import json, sys, os, tempfile
|
|
path, jx, jy, dr, until = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], float(sys.argv[5])
|
|
data = {"joy_x": jx, "joy_y": jy, "dir": dr, "until": until}
|
|
d = os.path.dirname(os.path.abspath(path))
|
|
fd, tmp = tempfile.mkstemp(dir=d)
|
|
with os.fdopen(fd, "w") as f:
|
|
json.dump(data, f)
|
|
os.replace(tmp, path)
|
|
PY
|
|
}
|
|
|
|
CMD="${1:-}"; shift || true
|
|
alive || die "守护进程未运行。先启动:setsid nohup python3 $D/robot-daemon.py > $D/daemon.log 2>&1 &"
|
|
|
|
# 平衡开关硬锁:拒绝关闭
|
|
case "$CMD" in
|
|
stable|balance)
|
|
die "拒绝:主动保持平衡不允许关闭(用户明确指令)。"
|
|
;;
|
|
esac
|
|
|
|
case "$CMD" in
|
|
go)
|
|
Y="${1:-50}"; MS="${2:-800}"
|
|
write_state 0 "$Y" stop "$(python3 -c "print(float($MS)/1000)")"
|
|
echo "[rc] 摇杆 y=$Y 持续 ${MS}ms" ;;
|
|
strafe)
|
|
X="${1:-50}"; MS="${2:-800}"
|
|
write_state "$X" 0 stop "$(python3 -c "print(float($MS)/1000)")"
|
|
echo "[rc] 摇杆 x=$X 持续 ${MS}ms" ;;
|
|
joy)
|
|
X="${1:-0}"; Y="${2:-0}"; MS="${3:-800}"
|
|
write_state "$X" "$Y" stop "$(python3 -c "print(float($MS)/1000)")"
|
|
echo "[rc] 摇杆 x=$X y=$Y 持续 ${MS}ms" ;;
|
|
jump)
|
|
MS="${1:-300}"
|
|
write_state 0 0 jump "$(python3 -c "print(float($MS)/1000)")"
|
|
echo "[rc] 跳 ${MS}ms" ;;
|
|
stop)
|
|
write_state 0 0 stop 0
|
|
echo "[rc] 已归零" ;;
|
|
hold)
|
|
MS="${1:-5000}"
|
|
write_state 0 0 stop "$(python3 -c "print(float($MS)/1000)")"
|
|
echo "[rc] 保持零帧 ${MS}ms" ;;
|
|
status)
|
|
echo "[rc] 守护进程 PID: $(cat "$PIDF")"
|
|
echo "[rc] 状态文件: $STATE"
|
|
cat "$STATE" 2>/dev/null || echo "(无)"
|
|
echo
|
|
echo "[rc] 最近日志:"; tail -6 "$D/daemon.log" 2>/dev/null ;;
|
|
*)
|
|
sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 1 ;;
|
|
esac
|