2、ROS2基础与环境搭建:ROS2 Humble安装、工作空间创建、节点与话题通信、服务与动作通信基础

各位同学,欢迎来到第二讲。上一章我们聊了机械臂和导航协同的整体架构,今天咱们得把地基打牢——ROS2环境搭建和基础通信机制。说实话,我见过太多项目因为环境没配好,后面调试时各种莫名其妙的问题。所以这一章,咱们一步一个脚印来。

2.1 ROS2 Humble 安装——别踩我踩过的坑

ROS2的版本很多,为什么我推荐Humble?因为它是个LTS版本,支持到2027年。做机器人项目,稳定性比什么都重要。我记得2021年有个项目用了非LTS版,半年后依赖包全变了,那叫一个酸爽。

安装前,先确认你的Ubuntu版本。Humble对应的是Ubuntu 22.04。别搞错了,否则依赖会打架。

⚠️ 重要提醒: 我建议你用一个干净的Ubuntu系统,或者至少是刚装好的虚拟机。我之前在已经装了ROS1的系统上装ROS2,结果环境变量冲突,折腾了一整天。

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

# 1. 设置编码
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

# 2. 添加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

# 3. 安装ROS2 Humble(桌面版,包含可视化工具)
sudo apt update
sudo apt install ros-humble-desktop

# 4. 设置环境变量
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source ~/.bashrc
💡 我的小习惯: 安装完成后,我会先跑一个 ros2 run demo_nodes_cpp talker 和 ros2 run demo_nodes_py listener 测试一下。如果能看到数据在跳动,说明安装成功了。这一步别跳过,我见过有人装完没验证,结果后面写代码才发现问题。

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

工作空间,说白了就是放你代码的地方。ROS2用的是colcon编译工具,和ROS1的catkin不太一样。我个人习惯把工作空间命名为 ros2_ws,简单明了。

# 创建工作空间
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
colcon build

嗯,这里要注意。第一次 colcon build 会生成 build、install、log 三个文件夹。很多人会忘记 source 环境变量:

source ~/ros2_ws/install/setup.bash

我建议把这个也加到 .bashrc 里,省得每次都要手动敲。你想想看,每次打开终端都要敲一遍,多烦人。

📌 关键点: 工作空间的结构是固定的。src 放源码,build 放编译中间文件,install 放编译结果。千万别手贱去删 src 里的东西,否则代码就没了。

2.3 节点与话题通信——机器人的"群"

节点是ROS2里最小的执行单元。一个节点可以是一个传感器驱动,也可以是一个算法模块。话题通信,就是节点之间通过"话题"来聊天。

打个比方:你有一个摄像头节点,它不断发布"图像数据"这个话题。另一个视觉处理节点订阅了这个话题,就能拿到图像数据做处理。这就是发布/订阅模式。

咱们写个简单的例子。先创建一个功能包:

cd ~/ros2_ws/src
ros2 pkg create my_pkg --build-type ament_python --dependencies rclpy std_msgs
cd ~/ros2_ws
colcon build
source install/setup.bash

然后写一个发布者节点 publisher.py:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class MyPublisher(Node):
    def __init__(self):
        super().__init__('my_publisher')
        self.publisher_ = self.create_publisher(String, 'topic', 10)
        self.timer = self.create_timer(0.5, self.timer_callback)
        self.i = 0

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello, ROS2! %d' % self.i
        self.publisher_.publish(msg)
        self.get_logger().info('Publishing: "%s"' % msg.data)
        self.i += 1

def main(args=None):
    rclpy.init(args=args)
    my_publisher = MyPublisher()
    rclpy.spin(my_publisher)
    my_publisher.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

再写一个订阅者节点 subscriber.py:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class MySubscriber(Node):
    def __init__(self):
        super().__init__('my_subscriber')
        self.subscription = self.create_subscription(
            String, 'topic', self.listener_callback, 10)

    def listener_callback(self, msg):
        self.get_logger().info('I heard: "%s"' % msg.data)

def main(args=None):
    rclpy.init(args=args)
    my_subscriber = MySubscriber()
    rclpy.spin(my_subscriber)
    my_subscriber.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()
💡 避坑指南: 我曾经在写话题通信时,发现订阅者收不到消息。查了半天,原来是话题名字写错了。ROS2的话题名是区分大小写的,topic 和 Topic 是两个不同的话题。这个坑我踩过,你们别踩。

2.4 服务通信——一问一答的模式

话题通信是"广播式"的,发布者只管发,不管有没有人收。但有些场景需要"一问一答",比如你让机械臂移动到某个位置,你得知道它到底到了没有。这时候就需要服务通信。

服务通信包含客户端和服务端。客户端发请求,服务端处理并返回响应。我举个例子,写一个简单的加法服务:

先定义服务接口文件 AddTwoInts.srv:

int64 a
int64 b
---
int64 sum

服务端代码 service_server.py:

import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts

class MyService(Node):
    def __init__(self):
        super().__init__('my_service')
        self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_callback)

    def add_callback(self, request, response):
        response.sum = request.a + request.b
        self.get_logger().info('Incoming request: a=%d b=%d' % (request.a, request.b))
        return response

def main(args=None):
    rclpy.init(args=args)
    my_service = MyService()
    rclpy.spin(my_service)
    my_service.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

客户端代码 service_client.py:

import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts

class MyClient(Node):
    def __init__(self):
        super().__init__('my_client')
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting...')
        self.req = AddTwoInts.Request()

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

def main(args=None):
    rclpy.init(args=args)
    my_client = MyClient()
    response = my_client.send_request(3, 5)
    my_client.get_logger().info('Result: %d' % response.sum)
    my_client.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()
📌 关键点: 服务通信是同步的,客户端发请求后会阻塞等待响应。这在机械臂控制中很常用,比如你发一个"移动到目标位置"的请求,服务端处理完才返回结果。但要注意,如果服务端处理时间太长,客户端会一直等,这时候可以考虑用动作通信。

2.5 动作通信——带反馈的"长任务"

动作通信是ROS2里最强大的通信机制。它结合了话题和服务的特点:你可以发送一个目标(像服务),同时可以收到进度反馈(像话题),还可以随时取消任务。

为什么需要动作?你想想看,让机械臂抓取一个物体,可能需要几秒钟。如果只用服务,客户端得一直等着,不知道进度。用动作,你可以实时看到机械臂走到哪了,甚至可以在中途叫停。

动作通信有三个角色:动作服务器、动作客户端、以及反馈话题。服务器执行任务,客户端发送目标并接收反馈。

下面是一个简单的动作服务器示例,模拟一个倒计时任务:

import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from example_interfaces.action import Fibonacci

class MyActionServer(Node):
    def __init__(self):
        super().__init__('my_action_server')
        self._action_server = ActionServer(
            self, Fibonacci, 'fibonacci', self.execute_callback)

    def execute_callback(self, goal_handle):
        self.get_logger().info('Executing goal...')
        feedback_msg = Fibonacci.Feedback()
        feedback_msg.sequence = [0, 1]
        for i in range(1, goal_handle.request.order):
            feedback_msg.sequence.append(
                feedback_msg.sequence[i] + feedback_msg.sequence[i-1])
            self.get_logger().info('Feedback: %s' % feedback_msg.sequence)
            goal_handle.publish_feedback(feedback_msg)
            time.sleep(1)
        goal_handle.succeed()
        result = Fibonacci.Result()
        result.sequence = feedback_msg.sequence
        return result

def main(args=None):
    rclpy.init(args=args)
    my_action_server = MyActionServer()
    rclpy.spin(my_action_server)
    rclpy.shutdown()

if __name__ == '__main__':
    main()
⚠️ 重要提醒: 动作通信的接口定义比话题和服务复杂,需要定义目标、反馈、结果三个部分。我建议你从官方示例开始,不要自己从头写接口文件,容易出错。

2.6 本章知识体系总览

为了让你更直观地理解ROS2的通信机制,我画了一张图。这张图展示了节点、话题、服务、动作之间的关系:

ROS2 通信机制知识体系 发布者节点 订阅者节点 话题 (Topic) 服务客户端 服务服务器 服务 (Service) 动作客户端 动作服务器 动作 (Action) 反馈 (Feedback) 话题:一对多广播 | 服务:一问一答同步 | 动作:带反馈的长任务 三种通信机制各有适用场景,机械臂协同中动作通信最常用

从这张图你可以看到,三种通信机制各有特点。话题适合传感器数据这种持续流,服务适合简单的查询,动作适合机械臂控制这种需要反馈和取消的任务。在实际项目中,我经常三种混着用。

2.7 本章小结

这一章我们走完了ROS2的入门流程。从环境搭建到工作空间创建,再到三种通信机制,每一步都是后面做机械臂和导航协同的基础。我个人觉得,理解通信机制比记住命令更重要。命令忘了可以查,但通信模型搞错了,整个系统架构都会出问题。

最后说一句,别怕犯错。我当年学ROS2时,光环境就重装了五次。每次踩坑都是成长,关键是踩完要记住。好了,这一章就到这里,咱们下一章见。

📌 核心要点回顾:
  • ROS2 Humble 安装要选对Ubuntu版本,装完一定要验证
  • 工作空间用colcon编译,记得source环境变量
  • 话题通信是发布/订阅模式,适合持续数据流
  • 服务通信是请求/响应模式,适合同步查询
  • 动作通信带反馈和取消功能,适合长时间任务

可可精品资料库