|
| 1 | +classdef SignalsExpPanel < eui.ExpPanel |
| 2 | + %EUI.SIGNALSEXPPANEL Basic UI control for monitoring a Signals experiment |
| 3 | + % Displays all values of events, inputs and outputs signals as they |
| 4 | + % arrive from the remote stimulus server. These events arrive as |
| 5 | + % 'signals' ExpEvents which are added to the SignalUpdates queue by |
| 6 | + % expUpdate and processed by processUpdates upon calling the update |
| 7 | + % method. |
| 8 | + % |
| 9 | + % |
| 10 | + % Part of Rigbox |
| 11 | + |
| 12 | + % 2015-03 CB created |
| 13 | + |
| 14 | + properties |
| 15 | + % Structure of signals event updates from SignalExp. As new updates |
| 16 | + % come in, previous updates in the list are overwritten |
| 17 | + SignalUpdates = struct('name', cell(500,1), 'value', cell(500,1), 'timestamp', cell(500,1)) |
| 18 | + % List of updates to exclude (when Exclude == true) or to exclusively |
| 19 | + % show (Exclude == false) in the InfoGrid. |
| 20 | + UpdatesFilter = {'inputs.wheel', 'pars'} |
| 21 | + % Flag for excluding updates in UpdatesFilter list from InfoGrid. When |
| 22 | + % false only those in the list are shown, when false those in the list |
| 23 | + % are hidden |
| 24 | + Exclude = true |
| 25 | + % The total number of |
| 26 | + NumSignalUpdates = 0 |
| 27 | + % containers.Map of InfoGrid ui labels mapped to their corresponding |
| 28 | + % Signal name |
| 29 | + LabelsMap |
| 30 | + % The colour of recently updated Signals update events in the InfoGrid |
| 31 | + RecentColour = [0 1 0] |
| 32 | + end |
| 33 | + |
| 34 | + |
| 35 | + methods |
| 36 | + function obj = SignalsExpPanel(parent, ref, params, logEntry) |
| 37 | + % Subclasses must chain a call to this. |
| 38 | + obj = obj@eui.ExpPanel(parent, ref, params, logEntry); |
| 39 | + obj.LabelsMap = containers.Map(); % Initialize labels map |
| 40 | + end |
| 41 | + |
| 42 | + function update(obj) |
| 43 | + % UPDATE Update the panel |
| 44 | + % Processes any new updates via a call to the processUpdates method |
| 45 | + % and changes colours of info field labels based on how recently |
| 46 | + % they updated. This method is the callback to the RefreshTimer in |
| 47 | + % MC. Subclasses must chain a call to this. |
| 48 | + % |
| 49 | + % See also eui.ExpPanel/update |
| 50 | + update@eui.ExpPanel(obj); % Elapsed timer updated by superclass |
| 51 | + processUpdates(obj); % Update labels with latest signal values |
| 52 | + labelsMapVals = values(obj.LabelsMap)'; |
| 53 | + labels = deal([labelsMapVals{:}]); |
| 54 | + if ~isempty(labels) % Colour decay by recency on labels |
| 55 | + dt = cellfun(@(t)etime(clock,t),... |
| 56 | + ensureCell(get(labels, 'UserData'))); |
| 57 | + c = num2cell(exp(-dt/1.5)*obj.RecentColour, 2); |
| 58 | + set(labels, {'ForegroundColor'}, c); |
| 59 | + end |
| 60 | + end |
| 61 | + end |
| 62 | + |
| 63 | + methods (Access = protected) |
| 64 | + function newTrial(obj, num, condition) |
| 65 | + % NEWTRIAL Process new trial conditions |
| 66 | + % Do nothing, this is for subclasses to override and react to, e.g. |
| 67 | + % to update plots, etc. based on a new trial's conditional |
| 68 | + % parameters. For a SignalsExp experiment, this may be called by |
| 69 | + % the processUpdates method upon an events.newTrial signal update. |
| 70 | + % In the future SignalsExp may send newTrial events (i.e. |
| 71 | + % independant of the 'signals' event updates) |
| 72 | + % |
| 73 | + % Inputs: |
| 74 | + % num (int) : The new trial number. May be used to index into |
| 75 | + % Block property |
| 76 | + % condition (struct) : Condition data for the new trial |
| 77 | + % |
| 78 | + % See also processUpdates, expUpdate, trialCompleted |
| 79 | + end |
| 80 | + |
| 81 | + function trialCompleted(obj, num, data) |
| 82 | + % TRIALCOMPLETED Process completed trial data |
| 83 | + % Do nothing, this is for subclasses to override and react to, e.g. |
| 84 | + % to update plots, etc. based on a complete trial's data. Called by |
| 85 | + % expUpdate method upon 'trialData' event (currently not used by |
| 86 | + % exp.SignalsExp). |
| 87 | + % |
| 88 | + % Inputs: |
| 89 | + % num (int) : The new trial number. May be used to index into |
| 90 | + % Block property |
| 91 | + % data (struct) : Completed trial data |
| 92 | + % |
| 93 | + % See also expUpdate, processUpdates, trialCompleted |
| 94 | + end |
| 95 | + |
| 96 | + function event(obj, name, t) |
| 97 | + % EVENT Process none-signals experiment event |
| 98 | + % Called by expUpdate callback to process all miscellaneous events, |
| 99 | + % i.e. experiment phases. This method is downstream of srv.ExpEvent |
| 100 | + % events. Updates ActivePhases list as well as the panel title |
| 101 | + % colour and, upon phase changes, the Status info field. |
| 102 | + % |
| 103 | + % Inputs: |
| 104 | + % name (char) : The event name |
| 105 | + % t (date vec) : The time the event occured |
| 106 | + % |
| 107 | + % Example: |
| 108 | + % if strcmp(evt.Data{1}, 'event') % srv.ExpEvent object |
| 109 | + % % Pass event info to be processed |
| 110 | + % obj.event(evt.Data{2}, evt.Data{3}) |
| 111 | + % end |
| 112 | + |
| 113 | + %called when an experiment event occurs |
| 114 | + phaseChange = false; |
| 115 | + if strEndsWith(name, 'Started') |
| 116 | + if strcmp(name, 'experimentStarted') |
| 117 | + obj.Root.TitleColor = [0 0.8 0.05]; % green title area |
| 118 | + else |
| 119 | + %phase has started, add it to active phases |
| 120 | + phase = name; |
| 121 | + phase(strfind(name, 'Started'):end) = []; |
| 122 | + obj.ActivePhases = [obj.ActivePhases; phase]; |
| 123 | + phaseChange = true; |
| 124 | + end |
| 125 | + elseif strEndsWith(name, 'Ended') |
| 126 | + if strcmp(name, 'experimentEnded') |
| 127 | + obj.Root.TitleColor = [0.98 0.65 0.22]; %amber title area |
| 128 | + obj.ActivePhases = {}; |
| 129 | + phaseChange = true; |
| 130 | + else |
| 131 | + %phase has ended, remove it from active phases |
| 132 | + phase = name; |
| 133 | + phase(strfind(name, 'Ended'):end) = []; |
| 134 | + obj.ActivePhases(strcmp(obj.ActivePhases, phase)) = []; |
| 135 | + phaseChange = true; |
| 136 | + end |
| 137 | + % else |
| 138 | + % disp(name); |
| 139 | + end |
| 140 | + if phaseChange % only update if there was a change for efficiency |
| 141 | + %update status with list of running phases |
| 142 | + phasesStr = ['[' strJoin(obj.ActivePhases, ',') ']']; |
| 143 | + set(obj.StatusLabel, 'String', sprintf('Running %s', phasesStr)); |
| 144 | + end |
| 145 | + end |
| 146 | + |
| 147 | + function processUpdates(obj) |
| 148 | + % PROCESSUPDATES Process all accumulated signals event updates |
| 149 | + % Process the signals events that have occured since the method was |
| 150 | + % last called. Any new field labels are created and all fields are |
| 151 | + % updated with the most recent signal values. |
| 152 | + % |
| 153 | + % This function is downstream of the update method, which is |
| 154 | + % |
| 155 | + % See also expUpdate, update |
| 156 | + updates = obj.SignalUpdates(1:obj.NumSignalUpdates); |
| 157 | + obj.NumSignalUpdates = 0; |
| 158 | + % fprintf('processing %i signal updates\n', length(updates)); |
| 159 | + for ui = 1:length(updates) |
| 160 | + signame = updates(ui).name; |
| 161 | + switch signame |
| 162 | + case 'events.trialNum' |
| 163 | + set(obj.TrialCountLabel, ... |
| 164 | + 'String', num2str(updates(ui).value)); |
| 165 | + otherwise |
| 166 | + % Check whether to display update using UpdatesFilter |
| 167 | + onList = any(ismember(signame, obj.UpdatesFilter)); |
| 168 | + if (obj.Exclude && ~onList) || (~obj.Exclude && onList) |
| 169 | + if ~isKey(obj.LabelsMap, signame) % If new update, add field |
| 170 | + obj.LabelsMap(signame) = obj.addInfoField(signame, ''); |
| 171 | + end |
| 172 | + str = toStr(updates(ui).value); % Convert the value to string |
| 173 | + set(obj.LabelsMap(signame), 'String', str, 'UserData', clock,... |
| 174 | + 'ForegroundColor', obj.RecentColour); % Update value |
| 175 | + end |
| 176 | + end |
| 177 | + end |
| 178 | + end |
| 179 | + |
| 180 | + function expUpdate(obj, rig, evt) |
| 181 | + % EXPUPDATE Callback to the remote rig ExpUpdate event |
| 182 | + % Processes a new experiment event. Signals events are added to the |
| 183 | + % SignalUpdates queue for processing by the processUpdates method. |
| 184 | + % |
| 185 | + % Inputs: |
| 186 | + % rig (srv.StimulusControl) : The source of the event |
| 187 | + % evt (srv.ExpEvent) : The experiment event object |
| 188 | + % |
| 189 | + % See also live, event, srv.StimulusControl, srv.ExpEvent |
| 190 | + if strcmp(evt.Name, 'signals') |
| 191 | + type = 'signals'; |
| 192 | + else |
| 193 | + type = evt.Data{1}; |
| 194 | + end |
| 195 | + switch type |
| 196 | + case 'signals' %queue signal updates |
| 197 | + updates = evt.Data; |
| 198 | + newNUpdates = obj.NumSignalUpdates + length(updates); |
| 199 | + if newNUpdates > length(obj.SignalUpdates) |
| 200 | + %grow message queue to accommodate |
| 201 | + obj.SignalUpdates(2*newNUpdates).value = []; |
| 202 | + end |
| 203 | + try |
| 204 | + obj.SignalUpdates(obj.NumSignalUpdates+1:newNUpdates) = updates; |
| 205 | + catch % see github.com/cortex-lab/Rigbox/issues/72 |
| 206 | + id = 'Rigbox:eui:SignalsExpPanel:signalsUpdateMismatch'; |
| 207 | + msg = 'Error caught in signals updates: length of updates = %g, length newNUpdates = %g'; |
| 208 | + warning(id, msg, length(updates), newNUpdates-(obj.NumSignalUpdates+1)) |
| 209 | + end |
| 210 | + obj.NumSignalUpdates = newNUpdates; |
| 211 | + case 'newTrial' |
| 212 | + cond = evt.Data{2}; %condition data for the new trial |
| 213 | + trialCount = obj.Block.numCompletedTrials; |
| 214 | + %add the trial condition to a new trial in the block |
| 215 | + obj.mergeTrialData(trialCount + 1, struct('condition', cond)); |
| 216 | + obj.newTrial(trialCount + 1, cond); |
| 217 | + case 'trialData' |
| 218 | + %a trial just completed |
| 219 | + data = evt.Data{2}; %the final data from that trial |
| 220 | + nTrials = obj.Block.numCompletedTrials + 1; |
| 221 | + obj.Block.numCompletedTrials = nTrials; %inc trial number in block |
| 222 | + %merge the new data with the rest of the trial data in the block |
| 223 | + obj.mergeTrialData(nTrials, data); |
| 224 | + obj.trialCompleted(nTrials, data); |
| 225 | + set(obj.TrialCountLabel, 'String', sprintf('%i', nTrials)); |
| 226 | + case 'event' |
| 227 | + % disp(evt.Data); |
| 228 | + obj.event(evt.Data{2}, evt.Data{3}); |
| 229 | + end |
| 230 | + end |
| 231 | + |
| 232 | + end |
| 233 | + |
| 234 | +end |
| 235 | + |
0 commit comments