基于 PICO 的 WebXR VR 遥操作

WebXR 控制器接入 EVA 的通用 teleop client,在 EVA 内将控制器的相对增量重定向为 EEF 目标,再经过 FK/IK 与安全检查后发送到机器人。

定位与支持范围

本页描述仓库在 0.2.0 中加入的基于 PICO 的 WebXR 输入路径。VR 节点是运行在 EVA 外部的输入适配器:它提供一个网页,读取浏览器的 WebXR 控制器姿态和按键,经过协议校验后通过 ZeroMQ 把规范化帧发布给 EVA。节点不连接机器人驱动,也不直接驱动电机;真正的机器人动作仍由 EVA 的 transport 执行。

PICO 是优先验证和推荐的设备。PICO 4、PICO 4 Ultra 等控制器配置与同一个 WebXR 节点配合使用;Quest 也兼容同一节点,Quest 的 Oculus Touch profile 会走同一套规范化与重定向路径。设备兼容不等于所有机器人都已支持 VR。当前仓库只有下表三套 WebXR 采集/RL 预设;其他机器人仍使用实体 Leader/Follower 或 ROS transport 输入。

机器人正式 VR 采集预设控制器绑定边界
AgiBot G2configs/02_collection/agibot_g2_vr.pyleft_arm=leftright_arm=rightWebXR 主采集路径;另有对应 RL 预设。
ARX X5configs/02_collection/arx_x5_vr.pyleft_arm=leftright_arm=rightWebXR 遥操作;硬件节点没有接入实体 Leader 适配器。
Dual Frankaconfigs/02_collection/dual_franka_vr.pyleft_arm=leftright_arm=rightWebXR 主采集路径;另有对应 RL 预设。
不要套用预设。UR5e、AgileX Piper、ARX R5、R1 Lite 等列表中的其他适配器没有 WebXR 预设。把它们的配置改成 vr_webxr 并不会自动获得 VR 支持。

控制数据路径:从控制器输入到经校验的机器人动作

浏览器向节点的 /ws 发送浏览器帧(浏览器帧自身的 version1)。节点检查 token、姿态维度、四元数和参考空间,把帧包装为 eva.teleop.vr 协议的 version=3,分配 session 并传递浏览器提供的递增 sequence,再从 PUB 端点 tcp://127.0.0.1:8765 发布。EVA 的 VR client 通过 SUB 接收帧,并从 tcp://127.0.0.1:8766 的 ACK 通道回送事件确认。

每个控制 tick,EVA 先读取机器人当前关节反馈并计算当前 canonical EEF(每臂八个值:x y z qw qx qy qz gripper)。VrTeleopClient 输出每臂的 canonical EEF 目标和 active-arm mask;应用层用当前关节位置作为 IK seed 求解,然后用 FK 验证跟踪误差,限幅后调用 transport 的 publish_action。因此 WebXR 节点负责输入和事件,teleop client 负责重定向,EVA 应用负责 IK、门控和发布。

text
PICO/Quest WebXR
    │  WebSocket /ws(token)
    ▼
vr_webxr/node.py(协议 v3、事件、心跳)
    │  ZMQ PUB 8765 / PULL ACK 8766
    ▼
EVA VrTeleopClient(相对重定向、逐臂授权)
    │  canonical EEF → IK → FK 误差与 qpos 限幅
    ▼
EVA transport.publish_action → 真实机器人
坐标含义。控制器发送的是 XR locallocal-floorbounded-floor 参考空间中的绝对姿态;它不是机器人坐标。客户端只累计相邻控制器姿态的相对增量,并叠加到首次锁存的机器人 EEF。

开始前准备 PICO

以下流程针对“EVA/节点在远程主机、PICO 通过 USB 连接到 Ubuntu 主机”的推荐拓扑。把所有相对路径命令放在 eva-client-new 仓库根目录执行。PICO 访问本机回环地址时使用 SSH 与 ADB 转发;不要把未加密的远程 IP HTTP 页面暴露给设备。

远程安全要求。若通过远程 IP 直接访问节点,必须为节点设置 --tls-cert--tls-key,使浏览器走 HTTPS/WSS,或使用 TLS 终止的反向代理。源码只把本机回环作为开发例外;PICO 推荐本机回环 + SSH/ADB。

端到端启动 PICO

先启动节点,再启动 EVA 和对应采集配置。节点会打印随机 token(没有传入 --token 时自动生成)及 PICO 命令;保存本次日志中的 token,不要猜测或省略它。

启动 WebXR 节点

在 EVA Client 仓库中执行源码 README 给出的页面端口和 ZMQ 端点:

bash
cd /path/to/eva-client-new
source .venv/bin/activate

python examples/input_sources/vr_webxr/node.py \
  --host 127.0.0.1 \
  --port 43876 \
  --endpoint tcp://127.0.0.1:8765 \
  --ack-endpoint tcp://127.0.0.1:8766

也可以用仓库脚本启动节点;它会定位仓库、激活 .venv,并把 VR_TOKEN 传给节点:

bash
VR_TOKEN="<OPTIONAL_TOKEN>" ./examples/input_sources/vr_webxr/run_node.sh

如果省略 --token 或令 VR_TOKEN 为空,节点仍会生成随机 token,但之后必须使用日志打印的 token。open_pico.sh 会拒绝空 token。

把页面端口转到 Ubuntu

在与 PICO 连接的 Ubuntu 主机上保持 SSH 隧道运行。下面的 <remote-host> 是运行节点的主机;命令把 Ubuntu 的回环 43876 转到远程主机的同名回环端口。

bash
ssh -N \
  -p 22 \
  -o ExitOnForwardFailure=yes \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -L 43876:127.0.0.1:43876 \
  <remote-user>@<remote-host>

确认 ADB 并打开 PICO 页面

不要跳过设备授权检查。先查询序列号;只有一个已授权设备时脚本会自动选它,多个设备时必须明确指定。

bash
adb devices -l

export VR_TOKEN="<TOKEN_FROM_NODE_LOG>"
./examples/input_sources/vr_webxr/open_pico.sh

脚本会执行 adb reverse tcp:43876 tcp:43876,再以 mode=ar 打开带 token 的页面。多台设备时使用参数或环境变量,参数优先于环境变量:

bash
./examples/input_sources/vr_webxr/open_pico.sh "<PICO_SERIAL>"

export PICO_SERIAL="<PICO_SERIAL>"
export VR_TOKEN="<TOKEN_FROM_NODE_LOG>"
./examples/input_sources/vr_webxr/open_pico.sh

页面打开后点击 ENTER MR,允许浏览器进入 WebXR 会话。若只是刷新页面,可使用源码 README 中的 URL 形式(包含 token、mode=ar 与时间戳):

bash
VR_URL="http://127.0.0.1:43876/?token=<TOKEN_FROM_NODE_LOG>&mode=ar&reload=$(date +%s)"
adb -s "$PICO_SERIAL" shell "am start -S -a android.intent.action.VIEW -d '$VR_URL'"
连接检查。节点终端应显示 WebXR node ready 及单行 VR RX seq=... 诊断;序列号应递增。EVA 控制台的 VR 状态还应显示已连接、较小的 input age 和正确的授权臂。

启动 EVA 的三套官方采集预设

在另一个终端仍位于 EVA Client 仓库根目录,三选一启动。它们都把 collection.teleop.control_source 设为 "client",并构建 vr_webxr client;不要同时运行多个机器人预设来“合并”控制。

目标命令采集特点
AgiBot G2eva --config configs/02_collection/agibot_g2_vr.py双臂 WebXR;使用继承配置的采集 schema。
ARX X5eva --config configs/02_collection/arx_x5_vr.py双臂、三路相机 schema 与任务列表;输出目录为 work_dirs/collection/arx_x5_vr
Dual Frankaeva --config configs/02_collection/dual_franka_vr.py双臂 WebXR;使用继承配置的采集 schema。

进入 COLLECT 后,先确认机器人反馈和 VR client 已连接,再按下表顺序操作:短按右手 B 打开全局 ARM,分别长按两只手的 grip 获得逐臂授权,最后短按右手 A 开始录制。录制期间让对应手的授权保持开启(它是切换状态,不需要持续按住 grip);停止时再次短按右手 A,取消时长按右手 A。

真实动作前先验证。首次运行应先在不接触危险空间的情况下确认左右控制器、坐标旋转和夹爪方向。ARM、逐臂授权、workspace 与 IK 阈值都必须通过后才会发布动作。

控制器按键与三条控制语义

节点为各控制器采用六个 WebXR gamepad button 槽位的固定布局:trigger=0grip=1primary=4secondary=5;button 2、3 不映射为操作事件。不同厂商的 profile 会被记录,但不改变这组已实现的布局。下表的 A/B/X/Y 是源码注释和测试采用的设备面板语义,括号内保留实际槽位。

输入边沿/时长发送或触发的语义
两手 trigger(button 0)每帧归一化到 [0,1]夹爪目标输入;不授权手臂也不等于停止夹爪更新,但 ARM OFF 时不会发布机器人动作。
两手 grip(button 1)默认长按 1000 ms;再次长按切换本地 debounce 后发送 grip_engaged。每只手独立授权,原始 grip 按键本身不发送给 EVA;切换后节点尝试发送强度 0.6、时长 80 ms 的 haptic。
右手 A / primary(button 4)短按在 release 产生;按住至少 1000 ms 只产生一次长按事件短按:record_toggle;长按:record_cancel,并抑制随后 release 的 toggle。
右手 B / secondary(button 5)短按在 release 产生arm_toggle,切换 COLLECT 的全局 ARM gate。
左手 X / primary(button 4)release 产生home;只有 ARM OFF、collection 未激活等共享状态允许时才会被接受。
左手 Y / secondary(button 5)release 产生intervention_toggle;用于已设置好的 RL REAL/HIL workspace。

这三条语义必须分开理解。全局 ARM是 EVA collection 的总门:B 通过共享的 web:collect_arm:on/off 命令改变它,ARM OFF 时任何手都不能让 collection 发布动作。逐臂 grip authorization是每只手的运动许可:只有 grip_engaged=true 的臂生成空间跟随目标,未授权臂的关节保持之前的安全值。trigger 夹爪是夹爪目标通道:它把 button 0 的数值按 gripper mapping 变成夹爪值,既不是 ARM 开关,也不是 grip authorization。

全局 ARM 切换或 teleop reset 会清空两手的授权和累计姿态参考;重新打开 ARM 不会恢复旧的授权。重连也不会沿用旧控制器 session。这样做会多一步操作,却避免头显、控制器或网络状态改变后自动重新运动。

增量重定向、姿态与夹爪

WebXR 帧中的 position 是三维位置,orientation_xyzw 是 xyzw 顺序四元数。节点和 client 都会检查有限值、维度及非零四元数,并归一化它。允许的参考空间只有 locallocal-floorbounded-floor。这些值描述头显的 XR 参考系,不能直接拿来当作机器人世界坐标。

每臂第一次在 grip_engaged 状态下更新时,retargeter 锁存当前测得的机器人 EEF 作为 home EEF,同时锁存控制器的参考位置和参考旋转;该帧输出 home,不产生跳变。之后每帧把“本帧相对于上一帧”的位置和旋转累加,再以 base_from_xr_rotation 把 XR 轴变换到机器人基座,以 position_scale 缩放位置,最后叠加到 home。姿态使用相对旋转乘 home 旋转,输出仍是 canonical 的 qw qx qy qz

松开逐臂 grip 会冻结已经累计的 EEF 增量,同时清空该臂的控制器参考;手可以在新的位置重新授权,新的相对增量会接着旧目标继续,不会因松开或重握产生跳变。完整 reset 会清空 home、参考位置、参考旋转、累计位置和累计旋转。一个臂追踪丢失时该 tick 被拒绝,恢复后沿用锚点,不会用错误姿态重新锚定。

eef_filter_alpha 是 EEF 低通滤波系数,范围为 (0,1]1.0 表示关闭平滑;较小值平滑位置和四元数,但夹爪值保持即时响应。可选 workspacemin/max 给出三个位置轴的有限边界;越界 target 会被 client 拒绝,不会下发。

夹爪 mode源码计算适用解释
binarytrigger ≥ threshold 时取 close_value,否则取 open_value阈值式开/关。
analog / linearopen_value + trigger * (close_value - open_value)官方三套 VR 预设均使用 linear;PICO 数字式 trigger 被按压时也会归一化为 1.0
toggletrigger 跨过 threshold 的上升沿时在开/关目标之间切换只在配置选择该 mode 时使用,状态由 retargeter 保存。

配置字段与官方简化示例

VR 字段位于 collection.teleop。应用会把 client 的 arm group 名称与机器人运行时的 arm groups 对照;因此 arms 必须覆盖且只覆盖机器人实际的组名,每个组绑定唯一的 leftright 控制器。配置解析阶段会拒绝错误端点、非正 timeout、非法旋转矩阵、非法 workspace 和超出 [0,1] 的阈值。

作用与约束
collection.teleop.control_source设为 "client" 才使用 client;"transport" 是另一条硬件侧 Leader 路径。
collection.teleop.client.type设为 "vr_webxr",由通用 factory 构建 VrTeleopClient
endpoint / ack_endpoint必须是不同的 tcp:// 地址;官方节点为 tcp://127.0.0.1:8765tcp://127.0.0.1:8766
input_timeout_s / heartbeat_timeout_s前者限制帧新鲜度,后者限制节点心跳年龄;三套预设分别为 0.252.0 秒。
position_scale / base_from_xr_rotation位置缩放必须为正;轴变换必须是有限、正交、行列式为 1 的 3×3 旋转矩阵。
eef_filter_alpha可选的 EEF 平滑系数;在 (0,1],省略时为 1.0
workspace.min/max可选三个位置轴边界;两者是有限三维向量且每个 min 小于 max。
grippermodebinaryanaloglineartogglethreshold[0,1],并提供有限的 open_value/close_value
arms.<group>.controller逐臂绑定 leftright;臂级 workspace、旋转、scale、filter 和 gripper 可覆盖共享值。
collection.teleop.safety应用层阈值:max_qpos_stepmax_position_error_mmax_orientation_error_rad

下面的片段取自正式的 ARX X5 VR 预设(其中的通用字段也出现在 Dual Franka 预设),省略了机器人基础配置、ARX X5 的相机和任务列表。修改时应以目标机器人的官方文件为准;例如 AgiBot G2 的正式 gripper 端点是 open_value=0.0close_value=-0.785,不能盲目复制下面的 1.0/0.0

python
collection = dict(
    teleop=dict(
        _delete_=True,
        control_source="client",
        safety=dict(
            max_qpos_step=0.08,
            max_position_error_m=0.08,
            max_orientation_error_rad=0.35,
        ),
        client=dict(
            type="vr_webxr",
            endpoint="tcp://127.0.0.1:8765",
            ack_endpoint="tcp://127.0.0.1:8766",
            input_timeout_s=0.25,
            heartbeat_timeout_s=2.0,
            position_scale=1.0,
            eef_filter_alpha=0.1,
            base_from_xr_rotation=[
                [0.0, 0.0, -1.0],
                [-1.0, 0.0, 0.0],
                [0.0, 1.0, 0.0],
            ],
            gripper=dict(
                mode="linear", threshold=0.6,
                open_value=1.0, close_value=0.0,
            ),
            arms=dict(
                left_arm=dict(controller="left"),
                right_arm=dict(controller="right"),
            ),
        ),
    ),
)
不要绕过校验。若要增加每臂 workspace 或覆盖参数,保持字段形状和约束;不应把机器人坐标值填入 XR 的绝对位置,也不应为了消除拒绝而无限放大边界或误差阈值。

ARM、FK/IK 与发布前安全机制

切换到 COLLECT 时 EVA 可以预热 client teleop 的 FK/IK 路径:它使用机器人初始 qpos 做一次 FK→IK→FK 验证,但预热不会发布机器人动作。正式每 tick 会读取 transport 的最新 qpos,先算 measured EEF,再轮询 VR client。client 返回 command 后,应用层用当前 qpos 作为 seed 解 IK,随后比较目标 EEF 与 FK 回算 EEF。

检查官方行为不通过时
全局 ARM 与 collection 状态必须处于 COLLECT、collection teleop 已激活且 ARM ON;RL workspace 对 collection VR 按键另有限制。不处理或拒绝事件,不发布 collection 动作。
逐臂 active mask握住授权手时该臂 active;未授权臂的关节恢复上一安全 qpos,但保留其独立的 gripper target。只保持该臂关节,不会因为另一臂有效而带动它。
workspaceretargeter 检查目标位置是否在每臂的 min/max 内。返回 rejected,本 tick 不发布;后续安全帧可以恢复。
IK/FK 残差位置误差上限 0.08 m,姿态误差上限 0.35 rad记录 rejected fault,不发布该 tick。
qpos 与夹爪限幅非夹爪关节每 tick 的步长上限为 0.08;夹爪值裁剪到机器人 gripper_open/gripper_close 的 min/max。不允许突变;非法或非有限命令会被丢弃。

client 使用显式的 TeleopResultCOMMAND 才能进入 IK 和发布,IDLE 表示没有可用动作,REJECTED 表示本帧或目标不安全。即使一次 stale、tracking loss、workspace 或 IK tick 被丢弃,collection 生命周期也不会因此自动重启;应查看状态后再继续或停用 teleop。

新鲜度、会话与重连的 fail-closed 规则

安全状态不仅由姿态决定,也由消息健康决定。节点每约 0.25 s 发布 heartbeat;EVA 只有在 worker 健康、浏览器连接、最近见到节点不超过 heartbeat_timeout_s=2.0 秒时才报告 connected。VR frame 的 seq 必须是非负整数且严格大于上一帧;旧的或重复 sequence 会被忽略。帧还必须带非空 session_id,事件必须属于当前 session 并按 event id 去重。

恢复动作。看到断开或 stale 时先松开所有按键和两手 grip,等待 neutral/connected,再重新短按 B 打开 ARM 并分别长按 grip。旧 session 的姿态绝不会自动恢复。

故障排查

排查时按“设备 → 页面 → 节点 → ZMQ → EVA → 机器人”的顺序检查。日志中的 token、session、sequence 和 source error 只用于当前会话;不要把 token 写进公开脚本或提交到仓库。

现象检查与处理
adb was not found安装 Android Platform Tools 并确认 adbPATH;脚本在检查通过前不会继续。
No authorized ADB device is connected检查 USB 线和头显授权提示,再重跑 adb devices -l;状态必须是 device
多台 ADB 设备把准确的序列号作为 ./examples/input_sources/vr_webxr/open_pico.sh "<PICO_SERIAL>" 的参数,或设置 PICO_SERIAL
VR_TOKEN is required / 页面 401从当前节点日志复制 token,执行 export VR_TOKEN="<TOKEN_FROM_NODE_LOG>" 后重开页面。token 缺失、拼写错误或不属于当前节点实例都不能通过 /ws 鉴权。
PICO 页面打不开或刷新无效确认 SSH 隧道仍在运行、端口是 43876,再确认 adb reverse tcp:43876 tcp:43876;用脚本重新打开。远程 IP 直连需 HTTPS/WSS。
节点无法启动 ZMQ bridge检查 87658766 是否已有进程占用,且 endpoint 与 ack-endpoint 不同;只保留一个节点实例。
EVA 显示 VR input is not connected先看节点是否 WebXR node ready,再看 VR RX seq 是否递增;检查 EVA 配置的 tcp://127.0.0.1:8765/:8766 与节点完全一致。
VR input frame stale 或 heartbeat disconnected重新进入 MR,检查页面与 SSH/ADB 路径,确保节点未退出;按 neutral 流程恢复,不要期待旧帧继续运动。
VR left/right controller is not tracked检查头显追踪与对应控制器电量/可见性;控制器恢复 valid=true 后,重新在安全位置长按该手 grip。
VR target outside workspace把控制器带回配置边界内,确认 workspace.min/max 与机器人工作空间一致;不要为了消除报错而删除真实安全边界。
IK failed 或位置/姿态 residual 超阈值检查机器人当前 qpos 反馈、EEF/URDF 标定、base_from_xr_rotation 和目标姿态;修正配置或起始姿态后再试,不要跳过 0.08 m/0.35 rad 检查。
只有一臂动、夹爪方向反或按键无效检查 arms.left_arm.controller/right_arm.controller、gripper 的 open/close 端点以及 button 4/5 的 release 语义;观察事件 ACK 与 authorized_groups,不要把 B 的 ARM 状态当成 grip authorization。
重连后没有动作这是预期的保护:松开所有控制,等待新 session 的 neutral 帧;再次 B→ARM ON,再分别长按 grip。旧累计 delta 和权限已被清除。

成功判据与采集结果

一次可接受的 VR 控制测试应同时满足四层判据:节点仍在运行且 VR RX seq 递增;EVA 状态的 connected=true、input age 小于 250 ms、heartbeat 未超时;ARM ON 后每个要动的 group 出现在 authorized_groupsengaged_groups,发布计数增加;首次 grip 帧从当前测得 EEF 开始,没有跳变,随后动作保持在 workspace 与 IK/FK 阈值内。

采集时,短按右手 A 开始,按需移动和操作夹爪,再短按右手 A 停止;等待 COLLECT 状态回到 idle。成功保存的 episode 会在任务数据集下新增 data/chunk-000/episode_NNNNNN.parquet、每个相机目录下的 videos/chunk-000/<camera>/episode_NNNNNN.mp4,并在 meta/episodes.jsonl 增加一行。长按 A 取消的录制不应写入内容。

先检查再扩大规模。若只需要确认链路,可先验证状态、neutral、授权和一帧无跳变目标;确认安全后再录制完整任务。红色质量 episode 仍会保存,但应在 COLLECT/REPLAY 中复核。

下一步