Source code for psychopy.hardware.joystick

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Control joysticks and gamepads from within PsychoPy.

You do need a window (and you need to be flipping it) for the joystick to be
updated.

Known issues:
    - currently under pyglet the joystick axes initialise to a value of zero
      and stay like this until the first time that axis moves

    - currently pygame (1.9.1) spits out lots of debug messages about the
      joystick and these can't be turned off :-/

Typical usage::

    from psychopy.hardware import joystick
    from psychopy import visual

    joystick.backend='pyglet'  # must match the Window
    win = visual.Window([400,400], winType='pyglet')

    nJoys = joystick.getNumJoysticks()  # to check if we have any
    id = 0
    joy = joystick.Joystick(id)  # id must be <= nJoys - 1

    nAxes = joy.getNumAxes()  # for interest
    while True:  # while presenting stimuli
        joy.getX()
        # ...
        win.flip()  # flipping implicitly updates the joystick info
"""

try:
    import pygame.joystick
    havePygame = True
except Exception:
    havePygame = False

try:
    from pyglet import input as pyglet_input  # pyglet 1.2+
    from pyglet import app as pyglet_app
    havePyglet = True
except Exception:
    havePyglet = False

try:
    import glfw
    haveGLFW = True
except ImportError:
    print("failed to import GLFW.")
    haveGLFW = False

from psychopy import logging, visual
backend = 'pyglet'  # 'pyglet' or 'pygame'


[docs]def getNumJoysticks(): """Return a count of the number of joysticks available.""" if backend == 'pyglet': return len(pyglet_input.get_joysticks()) elif backend == 'glfw': n_joys = 0 for joy in range(glfw.JOYSTICK_1, glfw.JOYSTICK_LAST): if glfw.joystick_present(joy): n_joys += 1 return n_joys else: pygame.joystick.init() return pygame.joystick.get_count()
if havePyglet: class PygletDispatcher: def dispatch_events(self): pyglet_app.platform_event_loop.step(timeout=0.001) pyglet_dispatcher = PygletDispatcher()
[docs]class Joystick: def __init__(self, id): """An object to control a multi-axis joystick or gamepad. .. note: You do need to be flipping frames (or dispatching events manually) in order for the values of the joystick to be updated. :Known issues: Currently under pyglet backends the axis values initialise to zero rather than reading the current true value. This gets fixed on the first change to each axis. """ self.id = id if backend == 'pyglet': joys = pyglet_input.get_joysticks() if id >= len(joys): logging.error("You don't have that many joysticks attached " "(remember that the first joystick has id=0 " "etc...)") else: self._device = joys[id] try: self._device.open() except pyglet_input.DeviceOpenException as e: pass self.name = self._device.device.name if len(visual.openWindows) == 0: logging.error( "You need to open a window before creating your joystick") else: for win in visual.openWindows: win()._eventDispatchers.append(pyglet_dispatcher) elif backend == 'glfw': # We can create a joystick anytime after glfwInit() is called, but # there should be a window open first. # Joystick events are processed when flipping the associated window. if not glfw.init(): logging.error("GLFW could not be initialized. Exiting.") # get all available joysticks, GLFW supports up to 16. joys = [] for joy in range(glfw.JOYSTICK_1, glfw.JOYSTICK_LAST): if glfw.joystick_present(joy): joys.append(joy) # error checks if not joys: # if the list is empty, no joysticks were found error_msg = ("No joysticks were found by the GLFW runtime. " "Check connections and try again.") logging.error(error_msg) raise RuntimeError(error_msg) elif id not in joys: error_msg = ("You don't have that many joysticks attached " "(remember that the first joystick has id=0 " "etc...)") logging.error(error_msg) raise RuntimeError(error_msg) self._device = id # just need the ID for GLFW self.name = glfw.get_joystick_name(self._device).decode("utf-8") if len(visual.openWindows) == 0: logging.error( "You need to open a window before creating your joystick") else: for win in visual.openWindows: # sending the raw ID to the window. win()._eventDispatchers.append(self._device) else: pygame.joystick.init() self._device = pygame.joystick.Joystick(id) self._device.init() self.name = self._device.get_name()
[docs] def getName(self): """Return the manufacturer-defined name describing the device.""" return self.name
[docs] def getNumButtons(self): """Return the number of digital buttons on the device.""" if backend == 'pyglet': return len(self._device.buttons) elif backend == 'glfw': _, count = glfw.get_joystick_buttons(self._device) return count else: return self._device.get_numbuttons()
[docs] def getButton(self, buttonId): """Get the state of a given button. buttonId should be a value from 0 to the number of buttons-1 """ if backend == 'pyglet': return self._device.buttons[buttonId] elif backend == 'glfw': bs, _ = glfw.get_joystick_buttons(self._device) return bs[buttonId] else: return self._device.get_button(buttonId)
[docs] def getAllButtons(self): """Get the state of all buttons as a list.""" if backend == 'pyglet': return self._device.buttons elif backend == 'glfw': bs, count = glfw.get_joystick_buttons(self._device) return [bs[i] for i in range(count)] else: bs = [] for id in range(self._device.get_numbuttons()): bs.append(self._device.get_button(id)) return bs
[docs] def getAllHats(self): """Get the current values of all available hats as a list of tuples. Each value is a tuple (x, y) where x and y can be -1, 0, +1 """ hats = [] if backend == 'pyglet': for ctrl in self._device.device.get_controls(): if ctrl.name != None and 'hat' in ctrl.name: hats.append((self._device.hat_x, self._device.hat_y)) elif backend == 'glfw': # GLFW treats hats as buttons pass else: for n in range(self._device.get_numhats()): hats.append(self._device.get_hat(n)) return hats
[docs] def getNumHats(self): """Get the number of hats on this joystick. The GLFW backend makes no distinction between hats and buttons. Calling 'getNumHats()' will return 0. """ if backend == 'pyglet': return len(self.getAllHats()) elif backend == 'glfw': return 0 else: return self._device.get_numhats()
[docs] def getHat(self, hatId=0): """Get the position of a particular hat. The position returned is an (x, y) tuple where x and y can be -1, 0 or +1 """ if backend == 'pyglet': if hatId == 0: return self._device.hat else: return self.getAllHats()[hatId] elif backend == 'glfw': # does nothing, hats are buttons in GLFW pass else: return self._device.get_hat(hatId)
[docs] def getX(self): """Return the X axis value (equivalent to joystick.getAxis(0)).""" if backend == 'pyglet': return self._device.x elif backend == 'glfw': return self.getAxis(0) else: return self._device.get_axis(0)
[docs] def getY(self): """Return the Y axis value (equivalent to joystick.getAxis(1)).""" if backend == 'pyglet': return self._device.y elif backend == 'glfw': return self.getAxis(1) else: return self._device.get_axis(1)
[docs] def getZ(self): """Return the Z axis value (equivalent to joystick.getAxis(2)).""" if backend == 'pyglet': return self._device.z elif backend == 'glfw': return self.getAxis(2) else: return self._device.get_axis(2)
[docs] def getAllAxes(self): """Get a list of all current axis values.""" axes = [] if backend == 'pyglet': names = ['x', 'y', 'z', 'rx', 'ry', 'rz', ] for axName in names: if hasattr(self._device, axName): axes.append(getattr(self._device, axName)) elif backend == 'glfw': _axes, count = glfw.get_joystick_axes(self._device) for i in range(count): axes.append(_axes[i]) else: for id in range(self._device.get_numaxes()): axes.append(self._device.get_axis(id)) return axes
[docs] def getNumAxes(self): """Return the number of joystick axes found. """ if backend == 'pyglet': return len(self.getAllAxes()) elif backend == 'glfw': _, count = glfw.get_joystick_axes(self._device) return count else: return self._device.get_numaxes()
[docs] def getAxis(self, axisId): """Get the value of an axis by an integer id. (from 0 to number of axes - 1) """ if backend == 'pyglet': val = self.getAllAxes()[axisId] if val is None: val = 0 return val elif backend == 'glfw': val, _ = glfw.get_joystick_axes(self._device) return val[axisId] else: return self._device.get_axis(axisId)
[docs]class XboxController(Joystick): """Joystick template class for the XBox 360 controller. Usage: xbctrl = XboxController(0) # joystick ID y_btn_state = xbctrl.y # get the state of the 'Y' button """ def __init__(self, id, *args, **kwargs): super(XboxController, self).__init__(id) # validate if this is an Xbox controller by its reported name if self.name.find("Xbox 360") == -1: logging.warning("The connected controller does not appear " "compatible with the 'XboxController' template. " "Unexpected input behaviour may result!") if backend != 'glfw': logging.error("Controller templates are only supported when using " "the GLFW window backend. You must also set " "joystick.backend='glfw' prior to creating a " "joystick.") # button mapping for the XBox controller self._button_mapping = {'a': 0, 'b': 1, 'x': 2, 'y': 3, 'left_shoulder': 4, 'right_shoulder': 5, 'back': 6, 'start': 7, 'left_stick': 8, 'right_stick': 9, 'up': 10, # hat 'down': 11, 'left': 12, 'right': 13} # axes groups self._axes_mapping = {'left_thumbstick': (0, 1), 'right_thumbstick': (2, 3), 'triggers': (4, 5), 'dpad': (6, 7)} @property def a(self): return self.get_a()
[docs] def get_a(self): """Get the 'A' button state. :return: bool, True if pressed down """ return self.getButton(self._button_mapping['a'])
@property def b(self): return self.get_b()
[docs] def get_b(self): """Get the 'B' button state. :return: bool, True if pressed down """ return self.getButton(self._button_mapping['b'])
@property def x(self): return self.get_x()
[docs] def get_x(self): """Get the 'X' button state. :return: bool, True if pressed down """ return self.getButton(self._button_mapping['x'])
@property def y(self): return self.get_y()
[docs] def get_y(self): """Get the 'Y' button state. :return: bool, True if pressed down """ return self.getButton(self._button_mapping['y'])
@property def left_shoulder(self): return self.get_left_shoulder()
[docs] def get_left_shoulder(self): """Get left 'shoulder' trigger state. :return: bool, True if pressed down """ return self.getButton(self._button_mapping['left_shoulder'])
@property def right_shoulder(self): return self.get_right_shoulder()
[docs] def get_right_shoulder(self): """Get right 'shoulder' trigger state. :return: bool, True if pressed down """ return self.getButton(self._button_mapping['right_shoulder'])
@property def back(self): return self.get_back()
[docs] def get_back(self): """Get 'back' button state (button to the right of the left joystick). :return: bool, True if pressed down """ return self.getButton(self._button_mapping['back'])
@property def start(self): return self.get_start()
[docs] def get_start(self): """Get 'start' button state (button to the left of the 'X' button). :return: bool, True if pressed down """ return self.getButton(self._button_mapping['start'])
@property def hat_axis(self): return self.get_hat_axis()
[docs] def get_hat_axis(self): """Get the states of the hat (sometimes called the 'directional pad'). The hat can only indicate direction but not displacement. This function reports hat values in the same way as a joystick so it may be used interchangeably with existing analog joystick code. Returns a tuple (X,Y) indicating which direction the hat is pressed between -1.0 and +1.0. Positive values indicate presses in the right or up direction. :return: tuple, zero centered X, Y values. """ # get button states button_states = self.getAllButtons() up = button_states[self._button_mapping['up']] dn = button_states[self._button_mapping['down']] lf = button_states[self._button_mapping['left']] rt = button_states[self._button_mapping['right']] # convert button states to 'analog' values return -1.0 * lf + rt, -1.0 * dn + up
@property def left_thumbstick(self): return self.get_left_thumbstick()
[docs] def get_left_thumbstick(self): """Get the state of the left joystick button; activated by pressing down on the stick. :return: bool, True if pressed down """ return self.getButton(self._button_mapping['left_stick'])
@property def right_thumbstick(self): return self.get_right_thumbstick()
[docs] def get_right_thumbstick(self): """Get the state of the right joystick button; activated by pressing down on the stick. :return: bool, True if pressed down """ return self.getButton(self._button_mapping['right_stick'])
[docs] def get_named_buttons(self, button_names): """Get the states of multiple buttons using names. A list of button states is returned for each string in list 'names'. :param button_names: tuple or list of button names :return: """ button_states = [] for button in button_names: button_states.append(self.getButton(self._button_mapping[button])) return button_states
@property def left_thumbstick_axis(self): return self.get_left_thumbstick_axis()
[docs] def get_left_thumbstick_axis(self): """Get the axis displacement values of the left thumbstick. Returns a tuple (X,Y) indicating thumbstick displacement between -1.0 and +1.0. Positive values indicate the stick is displaced right or up. :return: tuple, zero centered X, Y values. """ ax, ay = self._axes_mapping['left_thumbstick'] # we sometimes get values slightly outside the range of -1.0 < x < 1.0, # so clip them to give the user what they expect ax_val = self._clip_range(self.getAxis(ax)) ay_val = self._clip_range(self.getAxis(ay)) return ax_val, ay_val
@property def right_thumbstick_axis(self): return self.get_right_thumbstick_axis()
[docs] def get_right_thumbstick_axis(self): """Get the axis displacement values of the right thumbstick. Returns a tuple (X,Y) indicating thumbstick displacement between -1.0 and +1.0. Positive values indicate the stick is displaced right or up. :return: tuple, zero centered X, Y values. """ ax, ay = self._axes_mapping['right_thumbstick'] ax_val = self._clip_range(self.getAxis(ax)) ay_val = self._clip_range(self.getAxis(ay)) return ax_val, ay_val
@property def trigger_axis(self): return self.get_trigger_axis()
[docs] def get_trigger_axis(self): """Get the axis displacement values of both index triggers. Returns a tuple (L,R) indicating index trigger displacement between -1.0 and +1.0. Values increase from -1.0 to 1.0 the further a trigger is pushed. :return: tuple, zero centered L, R values. """ al, ar = self._axes_mapping['triggers'] al_val = self._clip_range(self.getAxis(al)) ar_val = self._clip_range(self.getAxis(ar)) return al_val, ar_val
[docs] def _clip_range(self, val): """Clip the range of a value between -1.0 and +1.0. Needed for joystick axes. :param val: :return: """ if -1.0 > val: val = -1.0 if val > 1.0: val = 1.0 return val

Back to top