Привет, Хабр! История о том, как желание «покрутить слайдеры» на настольном манипуляторе превратилось в глубокий реверс‑инжиниринг, полное переписывание бинарных прошивок и систему управления, которой напрямую командуют ИИ‑агенты через MCP.
Речь о myCobot 280 от Elephant Robotics (версия на базе M5Stack Basic) — популярный 6-осевой кобот для вузов и R&D. У него есть огромная беда: официальное ПО. Родной софт страдал от разрывов связи, пропуска кадров, зависаний шины и багов EEPROM. А главное — исходников прошивки эффектора Atom не существовало в принципе: только закрытые бинарники. Пришлось вооружиться описанием протокола, даташитами на сервы Feetech и ESP32-PICO, чтобы восстановить и расширить функционал с нуля.

В итоге — экосистема из четырёх частей:
кастомные прошивки Базы M5 и Atom;
асинхронный бэкенд Python + WebSockets со сквозным аппаратным эмулятором;
3D Web‑UI на Three.js;
нативный MCP‑сервер для ИИ‑агентов.
1. Архитектура системы и сквозной эмулятор

Главный принцип: симулятор — не отдельный слой, а точная виртуальная копия робота на уровне протокола. Бэкенд общается с роботом бинарными кадрами FE FE | len | cmd | data | FA — и виртуальный робот (backend/emulator.py), и железо принимают одни и те же байт‑массивы. Когда вы переключаете тумблер SIM / REAL в интерфейсе, бэкенд не меняет логику вызовов API. Он просто решает, отправлять ли сформированный кадр в физический транспорт (USB/Wi‑Fi) или скормить его локальному классу RobotEmulator, который тут же обновит состояние виртуальных сервоприводов, рассчитает прямую/обратную кинематику (DH‑матрицы) и выдаст ответный кадр «за 0 миллисекунд». Для UI разницы нет — телеметрия приходит одним и тем же путём.
2. Железо и реверс‑инжиниринг: борьба с коллизиями на шине
Это была самая тяжёлая часть работы. Открытых исходников прошивки Atom не нашлось ни у производителя, ни у энтузиастов — только закрытые бинарники. Выручили найденная принципиальная электрическая схема платы и описание опкодов протокола; всё остальное — стенд.
Топология, которую пришлось выяснять заново. Робот состоит из Базы M5Stack Basic (ESP32-D0WDQ6) и эффектора ATOM Matrix (ESP32-PICO‑D4), связанных Serial2 на 1 Мбод, 8N1 — на стороне базы это RX/TX = G16/G17. Но главная находка реверса в другом: у Atom нет отдельного серво‑UART. Те же два провода, те же 1 Мбод — это и линия связи с Базой, и шина сервоприводов Feetech. Я перебрал все свободные пары, single‑wire, шесть скоростей, DE‑пин, RS485-режим, enable‑пин — ноль. Пока не отправил на G25/G21 сырой Feetech‑READ: пришло FF FF 01 04 00 87 07 6C — ответ сервы ID1, позиция 1927. Дальше всё сложилось.
Итог: в одном потоке байт живут два несовместимых протокола и каждый игнорирует чужой заголовок. При этом мастеров два: База — мастер для Atom, Atom — мастер для серв. Электрически одновременно говорить нельзя, поэтому вся дальнейшая работа — это про арбитраж и терпимость к чужому трафику.
Что ломалось в стоке. База там не диспетчер, а слепой мост в отдельной задаче FreeRTOS, и перед каждой транзакцией она просто сливает приёмный буфер. Любой кадр не в такт уничтожается молча вместе с чужим ответом, а пришедший позже — ловится как ответ на следующий запрос. Плюс протокол разрешает слейву 500 мс, а библиотека ждёт 40, и Atom на мёртвой шине блокировал свой loop() почти на 540 мс (6 серв × 3 ретрая × 30 мс). Итог —get_coords() → None, «шина молчит», ложное снятие торка.
Арбитраж. Вместо моста — правило: пока говорит хост, База молчит. Хост активен, если открыт TCP‑клиент или за последние 3 с пришёл хоть один валидный кадр; ещё два глухих окна — 5 с после старта и всё время самодиагностики Atom.
last_rx_time = millis(); // любой кадр хоста в handleUserFrame() bool hostActive = (millis() - last_rx_time) < 3000; bool hostConnected = (tcpClient && tcpClient.connected()); if (!quiet && !hostConnected && !hostActive && millis() - lastPoll > 500) { /* свой 0x20 */ }
Опкоды как единственная документация. Восстановленный набор оказался шире публичного: официальное описание покрывало лишь часть. Из за жёсткого правила — оставаться совместимыми со стоковым pymycobot историю расширений пришлось переделать дважды: LED‑команды сначала жили в 0x82–0x85, а это официальные SET/GET_TOOL|WORLD_REF; кнопка Atom конфликтовала с SET_ELETRIC_GRIPPER. В итоге кастомные команды (пиксель/заливка/очистка/яркость матрицы, SG90, GPIO, калибровка set/save/get) пришлось впихивать в свободные окна. Своё добавил и на уровне Базы: экран с чанками изображения по 32 байта, состояние экрана, состояние аварийки, отчёт самодиагностики и прочее.
3. Фронтенд: 375 КБ статики, один сокет и никакой сборки

С него всё и начиналось. За референс был взят привычный мне стиль панели управления XArm к которому привык. Особенно мне нравился режим симулятора, в котором можно было безопасно тестировать новые программы. Но без подключенного робота им нельзя было пользовать. Поэтому был придуман независимый эмулятор, которым можно пользоваться даже без подключенного робота. Соблазн взять React с Vite был, но интерфейс здесь — тонкий клиент: вся логика уже на сервере (эмулятор, кинематика, ProgramRunner). Поэтому фронт — ванильный JS, который отдаёт тот же самый сервер: семь файлов, 375 КБ, ноль зависимостей, ноль сборки. Правка интерфейса — это перезапуск сервера, а не npm install.
Один канал. Всё общение идёт по одному WebSocket на тот же хост: ответ на команду возвращается с тем же action, что и запрос, а состояние сервер пушит сам — 12,5 Гц (80 мс). Частота подобрана под 3D: этого хватает, чтобы рука ехала плавно, и при этом браузер не жрёт ядро.
3D без glTF. К счтастью 3D‑модель нашлась на сайте производителя и не пришлось рисовать ее в ручную. STEP‑модель заранее разобрана на 9 бинарных мешей (~5,7 МБ: позиции, нормали, индексы) плюс links.json с pivot/joint/axis и цветами звеньев. Фронт забирает метаданные (/mesh/info/<имя>), потом ArrayBuffer, и собирает геометрию напрямую — парсить glTF в браузере не нужно. Шарниры при этом едут по той же DH‑модели, что и в прошивке: 3D — не «примерно похожая картинка», а тот же расчёт.
Экран и LED — это текстуры. Экран робота 320×240 рисуется на canvas и уходит в THREE.CanvasTexture на отдельный материал меша (корпус, чёрная рамка и сам экран — три материала на одной геометрии). Перерисовка — по «подписи» состояния: изменилось — рисуем, не изменилось — спим. Синхронная перерисовка матрицы 5×5 и экрана по 16 раз в секунду давала ровно те рывки, которых на железе не было.
Язык и тема — без перезагрузки. Словарь на три языка, разметка помечена data-i18n, хук onI18nChange заставляет канвас и инспектор перерисоваться на лету; тема — CSS‑переменные. И то и другое лежит в settings.json, поэтому переживает перезапуск.
Где это живёт. Окно — pywebview поверх WebView2. Если рантайма в системе нет, тот же интерфейс открывается в браузере: фронт не знает, в каком окне он работает.
Итог по цифрам: 375 КБ статики (app.js 101, ide.js 79, style.css 68, i18n.js 56, index.html 30, 3D‑модули 40), один сокет, из внешних библиотек — только three.min.js, локально, без CDN.
4. Сток‑совместимость и ретранслятор портов
У железа есть ограничение: Wi‑Fi стек робота в режиме TCP‑сервера (:9000) может стабильно держать только одно активное соединение. Если открыт Web‑UI, запустить параллельно Python‑скрипт было невозможно — робот слал RST.
Я развернул на бэкенде RelayServer (порт:9001) — мультиплексор. Любые сторонние утилиты (даже оригинальный pymycobot) подключаются к бэкенду на:9001, кадры прозрачно прокидываются сквозь ядро, не конфликтуя с WebSockets веб‑интерфейса. Так UI и внешние инструменты работают одновременно.
Дополнительно: робот сам сообщает свой IP хосту. Хост заранее говорит роботу адрес UDP‑колбэка (опкод 0xB5), и когда робот подключается к новой сети (перешёл из AP в STA) — шлёт хосту UDP‑пакет {ip, port} на :9002. Интерфейс подставляет найденный IP в поле подключения — никакого ручного поиска.
5. Кинематика изнутри: один решатель на всё

Центр всей математики — пара функций kin_fk() / kin_ik() в backend/emulator.py. Это восстановленная с нуля DH‑модель робота (standard DH), которую я сверил с эталонными 11 позами стокового робота: ошибка положения после сверки составила 6,3 мм RMS при вылете ~400 мм (~1,5%) — то есть в пределах механического допуска пластикового манипулятора с редукторами 1/345 и люфтом.
Численный IK — демпфированный Гаусс‑Ньютон (аналог псевдо‑инверсии Якобиана с адаптивным шагом и штрафом за выход из лимитов):
цели X/Y/Z сходятся с допуском 1 мм, ориентация rx/ry/rz — 2°;
учитываются лимиты суставов (
joint_min/joint_maxиз конфига) — решение не уходит за границы;при недостижимой или сингулярной цели решатель возвращает отказ, а не гонит руку в мусор: в прошивке это
0xFF, в ядре —converged: False.
Ключевой момент — паритет SIM/REAL на уровне математики: прошивка Atom и ядро используют один и тот же алгоритм с одним набором DH‑констант, поэтому ответ ik_solve в симуляторе совпадает с тем, что сделает реальный робот. Прогнать траекторию, посчитать круг (plan_circle) и проверить достижимость можно ещё до того, как рука сдвинется хоть на миллиметр — соответствующие инструменты ничего не двигают.
Известные границы: численный решатель вырождается у вертикальной позы (гимбал‑лок по осям rx/rz) — там он отказывает вместо того, чтобы дёргать суставы. Решение — аналитический IK, это открытая задача.
Последний штрих надёжности (на сервере): когда программа MiniIDE исполняет блок линейного перемещения (координатная поза), исполнитель обязан проверить результат IK — и если он не сошёлся, программа останавливается с явной ошибкой «поза недостижима», а не «успешно» проигрывает дальше с несъехавшей рукой.
6. OTA: прошивка базы по воздуху

Для удобства в кастомной прошивке появился HTTP‑апдейтер — обновление прилетает по Wi‑Fi.
Никаких сторонних сервисов: на базе поднимается WebServer, образ заливается на POST /update как multipart и пишется потоком, чанк за чанком (Update.write()), а не буфером целиком в RAM — 0,9 МБ в 520 КБ оперативки не влезли бы.
пишется только app‑раздел. SPIFFS не трогается — кольцо логов самодиагностики, настройки WiFI и записанные сессии LIVE RECORD переживают обновление;
прогресс видно на экране самой базы: «OTA UPDATE», имя файла, полоса заливки; по завершении —
ESP.restart();проверить версию после апдейта можно двумя способами:
GET /wifi/status→fw_ver, либо опкодом0xC1— тем же, что отдаёт сток, так что проверка не зависит от нашего софта;если робот остался без сети (ошиблись паролем, переехали в другой офис), он поднимает свою точку доступа
myCobot/mycobot123→192.168.4.1, и апдейт прилетает туда же. Плохая конфигурация Wi‑Fi больше не приговор;в UI это одна кнопка: Настройки → OTA → выбрать
.bin.
Чего OTA не делает — и это осознанно: эффектор Atom по воздуху не обновляется. Своего Wi‑Fi у него нет, он висит на той самой общей линии с сервами из предыдущего раздела, а единственный путь — USB‑FTDI на кисти. Зато это страховка: если база не поднялась после неудачного апдейта, Atom остаётся живым, и мост восстанавливается USB‑прошивкой базы.
И ложка дёгтя: OTA асинхронен по своей природе. Оборвалась заливка на середине — приложение в разделе помечено невалидным, и база уходит в циклическую перезагрузку, пока её не прошьют по USB. Поэтому питание и Wi‑Fi перед обновлением — не формальность.
7. Выжимаем максимум: попиксельный LED и медиа‑стриминг на LCD

Официальный софт умел только заливать LED одним цветом, а картинки грузились через проприетарный myStudio по проводу. Добавлены кастомные опкоды Atom: попиксельная матрица 5×5 (WS2812, GPIO 27, редактор в UI) и стриминг изображений чанками по 32 байта:
0x95 IMG_START: width | height | total_len 0x96 IMG_DATA: seq | chunk(32B) ← ×N 0x97 IMG_END: drawJpg() на LCD
На Базе — стейт‑машина экрана (LINK/CONTENT/MENU), анти‑мерцание: каркас рисуется один раз при смене состояния, обновляются только данные.
8. Серверный MiniIDE

Внизу интерфейса развёрнута блочная среда визуального программирования (MiniIDE) на чистом JS без тяжёлых фреймворков. Из блоков можно собирать алгоритмы: Поза (в углах или координатах), Линейное перемещение, Цикл (N раз или бесконечно), Условие по кнопке (A/B/C), Вывод на экран, GPIO‑порт OUT/IN, Гриппер, Запись траектории, Пауза.
Почему это надёжно:
Логика выполнения блоков крутится не в браузере. При нажатии «Плей» структура компилируется в JSON‑дерево и отправляется на бэкенд в многопоточный исполнитель
ProgramRunner(backend/runner.py).Если упадёт вкладка браузера, моргнёт Wi‑Fi или закроется ноутбук — робот не сойдёт с ума: поток выполнения живёт на сервере. Пауза и экстренный стоп синхронизированы на уровне ядра.
Программа исполняется в CONTENT‑режиме экрана (на старте — вход, по завершении — выход), так что во время прогона доступны кнопки.
«Домой по завершении» едет в активный HOME‑профиль, а не в нули.
9. Выход в реальный мир: интеграция с LLM‑агентами через MCP
Самая вкусная часть проекта — превращение учебного кобота в полноценного ИИ‑агента с помощью Model Context Protocol (MCP).
В системе развёрнут полноценный MCP‑сервер. Он — не отдельный драйвер, а тонкий WS‑клиент к собственному бэкенду: ходит на тот же WebSocket‑API, что и веб‑интерфейс. Поэтому физическое железо видит одно соединение, и ИИ‑агент с оператором за монитором работают одновременно. Доступ к реальной руке решает ровно тот же переключатель SIM/REAL в UI: в SIM кадры роботу не уходят вовсе, в REAL уходят.
Сервер умеет два транспорта:
streamable‑http
http://127.0.0.1:8085/mcp— поднимается автоматически в потоке внутриmain.py(uvicorn), еслиmcp.enabledвключён;stdio через
mcpserver/run.py— для клиентов, которые сами запускают плагин дочерним процессом.

Инструменты собраны в группы: состояние (углы, координаты, телеметрия серв, питание), движение (send_angles, send_coords, go_home, GPIO, PWM, гриппер), безопасность (stop — держит позу, emergency_toggle — снять/вернуть момент, кнопка Atom), экран и LED, кинематика (fk_solve, ik_solve, check_reachable, plan_circle — ничего не двигают), режимы и отдельно программы MiniIDE: program_save / program_run / program_pause / program_stop и другие.
Как это выглядит на практике:
В интерфейс ИИ‑клиента подаётся абстрактная команда: «Проверь статус робота. Опусти руку по оси Z на 80 единиц, поверни первый сустав на 90 градусов, а потом вернись в исходное положение».
Модель вызывает инструмент
robot_status, анализирует текущие углы.ИИ понимает задачу, обращается к инструменту перемещения, рассчитывает траекторию и шлёт команды. Физический робот и 3D‑модель синхронно отрабатывают движения.
«А теперь сгенерируй визуальную блочную программу для этой последовательности».
Модель вызывает инструменты создания программ MiniIDE, собирает структуру JSON‑файла, вычисляет точные углы, сохраняет файл на бэкенде и обновляет интерфейс.
Канвас MiniIDE заполняется визуальными блоками, которые ИИ только что составил сам, а
program_runисполняет их тем же многопоточнымProgramRunner, что и кнопка «Плей».
Безопасность разделена осознанно. Аварийные инструменты (stop, emergency_toggle, кнопка Atom) доступны агенту всегда. А вот рвущие сеть или перепрошивающие операции — reboot, wifi_set/wifi_reset, ota_upload, запись файлов — через MCP не отданы, они остаются только в руках оператора в UI.
Что дальше?
Аналитический IK: сейчас в прошивке и эмуляторе используется численный решатель обратной кинематики, который честно отбраковывает цели вблизи вертикальной сингулярности (гимбал‑лок). В планах — переход на полноценный аналитический IK.
Компьютерное зрение (VLM): добавление инструментов захвата изображения с камеры на эффекторе, чтобы LLM‑агент мог не просто двигать руку вслепую по координатам, а «видеть» объекты на столе и захватывать их.
Итог и ссылки
Проект доказал: даже из закрытого, багованного учебного железа при помощи реверс‑инжиниринга, грамотной многопоточной архитектуры на Python/C++ и современных протоколов вроде MCP можно сделать продвинутую платформу для ИИ‑исследований. Теперь роботом можно управлять не просто кодом, а текстовым промптом для нейросети. На данный момент проект дорос до первого публичного релиза. Чтобы порог входа был минимальным, я упаковал весь стек (3D Web‑UI, бэкенд, эмулятор и встроенный MCP‑сервер с ~47 инструментами) в единый portable‑файл. Для его запуска на Windows не требуется ставить Python, Conda или поднимать виртуальные окружения — достаточно распаковать в папку и запустить. Там же лежат скомпилированные бинарники кастомных прошивок для Базы и Atom.
Ссылка на portable приложение на GitHub: https://github.com/SkyseRoma/myCobot_UI_v1.0/releases/tag/v1.0
Видео демонстрации работы MCP (Локальный агент + Робот):
Обсуждение архитектуры на международном форуме: Ветка на Robot‑Forum: https://www.robot‑forum.com/robotforum/thread/56760-custom-3d‑web‑ui‑core‑emulator‑and‑mcp‑server‑for‑mycobot-280/
Если у вас в лаборатории или на полке пылится myCobot — самое время стереть с него пыль и развернуть локального ИИ‑агента!
Буду рад фидбеку, баг‑репортам и любым техническим вопросам в комментариях!

