3.1
Краткое изложение предыдущего урока и план на этот урок
Мы узнали, что такое рабочее пространство catkin, создали в рабочем
пространстве первый пакет ROS и получили первое представление о
структуре пакета, на языке Python создали в парадигме ROS две программы
– издателя и подписчика, научились создавать файлы запуска для сложных
проектов, научились искать и удалять пакеты ROS, узнали про фреймворк
rqt. Попутно мы узнали про права файлов, настройку
версии Python по умолчанию, ну и потихоньку осваивались в командной
строке. В этом уроке мы запустим нашу первую симуляцию мобильного
робота, коротко поговорим о кинематике и поиграемся с черепашкой –
конечно же, не с живой черепахой, а моделью робота, похожего на
черепаху, находящегося в некотором окружении.
3.2 Симулятор черепашки
Сегодня мы познакомимся с очень простой компьютерной моделью робота,
работающего в двумерном пространстве – turtlesim. В ROS wiki этому
“роботу” посвящена своя страница, и можно сказать, что
он очень популярен в учебных курсах на раннем этапе обучения, так как с
обной стороны это почти полноценный мобильный робот, а с другой стороны
мы ничего не сможем сломать, когда будем пробовать его в работе. Так как
turtlesim практически официальное учебное пособие, мы скорее, всего
найдём ROS-пакет для него уже установленным на нашем компьютере. Ну если
мы ставили версию full desktop. Давайте вспомним, как с помощью команды
rospack и поиска grep искать пакеты и проверим:
rospack list |grep turtlesim
Если пакет найден, замечательно, если не найден, ничего страшного,
поставим его на компьютер с помощью команды, которую можно найти на
страничке в ROS-вики:
Сразу вопрос, что я забыл сделать? Всё верно, я не сделал apt
update, а это очень, очень некрасиво. Вы можете не покормить
своего кота, но не сделать апдейт перед установкой вы не можете. Ну
хорош, согласен, с котом перебор, но в принципе это кощунство одного
порядка. Вернёмся от котиков к черепахе. Давайте попробуем её запустить.
Для этого надо запустить ROS-мастер, а потом взять команду запуска
черепахи со странички turtlesim. Ну или попробовать запустить её
самостоятельно с помощью rosrun, имени пакета и волшебства
TAB. Для запуска ноды (узла ROS, так я иногда буду называть
его далее) turtlesim из списка
я бы выбрал ноду с именем turtlesim_node, вроде бы
это логично. Давайте попробуем. Ну и если вы ещё не запустили терминал,
самое время это сделать. Итак:
roscore# Открыть новый терминалrosrun turtlesim turtlesim_node
Я вижу вот такой рисунок.
“Значит, сработала эта хреновина!”
Всё хорошо, мы, в отличие от героев Кин-дза-дзы, этого и добивались.
Если вы видите у себя на экране другую черепашку, а вы видите другую
черепашку, не переживайте, это нормально, она для каждого запуска
разная. Отлично, мы запустили процесс симуляцию некоего мобильного
робота похожего на черепашку и теперь можем посмотреть, что же она
рассказывает о себе ROS. Давайте откроем новый терминал и посмотрим,
какие темы сейчас есть в нашем ROS. Как посмотрим? Конечно, с помощью
команды rostopic list:
Мы снова видим надоевшие rosout и
rosout_agg, но пора к ним привыкнуть, мы их видим
всегда, если запущен ROS-мастер. А вот следующие три темы – это явно то,
что нас интересует. Можно предположить, что тема
cmd_vel – управление скоростью,
pose – информация о положении, а
color_sensor – некий датчик цвета. Давайте
применим в команде rostopic новый ключ
info и изучим эти топики (я уже говорил, что буду так
иногда называть темы?):
rostopic info /turtle1/cmd_vel
Посмотрим на результат:
Мы видим здесь, что формат этого сообщения
geometry_msgs/Twist. “Геометрические сообщения”
или geometry_msgs – это ещё один набор типов
сообщений, типа того, что мы уже использовали –
std_msgs. Давайте посмотрим, что входит в этот
набор, перейдём в общую папку ROS, папку, которая содержит всё пакеты,
что были установлены в процессе установки ROS. Перейдём в папку
geometry_msgs общей папки. Так это тоже ROS-пакет, за файлами
сообщений нам надо будет нырнуть в папку msg внутри папки этого
пакета:
ls /opt/ros/noetic/share/geometry_msgs/msg/
Посмотрим на результат:
Давайте посмотрим содержимое файла с определением сообщения
Twist с помощью ещё одной полезной команды –
cat.
Команда эта имеет мощный функционал и предназначена в первую очередь
для соединения файлов и отправки результата в стандартный вывод stdout
(для нас, работающих в терминале, сейчас это вывод текста в терминал),
но сейчас нам будет полезен побочный эффект, заключающийся в том, что
единственный файл, заданный в качестве аргумента команды
cat, также попадёт в стандартный вывод. Итак,
результат:
Здесь мы видим, что сообщение составлено из “линейной” и “угловой”
компоненты, каждая из которых также является неким вектором-три. Что
“три”, будем выяснять. Если мы с помощью команды cat
посмотрим на файл Vector3.msg, то мы поймём, что это за
вектор-три. Кстати, мы видим, что сообщение может состоять из других
сообщений. Надо отметить, что для сообщений ROS это распространённая
практика, когда сообщения объединяются в другие сообщения. Теперь
посмотрите сами, пожалуйста, файл
“/opt/ros/noetic/share/geometry_msgs/msg/Vector3.msg”.
Посмотрели? Очевидно, что это тип данных, который определяет некий набор
из трёх величин x, y, z формата 64-разрядное число с плавающей
точкой. Очевидно, что рассматриваемое нами сообщение
Twist состоит из шести 64-битных чисел с плавающей
точкой, объединённых в группы по три, называемые “линейное” и “угловое”.
Вероятнее всего, мы видим некоторый способ задать координаты в
декартовом пространстве. Чтобы не вводить в заблуждение, скажу, что мы
видим способ представления скоростей в трёхмерном пространстве с
использованием линейных и угловых компонент. Twist — это
сообщение для описания скоростей и, на мой взгляд, имена внутри
сообщения могут сбить с толку. Ни кто нам немешает, однако, обратиться к
ROS-сообществу и уточнить, для чего предназначен тот или иной тип
данных.
Возвращаясь именно к структуре сообщения могу сказать, что,
необязательно ходить по папкам, и смотреть файлы, чтобы разобраться со
структурой. Для того чтобы предъявить нам структуру сообщения, есть
команда rosmsg с ключом info или
show. Посмотрим структуру сообщения Twist.msg:
rosmsg info geometry_msgs/Twist
Посмотрим на результат:
Мы видим шесть чисел, объединённых в тройки с именами “линейное” и
“угловое”, а имена переменных в тройках – привычное обозначение осей в
трёх измерениях. Ознакомиться заранее с топиками пакета, с которым вы
начинаете работу – полезно. Если вернёмся немного назад, как выводу
данных по команде rostopic info /turtle1/cmd_vel, мы увидим
ещё и “направление” дуги графа ROS или говоря другими словами, кто
подписан на топик и кто его публикует. В качестве паблишера там мы видим
Никого, а вот сабскрайбером указана нода
/turtlesim. Теперь мы можем вполне обоснованно
утверждать, что turtlesim ждёт данных через топик
cmd_vel, то есть, мы можем управлять движением
черепашки, подавая данные через эту тему. Сейчас черепашка подписана на
тему, но никто не публикует данные, по которым она могла бы осуществлять
какое-то движение. Поэтому черепашка стоит на месте. Что нужно сделать,
чтобы заставить черепаху двигаться?
3.3 Нода для
управления симулятром черепашки
3.3.1 Создание ноды
Надо написать ноду паблишера, которая будет рассчитывать данные о
скорости черепахи и передавать их в топик управления скоростью. Это и
будет нашим сегодняшним маленьким проектом. Мы должны создать новый
пакет, написать ноду на Питоне, в которой организовать паблишер, который
будет задавать скорости движения черепашки. Оставим включенными
ROS-мастер и узел симуляции черепахи и перейдем исходники нашего
рабочего пространства. Если перешли, то давайте создадим пакет, как
делали это на прошлом уроке. Назовём мы такой пакет
turtle_play. Хорошо, надеюсь у всех получилось.
Что нужно сделать теперь? Верный ответ – это построить рабочее
пространство. Уверен, каждый из вас подал следующие команды:
cd ~/catkin_ws/src/catkin_create_pkg turtle_playcd ..catkin build
Надеюсь, перед тем, как построить рабочее пространство, вы не забыли
перейти из каталога с исходниками на уровень выше, так как я часто
забываю это делать. Хорошо перейдём в каталог с нашим новым пакетом и
создадим там папку для скриптов, которую назовём также, как и на прошлом
уроке – scripts. После этого папке со скриптами создадим файл
управления движением control.py и откроем его для редактирования.
Надеюсь, у всех была такая последовательность команд:
cd ~/catkin_ws/src/turtle_play/mkdir scriptscd scriptstouch control.pygedit control.py
Как мы выяснили раньше, черепашка подписана на топик
cmd_vel, а вот издателя у темы нет, значит,
напишем узел издателя. Начнём, как всегда, с шебанга, который скажет,
какой интерпретатор использовать для скрипта. Далее импортируем rospy и
тип данных Twist из geometry_msgs, инициализируем
узел с именем control_node. Теперь создаём
паблишер с именем темы /turtle1/cmd_vel, типом
данных Twist и размером очереди = 1. В этом месте появляется то, что вы
ещё не делали. В прошлый раз мы передавали целое число, а сейчас надо
передать составное сообщение. Также, как и для паблишера
pub, породим объект сообщения с именем msg от
класса Twist:
msg = Twist()
Теперь давайте зададим скорость по одной из координат. У класса
Twist, который представляет сообщение типа Twist и
от которого мы породили наш объект сообщения msg,
есть атрибуты linear и
angle, у которых в свою очередь есть атрибуты
x y z. Ими мы можем пользоваться также, как
обычными переменными, то есть, мы можем присвоить им какое-либо
значение, соответствующее их типу float64. Присвоим координате
x значение, равное единице. Записывать атрибуты
следует от объекта через точку:
msg.linear.x =1
Также мы можем присвоить значение и другим атрибутам, но сейчас
остановимся на linear.x. Дальше вам следует написать цикл
публикации сообщений с ожиданием окончания работы программы и
приостановкой программы на 1 секунду на каждом шаге – это мы уже делали
в предыдущем уроке. Отлично, всё сделано, сохраним и закроем текстовый
редактор. Нам осталось только дать скрипту право на исполнение и
запустить его с помощью rosrun. Сделайте это,
пожалуйста.
3.3.2 Разбор результатов
Теперь давайте разбираться, что у нас получилось. Ну или не
получилось, что интереснее. Для начала напомню, как дать право на
исполнение файла владельцем:
chmod u+x control.py
Теперь напишу команду, которой следует запускать нашу ноду:
rosrun turtle_play control.py
Получилось или появилась ошибка, как на картинке ниже?
Если получилось, значит вы выполнили эту команду в терминале, который
открыли после того, как перестроили рабочее пространство. Если не
получилось – откройте новый терминал и запустите ноду там, а после этого
ответьте, почему не получилось запустить в старом терминале. Ответ
простой, когда при запуске терминала подгрузился скрипт с настройками
рабочего пространства, в рабочем пространстве ещё не было нашего пакета,
поэтому терминал и надо перезагрузить, ну или дать одну команду, а вот
какую, скажите сами. Отлично, идём дальше. В этом месте вы можете
получить от своей ноды сообщения об ошибке в процессе выполнения. Если
это так, то для начала нужно прочитать, что вам пишет интерпретатор
Питона, понять, в чём может быть проблема и запустить ещё раз. В общем,
вы сейчас начали заниматься отладкой своего приложения, а это одна из
наиболее важных частей программирования. Но делать каждый следующий шаг
отладки стоит только после прочтения и осознания сообщения об ошибке,
правке кода и запуске. Не стоит без внесения правок и раз за разом
запускать не изменённый код. Ведь … > “Я уже говорил тебе… что
такое… безумие, а? Безумие… это… точное повторение… одного и того же
действия. Раз за разом… в надежде… на изменение. Это… есть…
безумие.”
Если в результате нескольких последовательных запусков и правок
ничего не получилось, то стоит сличить ваш код с тем кодом скрипта,
который написал я, и понять, в чём отличия. Понять, почему мой работает,
а ваш – нет. Итак, мой код:
#!/usr/bin/env python3import rospyfrom geometry_msgs.msg import Twist# Инициализация узла и публикатораrospy.init_node('control_node')pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size =1)# Инициализация сообщения и задание значения атрибуту linear.xmsg = Twist()msg.linear.x =1# Циклическая публикация сообщенияwhilenot rospy.is_shutdown(): pub.publish(msg) rospy.sleep(1)
3.3.3 После успешного запуска
Итак, у нас всё получилось, паблишер запустился, черепашка
подвинулась и … врезалась в стену
Мы видим это в симуляторе, мы можем прочитать предупреждение об этом
в терминале, где запущен симулятор, мы понимаем, что черепашка страдает,
так как эти сообщения идут непрекращающимся потоком. Остановим наш
паблишер и увидим, что черепашка перестала слать нам предупреждения.
Отлично, давайте сдвинем её назад, ведь стоять, уткнувшись лицом в стену
совсем не интересно. Что надо сделать? Во-первых, это изменить
направление подаваемой скорости, заменим его значение на обратное, на
минус единицу:
# msg.linear.x = 1msg.linear.x =-1
Во-вторых, если мы запустим ноду сейчас, то паблишер будет подавать
команду бесконечно и загонит черепашку в другую стену. Давайте применим
другой цикл, цикл for. Цикл со счётчиком – цикл, в
котором некоторая переменная изменяет своё значение от заданного
начального значения до конечного значения с некоторым шагом, и для
каждого значения этой переменной тело цикла выполняется один раз.
Используем цикл со счётчиком для диапазона длиной 5:
# while not rospy.is_shutdown():for i inrange(5):
Перед каждым изменением нашего кода вы можете видеть прежнюю версию,
но помеченную символом решётки. Как я говорил в прошлый раз, это
комментарий. Я оставил прежний вариант кода, просто закомментировал его
часть. Многие программисты так делают, а потом получают кучу мусора в
своём коде и не понимают зачем его там оставили. То есть, пользоваться
таким способом отката изменений можно, но злоупотреблять им не стоит –
лучше воспользоваться системами контроля версий. Хорошо, сохранили,
запустили. Что видим?
Давайте посмотрим теперь на темы, которые публикует симулятор
черепашки. Первая тема /turtle1/color_sensor. Над
разгадкой того, что это такое, бьются лучшие умы робототехники. Бьются и
не знают, что это цвет следа, который оставляет черепашка, когда ползает
по экрану. Цвет задаётся в формате rgb. Почему про это
не написано на страничке turtlesim в ROS wiki - не знаю, но это факт.
Если не верите, давайте сходим и посмотрим. Задаётся цвет следа с
помощью ещё одного средства связи ROS, так называемого сервиса
ROS, но о них мы будем говорить позже. Нам сейчас этот топик ни
к чему, забудем про него. Вторая публикуемая turtlesim тема –
/turtle1/pose. Этот топик намного интереснее и
полезнее. Если мы выведем содержимое этого топика, например, с помощью
команды rostopic echo /turtle1/pose, мы увидим следующие
данные: :
Разберём, что каждое поле означает.
3.3.4 Кинематика черепашки
Очевидно, мы имеем две координаты на плоскости, некий угол поворота
черепашки и угловую и линейную скорости. Давайте попробуем разобраться,
что это за угол. Нарисуем двумерную систему координат и некий объект в
ней.
У объекта есть координаты по осям X и Y и угол
направления на черепашку θ,
правильно? Наверное, нет. Если у нас есть координаты на плоскости, этот
угол для задания положения нам не нужен. Да и вообще, зачем нам нужен
угол для плоского случая, если у нас есть две координаты? Надо
вспомнить, что черепашка наша не материальная точка и не круг на
плоскости, у неё, как у всякой уважающей себя черепахи, есть голова, и
голова эта куда-то направлена.
И что это за направление, как нам теперь определить это угол? Это
угол между осью X системы координат и направлением головы черепахи. Для
программиста такое описание возможно и понятно, а вот компьютеру, даже
если он оснащён каким-нибудь генеративным предобученным трансформером,
об этом пока говорить рано. Для ROS точно такое описание понятно не
будет. Давайте дорисуем зелёную стрелку из левого бока черепахи и
допишем у красной стрелки x, а у зелёной y.
Мы видим две системы координат, одна из которых, обозначенная
строчными буквами, смещена и повёрнута относительно другой, обозначенной
прописными буквами, что вполне похоже на то, что даёт нам топик.
А в каком направлении у нас повернута “маленькая” система координат?
Давайте думать из трёхмерного пространства, мне так привычнее. Да и если
подумать, мы работаем сейчас с мобильным роботом, типа робота-пылесоса,
который перемещается по плоскости, не может перекатиться через бок или
кувыркнуться (повернуться вокруг осей x и y), не может
подпрыгнуть или провалиться сквозь землю, но всё равно существует в
трехмерном пространстве. Хорошо, предположим, что у нашей системы
координат есть ещё и ось Z и она на рисунке направлена на нас,
то есть мы поселим нашу черепашку в стандартную или правую
систему координат. Если так, то всё становится вполне понятно, хватаемся
правой рукой за ось Z, большой палец направляем по направлению
оси, а по направлению остальных пальцев смотрим, куда надо повернуть
систему координат. Следующая картинка не должна вводить в заблуждение
тем, что рука держится не за ту ось, картинка дана для примера.
Давайте теперь назовём наши системы координат. Систему координат,
которая никак не зависит ни от смещения, ни от поворота черепашки, будем
называть внешней. Внутренняя система координат – это система координат,
которую черепашка таскает с собой, та, которая «привязана» к самому
роботу. Для черепашки — это её собственный взгляд на мир: где бы она ни
находилась, её начало координат всегда в центре тела, ось X —
по направлению движения, Y — вбок, Z — вверх.
Теперь ещё один момент. Скорости, на которые подписан симулятор. Мы
помним этот самый cmd_vel, который состоит из двух
трёхкомпонентных векторов — linear и angular.
То есть теоретически мы могли задать скорость по всем шести координатам
нашего многострадального трёхмерного пространства: три линейные (x, y,
z) и три угловые (x, y, z). Так вот, нам давали возможность задать
скорости шестью координатами, а возвращает нам симулятор только две. Что
произошло? Куда пропали ещё четыре?
На самом деле, всё на месте — они просто не отображаются. И не могли бы
мы заставить черепашку двигаться по всем шести координатам — два.
Давайте разберём эти “раз” и “два”, и заодно поймём, зачем нам нужна
внутренняя СК.
“Логично” начать с “два”. Оказывается, через
cmd_vel мы можем перемещать черепаху только по
трём координатам: по x черепашка поедет в направлении головы,
если зададим скорости по y, черепашка двинется боком. Движение
по z не приведёт к видимому результату — из двумерного
пространства не вырваться. А вот если задать скорость по z для
вектора angular, то получим вращение черепашки вокруг своей
вертикальной оси.
Кстати, вы заметили, как незатейливо мы воспользовались внутренней СК
при задании скоростей черепашки? Именно так и работает внутренняя
система координат: ось X направлена вперёд (по направлению головы), Y —
вбок, а Z — вверх. Поэтому задавать скорости относительно неё очень
удобно, особенно когда ты сам — эта самая черепашка.
Теперь посмотрим на “раз” — данные темы pose симулятор нам
отдаёт во внешней системе координат, так мы всегда знаем, где черепашка
расположена на поле. Да и в принципе, я пока плохо представляю, как
можно было бы нам получать понятные человеку данные о положении
черепашки во внутренней системе себя — там она всегда находится в начале
СК. Вот если бы у нас было две черепахи, тогда можно было бы задать
координаты одной черепашки во внутренней СК другой.
> Но это уже совсем другая история.
3.3.5 Обратная связь от робота
Для чего нам может быть полезна информация о положении черепашки?
Информация о текущем положении черепашки, доступная через топик
/turtle1/pose или pose какого-то другого
робота, позволяет замкнуть контур управления. Например, если мы хотим
переместить робота в определённую точку или пройти по заданной
траектории, знание текущего положения критично для корректной работы
алгоритма. Я перестал говорить “черепашка” не просто так, публикация
информации о положении робота – это общая практика для узла ROS,
отвечающего за управление роботом на некотором, скажем так, уровне
оборудования. Фактически, мы уже попробовали переместить нашу черепашку
в нужную нам область, когда отодвигали её от стены, и даже добились
результата. Однако, дело в том, что мы использовали управление с
открытым контуром, то есть без обратной связи, которое нас не спасло бы
от нештатной ситуации. Если бы мы находились не у правой стенки, а
начали бы движение близко к левой стенке, наша черепашка снова ударилась
бы об стену. Управление с открытым контуром применяется редко – только
для очень предсказуемых систем. В реальности может случится, например,
проскальзывание колёс робота и робот по окончании подачи скорости
окажется совсем не там, где мы ожидали. Давайте построим систему
управления с замкнутым контуром. Для начала подпишемся на тему
/turtle1/pose в нашей программе control.py. Из
набора сообщений turtlesim.msg импортируйте
Pose и подпишитесь на тему с форматом
Pose и функцией обратного вызова с именем
callback, а затем напишите саму функцию, которая просто
печатает сообщение. Программу закончите спиннером, который нам нужен,
чтобы программа не закончилась сразу после цикла публикации скоростей.
После того, как мы дополнили код, сохраните его и попробуйте запустить с
помощью rosrun. Это уже должно войти в привычку, что вы
должны попробовать исправить программу самостоятельно, если в ней что-то
не так, но если что-то не получается, можно почитать код:
Отлично, давайте обсудим, что мы увидели после запуска. Черепашка
переместилась в сторону своего хвоста, в процессе движения мы видели,
что изменяются координаты и линейная скорость равнялась единице. После
окончания движения нода не прекратила работу, так как терминал не вышел
в приглашение, а печать данных будто бы остановилась. На самом деле,
данные продолжают выводиться на экран с частотой, с которой их публикует
симулятор, убедиться в этом можно, если прокрутить вывод терминала вверх
– мы не увидим линейную скорость, отличную от нуля. Давайте убедимся в
том, что наша нода принимает данные ещё одним способом – закомментируем
спиннер в тексте, сохраним и запустим. Только давайте перед сохранением
ещё и поменяем знак у заданной скорости, а то черепашка ударится
хвостом, а это не только больно, но и неожиданно. Запускаем и видим, что
черепашка переместилась, нода завершила работу и последнее сообщение,
которое она прочитала, имело ненулевую линейную скорость. Значит можно
сделать вывод, что на предыдущем этапе нода получала данные и после
окончания движения. Давайте ещё проверим, как симулятор отреагирует на
установку угловой скорости. Как я уже говорил, ни кувыркаться ни
перекатываться наша черепашка не умеет, а значит единственная
координата, вокруг которой её стоит вращать – z и угловую
скорость будем задавать, изменяя координату
msg.angular.z. Скорость задаём в радианах. Давайте
зададим, запустим и посмотрим, что получилось. Получился неожиданный
результат:
Крутили в положительном направлении, а угол получили отрицательный.
Скажите, пожалуйста, почему? Ответ – угол theta у нас получился больше,
чем Пи радиан, и число, которое мы видим на экране и 4 радиана, на
которые повернулась черепашка – это фактически одно и то же. Мы бы
достигли того же угла, в котором находимся, если бы двигались против
часовой стрелки, но угол этот был бы записан отрицательно. А получить
при компьютерных вычислениях число больше Пи нам не дадут обратные
тригонометрические функции. Значит, мы всегда будем получать результат в
диапазоне от -π до +π.
Почему 4 рад, а не 5 мы разбираться не будем, скажу только, что на
первое отправленное сообщение симулятор не реагирует движением, а так
как наш цикл был от 0 до 4, мы и повернулись на 4 рад. Если бы поставили
цикл с range(9), повернулись бы на 8 рад. Если хотите,
можете разобраться в этом самостоятельно. И ещё одно задание на
самостоятельную проработку – разобраться, что делает такой код нашего
узла:
Сейчас мы познакомимся с одним очень полезным инструментом ROS – tf (The Transform Library). Библиотека
tf была разработана для стандартизации работы с системами координат (СК)
и их преобразовавниями в ROS и сейчас является одной из основ работы
ROS. Большинство проектов роботов в ROS просто не запустятся, если в нём
не будет установлен пакет tf. Библиотека tf ROS самостоятельно выполняет
все преобразования СК и делает доступными эти СК всем пакетам,
работающим под управлением одного ROS-мастера.
Всё это хорошо, но какая нам от этого польза?
Представьте, что у нас есть мобильный робот. Он ездит по комнате, видит
объекты камерой, измеряет расстояния лазерным сканером и при этом
оснащён манипулятором с захватным устройством. Все эти компоненты —
камера, лазерный сканер, колёса, манипулятор — находятся в разных местах
относительно друг друга. И чтобы робот мог работать корректно, ему нужно
понимать, где что находится. Если мы не хотим считать все эти
преобразования самостоятельно, а мы не хотим, здесь-то и появляется
спасительный инструмент tf, который разберётся с большинством СК
самостоятельно.
А для чего вообще нужны системы координат?
Об этом мы говорили чуть раньше и на очень простом примере, где у нас
был единственный объект на плоскости – наша черепашка. А теперь
представьте, тот робот с камерой, колёсами, манипулятором и прочим, и
что этот робот обнаружил объект камерой в некой точке с координатами,
допустим, (x=2, y=3, z=1). Что это за координаты? Вероятнее всего, это
координаты относительно самой камеры. А если нам нужно, чтобы
манипулятор взял этот объект, то манипулятору нужны координаты не
относительно камеры, а относительно какого-то своего элемента, например,
основания. Вот тут и возникает задача перевода координат из одной
системы в другую, которую tf делает автоматически.
Как же tf работает?
TF строит дерево систем координат. Например для нашего робота с камерой
и манипулятором есть:
Мировая система координат — некоторая “основа”, относительно которой
всё происходит, базовая СК, относительно которой определяются все
остальные СК.
Система координат робота — это его “корпус”.
Система координат камеры, которая может крепиться к корпусу под
определённым углом и на определённом расстоянии.
Система координат манипулятора, который тоже крепится к корпусу в
каком-то месте. Так вот библиотека tf по данным о роботе, которые она
получает от разработчика в специальном формате, знает, как все эти узлы
робота связаны между собой: где камера относительно корпуса, где
манипулятор относительно корпуса, и так далее. То есть, tf построит
дерево с корнем в мировой системе координат, содержащее все СК робота.
Когда вы попросите бибилиотеку tf перевести координаты объекта из
системы камеры в систему манипулятора, бибилиотека tf просто пройдёт по
этому дереву и выполнит необходимые математические преобразования.
У сообщений tf есть временная составляющая, зачем она нужна?
Представьте, что данные от камеры приходят с задержкой. Например, камера
увидела объект полсекунды назад, а за это время робот уже немного
сдвинулся. Если мы просто возьмём старые координаты и попробуем их
использовать, они будут некорректными. TF решает эту проблему: он умеет
хранить историю преобразований и может выполнить перевод координат для
любого момента времени. То есть вы можете сказать: “Мне нужны координаты
объекта в тот момент, когда камера его увидела”, и бибилиотека tf всё
правильно посчитает.
Визуализация.
Ещё одна отличная вещь в tf — это возможность визуализировать системы
координат. Вы запускаете RViz, добавляете TF-плагин, и видите, как
робот, камера, манипулятор и другие компоненты связаны между собой. Это
очень помогает при отладке: если что-то работает неправильно, вы сразу
видите, где проблема.
В общем, понятно, что бибилиотека tf — это мощный инструмент, который
позволяет роботу ориентироваться в пространстве. Она решает задачи
преобразования координат, синхронизации данных и работы с временными
метками. Без неё робот не понимает, где что находится. Более того, он не
понимает, где находятся его руки, ноги и голова.
Кстати, для понимания того, сколько систем координат нам было бы
нужно корректно запрограммировать и обрабатывать в процессе
моделирования, можно взглянуть на tf робота PR2:
Вот это всё многообразие красно-зелёно-синих рогаток за нас считает
tf.
Наша черепашка это тоже робот и как мы выяснили чуть ранее, СК, которые
я теперь иногда буду называть tf, у неё тоже присутствуют. Давайте
попробуем понять tf на примере нашей черепашки. Перед запуском примера я
должен сказать, что tf в настоящий момент является устаревшей
библиотекой, на смену которой пришла доработанная версия tf –
tf2. Никаких возможностей первой версии версия вторая
не потеряла. В ROS для совместимости присутствует возможность работы с
tf, но реализована она теперь через tf2. Давайте пока поработаем с
первым поколением, так как понимание концепции оно нам даст, а большего
пока и не надо.
3.4.1
Демонстрационная программа для обучения tf
Для того, чтобы разобраться, как работают tf, запустим демонстрацию
из ROS-пакета turtle_tf, в котором мы увидим уже
знакомый нам turtlesim, но уже с двумя черепашками. Для запуска будем
использовать команду roslaunch, с которой мы уже знакомились в разделе
2.7 урока 2. Откроем новый терминал
и напишем в нём команду:
roslaunch turtle_tf turtle_tf_demo.launch
После запуска мы увидим экран с двумя черепашками, одна из которых
пока стоит на месте, а вторая стремиться к ней:
Теперь давайте подвигаем стоявшую черепашку, для чего в терминале,
откуда запустили демонстрацию командой roslaunch, будем клавишами со
стрелками подавать команды движения:
Что мы теперь видим? Одна черепашка, которой мы управляем, перемещается
по экрану, а следом за ней непрерывно бежит вторая.
3.4.2 Программирование
обмена tf между нодами
Что же у нас происходит “под капотом”, так сказать? Давайте для
начала посмотрим граф узлов ROS, мы это уже делали в
rqt из меню Plugins->Introspection->Node
Graph. Также это можно сделать командой rqt_graph из
терминала. Команда rqt_graph запустит плагин Node
Graph в качестве отдельного приложения, и покажет нам
следующее:
Здесь мы можем видеть ноду /teleop, которая обрабатывает
нажатие клавиш и публикует управляющие скорости для преследуемой
черепашки (turtle1) в топик /turtle1/cmd_vel, на который
подписана основная нода симулятора /sim. Также
/sim подписан на топик с управляющими скоростями для
преследующей черепашки (turtle2), который публикует нода
/turtle_pointer. В свою очередь,
/turtle_pointer получает некие /tf от узлов
(как мы помним, нода у нас – это жаргонное название узла ROS)
/turtle1_tf_broadcaster и
/turtle2_tf_broadcaster, которые подписаны на паблишеры
/turtle1/pose и /turtle2/pose. Всё, наконец
круг замкнулся, можно начинать разбираться, как это работает. ROS-пакет
с помощью библиотеки tf создал две системы координат: СК черепашки 1 и
СК черепашки 2, данные о которых транслируются через так называемый
вещатель tf. Эти данные обрабатываются
слушателем tf, который вычисляет разность систем
координат и осуществляет их широковещательную трансляцию. Давайте
посмотрим на код, запрятанный в недрах ros-пакетов, который реализует
работу вещателя tf – turtle_tf_broadcaster.py, от которого и
порождены узлы /turtle1_tf_broadcaster и
/turtle2_tf_broadcaster, которые запускаются launch-файлом
ROS-пакета с переменными /turtle1 и /turtle2,
чтобы их можно было идентифицировать при работе. Для лучшего понимания,
что происходит при выполнении этого скрипта, я этот код
прокомментировал:
#!/usr/bin/env python# Импорт необходимых модулей:# ROS для Питона, модуль библиотеки tf и модуль, описывающий сообщения из пакета симулятора черепашкиimport rospyimport tfimport turtlesim.msgdef handle_turtle_pose(msg, turtlename):# Тело функции обратного вызова для сабскрайбера,# который подпишется на топик с данными о положении, публикуемый симулятором черепашки# Создадим объект br на базе класса TransformBroadcaster модуля tf, который и будет осуществлять вещание br = tf.TransformBroadcaster()# Транслируем информацию о преобразовании (точно, ведь tf это transform) одной системы координат в другую br.sendTransform((msg.x, msg.y, 0), # Смещение tf tf.transformations.quaternion_from_euler(0, 0, msg.theta), # Поворот tf rospy.Time.now(), # Время преобразования turtlename, # СК преследуемой черпашки"world") # Мировая СК# Проверка на то, используется ли код в качестве скрипта или подключен как модульif__name__=='__main__':# Инициализация узла turtle_tf_broadcaster rospy.init_node('turtle_tf_broadcaster')# Получение имени черепашки turtlename = rospy.get_param('~turtle')# Уже знакомая нам функция подписки на топик rospy.Subscriber('/%s/pose'% turtlename, # Имя топика turtlesim.msg.Pose, # Тип данных сообщения handle_turtle_pose, # Функция обратного вызова turtlename) # Дополнительный параметр# Зациклим ROS-приложение с помощью спиннера rospy.spin()
На некоторых моментах текста скрипта я хочу остановиться подробнее.
Начнём с не относящейся к ROS, но достаточно стандартной для Питона
вещи, которую мы ещё не использовали – обработку переменной
__name__. Когда скрипт запускается на выполнение, Питон
присвоит переменной __name__ значение
__main__. В противном случае интерпретатор предположит, что
мы подключаем код как модуль и присвоит переменной __name__
имя модуля (имя файла с кодом, но без расширения .py). В строке
if __name__ == '__main__': производится проверка на то,
используется ли код в качестве скрипта или подключен как модуль. Если
это скрипт, о чём и говорит значение '__main__' переменной
__name__, исполняется основная часть. Фактически, это
аналог функции main() в языках C и C++. Чтобы понять, что
это хороший тон программирования на Питоне, давайте вспомним код нашего
скрипта, реализующего работу подписчика в предыдущем занятии. Могу
сказать, что нам повезло с длиной кода. Даже среди небольшого количества
строк место, в котором начинается основная часть скрипта, сразу увидеть
не просто. То есть, эта конструкция не только позволяет нам задать
поведение программы в ситуации “скрипт или подключаемый модуль”, но и
улучшает читаемость программы, что очень важно для последующего
сопровождения программного продукта. Давайте разберём работу этой
“функции main()”. Инициализацию ноды вы, несомненно, узнали. В следующей
строке получаем имя черепашки, которое было задано как параметр запуска
узла в launch-файле. Значение этого параметра через сервер
параметров ROS, о котором мы поговорим в следующем занятии,
становится доступной для скрипта через функцию
rospy.get_param('~turtle'). Так как программа эта не
сложная, имя черепашки здесь является и именем паблишера и именем
системы координат черепашки, однако, для более сложных программ стоит
давать имена, учитывая контекст.
Теперь разберём подробнее параметры, используемые при инициализации
подписчика:
rospy.Subscriber('/%s/pose'% turtlename, # Имя топика turtlesim.msg.Pose, # Тип данных сообщения handle_turtle_pose, # Функция обратного вызова turtlename) # Дополнительный параметр
Здесь мы уже знакомым нам способом подписываемся на топик
pose паблишера с именем черепашки, которое хранится в нашем
случае в переменной turtlename. Собранная из этих двух
частей строка будет первым параметром. Сообщения этого топика имеют тип
turtlesim.msg.Pos, что мы и задаём вторым параметром.
Реакцией на появление данных в топике будет вызов callback-функции или
функции обратного вызова (ФОВ), имя которой handle_turtle_pose задаётся
третьим параметром. Четвёртым параметром функции инициализации
подписчика, который мы не использовали в нашем первом опыте работы с
подписчиком, будет дополнительный аргумент, передаваемый ФОВ. Как можно
заметить, аргументов в данном случае у ФОВ два: msg с
данными сообщения и некий turtlename, который в теле
функции мы использовали как имя СК черепашки при создании сообщения
вещателя:
Давайте разберём параметры функции вещателя
sendTransform. Смещение tf – здесь смещение преследуемой
черепашки по осям x и y в мировой СК. Так как прыгать наша черепашка не
умеет, координате z присвоено значение 0. Поворот tf здесь задаётся
кватернионом, полученным из углов Эйлера позы черепашки с помощью
функции quaternion_from_euler. Так как черепашка не
научилась перекатываться и кувыркаться, повороты вокруг осей x и y
обнулены. Время преобразования – текущее время ROS. Последними двумя
параметрами функции трансляции tf задаются имя дочерней и родительской
СК. Для нашего случая это СК преследуемой черепашки и мировая СК. Общий
алгоритм работы ноды достаточно прост. Подписчик ожидает сообщение
сообщение с позицией преследуемой черепашки, вызывает ФОВ, транслирующую
данные о вычисленной СК и переходит в ожидание следующего сообщения о
положении черепашки. А где же код, который породит
черепашку-преследователя и будет ей управлять? Ответ на этот вопрос
придётся поискать в другом файле – файле, от которого порождён узел
turtle_pointer со слушателем tf
turtle_tf_listener.py:
#!/usr/bin/env python# Импорт необходимых модулей:# rospy - ROS# math - популярные математические функции и константы# tf - преобразоания СК# geometry_msgs.msg - геометрические сообщения# turtlesim.srv - доступ к сервисам симулятора черепашкimport rospyimport mathimport tfimport geometry_msgs.msgimport turtlesim.srv# Главная "функция"if__name__=='__main__': rospy.init_node('tf_turtle')# Создадим объект listener класса TransformListener модуля tf# Это слушатель нашего вещания listener = tf.TransformListener()# Три следующие строчки - порождение в симуляторе turtlesim черепашки-преследователя с именем turtle2 rospy.wait_for_service('spawn') spawner = rospy.ServiceProxy('spawn', turtlesim.srv.Spawn) spawner(4, 2, 0, 'turtle2')# Иницализация паблишера для управления скоростью черепашки-преследователя turtle_vel = rospy.Publisher('turtle2/cmd_vel', geometry_msgs.msg.Twist, queue_size=1)# Задание частоты работы основного цикла программы 10 Гц rate = rospy.Rate(10.0)# Пока работу ноды не прервалиwhilenot rospy.is_shutdown():# Получить преобразование из СК turtle1 в СК turtle2 в настоящий момент времени# В результате будет получена пара троек значений - смещение и поворотtry: (trans, rot) = listener.lookupTransform('/turtle2', '/turtle1', rospy.Time())except (tf.LookupException, tf.ConnectivityException, tf.ExtrapolationException):continue# Рассчитать угловую angular и линейную linear скорости для преследователя angular =4* math.atan2(trans[1], trans[0]) linear =0.5* math.sqrt(trans[0] **2+ trans[1] **2)# Подача скоростей черепашке-преследователю msg = geometry_msgs.msg.Twist() msg.linear.x = linear msg.angular.z = angular turtle_vel.publish(msg)# Приостановить выполнение цикла rate.sleep()
Как мы теперь понимаем, эта нода фактически состоит только из
основной функции. Давайте, подглядывая в комментарии в коде, разберём,
что в ней написано. С подключаемыми модулями всё должно уже быть понятно
– мы подключаем наборы функций, которые понадобятся в процессе работы
скрипта. Далее мы видим исполняемую часть скрипта, отвечающую за то,
чтобы породить черепашку-преследователя, получить tf черепашки, которую
надо догонять и рассчитать скорость преследователя в зависимости от
положения черепашек. Для начала здесь происходит инициализция ноды и
слушателя tf, который и будет получать данные от вещателя. Следом за
этим идёт код, порождающий черепашку-преследователя. Порождается она с
помощью ещё одного средства взаимодействия узлов ROS, об одном из
которых “издатель-подписчик” мы уже узнали и смогли его применить на
практике. Так вот, ещё один способ взаимодействия – это сервисы
ROS. Способ этот широко используется в ROS, так даже в этом
уроке мы о нём вспоминаем во второй раз. Его мы позже также разберём
теоретически и используем практически в процессе наших занятий. Сейчас
же достаточно понимать, что мы отправили некий запрос симулятору
черепашки, который заставит его породить ещё одну черепашку с заданными
координатами x=4, y=2, theta=0 и именем turtle2. Паблишер в следующей
строке нам хорошо знаком, мы использовали его совсем недавно. С помощью
этого паблишера мы подаём скорости движения черепашке с именем turtle2 –
это черпашка-преследователь. Теперь нам надо установить частоту работы
основного цикла программыю. Если раньше мы явно задавали тайм-аут в
работе цикла на 100 миллисекунд вызовом функции
rospy.sleep(0.1) в конце цикла, то здесь мы используем
связку из rate = rospy.Rate(10.0) перед циклом и
rate.sleep() в конце цикла. Функция rospy.Rate
задаст частоту выполнения цикла, а rate.sleep()
приостановит выполнение на соответствующий промежуток времени. Здесь
возникает закономерный вопрос, зачем нам использовать две команды там,
где прекрасно обходились одной? Ответ на него лежит практически на
поверхности, Rate задаёт частоту работы цикла, то есть
стремится сделать так, чтобы каждая итерация занимала в нашем случае 0,1
секунды. Если всё ещё не понятно, давайте предположим, что все операторы
внутри цикла, кроме sleep(0.1), выполняются за 50
миллисекунд, а потом мы ещё притормаживаем выполнение на 100
миллисекунд. Как результат, одна итерация будет выполняться у нас 0,15
секунды, а не 0,1. А вот команда Rate(10) заставляет ROS
стараться выполнять каждую итерацию за время максимально близкое к 0,1
секунды, то есть, подгонять время тайм-аута под заданную нами частоту.
Дальше наконец идёт знакомый нам цикл паблишера, выполняющийся до тех
пор, пока не будет подана команда останова. Тело цикла начинается с
преобразования одной tf в другую командой
listener.lookupTransform, которая заботливо окружена так
называемым блоком контроля исключений:
Если в блоке между try и exception
произойдёт ошибка времени выполнения программы (не синтаксическая
ашипка, за проверку синтаксиса отвечает транслятор языка, который просто
не даст выполнить команду, написанную не по правилом Python), так вот,
если произойдёт ошибка времени выполнения, то её тип будет передан
обработчику исключений. Если тип соответствует тому, который мы задали
после команды except, программа выполнит составной оператор
под except, в нашем случае это оператор перехода к
следующей итерации цикла continue. Если же обработчику
попадётся незнакомый тип ошибки, увы, программа остановится с сообщением
об ошибке времени выполнения. Функция
listener.lookupTransform принимает на вход в качестве
параметров:
в какую tf преобразуем (назовём её TF2),
из какой tf преобразуем ( назовём её TF1),
какой момент времени берём для преобразования. С первыми двумя
параметрами всё понятно, а вот третий может вызвать удивление, ну или
восторг. Мы что, на самом деле можем получить tf из прошлого? Да, из
прошлого можем, ну а будущее нам всё ещё недоступно из-за принципа
причинности. Более того, мы можем брать tf из разного момента времени
для каждой tf из пары, для этого существует функция
listener.lookupTransform с четырьмя параметрами на входе. В
качестве результата функция listener.lookupTransform отдаёт
нам пару наборов чисел: trans – три числа (x, y, z) для смещения из TF1
в TF2, rot – кватернион (x, y, z), необходимый для поворота из TF1 в
TF2. Дальше в цикле по рассчитываются угловая и линейная скорость по
линейному рассогласованию, угол theta нам здесь не помощник, ведь мы
хотим, чтобы преследователь не двигался параллельно с преследуемым, а
бежал в его сторону, ну или хотя бы лежал в его направлении, если устал.
Следующим блоком команд, который нам предельно понятен, так как мы его
только что делали, будет подготовка и публикация скоростей через топик
'turtle2/cmd_vel'. Завершается всё это
rate.sleep(), который, как мы говорили, за нас посчитает,
не сколько приостановить выполнение цикла паблишера. На этом же
завершается и наш разбор того, как устроены простейшие вещатель и
слушатель tf в ROS. Забегая наперёд скажу, что очень часто вы будете
использовать tf даже не подозревая об этом. Например, когда мы будем
создавать файл со структурой робота с помощью специального формата
описания роботов, ROS без нашей помощи породит tf для каждого
сочленения.
3.4.3 Утилиты для работы с tf
Давайте вспомним граф узлов демонстрационной программы tf из
предыдущего подраздела урока. Среди множества рёбер мы можем видеть два
ребра графа, обозначенные /tf, которые соединяют вершины
графа /turtle1_tf_broadcaster и
/turtle2_tf_broadcaster с вершиной
/turtle_pointer. Рёбрами графа называются связи между
вершинами графа. Вершинами графа в ROS являются узлы, рёбрами –
коммуникационные связи между ними. Итак, у нас сейчас в ROS присутствуют
tf, давайте посмотрим, как мы их можем обследовать.
Утилита TF Tree
Для начала давайте запустим визуализацию дерева tf. Сделать мы это
можем как с помощью вызова плагина TF tree программы
rqt через пункт меню
Plugins->Visualization->TF Tree. Также мы можем запустить
только этот плагин непосредственно:
rosrun rqt_tf_tree rqt_tf_tree. Как бы мы его не запускали,
в результате мы получим подобное дерево tf:
На этой картинке мы видим, что у нас есть две tf – turtle1 и turtle2,
которые “живут” в общей “мировой” системе координат. Также мы видим, кто
является вещателем этих tf и с какой частотой идёт вещание. К сожалению
“больше не знаем о них ничего”.
Утилита TF Echo
Ну так давайте узнаем. Давайте посмотрим на tf turtle1 с помощью
консольной утилиты tf_echo:
rosrun tf tf_echo world turtle1
Утилите tf_echo нам обязательно надо будет передать два
аргумента. Первым аргументом будет tf, относительно которой мы будем
получать информацию – в данном случае “мировая”, а вторым аргументом tf,
для которой мы эту информацию будем получать – в данном случае turtle1.
Третьим аргументом можно указать частоту обновления информации, но это
не обязательно. В терминале мы увидим нечто подобное:
Если мы перемещаем черепашку, информация для смещения и поворота
изменяется. Попробуйте погонять черепашку по экрану и посмотреть, как
изменяются координаты. Также можно попробовать выводить координаты
turtle2 в системе координат (не забываем, что tf – это инструментарий
для работы с СК) turtle1. Проделайте это сами, пожалуйста.
Утилита TF Monitor
Ещё одна консольная утилита для работы с tf – TF Monitor. Если
запустить её с параметрами двух СК, turtle1 и turtle2, например, так
rosrun tf tf_monitor turtle1 turtle2, то она выдаст
информацию о цепочке преобразований tf и работе вещателей:
Работа с tf в RViz
Давайте запустим RViz командой rviz и проделаем такую
операцию, на панели Displays в разделе Global
Options->Fixed Frame заменим map на world:
После этого в самом низу панели Displays нажмём кнопку
Add:
и там, в длинном списке, который мне пришлось обрезать, выберем By
display type->rviz->TF:
Мы назначили в качестве базовой системы координат для RViz СК world
симулятора turtlesim, а затем попросили предъявить нам на экране RViz
существующие на данный в ROS системы координат.
На основном экране мы увидим нечто похожее:
Если turtle1 и turtle2 находятся далеко от world, попробуйте двинуть
черепашек к началу мировой СК в левый нижний угол.
На этом с tf мы на сегодня закончим, но уже никуда от них не денемся.
Они будут сопровождать нас всё время работы с ROS.
3.5 Заключение (старое)
Давайте вспомним, чем мы занимались сегодня. Мы познакомились с
симулятором простейшего мобильного робота turtlesim на уровне понимания
взаимодействия с ним средствами ROS, узнали, как получить информацию о
формате сообщения, создали свою ноду, которая одновременно была и
издателем и подписчиком для симулятора черепашки, разобрались с тем, как
черепашка движется, и разобрались, для чего нужна обратная связь от
робота. Записанное коротко впечатляет не очень, но на самом деле мы
фактически написали программу для управления нашим первым мобильным
роботом. Также мы получили некоторое представление о средствах ROS для
работы с системами координат – библиотеке tf, принципах работы вещателя
и слушателя tf и утилитах, полезных при разработке и отладке приложений
касающихся tf.
3.5 Заключение (новое)
На этом уроке мы совершили важный шаг в освоении ROS: познакомились с
симулятором мобильного робота turtlesim, научились
взаимодействовать с ним через систему топиков, а также создали
собственную ноду, способную как управлять движением черепашки, так и
получать обратную связь о её положении.
Мы изучили структуру сообщений типа geometry_msgs/Twist,
научились подписываться на данные о состоянии робота и использовать их
для замкнутого управления. Важным этапом стало понимание разницы между
внутренней и внешней системами координат, что особенно критично при
работе с реальными роботами.
Кроме того, мы начали знакомство с библиотекой tf —
мощным инструментом для работы с преобразованиями систем координат. Мы
рассмотрели, как tf строит дерево координат, как используются вещатели
(TransformBroadcaster) и слушатели
(TransformListener), а также познакомились с полезными
утилитами, такими как rqt_tf_tree, tf_echo,
tf_monitor и визуализация в RViz.
Этот урок дал не только практические навыки программирования на Python в
ROS-среде, но и первое представление о том, как роботы воспринимают
пространство и взаимодействуют с ним. Вы уже умеете:
- Создавать и запускать ROS-пакеты; - Писать ноды, работающие с
топиками; - Управлять симуляцией и обрабатывать обратную связь; -
Работать с системами координат и tf; - Использовать основные инструменты
диагностики и отладки.
Это серьёзная база для дальнейшего изучения ROS.