diff --git a/@MyCommCont/MyCommCont.m b/@MyCommCont/MyCommCont.m index bcfd211..82875c8 100644 --- a/@MyCommCont/MyCommCont.m +++ b/@MyCommCont/MyCommCont.m @@ -1,167 +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); 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, cmd) + function writeString(this, str) try - fprintf(this.Comm, cmd); + fprintf(this.Comm, str); catch ME try % Attempt re-opening communication openComm(this); - fprintf(this.Comm, cmd); + fprintf(this.Comm, str); catch rethrow(ME); end end end % Query textual command - function result = queryString(this, cmd) + function result = queryString(this, str) try - result = query(this.Comm, cmd); + result = query(this.Comm, str); catch ME try % Attempt re-opening communication openComm(this); - result = query(this.Comm, cmd); + result = query(this.Comm, str); catch rethrow(ME); end end end end end diff --git a/@MyScpiInstrument/MyScpiInstrument.m b/@MyScpiInstrument/MyScpiInstrument.m index 0ba6422..b732191 100644 --- a/@MyScpiInstrument/MyScpiInstrument.m +++ b/@MyScpiInstrument/MyScpiInstrument.m @@ -1,247 +1,251 @@ % 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(queryCommand(this, read_command), format); + @()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)writeCommand(this, sprintf(write_command, x)); + @(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; 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 = queryCommand(this, read_commands{:}); + 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); 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; end end else warning(['Not all the properties could be read, ',... 'instrument class values are not updated.']); end end - end - methods (Access = protected) %% Write/query % These methods implement handling multiple SCPI commands. Unless - % overloaded, for communication with the device they rely on - % write/readString methods, which particular subclasses must + % 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 writeCommand(this, varargin) + function writeStrings(this, varargin) if ~isempty(varargin) + % Concatenate commands and send to the device - cmd_str=join(varargin,';'); - cmd_str=cmd_str{1}; + 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 = queryCommand(this, varargin) + 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); + 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),';'); + 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.']) 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 % 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 % 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 end end