function varargout = CedrusResponseBox(cmd, varargin)
% CedrusResponseBox - Interface to Cedrus Response Boxes.
%
% This function provides an interface to response button boxes from Cedrus,
% specifically model RB 530,...,830 and compatible models supporting the
% XID protocol (see http://www.cedrus.com).
%
% These response boxes connect via a serial line link interface, or a USB
% interface which emulates a serial link interface. They support the XID
% protocol for communication. See http://www.cedrus.com/xid for details.
%
% This function allows to establish a connection to the box, control a few
% of its parameters and most importantly query its button state and
% associated button press timestamps.
%
% It supports multiple subcommands, which accept and return different
% arguments, as listed below.
%
% Limitations:
% ------------
%
% Functionality is currently limited mostly to button queries (and RJ-45
% connector state queries), including timestamps, as well as control of
% built-in timers of the box. We also support basic configuration of TTL
% ports, but not yet all settings of the box like e.g., button debounce
% time. Adding such calls is straightforward and simple.
%
% We found communication with the Cedrus boxes to be unreliable quite
% often. It is an open question if this is a flaw in the design of the
% Cedrus devices and their firmware or protocols, or if the programming
% documentation for them is incomplete and therefore our implementation of
% the driver. However, the problems were reproduced under different
% operating systems, serial port drivers, toolboxes by different
% implementations written by different people, so it doesn't seem to be a
% simple glitch in one implementation. In general, the boxes work, but
% don't be surprised if you need to restart your script multiple times
% before you can establish communication, or if the more advanced
% fucntions, e.g., for configuration of the TTL RJ-45 connector, work
% unreliably for no apparent reason. Cedrus has been contacted, but so far
% no resolution or response from them.
%
% In short: If you are looking for a reliable response box that is painfree
% to use, don't buy Cedrus devices!
%
%
% Subfunctions and their meaning:
% -------------------------------
%
% Functions for device init and shutdown: Call once at beginning/end of
% your script. These are slow!
%
% handle = CedrusResponseBox('Open', port [, lowbaudrate]);
% - Open a compatible response box which is connected to the given named
% serial 'port'. 'port'names differ accross operating systems. A typical
% port name for Windows would be 'COM2', whereas a typical port name on OS/X
% or Linux would be the name of a serial port device file, e.g.,
% '/dev/cu.usbserial-FTDI125ZX9' on OS/X, or '/dev/ttyS0' on Linux.
%
% All names on OS/X are like '/dev/cu.XXXXX', where the XXXXX part depends
% on your serial port device, typically '/dev/cu.usbserial-XXXXX' for
% serial over USB devices with product name XXXXX.
%
% On Linux, all names are of pattern '/dev/ttySxx' for standard serial
% ports, e.g., '/dev/ttyS0' for the first serial port in the system, and of
% type '/dev/ttyUSBxx' for serial over USB devices, e.g., '/dev/ttyUSB0'
% for the first serial line emulated over the USB protocol.
%
%
% After the connection is established and some testing and initialization is,
% done, the function returns a device 'handle', a unique identifier to use
% for all other subfunctions.
%
% By default the commlink is opened at a baud transmission rate of 115200
% Baud (All DIP switches on the box need to be in 'down' position!). If you
% specify the optional flag 'lowbaudrate' as 1, then the speed will be
% lowered to 56 kBaud at device open time -- in case your system works
% unreliably at the higher rate.
%
% By default, the script uses Psychtoolbox's own IOPort() serial link
% driver for communication (ptb_cedrus_drivertype = 2). If you want to use
% a different driver for testing, change the 'ptb_cedrus_drivertype'
% parameter inside the code with the id of a supported driver (Matlab
% serial()). This option may go away in the future and is for debugging
% only!
%
%
% CedrusResponseBox('Close', handle);
% - Close connection to response box. The 'handle' becomes invalid after
% that command.
%
%
% CedrusResponseBox('CloseAll');
% - Close all connections to all response boxes. This is a convenience
% function for quick shutdown.
%
%
% dev = CedrusResponseBox('GetDeviceInfo', handle);
% - Return queried information about the device in a struct 'dev'. 'dev'
% contains (amongst other) the following fields:
%
% General information:
%  dev.Name = Device name string.
%  dev.VersionMajor and dev.VersionMinor = Major and Minor firmware revision.
%  dev.productId = Type of device, e.g., 'Lumina', 'VoiceKey' or 'RB response pad'.
%  dev.modelId   = Submodel of the device if the device is a RB response pad,
%                  e.g., 'RB-530', 'RB-730', 'RB-830' or 'RB-834'.
%
%  dev.port      = Portname of serial port, as passed to the open function.
%
% Diagnostic information for timing: Values of -1 or 0 usually mean "info
% not available".
%
%  dev.roundtriptime   = Median of estimated roundtrip latency for
%                        communication with the box - in seconds.
%
%  dev.roundtripstddev = Standard deviation from mean of roundtrip latency
%  measurements in seconds. Large numbers mean that your operating system
%  has bad scheduling and that reported event timestamps may be uncertain by
%  that amount.
%
%  dev.rttresetdelay   = Duration (in seconds) of a reaction time timer reset sequence
%  Values of more than 3 msecs indicate some problems with the box itself or
%  the communication link -- Measured event times or reaction times may not
%  be trustworthy!
%
%
% Functions for use within script. These are as fast as possible:
%
% CedrusResponseBox('ClearQueues', handle);
% - Clear all queues, discard all pending data.
%
% [status = ] CedrusResponseBox('FlushEvents', handle);
% - Empty/clear/flush the queue of pending events. Use this to get rid of
% any stale button press or release events before start of response
% collection in a trial. E.g., Assume you wait for a subjects keypress and
% finally receive that keypress via 'GetButtons' or 'WaitButtons'. You
% collected your response, the trial is done, but when the subject releases
% the button again, that will generate another event - a release event, in
% which you're not interested. Maybe the subject will accidentally hit the
% button as well. --> Good to clean the queue before a new trial.
%
% This function has a second use as well. It has an optional output
% argument, 'status', which will return the current status of all buttons
% (i.e. whether they are currently being pressed or not).
% Status is a 3 row by 8 column matrix: Row 1 describes the status of the
% up to eight pushbuttons of the box. Row 2 describes the status of the TTL
% lines of the RJ-45 accessory connector. Row 3 describes the status of the
% VoiceKey if any. Columns 1 to 8 of each row correspond to buttons 1-8,
% TTL lines 1-8 or inputs 1-8 of the VoiceKey.
%
% The mapping for the CB-530 for row 1 of 'status' status(1,:) is as follows:
%
% [top ??? left middle right bottom] -- the 2nd entry has no associated
% button, but it may be the scanner trigger input. The mapping on other boxes
% may be different.
%
% This is useful if you just want to know whether the subject is currently
% pressing any buttons before you proceed, but are not fussed about timing.
%
% E.g. I often find myself doing the following:
%   buttons = 1;
%   while any(buttons(1,:))
%     buttons = CedrusResponseBox('FlushEvents', mybox);
%   end
%
% ...to wait for the subject to release any buttons which might currently be down.
%
% evt = CedrusResponseBox('GetButtons', handle);
% - Return next queued button-press or button-release event from the box.
% Each time a button on the box is pressed or released, and each time the
% state of the accessory connector changes, an "event" data packet is sent
% from the box to the computer. The packet is timestamped with the time of
% the triggering event, as measured by the boxes reaction time timer.
%
% This function checks if such an event is available and returns its
% description in a 'evt' struct, if so. If no event is pending, it returns an
% empty 'evt', ie. isempty(evt) is true.
%
% 'evt' for a real fetched event is a struct with the following fields:
%
% evt.raw     = "raw" byte that describes the event. Only for debugging.
%
% evt.port    = Number of the device port on which the event occured. Push
%               buttons and scanner triggers are on port 0, the RJ-45 TTL
%               connector is on port 1, port 2 is the voice-key (if any).
%
% evt.action  = Action that triggered the event:
%               1 = Button press, 0 = Button release for pushbuttons.
%               1 = TTL line high, 0 = TTL line low for RJ-45 I/O lines.
%               1 = Voice onse, 0 = Voice offset/silence for Voicekey.
%
% evt.button  = Number of the button that was pressed or released (1 to 8)
%               or the TTL line that was going high/low. Numbers vary by
%               response box.
%
% evt.buttonID= Descriptive name string for pressed button, e.g., 'top' or
%               'left'. Please note that this mapping is only meaningful
%               for the RB-530 response box.
%
% evt.rawtime = Time of the event in secs since last reset of the reaction
%               time timer, measured in msecs resolution. This value is
%               always valid, but not directly comparable to any other
%               timestamps or time measurements within Psychtoolbox.
%
%
% evt = CedrusResponseBox('WaitButtons', handle);
% - Queries and returns the same info as 'GetButtons', but waits for
% events. If there isn't any event available, will wait until one becomes
% available.
%
% evt = CedrusResponseBox('WaitButtonPress', handle);
% - Like WaitButtons, but will wait until the subject /presses/ a key -- the
% signal that a key has been released is not acceptable -- Button release
% events are simply discarded.
%
%
% evt = CedrusResponseBox('GetBaseTimer', handle [, nSamples=1]);
% - Query current time of base timer of the box. Returned values are in
% seconds, resolution is milliseconds. evt.basetimer is the timers time,
% maybe corrected for serial link receive latency. evt.ptbreceivetime is a
% timestamp taken via PTB's GetSecs() at time of receive of the data.
% evt.ptbtime is the basetimers time mapped into PTB GetSecs time if such a
% mapping is possible, otherwise this field doesn't exist:
% evt.ptbreceivetime and evt.ptbtime shouldn't be significantly different
% if everything is good. Large differences indicate some timing problems
% with the connection to the box, or a timer problem - either with your
% computers timer or the hardware timer of the tox, or significant
% clock-drift between the computers timer and the boxes timer. In any case,
% reaction timer measurements and such will be problematic.
%
% Note that this automatically discards all pending events in the queue before
% performing the timer query!
%
% The optional argument 'nSamples' allows to specify if multiple samples of
% PTB timer vs. the response boxes timer should be measured. If 'nSamples'
% is set to a value greater than one, a cell array with nSamples elements
% will be returned, each corresponding to one measurement. This allows,
% e.g., to check if PTBs timer and the boxes timer drift against each
% other.
%
%
% resetTime = CedrusResponseBox('ResetRTTimer', handle);
% - Reset reaction time timer of box to zero. This should not be neccessary
% if you use the evt.ptbtime timestamps for time measurements or reaction
% time measurements. If you however use uncalibrated mode and the
% evt.rawtime values directly, this function may be useful to establish a
% zero baseline for reaction time measurements. However, as the communication
% delay for sending the reset command can't be reliably measured, using
% such a software triggered timer reset may not be the most reliable way of
% resetting the timer. The function returns 'resetTime' PTB's best guess of
% when the reset was carried out -- essentially a GetSecs() timestamp of
% when the reset command was sent.
%
% Note that this automatically discards all pending
% events in the queue before performing the query!
%
%
% slope = CedrusResponseBox('GetBoxTimerSlope', handle);
% - Compute slope (drift) between computer clock and device clock. 'slope'
% tells how many seconds of time "elapse" on the computer in GetSecs time
% for each "elapsed" second of box time. At device open time, the driver
% takes a timestamp from the device basetimer. This function also takes a
% timestamp and then computes the ratio of differences. The longer you'll
% wait after CedrusResponseBox('Open') before calling this function, the
% more accurate the clock-drift estimate will be.
%
%
% roundtrip = CedrusResponseBox('RoundTripTest', handle);
% - Initiate 100 trials of the roundtrip test of the box. Data is echoed
% forth and back 100 times between PTB and the box, and the latency is
% measured (in seconds, with msecs resolution). The vector of all samples
% is returned in 'roundtrip' for evaluation and debugging. The measured
% latency is also used for delay correction for the 'GetBaseTimer'
% subfunction. However, a roundtrip test is performed automatically when
% opening the response box connection, so this is rarely needed.
%
% Note that this automatically discards all pending
% events in the queue before performing the query!
%
%
% [currentMode] = CedrusResponseBox('SetConnectorMode', handle [, mode]);
% - Set or get mode of operation of external accessory connector: 'mode' can be
% any of the following text strings:
%
% 'GeneralPurpose': Input/Output assignment of pins can be freely
% programmed via the 'DefineInputLinesAndLevels' subcommand (see below),
% and the output lines only change if the 'SetOutputLineLevels' command
% (see below) is used. The connector doesn't change state by itself.
%
% 'ReflectiveContinuous': Line levels reflect button state: Line is active
% if button is pressed and goes inactive when the button is released again.
%
% 'ReflectiveSinglePulse': A single pulse is sent to an output line if a
% button is pressed on the box. Nothing is sent on release.
%
% 'ReflectiveDoublePulse': A single pulse is sent to an output line if a
% button is pressed on the box. Another pulse is sent on button release.
%
% If 'mode' is left out, the function queries and returns the current mode
% as return argument 'currentMode'. If mode is given, nothing is returned.
%
%
% CedrusResponseBox('SetOutputLineLevels', handle, outlevels);
% - Set accessory connector output lines to state specified in 'outlevels'.
% outlevels is an 8 element vector of zeros and ones. Each element
% corresponds to an output pin, and its values sets the output level of
% that pin. Example: outlevel = [1,1,1,1,0,0,0,0] would set the 4 lines
% with the lowest numbers (lines 0,1,2,3) to '1' aka active and the 4 lines
% with the highest numbers (lines 4,5,6,7) to '0' aka inactive.
% This corresponds to XiD command 'ah'.
%
% The command is only effective if connector is set to 'GeneralPurpose'.
%
%
% CedrusResponseBox('DefineInputLinesAndLevels', handle, inputlines, logiclevel, debouncetime);
% - Define which lines on the connector are inputs: 'inputlines' is a
% vector with the line numbers of the input lines. All other lines are
% designated as output lines, e.g., inputlines = [0, 2, 4] would set lines
% 0, 2 and 4 as inputs, remaining lines 1,3,5,6,7 as outputs. 'logiclevel'
% tells if the default TTL level of the input lines is low (logiclevel=1)
% or high (logiclevel=0). Example: logiclevel = 1 means that the lines are
% pulled low by default, so they will detect an active high state -- if
% their level is raised to TTL high state. The argument 'debouncetime' must
% be the debounce time for the input lines in milliseconds. After an event
% on a input line, the box will ignore all further events on than input
% line for 'debouncetime' milliseconds.
%
% This corresponds to XiD commands 'a4', 'a50' and 'a51', as well as 'a6'.
%
% The command is only effective if connector is set to 'GeneralPurpose'.
%
%
% inputLines = CedrusResponseBox('ReadInputLines', handle);
% - Read current state of the connectors input lines: Returns an 8 element
% vector where each element corresponds to one input line and a 1 means
% active, 0 means inactive. This corresponds to XiD command 'ar'.
%
% Note that this automatically discards all pending
% events in the queue before performing the query!
%
% The command is only effective if connector is set to 'GeneralPurpose'.
%
%

% Technical notes:
% USB VendorID of Cedrus:   0x0403
% USB ProductID:            0xf228
%
% Command for manual insertion of serial-over-USB module on Linux, if
% module doesn't recognize Cedrus device id's. Also edit rules files of
% usbdev, so ftdi_sio module gets auto-loaded on Cedrus insertion.
%
% sudo modprobe ftdi_sio product=0xf228

% Disabled help text snippets:
% [,doCalibrate=0]
% If you don't specify the optional 'doCalibrate' flag, or
% leave it at its default setting of 1, a couple of lengthy (multiple
% seconds) timing calibrations and tests are performed. These allow to
% assess the delays in communication between box and Matlab. They will also
% allow to return all times of events (as detected by the box) in PTB's
% standard GetSecs() time reference system -- Timestamps of button press
% events and TTL input events can be directly compared with timestamps
% delivered by other PTB functions like GetSecs, KbCheck, KbWait,
% Screen('Flip') etc.
%
% If you set the 'doCalibrate' flag to zero, all timing calibrations will
% be skipped: Startup time is drastically reduced. However there isn't any
% simple and straightforward way of comparing timestamps or timer readings
% delivered by the box with other timestamps of PTB functions. This only
% makes sense if you use some external triggering mechanism to reset the
% built-in reaction time timer via some external TTL input trigger signals
% and want to use raw timer measurements.

% evt.ptbtime = Time of the event in secs, measured in PTBs "GetSecs"
%               timebase. This is easier to correlate with other
%               timestamps, e.g., Screen('Flip') timestamps, but its
%               reliability hasn't been tested yet for the current
%               software release. When opening a connection to a response
%               box, we perform timing calibrations to establish the
%               mapping of time values as measured by the hardware timers
%               of your response pad to time values in PTB's reference
%               system. If you skipped that calibrations by setting the
%               optional 'doCalibrate' flag to zero at device open time,
%               then the evt.ptbtime field will not be available and you
%               have to cope with evt.rawtime values only.
%


% History:
%
% 03/21/08 Written. Based on example code donated by Cambridge Research Systems. (MK)
% 03/28/08 Altered by Jenny Read.
% 04/03/08 Refined and added MacOS/X support via SerialComm driver. (MK)
% 04/06/08 Improved timing code for mapping of box timers --> GetSecs time. (MK)
% 04/17/08 Disable Boxtime->Ptbtime mapping for now, use old drivers. (MK)
% 04/23/08 Add additional setup and query commands for external port. (MK)
% 05/09/10 Add additional button label definitions for RB830, contributed
%          by Jochen Laubrock. (MK)
% 6/11/17  Remove dead SerialComm() driver path. (MK)

% Hard-Coded drivertype to use: Defaults to our IOPort driver.
global ptb_cedrus_drivertype;

ptb_cedrus_drivertype = 2;

% Cell array of device structs. Globally available for main function and
% all subfunctions in this file, persistent across invocation:
global ptb_cedrus_devices;

% Subcommand dispatch:
if nargin < 1 || ~ischar(cmd)
    error('Must at least specify subcommand as textstring!')
end

% Following if-end blocks are roughly sorted by frequency of use. The most
% frequently used calls come first to achieve minimum dispatch
% latency in the trial loop.

if strcmpi(cmd, 'FlushEvents')
    % Flush all pending events/data:

    if nargin < 2
        error('You must provide the device "handle" for the box to flush!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    % Perform flush:
    CedrusStatus = FlushEvents(handle);

    if nargout>0
        varargout{1} = CedrusStatus;
    end
    return
end

if strcmpi(cmd, 'ClearQueues')
    % Clear all pending events/data:

    if nargin < 2
        error('You must provide the device "handle" for the box to clear!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    % Perform clear:
    ClearQueues(handle);

    return
end

% Wait until a key-pressed signal is detected:
if strcmpi(cmd, 'WaitButtonPress')
    if nargin < 2
        error('You must provide the device "handle" for the box to wait for!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    % Wait until the box reports that a key has been pressed (NOT released)
    keypress = 0;
    while ~keypress
        % Need at least 6 bytes. Only wait if not available:
        if BytesAvailable(handle) < 6
            % Poll at 4 msecs intervals as long as input buffer is totally empty,
            % to allow the CPU to execute other tasks.
            while BytesAvailable(handle) == 0
                % Choose 4 msecs, as PTB would not release the cpu for wait
                % times below 3 msecs (to account for MS-Windows miserable
                % process scheduler).
                if IsWin
                    WaitSecs(0.004);
                else
                    WaitSecs(0.001);
                end
            end

            % At least 1 byte available -- soon we'll have our required minimimum 6
            % bytes :-) -- Spin-Wait for the remaining few microseconds:
            while BytesAvailable(handle) < 6; end
        end

        % At least 6 bytes for one event available: Try to read them from box:
        response = ReadDev(handle, 6);
        
        % Timestamp receive completion in PTB's timeframe. Allows to get a
        % feeling on how much time elapses between keypress and data receive:
        ptbfetchtime = GetSecs;

        % Unpack this binary data into a more readable form:
        evt = ExtractKeyPressData(handle,response);        
        evt.ptbfetchtime = ptbfetchtime;
        
        keypress = evt.action;
        % This is 0 if the key was released, 1 if it was pressed down,
        % which is what we are waiting for.
    end
    % Assign evt as output argument:
    varargout{1} = evt;

    return % JCAR added
end

% Wait for at least one button event available:
if strcmpi(cmd, 'WaitButtons')
    if nargin < 2
        error('You must provide the device "handle" for the box to wait for!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    % Need at least 6 bytes. Only wait if not available:
    if BytesAvailable(handle) < 6
        % Poll at 4 msecs intervals as long as input buffer is totally empty,
        % to allow the CPU to execute other tasks.
        while BytesAvailable(handle) == 0
            % Choose 4 msecs, as PTB would not release the cpu for wait
            % times below 3 msecs (to account for MS-Windows miserable
            % process scheduler).
            if IsWin
                WaitSecs(0.004);
            else
                WaitSecs(0.001);
            end
        end

        % At least 1 byte available -- soon we'll have our required minimimum 6
        % bytes :-) -- Spin-Wait for the remaining few microseconds:
        while BytesAvailable(handle) < 6; end
    end

    % At least 6 bytes for one event available: Try to read them from box:
    response = ReadDev(handle, 6);

    % Timestamp receive completion in PTB's timeframe. Allows to get a
    % feeling on how much time elapses between keypress and data receive:
    ptbfetchtime = GetSecs;

    % Unpack this binary data into a more readable form:
    evt = ExtractKeyPressData(handle,response);
    evt.ptbfetchtime = ptbfetchtime;

    % Assign evt as output argument:
    varargout{1} = evt;

    return % JCAR added
end

% Polling Button state query: Returns immediately if no events available.
if strcmpi(cmd, 'GetButtons')
    % Button state change event query:

    if nargin < 2
        error('You must provide the device "handle" for the box to query!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    % XID devices send six bytes of information.
    % Check if at least 6 bytes for a full event report are available:
    if BytesAvailable(handle) < 6
        % Did not receive at least 6 bytes - No new event happened. Return an
        % empty evt result.
        evt = [];
        varargout{1} = evt;
        return;
    end

    % At least 6 bytes for one event available: Try to read them from box:
    response = ReadDev(handle, 6);

    % Timestamp receive completion in PTB's timeframe. Allows to get a
    % feeling on how much time elapses between keypress and data receive:
    ptbfetchtime = GetSecs;
    
    % Unpack this binary data into a more readable form:
    evt = ExtractKeyPressData(handle,response);
    evt.ptbfetchtime = ptbfetchtime;

    % Assign evt as output argument:
    varargout{1} = evt;

    return;
end

if strcmpi(cmd, 'RoundTripTest')
    % Initiate roundtrip-test procedure: Will receive data from device,
    % echo it back, then receive a roundtrip timestamp:

    if nargin < 2
        error('You must provide the device "handle" for the box to query!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});
        
    % Start roundtrip test and return results: They are also stored in the
    % device struct of 'handle':
    varargout{1} = RoundTripTestDev(handle);
    return;
end

if strcmpi(cmd, 'ResetRTTimer')
    % RT timer reset request:
    if nargin < 2
        error('You must provide the device "handle" for the box!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    % Reset reaction time timer of device and assign estimated time of reset
    % as basetime for all timing calculations:
    varargout{1} = ResetRTT(handle);

    return;
end

if strcmpi(cmd, 'Test')
    % Flush all pending events/data:

    if nargin < 2
        error('You must provide the device "handle" for the box!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    TestThis(handle);
    
    return
end

if strcmpi(cmd, 'GetBaseTimer')
    % Base Timer query:

    if nargin < 2
        error('You must provide the device "handle" for the box to query!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    if nargin >=3
        nQueries = varargin{2};
    else
        nQueries = 1;
    end
    
    % Preallocate output cell array:
    evts = cell(nQueries, 1);
    
    % Flush input buffer:
    FlushEvents(handle);

    for i=1:nQueries
        % Send basetimer query code:
        evt.roundtriptime = GetSecs;
        WriteDev(handle, 'e3');

        % Spin-Wait for first byte:
        while BytesAvailable(handle) < 1; end;

        % Timestamp receive completion of first byte. This is closest to the
        % real time when the transmitted timer values was actually generated on
        % the device:
        evt.ptbreceivetime = GetSecs;

        % Receive packet, then parse into raw timer value (in seconds):
        evt.basetimer = receiveAndParseTimePacket(handle);

        % Store roundtrip-time of query:
        evt.roundtriptime = evt.ptbreceivetime - evt.roundtriptime;
        
        % Correct reported time value of basetimer by half roundtrip delay
        % of serial link: We assume that transmission took half the total
        % measured roundtrip time, so we need to add that delay to the
        % basetimer value to get an estimate of the "real" basetimer time
        % at time of response packet receive "ptbtime":
        evt.basetimer = evt.basetimer + ptb_cedrus_devices{handle}.roundtriptime/2;

        % Assign mapped PTB GetSecs time if mapping possible:
        if ptb_cedrus_devices{handle}.baseToPtbSlope ~= 0
            % Simple linear equation mapping:
            evt.ptbtime = ptb_cedrus_devices{handle}.baseToPtbOffset + ptb_cedrus_devices{handle}.baseToPtbSlope * evt.basetimer;
        end
        
        % Assign i'th measurement event:
        evts{i} = evt;
    end
    
    % Assign evts as output argument:
    if nQueries > 1
        varargout{1} = evts;
    else
        varargout{1} = evt;
    end
    
    return;
end

if strcmpi(cmd, 'SetConnectorMode')
    % Change mode of external accessory connector:
    if nargin < 2
        error('You must provide the device "handle" for the box!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    if nargin < 3
        % Query instead of set:

        % Send query code:
        WriteDev(handle, '_a1');
        
        % Retrieve response:
        cc = ReadDev(handle, 4);
        WaitSecs(0.25);

        switch char(cc)
            case {'_a10'}
                rc = 'generalpurpose';
            case {'_a11'}
                rc = 'reflectivecontinuous';
            case {'_a12'}
                rc = 'reflectivesinglepulse';
            case {'_a13'}
                rc = 'reflectivedoublepulse';
            otherwise
                rc = cc;
                warning('SetConnectorMode received unknown old mode response!');
        end
        
        varargout{1} = rc;
        return;
    end

    switch lower(char(varargin{2}))
        case {'generalpurpose'}
            cc = 'a10';
        case {'reflectivecontinuous'}
            cc = 'a11';
        case {'reflectivesinglepulse'}
            cc = 'a12';
        case {'reflectivedoublepulse'}
            cc = 'a13';
        otherwise
            error('Unknown connector mode specified to SetConnectorMode.');
    end

    % Send command code:
    WriteDev(handle, cc);
    WaitSecs(0.25);
    
    return;
end

if strcmpi(cmd, 'DefineInputLinesAndLevels')
    % Change I/O assignment and default logic level of pins:
    if nargin < 2
        error('DefineInputLinesAndLevels: You must provide the device "handle" for the box!');
    end

    if nargin < 3
        error('DefineInputLinesAndLevels: You must provide the new list of integer input pin numbers!');
    end

    if nargin < 4
        error('DefineInputLinesAndLevels: You must provide the new logic detection level for the inputs: 1 for "Detect Low->High transition", 0 for "Detect High->Low" !');
    end

    if nargin < 5
        error('DefineInputLinesAndLevels: You must provide the new debounce time for TTL inputs in milliseconds !');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    ClearQueues(handle);
    
    inpins = varargin{2};
    if ~isnumeric(inpins)
        error('DefineInputLinesAndLevels: You must provide the new list of integer input pin numbers!');
    end

    if ~isempty(inpins)
        if min(inpins) < 0 || max(inpins) > 5
            error('DefineInputLinesAndLevels: Only input pin numbers between 0 and 5 are valid!');
        end
    end
    
    ipin = 0;
    for i=1:length(inpins)
        ipin = ipin + 2^(inpins(i));
    end
    
    % Send command code and mask:
    WriteDev(handle, ['a4' char(ipin)]);
    
    % Wait a bit:
    WaitSecs(1);    
    ClearQueues(handle);
    WaitSecs(1);    
    
    % Retrieve new mask:
    WriteDev(handle, '_a4');
    WaitSecs(1);
    resp = ReadDev(handle, 4);
    if length(resp)<4 || ~strcmp(char(resp(1:3)), '_a4')
        warning('DefineInputLinesAndLevels: Invalid response received from device!');
        char(resp) %#ok<NOPRT>
        varargout{1} = 0;
        return;
    else
        if resp(4)~=ipin
            warning('DefineInputLinesAndLevels: Real I/O bitmask not equal to requested one!');
            resp(4) %#ok<NOPRT>
            varargout{1} = 0;
            return;
        end
    end
    WaitSecs(1);
    
    % Send new TTL pull-level:
    if varargin{3} > 0
        % Pull lines low --> Detect lines high:
        cc = 'a50';
    else
        % Pull lines high --> Detect lines low:
        cc = 'a51';
    end
    WriteDev(handle, cc);
    WaitSecs(1);
    ClearQueues(handle);    
    WriteDev(handle, '_a5');
    WaitSecs(1);
    
    % Query pull level:
    resp = ReadDev(handle, 4);
    if length(resp) < 4
        warning('DefineInputLinesAndLevels: No response received from device!');
        varargout{1} = 0;
        return;        
    end
    
    if ~strcmp(char(resp(1:3)), '_a5')
        warning('DefineInputLinesAndLevels: Invalid response received from device!');
        char(resp) %#ok<NOPRT>
        varargout{1} = 0;
        return;        
    else
        if ~strcmp(char(resp(2:4)), cc)
            warning('DefineInputLinesAndLevels: Real TTL default not equal to requested one!');
            resp(2:4) %#ok<NOPRT>
            varargout{1} = 0;
            return;
        end
    end
    WaitSecs(1);
    
    % Send new debounce time:
    WriteDev(handle, ['a6' char(double(varargin{4}))]);
    WaitSecs(1);
    ClearQueues(handle);
    % Read it back:
    WriteDev(handle, '_a6');
    WaitSecs(1);

    % Query debounce time:
    resp = ReadDev(handle, 4);
    if length(resp) < 4
        warning('DefineInputLinesAndLevels: No response received from device!');
        varargout{1} = 0;
        return;        
    end
    
    if ~strcmp(char(resp(1:3)), '_a6')
        warning('DefineInputLinesAndLevels: Invalid response received from device!');
        char(resp);
        varargout{1} = 0;
        return;        
    else
        if resp(4) ~= double(varargin{4})
            warning('DefineInputLinesAndLevels: Real TTL debounce time not equal to requested one!');
            double(resp(4));
            varargout{1} = 0;
            return;
        end
    end
    WaitSecs(1);
    
    varargout{1} = 1;

    return;
end

if strcmpi(cmd, 'ReadInputLines')
    % Retrieve state of all input lines:
    if nargin < 2
        error('You must provide the device "handle" for the box!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    % Flush event queue:
    FlushEvents(handle);
    
    % Send command code:
    WriteDev(handle, 'ar');
    
    % Read one byte back:
    inplines = ReadDev(handle, 1);
    outv=zeros(1,8);
     
    for i=0:7
        if bitand(inplines, 2^i)
            outv(i+1)=1;
        end
    end
    
    varargout{1} = outv;
    
    return;
end

if strcmpi(cmd, 'SetOutputLineLevels')
    % Change signal level of output pins:
    if nargin < 2
        error('You must provide the device "handle" for the box!');
    end

    if nargin < 3
        error('You must provide the 8 element vector of output line levels!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    opins = varargin{2};
    if length(opins)~=8 || ~isnumeric(opins)
        error('You must provide an 8 element vector of output line levels!');
    end

    outval = 0;
    for i=1:8
        if opins(i)>0
            outval = outval + 2^(i-1);
        end
    end
    
    % Send command code and mask:
    WriteDev(handle, ['ah' char(outval)]);
    
    % Wait a bit:
    WaitSecs(0.1);
    
    return;
end



if strcmpi(cmd, 'GetDeviceInfo')
    % Query info about device:

    if nargin < 2
        error('You must provide the device "handle" for the box to query!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    dev = ptb_cedrus_devices{handle};
    
    varargout{1} = dev;

    return; % JCAR added
end

if strcmpi(cmd, 'Open')

    % Open a new connection to response box:

    if nargin < 2
        error('You must provide the "port" parameter for the serial port to which the box is connected!')
    end

    % Create serial object for provided port, configure connection
    % properly:
    port = varargin{1};
    % port = '/dev/cu.usbserial-FT3Z95V5'

    if nargin < 3
        % Assume user doesn't want time calibration:
        lowbaudrate = 0;
    else
        lowbaudrate = varargin{2};
    end
  
    if nargin < 4
        % Assume user doesn't want time calibration:
        doCalibrate = 0;
    else
        doCalibrate = varargin{3};
    end
    
    % Open device link at default baudrate of 115 kBaud, return 'dev' struct:
    dev = OpenDev(port, 115200);
    
    % Create new entry in our struct array:
    if isempty(ptb_cedrus_devices)
        ptb_cedrus_devices = cell(1,1);
    else
        ptb_cedrus_devices(end+1) = cell(1,1);
    end

    % Get a handle to it:
    handle = length(ptb_cedrus_devices);

    % Assign device struct to array:
    ptb_cedrus_devices{handle} = dev;
    clear dev;

    if lowbaudrate
        % Set the device protocol to XID mode
        WriteDev(handle, 'c10'); %JCAR removed cr

        % Give device time to settle:
        WaitSecs(0.5);

        % Initiate a device reset:
        WriteDev(handle, 'f7');

        % Give device time to settle:
        WaitSecs(0.5);

        % Change baudrate of device to 57600 Baud:
        WriteDev(handle, ['f1' char(3)])

        % Give device time to settle:
        WaitSecs(0.5);

        % Close connection:
        CloseDev(handle);

        % Give device time to settle:
        WaitSecs(0.5);

        % Reinit connection at new baud rate:
        % Open device link at new baudrate of 57600 Baud, return 'dev' struct:
        dev = OpenDev(port, 57600);

        % Reassign device struct to array:
        ptb_cedrus_devices{handle} = dev;
        clear dev;
    else
        % Set the device protocol to XID mode
        WriteDev(handle, 'c10'); %JCAR removed cr

        % Give device time to settle:
        WaitSecs(0.5);
        
        % Initiate a device reset:
        WriteDev(handle, 'f7');

        % Give device time to settle:
        WaitSecs(0.5);
    end
    
    % This is for keeping track of what buttons are currently up or
    % down. I assume that all buttons are up when the device is opened.
    ptb_cedrus_devices{handle}.CedrusStatus = zeros(3,8);
    
    % Put this in a try-catch loop so that if it doesn't work for any
    % reason, I can then close the link and you can try again. Otherwise,
    % the COM port is permanently busy and I have to restart Matlab.
%    try
        
        
        % % Debug information from http://www.cedrus.com/xid/properties.htm
        % %
        % % Note: 0 is ASCII value 48, 1 is ASCII value 49, and so forth.
        %
        % fprintf(s1,['_d3',char(13)]);
        % fread(s1,1)
        % fprintf(s1,['_d1',char(13)]);
        % fscanf(s1)

        % Query a few device properties:

        % Get product ID: 0 = Lumina, 1 = VoiceKey, 2 = RB response pad:

        % I have to put this in a while loop, because sometimes '_d2' fails to
        % evoke a response:
        bytes = 0;
        while bytes==0
            WriteDev(handle, '_d2');
            WaitSecs(0.25); % I also have to wait, because even when it does evoke a response,
            % there can be a long delay - tens of milliseconds. Jon Peirce confirms
            % this.
            bytes = BytesAvailable(handle);
        end

        response=ReadDev(handle, bytes);
        response=response(1);

        switch response(1)
            case 48
                ptb_cedrus_devices{handle}.productID = 'Lumina';
            case 49
                ptb_cedrus_devices{handle}.productID = 'VoiceKey';
            case 50
                ptb_cedrus_devices{handle}.productID = 'RB response pad';
            otherwise
                ptb_cedrus_devices{handle}.productID = 'Unknown';
        end

        % Get model ID: 0 = Unknown, 1 = RB-530, 2 = RB-730, 3 = RB-830, 4 = RB-834
        % I have to put this in a while loop, because sometimes '_d3' fails to
        % evoke a response:

        % Give device time to settle:
        WaitSecs(0.5);

        % Remove junk - if any:
        while BytesAvailable(handle)
            ReadDev(handle, 1);
        end
        
        bytes = 0;
        while bytes==0
            WriteDev(handle, '_d3');
            WaitSecs(0.25); % I also have to wait, because even when it does evoke a response,
            % there can be a long delay - tens of milliseconds. Jon Peirce confirms
            % same behaviour on his system.
            bytes = BytesAvailable(handle);
        end
        
        response=ReadDev(handle, bytes);
        response=response(1);

        if response==48
            ptb_cedrus_devices{handle}.modelID = 'Unknown';
            ptb_cedrus_devices{handle}.modelNo = 0;

        else if strcmp(ptb_cedrus_devices{handle}.productID,'RB response pad')
                switch response
                    case 49
                        ptb_cedrus_devices{handle}.modelID = 'RB-530';
                        ptb_cedrus_devices{handle}.modelNo = 530;
                    case 50
                        ptb_cedrus_devices{handle}.modelID = 'RB-730';
                        ptb_cedrus_devices{handle}.modelNo = 730;
                    case 51
                        ptb_cedrus_devices{handle}.modelID = 'RB-830';
                        ptb_cedrus_devices{handle}.modelNo = 830;
                    case 52
                        ptb_cedrus_devices{handle}.modelID = 'RB-834';
                        ptb_cedrus_devices{handle}.modelNo = 834;
                    otherwise
                        ptb_cedrus_devices{handle}.modelID = sprintf('Unknown id %i', response);
                        ptb_cedrus_devices{handle}.modelNo = 0;
                end
            else
                ptb_cedrus_devices{handle}.modelID = 'Unknown';
                ptb_cedrus_devices{handle}.modelNo = 0;
            end
        end

        % Firmware revision:
        bytes = 0;
        while bytes==0
            WriteDev(handle, '_d4');
            WaitSecs(0.1);
            bytes = BytesAvailable(handle);
        end
        ptb_cedrus_devices{handle}.VersionMajor = ReadDev(handle, bytes) - 48;

        bytes = 0;
        while bytes==0
            WriteDev(handle, '_d5');
            WaitSecs(0.1);
            bytes = BytesAvailable(handle);
        end
        ptb_cedrus_devices{handle}.VersionMinor = ReadDev(handle, bytes) - 48;

        % Product name string:
        bytes = 0;
        while bytes==0
            WriteDev(handle, '_d1');
            WaitSecs(0.1);
            bytes = BytesAvailable(handle);
        end
        % Weird casting procedure with replacement of char(13) by char(10),
        % so Octave can handle it:
        ptb_cedrus_devices{handle}.Name = double(ReadDev(handle, bytes));
        ptb_cedrus_devices{handle}.Name(find(ptb_cedrus_devices{handle}.Name == 13)) = 10; %#ok<FNDSB>
        ptb_cedrus_devices{handle}.Name = char(ptb_cedrus_devices{handle}.Name);
        
        % Try our best to totally drain the receive queue:
        WaitSecs(0.25);
        while 1
            bytes = BytesAvailable(handle);
            if bytes == 0
                break;
            end
            ReadDev(handle, bytes)
            WaitSecs(0.1);
        end
        
        % Reset base timer:
        WriteDev(handle, 'e1');
      
        % Calibration of PTB's timebase vs. Boxes timebase wanted?
        if doCalibrate

            % Set slope of 1 as a flag that ResetRTT should do a
            % calibrated, timestamped reset:
            ptb_cedrus_devices{handle}.baseToPtbSlope = 1;
            
            % Perform calibrated basetimer query:
            [hosttime, devicetime, minwin] = queryBaseTimer(handle);

            % And store its results:
            ptb_cedrus_devices{handle}.lastBaseTimeQuery = [hosttime, devicetime, minwin];
            
        else
            % Uncalibrated mode requested. Saves a few seconds of startup
            % time, but doesn't allow mapping of boxes time measurements
            % into GetSecs() timebase of PTB:
            
            % No link roundtrip time estimates available:
            ptb_cedrus_devices{handle}.roundtriptime = 0;
            ptb_cedrus_devices{handle}.roundtripstddev = 0;

            % No mapping of box time to PTB time available:
            ptb_cedrus_devices{handle}.baseToPtbSlope  = 0;
            ptb_cedrus_devices{handle}.baseToPtbOffset = 0;
            
            ptb_cedrus_devices{handle}.lastBaseTimeQuery = [];
        end
        
        % Reset reaction time timer of device: If calibration was
        % requested, this will also estimate the offset between RTT values
        % and basetimer values, which is needed for later mapping of RTT to
        % GetSecs time. In uncalibrated mode, this will just send out the
        % reset code.
        ResetRTT(handle);

        % Return handle:
        varargout{1} = handle;
%    catch
        % Close serial control link:
%        CloseDev(handle);
%    end
    return;
end

if strcmpi(cmd, 'GetBoxTimerSlope')
    % Close device:
    if nargin < 2
        error('You must provide the device "handle" for the box to compute slope for!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    % Perform calibrated basetimer query:
    [hosttime, devicetime] = queryBaseTimer(handle);
    
    lastBaseTimeQuery = ptb_cedrus_devices{handle}.lastBaseTimeQuery;
    
    baseToPtbSlope = (hosttime - lastBaseTimeQuery(1)) / (devicetime - lastBaseTimeQuery(2));
    
    % Store measured slope internally:
    ptb_cedrus_devices{handle}.baseToPtbSlope = baseToPtbSlope;

    % Return measured slope:
    varargout{1} = baseToPtbSlope;
    
    return;
end


if strcmpi(cmd, 'Close')
    % Close device:
    if nargin < 2
        error('You must provide the device "handle" for the box to close!');
    end

    % Retrieve handle and check if valid:
    handle = checkHandle(varargin{1});

    CloseDev(handle);
    
    return;
end

if strcmpi(cmd, 'CloseAll')
    % Close all open devices:

    if exist('ptb_cedrus_devices', 'var') 
        for handle = 1:length(ptb_cedrus_devices)
            if ~isempty(ptb_cedrus_devices{handle})
                CloseDev(handle);
            end
        end
        
        % All handles closed: Release the device array itself:
        clear ptb_cedrus_devices;
    end
    
    return;
end

error('Invalid subcommand given. Read the help.');
% ---- End of main routine ----


% ---- Start of internal helper functions ----

function TestThis(handle)
% Generic test blurb...
global ptb_cedrus_devices; %#ok<NUSED>

persistent testbyte;
if isempty(testbyte)
    testbyte = 0;
end

% Flush input buffer:
WaitSecs(0.2);
FlushEvents(handle);
WaitSecs(0.2);

% Set general mode for lines: "General purpose"
WriteDev(handle, 'a10');
WaitSecs(0.4);

% Set direction for lines: All output.
WriteDev(handle, ['a4' 0]);
WaitSecs(0.4);


% Query current state of outputs and inputs:
WriteDev(handle, 'ar');

% Wait for response:
inputLines = dec2bin(ReadDev(handle, 1)); %#ok<NASGU>

basetime = WaitSecs(0.5); %#ok<NASGU>

% Set all output lines low:
testbyte = mod(testbyte + 1, 256);
WriteDev(handle, ['ah' testbyte]);
%WriteDev(handle, ['ah' 255]);
WaitSecs(0.4);
WriteDev(handle, '_ah');

% Any activity, e.g., events???
%while BytesAvailable(handle) == 0
%    fprintf('Nothing yet at %f secs...\n', GetSecs - basetime);
%end

WaitSecs(0.1);
response = ReadDev(handle, 4); %#ok<NASGU>
%response2 = ReadDev(handle, 6)

% if length(response) == 6
%     [evt,CedrusStatus] = ExtractKeyPressData(handle,response)
% end

return;

function tReset = ResetRTT(handle)
% Try to reset the reaction time timer to zero within a small time
% window, so we can associate "time zero" of the RT timer with the
% current GetSecs() time. This way, the RT timer will encode elapsed
% time since that GetSecs basetime. We can then later on compute the
% time of a keypress simply as sum of the GetSecs baseline time and the
% reported event time (== value of RT timer at time of button press).
% Retry up to 100 times if reset doesn't occur within 10ms.
global ptb_cedrus_devices;

% Calibrated reset?
if ptb_cedrus_devices{handle}.baseToPtbSlope ~= 0
    % Calibrated reset:

    % Flush input buffer:
    WaitSecs(1);
    ClearQueues(handle);
    WaitSecs(1);

    % Switch to realtime priority if not already there:
    oldPriority=Priority;
    if oldPriority < MaxPriority('GetSecs')
        Priority(MaxPriority('GetSecs'));
    end
    
    % Get porthandle:
    blocking = 1;
    ntrials = 5;
    s = ptb_cedrus_devices{handle}.link;
    t = zeros(2,ntrials);
    
    % Perform up to ntrials trials:
    for ic=1:ntrials
        
        % Wait some random fraction of a millisecond. This will desync us
        % from the USB duty cycle and increase the chance of getting a very
        % small time window between scheduling, execution and acknowledge
        % of the send operation:
        WaitSecs(rand / 1000 + 1);
        
        % Take pre-Write timestamp: Sync command not emitted before that time:
        % Write sync command, wait 'blocking' for write completion, store
        % completion time in t(2,ic). Send RTT reset command code 'e5':
        [nw t(2,ic), errmsg, t(1,ic)] = IOPort('Write', s, 'e5', blocking);

        % We know that sync command emission has happened at some time
        % after t(1,ic) and before t(2,ic). This by design of the USB
        % standard, host controllers and operating system USB stack. This
        % is the only thing we can take for granted wrt. timing, so the
        % "time window" between those two timestamps is our window of
        % uncertainty about the real host time when sync started. However,
        % on a well working system without massive system overload one can
        % be reasonably confident that the real emission of the sync
        % command happened no more than 1 msec before t(2,ic). That is a
        % soft constraint however - useful for computing the final estimate
        % for hosttime, but nothing to be taken 100% for granted.
        
        if nw~=2
            % Send op failed!
            fprintf('CedrusResponseBox: RTTReset: Warning! Sync token send operation to box failed!\n');
            t(1,ic) = 0;
            t(2,ic) = inf;
            continue;
        end

        confidencewindow = t(2,ic) - t(1,ic);
        if confidencewindow < 0.001
            break;
        end

        % Next trial...
    end

    % Restore priority
    if Priority ~= oldPriority
        Priority(oldPriority);
    end
    
    % For each measurement, the time window t(2,ic)-t(1,ic) defines kind of
    % a confidence interval for the "real" host system time when the sync
    % command was emitted. The measurement with the smallest time window is
    % the most accurate one. Find it and use it:
    minwin = t(2,ic) - t(1,ic);
    
    % On OS/X or Linux we could easily do with 2 msecs, as a 1.2 msecs
    % minwin is basically never exceeded. On MS-Windows however, 2.x
    % durations are not uncommon, so we need to slack this to 3.
    if minwin > 0.002
        fprintf('CedrusResponseBox: RTTReset: Warning! Confidence interval for clock sync is %f msecs - More than 2 msecs!\n', minwin);
    end
    
    % If the 'minwin' window is smaller than 1 msec, we subtract 0.5 the
    % length of it from the t(2,idx) timestamp as best estimate for
    % hosttime -- reasonable assuming a uniform distribution in the
    % 'minwin' interval. If 'minwin' is more than 1 msecs, we assume it
    % happened 0.5 msecs before t(2,idx) -- taking advantage of the
    % soft-constraint that the real write usually happens within 1 msec of
    % t(2,idx) on a normally loaded and well working system:
    hosttime = t(2,ic) - (min(minwin, 0.001)/2);
    
    WaitSecs(0.2);
    FlushEvents(handle);
    WaitSecs(0.2);
    
    ptb_cedrus_devices{handle}.baseToPtbOffset = hosttime;
    ptb_cedrus_devices{handle}.rttresetdelay = minwin;
    tReset = minwin;
    
else
    % Only uncalibrated fast reset requested:

    % Reset offset fields to invalid values:
    ptb_cedrus_devices{handle}.baseToPtbOffset = 0;
    ptb_cedrus_devices{handle}.rttresetdelay = -1;

    % Send reaction time timer reset code:
    dx1 = GetSecs;
    WriteDev(handle, 'e5');
    dx2 = GetSecs;

    % Return estimated time of when reset probably roughly happened:
    tReset = (dx1+dx2)/2;
end

return;

function ClearQueues(handle)
    while BytesAvailable(handle)>0
        % Read and discard all bytes:
        ReadDev(handle, BytesAvailable(handle));
        WaitSecs(0.5);
    end
return;

function CedrusStatus=FlushEvents(handle)
% JCAR: I modified this because I don't want to just throw away information
% about key-presses. If I do, I lose track of what the current button
% status is. So, I will read the information and check whether any of it
% is key-presses. If so, I will use this information to update the current
% status.
global ptb_cedrus_devices;

CedrusStatus = ptb_cedrus_devices{handle}.CedrusStatus;

while BytesAvailable(handle)>0
    % Read 1 byte
    response=ReadDev(handle, 1);

    % See if this is "k", indicating that a key press is following
    if char(response)=='k'
        % Seems to be an event packet: Read remaining 5 more bytes
        last5=ReadDev(handle, 5);
        response(1:6) = [response(1) last5];
        [evt,CedrusStatus] = ExtractKeyPressData(handle,response);
    else
        fprintf('CedrusResponseBox:FlushEvents: Warning invalid value %s [%i] instead of "k" received!\n', char(response), response);
    end
end

return;

function label = findbuttonlabel(numbr, handle)
% The response box labels buttons by rather arbitrary numbers.
% I thought it might be helpful to have something more descriptive.
% THese descriptions assume the box is postioned with its cables/ports
% on the back edge furthest from the user.
global ptb_cedrus_devices;

switch (ptb_cedrus_devices{handle}.modelNo)
    case 530
        switch (numbr - 1)
            case 1
                label = 'top';
            case 6
                label = 'bottom';
            case 3
                label = 'left';
            case 5
                label = 'right';
            case 4
                label = 'middle';
            otherwise
                label = 'unknown';
        end
    case 730
        switch (numbr)
            case 2
                label = '1.Left';
            case 3
                label = '2.Left';
            case 4
                label = '3.Left';
            case 5
                label = '4.Left';
            case 6
                label = '5.Left';
            case 7
                label = '6.Left';
            case 8
                label = '7.Left';
            otherwise
                label = 'unknown';
        end
    % arrangement of key codes for Cedrus 830
    %   left    right   hand
    %   4  5    6  7
    % 8              1
    %       2  3
    case 830
        switch (numbr)
            case 1
                label = 'right.outer';
            case 2
                label = 'left.lower';
            case 3
                label = 'right.lower';
            case 4
                label = 'left.center';
            case 5
                label = 'left.inner';
            case 6
                label = 'right.inner';
            case 7
                label = 'right.center';
            case 8
                label = 'left.outer';
            otherwise
                label = 'unknown';
        end
    otherwise
        label = 'unknown';
end

return;

function [evt,CedrusStatus] = ExtractKeyPressData(handle,response)
%The XID device sends six bytes of information in the following format:
%<�k�><key info><RT>:
%
% The first parameter is simply the letter "k", lower case.
%
% The second parameter consists of one byte, divided into the following
% bits
%
% Bits 0-3 store the port number. For Lumina LP-400, the push buttons and
% scanner trigger are on port 0; the RJ45 I/O lines are on port 1.
% For SV?1, voice key is on port 2 and the RJ45 is on port 1 ? there is
% no port 0. For the RB-x30 response pads, the push buttons are on port 0
% and the RJ45 port is on port 1.
%
% Bit 4 stores an action flag. If set, the button has been pressed. If
% cleared, the button has been released.
%
% Bits 5-7 indicate which push button was pressed.
%
% The reaction time consists of four bytes and is the time elapsed since
% the Reaction Time timer was last reset. See description of command "e5".
%
% Information taken from http://www.cedrus.com/xid/protocols.htm

global ptb_cedrus_devices;

if length(response)~=6
    % Did not receive 6 bytes - This should not happen!
    error('In GetButtons: Received too short (or no) response packet from box!');
end

% According to cedrus, http://www.cedrus.com/xid/protocols.htm,
% The XID device sends six bytes of information in the following
% format: <�k�><key info><RT>:
% So the first byte is 107, ie the letter �k�, lower case

% Check byte 1 for correct value 'k':
if char(response(1))~='k'
    % Failed!
    error('Received invalid event packet [Not starting with a k] from box!');
end

% Extracts byte 2 to determine which button was pushed:
evt.raw = (response(2));

% Extract different bits into meaningful fields:
% According to Cedrus, the second parameter consists of one byte, divided into the following bits:
% Bits 0-3 store the port number.
% For Lumina LP-400, the push buttons and scanner trigger are on port 0;
% the RJ45 I/O lines are on port 1. For SV?1, voice key is on port 2 and
% the RJ45 is on port 1 � there is no port 0.
% For the RB-x30 response pads, the push buttons are on port 0 and the
% RJ45 port is on port 1.

% Port id: Bits 0-3
evt.port = bitand(evt.raw, 15);
% 15 is 1111, so this extracts the rightmost 4 bits from evt.raw, ie bits 0-3

% Button state: 1 = pressed, 0 = released. Bit 4
evt.action = bitand(bitshift(evt.raw, -4), 1);

% Button id: Which button? Bits 5-7
evt.button = bitshift(evt.raw, -5) + 1;
% This chops off the rightmost 5 bits, i.e. bits 0-4, leaving only bits
% 5-7

% Map to a more descriptive label: 
evt.buttonID = findbuttonlabel(evt.button, handle);

% Extracts bytes 3-6 and is the time elapsed in milliseconds since the
% Reaction Time timer was last reset.
%
% For more information about the use of XID timers refer to
% http://www.cedrus.com/xid/timing.htm
response = double(response);
evt.rawtime = 0.001 * (response(3)+(response(4)*256)+(response(5)*65536) +(response(6)*(65536*256)));

% Map rawtime to ptbtime if possible:
ptbTime = mapRTTimerToPTBTime(evt.rawtime, handle);

% Valid mapping? Assign if so. If mapping is impossible due to skipped
% timecalibration, we don't return the 'ptbtime' field. This way, usercode
% that relies on it without performing the mandatory calibration will die
% with a nice error message.
if ~isnan(ptbTime)
    evt.ptbtime = ptbTime;
end

% Try and keep track of which buttons are currently down and up, based on
% what bytes have been read in.
ptb_cedrus_devices{handle}.CedrusStatus(evt.port + 1, evt.button) = evt.action;

% CedrusStatus will tell you what buttons are currently up or down,
% based on the last time the device was read.
CedrusStatus = ptb_cedrus_devices{handle}.CedrusStatus;

return;

% Check if 'handle' is a valid handle into our struct array of devices,
% return it if it is valid, abort with error otherwise.
function retHandle = checkHandle(handle)
    global ptb_cedrus_devices;
    
    if handle > length(ptb_cedrus_devices) || isempty(ptb_cedrus_devices{handle})
        error('Invalid response box handle %i passed: No such response box device open!', handle);
    end
    retHandle = handle;
return;

% Helper function: Open serial connection:
function dev = OpenDev(port, baudrate)
global ptb_cedrus_drivertype;

% Test our default of type 2 -- Our own IOPort() driver:
if ptb_cedrus_drivertype == 2
    % Use IOPort:

    % Temporarily shut up the driver, so errors can be reasonably
    % handled:
    oldverb = IOPort('Verbosity', 0);

    % Open link:
    [dev.link, errmsg] = IOPort('OpenSerialPort', port, sprintf('BaudRate=%i Parity=None DataBits=8 StopBits=1 FlowControl=Hardware ReceiveTimeout=1 ReceiveLatency=0.0001 ', baudrate));

    IOPort('Verbosity', oldverb);

    % Success?
    if dev.link < 0
        % Nope. Do we know the cause?
        error(sprintf('Failed to open port %s for Cedrus response box via IOPort()! Reason: %s', port, errmsg)); %#ok<SPERR>
    end

    % Link is online.
    try
        
        % Clear all send and receive buffers and queues:
        IOPort('Purge', dev.link);

        % Assign output port, driverid and empty recvQueue:
        dev.driver = 2;
        dev.recvQueue = [];
        dev.port = port;

    catch
        error('Failed to open port %s for Cedrus response box via IOPort() driver.', port);
    end
    
    % Ready.
    return;
end

% Some non-standard driver: We support Matlab's serial() 
% Windows or Linux: Matlab supports serial() object in JVM mode:
if ~psychusejava('desktop')
    error('You must run Matlab in JVM mode (JAVA enabled) for Cedrus response box to work!');
end

try
    % Ok, Matlab with JVM on Windows or Linux: Let's do it!
    dev.link = serial(port, 'BaudRate', baudrate, 'DataBits', 8, 'StopBits', 1,...
        'FlowControl', 'none', 'Parity', 'none', 'Terminator', 'CR', 'Timeout', 400,...
        'InputBufferSize', 16000);

    fopen(dev.link);
    dev.driver = 0;
    dev.port = port;
    dev.recvQueue = [];
catch
    error('Failed to open port %s on Windows or Linux for Cedrus response box via Matlab serial() driver.', port);
end

% Ready.
return;

function CloseDev(handle)
    global ptb_cedrus_devices;

    % Give device time to settle:
    WaitSecs(0.5);

    % Initiate a device reset:
    % WriteDev(handle, 'f7');

    % Give device time to settle after reset:
    WaitSecs(0.5);

    if ptb_cedrus_devices{handle}.driver == 0
        % Matlabs serial() driver:

        % Close serial control link:
        dev = ptb_cedrus_devices{handle};

        % Close data link:
        fclose(dev.link);

        % Delete serial control link object:
        delete(dev.link);
        clear dev.link;
    else
        if ptb_cedrus_devices{handle}.driver == 2
            % IOPort driver:
            IOPort('Purge', ptb_cedrus_devices{handle}.link);
            IOPort('Close', ptb_cedrus_devices{handle}.link);
        end
    end

    % Clear out device struct:
    ptb_cedrus_devices{handle} = [];

return;

function nrAvail = BytesAvailable(handle)
    global ptb_cedrus_devices;

    if ptb_cedrus_devices{handle}.driver == 0
        % Matlabs serial() driver:
        
        % Readout BytesAvailable subfield of device link object:
        nrAvail = ptb_cedrus_devices{handle}.link.BytesAvailable;
    else
        if ptb_cedrus_devices{handle}.driver == 2
            % IOPort driver:            
            nrAvail = IOPort('BytesAvailable', ptb_cedrus_devices{handle}.link);
        end
    end
    
    if nrAvail > 0
        % Store timestamp when queue was not empty:
        ptb_cedrus_devices{handle}.lastTimeQueueNonEmpty = GetSecs;
    end
    
return;

function data = ReadDev(handle, nwanted)
    global ptb_cedrus_devices;

    if ptb_cedrus_devices{handle}.driver == 0
        % Matlabs serial() driver:
        
        % Read via fread 'nwanted' bytes from link. Block until we get the
        % wanted 'nwanted' bytes or until timeout / error:
        data = transpose(fread(ptb_cedrus_devices{handle}.link, nwanted));
    else
        if ptb_cedrus_devices{handle}.driver == 2
            % IOPort driver: Returns all data as data type double:
            % fprintf('In read....\n');
            [data, when, errmsg] = IOPort('Read', ptb_cedrus_devices{handle}.link, 1, nwanted);
            if length(data) < nwanted
                fprintf('Timed out: nwanted = %i, got %i bytes: %s\n', nwanted, length(data), char(data));
                fprintf('Read operation on response box timed out after 1 secs! errmsg = %s\n', errmsg);
                data = [];
                return;
            end
        end
    end
return;

function WriteDev(handle, data)
    global ptb_cedrus_devices;

    if ptb_cedrus_devices{handle}.driver == 0
        % Matlabs serial() driver:
        
        % Write data via fwrite: We provide our own '%s' formatting string
        % to make sure that data is passed as-is, without any terminators
        % (CR or LF or CR+LF) attached. This will block until send
        % completion:
        fwrite(ptb_cedrus_devices{handle}.link, char(data));
    else
        if ptb_cedrus_devices{handle}.driver == 2
            % IOPort driver:
            
            % Write data - without terminator:
            % fprintf('In write....\n');
            IOPort('Write', ptb_cedrus_devices{handle}.link, char(data), 1);
        end
    end
return;

function roundtrip = RoundTripTestDev(handle)
    global ptb_cedrus_devices;

    % Flush the queue:
    FlushEvents(handle);

    % Perform 100 measurement trials:
    roundtrip = zeros(1,100);
    for i=0:100
        % Wait a bit between each trial:
        WaitSecs(0.100);

        % Send 'e4' code to initiate procedure:
        WriteDev(handle, 'e4');

        % Wait for receive completion:
        while BytesAvailable(handle) < 1
        end;

        % Send echo, optimistically assuming we received a 'X':
        WriteDev(handle, 'X');

        % Get the really received byte and check:
        if char(ReadDev(handle, 1))~='X'
            error('Roundtrip test did not receive "X" char as expected!');
        end

        % Wait for receipt of timestamp:
        while BytesAvailable(handle) < 4
        end;

        response = ReadDev(handle, 4);

        if response(1)~='P' || response(2)~='T'
            error('Roundtrip test did not receive "PT" marker as expected!');
        end

        response = double(response);
        
        % We throw away the first trial:
        if i > 0
            roundtrip(i) = 0.001 * (response(3) + 256 * response(4));
        end
    end

    % Store median and stddev of roundtrip time in device struct:
    ptb_cedrus_devices{handle}.roundtriptime = median(roundtrip);
    ptb_cedrus_devices{handle}.roundtripstddev = std(roundtrip);

return;

function [hosttime, devicetime, minwin] = queryBaseTimer(handle)
    global ptb_cedrus_devices;

    % Flush input buffer:
    WaitSecs(1);
    ClearQueues(handle);
    WaitSecs(1);

    % Switch to realtime priority if not already there:
    oldPriority=Priority;
    if oldPriority < MaxPriority('GetSecs')
        Priority(MaxPriority('GetSecs'));
    end
    
    % Get porthandle:
    blocking = 1;
    ntrials = 5;
    s = ptb_cedrus_devices{handle}.link;
    t = zeros(2,ntrials);
    
    % Perform up to ntrials trials:
    for ic=1:ntrials
        
        % Wait some random fraction of a millisecond. This will desync us
        % from the USB duty cycle and increase the chance of getting a very
        % small time window between scheduling, execution and acknowledge
        % of the send operation:
        WaitSecs(rand / 1000 + 1);
        
        % Take pre-Write timestamp: Sync command not emitted before that time:
        % Write sync command, wait 'blocking' for write completion, store
        % completion time in t(2,ic). Send basetimer query command code 'e3':
        [nw t(2,ic), errmsg, t(1,ic)] = IOPort('Write', s, 'e3', blocking);

        % Wait for response from box, receive packet, 
        % then parse into raw timer value (in seconds):
        devicetime = receiveAndParseTimePacket(handle);
        
        % We know that query command emission has happened at some time
        % after t(1,ic) and before t(2,ic). This by design of the USB
        % standard, host controllers and operating system USB stack. This
        % is the only thing we can take for granted wrt. timing, so the
        % "time window" between those two timestamps is our window of
        % uncertainty about the real host time when sync started. However,
        % on a well working system without massive system overload one can
        % be reasonably confident that the real emission of the sync
        % command happened no more than 1 msec before t(2,ic). That is a
        % soft constraint however - useful for computing the final estimate
        % for hosttime, but nothing to be taken 100% for granted.
        if nw~=2
            % Send op failed!
            fprintf('CedrusResponseBox: queryBaseTimer: Warning! Query token send operation to box failed!\n');
            t(1,ic) = 0;
            t(2,ic) = inf;
            continue;
        end

        confidencewindow = t(2,ic) - t(1,ic);
        if confidencewindow < 0.001
            break;
        end

        % Next trial...
    end

    % Restore priority
    if Priority ~= oldPriority
        Priority(oldPriority);
    end
    
    % For each measurement, the time window t(2,ic)-t(1,ic) defines kind of
    % a confidence interval for the "real" host system time when the sync
    % command was emitted. The measurement with the smallest time window is
    % the most accurate one. Find it and use it:
    minwin = t(2,ic) - t(1,ic);
    
    % On OS/X or Linux we could easily do with 2 msecs, as a 1.2 msecs
    % minwin is basically never exceeded. On MS-Windows however, 2.x
    % durations are not uncommon, so we need to slack this to 3.
    if minwin > 0.002
        fprintf('CedrusResponseBox: queryBaseTimer: Warning! Confidence interval for clock sync is %f msecs - More than 2 msecs!\n', minwin);
    end
    
    % If the 'minwin' window is smaller than 1 msec, we subtract 0.5 the
    % length of it from the t(2,idx) timestamp as best estimate for
    % hosttime -- reasonable assuming a uniform distribution in the
    % 'minwin' interval. If 'minwin' is more than 1 msecs, we assume it
    % happened 0.5 msecs before t(2,idx) -- taking advantage of the
    % soft-constraint that the real write usually happens within 1 msec of
    % t(2,idx) on a normally loaded and well working system:
    hosttime = t(2,ic) - (min(minwin, 0.001)/2);
    
    WaitSecs(0.2);
    FlushEvents(handle);
    WaitSecs(0.2);
    
return;

% Reads raw basetimer response packet from box, converted to seconds, but
% not corrected for receive latency etc. Query command must have been sent
% by calling code!
function rawBaseTime = receiveAndParseTimePacket(handle)

    % Read all 6 bytes of basetimer response packet from box:
    response = ReadDev(handle, 6);

    if length(response)~=6
        % Did not receive 6 bytes - This should not happen!
        error('In receiveAndParseTimePacket: Received too short (or no) response packet from box!');
    end

    % Check bytes 1:2 for correct values 'e3':
    if char(response(1))~='e' || char(response(2))~='3'
        % Failed!
        error('In receiveAndParseTimePacket: Received invalid response packet [Not starting with "e3"] from box!');
    end

    % Extracts bytes 3-6 and is the time elapsed in milliseconds since the
    % base timer was last reset.
    %
    % For more information about the use of XID timers refer to
    % http://www.cedrus.com/xid/timing.htm
    % Conver to seconds:
    response = double(response);
    rawBaseTime = 0.001 * (response(3)+(response(4)*256)+(response(5)*65536) +(response(6)*(65536*256)));

return;

function ptbTime = mapRTTimerToPTBTime(rtt, handle)
    global ptb_cedrus_devices;

    if ptb_cedrus_devices{handle}.baseToPtbOffset ~= 0
        % rtt is the parsed timevalue (already mapped from msecs to seconds),
        % as received in a event packet from the box. We map it to ptbTime by
        % adding the offset between GetSecs time and device RTT time, as
        % estimated by last calibrated RTTReset():
        ptbTime = ptb_cedrus_devices{handle}.baseToPtbOffset + rtt;
    else
        % Missing clock sync. Return "invalid" result:
        ptbTime = nan;
    end
    
return;
