diff --git a/README.md b/README.md index 253813b..c056f1b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# UKT - Neurofeedback +# NINFA Small Matlab Framework for running Custom Neurofeedback Protocols on LSL Streams @@ -14,12 +14,20 @@ Small Matlab Framework for running Custom Neurofeedback Protocols on LSL Streams ## Configuration -1. [Optional] Adjust `TYPE` of LSL input stream (default is for NIRS device) -2. Click `OPEN` to connect with LSL input stream -3. Configure `SETTINGS`, `ID` and `EPOCHS` -4. Click `START` to run a session +1. Select your device +2. [Optional] Adjust `TYPE` of LSL input stream +3. Click `OPEN` to connect with LSL input stream +4. Configure `SETTINGS`, `ID` and `EPOCHS` +5. Click `START` to run a session -![ukt-nf-settings](https://github.com/cyberjunk/ukt-nf/assets/780159/0690b24e-7a20-4357-bd0d-9171c880115d) +![ninfa](https://github.com/user-attachments/assets/7f8ba7ad-ea09-4d08-8384-57f182961430) + +### DEVICE + +* Select your device type and model from the available options. +* Add a device by creating a `.json` file for it in subfolder `devices` + * Take a look at the existing `nirs_nirx_nirsport2.json` + * The most important part is defining the LSL channels sent by the device ### LSL STREAM @@ -38,10 +46,10 @@ Small Matlab Framework for running Custom Neurofeedback Protocols on LSL Streams | Setting | Description | |----------------------|------------------------------------------------------------------------------------------------| -| `SELECTED CHANNELS` | Comma separated list of LSL input channel numbers to use (others are ignored). | +| `PROTOCOL` | The Matlab file from folder `protocols` with algorithm executed on each window. | +| `CHANNELS` | Select LSL channels to use in the selected protocol | | `WINDOW SIZE (S)` | Size of the sliding window in seconds. The window always contains last n seconds of samples. | | `SESSION LENGTH (S)` | The session will automatically stop after this time. | -| `PROTOCOL` | The Matlab file from folder `protocols` with algorithm executed on each window. | ### ID @@ -88,8 +96,9 @@ An epoch is a configurable timespan within a session. * A protocol calculates a feedback value from an input window * To add a protocol put the Matlab file in subfolder `protocols` -* See `example1.m` (returns a random feedback value) - +* The `Gauss.m` example requires a NIRS device that sends at least one `HbO` channel with `μmol/L` unit +* The `RecordOnly.m` works with any device type and model and just records data + ## Drift and Execution Times * `DRIFT` shows current offset in playback schedule (`where we are` vs. `where we should be`) diff --git a/components/devices.m b/components/devices.m new file mode 100644 index 0000000..9aa267b --- /dev/null +++ b/components/devices.m @@ -0,0 +1,66 @@ +classdef devices < handle + %Devices + % Detailed explanation goes here + + properties (Constant) + types = ["NIRS", "EEG"] + end + + properties + nirs struct = []; + eeg struct = []; + selected struct = struct([]); + end + + methods + function self = devices() + self.reload(); + end + + function reload(self) + files = ls("devices/*.json"); + for f = 1:size(files, 1) + file = files(f, 1:end); + json = jsondecode(fileread("./devices/" + file)); + switch json.type + case "NIRS" + idx = length(self.nirs) + 1; + self.nirs(idx).name = json.name; + self.nirs(idx).type = json.type; + self.nirs(idx).lsl = json.lsl; + case "EEG" + idx = length(self.eeg) + 1; + self.eeg(idx).name = json.name; + self.eeg(idx).type = json.type; + self.eeg(idx).lsl = json.lsl; + otherwise + disp("Ignoring unknown device type"); + end + end + end + + function r = select(self, type, name) + switch type + case "NIRS" + for d = 1:length(self.nirs) + if self.nirs(d).name == name + self.selected = self.nirs(d); + r = true; + return + end + end + case "EEG" + for d = 1:length(self.eeg) + if self.eeg(d).name == name + self.selected = self.eeg(d); + r = true; + return + end + end + otherwise + warning("Ignoring unknown device type: " + type); + end + r = false; + end + end +end diff --git a/components/protocols.m b/components/protocols.m new file mode 100644 index 0000000..b77c174 --- /dev/null +++ b/components/protocols.m @@ -0,0 +1,67 @@ +classdef protocols < handle + %Protocols + % Detailed explanation goes here + + properties (Constant) + end + + properties + list struct = [] + selected struct = struct([]); + end + + methods + function reload(self, device) + self.list = []; + self.selected = struct(); + files = ls("protocols/*.m"); + for f = 1:size(files, 1) + file = files(f, 1:end); + name = strtrim(erase(file, ".m")); + fh = feval(name); + req = fh.requires(); + if ~self.iscompatible(req, device) + continue + end + idx = length(self.list) + 1; + self.list(idx).name = name; + self.list(idx).fh = fh; + self.list(idx).req = req; + end + end + + function r = iscompatible(~, req, dev) + % check device type + if req.devicetype ~= "ANY" && req.devicetype ~= dev.type + r = false; + return + end + % check channel requirements + for idx = 1:length(req.channels) + found = 0; + for lslch = 1:length(dev.lsl.channels) + if req.channels(idx).type == dev.lsl.channels(lslch).type && ... + req.channels(idx).unit == dev.lsl.channels(lslch).unit + found = found + 1; + end + end + if found < req.channels(idx).min + r = false; + return + end + end + r = true; + end + + function r = select(self, name) + for p = 1:length(self.list) + if self.list(p).name == name + self.selected = self.list(p); + r = true; + return + end + end + r = false; + end + end +end diff --git a/components/session.m b/components/session.m index ca38d0b..16af9ca 100644 --- a/components/session.m +++ b/components/session.m @@ -14,12 +14,16 @@ protocolavg double = 0.0; % avg tracked protocol exec time protocolsum double = 0.0; % sum tracked protocol exec time srate double = 0.0; % sample rate + device struct = struct(); % device used in session channels uint32 = []; % channel numbers - data double = zeros(0,0); % session data + fn cell = []; % field names in data and window + data struct = struct(); % session data + datasize uint32 = 0; % rows count in data times double = zeros(0); % timestamps of sesssion data idx uint32 = 0; % current index in data and times firsttime double = 0.0; % first time - window double = zeros(0,0); % current window + window struct = struct(); % current window + windowsize uint32 = 0; % rows count in window windowtimes double = zeros(0); % current window times windowidx uint32 = 0; % current index in window windownum uint32 = 1; % current window num @@ -32,7 +36,6 @@ study string = ""; % name of study subject uint32 = 1; % subject numer run uint32 = 1; % run number - ploth matlab.ui.Figure; end events @@ -43,36 +46,56 @@ end methods + %% Return Channel Counts for each Type + function r = countChannelTypes(self) + r = struct(); + lslchannels = self.device.lsl.channels; + numlslchannels = length(lslchannels); + for ch = self.channels + type = "unknown"; + if ch <= numlslchannels + type = lslchannels(ch).type; + end + if ~isfield(r, type) + r.(type) = 0; + end + r.(type) = r.(type) + 1; + end + end + %% Start a new session function r = start(self, protocol, lengthmax, window, srate, ... - channels, markerinfo, study, subject, run) + device, channels, markerinfo, study, subject, run) if self.running r = false; return; end - if isvalid(self.ploth) - close(self.ploth); - end - numrows = ceil(srate*lengthmax); - numcols = length(channels); - numrowswnd = ceil(srate*window); + self.datasize = ceil(srate*lengthmax); + self.windowsize = ceil(srate*window); self.running = true; self.protocol = protocol; self.protocolmax = 0.0; self.protocolavg = 0.0; self.protocolsum = 0.0; - self.lengthmax = lengthmax; + self.lengthmax = lengthmax; self.srate = srate; + self.device = device; self.channels = channels; - self.data = zeros(numrows, numcols); - self.times = zeros(numrows, 1); - self.feedback = zeros(numrows, 1); + counts = self.countChannelTypes(); + self.data = struct(); + self.window = struct(); + self.fn = fieldnames(counts); + for k = 1:numel(self.fn) + self.data.(self.fn{k}) = zeros(self.datasize, counts.(self.fn{k})); + self.window.(self.fn{k}) = zeros(self.windowsize, counts.(self.fn{k})); + end + self.times = zeros(self.datasize, 1); + self.feedback = zeros(self.datasize, 1); self.idx = 0; - self.window = zeros(numrowswnd, numcols); - self.windowtimes = zeros(numrowswnd, 1); + self.windowtimes = zeros(self.windowsize, 1); self.windowidx = 0; self.windownum = 1; - self.markers = zeros(numrows, 1); + self.markers = zeros(self.datasize, 1); self.markerinfo = markerinfo; self.marker = 0; self.bgcolor = [0 0 0]; @@ -84,6 +107,7 @@ self.starttime = now(); r = true; notify(self, "Started"); + self.update() end %% Stop a running session @@ -100,7 +124,6 @@ r = true; notify(self, "Stopped"); self.save(); - self.plot(); end @@ -134,7 +157,7 @@ function update(self) if ~epochfound; self.marker = 0.0; end if epochold ~= self.marker; notify(self, "Epoch"); end %% stop session if required samples are recorded - if self.idx >= length(self.data) + if self.idx >= self.datasize self.stop(); end %% stop session if time is up @@ -150,28 +173,47 @@ function pushSample(self, sample, ts) if ~self.running return; end - %% save timestamp of first sample + %% increment index for sample + self.idx = self.idx + 1; + %% save timestamp if self.firsttime == 0 self.firsttime = ts; end - relts = ts - self.firsttime; - %% add sample to data - self.idx = self.idx + 1; - self.data(self.idx,:) = sample; - self.times(self.idx,:) = relts; - self.markers(self.idx,:) = self.marker; - %% add sample to window - if self.windowidx < length(self.window) + relts = ts - self.firsttime; + %% shift window + if self.windowidx < self.windowsize self.windowidx = self.windowidx + 1; else - self.window = circshift(self.window, -1); + for k = 1:numel(self.fn) + self.window.(self.fn{k}) = ... + circshift(self.window.(self.fn{k}), -1); + end self.windowtimes = circshift(self.windowtimes, -1); end - self.window(self.windowidx,:) = sample; self.windowtimes(self.windowidx,:) = relts; + self.times(self.idx,:) = relts; + self.markers(self.idx,:) = self.marker; + %% add new sample to data and window + colidx = struct(); + lslchannels = self.device.lsl.channels; + numlslchannels = length(lslchannels); + for i = 1:length(self.channels) + type = "unknown"; + val = sample(i); + ch = self.channels(i); + if ch <= numlslchannels + type = lslchannels(ch).type; + end + if ~isfield(colidx, type) + colidx.(type) = 1; + end + self.data.(type)(self.idx, colidx.(type)) = val; + self.window.(type)(self.windowidx, colidx.(type)) = val; + colidx.(type) = colidx.(type) + 1; + end %% raise window event notify(self, "Window"); - if self.windowidx >= length(self.window) + if self.windowidx >= self.windowsize self.windownum = self.windownum + 1; end end @@ -198,44 +240,30 @@ function save(self) export.subject = self.subject; export.run = self.run; % meta info + export.device = self.device; export.protocol = self.protocol; export.samplerate = self.srate; export.channels = self.channels; export.starttime = datetime(self.starttime,'ConvertFrom','datenum'); export.stoptime = datetime(self.stoptime,'ConvertFrom','datenum'); export.duration = self.length; - export.windowsamples = length(self.window); + export.windowsamples = self.windowsize; % data + for k = 1:numel(self.fn) + export.data.(self.fn{k}) = self.data.(self.fn{k})(1:usedrows,:); + end export.times = self.times(1:usedrows,:); - export.data = self.data(1:usedrows,:); export.feedback = self.feedback(1:usedrows,:); export.marker = self.markers(1:usedrows,:); % export studyname = "unnamed"; - if self.study ~= ""; studyname = self.study; end + if self.study ~= ""; studyname = self.study; end filename = ... studyname + "-" + ... sprintf('%03d', self.subject) + "-" + ... - sprintf('%02d', self.run); + sprintf('%02d', self.run); save("./sessions/" + filename + ".mat", '-struct','export'); end - - function plot(self) - self.ploth = figure('Name', 'Session Plot'); - self.ploth.NumberTitle = 'off'; - nchannels = length(self.channels); - for i = (1:nchannels) - subplot(nchannels+2,1,i); - plot(self.data(:,i)); - title('Channel ' + string(self.channels(i))); - end - subplot(nchannels+2,1,nchannels+1); - plot(self.feedback(:,1)); - title('Feedback'); - subplot(nchannels+2,1,nchannels+2); - plot(self.markers(:,1)); - title('Marker'); - end end end diff --git a/devices/eeg_generic.json b/devices/eeg_generic.json new file mode 100644 index 0000000..e30301c --- /dev/null +++ b/devices/eeg_generic.json @@ -0,0 +1,8 @@ +{ + "name": "Generic", + "type": "EEG", + "lsl": { + "type": "", + "channels": [] + } +} diff --git a/devices/nirs_generic.json b/devices/nirs_generic.json new file mode 100644 index 0000000..d27271d --- /dev/null +++ b/devices/nirs_generic.json @@ -0,0 +1,8 @@ +{ + "name": "Generic", + "type": "NIRS", + "lsl": { + "type": "", + "channels": [] + } +} diff --git a/devices/nirs_nirx_nirsport2.json b/devices/nirs_nirx_nirsport2.json new file mode 100644 index 0000000..1d69478 --- /dev/null +++ b/devices/nirs_nirx_nirsport2.json @@ -0,0 +1,90 @@ +{ + "name": "nirX NIRSport2", + "type": "NIRS", + "lsl": { + "type": "NIRS", + "channels": [ + { "devch": 0, "type": "COUNTER", "unit": "" }, + { "devch": 1, "type": "WL760NM", "unit": "V" }, + { "devch": 2, "type": "WL760NM", "unit": "V" }, + { "devch": 3, "type": "WL760NM", "unit": "V" }, + { "devch": 4, "type": "WL760NM", "unit": "V" }, + { "devch": 5, "type": "WL760NM", "unit": "V" }, + { "devch": 6, "type": "WL760NM", "unit": "V" }, + { "devch": 7, "type": "WL760NM", "unit": "V" }, + { "devch": 8, "type": "WL760NM", "unit": "V" }, + { "devch": 9, "type": "WL760NM", "unit": "V" }, + { "devch": 10, "type": "WL760NM", "unit": "V" }, + { "devch": 11, "type": "WL760NM", "unit": "V" }, + { "devch": 12, "type": "WL760NM", "unit": "V" }, + { "devch": 13, "type": "WL760NM", "unit": "V" }, + { "devch": 14, "type": "WL760NM", "unit": "V" }, + { "devch": 15, "type": "WL760NM", "unit": "V" }, + { "devch": 16, "type": "WL760NM", "unit": "V" }, + { "devch": 17, "type": "WL760NM", "unit": "V" }, + { "devch": 18, "type": "WL760NM", "unit": "V" }, + { "devch": 19, "type": "WL760NM", "unit": "V" }, + { "devch": 20, "type": "WL760NM", "unit": "V" }, + { "devch": 1, "type": "WL850NM", "unit": "V" }, + { "devch": 2, "type": "WL850NM", "unit": "V" }, + { "devch": 3, "type": "WL850NM", "unit": "V" }, + { "devch": 4, "type": "WL850NM", "unit": "V" }, + { "devch": 5, "type": "WL850NM", "unit": "V" }, + { "devch": 6, "type": "WL850NM", "unit": "V" }, + { "devch": 7, "type": "WL850NM", "unit": "V" }, + { "devch": 8, "type": "WL850NM", "unit": "V" }, + { "devch": 9, "type": "WL850NM", "unit": "V" }, + { "devch": 10, "type": "WL850NM", "unit": "V" }, + { "devch": 11, "type": "WL850NM", "unit": "V" }, + { "devch": 12, "type": "WL850NM", "unit": "V" }, + { "devch": 13, "type": "WL850NM", "unit": "V" }, + { "devch": 14, "type": "WL850NM", "unit": "V" }, + { "devch": 15, "type": "WL850NM", "unit": "V" }, + { "devch": 16, "type": "WL850NM", "unit": "V" }, + { "devch": 17, "type": "WL850NM", "unit": "V" }, + { "devch": 18, "type": "WL850NM", "unit": "V" }, + { "devch": 19, "type": "WL850NM", "unit": "V" }, + { "devch": 20, "type": "WL850NM", "unit": "V" }, + { "devch": 1, "type": "HbO", "unit": "μmol/L" }, + { "devch": 2, "type": "HbO", "unit": "μmol/L" }, + { "devch": 3, "type": "HbO", "unit": "μmol/L" }, + { "devch": 4, "type": "HbO", "unit": "μmol/L" }, + { "devch": 5, "type": "HbO", "unit": "μmol/L" }, + { "devch": 6, "type": "HbO", "unit": "μmol/L" }, + { "devch": 7, "type": "HbO", "unit": "μmol/L" }, + { "devch": 8, "type": "HbO", "unit": "μmol/L" }, + { "devch": 9, "type": "HbO", "unit": "μmol/L" }, + { "devch": 10, "type": "HbO", "unit": "μmol/L" }, + { "devch": 11, "type": "HbO", "unit": "μmol/L" }, + { "devch": 12, "type": "HbO", "unit": "μmol/L" }, + { "devch": 13, "type": "HbO", "unit": "μmol/L" }, + { "devch": 14, "type": "HbO", "unit": "μmol/L" }, + { "devch": 15, "type": "HbO", "unit": "μmol/L" }, + { "devch": 16, "type": "HbO", "unit": "μmol/L" }, + { "devch": 17, "type": "HbO", "unit": "μmol/L" }, + { "devch": 18, "type": "HbO", "unit": "μmol/L" }, + { "devch": 19, "type": "HbO", "unit": "μmol/L" }, + { "devch": 20, "type": "HbO", "unit": "μmol/L" }, + { "devch": 1, "type": "HbR", "unit": "μmol/L" }, + { "devch": 2, "type": "HbR", "unit": "μmol/L" }, + { "devch": 3, "type": "HbR", "unit": "μmol/L" }, + { "devch": 4, "type": "HbR", "unit": "μmol/L" }, + { "devch": 5, "type": "HbR", "unit": "μmol/L" }, + { "devch": 6, "type": "HbR", "unit": "μmol/L" }, + { "devch": 7, "type": "HbR", "unit": "μmol/L" }, + { "devch": 8, "type": "HbR", "unit": "μmol/L" }, + { "devch": 9, "type": "HbR", "unit": "μmol/L" }, + { "devch": 10, "type": "HbR", "unit": "μmol/L" }, + { "devch": 11, "type": "HbR", "unit": "μmol/L" }, + { "devch": 12, "type": "HbR", "unit": "μmol/L" }, + { "devch": 13, "type": "HbR", "unit": "μmol/L" }, + { "devch": 14, "type": "HbR", "unit": "μmol/L" }, + { "devch": 15, "type": "HbR", "unit": "μmol/L" }, + { "devch": 16, "type": "HbR", "unit": "μmol/L" }, + { "devch": 17, "type": "HbR", "unit": "μmol/L" }, + { "devch": 18, "type": "HbR", "unit": "μmol/L" }, + { "devch": 19, "type": "HbR", "unit": "μmol/L" }, + { "devch": 20, "type": "HbR", "unit": "μmol/L" } + ] + } +} diff --git a/main.m b/main.m index 4832b5d..ee296be 100644 --- a/main.m +++ b/main.m @@ -19,14 +19,20 @@ % globals global mylsl; global mysession; +global mydevices; +global myprotocols; +global myselectchannels; global mysettings; global myfeedback; % init globals -mylsl = lsl(); -mysession = session(); -mysettings = app(); -myfeedback = feedback(); +mylsl = lsl(); +mysession = session(); +mydevices = devices(); +myprotocols = protocols(); +myselectchannels = selectchannels(); +mysettings = app(); +myfeedback = feedback(); % add listeners to lsl lhsample = addlistener(mylsl, "NewSample", @onNewSample); @@ -72,21 +78,25 @@ function onNewSample(src, ~) mysession.update(); end -function onSessionStarted(~, ~) +function onSessionStarted(src, ~) global mylsl; global myfeedback; + global myprotocols; mylsl.marker = 0; mylsl.trigger(100); myfeedback.showBar(); + myprotocols.selected.fh.init(); end function onSessionStopped(src, ~) global mylsl; global myfeedback; + global myprotocols; mylsl.marker = 0; mylsl.trigger(101); myfeedback.setBackground(src.bgcolor); myfeedback.hideBar(); + myprotocols.selected.fh.finish(src); end function onSessionEpoch(src, ~) @@ -103,21 +113,29 @@ function onSessionEpoch(src, ~) function onSessionWindow(src, ~) global myfeedback; + global myprotocols; prevfeedback = 0.5; if src.idx > 1 prevfeedback = src.feedback(src.idx-1); end + prevmarker = 0; + if src.idx > 1 + prevmarker = src.markers(src.idx-1); + end + tick = tic(); - r = feval (src.protocol, ... + r = myprotocols.selected.fh.process (... src.marker, src.srate, ... - src.idx, src.data(src.idx,:), ... + src.idx, src.data, ... src.windownum, src.window, ... - src.windowidx >= length(src.window), ... - prevfeedback); + src.windowidx >= src.windowsize, ... + prevfeedback, prevmarker); span = toc(tick); + r = min(max(r,0.0),1.0); + myfeedback.setFeedback(r); src.pushFeedback(r, span); end diff --git a/protocols/Gauss.m b/protocols/Gauss.m new file mode 100644 index 0000000..62f1622 --- /dev/null +++ b/protocols/Gauss.m @@ -0,0 +1,189 @@ +function fh = Gauss + fh.requires = @requires; + fh.init = @init; + fh.process = @process; + fh.finish = @finish; +end + +% REQUIREMENTS FOR PROTOCOL +function r = requires() + r.devicetype = "NIRS"; + % required window min and max durations + r.window.mins = 1.0; + r.window.maxs = 10.0; + % requires at least one HbO channel + r.channels(1).type = "HbO"; + r.channels(1).unit = "μmol/L"; + r.channels(1).min = 1; + r.channels(1).max = 64; + % HbR is optional + r.channels(2).type = "HbR"; + r.channels(2).unit = "μmol/L"; + r.channels(2).min = 0; + r.channels(2).max = 64; +end + +% EXECUTED ONCE ON START +function init() + global Filter + ordine = 15; + cutoff = 0.022; + Filter = gaussfir(cutoff, ordine); +end + +% EXECUTED FOR EACH SLIDING WINDOW +function r = process(... + marker, samplerate, samplenum, data, ... + windownum, window, isfullwindow, ... + prevfeedback, prevmarker) + + % IMPORTANT: + % Your algorithm must take less than (1/samplerate) seconds + % in average or else you fall behind schedule and get a drift. + % If you're algorithm requires more time than that then + % run your calculation on every n-th window only and + % repeat your previous feedback for all other windows. + global CounterRS + global DataRS + global RestValue + global Correction + global Filter + + % CONSTANTS + EXPECTED_AMPLITUDE = 0.1; + EXPECTED_MIN_DIFF = -0.4; + EXPECTED_MAX_DIFF = 0.4; + + r = 0.5; % default return + tick = tic(); % start time of execution + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + if marker == 2 + %% RESTING PHASE + + % reset on switch + if prevmarker ~= 2 + CounterRS = 0; + DataRS = []; + end + + % saving the HbO values of the last sample + CounterRS = CounterRS + 1; + DataRS(CounterRS,:) = window.HbO(end,:); + %disp(CounterRS) + + % 5 frames before 30 seconds of rest (to avoid final delays) + if CounterRS == floor(samplerate*30)-5 + %% CALCULATE CORRECTION FACTOR USING AMPLITUDE + % (1) Extract last ~15s of HbO channels of resting phase + % (2) Filter each HbO channel + % (3) Create average HbO channel from all filtered HbO channels + % (4) Sort average HbO channel + % (5) Calculate amplitude using mean of highest and lowest + filtered = DataRS(floor(samplerate*15):end,:); + for ch = 1:size(filtered,2) + filtered(:,ch) = conv(filtered(:,ch), Filter, 'same'); + end + mean_hbo = mean(filtered,2); + mean_hbo = sort(mean_hbo); + mean_top25 = mean(mean_hbo(end-35:end-10)); + mean_low25 = mean(mean_hbo(10:35)); + amplitude = abs(mean_top25 - mean_low25); + Correction = EXPECTED_AMPLITUDE / amplitude; + %disp("Amplitude: " + sprintf('%.3f', amplitude)); + %disp("Correction: " + sprintf('%.3f', Correction)); + + %% AVERAGE OF HBO OF LAST ~5S OF RESTING PHASE + DataFilt = DataRS(floor(samplerate*25):end,:); + for ch = 1:size(DataFilt,2) + DataFilt(:,ch) = conv(DataFilt(:,ch), Filter, 'same'); + end + RestValue = mean(mean(DataFilt,2)); + %disp("Rest Average: " + sprintf('%.3f', RestValue)); + end + + elseif marker == 3 + %% CONCENTRATION PHASE + + % filter each HbO channel in current sliding window + for ch = 1:size(window.HbO,2) + DataFilt(:,ch) = conv(window.HbO(:,ch), Filter, 'same'); + end + + % calculate mean HbO channel and mean HbO over time + mean_hbo = mean(mean(DataFilt,1)); + + % feedback is difference in HbO scaled by correction + feedback = mean_hbo - RestValue; + feedback = feedback * Correction; + + % convert from expected range to [0,1] using + % r = (((X-a)*(d-c)) / (b-a)) + c + % (a, b) = initial interval + % (c, d) = final interval + r = (((feedback-EXPECTED_MIN_DIFF)*(1.0-0.0)) / ... + (EXPECTED_MAX_DIFF-EXPECTED_MIN_DIFF)) + 0.0; + end + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + % time spent + span = toc(tick); + + % create debug output + output = ... + "| sample=" + sprintf('%05d', samplenum) + " " + ... + "| window=" + sprintf('%05d', windownum) + " " + ... + "| marker=" + sprintf('%02d', marker) + " " + ... + "| duration=" + sprintf('%.3f', span)+"s" + " " + ... + "| feedback=" + sprintf('%.3f', r) + " "; + + % add values for marker=3 + if marker == 3 + output = output + ... + "| restavg=" + sprintf('%.3f', RestValue) + " " + ... + "| wndavg=" + sprintf('%.3f', mean_hbo) + " " + ... + "| correction=" + sprintf('%.3f', Correction) + " "; + end + + % show debug output + disp(output + "|"); +end + +% EXECUTED AT THE END OF THE SESSION +function finish(session) + ploth = figure('Name', 'Session Plot'); + ploth.NumberTitle = 'off'; + + nplot = 1; % Current one + nplots = 3; % HbO, Feedback, Marker + if isfield(session.data, "HbR") + nplots = 4; % + HbR + end + + % Plotting unfiltered HbO mean channel + subplot(nplots,1,nplot); + plot(mean(session.data.HbO,2),'r'); + title('HbO [μmol/L]'); + + % Plotting unfiltered HbR mean channel (optional) + if isfield(session.data, "HbR") + nplot = nplot + 1; + subplot(nplots,1,nplot); + plot(mean(session.data.HbR,2),'b'); + title('HbR [μmol/L]'); + end + + % Plotting Feedback values + nplot = nplot + 1; + subplot(nplots,1,nplot); + plot(session.feedback(:,1)); + title('Feedback'); + + % Plotting Marker Values + nplot = nplot + 1; + subplot(nplots,1,nplot); + plot(session.markers(:,1)); + title('Marker'); +end diff --git a/protocols/RecordOnly.m b/protocols/RecordOnly.m new file mode 100644 index 0000000..7116cbe --- /dev/null +++ b/protocols/RecordOnly.m @@ -0,0 +1,52 @@ +function fh = RecordOnly + fh.requires = @requires; + fh.init = @init; + fh.process = @process; + fh.finish = @finish; +end + +% REQUIREMENTS FOR PROTOCOL +function r = requires() + r.devicetype = "ANY"; + r.window.mins = 1.0; + r.window.maxs = 300.0; + r.channels = struct([]); +end + +% EXECUTED ONCE ON START +function init() +end + +% EXECUTED FOR EACH SLIDING WINDOW +function r = process(~, ~, ~, ~, ~, ~, ~, ~, ~) + r = 0.0; +end + +% EXECUTED AT THE END OF THE SESSION +function finish(session) + ploth = figure('Name', 'Session Plot'); + ploth.NumberTitle = 'off'; + nchannels = length(session.channels); + nplots = nchannels+2; + + iplots = 1; + fn = fieldnames(session.data); %TODO: Cache this + for k = 1:numel(fn) + for i = 1:size(session.data.(fn{k}), 2) + subplot(nplots,1,iplots); + plot(session.data.(fn{k})(:,i)); + title('Channel'); + iplots = iplots + 1; + end + + end + + % Plotting Feedback values + subplot(nplots,1,nchannels+1); + plot(session.feedback(:,1)); + title('Feedback'); + % Plotting Marker Values + subplot(nplots,1,nchannels+2); + plot(session.markers(:,1)); + title('Marker'); +end diff --git a/protocols/example1.m b/protocols/example1.m deleted file mode 100644 index 91825b8..0000000 --- a/protocols/example1.m +++ /dev/null @@ -1,57 +0,0 @@ -% Example Algorithm 1 for Demonstration -% marker: current epoch marker -% samplerate: sample rate -% samplenum: current sample number -% sample: current sample values -% windownum: current window number -% window: current window values -% isfullwindow: true once first window is filled -% prevfeedback: previous feedback -% RETURN: normalized value between 0.0 (min) and 1.0 (max) - -function r = example1(... - marker, samplerate, samplenum, sample, ... - windownum, window, isfullwindow, prevfeedback) - - % IMPORTANT: - % Your algorithm must take less than (1/samplerate) seconds - % in average or else you fall behind schedule and get a drift. - % If you're algorithm requires more time than that then - % run your calculation on every n-th window only and - % repeat your previous feedback for all other windows. - - n = 1; - tick = tic(); - - % custom feedback range - minfb = -100; - maxfb = 100; - - % return 0.5 until first full window - if ~isfullwindow - r = 0.5; - - % calculate on every n-th window - elseif mod(windownum, n) == 0 - % simulate 80% computation time (80ms of 100ms for 10Hz) - pause((1.0/double(samplerate))*0.8); - % create dummy feedback value in [minfb, maxfb] - r = double(randi([minfb,maxfb])); - % map from [minfb, maxfb] to [0, 1] - r = (r-minfb) * (1.0/(maxfb-minfb)); - - % skip this sample/window - else - r = prevfeedback; - end - - % time spent - span = toc(tick); - - % debug - disp("Processed sample " + samplenum + ... - " (window=" + windownum + ... - ", marker=" + marker + ... - ", duration=" + sprintf('%.3f', span) + "s" + ... - ", fb=" + sprintf('%.3f', r) + ")"); -end diff --git a/settings/Gauss.mat b/settings/Gauss.mat new file mode 100644 index 0000000..6433732 Binary files /dev/null and b/settings/Gauss.mat differ diff --git a/ui/app.mlapp b/ui/app.mlapp index 972f3fa..1f32e22 100644 Binary files a/ui/app.mlapp and b/ui/app.mlapp differ diff --git a/ui/app_exported.m b/ui/app_exported.m index dd08084..5be1d43 100644 --- a/ui/app_exported.m +++ b/ui/app_exported.m @@ -2,63 +2,69 @@ % Properties that correspond to app components properties (Access = public) - UIFigure matlab.ui.Figure - FileMenu matlab.ui.container.Menu - LoadMenu matlab.ui.container.Menu - SaveMenu matlab.ui.container.Menu - LSLSTREAMPanel matlab.ui.container.Panel - GridLayout matlab.ui.container.GridLayout - TYPELabel matlab.ui.control.Label - TYPEEditField matlab.ui.control.EditField - OPENButton matlab.ui.control.Button - CHANNELSLabel_2 matlab.ui.control.Label - CHANNELSFOUNDLabel matlab.ui.control.Label - SAMPLERATEDescLabel matlab.ui.control.Label - SAMPLERATELabel matlab.ui.control.Label - SETTINGSPanel matlab.ui.container.Panel - GridLayout2 matlab.ui.container.GridLayout - SELECTEDCHANNELSEditFieldLabel matlab.ui.control.Label - SELECTEDCHANNELSEditField matlab.ui.control.EditField - WINDOWSIZESEditFieldLabel matlab.ui.control.Label - WINDOWSIZESEditField matlab.ui.control.NumericEditField - SESSIONLENGTHSEditFieldLabel matlab.ui.control.Label - SESSIONLENGTHSEditField matlab.ui.control.NumericEditField - PROTOCOLLabel matlab.ui.control.Label - PROTOCOLDropDown matlab.ui.control.DropDown - STARTButton matlab.ui.control.Button - SESSIONINFOPanel matlab.ui.container.Panel - GridLayout3 matlab.ui.container.GridLayout - SESSIONSTARTEDDescLabel matlab.ui.control.Label - SESSIONSTARTEDLabel matlab.ui.control.Label - SESSIONENDEDDescLabel matlab.ui.control.Label - SESSIONENDEDLabel matlab.ui.control.Label - SESSIONLENGTHSDescLabel matlab.ui.control.Label - SESSIONLENGTHLabel matlab.ui.control.Label - SESSIONSAMPLESLabel matlab.ui.control.Label - SESSIONDRIFTLabel matlab.ui.control.Label - SESSIONSAMPLESDescLabel matlab.ui.control.Label - SESSIONDRIFTDescLabel matlab.ui.control.Label - SESSSIONSTATUSDescLabel matlab.ui.control.Label - SESSIONSTATUSLabel matlab.ui.control.Label - EPOCHSPanel matlab.ui.container.Panel - MARKERTable matlab.ui.control.Table - MARKERAddButton matlab.ui.control.Button - MARKERDelButton matlab.ui.control.Button - COLORButton matlab.ui.control.Button - IDPanel matlab.ui.container.Panel - GridLayout4 matlab.ui.container.GridLayout - SUBJECTEditFieldLabel matlab.ui.control.Label - SUBJECTEditField matlab.ui.control.NumericEditField - RUNEditFieldLabel matlab.ui.control.Label - RUNEditField matlab.ui.control.NumericEditField - STUDYEditFieldLabel matlab.ui.control.Label - STUDYEditField matlab.ui.control.EditField - PROTOCOLTIMEPanel matlab.ui.container.Panel - GridLayout5 matlab.ui.container.GridLayout - PROTOCOLMaxLabel matlab.ui.control.Label - PROTOCOLMaxDescLabel matlab.ui.control.Label - PROTOCOLAvgLabel matlab.ui.control.Label - PROTOCOLAvgDescLabel matlab.ui.control.Label + UIFigure matlab.ui.Figure + FileMenu matlab.ui.container.Menu + LoadMenu matlab.ui.container.Menu + SaveMenu matlab.ui.container.Menu + LSLSTREAMPanel matlab.ui.container.Panel + GridLayout matlab.ui.container.GridLayout + TYPELabel matlab.ui.control.Label + TYPEEditField matlab.ui.control.EditField + OPENButton matlab.ui.control.Button + CHANNELSLabel_2 matlab.ui.control.Label + CHANNELSFOUNDLabel matlab.ui.control.Label + SAMPLERATEDescLabel matlab.ui.control.Label + SAMPLERATELabel matlab.ui.control.Label + SETTINGSPanel matlab.ui.container.Panel + GridLayout2 matlab.ui.container.GridLayout + WINDOWSIZESEditFieldLabel matlab.ui.control.Label + WINDOWSIZESEditField matlab.ui.control.NumericEditField + SESSIONLENGTHSEditFieldLabel matlab.ui.control.Label + SESSIONLENGTHSEditField matlab.ui.control.NumericEditField + PROTOCOLLabel matlab.ui.control.Label + PROTOCOLDropDown matlab.ui.control.DropDown + CHANNELSButton matlab.ui.control.Button + CHANNELSLabel_3 matlab.ui.control.Label + STARTButton matlab.ui.control.Button + SESSIONINFOPanel matlab.ui.container.Panel + GridLayout3 matlab.ui.container.GridLayout + SESSIONSTARTEDDescLabel matlab.ui.control.Label + SESSIONSTARTEDLabel matlab.ui.control.Label + SESSIONENDEDDescLabel matlab.ui.control.Label + SESSIONENDEDLabel matlab.ui.control.Label + SESSIONLENGTHSDescLabel matlab.ui.control.Label + SESSIONLENGTHLabel matlab.ui.control.Label + SESSIONSAMPLESLabel matlab.ui.control.Label + SESSIONDRIFTLabel matlab.ui.control.Label + SESSIONSAMPLESDescLabel matlab.ui.control.Label + SESSIONDRIFTDescLabel matlab.ui.control.Label + SESSSIONSTATUSDescLabel matlab.ui.control.Label + SESSIONSTATUSLabel matlab.ui.control.Label + EPOCHSPanel matlab.ui.container.Panel + MARKERTable matlab.ui.control.Table + MARKERAddButton matlab.ui.control.Button + MARKERDelButton matlab.ui.control.Button + COLORButton matlab.ui.control.Button + IDPanel matlab.ui.container.Panel + GridLayout4 matlab.ui.container.GridLayout + SUBJECTEditFieldLabel matlab.ui.control.Label + SUBJECTEditField matlab.ui.control.NumericEditField + RUNEditFieldLabel matlab.ui.control.Label + RUNEditField matlab.ui.control.NumericEditField + STUDYEditFieldLabel matlab.ui.control.Label + STUDYEditField matlab.ui.control.EditField + PROTOCOLTIMEPanel matlab.ui.container.Panel + GridLayout5 matlab.ui.container.GridLayout + PROTOCOLMaxLabel matlab.ui.control.Label + PROTOCOLMaxDescLabel matlab.ui.control.Label + PROTOCOLAvgLabel matlab.ui.control.Label + PROTOCOLAvgDescLabel matlab.ui.control.Label + DEVICEPanel matlab.ui.container.Panel + GridLayout6 matlab.ui.container.GridLayout + MODELDropDownLabel matlab.ui.control.Label + DEVICEDropDown matlab.ui.control.DropDown + TYPEDropDownLabel matlab.ui.control.Label + TYPEDropDown matlab.ui.control.DropDown end @@ -77,10 +83,10 @@ function onSessionStarted(app, src, ~) app.STARTButton.Text = "STOP"; app.LoadMenu.Enable = false; app.SaveMenu.Enable = false; - app.SELECTEDCHANNELSEditField.Enable = false; app.WINDOWSIZESEditField.Enable = false; app.SESSIONLENGTHSEditField.Enable = false; app.PROTOCOLDropDown.Enable = false; + app.CHANNELSButton.Enable = false; app.SUBJECTEditField.Enable = false; app.RUNEditField.Enable = false; app.STUDYEditField.Enable = false; @@ -99,10 +105,10 @@ function onSessionStopped(app, src, ~) app.STARTButton.Text = "START"; app.LoadMenu.Enable = true; app.SaveMenu.Enable = true; - app.SELECTEDCHANNELSEditField.Enable = true; app.WINDOWSIZESEditField.Enable = true; app.SESSIONLENGTHSEditField.Enable = true; app.PROTOCOLDropDown.Enable = true; + app.CHANNELSButton.Enable = true; app.SUBJECTEditField.Enable = true; app.RUNEditField.Enable = true; app.STUDYEditField.Enable = true; @@ -119,6 +125,9 @@ function onSessionStopped(app, src, ~) app.updateStatus(); end + function onChannelsSelected(app, ~, ~) + app.updateStartButton(); + end end methods (Access = public) @@ -147,7 +156,7 @@ function updateStatus(app) sprintf('%.2f', mysession.length) + " s"; app.SESSIONSAMPLESLabel.Text = ... string(mysession.idx) + "/" + ... - string(length(mysession.data)); + string(mysession.datasize); app.SESSIONDRIFTLabel.Text = ... sprintf('%.2f', drift) + " s"; if drift > 1.0 || drift < -1.0 @@ -165,7 +174,7 @@ function updateStatus(app) app.PROTOCOLAvgLabel.BackgroundColor = 'green'; end end - + function update(app) global mylsl global mysession @@ -189,6 +198,76 @@ function update(app) app.updateStatus(); end end + + function updateStartButton(app) + global myselectchannels + global mylsl + if ~mylsl.streaming + app.STARTButton.Text = "CONNECT LSL FIRST"; + app.STARTButton.Enable = false; + elseif ~myselectchannels.isok + app.STARTButton.Text = "SELECT CHANNELS FIRST"; + app.STARTButton.Enable = false; + else + app.STARTButton.Text = "START"; + app.STARTButton.Enable = true; + end + end + + function updateDevices(app) + global mydevices; + app.DEVICEDropDown.Items = {}; + type = lower(app.TYPEDropDown.Value); + for idx = 1:length(mydevices.(type)) + app.DEVICEDropDown.Items(idx) = ... + cellstr(mydevices.(type)(idx).name); + end + end + + function useDevice(app) + global mydevices; + global myprotocols; + type = convertCharsToStrings(app.TYPEDropDown.Value); + name = convertCharsToStrings(app.DEVICEDropDown.Value); + if mydevices.select(type, name) + disp("SELECTED DEVICE: " + mydevices.selected.name + ... + "(" + mydevices.selected.type + ")"); + app.TYPEEditField.Value = mydevices.selected.lsl.type; + myprotocols.reload(mydevices.selected); + app.PROTOCOLDropDown.Items = {}; + for idx = 1:length(myprotocols.list) + app.PROTOCOLDropDown.Items(idx) = ... + cellstr(myprotocols.list(idx).name); + end + app.useProtocol(); + end + end + + function useProtocol(app) + global myprotocols; + global myselectchannels; + name = convertCharsToStrings(app.PROTOCOLDropDown.Value); + if myprotocols.select(name) + disp("SELECTED PROTOCOL: " + myprotocols.selected.name) + myselectchannels.selected = []; + myselectchannels.initRequired(); + myselectchannels.initSelected(); + end + app.checkWindowSize(); + app.updateStartButton(); + end + + function checkWindowSize(app) + global myprotocols + v = app.WINDOWSIZESEditField.Value; + if ~isempty(myprotocols.selected) + r = myprotocols.selected.fh.requires(); + v = min(max(v, r.window.mins), r.window.maxs); + end + if app.WINDOWSIZESEditField.Value ~= v + app.WINDOWSIZESEditField.Value = v; + end + end end @@ -198,10 +277,16 @@ function update(app) % Code that executes after component creation function startupFcn(app) global mysession; - app.UIFigure.Name = "Settings"; + global myselectchannels; + app.UIFigure.Name = "NINFA v1.1.0"; addlistener(mysession, "Started", @app.onSessionStarted); addlistener(mysession, "Stopped", @app.onSessionStopped); - app.PROTOCOLDropDown.Items = string(ls("protocols/*.m")); + addlistener(myselectchannels, "Done", @app.onChannelsSelected); + for idx = 1:length(devices.types) + app.TYPEDropDown.Items(idx) = cellstr(devices.types(idx)); + end + app.updateDevices(); + app.useDevice(); app.MARKERTable.SelectionType = 'row'; app.MARKERTable.ColumnFormat = { 'short', 'short', 'short', 'logical', 'short', 'short', 'short' }; @@ -210,27 +295,29 @@ function startupFcn(app) % Button pushed function: OPENButton function OPENButtonPushed(app, event) global mylsl + global myselectchannels if ~mylsl.streaming r = mylsl.open(app.TYPEEditField.Value); if r app.tick = tic(); app.OPENButton.Text = "CLOSE"; + app.TYPEDropDown.Enable = false; + app.DEVICEDropDown.Enable = false; app.TYPEEditField.Enable = false; app.CHANNELSFOUNDLabel.Text = int2str(mylsl.lslchannels); - app.SELECTEDCHANNELSEditField.Enable = true; app.WINDOWSIZESEditField.Enable = true; app.SESSIONLENGTHSEditField.Enable = true; app.PROTOCOLDropDown.Enable = true; app.SUBJECTEditField.Enable = true; app.RUNEditField.Enable = true; - app.STUDYEditField.Enable = true; + app.STUDYEditField.Enable = true; + app.CHANNELSButton.Enable = true; app.MARKERTable.Enable = 'on'; app.MARKERAddButton.Enable = true; if size(app.MARKERTable.Data,1) > 0 app.MARKERDelButton.Enable = true; app.COLORButton.Enable = true; end - app.STARTButton.Enable = true; else msgbox("No LSL stream with type '" + ... app.TYPEEditField.Value + ... @@ -238,33 +325,38 @@ function OPENButtonPushed(app, event) end else mylsl.close(); + myselectchannels.close(); app.OPENButton.Text = "OPEN"; app.CHANNELSFOUNDLabel.Text = "-"; app.SAMPLERATELabel.Text = "-"; + app.TYPEDropDown.Enable = true; + app.DEVICEDropDown.Enable = true; app.TYPEEditField.Enable = true; - app.SELECTEDCHANNELSEditField.Enable = false; app.WINDOWSIZESEditField.Enable = false; app.SESSIONLENGTHSEditField.Enable = false; app.PROTOCOLDropDown.Enable = false; app.SUBJECTEditField.Enable = false; app.RUNEditField.Enable = false; app.STUDYEditField.Enable = false; + app.CHANNELSButton.Enable = false; app.MARKERTable.Enable = 'off'; app.MARKERAddButton.Enable = false; app.MARKERDelButton.Enable = false; app.COLORButton.Enable = false; - app.STARTButton.Enable = false; app.SAMPLERATELabel.BackgroundColor = 'none'; end + app.updateStartButton(); end % Button pushed function: STARTButton function STARTButtonPushed(app, event) global mylsl; global mysession; + global myselectchannels; + global mydevices; if ~mysession.running - strchannels = split(app.SELECTEDCHANNELSEditField.Value, ','); - channels = transpose(str2double(strchannels)); + channels = myselectchannels.selected; + device = mydevices.selected; srate = mylsl.sratenom; % prefer claimed samplerate if srate <= 0, srate = mylsl.srate; end % else use measured blocksize = app.WINDOWSIZESEditField.Value * srate; @@ -274,6 +366,7 @@ function STARTButtonPushed(app, event) app.SESSIONLENGTHSEditField.Value, ... app.WINDOWSIZESEditField.Value, ... srate, ... + device, ... channels, ... app.MARKERTable.Data, ... app.STUDYEditField.Value, ... @@ -285,26 +378,6 @@ function STARTButtonPushed(app, event) end - % Value changed function: SELECTEDCHANNELSEditField - function SELECTEDCHANNELSEditFieldValueChanged(app, event) - global mylsl; - newvalue = ""; - values = split(app.SELECTEDCHANNELSEditField.Value, ','); - for k = 1:length(values) - v = values{k}; - v = strip(v, ' '); - [x, s] = str2num(v); - if s && x > 0 && x <= mylsl.lslchannels - newvalue = newvalue + x + ','; - end - end - newvalue = strip(newvalue, ','); - if newvalue == "" - newvalue = "1"; - end - app.SELECTEDCHANNELSEditField.Value = newvalue; - end - % Button pushed function: MARKERAddButton function MARKERAddButtonPushed(app, event) app.MARKERTable.Data = [app.MARKERTable.Data;[0 0 1 1 0 0 0]]; @@ -336,6 +409,7 @@ function MARKERDelButtonPushed(app, event) % Menu selected function: LoadMenu function LoadMenuSelected(app, event) global mylsl; + global myselectchannels; [file, path] = uigetfile("./settings/*.mat"); figure(app.UIFigure); % focus back filepath = string(path) + string(file); @@ -343,11 +417,24 @@ function LoadMenuSelected(app, event) return; end settings = load(filepath); + if isfield(settings, 'devicetype') + app.TYPEDropDown.Value = settings.devicetype; + app.updateDevices(); + end + if isfield(settings, 'devicename') + if any(strcmp(app.DEVICEDropDown.Items, settings.devicename)) + app.DEVICEDropDown.Value = settings.devicename; + app.useDevice(); + else + msgbox("Device '" + settings.devicename + ... + "' was not found on this computer", "Warning", "warn"); + end + end if isfield(settings, 'lsltype') app.TYPEEditField.Value = settings.lsltype; end if isfield(settings, 'channels') - app.SELECTEDCHANNELSEditField.Value = settings.channels; + myselectchannels.selected = settings.channels; end if isfield(settings, 'windowsize') app.WINDOWSIZESEditField.Value = settings.windowsize; @@ -358,6 +445,7 @@ function LoadMenuSelected(app, event) if isfield(settings, 'protocol') if any(strcmp(app.PROTOCOLDropDown.Items, settings.protocol)) app.PROTOCOLDropDown.Value = settings.protocol; + app.useProtocol(); else msgbox("Protocol '" + settings.protocol + ... "' was not found on this computer", "Warning", "warn"); @@ -383,14 +471,18 @@ function LoadMenuSelected(app, event) % Menu selected function: SaveMenu function SaveMenuSelected(app, event) + global mydevices; + global myselectchannels; [file, path] = uiputfile("./settings/*.mat"); figure(app.UIFigure); % focus back if isequal(file,0) || isequal(path,0) return; end filepath = string(path) + string(file); + settings.devicetype = mydevices.selected.type; + settings.devicename = mydevices.selected.name; settings.lsltype = app.TYPEEditField.Value; - settings.channels = app.SELECTEDCHANNELSEditField.Value; + settings.channels = myselectchannels.selected; settings.windowsize = app.WINDOWSIZESEditField.Value; settings.sessionlength = app.SESSIONLENGTHSEditField.Value; settings.protocol = app.PROTOCOLDropDown.Value; @@ -429,6 +521,35 @@ function COLORButtonPushed(app, event) end app.updateColors(); end + + % Value changed function: DEVICEDropDown + function DEVICEDropDownValueChanged(app, event) + app.useDevice(); + end + + % Value changed function: TYPEDropDown + function TYPEDropDownValueChanged(app, event) + app.updateDevices(); + app.useDevice(); + end + + % Button pushed function: CHANNELSButton + function CHANNELSButtonPushed(app, event) + global myselectchannels; + myselectchannels.show(); + %myselectchannels.initRequired(); + %myselectchannels.initSelected(); + end + + % Value changed function: PROTOCOLDropDown + function PROTOCOLDropDownValueChanged(app, event) + app.useProtocol(); + end + + % Value changed function: WINDOWSIZESEditField + function WINDOWSIZESEditFieldValueChanged(app, event) + app.checkWindowSize(); + end end % Component initialization @@ -440,7 +561,7 @@ function createComponents(app) % Create UIFigure and hide until all components are created app.UIFigure = uifigure('Visible', 'off'); app.UIFigure.AutoResizeChildren = 'off'; - app.UIFigure.Position = [100 100 653 680]; + app.UIFigure.Position = [100 100 640 772]; app.UIFigure.Name = 'MATLAB App'; app.UIFigure.Resize = 'off'; @@ -462,11 +583,11 @@ function createComponents(app) app.LSLSTREAMPanel = uipanel(app.UIFigure); app.LSLSTREAMPanel.AutoResizeChildren = 'off'; app.LSLSTREAMPanel.Title = 'LSL STREAM'; - app.LSLSTREAMPanel.Position = [16 510 625 156]; + app.LSLSTREAMPanel.Position = [16 541 608 118]; % Create GridLayout app.GridLayout = uigridlayout(app.LSLSTREAMPanel); - app.GridLayout.ColumnWidth = {'1.5x', '2x', '1x'}; + app.GridLayout.ColumnWidth = {'1.32x', '2x', '1x'}; app.GridLayout.RowHeight = {'1x', '1x', '1x'}; % Create TYPELabel @@ -480,7 +601,6 @@ function createComponents(app) app.TYPEEditField = uieditfield(app.GridLayout, 'text'); app.TYPEEditField.Layout.Row = 1; app.TYPEEditField.Layout.Column = 2; - app.TYPEEditField.Value = 'NIRS'; % Create OPENButton app.OPENButton = uibutton(app.GridLayout, 'push'); @@ -519,48 +639,35 @@ function createComponents(app) app.SETTINGSPanel = uipanel(app.UIFigure); app.SETTINGSPanel.AutoResizeChildren = 'off'; app.SETTINGSPanel.Title = 'SETTINGS'; - app.SETTINGSPanel.Position = [16 340 375 157]; + app.SETTINGSPanel.Position = [16 363 375 165]; % Create GridLayout2 app.GridLayout2 = uigridlayout(app.SETTINGSPanel); app.GridLayout2.RowHeight = {'1x', '1x', '1x', '1x'}; - % Create SELECTEDCHANNELSEditFieldLabel - app.SELECTEDCHANNELSEditFieldLabel = uilabel(app.GridLayout2); - app.SELECTEDCHANNELSEditFieldLabel.HorizontalAlignment = 'center'; - app.SELECTEDCHANNELSEditFieldLabel.Layout.Row = 1; - app.SELECTEDCHANNELSEditFieldLabel.Layout.Column = 1; - app.SELECTEDCHANNELSEditFieldLabel.Text = 'SELECTED CHANNELS'; - - % Create SELECTEDCHANNELSEditField - app.SELECTEDCHANNELSEditField = uieditfield(app.GridLayout2, 'text'); - app.SELECTEDCHANNELSEditField.ValueChangedFcn = createCallbackFcn(app, @SELECTEDCHANNELSEditFieldValueChanged, true); - app.SELECTEDCHANNELSEditField.Enable = 'off'; - app.SELECTEDCHANNELSEditField.Tooltip = {'Enter comma separated numeric values larger than zero and less or equal to LSL channels.'}; - app.SELECTEDCHANNELSEditField.Layout.Row = 1; - app.SELECTEDCHANNELSEditField.Layout.Column = 2; - app.SELECTEDCHANNELSEditField.Value = '2'; - % Create WINDOWSIZESEditFieldLabel app.WINDOWSIZESEditFieldLabel = uilabel(app.GridLayout2); app.WINDOWSIZESEditFieldLabel.HorizontalAlignment = 'center'; - app.WINDOWSIZESEditFieldLabel.Layout.Row = 2; + app.WINDOWSIZESEditFieldLabel.FontSize = 11; + app.WINDOWSIZESEditFieldLabel.Layout.Row = 3; app.WINDOWSIZESEditFieldLabel.Layout.Column = 1; app.WINDOWSIZESEditFieldLabel.Text = 'WINDOW SIZE (S)'; % Create WINDOWSIZESEditField app.WINDOWSIZESEditField = uieditfield(app.GridLayout2, 'numeric'); app.WINDOWSIZESEditField.Limits = [0 3600]; + app.WINDOWSIZESEditField.ValueChangedFcn = createCallbackFcn(app, @WINDOWSIZESEditFieldValueChanged, true); app.WINDOWSIZESEditField.HorizontalAlignment = 'left'; app.WINDOWSIZESEditField.Enable = 'off'; - app.WINDOWSIZESEditField.Layout.Row = 2; + app.WINDOWSIZESEditField.Layout.Row = 3; app.WINDOWSIZESEditField.Layout.Column = 2; - app.WINDOWSIZESEditField.Value = 2; + app.WINDOWSIZESEditField.Value = 1; % Create SESSIONLENGTHSEditFieldLabel app.SESSIONLENGTHSEditFieldLabel = uilabel(app.GridLayout2); app.SESSIONLENGTHSEditFieldLabel.HorizontalAlignment = 'center'; - app.SESSIONLENGTHSEditFieldLabel.Layout.Row = 3; + app.SESSIONLENGTHSEditFieldLabel.FontSize = 11; + app.SESSIONLENGTHSEditFieldLabel.Layout.Row = 4; app.SESSIONLENGTHSEditFieldLabel.Layout.Column = 1; app.SESSIONLENGTHSEditFieldLabel.Text = 'SESSION LENGTH (S)'; @@ -569,43 +676,60 @@ function createComponents(app) app.SESSIONLENGTHSEditField.Limits = [0 3600]; app.SESSIONLENGTHSEditField.HorizontalAlignment = 'left'; app.SESSIONLENGTHSEditField.Enable = 'off'; - app.SESSIONLENGTHSEditField.Layout.Row = 3; + app.SESSIONLENGTHSEditField.Layout.Row = 4; app.SESSIONLENGTHSEditField.Layout.Column = 2; app.SESSIONLENGTHSEditField.Value = 10; % Create PROTOCOLLabel app.PROTOCOLLabel = uilabel(app.GridLayout2); app.PROTOCOLLabel.HorizontalAlignment = 'center'; - app.PROTOCOLLabel.Layout.Row = 4; + app.PROTOCOLLabel.FontSize = 11; + app.PROTOCOLLabel.Layout.Row = 1; app.PROTOCOLLabel.Layout.Column = 1; app.PROTOCOLLabel.Text = 'PROTOCOL'; % Create PROTOCOLDropDown app.PROTOCOLDropDown = uidropdown(app.GridLayout2); app.PROTOCOLDropDown.Items = {}; + app.PROTOCOLDropDown.ValueChangedFcn = createCallbackFcn(app, @PROTOCOLDropDownValueChanged, true); app.PROTOCOLDropDown.Enable = 'off'; app.PROTOCOLDropDown.Tooltip = {'Select the Matlab file that should be executed on each window calculating the next feedback. '}; - app.PROTOCOLDropDown.Layout.Row = 4; + app.PROTOCOLDropDown.Layout.Row = 1; app.PROTOCOLDropDown.Layout.Column = 2; app.PROTOCOLDropDown.Value = {}; + % Create CHANNELSButton + app.CHANNELSButton = uibutton(app.GridLayout2, 'push'); + app.CHANNELSButton.ButtonPushedFcn = createCallbackFcn(app, @CHANNELSButtonPushed, true); + app.CHANNELSButton.Enable = 'off'; + app.CHANNELSButton.Layout.Row = 2; + app.CHANNELSButton.Layout.Column = 2; + app.CHANNELSButton.Text = 'SELECT'; + + % Create CHANNELSLabel_3 + app.CHANNELSLabel_3 = uilabel(app.GridLayout2); + app.CHANNELSLabel_3.HorizontalAlignment = 'center'; + app.CHANNELSLabel_3.Layout.Row = 2; + app.CHANNELSLabel_3.Layout.Column = 1; + app.CHANNELSLabel_3.Text = 'CHANNELS'; + % Create STARTButton app.STARTButton = uibutton(app.UIFigure, 'push'); app.STARTButton.ButtonPushedFcn = createCallbackFcn(app, @STARTButtonPushed, true); app.STARTButton.Enable = 'off'; app.STARTButton.Tooltip = {'Start or stop session'}; - app.STARTButton.Position = [17 13 622 22]; + app.STARTButton.Position = [16 19 608 22]; app.STARTButton.Text = 'START'; % Create SESSIONINFOPanel app.SESSIONINFOPanel = uipanel(app.UIFigure); app.SESSIONINFOPanel.AutoResizeChildren = 'off'; app.SESSIONINFOPanel.Title = 'SESSION INFO'; - app.SESSIONINFOPanel.Position = [17 48 473 115]; + app.SESSIONINFOPanel.Position = [17 56 473 115]; % Create GridLayout3 app.GridLayout3 = uigridlayout(app.SESSIONINFOPanel); - app.GridLayout3.ColumnWidth = {'0.75x', '1x', '0.75x', '1x'}; + app.GridLayout3.ColumnWidth = {'0.7x', '1x', '0.7x', '1x'}; app.GridLayout3.RowHeight = {'1x', '1x', '1x'}; % Create SESSIONSTARTEDDescLabel @@ -693,7 +817,7 @@ function createComponents(app) app.EPOCHSPanel = uipanel(app.UIFigure); app.EPOCHSPanel.AutoResizeChildren = 'off'; app.EPOCHSPanel.Title = 'EPOCHS'; - app.EPOCHSPanel.Position = [16 175 625 157]; + app.EPOCHSPanel.Position = [16 185 608 162]; % Create MARKERTable app.MARKERTable = uitable(app.EPOCHSPanel); @@ -704,14 +828,14 @@ function createComponents(app) app.MARKERTable.CellEditCallback = createCallbackFcn(app, @MARKERTableCellEdit, true); app.MARKERTable.Tooltip = {'Define markers (a value between 1 and 99) for epochs here. An epoch is defined by its start and end time. A trigger will be sent at the beginning of each epoch.'}; app.MARKERTable.Enable = 'off'; - app.MARKERTable.Position = [11 8 494 120]; + app.MARKERTable.Position = [11 13 484 120]; % Create MARKERAddButton app.MARKERAddButton = uibutton(app.EPOCHSPanel, 'push'); app.MARKERAddButton.ButtonPushedFcn = createCallbackFcn(app, @MARKERAddButtonPushed, true); app.MARKERAddButton.Enable = 'off'; app.MARKERAddButton.Tooltip = {'Add an epoch'}; - app.MARKERAddButton.Position = [513 94 100 34]; + app.MARKERAddButton.Position = [504 99 93 34]; app.MARKERAddButton.Text = '+'; % Create MARKERDelButton @@ -719,7 +843,7 @@ function createComponents(app) app.MARKERDelButton.ButtonPushedFcn = createCallbackFcn(app, @MARKERDelButtonPushed, true); app.MARKERDelButton.Enable = 'off'; app.MARKERDelButton.Tooltip = {'Remove last or selected epochs'}; - app.MARKERDelButton.Position = [513 51 100 34]; + app.MARKERDelButton.Position = [504 56 93 34]; app.MARKERDelButton.Text = '-'; % Create COLORButton @@ -727,14 +851,14 @@ function createComponents(app) app.COLORButton.ButtonPushedFcn = createCallbackFcn(app, @COLORButtonPushed, true); app.COLORButton.Enable = 'off'; app.COLORButton.Tooltip = {'Select color for selected epochs'}; - app.COLORButton.Position = [513 8 100 34]; + app.COLORButton.Position = [504 13 93 34]; app.COLORButton.Text = 'COLOR'; % Create IDPanel app.IDPanel = uipanel(app.UIFigure); app.IDPanel.AutoResizeChildren = 'off'; app.IDPanel.Title = 'ID'; - app.IDPanel.Position = [400 340 241 157]; + app.IDPanel.Position = [401 363 223 164]; % Create GridLayout4 app.GridLayout4 = uigridlayout(app.IDPanel); @@ -789,7 +913,7 @@ function createComponents(app) app.PROTOCOLTIMEPanel = uipanel(app.UIFigure); app.PROTOCOLTIMEPanel.AutoResizeChildren = 'off'; app.PROTOCOLTIMEPanel.Title = 'PROTOCOL TIME'; - app.PROTOCOLTIMEPanel.Position = [500 48 141 115]; + app.PROTOCOLTIMEPanel.Position = [495 56 129 115]; % Create GridLayout5 app.GridLayout5 = uigridlayout(app.PROTOCOLTIMEPanel); @@ -823,6 +947,46 @@ function createComponents(app) app.PROTOCOLAvgDescLabel.Layout.Column = 1; app.PROTOCOLAvgDescLabel.Text = 'AVG:'; + % Create DEVICEPanel + app.DEVICEPanel = uipanel(app.UIFigure); + app.DEVICEPanel.AutoResizeChildren = 'off'; + app.DEVICEPanel.Title = 'DEVICE'; + app.DEVICEPanel.Position = [16 673 608 88]; + + % Create GridLayout6 + app.GridLayout6 = uigridlayout(app.DEVICEPanel); + app.GridLayout6.ColumnWidth = {'1.32x', '2x', '1x'}; + + % Create MODELDropDownLabel + app.MODELDropDownLabel = uilabel(app.GridLayout6); + app.MODELDropDownLabel.HorizontalAlignment = 'center'; + app.MODELDropDownLabel.Layout.Row = 2; + app.MODELDropDownLabel.Layout.Column = 1; + app.MODELDropDownLabel.Text = 'MODEL'; + + % Create DEVICEDropDown + app.DEVICEDropDown = uidropdown(app.GridLayout6); + app.DEVICEDropDown.Items = {}; + app.DEVICEDropDown.ValueChangedFcn = createCallbackFcn(app, @DEVICEDropDownValueChanged, true); + app.DEVICEDropDown.Layout.Row = 2; + app.DEVICEDropDown.Layout.Column = 2; + app.DEVICEDropDown.Value = {}; + + % Create TYPEDropDownLabel + app.TYPEDropDownLabel = uilabel(app.GridLayout6); + app.TYPEDropDownLabel.HorizontalAlignment = 'center'; + app.TYPEDropDownLabel.Layout.Row = 1; + app.TYPEDropDownLabel.Layout.Column = 1; + app.TYPEDropDownLabel.Text = 'TYPE'; + + % Create TYPEDropDown + app.TYPEDropDown = uidropdown(app.GridLayout6); + app.TYPEDropDown.Items = {}; + app.TYPEDropDown.ValueChangedFcn = createCallbackFcn(app, @TYPEDropDownValueChanged, true); + app.TYPEDropDown.Layout.Row = 1; + app.TYPEDropDown.Layout.Column = 2; + app.TYPEDropDown.Value = {}; + % Show the figure after all components are created app.UIFigure.Visible = 'on'; end diff --git a/ui/selectchannels.m b/ui/selectchannels.m new file mode 100644 index 0000000..cadcb8d --- /dev/null +++ b/ui/selectchannels.m @@ -0,0 +1,309 @@ +classdef selectchannels < handle + %LSL CHANNEL SELECTION + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + properties(Constant) + figwidth = 512; + figheight = 512; + padding = 8; + buttonheight = 24; + panelwidth = selectchannels.figwidth-2*selectchannels.padding; + tablewidth = selectchannels.panelwidth-2*selectchannels.padding; + panelheightrequired = 128; + panelheightchannels = selectchannels.figheight - ... + 4*selectchannels.padding - ... + selectchannels.panelheightrequired - ... + selectchannels.buttonheight; + tableheightrequired = selectchannels.panelheightrequired - ... + 2*selectchannels.padding - 16; + tableheightchannels = selectchannels.panelheightchannels - ... + 2*selectchannels.padding - 16; + end + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + properties + hFig matlab.ui.Figure; + hRequiredPanel matlab.ui.container.Panel; + hChannelsPanel matlab.ui.container.Panel; + hRequired matlab.ui.control.Table; + hChannels matlab.ui.control.Table; + hButton matlab.ui.control.Button; + hStyleOk matlab.ui.style.Style; + hStyleNotOk matlab.ui.style.Style; + selected uint32 = []; + isok logical = false; + end + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + events + Done + end + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + methods (Access = private) + + % returns true if channel matches a requirement or no requirements + function r = isChannelVisible(self, channel) + lenreqs = size(self.hRequired.Data, 1); + if lenreqs == 0 + r = true; + return + end + for idx = 1:lenreqs + if self.hRequired.Data(idx, 4) == channel.type && ... + self.hRequired.Data(idx, 5) == channel.unit + r = true; + return; + end + end + r = false; + end + + % updates ok status + function updateOK(self) + global myprotocols; + global mydevices; + chreq = myprotocols.selected.fh.requires().channels; + chlsl = mydevices.selected.lsl.channels; + self.isok = ~isempty(self.selected); + for idxreq = 1:length(chreq) + found = 0; + for idxlsl = [self.selected] + if chreq(idxreq).type == chlsl(idxlsl).type && ... + chreq(idxreq).unit == chlsl(idxlsl).unit + found = found + 1; + end + end + min = chreq(idxreq).min; + max = chreq(idxreq).max; + if found < min || found > max + self.isok = false; + if isvalid(self.hRequired) + self.hRequired.Data(idxreq, 3) = found; + addStyle(self.hRequired, ... + self.hStyleNotOk, 'cell', [idxreq 3]); + end + else + if isvalid(self.hRequired) + self.hRequired.Data(idxreq, 3) = found; + addStyle(self.hRequired, ... + self.hStyleOk, 'cell', [idxreq 3]); + end + end + end + if isvalid(self.hChannelsPanel) + self.hChannelsPanel.Title = "SELECTED: " + ... + length(self.selected); + end + if isvalid(self.hButton) + self.hButton.Enable = self.isok; + end + end + + % executed when checkbox is changed + function onSelectedChanged(self, ~, ~) + newselected = []; + for idx = 1:size(self.hChannels.Data, 1) + row = self.hChannels.Data(idx,:); + if row(1) == "1" + lslidx = str2double(row(2)); + newselected(end+1) = lslidx; + end + end + self.selected = newselected; + end + + % executed on OK button + function onButtonClicked(self, ~, ~) + notify(self, 'Done'); + self.close(); + end + end + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + methods + function r = get.selected(self) + r = self.selected; + end + + function set.selected(self,val) + self.selected = val; + self.updateOK(); + end + + function show(self) + % window already open + if isvalid(self.hFig) + self.initRequired(); + self.initSelected(); + figure(self.hFig); + return; + end + + % create figure + self.hFig = uifigure(); + self.hFig.Visible = 'off'; + self.hFig.Name = 'SELECT LSL CHANNELS'; + %self.hFig.Color = [0.0 0.0 0.0]; + self.hFig.MenuBar = 'none'; + self.hFig.Units = 'pixels'; + self.hFig.NumberTitle = 'off'; + self.hFig.Position = [0, 0, ... + selectchannels.figwidth, ... + selectchannels.figheight]; + + % create required panel + self.hRequiredPanel = uipanel(self.hFig); + self.hRequiredPanel.AutoResizeChildren = 'on'; + self.hRequiredPanel.Title = 'REQUIRED'; + self.hRequiredPanel.Position = [ + selectchannels.padding, ... + selectchannels.figheight - ... + selectchannels.padding - ... + selectchannels.panelheightrequired, ... + selectchannels.panelwidth, ... + selectchannels.panelheightrequired + ]; + + % create channels panel + self.hChannelsPanel = uipanel(self.hFig); + self.hChannelsPanel.AutoResizeChildren = 'on'; + self.hChannelsPanel.Title = 'SELECTED: 0'; + self.hChannelsPanel.Position = [ + selectchannels.padding, ... + selectchannels.buttonheight + 2*selectchannels.padding, ... + selectchannels.panelwidth, ... + selectchannels.panelheightchannels + ]; + + % create required table + self.hRequired = uitable(self.hRequiredPanel); + self.hRequired.ColumnName = {'MIN'; 'MAX'; 'SEL'; 'TYPE'; 'UNIT'}; + self.hRequired.ColumnWidth = {50, 50, 45, 'auto', 100}; + self.hRequired.ColumnFormat = { + 'short', 'short', 'short', 'char', 'char' }; + self.hRequired.RowName = {}; + self.hRequired.ColumnEditable = [false false false false false]; + self.hRequired.Position = [... + selectchannels.padding, ... + selectchannels.padding, ... + selectchannels.tablewidth, ... + selectchannels.tableheightrequired]; + self.hRequired.SelectionType = 'cell'; + + % create channels table + self.hChannels = uitable(self.hChannelsPanel); + self.hChannels.ColumnName = {''; 'LSL CH'; 'DEV CH'; 'TYPE'; 'UNIT'}; + self.hChannels.ColumnWidth = {25, 60, 60, 'auto', 100}; + self.hChannels.ColumnFormat = { + 'logical', 'short', 'short', 'char', 'char' }; + self.hChannels.RowName = {}; + self.hChannels.ColumnEditable = [true false false false false]; + self.hChannels.Position = [... + selectchannels.padding, ... + selectchannels.padding, ... + selectchannels.tablewidth, ... + selectchannels.tableheightchannels]; + self.hChannels.CellEditCallback = @self.onSelectedChanged; + self.hChannels.SelectionType = 'cell'; + + % create center style + s = uistyle(); + s.HorizontalAlignment = 'center'; + addStyle(self.hChannels, s, 'column', [2;3]); + addStyle(self.hRequired, s, 'column', [1;2;3]); + + % create ok button + self.hButton = uibutton(self.hFig, 'push'); + self.hButton.ButtonPushedFcn = @self.onButtonClicked; + self.hButton.FontWeight = 'bold'; + self.hButton.Text = 'OK'; + self.hButton.Position = [ + selectchannels.padding, ... + selectchannels.padding, ... + selectchannels.panelwidth, ... + selectchannels.buttonheight]; + + % init ok style + self.hStyleOk = uistyle(); + self.hStyleOk.BackgroundColor = 'green'; + self.hStyleOk.HorizontalAlignment = 'center'; + + % init notok style + self.hStyleNotOk = uistyle(); + self.hStyleNotOk.BackgroundColor = 'red'; + self.hStyleNotOk.HorizontalAlignment = 'center'; + + % init tables data + self.initRequired(); + self.initSelected(); + + % show + self.hFig.Visible = 'on'; + end + + function close(self) + if isvalid(self.hFig) + close(self.hFig); + end + end + + function initRequired(self) + if isempty(self.hFig) || ~isvalid(self.hFig) + return + end + global myprotocols; + req = myprotocols.selected.fh.requires(); + self.hRequired.Data = strings([0,5]); + for idx = 1:length(req.channels) + self.hRequired.Data(idx,:) = [ + req.channels(idx).min, ... + req.channels(idx).max, ... + 0, ... + req.channels(idx).type, ... + req.channels(idx).unit + ]; + end + end + + function initSelected(self) + if isempty(self.hFig) || ~isvalid(self.hFig) + return; + end + global mylsl; + global mydevices; + tblidx = 1; + lenreqs = size(self.hRequired.Data, 1); + self.hChannels.Data = strings([0,5]); + for idx = 1:mylsl.lslchannels + isselected = ismember(idx, self.selected); + isselectedstr = convertCharsToStrings(num2str(isselected)); + if idx <= size(mydevices.selected.lsl.channels, 1) && ... + self.isChannelVisible(mydevices.selected.lsl.channels(idx)) + self.hChannels.Data(tblidx,:) = [ + isselectedstr, ... + idx, ... + mydevices.selected.lsl.channels(idx).devch, ... + mydevices.selected.lsl.channels(idx).type, ... + mydevices.selected.lsl.channels(idx).unit + ]; + tblidx = tblidx + 1; + elseif lenreqs == 0 + self.hChannels.Data(tblidx,:) = [ + isselectedstr, ... + idx, ... + "", ... + "", ... + "" + ]; + tblidx = tblidx + 1; + end + end + self.updateOK(); + end + end +end