#!/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 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'])
@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