diff --git a/@MyAgilentDso/MyAgilentDso.m b/@MyAgilentDso/MyAgilentDso.m new file mode 100644 index 0000000..6df5bd9 --- /dev/null +++ b/@MyAgilentDso/MyAgilentDso.m @@ -0,0 +1,174 @@ +% Class for controlling 4-channel Agilent DSO scopes. +% Tested with DSO7034A + +classdef MyAgilentDso < MyScpiInstrument & MyDataSource & MyCommCont + + properties (Constant = true) + channel_no = 4 % number of channels + end + + methods (Access = public) + function this = MyAgilentDso(varargin) + this@MyCommCont(varargin{:}); + + % 1.6e7 is the maximum trace size of DSO7034A + %(8 mln point of 2-byte integers) + this.Comm.InputBufferSize = 2e7; %byte + this.Trace.name_x = 'Time'; + this.Trace.name_y = 'Voltage'; + this.Trace.unit_x = 's'; + this.Trace.unit_y = 'V'; + + createCommandList(this); + end + + function readTrace(this) + this.Comm.ByteOrder = 'littleEndian'; + + % Set data format to be signed integer, reversed byte order, + % 2 bytes per measurement point, read the maximum + % available number of points + writeStrings(this, ... + ':WAVeform:BYTeorder LSBFirst', ... + ':WAVeform:FORMat WORD', ... + ':WAVeform:POINts:MODE MAX', ... + ':WAVeform:UNSigned OFF', ... + ':WAVeform:DATA?'); + + % Read the trace data + y_data = int16(binblockread(this.Comm, 'int16')); + + % Read the preamble + pre_str = queryString(this, ':WAVeform:PREamble?'); + + % Drop the end-of-the-string symbol and split + pre = str2double(split(pre_str(1:end-1), ',')); + step_x = pre(5); + step_y = pre(8); + x_zero = pre(6); + y_zero = pre(9); + + % Calculate the y values + y = double(y_data)*step_y + y_zero; + n_points = length(y); + + % Calculate the x axis + x = linspace(x_zero, x_zero + step_x*(n_points-1), n_points); + + this.Trace.x = x; + this.Trace.y = y; + + triggerNewData(this); + end + + function acquireContinuous(this) + writeString(this, ':RUN'); + end + + function acquireSingle(this) + writeString(this, ':SINGLe'); + end + + function stopAcquisition(this) + writeString(this, ':STOP'); + end + + % Emulates the physical knob turning, works with nturns=+-1 + function turnKnob(this, knob, nturns) + switch upper(knob) + case 'HORZSCALE' + + % Timebase is changed + if nturns == -1 + this.time_scale = this.time_scale*2; + elseif nturns == 1 + this.time_scale = this.time_scale/2; + else + return + end + case {'VERTSCALE1', 'VERTSCALE2'} + + % Vertical scale is changed + n_ch = sscanf(upper(knob), 'VERTSCALE%i'); + tag = sprintf('scale%i', n_ch); + if nturns==-1 + this.(tag) = this.(tag)*2; + elseif nturns==1 + this.(tag) = this.(tag)/2; + else + return + end + end + end + end + + methods (Access = protected) + function createCommandList(this) + addCommand(this, 'channel', ':WAVeform:SOURce', ... + 'format', 'CHAN%i', ... + 'info', 'Channel from which the data is transferred'); + + addCommand(this, 'time_scale', ':TIMebase:SCALe',... + 'format', '%e',... + 'info', 'Time scale (s/div)'); + + addCommand(this, 'trig_lev', ':TRIGger:LEVel', ... + 'format', '%e'); + + % trigger slope - works, but incompatible with Tektronix + addCommand(this, 'trig_slope', ':TRIGger:SLOpe', ... + 'format', '%s', ... + 'value_list', {'NEGative', 'POSitive', 'EITHer', ... + 'ALTernate'}); + + addCommand(this, 'trig_source', ':TRIGger:SOUrce', ... + 'format', '%s', ... + 'value_list', {'CHAN1', 'CHAN2', 'CHAN3', 'CHAN4',... + 'EXT','LINE'}); + + % trigger mode + addCommand(this, 'trig_mode', ':TRIGger:SWEep', ... + 'format', '%s', ... + 'value_list', {'AUTO', 'NORMal'}); + + addCommand(this, 'acq_mode', ':ACQuire:TYPE', ... + 'format', '%s', ... + 'info', ['Acquisition mode: normal(sample), ', ... + 'high resolution or average'], ... + 'value_list', {'NORMal', 'AVERage', 'HRESolution', ... + 'PEAK'}); + + % Parametric commands + for i = 1:this.channel_no + i_str = num2str(i); + + addCommand(this, ... + ['cpl' i_str], [':CHANnel' i_str ':COUPling'], ... + 'format', '%s', ... + 'info', 'Channel coupling: AC, DC or GND', ... + 'value_list', {'AC','DC','GND'}); + + addCommand(this, ... + ['imp' i_str], [':CHANnel' i_str ':IMPedance'], ... + 'format', '%s', ... + 'info', 'Channel impedance: 1 MOhm or 50 Ohm', ... + 'value_list', {'FIFty','FIF','ONEMeg','ONEM'}); + + addCommand(this,... + ['offset' i_str], [':CHANnel' i_str ':OFFSet'], ... + 'format', '%e', ... + 'info', '(V)'); + + addCommand(this,... + ['scale' i_str], [':CHANnel' i_str ':SCAle'], ... + 'format', '%e', ... + 'info', 'Channel y scale (V/div)'); + + addCommand(this,... + ['enable' i_str], [':CHANnel' i_str ':DISPlay'], ... + 'format', '%b',... + 'info', 'Channel enabled'); + end + end + end +end \ No newline at end of file diff --git a/@MyAppColors/MyAppColors.m b/@MyAppColors/MyAppColors.m index 9cba18d..50e7068 100644 --- a/@MyAppColors/MyAppColors.m +++ b/@MyAppColors/MyAppColors.m @@ -1,156 +1,150 @@ % Set of colors indended to introduce some uniformity in GUIs classdef (Abstract) MyAppColors methods (Static) %% Predefined colors % Colors are represented by rgb triplets returned by static methods function rgb = warning() rgb = [0.93, 0.69, 0.13]; % Orange end function rgb = error() rgb = [1,0,0]; % Red end % Labview-style lamp indicator colors function rgb = lampOn() rgb = [0,1,0]; % Bright green end function rgb = lampOff() rgb = [0,0.4,0]; % Dark green end % Recolor app according to a new color scheme function applyScheme(Obj, scheme) persistent init_default default_main_color ... default_label_text_color default_edit_text_color ... default_edit_field_color default_axes_label_color if ~exist('scheme', 'var') scheme = 'default'; end switch lower(scheme) case 'dark' - main_color = [0.18, 0.19,0.57]; + main_color = [8, 62, 118]/255; % [0.18, 0.19,0.57] label_text_color = [1,1,1]; edit_text_color = [0,0,0]; edit_field_color = [1,1,1]; axes_label_color = [0.9,0.9,1]; case 'bright' main_color = [1,1,1]; label_text_color = [0,0,0.4]; edit_text_color = [0,0,0.]; edit_field_color = [1,1,1]; axes_label_color = [0,0,0]; case 'default' if isempty(init_default) % Create invisible components and read their % colors Uf = uifigure('Visible', false); Ef = uieditfield(Uf); Lbl = uilabel(Uf); Ax = axes(Uf); default_main_color = Uf.Color; default_label_text_color = Lbl.FontColor; default_edit_text_color = Ef.FontColor; default_edit_field_color = Ef.BackgroundColor; default_axes_label_color = Ax.XColor; delete(Uf); init_default = false; end main_color = default_main_color; label_text_color = default_label_text_color; edit_text_color = default_edit_text_color; edit_field_color = default_edit_field_color; axes_label_color = default_axes_label_color; otherwise error('Unknown scheme %s', scheme) end if isa(Obj, 'matlab.apps.AppBase') Fig = findFigure(Obj); MyAppColors.applyScheme(Fig, scheme); return end if ~isprop(Obj, 'Type') return end switch Obj.Type case 'figure' Obj.Color = main_color; case 'uitabgroup' % Nothing to do case 'uitab' Obj.ForegroundColor = edit_text_color; Obj.BackgroundColor = main_color; case 'uibutton' Obj.FontColor = label_text_color; Obj.BackgroundColor = main_color; case 'uistatebutton' Obj.FontColor = label_text_color; Obj.BackgroundColor = main_color; case 'uidropdown' Obj.FontColor = label_text_color; Obj.BackgroundColor = main_color; - case 'uieditfield' + case {'uieditfield', 'uispinner', 'uitextarea', 'uilistbox'} Obj.FontColor = edit_text_color; Obj.BackgroundColor = edit_field_color; case {'uilabel', 'uicheckbox', 'uiradiobutton'} Obj.FontColor = label_text_color; - case 'uilistbox' - Obj.FontColor = edit_text_color; - Obj.BackgroundColor = edit_field_color; - case 'uitextarea' - Obj.FontColor = edit_text_color; - Obj.BackgroundColor = edit_field_color; case {'uipanel', 'uibuttongroup'} Obj.ForegroundColor = label_text_color; Obj.BackgroundColor = main_color; case 'axes' try % This property is only present in uiaxes Obj.BackgroundColor = main_color; catch end Obj.XColor = axes_label_color; Obj.YColor = axes_label_color; Obj.GridColor = [0.15, 0.15, 0.15]; Obj.MinorGridColor = [0.15, 0.15, 0.15]; case 'uimenu' Obj.ForegroundColor = edit_text_color; case 'uicontrol' % The following switch case is for programmatically % created components switch Obj.Style case {'text', 'pushbutton', 'togglebutton'} Obj.ForegroundColor = label_text_color; Obj.BackgroundColor = main_color; case 'popupmenu' Obj.ForegroundColor = edit_text_color; Obj.BackgroundColor = edit_field_color; end end if isprop(Obj, 'Children') % Recolor children for i = 1:length(Obj.Children) MyAppColors.applyScheme(Obj.Children(i), scheme); end end end end end diff --git a/@MyDpo/MyDpo.m b/@MyDpo/MyDpo.m index f2dea4d..4e3558c 100644 --- a/@MyDpo/MyDpo.m +++ b/@MyDpo/MyDpo.m @@ -1,107 +1,109 @@ % Class for controlling 4-channel Tektronix DPO scopes. % Tested with DPO4034, DPO3034 classdef MyDpo < MyTekScope methods (Access = public) function this = MyDpo(varargin) this@MyTekScope(varargin{:}); % 2e7 is the maximum trace size of DPO4034-3034 %(10 mln point of 2-byte integers) this.Comm.InputBufferSize = 2.1e7; %byte - this.knob_list = lower({'GPKNOB1','GPKNOB2','HORZPos', ... - 'HORZScale, TRIGLevel','PANKNOB1', 'ZOOM', ... + this.knob_list = lower({'GPKNOB1', 'GPKNOB2', 'HORZPos', ... + 'HORZScale', 'TRIGLevel', 'PANKNOB1', 'ZOOM', ... 'VERTPOS1', 'VERTPOS2', 'VERTPOS3', 'VERTPOS4', ... 'VERTSCALE1', 'VERTSCALE2', 'VERTSCALE3', 'VERTSCALE4'}); createCommandList(this); end end methods (Access = protected) function createCommandList(this) addCommand(this, 'channel',':DATa:SOUrce', ... - 'format', 'CH%i', ... - 'info', 'Channel from which the trace is transferred', ... - 'value_list', {1, 2, 3, 4}); + 'format', 'CH%i', ... + 'info', ['Channel from which the trace ' ... + 'is transferred'], ... + 'value_list', {1, 2, 3, 4}); addCommand(this, 'ctrl_channel', ':SELect:CONTROl', ... - 'format', 'CH%i', ... - 'info', 'Channel currently selected in the scope display', ... - 'value_list', {1, 2, 3, 4}); + 'format', 'CH%i', ... + 'info', ['Channel currently selected in ' ... + 'the scope display'], ... + 'value_list', {1, 2, 3, 4}); addCommand(this, 'point_no', ':HORizontal:RECOrdlength', ... 'format', '%i', ... 'info', 'Numbers of points in the scope trace', ... 'value_list', {1000, 10000, 100000, 1000000, 10000000}); addCommand(this, 'time_scale', ':HORizontal:SCAle', ... 'format', '%e', ... 'info', 'Time scale (s/div)'); addCommand(this, 'trig_lev', ':TRIGger:A:LEVel',... 'format', '%e', ... 'info', '(V)'); addCommand(this, 'trig_slope', ':TRIGger:A:EDGE:SLOpe', ... 'format', '%s', ... 'value_list', {'RISe','FALL'}); addCommand(this, 'trig_source', ':TRIGger:A:EDGE:SOUrce', ... 'format', '%s', ... 'value_list', {'CH1','CH2','CH3','CH4', ... 'AUX','EXT','LINE'}); addCommand(this, 'trig_mode', ':TRIGger:A:MODe', ... 'format', '%s', ... 'value_list', {'AUTO', 'NORMal'}); addCommand(this, 'acq_state', ':ACQuire:STATE', ... 'format', '%b',... 'info', 'State of data acquisition by the scope'); addCommand(this, 'acq_mode', ':ACQuire:MODe', ... 'format', '%s', ... 'info', ['Acquisition mode: sample, peak ' ... 'detect, high resolution, average or envelope'], ... - 'value_list',{'SAMple', 'PEAKdetect', 'HIRes', ... + 'value_list', {'SAMple', 'PEAKdetect', 'HIRes', ... 'AVErage', 'ENVelope'}); % Parametric commands for i = 1:this.channel_no i_str = num2str(i); addCommand(this,... ['cpl',i_str],[':CH',i_str,':COUP'], ... 'format', '%s', ... 'info', 'Channel coupling: AC, DC or GND', ... 'value_list', {'DC','AC','GND'}); % impedances, 1MOhm or 50 Ohm addCommand(this,... ['imp', i_str], [':CH', i_str, ':IMPedance'],... 'format', '%s', ... 'info', 'Channel impedance: 1 MOhm or 50 Ohm', ... 'value_list', {'MEG', 'FIFty'}); % Offset addCommand(this, ... ['offset',i_str], [':CH',i_str,':OFFSet'], ... 'format', '%e', ... 'info', '(V)'); addCommand(this, ... ['scale',i_str], [':CH',i_str,':SCAle'], ... 'format', '%e', ... 'info', 'Channel y scale (V/div)'); addCommand(this,... ['enable',i_str], [':SEL:CH',i_str], ... 'format', '%b',... 'info', 'Channel enabled'); end end end end \ No newline at end of file diff --git a/@MyDso/MyDso.m b/@MyDso/MyDso.m deleted file mode 100644 index fa6cf1d..0000000 --- a/@MyDso/MyDso.m +++ /dev/null @@ -1,175 +0,0 @@ -% Class for controlling 4-channel Agilent DSO scopes. -% Tested with DSO7034A -classdef MyDso 1 warning('Multiple links found for the GUI element below.'); disp(Elem); return end updateElementByIndex(this, ind); end function addToCleanup(this, Obj) this.cleanup_list{end+1} = Obj; end end methods (Access = protected) function kernelDeletedCallback(this, ~, ~) % Switch off the AppBeingDeleted callback in order to prevent % an infinite loop this.Listeners.AppDeleted.Enabled = false; delete(this.App); delete(this); end function f = createPostSetCallback(this, Link) function postSetCallback(~,~) val = Link.getTargetFcn(); if ~isempty(Link.outputProcessingFcn) val = Link.outputProcessingFcn(val); end setIfChanged(Link.GuiElement, Link.gui_element_prop, val); % Optionally execute the update function defined within % the App if ~isempty(this.updateGuiFcn) this.updateGuiFcn(); end end f = @postSetCallback; end % Callback that is assigned to graphics elements as ValueChangedFcn function f = createValueChangedCallback(this, Link) function valueChangedCallback(~, ~) val = Link.GuiElement.Value; if ~isempty(Link.inputProcessingFcn) val = Link.inputProcessingFcn(val); end if ~isempty(Link.Listener) % Switch the listener off Link.Listener.Enabled = false; % Set the value Link.setTargetFcn(val); % Switch the listener on again Link.Listener.Enabled = true; else Link.setTargetFcn(val); end % Update non event based links updateAll(this); end f = @valueChangedCallback; end % MenuSelected callbacks are different from ValueChanged in that % the state needs to be toggled manually function f = createMenuSelectedCallback(this, Link) function menuSelectedCallback(~, ~) % Toggle the menu state if strcmpi(Link.GuiElement.Checked, 'on') Link.GuiElement.Checked = 'off'; val = 'off'; else Link.GuiElement.Checked = 'on'; val = 'on'; end if ~isempty(Link.inputProcessingFcn) val = Link.inputProcessingFcn(val); end if ~isempty(Link.Listener) % Switch the listener off Link.Listener.Enabled = false; % Set the value Link.setTargetFcn(val); % Switch the listener on again Link.Listener.Enabled = true; else Link.setTargetFcn(val); end % Update non event based links updateAll(this); end f = @menuSelectedCallback; end function f = createGetTargetFcn(~, Obj, prop_name, S) function val = refProp() val = Obj.(prop_name); end function val = subsrefProp() val = subsref(Obj, S); end if isempty(S) % Faster way to access property f = @refProp; else % More general way to access property S = [substruct('.', prop_name), S]; f = @subsrefProp; end end function f = createSetTargetFcn(~, Obj, prop_name, S) function assignProp(val) Obj.(prop_name) = val; end function subsasgnProp(val) Obj = subsasgn(Obj, S, val); end if isempty(S) % Faster way to assign property f = @assignProp; else % More general way to assign property S = [substruct('.', prop_name), S]; f = @subsasgnProp; end end % Update the value of one linked GUI element given the index of % corresponding link function updateElementByIndex(this, ind) Link = this.Links(ind); val = Link.getTargetFcn(); if ~isempty(Link.outputProcessingFcn) val = Link.outputProcessingFcn(val); end - + % Setting value to a matlab app elemen is time consuming, % so first check if the value has actually changed setIfChanged(Link.GuiElement, Link.gui_element_prop, val); end %% Subroutines of addLink % Parse input and create the base of Link structure function Link = createLinkBase(this, Elem, prop_ref, varargin) % Parse function inputs p = inputParser(); % GUI control element addRequired(p, 'Elem'); % Target to which the value of GUI element will be linked % relative to the App itself addRequired(p, 'prop_ref', @ischar); % Linked property of the GUI element (can be e.g. 'Color') addParameter(p, 'elem_prop', 'Value', @ischar); % If input_prescaler is given, the value assigned to the % instrument propery is related to the value x displayed in % GUI as x/input_presc. addParameter(p, 'input_prescaler', 1, @isnumeric); % Arbitrary processing functions can be specified for input and % output. outputProcessingFcn is applied to values before % assigning them to gui elements and in_proc_fcn is applied % before assigning to the linked properties. addParameter(p, 'outputProcessingFcn', [], ... @(f)isa(f,'function_handle')); addParameter(p, 'inputProcessingFcn', [], ... @(f)isa(f,'function_handle')); % Parameters relevant for uilamps addParameter(p, 'lamp_on_color', MyAppColors.lampOn(), ... @iscolor); addParameter(p, 'lamp_off_color', MyAppColors.lampOff(), ... @iscolor); % Option which allows converting a binary choice into a logical % value addParameter(p, 'map', {}, @this.validateMapArg); parse(p, Elem, prop_ref, varargin{:}); assert(all([this.Links.GuiElement] ~= p.Results.Elem), ... ['Another link for the same GUI element that is ' ... 'attempted to be linked to ' prop_ref ' already exists.']) % Create a new link structure Link = struct( ... 'reference', prop_ref, ... 'GuiElement', p.Results.Elem, ... 'gui_element_prop', p.Results.elem_prop, ... 'inputProcessingFcn', p.Results.inputProcessingFcn, ... 'outputProcessingFcn', p.Results.outputProcessingFcn, ... 'getTargetFcn', [], ... 'setTargetFcn', [], ... 'Listener', [] ... ); % Lamp indicators is a special case. It is often convenient to % make a lamp indicate on/off state. If a lamp is being linked % to a logical-type variable we therefore assign a dedicated % OutputProcessingFcn that puts logical values in % corresponcence with colors if strcmpi(Elem.Type, 'uilamp') Link.gui_element_prop = 'Color'; % Select between the on and off colors. Link.outputProcessingFcn = @(x)select(x, ... p.Results.lamp_on_color, p.Results.lamp_off_color); return end % Treat the special case of uimenus if strcmpi(Elem.Type, 'uimenu') Link.gui_element_prop = 'Checked'; end if ~ismember('map', p.UsingDefaults) ref_vals = p.Results.map{1}; gui_vals = p.Results.map{2}; % Assign input and output processing functions that convert % a logical value into one of the options and back Link.inputProcessingFcn = @(x)select( ... isequal(x, gui_vals{1}), ref_vals{:}); Link.outputProcessingFcn = @(x)select( ... isequal(x, ref_vals{1}), gui_vals{:}); end % Simple scaling is a special case of value processing % functions. if ~ismember('input_prescaler', p.UsingDefaults) if isempty(Link.inputProcessingFcn) && ... isempty(Link.outputProcessingFcn) Link.inputProcessingFcn = ... @(x) (x/p.Results.input_prescaler); Link.outputProcessingFcn = ... @(x) (x*p.Results.input_prescaler); else warning(['input_prescaler is ignored for target ' ... prop_ref 'as inputProcessingFcn or ' ... 'outputProcessingFcn has been already ' ... 'assigned instead.']); end end end function Link = extendMyInstrumentLink(~, Link, Instrument, tag) Cmd = Instrument.CommandList.(tag); % If supplied command does not have read permission, issue a % warning. if isempty(Cmd.readFcn) fprintf('Instrument property ''%s'' is nor readable\n', ... tag); % Try switching the color of the gui element to orange try Link.GuiElement.BackgroundColor = MyAppColors.warning(); catch try Link.GuiElement.FontColor = MyAppColors.warning(); catch end end end % Generate Items and ItemsData for dropdown menues if they were % not initialized manually if isequal(Link.GuiElement.Type, 'uidropdown') && ... isempty(Link.GuiElement.ItemsData) - if all(cellfun(@ischar, Cmd.value_list)) + str_value_list = cell(length(Cmd.value_list), 1); + + for i=1:length(Cmd.value_list) + if ischar(Cmd.value_list{i}) - % Capitalized displayed names for beauty - Link.GuiElement.Items = cellfun( ... - @(x)[upper(x(1)),lower(x(2:end))],... - Cmd.value_list, 'UniformOutput', false); - else + % Capitalized displayed names for beauty + str = Cmd.value_list{i}; + str_value_list{i} = [upper(str(1)), ... + lower(str(2:end))]; + else - % Items in a dropdown should be strings, so convert if - % necessary - str_value_list = cell(length(Cmd.value_list), 1); - - for i=1:length(Cmd.value_list) - if ~ischar(Cmd.value_list{i}) - str_value_list{i} = num2str(Cmd.value_list{i}); - end + % Items in a dropdown should be strings + str_value_list{i} = num2str(Cmd.value_list{i}); end - - Link.GuiElement.Items = str_value_list; end + + Link.GuiElement.Items = str_value_list; % Assign the list of unprocessed values as ItemsData Link.GuiElement.ItemsData = Cmd.value_list; end % Add tooltip if isprop(Link.GuiElement, 'Tooltip') && ... isempty(Link.GuiElement.Tooltip) Link.GuiElement.Tooltip = Cmd.info; end end % Decide what kind of callback (if any) needs to be created for % the GUI element. Options: 'ValueChangedFcn', 'MenuSelectedFcn' function callback_name = findElemCallbackType(~, ... Elem, elem_prop, Hobj, hobj_prop) % Check property attributes Mp = findprop(Hobj, hobj_prop); prop_write_accessible = strcmpi(Mp.SetAccess,'public') && ... (~Mp.Constant) && (~Mp.Abstract); % Check if the GUI element enabled and editable try gui_element_editable = strcmpi(Elem.Enable, 'on'); catch gui_element_editable = true; end % A check for editability is only meaningful for uieditfieds. % Drop-downs also have 'Editable' property, but it corresponds % to the editability of elements and should not have an effect % on assigning callbacks. if (strcmpi(Elem.Type, 'uinumericeditfield') || ... strcmpi(Elem.Type, 'uieditfield')) ... && strcmpi(Elem.Editable, 'off') gui_element_editable = false; end if ~(prop_write_accessible && gui_element_editable) callback_name = ''; return end % Do not create a new callback if one already exists (typically % it means that a callback was manually defined in AppDesigner) if strcmp(elem_prop, 'Value') && ... isprop(Elem, 'ValueChangedFcn') && ... isempty(Elem.ValueChangedFcn) % This is the most typical type of callback callback_name = 'ValueChangedFcn'; elseif strcmp(elem_prop, 'Checked') && ... strcmpi(Elem.Type, 'uimenu') && ... isempty(Elem.MenuSelectedFcn) % Callbacks for menus callback_name = 'MenuSelectedFcn'; else callback_name = ''; end end % Extract the top-most handle object in the reference, the end % property name and any further subreference function [Hobj, prop_name, Subs] = parseReference(this, prop_ref) % Make sure the reference starts with a dot and convert to % subreference structure if prop_ref(1)~='.' PropSubs = str2substruct(['.', prop_ref]); else PropSubs = str2substruct(prop_ref); end % Find the handle object to which the end property belongs as % well as the end property name Hobj = this.App; Subs = PropSubs; % Subreference relative to Hobj.(prop) prop_name = PropSubs(1).subs; for i=1:length(PropSubs)-1 testvar = subsref(this.App, PropSubs(1:end-i)); if isa(testvar, 'handle') Hobj = testvar; Subs = PropSubs(end-i+2:end); prop_name = PropSubs(end-i+1).subs; break end end end % Validate the value of 'map' optional argument in createLinkBase function validateMapArg(~, arg) try is_map_arg = iscell(arg) && length(arg)==2 && ... length(arg{1})==2 && length(arg{2})==2; catch is_map_arg = false; end assert(is_map_arg, ['The value must be a cell of the form ' ... '{{reference value 1, reference value 2}, ' ... '{GUI dispaly value 1, GUI dispaly value 2}}.']) end end end diff --git a/@MyInstrument/MyInstrument.m b/@MyInstrument/MyInstrument.m index 3360d65..621edad 100644 --- a/@MyInstrument/MyInstrument.m +++ b/@MyInstrument/MyInstrument.m @@ -1,274 +1,276 @@ % Generic instrument superclass % % Undefined/dummy methods: % queryString(this, cmd) % % These methods are intentionally not introduced as abstract as under % some conditions they are not necessary classdef MyInstrument < dynamicprops & matlab.mixin.CustomDisplay properties (Access = public) % Synchronize all properties after setting new value to one auto_sync = true end properties (SetAccess = protected, GetAccess = public) CommandList = struct() % identification string idn_str = '' end properties (Dependent = true) command_names end properties (Access = protected) % Copying existing metadata is much faster than creating a new one Metadata = MyMetadata.empty() + + % Logical variables that determine if writing to the instrument + % takes place when property is assigned new value + CommandWriteEnabled = struct() end methods (Access = public) function this = MyInstrument(varargin) P = MyClassParser(this); processInputs(P, this, varargin{:}); end % Read all parameters of the physical device function sync(this) read_ind = structfun(@(x) ~isempty(x.readFcn), ... this.CommandList); read_cns = this.command_names(read_ind); for i=1:length(read_cns) tag = read_cns{i}; read_value = this.CommandList.(tag).readFcn(); % Compare to the previous value and update if different. % Comparison prevents overhead for objects that listen to % the changes of property values. if ~isequal(this.CommandList.(tag).last_value, read_value) % Assign value without writing to the instrument - this.CommandList.(tag).Psl.Enabled = false; + this.CommandWriteEnabled.(tag) = false; this.(tag) = read_value; - this.CommandList.(tag).Psl.Enabled = true; + this.CommandWriteEnabled.(tag) = true; end end end function addCommand(this, tag, varargin) p = inputParser(); % Name of the command addRequired(p,'tag', @(x)isvarname(x)); % Functions for reading and writing the property value to the % instrument addParameter(p, 'readFcn', function_handle.empty(), ... @(x)isa(x, 'function_handle')); addParameter(p, 'writeFcn', function_handle.empty(), ... @(x)isa(x, 'function_handle')); % Function applied before writeFcn addParameter(p, 'validationFcn', function_handle.empty(), ... @(x)isa(x, 'function_handle')); % Function or list of functions executed after updating the % class property value addParameter(p, 'postSetFcn', function_handle.empty(), ... @(x)isa(x, 'function_handle')); addParameter(p, 'value_list', {}, @iscell); addParameter(p, 'default', 0); addParameter(p, 'info', '', @ischar); parse(p,tag,varargin{:}); assert(~isprop(this, tag), ['Property named ' tag ... ' already exists in the class.']); for fn = fieldnames(p.Results)' this.CommandList.(tag).(fn{1}) = p.Results.(fn{1}); end this.CommandList.(tag).info = ... toSingleLine(this.CommandList.(tag).info); vl = this.CommandList.(tag).value_list; if ~isempty(vl) && isempty(p.Results.validationFcn) this.CommandList.(tag).validationFcn = ... createListValidationFcn(this, vl); end % Assign default value from the list if not given explicitly if ~isempty(vl) && ismember('default', p.UsingDefaults) default = vl{1}; else default = p.Results.default; end % Create and configure a dynamic property H = addprop(this, tag); H.GetAccess = 'public'; H.SetObservable = true; H.SetMethod = createCommandSetFcn(this, tag); - % Assign the default value with post processing + % Assign the default value with post processing but without + % writing to the instrument + this.CommandWriteEnabled.(tag) = false; this.(tag) = default; + this.CommandWriteEnabled.(tag) = true; if ~isempty(this.CommandList.(tag).writeFcn) H.SetAccess = 'public'; else H.SetAccess = {'MyInstrument'}; end - - % Listener to PostSet event - this.CommandList.(tag).Psl = addlistener(this, tag, ... - 'PostSet', @this.commandPostSetCallback); end % Identification function [str, msg] = idn(this) assert(ismethod(this, 'queryString'), ['The instrument ' ... 'class must define queryString method in order to ' ... 'attempt identification.']) try str = queryString(this,'*IDN?'); catch ME str = ''; msg = ME.message; end this.idn_str = str; end % Measurement header function Mdt = readSettings(this) if isempty(this.Metadata) createMetadata(this); end % Ensure that instrument parameters are up to data sync(this); param_names = fieldnames(this.Metadata.ParamList); for i = 1:length(param_names) tag = param_names{i}; this.Metadata.ParamList.(tag) = this.(tag); end Mdt = copy(this.Metadata); end % Write settings from structure function writeSettings(this, Mdt) assert(isa(Mdt, 'MyMetadata'), ... 'Mdt must be of MyMetadata class.'); param_names = fieldnames(Mdt.ParamList); for i=1:length(param_names) tag = param_names{i}; if isprop(this, tag) this.(tag) = Mdt.ParamList.(tag); end end end end methods (Access = protected) function createMetadata(this) this.Metadata = MyMetadata('title', class(this)); % Add identification string addParam(this.Metadata, 'idn', this.idn_str); for i = 1:length(this.command_names) cmd = this.command_names{i}; addObjProp(this.Metadata, this, cmd, ... 'comment', this.CommandList.(cmd).info); end end % Create set methods for dynamic properties function f = createCommandSetFcn(~, tag) function commandSetFcn(this, val) % Validate new value vFcn = this.CommandList.(tag).validationFcn; if ~isempty(vFcn) vFcn(val); end - % Store unprocessed value for quick reference in the future - % and change tracking + % Store the unprocessed value for quick reference in + % the future and value change tracking this.CommandList.(tag).last_value = val; - - % Assign the value with post processing + + % Assign the value after post processing to the property pFcn = this.CommandList.(tag).postSetFcn; if ~isempty(pFcn) val = pFcn(val); end this.(tag) = val; + + if this.CommandWriteEnabled.(tag) + + % Write the new value to the instrument + this.CommandList.(tag).writeFcn(this.(tag)); + + if this.auto_sync + + % Confirm the changes by reading the state + sync(this); + end + end end f = @commandSetFcn; end function f = createListValidationFcn(~, value_list) function listValidationFcn(val) assert( ... any(cellfun(@(y) isequal(val, y), value_list)), ... ['Value must be one from the following list:', ... newline, var2str(value_list)]); end f = @listValidationFcn; end - % Post set function for dynamic properties - writing the new value - % to the instrument and optionally reading it back to confirm the - % change - function commandPostSetCallback(this, Src, ~) - tag = Src.Name; - - this.CommandList.(tag).writeFcn(this.(tag)); - - if this.auto_sync - sync(this); - end - end - % Overload a method of matlab.mixin.CustomDisplay in order to % modify the display of object. This serves two purposes % a) separate commands from other properties % b) order commands in a systematic way function PrGroups = getPropertyGroups(this) cmds = this.command_names; % We separate the display of non-command properties from the % rest props = setdiff(properties(this), cmds); PrGroups = [matlab.mixin.util.PropertyGroup(props), ... matlab.mixin.util.PropertyGroup(cmds)]; end end %% Set and Get methods methods function val = get.command_names(this) val = fieldnames(this.CommandList); end function set.idn_str(this, str) this.idn_str = toSingleLine(str); end end end diff --git a/@MyLakeshore336/MyLakeshore336.m b/@MyLakeshore336/MyLakeshore336.m index 18158ad..19f8ca1 100644 --- a/@MyLakeshore336/MyLakeshore336.m +++ b/@MyLakeshore336/MyLakeshore336.m @@ -1,130 +1,131 @@ % Class communication with Lakeshore Model 336 temperature controller. classdef MyLakeshore336 < MyScpiInstrument & MyCommCont - properties (SetAccess = protected, GetAccess = public) + properties (GetAccess = public, ... + SetAccess = {?MyClassParser, ?MyLakeshore336}, SetObservable) % Temperature unit, K or C. This variable should be set % before the command list is created. temp_unit = 'K' end properties (Constant = true) % Correspondence lists for numeric codes in parameter values. % Indexing starts from 0. These lists are for information only. % Values for the output mode, out_mode_n(1). out_mode_list = {'Off', 'Closed loop PID', 'Zone', ... 'Open loop', 'Monitor out', 'Warmup supply'}; % Control input, out_mode_n(2). control_input_list = {'None', 'A', 'B', 'C', 'D'}; % Heater ranges heater12_range_list = {'Off','Low','Medium','High'}; heater34_range_list = {'Off','On'}; end methods (Access = public) function this = MyLakeshore336(varargin) this@MyCommCont(varargin{:}); createCommandList(this); end % Create temperature logger function Lg = createLogger(this, varargin) function temp = readTemperature() sync(this); temp = [this.temp_a,this.temp_b,this.temp_c,this.temp_d]; end Lg = MyLogger(varargin{:}, 'MeasFcn', @readTemperature); % Make column headers inp_ch = {'A', 'B', 'C', 'D'}; headers = cell(1, 4); for i = 1:length(inp_ch) sens_name = sprintf('sens_name_%s', lower(inp_ch{i})); headers{i} = sprintf('T ch %s %s (%s)', inp_ch{i}, ... this.(sens_name), this.temp_unit); end if isempty(Lg.Record.data_headers) Lg.Record.data_headers = headers; end end end methods (Access = protected) function createCommandList(this) % Commands for the input channels inp_ch = {'A', 'B', 'C', 'D'}; for i = 1:4 nch = inp_ch{i}; addCommand(this, ['sens_name_' lower(nch)], 'INNAME', ... 'read_ending', ['? ' nch], ... 'write_ending', [' ' nch ',%s'], ... 'info', ['Sensor name channel ' nch]); info = sprintf('Reading channel %s (%s)', nch, ... this.temp_unit); addCommand(this, ['temp_' lower(nch)], ... [this.temp_unit 'RDG'], ... 'format', '%e', ... 'access', 'r', ... 'read_ending', ['? ' nch], ... 'info', info); end % Commands for the output channels for i = 1:4 nch = num2str(i); addCommand(this, ['setp_' nch], 'SETP', ... 'read_ending', ['? ' nch], ... 'write_ending', [' ' nch ',%e'], ... 'info', ['Output ' nch ' PID setpoint in ' ... 'preferred units of the sensor']); addCommand(this, ['out_mode_' nch], 'OUTMODE', ... 'read_ending', ['? ' nch], ... 'write_ending', [' ' nch ',%i,%i,%i'], ... 'info', ['Output ' nch ' settings: ' ... '[mode, cntl_input, powerup_enable]'], ... 'default', [0, 0, 0]); if i==1 || i==2 % Outputs 1 and 2 have finer range control than than 3 % and 4 addCommand(this, ['range_' nch], 'RANGE', ... 'read_ending', ['? ' nch], ... 'write_ending', [' ' nch ',%i'], ... 'info', ['Output ' nch ' range ' ... '0/1/2/3 -> off/low/medium/high'], ... 'value_list', {0, 1, 2, 3}); else addCommand(this, ['range_' nch], 'RANGE', ... 'read_ending', ['? ' nch], ... 'write_ending', [' ' nch ',%i'], ... 'info', ['Output ' nch ' range ' ... '0/1 -> off/on'], ... 'value_list', {0, 1}); end end end end methods function set.temp_unit(this, val) assert(strcmpi(val,'K') || strcmpi(val,'C'), ... 'Temperature unit must be K or C.') this.temp_unit = upper(val); end end end diff --git a/@MyLog/MyLog.m b/@MyLog/MyLog.m index 8d44d04..2bc79de 100644 --- a/@MyLog/MyLog.m +++ b/@MyLog/MyLog.m @@ -1,858 +1,865 @@ % Class to store data versus time. % Data can be continuously appended and saved. It is possible to add % labels (time marks) for particular moments in time. Data can be saved % and plotted with the time marks. % Metadata for this class is stored independently. % If instantiated as MyLog(load_path) then % the content is loaded from file classdef MyLog < matlab.mixin.Copyable properties (Access = public, SetObservable = true) % Save time as posixtime up to ms precision time_fmt = '%14.3f' % Save data as reals with up to 14 decimal digits. Trailing zeros % are removed by %g data_fmt = '%.14g' % Data column and line separators column_sep = '\t' line_sep = '\r\n' % File extension that is appended by default when saving the log % if a different one is not specified explicitly data_file_ext = '.log' % File extension for metadata meta_file_ext = '.meta' % Formatting options for the metadata metadata_opts = {} file_name = '' % Used to save or load the data data_headers = {} % Cell array of column headers length_lim = Inf % Keep the log length below this limit % Format for string representation of timestamps datetime_fmt = 'yyyy-MMM-dd HH:mm:ss' save_cont = false % If true changes are continuously saved end properties (GetAccess = public, SetAccess = protected, ... SetObservable = true) timestamps % Times at which data was aqcuired data % Array of measurements % Structure array that stores labeled time marks TimeLabels = struct( ... 'time', {}, ... % datetime object 'time_str', {}, ... % time in text format 'text_str', {}); % message string % Structure array that stores all the axes the log is plotted in PlotList = struct( ... 'Axes', {}, ... % axes handles 'DataLines',{}, ... % data line handles 'LbLines', {}, ... % label line handles 'BgLines', {}); % label line background handles % First timestamp that was saved in the current file. This value % is used to decide which time labels to retain under when the log % data is being trimmed. FirstSaveTime = datetime.empty() end properties (Dependent = true) channel_no % Number of data colums data_line_fmt % Format specifier for one data row to be printed column_headers % Time column header + data column headers data_file_name % File name with extension for data saving meta_file_name % File name with extension for metadata saving timestamps_num % Timestamps converted to numeric format end methods (Access = public) function this = MyLog(varargin) P = MyClassParser(this); processInputs(P, this, varargin{:}); end % save the entire data record function save(this, filename) % File name can be either supplied explicitly or given as the % file_name property if nargin() < 2 filename = this.file_name; else this.file_name = filename; end assert(~isempty(filename), 'File name is not provided.'); datfname = this.data_file_name; stat = createFile(datfname); if ~stat return end fid = fopen(datfname, 'w'); % Write column headers str = printDataHeaders(this); fprintf(fid, '%s', str); % Write the bulk of data fmt = this.data_line_fmt; for i = 1:length(this.timestamps) fprintf(fid, fmt, this.timestamps_num(i), this.data(i,:)); end fclose(fid); this.FirstSaveTime = this.timestamps(1); % Save time labels in a separate file saveMetadata(this); end %% Plotting % Plot the log data with time labels. Reurns plotted line objects. function Pls = plot(this, varargin) [~, ncols] = size(this.data); p = inputParser(); % Axes in which log should be plotted addOptional(p, 'Axes', [], @(x)assert( ... isa(x,'matlab.graphics.axis.Axes')||... isa(x,'matlab.ui.control.UIAxes'),... 'Argument must be axes or uiaxes.')); % If time labels are to be displayed addParameter(p, 'time_labels', true, @islogical); % If legend is to be displayed addParameter(p, 'legend', true, @islogical); % Logical vector defining the data columns to be displayed addParameter(p, 'isdisp', true(1,ncols), @(x) assert(... islogical(x) && isvector(x) && length(x)==ncols, ... ['''isdisp'' must be a logical vector of the size ',... 'equal to the number of data columns.'])); - % If 'reset' is true than all the data lines and time labels are - % re-plotted with default style even if they are already present - % in the plot - addParameter(p, 'reset', false, @islogical); - parse(p, varargin{:}); if ~isempty(p.Results.Axes) Axes = p.Results.Axes; else Axes = gca(); end % Find out if the log was already plotted in these axes. If % not, appned Ax to the PlotList. ind = findPlotInd(this, Axes); if isempty(ind) l = length(this.PlotList); this.PlotList(l+1).Axes = Axes; ind = l+1; end % Plot data if isempty(this.PlotList(ind).DataLines) % If the log was never plotted in Ax, % plot using default style and store the line handles Pls = line(Axes, this.timestamps, this.data); this.PlotList(ind).DataLines = Pls; else % Replace existing data Pls = this.PlotList(ind).DataLines; - for i=1:length(Pls) - Pls(i).XData = this.timestamps; - Pls(i).YData = this.data(:,i); + for i = 1:length(Pls) + + % Set new data and timestamps at the same time to + % prevent a conflict of array sizes + set(Pls(i), ... + 'XData', this.timestamps, ... + 'YData', this.data(:,i)); end end % Set the visibility of lines if ~ismember('isdisp', p.UsingDefaults) - for i=1:ncols + for i = 1:ncols Pls(i).Visible = p.Results.isdisp(i); end end if p.Results.time_labels % Plot time labels - plotTimeLabels(this, Axes); + plotTimeLabels(this, ind); else % Hide existing time labels try set(this.PlotList(ind).LbLines, 'Visible', 'off'); set(this.PlotList(ind).BgLines, 'Visible', 'off'); - catch + catch ME + warning(ME.message) end end - if (p.Results.legend)&&(~isempty(this.data_headers))&&... - (~isempty(this.data)) + % Add legend + if isempty(Axes.Legend) + legend(Axes, 'Location', 'southwest'); + end - % Add legend only for for those lines that are displayed - disp_ind = cellfun(@(x)strcmpi(x,'on'),{Pls.Visible}); - legend(Axes, Pls(disp_ind), this.data_headers{disp_ind},... - 'Location','southwest'); + if p.Results.legend && ~isempty(this.data_headers) && ... + ~isempty(this.data) + + % Display the legend + Axes.Legend.Visible = 'on'; + Axes.Legend.String = this.data_headers; + + % Include only those lines that are visible + for i = 1:ncols + Pls(i).Annotation.LegendInformation.IconDisplayStyle = ... + Pls(i).Visible; + end + else + + % Hide the legend + Axes.Legend.Visible = 'off'; end end % Append data point to the log function appendData(this, Time, val) % Format checks on the input data assert(isa(Time, 'datetime') || isnumeric(Time),... ['''time'' argument must be numeric or ',... 'of the class datetime.']); assert(isrow(val) && isnumeric(val), ... '''val'' argument must be a numeric row vector.'); if ~isempty(this.data) [~, ncols] = size(this.data); assert(length(val) == ncols,['Length of ''val'' ' ... 'does not match the number of data columns']); end % Ensure time format if isa(Time,'datetime') Time.Format = this.datetime_fmt; end % Append new data and time stamps this.timestamps = [this.timestamps; Time]; this.data = [this.data; val]; % Ensure the log length is within the length limit trim(this); % Optionally save the new data point to file if this.save_cont try if exist(this.data_file_name, 'file') == 2 % Otherwise open for appending fid = fopen(this.data_file_name, 'a'); else % If the file does not exist, create it and write % the column headers createFile(this.data_file_name); fid = fopen(this.data_file_name, 'w'); str = printDataHeaders(this); fprintf(fid, '%s', str); end % Convert the new timestamps to numeric form for saving if isa(Time, 'datetime') time_num = posixtime(Time); else time_num = Time; end % Append new data points to file fprintf(fid, this.data_line_fmt, time_num, val); fclose(fid); if isempty(this.FirstSaveTime) this.FirstSaveTime = Time; end catch warning(['Logger cannot save data at time = ',... datestr(datetime('now', ... 'Format', this.datetime_fmt))]); % Try closing fid in case it is still open try fclose(fid); catch end end end end %% Time labels % Add label function addTimeLabel(this, varargin) p = inputParser(); addParameter(p, 'Time', datetime('now'), ... @(x)assert(isa(x, 'datetime'), ['''Time'' must be of ' ... 'the type datetime.'])) addParameter(p, 'text', '', ... @(x)assert(iscellstr(x) || ischar(x) || isstring(x), ... '''text'' must be a string or cell array of strings.')) % If 'index' is specified, then the function configures an % existing time label instead of adding a new one addParameter(p, 'index', [], ... @(x)assert(floor(x)==x && x<=length(this.TimeLabels), ... ['Index must be a positive integer not exceeding the ' ... 'number of time labels.'])) parse(p, varargin{:}) Time = p.Results.Time; Time.Format = this.datetime_fmt; if ismember('text', p.UsingDefaults) % Invoke a dialog to input label time and name if ismember('index', p.UsingDefaults) % Default dialog field values dlg_args = {'', datestr(Time)}; else % Provide existing values for modification Tlb = this.TimeLabels(p.Results.index); dlg_args = {char(Tlb.text_str), Tlb.time_str}; end answ = inputdlg({'Label text', 'Time'},'Add time label',... [2 40; 1 40], dlg_args); % If 'Cancel' button is pressed or no input is provided, do % not modify or add a new label if isempty(answ) || isempty(answ{1}) return end % Convert the inputed value to datetime with proper format Time = datetime(answ{2}, 'Format', this.datetime_fmt); % Store multiple lines as cell array str = cellstr(answ{1}); else str = cellstr(p.Results.text); end if ~isempty(this.timestamps) && (Time < this.timestamps(1)) warning(['The time of label ''%s'' is earlier than ' ... 'the first timestamp in memory. Such time label ' ... 'will not be plotted but will be saved in ' ... 'metadata.'], str{1}); end if ~isempty(this.FirstSaveTime) && (Time < this.FirstSaveTime) warning(['The time of label ''%s'' is earlier than ' ... 'the first timestamp in the current data file. ' ... 'Such time label will not be automatically saved ' ... 'in metadata.'], str{1}); end % Find the index of time label. We do not supply a default % value for the end of the list in case the length was changed % over the course of function execution. if ismember('index', p.UsingDefaults) ind = length(this.TimeLabels)+1; else ind = p.Results.index; end this.TimeLabels(ind).time = Time; this.TimeLabels(ind).time_str = datestr(Time); this.TimeLabels(ind).text_str = str; % Order time labels by ascending time sortTimeLabels(this); if this.save_cont saveMetadata(this); end end function deleteTimeLabel(this, ind) this.TimeLabels(ind) = []; if this.save_cont saveMetadata(this); end end % Show the list of labels in a readable format function lst = printTimeLabels(this) if isempty(this.TimeLabels) lst = {}; return end if ~isempty(this.timestamps) % Select only those time labels that are within % the interval of time of currently stored data t_ind = ([this.TimeLabels.time] >= this.timestamps(1)); Tl = this.TimeLabels(t_ind); else Tl = this.TimeLabels; end % The returned output is a list of character strings lst = cell(length(Tl), 1); for i = 1:length(Tl) if ~isempty(Tl(i).text_str) % Display the first line of label lbl = Tl(i).text_str{1}; else lbl = ''; end lst{i} = [Tl(i).time_str, ' ', lbl]; end end %% Misc public functions % Clear log data and time labels function clear(this) % Clear while preserving the array types this.TimeLabels(:) = []; % Delete all the data lines and time labels for i=1:length(this.PlotList) delete(this.PlotList(i).DataLines); delete(this.PlotList(i).LbLines); delete(this.PlotList(i).BgLines); end this.PlotList(:) = []; % Clear data and its type this.timestamps = []; this.data = []; % Switch off continuous saving to prevent the overwriting of % previously saved data this.save_cont = false; end % Convert the log channel record to trace function Trace = toTrace(this, varargin) p = inputParser(); addParameter(p, 'channel', 1, ... @(x)assert(x>0 && floor(x)==x && x<=this.channel_no, ... ['Channel number must be an integer between 1 and ' ... num2str(this.channel_no) '.'])); addParameter(p, 'index', []); % If false, the beginning of x data in the trace is shifted to % zero addParameter(p, 'absolute_time', false, @islogical); parse(p, varargin{:}); n_ch = p.Results.channel; Trace = MyTrace(); if ismember('index', p.UsingDefaults) Trace.x = this.timestamps_num; Trace.y = this.data(:, n_ch); else Trace.x = this.timestamps_num(p.Results.index); Trace.y = this.data(p.Results.index, n_ch); end Trace.name_x = 'Time'; Trace.unit_x = 's'; if ~p.Results.absolute_time % Shift the beginning of data to zero Trace.x = Trace.x-Trace.x(1); end if length(this.data_headers) >= n_ch % Try extracting the name and unit from the column header. % The regexp will match anything of the format 'x (y)' with % any types of brackets, (), [] or {}. tokens = regexp(this.data_headers{n_ch}, ... '(.*)[\(\[\{](.*)[\)\]\}]', 'tokens'); if ~isempty(tokens) % Removes leading and trailing whitespaces tokens = strtrim(tokens{1}); Trace.name_y = tokens{1}; Trace.unit_y = tokens{2}; else Trace.name_y = this.data_headers{n_ch}; end end end end methods (Access = public, Static = true) % Load log from file. Formatting parameters can be supplied as % varargin function L = load(filename, varargin) assert(exist(filename, 'file') == 2, ... ['File ''', filename, ''' is not found.']) L = MyLog(varargin{:}); L.file_name = filename; % Load metadata if file is found if exist(L.meta_file_name, 'file') == 2 Mdt = MyMetadata.load(L.meta_file_name,L.metadata_opts{:}); setMetadata(L, Mdt); else disp(['Log metadata file is not found, continuing ' ... 'without it.']); end % Read column headers from the data file fid = fopen(filename,'r'); dat_col_heads = strsplit(fgetl(fid), L.column_sep, ... 'CollapseDelimiters', true); fclose(fid); % Assign column headers if length(dat_col_heads) > 1 L.data_headers = dat_col_heads(2:end); end % Read data as delimiter-separated values and convert to cell % array, skip the first line containing column headers fulldata = dlmread(filename, L.column_sep, 1, 0); L.data = fulldata(:, 2:end); L.timestamps = fulldata(:, 1); % Convert time stamps to datetime if the time column header % is 'posixtime' if ~isempty(dat_col_heads) && ... contains(dat_col_heads{1}, 'posix', 'IgnoreCase', true) L.timestamps = datetime(L.timestamps, ... 'ConvertFrom', 'posixtime', 'Format', L.datetime_fmt); end L.FirstSaveTime = L.timestamps(1); end end - methods (Access = protected) - function plotTimeLabels(this, Axes) - - % Find out if the log was already plotted in these axes - ind = findPlotInd(this, Axes); - if isempty(ind) - l = length(this.PlotList); - this.PlotList(l+1).Axes = Axes; - ind = l+1; - else - Axes = this.PlotList(ind).Axes; - end + methods (Access = protected) + + % Add time labels on the plot given the plot index in PlotList + function plotTimeLabels(this, ind) + Axes = this.PlotList(ind).Axes; if ~isempty(this.timestamps) && ~isempty(this.TimeLabels) % Select for plotting only those time labels that are within % the interval of time of currently stored data t_ind = ([this.TimeLabels.time] >= this.timestamps(1)); Tl = this.TimeLabels(t_ind); else Tl = this.TimeLabels; end % Plot labels for i = 1:length(Tl) T = Tl(i); n_lines = max(length(T.text_str), 1); try Lbl = this.PlotList(ind).LbLines(i); % Update the existing label line Lbl.Value = T.time; Lbl.Label = T.text_str; Lbl.Visible = 'on'; % Update the background width - font size times the % number of lines Bgl = this.PlotList(ind).BgLines(i); Bgl.Value = T.time; Bgl.LineWidth = Lbl.FontSize*n_lines; Bgl.Visible = 'on'; catch % Add new background line this.PlotList(ind).BgLines(i) = xline(Axes, T.time, ... 'LineWidth', 10*n_lines, ... 'Color', [1, 1, 1]); % Add new label line this.PlotList(ind).LbLines(i) = xline(Axes, T.time, ... '-', T.text_str, ... 'LineWidth', 0.5, ... 'LabelHorizontalAlignment', 'center', ... 'FontSize', 10); end end % Remove redundant markers if any n_tlbl = length(Tl); if length(this.PlotList(ind).LbLines) > n_tlbl delete(this.PlotList(ind).LbLines(n_tlbl+1:end)); this.PlotList(ind).LbLines(n_tlbl+1:end) = []; end if length(this.PlotList(ind).BgLines) > n_tlbl delete(this.PlotList(ind).BgLines(n_tlbl+1:end)); this.PlotList(ind).BgLines(n_tlbl+1:end) = []; end end % Ensure the log length is within length limit function trim(this) len = length(this.timestamps); if len <= this.length_lim return end % Remove data points beyond the length limit dn = len-this.length_lim; this.timestamps(1:dn) = []; this.data(1:dn, :) = []; if isempty(this.TimeLabels) return end % Remove only the time labels which times fall outside the % range of both a) trimmed data b) data in the current file BeginTime = min(this.timestamps(1), this.FirstSaveTime); ind = ([this.TimeLabels.time] < BeginTime); this.TimeLabels(ind) = []; end % Print column names to a string function str = printDataHeaders(this) cs = this.column_sep; str = sprintf(['%s',cs], this.column_headers{:}); str = [str, sprintf(this.line_sep)]; end % Find out if the log was already plotted in the axes Ax and return % the corresponding index of PlotList if it was function ind = findPlotInd(this, Ax) assert(isvalid(Ax),'Ax must be valid axes or uiaxes') if ~isempty(this.PlotList) ind_b = ([this.PlotList.Axes] == Ax); % Find the index of first match ind = find(ind_b, 1); else ind = []; end end % Re-order the elements of TimeLabels array so that labels % corresponding to later times have larger index function sortTimeLabels(this) times = [this.TimeLabels.time]; [~,ind] = sort(times); this.TimeLabels = this.TimeLabels(ind); end % Create metadata from log properties function Mdt = getMetadata(this) if ~isempty(this.FirstSaveTime) % Select for saving only those time labels that are within % the interval of time of data in the current file ind = ([this.TimeLabels.time] >= this.FirstSaveTime); Tl = this.TimeLabels(ind); else Tl = this.TimeLabels; end if ~isempty(Tl) % Add the textual part of TimeLabels structure Mdt = MyMetadata(this.metadata_opts{:}, ... 'title', 'TimeLabels'); Lbl = struct('time_str', {Tl.time_str}, ... 'text_str', {Tl.text_str}); addParam(Mdt, 'Lbl', Lbl); else Mdt = MyMetadata.empty(); end end % Save log metadata, owerwriting existing function saveMetadata(this, Mdt) if exist('Mdt', 'var') == 0 Mdt = getMetadata(this); end if isempty(Mdt) return end metfilename = this.meta_file_name; % Create or clear the file stat = createFile(metfilename, 'overwrite', true); if ~stat return end save(Mdt, metfilename); end % Process metadata function setMetadata(this, Mdt) % Assign time labels Tl = titleref(Mdt, 'TimeLabels'); if ~isempty(Tl) Lbl = Tl.ParamList.Lbl; for i=1:length(Lbl) this.TimeLabels(i).time_str = Lbl(i).time_str; this.TimeLabels(i).time = datetime(Lbl(i).time_str, ... 'Format', this.datetime_fmt); this.TimeLabels(i).text_str = Lbl(i).text_str; end end end end %% Set and get methods methods function set.length_lim(this, val) assert(isreal(val),'''length_lim'' must be a real number'); % Make length_lim non-negative and integer this.length_lim = max(0, round(val)); % Apply the new length limit to log trim(this); end function set.data_headers(this, val) assert(iscellstr(val) && isrow(val), ['''data_headers'' must '... 'be a row cell array of character strings.']) %#ok this.data_headers = val; end function set.save_cont(this, val) this.save_cont = logical(val); end function set.file_name(this, val) assert(ischar(val),'''file_name'' must be a character string.') if ~isequal(this.file_name, val) % Reset the first saved timestamp marker this.FirstSaveTime = datetime.empty(); %#ok end this.file_name = val; end % The get function for file_name adds extension if it is not % already present and also ensures proper file separators % (by splitting and combining the file name) function fname = get.data_file_name(this) fname = this.file_name; [filepath,name,ext] = fileparts(fname); if isempty(ext) ext = this.data_file_ext; end fname = fullfile(filepath,[name,ext]); end function fname = get.meta_file_name(this) fname = this.file_name; [filepath,name,~] = fileparts(fname); ext = this.meta_file_ext; fname = fullfile(filepath,[name,ext]); end function data_line_fmt = get.data_line_fmt(this) cs = this.column_sep; nl = this.line_sep; if isempty(this.data) l = 0; else [~,l] = size(this.data); end data_line_fmt = this.time_fmt; for i = 1:l data_line_fmt = [data_line_fmt, cs, this.data_fmt]; %#ok end data_line_fmt = [data_line_fmt, nl]; end function hdrs = get.column_headers(this) % Add header for the time column if isa(this.timestamps,'datetime') time_title_str = 'POSIX time (s)'; else time_title_str = 'Time'; end hdrs = [time_title_str, this.data_headers]; end function time_num_arr = get.timestamps_num(this) % Convert time stamps to numbers if isa(this.timestamps, 'datetime') time_num_arr = posixtime(this.timestamps); else time_num_arr = this.timestamps; end end function val = get.channel_no(this) if isempty(this.data) val = length(this.data_headers); else [~, val] = size(this.data); end end end end diff --git a/@MyLogger/MyLogger.m b/@MyLogger/MyLogger.m index 14dfcd2..58b66f6 100644 --- a/@MyLogger/MyLogger.m +++ b/@MyLogger/MyLogger.m @@ -1,236 +1,241 @@ % 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 + % 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 ([^(?:_\d)]*)', ... - 'tokens'); + '\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 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') + assert(isa(val, 'timer'), ... + '''MeasTimer'' must be a timer object') this.MeasTimer = val; end + + function set.FileCreationInterval(this, val) + assert(isa(val, 'duration'), ['''FileCreationInterval'' ' ... + 'must be a duration object.']) + this.FileCreationInterval = val; + end end end diff --git a/@MyNewportUsbComm/MyNewportUsbComm.m b/@MyNewportUsbComm/MyNewportUsbComm.m index 97dd250..f58bbdd 100644 --- a/@MyNewportUsbComm/MyNewportUsbComm.m +++ b/@MyNewportUsbComm/MyNewportUsbComm.m @@ -1,77 +1,78 @@ 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() 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 + if stat == 0 str = char(ToString(this.QueryData)); else - str=''; + str = ''; warning('Query to Newport usb driver was unsuccessful.'); end - this.isbusy=false; + 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 new instance of NewportUsbComm') + 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/@MyScpiInstrument/MyScpiInstrument.m b/@MyScpiInstrument/MyScpiInstrument.m index a60d16d..160fcff 100644 --- a/@MyScpiInstrument/MyScpiInstrument.m +++ b/@MyScpiInstrument/MyScpiInstrument.m @@ -1,358 +1,365 @@ % Class featuring a specialized framework for instruments supporting SCPI % % Undefined/dummy methods: % queryString(this, cmd) % writeString(this, cmd) -% createCommandList(this) classdef MyScpiInstrument < MyInstrument methods (Access = public) % Extend the functionality of base class method function addCommand(this, tag, command, varargin) p = inputParser(); p.KeepUnmatched = true; addRequired(p, 'command', @ischar); addParameter(p, 'access', 'rw', @ischar); addParameter(p, 'format', '%e', @ischar); addParameter(p, 'value_list', {}, @iscell); addParameter(p, 'validationFcn', function_handle.empty(), ... @(x)isa(x, 'function_handle')); addParameter(p, 'default', 0); % Command ending for reading addParameter(p, 'read_ending', '?', @ischar); % Command ending for writing, e.g. '%10e' addParameter(p, 'write_ending', '', @ischar); parse(p, command, varargin{:}); % Create a list of remaining parameters to be supplied to % the base class method sub_varargin = struct2namevalue(p.Unmatched); % Introduce variables for brevity format = p.Results.format; write_ending = p.Results.write_ending; if ismember('format', p.UsingDefaults) && ... ~ismember('write_ending', p.UsingDefaults) % Extract format specifier and symbol from the write ending [smb, format] = parseFormat(this, write_ending); else % Extract format symbol smb = parseFormat(this, format); end if ismember('b', smb) % '%b' is a non-MATLAB format specifier that is introduced % to designate logical variables format = replace(format, '%b', '%i'); write_ending = replace(write_ending, '%b', '%i'); end this.CommandList.(tag).format = format; % Add the full read form of the command, e.g. ':FREQ?' if contains(p.Results.access, 'r') read_command = [p.Results.command, p.Results.read_ending]; readFcn = ... @()sscanf(queryString(this, read_command), format); sub_varargin = [sub_varargin, {'readFcn', readFcn}]; else read_command = ''; end this.CommandList.(tag).read_command = read_command; % Add the full write form of the command, e.g. ':FREQ %e' if contains(p.Results.access,'w') if ismember('write_ending', p.UsingDefaults) write_command = [p.Results.command, ' ', format]; else write_command = [p.Results.command, write_ending]; end writeFcn = ... @(x)writeString(this, sprintf(write_command, x)); sub_varargin = [sub_varargin, {'writeFcn', writeFcn}]; else write_command = ''; end this.CommandList.(tag).write_command = write_command; % If the value list contains textual values, extend it with % short forms and add a postprocessing function value_list = p.Results.value_list; validationFcn = p.Results.validationFcn; if ~isempty(value_list) if any(cellfun(@ischar, value_list)) % Put only unique full-named values in the value list [long_vl, short_vl] = splitValueList(this, value_list); value_list = long_vl; % For validation, use an extended list made of full and % abbreviated name forms and case-insensitive % comparison validationFcn = createScpiListValidationFcn(this, ... [long_vl, short_vl]); postSetFcn = createToStdFormFcn(this, tag, long_vl); sub_varargin = [sub_varargin, ... {'postSetFcn', postSetFcn}]; end end % Assign validation function based on the value format if isempty(validationFcn) validationFcn = createArrayValidationFcn(this, smb); end sub_varargin = [sub_varargin, { ... 'value_list', value_list, ... 'validationFcn', validationFcn}]; % Assign default based on the format of value if ~ismember('default', p.UsingDefaults) default = p.Results.default; elseif ~isempty(value_list) default = value_list{1}; else - default = makeValidValue(this, smb); + default = createValidValue(this, smb); end sub_varargin = [sub_varargin, {'default', default}]; % Execute the base class method addCommand@MyInstrument(this, tag, sub_varargin{:}); end % Redefine the base class method to use a single read operation for % faster communication function sync(this) cns = this.command_names; ind_r = structfun(@(x) ~isempty(x.read_command), ... this.CommandList); read_cns = cns(ind_r); % List of names of readable commands read_commands = cellfun(... @(x) this.CommandList.(x).read_command, read_cns,... 'UniformOutput',false); res_list = queryStrings(this, read_commands{:}); if length(read_cns)==length(res_list) % Assign outputs to the class properties for i=1:length(read_cns) tag = read_cns{i}; val = sscanf(res_list{i}, ... this.CommandList.(tag).format); if ~isequal(this.CommandList.(tag).last_value, val) % Assign value without writing to the instrument - this.CommandList.(tag).Psl.Enabled = false; + this.CommandWriteEnabled.(tag) = false; this.(tag) = val; - this.CommandList.(tag).Psl.Enabled = true; + this.CommandWriteEnabled.(tag) = true; end end else warning(['Not all the properties could be read, ',... 'instrument class values are not updated.']); end end %% Write/query % These methods implement handling multiple SCPI commands. Unless % overloaded, they rely on write/readString methods for % communication with the device, which particular subclasses must % implement or inherit separately. % Write command strings listed in varargin function writeStrings(this, varargin) if ~isempty(varargin) % Concatenate commands and send to the device cmd_str = join(varargin,';'); cmd_str = cmd_str{1}; writeString(this, cmd_str); end end % Query commands and return the resut as cell array of strings function res_list = queryStrings(this, varargin) if ~isempty(varargin) % Concatenate commands and send to the device cmd_str = join(varargin,';'); cmd_str = cmd_str{1}; res_str = queryString(this, cmd_str); % Drop the end-of-the-string symbol and split res_list = split(deblank(res_str),';'); else res_list = {}; end end end methods (Access = protected) %% Misc utility methods % Split the list of string values into a full-form list and a % list of abbreviations, where the abbreviated forms are inferred % based on case. For example, the value that has the full name % 'AVErage' has the short form 'AVE'. function [long_vl, short_vl] = splitValueList(~, vl) - short_vl = {}; % Abbreviated forms + long_str_vl = {}; % Full forms of string values + short_vl = {}; % Abbreviated forms of string values + num_vl = {}; % Numeric values % Iterate over the list of values - for i=1:length(vl) + for i = 1:length(vl) % Short forms exist only for string values if ischar(vl{i}) + long_str_vl{end+1} = vl{i}; %#ok + + % Add short form idx = isstrprop(vl{i},'upper'); short_form = vl{i}(idx); if ~isequal(vl{i}, short_form) && ~isempty(short_form) - short_vl{end+1} = short_form; %#ok + short_vl{end+1} = short_form; %#ok end + else + num_vl{end+1} = vl{i}; %#ok end end % Remove duplicates short_vl = unique(lower(short_vl)); - % Make the list of full forms without reordering - long_vl = setdiff(lower(vl), short_vl, 'stable'); + % Make the list of full forms + long_vl = [num_vl, ... + setdiff(lower(long_str_vl), short_vl, 'stable')]; end % Create a function that returns the long form of value from % value_list function f = createToStdFormFcn(this, cmd, value_list) function std_val = toStdForm(val) % Standardization is applicable to char-valued properties % which have value list if isempty(value_list) || ~ischar(val) std_val = val; return end % find matching values n = length(val); ismatch = cellfun(@(x) strncmpi(val, x, ... min([n, length(x)])), value_list); assert(any(ismatch), ... sprintf(['%s is not present in the list of values ' ... 'of command %s.'], val, cmd)); % out of the matching values pick the longest mvals = value_list(ismatch); n_el = cellfun(@(x) length(x), mvals); std_val = mvals{n_el==max(n_el)}; end assert(ismember(cmd, this.command_names), ['''' cmd ... ''' is not an instrument command.']) f = @toStdForm; end % Find the format specifier symbol and options function [smb, format] = parseFormat(~, fmt_spec) [start, stop, tok] = regexp(fmt_spec, '%([\d\.]*)([a-z])', ... 'start', 'end', 'tokens'); assert(~isempty(tok) && ~isempty(tok{1}{2}), ... ['Format symbol is not found in ' fmt_spec]); % The first cell index corresponds to different matches if % there are more than one specifier smb = cellfun(@(x)x{2}, tok); % Return a substring that includes all the specifiers format = fmt_spec(min(start):max(stop)); end function createMetadata(this) createMetadata@MyInstrument(this); % Re-iterate the creation of command parameters to add the % format specifier for i = 1:length(this.command_names) cmd = this.command_names{i}; addObjProp(this.Metadata, this, cmd, ... 'comment', this.CommandList.(cmd).info, ... 'fmt_spec', this.CommandList.(cmd).format); end end % List validation function with case-insensitive comparison function f = createScpiListValidationFcn(~, value_list) function listValidationFcn(val) val = lower(val); assert( ... any(cellfun(@(y) isequal(val, y), value_list)), ... ['Value must be one from the following list:', ... newline, var2str(value_list)]); end f = @listValidationFcn; end % smb is an array of format specifier symbols function f = createArrayValidationFcn(~, smb) function validateNumeric(val) assert((length(val) == length(smb)) && isnumeric(val), ... ['Value must be a numeric array of length ' ... length(smb) '.']) end function validateInteger(val) assert((length(val) == length(smb)) && ... all(floor(val) == val), ['Value must be an ' ... 'integer array of length ' length(smb) '.']) end function validateLogical(val) assert((length(val) == length(smb)) && ... all(val==1 | val==0), ['Value must be a logical ' ... 'array of length ' length(smb) '.']) end function valudateCharacterString(val) assert(ischar(val), 'Value must be a character string.'); end % Determine the type of validation function if all(smb == 's' | smb == 'c') f = @valudateCharacterString; elseif all(smb == 'b') f = @validateLogical; elseif all(smb == 'b' | smb == 'i') f = @validateInteger; else f = @validateNumeric; end end - function val = makeValidValue(~, smb) + function val = createValidValue(~, smb) if all(smb == 's' | smb == 'c') val = ''; elseif all(smb == 'b') val = false(length(smb), 1); else val = zeros(length(smb), 1); end end end end diff --git a/@MyDpo/MyDpo.m b/@MyTekMdo/MyTekMdo.m similarity index 67% copy from @MyDpo/MyDpo.m copy to @MyTekMdo/MyTekMdo.m index f2dea4d..35c7c7e 100644 --- a/@MyDpo/MyDpo.m +++ b/@MyTekMdo/MyTekMdo.m @@ -1,107 +1,112 @@ -% Class for controlling 4-channel Tektronix DPO scopes. -% Tested with DPO4034, DPO3034 +% Control class for Tektronix MDO scopes, tested with MDO3034 -classdef MyDpo < MyTekScope +classdef MyTekMdo < MyTekScope methods (Access = public) - function this = MyDpo(varargin) + function this = MyTekMdo(varargin) this@MyTekScope(varargin{:}); - % 2e7 is the maximum trace size of DPO4034-3034 + % 2e7 is the maximum trace size of MDO3034 %(10 mln point of 2-byte integers) this.Comm.InputBufferSize = 2.1e7; %byte - this.knob_list = lower({'GPKNOB1','GPKNOB2','HORZPos', ... - 'HORZScale, TRIGLevel','PANKNOB1', 'ZOOM', ... + this.channel_no = 4; + this.knob_list = lower({'GPKNOB1', 'GPKNOB2', 'HORZPos', ... + 'HORZScale', 'TRIGLevel', 'PANKNOB1', 'ZOOM', ... 'VERTPOS1', 'VERTPOS2', 'VERTPOS3', 'VERTPOS4', ... 'VERTSCALE1', 'VERTSCALE2', 'VERTSCALE3', 'VERTSCALE4'}); createCommandList(this); end end methods (Access = protected) function createCommandList(this) - addCommand(this, 'channel',':DATa:SOUrce', ... - 'format', 'CH%i', ... - 'info', 'Channel from which the trace is transferred', ... - 'value_list', {1, 2, 3, 4}); + addCommand(this, 'channel', ':DATa:SOUrce', ... + 'format', 'CH%i', ... + 'info', ['Channel from which the trace is ' ... + 'transferred'], ... + 'value_list', {1, 2, 3, 4}); addCommand(this, 'ctrl_channel', ':SELect:CONTROl', ... - 'format', 'CH%i', ... - 'info', 'Channel currently selected in the scope display', ... - 'value_list', {1, 2, 3, 4}); + 'format', 'CH%i', ... + 'info', ['Channel currently selected in ' ... + 'the scope display'], ... + 'value_list', {1, 2, 3, 4}); addCommand(this, 'point_no', ':HORizontal:RECOrdlength', ... 'format', '%i', ... 'info', 'Numbers of points in the scope trace', ... - 'value_list', {1000, 10000, 100000, 1000000, 10000000}); + 'value_list', {1000, 10000, 100000, 1000000, 5000000, ... + 10000000}); addCommand(this, 'time_scale', ':HORizontal:SCAle', ... 'format', '%e', ... 'info', 'Time scale (s/div)'); - addCommand(this, 'trig_lev', ':TRIGger:A:LEVel',... + addCommand(this, 'trig_lev', ':TRIGger:A:LEVel',... % debug 'format', '%e', ... 'info', '(V)'); addCommand(this, 'trig_slope', ':TRIGger:A:EDGE:SLOpe', ... 'format', '%s', ... - 'value_list', {'RISe','FALL'}); + 'value_list', {'RISe', 'FALL', 'EITHer'}); addCommand(this, 'trig_source', ':TRIGger:A:EDGE:SOUrce', ... 'format', '%s', ... - 'value_list', {'CH1','CH2','CH3','CH4', ... - 'AUX','EXT','LINE'}); + 'value_list', {'CH1', 'CH2', 'CH3', 'CH4', ... + 'AUX', 'LINE', 'RF'}); addCommand(this, 'trig_mode', ':TRIGger:A:MODe', ... 'format', '%s', ... 'value_list', {'AUTO', 'NORMal'}); addCommand(this, 'acq_state', ':ACQuire:STATE', ... 'format', '%b',... 'info', 'State of data acquisition by the scope'); addCommand(this, 'acq_mode', ':ACQuire:MODe', ... 'format', '%s', ... 'info', ['Acquisition mode: sample, peak ' ... 'detect, high resolution, average or envelope'], ... 'value_list',{'SAMple', 'PEAKdetect', 'HIRes', ... 'AVErage', 'ENVelope'}); % Parametric commands for i = 1:this.channel_no i_str = num2str(i); addCommand(this,... ['cpl',i_str],[':CH',i_str,':COUP'], ... 'format', '%s', ... 'info', 'Channel coupling: AC, DC or GND', ... - 'value_list', {'DC','AC','GND'}); + 'value_list', {'DC', 'DCREJect', 'AC'}); % impedances, 1MOhm or 50 Ohm addCommand(this,... - ['imp', i_str], [':CH', i_str, ':IMPedance'],... - 'format', '%s', ... - 'info', 'Channel impedance: 1 MOhm or 50 Ohm', ... - 'value_list', {'MEG', 'FIFty'}); + ['imp', i_str], [':CH', i_str, ':TERmination'], ... + 'format', '%e', ... + 'info', ['Channel impedance: 50 Ohm, ' ... + '75 Ohm or 1 MOhm'], ... + 'value_list', {50, 75, 1e6}); % Offset addCommand(this, ... ['offset',i_str], [':CH',i_str,':OFFSet'], ... 'format', '%e', ... 'info', '(V)'); addCommand(this, ... ['scale',i_str], [':CH',i_str,':SCAle'], ... 'format', '%e', ... 'info', 'Channel y scale (V/div)'); addCommand(this,... ['enable',i_str], [':SEL:CH',i_str], ... 'format', '%b',... 'info', 'Channel enabled'); end end end -end \ No newline at end of file +end + diff --git a/@MyTekScope/MyTekScope.m b/@MyTekScope/MyTekScope.m index 47de89c..54f76e3 100644 --- a/@MyTekScope/MyTekScope.m +++ b/@MyTekScope/MyTekScope.m @@ -1,104 +1,108 @@ % Generic class for controlling Tektronix scopes classdef MyTekScope < MyScpiInstrument & MyDataSource & MyCommCont properties (GetAccess = public, SetAccess={?MyClassParser,?MyTekScope}) % number of channels channel_no = 4 % List of the physical knobs, which can be rotated programmatically knob_list = {} end methods (Access = public) function this = MyTekScope(varargin) this@MyCommCont(varargin{:}); this.Comm.InputBufferSize = 4.1e7; % byte this.Trace.name_x = 'Time'; this.Trace.name_y = 'Voltage'; end function readTrace(this) % Read raw y data y_data = readY(this); % Read units, offsets and steps for the scales parms = queryStrings(this, ... ':WFMOutpre:XUNit?', ... ':WFMOutpre:YUNit?', ... ':WFMOutpre:XINcr?', ... ':WFMOutpre:YMUlt?', ... ':WFMOutpre:XZEro?', ... ':WFMOutpre:YZEro?', ... ':WFMOutpre:YOFf?'); num_params = str2doubleHedged(parms); [unit_x, unit_y, step_x, step_y, x_zero, ... y_zero, y_offset] = num_params{:}; % Calculating the y data y = (y_data-y_offset)*step_y+y_zero; n_points = length(y); % Calculating the x data x = linspace(x_zero, x_zero + step_x*(n_points-1), n_points); this.Trace.x = x; this.Trace.y = y; % Discard "" when assiging the Trace labels this.Trace.unit_x = unit_x(2:end-1); this.Trace.unit_y = unit_y(2:end-1); triggerNewData(this); end function acquireContinuous(this) writeStrings(this, ... ':ACQuire:STOPAfter RUNSTop', ... ':ACQuire:STATE ON'); end function acquireSingle(this) writeStrings(this, ... ':ACQuire:STOPAfter SEQuence', ... ':ACQuire:STATE ON'); end function turnKnob(this, knob, nturns) writeString(this, sprintf(':FPAnel:TURN %s,%i', knob, nturns)); end end methods (Access = protected) % The default version of this method works for DPO3034-4034 scopes function y_data = readY(this) % Configure data transfer: binary format and two bytes per % point. Then query the trace. this.Comm.ByteOrder = 'bigEndian'; writeStrings(this, ... - ':DATA:ENCDG RPBinary', ... + ':DATA:ENCDG RIBinary', ... ':DATA:WIDTH 2', ... ':DATA:STARt 1', ... sprintf(':DATA:STOP %i', this.point_no), ... ':CURVE?'); y_data = double(binblockread(this.Comm, 'int16')); + + % For some reason MDO3000 scope needs to have an explicit pause + % between data reading and any other communication + pause(0.01); end end methods function set.knob_list(this, val) assert(iscellstr(val), ['Value must be a cell array of ' ... 'character strings.']) %#ok this.knob_list = val; end end end diff --git a/@MyTlb6700/MyTlb6700.m b/@MyTlb6700/MyTlb6700.m index ad8df93..27bd779 100644 --- a/@MyTlb6700/MyTlb6700.m +++ b/@MyTlb6700/MyTlb6700.m @@ -1,228 +1,230 @@ % Class for communication with NewFocus TLB6700 tunable laser controllers % Needs UsbDllWrap.dll from Newport USB driver on Matlab path % % Start instrument as MyTlb6700('address','USBaddr'), where USBaddr % is indicated in the instrument menu. Example: MyTlb6700('address', '1'). % % This class uses MyNewportUsbComm, an instance of which needs to be shared % between multiple devices, as the Newport driver, apparently, cannot % handle concurrent calls. classdef MyTlb6700 < MyScpiInstrument properties (GetAccess = public, ... SetAccess = {?MyClassParser, ?MyTlb6700}) % Interface field is not used in this instrument, but keep it % for the sake of information interface = 'usb'; address = ''; end properties (GetAccess = public, SetAccess = protected) % Instance of Newport.USBComm.USB used for communication. % Must be shared between the devices UsbComm end methods (Access = public) function this = MyTlb6700(varargin) P = MyClassParser(this); processInputs(P, this, varargin{:}); % Convert address to number this.address = str2double(this.address); % Get the unique instance of control class for Newport driver this.UsbComm = MyNewportUsbComm.instance(); + + createCommandList(this); end end methods (Access = protected) function createCommandList(this) % Commands for this class do not start from ':', as the % protocol does not fully comply with SCPI standard addCommand(this, 'wavelength', 'SENSe:WAVElength', ... 'format', '%e', ... 'info', 'Output wavelength (nm)', ... 'access', 'r'); addCommand(this, 'current', 'SENSe:CURRent:DIODe', ... 'format', '%e', ... 'info', 'Diode current (mA)', ... 'access', 'r'); addCommand(this, 'temp_diode', 'SENSe:TEMPerature:DIODe', ... 'format', '%e', ... 'info', 'Diode temperature (C)', ... 'access', 'r'); addCommand(this, 'power', 'SENSe:POWer:DIODe', ... 'format', '%e', ... 'info', 'Output power (mW)', ... 'access', 'r'); addCommand(this, 'wavelength_sp', 'SOURce:WAVElength', ... 'format', '%e', ... 'info', 'Wavelength setpoint (nm)'); addCommand(this, 'const_power', 'SOURce:CPOWer', ... 'format', '%b', ... 'info', 'Constant power mode on/off'); addCommand(this, 'power_sp', 'SOURce:POWer:DIODe', ... 'format', '%e', ... 'info', 'Power setpoint (mW)'); addCommand(this, 'current_sp', 'SOURce:CURRent:DIODe', ... 'format', '%e', ... 'info', 'Current setpoint (mA)'); % Control mode local/remote addCommand(this, 'control_mode', 'SYSTem:MCONtrol', ... 'format', '%s',... 'info', 'Control local/remote', ... 'value_list', {'LOC','REM'}); % Output on/off addCommand(this, 'enable_output', 'OUTPut:STATe', ... 'format', '%b', ... 'info', 'on/off'); % Wavelength track on/off addCommand(this, 'wavelength_track', 'OUTPut:TRACk', ... 'format', '%b', ... 'info', 'on/off'); % Wavelength scan related commands % Scan start wavelength (nm) addCommand(this, 'scan_start_wl', 'SOURce:WAVE:START', ... 'format', '%e', ... 'info', '(nm)'); % Scan stop wavelength (nm) addCommand(this, 'scan_stop_wl', 'SOURce:WAVE:STOP', ... 'format', '%e', ... 'info', '(nm)'); % Scan speed (nm/s) addCommand(this, 'scan_speed', 'SOURce:WAVE:SLEW:FORWard', ... 'format', '%e', ... 'info', '(nm/s)'); % Maximum scan speed (nm/s) addCommand(this, 'scan_speed_max', 'SOURce:WAVE:MAXVEL', ... 'format', '%e', ... 'info', '(nm/s)', ... 'access', 'r'); end end methods (Access = public) function openComm(this) % Opening a single device is not supported by Newport Usb % Driver, so open all the devices of the given type OpenDevices(this.UsbComm.Usb, hex2num('100A')); end % Query textual command function result = queryString(this, str) try result = query(this.UsbComm, this.address, str); catch ME try % Attempt re-opening communication openComm(this); result = query(this.UsbComm, this.address, str); catch rethrow(ME); end end end % Redefine queryStrings of MyScpiInstrument function res_list = queryStrings(this, varargin) if ~isempty(varargin) n_cmd = length(varargin); res_list = cell(n_cmd,1); % Query commands one by one as sending one query seems % to sometimes give errors if the string is very long for i = 1:n_cmd cmd = [varargin{i},';']; res_list{i} = queryString(this, cmd); end else res_list = {}; end end % Writing is done by sending a command and querying its status. % Still, redefine writeStrings of MyScpiInstrument for consistency % and clarity. function stat = writeString(this, str) stat = queryString(this, str); end function stat = writeStrings(this, varargin) stat = queryStrings(this, varargin{:}); end %% Laser power and scan control functions function stat = setMaxOutPower(this) % Depending on if the laser in the constat power or current % mode, set value to max if this.const_power stat = queryString(this, 'SOURce:POWer:DIODe MAX;'); else stat = queryString(this, 'SOURce:CURRent:DIODe MAX;'); end end % Returns minimum and maximum wavelengths of the laser. There does % not seem to be a more direct way of doing this with TLB6700 % other than setting and then reading the min/max values. function [wl_min, wl_max] = readMinMaxWavelength(this) tmp = this.scan_start_wl; % Read min wavelength of the laser writeStrings(this, 'SOURce:WAVE:START MIN'); resp = queryStrings(this, 'SOURce:WAVE:START?'); wl_min = str2double(resp{1}); % Read max wavelength of the laser writeStrings(this, 'SOURce:WAVE:START MAX'); resp = queryStrings(this, 'SOURce:WAVE:START?'); wl_max = str2double(resp{1}); % Return scan start to its original value this.scan_start_wl = tmp; end function configSingleScan(this) % Configure: % Do not switch the laser off during the backward scan, % Perform a signle scan, % Return at maximum speed writeStrings(this,'SOURce:WAVE:SCANCFG 0', ... 'SOURce:WAVE:DESSCANS 1', ... 'SOURce:WAVE:SLEW:RETurn MAX'); end function startScan(this) writeStrings(this, 'OUTPut:SCAN:START'); end function stopScan(this) writeStrings(this, 'OUTPut:SCAN:STOP'); end end end diff --git a/@MyTpg/MyTpg.m b/@MyTpg/MyTpg.m index be9aa7d..72c19e8 100644 --- a/@MyTpg/MyTpg.m +++ b/@MyTpg/MyTpg.m @@ -1,181 +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)); + this.gauge_stat = {gaugeStatusFromCode(this, arr(1)), ... + 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', @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/GUIs/GuiLogger.mlapp b/GUIs/GuiLogger.mlapp index b64e3a3..ec29872 100644 Binary files a/GUIs/GuiLogger.mlapp and b/GUIs/GuiLogger.mlapp differ diff --git a/GUIs/GuiTekScope.mlapp b/GUIs/GuiTekScope.mlapp index 121fe97..cb41f2c 100644 Binary files a/GUIs/GuiTekScope.mlapp and b/GUIs/GuiTekScope.mlapp differ diff --git a/GUIs/GuiTlb.mlapp b/GUIs/GuiTlb.mlapp index 54755e0..d2cbd37 100644 Binary files a/GUIs/GuiTlb.mlapp and b/GUIs/GuiTlb.mlapp differ diff --git a/Utility functions/DataAcquisitionMenu.mlapp b/Utility functions/DataAcquisitionMenu.mlapp index 35c225f..3540b55 100644 Binary files a/Utility functions/DataAcquisitionMenu.mlapp and b/Utility functions/DataAcquisitionMenu.mlapp differ diff --git a/Utility functions/InstrumentManager.mlapp b/Utility functions/InstrumentManager.mlapp index 880ad70..46636e5 100644 Binary files a/Utility functions/InstrumentManager.mlapp and b/Utility functions/InstrumentManager.mlapp differ diff --git a/Utility functions/runLogger.m b/Utility functions/runLogger.m index b82f8d1..8adfe72 100644 --- a/Utility functions/runLogger.m +++ b/Utility functions/runLogger.m @@ -1,101 +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']); + 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