Обзор основных концепций 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)
Учитывая объем материала, а также для облегчения восприятия мы постараемся разделить эту тему на несколько частей.
Итак начнем часть 1 Node (узлы)
Примечание: в данной статье мы продолжаем использовать ROS в Docker контейнере. Установка описана в предыдущей статье. Для контейнера используется следующий Dockerfile
Ноды (Nodes) в ROS
Ноды/Узлы — это процессы, которые выполняют вычисления. ROS спроектирована как модульная система, оперирующая узкоспециализированными узлами небольших размеров. Система управления роботом обычно состоит из множества узлов.
Современные роботы — это конструкторы из разнородных компонентов, например:
- 📷 Камеры
- 🚀 Двигатели
- 📡 Лидары
- 🧠 AI-ускорители
Как заставить их говорить на одном языке?
Ответ предлагаемый ROS - Ноды — это универсальные адаптеры, которые превращают любой компонент в "кирпичик" нашего робота!
Нода — независимый процесс выполняющий вычисления, который:
- Решает одну задачу (читает сенсор, управляет мотором, строит карту),
- Общается с другими нодами через ROS (но не знает, кто они такие! работа строится согласно протоколу – обмену сообщениями),
- Может упасть без паники всей системы и быть перезапущенной для восстановления
Ноды объединяются в вычислительный граф (Computation Graph) и взаимодействуют друг с другом с помощью тем (Topics), сервисов (RPC services), и сервера (Parameter Server)
Ключевая идея ROS:
«Одна нода — одна ответственность. Мы хотим добавить роботу новые возможности? Добавим новую ноду для общения с новым компонентом, а не будем увеличивать ответственности уже существующих нод.»
Таким образом достигается инкапсуляция и хороший уровень модульности.
Зачем дробить систему на ноды?
Решение проблемы монолитных систем: одна программа = 10 000 строк кода. Как следствие - сломался мотор? → Перезапускай всего робота!
Преимущества решения через ноды:
- Отказоустойчивость: Упала нода камеры → Навигация работает!
- Масштабируемость: Добавил ноду для нового лидара → Старый код не трогал.
- Тестирование: Проверяешь только «блок», а не всю «систему».
- Языковая свобода: Например - Python-нода для AI + C++ - нода для моторов.
Главное преимущество такого подхода - гибкость, если нода упала, робот не «умрёт» — а например просто перестанет делать селфи.
Имена нод
Каждая запущенная нода имеет уникальное имя ресурса графа по которому ее можно легко найти в системе. Имя должно отвечать следующему правилу
- Первый символ буква ([a-z|A-Z]), тильда(~) или символ слэша (/)
- Последующие символы могут быть буквенно-циферные ([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
2. Далее следуя стандарту и структуре файловой системы создадим файл с логикой нашей Ноды
Давайте посмотрим подробнее как строится Нода.
Первые строки выполняют импорт библиотеки rclpy а также модуля Node
Далее мы создаем класс TestNode который наследуется от базового класса Node предоставляемого библиотекой rclpy.
В конструкторе класса мы обращаемся к конструктору базового класса и передаем имя нашей Ноды - "its_working_node", по указанному здесь имени мы сможем далее обращаться к запущенной Ноде.
Дале мы логгируем строку "Node has been started." средствами логирования предоставляемыми базовым классом (позже мы увидим где будут доступны данные логи)
Давайте добавим интерактивности нашей Ноде, для этого создадим таймер, который будет с частотой 0.2 сек вызывать наш метод timer_callback задача которого просто выводить сообщение "<ROS2> it’s working"
Далее – прежде чем создать нашу Ноду сперва нужно инициализировать ROS communications
Строка rclpy.init(args) делает это для нас, при необходимости мы можем передать аргументы через командную строку или в launch файле ( список доступных к использованию аргументов )
После этого мы готовы к созданию экземпляра нашей Ноды
После выполнения этой команды будет создан экземпляр нашей TestNode, далее если в логике не предусмотрено никаких иных операций система завершит выполнение, поэтому мы используем :
Данная команда остановит выполнение на данной строке, ожидая запроса на удаление ноды, например нажатием комбинации клавиш CTRL+C в окне терминала.
При вызове CTRL+C программа покинет функцию spin и следующая строка освободит ресурсы занятые при выполнении rclpy.init
В целом описанный алгоритм можно описать следующей схемой
3. Следуя структуре созданного пакета откройте файл ноды расположенный по пути test_package/test_package/test_node.py и перенесите в него описанный код.
4. Для успешной установки пакета и Ноды нам необходимо указать входную точку в файле test_package/setup.py. Убедитесь что entry-points заполнено верно и указывает на файл с кодом Ноды
ВАЖНО: если в имени пакета или ноды вы использовали знак дефиса при указании entry point его необходимо заменить на знак подчеркивания
Например:
если для имени использовалось test-node необходимо указать test_node
5. Учитывая что мы планируем использовать библиотеку rclpy необходимо убедится что данная зависимость указана в файле test_package/package.xml
6. Теперь мы готовы собрать пакет с нодой, для этого мы будем использовать инструмент colcon (документация)
В результате сборки будут созданы папки:
- install
- build
- log
7. Чтобы начать пользоваться нашим пакетом и нодой необходимо выполнить
8. После этой команды созданный нами пакет станет доступен для использования. Запустим его используя команду
После запуска согласно логике нашей Ноды будет происходить вызов метода по таймеру и создание записей логов
9. Запросим информацию о запущенной Ноде с помощью команды (мы используем имя Ноды которое мы указывали в конструкторе ноды - its_working_node)
В случае работы через Docker нам будет необходимо выполнить эту команду в новом терминале
Данная команда выводит подробную информацию о конкретном узле - его связи и выполняемые действия. Итак нам доступна следующая информация
Подписчики (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. Далее следуя стандарту создадим файл с логикой нашей Ноды
В целом код похож на наш прошлый пример однако следует отметить несколько ключевых отличий:
- мы также пользуемся библиотекой 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. Настроим терминал чтобы начать пользоваться нашим пакетом и нодой выполнив команду
6. Запустим Ноду
7. Запросим информацию о жизненном цикле ноды выполнив команду в новом окне терминала (где its_working_node имя ноды заданное в конструкторе класса Ноды)
Как мы видим в настоящий момент Нода находится в состоянии - не сконфигурирована (unconfigured) и нам доступны переходы на состояние сконфигурирована (configuring) и выключение (shuttingdown).
8. Давайте выполним переход в состояние configuring, для этого воспользуемся командой
где:
- /its_working_node - имя найшей ноды
- configure - метод перехода в новое состояние
В результате выполнения команды мы должны получить сообщение об успешном завершении перехода
Так же в терминале с запущенной Нодой мы увидим результат работы нашего callback метода настроенного на переход в новое состояние
Проверим ещё раз состояние ноды
Как мы видим из текущего положения нам доступны переходы на очистку(clenup), активацию(activate), выключение(shutdown), и для каждого из вариантов мы видим между какими состояниями будет переход
9. Завершим запуск и активируем нашу Ноду
Переход успешно завершен - Нода полностью готова к работе.
Как мы видим данный подход предоставляет больше контроля над различными этапами жизненного цикла Ноды, что даем нам возможность, реализовывать свою логику проверки/конфигурации Ноды на каждом из этапов, если требуется убедится что все готово к работе прежде, чем полностью запускать Ноду.
Важно отметить - в наших примерах мы следовали ООП подходу в написании кода для нод, на данный момент это рекомендованный способ оформления нод.
P.S. Попробуйте создать свои Ноды. Если возникнут вопросы или темы, которые хотите обсудить — обязательно напишите в комментарии, — будем разбираться вместе и сделаем наши статьи ещё полезнее! 🛠
Подписывайтесь на наш Телеграмм канал, где мы публикуем как сами применяем полученные знания на реальном примере — популярно делимся нашими результатами, вместе будет интереснее https://t.me/it_s_working!