diff --git a/.doxyfile b/.doxyfile index c7f96d4a2c7c24d2dfdd0c51cda927fca5363e13..df5a0c7e32c36f9318a65bdf10e16a3e5eda9b6b 100644 --- a/.doxyfile +++ b/.doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = "Opticka" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2.09 +PROJECT_NUMBER = 2.16.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/.gitignore b/.gitignore index 8c8bb1e671a108268a8b1baf7cf4e90a5fcdfbdb..f2bb8bf49dbfcfe716b6af982ab98181d969b6db 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ Junk .idea .bzrignore .vscode -doxygen.log \ No newline at end of file +doxygen.log +build/ +resources/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..ff5f032b7be9bb340e01bbdac8a42ee43485678f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,86 @@ +# Changelog + +> [!NOTE] +> Changes which may affect your use of Opticka will be detailed here, starting with V2.16.x + +## V2.17.X + +* Add support for ØMQ for communication messages (command + serialised MATLAB data packet) across networked PTB instances using the `jzmqConnection` class. This is *much more robust* than raw TCP/UDP used by `pnet` & `dataConnection` and we are using it for communication across [CageLab devices](https://github.com/cogplatform/CageLab). This adds a dependency on , a MATLAB wrapper for [JeroMQ](https://github.com/zeromq/jeromq). The class explicitly supports a new neuroscience-targetted middleware called [cogmoteGO](https://github.com/Ccccraz/cogmoteGO) with an API designed to manage multiple remote PTB instances and broadcast behavioural data and results back to clients. +* Major update to the opticka UI for Alyx integration. There is an Alyx panel where you can connect to your Alyx instance to retrieve data from the server. Opticka can create a new Alyx session, and will upload the task data as a copy to the Alyx server. The data is sent to an AWS compatible data store linked to the Alyx session. The data is stored in a folder structure that matches the [International Brain Lab ONE Protocol](https://int-brain-lab.github.io/ONE/alf_intro.html) (see "A modular architecture for organizing, processing and sharing neurophysiology data," The International Brain Laboratory et al., 2023 Nat. Methods, [DOI](https://doi.org/10.1038/s41592-022-01742-6)). +* Add **awsManager** to support Alyx's AWS S3 storage. This is used to upload the task data to the AWS compatible data store linked to the Alyx session. This relies on the awscli command line tool to upload the data. You will need to install the AWS CLI and configure it with your AWS credentials. See [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) for more information. We use the cross-platform pixi package manager to install AWS CLI, using `pixi global install awscli` to install it. +* **HED Tagging** -- we now support HED tagging of the session data. This is used to tag parameters with metadata that can be used for search / analysis. The tags are stored in a TSV file in the same folder as the raw session data. See `tools/HEDTags.m` and `tools/HEDTagger.m`. The HED tags are generated from the task sequence and the task parameters. We want to support better data sharing and Alyx / ONE protocol do not have any task metadata so we chose HED from the EEGLab / BIDS projects. See for details. +* runExperiment -- big improvements to the logging system. Previously task events were stored in several places, but for Alyx / HED we need to centralise the event data. This is used to generate the HED tags and the Alyx session data. +* Add **joystickManager** — we have built our own HID compatible joystick hardware and this manager interfaces with this hardware. +* labJackT — we now send an 11bit strobed word rather than 8bit word. In theory this is backwards compatible, but you need to update the Lua server code running on the LabJack to use the 11bit word (`t = labJackT; t.initialiseServer`). 0-2047 controls EIO0-8 & CIO0-3. +* Tobii eyetrackers — update Titta interface to support the new adaptive monkey calibration. See Niehorster, D. C., Whitham, W., Lake, B. R., Schapiro, S. J., Andolina, I. M., & Yorzinski, J. L. (2024). Enhancing eye tracking for nonhuman primates and other subjects unable to follow instructions: Adaptive calibration and validation of Tobii eye trackers with the Titta toolbox. Behavior Research Methods, 57(1), 0. https://doi.org/10.3758/s13428-024-02540-y for details. +* **makeReport** — a new method in optickaCore thus available to all opticka objects. Uses the MATLAB report generator to make a PDF report of the data and property values contained in the core opticka classes (runExperiment, taskSequence, stateMachine, behaviouralRecord, tobii/eyelink/irec). Useful when analysing an experiment to get an overview of all experiment parameters for that session. +* **circularMask Shader** — add a simple texture shader that provides a circular mask for any texture stimulus (like an image). This is better than using a separate disc shader. Used in imageStimulus and movieStimulus so you can alpha blend a masked image/movie against a complex background. + +## V2.16.1 -- 106 files changed + +> [!TIP] +> Please double-check changes in `DefaultStateInfo.m` to see the changes for state machine files, this may inform changes you could add to your own state machine files... + + +* **BREAKING CHANGE**: we want to support the [International Brain Lab ONE Protocol](https://int-brain-lab.github.io/ONE/alf_intro.html) (see "A modular architecture for organizing, processing and sharing neurophysiology data," The International Brain Laboratory et al., 2023 Nat. Methods, [DOI](https://doi.org/10.1038/s41592-022-01742-6)), and we are now follwoing ALF filenaming for saved files. the root folder is still `OptickaFiles/savedData/` but now we use a folder hierarchy: if the `labName` field is empty we use the shorter ` / subjectName / YYYY-MM-DD / SessionID-namedetails.mat` otherwise we use `/ labName / subjects / subjectName / YYYY-MM-DD / SessionID-namedetails.mat` -- the opticka `MAT` file will **not** change structure or content (it will remain backwards compatible), but we will add extra metadata files to help data sharing in future releases. We will add an ALYX API call to start a session in a future release. +* **BREAKING CHANGE**: LabJack T4 -- we increased the strobe word from 8 to 11 bits, now on EIO1:8 CIO1:3, this should in theory be backwards compatible as 8bits is still the same lines. Upgrade the LabJack T4 (connected over USB) like this: +```matlab +t = labJackT(); +open(t); +initialiseServer(t); +close(t); +``` +* Add improved Rigid Body physics engine. We now use [dyn4j](https://dyn4j.org), an open-source Java 2D physics engine. `animationManager` is upgraded (previously it used my own simple physics engine, which couldn't scale to many collisions). Opticka uses degrees, and we do a simple mapping of degrees > meters, so 1deg stimulus is a 1m object.Test it with: \ +```matlab +sM = screenManager(); +b = imageStimulus('size',4,'filePath','moon.png',... + 'name','moon'); +b.speed = 25; % will define velocity +b.angle = -45; % will define velocity +aM = animationManager(); % our new animation manager +sv = open(sM); % open PTB screen, sv is screen info +setup(b, sM); % initialise stimulus with PTB screen +addScreenBoundaries(aM, sv); % add floor, ceiling and +% walls to rigidbody world based on the screen dimensions sv +addBody(aM, b); % add stimulus as a rigidbody +setup(aM); % initialise the simulation. +for i = 1:60 + draw(b); % draw the stimulus + flip(sM); % flip the screen + step(aM); % step the simulation +end +``` +* Improve touchManager to better use the rigid body animations with touch events. You can now finger-drag and "fling" physical objects around the screen. +* add Procedurally generated polar checkerboards: `polarBoardStimulus`, and improved polar gratings to mask with arc segments: `polarGratingStimulus`. +* added new stimulus: `dotlineStimulus` - a line made of dots. +* `pupilCoreStimulus` -- a calibration stimulus for pupil core eyetrackers. +* all stimuli: `updateXY()` method quickly updates the X and Y position without a full stimulus `update()`, used by the update `animationManager`. +* all stimuli: added `szPx` `szD` `xfinalD` and `yFinalD` properties so we have both pixels and degrees values available. +* all stimuli: `szIsPx` property tells us whether the dynamically generated size at each trial is in pixels or degrees. +* add `nirSmartManager` to support nirSmart FNIRS recording system. +* improved the mouse dummy mode for the touchscreen `touchManager`. +* `arduinoManager` can now use a raspberry pi GPIO if no arduino is present. +* Update image and movie stimuli to better handle mutliple images. +* Add a `Test Hardware` menu to opticka GUI. You can use this to test that the reward system / eyetracker / recording markers are working before you do any data collection each day. +* Updates to support the latest Titta toolbox for Tobii eyetrackers. +* `optickaCore.geyKeys()` -- support shift key. +* `runExperiment` -- better handling when no eyetracker is selected for a task that may have eyetracker functions. +* `screenManager` -- update movieRecording settings. You pass `screenManager.movieSettings.record = true` to enable screen recording. Note that the movie is handled automatically, so: +```matlab +s = screenManager(); +s.movieSettings.record = true; +s.open(); % this also initialises the video file +for i = 1:3 + s.drawText('Hello World); + s.flip(); % this also adds the frame to the movie +end +s.close(); % this also closes the video file. +``` +* lots of improvements for analysing Tobii and iRec data (see `tobiiAnalysis` and `iRecAnalysis`), in particular we integrate [Nyström, M. & Holmqvist, K. 2010](https://github.com/dcnieho/NystromHolmqvist2010) toolbox to improve data cleaning. +* switch to using string arrays for comment property fields. + + +### State Machine Changes: + +* `@()needFlip(me, false, 0);` -- add a 3rd parameter to control the flip of the eyetracker window. NOTE: the number 0=no-flip, 1=dontclear+dontforce, 2=clear+dontforce, 3=clear+force, 4=clear+force first frame then switch to 1 -- dontclear=leave previous frame onscreen, useful to show eyetrack, dontforce=don't force flip, faster as flip for the tracker is throttled +* `@()trackerTrialStart(eT, getTaskIndex(me));` & `@()trackerTrialEnd(eT, tS.CORRECT)` -- this is a new function that handles the several commands that were used previously to send the trial start/end info to the eyetracker. As we increase the number of supported eyetrackers, it is better to wrap this in a single function. NOTE: we mostly use the Eyelink message structure to define trials, even for other trackers, which simplifies analysis later on. \ No newline at end of file diff --git a/CoreProtocols/100msFlash_MOC.mat b/CoreProtocols/100msFlash_MOC.mat new file mode 100644 index 0000000000000000000000000000000000000000..9b789c975dfaf467711f9f4fbd1f3ecdac266980 Binary files /dev/null and b/CoreProtocols/100msFlash_MOC.mat differ diff --git a/CoreProtocols/AreaSummation.mat b/CoreProtocols/AreaSummation.mat index 95e64f3c97e3b04cfc439334da642079ebe50e75..9dd28cb47cc3d72c4ca35b5f7fb66a985ee3b38d 100644 Binary files a/CoreProtocols/AreaSummation.mat and b/CoreProtocols/AreaSummation.mat differ diff --git a/CoreProtocols/AreaSummationStateInfo.m b/CoreProtocols/AreaSummationStateInfo.m index b57681d5ad76756661c8fe5b0e6a7d7b6756c26c..650189579abf9b4ec1f5fd9b1c88b1a1b8971a26 100644 --- a/CoreProtocols/AreaSummationStateInfo.m +++ b/CoreProtocols/AreaSummationStateInfo.m @@ -19,31 +19,36 @@ %================================================================== %------------General Settings----------------- -tS.useTask = true; %==use taskSequence (randomised variable task object) -tS.rewardTime = 250; %==TTL time in milliseconds -tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. -tS.checkKeysDuringStimulus = true; %==allow keyboard control during all states? Slight drop in performance -tS.recordEyePosition = false; %==record eye position within PTB, **in addition** to the EDF? -tS.askForComments = true; %==little UI requestor asks for comments before/after run +tS.name = 'Area-Summation'; %==name of this protocol tS.saveData = true; %==save behavioural and eye movement data? -tS.name = 'area-summation'; %==name of this protocol -tS.nStims = stims.n; %==number of stimuli -tS.tOut = 5; %==if wrong response, how long to time out before next trial +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? +tS.askForComments = false; %==UI requestor asks for comments before/after run +tS.includeErrors = false; %==do we update the trial number even for incorrect saccade/fixate, if true then we call updateTask for both correct and incorrect, otherwise we only call updateTask() for correct responses +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.timeOut = 2; %==if wrong response, how long to time out before next trial tS.CORRECT = 1; %==the code to send eyetracker for correct trials tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume +% reward system values, set by GUI, but could be overridden here +%rM.reward.time = 250; %==TTL time in milliseconds +%rM.reward.pin = 2; %==Output pin, 2 by default with Arduino. %================================================================== -%----------------Debug logging to command window------------------ +%------------ ----DEBUG LOGGING to command window------------------ % uncomment each line to get specific verbose logging from each of these % components; you can also set verbose in the opticka GUI to enable all of % these… -%sM.verbose = true; %==print out stateMachine info for debugging -%stims.verbose = true; %==print out metaStimulus info for debugging -%io.verbose = true; %==print out io commands for debugging -%eT.verbose = true; %==print out eyelink commands for debugging -%rM.verbose = true; %==print out reward commands for debugging -%task.verbose = true; %==print out task info for debugging +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging %================================================================== %-----------------INITIAL Eyetracker Settings---------------------- @@ -55,174 +60,125 @@ tS.firstFixRadius = 2; % radius in degrees tS.strict = true; % do we forbid eye to enter-exit-reenter fixation window? tS.exclusionZone = []; % do we add an exclusion zone where subject cannot saccade to... tS.stimulusFixTime = 2; % time to fix on the stimulus -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); %================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options -% here they will OVERRIDE the GUI ones; if they are commented then the GUI -% options are used. me.elsettings and me.tobiisettings contain the GUI -% settings you can test if they are empty or not and set them based on -% that... -eT.name = tS.name; -if tS.saveData == true; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - eT.name = tS.name; - if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? - if tS.saveData == true; eT.recordData = true; end %===save EDF file? - if isempty(me.eyetracker.esettings) %==check if GUI settings are empty - eT.sampleRate = 250; %==sampling rate - eT.calibrationStyle = 'HV5'; %==calibration style - eT.calibrationProportion = [0.4 0.4]; %==the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation use 1-9 to show each dot, space to select fix as valid, - % INS key ON EYELINK KEYBOARD to accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; %==calibration target colour - eT.modify.calibrationtargetsize = 2; %==size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; %==width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; %==-1 = use any attachedkeyboard - eT.modify.targetbeep = 1; %==beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - eT.name = tS.name; - if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? - if isempty(me.eyetracker.tsettings) %==check if GUI settings are empty - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - eT.valPositions = [ .5 .5 ]; - end -end - -%Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix -eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%Ensure we don't start with any exclusion zones set up -eT.resetExclusionZones(); - -%================================================================== -%----WHICH states assigned as correct or break for online plot?---- -%----You need to use regex patterns for the match (doc regexp)----- -bR.correctStateName = "correct"; -bR.breakStateName = ["breakfix","incorrect"]; - -%================================================================== -%--------------randomise stimulus variables every trial?----------- -% if you want to have some randomisation of stimuls variables without -% using taskSequence task, you can uncomment this and runExperiment can -% use this structure to change e.g. X or Y position, size, angle -% see metaStimulus for more details. Remember this will not be "Saved" for -% later use, if you want to do controlled methods of constants experiments -% use taskSequence to define proper randomised and balanced variable -% sets and triggers to send to recording equipment etc... -% -% stims.choice = []; +%-----------------BEAVIOURAL PLOT CONFIGURATION-------------------- +%--WHICH states assigned correct / incorrect for the online plot?-- +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%========================================================================= +%------------------Randomise stimulus variables every trial?-------------- +% If you want to have some randomisation of stimuls variables WITHOUT using +% taskSequence task. Remember this will not be "Saved" for later use, if you +% want to do controlled experiments use taskSequence to define proper randomised +% and balanced variable sets and triggers to send to recording equipment etc... +% Good for training tasks, or stimulus variability irrelevant to the task. % n = 1; % in(n).name = 'xyPosition'; % in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; % in(n).stimuli = 1; % in(n).offset = []; % stims.stimulusTable = in; -stims.choice = []; -stims.stimulusTable = []; +stims.choice = []; +stims.stimulusTable = []; -%================================================================== -%-------------allows using arrow keys to control variables?------------- +%========================================================================= +%--------------allows using arrow keys to control variables?-------------- % another option is to enable manual control of a table of variables -% this is useful to probe RF properties or other features while still -% allowing for fixation or other behavioural control. -% Use arrow keys <- -> to control value and up/down to control variable +% in-task. This is useful to dynamically probe RF properties or other +% features while still allowing for fixation or other behavioural control. +% Use arrow keys <- -> to control value and ↑ ↓ to control variable. stims.controlTable = []; stims.tableChoice = 1; -%================================================================== -%this allows us to enable subsets from our stimulus list -% 1 = grating | 2 = fixation cross -stims.stimulusSets = {[2],[1,2]}; +%====================================================================== +% this allows us to enable subsets from our stimulus list +stims.stimulusSets = {[1,2],[1]}; stims.setChoice = 1; -hide(stims); -%================================================================== -% N x 2 cell array of regexpi strings, list to skip the current -> next state's exit functions; for example -% skipExitStates = {'fixate','incorrect|breakfix'}; means that if the currentstate is -% 'fixate' and the next state is either incorrect OR breakfix, then skip the FIXATE exit -% state. Add multiple rows for skipping multiple state's exit states. +%========================================================================= +% N x 2 cell array of regexpi strings, list to skip the current -> next +% state's exit functions; for example skipExitStates = +% {'fixate','incorrect|breakfix'}; means that if the currentstate is +% 'fixate' and the next state is either incorrect OR breakfix, then skip +% the FIXATE exit state. Add multiple rows for skipping multiple state's +% exit states. sM.skipExitStates = {'fixate','incorrect|breakfix'}; -%=================================================================== -%-----------------State Machine State Functions--------------------- -% each cell {array} holds a set of anonymous function handles which are executed by the -% state machine to control the experiment. The state machine can run sets -% at entry, during, to trigger a transition, and at exit. Remember these -% {sets} need to access the objects that are available within the -% runExperiment context (see top of file). You can also add global -% variables/objects then use these. The values entered here are set on -% load, if you want up-to-date values then you need to use methods/function -% wrappers to retrieve/set them. +%========================================================================= +% which stimulus in the list is defined as a saccade target? +stims.fixationChoice = 1; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of anonymous function handles which are +% executed by the state machine to control the experiment. The state +% machine can run sets at entry ['entryFcn'], during ['withinFcn'], to +% trigger a transition jump to another state ['transitionFcn'], and at exit +% ['exitFcn'. Remember these {sets} need to access the objects that are +% available within the runExperiment context (see top of file). You can +% also add global variables/objects then use these. The values entered here +% are set on load, if you want up-to-date values then you need to use +% methods/function wrappers to retrieve/set them. +%========================================================================= %--------------------pause entry pauseEntryFcn = { @()hide(stims); @()drawBackground(s); %blank the subject display - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); @()drawTextNow(s,'PAUSED, press [p] to resume...'); @()disp('PAUSED, press [p] to resume...'); - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawText(eT,'PAUSED, press [P] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', stims.stimulusPositions); @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()stopRecording(eT, true); %stop recording eye position data - @()needFlip(me, false); % no need to flip the PTB screen - @()needEyeSample(me,false); % no need to check eye position + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false, 0); % no need to flip the PTB screen or tracker + @()needEyeSample(me, false); % no need to check eye position }; %--------------------pause exit pauseExitFcn = { - @()disp('Leaving paused state...'); - @()startRecording(eT, true); %start recording eye position data again + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); }; %--------------------prefixation entry prefixEntryFcn = { - @()needFlip(me, true); - @()needEyeSample(me,true); % make sure we start measuring eye position - @()getStimulusPositions(stims,true); %make a struct the eT can use for drawing stim positions - @()hide(stims); + @()needFlip(me, true, 1); % enable the screen and trackerscreen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims); % make a struct eT can use for drawing stim positions + @()hide(stims); % hide all stimuli }; +%--------------------prefixate within prefixFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; +%--------------------prefixate exit prefixExitFcn = { - @()resetFixationHistory(eT); % reset the recent eye position history - @()resetExclusionZones(eT); % reset the exclusion zones on eyetracker + @()resetAll(eT); % reset the recent eye position history @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window - @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker + % send the trial start messages to the eyetracker + @()trackerTrialStart(eT, getTaskIndex(me)); @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - @()startRecording(eT); %start recording eye position data again - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawFixation(eT); % draw the fixation window - @()trackerDrawStimuli(eT,stims.stimulusPositions); %draw location of stimulus on eyelink - @()statusMessage(eT,'Initiate Fixation...'); %status text on the eyelink - @()needEyeSample(me,true); % make sure we start measuring eye position + % you can add any other messages, such as stimulus values as needed, + % e.g. @()trackerMessage(eT,['MSG:ANGLE' num2str(stims{1}.angleOut)]) etc. }; +%============================================================== +%====================================================FIXATION +%============================================================== %fixate entry fixEntryFcn = { @()show(stims{2}); @@ -231,57 +187,69 @@ fixEntryFcn = { %--------------------fix within fixFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); @()draw(stims); %draw stimulus }; %--------------------test we are fixated for a certain length of time inFixFcn = { - @()testSearchHoldFixation(eT,'stimulus','incorrect') + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testSearchHoldFixation(eT,'stimulus','breakfix') }; %--------------------exit fixation phase fixExitFcn = { - @()statusMessage(eT,'Show Stimulus...'); @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); %reset fixation time for stimulus = tS.stimulusFixTime @()show(stims{1}); @()trackerMessage(eT,'END_FIX'); }; +%======================================================== +%========================================================STIMULUS +%======================================================== + %--------------------what to run when we enter the stim presentation state stimEntryFcn = { - @()doSyncTime(me); %EDF sync message + % send an eyeTracker sync message (reset relative time to 0 after next flip) + @()doSyncTime(me); + % send stimulus value strobe (value alreadyset by updateVariables(me) function) @()doStrobe(me,true); }; %--------------------what to run when we are showing stimuli stimFcn = { @()draw(stims); - @()drawPhotoDiode(s,[1 1 1]); + @()drawPhotoDiodeSquare(s,[1 1 1]); @()animate(stims); % animate stimuli for subsequent draw }; -%--------------------test we are maintaining fixation +%-----------------------test we are maintaining fixation maintainFixFcn = { - @()testHoldFixation(eT,'correct','breakfix'); + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testHoldFixation(eT,'correct','incorrect'); }; %--------------------as we exit stim presentation state stimExitFcn = { - @()prepareStrobe(io, 255); + @()setStrobeValue(me, 255); % 255 indicates stimulus OFF @()doStrobe(me, true); }; %--------------------if the subject is correct (small reward) correctEntryFcn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM, 2000, 0.1, 0.1); % correct beep - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Correct! :-)'); - @()stopRecording(eT); - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.CORRECT); % send the end trial messages and other cleanup @()needEyeSample(me,false); % no need to collect eye data until we start the next trial @()hide(stims); @()logRun(me,'CORRECT'); %fprintf current trial info @@ -289,31 +257,27 @@ correctEntryFcn = { %--------------------correct stimulus correctFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %--------------------when we exit the correct state correctExitFcn = { + @()giveReward(rM); % send a reward TTL + @()beep(aM, tS.correctSound); % correct beep @()sendStrobe(io,250); + @()trackerDrawStatus(eT, 'CORRECT! :-)'); + @()needFlipTracker(me, 0); %for operator screen stop flip @()updatePlot(bR, me); %update our behavioural plot @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial @()updateVariables(me); %randomise our stimuli, and set strobe value too @()update(stims); %update our stimuli ready for display - @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions - @()trackerClearScreen(eT); @()checkTaskEnded(me); %check if task is finished - @()drawnow; + @()plot(bR, 1); % actually do our behaviour record drawing }; %--------------------incorrect entry incEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Incorrect! :-('); - @()stopRecording(eT); - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.INCORRECT); % send the end trial messages and other cleanup @()needEyeSample(me,false); @()hide(stims); @()logRun(me,'INCORRECT'); %fprintf current trial info @@ -321,13 +285,7 @@ incEntryFcn = { %--------------------break entry breakEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.BREAKFIX)); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Broke maintain fix! :-('); - @()stopRecording(eT); - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.BREAKFIX); % send the end trial messages and other cleanup @()needEyeSample(me,false); @()hide(stims); @()logRun(me,'BREAKFIX'); %fprintf current trial info @@ -335,11 +293,12 @@ breakEntryFcn = { %--------------------our incorrect stimulus incFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %--------------------incorrect / break exit incExitFcn = { + @()beep(aM,tS.errorSound); @()sendStrobe(io,251); @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() @()resetRun(task); %we randomise the run within this block to make it harder to guess next trial @@ -347,23 +306,25 @@ incExitFcn = { @()update(stims); %update our stimuli ready for display @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions @()checkTaskEnded(me); %check if task is finished - @()drawnow; + @()plot(bR, 1); % actually do our behaviour record drawing }; +%======================================================== +%========================================================EYETRACKER +%======================================================== %--------------------calibration function calibrateFcn = { @()drawBackground(s); %blank the display @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()rstop(io); @()trackerSetup(eT); %enter tracker calibrate/validate setup mode }; %--------------------drift correction function driftFcn = { @()drawBackground(s); %blank the display - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT); % stop recording in eyelink [others ignores this] + @()setOffline(eT); % set eyelink offline [others ignores this] @()driftCorrection(eT) % enter drift correct (only eyelink) }; offsetFcn = { @@ -374,30 +335,42 @@ offsetFcn = { }; -%--------------------debug override +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values %--------------------screenflash flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection %--------------------show 1deg size grid -gridFcn = {@()drawGrid(s)}; +gridFcn = { @()drawGrid(s) }; -%============================================================================== -%----------------------State Machine Table------------------------- +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- % specify our cell array that is read by the stateMachine stateInfoTmp = { 'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- 'pause' 'prefix' inf pauseEntryFcn [] [] pauseExitFcn; 'prefix' 'fixate' 0.5 prefixEntryFcn prefixFcn [] prefixExitFcn; 'fixate' 'incorrect' 5 fixEntryFcn fixFcn inFixFcn fixExitFcn; 'stimulus' 'incorrect' 5 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; -'incorrect' 'timeout' 0.5 incEntryFcn incFcn [] incExitFcn; -'breakfix' 'timeout' 0.5 breakEntryFcn incFcn [] incExitFcn; -'correct' 'prefix' 0.5 correctEntryFcn correctFcn [] correctExitFcn; +'incorrect' 'timeout' 0.1 incEntryFcn incFcn [] incExitFcn; +'breakfix' 'timeout' 0.1 breakEntryFcn incFcn [] incExitFcn; +'correct' 'prefix' 0.1 correctEntryFcn correctFcn [] correctExitFcn; 'timeout' 'prefix' tS.tOut {} {} {} {}; +%--------------------------------------------------------------------------------------------- 'calibrate' 'pause' 0.5 calibrateFcn [] [] []; 'drift' 'pause' 0.5 driftFcn [] [] []; +%--------------------------------------------------------------------------------------------- 'override' 'pause' 0.5 overrideFcn [] [] []; 'flash' 'pause' 0.5 flashFcn [] [] []; 'showgrid' 'pause' 10 [] gridFcn [] []; diff --git a/CoreProtocols/Back-Propagation-Mapping.mat b/CoreProtocols/Back-Propagation-Mapping.mat deleted file mode 100644 index fff06b4d4843384a34fb21deac2c73ba85816d0b..0000000000000000000000000000000000000000 Binary files a/CoreProtocols/Back-Propagation-Mapping.mat and /dev/null differ diff --git a/CoreProtocols/Back_PropagationStateInfo.m b/CoreProtocols/Back_PropagationStateInfo.m new file mode 100644 index 0000000000000000000000000000000000000000..ac6684cfd12566af80af3dab8d1b49dd6bf2a8fd --- /dev/null +++ b/CoreProtocols/Back_PropagationStateInfo.m @@ -0,0 +1,452 @@ +%> BACK-PROPAGATION -- fast full-screen receptive field mapping for many +%> channels. + +%================================================================== +%------------------------General Settings-------------------------- +% These settings are make changing the behaviour of the protocol easier. tS +% is just a struct(), so you can add your own switches or values here and +% use them lower down. Some basic switches like saveData, useTask, +% checkKeysDuringstimulus will influence the runeExperiment.runTask() +% functionality, not just the state machine. Other switches like +% includeErrors are referenced in this state machine file to change with +% functions are added to the state machine states… +tS.name = 'Back-Propagation'; %==name of this protocol +tS.useTask = true; %==use taskSequence (randomises stimulus variables) +tS.rewardTime = 250; %==TTL time in milliseconds +tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. +tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? +tS.askForComments = true; %==UI requestor asks for comments before/after run +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.includeErrors = false; %==do we update the trial number even for incorrect saccade/fixate, if true then we call updateTask for both correct and incorrect, otherwise we only call updateTask() for correct responses +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.tOut = 4; %==if wrong response, how long to time out before next trial +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume + +%================================================================= +%----------------Debug logging to command window------------------ +% uncomment each line to get specific verbose logging from each of these +% components; you can also set verbose in the opticka GUI to enable all of +% these… +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging + +%================================================================== +%-----------------INITIAL Eyetracker Settings---------------------- +% These settings define the initial fixation window and set up for the +% eyetracker. They may be modified during the task (i.e. moving the +% fixation window towards a target, enabling an exclusion window to stop +% the subject entering a specific set of display areas etc.) +% +% IMPORTANT: you need to make sure that the global state time is larger +% than the fixation timers specified here. Each state has a global timer, +% so if the state timer is 5 seconds but your fixation timer is 6 seconds, +% then the state will finish before the fixation time was completed! + +% initial fixation X position in degrees (0° is screen centre) +tS.fixX = 0; +% initial fixation Y position in degrees +tS.fixY = 0; +% time to search and enter fixation window +tS.firstFixInit = 3; +% time to maintain initial fixation within window, can be single value or a +% range to randomise between +tS.firstFixTime = 0.25; +% circular fixation window radius in degrees +tS.firstFixRadius = 2; +% do we forbid eye to enter-exit-reenter fixation window? +tS.strict = true; +% do we add an exclusion zone where subject cannot saccade to... +tS.exclusionZone = []; +% time to fix on the stimulus +tS.stimulusFixTime = 4; +%Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); +%Ensure we don't start with any exclusion zones set up +resetAll(eT); + +%================================================================== +%----WHICH states assigned as correct or break for online plot?---- +%----You need to use regex patterns for the match (doc regexp)----- +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%================================================================== +%--------------randomise stimulus variables every trial?----------- +% if you want to have some randomisation of stimuls variables without using +% taskSequence task (i.e. general training tasks), you can uncomment this +% and runExperiment can use this structure to change e.g. X or Y position, +% size, angle see metaStimulus for more details. Remember this will not be +% "Saved" for later use, if you want to do controlled methods of constants +% experiments use taskSequence to define proper randomised and balanced +% variable sets and triggers to send to recording equipment etc... +% +% stims.choice = []; +% n = 1; +% in(n).name = 'xyPosition'; +% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; +% in(n).stimuli = 1; +% in(n).offset = []; +% stims.stimulusTable = in; +stims.choice = []; +stims.stimulusTable = []; + +%======================================================================= +%-------------allows using arrow keys to control variables?------------- +% another option is to enable manual control of a table of variables +% this is useful to probe RF properties or other features while still +% allowing for fixation or other behavioural control. +% Use arrow keys <- -> to control value and ↑ ↓ to control variable. +stims.controlTable = []; +stims.tableChoice = 1; + +%====================================================================== +%this allows us to enable subsets from our stimulus list +% 1 = grating | 2 = fixation cross +stims.stimulusSets = {[1,2],[1]}; +stims.setChoice = 1; +hide(stims); + +%====================================================================== +% N x 2 cell array of regexpi strings, list to skip the current -> next +% state's exit functions; for example skipExitStates = +% {'fixate','incorrect|breakfix'}; means that if the currentstate is +% 'fixate' and the next state is either incorrect OR breakfix, then skip +% the FIXATE exit state. Add multiple rows for skipping multiple state's +% exit states. +sM.skipExitStates = {'fixate','incorrect|breakfix'}; + +%=================================================================== +%=================================================================== +%=================================================================== +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of anonymous function handles which are +% executed by the state machine to control the experiment. The state +% machine can run sets at entry ['entryFn'], during ['withinFn'], to +% trigger a transition jump to another state ['transitionFn'], and at exit +% ['exitFn'. Remember these {sets} need to access the objects that are +% available within the runExperiment context (see top of file). You can +% also add global variables/objects then use these. The values entered here +% are set on load, if you want up-to-date values then you need to use +% methods/function wrappers to retrieve/set them. +%=================================================================== +%=================================================================== +%=================================================================== + +%======================================================== +%========================================================PAUSE +%======================================================== + +%--------------------pause entry +pauseEntryFn = { + @()hide(stims); + @()drawBackground(s); %blank the subject display + @()drawPhotoDiodeSquare(s,[0 0 0]); %draw black photodiode + @()drawTextNow(s,'PAUSED, press [p] to resume...'); + @()disp('PAUSED, press [p] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', stims.stimulusPositions); + @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()resetAll(eT); % reset all fixation markers to initial state + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false); % no need to flip the PTB screen + @()needEyeSample(me,false); % no need to check eye position +}; + +%--------------------pause exit +pauseExitFn = { + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); +}; + +%======================================================== +%========================================================PREFIXATE +%======================================================== +%--------------------prefixate entry +prefixEntryFn = { + @()needFlip(me, true, 1); + @()needEyeSample(me, true); % make sure we start measuring eye position + @()hide(stims); % hide all stimuli + % update the fixation window to initial values + @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window + % send the trial start messages to the eyetracker + @()trackerTrialStart(eT, getTaskIndex(me)); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + % you can add any other messages, such as stimulus values as needed, + % e.g. @()trackerMessage(eT,['MSG:ANGLE' num2str(stims{1}.angleOut)]) + % draw to the eyetracker display +}; + +%--------------------prefixate within +prefixFn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +prefixExitFn = { + @()trackerMessage(eT,'MSG:Start Fix'); + @()trackerDrawStatus(eT, 'Init Fix...', stims.stimulusPositions, 0); +}; + +%======================================================== +%========================================================FIXATE +%======================================================== +%--------------------fixate entry +fixEntryFn = { + @()show(stims{tS.nStims}); + @()logRun(me,'INITFIX'); +}; + +%--------------------fix within +fixFn = { + @()draw(stims); %draw stimuli + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------test we are fixated for a certain length of time +inFixFn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testSearchHoldFixation(eT,'stimulus','breakfix') +}; + +%--------------------exit fixation phase +fixExitFn = { + % reset fixation timers to maintain fixation for tS.stimulusFixTime seconds + @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); + @()show(stims); % show all stims + @()trackerMessage(eT,'END_FIX'); %eyetracker message saved to data stream +}; + +%======================================================== +%========================================================STIMULUS +%======================================================== + +stimEntryFn = { + % send an eyeTracker sync message (reset relative time to 0 after next flip) + @()doSyncTime(me); + % send stimulus value strobe (value alreadyset by updateVariables(me) function) + @()doStrobe(me,true); +}; + +%--------------------what to run when we are showing stimuli +stimFn = { + @()draw(stims); + @()drawPhotoDiodeSquare(s,[1 1 1]); + @()animate(stims); % animate stimuli for subsequent draw +}; + +%-----------------------test we are maintaining fixation +maintainFixFn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testHoldFixation(eT,'correct','incorrect'); +}; + +%as we exit stim presentation state +stimExitFn = { + @()setStrobeValue(me, 255); % 255 indicates stimulus OFF + @()doStrobe(me, true); +}; + +%======================================================== +%========================================================DECISIONS +%======================================================== + +%========================================================CORRECT +%--------------------if the subject is correct (small reward) +correctEntryFn = { + @()trackerMessage(eT,'END_RT'); %send END_RT message to tracker + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); %send TRIAL_RESULT message to tracker + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()hide(stims); % hide all stims + @()logRun(me,'CORRECT'); % print current trial info +}; + +%--------------------correct stimulus +correctFn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------when we exit the correct state +correctExitFn = { + @()giveReward(rM); % send a reward TTL + @()beep(aM, tS.correctSound); % correct beep + @()trackerDrawStatus(eT, 'CORRECT! :-)'); + @()updatePlot(bR, me); + @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial + @()updateVariables(me); %randomise our stimuli, and set strobe value too + @()update(stims); %update our stimuli ready for display + @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions + @()resetAll(eT); %resets the fixation state timers + @()plot(bR, 1); % actually do our behaviour record drawing +}; + +%========================================================INCORRECT +%--------------------incorrect entry +incEntryFn = { + @()trackerMessage(eT,'END_RT'); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()needEyeSample(me,false); + @()hide(stims); + @()logRun(me,'INCORRECT'); %fprintf current trial info +}; + +%--------------------our incorrect/breakfix stimulus +incFn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------incorrect exit +incExitFn = { + @()beep(aM,tS.errorSound); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); %randomise our stimuli, set strobe value too + @()update(stims); %update our stimuli ready for display + @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions + @()resetAll(eT); %resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; + +%--------------------break entry +breakEntryFn = { + @()edfMessage(eT,'END_RT'); + @()edfMessage(eT,['TRIAL_RESULT ' num2str(tS.BREAKFIX)]); + @()stopRecording(eT); + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()needEyeSample(me,false); + @()sendStrobe(io,252); + @()hide(stims); + @()logRun(me,'BREAKFIX'); %fprintf current trial info +}; + +%--------------------break exit +breakExitFn = { + @()beep(aM,tS.errorSound); + @()trackerDrawStatus(eT,'BREAKFIX! :-(', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); %randomise our stimuli, set strobe value too + @()update(stims); %update our stimuli ready for display + @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions + @()resetAll(eT); %resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; + +%--------------------change functions based on tS settings +% this shows an example of how to use tS options to change the function +% lists run by the state machine. We can prepend or append new functions to +% the cell arrays. +% updateTask = updates task object +% resetRun = randomise current trial within the block +% checkTaskEnded = see if taskSequence has finished +if tS.includeErrors % we want to update our task even if there were errors + incExitFn = [ {@()updatePlot(bR, me); @()updateTask(me,tS.INCORRECT)}; incExitFn ]; %update our taskSequence + breakExitFn = [ {@()updatePlot(bR, me); @()updateTask(me,tS.BREAKFIX)}; breakExitFn ]; %update our taskSequence +else + incExitFn = [ {@()updatePlot(bR, me); @()resetRun(task)}; incExitFn ]; + breakExitFn = [ {@()updatePlot(bR, me); @()resetRun(task)}; breakExitFn ]; +end +if tS.useTask || task.nBlocks > 0 % we are using a task or repeat blocks + correctExitFn = [ correctExitFn; {@()checkTaskEnded(me)} ]; + incExitFn = [ incExitFn; {@()checkTaskEnded(me)} ]; + breakExitFn = [ breakExitFn; {@()checkTaskEnded(me)} ]; +end + +%======================================================== +%========================================================EYETRACKER +%======================================================== +%--------------------calibration function +calibrateFn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()rstop(io); + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; + +%--------------------drift correction function +driftFn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) (only eyelink) +}; +offsetFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; + +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values + +%--------------------screenflash +flashFn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFn = { @()drawGrid(s) }; + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine +stateInfoTmp = { +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFn {} {} pauseExitFn; +%--------------------------------------------------------------------------------------------- +'prefix' 'fixate' 0.5 prefixEntryFn {} {} {}; +'fixate' 'incorrect' 10 fixEntryFn fixFn inFixFn fixExitFn; +'stimulus' 'incorrect' 10 stimEntryFn stimFn maintainFixFn stimExitFn; +'correct' 'prefix' 0.1 correctEntryFn correctFn {} correctExitFn; +'incorrect' 'timeout' 0.1 incEntryFn incFn {} incExitFn; +'breakfix' 'timeout' 0.1 breakEntryFn incFn {} breakExitFn; +'timeout' 'prefix' tS.tOut {} incFn {} {}; +%--------------------------------------------------------------------------------------------- +'calibrate' 'pause' 0.5 calibrateFn {} {} {}; +'drift' 'pause' 0.5 driftFn {} {} {}; +'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFn {} {} {}; +'flash' 'pause' 0.5 flashFn {} {} {}; +'showgrid' 'pause' 10 {} gridFn {} {}; +}; +%--------------------------State Machine Table----------------------------- +%========================================================================== + +disp('=================>> Built state info file <<==================') +disp(stateInfoTmp) +disp('=================>> Built state info file <<=================') +clearvars -regexp '.+Fn$' % clear the cell array Fns in the current workspace diff --git a/CoreProtocols/Back_Propagation_Mapping.mat b/CoreProtocols/Back_Propagation_Mapping.mat new file mode 100644 index 0000000000000000000000000000000000000000..4fb5161368300726b4c463f31a5626e0fc7eeb59 Binary files /dev/null and b/CoreProtocols/Back_Propagation_Mapping.mat differ diff --git a/CoreProtocols/ColourGrating.mat b/CoreProtocols/ColourGrating.mat index 9b4847181c3d109c2dab130315f29668b88327a2..e8ecc284960919d8a033ccdffacb2a88956f783c 100644 Binary files a/CoreProtocols/ColourGrating.mat and b/CoreProtocols/ColourGrating.mat differ diff --git a/CoreProtocols/ColourGratingStateInfo.m b/CoreProtocols/ColourGratingStateInfo.m old mode 100755 new mode 100644 index 49388a086d9761f55abbdcb9bcc67d6968b760a7..348a8ef8920b066b3f03d07e663a56dafdf12625 --- a/CoreProtocols/ColourGratingStateInfo.m +++ b/CoreProtocols/ColourGratingStateInfo.m @@ -28,19 +28,22 @@ % includeErrors are referenced in this state machine file to change with % functions are added to the state machine states… tS.useTask = true; %==use taskSequence (randomised variable task object) -tS.rewardTime = 150; %==TTL time in milliseconds +tS.rewardTime = 250; %==TTL time in milliseconds tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? tS.askForComments = false; %==little UI requestor asks for comments before/after run tS.saveData = true; %==save behavioural and eye movement data? tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use tS.name = 'Colour Grating'; %==name of this protocol tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object -tS.tOut = 0.5; %==if wrong response, how long to time out before next trial +tS.tOut = 2; %==if wrong response, how long to time out before next trial tS.CORRECT = 1; %==the code to send eyetracker for correct trials tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume %================================================================= %----------------Debug logging to command window------------------ @@ -67,108 +70,19 @@ tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials % then the state will finish before the fixation time was completed! tS.fixX = 0; % X position in degrees tS.fixY = 0; % X position in degrees -tS.firstFixInit = 1; % time to search and enter fixation window +tS.firstFixInit = 3; % time to search and enter fixation window tS.firstFixTime = 0.25; % time to maintain fixation within windo tS.firstFixRadius = 2; % radius in degrees tS.strict = true; % do we forbid eye to enter-exit-reenter fixation window? -tS.exclusionZone = []; % do we add an exclusion zone where subject cannot saccade to... -tS.stimulusFixTime = 2; % time to fix on the stimulus -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; - -%================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options -% here they will OVERRIDE the GUI ones; if they are commented then the GUI -% options are used. me.elsettings and me.tobiisettings contain the GUI -% settings you can test if they are empty or not and set them based on -% that: -eT.name = tS.name; -if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? -if tS.saveData; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - if isempty(me.eyetracker.esettings) %==check if GUI settings are empty - eT.sampleRate = 250; %==sampling rate - eT.calibrationStyle = 'HV5'; %==calibration style - eT.calibrationProportion = [0.4 0.4]; %==the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation use 1-9 to show each dot, space to select fix as valid, - % INS key ON EYELINK KEYBOARD to accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; %==calibration target colour - eT.modify.calibrationtargetsize = 2; %==size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; %==width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; %==-1 = use any attachedkeyboard - eT.modify.targetbeep = 1; %==beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - if isempty(me.eyetracker.tsettings) %==check if GUI settings are empty - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - eT.valPositions = [ .5 .5 ]; - end -end - +tS.stimulusFixTime = 1; % time to maintain fixation within windo %Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%Ensure we don't start with any exclusion zones set up -eT.resetExclusionZones(); %================================================================== %----which states assigned as correct or break for online plot?---- bR.correctStateName = "correct"; %use regex for better matching bR.breakStateName = ["breakfix","incorrect"]; -%================================================================== -%--------------randomise stimulus variables every trial?----------- -% if you want to have some randomisation of stimuls variables without -% using taskSequence task, you can uncomment this and runExperiment can -% use this structure to change e.g. X or Y position, size, angle -% see metaStimulus for more details. Remember this will not be "Saved" for -% later use, if you want to do controlled methods of constants experiments -% use taskSequence to define proper randomised and balanced variable -% sets and triggers to send to recording equipment etc... -% -% stims.choice = []; -% n = 1; -% in(n).name = 'xyPosition'; -% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; -% in(n).stimuli = 1; -% in(n).offset = []; -% stims.stimulusTable = in; -stims.choice = []; -stims.stimulusTable = []; - -%================================================================== -%-------------allows using arrow keys to control variables?------------- -% another option is to enable manual control of a table of variables -% this is useful to probe RF properties or other features while still -% allowing for fixation or other behavioural control. -% Use arrow keys <- -> to control value and up/down to control variable -stims.controlTable = []; -stims.tableChoice = 1; - -%================================================================== -%this allows us to enable subsets from our stimulus list -% 1 = grating | 2 = fixation cross -stims.stimulusSets = {[2],[1,2]}; -stims.setChoice = 1; -hide(stims); - %================================================================== % N x 2 cell array of regexpi strings, list to skip the current -> next state's exit functions; for example % skipExitStates = {'fixate','incorrect|breakfix'}; means that if the currentstate is @@ -176,75 +90,89 @@ hide(stims); % state. Add multiple rows for skipping multiple state's exit states. sM.skipExitStates = {'fixate','incorrect|breakfix'}; -%=================================================================== -%-----------------State Machine State Functions--------------------- -% each cell {array} holds a set of anonymous function handles which are executed by the -% state machine to control the experiment. The state machine can run sets -% at entry, during, to trigger a transition, and at exit. Remember these -% {sets} need to access the objects that are available within the -% runExperiment context (see top of file). You can also add global -% variables/objects then use these. The values entered here are set on -% load, if you want up-to-date values then you need to use methods/function -% wrappers to retrieve/set them. +%========================================================================= +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of anonymous function handles which are +% executed by the state machine to control the experiment. The state +% machine can run sets at entry ['entryFcn'], during ['withinFcn'], to +% trigger a transition jump to another state ['transitionFcn'], and at exit +% ['exitFcn'. Remember these {sets} need to access the objects that are +% available within the runExperiment context (see top of file). You can +% also add global variables/objects then use these. The values entered here +% are set on load, if you want up-to-date values then you need to use +% methods/function wrappers to retrieve/set them. +%========================================================================= + +%============================================================== +%========================================================PAUSE +%============================================================== %--------------------pause entry pauseEntryFcn = { - @()hide(stims); - @()drawBackground(s); %blank the subject display - @()drawPhotoDiode(s,[0 0 0]); + @()hide(stims); % hide all stimuli + @()drawBackground(s); % blank the subject display + @()drawPhotoDiodeSquare(s,[0 0 0]); % draw black photodiode @()drawTextNow(s,'PAUSED, press [p] to resume...'); @()disp('PAUSED, press [p] to resume...'); - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawText(eT,'PAUSED, press [P] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', stims.stimulusPositions); @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()resetAll(eT); % reset all fixation markers to initial state @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()stopRecording(eT, true); %stop recording eye position data - @()needFlip(me, false); % no need to flip the PTB screen - @()needEyeSample(me,false); % no need to check eye position + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false, 0); % no need to flip the PTB screen + @()needEyeSample(me, false); % no need to check eye position }; %--------------------pause exit pauseExitFcn = { - @()startRecording(eT, true); %start recording eye position data again -}; - -%==================================================== -%====================================================PREFIXATE -%==================================================== -%--------------------prefixation entry -prefixEntryFcn = { - @()needFlip(me, true); - @()needEyeSample(me,true); % make sure we start measuring eye position - @()getStimulusPositions(stims,true); %make a struct the eT can use for drawing stim positions - @()hide(stims); + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); +}; + +%============================================================== +%====================================================PRE-FIXATION +%============================================================== +%--------------------prefixate entry +prefixEntryFcn = { + @()needFlip(me, true); % enable the screen and trackerscreen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()hide(stims); % hide all stimuli + % update the fixation window to initial values + @()resetFixationHistory(eT); + @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window + @()startRecording(eT); % start eyelink recording for this trial (tobii ignores this) + % tracker messages that define a trial start + @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands + @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure }; +%--------------------prefixate within prefixFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; +%--------------------prefixate exit prefixExitFcn = { - @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window - @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker - @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - @()startRecording(eT); %start recording eye position data again - @()trackerDrawStatus(eT,'Start Fixation...',stims.stimulusPositions); %draw location of stimulus on eyelink + @()trackerDrawStatus(eT,'Start...', stims.stimulusPositions); }; -%==================================================== -%====================================================FIXATE -%==================================================== -%fixate entry -fixEntryFcn = { +%============================================================== +%====================================================FIXATION +%============================================================== +%--------------------fixate entry +fixEntryFcn = { @()show(stims{tS.nStims}); - @()logRun(me,'INITFIX'); %fprintf current trial info to command window + @()logRun(me,'INITFIX'); }; %--------------------fix within fixFcn = { - @()drawPhotoDiode(s,[0 0 0]); - @()draw(stims); %draw stimulus + @()draw(stims); %draw stimuli + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %--------------------test we are fixated for a certain length of time @@ -255,35 +183,32 @@ inFixFcn = { % is returned and the state machine will jump to the correct state, % otherwise 'breakfix' is returned and the state machine will jump to the % breakfix state. If neither condition matches, then the state table below - % defines that after 5 seconds we will switch to the incorrect state. - @()testSearchHoldFixation(eT,'stimulus','incorrect') + % defines that after 15 seconds we will switch to the breakfix state. + @()testSearchHoldFixation(eT,'stimulus','breakfix') }; %--------------------exit fixation phase fixExitFcn = { - @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); %reset fixation time for stimulus = tS.stimulusFixTime - @()show(stims); - @()trackerMessage(eT,'END_FIX'); -}; + @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); + @()show(stims); % show all stims + @()trackerMessage(eT,'END_FIX'); %eyetracker message saved to data stream +}; -%==================================================== -%====================================================STIMULUS -%==================================================== +%======================================================== +%========================================================STIMULUS +%======================================================== -%--------------------what to run when we enter the stim presentation state stimEntryFcn = { % send an eyeTracker sync message (reset relative time to 0 after first flip of this state) @()doSyncTime(me); - % send stimulus value strobe (value set by updateVariables(me) function of previous trial). - % doStrobe(me) correctly times the strobe either before flip - % (vpixx/display++) or after flip (LabJack etc.) + % send stimulus value strobe (value set by updateVariables(me) function) @()doStrobe(me,true); }; %--------------------what to run when we are showing stimuli stimFcn = { @()draw(stims); - @()drawPhotoDiode(s,[1 1 1]); + @()drawPhotoDiodeSquare(s,[1 1 1]); @()animate(stims); % animate stimuli for subsequent draw }; @@ -296,12 +221,12 @@ maintainFixFcn = { % otherwise 'breakfix' is returned and the state machine will jump to the % breakfix state. If neither condition matches, then the state table below % defines that after 5 seconds we will switch to the incorrect state. - @()testSearchHoldFixation(eT,'correct','breakfix'); + @()testHoldFixation(eT,'correct','incorrect'); }; -%--------------------as we exit stim presentation state +%as we exit stim presentation state stimExitFcn = { - @()prepareStrobe(io, 255); + @()setStrobeValue(me, 255); % 255 indicates stimulus OFF @()doStrobe(me, true); }; @@ -312,8 +237,8 @@ stimExitFcn = { %====================================================CORRECT %--------------------if the subject is correct (small reward) correctEntryFcn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM, 2000, 0.1, 0.1); % correct beep + @()giveReward(rM); % send a reward TTL + @()beep(aM, tS.correctSound); % correct beep @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); @()trackerDrawStatus(eT,'Correct! :-)',stims.stimulusPositions); @@ -326,7 +251,7 @@ correctEntryFcn = { %--------------------correct stimulus correctFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %--------------------when we exit the correct state @@ -337,9 +262,8 @@ correctExitFcn = { @()updateVariables(me); %randomise our stimuli, and set strobe value too for next trial @()update(stims); %update our stimuli ready for display on next trial @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions for next trial - @()resetFixation(eT); %resets the fixation state timers - @()resetFixationHistory(eT); % reset the recent eye position history - @()resetExclusionZones(eT); % reset the exclusion zones on eyetracker + @()trackerClearScreen(eT); + @()resetAll(eT); % resets the fixation state timers @()checkTaskEnded(me); %check if task is finished @()plot(bR, 1); % actually do our behaviour record drawing }; @@ -347,7 +271,6 @@ correctExitFcn = { %====================================================INCORRECT/BREAKFIX %--------------------incorrect entry incEntryFcn = { - @()beep(aM,400,0.5,1); @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); @()trackerDrawStatus(eT,'Incorrect! :-(',stims.stimulusPositions); @@ -360,7 +283,6 @@ incEntryFcn = { %--------------------break entry breakEntryFcn = { - @()beep(aM,400,0.5,1); @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.BREAKFIX)); @()trackerDrawStatus(eT,'Broke Fixation! :-(',stims.stimulusPositions); @@ -373,32 +295,33 @@ breakEntryFcn = { %--------------------our incorrect stimulus incFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %--------------------incorrect / break exit incExitFcn = { + @()beep(aM, tS.errorSound); @()sendStrobe(io,251); @()updatePlot(bR, me); % update our behavioural plot; @()resetRun(task); % we randomise the run within this block to make it harder to guess next trial @()updateVariables(me, [], true); % randomise our stimuli, force override using true, set strobe value too @()update(stims); % update our stimuli ready for display - @()resetFixation(eT); % resets the fixation state timers - @()resetFixationHistory(eT); % reset the recent eye position history - @()resetExclusionZones(eT); % reset the exclusion zones on eyetracker @()getStimulusPositions(stims); % make a struct the eT can use for drawing stim positions + @()trackerClearScreen(eT); + @()resetAll(eT); % resets the fixation state timers @()checkTaskEnded(me); % check if task is finished - @()needFlip(me, false); + @()needFlip(me, false, 0); @()plot(bR, 1); % actually do our behaviour record drawing }; -%====================================================EYETRACKER +%======================================================== +%========================================================EYETRACKER +%======================================================== %--------------------calibration function calibrateFcn = { - @()drawBackground(s); % blank the display + @()drawBackground(s); %blank the display @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()rstop(io); @()trackerSetup(eT); %enter tracker calibrate/validate setup mode }; @@ -435,16 +358,19 @@ stateInfoTmp = { 'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; %--------------------------------------------------------------------------------------------- 'pause' 'prefix' inf pauseEntryFcn [] [] pauseExitFcn; +%--------------------------------------------------------------------------------------------- 'prefix' 'fixate' 1 prefixEntryFcn prefixFcn [] prefixExitFcn; -'fixate' 'incorrect' 5 fixEntryFcn fixFcn inFixFcn fixExitFcn; -'stimulus' 'incorrect' 5 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; -'incorrect' 'timeout' 0.5 incEntryFcn incFcn [] incExitFcn; -'breakfix' 'timeout' 0.5 breakEntryFcn incFcn [] incExitFcn; -'correct' 'prefix' 0.5 correctEntryFcn correctFcn [] correctExitFcn; +'fixate' 'breakfix' 15 fixEntryFcn fixFcn inFixFcn fixExitFcn; +'stimulus' 'incorrect' 15 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; +'incorrect' 'timeout' 0.1 incEntryFcn incFcn [] incExitFcn; +'breakfix' 'timeout' 0.1 breakEntryFcn incFcn [] incExitFcn; +'correct' 'prefix' 0.1 correctEntryFcn correctFcn [] correctExitFcn; 'timeout' 'prefix' tS.tOut [] [] [] []; +%--------------------------------------------------------------------------------------------- 'calibrate' 'pause' 0.5 calibrateFcn [] [] []; 'drift' 'pause' 0.5 driftFcn [] [] []; 'override' 'pause' 0.5 overrideFcn [] [] []; +%--------------------------------------------------------------------------------------------- 'flash' 'pause' 0.5 flashFcn [] [] []; 'showgrid' 'pause' 10 [] gridFcn [] []; }; diff --git a/CoreProtocols/DotColourStateInfo.m b/CoreProtocols/DotColourStateInfo.m index 3393afb0620e0848496a8e0276df7362035fd902..f6ad58e623f75aa21ac29b1f54bc0fc81c36abc5 100644 --- a/CoreProtocols/DotColourStateInfo.m +++ b/CoreProtocols/DotColourStateInfo.m @@ -162,7 +162,7 @@ prefixEntryFcn = { prefixFcn = { @()drawBackground(s); - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; prefixExitFcn = { @@ -188,7 +188,7 @@ fixEntryFcn = { %fix within fixFcn = { @()draw(stims); %draw stimulus - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %test we are fixated for a certain length of time @@ -213,7 +213,7 @@ stimEntryFcn = { %what to run when we are showing stimuli stimFcn = { @()draw(stims); - @()drawPhotoDiode(s,[1 1 1]); + @()drawPhotoDiodeSquare(s,[1 1 1]); @()animate(stims); % animate stimuli for subsequent draw }; @@ -230,7 +230,7 @@ stimExitFcn = { %if the subject is correct (small reward) correctEntryFcn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL + @()giveReward(rM); % send a reward TTL @()beep(aM,2000); % correct beep @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); @@ -245,7 +245,7 @@ correctEntryFcn = { %correct stimulus correctFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %when we exit the correct state @@ -262,7 +262,7 @@ correctExitFcn = { %incorrect entry incEntryFcn = { - @()beep(aM,400,0.5,1); + @()beep(aM,tS.errorSound); @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); @()trackerClearScreen(eT); @@ -277,7 +277,7 @@ incEntryFcn = { %our incorrect stimulus incFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %incorrect / break exit @@ -292,7 +292,7 @@ incExitFcn = { %break entry breakEntryFcn = { - @()beep(aM,400,0.5,1); + @()beep(aM,tS.errorSound); @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.BREAKFIX)); @()trackerClearScreen(eT); diff --git a/CoreProtocols/DotDirectionStateInfo.m b/CoreProtocols/DotDirectionStateInfo.m index 518cc8c5f3501330a600ee60ffd4425f28f90577..6f073092da6544a3adb5c4e7ea22d74c559c2382 100644 --- a/CoreProtocols/DotDirectionStateInfo.m +++ b/CoreProtocols/DotDirectionStateInfo.m @@ -46,32 +46,7 @@ tS.firstFixTime = 0.5; % time to maintain fixation within windo tS.firstFixRadius = 2; % radius in degrees tS.strict = true; % do we forbid eye to enter-exit-reenter fixation window? tS.exclusionZone = []; % do we add an exclusion zone where subject cannot saccade to... -tS.stimulusFixTime = 2; % time to fix on the stimulus -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; - -%================================================================== -%---------------------------Eyelink setup-------------------------- -eT.name = tS.name; -eT.sampleRate = 250; % sampling rate -eT.calibrationStyle = 'HV5'; % calibration style -eT.calibrationProportion = [0.25 0.25]; %the proportion of the screen occupied by the calibration stimuli -if tS.saveData == true; eT.recordData = true; end %===save EDF file? -if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? -%----------------------- -% remote calibration enables manual control and selection of each fixation -% this is useful for a baby or monkey who has not been trained for fixation -% use 1-9 to show each dot, space to select fix as valid, INS key ON EYELINK KEYBOARD to -% accept calibration! -eT.remoteCalibration = false; -%----------------------- -eT.modify.calibrationtargetcolour = [1 1 1]; % calibration target colour -eT.modify.calibrationtargetsize = 2; % size of calibration target as percentage of screen -eT.modify.calibrationtargetwidth = 0.15; % width of calibration target's border as percentage of screen -eT.modify.waitformodereadytime = 500; -eT.modify.devicenumber = -1; % -1 = use any attachedkeyboard -eT.modify.targetbeep = 1; % beep during calibration -%Initialise the eyeLink object with X, Y, FixInitTime, FixTime, Radius, StrictFix +tS.stimulusFixTime = 1; % time to fix on the stimulus eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); %================================================================== @@ -155,27 +130,26 @@ pauseExitFcn = { }; prefixEntryFcn = { - @()needFlip(me, true); - @()startRecording(eT); %start recording eye position data again + @()needFlip(me, true, 1); % enable the screen and trackerscreen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims); % make a struct eT can use for drawing stim positions + @()hide(stims); % hide all stimuli + % update the fixation window to initial values + @()resetFixationHistory(eT); % reset the recent eye position history + @()resetExclusionZones(eT); % reset the exclusion zones on eyetracker + @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window + % send the trial start messages to the eyetracker + @()trackerTrialStart(eT, getTaskIndex(me)); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure }; prefixFcn = { @()drawBackground(s); - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; prefixExitFcn = { - @()resetFixationHistory(eT); % reset the recent eye position history - @()resetExclusionZones(eT); % reset the exclusion zones on eyetracker - @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window - @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker - @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawFixation(eT); % draw the fixation window - @()trackerDrawStimuli(eT,stims.stimulusPositions); %draw location of stimulus on eyelink - @()statusMessage(eT,'Initiate Fixation...'); %status text on the eyelink - @()needEyeSample(me,true); % make sure we start measuring eye position + @()trackerDrawStatus(eT,'Start fix...', stims.stimulusPositions); }; %fixate entry @@ -187,7 +161,7 @@ fixEntryFcn = { %fix within fixFcn = { @()draw(stims); %draw stimulus - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %test we are fixated for a certain length of time @@ -205,14 +179,14 @@ fixExitFcn = { %what to run when we enter the stim presentation state stimEntryFcn = { - @()doSyncTime(me); %EDF sync message - @()doStrobe(me,true) + @()doSyncTime(me); %eyetracker sync time=0 message + @()doStrobe(me,true); }; %what to run when we are showing stimuli -stimFcn = { +stimFcn = { @()draw(stims); - @()drawPhotoDiode(s,[1 1 1]); + @()drawPhotoDiodeSquare(s,[1 1 1]); @()animate(stims); % animate stimuli for subsequent draw }; @@ -229,7 +203,7 @@ stimExitFcn = { %if the subject is correct (small reward) correctEntryFcn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL + @()giveReward(rM); % send a reward TTL @()beep(aM,2000); % correct beep @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,'TRIAL_RESULT 1'); @@ -244,7 +218,7 @@ correctEntryFcn = { %correct stimulus correctFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %when we exit the correct state @@ -252,7 +226,6 @@ correctExitFcn = { @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial @()updateVariables(me); %randomise our stimuli, and set strobe value too @()update(stims); %update our stimuli ready for display - @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions @()drawTimedSpot(s, 0.5, [0 1 0 1], 0.2, true); %reset the timer on the green spot @()updatePlot(bR, me); %update our behavioural plot @()drawnow; @@ -261,7 +234,7 @@ correctExitFcn = { %incorrect entry incEntryFcn = { - @()beep(aM,400,0.5,1); + @()beep(aM,tS.errorSound); @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,'TRIAL_RESULT -5'); @()trackerClearScreen(eT); @@ -277,22 +250,35 @@ incEntryFcn = { %our incorrect stimulus incFcn = { @()drawBackground(s); - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; -%incorrect / break exit -incExitFcn = { - @()updateVariables(me,[],[],false); %randomise our stimuli, don't run updateTask(task), and set strobe value too - @()update(stims); %update our stimuli ready for display - @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions - @()checkTaskEnded(me); %check if task is finished - @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() - @()drawnow; +%--------------------incorrect exit +incExitFcn = { + @()beep(aM, tS.errorSound); + @()logRun(me,'INCORRECT'); %fprintf current trial info + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; +%--------------------break exit +breakExitFcn = { + @()beep(aM, tS.errorSound); + @()logRun(me,'BREAK_FIX'); %fprintf current trial info + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing }; %break entry breakEntryFcn = { - @()beep(aM,400,0.5,1); + @()beep(aM,tS.errorSound); @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,'TRIAL_RESULT -1'); @()trackerClearScreen(eT); diff --git a/CoreProtocols/FigureGroundStateInfo.m b/CoreProtocols/FigureGroundStateInfo.m index f3d3824a684e26595cfb3134fd95e21a2d8ce791..12ddcbd9d469022e6a7cd5fe04cf7e5aaacde14e 100644 --- a/CoreProtocols/FigureGroundStateInfo.m +++ b/CoreProtocols/FigureGroundStateInfo.m @@ -41,6 +41,8 @@ tS.CORRECT = 1; %==the code to send eyetracker for correct trials tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials tS.luminancePedestal = [0.5 0.5 0.5]; %used during training, it sets the clip behind the figure to a different luminance which makes the figure more salient and thus easier to train to. +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume %================================================================== %----------------Debug logging to command window------------------ @@ -54,15 +56,6 @@ tS.luminancePedestal = [0.5 0.5 0.5]; %used during training, it sets the clip %rM.verbose = true; %==print out reward commands for debugging %task.verbose = true; %==print out task info for debugging -%================================================================== -%-----enable the magstimManager which uses FOI2 of the LabJack -if tS.useMagStim - mS = magstimManager('lJ',rM,'defaultTTL',2); - mS.stimulateTime = 240; - mS.frequency = 0.7; - mS.rewardTime = 25; - open(mS); -end %================================================================== %-----------------INITIAL Eyetracker Settings---------------------- @@ -71,6 +64,8 @@ end % fixation window towards a target, enabling an exclusion window to stop % the subject entering a specific set of display areas etc.) % +eT.name = tS.name; +if tS.saveData == true; eT.recordData = true; end %===save ET data? % initial fixation X position in degrees (0° is screen centre) tS.fixX = 0; % initial fixation Y position in degrees @@ -97,61 +92,10 @@ tS.targetFixInit = 1; tS.targetFixTime = [0.5 0.7]; tS.targetRadius = 5; -%================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options -% here they will OVERRIDE the GUI ones; if they are commented then the GUI -% options are used. me.elsettings and me.tobiisettings contain the GUI -% settings you can test if they are empty or not and set them based on -% that... -eT.name = tS.name; -if tS.saveData == true; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - eT.name = tS.name; - if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? - if tS.saveData == true; eT.recordData = true; end %===save EDF file? - if isempty(me.eyetracker.esettings) %==check if GUI settings are empty - eT.sampleRate = 250; %==sampling rate - eT.calibrationStyle = 'HV5'; %==calibration style - eT.calibrationProportion = [0.4 0.4]; %==the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation use 1-9 to show each dot, space to select fix as valid, - % INS key ON EYELINK KEYBOARD to accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; %==calibration target colour - eT.modify.calibrationtargetsize = 2; %==size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; %==width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; %==-1 = use any attachedkeyboard - eT.modify.targetbeep = 1; %==beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - eT.name = tS.name; - if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? - if isempty(me.eyetracker.tsettings) %==check if GUI settings are empty - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - eT.valPositions = [ .5 .5 ]; - end -end - %Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); %Ensure we don't start with any exclusion zones set up -eT.resetExclusionZones(); +resetAll(eT); %================================================================== %----WHICH states assigned as correct or break for online plot?---- @@ -270,7 +214,7 @@ fixEntryFcn = { %--------------------fix within fixFcn = { @()draw(stims); - @()drawPhotoDiode(s,[0 0 0]) + @()drawPhotoDiodeSquare(s,[0 0 0]) }; %--------------------test we are fixated for a certain length of time @@ -299,7 +243,7 @@ stimEntryFcn = { %--------------------what to run when we are showing stimuli stimFcn = { @()draw(stims); - @()drawPhotoDiode(s,[1 1 1]); + @()drawPhotoDiodeSquare(s,[1 1 1]); @()animate(stims); % animate stimuli for subsequent draw }; @@ -357,7 +301,7 @@ incEntryFcn = { % send END_RT message to eyetracker @()trackerMessage(eT,'END_RT'); @()trackerDrawText(eT,'Incorrect! :-('); - @()beep(aM,400,0.5,1); + @()beep(aM,tS.errorSound); % hide fixation spot @()hide(stims{4}); @()needEyeSample(me,false); @@ -386,7 +330,7 @@ incExitFcn = { breakEntryFcn = { @()trackerMessage(eT,'END_RT'); @()trackerDrawText(eT,'Broke Fixation!'); - @()beep(aM,400,0.5,1); + @()beep(aM,tS.errorSound); @()hide(stims{4}); @()needEyeSample(me,false); @()logRun(me,'BREAKFIX'); %fprintf current trial info diff --git a/CoreProtocols/FixationOnly.m b/CoreProtocols/FixationOnly.m index a86c4e8da774c63e5e32858dd1d651a0d887e0fd..19cf13159bdded5db719620b65af9a7c943d2f78 100644 --- a/CoreProtocols/FixationOnly.m +++ b/CoreProtocols/FixationOnly.m @@ -25,15 +25,15 @@ %================================================================== %------------General Settings----------------- -tS.useTask = false; %==use taskSequence (randomised variable task object) +tS.name = 'Fixation-only Task'; %==name of this protocol +tS.useTask = false; %==use taskSequence (randomised variable task object) tS.rewardTime = 250; %==TTL time in milliseconds tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. -tS.keyExclusionPattern = ["stimulus"]; %==which states to skip keyboard checking +tS.keyExclusionPattern = []; %==which states to skip keyboard checking tS.recordEyePosition = false; %==record eye position within PTB, **in addition** to the EDF? tS.askForComments = false; %==little UI requestor asks for comments before/after run -tS.saveData = false; %==save behavioural and eye movement data? +tS.saveData = false; %==save behavioural and eye movement data? tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… -tS.name = 'fixation'; %==name of this protocol tS.nStims = stims.n; %==number of stimuli tS.tOut = 5; %==if wrong response, how long to time out before next trial tS.CORRECT = 1; %==the code to send eyetracker for correct trials @@ -59,61 +59,18 @@ tS.fixY = 0; % X position in degrees tS.firstFixInit = 3; % time to search and enter fixation window tS.firstFixTime = [0.4 0.8];% time to maintain fixation within window tS.firstFixRadius = 2; % radius in degrees -tS.strict = false; % do we forbid eye to enter-exit-reenter fixation window? -tS.exclusionZone = []; % do we add an exclusion zone where subject cannot saccade to... -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; +tS.strict = false; % do we forbid [true] eye to enter-exit-reenter fixation window? -%================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options -% here they will OVERRIDE the GUI ones; if they are commented then the GUI -% options are used. me.elsettings and me.tobiisettings contain the GUI -% settings you can test if they are empty or not and set them based on -% that... +%========================================================================= +%-------------------------------Eyetracker setup-------------------------- +% NOTE: the opticka GUI can set eyetracker options too; me.eyetracker.esettings +% and me.eyetracker.tsettings contain the GUI settings. We test if they are +% empty or not and set general values based on that... eT.name = tS.name; if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? -if tS.saveData; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - if isempty(me.eyetracker.esettings) %==check if GUI settings are empty - eT.sampleRate = 250; %==sampling rate - eT.calibrationStyle = 'HV5'; %==calibration style - eT.calibrationProportion = [0.4 0.4]; %==the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation use 1-9 to show each dot, space to select fix as valid, - % INS key ON EYELINK KEYBOARD to accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; %==calibration target colour - eT.modify.calibrationtargetsize = 2; %==size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; %==width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; %==-1 = use any attachedkeyboard - eT.modify.targetbeep = 1; %==beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - if isempty(me.eyetracker.tsettings) %==check if GUI settings are empty - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - eT.valPositions = [ .5 .5 ]; - end -end +if tS.saveData; eT.recordData = true; end %===save ET data? %Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%Ensure we don't start with any exclusion zones set up -resetAll(eT); %================================================================== %----WHICH states assigned as correct or break for online plot?---- @@ -131,10 +88,11 @@ bR.breakStateName = ["breakfix","incorrect"]; % use taskSequence to define proper randomised and balanced variable % sets and triggers to send to recording equipment etc... % +d = 6; stims.choice = []; n = 1; in(n).name = 'xyPosition'; -in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; +in(n).values = [d d; d -d; -d d; -d -d; -d 0; d 0]; in(n).stimuli = 1; in(n).offset = []; stims.stimulusTable = in; @@ -175,7 +133,6 @@ hide(stims); %reward. Also which stimulus to set an exclusion zone around (where a %saccade into this area causes an immediate break fixation). stims.fixationChoice = 1; -stims.exclusionChoice = []; %=================================================================== %-----------------State Machine State Functions--------------------- @@ -190,20 +147,21 @@ stims.exclusionChoice = []; %--------------------enter pause state pauseEntryFcn = { + @()hide(stims); @()drawBackground(s); %blank the subject display @()drawTextNow(s,'PAUSED, press [p] to resume...'); @()disp('PAUSED, press [p] to resume...'); @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', stims.stimulusPositions); @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()stopRecording(eT, true); %stop recording eye position data - @()needFlip(me, false); % no need to flip the PTB screen - @()needEyeSample(me,false); % no need to check eye position - @()hide(stims); -}; + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false, 0); % no need to flip the PTB screen + @()needEyeSample(me, false); % no need to check eye position + }; %--------------------pause exit pauseExitFcn = { + @()fprintf('\n===>>>EXIT PAUSE STATE\n') %start recording eye position data again, note true is required here as %the eyelink is started and stopped on each trial, but the tobii runs %continuously, so @()startRecording(eT) only affects eyelink but @@ -212,28 +170,28 @@ pauseExitFcn = { }; %======================================================== -%========================================================PREFIXATE +%========================================================BLANK %======================================================== %prestim entry blEntryFcn = { - @()needFlip(me, true); % start PTB screen flips + @()needFlip(me, true, 1); % start PTB screen flips @()needEyeSample(me, true); % make sure we start measuring eye position - @()resetAll(eT); @()startRecording(eT); - % the fixation cross is moving around, so we need to find its current - % position and update the fixation window - @()updateFixationTarget(me, true, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius); @()update(stims); - @()trackerDrawStatus(eT,'Fixation Only Trial', stims.stimulusPositions); + @()updateFixationTarget(me, true, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius); + @()resetAll(eT); + @()trackerDrawStatus(eT,'Fixation Only Trial'); @()logRun(me,'PRESTIM'); %fprintf current trial info }; %prestimulus blank -blFcn = { }; +blFcn = { + @()trackerDrawFixation(eT); + @()trackerDrawEyePosition(eT); % draw the fixation position on the eyetracker +}; %exiting prestimulus state blExitFcn = { - @()needFlipTracker(me, 1); % set tobii operator screen to flip and not clear @()show(stims); }; @@ -245,8 +203,8 @@ stimEntryFcn = { %what to run when we are showing stimuli stimFcn = { @()draw(stims); - @()drawEyePosition(eT); % this shows the eye position on tobii @()animate(stims); % animate stimuli for subsequent draw + @()trackerDrawEyePosition(eT); % this shows the eye position on tobii }; %test we are maintaining fixation @@ -256,63 +214,70 @@ maintainFixFcn = { %as we exit stim presentation state stimExitFcn = { - @()hide(stims); + }; %if the subject is correct (small reward) correctEntryFcn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM,2000,0.1,0.1); % correct beep - @()trackerDrawStatus(eT,'CORRECT! :-)', stims.stimulusPositions, 0); + @()giveReward(rM); % send a reward TTL + @()beep(aM, tS.correctSound); % correct beep + @()trackerDrawStatus(eT,'CORRECT! :-)', stims.stimulusPositions); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()needEyeSample(me,false); + @()hide(stims); @()logRun(me,'CORRECT'); %fprintf current trial info }; %correct stimulus correctFcn = { - @()drawBackground(s); + }; %break entry breakEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerDrawStatus(eT,'BREAKFIX! :-(', stims.stimulusPositions, 0); + @()beep(aM,tS.errorSound); + @()trackerDrawStatus(eT,'BREAK! :-(', stims.stimulusPositions); + @()needFlipTracker(me, 0); %for tobii stop flip @()logRun(me,'BREAK'); %fprintf current trial info }; %incorrect entry inEntryFcn = { - @()beep(aM,400,0.5,1); + @()beep(aM,tS.errorSound); @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for tobii stop flip @()logRun(me,'INCORRECT'); %fprintf current trial info }; %our incorrect stimulus breakFcn = { @()drawBackground(s); + @()trackerDrawStatus(eT,'BREAKFIX! :-(', stims.stimulusPositions, 0); + }; %when we exit the breakfix/incorrect state ExitFcn = { @()updatePlot(bR, me); %update our behavioural plot @()needEyeSample(me,false); + @()needFlip(me, false, 0); @()randomise(stims); %uses stimulusTable to give new values to variables (not saved in data, used for training) + @()getStimulusPositions(stims); % make a struct the eT can use for drawing stim positions @()plot(bR, 1); % actually do our behaviour record drawing + @()checkTaskEnded(me); % check the trial / block # and if met stop the task }; +%======================================================== +%========================================================EYETRACKER +%======================================================== %--------------------calibration function calibrateFcn = { @()drawBackground(s); %blank the display @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()trackerSetup(eT) % enter tracker calibrate/validate setup mode -}; - -%--------------------drift offset function -offsetFcn = { - @()drawBackground(s); %blank the display - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()driftOffset(eT) % enter tracker offset + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode }; %--------------------drift correction function @@ -329,22 +294,17 @@ offsetFcn = { @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) }; -%--------------------screenflash -flashFcn = { - @()drawBackground(s); - @()flashScreen(s, 0.2); % fullscreen flash mode for visual background activity detection -}; +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values -%----------------------allow override -overrideFcn = { - @()keyOverride(me); -}; +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection -%----------------------show 1deg size grid -gridFcn = { - @()drawGrid(s); - @()drawScreenCenter(s); -}; +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; %============================================================================== %----------------------State Machine Table------------------------- @@ -355,7 +315,7 @@ stateInfoTmp = { 'pause' 'blank' inf pauseEntryFcn {} {} pauseExitFcn; %--------------------------------------------------------------------------------------------- 'blank' 'stimulus' 0.5 blEntryFcn blFcn {} blExitFcn; -'stimulus' 'incorrect' 2 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; +'stimulus' 'incorrect' 5 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; 'incorrect' 'timeout' 0.25 inEntryFcn breakFcn {} ExitFcn; 'breakfix' 'timeout' 0.25 breakEntryFcn breakFcn {} ExitFcn; 'correct' 'blank' 0.25 correctEntryFcn correctFcn {} ExitFcn; diff --git a/CoreProtocols/FixationOnly.mat b/CoreProtocols/FixationOnly.mat index fbbec97dac9c9addf242219836864aacd341d468..77ea9d92218bd0fb294be7822047b75c0fe1d02b 100644 Binary files a/CoreProtocols/FixationOnly.mat and b/CoreProtocols/FixationOnly.mat differ diff --git a/CoreProtocols/FixationTraining.mat b/CoreProtocols/FixationTraining.mat index aec5dfea530130ebb9da35110bb4ff89fe7a36a2..eafcca0d6e6212069e4ae91ad5d709d53b3d5b11 100644 Binary files a/CoreProtocols/FixationTraining.mat and b/CoreProtocols/FixationTraining.mat differ diff --git a/CoreProtocols/FixationTrainingAnimated.mat b/CoreProtocols/FixationTrainingAnimated.mat index dd90896c47f8fa2d85e322af77d024808c226279..b96a6c6ca6700820462190e02ed8bc67ace7b45a 100644 Binary files a/CoreProtocols/FixationTrainingAnimated.mat and b/CoreProtocols/FixationTrainingAnimated.mat differ diff --git a/CoreProtocols/FixationTrainingAnimatedStateInfo.m b/CoreProtocols/FixationTrainingAnimatedStateInfo.m index c1199a99f60f0248e7cdb1868890e32aeb3b26db..92fb03e95a0bbb640f8f9526ab900130df640411 100644 --- a/CoreProtocols/FixationTrainingAnimatedStateInfo.m +++ b/CoreProtocols/FixationTrainingAnimatedStateInfo.m @@ -32,18 +32,20 @@ tS.useTask = true; %==use taskSequence (randomises stimulus variables) tS.rewardTime = 250; %==TTL time in milliseconds tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. -tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking -tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? +tS.keyExclusionPattern = []; %==which states to skip keyboard checking +tS.enableTrainingKeys = true; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? tS.askForComments = false; %==UI requestor asks for comments before/after run tS.saveData = true; %==save behavioural and eye movement data? tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use -tS.name = 'animated fixation training'; %==name of this protocol +tS.name = 'fixation training'; %==name of this protocol tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object tS.tOut = 2; %==if wrong response, how long to time out before next trial tS.CORRECT = 1; %==the code to send eyetracker for correct trials tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials -tS.tobiiFlipRate = 15; %==For Tobii how many main flips should trigger and operator screen flip? +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume %================================================================== %----------------Debug logging to command window------------------ @@ -80,61 +82,8 @@ tS.strict = false; % do we add an exclusion zone where subject cannot saccade to... tS.exclusionZone = []; %tS.stimulusFixTime = 0.5; %==time to fix on the stimulus -% historical log of X and Y position, and exclusion zone -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; -me.lastXExclusion = []; -me.lastYExclusion = []; - -%================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options here -% they will OVERRIDE the GUI ones; if they are commented then the GUI options -% are used. runExperiment.elsettings and runExperiment.tobiisettings -% contain the GUI settings; we test if they are empty or not and set -% defaults based on that... -eT.name = tS.name; -if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? -if tS.saveData == true; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - if isempty(me.eyetracker.esettings) - eT.sampleRate = 250; % sampling rate - eT.calibrationStyle = 'HV5'; % calibration style - eT.calibrationProportion = [0.4 0.4]; %the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each fixation - % this is useful for a baby or monkey who has not been trained for fixation - % use 1-9 to show each dot, space to select fix as valid, INS key ON EYELINK KEYBOARD to - % accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; % calibration target colour - eT.modify.calibrationtargetsize = 2; % size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; % width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; % -1 = use any attachedkeyboard - eT.modify.targetbeep = 1; % beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - if isempty(me.eyetracker.tsettings) - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each fixation - % this is useful for a baby or monkey who has not been trained for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5 ]; - eT.valPositions = [ .5 .5 ]; - end -end -%Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix -eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%Ensure we don't start with any exclusion zones set up -eT.resetExclusionZones(); +% Initialise eyetracker with X, Y, FixInitTime, FixTime, Radius, StrictFix values +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); %================================================================== %----WHICH states assigned as correct or break for online plot?---- @@ -142,26 +91,6 @@ eT.resetExclusionZones(); bR.correctStateName = "correct"; bR.breakStateName = ["breakfix","incorrect"]; -%================================================================== -%--------------randomise stimulus variables every trial?----------- -% if you want to have some randomisation of stimuls variables without using -% taskSequence task (i.e. general training tasks), you can uncomment this -% and runExperiment can use this structure to change e.g. X or Y position, -% size, angle see metaStimulus for more details. Remember this will not be -% "Saved" for later use, if you want to do controlled methods of constants -% experiments use taskSequence to define proper randomised and balanced -% variable sets and triggers to send to recording equipment etc... -% -% stims.choice = []; -% n = 1; -% in(n).name = 'xyPosition'; -% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; -% in(n).stimuli = 1; -% in(n).offset = []; -% stims.stimulusTable = in; -stims.choice = []; -stims.stimulusTable = []; - %================================================================== %-------------allows using arrow keys to control variables?------------- % another option is to enable manual control of a table of variables @@ -173,16 +102,6 @@ stims.controlTable(n).variable = 'size'; stims.controlTable(n).delta = 0.5; stims.controlTable(n).stimuli = [1 2]; stims.controlTable(n).limits = [0.5 20]; -n = n + 1; -stims.controlTable(n).variable = 'xPosition'; -stims.controlTable(n).delta = 5; -stims.controlTable(n).stimuli = [1 2]; -stims.controlTable(n).limits = [-20 20]; -n = n + 1; -stims.controlTable(n).variable = 'yPosition'; -stims.controlTable(n).delta = 5; -stims.controlTable(n).stimuli = [1 2]; -stims.controlTable(n).limits = [-20 20]; %================================================================== %this allows us to enable subsets from our stimulus list @@ -233,39 +152,35 @@ pauseEntryFn = { @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF @()setOffline(eT); % set eyelink offline [tobii ignores this] @()stopRecording(eT, true); %stop recording eye position data - @()needFlip(me, false); % no need to flip the PTB screen + @()needFlip(me, false, 0); % no need to flip the PTB screen @()needEyeSample(me,false); % no need to check eye position - @()needFlipTracker(me, -1); %for tobii don't flip }; %--------------------exit pause state pauseExitFn = { @()fprintf('\n===>>>EXIT PAUSE STATE\n') - @()needFlip(me, true); % start PTB screen flips - @()needEyeSample(me,true); % make sure we start measuring eye position @()startRecording(eT, true); % start eyetracker recording for this trial }; %====================================================PRESTIMULUS %---------------------prestim entry psEntryFn = { - @()needFlip(me, true); % start PTB screen flips + @()needFlip(me, true, 1); % ensure PTB screen flips, and tracker screen flip @()needEyeSample(me, true); % make sure we start measuring eye position - @()needFlipTracker(me, 1); % set tobii operator screen to flip and not clear - @()startRecording(eT); % start eyelink recording for this trial [ignored by tobii as it always records] - @()updateFixationTarget(me, tS.useTask, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); - @()resetAll(eT); %reset the fixation counters ready for a new trial + @()startRecording(eT); % start eyelink recording for this trial (ignored by tobii/irec as they always records) + @()updateFixationTarget(me, true); + @()resetAll(eT); %reset all fixation counters and history ready for a new trial @()getStimulusPositions(stims,true); %make a struct the eT can use for drawing stim positions - @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker + @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink-specific commands, ignored by other eyetrackers + @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyetracker start trial marker @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - @()trackerDrawStatus(eT,'Prestim...', stims.stimulusPositions, 0); + @()trackerDrawStatus(eT,'Start...', stims.stimulusPositions); @()logRun(me,'PREFIX'); % log current trial info to command window AND timeLogger }; %---------------------prestimulus blank prestimulusFn = { - @()trackerDrawEyePosition(eT); % draw the fixation window + @()trackerDrawEyePosition(eT); % draw the fixation position on the eyetracker }; %---------------------exiting prestimulus state @@ -276,14 +191,14 @@ psExitFn = { %====================================================TARGET STIMULUS ALONE %---------------------stimulus entry state stimEntryFn = { - @()doSyncTime(me); + @()doSyncTime(me); % send SYNCTIME message to eyetracker after flip }; %---------------------stimulus within state stimFn = { @()draw(stims); % draw the stimuli @()animate(stims); % animate stimuli for subsequent draw - @()trackerDrawEyePosition(eT); % draw the fixation window + @()trackerDrawEyePosition(eT); % draw the fixation position on the eyetracker }; %-----------------------test we are maintaining fixation @@ -300,29 +215,29 @@ maintainFixFn = { %-----------------------as we exit stim presentation state stimExitFn = { - @()trackerMessage(eT,'END_FIX'); % tell EDF we finish fix - @()trackerMessage(eT,'END_RT'); % tell EDF we finish reaction time + @()trackerMessage(eT,'END_FIX'); % tell eyetracker we finish fix + @()trackerMessage(eT,'END_RT'); % tell eyetracker we finish reaction time }; %====================================================DECISION %-----------------------if the subject is correct (small reward) correctEntryFn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM,2000); % correct beep + @()giveReward(rM); % send a reward TTL + @()beep(aM, tS.correctSound); % correct beep @()trackerMessage(eT,['TRIAL_RESULT ' num2str(tS.CORRECT)]); % tell EDF trial was a correct - @()trackerDrawStatus(eT,'CORRECT! :-)', stims.stimulusPositions, 0); - @()needFlipTracker(me, -1); %for tobii stop flip + @()trackerDrawStatus(eT,'CORRECT! :-)', stims.stimulusPositions); + @()needFlipTracker(me, 0); %for operator screen stop flip @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] @()needEyeSample(me,false); % no need to collect eye data until we start the next trial - @()hide(stims); + @()hide(stims); % hide all stimuli @()logRun(me,'CORRECT'); %fprintf current trial info }; %-----------------------correct stimulus correctFn = { - @()drawText(s,'Correct'); % draw text + }; %----------------------when we exit the correct state @@ -337,10 +252,10 @@ correctExitFn = { %----------------------break entry breakEntryFn = { - @()beep(aM,400,0.5,1); + @()beep(aM,tS.errorSound); @()trackerMessage(eT,['TRIAL_RESULT ' num2str(tS.BREAKFIX)]); %trial incorrect message - @()trackerDrawStatus(eT,'BREAK! :-(', stims.stimulusPositions, 0); - @()needFlipTracker(me, -1); %for tobii stop flip + @()trackerDrawStatus(eT,'BREAK! :-(', stims.stimulusPositions); + @()needFlipTracker(me, 0); %for operator screen stop flip @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] @()needEyeSample(me,false); @@ -349,11 +264,11 @@ breakEntryFn = { }; %----------------------inc entry -incEntryFn = { - @()beep(aM,400,0.5,1); +incEntryFn = { + @()beep(aM,tS.errorSound); @()trackerMessage(eT,['TRIAL_RESULT ' num2str(tS.INCORRECT)]); %trial incorrect message - @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); - @()needFlipTracker(me, -1); %for tobii stop flip + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions); + @()needFlipTracker(me, 0); %for operator screen stop flip @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] @()needEyeSample(me,false); @@ -363,8 +278,7 @@ incEntryFn = { %----------------------our incorrect stimulus breakFn = { - @()drawBackground(s); - @()drawText(s,'Wrong'); + }; %----------------------break exit @@ -374,6 +288,7 @@ breakExitFn = { @()updateVariables(me); ... %update the task variables @()update(stims); %update our stimuli ready for display @()checkTaskEnded(me); + @()needFlip(me, false, 0); @()plot(bR, 1); % actually do our behaviour record drawing }; @@ -400,12 +315,6 @@ driftFn = { @()setOffline(eT); % set eyelink offline [tobii ignores this] @()driftCorrection(eT) % enter drift correct (only eyelink) }; -offsetFcn = { - @()drawBackground(s); %blank the display - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) -}; %--------------------screenflash flashFn = { @@ -440,16 +349,18 @@ stateInfoTmp = { 'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; %--------------------------------------------------------------------------------------------- 'pause' 'prestim' inf pauseEntryFn {} {} pauseExitFn; +%--------------------------------------------------------------------------------------------- 'prestim' 'stimulus' 1 psEntryFn prestimulusFn {} psExitFn; 'stimulus' 'incorrect' 5 stimEntryFn stimFn maintainFixFn stimExitFn; 'incorrect' 'timeout' 0.25 incEntryFn breakFn {} breakExitFn; 'breakfix' 'timeout' 0.25 breakEntryFn breakFn {} breakExitFn; 'correct' 'prestim' 0.25 correctEntryFn correctFn {} correctExitFn; 'timeout' 'prestim' tS.tOut {} {} {} {}; +%--------------------------------------------------------------------------------------------- 'calibrate' 'pause' 0.5 calibrateFn {} {} {}; 'offset' 'pause' 0.5 offsetFn {} {} {}; 'drift' 'pause' 0.5 driftFn {} {} {}; -'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- 'flash' 'pause' 0.5 {} flashFn {} {}; 'override' 'pause' 0.5 {} overrideFn {} {}; 'showgrid' 'pause' 1 {} gridFn {} {}; diff --git a/CoreProtocols/FixationTrainingDistractorStateInfo.m b/CoreProtocols/FixationTrainingDistractorStateInfo.m index 5792b2675c66b05aebe304d493f0487a4d7efbc6..42fb80053cb6ab6ba502fc247beabf0c3d621da7 100644 --- a/CoreProtocols/FixationTrainingDistractorStateInfo.m +++ b/CoreProtocols/FixationTrainingDistractorStateInfo.m @@ -1,19 +1,12 @@ -%> FIXATION TRAINING state configuration file +% FIXATION TRAINING state configuration file % -%> This presents a fixation spot with a flashing disk in a loop to train for -%> fixation. There is a distractor that moves around and subject must ignore -%> it. Adjust eyetracker setting values over training to refine behaviour +% This presents a fixation spot with a flashing disk in a loop to train for +% fixation. There is a distractor that moves around and subject must ignore +% it. Adjust eyetracker setting values over training to refine behaviour % % -% State files control the logic of a behavioural task, switching between -% states and executing functions on ENTER, WITHIN and on EXIT of states. In -% addition there are TRANSITION function sets which can test things like -% eye position to conditionally jump to another state. This state control -% file will usually be run in the scope of the calling -% runExperiment.runTask() method and other objects will be available at run -% time (with easy to use names listed below). The following class objects -% are already loaded by runTask() and available to use; each object has -% methods (functions) useful for running the task: +% The following class objects are already loaded by runTask() and available to +% use; each object has methods (functions) useful for running the task: % % me = runExperiment object ('self' in OOP terminology) % s = screenManager object @@ -25,6 +18,7 @@ % io = digital I/O to recording system % rM = Reward Manager (LabJack or Arduino TTL trigger to reward system/Magstim) % bR = behavioural record plot (on-screen GUI during a task run) +% uF = user defined functions % tS = structure to hold general variables, will be saved as part of the data %================================================================== @@ -32,16 +26,18 @@ tS.useTask = true; %==use taskSequence (randomises stimulus variables) tS.rewardTime = 250; %==TTL time in milliseconds tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. -tS.checkKeysDuringStimulus = true; %==allow keyboard control within stimulus state? Slight drop in performance… +tS.keyExclusionPattern = []; %==which states to skip keyboard checking tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? tS.askForComments = false; %==UI requestor asks for comments before/after run -tS.saveData = false; %==save behavioural and eye movement data? +tS.saveData = true; %==save behavioural and eye movement data? tS.name = 'fixation+distractor'; %==name of this protocol tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object tS.tOut = 5; %==if wrong response, how long to time out before next trial tS.CORRECT = 1; %==the code to send eyetracker for correct trials tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume %================================================================== %----------------Debug logging to command window------------------ @@ -65,61 +61,26 @@ tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials tS.fixX = 0; % X position in degrees tS.fixY = 0; % X position in degrees tS.firstFixInit = 4; % time to search and enter fixation window -tS.firstFixTime = 2; % time to maintain fixation within windo +tS.firstFixTime = [0.3 0.5]; % time to maintain fixation within windo tS.firstFixRadius = 2; % radius in degrees tS.strict = true; %do we forbid eye to enter-exit-reenter fixation window? tS.exclusionZone = []; %do we add an exclusion zone where subject cannot saccade to... me.lastXPosition = tS.fixX; me.lastYPosition = tS.fixY; +%================================================================== %================================================================== %---------------------------Eyetracker setup----------------------- % NOTE: the opticka GUI can set eyetracker options too, if you set options here % they will OVERRIDE the GUI ones; if they are commented then the GUI options % are used. runExperiment.elsettings and runExperiment.tobiisettings -% contain the GUI settings you can test if they are empty or not and set -% them based on that... +% contain the GUI settings; we test if they are empty or not and set +% defaults based on that... eT.name = tS.name; -if tS.saveData == true; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - if isempty(me.eyetracker.esettings) - eT.sampleRate = 250; % sampling rate - eT.calibrationStyle = 'HV5'; % calibration style - eT.calibrationProportion = [0.4 0.4]; %the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each fixation - % this is useful for a baby or monkey who has not been trained for fixation - % use 1-9 to show each dot, space to select fix as valid, INS key ON EYELINK KEYBOARD to - % accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; % calibration target colour - eT.modify.calibrationtargetsize = 2; % size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; % width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; % -1 = use any attachedkeyboard - eT.modify.targetbeep = 1; % beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - if isempty(tobiisettings) - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each fixation - % this is useful for a baby or monkey who has not been trained for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - eT.valPositions = [ .5 .5 ]; - end -end +if tS.saveData == true; eT.recordData = true; end %===save ET data? %Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%Ensure we don't start with any exclusion zones set up -eT.resetExclusionZones(); +eT.resetAll(); %================================================================== %----WHICH states assigned as correct or break for online plot?---- @@ -158,7 +119,7 @@ n = 1; stims.controlTable(n).variable = 'size'; stims.controlTable(n).delta = 0.5; stims.controlTable(n).stimuli = [1]; -stims.controlTable(n).limits = [0.5 20]; +stims.controlTable(n).limits = [0.5 15]; n = n + 1; stims.controlTable(n).variable = 'contrast'; stims.controlTable(n).delta = 0.05; @@ -167,7 +128,7 @@ stims.controlTable(n).limits = [0 1]; %================================================================== %this allows us to enable subsets from our stimulus list -stims.stimulusSets = {[1,2,3],3,1}; +stims.stimulusSets = {[1,2,3],3}; stims.setChoice = 1; hide(stims); @@ -210,61 +171,58 @@ sM.skipExitStates = {'fixate','incorrect|breakfix'}; %--------------------enter pause state pauseEntryFcn = { + @()hide(stims); @()drawBackground(s); %blank the subject display @()drawTextNow(s,'PAUSED, press [p] to resume...'); @()disp('PAUSED, press [p] to resume...'); - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawText(eT,'PAUSED, press [P] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [P] to resume...'); @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()needFlip(me, false); % no need to flip the PTB screen + @()stopRecording(eT, true); %stop recording eye position data + @()needFlip(me, false, 0); % no need to flip the PTB screen @()needEyeSample(me,false); % no need to check eye position }; %--------------------exit pause state pauseExitFcn = { - @()startRecording(eT, true); %start recording eye position data again @()fprintf('\n===>>>EXIT PAUSE STATE\n') - @()needFlip(me, true); % start PTB screen flips + @()needFlip(me, true, 1); % start PTB screen flips + @()startRecording(eT, true); % start eyetracker recording for this trial }; %---------------------prestim entry psEntryFcn = { - @()trackerClearScreen(eT); % blank the eyelink screen - @()resetFixation(eT); %reset the fixation counters ready for a new trial - @()resetFixationHistory(eT); %reset the fixation counters ready for a new trial - @()startRecording(eT); % start eyelink recording for this trial - @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker + @()needFlip(me, true, 1); % start PTB screen flips, and tracker screen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()startRecording(eT); % start eyelink recording for this trial (ignored by tobii/irec as they always records) + @()resetAll(eT); %reset all fixation counters and history ready for a new trial + @()getStimulusPositions(stims,true); %make a struct the eT can use for drawing stim positions + @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink-specific commands, ignored by other eyetrackers + @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyetracker start trial marker @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - @()statusMessage(eT,'Pre-fixation...'); %status text on the eyelink - @()trackerDrawFixation(eT); % draw the fixation window - @()needEyeSample(me,true); % make sure we start measuring eye position - @()showSet(stims, 1); % make sure we prepare to show the stimulus set - @()logRun(me,'PREFIX'); %fprintf current trial info to command window + @()trackerClearScreen(eT); %draw status to eyetracker display + @()logRun(me,'PREFIX'); % log current trial info to command window AND timeLogger }; %---------------------prestimulus blank prestimulusFcn = { - @()drawBackground(s); % only draw a background colour to the PTB screen + @()trackerDrawFixation(eT); + @()trackerDrawEyePosition(eT); % draw the fixation point }; %---------------------exiting prestimulus state psExitFcn = { - @()statusMessage(eT,'Stimulus...'); % show eyetracker status message + @()show(stims); % make sure we prepare to show the stimulus set }; %---------------------stimulus entry state -stimEntryFcn = { - @()logRun(me,'SHOW Fixation'); % log start to command window -}; +stimEntryFcn = { }; %---------------------stimulus within state stimFcn = { @()draw(stims); % draw the stimuli - @()drawEyePosition(eT); % draw the eye position to PTB screen @()animate(stims); % animate stimuli for subsequent draw + @()trackerDrawEyePosition(eT); % draw the fixation point }; %-----------------------test we are maintaining fixation @@ -285,12 +243,11 @@ stimExitFcn = { %-----------------------if the subject is correct (small reward) correctEntryFcn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM,2000); % correct beep - @()trackerMessage(eT,'TRIAL_RESULT 1'); % tell EDF trial was a correct - @()statusMessage(eT,'Correct! :-)'); %show it on the eyelink screen - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Correct! :-)'); + @()giveReward(rM); % send a reward TTL + @()beep(aM, tS.correctSound); % correct beep + @()trackerMessage(eT,['TRIAL_RESULT ' num2str(tS.CORRECT)]); % tell EDF trial was a correct + @()trackerDrawStatus(eT,'CORRECT! :-)'); + @()needFlipTracker(me, 0); %for operator screen stop flip @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] @()needEyeSample(me,false); % no need to collect eye data until we start the next trial @@ -299,57 +256,57 @@ correctEntryFcn = { %-----------------------correct stimulus correctFcn = { - @()drawBackground(s); % draw background colour - @()drawText(s,'Correct'); % draw text + }; %----------------------when we exit the correct state correctExitFcn = { - @()updateVariables(me,[],[],true); %update the task variables - @()update(stims); %update our stimuli ready for display - @()updateFixationTarget(me, true); % make sure the fixation follows stims.fixationChoice @()updatePlot(bR, me); % update the behavioural report plot - @()drawnow; % ensure we update the figure - @()checkTaskEnded(me); ... %check if task is finished + @()updateVariables(me,[],[],true); ... %update the task variables + @()update(stims); ... %update our stimuli ready for display + @()checkTaskEnded(me); + @()plot(bR, 1); % actually do our behaviour record drawing }; %----------------------break entry breakEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Broke fix! :-('); - @()trackerMessage(eT,'TRIAL_RESULT 0'); %trial incorrect message + @()beep(aM,tS.errorSound); + @()trackerMessage(eT,['TRIAL_RESULT ' num2str(tS.BREAKFIX)]); %trial incorrect message + @()trackerDrawStatus(eT,'BROKE FIX! :-('); + @()needFlipTracker(me, 0); %for operator screen stop flip @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] @()needEyeSample(me,false); + @()hide(stims); @()logRun(me,'BREAKFIX'); %fprintf current trial info }; %----------------------inc entry incEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Incorrect! :-('); - @()trackerMessage(eT,'TRIAL_RESULT 0'); %trial incorrect message + @()beep(aM,tS.errorSound); + @()trackerMessage(eT,['TRIAL_RESULT ' num2str(tS.INCORRECT)]); %trial incorrect message + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions); + @()needFlipTracker(me, 0); %for operator screen stop flip @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] @()needEyeSample(me,false); + @()hide(stims); @()logRun(me,'INCORRECT'); %fprintf current trial info + }; %----------------------our incorrect stimulus breakFcn = { - @()drawBackground(s); - @()drawText(s,'Wrong'); + }; %----------------------break exit breakExitFcn = { - @()update(stims); %update our stimuli ready for display - @()updateFixationTarget(me, true); % make sure the fixation follows stims.fixationChoice @()updatePlot(bR, me); - @()drawnow; - @()checkTaskEnded(me); %check if task is finished + @()updateVariables(me,[],[],false); ... %update the task variables + @()update(stims); %update our stimuli ready for display + @()checkTaskEnded(me); + @()plot(bR, 1); % actually do our behaviour record drawing }; %--------------------calibration function @@ -375,12 +332,6 @@ driftFcn = { @()setOffline(eT); % set eyelink offline [tobii ignores this] @()driftCorrection(eT) % enter drift correct (only eyelink) }; -offsetFcn = { - @()drawBackground(s); %blank the display - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) -}; %--------------------screenflash flashFcn = { @@ -410,18 +361,22 @@ gridFcn = { % transitionFcn = function to test a condition (i.e. fixation) during the state to switch to another state. % exitFcn = {array} of functions to run when leaving a state. %================================================================== -disp('================>> Building state info file <<================') stateInfoTmp = { 'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- 'pause' 'blank' inf pauseEntryFcn [] [] pauseExitFcn; +%--------------------------------------------------------------------------------------------- 'blank' 'stimulus' 0.5 psEntryFcn prestimulusFcn [] psExitFcn; 'stimulus' 'incorrect' 10 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; -'incorrect' 'blank' 2 incEntryFcn breakFcn [] breakExitFcn; -'breakfix' 'blank' 2 breakEntryFcn breakFcn [] breakExitFcn; +'incorrect' 'timeout' 2 incEntryFcn breakFcn [] breakExitFcn; +'breakfix' 'timeout' 2 breakEntryFcn breakFcn [] breakExitFcn; 'correct' 'blank' 0.5 correctEntryFcn correctFcn [] correctExitFcn; +'timeout' 'blank' tS.tOut {} {} {} {}; +%--------------------------------------------------------------------------------------------- 'calibrate' 'pause' 0.5 calibrateFcn [] [] []; 'offset' 'pause' 0.5 offsetFcn [] [] []; 'drift' 'pause' 0.5 driftFcn [] [] []; +%--------------------------------------------------------------------------------------------- 'flash' 'pause' 0.5 [] flashFcn [] []; 'override' 'pause' 0.5 [] overrideFcn [] []; 'showgrid' 'pause' 1 [] gridFcn [] []; @@ -429,9 +384,8 @@ stateInfoTmp = { %----------------------State Machine Table------------------------- %================================================================== +disp('================>> Building state info file <<================') disp(stateInfoTmp) -disp('================>> Loaded state info file <<================') -clear maintainFixFcn prestimulusFcn singleStimulus pauseEntryFcn ... - prestimulusFcn stimFcn stimEntryFcn stimExitfcn correctEntry ... - correctWithin correctExitFcn breakFcn maintainFixFcn psExitFcn ... - incorrectFcn calibrateFcn offsetFcn driftFcn gridFcn overrideFcn flashFcn breakFcn \ No newline at end of file +disp('=================>> Loaded state info file <<=================') + +clearvars -regexp '.+Fcn' \ No newline at end of file diff --git a/CoreProtocols/FixationTrainingStateInfo.m b/CoreProtocols/FixationTrainingStateInfo.m index 0eac403efa2374c17e48b33db7f21f95d43fb9b2..fcfcea5c966b2546303e0816bede7b600f2bcc5e 100644 --- a/CoreProtocols/FixationTrainingStateInfo.m +++ b/CoreProtocols/FixationTrainingStateInfo.m @@ -1,11 +1,13 @@ -%FIXATION TRAINING state configuration file +% FIXATION TRAINING: Protocol presents a pulsing fixation cross with a +% stimulus in a loop to train for fixation. stims should contain 2 stimuli: +% stims{1} is a attention grabber, stims{2} is the fixation cross. Adjust +% stimulus sizes and eyetracker setting values over training to refine +% behaviour. You can use the ↑ ↓ keys to set the variable (size, xPosition, +% yPosition) and ← → to change the value of the variable. You can also use +% keys to change fixation window size, and time to fixate during the +% session. % -% This presents a fixation cross with a stimulus in a loop to train for -% fixation. stims should contain 2 stimuli: stims{1} is a attention -% grabber, stims{2} is the fixation cross. Adjust stimulus sizes and -% eyetracker setting values over training to refine behaviour The following -% class objects are already loaded and available to use: -% +% The following class objects are already loaded and available to use: % % me = runExperiment object ('self' in OOP terminology) % s = screenManager object @@ -17,36 +19,40 @@ % io = digital I/O to recording system % rM = Reward Manager (LabJack or Arduino TTL trigger to reward system/Magstim) % bR = behavioural record plot (on-screen GUI during a task run) -% uF = user defined functions +% uF = user defined functions, see userFunctions.m % tS = structure to hold general variables, will be saved as part of the data %================================================================== %----------------------General Settings---------------------------- -tS.useTask = true; %==use taskSequence (randomises stimulus variables) -tS.rewardTime = 250; %==TTL time in milliseconds +tS.name = 'Fixation Training'; %==name of this protocol +tS.useTask = false; %==use taskSequence (randomises stimulus variables) +tS.rewardTime = 300; %==TTL time in milliseconds tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. -tS.checkKeysDuringStimulus = true; %==allow keyboard control within stimulus state? Slight drop in performance… -tS.recordEyePosition = true; %==record local copy of eye position, **in addition** to the eyetracker? +tS.keyExclusionPattern = []; %==which states to skip keyboard checking +tS.enableTrainingKeys = true; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? tS.askForComments = false; %==UI requestor asks for comments before/after run tS.saveData = true; %==save behavioural and eye movement data? -tS.name = 'fixation training'; %==name of this protocol +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object -tS.tOut = 5; %==if wrong response, how long to time out before next trial +tS.timeOut = 2; %==if wrong response, how long to time out before next trial tS.CORRECT = 1; %==the code to send eyetracker for correct trials tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [350, 1, 1]; %==freq,length,volume %================================================================== -%----------------Debug logging to command window------------------ +%------------ ----DEBUG LOGGING to command window------------------ % uncomment each line to get specific verbose logging from each of these % components; you can also set verbose in the opticka GUI to enable all of % these… -%sM.verbose = true; %==print out stateMachine info for debugging -%stims.verbose = true; %==print out metaStimulus info for debugging -%io.verbose = true; %==print out io commands for debugging -eT.verbose = true; %==print out eyelink commands for debugging -%rM.verbose = true; %==print out reward commands for debugging -%task.verbose = true; %==print out task info for debugging +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging %================================================================== %-----------------INITIAL Eyetracker Settings---------------------- @@ -55,123 +61,62 @@ eT.verbose = true; %==print out eyelink commands for debugging % fixation window towards a target, enabling an exclusion window to stop % the subject entering a specific set of display areas etc.) % -% initial fixation X position in degrees (0° is screen centre) -tS.fixX = 0; -% initial fixation Y position in degrees +% **IMPORTANT**: you must ensure that the global state time is larger than +% any fixation timers specified here. Each state has a global timer, so if +% the state timer is 5 seconds but your fixation timer is 6 seconds, then +% the state will finish before the fixation time was completed! +%------------------------------------------------------------------ +% initial fixation X position in degrees (0° is screen centre). Multiple windows +% can be entered using an array. +tS.fixX = 0; +% initial fixation Y position in degrees (0° is screen centre). Multiple windows +% can be entered using an array. tS.fixY = 0; -% time to search and enter fixation window +% time to search and enter fixation window (Initiate fixation) tS.firstFixInit = 3; -% time to maintain fixation within window, can be single value or a range -% to randomise between -tS.firstFixTime = 0.5; -% circular fixation window radius in degrees -tS.firstFixRadius = 5; +% time to maintain initial fixation within window, can be single value or a +% range to randomise between +tS.firstFixTime = [0.25 0.75]; +% fixation window radius in degrees; if you enter [x y] the window will be +% rectangular. +tS.firstFixRadius = 2; % do we forbid eye to enter-exit-reenter fixation window? -tS.strict = false; -% do we add an exclusion zone where subject cannot saccade to... +tS.strict = true; +% add an exclusion zone where subject cannot saccade to? tS.exclusionZone = []; -%tS.stimulusFixTime = 0.5; %==time to fix on the stimulus -% historical log of X and Y position, and exclusion zone -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; -me.lastXExclusion = []; -me.lastYExclusion = []; +% time to maintain fixation during stimulus state +tS.stimulusFixTime = 1; +% Initialise eyetracker with X, Y, FixInitTime, FixTime, Radius, StrictFix values +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); %================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options here -% they will OVERRIDE the GUI ones; if they are commented then the GUI options -% are used. runExperiment.elsettings and runExperiment.tobiisettings -% contain the GUI settings; we test if they are empty or not and set -% defaults based on that... -eT.name = tS.name; -if tS.saveData == true; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - if isempty(me.eyetracker.esettings) - eT.sampleRate = 250; % sampling rate - eT.calibrationStyle = 'HV5'; % calibration style - eT.calibrationProportion = [0.4 0.4]; %the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each fixation - % this is useful for a baby or monkey who has not been trained for fixation - % use 1-9 to show each dot, space to select fix as valid, INS key ON EYELINK KEYBOARD to - % accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; % calibration target colour - eT.modify.calibrationtargetsize = 2; % size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; % width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; % -1 = use any attachedkeyboard - eT.modify.targetbeep = 1; % beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - if isempty(me.eyetracker.tsettings) - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each fixation - % this is useful for a baby or monkey who has not been trained for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5 ]; - eT.valPositions = [ .5 .5 ]; - end -end -%Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix -eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%Ensure we don't start with any exclusion zones set up -eT.resetExclusionZones(); - -%================================================================== -%----WHICH states assigned as correct or break for online plot?---- -%----You need to use regex patterns for the match (doc regexp)----- +%-----------------BEAVIOURAL PLOT CONFIGURATION-------------------- +%----WHICH states assigned correct / break for the online plot?---- bR.correctStateName = "correct"; bR.breakStateName = ["breakfix","incorrect"]; %================================================================== -%--------------randomise stimulus variables every trial?----------- -% if you want to have some randomisation of stimuls variables without using -% taskSequence task (i.e. general training tasks), you can uncomment this -% and runExperiment can use this structure to change e.g. X or Y position, -% size, angle see metaStimulus for more details. Remember this will not be -% "Saved" for later use, if you want to do controlled methods of constants -% experiments use taskSequence to define proper randomised and balanced -% variable sets and triggers to send to recording equipment etc... -% -% n = 1; -% in(n).name = 'xyPosition'; -% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; -% in(n).stimuli = 1; -% in(n).offset = []; -% stims.stimulusTable = in; -stims.stimulusTable = []; -stims.choice = []; - -%================================================================== -%-------------allows using arrow keys to control variables?------------- -% another option is to enable manual control of a table of variables -% this is useful to probe RF properties or other features while still -% allowing for fixation or other behavioural control. +%--------------Control Stimulus Variable During Task?-------------- +% another option is to enable manual control of a table of variables this +% is useful to probe RF properties or other features while still allowing +% for fixation or other behavioural control. This is also useful for +% training. stims.tableChoice = 1; n = 1; stims.controlTable(n).variable = 'size'; stims.controlTable(n).delta = 0.5; stims.controlTable(n).stimuli = [1 2]; -stims.controlTable(n).limits = [0.5 20]; +stims.controlTable(n).limits = [0.5 10]; n = n + 1; stims.controlTable(n).variable = 'xPosition'; -stims.controlTable(n).delta = 1; +stims.controlTable(n).delta = 3; stims.controlTable(n).stimuli = [1 2]; -stims.controlTable(n).limits = [-15 15]; +stims.controlTable(n).limits = [-18 18]; n = n + 1; stims.controlTable(n).variable = 'yPosition'; -stims.controlTable(n).delta = 1; +stims.controlTable(n).delta = 3; stims.controlTable(n).stimuli = [1 2]; -stims.controlTable(n).limits = [-15 15]; +stims.controlTable(n).limits = [-18 18]; %================================================================== %this allows us to enable subsets from our stimulus list @@ -179,16 +124,7 @@ stims.stimulusSets = {[1,2]}; stims.setChoice = 1; hide(stims); -%================================================================== -% which stimulus in the list is used for a fixation target? For this -% protocol it means the subject must fixate this stimulus (the saccade -% target is #1 in the list) to get the reward. Also which stimulus to set -% an exclusion zone around (where a saccade into this area causes an -% immediate break fixation). -stims.fixationChoice = 2; -stims.exclusionChoice = []; - -%================================================================== +%========================================================================= % N x 2 cell array of regexpi strings, list to skip the current -> next % state's exit functions; for example skipExitStates = % {'fixate','incorrect|breakfix'}; means that if the currentstate is @@ -197,19 +133,16 @@ stims.exclusionChoice = []; % exit states. sM.skipExitStates = {'fixate','incorrect|breakfix'}; +%========================================================================= +% which stimulus in the list is used for a fixation target? For this +% protocol it means the subject must saccade to this stimulus (the saccade +% target is #1 in the list) to get the reward. +stims.fixationChoice = 1; + %=================================================================== %=================================================================== %=================================================================== %-----------------State Machine Task Functions--------------------- -% Each cell {array} holds a set of anonymous function handles which are -% executed by the state machine to control the experiment. The state -% machine can run sets at entry ['entryFcn'], during ['withinFcn'], to -% trigger a transition jump to another state ['transitionFcn'], and at exit -% ['exitFcn'. Remember these {sets} need to access the objects that are -% available within the runExperiment context (see top of file). You can -% also add global variables/objects then use these. The values entered here -% are set on load, if you want up-to-date values then you need to use -% methods/function wrappers to retrieve/set them. %====================================================PAUSE %--------------------enter pause state @@ -218,63 +151,57 @@ pauseEntryFn = { @()drawBackground(s); %blank the subject display @()drawTextNow(s,'PAUSED, press [p] to resume...'); @()disp('PAUSED, press [p] to resume...'); - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawText(eT,'PAUSED, press [P] to resume...'); - @()trackerFlip(eT); %for tobii show info if operator screen enabled + @()trackerDrawStatus(eT,'PAUSED, press [P] to resume...'); @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF @()setOffline(eT); % set eyelink offline [tobii ignores this] @()stopRecording(eT, true); %stop recording eye position data - @()needFlip(me, false); % no need to flip the PTB screen + @()needFlip(me, false, 0); % no need to flip the PTB screen @()needEyeSample(me,false); % no need to check eye position }; %--------------------exit pause state pauseExitFn = { @()fprintf('\n===>>>EXIT PAUSE STATE\n') - @()needFlip(me, true); % start PTB screen flips @()startRecording(eT, true); % start eyetracker recording for this trial }; %---------------------prestim entry psEntryFn = { - @()resetFixation(eT); %reset the fixation counters ready for a new trial - @()resetFixationHistory(eT); %reset the fixation counters ready for a new trial - @()startRecording(eT); % start eyelink recording for this trial - @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker + @()needFlip(me, true, 4); % start PTB screen flips, and tracker screen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()hide(stims); % hide all stimuli + @()getStimulusPositions(stims,true); %make a struct the eT can use for drawing stim positions + @()resetAll(eT); %reset all fixation counters and history ready for a new trial + @()updateFixationTarget(me, true); %move the fixation window to match the current stim position + @()trackerTrialStart(eT, getTaskIndex(me)); @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - @()statusMessage(eT,'Pre-fixation...'); %status text on the eyelink - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawFixation(eT); % draw the fixation window - @()trackerFlip(eT,1); %for tobii show info if operator screen enabled - @()needEyeSample(me,true); % make sure we start measuring eye position - @()logRun(me,'PREFIX'); %fprintf current trial info to command window + @()trackerDrawStatus(eT,'Pre-stimulus...', stims.stimulusPositions); + @()logRun(me,'PREFIX'); % log current trial info to command window AND timeLogger }; %---------------------prestimulus blank prestimulusFn = { - @()drawBackground(s); % only draw a background colour to the PTB screen - @()trackerDrawEyePosition(eT); % draw the fixation window - @()trackerFlip(eT,1); %for tobii show info if operator screen enabled + %@()trackerDrawFixation(eT); + @()trackerDrawEyePosition(eT); % draw the fixation point }; %---------------------exiting prestimulus state psExitFn = { @()show(stims); % make sure we prepare to show the stimulus set - @()statusMessage(eT,'Stimulus...'); % show eyetracker status message }; %---------------------stimulus entry state stimEntryFn = { - @()logRun(me,'SHOW Fixation Spot'); % log start to command window + }; %---------------------stimulus within state stimFn = { @()draw(stims); % draw the stimuli @()animate(stims); % animate stimuli for subsequent draw - @()trackerDrawEyePosition(eT); % draw the fixation window - @()trackerFlip(eT,1); %for tobii show info if operator screen enabled + @()trackerDrawFixation(eT); + @()trackerDrawStimuli(eT, stims.stimulusPositions); + @()trackerDrawEyePosition(eT); % draw the fixation point }; %-----------------------test we are maintaining fixation @@ -291,30 +218,24 @@ maintainFixFn = { %-----------------------as we exit stim presentation state stimExitFn = { - @()trackerMessage(eT,'END_FIX'); % tell EDF we finish fix - @()trackerMessage(eT,'END_RT'); % tell EDF we finish reaction time + @()trackerMessage(eT,'END_FIX'); % tell eyetracker we finish fix + @()trackerMessage(eT,'END_RT'); % tell eyetracker we finish reaction time }; %-----------------------if the subject is correct (small reward) correctEntryFn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM,2000); % correct beep - @()trackerMessage(eT,['TRIAL_RESULT ' num2str(tS.CORRECT)]); % tell EDF trial was a correct - @()statusMessage(eT,'Correct! :-)'); %show it on the eyelink screen - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Correct! :-)'); - @()trackerDrawEyePositions(eT); % draw the fixation window - @()trackerFlip(eT); %for tobii show info if operator screen enabled - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.CORRECT); % send the end trial messages and other cleanup + @()trackerDrawStatus(eT,'CORRECT! :-)', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for operator screen stop flip @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()giveReward(rM); % send a reward + @()beep(aM, tS.correctSound); % correct beep @()logRun(me,'CORRECT'); %fprintf current trial info }; %-----------------------correct stimulus correctFn = { - @()drawBackground(s); % draw background colour - @()drawText(s,'Correct! :-)'); % draw text + }; %----------------------when we exit the correct state @@ -322,39 +243,35 @@ correctExitFn = { @()updatePlot(bR, me); % update the behavioural report plot @()updateVariables(me,[],[],true); ... %update the task variables @()update(stims); ... %update our stimuli ready for display - @()checkTaskEnded(me); - @()drawnow; % ensure we update the figure + @()plot(bR, 1); % actually do our behaviour record drawing + @()checkTaskEnded(me); % check the trial / block # and if met stop the task }; %----------------------break entry breakEntryFn = { - @()beep(aM,200,0.5,1); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Broke fix! :-('); - @()trackerDrawEyePositions(eT); % draw the fixation window - @()trackerFlip(eT); %for tobii show info if operator screen enabled - @()trackerMessage(eT,['TRIAL_RESULT ' num2str(tS.BREAKFIX)]); %trial incorrect message - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.BREAKFIX); % send the end trial messages and other cleanup + @()trackerDrawStatus(eT,'BROKE FIX! :-(', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for operator screen stop flip @()needEyeSample(me,false); + @()hide(stims); + @()beep(aM, tS.errorSound); @()logRun(me,'BREAKFIX'); %fprintf current trial info }; %----------------------inc entry incEntryFn = { - @()beep(aM,200,0.5,1); - @()trackerMessage(eT,['TRIAL_RESULT ' num2str(tS.INCORRECT)]); %trial incorrect message - @()trackerDrawStatus(eT,'Incorrect! :-(', stims.stimulusPositions); - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.INCORRECT); % send the end trial messages and other cleanup + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions); + @()needFlipTracker(me, 0); %for operator screen stop flip @()needEyeSample(me,false); + @()hide(stims); + @()beep(aM, tS.errorSound); @()logRun(me,'INCORRECT'); %fprintf current trial info }; %----------------------our incorrect stimulus breakFn = { - @()drawBackground(s); - @()drawText(s,'Wrong'); + }; %----------------------break exit @@ -362,10 +279,13 @@ breakExitFn = { @()updatePlot(bR, me); @()updateVariables(me,[],[],false); ... %update the task variables @()update(stims); %update our stimuli ready for display - @()checkTaskEnded(me); - @()drawnow; + @()plot(bR, 1); % actually do our behaviour record drawing + @()checkTaskEnded(me); % check the trial / block # and if met stop the task }; +%======================================================== +%========================================================EYETRACKER +%======================================================== %--------------------calibration function calibrateFn = { @()drawBackground(s); %blank the display @@ -389,13 +309,10 @@ driftFn = { @()setOffline(eT); % set eyelink offline [tobii ignores this] @()driftCorrection(eT) % enter drift correct (only eyelink) }; -offsetFcn = { - @()drawBackground(s); %blank the display - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) -}; +%======================================================== +%========================================================GENERAL +%======================================================== %--------------------screenflash flashFn = { @()drawBackground(s); @@ -413,14 +330,6 @@ gridFn = { @()drawScreenCenter(s); }; -% N x 2 cell array of regexpi strings, list to skip the current -> next -% state's exit functions; for example skipExitStates = -% {'fixate','incorrect|breakfix'}; means that if the currentstate is -% 'fixate' and the next state is either incorrect OR breakfix, then skip -% the FIXATE exit state. Add multiple rows for skipping multiple state's -% exit states. -sM.skipExitStates = {'fixate','incorrect|breakfix'}; - %================================================================== %----------------------State Machine Table------------------------- % this table defines the states and relationships and function sets @@ -429,16 +338,18 @@ stateInfoTmp = { 'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; %--------------------------------------------------------------------------------------------- 'pause' 'blank' inf pauseEntryFn {} {} pauseExitFn; -'blank' 'stimulus' 0.5 psEntryFn prestimulusFn {} psExitFn; +%--------------------------------------------------------------------------------------------- +'blank' 'stimulus' 2 psEntryFn prestimulusFn {} psExitFn; 'stimulus' 'incorrect' 5 stimEntryFn stimFn maintainFixFn stimExitFn; -'incorrect' 'timeout' 2 incEntryFn breakFn {} breakExitFn; -'breakfix' 'timeout' 2 breakEntryFn breakFn {} breakExitFn; -'correct' 'blank' 0.5 correctEntryFn correctFn {} correctExitFn; -'timeout' 'blank' tS.tOut {} {} {} {}; +'incorrect' 'timeout' 0.1 incEntryFn breakFn {} breakExitFn; +'breakfix' 'timeout' 0.1 breakEntryFn breakFn {} breakExitFn; +'correct' 'blank' 0.1 correctEntryFn correctFn {} correctExitFn; +'timeout' 'blank' tS.timeOut {} {} {} {}; +%--------------------------------------------------------------------------------------------- 'calibrate' 'pause' 0.5 calibrateFn {} {} {}; 'offset' 'pause' 0.5 offsetFn {} {} {}; 'drift' 'pause' 0.5 driftFn {} {} {}; -'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- 'flash' 'pause' 0.5 {} flashFn {} {}; 'override' 'pause' 0.5 {} overrideFn {} {}; 'showgrid' 'pause' 1 {} gridFn {} {}; diff --git a/CoreProtocols/Isoluminant_Colours_StateInfo.m b/CoreProtocols/Isoluminant_Colours_StateInfo.m index 3d214f91fee4e545fac8fb6f999458b5846c9245..19e27e6386ca4840b2e7f2fe1730c952d73ed3f9 100644 --- a/CoreProtocols/Isoluminant_Colours_StateInfo.m +++ b/CoreProtocols/Isoluminant_Colours_StateInfo.m @@ -63,20 +63,24 @@ %================================================================== %------------------------General Settings-------------------------- +tS.name = 'Isoluminant'; %==name of this protocol +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… tS.useTask = true; %==use taskSequence (randomises stimulus variables) -tS.rewardTime = 250; %==TTL time in milliseconds -tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. -tS.checkKeysDuringStimulus = true; %==allow keyboard control within stimulus state? Slight drop in performance… +tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? -tS.askForComments = false; %==UI requestor asks for comments before/after run -tS.saveData = false; %==save behavioural and eye movement data? tS.includeErrors = false; %==do we update the trial number even for incorrect saccade/fixate, if true then we call updateTask for both correct and incorrect, otherwise we only call updateTask() for correct responses -tS.name = 'isoluminant'; %==name of this protocol tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object -tS.tOut = 5; %==if wrong response, how long to time out before next trial -tS.CORRECT = 1; %==the code to send eyetracker for correct trials -tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials -tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.timeOut = 2; %==if wrong response, how long to time out before next trial +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume +% reward system values, set by GUI, but could be overridden here +%rM.reward.time = 250; %==TTL time in milliseconds +%rM.reward.pin = 2; %==Output pin, 2 by default with Arduino. %================================================================== %----------------Debug logging to command window------------------ @@ -95,8 +99,16 @@ tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials % These settings define the initial fixation window and set up for the % eyetracker. They may be modified during the task (i.e. moving the % fixation window towards a target, enabling an exclusion window to stop -% the subject entering a specific set of display areas etc.) +% the subject entering a specific set of display areas etc.). +% +% The GUI sets some default values, but it is good practive to redefine +% them explicitly here. % +% **IMPORTANT**: you must ensure that the global state time is LARGER than +% any fixation timers specified here. Each state has a global timer, so if +% the state timer is 5 seconds but your fixation timer is 6 seconds, then +% the state will finish before the fixation time was completed! +%------------------------------------------------------------------ % initial fixation X position in degrees (0° is screen centre) tS.fixX = 0; % initial fixation Y position in degrees @@ -113,95 +125,31 @@ tS.strict = true; % do we add an exclusion zone where subject cannot saccade to... tS.exclusionZone = []; % time to fix on the stimulus -tS.stimulusFixTime = 0.5; -% historical log of X and Y position, and exclusion zone -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; -me.lastXExclusion = []; -me.lastYExclusion = []; - - -%================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options -% here they will OVERRIDE the GUI ones; if they are commented then the GUI -% options are used. me.elsettings and me.tobiisettings contain the GUI -% settings you can test if they are empty or not and set them based on -% that... -eT.name = tS.name; -if tS.saveData == true; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - eT.name = tS.name; - if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? - if tS.saveData == true; eT.recordData = true; end %===save EDF file? - if isempty(me.eyetracker.esettings) %==check if GUI settings are empty - eT.sampleRate = 250; %==sampling rate - eT.calibrationStyle = 'HV5'; %==calibration style - eT.calibrationProportion = [0.4 0.4]; %==the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation use 1-9 to show each dot, space to select fix as valid, - % INS key ON EYELINK KEYBOARD to accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; %==calibration target colour - eT.modify.calibrationtargetsize = 2; %==size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; %==width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; %==-1 = use any attachedkeyboard - eT.modify.targetbeep = 1; %==beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - eT.name = tS.name; - if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? - if isempty(me.eyetracker.tsettings) %==check if GUI settings are empty - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - eT.valPositions = [ .5 .5 ]; - end -end - +tS.stimulusFixTime = 1; %Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%Ensure we don't start with any exclusion zones set up -eT.resetExclusionZones(); - -%================================================================== -%----WHICH states assigned as correct or break for online plot?---- -%----You need to use regex patterns for the match (doc regexp)----- -bR.correctStateName = "correct"; -bR.breakStateName = ["breakfix","incorrect"]; %================================================================== -%--------------randomise stimulus variables every trial?----------- -% if you want to have some randomisation of stimuls variables without using -% taskSequence task (i.e. general training tasks), you can uncomment this -% and runExperiment can use this structure to change e.g. X or Y position, -% size, angle see metaStimulus for more details. Remember this will not be -% "Saved" for later use, if you want to do controlled methods of constants -% experiments use taskSequence to define proper randomised and balanced -% variable sets and triggers to send to recording equipment etc... -% -% stims.choice = []; -% n = 1; -% in(n).name = 'xyPosition'; -% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; -% in(n).stimuli = 1; -% in(n).offset = []; -% stims.stimulusTable = in; -stims.choice = []; -stims.stimulusTable = []; +%-----------------BEAVIOURAL PLOT CONFIGURATION-------------------- +%--WHICH states assigned correct / incorrect for the online plot?-- +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%========================================================================= +%------------------Randomise stimulus variables every trial?-------------- +% If you want to have some randomisation of stimuls variables WITHOUT using +% taskSequence task. Remember this will not be "Saved" for later use, if you +% want to do controlled experiments use taskSequence to define proper randomised +% and balanced variable sets and triggers to send to recording equipment etc... +% Good for training tasks, or stimulus variability irrelevant to the task. +% n = 1; +% in(n).name = 'xyPosition'; +% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; +% in(n).stimuli = 1; +% in(n).offset = []; +% stims.stimulusTable = in; +stims.choice = []; +stims.stimulusTable = []; %================================================================== %this allows us to enable subsets from our stimulus list @@ -210,7 +158,7 @@ stims.stimulusSets = {[1,2],[1]}; stims.setChoice = 1; hide(stims); -%================================================================== +%========================================================================= % N x 2 cell array of regexpi strings, list to skip the current -> next % state's exit functions; for example skipExitStates = % {'fixate','incorrect|breakfix'}; means that if the currentstate is @@ -219,34 +167,43 @@ hide(stims); % exit states. sM.skipExitStates = {'fixate','incorrect|breakfix'}; +%========================================================================= +% which stimulus in the list is defined as a saccade target? +stims.fixationChoice = 1; + %=================================================================== %=================================================================== %=================================================================== -%-----------------State Machine Task Functions--------------------- +%------------------State Machine Task Functions--------------------- % Each cell {array} holds a set of anonymous function handles which are % executed by the state machine to control the experiment. The state -% machine can run sets at entry ['entryFcn'], during ['withinFcn'], to -% trigger a transition jump to another state ['transitionFcn'], and at exit -% ['exitFcn'. Remember these {sets} need to access the objects that are +% machine can run sets at entry ['entryFn'], during ['withinFn'], to +% trigger a transition jump to another state ['transitionFn'], and at exit +% ['exitFn'. Remember these {sets} need to access the objects that are % available within the runExperiment context (see top of file). You can % also add global variables/objects then use these. The values entered here % are set on load, if you want up-to-date values then you need to use % methods/function wrappers to retrieve/set them. +%=================================================================== +%=================================================================== +%=================================================================== -%====================================================PAUSE +%======================================================== +%========================================================PAUSE +%======================================================== %--------------------pause entry pauseEntryFcn = { - @()hide(stims); - @()drawBackground(s); %blank the subject display + @()hide(stims); % hide all stimuli + @()drawBackground(s); % blank the subject display + @()drawPhotoDiodeSquare(s,[0 0 0]); % draw black photodiode @()drawTextNow(s,'PAUSED, press [p] to resume...'); @()disp('PAUSED, press [p] to resume...'); - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawText(eT,'PAUSED, press [P] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', stims.stimulusPositions); @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()stopRecording(eT, true); %stop recording eye position data - @()needFlip(me, false); % no need to flip the PTB screen - @()needEyeSample(me,false); % no need to check eye position + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false, 0); % no need to flip the PTB screen or tracker + @()needEyeSample(me, false); % no need to check eye position }; %pause exit @@ -260,60 +217,82 @@ pauseExitFcn = { %====================================================PREFIXATION prefixEntryFcn = { - @()needFlip(me, true); + @()needFlip(me, true, 1); @()needEyeSample(me, true); % make sure we start measuring eye position - @()hide(stims); + @()getStimulusPositions(stims); % make a struct eT can use for drawing stim positions + @()hide(stims); % hide all stimuli + @()resetAll(eT); % reset all fixation markers to initial state + % update the fixation window to initial values + @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window + % send the trial start messages to the eyetracker + @()trackerTrialStart(eT, getTaskIndex(me)); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + % you can add any other messages, such as stimulus values as needed, + % e.g. @()trackerMessage(eT,['MSG:ANGLE' num2str(stims{1}.angleOut)]) etc. }; prefixFcn = { @()drawBackground(s); }; +%--------------------prefixate exit +prefixExitFcn = { + @()logRun(me,'INITFIX'); + @()trackerMessage(eT,'MSG:Start Fix'); + @()trackerDrawStatus(eT,'Start trial...', stims.stimulusPositions,0); +}; + +%============================================================== +%====================================================FIXATION +%============================================================== %--------------------fixate entry fixEntryFcn = { - @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window - @()startRecording(eT); % start eyelink recording for this trial - @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker - @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawFixation(eT); %draw fixation window on eyelink computer - @()trackerDrawStimuli(eT,stims.stimulusPositions); %draw location of stimulus on eyelink - @()statusMessage(eT,'Initiate Fixation...'); %status text on the eyelink - @()show(stims{tS.nStims}); % show only last stim in list (fix cross) - @()logRun(me,'INITFIX'); %fprintf current trial info to command window + @()show(stims{tS.nStims}); % show last stim which is usually fixation cross }; %--------------------fix within fixFcn = { @()draw(stims); %draw stimuli - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); @()animate(stims); % animate stimuli for subsequent draw }; %--------------------test we are fixated for a certain length of time -initFixFcn = { - @()testSearchHoldFixation(eT,'stimulus','incorrect'); +inFixFcn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testSearchHoldFixation(eT,'stimulus','breakfix') }; %--------------------exit fixation phase fixExitFcn = { - @()statusMessage(eT,'Show Stimulus...'); - @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); %reset a maintained fixation of 1 second - @()show(stims); % show all stims) - @()trackerMessage(eT,'END_FIX'); + % reset fixation timers to maintain fixation for tS.stimulusFixTime seconds + @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); + @()show(stims); % show all stims + @()trackerMessage(eT,'END_FIX'); %eyetracker message saved to data stream }; -%--------------------what to run when we enter the stim presentation state -stimEntryFcn = { - @()doSyncTime(me); %EDF sync message - @()doStrobe(me,true) -}; +%======================================================== +%========================================================STIMULUS +%======================================================== + +stimEntryFcn = { + % send an eyeTracker sync message (reset relative time to 0 after next flip) + @()doSyncTime(me); + % send stimulus value strobe (value alreadyset by updateVariables(me) function) + @()doStrobe(me,true); +}; + %--------------------what to run when we are showing stimuli stimFcn = { @()draw(stims); - @()drawPhotoDiode(s,[1 1 1]); + @()drawPhotoDiodeSquare(s,[1 1 1]); @()animate(stims); % animate stimuli for subsequent draw }; @@ -326,112 +305,120 @@ maintainFixFcn = { % otherwise 'breakfix' is returned and the state machine will jump to the % breakfix state. If neither condition matches, then the state table below % defines that after 5 seconds we will switch to the incorrect state. - @()testSearchHoldFixation(eT,'correct','breakfix'); + @()testHoldFixation(eT,'correct','incorrect'); }; %as we exit stim presentation state -stimExitFcn = { - @()sendStrobe(io,255); +stimExitFcn = { + @()setStrobeValue(me, 255); % 255 indicates stimulus OFF + @()doStrobe(me, true); }; -%if the subject is correct (small reward) +%======================================================== +%========================================================DECISIONS +%======================================================== + +%========================================================CORRECT +%--------------------if the subject is correct (small reward) correctEntryFcn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM,2000); % correct beep - @()trackerMessage(eT,'END_RT'); %send END_RT message to tracker - @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.CORRECT)]); %send TRIAL_RESULT message to tracker - @()trackerDrawText(eT,'Correct! :-)'); - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.CORRECT); % send the end trial messages and other cleanup @()needEyeSample(me,false); % no need to collect eye data until we start the next trial - @()hide(stims); %hide all stims - @()logRun(me,'CORRECT'); %fprintf current trial info + @()hide(stims); % hide all stims }; -%correct stimulus -correctFcn = { - @()drawBackground(s); +%--------------------correct stimulus +correctFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); }; -%when we exit the correct state +%--------------------when we exit the correct state correctExitFcn = { - @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial - @()updateVariables(me); %randomise our stimuli, and set strobe value too - @()update(me.stimuli); %update our stimuli ready for display - @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions - @()trackerClearScreen(eT); - @()updatePlot(bR, me); %update our behavioural plot - @()checkTaskEnded(me); %check if task is finished - @()resetFixation(eT); %resets the fixation state timers - @()resetFixationHistory(eT); %reset the stored X and Y values - @()drawnow; + @()giveReward(rM); % send a reward + @()beep(aM, tS.correctSound); % correct beep + @()logRun(me,'CORRECT'); % print current trial info + @()trackerDrawStatus(eT, 'CORRECT! :-)'); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updatePlot(bR, me); % must run before updateTask + @()updateTask(me, tS.CORRECT); % make sure our taskSequence is moved to the next trial + @()updateVariables(me); % randomise our stimuli, and set strobe value too + @()update(stims); % update our stimuli ready for display + @()plot(bR, 1); % actually do our behaviour record drawing }; -%incorrect entry -incEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Incorrect! :-('); - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.INCORRECT)]); +%========================================================INCORRECT/BREAKFIX +%--------------------incorrect entry +incEntryFcn = { + @()trackerTrialEnd(eT, tS.INCORRECT); % send the end trial messages and other cleanup @()needEyeSample(me,false); @()hide(stims); - @()logRun(me,'INCORRECT'); %fprintf current trial info -}; - -%our incorrect/breakfix stimulus -incFcn = { - @()drawBackground(s); -}; - -%--------------------incorrect exit -incExitFcn = { - @()updateVariables(me); %randomise our stimuli, set strobe value too - @()update(stims); %update our stimuli ready for display - @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions - @()trackerClearScreen(eT); - @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() - @()resetFixation(eT); %resets the fixation state timers - @()resetFixationHistory(eT); %reset the stored X and Y values - @()drawnow; }; - %--------------------break entry breakEntryFcn = { - @()beep(aM,400,0.5,1); - @()edfMessage(eT,'END_RT'); - @()edfMessage(eT,['TRIAL_RESULT ' num2str(tS.BREAKFIX)]); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Broke maintain fix! :-('); - @()stopRecording(eT); - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.BREAKFIX); % send the end trial messages and other cleanup @()needEyeSample(me,false); - @()sendStrobe(io,252); @()hide(stims); - @()logRun(me,'BREAKFIX'); %fprintf current trial info }; -%--------------------break exit -breakExitFcn = incExitFcn; % we copy the incorrect exit functions +%--------------------our incorrect/breakfix stimulus +incFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------generic exit +exitFcn = { + % tS.includeErrors will prepend some code here... + @()beep(aM, tS.errorSound); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; %--------------------change functions based on tS settings -% this shows an example of how to use tS options to change the function -% lists run by the state machine. We can prepend or append new functions to -% the cell arrays... +% we use tS options to change the function lists run by the state machine. +% We can prepend or append new functions to the cell arrays. +% +% logRun = add current info to behaviural record +% updatePlot = updates the behavioural record +% updateTask = updates task object +% resetRun = randomise current trial within the block (makes it harder for +% subject to guess based on previous failed trial. +% checkTaskEnded = see if taskSequence has finished if tS.includeErrors % we want to update our task even if there were errors - incExitFcn = [ {@()updateTask(me,tS.INCORRECT)}; incExitFcn ]; %update our taskSequence - breakExitFcn = [ {@()updateTask(me,tS.BREAKFIX)}; breakExitFcn ]; %update our taskSequence + incExitFcn = [ { + @()logRun(me,'INCORRECT'); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()updatePlot(bR, me); + @()updateTask(me,tS.INCORRECT)}; + exitFcn ]; %update our taskSequence + breakExitFcn = [ { + @()logRun(me,'BREAK_FIX'); + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', stims.stimulusPositions, 0); + @()updatePlot(bR, me); + @()updateTask(me,tS.BREAKFIX)}; + exitFcn ]; %update our taskSequence +else + incExitFcn = [ { + @()logRun(me,'INCORRECT'); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFcn ]; + breakExitFcn = [ { + @()logRun(me,'BREAK_FIX'); + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', stims.stimulusPositions, 0); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFcn ]; end -if tS.useTask %we are using task +if tS.useTask || task.nBlocks > 0 correctExitFcn = [ correctExitFcn; {@()checkTaskEnded(me)} ]; incExitFcn = [ incExitFcn; {@()checkTaskEnded(me)} ]; breakExitFcn = [ breakExitFcn; {@()checkTaskEnded(me)} ]; - if ~tS.includeErrors % using task but don't include errors - incExitFcn = [ {@()resetRun(task)}; incExitFcn ]; %we randomise the run within this block to make it harder to guess next trial - breakExitFcn = [ {@()resetRun(task)}; breakExitFcn ]; %we randomise the run within this block to make it harder to guess next trial - end end + %--------------------enter tracker calibrate/validate setup mode calibrateFcn = { @()drawBackground(s); %blank the display diff --git a/CoreProtocols/OrientationTuningStateInfo.m b/CoreProtocols/OrientationTuningStateInfo.m index e40c9982d9c9761974f161396fc0a487a8aaa47c..ba5ee460e7d94ad1001de1250b094ddc40f249ea 100644 --- a/CoreProtocols/OrientationTuningStateInfo.m +++ b/CoreProtocols/OrientationTuningStateInfo.m @@ -15,177 +15,124 @@ %> uF = user functions - add your own functions to this class %> tS = structure to hold general variables, will be saved as part of the data -%================================================================== -%------------------------General Settings-------------------------- -% These settings are collected here to make changing the behaviour of the -% protocol easier. tS is just a struct(), so you can add your own switches -% or values here and use them lower down. Some basic switches like -% saveData, useTask, checkKeysDuringstimulus will influence the -% runeExperiment.runTask() functionality, not just the state machine. Other -% switches like includeErrors are referenced in this state machine file to -% change with functions are added to the state machine states… +%========================================================================= +%-----------------------------General Settings---------------------------- +% These settings make changing the behaviour of the protocol easier. tS +% is just a struct(), so you can add your own switches or values here and +% use them lower down. Some basic switches like saveData, useTask, +% enableTrainingKeys will influence the runeExperiment.runTask() +% functionality, not just the state machine. Other switches like +% includeErrors are referenced in this state machine file to change which +% functions are added to the state machine states… +tS.name = 'Orientation Tuning'; %==name of this protocol +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… tS.useTask = true; %==use taskSequence (randomises stimulus variables) -tS.rewardTime = 250; %==TTL time in milliseconds -tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. -tS.checkKeysDuringStimulus = false; %==allow keyboard control within stimulus state? Slight drop in performance… -tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker data file? +tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? tS.askForComments = false; %==UI requestor asks for comments before/after run -tS.saveData = false; %==save behavioural and eye movement data? -tS.showBehaviourPlot = false; %==open the behaviourPlot figure? Can cause more memory usein MATLAB -tS.name = 'orientation'; %==name of this protocol +tS.includeErrors = false; %==do we update the trial number even for incorrect saccade/fixate, if true then we call updateTask for both correct and incorrect, otherwise we only call updateTask() for correct responses tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object -tS.tOut = 5; %==if wrong response, how long to time out before next trial -tS.CORRECT = 1; %==the code to send eyetracker for correct trials -tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials -tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.timeOut = 2; %==if wrong response, how long to time out before next trial +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume +% reward system values, set by GUI, but could be overridden here +%rM.reward.time = 250; %==TTL time in milliseconds +%rM.reward.pin = 2; %==Output pin, 2 by default with Arduino. %================================================================== -%----------------Debug logging to command window------------------ +%------------ ----DEBUG LOGGING to command window------------------ % uncomment each line to get specific verbose logging from each of these % components; you can also set verbose in the opticka GUI to enable all of % these… -%sM.verbose = true; %==print out stateMachine info for debugging -%stims.verbose = true; %==print out metaStimulus info for debugging -%io.verbose = true; %==print out io commands for debugging -%eT.verbose = true; %==print out eyelink commands for debugging -%rM.verbose = true; %==print out reward commands for debugging -%task.verbose = true; %==print out task info for debugging +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging %================================================================== %-----------------INITIAL Eyetracker Settings---------------------- % These settings define the initial fixation window and set up for the % eyetracker. They may be modified during the task (i.e. moving the % fixation window towards a target, enabling an exclusion window to stop -% the subject entering a specific set of display areas etc.). +% the subject entering a specific set of display areas etc.). +% +% The GUI sets some default values, but it is good practive to redefine +% them explicitly here. % -% IMPORTANT: you need to make sure that the global state time is larger -% than the fixation timers specified here. Each state has a global timer, -% so if the state timer is 5 seconds but your fixation timer is 6 seconds, -% then the state will finish before the fixation time was completed! - -% initial fixation X position in degrees (0° is screen centre) +% **IMPORTANT**: you must ensure that the global state time is LARGER than +% any fixation timers specified here. Each state has a global timer, so if +% the state timer is 5 seconds but your fixation timer is 6 seconds, then +% the state will finish before the fixation time was completed! +%------------------------------------------------------------------ +% initial fixation X position in degrees (0° is screen centre). Multiple windows +% can be entered using an array of X values tS.fixX = 0; -% initial fixation Y position in degrees +% initial fixation Y position in degrees (0° is screen centre). Multiple windows +% can be entered using an array. tS.fixY = 0; -% time to search and enter fixation window +% time to search and enter fixation window (Initiate fixation) tS.firstFixInit = 3; % time to maintain initial fixation within window, can be single value or a % range to randomise between -tS.firstFixTime = [0.5 0.9]; -% circular fixation window radius in degrees +tS.firstFixTime = [0.5 0.75]; +% circular fixation window radius in degrees; if you enter [x y] the window will be +% rectangular. tS.firstFixRadius = 2; -% do we forbid eye to enter-exit-reenter fixation window? +% do we forbid eye to enter-exit-reenter fixation window? Set this to false +% during initial training, or if you want relaxed checking (non-forced +% choice gaze tasks). tS.strict = true; -% do we add an exclusion zone where subject cannot saccade to... +% add an exclusion zone where subject cannot saccade to? tS.exclusionZone = []; -% time to fix on the stimulus -tS.stimulusFixTime = 1.5; -% log of recent X and Y position, and exclusion zone, here set ti initial -% values -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; -me.lastXExclusion = []; -me.lastYExclusion = []; - -%================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options -% here they will OVERRIDE the GUI ones; if they are commented then the GUI -% options are used. me.elsettings and me.tobiisettings contain the GUI -% settings you can test if they are empty or not and set them based on -% that... -eT.name = tS.name; -if tS.saveData == true; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - eT.name = tS.name; - if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? - if tS.saveData == true; eT.recordData = true; end %===save EDF file? - if isempty(me.eyetracker.esettings) %==check if GUI settings are empty - eT.sampleRate = 250; %==sampling rate - eT.calibrationStyle = 'HV5'; %==calibration style - eT.calibrationProportion = [0.4 0.4]; %==the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation use 1-9 to show each dot, space to select fix as valid, - % INS key ON EYELINK KEYBOARD to accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; %==calibration target colour - eT.modify.calibrationtargetsize = 2; %==size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; %==width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; %==-1 = use any attachedkeyboard - eT.modify.targetbeep = 1; %==beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - eT.name = tS.name; - if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? - if isempty(me.eyetracker.tsettings) %==check if GUI settings are empty - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - eT.valPositions = [ .5 .5 ]; - end -end - -%Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix -eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%Ensure we don't start with any exclusion zones set up -eT.resetExclusionZones(); - -%================================================================== -%----WHICH states assigned as correct or break for online plot?---- -%----You need to use regex patterns for the match (doc regexp)----- -bR.correctStateName = "correct"; -bR.breakStateName = ["breakfix","incorrect"]; +% time to maintain fixation during the stimulus state +tS.stimulusFixTime = 1.5; +% Initialise eyetracker with X, Y, FixInitTime, FixTime, Radius, StrictFix values +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); %================================================================== -%--------------randomise stimulus variables every trial?----------- -% if you want to have some randomisation of stimuls variables without using -% taskSequence task (i.e. general training tasks), you can uncomment this -% and runExperiment can use this structure to change e.g. X or Y position, -% size, angle see metaStimulus for more details. Remember this will not be -% "Saved" for later use, if you want to do controlled methods of constants -% experiments use taskSequence to define proper randomised and balanced -% variable sets and triggers to send to recording equipment etc... -% -% stims.choice = []; -% n = 1; -% in(n).name = 'xyPosition'; -% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; -% in(n).stimuli = 1; -% in(n).offset = []; -% stims.stimulusTable = in; -stims.choice = []; -stims.stimulusTable = []; - -%================================================================== -%-------------allows using arrow keys to control variables?------------- +%-----------------BEAVIOURAL PLOT CONFIGURATION-------------------- +%--WHICH states assigned correct / incorrect for the online plot?-- +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%========================================================================= +%------------------Randomise stimulus variables every trial?-------------- +% If you want to have some randomisation of stimuls variables WITHOUT using +% taskSequence task. Remember this will not be "Saved" for later use, if you +% want to do controlled experiments use taskSequence to define proper randomised +% and balanced variable sets and triggers to send to recording equipment etc... +% Good for training tasks, or stimulus variability irrelevant to the task. +% n = 1; +% in(n).name = 'xyPosition'; +% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; +% in(n).stimuli = 1; +% in(n).offset = []; +% stims.stimulusTable = in; +stims.choice = []; +stims.stimulusTable = []; + +%========================================================================= +%--------------allows using arrow keys to control variables?-------------- % another option is to enable manual control of a table of variables -% this is useful to probe RF properties or other features while still -% allowing for fixation or other behavioural control. +% in-task. This is useful to dynamically probe RF properties or other +% features while still allowing for fixation or other behavioural control. % Use arrow keys <- -> to control value and ↑ ↓ to control variable. stims.controlTable = []; stims.tableChoice = 1; -%================================================================== -%this allows us to enable subsets from our stimulus list -% 1 = grating | 2 = fixation cross -stims.stimulusSets = {[2],[1,2]}; +%====================================================================== +% this allows us to enable subsets from our stimulus list +stims.stimulusSets = {[1,2],[1]}; stims.setChoice = 1; -hide(stims); -%================================================================== +%========================================================================= % N x 2 cell array of regexpi strings, list to skip the current -> next % state's exit functions; for example skipExitStates = % {'fixate','incorrect|breakfix'}; means that if the currentstate is @@ -194,9 +141,15 @@ hide(stims); % exit states. sM.skipExitStates = {'fixate','incorrect|breakfix'}; -%=================================================================== -%=================================================================== -%=================================================================== +%========================================================================= +% which stimulus in the list is defined as a saccade target? +stims.fixationChoice = 1; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= %------------------State Machine Task Functions--------------------- % Each cell {array} holds a set of anonymous function handles which are % executed by the state machine to control the experiment. The state @@ -207,26 +160,25 @@ sM.skipExitStates = {'fixate','incorrect|breakfix'}; % also add global variables/objects then use these. The values entered here % are set on load, if you want up-to-date values then you need to use % methods/function wrappers to retrieve/set them. -%=================================================================== -%=================================================================== -%=================================================================== +%========================================================================= +%============================================================== +%========================================================PAUSE +%============================================================== -%====================================================PAUSE %--------------------pause entry pauseEntryFcn = { - @()hide(stims); - @()drawBackground(s); %blank the subject display - @()drawPhotoDiode(s,[0 0 0]); + @()hide(stims); % hide all stimuli + @()drawBackground(s); % blank the subject display + @()drawPhotoDiodeSquare(s,[0 0 0]); % draw black photodiode @()drawTextNow(s,'PAUSED, press [p] to resume...'); @()disp('PAUSED, press [p] to resume...'); - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawText(eT,'PAUSED, press [P] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', stims.stimulusPositions); @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()stopRecording(eT, true); %stop recording eye position data - @()needFlip(me, false); % no need to flip the PTB screen - @()needEyeSample(me,false); % no need to check eye position + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false, 0); % no need to flip the PTB screen or tracker + @()needEyeSample(me, false); % no need to check eye position }; %--------------------pause exit @@ -238,43 +190,49 @@ pauseExitFcn = { @()startRecording(eT, true); }; -%====================================================PREFIXATION +%============================================================== +%====================================================PRE-FIXATION +%============================================================== +%--------------------prefixate entry prefixEntryFcn = { - @()needFlip(me, true); + @()needFlip(me, true, 1); % enable the screen and trackerscreen flip @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims); % make a struct eT can use for drawing stim positions @()hide(stims); % hide all stimuli - @()hide(stims); + @()resetAll(eT); % reset all fixation markers to initial state + % update the fixation window to initial values + @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window + % send the trial start messages to the eyetracker + @()trackerTrialStart(eT, getTaskIndex(me)); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + % you can add any other messages, such as stimulus values as needed, + % e.g. @()trackerMessage(eT,['MSG:ANGLE' num2str(stims{1}.angleOut)]) etc. }; +%--------------------prefixate within prefixFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; +%--------------------prefixate exit prefixExitFcn = { - @()resetFixationHistory(eT); % reset the recent eye position history - @()resetExclusionZones(eT); % reset the exclusion zones on eyetracker - @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window - @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker - @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - @()startRecording(eT); %start recording eye position data again - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawFixation(eT); % draw the fixation window - @()trackerDrawStimuli(eT,stims.stimulusPositions); %draw location of stimulus on eyelink - @()statusMessage(eT,'Initiate Fixation...'); %status text on the eyelink - @()needEyeSample(me,true); % make sure we start measuring eye position + @()logRun(me,'INITFIX'); + @()trackerMessage(eT,'MSG:Start Fix'); + @()trackerDrawStatus(eT,'Start trial...', stims.stimulusPositions,0); }; -%fixate entry -fixEntryFcn = { - @()show(stims{2}); - @()logRun(me,'INITFIX'); %fprintf current trial info to command window +%============================================================== +%====================================================FIXATION +%============================================================== +%--------------------fixate entry +fixEntryFcn = { + @()show(stims{tS.nStims}); % show last stim which is usually fixation cross }; %--------------------fix within fixFcn = { - @()drawPhotoDiode(s,[0 0 0]); - @()draw(stims); %draw stimulus + @()draw(stims); %draw stimuli + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %--------------------test we are fixated for a certain length of time @@ -286,34 +244,36 @@ inFixFcn = { % otherwise 'breakfix' is returned and the state machine will jump to the % breakfix state. If neither condition matches, then the state table below % defines that after 5 seconds we will switch to the incorrect state. - @()testSearchHoldFixation(eT,'stimulus','incorrect') + @()testSearchHoldFixation(eT,'stimulus','breakfix') }; %--------------------exit fixation phase fixExitFcn = { - @()statusMessage(eT,'Show Stimulus...'); % reset fixation timers to maintain fixation for tS.stimulusFixTime seconds - @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); %reset fixation time for stimulus = tS.stimulusFixTime - @()show(stims{1}); - @()trackerMessage(eT,'END_FIX'); -}; + @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); + @()show(stims); % show all stims + @()trackerMessage(eT,'END_FIX'); %eyetracker message saved to data stream +}; + +%======================================================== +%========================================================STIMULUS +%======================================================== -%====================================================STIMULUS stimEntryFcn = { - % send an eyeTracker sync message (reset relative time to 0 after first flip of this state) + % send an eyeTracker sync message (reset relative time to 0 after next flip) @()doSyncTime(me); - % send stimulus value strobe + % send stimulus value strobe (value alreadyset by updateVariables(me) function) @()doStrobe(me,true); }; %--------------------what to run when we are showing stimuli stimFcn = { @()draw(stims); - @()drawPhotoDiode(s,[1 1 1]); + @()drawPhotoDiodeSquare(s,[1 1 1]); @()animate(stims); % animate stimuli for subsequent draw }; -%--------------------test we are maintaining fixation +%-----------------------test we are maintaining fixation maintainFixFcn = { % this command performs the logic to search and then maintain fixation % inside the fixation window. The eyetracker parameters are defined above. @@ -322,109 +282,135 @@ maintainFixFcn = { % otherwise 'breakfix' is returned and the state machine will jump to the % breakfix state. If neither condition matches, then the state table below % defines that after 5 seconds we will switch to the incorrect state. - @()testHoldFixation(eT,'correct','breakfix'); + @()testHoldFixation(eT,'correct','incorrect'); }; -%--------------------as we exit stim presentation state +%as we exit stim presentation state stimExitFcn = { - @()prepareStrobe(io, 255); + @()setStrobeValue(me, 255); % 255 indicates stimulus OFF @()doStrobe(me, true); }; -%====================================================CORRECT +%======================================================== +%========================================================DECISIONS +%======================================================== + +%========================================================CORRECT %--------------------if the subject is correct (small reward) correctEntryFcn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM, 2000, 0.1, 0.1); % correct beep - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Correct! :-)'); - @()stopRecording(eT); - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.CORRECT); % send the end trial messages and other cleanup @()needEyeSample(me,false); % no need to collect eye data until we start the next trial - @()hide(stims); - @()logRun(me,'CORRECT'); %fprintf current trial info + @()hide(stims); % hide all stims }; %--------------------correct stimulus correctFcn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %--------------------when we exit the correct state correctExitFcn = { - @()sendStrobe(io,250); - @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() - @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial - @()updateVariables(me); %randomise our stimuli, and set strobe value too - @()update(stims); %update our stimuli ready for display - @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions - @()trackerClearScreen(eT); - @()checkTaskEnded(me); %check if task is finished - @()drawnow; + @()giveReward(rM); % send a reward + @()beep(aM, tS.correctSound); % correct beep + @()logRun(me,'CORRECT'); % print current trial info + @()trackerDrawStatus(eT, 'CORRECT! :-)'); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updatePlot(bR, me); % must run before updateTask + @()updateTask(me, tS.CORRECT); % make sure our taskSequence is moved to the next trial + @()updateVariables(me); % randomise our stimuli, and set strobe value too + @()update(stims); % update our stimuli ready for display + @()plot(bR, 1); % actually do our behaviour record drawing }; -%====================================================INCORRECT - +%========================================================INCORRECT/BREAKFIX %--------------------incorrect entry -incEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Incorrect! :-('); - @()stopRecording(eT); - @()setOffline(eT); % set eyelink offline [tobii ignores this] +incEntryFcn = { + @()trackerTrialEnd(eT, tS.INCORRECT); % send the end trial messages and other cleanup @()needEyeSample(me,false); @()hide(stims); - @()logRun(me,'INCORRECT'); %fprintf current trial info -}; - -%--------------------our incorrect stimulus -incFcn = { - @()drawPhotoDiode(s,[0 0 0]); -}; - -%--------------------incorrect / break exit -incExitFcn = { - @()sendStrobe(io,251); - @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() - @()resetRun(task); %we randomise the run within this block to make it harder to guess next trial - @()updateVariables(me); %randomise our stimuli, set strobe value too - @()update(stims); %update our stimuli ready for display - @()checkTaskEnded(me); %check if task is finished - @()drawnow; }; - %--------------------break entry breakEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.BREAKFIX)); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Broke maintain fix! :-('); - @()stopRecording(eT); - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.BREAKFIX); % send the end trial messages and other cleanup @()needEyeSample(me,false); @()hide(stims); - @()logRun(me,'BREAKFIX'); %fprintf current trial info }; +%--------------------our incorrect/breakfix stimulus +incFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------generic exit +exitFcn = { + % tS.includeErrors will prepend some code here... + @()beep(aM, tS.errorSound); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; + +%--------------------change functions based on tS settings +% we use tS options to change the function lists run by the state machine. +% We can prepend or append new functions to the cell arrays. +% +% logRun = add current info to behaviural record +% updatePlot = updates the behavioural record +% updateTask = updates task object +% resetRun = randomise current trial within the block (makes it harder for +% subject to guess based on previous failed trial. +% checkTaskEnded = see if taskSequence has finished +if tS.includeErrors % we want to update our task even if there were errors + incExitFcn = [ { + @()logRun(me,'INCORRECT'); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()updatePlot(bR, me); + @()updateTask(me,tS.INCORRECT)}; + exitFcn ]; %update our taskSequence + breakExitFcn = [ { + @()logRun(me,'BREAK_FIX'); + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', stims.stimulusPositions, 0); + @()updatePlot(bR, me); + @()updateTask(me,tS.BREAKFIX)}; + exitFcn ]; %update our taskSequence +else + incExitFcn = [ { + @()logRun(me,'INCORRECT'); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFcn ]; + breakExitFcn = [ { + @()logRun(me,'BREAK_FIX'); + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', stims.stimulusPositions, 0); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFcn ]; +end +if tS.useTask || task.nBlocks > 0 + correctExitFcn = [ correctExitFcn; {@()checkTaskEnded(me)} ]; + incExitFcn = [ incExitFcn; {@()checkTaskEnded(me)} ]; + breakExitFcn = [ breakExitFcn; {@()checkTaskEnded(me)} ]; +end + +%======================================================== +%========================================================EYETRACKER +%======================================================== %--------------------calibration function calibrateFcn = { @()drawBackground(s); %blank the display @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()rstop(io); @()trackerSetup(eT); %enter tracker calibrate/validate setup mode }; -%--------------------drift correction function (eyelink only) +%--------------------drift correction function driftFcn = { @()drawBackground(s); %blank the display - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT); % stop recording in eyelink [others ignores this] + @()setOffline(eT); % set eyelink offline [others ignores this] @()driftCorrection(eT) % enter drift correct (only eyelink) }; offsetFcn = { @@ -434,60 +420,52 @@ offsetFcn = { @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) }; -%--------------------drift offset function (eyelink | tobii) -offsetFcn = { - @()drawBackground(s); %blank the display - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) -}; - -%====================================================GENERAL - -%--------------------debug override +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values %--------------------screenflash flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection -%--------------------magstim -magstimFcn = { - @()drawBackground(s); - @()stimulate(mS); % run the magstim -}; - %--------------------show 1deg size grid gridFcn = { @()drawGrid(s) }; -%============================================================================== +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== %========================================================================== %========================================================================== %--------------------------State Machine Table----------------------------- % specify our cell array that is read by the stateMachine stateInfoTmp = { 'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; -%----------------------------------------------------------------------------------------- -'pause' 'prefix' inf pauseEntryFcn [] [] pauseExitFcn; -'prefix' 'fixate' 0.5 prefixEntryFcn prefixFcn [] prefixExitFcn; -'fixate' 'incorrect' 5 fixEntryFcn fixFcn inFixFcn fixExitFcn; -'stimulus' 'incorrect' 5 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; -'incorrect' 'prefix' tS.tOut incEntryFcn incFcn [] incExitFcn; -'breakfix' 'prefix' tS.tOut breakEntryFcn incFcn [] incExitFcn; -'correct' 'prefix' 0.5 correctEntryFcn correctFcn [] correctExitFcn; -%----------------------------------------------------------------------------------------- -'calibrate' 'pause' 0.5 calibrateFcn [] [] []; -'drift' 'pause' 0.5 driftFcn [] [] []; -'offset' 'pause' 0.5 offsetFcn [] [] []; -%----------------------------------------------------------------------------------------- -'override' 'pause' 0.5 overrideFcn [] [] []; -'flash' 'pause' 0.5 flashFcn [] [] []; -'magstim' 'prefix' 0.5 [] magstimFcn [] []; -'showgrid' 'pause' 10 [] gridFcn [] []; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFcn {} {} pauseExitFcn; +%--------------------------------------------------------------------------------------------- +'prefix' 'fixate' 0.75 prefixEntryFcn prefixFcn {} {}; +'fixate' 'breakfix' 10 fixEntryFcn fixFcn inFixFcn fixExitFcn; +'stimulus' 'incorrect' 10 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; +'correct' 'prefix' 0.1 correctEntryFcn correctFcn {} correctExitFcn; +'incorrect' 'timeout' 0.1 incEntryFcn incFcn {} incExitFcn; +'breakfix' 'timeout' 0.1 breakEntryFcn incFcn {} breakExitFcn; +'timeout' 'prefix' tS.timeOut {} incFcn {} {}; +%--------------------------------------------------------------------------------------------- +'calibrate' 'pause' 0.5 calibrateFcn {} {} {}; +'drift' 'pause' 0.5 driftFcn {} {} {}; +'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFcn {} {} {}; +'flash' 'pause' 0.5 flashFcn {} {} {}; +'showgrid' 'pause' 10 {} gridFcn {} {}; }; %--------------------------State Machine Table----------------------------- %========================================================================== + disp('=================>> Built state info file <<==================') disp(stateInfoTmp) -disp('=================>> Loaded state info file <<=================') +disp('=================>> Built state info file <<=================') clearvars -regexp '.+Fcn$' % clear the cell array Fcns in the current workspace - diff --git a/CoreProtocols/PupillaryReflex.m b/CoreProtocols/PupillaryReflex.m new file mode 100644 index 0000000000000000000000000000000000000000..92fc55f78a390ea1e380e9e585126da042892800 --- /dev/null +++ b/CoreProtocols/PupillaryReflex.m @@ -0,0 +1,440 @@ +%========================================================================= +%-----------------------------General Settings---------------------------- +% These settings are make changing the behaviour of the protocol easier. tS +% is just a struct(), so you can add your own switches or values here and +% use them lower down. Some basic switches like saveData, useTask, +% checkKeysDuringstimulus will influence the runeExperiment.runTask() +% functionality, not just the state machine. Other switches like +% includeErrors are referenced in this state machine file to change with +% functions are added to the state machine states… +tS.name = 'Pupillary Reflex'; %==name of this protocol +tS.useTask = true; %==use taskSequence (randomises stimulus variables) +tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? +tS.askForComments = false; %==UI requestor asks for comments before/after run +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.includeErrors = false; %==do we update the trial number even for incorrect saccade/fixate, if true then we call updateTask for both correct and incorrect, otherwise we only call updateTask() for correct responses +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.timeOut = 2; %==if wrong response, how long to time out before next trial +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume + +%================================================================== +%------------ ----DEBUG LOGGING to command window------------------ +% uncomment each line to get specific verbose logging from each of these +% components; you can also set verbose in the opticka GUI to enable all of +% these… +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging + +%================================================================== +%-----------------INITIAL Eyetracker Settings---------------------- +% These settings define the initial fixation window and set up for the +% eyetracker. They may be modified during the task (i.e. moving the +% fixation window towards a target, enabling an exclusion window to stop +% the subject entering a specific set of display areas etc.) +% +% **IMPORTANT**: you must ensure that the global state time is larger than +% any fixation timers specified here. Each state has a global timer, so if +% the state timer is 5 seconds but your fixation timer is 6 seconds, then +% the state will finish before the fixation time was completed! +%------------------------------------------------------------------ +% initial fixation X position in degrees (0° is screen centre). Multiple windows +% can be entered using an array. +tS.fixX = 0; +% initial fixation Y position in degrees (0° is screen centre). Multiple windows +% can be entered using an array. +tS.fixY = 0; +% time to search and enter fixation window (Initiate fixation) +tS.firstFixInit = 3; +% time to maintain initial fixation within window, can be single value or a +% range to randomise between +tS.firstFixTime = [0.35 0.75]; +% fixation window radius in degrees; if you enter [x y] the window will be +% rectangular. +tS.firstFixRadius = 3; +% do we forbid eye to enter-exit-reenter fixation window? +tS.strict = false; +% add an exclusion zone where subject cannot saccade to? +tS.exclusionZone = []; +% time to maintain fixation during stimulus state +tS.stimulusFixTime = 0.5; +% Initialise eyetracker with X, Y, FixInitTime, FixTime, Radius, StrictFix values +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); + +%================================================================== +%-----------------BEAVIOURAL PLOT CONFIGURATION-------------------- +%----WHICH states assigned correct / break for the online plot?---- +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%========================================================================= +%--------------Randomise stimulus variables every trial?----------- +% If you want to have some randomisation of stimuls variables WITHOUT using +% taskSequence task. Remember this will not be "Saved" for later use, if you +% want to do controlled experiments use taskSequence to define proper randomised +% and balanced variable sets and triggers to send to recording equipment etc... +% Good for training tasks, or stimulus variability irrelevant to the task. +% n = 1; +% in(n).name = 'xyPosition'; +% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; +% in(n).stimuli = 1; +% in(n).offset = []; +% stims.stimulusTable = in; +stims.choice = []; +stims.stimulusTable = []; + +%========================================================================= +%-------------allows using arrow keys to control variables?------------- +% another option is to enable manual control of a table of variables +% in-task. This is useful to dynamically probe RF properties or other +% features while still allowing for fixation or other behavioural control. +% Use arrow keys <- -> to control value and ↑ ↓ to control variable. +stims.controlTable = []; +stims.tableChoice = 1; + +%====================================================================== +% this allows us to enable subsets from our stimulus list +stims.stimulusSets = {[1,2],[1]}; +stims.setChoice = 1; + +%========================================================================= +% N x 2 cell array of regexpi strings, list to skip the current -> next +% state's exit functions; for example skipExitStates = +% {'fixate','incorrect|breakfix'}; means that if the currentstate is +% 'fixate' and the next state is either incorrect OR breakfix, then skip +% the FIXATE exit state. Add multiple rows for skipping multiple state's +% exit states. +sM.skipExitStates = {'fixate','incorrect|breakfix'}; + +%========================================================================= +% which stimulus in the list is used for a fixation target? For this +% protocol it means the subject must saccade to this stimulus (the saccade +% target is #1 in the list) to get the reward. +stims.fixationChoice = 1; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of anonymous function handles which are +% executed by the state machine to control the experiment. The state +% machine can run sets at entry ['entryFcn'], during ['withinFcn'], to +% trigger a transition jump to another state ['transitionFcn'], and at exit +% ['exitFcn'. Remember these {sets} need to access the objects that are +% available within the runExperiment context (see top of file). You can +% also add global variables/objects then use these. The values entered here +% are set on load, if you want up-to-date values then you need to use +% methods/function wrappers to retrieve/set them. +%========================================================================= + +%============================================================== +%========================================================PAUSE +%============================================================== + +%--------------------pause entry +pauseEntryFcn = { + @()hide(stims); % hide all stimuli + @()drawBackground(s); % blank the subject display + @()drawPhotoDiodeSquare(s,[0 0 0]); % draw black photodiode + @()drawTextNow(s,'PAUSED, press [p] to resume...'); + @()disp('PAUSED, press [p] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', stims.stimulusPositions); + @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false, 0); % no need to flip the PTB screen or tracker + @()needEyeSample(me, false); % no need to check eye position +}; + +%--------------------pause exit +pauseExitFcn = { + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); +}; + +%============================================================== +%====================================================PRE-FIXATION +%============================================================== +%--------------------prefixate entry +prefixEntryFcn = { + @()needFlip(me, true, 1); % enable the screen and trackerscreen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims); % make a struct eT can use for drawing stim positions + @()hide(stims); % hide all stimuli + @()resetAll(eT); % reset all fixation markers to initial state + % update the fixation window to initial values + @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window + % send the trial start messages to the eyetracker + @()trackerTrialStart(eT, getTaskIndex(me)); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + % you can add any other messages, such as stimulus values as needed, + % e.g. @()trackerMessage(eT,['MSG:ANGLE' num2str(stims{1}.angleOut)]) etc. +}; + +%--------------------prefixate within +prefixFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------prefixate exit +prefixExitFcn = { + @()logRun(me,'INITFIX'); + @()trackerMessage(eT,'MSG:Start Fix'); + @()trackerDrawStatus(eT,'Start trial...', stims.stimulusPositions, 0); +}; + +%============================================================== +%====================================================FIXATION +%============================================================== +%--------------------fixate entry +fixEntryFcn = { + @()show(stims{tS.nStims}); % show last stim which is usually fixation cross +}; + +%--------------------fix within +fixFcn = { + @()draw(stims); %draw stimuli + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------test we are fixated for a certain length of time +inFixFcn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testSearchHoldFixation(eT,'stimulus','breakfix') +}; + +%--------------------exit fixation phase +fixExitFcn = { + @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); + @()show(stims); % show all stims + @()trackerMessage(eT,'END_FIX'); %eyetracker message saved to data stream +}; + +%======================================================== +%========================================================STIMULUS +%======================================================== + +stimEntryFcn = { + % send an eyeTracker sync message (reset relative time to 0 after first flip of this state) + @()doSyncTime(me); + % send stimulus value strobe (value set by updateVariables(me) function) + @()doStrobe(me,true); +}; + +%--------------------what to run when we are showing stimuli +stimFcn = { + @()draw(stims); + @()drawPhotoDiodeSquare(s,[1 1 1]); + @()animate(stims); % animate stimuli for subsequent draw +}; + +%-----------------------test we are maintaining fixation +maintainFixFcn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testHoldFixation(eT,'correct','incorrect'); +}; + +%as we exit stim presentation state +stimExitFcn = { + @()setStrobeValue(me, 255); % 255 indicates stimulus OFF + @()doStrobe(me, true); +}; + +%======================================================== +%========================================================DECISIONS +%======================================================== + +%========================================================CORRECT +%--------------------if the subject is correct (small reward) +correctEntryFcn = { + @()trackerTrialEnd(eT, tS.CORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()hide(stims); % hide all stims +}; + +%--------------------correct stimulus +correctFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------when we exit the correct state +correctExitFcn = { + @()giveReward(rM); % send a reward + @()beep(aM, tS.correctSound); % correct beep + @()logRun(me,'CORRECT'); % print current trial info + @()trackerDrawStatus(eT, 'CORRECT! :-)',stims.stimulusPositions,0); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updatePlot(bR, me); % must run before updateTask + @()updateTask(me,tS.CORRECT); % make sure our taskSequence is moved to the next trial + @()updateVariables(me); % randomise our stimuli, and set strobe value too + @()update(stims); % update our stimuli ready for display + @()plot(bR, 1); % actually do our behaviour record drawing +}; + +%========================================================INCORRECT/BREAKFIX +%--------------------incorrect entry +incEntryFcn = { + @()trackerTrialEnd(eT, tS.INCORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; +%--------------------break entry +breakEntryFcn = { + @()trackerTrialEnd(eT, tS.BREAKFIX); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; + +%--------------------our incorrect/breakfix stimulus +incFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------incorrect exit +incExitFcn = { + % tS.includeErrors will prepend some code here... + @()beep(aM, tS.errorSound); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; +%--------------------break exit +breakExitFcn = { + % tS.includeErrors will prepend some code here... + @()beep(aM, tS.errorSound); + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()getStimulusPositions(stims); % make a struct the eT can use for drawing stim positions + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; + +%--------------------change functions based on tS settings +% we use tS options to change the function lists run by the state machine. +% We can prepend or append new functions to the cell arrays. +% +% logRun = add current info to behaviural record +% updatePlot = updates the behavioural record +% updateTask = updates task object +% resetRun = randomise current trial within the block (makes it harder for +% subject to guess based on previous failed trial. +% checkTaskEnded = see if taskSequence has finished +if tS.includeErrors % we want to update our task even if there were errors + incExitFcn = [ {@()logRun(me,'INCORRECT'); @()updatePlot(bR, me); @()updateTask(me,tS.INCORRECT)}; incExitFcn ]; %update our taskSequence + breakExitFcn = [ {@()logRun(me,'BREAK_FIX'); @()updatePlot(bR, me); @()updateTask(me,tS.BREAKFIX)}; breakExitFcn ]; %update our taskSequence +else + incExitFcn = [ {@()logRun(me,'INCORRECT'); @()updatePlot(bR, me); @()resetRun(task)}; incExitFcn ]; + breakExitFcn = [ {@()logRun(me,'BREAK_FIX'); @()updatePlot(bR, me); @()resetRun(task)}; breakExitFcn ]; +end +if tS.useTask || task.nBlocks > 0 + correctExitFcn = [ correctExitFcn; {@()checkTaskEnded(me)} ]; + incExitFcn = [ incExitFcn; {@()checkTaskEnded(me)} ]; + breakExitFcn = [ breakExitFcn; {@()checkTaskEnded(me)} ]; +end + +%======================================================== +%========================================================EYETRACKER +%======================================================== +%--------------------calibration function +calibrateFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; + +%--------------------drift correction function +driftFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [others ignores this] + @()setOffline(eT); % set eyelink offline [others ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) +}; +offsetFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; + +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values + +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine +stateInfoTmp = { +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFcn {} {} pauseExitFcn; +%--------------------------------------------------------------------------------------------- +'prefix' 'fixate' 2 prefixEntryFcn prefixFcn {} {}; +'fixate' 'incorrect' 10 fixEntryFcn fixFcn inFixFcn fixExitFcn; +'stimulus' 'incorrect' 10 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; +'correct' 'prefix' 0.1 correctEntryFcn correctFcn {} correctExitFcn; +'incorrect' 'timeout' 0.1 incEntryFcn incFcn {} incExitFcn; +'breakfix' 'timeout' 0.1 breakEntryFcn incFcn {} breakExitFcn; +'timeout' 'prefix' tS.timeOut {} incFcn {} {}; +%--------------------------------------------------------------------------------------------- +'calibrate' 'pause' 0.5 calibrateFcn {} {} {}; +'drift' 'pause' 0.5 driftFcn {} {} {}; +'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFcn {} {} {}; +'flash' 'pause' 0.5 flashFcn {} {} {}; +'showgrid' 'pause' 10 {} gridFcn {} {}; +}; +%--------------------------State Machine Table----------------------------- +%========================================================================== + +disp('=================>> Built state info file <<==================') +disp(stateInfoTmp) +disp('=================>> Built state info file <<=================') +clearvars -regexp '.+Fcn$' % clear the cell array Fcns in the current workspace diff --git a/CoreProtocols/PupillaryReflex.mat b/CoreProtocols/PupillaryReflex.mat new file mode 100644 index 0000000000000000000000000000000000000000..4aff67d25835645497796a0e37f5acbcd79797f5 Binary files /dev/null and b/CoreProtocols/PupillaryReflex.mat differ diff --git a/CoreProtocols/RFLocaliser.mat b/CoreProtocols/RFLocaliser.mat index c4060f8d4ca1e60fafc3100de7b995d921ba54eb..3cf0422f3327ad9cfba61ef628b2f4f7dcff6e4d 100644 Binary files a/CoreProtocols/RFLocaliser.mat and b/CoreProtocols/RFLocaliser.mat differ diff --git a/CoreProtocols/RFLocaliserStateInfo.m b/CoreProtocols/RFLocaliserStateInfo.m index 48607a5b26c84a8b8f6885a760038ec5ea3bdee7..6622b066e39b12b0d13c35fd1283a3767a3c5c4a 100644 --- a/CoreProtocols/RFLocaliserStateInfo.m +++ b/CoreProtocols/RFLocaliserStateInfo.m @@ -2,6 +2,10 @@ % visual responses from a wide range of stimulus classes while a subject % maintains fixation. % +% SEE keyboard mapping for the keyboard control keys to use. Basically the +% arrow keys to change variable types and values, < and > to change stimulus +% type, s to show/hide the mouse cursor. +% % This protocol uses mouse and keyboard control of 10 different classes of % stimuli (see opticka Stimulus List. You can change which stimulus and % what variables are during the task, while the subject maintains fixation. @@ -27,19 +31,21 @@ % tS = general structure to hold general variables, will be saved as part of the data %------------General Settings----------------- -tS.useTask = false; %==use taskSequence (randomised stimulus variable task object) -tS.rewardTime = 100; %==TTL time in milliseconds -tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. -tS.checkKeysDuringStimulus = true; %==allow keyboard control? Slight drop in performance -tS.recordEyePosition = false; %==record eye position within PTB, **in addition** to the EDF? -tS.askForComments = false; %==little UI requestor asks for comments before/after run -tS.saveData = false; %==we don't want to save any data tS.name = 'RF Localiser'; %==name of this protocol +tS.useTask = false; %==use taskSequence (randomised stimulus variable task object) +tS.keyExclusionPattern = []; %==which states to skip keyboard checking +tS.enableTrainingKeys = true; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? +tS.askForComments = false; %==UI requestor asks for comments before/after run +tS.saveData = false; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… tS.nStims = stims.n; %==number of stimuli tS.tOut = 5; %==if breakfix response, how long to timeout before next trial tS.CORRECT = 1; %==the code to send eyetracker for correct trials tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume %================================================================== %----------------Debug logging to command window------------------ @@ -49,7 +55,7 @@ tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials %sM.verbose = true; %==print out stateMachine info for debugging %stims.verbose = true; %==print out metaStimulus info for debugging %io.verbose = true; %==print out io commands for debugging -%eT.verbose = true; %==print out eyelink commands for debugging +%eT.verbose = true; %==print out eyetracker commands for debugging %rM.verbose = true; %==print out reward commands for debugging %task.verbose = true; %==print out task info for debugging @@ -58,93 +64,33 @@ tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials tS.fixX = 0; % X position in degrees tS.fixY = 0; % X position in degrees tS.firstFixInit = 3; % time to search and enter fixation window -tS.firstFixTime = 0.5; % time to maintain fixation within windo -tS.firstFixRadius = 6; % radius in degrees +tS.firstFixTime = 0.2; % time to maintain fixation within windo +tS.firstFixRadius = 10; % radius in degrees tS.strict = true; % do we forbid eye to enter-exit-reenter fixation window? tS.exclusionZone = []; % do we add an exclusion zone where subject cannot saccade to... -tS.stimulusFixTime = 2.5; % time to fix on the stimulus -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; - -%================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options -% here they will OVERRIDE the GUI ones; if they are commented then the GUI -% options are used. me.elsettings and me.tobiisettings contain the GUI -% settings you can test if they are empty or not and set them based on -% that... -eT.name = tS.name; -if tS.saveData == true; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - eT.name = tS.name; - if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? - if tS.saveData == true; eT.recordData = true; end %===save EDF file? - if isempty(me.eyetracker.esettings) %==check if GUI settings are empty - eT.sampleRate = 250; %==sampling rate - eT.calibrationStyle = 'HV5'; %==calibration style - eT.calibrationProportion = [0.4 0.4]; %==the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation use 1-9 to show each dot, space to select fix as valid, - % INS key ON EYELINK KEYBOARD to accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; %==calibration target colour - eT.modify.calibrationtargetsize = 2; %==size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; %==width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; %==-1 = use any attachedkeyboard - eT.modify.targetbeep = 1; %==beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - eT.name = tS.name; - if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? - if isempty(me.eyetracker.tsettings) %==check if GUI settings are empty - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - eT.valPositions = [ .5 .5 ]; - end -end - -%Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix -eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%make sure we don't start with any exclusion zones set up -eT.resetExclusionZones(); +tS.stimulusFixTime = 3; % time to fix while showing stimulus +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); %================================================================== %----which states assigned as correct or break for online plot?---- bR.correctStateName = "correct"; bR.breakStateName = ["breakfix","incorrect"]; -%================================================================== -%-------------------randomise stimulus variables every trial?----------- -% if you want to have some randomisation of stimuls variables without using -% a taskSequence task, you can uncomment this and runExperiment can use -% this structure to change e.g. X or Y position, size, angle see -% metaStimulus for more details. Remember this will not be "Saved" for -% later use, if you want to do controlled experiments use taskSequence to -% define proper randomised and balanced variable sets and triggers to send -% to recording equipment etc... -% -% n = 1; -% in(n).name = 'xyPosition'; -% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; -% in(n).stimuli = 1; -% in(n).offset = []; -% stims.stimulusTable = in; -stims.choice = []; -stims.stimulusTable = []; +%========================================================================= +%--------------Randomise stimulus variables every trial?----------- +% If you want to have some randomisation of stimuls variables WITHOUT using +% taskSequence task. Remember this will not be "Saved" for later use, if you +% want to do controlled experiments use taskSequence to define proper randomised +% and balanced variable sets and triggers to send to recording equipment etc... +% Good for training tasks, or stimulus variability irrelevant to the task. +% n = 1; +% in(n).name = 'xyPosition'; +% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; +% in(n).stimuli = 1; +% in(n).offset = []; +% stims.stimulusTable = in; +stims.choice = []; +stims.stimulusTable = []; %================================================================== %-------------allows using arrow keys to control variables?------------- @@ -156,28 +102,28 @@ stims.tableChoice = 1; n=1; stims.controlTable(n).variable = 'angle'; stims.controlTable(n).delta = 15; -stims.controlTable(n).stimuli = [7 8 9 10]; +stims.controlTable(n).stimuli = [1 7 8 9 10]; stims.controlTable(n).limits = [0 360]; n=n+1; stims.controlTable(n).variable = 'size'; stims.controlTable(n).delta = 0.25; -stims.controlTable(n).stimuli = [2 3 4 5 6 7 8 10]; -stims.controlTable(n).limits = [0.25 25]; +stims.controlTable(n).stimuli = [1 2 3 4 5 6 7 8 10]; +stims.controlTable(n).limits = [0.25 50]; n=n+1; stims.controlTable(n).variable = 'flashTime'; stims.controlTable(n).delta = 0.1; -stims.controlTable(n).stimuli = [1 2 3 4 5 6]; +stims.controlTable(n).stimuli = [2 3 4 5 6]; stims.controlTable(n).limits = [0.05 1.05]; n=n+1; stims.controlTable(n).variable = 'barHeight'; stims.controlTable(n).delta = 1; -stims.controlTable(n).stimuli = [8 9]; -stims.controlTable(n).limits = [0.5 15]; +stims.controlTable(n).stimuli = [1 8 9]; +stims.controlTable(n).limits = [0.5 50]; n=n+1; stims.controlTable(n).variable = 'barWidth'; stims.controlTable(n).delta = 0.25; -stims.controlTable(n).stimuli = [8 9]; -stims.controlTable(n).limits = [0.25 8.25]; +stims.controlTable(n).stimuli = [1 8 9]; +stims.controlTable(n).limits = [0.25 50]; n=n+1; stims.controlTable(n).variable = 'tf'; stims.controlTable(n).delta = 0.1; @@ -205,10 +151,9 @@ stims.controlTable(n).stimuli = [10]; stims.controlTable(n).limits = [0.02 0.5]; %------this allows us to enable subsets from our stimulus list -stims.stimulusSets = {[11], [1 11], [2 11], [3 11], [4 11], [5 11],... - [6 11], [7 11], [8 11], [9 11], [10 11]}; -stims.setChoice = 3; -showSet(stims); +stims.stimulusSets = {[1 11], [2 11], [3 11], [4 11], [5 11],... + [6 11], [7 11], [8 11], [9 11], [10 11], 11}; +stims.setChoice = 7; %----------------------State Machine States------------------------- % each cell {array} holds a set of anonymous function handles which are executed by the @@ -222,72 +167,69 @@ showSet(stims); %====================enter pause state pauseEntryFcn = { + @()hide(stims); % hide all stimuli @()drawBackground(s); %blank the subject display @()drawTextNow(s,'PAUSED, press [p] to resume...'); @()disp('PAUSED, press [p] to resume...'); - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawText(eT,'PAUSED, press [P] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume'); @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()resetAll(eT); % reset all fixation markers to initial state @()setOffline(eT); % set eyelink offline [tobii ignores this] @()stopRecording(eT, true); %stop recording eye position data @()needFlip(me, false); % no need to flip the PTB screen - @()needEyeSample(me,false); % no need to check eye position + @()needEyeSample(me, false); % no need to check eye position }; %--------------------exit pause state pauseExitFcn = { - @()showSet(stims,3); + @()startRecording(eT, true); + @()showSet(stims, 3); @()fprintf('\n===>>>EXIT PAUSE STATE\n') @()needFlip(me, true); % start PTB screen flips }; %====================prefix entry state prefixEntryFcn = { - @()needFlip(me, true); + @()resetAll(eT); % reset all fixation markers to initial state + @()needFlip(me, true, 1); % enable the screen and trackerscreen flip + @()needEyeSample(me, true); % make sure we start measuring eye position @()startRecording(eT); %start recording eye position data again }; %--------------------prefix within state prefixFcn = { @()drawBackground(s); - @()drawMousePosition(s,true); - @()drawText(s,'PREFIX'); }; %--------------------prefix exit state prefixExitFcn = { - @()resetFixationHistory(eT); % reset the recent eye position history - @()resetExclusionZones(eT); % reset any exclusion zones on eyetracker - @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window + @()updateFixationValues(eT,[],[],[],tS.firstFixTime); %reset fixation time for stimulus = tS.stimulusFixTime @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawFixation(eT); % draw the fixation window - @()statusMessage(eT,'Initiate Fixation...'); %status text on the eyelink - @()needEyeSample(me,true); % make sure we start measuring eye position }; %====================fixate entry fixEntryFcn = { - + @()trackerDrawStatus(eT,'Fixate', stims.stimulusPositions); }; %--------------------fix within fixFcn = { @()draw(stims{11}); %draw stimulus - @()drawMousePosition(s,true); + @()animate(stims{11}); % animate stimuli for subsequent draw + @()drawMousePosition(s); }; %--------------------test we are fixated for a certain length of time inFixFcn = { - @()testSearchHoldFixation(eT,'stimulus','incorrect') + @()testSearchHoldFixation(eT,'stimulus','breakfix') }; %--------------------exit fixation phase fixExitFcn = { - @()statusMessage(eT,'Show Stimulus...'); @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); %reset fixation time for stimulus = tS.stimulusFixTime + @()trackerDrawStatus(eT,'Stimulus', stims.stimulusPositions); @()trackerMessage(eT,'END_FIX'); }; @@ -299,71 +241,70 @@ stimEntryFcn = { %---------------------stimulus within state stimFcn = { @()draw(stims); % draw the stimuli - @()drawMousePosition(s); @()animate(stims); % animate stimuli for subsequent draw + @()drawMousePosition(s); }; %--------------------test we are maintaining fixation maintainFixFcn = { - @()testHoldFixation(eT,'correct','breakfix'); + @()testHoldFixation(eT,'correct','incorrect'); }; %--------------------as we exit stim presentation state stimExitFcn = { @()setStrobeValue(me,255); @()doStrobe(me,true); - @()mousePosition(s,true); %this just prints the current mouse position to the command window }; %====================if the subject is correct (small reward) correctEntryFcn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM,2000,0.1,0.1); % correct beep @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Correct! :-)'); + @()trackerDrawStatus(eT, 'CORRECT! :-)'); @()stopRecording(eT); @()setOffline(eT); % set eyelink offline [tobii ignores this] @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()needFlip(me, true, 0); % enable the screen but not trackerscreen flip @()logRun(me,'CORRECT'); % log start to command window }; %--------------------correct stimulus correctFcn = { @()drawBackground(s); - @()drawMousePosition(s,true); }; correctExitFcn = { + @()giveReward(rM); % send a reward TTL + @()beep(aM, tS.correctSound); % correct beep + @()mousePosition(s,true); %this just prints the current mouse position to the command window @()updatePlot(bR, me); @()update(stims); - @()drawnow; + @()plot(bR, 1); % actually do our behaviour record drawing }; %====================break entry breakEntryFcn = { - @()beep(aM,400,0.5,1); + @()beep(aM,tS.errorSound); @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.BREAKFIX)); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'BREAK! :-('); + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', [], 0); @()stopRecording(eT); @()setOffline(eT); % set eyelink offline [tobii ignores this] @()needEyeSample(me,false); + @()needFlip(me, true, 0); % enable the screen but not trackerscreen flip @()logRun(me,'BREAK'); % log start to command window }; %--------------------incorrect entry incorrEntryFcn = { - @()beep(aM,400,0.5,1); + @()beep(aM,tS.errorSound); @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Incorrect! :-('); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); @()stopRecording(eT); @()setOffline(eT); % set eyelink offline [tobii ignores this] @()needEyeSample(me,false); + @()needFlip(me, true, 0); % enable the screen but not trackerscreen flip @()logRun(me,'INCORRECT'); % log start to command window }; @@ -373,22 +314,33 @@ breakFcn = { @()drawMousePosition(s,true); }; +%--------------------our incorrect stimulus +tOutFcn = { + @()drawBackground(s); + @()drawText(s,'Timeout'); + @()drawMousePosition(s,true); +}; + %--------------------when we exit the incorrect/breakfix state ExitFcn = { + @()mousePosition(s,true); %this just prints the current mouse position to the command window @()updatePlot(bR, me); @()update(stims); - @()drawnow; + @()plot(bR, 1); % actually do our behaviour record drawing }; -%====================calibration function -calibrateFcn = { +%======================================================== +%========================================================EYETRACKER +%======================================================== +%--------------------calibration function +calibrateFcn = { @()drawBackground(s); %blank the display @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()trackerSetup(eT) % enter tracker calibrate/validate setup mode + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode }; -%====================drift correction function +%--------------------drift correction function driftFcn = { @()drawBackground(s); %blank the display @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @@ -402,22 +354,17 @@ offsetFcn = { @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) }; -%====================screenflash -flashFcn = { - @()drawBackground(s); - @()flashScreen(s, 0.2); % fullscreen flash mode for visual background activity detection -}; +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values -%====================allow override -overrideFcn = { - @()keyOverride(me); -}; +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection -%====================show 1deg size grid -gridFcn = { - @()drawGrid(s); - @()drawScreenCenter(s); -}; +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; % N x 2 cell array of regexpi strings, list to skip the current -> next state's exit functions; for example % skipExitStates = {'fixate','incorrect|breakfix'}; means that if the currentstate is @@ -426,21 +373,28 @@ gridFcn = { sM.skipExitStates = {'fixate','incorrect|breakfix'}; -%============================================================================== -%----------------------State Machine Table------------------------- +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- % specify our cell array that is read by the stateMachine stateInfoTmp = { -'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- 'pause' 'prefix' inf pauseEntryFcn {} {} pauseExitFcn; -'prefix' 'fixate' 0.5 prefixEntryFcn prefixFcn {} prefixExitFcn; -'fixate' 'incorrect' 5 fixEntryFcn fixFcn inFixFcn fixExitFcn; -'stimulus' 'incorrect' 5 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; -'incorrect' 'timeout' 0.5 incorrEntryFcn breakFcn {} ExitFcn; -'breakfix' 'timeout' 0.5 breakEntryFcn breakFcn {} ExitFcn; -'correct' 'prefix' 0.5 correctEntryFcn correctFcn {} correctExitFcn; -'timeout' 'prefix' tS.tOut {} breakFcn {} {}; +%--------------------------------------------------------------------------------------------- +'prefix' 'fixate' 1 prefixEntryFcn prefixFcn {} prefixExitFcn; +'fixate' 'incorrect' 10 fixEntryFcn fixFcn inFixFcn fixExitFcn; +'stimulus' 'incorrect' 10 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; +'incorrect' 'timeout' 0.1 incorrEntryFcn breakFcn {} ExitFcn; +'breakfix' 'timeout' 0.1 breakEntryFcn breakFcn {} ExitFcn; +'correct' 'prefix' 0.1 correctEntryFcn correctFcn {} correctExitFcn; +'timeout' 'prefix' tS.tOut {} tOutFcn {} {}; +%--------------------------------------------------------------------------------------------- 'calibrate' 'pause' 0.5 calibrateFcn {} {} {}; 'drift' 'pause' 0.5 driftFcn {} {} {}; +'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- 'flash' 'pause' 0.5 {} flashFcn {} {}; 'override' 'pause' 0.5 {} overrideFcn {} {}; 'showgrid' 'pause' 1 {} gridFcn {} {}; diff --git a/CoreProtocols/RevCorStateInfo.m b/CoreProtocols/RevCorStateInfo.m new file mode 100644 index 0000000000000000000000000000000000000000..116bdc38ef4ec195b7b3833b2a01ec54e26b33dc --- /dev/null +++ b/CoreProtocols/RevCorStateInfo.m @@ -0,0 +1,492 @@ +%> REVERSE CORRELATION PROTOCOL +% The following class objects are already loaded by runTask() and available +% to use; each object has methods (functions) useful for running the task: +% +% me = runExperiment object ('self' in OOP terminology) +% s = screenManager object +% aM = audioManager object +% stims = our list of stimuli (metaStimulus class) +% sM = State Machine (stateMachine class) +% task = task sequence (taskSequence class) +% eT = eyetracker manager +% io = digital I/O to recording system +% rM = Reward Manager (LabJack or Arduino TTL trigger to reward system/Magstim) +% bR = behavioural record plot (on-screen GUI during a task run) +% uF = user functions - add your own functions to this class +% tS = structure to hold general variables, will be saved as part of the data + +%================================================================== +%------------------------General Settings-------------------------- +% These settings are make changing the behaviour of the protocol easier. tS +% is just a struct(), so you can add your own switches or values here and +% use them lower down. Some basic switches like saveData, useTask, +% checkKeysDuringstimulus will influence the runeExperiment.runTask() +% functionality, not just the state machine. Other switches like +% includeErrors are referenced in this state machine file to change with +% functions are added to the state machine states… +tS.useTask = false; %==use taskSequence (randomises stimulus variables) +tS.rewardTime = 250; %==TTL time in milliseconds +tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. +tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking +tS.enableTrainingKeys = true; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? +tS.askForComments = false; %==UI requestor asks for comments before/after run +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.includeErrors = false; %==do we update the trial number even for incorrect saccade/fixate, if true then we call updateTask for both correct and incorrect, otherwise we only call updateTask() for correct responses +tS.name = 'REVCOR'; %==name of this protocol +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.tOut = 2; %==if wrong response, how long to time out before next trial +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials + +%================================================================= +%----------------Debug logging to command window------------------ging the behaviour of the protocol easier. tS +% is just a struct(), so you c +% uncomment each line to get specific verbose logging from each of these +% components; you can also set verbose in the opticka GUI to enable all of +% these… +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging + +%================================================================== +%-----------------INITIAL Eyetracker Settings---------------------- +% These settings define the initial fixation window and set up for the +% eyetracker. They may be modified during the task (i.e. moving the +% fixation window towards a target, enabling an exclusion window to stop +% the subject entering a specific set of display areas etc.) +% +% IMPORTANT: you need to make sure that the global state time is larger +% than the fixation timers specified here. Each state has a global timer, +% so if the state timer is 5 seconds but your fixation timer is 6 seconds, +% then the state will finish before the fixation time was completed! + +% initial fixation X position in degrees (0° is screen centre) +tS.fixX = 0; +% initial fixation Y position in degrees +tS.fixY = 0; +% time to search and enter fixation window +tS.firstFixInit = 3; +% time to maintain initial fixation within window, can be single value or a +% range to randomise between +tS.firstFixTime = 0.5; +% circular fixation window radius in degrees +tS.firstFixRadius = 2; +% do we forbid eye to enter-exit-reenter fixation window? +tS.strict = true; +% do we add an exclusion zone where subject cannot saccade to... +tS.exclusionZone = []; +% time to fix on the stimulus +tS.stimulusFixTime = 2; +% log of recent X and Y position, and exclusion zone, here set ti initial +% values +me.lastXPosition = tS.fixX; +me.lastYPosition = tS.fixY; +me.lastXExclusion = []; +me.lastYExclusion = []; + +%========================================================================= +%-----------------INITIAL Eyetracker Settings---------------------- +% These settings define the initial fixation window and set up for the +% eyetracker. They may be modified during the task (i.e. moving the fixation +% window towards a target, enabling an exclusion window to stop the subject +% entering a specific set of display areas etc.) +% +% **IMPORTANT**: you need to make sure that the global state time is larger than +% any fixation timers specified here. Each state has a global timer, so if the +% state timer is 5 seconds but your fixation timer is 6 seconds, then the state +% will finish before the fixation time was completed! +%------------------------------------------------------------------ +% initial fixation X position in degrees (0° is screen centre). Multiple windows +% can be entered using an array. +tS.fixX = 0; +% initial fixation Y position in degrees (0° is screen centre). Multiple windows +% can be entered using an array. +tS.fixY = 0; +% time to search and enter fixation window (Initiate fixation) +tS.firstFixInit = 3; +% time to maintain initial fixation within window, can be single value or a +% range to randomise between +tS.firstFixTime = [0.25 0.75]; +% fixation window radius in degrees; if you enter [x y] the window will be +% rectangular. +tS.firstFixRadius = 2; +% do we forbid eye to enter-exit-reenter fixation window? +tS.strict = true; +% add an exclusion zone where subject cannot saccade to? +tS.exclusionZone = []; +% time to maintain fixation during stimulus state +tS.stimulusFixTime = 1.036; +% Initialise eyetracker with X, Y, FixInitTime, FixTime, Radius, StrictFix values +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); + +%========================================================================= +%-------------------------ONLINE Behaviour Plot--------------------------- +% WHICH states assigned as correct or break for online plot? +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%========================================================================= +%--------------Randomise stimulus variables every trial?----------- +% If you want to have some randomisation of stimuls variables WITHOUT using +% taskSequence task. Remember this will not be "Saved" for later use, if you +% want to do controlled experiments use taskSequence to define proper randomised +% and balanced variable sets and triggers to send to recording equipment etc... +% Good for training tasks, or stimulus variability irrelevant to the task. +% n = 1; +% in(n).name = 'xyPosition'; +% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; +% in(n).stimuli = 1; +% in(n).offset = []; +% stims.stimulusTable = in; +stims.choice = []; +stims.stimulusTable = []; + +%========================================================================= +%-------------allows using arrow keys to control variables?------------- +% another option is to enable manual control of a table of variables +% this is useful to probe RF properties or other features while still +% allowing for fixation or other behavioural control. +% Use arrow keys <- -> to control value and ↑ ↓ to control variable. +stims.controlTable = []; +stims.tableChoice = 1; + +%====================================================================== +% this allows us to enable subsets from our stimulus list +% 1 = grating | 2 = fixation cross +stims.stimulusSets = {[1,2],[1]}; +stims.setChoice = 1; + +%========================================================================= +% N x 2 cell array of regexpi strings, list to skip the current -> next +% state's exit functions; for example skipExitStates = +% {'fixate','incorrect|breakfix'}; means that if the currentstate is +% 'fixate' and the next state is either incorrect OR breakfix, then skip +% the FIXATE exit state. Add multiple rows for skipping multiple state's +% exit states. +sM.skipExitStates = {'fixate','incorrect|breakfix'}; + +%========================================================================= +% which stimulus in the list is used for a fixation target? For this +% protocol it means the subject must saccade to this stimulus (the saccade +% target is #1 in the list) to get the reward. +stims.fixationChoice = 1; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of anonymous function handles which are +% executed by the state machine to control the experiment. The state +% machine can run sets at entry ['entryFcn'], during ['withinFcn'], to +% trigger a transition jump to another state ['transitionFcn'], and at exit +% ['exitFcn'. Remember these {sets} need to access the objects that are +% available within the runExperiment context (see top of file). You can +% also add global variables/objects then use these. The values entered here +% are set on load, if you want up-to-date values then you need to use +% methods/function wrappers to retrieve/set them. +%========================================================================= + +%============================================================== +%========================================================PAUSE +%============================================================== + +%--------------------pause entry +pauseEntryFcn = { + @()hide(stims); + @()drawBackground(s); %blank the subject display + @()drawPhotoDiodeSquare(s,[0 0 0]); %draw black photodiode + @()drawTextNow(s,'PAUSED, press [p] to resume...'); + @()disp('PAUSED, press [p] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', stims.stimulusPositions); + @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()resetAll(eT); % reset all fixation markers to initial state + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false); % no need to flip the PTB screen + @()needEyeSample(me,false); % no need to check eye position +}; + +%--------------------pause exit +pauseExitFcn = { + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); +}; + +%======================================================== +%========================================================PREFIXATE +%======================================================== +%--------------------prefixate entry +prefixEntryFcn = { + @()needFlip(me, true, 1); % enable the screen and trackerscreen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims); % make a struct eT can use for drawing stim positions + @()hide(stims); % hide all stimuli + % update the fixation window to initial values + @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window + @()trackerTrialStart(eT, getTaskIndex(me)); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure +}; + +%--------------------prefixate within +prefixFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------prefixate exit +prefixExitFcn = { + @()trackerDrawStatus(eT,'Start trial...', stims.stimulusPositions); +}; + +%======================================================== +%========================================================FIXATE +%======================================================== +%--------------------fixate entry +fixEntryFcn = { + @()show(stims{tS.nStims}); + @()logRun(me,'INITFIX'); +}; + +%--------------------fix within +fixFcn = { + @()draw(stims); %draw stimuli + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------test we are fixated for a certain length of time +inFixFcn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testSearchHoldFixation(eT,'stimulus','breakfix') +}; + +%--------------------exit fixation phase +fixExitFcn = { + @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); + @()show(stims); % show all stims + @()trackerMessage(eT,'END_FIX'); %eyetracker message saved to data stream +}; + +%======================================================== +%========================================================STIMULUS +%======================================================== + +stimEntryFcn = { + % send stimulus value strobe (value set by updateVariables(me) function) + @()doStrobe(me,true); +}; + +%--------------------what to run when we are showing stimuli +stimFcn = { + @()draw(stims); + @()drawPhotoDiodeSquare(s,[1 1 1]); +}; + +%-----------------------test we are maintaining fixation +maintainFixFcn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testHoldFixation(eT,'correct','incorrect'); +}; + +%as we exit stim presentation state +stimExitFcn = { + @()setStrobeValue(me, 255); % 255 indicates stimulus OFF + @()doStrobe(me, true); + % this adds stateMachine UUID to revcor stimulus frameLog + @()addTag(stims{1}, sM.currentUUID); +}; + +%======================================================== +%========================================================DECISIONS +%======================================================== + +%========================================================CORRECT +%--------------------if the subject is correct (small reward) +correctEntryFcn = { + @()trackerTrialEnd(eT, tS.CORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()hide(stims); % hide all stims +}; + +%--------------------correct stimulus +correctFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------when we exit the correct state +correctExitFcn = { + @()giveReward(rM); % send a reward + @()beep(aM, tS.correctSound); % correct beep + @()logRun(me,'CORRECT'); % print current trial info + @()trackerDrawStatus(eT, 'CORRECT! :-)'); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updatePlot(bR, me); % must run before updateTask + @()updateTask(me,tS.CORRECT); % make sure our taskSequence is moved to the next trial + @()updateVariables(me); % randomise our stimuli, and set strobe value too + @()update(stims); % update our stimuli ready for display + @()getStimulusPositions(stims); % make a struct the eT can use for drawing stim positions + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our behaviour record drawing +}; + +%========================================================INCORRECT/BREAKFIX +%--------------------incorrect entry +incEntryFcn = { + @()trackerTrialEnd(eT, tS.INCORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; +%--------------------break entry +breakEntryFcn = { + @()trackerTrialEnd(eT, tS.BREAKFIX); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; + +%--------------------our incorrect/breakfix stimulus +incFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------incorrect exit +incExitFcn = { + @()beep(aM, tS.errorSound); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()getStimulusPositions(stims); % make a struct the eT can use for drawing stim positions + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; +%--------------------break exit +breakExitFcn = { + @()beep(aM, tS.errorSound); + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', stims.stimulusPositions, 0); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()getStimulusPositions(stims); % make a struct the eT can use for drawing stim positions + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; + +%--------------------change functions based on tS settings +% we use tS options to change the function lists run by the state machine. +% We can prepend or append new functions to the cell arrays. +% +% logRun = add current info to behaviural record +% updatePlot = updates the behavioural record +% updateTask = updates task object +% resetRun = randomise current trial within the block (makes it harder for +% subject to guess based on previous failed trial. +% checkTaskEnded = see if taskSequence has finished +if tS.includeErrors % we want to update our task even if there were errors + incExitFcn = [ {@()logRun(me,'INCORRECT'); @()updatePlot(bR, me); @()updateTask(me,tS.INCORRECT)}; incExitFcn ]; %update our taskSequence + breakExitFcn = [ {@()logRun(me,'BREAK_FIX'); @()updatePlot(bR, me); @()updateTask(me,tS.BREAKFIX)}; breakExitFcn ]; %update our taskSequence +else + incExitFcn = [ {@()logRun(me,'INCORRECT'); @()updatePlot(bR, me); @()resetRun(task)}; incExitFcn ]; + breakExitFcn = [ {@()logRun(me,'BREAK_FIX'); @()updatePlot(bR, me); @()resetRun(task)}; breakExitFcn ]; +end +if tS.useTask || task.nBlocks > 0 + correctExitFcn = [ correctExitFcn; {@()checkTaskEnded(me)} ]; + incExitFcn = [ incExitFcn; {@()checkTaskEnded(me)} ]; + breakExitFcn = [ breakExitFcn; {@()checkTaskEnded(me)} ]; +end + +%======================================================== +%========================================================EYETRACKER +%======================================================== +%--------------------calibration function +calibrateFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; + +%--------------------drift correction function +driftFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) +}; +offsetFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; + +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values + +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine +stateInfoTmp = { +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFcn {} {} pauseExitFcn; +%--------------------------------------------------------------------------------------------- +'prefix' 'fixate' 0.75 prefixEntryFcn prefixFcn {} {}; +'fixate' 'incorrect' 10 fixEntryFcn fixFcn inFixFcn fixExitFcn; +'stimulus' 'incorrect' 10 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; +'correct' 'prefix' 0.1 correctEntryFcn correctFcn {} correctExitFcn; +'incorrect' 'timeout' 0.1 incEntryFcn incFcn {} incExitFcn; +'breakfix' 'timeout' 0.1 breakEntryFcn incFcn {} breakExitFcn; +'timeout' 'prefix' tS.tOut {} incFcn {} {}; +%--------------------------------------------------------------------------------------------- +'calibrate' 'pause' 0.5 calibrateFcn {} {} {}; +'drift' 'pause' 0.5 driftFcn {} {} {}; +'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFcn {} {} {}; +'flash' 'pause' 0.5 flashFcn {} {} {}; +'showgrid' 'pause' 10 {} gridFcn {} {}; +}; + +%--------------------------State Machine Table----------------------------- +%========================================================================== + +disp('=================>> Built state info file <<==================') +disp(stateInfoTmp) +disp('=================>> Built state info file <<=================') +clearvars -regexp '.+Fn$' % clear the cell array Fns in the current workspace diff --git a/CoreProtocols/ReverseCorrelation.mat b/CoreProtocols/ReverseCorrelation.mat new file mode 100644 index 0000000000000000000000000000000000000000..ee053d49886b9ea8ed501ab0d32027ba04b2693f Binary files /dev/null and b/CoreProtocols/ReverseCorrelation.mat differ diff --git a/CoreProtocols/SFTFStateInfo.m b/CoreProtocols/SFTFStateInfo.m index 339f5232693fbf9cef6fab8d73602106c59b41be..834ce17d3acd7133fb37327f7b292f6ad5a30e97 100644 --- a/CoreProtocols/SFTFStateInfo.m +++ b/CoreProtocols/SFTFStateInfo.m @@ -174,23 +174,54 @@ breakEntryFcn = { @()statusMessage(eT,'Broke Fixation :-('); ...%status message @()hide(obj.stimuli); ... }; -%calibration function -calibrateFcn = { @()setOffline(eT); % set eyelink offline [tobii ignores this] +%======================================================== +%========================================================EYETRACKER +%======================================================== +%--------------------calibration function +calibrateFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; + +%--------------------drift correction function +driftFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [others ignores this] + @()setOffline(eT); % set eyelink offline [others ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) +}; +offsetFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; + +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values -%debug override -overrideFcn = @()keyOverride(obj); %a special mode which enters a matlab debug state so we can manually edit object values +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection -%screenflash -flashFcn = @()flashScreen(s, 0.2); % fullscreen flash mode for visual background activity detection +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; -%show 1deg size grid -gridFcn = @()drawGrid(s); +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%----------------------State Machine Table------------------------- -disp('================>> Building state info file <<================') -%specify our cell array that is read by the stateMachine +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine stateInfoTmp = { ... -'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; ... +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; ... 'pause' 'fixate' inf pauseEntryFcn [] [] pauseExitFcn; ... 'prefix' 'fixate' 0.75 [] prefixFcn [] []; ... 'fixate' 'incorrect' 1 fixEntryFcn fixFcn initFixFcn fixExitFcn; ... @@ -204,8 +235,7 @@ stateInfoTmp = { ... 'showgrid' 'pause' 10 [] gridFcn [] []; ... }; +disp('=================>> Built state info file <<==================') disp(stateInfoTmp) -disp('================>> Loaded state info file <<================') -clear pauseEntryFcn fixEntryFcn fixFcn initFixFcn fixExitFcn stimFcn maintainFixFcn incEntryFcn ... - incFcn incExitFcn breakEntryFcn breakFcn correctEntryFcn correctFcn correctExitFcn ... - calibrateFcn overrideFcn flashFcn gridFcn \ No newline at end of file +disp('=================>> Built state info file <<=================') +clearvars -regexp '.+Fcn$' % clear the cell array Fcns in the current workspace diff --git a/CoreProtocols/Saccade_AntiSaccadeStateInfo.m b/CoreProtocols/SaccadePhospheneStateInfo.m similarity index 50% rename from CoreProtocols/Saccade_AntiSaccadeStateInfo.m rename to CoreProtocols/SaccadePhospheneStateInfo.m index 933815a4563f0afa39124c82d2b75a70490c8f7c..889c8376f839edf7fabdee0568f21b0220760bc2 100644 --- a/CoreProtocols/Saccade_AntiSaccadeStateInfo.m +++ b/CoreProtocols/SaccadePhospheneStateInfo.m @@ -1,22 +1,24 @@ -% SACCADE / ANTISACCADE state file, this gets loaded via runExperiment -% class, runTask() method. This task uses 3 stimuli: (1) pro-saccade target -% (2) anti-saccade target and (3) fixation cross. The task sequence is set -% up to randomise the X position of (1) ±10° on each trial, and (2) has a -% modifier set as the inverse (if (1) is -10° on a trial then (2) becomes -% +10°). For the pro-saccade task, show (1) and hide (2), fixation window -% set on (1) and an exclusion zone set around (2). In the anti-saccade task -% we show (2) and set the opacity of (1) during training to encourage the -% subject to saccade away from (2) towards (1); the fixation and exclusion -% windows keep the same logic as for the pro-saccade condition. -% -% State files control the logic of a behavioural task, switching between states -% and executing functions on ENTER, WITHIN and on EXIT of states. In addition -% there are TRANSITION function which can test things like eye position to -% conditionally jump to another state. This state control file will usually be -% run in the scope of the calling runExperiment.runTask() method and other -% objects will be available at run time (with easy to use names listed below). -% The following class objects are already loaded by runTask() and available to -% use; each object has methods (functions) useful for running the task: +% SACCADE PHOSPHENE TASK: See Chen et al., 2020 From Methods: +% Prior to surgical implantation of the electrode arrays, the monkeys were +% trained on a saccade task, in which they reported the location of a visually +% presented dot on a grey screen (with a background luminance of 16.6 cd/m2) +% with an eye movement. This task consisted of %visual trials and ‘catch +% trials,’ in equal proportion. During visual trials, the animal maintained +% fixation for 300 to 900 ms after fixation onset (uniform distribution). At the +% end of this interval, a circular visual target that varied in colour and had a +% diameter ranging from 0.2° to 0.6° appeared in the bottom-right quadrant of +% the screen, for 120-150 ms (uniform distribution). The animal had to make a +% saccade to the visual target within 250 ms of the onset of the visual target +% for a fluid reward. We used a large target window that spanned the lower right +% quadrant of the computer screen and a portion of the upper right and lower +% left quadrants to study the relation between the RF of stimulated neurons and +% the saccadic endpoint, preventing biases to visual field regions through the +% reward contingency. To calculate the saccadic end point, we calculated the eye +% velocity and determined the mean eye position in a time window when the eye +% was stationary (50-100 ms after the peak velocity). During catch trials, no +% visual target was presented, and the animal maintained fixation. On both +% visual trials and catch trials, reward delivery occurred at 1200 ms after +% fixation onset % % me = runExperiment class object % s = screenManager class object @@ -31,39 +33,18 @@ % tS = structure to hold general variables, will be saved as part of the data % uF = user functions - add your own functions to this class -%================================================================== -%--------------------TASK SPECIFIC CONFIG-------------------------- -tS.name = 'saccade-antisaccade'; %==name of this protocol -% update the trial number for incorrect saccades: if true then we call -% updateTask for both correct and incorrect trials, otherwise we only call +%========================================================================= +%----------------------General Settings---------------------------- +tS.name = 'saccade-to-phosphene'; +% if 'training' then show the saccade target and don't stimuluate, if +% 'stimulate' then we hide the saccade target and stimulate: +tS.type = 'training'; +% which pin to use for stimulation +tS.stimPin = 11; +% includeErrors: update the trial number for incorrect saccades: if true then we +% call updateTask for both correct and incorrect trials, otherwise we only call % updateTask() for correct responses. 'false' is useful during training. tS.includeErrors = false; -% is this run a 'saccade' or 'anti-saccade' task run? -tS.type = 'saccade'; -if strcmp(tS.type,'saccade') - % a flag to conditionally set visualisation on the eye tracker interface - stims{1}.showOnTracker = true; - stims{2}.showOnTracker = false; - tS.targetAlpha1 = 0.25; - tS.targetAlpha2 = 0.75; -else - % a flag to conditionally set visualisation on the eye tracker interface - stims{1}.showOnTracker = false; - stims{2}.showOnTracker = true; - % this can be used during training to keep saccade target visible (i.e. in - % the anti-saccade task the subject must saccade away from the - % anti-saccade target towards to place where the pro-saccade target is, so - % starting training keeping the pro-saccade target visible helps the - % subject understand the task - tS.targetAlpha1 = 0.15; - tS.targetAlpha2 = 0.05; - tS.antitargetAlpha1 = 0.5; - tS.antitargetAlpha2 = 0.75; -end -disp(['\n===>>> Task ' tS.name ' Type:' tS.type ' <<<===\n']) - -%================================================================== -%----------------------General Settings---------------------------- tS.useTask = true; %==use taskSequence (randomises stimulus variables) tS.rewardTime = 250; %==TTL time in milliseconds tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. @@ -71,14 +52,16 @@ tS.checkKeysDuringStimulus = false; %==allow keyboard control within stimulus s tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? tS.askForComments = false; %==UI requestor asks for comments before/after run tS.saveData = true; %==save behavioural and eye movement data? -tS.showBehaviourPlot = false; %==open the behaviourPlot figure? Can cause more memory use +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object tS.tOut = 5; %==if wrong response, how long to time out before next trial tS.CORRECT = 1; %==the code to send eyetracker for correct trials tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume -%================================================================== +%========================================================================= %----------------Debug logging to command window------------------ % uncomment each line to get specific verbose logging from each of these % components; you can also set verbose in the opticka GUI to enable all of @@ -90,148 +73,95 @@ tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials %rM.verbose = true; %==print out reward commands for debugging %task.verbose = true; %==print out task info for debugging -%================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options -% here they will OVERRIDE the GUI ones; if they are commented then the GUI -% options are used. me.elsettings and me.tobiisettings contain the GUI -% settings you can test if they are empty or not and set them based on -% that... -eT.name = tS.name; -if me.eyetracker.dummy; eT.isDummy = true; end %===use dummy or real eyetracker? -if tS.saveData; eT.recordData = true; end %===save Eyetracker data? -if strcmp(me.eyetracker.device, 'eyelink') - warning('Note: this protocol file is optimised for the Tobii eyetracker...') - if isempty(me.eyetracker.esettings) %==check if GUI settings are empty - eT.sampleRate = 250; %==sampling rate - eT.calibrationStyle = 'HV5'; %==calibration style - eT.calibrationProportion = [0.4 0.4]; %==the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation use 1-9 to show each dot, space to select fix as valid, - % INS key ON EYELINK KEYBOARD to accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; %==calibration target colour - eT.modify.calibrationtargetsize = 2; %==size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; %==width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; %==-1 = use any attachedkeyboard - eT.modify.targetbeep = 1; %==beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - if isempty(me.eyetracker.tsettings) %==check if GUI settings are empty - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - eT.valPositions = [ .5 .5 ]; - end -end -%================================================================== +%========================================================================= %-----------------INITIAL Eyetracker Settings---------------------- % These settings define the initial fixation window and set up for the -% eyetracker. They may be modified during the task (i.e. moving the -% fixation window towards a target, enabling an exclusion window to stop -% the subject entering a specific set of display areas etc.) +% eyetracker. They may be modified during the task (i.e. moving the fixation +% window towards a target, enabling an exclusion window to stop the subject +% entering a specific set of display areas etc.) % -% IMPORTANT: you need to make sure that the global state time is larger -% than the fixation timers specified here. Each state has a global timer, -% so if the state timer is 5 seconds but your fixation timer is 6 seconds, -% then the state will finish before the fixation time was completed! - +% **IMPORTANT**: you need to make sure that the global state time is larger than +% any fixation timers specified here. Each state has a global timer, so if the +% state timer is 5 seconds but your fixation timer is 6 seconds, then the state +% will finish before the fixation time was completed! +%------------------------------------------------------------------ % initial fixation X position in degrees (0° is screen centre) -tS.fixX = 0; -% initial fixation Y position in degrees +tS.fixX = 0; +% initial fixation Y position in degrees (0° is screen centre) tS.fixY = 0; -% time to search and enter fixation window +% time to search and enter fixation window (Initiate fixation) tS.firstFixInit = 3; -% time to maintain fixation within window, can be single value or a range -% to randomise between -tS.firstFixTime = 0.5; +% time to maintain initial fixation within window, can be single value or a +% range to randomise between +tS.firstFixTime = [0.3 0.9]; % circular fixation window radius in degrees -tS.firstFixRadius = 1; +tS.firstFixRadius = 2; % do we forbid eye to enter-exit-reenter fixation window? tS.strict = true; -% do we add an exclusion zone where subject cannot saccade to... -tS.exclusionZone = []; -% time to fix on the stimulus -tS.stimulusFixTime = 0.25; -% time to show both fix and stim -tS.fixstimTime = 1.5; -% in this task the subject must saccade to the pro-saccade target location. -% These settings define the rules to "accept" the target fixation as -% correct -tS.targetFixInit = 3; % time to find the target -tS.targetFixTime = 1; % to to maintain fixation on target -tS.targetRadius = 4; %radius to fix within. -% this task will establish an exclusion zone against the anti-saccade -% target for the pro and anti-saccade task. We can change the size of the -% exclusion zone, here set to 5° around the X and Y position of the -% anti-saccade target. -tS.exclusionRadius = 5; -% historical log of X and Y position, and exclusion zone -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; -me.lastXExclusion = []; -me.lastYExclusion = []; -%Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix -eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%Ensure we don't start with any exclusion zones set up -eT.resetExclusionZones(); - -%================================================================== -%----WHICH states assigned as correct or break for online plot?---- -%----You need to use regex patterns for the match (doc regexp)----- +% CATCH TRIAL TIME: +ts.catchTrialTime = 1; +% visual target +tS.targetFixInit = 0.75; % time to find the target +tS.targetFixTime = 0.75; % to to maintain fixation on target +tS.targetRadius = [8 15]; %radius widthxheight to fix within. + + +%========================================================================= +%---------------------------Eyetracker setup----------------------- +% NOTE: the opticka GUI can set eyetracker options too; me.eyetracker.esettings +% and me.eyetracker.tsettings contain the GUI settings. We test if they are +% empty or not and set general values based on that... +eT.name = tS.name; +if me.eyetracker.dummy; eT.isDummy = true; end %===use dummy or real eyetracker? +if tS.saveData; eT.recordData = true; end %===save Eyetracker data? +% Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); +% Ensure we don't start with any exclusion zones etc. set up +resetAll(eT); + +%========================================================================= +%----------------------ONLINE Behaviour Plot----------------------- +% WHICH states assigned as correct or break for online plot? +% You need to use regex patterns for the match (doc regexp). bR.correctStateName = "correct"; bR.breakStateName = ["breakfix","incorrect"]; -%================================================================== -%--------------randomise stimulus variables every trial?----------- -% if you want to have some randomisation of stimuls variables without using -% taskSequence task (i.e. general training tasks), you can uncomment this -% and runExperiment can use this structure to change e.g. X or Y position, -% size, angle see metaStimulus for more details. Remember this will not be -% "Saved" for later use, if you want to do controlled methods of constants -% experiments use taskSequence to define proper randomised and balanced -% variable sets and triggers to send to recording equipment etc... -% -% stims.choice = []; -% n = 1; -% in(n).name = 'xyPosition'; -% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; -% in(n).stimuli = 1; -% in(n).offset = []; -% stims.stimulusTable = in; -stims.choice = []; -stims.stimulusTable = []; - -%================================================================== +%========================================================================= +%--------------Randomise stimulus variables every trial?----------- +% If you want to have some randomisation of stimuls variables WITHOUT using +% taskSequence task. Remember this will not be "Saved" for later use, if you +% want to do controlled experiments use taskSequence to define proper randomised +% and balanced variable sets and triggers to send to recording equipment etc... +% Good for training tasks, or stimulus variability irrelevant to the task. +n = 1; +in(n).name = 'size'; +in(n).values = [0.4 0.6 0.8]; +in(n).stimuli = 1; +in(n).offset = []; +n = n + 1; +in(n).name = 'colour'; +in(n).values = {[0.8 0.3 0.3],[0.3 0.8 0.3],[0.8 0.8 0.3],[0.3 0.3 0.8],[0.3 0.8 0.8]}; +in(n).stimuli = 1; +in(n).offset = []; +stims.stimulusTable = in; + +%========================================================================= %-------------allows using arrow keys to control variables?------------- % another option is to enable manual control of a table of variables % this is useful to probe RF properties or other features while still % allowing for fixation or other behavioural control. -% Use arrow keys <- -> to control value and up/down to control variable +% Use arrow keys <- -> to control value and ↑ ↓ to control variable. stims.controlTable = []; stims.tableChoice = 1; -%================================================================== +%========================================================================= % this allows us to enable subsets from our stimulus list % 1 = saccade target | 2 = anti-saccade target | 3 = fixation cross -stims.stimulusSets = {3, [1 3], [1 2 3], [1 2]}; +stims.stimulusSets = {2, [1 2]}; stims.setChoice = 1; hide(stims); -%================================================================== +%========================================================================= % N x 2 cell array of regexpi strings, list to skip the current -> next % state's exit functions; for example skipExitStates = % {'fixate','incorrect|breakfix'}; means that if the currentstate is @@ -240,18 +170,19 @@ hide(stims); % exit states. sM.skipExitStates = {'fixate','incorrect|breakfix'}; -%================================================================== +%========================================================================= % which stimulus in the list is used for a fixation target? For this % protocol it means the subject must saccade this stimulus (the saccade % target is #1 in the list) to get the reward. Also which stimulus to set an % exclusion zone around (where a saccade into this area causes an immediate % break fixation). stims.fixationChoice = 1; -stims.exclusionChoice = 2; -%=================================================================== -%=================================================================== -%=================================================================== +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= %------------------State Machine Task Functions--------------------- % Each cell {array} holds a set of anonymous function handles which are % executed by the state machine to control the experiment. The state @@ -262,23 +193,25 @@ stims.exclusionChoice = 2; % also add global variables/objects then use these. The values entered here % are set on load, if you want up-to-date values then you need to use % methods/function wrappers to retrieve/set them. -%=================================================================== -%=================================================================== -%=================================================================== +%========================================================================= + +%============================================================== +%========================================================PAUSE +%============================================================== -%====================================================PAUSE %--------------------pause entry pauseEntry = { @()hide(stims); @()drawBackground(s); %blank the subject display @()drawTextNow(s,'PAUSED, press [p] to resume...'); @()disp('PAUSED, press [p] to resume...'); - @()statusMessage(eT,me.name); - @()trackerClearScreen(eT); % blank the eyetracker screen - @()trackerDrawText(eT,'PAUSED, press [P] to resume...'); - @()trackerFlip(eT); + @()trackerClearScreen(eT); + @()trackerDrawText(eT,'PAUSED, press [p] to resume'); + @()statusMessage(eT,'PAUSED'); @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF - @()stopRecording(eT, true); %stop recording eye position data + @()resetAll(eT); % reset all fixation markers to initial state + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii @()needFlip(me, false); % no need to flip the PTB screen @()needEyeSample(me,false); % no need to check eye position }; @@ -288,119 +221,120 @@ pauseExit = { @()startRecording(eT, true); %start recording eye position data again }; +%============================================================== %====================================================PRE-FIXATION +%============================================================== pfEntry = { - @()needFlip(me, true); - @()trackerClearScreen(eT); % blank the eyetracker screen - @()trackerFlip(eT); + @()needEyeSample(me,true); % make sure we start measuring eye position + @()needFlip(me, true); + @()needFlipTracker(me, 2); % eyetracker operator screen flip + @()getStimulusPositions(stims,true); %make a struct the eT can use for drawing stim positions + @()updateFixationValues(eT,tS.fixX,tS.fixY,tS.firstFixInit,tS.firstFixTime,tS.firstFixRadius,tS.strict); %reset fixation window + @()resetFixationHistory(eT); % reset the recent eye position history + @()resetExclusionZones(eT); % reset the exclusion zones on eyetracker + @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); + @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker + @()startRecording(eT); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + % draw general state to the eyetracker display (eyelink or tobii) + @()trackerClearScreen(eT); + @()trackerDrawText(eT,'Pre-fixation'); }; pfWithin = { - @()drawBackground(s); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; pfExit = { - @()startRecording(eT); - @()needEyeSample(me,true); % make sure we start measuring eye position - @()updateFixationValues(eT,tS.fixX,tS.fixY,tS.firstFixInit,tS.firstFixTime,tS.firstFixRadius,tS.strict); %reset fixation window - @()resetFixationHistory(eT); % reset the recent eye position history - @()resetExclusionZones(eT); % reset the exclusion zones on eyetracker - @()getStimulusPositions(stims,true); %make a struct the eT can use for drawing stim positions + }; +%============================================================== %====================================================FIXATION +%============================================================== %--------------------fixate entry fixEntry = { - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker - @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - % draw general state to the eyetracker display (eyelink or tobii) - @()trackerDrawStatus(eT,'Fixating...', stims.stimulusPositions); % show stimulus 3 = fixation cross - @()show(stims, 3); + @()show(stims, 2); + @()trackerDrawStatus(eT,'Fixation...'); + @()statusMessage(eT,'FIXATE'); @()logRun(me,'INITFIX'); %fprintf current trial info to command window + @()updateNextState(me,'trial'); %use taskSequence.trialVar for the next state }; %--------------------fix within fixWithin = { @()draw(stims); %draw stimulus - @()trackerDrawEyePosition(eT); % for tobii - @()trackerFlip(eT, 1); % for tobii + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %--------------------test we are fixated for a certain length of time initFix = { % this command performs the logic to search and then maintain fixation % inside the fixation window. The eyetracker parameters are defined above. - % If the subject does initiate and then maintain fixation, then 'fixstim' + % If the subject does initiate and then maintain fixation, then sM.tempNextState % is returned and the state machine will jump to that state, % otherwise 'incorrect' is returned and the state machine will jump there. + % sM.tempNextState is set using @()updateNextState(me,'trial') above. % If neither condition matches, then the state table below % defines that after 5 seconds we will switch to the incorrect state. - @()testSearchHoldFixation(eT,'fixstim','incorrect') + @()testSearchHoldFixation(eT, sM.tempNextState, 'incorrect') }; -%--------------------exit fixation phase -if strcmpi(tS.type,'saccade') - fixExit = { - @()show(stims, [1 3]); - @()edit(stims,1,'alphaOut',tS.targetAlpha1); - }; -else - fixExit = { - @()show(stims, [1 2 3]); - @()edit(stims,1,'alphaOut',tS.targetAlpha1); - @()edit(stims,2,'alphaOut',tS.antitargetAlpha1) - }; -end +fixExit = { }; -%====================================================FIX + TARGET STIMULUS -fsEntry = { - @()updateFixationValues(eT,[],[],[],0.5); %reset the fixation timer for 0.5 secs - @()logRun(me,'FIX+STIM'); %fprintf current trial info to command window +%============================================================== +%====================================================CATCH TRIAL +%============================================================== +% what to run when we enter the stim presentation state +catchEntry = { + @()updateFixationValues(eT,[],[],[],ts.catchTrialTime); %reset fixation window + @()trackerClearScreen(eT); + @()trackerDrawFixation(eT); + @()doStrobe(me,true); + @()statusMessage(eT,'CATCH TRIAL'); }; -fsWithin = { - @()draw(stims); %draw stimulus - @()trackerDrawEyePosition(eT); - @()trackerFlip(eT, 1); +% what to run when we are showing stimuli +catchWithin = { + @()draw(stims); + @()drawPhotoDiodeSquare(s, [1 1 1]); }; -% test we are fixated for a certain length of time, testHoldFixation assumes -% we are already fixated which we are coming from the fixate state... -fsFix = { - @()testHoldFixation(eT,'stimulus','incorrect') +% test we are finding the new target (stimulus 1, the saccade target) +catchFix = { + @()testHoldFixation(eT,'correct','breakfix'); % tests finding and maintaining fixation }; -% exit fixation phase -fsExit = { - % use our saccade target stimulus for next fix X and Y, see - % stims.fixationChoice above - @()updateFixationTarget(me, tS.useTask, tS.targetFixInit, tS.targetFixTime, tS.targetRadius, tS.strict); - % use our antisaccade target to define the exclusion zone, see - % stims.exclusionChoice above - @()updateExclusionZones(me, tS.useTask, tS.exclusionRadius); - @()trackerMessage(eT,'END_FIX'); - @()hide(stims, 3); +%as we exit catch trial +catchExit = { + @()setStrobeValue(me,255); + @()doStrobe(me,true); }; -if strcmpi(tS.type,'saccade') - fsExit = [ fsExit; { @()edit(stims,1,'alphaOut',tS.targetAlpha2) } ]; -else - fsExit = [ fsExit; { @()edit(stims,1,'alphaOut',tS.targetAlpha2); @()edit(stims,2,'alphaOut',tS.antitargetAlpha2) } ]; -end +%============================================================== %====================================================TARGET STIMULUS ALONE +%============================================================== % what to run when we enter the stim presentation state -stimEntry = { - @()doStrobe(me,true); - @()logRun(me,'STIMULUS'); %fprintf current trial info to command window +stimEntry = { + % use our saccade target stimulus for next fix X and Y, see + % stims.fixationChoice above + @()updateFixationTarget(me, tS.useTask, tS.targetFixInit, tS.targetFixTime, tS.targetRadius); + @()trackerClearScreen(eT); + @()trackerDrawFixation(eT); + @()statusMessage(eT,'SACCADE'); + @()doStrobe(me, true); }; +if matches(tS.type,'training') + stimEntry = [ {@()show(stims)}; stimEntry ]; % make sure our taskSequence is moved to the next trial +else + stimEntry = [ {@()timedTTL(rM,tS.stimPin,2)}; stimEntry ]; % we randomise the run within this block to make it harder to guess next trial +end % what to run when we are showing stimuli -stimWithin = { +stimWithin = { @()draw(stims); - @()animate(stims); % animate stimuli for subsequent draw - @()trackerDrawEyePosition(eT); - @()trackerFlip(eT, 1); + @()drawPhotoDiodeSquare(s,[1 1 1]); }; % test we are finding the new target (stimulus 1, the saccade target) @@ -414,75 +348,88 @@ stimExit = { @()doStrobe(me,true); }; +%============================================================== %====================================================DECISION +%============================================================== +%====================================================CORRECT %if the subject is correct (small reward) correctEntry = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM,2000); % correct beep - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.CORRECT)]); - @()trackerDrawStatus(eT,'Correct! :-)', stims.stimulusPositions); + @()giveReward(rM); % send a reward TTL + @()beep(aM, tS.correctSound); % correct beep + @()trackerMessage(eT,'END_RT'); %send END_RT message to tracker + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); %send TRIAL_RESULT message to tracker + @()trackerDrawStatus(eT,'Correct! :-)',[]); + @()statusMessage(eT,'CORRECT'); + @()needFlipTracker(me, 0); % eyetracker operator screen flip @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] @()needEyeSample(me,false); % no need to collect eye data until we start the next trial - @()hide(stims); - @()logRun(me,'CORRECT'); %fprintf current trial info + @()hide(stims); % hide all stims + @()logRun(me,'CORRECT'); % print current trial info }; %correct stimulus -correctWithin = { - @()drawBackground(s); % draw background alone -}; +correctWithin = { }; %when we exit the correct state correctExit = { @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial - @()updateVariables(me); %randomise our stimuli, and set strobe value too + @()updateVariables(me); %update independent variables, and set strobe value too + @()randomise(stims); %this uses metaStimulus.stimulusTable for stim changes @()update(stims); %update the stimuli ready for display @()resetExclusionZones(eT); %reset the exclusion zones @()checkTaskEnded(me); %check if task is finished + @()plot(bR, 1); % actually do our behaviour record drawing }; -%incorrect entry -incEntry = { - @()beep(aM,200,0.5,1); +%========================================================INCORRECT +%--------------------incorrect entry +incEntry = { + @()beep(aM,tS.errorSound); @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.INCORRECT)]); - @()trackerDrawStatus(eT,'Incorrect! :-(', stims.stimulusPositions); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); + @()trackerDrawStatus(eT,'INCORRECT! :-('); + @()statusMessage(eT,'INCORRECT'); + @()needFlipTracker(me, 0); % eyetracker operator screen flip @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()hide(stims); @()needEyeSample(me,false); + @()hide(stims); @()logRun(me,'INCORRECT'); %fprintf current trial info -}; +}; %our incorrect stimulus -incWithin = { - @()drawBackground(s); -}; +incWithin = { }; %incorrect / break exit incExit = { - @()updateVariables(me); %randomise our stimuli, don't run updateTask(task), and set strobe value too + @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() + @()updateVariables(me); %randomise our stimuli, set strobe value too + @()randomise(stims); %this uses metaStimulus.stimulusTable for stim changes @()update(stims); %update our stimuli ready for display @()resetExclusionZones(eT); %reset the exclusion zones @()checkTaskEnded(me); %check if task is finished + @()plot(bR, 1); % actually do our drawing }; if tS.includeErrors - incExit = [ {@()updatePlot(bR, me);@()updateTask(me,tS.BREAKFIX)}; incExit ]; % make sure our taskSequence is moved to the next trial + incExit = [ {@()updateTask(me,tS.BREAKFIX)}; incExit ]; % make sure our taskSequence is moved to the next trial else - incExit = [ {@()updatePlot(bR, me);@()resetRun(task)}; incExit ]; % we randomise the run within this block to make it harder to guess next trial + incExit = [ {@()resetRun(task)}; incExit ]; % we randomise the run within this block to make it harder to guess next trial end %break entry breakEntry = { @()beep(aM, 400, 0.5, 1); @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.BREAKFIX)]); - @()trackerDrawStatus(eT,'Fail to Saccade to Target! :-(', stims.stimulusPositions); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.BREAKFIX)); + @()trackerDrawStatus(eT,'Fail to Saccade to Target! :-('); + @()statusMessage(eT,'BREAKFIX'); + @()needFlipTracker(me, 0); % eyetracker operator screen flip @()needEyeSample(me,false); + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] @()hide(stims); @()logRun(me,'BREAKFIX'); %fprintf current trial info }; @@ -491,15 +438,21 @@ exclEntry = { @()beep(aM, 400, 0.5, 1); @()trackerMessage(eT,'END_RT'); @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.BREAKFIX)]); - @()trackerDrawStatus(eT,'Exclusion Zone entered! :-(', stims.stimulusPositions); + @()trackerDrawStatus(eT,'Exclusion Zone entered! :-(', [],true); + @()statusMessage(eT,'EXCLUSION'); @()needEyeSample(me,false); + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] @()hide(stims); @()logRun(me,'EXCLUSION'); %fprintf current trial info }; -%calibration function -calibrateFn = { - @()rstop(io); +%============================================================== +%========================================================EYETRACKER +%============================================================== + +%--------------------calibration function +calibrateFn = { @()drawBackground(s); %blank the display @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] @@ -520,47 +473,52 @@ offsetFcn = { @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) }; -%debug override +%============================================================== +%========================================================GENERAL +%============================================================== + +%--------------------DEBUGGER override overrideFn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values -%screenflash +%--------------------screenflash flashFn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection -%show 1deg size grid -gridFn = {@()drawGrid(s)}; - -%============================================================================== -%-----------------------------State Machine Table------------------------------ -% specify the cell array that is read by the stateMachine. -% REMEMBER that transitionFcn can override the time value, so for example -% stimulus shows 2 seconds time, but the transitionFcn can jump to other -% states (correct or breakfix) sooner than this, so this is an upper limit! -% initial state should be a pause, and keep calibrate, drift, override, -% flash, showgrid as these generic states are controlled by the keyboard. -% +%--------------------show 1deg size grid +gridFn = { @()drawGrid(s) }; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine stateInfoTmp = { -'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; %--------------------------------------------------------------------------------------------- 'pause' 'prefix' inf pauseEntry {} {} pauseExit; 'prefix' 'fixate' 0.5 pfEntry pfWithin {} pfExit; 'fixate' 'incorrect' 5 fixEntry fixWithin initFix fixExit; -'fixstim' 'incorrect' 5 fsEntry fsWithin fsFix fsExit +'catchtrial' 'incorrect' 5 catchEntry catchWithin catchFix catchExit 'stimulus' 'incorrect' 5 stimEntry stimWithin targetFix stimExit; 'correct' 'prefix' 0.25 correctEntry correctWithin {} correctExit; 'incorrect' 'timeout' 0.25 incEntry incWithin {} incExit; 'breakfix' 'timeout' 0.25 breakEntry incWithin {} incExit; 'exclusion' 'timeout' 0.25 exclEntry incWithin {} incExit; 'timeout' 'prefix' tS.tOut {} {} {} {}; +%--------------------------------------------------------------------------------------------- 'calibrate' 'pause' 0.5 calibrateFn {} {} {}; -'drift' 'pause' 0.5 driftFn {} {} {}; -'offset' 'pause' 0.5 offsetFcn {} {} {}; +'drift' 'pause' 0.5 driftFn {} {} {}; +'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- 'override' 'pause' 0.5 overrideFn {} {} {}; 'flash' 'pause' 0.5 flashFn {} {} {}; 'showgrid' 'pause' 10 {} gridFn {} {}; }; -% -%-----------------------------State Machine Table------------------------------ -%============================================================================== +%-------------------------State Machine Table------------------------------ +%========================================================================== disp('================>> Building state info file <<================') disp(stateInfoTmp) diff --git a/CoreProtocols/Saccade_AntiSaccade.m b/CoreProtocols/Saccade_AntiSaccade.m new file mode 100644 index 0000000000000000000000000000000000000000..f712d911ed08ba35a340853ead020dd5d3ec2b5f --- /dev/null +++ b/CoreProtocols/Saccade_AntiSaccade.m @@ -0,0 +1,561 @@ +% PRO-SACCADE and ANTI-SACCADE Task +% +% This task supports both pro and anti saccades and uses 3 stimuli: +% (1) pro-saccade target +% (2) anti-saccade target and +% (3) fixation cross. +% +% The task sequence is set up to randomise the X & Y position (xpPosition independent variable of (1) ±10° on +% each trial, and (2) has a modifier set as the inverse (if (1) is -10° on +% a trial then (2) becomes +10°) - the anti-saccade target is always +% opposite the saccade target. For the pro-saccade task, show (1) and hide +% (2), fixation window set on (1) and [optionally] exclusion zone set around (2). In +% the anti-saccade task we show (2) and can vary the opacity of (1) during +% training to encourage the subject to saccade away from (2) towards (1); +% the fixation and optional exclusion windows keep the same logic as for the +% pro-saccade condition. +% +% The exclusion zone is used to punish subject who saccade in the wrong +% direction. This is important suring training, but during data collection +% you may want to measure corrective saccades, in which case you would +% disable the exclusion zone. +% +% NOTE: this pro/anti-saccade task does not impose a response delay. Delays +% are common to help analysis of recorded neurons, however, teaching +% subjects to delay their [pro|anti]saccade interferes with the cognitive +% process we wish to measure!!! +% +% BUT this task does support delay of display of the [pro|anti]saccade +% target as this has a clear impact on error rates and reaction times, with +% 200ms showing max effect; you can control this by adding a delayTime +% parameter of 0.2s to the pro-saccade and anti-saccade target stimuli. See +% Fischer B & Weber H (1997) “Effects of stimulus conditions on the +% performance of antisaccades in man.” Experimental Brain Research 116(2), +% 191-200 [doi.org/10.1007/pl00005749](https://doi.org/10.1007/pl00005749) + + +%================================================================== +%--------------------TASK SPECIFIC CONFIG-------------------------- +% name +tS.name = 'prosaccade-antisaccade'; %==name of this protocol + +% we use manuN to show a selection menu to get values from the user. +title = {'[Pro|Anti]Saccade','Choose which type of task to perform.|You can also set the alpha of the |pro and anti saccade targets|which helps the subject during training.'}; +tS.options = {'r|¤Pro-Saccade|Anti-Saccade','Choose Protocol Type:';... + 'r|Use Exclusion Zone|¤Disable Exclusion Zone','Exclusion zone: if saccade is wrong direction, triggers incorrect';... + 't|0', 'Stimulus Visual Onset Gap (secs):'; ... + 't|0.1',' Prosaccade Target Initial Alpha [0-1]:';... + 't|0.75',' Prosaccade Target Main Alpha [0-1]:';... + 't|0.1','Antisaccade Target Initial Alpha [0-1]:';... + 't|0.75','Antisaccade Target Main Alpha [0-1]:'}; +if exist('isRunning','var') && isRunning == true % we are actually running a task, ask user + tS.ua = menuN(title,tS.options); +else % just loading the state file, pass defaults + tS.ua{1}=1;tS.ua{2}=1;tS.ua{3}=0;tS.ua{4}=0.1;yS.us{5}=0.75;tS.ua{6}=0.1;tS.ua{7}=0.1; +end +% task type +if tS.ua{1} == 1 + tS.type = 'saccade'; +else + tS.type = 'anti-saccade'; +end +% use an exclude zone around the opposite target? +if tS.ua{2} == 1 + tS.exclude = true; +else + tS.exclude = false; +end +% add a gap between when fixation disappears and target appears, see +% Fischer & Weber 1997 +if tS.ua{3} > 0 + stims{1}.delayTime = tS.ua{3}; + stims{2}.delayTime = tS.ua{3}; +end + +% update the trial number for incorrect saccades: if true then we call +% updateTask for both correct and incorrect trials, otherwise we only call +% updateTask() for correct responses. +tS.includeErrors = false; + +% note there are TWO alpha values, this is used by +% tS.fixAndStimTime below to control initial visualisation +% of the targets during fixation mostly used during training +% to guide the subject. +if strcmp(tS.type,'saccade') + % a flag to conditionally set visualisation on the eye tracker interface + stims{1}.showOnTracker = true; + stims{2}.showOnTracker = false; + tS.targetAlpha1 = tS.ua{4}; + tS.targetAlpha2 = tS.ua{5}; + tS.antitargetAlpha1 = 0; + tS.antitargetAlpha2 = 0; +else + % a flag to conditionally set visualisation on the eye tracker interface + stims{1}.showOnTracker = false; + stims{2}.showOnTracker = true; + % for use with tS.fixAndStimTime: + % alpha can be used during training to keep saccade target visible (i.e. in + % the anti-saccade task the subject must saccade away from the + % anti-saccade target towards to place where the pro-saccade target is, so + % starting training keeping the pro-saccade target visible helps the + % subject understand the task. Change the relative alpha values over + % training until ONLY the anti target is visible before collecting + % data. + tS.targetAlpha1 = tS.ua{4}; + tS.targetAlpha2 = tS.ua{5}; + tS.antitargetAlpha1 = tS.ua{6}; + tS.antitargetAlpha2 = tS.ua{7}; +end +disp(['\n===>>> Task ' tS.name ' Type:' tS.type ' <<<===\n']) + +%================================================================== +%----------------------General Settings---------------------------- +tS.useTask = true; %==use taskSequence (randomises stimulus variables) +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.keyExclusionPattern = ["fixstim","stimulus"]; %==which states to skip keyboard checking +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.tOut = 5; %==if wrong response, how long to time out before next trial +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume + +%================================================================== +%----------------Debug logging to command window------------------ +% uncomment each line to get specific verbose logging from each of these +% components; you can also set verbose in the opticka GUI to enable all of +% these… +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging +%uF.verbose = true; %==print out user function logg for debugging + +%================================================================== +%-----------------INITIAL Eyetracker Settings---------------------- +% These settings define the initial fixation window and set up for the +% eyetracker. They may be modified during the task (i.e. moving the +% fixation window towards a target, enabling an exclusion window to stop +% the subject entering a specific set of display areas etc.) +% +% IMPORTANT: you need to make sure that the global state time is larger +% than the fixation timers specified here. Each state has a global timer, +% so if the state timer is 5 seconds but your fixation timer is 6 seconds, +% then the state will finish before the fixation time was completed! + +% initial fixation X position in degrees (0° is screen centre) +tS.fixX = 0; +% initial fixation Y position in degrees +tS.fixY = 0; +% time to search and enter fixation window +tS.firstFixInit = 3; +% time to maintain initial fixation within window, can be single value or a +% range to randomise between +tS.firstFixTime = [0.8 1.2]; +% circular fixation window radius in degrees +tS.firstFixRadius = 1.25; +% do we forbid eye to enter-exit-reenter fixation window? +tS.strict = true; +% time to show BOTH fixation cross and [anti]saccade target +% this allows the first alpha values to be useful +tS.fixAndStimTime = 0; +% in this task the subject must saccade to the pro-saccade target location. +% These settings define the rules to "accept" the target fixation as +% correct +tS.targetFixInit = 0.5; % time to find the target +tS.targetFixTime = 0.5; % to to maintain fixation on target +tS.targetRadius = 8; %radius width x height to fix within. +% this task will establish an exclusion zone around the non-target +% target for the pro and anti-saccade task. We can change the size of the +% exclusion zone, here set to 5° around the X and Y position of the +% anti-saccade target. +if tS.exclude + tS.exclusionRadius = 5; %radius width x height to fix within. +else + tS.exclusionRadius = []; %empty thus exclusion zone removed +end +% Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); + +%================================================================== +%-----------------BEAVIOURAL PLOT CONFIGURATION-------------------- +%--WHICH states assigned correct / incorrect for the online plot?-- +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%====================================================================== +% N x 2 cell array of regexpi strings, list to skip the current -> next +% state's exit functions; for example skipExitStates = +% {'fixate','incorrect|breakfix'}; means that if the currentstate is +% 'fixate' and the next state is either incorrect OR breakfix, then skip +% the FIXATE exit state. Add multiple rows for skipping multiple state's +% exit states. +sM.skipExitStates = {'fixate','incorrect|breakfix'}; + +%================================================================== +% which stimulus in the list is used for a fixation target? For this +% protocol it means the subject must saccade this stimulus (the saccade +% target is #1 in the list) to get the reward. Also which stimulus to set an +% exclusion zone around (where a saccade into this area causes an immediate +% break fixation). +stims.fixationChoice = 1; +stims.exclusionChoice = 2; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of function handles that are executed by +% the state machine to control the experiment. The state machine can run +% sets at entry ['entryFcn'], during ['withinFcn'], to trigger a transition +% jump to another state ['transitionFcn'], and at exit ['exitFcn'. Remember +% these {sets} access the objects that are available within the +% runExperiment context. You can add custom functions and properties using +% userFunctions.m file. You can also add global variables/objects then use +% these. Any values entered here are set at load; if you want up-to-date +% values at trial time then you need to use methods/function wrappers to +% retrieve/set them. +%========================================================================= + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%==================================================================PAUSE +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%--------------------pause entry +pauseEntryFn = { + @()hide(stims); + @()drawBackground(s); %blank the subject display + @()drawTextNow(s,'PAUSED, press [p] to resume...'); + @()disp('PAUSED, press [p] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', [], 0, false); + @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()resetAll(eT); % reset all fixation markers to initial state + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false, 0); % no need to flip the PTB screen + @()needEyeSample(me,false); % no need to check eye position +}; + +%--------------------pause exit +pauseExitFn = { + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); %start recording eye position data again +}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%==============================================================PRE-FIXATION +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%====================================================PRE-FIXATION +pfEntryFn = { + @()needFlip(me, true, 1); % start PTB screen flips, and tracker screen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims,true); %make a struct the eT can use for drawing stim positions + @()hide(stims); + @()resetAll(eT); % reset all fixation markers to initial state + @()updateFixationValues(eT,tS.fixX,tS.fixY,tS.firstFixInit,tS.firstFixTime,tS.firstFixRadius,tS.strict); %reset fixation window + @()trackerTrialStart(eT, getTaskIndex(me)); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + % you can add any other messages, such as stimulus values as needed, + % e.g. @()trackerMessage(eT,['MSG:ANGLE' num2str(stims{1}.angleOut)]) etc. +}; + +pfWithinFn = { + @()trackerDrawFixation(eT); + @()trackerDrawEyePosition(eT); +}; + +pfExitFn = { + @()logRun(me,'INITFIX'); + @()trackerDrawStatus(eT,'Start trial...', stims.stimulusPositions, 0, 1); +}; + +%====================================================FIXATION +%--------------------fixate entry +fixEntryFn = { + % show stimulus 3 = fixation cross + @()show(stims, 3); + @()trackerMessage(eT,'MSG:Start Fix'); +}; + +%--------------------fix within +fixWithinFn = { + @()draw(stims); %draw stimulus + @()animate(stims); % animate stimuli for subsequent draw + @()trackerDrawEyePosition(eT); % for tobii +}; + +%--------------------test we are fixated for a certain length of time +initFixFn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'fixstim' + % is returned and the state machine will jump to that state, + % otherwise 'incorrect' is returned and the state machine will jump there. + % If neither condition matches, then the state table below + % defines that after X seconds we will switch to the incorrect state automatically. + @()testSearchHoldFixation(eT,'fixstim','breakfix') +}; + +%--------------------exit fixation phase +if strcmpi(tS.type,'saccade') + fixExitFn = { + @()show(stims, [1 3]); + @()edit(stims,1,'alphaOut',tS.targetAlpha1); + }; +else + fixExitFn = { + @()show(stims, [1 2 3]); + @()edit(stims,1,'alphaOut',tS.targetAlpha1); + @()edit(stims,2,'alphaOut',tS.antitargetAlpha1) + }; +end + +%====================================================FIX + TARGET STIMULUS +fsEntryFn = { + @()updateFixationValues(eT,[],[],[],tS.fixAndStimTime); %reset the fixation timer + @()logRun(me,'FIX+STIM'); %fprintf current trial info to command window +}; + +fsWithinFn = { + @()draw(stims); %draw stimulus + @()trackerDrawEyePosition(eT); +}; + +% test we are fixated for a certain length of time, testHoldFixation assumes +% we are already fixated which we are coming from the fixate state... +fsFixFn = { + @()testHoldFixation(eT,'stimulus','incorrect') +}; + +% exit fixation phase +fsExitFn = { + % use our saccade target stimulus for next fix X and Y, see + % stims.fixationChoice above + @()updateFixationTarget(me, tS.useTask, tS.targetFixInit, tS.targetFixTime, tS.targetRadius, tS.strict); + % use our antisaccade target to define the exclusion zone, see + % stims.exclusionChoice above + @()updateExclusionZones(me, tS.useTask, tS.exclusionRadius); + @()trackerMessage(eT,'END_FIX'); + @()hide(stims, 3); +}; +if strcmpi(tS.type,'saccade') + fsExitFn = [ fsExitFn; { @()edit(stims,1,'alphaOut',tS.targetAlpha2) } ]; +else + fsExitFn = [ fsExitFn; { @()edit(stims,1,'alphaOut',tS.targetAlpha2); @()edit(stims,2,'alphaOut',tS.antitargetAlpha2) } ]; +end + +%====================================================TARGET STIMULUS ALONE +% what to run when we enter the stim presentation state +stimEntryFn = { + % send an eyeTracker sync message (reset relative time to 0 after next flip) + @()doSyncTime(me); + % send stimulus value strobe (value alreadyset by updateVariables(me) function) + @()doStrobe(me,true); +}; + +% what to run when we are showing stimuli +stimWithinFn = { + @()draw(stims); + @()animate(stims); % animate stimuli for subsequent draw + @()trackerDrawEyePosition(eT); +}; + +% test we are finding the new target (stimulus 1, the saccade target) +targetFixFn = { + @()testSearchHoldFixation(eT,'correct','incorrect'); % tests finding and maintaining fixation +}; + +%as we exit stim presentation state +stimExitFn = { + @()setStrobeValue(me, me.strobe.stimOFFValue); + @()doStrobe(me,true); +}; + +%====================================================DECISION + +%if the subject is correct (small reward) +correctEntryFn = { + @()trackerTrialEnd(eT, tS.CORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()hide(stims); % hide all stims +}; + +%correct stimulus +correctWithinFn = { + +}; + +%when we exit the correct state +correctExitFn = { + @()giveReward(rM); % send a reward + @()beep(aM, tS.correctSound); % correct beep + @()trackerDrawStatus(eT, 'CORRECT! :-)', stims.stimulusPositions, 1, false); + @()logRun(me,'CORRECT'); % print current trial info + @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() + @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial + @()updateVariables(me); %randomise our stimuli, and set strobe value too + @()update(stims); %update the stimuli ready for display + @()resetAll(eT); %reset the exclusion zones + @()plot(bR, 1); % actually do our behaviour record drawing + @()checkTaskEnded(me); %check if task is finished + @()needFlip(me, false, 0); +}; + +%========================================================INCORRECT +%--------------------incorrect entry +incEntryFn = { + @()trackerTrialEnd(eT, tS.INCORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; + +%our incorrect stimulus +incWithinFn = { + +}; + +exitFn = { + % tS.includeErrors will prepend some code here... + @()beep(aM, tS.errorSound); + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing + @()checkTaskEnded(me); %check if task is finished + @()needFlip(me, false, 0); +}; + +if tS.includeErrors % do we allow incorrect trials to move to the next trial + incExitFn = [ { + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 1, false); + @()logRun(me,'INCORRECT'); + @()updatePlot(bR, me); + @()updateTask(me,tS.BREAKFIX)}; + exitFn ]; % make sure our taskSequence is moved to the next trial +else + incExitFn = [ { + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 1, false); + @()logRun(me,'INCORRECT'); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFn ]; % we randomise the run within this block to make it harder to guess next trial +end + +%========================================================BREAK +%break entry +breakEntryFn = { + @()trackerTrialEnd(eT, tS.BREAKFIX); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; + +exclEntryFn = breakEntryFn; + +if tS.includeErrors + breakExitFn = [ { + @()trackerDrawStatus(eT,'BREAKFIX! :-(', stims.stimulusPositions, 1, false); + @()logRun(me,'BREAKFIX'); + @()updatePlot(bR, me); + @()updateTask(me,tS.BREAKFIX)}; + exitFn ]; % make sure our taskSequence is moved to the next trial +else + breakExitFn = [ { + @()trackerDrawStatus(eT,'BREAKFIX! :-(', stims.stimulusPositions, 1, false); + @()logRun(me,'BREAKFIX'); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFn ]; % we randomise the run within this block to make it harder to guess next trial +end + +exclExitFcn = [{@()fprintf('EXCLUSION!\n')}; breakExitFn]; + +toEntryFn = { @()fprintf('\nTIME OUT!\n'); }; + +%======================================================== +%========================================================EYETRACKER +%======================================================== +%--------------------calibration function +calibrateFn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; + +%--------------------drift correction function +driftFn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) +}; +offsetFn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; + +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values + +%--------------------screenflash +flashFn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFn = { @()drawGrid(s) }; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine +stateInfoTmp = { +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFn {} {} pauseExitFn; +%--------------------------------------------------------------------------------------------- +'prefix' 'fixate' 0.5 pfEntryFn pfWithinFn {} pfExitFn; +'fixate' 'breakfix' 10 fixEntryFn fixWithinFn initFixFn fixExitFn; +'fixstim' 'breakfix' 10 fsEntryFn fsWithinFn fsFixFn fsExitFn +'stimulus' 'incorrect' 10 stimEntryFn stimWithinFn targetFixFn stimExitFn; +'correct' 'prefix' 0.1 correctEntryFn correctWithinFn {} correctExitFn; +'breakfix' 'timeout' 0.1 breakEntryFn incWithinFn {} incExitFn; +'incorrect' 'timeout' 0.1 incEntryFn incWithinFn {} breakExitFn; +'exclusion' 'timeout' 0.1 exclEntryFn incWithinFn {} breakExitFn; +'timeout' 'prefix' tS.tOut toEntryFn {} {} {}; +%--------------------------------------------------------------------------------------------- +'calibrate' 'pause' 0.5 calibrateFn {} {} {}; +'drift' 'pause' 0.5 driftFn {} {} {}; +'offset' 'pause' 0.5 offsetFn {} {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFn {} {} {}; +'flash' 'pause' 0.5 flashFn {} {} {}; +'showgrid' 'pause' 10 {} gridFn {} {}; +}; +% +%-----------------------------State Machine Table------------------------------ +%============================================================================== + +disp('================>> Building state info file <<================') +disp(stateInfoTmp) +disp('=================>> Loaded state info file <<=================') +clearvars -regexp '.+Fn' diff --git a/CoreProtocols/Saccade_AntiSaccade.mat b/CoreProtocols/Saccade_AntiSaccade.mat index c8b06911d7d9cdf59d45704c966ef97dec6ccb02..2f44e6821e086827aa5dba9fe3199ce3c8665a12 100644 Binary files a/CoreProtocols/Saccade_AntiSaccade.mat and b/CoreProtocols/Saccade_AntiSaccade.mat differ diff --git a/CoreProtocols/Saccade_AntiSaccade_TRAIN_1.m b/CoreProtocols/Saccade_AntiSaccade_TRAIN_1.m new file mode 100644 index 0000000000000000000000000000000000000000..383d92f6aa654375365fd66cf59c5ac518b4a659 --- /dev/null +++ b/CoreProtocols/Saccade_AntiSaccade_TRAIN_1.m @@ -0,0 +1,547 @@ +% PRO-SACCADE and ANTI-SACCADE Task +% +% This task supports both pro and anti saccades and uses 3 stimuli: +% (1) pro-saccade target +% (2) anti-saccade target and +% (3) fixation cross. +% +% The task sequence is set up to randomise the X & Y position (xpPosition independent variable of (1) ±10° on +% each trial, and (2) has a modifier set as the inverse (if (1) is -10° on +% a trial then (2) becomes +10°) - the anti-saccade target is always +% opposite the saccade target. For the pro-saccade task, show (1) and hide +% (2), fixation window set on (1) and [optionally] exclusion zone set around (2). In +% the anti-saccade task we show (2) and can vary the opacity of (1) during +% training to encourage the subject to saccade away from (2) towards (1); +% the fixation and optional exclusion windows keep the same logic as for the +% pro-saccade condition. +% +% The exclusion zone is used to punish subject who saccade in the wrong +% direction. This is important suring training, but during data collection +% you may want to measure corrective saccades, in which case you would +% disable the exclusion zone. +% +% NOTE: this pro/anti-saccade task does not impose a response delay. Delays +% are common to help analysis of recorded neurons, however, teaching +% subjects to delay their [pro|anti]saccade interferes with the cognitive +% process we wish to measure!!! +% +% BUT this task does support delay of display of the [pro|anti]saccade +% target as this has a clear impact on error rates and reaction times, with +% 200ms showing max effect; you can control this by adding a delayTime +% parameter of 0.2s to the pro-saccade and anti-saccade target stimuli. See +% Fischer B & Weber H (1997) “Effects of stimulus conditions on the +% performance of antisaccades in man.” Experimental Brain Research 116(2), +% 191-200 [doi.org/10.1007/pl00005749](https://doi.org/10.1007/pl00005749) + + +%================================================================== +%--------------------TASK SPECIFIC CONFIG-------------------------- +% name +tS.name = 'prosaccade-antisaccade'; %==name of this protocol + +% we use manuN to show a selection menu to get values from the user. +title = {'[Pro|Anti]Saccade','Choose which type of task to perform.|You can also set the alpha of the |pro and anti saccade targets|which helps the subject during training.'}; +tS.options = {'r|¤Pro-Saccade|Anti-Saccade','Choose Protocol Type:';... + 'r|¤Use Exclusion Zone|Disable Exclusion Zone','Exclusion zone: if saccade is wrong direction, triggers incorrect';... + 't|0.1','Prosaccade Target Initial Alpha:';... + 't|0.75','Prosaccade Target Main Alpha:';... + 't|0.1','Antisaccade Target Initial Alpha:';... + 't|0.75','Antisaccade Target Main Alpha:'}; +if exist('isRunning','var') && isRunning == true % we are actually running a task, ask user + tS.ua = menuN(title,tS.options); +else % just loading the state file, pass defaults + tS.ua{1}=1;tS.ua{2}=1;tS.ua{3}=0.1;yS.us{4}=0.75;tS.ua{5}=0.1;tS.ua{6}=0.1; +end +if tS.ua{1} == 1 + tS.type = 'saccade'; +else + tS.type = 'anti-saccade'; +end +if tS.ua{2} == 1 + tS.exclude = true; +else + tS.exclude = false; +end +% update the trial number for incorrect saccades: if true then we call +% updateTask for both correct and incorrect trials, otherwise we only call +% updateTask() for correct responses. +tS.includeErrors = false; + +% note there are TWO alpha values, this is used by +% tS.fixAndStimTime below to control initial visualisation +% of the targets during fixation mostly used during training +% to guide the subject. +if strcmp(tS.type,'saccade') + % a flag to conditionally set visualisation on the eye tracker interface + stims{1}.showOnTracker = true; + stims{2}.showOnTracker = false; + tS.targetAlpha1 = tS.ua{3}; + tS.targetAlpha2 = tS.ua{4}; + tS.antitargetAlpha1 = 0; + tS.antitargetAlpha2 = 0; +else + % a flag to conditionally set visualisation on the eye tracker interface + stims{1}.showOnTracker = false; + stims{2}.showOnTracker = true; + % for use with tS.fixAndStimTime: + % alpha can be used during training to keep saccade target visible (i.e. in + % the anti-saccade task the subject must saccade away from the + % anti-saccade target towards to place where the pro-saccade target is, so + % starting training keeping the pro-saccade target visible helps the + % subject understand the task. Change the relative alpha values over + % training until ONLY the anti target is visible before collecting + % data. + tS.targetAlpha1 = tS.ua{3}; + tS.targetAlpha2 = tS.ua{4}; + tS.antitargetAlpha1 = tS.ua{5}; + tS.antitargetAlpha2 = tS.ua{6}; +end +disp(['\n===>>> Task ' tS.name ' Type:' tS.type ' <<<===\n']) + +%================================================================== +%----------------------General Settings---------------------------- +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.useTask = true; %==use taskSequence (randomises stimulus variables) +tS.keyExclusionPattern = ["fixstim","stimulus"]; %==which states to skip keyboard checking +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.tOut = 1; %==if wrong response, how long to time out before next trial +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume + +%================================================================== +%----------------Debug logging to command window------------------ +% uncomment each line to get specific verbose logging from each of these +% components; you can also set verbose in the opticka GUI to enable all of +% these… +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging + +%================================================================== +%-----------------INITIAL Eyetracker Settings---------------------- +% These settings define the initial fixation window and set up for the +% eyetracker. They may be modified during the task (i.e. moving the +% fixation window towards a target, enabling an exclusion window to stop +% the subject entering a specific set of display areas etc.) +% +% IMPORTANT: you need to make sure that the global state time is larger +% than the fixation timers specified here. Each state has a global timer, +% so if the state timer is 5 seconds but your fixation timer is 6 seconds, +% then the state will finish before the fixation time was completed! + +% initial fixation X position in degrees (0° is screen centre) +tS.fixX = 0; +% initial fixation Y position in degrees +tS.fixY = 0; +% time to search and enter fixation window +tS.firstFixInit = 10; +% time to maintain initial fixation within window, can be single value or a +% range to randomise between +tS.firstFixTime = [0.1 0.3]; +% circular fixation window radius in degrees +tS.firstFixRadius = 5; +% do we forbid eye to enter-exit-reenter fixation window? +tS.strict = false; +% time to show BOTH fixation cross and [anti]saccade target +% this allows the first alpha values to be useful +tS.fixAndStimTime = 0; +% in this task the subject must saccade to the pro-saccade target location. +% These settings define the rules to "accept" the target fixation as +% correct +tS.targetFixInit = 10; % time to find the target +tS.targetFixTime = 0.1; % to to maintain fixation on target +tS.targetRadius = 10; %radius width x height to fix within. +% this task will establish an exclusion zone around the non-target +% target for the pro and anti-saccade task. We can change the size of the +% exclusion zone, here set to 5° around the X and Y position of the +% anti-saccade target. +if tS.exclude + tS.exclusionRadius = 5; %radius width x height to fix within. +else + tS.exclusionRadius = []; %empty thus exclusion zone removed +end +% Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); + +%================================================================== +%-----------------BEAVIOURAL PLOT CONFIGURATION-------------------- +%--WHICH states assigned correct / incorrect for the online plot?-- +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%====================================================================== +% N x 2 cell array of regexpi strings, list to skip the current -> next +% state's exit functions; for example skipExitStates = +% {'fixate','incorrect|breakfix'}; means that if the currentstate is +% 'fixate' and the next state is either incorrect OR breakfix, then skip +% the FIXATE exit state. Add multiple rows for skipping multiple state's +% exit states. +sM.skipExitStates = {'fixate','incorrect|breakfix'}; + +%================================================================== +% which stimulus in the list is used for a fixation target? For this +% protocol it means the subject must saccade this stimulus (the saccade +% target is #1 in the list) to get the reward. Also which stimulus to set an +% exclusion zone around (where a saccade into this area causes an immediate +% break fixation). +stims.fixationChoice = 1; +stims.exclusionChoice = 2; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of anonymous function handles which are +% executed by the state machine to control the experiment. The state +% machine can run sets at entry ['entryFn'], during ['withinFn'], to +% trigger a transition jump to another state ['transitionFn'], and at exit +% ['exitFn'. Remember these {sets} need to access the objects that are +% available within the runExperiment context (see top of file). You can +% also add global variables/objects then use these. The values entered here +% are set on load, if you want up-to-date values then you need to use +% methods/function wrappers to retrieve/set them. +%=================================================================== +%=================================================================== +%=================================================================== + +%======================================================== +%========================================================PAUSE +%======================================================== + +%--------------------pause entry +pauseEntryFn = { + @()hide(stims); + @()drawBackground(s); %blank the subject display + @()drawTextNow(s,'PAUSED, press [p] to resume...'); + @()disp('PAUSED, press [p] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', [], 0, false); + @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()resetAll(eT); % reset all fixation markers to initial state + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false, 0); % no need to flip the PTB screen + @()needEyeSample(me,false); % no need to check eye position +}; + +%--------------------pause exit +pauseExitFn = { + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); %start recording eye position data again +}; + +%====================================================PRE-FIXATION +pfEntryFn = { + @()needFlip(me, true, 1); % start PTB screen flips, and tracker screen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims,true); %make a struct the eT can use for drawing stim positions + @()hide(stims); + @()resetAll(eT); % reset all fixation markers to initial state + @()updateFixationValues(eT,tS.fixX,tS.fixY,tS.firstFixInit,tS.firstFixTime,tS.firstFixRadius,tS.strict); %reset fixation window + @()trackerTrialStart(eT, getTaskIndex(me)); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + % you can add any other messages, such as stimulus values as needed, + % e.g. @()trackerMessage(eT,['MSG:ANGLE' num2str(stims{1}.angleOut)]) etc. +}; + +pfWithinFn = { + @()trackerDrawFixation(eT); + @()trackerDrawEyePosition(eT); +}; + +pfExitFn = { + @()logRun(me,'INITFIX'); + @()trackerDrawStatus(eT,'Start trial...', stims.stimulusPositions, 0, 1); +}; + +%====================================================FIXATION +%--------------------fixate entry +fixEntryFn = { + % show stimulus 3 = fixation cross + @()show(stims, 3); + @()trackerMessage(eT,'MSG:Start Fix'); +}; + +%--------------------fix within +fixWithinFn = { + @()draw(stims); %draw stimulus + @()animate(stims); % animate stimuli for subsequent draw + @()trackerDrawEyePosition(eT); % for tobii +}; + +%--------------------test we are fixated for a certain length of time +initFixFn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'fixstim' + % is returned and the state machine will jump to that state, + % otherwise 'incorrect' is returned and the state machine will jump there. + % If neither condition matches, then the state table below + % defines that after X seconds we will switch to the incorrect state automatically. + @()testSearchHoldFixation(eT,'fixstim','breakfix') +}; + +%--------------------exit fixation phase +if strcmpi(tS.type,'saccade') + fixExitFn = { + @()show(stims, [1 3]); + @()edit(stims,1,'alphaOut',tS.targetAlpha1); + }; +else + fixExitFn = { + @()show(stims, [1 2 3]); + @()edit(stims,1,'alphaOut',tS.targetAlpha1); + @()edit(stims,2,'alphaOut',tS.antitargetAlpha1) + }; +end + +%====================================================FIX + TARGET STIMULUS +fsEntryFn = { + @()updateFixationValues(eT,[],[],[],tS.fixAndStimTime); %reset the fixation timer + @()logRun(me,'FIX+STIM'); %fprintf current trial info to command window +}; + +fsWithinFn = { + @()draw(stims); %draw stimulus + @()trackerDrawEyePosition(eT); +}; + +% test we are fixated for a certain length of time, testHoldFixation assumes +% we are already fixated which we are coming from the fixate state... +fsFixFn = { + @()testHoldFixation(eT,'stimulus','incorrect') +}; + +% exit fixation phase +fsExitFn = { + % use our saccade target stimulus for next fix X and Y, see + % stims.fixationChoice above + @()updateFixationTarget(me, tS.useTask, tS.targetFixInit, tS.targetFixTime, tS.targetRadius, tS.strict); + % use our antisaccade target to define the exclusion zone, see + % stims.exclusionChoice above + @()updateExclusionZones(me, tS.useTask, tS.exclusionRadius); + @()trackerMessage(eT,'END_FIX'); + @()hide(stims, 3); +}; +if strcmpi(tS.type,'saccade') + fsExitFn = [ fsExitFn; { @()edit(stims,1,'alphaOut',tS.targetAlpha2) } ]; +else + fsExitFn = [ fsExitFn; { @()edit(stims,1,'alphaOut',tS.targetAlpha2); @()edit(stims,2,'alphaOut',tS.antitargetAlpha2) } ]; +end + +%====================================================TARGET STIMULUS ALONE +% what to run when we enter the stim presentation state +stimEntryFn = { + % send an eyeTracker sync message (reset relative time to 0 after next flip) + @()doSyncTime(me); + % send stimulus value strobe (value alreadyset by updateVariables(me) function) + @()doStrobe(me,true); +}; + +% what to run when we are showing stimuli +stimWithinFn = { + @()draw(stims); + @()animate(stims); % animate stimuli for subsequent draw + @()trackerDrawEyePosition(eT); +}; + +% test we are finding the new target (stimulus 1, the saccade target) +targetFixFn = { + @()testSearchHoldFixation(eT,'correct','incorrect'); % tests finding and maintaining fixation +}; + +%as we exit stim presentation state +stimExitFn = { + @()setStrobeValue(me,255); + @()doStrobe(me,true); +}; + +%====================================================DECISION + +%if the subject is correct (small reward) +correctEntryFn = { + @()trackerTrialEnd(eT, tS.CORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()hide(stims); % hide all stims +}; + +%correct stimulus +correctWithinFn = { + +}; + +%when we exit the correct state +correctExitFn = { + @()giveReward(rM); % send a reward + @()beep(aM, tS.correctSound); % correct beep + @()trackerDrawStatus(eT, 'CORRECT! :-)', stims.stimulusPositions, 1, false); + @()logRun(me,'CORRECT'); % print current trial info + @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() + @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial + @()updateVariables(me); %randomise our stimuli, and set strobe value too + @()update(stims); %update the stimuli ready for display + @()resetAll(eT); %reset the exclusion zones + @()plot(bR, 1); % actually do our behaviour record drawing + @()checkTaskEnded(me); %check if task is finished + @()needFlip(me, false, 0); +}; + +%========================================================INCORRECT +%--------------------incorrect entry +incEntryFn = { + @()trackerTrialEnd(eT, tS.INCORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; + +%our incorrect stimulus +incWithinFn = { + +}; + +exitFn = { + % tS.includeErrors will prepend some code here... + @()beep(aM, tS.errorSound); + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing + @()checkTaskEnded(me); %check if task is finished + @()needFlip(me, false, 0); +}; + +if tS.includeErrors % do we allow incorrect trials to move to the next trial + incExitFn = [ { + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 1, false); + @()logRun(me,'INCORRECT'); + @()updatePlot(bR, me); + @()updateTask(me,tS.BREAKFIX)}; + exitFn ]; % make sure our taskSequence is moved to the next trial +else + incExitFn = [ { + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 1, false); + @()logRun(me,'INCORRECT'); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFn ]; % we randomise the run within this block to make it harder to guess next trial +end + +%========================================================BREAK +%break entry +breakEntryFn = { + @()trackerTrialEnd(eT, tS.BREAKFIX); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; + +exclEntryFn = breakEntryFn; + +if tS.includeErrors + breakExitFn = [ { + @()trackerDrawStatus(eT,'BREAKFIX! :-(', stims.stimulusPositions, 1, false); + @()logRun(me,'BREAKFIX'); + @()updatePlot(bR, me); + @()updateTask(me,tS.BREAKFIX)}; + exitFn ]; % make sure our taskSequence is moved to the next trial +else + breakExitFn = [ { + @()trackerDrawStatus(eT,'BREAKFIX! :-(', stims.stimulusPositions, 1, false); + @()logRun(me,'BREAKFIX'); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFn ]; % we randomise the run within this block to make it harder to guess next trial +end + +exclExitFcn = [{@()fprintf('EXCLUSION!\n')}; breakExitFn]; + +toEntryFn = { @()fprintf('\nTIME OUT!\n'); }; + +%======================================================== +%========================================================EYETRACKER +%======================================================== +%--------------------calibration function +calibrateFn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; + +%--------------------drift correction function +driftFn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) +}; +offsetFn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; + +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values + +%--------------------screenflash +flashFn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFn = { @()drawGrid(s) }; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine +stateInfoTmp = { +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFn {} {} pauseExitFn; +%--------------------------------------------------------------------------------------------- +'prefix' 'fixate' 0.5 pfEntryFn pfWithinFn {} pfExitFn; +'fixate' 'breakfix' 10 fixEntryFn fixWithinFn initFixFn fixExitFn; +'fixstim' 'breakfix' 10 fsEntryFn fsWithinFn fsFixFn fsExitFn +'stimulus' 'incorrect' 10 stimEntryFn stimWithinFn targetFixFn stimExitFn; +'correct' 'prefix' 0.1 correctEntryFn correctWithinFn {} correctExitFn; +'breakfix' 'timeout' 0.1 breakEntryFn incWithinFn {} incExitFn; +'incorrect' 'timeout' 0.1 incEntryFn incWithinFn {} breakExitFn; +'exclusion' 'timeout' 0.1 exclEntryFn incWithinFn {} breakExitFn; +'timeout' 'prefix' tS.tOut toEntryFn {} {} {}; +%--------------------------------------------------------------------------------------------- +'calibrate' 'pause' 0.5 calibrateFn {} {} {}; +'drift' 'pause' 0.5 driftFn {} {} {}; +'offset' 'pause' 0.5 offsetFn {} {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFn {} {} {}; +'flash' 'pause' 0.5 flashFn {} {} {}; +'showgrid' 'pause' 10 {} gridFn {} {}; +}; +% +%-----------------------------State Machine Table------------------------------ +%============================================================================== + +disp('================>> Building state info file <<================') +disp(stateInfoTmp) +disp('=================>> Loaded state info file <<=================') +clearvars -regexp '.+Fn' diff --git a/CoreProtocols/Saccade_AntiSaccade_TRAIN_1.mat b/CoreProtocols/Saccade_AntiSaccade_TRAIN_1.mat new file mode 100644 index 0000000000000000000000000000000000000000..a8082e87bf64dc2305514b9a38fff17de20acc28 Binary files /dev/null and b/CoreProtocols/Saccade_AntiSaccade_TRAIN_1.mat differ diff --git a/CoreProtocols/Saccade_to_Phosphene.mat b/CoreProtocols/Saccade_to_Phosphene.mat new file mode 100644 index 0000000000000000000000000000000000000000..b46a890519a7c74847cf8d0cef1e2757197387b5 Binary files /dev/null and b/CoreProtocols/Saccade_to_Phosphene.mat differ diff --git a/CoreProtocols/Saccadic_Countermanding.m b/CoreProtocols/Saccadic_Countermanding.m new file mode 100644 index 0000000000000000000000000000000000000000..f6da276e7e5e5eb018ecc7d2292df22512250260 --- /dev/null +++ b/CoreProtocols/Saccadic_Countermanding.m @@ -0,0 +1,492 @@ +% SACCADE COUNTERMANDING TASK - See Thakkar et al., 2011 | Hanes & Schall 1995 +% A fixation cross appears and after a delay it disappears and a saccade +% target appears. On 70% of trials a speeded saccade in <500ms is rewarded. +% In 30% of trials, the fixation cross reappears (STOP signal) after a +% delay and the subject MUST maintain fixation. The stop signal delay (SSD) +% is controlled by a staircase, and the percentage choice of STOP / NOSTOP +% trial is controlled by taskSequence.trialVar + +%========================================================================= +%-------------------------------Task Settings----------------------------- +% we use a up/down staircase to control the SSD (delay in seconds) +assert(exist('PAL_AMUD_setupUD','file'),'MUST Install Palamedes Toolbox: https://www.palamedestoolbox.org') +% Note this is managed by taskSequence, See Palamedes toolbox for the +% PAL_AM methods. 1up / 1down staircase starts at 225ms and steps at 32ms +task.staircase = []; +task.staircase.type = 'UD'; +task.staircase.sc = PAL_AMUD_setupUD('up',1,'down',1,'stepSizeUp',0.05,'stepSizeDown',0.05,... + 'stopRule',64,'startValue',0.25,'xMin',0.015,'xMax',0.5); +task.staircase.invert = true; % a correct increases value. +% we use taskSequence to randomise which state to switch to (independent +% trial-level factor). We call @()updateNextState(me,'trial') in the +% prefixation state; this sets one of these two trialVar.values as the next +% state. The nostopfix and stopfix states will then call nostop or stop +% stimulus states. +% These are actually set by the opticka GUI, but this is the task code +% task.trialVar.values = {'nostopfix','stopfix'}; +% task.trialVar.probability = [0.6 0.4]; +% task.trialVar.comment = 'nostep or step trial based on 60:40 probability'; +% tell timeLog which states are "stimulus" states +tL.stimStateNames = ["nostop","stop"]; + +%========================================================================= +%-----------------------------General Settings---------------------------- +% These settings are make changing the behaviour of the protocol easier. tS +% is just a struct(), so you can add your own switches or values here and +% use them lower down. Some basic switches like saveData, useTask, +% checkKeysDuringstimulus will influence the runeExperiment.runTask() +% functionality, not just the state machine. Other switches like +% includeErrors are referenced in this state machine file to change with +% functions are added to the state machine states… +tS.useTask = true; %==use taskSequence (randomises stimulus variables) +tS.rewardTime = 250; %==TTL time in milliseconds +tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. +tS.keyExclusionPattern = ["nostopfix","nostop","stopfix","stop"]; %==which states to skip keyboard checking (slightly improve performance) +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record a local copy of eye position, **in addition** to the eyetracker? +tS.askForComments = false; %==UI requestor asks for comments before/after run +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.name = 'Saccadic Countermanding'; %==name of this protocol +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.tOut = 1; %==if wrong response, how long to time out before next trial +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1.0, 1.0]; %==freq,length,volume + +%========================================================================= +%----------------Debug logging to command window------------------ +% uncomment each line to get specific verbose logging from each of these +% components; you can also set verbose in the opticka GUI to enable all of +% these… +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +task.verbose = true; %==print out task info for debugging +uF.verbose = true; %==print out user function logg for debugging + +%========================================================================= +%-----------------INITIAL Eyetracker Settings---------------------- +% These settings define the initial fixation window and set up for the +% eyetracker. They may be modified during the task (i.e. moving the fixation +% window towards a target, enabling an exclusion window to stop the subject +% entering a specific set of display areas etc.) +% +% **IMPORTANT**: you need to make sure that the global state time is larger than +% any fixation timers specified here. Each state has a global timer, so if the +% state timer is 5 seconds but your fixation timer is 6 seconds, then the state +% will finish before the fixation time was completed! +%------------------------------------------------------------------ +% initial fixation X position in degrees (0° is screen centre). +tS.fixX = 0; +% initial fixation Y position in degrees (0° is screen centre). +tS.fixY = 0; +% time to search and enter fixation window (Initiate fixation) +tS.firstFixInit = 3; +% time to maintain initial fixation within window, can be single value or a +% range to randomise between +tS.firstFixTime = [0.5 1.0]; +% fixation window radius in degrees; if you enter [x y] the window will be +% rectangular. +tS.firstFixRadius = 2; +% do we forbid eye to enter-exit-reenter fixation window? +tS.strict = true; +% --------------------------------------------------- +% in this task after iitial fixation a target appears +tS.targetFixInit = 3; +tS.targetFixTime = 1; +tS.targetFixRadius = 5; + +%========================================================================= +%-------------------------------Eyetracker setup-------------------------- +% NOTE: the opticka GUI sets eyetracker options, you can override them here if +% you need... +eT.name = tS.name; +if me.eyetracker.dummy; eT.isDummy = true; end %===use dummy or real eyetracker? +if tS.saveData; eT.recordData = true; end %===save Eyetracker data? +% Initialise eyetracker with X, Y, FixInitTime, FixTime, Radius, StrictFix +% values +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); + +%========================================================================= +%-------------------------ONLINE Behaviour Plot--------------------------- +% WHICH states assigned as correct or break for online plot? +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%========================================================================= +% which stimulus in the list is used for a fixation target? For this +% protocol it means the subject must saccade to this stimulus (the saccade +% target is #1 in the list) to get the reward. +stims.fixationChoice = 1; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of anonymous function handles which are +% executed by the state machine to control the experiment. The state +% machine can run sets at entry ['entryFcn'], during ['withinFcn'], to +% trigger a transition jump to another state ['transitionFcn'], and at exit +% ['exitFcn'. Remember these {sets} need to access the objects that are +% available within the runExperiment context (see top of file). You can +% also add global variables/objects then use these. The values entered here +% are set on load, if you want up-to-date values then you need to use +% methods/function wrappers to retrieve/set them. +%========================================================================= + +%============================================================== +%========================================================PAUSE +%============================================================== + +%--------------------pause entry +pauseEntryFcn = { + @()hide(stims); + @()drawPhotoDiodeSquare(s,[0 0 0]); %draw black photodiode + @()drawTextNow(s,'PAUSED, press [p] to resume...'); + @()disp('PAUSED, press [p] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume'); + @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()resetAll(eT); % reset all fixation markers to initial state + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false); % no need to flip the PTB screen + @()needEyeSample(me, false); % no need to check eye position +}; + +%--------------------pause exit +pauseExitFcn = { + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); +}; + +%============================================================== +%====================================================PRE-FIXATION +%============================================================== +%--------------------prefixate entry +prefixEntryFcn = { + @()setOffline(eT); % set eyelink offline [tobii/irec ignores this] + @()needFlip(me, true, 1); % enable the screen and trackerscreen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()hide(stims); % hide all stimuli + @()resetAll(eT); % reset the recent eye position history + @()updateFixationValues(eT,tS.fixX,tS.fixY,tS.firstFixInit,tS.firstFixTime,tS.firstFixRadius,tS.strict); %reset fixation window + @()getStimulusPositions(stims, true); %make a struct the eT can use for drawing stim positions + % tracker messages that define a trial start + @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands + @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + @()startRecording(eT); % start eyelink recording for this trial (tobii/irec ignore this) + @()trackerDrawStatus(eT,'PREFIX', stims.stimulusPositions); + % updateNextState method is critical, it reads the independent trial factor in + % taskSequence to select state to transition to next. This sets + % stateMachine.tempNextState. + @()updateNextState(me,'trial'); +}; + +%--------------------prefixate within +prefixFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------prefixate exit +prefixExitFcn = { + @()resetDelayTime(uF, 2, 0); % reset the delay time for stim 2 to 0; + @()show(stims{2}); +}; + +%======================================================== +%========================================================NOSTOP +%======================================================== + +nsfEntryFcn = { + @()trackerDrawStatus(eT,'NOSTOP', stims.stimulusPositions); + @()logRun(me,'NOSTOP-FIXATE'); +}; + +%--------------------fix within +nsfFcn = { + @()draw(stims{2}); %draw stimuli + @()trackerDrawEyePosition(eT); + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------test we are fixated for a certain length of time +nsfFixFcn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testSearchHoldFixation(eT, 'nostop', 'breakfix') +}; + +%--------------------exit fixation phase +nsfExitFcn = { + @()updateFixationTarget(me, true, tS.targetFixInit, tS.targetFixTime, tS.targetFixRadius, tS.strict); + @()hide(stims{2}); + @()show(stims{1}); +}; + +nsEntryFcn = { @()doStrobe(me,true); }; + +%--------------------fix within +nsFcn = { + @()draw(stims{1}); %draw stimuli + @()trackerDrawEyePosition(eT); + @()drawPhotoDiodeSquare(s,[1 1 1]); +}; + +%--------------------test we are fixated for a certain length of time +nsFixFcn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testSearchHoldFixation(eT, 'correct', 'incorrect') +}; + +%--------------------exit fixation phase +nsExitFcn = { + @()setStrobeValue(me,255); + @()doStrobe(me,true); +}; + +%======================================================== +%========================================================STOPSIGNAL +%======================================================== + +sfEntryFcn = { + @()trackerDrawStatus(eT,'STOP', stims.stimulusPositions); + @()logRun(me,'STOP-FIXATE'); +}; + +sfFcn = { + @()draw(stims{2}); + @()trackerDrawEyePosition(eT); + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +sfFixFcn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. + @()testSearchHoldFixation(eT, 'stop', 'breakfix'); +}; + +%as we exit stim presentation state +sfExitFcn = { + @()show(stims); + @()updateFixationValues(eT,[],[], 0.5, 1, tS.targetFixRadius); + @()setDelayTimeWithStaircase(uF,2); %sets the delayTime for fixation cross to reappear +}; + +sEntryFcn = { + @()doStrobe(me,true); +}; + +sFcn = { + @()draw(stims); + @()trackerDrawEyePosition(eT); + @()drawPhotoDiodeSquare(s,[1 1 1]); +}; + +sFixFcn = { + % this command performs the logic to maintain fixation + % inside the fixation window. + @()testHoldFixation(eT, 'correct', 'incorrect'); +}; + +%as we exit stim presentation state +sExitFcn = { + @()setStrobeValue(me,255); + @()doStrobe(me,true); +}; + +%======================================================== +%========================================================DECISIONS +%======================================================== + +%========================================================CORRECT +%--------------------if the subject is correct (small reward) +correctEntryFcn = { + @()giveReward(rM); % send a reward TTL + @()beep(aM, tS.correctSound); + @()trackerMessage(eT,'END_RT'); %send END_RT message to tracker + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); %send TRIAL_RESULT message to tracker + @()trackerDrawStatus(eT, 'CORRECT! :-)'); + @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()hide(stims); % hide all stims + @()logRun(me,'CORRECT'); % print current trial info +}; + +%--------------------correct stimulus +correctFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------when we exit the correct state +correctExitFcn = { + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()updatePlot(bR, me); % update our behavioural record, MUST be done before we update variables + @()updateTask(me,tS.CORRECT); % make sure our taskSequence is moved to the next trial + @()updateStaircaseAfterState(me,tS.CORRECT,'stop'); % only update staircase after a stop trial + @()updateVariables(me); % randomise our stimuli, and set strobe value too + @()update(stims); % update our stimuli ready for display + @()checkTaskEnded(me); % check if task is finished + @()plot(bR, 1); % actually do our behaviour record drawing +}; + +%========================================================INCORRECT +%--------------------incorrect entry +incEntryFcn = { + @()beep(aM, tS.errorSound); + @()trackerMessage(eT,'END_RT'); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()needEyeSample(me,false); + @()hide(stims); + @()logRun(me,'INCORRECT'); %fprintf current trial info +}; + +%--------------------our incorrect/breakfix stimulus +incFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------incorrect exit +incExitFcn = { + @()updatePlot(bR, me); % update our behavioural plot, must come before updateTask() / updateVariables() + @()updateTask(me,tS.BREAKFIX); % make sure our taskSequence is moved to the next trial + @()updateStaircaseAfterState(me,tS.BREAKFIX,'stop'); % only update staircase after a stop trial + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()plot(bR, 1); % actually do our behavioural record drawing +}; + +%--------------------break entry +breakEntryFcn = { + @()beep(aM, tS.errorSound); + @()trackerMessage(eT,'END_RT'); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.BREAKFIX)); + @()trackerDrawStatus(eT,'BREAKFIX before complete trial! :-(', stims.stimulusPositions, 0); + @()stopRecording(eT); + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()needEyeSample(me,false); + @()hide(stims); + @()logRun(me,'BREAKFIX'); %fprintf current trial info +}; + + +%--------------------break exit +breakExitFcn = { + @()updatePlot(bR, me); % update our behavioural plot, must come before updateTask() / updateVariables() + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()plot(bR, 1); % actually do our behavioural record drawing +}; + +%--------------------change functions based on tS settings +% this shows an example of how to use tS options to change the function +% lists run by the state machine. We can prepend or append new functions to +% the cell arrays. +if tS.useTask %we are using task + correctExitFcn = [ correctExitFcn; {@()checkTaskEnded(me)} ]; + incExitFcn = [ incExitFcn; {@()checkTaskEnded(me)} ]; + breakExitFcn = [ breakExitFcn; {@()checkTaskEnded(me)} ]; +end + +%======================================================== +%========================================================EYETRACKER +%======================================================== +%--------------------calibration function +calibrateFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; + +%--------------------drift correction function +driftFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) +}; +offsetFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; + +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values + +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine +stateInfoTmp = { +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFcn {} {} pauseExitFcn; +%--------------------------------------------------------------------------------------------- +'prefix' 'nostop' 1 prefixEntryFcn prefixFcn {} prefixExitFcn; +%--------------------------------------------------------------------------------------------- +'nostopfix' 'breakfix' 8 nsfEntryFcn nsfFcn nsfFixFcn nsfExitFcn; +'nostop' 'breakfix' 8 nsEntryFcn nsFcn nsFixFcn nsExitFcn; +'stopfix' 'breakfix' 8 sfEntryFcn sfFcn sfFixFcn sfExitFcn; +'stop' 'breakfix' 8 sEntryFcn sFcn sFixFcn sExitFcn; +%--------------------------------------------------------------------------------------------- +'breakfix' 'timeout' 0.5 breakEntryFcn incFcn {} breakExitFcn; +'incorrect' 'timeout' 0.5 incEntryFcn incFcn {} incExitFcn; +'correct' 'prefix' 0.5 correctEntryFcn correctFcn {} correctExitFcn; +'timeout' 'prefix' tS.tOut {} incFcn {} {}; +%--------------------------------------------------------------------------------------------- +'calibrate' 'pause' 0.5 calibrateFcn {} {} {}; +'drift' 'pause' 0.5 driftFcn {} {} {}; +'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFcn {} {} {}; +'flash' 'pause' 0.5 flashFcn {} {} {}; +'showgrid' 'pause' 10 {} gridFcn {} {}; +}; +%--------------------------State Machine Table----------------------------- +%========================================================================== + +disp('=================>> Built state info file <<==================') +disp(stateInfoTmp) +disp('=================>> Built state info file <<=================') +clearvars -regexp '.+Fcn$' % clear the cell array Fcns in the current workspace diff --git a/CoreProtocols/Saccadic_Countermanding.mat b/CoreProtocols/Saccadic_Countermanding.mat new file mode 100644 index 0000000000000000000000000000000000000000..1ce2b80d22d1eb51eed4a1b8af4cd7dc4a20c528 Binary files /dev/null and b/CoreProtocols/Saccadic_Countermanding.mat differ diff --git a/CoreProtocols/Saccadic_DoubleStep.m b/CoreProtocols/Saccadic_DoubleStep.m new file mode 100644 index 0000000000000000000000000000000000000000..cfcb41668c1ff78db1eff52844a2b50ff73f910b --- /dev/null +++ b/CoreProtocols/Saccadic_DoubleStep.m @@ -0,0 +1,454 @@ +% DOUBLESTEP SACCADE task, Thakkar et al., 2015 Brain & Cognition +% +% This paradigm should be more sensitive to online inhibition than +% anti-saccade. +% +% In nostep trials (60%), after 500-1000ms of intial fixation, a saccade target +% (target one) is flashed for 100ms in one of 8 equidistant positions. In +% step trials (40%) after taget one flashes after a delay (target step +% delay, TSD) a second target (target two) flashes 90deg away and subject must saccade +% to target two for successful trial. Subjects are not punished for +% reorienting from target one to target two. TSD is modified using a +% 1U/1D staircase, and nostep / step trial assignment use taskSequence.trialVar + +%========================================================================= +%-------------------------------Task Settings----------------------------- +% name +tS.name = 'Saccadic DoubleStep'; %==name of this protocol +% we use a up/down staircase to control the TSD (delay in seconds) +assert(exist('PAL_AMUD_setupUD','file'),'MUST Install Palamedes Toolbox: https://www.palamedestoolbox.org') + +% See Palamedes toolbox for the PAL_AM methods. +% 1up / 1down staircase starts at 225ms and steps at 34ms between 100 and +% 600ms +task.staircase = []; +task.staircase(1).type = 'UD'; +task.staircase(1).sc = PAL_AMUD_setupUD('up',1,'down',1,'stepSizeUp',0.034,'stepSizeDown',0.034,... + 'stopRule',64,'startValue',0.225,'xMin',0.1,'xMax',0.6); +task.staircase(1).invert = true; % a correct increases value. +% we use taskSequence to randomise which state to switch to (independent +% trial-level factor). We call @()updateNextState(me,'trial') in the +% prefixation state; this sets one of these two trialVar.values as the next +% state. The nostopfix and stopfix states will call nostep or step +% stimulus states respectively. +% These are actually set by the opticka GUI, but this is the task code do +% set this: +% task.trialVar.comment = 'nostep or step trial based on 60:40 probability'; +% task.trialVar.values = {'nostepfix','stepfix'}; +% task.trialVar.probability = [0.6 0.4]; +% tell timeLog which states are "stimulus" states +tL.stimStateNames = ["nostep","step"]; + +% update the trial number for incorrect saccades: if true then we call +% updateTask for both correct and incorrect trials, otherwise we only call +% updateTask() for correct responses. +tS.includeErrors = false; + +%================================================================== +%----------------------General Settings---------------------------- +tS.useTask = true; %==use taskSequence (randomises stimulus variables) +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.keyExclusionPattern = ["nostepfix","nostep","stepfix","step"]; %==which states to skip keyboard checking (slightly improve performance) +tS.recordEyePosition = false; %==record a local copy of eye position, **in addition** to the eyetracker? +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.tOut = 1; %==timeout if breakfix/incorrect response +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1.0, 1.0]; %==freq,length,volume + +%========================================================================= +%----------------Debug logging to command window------------------ +% uncomment each line to get specific verbose logging from each of these +% components; you can also set verbose in the opticka GUI to enable all of +% these… +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging +%uF.verbose = true; %==print out user function logg for debugging + +%================================================================== +%-----------------INITIAL Eyetracker Settings---------------------- +% These settings define the initial fixation window and set up for the +% eyetracker. They may be modified during the task (i.e. moving the +% fixation window towards a target, enabling an exclusion window to stop +% the subject entering a specific set of display areas etc.) +% +% **IMPORTANT**: you need to make sure that the overall state time is larger than +% any fixation timers specified here. Each state has a timer, so if the +% state timer is 5 seconds but your fixation timer is 6 seconds, then the state +% will finish before the fixation time was completed! +%------------------------------------------------------------------ +tS.fixX = 0; % initial fixation X position in degrees (0° is screen centre). +tS.fixY = 0; % initial fixation Y position in degrees (0° is screen centre). +tS.firstFixInit = 3; % time to search and enter fixation window (Initiate fixation) +tS.firstFixTime = [0.5 1.0]; % time to maintain initial fixation within window +tS.firstFixRadius = 2; % fixation window radius in degrees +tS.strict = true; % do we forbid eye to enter-exit-reenter fixation window? +% --------------------------------------------------- +% in this task after initial fixation a target appears +tS.targetFixInit = 3; +tS.targetFixTime = 1; +tS.targetFixRadius = 5; +% Initialise eyetracker with X, Y, FixInitTime, FixTime, Radius, StrictFix +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); + +%========================================================================= +%-------------------------ONLINE Behaviour Plot--------------------------- +% WHICH states assigned as correct or break for online plot? +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of function handles that are executed by +% the state machine to control the experiment. The state machine can run +% sets at entry ['entryFcn'], during ['withinFcn'], to trigger a transition +% jump to another state ['transitionFcn'], and at exit ['exitFcn'. Remember +% these {sets} access the objects that are available within the +% runExperiment context. You can add custom functions and properties using +% userFunctions.m file. You can also add global variables/objects then use +% these. Any values entered here are set at load; if you want up-to-date +% values at trial time then you need to use methods/function wrappers to +% retrieve/set them. +%========================================================================= + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%==================================================================PAUSE +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%--------------------pause entry +pauseEntryFcn = { + @()hide(stims); + @()drawPhotoDiodeSquare(s,[0 0 0]); %draw black photodiode + @()drawTextNow(s,'PAUSED, press [p] to resume...'); + @()disp('PAUSED, press [p] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', [], 0, false); + @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()resetAll(eT); % reset all fixation markers to initial state + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false, 0); % no need to flip the PTB screen + @()needEyeSample(me,false); % no need to check eye position +}; + +%--------------------pause exit +pauseExitFcn = { + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); %start recording eye position data again +}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%==============================================================PRE-FIXATION +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%====================================================PRE-FIXATION +pfEntryFcn = { + @()needFlip(me, true, 1); % start PTB screen flips, and tracker screen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims,true); %make a struct the eT can use for drawing stim positions + @()hide(stims); + @()resetAll(eT); % reset all fixation markers to initial state + @()updateFixationValues(eT,tS.fixX,tS.fixY,tS.firstFixInit,tS.firstFixTime,tS.firstFixRadius,tS.strict); %reset fixation window + @()trackerTrialStart(eT, getTaskIndex(me)); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + % you can add any other messages, such as stimulus values as needed, + % e.g. @()trackerMessage(eT,['MSG:ANGLE' num2str(stims{1}.angleOut)]) etc. +}; + +pfFcn = { + @()trackerDrawFixation(eT); + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +pfExitFcn = { + @()logRun(me,'INITFIX'); + @()trackerDrawStatus(eT,'Start trial...', stims.stimulusPositions, 0, 1); +}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%=======================================================NOSTEP FIX + STIMULATION +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%fixate entry +nsfEntryFcn = { + @()edit(stims,1,'offTime',0.1); % make sure we reset this just in case + @()resetTicks(stims); % this function regenerates the delay / off timers for stimulus drawing + @()trackerDrawFixation(eT); + @()logRun(me,'Nostep Fix'); %fprintf current trial info to command window +}; + +%fix within +nsfFcn = { + @()trackerDrawEyePosition(eT); + @()draw(stims, 3); %draw stimulus +}; + +%test we are fixated for a certain length of time +nsfTestFcn = { + % if subject found and held fixation, go to 'nostep' state, otherwise 'breakfix' + @()testSearchHoldFixation(eT,'nostep','breakfix'); +}; + +%exit fixation phase +nsfExitFcn = { + @()hide(stims, 3); + @()show(stims, 1); + @()updateFixationTarget(me, 1, tS.targetFixInit, ... + tS.targetFixTime, tS.targetFixRadius); + @()trackerMessage(eT,'END_FIX'); +}; + +%what to run when we enter the stim presentation state +nsEntryFcn = { + @()trackerDrawFixation(eT); + @()doStrobe(me,true); +}; + +%what to run when we are showing stimuli +nsFcn = { + @()draw(stims, 1); + @()trackerDrawEyePosition(eT); +}; + +%test we are maintaining fixation +nsTestFcn = { + % if subject found and held target, go to 'correct' state, otherwise 'incorrect' + @()testSearchHoldFixation(eT,'correct','incorrect'); +}; + +%as we exit stim presentation state +nsExitFcn = { + @()setStrobeValue(me,255); + @()doStrobe(me,true); +}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%=========================================================STEP FIX + STIMULATION +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +sfEntryFcn = { + @()edit(stims,1,'offTime',0.1); + @()trackerDrawFixation(eT); + @()logRun(me,'Step Fix'); %fprintf current trial info to command window +}; + +sfFcn = { + @()draw(stims, 3); %draw stimulus + @()trackerDrawEyePosition(eT); +}; + +sfTestFcn = { + % if subject found and held fixation, go to 'step' state, otherwise 'breakfix' + @()testSearchHoldFixation(eT,'step','breakfix'); +}; + +sfExitFcn = { + @()hide(stims, 3); + @()show(stims, [1 2]); + @()setDelayTimeWithStaircase(uF, 2, 0.1); + @()resetTicks(stims); + @()updateFixationTarget(me, 2, tS.targetFixInit, ... + tS.targetFixTime, tS.targetFixRadius); + @()trackerMessage(eT,'END_FIX'); +}; + +%what to run when we enter the stim presentation state +sEntryFcn = { + @()trackerDrawFixation(eT); + @()doStrobe(me,true); +}; + +%test we are fixated for a certain length of time +sTestFcn = { + % if subject found and held fixation, go to 'correct' state, otherwise 'incorrect' + @()testSearchHoldFixation(eT,'correct','incorrect'); +}; + +%what to run when we are showing stimuli +sFcn = { + @()trackerDrawEyePosition(eT); + @()draw(stims,[1 2]); +}; + +sExitFcn = { + @()setStrobeValue(me,255); + @()doStrobe(me,true); +}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%=======================================================================DECISION +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +correctEntryFcn = { + @()giveReward(rM); % send a reward TTL + @()beep(aM,tS.correctSound); % correct beep + @()trackerMessage(eT,'END_RT'); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); %send TRIAL_RESULT message to tracker + @()trackerDrawStatus(eT, 'CORRECT! :-)'); + @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()hide(stims); + @()logRun(me,'CORRECT'); %fprintf current trial info +}; + +%correct stimulus +correctFcn = { + @()drawBackground(s); +}; + +%when we exit the correct state +correctExitFcn = { + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()updatePlot(bR, me); %update our behavioural plot + @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial + @()updateStaircaseAfterState(me, tS.CORRECT,'step'); % only update staircase after a stop trial + @()updateVariables(me); %randomise our stimuli, and set strobe value too + @()update(stims); %update our stimuli ready for display + @()plot(bR, 1); % actually do our behaviour record drawing + @()checkTaskEnded(me); %check if task is finished +}; + +%incorrect entry +incEntryFcn = { + @()beep(aM, tS.errorSound); + @()trackerMessage(eT,'END_RT'); + @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.INCORRECT)]); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()needEyeSample(me,false); + @()hide(stims); + @()logRun(me,'INCORRECT'); %fprintf current trial info +}; + +%our incorrect stimulus +incFcn = { + @()drawBackground(s); +}; + +%incorrect / break exit +incExitFcn = { + @()updateStaircaseAfterState(me,tS.BREAKFIX,'step'); % only update staircase after a stop trial + @()updateVariables(me); %randomise our stimuli, don't run updateTask(task), and set strobe value too + @()update(stims); %update our stimuli ready for display + @()plot(bR, 1); % actually do our behaviour record drawing + @()checkTaskEnded(me); %check if task is finished +}; + +%break entry +breakEntryFcn = { + @()beep(aM, tS.errorSound); + @()trackerMessage(eT,'END_RT'); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.BREAKFIX)); + @()trackerDrawStatus(eT,'BREAKFIX before complete trial! :-(', stims.stimulusPositions, 0); + @()stopRecording(eT); + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()needEyeSample(me,false); + @()hide(stims); + @()logRun(me,'BREAKFIX'); %fprintf current trial info +}; + +breakExitFcn = incExitFcn; + +if tS.includeErrors + incExitFcn = [ {@()updatePlot(bR, me);@()updateTask(me,tS.INCORRECT)}; incExitFcn ]; + breakExitFcn = [ {@()updatePlot(bR, me);@()updateTask(me,tS.BREAKFIX)}; incExitFcn ]; +else + incExitFcn = [ {@()updatePlot(bR, me);@()resetRun(task)}; incExitFcn ]; % we randomise the run within this block to make it harder to guess next trial + breakExitFcn = [ {@()updatePlot(bR, me);@()resetRun(task)}; incExitFcn ]; % we randomise the run within this block to make it harder to guess next trial +end + + +%==================================================================EXPERIMENTAL CONTROL + +%================================================================== +%==================================================================EYETRACKER +%================================================================== +%--------------------calibration function +calibrateFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; + +%--------------------drift correction function +driftFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) +}; +offsetFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; + +%====================================================================== +%======================================================================GENERAL +%====================================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values + +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine +stateInfoTmp = { +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFcn {} {} pauseExitFcn; +'prefix' 'breakfix' 0.5 pfEntryFcn pfFcn {} pfExitFcn; +%--------------------------------------------------------------------------------------------- +'nostepfix' 'breakfix' 5 nsfEntryFcn nsfFcn nsfTestFcn nsfExitFcn; +'nostep' 'breakfix' 5 nsEntryFcn nsFcn nsTestFcn nsExitFcn; +'stepfix' 'breakfix' 5 sfEntryFcn sfFcn sfTestFcn sfExitFcn; +'step' 'breakfix' 5 sEntryFcn sFcn sTestFcn sExitFcn; +%--------------------------------------------------------------------------------------------- +'breakfix' 'timeout' 0.5 breakEntryFcn incFcn {} breakExitFcn; +'incorrect' 'timeout' 0.5 incEntryFcn incFcn {} incExitFcn; +'correct' 'prefix' 0.5 correctEntryFcn correctFcn {} correctExitFcn; +'timeout' 'prefix' tS.tOut {} {} {} {}; +%--------------------------------------------------------------------------------------------- +'calibrate' 'pause' 0.5 calibrateFcn {} {} {}; +'drift' 'pause' 0.5 driftFcn {} {} {}; +'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFcn {} {} {}; +'flash' 'pause' 0.5 flashFcn {} {} {}; +'showgrid' 'pause' 10 {} gridFcn {} {}; +}; +%--------------------------State Machine Table----------------------------- +%========================================================================== + +disp('=================>> Built state info file <<==================') +disp(stateInfoTmp) +disp('=================>> Built state info file <<=================') +clearvars -regexp '.+Fcn$' % clear the cell array Fcns in the current workspace diff --git a/CoreProtocols/Saccadic_DoubleStep.mat b/CoreProtocols/Saccadic_DoubleStep.mat new file mode 100644 index 0000000000000000000000000000000000000000..f3fb83d1681699f3d7a380db6002f7ad88603f50 Binary files /dev/null and b/CoreProtocols/Saccadic_DoubleStep.mat differ diff --git a/CoreProtocols/Saccadic_DoubleStep_TRAIN_1.m b/CoreProtocols/Saccadic_DoubleStep_TRAIN_1.m new file mode 100644 index 0000000000000000000000000000000000000000..cd12925117c7e63355fd7f6f12a1fba37a4d2f0e --- /dev/null +++ b/CoreProtocols/Saccadic_DoubleStep_TRAIN_1.m @@ -0,0 +1,471 @@ +% DOUBLESTEP SACCADE task, Thakkar et al., 2015 Brain & Cognition +% In nostep trials (60%), after 500-1000ms intial fixation, a saccade target +% (target one) is flashed for 100ms in one of 8 equidistant positions. In +% step trials (40%) after taget one flashes after a delay (target step +% delay, TSD) a second target (target two) flashes 90deg away and subject must saccade +% to target two for succesful trial. Subjects are not punished for +% reorienting from target one to target two. TSD is modified using a +% 1U/1D staircase, and nostep / step trial assignment use taskSequence.trialVar + +%========================================================================= +%-------------------------------Task Settings----------------------------- +% we use a up/down staircase to control the SSD (delay in seconds) +assert(exist('PAL_AMUD_setupUD','file'),'MUST Install Palamedes Toolbox: https://www.palamedestoolbox.org') +% See Palamedes toolbox for the PAL_AM methods. +% 1up / 1down staircase starts at 225ms and steps at 34ms between 100 and +% 600ms +task.staircase = []; +task.staircase(1).type = 'UD'; +task.staircase(1).sc = PAL_AMUD_setupUD('up',1,'down',1,'stepSizeUp',0.034,'stepSizeDown',0.034,... + 'stopRule',64,'startValue',0.225,'xMin',0.1,'xMax',0.6); +task.staircase(1).invert = true; % a correct increases value. +% we use taskSequence to randomise which state to switch to (independent +% trial-level factor). We call @()updateNextState(me,'trial') in the +% prefixation state; this sets one of these two trialVar.values as the next +% state. The nostopfix and stopfix states will call nostep or step +% stimulus states respectively. +% These are actually set by the opticka GUI, but this is the task code do +% set this: +% task.trialVar.comment = 'nostep or step trial based on 60:40 probability'; +% task.trialVar.values = {'nostepfix','stepfix'}; +% task.trialVar.probability = [0.6 0.4]; +% tell timeLog which states are "stimulus" states +tL.stimStateNames = ["nostep","step"]; + +%========================================================================= +%-----------------------------General Settings---------------------------- +% These settings are make changing the behaviour of the protocol easier. tS +% is just a struct(), so you can add your own switches or values here and +% use them lower down. Some basic switches like saveData, useTask, +% checkKeysDuringstimulus will influence the runeExperiment.runTask() +% functionality, not just the state machine. Other switches like +% includeErrors are referenced in this state machine file to change with +% functions are added to the state machine states… +tS.useTask = true; %==use taskSequence (randomises stimulus variables) +tS.includeErrors = true; %==do incorrect error trials count to move taskSequence forward +tS.rewardTime = 250; %==TTL time in milliseconds +tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. +tS.keyExclusionPattern = ["nostepfix","nostep","stepfix","step"]; %==which states to skip keyboard checking (slightly improve performance) +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record a local copy of eye position, **in addition** to the eyetracker? +tS.askForComments = false; %==UI requestor asks for comments before/after run +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.name = 'Saccadic DoubleStep'; %==name of this protocol +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.tOut = 1; %==timeout if breakfix/incorrect response +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1.0, 1.0]; %==freq,length,volume + +%========================================================================= +%----------------Debug logging to command window------------------ +% uncomment each line to get specific verbose logging from each of these +% components; you can also set verbose in the opticka GUI to enable all of +% these… +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +task.verbose = true; %==print out task info for debugging +uF.verbose = true; %==print out user function logg for debugging + +%========================================================================= +%---------------INITIAL Eyetracker Fixation Settings---------------------- +% These settings define the initial fixation window and set up for the +% eyetracker. They may be modified during the task (i.e. moving the fixation +% window towards a target, enabling an exclusion window to stop the subject +% entering a specific set of display areas etc.) +% +% **IMPORTANT**: you need to make sure that the overall state time is larger than +% any fixation timers specified here. Each state has a timer, so if the +% state timer is 5 seconds but your fixation timer is 6 seconds, then the state +% will finish before the fixation time was completed! +%------------------------------------------------------------------ +tS.fixX = 0; % initial fixation X position in degrees (0° is screen centre). +tS.fixY = 0; % initial fixation Y position in degrees (0° is screen centre). +tS.firstFixInit = 3; % time to search and enter fixation window (Initiate fixation) +tS.firstFixTime = [0.5 1.0]; % time to maintain initial fixation within window +tS.firstFixRadius = 2; % fixation window radius in degrees +tS.strict = true; % do we forbid eye to enter-exit-reenter fixation window? +% --------------------------------------------------- +% in this task after initial fixation a target appears +tS.targetFixInit = 3; +tS.targetFixTime = 1; +tS.targetFixRadius = 5; + +%========================================================================= +%-------------------------------Eyetracker setup-------------------------- +% NOTE: the opticka GUI sets eyetracker options, you can override them here if +% you need... +eT.name = tS.name; +if me.eyetracker.dummy; eT.isDummy = true; end %===use dummy or real eyetracker? +if tS.saveData; eT.recordData = true; end %===save Eyetracker data? +% Initialise eyetracker with X, Y, FixInitTime, FixTime, Radius, StrictFix +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); + +%========================================================================= +%-------------------------ONLINE Behaviour Plot--------------------------- +% WHICH states assigned as correct or break for online plot? +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of function handles that are executed by +% the state machine to control the experiment. The state machine can run +% sets at entry ['entryFcn'], during ['withinFcn'], to trigger a transition +% jump to another state ['transitionFcn'], and at exit ['exitFcn'. Remember +% these {sets} access the objects that are available within the +% runExperiment context. You can add custom functions and properties using +% userFunctions.m file. You can also add global variables/objects then use +% these. Any values entered here are set at load; if you want up-to-date +% values at trial time then you need to use methods/function wrappers to +% retrieve/set them. +%========================================================================= + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%==================================================================PAUSE +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%--------------------pause entry +pauseEntryFcn = { + @()hide(stims); + @()drawPhotoDiodeSquare(s,[0 0 0]); %draw black photodiode + @()drawTextNow(s,'PAUSED, press [p] to resume...'); + @()disp('PAUSED, press [p] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume'); + @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()resetAll(eT); % reset all fixation markers to initial state + @()setOffline(eT); % set eyelink offline [tobii/irec ignores this] + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false); % no need to flip the PTB screen + @()needEyeSample(me, false); % no need to check eye position +}; + +%--------------------pause exit +pauseExitFcn = { + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); +}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%==============================================================PRE-FIXATION +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%--------------------prefixate entry +prefixEntryFcn = { + @()setOffline(eT); % set eyelink offline [tobii/irec ignores this] + @()needFlip(me, true, 2); % enable the screen and trackerscreen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()hide(stims); % hide all stimuli + @()resetAll(eT); % reset the recent eye position history + @()updateFixationValues(eT,tS.fixX,tS.fixY,tS.firstFixInit,tS.firstFixTime,tS.firstFixRadius); %reset fixation window to initial values + @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions + % tracker messages that define a trial start + @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands + @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + @()trackerDrawStatus(eT,'PREFIX', stims.stimulusPositions); + @()startRecording(eT); % start eyelink recording for this trial (tobii/irec ignore this) + % updateNextState method is critical, it reads the independent trial factor in + % taskSequence to select state to transition to next. This sets + % stateMachine.tempNextState to override the state table's default next + % field. In this protocol that means we will move to either nostepfix + % or stepfix states + @()updateNextState(me,'trial'); +}; + +prefixFcn = { + @()trackerDrawFixation(eT); + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +prefixExitFcn = { + @()show(stims, 3); +}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%=======================================================NOSTEP FIX + STIMULATION +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%fixate entry +nsfEntryFcn = { + @()edit(stims,1,'offTime',0.1); % make sure we reset this just in case + @()resetTicks(stims); % this function regenerates the delay / off timers for stimulus drawing + @()trackerDrawFixation(eT); + @()logRun(me,'Nostep Fix'); %fprintf current trial info to command window +}; + +%fix within +nsfFcn = { + @()trackerDrawEyePosition(eT); + @()draw(stims, 3); %draw stimulus +}; + +%test we are fixated for a certain length of time +nsfTestFcn = { + % if subject found and held fixation, go to 'nostep' state, otherwise 'breakfix' + @()testSearchHoldFixation(eT,'nostep','breakfix'); +}; + +%exit fixation phase +nsfExitFcn = { + @()hide(stims, 3); + @()show(stims, 1); + @()updateFixationTarget(me, 1, tS.targetFixInit, ... + tS.targetFixTime, tS.targetFixRadius); + @()trackerMessage(eT,'END_FIX'); +}; + +%what to run when we enter the stim presentation state +nsEntryFcn = { + @()trackerDrawFixation(eT); + @()doStrobe(me,true); +}; + +%what to run when we are showing stimuli +nsFcn = { + @()draw(stims, 1); + @()trackerDrawEyePosition(eT); +}; + +%test we are maintaining fixation +nsTestFcn = { + % if subject found and held target, go to 'correct' state, otherwise 'incorrect' + @()testSearchHoldFixation(eT,'correct','incorrect'); +}; + +%as we exit stim presentation state +nsExitFcn = { + @()setStrobeValue(me,255); + @()doStrobe(me,true); +}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%=========================================================STEP FIX + STIMULATION +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +sfEntryFcn = { + @()edit(stims,1,'offTime',0.1); + @()trackerDrawFixation(eT); + @()logRun(me,'Step Fix'); %fprintf current trial info to command window +}; + +sfFcn = { + @()draw(stims, 3); %draw stimulus + @()trackerDrawEyePosition(eT); +}; + +sfTestFcn = { + % if subject found and held fixation, go to 'step' state, otherwise 'breakfix' + @()testSearchHoldFixation(eT,'step','breakfix'); +}; + +sfExitFcn = { + @()hide(stims, 3); + @()show(stims, [1 2]); + @()setDelayTimeWithStaircase(uF, 2, 0.1); + @()resetTicks(stims); + @()updateFixationTarget(me, 2, tS.targetFixInit, ... + tS.targetFixTime, tS.targetFixRadius); + @()trackerMessage(eT,'END_FIX'); +}; + +%what to run when we enter the stim presentation state +sEntryFcn = { + @()trackerDrawFixation(eT); + @()doStrobe(me,true); +}; + +%test we are fixated for a certain length of time +sTestFcn = { + % if subject found and held fixation, go to 'correct' state, otherwise 'incorrect' + @()testSearchHoldFixation(eT,'correct','incorrect'); +}; + +%what to run when we are showing stimuli +sFcn = { + @()trackerDrawEyePosition(eT); + @()draw(stims,[1 2]); +}; + +sExitFcn = { + @()setStrobeValue(me,255); + @()doStrobe(me,true); +}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%=======================================================================DECISION +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +correctEntryFcn = { + @()giveReward(rM); % send a reward TTL + @()beep(aM,tS.correctSound); % correct beep + @()trackerMessage(eT,'END_RT'); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); %send TRIAL_RESULT message to tracker + @()trackerDrawStatus(eT, 'CORRECT! :-)'); + @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()hide(stims); + @()logRun(me,'CORRECT'); %fprintf current trial info +}; + +%correct stimulus +correctFcn = { + @()drawBackground(s); +}; + +%when we exit the correct state +correctExitFcn = { + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()updatePlot(bR, me); %update our behavioural plot + @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial + @()updateStaircaseAfterState(me, tS.CORRECT,'step'); % only update staircase after a stop trial + @()updateVariables(me); %randomise our stimuli, and set strobe value too + @()update(stims); %update our stimuli ready for display + @()plot(bR, 1); % actually do our behaviour record drawing + @()checkTaskEnded(me); %check if task is finished +}; + +%incorrect entry +incEntryFcn = { + @()beep(aM, tS.errorSound); + @()trackerMessage(eT,'END_RT'); + @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.INCORRECT)]); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()needEyeSample(me,false); + @()hide(stims); + @()logRun(me,'INCORRECT'); %fprintf current trial info +}; + +%our incorrect stimulus +incFcn = { + @()drawBackground(s); +}; + +%incorrect / break exit +incExitFcn = { + @()updateStaircaseAfterState(me,tS.BREAKFIX,'step'); % only update staircase after a stop trial + @()updateVariables(me); %randomise our stimuli, don't run updateTask(task), and set strobe value too + @()update(stims); %update our stimuli ready for display + @()plot(bR, 1); % actually do our behaviour record drawing + @()checkTaskEnded(me); %check if task is finished +}; + +%break entry +breakEntryFcn = { + @()beep(aM, tS.errorSound); + @()trackerMessage(eT,'END_RT'); + @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.BREAKFIX)); + @()trackerDrawStatus(eT,'BREAKFIX before complete trial! :-(', stims.stimulusPositions, 0); + @()stopRecording(eT); + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()needEyeSample(me,false); + @()hide(stims); + @()logRun(me,'BREAKFIX'); %fprintf current trial info +}; + +breakExitFcn = incExitFcn; + +if tS.includeErrors + incExitFcn = [ {@()updatePlot(bR, me);@()updateTask(me,tS.INCORRECT)}; incExitFcn ]; + breakExitFcn = [ {@()updatePlot(bR, me);@()updateTask(me,tS.BREAKFIX)}; incExitFcn ]; +else + incExitFcn = [ {@()updatePlot(bR, me);@()resetRun(task)}; incExitFcn ]; % we randomise the run within this block to make it harder to guess next trial + breakExitFcn = [ {@()updatePlot(bR, me);@()resetRun(task)}; incExitFcn ]; % we randomise the run within this block to make it harder to guess next trial +end + + +%==================================================================EXPERIMENTAL CONTROL + +%================================================================== +%==================================================================EYETRACKER +%================================================================== +%--------------------calibration function +calibrateFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; + +%--------------------drift correction function +driftFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) +}; +offsetFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; + +%====================================================================== +%======================================================================GENERAL +%====================================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values + +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine +stateInfoTmp = { +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFcn {} {} pauseExitFcn; +'prefix' 'breakfix' 0.5 prefixEntryFcn prefixFcn {} prefixExitFcn; +%--------------------------------------------------------------------------------------------- +'nostepfix' 'breakfix' 5 nsfEntryFcn nsfFcn nsfTestFcn nsfExitFcn; +'nostep' 'breakfix' 5 nsEntryFcn nsFcn nsTestFcn nsExitFcn; +'stepfix' 'breakfix' 5 sfEntryFcn sfFcn sfTestFcn sfExitFcn; +'step' 'breakfix' 5 sEntryFcn sFcn sTestFcn sExitFcn; +%--------------------------------------------------------------------------------------------- +'breakfix' 'timeout' 0.5 breakEntryFcn incFcn {} breakExitFcn; +'incorrect' 'timeout' 0.5 incEntryFcn incFcn {} incExitFcn; +'correct' 'prefix' 0.5 correctEntryFcn correctFcn {} correctExitFcn; +'timeout' 'prefix' tS.tOut {} {} {} {}; +%--------------------------------------------------------------------------------------------- +'calibrate' 'pause' 0.5 calibrateFcn {} {} {}; +'drift' 'pause' 0.5 driftFcn {} {} {}; +'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFcn {} {} {}; +'flash' 'pause' 0.5 flashFcn {} {} {}; +'showgrid' 'pause' 10 {} gridFcn {} {}; +}; +%--------------------------State Machine Table----------------------------- +%========================================================================== + +disp('=================>> Built state info file <<==================') +disp(stateInfoTmp) +disp('=================>> Built state info file <<=================') +clearvars -regexp '.+Fcn$' % clear the cell array Fcns in the current workspace diff --git a/CoreProtocols/Saccadic_DoubleStep_TRAIN_1.mat b/CoreProtocols/Saccadic_DoubleStep_TRAIN_1.mat new file mode 100644 index 0000000000000000000000000000000000000000..9ed68fb0d0ec1739017a176a43fc73d57694d6c0 Binary files /dev/null and b/CoreProtocols/Saccadic_DoubleStep_TRAIN_1.mat differ diff --git a/CoreProtocols/TwoFigureGroundStateInfo.m b/CoreProtocols/TwoFigureGroundStateInfo.m index 703879d1d48d51ab4a24f905198a4c4ea72b4d53..a20c0a5a2d42b7329d29edb6eed783bbc46066bd 100644 --- a/CoreProtocols/TwoFigureGroundStateInfo.m +++ b/CoreProtocols/TwoFigureGroundStateInfo.m @@ -202,23 +202,54 @@ breakEntryFcn = { @()statusMessage(eT,'Broke Fixation :-('); ...%status message @()hide(obj.stimuli{6}); ... }; -%calibration function -calibrateFcn = { @()setOffline(eT); % set eyelink offline [tobii ignores this] +%======================================================== +%========================================================EYETRACKER +%======================================================== +%--------------------calibration function +calibrateFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; -%debug override -overrideFcn = @()keyOverride(obj); %a special mode which enters a matlab debug state so we can manually edit object values +%--------------------drift correction function +driftFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [others ignores this] + @()setOffline(eT); % set eyelink offline [others ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) +}; +offsetFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; -%screenflash -flashFcn = @()flashScreen(s, 0.2); % fullscreen flash mode for visual background activity detection +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values -%show 1deg size grid -gridFcn = @()drawGrid(s); +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; sM.skipExitStates = {'fixate','incorrect|breakfix'}; -%----------------------State Machine Table------------------------- -disp('================>> Building state info file <<================') -%specify our cell array that is read by the stateMachine +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine stateInfoTmp = { ... 'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; ... 'pause' 'prefix' inf pauseEntryFcn [] [] pauseExitFcn; ... @@ -233,9 +264,10 @@ stateInfoTmp = { ... 'flash' 'pause' 0.5 flashFcn [] [] []; ... 'showgrid' 'pause' 10 [] gridFcn [] []; ... }; +%--------------------------State Machine Table----------------------------- +%========================================================================== +disp('=================>> Built state info file <<==================') disp(stateInfoTmp) -disp('================>> Loaded state info file <<================') -clear pauseEntryFcn fixEntryFcn fixFcn initFixFcn fixExitFcn stimFcn maintainFixFcn incEntryFcn ... - incFcn incExitFcn breakEntryFcn breakFcn correctEntryFcn correctFcn correctExitFcn ... - calibrateFcn overrideFcn flashFcn gridFcn +disp('=================>> Built state info file <<=================') +clearvars -regexp '.+Fcn$' % clear the cell array Fcns in the current workspace diff --git a/CoreProtocols/Two_Images.m b/CoreProtocols/Two_Images.m new file mode 100644 index 0000000000000000000000000000000000000000..f5bc28423810d564fa7baef4c1024f55ee51ac69 --- /dev/null +++ b/CoreProtocols/Two_Images.m @@ -0,0 +1,445 @@ +%========================================================================= +% This task presents a face vs. object stimulus pairs randomly placed left +% or right of fixation. The subject must initiate fixation, then the +% fixation window encompasses most of the screen to allow free viewing of +% either object or face but they must complete 3 seconds of gaze overall in +% the screen window. +%-----------------------------General Settings---------------------------- +% These settings are make changing the behaviour of the protocol easier. tS +% is just a struct(), so you can add your own switches or values here and +% use them lower down. Some basic switches like saveData, useTask, +% checkKeysDuringstimulus will influence the runeExperiment.runTask() +% functionality, not just the state machine. Other switches like +% includeErrors are referenced in this state machine file to change with +% functions are added to the state machine states… +tS.name = 'Two Images'; %==name of this protocol +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.useTask = true; %==use taskSequence (randomises stimulus variables) +tS.includeErrors = false; %==do we update the trial number even for incorrect saccade/fixate, if true then we call updateTask for both correct and incorrect, otherwise we only call updateTask() for correct responses +tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? +tS.askForComments = false; %==UI requestor asks for comments before/after run +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.tOut = 2; %==if wrong response, how long to time out before next trial +tS.CORRECT = 1; %==the code to send eyetracker for correct trials +tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials +tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume + +%========================================================================= +%----------------Debug logging to command window------------------ +% uncomment each line to get specific verbose logging from each of these +% components; you can also set verbose in the opticka GUI to enable all of +% these… +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging + +%========================================================================= +%-----------------INITIAL Eyetracker Settings---------------------- +% These settings define the initial fixation window and set up for the +% eyetracker. They may be modified during the task (i.e. moving the fixation +% window towards a target, enabling an exclusion window to stop the subject +% entering a specific set of display areas etc.) +% +% **IMPORTANT**: you need to make sure that the global state time is larger than +% any fixation timers specified here. Each state has a global timer, so if the +% state timer is 5 seconds but your fixation timer is 6 seconds, then the state +% will finish before the fixation time was completed! +%------------------------------------------------------------------ +% initial fixation X position in degrees (0° is screen centre). Multiple windows +% can be entered using an array. +tS.fixX = 0; +% initial fixation Y position in degrees (0° is screen centre). Multiple windows +% can be entered using an array. +tS.fixY = 0; +% time to search and enter fixation window (Initiate fixation) +tS.firstFixInit = 3; +% time to maintain initial fixation within window, can be single value or a +% range to randomise between +tS.firstFixTime = 0.25; +% fixation window circular radius in degrees; if you enter [x y] the window +% will be rectangular. +tS.firstFixRadius = 2; +% do we forbid eye to enter-exit-reenter fixation window? +tS.strict = false; +% add an exclusion zone where subject cannot saccade to? +tS.exclusionZone = []; +% window & time to maintain fixation during stimulus state +tS.stimulusFixTime = 8; +tS.stimulusFixRadius = [25 20]; +% Initialise eyetracker with X, Y, FixInitTime, FixTime, Radius, StrictFix values +updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); + +%========================================================================= +%-------------------------ONLINE Behaviour Plot--------------------------- +% WHICH states assigned as correct or break for online plot? +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%========================================================================= +%--------------Randomise stimulus variables every trial?----------- +% If you want to have some randomisation of stimuls variables WITHOUT using +% taskSequence task. Remember this will not be "Saved" for later use, if you +% want to do controlled experiments use taskSequence to define proper randomised +% and balanced variable sets and triggers to send to recording equipment etc... +% Good for training tasks, or stimulus variability irrelevant to the task. +% n = 1; +% in(n).name = 'xyPosition'; +% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; +% in(n).stimuli = 1; +% in(n).offset = []; +% stims.stimulusTable = in; +stims.choice = []; +stims.stimulusTable = []; + +%========================================================================= +%-------------allows using arrow keys to control variables?------------- +% another option is to enable manual control of a table of variables +% this is useful to probe RF properties or other features while still +% allowing for fixation or other behavioural control. +% Use arrow keys <- -> to control value and ↑ ↓ to control variable. +stims.controlTable = []; +stims.tableChoice = 1; + +%====================================================================== +% this allows us to enable subsets from our stimulus list +% 1 = grating | 2 = fixation cross +stims.stimulusSets = {[1,2],[1]}; +stims.setChoice = 1; + +%========================================================================= +% N x 2 cell array of regexpi strings, list to skip the current -> next +% state's exit functions; for example skipExitStates = +% {'fixate','incorrect|breakfix'}; means that if the currentstate is +% 'fixate' and the next state is either incorrect OR breakfix, then skip +% the FIXATE exit state. Add multiple rows for skipping multiple state's +% exit states. +sM.skipExitStates = {'fixate','incorrect|breakfix'}; + +%========================================================================= +% which stimulus in the list is used for a fixation target? For this +% protocol it means the subject must saccade to this stimulus (the saccade +% target is #1 in the list) to get the reward. +stims.fixationChoice = 1; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of anonymous function handles which are +% executed by the state machine to control the experiment. The state +% machine can run sets at entry ['entryFcn'], during ['withinFcn'], to +% trigger a transition jump to another state ['transitionFcn'], and at exit +% ['exitFcn'. Remember these {sets} need to access the objects that are +% available within the runExperiment context (see top of file). You can +% also add global variables/objects then use these. The values entered here +% are set on load, if you want up-to-date values then you need to use +% methods/function wrappers to retrieve/set them. +%========================================================================= + +%============================================================== +%========================================================PAUSE +%============================================================== + +%--------------------pause entry +pauseEntryFcn = { + @()hide(stims); % hide all stimuli + @()drawBackground(s); % blank the subject display + @()drawPhotoDiodeSquare(s,[0 0 0]); % draw black photodiode + @()drawTextNow(s,'PAUSED, press [p] to resume...'); + @()disp('PAUSED, press [p] to resume...'); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', stims.stimulusPositions); + @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF + @()resetAll(eT); % reset all fixation markers to initial state + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii + @()needFlip(me, false); % no need to flip the PTB screen + @()needEyeSample(me, false); % no need to check eye position +}; + +%--------------------pause exit +pauseExitFcn = { + %start recording eye position data again, note true is required here as + %the eyelink is started and stopped on each trial, but the tobii runs + %continuously, so @()startRecording(eT) only affects eyelink but + %@()startRecording(eT, true) affects both eyelink and tobii... + @()startRecording(eT, true); +}; + +%============================================================== +%====================================================PRE-FIXATION +%============================================================== +%--------------------prefixate entry +prefixEntryFcn = { + @()needFlip(me, true, 1); % enable the screen and trackerscreen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims); % make a struct the eT can use for drawing stim positions + @()hide(stims); % hide all stimuli + % update the fixation window to initial values + @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime,tS.firstFixRadius,tS.strict); %reset fixation window + @()trackerTrialStart(eT, getTaskIndex(me)); + @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure + @()trackerDrawStatus(eT,'PREFIXATION...', stims.stimulusPositions, 0, false); +}; + +%--------------------prefixate within +prefixFcn = { + +}; + +%--------------------prefixate exit +prefixExitFcn = { + +}; + +%============================================================== +%====================================================FIXATION +%============================================================== +%--------------------fixate entry +fixEntryFcn = { + @()show(stims{3}); % show fixation cross + @()logRun(me,'INITFIX'); + @()trackerDrawStatus(eT,'Start fixation...', stims.stimulusPositions); +}; + +%--------------------fix within +fixFcn = { + @()draw(stims); %draw stimuli + @()trackerDrawEyePosition(eT); + @()animate(stims); +}; + +%--------------------test we are fixated for a certain length of time +inFixFcn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testSearchHoldFixation(eT,'stimulus','breakfix') +}; + +%--------------------exit fixation phase +fixExitFcn = { + % X, Y, FixInitTime, FixTime, Radius, StrictFix values + @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime, tS.stimulusFixRadius, tS.strict); + @()show(stims,[1 2]); % show images + @()hide(stims,3); % hide fixation cross +}; + +%======================================================== +%========================================================STIMULUS +%======================================================== + +stimEntryFcn = { + % send stimulus value strobe (value set by updateVariables(me) function on previous trial) + @()doStrobe(me,true); + % send 0-time sync signal to eyetracker + @()doSyncTime(me); +}; + +%--------------------what to run when we are showing stimuli +stimFcn = { + @()draw(stims); + @()trackerDrawEyePosition(eT); + @()animate(stims); % animate stimuli for subsequent draw +}; + +%-----------------------test we are maintaining fixation +maintainFixFcn = { + % this command performs the logic to search and then maintain fixation + % inside the fixation window. The eyetracker parameters are defined above. + % If the subject does initiate and then maintain fixation, then 'correct' + % is returned and the state machine will jump to the correct state, + % otherwise 'breakfix' is returned and the state machine will jump to the + % breakfix state. If neither condition matches, then the state table below + % defines that after 5 seconds we will switch to the incorrect state. + @()testHoldFixation(eT,'correct','breakfix'); +}; + +%as we exit stim presentation state +stimExitFcn = { + @()setStrobeValue(me, 255); % 255 indicates stimulus OFF + @()doStrobe(me, true); % send strobe on next flip +}; + +%======================================================== +%========================================================DECISIONS +%======================================================== + +%========================================================CORRECT +%--------------------if the subject is correct (small reward) +correctEntryFcn = { + @()trackerTrialEnd(eT, tS.CORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); % no need to collect eye data until we start the next trial + @()hide(stims); % hide all stims +}; + +%--------------------correct stimulus +correctFcn = { + +}; + +%--------------------when we exit the correct state +correctExitFcn = { + @()giveReward(rM); % send a reward + @()beep(aM, tS.correctSound); % correct beep + @()logRun(me,'CORRECT'); % print current trial info + @()trackerDrawStatus(eT, 'CORRECT! :-)', stims.stimulusPositions); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updatePlot(bR, me); % must run before updateTask + @()updateTask(me,tS.CORRECT); % make sure our taskSequence is moved to the next trial + @()updateVariables(me); % randomise our stimuli, and set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our behaviour record drawing +}; + +%========================================================INCORRECT/BREAKFIX +%--------------------incorrect entry +incEntryFcn = { + @()trackerTrialEnd(eT, tS.INCORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; +%--------------------break entry +breakEntryFcn = { + @()trackerTrialEnd(eT, tS.BREAKFIX); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; + +%--------------------our incorrect/breakfix stimulus +incFcn = { + +}; + +%--------------------incorrect exit +incExitFcn = { + @()beep(aM, tS.errorSound); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; +%--------------------break exit +breakExitFcn = { + @()beep(aM, tS.errorSound); + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', stims.stimulusPositions); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; + +%--------------------change functions based on tS settings +% we use tS options to change the function lists run by the state machine. +% We can prepend or append new functions to the cell arrays. +% +% logRun = add current info to behaviural record +% updatePlot = updates the behavioural record +% updateTask = updates task object +% resetRun = randomise current trial within the block (makes it harder for +% subject to guess based on previous failed trial. +% checkTaskEnded = see if taskSequence has finished +if tS.includeErrors % we want to update our task even if there were errors + incExitFcn = [ {@()logRun(me,'INCORRECT'); @()updatePlot(bR, me); @()updateTask(me,tS.INCORRECT)}; incExitFcn ]; %update our taskSequence + breakExitFcn = [ {@()logRun(me,'BREAK_FIX'); @()updatePlot(bR, me); @()updateTask(me,tS.BREAKFIX)}; breakExitFcn ]; %update our taskSequence +else + incExitFcn = [ {@()logRun(me,'INCORRECT'); @()updatePlot(bR, me); @()resetRun(task)}; incExitFcn ]; + breakExitFcn = [ {@()logRun(me,'BREAK_FIX'); @()updatePlot(bR, me); @()resetRun(task)}; breakExitFcn ]; +end +if tS.useTask || task.nBlocks > 0 + correctExitFcn = [ correctExitFcn; {@()checkTaskEnded(me)} ]; + incExitFcn = [ incExitFcn; {@()checkTaskEnded(me)} ]; + breakExitFcn = [ breakExitFcn; {@()checkTaskEnded(me)} ]; +end + +%======================================================== +%========================================================EYETRACKER +%======================================================== +%--------------------calibration function +calibrateFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerSetup(eT); %enter tracker calibrate/validate setup mode +}; + +%--------------------drift correction function +driftFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) +}; +offsetFcn = { + @()drawBackground(s); %blank the display + @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] + @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) +}; + +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values + +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine +stateInfoTmp = { +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFcn {} {} pauseExitFcn; +%--------------------------------------------------------------------------------------------- +'prefix' 'fixate' 0.75 prefixEntryFcn prefixFcn {} {}; +'fixate' 'incorrect' 10 fixEntryFcn fixFcn inFixFcn fixExitFcn; +'stimulus' 'incorrect' 12 stimEntryFcn stimFcn maintainFixFcn stimExitFcn; +'correct' 'prefix' 0.1 correctEntryFcn correctFcn {} correctExitFcn; +'incorrect' 'timeout' 0.1 incEntryFcn incFcn {} incExitFcn; +'breakfix' 'timeout' 0.1 breakEntryFcn incFcn {} breakExitFcn; +'timeout' 'prefix' tS.tOut {} incFcn {} {}; +%--------------------------------------------------------------------------------------------- +'calibrate' 'pause' 0.5 calibrateFcn {} {} {}; +'drift' 'pause' 0.5 driftFcn {} {} {}; +'offset' 'pause' 0.5 offsetFcn {} {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFcn {} {} {}; +'flash' 'pause' 0.5 flashFcn {} {} {}; +'showgrid' 'pause' 10 {} gridFcn {} {}; +}; +%--------------------------State Machine Table----------------------------- +%========================================================================== + +disp('=================>> Built state info file <<==================') +disp(stateInfoTmp) +disp('=================>> Built state info file <<=================') +clearvars -regexp '.+Fcn$' % clear the cell array Fcns in the current workspace diff --git a/CoreProtocols/Two_Images.mat b/CoreProtocols/Two_Images.mat new file mode 100644 index 0000000000000000000000000000000000000000..8c25b1100f2bf1c56488241686b6c03b2834905e Binary files /dev/null and b/CoreProtocols/Two_Images.mat differ diff --git a/CoreProtocols/Twostep_Saccade.mat b/CoreProtocols/Twostep_Saccade.mat deleted file mode 100644 index 07463191b462cbd8f66067bcbff198b1cc056019..0000000000000000000000000000000000000000 Binary files a/CoreProtocols/Twostep_Saccade.mat and /dev/null differ diff --git a/CoreProtocols/Twostep_Saccade_StateInfo.m b/CoreProtocols/Twostep_Saccade_StateInfo.m deleted file mode 100644 index bdf043d44d8183e634e84306d1e227aeca4f882f..0000000000000000000000000000000000000000 --- a/CoreProtocols/Twostep_Saccade_StateInfo.m +++ /dev/null @@ -1,519 +0,0 @@ -%> TWOSTEP SACCADE state file, this gets loaded by opticka via -%> runExperiment class. You can set up any state and define the logic of -%> which functions to run when you enter, are within, or exit a state. -%> Objects provide many methods you can run, like sending triggers, showing -%> stimuli, controlling the eyetracker etc. -% -%> This state file is loaded by the runExperiment class. runExperiment -%> initialises other classes that are used to control the experiment. The -%> following class objects are already loaded and available to use: -% -%> me = runExperiment object -%> s = screenManager -%> sM = State Machine -%> eT = eyetracker manager -%> task = task sequence (taskSequence class) -%> stims = our list of stimuli -%> io = digital I/O to recording system -%> aM = audioManager -%> rM = Reward Manager (LabJack or Arduino TTL trigger to reward system/Magstim) -%> bR = behavioural record plot (on screen GUI of trial performance during task run) -%> tL = timeLog that records the timing of experiment -%> tS = general struct to hold variables for this run, will be saved as part of the data - -%================================================================== -%---------------------------TASK CONFIG---------------------------- -%do we update the trial number even for incorrect saccades, if true then we -%call updateTask for both correct and incorrect, otherwise we only call -%updateTask() for correct responses -tS.includeErrors = false; -% we use taskSequence to randomise which state to switch to (independent -% trial-level factor). The idea is we we call -% @()updateNextState(me,'trial') in the prefixation state; this sets one of -% these two trialVar.values as the next state. The fix1Step and fix2Step -% states will then call onestep or twostep stimulus states. Therefore we can -% call different experiment structures based on this trial-level factor. -%task.trialVar.values = {'fix1Step','fix2Step'}; -%task.trialVar.probability = [0.6 0.4]; -task.trialVar.comment = 'one or twostep trial based on 60:40 probability'; -tL.stimStateNames = ["onestep","twostep"]; - -%================================================================== -%----------------------Staircase manager--------------------------- -scopts = struct('up',1,'down',3,'StepSizeDown',0.05,... - 'StepSizeUp',0.05,'stopcriterion','trials',... - 'stoprule',50,'startvalue',0.3); -task.staircase = staircaseManager('udsettings',{scopts}); - -%================================================================== -%----------------------General Settings---------------------------- -tS.useTask = true; %==use taskSequence (randomises stimulus variables) -tS.rewardTime = 250; %==TTL time in milliseconds -tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. -tS.checkKeysDuringStimulus = true; %==allow keyboard control? Slight drop in performance -tS.recordEyePosition = true; %==record eye position within PTB, **in addition** to the EDF? -tS.askForComments = false; %==little UI requestor asks for comments before/after run -tS.saveData = true; %==save behavioural and eye movement data? -tS.name = 'twostep-saccade'; %==name of this protocol -tS.nStims = stims.n; %==number of stimuli -tS.tOut = 1; %if wrong response, how long to time out before next trial -tS.CORRECT = 1; %==the code to send eyetracker for correct trials -tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials -tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials -tS.keyExclusionPattern = ["fixate","onestep","twostep"]; % avoid keyboard commands for these states - -%================================================================== -%------------Debug logging to command window----------------- -%io.verbose = true; %==print out io commands for debugging -%eT.verbose = true; %==print out eyelink commands for debugging -%rM.verbose = true; %==print out reward commands for debugging - -%================================================================== -%-----------------INITIAL Eyetracker Settings---------------------- -tS.fixX = 0; % X position in degrees (screen center) -tS.fixY = 0; % X position in degrees (screen center) -tS.firstFixInit = 3; % time to search and enter fixation window -tS.firstFixTime = [0.5 1.25]; % time to maintain fixation within window -tS.firstFixRadius = 3; % radius in degrees -tS.strict = true; % do we forbid eye to enter-exit-reenter fixation window? -tS.exclusionRadius = 5; % radius of the exclusion zone... -tS.targetFixInit = 3; % time to find the target -tS.targetFixTime = 0.25; % to to maintain fixation on target -tS.targetRadius = 6; %radius to fix within. -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; -me.lastXExclusion = []; -me.lastYExclusion = []; - -%================================================================== -%---------------------------Eyetracker setup----------------------- -% note: opticka UI sets some defaults, but these will override the UI -if strcmp(me.eyetracker.device, 'eyelink') - warning('Note this protocol is optimised for the Tobii eyetracker, beware...') - eT.name = tS.name; - eT.sampleRate = 250; % sampling rate - eT.calibrationStyle = 'HV5'; % calibration style - eT.calibrationProportion = [0.4 0.4]; %the proportion of the screen occupied by the calibration stimuli - if tS.saveData == true; eT.recordData = true; end %===save EDF file? - %----------------------- - % remote calibration enables manual control and selection of each fixation - % this is useful for a baby or monkey who has not been trained for fixation - % use 1-9 to show each dot, space to select fix as valid, INS key ON EYELINK KEYBOARD to - % accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; % calibration target colour - eT.modify.calibrationtargetsize = 2; % size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; % width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; % -1 = use any attachedkeyboard - eT.modify.targetbeep = 1; % beep during calibration -elseif strcmp(me.eyetracker.device, 'tobii') - eT.name = tS.name; - %eT.model = 'Tobii Pro Spectrum'; - %eT.sampleRate = 300; - %eT.trackingMode = 'human'; - %eT.calibrationStimulus = 'animated'; - %eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each fixation - % this is useful for a baby or monkey who has not been trained for fixation - %eT.manualCalibration = false; - %----------------------- - %eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - %eT.valPositions = [ .5 .5 ]; -end -%Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix -eT.updateFixationValues(tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%make sure we don't start with any exclusion zones set up -eT.resetExclusionZones(); - -%================================================================== -%----WHICH states assigned as correct or break for online plot?---- -%----You need to use regex patterns for the match (doc regexp)----- -bR.correctStateName = "correct"; -bR.breakStateName = ["breakfix","incorrect"]; - -%================================================================== -%-----simplistic randomisation of stimulus variables every trial?----- -% if you want to have some randomisation of stimuls variables without using -% taskSequence task (e.g. for training), you can uncomment this and -% runExperiment can use this structure to change e.g. X or Y position, -% size, angle see metaStimulus for more details. Remember this will not be -% "Saved" for later use, if you want to do controlled methods of constants -% experiments use taskSequence to define proper randomised and balanced -% variable sets and triggers to send to recording equipment etc... -% -stims.stimulusTable = []; -stims.choice = []; - -%================================================================== -%-------------allows using arrow keys to control variables?------------- -% another option is to enable manual control of a table of variables -% this is useful to probe RF properties or other features while still -% allowing for fixation or other behavioural control. -% Use arrow keys <- -> to control value and up/down to control variable -stims.controlTable = []; -stims.tableChoice = 1; - -%================================================================== -%this allows us to enable subsets from our stimulus list -% 1 = grating | 2 = fixation cross -stims.stimulusSets = {[2],[1,2]}; -stims.setChoice = 1; -hide(stims); - -%================================================================== -% which stimulus in the list is used for a fixation target? For this -% protocol it means the subject must fixate this stimulus (the saccade -% target is #1 in the list) to get the reward. Also which stimulus to set -% an exclusion zone around (where a saccade into this area causes an -% immediate break fixation). -stims.fixationChoice = [1 2]; -stims.exclusionChoice = []; - -%================================================================== -% N x 2 cell array of regexpi strings, list to skip the current -> next -% state's exit functions; for example skipExitStates = -% {'fixate','incorrect|breakfix'}; means that if the currentstate is -% 'fixate' and the next state is either incorrect OR breakfix, then skip -% the FIXATE exit state. Add multiple rows for skipping multiple state's -% exit states. -sM.skipExitStates = {'fixate','incorrect|breakfix'}; - - -%=================================================================== -%=================================================================== -%=================================================================== -%-----------------State Machine Task Functions--------------------- -% Each cell {array} holds a set of anonymous function handles which are -% executed by the state machine to control the experiment. The state -% machine can run sets at entry ['entryFcn'], during ['withinFcn'], to -% trigger a transition jump to another state ['transitionFcn'], and at exit -% ['exitFcn'. Remember these {sets} need to access the objects that are -% available within the runExperiment context (see top of file). You can -% also add global variables/objects then use these. The values entered here -% are set on load, if you want up-to-date values then you need to use -% methods/function wrappers to retrieve/set them. - -%====================================================PAUSE -%pause entry -pauseEntryFcn = { - @()hide(stims); - @()drawBackground(s); %blank the subject display - @()drawText(s,'PAUSED, press [p] to resume...'); - @()flip(s); - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawText(eT,'PAUSED, press [p] to resume...'); - @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF - @()stopRecording(eT, true); %stop recording eye position data - @()needFlip(me, false); % no need to flip the PTB screen - @()needEyeSample(me,false); % no need to check eye position - @()fprintf('\n\nPAUSED, press [p] to resume...\n\n'); -}; - -%pause exit -pauseExitFcn = { - @()startRecording(eT, true); %start recording eye position data again -}; - -%====================================================PREFIXATION -prefixEntryFcn = { - @()needFlip(me, true); - @()hide(stims); - @()edit(stims,3,'alphaOut',0.5); - @()edit(stims,3,'alpha2Out',1); - @()resetFixationHistory(eT); % reset the recent eye position history - @()resetExclusionZones(eT); % reset any exclusion zones on eyetracker - @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window to initial values - % updateNextState method is critical, it reads the independent trial factor in - % taskSequence to select state to transition to next. This sets - % stateMachine.tempNextState to override the state table's default next field. - @()updateNextState(me,'trial'); -}; - -prefixFcn = { - -}; - -prefixExitFcn = { - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker - @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure - @()trackerClearScreen(eT); % blank the eyelink screen - @()trackerDrawFixation(eT); % draw fixation window on eyetracker display - @()trackerDrawStimuli(eT,stims.stimulusPositions); %draw location of stimulus on eyetracker - @()needEyeSample(me,true); % make sure we start measuring eye position - % IMPORTANT! updateNextState uses the trial/block factor in task to select - % which state to transition to next - @()updateNextState(me,'trial'); -}; - -%====================================================ONESTEP FIXATION + STIMULATION - -%fixate entry -fixOSEntryFcn = { - @()show(stims, 3); - @()logRun(me,'INITFIXOneStep'); %fprintf current trial info to command window -}; - -%fix within -fixOSFcn = { - @()draw(stims, 3); %draw stimulus -}; - -%test we are fixated for a certain length of time -inFixOSFcn = { - % if subject found and held fixation, go to 'onestep' state, otherwise 'incorrect' - @()testSearchHoldFixation(eT,'onestep','incorrect'); -}; - -%exit fixation phase -fixOSExitFcn = { - @()resetTicks(stims); - @()show(stims, 1); - @()hide(stims, 2); - @()edit(stims,1,'offTime',0.1); - @()set(stims,'fixationChoice',1); % choose stim 1 as fixation target - @()updateFixationTarget(me, tS.useTask, tS.targetFixInit, ... - tS.targetFixTime, tS.targetRadius, false); - @()trackerMessage(eT,'END_FIX'); -}; - -%what to run when we enter the stim presentation state -osEntryFcn = { - @()doStrobe(me,true); - @()logRun(me,'ONESTEP'); %fprintf current trial info to command window -}; - -%what to run when we are showing stimuli -osFcn = { - @()draw(stims, 1); - @()drawText(s,'ONESTEP'); - @()animate(stims); % animate stimuli for subsequent draw -}; - -%test we are maintaining fixation -maintainFixFcn = { - % if subject found and held fixation, go to 'onestep' state, otherwise 'incorrect' - @()testSearchHoldFixation(eT,'correct','breakfix'); -}; - -%as we exit stim presentation state -sExitFcn = { - @()setStrobeValue(me,255); - @()doStrobe(me,true); -}; - -%====================================================TWOSTEP FIXATION + STIMULATION - -%fixate entry -fixTSEntryFcn = { - @()show(stims, 3); - @()logRun(me,'INITFIXTwoStep'); %fprintf current trial info to command window -}; - -%fix within -fixTSFcn = { - @()draw(stims, 3); %draw stimulus -}; - -%test we are fixated for a certain length of time -inFixTSFcn = { - % if subject found and held fixation, go to 'twostep' state, otherwise 'incorrect' - @()testSearchHoldFixation(eT,'twostep','incorrect'); -}; - -%exit fixation phase -fixTSExitFcn = { - @()resetTicks(stims); - @()edit(stims,1,'offTime',0.1); - @()edit(stims,2,'delayTime',0.1); - @()edit(stims,2,'offTime',0.2); - @()show(stims); - @()set(stims,'fixationChoice',2); % choose stim 2 as fixation target - @()updateFixationTarget(me, tS.useTask, tS.targetFixInit, ... - tS.targetFixTime, tS.targetRadius, false); - @()trackerMessage(eT,'END_FIX'); -}; - -%what to run when we enter the stim presentation state -tsEntryFcn = { - @()doStrobe(me,true); - @()logRun(me,'TWOSTEP'); %fprintf current trial info to command window -}; - -%what to run when we are showing stimuli -tsFcn = { - @()draw(stims,[1 2]); - @()drawText(s,'TWOSTEP'); - @()animate(stims); % animate stimuli for subsequent draw -}; - -%====================================================DECISION - -%if the subject is correct (small reward) -correctEntryFcn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM,2000); % correct beep - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.CORRECT)]); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Correct! :-)'); - @()needEyeSample(me,false); % no need to collect eye data until we start the next trial - @()hide(stims); - @()logRun(me,'CORRECT'); %fprintf current trial info -}; - -%correct stimulus -correctFcn = { - @()drawBackground(s); -}; - -%when we exit the correct state -correctExitFcn = { - @()updatePlot(bR, me); %update our behavioural plot - @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial - @()updateVariables(me); %randomise our stimuli, and set strobe value too - @()update(stims); %update our stimuli ready for display - @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions - @()resetExclusionZones(eT); %reset the exclusion zones - @()drawnow; - @()checkTaskEnded(me); %check if task is finished -}; - -%incorrect entry -incEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.INCORRECT)]); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Incorrect! :-('); - @()needEyeSample(me,false); - @()hide(stims); - @()logRun(me,'INCORRECT'); %fprintf current trial info -}; - -%our incorrect stimulus -incFcn = { - @()drawBackground(s); -}; - -%incorrect / break exit -incExitFcn = { - @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() - @()updateVariables(me); %randomise our stimuli, don't run updateTask(task), and set strobe value too - @()update(stims); %update our stimuli ready for display - @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions - @()resetExclusionZones(eT); %reset the exclusion zones - @()drawnow; - @()checkTaskEnded(me); %check if task is finished -}; -if tS.includeErrors - incExitFcn = [ {@()updateTask(me,tS.BREAKFIX)}; incExitFcn ]; % make sure our taskSequence is moved to the next trial -else - incExitFcn = [ {@()resetRun(task)}; incExitFcn ]; % we randomise the run within this block to make it harder to guess next trial -end - -%break entry -breakEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.BREAKFIX)]); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Broke maintain fix! :-('); - @()needEyeSample(me,false); - @()hide(stims); - @()logRun(me,'BREAKFIX'); %fprintf current trial info -}; - -exclEntryFcn = { - @()beep(aM,400,0.5,1); - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,['TRIAL_RESULT ' str2double(tS.BREAKFIX)]); - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Exclusion Zone entered! :-('); - @()needEyeSample(me,false); - @()hide(stims); - @()logRun(me,'EXCLUSION'); %fprintf current trial info -}; - -%====================================================EXPERIMENTAL CONTROL - -%calibration function, can only be triggered from keyboard -calibrateFcn = { - @()drawBackground(s); %blank the display - @()flip(s); - @()trackerMessage(eT,'TRIAL_RESULT -100'); - @()stopRecording(eT, true); % stop eyelink recording data - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()trackerSetup(eT); %enter tracker calibrate/validate setup mode -}; - -%--------------------drift correction function, can only be triggered from keyboard -driftFcn = { - @()drawBackground(s); %blank the display - @()flip(s); - @()trackerMessage(eT,'TRIAL_RESULT -100'); - @()stopRecording(eT, true); % stop eyelink recording data - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()driftCorrection(eT) % enter drift correct (only eyelink) -}; -offsetFcn = { - @()drawBackground(s); %blank the display - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()driftOffset(eT) % enter drift offset (works on tobii & eyelink) -}; - -%debug override, can only be triggered from keyboard -overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually inspect object values - -%screenflash, can only be triggered from keyboard -flashFcn = { - @()drawBackground(s); %blank the display - @()flip(s); - @()trackerMessage(eT,'TRIAL_RESULT -100'); - @()stopRecording(eT, true); % stop eyelink recording data - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()flashScreen(s, 0.2) % fullscreen flash mode for visual background activity detection -}; - -%show 1deg size grid -gridFcn = { - @()drawBackground(s); %blank the display - @()flip(s); - @()trackerMessage(eT,'TRIAL_RESULT -100'); - @()stopRecording(eT, true); % stop eyelink recording data - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()drawGrid(s); -}; - -%============================================================================== -%----------------------State Machine Table------------------------- -% specify our cell array that is read by the stateMachine -stateInfoTmp = { -'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; -'pause' 'prefix' inf pauseEntryFcn {} {} pauseExitFcn; -'prefix' 'UseTemp' 2 prefixEntryFcn prefixFcn {} prefixExitFcn; -'fix1Step' 'incorrect' 5 fixOSEntryFcn fixOSFcn inFixOSFcn fixOSExitFcn; -'fix2Step' 'incorrect' 5 fixTSEntryFcn fixTSFcn inFixTSFcn fixTSExitFcn; -'onestep' 'incorrect' 5 osEntryFcn osFcn maintainFixFcn sExitFcn; -'twostep' 'incorrect' 5 tsEntryFcn tsFcn maintainFixFcn sExitFcn; -'incorrect' 'timeout' 0.5 incEntryFcn incFcn {} incExitFcn; -'breakfix' 'timeout' 0.5 breakEntryFcn incFcn {} incExitFcn; -'exclusion' 'timeout' 0.5 exclEntryFcn incFcn {} incExitFcn; -'correct' 'prefix' 0.5 correctEntryFcn correctFcn {} correctExitFcn; -'useTemp' 'prefix' 0.5 {} {} {} {}; -'timeout' 'prefix' tS.tOut {} {} {} {}; -'calibrate' 'pause' 0.5 calibrateFcn {} {} {}; -'drift' 'pause' 0.5 driftFcn {} {} {}; -'override' 'pause' 0.5 overrideFcn {} {} {}; -'flash' 'pause' 0.5 flashFcn {} {} {}; -'showgrid' 'pause' 10 {} gridFcn {} {}; -}; -%----------------------State Machine Table------------------------- -%============================================================================== -disp('================>> Building state info file <<================') -disp(stateInfoTmp) -disp('=================>> Loaded state info file <<=================') -clearvars -regexp '.+Fcn$' % clear the cell array Fcns in the current workspace diff --git a/CoreProtocols/fNIRS.m b/CoreProtocols/fNIRS.m new file mode 100644 index 0000000000000000000000000000000000000000..8016a6c5dc8e6222a7a9441ac555cf2377226479 --- /dev/null +++ b/CoreProtocols/fNIRS.m @@ -0,0 +1,336 @@ +% FNIRS protocol. DOESN'T use the eyetracker, runs a polar grating with +% different conditions, but we do present a fixation cross to guide the +% subject's eye's stable. +% +% me = runExperiment object ('self' in OOP terminology) +% s = screenManager object +% aM = audioManager object +% stims = our list of stimuli (metaStimulus class) +% sM = State Machine (stateMachine class) +% task = task sequence (taskSequence class) +% eT = eyetracker manager +% io = digital I/O to recording system +% rM = Reward Manager (LabJack or Arduino TTL trigger to reward system/Magstim) +% bR = behavioural record plot (on-screen GUI during a task run) +% uF = user functions - add your own functions to this class +% tS = structure to hold general variables, will be saved as part of the data + +%========================================================================= +%-----------------------------General Settings---------------------------- +% These settings make changing the behaviour of the protocol easier. tS +% is just a struct(), so you can add your own switches or values here and +% use them lower down. Some basic switches like saveData, useTask, +% enableTrainingKeys will influence the runeExperiment.runTask() +% functionality, not just the state machine. Other switches like +% includeErrors are referenced in this state machine file to change which +% functions are added to the state machine states… +tS.name = 'FNIRS'; %==name of this protocol +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… +tS.useTask = true; %==use taskSequence (randomises stimulus variables) +tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording +tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? +tS.askForComments = true; %==UI requestor asks for comments before/after run +tS.includeErrors = false; %==do we update the trial number even for incorrect saccade/fixate, if true then we call updateTask for both correct and incorrect, otherwise we only call updateTask() for correct responses +tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object +tS.timeOut = 0.75; %==if wrong response, how long to time out before next trial +tS.CORRECT = 1; %==the code to send for correct trials +tS.BREAKFIX = -1; %==the code to send for break fix trials +tS.INCORRECT = -5; %==the code to send for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume + +%================================================================== +%------------ ----DEBUG LOGGING to command window------------------ +% uncomment each line to get specific verbose logging from each of these +% components; you can also set verbose in the opticka GUI to enable all of +% these… +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging + +%================================================================== +%-----------------BEAVIOURAL PLOT CONFIGURATION-------------------- +%--WHICH states assigned correct / incorrect for the online plot?-- +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%========================================================================= +%------------------Randomise stimulus variables every trial?-------------- +% If you want to have some randomisation of stimuls variables WITHOUT using +% taskSequence task. Remember this will not be "Saved" for later use, if you +% want to do controlled experiments use taskSequence to define proper randomised +% and balanced variable sets and triggers to send to recording equipment etc... +% Good for training tasks, or stimulus variability irrelevant to the task. +% n = 1; +% in(n).name = 'xyPosition'; +% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; +% in(n).stimuli = 1; +% in(n).offset = []; +% stims.stimulusTable = in; +stims.choice = []; +stims.stimulusTable = []; + +%========================================================================= +%--------------allows using arrow keys to control variables?-------------- +% another option is to enable manual control of a table of variables +% in-task. This is useful to dynamically probe RF properties or other +% features while still allowing for fixation or other behavioural control. +% Use arrow keys <- -> to control value and ↑ ↓ to control variable. +stims.controlTable = []; +stims.tableChoice = 1; + +%====================================================================== +% this allows us to enable subsets from our stimulus list +stims.stimulusSets = {[1,2],[1]}; +stims.setChoice = 1; + +%========================================================================= +% N x 2 cell array of regexpi strings, list to skip the current -> next +% state's exit functions; for example skipExitStates = +% {'fixate','incorrect|breakfix'}; means that if the currentstate is +% 'fixate' and the next state is either incorrect OR breakfix, then skip +% the FIXATE exit state. Add multiple rows for skipping multiple state's +% exit states. +sM.skipExitStates = {'fixate','incorrect|breakfix'}; + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= +%------------------State Machine Task Functions--------------------- +% Each cell {array} holds a set of anonymous function handles which are +% executed by the state machine to control the experiment. The state +% machine can run sets at entry ['entryFcn'], during ['withinFcn'], to +% trigger a transition jump to another state ['transitionFcn'], and at exit +% ['exitFcn'. Remember these {sets} need to access the objects that are +% available within the runExperiment context (see top of file). You can +% also add global variables/objects then use these. The values entered here +% are set on load, if you want up-to-date values then you need to use +% methods/function wrappers to retrieve/set them. +%========================================================================= + +%============================================================== +%========================================================PAUSE +%============================================================== + +%--------------------pause entry +pauseEntryFcn = { + @()hide(stims); % hide all stimuli + @()drawBackground(s); % blank the subject display + @()drawPhotoDiodeSquare(s,[0 0 0]); % draw black photodiode + @()drawTextNow(s,'PAUSED, press [p] to resume...'); + @()disp('PAUSED, press [p] to resume...'); + @()needFlip(me, false, 0); % no need to flip the PTB screen or tracker + @()needEyeSample(me, false); % no need to check eye position +}; + +%--------------------pause exit +pauseExitFcn = { + +}; + +%============================================================== +%====================================================PRE-FIXATION +%============================================================== +%--------------------prefixate entry +prefixEntryFcn = { + @()needFlip(me, true, 1); % enable the screen and trackerscreen flip + @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims); % make a struct eT can use for drawing stim positions + @()hide(stims); % hide all stimuli +}; + +%--------------------prefixate within +prefixFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------prefixate exit +prefixExitFcn = { + @()logRun(me,'INITFIX'); +}; + +%============================================================== +%====================================================FIXATION +%============================================================== +%--------------------fixate entry +fixEntryFcn = { + @()show(stims{tS.nStims}); % show last stim which is usually fixation cross +}; + +%--------------------fix within +fixFcn = { + @()draw(stims); %draw stimuli + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------exit fixation phase +fixExitFcn = { + @()show(stims); % show all stims +}; + +%======================================================== +%========================================================STIMULUS +%======================================================== + +stimEntryFcn = { + % send stimulus value strobe (value alreadyset by updateVariables(me) function) + @()doStrobe(me,true); +}; + +%--------------------what to run when we are showing stimuli +stimFcn = { + @()draw(stims); + @()drawPhotoDiodeSquare(s,[1 1 1]); + @()animate(stims); % animate stimuli for subsequent draw +}; + +%as we exit stim presentation state +stimExitFcn = { + @()prepareStrobe(io, 255); % stim OFF = 255 + @()doStrobe(me, true); +}; + +%======================================================== +%========================================================DECISIONS +%======================================================== + +%========================================================CORRECT +%--------------------if the subject is correct (small reward) +correctEntryFcn = { + @()hide(stims); % hide all stims +}; + +%--------------------correct stimulus +correctFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------when we exit the correct state +correctExitFcn = { + @()giveReward(rM); % send a reward + @()beep(aM, tS.correctSound); % correct beep + @()logRun(me,'CORRECT'); % print current trial info + @()updatePlot(bR, me); % must run before updateTask + @()updateTask(me, tS.CORRECT); % make sure our taskSequence is moved to the next trial + @()updateVariables(me); % randomise our stimuli, and set strobe value too + @()update(stims); % update our stimuli ready for display + @()plot(bR, 1); % actually do our behaviour record drawing +}; + +%========================================================INCORRECT/BREAKFIX +%--------------------incorrect entry +incEntryFcn = { + @()hide(stims); +}; +%--------------------break entry +breakEntryFcn = { + @()hide(stims); +}; + +%--------------------our incorrect/breakfix stimulus +incFcn = { + @()drawPhotoDiodeSquare(s,[0 0 0]); +}; + +%--------------------generic exit +exitFcn = { + % tS.includeErrors will prepend some code here... + @()beep(aM, tS.errorSound); + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers + @()plot(bR, 1); % actually do our drawing +}; + +%--------------------change functions based on tS settings +% we use tS options to change the function lists run by the state machine. +% We can prepend or append new functions to the cell arrays. +% +% logRun = add current info to behaviural record +% updatePlot = updates the behavioural record +% updateTask = updates task object +% resetRun = randomise current trial within the block (makes it harder for +% subject to guess based on previous failed trial. +% checkTaskEnded = see if taskSequence has finished +if tS.includeErrors % we want to update our task even if there were errors + incExitFcn = [ { + @()logRun(me,'INCORRECT'); + @()updatePlot(bR, me); + @()updateTask(me,tS.INCORRECT)}; + exitFcn ]; %update our taskSequence + breakExitFcn = [ { + @()logRun(me,'BREAK_FIX'); + @()updatePlot(bR, me); + @()updateTask(me,tS.BREAKFIX)}; + exitFcn ]; %update our taskSequence +else + incExitFcn = [ { + @()logRun(me,'INCORRECT'); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFcn ]; + breakExitFcn = [ { + @()logRun(me,'BREAK_FIX'); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFcn ]; +end +if tS.useTask || task.nBlocks > 0 + correctExitFcn = [ correctExitFcn; {@()checkTaskEnded(me)} ]; + incExitFcn = [ incExitFcn; {@()checkTaskEnded(me)} ]; + breakExitFcn = [ breakExitFcn; {@()checkTaskEnded(me)} ]; +end + +%======================================================== +%========================================================GENERAL +%======================================================== +%--------------------DEBUGGER override +overrideFcn = { @()keyOverride(me) }; %a special mode which enters a matlab debug state so we can manually edit object values + +%--------------------screenflash +flashFcn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual background activity detection + +%--------------------show 1deg size grid +gridFcn = { @()drawGrid(s) }; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================== +%========================================================================== +%========================================================================== +%--------------------------State Machine Table----------------------------- +% specify our cell array that is read by the stateMachine +stateInfoTmp = { +'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +%--------------------------------------------------------------------------------------------- +'pause' 'prefix' inf pauseEntryFcn {} {} pauseExitFcn; +%--------------------------------------------------------------------------------------------- +'prefix' 'fixate' 10 prefixEntryFcn prefixFcn {} {}; +'fixate' 'stimulus' 0.75 fixEntryFcn fixFcn {} fixExitFcn; +'stimulus' 'correct' 10 stimEntryFcn stimFcn {} stimExitFcn; +'correct' 'prefix' 0.1 correctEntryFcn correctFcn {} correctExitFcn; +'incorrect' 'timeout' 0.1 incEntryFcn incFcn {} incExitFcn; +'breakfix' 'timeout' 0.1 breakEntryFcn incFcn {} breakExitFcn; +'timeout' 'prefix' tS.timeOut {} incFcn {} {}; +%--------------------------------------------------------------------------------------------- +'override' 'pause' 0.5 overrideFcn {} {} {}; +'flash' 'pause' 0.5 flashFcn {} {} {}; +'showgrid' 'pause' 10 {} gridFcn {} {}; +}; +%--------------------------State Machine Table----------------------------- +%========================================================================== + +disp('=================>> Built state info file <<==================') +disp(stateInfoTmp) +disp('=================>> Built state info file <<=================') +clearvars -regexp '.+Fcn$' % clear the cell array Fcns in the current workspace diff --git a/CoreProtocols/fNIRS.mat b/CoreProtocols/fNIRS.mat new file mode 100644 index 0000000000000000000000000000000000000000..d10c3926b8fa2b7aa13219a89b622deb3a00f3be Binary files /dev/null and b/CoreProtocols/fNIRS.mat differ diff --git a/DefaultStateInfo.m b/DefaultStateInfo.m index 2b08f464c893c09eb852ce677af7470e61a80b5e..4b3400a3b70d8c2f032943350f07e53c21959f62 100644 --- a/DefaultStateInfo.m +++ b/DefaultStateInfo.m @@ -1,14 +1,14 @@ %> DEFAULT state configuration file for runExperiment.runTask (full -%> behavioural task design). This state file has a [prefix] state, a -%> [fixate] state for the subject to initiate fixation. If the subject fails -%> initial fixation, an [incorrect] state is called. If the subject fails -%> fixation DURING [stimulus] presentation, a [breakfix] state is called. It -%> assumes there are TWO stimuli in the stims object, the first (stims{1}) -%> is any type of visual stimulus and the second is a fixation cross -%> (stims{2}). For this task most state transitions are deterministic, but -%> for fixate there is a transitionFn that checks if the subject initiates -%> fixation [inFixFn], and for stimulus there is a check if the subject -%> maintains fixation for an additional time [maintainFixFn]. +%> behavioural task design). This state file has a [prefix] state (a blank before +%> fixation starts), then a [fixate] state for the subject to initiate fixation. +%> If the subject fails initial fixation, an [breakfix] state is called. If the +%> subject fails fixation DURING [stimulus] presentation, a [incorrect] state is +%> called. It assumes there are TWO stimuli in the stims object, the first +%> (stims{1}) is any type of visual stimulus and the second is a fixation cross +%> (stims{2}). For this task most state transitions are deterministic, but for +%> [fixate] there is a transitionFn that checks if the subject initiates fixation +%> [inFixFn], and for [stimulus] there is a check if the subject maintains +%> fixation for an additional time [maintainFixFn]. %> %> ┌───────────────────┐ %> │ prefix │ @@ -17,7 +17,7 @@ %> │ │ │ %> │ ▼ │ %> │ ┌───────────┐ inFixFn: ┌───────────────────┐ │ -%> │ │ incorrect │ incorrect │ fixate │ │ +%> │ │ breakfix │ breakfix │ fixate │ │ %> │ │ │ ◀─────────── │ show(stims,2) │ │ %> │ └───────────┘ └───────────────────┘ │ %> │ │ │ inFixFn: │ @@ -28,10 +28,10 @@ %>│ │ ◀─────────────────┼─────────────────────── │ show(stims,[1 2]) │ │ %>└─────────┘ │ └───────────────────┘ │ %> │ │ maintainFixFn: │ -%> │ │ breakfix │ +%> │ │ incorrect │ %> │ ▼ │ %> │ ┌───────────────────┐ │ -%> │ │ breakfix │ │ +%> │ │ incorrect │ │ %> │ └───────────────────┘ │ %> │ │ │ %> │ ▼ │ @@ -40,197 +40,143 @@ %> └──────────────────────▶ │ tS.tOut │ ─┘ %> └───────────────────┘ %> -%> State files control the logic of a behavioural task, switching between -%> states and executing functions on ENTER, WITHIN and on EXIT of states. In -%> addition there are TRANSITION function sets which can test things like -%> eye position to conditionally jump to another state. This state control -%> file will usually be run in the scope of the calling -%> runExperiment.runTask() method and other objects will be available at run -%> time (with easy to use names listed below). The following class objects -%> are already loaded by runTask() and available to use; each object has -%> methods (functions) useful for running the task: +%> This state control file will usually be run in the scope of the calling +%> runExperiment.runTask() method and other objects will be available at run time +%> (with easy to use names listed below). The following class objects are already +%> loaded by runTask() and available to use; each object has methods (functions) +%> useful for running the task: %> %> me = runExperiment object ('self' in OOP terminology) -%> s = screenManager object +%> tS = structure to hold general variables, will be saved as part of the data +%> s = PTB screen manager object (screenManager class) +%> sM = state machine (stateMachine class) parses and runs this file +%> task = task independent variable manager (taskSequence class) +%> stims = all visual stimuli (metaStimulus class) %> aM = audioManager object -%> stims = our list of stimuli (metaStimulus class) -%> sM = State Machine (stateMachine class) -%> task = task sequence (taskSequence class) -%> eT = eyetracker manager -%> io = digital I/O to recording system +%> eT = eyetracker manager (eyelink / tobii / irec / pupilcore classes) +%> tM = touchscreen manager +%> io = digital I/O for recording system %> rM = Reward Manager (LabJack or Arduino TTL trigger to reward system/Magstim) %> bR = behavioural record plot (on-screen GUI during a task run) -%> uF = user functions - add your own functions to this class -%> tS = structure to hold general variables, will be saved as part of the data +%> uF = user functions - add your own functions to this class -%================================================================== -%------------------------General Settings-------------------------- -% These settings are make changing the behaviour of the protocol easier. tS +%========================================================================= +%-----------------------------General Settings---------------------------- +% These settings make changing the behaviour of the protocol easier. tS % is just a struct(), so you can add your own switches or values here and % use them lower down. Some basic switches like saveData, useTask, -% checkKeysDuringstimulus will influence the runeExperiment.runTask() +% enableTrainingKeys will influence the runeExperiment.runTask() % functionality, not just the state machine. Other switches like -% includeErrors are referenced in this state machine file to change with +% includeErrors are referenced in this state machine file to change which % functions are added to the state machine states… +tS.name = 'Default Protocol'; %==name of this protocol +tS.saveData = true; %==save behavioural and eye movement data? +tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… tS.useTask = true; %==use taskSequence (randomises stimulus variables) -tS.rewardTime = 250; %==TTL time in milliseconds -tS.rewardPin = 2; %==Output pin, 2 by default with Arduino. tS.keyExclusionPattern = ["fixate","stimulus"]; %==which states to skip keyboard checking -tS.enableTrainingKeys = true; %==enable keys useful during task training, but not for data recording +tS.enableTrainingKeys = false; %==enable keys useful during task training, but not for data recording tS.recordEyePosition = false; %==record local copy of eye position, **in addition** to the eyetracker? -tS.askForComments = false; %==UI requestor asks for comments before/after run -tS.saveData = false; %==save behavioural and eye movement data? -tS.showBehaviourPlot = true; %==open the behaviourPlot figure? Can cause more memory use… tS.includeErrors = false; %==do we update the trial number even for incorrect saccade/fixate, if true then we call updateTask for both correct and incorrect, otherwise we only call updateTask() for correct responses -tS.name = 'default protocol'; %==name of this protocol tS.nStims = stims.n; %==number of stimuli, taken from metaStimulus object -tS.tOut = 2; %==if wrong response, how long to time out before next trial +tS.timeOut = 2; %==if wrong response, how long to time out before next trial tS.CORRECT = 1; %==the code to send eyetracker for correct trials tS.BREAKFIX = -1; %==the code to send eyetracker for break fix trials tS.INCORRECT = -5; %==the code to send eyetracker for incorrect trials +tS.correctSound = [2000, 0.1, 0.1]; %==freq,length,volume +tS.errorSound = [300, 1, 1]; %==freq,length,volume +% reward system values, set by GUI, but could be overridden here +%rM.reward.time = 250; %==TTL time in milliseconds +%rM.reward.pin = 2; %==Output pin, 2 by default with Arduino. -%================================================================= -%----------------Debug logging to command window------------------ +%================================================================== +%-----------------DEBUG LOGGING to command window------------------ % uncomment each line to get specific verbose logging from each of these % components; you can also set verbose in the opticka GUI to enable all of % these… -%sM.verbose = true; %==print out stateMachine info for debugging -%stims.verbose = true; %==print out metaStimulus info for debugging -%io.verbose = true; %==print out io commands for debugging -%eT.verbose = true; %==print out eyelink commands for debugging -%rM.verbose = true; %==print out reward commands for debugging -%task.verbose = true; %==print out task info for debugging +%sM.verbose = true; %==print out stateMachine info for debugging +%stims.verbose = true; %==print out metaStimulus info for debugging +%io.verbose = true; %==print out io commands for debugging +%eT.verbose = true; %==print out eyelink commands for debugging +%rM.verbose = true; %==print out reward commands for debugging +%task.verbose = true; %==print out task info for debugging %================================================================== %-----------------INITIAL Eyetracker Settings---------------------- % These settings define the initial fixation window and set up for the % eyetracker. They may be modified during the task (i.e. moving the % fixation window towards a target, enabling an exclusion window to stop -% the subject entering a specific set of display areas etc.) +% the subject entering a specific set of display areas etc.). +% +% The GUI sets some default values, but it is good practive to redefine +% them explicitly here. % -% IMPORTANT: you need to make sure that the global state time is larger -% than the fixation timers specified here. Each state has a global timer, -% so if the state timer is 5 seconds but your fixation timer is 6 seconds, -% then the state will finish before the fixation time was completed! - -% initial fixation X position in degrees (0° is screen centre) +% **IMPORTANT**: you must ensure that the global state time is LARGER than +% any fixation timers specified here. Each state has a global timer, so if +% the state timer is 5 seconds but your fixation timer is 6 seconds, then +% the state will finish before the fixation time was completed! +%------------------------------------------------------------------ +% initial fixation X position in degrees (0° is screen centre). Multiple windows +% can be entered using an array of X values tS.fixX = 0; -% initial fixation Y position in degrees +% initial fixation Y position in degrees (0° is screen centre). Multiple windows +% can be entered using an array. tS.fixY = 0; -% time to search and enter fixation window +% time to search and enter fixation window (Initiate fixation) tS.firstFixInit = 3; % time to maintain initial fixation within window, can be single value or a % range to randomise between -tS.firstFixTime = [0.5 0.9]; -% circular fixation window radius in degrees +tS.firstFixTime = [0.25 0.75]; +% circular fixation window radius in degrees; if you enter [x y] the window will be +% rectangular. tS.firstFixRadius = 2; -% do we forbid eye to enter-exit-reenter fixation window? +% do we forbid eye to enter-exit-reenter fixation window? Set this to false +% during initial training, or if you want relaxed checking (non-forced +% choice gaze tasks). tS.strict = true; -% do we add an exclusion zone where subject cannot saccade to... +% add an exclusion zone where subject cannot saccade to? tS.exclusionZone = []; -% time to fix on the stimulus -tS.stimulusFixTime = 1.5; -% log of recent X and Y position, and exclusion zone, here set ti initial -% values -me.lastXPosition = tS.fixX; -me.lastYPosition = tS.fixY; -me.lastXExclusion = []; -me.lastYExclusion = []; - -%================================================================== -%---------------------------Eyetracker setup----------------------- -% NOTE: the opticka GUI can set eyetracker options too, if you set options -% here they will OVERRIDE the GUI ones; if they are commented then the GUI -% options are used. me.elsettings and me.tobiisettings contain the GUI -% settings you can test if they are empty or not and set them based on -% that... -eT.name = tS.name; -if me.eyetracker.dummy == true; eT.isDummy = true; end %===use dummy or real eyetracker? -if tS.saveData; eT.recordData = true; end %===save ET data? -if strcmp(me.eyetracker.device, 'eyelink') - if isempty(me.eyetracker.esettings) %==check if GUI settings are empty - eT.sampleRate = 250; %==sampling rate - eT.calibrationStyle = 'HV5'; %==calibration style - eT.calibrationProportion = [0.4 0.4]; %==the proportion of the screen occupied by the calibration stimuli - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation use 1-9 to show each dot, space to select fix as valid, - % INS key ON EYELINK KEYBOARD to accept calibration! - eT.remoteCalibration = false; - %----------------------- - eT.modify.calibrationtargetcolour = [1 1 1]; %==calibration target colour - eT.modify.calibrationtargetsize = 2; %==size of calibration target as percentage of screen - eT.modify.calibrationtargetwidth = 0.15; %==width of calibration target's border as percentage of screen - eT.modify.waitformodereadytime = 500; - eT.modify.devicenumber = -1; %==-1 = use any attachedkeyboard - eT.modify.targetbeep = 1; %==beep during calibration - end -elseif strcmp(me.eyetracker.device, 'tobii') - if isempty(me.eyetracker.tsettings) %==check if GUI settings are empty - eT.model = 'Tobii Pro Spectrum'; - eT.sampleRate = 300; - eT.trackingMode = 'human'; - eT.calibrationStimulus = 'animated'; - eT.autoPace = true; - %----------------------- - % remote calibration enables manual control and selection of each - % fixation this is useful for a baby or monkey who has not been trained - % for fixation - eT.manualCalibration = false; - %----------------------- - eT.calPositions = [ .2 .5; .5 .5; .8 .5]; - eT.valPositions = [ .5 .5 ]; - end -end -%Initialise the eyeTracker object with X, Y, FixInitTime, FixTime, Radius, StrictFix +% time to maintain fixation during the stimulus state +tS.stimulusFixTime = 1; +% Initialise eyetracker with X, Y, FixInitTime, FixTime, Radius, StrictFix values updateFixationValues(eT, tS.fixX, tS.fixY, tS.firstFixInit, tS.firstFixTime, tS.firstFixRadius, tS.strict); -%Ensure we don't start with any exclusion zones set up -resetAll(eT); %================================================================== -%----WHICH states assigned as correct or break for online plot?---- -%----You need to use regex patterns for the match (doc regexp)----- -bR.correctStateName = "correct"; -bR.breakStateName = ["breakfix","incorrect"]; - -%================================================================== -%--------------randomise stimulus variables every trial?----------- -% if you want to have some randomisation of stimuls variables without using -% taskSequence task (i.e. general training tasks), you can uncomment this -% and runExperiment can use this structure to change e.g. X or Y position, -% size, angle see metaStimulus for more details. Remember this will not be -% "Saved" for later use, if you want to do controlled methods of constants -% experiments use taskSequence to define proper randomised and balanced -% variable sets and triggers to send to recording equipment etc... -% -% stims.choice = []; -% n = 1; -% in(n).name = 'xyPosition'; -% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; -% in(n).stimuli = 1; -% in(n).offset = []; -% stims.stimulusTable = in; -stims.choice = []; -stims.stimulusTable = []; - -%======================================================================= -%-------------allows using arrow keys to control variables?------------- +%-----------------BEAVIOURAL PLOT CONFIGURATION-------------------- +%--WHICH states assigned correct / incorrect for the online plot?-- +bR.correctStateName = "correct"; +bR.breakStateName = ["breakfix","incorrect"]; + +%========================================================================= +%------------------Randomise stimulus variables every trial?-------------- +% If you want to have some randomisation of stimuls variables WITHOUT using +% taskSequence task. Remember this will not be "Saved" for later use, if you +% want to do controlled experiments use taskSequence to define proper randomised +% and balanced variable sets and triggers to send to recording equipment etc... +% Good for training tasks, or stimulus variability irrelevant to the task. +% n = 1; +% in(n).name = 'xyPosition'; +% in(n).values = [6 6; 6 -6; -6 6; -6 -6; -6 0; 6 0]; +% in(n).stimuli = 1; +% in(n).offset = []; +% stims.stimulusTable = in; +stims.choice = []; +stims.stimulusTable = []; + +%========================================================================= +%--------------allows using arrow keys to control variables?-------------- % another option is to enable manual control of a table of variables -% this is useful to probe RF properties or other features while still -% allowing for fixation or other behavioural control. +% in-task. This is useful to dynamically probe RF properties or other +% features while still allowing for fixation or other behavioural control. % Use arrow keys <- -> to control value and ↑ ↓ to control variable. stims.controlTable = []; stims.tableChoice = 1; %====================================================================== -%this allows us to enable subsets from our stimulus list -% 1 = grating | 2 = fixation cross -stims.stimulusSets = {[1,2],[1]}; -stims.setChoice = 1; -hide(stims); +% this allows us to enable subsets from our stimulus list +stims.stimulusSets = {[1,2],[1]}; +stims.setChoice = 1; -%====================================================================== +%========================================================================= % N x 2 cell array of regexpi strings, list to skip the current -> next % state's exit functions; for example skipExitStates = % {'fixate','incorrect|breakfix'}; means that if the currentstate is @@ -239,9 +185,15 @@ hide(stims); % exit states. sM.skipExitStates = {'fixate','incorrect|breakfix'}; -%=================================================================== -%=================================================================== -%=================================================================== +%========================================================================= +% which stimulus in the list is defined as a saccade target? +stims.fixationChoice = 1; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%========================================================================= %------------------State Machine Task Functions--------------------- % Each cell {array} holds a set of anonymous function handles which are % executed by the state machine to control the experiment. The state @@ -252,28 +204,25 @@ sM.skipExitStates = {'fixate','incorrect|breakfix'}; % also add global variables/objects then use these. The values entered here % are set on load, if you want up-to-date values then you need to use % methods/function wrappers to retrieve/set them. -%=================================================================== -%=================================================================== -%=================================================================== +%========================================================================= -%======================================================== +%============================================================== %========================================================PAUSE -%======================================================== +%============================================================== %--------------------pause entry pauseEntryFn = { - @()hide(stims); - @()drawBackground(s); %blank the subject display - @()drawPhotoDiode(s,[0 0 0]); %draw black photodiode + @()hide(stims); % hide all stimuli + @()drawBackground(s); % blank the subject display + @()drawPhotoDiodeSquare(s,[0 0 0]); % draw black photodiode @()drawTextNow(s,'PAUSED, press [p] to resume...'); @()disp('PAUSED, press [p] to resume...'); - @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', stims.stimulusPositions); + @()trackerDrawStatus(eT,'PAUSED, press [p] to resume', [], 0, false); @()trackerMessage(eT,'TRIAL_RESULT -100'); %store message in EDF - @()resetAll(eT); % reset all fixation markers to initial state @()setOffline(eT); % set eyelink offline [tobii ignores this] @()stopRecording(eT, true); %stop recording eye position data, true=both eyelink & tobii - @()needFlip(me, false); % no need to flip the PTB screen - @()needEyeSample(me,false); % no need to check eye position + @()needFlip(me, false, 0); % no need to flip the PTB screen or tracker + @()needEyeSample(me, false); % no need to check eye position }; %--------------------pause exit @@ -285,48 +234,50 @@ pauseExitFn = { @()startRecording(eT, true); }; -%======================================================== -%========================================================PREFIXATE -%======================================================== +%============================================================== +%====================================================PRE-FIXATION +%============================================================== %--------------------prefixate entry prefixEntryFn = { - @()needFlip(me, true); + @()needFlip(me, true, 4); % enable the screen and trackerscreen flip @()needEyeSample(me, true); % make sure we start measuring eye position + @()getStimulusPositions(stims); % make a struct eT can use for drawing stim positions @()hide(stims); % hide all stimuli + @()resetAll(eT); % reset all fixation markers to initial state % update the fixation window to initial values @()updateFixationValues(eT,tS.fixX,tS.fixY,[],tS.firstFixTime); %reset fixation window - @()startRecording(eT); % start eyelink recording for this trial (tobii ignores this) - % tracker messages that define a trial start - @()trackerMessage(eT,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands - @()trackerMessage(eT,sprintf('TRIALID %i',getTaskIndex(me))); %Eyelink start trial marker + % send the trial start messages to the eyetracker + @()trackerTrialStart(eT, getTaskIndex(me)); @()trackerMessage(eT,['UUID ' UUID(sM)]); %add in the uuid of the current state for good measure % you can add any other messages, such as stimulus values as needed, - % e.g. @()trackerMessage(eT,['MSG:ANGLE' num2str(stims{1}.angleOut)]) - % draw to the eyetracker display + % e.g. @()trackerMessage(eT,['MSG:ANGLE' num2str(stims{1}.angleOut)]) etc. }; %--------------------prefixate within prefixFn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; +%--------------------prefixate exit prefixExitFn = { - @()trackerDrawStatus(eT,'Init Fix...', stims.stimulusPositions); + @()logRun(me,'INITFIX'); + @()trackerMessage(eT,'MSG:Start Fix'); + @()trackerDrawStatus(eT,'Start trial...', stims.stimulusPositions, 0, false); }; -%======================================================== -%========================================================FIXATE -%======================================================== +%============================================================== +%====================================================FIXATION +%============================================================== %--------------------fixate entry fixEntryFn = { - @()show(stims{tS.nStims}); - @()logRun(me,'INITFIX'); + @()show(stims{tS.nStims}); % show last stim which is usually fixation cross }; %--------------------fix within fixFn = { @()draw(stims); %draw stimuli - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); + @()animate(stims); % animate stimuli for subsequent draw }; %--------------------test we are fixated for a certain length of time @@ -338,12 +289,11 @@ inFixFn = { % otherwise 'breakfix' is returned and the state machine will jump to the % breakfix state. If neither condition matches, then the state table below % defines that after 5 seconds we will switch to the incorrect state. - @()testSearchHoldFixation(eT,'stimulus','incorrect') + @()testSearchHoldFixation(eT,'stimulus','breakfix') }; %--------------------exit fixation phase fixExitFn = { - @()statusMessage(eT,'Show Stimulus...'); % reset fixation timers to maintain fixation for tS.stimulusFixTime seconds @()updateFixationValues(eT,[],[],[],tS.stimulusFixTime); @()show(stims); % show all stims @@ -355,16 +305,16 @@ fixExitFn = { %======================================================== stimEntryFn = { - % send an eyeTracker sync message (reset relative time to 0 after first flip of this state) + % send an eyeTracker sync message (reset relative time to 0 after next flip) @()doSyncTime(me); - % send stimulus value strobe (value set by updateVariables(me) function) + % send stimulus value strobe (value alreadyset by updateVariables(me) function) @()doStrobe(me,true); }; %--------------------what to run when we are showing stimuli stimFn = { @()draw(stims); - @()drawPhotoDiode(s,[1 1 1]); + @()drawPhotoDiodeSquare(s,[1 1 1]); @()animate(stims); % animate stimuli for subsequent draw }; @@ -377,12 +327,13 @@ maintainFixFn = { % otherwise 'breakfix' is returned and the state machine will jump to the % breakfix state. If neither condition matches, then the state table below % defines that after 5 seconds we will switch to the incorrect state. - @()testSearchHoldFixation(eT,'correct','breakfix'); + @()testHoldFixation(eT,'correct','incorrect'); }; %as we exit stim presentation state stimExitFn = { - @()sendStrobe(io,255); + @()setStrobeValue(me, 255); % 255 indicates stimulus OFF + @()doStrobe(me, true); }; %======================================================== @@ -392,107 +343,102 @@ stimExitFn = { %========================================================CORRECT %--------------------if the subject is correct (small reward) correctEntryFn = { - @()timedTTL(rM, tS.rewardPin, tS.rewardTime); % send a reward TTL - @()beep(aM, 2000, 0.1, 0.1); % correct beep - @()trackerMessage(eT,'END_RT'); %send END_RT message to tracker - @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.CORRECT)); %send TRIAL_RESULT message to tracker - @()trackerClearScreen(eT); - @()trackerDrawText(eT,'Correct! :-)'); - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] + @()trackerTrialEnd(eT, tS.CORRECT); % send the end trial messages and other cleanup @()needEyeSample(me,false); % no need to collect eye data until we start the next trial @()hide(stims); % hide all stims - @()logRun(me,'CORRECT'); % print current trial info }; %--------------------correct stimulus correctFn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; %--------------------when we exit the correct state correctExitFn = { - @()updatePlot(bR, me); %update our behavioural record, MUST be done before we update variables - @()updateTask(me,tS.CORRECT); %make sure our taskSequence is moved to the next trial - @()updateVariables(me); %randomise our stimuli, and set strobe value too - @()update(stims); %update our stimuli ready for display - @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions - @()trackerClearScreen(eT); - @()resetFixation(eT); %resets the fixation state timers - @()resetFixationHistory(eT); % reset the recent eye position history - @()resetExclusionZones(eT); % reset the exclusion zones on eyetracker - @()checkTaskEnded(me); %check if task is finished + @()giveReward(rM); % send a reward + @()beep(aM, tS.correctSound); % correct beep + @()logRun(me,'CORRECT'); % print current trial info + @()trackerDrawStatus(eT, 'CORRECT! :-)', stims.stimulusPositions, 0, false); + @()updatePlot(bR, me); % must run before updateTask + @()updateTask(me, tS.CORRECT); % make sure our taskSequence is moved to the next trial + @()updateVariables(me); % randomise our stimuli, and set strobe value too + @()update(stims); % update our stimuli ready for display @()plot(bR, 1); % actually do our behaviour record drawing }; -%========================================================INCORRECT +%========================================================INCORRECT/BREAKFIX %--------------------incorrect entry -incEntryFn = { - @()beep(aM,400,0.5,1); - @()trackerMessage(eT,'END_RT'); - @()trackerMessage(eT,sprintf('TRIAL_RESULT %i',tS.INCORRECT)); - @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0); - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] +incEntryFn = { + @()trackerTrialEnd(eT, tS.INCORRECT); % send the end trial messages and other cleanup + @()needEyeSample(me,false); + @()hide(stims); +}; +%--------------------break entry +breakEntryFn = { + @()trackerTrialEnd(eT, tS.BREAKFIX); % send the end trial messages and other cleanup @()needEyeSample(me,false); @()hide(stims); - @()logRun(me,'INCORRECT'); %fprintf current trial info }; %--------------------our incorrect/breakfix stimulus incFn = { - @()drawPhotoDiode(s,[0 0 0]); + @()drawPhotoDiodeSquare(s,[0 0 0]); }; -%--------------------incorrect exit -incExitFn = { - @()updatePlot(bR, me); %update our behavioural plot, must come before updateTask() / updateVariables() - @()updateVariables(me); %randomise our stimuli, set strobe value too - @()update(stims); %update our stimuli ready for display - @()getStimulusPositions(stims); %make a struct the eT can use for drawing stim positions - @()trackerClearScreen(eT); - @()resetFixation(eT); %resets the fixation state timers - @()resetFixationHistory(eT); %reset the stored X and Y values +%--------------------generic exit +exitFn = { + % tS.includeErrors will prepend some code here... + @()beep(aM, tS.errorSound); + @()needFlipTracker(me, 0); %for operator screen stop flip + @()updateVariables(me); % randomise our stimuli, set strobe value too + @()update(stims); % update our stimuli ready for display + @()resetAll(eT); % resets the fixation state timers @()plot(bR, 1); % actually do our drawing }; -%--------------------break entry -breakEntryFn = { - @()beep(aM,400,0.5,1); - @()edfMessage(eT,'END_RT'); - @()edfMessage(eT,['TRIAL_RESULT ' num2str(tS.BREAKFIX)]); - @()trackerDrawStatus(eT,'BREAKFIX! :-(', stims.stimulusPositions, 0); - @()stopRecording(eT); - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()needEyeSample(me,false); - @()sendStrobe(io,252); - @()hide(stims); - @()logRun(me,'BREAKFIX'); %fprintf current trial info -}; - -%--------------------break exit -breakExitFn = incExitFn; % we copy the incorrect exit functions - %--------------------change functions based on tS settings -% this shows an example of how to use tS options to change the function -% lists run by the state machine. We can prepend or append new functions to -% the cell arrays. +% we use tS options to change the function lists run by the state machine. +% We can prepend or append new functions to the cell arrays. +% +% logRun = add current info to behaviural record +% updatePlot = updates the behavioural record % updateTask = updates task object -% resetRun = randomise current trial within the block +% resetRun = randomise current trial within the block (makes it harder for +% subject to guess based on previous failed trial. % checkTaskEnded = see if taskSequence has finished if tS.includeErrors % we want to update our task even if there were errors - incExitFn = [ {@()updateTask(me,tS.INCORRECT)}; incExitFn ]; %update our taskSequence - breakExitFn = [ {@()updateTask(me,tS.BREAKFIX)}; breakExitFn ]; %update our taskSequence + incExitFn = [ { + @()logRun(me,'INCORRECT'); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0, false); + @()updatePlot(bR, me); + @()updateTask(me,tS.INCORRECT)}; + exitFn ]; %update our taskSequence + breakExitFn = [ { + @()logRun(me,'BREAK_FIX'); + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', stims.stimulusPositions, 0, false); + @()updatePlot(bR, me); + @()updateTask(me,tS.BREAKFIX)}; + exitFn ]; %update our taskSequence +else + incExitFn = [ { + @()logRun(me,'INCORRECT'); + @()trackerDrawStatus(eT,'INCORRECT! :-(', stims.stimulusPositions, 0, false); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFn ]; + breakExitFn = [ { + @()logRun(me,'BREAK_FIX'); + @()trackerDrawStatus(eT,'BREAK_FIX! :-(', stims.stimulusPositions, 0, false); + @()updatePlot(bR, me); + @()resetRun(task)}; + exitFn ]; end -if tS.useTask %we are using task +if tS.useTask || task.nBlocks > 0 correctExitFn = [ correctExitFn; {@()checkTaskEnded(me)} ]; incExitFn = [ incExitFn; {@()checkTaskEnded(me)} ]; breakExitFn = [ breakExitFn; {@()checkTaskEnded(me)} ]; - if ~tS.includeErrors % using task but don't include errors - incExitFn = [ {@()resetRun(task)}; incExitFn ]; %we randomise the run within this block to make it harder to guess next trial - breakExitFn = [ {@()resetRun(task)}; breakExitFn ]; %we randomise the run within this block to make it harder to guess next trial - end end + %======================================================== %========================================================EYETRACKER %======================================================== @@ -501,19 +447,17 @@ calibrateFn = { @()drawBackground(s); %blank the display @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()rstop(io); @()trackerSetup(eT); %enter tracker calibrate/validate setup mode }; %--------------------drift correction function -driftFn = { +driftFn = { @()drawBackground(s); %blank the display - @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] - @()setOffline(eT); % set eyelink offline [tobii ignores this] - @()rstop(io); - @()driftCorrection(eT) % enter drift correct (only eyelink) (only eyelink) + @()stopRecording(eT); % stop recording in eyelink [others ignores this] + @()setOffline(eT); % set eyelink offline [others ignores this] + @()driftCorrection(eT) % enter drift correct (only eyelink) }; -offsetFcn = { +offsetFn = { @()drawBackground(s); %blank the display @()stopRecording(eT); % stop recording in eyelink [tobii ignores this] @()setOffline(eT); % set eyelink offline [tobii ignores this] @@ -532,27 +476,31 @@ flashFn = { @()flashScreen(s, 0.2) }; % fullscreen flash mode for visual backgro %--------------------show 1deg size grid gridFn = { @()drawGrid(s) }; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%------------------------------------------------------------------------% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + %========================================================================== %========================================================================== %========================================================================== %--------------------------State Machine Table----------------------------- % specify our cell array that is read by the stateMachine stateInfoTmp = { -'name' 'next' 'time' 'entryFcn' 'withinFcn' 'transitionFcn' 'exitFcn'; +'name' 'next' 'time' 'entryFn' 'withinFn' 'transitionFn' 'exitFn'; %--------------------------------------------------------------------------------------------- 'pause' 'prefix' inf pauseEntryFn {} {} pauseExitFn; %--------------------------------------------------------------------------------------------- -'prefix' 'fixate' 0.5 prefixEntryFn {} {} {}; -'fixate' 'incorrect' 10 fixEntryFn fixFn inFixFn fixExitFn; +'prefix' 'fixate' 0.75 prefixEntryFn prefixFn {} {}; +'fixate' 'breakfix' 10 fixEntryFn fixFn inFixFn fixExitFn; 'stimulus' 'incorrect' 10 stimEntryFn stimFn maintainFixFn stimExitFn; -'incorrect' 'timeout' 0.5 incEntryFn incFn {} incExitFn; -'breakfix' 'timeout' 0.5 breakEntryFn incFn {} breakExitFn; -'correct' 'prefix' 0.5 correctEntryFn correctFn {} correctExitFn; -'timeout' 'prefix' tS.tOut {} incFn {} {}; +'correct' 'prefix' 0.1 correctEntryFn correctFn {} correctExitFn; +'incorrect' 'timeout' 0.1 incEntryFn incFn {} incExitFn; +'breakfix' 'timeout' 0.1 breakEntryFn incFn {} breakExitFn; +'timeout' 'prefix' tS.timeOut {} incFn {} {}; %--------------------------------------------------------------------------------------------- 'calibrate' 'pause' 0.5 calibrateFn {} {} {}; 'drift' 'pause' 0.5 driftFn {} {} {}; -'offset' 'pause' 0.5 offsetFcn {} {} {}; +'offset' 'pause' 0.5 offsetFn {} {} {}; %--------------------------------------------------------------------------------------------- 'override' 'pause' 0.5 overrideFn {} {} {}; 'flash' 'pause' 0.5 flashFn {} {} {}; diff --git a/README.md b/README.md index 9d794259740d3c0c2440d15a8e08eab00cd753c8..a52f184c601693ba804e66433482441f771c8fb7 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,17 @@ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.592253.svg)](https://doi.org/10.5281/zenodo.592253) [![Open in Visual Studio Code](https://img.shields.io/badge/--007ACC?logo=visual%20studio%20code&logoColor=ffffff)](https://vscode.dev/github/iandol/opticka) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://gitHub.com/iandol/opticka/graphs/commit-activity) [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](http://perso.crans.org/besson/LICENSE.html) -Opticka is an object-oriented framework with optional GUI for the [Psychophysics toolbox (PTB)](http://psychtoolbox.org/), allowing full experimental presentation of complex visual or other stimuli. It is designed to work on Linux, macOS or Windows. It interfaces via strobed words and/or ethernet for recording neurophysiological and behavioural data. Full behavioural task control is available by use of a [Finite State-Machine](http://iandol.github.io/OptickaDocs/classstate_machine.html#details) controller, in addition to simple method of constants (MOC) experiments. Opticka uses the TCP interface for Eyelink & Tobii Pro eyetrackers affording better control, reliability and data recording over depending on analog voltage output (we don't depend on a DAQ card for eye data). The various base classes can be used _without_ the need to run the GUI (see [`optickatest.m`](http://iandol.github.io/OptickaDocs/optickatest.html) for an example), and plug-n-play stimuli provide a unified interface (`setup`, `animate`, `draw`, `update`, `reset`) to integrate into other PTB routines. Opticka's methods take care of all the background geometry and normalisation, meaning stimuli are much easier to use than “raw” PTB commands alone. Analysis routines are also present for taking e.g. Plexon files (`.PL2` or `.PLX`), Eyelink files (`.EDF`), and behavioural responses and parsing them into a consistent structure, interfacing directly with [Fieldtrip](http://fieldtrip.fcdonders.nl/start) for further spike, LFP, and spike-LFP analysis. Opticka is more modular and affords much better graphics control (most stimuli are optimised OpenGL with advanced control via PTB) than [MonkeyLogic](http://www.brown.edu/Research/monkeylogic/). +Opticka is an object-oriented framework with optional GUI for the [Psychophysics toolbox (PTB)](http://psychtoolbox.org/), allowing full experimental presentation of complex visual or other stimuli. It is designed to work on Linux, macOS or Windows. It interfaces via strobed words and/or ethernet for recording neurophysiological and behavioural data. Full behavioural task control is available by use of a [Finite State-Machine](http://iandol.github.io/OptickaDocs/classstate_machine.html#details) controller, in addition to simple method of constants (MOC) experiments. Opticka uses the TCP interface for Eyelink, Tobii Pro and iRecHS2 eyetrackers affording better control, reliability and data recording over depending on analog voltage output (we don't need a DAQ card for eye data). The base classes can be used _without_ the need to run the GUI (see [`optickatest.m`](http://iandol.github.io/OptickaDocs/optickatest.html) for an example), and plug-n-play stimuli provide a unified interface (`setup`, `animate`, `draw`, `update`, `reset`) to integrate into other PTB routines. Opticka's methods take care of all the background geometry and normalisation, meaning stimuli are much easier to use than “raw” PTB commands alone. Analysis routines are also present, eye data can be parsed into trials for any supported eyetracker, and for taking e.g. Plexon files (`.PL2` or `.PLX`), Eyelink files (`.EDF`), and behavioural responses and parsing them into a consistent structure, interfacing directly with [Fieldtrip](http://fieldtrip.fcdonders.nl/start) for further spike, LFP, and spike-LFP analysis. Opticka is more modular and affords much better stimulus control (most stimuli are optimised OpenGL with advanced control thanks to PTB) than e.g. [MonkeyLogic](http://www.brown.edu/Research/monkeylogic/). ## Sample hardware setup -The diagram below shows a sample Opticka configuration setup. Note that the eyetracker, display, synchronisation and electrophysiology systems **can be swapped** for other hardware ([see the list below](#Hardware-currently-supported)). While I prefer using a Display++ or a DataPixx/ViewPixx to ensure temporal fidelity, you can use a LabJack or Arduino for synchronisation (where a photodiode becomes more important): +The diagram below shows a sample Opticka configuration setup. **Note that the eyetracker, display, synchronisation and electrophysiology systems *can be swapped* for other hardware ([see the list below](#Hardware-currently-supported)).** While I prefer using a Display++ or a DataPixx/ViewPixx to guarantee temporal fidelity, you can use either a LabJack or Arduino/XIAO for synchronisation (where a photodiode becomes more important, which we integrate as an option): ![Example hardware setup to run Opticka](https://github.com/iandol/opticka/raw/gh-pages/images/Opticka-Setup.png) ## GUI -A GUI can be used to control the hardware, stimuli and variables needed for both method of constant (MOC) and more complex behavioural tasks that use the [state machine](#state-machine-control).It is useful for cases where staff running an experiment are not themselves programmers, and when you need to change experiment or stimulus parameters quickly between runs. The GUI is **not required** to utilise the underlying classes… +A GUI can be used to control the hardware, stimuli and variables needed for both method of constant (MOC) and more complex behavioural tasks that use the [state machine](#state-machine-control). GUI supports protocol files that are useful for cases where staff running an experiment are not themselves programmers, and when you need to change experiment or stimulus parameters quickly between runs: you make protocol files for each task, then load and run them as needed. The GUI is **not required** to utilise the underlying classes… ```matlab o = opticka; %==run the GUI, returns an object 'o' for introspection from the command window... @@ -35,11 +35,13 @@ runDemo(sM); * **Display & Digital I/O**: high quality display (high bit depths) and easy-to-use microsecond precise digital I/O: [DataPixx / ViewPixx / ProPixx](http://vpixx.com/products/tools-for-vision-sciences/). * **Display**: any normal monitor; remember that PTB can support 10bits and higher output, steroscopic display, HDR output etc. * **Digital I/O**: [LabJack](https://labjack.com/) USB U3/U6 or T4/T7 DAQs, strobed words up to 12bits. The T4/T7 are preferred as the I/O is asynchronous and work on all platforms. -* **Digital I/O**: [Arduino]() boards for simple TTL triggers for reward systems, MagStim etc. In particular, digitial TTLs are asynchronous so they do not block the experimental loop. The [seeeduino Xiao](https://wiki.seeedstudio.com/Seeeduino-XIAO/) is small, cheap, fast and works really well, but the [Uno](https://docs.arduino.cc/hardware/uno-rev3) is also well supported. -* **Eyetracking**: [Eyelink Eyetrackers](https://www.sr-research.com) -- uses the native ethernet link API. This enables much better two-way control, sending markers and stimulus data while drawing stimuli and experiment values onto the eyelink screen. EDF files are stored after each run and [`eyelinkAnalysis.m`](http://iandol.github.io/OptickaDocs/classeyelink_analysis.html) class uses native EDF loading (not ascii proxies) for full trial-by-trial analysis without conversion. -* **Eyetracking**: [Tobii Pro Eyetrackers](https://www.tobiipro.com) -- using the excellent [Titta toolbox](https://github.com/dcnieho/Titta) to manage calibration, command-response and recording. Tobii Pro eyetrackers do not require head fixation. +* **Digital I/O**: [Arduino]() boards for simple TTL triggers for reward systems, MagStim etc. In particular, digitial TTLs are asynchronous so they do not block the experimental loop. The [seeeduino Xiao](https://wiki.seeedstudio.com/Seeeduino-XIAO/) is small, cheap, fast and works well, as does the [Raspberry Pi Pico](https://www.raspberrypi.com/products/raspberry-pi-pico/) (using the [Arduino IDE interface](https://github.com/earlephilhower/arduino-pico)), but the [Uno](https://docs.arduino.cc/hardware/uno-rev3) is also well supported. +* **TouchScreen**: PTB can interface many kinds of touchscreen types and we have tested against many different brands. Our [`touchManager`]([`touchManager.m`](http://iandol.github.io/OptickaDocs/classtouch_manager.html) add support for touch windows, exclusion zones and more... +* **Eyetracking**: [Eyelink Eyetrackers](https://www.sr-research.com) -- uses the native ethernet link API. This enables much better two-way control, sending markers and stimulus data while drawing stimuli and experiment values onto the eyelink screen. EDF files are stored after each run and [`eyelinkAnalysis.m`](http://iandol.github.io/OptickaDocs/classeyelink_analysis.html) class uses native EDF loading (not ascii proxies) for full trial-by-trial analysis without conversion. See also [eyelinkManager](). +* **Eyetracking**: [Tobii Pro Eyetrackers](https://www.tobiipro.com) -- using the excellent [Titta toolbox](https://github.com/dcnieho/Titta) to manage calibration, command-response and recording. Tobii Pro eyetrackers do not require head fixation. See [tobiiManager](). +* **Eyetracking**: [iRecHS2](https://staff.aist.go.jp/k.matsuda/iRecHS2/index_e.html) -- this low-cost eyetracker nevertheless offers good quality eyetracking, see their paper: [A Widely Applicable Real-Time Mono/Binocular Eye Tracking System Using a High Frame-Rate Digital Camera (2017)](https://doi.org/10.1007/978-3-319-58071-5_45). We use a 500Hz Chameleon camera from FLIR. See See [iRecManager](). * **Electrophysiology**: in theory any recording system that accepts digital triggers / strobed words; we have dedicated code for the Plexon Omniplex system and can control Intan amplifier software. Opticka can use TCP communication over ethernet to transmit current variable data to allow online data visualisation (PSTHs etc. for each experiment variable) on the Omniplex machine. Digital triggers can be generated with good temporal fidelity. -* **Visual Calibration**: we support use of a CRS SpectrolCal II (preferred but expensive) or ColorCal 2, or a VPixx i1Pro, or manual interfacing with most other photometers that PTB supports. The [calibrateLuminance]() class. +* **Visual Calibration**: we support use of a CRS SpectrolCal II (preferred but expensive) or ColorCal 2, or a VPixx i1Pro, or manual interfacing with most other photometers that PTB supports. See the [calibrateLuminance]() class. * **Photodiode boxes**: we use TSL251R light-to-voltage photodiodes, which can be recorded directy into your electrophysiology system or can generate digital triggers via an [Arduino interface](https://github.com/iandol/opticka/tree/master/tools/photodiode). ## Quick Documentation diff --git a/addOptickaToPath.m b/addOptickaToPath.m index d6c8952f401a0695e9295fe1ec3b1402d5988ff2..c08b628de0adf529c4fccca4a192af3b3cc295a7 100644 --- a/addOptickaToPath.m +++ b/addOptickaToPath.m @@ -1,7 +1,7 @@ function addOptickaToPath() % adds opticka to path, ignoring at least some of the unneeded folders -t = tic; +tt = tic; mpath = path; mpath = strsplit(mpath, pathsep); opath = fileparts(mfilename('fullpath')); @@ -17,9 +17,31 @@ end opaths = genpath(opath); opaths = strsplit(opaths,pathsep); sep = regexptranslate('escape',filesep); -pathExceptions = [sep '\.git|' sep 'adio|' sep 'arduino|' sep 'photodiode|' ... - sep '+uix|' sep '+uiextras|' sep 'legacy|' sep 'html|' sep 'doc|' sep '.vscode']; -qAdd = cellfun(@isempty,regexpi(opaths,pathExceptions)); % true where regexp _didn't_ match -addpath(opaths{qAdd}); savepath; +pathExceptions = [".git" "adio" "arduino" "photodiode" "+uix" ... + "+zmq" "+uiextras" "legacy" "html" "doc" ".vscode"]; +qAdd = contains(opaths,pathExceptions); % true where regexp _didn't_ match +addpath(opaths{~qAdd}); -fprintf('--->>> Added opticka to the MATLAB path in %.1f ms...\n',toc(t)*1000); \ No newline at end of file +% Define the parent folder and the string array of optional project folder names +parentFolder = fileparts(opath); % Adjust this to your actual parent folder +folderNames = ["Palamedes" "CageLab/software" "matlab-jzmq", "matmoteGO", "PTBSimia"]; % Example folder names + +% Loop through each folder name +for i = 1:length(folderNames) + % Construct the full path for each folder + folderPath = fullfile(parentFolder, folderNames(i)); + + % Check if the folder exists + if isfolder(folderPath) + % Add the folder to the MATLAB path + addpath(folderPath); + fprintf('Additionally added to path: %s\n', folderPath); + else + fprintf('Folder does not exist: %s\n', folderPath); + end +end + +% Save the changes to the path for future sessions +savepath; + +fprintf('--->>> Added opticka to the MATLAB path in %.1f ms...\n',toc(tt)*1000); \ No newline at end of file diff --git a/analysis/analysisCore.m b/analysis/analysisCore.m index eaf8c939504834f8b8a648997f5fffb055b89919..22f992258dc499be08a0291125f809e0daa8be1b 100644 --- a/analysis/analysisCore.m +++ b/analysis/analysisCore.m @@ -11,7 +11,7 @@ classdef analysisCore < optickaCore properties %> generate plots? doPlots logical = true - %> +- time window (s) for baseline estimation/removal + %> +- time window (s) for baseline estimation/removal [-0.2 0] baselineWindow double = [-0.2 0] %> default range (s) to measure values from measureRange double = [0.1 0.2] @@ -323,10 +323,13 @@ classdef analysisCore < optickaCore if isnumeric(in) && length(in)==2 && in(1)1; data=reshape(data,size(data,2),1); end + + istable = false; + if isa(data,'timetable') + dim = 2; + istable = true; + end + if size(data,1)==1 && size(data,2)>1 && ~istable; data=reshape(data,size(data,2),1); end if size(data,1) > 1 && size(data,2) > 1 nvals = size(data,dim); else @@ -670,49 +679,65 @@ classdef analysisCore < optickaCore warning('Bootstrap will fails with such a small sample, switching to 2*SD') type = '2SD'; end - avg=avgfn(data,dim); + try + avg=avgfn(data,dim, 'omitnan'); + catch + avg=avgfn(data,dim, 'omitnan'); + end + if istable; avg = avg.Variables; end switch(type) case 'SE' - err=nanstd(data,0,dim); + err=std(data, 0, dim, 'omitnan'); + if istable; err = err.Variables; end error=sqrt(err.^2/nvals); case '2SE' - err=nanstd(data,0,dim); + err=std(data, 0, dim, 'omitnan'); + if istable; err = err.Variables; end error=sqrt(err.^2/nvals); error = error*2; case 'CIMEAN' if dim == 2;data = data';end - [error, raw] = bootci(1000,{@nanmean,data},'alpha',alpha); - avg = nanmean(raw); + data(isnan(data))=[]; + [error, raw] = bootci(1000,{@mean,data},'alpha',alpha); + avg = mean(raw); case 'CIMEDIAN' if dim == 2;data = data';end - [error, raw] = bootci(1000,{@nanmedian,data},'alpha',alpha); - avg = nanmedian(raw); + data(isnan(data))=[]; + [error, raw] = bootci(1000,{@median,data},'alpha',alpha); + avg = median(raw); case 'SD' - error=nanstd(data,0,dim); + error=std(data, 0, dim, 'omitnan'); + if istable; err = err.Variables; end case '2SD' - error=(nanstd(data,0,dim))*2; + error=(std(data, 0, dim, 'omitnan'))*2; + if istable; err = err.Variables; end case '3SD' - error=(nanstd(data,0,dim))*3; + error=(std(data, 0, dim, 'omitnan'))*3; + if istable; err = err.Variables; end case 'V' - error=nanstd(data,0,dim).^2; + error=std(data, 0, dim, 'omitnan').^2; + if istable; err = err.Variables; end case 'F' if max(data)==0 error=0; else - error=nanvar(data,0,dim)/nanmean(data,dim); + error=var(data, 0, dim, 'omitnan')/mean(data, dim, 'omitnan'); end + if istable; err = err.Variables; end case 'C' if max(data)==0 error=0; else - error=nanstd(data,0,dim)/nanmean(data,dim); + error=std(data,0,dim, 'omitnan')/mean(data,dim, 'omitnan'); end + if istable; err = err.Variables; end case 'A' if max(data)==0 error=0; else - error=nanvar(diff(data),0,dim)/(2*nanmean(data,dim)); + error=var(diff(data),0,dim, 'omitnan')/(2*mean(data,dim, 'omitnan')); end + if istable; err = err.Variables; end end if onlyerror avg=error; clear error; @@ -771,6 +796,17 @@ classdef analysisCore < optickaCore set(gca,'Layer','bottom'); if alpha == 1; set(gcf,'Renderer','painters'); end end + + % =================================================================== + %> [mm, cf] = pupilConversion(value, calibration, calSize) + %> eyelink uses arbitrary area units, convert to mm using this function + %> e.g. [mm, rs] = pupilConversion(2000, 8890, 8) + % =================================================================== + function [mm, conversionFactor] = pupilConversion(value, cal, calSize) + r = (sqrt(cal) / calSize)^2; + conversionFactor = 1 / sqrt(r); + mm = sqrt(value) * conversionFactor; + end % =================================================================== %> cellArray2Num @@ -822,7 +858,7 @@ classdef analysisCore < optickaCore function colors = optimalColours(n_colors,bg,func) %make optimally different colours for plots if nargin < 1; n_colors = 20; end if ~exist('makecform','file') %no im proc toolbox, just return default colours - colors = colormap(parula(n_colors)); + colors = parula(n_colors); colors = [0 0 0; 0.8 0 0; 0 0.8 0; 0 0 0.8; 0.5 0.5 0.5; 1 0.5 0; colors]; return; end diff --git a/analysis/eyelinkAnalysis.m b/analysis/eyelinkAnalysis.m index 9abedf7c5852799288ee9df380ddeab823e35e03..5c886ac9e6748c1de26732d31f7657b0072eb400 100644 --- a/analysis/eyelinkAnalysis.m +++ b/analysis/eyelinkAnalysis.m @@ -29,7 +29,7 @@ classdef eyelinkAnalysis < analysisCore %> as correct, incorrect, breakfix etc. trialEndMessage char = 'TRIAL_RESULT' %> override the rtStart time with a custom message? - rtOverrideMessage char = 'SYNCSTROBE' + rtOverrideMessage char = 'SYNCTIME' %> minimum saccade distance in degrees minSaccadeDistance double = 1.0 %> relative velocity threshold @@ -50,6 +50,9 @@ classdef eyelinkAnalysis < analysisCore pixelsPerCm double = 32 %> screen distance distance double = 57.3 + %> conversion pupil size to mm? true/false | calibration value | measured + %> diameter + useDiameter = {false, 9250, 8} end properties (Hidden = true) @@ -145,10 +148,19 @@ classdef eyelinkAnalysis < analysisCore %> pixels per degree calculated from pixelsPerCm and distance (cache) ppd_ %> allowed properties passed to object upon construction - allowedProperties char = ['correctValue|incorrectValue|breakFixValue|'... - 'trialStartMessageName|variableMessageName|trialEndMessage|file|dir|'... - 'verbose|pixelsPerCm|distance|xCenter|yCenter|rtStartMessage|minSaccadeDistance|'... - 'rtEndMessage|trialOverride|rtDivision|rtLimits|tS|ROI|TOI|VFAC|MINDUR'] + allowedProperties = {'correctValue', 'incorrectValue', 'breakFixValue', ... + 'trialStartMessageName', 'variableMessageName', 'trialEndMessage', 'file', 'dir', ... + 'verbose', 'pixelsPerCm', 'distance', 'xCenter', 'yCenter', 'rtStartMessage', 'minSaccadeDistance', ... + 'rtEndMessage', 'trialOverride', 'rtDivision', 'rtLimits', 'tS', 'ROI', 'TOI', 'VFAC', 'MINDUR',... + 'baselineWindow','measureRange','plotRange'} + trialsTemplate = {'variable','variableMessageName','idx','correctedIndex','time',... + 'times','gx','gy','hx','hy','pa',... + 'rt','rtoverride','fixations','nfix','saccades','nsacc','saccadeTimes',... + 'firstSaccade','uuid','result','correct','breakFix','incorrect','unknown',... + 'messages','sttime','entime','totaltime','startsampletime','endsampletime',... + 'timeRange','rtstarttime','rtstarttimeOLD','rtendtime','synctime','deltaT',... + 'rttime','msacc','sampleSaccades',... + 'microSaccades','radius'} end methods @@ -157,12 +169,12 @@ classdef eyelinkAnalysis < analysisCore %> % =================================================================== function me = eyelinkAnalysis(varargin) - if nargin == 0; varargin.name = ''; end - me=me@analysisCore(varargin); %superclass constructor - if all(me.measureRange == [0.1 0.2]) %use a different default to superclass - me.measureRange = [-0.4 0.8]; - end - if nargin>0; me.parseArgs(varargin, me.allowedProperties); end + args = optickaCore.addDefaults(varargin,struct('name','eyelinkAnal',... + 'measureRange',[-0.4 1],'plotRange',[-0.5 1],... + 'baselineWindow',[])); + me = me@analysisCore(args); %superclass constructor + me.parseArgs(args, me.allowedProperties); + if isempty(me.file) || isempty(me.dir) [f,p]=uigetfile('*.edf','Load EDF File:'); if ischar(f); me.file = f; me.dir = p; end @@ -190,6 +202,12 @@ classdef eyelinkAnalysis < analysisCore if isnumeric(me.raw.RECORDINGS(1).sample_rate) me.sampleRate = double(me.raw.RECORDINGS(1).sample_rate); end + if me.useDiameter{1} == true + fprintf(':#: Correcting pupil size using %.2f for a artificial size of %.2fmm\n',me.useDiameter{2}, me.useDiameter{3}); + for jj = 1: size(me.raw.FSAMPLE.pa,1) + me.raw.FSAMPLE.pa(jj,:) = analysisCore.pupilConversion(me.raw.FSAMPLE.pa(jj,:), me.useDiameter{2}, me.useDiameter{3}); + end + end fprintf('\n'); cd(oldpath) end @@ -209,7 +227,7 @@ classdef eyelinkAnalysis < analysisCore parseEvents(me); parseAsVars(me); if isempty(me.trials) - warning('---> eyelinkAnalysis.parseSimple: Could not parse!') + warning('---> eyelinkAnalysis.parseSimple: Could not parse trials, only raw data available!'); me.isParsed = false; else me.isParsed = true; @@ -329,6 +347,57 @@ classdef eyelinkAnalysis < analysisCore fprintf('Pruned %i trials from EDF trial data \n',num) end + % =================================================================== + %> @brief give a list of trials and it will plot both the raw eye position and the + %> events + %> + %> @param + %> @return + % =================================================================== + function plotRaw(me) + if isempty(me.raw); return; end + sample = me.raw.FSAMPLE.gx(:,100); %check which eye + if sample(1) == -32768 %only use right eye if left eye data is not present + eyeUsed = 2; %right eye index for FSAMPLE.gx; + else + eyeUsed = 1; %left eye index + end + t = double(me.raw.FSAMPLE.time - me.raw.FSAMPLE.time(1)); + t = t / 1e3; + gx = me.raw.FSAMPLE.gx(eyeUsed,:); + gy = me.raw.FSAMPLE.gy(eyeUsed,:); + pa = me.raw.FSAMPLE.pa(eyeUsed,:); + handle=figure('Name',me.raw.FILENAME,'Color',[1 1 1],'NumberTitle','off',... + 'Papertype','a4','PaperUnits','centimeters',... + 'PaperOrientation','landscape'); + figpos(1,[0.95 0.75],1,'%'); + p = tiledlayout(4,1,'TileSpacing','compact','Padding','compact'); + ax(1) = nexttile; + plot(t,gx); + axis tight + title(me.raw.FILENAME,'Interpreter','none') + ylabel('X Position'); + ax(2) = nexttile; + plot(t,gy); + axis tight + ylabel('Y Position'); + ax(3) = nexttile; + plot(t,pa); + axis tight + ylabel('Pupil'); + xlabel('Time (s)'); + ax(4) = nexttile; + box on; + ylim(ax(4),[0 20]); + ylabel('Messages') + linkaxes(ax,'x'); + for i = 1:length(me.raw.FEVENT) + x = double(me.raw.FEVENT(i).sttime - me.raw.FSAMPLE.time(1)) / 1e3; + yy = 20 * rand; + text(x,yy,{me.raw.FEVENT(i).codestring,me.raw.FEVENT(i).message},'FontSize',5); + end + end + % =================================================================== %> @brief give a list of trials and it will plot both the raw eye position and the %> events @@ -339,7 +408,7 @@ classdef eyelinkAnalysis < analysisCore function handle = plot(me,select,type,seperateVars,name) % plot(me,select,type,seperateVars,name) if ~exist('select','var') || ~isnumeric(select); select = []; end - if ~exist('type','var') || isempty(type); type = 'correct'; end + if ~exist('type','var') || isempty(type); type = 'all'; end if ~exist('seperateVars','var') || ~islogical(seperateVars); seperateVars = false; end if ~exist('name','var') || isempty(name) if isnumeric(select) && length(select) > 1 @@ -1249,26 +1318,14 @@ classdef eyelinkAnalysis < analysisCore this.pixelspercm = []; this.display = []; - trialDef.variable = []; - trialDef.variableMessageName = []; - trialDef.idx = []; - trialDef.correctedIndex = []; - trialDef.time = []; + trialDef = cell2struct(repmat({[]},length(me.trialsTemplate),1),me.trialsTemplate); trialDef.rt = false; trialDef.rtoverride = false; - trialDef.fixations = []; - trialDef.nfix = 0; - trialDef.saccades = []; - trialDef.nsacc = []; - trialDef.saccadeTimes = []; trialDef.firstSaccade = NaN; - trialDef.uuid = []; - trialDef.result = []; trialDef.correct = false; trialDef.breakFix = false; trialDef.incorrect = false; trialDef.unknown = false; - trialDef.messages = []; trialDef.sttime = NaN; trialDef.entime = NaN; trialDef.totaltime = 0; @@ -1281,12 +1338,6 @@ classdef eyelinkAnalysis < analysisCore trialDef.synctime = NaN; trialDef.deltaT = NaN; trialDef.rttime = NaN; - trialDef.times = []; - trialDef.gx = []; - trialDef.gy = []; - trialDef.hx = []; - trialDef.hy = []; - trialDef.pa = []; startSampleTemp = NaN; me.ppd; %faster to cache this now (dependant property sets ppd_ too) diff --git a/analysis/iRecAnalysis.m b/analysis/iRecAnalysis.m new file mode 100644 index 0000000000000000000000000000000000000000..ee3bd9ae8e72c443143378231b7d6ea8ec98f2b0 --- /dev/null +++ b/analysis/iRecAnalysis.m @@ -0,0 +1,2031 @@ +% ======================================================================== +%> @brief iRecAnalysis offers a set of methods to load, parse & plot raw CSV files. It +%> understands opticka trials (where CSV messages INT start a trial and 255 +%> ends a trial by default) so can parse eye data and plot it for trial groups. You +%> can also manually find microsaccades, and perform ROI/TOI filtering on the eye +%> movements. +%> +%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md +% ======================================================================== +classdef iRecAnalysis < analysisCore + % iRecAnalysis offers a set of methods to load, parse & plot raw CSV files. + properties + %> file name + fileName char = '' + %> which message contains the trial start tag + trialStartMessageName = [] + %> which message contains the variable name or value + variableMessageName = 'number' + %> message name to signal end of the trial + trialEndMessage = 0 + %> the CSV message name to start measuring stimulus presentation, + %> and this is the 0 time in the analysis by default, can be + %> overridden by send SYNCTIME or rtOverrideMessage + rtStartMessage char = -1500 + %> CSV message name to end the stimulus presentation or subject repsonse + rtEndMessage char = -1501 + %> override the rtStart time with a custom message? + rtOverrideMessage char = -1000 + %> minimum saccade distance in degrees + minSaccadeDistance double = 1.0 + %> relative velocity threshold + VFAC double = 5 + %> minimum saccade duration + MINDUR double = 2 %equivalent to 6 msec at 500Hz sampling rate (cf E&R 2006) + %> the temporary experiement structure which contains the eyePos recorded from opticka + tS struct + %> exclude incorrect trials when indexing (trials contain an idx and correctedIdx value and you can use either) + excludeIncorrect logical = false + %> region of interest? + ROI double = [ ] + %> time of interest? + TOI double = [ ] + %> verbose output? + verbose = false + %> screen resolution + pixelsPerCm double = 32 + %> screen distance + distance double = 57.3 + %> screen resolution + resolution = [ 1920 1080 ] + %> For Dee's analysis edit these settings + ETparams + %> Is measure range relative to start and end markers or absolute + %> to start marker? + relativeMarkers = false + end + + properties + SYNCTIME = -1499 + END_FIX = -1500 + END_RT = -1501 + END_EXP = -500 + end + + + + properties (Hidden = true) + %TRIAL_RESULT message values, optional but tags trials with these identifiers. + correctValue double = 1 + incorrectValue double = -5 + breakFixValue double = -1 + %occasionally we have some trials in the CSV not in the plx, this prunes them out + trialsToPrune double = [] + %> these are used for spikes spike saccade time correlations + rtLimits double + rtDivision double + %> trial list from the saved behavioural data, used to fix trial name bug in old files + trialOverride struct + %> screen X center in pixels + xCenter double = 640 + %> screen Y center in pixels + yCenter double = 512 + %>57.3 bug override + override573 = false + %> downsample the data for plotting + downSample logical = true + excludeTrials = [] + end + + properties (SetAccess = private, GetAccess = public) + %> have we parsed the CSV yet? + isParsed logical = false + %> sample rate + sampleRate double = 500 + %> raw data + raw table + %> markers + markers table + %> inidividual trials + trials struct + %> eye data parsed into invdividual variables + vars struct + %> the trial variable identifier, negative values were breakfix/incorrect trials + trialList double + %> correct trials indices + correct struct = struct() + %> breakfix trials indices + breakFix struct = struct() + %> incorrect trials indices + incorrect struct = struct() + %> unknown trials indices + unknown struct = struct() + %> the display dimensions parsed from the CSV + display double + %> other display info parsed from the CSV + otherinfo struct = struct() + %> for some early CSV files, there is no trial variable ID so we + %> recreate it from the other saved data + needOverride logical = false; + %>ROI info + ROIInfo + %>TOI info + TOIInfo + %> does the trial variable list match the other saved data? + validation struct + end + + properties (Dependent = true, SetAccess = private) + %> pixels per degree calculated from pixelsPerCm and distance + ppd + end + + properties (Constant, Hidden = true) + + end + + properties (SetAccess = private, GetAccess = private) + %> pixels per degree calculated from pixelsPerCm and distance (cache) + ppd_ + %> allowed properties passed to object upon construction + allowedProperties = {'correctValue', 'incorrectValue', 'breakFixValue', ... + 'trialStartMessageName', 'variableMessageName', 'trialEndMessage', 'file', 'dir', ... + 'verbose', 'pixelsPerCm', 'distance', 'xCenter', 'yCenter', 'rtStartMessage', 'minSaccadeDistance', ... + 'rtEndMessage', 'trialOverride', 'rtDivision', 'rtLimits', 'tS', 'ROI', 'TOI', 'VFAC', 'MINDUR'} + trialsTemplate = {'variable','variableMessageName','idx','correctedIndex','time',... + 'rt','rtoverride','fixations','nfix','saccades','nsacc','saccadeTimes',... + 'firstSaccade','uuid','result','invalid','correct','breakFix','incorrect','unknown',... + 'messages','sttime','entime','totaltime','startsampletime','endsampletime',... + 'timeRange','rtstarttime','rtstarttimeOLD','rtendtime','synctime','deltaT',... + 'rttime','times','gx','gy','hx','hy','pa','msacc','sampleSaccades',... + 'microSaccades','radius'} + end + + methods + % =================================================================== + %> @brief + %> + % =================================================================== + function me = iRecAnalysis(varargin) + if nargin == 0; varargin.name = ''; end + me=me@analysisCore(varargin); %superclass constructor + if all(me.measureRange == [0.1 0.2]) %use a different default to superclass + me.measureRange = [-0.5 1.0]; + end + if nargin>0; me.parseArgs(varargin, me.allowedProperties); end + me.ppd; %cache our initial ppd_ + x = which('defaultParameters.m'); + if isempty(x) + warning('Please add NystromHolmqvist2010 to the path for full functionality!') + else + p = fileparts(x); + addpath(genpath([p filesep 'function_library'])); + addpath(genpath([p filesep 'post-process'])); + end + if isempty(me.ETparams) + me.ETparams = defaultParameters; + % settings for code specific to Niehorster, Siu & Li (2015) + me.ETparams.extraCut = [0 0]; % extra ms of data to cut before and after saccade. + me.ETparams.qInterpMissingPos = true; % interpolate using straight lines to replace missing position signals? + % settings for the saccade cutting (see cutSaccades.m for documentation) + me.ETparams.cutPosTraceMode = 1; + me.ETparams.cutVelTraceMode = 1; + me.ETparams.cutSaccadeSkipWindow= 1; % don't cut during first x seconds + me.ETparams.samplingFreq = me.sampleRate; + me.ETparams.screen.resolution = [ me.resolution(1) me.resolution(2) ]; + me.ETparams.screen.size = [ me.resolution(1)/me.pixelsPerCm/100 me.resolution(2)/me.pixelsPerCm/100 ]; + me.ETparams.screen.viewingDist = me.distance/100; + me.ETparams.screen.dataCenter = [ me.resolution(1)/2 me.resolution(2)/2 ]; % center of screen has these coordinates in data + me.ETparams.screen.subjectStraightAhead = [ me.resolution(1)/2 me.resolution(2)/2 ]; % Specify the screen coordinate that is straight ahead of the subject. Just specify the middle of the screen unless its important to you to get this very accurate! + % change some defaults as needed for this analysis: + me.ETparams.data.alsoStoreComponentDerivs = true; + me.ETparams.data.detrendWithMedianFilter = true; + me.ETparams.data.applySaccadeTemplate = true; + me.ETparams.data.minDur = 100; + me.ETparams.fixation.doClassify = true; + me.ETparams.blink.replaceWithInterp = true; + me.ETparams.blink.replaceVelWithNan = true; + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function load(me, force) + if ~exist('force','var'); force = false;end + if isempty(me.fileName) + [f,p]=uigetfile('*.csv','Load Main CSV File:'); + if ischar(f); me.fileName = f; me.dir = p; end + end + if isempty(me.fileName) + warning('No CSV file specified...'); + return + end + if ~isempty(me.raw) && force == false; disp('Data loaded previously, skipping loading...');return; end + me.raw = []; me.markers = []; + tmain = tic; + oldpath = pwd; + [p,f,e] = fileparts(me.fileName); + if ~isempty(p); me.dir = p; end + cd(me.dir); + me.raw = readtable([f e],'ReadVariableNames',true); + me.markers = readtable([f 'net' e],'ReadVariableNames',true); + + me.sampleRate = 1/mean(diff(me.raw.time)); + + cd(oldpath) + if isempty(me.raw) || isempty(me.markers) + fprintf(':#: Loading Raw CSV Data failed...\n'); + else + fprintf(':#: Loading Raw CSV Data @ %.2fHz took %.2f secs\n',me.sampleRate,toc(tmain)); + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function parseSimple(me) + tmain = tic; + if isempty(me.raw); me.load(); end + me.isParsed = false; + parseEvents(me); + parseAsVars(me); + if isempty(me.trials) + warning('---> iRecAnalysis.parseSimple: Could not parse!') + me.isParsed = false; + else + me.isParsed = true; + end + fprintf('Simple Parsing of CSV Trials took %.2f secs\n',toc(tmain)); + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function parse(me) + tmain = tic; + if isempty(me.raw); me.load(); end + me.isParsed = false; + parseEvents(me); + if ~isempty(me.trialsToPrune) + me.pruneTrials(me.trialsToPrune); + end + parseAsVars(me); + parseSecondaryEyePos(me); + parseFixationPositions(me); + parseSaccades(me); + me.isParsed = true; + fprintf('\tOverall Parsing of CSV Data took %.2f secs\n',toc(tmain)); + end + + % =================================================================== + %> @brief parse saccade related data + %> + %> @param + %> @return + % =================================================================== + function parseSaccades(me) + parseROI(me); + parseTOI(me); + computeMicrosaccades(me); + computeFullSaccades(me); + end + + % =================================================================== + %> @brief remove trials from correct list that did not use a rt start or end message + %> + %> @param + %> @return + % =================================================================== + function pruneNonRTTrials(me) + for i = 1:length(me.trials) + if isnan(me.trials(i).rtstarttime) || isnan(me.trials(i).rtendtime) + me.trials(i).correct = false; + me.trials(i).incorrect = true; + end + end + me.correct.idx = find([me.trials.correct] == true); + me.correct.saccTimes = [me.trials(me.correct.idx).firstSaccade]; + tr = [me.trials(me.correct.idx).timeRange]; + tr = reshape(tr,[2,length(me.correct.idx)])'; + me.correct.timeRange = tr; + me.incorrect.idx = find([me.trials.incorrect] == true); + me.incorrect.saccTimes = [me.trials(me.incorrect.idx).firstSaccade]; + end + + % =================================================================== + %> @brief update correct index list + %> + %> @param + %> @return + % =================================================================== + function updateCorrectIndex(me,idx) + if max(idx) > length(me.trials) + warning('Your custom index exceeds the number of trials!') + return + end + if min(idx) < 1 + warning('Your custom index includes values less than 1!') + return + end + + me.correct.idx = idx; + me.correct.saccTimes = [me.trials(me.correct.idx).firstSaccade]; + tr = [me.trials(me.correct.idx).timeRange]; + tr = reshape(tr,[2,length(me.correct.idx)])'; + me.correct.timeRange = tr; + me.correct.isCustomIdx = true; + + for i = me.correct.idx + me.trials(i).correct = true; + me.trials(i).incorrect = false; + end + end + + % =================================================================== + %> @brief prunetrials -- very rarely (n=1) we lose a trial strobe in the plexon data and + %> thus when we try to align the plexon trial index and CSV trial index they are off-by-one, + %> this function is used once the index of the trial is know to prune it out of the CSV data + %> set and recalculate the indexes. + %> + %> @param + %> @return + % =================================================================== + function pruneTrials(me,num) + me.trials(num) = []; + me.trialList(num) = []; + me.correct.idx = find([me.trials.correct] == true); + me.correct.saccTimes = [me.trials(me.correct.idx).firstSaccade]; + tr = [me.trials(me.correct.idx).timeRange]; + tr = reshape(tr,[2,length(me.correct.idx)])'; + me.correct.timeRange = tr; + me.breakFix.idx = find([me.trials.breakFix] == true); + me.breakFix.saccTimes = [me.trials(me.breakFix.idx).firstSaccade]; + me.incorrect.idx = find([me.trials.incorrect] == true); + me.incorrect.saccTimes = [me.trials(me.incorrect.idx).firstSaccade]; + for i = num:length(me.trials) + me.trials(i).correctedIndex = me.trials(i).correctedIndex - 1; + end + fprintf('Pruned %i trials from CSV trial data \n',num) + end + + % =================================================================== + %> @brief give a list of trials and it will plot both the raw eye position and the + %> events + %> + %> @param + %> @return + % =================================================================== + function handle = plot(me,select,type,seperateVars,name,handle) + % plot(me,select,type,seperateVars,name) + if ~exist('select','var') || ~isnumeric(select); select = []; end + if ~exist('type','var') || isempty(type); type = 'correct'; end + if ~exist('seperateVars','var') || ~islogical(seperateVars); seperateVars = false; end + if ~exist('name','var') || isempty(name) + if isnumeric(select) && length(select) > 1 + name = [me.fileName ' | Select: ' num2str(length(select)) ' trials']; + else + name = [me.fileName ' | Select: ' num2str(select)]; + end + end + if ~exist('handle','var'); handle = []; end + if isnumeric(select) && ~isempty(select) + idx = select; + type = ''; + idxInternal = false; + else + switch lower(type) + case 'correct' + idx = me.correct.idx; + case 'breakfix' + idx = me.breakFix.idx; + case 'incorrect' + idx = me.incorrect.idx; + otherwise + idx = 1:length(me.trials); + end + idxInternal = true; + end + idx = setdiff(idx, me.excludeTrials); + if isempty(idx) + fprintf('No trials were selected to plot...\n') + return + end + if seperateVars == true && isempty(select) + vars = unique([me.trials(idx).id]); + for j = vars + me.plot(j,type,false); + drawnow; + end + return + end + + a = 1; + stdex = []; + meanx = []; + meany = []; + stdey = []; + sacc = []; + xvals = []; + yvals = []; + tvals = {}; + medx = []; + medy = []; + early = 0; + mS = []; + + map = me.optimalColours(length(me.vars)); + for i = 1:length(me.vars) + varidx(i) = str2num(me.vars(i).name); + end + + if isempty(select) + thisVarName = 'ALL'; + elseif length(select) > 1 + thisVarName = 'SELECTION'; + else + thisVarName = ['VAR' num2str(select)]; + end + + maxv = 1; + me.ppd; + if ~isempty(me.TOI) + t1 = me.TOI(1); t2 = me.TOI(2); + else + t1 = me.baselineWindow(1); t2 = me.baselineWindow(2); + end + + for i = idx + if ~exist('didplot','var'); didplot = false; end + if idxInternal == true %we're using the eyelink index which includes incorrects + f = i; + elseif me.excludeIncorrect %we're using an external index which excludes incorrects + f = find([me.trials.correctedIndex] == i); + else + f = find([me.trials.idx] == i); + end + if isempty(f); continue; end + + thisTrial = me.trials(f(1)); + + if thisTrial.invalid + continue; + end + + tidx = find(varidx==thisTrial.variable); + + if thisTrial.variable == 1010 || isempty(me.vars) %early CSV files were broken, 1010 signifies this + c = rand(1,3); + else + c = map(tidx,:); + end + + if isempty(select) || length(select) > 1 || ~isempty(intersect(select,idx)) + + else + continue + end + + t = thisTrial.times; %convert to seconds + ix = find((t >= me.measureRange(1)) & (t <= me.measureRange(2))); + ip = find((t >= me.plotRange(1)) & (t <= me.plotRange(2))); + tm = t(ix); + tp = t(ip); + xa = thisTrial.gx; + ya = thisTrial.gy; + if all(isnan(xa)) && all(isnan(ya)); continue; end + lim = 60; %max degrees in data + xa(xa < -lim) = -lim; xa(xa > lim) = lim; + ya(ya < -lim) = -lim; ya(ya > lim) = lim; + pupilAll = thisTrial.pa; + + %x = xa(ix); + %y = ya(ix); + %pupilMeasure = pupilAll(ix); + + xp = xa(ip); + yp = ya(ip); + xmin = min(xp); xmax = max(xp); + ymin = min(yp); ymax = max(yp); + pupilPlot = pupilAll(ip); + + if me.downSample && me.sampleRate > 500 + ds = floor(me.sampleRate/200); + idx = circshift(logical(mod(1:length(tp), ds)), -(ds-1)); %downsample every N as less points to draw + tp(idx) = []; + xp(idx) = []; + yp(idx) = []; + pupilPlot(idx) = []; + end + + if isempty(handle) + handle=figure('Name',name,'Color',[1 1 1],'NumberTitle','off',... + 'Papertype','a4','PaperUnits','centimeters',... + 'PaperOrientation','landscape','Renderer','painters'); + figpos(1,[0.6 0.9],1,'%'); + p = tiledlayout(3,2,'TileSpacing','compact','Padding','compact'); + end + figure(handle); + + sz = 100; + ax = nexttile(1); + hold on; + if isfield(thisTrial,'sampleSaccades') & ~isnan(thisTrial.sampleSaccades) & ~isempty(thisTrial.sampleSaccades) + for jj = 1: length(thisTrial.sampleSaccades) + if thisTrial.sampleSaccades(jj) >= me.plotRange(1) && thisTrial.sampleSaccades(jj) <= me.plotRange(2) + midx = me.findNearest(tp,thisTrial.sampleSaccades(jj)); + scatter(xp(midx),yp(midx),sz,'^','filled','MarkerEdgeColor',[1 1 1],'MarkerFaceAlpha',0.5,... + 'MarkerFaceColor',[0 0 0],'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable thisTrial.sampleSaccades(jj)],'ButtonDownFcn', @clickMe); + end + end + end + if isfield(thisTrial,'microSaccades') & ~isnan(thisTrial.microSaccades) & ~isempty(thisTrial.microSaccades) + for jj = 1: length(thisTrial.microSaccades) + if thisTrial.microSaccades(jj) >= me.plotRange(1) && thisTrial.microSaccades(jj) <= me.plotRange(2) + midx = me.findNearest(tp,thisTrial.microSaccades(jj)); + scatter(xp(midx),yp(midx),sz,'o','filled','MarkerEdgeColor',[0 0 0],... + 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable thisTrial.microSaccades(jj)],'ButtonDownFcn', @clickMe); + end + end + end + plot(xp, yp,'k-','Color',c,'LineWidth',1,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); + + ax = nexttile(2); + hold on; + plot(tp,abs(xp),'k-','Color',c,'MarkerSize',3,'MarkerEdgeColor',c,... + 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); + plot(tp,abs(yp),'k.-','Color',c,'MarkerSize',3,'MarkerEdgeColor',c,... + 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); + maxv = max([maxv, max(abs(xp)), max(abs(yp))]) + 0.1; + if isfield(thisTrial,'sampleSaccades') & ~isnan(thisTrial.sampleSaccades) & ~isempty(thisTrial.sampleSaccades) + if any(thisTrial.sampleSaccades >= me.plotRange(1) & thisTrial.sampleSaccades <= me.plotRange(2)) + scatter(thisTrial.sampleSaccades,-0.1,sz,'^','filled','MarkerEdgeColor',[1 1 1],'MarkerFaceAlpha',0.5,... + 'MarkerFaceColor',[0 0 0],'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); + end + end + if isfield(thisTrial,'microSaccades') & ~isnan(thisTrial.microSaccades) & ~isempty(thisTrial.microSaccades) + if any(thisTrial.microSaccades >= me.plotRange(1) & thisTrial.microSaccades <= me.plotRange(2)) + scatter(thisTrial.microSaccades,-0.1,sz,'o','filled','MarkerEdgeColor',[0 0 0],... + 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); + end + end + + ax = nexttile(5); + hold on; + for fix=1:length(thisTrial.fixations) + f=thisTrial.fixations(fix); + ti = double(f.time); le = double(f.length); + if ti >= me.plotRange(1)-0.1 && ti+le <= me.plotRange(2)+0.1 + plot3([ti ti+le],[f.gstx f.genx],[f.gsty f.geny],'k-o',... + 'LineWidth',1,'MarkerSize',5,'MarkerEdgeColor',[0 0 0],... + 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],... + 'ButtonDownFcn', @clickMe) + end + end + if ~isempty(thisTrial.saccades) + for sac=1:length(thisTrial.saccades) + s=thisTrial.saccades(sac); + ti = double(s.time); le = double(s.length); + if ti >= me.plotRange(1)-0.1 && ti+le <= me.plotRange(2)+0.1 + plot3([ti ti+le],[s.gstx s.genx],[s.gsty s.geny],'r-o',... + 'LineWidth',1.5,'MarkerSize',5,'MarkerEdgeColor',[1 0 0],... + 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],... + 'ButtonDownFcn', @clickMe) + end + end + elseif ~isempty(thisTrial.msacc) + for sac=1:length(thisTrial.msacc) + s=thisTrial.msacc(sac); + ti = s.time; le = s.endtime; + if ti >= me.plotRange(1)-0.1 && le <= me.plotRange(2)+0.1 + plot3([ti le],[s.dx s.dX],[s.dy s.dY],'r-o',... + 'LineWidth',1.5,'MarkerSize',5,'MarkerEdgeColor',[1 0 0],... + 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],... + 'ButtonDownFcn', @clickMe) + end + end + end + + ax = nexttile(6); + hold on; + plot(tp,pupilPlot,'Color',c, 'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); + + idxt = find(t >= t1 & t <= t2); + + tvals{a} = t(idxt); + xvals{a} = xa(idxt); + yvals{a} = ya(idxt); + if isfield(thisTrial,'firstSaccade') && thisTrial.firstSaccade > 0 + sacc = [sacc double(thisTrial.firstSaccade)/1e3]; + end + meanx = [meanx mean(xa(idxt))]; + meany = [meany mean(ya(idxt))]; + medx = [medx median(xa(idxt))]; + medy = [medy median(ya(idxt))]; + stdex = [stdex std(xa(idxt))]; + stdey = [stdey std(ya(idxt))]; + + udt = [thisTrial.idx thisTrial.correctedIndex thisTrial.variable]; + + ax = nexttile(3); + hold on; + plot(meanx(end), meany(end),'ko','Color',c,'MarkerSize',6,'MarkerEdgeColor',[0 0 0],... + 'MarkerFaceColor',c,'UserData', udt,'ButtonDownFcn', @clickMe); + + ax = nexttile(4); + hold on; + plot3(meanx(end), meany(end),a,'ko','Color',c,'MarkerSize',6,'MarkerEdgeColor',[0 0 0],... + 'MarkerFaceColor',c,'UserData', udt,'ButtonDownFcn', @clickMe); + a = a + 1; + didplot = true; + + end + + if ~didplot + close(handle); + return; + end + + colormap(map); + + display = [80 80]; + + ah = nexttile(1); + ah.ButtonDownFcn = @spawnMe; + ah.DataAspectRatio = [1 1 1]; + axis equal; + axis ij; + grid on; + box on; + xlim([xmin xmax]); ylim([ymin ymax]); + title(ah,[thisVarName upper(type) ': X vs. Y Eye Position']); + xlabel(ah,'X°'); + ylabel(ah,'Y°'); + + ah = nexttile(2); + ah.ButtonDownFcn = @spawnMe; + grid on; + box on; + axis tight; + axis([me.plotRange(1) me.plotRange(2) -0.2 maxv+1]) + ti=sprintf('ABS Mean/SD %.2f - %.2f s: X=%.2f / %.2f | Y=%.2f / %.2f', t1,t2,... + mean(abs(meanx)), mean(abs(stdex)), ... + mean(abs(meany)), mean(abs(stdey))); + ti2 = sprintf('ABS Median/SD %.2f - %.2f s: X=%.2f / %.2f | Y=%.2f / %.2f', t1,t2,median(abs(medx)), median(abs(stdex)), ... + median(abs(medy)), median(abs(stdey))); + h=title(sprintf('X & Y(dot) Position vs. Time\n%s\n%s', ti,ti2)); + set(h,'BackgroundColor',[1 1 1]); + xlabel(ah,'Time (s)'); + ylabel(ah,'°'); + + ah = nexttile(5); + ah.ButtonDownFcn = @spawnMe; + grid on; + box on; + axis([me.plotRange(1) me.plotRange(2) -10 10 -10 10]); + view([35 20]); + set(gca,'PlotBoxAspectRatio',[2 1 1]) + xlabel(ah,'Time (ms)'); + ylabel(ah,'X Position'); + zlabel(ah,'Y Position'); + [mn,er] = me.stderr(sacc,'SD'); + md = nanmedian(sacc); + h=title(sprintf('%s %s: Saccades (red) & Fixation (black) | First Saccade mean/median: %.2f / %.2f +- %.2f SD [%.2f <> %.2f]',... + thisVarName,upper(type),mn,md,er,min(sacc),max(sacc))); + set(h,'BackgroundColor',[1 1 1]); + + ah = nexttile(6); + ah.ButtonDownFcn = @spawnMe; + axis([me.plotRange(1) me.plotRange(2) -inf inf]); + grid on; + box on; + title(ah,[thisVarName upper(type) ': Pupil Diameter']); + xlabel(ah,'Time (s)'); + ylabel(ah,'Diameter'); + + ah = nexttile(3); + ah.ButtonDownFcn = @spawnMe; + axis ij; + grid on; + box on; + axis tight; + axis square; + %axis([-5 5 -5 5]) + h=title(sprintf('X & Y %.2f-%.2fs MD/MN/STD: \nX : %.2f / %.2f / %.2f | Y : %.2f / %.2f / %.2f', ... + t1,t2,mean(meanx), median(medx),mean(stdex),mean(meany),median(medy),mean(stdey))); + set(h,'BackgroundColor',[1 1 1]); + xlabel(ah,'X°'); + ylabel(ah,'Y°'); + + ah = nexttile(4); + ah.ButtonDownFcn = @spawnMe; + grid on; + box on; + axis tight; + %axis([-5 5 -5 5]); + %axis square + view([50 30]); + title(sprintf('%s %s Mean X & Y Pos %.2f-%.2fs over time',thisVarName,upper(type),t1,t2)); + xlabel(ah,'X°'); + ylabel(ah,'Y°'); + zlabel(ah,'Trial'); + + assignin('base','xvals',xvals); + assignin('base','yvals',yvals); + + function clickMe(src, ~) + if ~exist('src','var') + return + end + l=get(src,'LineWidth'); + if l > 1;src.LineWidth=1;else;src.LineWidth=3;end + if isprop(src,'LineStyle') + l=get(src,'LineStyle'); + if matches(l,'-');src.LineStyle=':';else;src.LineStyle='-';end + end + ud = get(src,'UserData'); + if ~isempty(ud) + disp(me.trials(ud(1))); + disp(['TRIAL | CORRECTED | VAR | microSaccade time = ' num2str(ud)]); + end + end + function spawnMe(src, ~) + fnew = figure('Color',[1 1 1]); + na = copyobj(src,fnew); + na.Position = [0.1 0.1 0.8 0.8]; + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function parseROI(me) + if isempty(me.ROI) + disp('No ROI specified...') + return + end + tROI = tic; + fixationX = me.ROI(1); + fixationY = me.ROI(2); + fixationRadius = me.ROI(3); + for i = 1:length(me.trials) + me.ROIInfo(i).variable = me.trials(i).variable; + me.ROIInfo(i).idx = i; + me.ROIInfo(i).correctedIndex = me.trials(i).correctedIndex; + me.ROIInfo(i).uuid = me.trials(i).uuid; + me.ROIInfo(i).fixationX = fixationX; + me.ROIInfo(i).fixationY = fixationY; + me.ROIInfo(i).fixationRadius = fixationRadius; + x = me.trials(i).gx; + y = me.trials(i).gy; + times = me.trials(i).times; + idx = find(times > 0); % we only check ROI post 0 time + times = times(idx); + x = x(idx); + y = y(idx); + r = sqrt((x - fixationX).^2 + (y - fixationY).^2); + within = find(r < fixationRadius); + if any(within) + me.ROIInfo(i).enteredROI = true; + else + me.ROIInfo(i).enteredROI = false; + end + me.trials(i).enteredROI = me.ROIInfo(i).enteredROI; + me.ROIInfo(i).x = x; + me.ROIInfo(i).y = y; + me.ROIInfo(i).times = times; + me.ROIInfo(i).r = r; + me.ROIInfo(i).within = within; + me.ROIInfo(i).correct = me.trials(i).correct; + me.ROIInfo(i).breakFix = me.trials(i).breakFix; + me.ROIInfo(i).incorrect = me.trials(i).incorrect; + end + fprintf(':#: Parsing eyelink region of interest (ROI) took %g secs\n', round(toc(tROI))) + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function parseTOI(me) + if isempty(me.TOI) + disp('No TOI specified...') + return + end + tTOI = tic; + me.ppd; + if length(me.TOI)==2 && ~isempty(me.ROI); me.TOI = [me.TOI me.ROI]; end + t1 = me.TOI(1); + t2 = me.TOI(2); + fixationX = me.TOI(3); + fixationY = me.TOI(4); + fixationRadius = me.TOI(5); + for i = 1:length(me.trials) + times = me.trials(i).times; + x = me.trials(i).gx; + y = me.trials(i).gy; + + idx = intersect(find(times>=t1), find(times<=t2)); + times = times(idx); + x = x(idx); + y = y(idx); + + r = sqrt((x - fixationX).^2 + (y - fixationY).^2); + + within = find(r <= fixationRadius); + if length(within) == length(r) + me.TOIInfo(i).isTOI = true; + else + me.TOIInfo(i).isTOI = false; + end + me.trials(i).isTOI = me.TOIInfo(i).isTOI; + me.TOIInfo(i).variable = me.trials(i).variable; + me.TOIInfo(i).idx = i; + me.TOIInfo(i).correctedIndex = me.trials(i).correctedIndex; + me.TOIInfo(i).uuid = me.trials(i).uuid; + me.TOIInfo(i).t1 = t1; + me.TOIInfo(i).t2 = t2; + me.TOIInfo(i).fixationX = fixationX; + me.TOIInfo(i).fixationY = fixationY; + me.TOIInfo(i).fixationRadius = fixationRadius; + me.TOIInfo(i).times = times; + me.TOIInfo(i).x = x; + me.TOIInfo(i).y = y; + me.TOIInfo(i).r = r; + me.TOIInfo(i).within = within; + me.TOIInfo(i).correct = me.trials(i).correct; + me.TOIInfo(i).breakFix = me.trials(i).breakFix; + me.TOIInfo(i).incorrect = me.trials(i).incorrect; + end + fprintf(':#: Parsing eyelink time of interest (TOI) took %g secs\n', round(toc(tTOI))) + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function plotROI(me) + if ~isempty(me.ROIInfo) + h=figure;figpos(1,[2000 1000]);set(h,'Color',[1 1 1],'Name',me.fileName); + + x1 = me.ROI(1) - me.ROI(3); + x2 = me.ROI(1) + me.ROI(3); + xmin = min([abs(x1), abs(x2)]); + xmax = max([abs(x1), abs(x2)]); + y1 = me.ROI(2) - me.ROI(3); + y2 = me.ROI(2) + me.ROI(3); + ymin = min([abs(y1), abs(y2)]); + ymax = max([abs(y1), abs(y2)]); + xp = [x1 x1 x2 x2]; + yp = [y1 y2 y2 y1]; + xpp = [xmin xmin xmax xmax]; + ypp = [ymin ymin ymax ymax]; + p=panel(h); + p.pack(1,2); + p.fontsize = 14; + p.margin = [15 15 15 15]; + yes = logical([me.ROIInfo.enteredROI]); + no = ~yes; + yesROI = me.ROIInfo(yes); + noROI = me.ROIInfo(no); + p(1,1).select(); + p(1,1).hold('on'); + patch(xp,yp,[1 1 0],'EdgeColor','none'); + p(1,2).select(); + p(1,2).hold('on'); + patch([0 1 1 0],xpp,[1 1 0],'EdgeColor','none'); + patch([0 1 1 0],ypp,[0.5 1 0],'EdgeColor','none'); + for i = 1:length(noROI) + c = [0.7 0.7 0.7]; + if noROI(i).correct == true + l = 'o-'; + else + l = '.--'; + end + t = noROI(i).times(noROI(i).times >= 0); + x = noROI(i).x(noROI(i).times >= 0); + y = noROI(i).y(noROI(i).times >= 0); + if ~isempty(x) + p(1,1).select(); + h = plot(x,y,l,'color',c,'MarkerFaceColor',c,'LineWidth',1); + set(h,'UserData',[noROI(i).idx noROI(i).correctedIndex noROI(i).variable noROI(i).correct noROI(i).breakFix noROI(i).incorrect],'ButtonDownFcn', @clickMeROI); + p(1,2).select(); + h = plot(t,abs(x),l,t,abs(y),l,'color',c,'MarkerFaceColor',c); + set(h,'UserData',[noROI(i).idx noROI(i).correctedIndex noROI(i).variable noROI(i).correct noROI(i).breakFix noROI(i).incorrect],'ButtonDownFcn', @clickMeROI); + end + end + for i = 1:length(yesROI) + c = [0.7 0 0]; + if yesROI(i).correct == true + l = 'o-'; + else + l = '.--'; + end + t = yesROI(i).times(yesROI(i).times >= 0); + x = yesROI(i).x(yesROI(i).times >= 0); + y = yesROI(i).y(yesROI(i).times >= 0); + if ~isempty(x) + p(1,1).select(); + h = plot(x,y,l,'color',c,'MarkerFaceColor',c); + set(h,'UserData',[yesROI(i).idx yesROI(i).correctedIndex yesROI(i).variable yesROI(i).correct yesROI(i).breakFix yesROI(i).incorrect],'ButtonDownFcn', @clickMeROI); + p(1,2).select(); + h = plot(t,abs(x),l,t,abs(y),l,'color',c,'MarkerFaceColor',c); + set(h,'UserData',[yesROI(i).idx yesROI(i).correctedIndex yesROI(i).variable yesROI(i).correct yesROI(i).breakFix yesROI(i).incorrect],'ButtonDownFcn', @clickMeROI); + end + end + hold off + p(1,1).select(); + p(1,1).hold('off'); + box on + grid on + p(1,1).title(['ROI PLOT for ' num2str(me.ROI) ' (entered = ' num2str(sum(yes)) ' | did not = ' num2str(sum(no)) ')']); + p(1,1).xlabel('X Position (degs)') + p(1,1).ylabel('Y Position (degs)') + axis square + %axis([-10 10 -10 10]); + p(1,2).select(); + p(1,2).hold('off'); + box on + grid on + p(1,2).title(['ROI PLOT for ' num2str(me.ROI) ' (entered = ' num2str(sum(yes)) ' | did not = ' num2str(sum(no)) ')']); + p(1,2).xlabel('Time(s)') + p(1,2).ylabel('Absolute X/Y Position (degs)') + axis square + %axis([0 0.5 0 10]); + end + function clickMeROI(src, ~) + if ~exist('src','var') + return + end + ud = get(src,'UserData'); + lw = get(src,'LineWidth'); + if lw < 1.8 + set(src,'LineWidth',2) + else + set(src,'LineWidth',1) + end + if ~isempty(ud) + disp(['ROI Trial (idx correctidx var iscorrect isbreak isincorrect): ' num2str(ud)]); + end + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function plotTOI(me) + if isempty(me.TOIInfo) + disp('No TOI parsed!!!') + return + end + h=figure;figpos(1,[2000 1000]);set(h,'Color',[1 1 1],'Name',me.fileName); + + t1 = me.TOI(1); + t2 = me.TOI(2); + x1 = me.TOI(3) - me.TOI(5); + x2 = me.TOI(3) + me.TOI(5); + xmin = min([abs(x1), abs(x2)]); + xmax = max([abs(x1), abs(x2)]); + y1 = me.TOI(4) - me.TOI(5); + y2 = me.TOI(4) + me.TOI(5); + ymin = min([abs(y1), abs(y2)]); + ymax = max([abs(y1), abs(y2)]); + xp = [x1 x1 x2 x2]; + yp = [y1 y2 y2 y1]; + xpp = [xmin xmin xmax xmax]; + ypp = [ymin ymin ymax ymax]; + p=panel(h); + p.pack(1,2); + p.fontsize = 14; + p.margin = [15 15 15 15]; + yes = logical([me.TOIInfo.isTOI]); + no = ~yes; + yesTOI = me.TOIInfo(yes); + noTOI = me.TOIInfo(no); + p(1,1).select(); + p(1,1).hold('on'); + patch(xp,yp,[1 1 0],'EdgeColor','none'); + p(1,2).select(); + p(1,2).hold('on'); + patch([t1 t2 t2 t1],xpp,[1 1 0],'EdgeColor','none'); + patch([t1 t2 t2 t1],ypp,[0.5 1 0],'EdgeColor','none'); + for i = 1:length(noTOI) + if noTOI(i).incorrect == true; continue; end + c = [0.7 0.7 0.7]; + if noTOI(i).correct == true + l = 'o-'; + else + l = '.--'; + end + t = noTOI(i).times; + x = noTOI(i).x; + y = noTOI(i).y; + if ~isempty(x) + p(1,1).select(); + h = plot(x,y,l,'color',c,'MarkerFaceColor',c,'LineWidth',1); + set(h,'UserData',[noTOI(i).idx noTOI(i).correctedIndex noTOI(i).variable noTOI(i).correct noTOI(i).breakFix noTOI(i).incorrect],'ButtonDownFcn', @clickMeTOI); + p(1,2).select(); + h = plot(t,abs(x),l,t,abs(y),l,'color',c,'MarkerFaceColor',c); + set(h,'UserData',[noTOI(i).idx noTOI(i).correctedIndex noTOI(i).variable noTOI(i).correct noTOI(i).breakFix noTOI(i).incorrect],'ButtonDownFcn', @clickMeTOI); + end + end + for i = 1:length(yesTOI) + if yesTOI(i).incorrect == true; continue; end + c = [0.7 0 0]; + if yesTOI(i).correct == true + l = 'o-'; + else + l = '.--'; + end + t = yesTOI(i).times; + x = yesTOI(i).x; + y = yesTOI(i).y; + if ~isempty(x) + p(1,1).select(); + h = plot(x,y,l,'color',c,'MarkerFaceColor',c); + set(h,'UserData',[yesTOI(i).idx yesTOI(i).correctedIndex yesTOI(i).variable yesTOI(i).correct yesTOI(i).breakFix yesTOI(i).incorrect],'ButtonDownFcn', @clickMeTOI); + p(1,2).select(); + h = plot(t,abs(x),l,t,abs(y),l,'color',c,'MarkerFaceColor',c); + set(h,'UserData',[yesTOI(i).idx yesTOI(i).correctedIndex yesTOI(i).variable yesTOI(i).correct yesTOI(i).breakFix yesTOI(i).incorrect],'ButtonDownFcn', @clickMeTOI); + end + end + hold off + p(1,1).select(); + p(1,1).hold('off'); + box on + grid on + p(1,1).title(['TOI PLOT for ' num2str(me.TOI) ' (yes = ' num2str(sum(yes)) ' || no = ' num2str(sum(no)) ')']); + p(1,1).xlabel('X Position (degs)') + p(1,1).ylabel('Y Position (degs)') + %axis([-4 4 -4 4]); + axis square + p(1,2).select(); + p(1,2).hold('off'); + box on + grid on + p(1,2).title(['TOI PLOT for ' num2str(me.TOI) ' (yes = ' num2str(sum(yes)) ' || no = ' num2str(sum(no)) ')']); + p(1,2).xlabel('Time(s)') + p(1,2).ylabel('Absolute X/Y Position (degs)') + %axis([t1 t2 0 4]); + axis square + + function clickMeTOI(src, ~) + if ~exist('src','var') + return + end + ud = get(src,'UserData'); + lw = get(src,'LineWidth'); + if lw < 1.8 + set(src,'LineWidth',2) + else + set(src,'LineWidth',1) + end + if ~isempty(ud) + disp(['TOI Trial (idx correctidx var iscorrect isbreak isincorrect): ' num2str(ud)]); + end + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function ppd = get.ppd(me) + if me.distance == 57.3 && me.override573 == true + ppd = round( me.pixelsPerCm * (67 / 57.3)); %set the pixels per degree, note this fixes some older files where 57.3 was entered instead of 67cm + else + ppd = round( me.pixelsPerCm * (me.distance / 57.3)); %set the pixels per degree + end + me.ppd_ = ppd; + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function fixVarNames(me) + if me.needOverride == true + if isempty(me.trialOverride) + warning('No replacement trials available!!!') + return + end + trials = me.trialOverride; %#ok<*PROP> + if max([me.trials.correctedIndex]) ~= length(trials) + warning('TRIAL ID LENGTH MISMATCH!'); + return + end + a = 1; + me.trialList = []; + for j = 1:length(me.trials) + if me.trials(j).incorrect ~= true + if a <= length(trials) && me.trials(j).correctedIndex == trials(a).index + me.trials(j).oldid = me.trials(j).variable; + me.trials(j).variable = trials(a).variable; + me.trialList(j) = me.trials(j).variable; + if me.trials(j).breakFix == true + me.trialList(j) = -[me.trialList(j)]; + end + a = a + 1; + end + end + end + parseAsVars(me); %need to redo this now + warning('---> Trial name override in place!!!') + else + me.trialOverride = struct(); + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function removeRawData(me) + + me.raw = []; + me.markers = []; + + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function reparseVars(me) + me.parseAsVars; + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function plotNH(me, trial, handle) + if ~me.isParsed;return;end + if ~exist('handle','var'); handle=[]; end + try + if isempty(handle) + handle=figure('Name','Saccade Plots','Color',[1 1 1],'NumberTitle','off',... + 'Papertype','a4','PaperUnits','centimeters',... + 'PaperOrientation','landscape'); + figpos(1,[0.5 0.9],1,'%'); + end + figure(handle); + data = me.trials(trial).data; + me.ETparams.screen.rect = struct('deg', [-5 -5 5 5]); + plotClassification(data,'deg','vel',me.ETparams.samplingFreq,... + me.ETparams.glissade.searchWindow,me.ETparams.screen.rect,... + 'title','Test','showSacInScan',true); + catch ME + getReport(ME) + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function explore(me, close) + persistent ww hh fig figA figB N + + if exist('close','var') && close == true + try delete(figB); end + try delete(figA); end + try delete(fig.f); end + fig = []; figA = []; figB = []; + return; + end + if isempty(N); N = 1; end + if isempty(fig); fig.f = figure('Units','Normalized','Position',[0 0.8 0.05 0.2],'CloseRequestFcn',@exploreClose); end + if isempty(figA); figA = figure('Units','Normalized','Position',[0.05 0 0.45 1],'CloseRequestFcn',@exploreClose); end + if isempty(figB); figB = figure('Units','Normalized','Position',[0.5 0 0.5 1],'CloseRequestFcn',@exploreClose); end + + if isempty(fig.f.Children)|| ~ishandle(fig.f) + fig.b0 = uicontrol('Parent',fig.f,'Units','Normalized',... + 'Style','text','String',['TRIAL: ' num2str(N)],'Position',[0.1 0.8 0.8 0.1]); + fig.b1 = uicontrol('Parent',fig.f,'Units','Normalized',... + 'String','Next','Position',[0.1 0.1 0.8 0.3],... + 'Callback', @exploreNext); + fig.b2 = uicontrol('Parent',fig.f,'Units','Normalized',... + 'String','Previous','Position',[0.1 0.5 0.8 0.3],... + 'Callback', @explorePrevious); + end + + if isempty(figA.Children) ; N = 0; exploreNext(); end + + function exploreNext(src, ~) + N = N + 1; + if N > length(me.trials); N = 1; end + clf(figA); clf(figB); + plotNH(me,N,figB); + plot(me,N,[],[],[],figA); + fig.b0.String = ['TRIAL: ' num2str(N)]; + end + function explorePrevious(src, ~) + N = N - 1; + if N < 1; N = length(me.trials); end + clf(figA); clf(figB); + plotNH(me,N,figB); + plot(me,N,[],[],[],figA); + fig.b0.String = ['TRIAL: ' num2str(N)]; + end + function exploreClose(src, ~) + me.explore(true); + end + + end + + end%-------------------------END PUBLIC METHODS--------------------------------% + + %============================================================================== + methods (Access = protected) %----------------------------------PRIVATE METHODS + %============================================================================== + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function closeUI(me, varargin) + try delete(me.handles.parent); end %#ok + me.handles = struct(); + me.openUI = false; + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function makeUI(me, varargin) + disp('Feature not finished yet...') + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function updateUI(me, varargin) + + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function notifyUI(me, varargin) + + end + + % =================================================================== + %> @brief main parse loop for CSV events, has to be one big serial loop + %> + %> @param + %> @return + % =================================================================== + function parseEvents(me) + isTrial = false; + tri = 0; %current trial that is being parsed + me.correct.idx = []; + me.correct.saccTimes = []; + me.correct.fixations = []; + me.correct.timeRange = []; + me.breakFix = me.correct; + me.incorrect = me.correct; + me.unknown = me.correct; + me.trialList = []; + + tmain = tic; + + trialDef = getTrialDef(me); + + me.ppd; %faster to cache this now (dependant property sets ppd_ too) + + if ~isempty(me.markers) && ~isempty(me.raw) + + if matches(me.variableMessageName,'number') + me.trialStartMessageName = 0; + end + + FEVENTN = height(me.markers); + pb = textprogressbar(FEVENTN, 'startmsg', 'Parsing iRec Events: ',... + 'showactualnum', true,'updatestep', round(FEVENTN/(FEVENTN/20))); + + for ii = 1:FEVENTN + m = me.markers.data(ii); + if m == intmin('int32') + continue; + elseif m > me.trialStartMessageName + if isTrial == true + tri = tri - 1; + isTrial = false; + continue; + end + tri = tri + 1; + isTrial = true; + trial = trialDef; + trial.variable = m; + trial.idx = tri; + trial.correctedIndex = trial.idx; + trial.sttime = me.markers.time_cpu(ii); + trial.rtstarttime = trial.sttime; + trial.synctime = trial.sttime; + trial.startsampletime = trial.sttime + me.measureRange(1); + elseif m == me.SYNCTIME + if isTrial + trial.synctime = me.markers.time_cpu(ii); + end + elseif m == me.END_FIX + if isTrial + trial.endfix = me.markers.time_cpu(ii); + end + elseif m == me.END_RT + if isTrial + trial.rtendtime = me.markers.time_cpu(ii); + end + elseif m == me.END_EXP + break; + elseif m == me.trialEndMessage + trial.entime = me.markers.time_cpu(ii); + if isnan(trial.rtendtime);trial.rtendtime = trial.entime;end + if me.relativeMarkers == true + trial.endsampletime = trial.entime + me.measureRange(2); + else + if me.measureRange(2) <= 0 + trial.endsampletime = trial.entime; + else + trial.endsampletime = trial.synctime + me.measureRange(2); + end + end + idx = find(me.raw.time >= trial.startsampletime & me.raw.time <= trial.endsampletime); + trial.times = me.raw.time(idx) - trial.synctime; + trial.timeRange = [min(trial.times) max(trial.times)]; + trial.gx = me.raw.x(idx); + trial.gy = me.raw.y(idx); + trial.pa = me.raw.pupil(idx); + trial.pratio = me.raw.pratio(idx); + trial.blink = me.raw.blink(idx); + trial.deltaT = trial.entime - trial.sttime; + if isempty(trial.gx) + trial.invalid = true; + else + trial.correct = true; + end + if trial.endsampletime > trial.entime + %warning('Sample beyond end marker on trial %i',tri); + end + if tri == 1 + me.trials = trial; + else + me.trials(tri) = trial; + end + isTrial = false; + end + pb(ii); + end + pb(ii); + + if isempty(me.trials) + warning('---> iRecAnalysis.parseEvents: No trials could be parsed in this data!') + return + end + + %prune the end trial if invalid + me.correct.idx = find([me.trials.correct] == true); + me.breakFix.idx = find([me.trials.breakFix] == true); + me.incorrect.idx = find([me.trials.incorrect] == true); + + % time range for correct trials + tr = [me.trials(me.correct.idx).timeRange]; + tr = reshape(tr,[2,length(tr)/2])'; + me.correct.timeRange = tr; + me.plotRange = [min(tr(:,1)) max(tr(:,2))]; + me.isParsed = true; + + fprintf(':#: Parsing CSV Events into %i Trials took %.2f secs\n',length(me.trials),toc(tmain)); + + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function parseAsVars(me) + if isempty(me.trials) + warning('---> eyelinkAnalysis.parseAsVars: No trials and therefore cannot extract variables!') + return + end + uniqueVars = sort(unique([me.trials.variable])); + nVars = length(uniqueVars); + me.vars = struct(); + me.vars(nVars).name = ''; + me.vars(nVars).var = []; + me.vars(nVars).varidx = []; + me.vars(nVars).variable = []; + me.vars(nVars).idx = []; + me.vars(nVars).correctedidx = []; + me.vars(nVars).correct = []; + me.vars(nVars).trial = []; + me.vars(nVars).sTime = []; + me.vars(nVars).sT = []; + me.vars(nVars).uuid = {}; + + labels = []; + try labels = me.exp.rE.task.varLabels; end + + for i = 1:length(me.trials) + trial = me.trials(i); + var = trial.variable; + if trial.invalid == true + continue + end + idx = find(uniqueVars==var); + if ~isempty(labels) && idx <= length(labels) + me.vars(idx).name = labels{idx}; + else + me.vars(idx).name = num2str(var); + end + me.vars(idx).var = var; + me.vars(idx).varidx = [me.vars(idx).varidx idx]; + me.vars(idx).variable = [me.vars(idx).variable var]; + me.vars(idx).idx = [me.vars(idx).idx i]; + me.vars(idx).correct = [me.vars(idx).correct trial.correct]; + if trial.correct > 0 + me.vars(idx).idxcorrect = [me.vars(idx).idxcorrect i]; + end + me.vars(idx).result = [me.vars(idx).result trial.result]; + me.vars(idx).correctedidx = [me.vars(idx).correctedidx i]; + %me.vars(idx).trial = [me.vars(idx).trial; trial]; + me.vars(idx).uuid{end+1} = trial.uuid; + if ~isempty(trial.saccadeTimes) + me.vars(idx).sTime = [me.vars(idx).sTime trial.saccadeTimes(1)]; + else + me.vars(idx).sTime = [me.vars(idx).sTime NaN]; + end + me.vars(idx).sT = [me.vars(idx).sT trial.firstSaccade]; + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function parseSecondaryEyePos(me) + if me.isParsed && isstruct(me.tS) && ~isempty(me.tS) + f=fieldnames(me.tS.eyePos); %get fieldnames + re = regexp(f,'^CC','once'); %regexp over the cell + idx = cellfun(@(c)~isempty(c),re); %check which regexp returned true + f = f(idx); %use this index + me.validation(1).uuids = f; + me.validation.lengthCorrect = length(f); + if length(me.correct.idx) == me.validation.lengthCorrect + disp('Secondary Eye Position Data appears consistent with CSV parsed trials') + else + warning('Secondary Eye Position Data inconsistent with CSV parsed trials') + end + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function parseFixationPositions(me) + if me.isParsed + for i = 1:length(me.trials) + t = me.trials(i); + f(1).isFix = false; + f(1).idx = -1; + f(1).times = -1; + f(1).x = -1; + f(1).y = -1; + if isfield(t.fixations,'time') + times = [t.fixations.time]; + fi = find(times > 50); + if ~isempty(fi) + f(1).isFix = true; + f(1).idx = i; + for jj = 1:length(fi) + fx = t.fixations(fi(jj)); + f(1).times(jj) = fx.time; + f(1).x(jj) = fx.x; + f(1).y(jj) = fx.y; + end + end + end + if t.correct == true + bname='correct'; + elseif t.breakFix == true + bname='breakFix'; + else + bname='incorrect'; + end + if isempty(me.(bname).fixations) + me.(bname).fixations = f; + else + me.(bname).fixations(end+1) = f; + end + end + + end + + end + + % =================================================================== + %> @brief + %> + % =================================================================== + function [outx, outy] = toDegrees(me,in) + if length(in)==2 + outx = (in(1) - me.xCenter) / me.ppd_; + outy = (in(2) - me.yCenter) / me.ppd_; + else + outx = []; + outy = []; + end + end + + % =================================================================== + %> @brief + %> + % =================================================================== + function [outx, outy] = toPixels(me,in) + if length(in)==2 + outx = (in(1) * me.ppd_) + me.xCenter; + outy = (in(2) * me.ppd_) + me.yCenter; + else + outx = []; + outy = []; + end + end + + % =================================================================== + %> @brief + %> + % =================================================================== + function computeFullSaccades(me) + assert(exist('runNH2010Classification.m'),'Please add NystromHolmqvist2010 to path!'); + % load parameters for event classifier + if isempty(me.ETparams) + me.ETparams = defaultParameters; + % settings for code specific to Niehorster, Siu & Li (2015) + me.ETparams.extraCut = [0 0]; % extra ms of data to cut before and after saccade. + me.ETparams.qInterpMissingPos = true; % interpolate using straight lines to replace missing position signals? + + % settings for the saccade cutting (see cutSaccades.m for documentation) + me.ETparams.cutPosTraceMode = 1; + me.ETparams.cutVelTraceMode = 1; + me.ETparams.cutSaccadeSkipWindow= 1; % don't cut during first x seconds + me.ETparams.screen.resolution = [ me.resolution(1) me.resolution(2) ]; + me.ETparams.screen.size = [ me.resolution(1)/me.pixelsPerCm/100 me.resolution(2)/me.pixelsPerCm/100 ]; + me.ETparams.screen.viewingDist = me.distance/100; + me.ETparams.screen.dataCenter = [ me.resolution(1)/2 me.resolution(2)/2 ]; % center of screen has these coordinates in data + me.ETparams.screen.subjectStraightAhead = [ me.resolution(1)/2 me.resolution(2)/2 ]; % Specify the screen coordinate that is straight ahead of the subject. Just specify the middle of the screen unless its important to you to get this very accurate! + % change some defaults as needed for this analysis: + me.ETparams.data.alsoStoreComponentDerivs = true; + me.ETparams.data.detrendWithMedianFilter = true; + me.ETparams.data.applySaccadeTemplate = true; + me.ETparams.data.minDur = 100; + me.ETparams.fixation.doClassify = true; + me.ETparams.blink.replaceWithInterp = true; + me.ETparams.blink.replaceVelWithNan = true; + end + me.ETparams.samplingFreq = me.sampleRate; + + % process params + ETparams = prepareParameters(me.ETparams); + + for ii = 1:length(me.trials) + fprintf('--->>> Full saccadic analysis of Trial %i:\n',ii); + x = (me.trials(ii).gx * me.ppd) + ETparams.screen.dataCenter(1); + y = (me.trials(ii).gy * me.ppd) + ETparams.screen.dataCenter(2); + p = me.trials(ii).pa; + t = me.trials(ii).times * 1e3; + + if length(x) < me.ETparams.data.minDur + data = struct([]); + else + data = runNH2010Classification(x,y,p,ETparams,t); + % replace missing data by linearly interpolating position and velocity + % between start and end of each missing interval (so, creating a ramp + % between start and end position/velocity). + data = replaceMissing(data,ETparams.qInterpMissingPos); + end + + % desaccade velocity and/or position + %data = cutSaccades(data,ETparams,cutPosTraceMode,cutVelTraceMode,extraCut,cutSaccadeSkipWindow); + % construct saccade only traces + %data = cutPursuit(data,ETparams,1); + + me.trials(ii).data = data; + + end + + end + + % =================================================================== + %> @brief + %> + % =================================================================== + function computeMicrosaccades(me) + VFAC=me.VFAC; + MINDUR=me.MINDUR; + sampleRate = me.sampleRate; + pb = textprogressbar(length(me.trials),'startmsg','Loading trials to compute microsaccades: ','showactualnum',true); + cms = tic; + for jj = 1:length(me.trials) + if me.trials(jj).invalid == true || me.trials(jj).unknown == true; continue; end + samples = []; sac = []; radius = []; monol=[]; monor=[]; + me.trials(jj).msacc = struct(); + me.trials(jj).sampleSaccades = []; + me.trials(jj).microSaccades = []; + samples(:,1) = me.trials(jj).times; + samples(:,2) = me.trials(jj).gx; + samples(:,3) = me.trials(jj).gy; + samples(:,4) = nan(size(samples(:,1))); + samples(:,5) = samples(:,4); + eye_used = 0; + try + switch eye_used + case 0 + v = vecvel(samples(:,2:3),sampleRate,2); + [sac, radius] = microsacc(samples(:,2:3),v,VFAC,MINDUR); + case 1 + v = vecvel(samples(:,2:3),sampleRate,2); + [sac, radius] = microsacc(samples(:,4:5),v,VFAC,MINDUR); + case 2 + MSlcoords=samples(:,2:3); + MSrcoords=samples(:,4:5); + vl = vecvel(MSlcoords,sampleRate,2); + vr = vecvel(MSrcoords,sampleRate,2); + [sacl, radiusl] = microsacc(MSlcoords,vl,VFAC,MINDUR); + [sacr, radiusr] = microsacc(MSrcoords,vr,VFAC,MINDUR); + [bsac, monol, monor] = binsacc(sacl,sacr); + sac = saccpar(bsac); + end + me.trials(jj).radius = radius; + for ii = 1:size(sac,1) + me.trials(jj).msacc(ii).n = round(sac(ii,1)); + me.trials(jj).msacc(ii).time = samples(sac(ii,1),1); + me.trials(jj).msacc(ii).endtime = samples(sac(ii,2),1); + me.trials(jj).msacc(ii).velocity = sac(ii,3); + me.trials(jj).msacc(ii).dx = sac(ii,4); + me.trials(jj).msacc(ii).dy = sac(ii,5); + me.trials(jj).msacc(ii).dX = sac(ii,6); + me.trials(jj).msacc(ii).dY = sac(ii,7); + [theta,rho]=cart2pol(sac(ii,6),sac(ii,7)); + me.trials(jj).msacc(ii).theta = me.rad2ang(theta); + me.trials(jj).msacc(ii).rho = rho; + me.trials(jj).msacc(ii).isMicroSaccade = rho<=me.minSaccadeDistance; + end + if ~isempty(sac) + me.trials(jj).sampleSaccades = [me.trials(jj).msacc(:).time]; + me.trials(jj).microSaccades = [me.trials(jj).sampleSaccades([me.trials(jj).msacc(:).isMicroSaccade])]; + else + me.trials(jj).sampleSaccades = NaN; + me.trials(jj).microSaccades = NaN; + + end + if isempty(me.trials(jj).microSaccades); me.trials(jj).microSaccades = NaN; end + catch ME + getReport(ME) + end + pb(jj) + end + fprintf(':#: Parsing MicroSaccades took %g secs\n', round(toc(cms))) + + function v = vecvel(xx,SAMPLING,TYPE) + %------------------------------------------------------------ + % FUNCTION vecvel.m + % Calculation of eye velocity from position data + % + % INPUT: + % xy(1:N,1:2) raw data, x- and y-components of the time series + % SAMPLING sampling rate (number of samples per second) + % TYPE velocity type: TYPE=2 recommended + % + % OUTPUT: + % v(1:N,1:2) velocity, x- and y-components + %------------------------------------------------------------- + + N = length(xx); % length of the time series + v = zeros(N,2); + + switch TYPE + case 1 + v(2:N-1,:) = [xx(3:end,:) - xx(1:end-2,:)]*SAMPLING/2; + case 2 + v(3:N-2,:) = [xx(5:end,:) + xx(4:end-1,:) - xx(2:end-3,:) - xx(1:end-4,:)]*SAMPLING/6; + v(2,:) = [xx(3,:) - xx(1,:)]*SAMPLING/2; + v(N-1,:) = [xx(end,:) - xx(end-2,:)]*SAMPLING/2; + end + return + end + + function [sac, radius] = microsacc(x,vel,VFAC,MINDUR) + %------------------------------------------------------------------- + % FUNCTION microsacc.m + % Detection of monocular candidates for microsaccades; + % + % INPUT: + % x(:,1:2) position vector + % vel(:,1:2) velocity vector + % VFAC relative velocity threshold + % MINDUR minimal saccade duration + % + % OUTPUT: + % radius threshold velocity (x,y) used to distinguish microsaccs + % sac(1:num,1) onset of saccade + % sac(1:num,2) end of saccade + % sac(1:num,3) peak velocity of saccade (vpeak) + % sac(1:num,4) horizontal component (dx) + % sac(1:num,5) vertical component (dy) + % sac(1:num,6) horizontal amplitude (dX) + % sac(1:num,7) vertical amplitude (dY) + %--------------------------------------------------------------------- + % SDS... VFAC (relative velocity threshold) E&M 2006 use a value of VFAC=5 + + % compute threshold + % SDS... this is sqrt[median(x^2) - (median x)^2] + msdx = sqrt( median(vel(:,1).^2,'omitnan') - (median(vel(:,1),'omitnan'))^2 ); + msdy = sqrt( median(vel(:,2).^2,'omitnan') - (median(vel(:,2),'omitnan'))^2 ); + if msdx1); + + % determine saccades + % SDS.. this loop reads through the index of above-threshold velocities, + % storing the beginning and end of each period (i.e. each saccade) + % as the position in the overall time series of data submitted + % to the analysis + N = length(indx); + sac = []; + nsac = 0; + dur = 1; + a = 1; + k = 1; + while k=MINDUR + nsac = nsac + 1; + b = k; % hence b is the last instant of the consecutive series constituting a microsaccade + sac(nsac,:) = [indx(a) indx(b)]; + end + a = k+1; + dur = 1; + end + k = k + 1; + end + + % check for minimum duration + % SDS.. this just deals with the final set of above threshold + % velocities; adds it to the list if the duration is long enough + if dur>=MINDUR + nsac = nsac + 1; + b = k; + sac(nsac,:) = [indx(a) indx(b)]; + end + + % compute peak velocity, horizonal and vertical components + for s=1:nsac + % onset and offset + a = sac(s,1); + b = sac(s,2); + % saccade peak velocity (vpeak) + vpeak = max( sqrt( vel(a:b,1).^2 + vel(a:b,2).^2 ) ); + sac(s,3) = vpeak; + % saccade vector (dx,dy) SDS.. this is the difference between initial and final positions + dx = x(b,1)-x(a,1); + dy = x(b,2)-x(a,2); + sac(s,4) = dx; + sac(s,5) = dy; + + % saccade amplitude (dX,dY) SDS.. this is the difference between max and min positions over the excursion of the msac + i = sac(s,1):sac(s,2); + [minx, ix1] = min(x(i,1)); % dX > 0 signifies rightward (if ix2 > ix1) + [maxx, ix2] = max(x(i,1)); % dX < 0 signifies leftward (if ix2 < ix1) + [miny, iy1] = min(x(i,2)); % dY > 0 signifies upward (if iy2 > iy1) + [maxy, iy2] = max(x(i,2)); % dY < 0 signifies downward (if iy2 < iy1) + dX = sign(ix2-ix1)*(maxx-minx); + dY = sign(iy2-iy1)*(maxy-miny); + sac(s,6:7) = [dX dY]; + end + + end + + function [sac, monol, monor] = binsacc(sacl,sacr) + %------------------------------------------------------------------- + % FUNCTION binsacc.m + % + % INPUT: saccade matrices from FUNCTION microsacc.m + % sacl(:,1:7) microsaccades detected from left eye + % sacr(:,1:7) microsaccades detected from right eye + % + % OUTPUT: + % sac(:,1:14) binocular microsaccades (right eye/left eye) + % monol(:,1:7) monocular microsaccades of the left eye + % monor(:,1:7) monocular microsaccades of the right eye + %--------------------------------------------------------------------- + % SDS.. The aim of this routine is to pair up msaccs in L & R eyes that are + % coincident in time. Some msaccs in one eye may not have a matching + % msacc in the other; the code also seems to allow for a msacc in one + % eye matching 2 events in the other eye - in which case the + % larger amplitude one is selected, and the other discarded. + + if size(sacr,1)*size(sacl,1)>0 + + % determine saccade clusters + TR = max(sacr(:,2)); + TL = max(sacl(:,2)); + T = max([TL TR]); + s = zeros(1,T+1); + for i=1:size(sacl,1) + s(sacl(i,1)+1:sacl(i,2)) = 1; % SDS.. creates time-series with 1 for duration of left eye msacc events and 0 for duration of intervals + % NB. sacl(i,1)+1 the +1 is necessary for the diff function [line 219] to correctly pick out the start instants of msaccs + end + for i=1:size(sacr,1) + s(sacr(i,1)+1:sacr(i,2)) = 1; % SDS.. superimposes similar for right eye; hence a time-series of binocular events + end % ... 'binocular' means that either L, R or both eyes moved + s(1) = 0; + s(end) = 0; + m = find(diff(s~=0)); % SDS.. finds time series positions of start and ends of (binocular) m.saccd phases + N = length(m)/2; % SDS.. N = number of microsaccades + m = reshape(m,2,N)'; % SDS.. col 1 is all sacc onsets; col 2 is all sacc end points + + % determine binocular saccades + NB = 0; + NR = 0; + NL = 0; + sac = []; + monol = []; + monor = []; + % SDS.. the loop counts through each position in the binoc list + for i=1:N % .. 'find' operates on the sacl (& sacr) matrices; + l = find( m(i,1)<=sacl(:,1) & sacl(:,2)<=m(i,2) ); % .. finds position of msacc in L eye list to match the timing of each msacc in binoc list (as represented by 'm') + r = find( m(i,1)<=sacr(:,1) & sacr(:,2)<=m(i,2) ); % .. finds position of msacc in R eye list ... + % .. N.B. some 'binoc' msaccs will not match one or other of the monoc lists + if length(l)*length(r)>0 % SDS.. Selects binoc msaccs. [use of 'length' function is a bit quaint.. l and r should not be vectors, but single values..?] + ampr = sqrt(sacr(r,6).^2+sacr(r,7).^2); % .. is allowing for 2 (or more) discrete monocular msaccs coinciding with a single event in the 'binoc' list + ampl = sqrt(sacl(l,6).^2+sacl(l,7).^2); + [h ir] = max(ampr); % hence r(ir) in L241 is the position in sacr of the larger amplitude saccade (if there are 2 or more that occurence of binoc saccade) + [h il] = max(ampl); % hence l(il) in L241 is the position in sacl of the larger amplitude saccade (if there are 2 or more that occurence of binoc saccade) + NB = NB + 1; + sac(NB,:) = [sacr(r(ir),:) sacl(l(il),:)]; % .. the final compilation selects the larger amplitude msacc to represent the msacc in that eye + else + % determine monocular saccades + if isempty(l) % If no msacc in L eye + NR = NR + 1; + monor(NR,:) = sacr(r,:); %.. record R eye monoc msacc. + end + if isempty(r) %If no msacc in R eye + NL = NL + 1; + monol(NL,:) = sacl(l,:); %.. record L eye monoc msacc + end + end + end + else + % special cases of exclusively monocular saccades + if size(sacr,1)==0 + sac = []; + monor = []; + monol = sacl; + end + if size(sacl,1)==0 + sac = []; + monol = []; + monor = sacr; + end + end + end + + function sac = saccpar(bsac) + %------------------------------------------------------------------- + % FUNCTION saccpar.m + % Calculation of binocular saccade parameters; + % + % INPUT: binocular saccade matrix from FUNCTION binsacc.m + % sac(:,1:14) binocular microsaccades + % + % OUTPUT: + % sac(:,1:9) parameters averaged over left and right eye data + %--------------------------------------------------------------------- + if size(bsac,1)>0 + sacr = bsac(:,1:7); + sacl = bsac(:,8:14); + + % 1. Onset + a = min([sacr(:,1)'; sacl(:,1)'])'; % produces single column vector of ealier onset in L v R eye msaccs + + % 2. Offset + b = max([sacr(:,2)'; sacl(:,2)'])'; % produces single column vector of later offset in L v R eye msaccs + + % 3. Duration + DR = sacr(:,2)-sacr(:,1)+1; + DL = sacl(:,2)-sacl(:,1)+1; + D = (DR+DL)/2; + + % 4. Delay between eyes + delay = sacr(:,1) - sacl(:,1); + + % 5. Peak velocity + vpeak = (sacr(:,3)+sacl(:,3))/2; + + % 6. Saccade distance + dist = (sqrt(sacr(:,4).^2+sacr(:,5).^2)+sqrt(sacl(:,4).^2+sacl(:,5).^2))/2; + angle1 = atan2((sacr(:,5)+sacl(:,5))/2,(sacr(:,4)+sacl(:,4))/2); + + % 7. Saccade amplitude + ampl = (sqrt(sacr(:,6).^2+sacr(:,7).^2)+sqrt(sacl(:,6).^2+sacl(:,7).^2))/2; + angle2 = atan2((sacr(:,7)+sacl(:,7))/2,(sacr(:,6)+sacl(:,6))/2); %SDS.. NB 'atan2'function operates on (y,x) - not (x,y)! + + sac = [a b D delay vpeak dist angle1 ampl angle2]; + else + sac = []; + end + end + + end + + % =================================================================== + %> @brief + %> + % =================================================================== + function trialDef = getTrialDef(me) + trialDef = cell2struct(repmat({[]},length(me.trialsTemplate),1),me.trialsTemplate); + trialDef.rt = false; + trialDef.rtoverride = false; + trialDef.firstSaccade = NaN; + trialDef.invalid = false; + trialDef.correct = false; + trialDef.breakFix = false; + trialDef.incorrect = false; + trialDef.unknown = false; + trialDef.sttime = NaN; + trialDef.entime = NaN; + trialDef.totaltime = 0; + trialDef.startsampletime = NaN; + trialDef.endsampletime = NaN; + trialDef.timeRange = [NaN NaN]; + trialDef.rtstarttime = NaN; + trialDef.rtstarttimeOLD = NaN; + trialDef.rtendtime = NaN; + trialDef.synctime = NaN; + trialDef.deltaT = NaN; + trialDef.rttime = NaN; + end + + end + +end + diff --git a/analysis/tobiiAnalysis.m b/analysis/tobiiAnalysis.m index c56ee82b484a80911d41d00bf7b1773ca6dc19b3..0664b0580338899606801a0685f85a4aa74ef23d 100644 --- a/analysis/tobiiAnalysis.m +++ b/analysis/tobiiAnalysis.m @@ -1,17 +1,17 @@ % ======================================================================== %> @brief eyelinkAnalysis offers a set of methods to load, parse & plot raw EDF files. It -%> understands opticka trials (where EDF messages TRIALID start a trial and TRIAL_RESULT +%> understands opticka trials (where messages TRIALID start a trial and TRIAL_RESULT %> ends a trial by default) so can parse eye data and plot it for trial groups. You %> can also manually find microsaccades, and perform ROI/TOI filtering on the eye %> movements. %> -%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md +%> Copyright ©2014-2024 Ian Max Andolina — released: LGPL3, see LICENCE.md % ======================================================================== classdef tobiiAnalysis < analysisCore - % eyelinkAnalysis offers a set of methods to load, parse & plot raw EDF files. + % eyelinkAnalysis offers a set of methods to load, parse & plot raw tobii files. properties %> file name - fileName char = '' + fileName char = '' %> which EDF message contains the trial start tag trialStartMessageName char = 'TRIALID' %> which EDF message contains the variable name or value @@ -25,7 +25,7 @@ classdef tobiiAnalysis < analysisCore %> as correct, incorrect, breakfix etc. trialEndMessage char = 'TRIAL_RESULT' %> override the rtStart time with a custom message? - rtOverrideMessage char = 'SYNCSTROBE' + rtOverrideMessage char = 'SYNCTIME' %> minimum saccade distance in degrees minSaccadeDistance double = 0.99 %> relative velocity threshold @@ -46,6 +46,17 @@ classdef tobiiAnalysis < analysisCore pixelsPerCm double = 32 %> screen distance distance double = 57.3 + %> screen resolution + resolution = [ 1920 1080 ] + %> For Dee's analysis edit these settings + ETparams = [ ] + %> Is measure range relative to start and end markers or absolute + %> to start marker? + relativeMarkers = false + %> subtract the baseline for the pupil plot and average? + baselinePupil = true + %> smooth the pupil signal for plot and average? + smoothPupil = true end properties (Hidden = true) @@ -58,23 +69,25 @@ classdef tobiiAnalysis < analysisCore %> these are used for spikes spike saccade time correlations rtLimits double rtDivision double - %> trial list from the saved behavioural data, used to fix trial name bug in old files - trialOverride struct %> screen X center in pixels xCenter double = 640 %> screen Y center in pixels yCenter double = 512 - %>57.3 bug override - override573 = false + %> downsample the data for plotting + downSample logical = true + excludeTrials = [] end properties (SetAccess = private, GetAccess = public) + xmod = 25 + ymod = 15 %> have we parsed the MAT yet? isParsed logical = false %> sample rate sampleRate double = 250 %> raw data raw struct + exp %> inidividual trials trials struct %> eye data parsed into invdividual variables @@ -84,7 +97,7 @@ classdef tobiiAnalysis < analysisCore %> correct trials indices correct struct = struct() %> breakfix trials indices - breakFix struct = struct() + breakFix struct = struct() %> incorrect trials indices incorrect struct = struct() %> unknown trials indices @@ -114,13 +127,23 @@ classdef tobiiAnalysis < analysisCore end properties (SetAccess = private, GetAccess = private) + varLabels %> pixels per degree calculated from pixelsPerCm and distance (cache) ppd_ %> allowed properties passed to object upon construction - allowedProperties char = ['correctValue|incorrectValue|breakFixValue|'... - 'trialStartMessageName|variableMessageName|trialEndMessage|file|dir|'... - 'verbose|pixelsPerCm|distance|xCenter|yCenter|rtStartMessage|minSaccadeDistance|'... - 'rtEndMessage|trialOverride|rtDivision|rtLimits|tS|ROI|TOI|VFAC|MINDUR'] + allowedProperties = {'correctValue', 'incorrectValue', 'breakFixValue', ... + 'trialStartMessageName', 'variableMessageName', 'trialEndMessage', 'file', 'dir', ... + 'verbose', 'pixelsPerCm', 'distance', 'xCenter', 'yCenter', 'rtStartMessage', 'minSaccadeDistance', ... + 'rtEndMessage', 'trialOverride', 'rtDivision', 'rtLimits', 'tS', 'ROI', 'TOI', 'VFAC', 'MINDUR',... + 'baselineWindow','measureRange','plotRange'} + trialsTemplate = {'variable','variableMessageName','idx','correctedIndex','time',... + 'times','gx','gy','hx','hy','pa','valid','validRatio',... + 'rt','rtoverride','fixations','nfix','saccades','nsacc','saccadeTimes',... + 'firstSaccade','uuid','result','invalid','correct','breakFix','incorrect','unknown',... + 'messages','sttime','entime','totaltime','startsampletime','endsampletime',... + 'timeRange','rtstarttime','rtstarttimeOLD','rtendtime','synctime','deltaT',... + 'rttime','msacc','sampleSaccades',... + 'microSaccades','radius','forcedend'} end methods @@ -129,17 +152,16 @@ classdef tobiiAnalysis < analysisCore %> % =================================================================== function me = tobiiAnalysis(varargin) - if nargin == 0; varargin.name = ''; end - me=me@analysisCore(varargin); %superclass constructor - if all(me.measureRange == [0.1 0.2]) %use a different default to superclass - me.measureRange = [-0.4 1]; - end - if nargin>0; me.parseArgs(varargin, me.allowedProperties); end - if isempty(me.file) || isempty(me.dir) - [f,d] = uigetfile('*.mat','Load MAT File:'); - me.fileName = [d filesep f]; + args = optickaCore.addDefaults(varargin,struct('name','tobiiAnal',... + 'measureRange',[-0.4 1],'plotRange',[-0.5 1],... + 'baselineWindow',[])); + me=me@analysisCore(args); %superclass constructor + me.parseArgs(args, me.allowedProperties); + + if isempty(me.fileName) + [f,d] = uigetfile('*.mat','Select Opticka Data MAT File:'); + if ~isnumeric(f); me.fileName = [d f]; end end - me.ppd; %cache our initial ppd_ end % =================================================================== @@ -151,18 +173,67 @@ classdef tobiiAnalysis < analysisCore function load(me,force) if ~exist('force','var');force=false;end if isempty(me.fileName) - warning('No EDF file specified...'); - return + [f,d] = uigetfile('*.mat','Select Opticka Data MAT File:'); + if ~isnumeric(f) + me.fileName = [d filesep f]; + else + warning('No Data file specified...'); + return + end end - tic + tt=tic; if isempty(me.raw) || force == true oldpath = pwd; [p, f, e] = fileparts(me.fileName); cd(p) - me.raw = load(me.fileName); + exp = load(me.fileName); + if isstruct(exp) && isfield(exp,'rE') && isa(exp.rE,'runExperiment')% runExperiment data + fprintf('... runExperiment (Opticka file) found\n'); + me.distance = exp.rE.screen.distance; + me.pixelsPerCm = exp.rE.screen.pixelsPerCm; + me.resolution = [exp.rE.screen.screenVals.width exp.rE.screen.screenVals.height]; + me.xmod = (exp.rE.screen.screenVals.width) / exp.rE.screen.ppd; + me.ymod = (exp.rE.screen.screenVals.height) / exp.rE.screen.ppd; + me.sampleRate = exp.rE.eyeTracker.sampleRate; + fprintf('Screen = distance: %.2f ppc: %.2f W: %i [%.2f] H: %i [%.2f], SampleRate: %i \n',... + me.distance,me.pixelsPerCm,me.resolution(1),me.xmod,me.resolution(2),me.ymod,me.sampleRate); + if isa(exp.rE.eyeTracker,'tobiiManager') + me.raw = exp.rE.eyeTracker.data; + me.exp = exp; + else + warning('This is not a Tobii eyetracker file, choose another file!!!'); + me.raw = []; + me.exp = []; + return; + end + else + warning('This is not an Opticka:runExperiment file, choose another file!!!'); + disp(exp); + if isfield(exp,'tobii'); disp(exp.tobii); end + me.raw = []; + me.exp = []; + return; + end cd(oldpath) end - fprintf(':#: Loading Raw MAT Data took %g ms\n',round(toc*1000)); + if ~isempty(me.exp) && isfield(me.exp,'tS') && isfield(me.exp,'rE') + try + me.name = [me.exp.tS.name '-' me.exp.rE.name]; + me.comment = me.exp.rE.comment; + fprintf('===>>> Experiment Details: name: %s | comment %s\n',... + me.name,me.comment); + end + fprintf('===>>> Eytracker Data %s containing %i messages and %i samples\n',... + me.exp.rE.eyeTracker.fullName,... + size(me.raw.messages,1),... + length(me.raw.data.gaze.deviceTimeStamp)); + if ~isempty(me.exp.rE.task.varLabels) + me.varLabels = me.exp.rE.task.varLabels; + fprintf('===>>> Variables contained in the task:\n'); + disp(me.varLabels); + end + end + fprintf(':#: Loading Raw MAT Data took %g ms\n',round(toc(tt)*1e3)); end % =================================================================== @@ -176,6 +247,11 @@ classdef tobiiAnalysis < analysisCore me.isParsed = false; tmain = tic; parseEvents(me); + if isempty(me.trials) || ~isfield(me.trials,'gx') + warning('Event parsing returned no trials! Check raw data...'); + fprintf('\tTook %g ms\n',round(toc(tmain)*1000)); + return + end parseAsVars(me); me.isParsed = true; fprintf('\tOverall Simple Parsing of EDF Trials took %g ms\n',round(toc(tmain)*1000)); @@ -192,6 +268,11 @@ classdef tobiiAnalysis < analysisCore me.isParsed = false; tmain = tic; parseEvents(me); + if isempty(me.trials) || ~isfield(me.trials,'gx') + warning('Event parsing returned no trials! Check obj.raw.messages for all Tobii messages!...'); + fprintf('Took %g ms\n',round(toc(tmain)*1000)); + return + end if ~isempty(me.trialsToPrune) me.pruneTrials(me.trialsToPrune); end @@ -214,6 +295,7 @@ classdef tobiiAnalysis < analysisCore parseROI(me); parseTOI(me); computeMicrosaccades(me); + computeFullSaccades(me); end % =================================================================== @@ -285,6 +367,44 @@ classdef tobiiAnalysis < analysisCore fprintf('Pruned %i trials from EDF trial data \n',num) end + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function data = saveChap(me) + if ~me.isParsed; warning('You need to parse data first...');return;end + t = double(me.raw.data.gaze.systemTimeStamp) / 1e3; + data.timestamps = t'; + data.pupil_size = ((me.raw.data.gaze.left.pupil.diameter+me.raw.data.gaze.right.pupil.diameter)/2)'; + data.pupil_x = me.raw.data.gaze.left.gazePoint.onDisplayArea(1,:)'; + data.pupil_y = me.raw.data.gaze.left.gazePoint.onDisplayArea(2,:)'; + data.rate = me.sampleRate; + data.name = me.fileName; + data.file_name = data.name; + tdata = {}; + vdata = {}; + for i = 1:length(me.trials) + tdata{i,1} = me.trials(i).idx; + tdata{i,2} = analysisCore.findNearest(t, me.trials(i).sttime); + tdata{i,3} = analysisCore.findNearest(t, me.trials(i).entime); + tdata{i,4} = tdata{i,3} - tdata{i,2}; + + vdata{i,1} = ['VAR' num2str(me.trials(i).variable)]; + vdata{i,2} = ['' me.trials(i).variableMessageName]; + if me.trials(i).correct; ctxt = 'correct'; elseif me.trials(i).breakFix; ctxt='breakfix';else; ctxt='incorrect';end + vdata{i,3} = ['' ctxt]; + vdata{i,4} = analysisCore.findNearest(t, me.trials(i).rtstarttime) - tdata{i,2}; + vdata{i,5} = tdata{i,4}; + end + data.trial_data = cell2table(tdata,'VariableNames',{'trial_names','Trial_Onset_num','Trial_Offset_num','trial_length'}); + data.total_var_data_table = cell2table(vdata,'VariableNames',{'VAR','NAME','CORRECT','event_stimulus_onset','event_Trial_Offset'}); + data.event_data = []; + data.events2 = []; + data.vars2 = []; + end + % =================================================================== %> @brief give a list of trials and it will plot both the raw eye position and the %> events @@ -292,20 +412,35 @@ classdef tobiiAnalysis < analysisCore %> @param %> @return % =================================================================== - function plot(me,select,type,seperateVars,name) + function plot(me,select,type,seperateVars,name,handle) % plot(me,select,type,seperateVars,name) + if ~me.isParsed; warning('You need to parse data first...');return;end if ~exist('select','var') || ~isnumeric(select); select = []; end - if ~exist('type','var') || isempty(type); type = 'correct'; end + if ~exist('type','var') || isempty(type); type = 'all'; end if ~exist('seperateVars','var') || ~islogical(seperateVars); seperateVars = false; end if ~exist('name','var') || isempty(name) if isnumeric(select) if length(select) > 1 - name = [me.file ' | Select: ' num2str(length(select)) ' trials']; + name = [me.fileName ' | Select: ' num2str(length(select)) ' trials']; + elseif isempty(select) + name = [me.fileName ' | Select: default']; else - name = [me.file ' | Select: ' num2str(select)]; + name = [me.fileName ' | Select: ' num2str(select)]; + ii = find(cellfun(@(x)ismember(select,x),{me.vars.idx}),1); + if ~isempty(ii) && ii > 0 + name = [name '-' me.vars(ii).name]; + end end end end + if seperateVars == true + for j = 1:length(me.vars) + me.plot(me.vars(j).idx,type,false,me.vars(j).name); + drawnow; + end + return + end + if ~exist('handle','var'); handle = []; end if isnumeric(select) && ~isempty(select) idx = select; type = ''; @@ -324,30 +459,24 @@ classdef tobiiAnalysis < analysisCore idxInternal = true; end if isempty(idx) - fprintf('No trials were selected to plot...\n') - return + fprintf('No trials were selected to plot, adding all...\n'); + idx = 1:length(me.trials); end - if seperateVars == true && isempty(select) - vars = unique([me.trials(idx).id]); - for j = vars - me.plot(j,type,false); - drawnow; - end - return + + vars = sort(unique([me.trials.variable])); + + if isempty(handle) + h1=figure('Name',name,'Color',[1 1 1],'NumberTitle','off',... + 'PaperPositionMode','auto','Papertype','a4',... + 'PaperUnits','centimeters',... + 'PaperOrientation','portrait',... + 'Tag','opticka'); + figpos(1,[0.6 0.9],1,'%'); + else + figure(handle) + h1=handle; end - h1=figure; - set(gcf,'Color',[1 1 1],'Name',name); - figpos(1,[1200 1200]); - p = panel(h1); - p.fontsize = 12; - p.margin = [10 10 10 20]; %left bottom right top - p.pack('v',{2/3, []}); - q = p(1); - q.margin = [20 20 10 25]; %left bottom right top - q.pack(2,2); - qq = p(2); - qq.margin = [20 20 10 25]; %left bottom right top - qq.pack(1,2); + tl = tiledlayout(h1,3,2,'TileSpacing','tight','Padding','tight'); a = 1; stdex = []; meanx = []; @@ -362,17 +491,16 @@ classdef tobiiAnalysis < analysisCore early = 0; mS = []; - map = me.optimalColours(length(me.vars)); - for i = 1:length(me.vars) - varidx(i) = str2num(me.vars(i).name); - end + map = me.optimalColours(length(vars)); if isempty(select) - thisVarName = 'ALL VARS'; + thisVarName = 'ALL'; elseif length(select) > 1 thisVarName = 'SELECTION'; else thisVarName = ['VAR' num2str(select)]; + ii = find(cellfun(@(x)ismember(select,x),{me.vars.idx}),1); + thisVarName = [thisVarName '-' me.vars(ii).name]; end maxv = 1; @@ -380,10 +508,11 @@ classdef tobiiAnalysis < analysisCore if ~isempty(me.TOI) t1 = me.TOI(1); t2 = me.TOI(2); else - t1 = 0; t2 = 0.1; + t1 = me.baselineWindow(1); t2 = me.baselineWindow(2); end for i = idx + if ~exist('didplot','var'); didplot = false; end if idxInternal == true %we're using the eyelink index which includes incorrects f = i; elseif me.excludeIncorrect %we're using an external index which excludes incorrects @@ -394,9 +523,14 @@ classdef tobiiAnalysis < analysisCore if isempty(f); continue; end thisTrial = me.trials(f(1)); - tidx = find(varidx==thisTrial.variable); + + if isfield(thisTrial, 'invalid') && thisTrial.invalid + continue; + end - if thisTrial.variable == 1010 || isempty(me.vars) %early edf files were broken, 1010 signifies this + tidx = find(vars==thisTrial.variable); + + if thisTrial.variable == 1010 || isempty(me.vars) %early CSV files were broken, 1010 signifies this c = rand(1,3); else c = map(tidx,:); @@ -409,16 +543,20 @@ classdef tobiiAnalysis < analysisCore end t = thisTrial.times / 1e3; %convert to seconds - ix = find((t >= me.measureRange(1)) & (t <= me.measureRange(2))); - ip = find((t >= me.plotRange(1)) & (t <= me.plotRange(2))); + ix = (t >= me.measureRange(1)) & (t <= me.measureRange(2)); + ip = (t >= me.plotRange(1)) & (t <= me.plotRange(2)); + ib = []; + if length(me.baselineWindow)==2 + ib = (t >= me.baselineWindow(1)) & (t <= me.baselineWindow(2)); + end tm = t(ix); tp = t(ip); - xa = thisTrial.gx / me.ppd_; - ya = thisTrial.gy / me.ppd_; - lim = 50; %max degrees in data - xa(xa < -lim) = -lim; xa(xa > lim) = lim; - ya(ya < -lim) = -lim; ya(ya > lim) = lim; + xa = thisTrial.gx; + ya = thisTrial.gy; pupilAll = thisTrial.pa; + lim = 50; %max degrees in data + xa(xa < -lim) = NaN; xa(xa > lim) = NaN; + ya(ya < -lim) = NaN; ya(ya > lim) = NaN; x = xa(ix); y = ya(ix); @@ -427,51 +565,45 @@ classdef tobiiAnalysis < analysisCore xp = xa(ip); yp = ya(ip); pupilPlot = pupilAll(ip); + if ~isempty(ib) && me.baselinePupil + pb = mean(pupilAll(ib),'omitnan'); + if isnumeric(pb) + pupilPlot = pupilPlot - pb; + end + end + + if me.smoothPupil + pupilPlot = smooth(pupilPlot); + end - q(1,1).select(); - q(1,1).hold('on') + nexttile(1); plot(xp, yp,'k-','Color',c,'LineWidth',1,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); if isfield(thisTrial,'microSaccades') & ~isnan(thisTrial.microSaccades) & ~isempty(thisTrial.microSaccades) + hold on for jj = 1: length(thisTrial.microSaccades) if thisTrial.microSaccades(jj) >= me.plotRange(1) && thisTrial.microSaccades(jj) <= me.plotRange(2) midx = me.findNearest(tp,thisTrial.microSaccades(jj)); - plot(xp(midx),yp(midx),'ko','Color',c,'MarkerSize',6,'MarkerEdgeColor',[0 0 0],... + plot(xp(midx),yp(midx),'^','Color',c,'MarkerSize',6,'MarkerEdgeColor',[1 1 0],... 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable thisTrial.microSaccades(jj)],'ButtonDownFcn', @clickMe); end end end - q(1,2).select(); - q(1,2).hold('on'); - plot(tp,abs(xp),'k-','Color',c,'MarkerSize',3,'MarkerEdgeColor',c,... + nexttile(2); + hold on + plot(tp,abs(xp),'-','Color',c,'MarkerSize',3,'MarkerEdgeColor',c,... 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); - plot(tp,abs(yp),'k.-','Color',c,'MarkerSize',3,'MarkerEdgeColor',c,... + plot(tp,abs(yp),':','Color',c,'MarkerSize',3,'MarkerEdgeColor',c,... 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); maxv = max([maxv, max(abs(xp)), max(abs(yp))]) + 0.1; if isfield(thisTrial,'microSaccades') & ~isnan(thisTrial.microSaccades) & ~isempty(thisTrial.microSaccades) if any(thisTrial.microSaccades >= me.plotRange(1) & thisTrial.microSaccades <= me.plotRange(2)) - plot(thisTrial.microSaccades,-0.1,'ko','Color',c,'MarkerSize',4,'MarkerEdgeColor',[0 0 0],... + plot(thisTrial.microSaccades,-0.1,'^','Color',c,'MarkerSize',4,'MarkerEdgeColor',[1 1 0],... 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); end end - - qq(1,1).select(); - qq(1,1).hold('on') - for fix=1:length(thisTrial.fixations) - f=thisTrial.fixations(fix); - plot3([f.time/1e3 f.time/1e3+f.length/1e3],[f.gstx f.genx],[f.gsty f.geny],'k-o',... - 'LineWidth',1,'MarkerSize',5,'MarkerEdgeColor',[0 0 0],... - 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe) - end - for sac=1:length(thisTrial.saccades) - s=thisTrial.saccades(sac); - plot3([s.time/1e3 s.time/1e3+s.length/1e3],[s.gstx s.genx],[s.gsty s.geny],'r-o',... - 'LineWidth',1.5,'MarkerSize',5,'MarkerEdgeColor',[1 0 0],... - 'MarkerFaceColor',c,'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe) - end - qq(1,2).select(); - qq(1,2).hold('on') + nexttile(5,[1 2]) plot(tp,pupilPlot,'Color',c, 'UserData',[thisTrial.idx thisTrial.correctedIndex thisTrial.variable],'ButtonDownFcn', @clickMe); idxt = find(t >= t1 & t <= t2); @@ -490,100 +622,82 @@ classdef tobiiAnalysis < analysisCore stdey = [stdey std(ya(idxt))]; udt = [thisTrial.idx thisTrial.correctedIndex thisTrial.variable]; - q(2,1).select(); - q(2,1).hold('on'); + nexttile(3); plot(meanx(end), meany(end),'ko','Color',c,'MarkerSize',6,'MarkerEdgeColor',[0 0 0],... 'MarkerFaceColor',c,'UserData', udt,'ButtonDownFcn', @clickMe); - q(2,2).select(); - q(2,2).hold('on'); + nexttile(4) plot3(meanx(end), meany(end),a,'ko','Color',c,'MarkerSize',6,'MarkerEdgeColor',[0 0 0],... 'MarkerFaceColor',c,'UserData', udt,'ButtonDownFcn', @clickMe); a = a + 1; end - display = me.display / me.ppd_; - - q(1,1).select(); - ah = gca; ah.ButtonDownFcn = @spawnMe; + %q(1,1).select(); + ah = nexttile(1); ah.FontSize = 6; + ah.ButtonDownFcn = @spawnMe; ah.DataAspectRatio = [1 1 1]; - axis ij; + axis ij; + axis tight; + axis equal; grid on; box on; - axis(round([-display(1)/2 display(1)/2 -display(2)/2 display(2)/2])); - title(q(1,1),[thisVarName upper(type) ': X vs. Y Eye Position']); - xlabel(q(1,1),'X Deg'); - ylabel(q(1,1),'Y Deg'); - - q(1,2).select(); - ah = gca; ah.ButtonDownFcn = @spawnMe; + %axis(round([-display(1)/2 display(1)/2 -display(2)/2 display(2)/2])); + title(ah,[thisVarName upper(type) ': X vs. Y Eye Position'],'FontSize',6); + xlabel(ah,'X Deg'); + ylabel(ah,'Y Deg'); + + %q(1,2).select(); + ah = nexttile(2); ah.FontSize = 6; + ah.ButtonDownFcn = @spawnMe; grid on; box on; axis tight; - if maxv > 10; maxv = 10; end - axis([me.plotRange(1) me.plotRange(2) -0.2 maxv]) + %axis([me.plotRange(1) me.plotRange(2) -0.2 maxv]) ti=sprintf('ABS Mean/SD %g - %g s: X=%.2g / %.2g | Y=%.2g / %.2g', t1,t2,... mean(abs(meanx)), mean(abs(stdex)), ... mean(abs(meany)), mean(abs(stdey))); ti2 = sprintf('ABS Median/SD %g - %g s: X=%.2g / %.2g | Y=%.2g / %.2g', t1,t2,median(abs(medx)), median(abs(stdex)), ... median(abs(medy)), median(abs(stdey))); - h=title(sprintf('X & Y(dot) Position vs. Time\n%s\n%s', ti,ti2)); - set(h,'BackgroundColor',[1 1 1]); - xlabel(q(1,2),'Time (s)'); - ylabel(q(1,2),'Degrees'); + title(sprintf('X & Y(dot) Position vs. Time\n%s\n%s', ti,ti2),'FontSize',6); + xlabel(ah,'Time (s)'); + ylabel(ah,'Degrees'); - qq(1,1).select(); - qq(1,1).margin = [30 20 10 35]; %left bottom right top - ah = gca; ah.ButtonDownFcn = @spawnMe; - grid on; - box on; - axis([me.plotRange(1) me.plotRange(2) -10 10 -10 10]); - view([35 35]); - xlabel(qq(1,1),'Time (ms)'); - ylabel(qq(1,1),'X Position'); - zlabel(qq(1,1),'Y Position'); - mn = nanmean(sacc); - md = nanmedian(sacc); - [~,er] = me.stderr(sacc,'SD'); - h=title(sprintf('%s %s: Saccades (red) & Fixation (black) | First Saccade mean/median: %.2g / %.2g � %.2g SD [%.2g <> %.2g]',... - thisVarName,upper(type),mn,md,er,min(sacc),max(sacc))); - set(h,'BackgroundColor',[1 1 1]); - qq(1,2).select(); - ah = gca; ah.ButtonDownFcn = @spawnMe; + ah = nexttile(5); ah.FontSize = 6; + ah.ButtonDownFcn = @spawnMe; axis([me.plotRange(1) me.plotRange(2) -inf inf]); grid on; - box on; - title(qq(1,2),[thisVarName upper(type) ': Pupil Diameter']); - xlabel(qq(1,2),'Time (s)'); - ylabel(qq(1,2),'Diameter'); + box on; axis tight; + title(ah,[thisVarName upper(type) ': Pupil Diameter'],'FontSize',6); + xlabel(ah,'Time (s)'); + ylabel(ah,'Diameter'); - q(2,1).select(); + ah = nexttile(3); ah.FontSize = 6; ah = gca; ah.ButtonDownFcn = @spawnMe; axis ij; grid on; box on; - axis tight; - axis([-1 1 -1 1]) - h=title(sprintf('X & Y %g-%gs MD/MN/STD: \nX : %.2g / %.2g / %.2g | Y : %.2g / %.2g / %.2g', ... - t1,t2,mean(meanx), median(medx),mean(stdex),mean(meany),median(medy),mean(stdey))); - set(h,'BackgroundColor',[1 1 1]); - xlabel(q(2,1),'X Degrees'); - ylabel(q(2,1),'Y Degrees'); - - q(2,2).select(); - ah = gca; ah.ButtonDownFcn = @spawnMe; + %axis tight; + %axis([-1 1 -1 1]) + title(sprintf('X & Y %g-%gs MD/MN/STD: \nX : %.2g / %.2g / %.2g | Y : %.2g / %.2g / %.2g', ... + t1,t2,mean(meanx), median(medx),mean(stdex),mean(meany),median(medy),mean(stdey)),... + 'FontSize',6); + xlabel(ah,'X Degrees'); + ylabel(ah,'Y Degrees'); + + ah = nexttile(4); ah.FontSize = 6; + ah.ButtonDownFcn = @spawnMe; grid on; box on; - axis tight; - axis([-1 1 -1 1]); + %axis tight; + %axis([-1 1 -1 1]); %axis square view(47,15); - title(sprintf('%s %s Mean X & Y Pos %g-%g-s over time',thisVarName,upper(type),t1,t2)); - xlabel(q(2,2),'X Degrees'); - ylabel(q(2,2),'Y Degrees'); - zlabel(q(2,2),'Trial'); + title(sprintf('%s %s Mean X & Y Pos %g-%g-s over time',thisVarName,upper(type),t1,t2),'FontSize',6); + xlabel(ah,'X Degrees'); + ylabel(ah,'Y Degrees'); + zlabel(ah,'Trial'); assignin('base','xvals',xvals); assignin('base','yvals',yvals); @@ -612,6 +726,53 @@ classdef tobiiAnalysis < analysisCore end end + % =================================================================== + %> @brief print messages + %> + %> @param + %> @return + % =================================================================== + function h = plotMessages(me) + if isempty(me.raw) || isempty(me.raw.messages) || ~iscell(me.raw.messages) + h = []; + return + end + + msgs = cell2table(me.raw.messages); + msgs.Properties.VariableNames = {'Tobii_Timestamp', 'Message'}; + msgs.Tobii_Timestamp = msgs.Tobii_Timestamp - msgs.Tobii_Timestamp(1); + msgs.Tobii_Timestamp = double(msgs.Tobii_Timestamp) / 1e6; + + h = build_gui(); + + set(h.uitable1,'Data',msgs); + + function h = build_gui() + fsmall = 12; + h.figure1 = uifigure( ... + 'Tag', 'msglog', ... + 'Units', 'normalized', ... + 'Position', [0.6 0 0.4 0.6], ... + 'Name', ['Log: ' me.fullName], ... + 'MenuBar', 'none', ... + 'NumberTitle', 'off', ... + 'Color', [0.94 0.94 0.94], ... + 'Resize', 'on'); + if ~isMATLABReleaseOlderThan("R2025a"); theme(h.figure1,'light'); end + h.uitable1 = uitable( ... + 'Parent', h.figure1, ... + 'Tag', 'msglogtable', ... + 'Units', 'normalized', ... + 'Position', [0 0 1 1], ... + 'FontName', me.monoFont, ... + 'FontSize', fsmall, ... + 'RowName', 'numbered',... + 'BackgroundColor', [1 1 1;0.95 0.95 0.95], ... + 'RowStriping','on', ... + 'ColumnEditable', [], ... + 'ColumnWidth', {'fit','4x'}); + end + end % =================================================================== %> @brief %> @@ -635,8 +796,8 @@ classdef tobiiAnalysis < analysisCore me.ROIInfo(i).fixationX = fixationX; me.ROIInfo(i).fixationY = fixationY; me.ROIInfo(i).fixationRadius = fixationRadius; - x = me.trials(i).gx / me.ppd_; - y = me.trials(i).gy / me.ppd_; + x = me.trials(i).gx; + y = me.trials(i).gy; times = me.trials(i).times / 1e3; idx = find(times > 0); % we only check ROI post 0 time times = times(idx); @@ -682,8 +843,8 @@ classdef tobiiAnalysis < analysisCore fixationRadius = me.TOI(5); for i = 1:length(me.trials) times = me.trials(i).times / 1e3; - x = me.trials(i).gx / me.ppd_; - y = me.trials(i).gy / me.ppd_; + x = me.trials(i).gx; + y = me.trials(i).gy; idx = intersect(find(times>=t1), find(times<=t2)); times = times(idx); @@ -728,7 +889,7 @@ classdef tobiiAnalysis < analysisCore % =================================================================== function plotROI(me) if ~isempty(me.ROIInfo) - h=figure;figpos(1,[2000 1000]);set(h,'Color',[1 1 1],'Name',me.file); + h=figure;figpos(1,[2000 1000]);set(h,'Color',[1 1 1],'Name',me.fileName); x1 = me.ROI(1) - me.ROI(3); x2 = me.ROI(1) + me.ROI(3); @@ -843,7 +1004,7 @@ classdef tobiiAnalysis < analysisCore disp('No TOI parsed!!!') return end - h=figure;figpos(1,[2000 1000]);set(h,'Color',[1 1 1],'Name',me.file); + h=figure;figpos(1,[2000 1000]);set(h,'Color',[1 1 1],'Name',me.fileName); t1 = me.TOI(1); t2 = me.TOI(2); @@ -958,11 +1119,7 @@ classdef tobiiAnalysis < analysisCore %> @return % =================================================================== function ppd = get.ppd(me) - if me.distance == 57.3 && me.override573 == true - ppd = round( me.pixelsPerCm * (67 / 57.3)); %set the pixels per degree, note this fixes some older files where 57.3 was entered instead of 67cm - else - ppd = round( me.pixelsPerCm * (me.distance / 57.3)); %set the pixels per degree - end + ppd = round( me.pixelsPerCm * (me.distance / 57.3)); %set the pixels per degree me.ppd_ = ppd; end @@ -972,39 +1129,140 @@ classdef tobiiAnalysis < analysisCore %> @param %> @return % =================================================================== - function fixVarNames(me) - if me.needOverride == true - if isempty(me.trialOverride) - warning('No replacement trials available!!!') - return + function plotNH(me, trial, handle) + if length(me.trials) < 1; return; end + if ~isfield(me.trials,'data'); warning('Need to parse NH data first'); return; end + if ~exist('trial','var'); trial = 1; end + if ~me.isParsed;return;end + if ~exist('handle','var'); handle=[]; end + try + if isempty(handle) + handle=figure('Name','Saccade Plots','Color',[1 1 1],'NumberTitle','off',... + 'Papertype','a4','PaperUnits','centimeters',... + 'PaperOrientation','landscape'); + figpos(1,[0.5 0.9],1,'%'); end - trials = me.trialOverride; %#ok<*PROP> - if max([me.trials.correctedIndex]) ~= length(trials) - warning('TRIAL ID LENGTH MISMATCH!'); - return + figure(handle); + data = me.trials(trial).data; + me.ETparams.screen.rect = struct('deg', [-30 -30 30 30]); + plotClassification(data,'deg','vel',me.ETparams.samplingFreq,... + me.ETparams.glissade.searchWindow,me.ETparams.screen.rect,... + 'title','Test','showSacInScan',true); + catch ME + getReport(ME) + end + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function explore(me, close) + persistent fig figA figB Nexp + + if exist('close','var') && close == true + try delete(figB); end + try delete(figA); end + try delete(fig.f); end + try delete(gcf); end + fig = []; figA = []; figB = []; Nexp = 0; + return; + end + if isempty(Nexp); Nexp = 0; end + if isempty(fig); fig.f = figure('Units','Normalized','Position',[0 0.8 0.1 0.2],'CloseRequestFcn',@exploreClose); end + if isempty(figA); figA = figure('Units','Normalized','Position',[0.1 0 0.49 1],'CloseRequestFcn',@exploreClose); end + if isempty(figB); figB = figure('Units','Normalized','Position',[0.5 0 0.5 1],'CloseRequestFcn',@exploreClose); end + + if isempty(fig.f.Children)|| ~ishandle(fig.f) + fig.b0 = uicontrol('Parent',fig.f,'Units','Normalized',... + 'Style','text','String',['TRIAL: ' num2str(Nexp)],'Position',[0.1 0.8 0.8 0.1]); + fig.b1 = uicontrol('Parent',fig.f,'Units','Normalized',... + 'String','->','Position',[0.1 0.1 0.8 0.3],... + 'Callback', @exploreNext); + fig.b2 = uicontrol('Parent',fig.f,'Units','Normalized',... + 'String','<-','Position',[0.1 0.5 0.8 0.3],... + 'Callback', @explorePrevious); + end + + if isempty(figA.Children) ; N = 0; exploreNext(); end + + function exploreNext(~) + Nexp = Nexp + 1; + if Nexp > length(me.trials); Nexp = 1; end + clf(figA); clf(figB); + plotNH(me,Nexp,figB); + plot(me,Nexp,[],[],[],figA); + fig.b0.String = ['TRIAL: ' num2str(Nexp)]; + end + function explorePrevious(~) + Nexp = Nexp - 1; + if Nexp < 1; Nexp = length(me.trials); end + clf(figA); clf(figB); + plotNH(me,N,figB); + plot(me,N,[],[],[],figA); + fig.b0.String = ['TRIAL: ' num2str(Nexp)]; + end + function exploreClose(~) + me.explore(true); + end + + end + + % =================================================================== + %> @brief + %> + %> @param + %> @return + % =================================================================== + function [out,in,avg,err] = computePupilAverage(me, trials, sampleRate) + if ~exist('trials','var') || isempty(trials); trials = me.correct.idx; end + if ~exist('sampleRate','var'); sampleRate = 100; end + in = {}; + a = 1; + if ~isempty(me.excludeTrials) + trials = setdiff(trials, me.excludeTrials); + fprintf('===>>> Excluded trials: %s',num2str(me.excludeTrials)); + end + for ii = trials + tr = me.trials(ii); + if isfield(tr,'data') + t = milliseconds(tr.data.time/1e3); + p = tr.data.pupil.size; + else + t = milliseconds(tr.times); + p = tr.pa; end - a = 1; - me.trialList = []; - for j = 1:length(me.trials) - if me.trials(j).incorrect ~= true - if a <= length(trials) && me.trials(j).correctedIndex == trials(a).index - me.trials(j).oldid = me.trials(j).variable; - me.trials(j).variable = trials(a).variable; - me.trialList(j) = me.trials(j).variable; - if me.trials(j).breakFix == true - me.trialList(j) = -[me.trialList(j)]; - end - a = a + 1; - end - end + if me.baselinePupil + tt = seconds(t); + mp = mean(p(tt >= me.baselineWindow(1) & tt <= me.baselineWindow(2))); + p = p - mp; end - parseAsVars(me); %need to redo this now - warning('---> Trial name override in place!!!') - else - me.trialOverride = struct(); + if me.smoothPupil + p = smooth(p); + end + in{a} = timetable(t,p); + a = a + 1; end + + out = synchronize(in{:},'regular','median','SampleRate',sampleRate); + + handle=figure('Name',me.name,'Color',[1 1 1],'NumberTitle','off',... + 'Papertype','a4','PaperUnits','centimeters',... + 'PaperOrientation','landscape','Renderer','painters'); + figpos(1,[0.5 0.5],1,'%'); + [avg, err] = analysisCore.stderr(out,'SE',false,0.05,2); + analysisCore.areabar(seconds(out.t), avg, err,[0.8 0.4 0.4]); + axis tight + box on; grid on + xlim(me.plotRange); + ylabel('Pupil Diameter \pm SE'); + xlabel('Time (s)'); + title(sprintf('%s - %i trials',me.name,width(out))); end + end%-------------------------END PUBLIC METHODS--------------------------------% %======================================================================= @@ -1164,11 +1422,9 @@ classdef tobiiAnalysis < analysisCore %> @return % =================================================================== function parseEvents(me) + tmain = tic; isTrial = false; tri = 1; %current trial that is being parsed - tri2 = 1; %trial ignoring incorrects - eventN = 0; - me.comment = me.raw.HEADER; me.trials = struct(); me.correct.idx = []; me.correct.saccTimes = []; @@ -1182,384 +1438,221 @@ classdef tobiiAnalysis < analysisCore this.distance = []; this.pixelspercm = []; this.display = []; - me.ppd; %faster to cache this now (dependant property sets ppd_ too) - sample = me.raw.FSAMPLE.gx(:,100); %check which eye - if sample(1) == -32768 %only use right eye if left eye data is not present - eyeUsed = 2; %right eye index for FSAMPLE.gx; + trialDef = getTrialDef(me); + + t1 = inf; t2 = -inf; + x1 = inf; x2 = -inf; + y1 = inf; y2 = -inf; + + if isprop(me,'exp') && isfield (me.exp,'rE') + if ~isempty(me.exp.rE.screen) && ~isempty(me.exp.rE.screen.screenVals) + sV = me.exp.rE.screen.screenVals; + sV.ppd = me.exp.rE.screen.ppd; + else + sV = me.exp.rE.screenVals; + end + end + if isempty(sV) + warning('NO SCREEN PROPORTIONS, X and Y positions will be inaccurate!!!'); else - eyeUsed = 1; %left eye index + if isfield(sV,'widthInDegrees') + me.xmod = sV.widthInDegrees; + me.xmod = sV.heightInDegrees; + else + me.xmod = sV.width / sV.ppd; + me.ymod = sV.height / sV.ppd; + end + fprintf('\n\n--->>> Using X = %.2f deg and Y = %.2f deg converting gx and gy from Tobii...\n\n', me.xmod, me.ymod); end - - FEVENTN = length(me.raw.FEVENT); - pb = textprogressbar(FEVENTN, 'startmsg', 'Parsing Eyelink Events: ',... - 'showactualnum', true,'updatestep', round(FEVENTN/100)); - for i = 1:FEVENTN - isMessage = false; - evt = me.raw.FEVENT(i); - - if evt.type == me.EVENT_TYPES.MESSAGEEVENT %strcmpi(evt.codestring,'MESSAGEEVENT') - isMessage = true; - no = regexpi(evt.message,'^(?!cal|!mode|validate|reccfg|elclcfg|gaze_coords|thresholds|elcl_)','names'); %ignore these first - if ~isempty(no) && ~isempty(no.NO) - continue - end + times = double(me.raw.data.gaze.systemTimeStamp) / 1e3; + nMessages = size(me.raw.messages,1); + pb = textprogressbar(nMessages, 'startmsg', 'Parsing Tobii Experiment Events: ',... + 'showactualnum', true,'updatestep', round(nMessages/(nMessages/20))); + for i = 1:nMessages + + evtT = double(me.raw.messages{i,1}) / 1e3; + evt = me.raw.messages{i,2}; + + if startsWith(evt,["WARMUP", "POINT ", "Calibration", "CALIBRATION", "START ", "STOP "]) + pb(i); + continue; end - if isMessage && ~isTrial - xy = regexpi(evt.message,'^DISPLAY_COORDS \d? \d? (?\d+) (?\d+)','names'); - if ~isempty(xy) && ~isempty(xy.x) - me.display = [str2double(xy.x) str2double(xy.y)]; - this.display = me.display; - me.xCenter = me.display(1) / 2; - me.yCenter = me.display(2) / 2; - continue - end - - ms = regexpi(evt.message,'^(?FRAMERATE|DISPLAY_PPD|DISPLAY_DISTANCE|DISPLAY_PIXELSPERCM) (?\d+)','names'); - if ~isempty(ms) && ~isempty(ms.t) && ~isempty(ms.n) - switch ms.t - case 'FRAMERATE' - this.FrameRate = str2double(ms.n); - case 'DISPLAY_PPD' - this.ppd = str2double(ms.n); - case 'DISPLAY_DISTANCE' - this.distance = str2double(ms.n); - case 'DISPLAY_PIXELSPERCM' - this.pixelspercm = str2double(ms.n); - end - continue - end - - rt = regexpi(evt.message,'^(?V_RT MESSAGE) (?\w+) (?\w+)','names'); - if ~isempty(rt) && ~isempty(rt.a) && ~isempty(rt.b) - me.rtStartMessage = rt.a; - me.rtEndMessage = rt.b; - continue - end + if ~isTrial - id = regexpi(evt.message,['^(?' me.trialStartMessageName ')(\s*)(?\d*)'],'names'); - if ~isempty(id) && ~isempty(id.TAG) - if isempty(id.ID) %we have a bug in early EDF files with an empty TRIALID!!! - id.ID = '1010'; + % NEW TRIAL + if startsWith(evt, me.trialStartMessageName) + id = regexpi(evt,['^(?' me.trialStartMessageName ')(\s*)(?\d*)'],'names'); + if isempty(id.ID) + id.ID = '101010'; + end + thisTrial = trialDef; + thisTrial.variable = str2double(id.ID); + if thisTrial.variable > 0 && thisTrial.variable <= length(me.varLabels) + thisTrial.variableMessageName = me.varLabels{thisTrial.variable}; end + thisTrial.idx = tri; + thisTrial.time = evtT; + thisTrial.sttime = evtT; + if tri > 1 + thisTrial.totaltime = thisTrial.sttime - me.trials(1).sttime; + end + thisTrial.rtstarttime = thisTrial.sttime; isTrial = true; - eventN=1; - me.trials(tri).variable = str2double(id.ID); - me.trials(tri).idx = tri; - me.trials(tri).correctedIndex = []; - me.trials(tri).time = double(evt.time); - me.trials(tri).rt = false; - me.trials(tri).rtoverride = false; - me.trials(tri).fixations = []; - me.trials(tri).nfix = 2; - me.trials(tri).saccades = []; - me.trials(tri).nsacc = []; - me.trials(tri).saccadeTimes = []; - me.trials(tri).firstSaccade = NaN; - me.trials(tri).uuid = []; - me.trials(tri).result = []; - me.trials(tri).correct = false; - me.trials(tri).breakFix = false; - me.trials(tri).incorrect = false; - me.trials(tri).unknown = false; - me.trials(tri).messages = []; - me.trials(tri).sttime = double(evt.sttime); - me.trials(tri).entime = NaN; - me.trials(tri).totaltime = (me.trials(tri).sttime - me.trials(1).sttime)/1e3; - me.trials(tri).startsampletime = NaN; - me.trials(tri).endsampletime = NaN; - me.trials(tri).rtstarttime = double(evt.sttime); - me.trials(tri).rtendtime = NaN; - me.trials(tri).synctime = NaN; - me.trials(tri).deltaT = NaN; - me.trials(tri).rttime = NaN; - me.trials(tri).times = []; - me.trials(tri).gx = []; - me.trials(tri).gy = []; - me.trials(tri).hx = []; - me.trials(tri).hy = []; - me.trials(tri).pa = []; continue end - end - if isTrial - if ~isMessage + elseif isTrial - if evt.type == me.EVENT_TYPES.STARTSAMPLES - me.trials(tri).startsampletime = double(evt.sttime); - continue - end + % assume we are missing an end trial message, force one + if startsWith(evt,"V_RT") + evt = [me.trialEndMessage ' ' num2str(-101010)]; + thisTrial.forcedend = true; + end - if evt.type == me.EVENT_TYPES.ENDFIX - fixa = []; - if isempty(me.trials(tri).fixations) - fix = 1; - else - fix = length(me.trials(tri).fixations)+1; - end - if me.trials(tri).rt == true - rel = me.trials(tri).rtstarttime; - fixa.rt = true; - else - rel = me.trials(tri).sttime; - fixa.rt = false; - end - fixa.n = eventN; - fixa.ppd = me.ppd_; - fixa.sttime = double(evt.sttime); - fixa.entime = double(evt.entime); - fixa.time = fixa.sttime - rel; - fixa.length = fixa.entime - fixa.sttime; - fixa.rel = rel; - - [fixa.gstx, fixa.gsty] = toDegrees(me, [evt.gstx, evt.gsty]); - [fixa.genx, fixa.geny] = toDegrees(me, [evt.genx, evt.geny]); - [fixa.x, fixa.y] = toDegrees(me, [evt.gavx, evt.gavy]); - [fixa.theta, fixa.rho] = cart2pol(fixa.x, fixa.y); - fixa.theta = me.rad2ang(fixa.theta); - - if fix == 1 - me.trials(tri).fixations = fixa; - else - me.trials(tri).fixations(fix) = fixa; - end - me.trials(tri).nfix = fix; - eventN = eventN + 1; - continue - end + % Reaction time start markers (normally start of stim + % onset + if startsWith(evt, me.rtStartMessage) || startsWith(evt, me.rtOverrideMessage) + thisTrial.synctime = true; + thisTrial.rtstarttime = evtT; + continue + end - if evt.type == me.EVENT_TYPES.ENDSACC % strcmpi(evt.codestring,'ENDSACC') - sacc = []; - if isempty(me.trials(tri).saccades) - nsacc = 1; - else - nsacc = length(me.trials(tri).saccades)+1; - end - if me.trials(tri).rt == true - rel = me.trials(tri).rtstarttime; - sacc.rt = true; + % Messages + if startsWith(evt,"MSG:") + msg = regexpi(evt,'^MSG:\s?(?[\w]+)[ =]*(?.*)','names'); + if ~isempty(msg) && ~isempty(msg.MSG) + if isfield(thisTrial.messages,msg.MSG) + thisTrial.messages.(msg.MSG){end+1} = msg.VAL; + thisTrial.messages.([msg.MSG 'TIME']){end+1} = double(evt.sttime); else - rel = me.trials(tri).sttime; - sacc.rt = false; + thisTrial.messages.(msg.MSG){1} = msg.VAL; + thisTrial.messages.([msg.MSG 'TIME']){1} = double(evt.sttime); end - sacc.n = eventN; - sacc.ppd = me.ppd_; - sacc.sttime = double(evt.sttime); - sacc.entime = double(evt.entime); - sacc.time = sacc.sttime - rel; - sacc.length = sacc.entime - sacc.sttime; - sacc.rel = rel; - - [sacc.gstx, sacc.gsty] = toDegrees(me, [evt.gstx evt.gsty]); - [sacc.genx, sacc.geny] = toDegrees(me, [evt.genx evt.geny]); - [sacc.x, sacc.y] = deal((sacc.genx - sacc.gstx), (sacc.geny - sacc.gsty)); - [sacc.theta, sacc.rho] = cart2pol(sacc.x, sacc.y); - sacc.theta = me.rad2ang(sacc.theta); - - if sacc.rho > me.minSaccadeDistance; sacc.microSaccade = false; - else sacc.microSaccade = true; end - - if nsacc == 1 - me.trials(tri).saccades = sacc; - else - me.trials(tri).saccades(nsacc) = sacc; + if strcmpi(msg.MSG, me.rtOverrideMessage) + thisTrial.rtstarttimeOLD = thisTrial.rtstarttime; + thisTrial.rtstarttime = double(evt.sttime); + thisTrial.rt = true; + thisTrial.rtoverride = true; end - me.trials(tri).nsacc = nsacc; - eventN = eventN + 1; - continue - end - - if evt.type == me.EVENT_TYPES.ENDSAMPLES %strcmpi(evt.codestring,'ENDSAMPLES') - me.trials(tri).endsampletime = double(evt.sttime); - idx = me.raw.FSAMPLE.time >= me.trials(tri).startsampletime & ... - me.raw.FSAMPLE.time <= me.trials(tri).endsampletime; - - me.trials(tri).times = double(me.raw.FSAMPLE.time(idx)); - me.trials(tri).times = me.trials(tri).times - me.trials(tri).rtstarttime; - - me.trials(tri).gx = me.raw.FSAMPLE.gx(eyeUsed, idx); - me.trials(tri).gx = me.trials(tri).gx - me.display(1)/2; - - me.trials(tri).gy = me.raw.FSAMPLE.gy(eyeUsed, idx); - me.trials(tri).gy = me.trials(tri).gy - me.display(2)/2; - - me.trials(tri).hx = me.raw.FSAMPLE.hx(eyeUsed, idx); - - me.trials(tri).hy = me.raw.FSAMPLE.hy(eyeUsed, idx); - - me.trials(tri).pa = me.raw.FSAMPLE.pa(eyeUsed, idx); - continue - end - - else - vari = regexpi(evt.message,['^(MSG:)?' me.variableMessageName ' (?[0-9\.]+)'],'names'); - if ~isempty(vari) && ~isempty(vari.VARI) - me.trials(tri).variable = str2double(vari.VARI); - me.trials(tri).variableMessageName = me.variableMessageName; - continue - end - - uuid = regexpi(evt.message,'^(MSG:)?UUID (?[\w]+)','names'); - if ~isempty(uuid) && ~isempty(uuid.UUID) - me.trials(tri).uuid = uuid.UUID; continue end + end - msg = regexpi(evt.message,'^MSG:\s?(?[\w]+) *(?.*)','names'); - if ~isempty(msg) && ~isempty(msg.MSG) - if isfield(me.trials(tri).messages,msg.MSG) - me.trials(tri).messages.(msg.MSG){end+1} = msg.VAL; - me.trials(tri).messages.([msg.MSG 'TIME']){end+1} = double(evt.sttime); + % !V Messages + if startsWith(evt,"!V") + msg = regexpi(evt,'^!V (?.+?) (?.+?) (?.+?)$','names'); + if ~isempty(msg) && isempty(msg.v) && ~isempty(msg.n) + tag = [msg.v '-' msg.n]; + if isfield(thisTrial.messages,tag) + thisTrial.messages.(tag){end+1} = msg.val; + thisTrial.messages.([tag 'TIME']){end+1} = double(evt.sttime); else - me.trials(tri).messages.(msg.MSG){1} = msg.VAL; - me.trials(tri).messages.([msg.MSG 'TIME']){1} = double(evt.sttime); - end - if strcmpi(msg.MSG, me.rtOverrideMessage) - me.trials(tri).rtstarttimeOLD = me.trials(tri).rtstarttime; - me.trials(tri).rtstarttime = double(evt.sttime); - me.trials(tri).rtoverride = true; - if ~isempty(me.trials(tri).fixations) - for lf = 1 : length(me.trials(tri).fixations) - me.trials(tri).fixations(lf).time = me.trials(tri).fixations(lf).sttime - me.trials(tri).rtstarttime; - me.trials(tri).fixations(lf).rt = true; - end - end - if ~isempty(me.trials(tri).saccades) - for lf = 1 : length(me.trials(tri).saccades) - me.trials(tri).saccades(lf).time = me.trials(tri).saccades(lf).sttime - me.trials(tri).rtstarttime; - me.trials(tri).saccades(lf).rt = true; - me.trials(tri).saccadeTimes(lf) = me.trials(tri).saccades(lf).time; - end - end + thisTrial.messages.(tag){1} = msg.val; + thisTrial.messages.([tag 'TIME']){1} = double(evt.sttime); end continue end + end - synct = regexpi(evt.message,'^SYNCTIME','match'); - if ~isempty(synct) - me.trials(tri).synctime = evt.sttime; - continue + % UUID from state machine + if startsWith(evt,"UUID") + uuid = regexpi(evt,'^(MSG:)?UUID (?[\w]+)','names'); + if ~isempty(uuid) && ~isempty(uuid.UUID) + thisTrial.uuid = uuid.UUID; end + continue + end - endfix = regexpi(evt.message,['^' me.rtStartMessage],'match'); - if ~isempty(endfix) - me.trials(tri).rtstarttime = double(evt.sttime); - me.trials(tri).rt = true; - if ~isempty(me.trials(tri).fixations) - for lf = 1 : length(me.trials(tri).fixations) - me.trials(tri).fixations(lf).time = me.trials(tri).fixations(lf).sttime - me.trials(tri).rtstarttime; - me.trials(tri).fixations(lf).rt = true; - end - end - if ~isempty(me.trials(tri).saccades) - for lf = 1 : length(me.trials(tri).saccades) - me.trials(tri).saccades(lf).time = me.trials(tri).saccades(lf).sttime - me.trials(tri).rtstarttime; - me.trials(tri).saccades(lf).rt = true; - me.trials(tri).saccadeTimes(lf) = me.trials(tri).saccades(lf).time; - end - end - continue + % trial END + if startsWith(evt, me.trialEndMessage) || startsWith(evt, 'TRIALEND') + if startsWith(evt, 'TRIALEND') + id.ID = -101010; + else + id = regexpi(evt,['^' me.trialEndMessage ' (?(\-|\+|\d)+)'],'names'); end - - endrt = regexpi(evt.message,['^' me.rtEndMessage],'match'); - if ~isempty(endrt) - me.trials(tri).rtendtime = double(evt.sttime); - if isfield(me.trials,'rtstarttime') - me.trials(tri).rttime = me.trials(tri).rtendtime - me.trials(tri).rtstarttime; - end - continue + if isempty(id.ID); id.ID = -101010; end + thisTrial.entime = evtT; + thisTrial.result = str2num(id.ID); + if thisTrial.result == me.correctValue + thisTrial.correct = true; + elseif thisTrial.result == me.incorrectValue + thisTrial.incorrect = true; + elseif thisTrial.result == me.breakFixValue + thisTrial.breakFix = true; + else + thisTrial.unknown = true; end - - id = regexpi(evt.message,['^' me.trialEndMessage ' (?(\-|\+|\d)+)'],'names'); - if ~isempty(id) && ~isempty(id.ID) - me.trials(tri).entime = double(evt.sttime); - me.trials(tri).result = str2num(id.ID); - sT=[]; - me.trials(tri).saccadeTimes = []; - for ii = 1:me.trials(tri).nsacc - t = me.trials(tri).saccades(ii).time; - me.trials(tri).saccadeTimes(ii) = t; - if isnan(me.trials(tri).firstSaccade) && t > 0 && me.trials(tri).saccades(ii).microSaccade == false - me.trials(tri).firstSaccade = t; - sT=t; - end + sT=[]; + thisTrial.deltaT = thisTrial.entime - thisTrial.sttime; + if isempty(thisTrial.times) + if isnan(thisTrial.startsampletime) + thisTrial.startsampletime = thisTrial.sttime; end - if any(find(me.trials(tri).result == me.correctValue)) - me.trials(tri).correct = true; - me.correct.idx = [me.correct.idx tri]; - me.trialList(tri) = me.trials(tri).variable; - if ~isempty(sT) && sT > 0 - me.correct.saccTimes = [me.correct.saccTimes sT]; - else - me.correct.saccTimes = [me.correct.saccTimes NaN]; - end - me.trials(tri).correctedIndex = tri2; - tri2 = tri2 + 1; - elseif any(find(me.trials(tri).result == me.breakFixValue)) - me.trials(tri).breakFix = true; - me.breakFix.idx = [me.breakFix.idx tri]; - me.trialList(tri) = -me.trials(tri).variable; - if ~isempty(sT) && sT > 0 - me.breakFix.saccTimes = [me.breakFix.saccTimes sT]; - else - me.breakFix.saccTimes = [me.breakFix.saccTimes NaN]; - end - me.trials(tri).correctedIndex = []; - elseif any(find(me.trials(tri).result == me.incorrectValue)) - me.trials(tri).incorrect = true; - me.incorrect.idx = [me.incorrect.idx tri]; - me.trialList(tri) = -me.trials(tri).variable; - if ~isempty(sT) && sT > 0 - me.incorrect.saccTimes = [me.incorrect.saccTimes sT]; - else - me.incorrect.saccTimes = [me.incorrect.saccTimes NaN]; - end - me.trials(tri).correctedIndex = []; + thisTrial.endsampletime = thisTrial.entime; + idx = times >= thisTrial.startsampletime & ... + times <= thisTrial.endsampletime; + if ~isempty(idx) + thisTrial.times = times(idx); + thisTrial.times = thisTrial.times - thisTrial.rtstarttime; + thisTrial.timeRange = [thisTrial.times(1) thisTrial.times(end)]; + thisTrial.gx = me.raw.data.gaze.left.gazePoint.onDisplayArea(1,idx); + thisTrial.gx = (thisTrial.gx * me.xmod) - (me.xmod / 2); + thisTrial.gy = me.raw.data.gaze.left.gazePoint.onDisplayArea(2,idx); + thisTrial.gy = (thisTrial.gy * me.ymod) - (me.ymod / 2); + if min(thisTrial.gx) < x1; x1 = min(thisTrial.gx); end + if max(thisTrial.gx) > x2; x2 = max(thisTrial.gx); end + if min(thisTrial.gy) < y1; y1 = min(thisTrial.gy); end + if max(thisTrial.gy) > y2; y2 = max(thisTrial.gy); end + thisTrial.hx = me.raw.data.gaze.left.gazePoint.inUserCoords(1,idx); + thisTrial.hy = me.raw.data.gaze.left.gazePoint.inUserCoords(2,idx); + thisTrial.pa = me.raw.data.gaze.left.pupil.diameter(idx); + thisTrial.valid = me.raw.data.gaze.left.pupil.valid(idx); + thisTrial.validRatio = length(thisTrial.valid) / sum(thisTrial.valid); + if thisTrial.times(1) < t1; t1 = thisTrial.times(1); end + if thisTrial.times(end) > t2; t2 = thisTrial.times(end); end else - me.trials(tri).unknown = true; - me.unknown.idx = [me.unknown.idx tri]; - me.trialList(tri) = -me.trials(tri).variable; - if ~isempty(sT) && sT > 0 - me.unknown.saccTimes = [me.unknown.saccTimes sT]; - else - me.unknown.saccTimes = [me.unknown.saccTimes NaN]; - end - me.trials(tri).correctedIndex = []; + thisTrial.result = -101010; end - me.trials(tri).deltaT = me.trials(tri).entime - me.trials(tri).sttime; - isTrial = false; - tri = tri + 1; - continue + end - end - end + if tri == 1 + me.trials = thisTrial; + else + me.trials(tri) = thisTrial; + end + isTrial = false; + clear thisTrial; + tri = tri + 1; + continue + end % END trial END + end % END isTrial pb(i); - end + end % END FOR pb(i); - - me.otherinfo = this; - %prune the end trial if invalid - if ~me.trials(end).correct && ~me.trials(end).breakFix && ~me.trials(end).incorrect - me.trials(end) = []; - me.correct.idx = find([me.trials.correct] == true); - me.correct.saccTimes = [me.trials(me.correct.idx).firstSaccade]; - me.breakFix.idx = find([me.trials.breakFix] == true); - me.breakFix.saccTimes = [me.trials(me.breakFix.idx).firstSaccade]; - me.incorrect.idx = find([me.trials.incorrect] == true); - me.incorrect.saccTimes = [me.trials(me.incorrect.idx).firstSaccade]; - end + me.plotRange = [t1/1e3 t2/1e3]; + fprintf('\n\n--->>> Tobii Time range: %.3f to %.3f\n', me.plotRange(1), me.plotRange(2)); + fprintf('--->>> GX Data range: %.3f to %.3f\n', x1, x2); + fprintf('--->>> GY Data range: %.3f to %.3f\n\n', y1, y2); - if max(abs(me.trialList)) == 1010 && min(abs(me.trialList)) == 1010 - me.needOverride = true; - me.salutation('','---> TRIAL NAME BUG OVERRIDE NEEDED!\n',true); - else - me.needOverride = false; + me.otherinfo = this; + + if isempty(me.trials) || ~isfield(me.trials,'correct') + warning('---> eyelinkAnalysis.parseEvents: No trials could be parsed in this data!') + return end + + me.correct.idx = find([me.trials.correct] == true); + me.breakFix.idx = find([me.trials.breakFix] == true); + me.incorrect.idx = find([me.trials.incorrect] == true); + me.unknown.idx = find([me.trials.unknown] == true); + + fprintf(':#: Parsing Tobii Events into %i Trials took %.2f secs | min-t = %.2f max-t = %.2f\n',length(me.trials),toc(tmain), t1/1e3, t2/1e3); + end % =================================================================== @@ -1569,38 +1662,73 @@ classdef tobiiAnalysis < analysisCore %> @return % =================================================================== function parseAsVars(me) + if isempty(me.exp) || ~isfield(me.exp,'rE') + uniqueVars = sort(unique([me.trials.variable])); + labels = []; + warning('---> Vars are being parsed from trials directly...') + else + if me.exp.rE.task.nVars == 0 + uniqueVars = 1; + labels = {'1'}; + else + uniqueVars = [me.exp.rE.task.varList{:,1}]; + labels = me.exp.rE.task.varLabels; + end + end + nVars = length(uniqueVars); + if isempty(me.trials) || ~isfield(me.trials,'correct') + warning('---> eyelinkAnalysis.parseAsVars: No trials and therefore cannot extract variables!') + return + end me.vars = struct(); - me.vars(1).name = ''; - me.vars(1).var = []; - me.vars(1).varidx = []; - me.vars(1).variable = []; - me.vars(1).idx = []; - me.vars(1).correctedidx = []; - me.vars(1).trial = []; - me.vars(1).sTime = []; - me.vars(1).sT = []; - me.vars(1).uuid = {}; - - uniqueVars = sort(unique([me.trials.variable])); + me.vars(nVars).name = ''; + me.vars(nVars).var = []; + me.vars(nVars).varidx = []; + me.vars(nVars).variable = []; + me.vars(nVars).idx = []; + me.vars(nVars).correct = []; + me.vars(nVars).idxcorrect = []; + me.vars(nVars).result = []; + me.vars(nVars).correctedidx = []; + me.vars(nVars).trial = []; + me.vars(nVars).sTime = []; + me.vars(nVars).sT = []; + me.vars(nVars).uuid = {}; + me.vars(nVars).varWarning = []; + + for i=uniqueVars + if i <= length(labels); me.vars(i).name = labels{i}; end + me.vars(i).var = uniqueVars(i); + end for i = 1:length(me.trials) trial = me.trials(i); var = trial.variable; - if trial.incorrect == true - continue - end - if trial.variable == 1010 + if trial.invalid == true continue end idx = find(uniqueVars==var); - me.vars(idx).name = num2str(var); + if var == 101010 || isempty(idx) + idx = 1; + me.vars(idx).varWarning = [me.vars(idx).varWarning i]; + end + if ~isempty(labels) && idx <= length(labels) + me.vars(idx).name = labels{idx}; + else + me.vars(idx).name = num2str(var); + end me.vars(idx).var = var; me.vars(idx).varidx = [me.vars(idx).varidx idx]; me.vars(idx).variable = [me.vars(idx).variable var]; me.vars(idx).idx = [me.vars(idx).idx i]; + me.vars(idx).correct = [me.vars(idx).correct trial.correct]; + if trial.correct > 0 + me.vars(idx).idxcorrect = [me.vars(idx).idxcorrect i]; + end + me.vars(idx).result = [me.vars(idx).result trial.result]; me.vars(idx).correctedidx = [me.vars(idx).correctedidx i]; - me.vars(idx).trial = [me.vars(idx).trial; trial]; - me.vars(idx).uuid = [me.vars(idx).uuid, trial.uuid]; + %me.vars(idx).trial = [me.vars(idx).trial; trial]; + me.vars(idx).uuid{end+1} = trial.uuid; if ~isempty(trial.saccadeTimes) me.vars(idx).sTime = [me.vars(idx).sTime trial.saccadeTimes(1)]; else @@ -1707,6 +1835,70 @@ classdef tobiiAnalysis < analysisCore end end + % =================================================================== + %> @brief + %> + % =================================================================== + function computeFullSaccades(me) + assert(exist('runNH2010Classification.m'),'Please add NystromHolmqvist2010 to path!'); + % load parameters for event classifier + if isempty(me.ETparams) + me.ETparams = defaultParameters; + % settings for code specific to Niehorster, Siu & Li (2015) + me.ETparams.extraCut = [0 0]; % extra ms of data to cut before and after saccade. + me.ETparams.qInterpMissingPos = true; % interpolate using straight lines to replace missing position signals? + + % settings for the saccade cutting (see cutSaccades.m for documentation) + me.ETparams.cutPosTraceMode = 1; + me.ETparams.cutVelTraceMode = 1; + me.ETparams.cutSaccadeSkipWindow= 1; % don't cut during first x seconds + me.ETparams.screen.resolution = [ me.resolution(1) me.resolution(2) ]; + me.ETparams.screen.size = [ me.resolution(1)/me.pixelsPerCm/100 me.resolution(2)/me.pixelsPerCm/100 ]; + me.ETparams.screen.viewingDist = me.distance/100; + me.ETparams.screen.dataCenter = [ me.resolution(1)/2 me.resolution(2)/2 ]; % center of screen has these coordinates in data + me.ETparams.screen.subjectStraightAhead = [ me.resolution(1)/2 me.resolution(2)/2 ]; % Specify the screen coordinate that is straight ahead of the subject. Just specify the middle of the screen unless its important to you to get this very accurate! + % change some defaults as needed for this analysis: + me.ETparams.data.alsoStoreComponentDerivs = true; + me.ETparams.data.detrendWithMedianFilter = true; + me.ETparams.data.applySaccadeTemplate = true; + me.ETparams.data.minDur = 100; + me.ETparams.fixation.doClassify = true; + me.ETparams.blink.replaceWithInterp = true; + me.ETparams.blink.replaceVelWithNan = true; + end + me.ETparams.samplingFreq = me.sampleRate; + + % process params + ETparams = prepareParameters(me.ETparams); + + for ii = 1:length(me.trials) + fprintf('--->>> Full saccadic analysis of Trial %i:\n',ii); + x = (me.trials(ii).gx * me.ppd) + ETparams.screen.dataCenter(1); + y = (me.trials(ii).gy * me.ppd) + ETparams.screen.dataCenter(2); + p = me.trials(ii).pa; + t = me.trials(ii).times * 1e3; + + if length(x) < me.ETparams.data.minDur + data = struct([]); + else + data = runNH2010Classification(x,y,p,ETparams,t); + % replace missing data by linearly interpolating position and velocity + % between start and end of each missing interval (so, creating a ramp + % between start and end position/velocity). + data = replaceMissing(data,ETparams.qInterpMissingPos); + end + + % desaccade velocity and/or position + %data = cutSaccades(data,ETparams,cutPosTraceMode,cutVelTraceMode,extraCut,cutSaccadeSkipWindow); + % construct saccade only traces + %data = cutPursuit(data,ETparams,1); + + me.trials(ii).data = data; + + end + + end + % =================================================================== %> @brief %> @@ -1718,14 +1910,14 @@ classdef tobiiAnalysis < analysisCore pb = textprogressbar(length(me.trials),'startmsg','Loading trials to compute microsaccades: ','showactualnum',true); cms = tic; for jj = 1:length(me.trials) - if me.trials(jj).incorrect == true || me.trials(jj).breakFix == true || me.trials(jj).unknown == true; continue; end + if any(isnan(me.trials(jj).timeRange)) || me.trials(jj).incorrect == true || me.trials(jj).breakFix == true || me.trials(jj).unknown == true; continue; end samples = []; sac = []; radius = []; monol=[]; monor=[]; me.trials(jj).msacc = struct(); me.trials(jj).sampleSaccades = []; me.trials(jj).microSaccades = []; samples(:,1) = me.trials(jj).times/1e3; - samples(:,2) = me.trials(jj).gx/me.ppd_; - samples(:,3) = me.trials(jj).gy/me.ppd_; + samples(:,2) = me.trials(jj).gx; + samples(:,3) = me.trials(jj).gy; samples(:,4) = nan(size(samples(:,1))); samples(:,5) = samples(:,4); eye_used = 0; @@ -2047,6 +2239,34 @@ classdef tobiiAnalysis < analysisCore end + % =================================================================== + %> @brief + %> + % =================================================================== + function trialDef = getTrialDef(me) + trialDef = cell2struct(repmat({[]},length(me.trialsTemplate),1),me.trialsTemplate); + trialDef.rt = false; + trialDef.rtoverride = false; + trialDef.firstSaccade = NaN; + trialDef.invalid = false; + trialDef.correct = false; + trialDef.breakFix = false; + trialDef.incorrect = false; + trialDef.unknown = false; + trialDef.sttime = NaN; + trialDef.entime = NaN; + trialDef.totaltime = 0; + trialDef.startsampletime = NaN; + trialDef.endsampletime = NaN; + trialDef.timeRange = [NaN NaN]; + trialDef.rtstarttime = NaN; + trialDef.rtstarttimeOLD = NaN; + trialDef.rtendtime = NaN; + trialDef.synctime = NaN; + trialDef.deltaT = NaN; + trialDef.rttime = NaN; + end + end end diff --git a/calibration/calibrateSize.m b/calibration/calibrateSize.m index b231222ffb101667530c630095701d02c2632394..85225a0b172cdbb2543601731536809406acf487 100644 --- a/calibration/calibrateSize.m +++ b/calibration/calibrateSize.m @@ -66,12 +66,11 @@ try black=BlackIndex(window); % Instructions - s=sprintf('Hold your %.1f-%s-wide object against the display.',objectInches/unitInches,unit); - theText={s,'Use one eye. Drag to match the object''s width.'}; + st=sprintf('Hold your %.1f-%s-wide object against the display.',objectInches/unitInches,unit); + theText={st,'Use one eye. Drag to match the object''s width.'}; Screen('TextFont',window,'Calibri'); - s=18; - Screen('TextSize',window,s); - textLeading=s+10; + Screen('TextSize',window,18); + textLeading=18+10; textRect=Screen('TextBounds',window,theText{1}); textRect(4)=length(theText)*textLeading; textRect=CenterRect(textRect,screenRect); diff --git a/communication/T4-watcher.lua b/communication/T4-watcher.lua index f8548f5fe884889606833f74dd3d099f56c33ed7..691f599a4d5c5b1941e2abdde8b5a4d8939a11f4 100644 --- a/communication/T4-watcher.lua +++ b/communication/T4-watcher.lua @@ -1,35 +1,40 @@ --- T4-watcher.lua -- This waits for new values placed in F32, then sends them as a strobed --- word to EIO as 2ms strobed word or CIO as 300ms TTL, runs on a LabJack T4 --- minifed version use https://mothereff.in/lua-minifier here: -LJ.setLuaThrottle(100)--print ("Current Lua Throttle Setting: ", LJ.getLuaThrottle()) +-- T4-watcher.lua -- This waits for new values placed in I32, then sends them as a strobed +-- word to EIO1:8 CIO1:3 as 2ms strobed word or CIO4 as 10ms TTL, runs on a LabJack T4 +-- minifed version use: https://mothereff.in/lua-minifier +-- V1.02 +LJ.setLuaThrottle(80)--print ("Current Lua Throttle Setting: ", LJ.getLuaThrottle()) local mbRead=MB.R local mbWrite=MB.W local cmd = -1 +local lsb = 0 +local msb = 0 mbWrite(2601,0,255) -- EIO_DIRECTION = output mbWrite(2602,0,255) -- CIO_DIRECTION = output mbWrite(2501,0,0) -- EIO_STATE all LOW mbWrite(2502,0,0) -- CIO_STATE all LOW -mbWrite(46000, 3, 0) -- set USER_RAM0_F32 (address 46000) to 0 +mbWrite(46080, 2, 0) -- set USER_RAM0_I32 (address 46080) to 0 while true do - cmd = mbRead(46000, 3) - if (cmd >= 1 and cmd <= 255) then - mbWrite(2501,0,cmd) -- EIO_STATE set to cmd + cmd = mbRead(46080, 2) + if (cmd >= 1 and cmd <= 2047) then + lsb = bit.band(cmd,0xff) + msb = bit.band(bit.rshift(cmd,8),0xff) + mbWrite(2501,0,lsb) -- EIO_STATE set to lsb + if (msb > 0) then mbWrite(2502,0,msb) end-- CIO1:3_STATE set to msb mbWrite(61590,1,2000) -- WAIT_US_BLOCKING 2000us = 2ms mbWrite(2501,0,0) -- EIO_STATE all LOW - print("HIGH: ", cmd) - elseif (cmd >= 256 and cmd <= 271) then - mbWrite(2502,0,(cmd-256)) - mbWrite(61590,1,100000) -- WAIT_US_BLOCKING max is 100ms, so repeat 3 times for 300ms - mbWrite(61590,1,100000) - mbWrite(61590,1,100000) + if (msb > 0) then mbWrite(2502,0,0) end + --print("HIGH: ", cmd) + elseif (cmd > 2047) then + mbWrite(2502,0,8) + mbWrite(61590,1,10000) -- WAIT_US_BLOCKING max is 100ms mbWrite(2502,0,0) - print("CIO: ", cmd) + --print("CIO4: ", 8) elseif (cmd == 0) then mbWrite(2501,0,0) - print("LOW: ", cmd) + --print("LOW: ", cmd) end if (cmd > -1) then - mbWrite(46000, 3, -1) --reset cmd - print("RESET") + mbWrite(46080, 2, -1) --reset cmd + --print("RESET") end end \ No newline at end of file diff --git a/communication/alyxManager.m b/communication/alyxManager.m new file mode 100644 index 0000000000000000000000000000000000000000..2820817a34b15b8da9068c99c4b22b481eac8c4e --- /dev/null +++ b/communication/alyxManager.m @@ -0,0 +1,1716 @@ +% ======================================================================== +classdef alyxManager < optickaCore +%> @class alyxManager +%> @brief manage connection to an Alyx database +%> +%> Copyright ©2014-2025 Ian Max Andolina — released: LGPL3, see LICENCE.md +% ======================================================================== + + %--------------------PUBLIC PROPERTIES----------% + properties + %> the URL of the ALYX database + baseURL char = 'http://172.16.102.30:8000' + %> the user to login + user char = 'admin' + %> the lab defined in the Alyx database + lab char = 'Lab' + %> the experimental subject + subject char = 'TestSubject' + %> where to save the temporary json files sent via REST + queueDir char = '' + %> if we open a new session this is the URL + sessionURL = [] + %> if we open a new session this is the base session URL + sessionParentURL = [] + %> limit how many values returned + pageLimit = 100 + %> more logging for debugging + verbose = false + end + + %--------------------TRANSIENT PROPERTIES-----------% + properties (Transient = true) + webOptions = weboptions('MediaType','application/json','Timeout',10); + end + + %--------------------DEPENDENT PROTECTED PROPERTIES----------% + properties (GetAccess = public, Dependent = true) + loggedIn + end + + %--------------------TRANSIENT PROTECTED PROPERTIES----------% + properties (Access = protected, Transient = true) + token char = '' + end + + %--------------------PRIVATE PROPERTIES----------% + properties (Access = private) + password char = '' + assignedUser char = '' + AWS_KEY char = '' + AWS_ID char = '' + %> properties allowed to be passed on construction + allowedProperties = {'baseURL','user','lab','subject','queueDir','pageLimit','verbose'} + alfMatch = '(?^[0-9\-]+)_(?\d+)_(?\w+)' + end + + + %======================================================================= + methods %----------------------------PUBLIC METHODS + %======================================================================= + + % =================================================================== + %> @brief Class constructor + %> + %> @param varargin are passed as a structure / cell of properties which is + %> parsed. + %> @return instance of class. + % =================================================================== + function me = alyxManager(varargin) + me=me@optickaCore(varargin); %superclass constructor + me.parseArgs(varargin, me.allowedProperties); + + flushQueue(me,true); + end + + + % =================================================================== + function setSecrets(me, password, AWS_ID, AWS_KEY) + % =================================================================== + arguments + me alyxManager + password char = '' + AWS_ID char = '' + AWS_KEY char = '' + end + + me.assignedUser = me.user; + + if isempty(password) && isempty(me.password) + try password = getSecret('AlyxPassword'); end %#ok<*TRYNC> + end + if isempty(AWS_ID) + try AWS_ID = getSecret('AWS_ID'); end + end + if isempty(AWS_KEY) + try AWS_KEY = getSecret('AWS_KEY'); end + end + + txt = ""; + if ~isempty(password); me.password = password; txt=txt+"password"; end + if ~isempty(AWS_ID); me.AWS_ID = AWS_ID; txt=txt+" AWS_ID";end + if ~isempty(AWS_KEY); me.AWS_KEY = AWS_KEY; txt=txt+" AWS_KEY";end + fprintf('\n≣≣≣≣⊱ alyxManager: set these secrets: %s\n', txt); + + end + + % =================================================================== + function logout(me) + % =================================================================== + if me.loggedIn + me.token = []; + me.sessionURL = []; + me.webOptions.HeaderFields = []; % Remove token from header field + if ~me.loggedIn + fprintf('\n≣≣≣≣⊱ alyxManager: user <<%s>> logged OUT of <<%s>> successfully...\n\n', me.user, me.baseURL); + end + end + end + + % =================================================================== + function success = login(me) + % =================================================================== + success = false; + if me.loggedIn; warning('Already Logged in...'); end + noDisplay = usejava('jvm') && ~feature('ShowFigureWindows'); + if isempty(me.user) + prompt = {'Alyx Username:'}; + if noDisplay + % use text-based alternative + answer = strip(input([prompt{:} ' '], 's')); + else + % use GUI dialog + dlg_title = 'Alyx Login'; + num_lines = 1; + defaultans = {'',''}; + answer = inputdlg(prompt, dlg_title, num_lines, defaultans); + end + if isempty(answer)|| (iscell(answer) && isempty(answer{1})); return; end + + if iscell(answer) + me.user = answer{1}; + else + me.user = answer; + end + end + + if isempty(me.password) + secretUI(me,'password'); + end + + try + me.getToken(me.user, me.password); + fprintf('\n≣≣≣≣⊱ alyxManager: user <<%s>> logged in to <<%s>> successfully...\n\n', me.user, me.baseURL); + success = me.loggedIn; + me.sessionURL = []; + me.sessionParentURL = []; + flushQueue(me, true); + catch ex + %products = ver; + %toolboxes = matlab.addons.toolbox.installedToolboxes; + % Check the correct toolboxes are installed + if strcmp(ex.identifier, 'Alyx:Login:FailedToConnect') + warning('Alyx:LoginFail:FailedToConnect', 'Failed To Connect.') + return + elseif contains(ex.message, 'credentials')||strcmpi(ex.message, 'Bad Request') + warning('Alyx:LoginFail:BadCredentials', 'Unable to log in with provided credentials. Will reset password') + me.pwd = ''; + return + elseif contains(ex.message, 'password')&&contains(ex.message, 'blank') + disp('Password may not be left blank') + else % Another error altogether + rethrow(ex) + end + end + end + + % =================================================================== + function r = hasEntry(me, type, name) + % =================================================================== + %HASENTRY check if an item exists, e.g. + % hasEntry('users','admin') checks if there is a user called + % admin. + [rt, st] = me.getData(type); + if st ~= 200; error('Problem retrieving %s from Alyx',type); end + switch type + case {'subjects'} + rt = {rt(:).nickname}; + case {'tags','tasks','procedures','labs', 'locations', 'projects','data-repository','dataset-types'} + rt = {rt(:).name}; + case {'users'} + rt = {rt(:).username}; + case {'sessions'} + rt = {rt(:).id}; + end + if iscell(rt) && any(contains(name, rt)) + r = true; + else + r = false; + end + end + + % =================================================================== + function [data, statusCode] = getData(me, endpoint, varargin) + % =================================================================== + %GETDATA Return a specific Alyx/REST read-only endpoint + % Makes a request to an Alyx endpoint; returns the data as a MATLAB struct. + % + % Examples: + % sessions = me.getData('sessions') + % sessions = me.getData('https://alyx.cortexlab.net/sessions') + % sessions = me.getData('sessions?type=Base') + % sessions = me.getData('sessions', 'type', 'Base') + % + % See also ALYX, MAKEENDPOINT, REGISTERFILE + if ~me.loggedIn || ~exist('endpoint','var'); return; end + data = []; hasNext = true; page = 1; + isPaginated = @(r)all(isfield(r, {'count', 'next', 'previous', 'results'})); + fullEndpoint = me.makeEndpoint(endpoint); % Get complete URL + options = me.webOptions; + options.MediaType = 'application/x-www-form-urlencoded'; + try + while hasNext + assert(page < me.pageLimit, 'Maximum number of page requests reached') + + result = webread(fullEndpoint, varargin{:}, options); + + if ~isPaginated(result) + data = result; + break + end + data = [data, result.results']; %#ok + hasNext = ~isempty(result.next); + fullEndpoint = result.next; + page = page + 1; + end + statusCode = 200; % Success + return + catch ex + switch ex.identifier + case {'MATLAB:webservices:UnknownHost', 'MATLAB:webservices:Timeout', ... + 'MATLAB:webservices:CopyContentToDataStreamError'} + warning(ex.identifier, '%s', ex.message) + statusCode = 000; + otherwise + response = regexp(ex.message, '(?:the status )(\d{3})', 'tokens'); + statusCode = str2double(me.cellflat(response)); + if statusCode == 403 % Invalid token + warning('Alyx:getData:InvalidToken', 'Invalid token, please re-login') + me.logout; % Delete token + me.login; % Re-login + if me.loggedIn % If succeded + data = me.getData(fullEndpoint); % Retry + end + else + warning(['alyxManager.getData: ' ex.identifier], '%s', ex.message); + end + end + end + end + + % =================================================================== + function [data, statusCode] = postData(me, endpoint, data, requestMethod) + % =================================================================== + %POSTDATA Post any new data to an Alyx/REST endpoint + % Description: Makes a request to an Alyx endpoint with new data as a + % MATLAB struct; returns the JSON response data as a MATLAB struct. + % This function will create a new record by default, if requestMethod is + % undefined. Other methods include 'PUT', 'PATCH and 'DELETE'. + % Example: + % subjects = me.postData('subjects', myStructData, 'post') + % + % See also ALYX, JSONPOST, FLUSHQUEUE, REGISTERFILE, GETDATA + if nargin == 3; requestMethod = 'post'; end % Default request method + assert(any(strcmpi(requestMethod, {'post', 'put', 'patch', 'delete'})),... + '%s not a valid HTTP request method', requestMethod) + + % Create the JSON command + jsonData = jsonencode(data); + + % Make a filename for the current command + queueFilename = [datestr(now, 'yyyy-mm-dd-HH-MM-SS-FFF') '.' lower(requestMethod)]; + queueFullfile = fullfile(me.queueDir, queueFilename); + % If local Alyx queue directory doesn't exist, create one + if ~exist(me.queueDir, 'dir') + me.queueDir = me.paths.parent; + mkdir(me.queueDir); + end + + % Save the endpoint and json locally + fid = fopen(queueFullfile, 'w'); + fprintf(fid, '%s\n%s', endpoint, jsonData); + fclose(fid); + + % Flush the queue + if me.loggedIn + [data, statusCode] = me.flushQueue(); + % Return only relevent data + if numel(statusCode) > 1; statusCode = statusCode(end); end + %if floor(statusCode/100) == 2 && ~isempty(data) + % data = data(end); + %end + else + statusCode = 000; + data = []; + warning('Alyx:flushQueue:NotConnected','Not connected to Alyx - saved in queue'); + end + end + + % =================================================================== + function [sessions, eids] = getSessions(me, varargin) + % =================================================================== + % GETSESSIONS Return sessions and eids for a given search query + % Returns Alyx records for specific refs (eid and/or expRef strings) + % and/or those matching search queries. Values may be char arrays, + % strings, or cell strings. If searching dates, values may also be a + % datenum or array thereof. + % + % Examples: + % sessions = ai.getSessions('cf264653-2deb-44cb-aa84-89b82507028a') + % sessions = ai.getSessions('2018-07-13_1_flowers') + % sessions = ai.getSessions('cf264653-2deb-44cb-aa84-89b82507028a', ... + % 'subject', {'flowers', 'ZM_307'}) + % sessions = ai.getSessions('lab', 'cortexlab', ... + % 'date_range', datenum([2018 8 28 ; 2018 8 31])) + % sessions = ai.getSessions('date', now) + % sessions = ai.getSessions('data', {'clusters.probes', 'eye.blink'}) + % [~, eids] = ai.getSessions(expRefs) + % + % See also ALYX.UPDATESESSIONS, ALYX.GETDATA + + p = inputParser; + if mod(length(varargin),2) % Uneven num args when ref is first input + validationFcn = @(x)(iscellstr(x) || isstring(x) || ischar(x)); + addOptional(p, 'ref', [], validationFcn); + end + % Parse Name-Value paired args + addParameter(p, 'subject', ''); + addParameter(p, 'users', ''); + addParameter(p, 'lab', ''); + addParameter(p, 'date_range', '', 'PartialMatchPriority', 2); + addParameter(p, 'dataset_types', ''); + addParameter(p, 'number', 1); + + [sessions, results, eids] = deal({}); % Initialize as empty + parse(p, varargin{:}) + + % Convert search params back to cell + names = setdiff(fieldnames(p.Results), [{'ref'} p.UsingDefaults]); + % Get values, and if nessesary convert datenums to datestrs + values = cellfun(@processValue, names, 'UniformOutput', 0); + assert(length(names) == length(values)) + queries = cell(length(names)*2,1); + queries(1:2:end) = names; + queries(2:2:end) = values; + + % Get sessions for specified refs + if isfield(p.Results, 'ref') && ~isempty(p.Results.ref) + refs = cellstr(p.Results.ref); + parsedRef = regexp(refs, me.alfMatch, 'names'); + sessFromRef = @(ref)me.getData('sessions/', ... + 'subject', ref.subject, 'date_range', [ref.date ',' ref.date], 'number', ref.seq); + b = cellfun(@isempty, parsedRef); + isRef = ~b; + sessions = [me.mapToCell(@(eid)me.getData(['sessions/' eid]), refs(~isRef))... + me.mapToCell(sessFromRef, parsedRef(isRef))]; + sessions = me.rmEmpty(sessions); + end + + % Do search for other queries + if ~isempty(queries); results = me.getData('sessions', queries{:}); end + % Return on empty + if isempty(sessions) && isempty(results); return; end + sessions = me.catStructs([sessions, me.ensureCell(results)]); + if nargout > 1 + eids = me.url2eid({sessions.url}); + end + function value = processValue(name) + if contains(name,'date') + if isnumeric(p.Results.(name)) + value = me.iff(isscalar(p.Results.(name)), repmat(p.Results.(name),1,2), p.Results.(name)); + value = string(datestr(value, 'yyyy-mm-dd')); + elseif isscalar(string(p.Results.(name))) && ~any(p.Results.(name)==',') + value = repmat(string(p.Results.(name)),2,1); + else + error('Alyx:getSessions:InvalidInput', 'The value of ''date_range'' is invalid') + end + else + value = p.Results.(name); + end + if iscellstr(value)||isstring(value); value = strjoin(value,','); end + end + end + + % =================================================================== + function subjects = listSubjects(me, stock, alive, sortByUser) + % =================================================================== + %ALYX.LISTSUBJECTS Lists recorded subjects + % subjects = ALYX.LISTSUBJECTS([stock, alive, sortByUser]) Lists the + % experimental subjects present in main repository. If logged in, + % returns a subject list generated from Alyx, with the option of + % filtering by stock (default false) and alive (default true). The + % sortByUser flag, when (default) true, returns the list with the user's + % animals at the top. + if nargin < 4; sortByUser = true; end + if nargin < 3; alive = true; end + if nargin < 2; stock = false; end + + if me.loggedIn % user provided an alyx instance + % convert bool to string for endpoint + alive = me.iff(islogical(alive)&&alive, 'True', 'False'); + stock = me.iff(islogical(stock)&&stock, 'True', 'False'); + + % get list of all living, non-stock mice from alyx + s = me.getData(sprintf('subjects?stock=%s&alive=%s', stock, alive)); + + % return on empty + if isempty(s); subjects = {'default'}; return; end + + % get cell array of subject names + subjNames = {s.nickname}; + + if sortByUser + % determine the user for each mouse + respUser = {s.responsible_user}; + + % determine which subjects belong to this user + thisUserSubs = sort(subjNames(strcmp(respUser, me.user))); + + % all the subjects + otherUserSubs = sort(subjNames(~strcmp(respUser, me.user))); + + % the full, ordered list + subjects = [{'default'}, thisUserSubs, otherUserSubs]'; + else + subjects = [{'default'}, subjNames]'; + end + else + % The remote 'main' repositories are the reference for the existence of + % experiments, as given by the folder structure + subjects = []; + end + end + + % =================================================================== + function narrative = updateNarrative(me, comments, endpoint, subject) + % =================================================================== + %UPDATENARRATIVE Update an Alyx session or subject narrative + % Update an Alyx narrative field with comments. If an endpoint is + % specified, the narrative for that record is updated, otherwise the last + % subsession URL is used. If the SessionURL property is empty and no + % endpoint is specified, the narrative field of the subject's Alyx record + % is updated. + % + % NARRATIVE = UPDATENARRATIVE(OBJ) + % If SessionURL is set, display comments dialog (unless Headless flag + % set) and post input to that subsession narrative, otherwise it returns + % an error. + % + % NARRATIVE = UPDATENARRATIVE(OBJ, COMMENTS) + % If SessionURL is set, posts COMMENTS to that subsession narrative, + % otherwise it returns an error. If COMMENTS is empty and not a charector + % array, prompts user for input (unless Headless flag set). + % + % NARRATIVE = UPDATENARRATIVE(OBJ, COMMENTS, ENDPOINT) + % Posts COMMENTS to ENDPOINT. If COMMENTS is empty and not a charector + % array, prompts user for input (unless Headless flag set). + % + % NARRATIVE = UPDATENARRATIVE(OBJ, COMMENTS, ENDPOINT, SUBJECT) + % Posts COMMENTS to ENDPOINT narrative. If ENDPOINT is empty, posts + % COMMENTS to SUBJECT description. + % + % See also ALYX, DAT.UPDATELOGENTRY, EUI.EXPPANEL/SAVELOGENTRY, PUTDATA + + % Validate inputs + if nargin < 2; comments = []; end + if nargin < 4; subject = []; end + + % If no specific endpoint is specified, use the last created subsession + if nargin < 3 + if ~isempty(me.sessionURL) + endpoint = me.sessionURL; + else % Nothing to go on, throw error + error('No endpoint specified and no subsession URL set'); + end + end + + if isempty(comments) && ~isa(comments, 'char') + if ~isempty(subject) && isempty(endpoint) + titleStr = 'Update subject description'; + else + titleStr = 'Update session narrative'; + end + comments = inputdlg('Enter narrative:', titleStr, [10 60]); + end + + session = me.getData(endpoint); + oldnarrative = session.narrative; + try + narrative = deblank(replace(comments, newline, "\n")); + narrative = strjoin(narrative,"\n"); + catch + narrative = ''; + end + if isempty(narrative); return; end + if ~isempty(oldnarrative) + narrative = strjoin([oldnarrative,narrative],"\n"); + end + + if ~isempty(subject) + % Assume post is intended for subject description + warning('Alyx:TODO', 'This feature is not yet implemented') + return + % TODO: retreive subject narrative endpoint, requires endpoint to allow + % PUT requests and /subject=%s option. NB: subject's 'narrative' field + % is called 'description' + else + % Remove trailing whitespaces, and ensure string is 1D. Replace newlines + % with escape charecters + + if iscell(narrative); narrative = narrative{:}; end % Make sure not a cell + try + % Update the record + data = me.postData(endpoint, struct('narrative', narrative), 'patch'); + if ~isempty(data) + narrative = strrep(data.narrative, '\n', newline); + else + error('Alyx:updateNarrative:FailedToUpdate',... + 'Failed to update narrative on Alyx') + end + catch ex + rethrow(ex) + end + end + end + + % =================================================================== + function [url, newSession] = newExp(me, path, sessionID, session, jsonData) + % =================================================================== + %NEWEXP Create a new unique experimental session in the database + % [ref, seq] = NEWEXP(path, sessionID, session, jsonData) + % Create a new experiment: + % + % subject/ + % |_ YYYY-MM-DD/ + % |_ sessionID/ + % + + if nargin < 3; error('Need to pass an ALF PATH, sessionID and session info'); end + if ~exist('jsonData','var'); jsonData = '[]'; end + + % Ensure user is logged in + if ~me.loggedIn; me.login; end + + % check items exists in the database + assert(me.hasEntry('labs',session.labName), 'Alyx:newExp:labNotFound', sprintf('lab "%s" does not exist', session.labName)); + assert(me.hasEntry('subjects',session.subjectName), 'Alyx:newExp:subjectNotFound', sprintf('subject "%s" does not exist', session.subjectName)); + assert(me.hasEntry('users',session.researcherName), 'Alyx:newExp:userNotFound', sprintf('user "%s" does not exist', session.researcherName)); + %assert(me.hasEntry('projects',session.project), 'Alyx:newExp:projectNotFound', sprintf('project "%s" does not exist', session.project)); + %assert(me.hasEntry('procedures',session.procedure), 'Alyx:newExp:procedureNotFound', sprintf('procedure "%s" does not exist', session.procedure)); + %assert(me.hasEntry('locations',session.location), 'Alyx:newExp:locationNotFound', sprintf('location "%s" does not exist', session.location)); + + me.sessionURL = ''; + me.sessionParentURL = ''; + + expDate = char(datetime("now",'Format','yyyy-MM-dd''T''HH:mm:ss')); + dayDate = expDate(1:10); + + % make sure the session is new + request = ['sessions?date_range=' dayDate ',' dayDate '&subject=' session.subjectName '&number=' num2str(sessionID)]; + [sessions, statusCode] = me.getData(request); + + if statusCode == 200 && ~isempty(sessions) + error("There is an existing session for ID = %i!!!", sessionID); + end + + %Now create a new SESSION, using the experiment number + newSession = []; url = []; + d = struct; + d.users = {session.researcherName}; + d.subject = session.subjectName; + try d.lab = session.labName; end + try d.location = session.location; end + try d.projects = {session.project}; end + try d.procedures = {session.procedure}; end + try d.task_protocol = session.taskProtocol; end + d.narrative = ['opticka-generated session: ' path]; + d.number = sessionID; + d.start_time = expDate; + d.type = 'Experiment'; + d.json = ['{ "parameters": ' jsonData ' }']; + + try + [newSession, statusCode] = me.postData('sessions', d, 'post'); + url = newSession.url; + me.sessionURL = url; + catch ME + if (isinteger(statusCode) && statusCode == 503) + warning(ME.identifier, 'Failed to create session file for %i: %s.', sessionID, ME.message) + else % Probably fatal user error + rethrow(ME) + end + end + end + + % =================================================================== + function session = closeSession(me, narrative, QC) + if isempty(me.sessionURL); return; end + if ~exist('narrative','var'); narrative = []; end + if ~exist('QC','var') || isempty(QC); QC = 'NOT_SET'; end + session = []; + [ses, s] = me.getData(me.sessionURL); + + if ~isempty(ses) + if ~isempty(narrative) + me.updateNarrative(narrative); + end + d.qc = QC; + d.end_time = char(datetime("now",'Format','yyyy-MM-dd''T''HH:mm:ss')); + session = me.postData(['sessions/' ses.id], d, 'patch'); + end + end + + % =================================================================== + function registerALF(me, alfDir, sessionURL) + % =================================================================== + %REGISTERALFTOALYX Register files contained within alfDir to Alyx + % This function registers files contained within the alfDir to Alyx. + % Files are only registered if their filenames match a datasetType's + % alf_filename field. Must also provide an alyx session URL. Optionally + % can provide alyxInstance as well. + % + % INPUTS: + % -alfDir: Directory containing ALF files, this will be searched + % recursively for all ALF files which match a datasetType + % -endpoint (optional): Alyx URL of the session to register this data + % to. If none supplied, will use SessionURL in me. If this is unset, + % an error is thrown. + % + % See also ALYX, REGISTERFILES, POSTDATA, HTTP.JSONGET + + if nargin < 3 + if isempty(me.sessionURL) + error('No session URL set') + else + sessionURL = me.sessionURL; + end + end + + assert(exist('dirPlus','file') == 2,... + 'Function ''dirPlus'' not found, make sure alyx helpers folder is added to path') + + %%INPUT VALIDATION + % Validate alfDir path + assert(~contains(alfDir,'/'), 'Do not use forward slashes in the path'); + assert(exist(alfDir,'dir') == 7 , 'alfDir %s does not exist', alfDir); + + % Validate alyxInstance, creating one if not supplied + if ~me.loggedIn; me = me.login; end + + %%Validate that the files within alfDir match a datasetType. + %1) Get all datasetTypes from the database, and list the filename patterns + datasetTypes = me.getData('dataset-types'); + datasetTypes = [datasetTypes{:}]; + datasetTypes_filemasks = {datasetTypes.filename_pattern}; + datasetTypes_filemasks(cellfun(@isempty,datasetTypes_filemasks)) = {''}; %Ensures all entries are character arrays + + %2) Get all the files contained within the alfDir, which match a + %datasetType in the Alyx database + function v = validateFcn(fileObj) + match = regexp(fileObj.name, regexptranslate('wildcard',datasetTypes_filemasks)); + v = ~isempty([match{:}]); + end + alfFiles = dirPlus(alfDir, 'ValidateFileFcn', @validateFcn, 'Struct', true); + assert(~isempty(alfFiles), 'No files within %s matched a datasetType', alfDir); + + %% Define a hierarchy of alfFiles based on the ALF naming scheme: parent.child.* + alfFileParts = cellfun(@(name) strsplit(name,'.'), {alfFiles.name}, 'uni', 0); + alfFileParts = cat(1, alfFileParts{:}); + + %Create parent datasets, which contain no filerecords themselves + [parentTypes, ~, parentID] = unique(alfFileParts(:,1)); + parentURLs = cell(size(parentTypes)); + fprintf('Creating parent datasets... '); + for parent = 1:length(parentTypes) + d = struct('created_by', me.User,... + 'dataset_type', parentTypes{parent},... + 'session', sessionURL,... + 'data_format', 'notData'); + w = me.postData('datasets',d); + parentURLs{parent} = w.url; + end + + %Now go through each file, creating a dataset and filerecord for that file + for file = 1:length(alfFiles) + fullPath = fullfile(alfFiles(file).folder, alfFiles(file).name); + fileFormat = alfFileParts{file,3}; + parentDataset = parentURLs{parentID(file)}; + + datasetTypes_filemasks(contains(datasetTypes_filemasks,'*.*')) = []; % Remove parant datasets from search + matchIdx = regexp(alfFiles(file).name, regexptranslate('wildcard', datasetTypes_filemasks)); + matchIdx = find(~cellfun(@isempty, matchIdx)); + assert(isscalar(matchIdx), 'Insufficient/Too many matches of datasetType for file %s', alfFiles(file).name); + datasetType = datasetTypes(matchIdx).name; + + me.registerFile(fullPath, fileFormat, sessionURL, datasetType, parentDataset); + + fprintf('Registered file %s as datasetType %s\n', alfFiles(file).name, datasetType); + end + + %% Alyx-dev + return + try %#ok + %Get datarepositories and their base paths + repo_paths = cellfun(@(r) r.name, me.getData('data-repository'), 'uni', 0); + + %Identify which repository the filePath is in + which_repo = cellfun( @(rp) contains(alfDir, rp), repo_paths); + assert(sum(which_repo) == 1, 'Input filePath\n%s\ndoes not contain the a repository path\n', alfDir); + + %Define the relative path of the file within the repo + dnsId = regexp(alfDir, ['(?<=' repo_paths{which_repo} '.*)\\?'], 'once')+1; + relativePath = alfDir(dnsId:end); + + me.BaseURL = 'https://alyx-dev.cortexlab.net'; + % subject = regexpi(relativePath, '(?<=Subjects\\)[A-Z_0-9]+', 'match'); + + % D.subject = subject{1}; + D.filenames = {alfFiles.name}; + D.path = alfDir; + % D.exists_in = repo_paths{which_repo}; + + me.postData('register-file', D); + catch ex + warning(ex.identifier, '%s', ex.message) + end + me.BaseURL = 'https://alyx.cortexlab.net'; + end + + % =================================================================== + function [fullpath, filename, fileID, records] = expFilePath(me, varargin) + % =================================================================== + %EXPFILEPATH Full path for file pertaining to designated experiment + % Returns the path(s) that a particular type of experiment file should be + % located at for a specific experiment. NB: Unlike dat.expFilePath, this + % CAN NOT be used to determine where a file should be saved to. This + % function only returns existing file records from Alyx. There may be + % files that exist but aren't on Alyx and likewise, may not exist but are + % still on Alyx. + % + % e.g. to get the paths for an experiments 2 photon TIFF movie: + % ALYX.EXPFILEPATH('mouse1', datenum(2013, 01, 01), 1, 'block'); + % + % [full, filename] = expFilePath(ref, type[, user, reposlocation]) + % [full, filename] = expFilePath(subject, date, seq, type[, user, reposlocation]) + % + % + % You specify: + % - subject/ref: a string with the subject name or an experiment + % reference + % - date: a string in 'yyyy-mm-dd', 'yyyymmdd' or 'yyyy-mm-ddTHH:MM:SS' + % format, or a datenum + % - seq: an integer number of the experiment you want + % - type: a case-insensitive string specifying which file you want, e.g. 'Block'. Must + % be a valid dataset type on Alyx (see /dataset-types) + % - user: optional string argument specifying the user who created the files + % - reposlocation: optional case-insensitive string argument specifying + % the location of the files e.g. 'zubjects'. Must be a valid data + % repository on Alyx (see /data-repository) + % + % Outputs: + % - fullpath: the full file paths of the files + % - filename: the names of the files + % - uuid: the Alyx ids of the files + % - records: the complete records returned by Alyx + % + % If more than one matching paths are found, output argument filePath + % will be a cell array of strings, otherwise just a string. + + % Validate input + assert(nargin > 2, 'Error: Not enough arguments supplied.') + + % Flag for searching by session start time, rather than dataset created + % time (see below) + strictSearch = true; + + parsed = regexp(varargin{1}, me.alfMatch, 'tokens'); + if isempty(parsed) % Subject, not ref + subject = varargin{1}; + expDate = varargin{2}; + seq = varargin{3}; + type = varargin{4}; + varargin(1:4) = []; + else % Ref, not subject + subject = parsed{1}{3}; + expDate = parsed{1}{1}; + seq = parsed{1}{2}; + type = varargin{2}; + varargin(1:2) = []; + end + + % Check date + if ~ischar(expDate) + expDate = datestr(expDate, 'yyyy-mm-dd'); + elseif ischar(expDate) && length(expDate) > 10 + expDate = expDate(1:10); + end + + if length(varargin) > 1 % Repository location defined + user = varargin{1}; + location = varargin{2}; + % Validate repository + repos = catStructs(me.getData('data-repository')); + idx = strcmpi(location, {repos.name}); + assert(any(idx), 'Alyx:expFilePath:InvalidType', ... + 'Error: ''%s'' is an invalid data set type', location) + location = repos(idx).name; % Ensures correct case + elseif ~isempty(varargin) + user = varargin{1}; + location = []; + else + location = []; + user = ''; + end + + % Validate type + dataSets = catStructs(me.getData('dataset-types')); + idx = strcmpi(type, {dataSets.name}); + assert(any(idx), 'Alyx:expFilePath:InvalidType', ... + 'Error: ''%s'' is an invalid data set type', type) + type = dataSets(idx).name; % Ensures correct case + + % Construct the endpoint + % FIXME: datasets endpoint filters no longer work + % @body because of this we must make a seperate query to obtain the + % datetime. Querying the sessions takes around 3 seconds. Otherwise we + % filter by created time under the assumption that the dataset was created + % on the same day as the session. See https://github.com/cortex-lab/alyx/issues/601 + if strictSearch + endpoint = sprintf(['/datasets?'... + 'subject=%s&'... + 'experiment_number=%s&'... + 'dataset_type=%s&'... + 'created_by=%s'],... + subject, num2str(seq), type, user); + records = me.getData(endpoint); + if ~isempty(records) + sessions = me.getSessions(me.url2eid({records.session})); + records = records(floor(me.datenum({sessions.start_time})) == datenum(expDate)); + end + else + endpoint = sprintf(['/datasets?'... + 'subject=%1$s&'... + 'experiment_number=%2$s&'... + 'dataset_type=%3$s&'... + 'created_by=%4$s&'... + 'created_datetime_gte=%5$s&'... + 'created_datetime_lte=%5$s'],... + subject, num2str(seq), type, user, expDate); + records = me.getData(endpoint); + end + % Construct the endpoint + % endpoint = sprintf('/datasets?subject=%s&date=%s&experiment_number=%s&dataset_type=%s&created_by=%s',... + % subject, expDate, num2str(seq), type, user); + % records = me.getData(endpoint); + + if ~isempty(records) + data = me.catStructs(records); + fileRecords = me.catStructs([data(:).file_records]); + else + fullpath = []; + filename = []; + fileID = []; + return + end + + if ~isempty(location) + % Remove records in unwanted repo locations + idx = strcmp({fileRecords.data_repository}, location); + fileRecords = fileRecords(idx); + end + + % Get the full paths + mkPath = @(x) me.iff(isempty(x.data_url), ... % If data url not present + [x.data_repository_path x.relative_path], ... % make path from repo path and relative path + x.data_url); % otherwise use data_url field + % Make paths + fullpath = arrayfun(mkPath, fileRecords, 'uni', 0); + filename = {data.name}; + fileID = {fileRecords.id}; + + % If only one record was returned, don't return a cell array + if isscalar(fullpath) + fullpath = fullpath{1}; + filename = filename{1}; + fileID = fileID{1}; + end + end + + % =================================================================== + function [datasets] = registerFile(me, rFiles) + % =================================================================== + %REGISTERFILE Registers filepath(s) to Alyx. data is a struct + %that follows https://openalyx.internationalbrainlab.org/docs/#register-file + % + % name = alyx repository + % filenames = 1+ file names as a cell array + % path = subject/date/number + % created_by = user + % hashes = SHA1 hashes + % bytes = bytes + + arguments(Input) + me + rFiles struct + end + + if isempty(fieldnames(rFiles)); warning('alyxManager.register file incorrect input'); end + + [datasets, statusCode] = me.postData('register-file', rFiles); + + if statusCode ~= 201 + warning('HTTP Error %i -- No file registering, return %s', statusCode, datasets) + end + + end + + % =================================================================== + function [datasets, filenames] = registerALFFiles(me, paths, session) + %> @fn registerALFFiles + %> + %> registers all files in an ALF path to Alyx + %> + %> @param + % =================================================================== + arguments(Input) + me + paths struct + session struct + end + arguments(Output) + datasets struct + filenames cell + end + + sFiles = dir(paths.ALFPath); + sFiles = sFiles(~[sFiles(:).isdir]); + for ii = 1:length(sFiles) + filenames{ii} = fullfile(sFiles(ii).folder, sFiles(ii).name); + bytes(ii) = sFiles(ii).bytes; + hashes{ii} = DataHash(filenames{ii},'file','sha1'); + end + + rf = struct('created_by',session.researcherName); + rf.name = session.dataBucket; + rf.path = paths.ALFKeyShort; + rf.filenames = filenames; + rf.hashes = hashes; + rf.bytes = bytes; + rf.labs = session.labName; + + datasets = me.registerFile(rf); + + end + + % =================================================================== + function initDatabase(me) + % =================================================================== + % add datasets types and species to the database + [r, s] = getData(me,'dataset-types/opticka.raw'); + if s ~= 200 + d = struct; + d.name = 'opticka.raw'; + d.created_by = me.user; + d.description = "Opticka raw mat file for single session"; + d.filename_pattern = "opticka.raw.*.mat"; + [r, s] = me.postData('dataset-types', d, 'post'); + if isempty(r) + warning("Couldn't create opticka.raw dataset type"); + end + end + % add datasets types and species to the database + [r, s] = getData(me,'dataset-types/opticka.details'); + if s ~= 200 + d = struct; + d.name = 'opticka.details'; + d.created_by = me.user; + d.description = "JSON of experiment details"; + d.filename_pattern = "opticka.details.*.json"; + [r, s] = me.postData('dataset-types', d, 'post'); + if isempty(r) + warning("Couldn't create events.table dataset type"); + end + end + % add datasets types and species to the database + [r, s] = getData(me,'dataset-types/events.table'); + if s ~= 200 + d = struct; + d.name = 'events.table'; + d.created_by = me.user; + d.description = "Events table with HED tag mapping"; + d.filename_pattern = "events.table.*.tsv"; + [r, s] = me.postData('dataset-types', d, 'post'); + if isempty(r) + warning("Couldn't create events.table dataset type"); + end + end + [r, s] = getData(me,'dataset-types/eyetracking.raw.tobii'); + if s ~= 200 + d = struct; + d.name = 'eyetracking.raw.tobii'; + d.created_by = me.user; + d.description = "Raw Tobii eyetracking data"; + d.filename_pattern = "eyetracking.raw.tobii*.mat"; + [r, s] = me.postData('dataset-types', d, 'post'); + if isempty(r) + warning("Couldn't create eyetracking.raw.tobii*.mat dataset type"); + end + end + [r, s] = getData(me,'dataset-types/eyetracking.raw.irec'); + if s ~= 200 + d = struct; + d.name = 'Raw iRec eyetracking data'; + d.created_by = me.user; + d.description = "Raw iRec eyetracking data"; + d.filename_pattern = "eyetracking.raw.irec*"; + [r, s] = me.postData('dataset-types', d, 'post'); + if isempty(r) + warning("Couldn't create eyetracking.raw.irec dataset type"); + end + end + [r, s] = getData(me,'dataset-types/eyetracking.raw.eyelink'); + if s ~= 200 + d = struct; + d.name = 'Raw iRec eyetracking data'; + d.created_by = me.user; + d.description = "Raw Eyelink EDF file"; + d.filename_pattern = "eyetracking.raw.eyelink*.edf"; + [r, s] = me.postData('dataset-types', d, 'post'); + if isempty(r) + warning("Couldn't create eyetracking.raw.irec dataset type"); + end + end + end + + % =================================================================== + function bool = get.loggedIn(me) + % =================================================================== + bool = ~isempty(me.user) && ~isempty(me.token); + end + + % =================================================================== + function set.queueDir(me, qDir) + % =================================================================== + %SET.QUEUEDIR Ensure directory exists + if ~exist(qDir, 'dir'); mkdir(qDir); end + me.queueDir = qDir; + end + + % =================================================================== + function set.baseURL(me, value) + % =================================================================== + % Drop trailing slash and ensure protocol defined + if isempty(value); me.baseURL = ''; return; end % return on empty + if ~matches(value(1:4), 'http'); value = ['https://' value]; end + if value(end)=='/'; me.baseURL = value(1:end-1); else; me.baseURL = value; end + end + + % =========================end========================================== + function set.user(me, value) + % =================================================================== + if ~matches(me.user, value) + if me.loggedIn; logout(me); end + me.password = ''; + fprintf('≣≣≣≣⊱ User name change, removed old password!\n'); + end + me.user = value; + end + + %======================================================================= + end %---END PUBLIC METHODS---% + %======================================================================= + + %======================================================================= + methods (Hidden = true) + %======================================================================= + + % =================================================================== + % GETSECRETS Function to retrieve user secrets + % + % Input Arguments: + % me - object containing user credentials + % + % Output Arguments: + % secrets - structure containing user credentials + function secrets = getSecrets(me) + % Extract user credentials from the object + secrets.user = me.user; + secrets.password = me.password; + secrets.AWS_KEY = me.AWS_KEY; + secrets.AWS_ID = me.AWS_ID; + end + + % =================================================================== + % SECRETUI Function to securely handle password input for the alyxManager + % + % Input Arguments: + % me - Instance of alyxManager + % field - Field name to store the password, default is 'password' + function secretUI(me,field) + arguments (Input) + me alyxManager + field char = 'password' + end + switch field + case 'password' + secret = 'AlyxPassword'; + case {'AWS_ID', 'AWS_KEY'} + secret = field; + otherwise + return + end + if noDisplay + % Temporarily disable diary logging to avoid storing the password + diaryState = get(0, 'Diary'); + diary('off'); % At minimum we can keep out of diary log file + passwd = input('Alyx password **INSECURE**: ', 's'); + me.(field) = passwd; % Store the entered password in the object + diary(diaryState); % Restore the previous diary state + else + setSecret(secret, true); % Get secret through a user interface + me.(field) = getSecret(secret); % Store the entered password in the object + end + end + + %======================================================================= + end %---END HIDDEN METHODS---% + %======================================================================= + + %======================================================================= + methods ( Access = protected ) %-------PRIVATE (protected) METHODS-----% + %======================================================================= + + % =================================================================== + function [me, statusCode] = getToken(me, username, password) + %GETTOKEN Acquire an authentication token for Alyx + % Makes a request for an authentication token to an Alyx instance; + % returns the token and status code. + % + % Example: + % statusCode = getToken('https://alyx.cortexlab.net', 'max', '123') + % + % See also ALYX, LOGIN + [statusCode, responseBody] = me.jsonPost('auth-token',... + ['{"username":"', username, '","password":"', password, '"}']); + if statusCode == 200 + me.token = responseBody.token; + me.user = username; + me.assignedUser = me.user; + % Add the token to the authorization header field + me.webOptions.HeaderFields = {'Authorization', ['Token ' me.token]}; + % Flush the local queue on successful login + me.flushQueue(); + elseif statusCode == 000 + me.assignedUser = ''; + me.token = ''; + error('Alyx:Login:FailedToConnect', responseBody) + elseif statusCode == 400 && isempty(password) + me.assignedUser = ''; + me.token = ''; + error('Alyx:Login:PasswordEmpty', 'Password may not be left blank') + else + me.assignedUser = ''; + me.token = ''; + error(responseBody) + end + end + + % =================================================================== + function fullEndpoint = makeEndpoint(me, endpoint) + % =================================================================== + assert(~isempty(endpoint)... + && (ischar(endpoint) || isStringScalar(endpoint))... + && endpoint ~= "", ... + 'Alyx:makeEndpoint:invalidInput', 'Invalid endpoint'); + if startsWith(endpoint, 'http') + % this is a full url already + fullEndpoint = endpoint; + else + fullEndpoint = [me.baseURL, '/', char(endpoint)]; + if isstring(endpoint) + fullEndpoint = string(fullEndpoint); + end + end + % drop trailing slash + fullEndpoint = strip(fullEndpoint, '/'); + end + + % =================================================================== + function [statusCode, responseBody] = jsonPost(me, endpoint, jsonData, requestMethod) + % =================================================================== + %JSONPOST Makes POST, PUT and PATCH requests to endpoint with a JSON request body + % Makes a POST request, with a JSON request body (`Content-Type: application/json`), + % and asking for a JSON response (`Accept: application/json`). + % + % Inputs: + % endpoint - REST API endpoint to make the request to + % requestBody - String to use as request body + % requestMethod - String indicating HTTP request method, i.e. 'POST' + % (default), 'PUT', 'PATCH' or 'DELETE' + % + % Output: + % statusCode - Integer response code + % responseBody - String response body or data struct + % + % See also JSONGET, JSONPUT, JSONPATCH + + % Validate the inputs + endpoint = me.makeEndpoint(endpoint); % Ensure absolute URL + if nargin == 3; requestMethod = 'post'; end % Default request method + assert(any(strcmpi(requestMethod, {'post', 'put', 'patch', 'delete'})),... + '%s not a valid HTTP request method', requestMethod) + % Set the HTTP request method in options + options = me.webOptions; + options.RequestMethod = lower(requestMethod); + + try % Post data + responseBody = webwrite(endpoint, jsonData, options); + if endsWith(endpoint,'auth-token') + statusCode = 200; + else + statusCode = 201; + end + catch ex + disp("=== Response Body:") + disp(responseBody) + getReport(ex) + responseBody = ex.message; + switch ex.identifier + case 'MATLAB:webservices:UnknownHost' + % Can't resolve URL + warning(ex.identifier, '%s Posting temporarily supressed', ex.message) + statusCode = 000; + case {'MATLAB:webservices:CopyContentToDataStreamError' + 'MATLAB:webservices:SSLConnectionFailure' + 'MATLAB:webservices:Timeout'} + % Connection refused or timed out, set as headless and continue on + warning(ex.identifier, '%s. Posting temporarily supressed', ex.message) + statusCode = 000; + otherwise + response = regexp(ex.message, '(?:the status )(?\d{3}).*"(?.+)"', 'names'); + if ~isempty(response) + statusCode = str2double(response.status); + responseBody = response.message; + else + statusCode = 000; + responseBody = ex.message; + end + end + end + end + + % =================================================================== + function [data, statusCode] = flushQueue(me, dontSend) + % =================================================================== + % FLUSHQUEUE Checks for and uploads queued data to Alyx + % Checks all .post and .put files in me.QueueDir and tries to post/put + % them to the database. If the upload is successfull, the queued file is + % deleted. If an error is returned the queued file is also deleted, + % unless it was a server error. + % + % Status codes: + % 200: Upload success - delete from queue + % 300: Redirect - delete from queue + % 400: User error - delete from queue + % 403: Invalid token - delete from queue + % 500: Server error - save in queue + % + % See also ALYX, ALYX.JSONPOST + if ~exist('dontSend','var'); dontSend = false; end + + if ~exist(me.queueDir,'dir') + me.queueDir = me.paths.parent; + end + + try + % Get all currently queued posts, puts, etc. + alyxQueue = [dir([me.queueDir filesep '*.post']);... + dir([me.queueDir filesep '*.put']);... + dir([me.queueDir filesep '*.patch'])]; + alyxQueueFiles = sort(cellfun(@(x) fullfile(me.queueDir, x), {alyxQueue.name}, 'uni', false)); + end + if isempty(alyxQueueFiles); return; end + + if dontSend + for curr_file = 1:length(alyxQueueFiles) + delete(alyxQueueFiles{curr_file}); + end + return + end + % Loop through all files, attempt to put/post + statusCode = ones(1,length(alyxQueueFiles))*401; % Initialize with user error code + data = cell(1,length(alyxQueueFiles)); + for curr_file = 1:length(alyxQueueFiles) + [~, ~, uploadType] = fileparts(alyxQueueFiles{curr_file}); + fid = fopen(alyxQueueFiles{curr_file}); + % First line is the endpoint + endpoint = fgetl(fid); + % Rest of the text is the JSON data + jsonData = fscanf(fid,'%c'); + fclose(fid); + + try + [statusCode(curr_file), responseBody] = me.jsonPost(endpoint, jsonData, uploadType(2:end)); + % [statusCode(curr_file), responseBody] = http.jsonPost(me.makeEndpoint(endpoint), jsonData, 'Authorization', ['Token ' me.Token]); + switch floor(statusCode(curr_file)/100) + case 2 + % Upload success - delete from queue + data{curr_file} = responseBody; + delete(alyxQueueFiles{curr_file}); + disp([int2str(statusCode(curr_file)) ' Success, uploaded to Alyx: ' jsonData]) + case 3 + % Redirect - delete from queue + data{curr_file} = responseBody; + delete(alyxQueueFiles{curr_file}); + disp([int2str(statusCode(curr_file)) ' Redirect, uploaded to Alyx: ' jsonData]) + case 4 + if statusCode(curr_file) == 403 % Invalid token + me.logout; % delete token + if ~me.headless % if user can see dialog... + me.login; % prompt for login + [data, statusCode] = me.flushQueue; % Retry + else % otherwise - save in queue + warning('Alyx:flushQueue:InvalidToken', '%s (%i): %s saved in queue',... + responseBody, statusCode(curr_file), alyxQueue(curr_file).name) + end + else % User error - delete from queue + delete(alyxQueueFiles{curr_file}); + warning('Alyx:flushQueue:BadUploadCommand', '%s (%i): %s',... + responseBody, statusCode(curr_file), alyxQueue(curr_file).name) + end + case 5 + % Server error - save in queue + warning('Alyx:flushQueue:InternalServerError', '%s (%i): %s saved in queue',... + responseBody, statusCode(curr_file), alyxQueue(curr_file).name) + end + catch ex + if strcmp(ex.identifier, 'MATLAB:weboptions:unrecognizedStringChoice') + warning('Alyx:flushQueue:MethodNotSupported',... + '%s method not supported', upper(uploadType(2:end))); + else + % If the JSON command failed (e.g. internet is down) + warning('Alyx:flushQueue:NotConnected', 'Alyx upload failed - saved in queue'); + end + end + end + data = me.cellflat(data(~cellfun('isempty',data))); % Remove empty cells + data = me.catStructs(data); % Convert cell array into struct + end + + % =================================================================== + %> @fn Delete method + %> + %> @param me + %> @return + % =================================================================== + function delete(me) + if me.verbose; fprintf('≣≣≣≣⊱ Delete: %s\n',me.fullName); end + end + + %======================================================================= + end %---END PRIVATE METHODS---% + %======================================================================= + + %======================================================================= + methods ( Static ) %----------STATIC METHODS + %======================================================================= + + % =================================================================== + function [a, wrapped] = ensureCell(a) + % =================================================================== + %ENSURECELL If arg not already a cell array, wrap it in one + if ~iscell(a);a = {a};wrapped = true; else; wrapped = false;end + end + + % =================================================================== + function flat = cellflat(c) + % =================================================================== + flat = {}; + for i = 1:numel(c) + elem = c{i}; + if iscell(elem); elem = alyxManager.cellflat(elem); end + if isempty(elem); elem = {elem}; end + flat = [flat; alyxManager.ensureCell(elem)]; + end + end + + % =================================================================== + function passed = rmEmpty(A) + % =================================================================== + if iscell(A) + empty = cellfun('isempty', A); + else + empty = arrayfun(@isempty, A); + end + passed = A(~empty); + end + + % =================================================================== + function [C1, varargout] = mapToCell(f, varargin) + % =================================================================== + %MAPTOCELL Like cellfun and arrayfun, but always returns a cell array + % [C1, ...] = MAPTOCELL(FUN, A1, ...) applies the function FUN to + % each element of the variable number of arrays A1, A2, etc, passed in. The + % outputs of FUN are used to build up cell arrays for each output. + % + % Unlike MATLAB's cellfun and arrayfun, MAPTOCELL(..) can take a mixture + % of standard and cell arrays, and will always output a cell array (which + % for cellfun and array requires the 'UniformOutput' = false flag). + nelems = numel(varargin{1}); + % ensure all input array arguments have the same size (todo: check shape) + assert(all(nelems == cellfun(@numel, varargin)),'Not all arrays have the same number of elements'); + inSize = size(varargin{1}); + nout = max(nargout, min(nargout(f), 1)); + % function that converts non-cell arrays to cell arrays + ensureCell = @(a) alyxManager.iff(~iscell(a), @() num2cell(a), a); + % make sure all input arguments are cell arrays... + varargin = cellfun(ensureCell, varargin, 'UniformOutput', false); + % ...so now we can concatenate them and treat them as cols in a table and + % read them row-wise + catarrays = cat(ndims(varargin{1}), varargin{:}); + linarrays = reshape(catarrays, nelems, numel(varargin)); + fout = cell(nout, 1); + arg = cell(nout, nelems); + % iterate over each element of input array(s), apply f, and save each (variable + % number of) output. + for i = 1:nelems + [fout{1:nout}] = f(linarrays{i,:}); + arg(1:nout,i) = fout(1:nout); + end + varargout = cell(nargout - 1, 1); + for i = 1:nout + if i == 1 + C1 = reshape(arg(i,:), inSize); + else + varargout{i - 1} = reshape(arg(i,:), inSize); + end + end + end + + % =================================================================== + function s = catStructs(cellOfStructs, missingValue) + % =================================================================== + %CATSTRUCTS Concatenates different structures into one structure array + % s = catStructs(cellOfStructs, [missingValue]) + % Returns a non-scalar structure made from concatinating the structures + % in `cellOfStructs` and optionally replacing any missing values. NB: all + % empty values in the output struct are replaced by `missingValue`, + % including ones present in the original input. + if nargin < 2; missingValue = []; end + fields = unique(alyxManager.cellflat(alyxManager.mapToCell(@fieldnames, cellOfStructs))); + function t = valueTable(s) + if ~isrow(s); s = reshape(s, 1, []); end + t = alyxManager.mapToCell(@(f) alyxManager.pick(s, f, 'cell', 'def', missingValue), fields); + t = vertcat(t{:}); + end + values = alyxManager.mapToCell(@valueTable, cellOfStructs); + values = horzcat(values{:}); + if numel(values) > 0; s = cell2struct(values, fields); else; s = struct([]); end + end + + % =================================================================== + function v = pick(from, key, varargin) + % =================================================================== + %PICK Retrieves indexed elements from a data structure + % Encapsulates different MATLAB syntax for retreival from data structures + % + % * For arrays, numeric keys mean indices: + % v = PICK(arr, idx) returns values at the specified indices in 'arr', e.g: + % PICK(2:2:10, [1 2 4]) % like arg1([1 2 4]) returns [2,4,8] + % + % * For structs & class objects and string key(s), fetch value of the + % struct's field or object's property: + % v = PICK(s, f) returns the value of field 'f' in structure 's' (or + % property 'f' in object 's'). If 's' is a structure or object array, + % return an array of each elements field or property value. e.g: + % s.a = 1; s.b = 3; + % PICK(s, 'a') % like s.a, returns 1 + % PICK(s, {'a' 'b'}) % selecting two fields, returns {[1] [2]} + % s(2).a = 2; s(2).b = 4; + % PICK(s, 'a') % like [s.a], returns [1 2] + % PICK(s, {'a' 'b'}) % selecting two fields, returns {[1 2] [3 4]} + % + % * For containers.Map object's with a valid key type, get keyed value: + % v = PICK(map, key) returns the value in 'map' of the specified 'key' + % If key is an array of keys (with valid types for that map), return a + % cell array with each element retreived by the corresponding key. e.g: + % m = containers.Map; + % m('number') = 1 + % m('word') = 'apple' + % PICK(m, 'word') % like m('word'), returns 'apple' + % PICK(m, {'word' 'number'}) % returns {'apple' [1]} + % + % When picking from structs, objects and maps, you can + % also specify a default value to be used when the specified key does not + % exist in your data structure: pass in a pair of parameters, 'def' + % followed by the default value (e.g. see (2) below). + % + % Finally, you can pass in the option 'cell', to return a cell array + % instead of a standard array (or scalar). This is useful e.g. if you are + % picking from fields containing strings in a struct array: + % w(1).a = 'hello'; w(2).a = 'goodbye'; + % PICK(w, 'a', 'cell') % like {w.a}, returns {'hello' 'goodbye'} + % + % Why is all this useful? A few reasons: + % 1) If a function returns an array or a structure, MATLAB does not allow + % you to use standard syntax to index it from the function call: + % e.g. you might only want the third element from some computation: + % fft(x)(2) % does not work, must do: + % y = fft(x); + % y(2) % ewww, a whole extra line! Try: + % y = PICK(fft(x), 2) % tidier, no? + % 2) Defaults are super useful & succint. e.g. default values for settings: + % e.g. settings = load('settings'); + % % now normally I have to say: + % if ~isfield(settings, 'dataPath') + % mypath = 'default/path'; % default value + % else + % mypath = settings.dataPath; + % end % pretty tedious, when we could just do: + % mypath = PICK(settings, 'dataPath, 'def', 'default/path') % yay! + % 3) Make code flexible without repetition. If you want code that can + % e.g. retrieve a bunch of data from some structure and process it, you + % might want it to be able to handle retrieving from a matrix or a cell + % array, but without all the 'if iscell(blah) blah{i} else blah(i)'. With + % PICK you can handle many different data structures with one function call. + if iscell(key) + v = mapToCell(@(k) pick(from, k, varargin{:}), key); + else + stringArgs = cellfun(@ischar, varargin); %string option params + [withDefault, default] = alyxManager.namedArg(varargin, 'def'); + cellOut = any(strcmpi(varargin(stringArgs), 'cell')); + if isa(from, 'containers.Map') + %% Handle MATLAB maps with key meaning key! + v = me.iff(withDefault && ~from.isKey(key), default, @() from(key)); + elseif ischar(key) + %% Handle structures and class objects with key meaning field/property + if ~iscell(from) + if cellOut + if ~withDefault + v = reshape({from.(key)}, size(from)); + elseif withDefault && (isfield(from, key) || isAProp(from, key)) + % create cell array, then replace empties with default value + v = reshape({from.(key)}, size(from)); + [v{cellfun(@isempty, v)}] = deal(default); + else + % default but field or property does not exist + v = repmat({default}, size(from)); + end + else + if ~withDefault + if isscalar(from) + v = from.(key); + else + v = reshape([from.(key)], size(from)); + end + else + % if using default but with default array output, first get cell + % output with defaults applied, then convert back to a MATLAB array: + asCell = alyxManager.pick(from, key, varargin{:}, 'cell'); + % v = cell2mat( pick(from, key, varargin{:}, 'cell')); + % cell2mat doesn't process single element cells if they contain a + % cell or string, so we short circuit ourselves in this case: + v = iff(isscalar(asCell), @() asCell{1}, @() cell2mat(asCell)); + end + end + else + if cellOut + % The following line was changed 2019-08 + % v = mapToCell(@(e) pick(pick(e, key, varargin{:}), 1), from); + v = me.mapToCell(@(e) me.pick(e, key, varargin{:}), from); + else + v = cellfun(@(e) me.pick(e, key, varargin{:}), from); + end + end + elseif iscell(from) + %% Handle cell arrays with key meaning indices + if cellOut + v = from(key); + else + v = [from{key}]; + end + else + v = from(key); + if cellOut + v = num2cell(v); + end + end + end + + function b = isAProp(v, name) + if isstruct(v) || isempty(v) + b = false; + else + b = isprop(v, name); + end + end + + end + + % =================================================================== + function [present, value, idx] = namedArg(args, name) + % =================================================================== + %NAMEDARG Returns value from name,value argument pairs + % [present, value, idx] = NAMEDARG(args, name) returns flag for presence + % and value of the argument 'name' in a list potentially containing + % adjacent (name, value) pairs. Also returns the index of 'name'. + matches = @(s) (ischar(s) || isStringScalar(s)) && strcmpi(s, name); + idx = find(cellfun(matches, args), 1); + if ~isempty(idx) + present = true; + value = args{idx + 1}; + else + present = false; + value = nil; + end + end + + % =================================================================== + function eid = url2eid(url) + % =================================================================== + % URL2EID Return eid portion of Alyx URL + % Provided a url (or array thereof) returns the eid portion. + % + % Example: + % url = + % 'https://www.url.com/sessions/bc93a3b2-070d-47a8-a2b8-91b3b6e9f25c'; + % eid = Alyx.url2eid(url) + % + % See also ALYX.MAKEENDPOINT + if iscell(url) + eid = mapToCell(@Alyx.url2eid, url); + return + end + + eid_length = 36; % Length of our uuids + % Ensure url longer than minimum length + assert(numel(url) >= eid_length, ... + 'Alyx:url2Eid:InvalidURL', 'URL may not contain eid') + + % Remove trailing slash if present + url = strip(url, 'right', '/'); + % Get eid component of url + % eid = mapToCell(@(str)str(end-eid_length+1:end), url); + eid = url(end-eid_length+1:end); + end + + % =================================================================== + function [ref, AlyxInstance] = parseAlyxInstance(varargin) + % =================================================================== + %PARSEALYXINSTANCE Converts input to string for UDP message and back + % [UDP_string] = ALYX.PARSEALYXINSTANCE(ref, AlyxInstance) + % [ref, AlyxInstance] = ALYX.PARSEALYXINSTANCE(UDP_string) + % + % AlyxInstance should be an Alyx object. + % + % See also SAVEOBJ, LOADOBJ + if nargin > 1 % in [ref, AlyxInstance] + ref = varargin{1}; % extract expRef + ai = varargin{2}; % extract AlyxInstance struct + if isa(ai, 'Alyx') % if there is an AlyxInstance + d = ai.saveobj; + end + d.expRef = ref; % Add expRef field + ref = jsonencode(d); % Convert to JSON string + else % in [UDP_string] + s = jsondecode(varargin{1}); % Convert JSON to structure + ref = s.expRef; % Extract the expRef + AlyxInstance = Alyx('',''); % Create empty Alyx object + if numel(fieldnames(s)) > 1 % Assume to be Alyx object as struct + AlyxInstance = AlyxInstance.loadobj(s); % Turn into object + end + end + end + + % =================================================================== + function [varargout] = iff(cond, evalTrue, evalFalse) + % =================================================================== + %IFF 'if' expression implementation + % v = IFF(cond, evalTrue, evalFalse) returns 'evalTrue' if 'cond' is true, + % otherwise returns 'evalFalse'. + % + % This enables you to write succint one-liners like: + % signstr = iff(x > 0, 'positive', 'negative'); + % + % Either of 'evalTrue' or 'evalFalse' can be functions, in which case + % the result of their execution is returned, but only the returned one + % will be executed. This allows for evaluations which only make sense + % depedent on the condition, e.g.: + % added = iff(ischar(x), @() [x x], @() x + x) + if isa(cond, 'function_handle'); cond = cond(); end + if cond; result = evalTrue; else; result = evalFalse; end + + if isa(result, 'function_handle') + if nargout == 0 || nargout(result) == 0 + result(); + varargout = {}; + else + [varargout{1:nargout}] = result(); + end + else + varargout = {result}; + end + end + + end%---END STATIC METHODS---% +end diff --git a/communication/arduinoIOPort.m b/communication/arduinoIOPort.m index 8da677f9681c88b7b6553d874c77776485bbc8f2..957b755e88e290eca6a6d699db6c226535c00732 100644 --- a/communication/arduinoIOPort.m +++ b/communication/arduinoIOPort.m @@ -60,7 +60,9 @@ classdef arduinoIOPort < handle a.startPin = startPin; end % check port - if ~ischar(port) + if isstring(port) + port = char(port); + elseif ~ischar(port) error('The input argument must be a string, e.g. ''/dev/ttyACM0'' '); end if strcmpi(port,'DEMO') @@ -72,6 +74,7 @@ classdef arduinoIOPort < handle % define IOPort serial object oldv = IOPort('Verbosity',0); + if strcmp(port,'/dev/ttyS0');error('Cannot use /dev/ttyS0!!!');return;end [a.conn, err] = IOPort('OpenSerialPort', port, a.params); IOPort('Verbosity',oldv); if a.conn == -1 @@ -106,8 +109,6 @@ classdef arduinoIOPort < handle % exit if there was no answer if isempty(r) IOPort('CloseAll'); - try delete(a.conn); end - try delete(a); end error('Connection unsuccessful!!! Please make sure that the board is powered on, \nrunning a sketch provided with the package, \nand connected to the indicated serial port. \nYou might also try to unplug and re-plug the USB cable before attempting a reconnection.'); end a.sktc = r(1)-48; %-48 to get the numeric value from the ASCII one [char(48)==0] diff --git a/communication/arduinoManager.m b/communication/arduinoManager.m index c382527b0524d875a27ce436a85407fd7a0696af..ad68532d18a03b8214bcd908a359b1417b44fa89 100644 --- a/communication/arduinoManager.m +++ b/communication/arduinoManager.m @@ -7,7 +7,7 @@ %> %> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md % ======================================================================== -classdef arduinoManager < optickaCore +classdef arduinoManager < optickaCore & rewardManager % ARDUINOMANAGER Connects and manages arduino communication. By default it % connects using arduinoIOPort and the adio.ino arduino sketch (the legacy % arduino interface by Mathworks), which provide much better performance @@ -17,15 +17,14 @@ classdef arduinoManager < optickaCore port = '' %> board type; uno [default] is a generic arduino, xiao is the seeduino xiao %> pico is RaspberryPi Pico - board = 'Uno' + board = 'Uno' %> run with no arduino attached, useful for debugging silentMode = false %> output logging info verbose = false - %> which pin to trigger the reward TTL by default? - rewardPin = 2 - %> time of the TTL sent by default? - rewardTime = 300 + %> parameters for use when giving rewards via fluid or food + %> actuator, type = TTL / fluidpump / food / rpi + reward = struct('type', 'TTL', 'pin', 2, 'time', 300, 'rotation', 60) %> specify the available pins to use; 2-13 is the default for an Uno %> 0-10 for the xiao (though xiao pins 11-14 can control LEDS) availablePins = {} @@ -51,25 +50,30 @@ classdef arduinoManager < optickaCore %> a screen object to bind to screen = [] end - + properties (SetAccess = private, GetAccess = private) allowedProperties = {'availablePins','rewardPin','rewardTime','openGUI','board'... 'port','silentMode','verbose','delayLength','shield','linePWM'} end - + methods%------------------PUBLIC METHODS--------------% - + %==============CONSTRUCTOR============% function me = arduinoManager(varargin) - % arduinoManager Construct an instance of this class + % arduinoManager Construct an instance of this class args = optickaCore.addDefaults(varargin,struct('name','arduino manager')); me=me@optickaCore(args); %we call the superclass constructor first me.parseArgs(args, me.allowedProperties); - if isempty(me.port) + if ~me.silentMode && isempty(me.port) checkPorts(me); if ~isempty(me.ports) - fprintf('--->arduinoManager: Ports available: %s\n',me.ports); - if isempty(me.port); me.port = char(me.ports{end}); end + fprintf('--->arduinoManager: Ports available:\n\t',me.ports); + for ii=1:length(me.ports) + fprintf('%s ',me.ports(ii)); + if mod(ii,5)==0; fprintf('\n\t'); end + end + fprintf('\n'); + if isempty(me.port); me.port = char(me.ports{1}); end else me.comment = 'No Serial Ports are available, going into silent mode'; fprintf('--->arduinoManager: %s\n',me.comment); @@ -82,12 +86,28 @@ classdef arduinoManager < optickaCore me.silentMode = true; end end - + %===============OPEN DEVICE================% function open(me) if me.isOpen || ~isempty(me.device);disp('-->arduinoManager: Already open!');return;end - if me.silentMode;disp('-->arduinoManager: In silent mode, try to close() then open()!');me.isOpen=false;return;end - if isempty(me.port);warning('--->arduinoManager: Better specify the port to use; will try to select one from available ports!');return;end + if matches(me.reward.type,'rpi') + try + system('raspi-gpio set 17 op'); + system('raspi-gpio set 27 op'); + system('raspi-gpio set 17 dl'); + system('raspi-gpio set 27 dl'); + me.silentMode = true; + end + end + if me.silentMode + disp('-->arduinoManager: In silent mode, reset() & open() to connect!'); + me.isOpen=false; + return; + end + if isempty(me.port) + warning('--->arduinoManager: Better specify the port to use; will try to select one from available ports!'); + me.port = char(me.ports(end)); + end close(me); checkPorts(me); try if IsWin && ~isempty(regexp(me.port, '^/dev/', 'once')) @@ -104,28 +124,35 @@ classdef arduinoManager < optickaCore switch me.board case {'Xiao','xiao'} if isempty(me.availablePins);me.availablePins = arrayfun(f,0:14);end - case {'Pico'} + case {'Pico','pico'} if isempty(me.availablePins);me.availablePins = arrayfun(f,[0:22 26 27 28]);end otherwise if isempty(me.availablePins);me.availablePins = arrayfun(f,2:13);end end endPin = max(cell2mat(me.availablePins)); startPin = min(cell2mat(me.availablePins)); - + try me.device = arduinoIOPort(me.port,endPin,startPin); - catch + failToOpen = false; + catch ME me.device.isDemo = true; + failToOpen = true; + getReport(ME); end - - if me.device.isDemo + + if failToOpen me.isOpen = false; me.silentMode = true; - warning('--->arduinoManager: IOport couldn''t open the port, going into silent mode!'); + uiwait(warndlg('--->arduinoManager: IOport couldn''t open the port, going into silent mode!','arduinoManager','modal')); return else me.deviceID = me.port; me.isOpen = true; end + for i = 1:length(me.availablePins) + pinMode(me,me.availablePins{i}, 'output'); + digitalWrite(me,me.availablePins{i},0); + end me.silentMode = false; catch ME me.silentMode = true; me.isOpen = false; @@ -136,19 +163,21 @@ classdef arduinoManager < optickaCore %===============CLOSE DEVICE================% function close(me) - try me.device = []; end %#ok<*TRYNC> + try me.device = []; end %#ok<*TRYNC> try close(me.handles.parent); me.handles=[];end try me.deviceID = ''; end try me.availablePins = ''; end me.isOpen = false; - me.silentMode = false; - checkPorts(me); + if ~me.silentMode + checkPorts(me); + end end - + %===============RESET================% - function reset(me) + function reset(me,hard) + if ~exist('hard','var');hard=false;end try close(me); end - me.silentMode = false; + if hard; me.silentMode = false; end notinlist = true; if ~isempty(me.ports) for i = 1:length(me.ports) @@ -161,7 +190,7 @@ classdef arduinoManager < optickaCore me.port = me.ports{end}; end end - + %===============PIN MODE================% function pinMode(me, line, mode) if ~me.isOpen || me.silentMode; return; end @@ -173,7 +202,7 @@ classdef arduinoManager < optickaCore pinMode(me.device) end end - + %===============ANALOG READ================% function value = analogRead(me, line) if ~me.isOpen || me.silentMode; return; end @@ -198,7 +227,7 @@ classdef arduinoManager < optickaCore value = digitalRead(me.device, line); if me.verbose;fprintf('-DIGREAD: pin %i = %i ',line,value);end end - + %===============DIGITAL WRITE================% function digitalWrite(me, line, value) if ~me.isOpen || me.silentMode; return; end @@ -207,179 +236,195 @@ classdef arduinoManager < optickaCore digitalWrite(me.device, line, value); if me.verbose;fprintf('-DIGWRITE: pin %i = %i ',line,value);end end - + + %===============REWARD SELECTION================% + function giveReward(me, type, varargin) + if ~exist('type','var'); type = me.reward.type; end + switch type + case 'TTL' + timedTTL(me, me.reward.pin, me.reward.time); + case 'fluidpump' + rwdByDCmotor(me, me.reward.time); + case 'rpi' + try + system('raspi-gpio set 27 dh'); + WaitSecs(me.reward.time); + system('raspi-gpio set 27 dl'); + end + case 'food' + stepper(me, varargin); + end + end + %===============SEND TTL (legacy)================% function sendTTL(me, line, time) timedTTL(me, line, time) end - + %===============TIMED TTL================% function timedTTL(me, line, time) - if ~me.isOpen; return; end - if ~me.silentMode - if ~exist('line','var') || isempty(line); line = me.rewardPin; end - if ~exist('time','var') || isempty(time); time = me.rewardTime; end - timedTTL(me.device, line, time); - if me.verbose;fprintf('===>>> timedTTL: TTL pin %i for %i ms\n',line,time);end - else - if me.verbose;fprintf('===>>> timedTTL: Silent Mode\n');end + if ~me.isOpen || me.silentMode + if me.verbose; fprintf('===>>> timedTTL: Silent Mode\n'); end + return; end + if ~exist('line','var') || isempty(line); line = me.reward.pin; end + if ~exist('time','var') || isempty(time); time = me.reward.time; end + timedTTL(me.device, line, time); + if me.verbose;fprintf('===>>> timedTTL: TTL pin %i for %i ms\n',line,time);end end - + %===============STROBED WORD================% function strobeWord(me, value) if ~me.isOpen; return; end if ~me.silentMode strobeWord(me.device, value); if me.verbose;fprintf('===>>> STROBED WORD: %i sent to pins 2-8\n',value);end - end - end - - %===============TIMED DOUBLE TTL================% - function timedDoubleTTL(me, line, time) - if ~me.silentMode - if ~exist('line','var') || isempty(line); line = me.rewardPin; end - if ~exist('time','var') || isempty(time); time = me.rewardTime; end - if time < 0; time = 0;end - timedTTL(me.device, line, 10); - WaitSecs('Yieldsecs',time/1e3); - timedTTL(me.device, line, 10); - if me.verbose;fprintf('===>>> timedTTL: double TTL pin %i for %i ms\n',line,time);end else - if me.verbose;fprintf('===>>> timedTTL: Silent Mode\n');end - end - end - - %===============TEST TTL================% - function test(me,line) - if me.silentMode || isempty(me.device); return; end - if ~exist('line','var') || isempty(line); line = 2; end - if me.verbose;fprintf('===>>> TEST: pin %i LOW/HIGH 20 times\n',line);end - digitalWrite(me.device, line, 0); - for ii = 1:20 - digitalWrite(me.device, line, mod(ii,2)); + if me.verbose;fprintf('===>>> STROBED WORD[silentmode]: %i sent to pins 2-8\n',value);end end end + %==================DRIVE STEPPER MOTOR============% - function stepper(me,ndegree) - ncycle = floor(ndegree/(1.8*4)); - nstep = round((rem(ndegree,(1.8*4))/7.2)*4); - switch me.shield - case 'new' - me.linePWM = [10 11]; - otherwise - me.linePWM = [3 11]; - end - if me.verbose;fprintf('===>>> STEPPER on %s shield: steps = %i \n',me.shield,nstep);end - for i=1:ncycle - cycleStepper(me) - end - switch nstep - case 1 - me.digitalWrite(9, 0); %//ENABLE CH A - me.digitalWrite(8, 1); %//DISABLE CH B - me.digitalWrite(12,1); %//Sets direction of CH A - me.digitalWrite(me.linePWM(1), 1); %//Moves CH A - WaitSecs(me.delayLength); - case 2 - me.digitalWrite(9, 0); %//ENABLE CH A - me.digitalWrite(8, 1); %//DISABLE CH B - me.digitalWrite(12,1); %//Sets direction of CH A - me.digitalWrite(me.linePWM(1), 1); %//Moves CH A - WaitSecs(me.delayLength); - - me.digitalWrite(9, 1); %//DISABLE CH A - me.digitalWrite(8, 0); %//ENABLE CH B - me.digitalWrite(13,0); %//Sets direction of CH B - me.digitalWrite(11,1); %//Moves CH B - WaitSecs(me.delayLength); - case 3 - me.digitalWrite(9, 0); %//ENABLE CH A - me.digitalWrite(8, 1); %//DISABLE CH B - me.digitalWrite(12,1); %//Sets direction of CH A - me.digitalWrite(me.linePWM(1), 1); %//Moves CH A - WaitSecs(me.delayLength); - - me.digitalWrite(9, 1); %//DISABLE CH A - me.digitalWrite(8, 0); %//ENABLE CH B - me.digitalWrite(13,0); %//Sets direction of CH B - me.digitalWrite(11,1); %//Moves CH B - WaitSecs(me.delayLength); - - me.digitalWrite(9, 0); %//ENABLE CH A- - me.digitalWrite(8, 1); %//DISABLE CH B - me.digitalWrite(12,0); %//Sets direction of CH A - me.digitalWrite(me.linePWM(1), 1); %//Moves CH A - WaitSecs(me.delayLength); - case 4 + function stepper(me, ndegree) + if ~exist("ndegree","var") || isempty(ndegree); ndegree = me.reward.rotation; end + ncycle = floor(ndegree/(1.8*4)); + nstep = round((rem(ndegree,(1.8*4))/7.2)*4); + switch me.shield + case 'new' + me.linePWM = [10 11]; + otherwise + me.linePWM = [3 11]; + end + if me.verbose;fprintf('===>>> STEPPER on %s shield: steps = %i \n',me.shield,nstep);end + for i=1:ncycle + cycleStepper(me) + end + switch nstep + case 1 + me.digitalWrite(9, 0); %//ENABLE CH A + me.digitalWrite(8, 1); %//DISABLE CH B + me.digitalWrite(12,1); %//Sets direction of CH A + me.digitalWrite(me.linePWM(1), 1); %//Moves CH A + WaitSecs(me.delayLength); + case 2 + me.digitalWrite(9, 0); %//ENABLE CH A + me.digitalWrite(8, 1); %//DISABLE CH B + me.digitalWrite(12,1); %//Sets direction of CH A + me.digitalWrite(me.linePWM(1), 1); %//Moves CH A + WaitSecs(me.delayLength); + + me.digitalWrite(9, 1); %//DISABLE CH A + me.digitalWrite(8, 0); %//ENABLE CH B + me.digitalWrite(13,0); %//Sets direction of CH B + me.digitalWrite(11,1); %//Moves CH B + WaitSecs(me.delayLength); + case 3 + me.digitalWrite(9, 0); %//ENABLE CH A + me.digitalWrite(8, 1); %//DISABLE CH B + me.digitalWrite(12,1); %//Sets direction of CH A + me.digitalWrite(me.linePWM(1), 1); %//Moves CH A + WaitSecs(me.delayLength); + + me.digitalWrite(9, 1); %//DISABLE CH A + me.digitalWrite(8, 0); %//ENABLE CH B + me.digitalWrite(13,0); %//Sets direction of CH B + me.digitalWrite(11,1); %//Moves CH B + WaitSecs(me.delayLength); + + me.digitalWrite(9, 0); %//ENABLE CH A- + me.digitalWrite(8, 1); %//DISABLE CH B + me.digitalWrite(12,0); %//Sets direction of CH A + me.digitalWrite(me.linePWM(1), 1); %//Moves CH A + WaitSecs(me.delayLength); + case 4 cycleStepper(me) - end + end stopStepper(me) end - - %================STEPPER CYCLR========== - function cycleStepper(me) - me.digitalWrite(9, 0); %//ENABLE CH A - me.digitalWrite(8, 1); %//DISABLE CH B - me.digitalWrite(12,1); %//Sets direction of CH A - me.digitalWrite(me.linePWM(1), 1); %//Moves CH A - WaitSecs(me.delayLength); - - me.digitalWrite(9, 1); %//DISABLE CH A - me.digitalWrite(8, 0); %//ENABLE CH B - me.digitalWrite(13,0); %//Sets direction of CH B - me.digitalWrite(11,1); %//Moves CH B - WaitSecs(me.delayLength); - - me.digitalWrite(9, 0); %//ENABLE CH A- - me.digitalWrite(8, 1); %//DISABLE CH B - me.digitalWrite(12,0); %//Sets direction of CH A - me.digitalWrite(me.linePWM(1), 1); %//Moves CH A - WaitSecs(me.delayLength); - - me.digitalWrite(9, 1); %//DISABLE CH A - me.digitalWrite(8, 0); %//ENABLE CH B- - me.digitalWrite(13,1); %//Sets direction of CH B - me.digitalWrite(11,1); %//Moves CH B - WaitSecs(me.delayLength); - end - - %================STOP STEPPER================ - function stopStepper(me) - me.digitalWrite(9,1); %//DISABLE CH A - me.digitalWrite(me.linePWM(1), 0); %//stop Move CH A - me.digitalWrite(8,1); %//DISABLE CH B - me.digitalWrite(11,0); %//stop Move CH B - WaitSecs(me.delayLength); + + %===========DRIVE DC MOTOR for SMALL PUMP=========% + % A DC motor need 3 digital pin to work, 2 general digital to control + % the direction and a pmw channel to control the speed. A L298 drive + % board or a motorshield + a arduino uno/pico ,which need a 12v DC + % input, can cooperate to drive a DC motor,in clockwise or the otherway. + function rwdByDCmotor(me, time) % this function is running on Pico + % seraildevice='arduino UNO'; + % seraildevice='pico'; + % need to wire the pico/arduino channel 3/4/5 to the L298N EN/IN1/IN2 + IN1=5; IN2=4; EN=3; + me.pinMode(IN1,'o'); me.pinMode(IN2,'o'); + me.digitalWrite(IN1, 0); me.digitalWrite(IN2, 0); % stop the motor + % here must be the analogWrite,1024/255 is the max for pico/uno + me.analogWrite(EN, 1000); + me.digitalWrite(IN1, 1); me.digitalWrite(IN2, 0);%run in one direction + WaitSecs('YieldSecs', time); + me.digitalWrite(IN1, 0); me.digitalWrite(IN2, 0);% stop the motor end - + %===========Check Ports==========% function checkPorts(me) if IsOctave if ~exist('serialportlist','file'); try pkg load instrument-control; end; end if ~verLessThan('instrument-control','0.7') - me.ports = serialportlist('available'); + me.ports = sort(serialportlist('available')); else me.ports = []; end else if ~verLessThan('matlab','9.7') % use the nice serialport list command - me.ports = serialportlist('available'); + me.ports = sort(serialportlist('available')); else - me.ports = seriallist; %#ok + me.ports = sort(seriallist); %#ok end end end - + %===========Delete Method==========% function delete(me) fprintf('arduinoManager: closing connection if open...\n'); - try me.close; end + try close(me); end end + end - + methods ( Access = private ) %----------PRIVATE METHODS---------% - + + %================STEPPER CYCLR========== + function cycleStepper(me) + me.digitalWrite(9, 0); %//ENABLE CH A + me.digitalWrite(8, 1); %//DISABLE CH B + me.digitalWrite(12,1); %//Sets direction of CH A + me.digitalWrite(me.linePWM(1), 1); %//Moves CH A + WaitSecs(me.delayLength); + + me.digitalWrite(9, 1); %//DISABLE CH A + me.digitalWrite(8, 0); %//ENABLE CH B + me.digitalWrite(13,0); %//Sets direction of CH B + me.digitalWrite(11,1); %//Moves CH B + WaitSecs(me.delayLength); + + me.digitalWrite(9, 0); %//ENABLE CH A- + me.digitalWrite(8, 1); %//DISABLE CH B + me.digitalWrite(12,0); %//Sets direction of CH A + me.digitalWrite(me.linePWM(1), 1); %//Moves CH A + WaitSecs(me.delayLength); + + me.digitalWrite(9, 1); %//DISABLE CH A + me.digitalWrite(8, 0); %//ENABLE CH B- + me.digitalWrite(13,1); %//Sets direction of CH B + me.digitalWrite(11,1); %//Moves CH B + WaitSecs(me.delayLength); + end + + %================STOP STEPPER================ + function stopStepper(me) + me.digitalWrite(9,1); %//DISABLE CH A + me.digitalWrite(me.linePWM(1), 0); %//stop Move CH A + me.digitalWrite(8,1); %//DISABLE CH B + me.digitalWrite(11,0); %//stop Move CH B + WaitSecs(me.delayLength); + end + %===========setLow Method==========% function setLow(me) if me.silentMode || ~me.isOpen; return; end @@ -397,7 +442,7 @@ classdef arduinoManager < optickaCore me.device.digitalWrite(i,0); end end - + end - + end diff --git a/communication/awsManager.m b/communication/awsManager.m new file mode 100644 index 0000000000000000000000000000000000000000..4de770f49b3d7996a6c72dcfcce849dcb5a3ee94 --- /dev/null +++ b/communication/awsManager.m @@ -0,0 +1,93 @@ +classdef awsManager < handle + % Use aws CLI command from MATLAB + % Can install cross-platform with pixi: + % > pixi global install awscli + % + % Secrets can be kept locally using setSecret('AWS_ID') + % and setSecret('AWS_KEY'), then passed with getSecret + % e.g. + % aws=awsManager(getSecret("AWS_ID"),getSecret("AWS_KEY"), "http://172.16.102.77:9000") + + properties + AWS_DEFAULT_REGION = 'cn-north-1' + ENDPOINT + end + + properties (Transient = true, Hidden = true) + AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + end + + methods + % =================================================================== + function me = awsManager(id,key,url) + [r, out] = system('which aws'); + assert(~logical(r),"--->>> awsManager: AWS CLI tool is not installed or available on path!!! %s",out) + + if nargin < 3; error("--->>> awsManager: must enter ID and key"); end + + me.AWS_ACCESS_KEY_ID = id; + me.AWS_SECRET_ACCESS_KEY = key; + me.ENDPOINT = url; + me.AWS_DEFAULT_REGION = 'cn-north-1'; + + updateENV(me); + end + + % =================================================================== + function updateENV(me) + setenv("AWS_ACCESS_KEY_ID", me.AWS_ACCESS_KEY_ID) + setenv("AWS_SECRET_ACCESS_KEY", me.AWS_SECRET_ACCESS_KEY) + setenv("AWS_DEFAULT_REGION", me.AWS_DEFAULT_REGION) + end + + % =================================================================== + function out = list(me) + cmdin = strjoin(["aws --endpoint-url " me.ENDPOINT " s3 ls"],""); + [~, out] = system(cmdin); + out = strtrim(out); + end + + % =================================================================== + function checkBucket(me, bucket) + buckets = list(me); + if ~contains(buckets,lower(bucket)) + createBucket(me, lower(bucket)); + end + end + + % =================================================================== + function success = createBucket(me, bucket) + if nargin < 2; error("--->>> awsManager: you must enter a bucket name"); end + cmdin = strjoin(["aws --endpoint-url " me.ENDPOINT " s3 mb s3://" bucket],""); + [r, out] = system(cmdin); + success = ~logical(r); + if ~success + warning("Problem making bucket: %s", out); + end + end + + % =================================================================== + function success = copyFiles(me, file, bucket, key) + if ~exist('file','var') || isempty(file); return; end + if ~exist('bucket','var') || isempty(bucket); return; end + if ~exist('key','var') || isempty(key); return; end + + if exist(file) == 7 + rec = "--recursive "; + else + rec = ""; + end + + cmdin = strjoin(["aws --no-progress " rec "--endpoint-url " me.ENDPOINT " s3 cp '" file "' s3://" bucket "/" key],""); + [r, out] = system(cmdin); + success = ~logical(r); + if ~success + warning("!!! Problem making bucket: %s - %s", cmdin, out); + else + fprintf('--->>> awsManager: Copy Details using %s\n',cmdin) + disp(out) + end + end + end +end \ No newline at end of file diff --git a/communication/dataConnection.m b/communication/dataConnection.m index 3362eb5e05768b0bf14e58ddba5af3a9b04d2bc7..eb42aebb28dc729f18fed3b4859a64b6fb40d9b7 100644 --- a/communication/dataConnection.m +++ b/communication/dataConnection.m @@ -25,11 +25,11 @@ %> and responds to commands, allowing you to send data and EVAL commands on a %> remote machine... %> -%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md +%> Copyright ©2014-2025 Ian Max Andolina — released: LGPL3, see LICENCE.md % ======================================================================== classdef dataConnection < handle - %dataConnection Allows send/recieve over Ethernet - % This uses the TCP/UDP library to manage connections between servers + % dataConnection Allows send/recieve over Ethernet + % This uses the PNET library to manage connections between servers % and clients in Matlab properties %> Whether this object is a 'client' or 'server' (TCP). Normally UDP @@ -51,8 +51,8 @@ classdef dataConnection < handle dataOut = [] %> the format the data is required dataType = 'string' - %> verbosity - verbosity = 1 + %> do we log to the command window? + verbosity = 0 %> this is a mode where the object sits in a loop and can be %> controlled by a remote matlab instance, which passes commands the %> server can 'put' or 'eval' @@ -61,6 +61,8 @@ classdef dataConnection < handle readTimeOut = 0 %> default write timeout writeTimeOut = 0 + %> default size of chunk to read for tcp + readSize = 1024 %> sometimes we shouldn't cleanup connections on delete, e.g. when we pass this %> object to another matlab instance as we will close the wrong connections!!! cleanup = true @@ -90,7 +92,7 @@ classdef dataConnection < handle properties (SetAccess = private, GetAccess = private) allowedProperties = {'type','protocol','lPort','rPort','lAddress',... 'rAddress','autoOpen','dataType','verbosity','autoServer',... - 'readTimeOut','writeTimeOut','cleanup'} + 'readTimeOut','writeTimeOut','readSize','cleanup'} remoteCmd = '--remote--' breakCmd = '--break--' busyCmd = '--busy--' @@ -104,7 +106,6 @@ classdef dataConnection < handle %> Configures input structure to assign properties % =================================================================== function me = dataConnection(varargin) - me.parseArgs(varargin); if me.autoServer == true me.type='server'; @@ -180,7 +181,6 @@ classdef dataConnection < handle end loop = loop + 1; end - if me.conn >= 0 % disable blocking pnet(me.conn ,'setwritetimeout', me.writeTimeOut); @@ -210,10 +210,16 @@ classdef dataConnection < handle %> %> Close the ethernet connection % =================================================================== - function status = close(me,type,force) - if ~exist('type','var');type = 'conn';end - if ~exist('force','var');force = true;end - if ischar(force);force = true;end + function status = close(me, type, force) + if ~exist('type','var') + if matches(me.type,'server') + type = 'rconn'; + else + type = 'conn'; + end + end + if ~exist('force','var'); force = true; end + if ischar(force); force = true; end status = 0; switch type @@ -241,17 +247,20 @@ classdef dataConnection < handle end else me.status = pnet(me.(name),'status'); - if me.status <=0; - me.isOpen = false; + if me.status <=0 me.salutation('close Method','Connection appears closed...'); else try %#ok pnet(me.(name), 'close'); end end - me.conn = -1; - end - end + me.(name) = -1; me.(list) = []; + end + end + me.isOpen = false; + me.conn = -1; + me.rconn = -1; + me.connList = []; end % =================================================================== @@ -261,8 +270,8 @@ classdef dataConnection < handle % =================================================================== function status = closeAll(me) me.status = 0; - me.conn = -1; - me.rconn = -1; + me.conn = -1; me.rconn = -1; + me.connList = []; me.rconnList = []; try pnet('closeall'); me.salutation('closeAll Method','Closed all PNet connections') @@ -270,6 +279,7 @@ classdef dataConnection < handle me.salutation('closeAll Method','Failed to close all PNet connections') me.status = -1; end + me.isOpen = false; status = me.status; end @@ -281,24 +291,24 @@ classdef dataConnection < handle %> %> @return hasData (logical) % =================================================================== - function hasData = checkData(me) - me.hasData = false; + function [hasData, data] = checkData(me) + hasData = false; + data = ''; switch me.protocol case 'udp'%============================UDP data = pnet(me.conn, 'read', 65536, me.dataType, 'view'); if isempty(data) - me.hasData = pnet(me.conn, 'readpacket') > 0; + hasData = pnet(me.conn, 'readpacket') > 0; else - me.hasData = true; + hasData = true; end case 'tcp'%============================TCP - data = pnet(me.conn, 'read', 1024, me.dataType, 'noblock', 'view'); + data = pnet(me.conn, 'read', 'noblock', 'view'); if ~isempty(data) - me.hasData = true; + hasData = true; end - end - hasData = me.hasData; + me.hasData = hasData; end % =================================================================== @@ -307,7 +317,9 @@ classdef dataConnection < handle %> Close all connections % =================================================================== function flush(me) - try flushStatus(me); end + while me.checkData + pnet(me.conn,'read', 256000); + end end % =================================================================== @@ -319,6 +331,13 @@ classdef dataConnection < handle % Read any avalable data from the given pnet socket. function data = readline(me) data = []; + if matches(me.type,'server') && me.rconn > -1 + conn = me.conn; + elseif me.conn > -1 + conn = me.conn; + else + return; + end switch me.protocol case 'udp'%============================UDP nBytes = pnet(me.conn, 'readpacket'); @@ -326,10 +345,34 @@ classdef dataConnection < handle data = pnet(me.conn, 'readline', nBytes, 'noblock'); end case 'tcp'%============================TCP - data = pnet(me.conn, 'readline', 1024,' noblock'); + data = pnet(me.conn, 'readline', me.readSize,' noblock'); end me.dataIn = data; end + + % =================================================================== + %> @brief read last N lines + %> + %> Flush the server messagelist + % =================================================================== + function data = readLines(me, N, order) + if ~exist('N','var') || isempty(N); N = 1; end + if ~exist('order','var') || ~matches(order,{'first','last'}); order = 'last'; end + data = []; + while 1 + thisData = pnet(me.conn, 'read', 512000, 'noblock'); + if isempty(thisData); break; end + data = [data thisData]; + end + if isempty(data); return; end + r = regexp(data,'\n'); + if length(r) <= N; return; end + if matches(order,'first') + data = data(1:r(N)); + else + data = data(r(end-N):end); + end + end % =================================================================== %> @brief Read any avalable data from the given pnet socket. @@ -342,7 +385,7 @@ classdef dataConnection < handle %> @param (optional) size is size in bytes to read %> @return data the data returned from the connection % =================================================================== - function data = read(me,all,dataType,size) + function data = read(me, all, dataType, size) if ~exist('all','var') all = 0; @@ -405,7 +448,7 @@ classdef dataConnection < handle me.dataIn = data; %============================TCP case 'tcp' - if ~exist('size','var');size=256000;end + if ~exist('size','var');size=me.readSize;end while loop > 0 dataIn=pnet(me.conn,'read', size, dataType,'noblock'); if all == false @@ -434,17 +477,17 @@ classdef dataConnection < handle %> @param sendPacket (0|1[default]) for UDP connections actually send the packet or wait to fill buffer with another call first % =================================================================== function write(me, data, formatted, sendPacket) - - if ~exist('data','var') + if ~me.isOpen; return; end + if ~exist('data','var') || isempty(data) data = me.dataOut; end - if ~exist('formatted','var') + if ~exist('formatted','var') || isempty(formatted) formatted = false; end - if ~exist('sendPacket','var') + if ~exist('sendPacket','var') || isempty(sendPacket) sendPacket = true; - end - + end + switch me.protocol case 'udp'%============================UDP if formatted == false @@ -455,9 +498,17 @@ classdef dataConnection < handle if sendPacket;pnet(me.conn, 'writepacket', me.rAddress, me.rPort);end case 'tcp'%============================TCP if formatted == false - pnet(me.conn, 'write', data); - else - pnet(me.conn, 'printf', data); + try + pnet(me.conn, 'write', data); + catch + warning('No data written') + end + else + try + pnet(me.conn, 'printf', data); + catch + warning('No data written') + end end end end @@ -497,7 +548,7 @@ classdef dataConnection < handle me.conn=pnet(me.rconn,'tcplisten'); if me.conn > -1 [rhost,rport]=pnet(me.conn,'gethost'); - fprintf('START SERVING NEW CONNECTION FROM IP %d.%d.%d.%d port:%d',rhost,rport) + fprintf('START SERVING NEW CONNECTION FROM IP %d.%d.%d.%d port:%d\n\n',rhost,rport) pnet(me.conn ,'setwritetimeout', me.writeTimeOut); pnet(me.conn ,'setreadtimeout', me.readTimeOut); me.rPort = rport; @@ -506,8 +557,7 @@ classdef dataConnection < handle isClient = true; else me.conn = -1; - me.isOpen = false; - me.salutation('No client available') + me.salutation('No client available yet...'); end catch me.conn = -1; @@ -533,49 +583,72 @@ classdef dataConnection < handle % #define STATUS_TCP_SERVER 12 % #define STATUS_UDP_CLIENT_CONNECT 18 % #define STATUS_UDP_SERVER_CONNECT 19 - function status = checkStatus(me,conn) %#ok - status = -1; - try - if ~exist('conn','var') || strcmp(conn,'conn') - conn='conn'; + function status = checkStatus(me, conn) %#ok + status = []; + if ~exist('conn','var') || isempty(conn) + if matches(me.type,'client') + conn = me.conn; else - conn = 'rconn'; + conn = [me .conn me.rconn]; end - me.status = pnet(me.(conn),'status'); - if me.status <=0;me.(conn) = -1; me.isOpen = false;me.salutation('checkStatus Method','Connection appears closed...');end - switch me.status - case -1 - me.statusMessage = 'STATUS_NOTFOUND'; - case 0 - me.statusMessage = 'STATUS_NOCONNECT'; - case 1 - me.statusMessage = 'STATUS_TCP_SOCKET'; - case 5 - me.statusMessage = 'STATUS_IO_OK'; - case 6 - me.statusMessage = 'STATUS_UDP_CLIENT'; - case 8 - me.statusMessage = 'UDP_SERVER'; - case 10 - me.statusMessage = 'STATUS_CONNECT'; - case 11 - me.statusMessage = 'STATUS_TCP_CLIENT'; - case 12 - me.statusMessage = 'STATUS_TCP_SERVER'; - case 18 - me.statusMessage = 'STATUS_UDP_CLIENT_CONNECT'; - case 19 - me.statusMessage = 'STATUS_UDP_SERVER_CONNECT'; - otherwise - me.statusMessage = 'UNDEFINED'; + elseif ischar(conn) && strcmp(conn,'conn') + conn = me.conn; + elseif ischar(conn) && strcmp(conn,'rconn') + conn = me.rconn; + end + try + for c = conn + fprintf('\n==Connection %i== ',c); + me.status = pnet(c,'status'); + if me.status < 0 + me.salutation('checkStatus Method','Connection appears closed...',true); + if me.conn == c + pnet(me.conn,'close'); + me.conn = -1; + elseif me.rconn == c + pnet(me.conn,'close'); + pnet(me.rconn,'close'); + me.conn = -1; + me.rconn = -1; + me.isOpen = false; + end + end + switch me.status + case -1 + me.statusMessage = 'STATUS_NOTFOUND'; + case 0 + me.statusMessage = 'STATUS_NOCONNECT'; + case 1 + me.statusMessage = 'STATUS_TCP_SOCKET'; + case 5 + me.statusMessage = 'STATUS_IO_OK'; + case 6 + me.statusMessage = 'STATUS_UDP_CLIENT'; + case 8 + me.statusMessage = 'UDP_SERVER'; + case 10 + me.statusMessage = 'STATUS_CONNECT'; + case 11 + me.statusMessage = 'STATUS_TCP_CLIENT'; + case 12 + me.statusMessage = 'STATUS_TCP_SERVER'; + case 18 + me.statusMessage = 'STATUS_UDP_CLIENT_CONNECT'; + case 19 + me.statusMessage = 'STATUS_UDP_SERVER_CONNECT'; + otherwise + me.statusMessage = 'UNDEFINED'; + end + me.salutation(me.statusMessage,'checkStatus') + status = [status me.status]; end - me.salutation(me.statusMessage,'checkStatus') - status = me.status; catch %#ok + close(me) me.status = -1; me.statusMessage = 'UNKNOWN'; status = me.status; - me.(conn) = -1; + me.conn = -1; + me.rconn = -1; me.isOpen = false; fprintf('Couldn''t check status\n') end @@ -587,16 +660,16 @@ classdef dataConnection < handle %> Initialize the server loop % =================================================================== function startServer(me) - me.conn = pnet('tcpsocket',me.lPort); - pnet(me.conn ,'setwritetimeout', me.writeTimeOut); - pnet(me.conn ,'setreadtimeout', me.readTimeOut); + me.rconn = pnet('tcpsocket',me.lPort); + pnet(me.rconn ,'setwritetimeout', me.writeTimeOut); + pnet(me.rconn ,'setreadtimeout', me.readTimeOut); ls = 1; msgloop=1; while ls if msgloop == 1;fprintf('\nWAIT FOR CONNECTION ON PORT: %d\n',me.lPort);end msgloop=2; try - me.rconn = pnet(me.conn,'tcplisten'); + me.conn = pnet(me.rconn,'tcplisten'); pause(0.01); catch ME disp 'Try: "pnet closeall" in all matlab sessions on this server.'; @@ -605,10 +678,10 @@ classdef dataConnection < handle rethrow(ME); end - if me.rconn >= 0 + if me.conn >= 0 msgloop=1; try - [me.rAddress,me.rPort]=pnet(me.rconn,'gethost'); + [me.rAddress,me.rPort]=pnet(me.conn,'gethost'); fprintf('START SERVING NEW CONNECTION FROM IP %d.%d.%d.%d port:%d\n\n',me.rAddress,me.rPort); me.serverLoop; catch %#ok<*CTCH> @@ -617,16 +690,14 @@ classdef dataConnection < handle end if me.checkForEscape == 1 - pnet(me.rconn,'close') - pnet(me.conn,'close') + pnet(me.rconn,'close'); + pnet(me.conn,'close'); me.conn = -1; me.rconn = -1; + close(me); break; end - end - - me.close; - + end end % =================================================================== @@ -676,6 +747,45 @@ classdef dataConnection < handle end end + + % =================================================================== + %> @brief echo server + %> + %> Run the server loop + % =================================================================== + function echoServer(me) + if ~strcmpi(me.type,'server'); warning('Object needs to be a server!');return;end + if ~me.isOpen; open(me); end + doEcho = true; + while doEcho + disp('Will wait for a client...') + while ~checkClient(me) + WaitSecs('YieldSecs',0.01); + if checkForEscape(me); doEcho=false; break; end + end + disp('...will now wait for some data...') + while pnet(me.conn,'status') > -1 + in=pnet(me.conn,'read'); + if ~isempty(in) + disp('==========GOT:'); + if isnumeric(in) + fprintf(' %i ',in); fprintf(' %i ',uint8(in)); + else + disp(in); disp(uint8(in)); + end + disp('=============='); + end + if checkForEscape(me); doEcho=false; break; end + s = pnet(me.conn,'status'); + if s == 0 + break; + end + WaitSecs('YieldSecs',0.01); + end %END WHILE + end + end + + end %END METHODS @@ -704,7 +814,7 @@ classdef dataConnection < handle out=false; [~,~,key] = KbCheck(-1); key=KbName(key); - if strcmpi(key,'escape') %allow keyboard break + if strcmpi(key,'escape') | strcmpi(key,'esc') %allow keyboard break out=true; end end @@ -723,7 +833,7 @@ classdef dataConnection < handle fprintf('Waiting for %s command...',me.remoteCmd); while ~strcmpi(str,me.remoteCmd) && pnet(me.rconn,'status') pause(0.01); - str=pnet(me.rconn,'readline',1024,'noblock'); + str=pnet(me.rconn,'readline',me.readSize,'noblock'); if checkForEscape(me) pnet(me.rconn,'close'); return; @@ -793,7 +903,7 @@ classdef dataConnection < handle pnet(me.rconn,'printf','\n--ready--\n'); else pnet(me.rconn,'printf','\n--error--\n'); - disp(sprintf('\nERROR: %s\n',lasterr)); + fprintf('\nERROR: %s\n\n', lasterr); end end %END WHILE ls @@ -801,22 +911,22 @@ classdef dataConnection < handle end %END while pnet(me.rconn,'status') end - - + % =================================================================== %> @brief Flush the server messagelist %> %> Flush the server messagelist % =================================================================== - function stat=flushStatus(me) + function stat = flushStatus(me, num) + if ~exist('number','var'); num = 1; end while 1 % Loop that finds, returns and leaves last text line in buffer. - str=pnet(me.conn,'read', 1024,'view','noblock'); - if length(regexp([str,' '],'\n'))<=1 - stat=pnet(me.conn,'readline',1024,'view','noblock'); % The return + str=pnet(me.conn,'read', me.readSize,'view','noblock'); + if length(regexp([str,' '],'\n'))<=num + stat=pnet(me.conn,'readline',me.readSize,'view','noblock'); % The return stat=stat(3:end-2); return; end - dump=pnet(me.conn,'readline',1024,'noblock'); % Then remove last line + dump=pnet(me.conn,'readline',me.readSize,'noblock'); % Then remove last line end end @@ -949,7 +1059,7 @@ classdef dataConnection < handle lp = 25; while lp > 0 lp = lp - 1; - dataclass=pnet(thisConnection,'readline',1024); + dataclass=pnet(thisConnection,'readline',me.readSize); switch dataclass case {'double' 'char' 'int8' 'int16' 'int32' 'uint8' 'uint16' 'uint32'} datadims=double(pnet(thisConnection,'Read',1,'uint32')); @@ -994,10 +1104,10 @@ classdef dataConnection < handle % =================================================================== function delete(me) if me.cleanup == true - me.salutation('dataConnection delete Method','Cleaning up now...') + me.salutation('dataConnection delete Method','Cleaning up now...'); me.close; else - me.salutation('dataConnection delete Method','Closing (no cleanup)...') + me.salutation('dataConnection delete Method','Closing (no cleanup)...'); end end @@ -1008,8 +1118,9 @@ classdef dataConnection < handle %> @param in the calling function %> @param message the message that needs printing to command window % =================================================================== - function salutation(me,in,message) - if me.verbosity > 0 + function salutation(me,in,message,verbose) + if ~exist('verbose','var'); verbose = me.verbosity; end + if verbose if ~exist('in','var') in = 'General Message'; end @@ -1057,14 +1168,21 @@ classdef dataConnection < handle %> dataConnection is open with the same numbered connection. cleanup %> property is used to stop the delete method from calling close methods. % =================================================================== - function lobj=loadobj(in) - fprintf('Loading dataConnection object...\n'); - in.cleanup=0; - in.conn=-1; - in.rconn=-1; - in.connList=[]; - in.rconnList=[]; - lobj=in; + function me=loadobj(in) + if isstruct(in) + me = dataConnection; + me.rPort = in.rPort; + me.rAddress = in.rAddress; + me.lPort = in.lPort; + me.lAddress = in.lAddress; + else + me = in; + end + me.cleanup=0; + me.conn=-1; + me.rconn=-1; + me.connList=[]; + me.rconnList=[]; end end end \ No newline at end of file diff --git a/communication/dumpmsgpack.m b/communication/dumpmsgpack.m new file mode 100644 index 0000000000000000000000000000000000000000..5a52a91aef0d50e0cbd40302f322f55fc44baf96 --- /dev/null +++ b/communication/dumpmsgpack.m @@ -0,0 +1,268 @@ +%DUMPMSGPACK dumps Matlab data structures as a msgpack data +% DUMPMSGPACK(DATA) +% recursively walks through DATA and creates a msgpack byte buffer from it. +% - strings are converted to strings +% - scalars are converted to numbers +% - logicals are converted to `true` and `false` +% - arrays are converted to arrays of numbers +% - matrices are converted to arrays of arrays of numbers +% - empty matrices are converted to nil +% - cell arrays are converted to arrays +% - cell matrices are converted to arrays of arrays +% - struct arrays are converted to arrays of maps +% - structs and container.Maps are converted to maps +% - function handles and matlab objects will raise an error. +% +% There is no way of encoding bins or exts + +% (c) 2016 Bastian Bechtold +% This code is licensed under the BSD 3-clause license + +function msgpack = dumpmsgpack(data) + msgpack = dump(data); + % collect all parts in a cell array to avoid frequent uint8 + % concatenations. + msgpack = [msgpack{:}]; +end + +function msgpack = dump(data) + % convert numeric matrices to cell matrices since msgpack doesn't know matrices + if (isnumeric(data) || islogical(data)) && ... + ~(isvector(data) && isa(data, 'uint8')) && ~isscalar(data) && ~isempty(data) + data = num2cell(data); + end + % convert character matrices to cell of strings or cell matrices + if (isstring(data) || ischar(data)) && ~(isvector(data)||isempty(data)) && ndims(data) == 2 + data = cellstr(data); + elseif (isstring(data) || ischar(data)) && ~isvector(data) + data = num2cell(data); + end + % convert struct arrays to cell of structs + if isstruct(data) && ~isscalar(data) + data = num2cell(data); + end + % standardize on always using maps instead of structs + if isstruct(data) + if ~isempty(fieldnames(data)) + data = containers.Map(fieldnames(data), struct2cell(data)); + else + data = containers.Map(); + end + end + + if isnumeric(data) && isempty(data) + msgpack = {uint8(192)}; % encode nil + elseif isa(data, 'uint8') && numel(data) > 1 + msgpack = dumpbin(data); + elseif islogical(data) + if data + msgpack = {uint8(195)}; % encode true + else + msgpack = {uint8(194)}; % encode false + end + elseif isinteger(data) + msgpack = {dumpinteger(data)}; + elseif isnumeric(data) + msgpack = {dumpfloat(data)}; + elseif ischar(data) || isstring(data) + msgpack = dumpstring(data); + elseif iscell(data) + msgpack = dumpcell(data); + elseif isa(data, 'containers.Map') + msgpack = dumpmap(data); + elseif isa(data, 'dictionary') + msgpack = dumpmapd(data); + else + error('transplant:dumpmsgpack:unknowntype', ... + ['Unknown type "' class(data) '"']); + end +end + +function bytes = scalar2bytes(value) + % reverse byte order to convert from little endian to big endian + bytes = typecast(swapbytes(value), 'uint8'); +end + +function msgpack = dumpinteger(value) + % if the values are small enough, encode as fixnum: + if value >= 0 && value < 128 + % first bit is 0, last 7 bits are value + msgpack = uint8(value); + return + elseif value < 0 && value > -32 + % first three bits are 111, last 5 bytes are value + msgpack = typecast(int8(value), 'uint8'); + return + end + + % otherwise, encode by type: + switch class(value) + case 'uint8' % encode as uint8 + msgpack = uint8([204, value]); + case 'uint16' % encode as uint16 + msgpack = uint8([205, scalar2bytes(value)]); + case 'uint32' % encode as uint32 + msgpack = uint8([206, scalar2bytes(value)]); + case 'uint64' % encode as uint64 + msgpack = uint8([207, scalar2bytes(value)]); + case 'int8' % encode as int8 + msgpack = uint8([208, scalar2bytes(value)]); + case 'int16' % encode as int16 + msgpack = uint8([209, scalar2bytes(value)]); + case 'int32' % encode as int32 + msgpack = uint8([210, scalar2bytes(value)]); + case 'int64' % encode as int64 + msgpack = uint8([211, scalar2bytes(value)]); + otherwise + error('transplant:dumpmsgpack:unknowninteger', ... + ['Unknown integer type "' class(value) '"']); + end +end + +function msgpack = dumpfloat(value) + % do double first, as it is more common in Matlab + if isa(value, 'double') % encode as float64 + msgpack = uint8([203, scalar2bytes(value)]); + elseif isa(value, 'single') % encode as float32 + msgpack = uint8([202, scalar2bytes(value)]); + else + error('transplant:dumpmsgpack:unknownfloat', ... + ['Unknown float type "' class(value) '"']); + end +end + +function msgpack = dumpstring(value) + b10100000 = 160; + + encoded = unicode2native(value, 'utf-8'); + len = length(encoded); + + if len < 32 % encode as fixint: + % first three bits are 101, last 5 are length: + msgpack = {uint8(bitor(len, b10100000)), encoded}; + elseif len < 256 % encode as str8 + msgpack = {uint8([217, len]), encoded}; + elseif len < 2^16 % encode as str16 + msgpack = {uint8(218), scalar2bytes(uint16(len)), encoded}; + elseif len < 2^32 % encode as str32 + msgpack = {uint8(219), scalar2bytes(uint32(len)), encoded}; + else + error('transplant:dumpmsgpack:stringtoolong', ... + sprintf('String is too long (%d bytes)', len)); + end +end + +function msgpack = dumpbin(value) + len = length(value); + if len < 256 % encode as bin8 + msgpack = {uint8([196, len]) value(:)'}; + elseif len < 2^16 % encode as bin16 + msgpack = {uint8(197), scalar2bytes(uint16(len)), value(:)'}; + elseif len < 2^32 % encode as bin32 + msgpack = {uint8(198), scalar2bytes(uint32(len)), value(:)'}; + else + error('transplant:dumpmsgpack:bintoolong', ... + sprintf('Bin is too long (%d bytes)', len)); + end +end + +function msgpack = dumpcell(value) + b10010000 = 144; + + % Msgpack can only work with 1D-arrays. Thus, Convert a + % multidimensional AxBxC array into a cell-of-cell-of-cell, so + % that indexing value{a, b, c} becomes value{a}{b}{c}. + if length(value) ~= prod(size(value)) + for n=ndims(value):-1:2 + value = cellfun(@squeeze, num2cell(value, n), ... + 'uniformoutput', false); + end + end + + % write header + len = length(value); + if len < 16 % encode as fixarray + % first four bits are 1001, last 4 are length + msgpack = {uint8(bitor(len, b10010000))}; + elseif len < 2^16 % encode as array16 + msgpack = {uint8(220), scalar2bytes(uint16(len))}; + elseif len < 2^32 % encode as array32 + msgpack = {uint8(221), scalar2bytes(uint32(len))}; + else + error('transplant:dumpmsgpack:arraytoolong', ... + sprintf('Array is too long (%d elements)', len)); + end + + % write values + for n=1:len + stuff = dump(value{n}); + msgpack = [msgpack stuff{:}]; + end +end + +function msgpack = dumpmap(value) + b10000000 = 128; + + % write header + len = length(value); + if len < 16 % encode as fixmap + % first four bits are 1000, last 4 are length + msgpack = {uint8(bitor(len, b10000000))}; + elseif len < 2^16 % encode as map16 + msgpack = {uint8(222), scalar2bytes(uint16(len))}; + elseif len < 2^32 % encode as map32 + msgpack = {uint8(223), scalar2bytes(uint32(len))}; + else + error('transplant:dumpmsgpack:maptoolong', ... + sprintf('Map is too long (%d elements)', len)); + end + + % write key-value pairs + keys = value.keys(); + values = value.values(); + for n=1:len + keystuff = dump(keys{n}); + valuestuff = dump(values{n}); + msgpack = [msgpack, keystuff{:}, valuestuff{:}]; + end +end + +function msgpack = dumpmapd(value) + b10000000 = 128; + + % write header + len = value.numEntries; + if len < 16 % encode as fixmap + % first four bits are 1000, last 4 are length + msgpack = {uint8(bitor(len, b10000000))}; + elseif len < 2^16 % encode as map16 + msgpack = {uint8(222), scalar2bytes(uint16(len))}; + elseif len < 2^32 % encode as map32 + msgpack = {uint8(223), scalar2bytes(uint32(len))}; + else + error('transplant:dumpmsgpack:maptoolong', ... + sprintf('Map is too long (%d elements)', len)); + end + + % write key-value pairs + k = value.keys(); + for n=1:len + if isstring(k(n)) + keys{n} = char(k(n)); + else + keys{n} = k(n); + end + v = value(k(n)); + if iscell(v); v = v{:}; end + if isstring(v) + values{n} = char(v); + else + values{n} = v; + end + end + for n=1:len + keystuff = dump(keys{n}); + valuestuff = dump(values{n}); + msgpack = [msgpack, keystuff{:}, valuestuff{:}]; + end +end diff --git a/communication/edfmex.mexglx b/communication/edfmex.mexglx deleted file mode 100644 index c9e4fb770fa1830d8071a6e31b9692202c84495e..0000000000000000000000000000000000000000 Binary files a/communication/edfmex.mexglx and /dev/null differ diff --git a/communication/eyelinkCustomCallback.m b/communication/eyelinkCustomCallback.m deleted file mode 100755 index 4aa728c22d4f8f87bb8779ebea0b65ec5cacf8fe..0000000000000000000000000000000000000000 --- a/communication/eyelinkCustomCallback.m +++ /dev/null @@ -1,492 +0,0 @@ -function rc = eyelinkCustomCallback(callArgs, msg) -% Retrieve live eye-image from Eyelink, show it in onscreen window. -% -% This function is normally called from within the Eyelink() mex file. -% Normal user code only calls it once to supply the eyelink defaults struct. -% This is handled within the EyelinkInitDefaults.m file, so you generally -% should not have to worry about this. However, if you change settings in -% the el structure, you may need to call it yourself. -% -% To define which onscreen window the eye image should be -% drawn to, call it with the return value from EyelinkInitDefaults, e.g., -% w=Screen('OpenWindow', ...); -% el=EyelinkInitDefaults(w); -% myEyelinkDispatchCallback(el); -% -% -% to actually receive and display the images, register this function as eyelink's callback: -% if Eyelink('Initialize', 'myEyelinkDispatchCallback') ~=0 -% error('eyelink failed init') -% end -% result = Eyelink('StartSetup',1) %put the tracker into a mode capable of sending images -% then you must hit 'return' on the PTB computer, this key command will be sent to the tracker host to initiate sending of images. -% -% This function fetches the most recent live image from the Eylink eye -% camera and displays it in the previously assigned onscreen window. -% -% History: -% 15.3.2009 Derived from MemoryBuffer2TextureDemo.m (MK). -% 4.4.2009 Updated to use EyelinkGetKey + fixed eyelinktex persistence crash (edf). -% 11.4.2009 Cleaned up. Should be ready for 1st release, although still -% pretty alpha quality. (MK). -% 15.6.2010 Added some drawing routines to get standard behaviour back. Enabled -% use of the callback by default. Clarified in helptext that user -% normally should not have to worry about calling this file. (fwc) -% 20.7.2010 drawing of instructions, eye-image+title, playing sounds in seperate functions -% -% 1.2.2010 modified to allow for cross hair and fix bugs. (nj) -% 29.10.2018 Drop 'DrawDots' for calibration target. Some white-space fixes. - -% Cached texture handle for eyelink texture: -persistent eyelinktex; -global dw dh offscreen rM; - -% Cached window handle for target onscreen window: -persistent eyewin; -persistent calxy; -persistent imgtitle; -persistent eyewidth; -persistent eyeheight; - -% Cached(!) eyelink stucture containing keycodes -persistent el; -persistent lastImageTime; %#ok -persistent drawcount; -persistent ineyeimagemodedisplay; -persistent clearScreen; -persistent drawInstructions; - -% Cached constant definitions: -persistent GL_RGBA; -persistent GL_RGBA8; -persistent hostDataFormat; - -persistent verbose; -persistent inDrift; -offscreen = 0; -newImage = 0; - -if isempty(verbose); verbose = false; end - -if 0 == Screen('WindowKind', eyelinktex) - eyelinktex = []; % got persisted from a previous ptb window which has now been closed; needs to be recreated -end -if isempty(eyelinktex) - % Define the two OpenGL constants we actually need. No point in - % initializing the whole PTB OpenGL mode for just two constants: - GL_RGBA = 6408; - GL_RGBA8 = 32856; - GL_UNSIGNED_BYTE = 5121; %#ok - GL_UNSIGNED_INT_8_8_8_8 = 32821; %#ok - GL_UNSIGNED_INT_8_8_8_8_REV = 33639; - hostDataFormat = GL_UNSIGNED_INT_8_8_8_8_REV; - drawcount = 0; - lastImageTime = GetSecs; -end - -% Preinit return code to zero: -rc = 0; - -if nargin < 2 - msg = []; -end - -if nargin < 1 - callArgs = []; -end - -if isempty(callArgs) - error('You must provide some valid "callArgs" variable as 1st argument!'); -end - -if ~isnumeric(callArgs) && ~isstruct(callArgs) - error('"callArgs" argument must be a EyelinkInitDefaults struct or double vector!'); -end - -% Eyelink el struct provided? -if isstruct(callArgs) && isfield(callArgs,'window') - % Check if el.window subfield references a valid window: - if Screen('WindowKind', callArgs.window) ~= 1 - error('argument didn''t contain a valid handle of an open onscreen window! pass in result of EyelinkInitDefaults(previouslyOpenedPTBWindowPtr).'); - end - - % Ok, valid handle. Assign it and return: - eyewin = callArgs.window; - - % Assume rest of el structure is valid: - el = callArgs; - clearScreen=1; - eyelinktex=[]; - lastImageTime=GetSecs; - ineyeimagemodedisplay=0; - drawInstructions=1; - if isfield(el,'verbose') - verbose = el.verbose; - end - return; -end - - -% Not an eyelink struct. Either a 4 component vector from Eyelink(), or something wrong: -if length(callArgs) ~= 4 - error('Invalid "callArgs" received from Eyelink() Not a 4 component double vector as expected!'); -end - -% Extract command code: -eyecmd = callArgs(1); - -if isempty(eyewin) - warning('Got called as callback function from Eyelink() but usercode has not set a valid target onscreen window handle yet! Aborted.'); %#ok - return; -end - -% Flag that tells if a new camera image was received and our camera image texture needs update: -newcamimage = 0; -needsupdate = 0; - -switch eyecmd - case 1 - % New videoframe received. See code below for actual processing. - newcamimage = 1; - needsupdate = 1; - case 2 - % Eyelink Keyboard query: - [rc, el] = EyelinkGetKey(el); - if rc == 32 - if exist('rM','var') && isa(rM,'arduinoManager') && rM.isOpen - timedTTL(rM,rM.rewardPin,rM.rewardTime); - end - end - if rc>0 && verbose; fprintf('--->>> EYELINKCALLBACK:2 Get Key: %g\n',rc); end - case 3 - % Alert message: - fprintf('--->>> EYELINKCALLBACK:3 Eyelink Alert: %s.\n', msg); - needsupdate = 1; - case 4 - % Image title of camera image transmitted from Eyelink: - if verbose; fprintf('--->>> EYELINKCALLBACK:4 Eyelink image title is %s. [Threshold = %f]\n', msg, callArgs(2)); end %#ok<*UNRCH> - if callArgs(2) ~= -1 - imgtitle = sprintf('Camera: %s [Threshold = %f]', msg, callArgs(2)); - else - imgtitle = msg; - end - needsupdate = 1; - case 5 - % Define calibration target and enable its drawing: - if verbose; fprintf('--->>> EYELINKCALLBACK:5 draw_cal_target: X=%.2g Y=%.2g\n', callArgs(2), callArgs(3)); end - calxy = callArgs(2:3); - clearScreen=1; - needsupdate = 1; - case 6 - % Clear calibration display: - if verbose; fprintf('--->>> EYELINKCALLBACK:6 clear_cal_display.\n'); end - clearScreen=1; - drawInstructions=1; - needsupdate = 1; - case 7 - % Setup calibration display: - if verbose; fprintf('--->>> EYELINKCALLBACK:7 Setup cal display\n'); end - if inDrift - drawInstructions = 0; - inDrift = 0; - else - drawInstructions = 1; - end - - clearScreen=1; - % drawInstructions=1; - drawcount = 0; - lastImageTime = GetSecs; - needsupdate = 1; - case 8 - newImage = 1; - % Setup image display: - eyewidth = callArgs(2); - eyeheight = callArgs(3); - if verbose; fprintf('--->>> EYELINKCALLBACK:8 setup_image_display for %i x %i pixels.\n', eyewidth, eyeheight); end - drawcount = 0; - lastImageTime = GetSecs; - ineyeimagemodedisplay=1; - drawInstructions=1; - needsupdate = 1; - case 9 - % Exit image display: - if verbose - fprintf('--->>> EYELINKCALLBACK:9 exit_image_display.\n'); - fprintf('--->>> EYELINKCALLBACK AVG FPS = %f Hz\n', drawcount / (GetSecs - lastImageTime)); - end - clearScreen=1; - ineyeimagemodedisplay=0; - drawInstructions=1; - needsupdate = 1; - case 10 - % Erase current calibration target: - if verbose; fprintf('--->>> EYELINKCALLBACK:10 erase_cal_target.\n'); end - calxy = []; - clearScreen=1; - needsupdate = 1; - case 11 - if verbose - fprintf('--->>> EYELINKCALLBACK:11 exit_cal_display.\n'); - fprintf('--->>> EYELINKCALLBACK AVG FPS = %f Hz\n', drawcount / (GetSecs - lastImageTime)); - end - clearScreen=1; - %drawInstructions=1; - needsupdate = 1; - case 12 - % New calibration target sound: - if verbose; fprintf('--->>> EYELINKCALLBACK:12 cal_target_beep_hook.\n'); end - EyelinkMakeSound(el, 'cal_target_beep'); - case 13 - % New drift correction target sound: - if verbose; fprintf('--->>> EYELINKCALLBACK:13 dc_target_beep_hook.\n'); end - EyelinkMakeSound(el, 'drift_correction_target_beep'); - case 14 - % Calibration done sound: - errc = callArgs(2); - if verbose; fprintf('--->>> EYELINKCALLBACK:14 cal_done_beep_hook: %i\n', errc); end - if errc > 0 - % Calibration failed: - EyelinkMakeSound(el, 'calibration_failed_beep'); - else - % Calibration success: - EyelinkMakeSound(el, 'calibration_success_beep'); - end - case 15 - % Drift correction done sound: - errc = callArgs(2); - if verbose; fprintf('--->>> EYELINKCALLBACK:15 dc_done_beep_hook: %i\n', errc); end - if errc > 0 - % Drift correction failed: - EyelinkMakeSound(el, 'drift_correction_failed_beep'); - else - % Drift correction success: - EyelinkMakeSound(el, 'drift_correction_success_beep'); - end - % add by NJ - case 16 - [width, height]=Screen('WindowSize', eyewin); - % get mouse - [x,y, buttons] = GetMouse(eyewin); - - HideCursor - if find(buttons) - rc = [width , height, x , y, dw , dh , 1]; - else - rc = [width , height, x , y , dw , dh , 0]; - end - if verbose; fprintf('--->>> EYELINKCALLBACK:16\n'); end - % add by NJ to prevent flashing of text in drift correct - case 17 - inDrift = 1; - otherwise - % Unknown command: - fprintf('--->>> EYELINKCALLBACK : Unknown eyelink command (%i)\n', eyecmd); - return -end - -% Display redraw and update needed? -if ~needsupdate - % Nope. Return from callback: - return; -end - -% Need to rebuild/redraw and flip the display: -% need to clear screen? -if clearScreen==1 - Screen('FillRect', eyewin, el.backgroundcolour); - clearScreen=0; -end -% New video data from eyelink? -if newcamimage - % Video callback from Eyelink: We have a 'eyewidth' by 'eyeheight' pixels - % live eye image from the Eyelink system. Each pixel is encoded as a 4 byte - % RGBA pixel with alpha channel set to a constant value of 255 and the RGB - % channels encoding a 1-Byte per channel R, G or B color value. The - % given 'eyeimgptr' is a specially encoded memory pointer to the memory - % buffer inside Eyelink() that encodes the image. - eyeimgptr = callArgs(2); - eyewidth = callArgs(3); - eyeheight = callArgs(4); - - % Create a new PTB texture of proper format and size and inject the 4 - % channel RGBA color image from the Eyelink memory buffer into the texture. - % Return a standard PTB texture handle to it. If such a texture already - % exists from a previous invocation of this routiene, just recycle it for - % slightly higher efficiency: - eyelinktex = Screen('SetOpenGLTextureFromMemPointer', eyewin, eyelinktex, eyeimgptr, eyewidth, eyeheight, 4, 0, [], GL_RGBA8, GL_RGBA, hostDataFormat); -end - -% If we're in imagemodedisplay, draw eye camera image texture centered in -% window, if any such texture exists, also draw title if it exists. -if ~isempty(eyelinktex) && ineyeimagemodedisplay==1 - imgtitle=EyelinkDrawCameraImage(eyewin, el, eyelinktex, imgtitle,newImage); -end - -% Draw calibration target, if any is specified: -if ~isempty(calxy) - drawInstructions=0; - EyelinkDrawCalibrationTarget(eyewin, el, calxy); -end - -% Need to draw instructions? -if drawInstructions==1 - EyelinkDrawInstructions(eyewin, el,msg); - drawInstructions=0; -end - -% Show it: We disable synchronization of Matlab to the vertical retrace. -% This way, display update itself is still synced and tear-free, but we -% don't waste time waiting for swap completion. Potentially higher -% performance for calibration displays and eye camera image updates... -% Neither do we erase buffer -Screen('Flip', eyewin, [], 1, 1); - -% Some counter, just to measure update rate: -drawcount = drawcount + 1; - -% Done. Return from callback: -return; - - -function EyelinkDrawInstructions(eyewin, el, msg) -%oldFont=Screen(eyewin,'TextFont',el.msgfont); -%oldFontSize=Screen(eyewin,'TextSize',el.msgfontsize); -DrawFormattedText(eyewin, el.helptext, 20, 20, el.msgfontcolour, [], [], [], 1); - -if el.displayCalResults && ~isempty(msg) - DrawFormattedText(eyewin, msg, 20, 400, el.msgfontcolour, [], [], [], 1); -end - -%Screen(eyewin,'TextFont',oldFont); -%Screen(eyewin,'TextSize',oldFontSize); - -function imgtitle=EyelinkDrawCameraImage(eyewin, el, eyelinktex, imgtitle,newImage) -persistent lasttitle; -global dh dw offscreen; -try - if ~isempty(eyelinktex) - eyerect=Screen('Rect', eyelinktex); - % we could cash some of the below values.... - wrect=Screen('Rect', eyewin); - [width, heigth]=Screen('WindowSize', eyewin); - dw=round(el.eyeimgsize/100*width); - dh=round(dw * eyerect(4)/eyerect(3)); - - drect=[ 0 0 dw dh ]; - drect=CenterRect(drect, wrect); - Screen('DrawTexture', eyewin, eyelinktex, [], drect); - % fprintf('EyelinkDrawCameraImage:DrawTexture \n'); - end - % imgtitle - % if title is provided, we also draw title - if ~isempty(eyelinktex) && exist( 'imgtitle', 'var') && ~isempty(imgtitle) - %oldFont=Screen(eyewin,'TextFont',el.imgtitlefont); - %oldFontSize=Screen('TextSize',eyewin,el.imgtitlefontsize); - rect=Screen('TextBounds', eyewin, imgtitle ); - [~, h2]=RectSize(rect); - - % added by NJ as a quick way to prevent over drawing and to clear text - if newImage || isempty(lasttitle) || ~strcmp(imgtitle,lasttitle) - if -1 == Screen('WindowKind', offscreen) - Screen('Close', offscreen); - end - - sn = Screen('WindowScreenNumber', eyewin); - offscreen = Screen('OpenOffscreenWindow', sn, el.backgroundcolour, [], [], 32); - Screen(offscreen,'TextFont',el.imgtitlefont); - Screen(offscreen,'TextSize',el.imgtitlefontsize); - Screen('DrawText', offscreen, imgtitle, width/2-dw/2, heigth/2+dh/2+h2, el.imgtitlecolour); - Screen('DrawTexture',eyewin,offscreen, [width/2-dw/2 heigth/2+dh/2+h2 width/2-dw/2+500 heigth/2+dh/2+h2+500], [width/2-dw/2 heigth/2+dh/2+h2 width/2-dw/2+500 heigth/2+dh/2+h2+500]); - Screen('Close',offscreen); - newImage = 0; - end - %imgtitle=[]; % return empty title, so it doesn't get drawn over and over again. - lasttitle = imgtitle; - end -catch %myerr - fprintf('EyelinkDrawCameraImage:error \n'); - disp(psychlasterror); -end - -function EyelinkMakeSound(el, s) -% set all sounds in one place, sound params defined in -% eyelinkInitDefaults -global aM -switch(s) - case 'cal_target_beep' - doBeep=el.targetbeep; - f=el.cal_target_beep(1); - v=el.cal_target_beep(2); - d=el.cal_target_beep(3); - case 'drift_correction_target_beep' - doBeep=el.targetbeep; - f=el.drift_correction_target_beep(1); - v=el.drift_correction_target_beep(2); - d=el.drift_correction_target_beep(3); - case 'calibration_failed_beep' - doBeep=el.feedbackbeep; - f=el.calibration_failed_beep(1); - v=el.calibration_failed_beep(2); - d=el.calibration_failed_beep(3); - case 'calibration_success_beep' - doBeep=el.feedbackbeep; - f=el.calibration_success_beep(1); - v=el.calibration_success_beep(2); - d=el.calibration_success_beep(3); - case 'drift_correction_failed_beep' - doBeep=el.feedbackbeep; - f=el.drift_correction_failed_beep(1); - v=el.drift_correction_failed_beep(2); - d=el.drift_correction_failed_beep(3); - case 'drift_correction_success_beep' - doBeep=el.feedbackbeep; - f=el.drift_correction_success_beep(1); - v=el.drift_correction_success_beep(2); - d=el.drift_correction_success_beep(3); - otherwise - % some defaults - doBeep=el.feedbackbeep; - f=1000; - v=0.5; - d=0.25; -end - -% function Beeper(frequency, [fVolume], [durationSec]); -if doBeep - if exist('aM','var') && isa(aM,'audioManager') - %fprintf('--->>> EYELINKCALLBACK: Audio %.2f %.2f %.2f\n',f,v,d) - aM.beep(f, d, v); - else - %fprintf('--->>> EYELINKCALLBACK: Beeper %.2f %.2f %.2f\n',f,v,d) - Beeper(f, v, d); - end -end - -%========================================================================================= -function EyelinkDrawCalibrationTarget(eyewin, el, calxy) -width = el.winRect(3); -if isempty(el.customTarget) - size=round(el.calibrationtargetsize/100*width); - if el.calibrationtargetwidth > 0 - insetSize=round(el.calibrationtargetwidth/100*width); - if insetSize < 2; insetSize = 2;end - else - insetSize = 0; - end - if sum(el.calibrationtargetcolour) < 0.6 - insetColour = [1 1 1]; - else - insetColour = [0 0 0]; - end - Screen('gluDisk', eyewin, el.calibrationtargetcolour, calxy(1), calxy(2), size/2); - if insetSize>0 - Screen('FillRect', eyewin, insetColour, CenterRectOnPointd([0 0 size insetSize], calxy(1), calxy(2))); - Screen('FillRect', eyewin, insetColour, CenterRectOnPointd([0 0 insetSize size], calxy(1), calxy(2))); - Screen('gluDisk', eyewin, el.calibrationtargetcolour, calxy(1), calxy(2), insetSize); - end -else - el.customTarget.draw - el.customTarget.animate; -end -%fprintf('--->>> EYELINKCALLBACK EyelinkDrawCalibrationTarget: %.5g %.5g | size:%i / %i px\n',calxy(1),calxy(2),size,insetSize); \ No newline at end of file diff --git a/communication/ioManager.m b/communication/ioManager.m index 9d77dd2b98edd75cc20c2d2d2fa22440ced4610e..10a2dc165277d2f708aabe367f2298286897d024 100644 --- a/communication/ioManager.m +++ b/communication/ioManager.m @@ -1,5 +1,6 @@ % ====================================================================== -%> @brief Input Output manager, currently just a dummy class +%> @brief Input Output manager, a dummy class to allow task to run without +%> strobe hardware %> %> %> @@ -14,6 +15,8 @@ classdef ioManager < optickaCore io %> silentMode logical = true + %> + stimOFFValue = 255 end properties (SetAccess = private, GetAccess = public, Dependent = true) @@ -38,10 +41,10 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function obj = ioManager(varargin) + function me = ioManager(varargin) if nargin == 0; varargin.name = 'IO Manager'; end - obj=obj@optickaCore(varargin); %superclass constructor - if nargin > 0; obj.parseArgs(varargin,obj.allowedProperties); end + me=me@optickaCore(varargin); %superclass constructor + if nargin > 0; me.parseArgs(varargin,me.allowedProperties); end end % =================================================================== @@ -49,9 +52,9 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function open(obj,varargin) - obj.silentMode = true; - obj.isOpen = false; + function open(me, varargin) + me.silentMode = true; + me.isOpen = false; end % =================================================================== @@ -59,7 +62,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function close(obj,varargin) + function close(me,varargin) end @@ -68,7 +71,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function resetStrobe(obj,varargin) + function resetStrobe(me,varargin) end @@ -77,7 +80,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function triggerStrobe(obj,varargin) + function triggerStrobe(me,varargin) end @@ -86,9 +89,10 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function prepareStrobe(obj,value) - obj.lastValue = obj.sendValue; - obj.sendValue = value; + function prepareStrobe(me,value) + if ~exist('value','var'); return; end + me.lastValue = me.sendValue; + me.sendValue = value; end % =================================================================== @@ -96,9 +100,10 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function sendStrobe(obj,value) - obj.lastValue = obj.sendValue; - obj.sendValue = value; + function sendStrobe(me, value) + if ~exist('value','var'); return; end + me.lastValue = me.sendValue; + me.sendValue = value; end % =================================================================== @@ -106,9 +111,10 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function strobeServer(obj,value) - obj.lastValue = obj.sendValue; - obj.sendValue = value; + function strobeServer(me,value) + if ~exist('value','var'); return; end + me.lastValue = me.sendValue; + me.sendValue = value; end % =================================================================== @@ -116,7 +122,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function sendTTL(obj,value) + function sendTTL(me,value) end @@ -125,7 +131,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function startRecording(obj,value) + function startRecording(me,value) end @@ -134,7 +140,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function resumeRecording(obj,value) + function resumeRecording(me,value) end @@ -143,7 +149,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function pauseRecording(obj,value) + function pauseRecording(me,value) end @@ -152,7 +158,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function stopRecording(obj,value) + function stopRecording(me,value) end @@ -161,7 +167,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function startFixation(obj) + function startFixation(me) end @@ -170,7 +176,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function correct(obj) + function correct(me) end @@ -179,7 +185,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function incorrect(obj) + function incorrect(me) end @@ -188,7 +194,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function breakFixation(obj) + function breakFixation(me) end @@ -197,7 +203,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function rstart(obj,varargin) + function rstart(me,varargin) end @@ -207,7 +213,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function rstop(obj,varargin) + function rstop(me,varargin) end @@ -216,7 +222,7 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function timedTTL(obj,varargin) + function timedTTL(me,varargin) end @@ -225,19 +231,19 @@ classdef ioManager < optickaCore %> %> @param % =================================================================== - function type = get.type(obj) - if isempty(obj.io) + function type = get.type(me) + if isempty(me.io) type = 'undefined'; else - if isa(obj.io,'plusplusManager') + if isa(me.io,'plusplusManager') type = 'Display++'; - elseif isa(obj.io,'labJackT') + elseif isa(me.io,'labJackT') type = 'LabJack T4/T7'; - elseif isa(obj.io,'labJack') + elseif isa(me.io,'labJack') type = 'LabJack U3/U6'; - elseif isa(obj.io,'dPixxManager') + elseif isa(me.io,'dPixxManager') type = 'DataPixx'; - elseif isa(obj.io,'arduinoManager') + elseif isa(me.io,'arduinoManager') type = 'Arduino'; end end @@ -247,10 +253,10 @@ classdef ioManager < optickaCore %> @brief Delete method, closes gracefully %> % =================================================================== - function delete(obj) + function delete(me) try - if ~isempty(obj.io) - close(obj); + if ~isempty(me.io) + close(me); end catch warning('IO Manager Couldn''t close hardware') diff --git a/communication/joystickManager.m b/communication/joystickManager.m new file mode 100644 index 0000000000000000000000000000000000000000..5d7b9cfc6afc7aa8e22e2c9ebf29a409286e71f2 --- /dev/null +++ b/communication/joystickManager.m @@ -0,0 +1,128 @@ +% ======================================================================== +classdef joystickManager < optickaCore +%> @class joystickManager +%> @brief Manages the Simia Joystick +%> +%> Copyright ©2014-2023 Ian Max Andolina — released: LGPL3, see LIv12345c12345CENCE.md +% ======================================================================== + + %---------------PUBLIC PROPERTIES---------------% + properties + joystickName = 'simia joystick' + silentMode = false; + verbose = false; + end + + %-----------------CONTROLLED PROPERTIES-------------% + properties (SetAccess = protected, GetAccess = public) + isConnected = false + nJoysticks = 0 + id = 0 + names = {} + end + + %--------------------PROTECTED PROPERTIES----------% + properties (SetAccess = protected, GetAccess = protected) + allowedProperties = {'joystickName'} + end + + %======================================================================= + methods %------------------PUBLIC METHODS + %======================================================================= + + % =================================================================== + function me = joystickManager(varargin) + %joystickManager Construct an instance of this class + %> @fn joystickManager(varargin) + % =================================================================== + args = optickaCore.addDefaults(varargin); + me = me@optickaCore(args); %we call the superclass constructor first + me.parseArgs(args, me.allowedProperties); + + testConnected(me); + + end + + function open(me) + if me.silentMode; me.id = 0; return; end + reset(me) + testConnected(me); + if me.id > 0 + me.silentMode = false; + me.isConnected = true; + end + end + + function reset(me, hardReset) + if exist('hardReset','var') && hardReset == true + Gamepad('Unplug'); + end + me.isConnected = false; + me.silentMode = false; + me.nJoysticks = 0; + me.id = 0; + me.names = {}; + end + + function test(me) + if ~me.isConnected + open(me); + end + if me.silentMode; return; end + s = screenManager; + sv = open(s); + + KbName('UnifyKeyNames') + stopKey = KbName('escape'); + centerKey = KbName('F1'); + oldr=RestrictKeysForKbCheck([stopKey centerKey]); + ListenChar(-1); + + SetMouse(sv.xCenter,sv.yCenter,sv.win); + x = Gamepad('GetAxis', me.id, 1); + y = Gamepad('GetAxis', me.id, 2); + [xm, ym] = GetMouse(sv.win); + HideCursor(s.screen); + + while true + x = Gamepad('GetAxis', me.id, 1); + y = Gamepad('GetAxis', me.id, 2); + [xm,ym] = GetMouse(sv.win); + xy = s.toDegrees([xm ym]); + s.drawText(sprintf('x = %.2f y = %.2f xm = %.2f ym = %.2f',x,y,xy(1),xy(2))); + s.drawCross(1,[1 0 0],xy(1),xy(2)); + s.flip; + [keyDown, ~, keyCode] = optickaCore.getKeys(); + if keyDown + if keyCode(stopKey); break; end + if keyCode(centerKey); SetMouse(sv.xCenter,sv.yCenter,sv.win); end + end + end + + RestrictKeysForKbCheck([]); + ListenChar(0); + ShowCursor; + WaitSecs(1); + reset(me); + SetMouse(500,500,0); + close(s); + end + + end + + methods + function testConnected(me) + if me.silentMode; me.id = 0; return; end + n = Gamepad('GetNumGamepads'); + me.names = Gamepad('GetGamepadNamesFromIndices', 1:n); + tmpid = Gamepad('GetGamepadIndicesFromNames', me.joystickName); + if ~isempty(tmpid) + me.id = tmpid; + fprintf('--->>> joystickManager: %s with ID %i is available\n',me.joystickName,me.id); + else + me.id = 0; + warning('--->>> joystickManager: no joystick attached, replug joystick, and try a Gamepad(''Unplug'') or clear all'); + end + end + end +end \ No newline at end of file diff --git a/communication/jzmqConnection.m b/communication/jzmqConnection.m new file mode 100644 index 0000000000000000000000000000000000000000..c6f9c995657162872a71b15df63786d3f29f3e80 --- /dev/null +++ b/communication/jzmqConnection.m @@ -0,0 +1,741 @@ +classdef jzmqConnection < optickaCore + %> jzmqConnection is a class to handle ØMQ connections for opticka class + %> communication. We use JeroMQ wrapper jzmq, a pure Java implementation of + %> ØMQ. + + properties + %> ØMQ connection type, e.g. 'REQ', 'REP', 'PUB', 'SUB', etc. + type jzmq.SocketType = "REQ" + %> transport for the socket, tcp | ipc | inproc + transport = 'tcp' + %> the address to open, use * for a server to bind to all interfaces + address = 'localhost' + %> the port to open + port = 6666 + %> default size of chunk to read/write for tcp + frameSize = [] + %> default read timeout in ms, -1 is blocking + readTimeOut = -1 + %> default write timeout in ms, -1 is blocking + writeTimeOut = -1 + %> do we log to the command window? + verbose = false + %> for sendCommand and receiveCommand use zmq.core.poll? + alwaysPoll = false + %> use a http proxy to send message + httpProxy = 'http://localhost:9012/api/cmds/proxies/matlab' + end + + properties (Dependent = true) + %> connection endpoint + endpoint + end + + properties (SetAccess = private, GetAccess = public, Transient = true) + %> is this connection open? + isOpen = false + %> Context() + context = [] + %> Socket() + socket = [] + %> poller + poller = [] + %> last message + messages = [] + end + + properties (SetAccess = private, GetAccess = private) + allowedProperties = {'type','protocol','port','address', 'alwaysPoll',... + 'verbose','readTimeOut','writeTimeOut','frameSize','cleanup'}; + end + + methods + + % =================================================================== + function me = jzmqConnection(varargin) + %> @brief Class constructor for jzmqConnection. + %> + %> @details Initializes a jzmqConnection object, setting up default + %> properties and parsing any provided arguments using the optickaCore + %> superclass constructor and argument parsing. + %> + %> @param varargin Optional name-value pairs to override default properties. + %> Allowed properties are defined in `me.allowedProperties`. + %> + %> @return me An instance of the zmqConnection class. + % =================================================================== + args = optickaCore.addDefaults(varargin,struct('name','jzmqConnection')); + me = me@optickaCore(args); %superclass constructor + me.parseArgs(args, me.allowedProperties); + if ~any(contains(javaclasspath, 'jeromq-0.6.0.jar')) + javaaddpath([fileparts(which('jzmq.ZContext')) filesep 'jeromq-0.6.0.jar'],'-begin'); + fprintf('Added JeroMQ jar to javaclasspath.\n'); + end + end + + + % =================================================================== + function status = open(me, context) + %> @brief Opens the ØMQ socket connection. + %> @details Creates the ØMQ context if it doesn't exist, creates the + %> socket based on the `type` property, sets socket options like + %> `ReceiveTimeOut`, `SNDTIMEO`, and `LINGER`, and then either binds (for + %> server types like REP, PUB, PUSH) or connects (for client types) + %> to the specified `endpoint`. Sets the `isOpen` flag to true. + %> @note Does nothing if the connection `isOpen` is already true. + % =================================================================== + arguments (Input) + me + context = [] + end + status = -1; + if me.isOpen; return; end + + if ~isempty(context) && isa(context, 'jzmq.ZContext') + me.context = context; + elseif isempty(me.context) + me.context = jzmq.ZContext(); + else + me.context.close; + end + + me.socket = me.context.createSocket(me.type); + if ~isempty(me.frameSize) + me.socket.pointer.setReceiveBufferSize(me.frameSize); + end + if me.readTimeOut ~= -1 + me.socket.pointer.setReceiveTimeOut(me.readTimeOut); + end + if me.writeTimeOut ~= -1 + me.socket.pointer.setSendTimeOut(me.readTimeOut); + end + me.socket.pointer.setLinger(1000); + switch string(me.type) + case {"REP","PUB","PUSH"} + status = me.socket.bind(me.endpoint); + otherwise + status = me.socket.connect(me.endpoint); + end + if ~status; warning('bind/connect failed...'); end + + me.poller = me.context.createPoller(); + me.poller.register(me.socket,jzmq.ZMQ.PollerEvent.POLLIN); + + if status + me.isOpen = true; + me.addMessage('Socket is opened'); + else + me.addMessage('Socket failed to bind/connect!!!') + try me.socket.close(); end + if ~isnan(me.messages) || ~isempty(me.messages) + error(me.messages(end)); + else + error('Problem opening...') + end + end + end + + % =================================================================== + function revents = poll(me, events, time) + %> @brief poll socket to identify whether we can send ('out') or receive ('in') + %> + %> @param events string 'in' 'out' or 'both' + %> @param time in ms, 0 = no wait, -1 = block until response + % =================================================================== + arguments (Input) + me + events string {mustBeMember(events,... + ["in","out","both"])} = "in" + time (1,1) double = 0 + end + arguments (Output) + revents logical + end + switch events + case 'in' + revents = me.poller.pollin(time); + case 'out' + revents = me.poller.pollout(time); + case 'both' + revents(1) = me.poller.pollin(time); + revents(2) = me.poller.pollout(time); + end + end + + % =================================================================== + function [rep, dataOut, status, nbytes, msg] = sendCommand(me, command, data, getReply) + %> @brief Sends a command and optional data, then waits for a reply. + %> + %> @details Primarily for REQ/REP patterns. Uses `sendObject` to send the + %> command string and serialized data. If successful, it then calls + %> `receiveObject` to wait for and receive the reply command and data. + %> + %> @param command The command string to send. + %> @param data (Optional) MATLAB data to serialize and send along with the command. Defaults to empty. + %> + %> @return rep The reply command string received from the peer. + %> @return dataOut The deserialized MATLAB data received in the reply. + %> @return status 0 on success (send and receive completed), -1 on failure (send or receive failed). + % =================================================================== + rep = ''; dataOut = []; status = -1; + if ~me.isOpen; return; end + if nargin < 4 || isempty(getReply); getReply = true; end + if nargin < 3 || isempty(data); data = {}; end + if nargin < 2 || isempty(command); error('You must pass a command!'); end + try + [status, nbytes, msg] = sendObject(me, command, data, true); + if status ~= 0 + warning(msg); + end + catch ME + t = sprintf('Receive status %i did not return any command: %s - %s...\n', status, ME.identifier, ME.message); + me.addMessage(t); + disp(t); + end + if status == 0 && getReply + [rep, dataOut, ~, t] = receiveObject(me); + if ~isempty(t); me.addMessage(t); end + if me.verbose + disp(t); + disp(dataOut); + end + end + end + + % =================================================================== + function [command, data, msg] = receiveCommand(me, sendReply) + %> @brief Receives a command and associated data, optionally sending an 'ok' reply. + %> @details Calls `receiveObject` to get the command string and any + %> serialized data. If `sendReply` is true (default) and a command was + %> successfully received, it sends back an 'ok' command using `sendObject`. + %> @param sendReply (Optional) Logical flag. If true (default), sends an + %> 'ok' reply upon successful receipt of a command. If false, no reply + %> is sent by this function. Defaults to true. + %> @return command The received command string. Empty if receive failed or timed out. + %> @return data The deserialized MATLAB data received with the command. Empty if no data part or on error. + % =================================================================== + command = ''; data = []; msg = ''; + if ~me.isOpen; return; end + if nargin < 2 + sendReply = true; % Default behavior: send 'ok' reply + end + try + [command, data, msg] = receiveObject(me, true); + if isempty(command) && ~isempty(msg) + msg = sprintf('Receive problem: %s', msg); % Log if receiveObject reported an issue + me.addMessage(msg); + warning(msg) + elseif ~isempty(command) + if me.verbose + fprintf('Received command: «%s»\n', command); + if ~isempty(data) + disp('Received data:'); + disp(data); + end + end + end + catch ME + fprintf('Error during receiveCommand: cmd: %s msg: %s err: %s - %s\n', command, msg, ME.identifier, ME.message); + command = ''; data = []; % Ensure empty return on error + return % Exit function on critical error + end + + % Send 'ok' reply only if requested and a command was actually received + if sendReply && ~isempty(command) + status = sendObject(me, 'ok', {}); + if status ~= 0 + msg = sprintf('Default "ok" reply failed to be sent for command "%s"', command); + me.addMessage(msg); + warning(msg); + me.sendState = false; % Update state on send failure + end + elseif ~isempty(command) + % If reply is not sent here, the caller is responsible. + end + end + + % =================================================================== + function flush(me) + %> @brief Flushes the receive buffer of the socket. + %> @details Temporarily sets the receive timeout (`ReceiveTimeOut`) to 0 (non-blocking) + %> and enters a loop calling `receive` until it returns a status of -1 + %> (indicating no more messages or an error). It then restores the + %> original `readTimeOut`. This is useful for discarding any pending + %> messages in the socket's incoming queue. + % =================================================================== + try + me.set('ReceiveTimeOut', 0); + N = 1000; + while N > 0 + status = 0; + if verifyEvent('in'); [~, status] = receive(me); end + if status == -1; N = 0; end + end + catch ME + if me.verbose; fprintf('Flush error: %s %s', ME.identifier, ME.message); end + end + me.set('ReceiveTimeOut', me.readTimeOut); + end + + % =================================================================== + function value = get(me, option) + %> @brief Gets the value of a ØMQ socket option. + %> @details A wrapper around the `zmq.Socket.get` method. + %> @param option The name of the socket option to retrieve (e.g., 'Linger', 'ReceiveTimeOut'). + %> Case-insensitive, 'ZMQ_' prefix is optional. + %> @return value The current value of the specified socket option. + %> @warning Issues a warning if `option` is not provided. + % =================================================================== + arguments (Input) + me + option string {mustBeMember(option,... + ["Linger","ReceiveBufferSize",... + "SendBufferSize","HWM",... + "ReceiveTimeOut","SendTimeOut",... + "SocketType","Type","Ctx"])} + end + arguments (Output) + value + end + value = me.socket.pointer.("get" + option); + end + + % =================================================================== + function status = set(me, option, value) + %> @brief Sets the value of a ØMQ socket option. + %> @details A wrapper around the `zmq.Socket.set` method. + %> @param option The name of the socket option to set (e.g., 'ReceiveTimeOut', 'LINGER'). + %> Case-insensitive, 'ZMQ_' prefix is optional. + %> @param value The value to assign to the socket option. + %> @return status 0 on success, non-zero on failure. + %> @warning Issues warnings if `option` or `value` are not provided. + % =================================================================== + arguments (Input) + me + option string {mustBeMember(option,... + ["Linger","ReceiveBufferSize",... + "SendBufferSize","HWM",... + "ReceiveTimeOut","SendTimeOut",... + "SocketType","Type","Ctx"])} + value + end + arguments (Output) + status + end + status = me.socket.pointer.("set" + option)(value); + if ~status + warning('zmqConnection:set:failure','Failed to set %s', option) + end + end + + % =================================================================== + function status = send(me, data) + %> @brief Sends raw data over the socket. + %> @details Determines the type of data and calls the appropriate + %> `zmq.Socket` send method (`send_string` for char/string, `send` for + %> uint8). If the data type is different, it attempts to use the + %> private `sendObject` method (which might not be intended for raw data). + %> @param data The data to send. Can be a character array, string, or uint8 array. + %> @return status 0 on success, -1 on failure (e.g., timeout, incorrect socket state). + % =================================================================== + try + status = []; + if ischar(data) || isstring(data) + result = me.socket.send(uint8(data)); + elseif isa(data,'uint8') + result = me.socket.send(data); + else + result = sendObject(me, data); + end + if result; status = 0; end + catch ME + status = -1; + t = sprintf('Couldn''t send, perhaps need to receive first: %s - %s', ME.identifier, ME.message); + me.addMessage(t); + warning(t); + end + end + + % =================================================================== + function [data, status] = receive(me) + %> @brief Receives raw data from the socket. + %> @details Calls `zmq.Socket.recv_multipart` to receive data. If the + %> result is a single-element cell array, it extracts the content. + %> @return data The received data, typically as a uint8 array or potentially + %> a cell array for true multipart messages. Empty on failure or timeout. + %> @return status 0 on success (implied, not explicitly returned on success), + %> -1 on failure (e.g., timeout). + % =================================================================== + data = []; status = -1; + try + data = me.receiveMultipart(); + if iscell(data) && isscalar(data) + data = data{:}; + end + status = 0; + catch ME + t = sprintf('No data received: %s - %s...\n', ME.identifier, ME.message); + me.addMessage(t); + if me.verbose; disp(t); end + end + end + + % =================================================================== + function close(me, keepContext) + %> @brief Closes the ØMQ socket and optionally the context. + %> @details Closes the underlying `zmq.Socket` if it's open. If + %> `keepContext` is false (default), it also closes the `zmq.Context`. + %> Sets the `isOpen` flag to false. + %> @param keepContext (Optional) Logical flag. If true, the ØMQ context + %> is kept open; otherwise (default), the context is also closed. + %> Defaults to false. + %> @note Uses `try...end` blocks to suppress errors during closure. + % =================================================================== + if ~exist('keepContext','var'); keepContext = false; end + + try me.socket.close(); me.socket = []; end %#ok<*TRYNC> + try me.poller.close(); me.poller = []; end %#ok<*TRYNC> + + if ~keepContext + me.context = []; + end + + me.isOpen = false; + end + function delete(me) + %> @brief Class destructor. + %> @details Ensures the socket and context are closed by calling `close(me, false)` + %> when the object is destroyed. + % =================================================================== + close(me, false); + end + + % =================================================================== + function endpoint = get.endpoint(me) + %> @brief Gets the full endpoint string for the connection. + %> @details Constructs the endpoint string (e.g., 'tcp://localhost:5555') + %> based on the `transport`, `address`, and `port` properties. + %> @return endpoint The formatted endpoint string. + % =================================================================== + endpoint = sprintf('%s://%s:%i',me.transport,me.address,me.port); + end + + % =================================================================== + function set.type(me, value) + %> @brief Sets the socket type. + %> @details Validates the socket type against a list of allowed types + %> and sets the `type` property. Throws an error if the type is invalid. + %> @param value The socket type to set (e.g., 'REQ', 'REP', 'PUB', 'SUB'). + %> @note Uses SocketType enum, defaults to 'REQ'. + % =================================================================== + arguments + me jzmqConnection + value (1,1) jzmq.SocketType + end + + try + me.type = value; + catch + me.type = 'REQ'; + warning('Invalid socket type. Defaulting to REQ.'); + end + end + + % =================================================================== + function [status, nbytes, msg] = sendObject(me, command, data, useJSON) + %> @brief (Private) Sends a command string and optional serialized MATLAB data. + %> @details This is the core sending method used by public methods like + %> `sendCommand` and `receiveCommand` (for replies). It checks if the + %> socket is open, validates the command is a string, serializes the + %> `data` using `getByteStreamFromArray` (if provided), and sends the + %> command and data as a two-part message using `zmq.Socket.send` with + %> the 'sndmore' flag if both parts exist. Handles sending only command, + %> only data, or an empty message if both are empty. + %> @param command The command string to send. Must be char or string. + %> + %> @param data (Optional) MATLAB data to serialize and send. + %> @param useJSON (optional) wrap command and data with JSON + %> + %> @return status 0 on success, -1 on failure. + %> @return nbytes The total number of bytes sent across all parts. + %> @return msg An error message string if `status` is -1. + % =================================================================== + + status = -1; nbytes = 0; msg = ''; + + % Check if the socket is open + if ~me.isOpen + error('Socket is not open. Please open the socket before sending data.'); + end + + % Check if the command is a string + if ~exist('command','var') || ~ischar(command) && ~isstring(command) + error('Command must be a string or character array.'); + end + + if nargin < 5 + options = {}; + end + + if nargin < 4 || isempty(useJSON) + useJSON = true; + end + + if nargin < 3 + data = []; + end + + % Serialize the object if it's not empty + if ~isempty(data) + serialData = getByteStreamFromArray(data); + else + % If no data, just send an empty array + serialData = uint8([]); + end + + try + if useJSON + j.command = command; + j.dataType = 'byteStream'; + j.data = serialData; + j = jsonencode(j); + b = unicode2native(j, 'UTF-8'); + nbytes = me.socket.send(uint8(b), options{:}); + elseif ~isempty(command) && ~isempty(serialData) + n1 = me.socket.send(uint8(command), 'sndmore'); + n2 = me.socket.send(serialData, options{:}); + nbytes = n1 + n2; + elseif ~isempty(command) + % Just send text + nbytes = me.socket.send(uint8(command), options{:}); + elseif ~isempty(serialData) + % Just send data + nbytes = me.socket.send(serialData, options{:}); + else + % Send empty message + nbytes = me.socket.send(uint8(''), options{:}); + end + status = 0; + catch ME + status = -1; + msg = [ME.identifier, ME.message]; + end + end + + % =================================================================== + function [command, data, raw, msg] = receiveObject(me, useJSON, options) + %> @brief (Private) Receives a command string and optional serialized MATLAB data. + %> @details This is the core receiving method. It calls `zmq.Socket.recv` + %> to get the first part (expected to be the command string). It checks + %> the 'rcvmore' socket option to see if a second part (data) exists. + %> If so, it calls `zmq.Socket.recv_multipart` to get the remaining part(s), + %> concatenates them if necessary, and deserializes the result using + %> `getArrayFromByteStream`. + %> @param options (Optional) Cell array of flags for the initial `recv` call (e.g., {'ZMQ_DONTWAIT'}). + %> @return command The received command string. Empty on failure or timeout. + %> @return data The deserialized MATLAB data. Empty if no data part, deserialization fails, or on error. + %> @return raw the original structure + %> @return msg An error message string if receiving the command failed or deserialization failed. + %> @note This is a private method. Throws an error if the socket is not open. Logs deserialization errors. + % =================================================================== + % Check if the socket is open + if ~me.isOpen + error('Socket is not open. Please open the socket before sending data.'); + end + + if nargin < 3; options = {}; end + + if nargin < 2 || isempty(useJSON); useJSON = true; end + + command = ''; data = []; msg = ''; raw = []; frames = {}; + + try + frames = me.receiveMultipart(); + catch ME + warning('Failed to get object: %s - %s', ME.identifier, ME.message); + if matches(ME.identifier,'zmq:core:recv:EFSM') + t = sprintf('EFSM error, let''s try to flush and send'); + me.addMessage(t); + if me.verbose; disp(t); end + me.flush; + me.sendObject('error',{''}); + end + end + if isempty(frames); return; end + + if useJSON + try + if iscell(frames) + str = native2unicode([frames{1:end}], 'UTF-8'); + else + str = native2unicode(frames, 'UTF-8'); + end + src = jsondecode(str); + if isstruct(src) + command = src.command; + if isfield(src,'data') && ~isempty(src.data) + data = getArrayFromByteStream(uint8(src.data)); + end + end + raw = src; + catch ME + msg = 'Cannot parse JSON...'; + me.addMessage(msg); + getReport(ME) + return + end + else + command = frames{1}; + if command == -1 + msg = 'No data received...'; + me.addMessage(msg); + command = ''; + return + end + command = char(command); + if length(frames) > 1 + data = frames{2:end}; + if iscell(data) + data = [data{:}]; + end + % Deserialize the object if it's not empty + if ~isempty(data) + try + data = getArrayFromByteStream(data); + catch ME + msg = sprintf('Failed to deserialize object: %s - %s', ME.identifier, ME.message); + me.addMessage(msg); + warning(msg); + data = []; + end + else + data = []; + end + else + % No object part in the message + data = []; + end + end + end + + + % =================================================================== + function [response, dataOut] = sendViaProxy(me, command, data, proxy) + %> @brief sendViaProxy - Sends data to a specified URL via a proxy + %> + %> This function sends the provided data to the specified URL using + %> a proxy server and returns the server's response. + %> + %> @param command A string specifying the command to be sent + %> @param data A MATLAB object containing the data to be sent in the request. + %> + %> @return response The response from the server after sending the data. + % =================================================================== + arguments(Input) + me jzmqConnection + command string = 'echo' + data = [] + proxy string = me.httpProxy + end + arguments(Output) + response struct + dataOut + end + response = []; dataOut = []; + opts = weboptions('MediaType', 'application/json', 'ContentType', 'json', 'CharacterEncoding', 'UTF-8'); + data = getByteStreamFromArray(data); + dataStruct = struct('command',command,'dataType','byteStream','data', data); + try + response = webwrite(proxy, dataStruct, opts); + fprintf('sendViaProxy send: "%s" - respone: \n%s\n', command, jsonencode(response)); + if isstruct(response) && isfield(response,'dataType') && matches(response.dataType,'byteStream') + try dataOut = getArrayFromByteStream(uint8(response.data)); end + if ~isempty(dataOut) + disp(dataOut); + end + end + catch ME + response = struct('error',string(ME.identifier)); + dataOut = struct('error',string(ME.identifier),'data', ME.message,'cause','Ensure cogmoteGO and theConductor are running!!!'); + warning('sendViaProxy: "%s" failed - error: %s - %s\n', command, ME.identifier, ME.message); + end + end + + end + + methods (Access = private) + + % =================================================================== + %> @brief Receives all parts of a multipart message from the socket. + %> @details Calls `recv` on the socket to get the first part, then + %> continues calling `recv` while `hasReceiveMore` is true, collecting + %> all parts into a cell array. If only one part is received, returns + %> it directly. + %> @return data Cell array of message parts, or the single part if only one. + % =================================================================== + function data = receiveMultipart(me) + data = {}; + data{1} = me.socket.recv(); + a = 2; + while me.socket.hasReceiveMore() + data{a} = me.socket.recv(); + end + if iscell(data) && isscalar(data) + data = data{:}; + end + end + + % =================================================================== + %> @brief Adds a message to the object's message log. + %> @details Converts the input `msg` to a string if necessary, then + %> appends it to the `messages` property. Handles struct, object, + %> char, and string types. If `msg` is empty or not provided, does nothing. + %> @param msg The message to add (struct, object, char, string, or array). + % =================================================================== + function addMessage(me, msg) + if nargin < 2; return; end + if isstruct(msg) || isobject(msg) + msg = formattedDisplayText(msg,"NumericFormat","short","LineSpacing","compact"); + elseif ischar(msg) + msg = string(msg); + elseif length(msg) > 1 + msg = join(msg); + else + return; + end + if isempty(me.messages) + me.messages(1) = msg; + else + me.messages(end+1) = msg; + end + end + + % =================================================================== + %> @brief Checks if the socket is ready for the specified event(s). + %> @details Uses polling to determine if the socket can send ('out'), + %> receive ('in'), or neither ('none'). If `alwaysPoll` is false, + %> always returns true. + %> @param events String: 'in', 'out', or 'none'. + %> @return out Logical true if the event is ready, false otherwise. + % =================================================================== + function out = verifyEvent(me, events) + if ~me.alwaysPoll; out = true; return; end + out = false; + r = poll(me,'both',0); + switch events + case 'in' + if matches(r,{'in','both'}) + out = true; + end + case 'out' + if matches(r,{'out','both'}) + out = true; + end + case 'none' + if matches(r,'none') + out = true; + end + end + end + end +end diff --git a/communication/labJackT.m b/communication/labJackT.m index 3c5d374cef489ea2afaa1da4332c4f4996322274..8e46aa893b700d0f020eac1bf054a37d2db895f1 100644 --- a/communication/labJackT.m +++ b/communication/labJackT.m @@ -5,9 +5,12 @@ %> %> Example: %> -%> ``` +%> ```matlab %> l = labJackT('openNow', true); -%> l.sendStrobe(128); % sends 128 via EIO 8 bits +%> l.sendStrobe(128); % sends value 128 : 0-2047 controls EIO0-8 & CIO0-3 11bit word - 2048 TTLs CIO-4 for 10ms +%> v = l.getAIN(1); % get a voltage +%> l.startStream() % start data streaming mode +%> l.stopStream(); % stop data streaming mode %> l.close; %> ``` %> @@ -16,7 +19,7 @@ classdef labJackT < handle properties - %> friendly object name, setting this to 'null' will force silentMode=1 + %> friendly object name, setting this to 'null' will force silentMode = true name char = 'labJackT' %> what LabJack device to use; 4 = T4, 7 = T7 deviceID double = 4 @@ -25,27 +28,27 @@ classdef labJackT < handle %> Connection type: ANY, USB, TCP, ETHERNET, WIFI connectType char = 'ANY' %> IP address if using network - IP char = '' + IP char = '' %> strobeTime is time of strobe in ms; max = 100ms - strobeTime double = 5 + strobeTime uint32 = 5 %> streamChannels which channels to stream - streamChannels double = 0 + streamChannels double = 0 %> stream sample rate (Hz) - streamSampleRate double = 2000; + streamSampleRate double = 2000; %> number of stream samples to collect in each read - streamSamples double = 500; + streamSamples double = 500; %> resolution of the stream 0-5 for T4, 0 is default (=1), 5 being best/slowest - streamResolution double = 0 + streamResolution double = 0 %> timeout for communication in ms timeOut double = 500 %> header needed by loadlib - header char = '/usr/local/include/LabJackM.h' + header char = '/usr/local/include/LabJackM.h' %> the library itself library char = '/usr/local/lib/libLabJackM' %> do we log everything to the command window? verbose logical = true - %> allows the constructor to run the open method immediately (default) - openNow logical = true + %> allows the constructor to run the open method immediately + openNow logical = false %> silentMode=true allows one to gracefully fail methods without a labJack connected silentMode logical = false %> comment @@ -53,8 +56,8 @@ classdef labJackT < handle end properties (Hidden = true) - winLibrary = 'C:\Windows\System32\LabJackM' - winHeader = 'C:\Program Files (x86)\LabJack\Drivers\LabJackM.h' + winLibrary = 'C:\Windows\System32\LabJackM' + winHeader = 'C:\Program Files (x86)\LabJack\Drivers\LabJackM.h' end properties (SetAccess = private, GetAccess = public) @@ -63,7 +66,7 @@ classdef labJackT < handle %> is streaming? isStreaming logical = false %> send this value for the next sendStrobe - sendValue double = 0 + sendValue int32 = 0 %> last value sent lastValue double = [] %> function list returned from loading LJM @@ -107,17 +110,17 @@ classdef labJackT < handle LJM_ctANY int32 = 0 LJM_ctUSB int32 = 1 LJM_ctTCP int32 = 2 - LJM_ctETHERNET int32 = 3 + LJM_ctETHERNET int32 = 3 LJM_ctWIFI int32 = 4 LJM_UINT16 int32 = 0 LJM_UINT32 int32 = 1 LJM_INT32 int32 = 2 LJM_FLOAT32 int32 = 3 - LJM_TESTRESULT uint32 = 1122867 + LJM_TESTRESULT uint32 = 1122867 %> RAM address for communication - RAMAddress uint32 = 46000 + RAMAddress uint32 = 46080 %> minimal lua server to allow fast asynchronous strobing of EIO & CIO - miniServer char = 'LJ.setLuaThrottle(100)local a=MB.R;local b=MB.W;local c=-1;b(2601,0,255)b(2602,0,255)b(2501,0,0)b(2502,0,0)b(46000,3,0)while true do c=a(46000,3)if c>=1 and c<=255 then b(2501,0,c)b(61590,1,2000)b(2501,0,0)elseif c>=256 and c<=271 then b(2502,0,c-256)b(61590,1,100000)b(61590,1,100000)b(61590,1,100000)b(2502,0,0)elseif c==0 then b(2501,0,0)end;if c>-1 then b(46000,3,-1)end end' + miniServer char = 'LJ.setLuaThrottle(80)local a=MB.R;local b=MB.W;local c=-1;local d=0;local e=0;b(2601,0,255)b(2602,0,255)b(2501,0,0)b(2502,0,0)b(46080,2,0)while true do c=a(46080,2)if c>=1 and c<=2047 then d=bit.band(c,0xff)e=bit.band(bit.rshift(c,8),0xff)b(2501,0,d)if e>0 then b(2502,0,e)end;b(61590,1,2000)b(2501,0,0)if e>0 then b(2502,0,0)end elseif c>2047 then b(2502,0,8)b(61590,1,10000)b(2502,0,0)elseif c==0 then b(2501,0,0)end;if c>-1 then b(46080,2,-1)end end' %> test Lua server, just spits out time every second testServer char = 'LJ.IntervalConfig(0,1000)while true do if LJ.CheckInterval(0)then print(LJ.Tick())end end' %> library name @@ -158,7 +161,7 @@ classdef labJackT < handle me.uuid = num2str(dec2hex(floor((now - floor(now))*1e10))); if strcmpi(me.name, 'null') %we were deliberately passed null, means go into silent mode me.silentMode = true; - me.openNow = false; + me.openNow = false; me.salutation('CONSTRUCTOR Method','labJack running in silent mode...') end if IsWin @@ -258,13 +261,20 @@ classdef labJackT < handle me.testValue = uint32(vals(3)); %initialise EIO and CIO - err = calllib(me.libName, 'LJM_eWriteNames', me.handle, 4, {'EIO_DIRECTION','CIO_DIRECTION',... - 'EIO_STATE','CIO_STATE'}, [255 255 0 0], 0); + err = calllib(me.libName, 'LJM_eWriteNames', me.handle, 6, {'FIO_DIRECTION','EIO_DIRECTION','CIO_DIRECTION',... + 'FIO_STATE','EIO_STATE','CIO_STATE'}, [255 255 255 0 0 0], 0); me.checkError(err); me.isValid = me.isHandleValid; - if ~me.silentMode;me.salutation('OPEN method',sprintf('Loading the LabJackT in %.2fsecs: success!',toc(tS)));end + if ~me.isServerRunning + warning('===>>> LabJack T: Lua server NOT running, strobes will fail! Will run labJackT.initialiseServer'); + initialiseServer(me); + else + me.salutation('OPEN method','Lua Server is Running :-)'); + end + + me.salutation('OPEN method',sprintf('Loading the LabJackT in %.2fsecs: success!',toc(tS))); end % =================================================================== @@ -351,65 +361,59 @@ classdef labJackT < handle % =================================================================== %> @brief sends a value to RAMAddress, requires the Lua server to - %> be running, 0-255 control EIO, 256-271 controls CIO + %> be running, 0-2047 controls EIO0-8 & CIO0-3 - 2048 TTLs CIO-4 %> + %> @param value 0 - 2048 % =================================================================== function sendStrobe(me, value) if me.silentMode || isempty(me.handle); return; end - if ~exist('value','var'); value = me.sendValue; end - calllib(me.libName, 'LJM_eWriteAddress', me.handle, me.RAMAddress, me.LJM_FLOAT32, value); + if ~exist('value','var') || isempty(value); value = me.sendValue; end + calllib(me.libName, 'LJM_eWriteAddress', me.handle, me.RAMAddress, me.LJM_INT32, int32(value)); if me.verbose; fprintf('--->>> LabjackT:sendStrobe Sending strobe: %i\n',value); end end + function setStrobeValue(me, value) + me.sendValue = value; + end % =================================================================== %> @brief % =================================================================== - function sendStrobedEIO(me,value) + function sendStrobedEIO(me, value) if me.silentMode || isempty(me.handle); return; end + if ~exist('value','var') || isempty(value); value = me.sendValue; end calllib(me.libName, 'LJM_eWriteAddresses', me.handle,... - 3, [2501 61590 2501], [me.LJM_UINT16 me.LJM_UINT32 me.LJM_UINT16], [value me.strobeTime*1000 0], 0); + 3, [2501 61590 2501], [me.LJM_UINT16 me.LJM_UINT32 me.LJM_UINT16], [uint16(value) me.strobeTime*1000 uint16(0)], 0); end % =================================================================== %> @brief % =================================================================== - function sendStrobedCIO(me,value) + function sendStrobedCIO(me, value) if me.silentMode || isempty(me.handle); return; end + if ~exist('value','var') || isempty(value); value = me.sendValue; end calllib(me.libName, 'LJM_eWriteAddresses', me.handle,... - 3, [2502 61590 2502], [me.LJM_UINT16 me.LJM_UINT32 me.LJM_UINT16], [value me.strobeTime*1000 0], 0); + 3, [2502 61590 2502], [me.LJM_UINT16 me.LJM_UINT32 me.LJM_UINT16], [uint16(value) me.strobeTime*1000 uint16(0)], 0); end - - % =================================================================== - %> @brief Prepare Strobe Word - %> - % =================================================================== - function prepareStrobe(me,value) - if me.silentMode || isempty(me.handle); me.sendValue=value; return; end - me.lastValue = me.sendValue; - me.sendValue = value; - cmd = zeros(64,1); - [err,~,~,~,~,~,~,cmd] = calllib(me.libName, 'LJM_AddressesToMBFB',... - 64, [2501 61590 2501], [0 1 0], [1 1 1], [1 1 1], [value me.strobeTime*1000 0], 3, cmd); - me.command = cmd; - me.checkError(err); - if me.verbose;fprintf('--->>> LabJackT:prepareStrobe saving strobe value: %i\n',value);end + + function setFIO(me, line, value, direction) + end % =================================================================== - %> @brief Send the Strobe command - %> - %> + %> @brief % =================================================================== - function strobeWord(me) - if me.silentMode || isempty(me.handle) || isempty(me.command); return; end - me.writeCmd(); + function sendTTL(me,fio,value) + if me.silentMode || isempty(me.handle); return; end + if ~exist('fio','var') || isempty(fio); fio = 1; end + if ~exist('value','var') || isempty(value); return; end + end % =================================================================== - %> @brief setAIO - %> setAIO sets the value for FIO, + %> @brief getAIN + %> getAIN gets the value from FIO, %> @param channels AIN channels 0-3 - %> @return out voltages + %> @return out voltages % =================================================================== function out = getAIN(me,channels) if me.silentMode || isempty(me.handle); return; end @@ -514,8 +518,8 @@ classdef labJackT < handle if mod(plotLoop,updateN) == 0 plot(ax,time,data); ax.HitTest = 'off'; - title(sprintf('Dev Backlog: %s | Lib Backlog: %s | Err: %i', ... - sprintf('%i ',dBList), sprintf('%i ',lBList), err)); + title(sprintf('%i -- Dev Backlog: %s | Lib Backlog: %s | Err: %i', ... + plotLoop, sprintf('%i ',dBList), sprintf('%i ',lBList), err)); drawnow; fprintf('\b\b\b\b\b\b\b\b\b\b\bLoop: %5i',plotLoop) end @@ -570,7 +574,7 @@ classdef labJackT < handle me.checkError(err,true); [~, ~, len] = calllib(me.libName, 'LJM_eReadName', me.handle, 'LUA_SOURCE_SIZE', 0); if me.verbose; fprintf('===>>> LUA Server init code sent: %i | recieved: %i | strobe length: %i\n',strN,len,me.strobeTime*1e3); end - if len < strN; error('Problem with the upload...'); end + if len < strN; error('Problem with the upload, check with Kipling...'); end %copy to flash calllib(me.libName, 'LJM_eWriteNames', me.handle, 2, {'LUA_SAVE_TO_FLASH','LUA_RUN_DEFAULT'}, ... @@ -619,6 +623,9 @@ classdef labJackT < handle %======================================================================= methods (Hidden = true) %======================================================================= + + % These commands are for compatability with older hardware and + % protocols etc. % =================================================================== %> @brief @@ -626,7 +633,7 @@ classdef labJackT < handle %> @param % =================================================================== function resetStrobe(me,varargin) - + me.sendValue = 0; end % =================================================================== @@ -664,14 +671,14 @@ classdef labJackT < handle function stopRecording(me,varargin) end - + % =================================================================== %> @brief %> %> @param % =================================================================== - function startFixation(me, varargin) - sendStrobe(me,248); + function rstop(me,varargin) + end % =================================================================== @@ -679,13 +686,38 @@ classdef labJackT < handle %> %> @param % =================================================================== - function sendTTL(me, varargin) - + function startFixation(me, varargin) + sendStrobe(me,248); end % =================================================================== - %> @brief sends a value to RAMAddress, requires the Lua server to - %> be running, 0-255 control EIO, 256-271 controls CIO + %> @brief LEGACY Command - create a command to strobe EIO 0-255 ONLY + %> + % =================================================================== + function prepareStrobe(me,value) + if me.silentMode || isempty(me.handle); me.sendValue=value; return; end + me.lastValue = me.sendValue; + me.sendValue = value; + cmd = zeros(64,1); + [err,~,~,~,~,~,~,cmd] = calllib(me.libName, 'LJM_AddressesToMBFB',... + 64, [2501 61590 2501], [0 1 0], [1 1 1], [1 1 1], [value me.strobeTime*1000 0], 3, cmd); + me.command = cmd; + me.checkError(err); + if me.verbose;fprintf('--->>> LabJackT:prepareStrobe saving strobe value: %i\n',value);end + end + + % =================================================================== + %> @brief LEGACY Command - create a command to strobe EIO 0-255 ONLY + %> + %> + % =================================================================== + function strobeWord(me) + if me.silentMode || isempty(me.handle) || isempty(me.command); return; end + me.writeCmd(); + end + + % =================================================================== + %> @brief LEGACY - use sendStrobe %> % =================================================================== function strobeServer(me, value) @@ -879,4 +911,4 @@ classdef labJackT < handle end end end -end \ No newline at end of file +end diff --git a/communication/nirSmartManager.m b/communication/nirSmartManager.m new file mode 100644 index 0000000000000000000000000000000000000000..2b33e68e732f9c4fb3185c79a4ed318cc38f30f6 --- /dev/null +++ b/communication/nirSmartManager.m @@ -0,0 +1,284 @@ +% ====================================================================== +%> @brief Input Output manager, currently just a dummy class +%> +%> +%> +%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md +% ====================================================================== +classdef nirSmartManager < optickaCore + + properties + ip char = '127.0.0.1' + port double = 8889 + %> the hardware object + io dataConnection + %> + silentMode logical = false + %> + stimOFFValue = 255 + %> + verbose = false + end + + properties (SetAccess = private, GetAccess = public, Dependent = true) + %> hardware class + %type char + end + + properties (SetAccess = protected, GetAccess = public) + isOpen logical = false + sendValue + lastValue + end + + properties (SetAccess = private, GetAccess = private) + %> properties allowed to be modified during construction + allowedProperties = {'ip','port','io','silentMode','stimOFFValue','verbose'} + end + + methods + % =================================================================== + %> @brief Class constructor + %> + %> @param + % =================================================================== + function me = nirSmartManager(varargin) + if nargin == 0; varargin.name = 'NirSmart Manager'; varargin.type = 'NirSmart'; end + me=me@optickaCore(varargin); %superclass constructor + if nargin > 0; me.parseArgs(varargin,me.allowedProperties); end + me.io = dataConnection('rAddress', me.ip, 'rPort', me.port); + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function open(me,varargin) + if me.silentMode; me.isOpen = true; return; end + if isempty(me.io) + me.io = dataConnection('rAddress', me.ip, 'rPort', me.port); + end + me.io.rAddress = me.ip; + me.io.rPort = me.port; + me.io.readSize = 1024; + try + open(me.io); + catch ERR + getREport(ERR); + me.isOpen = false; + return + end + if me.io.isOpen + me.isOpen = true; + fprintf('--->>> Connected to %s : %i\n',me.ip,me.port) + else + me.isOpen = false; + error('===>>> !!! Cannot open TCP port'); + end + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function close(me,varargin) + try close(me.io); end %#ok<*TRYNC> + me.isOpen = false; + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function prepareStrobe(me,value) + if ~me.isOpen || me.silentMode; return; end + me.lastValue = me.sendValue; + me.sendValue = value; + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function sendStrobe(me, value) + if ~me.isOpen || me.silentMode; return; end + if ~exist('value','var') || isempty(value) + if ~isempty(me.sendValue) + value = me.sendValue; + else + warning('--->>> nirSmartManager No strobe value set, no strobe sent!'); return + end + end + sendString = [250,252,251,253,3,value,252,253,250,251]; + write(me.io, uint8(sendString)); + me.sendValue = value; + if me.verbose; fprintf('===>>> nirSmartManager: We sent strobe %i!\n',value);end + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function resetStrobe(me,varargin) + if ~me.isOpen || me.silentMode; return; end + me.sendValue = 0; + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function triggerStrobe(me,varargin) + if ~me.isOpen || me.silentMode; return; end + if isempty(me.sendValue); warning('--->>> nirSmartManager No strobe value set, trigger failed!'); return; end + sendString = [250,252,251,253,3,me.sendValue,252,253,250,251]; + write(me.io, uint8(sendString)); + end + + end + + methods (Hidden = true) + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function strobeServer(me,value) + me.sendStrobe(value); + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function sendTTL(me,value) %#ok<*INUSD> + + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function startRecording(me,value) + + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function resumeRecording(me,value) + + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function pauseRecording(me,value) + + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function stopRecording(me,value) + + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function startFixation(me) + + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function correct(me) + + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function incorrect(me) + + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function breakFixation(me) + + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function rstart(me,varargin) + + end + + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function rstop(me,varargin) + + end + + % =================================================================== + %> @brief + %> + %> @param + % =================================================================== + function timedTTL(me,varargin) + + end + + + % =================================================================== + %> @brief Delete method, closes gracefully + %> + % =================================================================== + function delete(me) + try + if ~isempty(me.io) + close(me.io); + end + catch + warning('IO Manager Couldn''t close hardware') + end + end + + end + +end + diff --git a/communication/parsemsgpack.m b/communication/parsemsgpack.m new file mode 100644 index 0000000000000000000000000000000000000000..5137a5442b198ae6d959d96b8ba8c3bad60bcf4d --- /dev/null +++ b/communication/parsemsgpack.m @@ -0,0 +1,196 @@ +%PARSEMSGPACK parses a msgpack byte buffer into Matlab data structures +% PARSEMSGPACK(BYTES) +% reads BYTES as msgpack data, and creates Matlab data structures +% from it. The number of bytes consumed by the parsemsgpack call +% is returned in the variable IDX. +% - strings are converted to strings +% - numbers are converted to appropriate numeric values +% - true, false are converted to logical 1, 0 +% - nil is converted to [] +% - arrays are converted to cell arrays +% - maps are converted to containers.Map + +% (c) 2016 Bastian Bechtold +% This code is licensed under the BSD 3-clause license + +function [obj, idx] = parsemsgpack(bytes) + [obj, idx] = parse(uint8(bytes(:)), 1); +end + +function [obj, idx] = parse(bytes, idx) + + % masks: + b10000000 = 128; + b01111111 = 127; + b11000000 = 192; + b00111111 = 63; + b11100000 = 224; + b00011111 = 31; + b11110000 = 240; + b00001111 = 15; + % values: + b00000000 = 0; + b10010000 = 144; + b10100000 = 160; + + currentbyte = bytes(idx); + + if bitand(b10000000, currentbyte) == b00000000 + % decode positive fixint + obj = int8(currentbyte); + idx = idx + 1; + return + elseif bitand(b11100000, currentbyte) == b11100000 + % decode negative fixint + obj = typecast(currentbyte, 'int8'); + idx = idx + 1; + return + elseif bitand(b11110000, currentbyte) == b10000000 + % decode fixmap + len = double(bitand(b00001111, currentbyte)); + [obj, idx] = parsemap(len, bytes, idx+1); + return + elseif bitand(b11110000, currentbyte) == b10010000 + % decode fixarray + len = double(bitand(b00001111, currentbyte)); + [obj, idx] = parsearray(len, bytes, idx+1); + return + elseif bitand(b11100000, currentbyte) == b10100000 + % decode fixstr + len = double(bitand(b00011111, currentbyte)); + [obj, idx] = parsestring(len, bytes, idx + 1); + return + end + + switch currentbyte + case 192 % nil + obj = []; + idx = idx+1; + % case 193 % unused + case 194 % false + obj = false; + idx = idx+1; + case 195 % true + obj = true; + idx = idx+1; + case 196 % bin8 + len = double(bytes(idx+1)); + [obj, idx] = parsebytes(len, bytes, idx+2); + case 197 % bin16 + len = double(bytes2scalar(bytes(idx+1:idx+2), 'uint16')); + [obj, idx] = parsebytes(len, bytes, idx+3); + case 198 % bin32 + len = double(bytes2scalar(bytes(idx+1:idx+4), 'uint32')); + [obj, idx] = parsebytes(len, bytes, idx+5); + case 199 % ext8 + len = double(bytes(idx+1)); + [obj, idx] = parseext(len, bytes, idx+2); + case 200 % ext16 + len = double(bytes2scalar(bytes(idx+1:idx+2), 'uint16')); + [obj, idx] = parseext(len, bytes, idx+3); + case 201 % ext32 + len = double(bytes2scalar(bytes(idx+1:idx+4), 'uint32')); + [obj, idx] = parseext(len, bytes, idx+5); + case 202 % float32 + obj = bytes2scalar(bytes(idx+1:idx+4), 'single'); + idx = idx+5; + case 203 % float64 + obj = bytes2scalar(bytes(idx+1:idx+8), 'double'); + idx = idx+9; + case 204 % uint8 + obj = bytes(idx+1); + idx = idx+2; + case 205 % uint16 + obj = bytes2scalar(bytes(idx+1:idx+2), 'uint16'); + idx = idx+3; + case 206 % uint32 + obj = bytes2scalar(bytes(idx+1:idx+4), 'uint32'); + idx = idx+5; + case 207 % uint64 + obj = bytes2scalar(bytes(idx+1:idx+8), 'uint64'); + idx = idx+9; + case 208 % int8 + obj = bytes2scalar(bytes(idx+1), 'int8'); + idx = idx+2; + case 209 % int16 + obj = bytes2scalar(bytes(idx+1:idx+2), 'int16'); + idx = idx+3; + case 210 % int32 + obj = bytes2scalar(bytes(idx+1:idx+4), 'int32'); + idx = idx+5; + case 211 % int64 + obj = bytes2scalar(bytes(idx+1:idx+8), 'int64'); + idx = idx+9; + case 212 % fixext1 + [obj, idx] = parseext(1, bytes, idx+1); + case 213 % fixext2 + [obj, idx] = parseext(2, bytes, idx+1); + case 214 % fixext4 + [obj, idx] = parseext(4, bytes, idx+1); + case 215 % fixext8 + [obj, idx] = parseext(8, bytes, idx+1); + case 216 % fixext16 + [obj, idx] = parseext(16, bytes, idx+1); + case 217 % str8 + len = double(bytes(idx+1)); + [obj, idx] = parsestring(len, bytes, idx+2); + case 218 % str16 + len = double(bytes2scalar(bytes(idx+1:idx+2), 'uint16')); + [obj, idx] = parsestring(len, bytes, idx+3); + case 219 % str32 + len = double(bytes2scalar(bytes(idx+1:idx+4), 'uint32')); + [obj, idx] = parsestring(len, bytes, idx+5); + case 220 % array16 + len = double(bytes2scalar(bytes(idx+1:idx+2), 'uint16')); + [obj, idx] = parsearray(len, bytes, idx+3); + case 221 % array32 + len = double(bytes2scalar(bytes(idx+1:idx+4), 'uint32')); + [obj, idx] = parsearray(len, bytes, idx+5); + case 222 % map16 + len = double(bytes2scalar(bytes(idx+1:idx+2), 'uint16')); + [obj, idx] = parsemap(len, bytes, idx+3); + case 223 % map32 + len = double(bytes2scalar(bytes(idx+1:idx+4), 'uint32')); + [obj, idx] = parsemap(len, bytes, idx+5); + otherwise + error('transplant:parsemsgpack:unknowntype', ... + ['Unknown type "' dec2bin(currentbyte) '"']); + end +end + +function value = bytes2scalar(bytes, type) + % reverse byte order to convert from little-endian to big-endian + value = typecast(bytes(end:-1:1), type); +end + +function [str, idx] = parsestring(len, bytes, idx) + str = native2unicode(bytes(idx:idx+len-1)', 'utf-8'); + idx = idx + len; +end + +function [out, idx] = parsebytes(len, bytes, idx) + out = bytes(idx:idx+len-1); + idx = idx + len; +end + +function [out, idx] = parseext(len, bytes, idx) + out.type = bytes(idx); + out.data = bytes(idx+1:idx+len); + idx = idx + len + 1; +end + +function [out, idx] = parsearray(len, bytes, idx) + out = cell(1, len); + for n=1:len + [out{n}, idx] = parse(bytes, idx); + end +end + +function [out, idx] = parsemap(len, bytes, idx) + out = dictionary(string([]),{}); + for n = 1 : len + [key, idx] = parse(bytes, idx); + [val, idx] = parse(bytes, idx); + out(key) = {val}; + end +end diff --git a/communication/rewardManager.m b/communication/rewardManager.m new file mode 100644 index 0000000000000000000000000000000000000000..f3f79df74005bae633dff36a2d8fb02f0e6beff0 --- /dev/null +++ b/communication/rewardManager.m @@ -0,0 +1,17 @@ +% ======================================================================== +%> @brief +%> +%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md +% ======================================================================== +classdef rewardManager < handle + + properties + + end + + methods + function obj = rewardManager() + + end + end +end \ No newline at end of file diff --git a/communication/tittaCalCallback.m b/communication/tittaCalCallback.m deleted file mode 100644 index 3e04b705e111c7df6231b985dc78b36af4229139..0000000000000000000000000000000000000000 --- a/communication/tittaCalCallback.m +++ /dev/null @@ -1,23 +0,0 @@ -function tittaCalCallback(titta_instance,currentPoint,posNorm,posPix,stage,calState) -global rM %our reward manager object -if strcmpi(stage,'cal') - % this demo function is no-op for validation mode - if calState.status==0 - status = 'ok'; - if isa(rM,'arduinoManager') && rM.isOpen - timedTTL(rM,2,300); - fprintf('---!!!Calibration reward!\n'); - end - else - status = sprintf('failed (%s)',calState.statusString); - fprintf('---!!!NO Calibration reward!\n'); - end - titta_instance.sendMessage(sprintf('Calibration data collection status result for point %d, positioned at (%.2f,%.2f): %s',currentPoint,posNorm,status)); -elseif strcmpi(stage,'val') - if isa(rM,'arduinoManager') && rM.isOpen - timedTTL(rM,2,300); - fprintf('---!!!Validation reward!\n'); - else - fprintf('---!!!NO Validation reward!\n'); - end -end \ No newline at end of file diff --git a/communication/tittaCalMovieStimulus.m b/communication/tittaCalMovieStimulus.m deleted file mode 100644 index b51d4b598a5f2d994e193d01182bdb4193b02f80..0000000000000000000000000000000000000000 --- a/communication/tittaCalMovieStimulus.m +++ /dev/null @@ -1,184 +0,0 @@ -% ======================================================================== -%> @brief plays an animated movie for a calibration stimulus -%> -%> -%> -%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md -% ======================================================================== -classdef tittaCalMovieStimulus < handle - properties (Access=private, Constant) - calStateEnum = struct('undefined',0, 'moving',1, 'shrinking',2 ,'waiting',3 ,'blinking',4); - end - properties (Access=private) - calState - currentPoint - lastPoint - moveStartT - shrinkStartT - oscillStartT - blinkStartT - moveDuration - moveVec - accel - scrSize - startWaiting = false; - end - properties - doShrink = false - shrinkTime = 0.5 - doMove = true - moveTime = 1 % for whole screen distance, duration will be proportionally shorter when dot moves less than whole screen distance - moveWithAcceleration= true - doOscillate = false - oscillatePeriod = 1.5 - blinkInterval = 0.2 - blinkCount = 4 - fixBackSizeBlink = 35 - fixBackSizeMax = 50 - fixBackSizeMaxOsc = 35 - fixBackSizeMin = 15 - fixFrontSize = 5 - movie = [] - sM = [] - oldpos = [] - verbose = true - end - - methods - function obj = tittaCalMovieStimulus() - obj.setCleanState(); - end - - function setCleanState(obj) - obj.oldpos = []; - obj.calState = obj.calStateEnum.undefined; - obj.currentPoint= nan(1,3); - obj.lastPoint= nan(1,3); - if ~isempty(obj.movie) && isa(obj.movie,'movieStimulus') - obj.movie.reset(); - if ~isempty(obj.sM) - if ~obj.movie.isSetup; obj.movie.setup(obj.sM); end - if obj.verbose;fprintf('!!!>>>SET-CLEAN-STATE SETUP MOVIE\n');end - end - end - if obj.verbose;fprintf('!!!>>>SET-CLEAN-STATE DONE\n');end - end - - function initialise(obj,m) - obj.oldpos = []; - obj.movie = m; - obj.sM = m.sM; - if ~isempty(obj.sM) && isa(obj.movie,'movieStimulus') - if ~obj.movie.isSetup; obj.movie.setup(obj.sM); end - end - if ~isempty(obj.sM) && isa(obj.sM.audio,'audioManager') && ~obj.sM.audio.isSetup - obj.sM.audio.setup(); - end - obj.scrSize = obj.sM.winRect(3:4); - if obj.verbose;fprintf('!!!>>>CALMOVIESTIM SET INITIAL STATE\n');end - end - - function qAllowAcceptKey = doDraw(obj,wpnt,drawCmd,currentPoint,pos,~,~) - % last two inputs, tick (monotonously increasing integer and stage - % ("cal" or "val") are not used in this code - - % if called with drawCmd == 'cleanUp', this is a signal that - % calibration/validation is done, and cleanup can occur if - % wanted - if strcmp(drawCmd,'cleanUp') - if obj.verbose;fprintf('!!!>>>RUN CLEANUP\n');end - obj.setCleanState(); - return; - end - - % check point changed - curT = GetSecs; % instead of using time directly, you could use the 'tick' call sequence number input to this function to animate your display - if strcmp(drawCmd,'new') - if obj.doMove && ~isnan(obj.currentPoint(1)) - obj.calState = obj.calStateEnum.moving; - obj.moveStartT = curT; - % dot should move at constant speed regardless of - % distance to cover, moveTime contains time to move - % over width of whole screen. Adjust time to proportion - % of screen width covered by current move - dist = hypot(obj.currentPoint(2)-pos(1),obj.currentPoint(3)-pos(2)); - obj.moveDuration = obj.moveTime*dist/obj.scrSize(1); - if obj.moveWithAcceleration - obj.accel = dist/(obj.moveDuration/2)^2; % solve x=.5*a*t^2 for a, use dist/2 for x - obj.moveVec = (pos(1:2)-obj.currentPoint(2:3))/dist; - end - elseif obj.doShrink - obj.calState = obj.calStateEnum.shrinking; - obj.shrinkStartT = curT; - else - obj.sM.audio.play() - obj.calState = obj.calStateEnum.waiting; - obj.oscillStartT = curT; - end - obj.lastPoint = obj.currentPoint; - obj.currentPoint = [currentPoint pos]; - elseif strcmp(drawCmd,'redo') - % start blink, pause animation. - obj.calState = obj.calStateEnum.blinking; - obj.blinkStartT = curT; - else % drawCmd == 'draw' - % regular draw: check state transition - if (obj.calState==obj.calStateEnum.moving && (curT-obj.moveStartT)>obj.moveDuration) || ... - (obj.calState==obj.calStateEnum.blinking && (curT-obj.blinkStartT)>obj.blinkInterval*obj.blinkCount*2) - % move finished or blink finished - if obj.doShrink - obj.a.play(); - obj.calState = obj.calStateEnum.shrinking; - obj.shrinkStartT = curT; - else - obj.calState = obj.calStateEnum.waiting; - obj.oscillStartT = curT; - obj.sM.audio.play(); - end - elseif obj.calState==obj.calStateEnum.shrinking && (curT-obj.shrinkStartT)>obj.shrinkTime - obj.calState = obj.calStateEnum.waiting; - obj.oscillStartT = curT; - end - end - - % determine current point position - if obj.calState==obj.calStateEnum.moving - frac = (curT-obj.moveStartT)/obj.moveDuration; - if obj.moveWithAcceleration - if frac<.5 - curPos = obj.lastPoint(2:3) + obj.moveVec*.5*obj.accel*(curT-obj.moveStartT)^2; - else - % implement deceleration by accelerating from the - % other side in backward time - curPos = obj.currentPoint(2:3) - obj.moveVec*.5*obj.accel*(obj.moveDuration-curT+obj.moveStartT)^2; - end - else - curPos = obj.lastPoint(2:3).*(1-frac) + obj.currentPoint(2:3).*frac; - end - else - curPos = obj.currentPoint(2:3); - end - - % determine if we're ready to accept the user pressing the - % accept calibration point button. User should not be able to - % press it if point is not yet at the final position - qAllowAcceptKey = ismember(obj.calState,[obj.calStateEnum.shrinking obj.calStateEnum.waiting]); - - % draw - Screen('FillRect',wpnt,obj.sM.backgroundColour); % needed when multi-flipping participant and operator screen, doesn't hurt when not needed - if obj.calState~=obj.calStateEnum.blinking || mod((curT-obj.blinkStartT)/obj.blinkInterval/2,1)>.5 - obj.drawMovie(curPos); - end - end - end - - methods (Access = private, Hidden) - function drawMovie(obj,pos) - if isempty(obj.oldpos) || ~all(pos==obj.oldpos) - obj.oldpos = pos; - obj.movie.updatePositions(pos(1),pos(2)); - end - obj.movie.draw(); - end - end -end \ No newline at end of file diff --git a/communication/touchManager.m b/communication/touchManager.m index 993242f83b2e6dc636b6bbc35d688bf884cd02ba..be4ad8b8b3c282fdb667706ea60e552a7073dea9 100644 --- a/communication/touchManager.m +++ b/communication/touchManager.m @@ -1,43 +1,126 @@ +% ======================================================================== classdef touchManager < optickaCore - %UNTITLED Summary of this class goes here - % Detailed explanation goes here +%> @class touchManager @brief Manages touch screens (wraps the PTB +%> TouchQueue* functions), and provides touch area management methods. +%> +%> TOUCHMANAGER -- call this and setup with screen manager, then run your +%> task. This class can handles touch windows (circular or rectangular), +%> exclusion zones and more for multiple touch screens. NOTE: touch +%> interfaces do not keep state, so if you touch but do not move your finger +%> then there are no events, so touchManager ensures that the state is +%> handled for you. The touch queue is also asynchronous to the main PTB +%> task and so must be handled specifically. The default here is to drop all +%> but the latest event (drainEvent = true), when false all events are +%> processed which may take more time. Touchscreens can also cause unwanted +%> events, especially before/after the task runs (a subject can press +%> buttons, enter text etc.), so on Linux we also enable / disable the touch +%> screen when deviceName is passed to try to mitigate this problem. +%> +%> Copyright ©2014-2025 Ian Max Andolina — released: LGPL3, see LICENCE.md +% ======================================================================== %--------------------PUBLIC PROPERTIES----------% properties %> which touch device to connect to? - device = 1 + device double = 1 + %> touch device name, useful to enable it at the OS level before + %> PTB searches for the touch device + deviceName string = "" %> use the mouse instead of the touch screen for debugging - isDummy = false - %> accept window (circular when radius is 1 value, rectangular when radius = [width height]) - %> doNegation allows to return -100 (like exclusion) if touch is outside window. - window = struct('X', 0, 'Y', 0, 'radius', 2, 'doNegation', false); - %> Use exclusion zones where no eye movement allowed: [left,top,right,bottom] - %> Add rows to generate multiple exclusion zones. + isDummy logical = false + %> window is a touch window, X and Y are the screen postion + %> radius: circular when radius is 1 value, rectangular when radius = [width height]) + %> init: timer that tests time to first touch + %> hold: timer that determines how long to hold + %> release: timer to determine the time after hold in which window should be released + %> doNegation: allows to return -100 (like exclusion) if touch is OUTSIDE window. + %> when using the testHold etc functions. + %> negationBuffer: is an area around the window to allow some margin of error... + window struct = struct('X', 0, 'Y', 0, 'radius', 2, ... + 'init', 3, 'hold', 0.05, 'release', 1, ... + 'doNegation', false, 'negationBuffer', 2, 'strict', true); + %> Use exclusion zones where no touch allowed: + %> [left,top,right,bottom] Add rows to generate multiple exclusion + %> zones. These are checked before the touch windows are. exclusionZone = [] - %> number of slots for touch events - nSlots = 1e5 - %> size in degrees around the window for negation to trigger - negationBuffer = 2 - %> verbosity + %> drain the events to only get the latest? This ensures lots of + %> events don't pile up, often you only want the current event, but + %> potentially causes a longer delay each time getEvent is called. + %> In theory you may miss specific events like NEW touch so ensure + %> this works with your paradigm. + drainEvents logical = true + %> there can be up to 10 touch events, do we check if the touch ID matches? + trackID logical = true + %> which id to track as the main one (1 = first event). + %> Note this is different from the event keycode, which increments + %> with each event, this is the order of the touch event + mainID double = 1 + %> panel type, 1 = "front", 2 = "back" (reverses X position) + panelType double = 1 + %> verbose, log more info to command window + %> useful for debugging. verbose = false end properties (SetAccess=private, GetAccess=public) + % general touch info, latest x and y position + x = [] + y = [] + %> Accumulated X position in degrees since last flush + xAll = [] + %> Accumulated Y position in degrees since last flush + yAll = [] + %> times the xAll and yAll coordinates were recorded, see queueTime + %> which is the time of the last flush or a sync event to get relative times + tAll = [] + %> time of last flush or syncTime method call, useful for getting relative times + queueTime = 0 + % touch event info from getEvent() + event = [] + eventID = [] + eventType = [] + eventNew = false + eventMove = false + eventPressed = false + eventRelease = false + % window info from checkTouchWindows() + win = [] + wasInWindow = false + % hold info from isHold() + hold = [] + wasHeld = false + wasNegation = false + isSearching = false + isReleased = false + isOpen = false + isQueue = false + % others devices = [] names = [] allInfo = [] - x = -1 - y = -1 - isOpen = false - isQueue = false - end + end + + properties (Hidden = true) + %> number of slots for touch events + nSlots double = 1e5 + %> functions return immediately, useful for mocking + silentMode logical = false + end properties (Access = private) + deferLog = false + lastPressed = false + currentID = [] + pressed = false ppd = 36 screen = [] - win = [] + swin = [] screenVals = [] - allowedProperties = {'isDummy','device','verbose','window','nSlots','negationBuffer'} + allowedProperties = {'isDummy','device','deviceName','verbose','window','nSlots',... + 'panelType','drainEvents','exclusionZone'} + holdTemplate = struct('N',0,'inWindow',false,'touched',false,... + 'start',0,'now',0,'total',0,'search',0,'init',0,'releaseinit',0,... + 'length',0,'release',0) end %======================================================================= @@ -58,262 +141,738 @@ classdef touchManager < optickaCore args = optickaCore.addDefaults(varargin,struct('name','touchManager')); me = me@optickaCore(args); %superclass constructor me.parseArgs(args, me.allowedProperties); - try [me.devices,me.names,me.allInfo] = GetTouchDeviceIndices([], 1); end %#ok<*TRYNC> + + % try to ensure the named touch device is enabled using xinput in Linux-only + touchManager.enableTouchDevice(me.deviceName, "enable"); + + % PTB: find touch interfaces + try [me.devices,me.names,me.allInfo] = GetTouchDeviceIndices([], 1); end %#ok<*TRYNC> + me.hold = me.holdTemplate; end % ===================================================================SETUP - function setup(me, sM) - %> @fn setup + function setup(me, sM) + %> @fn setup(me, sM) %> - %> @param - %> @return + %> @param sM screenManager to use + %> @return % =================================================================== me.isOpen = false; me.isQueue = false; if isa(sM,'screenManager') && sM.isOpen me.screen = sM; - me.win = sM.win; + me.swin = sM.win; me.ppd = sM.ppd; me.screenVals = sM.screenVals; else - error('Need to pass an open screenManager object!'); + error('≣≣≣≣⊱touchManager:Need to pass an open screenManager object!'); end + try touchManager.enableTouchDevice(me.deviceName, "enable"); end try [me.devices,me.names,me.allInfo] = GetTouchDeviceIndices([], 1); end if me.isDummy me.comment = 'Dummy Mode Active'; - fprintf('--->touchManager: %s\n',me.comment); + fprintf('≣≣≣≣⊱touchManager: %s\n',me.comment); elseif isempty(me.devices) me.comment = 'No Touch Screen are available, please check USB!'; - fprintf('--->touchManager: %s\n',me.comment); - elseif length(me.devices)==1 - me.comment = 'found ONE Touch Screen...'; - fprintf('--->touchManager: %s\n',me.comment); + warning('≣≣≣≣⊱touchManager: %s\n',me.comment); + elseif isscalar(me.devices) + me.comment = sprintf('found ONE Touch Screen: %s',me.names{1}); + fprintf('≣≣≣≣⊱touchManager: %s\n',me.comment); elseif length(me.devices)==2 - me.comment = 'found TWO Touch Screens plugged...'; - fprintf('--->touchManager: %s\n',me.comment); + me.comment = sprintf('found TWO Touch Screens plugged %s %s',me.names{1},me.names{2}); + fprintf('≣≣≣≣⊱touchManager: %s\n',me.comment); end end % =================================================================== - function createQueue(me, choice) - %> @fn setup + function createQueue(me) + %> @fn createQueue(me) %> - %> @param - %> @return + %> @param choice which touch device to use, default uses me.device + %> @return % =================================================================== if me.isDummy; me.isQueue = true; return; end - if ~exist('choice','var') || isempty(choice); choice = me.device; end - for i = 1:length(choice) - try - TouchQueueCreate(me.win, me.devices(choice(i)), me.nSlots); - catch - warning('touchManager: Cannot create touch queue!'); - end + if isempty(me.devices) || isempty(me.device) || me.device <= 0 + error('≣≣≣≣⊱touchManager: no available devices!!!') end + try + TouchQueueCreate(me.swin, me.devices(me.device), me.nSlots); + catch + warning('≣≣≣≣⊱touchManager: Cannot create touch queue!'); + end + syncTime(me); me.isQueue = true; - if me.verbose;me.salutation('createQueue','Opened');end + if me.verbose; logOutput(me,'createQueue','Created...'); end end % =================================================================== - function start(me, choice) - %> @fn setup + function start(me) + %> @fn start(me) %> - %> @param - %> @return + %> @return % =================================================================== if me.isDummy; me.isOpen = true; return; end - if ~exist('choice','var') || isempty(choice); choice = me.device; end - if ~me.isQueue; createQueue(me,choice); end - for i = 1:length(choice) - TouchQueueStart(me.devices(choice(i))); - end + if ~me.isQueue; createQueue(me); end + if isempty(me.devices(me.device)); error("≣≣≣≣⊱touchManager: no device available!!!"); end + TouchQueueStart(me.devices(me.device)); me.isOpen = true; - if me.verbose;salutation(me,'start','Started queue...');end + if me.verbose; logOutput(me,'start','Started queue...'); end end - + % =================================================================== - function stop(me, choice) - %> @fn stop + function stop(me) + %> @fn stop(me) %> - %> @param - %> @return + %> @return % =================================================================== - if me.isDummy; me.isOpen = false; return; end - if ~exist('choice','var') || isempty(choice); choice = me.device; end - for i = 1:length(choice) - TouchQueueStop(me.devices(choice(i))); - end - me.isOpen = false; - salutation(me,'stop','Stopped queue...'); + if me.isDummy; me.isOpen = false; me.isQueue = false; return; end + TouchQueueStop(me.devices(me.device)); + me.isOpen = false; me.isQueue = false; + if me.verbose; logOutput(me,'stop','Stopped queue...'); end end % =================================================================== function close(me, choice) - %> @fn close + %> @fn close(me, choice) %> - %> @param - %> @return + %> @param choice which touch device to use, default uses me.device + %> @return % =================================================================== + flush(me); me.isOpen = false; me.isQueue = false; if me.isDummy; return; end if ~exist('choice','var') || isempty(choice); choice = me.device; end for i = 1:length(choice) - TouchQueueRelease(me.devices(choice(i))); + TouchQueueRelease(me.devices(me.device)); end - salutation(me,'close','Closing...'); + if me.verbose; logOutput(me,'close','Closed...'); end end % =================================================================== - function flush(me, choice) - %> @fn flush + function n = flush(me) + %> @fn flush(me) %> %> @param - %> @return + %> @return n number of flushed events % =================================================================== + reset(me); + n = 0; + syncTime(me); if me.isDummy; return; end - if ~exist('choice','var') || isempty(choice); choice = me.device; end - for i = 1:length(choice) - TouchEventFlush(me.devices(choice(i))); - end + n = TouchEventFlush(me.devices(me.device)); + if me.verbose; fprintf('≣Flush⊱ Touch queue flushed %i events...\n',n); end end % =================================================================== - function navail = eventAvail(me, choice) - %> @fn eventAvail + function syncTime(me, timestamp) + %> @fn syncTime(me) Set the time of the touch queue to the current + %> time. We can use this function to set the 0 time, for example + %> stimulus onset etc. which can be used for evt.Time and tAll data. + %> + %> @param timestamp: [optional] time to set the queue time to, default is GetSecs + % =================================================================== + if ~exist('timestamp','var');timestamp = GetSecs; end + me.queueTime = timestamp; + end + + % =================================================================== + function navail = eventAvail(me) + %> @fn eventAvail(me) %> %> @param - %> @return + %> @return navail number of available events % =================================================================== - navail = []; + navail = 0; if me.isDummy [~, ~, b] = GetMouse; - if any(b); navail = true; end - return + if any(b) || me.lastPressed; navail = 1; end + else + navail = TouchEventAvail(me.devices(me.device)); end - if ~exist('choice','var') || isempty(choice); choice=me.device; end - for i = 1:length(choice) - navail(i)=TouchEventAvail(me.devices(choice(i))); %#ok<*AGROW> + end + + % =================================================================== + function updateWindow(me,X,Y,radius,doNegation,negationBuffer,strict,init,hold,release) + %> @fn updateWindow update the touch ewindow parameters + %> + %> @param + %> @return + % =================================================================== + arguments(Input) + me + X = [] + Y = [] + radius = [] + doNegation = [] + negationBuffer = [] + strict = [] + init = [] + hold = [] + release = [] end + if ~isempty(X); me.window.X = X; end + if ~isempty(Y); me.window.Y = Y; end + if ~isempty(radius); me.window.radius = radius; end + if ~isempty(doNegation); me.window.doNegation = doNegation; end + if ~isempty(negationBuffer); me.window.negationBuffer = negationBuffer; end + if ~isempty(strict); me.window.strict = strict; end + if ~isempty(init); me.window.init = init; end + if ~isempty(hold); me.window.hold = hold; end + if ~isempty(release); me.window.release = release; end + if me.verbose + fprintf('≣updateWindow⊱ X:%s Y:%s R:%s Neg:%i Buf:%.1f Strict:%i Init:%.1f Hold:%.1f Rel: %.1f\n',... + num2str(me.window().X,'%.1f'), num2str(me.window().X,'%.1f'),... + num2str(me.window.radius,'%.1f'),me.window.doNegation,... + me.window.negationBuffer,me.window.strict,... + me.window.init,me.window.hold,me.window.release); + end + end + + % =================================================================== + function reset(me) + %> @fn reset + %> + %> @param + %> @return + % =================================================================== + me.lastPressed = false; + me.hold = me.holdTemplate; + me.x = []; + me.y = []; + me.xAll = []; + me.yAll = []; + me.tAll = []; + me.win = []; + me.wasInWindow = false; + me.wasHeld = false; + me.isReleased = false; + me.wasNegation = false; + me.isSearching = false; + me.eventNew = false; + me.eventMove = false; + me.eventPressed = false; + me.eventRelease = false; + me.eventID = []; + me.eventType = []; + me.event = []; + me.currentID = []; + me.queueTime = GetSecs; end % =================================================================== - function event = getEvent(me, choice) + function evt = getEvent(me) %> @fn getEvent %> %> @param - %> @return + %> @return event structure % =================================================================== - event = {}; - if me.isDummy - [mx, my, b] = GetMouse(me.win); - if any(b) - event{1} = struct('Type',2,'Time',GetSecs,... + evt = []; + if me.isDummy % use mouse to simulate touch + [mx, my, b] = GetMouse(me.swin); + if any(b) && ~me.lastPressed + type = 2; motion = false; press = true; + elseif any(b) && me.lastPressed + type = 3; motion = true; press = true; + elseif ~any(b) && me.lastPressed + type = 4; motion = false; press = false; + else + type = -1; motion = false; press = 0; + end + if type > 0 + evt = struct('Type',type,'Time',GetSecs,... 'X',mx,'Y',my,'ButtonStates',b,... 'NormX',mx/me.screenVals.width,'NormY',my/me.screenVals.height, ... - 'MappedX',mx,'MappedY',my); + 'MappedX',mx,'MappedY',my,... + 'Pressed',press,'Motion',motion,... + 'Keycode',55); + end + else + evt = getEvents(me); + end + if ~isempty(evt) + me.eventNew = false; me.eventMove = false; me.eventRelease = false; me.eventPressed = false; + for ii = 1:length(evt) + if me.trackID && ~isempty(me.currentID) && me.currentID ~= evt(ii).Keycode + continue; + end + switch evt(ii).Type + case 2 %NEW + me.eventNew = true; + me.eventPressed = true; + me.lastPressed = true; + me.eventMove = false; + case 3 %MOVE + me.eventNew = false; + me.eventMove = true; + me.eventPressed = true; + case 4 %RELEASE + if me.lastPressed || me.isDummy + me.eventNew = false; + me.eventMove = false; + me.eventRelease = true; + me.lastPressed = false; + end + me.currentID = []; + case 5 %ERROR + warning('≣≣≣≣⊱touchManager: Event lost!'); + me.event = []; evt = []; + me.lastPressed = false; + me.currentID = []; + return + end + if evt(ii).Type == 2 || evt(ii).Type == 3 + evt(ii).xy = me.screen.toDegrees([evt(ii).MappedX evt(ii).MappedY],'xy'); + if ~isempty(evt(ii).xy) && length(evt(ii).xy)==2 + me.xAll = [me.xAll evt(ii).xy(1)]; + me.yAll = [me.yAll evt(ii).xy(2)]; + me.tAll = [me.tAll evt(ii).Time]; + end + end + end + evt = evt(end); + me.eventID = evt.Keycode; + me.eventType = evt.Type; + if me.panelType == 2; evt.changeX = true; evt.MappedX = me.screenVals.width - evt.MappedX; end + evt.xy = me.screen.toDegrees([evt.MappedX evt.MappedY],'xy'); + me.event = evt; + me.x = evt.xy(1); me.y = evt.xy(2); + end + end + + % =================================================================== + %> @fn isTouch + %> + %> Simply checks for touch event irrespective of position + %> + %> @param + %> @return + % =================================================================== + function touch = isTouch(me) + touch = false; + getEvent(me); + touch = logical(me.lastPressed); + end + + % =================================================================== + function [result, win, wasEvent, wasTouch] = checkTouchWindows(me, windows, getEvt) + %> @fn [result, win, wasEvent] = checkTouchWindows(me, windows) + %> + %> Simply get latest touch event and check if it is in the defined window + %> + %> @param windows: [optional] touch rects to test (default use window parameters) + %> @param getEvent: [optional] do we get event or use the existing one? + %> @return result: -100 = negation, true / false otherwise + % =================================================================== + if ~exist('windows','var'); windows = []; end + if ~exist('getEvt','var'); getEvt = true; end + + nWindows = max([1 size(windows,1)]); + result = false; win = 1; wasEvent = false; wasTouch = false; + + if getEvt + evt = getEvent(me); + else + evt = me.event; + end + + while iscell(evt) && ~isempty(evt); evt = evt{1}; end + + if isempty(evt); return; end + + wasEvent = true; + wasTouch = me.eventPressed; + + if ~isempty(evt.xy) + if isempty(windows) + [result, win] = calculateWindow(me, evt.xy(1), evt.xy(2)); + else + for i = 1 : nWindows + [result(i,1), win] = calculateWindow(me, evt.xy(1), evt.xy(2), windows(i,:)); + if result(i,1); win = i; result = true; break;end + end + end + me.event.result = result; + if any(result); me.wasInWindow = true; end + end + if me.verbose + winres = win; + if winres == 0 + winres = 1; + end + fprintf('≣checkWin%s⊱%i wasHeld:%i type:%i result:%i new:%i mv:%i prs:%i lastprs: %i rel:%i {%.1fX %.1fY}\n\twin:%i [%.1fX %.1fY]\n',... + me.name, me.eventID, me.wasHeld, evt.Type, result, me.eventNew, me.eventMove, me.eventPressed, me.lastPressed, me.eventRelease,... + me.x, me.y, win, me.window(winres).X, me.window(winres).Y); + end + end + + % =================================================================== + %> @fn isHold + %> + %> This is the main function which runs touch timers and calculates + %> the logic of whether the touch is in a region and for how long. + %> + %> @param + %> @return + % =================================================================== + function [held, heldtime, release, releasing, searching, failed, touch] = isHold(me) + held = false; heldtime = false; release = false; + releasing = false; searching = true; failed = false; touch = false; + + me.hold.now = GetSecs; + if me.hold.start == 0 + me.hold.start = me.hold.now; + me.hold.N = 0; + me.hold.inWindow = false; + me.hold.touched = false; + me.hold.total = 0; + me.hold.search = 0; + me.hold.init = 0; + me.hold.length = 0; + me.hold.releaseinit = 0; + me.hold.release = 0; + me.wasHeld = false; + else + me.hold.total = me.hold.now - me.hold.start; + if ~me.hold.touched + me.hold.search = me.hold.total; + end + end + me.deferLog = true; + [held, ~, wasEvent] = checkTouchWindows(me); + me.deferLog = false; + if ~wasEvent % no touch + if me.hold.inWindow % but previous event was touch inside window + me.hold.length = me.hold.now - me.hold.init; + if me.hold.length >= me.window.hold + me.wasHeld = true; + heldtime = true; + releasing = true; + end + me.hold.release = me.hold.now - me.hold.releaseinit; + if me.hold.release > me.window.release + releasing = false; + failed = true; + end + elseif ~me.hold.inWindow && me.hold.search > me.window.init + failed = true; + searching = false; end return; + else + touch = true; end - if ~exist('choice','var') || isempty(choice); choice=me.device; end - for i = 1:length(choice) - event{i} = TouchEventGet(me.devices(choice(i)), me.win); + + if held == -100 + me.wasNegation = true; + searching = false; + failed = true; + if me.verbose; fprintf('≣isHold⊱ touchManager -100 NEGATION!\n'); end + return + end + + st = ''; + + if me.eventPressed && held %A + st = 'A'; + me.hold.touched = true; + me.hold.inWindow = true; + searching = false; + if me.eventNew == true || me.hold.N == 0 + me.hold.init = me.hold.now; + me.hold.N = me.hold.N + 1; + me.hold.releaseinit = me.hold.init + me.window.hold; + me.hold.length = 0; + else + me.hold.length = me.hold.now - me.hold.init; + end + if me.hold.search <= me.window.init && me.hold.length >= me.window.hold + me.wasHeld = true; + heldtime = true; + releasing = true; + end + if me.wasHeld + me.hold.release = me.hold.now - me.hold.releaseinit; + if me.hold.release <= me.window.release + releasing = true; + else + releasing = false; + failed = true; + end + end + elseif me.eventPressed && ~held %B + st = 'B'; + me.hold.inWindow = false; + me.hold.touched = true; + if me.hold.N > 0 + failed = true; + searching = false; + else + searching = true; + end + elseif me.eventRelease && held %C + st = 'C'; + searching = false; + me.hold.length = me.hold.now - me.hold.init; + if me.hold.inWindow + if me.hold.length >= me.window.hold + me.wasHeld = true; + heldtime = true; + releasing = true; + else + me.wasHeld = false; + failed = true; + end + me.hold.release = me.hold.now - me.hold.releaseinit; + if me.hold.release > me.window.release + releasing = false; + failed = true; + else + release = true; + releasing = false; + end + else + st = ['!!' st]; + end + me.hold.inWindow = false; + elseif me.eventRelease && ~held %D + st = 'D'; + me.hold.inWindow = false; + failed = true; + searching = false; + end + me.isSearching = searching; + me.isReleased = release; + if me.verbose + fprintf('≣isHold⊱%s⊱%s:%i new:%i mv:%i prs:%i rel:%i {%.1fX %.1fY - %.1f %.1f} tt:%.2f st:%.2f ht:%.2f rt:%.2f inWin:%i tchd:%i h:%i ht:%i r:%i rl:%i s:%i fail:%i N:%i\n',... + me.name,st,me.eventID,me.eventNew,me.eventMove,me.eventPressed,me.eventRelease,... + me.x,me.y,me.window.X,me.window.Y,... + me.hold.total,me.hold.search,me.hold.length,me.hold.release,... + me.hold.inWindow,me.hold.touched,... + held,heldtime,release,releasing,searching,failed,me.hold.N); end end - + + % =================================================================== - function [result, x, y] = checkTouchWindow(me, window) - %> @fn checkTouchWindow + function [out, held, heldtime, release, releasing, searching, failed, touch] = testHold(me, yesString, noString) + %> @fn testHold %> %> @param - %> @return - % =================================================================== - if ~exist('window','var'); window = []; end - result = false; x = []; y = []; - event = getEvent(me); - while ~isempty(event) && iscell(event); event = event{1}; end - if isempty(event) || ~isfield(event,'MappedX'); return; end - xy = me.screen.toDegrees([event.MappedX event.MappedY]); - result = calculateWindow(me, xy(1), xy(2), window); - x = xy(1); y = xy(2); - if me.verbose;fprintf('IN: %i Touch: x = %i (%.2f) y = %i (%.2f)\n',result, event.X, xy(1), event.Y, xy(2));end + %> @return + % =================================================================== + [held, heldtime, release, releasing, searching, failed, touch] = isHold(me); + out = ''; + if me.wasNegation || failed || (~held && ~searching) + out = noString; + elseif heldtime + out = yesString; + end + if me.verbose && ~isempty(out) + fprintf('≣testHold⊱ %s held:%i heldtime:%i rel:%i reling:%i ser:%i fail:%i touch:%i x:%.1f [%.1f] y:%.1f [%.1f]\n',... + out, held, heldtime, release, releasing, searching, failed, touch,... + me.x, me.y, me.window.X, me.window.Y) + end end % =================================================================== - function [result, x, y] = checkTouchWindows(me, windows) - %> @fn checkTouchWindow + function [out, held, heldtime, release, releasing, searching, failed, touch] = testHoldRelease(me, yesString, noString) + %> @fn testHoldRelease %> %> @param - %> @return - % =================================================================== - if ~exist('windows','var') || isempty(windows); return; end - nWindows = size(windows,1); - result = logical(zeros(nWindows,1)); x = zeros(nWindows,1); y = zeros(nWindows,1); - event = getEvent(me); - while ~isempty(event) && iscell(event); event = event{1}; end - if isempty(event) || ~isfield(event,'MappedX'); return; end - xy = me.screen.toDegrees([event.MappedX event.MappedY]); - for i = 1 : nWindows - result(i,1) = calculateWindow(me, xy(1), xy(2), windows(i,:)); - x(i,1) = xy(1); y(i,1) = xy(2); - if result(i,1)==true; break; end + %> @return + % =================================================================== + [held, heldtime, release, releasing, searching, failed, touch] = isHold(me); + out = ''; + if me.wasNegation || failed || (~held && ~searching) + out = noString; + elseif me.wasHeld && release + out = yesString; + end + if me.verbose && ~isempty(out) + fprintf('≣testHoldRelease⊱ %s held:%i time:%.3f rel:%i rel:%i ser:%i fail:%i touch:%i\n', out, held, heldtime, release, releasing, searching, failed, touch) end end % =================================================================== - function demo(me) + function demo(me, nTrials, useaudio) %> @fn demo %> %> @param - %> @return + %> @return % =================================================================== + arguments(Input) + me + nTrials double = 10 + useaudio logical = false + end if isempty(me.screen); me.screen = screenManager(); end + sM = me.screen; + if max(Screen('Screens'))==0 && me.verbose + PsychDebugWindowConfiguration; + end oldWin = me.window; oldVerbose = me.verbose; me.verbose = true; - sM = me.screen; - if ~sM.isOpen; open(sM); end - setup(me, sM); % !!! Run setup first - im = imageStimulus('size', 5); - setup(im, sM); - - createQueue(me); % !!! Create Queue - start(me); % !!! Start touch collection - try - for i = 1 : 5 + + if useaudio;a=audioManager();open(a);beep(a,3000,0.1,0.1);WaitSecs(0.2);beep(a,250,0.3,0.8);end + + try + if ~sM.isOpen; open(sM); end + WaitSecs(0.5); + setup(me, sM); %===================!!! Run setup first + im = discStimulus('size', 6); + setup(im, sM); + + quitKey = KbName('escape'); + doQuit = false; + createQueue(me); %===================!!! Create Queue + start(me); %===================!!! Start touch collection + + for i = 1 : nTrials + if doQuit; break; end tx = randi(20)-10; ty = randi(20)-10; im.xPositionOut = tx; im.yPositionOut = ty; + me.window.X = tx; + me.window.Y = ty; + me.window.radius = im.size/2; + me.window.release = 2; update(im); - rect = toDegrees(sM, im.mvRect, 'rect'); - - flush(me); %!!! flush the queue - txt = ''; - ts = GetSecs; - while GetSecs <= ts + 10 - x = []; y = []; + if useaudio;beep(a,1000,0.1,0.1);end + fprintf('\n\nTouchManager Demo TRIAL %i -- X = %i Y = %i R = %.2f\n',i,me.window.X,me.window.Y,me.window.radius); + t = sprintf('Negation buffer: %.2f | Init: %.2f s | Hold > time: %.2f s | Release < time %.2f s',... + me.window.negationBuffer, me.window.init, me.window.hold, me.window.release); + reset(me); + flush(me); %===================!!! flush the queue + vbl = flip(sM); ts = vbl; + result = 'timeout'; + while vbl <= ts + 20 + [r, hld, hldt, rel, reli, se, fl, tch] = testHoldRelease(me,'yes','no'); + if hld + txt = sprintf('%s IN x = %.1f y = %.1f - h:%i ht:%i r:%i rl:%i s:%i f:%i touch:%i N:%i\n%s',... + r,me.x,me.y,hld,hldt,rel,reli,se,fl,tch,me.hold.N,t); + elseif ~isempty(me.x) + txt = sprintf('%s OUT x = %.1f y = %.1f - h:%i ht:%i r:%i rl:%i s:%i f:%i touch:%i N:%i\n%s',... + r,me.x,me.y,hld,hldt,rel,reli,se,fl,tch,me.hold.N,t); + else + txt = sprintf('%s NO touch - h:%i ht:%i r:%i rl:%i s:%i f:%i touch:%i N:%i\n%s',... + r,hld,hldt,rel,reli,se,fl,tch,me.hold.N,t); + end + if ~me.wasHeld; draw(im); end drawText(sM,txt); drawGrid(sM); - draw(im); - flip(sM); - [r,x,y] = checkTouchWindow(me, rect); %!!! check touch window - if r - txt = sprintf('IN window x = %.2f y = %.2f',x,y); - elseif ~isempty(x) - txt = sprintf('OUT window x = %.2f y = %.2f',x,y); + vbl = flip(sM); + if strcmp(r,'yes') + if useaudio;beep(a,3000,0.1,0.1);end + result = sprintf('CORRECT!!!'); break; + elseif strcmp(r,'no') + if useaudio;beep(a,250,0.3,0.8);end + result = 'INCORRECT!!!'; break; + end + [keyDown,~,keys] = optickaCore.getKeys([]); + if keyDown && any(keys(quitKey)); doQuit = true; break; end + end + drawTextNow(sM, result); + tend = vbl - ts; + fprintf('≣≣≣≣⊱ TouchManager Demo RESULT: %s in %.2f \n',result,tend); + disp(me.hold); + WaitSecs(2); + end + stop(me); close(me); %===================!!! stop and close + me.window = oldWin; + me.verbose = oldVerbose; + if useaudio; try reset(a); end; end + try reset(im); end + try close(sM); end + clear Screen + catch ME + getReport(ME); + try reset(im); end + try close(sM); end + try close(me); end + if useaudio; try reset(a); end; end + try me.window = oldWin; end + try me.verbose = oldVerbose; end + clear Screen + rethrow(ME); + end + end + + % =================================================================== + function testEvents(me) + %> @fn test, test the touch event values + %> + %> @param + %> @return + % =================================================================== + if isempty(me.screen); me.screen = screenManager(); end + sM = me.screen; + sM.screen = 0; + sM.disableSyncTests = true; + sM.font.TextSize = 22; + if max(Screen('Screens'))==0 && me.verbose + PsychDebugWindowConfiguration; + end + + oldWin = me.window; + oldVerbose = me.verbose; + me.verbose = true; + + if ~sM.isOpen; sv = open(sM); end + setup(me, sM); %===================!!! Run setup first + + quitKey = KbName('escape'); + doQuit = false; + createQueue(me); %===================!!! Create Queue + start(me); %===================!!! Start touch collection + try + while ~doQuit + reset(me); + flush(me); %===================!!! flush the queue + me.updateWindow(0,0,3,true,2,true,5,1,1); + vbl = flip(sM); ts = vbl; + while vbl <= ts + 30 + [held, heldtime, release, releasing, searching, failed, touch] = isHold(me); + txt = sprintf('X: %.1f Y: %.1f - h:%i ht:%i r:%i rl:%i s:%i f:%i touch:%i N:%i - evtX:%.1f evtYvbn ',... + me.x,me.y,held,heldtime,release,releasing,searching,failed,touch,me.hold.N); + if ~isempty(me.event) && isstruct(me.event) + txt = sprintf('%s\n ID: %i type:%i evtX:%.1f evtY:%.1f nrmX: %.2f nrmY: %.2f mapX: %.1f mapY: %.1f press:%i motion:%i',... + txt, me.event.Keycode, me.event.Type, me.event.X, me.event.Y, me.event.NormX, me.event.NormY,... + me.event.MappedX,me.event.MappedY,me.event.Pressed,me.event.Motion); + txt = sprintf('%s\n new: %i pressed: %i lastpressed: %i move: %i release: %i',... + txt, me.eventNew, me.eventPressed, me.lastPressed, me.eventMove, me.eventRelease); end - flush(me); + drawGreenSpot(sM, 6); drawTextWrapped(sM,txt); drawScreenCenter(sM); + drawGrid(sM); + vbl = flip(sM); + [keyDown,~,keys] = optickaCore.getKeys([]); + if keyDown && any(keys(quitKey)); doQuit = true; break; end end - flip(sM); WaitSecs(1); + disp(me.hold); + WaitSecs(0.1); end - stop(me); close(me); + stop(me); close(me); %===================!!! stop and close me.window = oldWin; me.verbose = oldVerbose; try reset(im); end try close(sM); end + clear Screen + if ~isempty(me.xAll) + figure; + plot(me.xAll, me.yAll); + xlim([sv.leftInDegrees sv.rightInDegrees]); + ylim([sv.topInDegrees sv.bottomInDegrees]); + set(gca,'YDir','reverse'); + xlabel('X Position (deg)'); + xlabel('Y Position (deg)'); + end catch ME + getReport(ME); try reset(im); end - try close(s); end + try close(sM); end try close(me); end + clear Screen + rethrow(ME); end end @@ -322,6 +881,56 @@ classdef touchManager < optickaCore %======================================================================= methods (Static = true) %------------------STATIC METHODS %======================================================================= + + function enableTouchDevice(deviceName, enable) + %> On linux we can use xinput to list and enable/disable touch + %> interfaces, here we try to make the named touch interface + %> enabled or disabled. + arguments(Input) + deviceName string = "" + enable string = "enable" + end + + if ~IsLinux || matches(deviceName,""); return; end + + enable = lower(enable); + + if matches(enable,["on","yes","true","enable"]) + cmd = "enable"; + else + cmd = "disable"; + end + + ret = 0; msg = ''; attempt = false; + + try + [~,r] = system("xinput list"); + disp('===>>> XInput Initial Device List:'); + disp(r); + if isempty(deviceName); return; end + pattern = sprintf('(?%s)\\s+id=(?\\d+)', deviceName); + r = strsplit(string(r), newline); + for ii = 1:length(r) + tokens = regexp(r(ii), pattern, 'names'); + if ~isempty(tokens) + attempt = true; + [ret,msg] = system("xinput " + cmd + " " + tokens.id); + fprintf('===>>> XInput: Run on %s\n', cmd, tokens.id, deviceName); + WaitSecs('YieldSecs',1); + [~,r] = system("xinput list"); + disp('===>>>XInput Final Device List:'); + disp(r); + fprintf('\n\n'); + break + end + end + if attempt == true && ret == 1 + warning('touchManager.enableTouchDevice failed: %s',msg); + elseif attempt == false + warning('touchManager.enableTouchDevice device not found: %s',deviceName); + end + end + end end @@ -329,12 +938,28 @@ classdef touchManager < optickaCore methods (Access = protected) %------------------PROTECTED METHODS %======================================================================= + % =================================================================== + function [evt, n] = getEvents(me) + %> @fn getEvents + %> + %> @param + %> @return + % =================================================================== + n = 0; + while eventAvail(me) > 0 + n = n + 1; + evt(n) = TouchEventGet(me.devices(me.device), me.swin, 0); + end + if n == 0; evt = []; return; end + if me.drainEvents; evt = evt(end); end + end + % =================================================================== function [result, window] = calculateWindow(me, x, y, tempWindow) - %> @fn setup + %> @fn calculateWindow %> %> @param - %> @return + %> @return % =================================================================== if exist('tempWindow','var') && isnumeric(tempWindow) && length(tempWindow) == 4 pos = screenManager.rectToPos(tempWindow); @@ -347,8 +972,8 @@ classdef touchManager < optickaCore yWin = me.window.Y; end result = false; resultneg = false; match = false; - window = false; windowneg = false; - negradius = radius + me.negationBuffer; + window = false; windowneg = false; + negradius = radius + me.window.negationBuffer; ez = me.exclusionZone; % ---- test for exclusion zones first if ~isempty(ez) @@ -362,8 +987,8 @@ classdef touchManager < optickaCore end end % ---- circular test - if length(radius) == 1 - r = sqrt((x - xWin).^2 + (y - yWin).^2); %fprintf('x: %g-%g y: %g-%g r: %g-%g\n',x, me.window.X, me.y, me.window.Y,r,me.window.radius); + if isscalar(radius) + r = sqrt((x - xWin).^2 + (y - yWin).^2); %fprintf('X: %.1f-%.1f Y: %.1f-%.1f R: %.1f-%.1f\n',x, xWin, me.y, yWin, r, radius); window = find(r < radius); windowneg = find(r < negradius); else % ---- x y rectangular window test @@ -380,10 +1005,11 @@ classdef touchManager < optickaCore if match == true; break; end end end + me.win = window; if any(window); result = true;end if any(windowneg); resultneg = true; end if me.window.doNegation && resultneg == false - result = -100; + result = -100; end end end diff --git a/communication/zmqConnection.m b/communication/zmqConnection.m new file mode 100644 index 0000000000000000000000000000000000000000..65742668d6ff448d52a0fa914cbf876d36c0bc59 --- /dev/null +++ b/communication/zmqConnection.m @@ -0,0 +1,606 @@ +classdef zmqConnection < optickaCore + %> zmqConnection is a class to handle ØMQ connections for opticka class + %> communication. We use matlab-zmq (a basic libzmq binding), + %> and a REQ-REP pattern for main communication. + + properties + %> ØMQ connection type, e.g. 'REQ', 'REP', 'PUB', 'SUB', 'PUSH', 'PULL' + type = 'REQ' + %> transport for the socket, tcp | ipc | inproc + transport = 'tcp' + %> the address to open, use * for a server to bind to all interfaces + address = 'localhost' + %> the port to open + port = 6666 + %> default size of chunk to read/write for tcp + frameSize = 2^20 + %> default read timeout in ms, -1 is blocking + readTimeOut = 3e4 + %> default write timeout in ms, -1 is blocking + writeTimeOut = 3e4 + %> do we log to the command window? + verbose = false + %> for sendCommand and receiveCommand use zmq.core.poll? + alwaysPoll = false + + end + + properties (Dependent = true) + %> connection endpoint + endpoint + end + + properties (SetAccess = private, GetAccess = public, Transient = true) + %> is this connection open? + isOpen = false + %> zmq.Context() + context = [] + %> zmq.Socket() + socket = [] + %> last message + messages = [] + end + + properties (SetAccess = private, GetAccess = private) + sendState = false + recState = false + allowedProperties = {'type','protocol','port','address', 'alwaysPoll',... + 'verbose','readTimeOut','writeTimeOut','frameSize','cleanup'}; + end + + methods + + % =================================================================== + function me = zmqConnection(varargin) + %> @brief Class constructor for zmqConnection. + %> + %> @details Initializes a zmqConnection object, setting up default + %> properties and parsing any provided arguments using the optickaCore + %> superclass constructor and argument parsing. + %> + %> @param varargin Optional name-value pairs to override default properties. + %> Allowed properties are defined in `me.allowedProperties`. + %> + %> @return me An instance of the zmqConnection class. + % =================================================================== + args = optickaCore.addDefaults(varargin,struct('name','zmqConnection')); + me = me@optickaCore(args); %superclass constructor + me.parseArgs(args, me.allowedProperties); + end + + + % =================================================================== + function status = open(me) + %> @brief Opens the ØMQ socket connection. + %> @details Creates the ØMQ context if it doesn't exist, creates the + %> socket based on the `type` property, sets socket options like + %> `RCVTIMEO`, `SNDTIMEO`, and `LINGER`, and then either binds (for + %> server types like REP, PUB, PUSH) or connects (for client types) + %> to the specified `endpoint`. Sets the `isOpen` flag to true. + %> @note Does nothing if the connection `isOpen` is already true. + % =================================================================== + status = -1; + if me.isOpen; return; end + if ~isa(me.context, 'zmq.Context') + me.context = zmq.Context(); + else + me.context.close; + end + me.socket = me.context.socket(me.type); + me.socket.defaultBufferLength = me.frameSize; + if me.readTimeOut ~= -1 + me.set('RCVTIMEO', me.readTimeOut); + end + if me.writeTimeOut ~= -1 + me.set('SNDTIMEO', me.writeTimeOut); + end + me.set('LINGER', 1000); + me.set('RCVBUF', me.frameSize); + switch me.type + case {'REP','PUB','PUSH'} + status = me.socket.bind(me.endpoint); + otherwise + status = me.socket.connect(me.endpoint); + end + if status == 0 + me.isOpen = true; + me.addMessage('Socket is opened'); + else + me.addMessage('Socket failed to bind/connect!!!') + try me.socket.close(); end + error(me.messages(end)); + end + end + + % =================================================================== + function revents = poll(me, events, time) + %> @brief poll socket to identify whether we can send ('out') or receive ('in') + %> + %> @param events string 'in' 'out' or 'both' + %> @param time in ms, 0 = no wait, -1 = block until response + % =================================================================== + revents = 0; + if ~me.isOpen; return; end + if nargin < 3 || isempty(time); time = 0; end + if nargin < 2 || isempty(events); events = 'both'; end + revents = me.socket.poll(events,time); + end + + % =================================================================== + function [rep, dataOut, status, nbytes, msg] = sendCommand(me, command, data, getReply) + %> @brief Sends a command and optional data, then waits for a reply. + %> + %> @details Primarily for REQ/REP patterns. Uses `sendObject` to send the + %> command string and serialized data. If successful, it then calls + %> `receiveObject` to wait for and receive the reply command and data. + %> + %> @param command The command string to send. + %> @param data (Optional) MATLAB data to serialize and send along with the command. Defaults to empty. + %> + %> @return rep The reply command string received from the peer. + %> @return dataOut The deserialized MATLAB data received in the reply. + %> @return status 0 on success (send and receive completed), -1 on failure (send or receive failed). + %> @note Updates `sendState` and `recState` properties. Logs reply if verbose. + % =================================================================== + rep = ''; dataOut = []; status = -1; + if ~me.isOpen; return; end + if nargin < 4 || isempty(getReply); getReply = true; end + if nargin < 3 || isempty(data); data = {}; end + if nargin < 2 || isempty(command); error('---> zmq: You must pass a command!'); end + try + [status, nbytes, msg] = sendObject(me, command, data, true); + if status ~= 0 + me.sendState = false; me.recState = false; + warning(msg); + else + me.sendState = true; me.recState = false; + end + catch ME + t = sprintf('---> zmq: Receive status %i did not return any command: %s - %s...\n', status, ME.identifier, ME.message); + me.addMessage(t); + disp(t); + me.sendState = false; me.recState = false; + end + if status == 0 && getReply + [rep, dataOut] = receiveObject(me); + + me.addMessage(t); + if me.verbose + disp(t); + disp(dataOut); + end + me.sendState = false; me.recState = true; + end + end + + % =================================================================== + function [command, data, msg] = receiveCommand(me, sendReply) + %> @brief Receives a command and associated data, optionally sending an 'ok' reply. + %> @details Calls `receiveObject` to get the command string and any + %> serialized data. If `sendReply` is true (default) and a command was + %> successfully received, it sends back an 'ok' command using `sendObject`. + %> @param sendReply (Optional) Logical flag. If true (default), sends an + %> 'ok' reply upon successful receipt of a command. If false, no reply + %> is sent by this function. Defaults to true. + %> @return command The received command string. Empty if receive failed or timed out. + %> @return data The deserialized MATLAB data received with the command. Empty if no data part or on error. + %> @note Updates `sendState` and `recState` properties. Logs received command/data if verbose. + % =================================================================== + arguments(Input) + me + sendReply logical = true; + end + arguments(Output) + command string + data + msg string + end + command = ""; data = []; msg = ""; + if ~me.isOpen; return; end + + try + [command, data, msg] = receiveObject(me, true); + me.sendState = false; me.recState = true; % Update state after successful receive attempt + if isempty(command) && ~isempty(msg) + msg = sprintf('Receive problem: %s', msg); % Log if receiveObject reported an issue + me.addMessage(msg); + warning(msg) + me.recState = false; % Indicate receive wasn't fully successful + elseif ~isempty(command) + + if me.verbose > 0 + fprintf('---> zmq: Received command: «%s»\n', command); + if ~isempty(data) + disp('Received data:'); + disp(data); + end + end + end + catch ME + fprintf('---> zmq: Error during receiveCommand: cmd: %s msg: %s err: %s - %s\n', command, msg, ME.identifier, ME.message); + me.sendState = false; me.recState = false; % Reset state on error + command = ''; data = []; % Ensure empty return on error + return % Exit function on critical error + end + + % Send 'ok' reply only if requested and a command was actually received + if sendReply && ~isempty(command) && me.recState + status = sendObject(me, 'ok', {}); + if status ~= 0 + msg = sprintf('---> zmq: Default "ok" reply failed to be sent for command "%s"', command); + me.addMessage(msg); + warning(msg); + me.sendState = false; % Update state on send failure + else + me.sendState = true; me.recState = false; % Update state on send success + end + elseif ~isempty(command) && me.recState + % If reply is not sent here, the caller is responsible. + % The state remains sendState=false, recState=true. + end + end + + % =================================================================== + function flush(me) + %> @brief Flushes the receive buffer of the socket. + %> @details Temporarily sets the receive timeout (`RCVTIMEO`) to 0 (non-blocking) + %> and enters a loop calling `receive` until it returns a status of -1 + %> (indicating no more messages or an error). It then restores the + %> original `readTimeOut`. This is useful for discarding any pending + %> messages in the socket's incoming queue. + % =================================================================== + try + me.set('RCVTIMEO', 0); + N = 1000; + while N > 0 + status = 0; + if verifyEvent('in'); [~, status] = receive(me); end + if status == -1; N = 0; end + end + catch ME + if me.verbose; fprintf('---> zmq: Flush error: %s %s', ME.identifier, ME.message); end + end + me.set('RCVTIMEO', me.readTimeOut); + end + + % =================================================================== + function value = get(me, option) + %> @brief Gets the value of a ØMQ socket option. + %> @details A wrapper around the `zmq.Socket.get` method. + %> @param option The name of the socket option to retrieve (e.g., 'RCVTIMEO', 'SNDHWM'). + %> Case-insensitive, 'ZMQ_' prefix is optional. + %> @return value The current value of the specified socket option. + %> @warning Issues a warning if `option` is not provided. + % =================================================================== + if ~exist('option','var'); warning('---> zmq: No option given...'); return; end + value = me.socket.get(option); + end + + % =================================================================== + function status = set(me, option, value) + %> @brief Sets the value of a ØMQ socket option. + %> @details A wrapper around the `zmq.Socket.set` method. + %> @param option The name of the socket option to set (e.g., 'RCVTIMEO', 'LINGER'). + %> Case-insensitive, 'ZMQ_' prefix is optional. + %> @param value The value to assign to the socket option. + %> @return status 0 on success, non-zero on failure. + %> @warning Issues warnings if `option` or `value` are not provided. + % =================================================================== + if ~exist('option','var'); warning('---> zmq: No option given...'); return; end + if ~exist('value','var'); warning('---> zmq: No value given...'); return; end + status = me.socket.set(option, value); + if status ~= 0 + warning('zmqConnection:set:failure','---> zmq: Failed to set %s', option) + end + end + + % =================================================================== + function status = send(me, data) + %> @brief Sends raw data over the socket. + %> @details Determines the type of data and calls the appropriate + %> `zmq.Socket` send method (`send_string` for char/string, `send` for + %> uint8). If the data type is different, it attempts to use the + %> private `sendObject` method (which might not be intended for raw data). + %> @param data The data to send. Can be a character array, string, or uint8 array. + %> @return status 0 on success, -1 on failure (e.g., timeout, incorrect socket state). + %> @note Updates `sendState` and `recState`. Logs errors to the console. + % =================================================================== + try + status = 0; + if ischar(data) || isstring(data) + me.socket.send_string(data); + elseif isa(data,'uint8') + me.socket.send(data); + else + sendObject(me, data); + end + me.sendState = true; me.recState = false; + catch ME + status = -1; + t = sprintf('---> zmq: Couldn''t send, perhaps need to receive first: %s - %s', ME.identifier, ME.message); + me.addMessage(t); + if me.verbose; disp(t); end + end + end + + % =================================================================== + function [data, status] = receive(me) + %> @brief Receives raw data from the socket. + %> @details Calls `zmq.Socket.recv_multipart` to receive data. If the + %> result is a single-element cell array, it extracts the content. + %> @return data The received data, typically as a uint8 array or potentially + %> a cell array for true multipart messages. Empty on failure or timeout. + %> @return status 0 on success (implied, not explicitly returned on success), + %> -1 on failure (e.g., timeout). + %> @note Updates `sendState` and `recState`. Logs errors to the console. + % =================================================================== + data = []; status = -1; + try + data = me.socket.recv_multipart(); + if iscell(data) && isscalar(data) + data = data{:}; + end + status = 0; + me.sendState = false; me.recState = true; + catch ME + me.sendState = false; me.recState = false; + t = sprintf('---> zmq: No data received: %s - %s...\n', ME.identifier, ME.message); + me.addMessage(t); + if me.verbose; disp(t); end + end + end + + % =================================================================== + function close(me, keepContext) + %> @brief Closes the ØMQ socket and optionally the context. + %> @details Closes the underlying `zmq.Socket` if it's open. If + %> `keepContext` is false (default), it also closes the `zmq.Context`. + %> Sets the `isOpen` flag to false. + %> @param keepContext (Optional) Logical flag. If true, the ØMQ context + %> is kept open; otherwise (default), the context is also closed. + %> Defaults to false. + %> @note Uses `try...end` blocks to suppress errors during closure. + % =================================================================== + if ~exist('keepContext','var'); keepContext = false; end + try me.socket.close(); end %#ok<*TRYNC> + try me.context.close(); end + if ~keepContext + try me.context.term(); end + end + me.isOpen = false; + end + function delete(me) + %> @brief Class destructor. + %> @details Ensures the socket and context are closed by calling `close(me, false)` + %> when the object is destroyed. + % =================================================================== + close(me, false); + end + + % =================================================================== + function endpoint = get.endpoint(me) + %> @brief Gets the full endpoint string for the connection. + %> @details Constructs the endpoint string (e.g., 'tcp://localhost:5555') + %> based on the `transport`, `address`, and `port` properties. + %> @return endpoint The formatted endpoint string. + % =================================================================== + endpoint = sprintf('%s://%s:%i',me.transport,me.address,me.port); + end + + % =================================================================== + function [status, nbytes, msg] = sendObject(me, command, data, useJSON, options) + %> @brief (Private) Sends a command string and optional serialized MATLAB data. + %> @details This is the core sending method used by public methods like + %> `sendCommand` and `receiveCommand` (for replies). It checks if the + %> socket is open, validates the command is a string, serializes the + %> `data` using `getByteStreamFromArray` (if provided), and sends the + %> command and data as a two-part message using `zmq.Socket.send` with + %> the 'sndmore' flag if both parts exist. Handles sending only command, + %> only data, or an empty message if both are empty. + %> @param command The command string to send. Must be char or string. + %> @param data (Optional) MATLAB data to serialize and send. + %> @param useJSON (optional) wrap command and data with JSON + %> @param options (Optional) Cell array of additional flags for the final `send` call (e.g., {'ZMQ_DONTWAIT'}). + %> @return status 0 on success, -1 on failure. + %> @return nbytes The total number of bytes sent across all parts. + %> @return msg An error message string if `status` is -1. + %> @note This is a private method. Throws errors for invalid input or unopened socket. + % =================================================================== + + status = -1; nbytes = 0; msg = ''; + + % Check if the socket is open + if ~me.isOpen + error('---> zmq: Socket is not open. Please open the socket before sending data.'); + end + + % Check if the command is a string + if ~exist('command','var') || ~ischar(command) && ~isstring(command) + error('---> zmq: Command must be a string or character array.'); + end + + if nargin < 5 + options = {}; + end + + if nargin < 4 || isempty(useJSON) + useJSON = true; + end + + if nargin < 3 + data = []; + end + + % Serialize the object if it's not empty + if ~isempty(data) + serialData = getByteStreamFromArray(data); + else + % If no data, just send an empty array + serialData = uint8([]); + end + + try + if useJSON + j.command = command; + j.dataType = 'byteStream'; + j.data = serialData; + j = jsonencode(j); + b = matlab.net.base64encode(j); + nbytes = me.socket.send_multipart(uint8(b), options{:}); + elseif ~isempty(command) && ~isempty(serialData) + n1 = me.socket.send(uint8(command), 'sndmore'); + n2 = me.socket.send(serialData, options{:}); + nbytes = n1 + n2; + elseif ~isempty(command) + % Just send text + nbytes = me.socket.send(uint8(command), options{:}); + elseif ~isempty(serialData) + % Just send data + nbytes = me.socket.send(serialData, options{:}); + else + % Send empty message + nbytes = me.socket.send(uint8(''), options{:}); + end + status = 0; + catch ME + status = -1; + msg = [ME.identifier, ME.message]; + end + end + + % =================================================================== + function [command, data, msg] = receiveObject(me, useJSON, options) + %> @brief (Private) Receives a command string and optional serialized MATLAB data. + %> @details This is the core receiving method. It calls `zmq.Socket.recv` + %> to get the first part (expected to be the command string). It checks + %> the 'rcvmore' socket option to see if a second part (data) exists. + %> If so, it calls `zmq.Socket.recv_multipart` to get the remaining part(s), + %> concatenates them if necessary, and deserializes the result using + %> `getArrayFromByteStream`. + %> @param options (Optional) Cell array of flags for the initial `recv` call (e.g., {'ZMQ_DONTWAIT'}). + %> @return command The received command string. Empty on failure or timeout. + %> @return data The deserialized MATLAB data. Empty if no data part, deserialization fails, or on error. + %> @return msg An error message string if receiving the command failed or deserialization failed. + %> @note This is a private method. Throws an error if the socket is not open. Logs deserialization errors. + % =================================================================== + % Check if the socket is open + if ~me.isOpen + error('---> zmq: Socket is not open. Please open the socket before sending data.'); + end + + if nargin < 3; options = {}; end + + if nargin < 2 || isempty(useJSON); useJSON = true; end + + command = ''; data = []; msg = ''; frames = {}; + + try + frames = me.socket.recv_multipart(options{:}); + catch ME + warning('---> zmq: Failed to get object: %s - %s', ME.identifier, ME.message); + if matches(ME.identifier,'zmq:core:recv:EFSM') + t = sprintf('---> zmq: EFSM error, let''s try to flush and send'); + me.addMessage(t); + if me.verbose; disp(t); end + me.flush; + me.sendObject('error',{''}); + end + end + if isempty(frames); return; end + + + if useJSON + try + b = char([frames{1:end}]); + j = char(matlab.net.base64decode(b)); + src = jsondecode(j); + if isstruct(src) + command = src.command; + if isfield(src,'data') && ~isempty(src.data) + data = getArrayFromByteStream(uint8(src.data)); + end + end + catch ME + msg = '---> zmq: Cannot parse JSON...'; + me.addMessage(msg); + getReport(ME) + return + end + else + command = frames{1}; + if command == -1 + msg = '---> zmq: No data received...'; + me.addMessage(msg); + command = ''; + return + end + command = char(command); + if length(frames) > 1 + data = frames{2:end}; + if iscell(data) + data = [data{:}]; + end + % Deserialize the object if it's not empty + if ~isempty(data) + try + data = getArrayFromByteStream(data); + catch ME + msg = sprintf('---> zmq: Failed to deserialize object: %s - %s', ME.identifier, ME.message); + me.addMessage(msg); + warning(msg); + data = []; + end + else + data = []; + end + else + % No object part in the message + data = []; + end + end + end + end + + methods (Access = private) + + function addMessage(me, msg) + if nargin < 2; return; end + if isstruct(msg) || isobject(msg) + msg = formattedDisplayText(msg,"NumericFormat","short","LineSpacing","compact"); + elseif ischar(msg) + msg = string(msg); + elseif length(msg) > 1 + msg = join(msg); + else + return; + end + if isempty(me.messages) + me.messages(1) = msg; + else + me.messages(end+1) = msg; + end + end + + function out = verifyEvent(me, events) + if ~me.alwaysPoll; out = true; return; end + out = false; + r = poll(me,'both',0); + switch events + case 'in' + if matches(r,{'in','both'}) + out = true; + end + case 'out' + if matches(r,{'out','both'}) + out = true; + end + case 'none' + if matches(r,'none') + out = true; + end + end + end + + end + +end diff --git a/communication/Eyelink_Remote_Calibration.zip b/eyetracker/Eyelink_Remote_Calibration.zip similarity index 100% rename from communication/Eyelink_Remote_Calibration.zip rename to eyetracker/Eyelink_Remote_Calibration.zip diff --git a/communication/edfmex.m b/eyetracker/edfmex.m similarity index 100% rename from communication/edfmex.m rename to eyetracker/edfmex.m diff --git a/communication/edfmex.mexa64 b/eyetracker/edfmex.mexa64 similarity index 100% rename from communication/edfmex.mexa64 rename to eyetracker/edfmex.mexa64 diff --git a/communication/edfmex.mexmaci64 b/eyetracker/edfmex.mexmaca64 similarity index 47% rename from communication/edfmex.mexmaci64 rename to eyetracker/edfmex.mexmaca64 index 7c940fc5ad9f0d6174ce654e9053ff19ac928bdb..2e13e5a0a392f3d2512be78ac908357f268cde1d 100644 Binary files a/communication/edfmex.mexmaci64 and b/eyetracker/edfmex.mexmaca64 differ diff --git a/eyetracker/edfmex.mexmaci64 b/eyetracker/edfmex.mexmaci64 new file mode 100644 index 0000000000000000000000000000000000000000..4cf2742be59fa1969607785a68690e5a040d9e9f Binary files /dev/null and b/eyetracker/edfmex.mexmaci64 differ diff --git a/communication/edfmex.mexw64 b/eyetracker/edfmex.mexw64 similarity index 100% rename from communication/edfmex.mexw64 rename to eyetracker/edfmex.mexw64 diff --git a/eyetracker/eyelinkCustomCallback.m b/eyetracker/eyelinkCustomCallback.m new file mode 100755 index 0000000000000000000000000000000000000000..fe527579e0e7b320c849714ec8c6d49b52ded8f1 --- /dev/null +++ b/eyetracker/eyelinkCustomCallback.m @@ -0,0 +1,581 @@ +function rc = eyelinkCustomCallback(callArgs, msg) +% eyelinkCustomCallback implementes the EyeLink Core Graphics part +% of the EyeLink API. This "Core Graphics" part of our API is responsible +% for handling the times when the API and Host PC takes control of the eye +% tracking procedures. This includes the functionality to stream camera +% images during camera/participant setup, displaying targets at locations +% on the participant screen during calibration, validation, and drift/check +% and correction routines. Complimentary to handling the visual aspect of +% these operations contingent on display routines, the functionality +% implemented herewith also handles the playback of feedback sounds to +% the experimenter and participant for guiding these interactive +% procedures. During these modes of operation, this function also +% implements the forwarding of kepresses to the Host PC that are registered +% on the computer's keyboard which is running this implementation. The +% purpose of this is to make sure that bost Host and Display PCs are +% operating as identically in these modes of operation. +% +% +% This function is normally called from within the Eyelink() mex file. +% Normal user code only calls it once to supply the eyelink defaults struct. +% This is handled within the EyelinkInitDefaults.m file, so you generally +% should not have to worry about this. However, if you change settings in +% the el structure, you may need to call it yourself. +% +% To define which onscreen window the eye image should be +% drawn to, call it with the return value from EyelinkInitDefaults, e.g., +% w=Screen('OpenWindow', ...); +% el=EyelinkInitDefaults(w); +% myEyelinkDispatchCallback(el); +% +% +% To actually receive and display the images, register this function as +% eyelink's callback: +% +% +% if Eyelink('Initialize', 'myEyelinkDispatchCallback') ~=0 +% error('eyelink failed init') +% end +% result = Eyelink('StartSetup',1) %put the tracker into a mode capable of sending images +% +% +% then you must hit 'return' on the PTB computer, this key command will be +% sent to the tracker host to initiate sending of images. +% +% +% History: +% 15. 3.2009 Derived from MemoryBuffer2TextureDemo.m (MK). +% 4. 4.2009 Updated to use EyelinkGetKey + fixed eyelinktex persistence +% crash (edf). +% 11. 4.2009 Cleaned up. Should be ready for 1st release, although still +% pretty alpha quality. (MK). +% 15. 6.2010 Added some drawing routines to get standard behaviour back. +% Enabled use of the callback by default. Clarified in +% helptext that user normally should not have to worry +% about calling this file. (fwc) +% 20. 7.2010 Drawing of instructions, eye-image+title, playing sounds in +% seperate functions +% +% 1. 2.2010 Modified to allow for cross hair and fix bugs. (nj)= +% 29.10.2018 Drop 'DrawDots' for calibration target. Some white-space fixes. +% 24. 3.2020 Cleaned up the documentation of this function, and added +% additiontal handling for two types of stereoscopic +% calibrations, ability to reference video files for +% animated calibration targets, bug fixes for audio +% feedback playback. Apologies to NJ for removing +% previous comments where code was previously added, this +% was done for easier reading of the code. +% 15.3.2020 br added Snd('Close') after Beeper to free sound device +% and prevent problems downstream with PsychPortAudio; changed +% flip 'dontsync' to '0' to fix missing target flips on Linux; +% cleaned command 7 & 11 to fix issue drawing instructions +% + +% Cached texture handle for eyelink texture: +persistent eyelinktex; +global dw dh offscreen; +global eyelinkanimationtarget; +global aM rM; + +% Cached window handle for target onscreen window: +persistent eyewin; +persistent calxy; +persistent imgtitle; +persistent eyewidth; +persistent eyeheight; + +% Cached(!) eyelink stucture containing keycodes +persistent el; +persistent lastImageTime; %#ok +persistent drawcount; +persistent ineyeimagemodedisplay; +persistent clearScreen; +persistent drawInstructions; + +% Cached constant definitions: +persistent GL_RGBA; +persistent GL_RGBA8; +persistent hostDataFormat; + +persistent inDrift; +offscreen = 0; +newImage = 0; + + +if 0 == Screen('WindowKind', eyelinktex) + eyelinktex = []; % Previous PTB Screen() window has closed, needs to be recreated. +end + + +if isempty(eyelinktex) + % Define the two OpenGL constants we actually need. No point in + % initializing the whole PTB OpenGL mode for just two constants: + GL_RGBA = 6408; + GL_RGBA8 = 32856; + GL_UNSIGNED_BYTE = 5121; %#ok + GL_UNSIGNED_INT_8_8_8_8 = 32821; %#ok + GL_UNSIGNED_INT_8_8_8_8_REV = 33639; + hostDataFormat = GL_UNSIGNED_INT_8_8_8_8_REV; + drawcount = 0; + lastImageTime = GetSecs; +end + +% Preinit return code to zero: +rc = 0; + +if nargin < 2 + msg = []; +end + +if nargin < 1 + callArgs = []; +end + +if isempty(callArgs) + error('You must provide some valid "callArgs" variable as 1st argument!'); +end + +if ~isnumeric(callArgs) && ~isstruct(callArgs) + error('"callArgs" argument must be a EyelinkInitDefaults struct or double vector!'); +end + +% Eyelink el struct provided? +if isstruct(callArgs) && isfield(callArgs,'window') + % Check if el.window subfield references a valid window: + if Screen('WindowKind', callArgs.window) ~= 1 + error('argument didn''t contain a valid handle of an open onscreen window! pass in result of EyelinkInitDefaults(previouslyOpenedPTBWindowPtr).'); + end + + % Ok, valid handle. Assign it and return: + eyewin = callArgs.window; + + % Assume rest of el structure is valid: + el = callArgs; + clearScreen=1; + eyelinktex=[]; + lastImageTime=GetSecs; + ineyeimagemodedisplay=0; + drawInstructions=1; + return; +end + + +% Not an eyelink struct. Either a 4 component vector from Eyelink(), or something wrong: +if length(callArgs) ~= 4 + error('Invalid "callArgs" received from Eyelink() Not a 4 component double vector as expected!'); +end + +% Extract command code: +eyecmd = callArgs(1); + +if isempty(eyewin) + warning('Got called as callback function from Eyelink() but usercode has not set a valid target onscreen window handle yet! Aborted.'); %#ok + return; +end + +% (Re)set Flag for new camera image +newcamimage = 0; +needsupdate = 0; +% fprintf('dpc: %d\n', eyecmd); % for debug +switch eyecmd + case 1 % New Camera Image Received + newcamimage = 1; + needsupdate = 1; + + case 2 % EyeLink Keyboard Query + [rc, el] = EyelinkGetKey(el); + if rc == 32 && exist('rM','var') && isa(rM,'arduinoManager') && rM.isOpen + giveReward(rM); + end + + case 3 % Alert message + fprintf('Eyelink Alert: %s.\n', msg); + needsupdate = 1; + + case 4 % Camera Image Caption Text + if callArgs(2) ~= -1 + imgtitle = sprintf('Camera: %s [Threshold = %f]', msg, callArgs(2)); + else + imgtitle = msg; + end + needsupdate = 1; + + case 5 % Draw Cal Target + calxy = callArgs(2:3); + clearScreen=1; + needsupdate = 1; + if strcmpi(el.calTargetType, 'video') && ~isempty(eyelinkanimationtarget) + if el.calAnimationResetOnTargetMove && Screen('GetMovieTimeIndex', eyelinkanimationtarget.movie) + Screen('SetMovieTimeIndex', eyelinkanimationtarget.movie, 0, el.calAnimationSetIndexIsFrames); + end + Screen('PlayMovie', eyelinkanimationtarget.movie, 1, el.calAnimationLoopParam, el.calAnimationAudioVolume) + end + + case 6 % Clear Cal Display + clearScreen=1; + drawInstructions=1; + needsupdate = 1; + + case 7 % Setup Cal Display + if inDrift + drawInstructions = 0; + else + drawInstructions = 1; + end + clearScreen=1; + drawcount = 0; + lastImageTime = GetSecs; + needsupdate = 1; + + case 8 % Setup Image Display + newImage = 1; + eyewidth = callArgs(2); + eyeheight = callArgs(3); + drawcount = 0; + lastImageTime = GetSecs; + ineyeimagemodedisplay=1; + drawInstructions=1; + needsupdate = 1; + + case 9 % Exit Image Display + clearScreen=1; + ineyeimagemodedisplay=0; + drawInstructions=1; + needsupdate = 1; + + case 10 % Erase Cal Target + calxy = []; + if ~isempty(eyelinkanimationtarget) + eyelinkanimationtarget.calxy=calxy; + end + clearScreen=1; + needsupdate = 1; + + if strcmpi(el.calTargetType, 'video') && ~isempty(eyelinkanimationtarget) + texkill=Screen('GetMovieImage', eyewin, eyelinkanimationtarget.movie, 0); + Screen('PlayMovie', eyelinkanimationtarget.movie, 0, el.calAnimationLoopParam); + if texkill > 0 + Screen('Close', texkill); + end + end + + + + case 11 % Exit Cal Display + calxy = []; + if(~isempty(eyelinkanimationtarget) ) + eyelinkanimationtarget.calxy=calxy; + end + + if inDrift + inDrift = 0; + drawInstructions = 0; + else + drawInstructions = 1; + end + + clearScreen=1; + needsupdate=1; + if strcmpi(el.calTargetType, 'video') && ~isempty(eyelinkanimationtarget) + texkill=Screen('GetMovieImage', eyewin, eyelinkanimationtarget.movie, 0); + Screen('PlayMovie', eyelinkanimationtarget.movie, 0, el.calAnimationLoopParam); + if texkill > 0 + Screen('Close', texkill); + end + end + + case 12 % New Cal Target Sound + EyelinkMakeSound(el, 'cal_target_beep'); + + case 13 % New Drift Chk/Corr Sound + EyelinkMakeSound(el, 'drift_correction_target_beep'); + + case 14 % Cal Done Sound + errc = callArgs(2); + if errc > 0 + % Failed + EyelinkMakeSound(el, 'calibration_failed_beep'); + else + % Success + EyelinkMakeSound(el, 'calibration_success_beep'); + end + + case 15 % Drift Chk/Corr Done Sound + errc = callArgs(2); + if errc > 0 + % Failed + EyelinkMakeSound(el, 'drift_correction_failed_beep'); + else + % Success + EyelinkMakeSound(el, 'drift_correction_success_beep'); + end + + case 16 % Get Mouse Position + [width, height]=Screen('WindowSize', eyewin); + [x,y, buttons] = GetMouse(eyewin); + HideCursor; + if find(buttons) + rc = [width , height, x , y, dw , dh , 1]; + else + rc = [width , height, x , y , dw , dh , 0]; + end + + case 17 % + inDrift =1; + + otherwise % Unknown Command + fprintf('PsychEyelinkDispatchCallback: Unknown eyelink command (%i)\n', eyecmd); + return; + +end + +if ~needsupdate % Display redraw and update needed? + if ~isempty(eyelinkanimationtarget) && ~isempty(calxy) + EyelinkDrawCalibrationTarget(eyewin, el, calxy); + end + return; +end + +if clearScreen==1 % Need to clear and redraw display before new content flipped + EyelinkDrawClearScreen(eyewin, el); + clearScreen=0; +end + +if newcamimage % New image frame received from EyeLink camera stream + % Image has dimensions: 'eyewidth' by 'eyeheight' in pixel units + % Each pixel is encoded as a 4 byte RGBA pixel with A=255 always + % RGB channels each encode a 1-Byte per channel R, G or B color value. + % 'eyeimgptr' is a memory pointer to the buffer inside Eyelink() that + % encodes the image. + eyeimgptr = callArgs(2); + eyewidth = callArgs(3); + eyeheight = callArgs(4); + + % Creates a new or reuses an existing PTB texture for the cam image + eyelinktex = Screen('SetOpenGLTextureFromMemPointer', eyewin, eyelinktex, eyeimgptr, eyewidth, eyeheight, 4, 0, [], GL_RGBA8, GL_RGBA, hostDataFormat); +end + +if ~isempty(eyelinktex) && ineyeimagemodedisplay==1 % Draw cam image and caption + imgtitle=EyelinkDrawCameraImage(eyewin, el, eyelinktex, imgtitle, newImage); +% if eyelinktex >= 0 +% Screen('Close', eyelinktex); +% end +end + +if ~isempty(calxy) % Draw Cal Target + drawInstructions=0; + EyelinkDrawCalibrationTarget(eyewin, el, calxy); +end + +if drawInstructions == 1 % Draw Instructions + EyelinkDrawInstructions(eyewin, el, msg); + drawInstructions = 0; +end + +dontsync = 0; +Screen('Flip', eyewin, [], dontsync, 1); % Show it + + +% Some counter, just to measure update rate: +drawcount = drawcount + 1; +return; + + + +% Start of nested EyelinkDraw* function declarations + + function EyelinkDrawClearScreen(eyewin, el) + if el.winInfo.StereoMode ~= 0 + drawScreens = 2; % stereoscopic drawing + else + drawScreens = 1; % non-stereoscopic drawing + end + for it = 0:drawScreens-1 + Screen('SelectStereoDrawBuffer', eyewin, it); % select left eye window + Screen('FillRect', eyewin, ... + el.backgroundcolour); + end + end + + function EyelinkDrawInstructions(eyewin, el,msg) + if el.winInfo.StereoMode ~= 0 + drawScreens = 2; % stereoscopic drawing + else + drawScreens = 1; % non-stereoscopic drawing + end + + for it = 0:drawScreens-1 + Screen('SelectStereoDrawBuffer', eyewin, it); % select left eye window + oldFont=Screen(eyewin,'TextFont',el.msgfont); + oldFontSize=Screen(eyewin,'TextSize',el.msgfontsize); + DrawFormattedText(eyewin, el.helptext, 20, 20, el.msgfontcolour, [], [], [], 1); + + if el.displayCalResults && ~isempty(msg) + DrawFormattedText(eyewin, msg, 20, 150, el.msgfontcolour, [], [], [], 1); + end + + Screen(eyewin,'TextFont',oldFont); + Screen(eyewin,'TextSize',oldFontSize); + end + + + end + + + + function imgtitle=EyelinkDrawCameraImage(eyewin, el, eyelinktex, imgtitle, newImage) + persistent lasttitle; + % global dh dw offscreen; + if el.winInfo.StereoMode ~= 0 + drawScreens = 2; % stereoscopic drawing + else + drawScreens = 1; % non-stereoscopic drawing + end + + for it = 0:drawScreens-1 + try + if ~isempty(eyelinktex) + Screen('SelectStereoDrawBuffer', eyewin, it); % select left eye window + eyerect=Screen('Rect', eyelinktex); + % we could cash some of the below values.... + wrect=Screen('Rect', eyewin); + [width, heigth]=Screen('WindowSize', eyewin); + dw=round(el.eyeimgsize/100*width); + dh=round(dw * eyerect(4)/eyerect(3)); + + drect=[ 0 0 dw dh ]; + drect=CenterRect(drect, wrect); + Screen('DrawTexture', eyewin, eyelinktex, [], drect); + end + % imgtitle + % if title is provided, we also draw title + if ~isempty(eyelinktex) && exist( 'imgtitle', 'var') && ~isempty(imgtitle) + Screen('SelectStereoDrawBuffer', eyewin, it); % select left eye window + rect=Screen('TextBounds', eyewin, imgtitle ); + [w2, h2]=RectSize(rect); + + if -1 == Screen('WindowKind', offscreen) + Screen('Close', offscreen); + end + Screen('SelectStereoDrawBuffer', eyewin, it); % select left eye window + sn = Screen('WindowScreenNumber', eyewin); + offscreen = Screen('OpenOffscreenWindow', sn, el.backgroundcolour, [], [], 32); + Screen(offscreen,'TextFont',el.imgtitlefont); + Screen(offscreen,'TextSize',el.imgtitlefontsize); + Screen('DrawText', offscreen, imgtitle, width/2-dw/2, heigth/2+dh/2+h2, el.imgtitlecolour); + Screen('SelectStereoDrawBuffer', eyewin, it); % select left eye window + Screen('DrawTexture',eyewin,offscreen, [width/2-dw/2 heigth/2+dh/2+h2 width/2-dw/2+500 heigth/2+dh/2+h2+500], [width/2-dw/2 heigth/2+dh/2+h2 width/2-dw/2+500 heigth/2+dh/2+h2+500]); + Screen('Close',offscreen); + + lasttitle = imgtitle; + end + catch %myerr + fprintf('EyelinkDrawCameraImage:error \n'); + disp(psychlasterror); + end + end + end + + + + + + function EyelinkDrawCalibrationTarget(eyewin, el, calxy) + if el.winInfo.StereoMode ~= 0 + drawScreens = 2; % stereoscopic drawing + else + drawScreens = 1; % non-stereoscopic drawing + end + + for it = 0:drawScreens-1 + Screen('SelectStereoDrawBuffer', eyewin, it); % select eye window + switch el.calTargetType + case 'video' + if( ~isempty(el.calAnimationTargetFilename) && ~isempty(eyelinkanimationtarget)) + rect=CenterRectOnPoint([0 0 eyelinkanimationtarget.imgw eyelinkanimationtarget.imgh], calxy(1), calxy(2)); + if it == 0 + tex=Screen('GetMovieImage', eyewin, eyelinkanimationtarget.movie, 0); + end + if(tex>0) + Screen('DrawTexture', eyewin , tex, [], rect, [], 0); + if drawScreens == 1 || (drawScreens == 2 && it == 1) + Screen('Flip', eyewin); + end + end + if it == drawScreens-1 && tex > 0 + Screen('Close', tex); + end + end + + case 'image' + if ~isempty(el.calImageInfo) && ~isempty(el.calImageTexture) + rect=CenterRectOnPoint([0 0 el.calImageInfo.Width el.calImageInfo.Height], calxy(1), calxy(2)); + end + Screen('DrawTexture', eyewin , el.calImageTexture, [], rect, [], 0); + + otherwise + % default to el.calTargetType = 'ellipse' target + % drawing + size=round(el.calibrationtargetsize/100*1080); %BRtodo - add/test 'width' instead of 960 + inset=round(el.calibrationtargetwidth/100*1080);%BRtodo - add/test 'width' instead of 960 + if size < 4; size = 4; end + if inset < 2; inset = 2; end + % Use FillOval for larger dots: + Screen('FillOval', eyewin, el.calibrationtargetcolour, [calxy(1)-size/2 calxy(2)-size/2 calxy(1)+size/2 calxy(2)+size/2], size+2); + Screen('FillOval', eyewin, el.backgroundcolour, [calxy(1)-inset/2 calxy(2)-inset/2 calxy(1)+inset/2 calxy(2)+inset/2], inset+2) + end + end + end + + + function EyelinkMakeSound(el, s) + % set all sounds in one place, sound params defined in + % eyelinkInitDefaults + if ~exist('aM','var') || ~isa(aM,'audioManager'); return; end + switch(s) + case 'cal_target_beep' + doBeep=el.targetbeep; + f=el.cal_target_beep(1); + v=el.cal_target_beep(2); + d=el.cal_target_beep(3); + case 'drift_correction_target_beep' + doBeep=el.targetbeep; + f=el.drift_correction_target_beep(1); + v=el.drift_correction_target_beep(2); + d=el.drift_correction_target_beep(3); + case 'calibration_failed_beep' + doBeep=el.feedbackbeep; + f=el.calibration_failed_beep(1); + v=el.calibration_failed_beep(2); + d=el.calibration_failed_beep(3); + case 'calibration_success_beep' + doBeep=el.feedbackbeep; + f=el.calibration_success_beep(1); + v=el.calibration_success_beep(2); + d=el.calibration_success_beep(3); + case 'drift_correction_failed_beep' + doBeep=el.feedbackbeep; + f=el.drift_correction_failed_beep(1); + v=el.drift_correction_failed_beep(2); + d=el.drift_correction_failed_beep(3); + case 'drift_correction_success_beep' + doBeep=el.feedbackbeep; + f=el.drift_correction_success_beep(1); + v=el.drift_correction_success_beep(2); + d=el.drift_correction_success_beep(3); + otherwise + % some defaults + doBeep=el.feedbackbeep; + f=500; + v=0.5; + d=1.5; + end + + if doBeep==1 && isa(aM,'audioManager') && aM.isOpen + beep(aM, f, d, v); + end + end +end + + + diff --git a/communication/eyetrackerManager.m b/eyetracker/eyelinkManager.m old mode 100644 new mode 100755 similarity index 38% rename from communication/eyetrackerManager.m rename to eyetracker/eyelinkManager.m index b73ecef9e86cc476f100ab29adab4cb07dc0f580..e8e2505286cc98971b07749ce4c390ffedb0eb98 --- a/communication/eyetrackerManager.m +++ b/eyetracker/eyelinkManager.m @@ -1,146 +1,101 @@ % ======================================================================== -%> @class eyeTracker Manager -- parent class for all eyetrackers -%> WIP +classdef eyelinkManager < eyetrackerCore +%> @class eyelinkManager +%> @brief eyelinkManager wraps around the eyelink toolbox functions offering a +%> consistent interface and methods for fixation window control. See +%> eyetrackerCore for the common methods that handle fixation windows etc. %> -%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md +%> Copyright ©2014-2023 Ian Max Andolina — released: LGPL3, see LICENCE.md % ======================================================================== -classdef eyetrackerManager < optickaCore + + %-----------------CONTROLLED PROPERTIES-------------% + properties (SetAccess = protected, GetAccess = public) + %> type of eyetracker + type = 'eyelink' + end + %---------------PUBLIC PROPERTIES---------------% properties - %> fixation window in deg with 0,0 being the screen center: - %> - %> if X and Y have multiple rows, assume each row is a different - %> fixation window. so that multiple fixtation windows can be used. - %> - %> if radius has a single value, assume circular window if radius - %> has 2 values assume width × height rectangle (not strictly a - %> radius!) - %> - %> initTime is the time the subject has to initiate fixation - %> - %> time is the time the subject must maintain fixation within the - %> window - %> - %> strict = false allows subject to exit and enter window without - %> failure, useful during training - fixation struct = struct('X',0,'Y',0,'initTime',1,'time',1,... - 'radius',1,'strict',true) - %> Use exclusion zones where no eye movement allowed: [-degX +degX -degY - %> +degY] Add rows to generate multiple exclusion zones. - exclusionZone double = [] - %> we can define an optional window that the subject must stay - %> inside before they saccade to other targets. This restricts - %> guessing and "cheating", by forcing a minimum delay (default = - %> 100ms / 0.1s) before initiating a saccade. Only used if X - %> position is not empty. - fixInit struct = struct('X',[],'Y',[],'time',0.1,'radius',2) - %> add a manual offset to the eye position, similar to a drift correction - %> but handled by the eyelinkManager. - offset struct = struct('X',0,'Y',0) - %> start eyetracker in dummy mode? - isDummy logical = false - %> do we record and retrieve eyetracker EDF file? - recordData logical = true - %> do we ignore blinks, if true then we do not update X and Y position - %> from previous eye location, meaning the various methods will maintain - %> position, e.g. if you are fixated and blink, the within-fixation X - %> and Y position are retained so that a blink does not "break" - %> fixation. a blink is defined as a state whre gx and gy are MISSING - %> and pa is 0. Technically we can't really tell if a subject is - %> blinking or has removed their head using the float data. - ignoreBlinks logical = false + %> properties to setup and modify calibration + calibration = struct( ... + 'style','HV9', ... + 'proportion',[0.6 0.6], ... + 'manual',false, ... + 'paceDuration',1000, ... + 'IP','', ... + 'eyeUsed', 0, ... + 'enableCallbacks', true, ... + 'callback', 'eyelinkCustomCallback', ... + 'devicenumber', [], ... + 'targetbeep', 1, ... + 'feedbackbeep', 1, ... + 'calibrationtargetsize', 3, ... + 'calibrationtargetwidth', 1, ... + 'calibrationtargetcolour', [1 1 1]) + %> eyetracker defaults structure + defaults = struct() end + %---------------HIDDEN PROPERTIES---------------% properties (Hidden = true) - %> stimulus positions to draw on screen - stimulusPositions = [] - secondScreen = false - end - - properties (SetAccess = protected, GetAccess = public) - %> Gaze X position in degrees - x = [] - %> Gaze Y position in degrees - y = [] - %> pupil size - pupil = [] - %> last isFixated true/false result - isFix = false - %> did the fixInit test fail or not? - isInitFail = false - %> are we in a blink? - isBlink = false - %> are we in an exclusion zone? - isExclusion = false - %> total time searching and holding fixation - fixTotal = 0 - %> Initiate fixation length - fixInitLength = 0 - %how long have we been fixated? - fixLength = 0 - %> Initiate fixation time - fixInitStartTime = 0 - %the first timestamp fixation was true - fixStartTime = 0 - %> which fixation window matched the last fixation? - fixWindow = 0 - %> last time offset betweeen tracker and display computers - currentOffset = 0 - %> tracker time stamp - trackerTime = 0 - %current sample taken from eyelink - currentSample = [] - %current event taken from eyelink - currentEvent = [] - % are we connected to eyelink? - isConnected logical = false - % are we recording to an EDF file? - isRecording logical = false - % which eye is the tracker using? - eyeUsed = -1 - %version of eyelink - version = '' - %> the PTB screen to work on, passed in during initialise - screen = [] - %> All gaze X position in degrees reset using resetFixation - xAll = [] - %> Last gaze Y position in degrees reset using resetFixation - yAll = [] - %> all pupil size reset using resetFixation - pupilAll = [] + %> verbosity level + verbosityLevel = 4 + %> force drift correction? + forceDriftCorrect = true + %> drift correct max + driftMaximum = 15 + %> custom calibration target + customTarget = [] end + %---------------SEMI-PROTECTED PROPERTIES----------% properties (SetAccess = protected, GetAccess = ?optickaCore) + % value for missing data + MISSING_DATA = -32768 + tempFile = 'eyeData' + error = [] + %> previous message sent to eyelink + previousMessage = '' + end + + %--------------------PROTECTED PROPERTIES----------% + properties (SetAccess = protected, GetAccess = protected) %> allowed properties passed to object upon construction - allowedProperties char = ['fixation|exclusionZone|fixInit|offset|ignoreBlinks|sampleRate|'... - 'calibrationStyle|calibrationProportion|recordData|modify|' ... - 'enableCallbacks|callback|name|verbose|isDummy|remoteCalibration|IP'] + allowedProperties = {'calibration', 'defaults','verbosityLevel'} end - methods + %======================================================================= + methods %------------------PUBLIC METHODS + %======================================================================= + % =================================================================== + function me = eyelinkManager(varargin) + %> @fn eyelinkManager(varargin) %> @brief This is the constructor for this class %> % =================================================================== - function me = eyelinkManager(varargin) - args = optickaCore.addDefaults(varargin,struct('name','eyelink manager')); - me=me@optickaCore(args); %we call the superclass constructor first + args = optickaCore.addDefaults(varargin,struct('name','Eyelink','sampleRate',1000)); + me=me@eyetrackerCore(args); %we call the superclass constructor first me.parseArgs(args, me.allowedProperties); - me.defaults = EyelinkInitDefaults(); try % is eyelink interface working me.version = Eyelink('GetTrackerVersion'); catch %#ok me.version = 0; end + me.defaults = EyelinkInitDefaults(); end % =================================================================== - %> @brief initialise the eyelink, setting up the proper settings - %> and opening the EDF file if me.recordData is true + function success = initialise(me, sM) + %> @fn initialise + %> @brief initialise the eyelink with the screenManager object, setting + %> up the calibration options and opening the EDF file if me.recordData + %> is true. %> + %> @param sM screenManager to link to % =================================================================== - function success = initialise(me,sM) + if me.isOff; me.isDummy = true; return; end success = false; if ~exist('sM','var') warning('Cannot initialise without a PTB screen') @@ -152,192 +107,129 @@ classdef eyetrackerManager < optickaCore Eyelink('Shutdown'); %just make sure link is closed catch ME getReport(ME) - warning('Problems with Eyelink initialise, make sure you install Eyelink Developer libraries!'); + uiwait(warndlg('Problems with Eyelink initialise, make sure you install Eyelink Developer libraries!','eyelinkManager','modal')); me.isDummy = true; end end me.screen = sM; - if ~isempty(me.IP) && ~me.isDummy - me.salutation('Eyelink Initialise',['Trying to set custom IP address: ' me.IP],true) - ret = Eyelink('SetAddress', me.IP); + if isfield(me.calibration,'IP') && ~isempty(me.calibration.IP) && ~me.isDummy + me.salutation('Eyelink Initialise',['Trying to set custom IP address: ' me.calibration.IP],true) + ret = Eyelink('SetAddress', me.calibration.IP); if ret ~= 0 - warning('!!!--> Couldn''t set IP address to %s!!!\n',me.IP); + warning('!!!--> Couldn''t set IP address to %s!!!\n',me.calibration.IP); end end - if ~isempty(me.callback) && me.enableCallbacks - [res,dummy] = EyelinkInit(me.isDummy,me.callback); - elseif me.enableCallbacks - [res,dummy] = EyelinkInit(me.isDummy,1); - else - [res,dummy] = EyelinkInit(me.isDummy,0); - end - me.isDummy = logical(dummy); - me.checkConnection(); - if ~me.isConnected && ~me.isDummy - me.salutation('Eyelink Initialise','Could not connect, or enter Dummy mode...',true) - return - end - - if me.screen.isOpen == true + if me.screen.isOpen me.win = me.screen.win; me.defaults = EyelinkInitDefaults(me.win); - elseif ~isempty(me.win) - me.defaults = EyelinkInitDefaults(me.win); else me.defaults = EyelinkInitDefaults(); end - me.defaults.winRect=me.screen.winRect; - % this command is sent from EyelinkInitDefaults - % Eyelink('Command', 'screen_pixel_coords = %ld %ld %ld %ld',me.screen.winRect(1),me.screen.winRect(2),me.screen.winRect(3)-1,me.screen.winRect(4)-1); - if ~isempty(me.callback) && exist(me.callback,'file') - me.defaults.callback = me.callback; + if ~isempty(me.screen.winRect) && length(me.screen.winRect)==4 + me.defaults.winRect=me.screen.winRect; + end + + if ~isempty(me.calibration.callback) && exist(me.calibration.callback,'file') + me.defaults.callback = me.calibration.callback; end - me.defaults.backgroundcolour = me.screen.backgroundColour; + + me.defaults.backgroundcolour = [me.screen.backgroundColour(1:3) 1]; me.ppd_ = me.screen.ppd; me.defaults.ppd = me.screen.ppd; - %structure of eyelink modifiers - fn = fieldnames(me.modify); + fn = fieldnames(me.calibration); for i = 1:length(fn) if isfield(me.defaults,fn{i}) - me.defaults.(fn{i}) = me.modify.(fn{i}); + me.defaults.(fn{i}) = me.calibration.(fn{i}); end end + + if me.defaults.targetbeep==1; me.defaults.feedbackbeep=1; end me.defaults.verbose = me.verbose; + me.defaults.debugPrint = me.verbose; - if ~isempty(me.customTarget) - me.customTarget.reset(); - me.customTarget.setup(me.screen); - me.defaults.customTarget = me.customTarget; + updateDefaults(me); + + if me.calibration.enableCallbacks + [res,dummy] = EyelinkInit(me.isDummy, me.calibration.callback); else - me.defaults.customTarget = []; + [res,dummy] = EyelinkInit(me.isDummy,0); + end + + if ~res + me.isConnected = false; + me.isDummy = true; + else + me.isDummy = logical(dummy); + me.checkConnection(); end - - updateDefaults(me); if me.isDummy me.version = 'Dummy Eyelink'; else [~, me.version] = Eyelink('GetTrackerVersion'); + try + [~,majorVersion]=regexp(me.version,'.*?(\d)\.\d*?','Match','Tokens'); + majorVersion = majorVersion{1}{1}; + catch + majorVersion = 4; + end end - getTrackerTime(me); - getTimeOffset(me); + %getTrackerTime(me); + %getTimeOffset(me); me.salutation('Initialise Method', sprintf('Running on a %s @ %2.5g (time offset: %2.5g)', me.version, me.trackerTime,me.currentOffset),true); % try to open file to record data to if me.isConnected if me.recordData err = Eyelink('Openfile', me.tempFile); if err ~= 0 - warning('eyelinkManager Cannot setup Eyelink data file, aborting data recording'); me.isRecording = false; + error('eyelinkManager Cannot setup Eyelink data file, aborting data recording'); %#ok else Eyelink('Command', ['add_file_preamble_text ''Recorded by:' me.fullName ' tracker'''],true); me.isRecording = true; end end + Eyelink('Command', 'screen_pixel_coords = %ld %ld %ld %ld',me.screen.winRect(1),me.screen.winRect(2),me.screen.winRect(3)-1,me.screen.winRect(4)-1); Eyelink('Message', 'DISPLAY_COORDS %ld %ld %ld %ld',me.screen.winRect(1),me.screen.winRect(2),me.screen.winRect(3)-1,me.screen.winRect(4)-1); Eyelink('Message', 'FRAMERATE %ld',round(me.screen.screenVals.fps)); Eyelink('Message', 'DISPLAY_PPD %ld', round(me.ppd_)); Eyelink('Message', 'DISPLAY_DISTANCE %ld', round(me.screen.distance)); Eyelink('Message', 'DISPLAY_PIXELSPERCM %ld', round(me.screen.pixelsPerCm)); - Eyelink('Command', 'link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON'); - Eyelink('Command', 'link_sample_data = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS'); - Eyelink('Command', 'file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON'); - Eyelink('Command', 'file_sample_data = LEFT,RIGHT,GAZE,HREF,AREA,GAZERES,STATUS'); - %Eyelink('Command', 'use_ellipse_fitter = no'); + Eyelink('Command', 'link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,BUTTON,FIXUPDATE,INPUT'); + Eyelink('Command', 'file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT'); + if majorVersion > 3 % Check tracker version and include 'HTARGET' to save head target sticker data for supported eye trackers + Eyelink('Command', 'file_sample_data = LEFT,RIGHT,GAZE,HREF,RAW,AREA,HTARGET,GAZERES,BUTTON,STATUS,INPUT'); + Eyelink('Command', 'link_sample_data = LEFT,RIGHT,GAZE,GAZERES,AREA,HTARGET,STATUS,INPUT'); + else + Eyelink('Command', 'file_sample_data = LEFT,RIGHT,GAZE,HREF,RAW,AREA,GAZERES,BUTTON,STATUS,INPUT'); + Eyelink('Command', 'link_sample_data = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS,INPUT'); + end Eyelink('Command', 'sample_rate = %d',me.sampleRate); + Eyelink('Command', 'clear_screen 1') end - end + end - % =================================================================== - %> @brief - %> % =================================================================== function updateDefaults(me) - EyelinkUpdateDefaults(me.defaults); - end - - % =================================================================== - %> @brief reset the fixation counters ready for a new trial - %> - %> @param removeHistory remove the history of recent eye position? - % =================================================================== - function resetFixation(me,removeHistory) - if ~exist('removeHistory','var');removeHistory=false;end - me.fixStartTime = 0; - me.fixLength = 0; - me.fixInitStartTime = 0; - me.fixInitLength = 0; - me.fixTotal = 0; - me.fixWindow = 0; - me.fixN = 0; - me.fixSelection = 0; - if removeHistory - resetFixationHistory(me); - end - me.isFix = false; - me.isBlink = false; - me.isExclusion = false; - me.isInitFail = false; - if me.verbose - fprintf('-+-+-> eyelinkManager:reset fixation: %i %i %i\n',me.fixLength,me.fixTotal,me.fixN); - end - end - - % =================================================================== - %> @brief reset the fixation counters ready for a new trial - %> - % =================================================================== - function resetExclusionZones(me) - me.exclusionZone = []; - end - - % =================================================================== - %> @brief reset the fixation counters ready for a new trial - %> - % =================================================================== - function resetFixationTime(me) - me.fixStartTime = 0; - me.fixLength = 0; - end - - % =================================================================== - %> @brief reset the fixation history: xAll yAll pupilAll + %> @fn updateDefaults + %> @brief whenever you change me.defaults you should run this to update + %> the eyelink toolbox %> % =================================================================== - function resetFixationHistory(me) - me.xAll = []; - me.yAll = []; - me.pupilAll = []; - end - - % =================================================================== - %> @brief reset the fixation offset to 0 - %> - % =================================================================== - function resetOffset(me) - me.offset.X = 0; - me.offset.Y = 0; - end - - % =================================================================== - %> @brief reset the fixation offset to 0 - %> - % =================================================================== - function resetFixInit(me) - me.fixInit.X = []; - me.fixInit.Y = []; + EyelinkUpdateDefaults(me.defaults); end % =================================================================== - %> @brief check the connection with the eyelink + function connected = checkConnection(me) + %> @fn checkConnection + %> @brief check the connection with the eyelink is valid %> % =================================================================== - function connected = checkConnection(me) isc = Eyelink('IsConnected'); if isc == 1 me.isConnected = true; @@ -351,34 +243,54 @@ classdef eyetrackerManager < optickaCore end % =================================================================== - %> @brief sets up the calibration and validation + function trackerSetup(me) + %> @fn trackerSetup + %> @brief runs the calibration and validation %> % =================================================================== - function trackerSetup(me) - if ~me.isConnected; return; end + [rM, aM] = optickaCore.initialiseGlobals(); + if me.calibration.enableCallbacks && contains(me.calibration.callback, 'eyelinkCustomCallback') + try open(aM); end + %Snd('Open', aM.aHandle, 1); + end + + if isa(me.screen,'screenManager') && ~me.screen.isOpen; open(me.screen); end + + if ~me.isConnected || me.isOff; return; end oldrk = RestrictKeysForKbCheck([]); %just in case someone has restricted keys + ListenChar(0); fprintf('\n===>>> CALIBRATING EYELINK... <<<===\n'); Eyelink('Verbosity',me.verbosityLevel); - if ~isempty(me.calibrationProportion) && length(me.calibrationProportion)==2 - Eyelink('Command','calibration_area_proportion = %s', num2str(me.calibrationProportion)); - Eyelink('Command','validation_area_proportion = %s', num2str(me.calibrationProportion)); + if ~isempty(me.calibration.proportion) && length(me.calibration.proportion)==2 + Eyelink('Command','calibration_area_proportion = %s', num2str(me.calibration.proportion)); + Eyelink('Command','validation_area_proportion = %s', num2str(me.calibration.proportion)); % see https://www.sr-support.com/forum-37-page-2.html %Eyelink('Command','calibration_corner_scaling = %s', num2str([me.calibrationProportion(1)-0.1 me.calibrationProportion(2)-0.1])-); %Eyelink('Command','validation_corner_scaling = %s', num2str([me.calibrationProportion(1)-0.1 me.calibrationProportion(2)-0.1])); end - Eyelink('Command','horizontal_target_y = %i',me.screen.winRect(4)/2); - Eyelink('Command','calibration_type = %s', me.calibrationStyle); - Eyelink('Command','normal_click_dcorr = ON'); - Eyelink('Command', 'driftcorrect_cr_disable = OFF'); - Eyelink('Command', 'drift_correction_rpt_error = 10.0'); - Eyelink('Command', 'online_dcorr_maxangle = 15.0'); - Eyelink('Command','randomize_calibration_order = NO'); - Eyelink('Command','randomize_validation_order = NO'); - Eyelink('Command','cal_repeat_first_target = YES'); - Eyelink('Command','val_repeat_first_target = YES'); - Eyelink('Command','validation_online_fixup = NO'); - Eyelink('Command','generate_default_targets = YES'); - if me.remoteCalibration + Eyelink('Command','screen_pixel_coords = %ld %ld %ld %ld',me.screen.winRect(1),me.screen.winRect(2),me.screen.winRect(3)-1,me.screen.winRect(4)-1); + if me.calibration.eyeUsed == me.defaults.LEFT_EYE + Eyelink('Command','active_eye = LEFT'); + elseif me.calibration.eyeUsed == me.defaults.RIGHT_EYE + Eyelink('Command','active_eye = RIGHT'); + else + Eyelink('Command','active_eye = LEFT'); + end + %Eyelink('Command','horizontal_target_y = %i',round(me.screen.winRect(4)/2)); + Eyelink('Command','calibration_type = %s', me.calibration.style); + Eyelink('Command','enable_automatic_calibration = YES'); + Eyelink('Command','automatic_calibration_pacing = %s',num2str(round(me.calibration.paceDuration))); + %Eyelink('Command','normal_click_dcorr = ON'); + %Eyelink('Command','driftcorrect_cr_disable = OFF'); + %Eyelink('Command','drift_correction_rpt_error = 10.0'); + %Eyelink('Command','online_dcorr_maxangle = 15.0'); + %Eyelink('Command','randomize_calibration_order = NO'); + %Eyelink('Command','randomize_validation_order = NO'); + %Eyelink('Command','cal_repeat_first_target = YES'); + %Eyelink('Command','val_repeat_first_target = YES'); + %Eyelink('Command','validation_online_fixup = NO'); + %Eyelink('Command','generate_default_targets = YES'); + if me.calibration.manual Eyelink('Command','remote_cal_enable = 1'); Eyelink('Command','key_function 1 ''remote_cal_target 1'''); Eyelink('Command','key_function 2 ''remote_cal_target 2'''); @@ -407,10 +319,10 @@ classdef eyetrackerManager < optickaCore end % =================================================================== + function startRecording(me,~) %> @brief wrapper for StartRecording %> % =================================================================== - function startRecording(me,~) if me.isConnected Eyelink('StartRecording'); checkEye(me); @@ -418,33 +330,34 @@ classdef eyetrackerManager < optickaCore end % =================================================================== + function stopRecording(me,~) %> @brief wrapper for StopRecording %> % =================================================================== - function stopRecording(me,~) if me.isConnected Eyelink('StopRecording'); end end % =================================================================== + function setOffline(me) %> @brief set into offline / idle mode %> % =================================================================== - function setOffline(me) if me.isConnected Eyelink('Command', 'set_idle_mode'); end end % =================================================================== + function success = driftCorrection(me) %> @brief wrapper for EyelinkDoDriftCorrection %> % =================================================================== - function success = driftCorrection(me) + oldrk = RestrictKeysForKbCheck([]); %just in case someone has restricted keys success = false; - x=me.toPixels(me.fixation.X,'x'); %#ok<*PROPLC> - y=me.toPixels(me.fixation.Y,'y'); + x=me.toPixels(me.fixation.X(1),'x'); %#ok<*PROPLC> + y=me.toPixels(me.fixation.Y(1),'y'); if me.isConnected resetOffset(me); Eyelink('Command', 'driftcorrect_cr_disable = OFF'); @@ -457,7 +370,7 @@ classdef eyetrackerManager < optickaCore success = EyelinkDoDriftCorrection(me.defaults, round(x), round(y), 1, 1); [result,out] = Eyelink('CalMessage'); fprintf('DriftCorrect @ %.2f/%.2f px (%.2f/%.2f deg): result = %i msg = %s\n',... - x,y, me.fixation.X, me.fixation.Y,result,out); + x,y, me.fixation.X(1), me.fixation.Y(1),result,out); if success ~= 0 me.salutation('Drift Correct','FAILED',true); end @@ -467,73 +380,10 @@ classdef eyetrackerManager < optickaCore me.salutation('Drift Correct',sprintf('Results: %f %i %s\n',res,result,out),true); end end + RestrictKeysForKbCheck(oldrk); WaitSecs('YieldSecs',1); end - - % =================================================================== - function success = driftOffset(me) - %> @fn driftOffset - %> @brief wrapper for EyelinkDoDriftCorrection - %> - % =================================================================== - success = false; - escapeKey = KbName('ESCAPE'); - stopkey = KbName('Q'); - nextKey = KbName('SPACE'); - calibkey = KbName('C'); - driftkey = KbName('D'); - if me.isConnected || me.isDummy - x = me.toPixels(me.fixation.X,'x'); %#ok<*PROPLC> - y = me.toPixels(me.fixation.Y,'y'); - Screen('Flip',me.screen.win); - ifi = me.screen.screenVals.ifi; - breakLoop = false; i = 1; flash = true; - correct = false; - xs = []; - ys = []; - while ~breakLoop - getSample(me); - xs(i) = me.x; - ys(i) = me.y; - if mod(i,10) == 0 - flash = ~flash; - end - Screen('DrawText',me.screen.win,'Drift Correction...',10,10,[0.4 0.4 0.4]); - if flash - Screen('gluDisk',me.screen.win,[1 0 1 0.75],x,y,10); - Screen('gluDisk',me.screen.win,[1 1 1 1],x,y,4); - else - Screen('gluDisk',me.screen.win,[1 1 0 0.75],x,y,10); - Screen('gluDisk',me.screen.win,[0 0 0 1],x,y,4); - end - me.screen.drawCross(0.6,[0 0 0],x,y,0.1,false); - Screen('Flip',me.screen.win); - [~, ~, keyCode] = KbCheck(-1); - if keyCode(stopkey) || keyCode(escapeKey); breakLoop = true; break; end - if keyCode(nextKey); correct = true; break; end - if keyCode(calibkey); trackerSetup(me); break; end - if keyCode(driftkey); driftCorrection(me); break; end - i = i + 1; - end - if correct && length(xs) > 5 && length(ys) > 5 - success = true; - me.offset.X = median(xs) - me.fixation.X; - me.offset.Y = median(ys) - me.fixation.Y; - t = sprintf('Offset: X = %.2f Y = %.2f\n',me.offset.X,me.offset.Y); - me.salutation('Drift [SELF]Correct',t,true); - Screen('DrawText',me.screen.win,t,10,10,[0.4 0.4 0.4]); - Screen('Flip',me.screen.win); - else - me.offset.X = 0; - me.offset.Y = 0; - t = sprintf('Offset: X = %.2f Y = %.2f\n',me.offset.X,me.offset.Y); - me.salutation('REMOVE Drift [SELF]Offset',t,true); - Screen('DrawText',me.screen.win,'Reset Drift Offset...',10,10,[0.4 0.4 0.4]); - Screen('Flip',me.screen.win); - end - WaitSecs('YieldSecs',1); - end - end + % =================================================================== function error = checkRecording(me) @@ -556,389 +406,45 @@ classdef eyetrackerManager < optickaCore %> % =================================================================== if me.isConnected && Eyelink('NewFloatSampleAvailable') > 0 - me.currentSample = Eyelink('NewestFloatSample');% get the sample in the form of an event structure - if ~isempty(me.currentSample) && isstruct(me.currentSample) - if me.currentSample.gx(me.eyeUsed+1) == me.MISSING_DATA ... - && me.currentSample.gy(me.eyeUsed+1) == me.MISSING_DATA ... - && me.currentSample.pa(me.eyeUsed+1) == 0 ... - && me.ignoreBlinks - %me.x = toPixels(me,me.fixation.X,'x'); - %me.y = toPixels(me,me.fixation.Y,'y'); - me.pupil = 0; + sample = Eyelink('NewestFloatSample');% get the sample in the form of an event structure + if ~isempty(sample) && isstruct(sample) + sample.time = sample.time / 1e3; + x = sample.gx(me.eyeUsed+1); + y = sample.gy(me.eyeUsed+1); + p = sample.pa(me.eyeUsed+1); + if x == me.MISSING_DATA || y == me.MISSING_DATA + me.x = NaN; + me.y = NaN; + sample.valid = false; + me.pupil = NaN; + me.xAll = [me.xAll me.x]; + me.yAll = [me.yAll me.y]; + me.pupilAll = [me.pupilAll me.pupil]; me.isBlink = true; else - me.x = me.currentSample.gx(me.eyeUsed+1); % +1 as we're accessing MATLAB array - me.y = me.currentSample.gy(me.eyeUsed+1); - me.pupil = me.currentSample.pa(me.eyeUsed+1); + sample.valid = true; + xy = toDegrees(me, [x y]); + me.x = xy(1); me.y = xy(2); + me.pupil = p; me.xAll = [me.xAll me.x]; me.yAll = [me.yAll me.y]; me.pupilAll = [me.pupilAll me.pupil]; me.isBlink = false; end - %if me.verbose;fprintf('\n',me.x,me.y,me.pupil,me.isBlink);end - end - elseif me.isDummy %lets use a mouse to simulate the eye signal - if ~isempty(me.win) - [me.x, me.y] = GetMouse(me.win); - elseif ~isempty(me.screen) && ~isempty(me.screen.screen) - [me.x, me.y] = GetMouse(me.screen.screen); - else - [me.x, me.y] = GetMouse(); - end - me.pupil = 800 + randi(20); - me.currentSample.gx = me.x; - me.currentSample.gy = me.y; - me.currentSample.pa = me.pupil; - me.currentSample.time = GetSecs * 1000; - me.xAll = [me.xAll me.x]; - me.yAll = [me.yAll me.y]; - me.pupilAll = [me.pupilAll me.pupil]; - %if me.verbose;fprintf('\n',me.x,me.y,me.pupil,me.currentSample.time);end - end - sample = me.currentSample; - end - - - % =================================================================== - function updateFixationValues(me,x,y,inittime,fixtime,radius,strict) - %> @fn updateFixationValues(me,x,y,inittime,fixtime,radius,strict) - %> - %> Sinlge method to update the fixation parameters. See property - %> descriptions for full details. You can pass empty values if you only - %> need to update one parameter, e.g. me.updateFixationValues([],[],1); - %> - %> @param x X position - %> @param y Y position - %> @param inittime time to initiate fixation - %> @param fixtime time to maintain fixation - %> @paran radius radius of fixation window - %> @param strict allow or disallow re-entering the fixation window - % =================================================================== - resetFixation(me); - if nargin > 1 && ~isempty(x) - if isinf(x) - me.fixation.X = me.screen.screenXOffset; - else - me.fixation.X = x; - end - end - if nargin > 2 && ~isempty(y) - if isinf(y) - me.fixation.Y = me.screen.screenYOffset; - else - me.fixation.Y = y; - end - end - if nargin > 3 && ~isempty(inittime) - if iscell(inittime) - lst = {'initTime','time','radius','strict'}; - for i = 1:length(inittime) - if contains(lst{i},'time','ignorecase',true) && length(inittime{i}) == 2 - inittime{i} = randi(inittime{i}.*1000)/1000; - end - me.fixation.(lst{i}) = inittime{i}(1); - end - elseif length(inittime) == 2 - me.fixation.initTime = randi(inittime.*1000)/1000; - elseif length(inittime) == 1 - me.fixation.initTime = inittime; - end - end - if nargin > 4 && ~isempty(fixtime) - if length(fixtime) == 2 - me.fixation.time = randi(fixtime.*1000)/1000; - elseif length(fixtime) == 1 - me.fixation.time = fixtime; - end - end - if nargin > 5 && ~isempty(radius); me.fixation.radius = radius; end - if nargin > 6 && ~isempty(strict); me.fixation.strict = strict; end - if me.verbose - fprintf('-+-+-> eyelinkManager:updateFixationValues: X=%g | Y=%g | IT=%s | FT=%s | R=%g | Strict=%i\n', ... - me.fixation.X, me.fixation.Y, num2str(me.fixation.initTime,'%.2f '), num2str(me.fixation.time,'%.2f '), ... - me.fixation.radius,me.fixation.strict); - end - end - - % =================================================================== - %> @brief Sinlge method to update the exclusion zones - %> - %> @param x x position in degrees - %> @param y y position in degrees - %> @param radius the radius of the exclusion zone - % =================================================================== - function updateExclusionZones(me,x,y,radius) - resetExclusionZones(me); - if exist('x','var') && exist('y','var') && ~isempty(x) && ~isempty(y) - if ~exist('radius','var'); radius = 5; end - for i = 1:length(x) - me.exclusionZone(i,:) = [x(i)-radius x(i)+radius y(i)-radius y(i)+radius]; - end - end - end - - % =================================================================== - %> @brief isFixated tests for fixation and updates the fixLength time - %> - %> @return fixated boolean if we are fixated - %> @return fixtime boolean if we're fixed for fixation time - %> @return searching boolean for if we are still searching for fixation - % =================================================================== - function [fixated, fixtime, searching, window, exclusion, fixinit] = isFixated(me) - fixated = false; fixtime = false; searching = true; - exclusion = false; window = []; fixinit = false; - - if isempty(me.currentSample); return; end - - if me.isExclusion || me.isInitFail - exclusion = me.isExclusion; fixinit = me.isInitFail; searching = false; - return; % we previously matched either rule, now cannot pass fixation until a reset. - end - if me.fixInitStartTime == 0 - me.fixInitStartTime = me.currentSample.time; - me.fixTotal = 0; - me.fixInitLength = 0; - end - - % ---- add any offsets for following calculations - x = me.x - me.offset.X; y = me.y - me.offset.Y; - - % ---- test for exclusion zones first - if ~isempty(me.exclusionZone) - for i = 1:size(me.exclusionZone,1) - if (x >= me.exclusionZone(i,1) && x <= me.exclusionZone(i,2)) && ... - (me.y >= me.exclusionZone(i,3) && me.y <= me.exclusionZone(i,4)) - searching = false; exclusion = true; - me.isExclusion = true; me.isFix = false; - return; - end - end - end - - % ---- test for fix initiation start window - ft = (me.currentSample.time - me.fixInitStartTime) / 1e3; - if ~isempty(me.fixInit.X) && ft <= me.fixInit.time - r = sqrt((x - me.fixInit.X).^2 + (y - me.fixInit.Y).^2); - window = find(r < me.fixInit.radius); - if ~any(window) - searching = false; fixinit = true; - me.isInitFail = true; me.isFix = false; - fprintf('-+-+-> eyelinkManager: Eye left fix init window @ %.3f secs!\n',ft); - return; - end - end - % now test if we are still searching or in fixation window, if - % radius is single value, assume circular, otherwise assume - % rectangular - window = 0; - if length(me.fixation.radius) == 1 % circular test - r = sqrt((x - me.fixation.X).^2 + (y - me.fixation.Y).^2); %fprintf('x: %g-%g y: %g-%g r: %g-%g\n',x, me.fixation.X, me.y, me.fixation.Y,r,me.fixation.radius); - window = find(r < me.fixation.radius); - else % x y rectangular window test - for i = 1:length(me.fixation.X) - if (x >= (me.fixation.X - me.fixation.radius(1))) && (x <= (me.fixation.X + me.fixation.radius(1))) ... - && (me.y >= (me.fixation.Y - me.fixation.radius(2))) && (me.y <= (me.fixation.Y + me.fixation.radius(2))) - window = i;break; - end - end - end - me.fixWindow = window; - me.fixTotal = (me.currentSample.time - me.fixInitStartTime) / 1e3; - if any(window) % inside fixation window - if me.fixN == 0 - me.fixN = 1; - me.fixSelection = window(1); - end - if me.fixSelection == window(1) - if me.fixStartTime == 0 - me.fixStartTime = me.currentSample.time; - end - fixated = true; searching = false; - me.fixLength = (me.currentSample.time - me.fixStartTime) / 1e3; - if me.fixLength >= me.fixation.time - fixtime = true; - end - else - fixated = false; fixtime = false; searching = false; - end - me.isFix = fixated; me.fixInitLength = 0; - else % not inside the fixation window - if me.fixN == 1 - me.fixN = -100; - end - me.fixInitLength = (me.currentSample.time - me.fixInitStartTime) / 1e3; - if me.fixInitLength < me.fixation.initTime - searching = true; - else - searching = false; - end - me.isFix = false; me.fixLength = 0; me.fixStartTime = 0; - end - end - - % =================================================================== - %> @brief testExclusion - %> - %> - % =================================================================== - function out = testExclusion(me) - out = false; - if (me.isConnected || me.isDummy) && ~isempty(me.currentSample) && ~isempty(me.exclusionZone) - eZ = me.exclusionZone; x = me.x - me.offset.X; y = me.y - me.offset.Y; - for i = 1:size(eZ,1) - if (x >= eZ(i,1) && x <= eZ(i,2)) && (y >= eZ(i,3) && y <= eZ(i,4)) - out = true; - return - end - end - end - end - - % =================================================================== - %> @brief Checks for both searching and then maintaining fix. Input is - %> 2 strings, either one is returned depending on success or - %> failure, 'searching' may also be returned meaning the fixation - %> window hasn't been entered yet, and 'fixing' means the fixation - %> time is not yet met... - %> - %> @param yesString if this function succeeds return this string - %> @param noString if this function fails return this string - %> @return out the output string which is 'searching' if fixation has - %> been initiated, 'fixing' if the fixation window was entered - %> but not for the requisite fixation time, 'EXCLUDED!' if an exclusion - %> zone was entered or the yesString or noString. - % =================================================================== - function [out, window, exclusion, initfail] = testSearchHoldFixation(me, yesString, noString) - [fix, fixtime, searching, window, exclusion, initfail] = me.isFixated(); - if exclusion - out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testSearchHoldFixation EXCLUSION ZONE ENTERED time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail); end - return; - end - if initfail - out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testSearchHoldFixation FIX INIT TIME FAIL time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail); end - return - end - if searching - if (me.fixation.strict==true && (me.fixN == 0)) || me.fixation.strict==false - out = 'searching'; - else - out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testSearchHoldFixation STRICT SEARCH FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end - end - return - elseif fix - if (me.fixation.strict==true && ~(me.fixN == -100)) || me.fixation.strict==false - if fixtime - out = yesString; - if me.verbose; fprintf('-+-+-> Eyelink:testSearchHoldFixation FIXATION SUCCESSFUL!: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end - else - out = 'fixing'; - end - else - out = noString; - if me.verbose;fprintf('-+-+-> Eyelink:testSearchHoldFixation FIX FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end - end - return - elseif searching == false - out = noString; - if me.verbose;fprintf('-+-+-> Eyelink:testSearchHoldFixation SEARCH FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end - else - out = ''; - end - end - - % =================================================================== - %> @brief Checks if we're still within fix window. Input is - %> 2 strings, either one is returned depending on success or - %> failure, 'fixing' means the fixation time is not yet met... - %> - %> @param yesString if this function succeeds return this string - %> @param noString if this function fails return this string - %> @return out the output string which is 'fixing' if the fixation window was entered - %> but not for the requisite fixation time, or the yes or no string. - % =================================================================== - function [out, window, exclusion, initfail] = testHoldFixation(me, yesString, noString) - [fix, fixtime, searching, window, exclusion, initfail] = me.isFixated(); - if exclusion - out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testHoldFixation EXCLUSION ZONE ENTERED time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail); end - return; - end - if initfail - out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testHoldFixation FIX INIT TIME FAIL time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail); end - return - end - if fix - if (me.fixation.strict==true && ~(me.fixN == -100)) || me.fixation.strict==false - if fixtime - out = yesString; - if me.verbose; fprintf('-+-+-> Eyelink:testHoldFixation FIXATION SUCCESSFUL!: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end - else - out = 'fixing'; - end - else - out = noString; - if me.verbose;fprintf('-+-+-> Eyelink:testHoldFixation FIX FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end + if me.debug;fprintf('\n',me.x,me.y,me.pupil,me.isBlink);end end - return - else - out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testHoldFixation FIX FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... - out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end - return - end - end - - % =================================================================== - %> @brief testWithinFixationWindow simply tests we are in fixwindow - %> - %> - % =================================================================== - function out = testWithinFixationWindow(me, yesString, noString) - if isFixated(me) - out = yesString; - else - out = noString; - end - end - - % =================================================================== - %> @brief Checks if we've maintained fixation for correct time, if - %> true return yesString, if not return noString. This allows an - %> external code to quickly select a string based on this. Use - % testHoldFixation() if you want to maintain fixation with a window - % for a certain time... - %> - % =================================================================== - function out = testFixationTime(me, yesString, noString) - [fix,fixtime] = isFixated(me); - if fix && fixtime - out = yesString; %me.salutation(sprintf('Fixation Time: %g',me.fixLength),'TESTFIXTIME'); else - out = noString; + sample = getMouseSample(me); end + me.currentSample = sample; end - % =================================================================== + function eyeUsed = checkEye(me) %> @brief checks which eye is available, force left eye if %> binocular is enabled %> % =================================================================== - function eyeUsed = checkEye(me) if me.isConnected me.eyeUsed = Eyelink('EyeAvailable'); % get eye that's tracked if me.eyeUsed == me.defaults.BINOCULAR % if both eyes are tracked @@ -952,36 +458,11 @@ classdef eyetrackerManager < optickaCore end % =================================================================== - %> @brief draw the current eye position on the PTB display - %> - % =================================================================== - function drawEyePosition(me) - if (me.isDummy || me.isConnected) && isa(me.screen,'screenManager') && me.screen.isOpen && ~isempty(me.x) && ~isempty(me.y) - xy = toPixels(me,[me.x-me.offset.X me.y-me.offset.Y]); - if me.isFix - if me.fixLength > me.fixation.time && ~me.isBlink - Screen('DrawDots', me.win, xy, 6, [0 1 0.25 1], [], 3); - elseif ~me.isBlink - Screen('DrawDots', me.win, xy, 6, [0.75 0 0.75 1], [], 3); - else - Screen('DrawDots', me.win, xy, 6, [0.75 0 0 1], [], 3); - end - else - if ~me.isBlink - Screen('DrawDots', me.win, xy, 6, [0.75 0.5 0 1], [], 3); - else - Screen('DrawDots', me.win, xy, 6, [0.75 0 0 1], [], 3); - end - end - end - end - - % =================================================================== + function statusMessage(me,message) %> @brief displays status message on tracker, only sets it if %> message is not the previous message, so loop safe. %> % =================================================================== - function statusMessage(me,message) if ~strcmpi(message,me.previousMessage) && me.isConnected me.previousMessage = message; Eyelink('Command',['record_status_message ''' message '''']); @@ -990,11 +471,11 @@ classdef eyetrackerManager < optickaCore end % =================================================================== + function trackerMessage(me, message, varargin) %> @brief send message to store in EDF data %> %> % =================================================================== - function trackerMessage(me, message, varargin) if me.isConnected Eyelink('Message', message ); if me.verbose; fprintf('-+-+-> EDF Message: %s\n',message);end @@ -1002,42 +483,50 @@ classdef eyetrackerManager < optickaCore end % =================================================================== + function close(me) %> @brief close the eyelink and cleanup, send EDF file if recording %> is enabled %> % =================================================================== - function close(me) try me.isConnected = false; %me.isDummy = false; me.eyeUsed = -1; me.screen = []; - trackerClearScreen(me); - if me.isRecording == true && ~isempty(me.saveFile) + try trackerClearScreen(me); end + if me.isRecording == true Eyelink('StopRecording'); Eyelink('CloseFile'); + oldp = pwd; try - me.salutation('Close Method',sprintf('Receiving data file %s', me.tempFile),true); + if isfield(me.paths,'alfPath') && exist(me.paths.alfPath,'dir') + cd(me.paths.alfPath); + else + cd(me.paths.savedData); + end + me.salutation('Close Method',sprintf('Receiving data file %s.edf', me.tempFile),true); status=Eyelink('ReceiveFile'); if status > 0 me.salutation('Close Method',sprintf('ReceiveFile status %d', status)); end - if exist(me.tempFile, 'file') - me.salutation('Close Method',sprintf('Data file ''%s'' can be found in ''%s''', me.tempFile, strrep(pwd,'\','/')),true); - status = movefile(me.tempFile, me.saveFile,'f'); + if exist([me.tempFile '.edf'], 'file') + me.salutation('Close Method',sprintf('Data file ''%s.edf'' can be found in ''%s''', me.tempFile, strrep(pwd,'\','/')),true); + if ~contains(me.saveFile,'.edf'); me.saveFile = [me.saveFile '.edf']; end + status = copyfile([me.tempFile '.edf'], me.saveFile, 'f'); if status == 1 me.salutation('Close Method',sprintf('Data file copied to ''%s''', me.saveFile),true); - trackerDrawText(me,sprintf('Data file copied to ''%s''', me.saveFile)); end end catch ME me.salutation('Close Method',sprintf('Problem receiving data file ''%s''', me.tempFile),true); + warning('eyelinkManager.close(): EYELINK DATA NOT RECEIVED!') disp(ME.message); end + cd(oldp); end catch ME me.salutation('Close Method','Couldn''t stop recording, forcing shutdown...',true) - trackerClearScreen(me); + try trackerClearScreen(me); end Eyelink('Shutdown'); me.error = ME; me.salutation(ME.message); @@ -1050,86 +539,85 @@ classdef eyetrackerManager < optickaCore end % =================================================================== + function trackerClearScreen(me) %> @brief draw the background colour %> % =================================================================== - function trackerClearScreen(me) - if ~me.isConnected; return; end - Eyelink('Command', 'clear_screen 0'); + if ~me.isConnected || me.isOff; return; end + Eyelink('Command', 'clear_screen 1'); end % =================================================================== + function trackerDrawStatus(me, comment, ts, dontClear) %> @brief draw general status %> % =================================================================== - function trackerDrawStatus(me, comment, stimPos) - if ~me.isConnected; return; end + if ~me.isConnected || me.isOff; return; end if ~exist('comment','var'); comment=''; end - if ~exist('stimPos','var'); stimPos = struct; end - trackerClearScreen(me); - trackerDrawExclusion(me); + if ~exist('ts','var'); ts = []; end + if ~exist('dontClear','var'); dontClear = false; end + if dontClear == false; trackerClearScreen(me); end trackerDrawFixation(me); - trackerDrawStimuli(me,stimPos); - trackerDrawEyePositions(me); + if ~isempty(me.exclusionZone);trackerDrawExclusion(me);end + if ~isempty(ts);trackerDrawStimuli(me, ts);end if ~isempty(comment);trackerDrawText(me, comment);end end % =================================================================== + function trackerDrawStimuli(me, ts, dontClear, convertToPixels) %> @brief draw the stimuli boxes on the tracker display %> % =================================================================== - function trackerDrawStimuli(me, ts, clearScreen, convertToPixels) - if me.isConnected - if exist('ts','var') && isstruct(ts) - me.stimulusPositions = ts; - end - if ~exist('clearScreen','var') - clearScreen = false; - end - if ~exist('convertToPixels','var') - convertToPixels = false; - end - if clearScreen; Eyelink('Command', 'clear_screen 0'); end - for i = 1:length(me.stimulusPositions) - x = me.stimulusPositions(i).x; - y = me.stimulusPositions(i).y; - size = me.stimulusPositions(i).size; - if isempty(size); size = 1; end - rect = [0 0 size size]; - rect = CenterRectOnPoint(rect, x, y); - if convertToPixels; rect = toPixels(me, rect,'rect'); end - rect = round(rect); - if me.stimulusPositions(i).selected == true - Eyelink('Command', 'draw_box %d %d %d %d 10', rect(1), rect(2), rect(3), rect(4)); - else - Eyelink('Command', 'draw_box %d %d %d %d 11', rect(1), rect(2), rect(3), rect(4)); - end - end + if ~me.isConnected || me.isOff; return; end + if exist('ts','var') && isstruct(ts) && isfield(ts,'x') + me.stimulusPositions = ts; + else + return end + if ~exist('dontClear','var'); dontClear = true; end + if ~exist('convertToPixels','var'); convertToPixels = true; end + if dontClear==false; Eyelink('Command', 'clear_screen 0'); end + for i = 1:length(me.stimulusPositions) + x = me.stimulusPositions(i).x; + y = me.stimulusPositions(i).y; + size = me.stimulusPositions(i).size; + if isempty(size); size = 1; end + rect = [0 0 size size]; + rect = CenterRectOnPoint(rect, x, y); + if convertToPixels; rect = round(toPixels(me, rect,'rect')); end + if me.stimulusPositions(i).selected == true + Eyelink('Command', 'draw_box %d %d %d %d 15', rect(1), rect(2), rect(3), rect(4)); + else + Eyelink('Command', 'draw_box %d %d %d %d 13', rect(1), rect(2), rect(3), rect(4)); + end + end end % =================================================================== + function trackerDrawFixation(me) %> @brief draw the fixation box on the tracker display %> % =================================================================== - function trackerDrawFixation(me) - if me.isConnected - size = me.fixation.radius * 2; - rect = [0 0 size size]; - for i = 1:length(me.fixation.X) - nrect = CenterRectOnPoint(rect, me.fixation.X(i), me.fixation.Y(i)); - nrect = round(toPixels(me, nrect, 'rect')); - Eyelink('Command', 'draw_filled_box %d %d %d %d 10', nrect(1), nrect(2), nrect(3), nrect(4)); - end + if ~me.isConnected || me.isOff; return; end + if isscalar(me.fixation.radius) + rect = [0 0 me.fixation.radius*2 me.fixation.radius*2]; + else + rect = [0 0 me.fixation.radius(1)*2 me.fixation.radius(2)*2]; + end + for i = 1:length(me.fixation.X) + nrect = CenterRectOnPoint(rect, me.fixation.X(i), me.fixation.Y(i)); + nrect = round(toPixels(me, nrect, 'rect')); + Eyelink('Command', 'draw_filled_box %d %d %d %d 10', nrect(1), nrect(2), nrect(3), nrect(4)); end end % =================================================================== + function trackerDrawExclusion(me) %> @brief draw the fixation box on the tracker display %> % =================================================================== - function trackerDrawExclusion(me) - if me.isConnected && ~isempty(me.exclusionZone) && size(me.exclusionZone,2)==4 + if ~me.isConnected || me.isOff; return; end + if ~isempty(me.exclusionZone) && size(me.exclusionZone,2)==4 for i = 1:size(me.exclusionZone,1) rect = round(toPixels(me, me.exclusionZone(i,:))); % exclusion zone is [-degX +degX -degY +degY], but rect is left,top,right,bottom @@ -1139,20 +627,20 @@ classdef eyetrackerManager < optickaCore end % =================================================================== + function trackerDrawText(me,textIn) %> @brief draw the fixation box on the tracker display %> % =================================================================== - function trackerDrawText(me,textIn) - if me.isConnected - if exist('textIn','var') && ~isempty(textIn) - xDraw = toPixels(me, 0, 'x'); - yDraw = toPixels(me, 0, 'y'); - Eyelink('Command', 'draw_text %i %i %d %s', xDraw, yDraw, 3, textIn); - end + if ~me.isConnected || me.isOff; return; end + if exist('textIn','var') && ~isempty(textIn) + xDraw = toPixels(me, 0, 'x'); + yDraw = toPixels(me, 0, 'y'); + Eyelink('Command', 'draw_text %i %i %d %s', xDraw, yDraw, 3, textIn); end end % =================================================================== + function mode = currentMode(me) %> @brief check what mode the eyelink is in %> ##define IN_UNKNOWN_MODE 0 %> #define IN_IDLE_MODE 1 @@ -1165,7 +653,6 @@ classdef eyetrackerManager < optickaCore %> #define IN_PLAYBACK_MODE 256 %> #define LINK_TERMINATED_RESULT -100 % =================================================================== - function mode = currentMode(me) if me.isConnected mode = Eyelink('CurrentMode'); else @@ -1174,20 +661,19 @@ classdef eyetrackerManager < optickaCore end % =================================================================== + function syncTime(me) %> @brief Sync time message for EDF file %> % =================================================================== - function syncTime(me) - if me.isConnected - Eyelink('Message', 'SYNCTIME'); %zero-plot time for EDFVIEW - end + if ~me.isConnected || me.isOff; return; end + Eyelink('Message', 'SYNCTIME'); %zero-plot time for EDFVIEW end % =================================================================== + function offset = getTimeOffset(me) %> @brief Get offset between tracker and display computers %> % =================================================================== - function offset = getTimeOffset(me) if me.isConnected offset = Eyelink('TimeOffset'); me.currentOffset = offset; @@ -1197,10 +683,10 @@ classdef eyetrackerManager < optickaCore end % =================================================================== + function time = getTrackerTime(me) %> @brief Get offset between tracker and display computers %> % =================================================================== - function time = getTrackerTime(me) if me.isConnected time = Eyelink('TrackerTime'); me.trackerTime = time; @@ -1210,27 +696,13 @@ classdef eyetrackerManager < optickaCore end % =================================================================== - %> @brief automagically turn pixels to degrees - %> - % =================================================================== - function set.x(me,in) - me.x = toDegrees(me,in,'x'); %#ok<*MCSUP> - end - - % =================================================================== - %> @brief automagically turn pixels to degrees - %> - % =================================================================== - function set.y(me,in) - me.y = toDegrees(me,in,'y'); - end - - % =================================================================== + function runDemo(me, forcescreen) %> @brief runs a demo of the eyelink, tests this class %> % =================================================================== - function runDemo(me,forcescreen) - KbName('UnifyKeyNames') + [rM, aM] = optickaCore.initialiseGlobals(); + if ~aM.isSetup; try setup(aM); aM.beep(2000,0.1,0.1); end; end + PsychDefaultSetup(2); stopkey = KbName('Q'); nextKey = KbName('SPACE'); calibkey = KbName('C'); @@ -1241,81 +713,92 @@ classdef eyetrackerManager < optickaCore oldexc = me.exclusionZone; oldfixinit = me.fixInit; me.recordData = true; %lets save an EDF file - %set up a figure to plot eye position figure;plot(0,0,'ro');ax=gca;hold on;xlim([-20 20]);ylim([-20 20]);set(ax,'YDir','reverse'); title('eyelinkManager Demo');xlabel('X eye position (deg)');ylabel('Y eye position (deg)');grid on;grid minor;drawnow; + drawnow; % DEMO EXPERIMENT: try %open screen manager and dots stimulus - s = screenManager('debug',true,'pixelsPerCm',27,'distance',66); - if exist('forcescreen','var'); s.screen = forcescreen; end - s.backgroundColour = [0.5 0.5 0.5 0]; %s.windowed = [0 0 900 900]; + + if isempty(me.screen) || ~isa(me.screen,'screenManager') + s = screenManager('debug',true,'pixelsPerCm',36,'distance',57.3); + s.font.TextSize = 18; + s.font.TextBackgroundColor = [0.5 0.5 0.5 1]; + if exist('forcescreen','var'); s.screen = forcescreen; end + s.backgroundColour = [0.5 0.5 0.5 1]; %s.windowed = [0 0 900 900]; + else + s = me.screen; + end + if ~s.isOpen; open(s); end + o = dotsStimulus('size',me.fixation.radius(1)*2,'speed',2,'mask',true,'density',50); %test stimulus - open(s); % open our screen setup(o,s); % setup our stimulus with our screen object + % el=EyelinkInitDefaults(s.win); + % if ~EyelinkInit(me.isDummy,1) + % fprintf('Eyelink Init aborted.\n'); + % Eyelink('Shutdown'); + % close(s); + % return; + % end + % EyelinkDoTrackerSetup(el); + % EyelinkDoDriftCorrection(el); + initialise(me,s); % initialise eyelink with our screen - if ~me.isDummy && ~me.isConnected - reset(o); - close(s); - error('Could not connect to Eyelink or use Dummy mode...') - end - %ListenChar(-1); % capture the keyboard settings - setup(me); % setup + calibrate the eyelink - + ListenChar(-1); % capture the keyboard settings + trackerSetup(me); % setup + calibrate the eyelink + ListenChar(0); + % define our fixation widow and stimulus for first trial % x,y,inittime,fixtime,radius,strict me.updateFixationValues([0 -10],[0 -10],3,1,1,true); - o.sizeOut = me.fixation.radius(1)*2; - o.xPositionOut = me.fixation.X; - o.yPositionOut = me.fixation.Y; - ts.x = me.fixation.X; %ts is a simple structure that we can pass to eyelink to draw on its screen - ts.y = me.fixation.Y; - ts.size = o.sizeOut; - ts.selected = true; + o.sizeOut = me.fixation.radius(1) * 2; + o.xPositionOut = me.fixation.X(1); + o.yPositionOut = me.fixation.Y(1); + for i = 1:length(me.fixation.X) + ts(i).x = me.toPixels(me.fixation.X(i),'x'); + ts(i).y = me.toPixels(me.fixation.Y(i),'y'); + ts(i).size = o.sizeOut; + ts(i).selected = true; + end + update(o); % setup an exclusion zone where eye is not allowed - me.exclusionZone = [8 15 10 15]; + me.exclusionZone = [10 12 10 12]; exc = me.toPixels(me.exclusionZone); exc = [exc(1) exc(3) exc(2) exc(4)]; %psychrect=[left,top,right,bottom] setOffline(me); %Eyelink('Command', 'set_idle_mode'); - trackerClearScreen(me); % clear eyelink screen - trackerDrawFixation(me); % draw fixation window on tracker - trackerDrawStimuli(me,ts); % draw stimulus on tracker - Screen('TextSize', s.win, 18); - HideCursor(s.win); Priority(MaxPriority(s.win)); blockLoop = true; a = 1; - while blockLoop + while a < 6 && blockLoop + setOffline(me); % some general variables - trialLoop = true; b = 1; xst = []; yst = []; - correct = false; + trackerDrawStatus(me,'',ts); % !!! these messages define the trail start in the EDF for % offline analysis - edfMessage(me,'V_RT MESSAGE END_FIX END_RT'); - edfMessage(me,['TRIALID ' num2str(a)]); - % start the eyelink recording data for this trail - startRecording(me); + trackerMessage(me,'V_RT MESSAGE END_FIX END_RT'); + trackerMessage(me,['TRIALID ' num2str(a)]); + % start the eyelink recording data for this trial + setOffline(me);startRecording(me); % this draws the text to the tracker info box statusMessage(me,sprintf('DEMO Running Trial=%i X Pos = %g | Y Pos = %g | Radius = %g',a,me.fixation.X,me.fixation.Y,me.fixation.radius)); - WaitSecs('YieldSecs',0.25); - vbl=flip(s); - syncTime(me); - while trialLoop + WaitSecs('YieldSecs',1); + vbl = flip(s); tStart = vbl; + while vbl < tStart + 5 Screen('FillRect',s.win,[0.7 0.7 0.7 0.5],exc); Screen('DrawText',s.win,'Exclusion Zone',exc(1),exc(2),[0.8 0.8 0.8]); drawSpot(s,me.fixation.radius,[0.5 0.6 0.5 0.25],me.fixation.X,me.fixation.Y); drawCross(s,0.5,[1 1 0.5],me.fixation.X,me.fixation.Y); draw(o); drawGrid(s); drawScreenCenter(s); - + % get the current eye position and save x and y for local % plotting getSample(me); xst(b)=me.x - me.offset.X; yst(b)=me.y - me.offset.Y; @@ -1341,26 +824,29 @@ classdef eyetrackerManager < optickaCore vbl=Screen('Flip',s.win, vbl + s.screenVals.halfisi); % check the keyboard - [~, ~, keyCode] = KbCheck(-1); - if keyCode(stopkey); trialLoop = 0; blockLoop = 0; break; end - if keyCode(nextKey); trialLoop = 0; correct = true; break; end - if keyCode(calibkey); trackerSetup(me); break; end - if keyCode(driftkey); driftCorrection(me); break; end - if keyCode(offsetkey); driftOffset(me); break; end + [keyDown, ~, keyCode] = optickaCore.getKeys(); + if keyDown + if keyCode(stopkey); blockLoop = false; break; end + if keyCode(nextKey); correct = true; end + if keyCode(calibkey); trackerSetup(me); break; end + if keyCode(driftkey); driftCorrection(me); break; end + if keyCode(offsetkey); driftOffset(me); break; end + end % send a message for the EDF after 60 frames - if b == 60; edfMessage(me,'END_FIX');end + if b == 60; trackerMessage(me,'END_FIX'); syncTime(me);end b=b+1; end + Screen('Flip',s.win); + % clear tracker display + trackerClearScreen(me); % tell EDF end of reaction time portion - edfMessage(me,'END_RT'); - if correct - edfMessage(me,'TRIAL_RESULT 1'); - else - edfMessage(me,'TRIAL_RESULT 0'); - end + trackerMessage(me,'END_RT'); % stop recording data + WaitSecs(0.1); stopRecording(me); - setOffline(me); %Eyelink('Command', 'set_idle_mode'); + setOffline(me); + WaitSecs(0.1); + trackerMessage(me,'TRIAL_RESULT 1'); resetFixation(me); % set up the fix init system, whereby the subject must @@ -1372,37 +858,41 @@ classdef eyetrackerManager < optickaCore %me.fixInit.radius = 3; % prepare a random position for next trial - me.updateFixationValues([randi([-5 5]) -10],[randi([-5 5]) -10],[],[],randi([1 5])); - o.sizeOut = me.fixation.radius*2; + me.updateFixationValues([randi([-4 4]) -10],[randi([-4 4]) -10],[],[],randi([1 4])); + o.sizeOut = me.fixation.radius(1)*2; %me.fixation.radius = [me.fixation.radius me.fixation.radius]; o.xPositionOut = me.fixation.X(1); o.yPositionOut = me.fixation.Y(1); update(o); % use this struct for the parameters to draw stimulus % to screen - ts.x = me.fixation.X(1); - ts.y = me.fixation.Y(1); - ts.size = me.fixation.radius; - ts.selected = true; - % clear tracker display - trackerDrawStimuli(me,ts,true); - trackerDrawFixation(me); + for i = 1:length(me.fixation.X) + ts(i).x = me.toPixels(me.fixation.X(i),'x'); + ts(i).y = me.toPixels(me.fixation.Y(i),'y'); + ts(i).size = o.sizeOut; + ts(i).selected = true; + end % plot eye position for last trial and ITI plot(ax,xst,yst);drawnow; while GetSecs <= vbl + 1 % check the keyboard - [~, ~, keyCode] = KbCheck(-1); - if keyCode(calibkey); trackerSetup(me); break; end - if keyCode(driftkey); driftCorrection(me); break; end - if keyCode(offsetkey); driftOffset(me); break; end + [keyDown, ~, keyCode] = optickaCore.getKeys(); + if keyDown + if keyCode(calibkey); trackerSetup(me); break; end + if keyCode(driftkey); driftCorrection(me); break; end + if keyCode(offsetkey); driftOffset(me); break; end + end + WaitSecs(0.01); end a=a+1; end % clear tracker display trackerClearScreen(me); trackerDrawText(me,'FINISHED eyelinkManager Demo!!!'); - ListenChar(0);Priority(0);ShowCursor;RestrictKeysForKbCheck([]) + ListenChar(0);Priority(0);ShowCursor;RestrictKeysForKbCheck([]); close(s); + try close(aM); end + try close(rM); end close(me); clear s o if ~me.isDummy @@ -1410,7 +900,7 @@ classdef eyetrackerManager < optickaCore if strcmpi(an,'yes') if ~isdeployed; commandwindow; end evalin('base',['eA=eyelinkAnalysis(''dir'',''' ... - me.paths.savedData ''', ''file'',''myData.edf'');eA.parseSimple;eA.plot']); + me.paths.savedData ''', ''file'',''' me.saveFile ''');eA.parseSimple;eA.plot']); end end me.resetFixation; @@ -1426,12 +916,14 @@ classdef eyetrackerManager < optickaCore me.fixInit = oldfixinit; me.resetFixation; me.resetOffset; - ListenChar(0);Priority(0);ShowCursor;RestrictKeysForKbCheck([]) + ListenChar(0);Priority(0);ShowCursor;RestrictKeysForKbCheck([]); me.salutation('runDemo ERROR!!!') - Eyelink('Shutdown'); + try Eyelink('Shutdown'); end try close(s); end - sca; + try close(aM); end + try close(rM); end try close(me); end + sca; clear s o me.error = ME; me.salutation(ME.message); @@ -1446,47 +938,45 @@ classdef eyetrackerManager < optickaCore methods (Hidden = true) %------------------HIDDEN METHODS %======================================================================= + % === NOOPS + function trackerDrawEyePosition(~, varargin) + + end + function trackerDrawEyePositions(~, varargin) + + end + function trackerFlip(~, varargin) + + end + % =================================================================== + function evt = getEvent(me) %> @brief TODO %> % =================================================================== - function evt = getEvent(me) end % =================================================================== + function saveData(me,args) %> @brief compatibility with tobiiManager %> % =================================================================== - function saveData(me,args) end % =================================================================== - %> @brief send message to store in EDF data + function edfMessage(me, message) + %> @brief send message to store in EDF data, USE trackerMessage %> %> % =================================================================== - function edfMessage(me, message) if me.isConnected Eyelink('Message', message ); if me.verbose; fprintf('-+-+->EDF Message: %s\n',message);end end end - function trackerFlip(me, varargin) - - end - - function trackerDrawEyePosition(me) - - end - - function trackerDrawEyePositions(me) - - end - - end @@ -1494,57 +984,6 @@ classdef eyetrackerManager < optickaCore methods (Access = private) %------------------PRIVATE METHODS %======================================================================= - % =================================================================== - %> @brief - %> - % =================================================================== - function out = toDegrees(me,in,axis) - if ~exist('axis','var');axis='';end - switch axis - case 'x' - out = (in - me.screen.xCenter) / me.ppd_; - case 'y' - out = (in - me.screen.yCenter) / me.ppd_; - otherwise - if length(in)==2 - out(1) = (in(1) - me.screen.xCenter) / me.ppd_; - out(2) = (in(2) - me.screen.yCenter) / me.ppd_; - else - out = 0; - end - end - end - - % =================================================================== - %> @brief - %> - % =================================================================== - function out = toPixels(me,in,axis) - if ~exist('axis','var');axis='';end - switch axis - case '' - if length(in)==4 - out(1:2) = (in(1:2) * me.ppd_) + me.screen.xCenter; - out(3:4) = (in(3:4) * me.ppd_) + me.screen.yCenter; - elseif length(in)==2 - out(1) = (in(1) * me.ppd_) + me.screen.xCenter; - out(2) = (in(2) * me.ppd_) + me.screen.yCenter; - else - out = 0; - end - case 'rect' - out(1) = (in(1) * me.ppd_) + me.screen.xCenter; - out(2) = (in(2) * me.ppd_) + me.screen.yCenter; - out(3) = (in(3) * me.ppd_) + me.screen.xCenter; - out(4) = (in(4) * me.ppd_) + me.screen.yCenter; - case 'x' - out = (in * me.ppd_) + me.screen.xCenter; - case 'y' - out = (in * me.ppd_) + me.screen.yCenter; - end - end - end - end diff --git a/communication/eyelinkManager.m b/eyetracker/eyetrackerCore.m old mode 100755 new mode 100644 similarity index 39% rename from communication/eyelinkManager.m rename to eyetracker/eyetrackerCore.m index 6650b0a14fcfda5f699042015c20f943bc597048..42fb735f62e4f87e5c797a327cda73a88b1d230a --- a/communication/eyelinkManager.m +++ b/eyetracker/eyetrackerCore.m @@ -1,8 +1,5 @@ % ======================================================================== -%> @class eyelinkManager -%> @brief eyelinkManager wraps around the eyelink toolbox functions offering a -%> consistent interface and methods for fixation window control -%> +%> @class eyetrackerCore -- parent class for all eyetrackers %> Class methods enable the user to test for common behavioural eye tracking %> tasks with single commands. For example, to initiate a task we normally %> place a fixation cross on the screen and ask the subject to saccade to @@ -47,11 +44,14 @@ %> a reward systems during calibration / validation to improve subject %> performance compared to the eyelink toolbox alone. %> -%> @todo refactor this and tobiiManager to inherit from a common eyelinkManager -%> -%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md +%> Copyright ©2014-2023 Ian Max Andolina — released: LGPL3, see LICENCE.md % ======================================================================== -classdef eyelinkManager < optickaCore +classdef eyetrackerCore < optickaCore + + properties (Abstract, SetAccess = protected, GetAccess = public) + %> type of eyetracker + type + end properties %> fixation window in deg with 0,0 being the screen center: @@ -59,9 +59,9 @@ classdef eyelinkManager < optickaCore %> if X and Y have multiple rows, assume each row is a different %> fixation window. so that multiple fixtation windows can be used. %> - %> if radius has a single value, assume circular window if radius + %> if radius has as single value, assume circular window if radius %> has 2 values assume width × height rectangle (not strictly a - %> radius!) + %> radius I know!) %> %> initTime is the time the subject has to initiate fixation %> @@ -70,295 +70,219 @@ classdef eyelinkManager < optickaCore %> %> strict = false allows subject to exit and enter window without %> failure, useful during training - fixation struct = struct('X',0,'Y',0,'initTime',1,'time',1,... - 'radius',1,'strict',true) + fixation = struct('X',0,'Y',0,'initTime',1,'time',1,... + 'radius',1,'strict',true) %> Use exclusion zones where no eye movement allowed: [-degX +degX -degY %> +degY] Add rows to generate multiple exclusion zones. - exclusionZone double = [] + exclusionZone = [] %> we can define an optional window that the subject must stay %> inside before they saccade to other targets. This restricts %> guessing and "cheating", by forcing a minimum delay (default = %> 100ms / 0.1s) before initiating a saccade. Only used if X %> position is not empty. - fixInit struct = struct('X',[],'Y',[],'time',0.1,'radius',2) + fixInit = struct('X', [],'Y', [],'time', 0.1,'radius', 2) %> add a manual offset to the eye position, similar to a drift correction %> but handled by the eyelinkManager. - offset struct = struct('X',0,'Y',0) + offset = struct('X', 0,'Y', 0) + %> tracker update speed (Hz) + sampleRate = 500 %> start eyetracker in dummy mode? - isDummy logical = false - %> do we record and retrieve eyetracker EDF file? - recordData logical = true + isDummy = false + %> do we record and/or retrieve eyetracker data with remote interface? + recordData = true + %> use an operator screen for online display etc. + useOperatorScreen = false %> do we ignore blinks, if true then we do not update X and Y position %> from previous eye location, meaning the various methods will maintain %> position, e.g. if you are fixated and blink, the within-fixation X %> and Y position are retained so that a blink does not "break" - %> fixation. a blink is defined as a state whre gx and gy are MISSING - %> and pa is 0. Technically we can't really tell if a subject is - %> blinking or has removed their head using the float data. - ignoreBlinks logical = false - %> remote calibration enables manual control and selection of each - %> fixation this is useful for a baby or monkey who has not been trained - %> for fixation use 1-9 to show each dot, space to select fix as valid, - %> and INS key ON EYELINK KEYBOARD to accept calibration! - remoteCalibration logical = false - %> tracker update speed (Hz), should be 250 500 1000 2000 - sampleRate double = 1000 - %> calibration style, [H3 HV3 HV5 HV8 HV13] - calibrationStyle char = 'HV5' - %> proportion of screen used in horizontal and vertical co-ordinates - %> for calibration and validation, e.g. [0.3 0.3] - calibrationProportion double= [] - %> do we log messages to the command window? - verbose = false + %> fixation. + ignoreBlinks = false %> name of eyetracker EDF file - saveFile char = 'myData.edf' - %> eyetracker defaults structure - defaults struct = struct() - %> IP address of host - IP char = '' - % use callbacks - enableCallbacks logical = true - %> cutom calibration callback (enables better handling of - %> calibration, can trigger reward system etc.) - callback char = 'eyelinkCustomCallback' - %> eyelink defaults modifiers as a struct() - modify struct = struct('calibrationtargetcolour',[1 1 1],... - 'calibrationtargetsize',2,'calibrationtargetwidth',0.1,... - 'displayCalResults',1,'targetbeep',1,'devicenumber',-1,... - 'waitformodereadytime',500) + saveFile = 'eyetrackerData' + %> subject name + subjectName = '' + %> do we log debug messages to the command window? + verbose = false + end + + properties (Abstract) + %> info for setup / calibration + calibration end properties (Hidden = true) %> stimulus positions to draw on screen - stimulusPositions = [] - %> verbosity level - verbosityLevel double = 4 - %> force drift correction? - forceDriftCorrect logical = true - %> drift correct max - driftMaximum double = 15 - %> custom calibration target - customTarget = [] - %> compatibility with tobii which can use a second screen - secondScreen = false + stimulusPositions = [] + %> the PTB screen to work on, passed in during initialise + screen = [] + %> operator screen used during calibration + operatorScreen = [] + %> the PTB screen handle, normally set by screenManager but can force it to use another screen + win = [] + %> is operator screen being used? + secondScreen = false + %> size to draw eye position on screen + eyeSize = 10 + %> for trackerFlip, we can only flip every X frames + skipFlips = 8 + %> make the eyetracker not useable + isOff = false + %> lots of logging if debug = true + debug = false end properties (SetAccess = protected, GetAccess = public) %> Gaze X position in degrees - x = [] + x = [] %> Gaze Y position in degrees - y = [] + y = [] %> pupil size - pupil = [] + pupil = [] %> last isFixated true/false result - isFix = false + isFix = false %> did the fixInit test fail or not? - isInitFail = false + isInitFail = false %> are we in a blink? - isBlink = false + isBlink = false %> are we in an exclusion zone? - isExclusion = false - %> total time searching and holding fixation - fixTotal = 0 + isExclusion = false + %> total time searching for and holding fixation + fixTotal = 0 %> Initiate fixation length - fixInitLength = 0 - %how long have we been fixated? - fixLength = 0 + fixInitLength = 0 + %> how long have we been in the fixation window? + fixLength = 0 + %> when ~strict, we accumulate the total time in the window + fixBuffer = 0 %> Initiate fixation time - fixInitStartTime = 0 + fixInitStartTime = 0 %the first timestamp fixation was true - fixStartTime = 0 + fixStartTime = 0 %> which fixation window matched the last fixation? - fixWindow = 0 + fixWindow = 0 %> last time offset betweeen tracker and display computers - currentOffset = 0 + currentOffset = 0 %> tracker time stamp - trackerTime = 0 + trackerTime = 0 %current sample taken from eyelink - currentSample = [] + currentSample = [] %current event taken from eyelink - currentEvent = [] - % are we connected to eyelink? - isConnected logical = false - % are we recording to an EDF file? - isRecording logical = false + currentEvent = [] + % are we connected to eyetracker? + isConnected = false + % are we recording any data? + isRecording = false % which eye is the tracker using? - eyeUsed = -1 - %version of eyelink - version = '' - %> the PTB screen to work on, passed in during initialise - screen = [] + eyeUsed = -1 + %version of eyetracker interface + version = '' %> All gaze X position in degrees reset using resetFixation - xAll = [] + xAll = [] %> Last gaze Y position in degrees reset using resetFixation - yAll = [] + yAll = [] %> all pupil size reset using resetFixation - pupilAll = [] + pupilAll = [] + %> data streamed out from the Tobii + data = [] + %> validation data + validationData = {} end properties (SetAccess = protected, GetAccess = ?optickaCore) - % value for missing data - MISSING_DATA = -32768 - %> the PTB screen handle, normally set by screenManager but can force it to use another screen - win = [] - ppd_ double = 32 - tempFile char = 'MYDATA.edf' - % deals with strict fixation - fixN double = 0 - fixSelection = [] - error = [] - %> previous message sent to eyelink - previousMessage char = '' + % samples before any smoothing + xAllRaw + yAllRaw + %> flipTick + flipTick = 0 + %> currentSample template + sampleTemplate struct = struct('raw',[],'time',NaN,'timeD',NaN,'gx',NaN,'gy',NaN,... + 'pa',NaN,'valid',false) + ppd_ = 36 + % these are used to test strict fixation + fixN double = 0 + fixSelection = [] %> allowed properties passed to object upon construction - allowedProperties char = ['fixation|exclusionZone|fixInit|offset|ignoreBlinks|sampleRate|'... - 'calibrationStyle|calibrationProportion|recordData|modify|' ... - 'enableCallbacks|callback|name|verbose|isDummy|remoteCalibration|IP'] + allowedPropertiesBase = {'useOperatorScreen','fixation', 'exclusionZone', 'fixInit', ... + 'offset', 'sampleRate', 'ignoreBlinks', 'saveData',... + 'recordData', 'verbose', 'isDummy'} end + + %> ALL Children must implement these methods! + %======================================================================== + methods (Abstract) %-----------------ABSTRACT METHODS + %======================================================================== + out = initialise(in) + out = close(in) + out = checkConnection(in) + out = updateDefaults(in) + out = trackerSetup(in) + out = startRecording(in) + out = stopRecording(in) + out = getSample(in) + out = trackerMessage(in) + out = statusMessage(in) + out = runDemo(in) + end %---END ABSTRACT METHODS---% + - methods + %======================================================================== + methods %----------------------------PUBLIC METHODS + %======================================================================== + % =================================================================== %> @brief This is the constructor for this class %> % =================================================================== - function me = eyelinkManager(varargin) - args = optickaCore.addDefaults(varargin,struct('name','eyelink manager')); + function me = eyetrackerCore(varargin) + args = optickaCore.addDefaults(varargin); me=me@optickaCore(args); %we call the superclass constructor first - me.parseArgs(args, me.allowedProperties); - - me.defaults = EyelinkInitDefaults(); - try % is eyelink interface working - me.version = Eyelink('GetTrackerVersion'); - catch %#ok - me.version = 0; - end + me.parseArgs(args, me.allowedPropertiesBase); end - + % =================================================================== - %> @brief initialise the eyelink, setting up the proper settings - %> and opening the EDF file if me.recordData is true + %> @brief get mouse sample as eye data %> % =================================================================== - function success = initialise(me,sM) - success = false; - if ~exist('sM','var') - warning('Cannot initialise without a PTB screen') - return - end - - if ~me.isDummy - try - Eyelink('Shutdown'); %just make sure link is closed - catch ME - getReport(ME) - warning('Problems with Eyelink initialise, make sure you install Eyelink Developer libraries!'); - me.isDummy = true; - end - end - me.screen = sM; - - if ~isempty(me.IP) && ~me.isDummy - me.salutation('Eyelink Initialise',['Trying to set custom IP address: ' me.IP],true) - ret = Eyelink('SetAddress', me.IP); - if ret ~= 0 - warning('!!!--> Couldn''t set IP address to %s!!!\n',me.IP); - end - end - - if ~isempty(me.callback) && me.enableCallbacks - [res,dummy] = EyelinkInit(me.isDummy,me.callback); - elseif me.enableCallbacks - [res,dummy] = EyelinkInit(me.isDummy,1); - else - [res,dummy] = EyelinkInit(me.isDummy,0); - end - me.isDummy = logical(dummy); - me.checkConnection(); - if ~me.isConnected && ~me.isDummy - me.salutation('Eyelink Initialise','Could not connect, or enter Dummy mode...',true) - return - end - - if me.screen.isOpen == true - me.win = me.screen.win; - me.defaults = EyelinkInitDefaults(me.win); - elseif ~isempty(me.win) - me.defaults = EyelinkInitDefaults(me.win); - else - me.defaults = EyelinkInitDefaults(); - end - - me.defaults.winRect=me.screen.winRect; - % this command is sent from EyelinkInitDefaults - % Eyelink('Command', 'screen_pixel_coords = %ld %ld %ld %ld',me.screen.winRect(1),me.screen.winRect(2),me.screen.winRect(3)-1,me.screen.winRect(4)-1); - if ~isempty(me.callback) && exist(me.callback,'file') - me.defaults.callback = me.callback; - end - me.defaults.backgroundcolour = me.screen.backgroundColour; - me.ppd_ = me.screen.ppd; - me.defaults.ppd = me.screen.ppd; - - %structure of eyelink modifiers - fn = fieldnames(me.modify); - for i = 1:length(fn) - if isfield(me.defaults,fn{i}) - me.defaults.(fn{i}) = me.modify.(fn{i}); - end - end - - me.defaults.verbose = me.verbose; - - if ~isempty(me.customTarget) - me.customTarget.reset(); - me.customTarget.setup(me.screen); - me.defaults.customTarget = me.customTarget; + function sample = getMouseSample(me) + if ~isempty(me.win) + w = me.win; + elseif ~isempty(me.screen) && ~isempty(me.screen.screen) + w = me.screen.screen; else - me.defaults.customTarget = []; - end - - updateDefaults(me); - - if me.isDummy - me.version = 'Dummy Eyelink'; + w = []; + end + [x, y, b] = GetMouse(w); + sample = me.sampleTemplate; + sample.time = GetSecs; + sample.timeD = sample.time; + if b(3) == 1 + me.x = NaN; + me.y = NaN; + me.pupil = NaN; + me.isBlink = true; + sample.valid = false; + if me.debug;fprintf('BLINK\n');end else - [~, me.version] = Eyelink('GetTrackerVersion'); - end - getTrackerTime(me); - getTimeOffset(me); - me.salutation('Initialise Method', sprintf('Running on a %s @ %2.5g (time offset: %2.5g)', me.version, me.trackerTime,me.currentOffset),true); - % try to open file to record data to - if me.isConnected - if me.recordData - err = Eyelink('Openfile', me.tempFile); - if err ~= 0 - warning('eyelinkManager Cannot setup Eyelink data file, aborting data recording'); - me.isRecording = false; - else - Eyelink('Command', ['add_file_preamble_text ''Recorded by:' me.fullName ' tracker'''],true); - me.isRecording = true; - end + xy = toDegrees(me, [x y]); + me.x = xy(1); me.y = xy(2); + if strcmpi(me.type,'eyelink') + me.pupil = 800 + randi(20); + else + me.pupil = 5 + rand; end - Eyelink('Message', 'DISPLAY_COORDS %ld %ld %ld %ld',me.screen.winRect(1),me.screen.winRect(2),me.screen.winRect(3)-1,me.screen.winRect(4)-1); - Eyelink('Message', 'FRAMERATE %ld',round(me.screen.screenVals.fps)); - Eyelink('Message', 'DISPLAY_PPD %ld', round(me.ppd_)); - Eyelink('Message', 'DISPLAY_DISTANCE %ld', round(me.screen.distance)); - Eyelink('Message', 'DISPLAY_PIXELSPERCM %ld', round(me.screen.pixelsPerCm)); - Eyelink('Command', 'link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON'); - Eyelink('Command', 'link_sample_data = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS'); - Eyelink('Command', 'file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON'); - Eyelink('Command', 'file_sample_data = LEFT,RIGHT,GAZE,HREF,AREA,GAZERES,STATUS'); - %Eyelink('Command', 'use_ellipse_fitter = no'); - Eyelink('Command', 'sample_rate = %d',me.sampleRate); - end + me.isBlink = false; + sample.valid = true; + end + sample.gx = x; + sample.gy = y; + sample.pa = me.pupil; + me.xAll = [me.xAll me.x]; + me.yAll = [me.yAll me.y]; + me.pupilAll = [me.pupilAll me.pupil]; + me.xAllRaw = me.xAll; me.yAllRaw = me.yAll; + if me.debug;fprintf('< MOUSE X: %.2f | Y: %.2f | BLINK: %i>\n',me.x,me.y, me.isBlink);end end - - % =================================================================== - %> @brief - %> - % =================================================================== - function updateDefaults(me) - EyelinkUpdateDefaults(me.defaults); - end - % =================================================================== %> @brief reset all fixation/exclusion data @@ -367,18 +291,20 @@ classdef eyelinkManager < optickaCore function resetAll(me) resetExclusionZones(me); resetFixInit(me); - resetOffset(me); - resetFixation(me,true); + resetFixation(me, true); + me.flipTick = 0; end + % =================================================================== %> @brief reset the fixation counters ready for a new trial %> %> @param removeHistory remove the history of recent eye position? % =================================================================== - function resetFixation(me,removeHistory) - if ~exist('removeHistory','var');removeHistory=true;end + function resetFixation(me, removeHistory) + if ~exist('removeHistory','var'); removeHistory = false; end me.fixStartTime = 0; me.fixLength = 0; + me.fixBuffer = 0; me.fixInitStartTime = 0; me.fixInitLength = 0; me.fixTotal = 0; @@ -392,13 +318,14 @@ classdef eyelinkManager < optickaCore me.isBlink = false; me.isExclusion = false; me.isInitFail = false; + me.flipTick = 0; if me.verbose - fprintf('-+-+-> eyelinkManager:reset fixation: %i %i %i\n',me.fixLength,me.fixTotal,me.fixN); + fprintf('-+-+-> EyeTracker:RESET Fixation: fix length=%i fix total=%i fix #%i\n',me.fixLength,me.fixTotal,me.fixN); end end % =================================================================== - %> @brief reset the fixation counters ready for a new trial + %> @brief reset the exclusion state ready for a new trial %> % =================================================================== function resetExclusionZones(me) @@ -406,305 +333,130 @@ classdef eyelinkManager < optickaCore end % =================================================================== - %> @brief reset the fixation counters ready for a new trial + %> @brief reset the fixation time ready for a new trial %> % =================================================================== function resetFixationTime(me) me.fixStartTime = 0; me.fixLength = 0; + me.fixBuffer = 0; end % =================================================================== - %> @brief reset the fixation history: xAll yAll pupilAll + %> @brief reset the recent fixation history: xAll yAll pupilAll %> % =================================================================== function resetFixationHistory(me) me.xAll = []; me.yAll = []; me.pupilAll = []; + me.xAllRaw = []; + me.yAllRaw = []; end % =================================================================== - %> @brief reset the fixation offset to 0 - %> - % =================================================================== - function resetOffset(me) - me.offset.X = 0; - me.offset.Y = 0; - end - - % =================================================================== - %> @brief reset the fixation offset to 0 + %> @brief reset the fixation initiation to 0 %> % =================================================================== function resetFixInit(me) me.fixInit.X = []; me.fixInit.Y = []; end - - % =================================================================== - %> @brief check the connection with the eyelink - %> - % =================================================================== - function connected = checkConnection(me) - isc = Eyelink('IsConnected'); - if isc == 1 - me.isConnected = true; - elseif isc == -1 - me.isConnected = false; - me.isDummy = true; - else - me.isConnected = false; - end - connected = me.isConnected; - end - - % =================================================================== - %> @brief sets up the calibration and validation - %> - % =================================================================== - function trackerSetup(me) - if ~me.isConnected; return; end - oldrk = RestrictKeysForKbCheck([]); %just in case someone has restricted keys - fprintf('\n===>>> CALIBRATING EYELINK... <<<===\n'); - Eyelink('Verbosity',me.verbosityLevel); - if ~isempty(me.calibrationProportion) && length(me.calibrationProportion)==2 - Eyelink('Command','calibration_area_proportion = %s', num2str(me.calibrationProportion)); - Eyelink('Command','validation_area_proportion = %s', num2str(me.calibrationProportion)); - % see https://www.sr-support.com/forum-37-page-2.html - %Eyelink('Command','calibration_corner_scaling = %s', num2str([me.calibrationProportion(1)-0.1 me.calibrationProportion(2)-0.1])-); - %Eyelink('Command','validation_corner_scaling = %s', num2str([me.calibrationProportion(1)-0.1 me.calibrationProportion(2)-0.1])); - end - Eyelink('Command','horizontal_target_y = %i',me.screen.winRect(4)/2); - Eyelink('Command','calibration_type = %s', me.calibrationStyle); - Eyelink('Command','normal_click_dcorr = ON'); - Eyelink('Command', 'driftcorrect_cr_disable = OFF'); - Eyelink('Command', 'drift_correction_rpt_error = 10.0'); - Eyelink('Command', 'online_dcorr_maxangle = 15.0'); - Eyelink('Command','randomize_calibration_order = NO'); - Eyelink('Command','randomize_validation_order = NO'); - Eyelink('Command','cal_repeat_first_target = YES'); - Eyelink('Command','val_repeat_first_target = YES'); - Eyelink('Command','validation_online_fixup = NO'); - Eyelink('Command','generate_default_targets = YES'); - if me.remoteCalibration - Eyelink('Command','remote_cal_enable = 1'); - Eyelink('Command','key_function 1 ''remote_cal_target 1'''); - Eyelink('Command','key_function 2 ''remote_cal_target 2'''); - Eyelink('Command','key_function 3 ''remote_cal_target 3'''); - Eyelink('Command','key_function 4 ''remote_cal_target 4'''); - Eyelink('Command','key_function 5 ''remote_cal_target 5'''); - Eyelink('Command','key_function 6 ''remote_cal_target 6'''); - Eyelink('Command','key_function 7 ''remote_cal_target 7'''); - Eyelink('Command','key_function 8 ''remote_cal_target 8'''); - Eyelink('Command','key_function 9 ''remote_cal_target 9'''); - Eyelink('Command','key_function 0 ''remote_cal_target 0'''); - Eyelink('Command','key_function ins ''remote_cal_complete'''); - fprintf('\n===>>> REMOTE CALIBRATION ENABLED: 1-9 show point, space to choose point.\nINS key ON EYELINK == accept calibration!!!\n'); - else - Eyelink('Command','remote_cal_enable = 0'); - end - if ~isdeployed; commandwindow; end - EyelinkDoTrackerSetup(me.defaults); - if ~isempty(me.screen) && me.screen.isOpen - Screen('Flip',me.screen.win); - end - [result,out] = Eyelink('CalMessage'); - fprintf('-+-+-> CAL RESULT = %.2f | message: %s\n\n',result,out); - RestrictKeysForKbCheck(oldrk); - checkEye(me); - end - - % =================================================================== - %> @brief wrapper for StartRecording - %> - % =================================================================== - function startRecording(me,~) - if me.isConnected - Eyelink('StartRecording'); - checkEye(me); - end - end - - % =================================================================== - %> @brief wrapper for StopRecording - %> - % =================================================================== - function stopRecording(me,~) - if me.isConnected - Eyelink('StopRecording'); - end - end - - % =================================================================== - %> @brief set into offline / idle mode - %> - % =================================================================== - function setOffline(me) - if me.isConnected - Eyelink('Command', 'set_idle_mode'); - end - end - + % =================================================================== - %> @brief wrapper for EyelinkDoDriftCorrection + %> @brief reset the fixation offset to 0 %> % =================================================================== - function success = driftCorrection(me) - success = false; - x=me.toPixels(me.fixation.X,'x'); %#ok<*PROPLC> - y=me.toPixels(me.fixation.Y,'y'); - if me.isConnected - resetOffset(me); - Eyelink('Command', 'driftcorrect_cr_disable = OFF'); - Eyelink('Command', 'drift_correction_rpt_error = 10.0'); - Eyelink('Command', 'online_dcorr_maxangle = 15.0'); - Screen('DrawText',me.screen.win,'Drift Correction...',10,10); - Screen('gluDisk',me.screen.win,[1 0 1 0.5],x,y,8); - Screen('Flip',me.screen.win); - WaitSecs('YieldSecs',0.2); - success = EyelinkDoDriftCorrection(me.defaults, round(x), round(y), 1, 1); - [result,out] = Eyelink('CalMessage'); - fprintf('DriftCorrect @ %.2f/%.2f px (%.2f/%.2f deg): result = %i msg = %s\n',... - x,y, me.fixation.X, me.fixation.Y,result,out); - if success ~= 0 - me.salutation('Drift Correct','FAILED',true); - end - if me.forceDriftCorrect - res=Eyelink('ApplyDriftCorr'); - [result,out] = Eyelink('CalMessage'); - me.salutation('Drift Correct',sprintf('Results: %f %i %s\n',res,result,out),true); - end - end - WaitSecs('YieldSecs',1); + function resetOffset(me) + me.offset.X = 0; + me.offset.Y = 0; end % =================================================================== function success = driftOffset(me) %> @fn driftOffset - %> @brief wrapper for custom drift offset function + %> @brief our own version of eyelink's drift correct %> % =================================================================== success = false; - escapeKey = KbName('ESCAPE'); - stopkey = KbName('Q'); - nextKey = KbName('SPACE'); - calibkey = KbName('C'); - driftkey = KbName('D'); - if me.isConnected || me.isDummy - x = me.toPixels(me.fixation.X,'x'); %#ok<*PROPLC> - y = me.toPixels(me.fixation.Y,'y'); - Screen('Flip',me.screen.win); - ifi = me.screen.screenVals.ifi; - breakLoop = false; i = 1; flash = true; - correct = false; - xs = []; - ys = []; - while ~breakLoop - getSample(me); - xs(i) = me.x; - ys(i) = me.y; - if mod(i,10) == 0 - flash = ~flash; - end - Screen('DrawText',me.screen.win,'Drift Correction...',10,10,[0.4 0.4 0.4]); - if flash - Screen('gluDisk',me.screen.win,[1 0 1 0.75],x,y,10); - Screen('gluDisk',me.screen.win,[1 1 1 1],x,y,4); - else - Screen('gluDisk',me.screen.win,[1 1 0 0.75],x,y,10); - Screen('gluDisk',me.screen.win,[0 0 0 1],x,y,4); - end - me.screen.drawCross(0.6,[0 0 0],x,y,0.1,false); - Screen('Flip',me.screen.win); - [~, ~, keyCode] = KbCheck(-1); - if keyCode(stopkey) || keyCode(escapeKey); breakLoop = true; break; end - if keyCode(nextKey); correct = true; break; end - if keyCode(calibkey); trackerSetup(me); break; end - if keyCode(driftkey); driftCorrection(me); break; end - i = i + 1; + if me.isOff || ~me.isConnected || ~me.isDummy; return; end + + if ~isempty(me.screen) && isa(me.screen,'screenManager'); open(me.screen); end + if me.useOperatorScreen && isa(me.operatorScreen,'screenManager'); open(me.operatorScreen); end + s = me.screen; + if me.useOperatorScreen; s2 = me.operatorScreen; end + + ListenChar(0); + oldrk = RestrictKeysForKbCheck([]); %just in case someone has restricted keys + success = false; + if matches(me.type,'eyelink') + startRecording(me); + statusMessage(me,'Drift Offset Initiated'); + end + resetOffset(me); + trackerMessage(me,'Drift OFFSET'); + trackerDrawStatus(me,'Drift Offset',[],false,false); + menu = KbName('LeftShift'); + sample = KbName('RightShift'); + x = me.toPixels(me.fixation.X(1),'x'); %#ok<*PROP,*PROPLC> + y = me.toPixels(me.fixation.Y(1),'y'); + flip(s); trackerFlip(me); + breakLoop = false; i = 1; flash = true; + correct = false; + xs = []; + ys = []; + while ~breakLoop + getSample(me); + xs(i) = me.x; %#ok<*AGROW> + ys(i) = me.y; + if mod(i,10) == 0 + flash = ~flash; end - if correct && length(xs) > 5 && length(ys) > 5 - success = true; - me.offset.X = median(xs) - me.fixation.X; - me.offset.Y = median(ys) - me.fixation.Y; - t = sprintf('Offset: X = %.2f Y = %.2f\n',me.offset.X,me.offset.Y); - me.salutation('Drift [SELF]Correct',t,true); - Screen('DrawText',me.screen.win,t,10,10,[0.4 0.4 0.4]); - Screen('Flip',me.screen.win); + if me.useOperatorScreen + drawText(s,'Look at the cross...'); else - me.offset.X = 0; - me.offset.Y = 0; - t = sprintf('Offset: X = %.2f Y = %.2f\n',me.offset.X,me.offset.Y); - me.salutation('REMOVE Drift [SELF]Offset',t,true); - Screen('DrawText',me.screen.win,'Reset Drift Offset...',10,10,[0.4 0.4 0.4]); - Screen('Flip',me.screen.win); + drawText(s,'Look at the cross [LeftShift = quit | RightShift = sample]'); end - WaitSecs('YieldSecs',1); - end - end - - % =================================================================== - function error = checkRecording(me) - %> @fn checkRecording - %> Wrapper for CheckRecording - %> - % =================================================================== - if me.isConnected - error=Eyelink('CheckRecording'); - else - error = -1; - end - end - - % =================================================================== - function sample = getSample(me) - %> @fn getSample - %> Get a sample from the tracker, if dummymode=true then use - %> the mouse as an eye signal - %> - % =================================================================== - if me.isConnected && Eyelink('NewFloatSampleAvailable') > 0 - me.currentSample = Eyelink('NewestFloatSample');% get the sample in the form of an event structure - if ~isempty(me.currentSample) && isstruct(me.currentSample) - if me.currentSample.gx(me.eyeUsed+1) == me.MISSING_DATA ... - && me.currentSample.gy(me.eyeUsed+1) == me.MISSING_DATA ... - && me.currentSample.pa(me.eyeUsed+1) == 0 ... - && me.ignoreBlinks - %me.x = toPixels(me,me.fixation.X,'x'); - %me.y = toPixels(me,me.fixation.Y,'y'); - me.pupil = 0; - me.isBlink = true; - else - me.x = me.currentSample.gx(me.eyeUsed+1); % +1 as we're accessing MATLAB array - me.y = me.currentSample.gy(me.eyeUsed+1); - me.pupil = me.currentSample.pa(me.eyeUsed+1); - me.xAll = [me.xAll me.x]; - me.yAll = [me.yAll me.y]; - me.pupilAll = [me.pupilAll me.pupil]; - me.isBlink = false; - end - %if me.verbose;fprintf('\n',me.x,me.y,me.pupil,me.isBlink);end - end - elseif me.isDummy %lets use a mouse to simulate the eye signal - if ~isempty(me.win) - [me.x, me.y] = GetMouse(me.win); - elseif ~isempty(me.screen) && ~isempty(me.screen.screen) - [me.x, me.y] = GetMouse(me.screen.screen); + trackerDrawText(me,'Drift Correction: LeftShift = quit | RightShift = sample'); + if flash + Screen('gluDisk',s.win,[1 0 1 0.75],x,y,10); + Screen('gluDisk',s.win,[1 1 1 1],x,y,4); else - [me.x, me.y] = GetMouse(); + Screen('gluDisk',s.win,[1 1 0 0.75],x,y,10); + Screen('gluDisk',s.win,[0 0 0 1],x,y,4); + end + s.drawCross(0.8,[0 0 0],x,y,0.2,false); + trackerDrawEyePositions(me); + flip(s); + if me.useOperatorScreen; flip(s2); end + [pressed, ~, keyCode] = optickaCore.getKeys; + if pressed + if keyCode(menu); breakLoop = true; break; end + if keyCode(sample); correct = true; break; end end - me.pupil = 800 + randi(20); - me.currentSample.gx = me.x; - me.currentSample.gy = me.y; - me.currentSample.pa = me.pupil; - me.currentSample.time = GetSecs * 1000; - me.xAll = [me.xAll me.x]; - me.yAll = [me.yAll me.y]; - me.pupilAll = [me.pupilAll me.pupil]; - %if me.verbose;fprintf('\n',me.x,me.y,me.pupil,me.currentSample.time);end + i = i + 1; + end + if correct && length(xs) > 15 && length(ys) > 15 + success = true; + me.offset.X = median(xs(end-10:end)) - me.fixation.X(1); + me.offset.Y = median(ys(end-10:end)) - me.fixation.Y(1); + t = sprintf('Offset: X = %.2f Y = %.2f\n',me.offset.X,me.offset.Y); + me.salutation('Drift [SELF]Correct',t,true); + s.drawText(t); + s2.drawText(t); + drawDriftOffset(me); + flip(s); + trackerFlip(me); + else + me.offset.X = 0; + me.offset.Y = 0; + t = sprintf('Offset: X = %.2f Y = %.2f\n',me.offset.X,me.offset.Y); + me.salutation('REMOVE Drift [SELF]Offset',t,true); + drawText(me.screen,'Reset Drift Offset...'); + Screen('Flip',me.screen.win); end - sample = me.currentSample; + WaitSecs('YieldSecs',1); + RestrictKeysForKbCheck(oldrk); end @@ -749,39 +501,44 @@ classdef eyelinkManager < optickaCore end elseif length(inittime) == 2 me.fixation.initTime = randi(inittime.*1000)/1000; - elseif length(inittime) == 1 + elseif isscalar(inittime) me.fixation.initTime = inittime; end end if nargin > 4 && ~isempty(fixtime) if length(fixtime) == 2 me.fixation.time = randi(fixtime.*1000)/1000; - elseif length(fixtime) == 1 + elseif isscalar(fixtime) me.fixation.time = fixtime; end end if nargin > 5 && ~isempty(radius); me.fixation.radius = radius; end - if nargin > 6 && ~isempty(strict); me.fixation.strict = strict; end + if nargin > 6 && ~isempty(strict); me.fixation.strict = logical(strict); end if me.verbose - fprintf('-+-+-> eyelinkManager:updateFixationValues: X=%g | Y=%g | IT=%s | FT=%s | R=%g | Strict=%i\n', ... + fprintf('-+-+-> EyeTracker:updateFixationValues: X=%g | Y=%g | IT=%s | FT=%s | R=%s | Strict=%i\n', ... me.fixation.X, me.fixation.Y, num2str(me.fixation.initTime,'%.2f '), num2str(me.fixation.time,'%.2f '), ... - me.fixation.radius,me.fixation.strict); + num2str(me.fixation.radius,'%.2f '),me.fixation.strict); end end % =================================================================== - %> @brief Sinlge method to update the exclusion zones + %> @brief Sinlge method to update the exclusion zones, can pass multiple + %> x & y values for multiple exclusion zones, sharing the same radius %> - %> @param x x position in degrees - %> @param y y position in degrees - %> @param radius the radius of the exclusion zone + %> @param x x position[s] in degrees + %> @param y y position[s] in degrees + %> @param radius the radius of the exclusion zone, if length=2 becomes WxH % =================================================================== function updateExclusionZones(me,x,y,radius) resetExclusionZones(me); + if ~exist('radius','var') || isempty(radius); return; end if exist('x','var') && exist('y','var') && ~isempty(x) && ~isempty(y) - if ~exist('radius','var'); radius = 5; end for i = 1:length(x) - me.exclusionZone(i,:) = [x(i)-radius x(i)+radius y(i)-radius y(i)+radius]; + if length(radius) == 2 + me.exclusionZone(i,:) = [x(i)-radius(1) x(i)+radius(1) y(i)-radius(2) y(i)+radius(2)]; + else + me.exclusionZone(i,:) = [x(i)-radius x(i)+radius y(i)-radius y(i)+radius]; + end end end end @@ -792,25 +549,38 @@ classdef eyelinkManager < optickaCore %> @return fixated boolean if we are fixated %> @return fixtime boolean if we're fixed for fixation time %> @return searching boolean for if we are still searching for fixation + %> @return window which fixation window matched + %> @return exclusion was any exclusion window entered? + %> @return initfail did subject break fixinit rule? + %> @return blinking is the subject putatively blinkng? % =================================================================== - function [fixated, fixtime, searching, window, exclusion, fixinit] = isFixated(me) + function [fixated, fixtime, searching, window, exclusion, initfail, blinking] = isFixated(me) fixated = false; fixtime = false; searching = true; - exclusion = false; window = []; fixinit = false; - - if isempty(me.currentSample); return; end + exclusion = false; window = []; initfail = false; blinking = false; + if me.isOff || (~me.isDummy && ~me.isConnected); return; end + if me.isBlink; blinking = true; end if me.isExclusion || me.isInitFail - exclusion = me.isExclusion; fixinit = me.isInitFail; searching = false; + exclusion = me.isExclusion; initfail = me.isInitFail; searching = false; return; % we previously matched either rule, now cannot pass fixation until a reset. end + + if isempty(me.currentSample) || ~me.currentSample.valid; return; end + if me.fixInitStartTime == 0 me.fixInitStartTime = me.currentSample.time; + me.fixStartTime = 0; + me.fixLength = 0; + me.fixBuffer = 0; me.fixTotal = 0; me.fixInitLength = 0; + else + me.fixTotal = me.currentSample.time - me.fixInitStartTime; end % ---- add any offsets for following calculations x = me.x - me.offset.X; y = me.y - me.offset.Y; + if isempty(x) || isempty(y); return; end % ---- test for exclusion zones first if ~isempty(me.exclusionZone) @@ -825,46 +595,49 @@ classdef eyelinkManager < optickaCore end % ---- test for fix initiation start window - ft = (me.currentSample.time - me.fixInitStartTime) / 1e3; - if ~isempty(me.fixInit.X) && ft <= me.fixInit.time + if ~isempty(me.fixInit.X) && me.fixTotal <= me.fixInit.time r = sqrt((x - me.fixInit.X).^2 + (y - me.fixInit.Y).^2); window = find(r < me.fixInit.radius); if ~any(window) - searching = false; fixinit = true; + searching = false; initfail = true; me.isInitFail = true; me.isFix = false; - fprintf('-+-+-> eyelinkManager: Eye left fix init window @ %.3f secs!\n',ft); + if me.verbose;fprintf('-+-+-> EyeTracker: Eye left fix init window @ %.3f secs!\n',ft);end return; end end + % now test if we are still searching or in fixation window, if % radius is single value, assume circular, otherwise assume % rectangular - window = 0; - if length(me.fixation.radius) == 1 % circular test + w = 0; + if isscalar(me.fixation.radius) % circular test r = sqrt((x - me.fixation.X).^2 + (y - me.fixation.Y).^2); %fprintf('x: %g-%g y: %g-%g r: %g-%g\n',x, me.fixation.X, me.y, me.fixation.Y,r,me.fixation.radius); - window = find(r < me.fixation.radius); + w = find(r < me.fixation.radius); else % x y rectangular window test for i = 1:length(me.fixation.X) if (x >= (me.fixation.X - me.fixation.radius(1))) && (x <= (me.fixation.X + me.fixation.radius(1))) ... && (me.y >= (me.fixation.Y - me.fixation.radius(2))) && (me.y <= (me.fixation.Y + me.fixation.radius(2))) - window = i;break; + w = i; break; end end end - me.fixWindow = window; - me.fixTotal = (me.currentSample.time - me.fixInitStartTime) / 1e3; - if any(window) % inside fixation window - if me.fixN == 0 - me.fixN = 1; - me.fixSelection = window(1); + if ~isempty(w) && w > 0; me.fixWindow = w; else; me.fixWindow = 0; end + + if me.debug;fprintf('\n',x,y,me.fixWindow);end + + % logic if we are in or not in a fixation window + if me.fixWindow > 0 % inside fixation window + if me.fixStartTime == 0 + me.fixN = me.fixN + 1; + me.fixStartTime = me.currentSample.time; end - if me.fixSelection == window(1) - if me.fixStartTime == 0 - me.fixStartTime = me.currentSample.time; - end + if me.fixN == 1 + me.fixSelection = me.fixWindow; + end + if me.fixSelection == me.fixWindow fixated = true; searching = false; - me.fixLength = (me.currentSample.time - me.fixStartTime) / 1e3; - if me.fixLength >= me.fixation.time + me.fixLength = (me.currentSample.time - me.fixStartTime); + if me.fixLength + me.fixBuffer >= me.fixation.time fixtime = true; end else @@ -872,16 +645,20 @@ classdef eyelinkManager < optickaCore end me.isFix = fixated; me.fixInitLength = 0; else % not inside the fixation window - if me.fixN == 1 - me.fixN = -100; - end - me.fixInitLength = (me.currentSample.time - me.fixInitStartTime) / 1e3; + me.fixInitLength = (me.currentSample.time - me.fixInitStartTime); if me.fixInitLength < me.fixation.initTime searching = true; else searching = false; end - me.isFix = false; me.fixLength = 0; me.fixStartTime = 0; + me.isFix = false; + if me.fixation.strict + me.fixLength = 0; + me.fixBuffer = 0; + elseif ~me.fixation.strict && me.fixStartTime > 0 + me.fixBuffer = me.fixBuffer + me.fixLength; + end + me.fixStartTime = 0; end end @@ -908,7 +685,8 @@ classdef eyelinkManager < optickaCore %> 2 strings, either one is returned depending on success or %> failure, 'searching' may also be returned meaning the fixation %> window hasn't been entered yet, and 'fixing' means the fixation - %> time is not yet met... + %> time is not yet met... 'blinking' can be returned when ignoreBlinks + %> = true and we think a blink may be occuring. %> %> @param yesString if this function succeeds return this string %> @param noString if this function fails return this string @@ -919,45 +697,49 @@ classdef eyelinkManager < optickaCore % =================================================================== function [out, window, exclusion, initfail] = testSearchHoldFixation(me, yesString, noString) [fix, fixtime, searching, window, exclusion, initfail] = me.isFixated(); + if me.ignoreBlinks && me.isBlink + out = 'blinking'; + return + end if exclusion out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testSearchHoldFixation EXCLUSION ZONE ENTERED time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if me.verbose; fprintf('-+-+-> EyeTracker:testSearchHoldFixation EXCLUSION ZONE ENTERED time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail); end return; end if initfail out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testSearchHoldFixation FIX INIT TIME FAIL time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if me.verbose; fprintf('-+-+-> EyeTracker:testSearchHoldFixation FIX INIT TIME FAIL time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail); end return end if searching - if (me.fixation.strict==true && (me.fixN == 0)) || me.fixation.strict==false + if (me.fixation.strict == true && me.fixN == 0) || me.fixation.strict==false out = 'searching'; else out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testSearchHoldFixation STRICT SEARCH FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if me.verbose; fprintf('-+-+-> EyeTracker:testSearchHoldFixation STRICT SEARCH FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end end return elseif fix - if (me.fixation.strict==true && ~(me.fixN == -100)) || me.fixation.strict==false + if (me.fixation.strict==true && me.fixN == 1) || me.fixation.strict==false if fixtime out = yesString; - if me.verbose; fprintf('-+-+-> Eyelink:testSearchHoldFixation FIXATION SUCCESSFUL!: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if me.verbose; fprintf('-+-+-> EyeTracker:testSearchHoldFixation FIXATION SUCCESSFUL!: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end else out = 'fixing'; end else out = noString; - if me.verbose;fprintf('-+-+-> Eyelink:testSearchHoldFixation FIX FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if me.verbose;fprintf('-+-+-> EyeTracker:testSearchHoldFixation FIX FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end end return elseif searching == false out = noString; - if me.verbose;fprintf('-+-+-> Eyelink:testSearchHoldFixation SEARCH FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if me.verbose;fprintf('-+-+-> EyeTracker:testSearchHoldFixation SEARCH FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end else out = ''; @@ -976,38 +758,48 @@ classdef eyelinkManager < optickaCore % =================================================================== function [out, window, exclusion, initfail] = testHoldFixation(me, yesString, noString) [fix, fixtime, searching, window, exclusion, initfail] = me.isFixated(); + if me.ignoreBlinks && me.isBlink + out = 'blinking'; + return + end if exclusion out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testHoldFixation EXCLUSION ZONE ENTERED time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if me.verbose; fprintf('-+-+-> EyeTracker:testHoldFixation EXCLUSION ZONE ENTERED time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail); end return; end if initfail out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testHoldFixation FIX INIT TIME FAIL time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if me.verbose; fprintf('-+-+-> EyeTracker:testHoldFixation FIX INIT TIME FAIL time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail); end - return + return; end if fix - if (me.fixation.strict==true && ~(me.fixN == -100)) || me.fixation.strict==false + if me.fixation.strict==false || (me.fixation.strict == true && me.fixN == 1) if fixtime out = yesString; - if me.verbose; fprintf('-+-+-> Eyelink:testHoldFixation FIXATION SUCCESSFUL!: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if me.verbose; fprintf('-+-+-> EyeTracker:testHoldFixation FIXATION SUCCESSFUL!: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end else out = 'fixing'; end + return; else out = noString; - if me.verbose;fprintf('-+-+-> Eyelink:testHoldFixation FIX FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if me.verbose;fprintf('-+-+-> EyeTracker:testHoldFixation FIX FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end + return; end - return else - out = noString; - if me.verbose; fprintf('-+-+-> Eyelink:testHoldFixation FIX FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... + if (me.fixation.strict == true && me.fixN == 1) + out = noString; + if me.verbose; fprintf('-+-+-> EyeTracker:testHoldFixation FIX FAIL: %s time:[%.2f %.2f %.2f] f:%i ft:%i s:%i e:%i fi:%i\n', ... out, me.fixTotal, me.fixInitLength, me.fixLength, fix, fixtime, searching, exclusion, initfail);end - return + return; + else + out = 'fixing'; + return; + end end end @@ -1017,6 +809,10 @@ classdef eyelinkManager < optickaCore %> % =================================================================== function out = testWithinFixationWindow(me, yesString, noString) + if me.ignoreBlinks && me.isBlink + out = 'blinking'; + return + end if isFixated(me) out = yesString; else @@ -1034,6 +830,7 @@ classdef eyelinkManager < optickaCore % =================================================================== function out = testFixationTime(me, yesString, noString) [fix,fixtime] = isFixated(me); + me.ignoreBlinks if fix && fixtime out = yesString; %me.salutation(sprintf('Fixation Time: %g',me.fixLength),'TESTFIXTIME'); else @@ -1048,172 +845,175 @@ classdef eyelinkManager < optickaCore %> % =================================================================== function eyeUsed = checkEye(me) - if me.isConnected - me.eyeUsed = Eyelink('EyeAvailable'); % get eye that's tracked - if me.eyeUsed == me.defaults.BINOCULAR % if both eyes are tracked - me.eyeUsed = me.defaults.LEFT_EYE; % use left eye - end - eyeUsed = me.eyeUsed; - else - me.eyeUsed = -1; - eyeUsed = me.eyeUsed; - end + eyeUsed = 'both'; end % =================================================================== - %> @brief draw the current eye position on the PTB display + %> @fn drawEyePosition + %> @brief draw the current eye position on the main PTB display %> % =================================================================== - function drawEyePosition(me) + function drawEyePosition(me, ~) if (me.isDummy || me.isConnected) && isa(me.screen,'screenManager') && me.screen.isOpen && ~isempty(me.x) && ~isempty(me.y) xy = toPixels(me,[me.x-me.offset.X me.y-me.offset.Y]); if me.isFix - if me.fixLength > me.fixation.time && ~me.isBlink - Screen('DrawDots', me.win, xy, 6, [0 1 0.25 1], [], 3); + if me.fixLength+me.fixBuffer > me.fixation.time && ~me.isBlink + Screen('DrawDots', me.screen.win, xy, me.eyeSize, [0 1 0.25 1], [], 3); elseif ~me.isBlink - Screen('DrawDots', me.win, xy, 6, [0.75 0 0.75 1], [], 3); + Screen('DrawDots', me.screen.win, xy, me.eyeSize, [0.75 0 0.75 1], [], 3); else - Screen('DrawDots', me.win, xy, 6, [0.75 0 0 1], [], 3); + Screen('DrawDots', me.screen.win, xy, me.eyeSize, [0.75 0 0 1], [], 3); end else if ~me.isBlink - Screen('DrawDots', me.win, xy, 6, [0.75 0.5 0 1], [], 3); + Screen('DrawDots', me.screen.win, xy, me.eyeSize, [0.75 0.5 0 1], [], 3); else - Screen('DrawDots', me.win, xy, 6, [0.75 0 0 1], [], 3); + Screen('DrawDots', me.screen.win, xy, me.eyeSize, [0.75 0 0 1], [], 3); end end end end - + % =================================================================== - %> @brief displays status message on tracker, only sets it if - %> message is not the previous message, so loop safe. + %> @brief draw the sampled eye positions in xAll yAll on the subject + %> screen %> % =================================================================== - function statusMessage(me,message) - if ~strcmpi(message,me.previousMessage) && me.isConnected - me.previousMessage = message; - Eyelink('Command',['record_status_message ''' message '''']); - if me.verbose; fprintf('-+-+-> Eyelink status message: %s\n',message);end + function drawEyePositions(me) + if (me.isDummy || me.isConnected) && isa(me.screen,'screenManager') && me.screen.isOpen && ~isempty(me.xAll) + xy = [me.xAll;me.yAll]; + drawDots(me.operatorScreen, xy, me.eyeSize, [0.5 0.9 0 0.2]); end end - + % =================================================================== - %> @brief send message to store in EDF data - %> - %> + %> @brief Send trial start information to tracker + %> + %> @param trialNumber the unique number or string for this trial % =================================================================== - function trackerMessage(me, message, varargin) - if me.isConnected - Eyelink('Message', message ); - if me.verbose; fprintf('-+-+-> EDF Message: %s\n',message);end + function trackerTrialStart(me, trialNumber, task, stimuli) + if ~exist('trialNumber','var'); trialNumber = 'unknown'; end + if strcmpi(me.type,'eyelink') + startRecording(me); + end + trackerMessage(me,'V_RT MESSAGE END_FIX END_RT'); % Eyelink commands + if isnumeric(trialNumber) + trackerMessage(me,sprintf('TRIALID %i', trialNumber)); + else + trackerMessage(me,sprintf('TRIALID %s', trialNumber)); + end + if exist('task','var') && isa(task,'taskSequence') + end + if exist('stimuli','var') && isa(stimuli,'metaStimulus') + + end + end - + % =================================================================== - %> @brief close the eyelink and cleanup, send EDF file if recording - %> is enabled - %> + %> @brief Send trial end information to tracker + %> + %> @param result a code at the trial end % =================================================================== - function close(me) - try - me.isConnected = false; - %me.isDummy = false; - me.eyeUsed = -1; - me.screen = []; - trackerClearScreen(me); - if me.isRecording == true && ~isempty(me.saveFile) - Eyelink('StopRecording'); - Eyelink('CloseFile'); - try - me.salutation('Close Method',sprintf('Receiving data file %s', me.tempFile),true); - status=Eyelink('ReceiveFile'); - if status > 0 - me.salutation('Close Method',sprintf('ReceiveFile status %d', status)); - end - if exist(me.tempFile, 'file') - me.salutation('Close Method',sprintf('Data file ''%s'' can be found in ''%s''', me.tempFile, strrep(pwd,'\','/')),true); - status = movefile(me.tempFile, me.saveFile,'f'); - if status == 1 - me.salutation('Close Method',sprintf('Data file copied to ''%s''', me.saveFile),true); - trackerDrawText(me,sprintf('Data file copied to ''%s''', me.saveFile)); - end - end - catch ME - me.salutation('Close Method',sprintf('Problem receiving data file ''%s''', me.tempFile),true); - disp(ME.message); - end - end - catch ME - me.salutation('Close Method','Couldn''t stop recording, forcing shutdown...',true) - trackerClearScreen(me); - Eyelink('Shutdown'); - me.error = ME; - me.salutation(ME.message); + function trackerTrialEnd(me, result) + if ~exist('result','var'); result = -100; end + trackerMessage(me,'END_RT'); %send END_RT message to tracker + if isnumeric(result) + trackerMessage(me,sprintf('TRIAL_RESULT %i', result)); %send TRIAL_RESULT message to tracker + else + trackerMessage(me,sprintf('TRIAL_RESULT %s', result)); %send TRIAL_RESULT message to tracker + end + if strcmpi(me.type,'eyelink') + stopRecording(me); % stop recording in eyelink + setOffline(me); % set eyelink offline end - Eyelink('Shutdown'); - me.isConnected = false; - me.isRecording = false; - me.eyeUsed = -1; - me.screen = []; end - + % =================================================================== %> @brief draw the background colour %> % =================================================================== function trackerClearScreen(me) - if ~me.isConnected; return; end - Eyelink('Command', 'clear_screen 0'); + if me.isOff || ~me.isConnected || ~me.operatorScreen.isOpen; return; end + drawBackground(me.operatorScreen); + end + + % =================================================================== + %> @brief flip the tracker display, always use dontsync + %> + %> remember: dontclear affects the NEXT flip, not this one! + % =================================================================== + function trackerFlip(me, dontclear, force) + if me.isOff || ~me.isConnected || ~me.operatorScreen.isOpen; return; end + if ~exist('dontclear','var') || isempty(dontclear); dontclear = 1; end + if ~exist('force','var') || isempty(force); force = false; end + + me.flipTick = me.flipTick + 1; + if force || doFlip(me); me.flipTick = 1; end + if me.flipTick ~= 1; return; end + + if dontclear ~= 1; dontclear = 0; end + % Screen('Flip', windowPtr [, when] [, dontclear] [, dontsync] [, multiflip]); + me.operatorScreen.flip([], dontclear, 2); end % =================================================================== %> @brief draw general status %> % =================================================================== - function trackerDrawStatus(me, comment, ts, dontClear) - if ~me.isConnected; return; end - if ~exist('comment','var'); comment=''; end - if ~exist('ts','var'); ts = []; end - if ~exist('dontClear','var');dontClear = false; end - if dontClear==false; trackerClearScreen(me); end - trackerDrawExclusion(me); + function trackerDrawStatus(me, comment, stimPos, dontClear, dontFlip) + if me.isOff || ~me.isConnected || ~me.operatorScreen.isOpen; return; end + if ~exist('comment','var') || isempty(comment); comment=''; end + if ~exist('stimPos','var'); stimPos = []; end + if ~exist('dontClear','var') || isempty(dontClear); dontClear = 1; end + if ~exist('dontFlip','var') || isempty(dontFlip); dontFlip = true; end + + if dontClear == 0; trackerClearScreen(me); end trackerDrawFixation(me); - if ~isempty(ts);trackerDrawStimuli(me, ts);end - if ~isempty(comment);trackerDrawText(me, comment);end + drawGrid(me.operatorScreen); + if ~isempty(me.exclusionZone); trackerDrawExclusion(me); end + if ~isempty(stimPos); trackerDrawStimuli(me, stimPos); end + if ~isempty(comment); trackerDrawText(me, comment); end + if ~isempty(me.xAll); trackerDrawEyePositions(me); end + if me.offset.X ~= 0 || me.offset.Y ~= 0; drawDriftOffset(me); end + if dontFlip == false && dontClear == 0 + trackerFlip(me, 0, true); + elseif dontClear == 0 + trackerFlip(me, 0, false); + else + trackerFlip(me, 1, false); + end + end - + % =================================================================== %> @brief draw the stimuli boxes on the tracker display %> % =================================================================== - function trackerDrawStimuli(me, ts, dontClear, convertToPixels) - if ~me.isConnected; return; end - if exist('ts','var') && isstruct(ts) && ~isempty(fields(ts)) + function trackerDrawStimuli(me, ts, dontClear) + if me.isOff || ~me.isConnected || ~me.operatorScreen.isOpen; return; end + if exist('ts','var') && isstruct(ts) && isfield(ts,'x') me.stimulusPositions = ts; else return end if ~exist('dontClear','var') - dontClear = true; - end - if ~exist('convertToPixels','var') - convertToPixels = false; + dontClear = 1; + elseif islogical(dontClear) + dontClear = double(dontClear); end - if dontClear==false; Eyelink('Command', 'clear_screen 0'); end + if dontClear == 0; trackerClearScreen(me); end for i = 1:length(me.stimulusPositions) - x = me.stimulusPositions(i).x; - y = me.stimulusPositions(i).y; + x = me.stimulusPositions(i).x; + y = me.stimulusPositions(i).y; size = me.stimulusPositions(i).size; if isempty(size); size = 1; end - rect = [0 0 size size]; - rect = CenterRectOnPoint(rect, x, y); - if convertToPixels; rect = toPixels(me, rect,'rect'); end - rect = round(rect); + %fprintf('\n!!!===>>> eT Stim: %.2fx %.2fy %.2fsz\n',x,y,size) if me.stimulusPositions(i).selected == true - Eyelink('Command', 'draw_box %d %d %d %d 10', rect(1), rect(2), rect(3), rect(4)); + drawBox(me.operatorScreen,[x; y],size,[0.5 1 0 0.5]); else - Eyelink('Command', 'draw_box %d %d %d %d 11', rect(1), rect(2), rect(3), rect(4)); + drawBox(me.operatorScreen,[x; y],size,[0.6 0.6 0.3 0.5]); end end end @@ -1223,337 +1023,96 @@ classdef eyelinkManager < optickaCore %> % =================================================================== function trackerDrawFixation(me) - if ~me.isConnected; return; end - size = me.fixation.radius * 2; - rect = [0 0 size size]; - for i = 1:length(me.fixation.X) - nrect = CenterRectOnPoint(rect, me.fixation.X(i), me.fixation.Y(i)); - nrect = round(toPixels(me, nrect, 'rect')); - Eyelink('Command', 'draw_filled_box %d %d %d %d 10', nrect(1), nrect(2), nrect(3), nrect(4)); + if me.isOff || ~me.isConnected || ~me.operatorScreen.isOpen; return; end + if isscalar(me.fixation.radius) + drawSpot(me.operatorScreen,me.fixation.radius,[0.5 0.6 0.5 0.7],me.fixation.X,me.fixation.Y); + else + rect = [me.fixation.X - me.fixation.radius(1), ... + me.fixation.Y - me.fixation.radius(2), ... + me.fixation.X + me.fixation.radius(1), ... + me.fixation.Y + me.fixation.radius(2)]; + drawRect(me.operatorScreen,rect,[0.5 0.6 0.5 0.7]); end end - + % =================================================================== %> @brief draw the fixation box on the tracker display %> % =================================================================== function trackerDrawExclusion(me) - if ~me.isConnected; return; end - if ~isempty(me.exclusionZone) && size(me.exclusionZone,2)==4 - for i = 1:size(me.exclusionZone,1) - rect = round(toPixels(me, me.exclusionZone(i,:))); - % exclusion zone is [-degX +degX -degY +degY], but rect is left,top,right,bottom - Eyelink('Command', 'draw_box %d %d %d %d 12', rect(1), rect(3), rect(2), rect(4)); - end + if isempty(me.exclusionZone) || me.isOff || ~me.isConnected || ~me.operatorScreen.isOpen; return; end + for i = 1:size(me.exclusionZone,1) + drawRect(me.operatorScreen, [me.exclusionZone(1), ... + me.exclusionZone(3), me.exclusionZone(2), ... + me.exclusionZone(4)],[0.7 0.6 0.6 0.5]); end end - % =================================================================== - %> @brief draw the fixation box on the tracker display - %> - % =================================================================== - function trackerDrawText(me,textIn) - if ~me.isConnected; return; end - if exist('textIn','var') && ~isempty(textIn) - xDraw = toPixels(me, 0, 'x'); - yDraw = toPixels(me, 0, 'y'); - Eyelink('Command', 'draw_text %i %i %d %s', xDraw, yDraw, 3, textIn); - end - end % =================================================================== - %> @brief check what mode the eyelink is in - %> ##define IN_UNKNOWN_MODE 0 - %> #define IN_IDLE_MODE 1 - %> #define IN_SETUP_MODE 2 - %> #define IN_RECORD_MODE 4 - %> #define IN_TARGET_MODE 8 - %> #define IN_DRIFTCORR_MODE 16 - %> #define IN_IMAGE_MODE 32 - %> #define IN_USER_MENU 64 - %> #define IN_PLAYBACK_MODE 256 - %> #define LINK_TERMINATED_RESULT -100 - % =================================================================== - function mode = currentMode(me) - if me.isConnected - mode = Eyelink('CurrentMode'); - else - mode = -100; - end - end - - % =================================================================== - %> @brief Sync time message for EDF file + %> @brief draw the fixation position on the tracker display %> % =================================================================== - function syncTime(me) - if ~me.isConnected; return; end - Eyelink('Message', 'SYNCTIME'); %zero-plot time for EDFVIEW - end - - % =================================================================== - %> @brief Get offset between tracker and display computers - %> - % =================================================================== - function offset = getTimeOffset(me) - if me.isConnected - offset = Eyelink('TimeOffset'); - me.currentOffset = offset; + function trackerDrawEyePosition(me) + if isempty(me.x) || isempty(me.y) || me.isOff || ~me.isConnected || ~me.operatorScreen.isOpen; return; end + if me.isFix + if me.fixLength+me.fixBuffer > me.fixation.time + drawSpot(me.operatorScreen,0.5,[0 1 0.25 0.7],me.x,me.y); + else + drawSpot(me.operatorScreen,0.5,[0.75 0.25 0.75 0.7],me.x,me.y); + end else - offset = 0; + drawSpot(me.operatorScreen,0.5,[0.7 0.5 0 0.5],me.x,me.y); end end % =================================================================== - %> @brief Get offset between tracker and display computers + %> @brief draw the sampled eye positions in xAll yAll %> % =================================================================== - function time = getTrackerTime(me) - if me.isConnected - time = Eyelink('TrackerTime'); - me.trackerTime = time; - else - time = 0; + function trackerDrawEyePositions(me) + if me.isOff || ~me.isConnected || ~me.operatorScreen.isOpen; return; end + if ~isempty(me.xAll) && ~isempty(me.yAll) && (length(me.xAll)==length(me.yAll)) + xy = [me.xAll;me.yAll]; + drawDots(me.operatorScreen,xy,0.4,[0.5 0.9 0.2 0.2]); end end - + % =================================================================== - %> @brief automagically turn pixels to degrees + %> @brief draw the fixation box on the tracker display %> % =================================================================== - function set.x(me,in) - me.x = toDegrees(me,in,'x'); %#ok<*MCSUP> + function trackerDrawText(me,textIn) + if ~exist('textIn','var') || me.isOff || ~me.isConnected || ~me.operatorScreen.isOpen; return; end + drawText(me.operatorScreen, textIn); end - + % =================================================================== - %> @brief automagically turn pixels to degrees + %> @brief draw the fixation box on the tracker display %> % =================================================================== - function set.y(me,in) - me.y = toDegrees(me,in,'y'); + function trackerDrawCross(me,size) + if ~exist('textIn','var') || me.isOff || ~me.isConnected || ~me.operatorScreen.isOpen; return; end + drawCross(me.operatorScreen, size); end - - % =================================================================== - %> @brief runs a demo of the eyelink, tests this class - %> - % =================================================================== - function runDemo(me,forcescreen) - KbName('UnifyKeyNames') - stopkey = KbName('Q'); - nextKey = KbName('SPACE'); - calibkey = KbName('C'); - driftkey = KbName('D'); - offsetkey = KbName('O'); - oldx = me.fixation.X; - oldy = me.fixation.Y; - oldexc = me.exclusionZone; - oldfixinit = me.fixInit; - me.recordData = true; %lets save an EDF file - %set up a figure to plot eye position - figure;plot(0,0,'ro');ax=gca;hold on;xlim([-20 20]);ylim([-20 20]);set(ax,'YDir','reverse'); - title('eyelinkManager Demo');xlabel('X eye position (deg)');ylabel('Y eye position (deg)');grid on;grid minor;drawnow; - % DEMO EXPERIMENT: - try - %open screen manager and dots stimulus - s = screenManager('debug',true,'pixelsPerCm',27,'distance',66); - if exist('forcescreen','var'); s.screen = forcescreen; end - s.backgroundColour = [0.5 0.5 0.5 0]; %s.windowed = [0 0 900 900]; - o = dotsStimulus('size',me.fixation.radius(1)*2,'speed',2,'mask',true,'density',50); %test stimulus - open(s); % open our screen - setup(o,s); % setup our stimulus with our screen object - - initialise(me,s); % initialise eyelink with our screen - if ~me.isDummy && ~me.isConnected - reset(o); - close(s); - error('Could not connect to Eyelink or use Dummy mode...') - end - %ListenChar(-1); % capture the keyboard settings - setup(me); % setup + calibrate the eyelink - - % define our fixation widow and stimulus for first trial - % x,y,inittime,fixtime,radius,strict - me.updateFixationValues([0 -10],[0 -10],3,1,1,true); - o.sizeOut = me.fixation.radius(1)*2; - o.xPositionOut = me.fixation.X; - o.yPositionOut = me.fixation.Y; - ts.x = me.fixation.X; %ts is a simple structure that we can pass to eyelink to draw on its screen - ts.y = me.fixation.Y; - ts.size = o.sizeOut; - ts.selected = true; - - % setup an exclusion zone where eye is not allowed - me.exclusionZone = [8 15 10 15]; - exc = me.toPixels(me.exclusionZone); - exc = [exc(1) exc(3) exc(2) exc(4)]; %psychrect=[left,top,right,bottom] - - setOffline(me); %Eyelink('Command', 'set_idle_mode'); - trackerClearScreen(me); % clear eyelink screen - trackerDrawFixation(me); % draw fixation window on tracker - trackerDrawStimuli(me,ts); % draw stimulus on tracker - - Screen('TextSize', s.win, 18); - HideCursor(s.win); - Priority(MaxPriority(s.win)); - blockLoop = true; - a = 1; - - while blockLoop - % some general variables - trialLoop = true; - b = 1; - xst = []; - yst = []; - correct = false; - % !!! these messages define the trail start in the EDF for - % offline analysis - edfMessage(me,'V_RT MESSAGE END_FIX END_RT'); - edfMessage(me,['TRIALID ' num2str(a)]); - % start the eyelink recording data for this trail - startRecording(me); - % this draws the text to the tracker info box - statusMessage(me,sprintf('DEMO Running Trial=%i X Pos = %g | Y Pos = %g | Radius = %g',a,me.fixation.X,me.fixation.Y,me.fixation.radius)); - WaitSecs('YieldSecs',0.25); - vbl=flip(s); - syncTime(me); - while trialLoop - Screen('FillRect',s.win,[0.7 0.7 0.7 0.5],exc); Screen('DrawText',s.win,'Exclusion Zone',exc(1),exc(2),[0.8 0.8 0.8]); - drawSpot(s,me.fixation.radius,[0.5 0.6 0.5 0.25],me.fixation.X,me.fixation.Y); - drawCross(s,0.5,[1 1 0.5],me.fixation.X,me.fixation.Y); - draw(o); - drawGrid(s); - drawScreenCenter(s); - - % get the current eye position and save x and y for local - % plotting - getSample(me); xst(b)=me.x - me.offset.X; yst(b)=me.y - me.offset.Y; - - % if we have an eye position, plot the info on the display - % screen - if ~isempty(me.currentSample) - [~, ~, searching, window, exclusion, fixinit] = isFixated(me); - x = me.toPixels(me.x - me.offset.X,'x'); %#ok<*PROP> - y = me.toPixels(me.y - me.offset.Y,'y'); - txt = sprintf('Q = finish, SPACE = next. X = %3.1f / %2.2f | Y = %3.1f / %2.2f | RADIUS = %s | TIME = %.1f | FIX = %.1f | WIN = %i | SEARCH = %i | BLINK = %i | EXCLUSION = %i | FAIL-INIT = %i',... - x, me.x - me.offset.X, y, me.y - me.offset.Y, sprintf('%1.1f ',me.fixation.radius), ... - me.fixTotal, me.fixLength, window, searching, me.isBlink, exclusion, fixinit); - Screen('DrawText', s.win, txt, 10, 10,[1 1 1]); - drawEyePosition(me); - end - - % tell PTB we've finished drawing - finishDrawing(s); - % animate out stimulus - animate(o); - % flip the screen - vbl=Screen('Flip',s.win, vbl + s.screenVals.halfisi); - - % check the keyboard - [~, ~, keyCode] = KbCheck(-1); - if keyCode(stopkey); trialLoop = 0; blockLoop = 0; break; end - if keyCode(nextKey); trialLoop = 0; correct = true; break; end - if keyCode(calibkey); trackerSetup(me); break; end - if keyCode(driftkey); driftCorrection(me); break; end - if keyCode(offsetkey); driftOffset(me); break; end - % send a message for the EDF after 60 frames - if b == 60; edfMessage(me,'END_FIX');end - b=b+1; - end - % tell EDF end of reaction time portion - edfMessage(me,'END_RT'); - if correct - edfMessage(me,'TRIAL_RESULT 1'); - else - edfMessage(me,'TRIAL_RESULT 0'); - end - % stop recording data - stopRecording(me); - setOffline(me); %Eyelink('Command', 'set_idle_mode'); - resetFixation(me); - % set up the fix init system, whereby the subject must - % remain a certain time at the origin of the eye - % position before saccading to next target, use previous fixation location. - %me.fixInit.X = me.fixation.X; - %me.fixInit.Y = me.fixation.Y; - %me.fixInit.time = 0.1; - %me.fixInit.radius = 3; - - % prepare a random position for next trial - me.updateFixationValues([randi([-5 5]) -10],[randi([-5 5]) -10],[],[],randi([1 5])); - o.sizeOut = me.fixation.radius*2; - %me.fixation.radius = [me.fixation.radius me.fixation.radius]; - o.xPositionOut = me.fixation.X(1); - o.yPositionOut = me.fixation.Y(1); - update(o); - % use this struct for the parameters to draw stimulus - % to screen - ts.x = me.fixation.X(1); - ts.y = me.fixation.Y(1); - ts.size = me.fixation.radius; - ts.selected = true; - % clear tracker display - trackerDrawStimuli(me,ts,true); - trackerDrawFixation(me); - % plot eye position for last trial and ITI - plot(ax,xst,yst);drawnow; - while GetSecs <= vbl + 1 - % check the keyboard - [~, ~, keyCode] = KbCheck(-1); - if keyCode(calibkey); trackerSetup(me); break; end - if keyCode(driftkey); driftCorrection(me); break; end - if keyCode(offsetkey); driftOffset(me); break; end - end - a=a+1; - end - % clear tracker display - trackerClearScreen(me); - trackerDrawText(me,'FINISHED eyelinkManager Demo!!!'); - ListenChar(0);Priority(0);ShowCursor;RestrictKeysForKbCheck([]) - close(s); - close(me); - clear s o - if ~me.isDummy - an = questdlg('Do you want to load the data and plot it?'); - if strcmpi(an,'yes') - if ~isdeployed; commandwindow; end - evalin('base',['eA=eyelinkAnalysis(''dir'',''' ... - me.paths.savedData ''', ''file'',''myData.edf'');eA.parseSimple;eA.plot']); - end - end - me.resetFixation; - me.resetOffset; - me.fixation.X = oldx; - me.fixation.Y = oldy; - me.exclusionZone = oldexc; - me.fixInit = oldfixinit; - catch ME - me.fixation.X = oldx; - me.fixation.Y = oldy; - me.exclusionZone = oldexc; - me.fixInit = oldfixinit; - me.resetFixation; - me.resetOffset; - ListenChar(0);Priority(0);ShowCursor;RestrictKeysForKbCheck([]) - me.salutation('runDemo ERROR!!!') - Eyelink('Shutdown'); - try close(s); end - sca; - try close(me); end - clear s o - me.error = ME; - me.salutation(ME.message); - rethrow(ME); - end - - end - end%-------------------------END PUBLIC METHODS--------------------------------% %======================================================================= methods (Hidden = true) %------------------HIDDEN METHODS %======================================================================= + + function doflip = doFlip(me) + if me.flipTick >= me.skipFlips; doflip = true; else; doflip = false; end + end + % =================================================================== + %> @brief send message to store in EDF data + % =================================================================== + function edfMessage(me, message) + me.trackerMessage(message) + end + % =================================================================== %> @brief TODO %> @@ -1570,88 +1129,170 @@ classdef eyelinkManager < optickaCore end - % =================================================================== - %> @brief send message to store in EDF data - %> - %> - % =================================================================== - function edfMessage(me, message) - if me.isConnected - Eyelink('Message', message ); - if me.verbose; fprintf('-+-+->EDF Message: %s\n',message);end - end - end - - function trackerFlip(me, varargin) - - end - - function trackerDrawEyePosition(me) - - end - - function trackerDrawEyePositions(me) - - end - end %======================================================================= - methods (Access = private) %------------------PRIVATE METHODS + methods (Access = protected) %------------------PROTECTED METHODS %======================================================================= - + + function drawValidationResults(me, n) + if isempty(me.validationData); return; end + try %#ok<*TRYNC> + if ~exist('n','var') || n > length(me.validationData); n = length(me.validationData); end + vd = me.validationData(n); + s = me.operatorScreen; + for jj = 1:length(vd.vpos) + if ~strcmpi(vd.type,'sample') + thisPos = [vd.vpos(jj,1),vd.vpos(jj,2)]; + drawCross(s, 1,[],thisPos(1),thisPos(2)); + if ~isempty(vd.data{jj}) && size(vd.data{jj},1)==2 + x = vd.data{jj}(1,:); y = vd.data{jj}(2,:); + xm = median(x); ym = median(y); + xd = abs(vd.vpos(jj,1) - xm); + yd = abs(vd.vpos(jj,2) - ym); + xv = rmse( x - xm, 0); + yv = rmse( y - ym, 0); + txt = sprintf('A:%.1g %.1g P:%.2g %.2g', xd, yd, xv, yv); + a = 1; + xyl = zeros(2,length(vd.data{jj})*2); + for i = 1:length(vd.data{jj}) + xyl(:,a) = vd.data{jj}(:,i); + xyl(:,a+1) = thisPos; + a = a + 2; + end + drawLines(s,xyl,0.1,[0.95 0.65 0 0.1]); + drawDotsDegs(s,vd.dataS{jj},0.5,[1 1 0 0.35]); + drawText(s,txt,xm-2.5,ym+0.75); + end + else + vpos = me.calibration.valPositions; + for kk = 1:length(vpos) + drawCross(s, 1,[],vpos(kk,1),vpos(kk,2)); + end + if ~isempty(vd.data{jj}) && size(vd.data{jj},1)==2 + x = vd.data{jj}(1,:); y = vd.data{jj}(2,:); + xm = median(x); ym = median(y); + drawDotsDegs(s,vd.data{jj},0.5,[1 0.6 0 0.25]); + t = sprintf('#%i %.2gx %.2gy',jj,xm,ym); + try + drawText(s,t,xm-2.5,ym+0.75); + end + end + end + end + end + end + + % =================================================================== - %> @brief + %> @brief to visual degrees from pixels %> % =================================================================== - function out = toDegrees(me,in,axis) - if ~exist('axis','var');axis='';end + function drawDriftOffset(me) + if me.offset.X ~= 0 || me.offset.Y ~= 0 + drawCross(me.operatorScreen,0.5,[1 1 1],me.offset.X,me.offset.Y); + drawText(me.operatorScreen,sprintf('Offset: X = %.2f Y = %.2f\n',me.offset.X,me.offset.Y),me.offset.X,me.offset.Y); + end + end + % =================================================================== + %> @brief to visual degrees from pixels + %> + % =================================================================== + function out = toDegrees(me,in,axis,inputtype) + if ~exist('axis','var') || isempty(axis); axis=''; end + if ~exist('inputtype','var') || isempty(inputtype); inputtype = 'pixels'; end + out = 0; + if length(in)>2; return; end switch axis case 'x' - out = (in - me.screen.xCenter) / me.ppd_; + in = in(1); + switch inputtype + case 'pixels' + out = (in - me.screen.xCenter) / me.ppd_; + case 'relative' + out = (in - 0.5) * (me.screen.screenVals.width /me.ppd_); + end case 'y' - out = (in - me.screen.yCenter) / me.ppd_; + in = in(1); + switch inputtype + case 'pixels' + out = (in - me.screen.yCenter) / me.ppd_; return + case 'relative' + out = (in - 0.5) * (me.screen.screenVals.height /me.ppd_); + end otherwise - if length(in)==2 - out(1) = (in(1) - me.screen.xCenter) / me.ppd_; - out(2) = (in(2) - me.screen.yCenter) / me.ppd_; - else - out = 0; + switch inputtype + case 'pixels' + out(1) = (in(1) - me.screen.xCenter) / me.ppd_; + out(2) = (in(2) - me.screen.yCenter) / me.ppd_; + case 'relative' + out(1) = (in - 0.5) * (me.screen.screenVals.width /me.ppd_); + out(2) = (in - 0.5) * (me.screen.screenVals.height /me.ppd_); end end end % =================================================================== - %> @brief - %> + %> @brief to pixels from visual degrees / relative + %> input can be [x] [y] [-x -y +x +y]('rect') [xy] or [-x +x -y +y] % =================================================================== - function out = toPixels(me,in,axis) - if ~exist('axis','var');axis='';end + function out = toPixels(me, in, axis, inputtype) + if ~exist('axis','var') || isempty(axis); axis=''; end + if ~exist('inputtype','var') || isempty(inputtype); inputtype = 'degrees'; end + out = zeros(size(in)); + if length(in)>4; return; end switch axis - case '' - if length(in)==4 - out(1:2) = (in(1:2) * me.ppd_) + me.screen.xCenter; - out(3:4) = (in(3:4) * me.ppd_) + me.screen.yCenter; - elseif length(in)==2 - out(1) = (in(1) * me.ppd_) + me.screen.xCenter; - out(2) = (in(2) * me.ppd_) + me.screen.yCenter; - else - out = 0; - end - case 'rect' - out(1) = (in(1) * me.ppd_) + me.screen.xCenter; - out(2) = (in(2) * me.ppd_) + me.screen.yCenter; - out(3) = (in(3) * me.ppd_) + me.screen.xCenter; - out(4) = (in(4) * me.ppd_) + me.screen.yCenter; case 'x' - out = (in * me.ppd_) + me.screen.xCenter; + switch inputtype + case 'degrees' + out = (in * me.ppd_) + me.screen.xCenter; + case 'relative' + out = in * me.screen.screenVals.width; + end case 'y' - out = (in * me.ppd_) + me.screen.yCenter; + switch inputtype + case 'degrees' + out = (in * me.ppd_) + me.screen.yCenter; + case 'relative' + out = in * me.screen.screenVals.height; + end + case 'rect' + switch inputtype + case 'degrees' + w = ([in(1) in(3)] * me.ppd_) + me.screen.xCenter; + h = ([in(2) in(4)] * me.ppd_) + me.screen.yCenter; + out = [w(1) h(1) w(2) h(2)]; + case 'relative' + w = [in(1) in(3)] * me.screen.screenVals.width; + h = [in(2) in(4)] * me.screen.screenVals.height; + out = [w(1) h(1) w(2) h(2)]; + end + otherwise + switch inputtype + case 'degrees' + if length(in)==2 + out(1) = (in(1) * me.ppd_) + me.screen.xCenter; + out(2) = (in(2) * me.ppd_) + me.screen.yCenter; + elseif length(in)==4 + out(1:2) = (in(1:2) * me.ppd_) + me.screen.xCenter; + out(3:4) = (in(3:4) * me.ppd_) + me.screen.yCenter; + end + case 'relative' + if length(in)==2 + out(1) = in(1) * me.screen.screenVals.width; + out(2) = in(2) * me.screen.screenVals.height; + elseif length(in)==4 + out(1:2) = in(1:2) * me.screen.screenVals.width; + out(3:4) = in(3:4) * me.screen.screenVals.height; + end + end end end + + end end diff --git a/eyetracker/eyetrackerSmooth.m b/eyetracker/eyetrackerSmooth.m new file mode 100644 index 0000000000000000000000000000000000000000..1cbc62c38190aff61c93450aedc727b91b1e3bcd --- /dev/null +++ b/eyetracker/eyetrackerSmooth.m @@ -0,0 +1,145 @@ +% ======================================================================== +classdef eyetrackerSmooth < handle +%> @class eyetrackerSmooth +%> @brief Smoothes incoming eye sample data +%> +%> Copyright ©2014-2023 Ian Max Andolina — released: LGPL3, see LICENCE.md +% ======================================================================== + + properties + %> options for online smoothing of peeked data + % %> method = {'median','heuristic','heuristic2', 'savitsky-golay'} + smoothing = struct('nSamples', 8, 'method', 'median', 'window', 3,... + 'eyes', 'both', 'sampleRate', 500) + end + + properties (SetAccess = protected, GetAccess = public, Dependent = true) + %> calculates the smoothing in ms + smoothingTime double + end + + %======================================================================== + methods %----------------------------PUBLIC METHODS + %======================================================================== + + % =================================================================== + %> @brief This is the constructor for this class + %> + % =================================================================== + function me = eyetrackerSmooth() + if isprop(me,'sampleRate') + me.smoothing.sampleRate = me.sampleRate; %#ok<*MCNPN> + end + end + + % =================================================================== + %> @brief calculate smoothing Time in ms + %> + % =================================================================== + function value = get.smoothingTime(me) + value = (1000 / me.smoothing.sampleRate) * me.smoothing.nSamples; + end + + end%-------------------------END PUBLIC METHODS--------------------------------% + + %============================================================================ + methods (Hidden = true) %--HIDDEN METHODS + %============================================================================ + + % =================================================================== + %> @brief smooth data in M x N where M = 2 (x&y trace) or M = 4 is x&y + %> for both eyes. Output is 2 x 1 x + y average position + %> + % =================================================================== + function out = doSmoothing(me,in) + if size(in,2) > me.smoothing.window * 2 + switch me.smoothing.method + case 'median' + out = movmedian(in,me.smoothing.window,2); + out = median(out, 2); + case {'heuristic','heuristic1'} + out = eyetrackerSmooth.heuristicFilter(in,1); + out = median(out, 2); + case 'heuristic2' + out = eyetrackerSmooth.heuristicFilter(in,2); + out = median(out, 2); + case {'sg','savitzky-golay'} + out = sgolayfilt(in,1,me.smoothing.window,[],2); + out = median(out, 2); + otherwise + out = median(in, 2); + end + elseif size(in, 2) > 1 + out = median(in, 2); + else + out = in; + end + if size(out,1)==4 % XY for both eyes, combine together. + out = [mean([out(1) out(3)]); mean([out(2) out(4)])]; + end + if length(out) ~= 2 + out = [NaN NaN]; + end + end + + end + + %============================================================================ + methods (Static) %--STATIC METHODS + %============================================================================ + + % =================================================================== + %> @brief Stampe 1993 heuristic filter as used by Eyelink + %> + %> @param indata - input data + %> @param level - 1 = filter level 1, 2 = filter level 1+2 + %> @param steps - we step every # steps along the in data, changes the filter characteristics, 3 is the default (filter 2 is #+1) + %> @out out - smoothed data + % =================================================================== + function out = heuristicFilter(indata,level,steps) + if ~exist('level','var'); level = 1; end %filter level 1 [std] or 2 [extra] + if ~exist('steps','var'); steps = 3; end %step along the data every n steps + out=zeros(size(indata)); + for k = 1:2 % x (row1) and y (row2) eye samples + in = indata(k,:); + %filter 1 from Stampe 1993, see Fig. 2a + if level > 0 + for i = 1:steps:length(in)-2 + x = in(i); x1 = in(i+1); x2 = in(i+2); %#ok<*PROPLC> + if ((x2 > x1) && (x1 < x)) || ((x2 < x1) && (x1 > x)) + if abs(x1-x) < abs(x2-x1) %i is closest + x1 = x; + else + x1 = x2; + end + end + x2 = x1; + x1 = x; + in(i)=x; in(i+1) = x1; in(i+2) = x2; + end + end + %filter2 from Stampe 1993, see Fig. 2b + if level > 1 + for i = 1:steps+1:length(in)-3 + x = in(i); x1 = in(i+1); x2 = in(i+2); x3 = in(i+3); + if x2 == x1 && (x == x1 || x2 == x3) + x3 = x2; + x2 = x1; + x1 = x; + else %x2 and x1 are the same, find closest of x2 or x + if abs(x1 - x3) < abs(x1 - x) + x2 = x3; + x1 = x3; + else + x2 = x; + x1 = x; + end + end + in(i)=x; in(i+1) = x1; in(i+2) = x2; in(i+3) = x3; + end + end + out(k,:) = in; + end + end + end +end \ No newline at end of file diff --git a/eyetracker/iRecManager.m b/eyetracker/iRecManager.m new file mode 100644 index 0000000000000000000000000000000000000000..26bd64f13141a63d52d5b49e4d899b669364ce15 --- /dev/null +++ b/eyetracker/iRecManager.m @@ -0,0 +1,1094 @@ +% ======================================================================== +classdef iRecManager < eyetrackerCore & eyetrackerSmooth +%> @class iRecManager +%> @brief Manages the iRec eyetrackers https://staff.aist.go.jp/k.matsuda/iRecHS2/index_e.html +%> +%> The eyetrackerCore methods enable the user to test for common behavioural +%> eye tracking tasks with single commands. +%> +%> Multiple fixation windows can be assigned, the windows can be either +%> circular or rectangular. In addition rectangular exclusion windows can ensure a +%> subject doesn't saccade to particular parts of the screen. fixInit allows +%> you to define a minimum time with which the subject must initiate a +%> saccade away from a position (which stops a subject cheating in a trial). +%> +%> To initiate a task we normally place a fixation cross on the screen and +%> ask the subject to saccade to the cross and maintain fixation for a +%> particular duration. This is achieved using +%> testSearchHoldFixation('yes','no'), using the properties: +%> fixation.initTime to time how long the subject has to saccade into the +%> window, fixation.time for how long they must maintain fixation, +%> fixation.radius for the radius around fixation.X and fixation.Y position. +%> The method returns the 'yes' string if the rules are matched, and 'no' if +%> they are not, thus enabling experiment code to simply define what +%> happened. Other methods include isFixated(), testFixationTime(), +%> testHoldFixation(). +%> +%> Copyright ©2014-2023 Ian Max Andolina — released: LGPL3, see LIv12345c12345CENCE.md +% ======================================================================== + +%-----------------CONTROLLED PROPERTIES-------------% + properties (SetAccess = protected, GetAccess = public) + %> type of eyetracker + type = 'iRec' + %> TCP interface objec (dataConnection class) + tcp = dataConnection + %> udp interface object (dataConnection class) + udp = dataConnection + end + + %---------------PUBLIC PROPERTIES---------------% + properties + %> initial setup and calibration values + calibration = struct(... + 'ip', '127.0.0.1',... + 'udpport', 35000,... % used to send messages + 'tcpport', 35001,... % used to send commands + 'stimulus','animated',... % calibration stimulus can be animated, movie, image, pupilcore + 'size', 2,... % size of calibration target in degrees + 'movie', [],... % if movie optionally pass a filename + 'filePath', [],... + 'audioFeedback', true, ... + 'calPositions', [-12 0; 0 -12; 0 0; 0 12; 12 0],... + 'valPositions', [-12 0; 0 -12; 0 0; 0 12; 12 0],... + 'manual', false) + %> WIP we can optionally drive physical LEDs for calibration, each LED + %> is triggered by the me.calibration.calPositions order + useLEDs = false + end + + properties (Hidden = true) + % for led calibration, which arduino pin to start from + startPin = 3 + % stimulus used for calibration + calStim = [] + end + + %--------------------PROTECTED PROPERTIES----------% + properties (SetAccess = protected, GetAccess = protected) + rawSamples = [] + % screen values taken from screenManager + sv = [] + %> tracker time stamp + systemTime = 0 + %> allowed properties passed to object upon construction + allowedProperties = {'calibration', 'useLEDs'} + end + + %======================================================================= + methods %------------------PUBLIC METHODS + %======================================================================= + + % =================================================================== + function me = iRecManager(varargin) + %> @fn iRecManager(varargin) + %> + %> iRecManager CONSTRUCTOR + %> + %> @param varargin can be passed as a structure, or name+arg pairs + %> @return instance of the class. + % =================================================================== + args = optickaCore.addDefaults(varargin,struct('name','iRec',... + 'useOperatorScreen',true,'sampleRate',500,'saveFile','')); + me=me@eyetrackerCore(args); %we call the superclass constructor first + me.parseArgs(args, me.allowedProperties); + me.smoothing.sampleRate = me.sampleRate; + + me.udp.protocol = 'udp'; + me.udp.rAddress = me.calibration.ip; + me.udp.rPort = me.calibration.udpport; + + me.tcp.protocol = 'tcp'; + me.tcp.rAddress = me.calibration.ip; + me.tcp.rPort = me.calibration.tcpport; + + end + + % =================================================================== + function success = initialise(me, sM, sM2) + %> @fn initialise(me, sM, sM2) + %> @brief initialise + %> + %> @param sM - screenManager for the subject + %> @param sM2 - a second screenManager used for operator, if + %> none is provided a default will be made. + % =================================================================== + success = false; + if me.isOff; me.isConnected = false; success = true; return; end + + [rM, aM] = optickaCore.initialiseGlobals(); + + if ~exist('sM','var') || isempty(sM) + if isempty(me.screen) || ~isa(me.screen,'screenManager') + me.screen = screenManager; + end + else + me.screen = sM; + end + me.ppd_ = me.screen.ppd; + if me.screen.isOpen; me.win = me.screen.win; end + + me.rawSamples = round(0.2 / (1/me.sampleRate)); + + if me.screen.screen > 0 + oscreen = me.screen.screen - 1; + else + oscreen = 0; + end + if exist('sM2','var') + me.operatorScreen = sM2; + elseif isempty(me.operatorScreen) + me.operatorScreen = screenManager('pixelsPerCm',24,... + 'disableSyncTests',true,'backgroundColour',me.screen.backgroundColour,... + 'screen', oscreen, 'specialFlags', kPsychGUIWindow); + [w,h] = Screen('WindowSize',me.operatorScreen.screen); + me.operatorScreen.windowed = [20 20 round(w/1.6) round(h/1.8)]; + end + me.secondScreen = true; + if ismac; me.operatorScreen.useRetina = true; end + + me.smoothing.sampleRate = me.sampleRate; + + if strcmpi(me.calibration.stimulus,'movie') + if isempty(me.calStim) || ~isa(me.calStim,'movieStimulus') + me.calStim = movieStimulus('size',me.calibration.size,'filePath',me.calibration.filePath); + me.calStim.specialFlagsOpen = 2; % don't load sound + me.calStim.circularMask = true; + if me.calStim.nVideos > 1 + me.calStim.autoShuffle = 5; + end + else + if ~isempty(me.calStim) && isa(me.calStim,'movieStimulus'); try me.calStim.reset; end; end + me.calStim.size = me.calibration.size; + me.calStim.filePath = me.calibration.filePath; + end + elseif strcmpi(me.calibration.stimulus,'image') + if isempty(me.calStim) || ~isa(me.calStim,'imageStimulus') + me.calStim = imageStimulus('size',me.calibration.size,'filePath',me.calibration.filePath); + else + if ~isempty(me.calStim)&& isa(me.calStim,'imageStimulus'); try me.calStim.reset; end; end + me.calStim.size = me.calibration.size; + me.calStim.filePath = me.calibration.filePath; + end + elseif strcmpi(me.calibration.stimulus,'pupilcore') + me.calStim = pupilCoreStimulus(); + me.calStim.size = me.calibration.size; + elseif strcmpi(me.calibration.stimulus,'animated') + lw = me.calibration.size/8; + if lw > 0.2; lw = 0.2; end + me.calStim = fixationCrossStimulus('size',me.calibration.size,'lineWidth',lw,'type','pulse'); + else + if isempty(me.calStim) + lw = me.calibration.size/8; + if lw > 0.2; lw = 0.2; end + me.calStim = fixationCrossStimulus('size',me.calibration.size,'lineWidth',lw); + end + end + + if me.isDummy + me.salutation('Initialise', 'Running iRec in Dummy Mode', true); + me.isConnected = true; + else + if isempty(me.tcp) || ~isa(me.tcp,'dataConnection') + me.tcp = dataConnection('rAddress', me.calibration.ip,'rPort',... + me.calibration.tcpport,'protocol','tcp'); + else + me.tcp.close(); + me.tcp.protocol = 'tcp'; + me.tcp.rAddress = me.calibration.ip; + me.tcp.rPort = me.calibration.tcpport; + end + if isempty(me.udp) || ~isa(me.udp,'dataConnection') + me.udp = dataConnection('rAddress', me.calibration.ip,'rPort',... + me.calibration.udpport,'protocol','udp'); + else + me.udp.close(); + me.udp.protocol = 'udp'; + me.udp.rAddress = me.calibration.ip; + me.udp.rPort = me.calibration.udpport; + end + try + open(me.tcp); + if ~me.tcp.isOpen; warning('Cannot Connect to TCP');error('Cannot connect to TCP'); end + open(me.udp); + me.udp.write(intmin('int32')); + me.isConnected = true; + me.salutation('Initialise', ... + sprintf('Running on a iRec | Screen %i %i x %i @ %iHz', ... + me.screen.screen,... + me.screen.winRect(3),... + me.screen.winRect(4),... + me.screen.screenVals.fps),true); + catch + me.salutation('Initialise', 'Cannot connect, running in Dummy Mode', true); + me.isConnected = false; + me.isDummy = true; + end + end + if me.useLEDs + if ~rM.isOpen; open(rM); end + try + for i = 1:length(me.calibration.calPositions) + me.turnOnLED(i, rM); + WaitSecs(0.02); + end + for i = 1:length(me.calibration.calPositions) + me.turnOffLED(i, rM); + WaitSecs(0.02); + end + end + end + success = true; + end + + % =================================================================== + function cal = trackerSetup(me,varargin) + %> @fn trackerSetup(me, varargin) + %> @brief calibration + validation + %> + % =================================================================== + if me.isOff; return; end + + [rM, aM] = optickaCore.initialiseGlobals(); + + if me.calibration.audioFeedback; open(aM); beep(aM,2000,0.1,0.1); end + + cal = []; + if ~me.isConnected && ~me.isDummy + warning('Eyetracker not connected, cannot calibrate!'); + return + end + + if ~isempty(me.screen) && isa(me.screen,'screenManager'); open(me.screen); end + if me.useOperatorScreen && isa(me.operatorScreen,'screenManager'); open(me.operatorScreen); end + s = me.screen; + if me.useOperatorScreen; s2 = me.operatorScreen; end + me.win = me.screen.win; + me.ppd_ = me.screen.ppd; + + if ischar(me.calibration.calPositions); me.calibration.calPositions = str2num(me.calibration.calPositions); end + if ischar(me.calibration.valPositions); me.calibration.valPositions = str2num(me.calibration.valPositions); end + + fprintf('\n===>>> CALIBRATING IREC... <<<===\n'); + + f = me.calStim; + + hide(f); + setup(f, me.screen); + + startRecording(me); + + KbName('UnifyKeyNames'); + one = KbName('1!'); two = KbName('2@'); three = KbName('3#'); + four = KbName('4$'); five = KbName('5%'); six = KbName('6^'); + seven = KbName('7&'); eight = KbName('8*'); nine = KbName('9('); + zero = KbName('0)'); quit = KbName('q'); cal = KbName('c'); + val = KbName('v'); dr = KbName('d'); smpl = KbName('s'); menu = KbName('LeftShift'); + sample = KbName('RightShift'); shot = KbName('F1'); esc = KbName('escape'); + oldr = RestrictKeysForKbCheck([one two three four five six seven ... + eight nine zero quit cal val dr smpl menu sample shot esc]); + + cpos = me.calibration.calPositions; + vpos = me.calibration.valPositions; + + me.validationData = struct(); + me.validationData(1).type = 'validation'; + me.validationData(1).collected = false; + me.validationData(1).vpos = vpos; + me.validationData(1).time = datetime('now'); + me.validationData(1).data = cell(size(vpos,1),1); + me.validationData(1).dataS = me.validationData(1).data; + + loop = true; + ref = s.screenVals.fps; + a = -1; + mode = 'menu'; + vn = 1; + + while loop + + vn = length(me.validationData); + + switch mode + + case 'menu' + cloop = true; + resetFixation(me, true); + while cloop + a = a + 1; + getSample(me); + if me.useOperatorScreen + s2.drawText('MENU: q = exit | c = calibrate | v = validate | d = drift offset | s = sample | F1 = screenshot'); + if ~isempty(me.x) + s2.drawSpot(0.75,[0 1 0.25 0.2],me.x,me.y); + end + drawValidationResults(me, vn); + drawDriftOffset(me); + if mod(a,ref) == 0 + trackerFlip(me,0,true); + else + trackerFlip(me,1); + end + else + s.drawText('MENU: q = exit | c = calibrate | v = validate | d = drift offset | s = sample | F1 = screenshot'); + end + flip(s); + [pressed,~,keys, shift] = optickaCore.getKeys(); + if pressed + if keys(quit) && shift + error('You have force exited out of the calibration!!!'); + elseif keys(quit) && ~shift + cloop = false; loop = false; + elseif keys(cal) + mode = 'calibrate'; cloop = false; + elseif keys(val) + mode = 'validate'; cloop = false; + elseif keys(dr) + mode = 'driftoffset'; cloop = false; + elseif keys(smpl) + mode = 'sample'; cloop = false; + elseif keys(menu) + vn = vn - 1; + if vn < 1; vn = length(me.validationData); end + elseif keys(shot) + filename=[me.paths.parent filesep me.name '_' datestr(now,'YYYY-mm-DD-HH-MM-SS') '.png']; + captureScreen(s2, filename); + end + end + end + + case 'driftoffset' + trackerFlip(me,0,true); + oldrr = RestrictKeysForKbCheck([]); + driftOffset(me); + RestrictKeysForKbCheck(oldrr); + mode = 'menu'; + WaitSecs(0.5); + + case 'calibrate' + cloop = true; + thisX = 0; + thisY = 0; + lastK = 0; + thisPos = 1; + + me.validationData = struct(); + me.validationData(1).collected = false; + me.validationData(1).type = 'unknown'; + + f.xPositionOut = cpos(thisPos,1); + f.yPositionOut = cpos(thisPos,2); + update(f); + nPositions = size(cpos,1); + resetAll(me); + while cloop + a = a + 1; + me.getSample(); + draw(f); + animate(f); + % if me.useOperatorScreen + % s2.drawText ('CALIBRATE: lshift = exit | # = point'); + % s2.drawCross(1,[],thisX,thisY); + % if ~isempty(me.x);s2.drawSpot(0.75,[0 1 0.25 0.1],me.x,me.y);end + % if mod(a,ref) == 0 + % trackerFlip(me, 0, true); + % else + % trackerFlip(me, 1); + % end + % end + flip(s); + + [pressed,name,keys] = optickaCore.getKeys(); + if pressed + fprintf('key: %s\n',name); + if length(name)==2 % assume a number + k = str2double(name(1)); + if k == 0 + hide(f); + for ii=1:length(cpos);me.turnOffLED(ii,rM);end + trackerFlip(me,0,true); + elseif k > 0 && k <= nPositions + thisPos = k; + if k == lastK && f.isVisible + f.isVisible = false; + if me.useLEDs; me.turnOffLED(k,rM); end + thisPos = 0; + elseif ~f.isVisible + f.isVisible = true; + if me.useLEDs; me.turnOnLED(k,rM); end + end + lastK = k; + if thisPos > 0 && thisPos <= nPositions + thisX = cpos(thisPos,1); + thisY = cpos(thisPos,2); + f.xPositionOut = thisX; + f.yPositionOut = thisY; + update(f); + end + trackerFlip(me,0,true); + end + elseif keys(sample) + hide(f); + if me.useLEDs; for ii=1:length(cpos);me.turnOffLED(ii,rM); end; end + trackerFlip(me,0,true); + giveReward(rM); + if me.calibration.audioFeedback; beep(aM,2000,0.1,0.1); end + elseif keys(menu) + trackerFlip(me,0,true); + mode = 'menu'; cloop = false; + elseif keys(val) + mode = 'validate'; cloop = false; + end + end + end + + case 'validate' + cloop = true; + thisPos = 1; lastK = thisPos; + thisX = vpos(thisPos,1); + thisY = vpos(thisPos,2); + f.xPositionOut = thisX; + f.yPositionOut = thisY; + update(f); + + if me.validationData(end).collected == false + me.validationData(end).collected = true; + else + me.validationData(end+1).collected = true; + end + me.validationData(end).type = 'validation'; + me.validationData(end).vpos = vpos; + me.validationData(end).time = datetime('now'); + me.validationData(end).data = cell(size(vpos,1),1); + me.validationData(end).dataS = cell(size(vpos,1),1); + + vn = length(me.validationData); + + resetFixation(me, true); + nPositions = size(vpos,1); + while cloop + a = a + 1; + me.getSample(); + draw(f); + animate(f); + if me.useOperatorScreen + s2.drawText('VALIDATE: lshift = exit | rshift = sample | # = point'); + if ~isempty(me.x); s2.drawSpot(0.75,[0 1 0.25 0.25],me.x,me.y); end + drawValidationResults(me, vn); + if mod(a,ref) == 0 + trackerFlip(me, 0, true); + else + trackerFlip(me, 1); + end + end + flip(s); + + [pressed,name,keys] = optickaCore.getKeys(); + if pressed + fprintf('key: %s\n',name); + if length(name)==2 % assume a number + k = str2double(name(1)); + if k == 0 + resetFixationHistory(me); + thisPos = 0; + hide(f); + if me.useLEDs; for ii=1:length(cpos);me.turnOffLED(ii,rM);end; end + trackerFlip(me,0,true); + elseif k > 0 && k <= nPositions + thisPos = k; + if k == lastK && f.isVisible + f.isVisible = false; + if me.useLEDs; me.turnOffLED(k,rM); end + thisPos = 0; + elseif ~f.isVisible + f.isVisible = true; + if me.useLEDs; me.turnOnLED(k,rM); end + end + lastK = k; + if thisPos > 0 && thisPos <= nPositions + thisX = vpos(thisPos,1); + thisY = vpos(thisPos,2); + f.xPositionOut = thisX; + f.yPositionOut = thisY; + update(f); + end + trackerFlip(me,0,true); + end + elseif keys(sample) + if ~isempty(me.xAllRaw) + ld = length(me.xAllRaw); + sd = ld - me.rawSamples; + if sd < 1; sd = 1; end + me.validationData(end).data{lastK} = [me.xAllRaw(sd:ld); me.yAllRaw(sd:ld)]; + l=length(me.xAll); + if l > 5; l = 5; end + me.validationData(end).dataS{lastK} = [me.xAll(end-l:end); me.yAll(end-l:end)]; + end + giveReward(rM); + if me.calibration.audioFeedback; beep(aM,2000,0.1,0.1); end + f.isVisible = false; + if me.useLEDs; for ii=1:length(cpos);me.turnOffLED(ii,rM);end; end + thisPos = 0; + resetFixationHistory(me); + trackerFlip(me,0,true); + elseif keys(menu) + mode = 'menu'; cloop = false; + end + end + end + + case 'sample' + cloop = true; + thisPos = 1; lastK = thisPos; + + me.validationData(end+1).collected = true; + me.validationData(end).type = 'sample'; + me.validationData(end).vpos = cell(9,1); + me.validationData(end).time = datetime('now'); + me.validationData(end).data = cell(9,1); + me.validationData(end).dataS = cell(9,1); + + vn = length(me.validationData); + + resetFixationHistory(me); + nPositions = 9; + while cloop + a = a + 1; + me.getSample(); + drawGrid(s); + draw(f); + animate(f); + flip(s); + if me.useOperatorScreen + s2.drawText(sprintf('SAMPLE %i: lshift = exit | rshift = sample | # = point',thisPos)); + if ~isempty(me.x); s2.drawSpot(0.75,[0 1 0.25 0.25],me.x,me.y); end + drawValidationResults(me, vn); + if mod(a,ref) == 0 + trackerFlip(me,0,true); + else + trackerFlip(me,1); + end + end + + [pressed,name,keys] = optickaCore.getKeys(); + if pressed + fprintf('key: %s\n',name); + if length(name)==2 % assume a number + k = str2double(name(1)); + if k > 0 && k <= 9 + thisPos = k; lastK = k; + end + elseif keys(sample) + if ~isempty(me.xAllRaw) + ld = length(me.xAllRaw); + sd = ld - me.rawSamples; + if sd < 1; sd = 1; end + me.validationData(end).data{lastK} = [me.xAllRaw(sd:ld); me.yAllRaw(sd:ld)]; + l=length(me.xAll); + if l > 5; l = 5; end + me.validationData(end).dataS{lastK} = [me.xAll(end-l:end); me.yAll(end-l:end)]; + end + rM.giveReward; + resetFixationHistory(me); + trackerFlip(me,0,true); + elseif keys(menu) + mode = 'menu'; cloop = false; + end + end + end + end % switch mode + end + s.drawText('Calibration finished...'); + s2.drawText('Calibration finished...') + s.flip(); s2.flip(); s2.drawBackground; s2.flip(); + reset(f); + resetFixation(me, true); + RestrictKeysForKbCheck(oldr); + stopRecording(me); + WaitSecs(0.25); + fprintf('===>>> CALIBRATING IREC FINISHED... <<<===\n'); + end + + % =================================================================== + function startRecording(me, ~) + %> @fn startRecording(me,~) + %> @brief startRecording - for iRec this just starts TCP online + %> access, all data is saved to CSV irrespective of this + %> + % =================================================================== + if me.isDummy || me.isOff; return; end + if me.tcp.isOpen; me.tcp.write(int8('start')); end + me.isRecording = true; + end + + % =================================================================== + function stopRecording(me, ~) + %> @fn stopRecording(me,~) + %> @brief stopRecording - for iRec this just stops TCP online + %> access, all data is saved to CSV irrespective of this + %> + % =================================================================== + if me.isDummy || me.isOff; return; end + if me.tcp.isOpen; me.tcp.write(int8('stop')); end + me.isRecording = false; + end + + % =================================================================== + function sample = getSample(me) + %> @fn getSample() + %> @brief get latest sample from the tracker, if dummymode=true then use + %> the mouse as an eye signal + %> + % =================================================================== + if me.isOff; return; end + if me.isDummy %lets use a mouse to simulate the eye signal + sample = getMouseSample(me); + elseif me.isConnected && me.isRecording + sample = me.sampleTemplate; + xy = []; + td = me.tcp.readLines(me.smoothing.nSamples,'last'); + if isempty(td); me.currentSample=sample; return; end + td = str2num(td); %#ok<*ST2NM> + sample.raw = td; + sample.time = td(end,1); + sample.timeD = GetSecs; + xy(1,:) = td(:,2)'; + xy(2,:) = -td(:,3)'; + if ~isempty(xy) + me.xAllRaw = [me.xAllRaw xy(1,:)]; + me.yAllRaw = [me.yAllRaw xy(2,:)]; + sample.valid = true; + xy = doSmoothing(me,xy); + sample.gx = xy(1); + sample.gy = xy(2); + sample.pa = median(td(:,4)); + me.x = xy(1); + me.y = xy(2); + me.pupil = sample.pa; + if me.debug; fprintf('>>X: %2.2f | Y: %2.2f | P: %.2f\n',me.x,me.y,me.pupil);end + else + sample.gx = NaN; + sample.gy = NaN; + sample.pa = NaN; + me.x = NaN; + me.y = NaN; + me.pupil = NaN; + end + me.xAll = [me.xAll me.x]; + me.yAll = [me.yAll me.y]; + me.pupilAll = [me.pupilAll me.pupil]; + else + sample = me.sampleTemplate; + me.x = []; me.y = []; me.pupil = []; + if me.verbose; fprintf('-+-+-> iRecManager.getSample(): no data, are you sure you are recording?\n');end + end + me.currentSample = sample; + end + + % =================================================================== + function trackerMessage(me, message, ~) + %> @fn trackerMessage(me, message) + %> @brief Send message to store in tracker data, for iRec this can + %> only be a single 32bit signed integer. + %> + %> As we do send strings to eyelink / tobii, we process string messages + %> TRIALID and TRIALRESULT we extract the integer value, END_FIX becomes + %> -1500 and END_RT becomes -1501 + % =================================================================== + if me.isOff; return; end + if me.isConnected + if isnumeric(message) + me.udp.write(int32(message)); + elseif ischar(message) + if contains(message,'TRIAL_RESULT') || contains(message,'TRIALID') + message = strsplit(message, ' '); + if length(message)==2 + message = str2double(message{2}); + else + message = []; + end + elseif contains(message,'SYNCTIME') + message = -1499; + elseif contains(message,'END_FIX') + message = -1500; + elseif contains(message,'END_RT') + message = -1501; + end + end + if isempty(message); return; end + me.udp.write(int32(message)); + if me.verbose; fprintf('-+-+->IREC Message: %i\n', message);end + end + end + + % =================================================================== + function close(me) + %> @fn close(me) + %> @brief close the iRec and cleanup, call after experiment finishes + %> + % =================================================================== + try + try me.udp.write(int32(intmax('int32'))); end + try stopRecording(me); end + try me.tcp.close; end + try me.udp.close; end + me.isConnected = false; + me.isRecording = false; + resetAll(me); + if ~isempty(me.operatorScreen) && isa(me.operatorScreen,'screenManager') + try close(me.operatorScreen); end + end + catch ME + me.salutation('Close Method','Couldn''t stop recording, forcing shutdown...',true) + me.isConnected = false; + me.isRecording = false; + try me.tcp.close; end + try me.udp.close; end + try resetAll(me); end + if me.secondScreen && ~isempty(me.operatorScreen) && isa(me.operatorScreen,'screenManager') + try me.operatorScreen.close; end + end + getReport(ME); + end + end + + % =================================================================== + function runDemo(me, forcescreen) + %> @fn runDemo(me, forceScreen) + %> @brief runs a demo of this class, useful for testing + %> + %> @param forcescreen forces to use a specific screen number + % =================================================================== + KbName('UnifyKeyNames') + stopkey = KbName('q'); + upKey = KbName('uparrow'); + downKey = KbName('downarrow'); + leftKey = KbName('leftarrow'); + rightKey = KbName('rightarrow'); + calibkey = KbName('c'); + driftkey = KbName('d'); + RestrictKeysForKbCheck([stopkey upKey downKey leftKey rightKey calibkey driftkey]); + ofixation = me.fixation; + osmoothing = me.smoothing; + oldexc = me.exclusionZone; + oldfixinit = me.fixInit; + oldname = me.name; + me.name = 'iRecManager-runDemo'; + try + if ~me.isConnected; initialise(me);end + s = me.screen; s2 = me.operatorScreen; + s.font.FontName = me.monoFont; + if exist('forcescreen','var'); close(s); s.screen = forcescreen; end + s.disableSyncTests = true; s2.disableSyncTests = true; + if ~s.isOpen; open(s); end + if me.useOperatorScreen && ~s2.isOpen; s2.open(); end + sv = s.screenVals; + + trackerSetup(me); + + drawPhotoDiodeSquare(s,[0 0 0 1]); flip(s); %make sure our photodiode patch is black + + % set up the size and position of the stimulus + o = dotsStimulus('size',me.fixation.radius(1)*2,'speed',2,'mask',true,'density',50); %test stimulus + if length(me.fixation.radius) == 1 + f = discStimulus('size',me.fixation.radius(1)*2,'colour',[0 0 0],'alpha',0.25); + else + f = barStimulus('barWidth',me.fixation.radius(1)*2,'barHeight',me.fixation.radius(2)*2,... + 'colour',[0 0 0],'alpha',0.25); + end + setup(o,s); %setup our stimulus with open screen + setup(f,s); %setup our stimulus with open screen + o.xPositionOut = me.fixation.X; + o.yPositionOut = me.fixation.Y; + f.alpha + f.xPositionOut = me.fixation.X; + f.xPositionOut = me.fixation.X; + + methodl={'median','heuristic1','heuristic2','sg','simple'}; + eyel={'both','left','right'}; + m = 1; n = 1; + trialn = 1; + maxTrials = 5; + endExp = false; + + % set up an exclusion zone where eye is not allowed + me.exclusionZone = [8 10 8 10]; + exc = me.toPixels(me.exclusionZone); + exc = [exc(1) exc(3) exc(2) exc(4)]; %psychrect=[left,top,right,bottom] + + startRecording(me); + WaitSecs('YieldSecs',0.5); + + trackerMessage(me,0) + while trialn <= maxTrials && ~endExp + trialtick = 1; + drawPhotoDiodeSquare(s,[0 0 0 1]); + trackerDrawStatus(me,'Start Trial'); + resetFixation(me); + vbl = flip(s); tstart = vbl + sv.ifi; + trackerMessage(me, trialn); + while vbl < tstart + 6 + Screen('FillRect', s.win, [0.7 0.7 0.7 0.5],exc); Screen('DrawText',s.win,'Exclusion Zone',exc(1),exc(2),[0.8 0.8 0.8]); + drawGrid(s); draw(o); draw(f); + drawCross(s, 0.5, [1 1 0], me.fixation.X, me.fixation.Y); + drawPhotoDiodeSquare(s, [1 1 1 1]); + + getSample(me); isFixated(me); + + if ~isempty(me.currentSample) + txt = sprintf('Q = finish. X: %3.1f / %2.2f | Y: %3.1f / %2.2f | # = %2i %s %s | RADIUS = %s | TIME = %.2f | FIXATION = %.2f | EXC = %i | INIT FAIL = %i',... + me.currentSample.gx, me.x, me.currentSample.gy, me.y, me.smoothing.nSamples,... + me.smoothing.method, me.smoothing.eyes, sprintf('%1.1f ',me.fixation.radius), ... + me.fixTotal,me.fixLength,me.isExclusion,me.isInitFail); + Screen('DrawText', s.win, txt, 10, 10,[1 1 1]); + if ~me.useOperatorScreen;drawEyePosition(me,true);end + end + animate(o); + + if me.useOperatorScreen + trackerDrawExclusion(me); + trackerDrawFixation(me); + trackerDrawEyePosition(me); + end + + vbl(end+1) = Screen('Flip', s.win, vbl(end) + s.screenVals.halfifi); + if me.useOperatorScreen; trackerFlip(me); end + + [keyDown, ~, keyCode] = optickaCore.getKeys(); + if keyDown + if keyCode(stopkey); endExp = true; break; + elseif keyCode(calibkey); me.trackerSetup; + elseif keyCode(upKey); me.smoothing.nSamples = me.smoothing.nSamples + 1; if me.smoothing.nSamples > 400; me.smoothing.nSamples=400;end + elseif keyCode(downKey); me.smoothing.nSamples = me.smoothing.nSamples - 1; if me.smoothing.nSamples < 1; me.smoothing.nSamples=1;end + elseif keyCode(leftKey); m=m+1; if m>5;m=1;end; me.smoothing.method = methodl{m}; + end + end + + trialtick=trialtick+1; + end + if endExp == false + drawPhotoDiodeSquare(s,[0 0 0 1]); + vbl = flip(s); + trackerMessage(me,-1); + + if me.useOperatorScreen; trackerDrawStatus(me,'Finished Trial'); end + + resetAll(me); + + me.fixation.X = randi([-7 7]); + me.fixation.Y = randi([-7 7]); + if length(me.fixation.radius) == 1 + me.fixation.radius = randi([1 3]); + o.sizeOut = me.fixation.radius * 2; + f.sizeOut = me.fixation.radius * 2; + else + me.fixation.radius = [randi([1 3]) randi([1 3])]; + o.sizeOut = mean(me.fixation.radius) * 2; + f.barWidthOut = me.fixation.radius(1) * 2; + f.barHeightOut = me.fixation.radius(2) * 2; + end + o.xPositionOut = me.fixation.X; + o.yPositionOut = me.fixation.Y; + f.xPositionOut = me.fixation.X; + f.yPositionOut = me.fixation.Y; + update(o);update(f); + WaitSecs(0.5); + trialn = trialn + 1; + else + drawPhotoDiodeSquare(s,[0 0 0 1]); + vbl = flip(s); + trackerMessage(me,-100); + end + end + WaitSecs(0.5); + stopRecording(me); + ListenChar(0); Priority(0); ShowCursor; + RestrictKeysForKbCheck([]); + try close(s); close(s2); reset(o); reset(f); end %#ok<*TRYNC> + close(me); + me.fixation = ofixation; + me.smoothing = osmoothing; + me.exclusionZone = oldexc; + me.fixInit = oldfixinit; + me.name = oldname; + clear s s2 o + catch ERR + stopRecording(me); + me.fixation = ofixation; + me.smoothing = osmoothing; + me.exclusionZone = oldexc; + me.fixInit = oldfixinit; + me.name = oldname; + ListenChar(0);Priority(0);ShowCursor; + getReport(ERR) + try close(s); end + try close(s2); end + sca; + try close(me); end + clear s s2 o + rethrow(ERR) + end + + end + + end%-------------------------END PUBLIC METHODS--------------------------------% + + %============================================================================ + methods (Hidden = true) %--HIDDEN METHODS (compatibility with eyelinkManager) + %============================================================================ + + + % =================================================================== + %> @brief Sync time with tracker + %> + % =================================================================== + function syncTrackerTime(varargin) + + end + + % =================================================================== + %> @brief Save the data + %> + % =================================================================== + function saveData(varargin) + + end + % =================================================================== + %> @brief + %> + % =================================================================== + function updateDefaults(varargin) + + end + + % =================================================================== + %> @brief checks which eye is available, force left eye if + %> binocular is enabled + %> + % =================================================================== + function eyeUsed = checkEye(me) + if me.isConnected + eyeUsed = me.eyeUsed; + end + end + + % =================================================================== + %> @brief displays status message on tracker, only sets it if + %> message is not the previous message, so loop safe. + %> + % =================================================================== + function statusMessage(me,message) + if me.isConnected + if me.verbose; fprintf('-+-+->iRec status message: %s\n',message);end + end + end + + % =================================================================== + %> @brief send message to store in tracker data (compatibility) + %> + %> + % =================================================================== + function edfMessage(me, message) + trackerMessage(me,message) + end + + % =================================================================== + %> @brief + %> + % =================================================================== + function setup(me) + initialise(me) + end + + % =================================================================== + %> @brief set into offline / idle mode + %> + % =================================================================== + function setOffline(me) + + end + + % =================================================================== + %> @brief check the connection with the tobii + %> + % =================================================================== + function connected = checkConnection(me) + connected = me.isConnected; + end + + + % =================================================================== + %> @brief wrapper for EyelinkDoDriftCorrection + %> + % =================================================================== + function success = driftCorrection(me) + success = driftOffset(me); + end + + % =================================================================== + %> @brief check what mode the is in + %> + % ========================a=========================================== + function mode = currentMode(me) + if me.isConnected + mode = 0; + end + end + + % =================================================================== + %> @brief Sync time with tracker: send int32(-1000) + %> + % =================================================================== + function syncTime(me) + trackerMessage(me,int32(-1499)); + end + + + % =================================================================== + %> @brief Get offset between tracker and display computers + %> + % =================================================================== + function offset = getTimeOffset(me) + offset = 0; + end + + % =================================================================== + %> @brief Get tracker time + %> + % =================================================================== + function [trackertime, systemtime] = getTrackerTime(me) + trackertime = 0; + systemtime = 0; + end + + % =================================================================== + %> @brief + %> + % =================================================================== + function value = checkRecording(me) + if me.isConnected + value = true; + else + value = false; + end + end + + end%-------------------------END HIDDEN METHODS--------------------------------% + + %======================================================================= + methods (Access = private) %------------------PRIVATE METHODS + %======================================================================= + + function turnOnLED(me, val, rM) + if me.useLEDs + rM.digitalWrite(val-1 + me.startPin,1); + end + end + function turnOffLED(me, val, rM) + if me.useLEDs + rM.digitalWrite(val-1 + me.startPin,0); + end + end + + end %------------------END PRIVATE METHODS +end diff --git a/eyetracker/iView_checkfix_demo.m b/eyetracker/iView_checkfix_demo.m new file mode 100644 index 0000000000000000000000000000000000000000..648d41af7253d47b35474f95f5d8d196dffb1d87 --- /dev/null +++ b/eyetracker/iView_checkfix_demo.m @@ -0,0 +1,159 @@ +% Functions used from iViewX toolbox: +% iView('initialize', ivx) - Initializes the connection to the iView X system. +% iViewX_vk('calibrate', ivx) - Runs the eye tracker calibration procedure. +% iViewX_vk('openconnection', ivx) - Opens the data connection for streaming eye data. +% iViewX_vk('setscreensize', ivx) - Informs the iView X system about the display size. +% iViewX_vk('startrecording', ivx) - Begins recording eye movement data. +% iViewX_vk('message', ivx, '...') - Sends a message to be logged in the iView X data file. +% iViewX_vk('receivelast', ivx) - Retrieves the most recent eye gaze data sample. +% iViewX_vk('stoprecording', ivx) - Stops the eye movement recording. +% iViewX_vk('datafile', ivx, '...') - Saves the recorded data to a specified file path on the iView workstation. +% initIViewXDefaults_vk([], [], '100.1.1.1', []) - Initializes the iViewX structure with default settings and IP. + + +% Short MATLAB example program using iViewX and Psychophysics Toolboxes +% for a simple eyetracking experiment. + +% STEP 1: Initialize iViewX defaults and environment +% Get default iViewX settings, specify IP address. +ivx = initIViewXDefaults_vk([], [], '100.1.1.1', []); + +% Unify key names for cross-platform compatibility with Psychtoolbox. +KbName('UnifyKeyNames'); +stopkey = KbName('ESCAPE'); +startkey = KbName('space'); + +% STEP 2: Initialize connection with iViewX +% Connect to the iView X tracker. Exit if connection fails. +if iView('initialize', ivx) ~= 1 + return % Exit program on failure +end + + +% STEP 3: Open graphics window +% Open a Psychtoolbox window on the main screen. +ScreenNumber = 1; % Main screen +[window, screenRect] = Screen('OpenWindow', ScreenNumber, 0); % Open window, get handle and rect + +% Get screen center coordinates. +[xCenter, yCenter] = RectCenter(screenRect); +gray = GrayIndex(window); +white = WhiteIndex(window); +Screen('FillRect', window, gray); +Screen('Flip', window); + +% Store window handle in ivx structure. +ivx.window = window; + +% Wait for space key press to start calibration. +while 1 + [keyIsDown, secs, keyCode] = KbCheck; + if keyCode(startkey) % Check if start key is pressed + break; + end +end +WaitSecs(0.1); % Short pause +fprintf('start run the calibrate\n'); % Indicate calibration start + +% Set the data format for eye tracking output from iView X. +% Defines the format string for gaze and pupil data. +result = iViewXComm('send', ivx, 'ET_FRM "%TS: %SX,%SY,%DX,%DY" ');%TS: timestamp, SX SY: gaze X Y, DX DY: pupil diameter;X Y(PIXEL) +%ET_FRM: sets format for data output (check help"remote command reference") + +% STEP 4: Calibrate eye tracker +% Run the standard iView X calibration procedure. +iViewX_vk('calibrate', ivx); + +% Optional: Perform drift correction (fixation check at screen center). +% iViewX_vk('driftcorrection',ivx); + +% STEP 5: Start eye position recording +% Open data connection and start recording. +[success, ivx] = iViewX_vk('openconnection', ivx); % Open data stream +[success, ivx] = iViewX_vk('setscreensize', ivx); % Inform iView X about screen size +iViewX_vk('startrecording', ivx); % Begin recording + +%% STEP 6: Main measurement loop (Fixation check example) +% Set experiment parameters. +waitDuration = 1; % Duration for fixation check +ifi = Screen('GetFlipInterval', window); +numWaitFrames = round(waitDuration / ifi); +numFixFrames = numWaitFrames / 2; % Frames required for successful fixation + +% Define fixation window properties. +fixationWindowCenter = [xCenter, yCenter]; +fixationWindowSize = 60; + +% Define coordinates for drawing a fixation cross. +xCoords = [-10, 10, 0, 0]; +yCoords = [0, 0, -10, 10]; +allCoords = [xCoords; yCoords]; +lineWidthPix = 5; + +% Fixation state variables. +subjectFixState = 0; % 0: not fixating, 1: fixating +frames_in_window = 0; % Counter for frames inside fixation window +frames_out_of_window = 0; % Counter for frames outside fixation window (while in state 1) + +% Baseline fixation loop: Wait for subject to fixate. +waitflipstamp = Screen('Flip', window); + +for waitcontrolframe = 1:numWaitFrames + % Send message to iView X data file. + iViewX_vk('message', ivx, 'CheckFix'); + + % Draw fixation cross. + Screen('DrawLines', window, allCoords,... + lineWidthPix, white, [xCenter, yCenter], 2); + + % Get latest eye gaze data. + [iviewdata, ivx] = iViewX_vk('receivelast', ivx); + + % Parse gaze X and Y coordinates from the received string. + tokens = regexp(iviewdata, ':\s*(\d+),(\d+)', 'tokens'); + x = str2double(tokens{1}{1}); + y = str2double(tokens{1}{2}); + + % Check and update subject's fixation state. + is_gaze_in_window = (x >= fixationWindowCenter(1) - fixationWindowSize / 2) && ... + (x <= fixationWindowCenter(1) + fixationWindowSize / 2) && ... + (y >= fixationWindowCenter(2) - fixationWindowSize / 2) && ... + (y <= fixationWindowCenter(2) + fixationWindowSize / 2); + + if subjectFixState == 0 && is_gaze_in_window + subjectFixState = 1; % Transition to fixating state + end + + if subjectFixState == 1 + frames_in_window = frames_in_window + 1; + if ~is_gaze_in_window + frames_out_of_window = frames_out_of_window + 1; + end + + % Reset if fixation is lost for too many frames. + if frames_out_of_window >= 9 + subjectFixState = 0; + frames_in_window = 0; + frames_out_of_window = 0; + end + + % If fixation maintained for required frames, break loop. + if frames_in_window >= numFixFrames + iViewX_vk('message', ivx, 'FixSuccess'); % Mark fixation success + break; + end + end + + % Flip the screen to update display. + waitflipstamp = Screen('Flip', window, waitflipstamp + 0.5 * ifi); +end + +% STEP 7: Finish experiment +% Stop recording, save data, and close window. +iViewX_vk('stoprecording', ivx); % Stop eye tracking recording +iViewX_vk('datafile', ivx, 'D:\wqc\cwj.idf'); % Save data file on iView workstation +Screen('close', window); + +% Note: Depending on the iViewX_vk implementation + + diff --git a/eyetracker/pupilCoreManager.m b/eyetracker/pupilCoreManager.m new file mode 100644 index 0000000000000000000000000000000000000000..4e5d0debb83421c16c5dbfcf764fc8aea7a99dd7 --- /dev/null +++ b/eyetracker/pupilCoreManager.m @@ -0,0 +1,1087 @@ +% ======================================================================== +classdef pupilCoreManager < eyetrackerCore & eyetrackerSmooth +%> @class pupilCoreManager +%> @brief Manages the Pupil Labs Core +%> +%> The eyetrackerCore methods enable the user to test for common behavioural +%> eye tracking tasks with single commands. +%> +%> Multiple fixation windows can be assigned, the windows can be either +%> circular or rectangular. In addition rectangular exclusion windows can ensure a +%> subject doesn't saccade to particular parts of the screen. fixInit allows +%> you to define a minimum time with which the subject must initiate a +%> saccade away from a position (which stops a subject cheating in a trial). +%> +%> To initiate a task we normally place a fixation cross on the screen and +%> ask the subject to saccade to the cross and maintain fixation for a +%> particular duration. This is achieved using +%> testSearchHoldFixation('yes','no'), using the properties: +%> fixation.initTime to time how long the subject has to saccade into the +%> window, fixation.time for how long they must maintain fixation, +%> fixation.radius for the radius around fixation.X and fixation.Y position. +%> The method returns the 'yes' string if the rules are matched, and 'no' if +%> they are not, thus enabling experiment code to simply define what +%> happened. Other methods include isFixated(), testFixationTime(), +%> testHoldFixation(). +%> +%> Copyright ©2014-2023 Ian Max Andolina — released: LGPL3, see LICENCE.md +% ======================================================================== + + %-----------------CONTROLLED PROPERTIES-------------% + properties (SetAccess = protected, GetAccess = public) + %> type of eyetracker + type = 'pupil' + end + + %---------------PUBLIC PROPERTIES---------------% + properties + %> initial setup and calibration values + calibration = struct(... + 'ip', '127.0.0.1',... + 'port', 50020,... % used to send messages + 'stimulus','animated',... % calibration stimulus can be animated, movie + 'movie', [],... % if movie pass a movieStimulus + 'calPositions', [-12 0; 0 -12; 0 0; 0 12; 12 0],... + 'valPositions', [-12 0; 0 -12; 0 0; 0 12; 12 0],... + 'size', 2,... % size of calibration cross in degrees + 'doBeep',true,... % beep for calibration reward + 'manual', false,... + 'timeout', 1000) + end + + %-----------------CONTROLLED PROPERTIES-------------% + properties (SetAccess = protected, GetAccess = public) + %> communication socket + socket + %> subscription socket + sub + %> publishing socket + pub + %> endpoint + endpoint + %> subscribe endpoint + subEndpoint + %> subscribe endpoint + pubEndpoint + end + + properties (Hidden = true) + % for led calibration, which arduino pin to start from + startPin = 3 + end + + %--------------------PROTECTED PROPERTIES----------% + properties (SetAccess = protected, GetAccess = protected) + rawSamples = [] + % zmq context + ctx + % screen values taken from screenManager + sv = [] + %> tracker time stamp + systemTime = 0 + % stimulus used for calibration + calStim = [] + %> allowed properties passed to object upon construction + allowedProperties = {'calibration', 'useLEDs', 'smoothing'} + end + + %======================================================================= + methods %------------------PUBLIC METHODS + %======================================================================= + + % =================================================================== + function me = pupilCoreManager(varargin) + %> @fn pupilCoreManager(varargin) + %> + %> pupilCoreManager CONSTRUCTOR + %> + %> @param varargin can be passed as a structure, or name+arg pairs + %> @return instance of the class. + % =================================================================== + args = optickaCore.addDefaults(varargin,struct('name','PupilLabs',... + 'useOperatorScreen',true,'sampleRate',120)); + me=me@eyetrackerCore(args); %we call the superclass constructor first + me.parseArgs(args, me.allowedProperties); + me.smoothing.sampleRate = me.sampleRate; + + if ~exist('zmq.Context','class') + warning('Please install matlab-zmq package via https://github.com/iandol/matlab-zmq!!!') + else + fprintf('--->>> Pupil Labs core using %s', zmq.core.version); + end + end + + % =================================================================== + function success = initialise(me, sM, sM2) + %> @fn initialise(me, sM, sM2) + %> @brief initialise + %> + %> @param sM - screenManager for the subject + %> @param sM2 - a second screenManager used for operator, if + %> none is provided a default will be made. + % =================================================================== + + [rM, aM] = optickaCore.initialiseGlobals(); + + if ~exist('sM','var') || isempty(sM) + if isempty(me.screen) || ~isa(me.screen,'screenManager') + me.screen = screenManager; + end + else + me.screen = sM; + end + me.ppd_ = me.screen.ppd; + if me.screen.isOpen; me.win = me.screen.win; end + + me.rawSamples = round( 0.2 / ( 1 / me.sampleRate ) ); + + if me.screen.screen > 0 + oscreen = me.screen.screen - 1; + else + oscreen = 0; + end + if exist('sM2','var') + me.operatorScreen = sM2; + elseif isempty(me.operatorScreen) + me.operatorScreen = screenManager('pixelsPerCm',24,... + 'disableSyncTests',true,'backgroundColour',me.screen.backgroundColour,... + 'screen', oscreen, 'specialFlags', kPsychGUIWindow); + [w,h] = Screen('WindowSize',me.operatorScreen.screen); + me.operatorScreen.windowed = [20 20 round(w/1.6) round(h/1.8)]; + end + me.secondScreen = true; + if ismac; me.operatorScreen.useRetina = true; end + + me.smoothing.sampleRate = me.sampleRate; + + if me.isDummy + me.salutation('Initialise', 'Running Pupil Labs in Dummy Mode', true); + me.isConnected = false; + else + try + me.endpoint = ['tcp://' me.calibration.ip ':' num2str(me.calibration.port)]; + me.ctx = zmq.core.ctx_new(); + me.socket = zmq.core.socket(me.ctx, 'ZMQ_REQ'); + zmq.core.setsockopt(me.socket, 'ZMQ_RCVTIMEO', me.calibration.timeout); + zmq.core.setsockopt(me.socket, 'ZMQ_LINGER', 500); + fprintf('\n\n--->>> pupilLabsManager: Connecting to %s\n', me.endpoint); + err = zmq.core.connect(me.socket, me.endpoint); + if err == -1 + me.isConnected = false; + warning('Cannot Connect to Pupil Core!!!'); + close(me); + me.isDummy = true; + else + me.isConnected = true; + subscribe(me); + checkRoundTrip(me); + setPupilTime(me); + end + catch ME + close(me); + rethrow ME + end + end + + success = true; + end + + + % =================================================================== + function cal = trackerSetup(me,varargin) + %> @fn trackerSetup(me, varargin) + %> @brief calibration + validation + %> + % =================================================================== + [rM, aM] = optickaCore.initialiseGlobals(); + + cal = []; + if ~me.isConnected && ~me.isDummy + warning('Eyetracker not connected, cannot calibrate!'); + return + end + + if ~isempty(me.screen) && isa(me.screen,'screenManager'); open(me.screen); end + if me.useOperatorScreen && isa(me.operatorScreen,'screenManager'); open(me.operatorScreen); end + s = me.screen; + if me.useOperatorScreen; s2 = me.operatorScreen; end + me.win = me.screen.win; + me.ppd_ = me.screen.ppd; + + if ischar(me.calibration.calPositions); me.calibration.calPositions = str2num(me.calibration.calPositions); end + if ischar(me.calibration.valPositions); me.calibration.valPositions = str2num(me.calibration.valPositions); end + + fprintf('\n===>>> CALIBRATING PUPIL CORE... <<<===\n'); + + if strcmp(me.calibration.stimulus,'movie') + if isempty(me.stimulus.movie) || ~isa(me.stimulus.movie,'movieStimulus') + me.calStim = movieStimulus('size',me.calibration.size); + else + if ~isempty(me.calStim); try me.calStim.reset; end; end + me.calStim = me.movie.movie; + me.calStim.size = me.calibration.size; + end + else + if ~isempty(me.calStim); try me.calStim.reset; end; end + me.calStim = fixationCrossStimulus('size',me.calibration.size,'lineWidth',me.calibration.size/8,'type','pulse'); + end + + f = me.calStim; + + if true; return; end + + hide(f); + setup(f, me.screen); + + startRecording(me); + + KbName('UnifyKeyNames'); + one = KbName('1!'); two = KbName('2@'); three = KbName('3#'); + four = KbName('4$'); five = KbName('5%'); six = KbName('6^'); + seven = KbName('7&'); eight = KbName('8*'); nine = KbName('9('); + zero = KbName('0)'); esc = KbName('escape'); cal = KbName('c'); + val = KbName('v'); dr = KbName('d'); menu = KbName('LeftShift'); + sample = KbName('RightShift'); shot = KbName('F1'); + oldr = RestrictKeysForKbCheck([one two three four five six seven ... + eight nine zero esc cal val dr menu sample shot]); + + cpos = me.calibration.calPositions; + vpos = me.calibration.valPositions; + + me.validationData = struct(); + me.validationData(1).collected = false; + me.validationData(1).vpos = vpos; + me.validationData(1).time = datetime('now'); + me.validationData(1).data = cell(size(vpos,1),1); + me.validationData(1).dataS = me.validationData(1).data; + + loop = true; + ref = s.screenVals.fps; + a = -1; + mode = 'menu'; + + while loop + + switch mode + + case 'menu' + cloop = true; + resetAll(me); + while cloop + a = a + 1; + me.getSample(); + s.drawText('MENU: esc = exit | c = calibrate | v = validate | d = drift offset | F1 = screenshot'); + s.flip(); + if me.useOperatorScreen + s2.drawText('MENU: esc = exit | c = calibrate | v = validate | d = drift offset | F1 = screenshot'); + if ~isempty(me.x);s2.drawSpot(0.75,[0 1 0.25 0.2],me.x,me.y);end + drawValidationResults(me); + if mod(a,ref) == 0 + trackerFlip(me,0,true); + else + trackerFlip(me,1); + end + end + + [pressed,~,keys] = optickaCore.getKeys(); + if pressed + if keys(esc) + cloop = false; loop = false; + elseif keys(cal) + mode = 'calibrate'; cloop = false; + elseif keys(val) + mode = 'validate'; cloop = false; + elseif keys(dr) + mode = 'driftoffset'; cloop = false; + elseif keys(shot) + filename=[me.paths.parent filesep me.name '_' datestr(now,'YYYY-mm-DD-HH-MM-SS') '.png']; + captureScreen(s2, filename); + end + end + end + + case 'driftoffset' + trackerFlip(me,0,true); + oldrr = RestrictKeysForKbCheck([]); + driftOffset(me); + RestrictKeysForKbCheck(oldrr); + mode = 'menu'; + WaitSecs(0.5); + + case 'calibrate' + cloop = true; + thisX = 0; + thisY = 0; + lastK = 0; + thisPos = 1; + + me.validationData = struct(); + me.validationData(1).collected = false; + + f.xPositionOut = cpos(thisPos,1); + f.yPositionOut = cpos(thisPos,2); + update(f); + nPositions = size(cpos,1); + resetAll(me); + while cloop + a = a + 1; + me.getSample(); + drawGrid(s); + draw(f); + animate(f); + flip(s); + if me.useOperatorScreen + s2.drawText ('CALIBRATE: lshift = exit | # = point'); + s2.drawCross(1,[],thisX,thisY); + if ~isempty(me.x);s2.drawSpot(0.75,[0 1 0.25 0.1],me.x,me.y);end + if mod(a,ref) == 0 + trackerFlip(me,0,true); + else + trackerFlip(me,1); + end + end + + [pressed,name,keys] = optickaCore.getKeys(); + if pressed + fprintf('key: %s\n',name); + if length(name)==2 % assume a number + k = str2double(name(1)); + if k == 0 + hide(f); + for ii=1:length(cpos);me.turnOffLED(ii,rM);end + trackerFlip(me,0,true); + elseif k > 0 && k <= nPositions + thisPos = k; + if k == lastK && f.isVisible + f.isVisible = false; + me.turnOffLED(k,rM); + thisPos = 0; + elseif ~f.isVisible + f.isVisible = true; + me.turnOnLED(k,rM); + end + lastK = k; + if thisPos > 0 + thisX = vpos(thisPos,1); + thisY = vpos(thisPos,2); + f.xPositionOut = thisX; + f.yPositionOut = thisY; + update(f); + end + trackerFlip(me,0,true); + end + elseif keys(sample) + hide(f); + for ii=1:length(cpos);me.turnOffLED(ii,rM);end + trackerFlip(me,0,true); + rM.timedTTL; + elseif keys(menu) + trackerFlip(me,0,true); + mode = 'menu'; cloop = false; + elseif keys(val) + mode = 'validate'; cloop = false; + end + end + end + + case 'validate' + cloop = true; + thisPos = 1; lastK = thisPos; + thisX = vpos(thisPos,1); + thisY = vpos(thisPos,2); + f.xPositionOut = thisX; + f.yPositionOut = thisY; + update(f); + + if me.validationData(end).collected == false + me.validationData(end).collected = true; + else + me.validationData(end+1).collected = true; + end + me.validationData(end).vpos = vpos; + me.validationData(end).time = datetime('now'); + me.validationData(end).data = cell(size(vpos,1),1); + me.validationData(end).dataS = cell(size(vpos,1),1); + + resetFixationHistory(me); + nPositions = size(vpos,1); + while cloop + a = a + 1; + me.getSample(); + drawGrid(s); + draw(f); + animate(f); + flip(s); + if me.useOperatorScreen + s2.drawText('VALIDATE: lshift = exit | rshift = sample | # = point'); + if ~isempty(me.x); s2.drawSpot(0.75,[0 1 0.25 0.25],me.x,me.y); end + drawValidationResults(me); + if mod(a,ref) == 0 + trackerFlip(me,0,true); + else + trackerFlip(me,1); + end + end + + [pressed,name,keys] = optickaCore.getKeys(); + if pressed + fprintf('key: %s\n',name); + if length(name)==2 % assume a number + k = str2double(name(1)); + if k == 0 + resetFixationHistory(me); + thisPos = 0; + hide(f); + for ii=1:length(cpos);me.turnOffLED(ii,rM);end + trackerFlip(me,0,true); + elseif k > 0 && k <= nPositions + thisPos = k; + if k == lastK && f.isVisible + f.isVisible = false; + me.turnOffLED(k,rM); + thisPos = 0; + elseif ~f.isVisible + f.isVisible = true; + me.turnOnLED(k,rM); + end + lastK = k; + if thisPos > 0 + thisX = vpos(thisPos,1); + thisY = vpos(thisPos,2); + f.xPositionOut = thisX; + f.yPositionOut = thisY; + update(f); + end + trackerFlip(me,0,true); + end + elseif keys(sample) + if ~isempty(me.xAllRaw) + ld = length(me.xAllRaw); + sd = ld - me.rawSamples; + if sd < 1; sd = 1; end + me.validationData(end).data{lastK} = [me.xAllRaw(sd:ld); me.yAllRaw(sd:ld)]; + l=length(me.xAll); + if l > 5; l = 5; end + me.validationData(end).dataS{lastK} = [me.xAll(end-l:end); me.yAll(end-l:end)]; + end + rM.giveReward; + f.isVisible = false; + for ii=1:length(cpos);me.turnOffLED(ii,rM);end + thisPos = 0; + resetFixationHistory(me); + trackerFlip(me,0,true); + elseif keys(menu) + mode = 'menu'; cloop = false; + end + end + end + end + end + s.drawText('Calibration finished...'); + s2.drawText('Calibration finished...') + s.flip(); s2.flip(); s2.drawBackground; s2.flip(); + reset(f); + resetAll(me); + RestrictKeysForKbCheck(oldr); + stopRecording(me); + WaitSecs(0.25); + fprintf('===>>> CALIBRATING CORE FINISHED... <<<===\n'); + end + + % =================================================================== + function startRecording(me, ~) + %> @fn startRecording(me,~) + %> @brief startRecording - for iRec this just starts TCP online + %> access, all data is saved to CSV irrespective of this + %> + % =================================================================== + if me.isDummy; return; end + if me.isConnected + zmq.core.send(me.socket, uint8('R')); + result = zmq.core.recv(me.socket); + fprintf('Recording should start: %s\n', char(result)); + me.isRecording = true; + end + end + + % =================================================================== + function stopRecording(me, ~) + %> @fn stopRecording(me,~) + %> @brief stopRecording - for iRec this just stops TCP online + %> access, all data is saved to CSV irrespective of this + %> + % =================================================================== + if me.isDummy; return; end + if me.isConnected && ~isempty(me.socket) + result = []; + try zmq.core.send(me.socket, uint8('R')); end + try result = zmq.core.recv(me.socket); end + fprintf('Recording stopped: %s\n', char(result)); + me.isRecording = false; + end + end + + % =================================================================== + function sample = getSample(me) + %> @fn getSample() + %> @brief get latest sample from the tracker, if dummymode=true then use + %> the mouse as an eye signal + %> + % =================================================================== + sample = me.sampleTemplate; + if me.isDummy %lets use a mouse to simulate the eye signal + if ~isempty(me.win) + [mx, my] = GetMouse(me.win); + else + [mx, my] = GetMouse([]); + end + sample.valid = true; + me.pupil = 5 + randn; + sample.gx = mx; + sample.gy = my; + sample.pa = me.pupil; + sample.time = GetSecs; + me.x = me.toDegrees(sample.gx,'x'); + me.y = me.toDegrees(sample.gy,'y'); + me.xAll = [me.xAll me.x]; + me.xAllRaw = me.xAll; + me.yAll = [me.yAll me.y]; + me.yAllRaw = me.yAll; + me.pupilAll = [me.pupilAll me.pupil]; + %if me.verbose;fprintf('>>X: %.2f | Y: %.2f | P: %.2f\n',me.x,me.y,me.pupil);end + elseif me.isConnected && me.isRecording + xy = []; + td = me.tcp.readLines(me.smoothing.nSamples,'last'); + if isempty(td); me.currentSample=sample; return; end + td = str2num(td); %#ok<*ST2NM> + sample.raw = td; + sample.time = td(end,1); + sample.timeD = GetSecs; + xy(1,:) = td(:,2)'; + xy(2,:) = -td(:,3)'; + if ~isempty(xy) + me.xAllRaw = [me.xAllRaw xy(1,:)]; + me.yAllRaw = [me.yAllRaw xy(2,:)]; + sample.valid = true; + xy = doSmoothing(me,xy); + sample.gx = xy(1); + sample.gy = xy(2); + sample.pa = median(td(:,4)); + me.x = xy(1); + me.y = xy(2); + me.pupil = sample.pa; + if me.verbose;fprintf('>>X: %2.2f | Y: %2.2f | P: %.2f\n',me.x,me.y,me.pupil);end + else + sample.gx = NaN; + sample.gy = NaN; + sample.pa = NaN; + me.x = NaN; + me.y = NaN; + me.pupil = NaN; + end + me.xAll = [me.xAll me.x]; + me.yAll = [me.yAll me.y]; + me.pupilAll = [me.pupilAll me.pupil]; + else + me.x = []; me.y = []; me.pupil = []; + if me.verbose;fprintf('-+-+-> pupilCore.getSample(): are you sure you are recording?\n');end + end + me.currentSample = sample; + end + + % =================================================================== + function trackerMessage(me, message, ~) + %> @fn trackerMessage(me, message) + %> @brief Send message to store in tracker data + %> + % =================================================================== + if me.isDummy || ~me.isConnected; return; end + sendAnnotation(me,message); + if me.verbose; fprintf('-+-+->pupilCore: %i\n', message);end + + end + + % =================================================================== + function close(me) + %> @fn close(me) + %> @brief close the iRec and cleanup, call after experiment finishes + %> + % =================================================================== + try + try stopRecording(me); end + try unsubscribe(me); end + try zmq.core.disconnect(me.socket, me.endpoint); end + try zmq.core.close(me.socket); end + try zmq.core.ctx_shutdown(me.ctx); end + try zmq.core.ctx_term(me.ctx); end + + me.socket = []; + me.sub = []; me.pub = []; + me.subEndpoint = []; me.pubEndpoint = []; + me.ctx = []; + + me.isConnected = false; + me.isRecording = false; + resetAll(me); + if ~isempty(me.operatorScreen) && isa(me.operatorScreen,'screenManager') + try close(me.operatorScreen); end + end + catch ME + getReport(ME); + me.salutation('Close Method','Couldn''t stop recording, forcing shutdown...',true) + me.isConnected = false; + me.isRecording = false; + if ~isempty(me.operatorScreen) && isa(me.operatorScreen,'screenManager') + try close(me.operatorScreen); end + end + try stopRecording(me); end + try unsubscribe(me); end + try zmq.core.disconnect(me.socket, me.endpoint); end + try zmq.core.close(me.socket); end + try zmq.core.ctx_shutdown(me.ctx); end + try zmq.core.ctx_term(me.ctx); end + try resetAll(me); end + end + end + + % =================================================================== + function runDemo(me, forcescreen) + %> @fn runDemo(me, forceScreen) + %> @brief runs a demo of this class, useful for testing + %> + %> @param forcescreen forces to use a specific screen number + % =================================================================== + KbName('UnifyKeyNames') + stopkey = KbName('q'); + upKey = KbName('uparrow'); + downKey = KbName('downarrow'); + leftKey = KbName('leftarrow'); + rightKey = KbName('rightarrow'); + calibkey = KbName('c'); + driftkey = KbName('d'); + ofixation = me.fixation; + osmoothing = me.smoothing; + oldexc = me.exclusionZone; + oldfixinit = me.fixInit; + oldname = me.name; + me.name = 'pupilLabs-runDemo'; + try + if ~me.isConnected; initialise(me);end + s = me.screen; s2 = me.operatorScreen; + s.font.FontName = me.monoFont; + if exist('forcescreen','var'); close(s); s.screen = forcescreen; end + s.disableSyncTests = true; s2.disableSyncTests = true; + if ~s.isOpen; open(s); end + if me.useOperatorScreen && ~s2.isOpen; s2.open(); end + sv = s.screenVals; + + trackerSetup(me); + + drawPhotoDiodeSquare(s,[0 0 0 1]); flip(s); %make sure our photodiode patch is black + + % set up the size and position of the stimulus + o = dotsStimulus('size',me.fixation.radius(1)*2,'speed',2,'mask',true,'density',50); %test stimulus + if length(me.fixation.radius) == 1 + f = discStimulus('size',me.fixation.radius(1)*2,'colour',[0 0 0],'alpha',0.25); + else + f = barStimulus('barWidth',me.fixation.radius(1)*2,'barHeight',me.fixation.radius(2)*2,... + 'colour',[0 0 0],'alpha',0.25); + end + setup(o,s); %setup our stimulus with open screen + setup(f,s); %setup our stimulus with open screen + o.xPositionOut = me.fixation.X; + o.yPositionOut = me.fixation.Y; + f.alpha + f.xPositionOut = me.fixation.X; + f.xPositionOut = me.fixation.X; + + methodl={'median','heuristic1','heuristic2','sg','simple'}; + eyel={'both','left','right'}; + m = 1; n = 1; + trialn = 1; + maxTrials = 5; + endExp = false; + + % set up an exclusion zone where eye is not allowed + me.exclusionZone = [8 10 8 10]; + exc = me.toPixels(me.exclusionZone); + exc = [exc(1) exc(3) exc(2) exc(4)]; %psychrect=[left,top,right,bottom] + + startRecording(me); + WaitSecs('YieldSecs',0.5); + + trackerMessage(me,0) + while trialn <= maxTrials && ~endExp + trialtick = 1; + drawPhotoDiodeSquare(s,[0 0 0 1]); + trackerDrawStatus(me,'Start Trial'); + resetFixation(me); + vbl = flip(s); tstart = vbl + sv.ifi; + trackerMessage(me, trialn); + while vbl < tstart + 6 + Screen('FillRect', s.win, [0.7 0.7 0.7 0.5],exc); Screen('DrawText',s.win,'Exclusion Zone',exc(1),exc(2),[0.8 0.8 0.8]); + drawGrid(s); draw(o); draw(f); + drawCross(s, 0.5, [1 1 0], me.fixation.X, me.fixation.Y); + drawPhotoDiodeSquare(s, [1 1 1 1]); + + getSample(me); isFixated(me); + + if ~isempty(me.currentSample) + txt = sprintf('Q = finish. X: %3.1f / %2.2f | Y: %3.1f / %2.2f | # = %2i %s %s | RADIUS = %s | TIME = %.2f | FIXATION = %.2f | EXC = %i | INIT FAIL = %i',... + me.currentSample.gx, me.x, me.currentSample.gy, me.y, me.smoothing.nSamples,... + me.smoothing.method, me.smoothing.eyes, sprintf('%1.1f ',me.fixation.radius), ... + me.fixTotal,me.fixLength,me.isExclusion,me.isInitFail); + Screen('DrawText', s.win, txt, 10, 10,[1 1 1]); + if ~me.useOperatorScreen;drawEyePosition(me,true);end + end + animate(o); + + if me.useOperatorScreen + trackerDrawExclusion(me); + trackerDrawFixation(me); + trackerDrawEyePosition(me); + end + + vbl(end+1) = Screen('Flip', s.win, vbl(end) + s.screenVals.halfifi); + if me.useOperatorScreen; trackerFlip(me); end + + [keyDown, ~, keyCode] = optickaCore.getKeys(); + if keyDown + if keyCode(stopkey); endExp = true; break; + elseif keyCode(calibkey); me.trackerSetup; + elseif keyCode(upKey); me.smoothing.nSamples = me.smoothing.nSamples + 1; if me.smoothing.nSamples > 400; me.smoothing.nSamples=400;end + elseif keyCode(downKey); me.smoothing.nSamples = me.smoothing.nSamples - 1; if me.smoothing.nSamples < 1; me.smoothing.nSamples=1;end + elseif keyCode(leftKey); m=m+1; if m>5;m=1;end; me.smoothing.method = methodl{m}; + end + end + + trialtick=trialtick+1; + end + if endExp == false + drawPhotoDiodeSquare(s,[0 0 0 1]); + vbl = flip(s); + trackerMessage(me,-1); + + if me.useOperatorScreen; trackerDrawStatus(me,'Finished Trial'); end + + resetAll(me); + + me.fixation.X = randi([-7 7]); + me.fixation.Y = randi([-7 7]); + if length(me.fixation.radius) == 1 + me.fixation.radius = randi([1 3]); + o.sizeOut = me.fixation.radius * 2; + f.sizeOut = me.fixation.radius * 2; + else + me.fixation.radius = [randi([1 3]) randi([1 3])]; + o.sizeOut = mean(me.fixation.radius) * 2; + f.barWidthOut = me.fixation.radius(1) * 2; + f.barHeightOut = me.fixation.radius(2) * 2; + end + o.xPositionOut = me.fixation.X; + o.yPositionOut = me.fixation.Y; + f.xPositionOut = me.fixation.X; + f.yPositionOut = me.fixation.Y; + update(o);update(f); + WaitSecs(0.5); + trialn = trialn + 1; + else + drawPhotoDiodeSquare(s,[0 0 0 1]); + vbl = flip(s); + trackerMessage(me,-100); + end + end + WaitSecs(0.5); + stopRecording(me); + ListenChar(0); Priority(0); ShowCursor; + try close(s); close(s2); reset(o); reset(f); end %#ok<*TRYNC> + close(me); + me.fixation = ofixation; + me.smoothing = osmoothing; + me.exclusionZone = oldexc; + me.fixInit = oldfixinit; + me.name = oldname; + clear s s2 o + catch ME + stopRecording(me); + me.fixation = ofixation; + me.smoothing = osmoothing; + me.exclusionZone = oldexc; + me.fixInit = oldfixinit; + me.name = oldname; + ListenChar(0);Priority(0);ShowCursor; + getReport(ME) + try close(s); end + try close(s2); end + sca; + try close(me); end + clear s s2 o + rethrow(ME) + end + + end + + end%-------------------------END PUBLIC METHODS--------------------------------% + + %============================================================================ + methods (Hidden = true) %--HIDDEN METHODS + %============================================================================ + + function result = remoteCommand(me, cmd) + if ~me.isConnected; result = []; return; end + zmq.core.send(me.socket, uint8(cmd)); + result = char(zmq.core.recv(me.socket)); + if me.verbose; fprintf('--->>> remoteCommand result: %s\n', result); end + end + + function checkRoundTrip(me) + tx=zeros(100,1); + for i = 1:100 + tt = tic; % Measure round trip delay + remoteCommand(me, 't'); + tx(i) = toc(tt); + end + tx = tx .* 1e3; + [a, e] = analysisCore.stderr(tx,'SE'); + fprintf('--->>> Round trip command delay: %.2f ms +- %.2f SE\n', a, e); + end + + function setPupilTime(me, t) + if ~exist('t','var') || isempty(t); t = GetSecs(); end + if isnumeric(t); t = num2str(t); end + result = remoteCommand(me,['T ' t]); + fprintf('--->>> setPupilTime: time %s result %s\n', t, result); + end + + % =================================================================== + %> @brief Sync time with tracker + %> + % =================================================================== + function syncTrackerTime(varargin) + + end + + % =================================================================== + %> @brief Save the data + %> + % =================================================================== + function saveData(varargin) + + end + % =================================================================== + %> @brief + %> + % =================================================================== + function updateDefaults(varargin) + + end + + % =================================================================== + %> @brief checks which eye is available, force left eye if + %> binocular is enabled + %> + % =================================================================== + function eyeUsed = checkEye(me) + if me.isConnected + eyeUsed = me.eyeUsed; + end + end + + % =================================================================== + %> @brief displays status message on tracker, only sets it if + %> message is not the previous message, so loop safe. + %> + % =================================================================== + function statusMessage(me,message) + if me.isConnected + if me.verbose; fprintf('-+-+->iRec status message: %s\n',message);end + end + end + + % =================================================================== + %> @brief send message to store in tracker data (compatibility) + %> + %> + % =================================================================== + function edfMessage(me, message) + trackerMessage(me,message) + end + + % =================================================================== + %> @brief + %> + % =================================================================== + function setup(me) + initialise(me) + end + + % =================================================================== + %> @brief set into offline / idle mode + %> + % =================================================================== + function setOffline(me) + + end + + % =================================================================== + %> @brief check the connection with the tobii + %> + % =================================================================== + function connected = checkConnection(me) + connected = me.isConnected; + end + + + % =================================================================== + %> @brief wrapper for EyelinkDoDriftCorrection + %> + % =================================================================== + function success = driftCorrection(me) + success = driftOffset(me); + end + + % =================================================================== + %> @brief check what mode the is in + %> + % ========================a=========================================== + function mode = currentMode(me) + mode = 0; + end + + % =================================================================== + %> @brief Sync time with tracker: send int32(-1000) + %> + % =================================================================== + function syncTime(me) + + end + + + % =================================================================== + %> @brief Get offset between tracker and display computers + %> + % =================================================================== + function offset = getTimeOffset(me) + [trackertime, systemtime] = getTrackerTime(me); + offset = systemtime - trackertime; + end + + % =================================================================== + %> @brief Get tracker time + %> + % =================================================================== + function [trackertime, systemtime] = getTrackerTime(me) + trackertime = str2num(remoteCommand(me,'t')); + systemtime = GetSecs; + end + + % =================================================================== + %> @brief + %> + % =================================================================== + function value = checkRecording(me) + if me.isConnected + value = true; + else + value = false; + end + end + + end%-------------------------END HIDDEN METHODS--------------------------------% + + %======================================================================= + methods (Hidden = true) %------------------PRIVATE METHODS + %======================================================================= + + function subscribe(me) + if ~me.isConnected; return; end + subPort = remoteCommand(me,'SUB_PORT'); + if subPort == -1; warning('Cannot SUBscribe!'); return;end + pubPort = remoteCommand(me,'PUB_PORT'); + me.subEndpoint = ['tcp://' me.calibration.ip ':' subPort]; + me.pubEndpoint = ['tcp://' me.calibration.ip ':' pubPort]; + fprintf('--->>> Received sub/pub port: %s \n', subPort, pubPort); + me.sub = zmq.core.socket(me.ctx, 'ZMQ_SUB'); + me.pub = zmq.core.socket(me.ctx, 'ZMQ_PUB'); + zmq.core.setsockopt(me.sub, 'ZMQ_RCVTIMEO', me.calibration.timeout); + zmq.core.setsockopt(me.pub, 'ZMQ_RCVTIMEO', me.calibration.timeout); + + err = zmq.core.connect(me.sub, me.subEndpoint); + assert(err==0, '--->>> PupilLabs: Cannot subscribe to data stream!'); + + zmq.core.setsockopt(me.sub, 'ZMQ_SUBSCRIBE', 'pupil.'); + zmq.core.setsockopt(me.sub, 'ZMQ_SUBSCRIBE', 'gaze.'); + zmq.core.setsockopt(me.sub, 'ZMQ_SUBSCRIBE', 'notify.'); + + err = zmq.core.connect(me.pub, me.pubEndpoint); + assert(err==0, '--->>> PupilLabs: Cannot subscribe to Publish stream!'); + end + + + function unsubscribe(me) + if isempty(me.sub) || isempty(me.subEndpoint); return; end + try + zmq.core.disconnect(me.sub, me.subEndpoint); + zmq.core.close(me.sub); + fprintf('--->>> PupilLabs: Disconnected from SUB: %s\n', me.subEndpoint); + end + if isempty(me.pub) || isempty(me.pubEndpoint); return; end + try + zmq.core.disconnect(me.pub, me.pubEndpoint); + zmq.core.close(me.psub); + fprintf('--->>> PupilLabs: Disconnected from PUB: %s\n', me.pubEndpoint); + end + end + + function [topic, payload] = receiveMessage(me) + % Use socket to receive topics and their messages + % Messages are 2-frame zmq messages that include the topic + % and the message payload as a msgpack encoded string. + topic = []; payload = []; + if isempty(me.sub) || isempty(me.subEndpoint); return; end + topic = char(zmq.core.recv(me.sub), 255, 'ZMQ_DONTWAIT'); + lastwarn(''); % reset last warning + payload = zmq.core.recv(me.sub, 1024, 'ZMQ_DONTWAIT'); % receive payload + [~, warnId] = lastwarn; % fetch possible buffer length warning + if isequal(warnId, 'zmq:core:recv:bufferTooSmall') + payload = false; % set payload to false since it is incomplete + disp('Buffer too small'); + else + payload = parsemsgpack(payload); % parse payload + end + end + + function [ ] = sendNotification(me, notification, time) + %NOTIFY Use socket to send notification + % Notifications are container.Map objects that contain + % at least the key 'subject'. + topic = strcat('notify.', notification('subject')); + payload = dumpmsgpack(notification); + zmq.core.send(me.pub, uint8(topic), 'ZMQ_SNDMORE'); + zmq.core.send(me.pub, payload); + end + + function [ ] = sendAnnotation(me, annotation, time) + %NOTIFY Use socket to send notification + % Notifications are container.Map objects that contain + % at least the key 'subject'. + ts = str2num(char(remoteCommand(me,'t'))); + if ischar(annotation) + annot = dictionary(string([]),{}); + annot{"topic"} = 'annotation.general'; + annot{"label"} = annotation; + annot{"timestamp"} = ts; + annot{"duration"} = 0.0; + end + topic = annot{"topic"}; + payload = dumpmsgpack(annot); + zmq.core.send(me.pub, uint8(topic), 'ZMQ_SNDMORE'); + zmq.core.send(me.pub, payload); + end + + function msgs = flushBuffer(me) + msgs = []; + end + + end %------------------END PRIVATE METHODS +end diff --git a/eyetracker/pupilCoreStimulus.m b/eyetracker/pupilCoreStimulus.m new file mode 100644 index 0000000000000000000000000000000000000000..0ba729cc2a6cc8a79cd9ae6ad2e030fb3fc44743 --- /dev/null +++ b/eyetracker/pupilCoreStimulus.m @@ -0,0 +1,199 @@ +% ======================================================================== +%> @brief single disc stimulus, inherits from baseStimulus +%> SPOTSTIMULUS single spot stimulus, inherits from baseStimulus +%> The current properties are: +%> +%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md +% ======================================================================== +classdef pupilCoreStimulus < baseStimulus + + properties %--------------------PUBLIC PROPERTIES----------% + %> type can be "simple" or "flash" + type char = 'simple' + %> colour for flash, empty to inherit from screen background with 0 alpha + flashColour double = [] + %> time to flash on and off in seconds + flashTime double {mustBeVector(flashTime)} = [0.25 0.25] + %> is the ON flash the first flash we see? + flashOn logical = true + %> contrast scales from foreground to screen background colour + contrast double {mustBeInRange(contrast,0,1)} = 1 + %> stop marker? + stop = false + end + + properties (SetAccess = protected, GetAccess = public) + %> stimulus family + family char = 'marker' + end + + properties (SetAccess = private, GetAccess = public, Hidden = true) + typeList cell = {'simple','flash'} + end + + properties (Dependent = true, SetAccess = private, GetAccess = private) + %> a dependant property to track when to switch from ON to OFF of + %flash. + flashSwitch + end + + properties (SetAccess = private, GetAccess = private) + %> current flash state + flashState + %> internal counter + flashCounter = 1 + %> the OFF colour of the flash, usually this is set to the screen background + flashBG = [0.5 0.5 0.5] + %> ON flash colour, reset on setup + flashFG = [1 1 1] + currentColour = [1 1 1] + colourOutTemp = [1 1 1] + flashColourOutTemp = [1 1 1] + isInCompute = false + allowedProperties={'type', 'flashTime', 'flashOn', 'flashColour', 'contrast'} + ignoreProperties = {'flashSwitch','flashOn'} + end + + %======================================================================= + methods %------------------PUBLIC METHODS + %======================================================================= + + % =================================================================== + %> @brief Class constructor + %> + %> More detailed description of what the constructor does. + %> + %> @param args are passed as a structure of properties which is + %> parsed. + %> @return instance of the class. + % =================================================================== + function me = pupilCoreStimulus(varargin) + args = optickaCore.addDefaults(varargin,... + struct('name','pupilcorestim','colour',[1 1 0 1])); + me=me@baseStimulus(args); %we call the superclass constructor first + me.parseArgs(args, me.allowedProperties); + + me.isRect = false; %uses a rect for drawing? + + me.ignoreProperties = [me.ignorePropertiesBase me.ignoreProperties]; + me.salutation('constructor','Stimulus initialisation complete'); + end + + % =================================================================== + %> @brief Setup an structure for runExperiment + %> + %> @param sM handle to the current screenManager object + % =================================================================== + function setup(me,sM) + + reset(me); %reset object back to its initial state + me.inSetup = true; me.isSetup = false; + if isempty(me.isVisible); show(me); end + + me.sM = sM; + if ~sM.isOpen; error('Screen needs to be Open!'); end + me.ppd=sM.ppd; + me.screenVals = sM.screenVals; + me.texture = []; %we need to reset this + + fn = sort(properties(me)); + for j=1:length(fn) + if ~matches(fn{j}, me.ignoreProperties)%create a temporary dynamic property + p=me.addprop([fn{j} 'Out']); + if strcmp(fn{j},'xPosition'); p.SetMethod = @set_xPositionOut; end + if strcmp(fn{j},'yPosition'); p.SetMethod = @set_yPositionOut; end + me.([fn{j} 'Out']) = me.(fn{j}); %copy our property value to our tempory copy + end + end + + addRuntimeProperties(me); + + me.inSetup = false; me.isSetup = true; + computePosition(me); + setAnimationDelta(me); + if me.doAnimator;setup(me.animator, me);end + + function set_xPositionOut(me, value) + me.xPositionOut = value * me.ppd; + end + function set_yPositionOut(me,value) + me.yPositionOut = value * me.ppd; + end + + end + + % =================================================================== + %> @brief Update a structure for runExperiment + %> + %> @param + %> @return + % =================================================================== + function update(me) + resetTicks(me); + me.isInCompute = false; + me.inSetup = false; + computePosition(me); + setAnimationDelta(me); + end + + % =================================================================== + %> @brief Draw an structure for runExperiment + %> + %> @param sM runExperiment object for reference + %> @return stimulus structure. + % =================================================================== + function draw(me) + if me.isVisible && me.tick >= me.delayTicks && me.drawTick < me.offTicks + me.sM.drawPupilCoreMarker(me.sizeOut,me.xFinalD,me.yFinalD,me.stop); + me.drawTick = me.drawTick + 1; + end + if me.isVisible; me.tick = me.tick + 1; end + end + + % =================================================================== + %> @brief Animate an structure for runExperiment + %> + %> @param sM runExperiment object for reference + %> @return stimulus structure. + % =================================================================== + function animate(me) + if me.isVisible && me.tick >= me.delayTicks + if me.mouseOverride + getMousePosition(me); + if me.mouseValid + me.xFinal = me.mouseX; + me.yFinal = me.mouseY; + end + end + if me.doMotion == true + me.xFinal = me.xFinal + me.dX_; + me.yFinal = me.yFinal + me.dY_; + me.xFinalD = me.sM.toDegrees(me.xFinal,'x'); + me.yFinalD = me.sM.toDegrees(me.yFinal,'y'); + end + end + end + + % =================================================================== + %> @brief Reset an structure for runExperiment + %> + %> @param sM runExperiment object for reference + %> @return stimulus structure. + % =================================================================== + function reset(me) + resetTicks(me); + removeTmpProperties(me); + me.texture=[]; + me.isInCompute = false; + me.inSetup = false; me.isSetup = false; + end + + + end %---END PUBLIC METHODS---% + + %======================================================================= + methods ( Access = protected ) %-------PROTECTED METHODS-----% + %======================================================================= + + end +end \ No newline at end of file diff --git a/eyetracker/tittaAdvCalTest.m b/eyetracker/tittaAdvCalTest.m new file mode 100644 index 0000000000000000000000000000000000000000..ca56046fb28e5d92474c92744c2aed2f629c974a --- /dev/null +++ b/eyetracker/tittaAdvCalTest.m @@ -0,0 +1,388 @@ +% this demo code is part of Titta, a toolbox providing convenient access to +% eye tracking functionality using Tobii eye trackers +% +% Titta can be found at https://github.com/dcnieho/Titta. Check there for +% the latest version. +% When using Titta, please cite the following paper: +% +% Niehorster, D.C., Andersson, R. & Nystrom, M., (2020). Titta: A toolbox +% for creating Psychtoolbox and Psychopy experiments with Tobii eye +% trackers. Behavior Research Methods. +% doi: https://doi.org/10.3758/s13428-020-01358-8 + +% This version of readme.m demonstrates operation with separate +% presentation and operator screens. It furthermore demonstrates a +% procedure that is implemented using Titta's advanced calibration mode and +% designed for working with nonhuman primates (subjects unable to follow +% instructions). This version uses a controller for automatic positioning +% and calibration training, and uses video stimuli as calibration targets. +% Finally, different from the other example scripts, continuing to the +% stimuli is gaze contingent, a stimulus is shown after the fixation point +% is gaze at long enough, and gaze-contingent rewards are provided during +% data collection. +% +% NB: some care is taken to not update operator screen during timing +% critical bits of main script +% NB: this code assumes main and secondary screen have the same resolution. +% Titta's setup displays work fine if this is not the case, but the +% real-time gaze display during the mock experiment is not built for that. +% So if your two monitors have different resolutions, either adjust the +% code, or look into solutions e.g. with PsychImaging()'s 'UsePanelFitter'. + +clear +sca + +DEBUGlevel = 0; +fixClrs = [0 255]; +bgClr = 127; +eyeColors = {[255 127 0],[0 95 191]}; % for live data view on operator screen +videoFolder = '/home/cog/Videos/'; +videoExt = 'mp4'; +numCalPoints = 2; % 2, 3 or 5 +forceRewardButton = 'j'; +skipButton = 'x'; +if IsWin + scrParticipant = 1; + scrOperator = 2; + address = []; %leave empty to autodiscover +else + scrParticipant = 1; + scrOperator = 0; + address = 'tet-tcp://169.254.7.39'; %edit for your explicit address, +end +% task parameters +imageTime = 4; +% gaze contingent fixation point setup +fixMargin = 100; % square area around fixation point with sides 2x margin (pixels) +fixMinDur = 0.35; % s +fixRectColor = [255 255 0]; % to draw on operator display +% live view parameters +dataWindowDur = .5; % s + +% You can run addTittaToPath once to "install" it, or you can simply add a +% call to it in your script so each time you want to use Titta, it is +% ensured it is on path +home = cd; +cd ..; +addTittaToPath; +cd(home); + +try + eyeColors = cellfun(@color2RGBA,eyeColors,'uni',false); + + % get setup struct (can edit that of course): + settings = Titta.getDefaults('Tobii Pro Spectrum'); + % request some debug output to command window, can skip for normal use + settings.debugMode = true; + % tracking mode + settings.trackingMode = 'small_monkey'; + % customize colors of setup and calibration interface (yes, colors of + % everything can be set, so there is a lot here). + % operator screen + settings.UI.advcal.bgColor = bgClr; + settings.UI.advcal.fixBackColor = fixClrs(1); + settings.UI.advcal.fixFrontColor = fixClrs(2); + settings.UI.advcal.fixPoint.text.color = fixClrs(1); + settings.UI.advcal.avg.text.color = fixClrs(1); + settings.UI.advcal.instruct.color = fixClrs(1); + settings.UI.advcal.gazeHistoryDuration = dataWindowDur; + % setup what is shown on operator display + settings.UI.advcal.fixPoint.text.size = 24; + settings.UI.advcal.showHead = true; % show head display when interface opens + settings.UI.advcal.headScale = .30; + settings.UI.advcal.headPos = [.5 .15]; + settings.UI.advcal.instruct.size = 54; + settings.UI.advcal.instruct.strFun = @(x,y,z,rx,ry,rz) sprintf('X: %.1f cm, Y: %.1f cm\nDistance: %.1f cm',x,y,z); + % calibration display + settings.advcal.cal.pointPos = [settings.advcal.cal.pointPos; .65, .35; .35, .65]; + settings.advcal.val.pointPos = [.2 1/3; .4 1/3; .6 1/3; .8 1/3; .2 2/3; .4 2/3; .6 2/3; .8 2/3]; + % calibration display: custom calibration drawer + calViz = tittaAdvMovieStimulus(); + settings.advcal.drawFunction= @calViz.doDraw; + calViz.bgColor = bgClr; + % calibration logic: custom controller + rewardProvider = tittaRewardProvider(); + calController = tittaAdvancedController([],calViz,[],rewardProvider); + % hook up our controller with the state notifications provided by + % Titta.calibrateAdvanced (request extended notification) so that the + % calibration controller can keep track of whats going on and issue + % appropriate commands. + settings.advcal.cal.pointNotifyFunction = @calController.receiveUpdate; + settings.advcal.val.pointNotifyFunction = @calController.receiveUpdate; + settings.advcal.cal.useExtendedNotify = true; + settings.advcal.val.useExtendedNotify = true; + % show the button to start the controller. + settings.UI.button.advcal.toggAuto.visible = true; + if numCalPoints==2 + calPoints = [6 7]; + elseif numCalPoints==3 + calPoints = [3 6 7]; + elseif numCalPoints==5 + calPoints = [3 1 2 4 5]; + end + calController.setCalPoints(calPoints,settings.advcal.cal.pointPos(calPoints,:)); + if ismember(numCalPoints,[3 5]) + % issue a calibration computation command after collecting the + % first point (in the screen center!) before continuing to collect + % calibration data for the other points? + calController.calAfterFirstCollected = true; + end + calController.setValPoints([1:size(settings.advcal.val.pointPos,1)],settings.advcal.val.pointPos); %#ok + calController.forceRewardButton = forceRewardButton; + calController.skipTrainingButton = skipButton; + + if DEBUGlevel>0 + calController.logTypes = 1+2*(DEBUGlevel==2)+4; % always log actions calController is taking and reward state changes. Additionally log info about received commands when DEBUGlevel==2 + end + calController.logReceiver = 1; + + % init + EThndl = Titta(settings); + % EThndl = EThndl.setDummyMode(); % just for internal testing, enabling dummy mode for this readme makes little sense as a demo + EThndl.init(address); + calController.EThndl = EThndl; + nLiveDataPoint = ceil(dataWindowDur*EThndl.frequency); + + if DEBUGlevel>1 + % make screen partially transparent on OSX and windows vista or + % higher, so we can debug. + PsychDebugWindowConfiguration; + end + if DEBUGlevel + % Be pretty verbose about information and hints to optimize your code and system. + Screen('Preference', 'Verbosity', 4); + Screen('Preference', 'VisualDebugLevel', 2); + else + % Only output critical errors and warnings. + Screen('Preference', 'Verbosity', 2); + Screen('Preference', 'VisualDebugLevel', 2); + end + + PsychDefaultSetup(2); + [w,h] = Screen('WindowSize',0); + winrect = [0 0 round(w/1.25) round(h/1.25)]; + PsychImaging('PrepareConfiguration'); + PsychImaging('AddTask', 'General', 'UseFastOffscreenWindows'); + Screen('Preference', 'SyncTestSettings', 0.002); % the systems are a little noisy, give the test a little more leeway + [wpntP,winRectP] = PsychImaging('OpenWindow', 0, bgClr, winrect, [], [], [], 4, [], kPsychGUIWindow); + + PsychImaging('PrepareConfiguration'); + PsychImaging('AddTask', 'General', 'UseFastOffscreenWindows'); + winrect(1)=20; + [wpntO,winRectO] = PsychImaging('OpenWindow', 0, bgClr, winrect, [], [], [], 4, [], kPsychGUIWindow); + + hz=Screen('NominalFrameRate', wpntP); + Priority(1); + Screen('BlendFunction', wpntP, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + Screen('BlendFunction', wpntO, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + Screen('Preference', 'TextAlphaBlending', 1); + Screen('Preference', 'TextAntiAliasing', 2); + % This preference setting selects the high quality text renderer on + % each operating system: It is not really needed, as the high quality + % renderer is the default on all operating systems, so this is more of + % a "better safe than sorry" setting. + Screen('Preference', 'TextRenderer', 1); + KbName('UnifyKeyNames'); % for correct operation of the setup/calibration interface, calling this is required + + calController.scrRes = winRectP(3:4); + + vids = FileFromFolder(videoFolder, [], videoExt); + vids = arrayfun(@(x) fullfile(x.folder,x.name), vids, 'uni', false); + vp = VideoPlayer(wpntP,vids); + vp.start(); + calViz.setVideoPlayer(vp); + + % do calibration + try + ListenChar(-1); + catch ME + % old PTBs don't have mode -1, use 2 instead which also supresses + % keypresses from leaking through to matlab + ListenChar(2); + end + tobii.calVal{1} = EThndl.calibrateAdvanced([wpntP wpntO],[],calController); + ListenChar(0); + + % prep stimuli (get rabbits) - preload these before the trials to + % ensure good timing + rabbits = loadStimuliFromFolder(fullfile(PsychtoolboxRoot,'PsychDemos'),{'konijntjes1024x768.jpg','konijntjes1024x768blur.jpg'},wpntP,winRectP(3:4)); + % prep gaze areas for gaze-contingent stimulus showing and for reward + % when looking at the stimulus + fixRect = bsxfun(@plus,[-fixMargin fixMargin fixMargin -fixMargin -fixMargin; -fixMargin -fixMargin fixMargin fixMargin -fixMargin],[winRectP(3); winRectP(4)]/2); + for r=1:length(rabbits) + rabbits(r).scrPoly = [rabbits(r).scrRect([1 3 3 1 1]); rabbits(r).scrRect([2 2 4 4 2])]; + end + + % later: + EThndl.buffer.start('gaze'); + WaitSecs(.8); % wait for eye tracker to start and gaze to be picked up + + % send message into ET data file + EThndl.sendMessage('test'); + + % First draw a fixation point + Screen('gluDisk',wpntP,fixClrs(1),winRectP(3)/2,winRectP(4)/2,round(winRectP(3)/100)); + startT = Screen('Flip',wpntP); + % log when fixation dot appeared in eye-tracker time. NB: + % system_timestamp of the Tobii data uses the same clock as + % PsychToolbox, so startT as returned by Screen('Flip') can be used + % directly to segment eye tracking data + tobiiStartT = EThndl.sendMessage('FIX ON',startT); + + % now update also operator screen, once timing critical bit is done + % if we still have enough time till next flipT, update operator display + rewarding = false; + while true + Screen('gluDisk',wpntO,fixClrs(1),winRectO(3)/2,winRectO(4)/2,round(winRectO(3)/100)); + Screen('FrameRect',wpntO,fixRectColor,[fixRect(:,1) fixRect(:,3)],4); + drawLiveData(wpntO,EThndl.buffer.peekN('gaze',nLiveDataPoint),dataWindowDur,eyeColors{:},4,winRectO(3:4)); + rewardTxt = 'off'; + if rewarding + rewardTxt = 'on'; + end + Screen('DrawText',wpntO,sprintf('reward: %s',rewardTxt),10,10,0); + Screen('Flip',wpntO); + + % check if fixation point fixated + [dur,currentlyInArea] = getGazeDurationInArea(EThndl,tobiiStartT,[],winRectP(3:4),fixRect,true); + if dur>fixMinDur + break; + end + + if KbCheck; break; end + + % provide reward + rewarding = provideRewardHelper(rewardProvider,currentlyInArea,forceRewardButton); + end + + + % show on screen and log when it was shown in eye-tracker time. + % NB: by setting a deadline for the flip, we ensure that the previous + % screen (fixation point) stays visible for the indicated amount of + % time. See PsychToolbox demos for further elaboration on this way of + % timing your script. + Screen('DrawTexture',wpntP,rabbits(1).tex,[],rabbits(1).scrRect); + imgT = Screen('Flip',wpntP); + tobiiStartT = EThndl.sendMessage(sprintf('STIM ON: %s [%.0f %.0f %.0f %.0f]',rabbits(1).fInfo.name,rabbits(1).scrRect),imgT); + nextFlipT = imgT+imageTime-1/hz/2; + + % now update also operator screen, once timing critical bit is done + % if we still have enough time till next flipT, update operator display + while nextFlipT-GetSecs()>2/hz % arbitrarily decide two frames is enough headway + Screen('DrawTexture',wpntO,rabbits(1).tex); + Screen('FrameRect',wpntO,fixRectColor,rabbits(1).scrRect,4); + drawLiveData(wpntO,EThndl.buffer.peekN('gaze',nLiveDataPoint),dataWindowDur,eyeColors{:},4,winRectO(3:4)); + rewardTxt = 'off'; + if rewarding + rewardTxt = 'on'; + end + Screen('DrawText',wpntO,sprintf('reward: %s',rewardTxt),10,10,0); + Screen('Flip',wpntO); + + + + % provide reward + [~,currentlyInArea] = getGazeDurationInArea(EThndl,tobiiStartT,[],winRectP(3:4),rabbits(1).scrPoly,true); + rewarding = provideRewardHelper(rewardProvider,currentlyInArea,forceRewardButton); + end + + % record x seconds of data, then clear screen. Indicate stimulus + % removed, clean up + endT = Screen('Flip',wpntP,nextFlipT); + EThndl.sendMessage(sprintf('STIM OFF: %s',rabbits(1).fInfo.name),endT); + Screen('Close',rabbits(1).tex); + nextFlipT = endT+1; % less precise, about 1s give or take a frame, is fine + + % now update also operator screen, once timing critical bit is done + % if we still have enough time till next flipT, update operator display + while nextFlipT-GetSecs()>2/hz % arbitrarily decide two frames is enough headway + drawLiveData(wpntO,EThndl.buffer.peekN('gaze',nLiveDataPoint),dataWindowDur,eyeColors{:},4,winRectO(3:4)); + Screen('Flip',wpntO); + end + + % repeat the above but show a different image. lets also record some + % eye images, if supported on connected eye tracker + if EThndl.buffer.hasStream('eyeImage') + EThndl.buffer.start('eyeImage'); + end + % 1. fixation point + Screen('gluDisk',wpntP,fixClrs(1),winRectP(3)/2,winRectP(4)/2,round(winRectP(3)/100)); + startT = Screen('Flip',wpntP,nextFlipT); + tobiiStartT = EThndl.sendMessage('FIX ON',startT); + while true + Screen('gluDisk',wpntO,fixClrs(1),winRectO(3)/2,winRectO(4)/2,round(winRectO(3)/100)); + Screen('FrameRect',wpntO,fixRectColor,[fixRect(:,1) fixRect(:,3)],4); + drawLiveData(wpntO,EThndl.buffer.peekN('gaze',nLiveDataPoint),dataWindowDur,eyeColors{:},4,winRectO(3:4)); + rewardTxt = 'off'; + if rewarding + rewardTxt = 'on'; + end + Screen('DrawText',wpntO,sprintf('reward: %s',rewardTxt),10,10,0); + Screen('Flip',wpntO); + + if KbCheck; break; end + + % check if fixation point fixated + [dur,currentlyInArea] = getGazeDurationInArea(EThndl,tobiiStartT,[],winRectP(3:4),fixRect,true); + if dur>fixMinDur + break; + end + + % provide reward + rewarding = provideRewardHelper(rewardProvider,currentlyInArea,forceRewardButton); + end + % 2. image + Screen('DrawTexture',wpntP,rabbits(2).tex,[],rabbits(2).scrRect); + imgT = Screen('Flip',wpntP); + tobiiStartT = EThndl.sendMessage(sprintf('STIM ON: %s [%.0f %.0f %.0f %.0f]',rabbits(2).fInfo.name,rabbits(2).scrRect),imgT); + nextFlipT = imgT+imageTime-1/hz/2; + while nextFlipT-GetSecs()>2/hz % arbitrarily decide two frames is enough headway + Screen('DrawTexture',wpntO,rabbits(2).tex); + Screen('FrameRect',wpntO,fixRectColor,rabbits(2).scrRect,4); + drawLiveData(wpntO,EThndl.buffer.peekN('gaze',nLiveDataPoint),dataWindowDur,eyeColors{:},4,winRectO(3:4)); + rewardTxt = 'off'; + if rewarding + rewardTxt = 'on'; + end + Screen('DrawText',wpntO,sprintf('reward: %s',rewardTxt),10,10,0); + Screen('Flip',wpntO); + + % provide reward + [~,currentlyInArea] = getGazeDurationInArea(EThndl,tobiiStartT,[],winRectP(3:4),rabbits(2).scrPoly,true); + rewarding = provideRewardHelper(rewardProvider,currentlyInArea,forceRewardButton); + end + + % 3. end recording after x seconds of data again, clear screen. + endT = Screen('Flip',wpntP,nextFlipT); + EThndl.sendMessage(sprintf('STIM OFF: %s',rabbits(2).fInfo.name),endT); + Screen('Close',rabbits(2).tex); + Screen('Flip',wpntO); + + % stop recording + if EThndl.buffer.hasStream('eyeImage') + EThndl.buffer.stop('eyeImage'); + end + EThndl.buffer.stop('gaze'); + + % save data to mat file, adding info about the experiment + dat = EThndl.collectSessionData(); + dat.expt.resolution = winRectP(3:4); + dat.expt.stim = rabbits; + EThndl.saveData(dat, fullfile(cd,'t'), true); + % if you want to (also) save the data to Apache Parquet and json files + % that can easily be read in Python (Apache Parquet files are supported + % by Pandas), use: + % EThndl.saveDataToParquet(dat, fullfile(cd,'t'), true); + % All gaze data columns and messages can be dumped to tsv files using: + % EThndl.saveGazeDataToTSV(dat, fullfile(cd,'t'), true); + + % shut down + EThndl.deInit(); + + +catch me + sca + ListenChar(0); + rethrow(me) +end +sca diff --git a/eyetracker/tittaAdvImageStimulus.m b/eyetracker/tittaAdvImageStimulus.m new file mode 100644 index 0000000000000000000000000000000000000000..ba10d7501e63713e40fac08d84a5eecad8927296 --- /dev/null +++ b/eyetracker/tittaAdvImageStimulus.m @@ -0,0 +1,133 @@ +classdef tittaAdvImageStimulus < handle + properties (Access=private, Constant) + calStateEnum = struct('undefined',0, 'showing',1, 'blinking',2) + end + properties (SetAccess=private) + calState + pointStartT + blinkStartT + end + properties (Dependent, SetAccess=private) + pos % can't set position here, you set it through doDraw() with drawCmd 'new' + end + properties + blinkInterval = 0.3 + blinkCount = 2 + bgColor = 127 + size = [] + videoSize = [] + end + properties (Access=private) + qFloatColorRange + currentPoint + cumDurations + stimulus + screen + tex = 0 + end + + + methods + function me = tittaAdvImageStimulus(screen) + if exist('screen','var') && isa(screen,'screenManager') + me.screen = screen; + me.bgColor = round(screen.backgroundColour * 255); + end + me.setCleanState(); + end + + function setStimulus(me,in) + me.stimulus = in; + if in.isSetup + me.screen = in.sM; + me.videoSize = [me.stimulus.width me.stimulus.height]; + me.size = me.videoSize; + end + end + + function setCleanState(me) + me.calState = me.calStateEnum.undefined; + me.currentPoint = nan(1,3); + if ~isempty(me.stimulus) + try me.stimulus.reset(); end %#ok<*TRYNC> + end + end + + function pos = get.pos(me) + pos = me.currentPoint(2:3); + end + + function qAllowAcceptKey = doDraw(me,wpnt,drawCmd,currentPoint,pos,~,~) + % last two inputs, tick (monotonously increasing integer) and + % stage ("cal" or "val") are not used in this code + + % if called with drawCmd == 'fullCleanUp', this is a signal + % that calibration/validation is done, and cleanup can occur if + % wanted. If called with drawCmd == 'sequenceCleanUp' that + % means there should be a gap in the drawing sequence (e.g. no + % smooth animation between two positions). For this one we keep + % image playback state unless asked to fully clean up. + if ismember(drawCmd,{'fullCleanUp','sequenceCleanUp'}) + if strcmp(drawCmd,'fullCleanUp') + me.setCleanState(); + end + return; + end + + % make sure movieStimulus is setup + if ~me.stimulus.isSetup + setup(me.stimulus, me.screen); + me.videoSize = me.stimulus.mvRect; + me.size = me.videoSize; + end + + % now that we have a wpnt, interrogate window + if isempty(me.qFloatColorRange) && ~isempty(wpnt) + me.qFloatColorRange = Screen('ColorRange',wpnt)==1; + end + + % check point changed + curT = GetSecs; % instead of using time directly, you could use the 'tick' call sequence number input to this function to animate your display + if strcmp(drawCmd,'new') + me.currentPoint = [currentPoint pos]; + me.pointStartT = curT; + me.calState = me.calStateEnum.showing; + elseif strcmp(drawCmd,'redo') + % start blink, restart animation. + me.calState = me.calStateEnum.blinking; + me.blinkStartT = curT; + me.pointStartT = 0; + else % drawCmd == 'draw' + % regular draw: check state transition + if me.calState==me.calStateEnum.blinking && (curT-me.blinkStartT)>me.blinkInterval*me.blinkCount*2 + % blink finished + me.calState = me.calStateEnum.showing; + me.pointStartT = curT; + end + end + + % determine current point position + curPos = me.currentPoint(2:3); + + % determine if we're ready to accept the user pressing the + % accept calibration point button. User should not be able to + % press it if point is not yet at the final position + qAllowAcceptKey = me.calState~=me.calStateEnum.blinking; + + if ~isempty(wpnt) + me.stimulus.updateXY(curPos(1),curPos(2)); + if (me.calState~=me.calStateEnum.blinking || mod((curT-me.blinkStartT)/me.blinkInterval/2,1)>.5) + me.stimulus.draw(); + end + end + end + end + + methods (Access = private, Hidden) + function clr = getColorForWindow(me,clr) + if me.qFloatColorRange + clr = double(clr)/255; + end + end + end +end diff --git a/eyetracker/tittaAdvMovieStimulus.m b/eyetracker/tittaAdvMovieStimulus.m new file mode 100644 index 0000000000000000000000000000000000000000..637aa28cdbe52f9d8c195e4be16d641c6ed545b2 --- /dev/null +++ b/eyetracker/tittaAdvMovieStimulus.m @@ -0,0 +1,153 @@ +classdef tittaAdvMovieStimulus < handle + properties (Access=private, Constant) + calStateEnum = struct('undefined',0, 'showing',1, 'blinking',2) + end + properties (SetAccess=private) + calState + pointStartT + blinkStartT + end + properties (Dependent, SetAccess=private) + pos % can't set position here, you set it through doDraw() with drawCmd 'new' + + end + properties + blinkInterval = 0.3 + blinkCount = 2 + bgColor = 127 + videoSize = [] + doMask = true + end + properties (Access=private, Hidden = true) + currentPoint + qFloatColorRange + cumDurations + videoPlayer + tex = 0 + masktex = 0 + end + + methods + %============================================================= + function obj = tittaAdvMovieStimulus() + obj.setCleanState(); + end + + %============================================================= + function setVideoPlayer(obj,videoPlayer) + obj.videoPlayer = videoPlayer; + end + + %============================================================= + function setCleanState(obj) + obj.calState = obj.calStateEnum.undefined; + obj.currentPoint = nan(1,3); + if ~isempty(obj.videoPlayer) + obj.videoPlayer.cleanup(); + end + if obj.tex>0 + try Screen('Close', obj.tex); end %#ok<*TRYNC> + end + if obj.masktex > 0 && Screen(obj.masktex,'WindowKind') == -1 + try Screen('Close',obj.masktex); end %#ok<*TRYNC> + end + obj.masktex = 0; + end + + %============================================================= + function pos = get.pos(obj) + pos = obj.currentPoint(2:3); + end + + %============================================================= + function qAllowAcceptKey = doDraw(obj,wpnt,drawCmd,currentPoint,pos,~,~) + % last two inputs, tick (monotonously increasing integer) and + % stage ("cal" or "val") are not used in this code + + if obj.doMask && obj.masktex==0 + obj.masktex = CreateProceduralSmoothedDisc(wpnt,500, 500, [], 250, 60, true, 2); + end + + % if called with drawCmd == 'fullCleanUp', this is a signal + % that calibration/validation is done, and cleanup can occur if + % wanted. If called with drawCmd == 'sequenceCleanUp' that + % means there should be a gap in the drawing sequence (e.g. no + % smooth animation between two positions). For this one we keep + % image playback state unless asked to fully clean up. + if ismember(drawCmd,{'fullCleanUp','sequenceCleanUp'}) + if strcmp(drawCmd,'fullCleanUp') + obj.setCleanState(); + end + return; + end + + % now that we have a wpnt, interrogate window + if isempty(obj.qFloatColorRange) && ~isempty(wpnt) + obj.qFloatColorRange = Screen('ColorRange',wpnt)==1; + end + + % check point changed + curT = GetSecs; % instead of using time directly, you could use the 'tick' call sequence number input to this function to animate your display + if strcmp(drawCmd,'new') + obj.currentPoint = [currentPoint pos]; + obj.pointStartT = curT; + obj.calState = obj.calStateEnum.showing; + elseif strcmp(drawCmd,'redo') + % start blink, restart animation. + obj.calState = obj.calStateEnum.blinking; + obj.blinkStartT = curT; + obj.pointStartT = 0; + else % drawCmd == 'draw' + % regular draw: check state transition + if obj.calState==obj.calStateEnum.blinking && (curT-obj.blinkStartT)>obj.blinkInterval*obj.blinkCount*2 + % blink finished + obj.calState = obj.calStateEnum.showing; + obj.pointStartT = curT; + end + end + + % determine current point position + curPos = obj.currentPoint(2:3); + + % determine if we're ready to accept the user pressing the + % accept calibration point button. User should not be able to + % press it if point is not yet at the final position + qAllowAcceptKey = obj.calState~=obj.calStateEnum.blinking; + + % draw + newTex = obj.videoPlayer.getFrame(); + if newTex>0 + if obj.tex>0 + Screen('Close', obj.tex); + end + obj.tex = newTex; + end + + if ~isempty(wpnt) + Screen('FillRect',wpnt,obj.getColorForWindow(obj.bgColor)); % needed when multi-flipping participant and operator screen, doesn't hurt when not needed + if obj.tex>0 && (obj.calState~=obj.calStateEnum.blinking || mod((curT-obj.blinkStartT)/obj.blinkInterval/2,1)>.5) + if ~isempty(obj.videoSize) + ts = [0 0 obj.videoSize]; + else + ts = Screen('Rect',obj.tex); + end + rect = CenterRectOnPointd(ts,curPos(1),curPos(2)); + Screen('DrawTexture',wpnt,obj.tex,[],rect); + if obj.doMask + maskClr = obj.getColorForWindow(color2RGBA(obj.bgColor),true); + Screen('DrawTexture',wpnt,obj.masktex,[],rect,[], [], 1, maskClr'); + end + end + end + end + end + + methods (Access = private, Hidden) + %============================================================= + function clr = getColorForWindow(obj,clr,forceFloatRange) + if obj.qFloatColorRange || (nargin>2&&forceFloatRange) + clr = double(clr)/255; + end + end + end +end diff --git a/eyetracker/tittaAdvancedController.m b/eyetracker/tittaAdvancedController.m new file mode 100644 index 0000000000000000000000000000000000000000..72b772954ffb86e8ac7be96d133fe59e4347a48e --- /dev/null +++ b/eyetracker/tittaAdvancedController.m @@ -0,0 +1,1140 @@ +classdef tittaAdvancedController < handle + properties (Constant) + stateEnum = struct('cal_positioning', 0, 'cal_gazing',1, 'cal_calibrating',2, 'cal_done',3, ... + 'val_validating' ,12, 'val_done' ,13) + pointStateEnum = struct('nothing',0, 'showing',1, 'collecting',2, 'discarding',3, 'collected', 4) + end + properties (SetAccess=private) + % state + stage + + gazeOnScreen % true if we have gaze for both eyes and the average position is on screen + leftGaze + rightGaze + meanGaze + onScreenTimestamp % time of start of episode of gaze on screen + offScreenTimestamp % time of start of episode of gaze off screen + onVideoTimestamp % time of start of episode of gaze on video (for calibration) + latestTimestamp % latest gaze timestamp + + onScreenTimeThresh + videoSize + + calPoint + calPoints = [] % ID of calibration points to run by the controller, in provided order + calPoss = [] % corresponding positions + calPointsState = [] + + valPoint + valPoints = [] % ID of calibration points to run by the controller, in provided order + valPoss = [] % corresponding positions + valPointsState = [] + end + properties + % comms + EThndl + calDisplay % expected to be a VideoCalibrationDisplay instance + rewardProvider + forceRewardButton = 'j' % if provided, when key press on this button is detected, reward is forced on + skipTrainingButton = 'x' % if training, when key press on this button is detected and we're in calibration stage, skip forward to 'cal_calibrating' state (i.e. skip positioning and gazing training) + + gazeFetchDur = 100 % duration of gaze samples to peek on each iteration (ms, e.g., last 100 ms of gaze) + gazeAggregationMethod = 1 % 1: use mean of all samples during last gazeFetchDur ms, 2: use mean of last valid sample during last gazeFetchDur ms + minValidGazeFrac = .5 + scrRes = [] + + maxOffScreenTime = 40/60*1000 + onScreenTimeThreshCap = 400 % maximum time animal will be required to keep gaze onscreen for rewards + onScreenTimeThreshIncRate = 0.01 % chance to increase onscreen time threshold + + videoShrinkTime = 1000 % how long eyes on video before video shrinks + videoShrinkRate = 0.01 % chance to decrease video size + + videoSizes = [ 1600 1600; + 1200 1200; + 800 800; + 600 600; + 500 500; + 400 400; + 300 300; ] + calVideoSize = [200 200] + calShowVideoWhenDone = true + calShowVideoWhenDeactivated = true + calVideoSizeWhenNotActive = [600 600] + calNotActiveRewardDistFac = .5 % fraction of video width (so 0.5 means gaze anywhere on video, since distance is from center) + calNotActiveRewardTime = 500 % ms + + valVideoSize = [200 200] + valShowVideoWhenDone = true + valShowVideoWhenDeactivated = true + valVideoSizeNotActive = [600 600] + valNotActiveRewardDistFac = .5 % fraction of video width (so 0.5 means gaze anywhere on video, since distance is from center) + valNotActiveRewardTime = 500 % ms + + calOnTargetTime = 500 % ms + calOnTargetDistFac = 1/3 % max gaze distance to be considered close enough to a point to attempt calibration (factor of vertical size of screen) + calAfterFirstCollected = false % if true, a calibration compute_and_apply command will be given after the first calibration point is successfully collected, before continueing to collect the next calibration point + + valOnTargetDist = 150 % pixels + valOnTargetTime = 500 % ms + valRandomizeTargets = true + + reEntryStateCal = tittaAdvancedController.stateEnum.cal_calibrating % when reactivating controller, discard state up to beginning of this state + reEntryStateVal = tittaAdvancedController.stateEnum.val_validating % when reactivating controller, discard state up to beginning of this state + + videoRectColor = [255 255 0] % color in which to draw the rect indicating where the video is shown on the screen + showGazeToOperator = true % if true, aggregated gaze as used by the controller is drawn as a crosshair on the operator screen + logTypes = 1 % bitmask: if 0, no logging. bit 1: print basic messages about what its up to. bit 2: print each command received in receiveUpdate(), bit 3: print messages about rewards (many!) + logReceiver = 0 % if 0: matlab command line. if 1: Titta + end + properties (Access=private,Hidden=true) + rewardTimer = 500 % ms + lastRewardTime = 0 + nRewards = 0 + isActive = false + isNonActiveShowingVideo = false + isShowingPointManually = false + dispensingReward = false + dispensingForcedReward = false + controlState = tittaAdvancedController.stateEnum.cal_positioning; + shouldRewindState = false + shouldClearCal = false + clearCalNow = false + clearValNow = false + activationCount = struct('cal',0, 'val',0) + shouldUpdateStatusText + trackerFrequency % calling obj.EThndl.frequency is blocking when a calibration action is ongoing, so cache the value + + awaitingPointResult = 0 % 0: not awaiting anything; 1: awaiting point collect result; 2: awaiting point discard result; 3: awaiting compute and apply result; 4: calibration clearing result + lastUpdate = {} + + drawState = 0 % 0: don't issue draws from here; 1: new command should be given to drawer; 2: regular draw command should be given + drawExtraFrame = false % because command in tick() is only processed in Titta after fixation point is drawn, we need to draw one extra frame here to avoid flashing when starting calibration point collection + + backupPaceDuration = struct('cal',[],'val',[]) + end + + + methods + % =================================================================== + function obj = tittaAdvancedController(EThndl,calDisplay,scrRes,rewardProvider) + obj.setCleanState(); + obj.EThndl = EThndl; + assert(isa(calDisplay,"tittaAdvMovieStimulus")) + obj.calDisplay = calDisplay; + if nargin>2 && ~isempty(scrRes) + obj.scrRes = scrRes; + end + if nargin>3 && ~isempty(rewardProvider) + obj.rewardProvider = rewardProvider; + end + end + + % =================================================================== + function setCalPoints(obj, calPoints,calPoss) + assert(ismember(obj.controlState,[obj.stateEnum.cal_positioning obj.stateEnum.cal_gazing]),'cannot set calibration points when already calibrating or calibrated') + assert(length(unique(calPoints))==length(calPoints),'At least one calibration point ID is specified more than once. Specify each calibration point only once.') + obj.calPoints = calPoints; % ID of calibration points to run by the controller, in provided order + obj.calPoss = calPoss; % corresponding positions + obj.calPointsState = repmat(obj.pointStateEnum.nothing, 1, size(obj.calPoss,1)); + end + + % =================================================================== + function setValPoints(obj, valPoints,valPoss) + assert(obj.controlState < obj.stateEnum.val_validating,'cannot set validation points when already validating or validated') + assert(length(unique(valPoints))==length(valPoints),'At least one validation point ID is specified more than once. Specify each validation point only once.') + obj.valPoints = valPoints; % ID of calibration points to run by the controller, in provided order + obj.valPoss = valPoss; % corresponding positions + obj.valPointsState = repmat(obj.pointStateEnum.nothing, 1, size(obj.valPoss,1)); + end + + % =================================================================== + function commands = tick(obj) + commands = {}; + if (~isempty(obj.forceRewardButton) && ~isempty(obj.rewardProvider)) || ~isempty(obj.skipTrainingButton) + [~,~,keyCode] = KbCheck(); + if any(keyCode) + if ~isempty(obj.forceRewardButton) && any(ismember(KbName(keyCode),{obj.forceRewardButton})) + if bitget(obj.logTypes,3) + obj.log_to_cmd('calibrating (force reward)'); + end + obj.reward(true); + elseif ~isempty(obj.skipTrainingButton) && any(ismember(KbName(keyCode),{obj.skipTrainingButton})) + obj.controlState = obj.stateEnum.cal_calibrating; + obj.drawState = 1; + obj.calDisplay.videoSize = obj.calVideoSize; + obj.shouldUpdateStatusText = true; + if bitget(obj.logTypes,1) + obj.log_to_cmd('calibrating (skipped forward by key press)'); + end + end + elseif obj.dispensingForcedReward + obj.dispensingForcedReward = false; + obj.reward(false); + end + end + if ~isempty(obj.rewardProvider) + obj.rewardProvider.tick(); + end + if ~obj.isActive && ~obj.isNonActiveShowingVideo && ~obj.isShowingPointManually + return; + end + obj.updateGaze(); + offScreenTime = obj.latestTimestamp-obj.offScreenTimestamp; + if ~obj.isActive && (obj.isNonActiveShowingVideo || obj.isShowingPointManually) % check like this: this logic should only kick in when controller is not active + % check if should be giving reward: when gaze on/near video + obj.determineNonActiveReward(); + return + end + + % normal controller active mode + if strcmp(obj.stage,'cal') + if offScreenTime > obj.maxOffScreenTime + obj.reward(false); + end + if obj.clearCalNow + if obj.awaitingPointResult~=4 + commands = {{'cal','clear'}}; + obj.awaitingPointResult = 4; + if bitget(obj.logTypes,1) + obj.log_to_cmd('calibration state is not clean upon controller activation. Requesting to clear it first'); + end + elseif obj.awaitingPointResult==4 && ~isempty(obj.lastUpdate) && strcmp(obj.lastUpdate{1},'cal_cleared') + obj.awaitingPointResult = 0; + obj.clearCalNow = false; + obj.lastUpdate = {}; + if bitget(obj.logTypes,1) + obj.log_to_cmd('calibration data cleared, starting controller'); + end + end + else + switch obj.controlState + case obj.stateEnum.cal_positioning + if obj.shouldRewindState + obj.onScreenTimeThresh = 1; + obj.shouldRewindState = false; + obj.drawState = 1; + obj.shouldUpdateStatusText = true; + if bitget(obj.logTypes,1) + obj.log_to_cmd('rewinding state: reset looking threshold'); + end + elseif obj.onScreenTimeThresh < obj.onScreenTimeThreshCap + % training to position and look at screen + obj.trainLookScreen(); + else + obj.controlState = obj.stateEnum.cal_gazing; + obj.drawState = 1; + obj.shouldUpdateStatusText = true; + if bitget(obj.logTypes,1) + obj.log_to_cmd('training to look at video'); + end + end + case obj.stateEnum.cal_gazing + if obj.shouldRewindState + obj.videoSize = 1; + obj.drawState = 1; + obj.shouldUpdateStatusText = true; + if obj.reEntryStateCal + end + obj.awaitingPointResult = 2; + if bitget(obj.logTypes,1) + obj.log_to_cmd('clearing validation state to be sure its clean upon controller activation'); + end + elseif obj.awaitingPointResult==2 && ~isempty(obj.lastUpdate) && strcmp(obj.lastUpdate{1},'val_discard') + % check this is for the expected point + if all(obj.valPointsState==obj.pointStateEnum.nothing) + obj.awaitingPointResult = 0; + obj.clearValNow = false; + obj.shouldUpdateStatusText = true; + obj.lastUpdate = {}; + obj.drawState = 1; + if bitget(obj.logTypes,1) + obj.log_to_cmd('validation data cleared, starting controller'); + end + end + end + else + switch obj.controlState + case obj.stateEnum.val_validating + % validating + commands = obj.validate(); + case obj.stateEnum.val_done + % procedure is done: nothing to do + end + end + end + end + + % =================================================================== + function receiveUpdate(obj,~,currentPoint,posNorm,~,~,type,callResult) + % inputs: titta_instance, currentPoint, posNorm, posPix, stage, type, callResult + + % event communicated to the controller: + if bitget(obj.logTypes,2) + obj.log_to_cmd('received update of type: %s',type); + end + switch type + case {'cal_activate','val_activate'} + mode = type(1:3); + isCal = strcmpi(mode,'cal'); + obj.activationCount.(mode) = obj.activationCount.(mode)+1; + if isCal + if obj.activationCount.cal>1 && obj.controlState>=obj.reEntryStateCal + obj.shouldRewindState = true; + if obj.controlState>obj.reEntryStateCal + if obj.controlState > obj.stateEnum.cal_done + obj.controlState = obj.stateEnum.cal_done; + end + obj.controlState = obj.controlState-1; + end + elseif obj.shouldClearCal + obj.clearCalNow = true; + end + if obj.activationCount.cal==1 && obj.controlState>obj.stateEnum.cal_done + obj.controlState = obj.stateEnum.cal_positioning; + end + else + obj.clearValNow = true; % always issue a validation clear, in case there is any data + obj.controlState = obj.stateEnum.val_validating; + obj.calDisplay.videoSize = obj.valVideoSize; + end + obj.lastUpdate = {}; + obj.awaitingPointResult = 0; + obj.isActive = true; + obj.shouldUpdateStatusText = true; + obj.isNonActiveShowingVideo = false; + obj.onVideoTimestamp = nan; + % backup Titta pacing duration and set to 0, since the + % controller controls when data should be collected + obj.setTittaPacing(type(1:3),''); + if bitget(obj.logTypes,1) + if isCal + obj.log_to_cmd('controller activated for calibration. Activation #%d',obj.activationCount.(mode)); + else + obj.log_to_cmd('controller activated for validation. Activation #%d',obj.activationCount.(mode)); + end + end + case {'cal_deactivate','val_deactivate'} + obj.isActive = false; + obj.shouldUpdateStatusText = true; + % reset Titta pacing duration + obj.setTittaPacing('',type(1:3)); + % setup non active display, if wanted + if (strcmp(type(1:3),'cal') && obj.calShowVideoWhenDeactivated) || (strcmp(type(1:3),'val') && obj.valShowVideoWhenDeactivated) + obj.setupNonActiveVideo(); + end + if bitget(obj.logTypes,1) + obj.log_to_cmd('controller deactivated for %s',ternary(startsWith(type,'cal'),'calibration','validation')); + end + % cal/val mode switches + case 'cal_enter' + obj.stage = 'cal'; + if obj.isActive + obj.setTittaPacing('cal','val'); + elseif obj.isNonActiveShowingVideo + obj.setupNonActiveVideo(); + end + if bitget(obj.logTypes,1) + obj.log_to_cmd('calibration mode entered'); + end + case 'val_enter' + obj.stage = 'val'; + if obj.isActive + obj.setTittaPacing('val','cal'); + elseif obj.isNonActiveShowingVideo + obj.setupNonActiveVideo(); + end + if bitget(obj.logTypes,1) + obj.log_to_cmd('validation mode entered'); + end + case 'cal_collect_started' + obj.calDisplay.videoSize = obj.calVideoSize; + obj.isShowingPointManually = ~obj.isActive; + obj.shouldUpdateStatusText = obj.shouldUpdateStatusText || obj.isShowingPointManually; + case 'val_collect_started' + obj.calDisplay.videoSize = obj.valVideoSize; + obj.isShowingPointManually = ~obj.isActive; + obj.shouldUpdateStatusText = obj.shouldUpdateStatusText || obj.isShowingPointManually; + % calibration point collected + case 'cal_collect_done' + obj.lastUpdate = {type,currentPoint,posNorm,callResult}; + if bitget(obj.logTypes,1) + success = callResult.status==0; % TOBII_RESEARCH_STATUS_OK + obj.log_to_cmd('calibration point collect: %s',ternary(success,'success','failed')); + if success; obj.reward(true); end + end + % update point status + iPoint = find(obj.calPoints==currentPoint); + if ~isempty(iPoint) && all(posNorm==obj.calPoss(iPoint,:)) + obj.calPointsState(iPoint) = obj.pointStateEnum.collected; + end + obj.shouldClearCal = true; % mark that we need to clear calibration if controller is activated + obj.shouldUpdateStatusText = obj.shouldUpdateStatusText || obj.isShowingPointManually; + obj.isShowingPointManually = false; + if obj.isNonActiveShowingVideo + obj.setupNonActiveVideo(); + end + % validation point collected + case 'val_collect_done' + obj.reward(true); + obj.lastUpdate = {type,currentPoint,posNorm,callResult}; + if bitget(obj.logTypes,1) + obj.log_to_cmd('validation point collect: success'); + end + % update point status + iPoint = find(obj.valPoints==currentPoint); + if ~isempty(iPoint) && all(posNorm==obj.valPoss(iPoint,:)) + obj.valPointsState(iPoint) = obj.pointStateEnum.collected; + end + obj.shouldUpdateStatusText = obj.shouldUpdateStatusText || obj.isShowingPointManually; + obj.isShowingPointManually = false; + if obj.isNonActiveShowingVideo + obj.setupNonActiveVideo(); + end + % calibration point discarded + case 'cal_discard' + obj.lastUpdate = {type,currentPoint,posNorm,callResult}; + if bitget(obj.logTypes,2) + success = callResult.status==0; % TOBII_RESEARCH_STATUS_OK + obj.log_to_cmd('calibration point discard: %s',ternary(success,'success','failed')); + end + % update point status + iPoint = find(obj.calPoints==currentPoint); + if ~isempty(iPoint) && all(posNorm==obj.calPoss(iPoint,:)) + obj.calPointsState(iPoint) = obj.pointStateEnum.nothing; + end + % validation point discarded + case 'val_discard' + obj.lastUpdate = {type,currentPoint,posNorm,callResult}; + if bitget(obj.logTypes,2) + obj.log_to_cmd('validation point discard: success'); + end + % update point status + iPoint = find(obj.valPoints==currentPoint); + if ~isempty(iPoint) && all(posNorm==obj.valPoss(iPoint,:)) + obj.valPointsState(iPoint) = obj.pointStateEnum.nothing; + end + % new calibration computed (may have failed) or loaded + case 'cal_compute_and_apply' + obj.lastUpdate = {type,callResult}; + if bitget(obj.logTypes,2) + success = callResult.status==0 && strcmpi(callResult.calibrationResult.status,'success'); + obj.log_to_cmd('calibration compute and apply result received: %s',ternary(success,'success','failed')); + end + % a calibration was loaded + case 'cal_load' + % mark that we need to clear calibration if controller is activated + obj.shouldClearCal = true; + % calibration was cleared: now at a blank slate + case 'cal_cleared' + obj.lastUpdate = {type}; + if bitget(obj.logTypes,2) + obj.log_to_cmd('calibration clear result received'); + end + obj.shouldClearCal = false; + % interface exited from calibration or validation screen + case {'cal_finished','val_finished'} + % we're done according to operator, clean up + obj.setTittaPacing('',type(1:3)); + obj.reward(false); + obj.setCleanState(); + end + end + + % =================================================================== + function txt = getStatusText(obj,force) + % return '!!clear_status' if you want to remove the status text + if nargin<2 + force = false; + end + txt = ''; + if ~obj.shouldUpdateStatusText && ~force + return + end + if ~obj.isActive + txt = 'Inactive'; + if obj.isShowingPointManually + txt = [txt ', showing point manually']; + end + else + switch obj.controlState + case obj.stateEnum.cal_positioning + txt = sprintf('Positioning %d/%d',obj.onScreenTimeThresh, obj.onScreenTimeThreshCap); + case obj.stateEnum.cal_gazing + % draw video rect + txt = sprintf('Gaze training\nvideo size %d/%d',obj.videoSize,size(obj.videoSizes,1)); + case obj.stateEnum.cal_calibrating + txt = sprintf('Calibrating %d/%d',obj.calPoint,length(obj.calPoints)); + case obj.stateEnum.cal_done + txt = 'Calibration done'; + + case obj.stateEnum.val_validating + txt = sprintf('Validating %d/%d',obj.valPoint,length(obj.valPoints)); + case obj.stateEnum.val_done + txt = 'Validation done'; + end + end + txt = sprintf('%s\nReward: %s',txt,ternary(obj.dispensingReward,'on','off')); + obj.shouldUpdateStatusText = false; + end + + % =================================================================== + function draw(obj,wpnts,tick,sFac,offset,onlyDrawParticipant) + % wpnts: two window pointers. first is for participant screen, + % second for operator + % sFac and offset are used to scale from participant screen to + % operator screen, in case they have different resolutions + if ~obj.isActive && ~obj.isNonActiveShowingVideo && ~obj.isShowingPointManually + return; + end + if obj.drawState>0 && ~obj.isShowingPointManually + drawCmd = 'draw'; + if obj.drawState==1 + drawCmd = 'new'; + if obj.controlState == obj.stateEnum.cal_positioning + obj.calDisplay.videoSize = obj.videoSizes(1,:); + end + end + pos = [nan nan]; + if ~obj.isActive && obj.isNonActiveShowingVideo + pos = obj.scrRes/2; + elseif ismember(obj.controlState, [obj.stateEnum.cal_positioning obj.stateEnum.cal_gazing]) + pos = obj.scrRes/2; + elseif obj.controlState == obj.stateEnum.cal_calibrating + calPos = obj.calPoss(obj.calPoint,:).*obj.scrRes(:).'; + pos = calPos; + elseif obj.controlState == obj.stateEnum.val_validating + valPos = obj.valPoss(obj.valPoint,:).*obj.scrRes(:).'; + pos = valPos; + end + % Don't call draw here if we've issued a command to collect + % calibration data for a point and haven't gotten a status + % update yet, then Titta is showing the point for us + if obj.awaitingPointResult~=1 || obj.drawExtraFrame + obj.calDisplay.doDraw(wpnts(1),drawCmd,nan,pos,tick,obj.stage); + end + if ~isnan(pos(1)) + obj.drawState = 2; + end + + if obj.awaitingPointResult~=1 && obj.drawExtraFrame + obj.drawExtraFrame = false; + end + end + + if onlyDrawParticipant + return + end + + % draw video rect for operator + if (~obj.isActive && (obj.isNonActiveShowingVideo || obj.isShowingPointManually)) || ... + ismember(obj.controlState, [obj.stateEnum.cal_gazing obj.stateEnum.cal_calibrating obj.stateEnum.val_validating]) + pos = obj.calDisplay.pos; + sz = obj.calDisplay.videoSize; + rect = CenterRectOnPointd([0 0 sz*sFac],pos(1)*sFac+offset(1),pos(2)*sFac+offset(2)); + Screen('FrameRect',wpnts(end),obj.videoRectColor,rect,4); + end + + % draw gaze if wanted + if obj.showGazeToOperator + sz = [1/40 1/120]*obj.scrRes(2); + for p=1:3 + switch p + case 1 + pos = obj.leftGaze; + clr = [255 0 0]; + case 2 + pos = obj.rightGaze; + clr = [0 0 255]; + case 3 + pos = obj.meanGaze; + clr = 0; + end + rectH = CenterRectOnPointd([0 0 sz ], pos(1)*sFac+offset(1), pos(2)*sFac+offset(2)); + rectV = CenterRectOnPointd([0 0 fliplr(sz)], pos(1)*sFac+offset(1), pos(2)*sFac+offset(2)); + Screen('FillRect',wpnts(end), clr, rectH); + Screen('FillRect',wpnts(end), clr, rectV); + end + end + end + end + + methods (Static) + % =================================================================== + function canDo = canControl(type) + switch type + case 'calibration' + canDo = true; + case 'validation' + canDo = true; + otherwise + error('NonHumanPrimateCalController: controller capability "%s" not understood',type) + end + end + end + + methods (Access = private, Hidden) + % =================================================================== + function setCleanState(obj) + if bitget(obj.logTypes,1) + obj.log_to_cmd('cleanup state, total rewards: %i',obj.nRewards); + end + obj.isActive = false; + obj.isNonActiveShowingVideo = false; + obj.isShowingPointManually = false; + obj.dispensingReward = false; + obj.dispensingForcedReward = false; + obj.controlState = obj.stateEnum.cal_positioning; + obj.shouldRewindState = false; + obj.shouldClearCal = false; + obj.clearCalNow = false; + obj.clearValNow = false; + obj.activationCount.cal = 0; + obj.activationCount.val = 0; + obj.shouldUpdateStatusText = true; + + obj.stage = ''; + obj.gazeOnScreen = false; + obj.leftGaze = [nan nan].'; + obj.rightGaze = [nan nan].'; + obj.meanGaze = [nan nan].'; + obj.onScreenTimestamp = nan; + obj.offScreenTimestamp = nan; + obj.onVideoTimestamp = nan; + obj.latestTimestamp = nan; + obj.lastRewardTime = nan; + obj.nRewards = 0; + + obj.onScreenTimeThresh = 1; + obj.videoSize = 1; + + obj.calPoint = 1; + obj.calPoints = []; + obj.calPoss = []; + obj.calPointsState = []; + + obj.valPoint = 1; + obj.valPoints = []; + obj.valPoss = []; + obj.valPointsState = []; + + obj.awaitingPointResult = 0; + + obj.drawState = 1; + obj.drawExtraFrame = false; + obj.backupPaceDuration = struct('cal',[],'val',[]); + end + + % =================================================================== + function updateGaze(obj) + if isempty(obj.trackerFrequency) + obj.trackerFrequency = obj.EThndl.frequency; + end + gaze = obj.EThndl.buffer.peekN('gaze',round(obj.gazeFetchDur/1000*obj.trackerFrequency)); + if isempty(gaze) + obj.meanGaze = nan; + return + end + + obj.latestTimestamp = double(gaze.systemTimeStamp(end))/1000; % us -> ms + fValid = mean([gaze.left.gazePoint.valid; gaze.right.gazePoint.valid],2); + if any(fValid>obj.minValidGazeFrac) + switch obj.gazeAggregationMethod + case 1 + % take mean of valid samples + obj.leftGaze = mean(gaze. left.gazePoint.onDisplayArea(:,gaze. left.gazePoint.valid),2,'omitnan').*obj.scrRes(:); + obj.rightGaze= mean(gaze.right.gazePoint.onDisplayArea(:,gaze.right.gazePoint.valid),2,'omitnan').*obj.scrRes(:); + case 2 + % use last valid sample + qValid = all([gaze.left.gazePoint.valid; gaze.right.gazePoint.valid],1); + iSamp = find(qValid,1,'last'); + obj.leftGaze = gaze. left.gazePoint.onDisplayArea(:,iSamp).*obj.scrRes(:); + obj.rightGaze= gaze.right.gazePoint.onDisplayArea(:,iSamp).*obj.scrRes(:); + end + obj.meanGaze = mean([obj.leftGaze obj.rightGaze],2); + + obj.gazeOnScreen = obj.meanGaze(1) > 0 && obj.meanGaze(1) 0 && obj.meanGaze(2) ms + end + end + else + obj.gazeOnScreen = false; + obj.leftGaze = [nan nan].'; + obj.rightGaze= [nan nan].'; + obj.meanGaze = [nan nan].'; + obj.onScreenTimestamp = nan; + if isnan(obj.offScreenTimestamp) + obj.offScreenTimestamp = double(gaze.systemTimeStamp(1))/1000; % us -> ms + end + end + end + + % =================================================================== + function reward(obj,on) + if ~exist('on','var'); on = false; end + if isempty(obj.lastRewardTime) || isnan(obj.lastRewardTime); obj.lastRewardTime = GetSecs; end + if isempty(obj.rewardProvider); return; end + nextTime = obj.lastRewardTime + 0.5; + thisTime = GetSecs; + if on == true + if thisTime > nextTime + obj.lastRewardTime = thisTime; + obj.nRewards = obj.nRewards + 1; + if bitget(obj.logTypes,1) + obj.log_to_cmd('reward() REWARD N:%i @ %.10g > %.10g\n', obj.nRewards, thisTime, nextTime); + end + obj.rewardProvider.giveReward(); + else + + end + else + + end + end + + % =================================================================== + function trainLookScreen(obj) + onScreenTime = obj.latestTimestamp-obj.onScreenTimestamp; + % looking long enough on the screen, provide reward + if onScreenTime > obj.onScreenTimeThresh + obj.reward(true); + end + % if looking much longer than current looking threshold, + % possibly increase threshold + if onScreenTime > obj.onScreenTimeThresh*2 + if rand()<=obj.onScreenTimeThreshIncRate + obj.onScreenTimeThresh = min(obj.onScreenTimeThresh*2,obj.onScreenTimeThreshCap); % limit to onScreenTimeThreshCap + obj.shouldUpdateStatusText = true; + if bitget(obj.logTypes,1) + obj.log_to_cmd('on-screen looking time threshold increased to %d',obj.onScreenTimeThresh); + end + end + end + end + + % =================================================================== + function trainLookVideo(obj) + onScreenTime = obj.latestTimestamp-obj.onScreenTimestamp; + if onScreenTime > obj.onScreenTimeThresh + % check distance to center of video (which is always at + % center of screen) + dist = hypot(obj.meanGaze(1)-obj.scrRes(1)/2,obj.meanGaze(2)-obj.scrRes(2)/2); + % if looking close enough to video, provide reward and + % possibly decrease video size + if dist < obj.videoSizes(obj.videoSize,2)*2 + obj.reward(true); + if onScreenTime > obj.videoShrinkTime && rand()<=obj.videoShrinkRate + obj.videoSize = min(obj.videoSize+1,size(obj.videoSizes,1)); + obj.calDisplay.videoSize = obj.videoSizes(obj.videoSize,:); + obj.shouldUpdateStatusText = true; + if bitget(obj.logTypes,1) + obj.log_to_cmd('video size decreased to %dx%d',obj.videoSizes(obj.videoSize,:)); + end + end + else + obj.reward(false); + end + end + end + + % =================================================================== + function commands = calibrate(obj) + commands = {}; + calPos = obj.calPoss(obj.calPoint,:).*obj.scrRes(:).'; + dist = hypot(obj.meanGaze(1)-calPos(1),obj.meanGaze(2)-calPos(2)); + if obj.shouldRewindState + if obj.awaitingPointResult~=4 + % clear calibration + commands = {{'cal','clear'}}; + obj.calPoint = 1; + obj.drawState = 1; + obj.awaitingPointResult = 4; + obj.shouldUpdateStatusText = true; + if bitget(obj.logTypes,1) + obj.log_to_cmd('rewinding state: clearing the calibration'); + end + elseif obj.awaitingPointResult==4 && ~isempty(obj.lastUpdate) && strcmp(obj.lastUpdate{1},'cal_cleared') + obj.awaitingPointResult = 0; + if obj.reEntryStateCal0 + % we're waiting for the result of an action. Those are all + % blocking in the Python code, but not here. For identical + % behavior (and easier logic), we put all the response + % waiting logic here, short-circuiting the below logic that + % depends on where the subject looks + if isempty(obj.lastUpdate) + return; + end + if obj.awaitingPointResult==1 && strcmp(obj.lastUpdate{1},'cal_collect_done') + % check this is for the expected point + if obj.lastUpdate{2}==obj.calPoints(obj.calPoint) && all(obj.lastUpdate{3}==obj.calPoss(obj.calPoint,:)) + % check result + if obj.lastUpdate{4}.status==0 % TOBII_RESEARCH_STATUS_OK + % success, decide next action + if obj.calPoint attempt calibration + commands = {{'cal','compute_and_apply'}}; + obj.awaitingPointResult = 3; + obj.shouldUpdateStatusText = true; + if bitget(obj.logTypes,1) + if obj.calPoint==1 && obj.calAfterFirstCollected + obj.log_to_cmd('first calibration point successfully collected, requesting computing and applying calibration before continuing collection of other points'); + else + obj.log_to_cmd('all calibration points successfully collected, requesting computing and applying calibration'); + end + end + end + else + % failed collecting calibration point, discard + % (to be safe its really gone from state, + % overkill i think but doesn't hurt) + commands = {{'cal','discard_point', obj.calPoints(obj.calPoint), obj.calPoss(obj.calPoint,:)}}; + obj.awaitingPointResult = 2; + obj.drawState = 1; % Titta calibration logic tells drawer to clean up upon failed point. Reshow point here + if bitget(obj.logTypes,1) + obj.log_to_cmd('failed to collect calibration point %d, requesting to discard it', obj.calPoints(obj.calPoint)); + end + end + end + obj.lastUpdate = {}; + elseif obj.awaitingPointResult==2 && strcmp(obj.lastUpdate{1},'cal_discard') + % check this is for the expected point + if obj.lastUpdate{2}==obj.calPoints(obj.calPoint) && all(obj.lastUpdate{3}==obj.calPoss(obj.calPoint,:)) + if obj.lastUpdate{4}.status==0 % TOBII_RESEARCH_STATUS_OK + obj.awaitingPointResult = 0; + if bitget(obj.logTypes,1) + obj.log_to_cmd('successfully discarded calibration point %d', obj.calPoints(obj.calPoint)); + end + else + error('can''t discard point, something seriously wrong') + end + end + obj.lastUpdate = {}; + elseif obj.awaitingPointResult==3 && strcmp(obj.lastUpdate{1},'cal_compute_and_apply') + if obj.lastUpdate{2}.status==0 && strcmpi(obj.lastUpdate{2}.calibrationResult.status,'success') + % successful calibration + if obj.calPoint==1 && obj.calAfterFirstCollected + obj.calPoint = obj.calPoint+1; + obj.awaitingPointResult = 0; + obj.shouldUpdateStatusText = true; + obj.onVideoTimestamp = nan; + obj.drawState = 1; + if bitget(obj.logTypes,1) + obj.log_to_cmd('calibration successfully applied, continuing calibration. Continue with collection of point %d', obj.calPoints(obj.calPoint)); + end + else + obj.awaitingPointResult = 0; + obj.reward(false); + obj.controlState = obj.stateEnum.cal_done; + obj.shouldUpdateStatusText = true; + commands = {{'cal','disable_controller'}}; + obj.drawState = 0; + if obj.calShowVideoWhenDone + obj.setupNonActiveVideo(); + end + if bitget(obj.logTypes,1) + obj.log_to_cmd('calibration successfully applied, disabling controller'); + end + end + else + % failed, start over + for p=length(obj.calPoints):-1:1 % reverse so we can set cal state back to first point and await discard of that first point, will arrive last + commands = [commands {{'cal','discard_point', obj.calPoints(p), obj.calPoss(p,:)}}]; %#ok + end + obj.awaitingPointResult = 2; + obj.calPoint = 1; + obj.drawState = 1; + if bitget(obj.logTypes,1) + obj.log_to_cmd('calibration failed discarding all points and starting over'); + end + end + obj.lastUpdate = {}; + elseif ~isempty(obj.lastUpdate) + % unexpected (perhaps stale, e.g. from before auto was switched on) update, discard + if bitget(obj.logTypes,1) + obj.log_to_cmd('unexpected update from Titta during calibration: %s, discarding',obj.lastUpdate{1}); + end + obj.lastUpdate = {}; + end + elseif dist < obj.calOnTargetDistFac*obj.scrRes(2) + obj.reward(true); + if obj.onVideoTimestamp<0 || isnan(obj.onVideoTimestamp) + obj.onVideoTimestamp = obj.latestTimestamp; + end + onDur = obj.latestTimestamp-obj.onVideoTimestamp; + if onDur > obj.calOnTargetTime && obj.awaitingPointResult==0 + % request calibration point collection + commands = {{'cal','collect_point', obj.calPoints(obj.calPoint), obj.calPoss(obj.calPoint,:)}}; + obj.awaitingPointResult = 1; + obj.calPointsState(obj.calPoint) = obj.pointStateEnum.collecting; + obj.drawExtraFrame = true; + if bitget(obj.logTypes,1) + obj.log_to_cmd('request calibration of point %d @ (%.3f,%.3f)', obj.calPoints(obj.calPoint), obj.calPoss(obj.calPoint,:)); + end + end + else + if obj.onVideoTimestamp>0 || isnan(obj.onVideoTimestamp) + obj.onVideoTimestamp = -obj.latestTimestamp; + end + offDur = obj.latestTimestamp--obj.onVideoTimestamp; + if offDur > obj.maxOffScreenTime + obj.reward(false); + % request discarding data for this point if its being + % collected + if obj.calPointsState(obj.calPoint)==obj.pointStateEnum.collecting || obj.awaitingPointResult~=0 + commands = {{'cal','discard_point', obj.calPoints(obj.calPoint), obj.calPoss(obj.calPoint,:)}}; + obj.awaitingPointResult = 2; + obj.calPointsState(obj.calPoint) = obj.pointStateEnum.discarding; + if bitget(obj.logTypes,1) + obj.log_to_cmd('request discarding calibration point %d @ (%.3f,%.3f)',obj.calPoints(obj.calPoint), obj.calPoss(obj.calPoint,:)); + end + end + end + end + end + + % =================================================================== + function commands = validate(obj) + commands = {}; + if obj.awaitingPointResult>0 + % we're waiting for the result of an action. Check if there + % is a result and process. Unlike calibration, this does + % not short-circuit the logic below, as we may wish to + % abort collection of a validation point + if obj.awaitingPointResult==1 && ~isempty(obj.lastUpdate) && strcmp(obj.lastUpdate{1},'val_collect_done') + % check this is for the expected point + if obj.lastUpdate{2}==obj.valPoints(obj.valPoint) && all(obj.lastUpdate{3}==obj.valPoss(obj.valPoint,:)) + % validation points always succeed, decide next + % action + if obj.valPoint obj.valOnTargetTime && obj.awaitingPointResult==0 + obj.reward(true) + % request validation point collection + commands = {{'val','collect_point', obj.valPoints(obj.valPoint), obj.valPoss(obj.valPoint,:)}}; + obj.awaitingPointResult = 1; + obj.valPointsState(obj.valPoint) = obj.pointStateEnum.collecting; + obj.drawExtraFrame = true; + if bitget(obj.logTypes,1) + obj.log_to_cmd('request collection of validation data for point %d @ (%.3f,%.3f)', obj.valPoints(obj.valPoint), obj.valPoss(obj.valPoint,:)); + end + end + else + obj.reward(false) + % request discarding data for this point if its being + % collected + if obj.valPointsState(obj.valPoint)==obj.pointStateEnum.collecting || obj.awaitingPointResult~=0 + commands = {{'val','discard_point', obj.valPoints(obj.valPoint), obj.valPoss(obj.valPoint,:)}}; + obj.awaitingPointResult = 2; + obj.valPointsState(obj.valPoint) = obj.pointStateEnum.discarding; + if bitget(obj.logTypes,1) + obj.log_to_cmd('request discarding validation point %d @ (%.3f,%.3f)',obj.valPoints(obj.valPoint), obj.valPoss(obj.valPoint,:)); + end + end + end + end + + % =================================================================== + function setTittaPacing(obj,set,reset) + settings = obj.EThndl.getOptions(); + if ~isempty(set) + obj.backupPaceDuration.(set) = settings.advcal.(set).paceDuration; + settings.advcal.(set).paceDuration = 0; + if bitget(obj.logTypes,1) + obj.log_to_cmd('setting Titta pacing duration for %s to 0',ternary(strcmpi(set,'cal'),'calibration','validation')); + end + end + if ~isempty(reset) && ~isempty(obj.backupPaceDuration.(reset)) + settings.advcal.(reset).paceDuration = obj.backupPaceDuration.(reset); + obj.backupPaceDuration.(reset) = []; + if bitget(obj.logTypes,1) + obj.log_to_cmd('resetting Titta pacing duration for %s',ternary(strcmpi(reset,'cal'),'calibration','validation')); + end + end + obj.EThndl.setOptions(settings); + end + + % =================================================================== + function setupNonActiveVideo(obj) + if strcmp(obj.stage,'cal') + obj.calDisplay.videoSize = obj.calVideoSizeWhenNotActive; + else + obj.calDisplay.videoSize = obj.valVideoSizeNotActive; + end + obj.drawState = 1; + obj.isNonActiveShowingVideo = true; + obj.onVideoTimestamp = nan; + end + + % =================================================================== + function determineNonActiveReward(obj) + % for during manual calibration points and when showing video + % after a calibration or validation + vidPos = obj.calDisplay.pos; + distL = hypot(obj. leftGaze(1)-vidPos(1), obj. leftGaze(2)-vidPos(2)); + distR = hypot(obj.rightGaze(1)-vidPos(1), obj.rightGaze(2)-vidPos(2)); + distM = hypot(obj. meanGaze(1)-vidPos(1), obj. meanGaze(2)-vidPos(2)); + minDist = min([distM, distL, distR]); + + if strcmp(obj.stage,'cal') + distFac = obj.calNotActiveRewardDistFac; + dur = obj.calNotActiveRewardTime; + else + distFac = obj.valNotActiveRewardDistFac; + dur = obj.valNotActiveRewardTime; + end + sz = obj.calDisplay.videoSize; + dist = sz(1)*distFac; + + if minDist < dist + if obj.onVideoTimestamp<0 || isnan(obj.onVideoTimestamp) + obj.onVideoTimestamp = obj.latestTimestamp; + end + onDur = obj.latestTimestamp-obj.onVideoTimestamp; + if onDur > dur + obj.reward(true); + end + else + obj.reward(false); + end + end + + % =================================================================== + function log_to_cmd(obj,msg,varargin) + message = sprintf(['%s: ' msg],mfilename('class'),varargin{:}); + switch obj.logReceiver + case 0 + fprintf('%s\n',message); + case 1 + obj.EThndl.sendMessage(message); + otherwise + error('logReceived %d unknown',obj.logReceiver); + end + end + end +end + +%% helpers +function out = ternary(cond, a, b) +out = subsref({b; a}, substruct('{}', {cond + 1})); +end \ No newline at end of file diff --git a/eyetracker/tittaCalCallback.m b/eyetracker/tittaCalCallback.m new file mode 100644 index 0000000000000000000000000000000000000000..f6f7453ca4c6948dd1343b57434328aeffd3b154 --- /dev/null +++ b/eyetracker/tittaCalCallback.m @@ -0,0 +1,25 @@ +function tittaCalCallback(titta_instance,currentPoint,posNorm,posPix,stage,calState) +global rM aM %our reward manager and audio manager object +if strcmpi(stage,'cal') + % this demo function is no-op for validation mode + if calState.status==0 + status = 'ok'; + if isa(rM,'arduinoManager') && rM.isOpen + giveReward(rM); + try beep(aM,2000,0.1,0.1); end %#ok<*TRYNC> + fprintf('--->>> Calibration reward!\n'); + end + else + status = sprintf('failed (%s)',calState.statusString); + fprintf('--->>> NO Calibration reward given...\n'); + end + titta_instance.sendMessage(sprintf('Calibration data collection status result for point %d, positioned at (%.2f,%.2f): %s',currentPoint,posNorm,status)); +elseif strcmpi(stage,'val') + if isa(rM,'arduinoManager') && rM.isOpen + giveReward(rM); + try beep(aM,2000,0.1,0.1); end + fprintf('--->>> Validation reward!\n'); + else + fprintf('--->>> NO Validation reward given...\n'); + end +end \ No newline at end of file diff --git a/eyetracker/tittaCalStimulus.m b/eyetracker/tittaCalStimulus.m new file mode 100644 index 0000000000000000000000000000000000000000..65f7dbec9fe28712aeac2581c6878d4932818481 --- /dev/null +++ b/eyetracker/tittaCalStimulus.m @@ -0,0 +1,214 @@ +% This class is part of Titta, a toolbox providing convenient access to +% eye tracking functionality using Tobii eye trackers +% +% Titta can be found at https://github.com/dcnieho/Titta. Check there for +% the latest version. +% When using Titta or this class, please cite the following paper: +% +% Niehorster, D.C., Andersson, R. & Nystrom, M., (2020). Titta: A toolbox +% for creating Psychtoolbox and Psychopy experiments with Tobii eye +% trackers. Behavior Research Methods. +% doi: https://doi.org/10.3758/s13428-020-01358-8 + +classdef tittaCalStimulus < handle + properties (Access=private, Constant) + calStateEnum = struct('undefined',0, 'moving',1, 'shrinking',2 ,'waiting',3 ,'blinking',4); + end + properties (Access=private) + screen + calState + currentPoint + lastPoint + moveStartT + shrinkStartT + oscillStartT + blinkStartT + moveDuration + moveVec + accel + scrSize + end + properties + drawFcn = 'drawPupilCoreMarker' + doShrink = true + shrinkTime = 0.5 + doMove = true + moveTime = 1 % for whole screen distance, duration will be proportionally shorter when dot moves less than whole screen distance + moveWithAcceleration= true + doOscillate = true + oscillatePeriod = 1.5 + blinkInterval = 0.3 + blinkCount = 2 + fixBackSizeBlink = 3.5 + fixBackSizeMax = 5 + fixBackSizeMaxOsc = 3.5 + fixBackSizeMin = 1.5 + fixFrontSize = 5 + fixBackColor = 0 + fixFrontColor = 255 + bgColor = 127 + end + properties (Access=private, Hidden = true) + qFloatColorRange = []; + ppd + end + + + methods + function me = tittaCalStimulus(screen) + if exist('screen','var') + me.screen = screen; + me.ppd = screen.ppd; + me.bgColor = floor(screen.backgroundColour(1:3) * 255); + end + me.setCleanState(); + end + + function setCleanState(me) + me.calState = me.calStateEnum.undefined; + me.currentPoint= nan(1,3); + me.lastPoint= nan(1,3); + end + + function qAllowAcceptKey = doDraw(me,wpnt,drawCmd,currentPoint,pos,~,~) + % last two inputs, tick (monotonously increasing integer) and + % stage ("cal" or "val") are not used in this code + + % if called with drawCmd == 'fullCleanUp', this is a signal + % that calibration/validation is done, and cleanup can occur if + % wanted. If called with drawCmd == 'sequenceCleanUp' that + % means there should be a gap in the drawing sequence (e.g. no + % smooth animation between two positions). For this one we can + % just clean up state in both cases. + if ismember(drawCmd,{'fullCleanUp','sequenceCleanUp'}) + me.setCleanState(); + return; + end + + % now that we have a wpnt, get some needed variables + if isempty(me.scrSize) + me.scrSize = Screen('Rect',wpnt); me.scrSize(1:2) = []; + end + if isempty(me.qFloatColorRange) + me.qFloatColorRange = Screen('ColorRange',wpnt)==1; + end + + % check point changed + curT = GetSecs; % instead of using time directly, you could use the 'tick' call sequence number input to this function to animate your display + if strcmp(drawCmd,'new') + if me.doMove && ~isnan(me.currentPoint(1)) + me.calState = me.calStateEnum.moving; + me.moveStartT = curT; + % dot should move at constant speed regardless of + % distance to cover, moveTime contains time to move + % over width of whole screen. Adjust time to proportion + % of screen width covered by current move + dist = hypot(me.currentPoint(2)-pos(1),me.currentPoint(3)-pos(2)); + me.moveDuration = me.moveTime*dist/me.scrSize(1); + if me.moveWithAcceleration + me.accel = dist/(me.moveDuration/2)^2; % solve x=.5*a*t^2 for a, use dist/2 for x + me.moveVec = (pos(1:2)-me.currentPoint(2:3))/dist; + end + elseif me.doShrink + me.calState = me.calStateEnum.shrinking; + me.shrinkStartT = curT; + else + me.calState = me.calStateEnum.waiting; + me.oscillStartT = curT; + end + + me.lastPoint = me.currentPoint; + me.currentPoint = [currentPoint pos]; + elseif strcmp(drawCmd,'redo') + % start blink, pause animation. + me.calState = me.calStateEnum.blinking; + me.blinkStartT = curT; + else % drawCmd == 'draw' + % regular draw: check state transition + if (me.calState==me.calStateEnum.moving && (curT-me.moveStartT)>me.moveDuration) || ... + (me.calState==me.calStateEnum.blinking && (curT-me.blinkStartT)>me.blinkInterval*me.blinkCount*2) + % move finished or blink finished + if me.doShrink + me.calState = me.calStateEnum.shrinking; + me.shrinkStartT = curT; + else + me.calState = me.calStateEnum.waiting; + me.oscillStartT = curT; + end + elseif me.calState==me.calStateEnum.shrinking && (curT-me.shrinkStartT)>me.shrinkTime + me.calState = me.calStateEnum.waiting; + me.oscillStartT = curT; + end + end + + % determine current point position + if me.calState==me.calStateEnum.moving + frac = (curT-me.moveStartT)/me.moveDuration; + if me.moveWithAcceleration + if frac<.5 + curPos = me.lastPoint(2:3) + me.moveVec*.5*me.accel*( curT-me.moveStartT)^2; + else + % implement deceleration by accelerating from the + % other side in backward time + curPos = me.currentPoint(2:3) - me.moveVec*.5*me.accel*(me.moveDuration-curT+me.moveStartT)^2; + end + else + curPos = me.lastPoint(2:3).*(1-frac) + me.currentPoint(2:3).*frac; + end + else + curPos = me.currentPoint(2:3); + end + + % determine current point size + if me.calState==me.calStateEnum.moving + sz = [me.fixBackSizeMax me.fixFrontSize]; + elseif me.calState==me.calStateEnum.shrinking + dSize = me.fixBackSizeMax-me.fixBackSizeMin; + frac = 1 - (curT-me.shrinkStartT)/me.shrinkTime; + sz = [me.fixBackSizeMin + frac.*dSize me.fixFrontSize]; + elseif me.calState==me.calStateEnum.blinking + sz = [me.fixBackSizeBlink me.fixFrontSize]; + else + if me.doOscillate + dSize = me.fixBackSizeMaxOsc-me.fixBackSizeMin; + phase = cos((curT-me.oscillStartT)/me.oscillatePeriod*2*pi); + if me.doShrink + frac = 1-(phase/2+.5); % start small + else + frac = phase/2+.5; % start big + end + sz = [me.fixBackSizeMin + frac.*dSize me.fixFrontSize]; + else + sz = [me.fixBackSizeMin me.fixFrontSize]; + end + end + + % determine if we're ready to accept the user pressing the + % accept calibration point button. User should not be able to + % press it if point is not yet at the final position + qAllowAcceptKey = ismember(me.calState,[me.calStateEnum.shrinking me.calStateEnum.waiting]); + + % draw + %Screen('FillRect',wpnt,me.getColorForWindow(me.bgColor)); % needed when multi-flipping participant and operator screen, doesn't hurt when not needed + if me.calState~=me.calStateEnum.blinking || mod((curT-me.blinkStartT)/me.blinkInterval/2,1)>.5 + me.drawAFixPoint(wpnt,curPos,sz); + end + end + end + + methods (Access = private, Hidden) + function drawAFixPoint(me,~,pos,sz) + ListenChar(0); + for p=1:size(pos,1) + xy = me.screen.toDegrees([pos(p,1),pos(p,2)],'xy'); + me.screen.(me.drawFcn)(sz(1),xy(1),xy(2)); + end + end + + function clr = getColorForWindow(me,clr) + if me.qFloatColorRange + clr = double(clr)/255; + end + end + end +end diff --git a/eyetracker/tittaRewardProvider.m b/eyetracker/tittaRewardProvider.m new file mode 100644 index 0000000000000000000000000000000000000000..e4ac6f0e4c8c29ac3d9c3328c02b30694eb99041 --- /dev/null +++ b/eyetracker/tittaRewardProvider.m @@ -0,0 +1,78 @@ +classdef tittaRewardProvider < handle + properties + rM = [] + aM = [] + dummyMode = false + verbose = false % if true, prints state updates to command line + end + properties (Hidden = true) + dutyCycle = inf % ms. If set to something other than inf, reward will be on for dutyCycle ms, then off for dutyCycle ms, etc for as long rewards are on. This requires frequently calling tick( + end + properties ( SetAccess = private) + on = false + dispensing = false + end + properties (Access = private, Hidden = true) + startT + end + + methods + % =================================================================== + function obj = tittaRewardProvider(dummyMode) + if nargin>0 && ~isempty(dummyMode) + obj.dummyMode = ~~dummyMode; + end + % we share rewardManager and audioManager as singletons + [obj.rM, obj.aM] = optickaCore.initialiseGlobals(true, true); + end + + % =================================================================== + function delete(obj) + % ensure we stop the reward before we destruct + obj.stop(); + end + + % =================================================================== + function giveReward(obj) + if ~obj.dummyMode + obj.rM.giveReward(); + if ~isempty(obj.aM);obj.aM.beep(3000,0.1,0.1);end + if obj.verbose + fprintf('tittaRewardProvider: reward\n'); + end + end + end + + % =================================================================== + function start(obj) + if ~obj.dummyMode + obj.giveReward(); + end + end + + % =================================================================== + function tick(obj) + if ~obj.dummyMode + obj.startT = GetSecs(); + end + end + + % =================================================================== + function stop(obj) + if ~obj.dummyMode + + end + end + end + + methods (Access = private, Hidden) + % =================================================================== + function dispense(obj,start) + if start + obj.dispensing = true; + else + obj.dispensing = false; + end + end + end +end \ No newline at end of file diff --git a/communication/tobiiManager.m b/eyetracker/tobiiManager.m similarity index 38% rename from communication/tobiiManager.m rename to eyetracker/tobiiManager.m index 38e400d1475d6a0bd5c9a4946d12048c72bc4a70..cc584c9491fa3dc7842b082fe726af1cc5b9b36d 100644 --- a/communication/tobiiManager.m +++ b/eyetracker/tobiiManager.m @@ -1,5 +1,5 @@ % ======================================================================== -classdef tobiiManager < optickaCore +classdef tobiiManager < eyetrackerCore & eyetrackerSmooth %> @class tobiiManager %> @brief Manages the Tobii eyetrackers %> @@ -25,184 +25,61 @@ classdef tobiiManager < optickaCore %> screen. fixInit allows you to define a minimum time with which the subject %> must initiate a saccade away from a position (which stops a subject cheating). %> -%> @todo refactor this and eyelinkManager to inherit from a common eyelinkManager %> @todo handle new eye-openness signals in new SDK https://developer.tobiipro.com/commonconcepts/eyeopenness.html %> -%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md +%> Copyright ©2014-2023 Ian Max Andolina — released: LGPL3, see LICENCE.md % ======================================================================== + properties (SetAccess = protected, GetAccess = public) + %> type of eyetracker + type = 'tobii' + end + properties - %> fixation window: - %> if X and Y have multiple rows, assume each one is a different fixation window. - %> if radius has a single value, assume circular window - %> if radius has 2 values assume width x height rectangle - %> initTime is the time the subject has to initiate fixation - %> time is the time the sbject must maintain fixation within the window - %> strict = false allows subject to exit and enter window without - %> failure, useful during training - fixation struct = struct('X',0,'Y',0,'initTime',1,'time',1,... - 'radius',1,'strict',true) - %> When using the test for eye position functions, - %> exclusion zones where no eye movement allowed: [-degX +degX -degY +degY] - %> Add rows to generate succesive exclusion zones. - exclusionZone = [] - %> we can optional set an initial window that the subject must stay - %> inside of before they saccade to the target window. This - %> restricts guessing and "cheating", by forcing a minimum delay - %> (default = 100ms) before initiating a saccade. Only used if X is not - %> empty. - fixInit struct = struct('X',[],'Y',[],'time',0.1,'radius',2) - %> add a manual offset to the eye position, similar to a drift correction - %> but handled by the eyelinkManager. - offset struct = struct('X',0,'Y',0) - %> start eyetracker in dummy mode? - isDummy logical = false - %> model of eyetracker: - %> 'Tobi Pro Spectrum' - 'IS4_Large_Peripheral' - 'Tobii TX300' - model char {mustBeMember(model,{'Tobii Pro Spectrum','Tobii TX300',... - 'Tobii 4C','IS4_Large_Peripheral', 'Tobii Pro Nano'})} = 'Tobii Pro Spectrum' - %> tracker update speed (Hz) - %> Spectrum Pro: [60, 120, 150, 300, 600 or 1200] - %> 4C: 90 - sampleRate double {mustBeMember(sampleRate,[60 90 120 150 300 600 1200])} = 300 - %> use human, monkey, great_ape, Default or other tracking mode - trackingMode char {mustBeMember(trackingMode,{'human','monkey', 'great_ape', 'Default', ... - 'Infant', 'Bright light'})} = 'human' - %> options for online smoothing of peeked data {'median','heuristic','savitsky-golay'} - smoothing struct = struct('nSamples',8,'method','median','window',3,... - 'eyes','both') - %> type of calibration stimulus - calibrationStimulus char {mustBeMember(calibrationStimulus,{'animated',... - 'movie','normal'})} = 'animated' - %> Titta settings structure - settings struct = [] - %> name of eyetracker data file - saveFile char = 'tobiiData.mat' - %> do we use manual calibration mode? - manualCalibration logical = false - %> custom calibration positions, e.g. [ .2 .5; .5 .5; .8 .5] - calPositions = [] - %> custom validation positions - valPositions = [] - %> does calibration pace automatically? - autoPace logical = true - %> pace duration - paceDuration double = 0.8 - % which eye is the tracker using? - eyeUsed char {mustBeMember(eyeUsed,{'both','left','right'})} = 'both' - %> which movie to use for calibration, empty uses default - calibrationMovie movieStimulus - %> use an operator screen for calibration etc. - useOperatorScreen = false; + %> setup and calibration values + calibration = struct(... + 'model', 'Tobii Pro Spectrum',... + 'mode', 'human',... + 'stimulus', 'animated',... + 'calPositions', [],... + 'valPositions', [],... + 'manual', false,... + 'manualMode', 'standard',... + 'autoPace', true,... + 'paceDuration', 0.8,... + 'eyeUsed', 'both',... + 'movie', [], ... + 'filePath', [],... + 'size', 1,... % size of calibration target in degrees + 'reloadCalibration',true,... + 'calFile','tobiiCalibration.mat') + %> optional eyetracker address + address = [] end properties (Hidden = true) + %> Settings structure from Titta + settings struct = [] %> Titta class object - tobii Titta - %> do we log messages to the command window? - verbose = false - %> stimulus positions to draw on screen - stimulusPositions = [] + tobii %> - sampletime = [] - %> operator screen used during calibration - operatorScreen screenManager - %> is operator screen being used? - secondScreen logical = false - %> should we close it after calibration - closeSecondScreen logical = false - %> size to draw eye position on screen - eyeSize double = 6 - % not used, compatibility with eyelinkManager - recordData - ignoreBlinks logical = false - end - - properties (SetAccess = private, GetAccess = public, Dependent = true) - % are we recording to matrix? - isRecording logical - % calculates the smoothing in ms - smoothingTime double + sampletime = [] + %> last calibration data + calib end - properties (SetAccess = private, Hidden = true) - - end - - properties (SetAccess = private, GetAccess = public) - %> Last gaze X position in degrees - x = [] - %> Last gaze Y position in degrees - y = [] - %> pupil size - pupil = [] - %> last isFixated true/false result - isFix logical = false - %> did the fixInit test fail or not? - isInitFail logical = false - %> are we in an exclusion zone? - isExclusion logical = false - % are we in a blink? - isBlink logical = false - %> total time searching and holding fixation - fixTotal = 0 - %> Initiate fixation length - fixInitLength = 0 - %how long have we been fixated? - fixLength = 0 - %> Initiate fixation time - fixInitStartTime = 0 - %the first timestamp fixation was true - fixStartTime = 0 - %> which fixation window matched the last fixation? - fixWindow = 0 - %> last time offset betweeen tracker and display computers - currentOffset = 0 - %> tracker time stamp - trackerTime = 0 + properties (SetAccess = protected, GetAccess = protected) + %> reward provider for Titta + rewardProvider = [] + %> advanced calibration controller + calController %> tracker time stamp - systemTime = 0 - %> current sample taken from tobii - currentSample struct - %> current event taken from tobii - currentEvent struct - %> All gaze X position in degrees reset using resetFixation - xAll = [] - %> Last gaze Y position in degrees reset using resetFixation - yAll = [] - %> all pupil size reset using resetFixation - pupilAll = [] - %> the PTB screen to work with, passed in during initialise - screen screenManager - % are we connected to Tobii? - isConnected logical = false - %> data streamed out from the Tobii - data struct = struct() - %> calibration data - calibration = [] - end - - properties (SetAccess = private, GetAccess = private) - %> cache this to save time in tight loops - isRecording_ = false + systemTime = 0 + calibData calStim - %> currentSample template - sampleTemplate struct = struct('raw',[],'time',NaN,'timeD',NaN,'gx',NaN,'gy',NaN,... - 'pa',NaN,'valid',false) - %> the PTB screen handle, normally set by screenManager but can force it to use another screen - win = [] - ppd_ double = 36 - % these are used to test strict fixation - fixN double = 0 - fixSelection = [] - %> event N - eventN = 1 - %> previous message sent to tobii - previousMessage char = '' + isCollectedData = false %> allowed properties passed to object upon construction - allowedProperties char = ['tobii|screen|isDummy|saveFile|settings|calPositions|'... - 'valPositions|model|trackingMode|fixation|sampleRate|smoothing|calibrationMovie|'... - 'verbose|isDummy|manualCalibration|exclusionZone|fixInit'] + allowedProperties = {'calibration', 'settings', 'address'} end %======================================================================= @@ -218,29 +95,26 @@ classdef tobiiManager < optickaCore %> @param varargin can be passed as a structure or name,arg pairs %> @return instance of the class. % =================================================================== - args = optickaCore.addDefaults(varargin,struct('name','Tobii manager')); - me=me@optickaCore(args); %we call the superclass constructor first + args = optickaCore.addDefaults(varargin,struct('name','Tobii',... + 'sampleRate',300,'useOperatorScreen',true,'eyeUsed','both')); + me=me@eyetrackerCore(args); %we call the superclass constructor first me.parseArgs(args, me.allowedProperties); try % is tobii working? assert(exist('Titta','class')==8,'TOBIIMANAGER:NO-TITTA','Cannot find Titta toolbox, please install instead of Tobii SDK; exiting...'); - initTracker(me); - assert(isa(me.tobii,'Titta'),'TOBIIMANAGER:INIT-ERROR','Cannot Initialise...') - catch ME - ME.getReport - fprintf('!!! Error initialising Tobii: %s\n\t going into Dummy mode...\n',ME.message); - me.tobii = []; - me.isDummy = true; end - if contains(me.model,{'Tobii 4C','IS4_Large_Peripheral'}) + if contains(me.calibration.model,{'Tobii 4C','IS4_Large_Peripheral'}) me.model = 'IS4_Large_Peripheral'; me.sampleRate = 90; - me.trackingMode = 'Default'; + me.calibration.mode = 'Default'; end - p = fileparts(me.saveFile); + [p,f,e] = fileparts(me.saveFile); + if isempty(e); e = '.mat'; end if isempty(p) - me.saveFile = [me.paths.savedData filesep me.saveFile]; + initialiseSaveFile(me); + me.saveFile = [me.paths.savedData filesep f e]; end + me.smoothing.sampleRate = me.sampleRate; end % =================================================================== @@ -249,326 +123,343 @@ classdef tobiiManager < optickaCore %> @param sM - screenManager object we will use %> @param sM2 - a second screenManager used during calibration % =================================================================== - function initialise(me,sM,sM2) + function success = initialise(me,sM,sM2) + success = false; + if me.isOff; me.isConnected = false; return; end + if ~exist('sM','var') || isempty(sM) if isempty(me.screen) || ~isa(me.screen,'screenManager') - me.screen = screenManager(); + me.screen = screenManager; end else - if ~isempty(me.screen) && isa(me.screen,'screenManager') && me.screen.isOpen && ~strcmpi(sM.uuid,me.screen.uuid) - %close(me.screen); - end - me.screen = sM; - end - if me.useOperatorScreen && ~exist('sM2','var') - sM2 = screenManager('windowed',[0 0 1000 1000],'pixelsPerCm',25,'backgroundColour',sM.backgroundColour,'specialFlags', kPsychGUIWindow); + me.screen = sM; end - if ~exist('sM2','var') || ~isa(sM2,'screenManager') - me.secondScreen = false; + me.ppd_ = me.screen.ppd; + if me.screen.isOpen; me.win = me.screen.win; end + + if me.screen.screen > 0 + oscreen = me.screen.screen - 1; else - me.operatorScreen = sM2; - me.secondScreen = true; + oscreen = 0; + end + if exist('sM2','var') + me.operatorScreen = sM2; + elseif isempty(me.operatorScreen) + me.operatorScreen = screenManager('pixelsPerCm',20,... + 'disableSyncTests',true,'backgroundColour',me.screen.backgroundColour,... + 'screen', oscreen, 'specialFlags', kPsychGUIWindow); + [w,h] = Screen('WindowSize',me.operatorScreen.screen); + me.operatorScreen.windowed = [0 0 round(w/1.2) round(h/1.2)]; + end + me.secondScreen = true; + if ismac; me.operatorScreen.useRetina = true; end + + initialiseSaveFile(me); + [p,f,e] = fileparts(me.saveFile); + if isempty(e); e = '.mat'; end + if isempty(p) + me.saveFile = [me.paths.savedData filesep me.name '-' me.savePrefix '-' f e]; + else + me.saveFile = [p filesep me.name '-' me.savePrefix '-' f e]; end - if contains(me.model,{'Tobii 4C','IS4_Large_Peripheral'}) + + me.smoothing.sampleRate = me.sampleRate; + + if contains(me.calibration.model,{'Tobii 4C','IS4_Large_Peripheral'}) me.model = 'IS4_Large_Peripheral'; me.sampleRate = 90; - me.trackingMode = 'Default'; - end - if ~isa(me.tobii, 'Titta') || isempty(me.tobii); initTracker(me); end - assert(isa(me.tobii,'Titta'),'TOBIIMANAGER:INIT-ERROR','Cannot Initialise...') - - if me.isDummy - me.tobii = me.tobii.setDummyMode(); + me.calibration.mode = 'Default'; end - me.settings = Titta.getDefaults(me.model); - if ~contains(me.model,{'Tobii 4C','IS4_Large_Peripheral'}) + me.settings = Titta.getDefaults(me.calibration.model); + if ~contains(me.calibration.model,{'Tobii 4C','IS4_Large_Peripheral'}) me.settings.freq = me.sampleRate; - me.settings.trackingMode = me.trackingMode; - end - me.settings.calibrateEye = me.eyeUsed; + me.settings.trackingMode = me.calibration.mode; + end + me.settings.calibrateEye = me.calibration.eyeUsed; me.settings.cal.bgColor = floor(me.screen.backgroundColour*255); me.settings.UI.setup.bgColor = me.settings.cal.bgColor; - me.settings.UI.setup.showFixPointsToSubject = false; - me.settings.UI.setup.showHeadToSubject = true; - me.settings.UI.setup.showInstructionToSubject = true; - me.settings.UI.setup.eyeClr = 255; - if strcmpi(me.calibrationStimulus,'animated') - me.calStim = AnimatedCalibrationDisplay(); + me.settings.UI.val.bgColor = me.settings.cal.bgColor; + me.settings.advcal.bgColor = me.settings.cal.bgColor; + me.settings.UI.advcal.bgColor = me.settings.cal.bgColor; + me.settings.UI.setup.eyeClr = 255; + me.settings.UI.setup.instruct.font = me.sansFont; + me.settings.UI.button.setup.text.font = me.sansFont; + me.settings.UI.button.val.text.font = me.sansFont; + me.settings.UI.cal.errMsg.font = me.sansFont; + me.settings.UI.val.avg.text.font = me.monoFont; + me.settings.UI.val.hover.text.font = me.monoFont; + me.settings.UI.val.menu.text.font = me.monoFont; + + me.settings.UI.advcal.showHead = true; + me.settings.UI.advcal.headScale = .20; + me.settings.UI.advcal.headPos = [.6 .1]; + + if strcmpi(me.calibration.stimulus,'movie') || strcmpi(me.calibration.manualMode,'Smart') + calStim = tittaAdvMovieStimulus(); + calStim.bgColor = me.settings.cal.bgColor; + calStim.videoSize = [me.calibration.size*me.screen.ppd me.calibration.size*me.screen.ppd]; + if ~isempty(me.calibration.filePath); fp = me.calibration.filePath; else; fp = me.calibration.movie; end + vids = FileFromFolder(fp, [], 'mp4'); + vids = arrayfun(@(x) fullfile(x.folder,x.name), vids, 'uni', false); + vp = VideoPlayer(me.screen.win,vids); + vp.start(); + calStim.setVideoPlayer(vp); + me.settings.cal.drawFunction = @(a,b,c,d,e,f) calStim.doDraw(a,b,c,d,e,f); + if me.calibration.manual + me.settings.advcal.drawFunction = @calStim.doDraw; + end + me.calStim = calStim; + elseif strcmpi(me.calibration.stimulus,'image') + me.calStim = tittaAdvImageStimulus(me.screen); + me.calStim.bgColor = me.settings.cal.bgColor; + me.calStim.blinkCount = 3; + if ~isempty(me.calibration.filePath); fp = me.calibration.filePath; else; fp = me.calibration.movie; end + m = imageStimulus('size', me.calibration.size,'filePath', fp); + reset(m); setup(m, me.screen); + me.calStim.setStimulus(m); + me.settings.cal.drawFunction = @(a,b,c,d,e,f) me.calStim.doDraw(a,b,c,d,e,f); + if me.calibration.manual + me.settings.advcal.drawFunction = @(a,b,c,d,e,f) me.calStim.doDraw(a,b,c,d,e,f); + end + elseif strcmpi(me.calibration.stimulus,'pupilcore') + me.calStim = tittaCalStimulus(me.screen); + me.calStim.bgColor = me.settings.cal.bgColor; me.calStim.moveTime = 0.75; - me.calStim.oscillatePeriod = 1; + me.calStim.oscillatePeriod = 0.8; me.calStim.blinkCount = 4; - me.calStim.bgColor = me.settings.cal.bgColor; me.calStim.fixBackColor = 0; me.calStim.fixFrontColor = 255; me.settings.cal.drawFunction = @(a,b,c,d,e,f) me.calStim.doDraw(a,b,c,d,e,f); - if me.manualCalibration;me.settings.mancal.drawFunction = @(a,b,c,d,e,f) me.calStim.doDraw(a,b,c,d,e,f);end - elseif strcmpi(me.calibrationStimulus,'movie') - me.calStim = tittaCalMovieStimulus(); + if me.calibration.manual + me.settings.advcal.drawFunction = @(a,b,c,d,e,f) me.calStim.doDraw(a,b,c,d,e,f); + end + else + me.calStim = AnimatedCalibrationDisplay(); + me.calStim.bgColor = me.settings.cal.bgColor; + me.calStim.fixBackSizeMin = round(me.calibration.size * me.ppd_); + me.calStim.fixBackSizeMax = round((me.calibration.size*1.5) * me.ppd_); + me.calStim.fixBackSizeMaxOsc = me.calStim.fixBackSizeMax; + me.calStim.fixBackSizeBlink = me.calStim.fixBackSizeMax; me.calStim.moveTime = 0.75; me.calStim.oscillatePeriod = 1; - me.calStim.blinkCount = 3; - if isempty(me.calibrationMovie) - me.calibrationMovie = movieStimulus('size',4); - end - reset(me.calibrationMovie); - setup(me.calibrationMovie, me.screen); - me.calStim.initialise(me.calibrationMovie); + me.calStim.blinkCount = 4; + me.calStim.fixBackColor = 0; + me.calStim.fixFrontColor = 255; me.settings.cal.drawFunction = @(a,b,c,d,e,f) me.calStim.doDraw(a,b,c,d,e,f); - if me.manualCalibration;me.settings.mancal.drawFunction = @(a,b,c,d,e,f) me.calStim.doDraw(a,b,c,d,e,f);end + if me.calibration.manual + me.settings.advcal.drawFunction = @(a,b,c,d,e,f) me.calStim.doDraw(a,b,c,d,e,f); + end + end + if me.calibration.autoPace + me.settings.cal.autoPace = 2; + else + me.settings.cal.autoPace = 0; end - me.settings.cal.autoPace = me.autoPace; - me.settings.cal.paceDuration = me.paceDuration; - if me.autoPace + me.settings.cal.paceDuration = me.calibration.paceDuration; + if me.calibration.autoPace me.settings.cal.doRandomPointOrder = true; else me.settings.cal.doRandomPointOrder = false; end - if ~isempty(me.calPositions) - me.settings.cal.pointPos = me.calPositions; + if ~isempty(me.calibration.calPositions) + me.settings.cal.pointPos = me.calibration.calPositions; + else + me.calibration.calPositions = me.settings.cal.pointPos; end - if ~isempty(me.valPositions) - me.settings.val.pointPos = me.valPositions; + if ~isempty(me.calibration.valPositions) + me.settings.val.pointPos = me.calibration.valPositions; + else + me.calibration.valPositions = me.settings.val.pointPos; end - + if me.verbose; me.settings.debugMode=true; end me.settings.cal.pointNotifyFunction = @tittaCalCallback; me.settings.val.pointNotifyFunction = @tittaCalCallback; - if me.manualCalibration - me.settings.UI.mancal.bgColor = floor(me.screen.backgroundColour*255); - me.settings.mancal.bgColor = floor(me.screen.backgroundColour*255); - me.settings.mancal.cal.pointPos = me.calPositions; - me.settings.mancal.val.pointPos = me.valPositions; - me.settings.mancal.cal.paceDuration = me.paceDuration; - me.settings.mancal.val.paceDuration = me.paceDuration; - me.settings.UI.mancal.showHead = true; - me.settings.UI.mancal.headScale = 0.4; - me.settings.mancal.cal.pointNotifyFunction = @tittaCalCallback; - me.settings.mancal.val.pointNotifyFunction = @tittaCalCallback; + if me.calibration.manual + me.settings.advcal.cal.pointPos = me.calibration.calPositions; + me.settings.advcal.val.pointPos = me.calibration.valPositions; + me.settings.advcal.cal.pointNotifyFunction = @tittaCalCallback; + me.settings.advcal.val.pointNotifyFunction = @tittaCalCallback; + me.settings.UI.advcal.gazeHistoryDuration = 0.5; + end + if strcmpi(me.calibration.manualMode,'Smart') + me.rewardProvider = tittaRewardProvider(); + + + calCtrl = tittaAdvancedController([],me.calStim,[],me.rewardProvider); + me.settings.advcal.cal.pointNotifyFunction = @calCtrl.receiveUpdate; + me.settings.advcal.val.pointNotifyFunction = @calCtrl.receiveUpdate; + me.settings.advcal.cal.useExtendedNotify = true; + me.settings.advcal.val.useExtendedNotify = true; + cp = me.settings.cal.pointPos; + vp = me.settings.cal.pointPos; + switch size(cp,1) + case 1 + calCtrl.setCalPoints(1,cp); + case 2 + calCtrl.setCalPoints(2,cp); + case 3 + calCtrl.setCalPoints(1,cp(2,:)); + case 4 + calCtrl.setCalPoints(2:3,cp(2:3,:)); + calCtrl.calAfterFirstCollected = true; + case 5 + calCtrl.setCalPoints([1 3 5],cp([1 3 5],:)); + case 6 + calCtrl.setCalPoints(3:4,cp(3:4,:)); + calCtrl.calAfterFirstCollected = true; + otherwise + calCtrl.setCalPoints(1:size(cp,1),cp); + end + calCtrl.setValPoints(1:size(vp,1),vp); + calCtrl.forceRewardButton = 'j'; + calCtrl.skipTrainingButton = 'x'; + me.calController = calCtrl; + end + + if ~isa(me.tobii, 'Titta') || isempty(me.tobii); initTracker(me); end + assert(isa(me.tobii,'Titta'),'TOBIIMANAGER:INIT-ERROR','Cannot Initialise...') + if me.isDummy; me.tobii = me.tobii.setDummyMode(); end + + if strcmpi(me.calibration.manualMode,'Smart') + me.calController.EThndl = me.tobii; + end + if isempty(me.address) || me.isDummy + me.tobii.init(); + else + me.tobii.init(me.address); end - updateDefaults(me); - me.tobii.init(); - me.isConnected = true; + checkConnection(me); me.systemTime = me.tobii.getTimeAsSystemTime; - me.ppd_ = me.screen.ppd; + me.isConnected = true; + if me.screen.isOpen == true me.win = me.screen.win; end + me.ppd_ = me.screen.ppd; if ~me.isDummy - me.salutation('Initialise', ... - sprintf('Running on a %s (%s) @ %iHz mode:%s | Screen %i %i x %i @ %iHz', ... + me.version = sprintf('Running on a %s (%s) @ %iHz mode:%s [%s:%s]\nScreen %i %i x %i @ %iHz', ... me.tobii.systemInfo.model, ... me.tobii.systemInfo.deviceName,... me.tobii.systemInfo.frequency,... me.tobii.systemInfo.trackingMode,... + me.tobii.systemInfo.firmwareVersion,... + me.tobii.systemInfo.runtimeVersion,... me.screen.screen,... me.screen.winRect(3),... me.screen.winRect(4),... - me.screen.screenVals.fps),true); + me.screen.screenVals.fps); else - me.salutation('Initialise', 'Running in Dummy Mode', true); - end - end - - % =================================================================== - %> @brief close the tobii and cleanup - %> is enabled - %> - % =================================================================== - function close(me) - try - stopRecording(me); - out = me.tobii.deInit(); - me.isConnected = false; - me.isRecording_ = false; - resetFixation(me); - if me.secondScreen && ~isempty(me.operatorScreen) && isa(me.operatorScreen,'screenManager') - me.operatorScreen.close; - end - catch ME - me.salutation('Close Method','Couldn''t stop recording, forcing shutdown...',true) - me.tobii.deInit(); - me.isConnected = false; - me.isRecording_ = false; - resetFixation(me); - if me.secondScreen && ~isempty(me.operatorScreen) && isa(me.operatorScreen,'screenManager') - me.operatorScreen.close; - end - getReport(ME); - end - end - - % =================================================================== - %> @brief - %> - % =================================================================== - function updateDefaults(me) - if isa(me.tobii, 'Titta') - me.tobii.setOptions(me.settings); - end - end - - % =================================================================== - %> @brief reset all fixation/exclusion data - %> - % =================================================================== - function resetAll(me) - resetExclusionZones(me); - resetFixInit(me); - resetOffset(me); - resetFixation(me,true); - end - - % =================================================================== - %> @brief reset the fixation counters ready for a new trial - %> - % =================================================================== - function resetFixation(me,removeHistory) - if ~exist('removeHistory','var');removeHistory=true;end - me.fixStartTime = 0; - me.fixLength = 0; - me.fixInitStartTime = 0; - me.fixInitLength = 0; - me.fixTotal = 0; - me.fixWindow = 0; - me.fixN = 0; - me.fixSelection = 0; - if removeHistory - resetFixationHistory(me); - end - me.isFix = false; - me.isExclusion = false; - me.isInitFail = false; - if me.verbose - fprintf('-+-+-> tobiiManager:reset fixation: %i %i %i\n',me.fixLength,me.fixTotal,me.fixN); - end - end - - % =================================================================== - %> @brief reset the fixation counters ready for a new trial - %> - % =================================================================== - function resetExclusionZones(me) - me.exclusionZone = []; - end - - % =================================================================== - %> @brief reset the fixation counters ready for a new trial - %> - % =================================================================== - function resetFixationTime(me) - me.fixStartTime = 0; - me.fixLength = 0; - end - - % =================================================================== - %> @brief reset the fixation history: xAll yAll pupilAll - %> - % =================================================================== - function resetFixationHistory(me) - me.xAll = []; - me.yAll = []; - me.pupilAll = []; - end - - % =================================================================== - %> @brief reset the fixation offset to 0 - %> - % =================================================================== - function resetOffset(me) - me.offset.X = 0; - me.offset.Y = 0; - end - - % =================================================================== - %> @brief reset the fixation offset to 0 - %> - % =================================================================== - function resetFixInit(me) - me.fixInit.X = []; - me.fixInit.Y = []; - end - - % =================================================================== - %> @brief check the connection with the tobii - %> - % =================================================================== - function connected = checkConnection(me) - connected = false; - if isa(me.tobii,'Titta') && me.tobii.isInitialized - connected = true; + me.version = sprintf('Running in Dummy Mode\nScreen %i %i x %i @ %iHz',... + me.screen.screen,... + me.screen.winRect(3),... + me.screen.winRect(4),... + me.screen.screenVals.fps); end + me.salutation('tobiiManager.initialise()', me.version, true); + success = true; end % =================================================================== %> @brief sets up the calibration and validation %> % =================================================================== - function cal = trackerSetup(me,incal) + function cal = trackerSetup(me, incal) cal = []; if ~me.isConnected - warning('Eyetracker not connected, cannot calibrate!'); - return + warning('Eyetracker not connected [must initialise first], cannot calibrate!'); return end - if me.useOperatorScreen && ~me.closeSecondScreen; open(me.operatorScreen); end + + if ~me.screen.isOpen; open(me.screen); end + if ~me.operatorScreen.isOpen; open(me.operatorScreen); end + if me.isDummy - disp('--->>> Tobii Dummy Mode: calibration skipped') - return; + disp('--->>> Tobii Dummy Mode: calibration skipped');return; end - if ~me.screen.isOpen - open(me.screen); + + [p,f,~]=fileparts(me.calibration.calFile); + e = '.mat'; + if isempty(f); f = 'tobiiCalibration'; end + if isempty(p) || ~exist('p','dir'); p = me.paths.calibration; end + me.calibration.calFile = [p filesep f e]; + + if ~exist('incal','var'); incal = []; end + if me.calibration.reloadCalibration && exist(me.calibration.calFile,'file') + load(me.calibration.calFile); + if (isfield(cal,'attempt') && ~isempty(cal.attempt)) && (isfield(cal,'wasSkipped') && ~cal.wasSkipped) + incal = cal; cal = []; + end + elseif exist('incal','var') && isstruct(incal) && ~isempty(incal) + me.calib = incal; + else + incal = []; end + fprintf('\n===>>> CALIBRATING TOBII... <<<===\n'); - if ~exist('incal','var');incal=[];end wasRecording = me.isRecording; - if wasRecording; stopRecording(me); end - updateDefaults(me); % make sure we send any other settings changes + if wasRecording; stopRecording(me,true); end + %updateDefaults(me); % make sure we send any other settings changes + + oldr = RestrictKeysForKbCheck([]); ListenChar(-1); - if ~isempty(me.operatorScreen) && isa(me.operatorScreen,'screenManager') - if ~me.operatorScreen.isOpen - me.operatorScreen.open(); + %======================================================MANUAL + if me.calibration.manual + if strcmpi(me.calibration.manualMode,'standard') + ctrl = []; + else + ctrl = me.calController; + ctrl.scrRes = me.screen.winRect(3:4); end - if me.manualCalibration - if ~isempty(incal) && isstruct(incal) && isfield(incal,'type') && contains(incal.type,'manual') - me.calibration = me.tobii.calibrateManual([me.screen.win me.operatorScreen.win], incal); - else - me.calibration = me.tobii.calibrateManual([me.screen.win me.operatorScreen.win]); - end + if ~isempty(incal) && isstruct(incal)... + && (isfield(incal,'type') && contains(incal.type,'advanced'))... + && (isfield(cal,'wasSkipped') && ~cal.wasSkipped) else - if ~isempty(incal) && isstruct(incal) && isfield(incal,'type') && contains(incal.type,'standard') - me.calibration = me.tobii.calibrate([me.screen.win me.operatorScreen.win], [], incal); - else - me.calibration = me.tobii.calibrate([me.screen.win me.operatorScreen.win]); - end + incal = []; end + cal = me.tobii.calibrateAdvanced([me.screen.win me.operatorScreen.win], incal, ctrl); + %======================================================AUTO else - me.calibration = me.tobii.calibrate(me.screen.win,[],incal); %start calibration + if ~isempty(incal) && isstruct(incal)... + && (isfield(incal,'type') && contains(incal.type,'standard'))... + && (isfield(cal,'wasSkipped') && ~cal.wasSkipped) + cal = me.tobii.calibrate([me.screen.win me.operatorScreen.win], [], incal); + else + cal = me.tobii.calibrate([me.screen.win me.operatorScreen.win]); + end end ListenChar(0); - if strcmpi(me.calibrationStimulus,'movie') - me.calStim.movie.reset(); - %me.calStim.movie.setup(me.screen); + RestrictKeysForKbCheck(oldr); + + if ~isempty(cal) && isfield(cal,'wasSkipped') && ~cal.wasSkipped + cal.date = me.dateStamp; + assignin('base','cal',cal); %put our calibration ready to save manually + save(me.calibration.calFile,'cal'); + me.calib = cal; + end + + if strcmpi(me.calibration.stimulus,'movie') + try me.calStim.setCleanState(); end end - if ~isempty(me.calibration) && me.calibration.wasSkipped ~= 1 - cal = me.calibration; - if isfield(me.calibration,'selectedCal') - try - calMsg = me.tobii.getValidationQualityMessage(me.calibration); - fprintf('-+-+-> CAL RESULT = '); - disp(calMsg); + + if ~isempty(me.calib) && me.calib.wasSkipped ~= 1 && isfield(me.calib,'selectedCal') + try + calMsg = me.tobii.getValidationQualityMessage(me.calib); + fprintf('\n-+-+-> CAL RESULT = '); + disp(calMsg); + if isempty(me.validationData) + me.validationData{1} = calMsg; + else + me.validationData{end+1} = calMsg; end + me.calib.v = calMsg; end else -% disp('---!!! The calibration was unsuccesful or skipped !!!---') - end - if me.useOperatorScreen && me.closeSecondScreen && me.operatorScreen.isOpen - close(me.operatorScreen); - WaitSecs('YieldSecs',0.2); + disp('-+-+!!! The calibration was unsuccesful or skipped !!!+-+-') end resetAll(me); - if wasRecording; startRecording(me); end - me.isRecording_ = me.isRecording; + if wasRecording; startRecording(me,true); end end % =================================================================== @@ -581,8 +472,8 @@ classdef tobiiManager < optickaCore %> will just return. % =================================================================== function startRecording(me, override) - if ~exist('override','var') || isempty(override) || override~=true; return; end - if me.isConnected && ~me.isRecording + if ~exist('override','var') || isempty(override) || override==false; return; end + if me.isConnected success = me.tobii.buffer.start('gaze'); if success me.statusMessage('Starting to record gaze...'); @@ -608,7 +499,7 @@ classdef tobiiManager < optickaCore warning('Can''t START buffer() timeSync recording!!!') end end - me.isRecording_ = me.isRecording; + me.isRecording = me.tobii.buffer.isRecording('gaze'); end % =================================================================== @@ -622,7 +513,7 @@ classdef tobiiManager < optickaCore % =================================================================== function stopRecording(me, override) if ~exist('override','var') || isempty(override) || override~=true; return; end - if me.isConnected && me.isRecording + try if me.tobii.buffer.hasStream('eyeImage') && me.tobii.buffer.isRecording('eyeImage') success = me.tobii.buffer.stop('eyeImage'); if success @@ -661,508 +552,84 @@ classdef tobiiManager < optickaCore else warning('Can''t STOP buffer() GAZE recording!!!') end - end - me.isRecording_ = me.isRecording; - end - - % =================================================================== - %> @brief Custom drift offset command - %> - % =================================================================== - function success = driftOffset(me) - success = false; - escapeKey = KbName('ESCAPE'); - stopkey = KbName('Q'); - nextKey = KbName('SPACE'); - calibkey = KbName('C'); - driftkey = KbName('D'); - if me.isConnected || me.isDummy - x = me.toPixels(me.fixation.X,'x'); %#ok<*PROPLC> - y = me.toPixels(me.fixation.Y,'y'); - Screen('Flip',me.screen.win); - ifi = me.screen.screenVals.ifi; - breakLoop = false; i = 1; flash = true; - correct = false; - xs = []; - ys = []; - while ~breakLoop - getSample(me); - xs(i) = me.x; - ys(i) = me.y; - if mod(i,10) == 0 - flash = ~flash; - end - Screen('DrawText',me.screen.win,'Drift Correction...',10,10,[0.4 0.4 0.4]); - if flash - Screen('gluDisk',me.screen.win,[1 0 1 0.75],x,y,10); - Screen('gluDisk',me.screen.win,[1 1 1 1],x,y,4); - else - Screen('gluDisk',me.screen.win,[1 1 0 0.75],x,y,10); - Screen('gluDisk',me.screen.win,[0 0 0 1],x,y,4); - end - me.screen.drawCross(0.6,[0 0 0],x,y,0.1,false); - Screen('Flip',me.screen.win); - [~, ~, keyCode] = KbCheck(-1); - if keyCode(stopkey) || keyCode(escapeKey); breakLoop = true; break; end - if keyCode(nextKey); correct = true; break; end - if keyCode(calibkey); trackerSetup(me); break; end - if keyCode(driftkey); driftCorrection(me); break; end - i = i + 1; - end - if correct && length(xs) > 5 && length(ys) > 5 - success = true; - me.offset.X = median(xs) - me.fixation.X; - me.offset.Y = median(ys) - me.fixation.Y; - t = sprintf('Offset: X = %.2f Y = %.2f\n',me.offset.X,me.offset.Y); - me.salutation('Drift [SELF]Correct',t,true); - Screen('DrawText',me.screen.win,t,10,10,[0.4 0.4 0.4]); - Screen('Flip',me.screen.win); - else - me.offset.X = 0; - me.offset.Y = 0; - t = sprintf('Offset: X = %.2f Y = %.2f\n',me.offset.X,me.offset.Y); - me.salutation('REMOVE Drift [SELF]Offset',t,true); - Screen('DrawText',me.screen.win,'Reset Drift Offset...',10,10,[0.4 0.4 0.4]); - Screen('Flip',me.screen.win); - end - WaitSecs('YieldSecs',1); + + me.isRecording = me.tobii.buffer.isRecording('gaze'); end end - + % =================================================================== + function sample = getSample(me) + %> @fn getSample() %> @brief get a sample from the tracker, if dummymode=true then use %> the mouse as an eye signal %> % =================================================================== - function sample = getSample(me) - sample = me.sampleTemplate; + if me.isOff; return; end if me.isDummy %lets use a mouse to simulate the eye signal - if ~isempty(me.win) - [mx, my] = GetMouse(me.win); - else - [mx, my] = GetMouse([]); - end - sample.valid = true; - me.pupil = 5 + randn; - sample.gx = mx; - sample.gy = my; - sample.pa = me.pupil; - sample.time = GetSecs * 1e6; - me.x = me.toDegrees(sample.gx,'x'); - me.y = me.toDegrees(sample.gy,'y'); - me.xAll = [me.xAll me.x]; - me.yAll = [me.yAll me.y]; - me.pupilAll = [me.pupilAll me.pupil]; - %if me.verbose;fprintf('>>X: %.2f | Y: %.2f | P: %.2f\n',me.x,me.y,me.pupil);end - elseif me.isConnected && me.isRecording_ + sample = getMouseSample(me); + elseif me.isConnected && me.isRecording + sample = me.sampleTemplate; xy = []; td = me.tobii.buffer.peekN('gaze',me.smoothing.nSamples); if isempty(td);me.currentSample=sample;return;end sample.raw = td; - sample.time = double(td.systemTimeStamp(end)); %remember these are in microseconds - sample.timeD = double(td.deviceTimeStamp(end)); if any(td.left.gazePoint.valid) || any(td.right.gazePoint.valid) - switch me.smoothing.eyes - case 'left' - xy = td.left.gazePoint.onDisplayArea(:,td.left.gazePoint.valid); - case 'right' - xy = td.right.gazePoint.onDisplayArea(:,td.right.gazePoint.valid); - otherwise - if all(td.left.gazePoint.valid & td.right.gazePoint.valid) - v = td.left.gazePoint.valid & td.right.gazePoint.valid; - xy = [td.left.gazePoint.onDisplayArea(:,v);... - td.right.gazePoint.onDisplayArea(:,v)]; - else - xy = [td.left.gazePoint.onDisplayArea(:,td.left.gazePoint.valid),... - td.right.gazePoint.onDisplayArea(:,td.right.gazePoint.valid)]; - end - end - end - if ~isempty(xy) - sample.valid = true; - xy = doSmoothing(me,xy); - xy = toPixels(me, xy,'','relative'); - sample.gx = xy(1); - sample.gy = xy(2); - sample.pa = nanmean(td.left.pupil.diameter); - xy = me.toDegrees(xy); - me.x = xy(1); - me.y = xy(2); - me.pupil = sample.pa; - %if me.verbose;fprintf('>>X: %2.2f | Y: %2.2f | P: %.2f\n',me.x,me.y,me.pupil);end - else - sample.gx = NaN; - sample.gy = NaN; - sample.pa = NaN; - me.x = NaN; - me.y = NaN; - me.pupil = NaN; - end - me.xAll = [me.xAll me.x]; - me.yAll = [me.yAll me.y]; - me.pupilAll = [me.pupilAll me.pupil]; - else - if me.verbose;fprintf('-+-+-> tobiiManager.getSample(): are you sure you are recording?\n');end - end - me.currentSample = sample; - end - - % =================================================================== - %> @brief Method to update the fixation parameters - %> - % =================================================================== - function updateFixationValues(me,x,y,inittime,fixtime,radius,strict) - %tic - resetFixation(me,false) - if nargin > 1 && ~isempty(x) - if isinf(x) - me.fixation.X = me.screen.screenXOffset; - else - me.fixation.X = x; - end - end - if nargin > 2 && ~isempty(y) - if isinf(y) - me.fixation.Y = me.screen.screenYOffset; - else - me.fixation.Y = y; - end - end - if nargin > 3 && ~isempty(inittime) - if iscell(inittime) && length(inittime)==4 - me.fixation.initTime = inittime{1}; - me.fixation.time = inittime{2}; - me.fixation.radius = inittime{3}; - me.fixation.strict = inittime{4}; - elseif length(inittime) == 2 - me.fixation.initTime = randi(inittime.*1000)/1000; - elseif length(inittime)==1 - me.fixation.initTime = inittime; - end - end - if nargin > 4 && ~isempty(fixtime) - if length(fixtime) == 2 - me.fixation.time = randi(fixtime.*1000)/1000; - elseif length(fixtime) == 1 - me.fixation.time = fixtime; - end - end - if nargin > 5 && ~isempty(radius); me.fixation.radius = radius; end - if nargin > 6 && ~isempty(strict); me.fixation.strict = strict; end - if me.verbose - fprintf('-+-+-> tobiiManager:updateFixationValues: X=%g | Y=%g | IT=%s | FT=%s | R=%g\n', ... - me.fixation.X, me.fixation.Y, num2str(me.fixation.initTime), num2str(me.fixation.time), ... - me.fixation.radius); - end - end - - % =================================================================== - %> @brief Sinlge method to update the exclusion zones - %> - %> @param x x position in degrees - %> @param y y position in degrees - %> @param radius the radius of the exclusion zone - % =================================================================== - function updateExclusionZones(me,x,y,radius) - resetExclusionZones(me); - if exist('x','var') && exist('y','var') && ~isempty(x) && ~isempty(y) - if ~exist('radius','var'); radius = 5; end - for i = 1:length(x) - me.exclusionZone(i,:) = [x(i)-radius x(i)+radius y(i)-radius y(i)+radius]; - end - if me.verbose;fprintf('-+-+-> tobiiManager:updateExclusionZones');end - end - end - - % =================================================================== - %> @brief isFixated tests for fixation and updates the fixLength time - %> - %> @return fixated boolean if we are fixated - %> @return fixtime boolean if we're fixed for fixation time - %> @return searching boolean for if we are still searching for fixation - % =================================================================== - function [fixated, fixtime, searching, window, exclusion, fixinit] = isFixated(me) - fixated = false; fixtime = false; searching = true; window = []; exclusion = false; fixinit = false; window = 0; - - if isempty(me.currentSample) || isnan(me.currentSample.time);return;end - - if me.isExclusion || me.isInitFail - exclusion = me.isExclusion; fixinit = me.isInitFail; searching = false; - return; % we previously matched either rule, now cannot pass fixation until a reset. - end - - if me.fixInitStartTime == 0 - me.fixInitStartTime = me.currentSample.time; - end - % ---- test for exclusion zones first - if ~isempty(me.exclusionZone) - for i = 1:size(me.exclusionZone,1) - if (me.x >= me.exclusionZone(i,1) && me.x <= me.exclusionZone(i,2)) && ... - (me.y >= me.exclusionZone(i,3) && me.y <= me.exclusionZone(i,4)) - searching = false; exclusion = true; me.isExclusion = true; me.isFix = false; - return; - end - end - end - % ---- test for fix initiation start window - if ~isempty(me.fixInit.X) - if (me.currentSample.time - me.fixInitStartTime) < (me.fixInit.time * 1e6) - r = sqrt((me.x - me.fixInit.X).^2 + (me.y - me.fixInit.Y).^2); - window = find(r < me.fixInit.radius); - if ~any(window) - searching = false; exclusion = true; fixinit = true; - me.isInitFail = fixinit; me.isFix = false; - if me.verbose;fprintf('-+-+-> tobiiManager: Eye left fix init window @ %.3f secs!\n',(me.currentSample.time - me.fixInitStartTime));end - return; - end - end - end - % now test if we are still searching or in fixation window, if - % radius is single value, assume circular, otherwise assume - % rectangular - window = 0; - if length(me.fixation.radius) == 1 % circular test - r = sqrt((me.x - me.fixation.X).^2 + (me.y - me.fixation.Y).^2); %fprintf('x: %g-%g y: %g-%g r: %g-%g\n',me.x, me.fixation.X, me.y, me.fixation.Y,r,me.fixation.radius); - window = find(r < me.fixation.radius); - elseif length(me.fixation.radius) == 2 % x y rectangular window test - for i = 1:length(me.fixation.X) - if (me.x >= (me.fixation.X - me.fixation.radius(1))) && (me.x <= (me.fixation.X + me.fixation.radius(1))) ... - && (me.y >= (me.fixation.Y - me.fixation.radius(2))) && (me.y <= (me.fixation.Y + me.fixation.radius(2))) - window = i;break; - end - end - end - me.fixWindow = window; - me.fixTotal = (me.currentSample.time - me.fixInitStartTime) / 1e6; - if any(window) % inside fixation window - if me.fixN == 0 - me.fixN = 1; - me.fixSelection = window(1); - end - if me.fixSelection == window(1) - if me.fixStartTime == 0 - me.fixStartTime = me.currentSample.time; - end - fixated = true; searching = false; - me.fixLength = (me.currentSample.time - me.fixStartTime) / 1e6; - if me.fixLength >= me.fixation.time - fixtime = true; - else - fixtime = false; - end - else - fixated = false; fixtime = false; searching = false; - end - me.isFix = fixated; - else %not inside the fixation window - if me.fixN == 1 - me.fixN = -100; - end - me.fixInitLength = (me.currentSample.time - me.fixInitStartTime) / 1e6; - if me.fixInitLength <= me.fixation.initTime - searching = true; - else - searching = false; - end - me.isFix = false; me.fixLength = 0; me.fixStartTime = 0; - end - end - - % =================================================================== - %> @brief testExclusion checks if eye is in exclusion zones - %> - % =================================================================== - function out = testExclusion(me) - out = false; - if (me.isConnected || me.isDummy) && ~isempty(me.currentSample) && ~isempty(me.exclusionZone) - for i = 1:size(me.exclusionZone,1) - if (me.x >= me.exclusionZone(i,1) && me.x <= me.exclusionZone(i,2)) && ... - (me.y >= me.exclusionZone(i,3) && me.y <= me.exclusionZone(i,4)) - out = true; - if me.verbose;fprintf('-+-+-> Tobii:EXCLUSION ZONE %i ENTERED!\n',i);end - return - end - end - end - end - - % =================================================================== - %> @brief testFixation returns input yes or no strings based on - %> fixation state, useful for using via stateMachine - %> - % =================================================================== - function out = testWithinFixationWindow(me, yesString, noString) - if isFixated(me) - out = yesString; - else - out = noString; - end - end - - % =================================================================== - %> @brief Checks if we've maintained fixation for correct time, if - %> true return yesString, if not return noString. This allows an - %> external code to quickly select a string based on this. - %> - % =================================================================== - function out = testFixationTime(me, yesString, noString) - [fix,fixtime] = isFixated(me); - if fix && fixtime - out = yesString; %me.salutation(sprintf('Fixation time: %g',me.fixLength),'TESTFIXTIME'); - else - out = noString; - end - end - - % =================================================================== - %> @brief Checks if we're looking for fixation a set time. Input is - %> 2 strings, either one is returned depending on success or - %> failure, 'searching' may also be returned meaning the fixation - %> window hasn't been entered yet, and 'fixing' means the fixation - %> time is not yet met... - %> - %> @param yesString if this function succeeds return this string - %> @param noString if this function fails return this string - %> @return out the output string which is 'searching' if fixation is - %> still being initiated, 'fixing' if the fixation window was entered - %> but not for the requisite fixation time, or the yes or no string. - % =================================================================== - function [out, window, exclusion] = testSearchHoldFixation(me, yesString, noString) - [fix, fixtime, searching, window, exclusion, initfail] = me.isFixated(); - if exclusion - if me.verbose; fprintf('-+-+-> Tobii:testSearchHoldFixation EXCLUSION ZONE ENTERED!\n'); end - out = noString; window = []; - return - end - if initfail - if me.verbose; fprintf('-+-+-> Tobii:testSearchHoldFixation FIX INIT TIME FAILED!\n'); end - out = noString; - return - end - if searching - if (me.fixation.strict==true && (me.fixN == 0)) || me.fixation.strict==false - out = 'searching'; - else - out = noString; - if me.verbose; fprintf('-+-+-> Tobii:testSearchHoldFixation STRICT SEARCH FAIL: %s [%g %g %g]\n', out, fix, fixtime, searching);end - end - return - elseif fix - if (me.fixation.strict==true && ~(me.fixN == -100)) || me.fixation.strict==false - if fixtime - out = yesString; - if me.verbose; fprintf('-+-+-> Tobii:testSearchHoldFixation FIXATION SUCCESSFUL!: %s [%g %g %g]\n', out, fix, fixtime, searching);end + if isfield(td,'systemTimeStamp') && ~isempty(td.systemTimeStamp) + sample.time = double(td.systemTimeStamp(end)) / 1e6; %remember these are in microseconds else - out = 'fixing'; + sample.time = []; end - else - out = noString; - if me.verbose;fprintf('-+-+-> Tobii:testSearchHoldFixation FIX FAIL: %s [%g %g %g]\n', out, fix, fixtime, searching);end - end - return - elseif searching == false - out = noString; - if me.verbose;fprintf('-+-+-> Tobii:testSearchHoldFixation SEARCH FAIL: %s [%g %g %g]\n', out, fix, fixtime, searching);end - else - out = ''; - end - end - - % =================================================================== - %> @brief Checks if we're within fix window. Input is - %> 2 strings, either one is returned depending on success or - %> failure, 'fixing' means the fixation time is not yet met... - %> - %> @param yesString if this function succeeds return this string - %> @param noString if this function fails return this string - %> @return out the output string which is 'fixing' if the fixation window was entered - %> but not for the requisite fixation time, or the yes or no string. - % =================================================================== - function [out, window, exclusion] = testHoldFixation(me, yesString, noString) - [fix, fixtime, searching, window, exclusion, initfail] = me.isFixated(); - if exclusion - if me.verbose; fprintf('-+-+-> Tobii:testHoldFixation EXCLUSION ZONE ENTERED!\n'); end - out = noString; window = []; - return - end - if initfail - if me.verbose; fprintf('-+-+-> Tobii:testSearchHoldFixation FIX INIT TIME FAILED!\n'); end - out = noString; - return - end - if fix - if (me.fixation.strict==true && ~(me.fixN == -100)) || me.fixation.strict==false - if fixtime - out = yesString; - if me.verbose; fprintf('-+-+-> Tobii:testHoldFixation FIXATION SUCCESSFUL!: %s [%g %g %g]\n', out, fix, fixtime, searching);end - else - out = 'fixing'; - end - else - out = noString; - if me.verbose;fprintf('-+-+-> Tobii:testHoldFixation FIX FAIL: %s [%g %g %g]\n', out, fix, fixtime, searching);end - end - return - else - out = noString; - if me.verbose; fprintf('-+-+-> Tobii:testHoldFixation FIX FAIL: %s [%g %g %g]\n', out, fix, fixtime, searching);end - return - end - end - - % =================================================================== - %> @brief Save the data - %> - % =================================================================== - function saveData(me,tofile) - if ~exist('tofile','var') || isempty(tofile); tofile = true; end - ts = tic; - me.data = []; - if me.isConnected - me.data = me.tobii.collectSessionData(); - end - me.initialiseSaveFile(); - if ~isempty(me.data) && tofile - tobii = me; - if exist(me.saveFile,'file') - [p,f,e] = fileparts(me.saveFile); - me.saveFile = [p filesep f me.savePrefix e]; - end - save(me.saveFile,'tobii') - disp('===========================') - me.salutation('saveData',sprintf('Save: %s in %.1fms\n',strrep(me.saveFile,'\','/'),toc(ts)*1e3),true); - disp('===========================') - clear tobii - elseif isempty(me.data) - me.salutation('saveData',sprintf('NO data available... (%.1fms)...\n',toc(ts)*1e3),true); - elseif ~isempty(me.data) - me.salutation('saveData',sprintf('Data retrieved to object in %.1fms)...\n',toc(ts)*1e3),true); - end - end - - % =================================================================== - %> @brief draw the current eye position on the PTB display - %> - % =================================================================== - function drawEyePosition(me,details) - if ~exist('details','var'); details = false; end - if (me.isDummy || me.isConnected) && me.screen.isOpen ... - && ~isempty(me.currentSample) && me.currentSample.valid - xy = [me.currentSample.gx me.currentSample.gy]; - if details - if me.isFix - if me.fixLength > me.fixation.time - Screen('DrawDots', me.win, xy, me.eyeSize, [0 1 0.25 1], [], 0); - else - Screen('DrawDots', me.win, xy, me.eyeSize, [0.75 0 0.75 1], [], 0); - end + if isfield(td,'deviceTimeStamp') && ~isempty(td.deviceTimeStamp) + sample.timeD = double(td.deviceTimeStamp(end)) / 1e6; else - Screen('DrawDots', me.win, xy, me.eyeSize, [0.7 0.5 0 1], [], 0); + sample.timeD = []; end + sample.timeD2 = GetSecs; + switch me.smoothing.eyes + case 'left' + xy = td.left.gazePoint.onDisplayArea(:,td.left.gazePoint.valid); + case 'right' + xy = td.right.gazePoint.onDisplayArea(:,td.right.gazePoint.valid); + otherwise + if all(td.left.gazePoint.valid & td.right.gazePoint.valid) + v = td.left.gazePoint.valid & td.right.gazePoint.valid; + xy = [td.left.gazePoint.onDisplayArea(:,v);... + td.right.gazePoint.onDisplayArea(:,v)]; + else + xy = [td.left.gazePoint.onDisplayArea(:,td.left.gazePoint.valid),... + td.right.gazePoint.onDisplayArea(:,td.right.gazePoint.valid)]; + end + end + end + if ~isempty(xy) + me.xAllRaw = [me.xAllRaw xy(1,:)]; + me.yAllRaw = [me.yAllRaw xy(2,:)]; + sample.valid = true; + xy = doSmoothing(me,xy); + xy = toPixels(me, xy,'','relative'); + sample.gx = xy(1); + sample.gy = xy(2); + sample.pa = mean(td.left.pupil.diameter,'omitnan'); + xyd = me.toDegrees(xy); + me.x = xyd(1); me.y = xyd(2); + me.pupil = sample.pa; + if me.debug;fprintf('>>X: %2.2f | Y: %2.2f | P: %.2f\n',me.x,me.y,me.pupil);end else - Screen('DrawDots', me.win, xy, me.eyeSize, [0.7 0.5 0 1], [], 0); + sample.gx = NaN; + sample.gy = NaN; + sample.pa = NaN; + me.x = NaN; + me.y = NaN; + me.pupil = NaN; end + me.xAll = [me.xAll me.x]; + me.yAll = [me.yAll me.y]; + me.pupilAll = [me.pupilAll me.pupil]; + else + sample = me.sampleTemplate; + if me.debug;fprintf('-+-+-> tobiiManager.getSample(): are you sure you are recording?\n');end end + me.currentSample = sample; end % =================================================================== @@ -1198,6 +665,46 @@ classdef tobiiManager < optickaCore end end end + + % =================================================================== + %> @brief Save the data + %> + % =================================================================== + function saveData(me,tofile) + if ~exist('tofile','var') || isempty(tofile); tofile = true; end + ts = tic; + me.data = []; + if me.isConnected && ~me.isCollectedData + me.data = me.tobii.collectSessionData(); + me.isCollectedData = true; + end + if ~isempty(me.data) && tofile + tobii = me; + if exist(me.saveFile,'file') + initialiseSaveFile(me); + [p,f,e] = fileparts(me.saveFile); + me.saveFile = [p filesep me.savePrefix '-' f '.mat']; + end + try + save(me.saveFile,'tobii'); + disp('===========================') + me.salutation('saveData',sprintf('Save: %s in %.1fms\n',strrep(me.saveFile,'\','/'),toc(ts)*1e3),true); + disp('===========================') + clear tobii + catch ERR + warning('Save FAILED: %s in %.1fms\n',strrep(me.saveFile,'\','/'),toc(ts)*1e3); + getReport(ERR); + end + elseif isempty(me.data) + disp('===========================') + me.salutation('saveData',sprintf('NO data available... (%.1fms)...\n',toc(ts)*1e3),true); + disp('===========================') + elseif ~isempty(me.data) + disp('===========================') + me.salutation('saveData',sprintf('Data retrieved to object in %.1fms)...\n',toc(ts)*1e3),true); + disp('===========================') + end + end % =================================================================== %> @brief send message to store in tracker data @@ -1214,6 +721,60 @@ classdef tobiiManager < optickaCore if me.verbose; fprintf('-+-+->TOBII Message: %s\n',message);end end end + + % =================================================================== + %> @brief close the tobii and cleanup + %> is enabled + %> + % =================================================================== + function close(me) + try + try stopRecording(me,true); end + if me.recordData && ~me.isCollectedData + saveData(me,false); + end + out = me.tobii.deInit(); + me.isConnected = false; + me.isRecording = false; + resetFixation(me); + if me.secondScreen && ~isempty(me.operatorScreen) && isa(me.operatorScreen,'screenManager') + try me.operatorScreen.close; end + end + catch ME + me.salutation('Close Method','Couldn''t stop recording, forcing shutdown...',true) + me.tobii.deInit(); + me.isConnected = false; + me.isRecording = false; + resetFixation(me); + if me.secondScreen && ~isempty(me.operatorScreen) && isa(me.operatorScreen,'screenManager') + me.operatorScreen.close; + end + getReport(ME); + end + end + + % =================================================================== + %> @brief + %> + % =================================================================== + function updateDefaults(me) + if isa(me.tobii, 'Titta') + me.tobii.setOptions(me.settings); + end + end + + % =================================================================== + %> @brief check the connection with the tobii + %> + % =================================================================== + function connected = checkConnection(me) + if isa(me.tobii,'Titta') + me.isConnected = true; + else + me.isConnected = false; + end + connected = me.isConnected; + end % =================================================================== %> @brief Sync time with tracker @@ -1310,13 +871,13 @@ classdef tobiiManager < optickaCore %> % =================================================================== function runDemo(me,forcescreen) - KbName('UnifyKeyNames') - stopkey = KbName('q'); + PsychDefaultSetup(2); + stopKey = KbName('q'); upKey = KbName('uparrow'); downKey = KbName('downarrow'); leftKey = KbName('leftarrow'); rightKey = KbName('rightarrow'); - calibkey = KbName('c'); + calibKey = KbName('c'); ofixation = me.fixation; me.sampletime = []; osmoothing = me.smoothing; @@ -1331,35 +892,19 @@ classdef tobiiManager < optickaCore if isa(me.screen,'screenManager') && ~isempty(me.screen) s = me.screen; else - s = screenManager('blend',true,'pixelsPerCm',36,'distance',60); + s = screenManager('blend',true,'pixelsPerCm',36,'distance',60,'disableSyncTests',true); end - s.disableSyncTests = false; + if exist('forcescreen','var'); s.screen = forcescreen; end s.backgroundColour = [0.5 0.5 0.5 0]; - if length(Screen('Screens'))>1 && s.screen - 1 >= 0 - useS2 = true; - s2.pixelsPerCm = 45; - s2 = screenManager; - s2.screen = s.screen - 1; - s2.backgroundColour = s.backgroundColour; - [w,h] = Screen('WindowSize',s2.screen); - s2.windowed = [0 0 round(w/2) round(h/2)]; - s2.bitDepth = '8bit'; - s2.blend = true; - s2.disableSyncTests = true; - end - sv=open(s); %open our screen + if ~s.isOpen; sv=open(s); else; sv=s.screenVals; end + initialise(me, s); + if me.useOperatorScreen; s2 = me.operatorScreen; end + if ~s2.isOpen; open(s2); useS2 = true; end - if useS2 - me.closeSecondScreen = false; - initialise(me, s, s2); %initialise tobii with our screen - s2.open(); - else - initialise(me, s); %initialise tobii with our screen - end - trackerSetup(me); - ShowCursor; %titta fails to show cursor so we must do it + trackerSetup(me); ShowCursor; + drawPhotoDiodeSquare(s,[0 0 0 1]); flip(s); %make sure our photodiode patch is black % set up the size and position of the stimulus @@ -1374,19 +919,21 @@ classdef tobiiManager < optickaCore setup(f,s); %setup our stimulus with open screen o.xPositionOut = me.fixation.X; o.yPositionOut = me.fixation.Y; - f.alpha + f.alpha; f.xPositionOut = me.fixation.X; f.xPositionOut = me.fixation.X; % set up an exclusion zone where eye is not allowed - me.exclusionZone = [8 12 9 12]; + me.exclusionZone = [8 10 8 10]; exc = me.toPixels(me.exclusionZone); exc = [exc(1) exc(3) exc(2) exc(4)]; %psychrect=[left,top,right,bottom] + RestrictKeysForKbCheck([stopKey upKey downKey leftKey rightKey calibKey]); + % warm up fprintf('\n===>>> Warming up the GPU, Eyetracker etc... <<<===\n') Priority(MaxPriority(s.win)); - HideCursor(s.win); + %HideCursor(s.win); endExp = 0; trialn = 1; maxTrials = 10; @@ -1412,7 +959,7 @@ classdef tobiiManager < optickaCore drawPhotoDiodeSquare(s,[0 0 0 1]); flip(s); if useS2;flip(s2);end - ListenChar(-1); + ListenChar(-1); % ListenChar(0); update(o); %make sure stimuli are set back to their start state update(f); WaitSecs('YieldSecs',0.5); @@ -1421,8 +968,8 @@ classdef tobiiManager < optickaCore trialtick = 1; trackerMessage(me,sprintf('Settings for Trial %i, X=%.2f Y=%.2f, SZ=%.2f',trialn,me.fixation.X,me.fixation.Y,o.sizeOut)) drawPhotoDiodeSquare(s,[0 0 0 1]); - flip(s2,[],[],2); vbl = flip(s); tstart=vbl+sv.ifi; + if useS2;flip(s2,[],[],2);end trackerMessage(me,'STARTVBL',vbl); while vbl < tstart + 6 Screen('FillRect',s.win,[0.7 0.7 0.7 0.5],exc); Screen('DrawText',s.win,'Exclusion Zone',exc(1),exc(2),[0.8 0.8 0.8]); @@ -1434,18 +981,19 @@ classdef tobiiManager < optickaCore getSample(me); isFixated(me); if ~isempty(me.currentSample) - txt = sprintf('Q = finish. X: %3.1f / %2.2f | Y: %3.1f / %2.2f | # = %2i %s %s | RADIUS = %s | TIME = %.2f | FIXATION = %.2f | EXC = %i | INIT FAIL = %i',... + txt = sprintf('Q = finish. X: %3.1f / %2.2f | Y: %3.1f / %2.2f | # = %2i %s %s | RADIUS = %s | TIME = %.2f | FIXATION %i = %.2f (buffer: %.2f) | EXC = %i | INIT FAIL = %i',... me.currentSample.gx, me.x, me.currentSample.gy, me.y, me.smoothing.nSamples,... me.smoothing.method, me.smoothing.eyes, sprintf('%1.1f ',me.fixation.radius), ... - me.fixTotal,me.fixLength,me.isExclusion,me.isInitFail); + me.fixTotal,me.fixN,me.fixLength,me.fixBuffer,me.isExclusion,me.isInitFail); Screen('DrawText', s.win, txt, 10, 10,[1 1 1]); - drawEyePosition(me,true); + drawEyePosition(me); %psn{trialn} = me.tobii.buffer.peekN('positioning',1); end if useS2 drawGrid(s2); trackerDrawExclusion(me); trackerDrawFixation(me); + trackerDrawEyePosition(me); end finishDrawing(s); animate(o); @@ -1455,8 +1003,8 @@ classdef tobiiManager < optickaCore if useS2; flip(s2,[],[],2); end [keyDown, ~, keyCode] = KbCheck(-1); if keyDown - if keyCode(stopkey); endExp = 1; break; - elseif keyCode(calibkey); me.doCalibration; + if keyCode(stopKey); endExp = 1; break; + elseif keyCode(calibKey); me.trackerSetup; elseif keyCode(upKey); me.smoothing.nSamples = me.smoothing.nSamples + 1; if me.smoothing.nSamples > 400; me.smoothing.nSamples=400;end elseif keyCode(downKey); me.smoothing.nSamples = me.smoothing.nSamples - 1; if me.smoothing.nSamples < 1; me.smoothing.nSamples=1;end elseif keyCode(leftKey); m=m+1; if m>5;m=1;end; me.smoothing.method=methods{m}; @@ -1470,21 +1018,20 @@ classdef tobiiManager < optickaCore vbl = flip(s); if useS2; flip(s2,[],[],2); end trackerMessage(me,'END_RT',vbl); - trackerMessage(me,'TRIAL_RESULT 1') - trackerMessage(me,sprintf('Ending trial %i @ %i',trialn,int64(round(vbl*1e6)))) + trackerMessage(me,'TRIAL_RESULT 1'); + trackerMessage(me,sprintf('Ending trial %i @ %i',trialn,int64(round(vbl*1e6)))); resetFixation(me); - me.fixation.X = randi([-7 7]); - me.fixation.Y = randi([-7 7]); if length(me.fixation.radius) == 1 - me.fixation.radius = randi([1 3]); + r = randi([1 3]); o.sizeOut = me.fixation.radius * 2; f.sizeOut = me.fixation.radius * 2; else - me.fixation.radius = [randi([1 3]) randi([1 3])]; + r = [randi([1 3]) randi([1 3])]; o.sizeOut = mean(me.fixation.radius) * 2; f.barWidthOut = me.fixation.radius(1) * 2; f.barHeightOut = me.fixation.radius(2) * 2; end + updateFixationValues(me,randi([-7 7]),randi([-7 7])); o.xPositionOut = me.fixation.X; o.yPositionOut = me.fixation.Y; f.xPositionOut = me.fixation.X; @@ -1495,14 +1042,15 @@ classdef tobiiManager < optickaCore else drawPhotoDiodeSquare(s,[0 0 0 1]); vbl = flip(s); + if useS2; flip(s2,[],[],2); end trackerMessage(me,'END_RT',vbl); - trackerMessage(me,'TRIAL_RESULT -10 ABORT') - trackerMessage(me,sprintf('Aborting %i @ %i', trialn, int64(round(vbl*1e6)))) + trackerMessage(me,'TRIAL_RESULT -10 ABORT'); + trackerMessage(me,sprintf('Aborting %i @ %i', trialn, int64(round(vbl*1e6)))); end end - stopRecording(me); - ListenChar(0); Priority(0); ShowCursor; - try close(s); close(s2);end %#ok<*TRYNC> + stopRecording(me,true); + ListenChar(0); Priority(0); ShowCursor; RestrictKeysForKbCheck([]); + try close(s); if useS2;close(s2);end; end %#ok<*TRYNC> saveData(me); assignin('base','psn',psn); assignin('base','data',me.data); @@ -1514,19 +1062,20 @@ classdef tobiiManager < optickaCore me.fixInit = oldfixinit; clear s s2 o catch ME - stopRecording(me); + try stopRecording(me,true); end me.fixation = ofixation; me.saveFile = ofilename; me.smoothing = osmoothing; me.exclusionZone = oldexc; me.fixInit = oldfixinit; - ListenChar(0);Priority(0);ShowCursor; + ListenChar(0); Priority(0); ShowCursor; RestrictKeysForKbCheck([]); getReport(ME) - close(s); + try close(s); end + try if useS2;close(s2);end; end sca; - close(me); - clear s o - rethrow(ME) + try close(me); end + clear s s2 o + rethrow(ME); end end @@ -1535,216 +1084,13 @@ classdef tobiiManager < optickaCore %> @brief %> % =================================================================== - function doCalibration(me) - if me.isConnected - me.trackerSetup(); - end - end - - % =================================================================== - %> @brief draw the background colour - %> - % =================================================================== - function trackerClearScreen(me) - if ~me.isConnected || ~me.operatorScreen.isOpen; return;end - drawBackground(me.operatorScreen); - end - - % =================================================================== - %> @brief draw general status - %> - % =================================================================== - function trackerDrawStatus(me, comment, stimPos, dontClear) - if ~me.isConnected || ~me.operatorScreen.isOpen; return;end - if ~exist('comment','var'); comment=''; end - if ~exist('stimPos','var'); stimPos = struct; end - if ~exist('dontClear','var'); dontClear = 0; end - if ~dontClear; trackerClearScreen(me); end - trackerDrawExclusion(me); - trackerDrawFixation(me); - trackerDrawStimuli(me, stimPos); - trackerDrawEyePositions(me); - if ~isempty(comment);trackerDrawText(me, comment);end - trackerFlip(me,dontClear); - end - - % =================================================================== - %> @brief draw the stimuli boxes on the tracker display - %> - % =================================================================== - function trackerDrawStimuli(me, ts, dontClear) - if ~me.isConnected || ~me.operatorScreen.isOpen; return; end - if exist('ts','var') && isstruct(ts) - me.stimulusPositions = ts; - end - if isempty(me.stimulusPositions) || isempty(fieldnames(me.stimulusPositions));return;end - if ~exist('dontClear','var');dontClear = true;end - if dontClear==false; trackerClearScreen(me); end - for i = 1:length(me.stimulusPositions) - x = me.stimulusPositions(i).x; - y = me.stimulusPositions(i).y; - size = me.stimulusPositions(i).size; - if isempty(size); size = 1 * me.ppd_; end - if me.stimulusPositions(i).selected == true - drawBoxPx(me.operatorScreen,[x; y],size,[0.5 1 0 0.5]); - else - drawBoxPx(me.operatorScreen,[x; y],size,[0.6 0.6 0.3]); - end - end - end - - % =================================================================== - %> @brief draw the fixation box on the tracker display - %> - % =================================================================== - function trackerDrawFixation(me) - if ~me.isConnected || ~me.operatorScreen.isOpen; return; end - if length(me.fixation.radius) == 1 - drawSpot(me.operatorScreen,me.fixation.radius,[0.5 0.6 0.5 1],me.fixation.X,me.fixation.Y); - else - rect = [me.fixation.X - me.fixation.radius(1), ... - me.fixation.Y - me.fixation.radius(2), ... - me.fixation.X + me.fixation.radius(1), ... - me.fixation.Y + me.fixation.radius(2)]; - drawRect(me.operatorScreen,rect,[0.5 0.6 0.5 1]); - end - end - - % =================================================================== - %> @brief draw the fixation position on the tracker display - %> - % =================================================================== - function trackerDrawEyePosition(me) - if ~me.isConnected || ~me.operatorScreen.isOpen; return; end - if me.isFix - if me.fixLength > me.fixation.time - drawSpot(me.operatorScreen,0.3,[0 1 0.25 0.75],me.x,me.y); - else - drawSpot(me.operatorScreen,0.3,[0.75 0.25 0.75 0.75],me.x,me.y); - end - else - drawSpot(me.operatorScreen,0.3,[0.7 0.5 0 0.5],me.x,me.y); - end - end - - % =================================================================== - %> @brief draw the sampled eye positions in xAll yAll - %> - % =================================================================== - function trackerDrawEyePositions(me) - if ~me.isConnected || ~me.operatorScreen.isOpen; return; end - if ~isempty(me.xAll) && ~isempty(me.yAll) && (length(me.xAll)==length(me.yAll)) - xy = [me.xAll;me.yAll]; - drawDots(me.operatorScreen,xy,8,[0.5 1 0 0.2]); - end - end - - % =================================================================== - %> @brief draw the fixation box on the tracker display - %> - % =================================================================== - function trackerDrawExclusion(me) - if ~me.isConnected || ~me.operatorScreen.isOpen || isempty(me.exclusionZone); return; end - for i = 1:size(me.exclusionZone,1) - drawRect(me.operatorScreen, [me.exclusionZone(1), ... - me.exclusionZone(3), me.exclusionZone(2), ... - me.exclusionZone(4)],[0.7 0.6 0.6]); - end - end - - % =================================================================== - %> @brief draw the fixation box on the tracker display - %> - % =================================================================== - function trackerDrawText(me,textIn) - if ~me.isConnected || ~me.operatorScreen.isOpen || ~exist('textIn','var'); return; end - drawText(me.operatorScreen,textIn); - end - - % =================================================================== - %> @brief draw the fixation box on the tracker display - %> - % =================================================================== - function trackerFlip(me,dontclear) - if ~me.isConnected || ~me.operatorScreen.isOpen; return; end - if ~exist('dontclear','var');dontclear = 1; end - me.operatorScreen.flip([], dontclear, 2); - end - - % =================================================================== - %> @brief smooth data in M x N where M = 2 (x&y trace) or M = 4 is x&y - %> for both eyes. Output is 2 x 1 x&y averages position - %> - % =================================================================== - function out = doSmoothing(me,in) - if size(in,2) > me.smoothing.window * 2 - switch me.smoothing.method - case 'median' - out = movmedian(in,me.smoothing.window,2); - out = median(out, 2); - case {'heuristic','heuristic1'} - out = me.heuristicFilter(in,1); - out = median(out, 2); - case 'heuristic2' - out = me.heuristicFilter(in,2); - out = median(out, 2); - case {'sg','savitzky-golay'} - out = sgolayfilt(in,1,me.smoothing.window,[],2); - out = median(out, 2); - otherwise - out = median(in, 2); - end - elseif size(in, 2) > 1 - out = median(in, 2); - else - out = in; - end - if size(out,1)==4 % XY for both eyes, combine together. - out = [mean([out(1) out(3)]); mean([out(2) out(4)])]; - end - if length(out) ~= 2 - out = [NaN NaN]; - end - end - - % =================================================================== - %> @brief - %> - % =================================================================== - function value = get.isRecording(me) + function value = checkRecording(me) if me.isConnected value = me.tobii.buffer.isRecording('gaze'); else value = false; end - me.isRecording_ = value; - end - - % =================================================================== - %> @brief - %> - % =================================================================== - function value = get.smoothingTime(me) - value = (1000 / me.sampleRate) * me.smoothing.nSamples; - end - - % =================================================================== - %> @brief - %> - % =================================================================== - function set.model(me,value) - me.model = value; - switch me.model - case {'IS4_Large_Peripheral','Tobii 4C'} - me.sampleRate = 90; %#ok<*MCSUP> - me.trackingMode = 'Default'; - case {'Tobii TX300','TX300'} - me.sampleRate = 300; - me.trackingMode = 'Default'; - otherwise - me.sampleRate = 300; - me.trackingMode = 'human'; - end + me.isRecording = value; end end%-------------------------END PUBLIC METHODS--------------------------------% @@ -1808,14 +1154,6 @@ classdef tobiiManager < optickaCore success = driftOffset(me); end - % =================================================================== - %> @brief wrapper for CheckRecording - %> - % =================================================================== - function error = checkRecording(me) - error = false; - end - % =================================================================== %> @brief check what mode the tobii is in %> @@ -1866,153 +1204,16 @@ classdef tobiiManager < optickaCore %======================================================================= methods (Access = private) %------------------PRIVATE METHODS - %======================================================================= - - % =================================================================== - %> @brief Stampe 1993 heuristic filter as used by Eyelink - %> - %> @param indata - input data - %> @param level - 1 = filter level 1, 2 = filter level 1+2 - %> @param steps - we step every # steps along the in data, changes the filter characteristics, 3 is the default (filter 2 is #+1) - %> @out out - smoothed data - % =================================================================== - function out = heuristicFilter(~,indata,level,steps) - if ~exist('level','var'); level = 1; end %filter level 1 [std] or 2 [extra] - if ~exist('steps','var'); steps = 3; end %step along the data every n steps - out=zeros(size(indata)); - for k = 1:2 % x (row1) and y (row2) eye samples - in = indata(k,:); - %filter 1 from Stampe 1993, see Fig. 2a - if level > 0 - for i = 1:steps:length(in)-2 - x = in(i); x1 = in(i+1); x2 = in(i+2); %#ok<*PROPLC> - if ((x2 > x1) && (x1 < x)) || ((x2 < x1) && (x1 > x)) - if abs(x1-x) < abs(x2-x1) %i is closest - x1 = x; - else - x1 = x2; - end - end - x2 = x1; - x1 = x; - in(i)=x; in(i+1) = x1; in(i+2) = x2; - end - end - %filter2 from Stampe 1993, see Fig. 2b - if level > 1 - for i = 1:steps+1:length(in)-3 - x = in(i); x1 = in(i+1); x2 = in(i+2); x3 = in(i+3); - if x2 == x1 && (x == x1 || x2 == x3) - x3 = x2; - x2 = x1; - x1 = x; - else %x2 and x1 are the same, find closest of x2 or x - if abs(x1 - x3) < abs(x1 - x) - x2 = x3; - x1 = x3; - else - x2 = x; - x1 = x; - end - end - in(i)=x; in(i+1) = x1; in(i+2) = x2; in(i+3) = x3; - end - end - out(k,:) = in; - end - end - - % =================================================================== - %> @brief to pixels from visual degrees / relative - %> - % =================================================================== - function out = toPixels(me,in,axis,inputtype) - if ~exist('axis','var') || isempty(axis); axis=''; end - if ~exist('inputtype','var') || isempty(inputtype); inputtype = 'degrees'; end - out = 0; - if length(in)>4; return; end - switch axis - case 'x' - switch inputtype - case 'degrees' - out = (in * me.ppd_) + me.screen.xCenter; - case 'relative' - out = in * me.screen.screenVals.width; - end - case 'y' - switch inputtype - case 'degrees' - out = (in * me.ppd_) + me.screen.yCenter; - case 'relative' - out = in * me.screen.screenVals.height; - end - otherwise - switch inputtype - case 'degrees' - if length(in)==2 - out(1) = (in(1) * me.ppd_) + me.screen.xCenter; - out(2) = (in(2) * me.ppd_) + me.screen.yCenter; - elseif length(in)==4 - out(1:2) = (in(1:2) * me.ppd_) + me.screen.xCenter; - out(3:4) = (in(3:4) * me.ppd_) + me.screen.yCenter; - end - case 'relative' - if length(in)==2 - out(1) = in(1) * me.screen.screenVals.width; - out(2) = in(2) * me.screen.screenVals.height; - elseif length(in)==4 - out(1:2) = in(1:2) * me.screen.screenVals.width; - out(3:4) = in(3:4) * me.screen.screenVals.height; - end - end - end - end - - % =================================================================== - %> @brief to visual degrees from pixels - %> - % =================================================================== - function out = toDegrees(me,in,axis,inputtype) - if ~exist('axis','var') || isempty(axis); axis=''; end - if ~exist('inputtype','var') || isempty(inputtype); inputtype = 'pixels'; end - out = 0; - if length(in)>2; return; end - switch axis - case 'x' - in = in(1); - switch inputtype - case 'pixels' - out = (in - me.screen.xCenter) / me.ppd_; - case 'relative' - out = (in - 0.5) * (me.screen.screenVals.width /me.ppd_); - end - case 'y' - in = in(1); - switch inputtype - case 'pixels' - out = (in - me.screen.yCenter) / me.ppd_; return - case 'relative' - out = (in - 0.5) * (me.screen.screenVals.height /me.ppd_); - end - otherwise - switch inputtype - case 'pixels' - out(1) = (in(1) - me.screen.xCenter) / me.ppd_; - out(2) = (in(2) - me.screen.yCenter) / me.ppd_; - case 'relative' - out(1) = (in - 0.5) * (me.screen.screenVals.width /me.ppd_); - out(2) = (in - 0.5) * (me.screen.screenVals.height /me.ppd_); - end - end - end + %======================================================================= % =================================================================== %> @brief %> % =================================================================== function initTracker(me) - me.settings = Titta.getDefaults(me.model); - me.settings.cal.bgColor = 127; + if isempty(me.settings) + me.settings = Titta.getDefaults(me.calibration.model); + end me.tobii = Titta(me.settings); end diff --git a/help/METHODS.md b/help/METHODS.md index 1263c6f5d0d32d5db93ce628af5fa836e401718d..f0080488e44e75be8adb0fdfc0a49cf65802ba5c 100644 --- a/help/METHODS.md +++ b/help/METHODS.md @@ -1,6 +1,6 @@ # Useful Task Methods -The state machine (`stateMachine` class) defines states and the connections between them. The state machine can run cell arrays of methods (`@()` anonymous functions) when states are entered (run once), within (repeated on every screen redraw) and exited (run once). In addition there are ways to transition *out* of a state if some condition is met. For example if we are in a `[STATE 1]` state and the eyetracker tells us the subject has fixated for the correct time, then transition functions can jump us to another state to e.g. show a stimulus. +The state machine (`stateMachine` class) defines states and the connections between them. The state machine can run cell arrays of methods (`@()` anonymous functions) when states are entered (run once), within (repeated on every screen redraw) and exited (run once). In addition there are ways to transition *out* of a state if some condition is met. For example if we are in `[STATE 1]` and the eyetracker tells us the subject has fixated for the correct time, then transition functions can jump us to another state to e.g. show a stimulus. ```{.smaller} @@ -25,7 +25,7 @@ The state machine (`stateMachine` class) defines states and the connections betw └──────────┘ ``` -These various methods control the logic and flow of experiments. This document lists the most important ones used in flexible behavioural task design. It is better for these methods to evaluate properties (properties are the variables managed by the class object). Because of this we choose to create methods that alter the properties of each class. For example, `show(stims)` is a method that allows the stimulus manager to show all stimuli in the list; it does this by setting each stimulus' `isVisible` property to `true`. `hide(stims)` hides all stimuli bby setting `isVisible` property to `false`, or you could just hide the 3rd stimulus in the list: `hide(stims, 3)`. +These various methods control the logic and flow of experiments. This document lists the most important ones used in flexible behavioural task design. It is better for methods to evaluate properties (properties are the variables managed by the class object). Because of this we choose to create methods that alter the properties of each class. For example, `show(stims)` is a method that allows the stimulus manager to show all stimuli in the list; it does this by setting each stimulus' `isVisible` property to `true`. `hide(stims)` hides all stimuli bby setting `isVisible` property to `false`, or you could just hide the 3rd stimulus in the list: `hide(stims, 3)`. For those unfamiliar with object-oriented design, a *CLASS* (e.g. `stateMachine`) is initiated as an *OBJECT* variable (named `sM` during the experiment run, it is an *instance* of the class). **ALL** Opticka classes are [**handle classes**](https://www.mathworks.com/help/matlab/handle-classes.html); this means if we assign `sM2 = sM` — **both** of these named instances point to the **same** object. @@ -64,6 +64,9 @@ The principal class object that 'runs' the experiment. - `updateConditionalFixationTarget(me, stimulus, variable, value, varargin)` Say you have 4 stimuli each with a different angle changed on every trial by the task object, and want the stimulus matching `angle = 90` to be used as the fixation target. This method finds which stimulus is set to a particular variable value and assigns the fixation target X and Y position to that stimulus. +- `updateNextState(me, type)` + It is possible to force the stateMachine to jump to a tranisition to a named state by editing stateMachine.tempNextState. This method takes the current taskSequence trialVar or blockVar and sets the next state name to the value contained for the current trial. So for example you can set trialVar to `{'stimulus','catch'}` which randomises each trial with either 'stimulus' or 'catch', then use `@()updateNextState(me,'trial')` to choose this value as the temporary next state name. + ----------------------------------- ## Task sequence manager ("task" in the state file) @@ -151,8 +154,8 @@ This class manages groups of stimuli as a single object. Each stimulus can be sh ------------ -* **Question:** . -* **Answer:** . +* **Question:** I want to randomise some values of the stimuli but not include them as an indepedant variable. +* **Answer:** The metaStimulus object contains a stimulusTable which allows you to make changes to stimuli without them added to the trial structure. This is useful during training, or if you need randomisation tangential to the task. As an example, in Chen et al., 2020 Science their Saccade-to-Phosphene task randomises the size and colour of the target but this is not used as a task variable. In this case set stimulusTable and then call `@()randomise(stims);` in the state machine functions (normally just before you call `@()update(stims);`). This will give randomised size and colour without adding any independant variables. ------------ diff --git a/help/Opticka-Keyboard-Mapping.odt b/help/Opticka-Keyboard-Mapping.odt index d4e9f735c8afbfc3e9ef676706c00abdb30643dc..f39a34a12848ee9b724f51d67035a9575768dad4 100644 Binary files a/help/Opticka-Keyboard-Mapping.odt and b/help/Opticka-Keyboard-Mapping.odt differ diff --git a/help/uihelpfunctions.html b/help/uihelpfunctions.html index 7ec7070bd3281f9f3ca20273d353d38c4d174885..a4406131aa39c00e0150f7d94d2df177af8f95e8 100644 --- a/help/uihelpfunctions.html +++ b/help/uihelpfunctions.html @@ -4,7 +4,7 @@ - + uihelpfunctions + + diff --git a/ui/images/edit.svg b/ui/images/edit.svg new file mode 100644 index 0000000000000000000000000000000000000000..4cac2f900e4f147d67a1f35e31c2e4e6afab3706 --- /dev/null +++ b/ui/images/edit.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/ui/images/link.circle.off.svg b/ui/images/link.circle.off.svg new file mode 100644 index 0000000000000000000000000000000000000000..adaa94323aa5c7e3b1623e24782b9321556715aa --- /dev/null +++ b/ui/images/link.circle.off.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + diff --git a/ui/images/link.circle.svg b/ui/images/link.circle.svg new file mode 100644 index 0000000000000000000000000000000000000000..090200adbe0fe9fd0b876e73ec2d1a132a4b8f97 --- /dev/null +++ b/ui/images/link.circle.svg @@ -0,0 +1,44 @@ + + + + + + + + diff --git a/ui/images/new.svg b/ui/images/new.svg new file mode 100644 index 0000000000000000000000000000000000000000..ffbb96b45f50edcf78a43a3a0803a2a4187ba7f9 --- /dev/null +++ b/ui/images/new.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/ui/images/open.svg b/ui/images/open.svg new file mode 100644 index 0000000000000000000000000000000000000000..ff3cac4b80153201717e24ed1aec4ee73ae9583e --- /dev/null +++ b/ui/images/open.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/ui/images/opticka-small.png b/ui/images/opticka-small.png new file mode 100644 index 0000000000000000000000000000000000000000..6221af956ef4a8abf1aab081dbd1e9963e52f723 Binary files /dev/null and b/ui/images/opticka-small.png differ diff --git a/ui/images/question.svg b/ui/images/question.svg index 76b1cb4816b70215d9b8de5a8137d4b74be0dbc2..dcb00e68f2ddd7db0c496181ab1a270ceb0b8785 100644 --- a/ui/images/question.svg +++ b/ui/images/question.svg @@ -1 +1,16 @@ - \ No newline at end of file + + + + + + + + diff --git a/ui/images/refresh.svg b/ui/images/refresh.svg new file mode 100644 index 0000000000000000000000000000000000000000..503b429e61fa42858dcfb8cab493496ba5873335 --- /dev/null +++ b/ui/images/refresh.svg @@ -0,0 +1,15 @@ + + + + + + + diff --git a/ui/images/save.svg b/ui/images/save.svg new file mode 100644 index 0000000000000000000000000000000000000000..9b618384600e329af05683f986f9744cf137466c --- /dev/null +++ b/ui/images/save.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/ui/images/send.svg b/ui/images/send.svg new file mode 100644 index 0000000000000000000000000000000000000000..c59b4598a8fb8d8d5af557ed4021eb563d8df923 --- /dev/null +++ b/ui/images/send.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/ui/images/up.svg b/ui/images/up.svg new file mode 100644 index 0000000000000000000000000000000000000000..675c22243d77ab9dfe5556654dd5d2bf069c55be --- /dev/null +++ b/ui/images/up.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/ui/legacy/opticka_ui.m b/ui/legacy/opticka_ui.m deleted file mode 100644 index ec91b1d3dad254b4abb08dc1dc88c068424e86fe..0000000000000000000000000000000000000000 --- a/ui/legacy/opticka_ui.m +++ /dev/null @@ -1,4861 +0,0 @@ -function varargout = opticka_ui(varargin) -% OPTICKA_UI_EXPORT M-file for opticka_ui.fig -% OPTICKA_UI_EXPORT, by itself, creates a new OPTICKA_UI_EXPORT or raises the existing -% singleton*. -% -% H = OPTICKA_UI_EXPORT returns the handle to a new OPTICKA_UI_EXPORT or the handle to -% the existing singleton*. -% -% OPTICKA_UI_EXPORT('CALLBACK',hObject,eventData,handles,...) calls the local -% function named CALLBACK in OPTICKA_UI_EXPORT.M with the given input arguments. -% -% OPTICKA_UI_EXPORT('Property','Value',...) creates a new OPTICKA_UI_EXPORT or raises the -% existing singleton*. Starting from the left, property value pairs are -% applied to the GUI before opticka_ui_export_OpeningFcn gets called. An -% unrecognized property name or invalid value makes property application -% stop. All inputs are passed to opticka_ui_export_OpeningFcn via varargin. -% -% *See GUI Options on GUIDE's Tools menu. Choose "GUI allows only one -% instance to run (singleton)". -% -% See also: GUIDE, GUIDATA, GUIHANDLES - -% Edit the above text to modify the response to help opticka_ui - -% Last Modified by GUIDE v2.5 25-Feb-2015 20:32:16 - -% Begin initialization code - DO NOT EDIT -gui_Singleton = 1; -gui_State = struct('gui_Name', mfilename, ... - 'gui_Singleton', gui_Singleton, ... - 'gui_OpeningFcn', @opticka_ui_OpeningFcn, ... - 'gui_OutputFcn', @opticka_ui_OutputFcn, ... - 'gui_LayoutFcn', @opticka_ui_LayoutFcn, ... - 'gui_Callback', []); -if nargin && ischar(varargin{1}) - gui_State.gui_Callback = str2func(varargin{1}); -end - -if nargout - [varargout{1:nargout}] = gui_mainfcn(gui_State, varargin{:}); -else - gui_mainfcn(gui_State, varargin{:}); -end -% End initialization code - DO NOT EDIT - - -% --- Executes just before opticka_ui is made visible. -function opticka_ui_OpeningFcn(hObject, eventdata, handles, varargin) -% This function has no output args, see OutputFcn. -% hObject handle to figure -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -% varargin command line arguments to opticka_ui (see VARARGIN) - -% Choose default command line output for opticka_ui -handles.output = hObject; - -% Update handles structure -guidata(hObject, handles); - -% UIWAIT makes opticka_ui wait for user response (see UIRESUME) -% uiwait(handles.OKRoot); - - -% --- Outputs from this function are returned to the command line. -function varargout = opticka_ui_OutputFcn(hObject, eventdata, handles) -% varargout cell array for returning output args (see VARARGOUT); -% hObject handle to figure -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Get default command line output from handles structure -varargout{1} = handles.output; - -% -------------------------------------------------------------------- -function OKMenuFile_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuFile (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuEdit_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuEdit (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuTools_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuTools (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuStimulusLog_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuStimulusLog (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.task.showLog; -end - -% -------------------------------------------------------------------- -function OKMenuShowGammaPlots_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuShowGammaPlots (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen.gammaTable,'calibrateLuminance') - o.r.screen.gammaTable.plot; - end -end -% -------------------------------------------------------------------- -function OKMenuLogs_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuLogs (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuCheckIO_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuCheckIO (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - -end - -% -------------------------------------------------------------------- -function OKMenuEditConfiguration_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuEditConfiguration (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% -------------------------------------------------------------------- -function OKMenuAllTimingLogs_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuAllTimingLogs (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.showTimingLog; -end - -% -------------------------------------------------------------------- -function OKMenusMLogs_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuAllTimingLogs (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if ~isempty(isprop(o.r.stateMachine,'log')) && ~isempty(o.r.stateMachine.log) - o.r.stateMachine.plotLogs(o.r.stateMachine.log); - else - warndlg('No state machine log available yet...','Opticka') - end -end - -% -------------------------------------------------------------------- -function OKMenuMissedFrames_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuMissedFrames (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuCut_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuCut (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuCopy_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuCopy (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuPaste_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuPaste (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% --- Executes on selection change in OKSelectScreen. -function OKSelectScreen_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - - -function OKMonitorDistance_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - - -function OKpixelsPerCm_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - - -function OKscreenXOffset_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKscreenYOffset_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKWindowSize_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKGLSrc_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - - -function OKGLDst_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKbitDepth_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKAntiAliasing_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKUseGamma_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKSerialPortName_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -% --- Executes on button press in OKUsePhotoDiode. -function OKUsePhotoDiode_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -% --- Executes on button press in OKuseDataPixx. -function OKuseDataPixx_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if strcmpi(get(hObject,'Checked'),'on') - set(hObject,'Checked','off'); - else - set(hObject,'Checked','on'); - set(handles.OKuseDisplayPP,'Checked','off'); - set(handles.OKuseLabJackStrobe,'Checked','off'); - set(handles.OKuseLabJackTStrobe,'Checked','off'); - end - o.getScreenVals; -end - -% --- Executes on button press in OKuseDataPixx. -function OKuseDisplayPP_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if strcmpi(get(hObject,'Checked'),'on') - set(hObject,'Checked','off'); - else - set(hObject,'Checked','on'); - set(handles.OKuseDataPixx,'Checked','off'); - set(handles.OKuseLabJackStrobe,'Checked','off'); - set(handles.OKuseLabJackTStrobe,'Checked','off'); - end - o.getScreenVals; -end - -% --- Executes on button press in OKuseLabJack. -function OKuseLabJackStrobe_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if strcmpi(get(hObject,'Checked'),'on') - set(hObject,'Checked','off'); - else - set(hObject,'Checked','on'); - set(handles.OKuseDataPixx,'Checked','off'); - set(handles.OKuseDisplayPP,'Checked','off'); - set(handles.OKuseLabJackTStrobe,'Checked','off'); - end - o.getScreenVals; -end - -% --- Executes on button press in OKuseLabJack. -function OKuseLabJackTStrobe_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if strcmpi(get(hObject,'Checked'),'on') - set(hObject,'Checked','off'); - else - set(hObject,'Checked','on'); - set(handles.OKuseDataPixx,'Checked','off'); - set(handles.OKuseDisplayPP,'Checked','off'); - set(handles.OKuseLabJackStrobe,'Checked','off'); - end - o.getScreenVals; -end - -% --- Executes on button press in OKuseLabJack. -function OKuseLabJackReward_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if strcmpi(get(hObject,'Checked'),'on') - set(hObject,'Checked','off'); - else - set(hObject,'Checked','on'); - set(handles.OKuseArduino,'Checked','off'); - end - o.getScreenVals; -end - -% --- Executes on button press -function OKuseArduino_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if strcmpi(get(hObject,'Checked'),'on') - set(hObject,'Checked','off'); - else - set(hObject,'Checked','on'); - set(handles.OKuseLabJackReward,'Checked','off'); - end - o.getScreenVals; -end - -% --- Executes on button press -function OKuseEyeOccluder_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if strcmpi(get(hObject,'Checked'),'on') - set(hObject,'Checked','off'); - else - set(hObject,'Checked','on'); - end - o.getScreenVals; -end - -% --- Executes on button press -function OKuseEyeLink_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if strcmpi(get(hObject,'Checked'),'on') - set(hObject,'Checked','off'); - else - set(hObject,'Checked','on'); - set(handles.OKuseTobii,'Checked','off'); - end - o.getScreenVals; -end - -% --- Executes on button press in OKuseEyelink. -function OKuseTobii_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if strcmpi(get(hObject,'Checked'),'on') - set(hObject,'Checked','off'); - else - set(hObject,'Checked','on'); - set(handles.OKuseEyelink,'Checked','off'); - end - o.getScreenVals; -end - - -% --- Executes on button press in OKVerbose. -function OKVerbose_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -% --- Executes on button press in OKlogFrames. -function OKlogFrames_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen,'screenManager') - if get(hObject,'Value') == 1 - set(handles.OKbenchmark,'Value',0) - end - o.getScreenVals; - end -end - -% --- Executes on button press in OKOpenGLBlending. -function OKOpenGLBlending_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -% --- Executes on button press in OKHideFlash. -function OKHideFlash_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -% --- Executes on button press in OKDebug. -function OKDebug_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKbackgroundColour_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKNativeBeamPosition_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKrecordMovie_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKnBlocks_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -function OKisTime_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -function OKtrialTime_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -function OKrealTime_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -% -------------------------------------------------------------------- -function OKMenuNoiseTexture_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuNoiseTexture (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.store.visibleStimulus='dots'; - -end - -% -------------------------------------------------------------------- -function OKMenuLineTexture_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuLineTexture (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% -------------------------------------------------------------------- -function OKMenuTargetInducer_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSpot (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'targetInducerStimulus') - o.store.targetInducerStimulus=targetInducerStimulus('name','Target-Inducer Stimulus'); - end - o.store.visibleStimulus = o.store.targetInducerStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuDots_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSpot (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'dotsStimulus') - o.store.dotsStimulus=dotsStimulus('name', 'Coherent Dots Stimulus',... - 'speed', 2, 'colour', [1 1 1]); - end - o.store.dotsStimulus.speed = 2; - o.store.visibleStimulus = o.store.dotsStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuNDots_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuNDots (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'ndotsStimulus') - o.store.ndotsStimulus=ndotsStimulus('name','Newsome Dots Stimulus',... - 'speed', 2, 'colour', [1 1 1]); - end - o.store.ndotsStimulus.speed = 2; - o.store.visibleStimulus = o.store.ndotsStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuBar_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuBar (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'barStimulus') - o.store.barStimulus=barStimulus('name','Bar Stimulus'); - end - o.store.visibleStimulus = o.store.barStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuGrating_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuGrating (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'gratingStimulus') - o.store.gratingStimulus=gratingStimulus('name','Grating Stimulus'); - end - o.store.visibleStimulus = o.store.gratingStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuGabor_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuGabor (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'gaborStimulus') - o.store.gaborStimulus=gaborStimulus('name','Gabor Stimulus'); - end - o.store.visibleStimulus = o.store.gaborStimulus; - set(handles.OKPanelStimulusText,'String','') - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); -end - -% -------------------------------------------------------------------- -function OKMenuSpot_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSpot (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'spotStimulus') - o.store.spotStimulus=spotStimulus('name','Spot Stimulus'); - end - o.store.visibleStimulus = o.store.spotStimulus; - set(handles.OKPanelStimulusText,'String','') - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); -end - -% -------------------------------------------------------------------- -function OKMenuDisc_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuDisc (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'discStimulus') - o.store.discStimulus=discStimulus('name','Disc Stimulus'); - end - o.store.visibleStimulus = o.store.discStimulus; - set(handles.OKPanelStimulusText,'String','') - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); -end - -% -------------------------------------------------------------------- -function OKMenuFixationCross_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSpot (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'fixationCrossStimulus') - o.store.fixationCrossStimulus=fixationCrossStimulus('name','Fixation Cross'); - end - o.store.visibleStimulus = o.store.fixationCrossStimulus; - set(handles.OKPanelStimulusText,'String','') - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); -end - -% -------------------------------------------------------------------- -function OKMenuTexture_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuTexture (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'textureStimulus') - o.store.textureStimulus=textureStimulus('name','Picture / Texture Stimulus'); - end - o.store.visibleStimulus = o.store.textureStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuMovie_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuTexture (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - if o.r.stimuli.n == 0;handles.OKStimList.String='Once you''ve edited this stimulus, click [add]';end - - if ~isfield(o.store,'movieStimulus') - o.store.movieStimulus=movieStimulus('name','Movie Stimulus'); - end - o.store.visibleStimulus = o.store.movieStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - - -% -------------------------------------------------------------------- -function OKMenuPreferences_Callback(hObject, eventdata, handles) - - -function OKProtocolsList_Callback(hObject, eventdata, handles) - - -function OKHistoryList_Callback(hObject, eventdata, handles) - - -% --- Executes on button press in OKProtocolLoad. -function OKProtocolLoad_Callback(hObject, eventdata, handles) -% hObject handle to OKProtocolLoad (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('loadProtocol'); -end - -% --- Executes on button press in OKProtocolSave. -function OKProtocolSave_Callback(hObject, eventdata, handles) -% hObject handle to OKProtocolSave (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('saveProtocol'); -end - -% --- Executes on button press in OKProtocolSave. -function OKSaveData_Callback(hObject, eventdata, handles) -% hObject handle to OKProtocolSave (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('saveData'); -end - -% --- Executes on button press in OKProtocolDuplicate. -function OKProtocolDuplicate_Callback(hObject, eventdata, handles) -% hObject handle to OKProtocolDuplicate (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('duplicateProtocol'); -end - -% --- Executes on button press in OKProtocolDelete. -function OKProtocolDelete_Callback(hObject, eventdata, handles) -% hObject handle to OKProtocolDelete (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('deleteProtocol'); -end - -% -------------------------------------------------------------------- -function OKMenuCalibrateLuminance_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuCalibrateLuminance (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v=get(handles.OKSelectScreen,'Value'); - inp = struct('screen',v-1); - if isa(o.r.screen.gammaTable,'calibrateLuminance') - o.r.screen.gammaTable.run; - else - c = calibrateLuminance(inp); - o.r.screen.gammaTable = c; - c.filename = [o.paths.calibration filesep 'Cal-' date '-' c.comments]; - o.saveCalibration; - end - set(handles.OKUseGamma,'Value',1) - o.r.screen.gammaTable.choice = 0; - set(handles.OKUseGamma,'String',['None'; 'Gamma'; o.r.screen.gammaTable.analysisMethods]); -end - -% -------------------------------------------------------------------- -function OKMenuLoadGamma_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuLoadGamma (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - uiopen('~/OptickaFiles/Calibration') - if exist('tmp','var') && isa(tmp,'calibrateLuminance') - o.r.screen.gammaTable = tmp; - clear tmp; - if get(handles.OKUseGamma,'Value') > length(['None'; 'Gamma'; o.r.screen.gammaTable.analysisMethods]) - set(handles.OKUseGamma,'Value',1); - o.r.screen.gammaTable.choice = 0; - else - o.r.screen.gammaTable.choice = get(handles.OKUseGamma,'Value')-1; - end - set(handles.OKUseGamma,'String',['None'; 'Gamma'; o.r.screen.gammaTable.analysisMethods]); - end -end - -% -------------------------------------------------------------------- -function OKMenuSaveGamma_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSaveGamma (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen.gammaTable,'calibrateLuminance') - tmp=o.r.screen.gammaTable; - uisave('tmp',[o.paths.calibration filesep 'Calibration-' date '-' o.r.screen.gammaTable.comments]); - end -end - -% -------------------------------------------------------------------- -function OKMenuClearGamma_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSaveGamma (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen.gammaTable,'calibrateLuminance') - o.r.screen.gammaTable = calibrateLuminance; - set(handles.OKUseGamma,'Value',1); - set(handles.OKUseGamma,'String','None'); - end -end - -% -------------------------------------------------------------------- -function OKMenuCalibrateSize_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuCalibrateSize (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -v=get(handles.OKSelectScreen,'Value'); -s=str2num(get(handles.OKMonitorDistance,'String')); -[~,dpc]=calibrateSize(v-1,s); -set(handles.OKpixelsPerCm,'String',num2str(dpc)); - - -function OKibTime_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -function OKRandomise_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end -% --- Executes on selection change in OKrandomGenerator. -function OKrandomGenerator_Callback(hObject, eventdata, handles) -% hObject handle to OKrandomGenerator (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -% Hints: contents = cellstr(get(hObject,'String')) returns OKrandomGenerator contents as cell array -% contents{get(hObject,'Value')} returns selected item from OKrandomGenerator -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -function OKRandomSeed_Callback(hObject, eventdata, handles) -% hObject handle to OKRandomSeed (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -% Hints: get(hObject,'String') returns contents of OKRandomSeed as text -% str2double(get(hObject,'String')) returns contents of OKRandomSeed as a double -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -% --- Executes on button press in OKHistoryUp. -function OKHistoryUp_Callback(hObject, eventdata, handles) -% hObject handle to OKHistoryUp (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% --- Executes on button press in OKHistoryDown. -function OKHistoryDown_Callback(hObject, eventdata, handles) -% hObject handle to OKHistoryDown (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% --- Executes on button press in OKHistoryDelete. -function OKHistoryDelete_Callback(hObject, eventdata, handles) -% hObject handle to OKHistoryDelete (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% --- Executes on selection change in OKVariableList. -function OKVariableList_Callback(hObject, eventdata, handles) -% hObject handle to OKVariableList (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: contents = cellstr(get(hObject,'String')) returns OKVariableList contents as cell array -% contents{get(hObject,'Value')} returns selected item from OKVariableList - -% --- Executes on button press in OKCopyVariableName. -function OKCopyVariableName_Callback(hObject, eventdata, handles) -% hObject handle to OKCopyVariableName (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -string = get(handles.OKVariableList,'String'); -value = get(handles.OKVariableList,'Value'); -string=string{value}; -set(handles.OKVariableName,'String',string); - -function OKCopyVariableNameValues_Callback(hObject, eventdata, handles) -string = get(handles.OKVariableList,'String'); -value = get(handles.OKVariableList,'Value'); -string=string{value}; -set(handles.OKVariableName,'String',string); - -switch string - case 'angle' - string = num2str(-90:45:90); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'direction' - string = num2str(-90:45:90); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'phase' - string = num2str(0:22.5:180); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'size' - string = num2str([0 0.1 0.2 0.35 0.5 0.75 1 2 4 6 8]); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'contrast' - string = num2str(0:0.1:1); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'sf' - string = num2str([0 0.1 0.5 0.7 1 1.5 2 3 4 5 6]); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'tf' - string = num2str([0.5 1 2 3 4 5]); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'xPosition' - string = num2str(-1:0.2:1); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'yPosition' - string = num2str(-1:0.2:1); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'xyPosition' - string = '{[-5 -5],[0 0], [5 5]}'; - set(handles.OKVariableValues,'String',string); - case 'colour' - string = '{[1 0 0],[0 1 0],[0 0 1]}'; - set(handles.OKVariableValues,'String',string); -end - - -% --- Executes on button press in OKVariablesLinear. -function OKVariablesLinear_Callback(hObject, eventdata, handles) -values = str2num(get(handles.OKVariableValues,'String')); -if length(values) == 3 - values=linspace(values(1),values(2),values(3)); - string = num2str(values); -else - string = num2str(values); - string = regexprep(string,'\s+',' '); %collapse spaces -end -set(handles.OKVariableValues,'String',string); - -% --- Executes on button press in OKVariablesLog. -function OKVariablesLog_Callback(hObject, eventdata, handles) %#ok<*INUSD> -values = str2num(get(handles.OKVariableValues,'String')); -if length(values) == 3 - values=logspace(log10(values(1)),log10(values(2)),values(3)); - string = num2str(values); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); -else - warndlg('Please enter a minimum, maximum and number of values'); -end - -% --- Executes on button press in OKAddStimulus. -function OKAddStimulus_Callback(hObject, eventdata, handles) -% hObject handle to OKAddStimulus (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.readPanel(); - o.r.stimuli{o.r.stimuli.n+1} = o.store.visibleStimulus.clone(); - %o.r.stimuli{o.r.stimuli.n}.name = 'Stimulus'; - o.addStimulus; - if o.r.stimuli.n > 0 - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKDeleteStimulus,'Enable','on'); - set(handles.OKrefreshStimulusList,'Enable','on'); - set(handles.OKCopyStimulus,'Enable','on'); - set(handles.OKStimulusUp,'Enable','on'); - set(handles.OKStimulusDown,'Enable','on'); - set(handles.OKStimulusRun,'Enable','on'); - set(handles.OKStimulusRunBenchmark,'Enable','on'); - set(handles.OKStimulusRunAll,'Enable','on'); - set(handles.OKStimulusRunAllBenchmark,'Enable','on'); - set(handles.OKStimulusRunSingle,'Enable','on'); - end - end -end - -% --- Executes on button press in OKDeleteStimulus. -function OKDeleteStimulus_Callback(hObject, eventdata, handles) -% hObject handle to OKDeleteStimulus (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.deleteStimulus; - if o.r.stimuli.n == 0 - set(handles.OKAddStimulus,'Enable','off'); - set(handles.OKDeleteStimulus,'Enable','off'); - set(handles.OKrefreshStimulusList,'Enable','off'); - set(handles.OKCopyStimulus,'Enable','off'); - set(handles.OKStimulusUp,'Enable','off'); - set(handles.OKStimulusDown,'Enable','off'); - set(handles.OKStimulusRun,'Enable','off'); - set(handles.OKStimulusRunBenchmark,'Enable','off'); - set(handles.OKStimulusRunAll,'Enable','off'); - set(handles.OKStimulusRunAllBenchmark,'Enable','off'); - set(handles.OKStimulusRunSingle,'Enable','off'); - end -end - -% --- Executes on button press in OKCopyStimulus. -function OKCopyStimulus_Callback(hObject, eventdata, handles) -% hObject handle to OKCopyStimulus (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKStimList,'Value'); - o.r.stimuli{o.r.stimuli.n+1} = o.r.stimuli{v}.clone; - o.r.stimuli{o.r.stimuli.n}.name = ['Stimulus #' num2str(o.r.stimuli.n)]; - o.addStimulus; -end - -% --- Executes on selection change in OKStimList. -function OKStimList_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.editStimulus; -end - -% --- Executes on button press in OKStimulusDown. -function OKStimulusDown_Callback(hObject, eventdata, handles) %#ok<*INUSL> -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - value = get(handles.OKStimList,'Value'); %where are we in the stimulus list - slength = o.r.stimuli.n; - if value < slength - idx = 1:slength; - idx2 = idx; - idx2(value) = idx2(value)+1; - idx2(value+1) = idx2(value+1)-1; - o.r.stimuli(idx) = o.r.stimuli(idx2); - set(handles.OKStimList,'Value',value+1); - o.refreshStimulusList; - end -end - -% --- Executes on button press in OKStimulusUp. -function OKStimulusUp_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - value = get(handles.OKStimList,'Value'); %where are we in the stimulus list - slength = o.r.stimuli.n; - if value > 1 - idx = 1:slength; - idx2 = idx; - idx2(value) = idx2(value)-1; - idx2(value-1) = idx2(value-1)+1; - o.r.stimuli(idx) = o.r.stimuli(idx2); - set(handles.OKStimList,'Value',value-1); - o.refreshStimulusList; - end -end - -% --- Executes on button press in OKStimulusRun. -function OKStimulusRun_Callback(hObject, eventdata, handles) -% hObject handle to OKStimulusRun (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKStimList,'Value'); - if v > 0 && isobject(o.r.stimuli{v}) - trialTime = str2num(get(handles.OKtrialTime,'String')); - if IsWin - forceScreen = 1; - else - forceScreen = 0; - end - if isa(o.r.screen,'screenManager') && ~isempty(o.r.screen) - run(o.r.stimuli{v}, false, trialTime, o.r.screen, forceScreen); - else - run(o.r.stimuli{v}, false, trialTime, [], forceScreen); - end - end - set(handles.OKStimulusRunAll,'Enable','on'); -end - -% --- Executes on button press in OKStimulusRunBenchmark. -function OKStimulusRunBenchmark_Callback(hObject, eventdata, handles) -% hObject handle to OKStimulusRunBenchmark (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKStimList,'Value'); - if v > 0 && isa(o.r.stimuli{v},'baseStimulus') - if isa(o.r.screen,'screenManager') - run(o.r.stimuli{v}, true, 5, o.r.screen); - else - run(o.r.stimuli{v}, true, 5); - end - end -end - -% --- Executes on button press in OKStimulusRunAll. -function OKStimulusRunAll_Callback(hObject, eventdata, handles) -% hObject handle to OKStimulusRunAll (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.stimuli,'metaStimulus') && o.r.stimuli.n > 0 - trialTime = str2num(get(handles.OKtrialTime,'String')); - o.r.stimuli.choice = []; - if isa(o.r.screen,'screenManager') && ~isempty(o.r.screen) - run(o.r.stimuli, false, trialTime, o.r.screen); - else - run(o.r.stimuli, false, trialTime, []); - end - end -end - -% --- Executes on button press in OKStimulusRunAllBenchmark. -function OKStimulusRunAllBenchmark_Callback(hObject, eventdata, handles) -% hObject handle to OKStimulusRunAllBenchmark (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.stimuli,'metaStimulus') && o.r.stimuli.n > 0 - o.r.stimuli.choice = []; - if isa(o.r.screen,'screenManager') - run(o.r.stimuli, true, 5, o.r.screen); - else - run(o.r.stimuli, true, 5); - end - end -end - -% --- Executes on button press in OKStimulusRunSingle. -function OKStimulusRunSingle_Callback(hObject, eventdata, handles) -% hObject handle to OKStimulusRunSingle (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.stimuli,'metaStimulus') && o.r.stimuli.n > 0 - o.r.stimuli.choice = []; - if isa(o.r.screen,'screenManager') - runSingle(o.r.stimuli, o.r.screen); - end - end -end - - -% --- Executes on button press in OKInspectStimulus. -function OKInspectStimulus_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKStimList,'Value'); - if v > 0 - uiinspect(o.r.stimuli{v}); - end -end - -% --- Executes on button press in OKInspectVariable. -function OKInspectVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKVarList,'Value'); - if v > 0 - uiinspect(o.r.task.nVar(v)); - end -end - -% --- Executes on button press in OKInspectVariable. -function OKShuffleVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKVarList,'Value'); - if v > 0 - oldv = o.r.task.verbose; - o.r.task.verbose = true; - o.r.task.randomiseTask(); - o.r.task.verbose = oldv; - end -end - - -% --- Executes on button press in OKrefreshStimulusList. -function OKrefreshStimulusList_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKStimList,'Value'); - if v > 0 - seditor(o.r.stimuli{v}, handles.output); - end - -end - -% --- Executes on button press in OKAddVariable. -function OKAddVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.addVariable; - if o.r.task.nVars > 0 - set(handles.OKDeleteVariable,'Enable','on'); - set(handles.OKCopyVariable,'Enable','on'); - set(handles.OKEditVariable,'Enable','on'); - end -end - -% --- Executes on button press in OKDeleteVariable. -function OKDeleteVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.deleteVariable; - if o.r.task.nVars < 1 - set(handles.OKDeleteVariable,'Enable','off'); - set(handles.OKCopyVariable,'Enable','off'); - set(handles.OKEditVariable,'Enable','off'); - end -end - -function OKCopyVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.copyVariable; -end - -function OKEditVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.editVariable; -end - -function OKVariableOffset_Callback(hObject, eventdata, handles) -% hObject handle to OKVariableOffset (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKVariableOffset as text -% str2double(get(hObject,'String')) returns contents of OKVariableOffset -% as a double - -% -------------------------------------------------------------------- -function OKToolbarRun_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarRun (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if o.r.isRunning == true;return;end - if ~isempty(o.oc) && o.oc.isOpen == 1 && o.r.useLabJack == 1 - o.oc.write('--GO!--'); - pause(0.5); - end - drawnow; - o.r.uiCommand='run'; - o.r.runMOC; -end - -% -------------------------------------------------------------------- -function OKToolbarStop_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarStop (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.uiCommand='stop'; - drawnow; -end - -% -------------------------------------------------------------------- -function OKToolbarAbort_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarAbort (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.uiCommand='abort'; - if isa(o.oc,'dataConnection') - o.oc.write('--abort--'); - end - drawnow; -end - -function OKRemoteIP_Callback(hObject, eventdata, handles) -% hObject handle to OKRemoteIP (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKRemoteIP as text -% str2double(get(hObject,'String')) returns contents of OKRemoteIP as a double -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - - -function OKRemotePort_Callback(hObject, eventdata, handles) -% hObject handle to OKRemotePort (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKRemotePort as text -% str2double(get(hObject,'String')) returns contents of OKRemotePort as a double -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -% -------------------------------------------------------------------- -function OKToolbarToggleRemote_OnCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarToggleRemote (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - eval(o.store.serverCommand) -end - - -% -------------------------------------------------------------------- -function OKMenumanageCode_Callback(hObject, eventdata, handles) -% hObject handle to OKMenumanageCode (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -manageCode - - -% -------------------------------------------------------------------- -function OKMenurfMapperLog_Callback(hObject, eventdata, handles) -% hObject handle to OKMenurfMapperLog (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -function OKSettingsmovieSize_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.screen.movieSettings.size=str2num(get(hObject,'String')); -end - -function OKSettingsmovieFrames_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.screen.movieSettings.nFrames=str2num(get(hObject,'String')); %#ok<*ST2NM> -end - -function OKSettingsmoviePrecision_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.screen.movieSettings.quality=get(hObject,'Value'); -end - -function OKSettingsmovieType_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.screen.movieSettings.type=get(hObject,'Value'); -end - -function OKSettingsmovieCodec_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.screen.movieSettings.codec=get(hObject,'String'); -end - -% -------------------------------------------------------------------- -function OKPanelTellOmniplex_ClickedCallback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - resp = questdlg('Is opxOnline running on the Omniplex machine?','Check OPX','No'); - if strcmpi(resp,'Yes') - o.connectToOmniplex - end -end - -% -------------------------------------------------------------------- -function OKPanelReconnectOmniplex_ClickedCallback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.oc,'dataConnection') - o.oc.closeAll; - o.connectToOmniplex; - end -end - -% -------------------------------------------------------------------- -function OKPanelSendOmniplex_ClickedCallback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.oc,'dataConnection') - o.sendOmniplexStimulus; - end -end - -% -------------------------------------------------------------------- -function OKPanelDisconnectOmniplex_ClickedCallback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.oc,'dataConnection') - o.disconnectOmniplex; - end -end - -% -------------------------------------------------------------------- -function OKPanelPingOmniplex_ClickedCallback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - rAddress = get(handles.OKOmniplexIP,'String'); - status = o.ping(rAddress); - if status > 0 - set(o.h.OKOmniplexStatus,'String','Omniplex: machine ping ERROR!') - else - if isa(o.oc,'dataConnection') && o.oc.isOpen == 1 - o.oc.write('--ping--'); - loop = 1; - while loop < 8 - in = o.oc.read(0); - fprintf('\n{opticka said: %s}\n',in) - if regexpi(in,'ping') - fprintf('\nWe can ping omniplex master on try: %d\n',loop) - set(handles.OKOmniplexStatus,'String','Omniplex: connected and pinged') - break - else - fprintf('\nOmniplex master not responding, try: %d\n',loop) - set(handles.OKOmniplexStatus,'String','Omniplex: not responding') - end - loop=loop+1; - pause(0.1); - end - end - end -end - - -function OKverbosityLevel_Callback(hObject, eventdata, handles) -% hObject handle to OKverbosityLevel (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKverbosityLevel as text -% str2double(get(hObject,'String')) returns contents of OKverbosityLevel as a double -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen,'screenManager') - o.r.screen.verbosityLevel = str2double(get(hObject,'String')); - end -end - - -% --- Executes on button press in OKbenchmark. -function OKbenchmark_Callback(hObject, eventdata, handles) -% hObject handle to OKbenchmark (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -% Hint: get(hObject,'Value') returns toggle state of OKbenchmark -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen,'screenManager') - if get(hObject,'Value') == 1 - set(handles.OKlogFrames,'Value',0) - end - o.getScreenVals; - end -end - -% --- Executes on button press in OKOmniplexEnable. -function OKOmniplexEnable_Callback(hObject, eventdata, handles) -% hObject handle to OKOmniplexEnable (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -% Hint: get(hObject,'Value') returns toggle state of OKOmniplexEnable -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if get(hObject,'Value') < 1 - set(handles.OKOmniplexIP,'Enable','off'); - set(handles.OKOmniplexPort,'Enable','off'); - set(handles.OKToolbarTellOmniplex,'Enable','off'); - set(handles.OKPanelReconnectOmniplex,'Enable','off'); - set(handles.OKPanelSendOmniplex,'Enable','off'); - set(handles.OKPanelPingOmniplex,'Enable','off'); - set(handles.OKPanelDisconnectOmniplex,'Enable','off'); - set(handles.OKOmniplexStatus,'Enable','off'); - else - set(handles.OKOmniplexIP,'Enable','on'); - set(handles.OKOmniplexPort,'Enable','on'); - set(handles.OKToolbarTellOmniplex,'Enable','on'); - set(handles.OKPanelReconnectOmniplex,'Enable','on'); - set(handles.OKPanelSendOmniplex,'Enable','on'); - set(handles.OKPanelPingOmniplex,'Enable','on'); - set(handles.OKPanelDisconnectOmniplex,'Enable','on'); - set(handles.OKOmniplexStatus,'Enable','on'); - end -end - -% -------------------------------------------------------------------- -function OKMenuOpen_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuOpen (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('loadProtocol',false); -end - -% -------------------------------------------------------------------- -function OKMenuSave_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSave (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('saveProtocol'); -end - -% -------------------------------------------------------------------- -function OKMenuQuit_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuQuit (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if ~isempty(handles) && isappdata(handles.output,'o') - try - o = getappdata(handles.output,'o'); - o.savePrefs; - rmappdata(handles.output,'o'); - clear o; - catch ME - getReport(ME) - fprintf('!!!>>> Opticka failed to clear all data on close...\n'); - end -end -try; warning off; close(gcf); warning on; end - -% -------------------------------------------------------------------- -function OKMenuNewProtocol_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuNewProtocol (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r = []; - o.store.evnt = []; - o.store = rmfield(o.store,'evnt'); - o.store.visibleStimulus=[]; - o.store = rmfield(o.store,'visibleStimulus'); - o.clearStimulusList; - o.clearVariableList; - o.getScreenVals; - o.getTaskVals; -end - -% -------------------------------------------------------------------- -function OKToolbarInitialise_ClickedCallback(hObject, eventdata, handles) %#ok<*DEFNU> -% hObject handle to OKToolbarInitialise (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r = []; - o.store.evnt = []; - o.store = rmfield(o.store,'evnt'); - o.store.visibleStimulus=[]; - o.store = rmfield(o.store,'visibleStimulus'); - o.clearStimulusList; - o.clearVariableList; - o.getScreenVals; - o.getTaskVals; -end - -% -------------------------------------------------------------------- -function OKToolbarOpen_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarOpen (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('loadProtocol','1'); -end - -% -------------------------------------------------------------------- -function OKToolbarDefinePath_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarDefinePath (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - path = uigetdir; - if ischar(path) && exist(path,'dir') - initialiseSave(o, path); - if isa(o.r,'runExperiment'); - initialiseSave(o.r, path); - end - end -end - - -function OKTrainingName_Callback(hObject, eventdata, handles) -% hObject handle to OKTrainingName (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKTrainingName as text -% str2double(get(hObject,'String')) returns contents of OKTrainingName as a double -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.subjectName = hObject.String; -end - -% --- Executes during object creation, after setting all properties. -function OKTrainingName_CreateFcn(hObject, eventdata, handles) -% hObject handle to OKTrainingName (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles empty - handles not created until after all CreateFcns called - -% Hint: edit controls usually have a white background on Windows. -% See ISPC and COMPUTER. -if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) - set(hObject,'BackgroundColor','white'); -end - - -% -------------------------------------------------------------------- -function OKToggleUI_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToggleUI (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if strcmp(get(handles.OKPanelGlobal,'Visible'),'on') - - set(handles.OKPanelGlobal,'Visible','off'); - set(handles.OKPanelProtocols,'Visible','on'); - set(handles.OKPanelTraining,'Visible','off'); - -elseif strcmp(get(handles.OKPanelProtocols,'Visible'),'on') - - set(handles.OKPanelProtocols,'Visible','off') - set(handles.OKPanelGlobal,'Visible','off') - set(handles.OKPanelTraining,'Visible','on') - if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getStateInfo(); - end - -else - - set(handles.OKPanelProtocols,'Visible','off') - set(handles.OKPanelGlobal,'Visible','on') - set(handles.OKPanelTraining,'Visible','off') - -end - -% --- Executes on button press in OKEditStateFileButon. -function OKEditStateFileButon_Callback(hObject, eventdata, handles) -% hObject handle to OKEditStateFileButon (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if exist(o.r.paths.stateInfoFile,'file') - edit(o.r.paths.stateInfoFile); - end - o.getStateInfo(); -end - -function OKTrainingResearcherName_Callback(hObject, eventdata, handles) -% hObject handle to OKTrainingResearcherName (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKTrainingResearcherName as text -% str2double(get(hObject,'String')) returns contents of OKTrainingResearcherName as a double -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.researcherName = hObject.String; -end -% -------------------------------------------------------------------- -function OKMenuStateInfo_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuStateInfo (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('LoadStateInfo'); - o.getStateInfo(); -end - -% --- Executes on button press in OKLoadStateButton. -function OKLoadStateButton_Callback(hObject, eventdata, handles) -% hObject handle to OKLoadStateButton (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('LoadStateInfo'); - o.getStateInfo(); -end - -% --- Executes on button press in OKRefreshStateInfo. -function OKRefreshStateInfo_Callback(hObject, eventdata, handles) -% hObject handle to OKRefreshStateInfo (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getStateInfo(); -end - -% -------------------------------------------------------------------- -function OKRFMapper_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKRFMapper (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.store.rfLog = []; - rf=rfMapper; - rf.run(o.r); - o.store.rfLog = rf; - clear rf; -end - -% -------------------------------------------------------------------- -function OKRunRFMapper_Callback(hObject, eventdata, handles) -% hObject handle to OKRunRFMapper (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.store.rfLog = []; - rf=rfMapper; - rf.run(o.r); - o.store.rfLog = rf; - clear rf; -end - -% -------------------------------------------------------------------- -function OKRunTrainingSession_Callback(hObject, eventdata, handles) -% hObject handle to OKRunTrainingSession (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKRunExperiment_Callback(hObject, eventdata, handles) -% hObject handle to OKRunExperiment (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% -------------------------------------------------------------------- -function OKStartTask_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKStartTask (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if o.r.checkScreenError() - fprintf('>>> A previous task did not finish properly, resetting!\n') - end - if isa(o.r,'runExperiment') && ~o.r.isRunning - o.getScreenVals; - %o.r.screenSettings.optickahandle = handles.output; - initialiseSaveFile(o.r, o.paths.savedData) - if ~isempty(regexp(o.comment, '^Protocol','once')) - o.r.comment = o.comment; - end - runTask(o.r); - end -end - - -% -------------------------------------------------------------------- -function OKMenuIO_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuIO (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% --- Creates and returns a handle to the GUI figure. -function h1 = opticka_ui_LayoutFcn(policy) -% policy - create a new figure or use a singleton. 'new' or 'reuse'. - -persistent hsingleton; -if strcmpi(policy, 'reuse') & ishandle(hsingleton) - h1 = hsingleton; - return; -end -load opticka_ui.mat - -if ismac - nfont = 'Source Sans 3'; - mfont = 'menlo'; -elseif ispc - nfont = 'Calibri'; - mfont = 'Consolas'; -else %linux - if system('fc-list -q "Source Sans" family') - nfont = 'Source Sans 3'; - else - nfont = 'Liberation sans'; %get(0,'defaultAxesFontName'); - end - if system('fc-list -q "Fira Code" family') == 0 - mfont = 'Fira Code'; - else - mfont = 'Liberation Mono'; - end -end -ssize = 9; -msize = 11; -lsize = 12; -bcolour = [0.9 0.9 0.9]; - - -appdata.GUIDEOptions = struct(... - 'active_h', [], ... - 'taginfo', struct(... - 'figure', 2, ... - 'text', 217, ... - 'uipanel', 54, ... - 'listbox', 6, ... - 'edit', 181, ... - 'pushbutton', 59, ... - 'popupmenu', 20, ... - 'radiobutton', 4, ... - 'checkbox', 44, ... - 'axes', 6, ... - 'togglebutton', 4, ... - 'uitoolbar', 2, ... - 'uitoggletool', 3, ... - 'uipushtool', 20), ... - 'override', 1, ... - 'release', 13, ... - 'resize', 'simple', ... - 'accessibility', 'callback', ... - 'mfile', 1, ... - 'callbacks', 1, ... - 'singleton', 1, ... - 'syscolorfig', 0, ... - 'blocking', 0, ... - 'lastFilename', '/Users/ian/Code/opticka/ui/opticka_ui.fig', ... - 'lastSavedFile', '/Users/ian/Code/opticka/ui/opticka_ui.m'); -appdata.SavedVisible = 'on'; -appdata.GUIDELayoutEditor = []; -appdata.initTags = struct(... - 'handle', [], ... - 'tag', 'OKRoot'); - -h1 = figure(... -'PaperUnits','centimeters',... -'Units',get(0,'defaultfigureUnits'),... -'Position',[100 100 1400 750],... -'Visible','on',... -'Color',bcolour,... -'IntegerHandle','off',... -'DoubleBuffer','off',... -'MenuBar','none',... -'ToolBar','none',... -'Name','Opticka',... -'NumberTitle','off',... -'DockControls','off',... -'PaperPosition',[0 0 29.7 21.0],... -'PaperSize',[29.7 21.0],... -'PaperType','a4',... -'InvertHardcopy',get(0,'defaultfigureInvertHardcopy'),... -'PaperOrientation','landscape',... -'HandleVisibility','callback',... -'CloseRequestFcn',@(hObject,eventdata)opticka_ui('OKMenuQuit_Callback',hObject,eventdata,guidata(hObject)),... -'Tag','OKRoot',... -'UserData',[]); - - -h146 = uibuttongroup(... -'Parent',h1,... -'FontUnits','pixels',... -'Units','normalized',... -'ForegroundColor',[0.3 0.3 0.3],... -'TitlePosition','lefttop',... -'Title','Stimuli & Variables',... -'Position',[0 0 1 0.72],... -'BackgroundColor',bcolour,... -'Clipping','off',... -'Tag','OKPanelIndividual',... -'FontSize',lsize,... -'FontName',nfont,... -'FontWeight','bold'); - -if verLessThan('matlab','8.4');tp='top';else;tp='right';end -tabgp = uitabgroup(h1,'Position',[0 0.72 1 0.28],'TabLocation',tp,'Tag','OKtabgp'); -tab1 = uitab(tabgp,'Title','Display','ForegroundColor',[0.5 0.3 0.3],'Tag','OKtab1','Tooltip','Change Display and main settings...'); -tab2 = uitab(tabgp,'Title','Task','ForegroundColor',[0.5 0.3 0.3],'Tag','OKtab2','Tooltip','View/Change Behavioural Task settings...'); -tab3 = uitab(tabgp,'Title','Options','ForegroundColor',[0.5 0.3 0.3],'Tag','OKtab3','Tooltip','Load protocols, edit additional settings...'); -%tab4 = uitab(tabgp,'Title','Settings','Tag','OKtab4'); - -h2 = uimenu(... -'Parent',h1,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuFile_Callback',hObject,eventdata,guidata(hObject)),... -'Label','File',... -'Tag','OKMenuFile'); - -h3 = uimenu(... -'Parent',h2,... -'Accelerator','N',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuNewProtocol_Callback',hObject,eventdata,guidata(hObject)),... -'Label','New Protocol',... -'Tag','OKMenuNewProtocol'); - -h4 = uimenu(... -'Parent',h2,... -'Accelerator','O',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuOpen_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Open Protocol...',... -'Tag','OKMenuOpen'); - -h5 = uimenu(... -'Parent',h2,... -'Accelerator','V',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuSave_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Save as Protocol...',... -'Tag','OKMenuSave'); - -h5b = uimenu(... -'Parent',h2,... -'Accelerator','S',... -'Separator','on',... -'Callback',@(hObject,eventdata)opticka_ui('OKSaveData_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Save as Data...',... -'Tag','OKMenuSave'); - -h6 = uimenu(... -'Parent',h2,... -'Separator','on',... -'Accelerator','I',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuStateInfo_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Open stateMachine info...',... -'Tag','OKMenuStateInfo'); - -h7 = uimenu(... -'Parent',h2,... -'Separator','on',... -'Accelerator','X',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuQuit_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Quit Opticka',... -'Tag','OKMenuQuit'); - -h8 = uimenu(... -'Parent',h1,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuEdit_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Edit',... -'Tag','OKMenuEdit'); - -h9 = uimenu(... -'Parent',h8,... -'Enable','off',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuCut_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Cut',... -'Tag','OKMenuCut'); - -h10 = uimenu(... -'Parent',h8,... -'Enable','off',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuCopy_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Copy',... -'Tag','OKMenuCopy'); - -h11 = uimenu(... -'Parent',h8,... -'Enable','off',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuPaste_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Paste',... -'Tag','OKMenuPaste'); - -h12 = uimenu(... -'Parent',h1,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuTools_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Stimulus',... -'Tag','OKMenuTools'); - -h13 = uimenu(... -'Parent',h12,... -'Accelerator','G',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuGrating_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Grating',... -'Tag','OKMenuGrating'); - -h14 = uimenu(... -'Parent',h12,... -'Accelerator','R',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuGabor_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Gabor',... -'Tag','OKMenuGabor'); - -h15 = uimenu(... -'Parent',h12,... -'Accelerator','B',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuBar_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Bar',... -'Tag','OKMenuBar'); - -h16 = uimenu(... -'Parent',h12,... -'Accelerator','D',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuDots_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Coherent Dots',... -'Tag','OKMenuDots'); - -h17 = uimenu(... -'Parent',h12,... -'Accelerator','Q',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuNDots_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Newsome Dots',... -'Tag','OKMenuNDots'); - -h18a = uimenu(... -'Parent',h12,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuFixationCross_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Fixation Cross',... -'Tag','OKMenuSpot'); - -h18 = uimenu(... -'Parent',h12,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuSpot_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Spot',... -'Tag','OKMenuSpot'); - -h18b = uimenu(... -'Parent',h12,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuDisc_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Smoothed Disc',... -'Tag','OKMenuDisc'); - -h19 = uimenu(... -'Parent',h12,... -'Accelerator','T',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuTexture_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Picture',... -'Tag','OKMenuTexture'); - -h19b = uimenu(... -'Parent',h12,... -'Accelerator','K',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuMovie_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Movie',... -'Tag','OKMenuMovie'); - -h20 = uimenu(... -'Parent',h12,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuTargetInducer_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Target Inducer',... -'Tag','OKMenuTargetInducer'); - -h21 = uimenu(... -'Parent',h12,... -'Enable','off',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuPlaid_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Plaid',... -'Separator','on',... -'Tag','OKMenuPlaid'); - -h22 = uimenu(... -'Parent',h12,... -'Enable','off',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuNoiseTexture_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Noise Texture',... -'Tag','OKMenuNoiseTexture'); - -h23 = uimenu(... -'Parent',h12,... -'Enable','off',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuLineTexture_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Line Texture',... -'Tag','OKMenuLineTexture'); - -h24 = uimenu(... -'Parent',h12,... -'Enable','off',... -'Separator','on',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuRevCorr_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Reverse Correlation',... -'Tag','OKMenuRevCorr'); - -h25a = uimenu(... -'Parent',h1,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuTools_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Run',... -'Tag','OKMenuRun'); - -h26 = uimenu(... -'Parent',h25a,... -'Accelerator','1',... -'Callback',@(hObject,eventdata)opticka_ui('OKStartTask_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Label','Run Behavioural task',... -'Tag','OKRunTrainingSession'); - -h27 = uimenu(... -'Parent',h25a,... -'Accelerator','2',... -'Callback',@(hObject,eventdata)opticka_ui('OKToolbarRun_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Label','Run MOC Task',... -'Tag','OKRunExperiment'); - -h25 = uimenu(... -'Parent',h25a,... -'Separator','on',... -'Accelerator','3',... -'Callback',@(hObject,eventdata)opticka_ui('OKRunRFMapper_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Run RF Mapper',... -'Tag','OKRunRFMapper'); %#ok<*NASGU> - -h57 = uimenu(... -'Parent',h1,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuIO_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Input Output',... -'Tag','OKMenuIO'); - -h58 = uimenu(... -'Parent',h57,... -'Callback',@(hObject,eventdata)opticka_ui('OKuseEyeLink_Callback',hObject,eventdata,guidata(hObject)),... -'Checked','on',... -'Label','Use Eyelink eyetracker',... -'Tag','OKuseEyelink'); - -h58b = uimenu(... -'Parent',h57,... -'Callback',@(hObject,eventdata)opticka_ui('OKuseTobii_Callback',hObject,eventdata,guidata(hObject)),... -'Checked','off',... -'Label','Use Tobii eyetracker',... -'Tag','OKuseTobii'); - -h59 = uimenu(... -'Parent',h57,... -'Separator','on',... -'Callback',@(hObject,eventdata)opticka_ui('OKuseDataPixx_Callback',hObject,eventdata,guidata(hObject)),... -'Checked','off',... -'Label','Enable DataPixx for Strobe',... -'Tag','OKuseDataPixx'); - -h59b = uimenu(... -'Parent',h57,... -'Callback',@(hObject,eventdata)opticka_ui('OKuseDisplayPP_Callback',hObject,eventdata,guidata(hObject)),... -'Checked','off',... -'Label','Enable Display++ for Strobe',... -'Tag','OKuseDisplayPP'); - -h60 = uimenu(... -'Parent',h57,... -'Callback',@(hObject,eventdata)opticka_ui('OKuseLabJackTStrobe_Callback',hObject,eventdata,guidata(hObject)),... -'Checked','off',... -'Label','Enable LabJack T4 for Strobe',... -'Tag','OKuseLabJackTStrobe'); - -h60d = uimenu(... -'Parent',h57,... -'Callback',@(hObject,eventdata)opticka_ui('OKuseLabJackStrobe_Callback',hObject,eventdata,guidata(hObject)),... -'Checked','off',... -'Label','Enable LabJack U3/U6 for Strobe',... -'Tag','OKuseLabJackStrobe'); - -h60a = uimenu(... -'Parent',h57,... -'Callback',@(hObject,eventdata)opticka_ui('OKuseLabJackReward_Callback',hObject,eventdata,guidata(hObject)),... -'Checked','off',... -'Separator','on',... -'Label','Use LabJack for Reward',... -'Tag','OKuseLabJackReward'); - -h60b = uimenu(... -'Parent',h57,... -'Callback',@(hObject,eventdata)opticka_ui('OKuseArduino_Callback',hObject,eventdata,guidata(hObject)),... -'Checked','off',... -'Label','Use Arduino for Reward',... -'Tag','OKuseArduino'); - -h60c = uimenu(... -'Parent',h57,... -'Callback',@(hObject,eventdata)opticka_ui('OKuseEyeOccluder_Callback',hObject,eventdata,guidata(hObject)),... -'Checked','off',... -'Separator','on',... -'Label','Use Eye Occluder',... -'Tag','OKuseEyeOccluder'); - -h28 = uibuttongroup(... -'Parent',tab3,... -'FontUnits','pixels',... -'Units','normalized',... -'ForegroundColor',[0.3 0.3 0.3],... -'BorderType','none',... -'Title','',... -'Position',[0 0 1 1],... -'Visible','on',... -'BackgroundColor',bcolour,... -'Clipping','off',... -'Tag','OKPanelProtocols',... -'FontSize',lsize,... -'FontName',nfont,... -'FontWeight','bold'); - -h30 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Protocols',... -'Style','text',... -'Position',[0 0.88 0.06 0.12],... -'BackgroundColor',bcolour,... -'Children',[],... -'ForegroundColor',[0.3 0.3 0.3],... -'Tag','text96',... -'FontSize',lsize,... -'FontName',nfont,... -'FontWeight','bold'); - -h31 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','History',... -'Style','text',... -'Position',[0.287 0.88 0.05 0.12],... -'BackgroundColor',bcolour,... -'Children',[],... -'ForegroundColor',[0.3 0.3 0.3],... -'Enable','off',... -'Tag','text97',... -'FontSize',lsize,... -'FontName',nfont,... -'FontWeight','bold'); - -h32 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Load Protocol',... -'Position',[0 0.7 0.08 0.14],... -'Callback',@(hObject,eventdata)opticka_ui('OKProtocolLoad_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKProtocolLoad',... -'FontSize',msize,... -'FontName',nfont); - -h33 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Save As...',... -'Position',[0 0.55 0.08 0.14],... -'Callback',@(hObject,eventdata)opticka_ui('OKProtocolSave_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKProtocolSave',... -'FontSize',msize,... -'FontName',nfont); - -h34 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Duplicate Protocol',... -'Position',[0 0.4 0.08 0.14],... -'Callback',@(hObject,eventdata)opticka_ui('OKProtocolDuplicate_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKProtocolDuplicate',... -'FontSize',msize,... -'FontName',nfont); - -h35 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Delete...',... -'Position',[0 0.25 0.08 0.14],... -'Callback',@(hObject,eventdata)opticka_ui('OKProtocolDelete_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKProtocolDelete',... -'FontSize',msize,... -'FontName',nfont); - -h36 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Move Up',... -'Position',[0.287 0.7 0.08 0.14],... -'Callback',@(hObject,eventdata)opticka_ui('OKHistoryUp_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKHistoryUp',... -'FontSize',ssize,... -'FontName',nfont); - -h37 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Move Down',... -'Position',[0.287 0.55 0.08 0.14],... -'Callback',@(hObject,eventdata)opticka_ui('OKHistoryDown_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKHistoryDown',... -'FontSize',ssize,... -'FontName',nfont); - -h38 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Delete',... -'Position',[0.287 0.4 0.08 0.14],... -'Callback',@(hObject,eventdata)opticka_ui('OKHistoryDelete_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKHistoryDelete',... -'FontSize',ssize,... -'FontName',nfont); - -h39 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String',{'Patch Tuning'; 'Plaid'; 'Reverse Correlation' },... -'Style','listbox',... -'Value',1,... -'Position',[0.09 0.05 0.19 0.95],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKProtocolsList_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKProtocolsList',... -'FontSize',msize,... -'FontName',nfont); - -h29 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String',{ 'Listbox' },... -'Style','listbox',... -'Value',1,... -'Position',[0.38 0.05 0.19 0.95],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKHistoryList_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKHistoryList',... -'FontSize',msize,... -'FontName',nfont); - -h40 = uipanel(... -'Parent',h28,... -'FontUnits','pixels',... -'Title',blanks(0),... -'Position',[0.284 0 0.005 1],... -'Clipping','off',... -'Tag','uipanel35',... -'FontSize',ssize,... -'FontName',nfont ); - -h41 = uipanel(... -'Parent',h28,... -'FontUnits','pixels',... -'Title',blanks(0),... -'Position',[0.574 0 0.005 1],... -'Clipping','off',... -'Tag','uipanel41',... -'FontSize',ssize,... -'FontName',nfont); - - -h42 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Settings',... -'Style','text',... -'Position',[0.58 0.88 0.05 0.12],... -'BackgroundColor',bcolour,... -'Children',[],... -'ForegroundColor',[0.3 0.3 0.3],... -'Tag','text181',... -'FontSize',lsize,... -'FontName',nfont,... -'FontWeight','bold'); - -h43 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','400 400',... -'Style','edit',... -'Position',[0.58 0.76 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKSettingsmovieSize_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKSettingsmovieSize',... -'UserData',[],... -'FontSize',msize,... -'FontName',mfont); - -h44 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','100',... -'Style','edit',... -'Position',[0.58 0.6 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKSettingsmovieFrames_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKSettingsmovieFrames',... -'UserData',[],... -'FontSize',msize,... -'FontName',mfont); - -h45 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Movie High Precision',... -'Style','checkbox',... -'Position',[0.58 0.28 0.1 0.13],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKSettingsmoviePrecision_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','on',... -'Tag','OKSettingsmoviePrecision',... -'FontSize',ssize,... -'FontName',nfont); - -h46 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Movie Size (px)',... -'Style','text',... -'Position',[0.68 0.73 0.06 0.13],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text182',... -'UserData',[],... -'FontSize',ssize,... -'FontName',nfont); - -h47 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','# Frames',... -'Style','text',... -'Position',[0.68 0.6 0.06 0.098],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text183',... -'UserData',[],... -'FontSize',ssize,... -'FontName',nfont); - -h53 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Codec',... -'Style','text',... -'Position',[0.66 0.14 0.055 0.098],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text192',... -'UserData',[],... -'FontSize',ssize,... -'FontName',nfont); - -h48 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String',{ 'GStreamer'; 'Cortex' },... -'Style','popupmenu',... -'Value',1,... -'Position',[0.58 0.12 0.11 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKSettingsmovieType_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','on',... -'Tag','OKSettingsmovieType',... -'FontSize',ssize,... -'FontName',nfont); - -h48a = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Recorder',... -'Style','text',... -'Position',[0.58 0.05 0.08 0.13],... -'BackgroundColor',bcolour,... -'Children',[],... -'FontSize',ssize,... -'FontName',nfont); - -h52 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','default',... -'Style','edit',... -'Position',[0.58 0.44 0.07 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKSettingsmovieCodec_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','on',... -'Tag','OKSettingsmovieCodec',... -'UserData',[],... -'FontSize',ssize,... -'FontName',nfont); - -h54 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','OPX Server',... -'Style','checkbox',... -'Position',[0.8 0.9 0.08 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKOmniplexEnable_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKOmniplexEnable',... -'FontSize',ssize,... -'FontName',nfont ); - -h49 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','10.0.0.1',... -'Style','edit',... -'Position',[0.8 0.8 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Children',[],... -'Tag','OKOmniplexIP',... -'Enable','off',... -'FontSize',msize,... -'FontName',mfont ); - -h50 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','9889',... -'Style','edit',... -'Position',[0.93 0.8 0.04 0.13],... -'BackgroundColor',[1 1 1],... -'Children',[],... -'Tag','OKOmniplexPort',... -'Enable','off',... -'FontSize',msize,... -'FontName',mfont); - -h51 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Port',... -'Style','text',... -'Position',[0.97 0.8 0.02 0.13],... -'BackgroundColor',bcolour,... -'Children',[],... -'FontSize',ssize,... -'FontName',nfont); - -h55 = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','IP',... -'Style','text',... -'Position',[0.9 0.8 0.01 0.13],... -'BackgroundColor',bcolour,... -'Children',[],... -'FontSize',ssize,... -'FontName',nfont); - -h56b = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Eye Occluder Port',... -'Style','text',... -'Position',[0.9 0.57 0.1 0.13],... -'BackgroundColor',bcolour,... -'HorizontalAlignment','left',... -'Children',[],... -'FontSize',ssize,... -'FontName',nfont); - -h56 = uicontrol(...7 -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','',... -'Style','edit',... -'Position',[0.8 0.6 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Children',[],... -'Tag','OKeyeOccluder',... -'FontSize',msize,... -'FontName',mfont); - -h56c = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','simple',... -'Style','edit',... -'Position',[0.8 0.4 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Children',[],... -'Tag','OKdPPMode',... -'FontSize',msize,... -'FontName',mfont); - -h56d = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Strobe Type',... -'Style','text',... -'Position',[0.9 0.36 0.1 0.14],... -'BackgroundColor',bcolour,... -'HorizontalAlignment','left',... -'Children',[],... -'FontSize',ssize,... -'FontName',nfont); - -h56e = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','/dev/ttyACM0',... -'Style','edit',... -'Position',[0.8 0.2 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Children',[],... -'Tag','OKarduinoPort',... -'FontSize',msize,... -'FontName',mfont); - -h56f = uicontrol(... -'Parent',h28,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Arduino Port',... -'Style','text',... -'Position',[0.9 0.16 0.1 0.14],... -'BackgroundColor',bcolour,... -'HorizontalAlignment','left',... -'Children',[],... -'FontSize',ssize,... -'FontName',nfont); - -h61 = uibuttongroup(... -'Parent',tab2,... -'FontUnits','points',... -'Units','normalized',... -'ForegroundColor',[0.3 0.3 0.3],... -'Title','',... -'BorderType','none',... -'Position',[0 0 1 1],... -'Visible','on',... -'BackgroundColor',bcolour,... -'Clipping','off',... -'Tag','OKPanelTraining',... -'FontSize',lsize,... -'FontName',nfont,... -'FontWeight','bold'); - - -h64 = uicontrol(... -'Parent',h61,... -'FontUnits',get(0,'defaultuicontrolFontUnits'),... -'Units','normalized',... -'String','Load State File',... -'Position',[0.882 0.412 0.108 0.135],... -'Callback',@(hObject,eventdata)opticka_ui('OKLoadStateButton_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKLoadStateButton',... -'FontSize',msize,... -'FontName',nfont); - - -h65 = uicontrol(... -'Parent',h61,... -'FontUnits',get(0,'defaultuicontrolFontUnits'),... -'Units','normalized',... -'String','Edit State File',... -'Position',[0.882 0.251 0.108 0.135],... -'Callback',@(hObject,eventdata)opticka_ui('OKEditStateFileButon_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKEditStateFileButon',... -'FontSize',msize,... -'FontName',nfont); - - -h62 = uicontrol(... -'Parent',h61,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Simulcra',... -'Style','edit',... -'Position',[0.88 0.9 0.1 0.1],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKTrainingName_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','on',... -'Tag','OKTrainingName',... -'UserData',[],... -'FontSize',msize,... -'FontName',nfont); - - -h63 = uicontrol(... -'Parent',h61,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Subject',... -'Style','text',... -'Position',[0.88 0.8 0.05 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Enable','on',... -'Tag','text209',... -'UserData',[],... -'FontSize',ssize,... -'FontName',nfont); - - -h66 = uicontrol(... -'Parent',h61,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Joanna Doe',... -'Style','edit',... -'Position',[0.88 0.7 0.1 0.1],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKTrainingResearcherName_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','on',... -'Tag','OKTrainingResearcherName',... -'UserData',[],... -'FontSize',msize,... -'FontName',nfont); - - -h67 = uicontrol(... -'Parent',h61,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Researcher',... -'Style','text',... -'Position',[0.88 0.6 0.1 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Enable','on',... -'Tag','text4554',... -'UserData',[],... -'FontSize',ssize,... -'FontName',nfont); - - -h69 = uicontrol(... -'Parent',h61,... -'FontUnits',get(0,'defaultuicontrolFontUnits'),... -'Units','normalized',... -'String','Refresh View...',... -'Position',[0.882516188714154 0.0903225806451613 0.108233117483811 0.135483870967742],... -'Callback',@(hObject,eventdata)opticka_ui('OKRefreshStateInfo_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKRefreshStateInfo',... -'FontSize',msize,... -'FontName',nfont); - - -h70 = uicontrol(... -'Parent',h61,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','File Name:',... -'Style','text',... -'Position',[0 0 0.8 0.07],... -'BackgroundColor',bcolour,... -'Children',[],... -'ForegroundColor',[0.38 0.38 0.38],... -'Tag','OKTrainingFileName',... -'UserData',[],... -'FontSize',ssize,... -'FontName',mfont); - - -h68 = uicontrol(... -'Parent',h61,... -'FontUnits',get(0,'defaultuicontrolFontUnits'),... -'Units','normalized',... -'HorizontalAlignment','left',... -'Max',500,... -'String','State Info here...',... -'Style','edit',... -'Value',10,... -'Position',[0 0.1 0.88 0.9],... -'BackgroundColor',[1 1 1],... -'Children',[],... -'Tag','OKTrainingText',... -'FontSize',msize,... -'FontName',mfont); - - -h71 = uitoolbar(... -'Parent',h1,... -'Tag','OKToolbar'); - -appdata.toolid = []; -h72 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{1},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKToolbarInitialise_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'TooltipString','Initialise Opticka with fresh settings',... -'Tag','OKToolbarInitialise'); - -appdata.toolid = 'Standard.FileOpen'; -appdata.CallbackInUse = struct(... - 'ClickedCallback', 'opticka_ui(''OKToolbarOpen_ClickedCallback'',gcbo,[],guidata(gcbo))'); -h73 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{2},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKToolbarOpen_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'TooltipString','Open File',... -'BusyAction','cancel',... -'Interruptible','off',... -'Tag','OKToolbarOpen'); - -appdata.toolid = []; -h74 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{3},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKSaveData_Callback',hObject,eventdata,guidata(hObject)),... -'TooltipString','Save as Data',... -'Tag','OKToolbarSave'); - -appdata.toolid = []; -h75 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{4},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKToolbarDefinePath_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Separator','on',... -'TooltipString','Choose a default folder to save eyelink and other log files',... -'Tag','OKToolbarDefinePath'); - -appdata.toolid = []; -h76 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{5},... -'Enable','off',... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKToggleUI_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Separator','on',... -'TooltipString','Toggle settings panels...',... -'Tag','OKToggleUI'); - -appdata.toolid = []; -h77 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{6},... -'Separator','on',... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKRFMapper_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Separator','on',... -'TooltipString','Start the RF Mapper',... -'Tag','OKRFMapper'); - -appdata.toolid = []; -h78 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{7},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKStartTask_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Separator',get(0,'defaultuipushtoolSeparator'),... -'TooltipString','Start behavioural task...',... -'Tag','OKStartTask'); - -appdata.toolid = []; - -h79 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{8},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKToolbarRun_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Separator',get(0,'defaultuipushtoolSeparator'),... -'TooltipString','Start MOC Task',... -'Tag','OKToolbarRun'); - -appdata.toolid = []; - -h80 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{9},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKToolbarStop_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Enable','off',... -'TooltipString','STOP',... -'Tag','OKToolbarStop'); - -appdata.toolid = []; - -h81 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{10},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKToolbarAbort_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Enable','off',... -'TooltipString','ABORT',... -'Tag','OKToolbarAbort'); - -appdata.toolid = []; - -h82 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{11},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKPanelTellOmniplex_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Enable','off',... -'Separator','on',... -'TooltipString','Connect to Omniplex!',... -'Tag','OKToolbarTellOmniplex'); - -appdata.toolid = []; - -h83 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{12},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKPanelReconnectOmniplex_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Enable','off',... -'TooltipString','Reconnect to Omniplex!',... -'Tag','OKPanelReconnectOmniplex'); - -appdata.toolid = []; - -h84 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{13},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKPanelSendOmniplex_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Enable','off',... -'TooltipString','Send Omniplex Stimulus',... -'Tag','OKPanelSendOmniplex'); - -appdata.toolid = []; - -h85 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{14},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKPanelPingOmniplex_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'Enable','off',... -'TooltipString','Ping Omniplex',... -'Tag','OKPanelPingOmniplex'); - -appdata.toolid = []; - -h86 = uipushtool(... -'Parent',h71,... -'Children',[],... -'CData',mat{15},... -'ClickedCallback',@(hObject,eventdata)opticka_ui('OKPanelDisconnectOmniplex_ClickedCallback',hObject,eventdata,guidata(hObject)),... -'TooltipString','Disconnect Omniplex',... -'Enable','off',... -'Tag','OKPanelDisconnectOmniplex'); - -appdata.toolid = []; - -h87 = uitoggletool(... -'Parent',h71,... -'Children',[],... -'CData',mat{16},... -'Enable','off',... -'Separator','on',... -'TooltipString','Toggle into remote client mode',... -'OnCallback',@(hObject,eventdata)opticka_ui('OKToolbarToggleRemote_OnCallback',hObject,eventdata,guidata(hObject)),... -'Tag','OKToolbarToggleRemote'); - -h88 = uimenu(... -'Parent',h1,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuPreferences_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Calibration',... -'Tag','OKMenuPreferences'); - -h89 = uimenu(... -'Parent',h88,... -'Separator','on',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuCalibrateLuminance_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Calibrate Gamma...',... -'Tag','OKMenuCalibrateLuminance'); - -h90 = uimenu(... -'Parent',h88,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuCalibrateSize_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Calibrate Size...',... -'Tag','OKMenuCalibrateSize'); - -h91 = uimenu(... -'Parent',h88,... -'Separator','on',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuLoadGamma_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Load Gamma...',... -'Tag','OKMenuLoadGamma'); - -h92 = uimenu(... -'Parent',h88,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuSaveGamma_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Save Gamma...',... -'Tag','OKMenuSaveGamma'); - -h92a = uimenu(... -'Parent',h88,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuClearGamma_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Clear Gamma',... -'Tag','OKMenuClearGamma'); - -h93 = uimenu(... -'Parent',h88,... -'Enable','off',... -'Separator','on',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenumanageCode_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Check for Updates',... -'Tag','OKMenumanageCode'); - -h94 = uimenu(... -'Parent',h88,... -'Enable','off',... -'Separator','on',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuEditConfiguration_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Edit Configuration',... -'Tag','OKMenuEditConfiguration'); - -h95 = uibuttongroup(... -'Parent',tab1,... -'FontUnits','pixels',... -'Units','normalized',... -'ForegroundColor',[0.3 0.3 0.3],... -'TitlePosition','righttop',... -'Title','',... -'BorderType','none',... -'Position',[0 0 1 1],... -'BackgroundColor',bcolour,... -'Clipping','off',... -'Tag','OKPanelGlobal',... -'FontSize',lsize,... -'FontName',nfont,... -'FontWeight','bold'); - -h122 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Gamma',... -'Style','text',... -'Position',[0.287 0.4 0.08 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text189',... -'FontSize',msize,... -'FontName',nfont); - -h108 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Window',... -'Style','text',... -'Position',[0.1 0.38 0.06 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text83',... -'FontSize',msize,... -'FontName',nfont); - -h110 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Src Blend',... -'Style','text',... -'Position',[0.287 0.88 0.08 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text100',... -'FontSize',msize,... -'FontName',nfont); - -h111 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Dst blend',... -'Style','text',... -'Position',[0.287 0.72 0.08 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text101',... -'FontSize',msize,... -'FontName',nfont); - -h105 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','X Center',... -'Style','text',... -'Position',[0.1 0.23 0.06 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text6',... -'FontSize',msize,... -'FontName',nfont); - -h106 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Y Center',... -'Style','text',... -'Position',[0.1 0.07 0.06 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text7',... -'FontSize',msize,... -'FontName',nfont); - -h99 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Monitor',... -'Style','text',... -'Position',[0.1 0.872 0.06 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text3',... -'FontSize',msize,... -'FontName',nfont); - -h124 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Bit Depth',... -'Style','text',... -'Position',[0.287 0.57 0.08 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'TooltipString','Bit Depth of backing framebuffer',... -'Tag','text191',... -'FontSize',msize,... -'FontName',nfont); - -h100 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Distance(cm)',... -'Style','text',... -'Position',[0.1 0.7 0.06 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text4',... -'FontSize',msize,... -'FontName',nfont); - -h130 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Background',... -'Style','text',... -'Position',[0.54 0.88 0.09 0.09],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text8',... -'FontSize',msize,... -'FontName',nfont); - -h101 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Px / cm',... -'Style','text',... -'Position',[0.1 0.55 0.06 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text5',... -'FontSize',msize,... -'FontName',nfont); - -h96 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Anti-Aliasing',... -'Style','text',... -'Position',[0.287 0.235 0.08 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text102',... -'FontSize',msize,... -'FontName',nfont); - -h97 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','57.3',... -'Style','edit',... -'Position',[0 0.7 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKMonitorDistance_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKMonitorDistance',... -'FontSize',msize,... -'FontName',mfont); - -h98 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','32',... -'Style','edit',... -'Position',[0 0.547 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKpixelsPerCm_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Pixels per cm as calculated during screen calibration',... -'Tag','OKpixelsPerCm',... -'FontSize',msize,... -'FontName',mfont); - -h102 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','[]',... -'Style','edit',... -'Position',[0 0.38 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKWindowSize_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Enter an [] matrix for automatic fullscreen or a [W H] matrix for a widthxheight window',... -'Tag','OKWindowSize',... -'FontSize',msize,... -'FontName',mfont); - -h103 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','0',... -'Style','edit',... -'Position',[0 0.22 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKscreenXOffset_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKscreenXOffset',... -'FontSize',msize,... -'FontName',mfont); - -h104 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','0',... -'Style','edit',... -'Position',[0 0.0621 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKscreenYOffset_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKscreenYOffset',... -'FontSize',msize,... -'FontName',mfont); - -h107 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','GL Blending',... -'Value',1,... -'Style','checkbox',... -'Position',[0.369 0.73 0.1 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKOpenGLBlending_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Enable global OpenGL blending for PTB?',... -'Tag','OKOpenGLBlending',... -'FontSize',msize,... -'FontName',nfont); - -h109 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String',{ '0'; '1' },... -'Style','popupmenu',... -'Value',1,... -'Position',[0 0.86 0.1 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKSelectScreen_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKSelectScreen',... -'FontSize',msize,... -'FontName',mfont); - -h112 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Hide Flash',... -'Style','checkbox',... -'Position',[0.369 0.34375 0.1 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKHideFlash_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Use Mario''s gamma trick to hide black flash onset',... -'Tag','OKHideFlash',... -'FontSize',msize,... -'FontName',nfont); - -h112b = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Use Retina',... -'Style','checkbox',... -'Position',[0.369 0.22 0.1 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKHideFlash_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Use native retina resolution rather than OS downscaled resolution',... -'Tag','OKUseRetina',... -'FontSize',msize,... -'FontName',nfont); - -h112c = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Draw Fixation',... -'Style','checkbox',... -'Position',[0.369 0.1 0.1 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKHideFlash_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Draw simple fixation cross for MOC tasks only',... -'Tag','OKDrawFixation',... -'FontSize',msize,... -'FontName',nfont); - -h112c = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Dummy Eyetracker',... -'Style','checkbox',... -'Position',[0.48 0.1 0.11 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKHideFlash_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Use dummy-mode for eyetracker connection?',... -'Tag','OKDummyMode',... -'FontSize',msize,... -'FontName',nfont); - -h113 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Photodiode',... -'Style','checkbox',... -'Position',[0.48 0.478 0.1 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKUsePhotoDiode_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Show a white<->black square to log stimulus onset with a photodiode.',... -'Tag','OKUsePhotoDiode',... -'FontSize',msize,... -'FontName',nfont); - -h114 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Verbose',... -'Style','checkbox',... -'Position',[0.369 0.59375 0.1 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKVerbose_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Show more information in the command window?',... -'Tag','OKVerbose',... -'FontSize',msize,... -'FontName',nfont); - -h115 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Debug Mode',... -'Style','checkbox',... -'Position',[0.369 0.46875 0.1 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKDebug_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Show onscreen info and disregard timing errors',... -'Tag','OKDebug',... -'FontSize',msize,... -'FontName',nfont); - -h116 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String',{ 'GL_ZERO'; 'GL_ONE'; 'GL_DST_COLOR'; 'GL_ONE_MINUS_DST_COLOR'; 'GL_SRC_ALPHA'; 'GL_ONE_MINUS_SRC_ALPHA'; 'GL_DST_ALPHA'; 'GL_ONE_MINUS_DST_ALPHA'; 'GL_SRC_ALPHA_SATURATE' },... -'Style','popupmenu',... -'Value',5,... -'Position',[0.176 0.86 0.11 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKGLSrc_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKGLSrc',... -'FontSize',msize,... -'FontName',mfont); - -h117 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String',{ 'GL_ZERO'; 'GL_ONE'; 'GL_DST_COLOR'; 'GL_ONE_MINUS_DST_COLOR'; 'GL_SRC_ALPHA'; 'GL_ONE_MINUS_SRC_ALPHA'; 'GL_DST_ALPHA'; 'GL_ONE_MINUS_DST_ALPHA'; 'GL_SRC_ALPHA_SATURATE' },... -'Style','popupmenu',... -'Value',6,... -'Position',[0.176 0.7 0.11 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKGLDst_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKGLDst',... -'FontSize',msize,... -'FontName',mfont); - -h118 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','0',... -'Style','edit',... -'Position',[0.176 0.22 0.11 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKAntiAliasing_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Global anti-aliasing',... -'Tag','OKAntiAliasing',... -'FontSize',msize,... -'FontName',mfont); - -h119 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Native BeamPos',... -'Style','checkbox',... -'Position',[0.48 0.217 0.1 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKNativeBeamPosition_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Use the OS native beam position (ON) queries or override (OFF) with PTB routine; override is better for OS X along with use of the kernel driver',... -'Tag','OKNativeBeamPosition',... -'FontSize',msize,... -'FontName',nfont); - -h120 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Record Movie',... -'Style','checkbox',... -'Position',[0.48 0.35 0.1 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKrecordMovie_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Enable PTB stimulus recording, currently buggy as gstreamer not stable',... -'Tag','OKrecordMovie',... -'FontSize',msize,... -'FontName',nfont); - -h121 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','None',... -'Style','popupmenu',... -'Value',1,... -'Position',[0.176 0.38 0.11 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKUseGamma_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKUseGamma',... -'FontSize',msize,... -'FontName',nfont); - -h123 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Log Times',... -'Style','checkbox',... -'Value',1,... -'Position',[0.48 0.73 0.080 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKlogFrames_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Log each frame time to check for dropped frames etc.',... -'Tag','OKlogFrames',... -'FontSize',msize,... -'FontName',nfont); - -h125 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String', {'FloatingPoint32BitIfPossible'; 'FloatingPoint32Bit';... - 'Native10Bit'; '8bit'; 'PseudoGray'; ... - 'EnableBits++Bits++Output'; 'EnableBits++Mono++Output'; ... - 'EnableBits++Color++Output'; 'EnableBits++Mono++OutputWithOverlay'; ... - 'Native11Bit'; 'Native16Bit'; ... - 'Native16BitFloat'; 'FixedPoint16Bit'; 'FloatingPoint16Bit' },... -'Style','popupmenu',... -'Value',1,... -'Position',[0.176 0.547 0.11 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKbitDepth_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKbitDepth',... -'FontSize',msize,... -'FontName',nfont); - -h126 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','4',... -'Style','edit',... -'Position',[0.176 0.0621 0.11 0.13],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKverbosityLevel_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Level of verbosity logging used by PTB',... -'Tag','OKverbosityLevel',... -'FontSize',msize,... -'FontName',mfont); - -h127 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Benchmark',... -'Style','checkbox',... -'Position',[0.48 0.609 0.080 0.1],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKbenchmark_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Flip as fast as possible to test maximum framerate',... -'Tag','OKbenchmark',... -'FontSize',msize,... -'FontName',nfont); - -h128 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Verbosity',... -'TooltipString','Control the verbosity of PTB logging to command window',... -'Style','text',... -'Position',[0.287 0.085 0.08 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text203',... -'FontSize',msize,... -'FontName',nfont); - -h129 = uicontrol(... -'Parent',h95,... -'FontUnits','pixels',... -'Units','normalized',... -'String','0.5 0.5 0.5 0',... -'Style','edit',... -'Position',[0.370027752081406 0.87 0.17 0.13],... -'BackgroundColor',[1 1 1],... -'TooltipString','RGBA colour for background of screen',... -'Callback',@(hObject,eventdata)opticka_ui('OKbackgroundColour_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKbackgroundColour',... -'FontSize',msize,... -'FontName',mfont); - -h131 = uibuttongroup(... -'Parent',h95,... -'FontUnits','points',... -'Units','normalized',... -'ForegroundColor',[0.3 0.3 0.3],... -'Title','Trial Randomisation Options',... -'TitlePosition','righttop',... -'Position',[0.6 0.015 0.40 0.98],... -'BackgroundColor',bcolour,... -'Clipping','off',... -'Tag','uipanel45',... -'FontSize',ssize,... -'FontName',nfont,... -'FontWeight','bold'); - -h132 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Randomise Order',... -'Style','checkbox',... -'Value',1,... -'Position',[0.023 0.27 0.265 0.14],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKRandomise_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKRandomise',... -'FontSize',msize,... -'FontName',nfont); - -h133 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'String',{ 'mt19937ar'; 'mcg16807'; 'mrg32k3a'; 'swb2712' },... -'Style','popupmenu',... -'Value',1,... -'Position',[0.023 0.45 0.25 0.14],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKrandomGenerator_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKrandomGenerator',... -'FontSize',msize,... -'FontName',nfont); - -h134 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Random Type',... -'Style','text',... -'Position',[0.28 0.49 0.2 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text105',... -'FontSize',msize,... -'FontName',nfont); - -h135 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'String','[]',... -'Style','edit',... -'Position',[0.023 0.63 0.25 0.14],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKRandomSeed_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Random seed for the random number generator',... -'Tag','OKRandomSeed',... -'FontSize',msize,... -'FontName',mfont); - -h136 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Random Seed',... -'Style','text',... -'Position',[0.28 0.687 0.17 0.1],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text106',... -'FontSize',msize,... -'FontName',nfont); - -h137 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'String','5',... -'Style','edit',... -'Position',[0.023 0.83 0.25 0.14],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKnBlocks_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKnBlocks',... -'FontSize',msize,... -'FontName',mfont); - -h138 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'String','0.5',... -'Style','edit',... -'Position',[0.49 0.64 0.25 0.137],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKisTime_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Inter-trial time for acute run (not using state machine)',... -'Tag','OKisTime',... -'FontSize',msize,... -'FontName',mfont); - -h139 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Trial Blocks',... -'Style','text',... -'Position',[0.28 0.88 0.16 0.09],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text9',... -'FontSize',msize,... -'FontName',nfont); - -h140 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Intertrial Time',... -'Style','text',... -'Position',[0.75 0.68 0.22 0.09],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text10',... -'FontSize',msize,... -'FontName',nfont); - -h141 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'String','1',... -'Style','edit',... -'Position',[0.49 0.47 0.25 0.137],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKibTime_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Inter-block time (not using state machine)',... -'Tag','OKibTime',... -'FontSize',msize,... -'FontName',mfont); - -h142 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','InterBlock Time',... -'Style','text',... -'Position',[0.75 0.5 0.22 0.09],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text104',... -'FontSize',msize,... -'FontName',nfont); - -h143 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'String','2',... -'Style','edit',... -'Position',[0.49 0.842465753424657 0.25 0.137],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKtrialTime_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','Trial time for MOC task (not using state machine)',... -'Tag','OKtrialTime',... -'FontSize',msize,... -'FontName',mfont); - -h144 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','MOC Trial Time',... -'TooltipString','This is used for MOC tasks, for behavioural tasks it will be overwritten...',... -'Style','text',... -'Position',[0.75 0.88 0.247 0.09],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text107',... -'FontSize',msize,... -'FontName',nfont); - -h145 = uicontrol(... -'Parent',h131,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Use Clock Time?',... -'Style','checkbox',... -'Value',1,... -'Position',[0.023 0.11 0.24 0.14],... -'BackgroundColor',bcolour,... -'Callback',@(hObject,eventdata)opticka_ui('OKrealTime_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'TooltipString','If ON then we use absoloute time which is less liable to error build-up, otherwise measure time in number of frames needed to show, and even if frames are dropped, opticka simply shows the right number of frames ignoring possible time drift (better for record movie mode)',... -'Tag','OKrealTime',... -'FontSize',msize,... -'FontName',nfont); - -h184 = uipanel(... -'Parent',h146,... -'FontUnits',get(0,'defaultuipanelFontUnits'),... -'Units',get(0,'defaultuipanelUnits'),... -'ForegroundColor',[0.49 0.49 0.49],... -'HighlightColor',bcolour,... -'BorderType','none',... -'TitlePosition','centertop',... -'Title',blanks(0),... -'Position',[0.3 0.45 0.7 0.55],... -'BackgroundColor',bcolour,... -'Clipping','off',... -'Tag','OKPanelStimulus',... -'FontSize',lsize,... -'FontName',nfont); - -h185 = uicontrol(... -'Parent',h184,... -'FontUnits',get(0,'defaultuicontrolFontUnits'),... -'Units','normalized',... -'String','Stimulus options...',... -'Style','text',... -'Position',[0.38 0.4 0.22 0.088],... -'BackgroundColor',bcolour,... -'Children',[],... -'ForegroundColor',[0.49 0.49 0.49],... -'Enable','off',... -'Tag','OKPanelStimulusText',... -'FontSize',lsize,... -'FontName',nfont); - -h147 = uibuttongroup(... -'Parent',h146,... -'FontUnits','pixels',... -'Units','normalized',... -'ForegroundColor',[0.3 0.3 0.3],... -'ShadowColor',[0.50 0.50 0.50],... -'Title','Stimulus List',... -'Position',[0 0.45 0.3 0.55],... -'BackgroundColor',bcolour,... -'Clipping','off',... -'Tag','OKPanelStimulusList',... -'UserData',[],... -'FontSize',msize,... -'FontName',nfont,... -'FontWeight','bold'); - -h148 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Add',... -'Position',[0.0157 0.0266 0.172 0.113],... -'Callback',@(hObject,eventdata)opticka_ui('OKAddStimulus_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKAddStimulus',... -'FontSize',msize,... -'FontName',nfont); - -h149 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Delete',... -'Position',[0.213 0.0266 0.172 0.113],... -'Callback',@(hObject,eventdata)opticka_ui('OKDeleteStimulus_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKDeleteStimulus',... -'FontSize',msize,... -'FontName',nfont); - -h150 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Copy',... -'Position',[0.411 0.0266 0.172 0.113],... -'Callback',@(hObject,eventdata)opticka_ui('OKCopyStimulus_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKCopyStimulus',... -'FontSize',msize,... -'FontName',nfont); - -h151 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','?',... -'Position',[0.941 0.93 0.05 0.06],... -'Callback',@(hObject,eventdata)opticka_ui('OKInspectStimulus_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKInspectStimulus',... -'FontSize',ssize,... -'FontName',nfont); - -h152 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Modify / Examine',... -'Position',[0.61 0.026 0.37 0.113],... -'Callback',@(hObject,eventdata)opticka_ui('OKrefreshStimulusList_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKrefreshStimulusList',... -'FontSize',msize,... -'FontName',nfont); - -h153 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','↓',... -'Position',[0.883 0.93 0.05 0.06],... -'Callback',@(hObject,eventdata)opticka_ui('OKStimulusDown_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKStimulusDown',... -'FontSize',ssize,... -'FontName',nfont); - -h154 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','↑',... -'Position',[0.825 0.93 0.05 0.06],... -'Callback',@(hObject,eventdata)opticka_ui('OKStimulusUp_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKStimulusUp',... -'FontSize',ssize,... -'FontName',nfont); - -h155 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','R',... -'Position',[0.766 0.93 0.05 0.06],... -'Callback',@(hObject,eventdata)opticka_ui('OKStimulusRun_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'TooltipString','Run the selected stimulus in a window',... -'Tag','OKStimulusRun',... -'FontSize',ssize,... -'FontName',nfont); - -h156 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','BR',... -'Position',[0.708 0.93 0.05 0.06],... -'Callback',@(hObject,eventdata)opticka_ui('OKStimulusRunBenchmark_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'TooltipString','Run the selected stimulus and benchmark maximum FPS',... -'Tag','OKStimulusRunBenchmark',... -'FontSize',ssize,... -'FontName',nfont); - -h157 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','A',... -'Position',[0.650 0.93 0.05 0.06],... -'Callback',@(hObject,eventdata)opticka_ui('OKStimulusRunAll_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'TooltipString','Preview All Stimuli...',... -'Tag','OKStimulusRunAll',... -'FontSize',ssize,... -'FontName',nfont); - -h158 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','BA',... -'Position',[0.592 0.93 0.05 0.06],... -'Callback',@(hObject,eventdata)opticka_ui('OKStimulusRunAllBenchmark_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'TooltipString','Benchmark All Stimuli...',... -'Tag','OKStimulusRunAllBenchmark',... -'FontSize',ssize,... -'FontName',nfont); - -h159 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'Style','listbox',... -'Value',1,... -'Position',[0.0157 0.167 0.974 0.756],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKStimList_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKStimList',... -'FontSize',msize,... -'FontName',nfont); - -h160 = uicontrol(... -'Parent',h147,... -'FontUnits','pixels',... -'Units','normalized',... -'String','RS',... -'Position',[0.533 0.93 0.05 0.06],... -'Callback',@(hObject,eventdata)opticka_ui('OKStimulusRunSingle_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'TooltipString','Single run using eyetracker',... -'Tag','OKStimulusRunSingle',... -'FontSize',ssize,... -'FontName',nfont); - -h161 = uicontrol(... -'Parent',h146,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','right',... -'String','OPX Server for Online PSTH: disconnected',... -'Style','text',... -'Position',[0.287 0 0.7 0.037],... -'BackgroundColor',bcolour,... -'Children',[],... -'Enable','off',... -'ForegroundColor',[0.2 0.2 0.2],... -'TooltipString','This connection status is for the online display ONLY, it does not affect data collection communication via the datapixx',... -'Tag','OKOmniplexStatus',... -'FontSize',13,... -'FontName',nfont,... -'FontWeight','bold'); - -h162 = uicontrol(... -'Parent',h146,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Loading...',... -'Style','text',... -'Position',[0 0 0.3 0.037],... -'BackgroundColor',bcolour,... -'Children',[],... -'ForegroundColor',[0.8 0.2 0],... -'Tag','OKOptickaVersion',... -'FontSize',13,... -'FontName',nfont,... -'FontWeight','bold'); - -h163 = uipanel(... -'Parent',h146,... -'FontUnits','pixels',... -'Units',get(0,'defaultuipanelUnits'),... -'ForegroundColor',[0.38 0.38 0.38],... -'ShadowColor',[0.5 0.5 0.5],... -'TitlePosition','centertop',... -'Title','Independent Variables',... -'Position',[0.3 0.043 0.7 0.4],... -'BackgroundColor',bcolour,... -'Clipping','off',... -'Tag','OKPanelVariables',... -'FontSize',msize,... -'FontName',nfont,... -'FontWeight','bold'); - -h164 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Affects Stimuli',... -'Style','text',... -'Position',[0.4 0.43 0.1 0.08],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text140',... -'UserData',[],... -'FontSize',msize,... -'FontName',nfont); - -h165 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Variable Values',... -'Style','text',... -'Position',[0.4 0.57 0.1 0.08],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text141',... -'UserData',[],... -'FontSize',msize,... -'FontName',nfont); - -h166 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Variable Name',... -'Style','text',... -'Position',[0.4 0.74 0.1 0.08],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text142',... -'UserData',[],... -'FontSize',msize,... -'FontName',nfont); - -h167 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'String','<<',... -'Position',[0.52 0.53 0.05 0.1],... -'Callback',@(hObject,eventdata)opticka_ui('OKCopyVariableName_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKCopyVariableName',... -'FontSize',msize,... -'FontName',nfont); - -h170 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'String','<-',... -'Position',[0.52 0.42 0.05 0.1],... -'Callback',@(hObject,eventdata)opticka_ui('OKCopyVariableNameValues_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKCopyVariableNameValues',... -'FontSize',msize,... -'FontName',nfont); - - -h168 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'String','lin',... -'Position',[0.05 0.5 0.05 0.1],... -'Callback',@(hObject,eventdata)opticka_ui('OKVariablesLinear_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKVariablesLinear',... -'FontSize',msize,... -'FontName',nfont); - -h169 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'String','log',... -'Position',[0 0.5 0.05 0.1],... -'Callback',@(hObject,eventdata)opticka_ui('OKVariablesLog_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKVariablesLog',... -'FontSize',msize,... -'FontName',nfont); - -h171 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'HorizontalAlignment','left',... -'String','Stimulus; Offset',... -'Style','text',... -'Position',[0.4 0.28 0.11 0.08],... -'BackgroundColor',bcolour,... -'Children',[],... -'Tag','text178',... -'UserData',[],... -'FontSize',msize,... -'FontName',nfont); - -h172 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'String','1',... -'Style','edit',... -'Position',[0.11 0.39 0.287 0.16],... -'BackgroundColor',[1 1 1],... -'Children',[],... -'Tag','OKVariableStimuli',... -'UserData',[],... -'FontSize',13,... -'FontName',mfont); - -h173 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'String','{[-5 -5],[0 0],[5 5]}',... -'Style','edit',... -'Position',[0.11 0.55 0.287 0.16],... -'BackgroundColor',[1 1 1],... -'Children',[],... -'Tag','OKVariableValues',... -'UserData',[],... -'FontSize',13,... -'FontName',mfont); - -h174 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'String','xyPosition',... -'Style','edit',... -'Position',[0.11 0.72 0.287 0.16],... -'BackgroundColor',[1 1 1],... -'Children',[],... -'Tag','OKVariableName',... -'UserData',[],... -'FontSize',13,... -'FontName',mfont); - -h175 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'String','[]',... -'Style','edit',... -'Position',[0.11 0.22 0.287 0.16],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKVariableOffset_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKVariableOffset',... -'UserData',[],... -'FontSize',13,... -'FontName',mfont); - -h176 = uicontrol(... -'Parent',h163,... -'FontUnits','pixels',... -'Units','normalized',... -'String',{ 'xyPosition';'xPosition'; 'yPosition'; 'angle'; 'colour'; 'direction'; 'size'; 'sf'; 'tf'; 'contrast'; 'alpha'; 'speed'; 'phase'; 'startPosition'; 'barHeight'; 'barWidth'; 'alpha'; 'nDots'; 'coherence'; 'scale'; 'driftDirection' },... -'Style','listbox',... -'Value',1,... -'Position',[0.58 0.025 0.4 0.925],... -'BackgroundColor',[1 1 1],... -'Callback',@(hObject,eventdata)opticka_ui('OKVariableList_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKVariableList',... -'FontSize',13,... -'FontName',mfont); - -h177 = uibuttongroup(... -'Parent',h146,... -'FontUnits','pixels',... -'Units','normalized',... -'ForegroundColor',[0.3 0.3 0.3],... -'ShadowColor',[0.5 0.5 0.5],... -'Title','Independent Variable List',... -'Position',[0 0.043 0.3 0.4],... -'BackgroundColor',bcolour,... -'Clipping','off',... -'Tag','uipanel44',... -'UserData',[],... -'FontSize',msize,... -'FontName',nfont,... -'FontWeight','bold'); - -h178 = uicontrol(... -'Parent',h177,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Add',... -'Position',[0.0159 0.0266 0.233 0.114],... -'Callback',@(hObject,eventdata)opticka_ui('OKAddVariable_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Tag','OKAddVariable',... -'FontSize',msize,... -'FontName',nfont); - -h179 = uicontrol(... -'Parent',h177,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Delete',... -'Position',[0.261 0.0266 0.233 0.114],... -'Callback',@(hObject,eventdata)opticka_ui('OKDeleteVariable_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKDeleteVariable',... -'FontSize',msize,... -'FontName',nfont); - -h180 = uicontrol(... -'Parent',h177,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Copy',... -'Position',[0.506 0.0266 0.233 0.114],... -'Callback',@(hObject,eventdata)opticka_ui('OKCopyVariable_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKCopyVariable',... -'FontSize',msize,... -'FontName',nfont); - -h181 = uicontrol(... -'Parent',h177,... -'FontUnits','pixels',... -'Units','normalized',... -'String','Edit',... -'Position',[0.755 0.0266 0.233 0.114],... -'Callback',@(hObject,eventdata)opticka_ui('OKEditVariable_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','off',... -'Tag','OKEditVariable',... -'FontSize',msize,... -'FontName',nfont); - -h182 = uicontrol(... -'Parent',h177,... -'FontUnits','pixels',... -'Units','normalized',... -'String','?',... -'Position',[0.94 0.93 0.05 0.08],... -'Callback',@(hObject,eventdata)opticka_ui('OKInspectVariable_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','on',... -'Tag','OKInspectVariable',... -'FontSize',ssize,... -'FontName',nfont); - -h182b = uicontrol(... -'Parent',h177,... -'FontUnits','pixels',... -'Units','normalized',... -'String','#',... -'Position',[0.88 0.93 0.05 0.08],... -'Callback',@(hObject,eventdata)opticka_ui('OKShuffleVariable_Callback',hObject,eventdata,guidata(hObject)),... -'Children',[],... -'Enable','on',... -'Tag','OKShuffleVariable',... -'FontSize',ssize,... -'FontName',nfont); - -h183 = uicontrol(... -'Parent',h177,... -'FontUnits','pixels',... -'Units','normalized',... -'Style','listbox',... -'Value',1,... -'Position',[0.0159 0.168 0.975 0.757],... -'BackgroundColor',[1 1 1],... -'Children',[],... -'Tag','OKVarList',... -'FontSize',msize,... -'FontName',nfont); - -h186 = uimenu(... -'Parent',h1,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuLogs_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Logs',... -'Tag','OKMenuLogs'); - -h187 = uimenu(... -'Parent',h186,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuAllTimingLogs_Callback',hObject,eventdata,guidata(hObject)),... -'Label','All Timing Logs',... -'Tag','OKMenuAllTimingLogs'); - -h187b = uimenu(... -'Parent',h186,... -'Callback',@(hObject,eventdata)opticka_ui('OKMenusMLogs_Callback',hObject,eventdata,guidata(hObject)),... -'Label','State Machine Logs',... -'Tag','OKMenusMLogs'); - -h188 = uimenu(... -'Parent',h186,... -'Separator','on',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuStimulusLog_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Stimulus Sequence',... -'Tag','OKMenuStimulusLog'); - -h189 = uimenu(... -'Parent',h186,... -'Separator','on',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenuShowGammaPlots_Callback',hObject,eventdata,guidata(hObject)),... -'Label','Gamma Correction Plots',... -'Tag','OKMenuShowGammaPlots'); - -h190 = uimenu(... -'Parent',h186,... -'Enable','off',... -'Separator','on',... -'Callback',@(hObject,eventdata)opticka_ui('OKMenurfMapperLog_Callback',hObject,eventdata,guidata(hObject)),... -'Label','RF Mapper Log',... -'Tag','OKMenurfMapperLog'); - -hsingleton = h1; - -% --- Handles default GUIDE GUI creation and callback dispatch -function varargout = gui_mainfcn(gui_State, varargin) - -gui_StateFields = {'gui_Name' - 'gui_Singleton' - 'gui_OpeningFcn' - 'gui_OutputFcn' - 'gui_LayoutFcn' - 'gui_Callback'}; -gui_Mfile = ''; -for i=1:length(gui_StateFields) - if ~isfield(gui_State, gui_StateFields{i}) - error(message('MATLAB:guide:StateFieldNotFound', gui_StateFields{ i }, gui_Mfile)); - elseif isequal(gui_StateFields{i}, 'gui_Name') - gui_Mfile = [gui_State.(gui_StateFields{i}), '.m']; - end -end - -numargin = length(varargin); - -if numargin == 0 - % OPTICKA_UI_EXPORT - % create the GUI only if we are not in the process of loading it - % already - gui_Create = true; -elseif local_isInvokeActiveXCallback(gui_State, varargin{:}) - % OPTICKA_UI_EXPORT(ACTIVEX,...) - vin{1} = gui_State.gui_Name; - vin{2} = [get(varargin{1}.Peer, 'Tag'), '_', varargin{end}]; - vin{3} = varargin{1}; - vin{4} = varargin{end-1}; - vin{5} = guidata(varargin{1}.Peer); - feval(vin{:}); - return; -elseif local_isInvokeHGCallback(gui_State, varargin{:}) - % OPTICKA_UI_EXPORT('CALLBACK',hObject,eventData,handles,...) - gui_Create = false; -else - % OPTICKA_UI_EXPORT(...) - % create the GUI and hand varargin to the openingfcn - gui_Create = true; -end - -if ~gui_Create - % In design time, we need to mark all components possibly created in - % the coming callback evaluation as non-serializable. This way, they - % will not be brought into GUIDE and not be saved in the figure file - % when running/saving the GUI from GUIDE. - designEval = false; - if (numargin>1 && ishghandle(varargin{2})) - fig = varargin{2}; - while ~isempty(fig) && ~ishghandle(fig,'figure') - fig = get(fig,'parent'); - end - - designEval = isappdata(0,'CreatingGUIDEFigure') || (isscalar(fig)&&isprop(fig,'GUIDEFigure')); - end - - if designEval - beforeChildren = findall(fig); - end - - % evaluate the callback now - varargin{1} = gui_State.gui_Callback; - if nargout - [varargout{1:nargout}] = feval(varargin{:}); - else - feval(varargin{:}); - end - - % Set serializable of objects created in the above callback to off in - % design time. Need to check whether figure handle is still valid in - % case the figure is deleted during the callback dispatching. - if designEval && ishghandle(fig) - set(setdiff(findall(fig),beforeChildren), 'Serializable','off'); - end -else - if gui_State.gui_Singleton - gui_SingletonOpt = 'reuse'; - else - gui_SingletonOpt = 'new'; - end - - % Check user passing 'visible' P/V pair first so that its value can be - % used by oepnfig to prevent flickering - gui_Visible = 'auto'; - gui_VisibleInput = ''; - for index=1:2:length(varargin) - if length(varargin) == index || ~ischar(varargin{index}) - break; - end - - % Recognize 'visible' P/V pair - len1 = min(length('visible'),length(varargin{index})); - len2 = min(length('off'),length(varargin{index+1})); - if ischar(varargin{index+1}) && strncmpi(varargin{index},'visible',len1) && len2 > 1 - if strncmpi(varargin{index+1},'off',len2) - gui_Visible = 'invisible'; - gui_VisibleInput = 'off'; - elseif strncmpi(varargin{index+1},'on',len2) - gui_Visible = 'visible'; - gui_VisibleInput = 'on'; - end - end - end - - % Open fig file with stored settings. Note: This executes all component - % specific CreateFunctions with an empty HANDLES structure. - - - % Do feval on layout code in m-file if it exists - gui_Exported = ~isempty(gui_State.gui_LayoutFcn); - % this application data is used to indicate the running mode of a GUIDE - % GUI to distinguish it from the design mode of the GUI in GUIDE. it is - % only used by actxproxy at this time. - setappdata(0,genvarname(['OpenGuiWhenRunning_', gui_State.gui_Name]),1); - if gui_Exported - gui_hFigure = feval(gui_State.gui_LayoutFcn, gui_SingletonOpt); - - % make figure invisible here so that the visibility of figure is - % consistent in OpeningFcn in the exported GUI case - if isempty(gui_VisibleInput) - gui_VisibleInput = get(gui_hFigure,'Visible'); - end - set(gui_hFigure,'Visible','off') - - % openfig (called by local_openfig below) does this for guis without - % the LayoutFcn. Be sure to do it here so guis show up on screen. - movegui(gui_hFigure,'onscreen'); - else - gui_hFigure = local_openfig(gui_State.gui_Name, gui_SingletonOpt, gui_Visible); - % If the figure has InGUIInitialization it was not completely created - % on the last pass. Delete this handle and try again. - if isappdata(gui_hFigure, 'InGUIInitialization') - delete(gui_hFigure); - gui_hFigure = local_openfig(gui_State.gui_Name, gui_SingletonOpt, gui_Visible); - end - end - if isappdata(0, genvarname(['OpenGuiWhenRunning_', gui_State.gui_Name])) - rmappdata(0,genvarname(['OpenGuiWhenRunning_', gui_State.gui_Name])); - end - - % Set flag to indicate starting GUI initialization - setappdata(gui_hFigure,'InGUIInitialization',1); - - % Fetch GUIDE Application options - gui_Options = getappdata(gui_hFigure,'GUIDEOptions'); - % Singleton setting in the GUI M-file takes priority if different - gui_Options.singleton = gui_State.gui_Singleton; - - if ~isappdata(gui_hFigure,'GUIOnScreen') - % Adjust background color -% if gui_Options.syscolorfig -% set(gui_hFigure,'Color', get(0,'DefaultUicontrolBackgroundColor')); -% end - - % Generate HANDLES structure and store with GUIDATA. If there is - % user set GUI data already, keep that also. - data = guidata(gui_hFigure); - handles = guihandles(gui_hFigure); - if ~isempty(handles) - if isempty(data) - data = handles; - else - names = fieldnames(handles); - for k=1:length(names) - data.(char(names(k)))=handles.(char(names(k))); - end - end - end - guidata(gui_hFigure, data); - end - - % Apply input P/V pairs other than 'visible' - for index=1:2:length(varargin) - if length(varargin) == index || ~ischar(varargin{index}) - break; - end - - len1 = min(length('visible'),length(varargin{index})); - if ~strncmpi(varargin{index},'visible',len1) - try set(gui_hFigure, varargin{index}, varargin{index+1}), catch break, end - end - end - - % If handle visibility is set to 'callback', turn it on until finished - % with OpeningFcn - gui_HandleVisibility = get(gui_hFigure,'HandleVisibility'); - if strcmp(gui_HandleVisibility, 'callback') - set(gui_hFigure,'HandleVisibility', 'on'); - end - - feval(gui_State.gui_OpeningFcn, gui_hFigure, [], guidata(gui_hFigure), varargin{:}); - - if isscalar(gui_hFigure) && ishghandle(gui_hFigure) - % Handle the default callbacks of predefined toolbar tools in this - % GUI, if any - guidemfile('restoreToolbarToolPredefinedCallback',gui_hFigure); - - % Update handle visibility - set(gui_hFigure,'HandleVisibility', gui_HandleVisibility); - - % Call openfig again to pick up the saved visibility or apply the - % one passed in from the P/V pairs - if ~gui_Exported - gui_hFigure = local_openfig(gui_State.gui_Name, 'reuse',gui_Visible); - elseif ~isempty(gui_VisibleInput) - set(gui_hFigure,'Visible',gui_VisibleInput); - end - if strcmpi(get(gui_hFigure, 'Visible'), 'on') - figure(gui_hFigure); - - if gui_Options.singleton - setappdata(gui_hFigure,'GUIOnScreen', 1); - end - end - - % Done with GUI initialization - if isappdata(gui_hFigure,'InGUIInitialization') - rmappdata(gui_hFigure,'InGUIInitialization'); - end - - % If handle visibility is set to 'callback', turn it on until - % finished with OutputFcn - gui_HandleVisibility = get(gui_hFigure,'HandleVisibility'); - if strcmp(gui_HandleVisibility, 'callback') - set(gui_hFigure,'HandleVisibility', 'on'); - end - gui_Handles = guidata(gui_hFigure); - else - gui_Handles = []; - end - - if nargout - [varargout{1:nargout}] = feval(gui_State.gui_OutputFcn, gui_hFigure, [], gui_Handles); - else - feval(gui_State.gui_OutputFcn, gui_hFigure, [], gui_Handles); - end - - if isscalar(gui_hFigure) && ishghandle(gui_hFigure) - set(gui_hFigure,'HandleVisibility', gui_HandleVisibility); - end -end - -function gui_hFigure = local_openfig(name, singleton, visible) - -% openfig with three arguments was new from R13. Try to call that first, if -% failed, try the old openfig. -if nargin('openfig') == 2 - % OPENFIG did not accept 3rd input argument until R13, - % toggle default figure visible to prevent the figure - % from showing up too soon. - gui_OldDefaultVisible = get(0,'defaultFigureVisible'); - set(0,'defaultFigureVisible','off'); - gui_hFigure = matlab.hg.internal.openfigLegacy(name, singleton); - set(0,'defaultFigureVisible',gui_OldDefaultVisible); -else - % Call version of openfig that accepts 'auto' option" - gui_hFigure = matlab.hg.internal.openfigLegacy(name, singleton, visible); -% %workaround for CreateFcn not called to create ActiveX -% if feature('HGUsingMATLABClasses') -% peers=findobj(findall(allchild(gui_hFigure)),'type','uicontrol','style','text'); -% for i=1:length(peers) -% if isappdata(peers(i),'Control') -% actxproxy(peers(i)); -% end -% end -% end -end - -function result = local_isInvokeActiveXCallback(gui_State, varargin) - -try - result = ispc && iscom(varargin{1}) ... - && isequal(varargin{1},gcbo); -catch - result = false; -end - -function result = local_isInvokeHGCallback(gui_State, varargin) - -try - fhandle = functions(gui_State.gui_Callback); - result = ~isempty(findstr(gui_State.gui_Name,fhandle.file)) || ... - (ischar(varargin{1}) ... - && isequal(ishghandle(varargin{2}), 1) ... - && (~isempty(strfind(varargin{1},[get(varargin{2}, 'Tag'), '_'])) || ... - ~isempty(strfind(varargin{1}, '_CreateFcn'))) ); -catch - result = false; -end \ No newline at end of file diff --git a/ui/legacy/opticka_ui.mat b/ui/legacy/opticka_ui.mat deleted file mode 100644 index be3860d5c5258f4a604bbb974e138cc50b21fd58..0000000000000000000000000000000000000000 Binary files a/ui/legacy/opticka_ui.mat and /dev/null differ diff --git a/ui/legacy/opticka_ui_guide.fig b/ui/legacy/opticka_ui_guide.fig deleted file mode 100644 index 738ec4b3191f9a256ce6485d86df9df4645a3de6..0000000000000000000000000000000000000000 Binary files a/ui/legacy/opticka_ui_guide.fig and /dev/null differ diff --git a/ui/legacy/opticka_ui_guide.m b/ui/legacy/opticka_ui_guide.m deleted file mode 100644 index f45673d1817a657885d113cb70c15d1b6964d006..0000000000000000000000000000000000000000 --- a/ui/legacy/opticka_ui_guide.m +++ /dev/null @@ -1,1635 +0,0 @@ -function varargout = opticka_ui(varargin) -% OPTICKA_UI M-file for opticka_ui.fig -% OPTICKA_UI, by itself, creates a new OPTICKA_UI or raises the existing -% singleton*. -% -% H = OPTICKA_UI returns the handle to a new OPTICKA_UI or the handle to -% the existing singleton*. -% -% OPTICKA_UI('CALLBACK',hObject,eventData,handles,...) calls the local -% function named CALLBACK in OPTICKA_UI.M with the given input arguments. -% -% OPTICKA_UI('Property','Value',...) creates a new OPTICKA_UI or raises the -% existing singleton*. Starting from the left, property value pairs are -% applied to the GUI before opticka_ui_OpeningFcn gets called. An -% unrecognized property name or invalid value makes property application -% stop. All inputs are passed to opticka_ui_OpeningFcn via varargin. -% -% *See GUI Options on GUIDE's Tools menu. Choose "GUI allows only one -% instance to run (singleton)". -% -% See also: GUIDE, GUIDATA, GUIHANDLES - -% Edit the above text to modify the response to help opticka_ui - -% Last Modified by GUIDE v2.5 24-Feb-2015 17:20:41 - -% Begin initialization code - DO NOT EDIT -gui_Singleton = 1; -gui_State = struct('gui_Name', mfilename, ... - 'gui_Singleton', gui_Singleton, ... - 'gui_OpeningFcn', @opticka_ui_OpeningFcn, ... - 'gui_OutputFcn', @opticka_ui_OutputFcn, ... - 'gui_LayoutFcn', [] , ... - 'gui_Callback', []); -if nargin && ischar(varargin{1}) - gui_State.gui_Callback = str2func(varargin{1}); -end - -if nargout - [varargout{1:nargout}] = gui_mainfcn(gui_State, varargin{:}); -else - gui_mainfcn(gui_State, varargin{:}); -end -% End initialization code - DO NOT EDIT - - -% --- Executes just before opticka_ui is made visible. -function opticka_ui_OpeningFcn(hObject, eventdata, handles, varargin) -% This function has no output args, see OutputFcn. -% hObject handle to figure -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -% varargin command line arguments to opticka_ui (see VARARGIN) - -% Choose default command line output for opticka_ui -handles.output = hObject; - -% Update handles structure -guidata(hObject, handles); - -% UIWAIT makes opticka_ui wait for user response (see UIRESUME) -% uiwait(handles.OKRoot); - - -% --- Outputs from this function are returned to the command line. -function varargout = opticka_ui_OutputFcn(hObject, eventdata, handles) -% varargout cell array for returning output args (see VARARGOUT); -% hObject handle to figure -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Get default command line output from handles structure -varargout{1} = handles.output; - -% -------------------------------------------------------------------- -function OKMenuFile_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuFile (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuEdit_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuEdit (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuTools_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuTools (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuStimulusLog_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuStimulusLog (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.task.showLog; -end - -% -------------------------------------------------------------------- -function OKMenuShowGammaPlots_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuShowGammaPlots (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen.gammaTable,'calibrateLuminance') - o.r.screen.gammaTable.plot; - end -end -% -------------------------------------------------------------------- -function OKMenuLogs_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuLogs (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuCheckIO_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuCheckIO (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - -end - -% -------------------------------------------------------------------- -function OKMenuEditConfiguration_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuEditConfiguration (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuAllTimingLogs_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuAllTimingLogs (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.showTimingLog; -end - -% -------------------------------------------------------------------- -function OKMenuMissedFrames_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuMissedFrames (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuCut_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuCut (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuCopy_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuCopy (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKMenuPaste_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuPaste (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% --- Executes on selection change in OKSelectScreen. -function OKSelectScreen_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - - -function OKMonitorDistance_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - - -function OKpixelsPerCm_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - - -function OKscreenXOffset_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKscreenYOffset_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKWindowSize_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKGLSrc_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - - -function OKGLDst_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKbitDepth_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKAntiAliasing_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKUseGamma_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKSerialPortName_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -% --- Executes on button press in OKUsePhotoDiode. -function OKUsePhotoDiode_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -% --- Executes on button press in OKuseDataPixx. -function OKuseDataPixx_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if get(hObject,'Value') == 1 && get(handles.OKuseDataPixx,'Value') == 1 - set(handles.OKuseLabJack,'Value', 0) - end - o.getScreenVals; -end - -% --- Executes on button press in OKuseLabJack. -function OKuseLabJack_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if get(hObject,'Value') == 1 && get(handles.OKuseDataPixx,'Value') == 1 - set(handles.OKuseDataPixx,'Value', 0) - end - o.getScreenVals; -end - -% --- Executes on button press in OKuseEyeLink. -function OKuseEyeLink_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - - -% --- Executes on button press in OKVerbose. -function OKVerbose_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -% --- Executes on button press in OKlogFrames. -function OKlogFrames_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen,'screenManager') - if get(hObject,'Value') == 1 - set(handles.OKbenchmark,'Value',0) - end - o.getScreenVals; - end -end - -% --- Executes on button press in OKOpenGLBlending. -function OKOpenGLBlending_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -% --- Executes on button press in OKHideFlash. -function OKHideFlash_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -% --- Executes on button press in OKDebug. -function OKDebug_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKbackgroundColour_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKNativeBeamPosition_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKrecordMovie_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getScreenVals; -end - -function OKnBlocks_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -function OKisTime_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -function OKtrialTime_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -function OKrealTime_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -% -------------------------------------------------------------------- -function OKMenuNoiseTexture_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuNoiseTexture (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.store.visibleStimulus='dots'; - -end - -% -------------------------------------------------------------------- -function OKMenuLineTexture_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuLineTexture (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% -------------------------------------------------------------------- -function OKMenuTargetInducer_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSpot (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus'); - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - - if ~isfield(o.store,'targetInducerStimulus') - o.store.targetInducerStimulus=targetInducerStimulus('name','Target-Inducer Stimulus'); - end - o.store.visibleStimulus = o.store.targetInducerStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuDots_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSpot (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus'); - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - - if ~isfield(o.store,'dotsStimulus') - o.store.dotsStimulus=dotsStimulus('name', 'Coherent Dots Stimulus',... - 'speed', 2, 'colour', [1 1 1]); - end - o.store.dotsStimulus.speed = 2; - o.store.visibleStimulus = o.store.dotsStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuNDots_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuNDots (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus'); - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - - if ~isfield(o.store,'ndotsStimulus') - o.store.ndotsStimulus=ndotsStimulus('name','Newsome Dots Stimulus',... - 'speed', 2, 'colour', [1 1 1]); - end - o.store.ndotsStimulus.speed = 2; - o.store.visibleStimulus = o.store.ndotsStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuBar_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuBar (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus'); - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - - if ~isfield(o.store,'barStimulus') - o.store.barStimulus=barStimulus('name','Bar Stimulus'); - end - o.store.visibleStimulus = o.store.barStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuGrating_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuGrating (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus'); - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - - if ~isfield(o.store,'gratingStimulus') - o.store.gratingStimulus=gratingStimulus('name','Grating Stimulus'); - end - o.store.visibleStimulus = o.store.gratingStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - -% -------------------------------------------------------------------- -function OKMenuGabor_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuGabor (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus'); - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - - if ~isfield(o.store,'gaborStimulus') - o.store.gaborStimulus=gaborStimulus('name','Gabor Stimulus'); - end - o.store.visibleStimulus = o.store.gaborStimulus; - set(handles.OKPanelStimulusText,'String','') - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); -end - -% -------------------------------------------------------------------- -function OKMenuSpot_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSpot (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus'); - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - - if ~isfield(o.store,'spotStimulus') - o.store.spotStimulus=spotStimulus('name','Spot Stimulus'); - end - o.store.visibleStimulus = o.store.spotStimulus; - set(handles.OKPanelStimulusText,'String','') - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); -end - -% -------------------------------------------------------------------- -function OKMenuTexture_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuTexture (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - - if isfield(o.store,'visibleStimulus'); - o.store.visibleStimulus.closePanel(); - o.store = rmfield(o.store,'visibleStimulus'); - end - - set(handles.OKPanelStimulusText,'String','Loading Stimulus Panel...'); drawnow - - if ~isfield(o.store,'textureStimulus') - o.store.textureStimulus=textureStimulus('name','Picture / Texture Stimulus'); - end - o.store.visibleStimulus = o.store.textureStimulus; - o.store.visibleStimulus.makePanel(handles.OKPanelStimulus); - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKPanelStimulusText,'String','') -end - - -% -------------------------------------------------------------------- -function OKMenuPreferences_Callback(hObject, eventdata, handles) - - -function OKProtocolsList_Callback(hObject, eventdata, handles) - - -function OKHistoryList_Callback(hObject, eventdata, handles) - - -% --- Executes on button press in OKProtocolLoad. -function OKProtocolLoad_Callback(hObject, eventdata, handles) -% hObject handle to OKProtocolLoad (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('loadProtocol'); -end - -% --- Executes on button press in OKProtocolSave. -function OKProtocolSave_Callback(hObject, eventdata, handles) -% hObject handle to OKProtocolSave (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('saveProtocol'); -end - -% --- Executes on button press in OKProtocolDuplicate. -function OKProtocolDuplicate_Callback(hObject, eventdata, handles) -% hObject handle to OKProtocolDuplicate (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('duplicateProtocol'); -end - -% --- Executes on button press in OKProtocolDelete. -function OKProtocolDelete_Callback(hObject, eventdata, handles) -% hObject handle to OKProtocolDelete (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('deleteProtocol'); -end - -% -------------------------------------------------------------------- -function OKMenuCalibrateLuminance_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuCalibrateLuminance (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v=get(handles.OKSelectScreen,'Value'); - inp = struct('screen',v-1); - if isa(o.r.screen.gammaTable,'calibrateLuminance') - o.r.screen.gammaTable.run; - else - c = calibrateLuminance(inp); - o.r.screen.gammaTable = c; - c.filename = [o.paths.calibration filesep 'Cal-' date '-' c.comments]; - o.saveCalibration; - end - set(handles.OKUseGamma,'Value',1) - o.r.screen.gammaTable.choice = 0; - set(handles.OKUseGamma,'String',['None'; 'Gamma'; o.r.screen.gammaTable.analysisMethods]); -end - -% -------------------------------------------------------------------- -function OKMenuLoadGamma_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuLoadGamma (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - uiopen('~/OptickaFiles/Calibration') - if exist('tmp','var') && isa(tmp,'calibrateLuminance') - o.r.screen.gammaTable = tmp; - clear tmp; - if get(handles.OKUseGamma,'Value') > length(['None'; 'Gamma'; o.r.screen.gammaTable.analysisMethods]) - set(handles.OKUseGamma,'Value',1); - o.r.screen.gammaTable.choice = 0; - else - o.r.screen.gammaTable.choice = get(handles.OKUseGamma,'Value')-1; - end - set(handles.OKUseGamma,'String',['None'; 'Gamma'; o.r.screen.gammaTable.analysisMethods]); - end -end - -% -------------------------------------------------------------------- -function OKMenuSaveGamma_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSaveGamma (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen.gammaTable,'calibrateLuminance') - tmp=o.r.screen.gammaTable; - uisave('tmp',[o.paths.calibration filesep 'Calibration-' date '-' o.r.screen.gammaTable.comments]); - end -end - -% -------------------------------------------------------------------- -function OKMenuCalibrateSize_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuCalibrateSize (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -v=get(handles.OKSelectScreen,'Value'); -s=str2num(get(handles.OKMonitorDistance,'String')); -[~,dpc]=calibrateSize(v-1,s); -set(handles.OKpixelsPerCm,'String',num2str(dpc)); - - -function OKitTime_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -function OKRandomise_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end -% --- Executes on selection change in OKrandomGenerator. -function OKrandomGenerator_Callback(hObject, eventdata, handles) -% hObject handle to OKrandomGenerator (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -% Hints: contents = cellstr(get(hObject,'String')) returns OKrandomGenerator contents as cell array -% contents{get(hObject,'Value')} returns selected item from OKrandomGenerator -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -function OKRandomSeed_Callback(hObject, eventdata, handles) -% hObject handle to OKRandomSeed (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -% Hints: get(hObject,'String') returns contents of OKRandomSeed as text -% str2double(get(hObject,'String')) returns contents of OKRandomSeed as a double -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -% --- Executes on button press in OKHistoryUp. -function OKHistoryUp_Callback(hObject, eventdata, handles) -% hObject handle to OKHistoryUp (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% --- Executes on button press in OKHistoryDown. -function OKHistoryDown_Callback(hObject, eventdata, handles) -% hObject handle to OKHistoryDown (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% --- Executes on button press in OKHistoryDelete. -function OKHistoryDelete_Callback(hObject, eventdata, handles) -% hObject handle to OKHistoryDelete (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% --- Executes on selection change in OKVariableList. -function OKVariableList_Callback(hObject, eventdata, handles) -% hObject handle to OKVariableList (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: contents = cellstr(get(hObject,'String')) returns OKVariableList contents as cell array -% contents{get(hObject,'Value')} returns selected item from OKVariableList - -% --- Executes on button press in OKCopyVariableName. -function OKCopyVariableName_Callback(hObject, eventdata, handles) -% hObject handle to OKCopyVariableName (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -string = get(handles.OKVariableList,'String'); -value = get(handles.OKVariableList,'Value'); -string=string{value}; -set(handles.OKVariableName,'String',string); - -function OKCopyVariableNameValues_Callback(hObject, eventdata, handles) -string = get(handles.OKVariableList,'String'); -value = get(handles.OKVariableList,'Value'); -string=string{value}; -set(handles.OKVariableName,'String',string); - -switch string - case 'angle' - string = num2str(-90:45:90); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'direction' - string = num2str(-90:45:90); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'phase' - string = num2str(0:22.5:180); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'size' - string = num2str([0 0.1 0.2 0.35 0.5 0.75 1 2 4 6 8]); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'contrast' - string = num2str(0:0.1:1); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'sf' - string = num2str([0 0.1 0.5 0.7 1 1.5 2 3 4 5 6]); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'tf' - string = num2str([0.5 1 2 3 4 5]); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'xPosition' - string = num2str(-1:0.2:1); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); - case 'yPosition' - string = num2str(-1:0.2:1); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); -end - - - -% --- Executes on button press in OKVariablesLinear. -function OKVariablesLinear_Callback(hObject, eventdata, handles) -values = str2num(get(handles.OKVariableValues,'String')); -if length(values) == 3 - values=linspace(values(1),values(2),values(3)); - string = num2str(values); -else - string = num2str(values); - string = regexprep(string,'\s+',' '); %collapse spaces -end -set(handles.OKVariableValues,'String',string); - -% --- Executes on button press in OKVariablesLog. -function OKVariablesLog_Callback(hObject, eventdata, handles) %#ok<*INUSD> -values = str2num(get(handles.OKVariableValues,'String')); -if length(values) == 3 - values=logspace(log10(values(1)),log10(values(2)),values(3)); - string = num2str(values); - string = regexprep(string,'\s+',' '); %collapse spaces - set(handles.OKVariableValues,'String',string); -else - warndlg('Please enter a minimum, maximum and number of values'); -end - -% --- Executes on button press in OKAddStimulus. -function OKAddStimulus_Callback(hObject, eventdata, handles) -% hObject handle to OKAddStimulus (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isfield(o.store,'visibleStimulus') - o.store.visibleStimulus.readPanel(); - o.r.stimuli{o.r.stimuli.n+1} = o.store.visibleStimulus.clone(); - %o.r.stimuli{o.r.stimuli.n}.name = 'Stimulus'; - o.addStimulus; - if o.r.stimuli.n > 0 - set(handles.OKAddStimulus,'Enable','on'); - set(handles.OKDeleteStimulus,'Enable','on'); - set(handles.OKModifyStimulus,'Enable','on'); - set(handles.OKCopyStimulus,'Enable','on'); - set(handles.OKStimulusUp,'Enable','on'); - set(handles.OKStimulusDown,'Enable','on'); - set(handles.OKStimulusRun,'Enable','on'); - set(handles.OKStimulusRunBenchmark,'Enable','on'); - set(handles.OKStimulusRunAll,'Enable','on'); - set(handles.OKStimulusRunAllBenchmark,'Enable','on'); - set(handles.OKStimulusRunSingle,'Enable','on'); - end - end -end - -% --- Executes on button press in OKDeleteStimulus. -function OKDeleteStimulus_Callback(hObject, eventdata, handles) -% hObject handle to OKDeleteStimulus (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.deleteStimulus; - if o.r.stimuli.n == 0 - set(handles.OKAddStimulus,'Enable','off'); - set(handles.OKDeleteStimulus,'Enable','off'); - set(handles.OKModifyStimulus,'Enable','off'); - set(handles.OKCopyStimulus,'Enable','off'); - set(handles.OKStimulusUp,'Enable','off'); - set(handles.OKStimulusDown,'Enable','off'); - set(handles.OKStimulusRun,'Enable','off'); - set(handles.OKStimulusRunBenchmark,'Enable','off'); - set(handles.OKStimulusRunAll,'Enable','off'); - set(handles.OKStimulusRunAllBenchmark,'Enable','off'); - set(handles.OKStimulusRunSingle,'Enable','off'); - end -end - -% --- Executes on button press in OKCopyStimulus. -function OKCopyStimulus_Callback(hObject, eventdata, handles) -% hObject handle to OKCopyStimulus (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKStimList,'Value'); - o.r.stimuli{o.r.stimuli.n+1} = o.r.stimuli{v}.clone; - o.r.stimuli{o.r.stimuli.n}.name = ['Stimulus #' num2str(o.r.stimuli.n)]; - o.addStimulus; -end - -% --- Executes on selection change in OKStimList. -function OKStimList_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.editStimulus; -end - -% --- Executes on button press in OKStimulusDown. -function OKStimulusDown_Callback(hObject, eventdata, handles) %#ok<*INUSL> -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - value = get(handles.OKStimList,'Value'); %where are we in the stimulus list - slength = o.r.stimuli.n; - if value < slength - idx = 1:slength; - idx2 = idx; - idx2(value) = idx2(value)+1; - idx2(value+1) = idx2(value+1)-1; - o.r.stimuli(idx) = o.r.stimuli(idx2); - set(handles.OKStimList,'Value',value+1); - o.modifyStimulus; - end -end - -% --- Executes on button press in OKStimulusUp. -function OKStimulusUp_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - value = get(handles.OKStimList,'Value'); %where are we in the stimulus list - slength = o.r.stimuli.n; - if value > 1 - idx = 1:slength; - idx2 = idx; - idx2(value) = idx2(value)-1; - idx2(value-1) = idx2(value-1)+1; - o.r.stimuli(idx) = o.r.stimuli(idx2); - set(handles.OKStimList,'Value',value-1); - o.modifyStimulus; - end -end - -% --- Executes on button press in OKStimulusRun. -function OKStimulusRun_Callback(hObject, eventdata, handles) -% hObject handle to OKStimulusRun (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKStimList,'Value'); - if v > 0 && isobject(o.r.stimuli{v}) - run(o.r.stimuli{v}, false, [], o.r.screen); - end - set(handles.OKStimulusRunAll,'Enable','on'); -end - -% --- Executes on button press in OKStimulusRunBenchmark. -function OKStimulusRunBenchmark_Callback(hObject, eventdata, handles) -% hObject handle to OKStimulusRunBenchmark (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKStimList,'Value'); - if v > 0 && isa(o.r.stimuli{v},'baseStimulus') - run(o.r.stimuli{v}, true, [], o.r.screen); - end -end - -% --- Executes on button press in OKStimulusRunAll. -function OKStimulusRunAll_Callback(hObject, eventdata, handles) -% hObject handle to OKStimulusRunAll (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.stimuli,'metaStimulus') && o.r.stimuli.n > 0 - o.r.stimuli.choice = []; - if isa(o.r.screen,'screenManager') - run(o.r.stimuli, [], [], o.r.screen); - else - run(o.r.stimuli); - end - end -end - -% --- Executes on button press in OKStimulusRunAllBenchmark. -function OKStimulusRunAllBenchmark_Callback(hObject, eventdata, handles) -% hObject handle to OKStimulusRunAllBenchmark (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.stimuli,'metaStimulus') && o.r.stimuli.n > 0 - o.r.stimuli.choice = []; - if isa(o.r.screen,'screenManager') - run(o.r.stimuli, true, 5, o.r.screen); - else - run(o.r.stimuli, true); - end - end -end - -% --- Executes on button press in OKStimulusRunSingle. -function OKStimulusRunSingle_Callback(hObject, eventdata, handles) -% hObject handle to OKStimulusRunSingle (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.stimuli,'metaStimulus') && o.r.stimuli.n > 0 - o.r.stimuli.choice = []; - runSingle(o.r.stimuli, o.r.screen); - end -end - - -% --- Executes on button press in OKInspectStimulus. -function OKInspectStimulus_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKStimList,'Value'); - if v > 0; - uiinspect(o.r.stimuli{v}); - end -end - -% --- Executes on button press in OKInspectVariable. -function OKInspectVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKVarList,'Value'); - if v > 0; - uiinspect(o.r.task.nVar(v)); - end -end - - -% --- Executes on button press in OKModifyStimulus. -function OKModifyStimulus_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - v = get(handles.OKStimList,'Value'); - if v > 0; - seditor(o.r.stimuli{v}, handles.output); - end - -end - -% --- Executes on button press in OKAddVariable. -function OKAddVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.addVariable; - if o.r.task.nVars > 0 - set(handles.OKDeleteVariable,'Enable','on'); - set(handles.OKCopyVariable,'Enable','on'); - set(handles.OKEditVariable,'Enable','on'); - end -end - -% --- Executes on button press in OKDeleteVariable. -function OKDeleteVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.deleteVariable; - if o.r.task.nVars < 1 - set(handles.OKDeleteVariable,'Enable','off'); - set(handles.OKCopyVariable,'Enable','off'); - set(handles.OKEditVariable,'Enable','off'); - end -end - -function OKCopyVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.copyVariable; -end - -function OKEditVariable_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.editVariable; -end - -function OKVariableOffset_Callback(hObject, eventdata, handles) -% hObject handle to OKVariableOffset (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKVariableOffset as text -% str2double(get(hObject,'String')) returns contents of OKVariableOffset -% as a double - -% -------------------------------------------------------------------- -function OKToolbarRun_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarRun (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if ~isempty(o.oc) && o.oc.isOpen == 1 && o.r.useLabJack == 1 - o.oc.write('--GO!--'); - pause(0.5); - end - drawnow; - o.r.uiCommand='run'; - o.r.run; -end - -% -------------------------------------------------------------------- -function OKToolbarStop_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarStop (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.uiCommand='stop'; - drawnow; -end - -% -------------------------------------------------------------------- -function OKToolbarAbort_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarAbort (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.uiCommand='abort'; - if isa(o.oc,'dataConnection') - o.oc.write('--abort--'); - end - drawnow; -end - -function OKRemoteIP_Callback(hObject, eventdata, handles) -% hObject handle to OKRemoteIP (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKRemoteIP as text -% str2double(get(hObject,'String')) returns contents of OKRemoteIP as a double -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - - -function OKRemotePort_Callback(hObject, eventdata, handles) -% hObject handle to OKRemotePort (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKRemotePort as text -% str2double(get(hObject,'String')) returns contents of OKRemotePort as a double -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getTaskVals; -end - -% -------------------------------------------------------------------- -function OKToolbarToggleRemote_OnCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarToggleRemote (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - eval(o.store.serverCommand) -end - - -% -------------------------------------------------------------------- -function OKMenumanageCode_Callback(hObject, eventdata, handles) -% hObject handle to OKMenumanageCode (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -manageCode - - -% -------------------------------------------------------------------- -function OKMenurfMapperLog_Callback(hObject, eventdata, handles) -% hObject handle to OKMenurfMapperLog (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -function OKSettingsmovieSize_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.screen.movieSettings.size=str2num(get(hObject,'String')); -end - -function OKSettingsmovieFrames_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.screen.movieSettings.nFrames=str2num(get(hObject,'String')); %#ok<*ST2NM> -end - -function OKSettingsmoviePrecision_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.screen.movieSettings.quality=get(hObject,'Value'); -end - -function OKSettingsmovieType_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.screen.movieSettings.type=get(hObject,'Value'); -end - -function OKSettingsmovieCodec_Callback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r.screen.movieSettings.codec=get(hObject,'String'); -end - -% -------------------------------------------------------------------- -function OKPanelTellOmniplex_ClickedCallback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - resp = questdlg('Is opxOnline running on the Omniplex machine?','Check OPX','No'); - if strcmpi(resp,'Yes') - o.connectToOmniplex - end -end - -% -------------------------------------------------------------------- -function OKPanelReconnectOmniplex_ClickedCallback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.oc,'dataConnection') - o.oc.closeAll; - o.connectToOmniplex; - end -end - -% -------------------------------------------------------------------- -function OKPanelSendOmniplex_ClickedCallback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.oc,'dataConnection') - o.sendOmniplexStimulus; - end -end - -% -------------------------------------------------------------------- -function OKPanelDisconnectOmniplex_ClickedCallback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.oc,'dataConnection') - o.disconnectOmniplex; - end -end - -% -------------------------------------------------------------------- -function OKPanelPingOmniplex_ClickedCallback(hObject, eventdata, handles) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - rAddress = get(handles.OKOmniplexIP,'String'); - status = o.ping(rAddress); - if status > 0 - set(o.h.OKOmniplexStatus,'String','Omniplex: machine ping ERROR!') - else - if isa(o.oc,'dataConnection') && o.oc.isOpen == 1 - o.oc.write('--ping--'); - loop = 1; - while loop < 8 - in = o.oc.read(0); - fprintf('\n{opticka said: %s}\n',in) - if regexpi(in,'ping') - fprintf('\nWe can ping omniplex master on try: %d\n',loop) - set(handles.OKOmniplexStatus,'String','Omniplex: connected and pinged') - break - else - fprintf('\nOmniplex master not responding, try: %d\n',loop) - set(handles.OKOmniplexStatus,'String','Omniplex: not responding') - end - loop=loop+1; - pause(0.1); - end - end - end -end - - -function OKverbosityLevel_Callback(hObject, eventdata, handles) -% hObject handle to OKverbosityLevel (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKverbosityLevel as text -% str2double(get(hObject,'String')) returns contents of OKverbosityLevel as a double -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen,'screenManager') - o.r.screen.verbosityLevel = str2double(get(hObject,'String')); - end -end - - -% --- Executes on button press in OKbenchmark. -function OKbenchmark_Callback(hObject, eventdata, handles) -% hObject handle to OKbenchmark (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -% Hint: get(hObject,'Value') returns toggle state of OKbenchmark -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r.screen,'screenManager') - if get(hObject,'Value') == 1 - set(handles.OKlogFrames,'Value',0) - end - o.getScreenVals; - end -end - -% --- Executes on button press in OKOmniplexEnable. -function OKOmniplexEnable_Callback(hObject, eventdata, handles) -% hObject handle to OKOmniplexEnable (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -% Hint: get(hObject,'Value') returns toggle state of OKOmniplexEnable -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if get(hObject,'Value') < 1 - set(handles.OKOmniplexIP,'Enable','off'); - set(handles.OKOmniplexPort,'Enable','off'); - set(handles.OKPanelTellOmniplex,'Enable','off'); - set(handles.OKPanelReconnectOmniplex,'Enable','off'); - set(handles.OKPanelSendOmniplex,'Enable','off'); - set(handles.OKPanelPingOmniplex,'Enable','off'); - set(handles.OKPanelDisconnectOmniplex,'Enable','off'); - else - set(handles.OKOmniplexIP,'Enable','on'); - set(handles.OKOmniplexPort,'Enable','on'); - set(handles.OKPanelTellOmniplex,'Enable','on'); - set(handles.OKPanelReconnectOmniplex,'Enable','on'); - set(handles.OKPanelSendOmniplex,'Enable','on'); - set(handles.OKPanelPingOmniplex,'Enable','on'); - set(handles.OKPanelDisconnectOmniplex,'Enable','on'); - end -end - -% -------------------------------------------------------------------- -function OKMenuOpen_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuOpen (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('loadProtocol','1'); -end - -% -------------------------------------------------------------------- -function OKMenuSave_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuSave (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('saveProtocol'); -end - -% -------------------------------------------------------------------- -function OKMenuQuit_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuQuit (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - try - o = getappdata(handles.output,'o'); - o.savePrefs; - rmappdata(handles.output,'o'); - clear o; - catch ME - fprintf('>>> Opticka failed to clear all data on close...'); - rethrow ME - end -end -close(gcf); - -% -------------------------------------------------------------------- -function OKMenuNewProtocol_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuNewProtocol (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r = []; - o.store.evnt = []; - o.store = rmfield(o.store,'evnt'); - o.store.visibleStimulus=[]; - o.store = rmfield(o.store,'visibleStimulus'); - o.clearStimulusList; - o.clearVariableList; - o.getScreenVals; - o.getTaskVals; -end - -% -------------------------------------------------------------------- -function OKToolbarInitialise_ClickedCallback(hObject, eventdata, handles) %#ok<*DEFNU> -% hObject handle to OKToolbarInitialise (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.r = []; - o.store.evnt = []; - o.store = rmfield(o.store,'evnt'); - o.store.visibleStimulus=[]; - o.store = rmfield(o.store,'visibleStimulus'); - o.clearStimulusList; - o.clearVariableList; - o.getScreenVals; - o.getTaskVals; -end - -% -------------------------------------------------------------------- -function OKToolbarOpen_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarOpen (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('loadProtocol','1'); -end - -% -------------------------------------------------------------------- -function OKToolbarSave_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarSave (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('saveProtocol'); -end - - -% -------------------------------------------------------------------- -function OKToolbarDefinePath_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToolbarDefinePath (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - path = uigetdir; - if ischar(path) && exist(path,'dir') - initialiseSave(o, path); - if isa(o.r,'runExperiment'); - initialiseSave(o.r, path); - end - end -end - - -function OKTrainingName_Callback(hObject, eventdata, handles) -% hObject handle to OKTrainingName (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKTrainingName as text -% str2double(get(hObject,'String')) returns contents of OKTrainingName as a double - - -% --- Executes during object creation, after setting all properties. -function OKTrainingName_CreateFcn(hObject, eventdata, handles) -% hObject handle to OKTrainingName (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles empty - handles not created until after all CreateFcns called - -% Hint: edit controls usually have a white background on Windows. -% See ISPC and COMPUTER. -if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) - set(hObject,'BackgroundColor','white'); -end - - -% -------------------------------------------------------------------- -function OKToggleUI_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKToggleUI (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if strcmp(get(handles.OKPanelGlobal,'Visible'),'on') - - set(handles.OKPanelGlobal,'Visible','off'); - set(handles.OKPanelProtocols,'Visible','on'); - set(handles.OKPanelTraining,'Visible','off'); - -elseif strcmp(get(handles.OKPanelProtocols,'Visible'),'on') - - set(handles.OKPanelProtocols,'Visible','off') - set(handles.OKPanelGlobal,'Visible','off') - set(handles.OKPanelTraining,'Visible','on') - if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getStateInfo(); - end - -else - - set(handles.OKPanelProtocols,'Visible','off') - set(handles.OKPanelGlobal,'Visible','on') - set(handles.OKPanelTraining,'Visible','off') - -end - -% --- Executes on button press in OKEditStateFileButon. -function OKEditStateFileButon_Callback(hObject, eventdata, handles) -% hObject handle to OKEditStateFileButon (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if exist(o.r.paths.stateInfoFile,'file') - edit(o.r.paths.stateInfoFile); - end - o.getStateInfo(); -end - -function OKTrainingResearcherName_Callback(hObject, eventdata, handles) -% hObject handle to OKTrainingResearcherName (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of OKTrainingResearcherName as text -% str2double(get(hObject,'String')) returns contents of OKTrainingResearcherName as a double - -% -------------------------------------------------------------------- -function OKMenuStateInfo_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuStateInfo (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('LoadStateInfo'); - o.getStateInfo(); -end - -% --- Executes on button press in OKLoadStateButton. -function OKLoadStateButton_Callback(hObject, eventdata, handles) -% hObject handle to OKLoadStateButton (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.router('LoadStateInfo'); - o.getStateInfo(); -end - -% --- Executes on button press in OKRefreshStateInfo. -function OKRefreshStateInfo_Callback(hObject, eventdata, handles) -% hObject handle to OKRefreshStateInfo (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.getStateInfo(); -end - -% -------------------------------------------------------------------- -function OKRFMapper_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKRFMapper (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.store.rfLog = []; - rf=rfMapper; - rf.run(o.r); - o.store.rfLog = rf; - clear rf; -end - -% -------------------------------------------------------------------- -function OKRunRFMapper_Callback(hObject, eventdata, handles) -% hObject handle to OKRunRFMapper (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - o.store.rfLog = []; - rf=rfMapper; - rf.run(o.r); - o.store.rfLog = rf; - clear rf; -end - -% -------------------------------------------------------------------- -function OKRunTrainingSession_Callback(hObject, eventdata, handles) -% hObject handle to OKRunTrainingSession (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - - -% -------------------------------------------------------------------- -function OKRunExperiment_Callback(hObject, eventdata, handles) -% hObject handle to OKRunExperiment (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% -------------------------------------------------------------------- -function OKStartTask_ClickedCallback(hObject, eventdata, handles) -% hObject handle to OKStartTask (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) -if isappdata(handles.output,'o') - o = getappdata(handles.output,'o'); - if isa(o.r,'runExperiment') - %o.r.screenSettings.optickahandle = handles.output; - initialiseSave(o.r, o.paths.savedData) - if ~isempty(regexp(o.comment, '^Protocol','once')) - o.r.comment = o.comment; - end - runTask(o.r); - end -end - - -% -------------------------------------------------------------------- -function OKMenuIO_Callback(hObject, eventdata, handles) -% hObject handle to OKMenuIO (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) diff --git a/ui/legacy/opticka_ui_guide_App_report.html b/ui/legacy/opticka_ui_guide_App_report.html deleted file mode 100644 index 0595a18dadd668d5d5d5fc747b7decb01ceeb60a..0000000000000000000000000000000000000000 --- a/ui/legacy/opticka_ui_guide_App_report.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - Migration Report for <code>opticka_ui_guide</code> - - - -
-

Migration Report for opticka_ui_guide

-
-
- - - \ No newline at end of file diff --git a/ui/legacy/opticka_ui_old.mlapp b/ui/legacy/opticka_ui_old.mlapp deleted file mode 100644 index 4ebd019ffba53d8d3731d47bb4fc258bd544f195..0000000000000000000000000000000000000000 Binary files a/ui/legacy/opticka_ui_old.mlapp and /dev/null differ diff --git a/ui/menuN.m b/ui/menuN.m index 0ce0550ee3d1f4534d548fcb79b0d754f409b66b..99a227026c367cd164150d276f7efd9a77934c59 100644 --- a/ui/menuN.m +++ b/ui/menuN.m @@ -144,22 +144,54 @@ function choice = menuN(mtitle, options, Opt) % -Added support for a message under the title to mimic the behaviour of % menu(). Set mtitle [string] -> mtitle {cell 1x2} where {'title','message'} +lf = listfonts; %#ok<*PROPLC> +if ismac + SansFont = 'Avenir Next'; + MonoFont = 'Menlo'; +elseif ispc + SansFont = 'Calibri'; + MonoFont = 'Consolas'; +else %linux + SansFont = 'Ubuntu'; + MonoFont = 'Ubuntu Mono'; +end +if any(matches(lf,'Source Sans 3')) + SansFont = 'Source Sans 3'; +end +if any(matches(lf,'Fira Code')) + MonoFont = 'Fira Code'; +end + +fontSize = 9; +buttonFontSize = 10; +hidpi = false; + +if IsLinux && isMATLABReleaseOlderThan('R2024b') + try + [~,xr]=system('xrandr --screen 0 | grep -E ''\*'''); + if any(contains(xr,{'3840','4384','5120','5760','6144'})) + hidpi = true; + fontSize = 22; + buttonFontSize = 24; + end + end +end %% Set up default Opt struct: defOpt = struct(); -defOpt.fontName = 'Ubuntu Mono'; -defOpt.subtitleFontSize = 14; -defOpt.pushbuttonFontSize = 14; -defOpt.popupmenuFontSize = 12; -defOpt.radiobuttonFontSize = 13; -defOpt.checkboxFontSize = 13; -defOpt.listboxFontSize = 12; -defOpt.checkboxFontSize = 13; -defOpt.sliderFontSize = 12; +defOpt.fontName = SansFont; +defOpt.subtitleFontSize = buttonFontSize; +defOpt.pushbuttonFontSize = fontSize; +defOpt.popupmenuFontSize = fontSize; +defOpt.radiobuttonFontSize = fontSize; +defOpt.checkboxFontSize = fontSize; +defOpt.listboxFontSize = fontSize; +defOpt.checkboxFontSize = fontSize; +defOpt.sliderFontSize = fontSize; defOpt.sliderStepsFraction = [0.01,0.1]; defOpt.okButtonLabel = 'OK'; -defOpt.pixelHeigthUIcontrol = 25; -defOpt.pixelPaddingHeigth = [8, 5]; % [bottom/top, between uicontrols] +defOpt.pixelHeigthUIcontrol = 30; +defOpt.pixelPaddingHeigth = [5, 5]; % [bottom/top, between uicontrols] defOpt.pixelPaddingWidth = [6, 4]; % [bottom/top, between uicontrols] defOpt.InsteadOfPushUse = 'p'; % p = popupmenu, r = radiobuttons defOpt.cancelButton = true; @@ -192,31 +224,36 @@ else end % Set up a large amount of padding options []: -extentWidthUniversal = 200; % It is increased/decreased when necessary -extentWidthUniversalMin = 195; % Minimum allowed width of uicontrols +if hidpi + extentWidthUniversal = 900; % It is increased/decreased when necessary + extentWidthUniversalMin = 840; % Minimum allowed width of uicontrols +else + extentWidthUniversal = 450; % It is increased/decreased when necessary + extentWidthUniversalMin = 420; % Minimum allowed width of uicontrols +end -extentWidthSliderMin = 100; -extentHeigthTextInPadding = 0; -extentHeigthTextPadding = 2; +extentWidthSliderMin = 200; +extentHeigthTextInPadding = 5; +extentHeigthTextPadding = 5; extentWidthTextInPadding = 40; extentHeigthPushbuttonPadding = 4; -extentHeigthPushbuttonInPadding = 2; +extentHeigthPushbuttonInPadding = 12; extentWidthPushbuttonInPadding = 10; extentWidthPopupmenuInPadding = 12; -extentHeightPopupmenuPadding = 0; +extentHeightPopupmenuPadding = 10; extentHeigthCheckboxInPadding = 2; -extentHeigthCheckboxPadding = 1; +extentHeigthCheckboxPadding = 11; extentWidthCheckboxInPadding = 20; extentHeigthRadiobuttonInPadding = 2; -extentHeigthRadiobuttonPadding = 1; +extentHeigthRadiobuttonPadding = 11; extentWidthRadiobuttonInPadding = 20; -extentWidthGroupPadding = 15; -extentHeightTitlePadding = -Opt.pixelPaddingHeigth(2); +extentWidthGroupPadding = 10; +extentHeightTitlePadding = Opt.pixelPaddingHeigth(2); %% Check mTitle input: if ~iscell(mtitle) @@ -233,7 +270,7 @@ end hFig = figure('Name',mtitle{1},'Toolbar','none','Menubar','none','NumberTitle','off'); % Set initial necessary width that all uicontrols must be [pixels]: -tmpMinimumSize = [extentWidthUniversalMin, 15]; +tmpMinimumSize = [extentWidthUniversalMin, 24]; % Assume that a OK button is necessary: flagMakeOkButton = true; @@ -271,7 +308,7 @@ tmpCurrentPosition = [Opt.pixelPaddingWidth(1), Opt.pixelPaddingHeigth(1)]; %% Print OK & cancel button if flagMakeOkButton - tmpPosition = [tmpCurrentPosition, tmpMinimumSize]; + tmpPosition = [tmpCurrentPosition, tmpMinimumSize(1)/2 tmpMinimumSize(2)]; hOK = uicontrol( ... 'Style', 'Pushbutton',... 'String', Opt.okButtonLabel,... @@ -283,7 +320,7 @@ if flagMakeOkButton tmpExtent = get(hOK,'extent'); extentWidthUniversal = max(extentWidthUniversal, ... tmpExtent(3)+extentWidthPushbuttonInPadding); - tmpNewSize = [extentWidthUniversal, ... + tmpNewSize = [extentWidthUniversal/2, ... tmpExtent(4) + extentHeigthPushbuttonInPadding]; % Update size of uicontrol: set(hOK,'Position',[tmpCurrentPosition,tmpNewSize]); @@ -304,7 +341,7 @@ if flagMakeOkButton tmpExtent = get(hCancel,'extent'); extentWidthUniversal = max(extentWidthUniversal, ... tmpExtent(3)+extentWidthPushbuttonInPadding); - tmpNewSize = [extentWidthUniversal, ... + tmpNewSize = [extentWidthUniversal/2, ... tmpExtent(4) + extentHeigthPushbuttonInPadding]; % Update size of uicontrol: set(hCancel,'Position',[tmpCurrentPosition,tmpNewSize]); @@ -315,7 +352,7 @@ if flagMakeOkButton tmpCurrentPosition(2) = tmpCurrentPosition(2) + tmpNewSize(2) + ... extentHeigthPushbuttonPadding + Opt.pixelPaddingHeigth(2); elseif flagMakeOnlyCancelButton && Opt.cancelButton - tmpPosition = [tmpCurrentPosition, tmpMinimumSize]; + tmpPosition = [tmpCurrentPosition, tmpMinimumSize(1)/2 tmpMinimumSize(2)]; hCancel = uicontrol( ... 'Style', 'Pushbutton',... 'String', Opt.cancelButtonLabel,... @@ -518,9 +555,9 @@ for idxOptions = numOptionsGroups:-1:1 tmpExtentWidthMaxChild = max(tmpExtentWidthMaxChild, ... tmpExtent(3)+extentWidthRadiobuttonInPadding); tmpNewSize = [tmpExtentWidthMaxChild, ... - tmpExtent(4) + extentHeigthRadiobuttonInPadding]; + tmpExtent(4) + 10]; % Update size of uicontrol: - set(hObjects{idxObjects},'Position',[tmpCurrentPositionChild,tmpNewSize]); + set(hObjects{idxObjects},'Position',[tmpCurrentPositionChild,410,24]); % Update current position Y coordinate: tmpCurrentPositionChild(2) = tmpCurrentPositionChild(2) + tmpNewSize(2) + ... extentHeigthRadiobuttonPadding + Opt.pixelPaddingHeigth(2); @@ -627,9 +664,9 @@ for idxOptions = numOptionsGroups:-1:1 tmpExtent = get(hOptions{idxOptions},'extent'); extentWidthUniversal = max(extentWidthUniversal, ... tmpExtent(3)+extentWidthPopupmenuInPadding); - tmpNewSize = [extentWidthUniversal, tmpExtent(4)]; + tmpNewSize = [extentWidthUniversal, 22]; % Update size of uicontrol: - set(hOptions{idxOptions},'Position',[tmpCurrentPosition,tmpNewSize]); + set(hOptions{idxOptions},'Position',[tmpCurrentPosition,extentWidthUniversal, 22]); % Update current position Y coordinate: tmpCurrentPosition(2) = tmpCurrentPosition(2) + tmpNewSize(2) + ... extentHeightPopupmenuPadding + Opt.pixelPaddingHeigth(2); @@ -878,6 +915,9 @@ end screenSize = get(0,'ScreenSize'); figureSize = [extentWidthUniversal + 2*Opt.pixelPaddingWidth(1),... tmpCurrentPosition(2) + Opt.pixelPaddingHeigth(1) + extentHeightTitlePadding]; +if screenSize(3) > 2200 + figureSize = figureSize*2.5; +end figurePosition = 0.5*screenSize([3,4]) - 0.5*figureSize; set(hFig,'Position',[figurePosition,figureSize]); diff --git a/ui/opticka_ui.mlapp b/ui/opticka_ui.mlapp index 0e91ecbc34417757a53c6ec138590743badbcdae..004d70550e37bdd4a193eaf8648d23e72a9ca460 100644 Binary files a/ui/opticka_ui.mlapp and b/ui/opticka_ui.mlapp differ diff --git a/userFunctions.m b/userFunctions.m index 56c3e1a2c1e763c49b5f6dd632ba17ee22af32bd..31e22725ac747198c1a114bdadf65b4c33991a00 100644 --- a/userFunctions.m +++ b/userFunctions.m @@ -15,7 +15,7 @@ classdef userFunctions < handle %#ok<*MCFIL> %> same). They can add their own methods. The class will be added as a uF %> object and these methods can be used via the state info file. %> -%> Copyright ©2014-2022 Ian Max Andolina — released: LGPL3, see LICENCE.md +%> Copyright ©2014-2023 Ian Max Andolina — released: LGPL3, see LICENCE.md % ======================================================================== % task object handles are added here by runExperiment, DO NOT EDIT @@ -26,7 +26,7 @@ classdef userFunctions < handle %#ok<*MCFIL> sM %> screenManager s - %> taskManager + %> taskSequence task %> metaStimulus stimluli stims @@ -57,9 +57,37 @@ classdef userFunctions < handle %#ok<*MCFIL> if me.verbose; fprintf('\n\n===>>> User Functions instantiated…\n\n'); end end + % =================================================================== + function setDelayTimeWithStaircase(me, stim, duration) + %> uses a staircase to set the off time for a specific stimulus + %> + % =================================================================== + if ~isempty(me.task.staircase) + me.stims{stim}.delayTime = me.task.staircase(1).sc.xCurrent; + if exist('duration','var') + me.stims{stim}.offTime = me.stims{stim}.delayTime + duration; + end + me.stims{stim}.resetTicks(); + if me.verbose; fprintf('===>>> SET DELAYTIME on stim %i to %.2f off=%.2f\n', stim, me.stims{stim}.delayTime, me.stims{stim}.offTime);end + end + end + + % =================================================================== + function resetDelayTime(me, stim, value) + %> reset stimulus delay on time + %> + % =================================================================== + if ~isempty(me.task.staircase) + me.stims{stim}.delayTime = value; + me.stims{stim}.offTime = inf; + me.stims{stim}.resetTicks(); + if me.verbose;fprintf('===>>> SET DELAYTIME on stim %i to %.2f\n', stim, me.stims{stim}.delayTime);end + end + end + % =================================================================== function testFunction(me) - %>testFunction test method + %> testFunction test method %> Just prints a message % =================================================================== if isa(me.rE, 'runExperiment') @@ -69,11 +97,9 @@ classdef userFunctions < handle %#ok<*MCFIL> end end + % ADD YOUR FUNCTIONS BELOW ↓ - function myText(me) - me.s.drawText('Hello from myFunctions') - end end end \ No newline at end of file