mirror of
https://github.com/huggingface/lerobot.git
synced 2026-05-21 11:39:50 +00:00
176 lines
6.5 KiB
Python
176 lines
6.5 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2025 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
Simple SO100/SO101 leader-follower teleoperation with spacebar intervention toggle.
|
|
|
|
Modes:
|
|
- Default (not intervening): follower holds its current position.
|
|
The leader arm has torque ENABLED and mirrors the follower so there is no
|
|
large position jump when intervention starts.
|
|
- Intervention (SPACE pressed): leader torque DISABLED, human moves the leader
|
|
freely, and the follower mirrors the leader joint-by-joint.
|
|
|
|
Usage:
|
|
uv run python examples/so100_teleop/teleop.py
|
|
|
|
Controls:
|
|
SPACE — toggle intervention on/off
|
|
Ctrl+C — exit
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
from threading import Event, Thread
|
|
|
|
from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig
|
|
from lerobot.teleoperators.so_leader import SO101Leader
|
|
from lerobot.teleoperators.so_leader.config_so_leader import SOLeaderTeleopConfig
|
|
from lerobot.utils.robot_utils import precise_sleep
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── pynput keyboard listener ─────────────────────────────────────────────────
|
|
PYNPUT_AVAILABLE = True
|
|
try:
|
|
if "DISPLAY" not in os.environ and "linux" in sys.platform:
|
|
raise ImportError("No DISPLAY set, pynput skipped.")
|
|
from pynput import keyboard as pynput_keyboard
|
|
except Exception:
|
|
pynput_keyboard = None
|
|
PYNPUT_AVAILABLE = False
|
|
|
|
# ── Configure ports ──────────────────────────────────────────────────────────
|
|
FOLLOWER_PORT = "/dev/ttyUSB0" # ← change to your follower port
|
|
LEADER_PORT = "/dev/ttyUSB1" # ← change to your leader port
|
|
FPS = 30
|
|
|
|
|
|
def hold_position(robot) -> dict:
|
|
"""Read current joint positions and write them back as the goal.
|
|
|
|
This prevents the motors from snapping to a stale Goal_Position register
|
|
value (which can happen when torque is re-enabled after calibration).
|
|
Returns the current position dict for reuse.
|
|
"""
|
|
current = robot.bus.sync_read("Present_Position")
|
|
robot.bus.sync_write("Goal_Position", current)
|
|
return {f"{motor}.pos": val for motor, val in current.items()}
|
|
|
|
|
|
# ── Connect ───────────────────────────────────────────────────────────────────
|
|
follower_config = SO101FollowerConfig(
|
|
port=FOLLOWER_PORT,
|
|
id="follower_arm",
|
|
use_degrees=True,
|
|
)
|
|
leader_config = SOLeaderTeleopConfig(
|
|
port=LEADER_PORT,
|
|
id="leader_arm",
|
|
use_degrees=True,
|
|
)
|
|
|
|
follower = SO101Follower(follower_config)
|
|
leader = SO101Leader(leader_config)
|
|
|
|
follower.connect()
|
|
leader.connect()
|
|
|
|
# ── CRITICAL: hold both arms at their current position before doing anything ─
|
|
# configure() enables follower torque, and the Goal_Position register may contain
|
|
# a stale value from a previous session. Writing current→goal prevents sudden motion.
|
|
follower_current = hold_position(follower)
|
|
leader_current = hold_position(leader) # leader torque is still off here, but sets the register
|
|
|
|
# ── Intervention state + keyboard listener ───────────────────────────────────
|
|
is_intervening = False
|
|
stop_event = Event()
|
|
|
|
|
|
def _start_keyboard_listener():
|
|
if not PYNPUT_AVAILABLE:
|
|
logger.warning("pynput not available — spacebar toggle disabled.")
|
|
return None
|
|
|
|
def on_press(key):
|
|
global is_intervening
|
|
if key == pynput_keyboard.Key.space:
|
|
is_intervening = not is_intervening
|
|
state = "INTERVENTION (leader → follower)" if is_intervening else "IDLE (follower holds)"
|
|
print(f"\n[SPACE] {state}\n")
|
|
|
|
def listen():
|
|
with pynput_keyboard.Listener(on_press=on_press) as listener:
|
|
while not stop_event.is_set():
|
|
time.sleep(0.05)
|
|
listener.stop()
|
|
|
|
t = Thread(target=listen, daemon=True)
|
|
t.start()
|
|
return t
|
|
|
|
|
|
kbd_thread = _start_keyboard_listener()
|
|
|
|
# Enable leader torque AFTER writing its goal to current position, so it holds in place.
|
|
leader.bus.sync_write("Torque_Enable", 1)
|
|
leader_torque_on = True
|
|
|
|
print("\nTeleoperation ready.")
|
|
print(" SPACE → toggle intervention (leader controls follower)")
|
|
print(" Ctrl+C → exit\n")
|
|
|
|
try:
|
|
while True:
|
|
t0 = time.perf_counter()
|
|
|
|
if is_intervening:
|
|
# ── Intervention: leader torque OFF, follower mirrors leader ──────
|
|
if leader_torque_on:
|
|
leader.bus.sync_write("Torque_Enable", 0)
|
|
leader_torque_on = False
|
|
|
|
leader_action = leader.get_action() # reads present leader joints
|
|
follower.send_action(leader_action) # follower tracks leader
|
|
|
|
else:
|
|
# ── Idle: leader torque ON, leader mirrors follower, follower holds
|
|
if not leader_torque_on:
|
|
# Before re-enabling torque, set the leader's goal to its current
|
|
# position so it doesn't snap to the follower position suddenly.
|
|
hold_position(leader)
|
|
leader.bus.sync_write("Torque_Enable", 1)
|
|
leader_torque_on = True
|
|
|
|
follower_obs = follower.get_observation()
|
|
# Command leader to match follower (so next intervention has no jump)
|
|
goal_pos = {motor: follower_obs[f"{motor}.pos"] for motor in leader.bus.motors}
|
|
leader.bus.sync_write("Goal_Position", goal_pos)
|
|
# Follower holds — no send_action call
|
|
|
|
precise_sleep(max(1.0 / FPS - (time.perf_counter() - t0), 0.0))
|
|
|
|
except KeyboardInterrupt:
|
|
print("\nExiting...")
|
|
finally:
|
|
stop_event.set()
|
|
leader.bus.sync_write("Torque_Enable", 0)
|
|
follower.disconnect()
|
|
leader.disconnect()
|