空间鼠标远程操作指南:机器人学习的设置,校准和数据收集
如何使用3D连接SpaceMouse用于机器人远程操作:ROS2驱动程序设置,轴线映射,死区校准和录制HDF5示范用于模仿学习.OpenArm和ViperX兼容.
如何使用3D连接SpaceMouse用于6DOF机器人远程操作
为什么太空鼠标进行远程操作?
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 驱动器安装
步骤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. 死亡区调整和敏感性
无需过
# 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
调整程序:
- 设置死区为0.15 (保守). 放下
. 如果机器人仍然漂移,增加死区. 如果不,减少0.02次,直到你找到最小值,防止漂移. - 设置线性尺度为0.10.试着移动机器人10厘米.如果需要超过1秒,增加.如果机器人移动太快,减少.目标:0.5-1.0秒10厘米的移动.
- 设置角值为0.20.试着旋转终端效应器45度.与线性尺度相同的调整逻辑.
- 如果运动感觉
,增加.如果运动感觉 ,减少.
6.与OpenArm1的集成
[OpenArm 1]
# 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远程操作堆
# 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训练)
# 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美元的飞行员.







