基于rosbridge的ROS2小车浏览器控制实战——从零搭建网页按钮控制链路

发布时间:2026/10/18 1:23:18
基于rosbridge的ROS2小车浏览器控制实战——从零搭建网页按钮控制链路
如果你自己动手从头攒过一台 ROS2 小车一定经历过这种尴尬遥控器连接不稳定上位机软件又重又乱好不容易让小车在桌面上跑起来围观的朋友问你的第一句话永远是——“这不就是个遥控玩具吗”我这次想换一种做法不用手柄不上重型软件而是直接在浏览器里点一个按钮让小车往前走。做的东西说穿了一点也不神秘ROS2 小车 网页按钮。我把整个改造过程记成系列这是第 1 期目标只有一个按下网页上的“前进”车真的会动。一个人做这个项目最耗时间的往往不是写代码而是被各种环境问题反复折腾。所以这一篇我尽量把从方案选型、环境搭建、网页代码到第一次实测的完整过程写清楚你如果手头也有一套差速底盘可以直接照着这套流程复现。就算没有小车只看网页和 rosbridge 交互那部分也能理解 ROS2 如何通过 WebSocket 对外提供一个“翻译通道”让普通浏览器参与机器人控制。1. 方案选型为什么是 rosbridge而不是手柄和自建服务1.1 先想清楚“一个人改造”的痛点开始动手之前我先列了一个需求清单。首先这个项目要一个人完成开发、调试和演示没有团队帮我看环境所以任何需要单独安装客户端、配置复杂环境的方案都会被我一票否决。第二我希望演示的时候不挑设备手边的手机、平板、办公室的普通电脑都行。第三我希望后续扩展容易今天加一个按钮明天可以加摇杆后天可以加摄像头画面甚至从速控切到导航最好都只动前端和少量桥接逻辑。在这些条件下传统方案显得很别扭。用游戏手柄确实能控制但手柄接收器要先插到主机上手机想控制基本没戏。用 rqt 这类桌面端工具界面入手成本高给别人演示还得先教他怎么开节点。这些都是“能跑但不够干净”的方案我决定全部放掉直接走 Web 控制路线。1.2 rosbridge 的定位浏览器和 ROS2 之间的翻译官浏览器不认识 ROS2 的 DDS 通信协议也收不到话题消息这是最根本的问题。rosbridge 就是解决这个问题的标准做法。它在服务端开启一个 WebSocket 端口默认是 9090负责把 ROS2 的话题、服务、参数转换成 JSON 格式再通过 WebSocket 发给浏览器反过来浏览器把 JSON 消息发回来它再转成 ROS2 的消息发布到指定话题。整个过程用一句类比浏览器只会说“普通话”ROS2 只会说“方言”rosbridge 是两边都懂的翻译。页面端我只需要用 roslibjs 这个 JavaScript 库建立 WebSocket 连接然后像操作本地对象一样发布 ROS 消息。底层 DDS 的复杂分包、发现、握手机制全被挡在外面前端开发人员不需要理解机器人通信协议也能快速参与进来。我对比过几条路线结论很明确方案浏览器支持开发成本长期维护rosbridge roslibjs原生支持很低生态成熟只需维护前端页面可靠自己写 WebSocket 节点原生支持高要处理协议和序列化需要自己考虑异常场景MQTT 桥接需额外桥接层中多一层消息中间件多了跳板延迟点变多传统桌面工具不支持低不方便远程和演示rosbridge 不是性能最强的方案但它是我在这个场景下最“省心”的方案。严格来说是“够用”的按钮控制车这种低频指令几十毫秒延迟完全没问题就算未来做虚拟摇杆高频发布在局域网内也扛得住。1.3 系统结构一条命令从浏览器流到电机先看整体拓扑理解消息是怎么走的。小车计算平台运行两个核心节点一个是底盘驱动节点负责订阅/cmd_vel话题把 Twist 消息换算成电机控制指令另一个是 rosbridge_server它暴露 9090 端口给局域网内的所有浏览器。浏览器页面里的 roslibjs 通过ws://小车IP:9090建立连接页面按钮按下时发布 Twist 消息rosbridge 收到后转成 ROS2 话题消息底盘驱动节点收到后驱动电机小车就走起来了。反过来说如果小车状态要上报给网页比如电池电压、当前速度也可以用同样的通道反向推送。后续做数据可视化或者远程监控这套结构不用推翻重来这大大降低了我的负担。第一期我只做“按钮到电机”这一条最小链路把坑踩明白后面再往上面加东西就快很多。2. 环境搭建与核心节点配置2.1 硬件与软件版本怎么选我用的底盘是常见的两轮差速底盘包含两个直流减速电机、一块驱动板、一个 IMU 和一些编码器。真正做计算的是一块 ARM 小主机系统是 Linux 发行版装上 ROS2 后跑底盘节点和 rosbridge。如果手头有普通 x86 工控机同样可以只要性能别太差就行。驱动板一般会提供 ROS2 驱动源码先编译一遍确认它能订阅/cmd_vel并发布轮式里程计这样才能继续往下走。ROS2 版本建议优先选一个长期支持的稳定发行版不要追新。我这次用的是 Humble理由只有一个底盘的官方驱动示例已经适配过了我不用额外改接口。你可以在终端里运行ros2 version查看版本如果还没有 ROS2 环境先按官方文档的二进制方式安装不要自己从源码编译否则第 1 期会卡在编译依赖上大半天。2.2 安装并启动 rosbridge_server装 rosbridge 很简单官方软件源里直接有包不需要自己拉源码。在 ROS2 环境已激活的前提下执行sudo apt update sudo apt install ros-humble-rosbridge-server提示如果你的 ROS2 发行版不是 Humble请把humble替换成对应的 distro 名比如jazzy就换成ros-jazzy-rosbridge-server。装完以后启动服务只需要一条命令ros2 launch rosbridge_server rosbridge_websocket_launch.py看到类似Rosbridge WebSocket server started on port 9090的日志就说明启动成功了。默认监听所有网卡端口是 9090。我建议把这条启动命令写进一个 shell 脚本方便后面一键拉起。启动完成后你可以开另一个终端输入ros2 node list能看到 rosbridge 相关节点在运行。注意它本身并不属于哪个节点而是作为网关监听消息所以就算当前没有其他节点在发消息它也会一直保持 WebSocket 服务可用。2.3 让底盘驱动先正常工作在浏览器介入前我必须先保证底盘驱动节点是通的。这个节点一般可以直接 launch 启动但需要仔细检查参数轮距、轮径、最大线速度、最大角速度、里程计参考坐标系等。比如我的底盘驱动配置里写着wheel_base0.16m、wheel_radius0.0325m、max_linear_velocity0.5m/s这些参数直接决定电机转速换算如果填错了速度会偏得离谱。启动后用命令行发一条测试消息是最快验证方式ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist {linear: {x: 0.2}, angular: {z: 0.0}}如果车轮转了说明驱动链路没问题。这时候如果 rosbridge 也已经启动浏览器就具备了与底盘通信的条件。这里要注意顺序我习惯先把驱动节点启动再启动 rosbridge最后再打开网页避免网页一旦连接就尝试发布消息而底盘还没就绪。2.4 局域网网络准备与固定 IP小车要在局域网里被浏览器访问网络必须稳定。我踩过一个大坑就是小车连接 WiFi 后 IP 会变每次换地方演示都要改一遍前端里的地址非常浪费时间。解决办法是给小车在路由器里设置 DHCP 静态绑定或者直接在系统里配置固定 IP。如果你用的是 NetworkManager可以这样改nmcli con mod 你的WiFi名称 ipv4.method manual ipv4.addresses 192.168.1.91/24 ipv4.gateway 192.168.1.1 ipv4.dns 192.168.1.1 nmcli con up 你的WiFi名称固定之后用ip addr确认生效。电脑、手机和小车必须在同一个网段浏览器连接地址就是ws://192.168.1.91:9090。另外如果本机启用了防火墙记得放行 9090 端口sudo ufw allow 9090/tcp注意这个端口只应该在可信局域网内开放不要一时图方便映射到公网。机器人控制面一旦暴露到公网安全风险非常大后面我还会专门聊安全设计。3. 网页端代码一个按钮怎么变成电机转动3.1 引入 roslibjs打通 WebSocket前端技术栈极简只有一个 HTML 文件外加官方的 roslibjs 库。我直接用官方 CDN省去下载文件script srchttps://cdn.ros.org/js/roslibjs/current/roslib.min.js/script然后在页面的 JavaScript 里创建 ROS 连接对象const ros new ROSLIB.Ros({ url: ws://192.168.1.91:9090 }); ros.on(connection, () { document.getElementById(status).textContent 已连接; }); ros.on(error, (err) { document.getElementById(status).textContent 连接错误; }); ros.on(close, () { document.getElementById(status).textContent 连接断开; });这段代码会实时显示连接状态后续所有排查都靠它。连接不上时错误信息会直接告诉你是不是网络问题或者 rosbridge 没起来。注意192.168.1.91要替换成你自己的小车 IP。3.2 发布 Twist 消息按下和松开必须成对出现定义话题对象名字和类型必须和 ROS2 端完全一致我就是用它来发布速度指令const cmdVel new ROSLIB.Topic({ ros: ros, name: /cmd_vel, messageType: geometry_msgs/msg/Twist }); function sendSpeed(x, z) { const twist new ROSLIB.Message({ linear: { x: x, y: 0.0, z: 0.0 }, angular: { x: 0.0, y: 0.0, z: z } }); cmdVel.publish(twist); } function stopCar() { sendSpeed(0, 0); }这里linear.x对应前后速度单位是米每秒angular.z对应旋转角速度单位是弧度每秒。第一次做控制页面时我最容易犯的错误是用click事件绑定按钮。click只在鼠标按下再抬起时触发一次如果我想要“按住持续前进、松开停止”的效果必须用pointerdown和pointerup并且一定要成对处理const fwdBtn document.getElementById(btn-fwd); fwdBtn.addEventListener(pointerdown, () sendSpeed(0.15, 0)); fwdBtn.addEventListener(pointerup, stopCar); fwdBtn.addEventListener(pointercancel, stopCar); fwdBtn.addEventListener(mouseleave, stopCar);pointercancel和mouseleave是为了处理“按住按钮手指/鼠标滑出按钮区域”的情况避免小车一直往前冲。如果你只用pointerdown却忘了在pointerup里发送零速度那就是典型的“车松手了还继续跑”非常危险。3.3 加一道安全保险超时自动停车即使事件绑定再完善也会有例外比如浏览器标签页失去焦点、网页崩溃、网络闪断这时候pointerup可能根本不会触发。我强烈建议在发布速度时记录时间并用一个定时器定期检查如果超过数百毫秒没有收到新的速度指令就自动发布零速度。let lastSpeedTime Date.now(); function sendSpeed(x, z) { const twist new ROSLIB.Message({ linear: { x: x, y: 0.0, z: 0.0 }, angular: { x: 0.0, y: 0.0, z: z } }); cmdVel.publish(twist); lastSpeedTime Date.now(); } setInterval(() { if (Date.now() - lastSpeedTime 300) { cmdVel.publish(new ROSLIB.Message({ linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 } })); } }, 100);这段代码类似“看门狗”页面每 100 毫秒检查一次如果超过 300 毫秒没有新指令就认为控制权已丢失主动停车。它的逻辑和工业急停很像只不过把物理继电器换成了软件定时器。对单人开发来说这种冗余不是锦上添花而是保命设计。3.4 一个完整可用的页面雏形我把第一期要用的按钮做成了前、后、左、右、急停五个大按钮页面没有多余装饰优先保证点击面积足够大。完整结构如下你可以直接保存成 HTML 文件使用!DOCTYPE html html langzh-CN head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 titleROS2 小车 Web 控制台/title script srchttps://cdn.ros.org/js/roslibjs/current/roslib.min.js/script style body { font-family: sans-serif; text-align: center; padding: 20px; } .status { margin: 10px; color: #555; } .btn { width: 120px; height: 60px; font-size: 18px; margin: 8px; border-radius: 8px; cursor: pointer; } .btn-danger { background: #c00; color: #fff; width: 180px; height: 70px; } /style /head body h2ROS2 小车控制台/h2 div idstatus classstatus未连接/div div button classbtn idbtn-fwd前进/button button classbtn idbtn-back后退/button /div div button classbtn idbtn-left左转/button button classbtn idbtn-right右转/button /div div button classbtn btn-danger idbtn-stop急停/button /div script const ros new ROSLIB.Ros({ url: ws://192.168.1.91:9090 }); ros.on(connection, () { document.getElementById(status).textContent 已连接; }); ros.on(error, () { document.getElementById(status).textContent 连接错误; }); ros.on(close, () { document.getElementById(status).textContent 连接断开; }); const cmdVel new ROSLIB.Topic({ ros: ros, name: /cmd_vel, messageType: geometry_msgs/msg/Twist }); let lastSpeedTime Date.now(); function sendSpeed(x, z) { const twist new ROSLIB.Message({ linear: { x: x, y: 0.0, z: 0.0 }, angular: { x: 0.0, y: 0.0, z: z } }); cmdVel.publish(twist); lastSpeedTime Date.now(); } function stopCar() { sendSpeed(0, 0); } function bindPress(selector, handler) { const el document.getElementById(selector); el.addEventListener(pointerdown, handler); el.addEventListener(pointerup, stopCar); el.addEventListener(pointercancel, stopCar); } bindPress(btn-fwd, () sendSpeed(0.15, 0)); bindPress(btn-back, () sendSpeed(-0.15, 0)); bindPress(btn-left, () sendSpeed(0, 0.3)); bindPress(btn-right, () sendSpeed(0, -0.3)); document.getElementById(btn-stop).addEventListener(click, stopCar); setInterval(() { if (Date.now() - lastSpeedTime 300) { cmdVel.publish(new ROSLIB.Message({ linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 } })); } }, 100); /script /body /html这里前进/后退速度给到 0.15 米每秒转向角速度给到 0.3 弧度每秒相对保守。第一次在家里试车这个速度已经足够看到明显效果又不会因为太猛撞墙。4. 第一次实测从“没反应”到“一按就动”4.1 动手之前先把车轮架空把小车放地上就直接测试是新手最容易犯的错也是坏消息制造器。地面万一有坡、有地毯或者电线一点速度变化就会被误判成“驱动坏了”。我第一次测的时候用几本书把底盘垫高让四个轮子完全悬空然后顺序执行下面三步ros2 launch my_car my_car_launch.pyros2 launch rosbridge_server rosbridge_websocket_launch.pyros2 topic echo /cmd_velros2 topic echo /cmd_vel会实时打印出的 Twist 消息。如果我在网页上点“前进”这里能看到linear.x: 0.15这条消息那么从浏览器到 rosbridge 到话题这一整条链路都是通的。轮子转不转纯看底盘驱动是否收到消息并正确换算这就把问题范围缩小了。4.2 我踩过的几个典型问题第一页面显示连接错误。我先确认小车 IP 没变然后ping一下小车 IP再检查 9090 端口是否监听。如果 rosbridge 没启动会直接报连接错误。第二页面显示已连接但点击按钮ros2 topic echo没有任何消息。优先检查前端代码里name: /cmd_vel是否和底盘驱动订阅的一致。有人会写成了cmd_vel忘了斜杠有人写成了/cmdVel都会导致消息发不到目标话题。类型也必须是geometry_msgs/msg/Twist少写一个包名都会报错。第三echo能看到消息但轮子不转。这时底盘驱动节点可能没启动或者电机开关没打开。我最开始折腾了很久最后发现只是驱动板的供电没插紧。建议先用命令行发一条ros2 topic pub --once /cmd_vel ...如果命令行能转而网页不能转就说明问题在前端如果命令行都不能转问题一定在底盘侧。第四方向是反的。按下“前进”车却后退最简单的做法是把前端速度符号取反比如sendSpeed(-0.15, 0)。我习惯取反前端因为不用动参数和重启驱动调试速度更快。但长期使用建议在驱动配置里统一方向避免其他控制端也遇到符号问题。第五按住“前进”后页面显示正常但松手后车还在继续跑。这个基本就是pointerup没触发比如使用了click事件或者按钮绑定的回调里没有再发一次零速度。结合前面说的安全超时机制这个问题就能兜底。4.3 性能表现按钮控制其实非常轻量因为我第一期只是点按按钮不是仿真摇杆所以前端并不需要高频发布消息。按键按下发一条松开发一条统计下来一秒钟最多几条消息对局域网和树莓派级别的算力来说毫无压力。实测页面连接的延迟大概在几十毫秒内体感就是“我手放开车也马上停”。大家担心的“网页控制会不会卡”在这个场景下不用担心真正高频复杂的控制在后续加虚拟摇杆和视频回传时再考虑也不迟。4.4 为什么强调先从最小链路开始一个人做改造最怕一上来就铺开所有功能摄像头、语音、导航全部堆到一起结果出了问题根本不知道从哪查起。第 1 期只做“按钮到电机”这一条链路目的就是建立一条可靠的控制基线。基线存在之后后面每加一个功能都可以快速判断问题出在新模块还是老链路上。所以如果你正在复制我这个流程请务必抵制住马上加花哨功能的冲动。5. 首期完成后的经验与安全提醒5.1 速度限制和安全设计必须有底线无论代码量多少安全机制都不能省。我把前端最大速度限制在 0.15 米每秒这看起来保守但已经足够在房间里完成基本移动测试。如果担心误触还可以把驱动节点的max_velocity参数也调低形成双重限制也就是哪怕前端传过来 1 米每秒底盘驱动也会按自己的参数限制输出速度。页面的急停按钮要够大位置要固定并且始终在页面上可见。我给急停按钮绑定了click事件触发时直接停止同时还会把页面标题变成醒目颜色方便我确认。安全超时机制也建议保留这相当于给控制端加了一层“带看门狗的控制器”一旦浏览器和 rosbridge 之间断了联系车也不会继续运行。5.2 让小车开机自启更省心调试过程中反复手动启动节点很烦。我把驱动节点和 rosbridge 写成了 systemd 服务开机自动启动这样小车通电后等十几秒网页就能连上。示例单元文件长这样[Unit] DescriptionROS2 My Car Bridge Afternetwork-online.target [Service] Typesimple ExecStart/home/ubuntu/start_car_bridge.sh Restartalways Userubuntu [Install] WantedBymulti-user.target对应的 shell 脚本里先source /opt/ros/humble/setup.bash再source自己工作空间的 setup 文件最后依次启动驱动和 rosbridge。这样配置之后再也不用担心小车被别人拿去演示时光是把环境跑起来就要花五分钟。5.3 第 1 期之后我可以继续怎么玩这一步完成以后原来的“遥控玩具”终于有了一个真正属于它的交互入口。我已经预留了扩展空间下一步打算在页面里加一个虚拟摇杆用触摸坐标实时计算线速度和角速度然后把摄像头画面通过 WebRTC 推回浏览器真正变成带第一视角的远程操控台再往后就是让网页端能切换自动驾驶和手动模式把机器人导航状态可视化。每加一个功能我都会沿用这一期的链路网页到 rosbridgerosbridge 到 ROS2 节点节点再把结果反馈回网页。我个人在实测中最深的一个体会是能点一下按钮就动起来只是机器人入门的开始但这条“开始”特别重要。它让你少了一层对“高级技术”的畏惧如果 ROS2 小车都能被一个网页按钮驯服那后面要做导航、感知、多机协同本质上也只是在同一个翻译通道上扩展更多的消息类型而已。第 1 期到此为止下一期我会把虚拟摇杆和速度调节补上让控制手感更接近手柄而不是简单的开关量。