diff --git a/@MyLogger/MyLogger.m b/@MyLogger/MyLogger.m index f8e1530..14dfcd2 100644 --- a/@MyLogger/MyLogger.m +++ b/@MyLogger/MyLogger.m @@ -1,236 +1,236 @@ % 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 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 % 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, % as well as possible _n ending token = regexp(name, ... '\d\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 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 + catch ME warning(['Logger cannot take measurement at time = ',... - datestr(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 end end diff --git a/@MyTpg/MyTpg.m b/@MyTpg/MyTpg.m index f3d0d5c..be9aa7d 100644 --- a/@MyTpg/MyTpg.m +++ b/@MyTpg/MyTpg.m @@ -1,174 +1,181 @@ % Class for communication with Pfeiffer TPG single and dual pressure gauge % controllers. % Use 'serial' communication objects instead of 'visa' with this instrument % Tested with TPG 262 and 362. classdef MyTpg < MyInstrument & MyCommCont properties (Constant = true, Access = protected) % Named constants for communication ETX = char(3); % end of text CR = char(13); % carriage return \r LF = char(10); %#ok line feed \n ENQ = char(5); % enquiry ACK = char(6); % acknowledge NAK = char(21); % negative acknowledge end properties (SetAccess = protected, GetAccess = public, ... SetObservable = true) % Last measurement status gauge_stat = {'', ''}; end methods (Access = public) function this = MyTpg(varargin) this@MyCommCont(varargin{:}); createCommandList(this); end % read pressure from a single channel or both channels at a time function p_arr = readPressure(this) queryString(this, ['PRX', this.CR, this.LF]); str = queryString(this, this.ENQ); % Extract pressure and gauge status from reading. arr = sscanf(str,'%i,%e,%i,%e'); p_arr = transpose(arr(2:2:end)); % Status codes: % 0 –> Measurement data okay % 1 –> Underrange % 2 –> Overrange % 3 –> Sensor error % 4 –> Sensor off (IKR, PKR, IMR, PBR) % 5 –> No sensor (output: 5,2.0000E-2 [hPa]) % 6 –> Identification error this.gauge_stat{1} = gaugeStatusFromCode(this, arr(1)); this.gauge_stat{2} = gaugeStatusFromCode(this, arr(3)); end function pu = readPressureUnit(this) queryString(this, ['UNI',this.CR,this.LF]); str = queryString(this, this.ENQ); % Pressure units correspondence table: % 0 –> mbar/bar % 1 –> Torr % 2 –> Pascal % 3 –> Micron % 4 –> hPascal (default) % 5 –> Volt pu_code = sscanf(str,'%i'); pu = pressureUnitFromCode(this, pu_code); end function id_list = readGaugeId(this) queryString(this, ['TID',this.CR,this.LF]); str = queryString(this, this.ENQ); id_list = deblank(strsplit(str,{','})); end function code_list = turnGauge(this) queryString(this, ['SEN',char(1,1),this.CR,this.LF]); str = queryString(this, this.ENQ); code_list = deblank(strsplit(str,{','})); end % Attempt communication and identification of the device function [str, msg] = idn(this) try queryString(['AYT', this.CR, this.LF]); str = queryString(this.ENQ); catch ME str = ''; msg = ME.message; end this.idn_str = toSingleLine(str); end % Create pressure logger function Lg = createLogger(this, varargin) + function p = MeasPressure() + + % Sync the class properties which also will tirgger an + % update of all the guis to which the instrument is linked + sync(this); + p = this.pressure; + end - Lg = MyLogger(varargin{:}, 'MeasFcn', @this.readPressure); + Lg = MyLogger(varargin{:}, 'MeasFcn', @MeasPressure); pu = this.pressure_unit; if isempty(Lg.Record.data_headers) && ~isempty(pu) Lg.Record.data_headers = ... {['P ch1 (' pu ')'], ['P ch2 (' pu ')']}; end end end methods (Access = protected) function createCommandList(this) addCommand(this, 'pressure', ... 'readFcn', @this.readPressure, ... 'default', [0, 0]); addCommand(this, 'pressure_unit', ... 'readFcn', @this.readPressureUnit, ... 'default', 'mBar'); addCommand(this, 'gauge_id', ... 'readFcn', @this.readGaugeId, ... 'default', {'', ''}); end function createMetadata(this) createMetadata@MyInstrument(this); addObjProp(this.Metadata, this, 'gauge_stat', ... 'comment', 'Last measurement status'); end % Convert numerical code for gauge status to a string function str = gaugeStatusFromCode(~, code) switch int8(code) case 0 str = 'Measurement data ok'; case 1 str = 'Underrange'; case 2 str = 'Overrange'; case 3 str = 'Sensor error'; case 4 str = 'Sensor off'; case 5 str = 'No sensor'; case 6 str = 'Identification error'; otherwise str = ''; warning('Unknown gauge status code %i', code); end end % Convert numerical code for pressure unit to a string function str = pressureUnitFromCode(~, code) switch int8(code) case 0 str = 'mBar'; case 1 str = 'Torr'; case 2 str = 'Pa'; case 3 str = 'Micron'; case 4 str = 'hPa'; case 5 str = 'Volt'; otherwise str = ''; warning('unknown pressure unit, code=%i',pu_num) end end end end diff --git a/Local/runLogViewer.m b/Local/runLogViewer.m index b19ec4e..834d3cb 100644 --- a/Local/runLogViewer.m +++ b/Local/runLogViewer.m @@ -1,36 +1,39 @@ % Start a logger gui in dummy mode, which allows to browse existing logs function runLogViewer() name = 'LogViewer'; Collector = MyCollector.instance(); if ismember(name, Collector.running_instruments) % If LogViewer is already present in the Collector, do not create % a new one, but rather bring focus to the existing one. disp([name, ' is already running.']); Gui = getInstrumentGui(Collector, name); % Bring the window of existing GUI to the front try setFocus(Gui); catch end else % Start GuiLogger in dummy mode GuiLw = GuiLogger(); addInstrument(Collector, name, GuiLw.Lg, 'collect_header', false); addInstrumentGui(Collector, name, GuiLw); % Display the instrument's name Fig = findFigure(GuiLw); Fig.Name = char(name); % Apply color scheme applyLocalColorScheme(Fig); + + % Move the app figure to the center of the screen + centerFigure(Fig); end end diff --git a/Utility functions/runInstrumentWithGui.m b/Utility functions/runInstrumentWithGui.m index fc2e78a..ca14094 100644 --- a/Utility functions/runInstrumentWithGui.m +++ b/Utility functions/runInstrumentWithGui.m @@ -1,68 +1,71 @@ % Create an instrument instance with gui add them to the collector function [Instr, Gui] = runInstrumentWithGui(name, instr_class, gui, varargin) % Get the unique instance of Collector Collector = MyCollector.instance(); % Run instrument first if ~exist('instr_class', 'var') || ~exist('gui', 'var') % Run instrument without GUI Instr = runInstrument(name); % Load GUI name from InstrumentList InstrumentList = getLocalSettings('InstrumentList'); ind = cellfun(@(x)isequal(x, name), {InstrumentList.name}); assert(any(ind), [name ' must correspond to an entry in ' ... 'InstrumentList.']) InstrEntry = InstrumentList(ind); if length(InstrEntry) > 1 % Multiple entries found warning(['Multiple InstrumentList entries found with ' ... 'name ' name]); InstrEntry = InstrEntry(1); end gui = InstrEntry.gui; assert(~isempty(gui), ['GUI is not specified for ' name]); else % All arguments are supplied explicitly Instr = runInstrument(name, instr_class, varargin{:}); end % Check if the instrument already has GUI Gui = getInstrumentGui(Collector, name); if isempty(Gui) % Run a new GUI and store it in the collector Gui = feval(gui, Instr); addInstrumentGui(Collector, name, 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 diff --git a/Utility functions/runLogger.m b/Utility functions/runLogger.m index df2fa4b..b82f8d1 100644 --- a/Utility functions/runLogger.m +++ b/Utility functions/runLogger.m @@ -1,98 +1,101 @@ % 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); 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 = getInstrumentGui(C, name); if isempty(Gui) % Run a new GUI and store it in the collector Gui = GuiLogger(Lg); addInstrumentGui(C, name, 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