Skip to main content

ROS 2 — Robot Operating System

ROS 2 — Robot Operating System

ROS 2 — это middleware для создания робототехнических систем. Не операционная система, а набор библиотек, инструментов и соглашений, работающих поверх Linux, Windows или macOS.


Архитектура ROS 2

Уровни абстракции

┌─────────────────────────────────────────────┐
│           Application Layer                 │
│  (Nav2, MoveIt 2, perception, planning)     │
├─────────────────────────────────────────────┤
│           ROS 2 Client Libraries            │
│      (rclcpp, rclpy, rclrs, rclnodejs)      │
├─────────────────────────────────────────────┤
│              ROS 2 Middleware               │
│         (rcl, rmw, DDS abstraction)         │
├─────────────────────────────────────────────┤
│         DDS Implementation                  │
│  (Fast DDS, Cyclone DDS, Connext DDS)       │
├─────────────────────────────────────────────┤
│              Operating System               │
│        (Linux, Windows, macOS, RTOS)        │
└─────────────────────────────────────────────┘

Граф вычислений

                    ┌──────────┐
                    │  /camera │
                    │  (Node)  │
                    └────┬─────┘
                         │ /image_raw
┌──────────┐      ┌──────────────┐      ┌──────────┐
│ /lidar   │──────│  /perception │──────│ /planner │
│ (Node)   │      │    (Node)    │      │  (Node)  │
└──────────┘      └──────────────┘      └────┬─────┘
     │ /scan            │ /obstacles         │ /cmd_vel
     │                  │                    ▼
     │            ┌─────┴─────┐       ┌──────────┐
     └───────────►│ /costmap  │       │ /driver  │
                  │  (Node)   │       │  (Node)  │
                  └───────────┘       └──────────┘

DDS (Data Distribution Service)

Принцип работы

DDS обеспечивает decentralized discovery — узлы находят друг друга без центрального сервера (Master Node из ROS 1).

Процесс:

  1. Узел объявляет свои топики через multicast
  2. Другие узлы обнаруживают публикации/подписки
  3. DDS устанавливает прямые P2P соединения

Quality of Service (QoS)

#include "rclcpp/rclcpp.hpp"

auto qos = rclcpp::QoS(10)
    .reliability(RMW_QOS_POLICY_RELIABILITY_RELIABLE)
    .durability(RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL)
    .history(RMW_QOS_POLICY_HISTORY_KEEP_LAST)
    .deadline(std::chrono::milliseconds(100));

auto pub = node->create_publisher<std_msgs::msg::String>("topic", qos);
ПолитикаОпцииПрименение
ReliabilityRELIABLE / BEST_EFFORTКоманды / Сенсоры
DurabilityVOLATILE / TRANSIENT_LOCALПотоки / Параметры
HistoryKEEP_LAST(n) / KEEP_ALLБуферизация
DeadlineDurationReal-time
LifespanDurationУстаревание данных

Совместимость QoS

Publisher      Subscriber    Результат
─────────────────────────────────────
RELIABLE   +   RELIABLE   =  ✅ Работает
RELIABLE   +   BEST_EFFORT = ✅ Работает
BEST_EFFORT +  RELIABLE   =  ❌ Несовместимо!
BEST_EFFORT +  BEST_EFFORT = ✅ Работает

Основные концепции

Узлы (Nodes)

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan

class LidarProcessor(Node):
    def __init__(self):
        super().__init__('lidar_processor')
        
        # Параметры
        self.declare_parameter('min_range', 0.1)
        self.min_range = self.get_parameter('min_range').value
        
        # Подписка
        self.sub = self.create_subscription(
            LaserScan, '/scan', self.scan_callback, 10)
        
        # Публикация
        self.pub = self.create_publisher(LaserScan, '/scan_filtered', 10)
        
        self.get_logger().info('Lidar processor started')
    
    def scan_callback(self, msg: LaserScan):
        # Фильтрация близких точек
        msg.ranges = [r if r > self.min_range else float('inf') 
                      for r in msg.ranges]
        self.pub.publish(msg)

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

Сервисы (Services)

Request-Response паттерн:

# Сервер
from example_interfaces.srv import AddTwoInts

class AddServer(Node):
    def __init__(self):
        super().__init__('add_server')
        self.srv = self.create_service(
            AddTwoInts, 'add_two_ints', self.callback)
    
    def callback(self, request, response):
        response.sum = request.a + request.b
        return response

# Клиент
class AddClient(Node):
    def __init__(self):
        super().__init__('add_client')
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')
    
    async def call(self, a, b):
        req = AddTwoInts.Request()
        req.a, req.b = a, b
        return await self.cli.call_async(req)

Actions

Для длительных задач с обратной связью:

from nav2_msgs.action import NavigateToPose
from rclpy.action import ActionClient

class Navigator(Node):
    def __init__(self):
        super().__init__('navigator')
        self.client = ActionClient(self, NavigateToPose, 'navigate_to_pose')
    
    def go_to(self, x, y, theta):
        goal = NavigateToPose.Goal()
        goal.pose.header.frame_id = 'map'
        goal.pose.pose.position.x = x
        goal.pose.pose.position.y = y
        # ... quaternion from theta
        
        self.client.wait_for_server()
        future = self.client.send_goal_async(
            goal, 
            feedback_callback=self.feedback_cb)
        return future
    
    def feedback_cb(self, feedback_msg):
        feedback = feedback_msg.feedback
        self.get_logger().info(
            f'Distance remaining: {feedback.distance_remaining:.2f}')

Launch-файлы

Python Launch

# my_robot_launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare

def generate_launch_description():
    return LaunchDescription([
        # Аргументы
        DeclareLaunchArgument('use_sim', default_value='false'),
        DeclareLaunchArgument('robot_name', default_value='my_robot'),
        
        # Узлы
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            parameters=[{'robot_description': robot_description}],
        ),
        
        Node(
            package='controller_manager',
            executable='ros2_control_node',
            parameters=[
                PathJoinSubstitution([
                    FindPackageShare('my_robot'),
                    'config', 'controllers.yaml'
                ])
            ],
            condition=UnlessCondition(LaunchConfiguration('use_sim')),
        ),
        
        # Включение других launch-файлов
        IncludeLaunchDescription(
            PathJoinSubstitution([
                FindPackageShare('nav2_bringup'),
                'launch', 'navigation_launch.py'
            ]),
            launch_arguments={'use_sim_time': LaunchConfiguration('use_sim')}.items(),
        ),
    ])

Запуск

ros2 launch my_robot my_robot_launch.py use_sim:=true robot_name:=bot1

Стандартные пакеты

Навигация (Nav2)

┌─────────────────────────────────────────────────────┐
│                    Nav2 Stack                       │
├──────────┬──────────┬──────────┬───────────────────┤
│ Planner  │ Controller│ Recovery │ Behavior Tree    │
│ Server   │ Server    │ Server   │ Navigator        │
├──────────┴──────────┴──────────┴───────────────────┤
│              Costmap 2D (Global + Local)           │
├─────────────────────────────────────────────────────┤
│         AMCL / SLAM Toolbox (Localization)         │
├─────────────────────────────────────────────────────┤
│           Map Server / Lifecycle Manager           │
└─────────────────────────────────────────────────────┘

Конфигурация (nav2_params.yaml):

controller_server:
  ros__parameters:
    controller_frequency: 20.0
    FollowPath:
      plugin: "dwb_core::DWBLocalPlanner"
      max_vel_x: 0.5
      max_vel_theta: 1.0

planner_server:
  ros__parameters:
    GridBased:
      plugin: "nav2_navfn_planner/NavfnPlanner"
      tolerance: 0.5
      use_astar: true

Манипуляция (MoveIt 2)

from moveit_msgs.action import MoveGroup
from geometry_msgs.msg import Pose

# Через MoveIt Python API
move_group = MoveGroupInterface(node, "arm")
pose_goal = Pose()
pose_goal.position.x = 0.4
pose_goal.position.y = 0.0
pose_goal.position.z = 0.3
pose_goal.orientation.w = 1.0

move_group.set_pose_target(pose_goal)
success = move_group.go(wait=True)

Инструменты

CLI

# Список узлов, топиков, сервисов
ros2 node list
ros2 topic list -t
ros2 service list

# Просмотр сообщений
ros2 topic echo /scan
ros2 topic hz /scan

# Публикация
ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
  "{linear: {x: 0.5}, angular: {z: 0.1}}"

# Параметры
ros2 param list /my_node
ros2 param get /my_node max_speed
ros2 param set /my_node max_speed 1.5

# Запись/воспроизведение
ros2 bag record -a -o my_bag
ros2 bag play my_bag

RViz2

Визуализация:

  • TF дерево
  • PointCloud2
  • LaserScan
  • Markers
  • Costmaps
  • Paths

Gazebo

<!-- robot.urdf.xacro -->
<gazebo>
  <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
    <robot_param>robot_description</robot_param>
    <robot_param_node>robot_state_publisher</robot_param_node>
    <parameters>$(find my_robot)/config/controllers.yaml</parameters>
  </plugin>
</gazebo>

Сборка и пакеты

Структура пакета

my_robot/
├── CMakeLists.txt
├── package.xml
├── config/
│   ├── controllers.yaml
│   └── nav2_params.yaml
├── launch/
│   └── my_robot_launch.py
├── src/
│   └── my_node.cpp
├── scripts/
│   └── my_script.py
├── urdf/
│   └── robot.urdf.xacro
└── worlds/
    └── my_world.sdf

Сборка (colcon)

cd ~/ros2_ws
colcon build --packages-select my_robot --symlink-install
source install/setup.bash

Рекомендации

  1. Используйте композицию — запускайте узлы в одном процессе через Composition
  2. QoS по назначению — RELIABLE для команд, BEST_EFFORT для сенсоров
  3. Lifecycle nodes — для контролируемого запуска/остановки
  4. TF2 — всегда публикуйте трансформации
  5. Параметры через YAML — не хардкодьте значения

Версии ROS 2

ДистрибутивEOLLTSUbuntu
Foxy2023Да20.04
Humble2027Да22.04
Iron2024Нет22.04
Jazzy2029Да24.04
RollingDevLatest

Рекомендация: Jazzy (LTS) или Rolling для новых проектов.


Ссылки