Source code for isaaclab.devices.spacemouse.se2_spacemouse
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.# All rights reserved.## SPDX-License-Identifier: BSD-3-Clause"""Spacemouse controller for SE(2) control."""importhidimportnumpyasnpimportthreadingimporttimefromcollections.abcimportCallablefrom..device_baseimportDeviceBasefrom.utilsimportconvert_buffer
[docs]classSe2SpaceMouse(DeviceBase):r"""A space-mouse controller for sending SE(2) commands as delta poses. This class implements a space-mouse controller to provide commands to mobile base. It uses the `HID-API`_ which interfaces with USD and Bluetooth HID-class devices across multiple platforms. The command comprises of the base linear and angular velocity: :math:`(v_x, v_y, \omega_z)`. 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,v_x_sensitivity:float=0.8,v_y_sensitivity:float=0.4,omega_z_sensitivity:float=1.0):"""Initialize the spacemouse layer. Args: v_x_sensitivity: Magnitude of linear velocity along x-direction scaling. Defaults to 0.8. v_y_sensitivity: Magnitude of linear velocity along y-direction scaling. Defaults to 0.4. omega_z_sensitivity: Magnitude of angular velocity along z-direction scaling. Defaults to 1.0. """# store inputsself.v_x_sensitivity=v_x_sensitivityself.v_y_sensitivity=v_y_sensitivityself.omega_z_sensitivity=omega_z_sensitivity# acquire device interfaceself._device=hid.device()self._find_device()# command buffersself._base_command=np.zeros(3)# 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(2): {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+="\tMove mouse laterally: move base horizontally in x-y plane\n"msg+="\tTwist mouse about z-axis: yaw base about a corresponding axis"returnmsg""" Operations """
[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)->np.ndarray:"""Provides the result from spacemouse event state. Returns: A 3D array containing the linear (x,y) and angular velocity (z). """returnself._base_command
""" Internal helpers. """def_find_device(self):"""Find the device connected to computer."""found=False# implement a timeout for device searchfor_inrange(5):fordeviceinhid.enumerate():ifdevice["product_string"]=="SpaceMouse Compact":# set found flagfound=Truevendor_id=device["vendor_id"]product_id=device["product_id"]# connect to the deviceself._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(13)ifdataisnotNone:# readings from 6-DoF sensorifdata[0]==1:# along y-axisself._base_command[1]=self.v_y_sensitivity*convert_buffer(data[1],data[2])# along x-axisself._base_command[0]=self.v_x_sensitivity*convert_buffer(data[3],data[4])elifdata[0]==2:# along z-axisself._base_command[2]=self.omega_z_sensitivity*convert_buffer(data[3],data[4])# readings from the side buttonselifdata[0]==3:# press left buttonifdata[1]==1:# 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"]