From 3ba28cbe529075bcf38fbbf82ec3772df5ee6c82 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 16 Jul 2020 22:45:23 +0200 Subject: [PATCH 01/25] refactor set keys of interest --- getResponse.m | 57 +++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/getResponse.m b/getResponse.m index 265f2b3..7304318 100644 --- a/getResponse.m +++ b/getResponse.m @@ -67,33 +67,7 @@ % Clean and realease any queue that might be opened KbQueueRelease(responseBox); - %% Defines keys - % list all the response keys we want KbQueue to listen to - - % by default we listen to all keys - % but if responseKey is set in the parameters we override this - keysOfInterest = ones(1,256); - - fprintf('\n Will be listening for key presses on : ') - - if isfield(expParameters, 'responseKey') && ~isempty(expParameters.responseKey) - - keysOfInterest = zeros(1,256); - - for iKey = 1:numel(expParameters.responseKey) - fprintf('\n - %s ', expParameters.responseKey{iKey}) - responseTargetKeys(iKey) = KbName(expParameters.responseKey(iKey)); %#ok<*SAGROW> - end - - keysOfInterest(responseTargetKeys) = 1; - - else - - fprintf('ALL KEYS.') - - end - - fprintf('\n\n') + keysOfInterest = setKeysOfInterest(expParameters); % Create the keyboard queue to collect responses. KbQueueCreate(responseBox, keysOfInterest); @@ -183,3 +157,32 @@ function talkToMe(action, expParameters) end end + +function keysOfInterest = setKeysOfInterest(expParameters) +% list all the response keys we want KbQueue to listen to +% by default we listen to all keys +% but if responseKey is set in the parameters we override this + +keysOfInterest = ones(1,256); + +fprintf('\n Will be listening for key presses on : ') + +if isfield(expParameters, 'responseKey') && ~isempty(expParameters.responseKey) + + responseTargetKeys = nan(1,numel(expParameters.responseKey)); + + for iKey = 1:numel(expParameters.responseKey) + fprintf('\n - %s ', expParameters.responseKey{iKey}) + responseTargetKeys(iKey) = KbName(expParameters.responseKey(iKey)); + end + + keysOfInterest(responseTargetKeys) = 1; + +else + + fprintf('ALL KEYS.') + +end + +fprintf('\n\n') +end \ No newline at end of file From 252f20ed2d26e6bc599a2ca9d7bcea7eb9045dae Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 16 Jul 2020 22:45:56 +0200 Subject: [PATCH 02/25] refactor get all key events --- getResponse.m | 50 +++++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/getResponse.m b/getResponse.m index 7304318..1544831 100644 --- a/getResponse.m +++ b/getResponse.m @@ -80,27 +80,7 @@ case 'check' - iEvent = 1; - - while KbEventAvail(responseBox) - - event = KbEventGet(responseBox); - - % we only return the pressed keys by default - if getOnlyPress && event.Pressed==0 - else - - responseEvents(iEvent,1).onset = event.Time; - responseEvents(iEvent,1).trial_type = 'response'; - responseEvents(iEvent,1).duration = 0; - responseEvents(iEvent,1).key_name = KbName(event.Keycode); - responseEvents(iEvent,1).pressed = event.Pressed; - - end - - iEvent = iEvent + 1; - - end + responseEvents = getAllKeyEvents(responseEvents, responseBox, getOnlyPress); case 'flush' @@ -185,4 +165,32 @@ function talkToMe(action, expParameters) end fprintf('\n\n') +end + + + +function responseEvents = getAllKeyEvents(responseEvents, responseBox, getOnlyPress) + +iEvent = 1; + +while KbEventAvail(responseBox) + + event = KbEventGet(responseBox); + + % we only return the pressed keys by default + if getOnlyPress==true && event.Pressed==0 + else + + responseEvents(iEvent,1).onset = event.Time; + responseEvents(iEvent,1).trial_type = 'response'; + responseEvents(iEvent,1).duration = 0; + responseEvents(iEvent,1).key_name = KbName(event.Keycode); + responseEvents(iEvent,1).pressed = event.Pressed; + + iEvent = iEvent + 1; + + end + +end + end \ No newline at end of file From ae37cb6fdd9f32a978390b7745ccaf12eed1cede Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 16 Jul 2020 22:46:17 +0200 Subject: [PATCH 03/25] clean getResponse --- CPP_getResponseDemo.m | 2 +- getResponse.m | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/CPP_getResponseDemo.m b/CPP_getResponseDemo.m index b276cc2..d1968d8 100644 --- a/CPP_getResponseDemo.m +++ b/CPP_getResponseDemo.m @@ -110,7 +110,7 @@ eventType = 'released'; end - fprintf('%s was %s at time %.3f seconds\n', ... + fprintf('\n%s was %s at time %.3f seconds\n', ... responseEvents(iEvent).key_name, ... eventType, ... responseEvents(iEvent).onset - startSecs); diff --git a/getResponse.m b/getResponse.m index 1544831..8a8f797 100644 --- a/getResponse.m +++ b/getResponse.m @@ -16,8 +16,8 @@ % - flush: % - stop: % -% - getOnlyPress: if set to 1 the function will only return the key presses and -% will not return when the keys were released (default=1) +% - getOnlyPress: if set to true the function will only return the key presses and +% will not return when the keys were released (default=true) % See the section on OUTPUT below for more info % % @@ -44,15 +44,10 @@ if nargin < 4 - getOnlyPress = 1; + getOnlyPress = true; end responseEvents = struct; -responseEvents.onset = []; -responseEvents.trial_type = []; -responseEvents.duration = []; -responseEvents.key_name = []; -responseEvents.pressed = []; responseBox = cfg.responseBox; @@ -60,8 +55,8 @@ case 'init' - % Prevent spilling of keystrokes into console. If you use ListenChar(2) - % this will prevent you from using KbQueue. + % Prevent spilling of keystrokes into console. + % If you use ListenChar(2), this will prevent you from using KbQueue. ListenChar(-1); % Clean and realease any queue that might be opened @@ -138,6 +133,7 @@ function talkToMe(action, expParameters) end + function keysOfInterest = setKeysOfInterest(expParameters) % list all the response keys we want KbQueue to listen to % by default we listen to all keys From d02e11ef7759b1497fb83fa00c204f8342b96e0b Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 16 Jul 2020 23:40:55 +0200 Subject: [PATCH 04/25] Add function to check if user asked to abort experiment --- checkAbort.m | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 checkAbort.m diff --git a/checkAbort.m b/checkAbort.m new file mode 100644 index 0000000..a49232b --- /dev/null +++ b/checkAbort.m @@ -0,0 +1,17 @@ +function checkAbort(cfg) +% Check for experiment abortion from operator + +[keyIsDown, ~, keyCode] = KbCheck(cfg.keyboard); + +if keyIsDown && keyCode(KbName(cfg.escapeKey)) + + global stopEverything + stopEverything = true; + + cleanUp(); + + warning('\nEscape key press detected: aborting experiment.\n') + +end + +end \ No newline at end of file From e6210ab75c4279d75095889e24a885cde91a97f0 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 16 Jul 2020 23:41:32 +0200 Subject: [PATCH 05/25] checkAbort called at every getResponse check --- CPP_getResponseDemo.m | 2 ++ getResponse.m | 75 +++++++++++++++++++++---------------------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/CPP_getResponseDemo.m b/CPP_getResponseDemo.m index d1968d8..a2324a6 100644 --- a/CPP_getResponseDemo.m +++ b/CPP_getResponseDemo.m @@ -29,6 +29,8 @@ cfg.keyboard = []; cfg.responseBox = []; +cfg.escapeKey = 'Escape'; + % We set which keys are "valid", any keys other than those will be ignored expParameters.responseKey = {}; diff --git a/getResponse.m b/getResponse.m index 8a8f797..c81ae88 100644 --- a/getResponse.m +++ b/getResponse.m @@ -77,6 +77,7 @@ responseEvents = getAllKeyEvents(responseEvents, responseBox, getOnlyPress); + checkAbort(cfg) case 'flush' @@ -95,42 +96,6 @@ talkToMe(action, expParameters); - -end - - -function talkToMe(action, expParameters) - -if ~isfield(expParameters, 'verbose') || isempty(expParameters.verbose) - expParameters.verbose = false; -end - -switch action - - case 'init' - - case 'start' - - fprintf('\n starting to listen to keypresses\n') - - case 'check' - - if expParameters.verbose - fprintf('\n checking recent keypresses\n') - end - - case 'flush' - - if expParameters.verbose - fprintf('\n reinitialising keyboard queue\n') - end - - case 'stop' - - fprintf('\n stopping to listen to keypresses\n\n') - -end - end @@ -144,7 +109,7 @@ function talkToMe(action, expParameters) fprintf('\n Will be listening for key presses on : ') if isfield(expParameters, 'responseKey') && ~isempty(expParameters.responseKey) - + responseTargetKeys = nan(1,numel(expParameters.responseKey)); for iKey = 1:numel(expParameters.responseKey) @@ -164,7 +129,6 @@ function talkToMe(action, expParameters) end - function responseEvents = getAllKeyEvents(responseEvents, responseBox, getOnlyPress) iEvent = 1; @@ -189,4 +153,39 @@ function talkToMe(action, expParameters) end +end + + +function talkToMe(action, expParameters) + +if ~isfield(expParameters, 'verbose') || isempty(expParameters.verbose) + expParameters.verbose = false; +end + +switch action + + case 'init' + + case 'start' + + fprintf('\n starting to listen to keypresses\n') + + case 'check' + + if expParameters.verbose + fprintf('\n checking recent keypresses\n') + end + + case 'flush' + + if expParameters.verbose + fprintf('\n reinitialising keyboard queue\n') + end + + case 'stop' + + fprintf('\n stopping to listen to keypresses\n\n') + +end + end \ No newline at end of file From ca83c2b8562e1b5b67fe428defc205e9e1aef0d6 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 16 Jul 2020 23:56:42 +0200 Subject: [PATCH 06/25] make checkAbort throw an error --- .gitignore | 1 + CPP_getResponseDemo.m | 9 +-------- checkAbort.m | 2 +- getResponse.m | 2 +- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 638e3cc..a0a4806 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ # exclude content of logfiles folders *.tsv *.mat +check_my_code_report.txt \ No newline at end of file diff --git a/CPP_getResponseDemo.m b/CPP_getResponseDemo.m index a2324a6..c72a807 100644 --- a/CPP_getResponseDemo.m +++ b/CPP_getResponseDemo.m @@ -29,7 +29,7 @@ cfg.keyboard = []; cfg.responseBox = []; -cfg.escapeKey = 'Escape'; +cfg.escapeKey = 'ESCAPE'; % We set which keys are "valid", any keys other than those will be ignored expParameters.responseKey = {}; @@ -68,7 +68,6 @@ end - %% Run demo % Create the keyboard queue to collect responses. @@ -79,15 +78,11 @@ startSecs = GetSecs(); getResponse('start', cfg, expParameters, 1); - - % Here we wait for 5 seconds but are still collecting responses. % So you could still be doing something else (presenting audio and visual stim) and % still collect responses. WaitSecs(5); - - % Check what keys were pressed (all of them) responseEvents = getResponse('check', cfg, expParameters, 0); @@ -101,8 +96,6 @@ getResponse('stop', cfg, expParameters, 1); - - %% Now we look what keys were pressed and when for iEvent = 1:size(responseEvents, 1) diff --git a/checkAbort.m b/checkAbort.m index a49232b..d2107a8 100644 --- a/checkAbort.m +++ b/checkAbort.m @@ -10,7 +10,7 @@ function checkAbort(cfg) cleanUp(); - warning('\nEscape key press detected: aborting experiment.\n') + error('Escape key press detected: aborting experiment.') end diff --git a/getResponse.m b/getResponse.m index c81ae88..c8f1eeb 100644 --- a/getResponse.m +++ b/getResponse.m @@ -78,7 +78,7 @@ responseEvents = getAllKeyEvents(responseEvents, responseBox, getOnlyPress); checkAbort(cfg) - + case 'flush' KbQueueFlush(responseBox); From 75c80f9947e5088eae60511c00430e4a02473e92 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 17 Jul 2020 14:13:45 +0200 Subject: [PATCH 07/25] create demo folder --- CPP_getResponseDemo.m => demos/CPP_getResponseDemo.m | 1 + 1 file changed, 1 insertion(+) rename CPP_getResponseDemo.m => demos/CPP_getResponseDemo.m (99%) diff --git a/CPP_getResponseDemo.m b/demos/CPP_getResponseDemo.m similarity index 99% rename from CPP_getResponseDemo.m rename to demos/CPP_getResponseDemo.m index c72a807..5736cbd 100644 --- a/CPP_getResponseDemo.m +++ b/demos/CPP_getResponseDemo.m @@ -4,6 +4,7 @@ % (a wrapper around the KbQueue function from PTB) % start with a clean slate +cd .. clear; clc; if IsOctave more off % for a better display experience From 1909ceefe672f3590af45d72ee52e11f20478d34 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 17 Jul 2020 17:47:59 +0200 Subject: [PATCH 08/25] add demo checkAbort --- demos/CPP_checkAbortDemo.m | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 demos/CPP_checkAbortDemo.m diff --git a/demos/CPP_checkAbortDemo.m b/demos/CPP_checkAbortDemo.m new file mode 100644 index 0000000..0216534 --- /dev/null +++ b/demos/CPP_checkAbortDemo.m @@ -0,0 +1,12 @@ +cd .. + +cfg.keyboard = []; +cfg.escapeKey = 'ESCAPE'; +KbName('UnifyKeyNames'); + +% stay in the loop until the escape key is pressed +while GetSecs < Inf + + checkAbort(cfg) + +end \ No newline at end of file From 80c6d91e756eaf022bc5346cc2d1d1604307002e Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 11:04:07 +0200 Subject: [PATCH 09/25] change defaults and set up unit test for defaults --- .travis.yml | 25 +++++++++++++++++ setDefaultsPTB.m | 18 ++++++++---- tests/runtests.m | 51 ++++++++++++++++++++++++++++++++++ tests/test_setDefaultsPTB.m | 55 +++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 .travis.yml create mode 100644 tests/runtests.m create mode 100644 tests/test_setDefaultsPTB.m diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8e1e427 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +# Travis CI (https://travis-ci.org/) + +language: c +dist: bionic +cache: + apt: true # only works with Pro version + +env: + global: + - OCTFLAGS="--no-gui --no-window-system --silent" + +before_install: + - travis_retry sudo apt-get -y -qq update + - travis_retry sudo apt-get -y install octave + - travis_retry sudo apt-get -y install liboctave-dev + +install: + - octave $OCTFLAGS --eval "addpath (pwd); savepath ();" + +before_script: + # Change current directory + - cd tests + +script: + - octave $OCTFLAGS --eval "results = runtests; assert(all(~[results.Failed]))" diff --git a/setDefaultsPTB.m b/setDefaultsPTB.m index 308181b..3f371af 100644 --- a/setDefaultsPTB.m +++ b/setDefaultsPTB.m @@ -1,8 +1,13 @@ function cfg = setDefaultsPTB(cfg) + + if nargin<1 + cfg = struct; + end % list the default values -fieldsToSet.keyboard = []; -fieldsToSet.responseBox = []; +fieldsToSet.keyboard.keyboard = []; +fieldsToSet.keyboard.responseBox = []; +fieldsToSet.keyboard.responseKey = {}; fieldsToSet.debug = true; fieldsToSet.testingTranspScreen = true; @@ -10,9 +15,9 @@ fieldsToSet.backgroundColor = [0 0 0]; -fieldsToSet.textFont = 'Courier New'; -fieldsToSet.textSize = 18; -fieldsToSet.textStyle = 1; +fieldsToSet.text.font = 'Courier New'; +fieldsToSet.text.size = 18; +fieldsToSet.text.style = 1; fieldsToSet.monitorWidth = 42; fieldsToSet.screenDistance = 134; @@ -46,6 +51,9 @@ getfield(fieldsToSet, names{i})); %#ok end +% sort fields alphabetically +cfg = orderfields(cfg); + end diff --git a/tests/runtests.m b/tests/runtests.m new file mode 100644 index 0000000..a1b006f --- /dev/null +++ b/tests/runtests.m @@ -0,0 +1,51 @@ +function results = runtests(pth) +% Run tests +% List all the 'test_*.m' files located in the same directory as this +% function, run them and keep track of how many passed, failed or are +% incomplete. +%__________________________________________________________________________ + +% Copyright (C) 2019, Guillaume Flandin, Wellcome Centre for Human Neuroimaging +% Copyright (C) 2020--, CPP_BIDS developers + +%-Get the path of where this file is located +if ~nargin, pth = fileparts(mfilename('fullpath')); end + +%-List all the 'test_*.m' files located in the same directory as this +% function +d = dir(pth); +d([d.isdir]) = []; +d(arrayfun(@(x) isempty(regexp(x.name,'^test_.*\.m$','once')),d)) = []; + +results = struct('Passed',{},'Failed',{},'Incomplete',{},'Duration',{}); +for i=1:numel(d) + + results(i).Failed = false; + results(i).Passed = false; + results(i).Incomplete = false; + + tstart = tic; + + %-Run each test file and catch error message in case of failure + try + + fprintf('%s',d(i).name(1:end-2)); + feval(d(i).name(1:end-2)); + results(i).Passed = true; + + catch err + results(i).Failed = true; + fprintf('\n%s',err.message); + end + + results(i).Duration = toc(tstart); + + fprintf('\n'); + +end + +if ~nargout + fprintf(['Totals (%d tests):\n\t%d Passed, %d Failed, %d Incomplete.\n' ... + '\t%f seconds testing time.\n\n'],numel(results),nnz([results.Passed]),... + nnz([results.Failed]),nnz([results.Incomplete]),sum([results.Duration])); +end diff --git a/tests/test_setDefaultsPTB.m b/tests/test_setDefaultsPTB.m new file mode 100644 index 0000000..7f5dd13 --- /dev/null +++ b/tests/test_setDefaultsPTB.m @@ -0,0 +1,55 @@ +%% test basic cfg creation + +% set up +cfgToTest = struct(... + 'debug', true, ... + 'testingTranspScreen', true, ... + 'testingSmallScreen', true, ... + 'backgroundColor', [0 0 0], ... + 'text', struct('font', 'Courier New', 'size', 18, 'style', 1), ... + 'monitorWidth', 42, ... + 'screenDistance', 134); + +cfgToTest.keyboard.keyboard = []; +cfgToTest.keyboard.responseBox = []; +cfgToTest.keyboard.responseKey = {}; + +cfgToTest = orderfields(cfgToTest); + +% test +cfg = setDefaultsPTB; +assert(isequal(cfg, cfgToTest)) + + +%% test that values are not overwritten +clear cfg +cfg = struct('monitorWidth', 36); + +cfgToTest.monitorWidth = 36; + +cfg = setDefaultsPTB(cfg); +assert(isequal(cfg, cfgToTest)) + +cfgToTest.monitorWidth = 42; + +%% test with audio init + +% set up +cfgToTest.initAudio = 1; +cfgToTest.audio = struct(... + 'fs', 44800, ... + 'channels', 2, ... + 'initVolume', 1, ... + 'requestedLatency', 3, ... + 'repeat', 1, ... + 'startCue', 0, ... + 'waitForDevice', 1); + +cfgToTest = orderfields(cfgToTest); + +clear cfg +cfg.initAudio = 1; + +% test +cfg = setDefaultsPTB(cfg); +assert(isequal(cfg, cfgToTest)) \ No newline at end of file From 750c6ff098a2d2b97f5d6123cbe4b9f2c14f8c73 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 11:20:36 +0200 Subject: [PATCH 10/25] update wait for triggers to listen to all devices by defaults --- demos/CPP_wait4TriggerDemo.m | 5 ++--- setDefaultsPTB.m | 6 ++++++ tests/test_setDefaultsPTB.m | 1 + wait4Trigger.m | 38 ++++++++++++++++++++++++++---------- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/demos/CPP_wait4TriggerDemo.m b/demos/CPP_wait4TriggerDemo.m index c3584ac..117aed6 100644 --- a/demos/CPP_wait4TriggerDemo.m +++ b/demos/CPP_wait4TriggerDemo.m @@ -1,9 +1,8 @@ cd .. -cfg.device = 'Scanner'; - +cfg.device = 'scanner'; +cfg.MRI.repetitionTime = 3; cfg.numTriggers = 4; - cfg.triggerKey = 'space'; KbName('UnifyKeyNames'); diff --git a/setDefaultsPTB.m b/setDefaultsPTB.m index 3f371af..caf9644 100644 --- a/setDefaultsPTB.m +++ b/setDefaultsPTB.m @@ -5,6 +5,8 @@ end % list the default values +fieldsToSet.testingDevice = 'pc'; + fieldsToSet.keyboard.keyboard = []; fieldsToSet.keyboard.responseBox = []; fieldsToSet.keyboard.responseKey = {}; @@ -41,6 +43,10 @@ end +if isfield(cfg, 'testingDevice') && strcmpi(cfg.testingDevice, 'scanner') + fieldsToSet.MRI.repetitionTime = []; +end + % loop through the defaults and set them in cfg if they don't exist names = fieldnames(fieldsToSet); diff --git a/tests/test_setDefaultsPTB.m b/tests/test_setDefaultsPTB.m index 7f5dd13..e75934e 100644 --- a/tests/test_setDefaultsPTB.m +++ b/tests/test_setDefaultsPTB.m @@ -2,6 +2,7 @@ % set up cfgToTest = struct(... + 'testingDevice', 'pc', ... 'debug', true, ... 'testingTranspScreen', true, ... 'testingSmallScreen', true, ... diff --git a/wait4Trigger.m b/wait4Trigger.m index 30f5b48..9564663 100644 --- a/wait4Trigger.m +++ b/wait4Trigger.m @@ -1,8 +1,29 @@ -function wait4Trigger(cfg) +function wait4Trigger(cfg, deviceNumber) +% Counts a certain number of triggers coming from the scanner before returning. +% +% Will print the count down in the command line and on the PTB window if one is +% opened. +% +% If the fMRI sequence RT is provided (cgf.MRI.repetitionTime) then it will wait +% for half a RT before starting to check for next trigger, otherwise it will +% wait 500 ms. +% +% When no deviceNumber is set then it will check the default device: this is +% probably only useful in debug as you will want to make sure you get the +% triggers coming from the scanner in a real case scenario. +if nargin < 1 || isempty(cfg) + error('I need at least one input.') +end + +if nargin < 2 || isempty(deviceNumber) + deviceNumber = -1; + fprintf('Will wait for triggers on the main keyboard device.\n'); +end + triggerCounter = 0; -if strcmp(cfg.device, 'Scanner') +if strcmpi(cfg.testingDevice, 'scanner') msg = 'Waiting for trigger'; talkToMe(cfg, msg) @@ -11,7 +32,7 @@ function wait4Trigger(cfg) keyCode = []; %#ok - [~, keyCode] = KbPressWait(-1); + [~, keyCode] = KbPressWait(deviceNumber); if strcmp(KbName(keyCode), cfg.triggerKey) @@ -45,15 +66,12 @@ function talkToMe(cfg, msg) function pauseBetweenTriggers(cfg) -% we pause between triggers for half a repetition time or 500 ms if no RT -% is specified. -% we do this otherwise KbWait and KbPressWait might be too fast and could +% we pause between triggers otherwise KbWait and KbPressWait might be too fast and could % catch several triggers in one go. -if isfield(cfg, 'repetitionTime') - waitTime = cgf.repetitionTime / 2; -else - waitTime = .5; +waitTime = 0.5; +if ~isempty(cfg.MRI.repetitionTime) + waitTime = cfg.MRI.repetitionTime / 2; end WaitSecs(waitTime); From a00b2b08b20e839e187eeb09b15029500673a25b Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 11:21:31 +0200 Subject: [PATCH 11/25] make testKeyboard only test main device if response box is the same as the main --- testKeyboards.m | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/testKeyboards.m b/testKeyboards.m index c1f68a8..99dc71c 100644 --- a/testKeyboards.m +++ b/testKeyboards.m @@ -7,18 +7,21 @@ function testKeyboards(cfg) % Main keyboard used by the experimenter to quit the experiment if it is necessary % cfg.keyboard -fprintf('\n This is a test: press any key on the experimenter keyboard\n'); +fprintf('\n This is a test: press any key on the main keyboard\n'); t = GetSecs; -[~, keyCode, ~] = KbPressWait(cfg.keyboard, t+timeOut); -throwError(keyCode, cfg.keyboard, 1) - - -% For key presses for the subject -% cfg.responseBox -fprintf('\n This is a test: press any key on the participant response box\n'); -t = GetSecs; -[~, keyCode, ~] = KbPressWait(cfg.responseBox, t+timeOut); -throwError(keyCode, cfg.responseBox, 2) +[~, keyCode, ~] = KbPressWait(cfg.keyboard.keyboard, t+timeOut); +throwError(keyCode, cfg.keyboard.keyboard, 1) + +if ~isequal(cfg.keyboard.keyboard, cfg.keyboard.responseBox) + + % For key presses for the subject + % cfg.keyboard.responseBox + fprintf('\n Thiscfg.keyboard is a test: press any key on the participant response box\n'); + t = GetSecs; + [~, keyCode, ~] = KbPressWait(cfg.keyboard.responseBox, t+timeOut); + throwError(keyCode, cfg.keyboard.responseBox, 2) + +end end @@ -27,7 +30,7 @@ function throwError(keyCode, deviceNumber, keyboardType) switch keyboardType case 1 - keyboardType = 'experimenter keyboard'; + keyboardType = 'main keyboard'; case 2 keyboardType = 'response box'; end @@ -48,7 +51,7 @@ function throwError(keyCode, deviceNumber, keyboardType) fprintf(text1); if isempty(deviceNumber) - disp(' - no keyboard selected, default is the main keyboard') + disp('No keyboard selected, default is the main keyboard') else disp(deviceNumber) end From 81412fe2ab9c8939f0e7d2819794b684192cc6dd Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 11:22:01 +0200 Subject: [PATCH 12/25] make press space bar listen to all devices by defaults --- pressSpace4me.m | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pressSpace4me.m b/pressSpace4me.m index 6cee744..300b34b 100644 --- a/pressSpace4me.m +++ b/pressSpace4me.m @@ -1,13 +1,19 @@ -function pressSpace4me +function pressSpace4me(deviceNumber) % Use that to stop your script and only restart when the space bar is pressed. +% When no deviceNumber is set then it will check the default device + +if nargin < 1 || isempty(deviceNumber) + deviceNumber = -1; +end fprintf('\nPress space to continue.\n'); while 1 + % check keyboard very 100 ms WaitSecs(0.1); - [~, keyCode, ~] = KbWait(-1); + [~, keyCode, ~] = KbWait(deviceNumber); if strcmp(KbName(find(keyCode)), 'space') fprintf('starting the experiment...\n'); From 4f500ad5ab39ef54cfc11e5622d18332641c7958 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 11:39:28 +0200 Subject: [PATCH 13/25] update trigger and wait for functions - add comments - create demo - waitForTrigger returns automatically after last trigger --- demos/CPP_pressSpace4meDemo.m | 7 +++++++ demos/CPP_wait4TriggerDemo.m | 11 ++++++++--- pressSpace4me.m | 5 +++-- wait4Trigger.m | 5 ++++- 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 demos/CPP_pressSpace4meDemo.m diff --git a/demos/CPP_pressSpace4meDemo.m b/demos/CPP_pressSpace4meDemo.m new file mode 100644 index 0000000..ed137e0 --- /dev/null +++ b/demos/CPP_pressSpace4meDemo.m @@ -0,0 +1,7 @@ +cd .. + +% beginning of demo +KbName('UnifyKeyNames'); + +% press the key "space" to "start" the experiment +pressSpace4me \ No newline at end of file diff --git a/demos/CPP_wait4TriggerDemo.m b/demos/CPP_wait4TriggerDemo.m index 117aed6..9e0ece0 100644 --- a/demos/CPP_wait4TriggerDemo.m +++ b/demos/CPP_wait4TriggerDemo.m @@ -1,10 +1,15 @@ cd .. -cfg.device = 'scanner'; -cfg.MRI.repetitionTime = 3; +% set up +cfg.testingDevice = 'scanner'; cfg.numTriggers = 4; -cfg.triggerKey = 'space'; +cfg.triggerKey = 't'; +% this field is not required but can be mentioned +cfg.MRI.repetitionTime = 3; + +% beginning of demo KbName('UnifyKeyNames'); +% press the key "t" to simulate triggers wait4Trigger(cfg) \ No newline at end of file diff --git a/pressSpace4me.m b/pressSpace4me.m index 300b34b..0ecfd9c 100644 --- a/pressSpace4me.m +++ b/pressSpace4me.m @@ -1,5 +1,7 @@ function pressSpace4me(deviceNumber) % Use that to stop your script and only restart when the space bar is pressed. +% This is a good way to have a final checkpoint before starting and letting the +% user or the experimenter press the space bar to start. % When no deviceNumber is set then it will check the default device if nargin < 1 || isempty(deviceNumber) @@ -14,9 +16,8 @@ function pressSpace4me(deviceNumber) WaitSecs(0.1); [~, keyCode, ~] = KbWait(deviceNumber); - if strcmp(KbName(find(keyCode)), 'space') - fprintf('starting the experiment...\n'); + fprintf('Starting the experiment...\n'); break end diff --git a/wait4Trigger.m b/wait4Trigger.m index 9564663..94c3eba 100644 --- a/wait4Trigger.m +++ b/wait4Trigger.m @@ -41,7 +41,10 @@ function wait4Trigger(cfg, deviceNumber) msg = sprintf(' Trigger %i', triggerCounter); talkToMe(cfg, msg) - pauseBetweenTriggers(cfg) + % we only wait if this is not the last trigger + if triggerCounter < cfg.numTriggers + pauseBetweenTriggers(cfg) + end end end From 7f44c63170411be7d9e3f312c3a6ebdbd5e2caed Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 11:41:45 +0200 Subject: [PATCH 14/25] update initPTB to set font parameters - to match the way the cfg is now structured --- initPTB.m | 6 +++--- setDefaultsPTB.m | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/initPTB.m b/initPTB.m index 800c321..20932d4 100644 --- a/initPTB.m +++ b/initPTB.m @@ -230,8 +230,8 @@ function initDebug(cfg) function initText(cfg) -Screen('TextFont', cfg.win, cfg.textFont); -Screen('TextSize', cfg.win, cfg.textSize); -Screen('TextStyle', cfg.win, cfg.textStyle); +Screen('TextFont', cfg.win, cfg.text.Font); +Screen('TextSize', cfg.win, cfg.text.Size); +Screen('TextStyle', cfg.win, cfg.text.Style); end \ No newline at end of file diff --git a/setDefaultsPTB.m b/setDefaultsPTB.m index caf9644..283fa16 100644 --- a/setDefaultsPTB.m +++ b/setDefaultsPTB.m @@ -4,9 +4,10 @@ cfg = struct; end -% list the default values +%% list the default values fieldsToSet.testingDevice = 'pc'; +% keyboard defaults fieldsToSet.keyboard.keyboard = []; fieldsToSet.keyboard.responseBox = []; fieldsToSet.keyboard.responseKey = {}; @@ -17,6 +18,7 @@ fieldsToSet.backgroundColor = [0 0 0]; +% text defaults fieldsToSet.text.font = 'Courier New'; fieldsToSet.text.size = 18; fieldsToSet.text.style = 1; @@ -47,6 +49,7 @@ fieldsToSet.MRI.repetitionTime = []; end +%% set the defaults % loop through the defaults and set them in cfg if they don't exist names = fieldnames(fieldsToSet); @@ -57,7 +60,7 @@ getfield(fieldsToSet, names{i})); %#ok end -% sort fields alphabetically +%% sort fields alphabetically cfg = orderfields(cfg); From ce9f25d4d21ce86980b4b5885f9b4ac65d0be74c Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 11:51:26 +0200 Subject: [PATCH 15/25] add CPP_PTB to path in demos --- demos/CPP_checkAbortDemo.m | 4 +++- demos/CPP_pressSpace4meDemo.m | 4 +++- demos/CPP_wait4TriggerDemo.m | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/demos/CPP_checkAbortDemo.m b/demos/CPP_checkAbortDemo.m index 0216534..cfb649b 100644 --- a/demos/CPP_checkAbortDemo.m +++ b/demos/CPP_checkAbortDemo.m @@ -1,4 +1,6 @@ -cd .. +% add parent directory to the path (to make sure we can access the CPP_PTB +% functions) +addpath(fullfile(pwd, '..')) cfg.keyboard = []; cfg.escapeKey = 'ESCAPE'; diff --git a/demos/CPP_pressSpace4meDemo.m b/demos/CPP_pressSpace4meDemo.m index ed137e0..90784c6 100644 --- a/demos/CPP_pressSpace4meDemo.m +++ b/demos/CPP_pressSpace4meDemo.m @@ -1,4 +1,6 @@ -cd .. +% add parent directory to the path (to make sure we can access the CPP_PTB +% functions) +addpath(fullfile(pwd, '..')) % beginning of demo KbName('UnifyKeyNames'); diff --git a/demos/CPP_wait4TriggerDemo.m b/demos/CPP_wait4TriggerDemo.m index 9e0ece0..64665fe 100644 --- a/demos/CPP_wait4TriggerDemo.m +++ b/demos/CPP_wait4TriggerDemo.m @@ -1,5 +1,3 @@ -cd .. - % set up cfg.testingDevice = 'scanner'; cfg.numTriggers = 4; @@ -8,6 +6,10 @@ % this field is not required but can be mentioned cfg.MRI.repetitionTime = 3; +% add parent directory to the path (to make sure we can access the CPP_PTB +% functions) +addpath(fullfile(pwd, '..')) + % beginning of demo KbName('UnifyKeyNames'); From 99aaae443d93e36d613c623882186507b429cdb9 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 11:52:26 +0200 Subject: [PATCH 16/25] update checkAbort - will throw a specific error that can be catched to fail gracefully - update demo --- checkAbort.m | 38 ++++++++++++++++++++++++++++++-------- demos/CPP_checkAbortDemo.m | 27 ++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/checkAbort.m b/checkAbort.m index d2107a8..3140031 100644 --- a/checkAbort.m +++ b/checkAbort.m @@ -1,16 +1,38 @@ -function checkAbort(cfg) +function checkAbort(cfg, deviceNumber) % Check for experiment abortion from operator +% When no deviceNumber is set then it will check the default device +% When an abort key s detected this will set a global variable and throw a +% specific error that can then be catched. +% +% Maint script +% try +% % Your awesome experiment +% catch ME % when something goes wrong +% switch ME.identifier +% case 'checkAbort:abortRequested' +% % stuff to do when an abort is requested (save data...) +% otherwise +% % stuff to do otherwise +% rethrow(ME) % display the error +% end +% end -[keyIsDown, ~, keyCode] = KbCheck(cfg.keyboard); +if nargin < 1 || isempty(cfg) + error('I need at least one input.') +end -if keyIsDown && keyCode(KbName(cfg.escapeKey)) - - global stopEverything - stopEverything = true; +if nargin < 2 || isempty(deviceNumber) + deviceNumber = -1; +end + +[keyIsDown, ~, keyCode] = KbCheck(deviceNumber); + +if keyIsDown && keyCode(KbName(cfg.keyboard.escapeKey)) - cleanUp(); + errorStruct.message = 'Escape key press detected: aborting experiment.'; + errorStruct.identifier = 'checkAbort:abortRequested'; - error('Escape key press detected: aborting experiment.') + error(errorStruct) end diff --git a/demos/CPP_checkAbortDemo.m b/demos/CPP_checkAbortDemo.m index cfb649b..7808196 100644 --- a/demos/CPP_checkAbortDemo.m +++ b/demos/CPP_checkAbortDemo.m @@ -2,13 +2,30 @@ % functions) addpath(fullfile(pwd, '..')) -cfg.keyboard = []; -cfg.escapeKey = 'ESCAPE'; +% set up +cfg.keyboard.escapeKey = 'ESCAPE'; + +% beginning of demo KbName('UnifyKeyNames'); -% stay in the loop until the escape key is pressed -while GetSecs < Inf + +try + + % stay in the loop until the escape key is pressed + while GetSecs < Inf + + checkAbort(cfg) + + end + +catch ME - checkAbort(cfg) + switch ME.identifier + case 'checkAbort:abortRequested' + warning('You pressed the escape key: will try to fail gracefully.') + fprintf('\nWe did catch your abort signal.\n') + otherwise + rethrow(ME) % display other errors + end end \ No newline at end of file From e6354eae1a7a376ee681c220f80d034bf66089c5 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 12:30:22 +0200 Subject: [PATCH 17/25] refactor checkAbort --- checkAbort.m | 5 +---- errorAbort.m | 6 ++++++ 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 errorAbort.m diff --git a/checkAbort.m b/checkAbort.m index 3140031..cdb7dd7 100644 --- a/checkAbort.m +++ b/checkAbort.m @@ -29,10 +29,7 @@ function checkAbort(cfg, deviceNumber) if keyIsDown && keyCode(KbName(cfg.keyboard.escapeKey)) - errorStruct.message = 'Escape key press detected: aborting experiment.'; - errorStruct.identifier = 'checkAbort:abortRequested'; - - error(errorStruct) + errorAbort(); end diff --git a/errorAbort.m b/errorAbort.m new file mode 100644 index 0000000..178cda0 --- /dev/null +++ b/errorAbort.m @@ -0,0 +1,6 @@ +function errorAbort + errorStruct.message = 'Escape key press detected: aborting experiment.'; + errorStruct.identifier = 'checkAbort:abortRequested'; + + error(errorStruct) +end \ No newline at end of file From 9e3ebf9d430d53e211b1f517d662cedcf54d3fe7 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 12:30:38 +0200 Subject: [PATCH 18/25] set a default escape key --- setDefaultsPTB.m | 1 + tests/test_setDefaultsPTB.m | 2 ++ 2 files changed, 3 insertions(+) diff --git a/setDefaultsPTB.m b/setDefaultsPTB.m index 283fa16..733eedc 100644 --- a/setDefaultsPTB.m +++ b/setDefaultsPTB.m @@ -11,6 +11,7 @@ fieldsToSet.keyboard.keyboard = []; fieldsToSet.keyboard.responseBox = []; fieldsToSet.keyboard.responseKey = {}; +fieldsToSet.keyboard.escapeKey = 'ESCAPE'; fieldsToSet.debug = true; fieldsToSet.testingTranspScreen = true; diff --git a/tests/test_setDefaultsPTB.m b/tests/test_setDefaultsPTB.m index e75934e..2dd7d3a 100644 --- a/tests/test_setDefaultsPTB.m +++ b/tests/test_setDefaultsPTB.m @@ -14,6 +14,8 @@ cfgToTest.keyboard.keyboard = []; cfgToTest.keyboard.responseBox = []; cfgToTest.keyboard.responseKey = {}; +cfgToTest.keyboard.escapeKey = 'ESCAPE'; + cfgToTest = orderfields(cfgToTest); From 2132ca72609f91c77aeb1c2fc9fbcb1076742ca9 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 12:31:02 +0200 Subject: [PATCH 19/25] update readme on how to set up keyboards --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 2a60f40..8b9332d 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,30 @@ You can then use the [matlab package manager](https://github.com/mobeets/mpm), t end ``` +## Setting up keyboards + +To select a specific keyboard to be used by the experimenter or the participant, you need to know +the value assigned by PTB to each keyboard device. + +To know this copy-paste this on the command window: + +``` matlab +[keyboardNumbers, keyboardNames] = GetKeyboardIndices; + +disp(keyboardNumbers); +disp(keyboardNames); +``` + +You can then assign a specific device number to the main keyboard or the response box in the `cfg` structure + +- `cfg.keyboard.responseBox` would be the device number of the device used by the participant to give his/her +response: like the button box in the scanner or a separate keyboard for a behavioral experiment +- `cfg.keyboard.keyboard` would be the device number of the keyboard on which the experimenter will type or +press the keys necessary to start or abort the experiment. + +`cfg.keyboard.responseBox` and `cfg.keyboard.keyboard` can be different or the same. + +Using empty vectors (ie `[]`) or a negative value for those means that you will let PTB find and use the default device. ## Structure and function details From 3a6e4c0f7825dc422cc44fa4dc41202e48c39aa7 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 15:13:45 +0200 Subject: [PATCH 20/25] define tests demos and errors for getResponse --- demos/CPP_getResponseDemo.m | 116 +++++++++++++++++++------------- errorAbortGetReponse.m | 7 ++ errorRestrictedKeysGetReponse.m | 7 ++ tests/getResponseTest.m | 81 ++++++++++++++++++++++ 4 files changed, 163 insertions(+), 48 deletions(-) create mode 100644 errorAbortGetReponse.m create mode 100644 errorRestrictedKeysGetReponse.m create mode 100644 tests/getResponseTest.m diff --git a/demos/CPP_getResponseDemo.m b/demos/CPP_getResponseDemo.m index 5736cbd..47a2b1b 100644 --- a/demos/CPP_getResponseDemo.m +++ b/demos/CPP_getResponseDemo.m @@ -1,69 +1,64 @@ %% Demo showing how to use the getResponse function % This small script shows how to use the getReponse function -% (a wrapper around the KbQueue function from PTB) -% start with a clean slate -cd .. -clear; clc; +% add parent directory to matlab path (so we can access the CPP_PTB functions) +addpath(fullfile(pwd, '..')) + + +%% start with a clean slate +clear; +clc; + if IsOctave more off % for a better display experience end -%% set parameters +% use the default set up (use main keyboard and use the ESCAPE key to abort) +cfg = setDefaultsPTB; -% cfg.responseBox would be the device number of the device used by the participant to give his/her -% response: like the button box in the scanner or a separate keyboard for a behavioral experiment -% -% cfg.keyboard would be the device number of the keyboard on which the experimenter will type or -% press the keys necessary to start or abort the experiment. +% show the default option +disp(cfg.keyboard) -% cfg.responseBox and cfg.keyboard can be different or the same. +%% set parameters -% If you want to know the device number of all the keyboards and responses -% boxes connected to the computer you can use the following code. -% [cfg.keyboardNumbers, cfg.keyboardNames] = GetKeyboardIndices +% Change the values set by defaults (for more info about the keyboard see the doc) +% cfg.keyboard.keyboard = ?? +% cfg.keyboard.responseBox = ?? +% cfg.keyboard.escapeKey = ?? -% Using empty vectors should work for linux when to select the "main" -% keyboard. You might have to try some other values for Windows. To -% assigne a specific keyboard input the kb assigned value (see README) -cfg.keyboard = []; -cfg.responseBox = []; +% Decide which device you want to collect responses from +deviceNumber = []; % default device (PTB will find it for you) +% deviceNumber = cfg.keyboard.keyboard; % the one you may have chosen as the main keyboard +% deviceNumber = cfg.keyboard.responseBox; % the one you may have chosen as the response box -cfg.escapeKey = 'ESCAPE'; +% if you want getResponse to ignore the key release +getOnlyPress = 1; % We set which keys are "valid", any keys other than those will be ignored -expParameters.responseKey = {}; +cfg.keyboard.responseKey = {'a', 'b'}; +% This would make sure that you listen to presses of the escape key +cfg.keyboard.responseKey{end+1} = cfg.keyboard.escapeKey; -%% init -% Keyboard -% Make sure keyboard mapping is the same on all supported operating systems -% Apple MacOS/X, MS-Windows and GNU/Linux: -KbName('UnifyKeyNames'); - - -% we ask PTB to tell us which keyboard devices are connected to the computer -[cfg.keyboardNumbers, cfg.keyboardNames] = GetKeyboardIndices; - -cfg.keyboardNumbers -cfg.keyboardNames +%% Final checks +% Make sure keyboard mapping is the same on all supported operating systems +KbName('UnifyKeyNames'); % Test that the keyboards are correctly configured testKeyboards(cfg); -% Give the time to the test key to be released and not listened +% Give the time to the test key to be released and not listened to WaitSecs(1); - fprintf('\nDuring the next 5 seconds we will collect responses on the following keys: \n\n'); -if isempty(expParameters.responseKey) +if isempty(cfg.keyboard.responseKey) fprintf('\nALL KEYS\n\n'); else - for iKey=1:numel(expParameters.responseKey) - fprintf('\n%s', expParameters.responseKey{iKey}); + for iKey=1:numel(cfg.keyboard.responseKey) + fprintf('\n%s', cfg.keyboard.responseKey{iKey}); end fprintf('\n\n'); end @@ -71,13 +66,15 @@ %% Run demo +try + % Create the keyboard queue to collect responses. -getResponse('init', cfg, expParameters, 1); +getResponse('init', deviceNumber, cfg); % Start collecting responses for 5 seconds % Each new key press is added to the queue of events recorded by KbQueue startSecs = GetSecs(); -getResponse('start', cfg, expParameters, 1); +getResponse('start', deviceNumber); % Here we wait for 5 seconds but are still collecting responses. % So you could still be doing something else (presenting audio and visual stim) and @@ -85,16 +82,19 @@ WaitSecs(5); % Check what keys were pressed (all of them) -responseEvents = getResponse('check', cfg, expParameters, 0); - -% The following line would only return key presses and not releases -% responseEvents = getResponse('check', cfg, expParameters, 1); +% If the escapeKey was pressed at any time, it will only abort when you +% getResponse('check') +responseEvents = getResponse('check', deviceNumber, cfg, getOnlyPress); % This can be used to flush the queue: empty all events that are still present in the queue -getResponse('flush', cfg, expParameters, 1); +getResponse('flush', deviceNumber); + +% If you wan to stop listening to key presses. You could start listening again +% later by calling: getResponse('start', deviceNumber) +getResponse('stop', deviceNumber); -% If you wan to stop listening to key presses. -getResponse('stop', cfg, expParameters, 1); +% If you wan to destroyt the queue: you would have to initialize it again +getResponse('release', deviceNumber); %% Now we look what keys were pressed and when @@ -106,9 +106,29 @@ eventType = 'released'; end - fprintf('\n%s was %s at time %.3f seconds\n', ... + fprintf('\n %s was %s at time %.3f seconds\n', ... responseEvents(iEvent).key_name, ... eventType, ... responseEvents(iEvent).onset - startSecs); end + + +catch ME + + getResponse('release', deviceNumber) + + switch ME.identifier + + case 'getResponse:abortRequested' + warning('You pressed the escape key: will try to fail gracefully.') + + fprintf('\nWe did catch your abort signal.\n') + + otherwise + rethrow(ME) % display other errors + + end +end + + diff --git a/errorAbortGetReponse.m b/errorAbortGetReponse.m new file mode 100644 index 0000000..e563dc9 --- /dev/null +++ b/errorAbortGetReponse.m @@ -0,0 +1,7 @@ +function errorAbortGetReponse + + errorStruct.message = 'Escape key press detected by getResponse: aborting experiment.'; + errorStruct.identifier = 'getResponse:abortRequested'; + + error(errorStruct) +end \ No newline at end of file diff --git a/errorRestrictedKeysGetReponse.m b/errorRestrictedKeysGetReponse.m new file mode 100644 index 0000000..81cc77e --- /dev/null +++ b/errorRestrictedKeysGetReponse.m @@ -0,0 +1,7 @@ +function errorRestrictedKeysGetReponse + + errorStruct.message = 'getResponse reported a key press on a restricted key'; + errorStruct.identifier = 'getResponse:restrictedKey'; + + error(errorStruct) +end \ No newline at end of file diff --git a/tests/getResponseTest.m b/tests/getResponseTest.m new file mode 100644 index 0000000..4d69670 --- /dev/null +++ b/tests/getResponseTest.m @@ -0,0 +1,81 @@ +addpath(fullfile(pwd, '..')) + +clear; +clc; + +if IsOctave + more off % for a better display experience +end + +cfg = setDefaultsPTB; + +%% Set parameters + +% Decide which device you want to collect responses from +deviceNumber = []; % default device (PTB will find it for you) + +cfg.keyboard.responseKey = {'a', 'b'}; +cfg.keyboard.responseKey{end+1} = cfg.keyboard.escapeKey; + + +%% Final checks + +getOnlyPress = 1; + +% Make sure keyboard mapping is the same on all supported operating systems +KbName('UnifyKeyNames'); + +fprintf('\nDuring the next 5 seconds we will collect responses on the following keys: \n\n'); +if isempty(cfg.keyboard.responseKey) + fprintf('\nALL KEYS\n\n'); +else + for iKey=1:numel(cfg.keyboard.responseKey) + fprintf('\n%s', cfg.keyboard.responseKey{iKey}); + end + fprintf('\n\n'); +end + + +%% Run manual test +try + + getResponse('init', deviceNumber, cfg); + + getResponse('start', deviceNumber); + + WaitSecs(5); + + responseEvents = getResponse('check', deviceNumber, cfg, getOnlyPress); + + getResponse('release', deviceNumber); + + + %if some keys were pressed and that we are supposed to listen to only some + %keys, we make sure that only those keys were listened to + + if ~isempty(cfg.keyboard.responseKey) && isfield(responseEvents, 'keyName') + + for iEvent = 1:size(responseEvents, 1) + fprintf(' %s was pressed\n ', ... + responseEvents(iEvent).keyName); + + if ~any(strcmp({responseEvents(iEvent).keyName}, cfg.keyboard.responseKey)) + errorRestrictedKeysGetReponse(); + end + end + + end + +catch ME + + switch ME.identifier + case 'getResponse:restrictedKey' + rethrow(ME) + + otherwise + getResponse('release', deviceNumber); + + rethrow(ME) + end + +end From 33ea8574a21b21e79a9d870bf3cd9c0f7f872fcb Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 15:15:42 +0200 Subject: [PATCH 21/25] make getResponse verbose by default --- getResponse.m | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/getResponse.m b/getResponse.m index c8f1eeb..e335b8e 100644 --- a/getResponse.m +++ b/getResponse.m @@ -156,36 +156,34 @@ end -function talkToMe(action, expParameters) +function talkToMe(action) -if ~isfield(expParameters, 'verbose') || isempty(expParameters.verbose) - expParameters.verbose = false; -end switch action - + case 'init' - - case 'start' - - fprintf('\n starting to listen to keypresses\n') + msg = 'Initialising KbQueue.'; + + case 'start' + msg = 'Starting to listen to keypresses.'; case 'check' - - if expParameters.verbose - fprintf('\n checking recent keypresses\n') - end + msg = 'Checking recent keypresses.'; case 'flush' + msg = 'Reinitialising keyboard queue.'; - if expParameters.verbose - fprintf('\n reinitialising keyboard queue\n') - end - - case 'stop' + case 'stop' + msg = 'Stopping to listen to keypresses.'; + + case 'release' + msg = 'Releasing KbQueue.'; - fprintf('\n stopping to listen to keypresses\n\n') + otherwise + msg = ''; end +fprintf('\n %s\n\n', msg); + end \ No newline at end of file From 90dcc065cb86ecb0b066cb3e7f5d78fe636dadbc Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 15:21:31 +0200 Subject: [PATCH 22/25] update getResponse - can listen to a specific device - all information about is passed through the cfg structure even info about restricted keys - information are passed through cfg.keyboard --- getResponse.m | 75 ++++++++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/getResponse.m b/getResponse.m index e335b8e..1191ca6 100644 --- a/getResponse.m +++ b/getResponse.m @@ -1,4 +1,4 @@ -function responseEvents = getResponse(action, cfg, expParameters, getOnlyPress) +function responseEvents = getResponse(action, deviceNumber, cfg, getOnlyPress) % wrapper function to use KbQueue % The queue will be listening to key presses on the response box as defined % in the cfg structure : see setParameters for more details. @@ -43,14 +43,22 @@ % pressed == 0 --> the key was released +if nargin < 2 || isempty(deviceNumber) + deviceNumber = -1; +end + +if nargin < 3 + cfg = struct(... + 'keyboard', struct('responseKey', {})... + ); +end + if nargin < 4 getOnlyPress = true; end responseEvents = struct; -responseBox = cfg.responseBox; - switch action case 'init' @@ -60,82 +68,81 @@ ListenChar(-1); % Clean and realease any queue that might be opened - KbQueueRelease(responseBox); + KbQueueRelease(deviceNumber); - keysOfInterest = setKeysOfInterest(expParameters); + keysOfInterest = setKeysOfInterest(cfg.keyboard); % Create the keyboard queue to collect responses. - KbQueueCreate(responseBox, keysOfInterest); - - - case 'start' - - KbQueueStart(responseBox); + KbQueueCreate(deviceNumber, keysOfInterest); + case 'start' + KbQueueStart(deviceNumber); case 'check' + responseEvents = getAllKeyEvents(responseEvents, deviceNumber, getOnlyPress); responseEvents = getAllKeyEvents(responseEvents, responseBox, getOnlyPress); checkAbort(cfg) - case 'flush' - - KbQueueFlush(responseBox); - - - case 'stop' + case 'flush' + KbQueueFlush(deviceNumber); + + case 'stop' + KbQueueStop(deviceNumber) - KbQueueRelease(responseBox); + case 'release' + KbQueueRelease(deviceNumber); % Give me my keyboard back... Pretty please. ListenChar(0); - - + end -talkToMe(action, expParameters); +talkToMe(action); end -function keysOfInterest = setKeysOfInterest(expParameters) +function keysOfInterest = setKeysOfInterest(keyboard) % list all the response keys we want KbQueue to listen to % by default we listen to all keys % but if responseKey is set in the parameters we override this -keysOfInterest = ones(1,256); +keysOfInterest = zeros(1,256); -fprintf('\n Will be listening for key presses on : ') +fprintf('\n Will be listening for key presses on : '); -if isfield(expParameters, 'responseKey') && ~isempty(expParameters.responseKey) +if ~isempty(keyboard.responseKey) - responseTargetKeys = nan(1,numel(expParameters.responseKey)); + responseTargetKeys = nan(1,numel(keyboard.responseKey)); - for iKey = 1:numel(expParameters.responseKey) - fprintf('\n - %s ', expParameters.responseKey{iKey}) - responseTargetKeys(iKey) = KbName(expParameters.responseKey(iKey)); + for iKey = 1:numel(keyboard.responseKey) + fprintf('\n - %s ', keyboard.responseKey{iKey}); + responseTargetKeys(iKey) = KbName(keyboard.responseKey(iKey)); end keysOfInterest(responseTargetKeys) = 1; else - fprintf('ALL KEYS.') + keysOfInterest = ones(1,256); + + fprintf('ALL KEYS.'); end -fprintf('\n\n') +fprintf('\n\n'); end -function responseEvents = getAllKeyEvents(responseEvents, responseBox, getOnlyPress) +function responseEvents = getAllKeyEvents(responseEvents, deviceNumber, getOnlyPress) iEvent = 1; -while KbEventAvail(responseBox) +while KbEventAvail(deviceNumber) - event = KbEventGet(responseBox); + event = KbEventGet(deviceNumber); % we only return the pressed keys by default if getOnlyPress==true && event.Pressed==0 From bcca2cf154a767e94f72477478dc48da6c4db4b7 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 15:22:32 +0200 Subject: [PATCH 23/25] make getResponse abort if escape key is pressed - this will only work during the "check" --- getResponse.m | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/getResponse.m b/getResponse.m index 1191ca6..160cf39 100644 --- a/getResponse.m +++ b/getResponse.m @@ -81,10 +81,8 @@ case 'check' responseEvents = getAllKeyEvents(responseEvents, deviceNumber, getOnlyPress); - responseEvents = getAllKeyEvents(responseEvents, responseBox, getOnlyPress); - - checkAbort(cfg) - + checkAbortGetResponse(responseEvents, cfg); + case 'flush' KbQueueFlush(deviceNumber); @@ -162,6 +160,16 @@ end +function checkAbortGetResponse(responseEvents, cfg) + + if isfield(responseEvents, 'keyName') > 0 && ... + any( ... + strcmpi({responseEvents(:).keyName}, cfg.keyboard.escapeKey)... + ) + errorAbortGetReponse(responseEvents) + end +end + function talkToMe(action) From c186e4c519d3a7fc66b9e0eb13d58f72a5ee2a8b Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 15:24:02 +0200 Subject: [PATCH 24/25] keysOfInterest = zeros(1,256); --- demos/CPP_getResponseDemo.m | 2 +- getResponse.m | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/demos/CPP_getResponseDemo.m b/demos/CPP_getResponseDemo.m index 47a2b1b..f6c2c0f 100644 --- a/demos/CPP_getResponseDemo.m +++ b/demos/CPP_getResponseDemo.m @@ -107,7 +107,7 @@ end fprintf('\n %s was %s at time %.3f seconds\n', ... - responseEvents(iEvent).key_name, ... + responseEvents(iEvent).keyName, ... eventType, ... responseEvents(iEvent).onset - startSecs); diff --git a/getResponse.m b/getResponse.m index 160cf39..63701f6 100644 --- a/getResponse.m +++ b/getResponse.m @@ -1,10 +1,12 @@ function responseEvents = getResponse(action, deviceNumber, cfg, getOnlyPress) -% wrapper function to use KbQueue -% The queue will be listening to key presses on the response box as defined -% in the cfg structure : see setParameters for more details. +% Wrapper function to use KbQueue % -% Check the CPP_getResponseDemo for a quick script on how to use it. +% The queue will be listening to key presses on a keyboard device: +% cfg.keyboard.responseBox or cfg.keyboard.keyboard are 2 main examples. +% +% When no deviceNumber is set then it will listen to the default device. % +% Check the CPP_getResponseDemo for a quick script on how to use it. % % % INPUT @@ -12,9 +14,11 @@ % - action: Defines what we want the function to do % - init: to initialise the queue % - start: to start listening to keypresses -% - check: -% - flush: -% - stop: +% - check: checks all the key presses events since 'start', or since last +% 'check' or 'flush' (whichever was the most recent) +% - flush: empties the queue of events in case you want to restart from a clean +% queue +% - stop: stops listening to key presses % % - getOnlyPress: if set to true the function will only return the key presses and % will not return when the keys were released (default=true) @@ -149,7 +153,7 @@ responseEvents(iEvent,1).onset = event.Time; responseEvents(iEvent,1).trial_type = 'response'; responseEvents(iEvent,1).duration = 0; - responseEvents(iEvent,1).key_name = KbName(event.Keycode); + responseEvents(iEvent,1).keyName = KbName(event.Keycode); responseEvents(iEvent,1).pressed = event.Pressed; iEvent = iEvent + 1; From f91826c0c843854b1f133cf7040dd0867939b865 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 19 Jul 2020 15:38:12 +0200 Subject: [PATCH 25/25] update readme --- README.md | 20 ++++++++++++-------- getResponse.m | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8b9332d..20e68a2 100644 --- a/README.md +++ b/README.md @@ -130,23 +130,27 @@ It is wrapper function to use `KbQueue` which is definitely what you should used You can easily collect responses while running some other code at the same time. -It will only take responses from the `response box` which can simply be the "main keyboard" or -another keyboard connected to the computer or the response box that the participant is using. +It will only take responses from one device which can simply be the "main keyboard" +(the default device that PTB will find) or another keyboard connected to the computer +or the response box that the participant is using. You can use it in a way so that it only takes responses from certain keys and ignore others (like the triggers from an MRI scanner). If you want to know more on how to use it check its help section and the `CPP_getResponseDemo.m`. -To select a specific keyboard to be used by the experimenter or the participant, you need to know -the value assigned by PTB to each keyboard device. +In brief, there are several actions you can execute with this function. -To know this copy-paste this on the command window: +- init: initialize the buffer for key presses on a given device (you can also specify the keys of interest that should be listened to). +- start: start listening to the key presses (carefully insert into your script - where do you want to start buffering the responses). +- check: till that point, it will check the buffer for all key presses. + - It only reports presses on the keys of interest mentioned at initialization. + - It **can** also check for presses on the escape key and abort if the escape key is part of the keys of interest. +- flush: Empties the buffer of key presses in case you want to discard any previous key presses. +- stop: Stops buffering key presses. You can still restart by calling "start" again. +- release: Closes the buffer for good. - [keyboardNumbers, keyboardNames] = GetKeyboardIndices; - keyboardNumbers - keyboardNames ### deg2Pix diff --git a/getResponse.m b/getResponse.m index 63701f6..e77032e 100644 --- a/getResponse.m +++ b/getResponse.m @@ -40,7 +40,7 @@ % % responseEvents.duration = 0; % -% responseEvents.key_name : the name of the key pressed +% responseEvents.keyName : the name of the key pressed % % responseEvents(iEvent,1).pressed : if % pressed == 1 --> the key was pressed