Guidesに戻る

スペースマウスのテレオペレーションガイド: ロボット学習のための設定,校正,データ収集

ロボット遠隔操作のために3Dコネクションスペースマウスを使用する方法:ROS2ドライバー設定,軸マッピング,死区校正,模倣学習のためのHDF5デモの記録.OpenArmとViperX互換性.

[←ガイド]

3Dconnection SpaceMouse を 6DOF ロボット 遠隔操作 に 用いる方法 ドライバー インストール,軸マッピング,死区 カリブレーション,および HDF5 形式 で 模倣学習 の 記録 デモンストレーション.OpenArm 1 と ViperX-300 で テスト

なぜ スペースマウスが 遠隔操作を?

3Dconnexion SpaceMouseは,手動を同時に翻訳 (X,Y,Z) と回転 (ロール,ピッチ,ヤウ) コマンドに変換する6DOF入力装置である.ロボットの遠隔操作には,ユニークな利点の組み合わせを提供しています.

  • **6-DOF1手:**1つのSpaceMouseがロボットエンドエフェクターの自由度6度すべてを同時に制御する.モードスイッチング,ボタンの組み合わせなし.これはほとんどのオペレーターにとって最も直感的な単デバイスの遠隔操作インターフェースです.
  • 入場コスト低: SpaceMouse Compact ($129) は,10100倍以上のコストのシステムと同じ6DOF入力を提供します.リーダーフォロワー腕 ($2,0005,000),VRヘッドセット ($500++コントローラー),ハプティックデバイス ($3,000+) と比較してください.
  • 研究で広く使用されている: スペースマウスのテレオペレーションは,robomimic,MimicGen,およびrobosuiteコードベースで使用され,DROIDデータセット (500K+エピソード) のデフォルトテレオペレーション方法である. [VLAfine-tuning] (T14) のためにデータを収集している場合は,SpaceMouse データが既存の訓練前のデータセットと最も互換性がある.
  • キャリブレーションドリフがない: VRコントローラとは異なり,スペースマウスは,リリースされたときに中心に戻るUSBデバイスです. IMUドリフ,追跡損失,セッション間の再キャリブレーションはありません.
  • 操作者の疲労量が低い: デバイスは机の上に座っている.操作者の手は自然にノブの上に座っている. VR コントローラを腕の高さで握ったり,リーダー腕を物理的に動かしたりするよりも,SpaceMouseの遠隔操作は長時間データ収集セッションでかなり疲労が少ない.

Tradeoffs: 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) 双手セットアップのための2つのSpaceMouse Compact ($258合計) プロは4時間以上のセッションに快適性を追加しますが,ほとんどのチームでは価格の2倍ではありません.RCSVはすべてのコレクションステーションでSpaceMouse Compactユニットを使用します.

身体 的 な 構成

  • スペースマウスをテーブルの高さで,操作者の直前に安定した表面に配置する
  • 操作者に見えるロボット腕の位置 (カメラのみの視線よりも直視線が好まれる)
  • USB を通して接続する.USB ハブを避けます. 低迷度のために,直接ワークステーション マザーボードのUSBポートに接続する
  • デバイスが Linux で T10 として表示されていることを確認します.

3. ROS2ドライバーのインストール

T12図書館は3D接続デバイスに純粋なPythonインターフェースを提供します. ロボット制御スタックと統合するために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.5-1.0秒.
  3. 角質を0.20 に設定します. 末効果を45度回転してみてください. 線形スケールと同じ調節論理です.
  4. 滑らかな動きを 0.4 に設定します 動きが動かないと増やし 遅い場合は減らし

6. OpenArm1への統合

[OpenArm 1]T15) は,CANバスインターフェースを持つダミアオアクチュエーターを使用している.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

双手間遠隔操作のために2つの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ハブの電源管理または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つのデバイスが交換される

原因: USBデバイスの表記順は決定的ではありません. 修正: デバイスの識別のためにシリアル番号を使用します (セクション7のコードを参照してください). シリアル番号に基づいて一貫したシンボルリンクを割り当てる udev ルールを作成します.

関連 読書

  • [ロボット訓練データを収集する方法 (完全なガイド) ]
  • [テレオペレーションを開始する]
  • [双手電動操作ハードウェアの設定]
  • [VLAモデル比較2026]
  • [2026年の物理AI]
  • T22
  • [RCSVデータサービス]

RCSVでテレオペレーションデータを収集する

スペースマウスのリグは既にOpenArm,ViperX,DK1プラットフォームで構成・校準化されています. 訓練されたオペレーター,標準化されたHDF5出力,2,500ドルパイロット.

[データサービスを探求] T24] [私たちと連絡してください] T25]