Xarm-DataCollection/ufactory_lerobot/teleoperators/space_mouse/space_mouse.py
Vinman 4467e19322 feat(robot): 添加 uFactory 机械臂完整功能包
包含机器人控制(uf_robot)、遥操作(teleoperators)、
摄像头(cameras)、设备驱动(devices)和执行脚本(scripts)等模块。
2026-06-11 11:55:23 +08:00

187 lines
5.6 KiB
Python

#!/usr/bin/env python
import time
import numpy as np
from typing import Any
from threading import Thread, Event
from spnav import spnav_open, spnav_poll_event, spnav_close, SpnavMotionEvent, SpnavButtonEvent
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
from collections import defaultdict
from lerobot.teleoperators import Teleoperator
from .space_mouse_config import SpaceMouseTeleopConfig
class SpaceMouseTeleop(Teleoperator, Thread):
config_class = SpaceMouseTeleopConfig
name = "Space Mouse Teleop For xArm"
def __init__(self, config: SpaceMouseTeleopConfig):
super().__init__(config)
Thread.__init__(self) # Do NOT REMOVE!
self.stop_event = Event()
self.config = config
self.max_value = config.max_value
self.frequency = config.frequency
self.max_pos_speed = config.max_pos_speed
deadzone = config.deadzone
self._is_connected = False
self.dtype = np.float32 # CHECK! make it configurable ???
if np.issubdtype(type(deadzone), np.number):
self.deadzone = np.full(6, fill_value=deadzone, dtype=self.dtype)
else:
self.deadzone = np.array(deadzone, dtype=self.dtype)
assert (self.deadzone >= 0).all()
self.motion_event = SpnavMotionEvent([0,0,0], [0,0,0], 0)
self.button_state = defaultdict(lambda: False)
self.tx_zup_spnav = np.array([
[0,0,-1],
[1,0,0],
[0,1,0]
], dtype=np.float32)
@property
def action_features(self) -> dict:
if self.config.use_gripper:
return {
"dtype": "float32",
"shape": (4,),
"names": {"pose.dx": 0, "pose.dy": 1, "pose.dz": 2, "gripper.pos": 3},
}
else:
return {
"dtype": "float32",
"shape": (3,),
"names": {"pose.dx": 0, "pose.dy": 1, "pose.dz": 2},
}
@property
def feedback_features(self) -> dict:
if self.config.use_gripper:
return {
"dtype": "float32",
"shape": (4,),
"names": {"pose.dx": 0, "pose.dy": 1, "pose.dz": 2, "gripper.pos": 3},
}
else:
return {
"dtype": "float32",
"shape": (3,),
"names": {"pose.dx": 0, "pose.dy": 1, "pose.dz": 2},
}
@property
def is_connected(self) -> bool:
return self._is_connected
@property
def is_calibrated(self) -> bool:
return self._is_calibrated
def calibrate(self) -> None:
# CHECK!!
pass
def configure(self) -> None:
pass
def connect(self, calibrate: bool = False) -> None:
self.start()
def disconnect(self):
if not self._is_connected:
DeviceNotConnectedError(f"{self} is not connected.")
self.stop_event.set()
self._is_connected = False
self.join()
def get_motion_state(self):
me = self.motion_event
state = np.array(me.translation + me.rotation,
dtype=self.dtype) / self.max_value
is_dead = (-self.deadzone < state) & (state < self.deadzone)
state[is_dead] = 0
return state
def get_motion_state_transformed(self):
"""
Return in right-handed coordinate
z
*------>y right
| _
| (O) space mouse
v
x
back
"""
state = self.get_motion_state()
tf_state = np.zeros_like(state)
tf_state[:3] = self.tx_zup_spnav @ state[:3]
tf_state[3:] = self.tx_zup_spnav @ state[3:]
return tf_state
def is_button_pressed(self, button_id):
return self.button_state[button_id]
def run(self):
spnav_open()
self._is_connected = True
try:
while not self.stop_event.is_set():
event = spnav_poll_event()
if isinstance(event, SpnavMotionEvent):
self.motion_event = event
elif isinstance(event, SpnavButtonEvent):
self.button_state[event.bnum] = event.press
else:
time.sleep(1/200)
finally:
self._is_connected = False
spnav_close()
# delta action
def get_action(self) -> dict[str, Any]:
if not self.is_connected:
raise DeviceNotConnectedError(
"SpaceMouseTeleop is not connected. You need to run `connect()` before `get_action()`."
)
# self._drain_pressed_keys()
sm_state = self.get_motion_state_transformed()
dpos = sm_state[:3] * self.max_pos_speed / self.frequency
# Currently No rotation operation
# drot_xyz = sm_state[3:] * (max_rot_speed / frequency)
# if not self.is_button_pressed(0):
# # translation mode
# drot_xyz[:] = 0
# else:
# dpos[:] = 0
# if not self.is_button_pressed(1):
# X-Y 2D translation mode, no gripper control. Modify the code if you need more DOF control
dpos[2] = 0
gripper_action = 1.0
# output is delta change of the robot pose
action_dict = {
"pose.dx": dpos[0],
"pose.dy": dpos[1],
"pose.dz": dpos[2],
}
if self.config.use_gripper:
action_dict.update({"gripper.pos": gripper_action})
return action_dict
def send_feedback(self, feedback: dict[str, float]) -> None:
raise NotImplementedError