WebSocket 控制接口

底盘遥控这类高频操作走 WebSocket,比 HTTP 轮询延迟低得多。控制页的虚拟摇杆用的就是 这条通道。

连接

ws://<机器人IP>/ws/control          # HTTPS 页面上用 wss://

协议格式

双方都发二进制帧,第一个字节是类型标记。

客户端 → 服务端

帧说明
0xAA + int8 x + int8 y摇杆,3 字节;x 转向、y 油门,各 -128~127
0xDD + UTF-8 JSON低频命令(见下),文本帧发 JSON 也接受

摇杆换算:fy = y/127、fx = x/127,左 = (fy+fx)×100、右 = (fy-fx)×100,限幅 到 ±100 后下发。不带时长、不阻塞,所以松手要自己补一帧停止。

操作字节说明
前进 50AA 00 32X=0, Y=50
后退 30AA 00 E2X=0, Y=-30(补码)
左转 25AA E7 00X=-25, Y=0
右转 25AA 19 00X=25, Y=0
停止AA 00 00X=0, Y=0

JSON 命令(0xDD + JSON):

type说明
{"type":"ip"}查 IP,回 {"type":"ip","ip":"…"}
{"type":"action","action":"stop","speed":50,"time":0}走和 HTTP /api/control 同一个动作入口(不阻塞,发完就走)
{"type":"raw_command","cmd":"…"}透传固件原始指令(排障用)
{"type":"arm_cmd","payload":{"command":"grab"}}夹爪开/合(只认 grab / release),不回复
{"type":"reinitialize"}重连底盘(清 PID/编码器,不掉线、不打断运行)

其它 type 会被忽略(只记 debug 日志)。

服务端 → 客户端

帧说明
0xBB + int16LE 左轮 + int16LE 右轮轮速,单位 m/s × 1000;每 200ms 推一次
0xDD + UTF-8 JSON状态与应答。连上即推 {"type":"ip","ip":"…"},以及 {"type":"motor_status","motor":{…}}(此后仅在底盘连接状态变化时推)

前端示例

const ws = new WebSocket("ws://192.168.4.1/ws/control");
ws.binaryType = "arraybuffer";

ws.send(new Uint8Array([0xAA, 0, 50]));   // 前进
ws.send(new Uint8Array([0xAA, 0, 0]));    // 停

ws.onmessage = (event) => {
    const b = new DataView(event.data);
    if (b.getUint8(0) === 0xBB) {
        const left = b.getInt16(1, true) / 1000;
        const right = b.getInt16(3, true) / 1000;
        console.log(`左: ${left} m/s, 右: ${right} m/s`);
    }
};

生命周期与断线

  • 服务端每 4 秒 ping 一次;90 秒收不到任何字节才判死回收连接 —— 阈值给得宽是 因为手机 WiFi 省电模式会让上行停摆几十秒,秒级判死只会造成反复断开重连。
  • ⚠️ 断开连接不会停车(这点和 Python 版相反):判死只用于回收僵尸连接,不再承担 安全停车职责。停车靠指令本身(摇杆发 0 或 action stop),以及 ESP32 固件侧的 心跳看门狗。
  • 同一时刻只保持一个控制连接:多个连接同时发会互相抢(谁后发谁生效)。