Мы создали симуляцию 3D‑города с машинами, пешеходами и светофорами, по которому ездит шестиколёсный робот под управлением настоящего скетча Arduino. Все это для обучения детей и взрослых в кружках робототехники.

У нас есть онлайн‑лаборатория PinPort: в браузере собираешь схему, пишешь скетч для Arduino, и он исполняется на эмуляторе микроконтроллера. Светодиоды и датчики там давно, потом появились роботы. В этот раз задача была такая: сделать виртуальный город с улицами, машинами, пешеходами и светофорами. По нему ездит робот‑доставщик, и его задача — довезти заказ из пункта A в пункт Б.

Робота назвали R‑D6: шесть колёс, разворот на месте. Управляет им не скрипт на JavaScript, а обычный скетч для Arduino Uno. Скетч собирается на сервере через arduino‑cli, а полученная прошивка исполняется покомандно в эмуляторе ATmega328P. Ниже расскажу, из чего всё это состоит.

Три части успеха

У системы три части, и они движутся строго вместе:

  1. Плата. Эмулятор AVR исполняет прошивку с частотой 16 МГц: таймеры, ШИМ, I2C, UART.

  2. Физика. Робот — твёрдые тела в Rapier: корпус, шесть колёс, моторы. Шаг мира — 2 мс.

  3. Город. Светофоры, машины и пешеходы — своя логика, которая задаёт положение кинематических тел в том же мире Rapier.

Плата и физика идут поочередно: две миллисекунды процессора, затем один шаг мира. За эти две миллисекунды придуманный нами класс собирает с выводов платы скважность ШИМ на ENA/ENB и уровни IN1–IN4 драйвера L298N и превращает их в напряжение на моторах. В обратную сторону идут показания датчиков: цифровые пины попадают на выводы D2/D3, лидар и навигатор — в регистры устройств на шине I2C.

Почему именно так? Из‑за повторяемости. Если связать слои через реальное время, тот же скетч на слабом ноутбуке и на сильном десктопе поедет по‑разному. У нас есть уроки с автопроверкой и арена, где заезды считает сервер, так что результат обязан быть одинаковым везде.

Датчики как файлы регистров

В жизни лидар и навигатор общались бы с контроллером по UART со своим протоколом. Мы взяли самую простую для школьника схему — файл регистров на I2C. Первый байт записи задаёт номер регистра, дальше чтение идёт подряд, как у MPU6050 или DS3231.

Устройство

Адрес

Что отдаёт

Как часто обновляется

Лидар

0×52

36 секторов по 10°, расстояние в см (uint16), номер оборота

10 раз в секунду

Навигатор с камерой светофоров

0×53

флаги, сигнал перехода, номер точки, курс, угол на цель, расстояния, скорость, координаты

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 мс по контактам физики.

Детерминизм: один скетч, один хеш

Ради этого всё и делалось так аккуратно. Три правила:

  • Случайность — только из генератора с зерном (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

Посмотреть код и попробовать: «робот R‑D6 везёт заказ из A в Б по городу», кнопка «Запустить», регистрация не нужна. https://pinport.ru/p/ex‑robot‑rd6-delivery