2. ROS2基础

在开始集成任何传感器之前,必须先理解ROS2的“骨架”。ROS2不是一套单纯的库,而是一个分布式通信中间件。它把机器人软件拆解为若干独立单元,通过标准接口协作。本章聚焦四个核心概念——节点、话题、服务、动作,以及工作空间与功能包的创建方法。掌握这些,后续驱动激光雷达或IMU时才能理解数据从哪来、到哪去。

核心概念:节点、话题、服务、动作

节点(Node)是ROS2中执行计算的最小进程单元。一个传感器驱动、一个里程计解算器、一个路径规划器,都可以是独立节点。节点之间不直接调用函数,而是通过“命名图”通信。每个节点有唯一名称,例如 /lidar_driver 或 /imu_filter。

话题(Topic)是异步、单向、连续的数据流通道。发布者(Publisher)向话题写入消息,订阅者(Subscriber)从话题读取消息。话题适合高频传感器数据,如激光雷达点云(/scan)、IMU姿态(/imu/data_raw)。消息类型由接口定义,例如 sensor_msgs/msg/LaserScan。

服务(Service)是同步、双向、一次性的请求-应答模式。客户端发送请求并等待响应。适合低频控制指令,如“开启相机曝光”“重置里程计”。服务用 srv 文件定义请求与响应结构。

动作(Action)用于长时间、可取消、带反馈的任务。例如“导航到目标点”或“机械臂抓取”。动作由目标(Goal)、反馈(Feedback)、结果(Result)三部分组成,客户端可随时取消。动作在ROS2中基于话题和服务实现,但提供了更高层语义。

选型判断:传感器数据流(连续、高频)→ 话题;一次性指令(开/关/查询)→ 服务;复杂任务(移动/抓取/扫描建图)→ 动作。
节点 Node /lidar_driver /imu_filter 话题 Topic /scan (LaserScan) /imu/data_raw 服务 Service /reset_odom /enable_camera 动作 Action /navigate_to_pose /follow_joint_traj 发布 订阅 请求/响应 目标/反馈/结果 通信模式对比 话题:异步单向,适合高频数据流(如雷达扫描) 服务:同步双向,适合一次性指令 动作:异步+反馈+可取消,适合长时间任务

工作空间与功能包创建

ROS2采用工作空间(Workspace)组织代码。典型结构为 src(源码)、build(编译中间文件)、install(安装产物)、log(日志)。使用 colcon 工具构建。功能包(Package)是ROS2的软件单元,包含节点源码、接口定义、配置文件等。

创建功能包的标准流程如下:

  1. 创建工作空间目录并初始化 src 文件夹。
  2. 使用 ros2 pkg create 生成包骨架。
  3. 编写节点源码与 CMakeLists.txt / package.xml。
  4. 使用 colcon build 编译。
  5. source install/setup.bash 使包可见。
# 创建工作空间
mkdir -p ~/ros2_sensor_ws/src
cd ~/ros2_sensor_ws

# 创建C++功能包(含依赖)
ros2 pkg create lidar_driver_pkg \
  --build-type ament_cmake \
  --dependencies rclcpp sensor_msgs

# 创建Python功能包
ros2 pkg create imu_reader_pkg \
  --build-type ament_python \
  --dependencies rclpy sensor_msgs

# 编译整个工作空间
colcon build --symlink-install

# 使环境生效
source install/setup.bash

# 验证包可见
ros2 pkg list | grep sensor
实用建议:开发传感器驱动时,优先使用 --symlink-install 选项。这样修改Python脚本后无需重新编译即可生效,大幅提升调试效率。

案例:搭建激光雷达驱动包

假设我们有一款2D激光雷达,需要编写驱动节点发布 sensor_msgs/msg/LaserScan 消息。以下展示最小实现框架:

// lidar_driver_node.cpp (核心片段)
#include "rclcpp/rclcpp.hpp"
#include "sensor_msgs/msg/laser_scan.hpp"

class LidarDriver : public rclcpp::Node {
public:
  LidarDriver() : Node("lidar_driver") {
    publisher_ = this->create_publisher<sensor_msgs::msg::LaserScan>("/scan", 10);
    timer_ = this->create_wall_timer(
      std::chrono::milliseconds(100),
      std::bind(&LidarDriver::publish_scan, this));
  }

private:
  void publish_scan() {
    auto msg = sensor_msgs::msg::LaserScan();
    msg.header.stamp = this->now();
    msg.angle_min = 0.0;
    msg.angle_max = 6.28319;  // 360度
    msg.angle_increment = 0.0174533; // 1度
    msg.range_min = 0.1;
    msg.range_max = 10.0;
    // msg.ranges 填充实际测量数据...
    publisher_->publish(msg);
  }
  rclcpp::Publisher<sensor_msgs::msg::LaserScan>::SharedPtr publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<LidarDriver>());
  rclcpp::shutdown();
  return 0;
}

编译后运行节点,可用 ros2 topic echo /scan 查看数据流,用 ros2 node info /lidar_driver 查看节点详情。

常见误区与复盘

误区 后果 正确做法
所有通信都用话题 服务类指令(如校准)难以同步确认 根据请求-响应语义选择服务
忽略 package.xml 依赖声明 在别的机器上编译失败 创建包时用 --dependencies 显式声明
忘记 source install/setup.bash 运行时报“找不到包” 每次新终端先source,或加入 ~/.bashrc
话题名与消息类型不匹配 订阅者收不到数据 用 ros2 topic info /scan 检查类型
关键提醒:在ROS2中,节点名称、话题名称、服务名称共同组成命名空间。如果两个传感器驱动都发布 /scan,会发生数据冲突。建议使用命名空间隔离,例如 /left_lidar/scan 与 /right_lidar/scan。

复盘一个典型调试场景:当你运行 ros2 run lidar_driver_pkg lidar_driver_node 却收到 Package not found 错误时,90%的原因是当前终端未source工作空间。执行 source ~/ros2_sensor_ws/install/setup.bash 即可解决。若仍失败,检查 colcon build 是否成功,以及包名是否拼写正确。

理解节点间的通信模式差异,是设计传感器驱动架构的基础。话题负责数据洪流,服务处理控制指令,动作管理复杂任务。工作空间与功能包则是代码组织的基石。下一阶段,我们将基于这些概念,实际接入第一个传感器并可视化数据流。