Metadata-Version: 2.4
Name: lerobot_teleoperator_spacemouse
Version: 0.1.6
Summary: SpaceMouse teleoperation plugin and Cartesian control adapters for LeRobot.
Author: Shunyu Yao
License-Expression: Apache-2.0
Project-URL: homepage, https://github.com/Jas000n/lerobot-teleoperator-spacemouse
Project-URL: source, https://github.com/Jas000n/lerobot-teleoperator-spacemouse
Project-URL: issues, https://github.com/Jas000n/lerobot-teleoperator-spacemouse/issues
Keywords: lerobot,spacemouse,3dconnexion,teleoperation,robotics,soarm,so101,dexterous-manipulation,precise-teleoperation,robot-arm,inverse-kinematics
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: lerobot[feetech,kinematics]<0.7.0,>=0.5.0
Requires-Dist: numpy>=2.0.0
Requires-Dist: pyspacemouse<3.0.0,>=1.1.5
Provides-Extra: dev
Requires-Dist: build<2.0.0,>=1.2.0; extra == "dev"
Requires-Dist: pytest<9.0.0,>=8.0.0; extra == "dev"
Requires-Dist: ruff>=0.6.0; extra == "dev"
Requires-Dist: twine<7.0.0,>=6.0.0; extra == "dev"
Dynamic: license-file

# lerobot-teleoperator-spacemouse

[![PyPI](https://img.shields.io/pypi/v/lerobot-teleoperator-spacemouse)](https://pypi.org/project/lerobot-teleoperator-spacemouse/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Python](https://img.shields.io/pypi/pyversions/lerobot-teleoperator-spacemouse)](https://pypi.org/project/lerobot-teleoperator-spacemouse/)

A [LeRobot](https://github.com/huggingface/lerobot) plugin that turns a [3Dconnexion SpaceMouse](https://3dconnexion.com/spacemouse) into a 6-DoF robot teleoperator.

Push the puck to move the end-effector in X/Y/Z. Tilt/twist to rotate. Press the side buttons to open or close the gripper. Works out of the box with SO-101 / SO-ARM follower robots via built-in inverse kinematics, and supports any custom URDF through a profile API.

SpaceMouse teleoperation is particularly useful for fine-grained manipulation: its compact form factor and incremental Cartesian input allow precise end-effector adjustments without the workspace and hardware footprint of a conventional leader arm.

---

## Features

- **6-DoF teleoperation** — translation and rotation deltas from SpaceMouse physical axes
- **IK mode** — converts end-effector targets to joint commands using LeRobot's `RobotKinematics` and a bundled SO-101 URDF (no calibration file needed)
- **Direct EEF mode** — passes Cartesian targets straight to robots that already accept `ee.*` action keys
- **Native LeRobot recording** — stores processed robot actions with observations and camera frames via `lerobot-record`
- **Gripper control** — left/right buttons open and close the gripper at a configurable speed
- **Axis remapping & sign flip** — remap any physical SpaceMouse axis to any robot axis; flip signs per axis
- **Deadzone & timeout** — configurable deadzone and input-stale timeout prevent drift when the puck is released
- **Workspace clamping** — optional per-axis position limits and max-step rate limiter
- **Custom robot profiles** — register your own URDF + frame + motor list at runtime

---

## Installation

```bash
pip install lerobot-teleoperator-spacemouse
```

LeRobot automatically discovers the installed plugin, so `--teleop.type=spacemouse` is available to
both `lerobot-teleoperate` and `lerobot-record` without a separate recording extra.

Python 3.12 or newer is required because the supported LeRobot releases (`>=0.5.0,<0.7.0`) require it too.
The plugin does not otherwise depend on Python-3.12-specific behavior, so its Python requirement should
stay aligned with LeRobot rather than be relaxed independently.

This release is tested with both LeRobot 0.5 and 0.6. Future LeRobot minor releases are added to the
supported range after their API compatibility has been verified.

SO-101 / SO-ARM IK support is included by default. Other URDF-described robots can be used by providing a custom kinematics profile — see [Extending to Other Robots](#extending-to-other-robots).

---

## Quick Start

### SO-101 follower over USB

```bash
lerobot-teleoperate \
  --robot.type=so101_follower \
  --robot.port=/dev/ttyACM0 \
  --robot.id=my_robot \
  --teleop.type=spacemouse \
  --teleop.adapter.mode=ik \
  --fps=60
```

### Conservative first run (smaller steps, display enabled)

```bash
lerobot-teleoperate \
  --robot.type=so101_follower \
  --robot.port=/dev/ttyACM0 \
  --robot.id=my_robot \
  --teleop.type=spacemouse \
  --teleop.adapter.mode=ik \
  --teleop.deadzone=0.1 \
  --teleop.input_timeout_s=0.08 \
  --teleop.adapter.translation_step_m=0.001 \
  --teleop.adapter.rotation_step_rad=0.005 \
  --display_data=true
```

### Record a LeRobot dataset

The recording integration converts SpaceMouse deltas to the robot's native action format before each
frame is written. In IK mode, an SO-101 dataset therefore contains the follower's joint position
actions rather than the raw SpaceMouse axes.

```bash
lerobot-record \
  --robot.type=so101_follower \
  --robot.port=/dev/ttyACM0 \
  --robot.id=my_robot \
  --robot.cameras='{"front": {"type": "opencv", "index_or_path": 0, "width": 640, "height": 480, "fps": 30}}' \
  --teleop.type=spacemouse \
  --teleop.adapter.mode=ik \
  --dataset.repo_id=my_user/so101_spacemouse \
  --dataset.single_task="Pick up the cube" \
  --dataset.num_episodes=10 \
  --dataset.episode_time_s=30 \
  --dataset.reset_time_s=15 \
  --dataset.fps=30 \
  --dataset.push_to_hub=false \
  --display_data=true
```

Remove `--robot.cameras` for a state-only dataset. Set `--dataset.push_to_hub=true` after authenticating
with Hugging Face if the completed dataset should be uploaded.

### Test your SpaceMouse (no robot needed)

```bash
lerobot-teleoperator-spacemouse-test
```

This prints raw SpaceMouse state for 30 seconds so you can verify axes and buttons before connecting a robot.

---

## Configuration Reference

All options are set via `--teleop.<option>` or `--teleop.adapter.<option>` on the command line.

### Teleoperator options (`--teleop.*`)

| Option | Default | Description |
|---|---|---|
| `device` | `None` | Device name or HID device path. Auto-detected when `None`. |
| `deadzone` | `0.08` | Ignore inputs below this fraction of full deflection. |
| `rescale_after_deadzone` | `True` | Rescale remaining range to `[0, 1]` after deadzone. |
| `max_axis_value` | `1.0` | Clamp raw axis values to `[-max, max]`. |
| `input_timeout_s` | `0.08` | Zero-out input if no new state arrives within this window (seconds). |
| `read_drain_count` | `32` | Max HID reads per `get_action()` call to drain the queue. |
| `x_axis` | `"y"` | SpaceMouse attribute name mapped to robot `target_x`. |
| `y_axis` | `"x"` | SpaceMouse attribute name mapped to robot `target_y`. |
| `z_axis` | `"z"` | SpaceMouse attribute name mapped to robot `target_z`. |
| `wx_axis` | `"roll"` | SpaceMouse attribute name mapped to robot `target_wx`. |
| `wy_axis` | `"pitch"` | SpaceMouse attribute name mapped to robot `target_wy`. |
| `wz_axis` | `"yaw"` | SpaceMouse attribute name mapped to robot `target_wz`. |
| `x_sign` | `1.0` | Sign multiplier for `target_x`. Use `-1.0` to flip. |
| `y_sign` | `-1.0` | Sign multiplier for `target_y`. |
| `z_sign` | `1.0` | Sign multiplier for `target_z`. |
| `wx_sign` | `1.0` | Sign multiplier for `target_wx`. |
| `wy_sign` | `1.0` | Sign multiplier for `target_wy`. |
| `wz_sign` | `1.0` | Sign multiplier for `target_wz`. |
| `use_gripper` | `True` | Enable gripper button control. |
| `gripper_open_button` | `1` | Button index that opens the gripper. |
| `gripper_close_button` | `0` | Button index that closes the gripper. |
| `require_enable_button` | `False` | Require a dedicated button to enable motion. |
| `enable_button` | `None` | Button index used when `require_enable_button=True`. |
| `idle_enabled` | `False` | Whether `enabled=True` is sent when the device is idle. |

### Adapter options (`--teleop.adapter.*`)

| Option | Default | Description |
|---|---|---|
| `mode` | `"auto"` | `"ik"`, `"eef"`, or `"auto"` (uses `"eef"` if robot exposes `ee.*` keys). |
| `robot_profile` | `"so101_follower"` | Built-in kinematics profile. Use `"custom"` to supply your own URDF. |
| `translation_step_m` | `0.001` | End-effector translation per frame at full SpaceMouse deflection (meters). |
| `rotation_step_rad` | `0.005` | End-effector rotation per frame at full deflection (radians). |
| `workspace_min` | `None` | `[x, y, z]` lower workspace bound (meters). |
| `workspace_max` | `None` | `[x, y, z]` upper workspace bound (meters). |
| `max_ee_step_m` | `0.02` | Maximum allowed end-effector step per frame (rate limiter). |
| `position_weight` | `1.0` | IK position tracking weight. |
| `orientation_weight` | `0.01` | IK orientation tracking weight. |
| `gripper_speed_factor` | `2.0` | Gripper velocity → position integration scale. |
| `gripper_min` | `0.0` | Gripper position lower bound (normalized joint units). |
| `gripper_max` | `100.0` | Gripper position upper bound (normalized joint units). |

---

## Linux HID Setup

The SpaceMouse uses the hidraw subsystem. On Linux you need the shared library and a udev rule.

**conda environment:**

```bash
conda install -c conda-forge libhidapi
```

**Debian / Ubuntu system Python:**

```bash
sudo apt-get install libhidapi-hidraw0 libhidapi-dev
```

**udev rule (allows non-root access):**

```bash
sudo tee /etc/udev/rules.d/99-spacemouse-hidraw.rules >/dev/null <<'EOF'
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="256f", MODE="0660", GROUP="plugdev", TAG+="uaccess"
EOF
sudo usermod -aG plugdev "$USER"
sudo udevadm control --reload-rules
sudo udevadm trigger
```

Unplug and replug the SpaceMouse. Log out and back in after the group change.

---

## Extending to Other Robots

### One-off custom URDF

```bash
lerobot-teleoperate \
  --teleop.type=spacemouse \
  --teleop.adapter.mode=ik \
  --teleop.adapter.robot_profile=custom \
  --teleop.adapter.urdf_path=/path/to/robot.urdf \
  --teleop.adapter.target_frame_name=gripper_tip \
  --teleop.adapter.motor_names='["joint1","joint2","joint3","joint4","joint5","gripper"]' \
  --teleop.adapter.gripper_name=gripper
```

### Reusable profile (Python)

Register a profile from your own package and it becomes available as a named `robot_profile`:

```python
from lerobot_teleoperator_spacemouse.profiles import KinematicsProfile, register_kinematics_profile

register_kinematics_profile(
    "my_arm",
    KinematicsProfile(
        urdf_resource=None,           # set urdf_path on the adapter instead
        target_frame_name="tip_link",
        motor_names=("j1", "j2", "j3", "j4", "j5", "gripper"),
        gripper_name="gripper",
    ),
)
```

Then use `--teleop.adapter.robot_profile=my_arm`.

---

## Troubleshooting

**Arm keeps moving after releasing the SpaceMouse** — Increase `--teleop.deadzone` (e.g. `0.15`) or lower `--teleop.input_timeout_s` (e.g. `0.05`).

**Motion is too fast / too slow** — Tune `--teleop.adapter.translation_step_m` and `--teleop.adapter.rotation_step_rad`.

**Axis direction is wrong** — Flip the sign: `--teleop.x_sign=-1`, `--teleop.y_sign=-1`, etc.

**Device not found / permission denied** — Run `lerobot-teleoperator-spacemouse-test` first. Follow the [Linux HID Setup](#linux-hid-setup) section if it fails.

---

## Development

```bash
git clone https://github.com/Jas000n/lerobot-teleoperator-spacemouse.git
cd lerobot-teleoperator-spacemouse
pip install -e ".[dev]"
pytest
```


---

## License

Apache 2.0.
