Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

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

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

Статьи цикла:

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

Полезные ссылки:

Введение

В данной статье мы продолжим рассказывать об исследованиях графа вычислений ROS (ROS Computation Graph).

В прошлый раз мы коснулись темы Нод/Узлов — вычислительных процессов выполняющих узкоспециализированные задачи, но могут ли Ноды поделиться результатами своей работы с другими Нодами, могут ли они получать и использовать эти результаты в своих целях — конечно да, и в этом помогают концепции Тем (Topics) и Сообщений (Messages). В данной статье мы подробнее остановимся на этом.

Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

🔍 Что такое темы и сообщения?

Тема/Топик (topic)

Темы/Топики — это именованные шины, по которым узлы (Node) обмениваются сообщениями.

Представьте, что у вас есть группы экспертов в комнате:

  • Одни производят информацию (например, датчики)
  • Другие потребляют её (алгоритмы управления)Но они не знают друг друга лично! Как им общаться? Через топики — тематические «доски объявлений».
Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

🔑 Ключевые свойства:

  1. Анонимность
    Ноды публикуют и читают данные, не зная друг о друге. Датчик температуры просто «вещает» в эфир, а все заинтересованные системы (кондиционер, логгер, интерфейс) сами «прослушивают» его.

  2. Сильная типизация
    Каждый топик работает только с одним типом сообщений. Попробуете отправить число вместо строки? ROS скажет: «Извините, я вас не понимаю!»
    Важно: Все участники должны использовать одинаковые версии форматов сообщений (проверка по MD5-сумме).


  3. Мультиподключение
    Один топик может иметь:

    Несколько издателей (например, 2 камеры → топик /cameras)

    Множество подписчиков (алгоритм навигации + система записи → топик /cameras)


  4. Транспорт данных
    По умолчанию используется TCPROS (надёжный, потоковый), TCPROS — это транспорт по умолчанию, используемый в ROS, и единственный транспорт, который должны поддерживать клиентские библиотеки. Но есть и UDP (для быстрых, но возможных потерь данных).

🛠 Инструмент topic

Хотите отладить обмен данными? В ROS есть инструмент — topic, предоставляющий следующие полезные команды:

list
ros2 topic list
Список всех активных топиков

info
ros2 topic info /sensor_temp
Показать тип сообщения, издателей и подписчиков
(где /sensor_temp — имя топика)


echo

ros2 topic echo /sensor_temp
Выводить сообщения в реальном времени
(где /sensor_temp — имя топика)


pub
ros2 topic pub /sensor_temp std_msgs/Bool True
Опубликовать сообщение вручную
(где /sensor_temp — имя топика)


bw
ros2 topic bw /sensor_temp
Измерить пропускную способность
(где /sensor_temp — имя топика)


hz
ros2 topic hz /sensor_temp
Проверить частоту обновления
(где /sensor_temp — имя топика)


find
ros2 topic find sensor_msgs/LaserScan
Найти топики по типу сообщения

✉ Сообщения (Messages)

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

📦 Что такое сообщение?

  • Простая структура данных (как конверт с заполненными полями)
  • Содержит типизированные поля: числа (int32, float64), логические значения (bool), строки (string), массивы (float32[])
  • Могут содержать вложенные структуры

Пример

# sensor_msgs/Temperature.msg Header header # Метаданные float32 value # Значение температуры string unit # «Celsius» или «Fahrenheit»

🖋 Как создаются сообщения?

Типы сообщений используют стандартные соглашения об именовании ROS: имя пакета + /msg/ + имя файла .msg.Например, std_msgs/msg/String.msg имеет тип сообщения std_msgs/String.

Через .msg-файлы в папке msg вашего пакета. В виде простого текстового формата см. документацию

# Weather.msg time measurement_time float32 temperature float32 humidity string location

🔐 Важные особенности:

  1. Строгая типизация
    Нельзя отправить строку вместо числа — ROS выдаст ошибку

  2. Версионность через MD5
    При изменении структуры сообщения автоматически меняется его «цифровой отпечаток». Разные версии несовместимы!

  3. Сообщение может включать специальный тип сообщения, называемый «Заголовок», который включает некоторые общие поля метаданных, такие как временная метка и идентификатор кадра. Клиентские библиотеки ROS автоматически установят некоторые из этих полей для вас, если вы этого захотите, поэтому их использование настоятельно рекомендуется.

В заголовке сообщения, показанном ниже, есть три поля:

  1. Поле seq соответствует идентификатору, который автоматически увеличивается по мере отправки сообщений от данного издателя.
  2. Поле stamp хранит информацию о времени, которая должна быть связана с данными в сообщении. Например, в случае лазерного сканирования stamp может соответствовать времени, когда было выполнено сканирование.
  3. Поле frame_id хранит информацию о кадре, которая должна быть связана с данными в сообщении. В случае лазерного сканирования это будет установлено на кадр, в котором было выполнено сканирование.
# Standard metadata for higher-level stamped data types. # This is generally used to communicate timestamped data # in a particular coordinate frame. # # sequence ID: consecutively increasing ID uint32 seq #Two-integer timestamp that is expressed as: # * stamp.sec: seconds (stamp_secs) since epoch (in Python the variable is called 'secs') # * stamp.nsec: nanoseconds since stamp_secs (in Python the variable is called 'nsecs') # time-handling sugar is provided by the client library time stamp #Frame this data is associated with # 0: no frame # 1: global frame string frame_id

Рабочий пример

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

Мы можем поступить двумя способами:

  1. Использовать контейнер с предустановленным ROS и создавать необходимые пакеты в рамках работы с контейнером.

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

Мы последуем подходу №2, поскольку он представит более удобный опыт, а также результат нашей работы будет сохранен.

Вы можете найти все описанные далее файлы в нашем репозитории

Мы планируем реализовать следующую схему:

Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

Итак, приступим.

Издатель (Publisher) сообщений

Будем пользоваться структурой папок, полученной по команде создания нового пакета:

ros2 pkg create --node-name test_pub_node --description its-working-test-package-with-publish-node --license MIT test_package --build-type ament_python

Мы планируем работать с сообщениями. Для этого необходимо объявить требуемые зависимости в файле packge.xml:

<depend>rclpy</depend> <depend>std_msgs</depend>
Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

Далее убедимся что entrypoint для ноды задан верно. Для этого проверим файл setup.py

. . . . entry_points={ 'console_scripts': [ 'test_pub_node = test_package.test_pub_node:main' ], } . . . .
Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

Теперь мы готовы описать логику нашей ноды. Она будет простая. В прошлой статье мы создавали ноду, которая писала логи в заданный интервал времени, а в этот раз сделаем ноду которая будет публиковать сообщения в топик. Также — по таймеру.

import rclpy from rclpy.node import Node from std_msgs.msg import String import sys class TestPublisherNode(Node): def __init__(self): super().__init__("its_publisher_node") self.get_logger().info("Node has been started") self.publisher_ = self.create_publisher(String, 'pub_topic', 10) timer_period = 1 # seconds self.timer = self.create_timer(timer_period, self.timer_callback) self.i = 0 def timer_callback(self): self.get_logger().info("<ROS2> it's working") msg = String() msg.data = 'Hi from publisher node _its working_: %d' % self.i self.publisher_.publish(msg) self.get_logger().info('Publishing message: "%s"' % msg.data) self.i += 1 def main(args=None): if args is None: args = sys.argv rclpy.init(args=args) node = TestPublisherNode() rclpy.spin(node) if __name__ == '__main__': main()

В целом, код ноды похож на наш недавний пример. Однако, есть отличия. Остановимся на них подробнее.

В качестве сообщений мы планируем пока передавать простые текстовые сообщения. Для этого мы добавили импорт строкового типа сообщения, предоставляемого библиотекой rclpy:

from std_msgs.msg import String

Логика конструктора класса нашей ноды была также обновлена:

def __init__(self): super().__init__("its_publisher_node") self.get_logger().info("Node has been started") self.publisher_ = self.create_publisher(String, 'pub_topic', 10) timer_period = 1 # seconds self.timer = self.create_timer(timer_period, self.timer_callback) self.i = 0

Строка

self.publisher_ = self.create_publisher(String, 'pub_topic', 10)

объявляет, что нода публикует сообщения типа String (импортированные из модуля std_msgs.msg) в топик pub_topic и что «размер очереди» равен 10.

Метод обработчика событий таймера также изменился:

def timer_callback(self): self.get_logger().info("<ROS2> it's working") msg = String() msg.data = 'Hi from publisher node _its working_: %d' % self.i self.publisher_.publish(msg) self.get_logger().info('Publishing message: "%s"' % msg.data) self.i += 1

Основные изменения связаны с необходимостью публиковать сообщения.

Для начала мы создаем сообщение для типа string:

msg = String()

Далее мы задаем содержимое нашего сообщения. Это будет текст:

msg.data = 'Hi from publisher node _its working_: %d' % self.i

И в конце мы публикуем созданное сообщение в топик:

self.publisher_.publish(msg)

Для докер-образа мы будем использовать следующий Dockerfile

FROM ros:jazzy SHELL ["/bin/bash", "-c"] WORKDIR /app RUN apt-get update && apt-get install -y RUN apt-get install vim nano -y COPY test_package test_package RUN cd test_package && colcon build COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["bash"]

Для запуска ноды нам будет достаточно собрать и запустить представленный докер-образ:

sudo docker build --tag 'ros-test-publisher' sudo docker image prune sudo docker run -v ./shared:/app/workspace -it --rm 'ros-test-publisher'

После запуска мы увидим логи с информацией о публикуемых сообщениях:

Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

Проинспектируем ROS, чтобы узнать какие данные о созданном топике мы можем получить. Для этого мы будем использовать описанный ранее инструмент ros2 topic.

Запросим список доступных топиков в системе. Выполним в новом терминале следующую команду:

sudo docker exec <имя docker контейнера> g /bin/bash -c ". /opt/ros/jazzy/setup.bash && ros2 topic list"
Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

Мы видим несколько стандартных для Ros топиков:

  • rosout — топик, отвечающий за логирование (http://wiki.ros.org/rosout);

  • Parameter_events — обрабатывает любые события, связанные с параметрами, такие как изменение или удаление параметра, с использованием ParameterEvent.msg;

  • И созданный нами pub_topic;

Запросим данные о нашем топике:

sudo docker exec <имя docker контейнера> /bin/bash -c ". /opt/ros/jazzy/setup.bash && ros2 topic info /pub_topic"
Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

Как мы видим, доступна информация

  • о типе сообщений, публикуемых в данном топике;
  • о количестве нод, публикующих (publisher count) сообщения в данный топик
  • о количестве нод, читающих (subscription count) сообщения из данного топика.

Теперь проверим сообщения, которые доступны в данном топике

sudo docker exec pensive_hawking /bin/bash -c ". /opt/ros/jazzy/setup.bash && ros2 topic echo /pub_topic"
Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

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

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

Можно заметить, что как только нода сообщает нам об отправке сообщения в топик, команда echo обнаруживает факт публикации нового сообщения в топике.

Подписчик (Subscriber) сообщений

Будем пользоваться структурой папок, полученной по команде создания нового пакета:

ros2 pkg create --node-name test_sub_node --description its-working-test-package-with-subscribe-node --license MIT test_package --build-type ament_python

Шаги по настройке зависимостей и entrypoint будут идентичны действиям описанным для пакета publisher’a (смотри предыдущий раздел).

Подготовим логику для ноды, которая будет обрабатывать сообщения топика:

import rclpy from rclpy.node import Node from std_msgs.msg import String import sys class TestSubscriberNode(Node): def __init__(self): super().__init__("its_subscriber_node") self.get_logger().info("Node has been started") self.subscription = self.create_subscription( msg_type=String, topic='pub_topic', callback=self.listener_callback, qos_profile=10) self.subscription # prevent unused variable warning def listener_callback(self, msg: String): self.get_logger().info("<ROS2> it's working and listening to the topic %s" % msg.data) def main(args=None): if args is None: args = sys.argv rclpy.init(args=args) node = TestSubscriberNode() rclpy.spin(node) if __name__ == '__main__': main()

Как видно, текст почти идентичен тексту ноды publisher, однако, есть и отличия:

def __init__(self): super().__init__("its_subscriber_node") self.get_logger().info("Node has been started") self.subscription = self.create_subscription( msg_type=String, topic='pub_topic', callback=self.listener_callback, qos_profile=10) self.subscription # prevent unused variable warning

В конструкторе класса ноды создаётся подписка

self.create_subscription — при создании определяется

  • тип/формат передаваемых сообщений msg_type=String
  • имя топика для чтения: topic='pub_topic'
  • ссылка на метод обработчик сообщений: callback=self.listener_callback
  • очередь сообщений:qos_profile=10

Логика метода обработки получаемых сообщений:

def listener_callback(self, msg: String): self.get_logger().info("<ROS2> it's working and listenning to the topic %s" % msg.data)

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

Учитывая, что структура создаваемого пакета и имя совпадают с прошлым примером Dockerfile будет идентичным

FROM ros:jazzy SHELL ["/bin/bash", "-c"] WORKDIR /app RUN apt-get update && apt-get install -y RUN apt-get install vim nano -y COPY test_package test_package RUN cd test_package && colcon build COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["bash"]

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

sudo docker build --tag 'ros-test-subscriber' sudo docker image prune sudo docker run -v ./shared:/app/workspace -it --rm 'ros-test-subscriber'

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

Обзор основных концепций ROS. Топики (Topics) | Сообщения (Messages).

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

В следующих статьях, используя полученные знания, мы расскажем о развёртывании и настройке SLAM.

Также мы продолжим готовить обзоры основных концепций ROS.

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

1
Начать дискуссию