5.6 Подведём итоги ## 5.1 Вспомним,
что мы узнали на предыдущих уроках и узнаем, о чём будем говорить на
этом занятии
В первом уроке мы узнали, что такое ROS, для чего ROS может быть
полезен инженеру-робототехнику, немного узнали об ОС Ubuntu и её
отличиях от ОС Windows, получили начальные сведения об эмуляторе
терминала Linux, установили ROS 1 и пакеты для создания собственного
программного обеспечения. Во втором уроке мы узнали, что такое рабочее
пространство ROS, создали первый пакет, состоящий из двух узлов, один из
которых является издателем, а второй подписчиком, связали их посредством
механизма сообщений ROS. Также научились создавать файлы запуска,
которые помогают организовать сложные схемы запуска пакетов ROS. В
третьем уроке мы познакомились с простейшим симулятор мобильного робота
turtlesim, поняли его кинематику и подключились к нему с помощью
собственного узла, реализующего взаимодействие с симулятором и как
издатель и как подписчик посредством сообщений ROS. Также в наших уроках
мы постоянно имеем дело с различными командами Linux, ROS и языком
Python3.
На этом занятии мы продолжим знакомиться с основами взаимодействия узлов
ROS.
Мы поговорим о ROS-сервисах, которые являются реализацией ещё одного
подхода к взаимодействию узлов, взаимодействию по запросу. В этом
занятии мы разберёмся с такой архитектурой взаимодействия, создадим
ROS-пакет из двух узлов, основанных на такой архитектуре взаимодействия.
Также попробуем создать пакет, который будет основан на обеих
архитектурах.
Мы также познакомимся с архитектурой действий (actions) в ROS и научимся
выбирать между подпиской на топики и запросами к сервисам в зависимости
от задачи.
Занятие получается большое, поэтому начнём побыстрее.
5.2 Различие в
архитектурах взаимодействий
Давайте вспомним, что такое взаимодействие по подписке. У нас есть
некий издатель, который публикует интересующий нас контент, у нас есть
канал этого издателя. С другой стороны у нас есть подписчики, которые
подключаются к каналу и получают из него интересующую их информацию.
Фактически, всё завязано вокруг канала или в терминах ROS – темы
(топика), в который публикатор (паблишер) что-то выкладывает, а
подписчик (сабскрайбер) оттуда читает. Схематично можно представить
так:
flowchart LR
Subs([Подписчик])
Publ([Издатель])
Publ-. Тема .->Subs
Когда паблишер отправляет данные и те приходят в топик, у подписчика
срабатывает callback-функция, с помощью которой можно получить
обработать данные топика. Подписчиков может быть сколько угодно (на
сколько хватит мощности компьютеров), в том числе и ноль, что не
помешает издателю публиковать информацию. Также, издателю в ROS не
важно, что сделал подписчик с информацией, понравилась она ему или
нет.
В архитектуре взаимодействия по запросу у нас реализована двузвенная
связь “клиент-сервер”. На таком принципе построено ваше взаимодействие с
сайтами в интернете. Вы с помощью клиента, браузера, отправляете запрос
веб-серверу, а тот в ответ отправляет ему данные в формате
гипертекстового документа. Если в процессе интернет-сёрфинга ваши
взаимодействия “клиент-сервер” используют HTTP (протокол передачи
гипертекста), то в ROS для этих целей служит… Служба
(Service).
Итак, у нас есть Клиент (Client), Сервер (Server) и
Служба (Service), Клиент посылает Серверу Запрос
(Request), на который Сервер даёт Ответ (Response).
Схематически это можно представить так:
Пунктирной стрелочкой я намекаю, что у Сервера по Запросу начинается
некая вычислительная деятельность, которая приводит к Ответу. Нужно
добавить, что через Сервис с Сервером может взаимодействовать множество
Клиентов.
Вот вкратце и всё об отличии одной архитектуры от другой. Давайте
займёмся программированием и постигнем Сервисы на практике.
5.3 Разработка пакета
“Клиент-сервер”
Давайте перейдем в наше рабочее пространство, точнее, в тот его
каталог, где мы храним исходники наших пакетов, и создадим ROS-пакет
client_server, а потом создадим в каталоге
client_server подкаталог, в котором мы договорились хранить
тексты наших скриптов с логикой работы узлов ROS – это подкаталог
scripts. Дальше мы создадим в подкаталоге scripts файл
server.py, и откроем его для редактирования. Расскажите,
пожалуйста, как вы это сделаете?
___ У меня получилось так, я открыл новый терминал и подал команды:
cd ~/catkin_ws/src/catkin_create_pkg client_servermkdir client_server/scriptstouch client_server/scripts/server.pygedit client_server/scripts/server.py
Давайте набросаем некоторую заготовку нашего скрипта по аналогии со
скриптом сабскрайбера из второго занятия.
Попробуйте сами написать текст по такому плану: * как всегда, сообщите
системе, какой интерпретатор мы хотим использовать, * импортируйте
модуль ROS для Python, * импортируйте инструмент для работы с 16-битным
целым из модуля стандартных сообщений, * инициируйте ноду с именем
server, * инициируйте подписчик на пустую тему формата
16-битного целого с функцией обратного вызова (иногда дальше по тексту,
особенно в комментариях скриптов, я буду сокращать её ФОВ) с именем
callback, * не забудьте создать заготовку функции
callback, * завершите всё спиннером. ___ У меня вот
так:
#!/usr/bin/env python3import rospyfrom std_msgs.msg import Int16def callback(msg):pass# Я молодец, я знаю про pass. Но потом всё равно сжечь.rospy.init_node('server')rospy.Subscriber('', Int64, callback) rospy.spin()
Вопрос! Зачем я сошёл с ума и заставил вас ещё раз написать код
подписчика, да ещё и недоделанный? Первую часть вопроса оставим, ответ
на неё не подвластен даже психиатрам, давайте займёмся частью второй. И
вообще поймём, почему раньше главным был издатель, а сейчас сервер,
который похож на подписчика. Чем сервер и подписчик похожи. И есть ли
там вообще главный.
Начну с конца – главных нет, для ROS все они одноранговы, и паблишер с
сабскрайбером, и сервер с клиентом. Свобода, равенство, братство.
### 5.3.1 Файлы описания сервисов – .srv-файлы Теперь давайте посмотрим,
как же службы задаются в ROS. Для этого посмотрим на файл
SetBool.srv из каталога со стандартными службами ROS. Вот
так:
bool data # e.g. for hardware enabling / disabling
---
bool success # indicate successful run of triggered service
string message # informational, e.g. for error messages
Не задумывайтесь, на каком языке написан этот файл, давайте так – на
языке описания служб ROS, который очень похож на язык описания сообщений
ROS, а лучше разберём его структуру.
В этом файле есть две части: описание сообщения запроса и описание
сообщения ответа. Сначала идёт часть с описанием запроса, потом
разделитель “—”, затем часть с описанием ответа. Фактически, мы можем
взять тексты из двух msg-файлов, соединить их разделителем (так!) и это
будет корректный srv-файл.
Здесь, в рассматриваемом нами файле, описывается, что мы можем подать
запрос со значением данных Ложь или Клади Истина – это булева
переменная, а в ответ получить комбинацию из булевой переменной об
успешности выполнения и строке с сообщением об ошибке. Кстати, как вы
видите, здесь можно использовать комментарии, аналогичные
питоновским.
Давайте придумаем, где бы мы могли использовать такую службу.
Предположим, вы удалённо управляете освещением в комнате. В можете
подать сигнал “Свет горит” через поле data со значением
Истина – True в питоне или Ложь – False.
Предположим, мы подали сигнал True, то есть, мы хотим
включить свет. Сервер, получив такой запрос, пытается включить свет, в
случае успеха устанавливает значение поля success равное
True, а в случае неудачи этому полю присваивается
False и полю message присваивается строка с
объяснением, почему свет зажечь не удалось “Свет уже горит”,
“Выключатель сломан”, “Электричество отключили за неуплату” или “Ты
бездомный, где ты собрался включать свет!”.
### 5.3.2 Реализация сервера
Замечательно, всё стало понятно, а теперь время поправить наше творение,
наш скрипт сервера.
Для начала давайте изменим модуль и инструмент, так как мы будем
пользоваться не стандартными сообщениями, а стандартными службами. И
импортируем оттуда только что разобранную нами службу.
from std_srvs.srv import SetBool
Теперь мы должны выкинуть строчку инициализации подписчика, ну или
исправить её на вызов инициализации сервиса. Тут внимательнее, я не
опечатался, именно сервиса, не сервера. Имя службе дадим, например,
test_service, форматом её будет SetBool, имя
функции обратного вызова не меняем, она нам нужна:
rospy.Service('test_service', SetBool, callback)
А вот с самой функцией придётся поработать. Во-первых, поменяем имя
аргумента для наглядности, так как это запрос, заменим msg
на req, и изменим тело функции.
Здесь придётся немного напрячься. Нам надо где-то взять структуру данных
ответа SetBool.srv, когда вы будете писать свои srv-файлы, вы
увидите, что ROS при постройке рабочего пространства создаёт для каждого
из таких файлов файл с описанием запроса и описанием ответа, которые мы
должны импортировать. Теперь нам точно придётся импортировать инструмент
для ответа, с запросом, надеюсь, обойдётся из-за нестрогой типизации в
питоне.
Хорошо, допишем строку с импортом так:
from std_srvs.srv import SetBool, SetBoolResponse
И перепишем функцию обратного вызова:
def callback(req): # Аргументом запрос response = SetBoolResponse() # Инициируем переменную ответаif req.data ==True: # Из запроса берём его единственное поле data и проверяем на истинность response.success =True# Здесь будем считать, что у нас сервис отработал успешно response.message ="Your device was enabled"# Об успехе тоже не надо стесняться сообщатьelse: # В случае ложного значения response.success =True# Здесь будем считать, что у нас сервис отработал успешно response.message ="Your device was disabled"# Не то, чтобы это неудача, просто устройство отключилиreturn response # Эта строка "возвращает" данные из функции
По-моему получился хороший код. А как на ваш взгляд, нет ли чего-то
некорректного.
___ Мы и в случае удачи и в случае неудачи присвоили переменной
response.success значение True. Мы ошиблись? А
вот и нет. Хотя кажется странным, в данном случае мы всегда
устанавливаем response.success = True, потому что результат
выполнения мы уже отдаём через другое поле — строку
message.
В ROS на Python значение success в ответе сервиса — часть
сообщения, его смысл определяет разработчик. Оно не влияет на работу
клиента или сервера. Клиент получает всё сообщение и сам решает, как его
интерпретировать.
Это корректно: клиент анализирует message, а не только
флаг success. Установка success = False не
приведёт к ошибке — клиент всё равно получит ответ. В C++ поле
success играет более важную роль, но в Python акцент на
том, чтобы данные были понятны человеку через message. В
C++ возврат false из callback-функции означает ошибку на
уровне сервиса, а success — логика выполнения. В Python
значение success не влияет на работу взаимодействия
“клиент-сервер” ROS напрямую — всё зависит от интерпретации клиента.
Ближе к концу поэкспериментируем с этим, но мне кажется, здесь мы можем
возвращать и True и False.
Хорошо, давайте теперь посмотрим на весь код целиком:
#!/usr/bin/env python3# Импортируем rospy и инструменты для SetBool из набора стандартных сервисовimport rospyfrom std_srvs.srv import SetBool, SetBoolResponse# ФОВ на вызова сервисаdef callback(req): # Аргументом запрос response = SetBoolResponse() # Инициируем переменную ответаif req.data ==True: # Из запроса берём его единственное поле data и проверяем на истинность response.success =True# Здесь будем считать, что у нас сервис отработал успешно response.message ="Your device was enabled"# Об успехе тоже не надо стесняться сообщатьelse: # В случае ложного значения response.success =True# Здесь будем считать, что у нас сервис отработал успешно response.message ="Your device was disabled"# Не то, чтобы это неудача, просто устройство отключилиreturn response # Эта строка "возвращает" данные из функцииrospy.init_node('server') # Инициируем узелrospy.Service('test_service', SetBool, callback) # Инициируем сервис rospy.spin() # Включаем спиннер
Теперь давайте сохраним текст скрипта и запустим узел. Что для этого
нужно сделать?
___ Я сделал так:
# Открыть терминал Ctrl+Alt+troscore# Открыть ещё один терминал и в нём # Сделать скрипт исполняемымchmod u+x ~/catkin_ws/src/client_server/scripts/server.py# Перейти в рабочее пространство и построить его, чтобы познакомить ROS с нашим новым пакетомcd ~/catkin_ws/catkin build# Закрыть терминал и открыть новый, чтобы подгрузить настройки рабочей среды# Запустить узел сервераrosrun client_server server.py
У меня всё запустилось, но не выдало ни какой информации (что плохо,
надо было вывести сообщение о том, что сервер запущен). Как проверить,
что наш сервис работает?
Откроем новый терминал и подадим команду rosservice list,
та ожидаемо выдаст нам список работающих сервисов, среди которых есть
наш /test_service:
### 5.3.3 Реализация клиента Наш сервис работает, остальные сервисы нам
неинтересны, пришла пора писать клиент. Создайте в подкаталоге со
скриптами нашего ROS-пакета файл client.py.
И в нём давайте начнём писать скрипт по плану:
* сообщите системе, какой интерпретатор мы хотим использовать, *
импортируйте модуль ROS для Python, * импортируйте инструменты SetBool и
SetBoolRequest из модуля стандартных сервисов, * инициируйте ноду с
именем client. Здесь остановимся и посмотрим, что получилось. У
меня получилось так:
#!/usr/bin/env python3# Импортируем rospy и инструменты для SetBool из набора стандартных сервисовimport rospyfrom std_srvs.srv import SetBool, SetBoolRequestrospy.init_node('client') # Инициируем узел
Вот и в ROS-питоне появился инструмент SetBoolRequest,
что логично, ведь мы должны послать запрос. Инструмент с окончанием
Request также создаётся по умолчанию для сервисов. Теперь
инициируем “запросчик”, если можно так сказать, для сервиса:
# Инициируем запросчик для сервиса test_service типа SetBoolclient = rospy.ServiceProxy ('test_service', SetBool)
В ROS-питоне название мне нравится больше, чем в C++. Там нам
пришлось бы иметь дело с функцией с названием
serviceClient, что, как мне кажется, может несколько
запутать. В любом случае, когда будете писать ROS-клиент на С++, помните
об этом.
Кстати, путанице. Не пугайтесь, пожалуйста, того, что у нас и имя
скрипта client и имя узла client и переменная тоже client. У них у всех
разные хозяева. Файловая система видит имя файла, но не переменную и имя
ноды. ROS видит имя ноды и имя файла, но прекрасно их различает, но не
видит имя переменной. Интерпретатор видит всех, но для него это тоже
совершенно разные объекты. Мы же не путаем людей с одинаковыми именами,
если знакомы с ними.
Продолжим. Теперь нам надо понять, есть ли в ROS-сети сервер, к
которому мы хотим обратиться через сервис, или, проще говоря,
посмотреть, есть ли сервис. А так как без сервиса наше клиентское
приложение бессмысленно, мы не должны его выполнять, пока сервис не
появится. Подождём появления сервиса:
# Ждать сервис и ничего не делатьclient.wait_for_service()
Мы здесь не указываем, какой сервис ждать, так как мы показали через
ServiceProxy, что работаем с сервисом
/test_service. После того как мы дождались появления сервиса,
мы можем подать ему запрос, который, естественно, должны для начала
сформировать. Инициируем переменную request через
присвоение её SetBoolRequest() (в питоне всё объект и здесь
мы порождаем объект request класса SetBoolRequest).
Дальше присваиваем полю данных, имя которого прописано в srv-файле,
значение. Всё это занимает пару строчек:
# Инициализировать объект запроса requestrequest = SetBoolRequest()# Задать значение данныхrequest.data =True
Теперь давайте кольнем сервис. Стоп, это опять из С++, там вызов
сервиса осуществляется функцией call. Здесь, в питоне, мы
вызовем обращение к сервису тем, что попросим вызвать объект как
функцию. Есть у питона такая возможность, внутри класса можно
запрограммировать функцию __call__ и вызывать объект, порождённый от
класса, с некоторым параметром. Питон прост, но сложен. Необязательный
пример:
class Multiply:def__init__(self, value):self.value = valuedef__call__(self, x):return x *self.value# Здесь мы неявно вызвали __init__ классаmul1 = Multiply(5) # Теперь mul1 имеет значение 5# Здесь неявно вызывается __call__ классаresult = mul1(10) # Теперь result имеет значение 5*10=50print(result)
Так и в случае с нашим клиентом, вызов
response = client(request) неявно вызовет __call__ класса
SetBoolRequest, в котором уже будет обращение к сервису,
получение ответа от него и передача полученного значения переменой
response. Ну и напечатаем ответ. Получились заключительные
строчки:
# Послать запрос и получить ответresponse = client(request)# Ответ напечататьprint(response)
Объединим всё в тексте скрипта client.py:
#!/usr/bin/env python3# Импортируем rospy и инструменты для SetBool из набора стандартных сервисовimport rospyfrom std_srvs.srv import SetBool, SetBoolRequestrospy.init_node('client') # Инициируем узел# Инициируем запросчик для сервиса test_service типа SetBoolclient = rospy.ServiceProxy ('test_service', SetBool)# Ждать сервис и ничего не делатьclient.wait_for_service()# Инициализировать объект запроса requestrequest = SetBoolRequest()# Задать значение данныхrequest.data =True# Послать запрос и получить ответresponse = client(request)# Ответ напечататьprint(response)
Сделаем скрипт исполняемым, проверим, запущен ли сервер и запустим
клиент:
rosrun client_server client.py
rosrun-client.png
Отлично, всё получилось. Клиент подал запрос, получил ответ и вышел в
приглашение терминала. Сервер остался в режиме ожидания. Давайте
напоследок проверим, что будет, если мы отправим False в
ответ на запрос и завершим на этом. Ну и добавим для сервера сообщение о
том, что он запущен и ждёт запроса. Попробуйте это сделать сами.
___ В сервере я заменил в строчках в callback-функции True
на False:
else: # В случае ложного значения response.success =False# Здесь будем считать, что у нас сервис отработал успешно
И добавил строку перед rospy.spin():
print ("Our server is waiting of a call now")
В клиенте заменил True на False в строчке
задания данных:
# Задать значение данныхrequest.data =False
Поверка показывает, что, как мы и предполагали, это лишь передача
данных и такая замена не приведёт к поломке механизма взаимодействия
через службу.
А теперь давайте попробуем написать свой проект с блэкджеком и…
темами и сервисами. Да так, чтобы темы сервисы использовали структуры
данных, которые мы придумаем сами.
5.4
Разработка клиент-серверного приложения с комплексным
взаимодействием
5.4.1 Создание файлов описания
При знакомстве с архитектурами издатель-подписчик и клиент-сервер мы
использовали уже готовые файлы описаний для сообщений и сервисов. А что
случится, если мы не найдём сообщение или сервис подходящего типа.
Ничего страшного, просто нам придётся описать такой тип самим и дать
пакету доступ к этому типу.
Здесь нам придется поработать с несколькими каталогами и файлами.
Давайте создадим новый ROS-пакет communicato, на котором и
будем тренироваться. В его каталоге создадим подкаталоги scripts, msg и
srv. Дальше мы создадим в подкаталоге scripts файлы
server.py и client.py. Расскажите, пожалуйста, как вы
это сделаете?
___ У меня получилось так, я открыл новый терминал и подал команды:
cd ~/catkin_ws/src/catkin_create_pkg communicatomkdir communicato/scriptsmkdir communicato/msgmkdir communicato/srvtouch communicato/scripts/server.pytouch communicato/scripts/client.py
Теперь давайте предположим, что у нас есть прибор, например термометр
в климатической камере, который должен вести трансляцию значений
температуры в камере и состояние замка двери. Но делать он это должен
только тогда, когда мы проводим эксперимент, задание на который даёт
программа, собирающая данные от этого термометра. Показания термометра
должны помечаться именем эксперимента, который задаёт программа сбора
данных.
Здесь напрашивается такая схема взаимодействия: у нас есть клиент и есть
сервер к которому подключен термометр. У сервера также есть возможность
публиковать данные – выступать паблишером. Узел сбора данных является
клиентом, дающим серверу команду, также этот узел имеет возможность
подписаться на данные термометра.
graph LR
subgraph ServerNode[Узел сервера]
Server(((Сервер)))
Publisher(((Издатель)))
Server-.-Publisher
end
subgraph ClientNode[Узел клиента]
Client(((Клиент)))
Subscriber(((Подписчик)))
end
Client-->S[[Сервис]]
S-->Server
Publisher==>T[[Тема]]
T==>Subscriber
Server-->S-->Client
style ClientNode fill:#9ec6e1,stroke:#333,stroke-width:4px
style ServerNode fill:#66cd61,stroke:#333,stroke-width:4px
Давайте набросаем структуру данных для сообщения:
* Имя эксперимента – строка; * Время получения данных – некая структура
с данными о времени; * Температура – число с плавающей точкой; * Замок
закрыт – булева переменная.
Теперь структура сервиса:
* Запрос: * Имя эксперимента – строка; * Ответ: * Состояние готовности
камеры – булева переменная; * Текстовая расшифровка состояния –
строка.
Как мы говорили ранее, структуры данных для взаимодействий
описываются специальными файлам, с расширением .msg для
сообщений и .srv для сервисов, которые в структуре пакета лежат
в папках msg и srv соответственно.
В нашем случае необходимо создать файлы для одного сообщения и одного
сервиса. Приступим. Первым делом нам надо посмотреть в ROS Wiki, как
создавать файлы сообщений и файлы сервисов. Причём, файлы
сообщений надо понять в первую очередь, так как, как я говорил ранее
файлы сервисов фактически собираются из файлов сообщений, из которых
составлены запрос и ответ, разделённые конструкцией ---.
Итак, начнём писать наш файл сообщения temperature.msg, который
для начала создадим в каталоге msg и откроем его для
редактирования.
Смотрим тип для строки – string. Запишем тип и имя поля
сообщения string name. Дальше ищем в примитивах время –
time и создаём поле для записи текущего времени
time currtime. Для температуры подойдёт
float32, запишем float32 temperature, а для
состояния замка bool, пусть мы проверяем, закрыт ли замок –
bool locked. А теперь соберём всё это в один файл в формате
записи каждого нового поля с новой строки:
string name # Имя эксперимента
time currtime # Время получения данных
float32 temperature # Температура
bool locked # Указывает, закрыт ли замок (Истина — закрыт)
Сохраним файл и закроем редактор.
А теперь по тому же принципу создадим файл command.srv в
каталоге srv:
Так же, как и msg-файлы, srv-файлы задают не просто формат обмена
данными, а некоторый контракт между участниками
взаимодействия.
Контракт — это чётко определённое соглашение о том, какие данные будут
переданы и в какой форме.
Для сервисов это значит: клиент знает, какие поля должен заполнить в
запросе, а сервер гарантирует, что вернёт ответ в строго заданной
структуре. Такой подход позволяет заранее согласовать поведение узлов,
даже если они написаны на разных языках или разными
разработчиками.
В случае сообщений и msg-файлов контракт гарантирует, что издатель и
подписчик будут работать с одинаковой структурой данных.
Именно благодаря этим контрактам система ROS остаётся гибкой, но при
этом предсказуемой и надёжной.
Теперь добавим в srv-файл такой текст с пояснениями в
комментариях:
# Запрос
string name # Имя эксперимента
---
# Ответ
bool ready # Готовность камеры
string message # Текстовое сообщение о готовности
Теперь у нас есть два файла со структурой данных, которые мы должны
“подключить” к нашим текстам скриптов. Как мы это делали в наших
предыдущих программах? ___ Мы подключали Python-модуль из которого брали
необходимые инструменты для работы с сервисом или сообщением. Как нам
это сделать теперь, когда у нас явно нет таких модулей и инструментов?
Здесь нам придётся поработать с файлами, конфигурирующими наш пакет –
CMakeLists.txt и package.xml.
Начнём с package.xml и откроем его для редактирования
gedit ~/catkin_ws/src/communicato/package.xml. Сначала мы
дадим распоряжение порождать файлы во время построения рабочей области.
В файле package.xml найдём строку
<build_depend>message_generation</build_depend>,
которая, скорее всего, закомментирована. От этого комментария мы и
должны избавиться, то есть, строку:
Не забудьте убрать и открывающую и закрывающую части
комментария.
Теперь надо найти и раскомментировать строку
<exec_depend>message_runtime</exec_depend>
(есть вероятность, что где-то в документации вы можете увидеть её с
другим тегом
<run_depend>message_runtime</run_depend>),
говорящую, что надо использовать сообщения во время выполнения. Из
такой:
Сохраним и закроем файл и перейдём к редактированию
CMakeLists.txt –
gedit ~/catkin_ws/src/communicato/CMakeLists.txt. В
соответствии с документацией мы должны были бы добавить зависимость
message_generation в вызов
find_package(catkin REQUIRED COMPONENTS зависимости), но я
нашёл там незакомментированным только вызов
find_package(catkin REQUIRED), поэтому я предлагаю после
этой строки вставить строку:
В принципе, этот вызов можно вставить в любое место файла до
вызова catkin_package. Если заметили, то здесь я
вставил ещё и std_msgs. Стандартные сообщения нужны, потому
что мы будем к ним обращаться и без них даже построить рабочее
пространство не сможем.
Теперь найдём и раскомментируем вызов add_message_files и
поправим его вот так:
add_message_files( FILES temperature.msg )
А следом и вызов add_service_files:
add_service_files( FILES command.srv )
Наконец, найдём вызов generate_messages и
раскомментируем его целиком. Он должен получиться таким:
generate_messages( DEPENDENCIES std_msgs # Or other packages containing msgs )
Внимание! Все эти вызовы должны быть сделаны до
вызова catkin_package.
Теперь найдём незакомментированный вызов catkin_package,
раскомментируем в нём строку
# CATKIN_DEPENDS other_catkin_pkg, удалив в начале строк
знаки #, и заменим в ней имя пакета на
message_runtime. Ну или напишем между открывающей и
закрывающей скобкой catkin_package строку полностью:
Если построили успешно, посмотрим на каталог подкаталог нашего
рабочего пространства devel/include, в котором генерируются
подключаемые файлы, ну или модули питона, для создаваемых нами пакетов.
Мы обнаружим там подкаталог с именем нашего пакета communicato,
в который тоже заглянем и выведем на экран его содержимое. Итак:
cd ~/catkin_ws/devel/include/ls
include-dir.png
cd communicatols
pkg-includes.png
Великолепно! Здесь у нас полный набор модулей для сервиса и модуль
для сообщения температуры. Пора начинать писать узлы.
Давайте начнём с клиент-серверного соединения.
5.4.2 Сервер
Фактически, мы только что писали взаимодействие через сервер, поэтому
нам всё должно быть понятно, кроме того, как подключить модули.
Подключать модули от собственных сообщений и сервисов довольно просто –
надо в качестве имени модуля использовать имя пакета и .srv
для сервисов или .msg для сообщений. Ещё раз, в нашем
пакете communicato наши сервисы импортируются из
communicato.srv, а наши сообщения из
communicato.msg.
Теперь мы готовы написать скрипт сервера. Так как у нас нет никакой
климатической камеры, состояние её “работоспособности” мы сымитируем,
заведём переменную in_action, которой присвоим значение
True или False, в зависимости от
необходимости. Также мы инициируем переменную trial_name, в
которой будем хранить имя эксперимента, полученное от клиента. Сервис мы
инициируем с именем 'temp_service'. В принципе, с этими
уточнениями и с комментариями по тексту, я думаю, скрипт должен быть
понятен:
#! /usr/bin/env python3# Импортируем rospy и инструменты для commandimport rospyfrom communicato.srv import command, commandResponse # Из модуля communicato.srv импортируем инструменты для работы с сервисом и ответом сервисаin_action =True# Переменная, в которой хранится состояние "устройства"trial_name =""# Имя эксперимента инициируем здесь, чтобы иметь к нему доступ по всему коду # ФОВ на вызов сервисаdef callback(req): # Аргументом запросglobal trial_name # Чтобы локальная переменная не перекрыла глобальную, укажем, что в этой функции используется только глобальная trial_name response = commandResponse() # Инициируем переменную ответа trial_name = req.name # Сервер кольнули именем эксперимента, сохраним егоprint ("Start trial with name --", trial_name, "--") # Сообщим о начале экспериментаif in_action ==True: # Если "устройство" включено (фиктивно, определили наверху переменной in_action) response.ready =True# Отвечаем, что оно готово response.message ="Device is ready"# И сообщаем об этом жеelse: # Если не включено response.ready =False# Отвечаем, что "устройство" не готово response.message ="Device is unplugged"# И сообщаем, что оно не подключено к узлуreturn response # Эта строка "возвращает" данные из функцииrospy.init_node('server') # Инициируем узелrospy.Service('/temp_service', command, callback) # Инициируем сервис print ("Our server is waiting of a call now")rospy.spin() # Включаем спиннер
Не забудем сделать его исполняемым
chmod u+x ~/catkin_ws/src/communicato/scripts/server.py и
перейдём к тексту клиента, который практически не отличается от того,
что мы писали недавно.
5.4.3 Клиент
Текст клиента я практически полностью скопировал из предыдущего узла
клиента. Правки внесены только для того, чтобы заменить стандартный
сервис на наш. С комментариями текст не должен вызвать проблем с
пониманием:
#! /usr/bin/env python3# Импортируем rospy и инструменты для SetBool из набора стандартных сервисовimport rospyfrom communicato.srv import command, commandRequest # Из модуля communicato.srv импортируем инструменты для работы с сервисом и ответом сервисаrospy.init_node('client') # Инициируем узел# Инициируем запросчик для сервиса temp_service типа commandclient = rospy.ServiceProxy ('/temp_service', command)# Ждать сервис и ничего не делатьclient.wait_for_service()# Инициализировать объект запроса requestrequest = commandRequest()# Задать имя экспериментаrequest.name ="Trial number one"# Послать запрос и получить ответresponse = client(request)# Ответ напечататьprint(response)
Также не забудем сделать его исполняемым
chmod u+x ~/catkin_ws/src/communicato/scripts/client.py и
испытаем сервис. Откроем два новых терминала и запустим в одном из них
сервер rosrun communicato server.py, а в другом терминале
клиент rosrun communicato client.py:
Хорошо, мы установили связь с помощью сервиса. Остановим сервер и
снова откроем наши скрипты для редактирования, пришло время заняться
взаимодействием издатель-подписчик.
5.4.4 Издатель
Издателя, как и договаривались, мы разместим в том же узле, что и
сервер. Импортируем инструмент для публикации сообщения:
from communicato.msg import temperature
После строки trial_name = "" добавим строку
инициализации паблишера:
Это плохо с точки зрения читаемости кода, так как мы начали основную
программу, после этого пишем тела различных функций, а после этого снова
пишем основную часть программы. Лучше было бы оформить по другому, в
конце занятия я постараюсь это сделать, но сейчас нам главное понять
принцип.
Далее запишем тела функций, о которых я сказал. Это будут две функции,
функция имитации работы нашей климатической камеры и функция обратного
вызова, которая будет вызываться с частотой 10 Гц, что мы настроим
позже.
Итак, имитационная функция:
def imitation ():# Имитация работы климатической камеры. Дверь открыта - температура повышается, дверь закрыта - температура понижается.# Если Температура упала ниже +26, дверь откроется, если выросла выше 36.6, дверь закроется.ifnothasattr(imitation, "door_locked"): imitation.door_locked =Trueifnothasattr(imitation, "Temperature"): imitation.Temperature =36.6if imitation.door_locked ==True: imitation.Temperature -=0.1else: imitation.Temperature +=0.1if imitation.Temperature <26: imitation.door_locked =Falseif imitation.Temperature >36.6: imitation.door_locked =Truereturn imitation.door_locked, imitation.Temperature
В ней мы “управляем” температурой и состоянием двери. Логика работы
функции понятна, и можно было бы переходить к описанию следующей
функции, однако стоит задержаться на переменных внутри этой функции.
Переменные door_locked и Temperature здесь
обычные локальные переменные, которые почему-то обрабатываются необычным
образом. Дела в том, что нам надо хранить состояние этих переменных
после того, как функция отработала и если не принят мер к переводу этих
перемен в статические, говоря языком программиста на С, мы потеряем эти
значения. Теперь вспомним, что в Python всё объект, поэтому мы можем
посмотреть, есть ли у объекта-функции атрибуты door_locked
и Temperature, если нет, инициализировать их какими-то
начальными значениями, а потом использовать как атрибуты, значения
которых уже никуда не денутся. По крайней мере, пока функция сама не
уйдёт, в скажем так, в кремниевый рай.
Теперь перейдём к функции, которая отвечает за публикацию в топик:
def pub_callback(event):global trial_name # В функции используем глобальную переменную trial_name tmprt = temperature() # Инициируем переменную с данными о работе нашего устройстваif trial_name !="": # Чтобы передавать данные, если эксперимента нет, передаём только тогда, когда имя не пустое# Заполним атрибуты переменной для публикации tmprt.name = trial_name # Имя tmprt.currtime = rospy.Time.now() # Время tmprt.locked, tmprt.temperature = imitation() # Данные от устройстваifnot rospy.is_shutdown(): # Если узел не остановлен, pub.publish(tmprt) # публикуем
Как мне кажется, функция не сложная для понимания, только я бы хотел
остановиться в ней на паре моментов.
Первое, это замечательный костыль, с которым наш паблишер теперь шагает
по жизни – if trial_name != "":. Так как мы не
предусмотрели сервиса по включению и выключению публикации, то
приходится использовать имя эксперимента, как флаг и уповать на то, что
пользователь не передаст пустое имя, по которому мы решаем, что
эксперимента-то и нет. А первое, что сделает ленивый пользователь –
плюнет на именования экспериментов. В общем, так лучше не делать, а
использовать отдельную булеву переменную или сервис для управления
состоянием.
Второе – наоборот, – как в питоне делать стоит. Если мы вернёмся к
функции имитации, мы увидим, что она возвращает пару значений разных
типов return imitation.door_locked, imitation.Temperature.
Она возвращает кортеж,
список переменных различных типов, который нам надо создать, просто
перечислив переменные через запятую, что очень удобно. При вызове
функции мы присвоим её результат также кортежу, который создадим также,
перечислив переменные через запятую
tmprt.locked, tmprt.temperature = imitation(). Нам осталось
только организовать вызов функции публикации с частотой 10 Гц или каждые
0.1 секунды. Перед печатью сообщения об ожидании запроса, инициируем
таймер, который будет срабатывать с периодичностью, заданной
rospy.Duration(0.1), вызывая на это событие функцию
обратного вызова pub_callback
rospy.Timer(rospy.Duration(0.1), pub_callback)
Получилось достаточно сложно, поэтому давайте приведём теперь весь
код целиком:
#! /usr/bin/env python3# Импортируем rospy и инструменты для commandimport rospyfrom communicato.srv import command, commandResponse # Из модуля communicato.srv импортируем инструменты для работы с сервисом и ответом сервисаfrom communicato.msg import temperaturein_action =True# Переменная, в которой хранится состояние "устройства"trial_name =""# Имя эксперимента инициируем здесь, чтобы иметь к нему доступ по всему коду pub = rospy.Publisher('/temperature', temperature, queue_size =1)# ФОВ на вызов сервисаdef callback(req): # Аргументом запросglobal trial_name # Чтобы локальная переменная не перекрыла глобальную, укажем, что в этой функции используется только глобальная trial_name response = commandResponse() # Инициируем переменную ответа trial_name = req.name # Сервер кольнули именем эксперимента, сохраним егоprint ("Start trial with name --", trial_name, "--") # Сообщим о начале экспериментаif in_action ==True: # Если "устройство" включено (фиктивно, определили наверху переменной in_action) response.ready =True# Отвечаем, что оно готово response.message ="Device is ready"# И сообщаем об этом жеelse: # Если не включено response.ready =False# Отвечаем, что "устройство" не готово response.message ="Device is unplugged"# И сообщаем, что оно не подключено к узлуreturn response # Эта строка "возвращает" данные из функцииdef imitation ():# Имитация работы климатической камеры. Дверь открыта -температура повышается, дверь закрыта - температура понижается.# Если Температура упала ниже +26, дверь откроется, если выросла выше 36.6, дверь закроется.ifnothasattr(imitation, "door_locked"): imitation.door_locked =Trueifnothasattr(imitation, "Temperature"): imitation.Temperature =36.6if imitation.door_locked ==True: imitation.Temperature -=0.1else: imitation.Temperature +=0.1if imitation.Temperature <26: imitation.door_locked =Falseif imitation.Temperature >36.6: imitation.door_locked =Truereturn imitation.door_locked, imitation.Temperaturedef pub_callback(event):global trial_name # В функции используем глобальную переменную trial_name tmprt = temperature() # Инициируем переменную с данными о работе нашего устройстваif trial_name !="": # Чтобы передавать данные, если эксперимента нет, передаём только тогда, когда имя не пустое# Заполним атрибуты переменной для публикации tmprt.name = trial_name # Имя tmprt.currtime = rospy.Time.now() # Время tmprt.locked, tmprt.temperature = imitation() # Данные от устройстваifnot rospy.is_shutdown(): # Если узел не остановлен, pub.publish(tmprt) # публикуемrospy.init_node('server') # Инициируем узелrospy.Service('/temp_service', command, callback) # Инициируем сервис rospy.Timer(rospy.Duration(0.1), pub_callback) # Вызывать pub_callback через 0.1 секprint ("Our server is waiting of a call now")rospy.spin() # Включаем спиннер
И перейдём к скрипту клиента, где организуем подписчик.
5.4.5 Подписчик
Я сразу приведу модифицированный код клиента, а после мы разберём
изменения в нём:
#! /usr/bin/env python3# Импортируем rospy и инструменты для SetBool из набора стандартных сервисовimport rospyfrom communicato.srv import command, commandRequest # Из модуля communicato.srv импортируем инструменты для работы с сервисом и ответом сервисаfrom communicato.msg import temperature # Как и для сервера, импортируем инструмент сообщенияdef callback(msg): # Создадим ФОВ для подписчикаprint('Current temperature is:', msg.temperature, end=" ") # Напечатаем температуруif msg.locked ==True: # и состояние замка двериprint ("and the door is locked")else:print ("and the door is unlocked")rospy.init_node('client') # Инициируем узел# Инициализируем подписчик с использованием ФОВrospy.Subscriber('/temperature', temperature, callback) # Инициализируем запросчик для сервиса temp_service типа commandclient = rospy.ServiceProxy ('/temp_service', command)# Ждать сервис и ничего не делатьclient.wait_for_service()# Инициализировать объект запроса requestrequest = commandRequest()# Задать имя эксперимента# request.name = "Trial number one"request.name = rospy.get_param('~trial_name')# Послать запрос и получить ответresponse = client(request)# Ответ напечататьprint(response)rospy.spin()
Во-первых, мы импортировали инструмент
from communicato.msg import temperature. Во-вторых, мы
создали callback-функцию, которая печатает данные о температуре и
состоянии двери. В-третьих, мы инициализировали сабскрайбер
rospy.Subscriber('/temperature', temperature, callback). И
в-четвёртых мы заменили жёсткое присвоение имени эксперимента
request.name = rospy.get_param('~trial_name').
Начнём с описания этого действия. Мы с вами уже говорили о параметрах
ROS (TO DO: Вернуть пятому уроку номер четыре), вот и
здесь мы воспользуемся частным ROS-параметром, который будем задавать
сразу при запуске узла клиента командой rosrun. Запускать
узел будем такой командой:
rosrun communicato client.py _trial_name:=Num1
В качестве параметра можете задать любое имя эксперимента, только
учтите, строку с пробелами надо взять в кавычки.
Теперь перейдём к “во-вторых”, то есть к callback-функции, а точнее, к
первом её оператору print, в качестве последнего параметра
которого я использую конструкцию end=" ". В принципе, эта
конструкция неявно присутствует в каждом операторе print,
только там, где мы её не видим, она выглядит так end="\n",
то есть содержит в себе символ перевода строки, который мы заменили на
символ пробела. Таким способом мы избегаем автоматического перевода
строки при печати составной строки.
5.4.6 Доработка сервера
Давайте всё же приведём сервер в читаемый вид. Не по формату PEP8, конечно, что было бы хорошо, но
сейчас наша задача учиться ROS, а не оформлению программ.
Я не буду здесь особо много рассказывать, что сделано, код достаточно
подробно прокомментирован. Главное, я оформлю основной код скрипта в
отдельную главную функцию и вызову и исправлю некоторые недочёты.
#! /usr/bin/env python3# Импортируем rospy и инструменты для commandimport rospyfrom communicato.srv import command, commandResponse # Из модуля communicato.srv импортируем инструменты для работы с сервисом и ответом сервисаfrom communicato.msg import temperature# ФОВ на вызоа сервисаdef callback(req): # Аргументом запросglobal trial_name # Чтобы локальная переменная не перекрыла глобальную, укажем, что в этой функции используется только глобальная trial_name response = commandResponse() # Инициируем переменную ответа trial_name = req.name # Сервер кольнули именем эксперимента, сохраним егоif in_action ==True: # Если "устройство" включено (фиктивно, определили наверху переменной in_action)if trial_name =="/s": # См. ниже про костыли и велосипедыprint ("Trial was stopped") # Сообщим об окончании экспериментаelse:print ("Start trial with name --", trial_name, "--") # Сообщим о начале эксперимента, имеет смысл, если устройство работает response.ready =True# Отвечаем, что оно готово response.message ="Device is ready"# И сообщаем об этом жеelse: # Если не включено response.ready =False# Отвечаем, что "устройство" не готово response.message ="Device is unplugged"# И сообщаем, что оно не подключено к узлуreturn response # Эта строка "возвращает" данные из функции# Имитация работы климатической камеры. Дверь открыта -температура повышается, дверь закрыта - температура понижается.# Если Температура упала ниже +26, дверь откроется, если выросла выше 36.6, дверь закроется.def imitation ():ifnothasattr(imitation, "door_locked"): imitation.door_locked =Trueifnothasattr(imitation, "Temperature"): imitation.Temperature =36.6if imitation.door_locked ==True: imitation.Temperature -=0.1else: imitation.Temperature +=0.1if imitation.Temperature <26: imitation.door_locked =Falseif imitation.Temperature >36.6: imitation.door_locked =Truereturn imitation.door_locked, imitation.Temperature# ФОВ на событие таймераdef pub_callback(event):global trial_name # В функции используем глобальную переменную trial_nameglobal pub # В функции используем глобальную переменную для паблишера tmprt = temperature() # Инициируем переменную с данными о работе нашего устройства# Здесь наш замечательный костыль сломался. Оказалось, что нельзя передать пустую строку как параметр клиенту. # Он её не примет, а возьмёт из сервера параметров предыдущее значение.# Изобретём велосипед -- будем останавливать эксперимент знаком "/s".# if trial_name != "": # Чтобы не передавать данные, если эксперимента нет, передаём только тогда, когда имя не пустоеif (trial_name !="") and (trial_name !="/s"): # Чтобы не передавать данные, если эксперимента нет или он остановлен. # Передаём только тогда, когда имя не пустое и не равно "/s"# Заполним атрибуты переменной для публикации tmprt.name = trial_name # Имя tmprt.currtime = rospy.Time.now() # Время tmprt.locked, tmprt.temperature = imitation() # Данные от устройстваifnot rospy.is_shutdown(): # Если узел не остановлен, pub.publish(tmprt) # публикуемdef main():global in_actionglobal trial_nameglobal pub trial_name =""# Имя эксперимента инициируем здесь, чтобы иметь к нему доступ по всему коду rospy.init_node('server') # Инициируем узел# Именно после init_node, не раньше, получим параметр для узла, так как раньше узла нет in_action = rospy.get_param("~in_action", True) # Переменная, в которой хранится состояние "устройства", по умолчанию установим True# Внимание, здесь интересный момент. True по умолчанию, только если прраметра ещё не было. # Если мы хоть один раз запустили узел -- параметр существует. Сервер параметров его запомнил rospy.Service('/temp_service', command, callback) # Инициируем сервис print ("Our server is waiting of a call now")# Есть смысл отправлять данные, только если устройство работает if in_action ==True: pub = rospy.Publisher('/temperature', temperature, queue_size =1) rospy.Timer(rospy.Duration(0.1), pub_callback) # Вызывать pub_callback через 0.1 сек#Если не работает, сообщим об этомelse:print ("Device is unplugged, test it and rerun server") rospy.spin() # Включаем спиннер# Если это скрипт, а не модуль, то вызвать главную функцию if__name__=="__main__": main()
Теперь понятно, что и клиент тоже должен измениться под наши
костыльно-велосипедные упражнения.
#! /usr/bin/env python3# Импортируем rospy и инструменты для SetBool из набора стандартных сервисовimport rospyfrom communicato.srv import command, commandRequest # Из модуля communicato.srv импортируем инструменты для работы с сервисом и ответом сервисаfrom communicato.msg import temperature # Как и для сервера, импортируем инструмент сообщенияdef callback(msg): # Создадим ФОВ для подписчикаprint('Current temperature is:', msg.temperature, end=" ") # Напечатаем температуруif msg.locked ==True: # и состояние замка двериprint ("and the door is locked")else:print ("and the door is unlocked")rospy.init_node('client') # Инициируем узел# Инициализируем подписчик с использованием ФОВrospy.Subscriber('/temperature', temperature, callback) # Инициализируем запросчик для сервиса temp_service типа commandclient = rospy.ServiceProxy ('/temp_service', command)# Ждать сервис и ничего не делатьclient.wait_for_service()# Инициализировать объект запроса requestrequest = commandRequest()# Задать имя эксперимента# request.name = "Trial number one"request.name = rospy.get_param('~trial_name')print (request.name)# Послать запрос и получить ответresponse = client(request)# Ответ напечататьprint(response)rospy.spin()
На этом с сервисами и сообщениями закончим, и перейдём ещё к одной
архитектуре взаимодействий – действиям.
Сейчас у нас есть код проекта, в котором по запросу сервер начинает
посылать данные эксперимента клиенту до тех пор, пока клиент не подаст
запрос на прекращение эксперимента. Мы использовали два типа
взаимодействий, создавали для описания их структур данных разные файлы,
прописывали логику совместной работы взаимодействий посредством
сообщений и сервиса. Неужели мы выбрали настолько сложный пример для
реализации, что в ROS не предусмотрено взаимодействие для такого случая?
Это сомнительно, так как, например, вполне реальная задача – это подать
захватному устройству манипулятора команду сжимать губки до
определённого расстояния между ними, при этом контролировать ход
выполнения и подать команду об окончании при превышении силы сжатия или
подать другое целевое положение губок, не дожидаясь окончания
перемещения.
Теперь давайте сформулируем, что мы хотим от взаимодействия:
* Клиент-серверное взаимодействие, * Возможность отменить выполнение
запроса со стороны клиента, * Получение потоковой информации о состоянии
обработки запроса.
Всем этим условиям удовлетворяет ещё один тип взаимодействия – через протокол действий ROS, за
реализацию которого отвечает библиотека actionlib. Называется такое
взаимодействие Действия или Action.
5.5 Разработка пакета
ROS на основе action
Давайте повторим наш предыдущий пакет, но перепишем его под action
(мне не очень нравится назвать это взаимодействие действиями, так как
такое название может нас запутать). Учтем предыдущие ошибки и будем
запускать испытание на определённое время, а имя испытания передавать не
будем вовсе. Если будет необходимость вести некий протокол, данные всё
равно целесообразно сохранять на стороне клиента. В процессе испытания
клиент должен получать данные с частотой 10 Гц о текущей температуре и
состоянии замка двери климатической камеры, а по окончании получить
среднюю температуру в камере за время эксперимента. Как и ранее,
оборудование подключено к серверу. Оборудование мы будем
имитировать.
Создадим пакет с названием actio:
Разберём, что мы здесь делаем. В каталоге src нашего
рабочего пространства ROS мы создаём пакет с именем actio и,
что очень удобно, но чего мы ещё не делали, необходимыми нам
зависимостями. Побочным эффектом создания пакета с зависимостями будет
появление Далее нам надо переименовать подкаталог src в
scripts, создать каталог action и в нём файл с
описанием структуры данных Trial.action. В подкаталоге
scripts создадим файлы скриптов для сервера server.py
и клиента client.py, которым присвоим право на
выполнение.
Теперь перейдём к тексту файла package.xml в который добавим
строку
<exec_depend>message_generation</exec_depend> к
другим строкам зависимостей выполнения exec_depend.
Теперь откроем для редактирования файл CMakeLists.txt и просто
удалим из него весь предыдущий текст и вставим вот этот:
# Версия CMake cmake_minimum_required(VERSION 3.0.2)# Имя нашего проекта, оно же -- имя пакетаproject(actio)# Указать, какие пакеты необходимо найти CMake для сборки нашего проектаfind_package(catkin REQUIRED COMPONENTS actionlib actionlib_msgs message_generation rospy std_msgs)# Из подкаталога проекта action для описания структуры взаимодействия используем файл с описанием Trial.actionadd_action_files( DIRECTORY action FILES Trial.action)# Генерировать сообщения и служебные файлы (например, хидеры) в каталоге devel рабочего пространстваgenerate_messages( DEPENDENCIES actionlib_msgs std_msgs )# Настройка для сборки пакета catkincatkin_package(# INCLUDE_DIRS include# LIBRARIES actio CATKIN_DEPENDS actionlib actionlib_msgs message_generation rospy std_msgs # Установить, от каких проектов catkin зависит наш проект# DEPENDS system_lib)# Путь к хидерам заголовкам библиотек, кооторые нужны для постройки проектаinclude_directories(# include ${catkin_INCLUDE_DIRS})
В комментариях я постарался описать, какие функции выполняют
различные команды и макросы этого файла. Подробно файлы проектов
catkin для сборки CMake описаны в соответствующем
разделе ROS Wiki. Как можно заметить, я выкинул из файла все
закомментированные строки, которые были созданы автоматически. Так можно
делать. Я бы даже сказал, так делать нужно, так как читаемость файла
проекта очевидно повышается.
5.5.1 Создание файла описания
action
Давайте опишем структуру данных с помощью специализированного
action-файла описания структуры взаимодействия. Как и файл с описанием
сервиса, этот файл фактически состоит из нескольких секций, разделённых
кодом ---. Секции также, описываются как msg-файлы. В
отличие от файлов с описанием сервисов, здесь не две секции, а три: *
Цель, * Результат, * Обратная связь.
Теперь давайте откроем наш файл Trial.action и вставим туда:
# Цель
uint32 num_seconds # Время эксперимента в секундах
---
# Финальный результат
float32 a_mean # Среднее значение
---
# Промежуточные результаты -- обратная связь
bool locked # Состояние двери
float32 temperature # Текущая температура
То есть, клиент будет подавать запрос серверу на выполнение
эксперимента продолжительностью N секунд, в результате эксперимента
клиент получит от сервера среднюю температуру во время испытания, от
начала отработки цели до её достижения сервер может отдавать клиенту
результаты о некотором текущем состоянии выполняемого action –
температуре и состоянии замка двери.
Для наглядности можно набросать такую структуру взаимодействия в нашем
пакете: mermaid sequenceDiagram participant Клиент participant Сервер note left of Клиент: Запрос Клиент->>+Сервер: Выполни цель "работать N секунд" Сервер-->>Клиент: Первый пакет обратной связи (Температура, Замок) loop Повторять с частотой 10 Гц Сервер-->>Клиент: Пакеты обратной связи (Температура, Замок) end Сервер-->>Клиент: Последний пакет обратной связи (Температура, Замок) note right of Сервер: Ответ Сервер->>-Клиент: Результат -- средняя температура испытания
Предлагаю теперь перейти в рабочее пространство
cd ~/catkin_ws и построить его catkin build. В
результате построения для организации взаимодействия в каталоге
~/catkin_ws/devel/share/actio/msg автоматически были созданы
несколько файлов сообщений: * nameAction.msg * nameActionGoal.msg *
nameActionResult.msg * nameActionFeedback.msg * nameGoal.msg *
nameResult.msg * nameFeadback.msg Где name – это имя action-файла без
расширения. В нашем случае это такие файлы:
Также автоматически для этих файлов сообщений в каталоге
~/catkin_ws/devel/include/actio были созданы заголовочные
файлы:
Эти файлы не стоит изменять, даже если вы уверены, что внесёте
корректные правки, так как все изменения пропадут после перестройки
рабочего пространства.
Теперь мы можем импортировать инструменты для работы с action, а значит
– писать код узлов.
5.5.2 Сервер пакета с
action-взаимодействием
Необходимо сказать, что библиотека actionlib обширна и
предоставляет широкие возможности использования action-взаимодействия.
Библиотека поддерживает работу как с одиночными целями так и с
множеством целей. В этом уроке мы не будем использовать взаимодействие с
несколькими целями, только с одиночной, поэтому для программирования нам
будет достаточно классов сервера SimpleActionServer и клиента
SimpleActionClient. Напомню, что наша цель — понимание основных
возможностей ROS, а не детальное изучение всех
функций.
Теперь давайте откроем для редактирования файл с текстом скрипта узла
сервера gedit ~/catkin_ws/src/actio/scripts/server.py и
вставим туда следующий текст:
#! /usr/bin/env python3# Импортируем rospy и actionlib import rospyimport actionlib# При постройке проекта catkin автоматически создаст для нашего файла действия Trial.Action в каталоге ~/catkin_ws/devel/share/actio/msg# файлы сообщений: # TrialActionFeedback.msg # TrialActionResult.msg # TrialResult.msg # TrialActionGoal.msg # TrialFeedback.msg # TrialAction.msg # TrialGoal.msg# и также создаст для них соотвтествующие хидеры, которые мы можем использовать как инструменты# Здесь воспользуемся интрументами для самого действия, обратной связи и результатаfrom actio.msg import TrialAction, TrialFeedback, TrialResult# Разработаем класс для сервера. Это не обязательноclass ServerActionClass(object):# Ниже все переменные, идущие после self. -- это переменные экземпляра класса (объекта) мы их можем менять в процессе выполнения скрипта# Для функций аналогично -- это функции экземпляра класса, хоть их код не поменяется, но они будут вызваны только для объекта, а не для класса# С помощью self. мы работаекм именно с объектомdef__init__(self): # Т.н. конструктор -- функция, которая будет автоматически вызвана при создании объекта этого класса. В ней стоит задать начальные значенияself.door_locked =True# Переменная состояния замкаself.Temperature =36.6# Переменная температурыself.mean =0# Эта переменная будет использована для подсчёта среднегоself.steps =0# Количество измерений, которые проведены в эксперименте# Инициализируем сервер _as с именем trial_act (это "пространство имён", оно должно совпадать для клиента и сервера) # для нашего типа взаимодейтсвия TrialAction, описанного в файле Trial.action# с ФОВ execute_cb для отработки цели и без автоматического запускаself._as = actionlib.SimpleActionServer("trial_act", TrialAction, execute_cb=self.execute_cb, auto_start =False)# Теперь запустим серверself._as.start()# Логируем запуск сервера. Здесь мы запишем в журнал, которым сейчас явлется терминал, сообщение о запуске сервера# Параметром этой функции выступает форматированная строка старого формата python. # На место %s здесь будет подставлено имя узла, который можно получить функцией rospy.get_name() rospy.loginfo("Action server %s started."% rospy.get_name())def init_imitation (self):# Каждый новый эксперимент начинается с нулевого среднего и нулевого количества измеренийself.mean =0self.steps =0def imitation (self):# Имитация работы климатической камеры. Дверь открыта -температура повышается, дверь закрыта - температура понижается.# Если Температура упала ниже +26, дверь откроется, если выросла выше 36.6, дверь закроется.# Функцию упростили, так как пользуемся переменными объектаifself.door_locked ==True: self.Temperature -=0.1else: self.Temperature +=0.1ifself.Temperature <26: self.door_locked =Falseifself.Temperature >36.6: self.door_locked =Trueself.mean +=self.Temperatureself.steps +=1returnself.door_locked, self.Temperaturedef execute_cb(self, goal):# ФОВ отработки цели. Переменная с настройками цели передаётся в функцию в качестве аргумента freq =10# Будем передавать обратную связь с частотой 10 Гц. self. нет, так как это локаольная переменная rospy.loginfo('%s trial is running for %i seconds'% (rospy.get_name(), goal.num_seconds)) success =True# По умолчанию работа функции закончится успешно rate = rospy.Rate(freq) # Способ приостановки потока с установленной частотойself.init_imitation () # Сбросить стартовые данные для испытанияfor i inrange(0, goal.num_seconds * freq):# Выполним цикл с количеством шагов, равным количество секунд * на частоту работыifself._as.is_preempt_requested():# Если сервер получит вытесняющий отрабатываемую цель запрос rospy.loginfo('%s: Preempted'% rospy.get_name()) # Сообщить self._as.set_preempted() # Установить для текущей сессии отработки цели статус "вытеснено" success =False# Отработка цели завершилась безрезультативноbreak# Завершить цикл feedback = TrialFeedback() # Инициализировать переменную для обратной связи door_state, temperature =self.imitation() # Получить данные о температуре и состоянии замка# И присвоить эти данные соответсвующим полям обратной переменной связи feedback.locked = door_state feedback.temperature = temperatureself._as.publish_feedback(feedback) # Опубиковать обратную связь через механизм actionlib rate.sleep()if success: # Если цель достигнута успешноself.mean /=self.steps # Посчитать среднее арифметическое температур rospy.loginfo('%s: Succeeded'% rospy.get_name())self._as.set_succeeded(TrialResult(self.mean)) # Установить для текущей сессии отработки цели статус "успешно" и передать результат отработкиelse: rospy.loginfo('%s: Unsucceeded'% rospy.get_name())def main (): rospy.init_node('trial_as') # Инициализировать узел server = ServerActionClass() # Инициализировать объект нашего класса для сервера rospy.spin() # Включиьь спиннер# Если это скрипт, а не модуль, вызываем главную функцию if__name__=='__main__': main()
Я постарался прокомментировать текст подробно, поэтому есть смысл
пройти по его комментариям и попытаться разобраться. Здесь есть пожалуй
только одно, что вы ещё не делали, не программировали собственный класс.
Класс – это довольно удобная модель, которая объединяет в себе данные и
функции для работы с этими данными. От класса можно наследовать другой
класс – это удобно, когда для нас уже кто-то написал библиотеку классов
и нам требуется приспособить класс для своих нужд. Например, кто-то
разработал класс фрукт, который имеет поля данных о весе и
объеме и функцию, определяющую по переданному ей региону, растёт ли там
этот фрукт. Мы хотим посадить яблоневый сад и на базе этого класса
создаём класс яблоко, у которого есть функция определения,
червивое оно или нет. От нашего класса яблоко мы можем породить объекты
НашеЯблоко1, НашеЯблоко2 и так далее. Кстати, от
класса фрукт вы, скорее всего, объект породить не сможете,
потому что абстрактный фрукт, например, неизвестно где растёт и функция,
которая даёт нам информацию о том, растёт ли фрукт в регионе, будет
пустой. Даже не просто пустой, она будет требовать реализации в дочернем
классе.
Добавлю, что класс — это не просто “шаблон”, а способ мыслить реальными
объектами, которые могут изменяться, взаимодействовать и сохранять своё
состояние. Именно так работает наш сервер действий: хранит текущие
данные и управляет ими через методы. Такой подход, называемый
инкапсуляцией, делает код понятнее, организованнее и проще в расширении.
Особенно полезен в ROS, где часто нужно управлять состоянием,
обрабатывать события и взаимодействовать между узлами. В случае
написанного нами скрипта внутри класса определены
атрибуты – переменные, хранящие состояние (например,
self.door_locked, self.Temperature). Также
есть методы – функции, которые этим состоянием
управляют: __init__ (начальные настройки),
imitation (логика изменения температуры),
execute_cb (обработка цели от клиента). Создавая объект
server = ServerActionClass(), мы получаем экземпляр класса
с собственными атрибутами и доступом ко всем методам.
Это очень краткое введение в ООП (Объектно-ориентированное
программирование, а не Организация Освобождения Палестины).
Если есть желание почитать серьёзную литературу по ООП, можно найти
какую-нибудь книгу Гради Буча по ООП. Они отлично помогали мне от
бессонницы во время учёбы. А если серьёзно — она научит видеть за кодом
архитектуру.
Вернёмся к скрипту сервера, сохраним файл и перейдём к клиенту.
5.5.3 Клиент пакета с
action-взаимодействием
Теперь давайте напишем код скрипта клиента. Давайте откроем для
редактирования файл с текстом клиента
gedit ~/catkin_ws/src/actio/scripts/client.py и вставим
туда следующий текст:
#! /usr/bin/env python3# Импортируем rospy и actionlib import rospyimport actionlib# При постройке проекта catkin автоматически создаст для нашего файла действия Trial.Action в каталоге ~/catkin_ws/devel/share/actio/msg# файлы сообщений: # TrialActionFeedback.msg # TrialActionResult.msg # TrialResult.msg # TrialActionGoal.msg # TrialFeedback.msg # TrialAction.msg # TrialGoal.msg# и также создаст для них соотвтествующие хидеры, которые мы можем использовать как инструменты# Здесь воспользуемся интрументами для самого действия, цели, обратной связи и результатаfrom actio.msg import TrialAction, TrialGoal, TrialFeedback, TrialResult# Создадим словарь для состояний сервера. Слева ключ (числовой код состояния), справа после двоеточия краткая текстовая расшифровка goal_status_text = {0: "PENDING", # The goal has yet to be processed by the action server1: "ACTIVE", # The goal is currently being processed by the action server2: "PREEMPTED", # The goal received a cancel request after it started executing and has since completed its execution (Terminal State)3: "SUCCEEDED", # The goal was achieved successfully by the action server (Terminal State)4: "ABORTED", # The goal was aborted during execution by the action server due to some failure (Terminal State)5: "REJECTED", # The goal was rejected by the action server without being processed, because the goal was unattainable or invalid (Terminal State)6: "PREEMPTING", # The goal received a cancel request after it started executing and has not yet completed execution7: "RECALLING", # The goal received a cancel request before it started executing, but the action server has not yet confirmed that the goal is canceled8: "RECALLED", # The goal received a cancel request before it started executing and was successfully cancelled (Terminal State)9: "LOST", # An action client can determine that a goal is LOST. This should not be sent over the wire by an action server}# Словарь для состояний двериdoor_lock_state = {False: "unlocked",True: "locked",}# ФОВ вызывается при переходе в активный режим. Сервер пошёл к целиdef callback_active():print ("Action server is processing the goal")# ФОВ вызывается при переходе в режим "выполнено". Сервер пришёл к целиdef callback_done(state, result):# Печатаем состояние goal_status_text.get(state) по ключу ключу и среднее значение result.a_mean, которое мы объявили в action-файлеprint ("The trial is done with state ", goal_status_text.get(state), " and ariphmetic mean = ", result.a_mean)print ("Press Ctrl+С to exit")# ФОВ вызывается при получении обратной связи от сервераdef callback_feedback(feedback):# Печатаем округлённое до двух знаков после запятой значение текущей температуры и состояние замка двериprint ("Trial feedback -- Current temperature is ", "{:.2f}".format(round(feedback.temperature, 2)), " and the door lock state is ", door_lock_state.get (feedback.locked)) # Функкция инициализации клиента и подачи цели серверуdef trial_client():# Инциализируем клиент с именем trial_act (это "пространство имён", оно должно совпадать для клиента и сервера) # для нашего типа взаимодейтсвия TrialAction, описанного в файле Trial.action client = actionlib.SimpleActionClient('trial_act', TrialAction)# Ожидаем появления сервераprint ("Waiting for action server...") client.wait_for_server()# Если сервер есть, посылаем целевое значение и определяем ФОВы, которые будут работать с клиентом# Здесь я задаю время эксперимента в непосредственно в скрипте, как го получить в виде параметра, можно посмотреть в предыдущей части занятия client.send_goal(TrialGoal(10), active_cb=callback_active, feedback_cb=callback_feedback, done_cb=callback_done)print("Target time has been sent to the action server")# Основная функция def main():try: # Знакомьтесь, это исключение. Если в строках от try до except случиться ошибка ROS, то выполнится код после except <E>: rospy.init_node('trial_ac') # Инициализировали узел trial_client() # Запустили клиент rospy.spin() # Повисли на спиннереexcept rospy.ROSInterruptException:print("program interrupted before completion") # Сообщить об исключении# Если это скрипт, а не модуль, вызываем главную функцию if__name__=='__main__': main()
Здесь также остановлюсь на новом для нас – это словарь Python.
Словарь – это список неупорядоченных пар “ключ”:“значение” очень удобный
инструмент, когда по некоторому ключу мы можем найти значение. Например,
телефонный справочник является словарём, где по имени мы можем найти
номер телефона. Причём такой телефонный справочник нам даже не надо
упорядочивать – запросили номер по имени, получили номер. В тексте я
пользуюсь конструкцией вида phone_no = phonebook.get(Name),
можно воспользоваться конструкцией
phone_no = phonebook[Name], но тогда, если мы зададим
несуществующий ключ, программа остановится с сообщением об ошибке.
5.5.4 Запуск пакета
Понимаю, что учить вас запускать узлы пакета уже немножко
бессмысленно, так как вы это умеете, но всё же напомню:
# Открыть терминалroscore# Открыть новый терминал rosrun actio server.py# Открыть новый терминал rosrun actio client.py
После запуска сервера мы получим следующую картинку:
Мы видим сообщение о том, что запущен сервер trial_as.
На самом деле, если вспомнить код, здесь показано не имя action-сервера,
имя которого надо бы получить из переменной trial_act, а
имя узла сервера. Моя оплошность, списал у кого-то заготовку для кода и
не переделал, хоть и тщательно прокомментировал. Подглядывать нехорошо!
Давайте найдём нужное место в коде и внесём правку.
Я не уверен, что мы можем получить от action-сервера его имя, да и
разбираться не хочу. Давайте внесём небольшую правку, которая сделает
наше логирование корректным.
Давайте вот в эту часть функции def __init__(self)::
# Инициализируем сервер _as с именем trial_act (это "пространство имён", оно должно совпадать для клиента и сервера) # для нашего типа взаимодейтсвия TrialAction, описанного в файле Trial.action# с ФОВ execute_cb для отработки цели и без автоматического запускаself._as = actionlib.SimpleActionServer("trial_act", TrialAction, execute_cb=self.execute_cb, auto_start =False)# Теперь запустим серверself._as.start()# Логируем запуск сервера. Здесь мы запишем в журнал, которым сейчас явлется терминал, сообщение о запуске сервера# Параметром этой функции выступает форматированная строка саарого формата python. # На место %s здесь будет подставлено имя узла, который можно получить функцией rospy.get_name() rospy.loginfo("Action server %s started."% rospy.get_name())
внесём следующие изменения, которые я отметил комментариями:
self.server_name ="trial_act"# Явно задаём имя сервера действияself._as = actionlib.SimpleActionServer(self.server_name, # Используем заданное имя TrialAction, execute_cb=self.execute_cb, auto_start=False ) rospy.loginfo("Action server '%s' started."%self.server_name) # Выведем имя сервера из переменной
Мы явно задаём имя сервера действия в переменной
self.server_name. Это позволяет нам контролировать, как
будет называться сервер, и делает вывод в журнал более осмысленным.
Такой подход особенно полезен, если вы хотите запустить несколько
серверов действия в одном узле.
Кстати, мы видим сообщения в некотором явно не случайном формате. Это
логирование с помощью функции rospy.loginfo. rospy.loginfo — это функция в ROS для вывода информационных
сообщений. Она помогает отслеживать работу узлов, записывает данные в
логи и терминал. В отличие от print, интегрируется в
систему ROS, поддерживает уровни логирования и фильтрацию. Сообщения
помечаются как информационные и могут быть сохранены для последующего
анализа.
В уроке мы использовали её так:
rospy.loginfo("Action server '%s' started."%self.server_name)
Это позволило видеть, какой сервер запущен, особенно при работе с
несколькими узлами. Функция поддерживает форматирование строк, что
делает вывод гибким и информативным. Использование
rospy.loginfo вместо print — хорошая практика
в ROS: сообщения корректно интегрируются в систему, их можно
анализировать позже, а также фильтровать по уровням важности. Это
особенно полезно при отладке и тестировании роботов.
После запуска клиента сюжет будет развиваться куда стремительнее,
сервер получил целевое время 10 секунд и запустил отработку, о чём нам и
сообщил:
Клиент в это время сообщил о том, что ждёт сервер, потом о том, что
отправил целевые данные серверу и уверенно доложил, что сервер начал
выполнять отработку цели. Кстати, если мы вернёмся к коду, увидим, что
это не самоуверенное заявление – клиент получил информацию об этом от
actionlib через функцию обратного вызова. Далее клиент
через другую ФОВ начал получать обратную связь, отдаваемую
сервером:
И через десять секунд сервер завершил свой цикл сообщением об успехе
и перешёл в к ожиданию новой цели:
Клиент же закончил приём обратной связи, принял конечный результат о
средней температуре и сообщил о необходимости его, клиент,
остановить:
5.5.5 Прерывание отработки цели
Теперь давайте вмешаемся в беспроблемную отработку цели и для начала
попробуем запустить ещё один клиент. Наш пакет устроен так, что если мы
пошлём серверу другую цель в процессе отработки, запустив второй
экземпляр клиента, сервер бросит нашу цель, о чём
actionlib сообщит клиенту, и сервер начнёт отработку
новой цели.
Сервер сообщит, что стартовал, затем, что отрабатывает цель в 10 секунд
– это запущен клиент 1. Затем мы запустили клиент 2 и сервер сообщает,
что цель вытеснена и не завершилась удачно, затем сообщает, что
отрабатывает цель в 10 секунд и в конце сообщает об успехе отработки
цели:
Клиент 1 во время запуска клиента 2 сообщает “ну да, ну да, пошёл
я” о запуске узла с тем же именем (для клиента 1 проблема именно в
том, что появился двойник, actionlib здесь ни при чем )
и завершает работу:
Ну а клиент 2 в этом случае отрабатывает нормально, так как ничего
про драму между сервером, сердце которого склонно к измене, и клиентом 1
не знает.
Теперь давайте остановим отработку цели корректно. Это можно сделать
с помощью функции клиента cancel_goal(). Для начала нам
надо сделать имя переменой клиента глобальным, перед инициализацией
клиента в функции trial_client нашего скрипта вставим
строку global client. После этого мы добавим три строки в
ФОВ callback_feedback, которые отменят цель, если
температура упала ниже 30 градусов:
global clientif (feedback.temperature <30): client.cancel_goal ()
Сохраним текст и перезапустим сервер и модифицированный клиент.
Сервер сообщит:
Клиент:
На этом закончим наше знакомство с взаимодействиями ROS и подведём
итоги.
5.6 Подведём итоги
В этом занятии мы узнали про архитектуру взаимодействия в ROS,
которая называется “клиент-сервер” и работает через ещё одно средство
взаимодействия узлов ROS - службу или service. Также
мы создали клиент-серверный ROS-пакет и опробовали его. При этом
результаты его работы совпали с тем, что мы ожидали, значит, концепцию
мы поняли правильно. Мы создали пакет с комплексным взаимодействием, где
узлы общались как через сервис, так и через топик. И в завершение мы
узнали про actionlib и на основе этого типа
взаимодействия переписали свой пакет с комплексным взаимодействием.
Давайте в заключение подумаем, в каком случае какую архитектуру стоит
применять в ROS-проектах. Во-первых, и это очень просто, мы можем
зависеть от уже существующего узла, а значит права выбора у нас не
будет. А вот “во-вторых” сложнее и интереснее и без однозначного ответа.
Это уже из области проектирования программных продуктов – мы должны
понять, какая архитектура будет нам выгоднее. Здесь нужно понять, есть
ли необходимость в постоянном потоке данных – например, мониторинг
движения робота в ограниченном пространстве лучше осуществлять
непрерывно, то есть, использовать паблишер, чтобы успеть остановить его
при приближении к препятствию. А вот команду остановки лучше давать
через сервис, чтобы не обрабатывать постоянно одно и то же сообщение
“иди”. Или другой случай. Вы хотите управлять скоростью колёс робота,
например. Можно делать это при помощи сервиса, коллируя его с частотой
1000 Гц, а можно настроить пару издатель-подписчик, где вы будете
потоком с заранее заданными параметрами слать скорости, а робот будет их
обрабатывать.
В общем, сервисы стоит использовать там, где нужно подать команду и
получить ответ и при этом делать это нужно не очень часто, а в ответ мы
получаем сообщение только о том, фактически, что команда получена.
В свою очередь, action стоит использовать там, где мы должны подать
команду, и получить ответ, но при этом иметь возможность контролировать
процесс выполнения этой команды, но только во время выполнения команды,
а значит не нуждаемся в постоянном потоке данных. На этом расплывчатом
объяснении это занятие закончим.
5.6 Заключение (новое)
На этом уроке вы познакомились с двумя ключевыми архитектурами
взаимодействия в ROS — сервисами и действиями
(actions). Эти механизмы позволяют организовать чёткое,
целенаправленное взаимодействие между узлами, в отличие от топиков,
которые подходят для потоковой передачи данных.
Вы научились создавать собственные .srv-файлы,
описывающие структуру запросов и ответов. Это дало вам возможность
определять собственные типы сообщений и использовать их в реальной
логике работы системы. Вы также реализовали клиент и сервер сервиса, где
сервер обрабатывает входящие запросы, а клиент отправляет данные и
получает результат.
Важным этапом стало освоение библиотеки actionlib. Вы
узнали, что действия — это способ организации долгих задач с
возможностью получения обратной связи и отмены цели. Вы разработали
сервер действия на основе SimpleActionServer, реализовали
callback-функцию execute_cb, которая управляет выполнением
цели, и добавили механизм обратной связи (feedback) для
отслеживания прогресса.
С помощью класса TrialAction вы организовали хранение
состояния и поведения в одном месте, применив принципы ООП. Это
позволило структурировать код, сделать его понятнее и проще для
дальнейшего расширения.
Кроме того, вы поработали с параметрами сервера ROS через
rosparam, научились использовать launch-файлы для запуска
нескольких узлов и передачи параметров.
Функция rospy.loginfo стала вашим инструментом
диагностики: теперь вы можете не только видеть, что происходит в
системе, но и сохранять информацию о работе узла для последующего
анализа.
Вы также познакомились с базовыми шаблонами работы с действиями: как
создаются .action-файлы, из которых автоматически
генерируются все необходимые .msg-типы (Goal,
Feedback, Result). Это даёт вам гибкость при
проектировании сложных взаимодействий.
Теперь вы умеете: - Создавать собственные сервисы и действия, -
Писать серверы и клиенты на Python, - Управлять состоянием узла и
передавать промежуточную информацию через feedback, -
Использовать параметры сервера и launch-файлы для настройки поведения
без изменения кода.
Этот урок дал вам не просто новые технические навыки, но и понимание,
когда какой механизм взаимодействия применить. Такой
подход — основа построения масштабируемых и надёжных робототехнических
систем.
Поздравляю — вы стали не просто программистом, а полноценным
участником создания ROS-приложений, способным моделировать и управлять
взаимодействием компонентов.