Source code for isaaclab.devices.spacemouse.se3_spacemouse
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.# All rights reserved.## SPDX-License-Identifier: BSD-3-Clause"""Spacemouse controller for SE(3) control."""importhidimportnumpyasnpimportthreadingimporttimefromcollections.abcimportCallablefromscipy.spatial.transformimportRotationfrom..device_baseimportDeviceBasefrom.utilsimportconvert_buffer
[docs]classSe3SpaceMouse(DeviceBase):"""A space-mouse controller for sending SE(3) commands as delta poses. This class implements a space-mouse controller to provide commands to a robotic arm with a gripper. It uses the `HID-API`_ which interfaces with USD and Bluetooth HID-class devices across multiple platforms [1]. The command comprises of two parts: * delta pose: a 6D vector of (x, y, z, roll, pitch, yaw) in meters and radians. * gripper: a binary command to open or close the gripper. Note: The interface finds and uses the first supported device connected to the computer. Currently tested for following devices: - SpaceMouse Compact: https://3dconnexion.com/de/product/spacemouse-compact/ .. _HID-API: https://github.com/libusb/hidapi """
[docs]def__init__(self,pos_sensitivity:float=0.4,rot_sensitivity:float=0.8):"""Initialize the space-mouse layer. Args: pos_sensitivity: Magnitude of input position command scaling. Defaults to 0.4. rot_sensitivity: Magnitude of scale input rotation commands scaling. Defaults to 0.8. """# store inputsself.pos_sensitivity=pos_sensitivityself.rot_sensitivity=rot_sensitivity# acquire device interfaceself._device=hid.device()self._find_device()# read rotationsself._read_rotation=False# command buffersself._close_gripper=Falseself._delta_pos=np.zeros(3)# (x, y, z)self._delta_rot=np.zeros(3)# (roll, pitch, yaw)# dictionary for additional callbacksself._additional_callbacks=dict()# run a thread for listening to device updatesself._thread=threading.Thread(target=self._run_device)self._thread.daemon=Trueself._thread.start()
def__del__(self):"""Destructor for the class."""self._thread.join()def__str__(self)->str:"""Returns: A string containing the information of joystick."""msg=f"Spacemouse Controller for SE(3): {self.__class__.__name__}\n"msg+=f"\tManufacturer: {self._device.get_manufacturer_string()}\n"msg+=f"\tProduct: {self._device.get_product_string()}\n"msg+="\t----------------------------------------------\n"msg+="\tRight button: reset command\n"msg+="\tLeft button: toggle gripper command (open/close)\n"msg+="\tMove mouse laterally: move arm horizontally in x-y plane\n"msg+="\tMove mouse vertically: move arm vertically\n"msg+="\tTwist mouse about an axis: rotate arm about a corresponding axis"returnmsg""" Operations """
[docs]defreset(self):# default flagsself._close_gripper=Falseself._delta_pos=np.zeros(3)# (x, y, z)self._delta_rot=np.zeros(3)# (roll, pitch, yaw)
[docs]defadd_callback(self,key:str,func:Callable):# check keys supported by callbackifkeynotin["L","R"]:raiseValueError(f"Only left (L) and right (R) buttons supported. Provided: {key}.")# TODO: Improve this to allow multiple buttons on same key.self._additional_callbacks[key]=func
[docs]defadvance(self)->tuple[np.ndarray,bool]:"""Provides the result from spacemouse event state. Returns: A tuple containing the delta pose command and gripper commands. """rot_vec=Rotation.from_euler("XYZ",self._delta_rot).as_rotvec()# if new command received, reset event flag to False until keyboard updated.returnnp.concatenate([self._delta_pos,rot_vec]),self._close_gripper
""" Internal helpers. """def_find_device(self):"""Find the device connected to computer."""found=False# implement a timeout for device searchfor_inrange(5):fordeviceinhid.enumerate():if(device["product_string"]=="SpaceMouse Compact"ordevice["product_string"]=="SpaceMouse Wireless"):# set found flagfound=Truevendor_id=device["vendor_id"]product_id=device["product_id"]# connect to the deviceself._device.close()self._device.open(vendor_id,product_id)# check if device foundifnotfound:time.sleep(1.0)else:break# no device found: return falseifnotfound:raiseOSError("No device found by SpaceMouse. Is the device connected?")def_run_device(self):"""Listener thread that keeps pulling new messages."""# keep runningwhileTrue:# read the device datadata=self._device.read(7)ifdataisnotNone:# readings from 6-DoF sensorifdata[0]==1:self._delta_pos[1]=self.pos_sensitivity*convert_buffer(data[1],data[2])self._delta_pos[0]=self.pos_sensitivity*convert_buffer(data[3],data[4])self._delta_pos[2]=self.pos_sensitivity*convert_buffer(data[5],data[6])*-1.0elifdata[0]==2andnotself._read_rotation:self._delta_rot[1]=self.rot_sensitivity*convert_buffer(data[1],data[2])self._delta_rot[0]=self.rot_sensitivity*convert_buffer(data[3],data[4])self._delta_rot[2]=self.rot_sensitivity*convert_buffer(data[5],data[6])*-1.0# readings from the side buttonselifdata[0]==3:# press left buttonifdata[1]==1:# close gripperself._close_gripper=notself._close_gripper# additional callbacksif"L"inself._additional_callbacks:self._additional_callbacks["L"]()# right button is for resetifdata[1]==2:# reset layerself.reset()# additional callbacksif"R"inself._additional_callbacks:self._additional_callbacks["R"]()ifdata[1]==3:self._read_rotation=notself._read_rotation