Search code examples
matlabsignal-processingtimingpsychtoolbox

Using KbQueueCheck to continuously check for device input


I am using PsychToolbox to run an experiment on a fMRI scanner. I want to collect the timepoints of pulses sent out by the scanner. If you are not familiar with this topic: A scanner gives out a signal - equivalent to pressing keyboard digit "5" - in defined time-intervals. The first such signal starts the code, such that measurements by the scanner, and starting time of the code are synchronized. My code is sequential, as sketched out below. How could I implement a loop which at any time checks for these inputed "5", even though my main code runs in a "for"-loop?

My code:

% here I wait for the scanner to input a "5", then my code will start
KbQueueCreate;
KbQueueStart;
Trigger = KbName('5%');

keyboard = -1;
AllowedKeys = zeros(256,1); %Disable all keys
AllowedKeys([LeftPress RightPress MiddlePress]) = 1; %Also allow escape always.


while 1

      [pressed, firstPress] = KbQueueCheck();

    if firstPress(Trigger)
        startExperiment = GetSecs;
break
    end
end


KbQueueStop;
KbQueueRelease;

% In the foolowing loop, the main experiment runs. here I show a screen, 
% signal etc.

for i = 1:nrTrials


% here I would like to have code checking for scanner inputs

end

The function KbQueueCheck should check for any keyboard input starting from the most recent call to KbQueueStart. Any input is very welcome.


Solution

  • We do the similar stuff with our scanner, so I providing a function I use. I normally only detect first trigger to start the stimulus, and won't read later triggers. But I do register all triggers, as well as other response, in the background, so I can check the events in case there is any problem.

    The function, KbQueue.m, can do almost all related response collection, including background key register. Please read the help for how to use different functionality. Some part of the code, such as different keyboard indices, is not well tested for MAC and Linux.

    function varargout = KbQueue(cmd, param)
    % KbQueueXXX series functions have some good features: 
    %  1. detect short key event, like those from fOPR KbCheck may miss; 
    %  2. detect only interested keys; 
    %  3. buffer key event while code is running. 
    % 
    % But the syntax of KbQueueXXX is a little inconvenient due to the flexibility.
    % 
    % This code tries to provide a convenient wrapper for practical purpose, by
    % sacrificing some flexibility, like deviceIndex etc. By default, KbQueue
    % detects events for the first device (this doesn't seem the case for Windows,
    % where it detects all devices). If you have more than one keyboard, and like to
    % detect a different one, you have to add following in your code: 
    % 
    % global KbQueueDevice; KbQueueDevice = 2; 
    % 
    % where 2 means the second device. You need to find this index for your target
    % keyboard.
    % 
    % KbQueue('start', keys); 
    % - Create a queue and start it. The second input specify the keys to be queued.
    % The default are numbers 1~5, both keyboard and keypad numbers. The key names
    % adopt those with KbName('UnifyKeyNames'). The 'start' command can be called
    % more than once to update new keys. If it is not called, other subfunctions
    % will call it with the default keys automatically. 
    % 
    % Example:
    % KbQueue('start', {'LeftArrow' 'RightArrow'}); % start to queue 2 arrows
    % 
    % The input keys can also be 256-element keyCode, such as that returned by
    % KbCheck etc. It can also be the index in the keyCode, such as that returned by
    % KbQueue('wait') etc.
    % 
    % nEvents = KbQueue('nEvents' [, 'type']);
    % - Return number of events in the queue. The default event type is 'press',
    % which returns the number of keypress. It can be 'release' or 'all', which will
    % return number of release events or all events.
    % 
    % [pressTime, pressCode] = KbQueue('wait' [, secs or keys]);
    % - Wait for a new event, and return key press time and keycode. During the
    % wait, pressing Escape will abort code execution, unless 'Escape' is included
    % in defined keys.
    % 
    % If there is no second input, this will wait forever till a defined key by
    % previous 'start' command is detected.
    % 
    % If the second input is numeric, it will be interpreted as the seconds to wait,
    % and wait for a key defined by previous 'start' command is pressed, or the
    % seconds has elapsed, whichever is earlier. If there is no any event during the
    % time window, both output will be empty. For example:
    % 
    % [t, key] = KbQueue('wait', 1); % wait for 1 sec or a key is detected
    % 
    % If the second input is a string or cellstr, it will be interpreted as the
    % key(s) to detect. The provided keys here affects only this call, i.e. it has
    % no effect on the queue keys defined by previous 'start' command. For example:
    % 
    % t = KbQueue('wait', {'5%' '5'}); % wait till 5 is pressed
    % 
    % [pressCodeTime, releaseCodeTime] = KbQueue('check' [, t0]);
    % - Return first and last press keycode and times for each queued key, and
    % optionally release keycode and times. The output will be empty if there is no
    % buffered response. Both output are two row vector, with the first row for the
    % keycode and second row for times. If the second input t0, default 0, is
    % provided, the returned times will be relative to t0. For example:
    % 
    % press = KbQueue('check'); % return 1st and last key press in the queue 
    % pressedKey = KbName(press(1, :); % convert keycode into key names
    % 
    % KbQueue('flush');
    % - Flush events in the current queue.
    % 
    % t = KbQueue('until', whenSecs);
    % - This is the same as WaitSecs('UntilTime', whenSecs), but allows to exit by
    % pressing ESC. If whenSecs is not provided or is already passed, this still
    % checks ESC, so allows use to quit. For example:
    % KbQueue('until', GetSecs+5); % wait for 5 secs from now
    % KbQueue('until'); % only check ESC exit
    % 
    % [pressCodeTime, releaseCodeTime] = KbQueue('stop' [, t0]);
    % - This is similar to 'check' command, but it returns all queued events since
    % last 'flush', or since the queue was  started. It also stops and releases the
    % queue. This provides a way to check response in the end of a session. For
    % example: 
    % KbQueue('start', {'5%' '5'}); % start to queue 5 at beginning of a session
    % KbQueue('flush'); % optionally remove unwanted events at a time point 
    % t0 = GetSecs; % the start time of your experiment 
    % % run your experiment
    % pressCodeTime = KbQueue('stop', t0); % get all keycode and time
    
    % 10/2012   wrote it, xiangrui.li@gmail.com
    % 12/2012   try to release queue from GetChar etc, add nEvents
    % 11/2014   try to use response device for OSX and Linux
    
    persistent kCode started evts;
    global KbQueueDevice; % allow to change in user code
    if isempty(started), started = false; end
    if nargin<1 || isempty(cmd), cmd = 'start'; end
    if any(cmd=='?'), subFuncHelp('KbQueue', cmd); return; end
    
    if strcmpi(cmd, 'start')
        if started, BufferEvents; end
        if nargin<2
            param = {'1' '2' '3' '4' '5' '1!' '2@' '3#' '4$' '5%'};
        end
        KbName('UnifyKeyNames');
        if ischar(param) || iscellstr(param) % key names
            kCode = zeros(256, 1);
            kCode(KbName(param)) = 1;
        elseif length(param)==256 % full keycode
            kCode = param;
        else
            kCode = zeros(256, 1);
            kCode(param) = 1;        
        end
        if isempty(KbQueueDevice), KbQueueDevice = responseDevice; end
        try KbQueueReserve(2, 1, KbQueueDevice); end %#ok
        KbQueueCreate(KbQueueDevice, kCode);
        KbQueueStart(KbQueueDevice);
        started = true;
        return;
    end
    
    if ~started, KbQueue('start'); end
    
    if strcmpi(cmd, 'nEvents')
        BufferEvents;
        n = length(evts);
        if n, nPress = sum([evts.Pressed] == 1);
        else nPress = 0;
        end
        if nargin<2, param = 'press'; end
        if strncmpi(param, 'press', 5)
            varargout{1} = nPress;
        elseif strncmpi(param, 'release', 7)
            varargout{1} = n - nPress;
        else
            varargout{1} = n;
        end
    elseif strcmpi(cmd, 'check')
        [down, p1, r1, p2, r2] = KbQueueCheck(KbQueueDevice);
        if ~down 
            varargout = repmat({[]}, 1, nargout);
            return;
        end
        if nargin<2, param = 0; end
        i1 = find(p1); i2 = find(p2);
        varargout{1} = [i1 i2; [p1(i1) p2(i2)]-param];
        if nargout>1
            i1 = find(r1); i2 = find(r2);
            varargout{2} = [i1 i2; [r1(i1) r2(i2)]-param];
        end
    elseif strcmpi(cmd, 'wait')
        endSecs = GetSecs;
        secs = inf; % wait forever unless secs provided
        newCode = kCode; % use old keys unless new keys provided
        if nargin>1 % new keys or secs provided
            if isempty(param), param = inf; end
            if isnumeric(param) % input is secs
                secs = param;
            else % input is keys
                newCode = zeros(256, 1);
                newCode(KbName(param)) = 1;
            end
        end
        esc = KbName('Escape');
        escExit = ~newCode(esc);
        newCode(esc) = 1;
        changed = any(newCode~=kCode);
        if changed % change it so we detect new keys
            BufferEvents;
            KbQueueCreate(KbQueueDevice, newCode);
            KbQueueStart(KbQueueDevice); % Create and Start are twins here :)
        else
            KbQueueFlush(KbQueueDevice, 1); % flush KbQueueCheck buffer
        end
        endSecs = endSecs+secs;
        while 1
            [down, p1] = KbQueueCheck(KbQueueDevice);
            if down || GetSecs>endSecs, break; end
            WaitSecs('YieldSecs', 0.005);
        end
        if changed % restore original keys if it is changed
            BufferEvents;
            KbQueueCreate(KbQueueDevice, kCode);
            KbQueueStart(KbQueueDevice);
        end
        if isempty(p1)
            varargout = repmat({[]}, 1, nargout);
            return;
        end
        ind = find(p1);
        if escExit && any(ind==esc)
            error('User pressed ESC. Exiting ...'); 
        end
        varargout = {p1(ind) ind};
    elseif strcmpi(cmd, 'flush')
        KbQueueFlush(KbQueueDevice, 3); % flush both buffers
        evts = [];
    elseif strcmpi(cmd, 'until')
        if nargin<2 || isempty(param), param = 0; end
        while 1
            [down, t, kc] = KbCheck(-1);
            if down && kc(KbName('Escape'))
                error('User pressed ESC. Exiting ...'); 
            end
            if t>=param, break; end
            WaitSecs('YieldSecs', 0.005);
        end
        if nargout, varargout = {t}; end
    elseif strcmpi(cmd, 'stop')
        KbQueueStop(KbQueueDevice);
        started = false;
        if nargout
            BufferEvents;
            if isempty(evts)
                varargout = repmat({[]}, 1, nargout);
                return;
            end
    
            isPress = [evts.Pressed] == 1;
            if nargin<2, param = 0; end
            varargout{1} = [[evts(isPress).Keycode] 
                            [evts(isPress).Time]-param];
            if nargout>1
                varargout{2} = [[evts(~isPress).Keycode] 
                                [evts(~isPress).Time]-param];
            end
        end
        KbQueueRelease(KbQueueDevice);
    else 
        error('Unknown command: %s.', cmd);
    end
    
        function BufferEvents % buffer events so we don't lose them
            n = KbEventAvail(KbQueueDevice);
            if n<1, return; end
            for ic = 1:n
                foo(ic) = KbEventGet(KbQueueDevice); %#ok
            end
            if isempty(evts), evts = foo;
            else evts = [evts foo];
            end
        end
    
    end
    
    function idx = responseDevice
        if IsWin, idx = []; return; end % all keyboards
    
        clear PsychHID; % refresh
        [ind, pName] = GetKeyboardIndices;
        if IsOSX
            idx = ind(1); % based on limited computers
        else % Linux
            for i = length(ind):-1:1
                if ~isempty(strfind(pName{i}, 'HIDKeys')) || ...
                    ~isempty(strfind(pName{i}, 'fORP')) % faked, need to update
                    idx = ind(i);
                    return;
                end
                idx = ind(end); % based on limited computers
            end
        end
    end