diff --git a/@MyCollector/MyCollector.m b/@MyCollector/MyCollector.m index 975b10e..01a42da 100644 --- a/@MyCollector/MyCollector.m +++ b/@MyCollector/MyCollector.m @@ -1,364 +1,364 @@ classdef MyCollector < MySingleton properties (GetAccess = public, SetAccess = private, ... SetObservable = true) % Structure accomodating handles of instrument objects InstrList = struct() % Properties of instruments InstrProps = struct() % Structure accomodating handles of apps which contain user % interface elements (excluding instrument GUIs) AppList = struct() end properties (Access = private) Listeners = struct() % Metadata indicating the state of Collector Metadata = MyMetadata.empty() end properties (Dependent = true) running_instruments running_apps end events NewDataWithHeaders end methods (Access = private) % The constructor of a singleton class must be private function this = MyCollector() + disp(['Creating a new instance of ' class(this)]) end end methods (Access = public) function delete(this) cellfun(@(x) deleteListeners(this, x), ... this.running_instruments); end function addInstrument(this, name, Instrument, varargin) assert(isvarname(name), ['Instrument name must be a valid ' ... 'MATLAB variable name.']) assert(~ismember(name, this.running_instruments), ... ['Instrument ' name ' is already present in the ' ... 'collector. Delete the existing instrument before ' ... 'adding a new one with the same name.']) p = inputParser(); % Optional - put the instrument in global workspace addParameter(p, 'make_global', true, @islogical); % Read the settings of this instrument when new data is % acquired addParameter(p, 'collect_header', true, @islogical); parse(p, varargin{:}); this.InstrList.(name) = Instrument; % Configure instrument properties this.InstrProps.(name) = struct( ... 'collect_header', p.Results.collect_header, ... 'global_name', '', ... 'Gui', []); if p.Results.make_global global_name = name; % Assign instrument handle to a variable in global % workspace for quick reference if isValidBaseVar(global_name) base_ws_vars = evalin('base', 'who'); warning(['A valid variable named ''' global_name ... ''' already exists in global workspace.']) % Generate a new name excluding all the variable names % existing in the base workspace global_name = matlab.lang.makeUniqueStrings( ... global_name, base_ws_vars); end % Put the instrument in global workspace assignin('base', global_name, Instrument); this.InstrProps.(name).global_name = global_name; end if this.InstrProps.(name).collect_header && ... ~ismethod(Instrument, 'readSettings') % If the class does not have a header generation function, % it can still be added to the collector and transfer data % to Daq this.InstrProps.(name).collect_header = false; warning(['%s does not have a readSettings function, ',... 'measurement headers will not be collected from ',... 'this instrument.'],name) end % If the added instrument has a newdata event, we add a % listener for it. if ismember('NewData', events(this.InstrList.(name))) this.Listeners.(name).NewData = ... addlistener(this.InstrList.(name),'NewData',... @(~, EventData) acquireData(this, name, EventData)); end %Cleans up if the instrument is closed this.Listeners.(name).Deletion = ... addlistener(this.InstrList.(name), ... 'ObjectBeingDestroyed', ... @(~,~) instrumentDeletedCallback(this, name)); end % Get existing instrument function Instr = getInstrument(this, name) assert(isfield(this.InstrList, name), ... ['Name must correspond to one of the running ' ... 'instruments.']) Instr = this.InstrList.(name); end % Interface for accessing internally stored instrument properties function val = getInstrumentProp(this, instr_name, prop_name) assert(isfield(this.InstrProps, instr_name), ... ['''instr_name'' must correspond to one of the ' ... 'running instruments.']) assert(isfield(this.InstrProps.(instr_name), prop_name), ... ['''prop_name'' must correspond to one of the following'... 'instrument properties: ' ... var2str(fieldnames(this.InstrProps.(instr_name)))]) val = this.InstrProps.(instr_name).(prop_name); end function setInstrumentProp(this, instr_name, prop_name, val) assert(isfield(this.InstrProps, instr_name), ... ['''instr_name'' must correspond to one of the ' ... 'running instruments.']) assert(isfield(this.InstrProps.(instr_name), prop_name), ... ['''prop_name'' must correspond to one of the following'... 'instrument properties: ' ... var2str(fieldnames(this.InstrProps.(instr_name)))]) this.InstrProps.(instr_name).(prop_name) = val; end function addApp(this, App, app_name) assert(~isfield(this.AppList, app_name), ['App with name ''' ... app_name ''' is already present in the collector.']) this.AppList.(app_name) = App; % Set up a listener that will update the list when the app % is deleted addlistener(App, 'ObjectBeingDestroyed', ... @(~,~)removeApp(this, app_name)); end function App = getApp(this, app_name) assert(isfield(this.AppList, app_name), [app_name ... ' does not correspond to any of the running apps.']) App = this.AppList.(app_name); end function acquireData(this, name, InstrEventData) src = InstrEventData.Source; % Check that event data object is MyNewDataEvent, % and fix otherwise if ~isa(InstrEventData,'MyNewDataEvent') InstrEventData = MyNewDataEvent(); InstrEventData.new_header = true; InstrEventData.Trace = copy(src.Trace); end % Indicate the name of acquiring instrument InstrEventData.src_name = name; % Collect the headers if the flag is on and if the triggering % instrument does not request suppression of header collection if InstrEventData.new_header % Add the name of acquisition instrument AcqInstrMdt = MyMetadata('title', 'AcquiringInstrument'); addParam(AcqInstrMdt, 'Name', InstrEventData.src_name); % Make the full metadata Mdt = [AcqInstrMdt, acquireHeaders(this)]; %We copy the MeasHeaders to both copies of the trace - the %one that is with the source and the one that is forwarded %to Daq. InstrEventData.Trace.MeasHeaders = copy(Mdt); src.Trace.MeasHeaders = copy(Mdt); end triggerNewDataWithHeaders(this, InstrEventData); end % Collects headers for open instruments with the header flag on function Mdt = acquireHeaders(this) Mdt = MyMetadata.empty(); for i = 1:length(this.running_instruments) name = this.running_instruments{i}; if this.InstrProps.(name).collect_header try TmpMdt = readSettings(this.InstrList.(name)); TmpMdt.title = name; Mdt = [Mdt, TmpMdt]; %#ok catch ME warning(['Error while reading metadata from ' ... '%s. Measurement header collection is '... 'switched off for this instrument.' ... '\nError: %s'], name, ME.message) this.InstrProps.(name).collect_header = false; end end end % Add field indicating the time when the trace was acquired TimeMdt = MyMetadata.time('title', 'AcquisitionTime'); % Add the state of Collector CollMdt = getMetadata(this); Mdt = [TimeMdt, Mdt, CollMdt]; end function bool = isrunning(this, name) assert(ischar(name)&&isvector(name),... 'Instrument name must be a character vector, not %s',... class(name)); bool = ismember(name, this.running_instruments); end % Remove instrument from collector without deleting the instrument % object function removeInstrument(this, name) if isrunning(this, name) % Remove the instrument entries this.InstrList = rmfield(this.InstrList, name); this.InstrProps = rmfield(this.InstrProps, name); deleteListeners(this, name); end end function removeApp(this, name) if isfield(this.AppList, name) this.AppList = rmfield(this.AppList, name); end end % Delete all presesently running instruments function flush(this) instr_names = this.running_instruments; for i = 1:length(instr_names) nm = instr_names{i}; % We rely on the deletion callbacks to do cleanup delete(this.InstrList.(nm)); end end end methods (Access = private) function instrumentDeletedCallback(this, name) % Clear the base workspace wariable gn = this.InstrProps.(name).global_name; if ~isempty(gn) try evalin('base', sprintf('clear(''%s'');', gn)); catch ME warning(['Could not clear global variable ''' ... gn '''. Error: ' ME.message]); end end % Remove the instrument entry from Collector removeInstrument(this, name); end % Create metadata that stores information about the Collector % state function Mdt = getMetadata(this) % Create new metadata if it has not yet been initialized if isempty(this.Metadata) this.Metadata = MyMetadata('title', 'SessionInfo'); addParam(this.Metadata, 'instruments', {}); addParam(this.Metadata, 'Props', struct()); end % Update metadata parameters this.Metadata.ParamList.instruments = this.running_instruments; for fn = this.running_instruments' this.Metadata.ParamList.Props.(fn).collect_header = ... this.InstrProps.collect_header; this.Metadata.ParamList.Props.(fn).is_global = ... ~isempty(this.InstrProps.global_name); % Indicate if the instrument has gui this.Metadata.ParamList.Props.(fn).has_gui = ... ~isempty(this.InstrProps.Gui); end Mdt = copy(this.Metadata); end end methods(Static = true) % Singletone constructor. function this = instance() persistent UniqueInstance if isempty(UniqueInstance)||(~isvalid(UniqueInstance)) - disp('Creating a new instance of MyCollector') this = MyCollector(); UniqueInstance = this; else this = UniqueInstance; end end end methods (Access = private) function triggerNewDataWithHeaders(this, InstrEventData) notify(this, 'NewDataWithHeaders', InstrEventData); end %deleteListeners is in a separate file deleteListeners(this, obj_name); end methods function val = get.running_instruments(this) val = fieldnames(this.InstrList); end function val = get.running_apps(this) val = fieldnames(this.AppList); end end end diff --git a/@MyLogger/MyLogger.m b/@MyLogger/MyLogger.m index 58b66f6..413f4aa 100644 --- a/@MyLogger/MyLogger.m +++ b/@MyLogger/MyLogger.m @@ -1,241 +1,316 @@ % Generic logger that executes measFcn according to MeasTimer, stores the % results and optionally continuously saves them. % measFcn should be a function with no arguments. % measFcn need to return a row vector of numbers in order to save the log % in text format or display it. With other kinds of returned values the % log can still be recorded, but not saved or dispalyed. classdef MyLogger < handle properties (Access = public) % Timer object MeasTimer = timer.empty() % Function that provides data to be recorded measFcn = @()0 % MyLog object to store the recorded data Record = MyLog.empty() % Format for displaying readings (column name: value) disp_fmt = '\t%15s:\t%.5g' % Option for daily/weekly creation of a new log file FileCreationInterval = duration.empty() end properties (SetAccess = protected, GetAccess = public) % If last measurement was succesful % 0-false, 1-true, 2-never measured last_meas_stat = 2 end + properties (Access = protected) + Metadata = MyMetadata.empty() + end + events % Event that is triggered each time measFcn is successfully % executed NewMeasurement % Event for transferring data to the collector NewData end methods (Access = public) function this = MyLogger(varargin) P = MyClassParser(this); addParameter(P, 'log_opts', {}, @iscell); processInputs(P, this, varargin{:}); this.Record = MyLog(P.Results.log_opts{:}); % Create and confitugure timer this.MeasTimer = timer(); this.MeasTimer.BusyMode = 'drop'; % Fixed spacing mode of operation does not follow the % period very well, but is robust with respect to % function execution delays this.MeasTimer.ExecutionMode = 'fixedSpacing'; this.MeasTimer.TimerFcn = @this.loggerFcn; end function delete(this) % Stop and delete the timer try stop(this.MeasTimer); catch ME warning(['Could not stop measurement timer. Error: ' ... ME.message]); end try delete(this.MeasTimer); catch ME warning(['Could not delete measurement timer. Error: ' ... ME.message]); end end % Redefine start/stop functions for the brevity of use function start(this) if ~isempty(this.FileCreationInterval) && ... isempty(this.Record.FirstSaveTime) % If run in the limited length mode, extend the record % file name createLogFileName(this); end start(this.MeasTimer); end function stop(this) stop(this.MeasTimer); end + function bool = isrunning(this) + try + bool = strcmpi(this.MeasTimer.running, 'on'); + catch ME + warning(['Cannot check if the measurement timer is on. '... + 'Error: ' ME.message]); + + bool = false; + end + end + % Trigger an event that transfers the data from one log channel % to Daq function triggerNewData(this, varargin) % Since the class does not have Trace property, a Trace must be % supplied explicitly Trace = toTrace(this.Record, varargin{:}); EventData = MyNewDataEvent('Trace',Trace, 'new_header',false); notify(this, 'NewData', EventData); end % Display reading function str = printReading(this, ind) if isempty(this.Record.timestamps) str = ''; return end % Print the last reading if index is not given explicitly if nargin()< 2 ind = length(this.Record.timestamps); end switch ind case 1 prefix = 'First reading '; case length(this.Record.timestamps) prefix = 'Last reading '; otherwise prefix = 'Reading '; end str = [prefix, char(this.Record.timestamps(ind)), newline]; data_row = this.Record.data(ind, :); for i=1:length(data_row) if length(this.Record.data_headers)>=i lbl = this.Record.data_headers{i}; else lbl = sprintf('data%i', i); end str = [str,... sprintf(this.disp_fmt, lbl, data_row(i)), newline]; %#ok end end % Generate a new file name for the measurement record function createLogFileName(this, path, name, ext) [ex_path, ex_name, ex_ext] = fileparts(this.Record.file_name); if ~exist('path', 'var') path = ex_path; end if ~exist('name', 'var') name = ex_name; end if ~exist('ext', 'var') if ~isempty(ex_ext) ext = ex_ext; else ext = this.Record.data_file_ext; end end % Remove the previous time stamp from the file name if exists token = regexp(name, ... '\d\d\d\d-\d\d-\d\d \d\d-\d\d (.*)', 'tokens'); if ~isempty(token) name = token{1}{1}; end % Prepend a new time stamp name = [datestr(datetime('now'),'yyyy-mm-dd HH-MM '), name]; file_name = fullfile(path, [name, ext]); % Ensure that the generated file name is unique file_name = createUniqueFileName(file_name); this.Record.file_name = file_name; end + + function Mdt = readSettings(this) + if isempty(this.Metadata) + this.Metadata = MyMetadata('title', class(this)); + + addParam(this.Metadata, 'meas_period', [], 'comment', ... + 'measurement period (s)'); + + addParam(this.Metadata, 'save_cont', [], 'comment', ... + 'If measurements are continuously saved (true/false)'); + + addParam(this.Metadata, 'file_creation_interval', [], ... + 'comment', ['The interval over which new data ' ... + 'files are created when saving continuously ' ... + '(days:hours:min:sec)']); + + addParam(this.Metadata, 'log_length_limit', [], ... + 'comment', ['The maximum number of points kept ' ... + 'in the measurement record']); + end + + % Update parameter values + this.Metadata.ParamList.meas_period = this.MeasTimer.Period; + this.Metadata.ParamList.save_cont = this.Record.save_cont; + this.Metadata.ParamList.file_creation_interval = ... + char(this.FileCreationInterval); + this.Metadata.ParamList.log_length_limit = ... + this.Record.length_lim; + + Mdt = copy(this.Metadata); + end + + % Configure the logger settings from metadata + function writeSettings(this, Mdt) + + % Stop ongoing measurements + stop(this); + + if isparam(Mdt, 'meas_period') + this.MeasTimer.Period = Mdt.ParamList.meas_period; + end + + if isparam(Mdt, 'save_cont') + this.Record.save_cont = Mdt.ParamList.save_cont; + end + + if isparam(Mdt, 'file_creation_interval') + this.FileCreationInterval = ... + duration(Mdt.ParamList.file_creation_interval); + end + + if isparam(Mdt, 'log_length_limit') + this.Record.length_lim = Mdt.ParamList.log_length_limit; + end + end end methods (Access = protected) % Perform measurement and append point to the log function loggerFcn(this, ~, event) Time = datetime(event.Data.time); try meas_result = this.measFcn(); this.last_meas_stat = 1; % last measurement ok catch ME warning(['Logger cannot take measurement at time = ',... datestr(Time) '.\nError: ' ME.message]); this.last_meas_stat = 0; % last measurement not ok return end if this.Record.save_cont && ... ~isempty(this.FileCreationInterval) && ... ~isempty(this.Record.FirstSaveTime) && ... (Time - this.Record.FirstSaveTime) >= ... this.FileCreationInterval % Switch to a new data file createLogFileName(this); end % Append measurement result together with time stamp appendData(this.Record, Time, meas_result); notify(this, 'NewMeasurement'); end end %% Set and get functions methods function set.measFcn(this, val) assert(isa(val, 'function_handle'), ... '''measFcn'' must be a function handle.'); this.measFcn = val; end function set.Record(this, val) assert(isa(val, 'MyLog'), '''Record'' must be a MyLog object') this.Record = val; end function set.MeasTimer(this, val) assert(isa(val, 'timer'), ... '''MeasTimer'' must be a timer object') this.MeasTimer = val; end - function set.FileCreationInterval(this, val) - assert(isa(val, 'duration'), ['''FileCreationInterval'' ' ... + function set.FileCreationInterval(this, Val) + assert(isa(Val, 'duration'), ['''FileCreationInterval'' ' ... 'must be a duration object.']) - this.FileCreationInterval = val; + + if ~isempty(Val) + Val.Format = 'dd:hh:mm:ss'; + end + + this.FileCreationInterval = Val; end end end diff --git a/@MyNewportUsbComm/MyNewportUsbComm.m b/@MyNewportUsbComm/MyNewportUsbComm.m index f58bbdd..8fb656d 100644 --- a/@MyNewportUsbComm/MyNewportUsbComm.m +++ b/@MyNewportUsbComm/MyNewportUsbComm.m @@ -1,78 +1,79 @@ classdef MyNewportUsbComm < MySingleton properties (GetAccess = public, SetAccess = private) isbusy = false % driver in use QueryData % query buffer end properties (Access = public) Usb % Instance of Newport.USBComm.USB class end methods(Access = private) % The constructor of a singleton class should only be invoked from % the instance method. function this = MyNewportUsbComm() + disp(['Creating a new instance of ' class(this)]) + this.QueryData = System.Text.StringBuilder(64); loadLib(this); end end methods(Access = public) % Load dll function loadLib(this) dll_path = which('UsbDllWrap.dll'); if isempty(dll_path) error(['UsbDllWrap.dll is not found. This library ',... 'is a part of Newport USB driver and needs ',... 'to be present on Matlab path.']) end NetAsm = NET.addAssembly(dll_path); % Create an instance of Newport.USBComm.USB class Type = GetType(NetAsm.AssemblyHandle,'Newport.USBComm.USB'); this.Usb = System.Activator.CreateInstance(Type); end function str = query(this, addr, cmd) % Check if the driver is already being used by another process. % A race condition with various strange consequences is % potentially possible if it is. if this.isbusy warning('NewportUsbComm is already in use') end this.isbusy = true; % Send query using the QueryData buffer stat = Query(this.Usb, addr, cmd, this.QueryData); if stat == 0 str = char(ToString(this.QueryData)); else str = ''; warning('Query to Newport usb driver was unsuccessful.'); end this.isbusy = false; end end methods(Static) % Concrete implementation of the singleton constructor. function this = instance() persistent UniqueInstance if isempty(UniqueInstance) || ~isvalid(UniqueInstance) - disp('Creating a new instance of NewportUsbComm') this = MyNewportUsbComm(); UniqueInstance = this; else this = UniqueInstance; end end end end diff --git a/@MySingleton/MySingleton.m b/@MySingleton/MySingleton.m index c79d462..bdb200f 100644 --- a/@MySingleton/MySingleton.m +++ b/@MySingleton/MySingleton.m @@ -1,13 +1,12 @@ % This an abstract class used to derive subclasses only one instance of % which can exist at a time. % See https://ch.mathworks.com/matlabcentral/fileexchange/24911-design-pattern-singleton-creational % for more information. classdef MySingleton < handle - methods(Abstract, Static) + methods (Abstract, Static) this = instance(); end - end diff --git a/Utility functions/runLogger.m b/Utility functions/runLogger.m index ca61983..8bfe075 100644 --- a/Utility functions/runLogger.m +++ b/Utility functions/runLogger.m @@ -1,100 +1,100 @@ % Create and add to Collector an instrument logger using buil-in method % of the instrument class. % % This function is called with two syntaxes: % % runLogger(instr_name) where instr_name corresponds to an entry in % the local InstrumentList % % runLogger(Instrument) where Instrument is an object that is % already present in the collector function [Lg, Gui] = runLogger(arg) % Get the instance of collector C = MyCollector.instance(); if ischar(arg) % The function is called with an instrument name instr_name = arg; Instr = runInstrument(instr_name); else % The function is called with an instrument object Instr = arg; % Find the instrument name from the collector ri = C.running_instruments; ind = cellfun(@(x)isequal(Instr, getInstrument(C, x)), ri); assert(nnz(ind) == 1, 'Instrument must be present in Collector'); instr_name = ri{ind}; end % Make a logger name name = [instr_name 'Logger']; % Add logger to the collector so that it can transfer data to Daq if ~isrunning(C, name) assert(ismethod(Instr, 'createLogger'), ['A logger is not ' ... 'created as instrument class ' class(Instr) ... ' does not define ''createLogger'' method.']) % Create and set up a new logger try dir = getLocalSettings('default_log_dir'); catch try dir = getLocalSettings('measurement_base_dir'); dir = createSessionPath(dir, [instr_name ' log']); catch dir = ''; end end Lg = createLogger(Instr); createLogFileName(Lg, dir, instr_name); % Add logger to Collector - addInstrument(C, name, Lg, 'collect_header', false); + addInstrument(C, name, Lg); else disp(['Logger for ' instr_name ' is already running. ' ... 'Returning existing.']) Lg = getInstrument(C, name); end % Check if the logger already has a GUI Gui = getInstrumentProp(C, name, 'Gui'); if isempty(Gui) || ~isvalid(Gui) % Run a new GUI and store it in the collector Gui = GuiLogger(Lg); setInstrumentProp(C, name, 'Gui', Gui); % Display the instrument's name Fig = findFigure(Gui); if ~isempty(Fig) Fig.Name = char(name); else warning('No UIFigure found to assign the name') end % Apply color scheme applyLocalColorScheme(Fig); % Move the app figure to the center of the screen centerFigure(Fig); else % Bring the window of existing GUI to the front try setFocus(Gui); catch end end end