3、ROS2基础与开发环境搭建:ROS2 Humble安装,工作空间创建,Topic/Service通信,Launch文件编写,Rviz2可视化

各位同学,欢迎来到第三章。说实话,ROS2这块内容,我当年刚接触时也踩了不少坑。尤其是从ROS1迁移过来那会儿,很多习惯都得改。但别担心,这一章我会把最实用的东西拎出来,咱们一步步搭好环境,把基础打牢。

3.1 ROS2 Humble 安装——别走弯路

ROS2的版本很多,为什么我推荐Humble?因为它是个长期支持版(LTS),稳定性好。我在项目里用过Foxy、Galactic,最后还是觉得Humble最省心。你想想看,做SLAM建图,系统稳定性是第一位的,谁也不想跑着跑着节点崩了。

安装前,先确认你的Ubuntu版本。Humble对应的是Ubuntu 22.04。如果你用的是20.04,那就装Foxy或者Galactic。别搞混了,否则依赖会出问题。

注意: 我曾经帮一个学员排查问题,折腾了两小时,最后发现他拿Ubuntu 20.04硬装Humble。嗯,这种低级错误咱们别犯。

安装步骤其实很简单,核心就这几条命令:

# 设置locale
sudo apt update && sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8

# 添加ROS2源
sudo apt install software-properties-common
sudo add-apt-repository universe
sudo apt update && sudo apt install curl -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 $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null

# 安装ROS2 Humble桌面版(推荐)
sudo apt update
sudo apt install ros-humble-desktop

# 安装开发工具
sudo apt install ros-dev-tools

装完之后,别忘了配置环境变量。我个人习惯把它写到 ~/.bashrc 里:

echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source ~/.bashrc

验证一下:ros2 --version。如果看到版本号,恭喜你,环境搭好了。

小技巧: 如果你同时装了ROS1和ROS2,建议在.bashrc里用别名区分。比如 alias ros1='source /opt/ros/noetic/setup.bash',这样切换起来方便。

3.2 工作空间创建——你的代码仓库

工作空间,说白了就是放代码的地方。ROS2里我们用的是colcon来编译,跟ROS1的catkin不太一样。我第一次用colcon时还觉得别扭,用习惯了才发现,真香。

创建一个工作空间很简单:

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
colcon build

编译完成后,你会看到 buildinstalllog 三个文件夹。记得每次编译完都要source一下:

source install/setup.bash

你也可以把它加到.bashrc里,省得每次手动敲。不过我个人建议,项目多的时候还是手动source比较好,避免环境冲突。

核心要点: 工作空间的结构是 src → build → install → log。所有你自己写的代码都放在src目录下。

3.3 Topic/Service通信——ROS2的灵魂

ROS2的通信机制,说白了就两种:Topic和Service。Topic是发布/订阅模式,Service是请求/响应模式。做SLAM时,激光雷达数据就是通过Topic发布的,而调用地图保存功能则用Service。

3.3.1 Topic通信

先看一个最简单的发布者。假设我们要发布一个字符串消息:

// talker.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

int main(int argc, char *argv[]) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared<rclcpp::Node>("talker");
    auto publisher = node->create_publisher<std_msgs::msg::String>("topic", 10);
    rclcpp::WallRate loop_rate(1);

    while (rclcpp::ok()) {
        auto msg = std_msgs::msg::String();
        msg.data = "Hello, SLAM!";
        publisher->publish(msg);
        rclcpp::spin_some(node);
        loop_rate.sleep();
    }
    rclcpp::shutdown();
    return 0;
}

对应的订阅者:

// listener.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

void topic_callback(const std_msgs::msg::String::SharedPtr msg) {
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "I heard: '%s'", msg->data.c_str());
}

int main(int argc, char *argv[]) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared<rclcpp::Node>("listener");
    auto subscription = node->create_subscription<std_msgs::msg::String>("topic", 10, topic_callback);
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

编译时别忘了在CMakeLists.txt里加上:

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

add_executable(talker src/talker.cpp)
ament_target_dependencies(talker rclcpp std_msgs)

add_executable(listener src/listener.cpp)
ament_target_dependencies(listener rclcpp std_msgs)

install(TARGETS talker listener DESTINATION lib/${PROJECT_NAME})
避坑指南: 我曾经在项目里忘记加 rclcpp::spin_some(node),结果消息发不出去。ROS2的spin机制跟ROS1不太一样,一定要记得调用。

3.3.2 Service通信

Service适合那种一问一答的场景。比如你问机器人“当前位置在哪?”,它返回一个坐标。定义Service接口:

# GetPosition.srv
float32 x
float32 y
---
float32 position_x
float32 position_y

服务端代码:

# server.py
import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger

class PositionServer(Node):
    def __init__(self):
        super().__init__('position_server')
        self.srv = self.create_service(Trigger, 'get_position', self.callback)

    def callback(self, request, response):
        response.success = True
        response.message = "x: 1.0, y: 2.0"
        return response

def main():
    rclpy.init()
    node = PositionServer()
    rclpy.spin(node)
    rclpy.shutdown()

客户端调用:

# client.py
import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger

class PositionClient(Node):
    def __init__(self):
        super().__init__('position_client')
        self.cli = self.create_client(Trigger, 'get_position')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting...')
        self.req = Trigger.Request()

    def send_request(self):
        self.future = self.cli.call_async(self.req)
        rclpy.spin_until_future_complete(self, self.future)
        return self.future.result()

def main():
    rclpy.init()
    client = PositionClient()
    response = client.send_request()
    client.get_logger().info(f'Result: {response.message}')
    rclpy.shutdown()

3.4 Launch文件编写——一键启动所有节点

做SLAM时,你往往要同时启动好几个节点:激光雷达驱动、里程计、建图算法...一个个手动敲命令?太累了。Launch文件就是干这个的。

ROS2的Launch文件用Python写,比ROS1的XML灵活多了。看个例子:

# slam_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='slam_toolbox',
            executable='async_slam_toolbox_node',
            name='slam_node',
            output='screen',
            parameters=[{'use_sim_time': False}]
        ),
        Node(
            package='rviz2',
            executable='rviz2',
            name='rviz2',
            arguments=['-d', '/path/to/config.rviz']
        )
    ])

启动方式:

ros2 launch your_package slam_launch.py
注意: Launch文件里可以嵌套include,也可以写条件判断。比如根据参数决定是否启动可视化节点。这个在调试时特别有用。

3.5 Rviz2可视化——让数据看得见

做SLAM,没有可视化工具就像闭着眼睛开车。Rviz2就是ROS2里的可视化利器。启动方式很简单:

ros2 run rviz2 rviz2

进去之后,你需要手动添加显示项。常用的有:

  • LaserScan:显示激光雷达数据
  • Map:显示建好的栅格地图
  • TF:显示坐标系变换关系
  • Path:显示机器人轨迹

我个人习惯把配置保存下来,下次直接加载。点击左上角的 File → Save Config As,保存为 .rviz 文件。然后在Launch文件里指定这个配置,省得每次重新调。

小技巧: 调试时,我经常在Rviz2里同时打开LaserScan和Map,对比原始数据和建图结果。如果发现地图有偏移,多半是TF树没配好。

好了,这一章的内容就到这里。环境搭好了,通信机制理解了,Launch文件会写了,Rviz2也能用了。下一章我们开始真正接触激光雷达数据,看看怎么把原始点云变成有用的信息。

课后练习: 自己动手写一个发布者,发布一个自定义消息(比如包含位置和姿态),然后用Rviz2的TF显示出来。试试看,不难。