第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(系统自带适配版本)

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

bash 复制代码
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话题接收运动指令,字段定义固定:

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

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

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

bash 复制代码
# 新建工作空间(已存在可跳过)
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

bash 复制代码
<?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语法校验(启动前必做)

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

bash 复制代码
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

bash 复制代码
<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完整仿真环境

bash 复制代码
cd ~/catkin_ws
source devel/setup.bash
roslaunch simple_car_sim simple_car_gazebo.launch

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

5.2 键盘遥控小车运动

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

bash 复制代码
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版本高频隐性报错

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

    bash 复制代码
    # 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话题通信状态

bash 复制代码
# 查看所有活跃话题
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

python 复制代码
#!/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

添加权限并运行:

python 复制代码
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

bash 复制代码
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话题 #雷达传感器