diff --git a/@MyCommCont/MyCommCont.m b/@MyCommCont/MyCommCont.m index 82875c8..80e92db 100644 --- a/@MyCommCont/MyCommCont.m +++ b/@MyCommCont/MyCommCont.m @@ -1,167 +1,168 @@ % 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' + 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); + this.Comm = eval(this.address); case 'visa' % visa brand is 'ni' by default - this.Comm=visa('ni', this.address); + 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); + this.Comm = tcpip(this.address); case 'serial' - this.Comm=serial(this.address); + 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/@MyInstrument/MyInstrument.m b/@MyInstrument/MyInstrument.m index 76aceed..ed44378 100644 --- a/@MyInstrument/MyInstrument.m +++ b/@MyInstrument/MyInstrument.m @@ -1,248 +1,252 @@ % Generic instrument superclass % % Undefined/dummy methods: % queryString(this, cmd) % createCommandList(this) % % These methods are intentionally not introduced as abstract as under % some conditions they are not necessary classdef MyInstrument < dynamicprops 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 end methods (Access = public) function this = MyInstrument(varargin) P = MyClassParser(this); processInputs(P, this, varargin{:}); createCommandList(this); createMetadata(this); end % Read all parameters of the physical device function read_cns = 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.(tag) = read_value; this.CommandList.(tag).Psl.Enabled = 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',[], @(x)isa(x, 'function_handle')); addParameter(p,'writeFcn',[], @(x)isa(x, 'function_handle')); % Function applied before writeFcn addParameter(p,'validationFcn',[], ... @(x)isa(x, 'function_handle')); % Function or list of functions executed after updating the % class property value addParameter(p,'postSetFcn',[], @(x)isa(x, 'function_handle')); addParameter(p,'value_list',{}, @iscell); addParameter(p,'default',[]); 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); if ~isempty(this.CommandList.(tag).value_list) assert(isempty(this.CommandList.(tag).validationFcn), ... ['validationFcn is already assigned, cannot ' ... 'create a new one based on value_list']); this.CommandList.(tag).validationFcn = ... @(x) any(cellfun(@(y) isequal(y, x),... this.CommandList.(tag).value_list)); end % Create and configure a dynamic property H = addprop(this, tag); this.(tag) = p.Results.default; + this.CommandList.(tag).last_value = p.Results.default; H.GetAccess = 'public'; H.SetObservable = true; H.SetMethod = createCommandSetFcn(this, tag); 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,~,msg] = queryString(this,'*IDN?'); catch ErrorMessage str = ''; msg = ErrorMessage.message; end this.idn_str = str; end % Measurement header function Mdt = readSettings(this) % 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) % Dummy function that is redefined in subclasses to % incorporate addCommand statements function createCommandList(~) end function createMetadata(this) + this.Metadata = MyMetadata('title', class(this)); - % Add identification string as parameter - this.Metadata.idn = this.idn_str; + % 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) - assert(vFcn(val), ['Value assigned to property ''' ... - tag ''' must satisfy ' func2str(vFcn) '.']); + if ~vFcn(val) + error(['Value assigned to property ''' ... + tag ''' must satisfy ' func2str(vFcn) '.']); + end end % Store unprocessed value for quick reference in the future % and change tracking this.CommandList.(tag).last_value = val; pFcn = this.CommandList.(tag).postSetFcn; if ~isempty(pFcn) val = pFcn(val); end this.(tag) = val; end f = @commandSetFcn; 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 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/@MyRsa/MyRsa.m b/@MyRsa/MyRsa.m index ce22bb6..273acb5 100644 --- a/@MyRsa/MyRsa.m +++ b/@MyRsa/MyRsa.m @@ -1,192 +1,194 @@ % Class for controlling Tektronix RSA5103 and RSA5106 spectrum analyzers classdef MyRsa < MyScpiInstrument & MyDataSource & MyCommCont properties (SetAccess = protected, GetAccess = public) acq_trace = [] % Last read trace end methods (Access = public) function this = MyRsa(varargin) + this@MyCommCont(varargin{:}); + P = MyClassParser(this); processInputs(P, this, varargin{:}); this.Trace.unit_x = 'Hz'; this.Trace.unit_y = '$\mathrm{V}^2/\mathrm{Hz}$'; this.Trace.name_y = 'Power'; this.Trace.name_x = 'Frequency'; end end methods (Access = protected) function createCommandList(this) addCommand(this, 'rbw', ':DPX:BAND:RES', ... 'format', '%e', ... 'info', 'Resolution bandwidth (Hz)', ... 'default', 1e3); addCommand(this, 'auto_rbw', ':DPX:BAND:RES:AUTO', ... 'format', '%b', ... 'default', true); addCommand(this, 'span', ':DPX:FREQ:SPAN', ... 'format', '%e', ... 'info', '(Hz)', ... 'default', 1e6); addCommand(this, 'start_freq', ':DPX:FREQ:STAR',... 'format', '%e', ... 'info', '(Hz)', ... 'default', 1e6); addCommand(this, 'stop_freq', ':DPX:FREQ:STOP',... 'format', '%e', ... 'info', '(Hz)', ... 'default', 2e6); addCommand(this, 'cent_freq', ':DPX:FREQ:CENT',... 'format', '%e', ... 'info', '(Hz)', ... 'default', 1.5e6); % Continuous triggering addCommand(this, 'init_cont', ':INIT:CONT', ... 'format', '%b',... 'info', 'Continuous triggering on/off', ... 'default', true); % Number of points in trace addCommand(this, 'point_no', ':DPSA:POIN:COUN', ... 'format', 'P%i', ... 'value_list', {801, 2401, 4001, 10401}, ... 'default', 10401); % Reference level (dB) addCommand(this, 'ref_level',':INPut:RLEVel', ... 'format', '%e',... 'info', '(dB)', ... 'default', 0); % Display scale per division (dBm/div) addCommand(this, 'disp_y_scale', ':DISPlay:DPX:Y:PDIVision',... 'format', '%e', ... 'info', '(dBm/div)', ... 'default', 10); % Display vertical offset (dBm) addCommand(this, 'disp_y_offset', ':DISPLAY:DPX:Y:OFFSET', ... 'format', '%e', ... 'info', '(dBm)', ... 'default', 0); % Parametric commands for i = 1:3 i_str = num2str(i); % Display trace addCommand(this, ['disp_trace',i_str], ... [':TRAC',i_str,':DPX'], ... 'format', '%b', ... 'info', 'on/off', ... 'default', false); % Trace Detection addCommand(this, ['det_trace',i_str],... [':TRAC',i_str,':DPX:DETection'],... 'format', '%s', ... 'value_list', {'AVERage', 'NEGative', 'POSitive'},... 'default', 'average'); % Trace Function addCommand(this, ['func_trace',i_str], ... [':TRAC',i_str,':DPX:FUNCtion'], ... 'format', '%s', ... 'value_list', {'AVERage', 'HOLD', 'NORMal'},... 'default', 'average'); % Number of averages addCommand(this, ['average_no',i_str], ... [':TRAC',i_str,':DPX:AVER:COUN'], ... 'format', '%i', ... 'default', 1); % Count completed averages addCommand(this, ['cnt_trace',i_str], ... [':TRACe',i_str,':DPX:COUNt:ENABle'], ... 'format', '%b', ... 'info', 'Count completed averages', ... 'default', false); end end end methods (Access = public) function readTrace(this, varargin) if ~isempty(varargin) n_trace = varargin{1}; else n_trace = this.acq_trace; end % Ensure that device parameters, especially those that will be % later used for the calculation of frequency axis, are up to % date sync(this); writeString(this, sprintf('fetch:dpsa:res:trace%i?', n_trace)); data = binblockread(this.Comm, 'float'); % Calculate the frequency axis this.Trace.x = linspace(this.start_freq, this.stop_freq,... this.point_no); % Calculates the power spectrum from the data, which is in dBm. % Output is in V^2/Hz this.Trace.y = (10.^(data/10))/this.rbw*50*0.001; this.acq_trace = n_trace; % Trigger acquired data event triggerNewData(this); end % Abort data acquisition function abortAcq(this) writeString(this, ':ABORt'); end % Initiate data acquisition function initAcq(this) writeString(this, ':INIT'); end % Wait for the current operation to be completed function val = opc(this) val = queryString(this, '*OPC?'); end % Extend readHeader function function Hdr = readHeader(this) %Call parent class method and then append parameters Hdr = readHeader@MyScpiInstrument(this); %Hdr should contain single field addParam(Hdr, Hdr.field_names{1}, ... 'acq_trace', this.acq_trace, ... 'comment', 'Last read trace'); end end methods function set.acq_trace(this, val) assert((val==1 || val==2 || val==3), ... 'Acquisition trace number must be 1, 2 or 3.'); this.acq_trace = val; end end end diff --git a/@MyScpiInstrument/MyScpiInstrument.m b/@MyScpiInstrument/MyScpiInstrument.m index 146dedf..ea5dc18 100644 --- a/@MyScpiInstrument/MyScpiInstrument.m +++ b/@MyScpiInstrument/MyScpiInstrument.m @@ -1,263 +1,269 @@ % 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); % 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; smb = findReadFormatSymbol(this, format); if smb == 'b' % '%b' is a non-MATLAB format specifier that is introduced % to be used with 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; % Execute the base class method addCommand@MyInstrument(this, tag, sub_varargin{:}); % If the value list contains textual values, extend it with % short forms and add a postprocessing function vl = this.CommandList.(tag).value_list; if ~isempty(vl) && any(cellfun(@ischar, vl)) % Put only unique full-named values in the value list [long_vl, short_vl] = splitValueList(this, vl); this.CommandList.(tag).value_list = long_vl; % For validation, use an extended list made of full and % abbreviated name forms and case-insensitive comparison this.CommandList.(tag).validationFcn = ... @(x) any(cellfun(@(y) isequal(y, lower(x)), ... [long_vl, short_vl])); - this.CommandList.(tag).postSetFcn = @this.toStandardForm; + this.CommandList.(tag).postSetFcn = ... + createToStdFormFcn(this, tag, long_vl); end % Assign validation function based on the value format if isempty(this.CommandList.(tag).validationFcn) switch smb case {'d','f','e','g'} this.CommandList.(tag).validationFcn = @isnumeric; case 'i' this.CommandList.(tag).validationFcn = ... @(x)(floor(x)==x); case 's' this.CommandList.(tag).validationFcn = @ischar; case 'b' this.CommandList.(tag).validationFcn = ... @(x)(x==0 || x==1); end end end % Redefine the base class method to use a single read operation for % faster communication function read_cns = 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) - val = sscanf(res_list{i},... - this.CommandList.(read_cns{i}).format); + 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.(read_cns{i}).Psl.Enabled = false; - this.(read_cns{i}) = val; - this.CommandList.(read_cns{i}).Psl.Enabled = true; + 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 long_vl = setdiff(lower(vl), short_vl); end - % Return the long form of value from value_list - function std_val = toStandardForm(this, cmd) - assert(ismember(cmd, this.command_names), ['''' cmd ... - ''' is not an instrument command.']) + % 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) - val = this.(cmd); - value_list = this.CommandList.(cmd).ext_value_list; - - % Standardization is applicable to char-valued properties which - % have value list - if isempty(value_list) || ~ischar(val) - std_val = val; - return - end + % 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)); + % find matching values + n = length(val); + ismatch = cellfun(@(x) strncmpi(val, x, ... + min([n, length(x)])), value_list); - % 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)}; + 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 = findReadFormatSymbol(~, fmt_spec) ind_p = strfind(fmt_spec,'%'); ind = ind_p+find(isletter(fmt_spec(ind_p:end)),1)-1; smb = fmt_spec(ind); assert(ind_p+1 == ind, ['Correct reading format must not ' ... 'have characters between ''%'' and format symbol.']) 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 end end