Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions expyfun/_experiment_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def __init__(self, exp_name, audio_controller=None, response_device=None,
# placeholder for extra actions to do on flip-and-play
self._on_every_flip = []
self._on_next_flip = []
self._on_every_wait = []
self._on_trial_ok = []
# placeholder for extra actions to run on close
self._extra_cleanup_fun = [] # be aware of order when adding to this
Expand Down Expand Up @@ -680,6 +681,20 @@ def call_on_every_flip(self, function):
else:
self._on_every_flip = []

def call_on_every_wait(self, function):
"""Add a function to be executed on every wait.

Parameters
----------
function : function | None
The function to call. If ``None``, all the "on every wait"
functions will be cleared.
"""
if function is not None:
self._on_every_wait.append(function)
else:
self._on_every_wait = []

def _convert_units(self, verts, fro, to):
"""Convert between different screen units"""
check_units(to)
Expand Down
52 changes: 48 additions & 4 deletions expyfun/_eyelink_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,18 @@ class EyelinkController(object):
Sample rate to use. Must be one of [250, 500, 1000, 2000].
verbose : bool, str, int, or None
If not None, override default verbose level (see expyfun.verbose).
calbration_key : iterable
Keys that can be pressed to trigger recalibration when
``EyelinkController.check_recalibrate'' is called.

Notes
-----
The data will be saved to the ExperimentController ``output_dir``.
If this was `None`, data will be saved to the current working dir.
"""
@verbose_dec
def __init__(self, ec, link='default', fs=1000, verbose=None):
def __init__(self, ec, link='default', fs=1000, verbose=None,
calibration_key=('c',)):
if link == 'default':
link = get_config('EXPYFUN_EYELINK', None)
if link is not None and pylink is None:
Expand Down Expand Up @@ -189,6 +193,7 @@ def __init__(self, ec, link='default', fs=1000, verbose=None):
self._current_open_file = None
logger.debug('EyeLink: Setup complete')
self._ec.flush()
self.calibration_key = calibration_key

def _setup(self, fs=1000):
"""Start up Eyelink
Expand Down Expand Up @@ -381,6 +386,42 @@ def calibrate(self, beep=False, prompt=True):
self._start_recording()
return fname

def check_recalibrate(self, keys=None, prompt=True):
"""Compare key buffer to recalibration keys and calibrate if matched.

The function takes key presses from the key buffer and compares them
to EyelinkController 'calibration_keys'. If one of the calibration keys
has been pressed prior to calling this function, a new calibration will
start. Instead of using the key buffer, keys can also be manually input
into the function.

Parameters
----------
keys : list or string or None
Keys to check against the set of calibration keys that trigger a
calibration. None if using keys from the key buffer.
prompt : bool
Whether to show the calibration prompt screen before starting the
calibation procedure,
"""
calibrate = False
if keys is None:
check = self.calibration_key
keys = self._ec._response_handler._retrieve_keyboard_events(check)
else:
if isinstance(keys, string_types):
keys = [keys]
if isinstance(keys, list):
keys = [k for k in keys if k in self.calibration_key]
else:
raise TypeError('Calibration checking requires a string or '
' list of strings, not a {}.'
''.format(type(keys)))
if len(keys):
self.calibrate(prompt=prompt)
calibrate = True
return calibrate

def _stamp_trial_id(self, ids):
"""Send trial id message

Expand Down Expand Up @@ -505,7 +546,7 @@ def wait_for_fix(self, fix_pos, fix_time=0., tol=100., max_wait=np.inf,
"""
# initialize eye position to be outside of target
fix_success = False

calibrate = False
# sample eye position for el.fix_hold seconds
time_in = time.time()
time_out = time_in + max_wait
Expand All @@ -514,7 +555,7 @@ def wait_for_fix(self, fix_pos, fix_time=0., tol=100., max_wait=np.inf,
raise ValueError('fix_pos must be a 2-element array-like vector')
fix_pos = self._ec._convert_units(fix_pos[:, np.newaxis], units, 'pix')
fix_pos = fix_pos[:, 0]
while (time.time() < time_out and not
while (time.time() < time_out and not calibrate and not
(fix_success and time.time() - time_in >= fix_time)):
# sample eye position
eye_pos = self.get_eye_position() # in pixels
Expand All @@ -525,7 +566,10 @@ def wait_for_fix(self, fix_pos, fix_time=0., tol=100., max_wait=np.inf,
time_in = time.time()
self._ec._response_handler.check_force_quit()
self._ec.wait_secs(check_interval)

calibrate = self.check_recalibrate()
# rerun wait_for_fix if recalibrated
if calibrate:
fix_success = False
return fix_success

def maintain_fix(self, fix_pos, check_duration, tol=100., period=.250,
Expand Down
2 changes: 2 additions & 0 deletions expyfun/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,8 @@ def wait_secs(secs, ec=None):
win.dispatch_events()
if ec is not None:
ec.check_force_quit()
for function in ec._on_every_wait:
function()


def running_rms(signal, win_length):
Expand Down
4 changes: 4 additions & 0 deletions expyfun/tests/test_experiment_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,10 @@ def test_ec(ac=None, rd=None):
ec.stop()
assert_true(ec._playing is False)

ec.call_on_every_wait(ec.check_force_quit)
ec.wait_secs(0.05)
ec.call_on_every_wait(None)

ec.flip(-np.inf)
assert_true(ec._playing is False)
ec.estimate_screen_fs()
Expand Down
6 changes: 5 additions & 1 deletion expyfun/tests/test_eyelink_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import warnings

from expyfun import EyelinkController, ExperimentController
from expyfun._utils import _TempDir, _hide_window, requires_opengl21
from expyfun._utils import (_TempDir, _hide_window, requires_opengl21,
fake_button_press)

warnings.simplefilter('always')

Expand Down Expand Up @@ -46,6 +47,9 @@ def test_eyelink_methods():
# run much of the calibration code, but don't *actually* do it
el._fake_calibration = True
el.calibrate(beep=False, prompt=False)
fake_button_press(ec, 'c')
assert el.check_recalibrate(prompt=False)
el.check_recalibrate('k', prompt=False)
el._fake_calibration = False
# missing el_id
assert_raises(KeyError, ec.identify_trial, ec_id='foo', ttl_id=[0])
Expand Down