Drobotics logo DROBOTICSTechnical Library

Rover Guide

Rover UDP Agent: Raspberry Pi Remote Control Middleware for STM32 Ackermann Rover

All Rover
无人车 UDP Agent | Rover 远程控制中间件

无人车 UDP Agent

Raspberry Pi 远程控制中间件 · STM32 串行协议 · JSON 命令接口

UDP 服务器 STM32 C30D 多线程运动 安全停止 JSON 命令
概述

本文档介绍 Raspberry Pi 上的 rover_agent.py 程序。该代理接收来自 PC 任务计算机的 UDP 命令,将其转换为 Wheeltec STM32 串行包格式,并通过 USB 串行发送至无人车电机控制器。

PC mission script -> UDP JSON command Raspberry Pi 4B rover_agent.py -> USB serial packet STM32 motor controller -> motors and steering
这使 PC 端的任务脚本保持简单,只发送高层命令:ping · stop · prime · move
网络设置
LISTEN_IP = "0.0.0.0" LISTEN_PORT = 15000
使用 0.0.0.0 表示代理通过树莓派的任何活跃网络接口(以太网或 Wi-Fi)接收命令。
在 LQ3/以太网设置中,PC 任务脚本应发送命令到树莓派以太网 IP,如 192.168.1.102
串口设置
MOTOR_PORT = "/dev/serial/by-id/usb-1a86_USB_Single_Serial_5C2C061781-if00" BAUD = 115200
使用稳定的 /dev/serial/by-id/... 路径优于 /dev/ttyACM0,因为重启或 USB 重连后 ACM 设备编号可能互换。
STM32 包格式

电机控制器期望 11 字节数据包:

7B 00 00 XX XX 00 00 ZZ ZZ CHECKSUM 7D
字段说明
7B帧头
00 00保留字节
XX XXX 速度,16位有符号,mm/s
00 00Y 速度(此处未使用)
ZZ ZZZ 速度/转向,16位有符号
CHECKSUM前 9 字节的 XOR 校验和
7D帧尾
x_speed 控制前进/后退:正 = 前进,负 = 后退,0 = 停止
z_speed 控制转向:正/负 = 左/右转(取决于配置),0 = 直行
有符号 16 位转换
def signed16(value): value = int(max(-1000, min(1000, value))) if value < 0: value = 65536 + value return value
100 → 0x0064
300 → 0x012C
-100 → 0xFF9C
脚本将值限制在 -10001000 之间以确保安全。
停止行为
send_drive(0, 0)
X 速度 = 0,Z 速度 = 0
stop_rover() 发送中性包并打印 [ROVER] STOP
Prime 行为
for _ in range(10): send_drive(0, 0) time.sleep(0.1) for _ in range(5): send_drive(120, 0) time.sleep(0.1) for _ in range(10): send_drive(0, 0) time.sleep(0.1)
警告 无人车可能在启动时轻微移动:120 mm/s 约 0.5 秒。
如果启动移动不可接受,不要自动调用 prime_rover(),改为仅在安全时从 PC 发送手动 prime 命令。
支持的 UDP 命令
Ping
{"cmd": "ping"}
响应:{"ok": true, "status": "alive"}
Stop
{"cmd": "stop"}
响应:{"ok": true, "status": "stopped"}
Prime
{"cmd": "prime"}
响应:{"ok": true, "status": "primed"}
Move
{"cmd": "move", "x": 200, "z": 0, "duration": 2}
含义:前进 200 mm/s,无转向,持续 2 秒,然后停止。
响应:{"ok": true, "status": "moving", "x": 200, "z": 0, "duration": 2}
运动线程
def run_motion(x_speed, z_speed, duration):...
运动在后台线程中执行,每 0.10 秒重复发送运动包,确保 STM32 在运动期间持续接收新鲜命令。时长结束或收到停止命令时发送最终 STOP。
安全行为
脚本以 Ctrl-C 退出或收到终止信号时执行 exit_handler()
• 设置停止事件
• 向无人车发送 STOP
• 关闭串口
• 干净退出
通过 防止脚本停止后无人车继续移动。
PC 测试命令
Windows PowerShell
# Ping py -c "import socket,json; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(2); s.sendto(json.dumps(dict(cmd='ping')).encode(),('192.168.1.102',15000)); print(s.recvfrom(4096))" # Move py -c "import socket,json; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(2); s.sendto(json.dumps(dict(cmd='move',x=200,z=0,duration=2)).encode(),('192.168.1.102',15000)); print(s.recvfrom(4096))" # Stop py -c "import socket,json; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(2); s.sendto(json.dumps(dict(cmd='stop')).encode(),('192.168.1.102',15000)); print(s.recvfrom(4096))"
完整脚本
#!/usr/bin/env python3 import socket import json import time import signal import sys import serial import threading MOTOR_PORT = "/dev/serial/by-id/usb-1a86_USB_Single_Serial_5C2C061781-if00" BAUD = 115200 LISTEN_IP = "0.0.0.0" LISTEN_PORT = 15000 SEND_INTERVAL = 0.10 ser = None motion_lock = threading.Lock() motion_stop_event = threading.Event() motion_thread = None def signed16(value): value = int(max(-1000, min(1000, value))) if value < 0: value = 65536 + value return value def make_drive_cmd(x_speed, z_speed): x = signed16(x_speed) z = signed16(z_speed) packet = [ 0x7B, 0x00, 0x00, (x >> 8) & 0xFF, x & 0xFF, 0x00, 0x00, (z >> 8) & 0xFF, z & 0xFF, ] checksum = 0 for b in packet: checksum ^= b packet.append(checksum) packet.append(0x7D) return bytes(packet) def send_drive(x_speed, z_speed): data = make_drive_cmd(x_speed, z_speed) ser.write(data) ser.flush() def stop_rover(): send_drive(0, 0) print("[ROVER] STOP", flush=True) def prime_rover(): print("[ROVER] Priming STM32 serial control...", flush=True) for _ in range(10): send_drive(0, 0) time.sleep(0.1) for _ in range(5): send_drive(120, 0) time.sleep(0.1) for _ in range(10): send_drive(0, 0) time.sleep(0.1) print("[ROVER] Prime complete", flush=True) def run_motion(x_speed, z_speed, duration): print("[ROVER] MOVE x=%d z=%d duration=%.1f" % (x_speed, z_speed, duration), flush=True) end_time = time.time() + duration while time.time() < end_time: if motion_stop_event.is_set(): break send_drive(x_speed, z_speed) time.sleep(SEND_INTERVAL) stop_rover() def start_motion(x_speed, z_speed, duration): global motion_thread with motion_lock: motion_stop_event.set() if motion_thread and motion_thread.is_alive(): motion_thread.join(timeout=1) motion_stop_event.clear() motion_thread = threading.Thread( target=run_motion, args=(x_speed, z_speed, duration), daemon=True ) motion_thread.start() def handle_command(cmd): name = cmd.get("cmd") if name == "stop": motion_stop_event.set() stop_rover() return {"ok": True, "status": "stopped"} if name == "prime": motion_stop_event.set() prime_rover() return {"ok": True, "status": "primed"} if name == "move": x = int(cmd.get("x", 0)) z = int(cmd.get("z", 0)) duration = float(cmd.get("duration", 0)) if duration <= 0: return {"ok": False, "error": "duration must be > 0"} start_motion(x, z, duration) return {"ok": True, "status": "moving", "x": x, "z": z, "duration": duration} if name == "ping": return {"ok": True, "status": "alive"} return {"ok": False, "error": "unknown command"} def exit_handler(sig=None, frame=None): print("\n[EXIT] STOP", flush=True) try: motion_stop_event.set() stop_rover() except Exception: pass try: if ser: ser.close() except Exception: pass sys.exit(0) signal.signal(signal.SIGINT, exit_handler) signal.signal(signal.SIGTERM, exit_handler) print("[ROVER] Connecting STM32...", flush=True) ser = serial.Serial(MOTOR_PORT, BAUD, timeout=1) time.sleep(2) print("[ROVER] STM32 OK", flush=True) prime_rover() sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((LISTEN_IP, LISTEN_PORT)) print("[ROVER] UDP listening on %s:%d" % (LISTEN_IP, LISTEN_PORT), flush=True) while True: data, addr = sock.recvfrom(4096) try: cmd = json.loads(data.decode("utf-8")) print("[ROVER] RX from", addr, cmd, flush=True) reply = handle_command(cmd) except Exception as exc: reply = {"ok": False, "error": str(exc)} try: sock.sendto(json.dumps(reply).encode("utf-8"), addr) except Exception: pass

平台:Raspberry Pi 4B · STM32 C30D · Ubuntu 22.04 · Python 3 · UDP
(c) 卓博泰科技 · Rover UDP Agent 完整文档