Configuração de Teleop do Meta Quest 3 VR

Caminho completo passo a passo de um Quest 3 nu para teleoperação ao vivo de braço robótico. Cobre configuração de rede, configuração do Unity, o servidor UDP Python, piper_controller.py e o padrão de adaptador para qualquer braço.

1

Rede e Pré-requisitos

Antes de escrever qualquer código, confirme que o Meta Quest 3 e o PC de controle podem se comunicar entre si na mesma rede local. O sistema usa UDP — ambos os dispositivos devem compartilhar a mesma sub-rede, e as portas UDP 8888 e 8889 devem estar abertas no firewall do PC.

Lista de verificação de requisitos:

  • Headset Meta Quest 3 com rastreamento de mão habilitado (Configurações → Rastreamento de Movimento → Rastreamento de Mão)
  • Ponto de acesso Wi-Fi 6 — fortemente recomendado para manter a latência de trânsito UDP abaixo de 10 ms
  • PC de controle rodando Linux (Ubuntu 22.04+) ou macOS com Python 3.10+
  • Para AgileX Piper: adaptador USB para CAN (por exemplo, Kvaser ou PEAK), cabo CAN conectado ao braço
  • Portas UDP 8888 e 8889 abertas para entrada no firewall do PC

Verifique a conectividade:

# 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 "importar soquete; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.bind(('0.0.0.0',8888)); print('Ouvindo...'); print(s.recvfrom(256))"
Mesma sub-rede necessária. Executar ip addr no PC e verifique se os três primeiros octetos correspondem ao IP do Quest 3 (visível em Configurações do Quest → Wi-Fi → ícone de engrenagem). Um problema comum é que redes Wi-Fi corporativas isolam os clientes uns dos outros — use um ponto de acesso dedicado para trabalho com robótica.
2

Configuração do Aplicativo Unity

O aplicativo Unity em execução no Quest 3 é construído com Unity 2022.3 LTS ou posterior, usando o pacote XR Hands para rastreamento de mãos. Três scripts lidam com a parte de teleoperação:

  • VRHandPoseSender.cs — lê a pose da mão do subsistema XR Hands, serializa para um pacote binário de 45 bytes, envia via UDP
  • VRGripperController.cs — mapeia a força de pinça para um valor de garra normalizado [0, 1]
  • VRTeleoperationManager.cs — gerenciamento de ciclo de vida, UI de status de conexão, reconexão automática

Você faz não precisa recompilar o aplicativo Unity para alternar entre braços robóticos. Exponha os seguintes campos como campos de Inspetor serializados e ajuste-os a partir do Editor Unity ou via um arquivo de configuração:

Campo do Inspetor Valor Inicial do AgileX Piper Notas
IP de destino O endereço IP do seu PC Executar ip addr no PC para encontrá-lo
positionOffset (m) (0, 0, 0.3) Desloca a origem do robô virtual; o alcance do Piper é menor que o do xArm6
rotationOffset (deg) (0, 90, 0) Correção de 90° Y para o quadro Piper CAN; ajuste conforme a orientação da montagem
fatorDeEscala 0.75 Reduz a faixa de movimento da mão para se ajustar ao espaço de trabalho Piper (~600 mm de alcance)
Espaço de Trabalho X (mm) ±400 Deixe uma margem de 50 mm dentro dos limites físicos
Espaço de Trabalho Z (mm) 50 – 700 Mantenha Z min acima da superfície da mesa para evitar colisões
Calibre antes da sua primeira execução ao vivo. Mova o Quest 3 lentamente para cada borda do espaço de trabalho pretendido e observe a posição comandada na saída do seu terminal. Confirme que o robô permanece bem dentro de seus limites de articulação antes de habilitar o streaming em velocidade total.
3

Configuração do Servidor UDP em Python

O servidor Python executa três threads simultâneas: uma fio do receptor que lê datagramas UDP brutos, um thread de segurança que valida pacotes e os coloca na fila, e uma thread de controle do robô que esvazia a fila e chama o controlador do robô. Essa separação garante que chamadas lentas do SDK do robô nunca bloqueiem a recepção de UDP.

Instale as dependências:

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

Execute o servidor de teleoperação:

python3 teleoperation_main.py

O servidor se vincula a 0.0.0.0:8888 (mão direita) e opcionalmente 0.0.0.0:8889 (mão esquerda). Quando um pacote chega com o valid sinalizador ativado, a thread de controle chama robot.set_pose() e robot.set_gripper(). Pressão Ctrl-C para acionar uma parada de emergência e desligamento limpo.

A estrutura simplificada do servidor mostrando como as três threads interagem:

# teleoperation_main.py (simplified structure)
matéria socket, struct, queue, threading, signal, time
de piper_controller matéria 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()

def receptor_udp(port: int):
    """Thread 1 — receber datagramas UDP brutos."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((HOST, port))
    sock.settimeout(1.0)
    enquanto não shutdown_event.is_set():
        barraca:
            data, _ = sock.recvfrom(256)
            pose = parse_packet(data)
            com pose e pose["válido"]:
                barraca:
                    pose_queue.put_nowait(pose)
                exceto queue.Full:
                    pose_queue.get_nowait()   # drop oldest frame
                    pose_queue.put_nowait(pose)
        exceto socket.timeout:
            continuar
    sock.close()

def loop_de_controle_do_robô(robot: PiperController):
    """Thread 3 — esvaziar a fila e comandar o robô a CONTROL_HZ."""
    period = 1.0 / CONTROL_HZ
    last_pose = None
    enquanto não shutdown_event.is_set():
        t0 = time.monotonic()
        barraca:
            pose = pose_queue.get(timeout=0.1)
            com pose["estop"]:
                robot.emergency_stop()
                continuar
            com pose["válido"]:
                last_pose = pose
                x, y, z = transform_position(pose["posição"])
                roll, pitch, yaw = quat_to_euler(pose["rotação"])
                robot.set_pose(x, y, z, roll, pitch, yaw)
                robot.set_gripper(pose["garra"])
            # tracking lost — hold last known position (do not send zero)
        exceto queue.Empty:
            passar
        time.sleep(max(0, period - (time.monotonic() - t0)))
O tamanho da fila controla a latência versus suavidade. A QUEUE_MAXSIZE um valor de 1 oferece latência mínima, mas pode parecer irregular. Um valor de 3–5 suaviza o movimento em caso de perda de pacotes, mas adiciona até 100 ms de latência extra a 30 Hz. Comece em 3 e ajuste conforme necessário.
4

Tutorial do piper_controller.py

A piper_controller.py o módulo envolve o AgileX piper_sdk Biblioteca Python. Ele implementa a interface de cinco métodos esperada por teleoperation_main.py. Decisões de design chave:

  • CAN via USB: O braço Piper se comunica via barramento CAN. O SDK aceita um nome de interface CAN (can0) e requer que a interface esteja habilitada antes de conectar.
  • Modo escravo: Chamando MasterSlaveConfig(0xFC, 0, 0, 0) coloca o braço em modo escravo para que aceite comandos de posição em streaming.
  • Limitação do espaço de trabalho: As posições são fixadas rigidamente aos constantes no topo do arquivo antes de cada chamada ao SDK — estas são a última linha de defesa de software antes do firmware.
  • unidades do SDK: A posição está em micrômetros (inteiro), a orientação em milidegraus — multiplique os valores flutuantes mm/deg por 1000 antes de passar para EndEffectorCtrl.
# piper_controller.py
de piper_sdk matéria C_PiperInterface
matéria 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


aula PiperController:
    """Substituição direta para XArmController — mesma interface de cinco métodos."""

    def __init__(self, can_interface: str = "can0"):
        self.can_interface = can_interface
        self._piper: C_PiperInterface | None = None
        self.connected = False

    def conectar(self) -> bool:
        """Abrir porta CAN e habilitar modo escravo."""
        barraca:
            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("Piper conectado em %s", self.can_interface)
            retornar True
        exceto Exception como e:
            logger.error("Falha ao conectar o Piper: %s", e)
            retornar False

    def desconectar(self):
        """Desabilitar servo e fechar porta."""
        com self._piper:
            barraca:
                self._piper.DisableArm(7)   # disable all joints
            exceto Exception:
                passar
        self.connected = False

    def set_pose(self, x: float, y: float, z: float,
                 roll: float, pitch: float, yaw: float):
        """Mover o efetor final para (x,y,z) mm com orientação (roll,pitch,yaw) graus."""
        se não self.connected:
            retornar
        x = max(X_MIN, min(X_MAX, x))
        y = max(Y_MIN, min(Y_MAX, y))
        z = max(Z_MIN, min(Z_MAX, z))
        barraca:
            self._piper.EndEffectorCtrl(
                int(x * 1000), int(y * 1000), int(z * 1000),
                int(roll  * 1000),
                int(pitch * 1000),
                int(yaw   * 1000),
                SPEED_PERCENT,
            )
        exceto Exception como e:
            logger.warning("erro set_pose: %s", e)

    def set_gripper(self, value: float):
        """Definir abertura do gripper. 0.0 = totalmente fechado, 1.0 = totalmente aberto."""
        se não self.connected:
            retornar
        GRIPPER_MAX_UM = 70_000   # 70 mm max opening in µm
        target_um = int(value * GRIPPER_MAX_UM)
        barraca:
            self._piper.GripperCtrl(target_um, SPEED_PERCENT, 0x01, 0)
        exceto Exception como e:
            logger.warning("erro set_gripper: %s", e)

    def parada_de_emergência(self):
        """Desative imediatamente todas as articulações. Seguro para chamar de qualquer thread."""
        com self._piper:
            barraca:
                self._piper.DisableArm(7)
            exceto Exception:
                passar
Verifique as unidades do SDK para sua versão de firmware. O SDK Piper evolui rapidamente. Em algumas versões de firmware, a posição está em micrômetros (inteiro); em outras, está em milímetros (float). Imprima piper.GetPiperFirmwareVersion() e verifique contra a documentação do desenvolvedor AgileX antes de comandar qualquer movimento.
5

Validação de segurança e primeira sessão

Leia antes de ligar. Comandos de teleoperação VR controlam o robô em tempo real com base no movimento da sua mão. Um positionOffset ou scaleFactor calibrado incorretamente pode fazer com que o braço se mova instantaneamente para uma posição que danifique a si mesmo ou machuque pessoas próximas. Sempre faça um teste sem carga com a energia do robô DESLIGADA primeiro.

Lista de verificação para teste sem carga (energia DESLIGADA):

  • Inicie o servidor Python e conecte o Quest 3. Observe a transform_position saída impressa no terminal.
  • Mantenha sua mão no centro da área de trabalho pretendida. Confirme que os valores XYZ impressos estão próximos à posição inicial do robô (aproximadamente 0, 0, 300 mm para o Piper).
  • Mova sua mão para cada borda da área de trabalho. Confirme que os valores permanecem dentro dos limites fixos e nunca ficam negativos em Z.
  • Pressione o botão Menu no Quest para acionar a parada de emergência do software. Confirme emergency_stop() que é chamado e o loop para.

Primeira sessão ao vivo:

  • Comece em SPEED_PERCENT = 20 — esta é aproximadamente a velocidade máxima da articulação de 40°/s.
  • Ative a energia do servo. Mova lentamente, permanecendo perto da posição inicial do braço durante o primeiro minuto.
  • Verifique se o movimento da mão e o movimento do robô estão na mesma direção. Se o pulso girar para trás, ajuste rotationOffset em ±90° no eixo Y no Inspector do Unity.
  • Expanda gradualmente o alcance de movimento uma vez que o mapeamento de direção esteja confirmado como correto.
  • Mantenha um botão de parada de emergência físico (relé de potência) ao alcance a todo momento.

Dois caminhos de e-stop de software são implementados:

  • Botão do menu Quest 3: Define o Bit 1 do byte de flags em cada pacote UDP subsequente. O servidor Python chama robot.emergency_stop() imediatamente.
  • Ctrl-C (SIGINT): O manipulador de sinal define o evento de desligamento, o que faz com que o loop de controle chame emergency_stop() e saia de forma limpa.
A perda de rastreamento é tratada de forma segura. Quando o Quest 3 perde o rastreamento das mãos (a mão sai do campo de visão da câmera, os dedos se sobrepõem), o valid flag no pacote UDP cai para 0. O servidor Python mantém a última posição conhecida em vez de mover o braço para a posição zero.
6

Interface do Adaptador — Porta para Qualquer Braço

O padrão de troca de controlador se generaliza para qualquer braço com um SDK Python. O único requisito é que sua classe de controlador implemente esses cinco métodos com exatamente esta assinatura:

Método Assinatura Contrato
connect() () → bool Abre o canal de comunicação; retorna True em caso de sucesso
disconnect() () → None Desativa a energia do servo e fecha a porta ou soquete
set_pose(x, y, z, rotação, inclinação, guinada) (float×6) → Nenhum Alvo do efetor final cartesiano em mm + graus; deve ser fixado internamente
set_gripper(valor) (float) → Nenhum Abertura normalizada 0.0–1.0; mapear para unidades específicas do braço internamente
emergency_stop() () → None Deve ser seguro chamar de qualquer thread a qualquer momento

Passos para adicionar um novo braço:

  1. Escrever myarm_controller.py implementando os cinco métodos acima usando o SDK do seu braço. Defina os limites da área de trabalho e os limites de velocidade como constantes de nível de módulo.
  2. Teste unitário em isolamento: chame connect(), então set_pose com uma posição segura a 5 cm da posição inicial. Verifique se o braço se move e retorna a posição esperada.
  3. Troque a importação em teleoperation_main.py: substitua from piper_controller import PiperController pelo seu novo controlador. Nenhuma outra alteração é necessária.
  4. Calibre os parâmetros do Inspector do Unity (positionOffset, rotationOffset, scaleFactor) para a área de trabalho do novo braço.
  5. Valide o comportamento de e-stop, retenção de perda de rastreamento e grampos de espaço de trabalho antes de qualquer sessão do operador.
Usando ROS 2 em vez de um SDK direto? Publique em um geometry_msgs/PoseStamped tópico dentro de set_pose — o restante da arquitetura permanece idêntico. O design baseado em fila absorve naturalmente a latência de um ciclo pub/sub do ROS 2.

Configuração Completa?

Verifique as especificações completas do pacote UDP ou leia o wiki completo do desenvolvedor para uma cobertura mais profunda.