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.
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))"
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.
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 |
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)))
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.
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
piper.GetPiperFirmwareVersion() e verifique contra a documentação do desenvolvedor AgileX antes de comandar qualquer movimento.
Validação de segurança e primeira sessão
Lista de verificação para teste sem carga (energia DESLIGADA):
- Inicie o servidor Python e conecte o Quest 3. Observe a
transform_positionsaí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
rotationOffsetem ±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.
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.
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:
- Escrever
myarm_controller.pyimplementando 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. - Teste unitário em isolamento: chame
connect(), entãoset_posecom uma posição segura a 5 cm da posição inicial. Verifique se o braço se move e retorna a posição esperada. - Troque a importação em
teleoperation_main.py: substituafrom piper_controller import PiperControllerpelo seu novo controlador. Nenhuma outra alteração é necessária. - Calibre os parâmetros do Inspector do Unity (
positionOffset,rotationOffset,scaleFactor) para a área de trabalho do novo braço. - 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.
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.