- BrainTools - https://www.braintools.ru -
Мы создали симуляцию 3D-города с машинами, пешеходами и светофорами, по которому ездит шестиколёсный робот под управлением настоящего скетча Arduino. Все это для обучения [1] детей и взрослых в кружках робототехники.

У нас есть онлайн-лаборатория PinPort [2]: в браузере собираешь схему, пишешь скетч для Arduino, и он исполняется на эмуляторе микроконтроллера. Светодиоды и датчики там давно, потом появились роботы. В этот раз задача была такая: сделать виртуальный город с улицами, машинами, пешеходами и светофорами. По нему ездит робот-доставщик, и его задача — довезти заказ из пункта A в пункт Б.
Робота назвали R-D6: шесть колёс, разворот на месте. Управляет им не скрипт на JavaScript, а обычный скетч для Arduino Uno. Скетч собирается на сервере через arduino-cli, а полученная прошивка исполняется покомандно в эмуляторе ATmega328P. Ниже расскажу, из чего всё это состоит.
У системы три части, и они движутся строго вместе:
Плата. Эмулятор AVR исполняет прошивку с частотой 16 МГц: таймеры, ШИМ, I2C, UART.
Физика. Робот — твёрдые тела в Rapier: корпус, шесть колёс, моторы. Шаг мира — 2 мс.
Город. Светофоры, машины и пешеходы — своя логика [3], которая задаёт положение кинематических тел в том же мире Rapier.
Плата и физика идут поочередно: две миллисекунды процессора, затем один шаг мира. За эти две миллисекунды придуманный нами класс собирает с выводов платы скважность ШИМ на ENA/ENB и уровни IN1–IN4 драйвера L298N и превращает их в напряжение на моторах. В обратную сторону идут показания датчиков: цифровые пины попадают на выводы D2/D3, лидар и навигатор — в регистры устройств на шине I2C.
Почему именно так? Из-за повторяемости. Если связать слои через реальное время, тот же скетч на слабом ноутбуке и на сильном десктопе поедет по-разному. У нас есть уроки с автопроверкой и арена, где заезды считает сервер, так что результат обязан быть одинаковым везде.
В жизни лидар и навигатор общались бы с контроллером по UART со своим протоколом. Мы взяли самую простую для школьника схему — файл регистров на I2C. Первый байт записи задаёт номер регистра, дальше чтение идёт подряд, как у MPU6050 или DS3231.
|
Устройство |
Адрес |
Что отдаёт |
Как часто обновляется |
|---|---|---|---|
|
Лидар |
0x52 |
36 секторов по 10°, расстояние в см (uint16), номер оборота |
10 раз в секунду |
|
Навигатор с камерой светофоров |
0x53 |
флаги, сигнал перехода, номер точки, курс, угол на цель, расстояния, скорость, координаты |
50 раз в секунду |
Скан лидара — 72 байта, а буфер библиотеки Wire на Uno — 32. Поэтому встроенная библиотека RD6.h читает его тремя кусками по 24 байта, и только когда счётчик оборотов изменился. Мелочь, но она честная: с таким же ограничением сталкивается любой, кто пишет драйвер датчика на настоящей плате.
Сам лидар — это лучи в мире движка Rapier. В каждом секторе их три (−3°, 0°, +3°), и берётся ближайшее попадание. С одним лучом на сектор столб светофора толщиной 14 см с пяти метров проскакивал между лучами, и робот в него врезался. Дальность 20 м, дальше лидар отдаёт 2000 см — «ничего нет».
План — сетка улиц 3 × 3 с шагом 32 м. Проезжая часть 8 м, по полосе в каждую сторону, тротуары по 3 м. На пяти перекрёстках стоят светофоры, у них 16 зебр. Цикл светофора — 50 с: 20 с зелёный одному направлению, 3 с жёлтый, 2 с все красные. У пешеходов зелёный горит вместе с попутным потоком машин, последние 4 с мигает.
Машины (12 штук) ездят по полосам, стоят на красный, держат дистанцию и на перекрёстке выбирают поворот случайно. Повороты идут по дугам: направо — вокруг угла бордюра радиусом 2 м, налево — радиусом 6 м. На перекрёсток машина не въезжает, пока там едет кто-то, чей путь пересекается с её путём. Если подъехали одновременно, первой едет та, у кого номер меньше. Столь тупое правило надёжно исключает взаимную блокировку.
Пешеходы (20 человек) ходят по петлям тротуаров вокруг кварталов, каждый на своём расстоянии от бордюра. У зебры человек с вероятностью 0,4 решает перейти улицу. Он ждёт у бордюра и начинает переход только на зелёный, а на мигающий уже не начинает. Роботу пешеходы уступают. Если робот стоит впереди ближе 1,7 м, человек останавливается, а через 0,6 с отступает в свою сторону, не перебегая перед роботом.
Машины и пешеходы — кинематические тела того же мира. Нас интересуют два последствия. Лидар видит их теми же лучами, что дома и деревья. А удар робота о пешехода — настоящий контакт в физике.

Маршрут строится алгоритмом Дейкстры по графу. Узлы графа — углы петель тротуаров, концы зебр, пункты A и Б.
Угол на цель навигатор отдаёт не на следующую точку, а на «морковку» — точку на линии маршрута в 2,5 м впереди робота. Это классический pure pursuit ( геометрический алгоритм управления движением мобильных роботов и автономных автомобилей, который заставляет транспортное средство следовать по заданной траектории, «преследуя» целевую точку на некотором расстоянии впереди ). Без него робот срезал к точке, до которой ещё 20 метров, уходил к бордюру и вставал перед деревьями, которые растут вдоль бордюра через каждые 8 м. С морковкой он держится середины тротуара и оси зебры.
Рядом работает судья. Он записывает финиш (центр робота ближе 1,5 м к Б), переход на красный (робот ступил на зебру, когда пешеходам горит красный), выезд на проезжую часть не по переходу и столкновения — раз в 20 мс по контактам физики.
Ради этого всё и делалось так аккуратно. Три правила:
Случайность [4] — только из генератора с зерном (mulberry32). Зерно задаётся свойством детали «Случай»: кто где появился, как сдвинуты фазы светофоров, кто решит перейти улицу.
Время — только шаги физики. Никаких Date.now() и performance.now() в модели.
Решения — на фиксированной сетке. Машины и пешеходы решают, что делать, раз в 20 мс, а двигаются на каждом шаге.
Проверяется это тестом. Одну и ту же прошивку гоняем дважды с разной нарезкой времени: по 2 мс и по 7,3 мс за вызов. Сравниваем хеш состояния мира — он должен совпасть. В бою проверка такая: на арене есть лига «Доставка», и там сервер гоняет прошивку участника в Node.js на тех же модулях, что и браузер. Хеши заездов на сервере совпали с теми, что мы получили локально, до последнего знака. А время доставки в браузере совпадает с тестом до десятой секунды.
Отрисовка сделана на своём маленьком WebGL-рендере без движков. Дома — коробки, окна рисует фрагментный шейдер по координатам: окно через 2,6 м вдоль стены и через 3 м по высоте, на первом этаже витрины. Ночью тот же шейдер зажигает около 45% окон по хешу этажа и пролёта — без единой текстуры. Камера едет за роботом и сама заходит ему за спину. Если робота закрывает дом, она поднимается; кроны деревьев на линии взгляда просто не рисуются.

Есть и вид сверху на Canvas 2D. Ему не нужен WebGL, и для отладки он полезнее объёмного: видно маршрут, сигналы всех светофоров и куда бьют лучи лидара.
Автопилот из пяти правил, который лежит в галерее примеров, прошёл все 12 комбинаций «маршрут × случай» без нарушений. Доставка занимает от 65 до 135 секунд модельного времени. Если вырезать из скетча правило светофора, судья записывает переход на красный. Потом появились уроки с автопроверкой и лига на арене. Добавились и условия: ночь, час пик с 40 пешеходами и ремонт тротуара, который приходится объезжать.
В следующей статье разберём сам автопилот: пять правил, коридор по лидару и светофор — с кодом.
Детали робота R-D6: https://pinport.ru/robot/rd6 [5]
Посмотреть код и попробовать: «робот R-D6 везёт заказ из A в Б по городу», кнопка «Запустить», регистрация не нужна. https://pinport.ru/p/ex-robot-rd6-delivery [6]
Автор: b_vova
Источник [7]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/36852
URLs in this post:
[1] обучения: http://www.braintools.ru/article/5125
[2] PinPort: https://pinport.ru/
[3] логика: http://www.braintools.ru/article/7640
[4] Случайность: http://www.braintools.ru/article/6560
[5] https://pinport.ru/robot/rd6: https://pinport.ru/robot/rd6
[6] https://pinport.ru/p/ex-robot-rd6-delivery: https://pinport.ru/p/ex-robot-rd6-delivery
[7] Источник: https://habr.com/ru/articles/1093544/?utm_campaign=1093544&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.