资讯动态

网页按钮控制ROS2小车:从rosbridge到电机驱动的完整链路

发布时间:2026/10/18 2:05:47 来源:尧图企业网站定制
1. 从“能跑起来”到“点一下就能动”这期项目到底要做什么按下网页上那个“前进”按钮的瞬间我的 ROS2 小车在地板上猛地往前蹿了一下吓得我赶紧又点了“停止”。说句实话从原理上讲这一步不过是 WebSocket 发了几行 JSON、DDS 转发了一条 Twist但当“网页按钮”和“车轮真的转动”这两件事被串起来的那一刻那种“整条链路终于打通了”的感觉还是很上头的。这个项目的缘起其实特别简单车在更早之前就已经能跑了。我早就把电机驱动、话题通信、键盘遥控全调通了当时用的是teleop_twist_keyboard这种经典操作方式。问题在于每次想在地板上试一下车我都得蹲在电脑前敲命令或者把数字键盘按得噼啪响人还要屁颠屁颠跟着车跑。我心里想的是掏出手机打开一个网页点一下上面的按钮车就动。不要装 App不要连手柄不要手动敲命令纯粹“点一下按钮”。这一期的核心目标就一句话让普通的 HTTP 网页按钮通过 rosbridge 直接操作小车的 /cmd_vel从而控制电机。听起来不复杂但要把“前端点击”变成“轮子转动”中间要串起至少四个环节网页按钮、WebSocket 桥接、ROS2 话题、底层电机驱动。任何一个环节没对齐结果都是“页面显示已连接车就是不动”。要说明的是这一期我刻意没有碰很多东西。没有接摄像头没有做 SLAM没有做自动导航也没有搞手柄配对。原因很简单这期要解决的是“控制链路的可达性”问题先把最基础的一条路走到通。后面再往这条通路上加摄像头画面、加导航指令都是顺着这条路继续扩展的事地基不打牢后面全是坑。先给还不熟悉的读者交代一下完整的数据链路。整个过程从你按下按钮开始往下走是这样的网页按钮触发 JavaScript 的pointerdown事件roslibjs把动作封装成 ROS2 的Twist消息通过 WebSocket 发出去车上的rosbridge_server收到 JSON 格式的消息恢复成 DDS 数据发布到/cmd_vel话题motor_controller节点订阅/cmd_vel用差速模型算出左右轮速度转成 PWM 百分比百分比通过串口发给 ESP32 这类单片机ESP32 输出 PWM 给电机驱动板驱动板放大电流驱动电机轮子转。这条链路里的每一段我在下面都会单独拆出来讲包括选型理由、实际代码、踩过的坑和我的排查顺序。如果你已经对 ROS2 的节点、话题、消息这些基础概念有了解但一直没把“网页控制”这件事串起来这篇应该能让你少走不少弯路。2. 硬件怎么搭主控、驱动板和底盘选择2.1 我的硬件清单与各自分工很多人上来就问“什么配置能跑”其实这套项目对硬件的要求并不高。我自己手里这套配置属于非常典型的入门级 ROS2 小车清单如下部件型号/规格在本项目里的角色底盘常见的 2WD 或 4WD 小车底盘配 TT 电机提供运动结构主控带 WiFi 的 ARM64 单板机4GB 内存跑 Ubuntu ROS2 rosbridge是真正的“大脑”单片机ESP32 开发板生成 PWM 驱动电机与主控串口通信电机驱动DRV8833 或 TB6612 双路驱动板把 PWM 信号放大成电机需要的电流电源两节 18650 串联加降压模块另配 5V 给主控供电动力与主控分开供电避免电机干扰为什么要拆成“主控 单片机”两层而不是让单板机直接去驱动电机这个问题我在下一小节详细说。这里先提一句关键点底层的 PWM 生成和方向控制最好别让跑 ROS2 的那块板子亲自干。2.2 为什么不让主控直接去驱动电机新手最容易犯的错就是以为单板机上 GPIO 引脚多能输出 PWM就直接把电机驱动板接上去了。理论上能转实际用起来全是泪。我在更早的项目里就这么干过后来才痛下决心把底层控制拆出去。原因有三个。第一单板机上跑着 Linux 和 ROS2CPU 要处理 WiFi、DDS 通信、各种日志对 GPIO 的 PWM 输出精度和稳定性影响很大。尤其是家用 WiFi 环境网络抖动会直接反映在电机控制上表现为“车速忽快忽慢”。第二单板机的 GPIO 电流驱动能力很弱直接接电机驱动板的信号引脚还好但一旦接线不规范或者驱动板失控反灌电流烧引脚的风险一直存在。第三也是最重要的把底层 PWM 放在一个独立的单片机上意味着即使 ROS2 主控崩了、网页卡了电机也不会失去控制逻辑。所以我现在的做法是ESP32 只负责三件事——从串口收指令、解析出来、输出 PWM。它跑的是一个不依赖操作系统的裸机程序没有调度延迟也没有“系统卡了”的说法实时性比 Linux 好得多。2.3 串口协议交给 ESP32 的活越简单越好单片机这端我用的串口协议是纯文本行协议。每条命令一行格式是L左轮百分比,R右轮百分比\n例如L50,R-50\n。百分比范围是 -100 到 100正值表示前进方向负值表示倒退。不搞二进制协议不加复杂校验原因很简单协议越简单单片机代码越不容易出错排查越方便。真机上如果担心干扰导致错包可以在命令行后面追加校验和但第一版我建议先跑通最简协议。对应的 ESP32 固件核心逻辑大概是这样的骨架实际用的时候需要根据你的开发板和引脚改一下const int FREQ 1000; // PWM 频率 1kHz足够普通直流减速电机用 const int RES 8; // 8 位分辨率0~255 void setup() { Serial.begin(115200); ledcSetup(0, FREQ, RES); ledcSetup(1, FREQ, RES); ledcAttachPin(13, 0); // 左电机 PWM 引脚 ledcAttachPin(12, 1); // 右电机 PWM 引脚 pinMode(14, OUTPUT); // 左电机方向引脚 pinMode(27, OUTPUT); // 右电机方向引脚 } void loop() { if (Serial.available()) { String line Serial.readStringUntil(\n); int sep line.indexOf(,); if (sep 0) return; int left line.substring(1, sep).toInt(); int right line.substring(sep 2).toInt(); setMotor(left, 14, 0); setMotor(right, 27, 1); } } void setMotor(int pct, int dirPin, int ch) { int pwm map(abs(pct), 0, 100, 0, 255); digitalWrite(dirPin, pct 0 ? HIGH : LOW); ledcWrite(ch, pwm); }注意一点Serial.readStringUntil会有字符阻塞等待实际生产级代码一般要做非阻塞解析但第一版手动测试完全够用。等到后面要做高频闭环控制再考虑换帧格式、加校验和。2.4 标定最大轮速做差速换算前必做的一步很多人忽略这步直接开跑结果就是速度换算完全不对。我们后面说差速模型时会用到“电机最大轮速”这个参数这个参数不是封装的额定值而是要在你自己这套电压、负载条件下实测出来的。我的标定方法很土但有效把车架空把电池充满给两个电机同时发 100% PWM然后量轮子转多少圈用多少秒反推线速度。或者更简单一点把车放地上发 100% PWM用卷尺量它 3 秒走多远。我实测下来TT 电机加常见轮子在满电状态下大概能到 1.0 ~ 1.2 m/s。我代码里把MAX_WHEEL_SPEED设为 1.0留一点余量后面所有速度换算都以这个数为基准。这里有个经验之谈标定要在实际运行电压下做不要在电脑上算理论值。电池电压从满电到快没电电机带载表现差异很大标定时用的电压要和日常使用场景一致否则标出来不准后面控制会发飘。3. 打通 ROS2 移动层从 /cmd_vel 到左右轮速3.1 一个节点把 Twist 变成左右轮 PWM 百分比在 ROS2 这边我建了一个motor_controller节点订阅/cmd_vel话题。这个节点做的事情就是把geometry_msgs/msg/Twist里的linear.x和angular.z换算成两侧轮子的速度百分比再通过串口发给 ESP32。创建一个 Python 包很简单ros2 pkg create --build-type ament_python motor_controller然后在motor_controller/motor_controller/motor_controller.py里写核心逻辑。下面是完整节点代码的骨架你直接抄去改参数就能用#!/usr/bin/env python3 import serial import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist WHEEL_BASE 0.16 # 两轮中心距单位 m WHEEL_RADIUS 0.032 # 轮子半径单位 m MAX_WHEEL_SPEED 1.0 # 实测最大轮速 m/s MOTOR_SERIAL /dev/ttyUSB0 BAUD_RATE 115200 class MotorController(Node): def __init__(self): super().__init__(motor_controller) self.sub self.create_subscription( Twist, /cmd_vel, self.cmd_vel_callback, 10) self.ser serial.Serial(MOTOR_SERIAL, BAUD_RATE, timeout0.1) self.timer self.create_timer(0.5, self.watchdog_callback) self.last_cmd_time self.get_clock().now() self.get_logger().info(motor_controller started) def cmd_vel_callback(self, msg): vx msg.linear.x # 前进速度m/s wz msg.angular.z # 旋转角速度rad/s left (vx - wz * WHEEL_BASE / 2.0) / WHEEL_RADIUS right (vx wz * WHEEL_BASE / 2.0) / WHEEL_RADIUS left_pct int(max(-1.0, min(1.0, left / MAX_WHEEL_SPEED)) * 100) right_pct int(max(-1.0, min(1.0, right / MAX_WHEEL_SPEED)) * 100) self.send_speed(left_pct, right_pct) self.last_cmd_time self.get_clock().now() def send_speed(self, left, right): cmd fL{left},R{right}\n self.ser.write(cmd.encode(utf-8)) def watchdog_callback(self): # 0.6 秒内没收到新指令强制停车 if (self.get_clock().now() - self.last_cmd_time).nanoseconds 6e8: self.send_speed(0, 0) self.get_logger().warn(watchdog triggered, car stopped) def main(argsNone): rclpy.init(argsargs) node MotorController() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()注意看这里没有把angular.z单独映射成“转向”而是把它和线速度合成到左右轮。这就是下一小节要解释的差速模型也是在 ROS2 小车控制里最核心的换算。3.2 差速模型换算公式怎么来的如果你只有两个驱动轮前进和转向都靠左右轮的转速差来实现。比如左轮慢、右轮快车就会向右转弯反过来向左。Twist消息给你的不是左右轮速度而是一个综合的线速度v和角速度ω你需要把它们折算到两个轮子上。差速模型的标准公式是左轮速度 (v - ω × L/2) / r右轮速度 (v ω × L/2) / r其中L是两轮中心距r是轮子半径。直觉上怎么理解你想象人走路转弯外侧的脚总是迈得比内侧的脚快“外侧”就是转弯圆弧的外侧。如果以车中心为基准左轮离转弯中心更近所以它要比中心的线速度慢一点右轮离得更远就要快一点。这个快慢差正好是ω × L/2除以轮子半径r后就变成轮子的角速度单位。所以我的代码里这两行left (vx - wz * WHEEL_BASE / 2.0) / WHEEL_RADIUS right (vx wz * WHEEL_BASE / 2.0) / WHEEL_RADIUS出来的left和right是轮子的切线速度单位 m/s然后再除以MAX_WHEEL_SPEED归一化成 -1 到 1 的范围乘 100 变成百分比。这步是关键因为单片机的 PWM 只能按百分比接受指令。一个常见的误区是把angular.z当“舵机角度”来用直接映射到转向。那是阿克曼转向的思维用到差速车上就完全错了。差速底盘必须用这种“速度合成”的方式前后和转向是耦合的不是两个独立通道。3.3 断线保护watchdog 定时器是底线这段代码里我最看重的是watchdog_callback。它的逻辑很简单每 0.5 秒检查一次最近一次收到/cmd_vel指令的时间如果超过 0.6 秒没有收到新指令就自动发送L0,R0停车。为什么必须有这一层因为网页控制的一个隐藏风险是“指令断流”。你按着前进键网页通过 WebSocket 发速度指令一旦 WiFi 卡了一下、手机锁屏、浏览器切后台指令流就可能中断而车上最后的指令仍然是“前进”车就会一直开下去直到撞墙。如果没有 watchdog这锅只能你自己背。有 watchdog 之后即使前端完全挂了最多 0.6 秒车就会自动停下。这是在安全层面非常重要的一道保险比任何“急停按钮”都更底层。后面做导航、做自动巡航时这个 watchdog 依然有效。3.4 先别急网页用命令行验证底层网页还没写的阶段我先在命令行用ros2 topic pub验证底层链路ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist {linear: {x: 0.2, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}这条命令每 0.1 秒发一次速度为 0.2 m/s 的直线前进指令。如果motor_controller节点和 ESP32 都正常轮子应该以很慢的速度转起来。实测经验先用极小速度0.1 m/s测试架空轮子或者把车固定住再做。我第一次测试的时候就把速度设到 0.8 m/s车直接从办公桌上冲下去幸好轮子离地前我按了 CtrlC。命令行测试通过之后才可以进入下一阶段的网页联调。我还建议在车上另开一个终端跑ros2 topic echo /cmd_vel这样可以在车端直观看到话题上有没有数据流排查问题时能分清是“前端没发出来”还是“底层没收到”。4. rosbridge网页和 ROS2 之间的翻译官4.1 为什么选 rosbridge_suite而不是自己写 socket到了这一步就面临一个选择前端网页怎么和 ROS2 通信有人会想我直接用 Python 写个 HTTP 接口网页请求一下然后由这个接口去 publish。能跑但不推荐。第二个更常见的思路是自己写 WebSocket 服务器在服务端把收到的消息转成 ROS2 话题发布这就等于自己在造轮子。我最终选择的是rosbridge_suite原因很直接它已经包含了“WebSocket 通信 JSON 协议 ROS2 话题/服务/动作桥接”的完整方案社区维护了多年协议稳定。前端只要用roslibjs就能操作它不用自己处理消息序列化、不用处理 DDS 细节。最重要的是它天然支持话题的订阅和发布、服务的调用意味着后面加摄像头画面、加导航服务前端代码风格基本不变。rosbridge 有一个需要注意的特性它传输的是 JSON 文本效率比原始 DDS 低。但对我们第一期这种低频控制指令10Hz 左右的 Twist来说完全不是瓶颈。后面如果要把大分辨率图像、点云数据塞进 rosbridge才会遇到性能瓶颈到时候再考虑 MJPEG 流或者 WebRTC 方案那是后话。4.2 安装、启动、检查端口如果是 ROS2 Humble 版本安装特别简单sudo apt update sudo apt install ros-humble-rosbridge-suite启动 rosbridge 服务器只要一条命令ros2 launch rosbridge_server rosbridge_websocket_launch.xml默认情况下它会监听9090端口。启动后我习惯先验证三件事免得后面前端连不上时一头雾水ros2 node list # 看 rosbridge 相关节点是否在 ss -tlnp | grep 9090 # 看端口是否在监听 ros2 topic list | grep cmd_vel # 看 /cmd_vel 是否存在如果是车上的单板机我还建议让 rosbridge 启动命令跟着系统开机自启用 systemd 服务把它变成常驻进程这样第 1 期完成后开机的车直接就是“可用”状态不用每次 SSH 进去手动启动。4.3 用 Python websocket 先捅一次确认链路通在写网页之前先用命令行级的 WebSocket 客户端脚本做一次“最小链路测试”能省很多时间。测试思路从开发机用 Python 连接小车的 9090 端口通过 rosbridge 协议往/cmd_vel发一条速度指令看车端ros2 topic echo /cmd_vel能不能收到。import json import websocket ws websocket.create_connection(ws://192.168.1.100:9090) # 先告诉 rosbridge 我要发布这个话题 ws.send(json.dumps({ op: advertise, topic: /cmd_vel, type: geometry_msgs/msg/Twist })) # 发布一条速度为 0.1 的直线前进指令 ws.send(json.dumps({ op: publish, topic: /cmd_vel, msg: { linear: {x: 0.1, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0} } }))注意advertise这一步不能省。rosbridge 协议里发布者第一次发布前要声明话题名和消息类型这个过程叫“广告”。很多人写前端时忘了这个操作结果是页面显示已连接但发布出去的消息无人受理因为节点订阅信息里根本没这个声明。跑完这个脚本如果车端ros2 topic echo /cmd_vel能看到linear.x等于 0.1就说明整个 WebSocket 到 DDS 的链路已经通了接下来前端只是换个客户端而已。5. 网页按钮是怎么让车动的前端实现5.1 页面布局手机端操作的几个小细节前端我用了一个纯 HTML 单文件没有框架没有构建工具方便直接部署在任何静态服务器上。页面布局就是一个仿手柄的网格上面是“前进”下面是“后退”左右是两个转向按钮中间一个大的“停止”按钮。版面本身没什么好讲的真正容易出问题的是一些手机端交互细节。第一按钮必须支持触摸事件不能只监听鼠标事件否则手机上点了没反应。第二要防止手机按住按钮时页面滚动或缩放我给按钮加了touch-action: none和user-select: none。第三按钮要大间距要够我踩过“手忙脚乱时按错按钮”的坑最后把“停止”按钮单独设计成最大、红色、置底才能保证出问题时一眼就能找到。5.2 roslibjs 连接和发布 Twist前端核心依赖是roslibjs我直接用 CDN 引入script srchttps://cdn.jsdelivr.net/npm/roslib1/build/roslib.min.js/script连接和发布的核心代码就三块创建ROSLIB.Ros对象连接 WebSocket、创建ROSLIB.Topic声明/cmd_vel话题、调用publish发Twist消息。const ros new ROSLIB.Ros({ url: ws://192.168.1.100:9090 }); ros.on(connection, () { document.getElementById(status).textContent 已连接; }); ros.on(error, () { document.getElementById(status).textContent 连接失败; }); const cmdVel new ROSLIB.Topic({ ros: ros, name: /cmd_vel, messageType: geometry_msgs/msg/Twist }); function sendTwist(linear, angular) { const twist new ROSLIB.Message({ linear: { x: linear, y: 0, z: 0 }, angular: { x: 0, y: 0, z: angular } }); cmdVel.publish(twist); }注意messageType的写法。新版roslibjs用的是geometry_msgs/msg/Twist这种带/msg/的格式但网上很多老教程写的是geometry_msgs/Twist。如果你的小车端话题接收不到数据先检查是不是这里写错了。rosbridge 对消息类型有严格匹配类型不符会被静默丢弃页面却仍然显示连接正常这类问题最隐蔽。5.3 按住动、松开停事件绑定别踩的坑网页遥控的交互逻辑是按住“前进”按钮车持续前进松开车停。这里我用了两个机制保证可靠。第一个机制是“持续发布”。我没有在每次点击事件里只发一次速度指令而是在按住按钮期间用setInterval以 10Hz 的频率持续发布 Twist让cmd_vel上一直有新鲜的速度数据。这样即使某一帧 WebSocket 消息在网络里丢了下一秒还有新消息补上配合底层的 watchdog也不会因为单帧丢失导致掉速度。第二个机制是“双向停止”。鼠标事件里除了mousedown/mouseup我同时监听了pointerdown/pointerup和pointerleave。为什么不只监听mouseup因为我实测过按住按钮后手指或鼠标稍微滑出按钮范围mouseup事件可能不会触发车就会一直跑。用pointerleave做兜底只要指针离开按钮就发停止再加上 document 级别的pointerup兜底基本杜绝“松手不停车”的情况。还有一个小细节停止按钮在pointerdown时直接clearInterval并发布全零 Twist同时切断所有正在运行的持续发布任务。我称之为“急停优先级”任何操作状态下点它都应该优先被打断。6. 联调实测点一下真的动了6.1 完整测试流程和实测延迟整个联调我分四步走每一步都在上一步通过后才继续架空测试。把车架在两摞书上轮子离地。先从网页点“前进”确认轮子转向正确再点“后退”确认方向反转。这个阶段速度设到 0.1 m/s足够验证方向。低速贴地测试。把车放到空旷地面用 0.2 m/s 的速度跑直线、转弯反复确认网页端“按住才走松手即停”的节奏。延迟感受测试。我用手机在旁边录视频另一只手按按钮回放时逐帧看按下瞬间和轮子开始转动的帧差实测下来家里 WiFi 下大约在 60~120ms 之间。这个延迟对低速遥控完全够用不会有“按下去半天车才动”的失控感。连续操作稳定性测试。连续向前、后退、转弯切换 20 分钟观察是否有指令丢失、ROS2 节点掉线、rosbridge 日志报错。整套流程走完结论就是文章开头那句话点一下按钮车真的动了。而且反复操作下来没有出现一次“车停不下来”的情况这主要归功于 watchdog 和松手停发的双保险。6.2 踩坑记录六个问题和我最终的排查顺序联调过程中遇到很多问题我把最典型的六个整理成表格连同排查方向一起记录下来现象排查方向解决办法页面一直“未连接”rosbridge 没启动、IP 写错、9090 端口没监听ss -tlnp | grep 9090验证确认网页里 IP 和小车实际 IP 一致页面显示已连接但车不动话题名不一致、消息类型不匹配车端ros2 topic list、ros2 topic info /cmd_vel核对检查messageType是否带/msg/前进变成了后退方向定义不一致或电机接线反了检查 ESP32 方向引脚高低电平定义必要时在代码里反转符号松手了车还继续走触摸/鼠标事件没绑定全按钮滑动出范围用pointerup 按钮pointerleave双向兜底并发布全零 Twist左右轮速度不一致车跑偏两个电机特性差异、电池电压波动、两路 PWM 精度不同代码里给左右轮各加一个标定系数分别微调比例串口打开失败 permission denied当前用户不在 dialout 组sudo usermod -aG dialout 用户名重登生效排查顺序我一般坚持“从底层往上层”走先在命令行确认 ESP32 能收到串口指令再确认ros2 topic pub能控制电机然后确认 rosbridge 能不能桥接最后才怀疑网页本身。按这个顺序大部分问题都能快速定位而不是在页面和代码之间凭感觉乱猜。6.3 安全心得与后续方向做这种会动的硬件项目安全永远排在功能前面。这次联调里我坚持的几个安全习惯分享给你第一次测试必须架空轮子速度从 0.1 m/s 开始慢慢加车上始终留一个物理总开关一旦网页失控直接断电测试场地避开人群和易碎物品。这些不是什么高深技术但能避免很多事故。最后再分享一点我自己的体会rosbridge 默认没有鉴权机制任何能访问到你局域网并知道 IP 和端口的人理论上都能给你的车下发指令。所以第 1 期阶段我只让它监听内网不要做端口映射暴露到公网如果确实需要外网访问一定要在 rosbridge 前面加认证层。这不是危言耸听而是所有 ROS 开发者在做 Web 桥接时都必须考虑的事。这一期把它“按一下按钮车能动”这个目标跑通了后面就顺着这条链路继续扩展把摄像头画面接到同一个网页里做一个带实时画面的遥控界面再往后可以挂上 SLAM 建图和导航栈让网页按钮从“手动遥控”变成“下达目标点”。每一步都是往这条既有链路上加模块而不是推翻重来这正是当初花时间理清链路结构带来的最大收益。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑