news 2026/7/2 7:32:52

第13天 ROS Noetic Gazebo差速小车从零搭建|键盘遥控、雷达数据读取、Python自主节点全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第13天 ROS Noetic Gazebo差速小车从零搭建|键盘遥控、雷达数据读取、Python自主节点全流程

第13天:ROS控制小车运动-自动

第13天:ROS控制小车运动

简介:基于Ubuntu20.04+ROS Noetic搭建完整机器人仿真平台,从零手写差速小车URDF模型,集成360°激光雷达,实现Gazebo物理仿真、键盘遥控、RViz雷达可视化、Python自主控车+雷达数据读取节点。全程无冗余操作,代码可直接复制运行,完美适配ROS入门学习、高校课程实验、机器人实训。


一、前置环境说明

1.1 软硬件环境配置

本次实战基于稳定通用的ROS仿真环境,适配绝大多数虚拟机、物理机设备:

  • 操作系统:Ubuntu 20.04 LTS

  • ROS版本:Noetic Ninjemys

  • 仿真工具:Gazebo 11(系统自带适配版本)

一键安装全部依赖库,包含仿真插件、键盘控制工具,无需逐个安装:

sudo apt install ros-noetic-desktop-full ros-noetic-gazebo-ros ros-noetic-gazebo-plugins ros-noetic-teleop-twist-keyboard

1.2 核心通信链路总览(必懂原理)

整个小车仿真运行逻辑极简,所有操作都遵循ROS话题通信机制,两条核心链路贯穿全程:

小车运动链路:键盘按键 → teleop遥控节点 → 发布/cmd_vel速度话题 → Gazebo差速驱动插件 → 计算左右轮转速 → 物理引擎驱动小车移动

雷达传感链路:Gazebo射线仿真雷达 → 激光插件解析数据 → 发布/scan雷达话题 → RViz可视化/自定义Python节点读取数据

1.3 差速小车运动数学原理

差速底盘是移动机器人最基础的底盘结构,仅依靠左右两个主动轮的转速差,即可实现前进、后退、转向,无需额外转向机构。

左轮速度 vL

右轮速度 vR

小车运动状态

v

v

直线前进

-v

-v

直线后退

v

-v

原地右转

-v

v

原地左转

核心计算公式(轮距L、小车线速度v、角速度ω):

Gazebo差速插件会自动根据该公式计算轮速,开发者只需发布线速度、角速度即可控车。

1.4 核心通信消息 Twist 详解

ROS所有地面移动机器人统一使用geometry_msgs/Twist消息,通过/cmd_vel话题接收运动指令,字段定义固定:

linear.x 前后线速度(m/s),前进为正、后退为负 linear.y/z 地面小车固定为0(无上下、平移运动) angular.z 旋转角速度(rad/s),左转正、右转负 angular.x/y 地面小车固定为0(无俯仰、横滚运动)

二、步骤1:创建仿真功能包 & 工程目录

新建ROS工作空间与仿真专用功能包,自动配置依赖,创建标准化工程目录:

# 新建工作空间(已存在可跳过) mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src # 创建仿真功能包,导入核心依赖 catkin_create_pkg simple_car_sim urdf gazebo_ros gazebo_plugins rospy # 创建标准化目录 cd simple_car_sim mkdir urdf launch worlds scripts # 编译工作空间并刷新环境 cd ~/catkin_ws && catkin_make source devel/setup.bash

目录功能说明

  • urdf:存放机器人三维模型文件

  • launch:存放仿真启动脚本

  • scripts:存放Python控车、雷达读取节点

  • worlds:存放自定义仿真场景文件


三、步骤2:编写完整URDF机器人模型

本次模型包含:基准坐标系、蓝色底盘、左右驱动轮、万向支撑轮、360°激光雷达,集成Gazebo物理仿真插件与雷达传感插件,可直接仿真运行。

文件路径:~/catkin_ws/src/simple_car_sim/urdf/simple_car.urdf

<?xml version="1.0"?> <robot name="simple_car"> <!-- ============================================================ 1. 材质定义 ============================================================ --> <material name="blue"> <color rgba="0.2 0.4 0.8 1.0"/> </material> <material name="black"> <color rgba="0.1 0.1 0.1 1.0"/> </material> <material name="gray"> <color rgba="0.6 0.6 0.6 1.0"/> </material> <!-- ============================================================ 2. base_footprint ============================================================ --> <link name="base_footprint"/> <joint name="base_joint" type="fixed"> <parent link="base_footprint"/> <child link="base_link"/> <origin xyz="0 0 0.1"/> <!-- 修复:抬高 = 轮子半径 0.05 + 车体半高 0.05 = 0.1m 确保车体在轮子正确支撑下,底面恰好在轮子顶端 --> </joint> <!-- ============================================================ 3. base_link ============================================================ --> <link name="base_link"> <visual> <geometry> <box size="0.3 0.2 0.1"/> </geometry> <material name="blue"/> </visual> <collision> <geometry> <box size="0.3 0.2 0.1"/> </geometry> </collision> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="1.0"/> <inertia ixx="0.0058" ixy="0" ixz="0" iyy="0.0108" iyz="0" izz="0.0158"/> </inertial> </link> <!-- ============================================================ 4. 左驱动轮 ============================================================ --> <link name="left_wheel"> <visual> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.05" length="0.04"/> </geometry> <material name="black"/> </visual> <collision> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.05" length="0.04"/> </geometry> </collision> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="0.2"/> <inertia ixx="0.000183" ixy="0" ixz="0" iyy="0.000183" iyz="0" izz="0.00025"/> </inertial> </link> <joint name="left_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="left_wheel"/> <!-- base_link 原点在车体中心(z方向) 轮子需要在车体底面(z = -0.05)处,让轮轴与地面平行 rpy="1.5708 0 0":绕X轴转90°,圆柱竖轴变为Y轴方向(即横向轮子) --> <origin xyz="-0.05 0.12 -0.05" rpy="1.5708 0 0"/> <!-- 旋转轴:在子坐标系(已绕X转90°)中,z轴 = 全局Y轴方向,即轮轴方向 ✅ --> <axis xyz="0 0 -1"/> <!-- 修复:统一改为 0 0 -1,方向由 rpy 决定,左右轮对称由 y 坐标正负保证 --> </joint> <!-- ============================================================ 5. 右驱动轮 ============================================================ --> <link name="right_wheel"> <visual> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.05" length="0.04"/> </geometry> <material name="black"/> </visual> <collision> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.05" length="0.04"/> </geometry> </collision> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="0.2"/> <inertia ixx="0.000183" ixy="0" ixz="0" iyy="0.000183" iyz="0" izz="0.00025"/> </inertial> </link> <joint name="right_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="right_wheel"/> <origin xyz="-0.05 -0.12 -0.05" rpy="1.5708 0 0"/> <axis xyz="0 0 -1"/> <!-- 修复:与左轮统一为 0 0 -1,差速驱动插件内部会处理左右轮的正反转 --> </joint> <!-- ============================================================ 6. 前万向轮 ============================================================ --> <link name="caster_wheel"> <visual> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <sphere radius="0.025"/> </geometry> <material name="gray"/> </visual> <collision> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <sphere radius="0.025"/> </geometry> <!-- 修复:<surface> 不属于 URDF,移到 <gazebo reference> 里 --> </collision> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="0.1"/> <inertia ixx="0.000010" ixy="0" ixz="0" iyy="0.000010" iyz="0" izz="0.000010"/> </inertial> </link> <joint name="caster_joint" type="fixed"> <parent link="base_link"/> <child link="caster_wheel"/> <!-- 修复:万向轮球心 z = -(车体半高 + 球半径) = -(0.05 + 0.025) = -0.075 这样球体最低点 = -0.075 - 0.025 = -0.1,与驱动轮最低点一致 ✅ --> <origin xyz="0.1 0 -0.075"/> </joint> <!-- ============================================================ 7. 激光雷达 ============================================================ --> <link name="laser_link"> <visual> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.04" length="0.04"/> </geometry> <material name="gray"/> </visual> <collision> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.04" length="0.04"/> </geometry> </collision> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="0.1"/> <inertia ixx="0.000033" ixy="0" ixz="0" iyy="0.000033" iyz="0" izz="0.000080"/> </inertial> </link> <joint name="laser_joint" type="fixed"> <parent link="base_link"/> <child link="laser_link"/> <origin xyz="0 0 0.07"/> </joint> <!-- ============================================================ 8. Gazebo 插件:差速驱动 ============================================================ --> <gazebo> <plugin name="diff_drive_controller" filename="libgazebo_ros_diff_drive.so"> <leftJoint>left_wheel_joint</leftJoint> <rightJoint>right_wheel_joint</rightJoint> <wheelSeparation>0.24</wheelSeparation> <wheelDiameter>0.1</wheelDiameter> <!-- 修复:添加 wheelTorque 限制最大输出扭矩,防止速度阶跃 --> <wheelTorque>5.0</wheelTorque> <!-- 修复:加速度限制,平滑速度变化 --> <wheelAcceleration>1.0</wheelAcceleration> <commandTopic>cmd_vel</commandTopic> <odometryTopic>odom</odometryTopic> <odometryFrame>odom</odometryFrame> <robotBaseFrame>base_footprint</robotBaseFrame> <updateRate>30</updateRate> <publishTf>1</publishTf> <publishWheelTF>false</publishWheelTF> <publishOdomTF>true</publishOdomTF> <!-- 关键:显式开启关节状态发布 --> <publishWheelJointState>true</publishWheelJointState> </plugin> </gazebo> <!-- ============================================================ 9. Gazebo 插件:激光雷达 ============================================================ --> <gazebo reference="laser_link"> <sensor type="ray" name="lidar_sensor"> <pose>0 0 0 0 0 0</pose> <visualize>true</visualize> <update_rate>10</update_rate> <ray> <scan> <horizontal> <samples>360</samples> <resolution>1</resolution> <min_angle>-3.14159</min_angle> <max_angle> 3.14159</max_angle> </horizontal> </scan> <range> <min>0.12</min> <max>10.0</max> <resolution>0.01</resolution> </range> <noise> <type>gaussian</type> <mean>0.0</mean> <stddev>0.01</stddev> </noise> </ray> <plugin name="lidar_plugin" filename="libgazebo_ros_laser.so"> <topicName>scan</topicName> <frameName>laser_link</frameName> </plugin> </sensor> </gazebo> <!-- ============================================================ 10. Gazebo 材质 & 摩擦力属性 ============================================================ --> <gazebo reference="base_link"> <material>Gazebo/Blue</material> </gazebo> <gazebo reference="left_wheel"> <material>Gazebo/Black</material> <mu1>1.0</mu1> <mu2>1.0</mu2> <!-- 轮子与地面接触的最大横向修正速度,减少数值抖动 --> <maxVel>1.0</maxVel> <minDepth>0.001</minDepth> </gazebo> <gazebo reference="right_wheel"> <material>Gazebo/Black</material> <mu1>1.0</mu1> <mu2>1.0</mu2> <maxVel>1.0</maxVel> <minDepth>0.001</minDepth> </gazebo> <!-- 修复:万向轮摩擦力必须在 <gazebo reference> 里设置才生效 --> <gazebo reference="caster_wheel"> <material>Gazebo/Grey</material> <mu1>0.0</mu1> <mu2>0.0</mu2> <maxVel>0.1</maxVel> <minDepth>0.001</minDepth> </gazebo> <gazebo reference="laser_link"> <material>Gazebo/Grey</material> </gazebo> </robot>

URDF语法校验(启动前必做)

提前检测模型语法错误,规避启动崩溃问题:

cd ~/catkin_ws/src/simple_car_sim/urdf check_urdf simple_car.urdf

输出Successfully Parsed XML即为正常;若报错,检查标签闭合、参数格式即可。


四、步骤3:编写仿真启动Launch文件

Launch文件一键启动Gazebo、加载机器人模型、生成仿真小车、发布TF坐标,无需逐个启动节点。

文件路径:~/catkin_ws/src/simple_car_sim/launch/simple_car_gazebo.launch

<launch> <!-- 将URDF模型加载到ROS参数服务器 --> <param name="robot_description" command="cat '$(find simple_car_sim)/urdf/simple_car.urdf'" /> <!-- 启动Gazebo空白仿真世界,启用仿真时间 --> <include file="$(find gazebo_ros)/launch/empty_world.launch"> <arg name="paused" value="false"/> <arg name="use_sim_time" value="true"/> <arg name="gui" value="true"/> </include> <!-- 在Gazebo原点生成机器人模型 --> <node name="spawn_urdf" pkg="gazebo_ros" type="spawn_model" args="-urdf -model simple_car -param robot_description -x 0 -y 0 -z 0.1" output="screen"/> <!-- 发布机器人TF坐标变换,为RViz可视化提供支撑 --> <node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher" output="screen"> <param name="publish_frequency" value="30.0"/> </node> </launch>

五、步骤4:启动仿真 & 基础功能测试

5.1 启动Gazebo完整仿真环境

cd ~/catkin_ws source devel/setup.bash roslaunch simple_car_sim simple_car_gazebo.launch

首次启动耗时30秒左右,加载完成后可见:Gazebo窗口中出现蓝色小车,车顶雷达带有绿色激光射线。

5.2 键盘遥控小车运动

新建终端,执行命令启动键盘控制节点:

source ~/catkin_ws/devel/setup.bash rosrun teleop_twist_keyboard teleop_twist_keyboard

按键说明(重点):必须鼠标选中该终端窗口,按键才生效

  • i:前进、,:后退

  • j:原地左转、l:原地右转

  • k:紧急停止

  • q/z:增大/减小全局运动速度

  • 问题1:teleop_twist_keyboard 可执行文件找不到(精准报错解决)

  • 报错现象:执行rosrun teleop_twist_keyboard teleop_twist_keyboard,提示Couldn't find executable named teleop_twist_keyboard,仅检索到文件夹无可执行文件

  • 报错原因:依赖安装不完整、软件包未编译、环境变量未刷新,是Noetic版本高频隐性报错

  • 终极解决命令(逐条执行)

    # 1. 重新安装完整键盘控制依赖(覆盖缺失文件) sudo apt install --reinstall ros-noetic-teleop-twist-keyboard # 2. 刷新ROS全局环境变量 source /opt/ros/noetic/setup.bash # 3. 回到工作空间刷新本地环境 cd ~/catkin_ws source devel/setup.bash # 4. 直接运行(无需编译,官方包自带可执行文件) rosrun teleop_twist_keyboard teleop_twist_keyboard
  • 彻底根治方案:若依旧报错,说明本地源码冲突,删除冗余源码文件夹~/xxx_ws/src/teleop_twist_keyboard,再重新安装依赖即可。

5.3 查看ROS话题通信状态

# 查看所有活跃话题 rostopic list # 实时打印小车速度指令 rostopic echo /cmd_vel # 实时打印激光雷达测距数据 rostopic echo /scan # 查看话题发布频率 rostopic hz /scan rostopic hz /cmd_vel

5.4 RViz可视化小车与雷达点云

新建终端输入rviz打开可视化工具,按以下步骤配置:

  1. 将左侧Fixed Frame修改为base_footprint

  2. 点击左下角 Add,添加RobotModel,显示小车三维模型

  3. 再次 Add,添加LaserScan,Topic选择/scan

  4. 将LaserScan的Size参数改为0.05,放大雷达点云,更清晰

操控小车移动,可实时观察RViz中雷达点云随障碍物变化。


六、步骤5:自主编写Python功能节点

6.1 小车自动画圆节点(自主控车)

原理:固定线速度+角速度,让小车持续做圆周运动,理解速度话题发布逻辑。

文件路径:simple_car_sim/scripts/move_circle.py

#!/usr/bin/env python3 """ move_circle.py 让小车以固定的线速度和角速度做圆周运动。 目的:理解 cmd_vel 的发布方式。 """ import rospy from geometry_msgs.msg import Twist def main(): # 初始化节点 rospy.init_node('move_circle', anonymous=True) # 创建发布者,发布到 /cmd_vel 话题 # queue_size=10 表示消息队列最多缓存10条 pub = rospy.Publisher('/cmd_vel', Twist, queue_size=10) # 发布频率:10Hz(每秒发10次指令) rate = rospy.Rate(10) rospy.loginfo("小车开始画圆,按 Ctrl+C 停止...") while not rospy.is_shutdown(): # 构造 Twist 消息 msg = Twist() # 线速度:向前 0.3 m/s msg.linear.x = 0.3 # 角速度:绕 z 轴 0.5 rad/s(左转) # 转弯半径 r = v / ω = 0.3 / 0.5 = 0.6 m msg.angular.z = 0.5 # 发布消息 pub.publish(msg) # 按照设定频率休眠 rate.sleep() # 节点退出时发送停止指令 rospy.loginfo("停止小车...") stop_msg = Twist() # 默认全0,即停止 pub.publish(stop_msg) if __name__ == '__main__': try: main() except rospy.ROSInterruptException: pass

添加权限并运行:

chmod +x ~/catkin_ws/src/simple_car_sim/scripts/move_circle.py rosrun simple_car_sim move_circle.py

6.2 激光雷达数据读取节点

原理:订阅/scan话题,解析雷达数据,打印小车前、左、右三方障碍物距离。

文件路径:simple_car_sim/scripts/read_laser.py

import rospy from sensor_msgs.msg import LaserScan def laser_callback(msg): """ 每收到一帧雷达数据就调用这个函数。 msg.ranges 是一个列表,包含 360 个距离值。 索引 0 对应正前方(0°),索引 90 对应左方(90°),以此类推。 注意:inf 表示该方向没有测到障碍物(超过最大量程)。 """ # 取正前方(索引0)的距离 front_dist = msg.ranges[0] # 取左方(索引90)的距离 left_dist = msg.ranges[90] # 取右方(索引270,即-90°)的距离 right_dist = msg.ranges[270] rospy.loginfo( f"前方: {front_dist:.2f}m | 左方: {left_dist:.2f}m | 右方: {right_dist:.2f}m" ) def main(): rospy.init_node('read_laser', anonymous=True) # 订阅 /scan 话题,每次收到消息调用 laser_callback rospy.Subscriber('/scan', LaserScan, laser_callback) rospy.loginfo("开始读取激光雷达数据...") # spin() 让节点保持运行,持续接收回调 rospy.spin() if __name__ == '__main__': try: main() except rospy.ROSInterruptException: pass chmod +x ~/catkin_ws/src/simple_car_sim/scripts/read_laser.py rosrun simple_car_sim read_laser.py

添加权限并运行:

chmod +x ~/catkin_ws/src/simple_car_sim/scripts/read_laser.py rosrun simple_car_sim read_laser.py

七、整体通信数据流总结

7.1 小车运动数据流

键盘输入 / Python节点 → 发布/cmd_vel(Twist)→ 差速驱动插件 → 计算左右轮速 → Gazebo物理引擎驱动小车运动

7.2 雷达传感数据流

Gazebo射线仿真采集距离 → 激光插件解析数据 → 发布/scan(LaserScan)→ RViz可视化 / 自定义节点数据读取


八、常见报错与解决方案(全覆盖)

  • 问题1:spawn_urdf 模型生成失败:URDF文件路径错误,核对$(find)功能包名与文件路径一致

  • 问题2:robot_state_publisher 进程闪退:所有link标签缺少<inertial>惯性参数,本文代码已修复该问题

  • 问题3:RViz不显示雷达点云:Fixed Frame设置为base_footprint,手动选择话题/scan

  • 问题4:键盘控制无响应:鼠标点击键盘控制终端,激活窗口焦点

  • 问题5:Gazebo启动卡顿、缓慢:首次启动加载资源属于正常现象,关闭多余后台程序即可


九、项目拓展学习方向

  1. 新增摄像头URDF模型,实现机器人视觉仿真

  2. 基于雷达数据编写自主避障、循迹行走算法

  3. 订阅/odom里程计话题,实现小车坐标定位

  4. 使用Xacro拆分URDF文件,实现机器人模型模块化搭建

  5. 添加仿真障碍物场景,完成自主导航实训


文末福利:本文全套可运行工程文件、完整源码已整理完毕,适合ROS入门实训、期末课程设计、机器人仿真作业,直接复制即可运行,无报错、无删减!

标签#ROS #Gazebo仿真 #差速小车 #URDF建模 #机器人仿真 #Twist话题 #雷达传感器

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 7:31:46

2026深圳国际物流公司甄选指南,靠谱推荐看这里

做外贸最怕什么&#xff1f;货到了港口&#xff0c;物流公司说“柜子被甩了”“船期延误了”“清关出了问题”。尤其在深圳这个全球贸易港口&#xff0c;每天数万个集装箱进出&#xff0c;选错物流公司&#xff0c;轻则多花冤枉钱&#xff0c;重则丢失客户、赔掉订单。前阵子一…

作者头像 李华
网站建设 2026/7/2 7:30:50

CycleGAN实现OCR图像去噪:无需配对数据的文档预处理方案

1. 项目概述&#xff1a;为什么用CycleGAN给OCR图像“洗照片”你有没有遇到过这样的场景&#xff1a;手头有一堆扫描件、老文档翻拍图、手机随手拍的发票或合同&#xff0c;字迹模糊、背景发灰、纸张褶皱、光照不均——拿去OCR识别&#xff0c;结果错字连篇&#xff0c;标点全丢…

作者头像 李华
网站建设 2026/7/2 7:24:38

Python与Fluke 8808A通信:实时获取电流电压值、绘制曲线并保存CSV数据

1. 项目概述 本文将详细介绍如何使用Python与Fluke 8808A数字万用表进行通信&#xff0c;实现&#xff1a; 实时数据采集&#xff1a;通过SCPI命令获取电流和电压测量值实时曲线显示&#xff1a;使用Matplotlib动态绘制数据变化曲线数据持久化&#xff1a;将采集的数据实时保存…

作者头像 李华
网站建设 2026/7/2 7:24:38

如何高效获取京东商品详情数据

一、方案选型&#xff1a;三种主流获取方式对比 想要稳定、高效拿到京东完整商品详情&#xff08;标题、SKU、实时价格、库存、参数、图文、促销&#xff09;&#xff0c;行业分三类方案&#xff0c;从合规、稳定性、开发成本区分优先级。 官方开放 API&#xff08;长期业务首…

作者头像 李华
网站建设 2026/7/2 7:23:00

DBeaver SQL编辑器实战:5类高频SQL模板+3级自动补全配置指南

1. 为什么你写的 SQL 总是“差点意思”?——DBeaver 里那层看不见的 AI 隔膜 我接手过三个不同团队的 SQL 审查任务,发现一个高度一致的现象:90% 的低效查询、冗余 JOIN、漏写 WHERE 条件、甚至字段类型误用,都不是因为开发者不会写 SQL,而是因为他们在 DBeaver 里敲完 S…

作者头像 李华