ROS 2从零构建自主移动机器人 (1)

本系列是关于使用ROS 2 Humble和Gazebo构建生产级AMR(自主移动机器人)的10部分教程的第一部分——从绝对零基础到完全自主导航的机器人。

ROS 2从零构建自主移动机器人 (1)
微信 ezpoda免费咨询:AI编程 | AI模型微调| AI私有化部署
AI工具导航 | ONNX模型库 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo

大多数ROS 2教程给你一个能运行的机器人代码然后就结束了。你复制代码,它能运行,但你不知道为什么。当出现问题时——比如错误的坐标系名称、缺失的主题、导致Gazebo崩溃的惯性值——你完全束手无策。

这个系列不同。每个概念都从第一性原理解释。每行代码都有其理由。到最后,你不仅会有一个能运行的AMR——你还会足够深入地理解它,从而能够构建一个真正的机器人。

你将构建什么: 一个模拟的差速驱动AMR,能够映射环境、定位自身,并自主地从A点导航到B点同时避开障碍物。所有内容都在ROS 2 Humble和Gazebo Classic上运行,基于Ubuntu 22.04。

适合谁: 了解Python和Linux基础知识,想要构建严肃的机器人系统而不仅仅是跟着教程走的工程师。

1、ROS 2到底是什么

大多数介绍说"ROS 2是中间件"然后就结束了。让我们更精确一些。

你正在构建一个AMR。在任何给定时刻,你有:

  • 一个激光雷达以10Hz的频率产生360个距离读数
  • 一个电机控制器以50Hz的频率等待速度指令
  • 一个SLAM算法消费激光雷达数据并生成地图
  • 一个导航算法消费地图并生成速度指令

这些都是完全独立的进程。它们可能用不同的语言编写。在真实的生产机器人上,它们可能在不同的计算机上运行。它们需要快速、可靠地通信,而无需了解彼此的内部实现。

ROS 2通过基于DDS(数据分发服务)发布-订阅通信模型来解决这个问题——这是军事和自动驾驶系统中使用的相同协议,而不是业余爱好者框架。

关键的架构洞察:节点是完全解耦的。 激光雷达驱动程序不知道SLAM的存在。SLAM不知道Nav2的存在。如果SLAM崩溃,激光雷达继续运行。如果你想更换SLAM算法,只需替换一个节点——其他什么都不变。这正是生产级AMR(Amazon Kiva、Boston Dynamics Spot、医院配送机器人)这样构建的原因。

2、安装

先决条件: Ubuntu 22.04,至少4GB内存,20GB磁盘空间。

# 设置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

# 添加ROS 2仓库
sudo apt install software-properties-common curl
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

# 安装ROS 2 Humble Desktop Full
sudo apt update
sudo apt install ros-humble-desktop-full

# 安装构建工具
sudo apt install python3-colcon-common-extensions \
python3-rosdep \
ros-humble-gazebo-ros-pkgs

# 在每个终端中source——添加到~/.bashrc
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source ~/.bashrc

验证安装:

ros2 --help
# 应该打印完整的ros2 CLI帮助——如果这能工作,ROS 2就安装好了

3、工作空间——实际发生了什么

mkdir -p ~/amr_ws/src
cd ~/amr_ws
colcon build
source install/setup.bash
echo "source ~/amr_ws/install/setup.bash" >> ~/.bashrc

大多数人运行colcon build然后等待。以下是它实际做的:

~/amr_ws/
├── src/ ← 你在这里写代码
├── build/ ← colcon的工作目录(永远不要碰这个)
├── install/ ← 真正的输出——ROS 2从这里读取,而不是src/
└── log/ ← 构建日志——构建失败时查看这里

添加到~/.bashrc意味着每个新终端自动看到你的工作空间。

4、创建你的第一个包

cd ~/amr_ws/src
ros2 pkg create --build-type ament_python amr_basics \
--dependencies rclpy std_msgs geometry_msgs

这会创建包的脚手架。有两个文件定义每个ROS 2包:

package.xml —— 声明这个包需要什么来构建和运行:

<?xml version="1.0"?>
<package format="3">
  <name>amr_basics</name>
  <version>0.0.1</version>
  <description>AMR基础——发布者、订阅者示例</description>
  <maintainer email="you@example.com">Your Name</maintainer>
  <license>Apache-2.0</license>
  
  <buildtool_depend>ament_python</buildtool_depend>
  
  <depend>rclpy</depend>
  <depend>geometry_msgs</depend>
  
  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

setup.py —— 将你的Python脚本注册为可执行的ROS 2节点:

from setuptools import setup

package_name = 'amr_basics'

setup(
    name=package_name,
    version='0.0.1',
    packages=[package_name],
    install_requires=['setuptools'],
    entry_points={
        'console_scripts': [
            # 'command_name = package.module:function'
            'velocity_publisher = amr_basics.velocity_publisher:main',
            'velocity_subscriber = amr_basics.velocity_subscriber:main',
        ],
    },
)

5、节点——计算单元

ROS 2节点是一个带有名称的进程,运行在ROS 2网络中。这里是一个发布者节点,发送速度指令——你的整个导航栈将使用的相同消息类型:

amr_basics/amr_basics/velocity_publisher.py

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist

class VelocityPublisher(Node):
    def __init__(self):
        # 在DDS网络中将此节点注册为'velocity_publisher'
        # 其他节点现在可以通过ros2 node list发现它
        super().__init__('velocity_publisher')
        
        # 在/cmd_vel主题上创建发布者
        # Twist是移动机器人的标准速度消息类型
        # 10 = 队列深度:如果订阅者慢,保留最后10条消息
        self.publisher_ = self.create_publisher(Twist, 'cmd_vel', 10)
        
        # 定时器:每0.5秒调用send_velocity
        # ROS 2在内部管理这个——不是Python线程
        self.timer = self.create_timer(0.5, self.send_velocity)
        self.get_logger().info('Velocity publisher started')
    
    def send_velocity(self):
        msg = Twist()
        # 对于差速驱动:只有linear.x和angular.z重要
        # linear.x = 前进速度,单位m/s(正值=前进)
        # angular.z = 旋转速度,单位rad/s(正值=左转)
        msg.linear.x = 0.2
        msg.angular.z = 0.1
        self.publisher_.publish(msg)
        self.get_logger().info(
            f'Publishing: linear={msg.linear.x}, angular={msg.angular.z}'
        )

def main(args=None):
    rclpy.init(args=args)  # 初始化DDS通信
    node = VelocityPublisher()
    rclpy.spin(node)  # 事件循环——在这里阻塞,触发回调
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

amr_basics/amr_basics/velocity_subscriber.py

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist

class VelocitySubscriber(Node):
    def __init__(self):
        super().__init__('velocity_subscriber')
        self.subscription = self.create_subscription(
            Twist,
            'cmd_vel',
            self.velocity_callback,
            10
        )
    
    def velocity_callback(self, msg: Twist):
        # 差速驱动运动学:
        # 将线速度+角速度转换为单个轮速
        # L = 0.25m = 轮距的一半(两轮之间的距离/2)
        L = 0.25
        left_wheel = msg.linear.x - (msg.angular.z * L)
        right_wheel = msg.linear.x + (msg.angular.z * L)
        
        self.get_logger().info(
            f'Left wheel: {left_wheel:.3f} m/s | '
            f'Right wheel: {right_wheel:.3f} m/s'
        )

def main(args=None):
    rclpy.init(args=args)
    node = VelocitySubscriber()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

构建并运行:

cd ~/amr_ws
colcon build --packages-select amr_basics
source install/setup.bash

# 终端1
ros2 run amr_basics velocity_publisher

# 终端2
ros2 run amr_basics velocity_subscriber

# 终端3——检查实时系统
ros2 topic list  # 所有活动主题
ros2 topic echo /cmd_vel  # 实时打印消息
ros2 node list  # 所有运行中的节点
ros2 node info /velocity_publisher  # 这个节点的主题
rqt_graph  # 可视化节点图——显示谁在跟谁通信

终端2的预期输出:

[INFO] Left wheel: 0.175 m/s | Right wheel: 0.225 m/s
[INFO] Left wheel: 0.175 m/s | Right wheel: 0.225 m/s

6、理解rclpy.spin()——最容易被误解的一行代码

rclpy.spin(node)

这是一个事件循环——概念上等同于JavaScript的事件循环或Python的asyncio。它阻塞并等待:

  • 定时器回调 —— 你的0.5秒定时器触发,send_velocity被调用
  • 订阅回调 —— /cmd_vel上收到消息,velocity_callback被调用
  • 服务请求 —— 客户端调用你的服务,你的处理程序被调用

没有spin(),你的节点启动并立即退出。没有回调会被触发。这是ROS 2初学者从脚本背景过来时最常见的错误。

7、TF2坐标系统——一切的基础

这个概念在这里引入是因为它支撑了后续每个阶段。现在就把这个心理模型弄对,以后调试一切都会更快。

你机器人的每个物理部分都生活在一个命名的坐标系中。TF2跟踪每个坐标系如何随时间与其他每个坐标系相关联。

你的完整AMR将有这个坐标系层次结构:

map ← 全局坐标系,由SLAM创建
└── odom ← 连续里程计,随时间漂移
    └── base_footprint ← 机器人地面投影
        └── base_link ← 机器人本体中心
            ├── left_wheel
            ├── right_wheel
            ├── lidar_link
            ├── imu_link
            └── camera_link
                └── camera_optical_frame

为什么odommap是两个独立的坐标系——这让每个初学者都困惑:

odom是平滑的但会累积误差。轮子打滑、编码器噪声和不平的地面意味着行驶50米后,基于里程计的位置可能比真实位置偏差1-2米。

map是全局准确的但可能不连续跳跃。当SLAM识别到之前访问过的位置(回环闭合)时,它会校正整个地图——map→odom变换更新以吸收漂移校正。

Nav2使用odom进行瞬时速度控制(平滑,无抖动)和map进行全局路径规划(准确,已校正)。两个坐标系都是必需的。单独任何一个都不够。

8、常见错误和解决方法

"构建后找不到包":

# 你忘记在构建后source
source ~/amr_ws/install/setup.bash
# 然后重试你的ros2命令

节点运行但订阅者收不到任何东西:

# 检查主题名称完全匹配——拼写错误是隐形的
ros2 topic list
ros2 topic echo /cmd_vel
# 如果/cmd_vel没有出现,发布者没有运行或有不同的主题名称

colcon build失败,显示"找不到包":

# 你缺少依赖——安装它
sudo apt install ros-humble-<package-name>
# 或者在package.xml中声明它并运行:
rosdep install --from-paths src --ignore-src -r -y

rqt_graph显示节点但没有连接:

# 如果没有消息流动,这是正常的
# 先开始发布,然后打开rqt_graph
# 在rqt_graph中点击刷新按钮(圆形箭头)

9、结束语

此时你有:

  • 一个正常工作的ROS 2 Humble安装
  • 一个具有正确构建系统的结构化工作空间
  • 一个交换速度指令的发布者和订阅者节点
  • 关于节点、主题和TF坐标系如何关联的清晰心理模型

这是每个后续部分建立的基础。订阅者中的差速驱动运动学(left = v - ω*Lright = v + ω*L)是你的ros2_control硬件接口将在第5部分实现的相同数学。

在第2部分中,我们从零开始建模物理机器人:底盘、轮子、脚轮和所有传感器作为一个URDF文件。你将理解惯性张量、关节类型、坐标系约定,以及为什么在模型中把这些弄对可以防止后来在模拟中花费数小时调试。


原文链接: Building an Autonomous Mobile Robot from Scratch — Part 1: ROS 2 Fundamentals

汇智网翻译整理,转载请标明出处