-
Notifications
You must be signed in to change notification settings - Fork 1
/
ScannerSynchClass.m
executable file
·466 lines (409 loc) · 17.3 KB
/
ScannerSynchClass.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
% Interface for National Instruments PCI 6503 card
% Version 3.2
%
% DESCRIPTION
%
% N.B.: It does not monitor pulses in the background, so you have to make sure that you wait for any pulse before it comes!
%
% Properties (internal variables):
% IsValid = device set and operational
% TR = set a non-zero value (in seconds) for emulation mode (will not detect "real" pulses)
% Keys = set a cell of key names for emulation mode. For key names look for KbName.m.
% N.B.: Requires PTB.
% N.B.: Suppress passing keypresses to MATLAB during the whole experiment
%
% Clock = interal clock (seconds past since the first scanner pulse or clock reset)
% Synch = current state of the scanner synch pulse
% TimeOfLastPulse = time (according to the internal clock) of the last pulse
% MeasuredTR = estimated TR
% SynchCount = number of scanner synch pulses
% MissedSynch = number of missed scanner synch pulses
%
% EmulSynch = is scanner synch pulse emulated
% EmulButtons = is button box emulated
%
% Buttons = current state of the any button
% ButtonPresses = index/indices of each button(s) pressed since last check
% TimeOfButtonPresses = time (according to the internal clock) of each button(s) pressed since last check
% LastButtonPress = index/indices of the last button(s) pressed
% TimeOfLastButtonPress = time (according to the internal clock) of the last button press (any)
% BBoxTimeout = set a non-Inf value (in seconds) to wait for button press only for a limited time
% = set a negative value (in seconds) to wait even in case of response
%
% Methods (internal functions):
% ScannerSynchClass = constructor
% delete = destructor
% ResetClock = reset internal clock
%
% ResetSynchCount = reset scanner synch pulse counter
% SetSynchReadoutTime(t) = blocks scanner synch pulse readout after a pulse for 't' seconds
% WaitForSynch = wait until a scanner synch pulse arrives
% CheckSynch(t) = wait for a scanner synch pulse for 't' seconds or unitl a scanner synch pulse arrives (whichever first) and returns whether a scanner synch pulse was detected
%
% SetButtonReadoutTime(t) = blocks individual button readout after a button press for 't' seconds (detection of other buttons is still possible)
% SetButtonBoxReadoutTime(t) = blocks the whole button box readout after a button press for 't' seconds (detection of other buttons is also not possible)
% WaitForButtonPress = wait until a button is pressed
% WaitForButtonRelease = wait until a button is released
%
% USAGE
%
% Initialise:
% SSO = ScannerSynchClass;
% % SSO = ScannerSynchClass(1); % emulate scanner synch pulse
% % SSO = ScannerSynchClass(0,1); % emulate button box
% % SSO = ScannerSynchClass(1,1); % emulate scanner synch pulse and button box
%
% Close:
% SSO.delete;
%
% Example for scanner synch pulse #1: - Simple case
% SSO.SetSynchReadoutTime(0.5);
% SSO.TR = 2; % allows detecting missing pulses
% while SSO.SynchCount < 10 % polls 10 pulses
% SSO.WaitForSynch;
% fprintf('Pulse %d: %2.3f. Measured TR = %2.3fs\n',...
% SSO.SynchCount,...
% SSO.TimeOfLastPulse,...
% SSO.MeasuredTR);
% end
%
% Example for scanner synch pulse #2 - Chance for missing pulse
% SSO.SetSynchReadoutTime(0.5);
% SSO.TR = 2; % allows detecting missing pulses
% while SSO.SynchCount < 10 % until 10 pulses
% WaitSecs(Randi(100)/1000); % in every 0-100 ms ...
% if SSO.CheckSynch(0.01) % ... waits for 10 ms for a pulse
% fprintf('Pulse %d: %2.3f. Measured TR = %2.3fs. %d synch pulses has/have been missed\n',...
% SSO.SynchCount,...
% SSO.TimeOfLastPulse,...
% SSO.MeasuredTR,...
% SSO.MissedSynch);
% end
% end
%
% Example for buttons:
% SSO.SetButtonReadoutTime(0.5); % block individual buttons
% % SSO.SetButtonBoxReadoutTime(0.5); % block the whole buttonbox
% % SSO.Keys = {'f1','f2','f3','f4'}; % emulation Buttons #1-#4 with F1-F4
% n = 0;
% % SSO.BBoxTimeout = 1.5; % Wait for button press for 1.5s
% % SSO.BBoxTimeout = -1.5; % Wait for button press for 1.5s even in case of response
% SSO.ResetClock;
% while n ~= 10 % polls 10 button presses
% SSO.WaitForButtonPress; % Wait for any button to be pressed
% % SSO.WaitForButtonRelease; % Wait for any button to be released
% % SSO.WaitForButtonPress([],2); % Wait for Button #2
% % SSO.WaitForButtonPress(2); % Wait for any button for 2s (overrides SSO.BBoxTimeout only for this event)
% % SSO.WaitForButtonPress(-2); % Wait for any button for 2s even in case of response (overrides SSO.BBoxTimeout only for this event)
% % SSO.WaitForButtonPress(2,2); % Wait for Button #2 for 2s (overrides SSO.BBoxTimeout only for this event)
% % SSO.WaitForButtonPress(-2,2); % Wait for Button #2 for 2s even in case of response (overrides SSO.BBoxTimeout only for this event)
% n = n + 1;
% for b = 1:numel(SSO.ButtonPresses)
% fprintf('#%d Button %d ',b,SSO.ButtonPresses(b));
% fprintf('pressed at %2.3fs\n',SSO.TimeOfButtonPresses(b));
% end
% fprintf('Last: Button %d ',SSO.LastButtonPress);
% fprintf('pressed at %2.3fs\n\n',SSO.TimeOfLastButtonPress);
% end
%_______________________________________________________________________
% Copyright (C) 2015 MRC CBSU Cambridge
%
% Tibor Auer: [email protected]
%_______________________________________________________________________
classdef ScannerSynchClass < handle
properties
TR = 0 % emulated pulse frequency
PulseWidth = 0.005 % emulated pulse width
Keys = {}
BBoxTimeout = Inf % second (timeout for WaitForButtonPress)
end
properties (SetAccess = private)
SynchCount = 0
MissedSynch = 0
ButtonPresses
TimeOfButtonPresses
LastButtonPress
EmulSynch
EmulButtons
end
properties (Access = private)
DAQ
nChannels
tID % internal timer
Data % current data
Datap % previous data
TOA % time of access 1*n
TOAp % previous time of access 1*n
ReadoutTime = 0 % sec to store data before refresh 1*n
BBoxReadout = false
BBoxWaitForRealease = false % wait for release instead of press
isDAQ
isPTB
end
properties (Dependent)
IsValid
Clock
Synch
TimeOfLastPulse
MeasuredTR
Buttons
TimeOfLastButtonPress
end
methods
%% Contructor and destructor
function obj = ScannerSynchClassVerity(emulSynch,emulButtons)
fprintf('Initialising Scanner Synch...');
% test environment
obj.isDAQ = true;
try
D = daq.getDevices;
if ~D.isvalid ||...
~any(strcmp({D.Vendor.ID},'ni')) ||...
~D.Vendor.isvalid ||...
~D.Vendor.IsOperational, ...
obj.isDAQ = false;
end % no NI card or not working
catch
obj.isDAQ = false; % no DA Toolbox
end
% Create session
if ((nargin<2) || ~emulSynch || ~emulButtons) && obj.isDAQ
warning off daq:Session:onDemandOnlyChannelsAdded
obj.DAQ = daq.createSession('ni');
% Add channels for scanner pulse
obj.DAQ.addDigitalChannel('Dev1', 'port0/line0', 'InputOnly');
% Add channels for button 1-4
obj.DAQ.addDigitalChannel('Dev1', 'port0/line1', 'InputOnly');
obj.DAQ.addDigitalChannel('Dev1', 'port0/line2', 'InputOnly');
obj.DAQ.addDigitalChannel('Dev1', 'port0/line3', 'InputOnly');
obj.DAQ.addDigitalChannel('Dev1', 'port0/line4', 'InputOnly');
% Add channels for button 5-8
obj.DAQ.addDigitalChannel('Dev1', 'port0/line5', 'InputOnly');
obj.DAQ.addDigitalChannel('Dev1', 'port0/line6', 'InputOnly');
obj.DAQ.addDigitalChannel('Dev1', 'port0/line7', 'InputOnly');
obj.DAQ.addDigitalChannel('Dev1', 'port1/line0', 'InputOnly');
switch nargin
case 2
obj.EmulSynch = emulSynch;
obj.EmulButtons = emulButtons;
case 1
obj.EmulSynch = emulSynch;
obj.EmulButtons = false;
case 0
obj.EmulSynch = false;
obj.EmulButtons = false;
end
else
obj.isDAQ = false;
obj.DAQ.isvalid = true;
obj.DAQ.Vendor.isvalid = true;
obj.DAQ.Vendor.IsOperational = true;
obj.EmulSynch = true;
obj.EmulButtons = true;
obj.DAQ.Channels = 1:9;
fprintf('\n');
fprintf('WARNING: DAQ card is not in use!\n');
end
obj.isPTB = exist('KbCheck','file') == 2;
if ~obj.IsValid
warning('WARNING: Scanner Synch is not open!');
obj.delete;
return
end
if obj.EmulSynch
fprintf('Emulation: Scanner synch pulse is not in use --> ');
fprintf('You may need to set TR!\n');
end
if obj.EmulButtons
fprintf('Emulation: ButtonBox is not in use --> ');
fprintf('You may need to set Keys!\n');
end
obj.nChannels = numel(obj.DAQ.Channels);
obj.Data = zeros(1,obj.nChannels);
obj.Datap = zeros(1,obj.nChannels);
obj.ReadoutTime = obj.ReadoutTime * ones(1,obj.nChannels);
obj.ResetClock;
fprintf('Done\n');
end
function delete(obj)
fprintf('Scanner Synch is closing...');
if obj.isDAQ
obj.DAQ.release();
delete(obj.DAQ);
warning on daq:Session:onDemandOnlyChannelsAdded
end
if obj.isPTB, ListenChar(0); end
fprintf('Done\n');
end
%% Utils
function val = get.IsValid(obj)
val = ~isempty(obj.DAQ) &&...
obj.DAQ.isvalid &&...
obj.DAQ.Vendor.isvalid &&...
obj.DAQ.Vendor.IsOperational &&...
(~obj.EmulButtons || (obj.EmulButtons && obj.isPTB));
end
function ResetClock(obj)
obj.tID = tic;
obj.TOA = zeros(1,obj.nChannels);
obj.TOAp = zeros(1,obj.nChannels);
end
function val = get.Clock(obj)
val = toc(obj.tID);
end
function set.Keys(obj,val)
obj.Keys = val;
if obj.EmulButtons && obj.isPTB, ListenChar(2); end % suppress passing keypresses to MATLAB
end
%% Scanner Pulse434737
function ResetSynchCount(obj)
obj.SynchCount = 0;
end
function SetSynchReadoutTime(obj,t)
obj.ReadoutTime(1) = t;
end
function WaitForSynch(obj)
while ~obj.Synch
end
obj.NewSynch;
end
function val = CheckSynch(obj,timeout)
SynchQuery = obj.Clock;
val = false;
while (obj.Clock - SynchQuery) < timeout
if obj.Synch
obj.NewSynch;
val = true;
break;
end
end
end
function val = get.TimeOfLastPulse(obj)
val = obj.TOA(1);
end
function val = get.MeasuredTR(obj)
val = (obj.TOA(1)-obj.TOAp(1))/(obj.MissedSynch+1);
end
%% Buttons
function SetButtonReadoutTime(obj,t)
obj.ReadoutTime(2:end) = t;
obj.BBoxReadout = false;
end
function SetButtonBoxReadoutTime(obj,t)
obj.ReadoutTime(2:end) = t;
obj.BBoxReadout = true;
end
function WaitForButtonPress(obj,timeout,ind)
BBoxQuery = obj.Clock;
% Reset indicator
obj.ButtonPresses = [];
obj.TimeOfButtonPresses = [];
obj.LastButtonPress = [];
% timeout
if (nargin < 2 || isempty(timeout)), timeout = obj.BBoxTimeout; end
wait = timeout < 0; % wait until timeout even in case of response
timeout = abs(timeout);
while (~obj.Buttons ||... % button pressed
wait || ...
(nargin >= 3 && ~isempty(ind) && ~any(obj.LastButtonPress == ind))) && ... % correct button pressed
(obj.Clock - BBoxQuery) < timeout % timeout
if ~isempty(obj.LastButtonPress)
if nargin >= 3 && ~isempty(ind) && ~any(obj.LastButtonPress == ind), continue; end % incorrect button
if ~isempty(obj.TimeOfButtonPresses) && (obj.TimeOfButtonPresses(end) == obj.TimeOfLastButtonPress), continue; end % same event
obj.ButtonPresses = horzcat(obj.ButtonPresses,obj.LastButtonPress);
obj.TimeOfButtonPresses = horzcat(obj.TimeOfButtonPresses,ones(1,numel(obj.LastButtonPress))*obj.TimeOfLastButtonPress);
end
end
end
function WaitForButtonRelease(obj,varargin)
% backup settings
rot = obj.ReadoutTime(2:end);
bbro = obj.BBoxReadout;
% config for release
obj.BBoxWaitForRealease = true;
obj.SetButtonBoxReadoutTime(0);
WaitForButtonPress(obj,varargin);
% restore settings
obj.BBoxWaitForRealease = false;
obj.ReadoutTime(2:end) = rot;
obj.BBoxReadout = bbro;
end
function val = get.TimeOfLastButtonPress(obj)
val = max(obj.TOA(2:end)) * ~isempty(obj.LastButtonPress);
end
function [b, t] = ReadButton(obj)
b = obj.LastButtonPress;
t = obj.TimeOfLastButtonPress;
obj.LastButtonPress = [];
obj.ButtonPresses = [];
obj.TimeOfButtonPresses = [];
end
%% Low level access
function val = get.Synch(obj)
val = 0;
obj.Refresh;
if obj.Data(1)
obj.Data(1) = 0;
val = 1;
end
end
function val = get.Buttons(obj)
val = 0;
obj.Refresh;
if obj.BBoxWaitForRealease
if any(obj.Datap(2:end)) && all(~(obj.Data(2:end).*obj.Datap(2:end)))
obj.LastButtonPress = find(obj.Datap(2:end));
obj.Datap(2:end) = 0;
val = 1;
end
else
if any(obj.Data(2:end))
obj.LastButtonPress = find(obj.Data(2:end));
obj.Data(2:end) = 0;
val = 1;
end
end
end
end
methods (Access = private)
function Refresh(obj)
t = obj.Clock;
% get data
if obj.isDAQ
data = ~inputSingleScan(obj.DAQ); % inverted
else
data = zeros(1,obj.nChannels);
end
% scanner synch pulse emulation
if obj.EmulSynch && obj.TR
data(1) = ~obj.SynchCount || ((t-obj.TOA(1) >= obj.TR) && (mod(t-obj.TOA(1),obj.TR) <= obj.PulseWidth));
end
% button press emulation (keyboard) via PTB
if obj.EmulButtons
nKeys = numel(obj.Keys);
if obj.isPTB && nKeys
[ ~, ~, keyCode ] = KbCheck;
data(2:2-1+nKeys) = keyCode(KbName(obj.Keys));
end
end
if obj.BBoxReadout, obj.TOA(2:end) = max(obj.TOA(2:end)); end
ind = obj.ReadoutTime < (t-obj.TOA);
obj.Datap = obj.Data;
obj.Data(ind) = data(ind);
obj.TOAp = obj.TOA;
obj.TOA(logical(obj.Data)) = t;
end
function NewSynch(obj)
if ~obj.SynchCount
obj.ResetClock;
obj.SynchCount = 1;
else
if obj.TR
obj.MissedSynch = 0;
obj.MissedSynch = round(obj.MeasuredTR/obj.TR)-1;
end
obj.SynchCount = obj.SynchCount + 1 + obj.MissedSynch;
end
end
end
end