diff --git a/@MyCommCont/MyCommCont.m b/@MyCommCont/MyCommCont.m index 29689f7..a9fe776 100644 --- a/@MyCommCont/MyCommCont.m +++ b/@MyCommCont/MyCommCont.m @@ -1,168 +1,167 @@ % Communicator container. % This class provides extended functionality for communication using VISA, % tcpip and serial objects or any other objects that have a similar usage. classdef MyCommCont < handle % Giving explicit set access to this class makes properties protected % instead of private properties (GetAccess=public, SetAccess={?MyClassParser,?MyCommCont}) interface = 'serial' address = 'placeholder' end properties (GetAccess = public, SetAccess = protected) Comm % Communication object end methods (Access = public) %% Constructor and destructor function this = MyCommCont(varargin) P = MyClassParser(this); - P.KeepUnmatched = true; processInputs(P, this, varargin{:}); try connect(this); catch ME warning(ME.message); % Create a dummy this.Comm = serial('placeholder'); end configureCommDefault(this); end function delete(this) % Close the connection to the device try closeComm(this); catch warning('Connection could not be closed.'); end % Delete the device object try delete(this.Comm); catch warning('Communication object could not be deleted.'); end end %% Set up communication % Create an interface object function connect(this) switch lower(this.interface) % Use 'constructor' interface to create an object with % more that one parameter passed to the constructor case 'constructor' % In this case 'address' is a MATLAB command that % creates communication object when executed. % Such commands, for example, are returned by % instrhwinfo as ObjectConstructorName. this.Comm = eval(this.address); case 'visa' % visa brand is 'ni' by default this.Comm = visa('ni', this.address); case 'tcpip' % Works only with default socket. Use 'constructor' % if socket or other options need to be specified this.Comm = tcpip(this.address); case 'serial' this.Comm = serial(this.address); otherwise error(['Unknown interface ''' this.interface ... ''', a communication object is not created.' ... ' Valid interfaces are ',... '''constructor'', ''visa'', ''tcpip'' and ''serial''']) end end % Set larger buffer sizes and longer timeout than the MATLAB default function configureCommDefault(this) comm_props = properties(this.Comm); if ismember('OutputBufferSize',comm_props) this.Comm.OutputBufferSize = 1e7; % bytes end if ismember('InputBufferSize',comm_props) this.Comm.InputBufferSize = 1e7; % bytes end if ismember('Timeout',comm_props) this.Comm.Timeout = 10; % s end end function bool = isopen(this) try bool = strcmp(this.Comm.Status, 'open'); catch warning('Cannot access the communicator Status property'); bool = false; end end % Opens the device if it is not open. Does not throw error if % device is already open for communication with another object, but % tries to close existing connections instead. function openComm(this) try fopen(this.Comm); catch % try to find and close all the devices with the same % VISA resource name instr_list = instrfind('RsrcName', this.Comm.RsrcName); fclose(instr_list); fopen(this.Comm); warning(['Multiple instrument objects of ' ... 'address %s exist'], this.address); end end function closeComm(this) fclose(this.Comm); end %% Communication % Write textual command function writeString(this, str) try fprintf(this.Comm, str); catch ME try % Attempt re-opening communication openComm(this); fprintf(this.Comm, str); catch rethrow(ME); end end end % Query textual command function result = queryString(this, str) try result = query(this.Comm, str); catch ME try % Attempt re-opening communication openComm(this); result = query(this.Comm, str); catch rethrow(ME); end end end end end diff --git a/@MyGuiSync/MyGuiSync.m b/@MyGuiSync/MyGuiSync.m index e5527bb..1e33b5c 100644 --- a/@MyGuiSync/MyGuiSync.m +++ b/@MyGuiSync/MyGuiSync.m @@ -1,751 +1,751 @@ % A mechanism to implement synchronization between parameters and GUI % elements in app-based GUIs classdef MyGuiSync < handle properties (GetAccess = public, SetAccess = protected) Listeners = struct() % Link structures Links = struct( ... 'reference', {}, ... % reference to the link target 'GuiElement', {}, ... % graphics object 'gui_element_prop', {}, ... 'inputProcessingFcn', {}, ... % applied after a value is ... % inputed to GUI 'outputProcessingFcn', {}, ... % applied before a new value is ... % displayed in GUI 'getTargetFcn', {}, ... 'setTargetFcn', {}, ... 'Listener', {} ... % PostSet listener (optional) ); % List of objects to be deleted when App is deleted cleanup_list = {} end properties (Access = protected) App updateGuiFcn end methods (Access = public) function this = MyGuiSync(App, varargin) p = inputParser(); addRequired(p, 'App', ... @(x)assert(isa(x, 'matlab.apps.AppBase'), ... 'App must be a Matlab app.')); % Deletion of kernel object triggers the delition of app addParameter(p, 'KernelObj', [], @(x)assert( ... ismember('ObjectBeingDestroyed', events(x)), ... ['Object must define ''ObjectBeingDestroyed'' event ' ... 'to be an app kernel.'])); % Optional function, executed after an app parameter has been % updated (either externally of internally) addParameter(p, 'updateGuiFcn', [], ... @(x)isa(x, 'function_handle')); parse(p, App, varargin{:}); this.updateGuiFcn = p.Results.updateGuiFcn; this.App = App; this.Listeners.AppDeleted = addlistener(App, ... 'ObjectBeingDestroyed', @(~, ~)delete(this)); if ~isempty(p.Results.KernelObj) KernelObj = p.Results.KernelObj; addToCleanup(this, p.Results.KernelObj); this.Listeners.KernelObjDeleted = addlistener(KernelObj,... 'ObjectBeingDestroyed', @this.kernelDeletedCallback); end end function delete(this) % Delete generic listeners try lnames = fieldnames(this.Listeners); for i=1:length(lnames) try delete(this.Listeners.(lnames{i})); catch fprintf(['Could not delete the listener to ' ... '''%s'' event.\n'], lnames{i}) end end catch fprintf('Could not delete listeners.\n'); end % Delete link listeners for i=1:length(this.Links) try delete(this.Links(i).Listener); catch ME warning(['Could not delete listener for a GUI ' ... 'link. Error: ' ME.message]) end end % Delete the content of cleanup list for i = 1:length(this.cleanup_list) Obj = this.cleanup_list{i}; try if isa(Obj, 'timer') % Stop if object is a timer try stop(Obj); catch end end % Check if the object has an appropriate delete method. % This is a safety measure to never delete a file by % accident. if ismethod(Obj, 'delete') delete(Obj); else fprintf(['Object of class ''%s'' ' ... 'does not have ''delete'' method.\n'], ... class(Obj)) end catch fprintf(['Could not delete an object of class ' ... '''%s'' from the cleanup list.\n'], class(Obj)) end end end % Establish a correspondence between the value of a GUI element and % some other property of the app % % Elem - graphics object % prop_ref - reference to a content of app, e.g. 'var1.subprop(3)' function addLink(this, Elem, prop_ref, varargin) % Parse function inputs p = inputParser(); p.KeepUnmatched = true; % The decision whether to create ValueChangedFcn and % a PostSet callback is made automatically by this function, % but the parameters below enforce these functions to be *not* % created. addParameter(p, 'create_elem_callback', true, @islogical); addParameter(p, 'event_update', true, @islogical); parse(p, varargin{:}); % Make the list of unmatched name-value pairs for subroutine sub_varargin = struct2namevalue(p.Unmatched); % Find the handle object which the end property belongs to, % the end property name and, possibly, further subscripts [Hobj, hobj_prop, RelSubs] = parseReference(this, prop_ref); % Check if the specified target is accessible for reading try if isempty(RelSubs) Hobj.(hobj_prop); else subsref(Hobj.(hobj_prop), RelSubs); end catch disp(['Property referenced by ' prop_ref ... ' is not accessible, the corresponding GUI ' ... 'element will be not linked and will be disabled.']) Elem.Enable = 'off'; return end % Create the basis of link structure (everything except for % set/get functions) Link = createLinkBase(this, Elem, prop_ref, sub_varargin{:}); % Do additional link processing in the case of % MyInstrument commands if isa(Hobj, 'MyInstrument') && ... ismember(hobj_prop, Hobj.command_names) Link = extendMyInstrumentLink(this, Link, Hobj, hobj_prop); end % Assign the function that returns the value of reference Link.getTargetFcn = createGetTargetFcn(this, Hobj, ... hobj_prop, RelSubs); % Check if ValueChanged or another callback needs to be created elem_prop = Link.gui_element_prop; cb_name = findElemCallbackType(this, Elem, elem_prop, ... Hobj, hobj_prop); if p.Results.create_elem_callback && ~isempty(cb_name) % Assign the function that sets new value to reference Link.setTargetFcn = createSetTargetFcn(this, Hobj, ... hobj_prop, RelSubs); switch cb_name case 'ValueChangedFcn' Elem.ValueChangedFcn = ... createValueChangedCallback(this, Link); case 'MenuSelectedFcn' Elem.MenuSelectedFcn = ... createMenuSelectedCallback(this, Link); otherwise error('Unknown callback name %s', cb_name) end end % Attempt creating a callback to PostSet event for the target % property. If such callback is not created, the link needs to % be updated manually. if p.Results.event_update try Link.Listener = addlistener(Hobj, hobj_prop, ... 'PostSet', createPostSetCallback(this, Link)); catch Link.Listener = event.proplistener.empty(); end end % Store the link structure ind = length(this.Links)+1; this.Links(ind) = Link; % Update the value of GUI element updateElementByIndex(this, ind); end % Change link reference for a given element or update the functions % that get and set the value of the existing reference. function reLink(this, Elem, prop_ref) % Find the index of link structure corresponding to Elem ind = findLinkInd(this, Elem); if isempty(ind) return end if ~exist('prop_ref', 'var') % If the reference is not supplied, update existing prop_ref = this.Links(ind).reference; end this.Links(ind).reference = prop_ref; if ~isempty(this.Links(ind).Listener) % Delete and clear the existing listener delete(this.Links(ind).Listener); this.Links(ind).Listener = []; end [Hobj, hobj_prop, RelSubs] = parseReference(this, prop_ref); this.Links(ind).getTargetFcn = createGetTargetFcn(this, ... Hobj, hobj_prop, RelSubs); if ~isempty(this.Links(ind).setTargetFcn) % Create a new ValueChanged callback this.Links(ind).setTargetFcn = createSetTargetFcn(this, ... Hobj, hobj_prop, RelSubs); this.Links(ind).GuiElement.ValueChangedFcn = ... createValueChangedCallback(this, this.Links(ind)); end % Attempt creating a new listener try this.Links(ind).Listener = addlistener(Hobj, hobj_prop, ... 'PostSet', createPostSetCallback(this, ... this.Links(ind))); catch this.Links(ind).Listener = event.proplistener.empty(); end % Update the value of GUI element according to the new % reference updateElementByIndex(this, ind); end function updateAll(this) for i = 1:length(this.Links) % Only update those elements for which listeners do not % exist if isempty(this.Links(i).Listener) updateElementByIndex(this, i); end end % Optionally execute the update function defined within the App if ~isempty(this.updateGuiFcn) this.updateGuiFcn(); end end % Update the value of one linked GUI element. function updateElement(this, Elem) ind = findLinkInd(this, Elem); if ~isempty(ind) updateElementByIndex(this, ind); end 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 % Find the link structure corresponding to Elem function ind = findLinkInd(this, Elem) % Argument 2 is a GUI element, for which we find the link ind = ([this.Links.GuiElement] == Elem); ind = find(ind); if isempty(ind) warning('No link found for the GUI element below.'); disp(Elem); elseif length(ind) > 1 warning('Multiple links found for the GUI element below.'); disp(Elem); ind = []; 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.Result.input_prescaler); Link.outputProcessingFcn = ... @(x) (x*p.Result.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) + isempty(Link.GuiElement.Items) if all(cellfun(@ischar, Cmd.value_list)) % Capitalized displayed names for beauty Link.GuiElement.Items = cellfun( ... @(x)[upper(x(1)),lower(x(2:end))],... Cmd.value_list, 'UniformOutput', false); 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 end Link.GuiElement.Items = str_value_list; end % 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/@MyNa/MyNa.m b/@MyNa/MyNa.m index 0e2591b..2613160 100644 --- a/@MyNa/MyNa.m +++ b/@MyNa/MyNa.m @@ -1,187 +1,187 @@ % The class for communication with Agilent E5061B Network Analyzer -classdef MyNa < MyScpiInstrument - - properties(Access=public) + +classdef MyNa < MyScpiInstrument & MyCommCont & MyDataSource + properties(Access = public, SetObservable = true) Trace1 Trace2 - transf_n=1; % trace that triggers NewData event + transf_n = 1 % trace that triggers NewData event end - properties (SetAccess=protected, GetAccess=public) - active_trace = -1; % manipulating with active traces seems unavoidable - % for selecting the data format. -1 stands for unknown + properties (SetAccess = protected, GetAccess = public, ... + SetObservable = true) + + % Manipulating active traces seems unavoidable for data format + % selection. -1 stands for unknown. + active_trace = -1 % data formats for the traces 1-2, options: % 'PHAS', 'SLIN', 'SLOG', 'SCOM', 'SMIT', 'SADM', 'MLOG', 'MLIN', %'PLIN', 'PLOG', 'POL' - form1 = 'MLOG'; - form2 = 'PHAS'; + form1 = 'MLOG' + form2 = 'PHAS' end methods - function this=MyNa(interface, address, varargin) - this@MyScpiInstrument(interface, address, varargin{:}); + function this = MyNa(varargin) + this@MyCommCont(varargin{:}); + this.Trace1 = MyTrace(); this.Trace2 = MyTrace(); this.Trace1.unit_x = 'Hz'; this.Trace1.name_x = 'Frequency'; this.Trace2.unit_x = 'Hz'; this.Trace2.name_x = 'Frequency'; + + createCommandList(this); end % Generate a new data event with header collection suppressed function transferTrace(this, n_trace) trace_tag = sprintf('Trace%i', n_trace); + % Assign either Trace1 or 2 to Trace while keeping the metadata - this.(trace_tag).MeasHeaders=copy(this.Trace.MeasHeaders); - this.Trace=copy(this.(trace_tag)); + this.(trace_tag).MeasHeaders = copy(this.Trace.MeasHeaders); + this.Trace = copy(this.(trace_tag)); - triggerNewData(this,'new_header',false); + triggerNewData(this, 'new_header', false); end - function data = readTrace(this, n_trace) + function readTrace(this, n_trace) writeActiveTrace(this, n_trace); - freq_str = strsplit(query(this.Device,':SENS1:FREQ:DATA?'),','); - data_str = strsplit(query(this.Device,':CALC1:DATA:FDAT?'),','); - data = struct(); - data.x = str2double(freq_str); + + freq_str = strsplit(queryString(this,':SENS1:FREQ:DATA?'),','); + data_str = strsplit(queryString(this,':CALC1:DATA:FDAT?'),','); + + data_x = str2double(freq_str); + % In the returned string there is in general 2 values for each % frequency point. In the Smith data format this can be used to % transfer magnitude and phase of the signal in one trace. With % MLOG, MLIN and PHAS format settings every 2-nd element should % be 0 - data.y1 = str2double(data_str(1:2:end)); - data.y2 = str2double(data_str(2:2:end)); + data_y1 = str2double(data_str(1:2:end)); % set the Trace properties trace_tag = sprintf('Trace%i', n_trace); - this.(trace_tag).x = data.x; - this.(trace_tag).y = data.y1; + this.(trace_tag).x = data_x; + this.(trace_tag).y = data_y1; - if this.transf_n==n_trace - this.Trace=copy(this.(trace_tag)); + if this.transf_n == n_trace + this.Trace = copy(this.(trace_tag)); triggerNewData(this); end end function writeActiveTrace(this, n_trace) - fprintf(this.Device, sprintf(':CALC1:PAR%i:SEL',n_trace)); + writeString(this, sprintf(':CALC1:PAR%i:SEL', n_trace)); this.active_trace = n_trace; end function writeTraceFormat(this, n_trace, fmt) - this.writeActiveTrace(n_trace); + writeActiveTrace(this, n_trace); + n_str = num2str(n_trace); - this.(['form',n_str]) = fmt; - fprintf(this.Device, sprintf(':CALC1:FORM %s', fmt)); + + this.(['form', n_str]) = fmt; + writeString(this, sprintf(':CALC1:FORM %s', fmt)); end function singleSweep(this) - openDevice(this); - writeProperty(this,'cont_trig', true); + % Set the triger source to remote control - writeProperty(this,'trig_source', 'BUS'); + this.trig_source = 'BUS'; + this.cont_trig = true; + % Start a sweep cycle - fprintf(this.Device,':TRIG:SING'); + writeString(this, ':TRIG:SING'); + % Wait for the sweep to finish (for the query to return 1) - query(this.Device,'*OPC?'); - closeDevice(this); + queryString(this, '*OPC?'); end function startContSweep(this) - openDevice(this); - writeProperty(this,'cont_trig', true); - % Set the triger source to be internal - writeProperty(this,'trig_source', 'INT'); - closeDevice(this); + + % Set the triger source to internal + this.trig_source = 'INT'; + this.cont_trig = true; end function abortSweep(this) - openDevice(this); - writeProperty(this, 'trig_source', 'BUS'); - fprintf(this.Device,':ABOR'); - closeDevice(this); + this.trig_source = 'BUS'; + writeString(this, ':ABOR'); end end - %% Protected functions - methods (Access=protected) - % Command attributes are {class, attributtes} accepted by - % validateattributes() + methods (Access = protected) function createCommandList(this) - addCommand(this,... - 'cent_freq',':SENS1:FREQ:CENT', 'default',1.5e6,... - 'fmt_spec','%e',... - 'info','(Hz)'); - addCommand(this,... - 'start_freq',':SENS1:FREQ:START', 'default',1e6,... - 'fmt_spec','%e',... - 'info','(Hz)'); - addCommand(this,... - 'stop_freq',':SENS1:FREQ:STOP', 'default',2e6,... - 'fmt_spec','%e',... - 'info','(Hz)'); - addCommand(this,... - 'span',':SENS1:FREQ:SPAN', 'default',1e6,... - 'fmt_spec','%e',... - 'info','(Hz)'); - % IF bandwidth - addCommand(this,... - 'ifbw',':SENS1:BAND', 'default',100,... - 'fmt_spec','%e',... - 'info','IF bandwidth (Hz)'); - % number of points in the sweep - addCommand(this,... - 'point_no',':SENS1:SWE:POIN', 'default',1000,... - 'fmt_spec','%i'); - % number of averages - addCommand(this,... - 'average_no',':SENS1:AVER:COUN', 'default',1,... - 'fmt_spec','%i'); - % number of traces - addCommand(this,... - 'trace_no',':CALC1:PAR:COUN', 'default',1,... - 'fmt_spec','%i',... - 'info','Number of traces'); - % linear or log sweep - addCommand(this,... - 'sweep_type',':SENS1:SWE:TYPE', 'default','LIN',... - 'fmt_spec','%s',... - 'info','Linear or log sweep'); - % switch the output signal on/off - addCommand(this,... - 'enable_out',':OUTP', 'default',0,... - 'fmt_spec','%b',... - 'info','output signal on/off'); - % probe power [dB] - addCommand(this,... - 'power',':SOUR:POW:LEV:IMM:AMPL', 'default',-10,... - 'fmt_spec','%e',... - 'info','Probe power (dB)'); - % windows arrangement on the display, e.g 'D1' - addCommand(this,... - 'disp_type',':DISP:WIND1:SPL', 'default','D1',... - 'fmt_spec','%s',... - 'info','Window arrangement'); - % Continuous sweep triggering - addCommand(this,... - 'cont_trig',':INIT1:CONT', 'default', 0,... - 'fmt_spec','%b'); - addCommand(this,... - 'trig_source', ':TRIG:SOUR', 'default', 'BUS',... - 'fmt_spec','%s') + addCommand(this, 'cent_freq', ':SENS1:FREQ:CENT', ... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this, 'start_freq', ':SENS1:FREQ:START', ... + 'format', '%e',... + 'info', '(Hz)'); + + addCommand(this, 'stop_freq', ':SENS1:FREQ:STOP', ... + 'format', '%e',... + 'info', '(Hz)'); + + addCommand(this, 'span', ':SENS1:FREQ:SPAN', ... + 'format', '%e',... + 'info', '(Hz)'); + + addCommand(this, 'ifbw', ':SENS1:BAND', ... + 'format', '%e', ... + 'info', 'IF bandwidth (Hz)'); + + addCommand(this, 'point_no', ':SENS1:SWE:POIN', ... + 'format', '%i'); + + addCommand(this, 'average_no', ':SENS1:AVER:COUN', ... + 'format', '%i'); + + addCommand(this, 'trace_no', ':CALC1:PAR:COUN', ... + 'format', '%i',... + 'info', 'Number of traces', ... + 'value_list', {1, 2}); + + addCommand(this, 'sweep_type', ':SENS1:SWE:TYPE', ... + 'format', '%s',... + 'info', 'Linear or log sweep', ... + 'value_list', {'LIN', 'LOG'}); + + addCommand(this, 'enable_out', ':OUTP', ... + 'format', '%b',... + 'info', 'output signal on/off'); + + addCommand(this, 'power', ':SOUR:POW:LEV:IMM:AMPL', ... + 'format', '%e',... + 'info', 'Probe power (dB)'); + + addCommand(this, 'disp_type', ':DISP:WIND1:SPL',... + 'format', '%s',... + 'info', 'Window arrangement', ... + 'default', 'D1'); + + addCommand(this, 'cont_trig', ':INIT1:CONT', ... + 'format', '%b'); + + addCommand(this, 'trig_source', ':TRIG:SOUR', ... + 'format', '%s', ... + 'default', 'BUS') % Parametric commands for traces, i can be extended to 4 for i = 1:2 + % measurement parameters for the traces 1-2, e.g. 'S21' i_str = num2str(i); addCommand(this,... - ['meas_par',i_str],[':CALC1:PAR',i_str,':DEF'],... - 'default','S21',... - 'fmt_spec','%s',... - 'info','Measurement parameter'); + ['meas_par',i_str], [':CALC1:PAR',i_str,':DEF'], ... + 'format', '%s',... + 'info', 'Measurement parameter', ... + 'default', 'S21'); end end end end diff --git a/@MyScpiInstrument/MyScpiInstrument.m b/@MyScpiInstrument/MyScpiInstrument.m index b2df7d6..02d0d97 100644 --- a/@MyScpiInstrument/MyScpiInstrument.m +++ b/@MyScpiInstrument/MyScpiInstrument.m @@ -1,358 +1,358 @@ % 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); 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.(tag) = val; this.CommandList.(tag).Psl.Enabled = 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 % Iterate over the list of values for i=1:length(vl) % Short forms exist only for string values if ischar(vl{i}) 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 end 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'); 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 | x==0), ['Value must be a logical ' ... + 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) 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/GUIs/GuiNa.mlapp b/GUIs/GuiNa.mlapp index 395fe86..419d63e 100644 Binary files a/GUIs/GuiNa.mlapp and b/GUIs/GuiNa.mlapp differ