整体四层架构(数据流总览)

  1. Meta Quest(Android):手柄位姿、手部追踪、按键、IMU → 向外发送控制指令
  2. Ubuntu 中间层(ROS2):接收 VR 数据、转换 ROS 话题、运动解算、下发机器人指令;同时回传机器人图像 / 状态给 VR 显示
  3. 机器人端(PC / 开发板,跑 ROS2):底盘 / 机械臂驱动、传感器、运动控制器
  4. 底层通信链路分两大类:
    • VR ↔ Ubuntu PC:TCP/UDP/ROS-TCP 桥(主流),Quest 原生不支持 DDS
    • Ubuntu PC ↔ 机器人:ROS2 原生 DDS 分布式通信(局域网 / 有线)

一、硬件网络前置条件(必须先配置)

1. 同一局域网(核心前提)

  • Ubuntu 电脑、Meta Quest、机器人(Jetson / 工控机)全部连同一个路由器 5G Wi‑Fi,或交换机有线
  • 查看 Ubuntu 本机 IP:ip a,记下 192.168.x.x
  • Quest 设置 → WLAN,查看 VR 内网 IP,PC ping VR IP 验证连通
ping 192.168.1.105  # 替换为QuestIP

2. Ubuntu 防火墙放行端口

# ROS-TCP默认10000,UDP常用8888
sudo ufw allow 10000/tcp
sudo ufw allow 8888/udp
sudo ufw reload

3. Meta Quest 开启开发者模式

Meta 手机 App → 设备 → 开发者模式开启;USB 连接电脑允许调试,SideQuest 可安装自研 Unity VR APP

二、模块 1:Ubuntu ↔ 机器人(ROS2 原生 DDS 通信,最简单)

机器人本身运行 ROS2(Jetson Nano/Orin、工控机 Ubuntu),和上位机 Ubuntu 跨机 DDS 互通,不需要额外桥接

配置步骤

  1. 所有设备(Ubuntu 主机、机器人)终端统一环境变量
# 任意数字0~255,全部设备保持一致
export ROS_DOMAIN_ID=10
# 禁止仅本地回环
export ROS_LOCALHOST_ONLY=0
# 写入~/.bashrc永久生效
echo "export ROS_DOMAIN_ID=10" >> ~/.bashrc
source ~/.bashrc
  1. 验证通信

机器人端发布话题:

​ros2 topic pub /robot/cmd_vel geometry_msgs/msg/Twist "{linear: {x:0.1}}"

Ubuntu 主机查看:

​ros2 topic list、ros2 topic echo /robot/cmd_vel 

能收到即成功

原理

ROS2 底层 DDS(FastDDS/CycloneDDS)基于 UDP 组播,同 Domain ID 自动发现所有节点; 机器人底盘、机械臂、激光雷达全部通过标准 Topic/Service/Action 交互,是整套系统的数据核心枢纽

三、模块 2:Meta Quest ↔ Ubuntu ROS2(3 套主流通信方案,按需求选)

Quest 是 Android 系统,不原生支持 DDS/ROS2,必须通过桥接层中转 VR 数据到 ROS 话题。

方案 1:ROS-TCP-Endpoint(工业标准,推荐新手)

架构

Quest Unity APP(ROS#/ROS-TCP-Connector)→ TCP 长连接 → Ubuntu ros_tcp_endpoint ROS2 节点 → 生成标准 ROS 话题(手柄位姿、手部追踪、按键)

完整部署步骤
  1. Ubuntu 工作空间拉取桥接包
cd ~/colcon_ws/src
git clone -b main-ros2 https://github.com/ros2/ros_tcp_endpoint.git
cd .. && colcon build --packages-select ros_tcp_endpoint
source install/setup.bash
  1. 启动 TCP 服务端(Ubuntu)
# 默认端口10000,监听Quest连接
ros2 run ros_tcp_endpoint default_server_endpoint --ros-args -p server_ip:=0.0.0.0
  1. Unity 侧开发 Quest APP
  • Unity 导入 ROS-TCP-Connector + Oculus XR Interaction SDK
  • VR 脚本填写 Ubuntu 电脑局域网 IP、端口 10000
  • 发布手柄 Pose、手部追踪、按键到 ROS 话题:/vr/left_controller /vr/right_controller /vr/hands
  • 可双向通信:机器人图像 / TF 坐标通过 TCP 回传到 VR 渲染机器人模型、相机画面
  1. 现成免开发 APP:Quest2ROS 商店 /sidequest 直接安装,填入 Ubuntu IP 即可自动发布 VR 数据,无需写 Unity 代码
优缺点

 双向通信、完美兼容所有 ROS 消息、支持 TF / 图像 / 点云、生态成熟

TCP 有轻微延迟(20~60ms),高速手部精细抓取场景延迟敏感

方案 2:原生 UDP Socket(低延迟,机械臂遥操首选)

架构

Quest Unity UDP 客户端(高频发包手柄 / 手部坐标)→ Ubuntu Python UDP 服务节点 → 封装成 ROS2 Topic 下发机器人

  1. Quest 侧:Unity C# 脚本每 10ms 发送二进制位姿包(端口 8888)
  2. Ubuntu 接收节点(Python)示例逻辑:
import socket, rclpy
from geometry_msgs.msg import PoseStamped
# UDP监听
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 8888))
# 解析VR手柄坐标,发布/vr/right_pose话题给机械臂MoveIt2
优缺点

延迟极低(<15ms),适合高精度手控机械臂抓取

需要手动序列化 / 反序列化数据,无原生 ROS 消息支持,双向图像传输麻烦

方案 3:Websocket / Vuer(无需 Unity,浏览器 VR,快速原型)

架构

Ubuntu 启动robotis_vuer ROS 节点 → 本地 Web 服务 → Quest 浏览器打开网页 VR,手部追踪通过 Websocket 传 ROS 话题

适用场景

快速验证遥操作逻辑、无 Unity 开发环境;缺点画质、追踪精度低于原生 Unity APP

四、完整三层数据流示例(VR 遥控机械臂)

正向控制流(VR → 机器人)

  1. Meta Quest 3:XR 手部追踪读取左右手 6DOF 位姿、夹爪按键
  2. Unity APP → TCP 发送至 Ubuntu ros_tcp_endpoint
  3. Ubuntu 生成 ROS 话题 /vr/hand_target_pose(PoseStamped)
  4. ROS2 MoveIt2 订阅该话题,计算机械臂运动轨迹
  5. MoveIt2 下发关节速度话题 /arm/joint_cmd
  6. 机械臂 Jetson 开发板(同 DDS Domain)订阅关节指令,驱动电机运动

反向反馈流(机器人 → VR 视觉)

  1. 机器人相机节点发布 /camera/image_raw
  2. Ubuntu TCP 桥转发图像流至 Quest Unity
  3. VR 眼镜内实时渲染机器人第一视角画面,实现沉浸式遥操
  4. 机器人 TF 坐标树通过 TCP 同步,VR 渲染真实尺寸机器人 3D 模型

五、三种通信方案对比选型表

方案 延迟 开发成本 双向图像 / TF 适用场景
ROS-TCP 桥 20~60ms 中(Unity 简单集成) 完整支持 移动机器人、通用遥操、SLAM 可视化
UDP 原生 Socket 8~20ms 高(手动编解码) 不适合大图 高精度机械臂、精细抓取
Vuer 网页 VR 50~120ms 极低(无需 Unity) 低清图像 快速 Demo、教学原型

六、常见排错关键点

  1. Quest 收不到 Ubuntu 数据
    • 检查二者同 5G 局域网,关闭电脑防火墙,放行 10000/8888 端口
    • Unity 内 IP 填Ubuntu 内网 IP,不要 127.0.0.1
  2. Ubuntu 看不到 VR ROS 话题
    • 确认ros_tcp_endpoint正常运行,Quest APP 已成功连接服务端
    • QoS 匹配:VR 端消息可靠性设为 RELIABLE
  3. Ubuntu 和机器人无 ROS 通信
    • 全部设备ROS_DOMAIN_ID完全一致,关闭本地仅回环
    • 机器人 Wi‑Fi 切换 5G,避免 2.4G 组播丢包
  4. VR 操控机械臂延迟高
    • 切换 UDP 方案、路由器换 Wi‑Fi6、关闭 Quest 后台程序
    • DDS 开启 CycloneDDS 优化单播传输,关闭组播冗余

七、两套完整代码:Unity ROS-TCP 发送 VR 手柄 + Ubuntu Python 接收转发 ROS2

  1. 通信链路:Meta Quest (Unity) ↔ Ubuntu (ros_tcp_endpoint) ↔ ROS2 机器人
  2. 环境准备
    • Ubuntu 22.04 Humble,装好 ros_tcp_endpoint
    • Unity 导入包:ROS-TCP-Connector + Meta XR SDK
    • 所有设备同局域网,Ubuntu IP 举例:192.168.1.100

八、Unity C# 脚本(Quest 手柄位姿发布 ROS 话题)

挂载在 XR Origin / Right Controller 物体上

using UnityEngine;
using RosMessageTypes.Geometry;
using RosMessageTypes.Std;
using Unity.Robotics.ROSTCPConnector;
using UnityEngine.XR;

public class VRControllerPublisher : MonoBehaviour
{
    // ROS TCP配置
    private ROSConnection ros;
    // 话题名
    private const string controllerTopic = "/vr/right_controller_pose";
    // 手柄输入设备
    private InputDevice rightController;

    void Start()
    {
        // 填入你的Ubuntu内网IP
        ros = ROSConnection.GetOrCreateInstance();
        ros.RosIPAddress = "192.168.1.100";
        ros.RosPort = 10000;
        ros.Connect();

        // 获取右手柄设备
        var devices = InputTracking.GetDevices();
        foreach (var dev in devices)
        {
            if (dev.characteristics.HasFlag(InputDeviceCharacteristics.Controller)
                && dev.characteristics.HasFlag(InputDeviceCharacteristics.Right))
            {
                rightController = dev;
                break;
            }
        }
    }

    void Update()
    {
        if (!rightController.isValid) return;

        // 读取手柄6DOF位姿
        rightController.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 pos);
        rightController.TryGetFeatureValue(CommonUsages.deviceRotation, out Quaternion rot);

        // 转换Unity坐标系 → ROS坐标系(Unity Y上,ROS Z上)
        MposeStampedMsg poseMsg = new MposeStampedMsg();
        poseMsg.header.frame_id = "vr_base";
        poseMsg.header.stamp = ros.GetTime();

        // 坐标转换修正
        poseMsg.pose.position.x = pos.x;
        poseMsg.pose.position.y = pos.z;
        poseMsg.pose.position.z = pos.y;

        poseMsg.pose.orientation.x = rot.x;
        poseMsg.pose.orientation.y = rot.z;
        poseMsg.pose.orientation.z = rot.y;
        poseMsg.pose.orientation.w = rot.w;

        // 发布位姿话题
        ros.Publish(controllerTopic, poseMsg);
    }
}

Unity 配套设置

  1. Package Manager 安装:ROS-TCP-ConnectorMeta XR All-in-One SDK
  2. 场景添加空物体 ROSConnection
  3. 脚本挂载到右手柄,修改代码内 IP 为你电脑 IP
  4. Build APK 侧载到 Meta Quest

九、Ubuntu Python ROS2 接收脚本(订阅 VR 手柄话题)

功能:接收手柄 Pose,可直接转换为机械臂目标位姿 / 小车速度指令 新建 vr_listener.py,放入你的 python 功能包

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseStamped, Twist

class VRControllerListener(Node):
    def __init__(self):
        super().__init__("vr_controller_listener")
        # 订阅VR手柄位姿
        self.sub = self.create_subscription(
            PoseStamped,
            "/vr/right_controller_pose",
            self.vr_callback,
            10
        )
        # 发布小车速度指令示例
        self.cmd_pub = self.create_publisher(Twist, "/cmd_vel", 10)
        self.get_logger().info("VR手柄监听节点已启动")

    def vr_callback(self, msg: PoseStamped):
        """收到VR手柄数据回调"""
        x = msg.pose.position.x
        y = msg.pose.position.y
        z = msg.pose.position.z

        self.get_logger().info(f"VR手柄坐标 x:{x:.2f}, y:{y:.2f}, z:{z:.2f}")

        # 简单映射手柄前后左右 → 小车线速度角速度
        twist = Twist()
        twist.linear.x = x * 0.3
        twist.angular.z = -y * 0.8
        self.cmd_pub.publish(twist)

def main(args=None):
    rclpy.init(args=args)
    node = VRControllerListener()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == "__main__":
    main()

配置运行步骤

  1. 功能包 setup.py 添加入口
entry_points={
    'console_scripts': [
        'vr_listener = my_py_pkg.vr_listener:main',
    ],
},
  1. 编译运行
cd ~/colcon_ws
colcon build --packages-select my_py_pkg
source install/setup.bash
ros2 run my_py_pkg vr_listener
  1. 验证数据 新开终端查看 VR 话题:
ros2 topic echo /vr/right_controller_pose

十、低延迟 UDP 方案全套代码(机械臂高精度遥操)

1. Ubuntu UDP 服务端(接收 VR 二进制坐标,发布 ROS)

import socket
import struct
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseStamped

UDP_PORT = 8888
UDP_BIND_IP = "0.0.0.0"

class UdpVrReceiver(Node):
    def __init__(self):
        super().__init__("udp_vr_receiver")
        self.pub = self.create_publisher(PoseStamped, "/vr/udp_hand_pose", 10)
        # 创建UDP套接字
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.bind((UDP_BIND_IP, UDP_PORT))
        self.sock.setblocking(False)
        self.timer = self.create_timer(0.01, self.recv_loop)
        self.get_logger().info(f"UDP监听 {UDP_BIND_IP}:{UDP_PORT}")

    def recv_loop(self):
        try:
            data, addr = self.sock.recvfrom(1024)
            # 解析6个float: px,py,pz,qx,qy,qz,qw
            px, py, pz, qx, qy, qz, qw = struct.unpack("7f", data)
            msg = PoseStamped()
            msg.header.frame_id = "vr_base"
            msg.pose.position.x = px
            msg.pose.position.y = py
            msg.pose.position.z = pz
            msg.pose.orientation.x = qx
            msg.pose.orientation.y = qy
            msg.pose.orientation.z = qz
            msg.pose.orientation.w = qw
            self.pub.publish(msg)
        except BlockingIOError:
            return

def main(args=None):
    rclpy.init(args=args)
    node = UdpVrReceiver()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == "__main__":
    main()

2. Unity UDP 发送 C# 脚本

using UnityEngine;
using System.Net.Sockets;
using System.Net;
using UnityEngine.XR;

public class VrUdpSender : MonoBehaviour
{
    private Socket udpSocket;
    private IPEndPoint serverEp;
    private InputDevice rightController;
    byte[] sendBuf = new byte[28]; // 7个float *4字节

    void Start()
    {
        // Ubuntu IP端口
        string serverIp = "192.168.1.100";
        int port = 8888;
        udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        serverEp = new IPEndPoint(IPAddress.Parse(serverIp), port);

        // 获取右手柄
        var devs = InputTracking.GetDevices();
        foreach (var d in devs)
        {
            if (d.characteristics.HasFlag(InputDeviceCharacteristics.Right | InputDeviceCharacteristics.Controller))
            {
                rightController = d;
                break;
            }
        }
    }

    void Update()
    {
        if (!rightController.isValid) return;
        rightController.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 pos);
        rightController.TryGetFeatureValue(CommonUsages.deviceRotation, out Quaternion rot);

        // 打包7个浮点数
        System.BitConverter.GetBytes(pos.x).CopyTo(sendBuf, 0);
        System.BitConverter.GetBytes(pos.z).CopyTo(sendBuf, 4);
        System.BitConverter.GetBytes(pos.y).CopyTo(sendBuf, 8);

        System.BitConverter.GetBytes(rot.x).CopyTo(sendBuf, 12);
        System.BitConverter.GetBytes(rot.z).CopyTo(sendBuf, 16);
        System.BitConverter.GetBytes(rot.y).CopyTo(sendBuf, 20);
        System.BitConverter.GetBytes(rot.w).CopyTo(sendBuf, 24);

        udpSocket.SendTo(sendBuf, serverEp);
    }
}
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐