Skip to content

Commit 539214c

Browse files
committed
Add triggered buffer base class.
1 parent d55f569 commit 539214c

7 files changed

Lines changed: 237 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Info>
3+
<Category UUID="FileClassCategory">
4+
<Label UUID="design"/>
5+
</Category>
6+
</Info>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Info location="triggeredBuffer.m" type="File"/>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Info>
3+
<Category UUID="FileClassCategory">
4+
<Label UUID="design"/>
5+
</Category>
6+
</Info>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Info location="demo_triggered_buffer.m" type="File"/>

toolbox/dependencies/+bossapi

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
%% Create bossdevice object and start
2+
bd = bossdevice;
3+
bd.start;
4+
5+
%% Create and arm triggered buffer object
6+
bufObj = triggeredBuffer(bd,'spf_eeg','gen_running', @(x) x>0, 100, 1000);
7+
bufObj.arm;
8+
9+
%% When buffer is complete (check isComplete or listen to BufferComplete)
10+
data = bufObj.read;
11+
triggerTime = bufObj.TriggerTime;
12+
13+
%% Plot buffer data
14+
bufObj.plot;

toolbox/src/triggeredBuffer.m

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
classdef triggeredBuffer < bossapi.inst.streamingAsyncBuffer
2+
%TRIGGEREDBUFFER Creates a buffer that stores signal data according to a trigger condition
3+
4+
properties (SetAccess = immutable)
5+
preTrigger_ms
6+
postTrigger_ms
7+
8+
TriggerSignal string
9+
TriggerCondition function_handle
10+
end
11+
12+
properties (GetAccess = private, SetAccess = immutable, Hidden)
13+
RemainingPostTriggerSamplesInitial int32
14+
end
15+
16+
properties (SetAccess = private)
17+
isTriggered logical
18+
isArmed logical
19+
20+
TriggerTime {mustBePositive} % Time of occurrence of trigger event
21+
end
22+
23+
properties (Dependent)
24+
isComplete logical
25+
end
26+
27+
properties (Access = private, Hidden)
28+
RemainingPostTriggerSamplesCurrent int32
29+
Target slrealtime.Target
30+
Inst slrealtime.Instrument
31+
end
32+
33+
events
34+
BufferComplete
35+
end
36+
37+
methods
38+
function obj = triggeredBuffer(bossObj, signalName, triggerSignal, triggerCondition, preTrigger_ms, postTrigger_ms, options)
39+
arguments
40+
bossObj {mustBeA(bossObj,"bossdevice")}
41+
signalName {mustBeTextScalar}
42+
triggerSignal {mustBeTextScalar}
43+
triggerCondition function_handle
44+
preTrigger_ms {mustBeInteger,mustBeNonnegative}
45+
postTrigger_ms {mustBeInteger,mustBeNonnegative}
46+
options.ArrayIndex {mustBeVector(options.ArrayIndex,"allow-all-empties"),mustBeInteger} = [];
47+
options.SignalProps {mustBeText} = {};
48+
end
49+
50+
options.AppName = bossObj.appName;
51+
bufOptions = struct2pairs(options);
52+
obj@bossapi.inst.streamingAsyncBuffer(signalName, '', (preTrigger_ms+postTrigger_ms)/1000, true, bufOptions{:});
53+
54+
obj.preTrigger_ms = preTrigger_ms;
55+
obj.postTrigger_ms = postTrigger_ms;
56+
obj.RemainingPostTriggerSamplesInitial = 1+round(postTrigger_ms/obj.getSamplePeriod(bossObj.appName));
57+
% Check if trigger signal exists by calling its info. Errors out if signal is not found
58+
bossapi.inst.getInfoSignalFromMldatx(options.AppName,triggerSignal);
59+
obj.TriggerSignal = triggerSignal;
60+
obj.TriggerCondition = triggerCondition;
61+
62+
obj.Target = bossObj.targetObject;
63+
64+
% Reset current object once is set up
65+
obj.reset;
66+
67+
function C=struct2pairs(S)
68+
% Turns a scalar struct S into a cell of string-value pairs C
69+
if iscell(S)
70+
C=S; return
71+
elseif length(S)>1
72+
error('Input must be a scalar struct or cell');
73+
end
74+
75+
C=[fieldnames(S).'; struct2cell(S).'];
76+
C=C(:).';
77+
end
78+
end
79+
80+
function isComplete = get.isComplete(obj)
81+
isComplete = obj.RemainingPostTriggerSamplesCurrent <= 0;
82+
end
83+
84+
function addToInstrument(obj, instObj)
85+
addToInstrument@bossapi.inst.signalCallbackManager(obj, instObj);
86+
end
87+
88+
function arm(obj)
89+
assert(~obj.isArmed,'Buffer is already armed.');
90+
obj.Inst = slrealtime.Instrument;
91+
obj.Inst.addSignal(obj.TriggerSignal);
92+
obj.addToInstrument(obj.Inst);
93+
obj.Inst.connectCallback(@obj.write);
94+
95+
obj.Target.addInstrument(obj.Inst);
96+
obj.isArmed = true;
97+
end
98+
99+
function disarm(obj)
100+
obj.Target.removeInstrument(obj.Inst);
101+
obj.isArmed = false;
102+
end
103+
104+
function reset(obj)
105+
obj.isArmed = false;
106+
obj.isTriggered = false;
107+
obj.RemainingPostTriggerSamplesCurrent = obj.RemainingPostTriggerSamplesInitial;
108+
reset@bossapi.inst.streamingAsyncBuffer(obj);
109+
end
110+
111+
function delete(obj)
112+
obj.disarm;
113+
end
114+
115+
function out = read(obj)
116+
if obj.isComplete
117+
out = read@bossapi.inst.streamingAsyncBuffer(obj,'extractAsTimetable',true);
118+
else
119+
error('Buffer is not complete yet. Please wait.');
120+
end
121+
end
122+
123+
function write(obj, instObj, event)
124+
arguments
125+
obj
126+
instObj slrealtime.Instrument
127+
event slrealtime.internal.instrument.AcquireGroupDataEvent
128+
end
129+
130+
if obj.Enable && ~obj.isComplete
131+
% Get data for trigger signal
132+
[triggerTime,triggerData] = getCallbackDataForSignal(instObj, event, obj.TriggerSignal);
133+
% Get data for buffer signal
134+
[signalTime,signalData] = obj.getCallbackDataForSignal(instObj, event);
135+
if ~ismatrix(signalData)
136+
signalData = squeeze(signalData)';
137+
end
138+
139+
% Evaluate trigger condition
140+
cond = feval(obj.TriggerCondition, triggerData);
141+
triggerIdx = find(cond==true,1,"first");
142+
143+
if ~isempty(triggerIdx) && ~obj.isTriggered
144+
% Trigger found in this data chunk
145+
obj.TriggerTime = triggerTime(triggerIdx);
146+
obj.isTriggered = true;
147+
148+
% Add all buffers pretrigger to buffer
149+
obj.writeToBuffer(signalTime(1:triggerIdx-1), signalData(1:triggerIdx-1,:));
150+
151+
% Remove rows already added to buffer
152+
signalTime(1:triggerIdx-1) = [];
153+
signalData(1:triggerIdx-1,:) = [];
154+
end
155+
156+
% Fill buffer with new samples depending on trigger
157+
numRows = numel(signalTime);
158+
if obj.isTriggered
159+
rowsToAdd = min(obj.RemainingPostTriggerSamplesCurrent, numRows);
160+
obj.writeToBuffer(signalTime(1:rowsToAdd), signalData(1:rowsToAdd,:));
161+
162+
% Update remaining post trigger samples
163+
obj.RemainingPostTriggerSamplesCurrent = obj.RemainingPostTriggerSamplesCurrent - rowsToAdd;
164+
else
165+
% Fill buffer with all new samples
166+
obj.writeToBuffer(signalTime, signalData);
167+
end
168+
169+
% Check if buffer is complete in this call
170+
if obj.isComplete
171+
notify(obj,'BufferComplete');
172+
end
173+
end
174+
end
175+
176+
function p = plot(obj, parentFig)
177+
arguments
178+
obj
179+
parentFig = uifigure
180+
end
181+
182+
% WIP
183+
data = obj.read;
184+
p = plot(parentFig, data.Time, data.Variables);
185+
end
186+
end
187+
188+
methods (Access = private)
189+
function writeToBuffer(obj, time, data)
190+
if ~isempty(data)
191+
if ~isempty(obj.ArrayIndex)
192+
obj.Buffer.write([time, data(:, obj.ArrayIndex)]);
193+
else
194+
obj.Buffer.write([time, data]);
195+
end
196+
end
197+
end
198+
end
199+
200+
methods (Hidden)
201+
function peek(varargin)
202+
peek@bossapi.inst.streamingAsyncBuffer(varargin);
203+
end
204+
end
205+
206+
end

0 commit comments

Comments
 (0)