diff --git a/expyfun/_experiment_controller.py b/expyfun/_experiment_controller.py index 50a3749c..78b53553 100644 --- a/expyfun/_experiment_controller.py +++ b/expyfun/_experiment_controller.py @@ -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 @@ -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) diff --git a/expyfun/_eyelink_controller.py b/expyfun/_eyelink_controller.py index 7cfeafb7..e6b1d20e 100644 --- a/expyfun/_eyelink_controller.py +++ b/expyfun/_eyelink_controller.py @@ -137,6 +137,9 @@ 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 ----- @@ -144,7 +147,8 @@ class EyelinkController(object): 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: @@ -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 @@ -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 @@ -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 @@ -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 @@ -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, diff --git a/expyfun/_utils.py b/expyfun/_utils.py index 15544ec0..4aaa2217 100644 --- a/expyfun/_utils.py +++ b/expyfun/_utils.py @@ -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): diff --git a/expyfun/tests/test_experiment_controller.py b/expyfun/tests/test_experiment_controller.py index bc2df489..a898b83e 100644 --- a/expyfun/tests/test_experiment_controller.py +++ b/expyfun/tests/test_experiment_controller.py @@ -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() diff --git a/expyfun/tests/test_eyelink_controller.py b/expyfun/tests/test_eyelink_controller.py index 87c08660..3d830b23 100644 --- a/expyfun/tests/test_eyelink_controller.py +++ b/expyfun/tests/test_eyelink_controller.py @@ -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') @@ -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])