187 lines
5.6 KiB
Python
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 |