Обзор основных концепций ROS. Ноды (Nodes).

Обзор основных концепций ROS. Ноды (Nodes).

Друзья, привет! 👋

Продолжаем наш цикл статей о ROS.

В прошлой статье мы поговорили об основах устройства файловой системы ROS2, сегодня мы перейдем к обзору вычислительного графа ROS, и начнем с концепции Нод, поговорим о том каких видов они бывают, как они используются, как создавать свои и тд.

Наш Телеграм канал, где мы описываем прогресс нашего проекта — https://t.me/it_s_working.

Введение

Вычислительный граф в ROS (ROS Computation Graph) – это сетевая структура (одноранговая сеть), которая позволяет различным нодам/узлам (nodes) ROS обмениваться данными и взаимодействовать друг с другом. Он состоит из узлов, которые представляют собой отдельные процессы, обменивающиеся информацией через топики/темы, сервисы и параметры. Основные компоненты вычислительного графа – это:

  • Ноды/Узлы (Nodes)
  • Мастер нода(Ros Master)
  • Сервер параметров (Parameter Server)
  • Сообщения (Messages)
  • Темы (Topics)* Сервисы (Services)
  • Хранение данных (Bags)
Обзор основных концепций ROS. Ноды (Nodes).

Учитывая объем материала, а также для облегчения восприятия мы постараемся разделить эту тему на несколько частей.

Итак начнем часть 1 Node (узлы)

Примечание: в данной статье мы продолжаем использовать ROS в Docker контейнере. Установка описана в предыдущей статье. Для контейнера используется следующий Dockerfile

FROM ros:jazzy SHELL ["/bin/bash", "-c"] WORKDIR /app RUN apt-get update && apt-get install -y CMD ["bash"]

Ноды (Nodes) в ROS

Ноды/Узлы — это процессы, которые выполняют вычисления. ROS спроектирована как модульная система, оперирующая узкоспециализированными узлами небольших размеров. Система управления роботом обычно состоит из множества узлов.

Пример сложной системы состоящей из множества Нод/Узлов
Пример сложной системы состоящей из множества Нод/Узлов

Современные роботы — это конструкторы из разнородных компонентов, например:

  • 📷 Камеры
  • 🚀 Двигатели
  • 📡 Лидары
  • 🧠 AI-ускорители

Как заставить их говорить на одном языке?

Ответ предлагаемый ROS - Ноды — это универсальные адаптеры, которые превращают любой компонент в "кирпичик" нашего робота!

Нода — независимый процесс выполняющий вычисления, который:

  • Решает одну задачу (читает сенсор, управляет мотором, строит карту),
  • Общается с другими нодами через ROS (но не знает, кто они такие! работа строится согласно протоколу – обмену сообщениями),
  • Может упасть без паники всей системы и быть перезапущенной для восстановления

Ноды объединяются в вычислительный граф (Computation Graph) и взаимодействуют друг с другом с помощью тем (Topics), сервисов (RPC services), и сервера (Parameter Server)

Ключевая идея ROS:

«Одна нода — одна ответственность. Мы хотим добавить роботу новые возможности? Добавим новую ноду для общения с новым компонентом, а не будем увеличивать ответственности уже существующих нод.»

Таким образом достигается инкапсуляция и хороший уровень модульности.

Зачем дробить систему на ноды?

Решение проблемы монолитных систем: одна программа = 10 000 строк кода. Как следствие - сломался мотор? → Перезапускай всего робота!

Преимущества решения через ноды:

  • Отказоустойчивость: Упала нода камеры → Навигация работает!
  • Масштабируемость: Добавил ноду для нового лидара → Старый код не трогал.
  • Тестирование: Проверяешь только «блок», а не всю «систему».
  • Языковая свобода: Например - Python-нода для AI + C++ - нода для моторов.

Главное преимущество такого подхода - гибкость, если нода упала, робот не «умрёт» — а например просто перестанет делать селфи.

Имена нод

Каждая запущенная нода имеет уникальное имя ресурса графа по которому ее можно легко найти в системе. Имя должно отвечать следующему правилу

  1. Первый символ буква ([a-z|A-Z]), тильда(~) или символ слэша (/)
  2. Последующие символы могут быть буквенно-циферные ([0-9|a-z|A-Z]), символ подчеркивания (_), или символ слэша (/)

Например: /hokuyo_node может быть именем Hokuyo драйвера лазерного сканера.

Типы нод

Начиная с релиза ROS 2 Humble в ROS2 выделяют 2 основных типа Нод:

  • Regular - стандартная Нода, подходящая для решения простых задач, при запуске подобной Ноды ROS изменяет ее состояние из inactive → active (при условии что конфигурация и запуск Ноды прошли успешно)

  • Managed nodes (https://design.ros2.org/articles/node_lifecycle.html ) - данный тип был представлен в релизе ROS 2 Humble и предоставляет управляемый жизненный цикл Нод, что обеспечивает больший контроль над состоянием системы ROS. Жизненный цикл охватывает 4 основных состояния: unconfigured (ненастроенный), inactive (неактивный), active (активный ), and finalized (завершенный). На каждом из этапов разработчику доступны callback методы которые позволяют контролировать состояние Ноды перед продвижением к следующему состоянию.

Давайте посмотрим на работу с Нодами на практике

Regular (стандартные Ноды)

Работа с Regular нодами выполняется с помощью следующих библиотек:

1. Для начала создадим новый пакет способом описанным в предыдущей статье. Мы будем использовать Python для написания Ноды так что важно указать тип используемой сборки build-type ament_python

ros2 pkg create --node-name test_node --description its-working-test-package --license MIT test_package --build-type ament_python
Результат выполнения команды по созданию Ноды
Результат выполнения команды по созданию Ноды

2. Далее следуя стандарту и структуре файловой системы создадим файл с логикой нашей Ноды

import rclpy from rclpy.node import Node class TestNode(Node): def __init__(self): super().__init__("its_working_node") self.get_logger().info("Node has been started.") self.create_timer(0.2, self.timer_callback) def timer_callback(self): self.get_logger().info("<ROS2> it’s working") def main(args=None): rclpy.init(args=args) node = TestNode() rclpy.spin(node) rclpy.shutdown() if __name__ == "__main__": main()

Давайте посмотрим подробнее как строится Нода.

Первые строки выполняют импорт библиотеки rclpy а также модуля Node

import rclpy from rclpy.node import Node

Далее мы создаем класс TestNode который наследуется от базового класса Node предоставляемого библиотекой rclpy.

class TestNode(Node):

В конструкторе класса мы обращаемся к конструктору базового класса и передаем имя нашей Ноды - "its_working_node", по указанному здесь имени мы сможем далее обращаться к запущенной Ноде.

Дале мы логгируем строку "Node has been started." средствами логирования предоставляемыми базовым классом (позже мы увидим где будут доступны данные логи)

def __init__(self): super().__init__("its_working_node") self.get_logger().info("Node has been started.")

Давайте добавим интерактивности нашей Ноде, для этого создадим таймер, который будет с частотой 0.2 сек вызывать наш метод timer_callback задача которого просто выводить сообщение "<ROS2> it’s working"

def __init__(self): * * * * self.create_timer(0.2, self.timer_callback) def timer_callback(self): self.get_logger().info("<ROS2> it’s working")

Далее – прежде чем создать нашу Ноду сперва нужно инициализировать ROS communications

def main(args=None): rclpy.init(args=args)

Строка rclpy.init(args) делает это для нас, при необходимости мы можем передать аргументы через командную строку или в launch файле ( список доступных к использованию аргументов )

После этого мы готовы к созданию экземпляра нашей Ноды

node = TestNode()

После выполнения этой команды будет создан экземпляр нашей TestNode, далее если в логике не предусмотрено никаких иных операций система завершит выполнение, поэтому мы используем :

rclpy.spin(node)

Данная команда остановит выполнение на данной строке, ожидая запроса на удаление ноды, например нажатием комбинации клавиш CTRL+C в окне терминала.

При вызове CTRL+C программа покинет функцию spin и следующая строка освободит ресурсы занятые при выполнении rclpy.init

rclpy.shutdown()

В целом описанный алгоритм можно описать следующей схемой

Алгоритм работы ноды
Алгоритм работы ноды

3. Следуя структуре созданного пакета откройте файл ноды расположенный по пути test_package/test_package/test_node.py и перенесите в него описанный код.

4. Для успешной установки пакета и Ноды нам необходимо указать входную точку в файле test_package/setup.py. Убедитесь что entry-points заполнено верно и указывает на файл с кодом Ноды

* * * * entry_points={ 'console-scripts': [ 'test_node = test_package.test_node.main' ] }
Обзор основных концепций ROS. Ноды (Nodes).

ВАЖНО: если в имени пакета или ноды вы использовали знак дефиса при указании entry point его необходимо заменить на знак подчеркивания

Например:
если для имени использовалось test-node необходимо указать test_node

5. Учитывая что мы планируем использовать библиотеку rclpy необходимо убедится что данная зависимость указана в файле test_package/package.xml

<depend>rclpy</depend>
Обзор основных концепций ROS. Ноды (Nodes).

6. Теперь мы готовы собрать пакет с нодой, для этого мы будем использовать инструмент colcon (документация)

Результат сборки 
Результат сборки 

В результате сборки будут созданы папки:

  • install
  • build
  • log
Файловая структура после сборки пакета
Файловая структура после сборки пакета

7. Чтобы начать пользоваться нашим пакетом и нодой необходимо выполнить

source /app/test_package/install/setup.bash

8. После этой команды созданный нами пакет станет доступен для использования. Запустим его используя команду

ros2 run <имя пакета> <имя исполняемого файла/ноды> ros2 run test_package test_node

После запуска согласно логике нашей Ноды будет происходить вызов метода по таймеру и создание записей логов

Результат работы Ноды
Результат работы Ноды

9. Запросим информацию о запущенной Ноде с помощью команды (мы используем имя Ноды которое мы указывали в конструкторе ноды - its_working_node)

ros2 node info /its_working_node

В случае работы через Docker нам будет необходимо выполнить эту команду в новом терминале

sudo docker exec <имя контейнера> /bin/bash -c ". /opt/ros/jazzy/setup.bash && ros2 node info /its_working_node"
Обзор основных концепций ROS. Ноды (Nodes).

Данная команда выводит подробную информацию о конкретном узле - его связи и выполняемые действия. Итак нам доступна следующая информация

Подписчики (Subscribers)

Список топиков, на которые узел подписан:

  • Имя топика (например, /topic_name)
  • Тип сообщения (например, std_msgs/msg/String)
  • QoS-профиль (например, Reliable, Volatile, TransientLocal).

Издатели (Publishers)

Список топиков, в которые узел публикует данные, с указанием:

  • Имени топика
  • Типа сообщения
  • QoS-профиля.

Сервис-серверы (Service Servers)

Список сервисов, которые узел предоставляет (т.е. сервисы, на которые он отвечает):

  • Имя сервиса (например, /service_name)
  • Тип сервиса (например, example_interfaces/srv/Trigger).

Клиенты сервисов (Service Clients)

Список сервисов, к которым узел обращается (т.е. сервисы, которые он вызывает):

  • Имя сервиса
  • Тип сервиса.

Серверы действий (Action Servers)

Список действий (actions), которые узел обрабатывает (если поддерживает):

  • Имя действия (например, /action_name)
  • Тип действия (например, example_interfaces/action/Fibonacci).

Клиенты действий (Action Clients)

Список действий, которые узел инициирует (если использует):

  • Имя действия
  • Тип действия.

Анализ данной информации может быть полезен при отладке Ноды и лучшего понимания ее работы и возможностей.

Managed (Ноды с контролем жизненного цикла)

Работа с Managed нодами выполняется с помощью следующих библиотек:

Данный вид Нод появился начиная с релиза ROS 2 Humble, основное отличие от Regular (обычных/стандартных) Нод состоит в следующем:

  • Regular сразу переводят Ноду в режим active (при условии что конфигурация в порядке)
  • Managed предоставляет возможность контроля для перехода с одного состояния в другое, переходы осуществляются между 4-я состояниями: unconfigured, inactive, active, and finalized.

После первоначальной настройки ненастроенный(unconfigured) узел переходит в неактивное(inactive) состояние — в целом, узел готов к работе, но callback вызовы а также публикация сообщений еще не работают. На этом этапе мы можем перенастроить узел — изменить параметры, добавить и удалить подписки и т. д. — не изменяя его поведения.

После полной готовности к работе узел может перейти в активное(active) состояние. На этом этапе он обладает всеми характеристиками стандартного узла ROS — он отвечает на запросы, считывает и обрабатывает входящие сообщения и публикует свои сообщения.

На последнем этапе узел переходит в финализированное(finalized) состояние. Это состояние всегда является конечным — единственный переход отсюда — это уничтожение, что впоследствии освобождает память узла.

Полная схема жизненного цикла Ноды
Полная схема жизненного цикла Ноды

Хороший пример применения данного подхода это пакет Nav2, запуск которого включает в себя использования множества пакетов и нод, которые запускаются в строго определенном порядке – данный процесс отслеживается и если одна Нода не сможет быть запущена – весь процесс запуска будет остановлен.

Создадим Managed Node для ознакомления

1. Повторим шаг1 с предыдущего раздела чтобы создать пакет

2. Далее следуя стандарту создадим файл с логикой нашей Ноды

import rclpy from rclpy.lifecycle import State, TransitionCallbackReturn, Node class TestNode(Node): def __init__(self): super().__init__("its_working_node") self.get_logger().info("Node has been started") def on_configure(self, state: State) -> TransitionCallbackReturn: self.get_logger().info(f"<ROS2> node '{self.get_name()}' is in state '{state.label}'. Transitioning to 'configure'") return TransitionCallbackReturn.SUCCESS def on_activate(self, state: State) -> TransitionCallbackReturn: self.get_logger().info(f"<ROS2> node '{self.get_name()}' is in state '{state.label}'. Transitioning to 'activate'") return TransitionCallbackReturn.SUCCESS def on_deactivate(self, state: State) -> TransitionCallbackReturn: self.get_logger().info(f"<ROS2> node '{self.get_name()}' is in state '{state.label}'. Transitioning to 'deactivate'") return TransitionCallbackReturn.SUCCESS def on_shutdown(self, state: State) -> TransitionCallbackReturn: self.get_logger().info(f"<ROS2> node '{self.get_name()}' is in state '{state.label}'. Transitioning to 'shutdown'") return TransitionCallbackReturn.SUCCESS def main(args=None): rclpy.init(args=args) node = TestNode() rclpy.spin(node) rclpy.shutdown() if __name__ == '__main__': main()

В целом код похож на наш прошлый пример однако следует отметить несколько ключевых отличий:

  • мы также пользуемся библиотекой rclpy, однако в этот раз мы используем пространство rclpy.lifecycle, откуда мы и используем базовый класс для нашей Ноды - Node
  • мы объявляем несколько callback методов которые будут вызваны при переходе из одного состояния в другое: on_configure, on_activate, on_deactivate, on_shutdown. В нашем примере callback методы будут просто выводить сообщения чтобы мы могли убедится в факте их вызова, так же следуя конвенции каждый callback метод должен возвращать TransitionCallbackReturn который предоставляет варианты SUCCESS и FAILURE для успешного и не успешного результата выполнения соответственно.

3. Настройки файлов package.xml и setup.py будет аналогичен нашему прошлому примеру.

4. Соберем наш пакет командой colcon build

5. Настроим терминал чтобы начать пользоваться нашим пакетом и нодой выполнив команду

source /app/test-package/install/setup.bash

6. Запустим Ноду

ros2 run <имя пакета> <имя исполняемого файла/ноды> ros2 run test_package test_node
Обзор основных концепций ROS. Ноды (Nodes).

7. Запросим информацию о жизненном цикле ноды выполнив команду в новом окне терминала (где its_working_node имя ноды заданное в конструкторе класса Ноды)

ros2 lifecycle list /its_working_node
Обзор основных концепций ROS. Ноды (Nodes).

Как мы видим в настоящий момент Нода находится в состоянии - не сконфигурирована (unconfigured) и нам доступны переходы на состояние сконфигурирована (configuring) и выключение (shuttingdown).

Обзор основных концепций ROS. Ноды (Nodes).

8. Давайте выполним переход в состояние configuring, для этого воспользуемся командой

ros2 lifecycle set /its_working_node configure

где:

  • /its_working_node - имя найшей ноды
  • configure - метод перехода в новое состояние

В результате выполнения команды мы должны получить сообщение об успешном завершении перехода

Обзор основных концепций ROS. Ноды (Nodes).

Так же в терминале с запущенной Нодой мы увидим результат работы нашего callback метода настроенного на переход в новое состояние

Обзор основных концепций ROS. Ноды (Nodes).

Проверим ещё раз состояние ноды

ros2 lifecycle list /its_working_node
Обзор основных концепций ROS. Ноды (Nodes).

Как мы видим из текущего положения нам доступны переходы на очистку(clenup), активацию(activate), выключение(shutdown), и для каждого из вариантов мы видим между какими состояниями будет переход

Обзор основных концепций ROS. Ноды (Nodes).

9. Завершим запуск и активируем нашу Ноду

ros2 lifecycle set /its_working_node activate
Обзор основных концепций ROS. Ноды (Nodes).

Переход успешно завершен - Нода полностью готова к работе.

Как мы видим данный подход предоставляет больше контроля над различными этапами жизненного цикла Ноды, что даем нам возможность, реализовывать свою логику проверки/конфигурации Ноды на каждом из этапов, если требуется убедится что все готово к работе прежде, чем полностью запускать Ноду.

Важно отметить - в наших примерах мы следовали ООП подходу в написании кода для нод, на данный момент это рекомендованный способ оформления нод.

P.S. Попробуйте создать свои Ноды. Если возникнут вопросы или темы, которые хотите обсудить — обязательно напишите в комментарии, — будем разбираться вместе и сделаем наши статьи ещё полезнее! 🛠

Подписывайтесь на наш Телеграмм канал, где мы публикуем как сами применяем полученные знания на реальном примере — популярно делимся нашими результатами, вместе будет интереснее https://t.me/it_s_working!

33