Максим Данилин, 20.07.2021

Как сделать робота на ROS своими руками. Часть 1: шасси и бортовая электроника

Привет!

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

Далее мы постараемся пошагово и как можно подробнее рассказать вам о процессе проектирования, конструирования и программирования робота, а также расскажем, с каким трудностями и проблемами мы столкнулись.

Содержание

Введение

Создание свеого робота — это не самая сложная, но и не тривиальная задача. Она требует опредёленных навыков хотя бы на минимальном уровне:

  • Работа с OS Linux.
  • Твердотельное 3D-моделирование.
  • Программирование на С++ или Python.
  • Навыки 3D-печати.

Также вам понадобится персональный компьютер под управлением OS Linux, Windows и локальная сеть Wi-Fi.

Все исходники — как конструкторские САПР-файлы, так и исходный код — мы разместили в GitHub-репозитории https://github.com/amperka/abot.

Цель робота

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

  • Как должен выглядеть мой робот?
  • Из каких частей/сегментов будет состоять мой робот?
  • Что должен делать мой робот и как?

Все промышленные роботы создаются с какой-либо целью. Люди стремится облегчить себе жизнь, переложив часть своих задач на роботов. Машины могут выполнять тяжелую физическую работу: например, как сварочные роботы-манипуляторы или транспортные роботы на автоматизированных складах-хранилищах. Или же роботы могут взяться за опасные для человека задачи: например, обезвредить бомбу вместо сапёра, работать в завалах или токсичных и ядовитых средах. А такие вещи, как робот-пылесос и беспилотный транспорт избавляют человека от рутинных задач для экономии времени.

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

Мы попробуем создать «офисного питомца» — робота, который будет жить с нами в офисе и передвигаться туда, куда мы ему скажем. При этом робот должен будет самостоятельно ориентироваться в помещении. Кажется, что навигация в офисе — это лёгкая задача, однако это совсем не так. Мы планируем постепенно добавлять нашему роботу новые функции, но начнём именно с этой конкретной задачи.

Поскольку мы решили, что наш робот не стационарный (как робот-манипулятор), а мобильный, то ему сперва понадобится какое-то подвижное шасси.

Привод робота

Типы приводов

Рассмотрим разные типы приводов мобильных роботов и выясним, какую механику они используют. Затем, сравнивая все плюсы и минусы, выберем привод для нашего робота.

Роботы могут передвигаться в 2D- или 3D-пространстве. Очевидно, что только летающие роботы способны маневрировать во всех плоскостях, и они чрезвычано сложны. Например, летающие дроны ориентируются в помещении или на местности, используя трёхмерные камеры глубины. Постройка такого робота потребует сложнейшего железа и программного обеспечения, поэтому летающего дрона мы не рассматриваем.

Если роботу нужно передвигаться только в двухмерном пространстве, всё становится уже проще. Мы можем рассмотреть движение робота как движение материальной точки в плоскости (X, Y) в прямоугольной или Декартовой системе координат (X, Y, Z).

Движение робота в плоскости может быть голономным (Holonomic) или неголономным (Non-holonomic). Что это значит? При голономном движении робот способен свободно двигаться по любому вектору XY, не меняя при этом своей ориентации. При неголономном движении робот может передвигаться только в нескольких ограниченных направлениях.

part_1_ru_robot_drive_0_scheme_1.png

Например, обычный автомобиль не может взять и подвинуться строго вправо или влево с места — значит, его движение неголономно. С другой стороны, если бы у автомобиля вместо обычных колёс стояли всенаправленные, то он смог бы двигаться голономно.

Вы, наверное, спрашиваете себя: «Зачем мне всё это нужно знать?» Понимание того, каким именно образом движется робот, и понимание принципов получения проекций его скоростей в системе координат чрезвычайно важно при создании управляющей программы. Чем больше направлений движения и вращения имеет робот, тем сложнее сконструировать его механику и контролировать его движение.

Ещё одиним важным фактором при выборе привода робота является сложность получения одометрии и её точность.

Одометрия — это использование данных с установленных на роботе сенсоров и датчиков для расчёта его текущего положения и ориентации в пространстве. С некоторых приводов получить качественную одометрию очень легко — например, с двухколёсного дифференциального привода (2WD differential drive). Для этого достаточно парочки колёсных энкодеров. С других же приводов получить точную одометрию невероятно трудно: например, для шагающей робо-собаки или робота-гуманоида вам понадобятся десятки различных 2D/3D-сенсоров и сложнейший софт.

Мы попробовали собрать самые популярные способы передвижения для хобби-роботов.

Дифференциальный привод двумя ведущими колёсами с пассивными опорами

Двухколёсный дифференциальный привод — самый простой и распространённый тип привода в любительской робототехнике. Именно он используется в домашних роботах-пылесосах.

Двухколёсное дифференциальное шасси состоит из двух ведущих колёс, которые установлены на противоположных сторонах корпуса, и одного или нескольких всенаправленных (пассивных) колёс или опор. Каждое ведущее колесо приводится в движение собственным мотором и управляется независимо друг от друга. Пассивные колёса-опоры устанавливаются на такое шасси для достижения равновесия всей платформы.

К слову, именно такой тип привода имеют наши Робоняша и Драгстер.

Движение двухколёсного дифференциального привода неголономно. Перемещение робота здесь задаётся линейной скоростью по оси Х (вперёд или назад) и угловой скоростью вокруг оси Z (вращение на месте). Синтез этих двух скоростей заставляет робота поворачивать во время движения.

Вот основные типы движения такого робота:

part_1_ru_robot_drive_1_scheme_1.png

part_1_ru_robot_drive_1_scheme_2.png

part_1_ru_robot_drive_1_scheme_3.png

part_1_ru_robot_drive_1_scheme_4.png

Особенности шасси:

  • Легко сконструировать.
  • Легко программировать контроллер движения.
  • Легко получить относительно качественную одометрию всего двумя датчиками вращения колёс.
  • Шасси не предназначено для движения по бездорожью. Любая значительная преграда на пути может вывести двухколёсную платформу из равновесия. Чаще всего это шасси используется в помещении и для движения по ровной поверхности.

part_1_robot_drive_1_showcase_1.jpg

Turtlebot3 от Robotis

part_1_robot_drive_1_showcase_2.jpg

PAL Robotics

part_1_robot_drive_1_showcase_3.jpg

Мобильный робот MP-500 от Neobotix

part_1_robot_drive_1_showcase_4.jpg

The Innok Robotics

Дифференциальный привод Skid-steer

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

Движение привода skid-steer неголономно. Принцип движения такой же, как и у двухколёсного дифференциального привода.

Пример задания скоростей робота:

part_1_ru_robot_drive_2_scheme_1.png

Особенности шасси:

  • В сравнении с двухколёсной платформой skid-steer обладает повышенной проходимостью. Это достигается благодаря множеству колёс и отсутствию пассивных опор. Робот с большими колесами может быть очень эффективен на пересечённой местности.
  • Одометрию так же легко получить, используя датчики вращения колёс. Однако каждое колесо шасси skid-steer нуждается в собственном сенсоре. Точность одометрии в сравенении с двухколёсной платформой заметно ниже. При поворотах робота колёса шасси проскальзывают. При движении такого шасси по ровной местности моменты заноса и скольжения можно определить и исправить программно. Но для получения одометрии при движении платформы skid-steer по пересечённой местности одних только датчиков вращения колёс может быть уже не достаточно.

part_1_robot_drive_2_showcase_1.jpg

Husky robot

part_1_robot_drive_2_showcase_2.jpg

Wild Thumper 6WD от DAGU Electronics

part_1_robot_drive_2_showcase_3.jpg

The Innok Robotics

Дифференциальный привод с гусеницами

Дифференциальный привод с гусеницами (танковое шасси) — версия привода skid-steer с гусеницами вместо дополнительных колёс. Как и ранее, каждая гусеница и сторона робота контролируется одним мотором.

Можно интерпретировать этот привод как двухколёсный дифференциальный, где колесо имеет некруглую форму и увеличенную длину окружности. Или как привод skid-steer с бесконечным количеством колёс на определённой длине.

Движение привода с гусеницами неголономно. Принцип движения тут такой же, как и у привода skid-steer. Только для обработки перемещения робота используются не угловые скорости колёс, а скорости гусениц.

Особенности шасси:

  • Танковое шасси обладает самыми высокими эксплуатационными характеристиками на пересечённой местности благодаря форме гусениц и сцеплению с землёй.
  • Усложнённая механика. В конструкции танкового шасси множество непростых деталей: части трака, натяжители, опорные ролики и т. д.
  • Получить одометрию ещё сложнее, чем при использовании привода skid-steer. При движении танкового шасси тоже происходят проскальзывания гусениц и заносы (особенно это заметно, когда робот-танк вращается на месте на ровной поверхности). Однако ввиду наличия всего двух датчиков вращения программно компенсировать ошибки одометрии очень тяжело. При движении по ровной поверхности одометрия неточная, а на пересечённой местности датчики вращения становятся практически бесполезны, и для одометрии понадобятся другие источники.

part_1_robot_drive_3_showcase_1.jpg

Robodyne MAXXII

part_1_robot_drive_3_showcase_2.jpg

Tank chassis

part_1_robot_drive_3_showcase_3.jpg

Dragon Runner Bomb Disposal Robot

Рулевой привод Аккермана

Привод Ackermann steering — самый распространённый в мире, так как используется в каждом автомобиле. Привод Аккермана состоит из двух ведущих и двух рулевых колёс. Ведущая пара колёс отвечает за движение робота, а рулевые колёса отвечают за повороты. Чтобы избежать заноса и скольжения, рулевое управление Аккермана спроектировано таким образом, что при повороте внутреннее колесо поворачивается на больший угол, чем внешнее. Для каждого колеса угол поворота рассчитывается на основе желаемого диапазона углов поворота робота.

Рулевой привод Аккермана имеет неголономное движение. Этот привод управляется линейной скоростью вдоль оси X и угловой скоростью по оси Z. Но в отличие от дифференциальных приводов, при ненулевой угловой скорости вокруг оси Z линейная скорость по X не может быть равна нулю. Как и автомобиль, робот не сможет развернуться, стоя на месте.

part_1_ru_robot_drive_4_scheme_1.png

Особенности шасси:

  • Рулевое управление Аккермана обычно используют на ровной поверхности для быстродвижущихся роботов, которые нуждаются в большом дорожном просвете и сцеплении с землёй.
  • Наличие вращающихся рулевых колёс усложняет конструкцию робота и требует дополнительных двигателей и приводов.
  • Отличный пример использования этого привода в робототехнике — настоящие беспилотные автомобили. Кроме того, этот привод используется в хобби-робототехнике, если робот построен на базе радиоуправляемой игрушечной машинки.

part_1_robot_drive_4_showcase_1.jpg

VolksBots

part_1_robot_drive_4_showcase_2.jpg

RB-CAR от Robotnik

Привод с Omni-колёсами

Этот тип привода использует особые Omni-колёса или поликолеса вместо обычных. Поликолесо — это всенаправленное колесо с небольшими роликами, расположенными по окружности. Оси роликов перпендикулярны оси вращения колеса. Контролируя скорость и направление вращения поликолёс, вы можете заставить робота двигаться в любом направлении — другими словами, сделать его движение голономным.

Обычно шасси с Omni-колёсами насчитывает ровно 3 или 4 колеса. Шасси с тремя колёсами обеспечивает большую тягу, поскольку любая реактивная сила распределяется только через три точки, и робот хорошо сбалансирован даже на неровной местности. Всенаправленные колёса имеют высокую стоимость, поэтому трёхколёсное шасси обходится заметно дешевле варианта с четырьмя.

Чаще всего для трёхколёсного шасси выбирают угол установки 120°. Иногда два колеса параллельны друг другу, а третье ставится перпендикулярно к ним. Последняя конструкция может быть более эффективной, потому что когда колёса расположены под углом 120°, только одно из них является ведущим, а два других по сути тормозят его, снижая общую скорость.

Поскольку колёса на таком шасси не выровнены по осям, каждое из них требует индивидуального расчёта скорости.

Пример определения векторов скоростей колёс для трёхколёсного шасси:

part_1_ru_robot_drive_5_scheme_1.png

Четырёхколёсное шасси имеет четыре ведущих колеса, расположенных под углом 90° друг к другу. Эта конструкция удобнее для расчёта скоростей, так как два колеса параллельны друг другу, а два других перпендикулярны к ним. Как и в трёхколёсном шасси, КПД всех колёс также не используется на 100%. Но в отличие от трёхколёсного шасси, здесь есть два ведущих колеса и два свободных. Таким образом, четырёхколёсное шасси движется быстрее, чем трёхколёсное. Четвёртое колесо добавляет шасси ещё одну точку опоры, и на неровной местности одно из колёс робота может оказаться в воздухе.

Пример определения скорости вращения колёс для четырёхколёсного шасси:

part_1_ru_robot_drive_5_scheme_2.png

Особенности шасси:

  • Поскольку всенаправленные колёса представляют собой комбинацию из множества роликов, возникает сопротивление вращению, что приводит к повышенному трению и значительным потерям энергии.
  • Не все колёса являются ведущими, в каждый момент времени эффективно работают лишь одно-два поликолеса.
  • С помощью Omni-колёс достигается голономное движение робота.
  • Работа поликолёс изначально строится на принципах проскальзывания. Невозможно поставить датчик вращения на каждый ролик Omni-колеса, поэтому полученная колёсная одометрия совсем неточная.
  • Чаще всего роботы с таким шасси используются внутри помещений на ровных и гладких поверхностях.

part_1_robot_drive_5_showcase_1.jpg

3WD Omni wheel chassis от NEXUS robot

part_1_robot_drive_5_showcase_2.jpg

Soccer robots от RoboFEI Team

part_1_robot_drive_5_showcase_3.jpg

King Kong 4WD Omni Wheel chassis

Привод с Mecanum-колёсами

Этот тип привода использует вместо обычных колес колёса Mecanum (шведское колесо Илона), предназначенные для грузоподъёмных и проходимых роботов. По сути это разновидность Omni-колеса, только на шведском колесе Илона ролики по всей окружности обода расположены под углом 45° к плоскости и 45° к оси вращения колеса.

Поворот оси ролика позволяет использовать колёса Mecanum в приводах skid-steer. Эта комбинация объединяет преимущества шасси skid-steer и привода со всенаправленными колёсами. Колёса Илона заменяют обычные для достижения голономного движения робота. Чаще всего этот тип шасси имеет 4 Mecanum-колеса, но иногда встречается и 6 колёс.

При вращении колеса Илона прилагается сила под углом 45° к его оси. Направление вращения определяет направление приложенной силы. Комбинации сил от всех колёс позволяют роботу двигаться в разных направлениях.

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

part_1_ru_robot_drive_6_scheme_1.png

part_1_ru_robot_drive_6_scheme_2.png

part_1_ru_robot_drive_6_scheme_3.png

part_1_ru_robot_drive_6_scheme_4.png

Особенности шасси:

  • Привод с колёсами Илона используется, если робот должен обладать голономным движением и высокой грузоподъёмностью.
  • Чаще всего этот тип шасси встречается у грузовых роботов, которые работают на ровной и гладкой поверхности. При использовании такого шасси на бездорожье управление движением и получение качественной одометрии с датчиков вращения колеса крайне затруднено.

part_1_robot_drive_6_showcase_1.jpg

Kuka robot

part_1_robot_drive_6_showcase_2.jpg

Mobile Robot MPO-500 от Neobotix

part_1_robot_drive_6_showcase_3.jpg

SUMMIT-XL STEEL от Robotnik

Скелетные роботы

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

Роботы на конечностях обладают голономным движением, также как и живые организмы, кинематику которых они повторяют. Например, робот-гексапод может идти в любом направлении, не меняя ориентацию своего тела.

Подобные роботы являюстя самыми мобильными, но и самыми сложными в конструировании. Скелет конечности должен обладать множеством стенепенй свободы. Для этого требуется множество двигателей и приводов, а также сложные системы управления. Из-за большого количества приводов скелетные роботы потребляют больше всего энергии. Для получения одометрии с шагающего шасси используется синтез данных с множества различных сенсоров (энкодеры приводов, IMU-сенсоры, 3D-лидары, 3D RGB-камеры глубины, контактные датчики давления и т. д.), а также машинное обучение.

part_1_robot_drive_7_showcase_1.jpg

Agility Robotics

part_1_robot_drive_7_showcase_2.jpg

Spot от Boston Dynamics

Другие типы приводов

В хобби-робототехнике существует великое множество уникальных приводов движения, которые используются крайне редко. Вот лишь некоторые из них:

  • Segway drive — дифференциальный двухколёсный привод без пассивных колес. Равновесное состояние робота достигается с помощью датчиков и контроллеров. Пример — автономный сегвей.
  • Forklift steering drive — разновидность рулевого привода Аккермана, но с задней парой рулевых колёс и передней парой ведущих.
  • Independent drive — привод, в котором все колёса являются ведущими и рулевыми одновременно. Колёс может быть четыре, шесть и более. Пример — марсоход.
  • Articulated drive — разновидность рулевого привода Аккермана. Чтобы рулить роботом в движении, Articulated drive не поворачивает рулевые колёса, а деформирует всю рулевую часть рамы или шасси.
  • Ball drive — привод, при котором робот балансирует и перемещается на сфере.
  • Ползучие червеобразные и змееподобные роботы, движение которых основано на трении тела с поверхностью.

Выбор шасси

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

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

Сперва мы попробовали танковое шасси Rover 5 с резиновыми гусеницами. Установили дополнительные двигатели и энкодеры на колёса. Но, как оказалось, получить качественную одометрию только с помощью энкодеров довольно сложно. Когда робот вращается на месте и на высоких скоростях, гусеницы регулярно проскальзывают и результирующая одометрия отличается от фактического положения робота. Поэтому мы решили начать с более простого шасси.

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

Чтобы самостоятельно собрать качественное робо-шасси, нужно обладать некоторыми навыками проектирования машин, разбираться в материалах, комплектующих и умело работать руками. Собранное своими руками шасси даёт вам полное знание всех его деталей, узлов, ключевых моментов и слабых мест. Чем сложнее тип привода и шасси, тем больше вероятность, что вам придётся конструировать его самостоятельно.

С другой стороны, многие производители предлагают высококачественные шасси для хобби-робототехники. Приобретя готовое шасси, вы сможете сэкономить много времени на механической составляющей робота и потратить это время на электронную и программную часть.

Это важно! При покупке готового шасси выбирайте наиболее документированное, с маркировкой деталей, информацией о двигателях и полными чертежами основных деталей и компонентов в САПР.

Шасси Turtle

В нашем роботе мы решили использовать робо-платформу Turtle от DFRobot.

part_2_prod_chassis_1.jpg

Это шасси для небольшого мобильного робота. Рама изготовлена из металла и состоит из двух согнутых листовых металлических пластин. Обе пластины имеют перфорацию и вырезы для установки электроники. Шасси содержит два мотор-редуктора (160 об/мин, 6 В) типа TT с двухсторонним валом L-образной формы, два пластиковых колеса диаметром 65 мм и 15-миллиметровое стальное шариковое колесо. В комплект шасси входит ещё много других деталей и креплений, но они нам не нужны. Понадобится только рама шасси.

part_2_prod_chassis_2.jpg

Шасси Turtle недорогое, но и не самого лучшего качества:

  • Данная робо-платформа слабо документирована. Мы не нашли чертежей шасси в открытом доступе.
  • Покрышки колёс, которые идут в комплекте, пластиковые и бесполезные, потому что у них почти нет сцепления с землёй. Мы сразу же заменили их резиновыми шинами для 2WD и 4WD от того же производителя.
  • Коробки передач моторов типа TT имеют пластиковый редуктор 1:120. Было бы лучше, если коробка передач была сделана из металла.
  • Двигатели постоянного тока работают без обвязки и генерируют значительные электромагнитные наводки. На полной скорости эти двигатели существенно влияют на работу близлежащих аналоговых и цифровых электронных устройств.

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

Энкодеры

Для колёсной одометрии нам понадобятся датчики угла поворота или энкодеры. Готовые шасси часто уже оснащены энкодерами, но платформа Turtle поставляется без них. По типу отдаваемых данных энкодеры могут быть абсолютными или инкрементальными.

Абсолютный энкодер выдаёт сигнал, который однозначно соответствует углу поворота вала. Энкодеры этого типа не требуют привязки системы отсчёта к какому-либо нулевому положению.

Инкрементальный энкодер генерирует импульсы на выходе. Контроллер подсчитывает количество импульсов с помощью счётчика и определяет текущее положение вала. Сразу после включения контроллера положение вала неизвестно. Для привязки системы отсчёта к нулевой позиции существует специальная нулевая отметка, через которую вал должен пройти после включения. Основным недостатком энкодеров такого типа является невозможность определить пропуск импульса, вызванный какой-либо причиной. Пропуски импульсов накапливают погрешность в угле поворота вала до тех пор, пока не будет пройдена нулевая отметка. Инкрементальный энкодер может быть и без нулевой отметки. В этом случае отсчёт импульсов, полученный в начале накопления, является началом системы отсчёта.

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

В основном в хобби-робототехнике используются оптические и магнитные инкрементальные энкодеры или магнитные абсолютные энкодеры. Не критично, какой тип энкодера вы будете применять. При выборе энкодера основными параметрами являются количество каналов передачи данных, количество импульсов на оборот (PPR) и максимально допустимая скорость вращения вала.

Наиболее популярны квадратурные энкодеры с двумя каналами А и В. Реже у энкодеров в хобби-сегменте есть канал с нулевой отметкой — Z. Чем выше значение PPR, тем меньше угол поворота вала, который может зафиксировать датчик. Чем точнее энкодер, тем точнее одометрия робота, поэтому не пренебрегайте высококачественными энкодерами и не используйте энкодеры с низким значением PPR. Максимальная скорость вращения может быть любой, так как большинство энкодеров способно работать на очень высоких скоростях. Скорее всего, скорость вращения, которую вы будете измерять, будет в несколько раз меньше максимальной. Даже если вы планируете использовать высокоскоростные BLDC двигатели с высоким значением kv, то существуют энкодеры, которые работают с максимальными скоростями 28000 об/мин, 60000 об/мин или даже больше.

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

Мы обзавелись двумя такими двигателями с энкодерами — мотор-редуктор TT с энкодером (160 об/мин, 6 В, 120:1).

part_2_prod_chassis_3.jpg

Почему мы выбрали именно эти моторы?

  • Во-первых, их конструкция специально разработана для нашего шасси Turtle, и нам не придётся придумывать крепление.
  • Во-вторых, производитель исправил существенные недостатки, заменил пластиковые шестерни металлическими и добавил схеме двигателя обвязку.
  • Эта сборка имеет квадратурный магнитный энкодер с разрешением 16 PPR, установленный на валу двигателя. Передаточное отношение редуктора 120:1 даёт полное разрешение в 1920 импульсов на оборот колеса с минимальным измеряемым шагом в 0°11’15". Для поставленной нам задачи такой точности более чем достаточно.

Заменив моторы и убрав всё лишнее, мы получили вот такое шасси:

ROS

Мы разобрались с шасси. Давайте начнём разбираться в программном обеспечении. Для настоящих роботов привычные нам программы микроконтроллеров не подходят. Вместо этого мы используем ROS.

ROS (Robot Operation System) — это операционная система для роботов. Она обеспечивает всю необходимую функциональность для распределённой работы всех узлов робота. На самом деле ROS — это библиотека, надстройка поверх компьютерной операционной системы. ROS предоставляет стандартные возможности операционной системы, такие как аппаратная абстракция, низкоуровневое управление устройствами, реализация часто используемых функций, передача сообщений между процессами и управление пакетами.

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

Пакетов очень и очень много. Поскольку ROS — это проект с открытым исходным кодом, сообщество уже написало большинство пакетов для реализации стандартных функций роботов. Именно благодаря обилию готовых open-source пакетов ROS стал так крут и популярен. Тем не менее, некоторые ноды нам придётся написать самостоятельно: например, низкоуровневые драйверы, но большая часть программного обеспечения уже сделана. Нам останется только собрать всё вместе.

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

ROS содержит множество пакетов для создания виртуального робота и симулирования его поведения — например, стек пакетов gazebo_ros_pkgs. C помощью Gazebo вы сможете даже симулировать знаменитого робота Atlas от Boston Dynamics у себя на компьютере.

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

Мы сделаем всё наоборот: построим робота из недорогих деталей, которые есть под рукой. Симуляцию использовать не будем, но настроим ROS для нашего конкретного робота.

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

Предположим, что у вас уже есть настольный компьютер. Теперь вам нужно разобраться с бортовым.

Бортовой компьютер

Выбор железа

Итак, робот работает на ROS, которой нужен Linux. Давайте выберем подходящий бортовой Linux-компьютер для робота.

Крупные мобильные роботы, способные нести большую нагрузку, имеют на борту большие и мощные компьютеры. Если не стоит вопрос размера и формы бортового компьютера, чаще всего в роботах устанавливают мощный ноутбук. Но наш робот маленький, и на нём нет места для кучи оборудования и соответствующего питания. Поэтому мы и возьмём одноплатный компьютер, которых на сегодняшний день существует целое множество. Перечислим самые популярные:

  • Raspberry Pi Zero, 3, 4 — самые популярные платы. Изначально Raspberry Pi предназначен для обучения программированию и Linux. Эти платы самые доступные и хорошо задокументированные. Семейство Raspberry Pi имеет множество реплик и копий, дополненных различными периферийными интерфейсами и устройствами для разных задач: Orange Pi, Rock Pi, Banana Pi.
  • NVIDIA Jetson Nano Developer Kit — самая многофункциональная плата. Она может применяться для настольного компьютера, но изначально предназначена для разработки мобильного искусственного интеллекта и машинного обучения. Эта плата использует для вычислений мощный графический процессор NVIDIA из множдества ядер.
  • Coral Dev Board — лучшая плата машинного обучения с использованием фреймворка Tensorflow. Она специально заточена для работы с нейронной сетью TensorFlow Lite для микроконтроллеров.
  • ODYSSEY X86J4105800 — самый большой и мощный одноплатник. Он способнен работать под полноценной версией Windows 10 и обладает всеми функциями настольного ПК.
  • Rock Pi N10 — лучшая плата для машинного обучения, которая имеет вычислительную мощность до 3,0 Терафлопс.

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

Мы не планируем заниматься машинным обучением или выполнять массивные вычисления, а это означает, что нашему роботу подойдёт практически любой компактный одноплатник.

Мы выбрали Raspberry Pi 4 Model B на 4 ГБ. Плата Raspberry Pi 4 может сильно нагреваться, так что мы установили на неё пару алюминиевых радиаторов для пассивного охлаждения.

part_3_irl_mcu_1.jpg

Подключите любую клавиатуру и мышь к USB-портам платы. Подключите любой дисплей или монитор с помощью кабеля micro-HDMI.

part_3_irl_mcu_2.jpg

Всё это нужно нам для первичной установки программного обеспечения и отладки программ. Позже нам уже не понадобится ничего лишнего, так как мы будем удалённо работать с Raspberry Pi по сети с помощью SSH.

Производитель одноплатника рекомендует использовать источник питания 3 А и более. Когда мы установим Raspberry Pi на робота, мы обеспечим плату достаточным напряжением и током, но в первый раз мы можем запитать плату от любого источника питания USB через кабель USB Type-C — например от импульсного блока питания 5 В / 3 А.

Это важно! Не выключайте плату Raspberry Pi 4 с установленной в неё картой памяти простым выдёргиванием кабеля! Используйте корректное завершение работы средствами операционной системы.

Выбор и установка дистрибутива Linux

Прежде чем выбрать ОС для установки, вам нужно определиться с версией ROS, где выпускаются дистрибутивы для различных операционных систем и архитектур: Windows, Ubuntu, Debian и других ОС Linux. Глобальные обновления ROS основаны на глобальных обновлениях ОС Ubuntu. Вскоре после выходна новой версии Ubuntu появляется и новая версия ROS. Предыдущие версии ROS продолжают поддерживаться до тех пор, пока поддерживается дистрибутив Ubuntu, для которого они были созданы. На момент написания этого руководства нам доступны два поддерживаемых дистрибутива ROS:

  • ROS Melodic Morenia, нацеленный на Ubuntu 18.04 (Bionic). Поддерживает Ubuntu 17.10 (Artful). Дата релиза — 23 мая 2018. Дата конца поддержки — май 2023 (Bionic).
  • ROS Noetic Ninjemys, нацеленный на Ubuntu 20.04 (Focal). Дата релиза — 23 мая 2020. Дата конца поддержки — май 2025 (Focal).

Официальная документация рекомендует установку последней версии ROS Noetic для последней версии Ubuntu Focal. Поскольку ROS живёт благодаря сообществу, то обновление и поддержка различных пакетов может запаздывать, даже если ядро обновляется всегда вовремя. Обратите внимание, что некоторые интересные пакеты всё ещё не обновлены для новой версии Noetic, и вам, возможно, придётся собирать их вручную.

Перейдём в раздел установки ROS Noetic и посмотрим, какие операционки и процессорные архитектуры мы можем использовать:

  • Ubuntu Focal на amd64, armhf, arm64.
  • Debian Buster на amd64, arm64.
  • Windows 10 на amd64.
  • Не гарантировано: любой дистрибутив Linux на amd64, i686, arm, armv6h, armv7h, aarch64.

Raspberry Pi 4 имеет набор инструкций ARMv8-A и поддерживает 32-разрядные и 64-разрядные вычисления. Обычно пользователи устанавливают на Raspberry 32-разрядную ОС. Однако у нашей малины 4 ГБ оперативной памяти, а бывает и версия с 8 ГБ. С таким объёмом оперативной памяти вы можете попробовать 64-разрядную ОС, которая будет справляться с определёнными задачами быстрее.

Для ROS Noetic мы устанавливаем на одноплатник Ubuntu Server 20.04.2 (arm64). Для хранения ОС вам понадобится флеш-карта microSD. Чем больше ёмкость карты и её класс скорости — тем лучше. Мы использовали карточку microSD на 16 ГБ.

Скачайте образ ОС. Зайдите на официальный сайт Ubuntu и перейдите в раздел DownloadsUbuntu for IOTRaspberry Pi 2, 3 or 4. Загрузите образ 64-разрядной ОС Ubuntu Server для архитектуры arm.

Установите программу для создания загрузочных флеш-накопителей. Мы используем официальную программу Raspberry Pi Imager. Перейдите на официальный сайт Raspberry Pi Foundation. Затем перейдите в раздел DownloadsRaspberry Pi Imager, загрузите и установите версию для вашей ОС.

Вставьте SD-карту в компьютер и отформатируйте её.

Запустите Raspberry Pi Imager. Затем войдите в меню Operating system и выберите Use custom.

part_4_linux_install_1.png

Укажите путь к скачанному образу Ubuntu, затем путь к вашей флеш-карте и нажмите кнопку Write.

part_4_linux_install_2.png

part_4_linux_install_3.png

Когда запись ОС завершится, вставьте microSD-карту в Raspberry Pi и включите плату, подав на неё питание.

Первый запуск

Убедимся, что Linux работает. При первой загрузке Ubuntu попросит вас изменить стандартный логин ubuntu и пароль ubuntu. Установите свои данные для администраторского входа в систему. Обратите внимание: пароль не может быть палиндромом; это одна из стандартных настроек ОС Ubuntu Server.

Ubuntu 20.04.2 LTS ubnutu tty1

ubuntu login: ubuntu
Password:
You are requested to change your password immediately (root enforced)
Changing password for ubuntu.
(current) UNIX password:
Enter new UNIX password:
Retype new UNIX password:
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-1015-raspi aarch64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Wed Apr 1 17:25:35 UTC 2020

  System load:  0.27                Swap usage:  0%          Users logged in: 0
  Usage of /:   12.8% of 14.03GB    Temperature: 46.7 C 
  Memory usage: 6%                  Processes:   134

0 packages can be updated.
0 updates are security updates.

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ubuntu@ubuntu:~$

Пароль для root не установлен в Ubuntu по умолчанию, и вход в систему пользователя root отключён. Включим учетную запись root и установим для неё пароль. Затем переключимся на root:

sudo passwd root
su root

При желании вы можете переименовать стандартное имя текущего хоста в более привлекательное. Мы назовём бортовой компьютер robot.

Отредактируем файл /etc/hostname и заменим ubuntu на robot:

nano /etc/hostname

Затем перезагрузимся и снова войдём в систему под root:

reboot now

Настройка Wi-Fi и графической оболочки

Теперь нам нужно подключиться к Интернету через Wi-Fi-адаптер на Raspberry. Предполагается, что у вас уже есть точка доступа Wi-Fi.

Первый шаг — определить имя вашего беспроводного сетевого интерфейса. Оно может быть разное, но обычно это wlan0:

ls /sys/class/net/

Затем перейдём в каталог /etc/netplan и найдём соответствующие файлы конфигурации Netplan. Файл конфигурации имеет имя типа 50-cloud-init.yaml:

ls /etc/netplan/

Отредактируем файл конфигурации Netplan:

nano /etc/netplan/50-cloud-init.yaml

Весь файл конфигурации должен выглядеть примерно так, как показано ниже. Убедитесь, что все блоки кода выровнены. Для выравнивания используйте пробелы вместо табуляции. Замените строки SSID и PASSWORD на имя и пароль вашей сети Wi-Fi.

# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets:
        eth0:
            dhcp4: true
            optional: true
    version: 2
    wifis:
        wlan0:
            optional: true
            access-points:
                "SSID":
                    password: "PASSWORD"
            dhcp4: true

Запустим службу, перезагрузимся и войдём в систему:

systemctl start wpa_supplicant
reboot now

Применим изменения Netplan и подключимся к беспроводной сети:

sudo netplan generate
sudo netplan apply

Ещё раз перезагрузимся и снова войдём в систему. Теперь наша Raspberry в сети Wi-Fi, и мы можем пропинговать наш сетевой шлюз:

ip addr show
ping 192.168.88.1

Следующим шагом будет установка графического интерфейса для удобства работы с операционной системой. Обновим список пакетов из репозитория, обновим сами пакеты и установим любую понравившуюся графическую оболочку. Например, мы выбрали XFce:

sudo apt-get update && apt-get upgrade
sudo apt-get install xubuntu-desktop

Установка графической оболочки может занять некоторое время. После установки перезагрузимся и войдём в систему под ubuntu.

Установка и настройка ROS

Мы установим ROS на две машины: на Raspberry Pi и настольный компьютер, а затем обьединим системы ROS в единую сеть.

Зачем нужно два компьютера с ROS? Разработка программного обеспечения для робота тесно связана с ресурсоёмкой визуализацией. Работа с графикой, трёхмерными обьектами и визуализация на Raspberry существенно тормозит разработку ПО.

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

Если бы вместо Raspberry Pi на нашем роботе стоял мощный ноутбук, мы бы смогли вести весь проект от начала и до конца на одной ROS-машине. В нашем случае ROS на роботе будет оперировать простыми вычислениями и поддерживать работу драйверов низкого уровня, а настольный компьютер с ROS будет заниматься трудоёмкими вычислениями навигации и визуализацией.

Установка ROS на Raspberry Pi

Установим ROS Noetic на Raspberry, следуя рекомендациям из руководства по инсталляции. Откроем новый терминал и продолжим под юзером ubuntu.

Настроим Raspberry на приём программного обеспечения из репозитория packages.ros.org:

sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'

Настраиваем ключи:

sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654

Обновим список пакетов:

sudo apt-get update

Установим полную версию ROS, используя стек пакетов ros-noetic-desktop-full:

sudo apt install ros-noetic-desktop-full

Настраиваем переменные окружения ROS и автоматически добавляем их в bash-сеанс при каждом новом запуске Ubuntu:

echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc
source ~/.bashrc

Установим пакет rosdep, который позволяет вам легко устанавливать системные зависимости для компилируемого исходного кода и необходим для запуска некоторых основных компонентов в ROS:

sudo apt-get install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essential

Инициализируем rosdep:

sudo rosdep init
rosdep update

Убедимся, что ROS на Raspberry работает. Откроем новый терминал и запустим ядро ROS:

roscore

part_5_rpi_side_screen_1.png

Установка ROS на настольный компьютер

Установка ROS на настольный компьютер ничем не отличается от установки ROS на Raspberry. Вам по-прежнему нужно отталкиваться от версии Linux, которая установлена на вашем настольном компьютере.

В нашем случае компьютер работает под Ubuntu 20.04.1 LTS (Focal), поэтому на него мы так же устанавливаем ROS Noetic.

Создание рабочего пространства ROS

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

  • Что такое переменные среды ROS?
  • Как выглядит файловая система и какова структура пакета ROS?
  • Что такое ноды или узлы (nodes), топики или темы (topics), сервисы (sevices), сообщения (messages), издатели (publishers), подписчики (subscribers)?
  • Каким образом вышеописанные обьекты взаимодействуют друг с другом?

Создадим новое рабочее пространство ROS. Это место, где вы храните всё программное обеспечение робота или проекта: пакеты с нодами, исполняемые файлы, файлы конфигурации, файлы описаний и прочее. На самом деле рабочее пространство — просто папка в файловой системе Linux.

Мы назвали наше рабочее пространство ros. Пакеты с исходным кодом должны находиться в подкаталоге src.

mkdir -p ~/ros/src
cd ~/ros/

Рабочее пространство в настоящее время пусто, давайте соберём его с помощью catkin. Это удобный инструмент для компиляции исходного кода C++ или Python, создания исполняемых файлов, связывания пакетов, и этот инструмент регулярно используется при работе с ROS.

catkin_make

Взгляните в каталог рабочего пространства. Теперь там появились папки build и devel. В папке build хранятся исполняемые файлы, бинарные файлы и файлы сборки. Папка devel содержит множество сгенерированных файлов setup.*sh, которые используются для наложения рабочей области ROS поверх среды Linux.

Процесс разработки

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

Добиться актуализации данных и синхронизировать рабочие пространства на настольном компьютере и Raspberry можно с помощью git или различных утилит типа rsync.

Далее, если мы производим какие-нибудь изменения в рабочем пространсве ros, то подразумевается, что эти изменения выполнены как на Raspberry, так и на настольном компьютере. Наш проект будет храниться в клонированном виде в двух местах, но на разных машинах мы будем запускать разные части проекта.

Настойка локальной сети и сети ROS

Настроим общение по сети между настольным компьютером и Raspberry, а также обьединим ROS на обоих в компьютерах в сеть. В документации ROS есть подробная статья о настройке системы на нескольких машинах.

Узнаем IP-адреса Raspberry Pi и десктопного компьютера в локальной сети. В нашем случае компьютер получил сетевой адрес 192.168.88.24, а RPi — 192.168.88.82. IP-адрес можно узнать утилитами ip addr или ifconfig.

Настольный компьютер имеет имя robot-user, а Raspberry — имя robot. Имена можно посмотреть в файле /etc/hostname.

Отредактируем /etc/hosts на обеих машинах:

sudo nano /etc/hosts

Добавим в этот файл несколько строк:

192.168.88.24 robot-user
192.168.88.82 robot

В сети ROS может быть запущено только одно ядро roscore. Именно машина с запущенным ядром отвечает за работу всей системы. В сети ROS она называется master, а остальные машины становятся slave. Мы в качестве master выбираем настольный компьютер robot-user. Для всех ROS-компьютеров в сети нужно указать, какая именно машина является master.

Добавим новые сетевые переменные окружения ROS в автозапуск. Сперва на настольном компьютере:

echo "ROS_MASTER_URI=http://robot-user:11311" >> ~/.bashrc
echo "ROS_HOSTNAME=robot-user" >> ~/.bashrc
echo "ROS_IP=192.168.88.24" >> ~/.bashrc

А затем то же самое на Raspberry:

echo "ROS_MASTER_URI=http://robot-user:11311" >> ~/.bashrc
echo "ROS_HOSTNAME=robot" >> ~/.bashrc
echo "ROS_IP=192.168.88.82" >> ~/.bashrc

Перезагрузим обе машины и протестируем сеть ROS.

Запустим ядро на настольном компьютере:

roscore

part_5_desk_side_screen_1.png

Как мы видим из лога, ядро ROS запустилось на машине, которая имеет сетевое имя robot-user. А значит, мы уже можем увидеть ядро в сети. Проверим на Raspberry, появились ли какие-либо топики ROS:

rostopic list

У нас появились два системных топика ROS — значит, система на Raspberry увидела наш master.

part_5_rpi_side_screen_1.png

Описание робота

Пришло время дать нашему роботу имя. Мы решили назвать его abot — акроним от Amperka Bot.

Имя робота очень важно, так как оно является своего рода пространством имён (namespace) при работе с программным обеспечением.

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

Аналогично, программное обеспечение должно иметь полный доступ к роботу в любой момент времени. Для этого программе необходимо предоставить полное описание реального робота (Robot Descriprton).

Формат URDF

Для описания роботов существуют различные форматы. ROS использует формат URDF (Unified Robot Description Format), который является специализацией XML.

С помощью URDF можно описать каждую часть реального робота. Чем полнее описание робота, тем больше функций программного обеспечения можно будет использовать, например, для симуляции физического поведения. В описании все детали робота — сегменты (links), сочленения (joints) и датчики (sensors) — организованы в виде дерева. Описание URDF различается в зависимости от реализации, но есть несколько основных элементов, которые встречаются регулярно:

  • <link> описывает кинематические и динамические свойства жёсткой инерционной детали или узла робота (абсолютно твёрдого тела).
  • <visual> описывает визуальные свойства <link>. То есть, как выглядит деталь робота. Узел робота может быть условно описан геометрически как куб, цилиндр или сфера, или же задан полигональной 3D-моделью.
  • <collision> описывает упрощённую геометрию <link>. Данный элемент используется для задания хитбоксов и областей, которые нужны для физических расчетов во время симуляции.
  • <inertial> описывает инерционные свойства <link>. Элемент задаёт массу детали или узла робота, центр масс и тензор инерции в виде матрицы 3×3.
  • <joint> описывает шарнир, сочленение или соединение двух жёстких частей (двух элементов <link>). В этом элементе определяется тип соедининия двух деталей, их кинематика и динамика, пределы безопасного движения, физическое демпфирование и параметры трения.
  • <transmission> описывает взаимосвязь между <joint> и конкретным приводом. Например, один мотор робота может управлять несколькими сочленениями.

Описание робота с множеством частей и соединений может занимать тысячи строк и быть неудобным для чтения. Решением является язык макросов xacro. С помощью макросов xacro вы можете создавать более короткие и удобочитаемые XML-файлы.

Для описания нашего abot’a мы используем urdf и xacro. Просмотреть примеры описаний роботов можно в уроках URDF.

Это важно! URDF-описание — это неотъемлемая часть роботов, построенных на ROS. Так сделаны сотни роботов, и будьте уверены, каждый из них имеет своё описание.

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

Процесс создания файла URDF можно автоматизировать с помощью специального программного обеспечения, которое экспортирует 3D-модель робота из CAD-систем в URDF. А для этого вам понадобится 3D-модель робота в САПР (Системе автоматизированного проектирования).

3D-модель робота в САПР

В теории 3D-модель робота можно создать в любом 3D-редакторе, но лучше использовать твердотельное моделирование.

Мы выбрали редактор SolidWorks 2017 для платформы Windows, потому что в нём есть отличный плагин для экспорта проекта в формат URDF.

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

Всё наше шасси пришлось детализировать вручную. Вот почему так важны чертежи от производителя. Глядите, что у нас получилось:

part_6_cad_drawing_1

На данный момент наша модель состоит из четырёх сегментов:

  • abot_base
  • abot_left_wheel
  • abot_right_wheel
  • abot_caster_wheel

И трёх сочленений (joints):

  • left_wheel_to_base
  • right_wheel_to_base
  • caster_wheel_to_base

Для правильного экспорта нужно выполнить несколько действий.

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

ROS и URDF требуют правосторонних систем координат (Правило правой руки). Определите, где находится передняя, задняя и верхняя части вашего робота. Ось X должна указывать вперёд, ось Y — влево, а ось Z — вверх. По умолчанию стандартные виды SolidWorks и система координат повёрнуты на 90 градусов вокруг осей X и Z. Для удобства правильного размещения осей в SolidWorks можно разместить направляющие линии.

Каждый сегмент модели имеет свою систему координат:

  • CS_BASE
  • CS_RIGHT_WHEEL
  • CS_LEFT_WHEEL
  • CS_CASTER_WHEEL

Система координат CS_BASE условно расположена в центре нашего робота. CS_LEFT_WHEEL и CS_RIGHT_WHEEL находятся в центрах колёс. CS_CASTER_WHEEL находится в центре сферы всенаправленного опорного колеса.

part_6_cad_drawing_2

Добавьте оси вращения для подвижных соединений и сочленений (joints). У нас есть три подвижных соединения: два вращающихся на осях колеса и одно всенаправленное колесо, которое вращается во все стороны и поэтому не нуждается в оси. Для колёс мы поместили оси AXIS_LEFT_WHEEL и AXIS_RIGHT_WHEEL.

part_6_cad_drawing_3

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

Экспорт в URDF

Чтобы экспортировать модель, мы установили специальный плагин solidworks_urdf_exporter. Последовательность установки хорошо описана в документации на плагин.

После установки включите этот плагин в SolidWorks. Для этого перейдите в меню «Tools → Add-Ins» и установите флажок рядом с плагином SW2URDF. Чтобы начать экспорт, перейдите в раздел «Tools - Export as URDF» или в «Files - Export as URDF». Откроется меню экспорта, в котором необходимо указать все сегменты, используемые в модели.

Сначала мы указываем сегмент abot_base. Это базовый и родительский сегмент. В качестве базового сегмента для робота лучше выбрать что-то массивное — например, каркас робота.

Затем мы определяем три дочерних сегмента для abot_base и систему координат CS_BASE. Для задания геометрии этого сегмента мы выбираем все части 3D-модели, кроме боковых колёс и всенаправленного колеса. При экспорте выбранные детали преобразуются в mesh-модели формата STL, необходимые для визуализации.

part_6_cad_drawing_4

Следующий шаг — описать сегменты боковых колёс, которые являются сегментами-потомками abot_base. Мы установили имена соединений abot_left_wheel и abot_right_wheel. В качестве систем координат мы выбираем соответствующие CS_LEFT_WHEEL и CS_RIGHT_WHEEL. Оси вращения колес — AXIS_LEFT_WHEEL и AXIS_RIGHT_WHEEL. В качестве геометрии мы выбираем детали колёс нашей 3D-модели. Для обычного колеса тип соединения (joint) является continuous. Также задаём имена для сочленений колёс — left_wheel_to_base и right_wheel_to_base.

part_6_cad_drawing_5

Добавляем последний сегмент abot_caster_wheel и сочленение caster_wheel_to_base для всенаправленного колеса. Здесь тип соединения должен быть continuous, а система координат — CS_CASTER_WHEEL.

part_6_cad_drawing_6

Это важно! Для двухколёсного дифференциального привода последнее всенаправленное колесо никак не влияет на алгоритм задания скоростей движения робота. Мы не можем управлять этим колесом, задавать скорость вращения и контролировать его. То есть, в теории нет необходимости описывать его в модели и можно «слить» сегмент всенаправленного колеса с базовым сегментом всего робота. Но это не так: желательно описывать все подвижные части вашего робота. Если что-то в роботе двигается — значит, для этого нужно сочленение (joint), даже если вы не планируете его использовать. Особенно это важно, если вы планируете использовать симуляции. Например, мы можем не описывать всенаправленное колесо, тогда оно останется в описании робота твёрдой опорой и частью другого сегмента. В этом случае при симуляции движения робота программа не будет знать, что это колесо, и расценит его как твёрдый объект, который роботу приходится «тащить» по полу, преодолевая лишние силы трения, препятствующие движению. Таким образом, движение робота в симуляции и в реальной жизни будет кардинально отличаться.

Когда вы опишите все сегменты и сочленения, нажмите кнопку «Preview and Export…».

Проверьте параметры сочленений и нажмите кнопку «Next».

part_6_cad_drawing_7

Проверьте параметры сегментов.

part_6_cad_drawing_8

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

Завершите экспорт, нажав на кнопку «Export URDF and Meshes…». Укажите имя папки для экспорта. Мы назвали её abot. Экспортёр создаст в этой папке готовый пакет ROS со многими файлами, которые вам в действительности не нужны. Вам понадобятся файлы 3D-моделей формата *.STL из папки meshes и файл описания abot.urdf из папки urdf.

Отложим эти файлы на некоторое время в сторону.

Пакет robot_description

Вернёмся на Linux и перейдём в наше рабочее пространство ros настольном компьютере.

Cоздадим первый пакет проекта, который содержит описание робота. Традиционно этот пакет называют robot_description. Чтобы избежать путаницы с именем, мы назовем его abot_description. В директории ros/src в терминале вводим:

catkin_create_pkg abot_description tf rviz urdf xacro

С помощью этой команды вы можете создать пустой пакет ROS, а именно файлы CMakelists.txt и package.xml. В команде после имени пакета указываются пакеты зависимости. Для пакета abot_description мы устанавливаем пакеты зависимостей tf, urdf, xacro и rviz.

Для каждого пакета ROS не забывайте указывать необходимые пакеты зависимости, а также редактировать файлы CMakelists.txt и package.xml при добавлении новых функций. Подробнее о процессе создания пакета читайте в учебной статье.

Внутри пакета мы создаём четыре папки с именами urdf, meshes, rviz, and launch.

mkdir abot_description/urdf abot_description/meshes abot_description/rviz abot_description/launch

В папку abot_description/meshes нужно поместить 3D-файлы STL, сгенерированные ранее при экспорте нашей модели в URDF. В папку abot_description/urdf поместите сгенерированный файл abot.urdf.

Это важно! Внутри пакета ROS в папке c именем launch традиционно хранятся файлы запуска нод. Эти файлы имеют расширение .launch. С помощью launch-файлов можно запустить несколько нод ROS, используя всего один терминал. Кроме этого, через launch-файл можно задать различные параметры для нод или глобальные параметры для параметрического сервера ROS. Читайте подробнее в документации на roslaunch.

Приводим в порядок URDF-файл описания робота

Экспортёр генерирует описание URDF в одном большом файле. Это не всегда удобно. Откройте файл abot.urdf и посмотрите, как выглядит описание робота.

В будущем мы будем постоянно совершенствовать робота и расширять его описание, поэтому для удобства давайте разделим файл abot.urdf на несколько частей с помощью макросов xacro и исправим некоторые сгенерированные ошибки.

Мы решили разделить описание на несколько частей:

  • abot.xacro — основная информация о роботе и его базовых сегментах.
  • abot_left_wheel.xacro — описание сегмента левого колеса и его сочленения с базой робота.
  • abot_right_wheel.xacro — описание сегмента правого колеса и его сочленения с базой робота.
  • abot_caster_wheel.xacro — описание сегмента всенаправленного колеса и его сочленения с базой робота.
  • abot_materials.xacro — описание цветов для визуализации.

Давайте разбираться. Начнем с простого: сделаем цвета для визуализации. В папке urdf cоздадите файл abot_materials.xacro и заполните его несколькими элементами, которые описывают цвета:

<?xml version="1.0"?>
<robot
	xmlns:xacro="http://www.ros.org/wiki/xacro">
	<material name="Green">
		<color rgba="0.0 1.0 0.0 1.0"/>
	</material>
	<material name="Blue">
		<color rgba="0.0 0.0 1.0 1.0"/>
	</material>
	<material name="Red">
		<color rgba="1.0 0.0 0.0 1.0"/>
	</material>
	<material name="White">
		<color rgba="1.0 1.0 1.0 1.0"/>
	</material>
	<material name="Yellow">
		<color rgba="1.0 1.0 0.0 1.0"/>
	</material>
</robot>

Теперь создадим файл abot.xacro и заполним его информацией о сегменте abot_base.

Изменим путь до трёхмерных файлов с package://abot/meshes/abot_base.STL на package://abot_description/meshes/abot_base.STL.

Включите в abot.xacro файл с нашими цветами abot_materials.xacro и замените все экспортированные теги material на новые. Пусть сегмент abot_base визуализируется белым цветом.

Вот каким получилось содержание файла abot.xacro.

<?xml version="1.0" encoding="utf-8"?>
<robot name="abot"
	xmlns:xacro="http://www.ros.org/wiki/xacro">
	<!-- Matherials -->
	<xacro:include filename="$(find abot_description)/urdf/abot_matherials.xacro" />
	<!-- abot_base -->
	<link name="abot_base">
		<inertial>
			<origin xyz="-0.024498 1.0952E-13 0.022295" rpy="0 0 0"/>
			<mass value="0.27459"/>
			<inertia ixx="0.00032396" ixy="-1.1142E-12" ixz="-9.1302E-06" iyy="0.00030091" iyz="-3.3253E-10" izz="0.00056103"/>
		</inertial>
		<visual>
			<origin xyz="0 0 0" rpy="0 0 0" />
			<geometry>
				<mesh filename="package://abot_description/meshes/abot_base.STL" />
			</geometry>
			<material name="White" />
		</visual>
		<collision>
			<origin xyz="0 0 0" rpy="0 0 0" />
			<geometry>
				<mesh filename="package://abot_description/meshes/abot_base.STL" />
			</geometry>
		</collision>
	</link>
</robot>

Ещё одна фундаментальная деталь. Согласно конвенции, описание URDF для ROS должно иметь сегмент с именем base_link. Именно этот сегмент служит отправной точкой для дерева описания робота. Вы можете добавить этот сегмент в дерево описания с любой простой геометрией, например, со сферой радиусом 1 мм.

Мы добавляем в описание base_link и «прикрепляем» его к сегменту abot_base с помощью соединения fixed. Добавляем следующие строки в файл abot.xacro:

<!-- base_link -->
<link name="base_link">
	<visual>
		<origin xyz="0 0 0" rpy="0 0 0" />
		<geometry>
			<sphere radius="0.001" />
		</geometry>
	</visual>
</link>
<joint name="base_link_to_abot_base" type="fixed">
	<origin xyz="0 0 0" rpy="0 0 0" />
	<parent link="base_link" />
	<child link="abot_base" />
</joint>

Заполним файлы для правого (abot_right_wheel.xacro), левого (abot_left_wheel.xacro) и всенаправлленого колеса (abot_caster_wheel.xacro) подобным образом. Отредактируем все экспортированные данные и разделим их по файлам. Пусть все колёса будут зелёными.

Файл описания левого колеса:

<?xml version="1.0" encoding="utf-8"?>
<robot name="abot"
	xmlns:xacro="http://www.ros.org/wiki/xacro">
	<!-- left_wheel -->
	<link name="abot_left_wheel">
		<inertial>
			<origin xyz="1.9255E-10 0.00056576 -1.0414E-10" rpy="0 0 0"/>
			<mass value="0.050464"/>
			<inertia ixx="2.0701E-05" ixy="-3.8089E-14" ixz="1.3584E-15" iyy="3.5827E-05" iyz="2.1838E-15" izz="2.0701E-05"/>
		</inertial>
		<visual>
			<origin xyz="0 0 0" rpy="0 0 0" />
			<geometry>
				<mesh filename="package://abot_description/meshes/abot_left_wheel.STL" />
			</geometry>
			<material name="Green" />
		</visual>
		<collision>
			<origin xyz="0 0 0" rpy="0 0 0" />
			<geometry>
				<mesh filename="package://abot_description/meshes/abot_left_wheel.STL" />
			</geometry>
		</collision>
	</link>
	<joint name="left_wheel_to_base" type="continuous">
		<origin xyz="0 0.068 0.0145" rpy="0 0 0" />
		<parent link="abot_base" />
		<child link="abot_left_wheel" />
		<axis xyz="0 1 0" />
	</joint>
</robot>

Файл описания правого колеса:

<?xml version="1.0" encoding="utf-8"?>
<robot name="abot"
	xmlns:xacro="http://www.ros.org/wiki/xacro">
	<!-- right_wheel -->
	<link name="abot_right_wheel">
		<inertial>
			<origin xyz="1.9255E-10 -0.00056576 1.0414E-10" rpy="0 0 0"/>
			<mass value="0.050464"/>
			<inertia ixx="2.0701E-05" ixy="3.8089E-14" ixz="-1.3584E-15" iyy="3.5827E-05" iyz="2.1838E-15" izz="2.0701E-05"/>
		</inertial>
		<visual>
			<origin xyz="0 0 0" rpy="0 0 0" />
			<geometry>
				<mesh filename="package://abot_description/meshes/abot_right_wheel.STL" />
			</geometry>
			<material name="Green" />
		</visual>
		<collision>
			<origin xyz="0 0 0" rpy="0 0 0" />
			<geometry>
				<mesh filename="package://abot_description/meshes/abot_right_wheel.STL" />
			</geometry>
		</collision>
	</link>
	<joint name="right_wheel_to_base" type="continuous">
		<origin xyz="0 -0.068 0.0145" rpy="0 0 0" />
		<parent link="abot_base" />
		<child link="abot_right_wheel" />
		<axis xyz="0 1 0" />
	</joint>
</robot>

Файл описания всенаправленного колеса:

<?xml version="1.0" encoding="utf-8"?>
<robot name="abot"
	xmlns:xacro="http://www.ros.org/wiki/xacro">
	<!-- caster_wheel -->
	<link name="abot_caster_wheel">
		<inertial>
			<origin xyz="0 1.6073E-19 0" rpy="0 0 0"/>
			<mass value="0.011207"/>
			<inertia ixx="2.1965E-07" ixy="-1.5533E-55" ixz="-1.9776E-56" iyy="2.1965E-07" iyz="-2.2674E-40" izz="2.1965E-07"/>
		</inertial>
		<visual>
			<origin xyz="0 0 0" rpy="0 0 0" />
			<geometry>
				<mesh filename="package://abot_description/meshes/abot_caster_wheel.STL" />
			</geometry>
			<material name="Green" />
		</visual>
		<collision>
			<origin xyz="0 0 0" rpy="0 0 0" />
			<geometry>
				<mesh filename="package://abot_description/meshes/abot_caster_wheel.STL" />
			</geometry>
		</collision>
	</link>
	<joint name="caster_wheel_to_base" type="continuous">
		<origin xyz="-0.078 0 -0.011" rpy="0 0 0" />
		<parent link="abot_base" />
		<child link="abot_caster_wheel" />
		<axis xyz="0 1 0" />
	</joint>
</robot>

Включим все новые файлы описания колёс в конец главного файла abot.xacro.

<!-- Wheels -->
<xacro:include filename="$(find abot_description)/urdf/abot_left_wheel.xacro" />
<xacro:include filename="$(find abot_description)/urdf/abot_right_wheel.xacro" />
<xacro:include filename="$(find abot_description)/urdf/abot_caster_wheel.xacro" />

Визуализация URDF-модели

Давайте визуализируем нашего робота в ROS на настольном компьютере и посмотрим, что получится.

Для визуализации используем мощный инструмент rviz. Вы можете прочитать больше о rviz в документации на Wiki.

Если вы установили полную версию ROS (ros-desktop-full), то у вас уже есть все необходимые пакеты для визуализации. Однако нужно ещё установить дополнительный пакет joint-state-publisher-gui для ручного управления сочленениями. Для нашей ROS Noetic устанавливаем:

sudo apt-get install ros-noetic-joint-state-publisher-gui

Создадим новый файл запуска нод ROS. Подобные файлы имеют разрешение *.launch.

В нашем пакете abot_description в папке launch создаём файл display_model.launch и заполняем его следующими строками:

<launch>
	<!-- Args -->
	<arg name="gui" default="true" />
	<arg name="model" default="$(find abot_description)/urdf/abot.xacro" />
	<!-- Params -->
	<param name="use_gui" value="$(arg gui)" />
	<!-- Robot Description from URDF -->
	<param name="robot_description" command="$(find xacro)/xacro --inorder $(arg model)" />
	<node name="joint_state_publisher_gui" pkg="joint_state_publisher_gui" type="joint_state_publisher_gui" />
	<node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher" />
	<!-- Rviz -->
	<node name="rviz" pkg="rviz" type="rviz" required="false"/>
</launch>

Что делает этот файл? При запуске он загрузит необходимые для визуализации ноды ROS, а также загрузит URDF-описание нашего робота на сервер параметров ROS.

Перейдём в рабочее пространство ros и соберём его с нашим новым пакетом abot_description.

cd ~/ros
catkin_make

Загрузим переменные окружения нашего рабочего пространства и запустим display_model.launch.

source devel/setup.bash
roslaunch abot_description display_model.launch

Если всё сделано верно, появится окно rviz и окно joint_state_publisher_gui.

part_6_rviz_screen_1

В окне rviz в меню «Displays → Global Options» устанавливаем значение параметра Fixed Frame в base_link.

Нажимаем кнопку «Add» в левой нижней части экрана и добавляем два элемента визуализации: RobotModel и TF.

part_6_rviz_screen_2

На экране появится модель робота, созданная по нашему описанию URDF, и дерево транформаций.

part_6_rviz_screen_3

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

Например, мы сохраняем настройки rviz для отображения модели в виде файла abot_model.rviz в папке abot_description/rviz и добавляем соответствующий аргумент в файл запуска display_model.launch.

Теперь наш файл запуска display_model.launch выглядит таким образом:

<launch>
	<!-- Args -->
	<arg name="gui" default="true" />
	<arg name="rvizconfig" default="$(find abot_description)/rviz/abot_model.rviz" />
	<arg name="model" default="$(find abot_description)/urdf/abot.xacro" />
	<!-- Params -->
	<param name="use_gui" value="$(arg gui)" />
	<!-- Robot Description from URDF -->
	<param name="robot_description" command="$(find xacro)/xacro --inorder $(arg model)" />
	<node name="joint_state_publisher_gui" pkg="joint_state_publisher_gui" type="joint_state_publisher_gui" />
	<node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher" />
	<!-- Rviz -->
	<node name="rviz" pkg="rviz" type="rviz" args="-d $(arg rvizconfig)" required="false"/>
</launch>

А отображение модели стало намного более приятным и понятным:

part_6_rviz_screen_4

Можно отключить видимость 3D-моделей и проверить всё дерево элементов вашего описания, а также направления осей систем координат.

part_6_rviz_screen_5

Не забудьте, что у нас открыто и второе окно joint_state_publisher_gui.

part_6_rviz_screen_6

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

Footprint робота

Сейчас при визуализации наш робот находится в точке (0, 0, 0) на плоскости grid, которая выступает в роли пола, а фиксированный кадр Fixed Frame, в котором отображается наш робот, является глобальным (Global Frame). На самом деле робот не должен находиться в этой точке, ведь он стоит на своих колёсах, а не замурован где-то в полу.

Нам необходимо «поднять» робота над полом, а также указать его проекцию на пол — footprint. В соответствии с конвенцией ROS для этого используется сегмент base_footprint. Нам нужно ввести в описание робота этот новый сегмент и связать его с базовым сегментом base_link. Также нужно указать геометрические размеры проекции робота на пол и клиренс.

Добавим новые данные в основной файл описания робота abot.xacro. Добавим в начало файла параметр clearance — расстояние в метрах от пола до исходных точек сегментов base_link и abot_base. Поместим следующую строку в начало файла abot.xacro:

<xacro:property name="clearance" value="0.018" />

В качестве проекции робота на пол можно указать простую геометрическую фигуру. Например, наш робот условно круглый, и в качестве проекции мы можем указать цилиндр высотой в 1 мм и радиусом 100 мм, а его цвет на визуализации сделать синим.

Добавляем в abot.xacro новый сегмент и связываем его с имеющимися.

<!-- base_footprint -->
<link name="base_footprint">
	<visual>
		<origin xyz="0 0 0" rpy="0 0 0" />
		<geometry>
			<cylinder length="0.001" radius="0.010" />
		</geometry>
		<material name="Blue" />
	</visual>
</link>
<joint name="base_footprint_to_base_link" type="fixed">
	<origin xyz="0 0 ${clearance}" rpy="0 0 0" />
	<parent link="base_footprint" />
	<child link="base_link" />
</joint>

Давайте снова запустим визуализацию rviz, но в качестве фиксированного кадра визуализации (Fixed Frame) укажем base_footprint.

Теперь наш робот стоит колёсами на земле:

part_6_rviz_screen_7

Сейчас дерево сегментов и сочлненений нашего робота выглядит так:

part_6_rqt_screen_1

Raspberry Pi HAT и крепление электроники

Следующим шагом будет подключение двигателей и энкодеров к роботу. Одноплатник Raspberry Pi в этом плане — отличный выбор, так как даёт прямой доступ к контактам GPIO.

Однако не всегда удобно подключать электронику к штыревой вилке GPIO напрямую. Лучше использовать специальные платы расширения и адаптеры.

Для нашего робота мы взяли универсальный хаб для Raspberry Pi — Troyka HAT.

part_7_prod_electronics_1.jpg

Этот адаптер легко вставляется в гребёнку пинов на малинке. Установим Troyka HAT на Raspberry Pi:

part_7_irl_electronics_1.jpg

Troyka HAT также имеет дополнительный контроллер — расширитель GPIO-портов, который обеспечивает восемь дополнительных портов ввода-вывода с аппаратной поддержкой 12-битного АЦП и 16-битной ШИМ. Позже мы обязательно воспользуемся этой особенностью.

Так выглядит распиновка платы Troyka HAT:

part_7_schemes_1.png

Пришло время прикрепить наш бортовой компьютер к шасси робота. Можно не заморачиваться и прикрепить Raspberry хоть на двухсторонний скотч. Однако, если вы хотите сделать качественного робота, всё следует делать по уму.

Для крепления электроники мы спроектировали и напечатали на 3D-принтере новую деталь для робота в виде панели или диска. На этой детали мы сделали отверстия для крепежа Raspberry и прочих электронных компонентов. Деталь напечатали на Prusa i3 MK3S из серого PLA-пластика eSUN.

Вот так выглядит панель:

part_7_irl_electronics_2.jpg

Крепим плату Raspberry Pi на напечатанную панель винтами M2,5×10, гайками M2,5 и контрим гроверными шайбами M2,5.

part_7_irl_electronics_3.jpg

Бортовое питание

Для бортового питания нашего робота мы будем использовать аккумуляторы Li-Ion. Энергии нужно много: одна только Raspberry Pi 4 нуждается в 3 А тока, а ведь помимо неё на борту робота будут и другие потребители. Так что нужны аккумуляторы с большой токоотдачей и ёмкостью. Можно использовать аккумуляторы Li-Pol, которые обычно применяются в радиоуправляемых моделях и способны отдавать токи величиной в 2С и более. Однако слишком уж большие токи нам ни к чему, и мы решили взять именно аккумуляторы Li-Ion в формате 18650 на 3,6 В и 2600 мА·ч.

Мы выбрали аккумуляторы Li-Ion 18650 Ansmann 3.6 В 2600 мА·ч.

part_7_irl_electronics_4.jpg

Один такой аккумулятор — это одна литий-ионная «банка» 3,6 В с максимальной отдачей тока в 5 А. Всего мы будем использовать четыре аккумулятора, соединенных попарно паралеленно. В сумме это даст нам батарею 7,2 В и 5200 мА·ч. Это не так уж и много для мобильного робота, но на первое время нам хватит. Ещё один существенный плюс этих аккумуляторов — наличие схемы защиты от глубокого разряда и короткого замыкания.

7,2 В с батареи мы можем подать на DC-разъём платы Troyka HAT и на её понижающий DC-DC преобразователь. Этим же напряжением 7,2 В мы можем управлять двумя 6-вольтовыми DC-моторами в шасси.

Аккумуляторы поместим в два батарейных отсека 2×18650.

part_7_irl_electronics_5.jpg

Батарейные отсеки крепим на панели робота винтами М2×6, гайками М2 и контрим гроверными шайбами М2.

part_7_irl_electronics_6.jpg

Примерим собранную панель на шасси:

part_7_irl_electronics_7.jpg

Актуализация модели робота

С добавлением новой напечатанной детали наш робот немного изменился внешне. А значит, изменилась его 3D-модель. И мы должны отредактировать его URDF-описание.

Это важно! Всегда регистрируйте любые изменения реального робота как в САПР-документации, так и в URDF-описании. Это действие не является обязательным, но оно прививает привычку контролировать документацию и поможет вам быстрее находить ошибки в программах и, что ещё важнее, их причины.

Обновляем 3D-модель:

part_7_cad_1.png

Также обновляем URDF-описание робота. Делаем всё через экспортёр, как и в прошлый раз — не будем заново описывать процесс экспорта. В этот раз у нас изменился только сегмент abot_base: его <visual>-составляющая, STL-файл, а также инерционные свойства.

part_7_rviz_1.png

Низкоуровневые драйверы

Давайте начнём писать драйверы для железа нашего робота.

Пакет abot_driver

Создадим в рабочем пространстве ros новый ROS-пакет, который будет отвечать за драйверы.

Назовём пакет abot_driver.

Не забываем оформлять файлы CMakelists.txt и package.xml для каждого нового пакета. В качестве пакетов зависимостей устанавливаем:

Внутри пакета создаём папку с именем src, в которой хранятся исходные файлы программ согласно конвенции ROS.

Это важно! Ноды ROS, которые мы напишем для этого пакета, будут запускаться только на Raspberry Pi, а не на настольном компьютере.

Библиотека WiringPi

Чтобы использовать GPIO-контакты Raspberry Pi, вам нужна библиотека. Существует множество библиотек, которые различаются по языку и по глубине использования функций — например, на C, С++, C#, Python, JavaScript, Perl, Java и Ruby. Вы можете просмотреть полный список существующих библиотек, который мы нашли. Мы будем использовать библиотеку для C++ WiringPi, потому что писать свои ноды ROS мы будем как раз на C++.

WiringPi — это несложная и популярная библиотека. Однако она уже долгое время не поддерживается, потому что её разработка была прекращена. В ней и сейчас есть нереализованные функции и программные ошибки. Последняя официальная версия библиотеки — 2.52 для Raspberry Pi 4B, и она собрана для архитектуры armhf, а у нас архитектура arm64.

На просторах GitHub мы нашли хороший форк библиотеки WiringPi. Мы возьмём именно эту версию и cоберем её под нашу версию Linux.

В терминале на Raspberry Pi вводим:

git clone https://github.com/WiringPi/WiringPi.git
cd WiringPi
./build

Убедимся, что библиотека собралась и установилась правильно:

gpio -v

part_8_rpi_side_screen_1.png

Чтобы просмотреть назначение всех контактов Raspberry Pi 4B, а также их GPIO/Broadcom/WiringPi-маппинг, используйте команду:

gpio readall

part_8_rpi_side_screen_2.png

Драйвер энкодеров

Наш первый драйвер — для считывания показаний энкодеров моторов.

Схема подключения энкодеров

Мы используем квадратурные энкодеры, каждый из которых имеет два канала A/B и подключается по двум проводам. Энкодер генерирует простые логические сигналы — HIGH или LOW. Если вал двигателя вращается быстро, то и изменения в логических сигналах также генерируются быстро. Поэтому, чтобы случайно не пропустить какие-либо изменения, мы будем считывать сигналы с энкодеров, используя аппаратные прерывания. Raspberry Pi допускает использование прерываний на всех выводах, и вы можете подключить энкодер к любым неиспользуемым пинам.

Мы подключили энкодер левого двигателя к выводу Broadcom BCM 17 (пин 0 для WiringPi) и BCM 27 (пин 2 для WiringPi). Энкодер правого двигателя — к выводам BCM 24 (пин 5 для WiringPi) и BCM 25 (пин 6 для WiringPi).

Энкодеры питаются напряжением 5 В, которое можно взять с колодок Troyka HAT.

part_8_schemes_1.png

Класс Encoder

Напишем класс С++ для декодирования квадратурного энкодера на RPi с использованием прерываний. Назовём его EncoderWiringPi.

Чтобы использовать прерывания, мы создали две глобальные Сallback-функции encoderISR1 и encoderISR2. Необработанные значения тиков энкодера содержатся в переменных encoder_position_1 и encoder_position_2 типа long.

Задайте значение PPR (импульсов на оборот) ваших энкодеров в коде. Для наших энкодеров значение PULSES_PER_REVOLUTION равно 1920.

Создаём заголовочный файл С++ encoder_wiring_pi.h в папке abot_driver/src.

#ifndef ENCODER_WIRING_PI_H_
#define ENCODER_WIRING_PI_H_

#include <ros/ros.h>
#include <wiringPi.h>

constexpr uint8_t ENCODER_1_PIN_A = 17;  // Wiring pi 0 = BCM 17
constexpr uint8_t ENCODER_1_PIN_B = 27;  // Wiring pi 2 = BCM 27
constexpr uint8_t ENCODER_2_PIN_A = 24;  // Wiring pi 5 = BCM 24
constexpr uint8_t ENCODER_2_PIN_B = 25;  // Wiring pi 6 = BCM 25

constexpr uint16_t PULSES_PER_REVOLUTION = 1920;

namespace EncoderWiringPiISR {

    volatile long encoder_position_1;
    volatile long  encoder_position_2;
    volatile uint8_t encoder_state_1;
    volatile uint8_t encoder_state_2;

    void encoderISR(const int pin_A, const int pin_B, volatile long &encoder_position, volatile uint8_t &encoder_state) {
        uint8_t val_A = digitalRead(pin_A);
        uint8_t val_B = digitalRead(pin_B);
        uint8_t s = encoder_state & 3;
        if (val_A) s |= 4;
        if (val_B) s |= 8; 
        encoder_state = (s >> 2);
        if (s == 1 || s == 7 || s == 8 || s == 14)
            encoder_position++;
        else if (s == 2 || s == 4 || s == 11 || s == 13)
            encoder_position--;
        else if (s == 3 || s == 12)
            encoder_position += 2;
        else if (s == 6 || s == 9)
            encoder_position -= 2;
    }

    void encoderISR1(void) {
        encoderISR(ENCODER_1_PIN_A, ENCODER_1_PIN_B,  encoder_position_1, encoder_state_1);
    }

    void encoderISR2(void) {
        encoderISR(ENCODER_2_PIN_A, ENCODER_2_PIN_B,  encoder_position_2, encoder_state_2);
    }
}

class EncoderWiringPi {
public:
    EncoderWiringPi(const int &pin_A, const int &pin_B, void (*isrFunction)(void), volatile long* encoder_position);
    double getAngle();
private:
    int _pin_A;
    int _pin_B;
    volatile long* _encoder_position;
    double _initial_angle;
    double ticks2Angle(long position);
};

EncoderWiringPi::EncoderWiringPi(const int &pin_A, const int &pin_B, void (*isrFunction)(void), volatile long* encoder_position) {
    _encoder_position = encoder_position;

    if (wiringPiSetupSys() < 0) {
        throw std::runtime_error("Encoder wiringPi error: GPIO setup error");
    }

    ROS_INFO("Encoder wiringPi: GPIO setup");
    _pin_A = pin_A;
    _pin_B = pin_B;
    pinMode(_pin_A, INPUT);
    pinMode(_pin_B, INPUT);
    pullUpDnControl(_pin_A, PUD_UP);
    pullUpDnControl(_pin_B, PUD_UP);

    if (wiringPiISR(_pin_A, INT_EDGE_BOTH, isrFunction) < 0) {
        throw std::runtime_error("Encoder wiringPi error: ISR pinA error");
    }

    if (wiringPiISR(_pin_B, INT_EDGE_BOTH, isrFunction) < 0) {
        throw std::runtime_error("Encoder wiringPi error: ISR pinB error");
    }

    _initial_angle = ticks2Angle(*_encoder_position);
    ROS_INFO("Encoder wiringPi: ISR setup");
}

double EncoderWiringPi::getAngle() {
    double current_angle = ticks2Angle(*_encoder_position);
    return current_angle - _initial_angle;
}

double EncoderWiringPi::ticks2Angle(long position) {
	return position * ((double)2 * M_PI / PULSES_PER_REVOLUTION / 2);
}

#endif // ENCODER_WIRING_PI_H_

Также нам понадобится проверка на «переполнение» переменных encoder_position_1 и encoder_position_2, но об этом позже.

Нода энкодеров

Теперь напишем ROS-ноду для наших энкодеров. Назовем её encoders.

Как будет работать эта нода? Класс Encoder, который мы описали выше, считывает сигналы с энкодеров через прерывания, накапливает их, а затем конвертирует в углы поворота колёс (в радианах). Эти углы затем используются для расчёта одометрии робота и для определения текущей скорости вращения колёс.

Также мы создадим класс EncodersPair, который использует углы поворота колёс для расчёта:

  • Пройденного каждым колесом растояния (_left_wheel_angle, _right_wheel_angle). В нашем случае пройденное расстояние хранится в виде количества оборотов колеса в радианах.
  • Текущей скорости вращения колеса (_letf_wheel_velocity, _right_wheel_velocity) в радианах в секунду.

Расчёты производятся раз в 10 мс по ROS-таймеру _encoders_timer — то есть, с частотой 100 герц.

Пройденные колёсами расстояния помещаются в сообщения типа std_msgs/Float64 и публикуются в топики /abot/left_wheel/angle и /abot/right_wheel/angle.

Теукщие скорости вращения колёс также помещаются в сообщения типа std_msgs/Float64 и публикуются в топики /abot/left_wheel/current_velocity и /abot/right_wheel/current_velocity.

Создадим файл encoders.cpp в папке abot_driver/src:

#include "encoder_wiring_pi.h"
#include <std_msgs/Float64.h>
#include <chrono>

typedef boost::chrono::steady_clock time_source;

class EncodersPair {
public:
    EncodersPair(double update_rate);

private:
    ros::NodeHandle _node;

    ros::Publisher _left_wheel_angle_pub;
    ros::Publisher _right_wheel_angle_pub;
    ros::Publisher _left_wheel_velocity_pub;
    ros::Publisher _right_wheel_velocity_pub;

    ros::Timer _encoders_timer;

    std_msgs::Float64 _left_wheel_angle_msg;
    std_msgs::Float64 _right_wheel_angle_msg;
    std_msgs::Float64 _left_wheel_velocity_msg;
    std_msgs::Float64 _right_wheel_velocity_msg;

    EncoderWiringPi _encoder_left;
    EncoderWiringPi _encoder_right;

    double _left_wheel_angle;
    double _right_wheel_angle;
    double _left_wheel_velocity;
    double _right_wheel_velocity;
    double _left_wheel_position;
    double _right_wheel_position;

    time_source::time_point _last_time;

    void encodersCallback(const ros::TimerEvent& event);
};

EncodersPair::EncodersPair(double update_rate) :
    _encoder_left(ENCODER_1_PIN_A, ENCODER_1_PIN_B, &EncoderWiringPiISR::encoderISR1, &EncoderWiringPiISR::encoder_position_1),
    _encoder_right(ENCODER_2_PIN_A, ENCODER_2_PIN_B, &EncoderWiringPiISR::encoderISR2, &EncoderWiringPiISR::encoder_position_2) {
    _left_wheel_angle_pub = _node.advertise<std_msgs::Float64>("/abot/left_wheel/angle", 1);
    _right_wheel_angle_pub = _node.advertise<std_msgs::Float64>("/abot/right_wheel/angle", 1);
    _left_wheel_velocity_pub = _node.advertise<std_msgs::Float64>("/abot/left_wheel/current_velocity", 1);
    _right_wheel_velocity_pub = _node.advertise<std_msgs::Float64>("/abot/right_wheel/current_velocity", 1);
   
    _encoders_timer = _node.createTimer(ros::Duration(update_rate), &EncodersPair::encodersCallback, this);
}

void EncodersPair::encodersCallback(const ros::TimerEvent& event) {
    time_source::time_point this_time = time_source::now();
    boost::chrono::duration<double> elapsed_duration = this_time - _last_time;
    ros::Duration elapsed(elapsed_duration.count());
    _last_time = this_time;

    _left_wheel_angle = -1 * _encoder_left.getAngle();
    _right_wheel_angle = 1 * _encoder_right.getAngle();

    _left_wheel_angle_msg.data = _left_wheel_angle;
    _right_wheel_angle_msg.data = _right_wheel_angle;

    _left_wheel_angle_pub.publish(_left_wheel_angle_msg);
    _right_wheel_angle_pub.publish(_right_wheel_angle_msg);

    double delta_left_wheel = _left_wheel_angle - _left_wheel_position;
    double delta_right_wheel = _right_wheel_angle - _right_wheel_position;

    _left_wheel_position += delta_left_wheel;
    _left_wheel_velocity = delta_left_wheel / elapsed.toSec();

    _right_wheel_position += delta_right_wheel;
    _right_wheel_velocity = delta_right_wheel / elapsed.toSec();
 
    _left_wheel_velocity_msg.data = _left_wheel_velocity;
    _right_wheel_velocity_msg.data = _right_wheel_velocity;

    _left_wheel_velocity_pub.publish(_left_wheel_velocity_msg);
    _right_wheel_velocity_pub.publish(_right_wheel_velocity_msg);
}

int main(int argc, char** argv) {
    ros::init(argc, argv, "encoders");
    EncodersPair encoders_pair(0.01);
    ros::spin();
    return 0;
}

Добавим новый исполняемый файл в правило сборки CMakelists.txt пакета abot_driver:

add_executable(encoders src/encoders.cpp)
target_link_libraries(encoders ${catkin_LIBRARIES} -lwiringPi -lpthread -lcrypt -lm -lrt)

Соберём пакет abot_driver с новой нодой:

cd ~/ros
catkin_make

Тест энкодеров

Проверим, как работает новая нода encoders.

Если вдруг вы выключили ядро (roscore) на настольном компьютере, включите его заново.

В рабочем пространстве на RPi запустим новую ноду вручную под пользователем root:

cd ~/ros
su root
source devel/setup.bash
rosrun abot_driver encoders

part_8_rpi_side_screen_3.png

На настольном компьютере проверим, появились ли новые топики /abot/left_wheel/angle, /abot/right_wheel/angle, /abot/left_wheel/current_velocity и /abot/right_wheel/current_velocity, созданные нодой encoders.

rostopic list

part_8_desk_side_screen_1.png

Посмотрим командой rostopic echo, какие сообщения приходят в эти топики. Например, посмотрим на пройденный путь и скорость вращения правого колеса:

rostopic echo /abot/right_wheel/angle
rostopic echo /abot/right_wheel/current_velocity

Проверим работу энкодеров. Установим робота на какую-нибудь подставку, а затем будем руками поворачивать колесо и при этом наблюдать за изменениями угла поворота и скорости вращения в мониторе топика.

Например, повернув правое колесо примерно на один оборот, соответствующее значение угла поворота в топике /abot/right_wheel/angle должно измениться с 0 до примерно 2 * PI.

Ту же операцию проделаем и с левым колесом.

Драйвер моторов

Наш второй драйвер — для управления двумя DC-моторами, установленными на шасси.

Подключение моторов

Моторы нужно подключить к Raspberry Pi. Но мы не можем напрямую подключить двигатели постоянно тока к плате RPi и управлять ими. Нам нужен специльный модуль, который будет управлять моторами.

Наши DC-моторы потребляют мало тока и не нуждаются в большом напряжении, поэтому в качестве платы управления мы можем использовать небольшой H-мост. Мы взяли двухканальный H-мост в формате Troyka-модуля, который разработан для управления двумя DC-моторами с максимальным током до 1,2 А на канал.

part_8_prod_electronics_1.jpg

Мы также используем адаптер Troyka Pad 1×2 для более удобного подключения Troyka-модулей и крепления двухканального H-моста к нашей панели электроники. На панели мы зарнее предусмотрели монтажные отверстия для двухъюнитового Troyka Pad.

Закрепляем модуль на панели:

part_8_irl_electronics_1.jpg

Два контакта D и E управляют одним каналом двигателя. Вывод E (Enable) принимает ШИМ-сигнал, который отвечает за скорость вращения двигателя. Вывод D (Направление) принимает логический (HIGH или LOW) сигнал для задания направления вращения. Всего для управления двумя DC-моторами задействованы четыре контакта.

Схема подключения моторов

Raspberry Pi 4B может генерировать аппаратный ШИМ-сигнал только на двух каналах PWM0 и PWM1, которыми мы и воспользуемся. Помимо аппаратного ШИМ RPi может генерировать ещё программный ШИМ-сигнал на любом из своих выводов, но такой сигнал будет потреблять значительную часть вычислительной мощности. Канал PWM0 может быть назначен на вывод Broadcom BCM 12 (пин 26 для WiringPi) или BCM 18 (пин 1 для WiringPi). Канал PWM1 может быть назначен на вывод Broadcom BCM 13 (пин 23 для WiringPi) или BCM 19 (пин 24 для WiringPi). Логические контакты для управления направлением вращения моторов можно подлючить к любым пинам Raspberry.

Мы подключили левый двигатель к WiringPi-контактам 7 и 1, а правый двигатель — к WiringPi-контактам 12 и 13:

part_8_schemes_2.png

Класс DCMotor

Напишем простой класс С++ для управления двигателем постоянного тока через H-мост на RPi под ROS.

Вал двигателя может вращаться по часовой стрелке — cw (clockwise), против часовой стрелки — ccw (counter clockwise) или остановиться — stop. Разрешение аппаратного ШИМ на Raspberry составляет 10 бит (максимальное значение — 1023).

Создим заголовочный файл C++ dc_motor_wiring_pi.h и поместим его в папку abot_driver/src.

#ifndef DC_MOTOR_WIRING_PI_H_
#define DC_MOTOR_WIRING_PI_H_

#include <ros/ros.h>
#include <wiringPi.h>

constexpr uint16_t RPI_MAX_PWM_VALUE = 1023;

class DCMotorWiringPi {
public:
    DCMotorWiringPi(int8_t direction_pin, int8_t enable_pin);
    void cw(uint16_t val);
    void ccw(uint16_t val);
    void stop();
private:
    int8_t _direction_pin;
    int8_t _enable_pin;
    uint16_t protectOutput(uint16_t val);
};

DCMotorWiringPi::DCMotorWiringPi(int8_t direction_pin, int8_t enable_pin) {
    _direction_pin = direction_pin;
    _enable_pin = enable_pin;
    if (wiringPiSetupGpio() < 0) {
        throw std::runtime_error("DCMotor wiringPi error: GPIO setup error");
    }
    ROS_INFO("DCMotor wiringPi: GPIO setup");
    pinMode(_direction_pin, OUTPUT);
    pinMode(_enable_pin, PWM_OUTPUT);
    stop();
    ROS_INFO("DCMotor wiringPi: Motor setup");
}

void DCMotorWiringPi::stop() {
    pwmWrite(_enable_pin, 0);
    digitalWrite(_direction_pin, 0);
}

void DCMotorWiringPi::cw(uint16_t val) {
    pwmWrite(_enable_pin, protectOutput(val));
    digitalWrite(_direction_pin, 1);
}

void DCMotorWiringPi::ccw(uint16_t val) {
    pwmWrite(_enable_pin, protectOutput(val));
    digitalWrite(_direction_pin, 0);
}

uint16_t DCMotorWiringPi::protectOutput(uint16_t val) {
    return val > RPI_MAX_PWM_VALUE ? RPI_MAX_PWM_VALUE : val;
}

#endif // DC_MOTOR_WIRING_PI_H_

PID-контроллер

Для управления скоростью вращения колёс мы будем использовать PID-контроллер. Каждому колесу будет присвоен свой PID-контроллер, задача которого — поддержание необходимой скорости вращения.

Реализацию программы PID-контроллера писать самим не обязательно, в ROS уже есть готовый пакет — pid.

Установим ROS-пакет pid на Raspberry:

sudo apt-get install ros-noetic-pid

Принцип действия PID-контроллера для колеса нашего робота:

  • На вход PID-контроллера (Input или State) поступает текущая скорость вращения колеса (в радианах в секунду), которая считается нодой энкодеров и публикуюется в топики /abot/right_wheel/current_velocity и /abot/left_wheel/current_velocity.
  • Целью PID-контроллера (Setpoint) является поддержание требуемой скорости вращения колеса (в радианах в секунду). Нужную нам скорость вращения мы будем публиковать в топики /abot/right_wheel/target_velocity и /abot/left_wheel/target_velocity.
  • На выходе PID-контроллера (Output или Сontrol effort) будет величина ШИМ, которую нужно подать на H-мост для коррекции скорости вращения. Значения ШИМ будем публиковать в топики /abot/right_wheel/pwm и /abot/left_wheel/pwm.

Настроим два PID-контроллера для левого и правого колеса.

В пакете abot_driver создадим папку launch. В этой папке создадим новый файл abot_pid.launch для запуска двух нод controller из пакета pid.

<launch>
    <node name="controller" pkg="pid" type="controller" ns="/abot/left_wheel" output="screen" >
        <param name="node_name" value="left_wheel_pid" />
        <param name="Kp" value="1.0" />
        <param name="Ki" value="0.0" />
        <param name="Kd" value="0.0" />
        <param name="upper_limit" value="10.23" />
        <param name="lower_limit" value="-10.23" />
        <param name="windup_limit" value="10.23" />
        <param name="max_loop_frequency" value="100.0" />
        <param name="min_loop_frequency" value="100.0" />
        <remap from="/abot/left_wheel/setpoint" to="/abot/left_wheel/target_velocity" />
        <remap from="/abot/left_wheel/state" to="/abot/left_wheel/current_velocity" />
        <remap from="/abot/left_wheel/control_effort" to="/abot/left_wheel/pwm" />
    </node>
    <node name="controller" pkg="pid" type="controller" ns="/abot/right_wheel" output="screen" >
        <param name="node_name" value="right_wheel_pid" />
        <param name="Kp" value="1.0" />
        <param name="Ki" value="0.0" />
        <param name="Kd" value="0.0" />
        <param name="upper_limit" value="10.23" />
        <param name="lower_limit" value="-10.23" />
        <param name="windup_limit" value="10.23" />
        <param name="max_loop_frequency" value="100.0" />
        <param name="min_loop_frequency" value="100.0" />
        <remap from="/abot/right_wheel/setpoint" to="/abot/right_wheel/target_velocity" />
        <remap from="/abot/right_wheel/state" to="/abot/right_wheel/current_velocity" />
        <remap from="/abot/right_wheel/control_effort" to="/abot/right_wheel/pwm" />
    </node>
</launch>

Этим файлом мы запустим две ноды PID-контроллера, которые подписываются на топики /abot/right_wheel/target_velocity, /abot/left_wheel/target_velocity, /abot/right_wheel/current_velocity и /abot/left_wheel/current_velocity.

Оба PID-контроллера работают с частотой 100 герц (за это отвечают параметры min_loop_frequency и max_loop_frequency). Для корректировки скорости наши PID-контроллеры публикуют значения ШИМ в топики /abot/right_wheel/pwm и /abot/left_wheel/pwm.

Это важно! Значения ШИМ находятся в диапазоне [-10.23 , 10.23]. Мы решили отправлять их именно в таком диапазоне, а не в диапазоне [-1023 , 1023]. Это обусловлено особенностями подбора коэффициентов контроллера Kp, Ki и Kd в ROS-пакете pid. По нашему опыту, в ROS не очень часто можно встретить целочисленные значения. Большинство ROS-нод обмениваются сообщениями со значениями типа floating-point (с плавающей запятой) и в диапазоне [-1.0 , 1.0].

На этом этапе мы задаём стандартные значения коэффициентов Kp, Ki, Kd. Настраивать коэффициенты будем чуть позже.

Нода моторов

Теперь мы можем написать окончательную ROS-ноду для моторов робота. Назовем её dc_motors.

Создадим файл dc_motors.cpp в папке about_driver/src.

Как будет работать нода моторов? Она подписывается на два топика ROS — /abot/right_wheel/pwm и /abot/left_wheel/pwm. Через эти топики нода получает сообщения типа std_msgs/Float64, которые содержат значения ШИМ-сигналов коррекции скоростей в диапазоне [-10.23 , 10.23]. Далее эти значения умножаются на 100 и отправляются на H-мосты через объекты класса DCMotorWiringPi.

#include "dc_motor_wiring_pi.h"
#include <std_msgs/Float64.h>

constexpr uint8_t MOTOR_1_PIN_D = 4;        // Wiring pi 7 = BCM 4
constexpr uint8_t MOTOR_1_PIN_E = 18;       // Wiring pi 1 = BCM 18
constexpr uint8_t MOTOR_2_PIN_D = 12;       // Wiring pi 26 = BCM 12
constexpr uint8_t MOTOR_2_PIN_E = 13;       // Wiring pi 23 = BCM 13

DCMotorWiringPi left_dc_motor(MOTOR_1_PIN_D, MOTOR_1_PIN_E);
DCMotorWiringPi right_dc_motor(MOTOR_2_PIN_D, MOTOR_2_PIN_E);

void leftMotorCallback(const std_msgs::Float64& msg) {
    int16_t pwm = msg.data * 100;
    if (pwm > 0) {
        left_dc_motor.ccw(abs(pwm));
    } else if (pwm < 0) {
        left_dc_motor.cw(abs(pwm));
    } else if (pwm == 0) {
        left_dc_motor.stop();
    }
}

void rightMotorCallback(const std_msgs::Float64& msg) {
    int16_t pwm = msg.data * 100;
    if (pwm > 0) {
        right_dc_motor.ccw(abs(pwm));
    } else if (pwm < 0) {
        right_dc_motor.cw(abs(pwm));
    } else if (pwm == 0) {
        right_dc_motor.stop();
    }
}

int main(int argc, char **argv) {
    ros::init(argc, argv, "dc_motors");
    ros::NodeHandle node;
    ros::Subscriber left_motor_target_vel_sub = node.subscribe("/abot/left_wheel/pwm", 1, &leftMotorCallback);
    ros::Subscriber right_motor_target_vel_sub = node.subscribe("/abot/right_wheel/pwm", 1, &rightMotorCallback);
    ros::spin();
    return 0;
}

Добавим новый исполняемый файл в правило сборки CMakelists.txt в нашем пакете abot_driver.

add_executable(dc_motors src/dc_motors.cpp)
target_link_libraries(dc_motors ${catkin_LIBRARIES} -lwiringPi -lpthread -lcrypt -lm -lrt)

Соберём пакет abot_driver с новой нодой:

cd ~/ros
catkin_make

Запуск драйверов

Отлично! Наши драйверы готовы.

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

В пакете abot_driver в папке launch создадим файл запуска abot_drivers.launch. В нём запустим созданные нами ноды, а также включим файл запуска PID-контроллеров:

<launch>
    <node name="encoders" pkg="abot_driver" type="encoders" output="screen" />
    <node name="dc_motors" pkg="abot_driver" type="dc_motors" output="screen" />
    <include file="$(find abot_driver)/launch/abot_pid.launch" />
</launch>

Теперь мы можем одновременно запускать все драйверы одной командой из-под root:

su root
source devel/setup.bash
roslaunch abot_driver abot_drivers.launch

part_8_rpi_side_screen_5.png

part_8_rpi_side_screen_6.png

На настольном компьютере проверяем, что появились топики от обоих драйверов и от PID-контроллеров:

rostopic list

part_8_desk_side_screen_2.png

Тест моторов и настройка параметров PID-контроллера

Протестируем, как работают наши моторы, и настроим коэффициенты PID-контроллеров.

На Raspberry в первом терминале запустим все наши драйверы под пользователем root:

cd ~/ros
su root
source devel/setup.bash
roslaunch abot_driver abot_drivers.launch

Чтобы публиковать значения в топики вручную через терминалы, используем ROS-утилиту rqt.

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

На настольном компьютере в новом терминале вводим:

rqt

Перед нами появится окно rqt.

Сперва откроем плагин для публикации сообщений в топик. Нажмём «Plugins → Topics → Message Publisher». В появившемся окне добавим два топика задания скоростей — /abot/left_wheel/target_velocity и /abot/right_wheel/target_velocity. В эти топики будем публиковать сообщения типа std_msgs/Float64. В столбце rate установим частоту публикации сообщений 100 герц. Для запуска процесса публикации нужно установить галочку слева от имени топика.

part_8_desk_side_screen_3.png

Теперь откроем плагин для просмотра сообщений в топике. Нажмём «Plugins → Visualisation → Plot». Данный плагин отслеживает значения в указанных топиках и строит график изменения этих значений в реальном времени. Например, для отслеживания скоростей левого колеса добавим два топика — /abot/left_wheel/target_velocity и /abot/left_wheel/current_velocity.

part_8_desk_side_screen_4.png

Ещё откроем плагин для настройки коэффициентов PID-контроллера в реальном времени. Нажмём «Plugins → Dynamic Reconfigure».

part_8_desk_side_screen_5.png

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

Оперируем значениями в топиках /abot/left_wheel/target_velocity, /abot/right_wheel/target_velocity и коэффициентами PID-контроллеров и наблюдаем за скоростями вращения колёс /abot/right_wheel/сurrent_velocity и /abot/left_wheel/сurrent_velocity.

Используя rqt и плагин «Introspection → Node Graph», вы можете взглянуть на все запущенные в вашей системе ROS ноды, а также все топики, на которые они пописаны или в которые они публикуют сообщения.

Это также можно сделать через терминал командой:

rqt_graph

Сейчас граф наших ROS-нод и топиков выглядит так:

part_8_rqt_screen_1.png

Контроль движения

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

Пакет robot_control

В ROS есть много готовых контроллеров — ros_controllers. Ознакомьтесь с ними.

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

Традиционно в ROS пакет с контроллерами робота называют robot_control. Мы назовём наш пакет abot_control.

Cоздадим новый пакет с именем abot_control в рабочей области ros нашего проекта. Этот пакет будет хранить описание контроллеров робота. Для пакета установим следующие пакеты-зависимости:

Настройки контроллеров робота

В пакете abot_control создадим папку config, а в ней файл abot_controllers.yaml, который будет хранить настройки контроллеров нашего робота. Файлы настроек контроллеров в ROS традиционно имеют формат *.yaml.

Сперва у нашего робота будет два контроллера.

Первый контроллер относится к типу joint_state_controller/JointStateController. Он отвечает за обновление положений всех сочленений (joints) вашего робота на параметрическом сервере ROS.

Любому используемому контроллеру нужно дать имя. Мы назвали первый контроллер joint_state_controller.

Контроллер имеет один-единственный параметр: частоту обновления положений сочленений — publish_rate. Установим частоту обновления в 100 герц.

joint_state_controller:
  type: joint_state_controller/JointStateController
  publish_rate: 100

Второй контроллер относится к типу diff_drive_controller/DiffDriveController. Мы назвали его mobile_abot.

Этот тип контроллера отвечает непосредственно за движение двухколёсного робота. Как это работает? Мы даём контроллеру входные данные. Например, мы хотим, чтобы робот ехал с линейной скоростью в 1 м/c и угловой скоростью в 2 рад/c. Но не знаем, какие при этом должны быть скорости колёс. Контроллер берёт эти входные данные, а также значения текущих скоростей и положение робота, полученное с одометрии, обрабатывает всё это и на выходе отдаёт необходимые угловые скорости вращения колёс, чтобы наш робот двигался именно заданным образом.

В ROS движение робота конвенционально осуществляется через топики, которые обмениваются сообщениями типа geometry_msgs/Twist. Сообщение этого типа состоит из двух векторов linear и angular типа geometry_msgs/Vector3. Каждый такой вектор — трёхмерный. Вектор geometry_msgs/Vector3 linear описывает линейные скорости робота вдоль осей X, Y, Z в глобальной системе координат. Вектор geometry_msgs/Vector3 angular представляет скорость вращения робота вокруг осей X, Y, Z.

Наш двухколёсный дифференциальный привод обладает неголономным движением. Он контролируется только линейной скростью по оси X и угловой скоростью вокруг оси Z. Таким образом, наш вектор скорости linear будет всегда иметь нулевые линейные скорости по осям Y, Z и нулевые угловые скорости вокруг осей X и Y.

Опишем принцип работы контроллера. Мы отправляем желаемые векторы скорости робота в топик /cmd_vel. Контроллер анализирует полученные векторы, вычисляет необходимые скорости вращения правого и левого колеса и отправляет рассчитанные значения в топики моторов левого и правого колеса. Одновременно с этим контроллер считывает текущие углы поворота колёс и вычисляет текущую траекторию и одометрию робота. Сообщения об одометрии имеют тип nav_msgs/Odometry и публикуются в топик /odom.

В отличие от первого контроллера, этот имеет множество параметров, но настраиваются только основные из них. Вам необходимо указать:

  • Имена сочленений из описания робота для правого и левого колеса — left_wheel и right_wheel. У нас это left_wheel_to_base и right_wheel_to_base.
  • Имя базового сегмента робота — base_frame_id. У нас это base_footprint.
  • Максимальную линейную скорость и ускорение робота робота вдоль оси X, а также максимальную угловую скорость и ускорение робота вокруг оси Z. Минимальные значения этих скоростей можно не указывать, по умолчанию они равны максимальным с противоположным знаком.
  • Матрицы смещения twist_covariance_diagonal и pose_covariance_diagonal для погрешности одометрии. Эти параметры можно оставить по умолчанию.

Это важно! Откуда можно узнать максимальные значения скоростей и ускорений робота? Эти параметры — расчётные. Например, максимальную линейную скорость робота по оси Х легко вычислить, замерив пройденный путь колеса при максимальной скорости вращения. Тем не менее, точный расчёт максимальных скоростей, особенно ускорений робота — это трудная математическая задача, и для её решения пришлось бы написать десяток тестов. Мы же только новички в робототехнике, поэтому максимальные скорости будем вычислять эмпирически.

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

Мы установили частоту работы контроллера в 100 герц, а наши максимальные скорости и ускорения выглядят следующим образом:

mobile_abot:
  type : "diff_drive_controller/DiffDriveController"
  left_wheel: 'left_wheel_to_base'
  right_wheel: 'right_wheel_to_base'
  publish_rate: 100.0
  pose_covariance_diagonal: [0.001, 0.001, 1000000.0, 1000000.0, 1000000.0, 1000.0]
  twist_covariance_diagonal: [0.001, 0.001, 1000000.0, 1000000.0, 1000000.0, 1000.0]
  wheel_separation_multiplier: 1.0 # default: 1.0
  wheel_radius_multiplier    : 1.0 # default: 1.0
  cmd_vel_timeout: 0.1
  base_frame_id: base_footprint
  linear:
    x:
      has_velocity_limits : true
      max_velocity : 0.6 # m/s
      has_acceleration_limits: true
      max_acceleration : 6.0 # m/s^2
  angular:
    z:
      has_velocity_limits : true
      max_velocity : 4.71 # rad/s
      has_acceleration_limits: true
      max_acceleration : 9.42 # rad/s^2
  enable_odom_tf: true

Настраиваем описания робота для контроллера

Для правильного расчёта скоростей и одометрии контроллеру необходимы ещё два параметра: радиус колеса — wheel_radius и расстояние между колёсами — wheel_separation. По умолчанию контроллер ищет эти параметры в URDF описании робота robot_description, а в нашем случае — в abot_description.

Кроме этого, нам необходимо внести некоторые радикальные изменения в описание робота. В настройках контроллера mobile_abot мы указали имена сочленений колёс. Однако контроллер типа diff_drive_controller/DiffDriveController ожидает, что сегменты колёс, прикреплённые к этим сочленениям, имеют круглую форму.

Сейчас в описании нашего робота левое и правое колесо представлены в виде 3D-модели, взятой из файла .STL. Колесо из нашего .STL-файла красиво отображается в визуализации и хоть оно и круглой формы но является лишь набором полигонов. Контроллеру же нужна точная геометрия колеса, то есть оно должно быть описано математически.

Для этого мы создим ещё два сегмента в URDF-описании abot_description. Назовем их left_wheel и right_wheel. В этих сегментах мы опишем визуальную составлящую <visual> не 3D-моделью, а геометрией <geometry>. Теперь сочленения left_wheel_to_base и right_wheel_to_base будут вести к этим двум новым сегментам колёс. Старые сегменты с 3D-моделями мы удалять не будем, а оставим их зафиксированными на новых. Новые сегменты колёс описываем как цилиндры (<cylinder>) высотой 26 мм и радиусом 32,5 мм. Таковы реальные размеры наших колес.

Вносим изменения в файл описания abot.xacro:

<xacro:property name="wheel_radius" value="0.0325"/>
<xacro:property name="wheel_separation" value="0.128"/>
<xacro:property name="wheel_width" value="0.026"/>
<xacro:property name="PI" value="3.1415926"/>

В описание левого колеса:

<link name="left_wheel">
	<visual>
		<origin xyz="0 0 0" rpy="${PI/2} 0 0" />
		<geometry>
			<cylinder length="${wheel_width}" radius="${wheel_radius}"/>
		</geometry>
		<material name="Green" />
	</visual>
	<collision>
		<origin xyz="0 0 0" rpy="${PI/2} 0 0" />
		<geometry>
			<cylinder length="${wheel_width}" radius="${wheel_radius}"/>
		</geometry>
	</collision>
</link>
<joint name="left_wheel_to_abot_left_wheel" type="fixed">
	<origin xyz="0 0 0" rpy="0 0 0" />
	<parent link="left_wheel" />
	<child link="abot_left_wheel" />
</joint>
<joint name="left_wheel_to_base" type="continuous">
	<origin xyz="0 0.068 0.0145" rpy="0 0 0" />
	<parent link="abot_base" />
	<child link="left_wheel" />
	<axis xyz="0 1 0" />
</joint>

В описание правого колеса:

<link name="right_wheel">
	<visual>
		<origin xyz="0 0 0" rpy="${PI/2} 0 0" />
		<geometry>
			<cylinder length="${wheel_width}" radius="${wheel_radius}"/>
		</geometry>
		<material name="Green" />
	</visual>
	<collision>
		<origin xyz="0 0 0" rpy="${PI/2} 0 0" />
		<geometry>
			<cylinder length="${wheel_width}" radius="${wheel_radius}"/>
		</geometry>
	</collision>
</link>
<joint name="right_wheel_to_abot_right_wheel" type="fixed">
	<origin xyz="0 0 0" rpy="0 0 0" />
	<parent link="right_wheel" />
	<child link="abot_right_wheel" />
</joint>
<joint name="right_wheel_to_base" type="continuous">
	<origin xyz="0 -0.068 0.0145" rpy="0 0 0" />
	<parent link="abot_base" />
	<child link="right_wheel" />
	<axis xyz="0 1 0" />
</joint>

Дерево нашей модели теперь выглядит подобным образом:

part_9_rqt_screen_1.png

Запуск контроллеров

Создадим новый файл запуска для одновременной активации всех контроллеров.

В пакете abot_control создадим папку launch, а в ней новый файл запуска abot_control.launch.

В новом файле запуска мы загрузим на параметрический сервер ROS параметры наших контроллеров из файла abot_controllers.yaml и запустим ноду менеджера контроллеров — controller_spawner:

<launch>
	<rosparam file="$(find abot_control)/config/abot_controllers.yaml" command="load"/>
	<node name="controller_spawner" pkg="controller_manager" type="spawner" respawn="false" output="screen"
		args="joint_state_controller mobile_abot"></node>
</launch>

Пакет robot_base

Мы создали два контроллера и два драйвера. Контроллеры полностью настроены и готовы к работе, но они не знают, с чем именно им нужно работать. Мы должны соединить наши контроллеры с драйверами. Для этого нам нужно написать так называемый hardware_interface, с помощью которого мы подключим конкретные низкоуровневые драйверы робота к ROS-инфраструктуре более высокого уровня.

Традиционно в ROS для описания hardware_interface используется пакет с именем robot_base. Наш пакет уже по привычке называем abot_base.

Создаём в рабочем пространстве новый пакет abot_base.

В качестве пакетов зависимостей устанавливаем:

Класс AbotHardwareInterface

В пакете abot_base необходимо создать определённый класс C++ — наследник класса hardware_interface::RobotHW пакета hardware_interface в ROS. Это обязательное условие. Подробнее вы можете прочитать в Wiki-инструкции пакета ros_controls. Данный класс-наследник должен регистрировать все описанные контроллеры робота и взаимодествовать с драйверами.

Мы назвали наш класс AbotHardwareInterface. Он подписывается на топики драйверов /abot/left_wheel_angle и /abot/right_wheel_angle и публикует сообщения в топики /abot/left_wheel_target_velocity и /abot/right_wheel_target_velocity.

Два главных метода класса — это updateJointsFromHardware и writeCommandsToHardware. Метод updateJointsFromHardware обрабатывает текущие углы поворота колёс с драйвера, вычисляет текущие скорости вращения и передаёт их в контроллер mobile_abot. И наоборот, метод writeCommandsToHardware берёт от контроллера mobile_abot значения необходимых скоростей колёс и отправляет их в драйвер.

В пакете abot_base создаём папку src, а в ней новый заголовочный файл C++ abot_hardware_interface.h.

#ifndef ABOT_HARDWARE_INTERFACE_H_
#define ABOT_HARDWARE_INTERFACE_H_

#include <boost/assign/list_of.hpp>
#include <sstream>
#include <std_msgs/Float64.h>
#include <std_srvs/Empty.h>

#include <controller_manager/controller_manager.h>
#include <hardware_interface/joint_command_interface.h>
#include <hardware_interface/joint_state_interface.h>
#include <hardware_interface/robot_hw.h>
#include <ros/ros.h>
#include <ros/console.h>

class AbotHardwareInterface : public hardware_interface::RobotHW {
public:
    AbotHardwareInterface(ros::NodeHandle node, ros::NodeHandle private_node, double target_max_wheel_angular_speed);

    void updateJointsFromHardware(const ros::Duration& period);
    void writeCommandsToHardware();

private:
    ros::NodeHandle _node;
    ros::NodeHandle _private_node;

    hardware_interface::JointStateInterface _joint_state_interface;
    hardware_interface::VelocityJointInterface _velocity_joint_interface;

    ros::Subscriber _left_wheel_angle_sub;
    ros::Subscriber _right_wheel_angle_sub;
    ros::Publisher _left_wheel_vel_pub;
    ros::Publisher _right_wheel_vel_pub;

    struct Joint {
        double position;
        double position_offset;
        double velocity;
        double effort;
        double velocity_command;

        Joint()
            : position(0)
            , velocity(0)
            , effort(0)
            , velocity_command(0) {}
    } _joints[2];

    double _left_wheel_angle;
    double _right_wheel_angle;
    double _max_wheel_angular_speed;

    void registerControlInterfaces();
    void leftWheelAngleCallback(const std_msgs::Float64& msg);
    void rightWheelAngleCallback(const std_msgs::Float64& msg);
    void limitDifferentialSpeed(double& diff_speed_left_side, double& diff_speed_right_side);
};

AbotHardwareInterface::AbotHardwareInterface(ros::NodeHandle node, ros::NodeHandle private_node, double target_max_wheel_angular_speed)
    : _node(node)
    , _private_node(private_node)
    , _max_wheel_angular_speed(target_max_wheel_angular_speed) {
    registerControlInterfaces();

    _left_wheel_vel_pub = _node.advertise<std_msgs::Float64>("/abot/left_wheel/target_velocity", 1);
    _right_wheel_vel_pub = _node.advertise<std_msgs::Float64>("/abot/right_wheel/target_velocity", 1);
    _left_wheel_angle_sub = _node.subscribe("abot/left_wheel/angle", 1, &AbotHardwareInterface::leftWheelAngleCallback, this);
    _right_wheel_angle_sub = _node.subscribe("abot/right_wheel/angle", 1, &AbotHardwareInterface::rightWheelAngleCallback, this);
}

void AbotHardwareInterface::writeCommandsToHardware() {
    double diff_angle_speed_left = _joints[0].velocity_command;
    double diff_angle_speed_right = _joints[1].velocity_command;

    limitDifferentialSpeed(diff_angle_speed_left, diff_angle_speed_right);

    std_msgs::Float64 left_wheel_vel_msg;
    std_msgs::Float64 right_wheel_vel_msg;

    left_wheel_vel_msg.data = diff_angle_speed_left;
    right_wheel_vel_msg.data = diff_angle_speed_right;

    _left_wheel_vel_pub.publish(left_wheel_vel_msg);
    _right_wheel_vel_pub.publish(right_wheel_vel_msg);
}

void AbotHardwareInterface::updateJointsFromHardware(const ros::Duration& period) {
    double delta_left_wheel = _left_wheel_angle - _joints[0].position - _joints[0].position_offset;
    double delta_right_wheel = _right_wheel_angle - _joints[1].position - _joints[1].position_offset;

    if (std::abs(delta_left_wheel) < 1) {
        _joints[0].position += delta_left_wheel;
        _joints[0].velocity = delta_left_wheel / period.toSec();
    } else {
        _joints[0].position_offset += delta_left_wheel;
    }

    if (std::abs(delta_right_wheel) < 1) {
        _joints[1].position += delta_right_wheel;
        _joints[1].velocity = delta_right_wheel / period.toSec();
    } else {
        _joints[1].position_offset += delta_right_wheel;
    }
}

void AbotHardwareInterface::registerControlInterfaces() {
    ros::V_string joint_names = boost::assign::list_of("left_wheel_to_base")("right_wheel_to_base");

    for (unsigned int i = 0; i < joint_names.size(); i++) {
        hardware_interface::JointStateHandle joint_state_handle(joint_names[i], &_joints[i].position, &_joints[i].velocity, &_joints[i].effort);
        _joint_state_interface.registerHandle(joint_state_handle);

        hardware_interface::JointHandle joint_handle(joint_state_handle, &_joints[i].velocity_command);
        _velocity_joint_interface.registerHandle(joint_handle);
    }
    registerInterface(&_joint_state_interface);
    registerInterface(&_velocity_joint_interface);
}

void AbotHardwareInterface::leftWheelAngleCallback(const std_msgs::Float64& msg) {
    _left_wheel_angle = msg.data;
}

void AbotHardwareInterface::rightWheelAngleCallback(const std_msgs::Float64& msg) {
    _right_wheel_angle = msg.data;
}

void AbotHardwareInterface::limitDifferentialSpeed(double& diff_speed_left_side, double& diff_speed_right_side) {
    double large_speed = std::max(std::abs(diff_speed_left_side), std::abs(diff_speed_right_side));
    if (large_speed >  _max_wheel_angular_speed) {
        diff_speed_left_side *=  _max_wheel_angular_speed / large_speed;
        diff_speed_right_side *=  _max_wheel_angular_speed / large_speed;
    }
}

#endif // ABOT_HARDWARE_INTERFACE_H_

Весь этот процесс можно описать классической теорией управления. У нас есть контроллер типа closed-loop. На входе контроллера — вектора скорости движения робота на плоскости (X, Y). На выходе — требуемая скорость вращения колёс для заданной скорости робота. В качестве обратной связи используются текущая скорость вращения колёс и одометрия.

Нода robot_base

Теперь создадим ноду robot_base, которая запустит наш контроллер замкнутого цикла и свяжет ROS с драйверами. Назовём ноду abot_base.

Нода будет принимать два параметра ROS:

  • control_frequency — частота работы контроллера.
  • max_wheel_angular_speed — максимальная угловая скрость вращения колеса. Этот параметр нужен, чтобы случайно не отправить на драйвер колеса слишком большую скорость вращения.

Контроллер срабатывает по таймеру control_loop типа ros::Timer с частотой control_frequency.

В пакете abot_base создаём новый файл abot_base.cpp:

#include <chrono>
#include <functional>
#include <ros/callback_queue.h>

#include "abot_hardware_interface.h"

typedef boost::chrono::steady_clock time_source;

void controlLoop(AbotHardwareInterface& hardware, controller_manager::ControllerManager& cm, time_source::time_point& last_time) {
    time_source::time_point this_time = time_source::now();
    boost::chrono::duration<double> elapsed_duration = this_time - last_time;
    ros::Duration elapsed(elapsed_duration.count());
    last_time = this_time;

    hardware.updateJointsFromHardware(elapsed);
    cm.update(ros::Time::now(), elapsed);
    hardware.writeCommandsToHardware();
}

int main(int argc, char** argv) {
    ros::init(argc, argv, "abot_base");
    ros::NodeHandle node;
    ros::NodeHandle private_node("~");

    int control_frequency;
    double max_wheel_angular_speed;

    private_node.param<int>("control_frequency", control_frequency, 1);
    private_node.param<double>("max_wheel_angular_speed", max_wheel_angular_speed, 1.0);

    AbotHardwareInterface hardware(node, private_node, max_wheel_angular_speed);

    controller_manager::ControllerManager cm(&hardware, node);

    ros::CallbackQueue abot_queue;
    ros::AsyncSpinner abot_spinner(1, &abot_queue);

    time_source::time_point last_time = time_source::now();

    ros::TimerOptions control_timer(
        ros::Duration(1 / control_frequency),
        boost::bind(controlLoop, std::ref(hardware), std::ref(cm), std::ref(last_time)), &abot_queue);

    ros::Timer control_loop = node.createTimer(control_timer);

    abot_spinner.start();
    ros::spin();
    return 0;
}

Не забываем пересобирать наш проект при добавлении в него новых исходных файлов C++:

catkin_make

Запуск ноды robot_base

Создадим файл запуска для новой ноды. В пакете abot_base делаем папку launch, а в ней новый файл запуска abot_base.launch:

<launch>
	<node name="abot_base_node" pkg="abot_base" type="abot_base_node" output="screen">
		<param name="control_frequency" type="int" value="100"/>
		<param name="max_wheel_angular_speed" type="double" value="18.0"/>
	</node>
</launch>

Тестируем движение робота

Пришло время проверить движение нашего робота. У нас есть всё необходимое для этого.

Давайте создадим новый файл запуска для тестов движения робота. Назовём файл bringup.launch. Разместим его в пакете с описанием робота abot_description.

Это важно! Данный файл запуска — один из главных, и с этого момента мы будем запускать его только на Raspberry Pi. Ноды ROS, запущенные этим файлом, должны работать только на борту робота!

В создаваемом файле запуска мы:

  • Загрузим URDF-описание робота на параметрический сервер ROS.
  • Запустим драйверы робота.
  • Запустим контроллеры робота.
  • Запустим ноду abot_base.
<launch>
	<param name="robot_description" command="$(find xacro)/xacro '$(find abot_description)/urdf/abot.xacro' --inorder"/>
	<node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher" respawn="false" output="screen" />
	<include file="$(find abot_base)/launch/abot_base.launch" />
	<include file="$(find abot_control)/launch/abot_control.launch" />
	<include file="$(find abot_driver)/launch/abot_drivers.launch" />
</launch>

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

На настольном компьютере запускаем ядро ROS:

roscore

Затем на Raspberry запускаем файл bringup.launch.

cd ~/ros
su root
source devel/setup.bash
roslaunch abot_description bringup.launch

part_9_rpi_side_screen_1.png

part_9_rpi_side_screen_2.png

part_9_rpi_side_screen_3.png

На настольном компьютере проверим список топиков ROS:

rostopic list

part_9_desk_side_screen_1.png

Как видите, у нас появилось много новых топиков.

Самые интересные — топики, созданные нашим контроллером движения дифференциального привода mobile_abot: /mobile_abot/cmd_vel и /mobile_abot/odom. Ради этих топиков мы и создавали контроллеры.

Топик /mobile_abot/cmd_vel принимает сообщения типа geometry_msgs/Twist. В него мы отправляем желаемые скорости движения робота.

Топик /mobile_abot/odom отдаёт нам сообщения типа nav_msgs/Odometry, которые содержат рассчитанную контроллером одометрию робота, его положение и ориентацию в пространстве.

Сейчас граф наших ROS-нод и топиков выглядит так:

part_9_rqt_screen_1.png

Попробуем порулить роботом.

Для рулёжки будем использовать уже привычную утилиту rqt и плагин rqt_robot_steering. Этот плагин специально создан для управления двухколёсным дифференциальным приводом.

На настольном комьютере в новом терминале запустим rqt. В окне rqt выбираем «Plugins → Robot Tools → Robot Steering». Откроется окно со слайдерами, используемыми для рулёжки.

part_9_rqt_screen_2.png

В верхней части окна нужно написать имя нашего топика для задания скоростей — /mobile_abot/cmd_vel.

На краях слайдеров есть поля, куда нужно установить максимальные и минимальные значения скоростей робота. Мы их уже знаем. Их можно взять из файла конфигурации контроллера — abot_controllers.yaml.

Начнём двигать слайдеры и следить за движением робота.

Визуализация одометрии

Мы проверили работу контроллера в одном направлении — отправили желаемые скорости робота в топик /mobile_abot/cmd_vel.

Теперь проверим, как работает наш контроллер в обратном направлении — визуализируем одометрию робота.

В пакете abot_description в папке launch создадим новый файл запуска для визуализации одометрии. Назовем его display_movement.launch.

Визуализировать одометрию будем с помощью уже знакомого нам rviz. В файле запуска запустим ноду rviz с новым файлом настройки визуализации abot_movement.rviz.

Новый файл настроек abot_movement.rviz отличается от прежнего abot_model.rviz тем, что параметр Fixed Frame из меню Global Options теперь имеет значение odom.

<launch>
	<arg name="rvizconfig" default="$(find abot_description)/rviz/abot_movement.rviz" />
	<arg name="model" default="$(find abot_description)/urdf/abot.xacro" />
	<param name="robot_description" command="$(find xacro)/xacro --inorder $(arg model)" />
	<node name="rviz" pkg="rviz" type="rviz" args="-d $(arg rvizconfig)" required="false"/>
</launch>

Запускаем все файлы запуска, как в прошлой главе. Если вы вдруг их закрыли, то запускаем на RPi главный файл запуска bringup.launch, а на настольном компьютере rqt с плагином rqt_robot_steering.

Запускаем на настольном компьютере визуализацию одометрии:

cd ~/ros
source devel/setup.bash
roslaunch abot_description display_movement.launch

Откроется окно rviz с новыми настройками. Попробуйте порулить роботом из rqt_robot_steering и наблюдайте за визуализацией в rviz.

Вот она, магия инфраструктуры ROS! Если вы всё сделали верно, то любое реальное движение робота будет регистрироваться и визуализироваться в rviz! Одна ячейка сетки (grid) у нас равна 10 см. Мы можем даже не смотреть на реального робота (или даже находиться в километре от него), но мы будем знать, где он находится и куда смотрит.

Изначально при запуске bringup.launch робот находится в точке с координатами (0, 0, 0) в глобальной системе координат. В этих же координатах находится базовая точка одометрии — odom. Когда мы начинаем рулить роботом, мы уезжаем от этой базовой точки. В процессе движения rviz рисует нам вектор до базовой точки, тем самым показывая, как далеко уехал робот с начала работы программы.

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

Тем не менее, колёсная одометрия не слишком точна. Со временем данные одометрии начинают «плавать» (odometry drift), и в ней накапливаются ошибки. Точность одометрии достигается настройкой драйверов робота и параметров контроллера движения.

Точность колёсной одометрии легко проверить. Можно нарисовать под роботом крестик на стартовой позиции, обозначив таким образом координату (0, 0, 0) в реальном мире. Затем как следует порулить роботом: поделать развороты, повороты, разгоны и торможения. Спустя некоторое время нужно сравнить вектор смещения робота относительно базовой точки odom в rviz с реальным смещением робота относительно крестика на полу. Если эти смещения более-менее совпадают — робот смотрит в одну и ту же сторону, находится на примерно одинаковом расстоянии от координаты (0, 0, 0) — то одометрию можно считать точной.

Продолжение следует

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

Читать далее >