Пара слов в своё оправдание
В статье Я выпустил нейросеть в реальный мир — и стало не смешно в одном из комментариев попросили рассказать про железо и этот комментарий был поддержан плюсиками. Поэтому не могу не рассказать.
Почему этот робот собран так, а не иначе? Главный мой интерес был не в том, чтобы сделать качественную тележку, а в том, чтобы дать ИИ хоть какое-то тело — и посмотреть, как модели ведут себя в реальном, физическом мире. О том, что из этого вышло, и была первая статья. Эта — про само тело: как оно собрано и обо что я при этом бился.
Метод сборки — дендро-фекальный. Робот буквально собран из того, что нашлось у меня в кладовке. Отдельно докуплены только две вещи: пневматический пистолет и серва к нему. Всё остальное уже лежало и пылилось.
Поэтому это не туториал «как собрать робота дома». Это, скорее, карта граблей: на какие из них я наступил, чтобы вы могли наступить на свои. Особого смысла расписывать соединение каждого провода тоже нет смысла, это всё есть в документации к плате или датчику.
Ссылка на код будет в конце.
Из чего это собрано
Тележка — DF Robot Pirate. Дифференциальная платформа: четыре колеса, четыре мотора, рулится как танк — разностью скоростей бортов. Примерно 22 см в длину, 18 в ширину. В принципе заменяется на любую другую; я одно время всерьёз думал пустить на проект свой старый робот-пылесос.
Arduino Leonardo — мозг тележки. Мозг тупенький: почти ничего на нём не происходит, и дальше я объясню, почему так и задумано. Про саму Arduino писать не буду — вряд ли скажу что-то новое.
Дальше — то, что в разное время приехало с Амперки:
Motor Shield Plus — драйвер моторов. Управляет парой моторов; правый и левый борта у меня запараллелены. Умеет читать потребляемый моторами ток — пригодится.
Troyka Shield — продвинутая макетка. Удобно втыкать модули, есть место распаять своё.
IMU 10 DOF v2 — на борту акселерометр, гироскоп, магнетометр и барометр. Из всего набора сначала пригодился компас (магнетометр), потом от него пришлось отказаться в пользу гироскопа — отдельная история, к ней ещё вернёмся. Гироскоп L3G4250D так себе, бывают точнее.
Дальномер — лазерный Benewake TFmini-S LiDAR. Выбор был между ультразвуком и лазером. Лазер показался модным — взял его. Зря: в опытах с зеркалами ультразвук выручил бы там, где лазер откровенно врал. Но об этом позже.
Радиомодуль на 433 МГц неизвестного происхождения. Подключается к Arduino по серийному порту, в компьютер втыкается ответная часть в USB и выглядит как обычный COM-порт. Работает на 57600 бод.
Камера — Tapo C200. Особого выбора не было: тоже лежала в кладовке. Качество так себе, но для задачи хватает. Важная деталь: видео с неё идёт отдельным каналом — по Wi-Fi, мимо радиомодуля. Почему так — в следующем разделе.
Питание — пара LiPo-аккумуляторов, на 1.3 и 2.2 Ач, оба достались от каких-то игрушек.
Пистолет — Borner PM-X, реплика Макарова. Красивый, небольшой и недорогой. К нему — серва MG996R с заявленным моментом 13 кгс·см.
Косметика — напечатанная на принтере передняя панелька с матовым стеклом (вырезано из бака сломанного увлажнителя) и гирлянда из десятка светодиодов, синих и красных, на куске того, что когда-то было макетной платой.
Тупое тело, умный хост
Главный принцип всей конструкции: тело делает как можно меньше, мозг — на компьютере.
На Arduino — только то, что обязано жить рядом с железом и реагировать быстро: крутить моторы, читать датчики, плавно разгонять, отдавать телеметрию. Никакого интеллекта. Всё «думание» — зрение, навигация, решения — на хосте, на обычном ПК.
Связь устроена двумя независимыми каналами. По радио — управление и телеметрия: лёгкий поток коротких команд и чисел в обе стороны. Видео же идёт своим путём, по Wi-Fi прямо с камеры, мимо радиомодуля. Лёгкое — по радио, тяжёлое — по Wi-Fi.
Про то, что происходит на стороне ПК, я здесь почти не рассказываю — это тема для отдельного разговора. Но один кусочек хоста показать всё-таки придётся: протокол, на котором тело и мозг разговаривают.
На каком языке они говорят
Протокол я сделал текстовым и человекочитаемым — чтобы можно было подцепиться терминалом и глазами увидеть, что робот про себя думает.
Тело раз в ~100 мс выплёвывает кадр телеметрии:
---
SQ:1240 # номер пакета (счётчик с переполнением)
LS:50 # скорости левого/правого моторов
RS:50
LC:312 # ток левого/правого
RC:298
GX:-120.4 # магнетометр
GY:88.1
V:11.8 # напряжение аккумулятора
RF:610 # дальномер вперёд, мм
***
В обратную сторону — такие же короткие команды: L/R — моторам, P — биппер, F — выстрел.
Один скромный на вид параметр окажется важнее, чем кажется, — SQ, номер пакета. Он просто монотонно растёт. Но если он вдруг скакнул или обнулился — значит, плата только что перезагрузилась. Этот счётчик ещё спасёт мне не один час отладки. Полная спецификация — в src/docs/protocol.md, здесь её разворачивать не буду.
Тележка поехала
Дальше — месяц кодирования. Прошивка, аппаратные тесты, плавный разгон моторов (резко давать полный газ нельзя — мотор-редукторы дёргаются, тележка прыгает). Подбор максимальной скорости, делителя для замера напряжения.
Но тележка поехала. Вперёд, назад, повороты.
А потом начались чудеса.
Зависания
Робот ни с того ни с сего замолкал и перезагружался. Случайно, без системы. Поймать такое — худшее, что бывает: оно не воспроизводится по команде.
Спас тот самый SQ. Я смотрел в поток телеметрии и видел: номер пакета спокойно растёт, растёт — и вдруг обнуляется. Плата перезагрузилась. Не зависла в коде, не потеряла связь — именно перезагрузилась, на ровном месте.
Виноваты оказались драйверы моторов. Изначально вся электроника была собрана в “бутерброд” из четырёх плат. Ардуино, далее в нее воткнут драйвер моторов, в драйвер моторов воткнута макетка, в макетку воткнут гироскоп и товарищи. Драйвер моторов стоял слишком близко к Arduino, и наводки от моторных токов сбивали плату. Лечение нашлось грубое и физическое: разнести драйверы подальше. Помогло. Десять часов на то, чтобы передвинуть железку на пару сантиметров, — нормальный размен.
Параллельно был эпизод с искрами. Перепутал провода, откуда-то прилетели искры и появился приятный запах горелых микросхем. Хорошо, что плат Leonardo у меня было две.
Хромота
А однажды робот захромал.
Поехал — и заметно повело влево. Не чуть-чуть, а так, что мимо не пройдёшь. Я покрутил колёса руками: заднее левое идёт ощутимо туже остальных. Внутри всё опустилось — приехали. Мотор или редуктор под замену, а раз менять, то, наверное, все четыре сразу, чтоб одинаковые. Плюс заново настраивать управление под новые мотор-редукторы. Неделя коту под хвост, не меньше.
Полез внутрь — глянуть на моторы перед похоронами. А там провода лежат комом, как попало. И один проводок немного касается вала мотора. Чуть-чуть, но достаточно, чтобы притормаживать колесо.
Отодвинул провод. Колесо завертелось как новенькое.
С тех пор проводка внутри уложена аккуратно, по местам. И мораль, которую я каждый раз забываю и каждый раз вспоминаю: делай сразу хорошо.
Куда я еду?
С тем, чтобы ехать, разобрались. Дальше — куда ехать и насколько. И тут выяснилось, что в реальном мире все датчики немножко врут, и каждый — по-своему.
Компас-предатель
Дистанцию вперёд я мерил лидаром, повороты — компасом. По компасу всё выглядело логично: вот тебе магнитное поле Земли, вот стороны света, держи курс. На столе работало.
В квартире компас превратился в генератор случайных чисел.
Сначала я грешил на моторы — они и правда наводят своё поле. Но дело было глубже. Я написал скрипт, который строил карту магнитного поля (ну почти карту). Карта вышла прекрасная: поле гуляло как хотело — арматура в стенах, проводка. Никакого ровного «на север» там не было и в помине. Для абсолютной навигации по компасу квартира оказалась непригодна в принципе.
Компас отправился в отставку.
Гироскоп и его капризы
На замену пришёл гироскоп. Он не знает, где север, зато честно говорит, с какой скоростью ты поворачиваешься. Интегрируешь угловую скорость по времени — получаешь, на сколько повернул. Сначала я подмешивал к нему компас комплементарным фильтром, потом плюнул и оставил чистый гироскоп: в квартире от компаса было больше вреда, чем пользы.
Гироскоп L3G4250D тоже с характером. Во-первых, спайки — случайные выбросы на ровном месте, которые при интегрировании превращаются в накопленную ошибку. Лечится медианным фильтром. Во-вторых, дрейф нуля: даже стоя на месте гироскоп считает, что чуть-чуть поворачивается. Перед каждым стартом робот теперь пару секунд стоит неподвижно и замеряет этот фон, чтобы потом его вычитать. Сначала я брал на калибровку 20 сэмплов — оказалось мало, датчику нужно время устаканиться. Поднял до 200, и повороты стали стабильными — точность около трёх градусов.
И классика жанра, без которой никуда: робот упорно поворачивал не в ту сторону. Просишь направо — едет налево, и LLM этому удивляется. Знак угловой скорости был перепутан. Поменял знак — поехал куда просили.
Ковёр против ламината
Казалось бы, поворот на 90° — он и есть 90°. Но на ковре робот ворочается тяжелее, чем на ламинате, инерция и проскальзывание разные. Пришлось добавить защиту от застревания (если робот уже стоит, а угол не добран — поддать газу) и подобрать скорости так, чтобы ошибка держалась в пределах трёх градусов на любой поверхности.
С ездой по прямой — та же физика. Робот незаметно уводило в сторону, и я включил коррекцию по гироскопу: если в процессе движения накопилось отклонение от курса — подравниваю моторы разностью скоростей. Поехал заметно ровнее.
Чтобы всё это не зависело от задержек радиоканала, управление поворотами и движением я в итоге перенёс на саму плату — пусть крутит свой регулятор 50 раз в секунду локально. Запомнился баг, когда из регулятора по ошибке выпала одна проверка, и робот вместо плавной доводки угла срывался в неуправляемое вращение. Выглядело эффектно.
Дальномер и его обманы
Лидар заслуживает отдельной главы — он обманул меня дважды, и каждый раз по-новому.
Первый обман был технический. Лидар висел на программном последовательном порту, и данные приходили с заметной задержкой. Данные иногда застревали, иногда приходили “битые”. Бороться с этим в одиночку Arduino не успевала. В итоге я отдал лидар отдельной маленькой плате — Arduino Pro Micro, — которая только и делает, что опрашивает дальномер на полной скорости и по запросу отдаёт основному мозгу свежее число по шине I2C. Лаг ушёл.
Второй обман был коварнее — геометрический.
Лидар стоял сантиметров на десять ниже камеры. Камера смотрит вперёд и видит кухонный шкаф. А луч лидара, пущенный горизонтально с уровня пониже, проходит под этим шкафом и утыкается в стену где-то за ним. Камера говорит: впереди препятствие. Лидар говорит: свободно, метра два. Робот верит цифре — и упорно лезет под шкаф.
Особенно показательно это вышло у Grok: он был железно уверен, что путь открыт, и раз за разом таранил шкаф, не понимая, что происходит. Лечится переносом лидара вверх, к уровню камеры, — чтобы они смотрели в одну точку.
А третий обман я предвидел заранее, ещё на этапе выбора. Лазер не дружит с зеркалами: луч уходит «за» зеркало и возвращается длиной вдвое больше реальной. В опытах с зеркалами это постоянно сбивало картину. Вот тут ультразвуковой дальномер был бы честнее лазерного — он бы просто отразился от стекла. Но лазер показался модным. Запомним.
Пистолет, который съел неделю
Роботу нужно не только смотреть и говорить, но и как-то влиять на мир. Манипулятор — сложно и долго. Пистолет — просто: спустить курок может и серва. Так я думал.
Планировал на всё про всё один день. Потратил пять.
Первая засада — усилие на курке. Оно оказалось неожиданно большим, и имевшаяся серва не тянула его вообще. Заказал серву помощнее, MG996R, с моментом 13 кгс·см — этой хватило.
Дальше — механика передачи. Первый вариант: ось сервы совпадает с осью курка, тянем через качалку. Качалка под нагрузкой изгибается, усилие уходит в её прогиб — ненадёжно. Второй вариант, он же финальный: серва уезжает назад, а курок тянет вилка-тяга, симметрично, на растяжение. Заработало.
Но тут вылез сюрприз посерьёзнее. При выстреле алюминиевая пластина шасси заметно изгибалась — усилие сервы было велико. Пришлось добавить ребро жёсткости, а из-за него — переставлять и перекомпоновывать камеру с лидаром. И всё это с бесконечными итерациями 3D-печати: напечатал, примерил, не сошлось, поправил модель, напечатал заново.
В прошивке пистолет живёт тихо: серва сидит на отдельном таймере, чтобы не драться с моторами за железо, и срабатывает по короткой команде, не блокируя основной цикл. Это как раз было просто. Сложным оказалось железо.
Лицо и подсветка
Чистая косметика, но без неё робот — просто этажерка на колёсах.
Спереди — напечатанная на принтере панелька-морда с сотовым узором и матовым стеклом. Стекло я вырезал из бака сломанного увлажнителя: жалко было выбрасывать, пригодилось.
Подсветку — десяток светодиодов, синие и красные — я ставил поначалу просто для красивых фотографий: пусть робот в кадре светится. А потом подумал: чего я сам ими управляю? Отдал управление роботу. В одной из сессий из первой статьи это сыграло неожиданно — модель сама выбрала себе цвет и сама же объяснила почему («холодный, как мой металлический характер»). Но это уже про мозг, не про тело.
Камеру я уже поминал — Tapo, из кладовки, качество так себе. Зато круглая белая голова неплохо смотрится и придаёт всей конструкции что-то живое.
Что я из этого вынес
Если коротко.
Половина проблем была не в коде, а в физике: наводки, питание, провод, прилипший к валу. Датчики врут, и врут по-разному — компас в квартире бесполезен, лазер не видит зеркал и лезет под шкафы. Механика съедает время непропорционально обещанному: то, что на бумаге один день, в железе оборачивается неделей. И реальный мир всегда жёстче любой модели у тебя в голове.
Всё это, в итоге, — просто рабочее место для ИИ. Тело, чтобы было чем смотреть, ехать и дотягиваться до мира. А о том, что в этом теле делает сам ИИ, как он себя в нём ведёт и что из этого вышло, — была первая статья.
Осталась третья сторона — мозг на компьютере: зрение, навигация, агентный цикл, монтаж записей сессий. Про неё я здесь сознательно почти промолчал. Вопрос к вам: интересно ли про эту, хостовую часть? Если да — напишите в комментариях, расскажу.
Там же лежит написанный Клодм файл CLAUDE.md с пояснениями, что и как устроено.
Автор: stg34


