Guía de Teleoperación del ratón espacial: Configuración, Calibración y Recopilación de Datos para el aprendizaje de robots
Cómo utilizar un SpaceMouse de conexión 3D para la teleoperación del robot: configuración del controlador ROS2, mapeo de eje, calibración de zonas muertas y grabación de demostraciones HDF5 para el aprendizaje de imitación.
[← Guías]
Cómo utilizar un SpaceMouse de conexión 3D para la teleoperación de robots de 6 DOF
¿Por qué SpaceMouse para la teleoperación?
El SpaceMouse 3Dconnexion es un dispositivo de entrada de 6 DOF que traduce el movimiento de la mano en comandos de traducción simultánea (X, Y, Z) y rotación (roll, pitch, yaw).
- 6-DOF en una mano: Un solo SpaceMouse controla los seis grados de libertad de un factor final del robot simultáneamente.
- ** Bajo costo de entrada:** El SpaceMouse Compact ($129) proporciona la misma entrada de 6 DOF que los sistemas que cuestan 10-100 veces más.
- Usa ampliamente en la investigación: La teleoperación de SpaceMouse se utiliza en las bases de código robomimic, MimicGen y robosuite, y es el método de teleoperación predeterminado para el conjunto de datos DROID (500K+ episodios). Si está recopilando datos para VLA fine-tuning, los datos de SpaceMouse son los más compatibles con los conjuntos de datos pre-entrenamiento existentes.
- No se deriva de calibración: A diferencia de los controladores VR que requieren rastreo a escala de habitación, un SpaceMouse es un dispositivo USB que vuelve al centro cuando se libera.
- ** Bajo cansancio del operador:** El dispositivo se sienta en un escritorio. La mano del operador descansa naturalmente sobre el botón. En comparación con mantener un controlador VR a la altura del brazo o mover físicamente un brazo líder, la teleoperatoria de SpaceMouse es significativamente menos fatigante para sesiones largas de recopilación de datos.
Tradeoffs: SpaceMouse no proporciona retroalimentación de la fuerza (el operador no puede sentir lo que el robot toca) y carece de la intuitividad posicional de los brazos líder-seguidor (donde el brazo líder refleja la postura del robot). Para la selección general y el lugar, la reorganización de objetos y la mayoría de las tareas de manipulación de mesa, SpaceMouse es el mejor compromiso de calidad de costo.
2. Configuración de hardware
Modelos de ratones espaciales
| 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. |
Nuestra recomendación: SpaceMouse Compact ($129) para una sola operación por tele. Dos SpaceMouse Compacts ($258 en total) para configuraciones bimanual. El Pro añade comodidad para sesiones de más de 4 horas, pero no vale el doble del precio para la mayoría de los equipos. RCSV utiliza unidades SpaceMouse Compact en todas las estaciones de recogida.
El ordenamiento físico
- Coloque el SpaceMouse en una superficie estable a la altura del escritorio, directamente delante del operador.
- Posicionar el brazo robótico visible para el operador (línea de visión directa preferida sobre la vista de la cámara única)
- Conectar a través de USB. Evite los hubs USB
conectar directamente a los puertos USB de la placa base de la estación de trabajo para la menor latencia - Verifique que el dispositivo aparece como
T10 en Linux: T11 debe mostrar uno o más dispositivos
3. Instalación del conductor ROS2
La biblioteca
Paso 1: Instalar el 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())"
Paso 2: Crea el nodo de teleoperación 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()
Paso 3: Lanzamiento
# 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. Mapeo y configuración de los ejes
El SpaceMouse emite seis valores de eje en su propio marco de coordenadas. Su robot utiliza un marco diferente. El mapeo entre ellos depende de cómo su robot define su sistema de coordenadas base.
Mapas de Eje común
| 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 |
** Cómo determinar su mapeo:** Mueve el botón del SpaceMouse en cada dirección uno a la vez mientras observa al robot. Si el robot se mueve en la dirección equivocada, niega ese eje. Si se mueve en el eje equivocado, cambie los ejes. El proceso dura 5-10 minutos y solo se necesita hacer una vez por configuración del robot.
5. Arreglo y sensibilidad en zonas muertas
El SpaceMouse es muy sensible. Sin filtración de zona muerta, el robot se desplazará cuando la mano del operador descansa sobre el botón.
# 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
Procedimiento de ajuste:
- Si el robot sigue desviándose, aumenta la zona muerta. Si no, disminuye 0,02 incrementos hasta que encuentre el valor mínimo que impide la deriva.
- Si el robot se mueve demasiado rápido para controlarlo, disminuye. Objetivo: 0,5-1,0 segundos para un movimiento de 10 cm.
- Establezca la escala angular a 0,20. Trate de girar el factor final 45 grados. La misma lógica de sintonización que la escala lineal.
- Si los movimientos se sienten tirados, aumenten.
6. Integración con OpenArm 1
El [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. Integración con ViperX / ALOHA Configuración
Para los sistemas ViperX-300 y ALOHA, el SpaceMouse se integra a través del SDK Interbotix o la pila de teleoperaciones ALOHA. La base de código ALOHA ya incluye el soporte de SpaceMouse como alternativa a la teleoperación líder-seguidor:
# 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
Cuando se utilicen dos dispositivos de SpaceMouse para la teleoperación bimanual, etiquete los dispositivos físicamente (canta, pegatinas) y asigne ID de dispositivo consistentes.
# 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. Grabación de las demostraciones a HDF5
El bucle de grabación captura marcos de cámara sincronizados, estados conjuntos, acciones y anotaciones de lenguaje en archivos 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. Consejos de calidad de los datos
Las demostraciones de SpaceMouse de alta calidad producen políticas más fluidas que generalizan mejor.
- Las primeras demostraciones de una sesión son siempre peores que las posteriores.
- Moverse lentamente y sin problemas. Los movimientos sin sentido crean etiquetas de acción ruidosas. Si necesita hacer un movimiento rápido, solte el botón primero (velocidad cero), luego vuelva a activarse sin problemas.
- Varias condiciones iniciales. Entre los episodios, aleatoriza la posición del objeto (dentro del espacio de trabajo), la orientación del objeto y la configuración de inicio del robot.
- Reset consistente. Utilice un reset scripted que devuelva al robot a una posición de inicio fija entre los episodios.
- Anotar inmediatamente. Escriba la descripción de la tarea antes o durante la grabación, no después. La anotación post hoc es propensa a errores y ralentiza el flujo de trabajo de la recopilación.
- Duración de la sesión: Limitar las sesiones de recogida a 2 horas sin interrupción. Después de 2 horas, la fatiga del operador degrada de manera medible la calidad de la demostración (la movilidad de acción aumenta un 20-40% en función de los puntos de referencia internos del RCSV).
10. Convertir a formato RLDS / LeRobot
Después de grabar los episodios de HDF5, conviértelos en formato RLDS o LeRobot para entrenamiento 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. Problemas y soluciones comunes
Drift de SpaceMouse (el robot se mueve cuando se libera el botón)
Causa: Zona muerta demasiado pequeña, o el dispositivo tiene un ligero sesgo mecánico. Fix: Aumente la zona muerta a 0,10-0,15. Si la deriva persiste, recalibra el dispositivo: desconecta, coloca en una superficie plana, vuelve a conectar. El dispositivo calibra automáticamente su posición central en la potencia.
Inversión del eje (robot se mueve en dirección opuesta)
** Causa:** Indica la falta de coincidencia entre el marco de SpaceMouse y el marco del robot. Fix: Negar el eje ofensor en su configuración. Mueve cada eje uno a la vez y verifique la dirección.
Disconexión USB durante la grabación
Causa: Conexión USB suelta, gestión de energía del centro USB, o el sistema operativo que suspende el dispositivo. Fix:
# 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"
Un idiota de acción en grabaciones
Causa: Factor de suavización demasiado bajo o inexperiencia del operador. Fix: Aumenta el factor de suavización a 0,5-0,6.
Dos dispositivos SpaceMouse que se intercambian
Causa: El orden de enumeración del dispositivo USB no es determinista. Fix: Utilice números de serie para la identificación del dispositivo (ver código en la sección 7).
Lectura relacionada
- [Cómo recopilar datos de entrenamiento de robots (Guía completa) ]
- [Empezando con la teleoperatoria]
- [Construcción de hardware de teleoperatorio bimanual]
- [Comparación de modelos VLA 2026]
- [IA física en 2026]
- [OpenArm 1]
- [Servicios de datos del RCSV]
Recoger datos de teleoperación en el RCSV
Las plataformas de SpaceMouse ya están configuradas y calibradas en OpenArm, ViperX y DK1. Operadores capacitados, salida HDF5 estandarizada, piloto de $2,500.
[Explorar los Servicios de Datos]







