function varargout = PsychRTBox(varargin) % Driver for the USTC reaction time button box (RTBox) by Xiangrui Li et al. % varargout = PsychRTBox(cmd, varargin); % % This driver allows to control most functions of the USTC RTBox response % button box. In theory this driver should support boxes up to Box/Firmware % version 5. In practice it has only been tested by the Psychtoolbox % developer up to firmware and box version 1.3, therefore you may encounter % bugs for later versions. The box itself comes bundled with an alternative % driver called "RTBox" which is maintained by the developers of the box % hardware itself, in case this driver doesn't work with your box or % firmware. % % The RTBox is a USB device which provides 4 response buttons (pushbuttons) % for subject responses and can report any button press- or release by the % subject. Additionally it has an input for reporting of external % electronic trigger signals and a photo-diode input for reporting of % visual stimulus onset. The box uses a built-in high-resolution clock to % timestamp all button- or trigger events, independent of the host % computers clock in order to make it more reliable for response time % measurements than most other response devices. It also buffers all events % internally, so experiment scripts can read back events when it is most % convenient. Timestamps can be either reported in Psychtoolbox standard % GetSecs timebase for direct comparison with timestamps from GetSecs, % WaitSecs, KbCheck et al., Screen('Flip') and PsychPortAudio, etc. This % simplifies reaction time calculations. Timestamps can also be reported in % the timebase of the boxe, e.g., time of a button press relative to the % photo-diode light trigger signal or electronic trigger signal, if this is % more convenient for a given experiment setup. % % Current versions of the RTBox have additional functionality, e.g., more % digital trigger inputs, some sound trigger, and TTL trigger outputs. % % See http://lobes.osu.edu/rt-box.php for up to date product information and % additional driver software. % % Please note that while the device documentation claims that the external % electronic pulse port is able to receive TTL trigger signals, we couldn't % verify that this is the case with our test sample of version 1 of the % RTbox hardware. While the pulse input port responded to some pulses sent % by one TTL compatible device, it failed to detect the majority of signals % from TTL most other test devices. % % This indicates that the pulse port may not be fully TTL compliant and % will need additional tinkering for your setup. However, results with % later versions of the hardware may be different than our experience with % our test sample. % % % The following subcommands are currently suppported: % =================================================== % % % handle = PsychRTBox('Open' [, deviceID] [, skipSync=0]); % -- Try to open a connected RTBox, return a device handle 'handle' to it % on success. The handle can be used in all further subcommands to refer to % the box. By default, all USB ports (or rather USB-Serial ports) are scanned % for a connected RTBox and the driver will connect to the first box found. % Alternatively you can specify which box to use via the optional % 'deviceID' namestring. This can be either the name of a box, or the name % of the USB-Serial port to which the box is connected. This way you can avoid % scanning of all ports and disambiguate in case multiple boxes are % connected to your computer. % % Btw., if you only make use of one single RTBox, you don't need to specify % the 'handle' parameter to all following subfunctions. Instead you can % specify that parameter as [] or omit it and the driver will use the only % open box connected. % % The optional parameter 'skipSync', if set to 1, will prevent the open % routine from performing an initial clock synchronization. By default, it % will perform an initial clock synchronization. % % After opening the box, you may want to invoke this method: % % % clockRatio = PsychRTBox('ClockRatio' [, handle] [, durationSecs]); % -- Perform a clock drift calibration between the computers GetSecs host % clock and the internal clock of the box 'handle'. Restrict calibration to % a maximum of 'durationSecs' (default 60 seconds if omitted). Return the % computed 'clockRatio' and use it for all further operations. % % Due to manufacturing imperfections and environmental factors, no two % clocks ever run at exactly the same speed. Therefore the computer clock % and box clock will slowly "drift out of sync" under normal conditions, % rendering retrieved event timestamps inaccurate over the course of a long % experiment session. This calibration routine will exercise the clocks and % compute the clock drift due to this speed difference, then use the % computed drift (= clockRatio) to correct all reported timestamps for this % drift, thereby providing the accuracy needed for reaction time studies. % % The clockRatio value tells, how many seconds of GetSecs time elapse when % the box clock measures 1 second elapsed time. Ideally this value would be % 1, ie. both clocks run at the same speed. A more realistic value would be, % e.g., 1.000009 -- The computer clock goes 9 microseconds faster than the % box clock, so the drift will accumulate an error of 9 microseconds for % each elapsed second of your study. % % As every calibration, this routine involves some measurement/calibration % error and is therefore not perfect, so even after a successfull % 'ClockRatio' calibration, timestamps reported during your experiment will % accumulate some error during the course of long experiment sessions. % % There are multiple ways to handle this: % % a) Use a long calibration time in this function for accurate results, and % a reasonably short experiment duration. % % b) Repeat this procedure after every large block of trials, ie., every % couple of minutes, e.g., while the subject is allowed to take a break in % a long experiment session. % % c) Use the PsychRTBox('SyncClocks') function after each short block of % trials, or even after each trial, for the highest accuracy. % % d) Don't care for clock drift throughout the experiment session, just % collect the event timestamps in box clock format (see the 3rd return % argument of PsychRTBox('GetSecs') or the returned timing array of % PsychRTBox('BoxSecs');) and store them in some array. Remap all timestamps % into the computers GetSecs time at the end of your session via % PsychRTBox('BoxsecsToGetsecs'). This requires a bit more % discipline from you in programming and organizing your data, but it % provides the most accurate timestamps. % % % [syncResult, clockRatio] = PsychRTBox('SyncClocks' [, handle]); % -- Synchronize or resynchronize the clocks of the host computer and the % box. Return result in 'syncResult' and the current clockRatio in % 'clockRatio'. This routine is automatically carried out during invocation % of PsychRTBox('ClockRatio'); but you can repeat the sync procedure % anytime between trials via this subfunction for extra accuracy at the % expense of about 0.5 - 1 second additional time for each invocation. You % would typically execute this function at the start of each large block of % trials, or before start of each trial if you are really picky about % super-exact timing. The syncResult contains three values: % % syncResult(1) = Host time (GetSecs time) at time of clock sync. % % syncResult(2) = Box time at time of clock sync. % % syncResult(3) = Confidence interval for the accuracy of the sync. This % value (in seconds) provides a reliable upper bound for the possible error % introduced in all reported timestamps from the box. The real error may be % significantly smaller, this is just an upper bound that you can check. % Typical results on a well working system should be in the sub-millisecond % range, e.g., 0.0003 seconds or 0.3 msecs. Typical results on a rather % noisy system would be around 0.001 second or 1 msec. Results worse than 2 % msecs indicate some problem with your system setup that should be fixed % before executing any experiment study which involves reaction time % measurements. By default, the sync procedure will abort with an error if % it can't calibrate to an accuracy with a maximum error of 1.3 msecs within % a duration of 0.5 seconds. You can change these default constraints with % a call to PsychRTBox('SyncConstraints'). % % % [oldmaxDurationSecs, oldgoodEnoughSecs, oldrequiredSecs, oldsyncMethod] = PsychRTBox('SyncConstraints'[, maxDurationSecs][, goodEnoughSecs][, requiredSecs][, syncMethod]); % -- Change the constraints to apply during calls to PsychRTBox('SyncClocks'); % Optionally return old settings. % % 'maxDurationSecs' limits any call to 'SyncClocks' to a duration of at % most the given number of seconds. Calibration aborts after at most that % time, even if unsuccessfull - in that case with an error message. By % default, the duration is limited to 0.5 seconds. % 'goodEnoughSecs' Calibration will finish before 'maxDurationSecs' have % elapsed, if the result is more accurate than an error of at most % 'goodEnoughSecs'. By default, this is set to zero seconds, i.e., % calibration will always take 'maxDurationSecs'. % 'requiredSecs' - The calibration will only use samples with an % uncertainty of at most 'requiredSecs'. If not even a single sample of the % required precision can be acquired within 'maxDurationSecs', the call % will fail with an error, indicating that your system setup doesn't % provide the required timing precision for your demands. By default, the % minimum required precision is 0.0013 seconds, ie., it will tolerate an % error of at most 1.3 msecs. % 'syncMethod' - Select the synchronization method to use. Either, method 0 % (prewrite sync), or method 1 (postwrite sync), or method 2 (average). If % you want to know the difference between the methods, please consult the % source code of this file and read the code for the subroutine 'function % syncClocks'. All three methods are robust and accurate within the % returned confidence window, usually better than 1 msec. So far, method 1 % seems to get the best results on our test setups, so this is the default % for the current driver release. However, we are still evaluating if % method 0 would be a tiny bit better and worth switching the default to % that. % % % oldverbose = PsychRTBox('Verbosity' [, handle], verbosity); % -- Set level of verbosity for driver: 0 = Shut up. 1 = Report errors % only. 2 = Report warnings as well. 3 = Report additional status info. 4 = % Be very verbose about what is going on. The default setting is 3 -- % Report moderate status output. % % % devinfo = PsychRTBox('BoxInfo' [, handle] [, newdevinfo]); % -- Return a struct 'devinfo' with all information about the current % status and parameter settings for RTBox 'handle'. Optionally set a new % struct with updated parameters via 'newdevinfo'. This function is mostly % useful for debugging and benchmarking the driver itself. Most information % contained in 'devinfo' will be useless for your purpose. % % % PsychRTBox('Close', handle); % -- Close connection to specific box 'handle'. Release all associated % ressources. % % % PsychRTBox('CloseAll'); % -- Close connections to all attached RTBox devices. Reset the PsychRTBox % driver completely. You'll usually use this function at the end of your % experiment script to clean up. % % % oldeventspec = PsychRTBox('Enable' [,handle][, eventspec]); % -- Enable specified type of event 'eventspec' on box 'handle'. This % allows to enable detection and reporting of a specific type of event. By % default, only reporting of push-button press is enabled, as this is the % most common use of a response box. % % The following names are valid for the name string 'eventspec': % 'press' = Report push-button press. This is the default setting. % 'release' = Report push-button release. % 'pulse' = Report electronic trigger events on external input port. % 'light' = Report reception of light flashes by photo-diode on light port. % 'tr' = Report reception of scanner trigger "TR" (TTL input from pin 7 of DB-9 port). % 'all' = Enable all events. % % If called without the 'eventspec' parameter, the function will return the % names of all currently enabled events. % % % oldeventspec = PsychRTBox('Disable' [,handle][, eventspec]); % -- Disable specified type of event 'eventspec' on box 'handle'. This % allows to disable detection and reporting of a specific type of event. By % default, only reporting of push-button press is enabled, as this is the % most common use of a response box. % % See 'Enable' call for help on parameters. % % % Once you have setup and calibrated the box and selected the type of % events to detect and report, you will want to actually retrieve % information about events. For this you use these commands: % % % PsychRTBox('Start' [, handle] [, dontwaitforstart=0]); % -- Start event detection and reporting by the box. The box will start % detecting button and trigger events from here on and record them in the % event buffer. % % You will usually call this at the beginning of a response period. By % default, the box has reporting already enabled after 'Open'ing it. % % The optional 'dontwaitforstart' parameter, if set to 1, will ask the % 'Start' function to return control as soon as possible, ie., without % waiting for confirmation of the box that event reporting has actually % started. By default, the routine waits for an acknowledgement from the % box, which can take 16 - 30 msecs in some cases. % % % PsychRTBox('Stop' [, handle]); % -- Stop event detection and reporting by the box. The box will ignore % detecting button and trigger events from here on and no longerrecord them % in the event buffer. % % You will usually call this at the end of a response period. % % % PsychRTBox('Clear' [, handle] [, syncClocks=0] [, dontRestart=0]); % -- Stop event detection and reporting by the box, clear all recorded % events so far, then restart reporting if it was active before calling % this function. % % Instead of calling 'Start' and 'Stop' to mark the start and end of a % response period in a trial you can also simply use this function at the % beginning of a trial (or its response period) to discard any stale data % from a previous trial (or non-response interval). % % You can prevent an automatic restart of event reporting by setting the % optional flag 'dontRestart' to a value of 1. % % You can ask the box to resynchronize its clock to the host computer clock % by setting the optional flag 'syncClocks' to a value of 1. This is the % same as calling PsychRTBox('SyncClocks'). % % % [time, event, boxtime] = PsychRTBox('GetSecs' [, handle] [, interTimeout=0.1] [, maxTimeout=interTimeout] [, maxItems=inf]); % -- Retrieve recorded events from the box 'handle'. % % By default, as many events are returned as are available within the % test interval, but you can select a specific number of wanted events % by setting the optional parameter 'maxItems'. If there aren't any pending % events from the box, by default the driver waits for up to 0.1 seconds % for events to arrive. You can change this 'interTimeout' interval via the % positive (non-zero) 'interTimeout' parameter. The function will return if % no new events show up within 'interTimeout' seconds. If something shows % up, the deadline for return is extended by 'interTimeout' seconds. You % can set an absolute upper limit to the response interval via the % 'maxTimeout' parameter. That defaults to 'interTimeout' if omitted. % Please note that after an event is detected by the box, up to 16-32 msecs % can elapse until the event is received by the computer, so you may not % want to set these timeout values too small! % % The function will return an array of timestamps in 'time', and an array % of corresponding names of the events in 'event'. E.g., event(1) will % report the identity of the first detected event, e.g., '1' if button 1 % was pressed, whereas time(1) will tell you when the event happened, ie., % when button 1 was pressed. 'time' is expressed in host clock time, aka % GetSecs() time. If no events are pending since last invocation of this % function, empty vectors will be returned. % % Additionally, the vector 'boxtime' contains the same timestamp, but % expressed in box clock time. See below for a use of that. % % By default, the following names are possible for 'event's: % % '1' = 1st button pressed, '1up' = 1st button released. % '2' = 2nd button pressed, '2up' = 2nd button released. % '3' = 3rd button pressed, '3up' = 3rd button released. % '4' = 4th button pressed, '4up' = 4th button released. % 'pulse' = electronic pulse received on electronic pulse input port. % 'light' = Light pulse received by photo-diode connected to light input port. % 'tr' = Scanner trigger "TR" (TTL input from pin 7 of DB-9 port) received. % 'serial' = PsychRTBox('Trigger') Softwaretrigger signal received on USB-Serial port. % % Note: 'tr' is only supported on boxes with Firmware version 3.0 or later. % % However, you can assign arbitrary names to the buttons and events if you % don't like this nomenclature via the PsychRTBox('ButtonNames') command. % % The reported timestamps are expressed in host clock time, ie., in the % same units as the timestamps returned by GetSecs, Screen('Flip'), % PsychPortAudio, KbCheck, KbWait, etc., so you can directly calculate % reaction times to auditory stimuli, visual stimuli and other events. % % See the help for PsychRTBox('SyncClocks') and PsychRTBox('ClockRatio') % for the accuracy of these timestamps and tips for obtaining optimal % accuracy. % % Additionally the event times are also returned in 'boxtime', but this % time expressed in box time -- the time of the box internal clock. % % % There are multiple variants of this query command with the same optional % input arguments, but different return arguments. All of these return % timestamps in box time without remapping to GetSecs time by calling: % % [boxtime, event] = PsychRTBox('BoxSecs' ...); % -- Timestamps are in raw box clock time, everything else is the same as % in PsychRTBox('GetSecs' ...). % % If you have the 'boxtime' timestamps from one of the previous functions % around, you can map them later to GetSecs time with very high precision % at the end of your experiment session via: % % [GetSecs, Stddev] = PsychRTBox('BoxsecsToGetsecs' [, handle], boxTimes); % -- Perform a post-hoc mapping of a vector of raw box timestamps % 'boxTimes' into a vector of host clock 'GetSecs' timestamps. Return some % error measure in 'Stddev' as well, if available. % % This method can be used to convert event timestamps expressed in the box % clocks timebase into timestamps in Psychtoolbox GetSecs host clock % timebase. It has the advantage of providing the highest possible accuracy % in mapping, because it computes an optimal mapping function for this % purpose, which is based on all the timing information collected % throughout a whole experiment session. The disadvantage is that it will % only provide meaningful results if you call it at the end of your % experiment session, so you'll need to manage all your collected % timestamps in a format that is suitable as input to this function. % % % Timestamps can also be returned relative to a specific trigger event: You % specify which event acts as a trigger. Then all timestamps of all events % are expressed relative to the time of that trigger event, i.e., as % deltas. Any event can be the trigger. Format of all arguments is % as in PsychRTBox('BoxSecs' ...); % % E.g., PsychRTBox('serial', ...); Returns timestamps relative to the first % occurence of a electronic input port trigger signal since the last query. % PsychRTBox('light', ...); Returns timestamps relative to photo-diode % light pulse. PsychRTBox('1'); returns relative to press of 1st button, % etc. etc. % % % sendTime = PsychRTBox('SerialTrigger' [, handle]); % -- Send a software generated trigger to the box via the serial port % connection. This will register as a event of type 'serial' and you can % retrieve timestamps relative to the first trigger within a response % period via the PsychRTBox('serial', ...); command. % % % sendTime = PsychRTBox('EngageLightTrigger [, handle]); % sendTime = PsychRTBox('EngagePulseTrigger [, handle]); % sendTime = PsychRTBox('EngageTRTrigger [, handle]); % % -- Engage trigger input on the box for reception of a one-shot trigger % signal. This function will return immediately after submitting the % request to the box. It may take up to 5 msecs worst-case until the % trigger input is really enabled. If you want to wait for the trigger to % be really enabled, call, e.g., PsychRTBox('Enable', handle, 'lighton'); instead, % as that function will wait until the trigger is really active. % % Trigger events are special: If a trigger has been received, the % box auto-disables the trigger input, preventing reception of any % further trigger events, until the trigger gets reenabled. The trigger gets % reenabled on many occasions if it has been enabled once via the % PsychRTBox('Enable', ...); command, e.g., at each call to % PsychRTBox('Start'); or PsychRTBox('Clear'). If you want to enable the % trigger on-the-fly, then this function is your friend. % % The reason why light trigger auto-disables itself is because a typical % CRT display monitor would generate such trigger signals at the rate of % video refresh, once your stimulus is displayed, e.g., at a rate of 100 % Hz. Usually you only want to know one defined timestamp of initial % stimulus onset, therefore the box prevents reception of all but the % first light trigger. % % Similar reasoning applies to Pulse and TR triggers. % % % oldNames = PsychRTBox('ButtonNames' [, handle] [, newNames]); % -- Query or assign labels for the four response box buttons other than % the default names. % % This function allows to assign arbitrary names to the four buttons on the % box. These names are reported when querying for button presses and % releases. By default, oldNames = PychRTBox('ButtonNames') would return % the cell array with the four following names: '1', '2', '3', '4'. These % are the names reported for button presses. Button releases would report % the names with an 'up' appended, ie., '1up', '2up', '3up', '4up'. You can % assign arbitrary new names by passing a cell array with four namestrings, % e.g., PsychRTBox('ButtonNames', [], {'7', 'whats', 'hick', 'screw'}) % would assign the names '7', 'whats', 'hick' and 'screw' for button press % events, and '7up', 'whatsup', 'hickup' and 'screwup' for release events % of the corresponding buttons. % % Please note that the assignment of names to buttons must be unique, ie. % assigning the same name to multiple buttons is not allowed. % % % oldIntervals = PsychRTBox('DebounceInterval' [, handle] [, debounceSecs]); % -- Query current button debounce intervals (in 4-element vector % 'oldIntervals', one value for each button), and optionally set new % debounce interval in seonds via the optional argument 'debounceSecs'. % 'debounceSecs' can be a scalar, in which case the same setting is applied % to all buttons, or a 4-element row vector, e.g., [0.1, 0.1, 0.1, 0.1] to % set an individual interval for each of the four buttons. % % The built-in debouncer prevents multiple button responses (button press % or release actions) from getting recorded/reported within some % 'debounceSecs' debounce interval. After a button has changed state, only % the type (press or release), identity (which button) and timestamp (when) % of the first state change is reported. Any other state change within % 'debounceSecs' seconds of time after that first change will be ignored. % After that time has elapsed, further state changes are reported again. By % default, this dead "debounce" interval is set to 0.050 seconds, ie., 50 % msecs. Button bouncing happens if a subject presses or releases a button % very rapidly or vigorously. If such quick multiple events or bounces are % not ignored, they will create multiple apparent button responses which % are a hazzle to deal with in experiment scripts and data analysis. % % If you find multiple responses generated for only one apparent button % press during piloting, you may want to set a bigger debounce interval % with this function. % % Please note that debouncing doesn't apply to the PsychRTBox('ButtonDown') % function. % % Please also note that there is another hardware debouncer with a duration % of 0.3 msecs on RTBox versions with firmware versions 1.3 and older, which % can't be disabled, so even if you'd set a zero interval here, you'd still % get a minimum 0.3 msecs debounce period from the hardware itself. % % Later versions of the firmware support the following % PsychRTBox('HardwareDebounce') command to control the hardware debounce % interval more fine-grained. % % % [oldValue] = PsychRTBox('HardwareDebounce' [, handle] [, scanNum]); % -- Set/get hardware debouncer setting. The hardware will treat a button % event as valid only if the button state stays stable for at least % 'scanNum' scanning iterations of the firmware. The scan interval is about % 67 microseconds. The valid scanNum is from 1 through 255, with a default % setting of 16 cycles for 1.072 msecs debounce interval. % % For software debouncing at the driver level, see PsychRTBox('DebounceInterval') % above. % % % buttonState = PsychRTBox('ButtonDown' [, handle] [, whichButtons]); % -- This reports the current button state of all response buttons of box % 'handle', or a subset of response buttons if specified by the optional % 'whichButtons' argument, e.g., whichButton = {'1', '4'} to only test % buttons 1 and 4. 'buttonState' is a vector which contains a 1 for each % pressed button, and a zero for each released button. % % This query is as instantaneous and "live" as possible. The reported state % is not subject to button debouncing, but the measured "raw state". % Usually you will want to use the PsychRTBox('GetSecs' ...) functions ans % similar functions to query timestamped button state. They are typically % as fast as this method and they provide timestamps of when the state was % queried, whereas this function doesn't give you information about how % "fresh" or recent the query is. However for simple button queries outside % the response interval, e.g., while the box is PsychRTBox('Stop')'ped with % no need for timestamps, this may be an option. % % Due to the design of the USB bus, the query may be outdated wrt. to the % real state by up to 16 - 21 msecs, depending on operating system and % driver configuration. % % % buttonState = PsychRTBox('WaitButtonDown' [, handle] [, whichButtons]); % -- Wait until at least one of the specified buttons in 'whichButtons' is % pressed down. If 'whichButtons' is omitted, all buttons are tested. % % % PsychRTBox('WaitButtonUp' [, handle] [, whichButtons]); % -- Wait until all of the specified buttons in 'whichButtons' are % released. If 'whichButtons' is omitted, all buttons are tested. % % % [timeSent, confidence] = PsychRTBox('TTL' [, handle] [, eventCode=1]); % - Send TTL to DB-25 port (pin 8 is bit 0). The second input is event code % (default 1 if omitted), 4-bit (0~15) for box versions < 5, and 8-bit % (0~255) for later versions. It can also be equivalent binary string, such % as '0011'. % % The optional return arguments are the 'timeSent' when the TTL update was % performed, and an upper bound on the uncertainty 'confidence' of % 'timeSent'. % % The width (duration) of the TTL pulse is controlled by the % PsychRTBox('TTLWidth') command. % % This function is only supported for v3.0 RTBoxes and later, the ones with % EEG event code support. % % % [oldValue] = PsychRTBox('TTLWidth' [, handle][, widthSecs]); % - Set/get TTL pulse width in seconds. The default width is 0.97e-3, ie. % 97 microseconds when the device is opened. The actual width may have some % small variation. The supported width ranges from 0.14e-3 to 35e-3 secs. A % infinite width 'inf' is also supported. Infinite width means the TTL will % stay until it is changed by the next PsychRTBox('TTL') command, such as % PsychRTBox('TTL',0). % % This function is only supported for v3.0 RTBoxes and later, the ones with % EEG event code port support. % % In Version <5.0, the TTL width at DB-25 pins 17~24 is controlled by a % potentiometer inside the box. In Version >= 5, the width is also % controlled by 'TTLWidth' command. % % % [oldValue] = RTBox('TTLResting' [, handle][, newLevel]); % - Set/get TTL polarity for DB-25 pins 1~8. The default is 0, meaning the % TTL resting is low. If you set newLevel to nonzero, the resting TTL will % be high level. If you need different polarity for different pins, let us % know. This function is only supported with firmware version 3.1 and % later. % % In Version 5.0 and later, newLevel has second value, which is the % polarity for pins 17~24. % % TODO: % % - Debouncing for PsychRTBox('Buttondown') as well, or leave it "raw"? % % History: % 08/01/2008 Initial implementation based on RTBox.m from Xiangrui Li (MK). % 01/29/2009 "Close to beta" release. First checkin to SVN (MK). % 01/30/2009 Improved syncClocks algorithm, option to spec a specific box % by port in the open call (MK). % 02/08/2009 Huge redesign of API and internal routines. Now we use an % internal queue (MK). % 02/14/2009 Refinements and cleanup (MK). % 02/15/2009 More refinements and rework of post-hoc timestamp remapping (MK). % 06/07/2009 Check for ambiguous assignment of buttonnames to avoid errors. % Bug found by Vinzenz Schoenfelder (MK). % 06/14/2009 Remove special case code for Octave. No longer needed (MK). % 12/14/2009 Update for RTBox'es with firmware v1.4 and later: % % * Store box firmware version numerically for easier comparison. % % * v >= 1.4 sends '?' acknowledge for live button query before % the byte that reports buttons state --> Handle this. % % * Detect clock frequency of RTBox and adapt our bytes2secs() mapping % accordingly for V1.4 and later. % % * 'lightoff' event is dead, now its called 'tr' instead for TR % scanner trigger reception on new V3.0 firmware boxes. % % * Switch v1.3 and later boxes back to 'x' simple E-Prime et % al. compatible mode at PsychRTBox('close') time. % % * Support new 'hardwaredebounce' command on v1.4+ % % * Support new 'ttlwidth' for v3.0, 'ttlresting' for v3.1+ % % * Support 4-bit TTL out port for v3.0+ % % * Add fast-engage commands for pulse and TR triggers as well. % % 10/20/2011 Switch fast calibration from robustfit to polyfit() - Now we % always use polyfit() in all cases to avoid need for Matlab % statistics toolbox. The PostHoc routine always did this, but % the online routine didn't. (MK) % % 11/26/2011 Updates for Firmware versions up to V5 (MK): % % * TTL out is now 8 bit capable and 'ttlwidth' and 'ttlresting' % needed to be updated as well. % * Event enable handling has changed. % * Various other stuff. % % None of this is tested due to lack of hardware/firmware. % % 01/06/2012 Bugfix for Firmware versions >= 4.1. Did not receive events % due to wrong acknowledgement handling. (MK) % % 06/15/2019 Add device detection for virtual RTBox which is emulated by % CRS Bits#. Also add 'Lenient' to IOPort Open to deal with % inability of Bits# to open port in low-latency mode. (MK) % Global variables: Need to be persistent across driver invocation and % shared with internal subfunctions: global rtbox_info; global rtbox_global; % Start of driver code -- Entry point: % First time invocation? Perform init of device arrays and global % settings: if isempty(rtbox_info) % Setup device info struct array, as well as per-device default % settings: % CAUTION: Same settings are reassigned in the openRTBox() % subfunction each time PsychRTBox('Open') is called! The settings % made there override the settings made here!!! rtbox_info=struct('events',{{'1' '2' '3' '4' '1up' '2up' '3up' '4up' 'pulse' 'light' 'tr' 'serial'}},... 'enabled',[], 'ID','','handle',-1,'portname',[],'sync',[],'version',[],'clkRatio',1,'verbosity',3, ... 'busyUntil', 0, 'boxScanning', 0, 'ackTokens', [], 'buttons', [0 0 0 0; 0 0 0 0; 0 0 0 0], ... 'syncSamples', [], 'recQueue', [], 'boxClockTickIntervalSecs', 1/115200); % Setup event codes: rtbox_global.eventcodes=[49:2:55 50:2:56 97 48 57 89]; % code for 12 events % List of supported subcommands: rtbox_global.cmds={'close', 'closeall', 'clear', 'stop', 'start', 'test', 'buttondown', 'buttonnames', 'enable', 'disable', 'clockratio', 'syncclocks', ... 'box2getsecs', 'boxinfo', 'getcurrentboxtime','verbosity','syncconstraints', 'boxsecstogetsecs', 'serialtrigger', ... 'debounceinterval', 'engagelighttrigger', 'waitbuttondown', 'waitbuttonup', 'ttlwidth', 'ttlresting', 'hardwaredebounce', ... 'ttl', 'engagepulsetrigger', 'engagetrtrigger' }; % Names of events that can be enabled/disabled for reporting: rtbox_global.events4enable={'press' 'release' 'pulse' 'light' 'tr' 'all'}; % Low-level protocol codes corresponding to the events: rtbox_global.enableCode='DUPOFA'; % char to enable above events, lower case to disable % Preload some functions of PTB we'll need: eval('GetSecs;WaitSecs(0.001);'); % Selection of blocking strategy that the IOPort driver shall use % for blocking writes: if IsWin || IsOSX % A blocking wait gives very good results on OS/X. On Windows % it gives the same results as a polling wait (setting 2), so % we prefer blocking for lower cpu load at same quality on % Windows as well: rtbox_global.blocking = 1; else % On Linux, a polling wait is of advantage, so we use that: rtbox_global.blocking = 2; end % No devices open at first invocation: rtbox_global.nrOpen = 0; % Default settings for the syncClocks() function: % ----------------------------------------------- % Use syncClocks() method 1 with postwrite timestamp by default: rtbox_global.syncmode = 1; % Maximum duration of a syncClocks calibration run is 0.5 seconds: rtbox_global.maxDuration = 0.5; % Desired 'minwin' calibration accuracy is 0.0 msecs: If we manage % to get better than that, we abort sampling. We also abort % sampling of the rtbox_global.maxDuration is reached: rtbox_global.optMinwinThreshold = 0.0; % Maximum allowable (ie. worst) acceptable minwin for a sample: % We default to 1.3 msecs, as a 1.2 msecs minwin is basically never % exceeded. It is unlikely that all samples within a syncClocks run % are worse than 1.3 msecs and the run would therefore fail. rtbox_global.maxMinwinThreshold = 0.0013; % Worst case delay after a command has been received by the box, % before it gets actually dequeued from the microprocessors serial % receive buffer and executed: 5 msecs is a very generous value to % be on the safe side: rtbox_global.maxbusy = 0.005; end if nargin < 1 error('You must provide a command string to PsychRTBox!'); end % Command dispatch: cmd = lower(varargin{1}); if isempty(cmd) error('You must provide a non-empty command string to PsychRTBox!'); end if strcmp(cmd, 'closeall') % Close all devices % Only close our devices, not other devices that may be opened % via IOPort but unrelated to us: for i=1:length(rtbox_info) s=rtbox_info(i).handle; if s>=0 % Disable all scanning on box before close: stopBox(i); % Close connection: IOPort('Close', s); rtbox_info(i).handle = -1; end end rtbox_global.nrOpen = 0; % Reset count of open devices to zero. clear rtbox_info; % clear main device info struct array. clear rtbox_global; % clear main global settings struct. return; end % Open the connection to device, do initial setup and sync: if strcmp(cmd, 'open') % Assign deviceID identifier of device to open, or the default % name 'Default' if none specified: User can also specify a serial % port device name for a device: if nargin < 2 deviceID=[]; else deviceID=varargin{2}; end if isempty(deviceID) deviceID = 'Default'; end if nargin < 3 skipSync = []; else skipSync = varargin{3}; end if isempty(skipSync) skipSync = 0; end % Open and initialize box: openRTBox(deviceID, rtbox_global.nrOpen+1); % Increment count of open boxes: rtbox_global.nrOpen = rtbox_global.nrOpen + 1; % Return as handle: varargout{1} = rtbox_global.nrOpen; if ~skipSync % Perform initial mandatory clock sync: syncClocks(rtbox_global.nrOpen); end % Perform initial button state query: buttonQuery(rtbox_global.nrOpen); % Start event scanning on box, with the above default enabled setting, % i.e., only button press 'D' reporting active: startBox(rtbox_global.nrOpen, 1); return; end if strcmp(cmd, 'syncconstraints') % Return current constraint settings: varargout{1} = rtbox_global.maxDuration; varargout{2} = rtbox_global.optMinwinThreshold; varargout{3} = rtbox_global.maxMinwinThreshold; varargout{4} = rtbox_global.syncmode; % Set constraints for syncClocks: if nargin > 1 && ~isempty(varargin{2}) rtbox_global.maxDuration = varargin{2}; end if nargin > 2 && ~isempty(varargin{3}) rtbox_global.optMinwinThreshold = varargin{3}; end if nargin > 3 && ~isempty(varargin{4}) rtbox_global.maxMinwinThreshold = varargin{4}; end if nargin > 4 && ~isempty(varargin{5}) rtbox_global.syncmode = varargin{5}; end return; end % Deal with variable number of inputs: if nargin - 1 > 0 nIn = nargin - 1; else nIn = 0; end if nIn > 1 in2=varargin{3}; end % Device handle provided? if nargin > 1 % Yes: Store it in 'id': id = varargin{2}; else % Nope. id = []; end % If no device handle - or empty default handle - provided, just % default to the first open RTBox device for convenience in % setups/scripts that only use one RTBox -- which is probably the % common case: if isempty(id) id = 1; end % Child protection: if ~isscalar(id) || id < 1 || id > length(rtbox_info) || id > rtbox_global.nrOpen error('Invalid device handle specified! Did you open the device already?'); end if isempty(rtbox_info(id).handle) error('Invalid device handle specified! Maybe you closed this device already?'); end % Build additional cell array of valid read commands: read=rtbox_info(id).events; read{end+1}='secs'; % Like GetSecs see below. read{end+1}='boxsecs'; % All events measured in absolute box time. read{end+1}='getsecs'; % All events measured in absolute GetSecs time. % Assign serial port handle: s = rtbox_info(id).handle; % Subcommand dispatch: switch cmd case 'verbosity' if nIn<2 error('You must provide the new level of "verbosity" to assign!'); end % Return old level of verbosity: varargout{1} = rtbox_info(id).verbosity; % Assign new level of verbosity to device: rtbox_info(id).verbosity = in2; case 'serialtrigger' % send serial trigger to device tWritten = sendTrigger(id); if nargout, varargout{1}=tWritten; end case 'engagelighttrigger' % Enable light on trigger quickly. tWritten = engageTrigger(id, 'O'); if nargout, varargout{1}=tWritten; end case 'engagepulsetrigger' % Enable pulsetrigger quickly. tWritten = engageTrigger(id, 'P'); if nargout, varargout{1}=tWritten; end case 'engagetrtrigger' % Enable TR-Trigger quickly. tWritten = engageTrigger(id, 'F'); if nargout, varargout{1}=tWritten; end case 'ttl' % Send TTL 4 bit event to output port on supported hardware: if rtbox_info(id).version < 3 RTBoxWarn('notSupported', in1, 3); return; end % Default event code is 1: if isempty(in2), in2 = 1; end % Can be a binary string: if ischar(in2), in2=bin2dec(in2); end % Range check: if rtbox_info(id).version < 5 maxTTL = 15; else maxTTL = 255; end if (in2 < 0) || (in2 > maxTTL) || (in2~=round(in2)) RTBoxError('invalidTTL'); end % Decode to final trigger byte: if rtbox_info(id).version < 3.2 in2 = dec2bin(in2,4); in2 = uint8(bin2dec(in2(4:-1:1))); % reverse bit order end if rtbox_info(id).version >= 5 in2=[1 in2]; end % Emit via blocking write: [tsend twin] = sendTTLPortEvent(id, in2); if nargout varargout={tsend twin}; end if twin > 0.003 fprintf('PsychRTBox: Warning! TTL trigger, send timestamp uncertainty %f msecs exceeds 3 msecs!\n', twin * 1000); end case 'debounceinterval' % Return old debouncer settings for each button: varargout{1} = rtbox_info(id).buttons(3, :); if nIn<2 return; end % Assign new settings: if isscalar(in2) % Single value: Apply to all buttons. rtbox_info(id).buttons(3, :) = [in2, in2, in2, in2]; else % Multi value: Apply individually to each button: if size(in2,1)~=1 || size(in2, 2)~=4 error('Either set a single common debounce value for all buttons or pass a 4-element row vector with 4 settings for all 4 buttons!'); end rtbox_info(id).buttons(3, :) = in2; end % Reset debouncer: rtbox_info(id).buttons(2, :) = [0, 0, 0, 0]; % Retrieve all pending events from the box, aka the serial port % receive buffers, parse them, filter/postprocess them, optionally % return mapped event timestamps in GetSecs timebase: case read % 12 triggers, plus 'secs' 'boxsecs' 'getsecs' cmdInd=strmatch(cmd,read,'exact'); % which command % Timestamp relative to trigger wanted? if cmdInd<13 ind=[cmdInd<5 (cmdInd<9 && cmdInd>4) cmdInd==9:11]; if ~rtbox_info(id).enabled(ind), RTboxError('triggerDisabled',rtbox_global.events4enable{ind}); end % minbytes=14; % at least 2 events end % Preinit return args to empty in case no event is detected: varargout={[] '' []}; % 2nd argument is inter-response timeout: Return if there isn't % any data received for that amount of time. Each received new % item will extend that timeout by given amount: if nIn > 1 && ~isempty(varargin{3}) intertimeout = varargin{3}; if intertimeout <=0 error('Invalid interTimeout value specified. Must be significantly > 0 secs!'); end else % Default is 0.1 secs aka 100 msecs: intertimeout = 0.1; end % 3rd argument is absolute timeout for all responses, the % absolute upper bound: if nIn > 2 && ~isempty(varargin{4}) abstimeout = varargin{4}; if abstimeout <=0 error('Invalid maxTimeout value specified. Must be significantly > 0 secs!'); end else % Default is to set it to intertimeout: abstimeout = intertimeout; end % 4th argument is maximum number of responses to fetch at most: if nIn > 3 && ~isempty(varargin{5}) maxItems = varargin{5}; else % Default is to infinite, i.e., no limits: We get all we % can get within the setup timeout intervals: maxItems = inf; end % Retrieve events: [evid, timing] = getEvents(id, maxItems, maxItems, abstimeout, intertimeout); nevent = length(evid); % Anything retrieved? if nevent == 0 return; end if nargout > 1 || cmdInd < 13 % Map event id to human readable label string: for i=1:nevent % extract each event and time ind=min(find(evid(i)==rtbox_global.eventcodes)); %#ok % which event if isempty(ind) RTboxWarn('invalidEvent',evid(i)); break; % not continue, rest must be messed up end event{i} = rtbox_info(id).events{ind}; %#ok event name end end if isempty(timing), return; end % Convert boxtiming and/or map it to host clock time: if cmdInd==15 || cmdInd==13 % Convert into computer time: MK-Style % First return optional "raw" array with boxtimes: varargout{3} = timing; % Then remap to GetSecs host timebase: timing = box2GetSecsTime(id, timing); elseif cmdInd<13 % trigger: relative to trigger ind=strmatch(cmd,lower(event),'exact'); % trigger index if isempty(ind), RTboxWarn('noTrigger',cmd); return; end ind=ind(1); % use the 1st in case of more than 1 triggers trigT=timing(ind); % time of trigger event event(ind)=[]; timing(ind)=[]; % omit trigger and its time from output if isempty(event), return; end % if only trigger event, return empty timing=timing-trigT; % relative to trigger time end varargout{1} = timing; if nargout > 1 if length(event)==1, event=event{1}; end % if only 1 event, use string varargout{2} = event; end case 'boxinfo' % Return complete device info struct: varargout{1} = rtbox_info(id); % Optionally set a new one -- Only for debugging!! if nIn > 1 rtbox_info(id) = in2; end case 'box2getsecs' % Map boxtime to GetSecs time with the recommended method: if nIn<2 error('You must provide the boxtime to map!'); end varargout{1} = box2GetSecsTime(id, varargin{3}); case 'boxsecstogetsecs' % Map boxtime to GetSecs time post-hoc style: % We compute an optimal least-squares fit linear mapping % function of boxtime to hosttime, using all collected % syncClocks samples from the whole experiment session. Then we % remap all given input boxsecs samples to getsecs time via % lookup in that linear best-fit. This automatically corrects % for clock-drift and should provide the least possible error % on the mapping procedure, because it makes use of all % available sync information of a whole session: if nIn<2 error('You must provide the boxtimes to map!'); end [remapped, sd, clockratio] = box2GetSecsTimePostHoc(id, varargin{3}); varargout = { remapped, sd , clockratio}; case 'getcurrentboxtime' % Retrieve current time of box clock. % We do so by performing a syncClocks call, but retaining the % current .sync results, so we just (mis-)use the function for % our purpose: tmpsync = rtbox_info(id).sync; syncClocks(id); % clear buffer, sync clocks varargout{1} = rtbox_info(id).sync; rtbox_info(id).sync = tmpsync; case 'stop' % Stop event processing and reporting on box: % This will store all pending events in internal queue: stopBox(id); case 'start' % (Re-)Start event processing and reporting on box: if nIn >= 2 && ~isempty(varargin{3}) && varargin{3} % Asynchronous start, i.e. don't wait for acknowledge but % return asap: waitForStart = 0; else % Wait for acknowledge of start: waitForStart = 1; end startBox(id, waitForStart); case 'clear' % Clear all pending events on box, optionally perform clocksync: % By default, box is restarted after clear and no clocksync is % performed, but box is not restarted if usercode doesn't want % this or if it wasn't running before. Optionally clocksync is % executed: % Stop event processing on box, if active: boxActive = rtbox_info(id).boxScanning; if boxActive % This will store all pending events in internal queue: stopBox(id); end % Clear all buffers (serial buffers and event queue): purgeRTbox(id); % Optional syncClocks requested? if nIn >= 2 && ~isempty(varargin{3}) && varargin{3} % Perform clockSync: syncClocks(id); if nargout varargout{1}=rtbox_info(id).sync; varargout{2}=rtbox_info(id).clkRatio; end end % Restart box? We restart if it was running before and usercode % doesn't forbid a restart: if boxActive && (nIn < 3 || isempty(varargin{4}) || varargin{4} == 0) startBox(id, 1); end case 'syncclocks' % Synchronize host clock and box clock, i.e., establish mapping % between both: % Stop event processing on box, if active: boxActive = rtbox_info(id).boxScanning; if boxActive % This will store all pending events in internal queue: stopBox(id); end syncClocks(id); % clear buffer, sync clocks if nargout varargout{1}=rtbox_info(id).sync; varargout{2}=rtbox_info(id).clkRatio; end if boxActive % Restart box if it was running: startBox(id, 1); end case 'buttondown' % Perform query: b2 = buttonQuery(id); if nIn<2, in2=read(1:4); end % not specified which button in2=cellstr(in2); % convert it to cellstr if it isn't for i=1:length(in2) ind=strmatch(lower(in2{i}),read(1:4),'exact'); if isempty(ind), RTboxError('invalidButtonName',in2{i}); end bState(i)=b2(ind); %#ok end varargout{1} = bState; case 'waitbuttondown' if nIn<2, in2=read(1:4); end % not specified which button in2=cellstr(in2); % convert it to cellstr if it isn't bState = zeros(1,length(in2)); % Repeat queries until at least one of the target buttons is % down: while ~any(bState) b2 = buttonQuery(id); for i=1:length(in2) ind=strmatch(lower(in2{i}),read(1:4),'exact'); if isempty(ind), RTboxError('invalidButtonName',in2{i}); end bState(i)=b2(ind); %#ok end end varargout{1} = bState; case 'waitbuttonup' if nIn<2, in2=read(1:4); end % not specified which button in2=cellstr(in2); % convert it to cellstr if it isn't bState = ones(1,length(in2)); % Repeat queries until all of the target buttons are up: while any(bState) b2 = buttonQuery(id); for i=1:length(in2) ind=strmatch(lower(in2{i}),read(1:4),'exact'); if isempty(ind), RTboxError('invalidButtonName',in2{i}); end bState(i)=b2(ind); %#ok end end case 'buttonnames' % set or query button names oldNames=rtbox_info(id).events(1:4); if nIn<2, varargout{1}=oldNames; return; end if length(in2)~=4 || ~iscellstr(in2), RTboxError('invalidButtonNames'); end for i=1:length(in2) in2{i} = lower(in2{i}); end % Check for unambiguous assignment: Each buttonname must be % unique! for i=1:length(in2) if length(strmatch(lower(in2{i}), in2,'exact')) ~= 1 % Ambituous naming -> Same buttoname multiple times! error('Same name assigned to multiple buttons! Sorry, names must be unique!'); end end rtbox_info(id).events(1:4)=in2; for i=5:8 rtbox_info(id).events(i)=cellstr([char(rtbox_info(id).events(i-4)) 'up']); end if nargout, varargout{1}=oldNames; end case 'ttlwidth' % One of the commands that change Firmware device settings. % Need to stop event processing and drain the event queue, as % the protocol doesn't implement proper acknowledge tokens: if rtbox_info(id).version < 3 RTBoxWarn('notSupported', in1, 3); return; end % Stop event processing on box, if active: boxActive = rtbox_info(id).boxScanning; if boxActive % This will store all pending events in internal queue: stopBox(id); end % Perform active query for current firmware settings: b8=get8bytes(id); % TTL width unit in s, not very accurate: wUnit=0.139e-3; % Return old/current setting as 1st argument: if rtbox_info(id).version > 4, b8(1) = 255 - b8(1); end varargout{1} = b8(1) * wUnit; % New settings provided? if nIn >= 2 if isempty(in2) % Default to 0.00097 seconds: in2=0.00097; end % Infinity means: Disable TTL-width, hold setting until % manually changed: if in2 == inf in2 = 0; end % Range check: if (in2 < wUnit * 0.9 || in2 > wUnit * 255 * 1.1) && (in2>0) RTBoxWarn('invalidTTLwidth', wUnit); return; end width = double(uint8(in2 / wUnit)) * wUnit; b8(1) = width / wUnit; if rtbox_info(id).version > 4, b8(1) = 255 - b8(1); end % Writeback new firmware settings: set8bytes(id, b8); if (in2 > 0) && (abs(width - in2) / in2 > 0.1) RTBoxWarn('widthOffset', width); end if width==0 width=inf; end % Return new / current setting as 1st argument: varargout{1} = width; end if boxActive % Restart box if it was running: startBox(id, 1); end case 'ttlresting' % One of the commands that change Firmware device settings. % Need to stop event processing and drain the event queue, as % the protocol doesn't implement proper acknowledge tokens: if rtbox_info(id).version < 3.1 RTBoxWarn('notSupported', in1, 3.1); return; end % Stop event processing on box, if active: boxActive = rtbox_info(id).boxScanning; if boxActive % This will store all pending events in internal queue: stopBox(id); end % Perform active query for current firmware settings: b8=get8bytes(id); % Return current setting as 1st return argument: varargout{1} = (b8(3) > 0); % New settings provided? if nIn >= 2 if isempty(in2) % Default to 0: in2 = logical([0 1]); end % Assign valid new setting: if rtbox_info(id).version < 5 b8(3) = in2(1) * 240; % '11110000' else b8(3) = bitset(b8(3),1,in2(1)); b8(3) = bitset(b8(3),2,in2(2)); end % Writeback new firmware settings: set8bytes(id, b8); end if boxActive % Restart box if it was running: startBox(id, 1); end case 'hardwaredebounce' % One of the commands that change Firmware device settings. % Need to stop event processing and drain the event queue, as % the protocol doesn't implement proper acknowledge tokens: if rtbox_info(id).version < 1.4 RTBoxWarn('notSupported', in1, 1.4); return; end % Stop event processing on box, if active: boxActive = rtbox_info(id).boxScanning; if boxActive % This will store all pending events in internal queue: stopBox(id); end % Perform active query for current firmware settings: b8=get8bytes(id); % Return current setting as 1st return argument: varargout{1} = b8(2); % New settings provided? if nIn >= 2 if isempty(in2) % Default to 16 firmware scancycles: in2 = 16; end % Range check: if in2 < 1 || in2 > 255 % Invalid: Warn & Ignore: RTBoxWarn('invalidScanNum'); else % Assign valid new setting: b8(2) = uint8(in2); % Writeback new firmware settings: set8bytes(id, b8); end end if boxActive % Restart box if it was running: startBox(id, 1); end case {'enable', 'disable'} % enable/disable event detection if nIn<2 % no event, return current state varargout{1}=rtbox_global.events4enable(rtbox_info(id).enabled); return; end % Stop event processing on box, if active: boxActive = rtbox_info(id).boxScanning; if boxActive % This will store all pending events in internal queue: stopBox(id); end isEnable=strcmp(cmd,'enable'); in2=lower(cellstr(in2)); for i=1:length(in2) ind=strmatch(in2{i},rtbox_global.events4enable,'exact'); if isempty(ind), RTboxError('invalidEnable',rtbox_global.events4enable); end if ind==6, ind=1:5; end % all rtbox_info(id).enabled(ind)=isEnable; % update state end if nargout, varargout{1}=rtbox_global.events4enable(rtbox_info(id).enabled); end if ~any(rtbox_info(id).enabled), RTboxWarn('allDisabled',rtbox_info(id).ID); end if boxActive % Restart box if it was running: startBox(id, 1); end case 'clockratio' % measure clock ratio computer/box % Default to 60 seconds for clock ratio calibration, unless % specified otherwise: if nIn<2, in2=60; end; % Interval between trials interval = 1.5 * rtbox_global.maxDuration; % Number of trials to perform: ntrial=max(5,round(in2/interval)); if rtbox_info(id).verbosity > 2 fprintf('PsychRTBox: Measuring clock ratio on box "%s". Trials remaining:%4.f', rtbox_info(id).ID, ntrial); end % Stop event processing on box, if active: boxActive = rtbox_info(id).boxScanning; if boxActive % This will store all pending events in internal queue: stopBox(id); end % Switch to realtime priority if not already there: oldPriority=Priority; if oldPriority < MaxPriority('GetSecs') Priority(MaxPriority('GetSecs')); end % Perform ntrial calibration trials: tnow = GetSecs; for i=1:ntrial % Update rtbox_info.sync via a syncClocks() operation: syncClocks(id); % Store new syncClocks sample in array: t(i,:)=rtbox_info(id).sync; %#ok % Give status output: if rtbox_info(id).verbosity > 2 fprintf('\b\b\b\b%4.f',ntrial-i); end % Make sure that trials are at least 'interval' secs apart: tnow = WaitSecs('UntilTime', tnow + interval); end % Restore priority to old value: if Priority ~= oldPriority Priority(oldPriority); end % Delete syncClocks samples collected during this clockRatio % calibration from internal array, as they might bias later % box->host time remapping: rtbox_info(id).syncSamples = rtbox_info(id).syncSamples(1:end-ntrial, :); % Restart scanning on box if it was active before: if boxActive startBox(id, 1); end % Use always polyfit to fit a line (with least squares error) % to the samples of host clock and box clock measurements. We % use polyfit because it is part of default Matlab/Octave, % doesn't require statistics toolbox or fitting toolbox: [coef, st] = polyfit(t(:,2)-t(1,2),t(:,1)-t(1,1), 1); % fit a line sd = st.normr * 1000; % std in ms % Assign new clock ratio for use by the timestamp mapping % routines later on: rtbox_info(id).clkRatio = coef(1); if rtbox_info(id).verbosity > 2 fprintf('\n Clock ratio (computer/box): %.7f\n', rtbox_info(id).clkRatio); fprintf(' Sigma (ms): %.2g\n',sd); end if (sd > 0.5) && rtbox_info(id).verbosity > 1 fprintf(2,' The ratio may not be reliable. Try longer time for ClockTest.\n'); end % Optionally return new ratio: if nargout varargout{1} = rtbox_info(id).clkRatio; end case 'test' % for device test, no complain on error byte=IOPort('bytesAvailable',s); nevent=floor(byte/7); data=IOPort('read',s,1,nevent*7); % read data data=reshape(data,[7 nevent]); enableEvent(id, 'a'); % avoid interference to the following commands purgeRTbox(id); % clear buffer fprintf(' ID(%g): %s, %s\n',id,rtbox_info(id).ID,rtbox_info(id).version); fprintf(' Serial port: %s (handle = %g)\n',rtbox_info(id).portname, s); IOPort('write',s,'?'); % ask button state b2=IOPort('read',s,1,2); b2=dec2bin(b2(1)/16,4); b2=sprintf('%s ',b2(4:-1:1)); fprintf(' Button down: %s\n',b2); fprintf(' Events enabled: %s\n',cell2str(rtbox_global.events4enable(find(rtbox_info(id).enabled)>0))); syncClocks(id); % sync clocks, restore detection fprintf(' ComputerClock/BoxClock: %.7f\n',rtbox_info(id).clkRatio); fprintf(' GetSecs-BoxSecs: %.4f\n',rtbox_info(id).sync(1)); fprintf(' Number of events: %g\n',nevent); if nevent==0, return; end t0=bytes2secs(data(2:7,1), id); % first event time fprintf(' BoxSecs of event 1 (t0): %.3f s\n\n',t0); fprintf(' Data (7 bytes each)%19s BoxSecs-t0\n','Events'); for i=1:nevent % disp each event and time ind=find(data(1,i)==rtbox_global.eventcodes); % which event if isempty(ind) event=''; t=nan; % no error complain else event=rtbox_info(id).events{ind}; % event ID t=bytes2secs(data(2:7,i), id); % device time end fprintf('%4g',data(:,i)); fprintf('%12s%8.3f\n',event,t-t0); % relative to first event end case 'close' % Close given device % Close port if ~isempty(rtbox_info(id)) if s>=0 % Disable all scanning on box before close: stopBox(id); % Box with firmware version 1.3 or later? if rtbox_info(id).version > 1.3 % Yes. This one supports "simple mode" for E-Prime % compatibility as well, so switch it into that % mode: % Enable all events WaitSecs(0.1); IOPort('Write', s, 'A'); % Switch to simple mode for E-Prime et al.: WaitSecs(0.1); IOPort('Write',s,'x'); % Give some time to settle: WaitSecs(0.1); end % Close connection: IOPort('Close', s); end % Mark corresponding struct slot as dead: rtbox_info(id).handle = []; % Decrease count of open devices: rtbox_global.nrOpen = rtbox_global.nrOpen - 1; end otherwise % Unknown command: RTboxError('unknownCmd',cmd,rtbox_global.cmds,rtbox_info(id),events); end % End of subfunction dispatch. % End of drivers "main" routine: end % Helper functions: % ----------------- % Map box timestamps to host clock (aka GetSecs) timestamps, based on clock % sync results from syncClocks, and clock drift correction from clock ratio % calibration: function timing = box2GetSecsTime(id, timing) global rtbox_info; thostbase = rtbox_info(id).sync(1); tboxbase = rtbox_info(id).sync(2); % Compute delta in box seconds from time when calibration was % done, wrt. box timer, then multiply with clockRatio % conversion factor to translate that delta into a delta wrt. % host clock, so timing will be the delta in host clock seconds % wrt. calibration time: timing = (timing - tboxbase) * rtbox_info(id).clkRatio; % Add basetime on host - this should give us correctly mapped % time in the GetSecs timebase of the computer: timing = timing + thostbase; end % Map box timestamps to host clock (aka GetSecs) timestamps, based on clock % sync results from all syncClocks samples from a session: function [timing, sd, clockratio] = box2GetSecsTimePostHoc(id, timing) global rtbox_info; global rtbox_global; % Stop box, if active, drain all queues. Please note that we don't % auto-restart the box, because this method is meant to be only called % at the end of an experiment session, so usercode must manually % restart if it wants to: stopBox(id); % Check if the latest syncClocks sample is older than 30 seconds. If % so, then we acquire a new final sample. We also resample if the last % sample is of too low accuracy, or if there are less than 2 samples in % total, as the fitting procedure needs at least 2 samples to work: while (size(rtbox_info(id).syncSamples, 1) < 2) || ... ((GetSecs - rtbox_info(id).syncSamples(end, 1)) > 30) || ... (rtbox_info(id).syncSamples(end, 3) > rtbox_global.maxMinwinThreshold) % Perform a syncClocks to get a fresh sample to finalize the sampleset: syncClocks(id); end % Extract samples for line fit: tbox = rtbox_info(id).syncSamples(:, 2); thost = rtbox_info(id).syncSamples(:, 1); % MK: Change of strategy: We always use polyfit() instead of sometimes % regress() or robustfit() for the following reasons: % % 1. Strategy switching may introduce artifacts / non-predictable % behaviour into the results. % % 2. A simple polyfit() is better understood than the more advanced % robustfit() with its tons of tunable parameters and strategies, for % which i don't know which are appropriate for our data. % % 3. robustfit() and regress() require the Matlab statistics toolbox % and therefore won't work on a plain standard Matlab installation, % whereas polyfit() is part of default Matlab and Octave. % % The old code is left here for documentation purpose: % % % More than 2 samples available? % if 0 && length(tbox) > 2 % % Octave and older versions of Matlab don't have 'robustfit', % % so we fall back to 'regress' if this function is lacking: % if exist('robustfit') %#ok % [coef st]=robustfit(tbox,thost); % fit a line % sd=st.robust_s; % stddev. in seconds. % else % coef =regress(thost, [ones(size(thost,1), 1), tbox ]); % fit a line % sd=0; % stddev. undefined with regress(). % end % clockratio = coef(2); % clockbias = coef(1); % else % Use polyfit, i.e., a least squares fit of a polynomial of either % degree 1 (purely linear), or degree 2 (quadratic). We choose a purely % linear fit if only 2 samples are available - the best we can do. If % more than 2 samples are available, we use a quadratic fit to account % for small non-linearities in the clock hardware over long intervals: [coef, st, mu] = polyfit(tbox, thost, min(length(tbox)-1, 2)); clockratio = coef(1); clockbias = coef(2); %#ok sd.norm = st.normr; % end % Ok, got mapping equation getsecst = timing * clockratio + clockbias; % Apply it to our input timestamps: % timing = timing * clockratio + clockbias; % % % Actually, we use polyval() to evaluate the equation. For linear case, % this is the same as above equation, but it conveniently also handles % the quadratic case and provides some margins on mapping error: if length(tbox) > 3 [timing, sd.delta] = polyval(coef, timing, st, mu); else % sd.delta undefined for less than 4 elements in timing: sd.delta = repmat(Inf, size(timing)); timing = polyval(coef, timing, st, mu); end if rtbox_info(id).verbosity > 3 fprintf('PsychRTBox: In post-hoc box->host mapping: Coefficients of mapping polynom (in decreasing order) are: '); disp(coef); fprintf('\n\n'); end % Ready. end % Clock sync routine: Synchronizes host clock (aka GetSecs time) to box % internal clock via a sampling and calibration procedure: function syncClocks(id) global rtbox_info; global rtbox_global; % Query level of verbosity to use: verbosity = rtbox_info(id).verbosity; % Perform multiple measurement trials per syncClocks run, take the best % one for final result. We use the "best" one because we have a good % criterion to find the best one. % We preallocate the sampling arrays for 250 samples at most. The % arrays will grow if 250 samples are not sufficient, at a small % performance penalty: ntrials = 250; % Any event reporting active? boxActive = rtbox_info(id).boxScanning; if boxActive % Disable all events on box. This will also clear all buffers: warning('PsychRTBox: syncClocks: Box was still scanning during invocation -- Driverbug?'); %#ok stopBox(id); end % Switch to realtime priority if not already there: oldPriority=Priority; if oldPriority < MaxPriority('GetSecs') Priority(MaxPriority('GetSecs')); end % Get porthandle: s = rtbox_info(id).handle; t = zeros(3,ntrials); minwin = inf; tdeadline = GetSecs + rtbox_global.maxDuration; ic = 0; % Perform measurement trials until success criterion satisfied: % Either a sample with a maximum error 'minwin' less than desired % threshold, or maximum allowable calibration time reached: while (minwin > rtbox_global.optMinwinThreshold) && (GetSecs < tdeadline) % 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); % Take pre-Write timestamp in tpre - Sync command not emitted % before that time. Write sync command, wait 'rtbox_global.blocking' for write % completion, store completion time in post-write timestamp tpost: [nw tpost, errmsg, tpre] = IOPort('Write', s, 'Y', rtbox_global.blocking); % We know that sync command emission has happened at some time % after tpre and before tpost. 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 tpost. That is a % soft constraint however - useful for computing the final estimate % for hosttime, but nothing to be taken 100% for granted. % Write op successfull? if nw==1 % Retrieve 7-Byte packet with timestamp from box: [b7 dummy errmsg] = IOPort('Read', s, 1, 7); else % Send op failed! fprintf('PsychRTBox: Warning! Sync token send operation to box failed! [%s]\n', errmsg); continue; end if length(b7)~=7 || b7(1)~=89 % Receive op failed! fprintf('PsychRTBox: Warning! Corrupt data received from box! [%s]\n', errmsg); continue; end % Decode boxtime into seconds (uncorrected for clock-drift): % % From the computed raw box clock time in seconds, we subtract % another 0.00016 secs, aka 160 microseconds to compensate for the % average delay between reception of the sync token byte, and the % timestamping on the box. Delay is: % 1. Transmission delay from FTDI chip to Microprocessor over % serial link at 115200 baud with 10 bits (1 start + 8 data + 1 % stop): 1000/115200*10 msecs = 0.087 msecs. % 2. Average scanning delay by firmware scanning loop: Interval is % 0.01 msecs, expected delay therefore 0.01/2 = 0.005 msecs. % 3. Time taken for firmware to take a snapshot of current box % clock time: 0.060 msecs. % % == 0.152 msecs. To account for minimal other delays, we round up % to 0.16 msecs aka 0.00016 secs and subtract this expected delay: tbox = bytes2secs(b7(2:7), id) - 0.00016; % Compute confidence interval for this sample: % For each measurement, the time window tpost - tpre defines a % worst case confidence interval for the "real" host system time % when the sync command was emitted. confidencewindow = tpost - tpre; % If the confidencewindow is greater than the maximum acceptable % window, then we reject this sample, else we accept it: if confidencewindow <= rtbox_global.maxMinwinThreshold % Within acceptable range. Accept this sample and check if it % is the best wrt. to window size so far: if confidencewindow < minwin % Best confidencewindow so far. Update minwin, as this is one % of the abortion critera: minwin = confidencewindow; end % Increase sample index to permanently accept this sample for % final set of competitors: ic = ic + 1; % Assign values: t(1,ic) = tpre; t(2,ic) = tpost; t(3,ic) = tbox; else % Inacceptably large error confidencewindow. Reject this sample: continue; end % Next sample pass: end % Done with sampling: We have up to 'ic' valid samples, unless minwin % is still == inf. % Restore priority to state pre syncClocks: if Priority ~= oldPriority Priority(oldPriority); end % Is box scanning supposed to be active? if boxActive % Restore event reporting: startBox(id, 1); end % At least one sample with acceptable precision acquired? if (minwin > rtbox_global.maxMinwinThreshold) || (ic < 1) % No, not even a single one! Set all results to "invalid" rtbox_info(id).sync=[inf, inf, inf]; if verbosity > 1 fprintf('PsychRTBox: Warning: On Box "%s", Clock sync failed due to confidence interval of best sample %f secs > allowable maximum %f secs.\n', rtbox_info(id).ID, minwin, rtbox_global.maxMinwinThreshold); fprintf('PsychRTBox: Warning: Likely your system is massively overloaded or misconfigured!\n'); fprintf('PsychRTBox: Warning: See the help for PsychRTBox(''SyncConstraints'') on how to relax the constraints if you wish to do so.\n'); end % Warn user: RTboxWarn('poorSync', minwin); % That's it: return; end % Ok, we have 'ic' > 0 samples with acceptable precision, according to % user specified constraints. Prune result array to valid samples 1 to ic: t = t(:, 1:ic); % We have three different methods for final sample selection. Each has % its favorable cases, but none of them is better by a large margin. % All can't guarantee accurate results on highly overloaded systems, % but only provide the best under given circumstances. % % Under low-load conditions, the new style methods may be slightly more % accurate under some conditions, but the difference is usually in the % sub-millisecond range, so either choice is ok. The options are mostly % here for benchmarking and stress testing of the driver... % % The default method (unless overriden by usercode or % userconfiguration) is method 0 -- New style with prewrite timestamps. % This because this method empirically seems to give best results on % low or medium load on our test set of machines and operating systems. switch rtbox_global.syncmode case 2 % Old style method 2 - Middle of minwin confidence window: % Select sample with smallest confidence interval [tpre; tpost]: [mintdiff, idx] = min(t(2,:) - t(1,:)); % Host time corresponds to midpoint of the confidence interval, % assuming a uniform distribution of likelyhood of the true write % time in the interval [tpre ; tpost]: hosttime = (t(1,idx) + t(2,idx)) / 2; case 1 % New style method 1 - Postwrite timestamps: % Choose the most accurate sample from the set of candidates. This is % the sample with the smallest difference between the postwrite % timestamp and the associated box timestamp, ie., with the smallest % offset between postwrite host clock time and box clock time at % receive of sync command. The reasoning behind this goes like this: % % 1) The time offset between host clock and box clock is a constant - % at least within a significant multi-second time interval between % successive syncClocks calls (due to only small clock drift), but % certainly within a syncClocks run of a few hundred milliseconds % (error due to clock drift in this interval is negligible). % % 2) Computed box clock time t(3,:) is "close to perfect", as this % timestamp is taken by box microprocessor and firmware with a very % small and basically constant delay after sync token receive, ie., % write completion. (Maximum theoretical error is smaller than 0.1 msecs). % % 3) The correct and optimal clock offset between host and box would be % tdiff = tsend - t(3,:) iff tsend would be host time at true write % completion. % % 4) The measured host time at write completion t(2,:) is always later % (and therefore numerically greater) than the true host time tsend at % write completion due to an unknown, random, greater than zero delay % tdelta, i.e., t(2,:) = tsend + tdelta, tdelta > 0. tdelta is the sum % of: % % a) The unknown delay of up to 1 msec between USB write-URB completion % by the USB host controller (which would be the real completion time % tsend) and detection of completion due to USB IOC (Interrupt-On- % Completion) due to invocation of the host controllers hardware % interrupt handler and host controller schedule scan and URB % retirement inside the interrupt handler. % % b) Random (and theoretically unbounded) scheduling delay / execution % delay between status update of the serial port data structures by the % interrupt handler and detection of write completion + timestamping by % the IOPort driver in polling mode, or scheduling delay between % wakeup-operation caused by the interrupt handler and start of % execution of the timestamping in the IOPort driver in blocking mode. % % The syncClocks error is therefore directly proportional to the size % of tdelta. Therefore: % % tdiff(:) = t(2,:) - t(3,:) by definition of clock offset host vs. box. % t(2,:) = tsend(:) + tdelta(:) by unknown scheduling/execution noise tdelta. % % It follows that by above definitions: % % tdiff(:) = tsend(:) - t(3,:) + tdelta(:); % % --> As we defined tsend(:) to be the unknown, but perfect and % noise-free, true send timestamp, and t(3,:) to be the perfect receive % timestamp by the box, it follows that by selecting the sample 'idx' % with the minimal tdiff(idx) from the set tdiff(:), we will select the % sample with the unknown, but minimal tdelta(idx). As tdelta accounts % for all the remaining calibration error, minimizing tdelta will % maximize the accuracy of the clock sync. % % ==> Select sample with minimum t(2,:) - t(3,:) as final best result: [mintdiff, idx] = min(t(2,:) - t(3,:)); % mintdiff is our best estimate of clock offset host vs. box, and % t(:,idx) is the associated best sample. Unfortunately there isn't any % way to compute the exact residual calibration error tdelta(idx). The % only thing we know is that the error is bounded by the length of the % associated 'minwin' confidence interval of this sample, so we will % return 'minwin' as an upper bound on the calibration error. As % 'minwin' was used as a threshold in the sample loop for outlier % rejection, we can be certain that our estimate carries no greater % error than 'rtbox_global.maxMinwinThreshold'. % Extract all relevant values for the final sample: % Host time corresponds to tpost write timestamp, which should be as % close as possible to real host send timestamp: hosttime = t(2,idx); case 0 % New style method 0 - Prewrite timestamps: % Choose the most accurate sample from the set of candidates. % This is the sample with the biggest difference between the % prewrite timestamp and the associated box timestamp, ie., % with the biggest offset between prewrite host clock time and % box clock time at receive of sync command. The reasoning % behind this goes like this: % % 1) The time offset between host clock and box clock is a constant - % at least within a significant multi-second time interval between % successive syncClocks calls (due to only small clock drift), but % certainly within a syncClocks run of a few hundred milliseconds % (error due to clock drift in this interval is negligible). % % 2) Computed box clock time t(3,:) is "close to perfect", as this % timestamp is taken by box microprocessor and firmware with a very % small and basically constant delay after sync token receive, ie., % write completion. (Maximum theoretical error is smaller than 0.1 msecs). % % 3) The correct and optimal clock offset between host and box would be % tdiff = tsend - t(3,:) where tsend would be unknown host time % at true write completion. % % 4) The measured host time before write submission t(1,:) is % always earlier % (and therefore numerically smaller) than the true host time tsend at % write completion due to an unknown, random, greater than zero delay % tdelta, i.e., t(1,:) = tsend - tdelta, tdelta > 0. tdelta is the sum % of: % % a) The unknown delay of up to 1 msec between USB write-URB completion % by the USB host controller (which would be the real completion time % tsend) and detection of completion due to USB IOC (Interrupt-On- % Completion) due to invocation of the host controllers hardware % interrupt handler and host controller schedule scan and URB % retirement inside the interrupt handler. % % b) Random (and theoretically unbounded) scheduling delay / execution % delay between status update of the serial port data structures by the % interrupt handler and detection of write completion + timestamping by % the IOPort driver in polling mode, or scheduling delay between % wakeup-operation caused by the interrupt handler and start of % execution of the timestamping in the IOPort driver in blocking mode. % % The syncClocks error is therefore directly proportional to the size % of tdelta. Therefore: % % tdiff(:) = t(1,:) - t(3,:) by definition of clock offset host vs. box. % t(1,:) = tsend(:) - tdelta(:) by unknown execution noise tdelta. % % It follows that by above definitions: % % tdiff(:) = tsend(:) - t(3,:) - tdelta(:); % % --> As we defined tsend(:) to be the unknown, but perfect and % noise-free, true send timestamp, and t(3,:) to be the perfect receive % timestamp by the box, it follows that by selecting the sample 'idx' % with the maximal tdiff(idx) from the set tdiff(:), we will select the % sample with the unknown, but minimal tdelta(idx). As tdelta accounts % for all the remaining calibration error, minimizing tdelta will % maximize the accuracy of the clock sync. % % ==> Select sample with maximum t(1,:) - t(3,:) as final best result: [maxtdiff, idx] = max(t(1,:) - t(3,:)); % maxtdiff is our best estimate of clock offset host vs. box, and % t(:,idx) is the associated best sample. Unfortunately there isn't any % way to compute the exact residual calibration error tdelta(idx). The % only thing we know is that the error is bounded by the length of the % associated 'minwin' confidence interval of this sample, so we will % return 'minwin' as an upper bound on the calibration error. As % 'minwin' was used as a threshold in the sample loop for outlier % rejection, we can be certain that our estimate carries no greater % error than 'rtbox_global.maxMinwinThreshold'. % Extract all relevant values for the final sample: % Host time corresponds to tpre write timestamp, which should be as % close as possible to real host send timestamp: hosttime = t(1,idx); otherwise error('PsychRTBox: syncClocks: Unknown timestamping method provided. This is a driverbug!!'); end % Box timers time taken "as is": boxtime = t(3,idx); % Recalculate upper bound on worst case error 'minwin' from this best % samples tpost - tpre: minwin = t(2,idx) - t(1,idx); if verbosity > 3 fprintf('PsychRTBox: ClockSync(%i): Box "%s": Got %i valid samples, maxconfidence interval = %f msecs, winner interval %f msecs.\n', rtbox_global.syncmode, rtbox_info(id).ID, ic, 1000 * rtbox_global.maxMinwinThreshold, 1000 * minwin); if rtbox_global.syncmode == 1 fprintf('PsychRTBox: Confidence windows in interval [%f - %f] msecs. Range of clock offset variation: %f msecs.\n', 1000 * min(t(2,:)-t(1,:)), 1000 * max(t(2,:)-t(1,:)), 1000 * psychrange(t(2,:) - t(3,:))); else fprintf('PsychRTBox: Confidence windows in interval [%f - %f] msecs. Range of clock offset variation: %f msecs.\n', 1000 * min(t(2,:)-t(1,:)), 1000 * max(t(2,:)-t(1,:)), 1000 * psychrange(t(1,:) - t(3,:))); end end % Assign (host,box,confidence) sample to sync struct: rtbox_info(id).sync=[hosttime, boxtime, minwin]; % Also store the sample in the syncSamples history: rtbox_info(id).syncSamples(end+1, :) = [hosttime, boxtime, minwin]; end % Send enable/disable string to tell box to enable or disable detection and % reporting of certain events. Wait for acknowledge from box that the % requested event enable/disable was carried out properly. Certain % characters encode certain types of events. A capital character enables % that event type, a minor character disables that event type. The special % letter 'A' or 'a' enables or disables all events. See the init routine % for a mapping table of button/electronictrigger,lightTrigger,serialTrigger % events to control characters. % % It is useful to disable event detection/reporting both to filter out % unwanted events for a given experiment, but also during clock calibration % and clockSync to prevent unwanted events from messing up the % communication protocol between the box and the driver and from % introducing unwanted processing delays during this time critical % procedure. function enableEvent(handle, str) global rtbox_info; global rtbox_global; if rtbox_info(handle).boxScanning error('PsychRTBox: enableEvent() called while box is scanning! Driverbug?'); %#ok end s = rtbox_info(handle).handle; if isnumeric(str) && isscalar(str) % Firmware version >= 4.1 : Encodes enabled events by a single byte. % First send event setup command code "e", then the given enable % byte: str = [uint8('e') , uint8(str)]; end for ie=1:length(str) % Wait until we can be sure that the box is ready to receive new % commands. The deadline is computed so that in the worst conceivable % case the box will be ready to receive at least 1 new command byte: WaitSecs('UntilTime', rtbox_info(handle).busyUntil); % Send control character for event enable/disable: [nw, tpost] = IOPort('Write', s, str(ie)); % Command submission completed at time 'tpost'. Set a new busyUntil % time. After that time, the box should have stopped at the latest: rtbox_info(handle).busyUntil = tpost + rtbox_global.maxbusy; % On firmware versions < 4.1 we expect an acknowledge byte for each % sent byte. On versions 4.1 to < 5.0 we only get an acknowledge for the % first 'e' character, but not the actual enable info byte afterwards. On % firmware version 5.0 and later we don't get acknowledges at all: if (rtbox_info(handle).version < 4.1) || ((ie == 1) && (rtbox_info(handle).version < 5.0)) % Store this command code as one of the codes to be waited for in parseQueue: rtbox_info(handle).ackTokens(end+1) = str(ie); end end % All enable tokens submitted. return; end % Retrieve firmware internal settings: function b8 = get8bytes(handle) global rtbox_info; % Get handle to serial port: s = rtbox_info(handle).handle; % Write query command blocking: IOPort('Write', s, 's'); % Wait blocking for 8 byte response: b8=IOPort('Read',s,1,8); return; end % Set/Update firmware internal settings: function set8bytes(handle, b8) global rtbox_info; global rtbox_global; % Get handle to serial port: s = rtbox_info(handle).handle; % Write 'S'et command blocking: IOPort('Write',s,'S'); % Wait blocking for 1 byte ack: IOPort('Read', s, 1, 1); % Write blocking new 8 byte settings: [nw tpost] = IOPort('Write', s, uint8(b8)); % Command submission completed at time 'tpost'. Set a new busyUntil % time. After that time, the box should have completed command at the latest: rtbox_info(handle).busyUntil = tpost + rtbox_global.maxbusy; return; end function [tsend, twin] = sendTTLPortEvent(handle, ecode) global rtbox_info; global rtbox_global; % Get handle to serial port: s = rtbox_info(handle).handle; % Wait until we can be sure that the box is ready to receive new % commands. The deadline is computed so that in the worst conceivable % case the box will be ready to receive at least 1 new command byte: WaitSecs('UntilTime', rtbox_info(handle).busyUntil); % Submit event code to RTbox: [nw tpost err tpre] = IOPort('Write', s, ecode); % send % Compute tsend time approximation and confidence window twin: twin = tpost - tpre; tsend = tpre; % Command submission completed at time 'tpost'. Set a new busyUntil % time. After that time, the box should have completed command at the latest: rtbox_info(handle).busyUntil = tpost + rtbox_global.maxbusy; return; end % Send a 'Y' serial trigger command to box: function tpost = sendTrigger(handle) global rtbox_info; global rtbox_global; % Get handle to serial port: s = rtbox_info(handle).handle; % Wait until we can be sure that the box is ready to receive new % commands. The deadline is computed so that in the worst conceivable % case the box will be ready to receive at least 1 new command byte: WaitSecs('UntilTime', rtbox_info(handle).busyUntil); % Box ready to receive our "serial trigger" command code % 'Y'. Send it blocking: [nw, tpost] = IOPort('Write', s, 'Y'); % Command submission completed at time 'tpost'. Set a new busyUntil % time. After that time, the box should have stopped at the latest: rtbox_info(handle).busyUntil = tpost + rtbox_global.maxbusy; return; end % Send a "enable optical/pulse/TTL trigger" command evType to box: function tpost = engageTrigger(handle, evType) global rtbox_info; global rtbox_global; % Get handle to serial port: s = rtbox_info(handle).handle; % Wait until we can be sure that the box is ready to receive new % commands. The deadline is computed so that in the worst conceivable % case the box will be ready to receive at least 1 new command byte: WaitSecs('UntilTime', rtbox_info(handle).busyUntil); % Box ready to receive our enable light trigger command code % evType. Send it blocking: [nw, tpost] = IOPort('Write', s, evType); % Command submission completed at time 'tpost'. Set a new busyUntil % time. After that time, the box should have stopped at the latest: rtbox_info(handle).busyUntil = tpost + rtbox_global.maxbusy; return; end % Query current state of box buttons -- the raw state, unaffected by % debouncing or actual enable event settings, as close to now-time as % possible: function bState = buttonQuery(handle) global rtbox_info; % Get handle to serial port: s = rtbox_info(handle).handle; % Box in scanning mode? And button state change event reporting active? if (rtbox_info(handle).boxScanning) && all(rtbox_info(handle).enabled(1:2)) % Yes. Box provides button press/release events, so its sufficient % to parse the receive queue. % Drain the serial receive buffer until either 10 seconds total % time elapsed or no new data for at least 20 msecs: parseQueue(handle, inf, 10, 0.020); else % Box is maybe scanning, but not reporting. % Is box scanning? isActive = rtbox_info(handle).boxScanning; if isActive % Yes. We can't perform a synchronous query with box in % scanning mode. Need to stopBox(): stopBox(handle); % Now we startBox(), because this will trigger a callback into % us recursively, i.e. to buttonQuery(), but this time with % isActive == false, therefore the recursive call will go % through the else-clause and execute the query. -- No need for % us to do it here redundantly :-) startBox(handle, 1); % Done. else % Box is not scanning and reporting, all serial buffers are empty % and idle. We perform a synchronous query: IOPort('write',s,'?'); % ask button state: '4321'*16 63 b2=IOPort('read',s, 1, 2); % ? returns 2 bytes if length(b2)~=2 || ... ((rtbox_info(handle).version < 1.4) && (b2(2)~=63 || mod(b2(1),16)~=0)) || ... ((rtbox_info(handle).version >= 1.4) && (b2(1)~=63 || mod(b2(2),16)~=0)) % Failed! Corrupt response: warning('PsychRTBox: Corrupt 2-byte response received in explicit buttonQuery()!'); %#ok else % Uppermost 4 bits are button states: rtbox_info(handle).buttons(1, :) = [ 0, 0, 0, 0 ]; % Set corresponding entries for pressed buttons: if rtbox_info(handle).version < 1.4 % Version prior 1.4: Data first, then ? rtbox_info(handle).buttons(1, logical(bitget(b2(1), 5:8))) = 1; else % Version >= 1.4: ? first, then data: rtbox_info(handle).buttons(1, logical(bitget(b2(2), 5:8))) = 1; end end end end bState = rtbox_info(handle).buttons(1, :); return; end % Start event scanning, detection and reporting on box: function startBox(handle, WaitForStart) global rtbox_info; global rtbox_global; % Box scanning? if rtbox_info(handle).boxScanning % Box already started. Nothing to do: return; end % Emit a buttonstate query command, so we get a button state update: buttonQuery(handle); % Enable all events that are selected by usercode: customEnable = rtbox_info(handle).enabled(1:6); % Synchronize the button up/down events. Either both on or both off. % We do this so our software based button state live reporting in % parseQueue() works correctly - It needs press and release to be % reported: if any(customEnable(1:2)) customEnable(1:2) = [1 1]; else customEnable(1:2) = [0 0]; end % The box always starts and stops with all event processing disabled, % transitions are only possible in startBox() and stopBox(), and we can % only enter startBox() after a previous call to stopBox() or at % initial startup. Therefore we know that all event processing is % disabled when we reach this point. % % enableEvent() will enable the user defined events non-blocking, ie. % without waiting for acknowledge from the box: if rtbox_info(handle).version < 4.1 % Old style enable codes: enableEvent(handle, rtbox_global.enableCode(find(customEnable>0)) ); %#ok else % Firmware version >= 4.1 : Encodes enabled events by a single byte. % Translate our customEnable into a single byte: byteEnable = 2.^(0:5) * customEnable'; enableEvent(handle, byteEnable); end % Box event scanning and reporting is active: rtbox_info(handle).boxScanning = 1; % Should we wait until we get acknowledge from box? if WaitForStart % Yes. Stay in parseQueue until all event enable codes % acknowledged: parseQueue(handle, -2, inf, 10); end return; end % Stop event detection by box and all data transmission. Drain serial % receive queue completely after box is idle and enqueue to event queue: function stopBox(handle) global rtbox_info; global rtbox_global; % Box scanning? if rtbox_info(handle).boxScanning == 0 % Box already stopped. Nothing to do: return; end % Get handle to serial port: s = rtbox_info(handle).handle; % Wait until we can be sure that the box is ready to receive new % commands. The deadline is computed so that in the worst conceivable % case the box will be ready to receive at least 1 new command byte: WaitSecs('UntilTime', rtbox_info(handle).busyUntil); % Box ready to receive our "disable all event reporting" command code % 'a'. Send it blocking: [nw, tpost] = IOPort('Write', s, 'a'); % Command submission completed at time 'tpost'. Set a new busyUntil % time. After that time, the box should have stopped at the latest: rtbox_info(handle).busyUntil = tpost + rtbox_global.maxbusy; WaitSecs('UntilTime', rtbox_info(handle).busyUntil); % Box should be idle now. Submit a marker token which the box can % acknowledge, so we know when all receive queues have fully drained. % We send all disable codes, as they are no-ops (all events are already % disabled) but get acknowledged by the box: markerStr = 'dufopa'; for i=1:length(markerStr) IOPort('Write', s, markerStr(i)); % We delay for 0.1 msecs after each write just to make absolutely % sure it gets through: WaitSecs(0.0001); end % Marker submitted. After some roundtrip delay, the box will feed this % back as the last element in the receive buffer, after all events that % were detected and dispatched by the box before the 'a' disable all % call took effect. % Drain the serial receive buffer until the marker token has been % received and all queues are completely empty: We allow thist to take % as long as neccessary, i.e., absolute timeout is inf-inity, but we % abort if more than 10 seconds elapse without any new data. Rationale: % If tons of events are pending in the receive buffer, it might take % long to fetch all of them, therefore the generous unlimited inf, but % if nothing new arrives within 10 seconds then the buffer is clearly % empty, no new data is arriving from box and the marker token hasn't % been detected (-1), so something went seriously wrong, e.g., % communication failure with box or corrupt marker token received, and % we need to recover by aborting the parse-Op: parseQueue(handle, -1, inf, 10); % Box event scanning and reporting is stopped: rtbox_info(handle).boxScanning = 0; % Ok, the box is completely idle and all serial queues are empty. The % software queue contains all remaining events. return; end % Parse serial port receive queue, dequeue all stored events and store it % in internal software queue. function [nadded, tlastadd] = parseQueue(handle, minEvents, timeOut, interEventDelay) global rtbox_info; % Count of added items in this invocation: nadded = 0; tlastadd = 0; % Child protection: if ~rtbox_info(handle).boxScanning warning('PsychRTBox: parseQueue() called but box not in active scanning mode! Driverbug?!?'); %#ok end % Get handle to serial port: s = rtbox_info(handle).handle; if minEvents == -1 untilMarker = 1; minEvents = inf; else untilMarker = 0; end if minEvents == -2 untilAllAcknowledged = 1; minEvents = inf; else untilAllAcknowledged = 0; end % Setup deadline for stop of parsing, based on timeOut: tcurrent = GetSecs; timeOut = tcurrent + timeOut; tInterTimeout = tcurrent + interEventDelay; % Parse until timeOut reached, or minimum number of requested events % dequeued, whatever comes first: while (tcurrent <= timeOut) && (minEvents > 0) && (tcurrent <= tInterTimeout) % Shall we repeat until all outstanding tokens acknowledged? if untilAllAcknowledged && isempty(rtbox_info(handle).ackTokens) % Yes. And we're done: break; end % Fetch at most one byte, non-blocking: Also update current % timestamp: [evid, tcurrent] = IOPort('Read', s, 0, 1); if isempty(evid) % Nothing received. Sleep a msec, slacky, to release cpu: tcurrent = WaitSecs('YieldSecs', 0.001); continue; end % Something received. Update interevent timeout: tInterTimeout = tcurrent + interEventDelay; % rtbox_global.eventcodes=[49:2:55 50:2:56 97 48 57 89]; % code for 12 events % Dispatch depending on type of received data: % 7-Byte event packet? if (evid >= 48 && evid <= 57) || (evid == 89) || (evid == 97) % Read remaining 6 bytes of 7 byte packet, either a timestamp % or parameters. We do this blocking as we know there must be 6 % more bytes pending: [b6, tcurrent] = IOPort('Read', s, 1, 6); if length(b6)<6 warning('PsychRTBox: parseQeue: Corrupt 7-byte data packet received from box!'); %#ok else % evid 'a' is a special case: An 'a' can be either an % acknowledge for disable of all event reporting, or a electronic % pulse trigger event :-( % Treat this special case of character a == 97, but only if % a marker token is expected. Then we might receive the % special 'dufopa' signature in b6: if untilMarker && (evid == 97) && strcmp(char(b6), 'dufopa') % Marker token detected! This is the end of our % parsing operation, as we know no further data can % be received from the box after this marker. % Btw. the 'dufopa' also corresponds to a specific 6 % byte timestamp, so this is ambiguous in theory. % However the timestamp corresponding to 'dufopa' would % correspond to a box uptime (and therefore computer % uptime) of over 30 years -- Unlikely / Impossible to % ever happen in reality, so this is a non-issue. % Break out of parser loop: break; end % If we reach this point, then it is a standard event % packet with a b6 6 byte box timestamp. % Decode timestamp into uncorrected box time in seconds: secs=bytes2secs(b6, handle); % Buttonstate update needed? if (evid >= 49 && evid <= 56) % One of the four pushbuttons: Pressed or released? buttonid = floor((evid - 47)/2); isdown = mod(evid, 2); % Update live state: rtbox_info(handle).buttons(1, buttonid) = isdown; % Debouncer implementation: Is this button updated % after the debounce deadline which defines the "dead" % interval of ignoring this button? if secs < rtbox_info(handle).buttons(2, buttonid) % This event was received within the "dead % interval" in which state updates for the button % should be ignored for the purpose of button % debouncing. We abort processing for this event, % ie., the event gets discarded: continue; else % Button event received after debounce deadline. We % accept this event and update the debounce % deadline accordingly: rtbox_info(handle).buttons(2, buttonid) = secs + rtbox_info(handle).buttons(3, buttonid); end % Special handling for button press/release events. % These are always reported by box if one of them is % enabled. Check if we should discard some of them: if (isdown && ~rtbox_info(handle).enabled(1)) || (~isdown && ~rtbox_info(handle).enabled(2)) % This event shall not be reported to higher % layers. Skip it: continue; end end % Are there any tokens pending for acknowledge? if ~isempty(rtbox_info(handle).ackTokens) % Yes. Unacknowledged tokens pending. We discard this % event. We will discard all events until there are no % more unacknowedged tokens around: continue; end % Enqueue event in internal queue: rtbox_info(handle).recQueue(end+1, :) = [ double(evid), secs ]; % Increment total count of added events: nadded = nadded + 1; % Update timestamp of last event add operation: tlastadd = GetSecs; % Decrement count of events to parse: minEvents = minEvents - 1; end % Next iteration: continue; end % Any acknowledge tokens to wait for? if ~isempty(rtbox_info(handle).ackTokens) % Is current event the next expected token? if evid == rtbox_info(handle).ackTokens(1) % Yes. Dequeue this token. rtbox_info(handle).ackTokens = rtbox_info(handle).ackTokens(2:end); % Next iteration: continue; end end % Button state live query result packet? This has the evid 63 == '?' % Ok, this doesn't work due to a design-flaw in the protocol. The % box sends the data first in the evid byte, then the '?' % identifier in the 2nd byte, so our parser can't dispatch % reliably. :-( % % The only solution / workaround is to never submit a '?' query % while the box is in scanning mode, so this event can't ever % happen. -- We use lot's of ugly magic in buttonQuery() to work % around this synchronously... % % Note: The bug is fixed in firmware v1.4 and later, but that % doesn't help us anymore, so we keep the current strategy... % % % if evid == 63 % b1=IOPort('read', s, 1, 1); % ? returns 1 byte with encoded button state. % if isempty(b1) || mod(b1,16)~=0 % warning('PsychRTBox: parseQeue: Corrupt 2-byte data packet received from box!'); %#ok % else % % Uppermost 4 bits are button states: % rtbox_info(handle).buttons(1, :) = [ 0, 0, 0, 0 ]; % % Set corresponding entries for pressed buttons: % rtbox_info(handle).buttons(1, logical(bitget(b1, 5:8))) = 1; % end % % % Decrement count of events to parse: % % This is a non-event in the sense of ther parse function... minEvents = minEvents - 1; % % % Next iteration: % continue; % end % Next parse iteration: end if untilAllAcknowledged && ~isempty(rtbox_info(handle).ackTokens) warning(['PsychRTBox: parseQeue: Did not receive expected acknowledgements from box! Missing acks: ' char(rtbox_info(handle).ackTokens)]); %#ok end % Done. return; end % Retrieve events from internal driver queue, optionally fetch new items % from serial port device receive queue if internal queue can't satisfy the % amount of requested events or usercode wants full coverage of all what is % there: % % - Most of the time you will want to set minItems == MaxItems == nItems % with nItems being the exact amount of events you'd like to have within % some timeout period (for recovery from sleeping subjects), or nItems == % inf and some reasonable timeouts to get all responses within some % defined response interval. % % - If the box is not in scanning mode, this will return all data up to % 'maxItems' events from the internal receive queue instantaneously and % the timeouts and minItems don't apply. % function [evts, boxtimes] = getEvents(handle, minItems, maxItems, timeOut, maxInterEvent) global rtbox_info; % Must not operate much longer than tdeadline for given timeOut: tcurrent = GetSecs; tdeadline = tcurrent + timeOut; tInterdeadline = tcurrent + maxInterEvent; evAvail = 0; % Box in scanning mode? % If box is not in active scanning mode then all serial receive buffers % are empty and the box won't produce new data. All we can ever get is % what is already in our internal recQueue. In that case, we skip the % hardware buffer scan loop. if rtbox_info(handle).boxScanning % Yes: New data may arrive from box anytime... % Repeat fetch & assign operation until timeout, or enough items found: while (tcurrent < tdeadline) && (tcurrent < tInterdeadline) && (evAvail < minItems) % How many events are stored in our own filtered, debounced event Queue? evAvail = size(rtbox_info(handle).recQueue, 1); % How many do we need to fetch from box serial receive buffer? needMore = minItems - evAvail; % Need any? if needMore > 0 % Need to fetch from serial queue: if tdeadline - tcurrent < tInterdeadline - tcurrent tscandeadline = tdeadline - tcurrent; else tscandeadline = tInterdeadline - tcurrent; end [nadded, tlastadd] = parseQueue(handle, needMore, tscandeadline, maxInterEvent); else nadded = 0; end % Update time: tcurrent = GetSecs; % Anything added? if nadded > 0 % Yes. Update interevent deadline: tInterdeadline = tlastadd + maxInterEvent; end if (needMore > 0) && (nadded == 0) % Nothing received. Sleep a msec, slacky, to release cpu: tcurrent = WaitSecs('YieldSecs', 0.001); end % Check again. end end % How many events are stored in our own filtered, debounced event Queue? evAvail = size(rtbox_info(handle).recQueue, 1); % Hopefully got something. Extract subset of at most maxItems oldest % elements: if evAvail > maxItems evAvail = maxItems; end % Assign first evAvail elements of queue to return arguments: if evAvail > 0 evts = rtbox_info(handle).recQueue(1:evAvail, 1); boxtimes = rtbox_info(handle).recQueue(1:evAvail, 2); % Strip them from queue: rtbox_info(handle).recQueue = rtbox_info(handle).recQueue(evAvail+1:end, :); else evts = []; boxtimes = []; end return; end % Purge all buffers from stale data: function purgeRTbox(handle) global rtbox_info; % Get IOPort handle: s = rtbox_info(handle).handle; % Stop all processing on box, drain its buffers: stopBox(handle); if IOPort('BytesAvailable', s) warning('PsychRTBox: purgeRTBox: Unexpected data in receive buffer after stopBox()! n = %i\n', IOPort('BytesAvailable', s)); %#ok end % Make additional low-level call to clear all serial port buffers - % This is redundant: IOPort('Purge', s); if IOPort('BytesAvailable', s) warning('PsychRTBox: purgeRTBox: Unexpected data in receive buffer after serial purge! n = %i\n', IOPort('BytesAvailable', s)); %#ok end % Clear the receive event queue of the box: rtbox_info(handle).recQueue = []; % All pending events cleared: return; end % Convert 6-byte raw box timestamp x into seconds: boxClockTickIntervalSecs % is the time unit of the device clock, i.e., the device clock increments % with a granularity of boxClockTickIntervalSecs: function secs=bytes2secs(b6, handle) global rtbox_info; secs = (256.^(5:-1:0) * b6(:) * rtbox_info(handle).boxClockTickIntervalSecs); end % Open first available (not yet opened) RT-Box if no specific 'deviceID' is % given, otherwise try to open box with that 'deviceID'. Store info about % opened box in slot 'handle' of the rtbox_info struct array: function openRTBox(deviceID, handle) global rtbox_info; global rtbox_global; %#ok % Setup possible port probe list for different operating systems: if IsWin if ~isempty(strfind(deviceID, 'COM')) % COM port name provided: Use specified COM port: ports{1} = deviceID; else % Enumerate all possible ports as candidates for box: % Suppose RTBox not assigned to COM1 or COM2, as these are % usually the native serial ports, if any: for kk=3:5 ports{kk - 2} = { sprintf('\\\\.\\COM%i', kk) }; end ports=strtrim(ports); % needed for Matlab R2009b and later. end nPorts=length(ports); end if IsOSX || IsLinux if ~isempty(strfind(deviceID, '/dev/')) % Explicit port name provided: Use it. ports{1} = deviceID; nPorts = 1; else % Enumerate all available USB-Serial ports as candidates for box: if IsOSX candidateports = [dir('/dev/cu.usbserial*') ; dir('/dev/cu.usbmodem*')]; else candidateports = [dir('/dev/ttyUSB*') ; dir('/dev/ttyACM*')]; end for i=1:length(candidateports) ports{i} = ['/dev/', candidateports(i).name]; end nPorts = length(candidateports); end end % Any found? if nPorts==0 RTboxError('noUSBserial'); end % Silence IOPorts output to prevent visual clutter: verbo=IOPort('Verbosity', 0); % Prepare device detection loop: deviceFound=0; rec=struct('avail','','busy',''); % for error record only trycount = 0; % Retry device detection procedure up to a total of 2 times: % Sometimes a freshly connected RTBox has a little hickup at first try, % but it reliably works at the 2nd iteration: while (trycount < 2) && (~deviceFound) % Scan each possible port if it is the response box: for ic=1:nPorts % Device file name of port to probe: port = char(ports{ic}); % Already opened? if strmatch(port, {rtbox_info(1:handle-1).portname},'exact') % Yes. Skip this candidate: continue; end % Try to open port: We open at maximum supported baud rate of 115200 % bits, use a timeout for blocking reads of 1 second, and request % low-latency polling for write completion if the IOPort('Write') % command uses a polling method for waiting for write completion. Set % the "sleep time" between consecutive polls to 0.0001 seconds = 0.1 % msecs. That is good enough for our purpose and still prevents system % overload on OS/X and Linux. We also set the ReceiveLatency to 0.1 msecs, % aka 0.0001 seconds: This parameter is silently ignored on Windows, honored % in some way by some serial port drivers on OS/X (well, maybe, who knows?), % and on Linux any value <= 1 msecs enables ASYNC_LOW_LATENCY mode on serial % ports, ie. some low-latency optimizations. E.g., with the FTDI chips used in % RTBox it will automatically set the chips latency timer to its minimum of 1 msec: % UPDATE 22-May-2017: Actually no, on Linux it will not set 1 msec hw latency timer % unless Octave/Matlab is run as sudo root, which is not generally recommended! Our % psychtoolbox.rules udev rules file though will set the hw timer to 1 msec nonetheless, % so ReceiveLatency on Linux neither helps nor hurts. [s errmsg]=IOPort('OpenSerialPort', port, 'Lenient BaudRate=115200 ReceiveTimeout=1.0 PollLatency=0.0001 ReceiveLatency=0.0001 '); % Worked? if s>=0 % Device open succeeded. Test if it is really our box and not some % other serial-USB device: % Flush all transmit/receive queues: % N.B. This flushes receive and send buffers on Windows and OS/X, % but doesn't flush the send buffers on Linux -- not implemented at % the FTDI driver level as of Linux 2.6.22: IOPort('Purge', s); % Read out whatever junk maybe in the input buffer: IOPort('Read', s, 0); % Write the 'X' command code to ask box for its identity and to % switch it into advanced mode with the clever protocol we % use. The box powers up in "simple" mode, where most cool % functionality and protocol is disabled but it acts in a % way that is compatible to E-Prime et al. -- Just sends 1 % Byte event code whenever some button or input port % changes state: IOPort('Write', s, 'X'); % Wait blocking with 1 sec timeout (see above) for id string response from box: idn=char(IOPort('Read', s, 1, 21)); % Expected response is a 21-Bytes string of format 'USTCRTBOX,115200,v?.?' % with ?.? being the major and minor firmware/box revision. if strfind(idn,'USTCRTBOX') % Found device: deviceFound=1; % Make sure we get the full valid string, repeat query if neccessary: while length(idn) ~= 21 % Read out whatever junk maybe in the input buffer: IOPort('Read', s, 0); % Write the 'X' command code to ask box for its identity again: IOPort('Write', s, 'X'); % Wait blocking with 1 sec timeout for id string: idn=char(IOPort('Read', s, 1, 21)); end break; end % Not our box :-( IOPort('Close', s); % Store port as existent but not used by us: rec.avail{end+1}=port; %#ok elseif isempty(strfind(errmsg,'ENOENT')) % Failed to open port, but not with error code ENOENT. % Open failed, but port exists. That means it is busy - used by % ourselves or some other process: rec.busy{end+1}=port; %#ok if isempty(strfind(errmsg,'EPERM')) && isempty(strfind(errmsg,'EBUSY')) % No permission or busy error. Something else went % wrong. Tell user: RTboxWarn('openError', errmsg); end end % Scan next candidate port: end % Tried trycount times to perform open operation: trycount = trycount + 1; % Retry, if neccessary: end % Done with scan. Restore normal level of debug output of IOPort, as % selected by external usercode or defaults: IOPort('Verbosity',verbo); % Found a suitable RTBox within number of retries? if ~deviceFound % Nope. Bail out: RTboxError('noDevice', rec, rtbox_info); end % Ok, found a box and opened connection. Setup its device info structure: % First the default settings... rtbox_info(handle)=struct('events',{{'1' '2' '3' '4' '1up' '2up' '3up' '4up' 'pulse' 'light' 'tr' 'serial'}},... 'enabled',[], 'ID','','handle',-1,'portname',[],'sync',[],'version',[],'clkRatio',1,'verbosity',3, ... 'busyUntil', 0, 'boxScanning', 0, 'ackTokens', [], 'buttons', [0 0 0 0; 0 0 0 0; 0.05 0.05 0.05 0.05], ... 'syncSamples', [], 'recQueue', [], 'boxClockTickIntervalSecs', 1/115200); % Enabled events at start: rtbox_info(handle).enabled=logical([1 0 0 0 0 0]); % IOPort serial port handle: rtbox_info(handle).handle=s; % DeviceID: rtbox_info(handle).ID=deviceID; % store handle and deviceID % Portname: rtbox_info(handle).portname=port; % Box version: ind = strfind(idn, ',v') + 2; rtbox_info(handle).version = str2double(char(idn(ind + (0:2)))); % Duration of a single clock tick of the RTBox microprocessor clock: % Defaults to 1/115200 second, but will be overridden to 1/921600 second % if the spec string is found as part of the idn string. This finer % resolution is present in the latest RTBox devices (Firmware v1.4: if strfind(idn,'921600') rtbox_info(handle).boxClockTickIntervalSecs = 1/921600; end % Init clock-ratio to an uncalibrated 1.0: rtbox_info(handle).clkRatio=1; % Start with all scanning disabled. We do the initial disable via % low-level writes to make absolutely sure that the box is "off", as % all other utility routines rely on a well defined initial off state. % Read out whatever junk maybe in the input buffer: IOPort('Read', s, 0); % Submit disable all command: if IOPort('Write', s, 'a')~=1 RTBoxError('[In event disable]: Failed to initialize RTBox to proper startup settings!'); end % Wait 200 msecs: WaitSecs(0.2); % Go into an infinite loop that repeats until nothing available in % receive buffer: while 1 curItem = char(IOPort('Read', s, 1, 1)); if IOPort('BytesAvailable', s) == 0 break; end WaitSecs(0.050); end % Last item == disable all acknowledge? if curItem ~= 'a' RTBoxError('[In event disable II]: Failed to initialize RTBox to proper startup settings!'); end % Ok, now the box is fully idle and we return with all event scanning % disabled. % Device open and initialized. fprintf('PsychRTBox: RTBox device "%s" opened on serial port device %s.\n', deviceID, port); end % put verbose error message here, to make main code cleaner function RTboxError(err,varargin) global rtbox_info; global rtbox_global; switch err case 'invalidTTL' str='TTL output must be integer from 0 to 15, or bits from ''0000'' to ''1111''.'; case 'noUSBserial' str=' No connected USB-Serial devices found. Is your device connected and the USB-Serial driver properly installed?'; case 'noDevice' p=varargin{1}; str=sprintf(' No RTBox on available ports found or no USB-Serial port found at all, or no port with the given name exists.'); if ~isempty(p.avail) % have available ports str=sprintf([' USB-Serial Port(s) available: %s, but failed to identify any of them as an RTBox device.\n' ... ' Is any of them the RTBox device? If yes, try again.\n' ... ' It may help to unplug, then replug the device.\n\n'],cell2str(p.avail)); end if ~isempty(p.busy) % have busy ports str=sprintf(['%s USB-Serial Port(s) unavailable:\n%s\nProbably already in use by another program.\n' ... 'Is any of the given busy ports the RTBox device? If yes, try ''clear all'' to close the ports and then retry.\n'], str, cell2str(p.busy)); end % Have opened RTbox? if rtbox_global.nrOpen > 0 str=sprintf('%s\nAlready opened RTBox device(s): ', str); for i=1:length(rtbox_info) str=sprintf('%s %s at %s,\n',str, char(rtbox_info(i).ID), char(rtbox_info(i).portname)); end str(end)='.'; end case 'unknownCmd' str=sprintf(['Unknown subcommand or trigger: ''%s''.\n'... 'The first string input must be one of the commands or events:\n%s,\n%s.'],... varargin{1},cell2str(varargin{2}),cell2str(varargin{3})); case 'invalidButtonNames' str=sprintf('ButtonNames requires a cellstr containing four button names.'); case 'invalidButtonName' str=sprintf('Invalid button name: %s.',varargin{1}); case 'notRespond' str=sprintf('Failed to communicate with the device due to communication error. Try to close and re-connect the device.'); case 'invalidEnable' str=sprintf('Valid events for enable/disable: %s.',cell2str(varargin{1})); case 'triggerDisabled' str=sprintf('Trigger is not enabled. You need to enable ''%s''.',varargin{1}); otherwise, str=err; end error(sprintf('PsychRTBox:%s %s', err, WrapString(str) )); %#ok end % Show warning message, but code will keep running function RTboxWarn(err,varargin) switch err case 'invalidTTLwidth' str=sprintf('Supported TTL width is from %.2fe-3 to %.2fe-3 s .',[1 255]*varargin{1}*1000); case 'widthOffset' str=sprintf('TTL width will be about %.5f s', varargin{1}); case 'invalidScanNum' str=sprintf('The scan number for HardwareDebounce must be an integer from 1 to 255.'); case 'notSupported' str=sprintf('The command %s is supported only for Firmware version V%.1f or later.',varargin{1:2}); case 'invalidEvent' str=sprintf('%4i',varargin{1}); str=sprintf('PsychRTBox: Events not recognized: %s.\nPlease do PsychRTBox(''clear'') before showing stimulus.\nGetSecs = %.1f',str,GetSecs); case 'noTrigger' str=sprintf('PsychRTBox: Trigger ''%s'' not detected. GetSecs = %.1f', varargin{1}, GetSecs); case 'poorSync' str=sprintf('PsychRTBox: Unreliable synchronization detected (variation range %.1f ms).\nGetSecs = %.1f', varargin{1}*1000,GetSecs); case 'allDisabled' str=sprintf('PsychRTBox: All event detection has been disabled for %s.', varargin{1}); case 'openError' str=sprintf('PsychRTBox: Some unknown error occured while trying to open port. The IOPOrt driver says: %s', varargin{1}); otherwise str=sprintf('PsychRTBox: %s. GetSecs = %.1f',err,GetSecs); end str=WrapString(str); warning(['PsychRTBox:' err], str); end % return str from cellstr for printing, also remove port path function str=cell2str(Cstr) if isempty(Cstr) str=''; return; end str=''; for i=1:length(Cstr) str = [str Cstr{i} ' ']; %#ok end end