- BrainTools - https://www.braintools.ru -

Unitree Go2: получаем root, исправляем Wi-Fi и добавляем русские голосовые команды

В нашей Unitree Go2 был штатный голосовой помощник BenBen. По-русски он с нами не разговаривал, а собака не понимала русские команды. Хотелось сказать «сидеть», «лежать» или «дай лапу» прямо роботу, без телефона в роли переводчика.

Начали разбираться с голосом. По дороге получили root основного компьютера, исправили срывы подключения к Wi-Fi и обнаружили программные команды, для которых не нашли кнопок в используемом приложении: от обычных поз до сальто. Затем собрали собственный голосовой мост и научили Go2 подходить и следовать за человеком с UWB-пультом. Всё это работает на борту.

Я Лев Ларин, инженер Unibot. В прошлой серии мы подключали робота-доставщика SpeedyBot к своей платформе. Теперь разбираемся с Unitree Go2. В этой статье — доступ к системе и русское управление. Во второй части [1] — развёртывание локальной LLM, шумоподавление и задержки разговора.

Автор в домашнем кресле гладит Unitree Go2

Автор в домашнем кресле гладит Unitree Go2

Постановочная ИИ-иллюстрация по фотографии автора. Домашнее голосовое управление — тот самый сценарий, ради которого всё началось.

Чего мы хотели от BenBen

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

У штатного голосового управления несколько частей. Есть офлайн-голосовой модуль, есть BenBen в приложении, есть распознавание, генерация ответа и синтез. Русский текст в одном из этих звеньев ещё не даёт работающий русский диалог.

В сохранённом коде штатного помощника текст мог пройти дальше без потери кириллицы, но обычная ветка синтеза выбирала китайский или английский TTS. Передача готового PCM позволяла этот синтез обойти. Одновременно выяснилось, что часть офлайн-речи связана с отдельным голосовым модулем и его ресурсами: заменить несколько WAV в Linux и получить полную русификацию не получалось.

Поэтому мы занялись собственной голосовой цепочкой. Это решало нашу практическую задачу, не требуя сначала перепрошивать голосовой микроконтроллер. Его мы в итоге и не прошивали.

Два компьютера, доступ к одному

Наш экземпляр — Go2 EDU с прошивкой 1.1.15. Внутри два отдельных Linux-компьютера со своими процессорами, памятью [2] и накопителями. Слово «контроллер» здесь легко создаёт ложное впечатление [3] маленького микроконтроллера. Основная плата тоже довольно мощная:

Компонент

Основной контроллер Unitree

Дополнительный компьютер Jetson

Плата / SoC

Unitree GO2 Board, Rockchip RK3588S

NVIDIA Jetson Orin Nano 8GB

CPU

8 ядер: 4 × Cortex-A76 + 4 × Cortex-A55

6 × Cortex-A78AE

GPU

Arm Mali-G610 MP4

NVIDIA Ampere, 1024 CUDA-ядра, 32 Tensor Cores

Другие ускорители

NPU Rockchip с паспортной производительностью до 6 TOPS

В нашем тракте используется CUDA GPU

Оперативная память

8 ГБ

8 ГБ LPDDR5, общие для CPU и GPU

Накопитель нашего экземпляра

eMMC класса 64 ГБ

NVMe SSD 512 ГБ

Система в сохранённой диагностике

Ubuntu 20.04.5 LTS, aarch64

Ubuntu 20.04.5 LTS, JetPack 5.1.1 / L4T R35.3.1, CUDA 11.4

Эти сведения относятся к нашей комплектации. Номинальные 8 ГБ — не объём свободной памяти: в сохранённом /proc/meminfo основной контроллер показывал около 7,72 ГиБ MemTotal, Jetson — около 7,17 ГиБ. Память двух плат не складывается в общий пул для модели.

Модель платы и SoC основного контроллера сверили по device tree, состав CPU — по /proc/cpuinfo, память и накопители — по /proc/meminfo и lsblk. Состав ускорителей чипов можно сопоставить с описанием RK3588S у производителя модулей Boardcon [4] и характеристиками Orin Nano у NVIDIA [5]. Это характеристики кремния; 6 TOPS не являются нашим измерением и не означают, что голосовой сервис использует NPU.

Основной контроллер, RK3588S. Здесь находятся штатные сервисы Unitree, Wi-Fi, USB-микрофон и динамик. Наш сервис на его CPU подписывается на микрофонный DDS-поток, декодирует Opus в PCM и отправляет звук на Jetson. Обратно принимает готовые WAV и запросы действий, вызывает штатные API и возвращает результат. Здесь же на CPU работает наш UWB-контур: получает расстояние и направление до пульта, рассчитывает скорости подхода или сопровождения и передаёт их штатному сервису. Алгоритмы походки и управление приводами остаются штатными.

Mali GPU и NPU у RK3588S есть, но наш голосовой мост к ним не обращается. Запускать на этой плате ещё одну копию Qwen или переносить на её NPU распознавание мы в описанной конфигурации не стали. Наличие ускорителя в SoC само по себе не подключает его к PyTorch или llama.cpp.

Jetson Orin Nano. У него есть собственный шестиядерный CPU, а не только CUDA GPU. На CPU Jetson выполняются профиль шума кулеров, усиление, выделение речи, распознавание и разбор команд. В финальном варианте GigaAM v3 RNNT int8 работает через CPU-провайдер sherpa-onnx в два потока. GPU Jetson используется для Qwen 4B и синтеза Silero. История разговора и выбранные профили тоже хранятся на Jetson.

CPU и GPU Jetson используют одни и те же 8 ГБ оперативной памяти. Дополнительных «8 ГБ видеопамяти» рядом с ними нет. Поэтому даже распознаватель, запущенный на CPU, занимает часть памяти, которая нужна модели и синтезу. В наших испытаниях Jetson работал в режиме 15 Вт.

Компьютеры связаны внутренним Ethernet. По нему мы передаём аудио и запросы между двумя нашими сервисами:

Два компьютера Go2 и разделение задач нашего голосового моста

Два компьютера Go2 и разделение задач нашего голосового моста

Микрофон находится на компьютере, к которому сначала не было SSH-доступа. Установить распознаватель на Jetson оказалось недостаточно.

Доступ к Jetson у нас уже был. На основном контроллере SSH не слушал. Подключённый кабель этого не менял: с самой Jetson получали отказ соединения на SSH-портах, хотя WebRTC-сервис основного компьютера отвечал.

Это был первый существенный поворот. Чтобы разобраться со штатным голосом и поставить сервис рядом с микрофоном, понадобился административный доступ к другому компьютеру той же собаки.

Root через программируемую кнопку

Путь нашёлся в сервисе programming_actuator, который исполняет загруженные программы по комбинации кнопок пульта. Мы изучили публичный UnLeash-Lite [6] и использовали описанный там механизм для своей процедуры получения доступа.

Суть в порядке инициализации Python. Программа работает в окружении с ограничениями, но sitecustomize.py исполняется при запуске интерпретатора до загрузки seccomp-фильтра. Возможность записать этот файл позволяет вынести подготовленный код за ограничения песочницы. Связь с сервисом шла через WebRTC-мост с аутентификацией ключом нашего устройства.

Готовый сценарий разблокировки менял больше настроек, чем требовалось для нашей задачи. Мы подготовили временный вариант: сохранить исходные Python-файлы с метаданными, установить однократный хук, а при следующем запуске интерпретатора сначала восстановить оригиналы и только затем поднять отдельный SSH по ключу.

Программу назначили на свободную комбинацию L1+Y. Потребовались два физических нажатия с паузой: первое записывало хук, второе запускало его в новом интерпретаторе. После этого открыли root-сессию на временном порту 2222.

Последовательность получения временного и постоянного административного доступа

Последовательность получения временного и постоянного административного доступа

Схема нашей процедуры. После срабатывания временного хука исходные Python-файлы восстанавливаются.

Сверили восстановленные файлы: содержимое, владельцев, права и время изменения. Удалили нашу привязку кнопки и временные файлы. Штатные пароли, основной SSH-конфиг и скрипты обновления при этой процедуре не меняли.

Получив доступ, сделали резервные копии обоих компьютеров с проверкой SHA-256 и чтением архивов. Это копии работающих файловых систем с дополнительными образами разделов, а не атомарный снимок всей собаки.

Для дальнейшей работы завели отдельного пользователя go2owner с sudo и отдельную SSH-службу на порту 2223. Этот постоянный доступ проверили после полного выключения и включения робота. Временный root-вход выполнил свою задачу; повседневная работа продолжилась через собственную учётную запись.

Wi-Fi помнил старую неудачу

Пока разбирались с доступом, приходилось заниматься и связью. Go2 могла подключиться к домашней сети при одном запуске и остаться без Wi-Fi при следующем. Сеть и пароль были сохранены, но это не гарантировало повторного подключения.

В неудачном запуске штатный скрипт сообщал network not found, затем завершал wpa_supplicant. IP-адрес на wlan0 так и не появлялся. В удачном запуске начальные ошибки [7] сканирования тоже встречались, однако через несколько секунд робот подключался и получал адрес по DHCP.

Разница нашлась в скрипте:

/unitree/module/network_manager/upper_bluetooth/wpa_connect/wpa_connect.sh

Он искал ошибки по всему журналу, включая сообщения прежних попыток. Проверка ошибок стояла раньше проверки текущего wpa_state=COMPLETED. Кроме того, для отсутствующей сети был ранний выход примерно через десять секунд при общем тайм-ауте тридцать секунд. При неуспехе cleanup останавливал supplicant, поэтому продолжить сканирование было уже некому.

Исправили порядок: сначала проверять текущее соединение, затем учитывать ошибки только текущей попытки. Промежуточный неудачный scan перестал быть поводом оборвать весь отведённый срок подключения.

Обработка состояния Wi-Fi до и после исправления

Обработка состояния Wi-Fi до и после исправления

Исправление касается алгоритма подключения. Радиопричина первого неудачного сканирования осталась отдельным вопросом.

Упрощённо исправленный порядок выглядит так:

while connection_deadline_not_reached():
    if current_wpa_state() == "COMPLETED":
        return CONNECTED
    error = read_errors_from_current_attempt()
    if error in (WRONG_PASSWORD, AUTHENTICATION_FAILED):
        return AUTH_ERROR
    # Неудачный scan оставляет время на следующую попытку.
    wait_for_next_check()

Это псевдокод порядка проверок. Рабочая правка выполнена в штатном shell-скрипте; параметры сети она не меняет.

Добавили отдельную службу восстановления. Раз в тридцать секунд она проверяет состояние; даёт работающему supplicant время восстановиться самостоятельно и при необходимости повторяет подключение штатным скриптом. В режиме точки доступа и во время переключений, запущенных приложением, служба не вмешивается. SSID и пароль берутся из прежней конфигурации.

Проверили восстановление после намеренной остановки supplicant: служба повторила подключение, получили COMPLETED и IP-адрес. Для возвращения приложения в этой проверке потребовался его перезапуск, после чего соединение восстановилось.

Причину первого неудачного радиосканирования мы этим не установили. Зато исправили конкретную программную причину, по которой временная неудача оставляла собаку без сети.

Через Wi-Fi открыли наш SSH основного контроллера, а Jetson стала доступна через ProxyJump. Получилась обычная схема обслуживания: ноутбук → домашняя сеть → основной контроллер → внутренний Ethernet → Jetson. Кабель для каждой правки больше не нужен.

Сальто есть, кнопки нет

Следующим вопросом был список действий. Мы хотели связать русские слова с реальными командами, а для этого требовалось понять, что собака вообще умеет принимать.

В используемом нами приложении и на пульте не было полного набора нужных команд. Искали «сидеть», «лежать», трюки; в программных интерфейсах обнаружился значительно более широкий каталог. В том числе сальто вперёд, назад и влево, режимы передних и задних лап, несколько походок.

Ориентировались на SDK, установленный именно в нашей прошивке, и сверяли его с публичным SDK Unitree [8]. У каждого вызова есть конкретный API ID и формат параметров. Например:

Русская команда

Метод Sport API

API ID

Встать

StandUp()

1004

Лежать

StandDown()

1005

Сидеть

Sit()

1009

Подняться из сидячего положения

RiseSit()

1010

Дай лапу

Hello()

1016

Сальто вперёд

FrontFlip()

1030

Боковое сальто

LeftFlip()

2041

Сальто назад

BackFlip()

2043

StandDown — штатное опускание корпуса; это значение мы связали с русским «лежать». HandStand и WalkUpright включают соответствующие режимы, а не задают маршрут или число шагов.

Мы не создавали алгоритм сальто. Нашли существующую точку входа и добавили к ней понятную русскую команду. При этом наличие метода в SDK не доказывает, что конкретная прошивка выполнит его в любом текущем режиме.

В итоге каталог вырос до 93 голосовых намерений и 39 операций публичного Sport API. Намерения включают также свет, звук, разговорные настройки и навигацию, поэтому это не «93 разных трюка». Несколько формулировок могут вести в один обработчик: «сидеть» и «сядь» означают одно действие.

Полное покрытие каталога мы проверяли на уровне разбора фраз и формирования вызовов. Все сальто и стойки физически не испытывали.

Поиск сальто в нашем каталоге русских команд

Поиск сальто в нашем каталоге русских команд

Настоящий скриншот каталога: три намерения для сальто. Наличие карточки и обработчика не означает физическую приёмку трюка.

Принять команду и выполнить трюк — разные события

Для некоторых операций недостаточно отправить API ID. Нужны параметры с правильными типами. Например, Move получает линейные скорости и угловую скорость, а переключение режима — булево поле data.

Поэтому сделали типизированный адаптер: известный метод, ожидаемые поля, проверка типов и конечности чисел, затем JSON для штатного API. Неизвестные методы и дополнительные параметры он не отправляет.

Отдельно разобрали ответы. В нашем адаптере различаются принятие контроллером, отказ с кодом 3203 и отсутствие подтверждения с кодом 3104. Последний случай не приводит к автоматической повторной отправке движения: неизвестно, потерялся запрос или только ответ.

Так выглядит сокращённая логика [9] обработки результата из адаптера:

if code == 3104:
    status = "sent_unconfirmed"  # Автоматически не повторяем.
elif code == 3203:
    status = "unsupported"       # Не реализовано в текущем режиме.
elif code:
    status = "rejected"
else:
    status = "accepted"          # Принято контроллером.

Даже успешное подтверждение означает «контроллер принял команду». Для утверждения «собака выполнила сальто» нужна проверка самого движения. Этот статус сохранили и в программном интерфейсе, и в формулировках ответов.

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

Подключаем голос к штатным API

Список команд был готов, но оставался вопрос, откуда брать звук. USB-микрофон основного контроллера уже использовал штатный audiohub. На Jetson его не было, а останавливать audiohub ради собственного arecord мы не хотели.

В работающей системе нашли DDS-тему rt/audiosender с сообщениями AudioData_. В поле data приходил сжатый Opus. Название типа не означает, что эти байты можно сразу передать распознавателю как PCM.

Подписались на штатный поток, декодировали пакеты через libopus в mono PCM16 с частотой 16 кГц и передали звук на Jetson по внутреннему Ethernet. Обратным путём идут действия и озвученные ответы.

Микрофон → штатный audiohub → DDS / Opus
                                ↓
                    наш сервис основного контроллера
                                ↓ PCM по Ethernet
                    Jetson: обработка и распознавание
                                ↓
                     диспетчер русских команд
                                ↓ действие по Ethernet
                    основной контроллер → штатный API

Между нашими сервисами — TCP-соединение с кадрированием: длина JSON-заголовка, сам заголовок и двоичная полезная нагрузка. Так по одному соединению проходят звук, запросы действий и их результаты без упаковки аудио в Base64.

Простые точные команды идут по короткому пути. Распознанное «дай лапу» связывается с намерением give_paw, а оно — с Hello, API 1016. Для этого не нужно ждать, пока языковая модель сочинит ответ и выберет функцию.

Путь русской команды к штатному API и отдельная ветка свободного запроса

Путь русской команды к штатному API и отдельная ветка свободного запроса

Сокращённый пример словаря и таблицы маршрутизации:

PHRASES = {
    "сидеть": "sit",
    "сядь": "sit",
    "лежать": "lie_down",
    "дай лапу": "give_paw",
}
SPORT = {
    "sit": 1009,
    "lie_down": 1005,
    "give_paw": 1016,
}

Для «сядь» результатом будет sit и запрос в сервис sport с API ID 1009 и пустым JSON-объектом параметров. Русский текст не превращается в Python-код для исполнения.

Свободные вопросы передаются локальной Qwen 4B. Если для управления нужны параметры, используется отдельный типизированный вызов. Сам текст ответа модели командой для моторов не является.

Распознавание потребовало нескольких итераций. Первоначально использовали Vosk для коротких команд и T-one для разговоров. Позже основным финальным распознавателем стал GigaAM v3, перед ним — мягкий профиль шума кулеров, усиление и Silero VAD. Выяснилось, что маленький словарный распознаватель может принять постороннюю речь за ближайшую известную команду. Поэтому его предположение не исполняется преждевременно без проверки открытым распознаванием.

Получение чистого голоса из динамика тоже потребовало отдельной попытки. Наш вывод через DDS с кодированием Opus хрипел. Через штатный WAV-путь воспроизведение получилось чистым, его и оставили. Фильтры, сравнение распознавателей и устройство этого вывода подробно разберём во второй статье.

Оба наших сервиса работают на компьютерах собаки. Ноутбук нужен для разработки и диагностики. Для обычных команд и локального разговора внешняя сеть не участвует в вычислениях.

«Ко мне» не оказалось одним вызовом

Пока команда означает позу или жест, достаточно правильно выбрать штатную операцию. «Ко мне» добавляет вопрос: где находится человек?

У нас есть UWB-пульт. Робот получает оценку расстояния и направления до него. Значит, для этой задачи можно обойтись без карты помещения: цель находится относительно корпуса собаки. При этом Go2 идёт к пульту, который человек держит при себе. Распознавания личности или сопровождения по камере здесь нет.

Первым вариантом было штатное сопровождение. Но в наших проверках пульт оставался в режиме джойстика и восстановления стойки. Запись в DDS-переключатель не меняла реальный флаг включения. Нашли действующий RPC-сервис uwbswitch и стали проверять записанное состояние обратным чтением.

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

На этой стадии получили особенно полезную неисправность: API принимал команды, собака стояла.

Для движения через этот путь понадобились три настройки: выбрать скорости из API, включить обход препятствий и включить Sport-режим FreeAvoid. Без последнего принятые запросы не приводили к ожидаемому движению.

В нашей прошивке получилась следующая связка:

Сервис

API ID

Назначение

obstacles_avoid

1004

Выбрать команды скорости из API

obstacles_avoid

1001

Включить обход препятствий

sport

2048

Включить FreeAvoid

obstacles_avoid

1003

Передавать x, y, yaw, mode

В этом контуре используем максимум 0,35 м/с вперёд и 0,65 рад/с на повороте. Зона завершения подхода — около 1,14 м по UWB-измерению. Это параметры нашего регулятора сопровождения.

Локальный UWB-контур подхода и сопровождения

Локальный UWB-контур подхода и сопровождения

Схема алгоритма, не запись траектории испытания. Для подхода отслеживаем расстояние и направление до пульта.

При цели за спиной возник ещё один эффект. Угол около границы +π и −π от небольшого шума менял знак. Регулятор начинал выбирать попеременно левый и правый разворот. Исправили сохранением выбранного направления, пока пульт не окажется в передней полусфере.

«Ко мне» завершает задачу после подхода. «За мной» продолжает следить за положением пульта до остановки. При пропаже свежих измерений передаём нулевую скорость; с возвращением данных сопровождение может продолжиться. Движение джойстика отменяет наш контур.

Для «стой» оказалось принципиально завершать источник следующих команд. Если только отправить StopMove, работающий цикл через пятьдесят миллисекунд снова задаст скорость. Поэтому сначала останавливаем задачу, обнуляем движение и возвращаем настройки источника управления и обхода, затем отправляем остановку.

И подход, и сопровождение проверили на настоящем роботе: Go2 подошла и остановилась; затем следовала за идущим человеком и остановилась по команде. Карта и интернет для этих двух сценариев не потребовались.

Что получилось на самой Go2

На борту работают русское распознавание, диспетчер команд, локальная Qwen 4B и синтез речи. Основной контроллер получает микрофон, воспроизводит звук и обращается к штатным API; Jetson выполняет обработку речи и разговорную часть.

Голосом проверены «дай лапу» и управление фонарём. Подход, сопровождение и остановка подтверждены в физических проверках. Остальной расширенный каталог имеет разную степень проверки: корректное сопоставление фразы и API не выдаём за испытание каждого трюка. Последние версии голосового моста и сопровождения ещё предстоит проверить полным выключением и включением собаки.

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

Всё началось с желания дать Unitree Go2 команду по-русски. Для этого пришлось разобраться с двумя Linux-компьютерами, правами доступа, сетевым скриптом, аудиопотоком и форматами штатных вызовов. Теперь поверх этих частей есть собственный интерфейс управления и настройки собаки.

Во второй части разбираем, как Go2 слышит человека рядом с собственными вентиляторами и сколько времени проходит до первого звука ответа локальной LLM.

Часть 2. Unitree Go2: локальная LLM, шумоподавление и ускорение голосовых ответов → [1]

Связанный разбор — разлочка Quectel EG25-G с eSIM в Unitree Go2 [10]. Публикация 4 ноября 2026 года в 14:00 МСК.

Автор: Tigramaan

Источник [11]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/37990

URLs in this post:

[1] второй части: https://habr.com/ru/articles/1103288/

[2] памятью: http://www.braintools.ru/article/4140

[3] впечатление: http://www.braintools.ru/article/2012

[4] описанием RK3588S у производителя модулей Boardcon: https://www.boardcon.com/CM3588S_SOM/

[5] характеристиками Orin Nano у NVIDIA: https://developer.nvidia.com/blog/solving-entry-level-edge-ai-challenges-with-nvidia-jetson-orin-nano

[6] UnLeash-Lite: https://github.com/a-bissell/UnLeash-Lite

[7] ошибки: http://www.braintools.ru/article/4192

[8] публичным SDK Unitree: https://github.com/unitreerobotics/unitree_sdk2_python/tree/814556d15970dd2ecf1c9984e845ca02ab07e206/unitree_sdk2py/go2/sport

[9] логика: http://www.braintools.ru/article/7640

[10] разлочка Quectel EG25-G с eSIM в Unitree Go2: https://habr.com/ru/articles/1103320/

[11] Источник: https://habr.com/ru/articles/1103278/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1103278

www.BrainTools.ru

Rambler's Top100