从零到一:用 ROS Noetic 搭建可键盘控制的 Gazebo 小车模型

ROS Noetic:从零到一搭建 Gazebo 小车模型

本文将完整记录在 Ubuntu 20.04 + ROS Noetic 环境下,从创建 URDF 模型到实现 Gazebo 物理仿真+键盘控制的全过程,适合 ROS 入门学习者参考。

一、整体方案概述

我们将完成以下核心目标:

  1. 编写小车 URDF 模型:定义底盘、车轮等部件及物理属性
  2. 配置 Gazebo 仿真环境:将小车模型加载到物理场景中
  3. 实现 C++ 键盘控制节点 :通过 /cmd_vel 话题驱动小车运动
  4. 编写 Launch 文件:一键启动可视化与仿真

二、环境准备

1. 安装依赖包

Bash 复制代码
# 核心 ROS 依赖
sudo apt-get install ros-noetic-urdf ros-noetic-robot-state-publisher ros-noetic-joint-state-publisher-gui ros-noetic-rviz
# Gazebo 仿真依赖
sudo apt-get install ros-noetic-gazebo-ros ros-noetic-gazebo-ros-pkgs ros-noetic-gazebo-plugins

2. 创建功能包

Bash 复制代码
cd ~/catkin_ws/src
catkin_create_pkg car_urdf_cpp roscpp urdf rviz geometry_msgs sensor_msgs
cd ~/catkin_ws && catkin_make
source devel/setup.bash

三、核心文件编写

1. 小车 URDF 模型(car_model.urdf

路径:~/catkin_ws/src/car_urdf_cpp/urdf/car_model.urdf

XML 复制代码
<?xml version="1.0"?>
<robot name="cpp_car">
    <!-- 虚拟根链接,避免 KDL 警告 -->
    <link name="dummy_link"/>
    <joint name="dummy_joint" type="fixed">
        <parent link="dummy_link"/>
        <child link="base_link"/>
        <origin xyz="0 0 0" rpy="0 0 0"/>
    </joint>

    <!-- 底盘 base_link -->
    <link name="base_link">
        <inertial>
            <mass value="2.0"/>
            <origin xyz="0 0 0.05" rpy="0 0 0"/>
            <inertia ixx="0.01" ixy="0.0" ixz="0.0"
                     iyy="0.02" iyz="0.0" izz="0.03"/>
        </inertial>
        <visual>
            <origin xyz="0 0 0.05" rpy="0 0 0"/>
            <geometry><box size="0.5 0.3 0.1"/></geometry>
            <material name="blue"><color rgba="0 0 1 1"/></material>
        </visual>
        <collision>
            <origin xyz="0 0 0.05" rpy="0 0 0"/>
            <geometry><box size="0.5 0.3 0.1"/></geometry>
        </collision>
    </link>

    <!-- 左前轮 -->
    <link name="left_front_wheel">
        <inertial>
            <mass value="0.2"/>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <inertia ixx="0.001" ixy="0.0" ixz="0.0"
                     iyy="0.001" iyz="0.0" izz="0.001"/>
        </inertial>
        <visual>
            <origin xyz="0 0 0" rpy="1.5708 0 0"/>
            <geometry><cylinder length="0.05" radius="0.08"/></geometry>
            <material name="black"><color rgba="0 0 0 1"/></material>
        </visual>
        <collision>
            <origin xyz="0 0 0" rpy="1.5708 0 0"/>
            <geometry><cylinder length="0.05" radius="0.08"/></geometry>
        </collision>
    </link>

    <!-- 右前轮 -->
    <link name="right_front_wheel">
        <inertial>
            <mass value="0.2"/>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <inertia ixx="0.001" ixy="0.0" ixz="0.0"
                     iyy="0.001" iyz="0.0" izz="0.001"/>
        </inertial>
        <visual>
            <origin xyz="0 0 0" rpy="1.5708 0 0"/>
            <geometry><cylinder length="0.05" radius="0.08"/></geometry>
            <material name="black"/>
        </visual>
        <collision>
            <origin xyz="0 0 0" rpy="1.5708 0 0"/>
            <geometry><cylinder length="0.05" radius="0.08"/></geometry>
        </collision>
    </link>

    <!-- 左后轮 -->
    <link name="left_back_wheel">
        <inertial>
            <mass value="0.2"/>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <inertia ixx="0.001" ixy="0.0" ixz="0.0"
                     iyy="0.001" iyz="0.0" izz="0.001"/>
        </inertial>
        <visual>
            <origin xyz="0 0 0" rpy="1.5708 0 0"/>
            <geometry><cylinder length="0.05" radius="0.08"/></geometry>
            <material name="black"/>
        </visual>
        <collision>
            <origin xyz="0 0 0" rpy="1.5708 0 0"/>
            <geometry><cylinder length="0.05" radius="0.08"/></geometry>
        </collision>
    </link>

    <!-- 右后轮 -->
    <link name="right_back_wheel">
        <inertial>
            <mass value="0.2"/>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <inertia ixx="0.001" ixy="0.0" ixz="0.0"
                     iyy="0.001" iyz="0.0" izz="0.001"/>
        </inertial>
        <visual>
            <origin xyz="0 0 0" rpy="1.5708 0 0"/>
            <geometry><cylinder length="0.05" radius="0.08"/></geometry>
            <material name="black"/>
        </visual>
        <collision>
            <origin xyz="0 0 0" rpy="1.5708 0 0"/>
            <geometry><cylinder length="0.05" radius="0.08"/></geometry>
        </collision>
    </link>

    <!-- 底盘-左前轮关节 -->
    <joint name="left_front_joint" type="continuous">
        <parent link="base_link"/>
        <child link="left_front_wheel"/>
        <origin xyz="0.2 0.15 0.05" rpy="0 0 0"/>
        <axis xyz="0 0 1"/>
        <limit effort="100" velocity="100"/>
    </joint>

    <!-- 底盘-右前轮关节 -->
    <joint name="right_front_joint" type="continuous">
        <parent link="base_link"/>
        <child link="right_front_wheel"/>
        <origin xyz="0.2 -0.15 0.05" rpy="0 0 0"/>
        <axis xyz="0 0 1"/>
        <limit effort="100" velocity="100"/>
    </joint>

    <!-- 底盘-左后轮关节 -->
    <joint name="left_back_joint" type="continuous">
        <parent link="base_link"/>
        <child link="left_back_wheel"/>
        <origin xyz="-0.2 0.15 0.05" rpy="0 0 0"/>
        <axis xyz="0 0 1"/>
        <limit effort="100" velocity="100"/>
    </joint>

    <!-- 底盘-右后轮关节 -->
    <joint name="right_back_joint" type="continuous">
        <parent link="base_link"/>
        <child link="right_back_wheel"/>
        <origin xyz="-0.2 -0.15 0.05" rpy="0 0 0"/>
        <axis xyz="0 0 1"/>
        <limit effort="100" velocity="100"/>
    </joint>

    <!-- Gazebo 物理材质 -->
    <gazebo reference="base_link">
        <material>Gazebo/Blue</material>
    </gazebo>
    <gazebo reference="left_front_wheel">
        <material>Gazebo/Black</material>
    </gazebo>
    <gazebo reference="right_front_wheel">
        <material>Gazebo/Black</material>
    </gazebo>
    <gazebo reference="left_back_wheel">
        <material>Gazebo/Black</material>
    </gazebo>
    <gazebo reference="right_back_wheel">
        <material>Gazebo/Black</material>
    </gazebo>
</robot>

2. Gazebo 启动 Launch 文件(car_gazebo.launch

路径:~/catkin_ws/src/car_urdf_cpp/launch/car_gazebo.launch

XML 复制代码
<?xml version="1.0"?>
<launch>
    <!-- 1. 启动 Gazebo 空世界 -->
    <include file="$(find gazebo_ros)/launch/empty_world.launch">
        <arg name="gui" value="true"/>
        <arg name="paused" value="false"/>
        <arg name="use_sim_time" value="true"/>
    </include>

    <!-- 2. 加载 URDF 到参数服务器 -->
    <param name="robot_description" command="cat $(find car_urdf_cpp)/urdf/car_model.urdf"/>

    <!-- 3. 发布机器人状态(TF 变换) -->
    <node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher" output="screen"/>

    <!-- 4. 将小车模型 spawn 到 Gazebo -->
    <node name="spawn_model" pkg="gazebo_ros" type="spawn_model" output="screen"
          args="-urdf -model my_car -param robot_description -x 0 -y 0 -z 0.2"/>
</launch>

3. C++ 键盘控制节点(car_teleop.cpp

路径:~/catkin_ws/src/car_urdf_cpp/src/car_teleop.cpp

C++ 复制代码
#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <cmath>

// 速度限制
#define MAX_LIN_VEL 0.5   // 最大线速度 (m/s)
#define MAX_ANG_VEL 1.0   // 最大角速度 (rad/s)
#define LIN_VEL_STEP 0.1  // 线速度步长
#define ANG_VEL_STEP 0.1  // 角速度步长

// 非阻塞检查按键
int kbhit(void) {
    struct termios oldt, newt;
    int ch;
    int oldf;

    tcgetattr(STDIN_FILENO, &oldt);
    newt = oldt;
    newt.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &newt);
    oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);

    ch = getchar();

    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
    fcntl(STDIN_FILENO, F_SETFL, oldf);

    if(ch != EOF) {
        ungetc(ch, stdin);
        return 1;
    }
    return 0;
}

// 读取按键
char getKey() {
    if(kbhit()) {
        switch(getchar()) {
            case 'w': return 'w';
            case 's': return 's';
            case 'a': return 'a';
            case 'd': return 'd';
            case 'x': return 'x';
            case 'q': return 'q';
            default: return '\0';
        }
    }
    return '\0';
}

int main(int argc, char** argv) {
    ros::init(argc, argv, "car_teleop_cpp_node");
    ros::NodeHandle nh;
    ros::Publisher vel_pub = nh.advertise<geometry_msgs::Twist>("/cmd_vel", 10);
    ros::Rate loop_rate(10);

    double linear_vel = 0.0;
    double angular_vel = 0.0;
    geometry_msgs::Twist vel_cmd;

    ROS_INFO("===== C++ 小车键盘控制 =====");
    ROS_INFO("w:前进  s:后退  a:左转  d:右转");
    ROS_INFO("x:停止  q:退出");

    while(ros::ok()) {
        char key = getKey();
        switch(key) {
            case 'w': linear_vel += LIN_VEL_STEP; angular_vel = 0.0; break;
            case 's': linear_vel -= LIN_VEL_STEP; angular_vel = 0.0; break;
            case 'a': angular_vel += ANG_VEL_STEP; break;
            case 'd': angular_vel -= ANG_VEL_STEP; break;
            case 'x': linear_vel = 0.0; angular_vel = 0.0; break;
            case 'q':
                ROS_INFO("退出控制");
                linear_vel = 0.0; angular_vel = 0.0;
                vel_cmd.linear.x = linear_vel;
                vel_cmd.angular.z = angular_vel;
                vel_pub.publish(vel_cmd);
                return 0;
            default:
                linear_vel *= 0.9;
                angular_vel *= 0.9;
                if(fabs(linear_vel) < 0.01) linear_vel = 0.0;
                if(fabs(angular_vel) < 0.01) angular_vel = 0.0;
                break;
        }

        // 限制速度范围
        linear_vel = std::max(-MAX_LIN_VEL, std::min(linear_vel, MAX_LIN_VEL));
        angular_vel = std::max(-MAX_ANG_VEL, std::min(angular_vel, MAX_ANG_VEL));

        vel_cmd.linear.x = linear_vel;
        vel_cmd.angular.z = angular_vel;
        vel_pub.publish(vel_cmd);
        loop_rate.sleep();
    }

    // 停止小车
    vel_cmd.linear.x = 0.0;
    vel_cmd.angular.z = 0.0;
    vel_pub.publish(vel_cmd);
    return 0;
}

4. CMakeLists.txt 配置

路径:~/catkin_ws/src/car_urdf_cpp/CMakeLists.txt

CMake 复制代码
cmake_minimum_required(VERSION 3.0.2)
project(car_urdf_cpp)

add_compile_options(-std=c++11)

find_package(catkin REQUIRED COMPONENTS
  geometry_msgs
  roscpp
  rviz
  sensor_msgs
  urdf
  robot_state_publisher
  joint_state_publisher
)

catkin_package(
  CATKIN_DEPENDS geometry_msgs roscpp rviz sensor_msgs urdf
)

include_directories(${catkin_INCLUDE_DIRS})

# 编译键盘控制节点
add_executable(car_teleop_node src/car_teleop.cpp)
target_link_libraries(car_teleop_node ${catkin_LIBRARIES})

# 安装可执行文件与资源
install(TARGETS car_teleop_node
  RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

install(DIRECTORY launch urdf rviz
  DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)

四、运行步骤

1. 编译工作空间

Bash 复制代码
cd ~/catkin_ws
catkin_make
source devel/setup.bash

2. 启动 Gazebo 仿真

Bash 复制代码
roslaunch car_urdf_cpp car_gazebo.launch

此时 Gazebo 界面会打开,并在场景中加载蓝色底盘+黑色车轮的小车模型。

3. 启动键盘控制节点(新开终端)

Bash 复制代码
source ~/catkin_ws/devel/setup.bash
rosrun car_urdf_cpp car_teleop_node

4. 控制小车

  • w:前进
  • s:后退
  • a:左转
  • d:右转
  • x:停止
  • q:退出控制

五、常见问题与解决

  1. URDF 解析失败 :使用 check_urdf car_model.urdf 检查语法错误,确保 Gazebo 标签格式正确。
  2. Gazebo spawn 超时:等待 Gazebo 界面完全加载后再启动 launch 文件,或调整 spawn 时的 z 轴高度(避免模型穿透地面)。
  3. 键盘无响应:确保终端窗口处于激活状态,且 Python/C++ 节点有执行权限。
  4. RViz 不显示模型 :将 Fixed Frame 设置为 dummy_link,并添加 RobotModel 显示插件。

六、总结

本文完整实现了从 URDF 建模 → Gazebo 物理仿真 → C++ 键盘控制 的小车开发流程,覆盖了 ROS 机器人建模与控制的核心知识点。通过这个案例,你可以快速理解 ROS 中模型描述、仿真环境与控制节点的协作方式,为后续更复杂的机器人开发打下基础。

相关推荐
站长工具箱2 小时前
鼠标在线测试工具:全维度检测鼠标性能,本地处理更安全
测试工具·计算机外设
开开心心就好2 小时前
免费轻量级PDF阅读器,打开速度快
windows·计算机视觉·visualstudio·pdf·计算机外设·excel·myeclipse
春日见1 天前
Matlab快速入门 基础语法教学
java·开发语言·驱动开发·matlab·docker·计算机外设
Saniffer_SH1 天前
【高清视频】AI服务器调试利器:PCIe功耗分析设备 Quarch PAM 深度解析
网络·人工智能·驱动开发·嵌入式硬件·测试工具·计算机外设·压力测试
Never_Satisfied1 天前
在JavaScript / HTML中,监听鼠标滚动事件
javascript·html·计算机外设
触想工业平板电脑一体机2 天前
【触想智能】工业触摸屏显示器的主要特点以及其应用领域分析
计算机外设
TESmart碲视3 天前
Mac+PC双系统如何共享双屏?KVM切换器选购的5个关键指标|TESmart用户真实体验复盘
macos·计算机外设·kvm切换器·tesmart·双屏kvm切换器·碲视
春日见3 天前
车载系统中的CPU与内存监管
java·开发语言·驱动开发·docker·计算机外设
深念Y4 天前
鼠标键盘按键失灵维修 微动和滚轮
计算机外设·键盘·鼠标·电子·维修·电子维修·