# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).# All rights reserved.## SPDX-License-Identifier: BSD-3-Clause"""Spacemouse controller for SE(2) control."""importhidimportnumpyasnpimportthreadingimporttimeimporttorchfromcollections.abcimportCallablefromdataclassesimportdataclassfromisaaclab.utils.arrayimportconvert_to_torchfrom..device_baseimportDeviceBase,DeviceCfgfrom.utilsimportconvert_buffer@dataclassclassSe2SpaceMouseCfg(DeviceCfg):"""Configuration for SE2 space mouse devices."""v_x_sensitivity:float=0.8v_y_sensitivity:float=0.4omega_z_sensitivity:float=1.0sim_device:str="cpu"
[文档]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 """
[文档]def__init__(self,cfg:Se2SpaceMouseCfg):"""Initialize the spacemouse layer. Args: cfg: Configuration for the spacemouse device. """# store inputsself.v_x_sensitivity=cfg.v_x_sensitivityself.v_y_sensitivity=cfg.v_y_sensitivityself.omega_z_sensitivity=cfg.omega_z_sensitivityself._sim_device=cfg.sim_device# 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 """
[文档]defadd_callback(self,key:str,func:Callable):"""Add additional functions to bind spacemouse. Args: key: The keyboard button to check against. func: The function to call when key is pressed. The callback function should not take any arguments. """self._additional_callbacks[key]=func
[文档]defadvance(self)->torch.Tensor:"""Provides the result from spacemouse event state. Returns: A 3D tensor containing the linear (x,y) and angular velocity (z). """returnconvert_to_torch(self._base_command,device=self._sim_device)
""" 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"]