Привет! Меня зовут Алексей и этой зимой на меня напало желание наконец написать статью о том, как я и моя команда СПОшников участвовали в Eurobot Open 2022 и 2023.
Это моя первая статья здесь.
Сразу дисклеймер: в данной статье будет мало рассуждений и историй о механике роботов и процессе разработки приводов и корпуса. Будет обзор именно того, как я разрабатывал программную часть, и того, как быстро на самом деле происходит обучение на реальном проекте. Материалы сошкрябывались с переписок в ВК и телеге, потому что нам не пришло в голову подробно документировать наши разработки
Костяк команды состоял из 5 человек: Я (программист), Дима (главный механик), Павел (электрика), Александр (механик), Михаба (человек на все руки).
Статья от нашего механика Димы
Eurobot это некогда международное соревнование по робототехнике, которое имеет две категории: Junior и Open. В обеих категориях основные правила схожи: 1-2 робота от команды выполняют различные игровые задания, пытаясь набрать как можно больше очков за 100 секунд. Делают это они вместе с роботами команды соперников, при этом активно мешать другой команде можно, но столкновения и откровенная агрессия запрещена. Этот формат, как и размер поля слабо менялись из года в год, начиная аж 1998 года, когда проходил первый Eurobot еще только во Франции.
Некогда международное - потому что начиная с 2020/21 года принимать участников-призеров с всероссийского этапа на международный организаторы отказались. Но это не остановило русскую сторону организаторов от проведения региональных и всероссийского этапов (что они все еще успешно делают https://vk.com/eurobotrussia). Не такой уж и Open.
В лиге Junior молодежь до 18 лет делает роботов на пультах управления (проводных/беспроводных). Но одно дело собрать телегу на китайских примерах, другое дело - полностью автономный робот. Контраст сложности между даже навороченным роботом на пульте и простым автономом - колоссальный.
В уютном СПО по монтажу и ремонту элетроники наш идейный и замечательный преподаватель решил участвовать сначала в Junior версии, а затем и в Open. О юниорской лиге здесь рассказа не будет, хотя это тоже был незабываемый опыт.
ROS
Robot Operating System - мета операционная система для разработки роботов. На самом деле это большая пачка пакетов, которые предоставляют инструменты для разработки, рантайм и множество готовых решений, которые нужны при программировании роботов.
Основная фича ROS 1 (оба года были на нем, а именно версия noetic) - мастер, который контроллирует обмен сообщений между процессами робота (нодами). Вся эта система очень похожа на MQTT, где существуют топики, у которых есть ноды-подписчики и ноды-публикаторы. Структура сообщений описывается заранее и код генерируется на этапе сборки для всех поддерживаемых языков: C++, Python, JS.
Также есть расширения на PUB/SUB моделью: services и actions. Экшны не использовались мною в обоих проектах. Сервисы позволяют синхронно обратиться к "серверу" - исполнителю сервиса.
Eurobot Open 2022
Правила соревнований выкладываются в сырой форме в сентябре и слегка дорабатываются и разъясняются в течении пары месяцев, сами соревнования проходят примерно в середние мая. Тематикой этого года были «раскопки древней цивилизации роботов».
Это был 4ый курс из целых 5 на моем СПО. Как раз в сентябре я устроился работать в сервисный центр при местном интернет-провайдере. Какая-никакая работа, где я зарабатывал бешенные деньги и проходил потом производственную практику.
Получив правила и достаточно настрадавшись при подготовке к лиге Junior прошлого года мы знали, что начинать надо сразу! Но ведь еще столько времени впереди, можно не париться, верно? Видимо плохо нас учил опыт
![Модель поля для Eurobot 2022 Модель поля для Eurobot 2022](https://www.pvsm.ru/images/2025/02/09/kak-studenty-spo-robotov-na-ROS-pilili.png)
Время летело незаметно. На фоне лишь начиналась какая то абсолютно минимальная разработка с моей стороны и набрасывание идей о механике со стороны главного механика. Благо я сразу догадался вести разработку на github. В коммитах прямо перед и во время соревнований мне было уже не до сообщений в коммитах.
![По сути основная работа пришлась на 3 последних месяца. Можно увидеть боль в даты самих соревнований По сути основная работа пришлась на 3 последних месяца. Можно увидеть боль в даты самих соревнований](https://www.pvsm.ru/images/2025/02/09/kak-studenty-spo-robotov-na-ROS-pilili-2.png)
Я почти не знал, что такое программирование. Мой единственный опыт был - написать клон Pong/Breakout на двоих на pygame. Почему же тогда ROS? Дело в том, что мы участвовали также в соревнованиях WorldSkills по сервисной робототехнике, которые были тогда супер-супер свежими.
На этих соревнованиях я впервые познакомился вообще с линуксом. И конечно же с ROS. Но пользоваться и абсолютно минимально донастраивать готового учебного робота в тепличных условиях != разрабатывать чтото свое. Поэтому конечно же я решил отказаться от использования готовых пакетов вроде slam_navigation. На самом деле я не понимал как ими пользоваться. В ретроспективе отказ от готовых решений был хорошей идеей. Но в первый год было ужасно тяжело.
Рос хорошо помогал вплане визуализации того, что происходит в роботе: rviz позволял относительно легко нарисовать карту и маршрут на ней.
Полностью я от готового не отказывался. Архитектуру нод я использовал похожую на то, что используется в slam navigation
![Финальный ультра постер сделанный почти на коленке Финальный ультра постер сделанный почти на коленке](https://www.pvsm.ru/images/2025/02/09/kak-studenty-spo-robotov-na-ROS-pilili-3.png)
Как в холодную воду я погружался в CMake, никогда раньше с ним не работав. Я был знаком с C и C++ только через Arduino, а тут расширенная система сборки поверх симейка (catkin), которая через приседания позволяет "без боли" собрать пакеты с гитхаба и использовать их в других проектах в системе. Для меня симейк тогда был как страшный черный ящик, который чтото делает.
Прошивка для Arduino
Первое что нужно было разработать: прошивку для Arduino Mega 1560. У нас были китайские моторчики, пластиковые омни-колеса и удобные шилды с драйверами для моторов Moebius (как раз на мегу).
Цель ардуино в проекте: она должна была обрабатывать тики энкодера, и управлять моторами. Прошивка на ардуино предоставляла топик для управления моторами, а также писала в другой топик тики энкодеров с моторов. Одной из причиной выбора ROS было впечатление, что он мне упростит написание взаимодействия ардуинки с Raspberry.
Но на самом деле разработка была очень неудобная: для того, чтобы собрать прошивку нужно было иметь установленным Arduino IDE и сплясать с генерацией библиотек под ардуинку, в том числе кодогенерация для стандартных и кастомных типов сообщений. Ближе к середине я написал для себя простой скрипт с автоматизацией этого процесса. Особенно мне в нем нравится способ определения папки с прошивкой).
Это был почти самый долгий этап: написание PID регулятора, а также кинематики для омни колес. Кинематика оказалась довольно простой - спроецировать вектор желаемого направления на вектора направлений колес. Я слышал про то, что float не очень хорошо использовать в коде ограниченном по времени, но меня это не волновало. Пол месяца чтобы родить вот такой код. Хотя конечно дольше тестировать и учится подбирать коэффициенты.
const float dtime = (loop_delay / 1000.0);
void PID(int mot)
{
float error = targ_spd[mot] - curr_spd[mot];
inter_term[mot] += dtime * error;
pwm[mot] = error * prop_coeff[mot]
+ inter_term[mot] * inter_coeff[mot]
- (error - last_error[mot]) / dtime * diff_coeff[mot];
inter_term[mot] = constrain(inter_term[mot], -30000, 30000);
last_error[mot] = error;
pwm[mot] = constrain(pwm[mot], -255, 255);
}
Сначала он ползал, но потом научился ездить. Правда за оба года соревнований я так и не подобрал хорошие коэффициенты
Costmap server
К этому моменту я уже уволился с работы.
Очевидно сам я не додумался бы, что нужна какая то костпама, но я увидел как она используется в учебных роботах. Но я быстро понял, что она сильно упрощает построение маршрута: объезд целей на безопасном расстоянии за счет того, что каждая точка, по которой ехать нельзя получает "стоимость" в 100. Точка - минимальная единица на сетке навигации, была у нас 2х2 сантиметра, более точную дескритизацию мой питон код не потянул бы
Все остальные точки получают убывающую стоимость в зависимости от близости к полным препятствиям. Такая система позволяет выставаить порог максимальной цены, по которой может ехать робот, а также, например, ехать медленней там, где "опасно"
Почему то в этом проекте во мне чтото переклинило и почти все ноды выполнялись в виде даже не синглтонов, а питонячих классов, в которых все статика. Вообще в ноде не зазорно делать просто глобальные переменные и функции, потому что одна программа запускается в одном или нескольких экземплярах и вся модульность строится на уровне нескольких процессов. Но я захотел ООП! В итоге это только добавляло мусора и шума в код, непонятно зачем
class Costmap():
publish_on_obstacles_recieve = rospy.get_param('~publish_on_obstacles_recieve', 1)
write_map_enable = rospy.get_param('~write_map_enable', 1)
debug = rospy.get_param('~debug', 1)
interpolate_enable = rospy.get_param('~interpolate_enable',1)
...
@classmethod
def publish(cls):
msg = OccupancyGrid()
curr_time = rospy.Time.now()
msg.header.frame_id = "costmap" ###????????
msg.header.stamp = curr_time
msg.info.resolution = cls.resolution
msg.info.height = cls.height
msg.info.width = cls.width
for y, x in cls.grid_parser:
#print(x,y)
data = int(cls.grid[y][x] + cls.mask[y][x])
if data > 100:
data = 100
msg.data.append(data)
Я долго возился с вот этим замечательным методом публикации карты, потому что я некорректно заполнял параметры в сообщении OccupancyGrid.
![Попытки отрисовать костмапу в rviz Попытки отрисовать костмапу в rviz](https://www.pvsm.ru/images/2025/02/09/kak-studenty-spo-robotov-na-ROS-pilili-4.jpg)
Также можно заметить вот такую строку
data = int(cls.grid[y][x] + cls.mask[y][x])
В моем сервере было разделение на статическую карту (которая подгружалась из .png картинки) и динамика, которая приходит с лидара. Точек, для которых нужно проводить "инфляцию" в динамике в разы меньше, по моему препятствием была как раз всего одна точка (группа точек на лидаре). Наглядно невероятную скорость моего алгоритма для инфляции можно увидеть здесь.
У меня тогда был бзик на темную вырвиглазно-контрастную тему в VSCode
Читаю я этот код и абсолютно нифига не понимаю, что у меня тогда творилось в голове. Вроде вот таких перлов:
for num, _tup in enumerate(cls.grid_parser):
y,x = _tup
#if y/50 >= 1.5 and x/50 >= 1.5:
#raise SyntaxError()
...
def obstaclesCallback(obst):
if _node_ready:
Objects.clear()
for obj in obst.data:
...
Objects.updateMask()
# AHAHAHAHAHAAHAHA H
# AHAHAHAHHAHAHAHAAHHAHAHHAHAHAAHAHAHAHAHAHAHAHAHAHAHAHHAHAHA Питон момент
# короче здесь был лишний обступ и он обновлял и пересылал карту НА КАЖДОМ объекте, а.к.а. они появлялись по очереди и планер не всегда их видел вовремя))))
Комментарий оригинальный 3х годовой давности. По моему я этот баг долго искал, он был еще и плавающий. Objects.updateMask()
находился на два таба правее и я это долго не замечал.
Глобальный планировщик
Самая большая жертва в этом проекте. Нет, я не стал читать про то, какие люди вообще планировщики придумали на свете. Я пошел своим путем. Я придумал гениальную идею - что-то вроде ray marching.
-
Кидает вектор фиксированной длины до цели
-
Если ни одна из точек, через которую прошел вектор не слишком дорогая - то кусок маршрута построен, кидаем следующий
-
Если нет, то начинаем перебирать, повернув вектор влево или вправо, пока не переберем все варианты или не получится поставить вектор не задев препятствий (180 градусов по моему был максимум)
-
???
-
Profit
Кто-то скажет, что это ужасный планировщик маршрута - и будет абсолютно прав. Это бред! Мне просто очень понравилось видео Mathologer про e^pi = -1. И я подумал - блиииин вот можно же вектора поворачивать в питоне умножением, если вектора это комплексные числа
Эта нода пострадала больше всех от бесконечных допиливаний. Почему то сюда потом переехал кусок логики сделанный на модных Action Server для выполнения заданий движения. Полный хаос.
Но. Это работало. Работало потому что все строилось на предположении, что препятствия круглые. Была навешана тонна костылей, чтобы оно не проваливало построение маршрута на каждом чихе, или не дай бог в углу поля
![Шикарный маршрут Шикарный маршрут](https://www.pvsm.ru/images/2025/02/09/kak-studenty-spo-robotov-na-ROS-pilili-5.jpg)
![Еще лучше Еще лучше](https://www.pvsm.ru/images/2025/02/09/kak-studenty-spo-robotov-na-ROS-pilili-6.jpg)
Локальный планировщик
Локальный планировщик - нода, которая транслирует построенный в глобальном маршрут в конкретные команды для ардуинки на исполнение. Т.е. транслирует глобальную команду, например "ехать вверх по иксу", в локальную, в зависимости от текущего поворота робота (для которого вверх по иксу может быть и вперед и назад и что угодно).
Как и во всех моих нодах в этом проекте, со временем это чудо обросло специфичными костылями и "оптимизациями". Там была какая то замудернная логика, что ехать можно не к следующей точке маршрута, а пропускать их какое то количество в зависимости от свободности маршрута, но я уже не помню точно. Ехать прямо к следующей в любом случае нельзя, потому что легко перескочить точку и не отметить ее пройденной.
Здесь не было никакого порядка - полный хаос, набрасывание гениальных идей на вентилятор. А время все тикало. До сорвенований на этот момент было уже полтора месяца? Или один? Чтобы хоть чтото из этого бардака заработало приходилось подолгу муторно подбирать параметры, добавлять хаки. Зато свое любимое.
На видео ниже робот лежит на столе у меня дома на банках и крутит колесами в воздухе, которые являлись на тот момент единственным источником положения для него (лидар пока еще не был готов).
Моя счастливая реакция на первый успешный маршрут
Более позднее видео
Обработчик лидара
По отдельности каждая нода делает совсем немного. В этом конечно плюс ROS. Он навязывает архитектуру. Да для простого робота это все довольно переусложнено, но в целом архитектура правильная. Я сужу по себе. Будучи абсолютным нулем, я писал код, и как только один файл перерастал 500 строк в нем начинался хаос. Все таки в ретроспективе ROS был полезен с этой стороны. Четкое и резкое отсечение границы между процессами заставляла писать с начала.
Маленькие, тестируемые ноды вместе объединялись в чтото уже похожее на робота. Теперь пришла пора заводить замечательный лидар, который все это время только пылился.
Никаких статей и гайдов по работе с лидаром я не читал, но у меня был небольшой опыт его использования, опять же в роботе turtlebro. Я уже поднаторел в использовании ROS, научился даже отображать маячки в RVIZ.
Стартовое положение робота задается заранее. Координаты опорных маяков заданы заранее также. Для них на поле предусмотрены специальные площадки. Всего маяков 3 штуки. Для получения сканов лидара использовался поставляемый производителем лидара пакет для ROS - rplidar-ros-noetic.
Алгоритм я придумал следующий:
-
По точкам, приходящим с лидара (полный скан 360 градусов — 8к точек при 10 оборотах/сек ~800 точек на оборот) производится обход. И если обнаруживается резкий скачок в расстоянии — обход заканчивается на текущей точке (найден объект). Продолжаем с новой точки (которая превысила порог расстояния только что). И так пока не закончится точки со всего оборота лидара. Также «объект» отсекается если превышено количество точек на объект (сплошная стена точек станет стеной из «объектов»)
-
«Объект» самый ближний к ожидаемой позиции маяка (белый цилиндр на видео) будет считаться за «реальное» положение маяка (розовый цилиндр).
-
Раз в Н секунд высчитывается среднее отклонение положения «реальных» маяков от «ожидаемых». (Если видно хотя бы 2 маяка из 3-х)
-
Это отклонение умноженное на коэффициент меньше 1 вычитается из положения робота.
Таким образом лидары могут поправлять накапливающуюся погрешность с энкодеров на колесах.
Это плохой алгоритм, очень плохой. Он еле-еле работал. Приходилось молиться, чтобы робот свой или противника не перегородил маяк. Еще хуже если ктото встанет рядом с маяком и сам станет "маяком". Отличать материалы наши дешевые лидары не умели. Точнее умели, но так они теряли треть точек, а они и без этого еле видели маяки толщиной с цилиндрическую банку чипсов (ее мы и использовали)
![Скрин уже на момент тестирования (здесь где-то неделя до регионального этапа) Скрин уже на момент тестирования (здесь где-то неделя до регионального этапа)](https://www.pvsm.ru/images/2025/02/09/kak-studenty-spo-robotov-na-ROS-pilili-7.jpg)
Менеджер заданий
Остался самый верхний уровень и две недели до соревнований. Что может пойти не так?
Верхний уровень я взял амбициозный — буду парсить ямль файлы. Здесь я уже умел в asyncio (не сразу, сначала боль). Я научился немного в чтото уже похоже на ООП и сделал по сути два DSL — один для «скриптов» и второй для маршрутов. Маршрутный ямль умел вызывать сервисы из «скриптов».
Асинкио там был довольно бесполезен, эта штука была на самом деле почти полностью синхронной.
«Скрипты» или «Вызовы» как я их называл позволяли давать имена простым действиям (чтобы в коде не было выставления положения сервомотора в некие 50%.
- "ArmUp":
- "servos_service":
- "num": 0
- "state": 80
- "ArmDown":
- "servos_service":
- "num": 0
- "state": 0
Не знаю как, но за такие короткие сроки я успел набросать в эту систему много вещей: прерывания, вызовы с аргументами. Например было задание, в котором нужно замерить сопротивление резистора, чтобы решить: переворачивать деревянную плитку или нет. Мое решение было: если приходит сообщение, что замер показал нужное значение — мы поднимаем гордно манипулятор, чтобы перевернуть плитку. Причем источником прерывания мог быть также и таймер (через это была, например, сделана экстренная остановка на 98 секунде).
От большинства фич пришлось отказаться, потому что двух дней не хватило написать нормлаьные маршруты и дособирать нужные части робота.
Небольшой кусок маршрута робота 2 (бобы) - его задача была поменять местами кубики (статуэтку и реплику):
tasks:
za_statuetkoi:
- call: AdjustOn
- call: NeNadoPlitka
- change_cost: 50
- move : 1/2.6/0.23
- change_cost: 8
- call: AdjustOn
- move : 0.49/2.58/0.2
- sleep: 0.5
- move : 0.49/2.58/0.2
- call : MicroArmHalf
- call: PumpOn
- sleep: 0.2
- change_cost: 5
- move: 0.365/2.695/0.2
- change_cost: 8
- call: MicroArmUp
Результат трудов
Мы поучаствовали в региональном этапе в Калиниграде. Мы были единственной командой.
Две недели подготовки к всероссийскому этапу пролетели мгновенно. Сделано было очень много. И вот мы прилетаем в Москву, едем в Королев уже уставшимы Иии... не проходим отбор (так называемую гомологизацию - проверку того, что робот избегает столкновений). Ну ничего - пол ночи войны, пешком до отеля, триллион правок параметров утром и мы еле-еле проходим отбор.
Мы могли занять второе место. Именно против нас у команды Сколково отказал один из двух ботов, но у нас все еще хуже пошло. Ну ничего против нас не самая сильная команда в битве за 3е место.... Провал. Я не знаю что произошло. Роботы просто встали. Может сбилось положение. Может я поставил роботов криво. Я не знаю. С одной стороны обидно. С другой стороны - все. Господи, наконец то это счастье мучение кончилось.
В отличии от механиков, которые могли насладиться отдыхом и повеселиться на соревнованиях я пахал. Это тоже плохая идея, обычно только больше вреда приносится правками в последний момент.
Было весело, чертовски весело. Я научился очень многому. Бесценный опыт. Проект все таки учебный и я очень рад, что решил пилить свои кривые костыли. И ничего что мы заняли 4е место - тоже неплохо, в следующем году костыли будут сверкать. Работать с командой друзей над таким проектом было очень круто. Наше 4 место в отеле я отметил с товарищами гордой банкой Балтики 9.
Самый лучшим был заезд номер 1. У нас еще такой забавный противник был, который почти ничего не делал - команда вытянутая бобышка. Но над ней не стоит смеяться - в следующем году они устроили просто шоу. Невероятное.
Но это история уже для второй части статьи, где будет код на аж плюсах. А то все питон да питон. В ней я успел поработать на плюсах за деньги на своей первой работе программистом и на опыте первого года сделать конфету!
Ну а здесь видео наших роботов уже в более готовом виде.
Спасибо всем за внимание!
Автор: cyanidle