Настройка Meta Quest 3 VR Teleop

Полный пошаговый путь от простого квеста 3 до дистанционного управления роботизированной рукой. Охватывает настройку сети, конфигурацию Unity, UDP-сервер Python, Piper_controller.py и шаблон адаптера для любого рычага.

1

Сеть и необходимые условия

Прежде чем писать какой-либо код, убедитесь, что Meta Quest 3 и управляющий компьютер могут связаться друг с другом через одну и ту же локальную сеть. Система использует UDP — оба устройства должны находиться в одной подсети, а UDP-порты 8888 и 8889 должны быть открыты в брандмауэре ПК.

Контрольный список требований:

  • Гарнитура Meta Quest 3 с включенным отслеживанием рук (Настройки → Отслеживание движений → Отслеживание рук)
  • Точка доступа Wi-Fi 6 — настоятельно рекомендуется поддерживать задержку передачи UDP ниже 10 мс.
  • Управляющий компьютер под управлением Linux (Ubuntu 22.04+) или macOS с Python 3.10+.
  • Для AgileX Piper: адаптер USB-CAN (например, Kvaser или PEAK), кабель CAN, подключенный к рычагу.
  • UDP-порты 8888 и 8889 открыты для входящих подключений на брандмауэре ПК.

Проверьте подключение:

# On the control PC — find your IP address
ip addr show   # Linux
ipconfig getifaddr en0   # macOS (Wi-Fi)

# Quick UDP listener test (run on PC, send any packet from Quest)
python3 -c "импортировать сокет; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.bind(('0.0.0.0',8888)); print('Прослушивание...'); print(s.recvfrom(256))"
Требуется та же подсеть. Бегать ip addr на ПК и убедитесь, что первые три октета соответствуют IP-адресу Quest 3 (отображается в настройках Quest → Wi-Fi → значок шестеренки). Распространенной проблемой является то, что корпоративные сети Wi-Fi изолируют клиентов друг от друга — используйте выделенную точку доступа для работы робототехники.
2

Конфигурация приложения Unity

Приложение Unity, работающее на Quest 3, создано на базе Unity 2022.3 LTS или более поздней версии с использованием пакета XR Hands для отслеживания рук. Три сценария управляют телеоперацией:

  • VRHandPoseSender.cs — считывает позу руки из подсистемы XR Hands, сериализует в 45-байтовый двоичный пакет, отправляет через UDP
  • VRGripperController.cs — сопоставляет силу защемления с нормализованным значением захвата [0, 1]
  • VRTeleoperationManager.cs — управление жизненным циклом, пользовательский интерфейс состояния подключения, автоматическое повторное подключение

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

Инспектор Филд Начальное значение AgileX Piper Примечания
Целевой IP IP-адрес вашего компьютера Бегать ip addr на ПК, чтобы найти его
позицияСмещение (м) (0, 0, 0.3) Смещает происхождение виртуального робота; Piper reach is shorter than xArm6
смещение вращения (градусы) (0, 90, 0) Коррекция угла Y 90° для рамы Piper CAN; отрегулируйте в соответствии с ориентацией крепления
масштабный фактор 0.75 Уменьшает диапазон движений руки, чтобы соответствовать рабочему пространству Piper (вылет ~600 мм).
Рабочее пространство X (мм) ±400 Оставьте запас 50 мм в пределах физических ограничений.
Рабочее пространство Z (мм) 50 – 700 Держите Z мин над поверхностью стола, чтобы избежать столкновения.
Выполните калибровку перед первым запуском в реальном времени. Медленно переместите Quest 3 к каждому краю предполагаемого рабочего пространства и наблюдайте за заданным положением на выходе терминала. Прежде чем включить потоковую передачу на полной скорости, убедитесь, что робот остается в пределах своих суставов.
3

Настройка UDP-сервера Python

Сервер Python запускает три параллельных потока: поток приемника который читает необработанные датаграммы UDP, предохранительная нить который проверяет пакеты и ставит их в очередь, а поток управления роботом который опустошает очередь и вызывает контроллер робота. Такое разделение гарантирует, что медленные вызовы SDK робота никогда не блокируют прием UDP.

Установите зависимости:

pip install python-can piper_sdk

# Activate the CAN interface (run once per boot, requires root or dialout group)
bash can_activate.sh can0 1000000

# Verify the interface is UP
ifconfig can0

Запустите сервер телеоперации:

python3 teleoperation_main.py

Сервер привязывается к 0.0.0.0:8888 (правая рука) и опционально 0.0.0.0:8889 (левая рука). Когда приходит пакет с valid флаг установлен, поток управления вызывает robot.set_pose() и robot.set_gripper(). Нажимать Ctrl-C для запуска аварийной остановки и чистого отключения.

Упрощенная структура сервера, показывающая, как взаимодействуют все три потока:

# teleoperation_main.py (simplified structure)
импортировать socket, struct, queue, threading, signal, time
от piper_controller импортировать PiperController   # swap this to change arms

HOST = "0.0.0.0"
RIGHT_PORT    = 8888
LEFT_PORT     = 8889
QUEUE_MAXSIZE = 3   # drop stale frames if robot is slow
CONTROL_HZ    = 30  # robot command rate

pose_queue     = queue.Queue(maxsize=QUEUE_MAXSIZE)
shutdown_event = threading.Event()

защита udp_receiver(port: int):
    """Поток 1 — получение необработанных датаграмм UDP."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((HOST, port))
    sock.settimeout(1.0)
    пока нет shutdown_event.is_set():
        пытаться:
            data, _ = sock.recvfrom(256)
            pose = parse_packet(data)
            если pose и pose["действительный"]:
                пытаться:
                    pose_queue.put_nowait(pose)
                кроме queue.Full:
                    pose_queue.get_nowait()   # drop oldest frame
                    pose_queue.put_nowait(pose)
        кроме socket.timeout:
            продолжать
    sock.close()

защита robot_control_loop(robot: PiperController):
    """Поток 3 — очистка очереди и командование роботом на CONTROL_HZ."""
    period = 1.0 / CONTROL_HZ
    last_pose = None
    пока нет shutdown_event.is_set():
        t0 = time.monotonic()
        пытаться:
            pose = pose_queue.get(timeout=0.1)
            если pose["останавливаться"]:
                robot.emergency_stop()
                продолжать
            если pose["действительный"]:
                last_pose = pose
                x, y, z = transform_position(pose["позиция"])
                roll, pitch, yaw = quat_to_euler(pose["вращение"])
                robot.set_pose(x, y, z, roll, pitch, yaw)
                robot.set_gripper(pose["захват"])
            # tracking lost — hold last known position (do not send zero)
        кроме queue.Empty:
            проходить
        time.sleep(max(0, period - (time.monotonic() - t0)))
Размер очереди контролирует задержку и плавность. A QUEUE_MAXSIZE Значение 1 дает минимальную задержку, но может ощущаться рывками. Значения 3–5 сглаживают движение при потере пакетов, но добавляют дополнительную задержку до 100 мс при частоте 30 Гц. Начните с 3 и настройте по вкусу.
4

piper_controller.py Прохождение

The piper_controller.py модуль оборачивает AgileX piper_sdk Библиотека Python. Он реализует интерфейс из пяти методов, ожидаемый teleoperation_main.py. Ключевые дизайнерские решения:

  • МОЖЕТ через USB: Рука Piper обменивается данными через шину CAN. SDK принимает имя интерфейса CAN (can0) и требует активации интерфейса перед подключением.
  • Ведомый режим: Вызов MasterSlaveConfig(0xFC, 0, 0, 0) переводит руку в подчиненный режим, чтобы она принимала потоковые команды положения.
  • Зажим рабочего пространства: Позиции жестко привязываются к константам вверху файла перед каждым вызовом SDK — это последняя линия защиты ПО перед прошивкой.
  • SDK-единицы: Позиция указана в микрометрах (целое число), ориентация в миллиградусах — умножьте значения с плавающей запятой в мм/градусах на 1000, прежде чем перейти к EndEffectorCtrl.
# piper_controller.py
от piper_sdk импортировать C_PiperInterface
импортировать math, logging

logger = logging.getLogger(__name__)

# Piper workspace limits (millimetres) — keep 50 mm inside physical limits
X_MIN, X_MAX = -400, 400
Y_MIN, Y_MAX = -400, 400
Z_MIN, Z_MAX =  50,  700

# Maximum joint speed (0–100 %; keep conservative for teleop)
SPEED_PERCENT = 25


сорт Пайперконтроллер:
    """Замена XArmController — тот же интерфейс с пятью методами."""

    защита __init__(self, can_interface: str = "может0"):
        self.can_interface = can_interface
        self._piper: C_PiperInterface | None = None
        self.connected = False

    защита соединять(self) -> bool:
        """Откройте порт CAN и включите подчиненный режим."""
        пытаться:
            self._piper = C_PiperInterface(
                can_name=self.can_interface,
                judge_flag=False,     # allow 3rd-party CAN adapters
                can_auto_init=True,
                dh_is_offset=1,      # firmware >= V1.6-3
            )
            self._piper.ConnectPort()
            self._piper.MasterSlaveConfig(0xFC, 0, 0, 0)  # enter slave mode
            self.connected = True
            logger.info(«Пайпер подключена к %s», self.can_interface)
            возвращаться True
        кроме Exception как e:
            logger.error(«Ошибка подключения Piper: %s», e)
            возвращаться False

    защита отключиться(self):
        """Отключить сервопривод и закрыть порт."""
        если self._piper:
            пытаться:
                self._piper.DisableArm(7)   # disable all joints
            кроме Exception:
                проходить
        self.connected = False

    защита set_pose(self, x: float, y: float, z: float,
                 roll: float, pitch: float, yaw: float):
        """Переместите рабочий орган на (x,y,z) мм с ориентацией (крен, наклон, рысканье) градусов."""
        если не self.connected:
            возвращаться
        x = max(X_MIN, min(X_MAX, x))
        y = max(Y_MIN, min(Y_MAX, y))
        z = max(Z_MIN, min(Z_MAX, z))
        пытаться:
            self._piper.EndEffectorCtrl(
                интервал(x * 1000), интервал(y * 1000), интервал(z * 1000),
                интервал(roll  * 1000),
                интервал(pitch * 1000),
                интервал(yaw   * 1000),
                SPEED_PERCENT,
            )
        кроме Exception как e:
            logger.warning(«ошибка set_pose: %s», e)

    защита set_gripper(self, value: float):
        """Установка открытия захвата. 0,0 = полностью закрыто, 1,0 = полностью открыто."""
        если не self.connected:
            возвращаться
        GRIPPER_MAX_UM = 70_000   # 70 mm max opening in µm
        target_um = интервал(value * GRIPPER_MAX_UM)
        пытаться:
            self._piper.GripperCtrl(target_um, SPEED_PERCENT, 0x01, 0)
        кроме Exception как e:
            logger.warning(«Ошибка set_gripper: %s», e)

    защита аварийная_остановка(self):
        """Немедленно отключить все соединения. Безопасно звонить из любого потока."""
        если self._piper:
            пытаться:
                self._piper.DisableArm(7)
            кроме Exception:
                проходить
Проверьте модули SDK на наличие версии прошивки. Piper SDK быстро развивается. В некоторых версиях прошивки позиция указывается в микрометрах (целое число); в других — миллиметры (с плавающей запятой). Распечатать piper.GetPiperFirmwareVersion() и сверяйте их с документами разработчика AgileX, прежде чем давать команду на какие-либо действия.
5

Проверка безопасности и первый сеанс

Прочтите перед включением питания. Дистанционное управление VR управляет роботом в режиме реального времени на основе движения вашей руки. Неправильно откалиброванная позицияOffset или ScaleFactor может привести к мгновенному перемещению руки в положение, которое повредит ее самой или нанесет травму находящимся рядом людям. Всегда сначала проводите сухой запуск, отключив питание робота.

Контрольный список пробного запуска (питание выключено):

  • Запустите сервер Python и подключите Quest 3. Посмотрите transform_position вывод, выведенный на терминал.
  • Держите руку в центре предполагаемого рабочего пространства. Убедитесь, что напечатанные значения XYZ находятся рядом с исходным положением робота (приблизительно 0, 0, 300 мм для Piper).
  • Поднесите руку к каждому краю рабочего пространства. Убедитесь, что значения остаются в пределах фиксированных границ и никогда не становятся отрицательными по Z.
  • Нажмите кнопку «Меню» на Quest, чтобы активировать программный аварийный останов. Подтверждать emergency_stop() вызывается и цикл останавливается.

Первый прямой эфир:

  • Начать с SPEED_PERCENT = 20 — это максимальная скорость соединения примерно 40°/с.
  • Включите питание сервопривода. Двигайтесь медленно, оставаясь в исходном положении руки в течение первой минуты.
  • Убедитесь, что движение руки и движение робота направлены в одном направлении. Если запястье поворачивается назад, отрегулируйте rotationOffset под углом ±90° по осям Y в Unity Inspector.
  • Постепенно расширяйте диапазон движения, как только карта направления будет подтверждена правильно.
  • Всегда держите физический аварийный останов (силовое реле) под рукой.

Реализованы два пути программного аварийного останова:

  • Кнопка меню квеста 3: Устанавливает бит 1 байта флагов в каждом последующем пакете UDP. Сервер Python вызывает robot.emergency_stop() немедленно.
  • Ctrl-C (СИГНАЛ): Обработчик сигнала устанавливает событие выключения, которое вызывает вызов контура управления. emergency_stop() и выйти чисто.
Потеря отслеживания обрабатывается безопасно. Когда Quest 3 теряет отслеживание рук (рука выходит из поля зрения камеры, пальцы перекрываются), valid Флаг в пакете UDP падает до 0. Сервер Python сохраняет последнюю известную позицию, а не переводит рычаг в нулевое положение.
6

Интерфейс адаптера — порт на любой рычаг

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

Метод Подпись Договор
соединять() () → логическое значение Открывает канал связи; возвращает True в случае успеха
отключить() () → Нет Отключает питание сервопривода и закрывает порт или разъем.
set_pose(x, y, z, крен, шаг, рыскание) (с плавающей точкой×6) → Нет Декартова цель рабочего органа в мм + градусах; должен зажиматься внутри
set_gripper (значение) (плавающее) → Нет Нормализованная открытость 0,0–1,0; внутренняя карта с юнитами, специфичными для конкретного оружия
Emergency_stop() () → Нет Должен быть безопасным вызов из любого потока в любое время.

Шаги по добавлению новой руки:

  1. Писать myarm_controller.py реализация пяти вышеуказанных методов с использованием SDK вашей руки. Границы рабочей области жесткого кода и ограничения скорости как константы уровня модуля.
  2. Юнит-тест изолированно: вызов connect(), затем set_pose с безопасным положением в 5 см от дома. Убедитесь, что рука движется и возвращается в ожидаемое положение.
  3. Замените импорт в teleoperation_main.py: заменять from piper_controller import PiperController с вашим новым контроллером. Никаких других изменений не требуется.
  4. Откалибруйте параметры Unity Inspector (positionOffset, rotationOffset, scaleFactor) для рабочего пространства новой руки.
  5. Перед любым сеансом оператора проверяйте поведение аварийного останова, удержание потери отслеживания и зажимы рабочего пространства.
Используете ROS 2 вместо прямого SDK? Опубликовать в geometry_msgs/PoseStamped тема внутри set_pose — остальная архитектура остается идентичной. Конструкция на основе очередей естественным образом поглощает задержку цикла публикации/подписки ROS 2.

Настройка завершена?

Ознакомьтесь с полными спецификациями пакетов UDP или прочитайте полную вики-страницу для разработчиков, чтобы получить более подробную информацию.