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

Для начала определимся с исходными данными. Как у программиста, у меня был крайне разнообразный опыт (сейчас я занимаюсь инфраструктурой рантайма больших языковых моделей в Яндексе, в том числе для Поиска и Нейро). Но в робототехнике опыт был почти нулевой (играл в LEGO MINDSTORMS).

Однажды я увидел, как робот ребят из MIT собирает кубик Рубика за 0,38 секунды. Пересмотрев ролик в замедленной съёмке, решил, что в их решении есть что оптимизировать и этот рекорд можно побить. Как раз в то же время я оказался в окружении людей, которые занимаются робототехникой, поэтому интерес к проекту в целом поддерживался, да и разнообразное оборудование было в доступе.

Из этой статьи вы узнаете, как мне удалось превратить сырую идею в новый рекорд, не обладая необходимым опытом и совершая ошибки на каждом возможном этапе. Моя история, пожалуй, хорошо иллюстрирует высказывание «Дорогу осилит идущий».

Планирование

Чтобы собрать кубик Рубика, необходимо выполнить три простых действия:

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

  2. Найти решение. Для этого я нашёл довольно популярный двухфазный алгоритм Коцембы. Работает быстро, находит субоптимальное решение — меня вполне устроило.

  3. Собрать сам кубик. Самая, пожалуй, сложная часть. Робототехника в основном происходит именно здесь. Про это и будет в основном мой рассказ.

Довольно очевидная схема, было забавно собрать её с помощью PlantUML
Довольно очевидная схема, было забавно собрать её с помощью PlantUML

Решать задачу я начал с конца, так как если не будет на чём ехать, то, вне зависимости от алгоритмов (только если кубик не находится в уже собранном состоянии), ничего не поедет. Одновременно игрался с переписыванием оригинальной реализации солвера с Python на компилируемые языки (C++ и Rust), но в итоге нашёл, что до меня это уже хорошо сделали. С частью CV быстро понял, что руками выставлять коэффициенты, где какой цвет, работает плохо, и оставил её на потом, так как уже было понимание: с помощью робота можно будет собрать датасет.

Первые опыты, поиск двигательной базы

Итак, задача — нужно повернуть одну грань кубика Рубика на 90º. А если можно, то уметь это делать со всеми шестью гранями (фактически достаточно пяти, но в таком случае это требует чуть более длинной последовательности сборки).

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

Вариант первый — Maxon Motor

Если интересно, вот компонентная база: драйвер, мотор, энкодер, редуктор.

Моторы отличные, работать с ними мне понравилось, однако они шли в неразборном корпусе с редуктором и их номинальная скорость с учётом передаточного числа была 8040/(299/14)/60 \approx 6.27 rev/s. А хотелось бы как минимум что‑то в районе 15 мс на четверть оборота, что без учёта ускорений примерно в три раза больше. Крутящего момента, я думаю, у них бы хватило, но до чисел не добрался.

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

Прикинул необходимое отношение (не помню уже какое, кто хочет, может посчитать зубцы шестерней), обнаружил такую прикольную концепцию, как планетарная передача, и пошёл делать. Из основного доступного есть пара 3D‑принтеров у соседа по общаге, FDM и SLA.

Задизайнил редуктор в OpenSCAD, напечатал:

​​И дальше мучался с тем, что мотор попросту проворачивался в детали из алюминия, которая передавала вращение «планетам» через ещё одну промежуточную SLA‑деталь:

В итоге я просто посадил на клей (фиксатор резьбы) вал прямо в SLA‑деталь — держать стало добротно, но, чтобы снять, приходилось раскалывать деталь. Это меня устраивало, так что я получил в целом рабочее начало:

На этом моменте сложилось понимание, что кубик собрать в целом можно. Ну наконец! А то я уже начал сомневаться. Хотя редуктор получился какой‑то сомнительный, как будто с ним даже медленнее, чем без него…

Вариант второй — сервопривод из подручных материалов

Не успел я победить один редуктор другим, как мне попались интересные железки — ODrive v3.6. На этом моменте я узнал, как по факту работают драйверы из предыдущего раздела, и в целом понял, что сейчас все подобные высокопроизводительные задачи делаются на основе FOC + BLDC/PMSM.

Здесь для теста мне понадобилось:

  1. 1x ODrive v3.6 — драйвер, чтобы управлять моторами с двумя каналами.

  2. 2x AS5048 — энкодеры для обратной связи, чтобы драйвер знал, на какие обмотки подавать какой ток.

  3. 2x T‑MOTOR U8 Lite KV85 — сами моторы, какие‑то китайские BLDC для огромных дронов.

  4. Пара магнитиков на ротор моторов — нужны для энкодеров.

Всё было довольно быстро соединено Dupont‑проводами, от моторов до кубика были напечатаны валы на SLA‑принтере (на этот раз дизайн уже во Fusion 360), и оно поехало. Вернее сказать, полетело (моторы‑то для дронов):

Выполняемая последовательность движений — R F’ R’ F R F’ R’ F. Намного быстрее, чем в предыдущем варианте. Примерно на этой скорости я и летал почти до самого конца.

Собираем кубик по-настоящему

Умножаем то, что было, на три, меняем Dupont на наглый навесной монтаж и получаем:

Приводим к нормальному виду

В процессе сборки полного комплекта из шести моторов стало понятно, что Dupont‑провода не выглядят как промышленное решение, напаивать навесным монтажом тоже не очень прикольно, поэтому было принято решение развести печатную плату. К ней можно и энкодеры красиво подключить, и вспышку для камер (пока игрался с CV, оказалось, что я очень хочу стабильное освещение), и даже CAN‑шину к контроллеру (ведь я хочу управлять моторами с минимальными задержками, поэтому взял попавшийся ESP32). Получается такая плата расширения, которая просто ставится на ODrive:

Ох, как же я тогда ошибался… Но я об этом в тот момент не знал, и поэтому всё было ок. Вспышка тут включалась через мосфет между светодиодами и стабилизатором напряжения. Таким образом, получался ШИМ‑димминг, причём димминг через оптопару, изоляция управляющей электроники от напряжения в 15 вольт. Почему бы и нет, правильно же?

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

Немного компьютерного зрения

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

Запускаем робота на пару часов работать и получаем датасет из изображений подобного плана, снятых с камер PlayStation Eye под вспышкой:

Руками выставлять промежутки по HSV не очень хочется, да и нужно это сделать для каждого отдельного элемента, ведь, как оказалось, одни и те же пиксели в разных частях изображения могут означать разные цвета. Ох уж эти камеры, не предназначенные для корректной передачи цвета, и неравномерное внешнее освещение…

Но не беда, имея N картинок в датасете и зная, где какой цвет, можно простыми булевыми операциями с пороговыми значениями получить маски для каждого отдельного элемента, по усреднённым цветам из которых образуются аккуратные кластеры. В наше время слова «Machine Learning» вызывают больше ожиданий, чем такая простота, однако это оно и есть.

И всё, на этом с распознаванием закончили. Стоит упомянуть только, что в процессе всего этого обработка была переписана с Python на Rust и стала занимать <0,5 мс (ускорение в 100 раз, но есть гипотеза, что я раскрыл не весь потенциал обоих языков). Драйвер для используемых камер тоже был написан свой на Rust, так как я хотел избавиться от всех лишних буферов, чтобы получать картинку как можно быстрее.

В итоге такой удивительно простой алгоритм калибруется на новом месте за 5 минут (собирая датасет из 200 картинок) и при стабильности освещения распознаёт кубик корректно в 100% случаев. Позже я поменяю светодиоды и узнаю, что он работает даже с худшими данными, где много теней и бликов.

Почему бы не переделать всё

Мне не нравилось, что провода энкодеров должны правильно лежать, чтобы работать. Не круто это. Также оргстекло начало трескаться, пора бы заменить его чем‑то посерьёзнее. Да и на ESP32 нет настоящего USB, только UART, а он медленный.

Код с ESP32 был переписан под Teensy 4.0, причём опять же на Rust, потому что к этому моменту я уже был сконвертирован в краба, да и задача у меня требовала blazingly‑fast‑технологий.

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

Итак, что было сделано:

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

  • Плата расширения для ODrive стала намного проще, так как я отказался от лишней электроники на ней (для неё пока оставил прошлую плату).

  • Вместо интернетной витой пары использовался полноценный USB‑C-провод (со всеми высокоскоростными каналами).

USB‑C был выбран по простой причине — отличная штука. И витых пар достаточно, чтобы каждый сигнал завивать с землёй (ну или с противофазным сигналом, если бы заморочился). И ещё и все эти пары должны быть вроде как экранированы, да и вообще до какой‑то степени хорошо справляться с помехами.

С USB‑C-разъёмами было сложно. Поскольку мне нужны были почти все витые пары в проводе, мне нужны были и полноценные разъёмы на все USB‑C-пины. С их пайкой я справлялся довольно сомнительно, причём на один разъём уходила куча времени, да ещё и возникали где‑то под корпусом мосты. Вторую половину просто сдал в ремонт телефонов, там более опытные ребята мне всё сделали.

Внезапно с проводами было ещё сложнее. Из требований: метровая длина, все контакты, пассивный кабель. Это довольно сложная задача. Большинство длинных проводов, которые можно найти, ограничиваются одной витой парой и питанием. Если хочешь что‑то получше, будь добр, заплати, а там, возможно, тебе дадут ещё активный кабель, который сделает с твоим сигналом неизвестно что (у меня же не дифференциальный сигнал, да ещё и напряжение другое). Повезло: после нескольких попыток на одном из маркетплейсов нашёл кабели, которые работали хорошо.

Рама робота была просто подменена на стальную, оказалось даже, что сталь резать дешевле, чем оргстекло (вероятно, зависит от исполнителя), и робот стал выглядеть уже максимально серьёзно:

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

Где‑то на этой же итерации проекта моторы были затюнены так, что собирали кубик меньше чем за 300 мс, делая робота быстрейшим в мире (если ориентироваться на действующий мировой рекорд):

Здесь 1 кадр = 1 миллисекунда, запись на Sony RX100 V

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

Кроме этого, в процессе настройки моторов и большого стресса на компоненты один из SLA‑валов всё‑таки разлетелся:

Программная часть

Кроме разговоров о hardware, нужно упомянуть и software‑часть, ведь без неё ничего бы в целом не работало. Здесь, пожалуй, буду краток, ведь не так много всего произошло.

Первые несколько итераций, в основном тестовые варианты, были написаны на Python (host часть) и C++ (embedded часть на ESP32, ESP‑IDF FreeRTOS). В конечном итоге всё было переписано на Rust, а на Python остался только один notebook, в котором я экспериментировал с алгоритмом для распознавания кубика.

Embedded у меня на Teensy 4.0 с легковесным async‑фреймворком embassy. Общение с хостом через нативный USB 2.0 на контроллере — данные передаются быстрее и надёжнее, чем по популярной схеме с UART и конвертером его в USB. В качестве протокола у меня реализован простой синхронный RPC поверх postcard — вполне приятного бинарного формата, он быстрый и экономный. Ранее я использовал serde_json, не столь подходящий для embedded, он у меня занимал примерно половину от всего бинарника, а память на контроллере очень ограничена.

В конечном итоге запрос‑ответ с вызовом пустого метода на контроллере занимал 90 микросекунд с учётом всех накладных расходов на хосте и т. д. На всю сборку кубика мне достаточно двух запросов, так что на этом я решил, что хватит оптимизаций. Не знаю, сколько ещё микросекунд можно срезать, но для такого результата мне уже пришлось отключить Turbo Core, потому что он создавал случайные задержки от 0,1–0,5 мс, что мне очень не нравилось.

Со стороны хоста было написано достаточно много вспомогательных программ, например для отображения изображения с камер или для калибровки PID регулятора моторов (на графики особо не смотрите, проблемы с контроллером тока и овершутинг поправили).

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

Open Sauce 2024

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

Заказал новую раму, на этот раз из оцинкованной стали, чтобы не ржавела. Нашёл ребят, которые сделали мне валы из алюминия. И переделал плату расширения для Teensy 4.0, теперь охлаждение ODrive питается прямо с неё, да и для светодиодов поставил драйверы специальные, чтобы управлять как надо — по току, а не по напряжению (у них и встроенный ШИМ‑димминг есть, который работает точнее, чем предыдущая схема):

Приехал в Калифорнию, сел в отеле и так в нём сидел. Паял, программировал, тестировал — всё делал, только не гулял. Это у меня был отпуск.

За пару дней до выставки тюнил моторы (а то назвался быстрейшим, а пока ехал до Америки, ребята из Mitsubishi Electric обновили рекорд, а значит, нужно обгонять и отрываться) и внезапно обнаружил, что один из моторов перестал корректно работать. Времени разбираться не было, поэтому так и выступал. Благо для сборки кубика достаточно пяти моторов, и в такой конфигурации робот был достаточно быстр, чтобы никто ничего не понял. Заметил подвох только один мелкий пацан: спросил, почему одна грань не крутится, респект ему за внимательность.

Там же на Open Sauce я познакомился с Оскаром из ODrive Robotics. Он мне предложил коллаборацию: они мне дают драйверы поновее, помогают внутренними инструментами и опытом с их настройкой, и робот становится ещё быстрее. С моей стороны ничего как будто не требовалось, только зарегистрировать рекорд, что я и так собирался сделать. Кроме этого, там же нашёлся парень с крутой слоумо‑камерой, которая давала картинку получше, чем у меня:

Под конец выставки кубик уже подустал, смазка потеряла свои свойства, его начало подклинивать, из-за чего логика синхронизации соседних сторон немного поехала, и были получены красивые кадры того, что называется надёжностью:

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

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

Мировой рекорд

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

  • Кубик и запутывание должны соответствовать правилам World Cube Association.

  • Камеры не должны видеть больше одной грани кубика до начала отсчёта времени.

  • Во время входит всё: от распознавания кубика до его полной сборки.

  • Нужно найти двух независимых свидетелей и двух опытных хронометристов.

Следующие пару недель я провёл также в отеле: настраивал, писал код и дизайнил. Нужно было поменять драйверы с ODrive v3.6 на ODrive Pro, поменять энкодеры с кастомных на AMT212B (он подключается к ODrive по RS485 — правильный дифференциальный сигнал, а не как было). Эти энкодеры уже насаживаются на вал, пришлось ещё придумывать вал из подручных средств.

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

Здесь я уже по‑серьёзному срезал миллисекунды везде, где мог: запускал поиск решения на компе помощнее, менял протокол взаимодействия с Teensy, разгонял процессор, чтобы уменьшить задержки ответа по USB (RTT при включённом AMD Core Performance Boost занимал до 0,5 мс и плавал, при выключенном — стабильно меньше 0,1 мс), настраивал пороги резки углов, оптимизировал работу с CAN‑шиной.

Спустя некоторое время настройки был получен такой результат: здесь примерно 160 мс сборка кубика и ещё 20 мс на CV и алгоритмы, что в сумме даёт 180 мс рекордного времени. Забавно, что даже в слоумо с замедлением в 40 раз всё равно всё очень быстро:

Его можно было ускорять ещё, но времени оставалось очень мало: договорились ставить рекорд 5 июля 2024 года. Поэтому оставил как есть. Из очевидного, что я не успел сделать, — можно модифицировать поиск решения так, чтобы он учитывал конкретные возможности конкретного робота (например, что поворот на 180º делается в полтора раза дольше, чем на 90º), или хотя бы иногда поворачивать грань в другую сторону на угол -180º вместо 180º (это позволяет лучше резать углы).

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

Итак, мы собрались в Noisebridge, всё настроили, поставили, нашли необходимых людей (необходимых по правилам людей нашли прямо в хакерспейсе), откалибровали камеры и два раза зафиксировали рекорд (однако робот был в чуть более медленном режиме для надёжности):

На самом деле было сделано две попытки: можно посмотреть общий план первой и второй. Однако в первый раз я забыл включить замедленную съёмку, поэтому пришлось сделать ещё одну попытку. А я уже обрадовался, что из 0,2 секунд вышли.

После чего доказательства были отправлены в Книгу рекордов Гиннесса. Удастся ли мне в итоге туда попасть — покажет время, но я как минимум впервые получил независимое подтверждение скорости моего робота.

Позже, когда отдохнул от инженерной гонки на время, обнаружил несколько вещей:

  • Я запускал рекордный забег с не самой быстрой конфигурацией по коду, можно было бесплатно выиграть 1–2 мс.

  • Комбинация попалась jдна из худших, я уже постфактум (зная статистику от робота) посчитал распределение времени сборки в текущей конфигурации, и оказалось, что не повезло:

Кроме того, что я упустил в сделанном рекордном запуске:

  • Помним про то, что робот умеет работать несколько быстрее, хотя у меня нет статистики по таким сборкам, чтобы построить графики.

  • Появились идеи, как намного ускорить поиск решения, и нашлись камеры побыстрее — я ожидаю сэкономить 5–10 мс в сумме, но пока не тестировал.

  • Я несколько поменял конфигурацию поиска решения и получил:

Таким образом, рекорд сразу можно пытаться улучшить и поставить 0,16 с или даже 0,15 с.

Может быть, я ещё как‑нибудь ускорю робота и обновлю рекорд, но пока это только в мечтах.

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

P. S. Узнал про технологию NeRF, которая умеет хорошо восстанавливать сцену по видео. Для неё есть открытое GUI и прикольное демо с моим роботом.

Комментарии (0)