#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2021 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).
"""This provides a basic ButtonBox class, and imports the `ioLab python library
<http://github.com/ioLab/python-ioLabs>`_.
"""
# This file can't be named ioLabs.py, otherwise "import ioLabs" doesn't work.
# And iolabs.py (lowercase) did not solve it either, something is case
# insensitive somewhere
from __future__ import absolute_import, division, print_function
from builtins import range
from numpy import ubyte
from psychopy import core, event, logging
try:
import ioLabs
from ioLabs import USBBox, REPORT, COMMAND
except ImportError:
err = """Failed to import the ioLabs library. If you're using your own
copy of python (not the Standalone distribution of PsychoPy) then
try installing it with:
> pip install ioLabs""".replace(' ', '')
logging.error(err)
from psychopy.constants import PRESSED, RELEASED
btn2str = {0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7',
64: 'voice'}
# hack to fake a USBBox on ubuntu during documentation
import sys
if 'sphinx' in sys.modules:
USBBox = object
[docs]class ButtonBox(USBBox):
"""PsychoPy's interface to ioLabs.USBBox. Voice key completely untested.
Original author: Jonathan Roberts
PsychoPy rewrite: Jeremy Gray, 2013
"""
def __init__(self):
"""Class to detect and report
`ioLab button box <http://www.iolab.co.uk>`_.
The ioLabs library needs to be installed. It is included in the
*Standalone* distributions of PsychoPy as of version 1.62.01.
Otherwise try "pip install ioLabs"
Usage::
from psychopy.hardware import iolab
bbox = iolab.ButtonBox()
For examples see the demos menu of the PsychoPy Coder or go to the
URL above.
All times are reported in units of seconds.
"""
ioLabs.USBBox.__init__(self)
logging.debug('init iolabs bbox')
self.events = []
self.status = None # helps Builder
self._lastReset = 0.0 # time on baseclock when bbox clock was reset
self._baseclock = core.Clock() # for basetime, not RT time
self.resetClock(log=True) # internal clock on the bbox
msg = 'button box resetClock(log=True) took %.4fs'
logging.exp(msg % self._baseclock.getTime())
self.commands.add_callback(REPORT.KEYDN, self._onKeyDown)
self.commands.add_callback(REPORT.KEYUP, self._onKeyUp)
self.commands.add_callback(REPORT.RTCREP, self._onRtcRep)
# set up callbacks for key events ("key" = button and or voice key):
def _onKey(self, report):
report.rt = report.rtc / 1000.
report.btn = report.key_code # int
report.key = btn2str[report.key_code] # str
self.events.append(report)
def _onKeyDown(self, report):
report.direction = PRESSED
self._onKey(report)
def _onKeyUp(self, report):
report.direction = RELEASED
self._onKey(report)
def _onRtcRep(self, report):
# read internal clock without needing a button-press; not working
report.rt = self.commands.rtcget()['rtc'] / 1000.
def __del__(self):
# does not seem to ever get called
self.standby()
for rep in [REPORT.KEYDN, REPORT.KEYUP, REPORT.RTCREP]:
self.remove_callback(rep)
ioLabs.USBBox.__del__(self)
[docs] def standby(self):
"""Disable all buttons and lights.
"""
self.buttons.enabled = 0x00 # 8 bit pattern 0=disabled 1=enabled
self.leds.state = 0xFF # leds == port2 == lights, 8 bits 0=on 1=off
return self
[docs] def resetClock(self, log=True):
"""Reset the clock on the bbox internal clock, e.g., at the start
of a trial.
~1ms for me; logging is much faster than the reset
"""
# better / faster than self.reset_clock() (no wait for report):
self.commands.resrtc()
self._lastReset = self._baseclock.getTime()
if log:
msg = 'reset bbox internal clock at basetime = %.3f'
logging.exp(msg % self._lastReset)
[docs] def _getTime(self, log=False):
"""Return the time on the bbox internal clock, relative to last reset.
Status: rtcget() not working
`log=True` will log the bbox time and elapsed CPU (python) time.
"""
bboxTime = self.commands.rtcget()['rtc'] / 1000.
logging.debug('bbox rtc: %.3f' % bboxTime)
if log:
cpuTime = self._baseclock.getTime() - self._lastReset
logging.debug('cpu time: %.3f' % cpuTime)
return bboxTime
[docs] def getBaseTime(self):
"""Return the time since init (using the CPU clock, not ioLab bbox).
Aim is to provide a similar API as for a Cedrus box.
Could let both clocks run for a long time to assess relative drift.
"""
return self._baseclock.getTime()
[docs] def setEnabled(self, buttonList=(0, 1, 2, 3, 4, 5, 6, 7), voice=False):
"""Set a filter to suppress events from non-enabled buttons.
The ioLabs bbox filters buttons in hardware; here we just tell it
what we want:
None - disable all buttons
an integer (0..7) - enable a single button
a list of integers (0..7) - enable all buttons in the list
Set voice=True to enable the voiceKey - gets reported as button 64
"""
allInRange = all([b in range(8) for b in buttonList])
if not (buttonList is None or allInRange):
raise ValueError('buttonList needs to be a list of 0..7, or None')
self.buttons.enabled = _list2bits(buttonList)
self.int0.enabled = int(voice)
[docs] def getEnabled(self):
"""Return a list of the buttons that are currently enabled.
"""
return _bits2list(self.buttons.enabled)
[docs] def setLights(self, lightList=(0, 1, 2, 3, 4, 5, 6, 7)):
"""Turn on the specified LEDs (None, 0..7, list of 0..7)
"""
self.leds.state = ~_list2bits(lightList)
[docs] def waitEvents(self, downOnly=True, timeout=0, escape='escape',
wait=0.002):
"""Wait for and return the first button press event.
Always calls `clearEvents()` first (like PsychoPy keyboard waitKeys).
Use `downOnly=False` to include button-release events.
`escape` is a list/tuple of keyboard events that, if pressed, will
interrupt the bbox wait; `waitKeys` will return `None` in that case.
`timeout` is the max time to wait in seconds before returning `None`.
`timeout` of 0 means no time-out (= default).
"""
self.clearEvents() # e.g., removes UP from previous DOWN
if timeout > 0:
c = core.Clock()
if escape and not type(escape) in [list, tuple]:
escape = [escape]
while True:
if wait:
core.wait(wait, 0) # throttle CPU; event RTs come from bbox
evt = self.getEvents(downOnly=downOnly)
if evt:
evt = evt[0]
break
if escape and event.getKeys(escape) or 0 < timeout < c.getTime():
return
return evt
[docs] def getEvents(self, downOnly=True):
"""Detect and return a list of all events (likely just one); no block.
Use `downOnly=False` to include button-release events.
"""
if downOnly is False:
raise NotImplementedError()
self.process_received_reports()
evts = []
for evt in self.events:
if evt.direction == PRESSED or not downOnly:
evts.append(evt)
return evts
[docs] def clearEvents(self):
"""Discard all button / voice key events.
"""
self.events[:] = []
self.commands.clear_received_reports()
logging.debug('bbox clear events')
pow2 = [2**i for i in range(8)]
def _list2bits(arg):
# return a numpy.ubyte with bits set based on integers 0..7 in arg
if type(arg) == int and 0 <= arg < 8:
return ubyte(pow2[arg])
elif hasattr(arg, '__iter__'):
return ubyte(sum([pow2[btn] for btn in arg]))
else: # None
return ubyte(0)
def _bits2list(bits):
# inverse of _list2bits: return 8 bits as converted to a buttonList
return [i for i in range(8) if bits & pow2[i]]