从零搭一套ROS 2仿真监控系统
之前两篇写的都是桌面工具和安卓App,这次想试试不一样的东西。
我对ROS 2和机器人仿真基本是零基础,之前没跑过Gazebo,也没写过ROS节点。这次的目标很简单:在虚拟机里把ROS 2环境跑起来,让Gazebo里的小车动起来,然后把位置数据取出来,用网页显示一下。不碰真实硬件,全程在虚拟机上完成。
这篇文章就是我的完整操作记录,从装系统开始,到网页上看到数据更新结束。我不是什么高手,每一步都是现查现试的,遇到什么问题就记录什么。如果你也刚开始接触ROS 2,卡在环境搭建或者不知道下一步该做什么,这篇应该能帮你省点时间。
环境
系统:Ubuntu 24.04 LTS
虚拟机:VMware Workstation
ROS版本:Jazzy(Ubuntu 24.04默认推荐版本)
仿真:Gazebo + TurtleBot3
第一步:装ROS 2
先更新系统,添加ROS的软件源,导入密钥,然后安装ROS 2桌面版和编译工具。
sudo apt update && sudo apt upgrade -y
sudo apt install -y software-properties-common curl
sudo add-apt-repository universe -y
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg –print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
sudo apt update
sudo apt install -y ros-jazzy-desktop python3-colcon-common-extensions
安装完成后验证一下:
source /opt/ros/jazzy/setup.bash
ros2 run demo_nodes_cpp talker
终端里开始刷Hello World,说明ROS 2本身是能用的。Ctrl+C关掉,继续下一步。
第二步:装Gazebo和TurtleBot3
装仿真环境,把Gazebo和TurtleBot3的仿真包一并装好。
sudo apt install -y ros-jazzy-ros-gz-sim ros-jazzy-turtlebot3-simulations
然后启动Gazebo,看能不能弹出仿真窗口:
source /opt/ros/jazzy/setup.bash
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py
第一次启动的时候,Gazebo窗口弹出来了,但画面一直在闪,根本没法看。后来查到是虚拟机显卡渲染的问题,在启动命令前加上环境变量强制软件渲染,频闪就解决了。
export LIBGL_ALWAYS_SOFTWARE=1
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py
画面稳定之后,Ctrl+C关掉,继续下一步。
第三步:创建工作区和功能包
Gazebo能正常跑起来之后,开始写自己的ROS节点。
建一个工作区,在里面用ros2 pkg create创建一个Python功能包,取名sim_bridge_new:
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create sim_bridge_new –build-type ament_python –dependencies rclpy std_msgs geometry_msgs
这个包的作用是模拟产生位置数据,然后通过HTTP发出去。包的结构是ROS 2标准的Python包布局,主要就是一个Python节点文件加上setup.py里的入口配置。
第四步:写ROS节点
在功能包目录下创建data_publisher.py:
cat > ~/ros2_ws/src/sim_bridge_new/sim_bridge_new/data_publisher.py << 'EOF'
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
import requests
import time
import math
class DataPublisher(Node):
def __init__(self):
super().__init__('data_publisher')
self.flask_url = 'http://localhost:5000/api/data'
self.t = 0.0
self.timer = self.create_timer(0.1, self.timer_callback)
self.get_logger().info('Data Publisher started (simulated motion)')
def timer_callback(self):
self.t += 0.1
x = math.sin(self.t) * 2.0
y = math.cos(self.t) * 2.0
data = {'x': x, 'y': y, 'timestamp': time.time()}
try:
requests.post(self.flask_url, json=data, timeout=0.5)
except:
pass
def main(args=None):
rclpy.init(args=args)
node = DataPublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
EOF
这个节点的逻辑很简单:
初始化ROS节点
创建一个定时器,每0.1秒触发一次
每次触发时计算当前的x和y坐标(走圆形轨迹)
用requests.post把数据发给本地的Flask后端
这个节点不订阅任何话题,纯粹是模拟数据源。为什么不直接订阅/odom?因为我想先把数据流打通,后面再接真实话题,那样改起来更方便。
然后修改setup.py里的入口点配置:
cat > ~/ros2_ws/src/sim_bridge_new/setup.py << 'EOF'
from setuptools import setup
import os
from glob import glob
package_name = 'sim_bridge_new'
setup(
name=package_name,
version='0.0.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='bologna',
maintainer_email='bologna@todo.todo',
description='ROS 2 data publisher for cloud monitoring',
license='MIT',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'data_publisher = sim_bridge_new.data_publisher:main',
],
},
)
EOF
第五步:装Python依赖
安装Flask和相关依赖的时候报了个错,Ubuntu 24.04默认的Python环境有“外部管理”限制,直接用pip3 install会拒绝安装。
pip3 install flask flask-cors requests
报错之后查了下,需要用–break-system-packages参数绕过去:
pip3 install flask flask-cors requests –break-system-packages
成功装上。
第六步:写Flask后端
在~/ros_cloud_demo/cloud_backend目录下创建app.py:
mkdir -p ~/ros_cloud_demo/cloud_backend
cat > ~/ros_cloud_demo/cloud_backend/app.py << 'EOF'
#!/usr/bin/env python3
from flask import Flask, request, jsonify
from flask_cors import CORS
from collections import deque
import time
app = Flask(__name__)
CORS(app)
history = deque(maxlen=100)
latest = {}
@app.route('/api/data', methods=['POST'])
def receive_data():
global latest
data = request.get_json()
if not data:
return jsonify({'error': 'no data'}), 400
data['server_time'] = time.time()
history.append(data)
latest = data
return jsonify({'status': 'ok'}), 200
@app.route('/api/latest', methods=['GET'])
def get_latest():
if latest:
return jsonify(latest)
return jsonify({'error': 'no data'}), 404
@app.route('/api/history', methods=['GET'])
def get_history():
return jsonify(list(history))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
EOF
写了三个接口:
/api/data:POST接口,接收ROS节点发来的数据,存到一个队列里(只保留最近100条)
/api/latest:GET接口,返回最新一条数据
/api/history:GET接口,返回所有历史数据
用flask-cors解决了跨域问题,方便前端直接调用。
第七步:写前端看板
在~/ros_cloud_demo/web_dashboard目录下创建index.html:
mkdir -p ~/ros_cloud_demo/web_dashboard
cat > ~/ros_cloud_demo/web_dashboard/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ROS监控</title>
<style>
body { background: #0d1117; color: #c9d1d9; font-family: monospace; padding: 20px; }
.data { font-size: 24px; padding: 10px; background: #161b22; border-radius: 8px; margin: 5px 0; }
.label { color: #58a6ff; }
#history { margin-top: 20px; font-size: 12px; max-height: 400px; overflow-y: scroll; }
</style>
</head>
<body>
<h1>仿真小车实时数据</h1>
<div class="data"><span class="label">X:</span> <span id="x">–</span></div>
<div class="data"><span class="label">Y:</span> <span id="y">–</span></div>
<div class="data"><span class="label">时间:</span> <span id="t">–</span></div>
<div id="history"></div>
<script>
function fetchData() {
fetch('http://localhost:5000/api/latest')
.then(r => r.json())
.then(d => {
document.getElementById('x').textContent = d.x?.toFixed(6) || '–';
document.getElementById('y').textContent = d.y?.toFixed(6) || '–';
document.getElementById('t').textContent = new Date(d.server_time * 1000).toLocaleTimeString();
})
.catch(() => {});
fetch('http://localhost:5000/api/history')
.then(r => r.json())
.then(data => {
const div = document.getElementById('history');
div.innerHTML = data.slice(-20).reverse().map(d =>
`X: ${d.x?.toFixed(6)} Y: ${d.y?.toFixed(6)} ${new Date(d.server_time * 1000).toLocaleTimeString()}`
).join('<br>');
})
.catch(() => {});
}
fetchData();
setInterval(fetchData, 1000);
</script>
</body>
</html>
EOF
纯静态页面,用fetch轮询后端的/api/latest和/api/history接口。
页面分成两部分:上面显示最新的X、Y坐标和时间戳,下面滚动显示最近20条历史记录。刷新间隔设成了1秒。
在浏览器里打开这个HTML文件后,能看到数据在不断更新。为了验证数据流是否真正跑通,我在终端里同时观察ROS节点的输出和Flask的接收日志,确认每个环节都在正常工作。
第八步:编译工作区
在启动之前,先编译工作区:
cd ~/ros2_ws
colcon build –packages-select sim_bridge_new
source install/setup.bash
第九步:启动全流程
全部写完后,需要同时启动4个终端:
终端启动内容
终端1Gazebo仿真环境
终端2ROS节点
终端3Flask后端
终端4前端看板
终端1 – Gazebo:
export LIBGL_ALWAYS_SOFTWARE=1
source /opt/ros/jazzy/setup.bash
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py
终端2 – ROS节点:
cd ~/ros2_ws
source install/setup.bash
ros2 run sim_bridge_new data_publisher
终端3 – Flask后端:
cd ~/ros_cloud_demo/cloud_backend
python3 app.py
终端4 – 前端看板:
firefox ~/ros_cloud_demo/web_dashboard/index.html
启动顺序很重要。如果Flask没起来就启动了ROS节点,节点会报连接失败。所以我是先Gazebo,再Flask,再ROS节点,最后开浏览器。
全部跑通之后,页面上能看到X、Y坐标以圆形轨迹规律变化,历史记录也在不断刷新。整个数据链从仿真到前端完整跑通,耗时约两天,大部分时间花在环境适配和踩坑上。
遇到的主要问题
虚拟机里启动Gazebo画面闪烁严重,几乎无法观察。解决方法是在启动命令前加环境变量强制软件渲染
export LIBGL_ALWAYS_SOFTWARE=1
setup.py的entry_points配置必须和实际文件路径一致,否则ros2 run会报找不到可执行文件。检查entry_points里的data_publisher = sim_bridge_new.data_publisher:main是否跟实际文件路径sim_bridge_new/data_publisher.py匹配。


