From 2e899b1c2c8af6b1d3ca98e6e84c1c8ebea8e668 Mon Sep 17 00:00:00 2001 From: Benjamin Pettit Date: Wed, 11 Sep 2024 20:31:38 +1000 Subject: [PATCH] allow user to start and escape experiments with keyboard and vr controllers. --- eegnb/experiments/Experiment.py | 85 +++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/eegnb/experiments/Experiment.py b/eegnb/experiments/Experiment.py index 1feafe28..b6e06db0 100644 --- a/eegnb/experiments/Experiment.py +++ b/eegnb/experiments/Experiment.py @@ -11,6 +11,7 @@ from abc import abstractmethod from typing import Callable from psychopy import prefs +from psychopy.visual.rift import Rift #change the pref libraty to PTB and set the latency mode to high precision prefs.hardware['audioLib'] = 'PTB' prefs.hardware['audioLatencyMode'] = 3 @@ -50,8 +51,11 @@ def __init__(self, exp_name, duration, eeg, save_fn, n_trials: int, iti: float, self.soa = soa self.jitter = jitter self.use_vr = use_vr + if use_vr: + # VR interface accessible by specific experiment classes for customizing and using controllers. + self.rift: Rift = visual.Rift(monoscopic=True, headLocked=True) self.use_fullscr = use_fullscr - self.window_size = [1600,800] + self.window_size = [1600,800] @abstractmethod def load_stimulus(self): @@ -86,7 +90,7 @@ def setup(self, instructions=True): # Setting up Graphics self.window = ( - visual.Rift(monoscopic=True, headLocked=True) if self.use_vr + self.rift if self.use_vr else visual.Window(self.window_size, monitor="testMonitor", units="deg", fullscr=self.use_fullscr)) # Loading the stimulus from the specific experiment, throws an error if not overwritten in the specific experiment @@ -123,8 +127,11 @@ def show_instructions(self): # Disabling the cursor during display of instructions self.window.mouseVisible = False - # Waiting for the user to press the spacebar to start the experiment - while len(event.getKeys(keyList="space")) == 0: + # clear/reset any old key/controller events + self.__clear_user_input() + + # Waiting for the user to press the spacebar or controller button or trigger to start the experiment + while not self.__user_input('start'): # Displaying the instructions on the screen text = visual.TextStim(win=self.window, text=self.instruction_text, color=[-1, -1, -1]) self.__draw(lambda: self.__draw_instructions(text)) @@ -132,6 +139,59 @@ def show_instructions(self): # Enabling the cursor again self.window.mouseVisible = True + def __user_input(self, input_type): + if input_type == 'start': + key_input = 'spacebar' + vr_inputs = [ + ('RightTouch', 'A', True), + ('LeftTouch', 'X', True), + ('Xbox', 'A', None) + ] + + elif input_type == 'cancel': + key_input = 'escape' + vr_inputs = [ + ('RightTouch', 'B', False), + ('LeftTouch', 'Y', False), + ('Xbox', 'B', None) + ] + + if len(event.getKeys(keyList=key_input)) > 0: + return True + + if self.use_vr: + for controller, button, trigger in vr_inputs: + if self.get_vr_input(controller, button, trigger): + return True + + return False + + def get_vr_input(self, vr_controller, button=None, trigger=False): + """ + Method that returns True if the user presses the corresponding vr controller button or trigger + Args: + vr_controller: 'Xbox', 'LeftTouch' or 'RightTouch' + button: None, 'A', 'B', 'X' or 'Y' + trigger (bool): Set to True for trigger + + Returns: + + """ + trigger_squeezed = False + if trigger: + for x in self.rift.getIndexTriggerValues(vr_controller): + if x > 0.0: + trigger_squeezed = True + + button_pressed = False + if button is not None: + button_pressed, tsec = self.rift.getButtons([button], vr_controller, 'released') + + if trigger_squeezed or button_pressed: + return True + + return False + def __draw_instructions(self, text): text.draw() self.window.flip() @@ -147,6 +207,17 @@ def __draw(self, present_stimulus: Callable): self.window.setDefaultView() present_stimulus() + def __clear_user_input(self): + event.getKeys() + self.clear_vr_input() + + def clear_vr_input(self): + """ + Clears/resets input events from vr controllers + """ + if self.use_vr: + self.rift.updateInputState() + def run(self, instructions=True): """ Do the present operation for a bunch of experiments """ @@ -171,7 +242,11 @@ def iti_with_jitter(): # Current trial being rendered rendering_trial = -1 - while len(event.getKeys()) == 0 and (time() - start) < self.record_duration: + + # Clear/reset user input buffer + self.__clear_user_input() + + while not self.__user_input('cancel') and (time() - start) < self.record_duration: current_experiment_seconds = time() - start # Do not present stimulus until current trial begins(Adhere to inter-trial interval).