Добро пожаловать в первую часть нашей серии статей о динамической композиции в ROS 2 и о том, как мы в AICA создали на ее основе расширенные функции. Эта статья задумана как исчерпывающее руководство для тех, кто имеет базовые знания в области робототехники и программирования, но может быть новичком в ROS 2 и концепции динамической композиции. Давайте погрузимся!

Что такое РОС 2?

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

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

Одной из фундаментальных концепций ROS и, соответственно, ROS 2 является идея «узла». Узел — это, по сути, процесс, выполняющий вычисления. Узлы общаются друг с другом, передавая сообщения через темы.

Вот простой фрагмент кода, показывающий, как можно создать узел в ROS 2:

#include "rclcpp/rclcpp.hpp"

class MyNode : public rclcpp::Node
{
public:
  MyNode() : Node("my_node") {}
};

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

В этом коде мы определяем новый узел с именем «my_node». Функция rclcpp::init инициализирует ROS 2, а rclcpp::spin заставляет ROS 2 войти в цикл, обрабатывая данные и передавая сообщения, пока программа не будет завершена.

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

Использование ROS 2 и, в частности, концепции узлов дает несколько преимуществ по сравнению с традиционными роботизированными сценариями:

  1. Модульность. Узлы в ROS 2 позволяют использовать модульный подход к созданию роботизированных систем. Каждый узел можно разрабатывать, тестировать и развертывать независимо, что делает систему более управляемой и масштабируемой.
  2. Взаимодействие. Узлы взаимодействуют друг с другом с помощью шаблона публикации-подписки, что позволяет легко интегрировать различные компоненты. Это означает, что вы можете легко комбинировать узлы, разработанные разными командами или даже разными организациями.
  3. Изоляция сбоев. Если узел выходит из строя, это не останавливает всю систему. Это значительное преимущество по сравнению с традиционными монолитными приложениями, где одна ошибка может привести к сбою всей системы.
  4. Производительность в реальном времени: ROS 2 был разработан с учетом производительности в реальном времени. Это имеет решающее значение для многих роботизированных приложений, где время имеет решающее значение.

Хотя мы демонстрируем здесь примеры, написанные на C++, одной из самых полезных функций ROS/ROS 2 является возможность писать узлы как на C++, так и на python (также доступны дополнительные языки, хотя они не обязательно официально поддерживаются). Затем эти узлы сосуществуют и могут обмениваться сообщениями независимо от их основного языка.

Узлы жизненного цикла в ROS 2

В дополнение к стандартным узлам в ROS 2 представлена ​​концепция узла жизненного цикла. Это узел с управляемым жизненным циклом, позволяющий лучше контролировать состояние системы. Это гарантирует, что все элементы были созданы правильно, прежде чем позволить узлу начать выполнять свое поведение. Это также позволяет перезапускать или заменять узлы в режиме онлайн.

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

В жизненном цикле узла есть четыре основных состояния:

  1. Не настроен
  2. Неактивный
  3. Активный
  4. Завершено

И есть семь переходов, подвергаемых надзорному процессу:

  1. Создавать
  2. Настроить
  3. Очистка
  4. Активировать
  5. Деактивировать
  6. Неисправность
  7. Разрушать

На следующей диаграмме из официальной документации показаны все возможные состояния жизненного цикла и переходы между ними.

Вот простой фрагмент кода, показывающий, как можно создать узел жизненного цикла в ROS 2:

#include "rclcpp_lifecycle/lifecycle_node.hpp"

class MyLifecycleNode : public rclcpp_lifecycle::LifecycleNode
{
public:
  MyLifecycleNode() : LifecycleNode("my_lifecycle_node") {}

rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_configure(const rclcpp_lifecycle::State &)
{
  RCLCPP_INFO(get_logger(), "Configuring…");
  // Add your configuration code here
  return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}

rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_activate(const rclcpp_lifecycle::State &)
{
  RCLCPP_INFO(get_logger(), "Activating…");
  // Add your activation code here
  return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}

// Do the same for on_deactivate, on_cleanup, on_shutdown
};

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<MyLifecycleNode>();
  rclcpp::spin(node->get_node_base_interface());
  rclcpp::shutdown();
  return 0;
}

В этом коде мы определяем новый узел жизненного цикла с именем my_lifecycle_node. Этот узел имеет несколько функций обратного вызова, которые вызываются, когда узел переходит в другое состояние. Например, функция on_configure вызывается, когда узел переходит из состояния Unconfigured в состояние Inactive, а функция on_activate вызывается, когда узел переходит в состояние Active. Каждая из этих функций возвращает состояние, указывающее, был ли переход успешным.

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

Сравнение узлов и узлов жизненного цикла

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

Таким образом, по сравнению с традиционными узлами, узлы жизненного цикла имеют дополнительные преимущества:

  1. Управление жизненным циклом. Узлы жизненного цикла в ROS 2 обеспечивают управляемый жизненный цикл для узлов, что позволяет лучше контролировать состояние системы. Это может привести к созданию более надежных систем, способных более изящно обрабатывать ошибки.
  2. Управление состоянием. Узлы жизненного цикла допускают явные переходы состояний, такие как настройка, активация, деактивация и завершение работы. Это может упростить управление и отладку системы по сравнению с традиционными сценариями, когда состояние системы может быть труднее понять.
  3. Онлайн-замена и обновления. Благодаря узлам жизненного цикла можно заменять или обновлять части системы онлайн, не отключая всю систему. Это может привести к увеличению времени безотказной работы и большей гибкости в обслуживании и улучшении системы.
  4. Эффективность использования ресурсов. Разрешение узлам находиться в разных состояниях позволяет лучше использовать ресурсы. Ресурсоемкие узлы можно включать/выключать только при необходимости, экономя вычислительную мощность и, в конечном счете, пропускную способность сети.

Понимание компонентов и динамической композиции

В ROS 2 также представлена ​​концепция компонентов. Компонент — это, по сути, узел, предназначенный для использования с динамической композицией. Он встроен в разделяемую библиотеку и не имеет основной функции, поскольку не предназначен для непосредственного создания экземпляра. Вместо этого он является подклассом rclcpp::Node и регистрируется с помощью макросов из пакета rclcpp_components. Это делает компонент доступным для обнаружения, когда его библиотека загружается в работающий процесс.

Вот пример того, как вы можете определить компонент в ROS 2:

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_components/register_node_macro.hpp"

class MyComponent : public rclcpp::Node
{
public:
  MyComponent() : Node("my_component") {}
};

RCLCPP_COMPONENTS_REGISTER_NODE(MyComponent)

В этом коде мы определяем новый компонент с именем «my_component». Макрос RCLCPP_COMPONENTS_REGISTER_NODE регистрирует компонент, делая его доступным для обнаружения, когда его библиотека загружается в работающий процесс.

Хотя в этом примере мы продемонстрировали узлы, та же концепция применима и к узлам жизненного цикла. В этом случае они просто наследуются от rclcpp_lifecycle::LifecycleNode вместо rclcpp::Node.

Компонент создается не для прямого создания, а для динамической загрузки из файла ComponentManager. Это понятие называется динамической композицией.

Следующий простой фрагмент кода показывает, как можно использовать динамическую композицию в ROS 2:

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_components/component_manager.hpp"

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);
  auto executor = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
  auto manager = std::make_shared<rclcpp_components::ComponentManager>(executor);
  executor->add_node(manager);
  executor->spin();
  rclcpp::shutdown();
  return 0;
}

Используя ComponentManager, вы можете использовать сервисы для динамической загрузки и выгрузки компонентов. Официальная документация представляет подробное руководство по этой функции.

Динамическая композиция в ROS 2 предлагает несколько преимуществ:

  1. Эффективность. Динамическая композиция позволяет объединять несколько узлов в один процесс во время выполнения, уменьшая накладные расходы на межпроцессное взаимодействие. Это может привести к более эффективному использованию системных ресурсов и потенциально более быстрой связи между узлами.
  2. Гибкость. Благодаря динамической композиции расположение процессов и узлов можно изменить во время выполнения. Это обеспечивает высокую степень гибкости в том, как структурирована система, и может адаптироваться к различным требованиям или условиям во время выполнения.
  3. Масштабируемость. Динамическая композиция поддерживает более модульный и масштабируемый подход к созданию роботизированных систем. Это позволяет создавать сложные системы из более простых частей, которые можно разрабатывать, тестировать и развертывать независимо друг от друга.
  4. Настройка производительности. Динамическая композиция позволяет точно контролировать выполнение узлов, что можно использовать для оптимизации производительности системы. Например, узлы, которые часто обмениваются данными, могут быть включены в один и тот же процесс, чтобы уменьшить задержку связи.
  5. Повторное использование кода. Компоненты, предназначенные для динамической композиции, можно повторно использовать в разных контекстах и ​​компоновать по-разному, что способствует повторному использованию кода и снижает трудоемкость разработки.

Таким образом, динамическая композиция — это мощная функция ROS 2, которая может привести к созданию более эффективных, гибких и надежных роботизированных систем.

Ограничения текущей реализации

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

Одним из наиболее ограничивающих аспектов является отсутствие в Python реализации класса ComponentManager, что ограничивает поддержку динамической композиции только узлами на основе C++. Лучшая особенность ROS / ROS 2 заключается в возможности сосуществования узлов, написанных на C++ и Python, таким образом, ограничение одной из лучших функций ROS 2 только C++ — нонсенс. Универсальный ComponentManager, способный беспрепятственно обрабатывать компоненты C++ и Python, еще не представлен.

Мы также считаем, что инфраструктуру можно расширить, в частности, в сочетании с функциями, предлагаемыми узлами жизненного цикла (которые, кстати, еще не доступны в Python). Расширение этой функции может, наконец, включить Task And Motion Planning (TAMP) в ROS 2.

TAMP — это фундаментальный аспект робототехники, который включает определение последовательности действий (планирование задач) и конкретных движений (планирование движений), которые робот должен выполнять для достижения конкретной цели. Это похоже на то, как шеф-повар следует рецепту, где рецепт описывает шаги (задачи), а шеф-повар выясняет, как физически выполнить каждый шаг (движения). TAMP — это сложный, но важный процесс, который позволяет роботам выполнять сложные задачи автономно, принимая во внимание возможности робота, окружающую среду и конкретные требования задачи. Все базовые элементы существуют для разработки такого решения. Специальные компоненты определяют каждое конкретное движение (планирование движения), а динамическая композицияуправляетпереходами между ними. Простой способ определения графа действий (планирование задач) по-прежнему необходим, поскольку файлы запуска ROS, как правило, чрезмерно усложняют этот процесс.

TAMP также в значительной степени опирается на понятие предикатов для запуска переходов между действиями. Предикат — это утверждение о мире, которое может быть как истинным, так и ложным, служащее для понимания системой фактов об окружающей среде. Предикаты, такие как IsPartReady или IsMotionDone, представляют знания таким образом, что робот или ИИ могут их понять и обосновать, помогая в принятии решений и планировании. Например, робот может использовать логическую комбинацию этих предикатов (ворота И), чтобы принять решение о закрытии захвата для захвата объекта. По сути, предикаты помогают роботам понимать свое окружение и определять соответствующие действия. В ROS 2 нет специального решения для обработки предикатов, но базовая передача сообщений логических значений является возможным способом их реализации (хотя дискретные события, т. е. предикаты, которые являются истинными, а затем становятся ложными при использовании, немного сложнее воспроизвести).

В AICA мы используем возможности узлов жизненного цикла и динамической композиции для разработки собственной инфраструктуры TAMP. Мы внедрили Dynamic State Engine, который не только обеспечивает динамическую композицию узлов C++ и Python и узлов жизненного цикла, но также обеспечивает переход состояний жизненного цикла из логики предикатов. Мы также определили текстовый синтаксис, который структурирует эту концепцию, упрощая написание таких переходов. Этот синтаксис позволяет нам затем построить графическое представление компонентов и их перехода, обеспечивая правильную динамическую композицию без кода.

В следующей части этой серии статей мы углубимся в концепции и решения, которые мы разработали. Следите за обновлениями!

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