返回Guides

空间鼠标远程操作指南:机器人学习的设置,校准和数据收集

如何使用3D连接SpaceMouse用于机器人远程操作:ROS2驱动程序设置,轴线映射,死区校准和录制HDF5示范用于模仿学习.OpenArm和ViperX兼容.

如何使用3D连接SpaceMouse用于6DOF机器人远程操作 驾驶员安装,轴线映射,死区校准和录制示范在HDF5格式中模仿学习.

为什么太空鼠标进行远程操作?

3D连接空间鼠标是一个6DOF输入设备,将手动转换为同时转换 (X,Y,Z) 和旋转 (滚动,音量,) 命令.

  • 6-DOF在一只手中: 一个SpaceMouse同时控制机器人终端效应器的六个自由度.没有模式切换,没有按组合.这使其成为大多数操作员最直观的单设备远程操作接口.
  • ** 进入成本较低:** SpaceMouse Compact ($129) 与成本高于10-100倍的系统相同的6DOF输入.
  • **广泛用于研究:**SpaceMouse远程操作用于robomimic,MimicGen和robosuite代码库,并且是DROID数据集 (500K+集) 的默认远程操作方法.如果你正在收集VLA细调 的数据,SpaceMouse数据最适合现有的预训练数据集.
  • **没有校准漂移:**与室内监测需要VR控制器不同,SpaceMouse是一个USB设备,在释放后返回中心.没有IMU漂移,没有跟踪损失,没有会议之间的校准.
  • **低操作员疲劳:**设备坐在桌子上.操作员的手自然靠在上.与保持VR控制器在手臂高度或物理移动领袖手臂相比,SpaceMouse远程操作对于长时间收集数据的会议来说显著更不疲.

** 交易:** 空间鼠标没有力量反 (操作员无法感觉机器人触摸什么) 并缺乏领导跟随者的位置直观性 (领导的手臂反映了机器人的姿势).对于需要细力控制 (插入,抛光) 或复杂双手协调的任务,领导跟随者的手臂产生更高质量的示范. 对于一般的选择和位置,对象重新排列和大多数桌面操作任务,

硬件设置

空间鼠标模型

Model Price DOF Buttons Connection Recommendation
SpaceMouse Compact $129 6 2 USB Best value. Use left button for gripper toggle, right for episode save.
SpaceMouse Pro $299 6 15 USB Extra buttons useful for multi-function teleop (speed presets, mode switching).
SpaceMouse Pro Wireless $399 6 15 USB/BT Wireless adds 2-5ms latency. USB mode recommended for data collection.
SpaceMouse Enterprise $529 6 31 USB Overkill for teleoperation. The extra buttons go unused.

我们的建议: 单臂远程操作的SpaceMouse Compact ($129) . 双手设置的两个SpaceMouse Compact ($258总) . 专用增添4个小时以上的舒适性,但价格不值大多数团队的两倍.RCSV在所有收藏站使用SpaceMouse Compact单元.

身体的设置

  • 放置SpaceMouse在桌面高度的稳定表面,直接在操作员面前
  • 机器人手臂可见的位置 (直接视线偏好于仅使用摄像头的视线)
  • 通过USB连接. 避免USB枢纽直接连接到工作站主板USB端口,以实现最低延迟
  • 检查设备在Linux上显示为T10:T11应显示一个或多个设备

3. ROS2 驱动器安装

T12库为所有3D连接设备提供了纯字thon接口.我们将其包裹在ROS2节点中,用于与机器人控制堆的集成.

步骤1:安装 pyspacemouse

# Install the library and its hidapi dependency
pip install pyspacemouse hidapi

# On Ubuntu, you also need the udev rules for non-root access
sudo tee /etc/udev/rules.d/99-spacemouse.rules <<'EOF'
SUBSYSTEM=="usb", ATTR{idVendor}=="256f", MODE="0666"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="256f", MODE="0666"
EOF
sudo udevadm control --reload-rules && sudo udevadm trigger

# Verify the device is detected
python3 -c "import pyspacemouse; print(pyspacemouse.list_devices())"

步骤 2:创建ROS2电操作节点

#!/usr/bin/env python3
"""spacemouse_teleop_node.py - ROS2 node for SpaceMouse teleoperation."""
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
import pyspacemouse
import numpy as np

class SpaceMouseTeleop(Node):
    def __init__(self):
        super().__init__('spacemouse_teleop')

        # Parameters (tune these per-robot)
        self.declare_parameter('linear_scale', 0.15)   # m/s per unit
        self.declare_parameter('angular_scale', 0.3)    # rad/s per unit
        self.declare_parameter('dead_zone', 0.08)       # ignore below this
        self.declare_parameter('publish_rate', 50.0)     # Hz

        self.linear_scale = self.get_parameter('linear_scale').value
        self.angular_scale = self.get_parameter('angular_scale').value
        self.dead_zone = self.get_parameter('dead_zone').value
        rate = self.get_parameter('publish_rate').value

        # Publisher
        self.pub = self.create_publisher(Twist, 'spacemouse/twist', 10)
        self.timer = self.create_timer(1.0 / rate, self.timer_callback)

        # Open device
        success = pyspacemouse.open()
        if not success:
            self.get_logger().error('Failed to open SpaceMouse device')
            raise RuntimeError('SpaceMouse not found')
        self.get_logger().info('SpaceMouse connected successfully')

    def apply_dead_zone(self, value):
        """Apply dead zone and normalize."""
        if abs(value) < self.dead_zone:
            return 0.0
        sign = 1.0 if value > 0 else -1.0
        return sign * (abs(value) - self.dead_zone) / (1.0 - self.dead_zone)

    def timer_callback(self):
        state = pyspacemouse.read()

        msg = Twist()
        # SpaceMouse axes -> robot EEF frame
        # NOTE: axis mapping depends on your robot's base frame convention.
        # These defaults work for OpenArm 1 and ViperX with Z-up, X-forward.
        msg.linear.x = self.apply_dead_zone(state.y) * self.linear_scale
        msg.linear.y = self.apply_dead_zone(state.x) * -self.linear_scale
        msg.linear.z = self.apply_dead_zone(state.z) * self.linear_scale
        msg.angular.x = self.apply_dead_zone(state.roll) * self.angular_scale
        msg.angular.y = self.apply_dead_zone(state.pitch) * self.angular_scale
        msg.angular.z = self.apply_dead_zone(state.yaw) * self.angular_scale

        self.pub.publish(msg)

def main():
    rclpy.init()
    node = SpaceMouseTeleop()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

步骤3:启动

# Terminal 1: Start the SpaceMouse node
ros2 run your_package spacemouse_teleop_node \
  --ros-args -p linear_scale:=0.15 -p angular_scale:=0.3 -p dead_zone:=0.08

# Terminal 2: Verify output
ros2 topic echo /spacemouse/twist

4.轴线绘图和配置

机器人使用不同的框架,它们之间的映射取决于机器人如何定义其基座位系统.

共同轴线地图

SpaceMouse Axis OpenArm 1 ViperX-300 / ALOHA Franka Research 3
X (push/pull) +Y (forward) +X (forward) +X (forward)
Y (left/right) -X (right) -Y (right) +Y (left)
Z (up/down) +Z (up) +Z (up) +Z (up)
Roll (tilt left/right) +Roll +Roll +Roll
Pitch (tilt fwd/back) +Pitch +Pitch -Pitch
Yaw (twist) +Yaw +Yaw +Yaw

** 如何确定您的地图:** 观察机器人时,按一下移动空间鼠标按.如果机器人朝着错误的方向移动,则否定该轴.如果它朝着错误的轴移动,则切换轴.这个过程需要5到10分钟,只需要每机器人配置一次.

5. 死亡区调整和敏感性

无需过死区,机器人会漂移当操作员的手靠在上.死区是对远程操作数据质量最重要的校准参数.

# spacemouse_config.yaml - Per-axis configuration
spacemouse_teleop:
  ros__parameters:
    # Dead zone: ignore input below this threshold (0.0-1.0)
    # Higher = less drift, but less responsive to small movements
    dead_zone_translation: 0.08   # Good default for most operators
    dead_zone_rotation: 0.12      # Rotation is more sensitive; higher DZ needed

    # Scaling: how fast the robot moves per unit of SpaceMouse input
    # Start conservative (0.1), increase as operator gains confidence
    linear_scale_x: 0.15    # m/s - forward/backward
    linear_scale_y: 0.15    # m/s - left/right
    linear_scale_z: 0.10    # m/s - up/down (slower to prevent table crashes)
    angular_scale_roll: 0.25   # rad/s
    angular_scale_pitch: 0.25  # rad/s
    angular_scale_yaw: 0.30    # rad/s - wrist yaw is the most-used rotation

    # Smoothing: exponential moving average (0.0 = no smoothing, 0.95 = very smooth)
    # Smoothing reduces jitter but adds latency. 0.3-0.5 is a good range.
    smoothing_factor: 0.4

调整程序:

  1. 设置死区为0.15 (保守). 放下. 如果机器人仍然漂移,增加死区. 如果不,减少0.02次,直到你找到最小值,防止漂移.
  2. 设置线性尺度为0.10.试着移动机器人10厘米.如果需要超过1秒,增加.如果机器人移动太快,减少.目标:0.5-1.0秒10厘米的移动.
  3. 设置角值为0.20.试着旋转终端效应器45度.与线性尺度相同的调整逻辑.
  4. 如果运动感觉,增加.如果运动感觉,减少.

6.与OpenArm1的集成

[OpenArm 1]T15) 使用加拿大CAN-bus接口的达迈奥动机.SpaceMouse扭转命令转换为终端效应器速度目标,而反动动力学 (IK) 解决器转换为联合速度命令.

# openarm_spacemouse_teleop.py - Tested configuration for OpenArm 1
# Requires: openarm_sdk, pyspacemouse, numpy

import numpy as np
from openarm_sdk import OpenArm
from openarm_sdk.ik import DampedLeastSquaresIK
import pyspacemouse

# OpenArm-specific settings (tested at RCSV San Francisco lab)
OPENARM_CONFIG = {
    'axis_map': {
        'x': ('y', 1.0),      # SpaceMouse Y -> OpenArm +X (forward)
        'y': ('x', -1.0),     # SpaceMouse X -> OpenArm -Y (right)
        'z': ('z', 1.0),      # SpaceMouse Z -> OpenArm +Z (up)
        'roll': ('roll', 1.0),
        'pitch': ('pitch', 1.0),
        'yaw': ('yaw', 1.0),
    },
    'linear_scale': 0.12,     # m/s - OpenArm is precise; keep speed moderate
    'angular_scale': 0.25,    # rad/s
    'dead_zone': 0.08,
    'ik_damping': 0.05,       # Damped least-squares regularization
    'control_rate': 50,       # Hz - matches OpenArm servo rate
}

arm = OpenArm(port='/dev/ttyUSB0')
ik = DampedLeastSquaresIK(arm.urdf_path, damping=OPENARM_CONFIG['ik_damping'])
pyspacemouse.open()

print("SpaceMouse teleop active. Left button = toggle gripper. Right button = e-stop.")

try:
    while True:
        state = pyspacemouse.read()

        # Apply dead zone and scaling
        twist = np.zeros(6)
        for i, axis in enumerate(['x', 'y', 'z', 'roll', 'pitch', 'yaw']):
            raw = getattr(state, OPENARM_CONFIG['axis_map'][axis][0])
            raw *= OPENARM_CONFIG['axis_map'][axis][1]
            if abs(raw) < OPENARM_CONFIG['dead_zone']:
                raw = 0.0
            scale = OPENARM_CONFIG['linear_scale'] if i < 3 else OPENARM_CONFIG['angular_scale']
            twist[i] = raw * scale

        # IK solve: twist -> joint velocities
        current_joints = arm.get_joint_positions()
        joint_velocities = ik.solve(current_joints, twist)
        arm.set_joint_velocities(joint_velocities)

        # Button handling
        if state.buttons[0]:  # Left button -> toggle gripper
            arm.toggle_gripper()
        if state.buttons[1]:  # Right button -> emergency stop
            arm.stop()
            break

except KeyboardInterrupt:
    arm.stop()

7. 集成到ViperX/ ALOHA 设置

对于ViperX-300和ALOHA系统,SpaceMouse通过Interbotix SDK或ALOHA远程操作堆集成.ALOHA代码库已经包括SpaceMouse支持作为领导跟踪者远程操作的替代品:

# Using the ALOHA codebase with SpaceMouse
cd ~/aloha
python3 scripts/teleop_spacemouse.py \
  --robot_config configs/viperx300s.yaml \
  --spacemouse_config configs/spacemouse_compact.yaml \
  --linear_scale 0.15 \
  --angular_scale 0.30 \
  --dead_zone 0.08

# For bimanual ALOHA with two SpaceMouse devices:
python3 scripts/teleop_spacemouse_bimanual.py \
  --left_device_id 0 \
  --right_device_id 1

通过两个SpaceMouse设备进行双手机远程操作时,将它们物理标签 (磁带,贴纸) 和分配一致的设备ID.设备ID分配可以在USB连接之间发生变化.

# List connected SpaceMouse devices with serial numbers
python3 -c "
import pyspacemouse
devices = pyspacemouse.list_devices()
for d in devices:
    print(f'Name: {d.name}, Serial: {d.serial}, ID: {d.id}')
"

8. 录制示范到HDF5

录制循环将同步的摄像头框架,联合状态,操作和语言注释捕获到HDF5文件中.

#!/usr/bin/env python3
"""record_episode.py - Record a teleoperation episode to HDF5."""
import h5py
import numpy as np
import time
from datetime import datetime

class EpisodeRecorder:
    def __init__(self, arm, cameras, save_dir='./episodes'):
        self.arm = arm
        self.cameras = cameras  # dict of {name: camera_object}
        self.save_dir = save_dir
        self.recording = False
        self.episode_data = None

    def start_episode(self, task_description=""):
        """Begin recording a new episode."""
        self.recording = True
        self.episode_data = {
            'joint_positions': [],
            'joint_velocities': [],
            'eef_pos': [],
            'eef_quat': [],
            'gripper_state': [],
            'actions': [],          # The twist commands sent to the robot
            'timestamps': [],
            'task_description': task_description,
        }
        # Add camera frame lists
        for cam_name in self.cameras:
            self.episode_data[f'camera_{cam_name}'] = []
        print(f"Recording started: '{task_description}'")

    def record_step(self, action_twist):
        """Record one timestep of data."""
        if not self.recording:
            return
        t = time.time()
        self.episode_data['joint_positions'].append(self.arm.get_joint_positions())
        self.episode_data['joint_velocities'].append(self.arm.get_joint_velocities())
        self.episode_data['eef_pos'].append(self.arm.get_eef_position())
        self.episode_data['eef_quat'].append(self.arm.get_eef_quaternion())
        self.episode_data['gripper_state'].append(self.arm.get_gripper_state())
        self.episode_data['actions'].append(action_twist)
        self.episode_data['timestamps'].append(t)
        for cam_name, cam in self.cameras.items():
            self.episode_data[f'camera_{cam_name}'].append(cam.get_frame())

    def save_episode(self):
        """Save the recorded episode to HDF5."""
        if not self.recording:
            return None
        self.recording = False
        n_steps = len(self.episode_data['timestamps'])
        if n_steps < 10:
            print(f"Episode too short ({n_steps} steps). Discarding.")
            return None

        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filepath = f"{self.save_dir}/episode_{timestamp}.hdf5"

        with h5py.File(filepath, 'w') as f:
            # Metadata
            f.attrs['task_description'] = self.episode_data['task_description']
            f.attrs['n_steps'] = n_steps
            f.attrs['timestamp'] = timestamp
            f.attrs['control_freq_hz'] = 50

            # State data
            f.create_dataset('joint_positions',
                data=np.array(self.episode_data['joint_positions'], dtype=np.float32))
            f.create_dataset('joint_velocities',
                data=np.array(self.episode_data['joint_velocities'], dtype=np.float32))
            f.create_dataset('eef_pos',
                data=np.array(self.episode_data['eef_pos'], dtype=np.float32))
            f.create_dataset('eef_quat',
                data=np.array(self.episode_data['eef_quat'], dtype=np.float32))
            f.create_dataset('gripper_state',
                data=np.array(self.episode_data['gripper_state'], dtype=np.float32))
            f.create_dataset('actions',
                data=np.array(self.episode_data['actions'], dtype=np.float32))
            f.create_dataset('timestamps',
                data=np.array(self.episode_data['timestamps'], dtype=np.float64))

            # Camera data (compressed)
            for cam_name in self.cameras:
                frames = np.array(self.episode_data[f'camera_{cam_name}'],
                                  dtype=np.uint8)
                f.create_dataset(f'camera_{cam_name}', data=frames,
                    compression='gzip', compression_opts=4)

        print(f"Saved episode: {filepath} ({n_steps} steps, "
              f"{n_steps/50:.1f}s)")
        return filepath

9 数据质量提示

通过高质量的SpaceMouse示范,产生了更顺利的政策,

  • 在录音之前,请在5分钟内加热.
  • ** 慢慢地滑动.** 的运动会产生噪音的动作标签.如果你需要快速地移动,先放下按 (零速度),然后再滑动. 目标15-30秒的集进行选择和放置任务.
  • ** 开始的条件*** 间隔,随机定制对象位置 (工作空间内),对象导向和机器人启动配置.
  • ** 始终重置.** 使用编写的重置,使机器人在剧集之间恢复到固定的起始姿势.不一致的重置会在最初状态分布中引入噪音.
  • 立即注释. 在录制之前或录制期间,而不是之后,写任务描述.
  • 会议时间: 限制收集会议时间为2小时,不间断. 2小时后,操作员疲劳可测量地降低示范质量 (根据RCSV内部基准,动作动率增加20-40%).

10.转换为RLDS/LeRobot格式

在录制HDF5集后,将其转换为RLDS或LeRobot格式 (VLA训练) T16):

# Convert HDF5 episodes to LeRobot format (for SmolVLA, ACT training)
# Requires: pip install lerobot

from lerobot.common.datasets.push_dataset_to_hub import hdf5_to_lerobot

hdf5_to_lerobot(
    raw_dir="./episodes/",           # Directory of HDF5 files
    repo_id="your-org/your-dataset", # Hugging Face Hub repo
    fps=50,
    video=True,                      # Encode camera frames as mp4
    push_to_hub=True,                # Upload to Hugging Face Hub
)

# Convert HDF5 to RLDS format (for OpenVLA fine-tuning)
# Requires: pip install tensorflow-datasets

python3 -m rlds_tools.hdf5_to_rlds \
  --input_dir ./episodes/ \
  --output_dir ./rlds_dataset/ \
  --dataset_name my_spacemouse_data \
  --action_key actions \
  --state_key joint_positions

11 常见问题和解决方案

空间鼠标漂移 (机器人在释放时移动)

原因: 死亡区太小,或者设备有轻微的机械偏差. ** 修正:** 增加死区到0.10-0.15.如果漂移持续下去,重新校准设备:切断,放置在平面表面,重新校准.设备自动校准其中心位置在电源上.

轴反转 (机器人移动相反方向)

原因:** 轴突出标志 SpaceMouse 框架和机器人框架之间的不匹配. ** 修正:** 否定您的配置中违反轴. 一次一次移动每个轴,验证方向.

在录音过程中断开USB

原因: 失联USB连接,USB枢纽电源管理,或操作系统暂停设备. ** 修复:**

# Disable USB autosuspend for 3Dconnexion devices
echo -1 | sudo tee /sys/bus/usb/devices/*/power/autosuspend_delay_ms

# Or permanently in /etc/udev/rules.d/99-spacemouse.rules:
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="256f", \
  TEST=="power/autosuspend_delay_ms", \
  ATTR{power/autosuspend_delay_ms}="-1"

录音中,有着高动作的

原因: 滑滑因子太低,或操作员缺乏经验. ** 修正:** 提高滑滑因子到0.5-0.6. 确保操作员在记录生产数据之前至少完成30分钟的练习.

两个SpaceMouse设备被交换

原因: USB设备编号顺序不是确定性的. ** 修正:** 用序列号来识别设备 (见第7节的代码). 创建一个基于序列号的一致符号链接的 udev 规则.

相关阅读

  • [如何收集机器人训练数据 (完整指南) ]T17)
  • [开始电话操作]
  • 电脑系统的设备设置
  • [VLA模型比较2026年]
  • 现在,我们需要一个新的技术.
  • ,我不想说,我不想说.
  • [RCSV数据服务]

在RCSV收集远程操作数据

太空鼠标机器已经在OpenArm,ViperX和DK1平台上配置和校准.训练有素的操作员,标准化的HDF5输出,2,500美元的飞行员.

查看数据服务 联系我们