Source code for isaaclab.devices.gamepad.se3_gamepad
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.# All rights reserved.## SPDX-License-Identifier: BSD-3-Clause"""Gamepad controller for SE(3) control."""importnumpyasnpimportweakreffromcollections.abcimportCallablefromscipy.spatial.transformimportRotationimportcarbimportomnifrom..device_baseimportDeviceBase
[docs]classSe3Gamepad(DeviceBase):"""A gamepad controller for sending SE(3) commands as delta poses and binary command (open/close). This class is designed to provide a gamepad controller for a robotic arm with a gripper. It uses the gamepad interface to listen to gamepad events and map them to the robot's task-space commands. 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. Stick and Button bindings: ============================ ========================= ========================= Description Stick/Button (+ve axis) Stick/Button (-ve axis) ============================ ========================= ========================= Toggle gripper(open/close) X Button X Button Move along x-axis Left Stick Up Left Stick Down Move along y-axis Left Stick Left Left Stick Right Move along z-axis Right Stick Up Right Stick Down Rotate along x-axis D-Pad Left D-Pad Right Rotate along y-axis D-Pad Down D-Pad Up Rotate along z-axis Right Stick Left Right Stick Right ============================ ========================= ========================= .. seealso:: The official documentation for the gamepad interface: `Carb Gamepad Interface <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/gamepad.html>`__. """
[docs]def__init__(self,pos_sensitivity:float=1.0,rot_sensitivity:float=1.6,dead_zone:float=0.01):"""Initialize the gamepad layer. Args: pos_sensitivity: Magnitude of input position command scaling. Defaults to 1.0. rot_sensitivity: Magnitude of scale input rotation commands scaling. Defaults to 1.6. dead_zone: Magnitude of dead zone for gamepad. An event value from the gamepad less than this value will be ignored. Defaults to 0.01. """# turn off simulator gamepad controlcarb_settings_iface=carb.settings.get_settings()carb_settings_iface.set_bool("/persistent/app/omniverse/gamepadCameraControl",False)# store inputsself.pos_sensitivity=pos_sensitivityself.rot_sensitivity=rot_sensitivityself.dead_zone=dead_zone# acquire omniverse interfacesself._appwindow=omni.appwindow.get_default_app_window()self._input=carb.input.acquire_input_interface()self._gamepad=self._appwindow.get_gamepad(0)# note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is calledself._gamepad_sub=self._input.subscribe_to_gamepad_events(self._gamepad,lambdaevent,*args,obj=weakref.proxy(self):obj._on_gamepad_event(event,*args),)# bindings for gamepad to commandself._create_key_bindings()# command buffersself._close_gripper=False# When using the gamepad, two values are provided for each axis.# For example: when the left stick is moved down, there are two evens: `left_stick_down = 0.8`# and `left_stick_up = 0.0`. If only the value of left_stick_up is used, the value will be 0.0,# which is not the desired behavior. Therefore, we save both the values into the buffer and use# the maximum value.# (positive, negative), (x, y, z, roll, pitch, yaw)self._delta_pose_raw=np.zeros([2,6])# dictionary for additional callbacksself._additional_callbacks=dict()
def__del__(self):"""Unsubscribe from gamepad events."""self._input.unsubscribe_to_gamepad_events(self._gamepad,self._gamepad_sub)self._gamepad_sub=Nonedef__str__(self)->str:"""Returns: A string containing the information of joystick."""msg=f"Gamepad Controller for SE(3): {self.__class__.__name__}\n"msg+=f"\tDevice name: {self._input.get_gamepad_name(self._gamepad)}\n"msg+="\t----------------------------------------------\n"msg+="\tToggle gripper (open/close): X\n"msg+="\tMove arm along x-axis: Left Stick Up/Down\n"msg+="\tMove arm along y-axis: Left Stick Left/Right\n"msg+="\tMove arm along z-axis: Right Stick Up/Down\n"msg+="\tRotate arm along x-axis: D-Pad Right/Left\n"msg+="\tRotate arm along y-axis: D-Pad Down/Up\n"msg+="\tRotate arm along z-axis: Right Stick Left/Right\n"returnmsg""" Operations """
[docs]defadd_callback(self,key:carb.input.GamepadInput,func:Callable):"""Add additional functions to bind gamepad. A list of available gamepad keys are present in the `carb documentation <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/gamepad.html>`__. Args: key: The gamepad 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
[docs]defadvance(self)->tuple[np.ndarray,bool]:"""Provides the result from gamepad event state. Returns: A tuple containing the delta pose command and gripper commands. """# -- resolve position commanddelta_pos=self._resolve_command_buffer(self._delta_pose_raw[:,:3])# -- resolve rotation commanddelta_rot=self._resolve_command_buffer(self._delta_pose_raw[:,3:])# -- convert to rotation vectorrot_vec=Rotation.from_euler("XYZ",delta_rot).as_rotvec()# return the command and gripper statereturnnp.concatenate([delta_pos,rot_vec]),self._close_gripper
""" Internal helpers. """def_on_gamepad_event(self,event,*args,**kwargs):"""Subscriber callback to when kit is updated. Reference: https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/gamepad.html """# check if the event is a button presscur_val=event.valueifabs(cur_val)<self.dead_zone:cur_val=0# -- buttonifevent.input==carb.input.GamepadInput.X:# toggle gripper based on the button pressedifcur_val>0.5:self._close_gripper=notself._close_gripper# -- left and right stickifevent.inputinself._INPUT_STICK_VALUE_MAPPING:direction,axis,value=self._INPUT_STICK_VALUE_MAPPING[event.input]# change the value only if the stick is moved (soft press)self._delta_pose_raw[direction,axis]=value*cur_val# -- dpad (4 arrow buttons on the console)ifevent.inputinself._INPUT_DPAD_VALUE_MAPPING:direction,axis,value=self._INPUT_DPAD_VALUE_MAPPING[event.input]# change the value only if button is pressed on the DPADifcur_val>0.5:self._delta_pose_raw[direction,axis]=valueself._delta_pose_raw[1-direction,axis]=0else:self._delta_pose_raw[:,axis]=0# additional callbacksifevent.inputinself._additional_callbacks:self._additional_callbacks[event.input]()# since no error, we are fine :)returnTruedef_create_key_bindings(self):"""Creates default key binding."""# map gamepad input to the element in self._delta_pose_raw# the first index is the direction (0: positive, 1: negative)# the second index is the axis (0: x, 1: y, 2: z, 3: roll, 4: pitch, 5: yaw)# the third index is the sensitivity of the commandself._INPUT_STICK_VALUE_MAPPING={# forward commandcarb.input.GamepadInput.LEFT_STICK_UP:(0,0,self.pos_sensitivity),# backward commandcarb.input.GamepadInput.LEFT_STICK_DOWN:(1,0,self.pos_sensitivity),# right commandcarb.input.GamepadInput.LEFT_STICK_RIGHT:(0,1,self.pos_sensitivity),# left commandcarb.input.GamepadInput.LEFT_STICK_LEFT:(1,1,self.pos_sensitivity),# upward commandcarb.input.GamepadInput.RIGHT_STICK_UP:(0,2,self.pos_sensitivity),# downward commandcarb.input.GamepadInput.RIGHT_STICK_DOWN:(1,2,self.pos_sensitivity),# yaw command (positive)carb.input.GamepadInput.RIGHT_STICK_RIGHT:(0,5,self.rot_sensitivity),# yaw command (negative)carb.input.GamepadInput.RIGHT_STICK_LEFT:(1,5,self.rot_sensitivity),}self._INPUT_DPAD_VALUE_MAPPING={# pitch command (positive)carb.input.GamepadInput.DPAD_UP:(1,4,self.rot_sensitivity*0.8),# pitch command (negative)carb.input.GamepadInput.DPAD_DOWN:(0,4,self.rot_sensitivity*0.8),# roll command (positive)carb.input.GamepadInput.DPAD_RIGHT:(1,3,self.rot_sensitivity*0.8),# roll command (negative)carb.input.GamepadInput.DPAD_LEFT:(0,3,self.rot_sensitivity*0.8),}def_resolve_command_buffer(self,raw_command:np.ndarray)->np.ndarray:"""Resolves the command buffer. Args: raw_command: The raw command from the gamepad. Shape is (2, 3) This is a 2D array since gamepad dpad/stick returns two values corresponding to the positive and negative direction. The first index is the direction (0: positive, 1: negative) and the second index is value (absolute) of the command. Returns: Resolved command. Shape is (3,) """# compare the positive and negative value decide the sign of the value# if the positive value is larger, the sign is positive (i.e. False, 0)# if the negative value is larger, the sign is positive (i.e. True, 1)delta_command_sign=raw_command[1,:]>raw_command[0,:]# extract the command valuedelta_command=raw_command.max(axis=0)# apply the sign# if the sign is positive, the value is already positive.# if the sign is negative, the value is negative after applying the sign.delta_command[delta_command_sign]*=-1returndelta_command