Настройка Meta Quest 3 VR Teleop
Полный пошаговый путь от простого квеста 3 до дистанционного управления роботизированной рукой. Охватывает настройку сети, конфигурацию Unity, UDP-сервер Python, Piper_controller.py и шаблон адаптера для любого рычага.
Сеть и необходимые условия
Прежде чем писать какой-либо код, убедитесь, что 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 изолируют клиентов друг от друга — используйте выделенную точку доступа для работы робототехники.
Конфигурация приложения 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 мин над поверхностью стола, чтобы избежать столкновения. |
Настройка 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)))
QUEUE_MAXSIZE Значение 1 дает минимальную задержку, но может ощущаться рывками. Значения 3–5 сглаживают движение при потере пакетов, но добавляют дополнительную задержку до 100 мс при частоте 30 Гц. Начните с 3 и настройте по вкусу.
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: проходить
piper.GetPiperFirmwareVersion() и сверяйте их с документами разработчика AgileX, прежде чем давать команду на какие-либо действия.
Проверка безопасности и первый сеанс
Контрольный список пробного запуска (питание выключено):
- Запустите сервер 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()и выйти чисто.
valid Флаг в пакете UDP падает до 0. Сервер Python сохраняет последнюю известную позицию, а не переводит рычаг в нулевое положение.
Интерфейс адаптера — порт на любой рычаг
Шаблон замены контроллера распространяется на любую руку с Python SDK. Единственное требование состоит в том, чтобы ваш класс контроллера реализовал эти пять методов именно с такой сигнатурой:
| Метод | Подпись | Договор |
|---|---|---|
| соединять() | () → логическое значение | Открывает канал связи; возвращает True в случае успеха |
| отключить() | () → Нет | Отключает питание сервопривода и закрывает порт или разъем. |
| set_pose(x, y, z, крен, шаг, рыскание) | (с плавающей точкой×6) → Нет | Декартова цель рабочего органа в мм + градусах; должен зажиматься внутри |
| set_gripper (значение) | (плавающее) → Нет | Нормализованная открытость 0,0–1,0; внутренняя карта с юнитами, специфичными для конкретного оружия |
| Emergency_stop() | () → Нет | Должен быть безопасным вызов из любого потока в любое время. |
Шаги по добавлению новой руки:
- Писать
myarm_controller.pyреализация пяти вышеуказанных методов с использованием SDK вашей руки. Границы рабочей области жесткого кода и ограничения скорости как константы уровня модуля. - Юнит-тест изолированно: вызов
connect(), затемset_poseс безопасным положением в 5 см от дома. Убедитесь, что рука движется и возвращается в ожидаемое положение. - Замените импорт в
teleoperation_main.py: заменятьfrom piper_controller import PiperControllerс вашим новым контроллером. Никаких других изменений не требуется. - Откалибруйте параметры Unity Inspector (
positionOffset,rotationOffset,scaleFactor) для рабочего пространства новой руки. - Перед любым сеансом оператора проверяйте поведение аварийного останова, удержание потери отслеживания и зажимы рабочего пространства.
geometry_msgs/PoseStamped тема внутри set_pose — остальная архитектура остается идентичной. Конструкция на основе очередей естественным образом поглощает задержку цикла публикации/подписки ROS 2.