스페이스마우스 텔레오퍼레이션 가이드: 로봇 학습의 설정, 캘리브레이션 및 데이터 수집
로봇 원격 조작을 위해 3D 연결 스페이스마우스를 사용하는 방법: ROS2 드라이버 설정, 축 지도, 죽은 영역 캘리브레이션, 그리고 모방 학습을 위해 HDF5 시범을 녹음. OpenArm 및 ViperX 호환성.
[← 가이드]
3Dconnexion SpaceMouse를 6DOF 로봇 텔레오퍼레이션에 사용하는 방법
왜 스페이스마우스가 원격 조작을 위해?
3Dconnexion SpaceMouse는 6DOF 입력 장치로 손 동작을 동시 번역 (X, Y, Z) 및 회전 (roll, pitch, yaw) 명령으로 변환합니다. 로봇 텔레오퍼레이션에 있어서, 그것은 특유의 장점을 제공합니다:
- 6-DOF 한 손: 단일 스페이스마우스는 로봇 최종 효과자의 모든 6 가지 자유도를 동시에 제어합니다. 모드 스위치, 버튼 조합이 없습니다. 이것은 대부분의 운영자에게 가장 직관적인 단일 장치 텔레오퍼레이션 인터페이스로 만듭니다.
- ** 입구 비용 낮:** SpaceMouse Compact ($129) 은 10-100배 더 많은 시스템과 같은 6DOF 입력량을 제공합니다. 리더-따라자 팔 ($2,000-5,000), VR 헤드셋 ($500++ 컨트롤러), 촉각 장치 ($3,000+) 와 비교하십시오.
- ** 연구에서 널리 사용된다:** SpaceMouse 텔레오퍼레이션은 robomimic, MimicGen, 그리고 robosuite 코드베이스에서 사용되며, DROID 데이터 세트 (500K+ 에피소드) 의 기본 텔레오퍼레이션 방법이다. [VLA 미세 조정] (
T14 ) 를 위해 데이터를 수집하는 경우, SpaceMouse 데이터는 기존 사전 훈련 데이터 세트와 가장 호환적입니다. - ** 기밀 유도:** 방 규모 추적을 필요로 하는 VR 컨트롤러와 달리, SpaceMouse는 방출되면 중앙으로 돌아가는 USB 장치입니다. IMU 유도, 추적 손실, 세션 사이 재 기밀이 없습니다.
- 운동자 피로감 낮: 기기는 책상 위에 앉아 있다.운동자의 손이 자연스럽게 버튼에 있다. VR 컨트롤러를 팔높이에 들고 있거나 리더 팔을 물리적으로 움직일 때, 스페이스마우스 텔레오퍼레이션은 긴 데이터 수집 세션에 있어서 피로감이 훨씬 적습니다.
Tradeoffs: SpaceMouse는 힘 피드백을 제공하지 않습니다 (운전자는 로봇이 무엇을 만지는 것을 느낄 수 없습니다) 그리고 리더-후보 팔의 위치 직관성이 부족합니다 ( 리더 팔이 로봇의 자세를 반영합니다). 미세한 힘 제어 (insertion, polishing) 또는 복잡한 양동 조정이 필요한 작업에 있어서 리더-후보 팔은 더 높은 품질의 시범을 제공합니다. 일반 픽앤플레이, 객체 재편, 그리고 대부분의 테이블톱 조작 작업에 있어서 SpaceMouse는 비용과 품질의 가장 좋은 타협입니다.
2· 하드웨어 설정
스페이스마우스 모델
| 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 시간 이상의 세션에 편안함을 추가하지만 대부분의 팀에 대한 가격의 2배가 되지 않습니다. RCSV는 모든 컬렉션 스테이션에서 SpaceMouse Compact 단위를 사용합니다.
신체적 설정
- 스페이스마우스를 테이블 높이에서 안정적인 표면에 배치하고, 바로 운영자의 앞에
- 로봇 팔을 운영자에게 가시하게 배치 (카메라만 보는 것보다 직선 시야를 선호한다)
- USB를 통해 연결하십시오. USB 허브를 피하십시오.
- 리눅스에서 장치가
T10 로 표시되는 것을 확인합니다. T11 은 하나 이상의 장치가 표시되어야 합니다.
3. ROS2 드라이버 설치
단계 1: 피스파크마우스 설치
# 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. 축 지도 및 구성
스페이스마우스는 자신의 좌표 프레임에서 6개의 축 값을 내보내는데, 로봇은 다른 프레임을 사용합니다. 그 사이의 지도는 로봇이 기본 좌표 시스템을 어떻게 정의하느냐에 달려 있습니다.
공통축 지도
| 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
조정 절차:
- 죽은 구역을 0.15 (보유) 로 설정하세요. 버튼을 풀어주세요. 로봇이 여전히 움직이지 않으면, 죽은 구역을 늘려주세요. 그렇지 않으면, 움직이지 않는 최소 값을 찾을 때까지 0.02 단계로 줄여주세요.
- 선형 스케일을 0.10로 설정하세요. 로봇을 10cm로 움직여보도록 노력하세요. 1초 이상 걸린다면, 증가하세요. 로봇이 제어할 수 없을 정도로 빠르게 움직인다면 감소하세요. 목표: 10cm의 움직임에 0.5-1.0초.
- 각도 스케일을 0.20로 설정하세요. 최종 효과자를 45도로 돌려보도록 시도하세요. 선형 스케일과 같은 조율 논리입니다.
- 가벼운 움직임이 0.4로 설정해 보세요.
6. OpenArm 1에 통합
[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 텔레오퍼레이션 스택을 통해 통합됩니다. 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· 데이터 품질 팁
고품질의 스페이스마우스 시범은 보다 원활한 정책을 만들어 더 잘 일반화합니다.
- 녹음하기 전에 5분 동안 따뜻해 주세요. 세션의 첫 몇 번의 시연은 항상 후기보다 더 나쁘습니다.
- ** 느리고 부드럽게 움직여.** 어색한 움직임은 잡음적인 행동 표지를 만듭니다. 빠른 움직임을 해야 한다면 먼저 버튼을 풀 (허속) 하고, 다시 부드럽게 움직입니다. 선택과 장소 작업에 15-30초의 에피소드를 목표로 합니다.
- **** 에피소드 사이에는 객체 위치 ( 작업 공간 내에서), 객체 지향 및 로봇 시작 구성을 무작위로 조정한다. 이 변이는 일반화에서 에피소드 수보다 더 중요하다.
- 일반적으로 재설정. 로봇을 에피소드 사이 고정된 시작 자세로 되돌리는 스크립트 재설정 사용. 일반적 재설정으로 인해 초기 상태 분포에서 잡음이 발생한다.
- ** 즉시 해설.** 기록 전에 또는 기록 중에 작업 설명을 작성하고, 그 후에 작성하지 마십시오. 포스트하크 해설은 오류가 발생하고 수집 작업 흐름을 느리게 합니다.
- ** 세션 길이는:** 회수 세션 을 2 시간 까지 중단 없이 제한 합니다. 2 시간 후, 운영자 피로 는 시범 품질을 측정 할 수 있게 저하 합니다 (RCSV 내부 기준에 따라 행동
은 20%~40% 증가 합니다).
10. RLDS/레로봇 형식으로 변환
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로 증가하십시오.
축 반전 (로봇은 반대 방향으로 움직인다)
** 원인:** 스페이스마우스 프레임과 로봇 프레임 사이에 축이 일치하지 않는 신호를 표시합니다. ** 수정:** 설정에서 침해하는 축을 부정합니다. 각 축을 한 번에 이동하고 방향을 확인합니다.
녹음 중에 USB 연결을 끊는다
** 원인:** USB 연결, USB 허브 전력 관리, 또는 OS 기기를 중지. ** 수정:**
# 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로 증가시켜야 합니다.
스페이스마우스 2개의 기기가 교환되고 있습니다.
**유스버 장치의 명목 순서는 결정적이지 않습니다. 정치: 장치 식별을 위해 일련 번호를 사용하십시오. (부 7의 코드를 참조하십시오.) 일련 번호에 따라 일관된 증상 링크를 할당하는 udev 규칙을 생성하십시오.
관련 독서
- [로봇 훈련 데이터를 수집하는 방법 (완전 안내서) ]
- [전체 수술을 시작]
- [중동전화기계 설치]
- [VLA 모델 비교 2026]
- [2026년 물리 인공지능]
- [오픈아름 1]
- [RCSV 데이터 서비스]
RCSV에서 텔레오퍼레이션 데이터를 수집
스페이스마우스 기구들은 이미 오픈아름, 바이퍼X, DK1 플랫폼에서 구성 및 캘리브레이션되어 있습니다. 훈련된 운영자, 표준화된 HDF5 출력, $2,500 파일럿.







