diff --git a/@MyInstrument/MyInstrument.m b/@MyInstrument/MyInstrument.m index 44f1a53..0206fd5 100644 --- a/@MyInstrument/MyInstrument.m +++ b/@MyInstrument/MyInstrument.m @@ -1,258 +1,257 @@ % 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 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); vl = this.CommandList.(tag).value_list; if ~isempty(vl) && ismember('validationFcn', p.UsingDefaults) this.CommandList.(tag).validationFcn = ... createListValidationFcn(this, vl); 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); + % Assign the value with post processing + this.(tag) = p.Results.default; + 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 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 this.CommandList.(tag).last_value = val; % Assign the value with post processing pFcn = this.CommandList.(tag).postSetFcn; if ~isempty(pFcn) val = pFcn(val); end this.(tag) = val; 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 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/@MyScpiInstrument/MyScpiInstrument.m b/@MyScpiInstrument/MyScpiInstrument.m index 038d112..081df48 100644 --- a/@MyScpiInstrument/MyScpiInstrument.m +++ b/@MyScpiInstrument/MyScpiInstrument.m @@ -1,284 +1,296 @@ % 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); % 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)) + value_list = p.Results.value_list; + 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, vl); - this.CommandList.(tag).value_list = long_vl; + % 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 - this.CommandList.(tag).validationFcn = ... - createScpiListValidationFcn(this, [long_vl, short_vl]); - - this.CommandList.(tag).postSetFcn = ... - createToStdFormFcn(this, tag, 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, { ... + 'value_list', value_list, ... + 'validationFcn', validationFcn, ... + 'postSetFcn', postSetFcn}]; + else + + % Append the value list without modification + sub_varargin = [sub_varargin, ... + {'value_list', value_list}]; + end end + % Execute the base class method + addCommand@MyInstrument(this, tag, sub_varargin{:}); + % 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 = @(x) ... assert(isnumeric(x), 'Value must be numeric.'); case 'i' this.CommandList.(tag).validationFcn = @(x) ... assert(floor(x)==x, 'Value must be integer.'); case 's' this.CommandList.(tag).validationFcn = @(x) ... assert(ischar(x), ... 'Value must be character string.'); case 'b' this.CommandList.(tag).validationFcn = @(x) ... assert(x==0 || x==1, 'Value must be logical.'); end end 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 long_vl = setdiff(lower(vl), short_vl); 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 = 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 % 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 end end diff --git a/@MyTekScope/MyTekScope.m b/@MyTekScope/MyTekScope.m index af5441d..d13b8d0 100644 --- a/@MyTekScope/MyTekScope.m +++ b/@MyTekScope/MyTekScope.m @@ -1,199 +1,201 @@ 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{:}); + P = MyClassParser(this); processInputs(P, this, varargin{:}); this.Comm.InputBufferSize = 4.1e7; %byte this.Comm.ByteOrder = 'bigEndian'; end function readTrace(this) % Configure data transfer: binary format and two bytes per % point. Then query the trace. writeCommand(this, ... ':WFMInpre:ENCdg BINary', ... ':DATA:WIDTH 2', ... ':DATA:STARt 1', ... sprintf(':DATA:STOP %i', this.point_no), ... ':CURVE?'); y_data = int16(binblockread(this.Comm, 'int16')); if this.Comm.BytesAvailable~=0 % read the terminating character fscanf(this.Comm, '%s'); end % Read units, offsets and steps for the scales parms = queryCommand(this, ... ':WFMOutpre:XUNit?', ... ':WFMOutpre:YUNit?', ... ':WFMOutpre:YMUlt?', ... ':WFMOutpre:XINcr?', ... ':WFMOutpre:XZEro?', ... ':WFMOutpre:YZEro?', ... ':WFMOutpre:YOFf?'); [unit_y, unit_x, step_x, step_y, x_zero, ... y_zero, y_offset] = parms{:}; % Calculating the y data y = (double(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) writeCommand(this, ... ':ACQuire:STOPAfter RUNSTop', ... ':ACQuire:STATE ON'); end function acquireSingle(this) writeCommand(this, ... ':ACQuire:STOPAfter SEQuence', ... ':ACQuire:STATE ON'); end function turnKnob(this, knob, nturns) - is_knob_valid = any(cellfun(@(x)strcmpi(x, knob), ... - this.knob_list)); + is_knob_valid = ismember(lower(knob), lower(this.knob_list)); assert(is_knob_valid, ['Knob must be a member of the ' ... - 'scope knob list: ', sprintf('%s,', this.knob_list)]) + 'scope knob list: ', newline, var2str(this.knob_list)]) writeCommand(this, ... sprintf(':FPAnel:TURN %s,%i', knob, nturns)); end end methods (Access = protected) function createCommandList(this) - addCommand(this,'channel',':DATa:SOUrce',... + addCommand(this, 'channel',':DATa:SOUrce',... 'format', 'CH%i',... 'info', 'Channel from which the trace is transferred', ... + 'value_list', {1, 2, 3, 4}, ... 'default', 1); addCommand(this, 'ctrl_channel', ':SELect:CONTROl',... 'format', 'CH%i',... 'info', 'Channel currently selected in the scope display', ... 'default', 1); addCommand(this, 'point_no', ':HORizontal:RECOrdlength', ... 'format', '%i', ... 'info', 'Numbers of points in the scope trace', ... 'value_list', {1000, 10000, 100000, 1000000, 10000000}, ... 'default', 100000); addCommand(this, 'time_scale', ':HORizontal:SCAle', ... 'format', '%e', ... 'info', 'Time scale (s/div)', ... 'default', 10E-3); addCommand(this, 'trig_lev', ':TRIGger:A:LEVel',... 'format', '%e', ... 'info', '(V)', ... 'default', 1); addCommand(this, 'trig_slope', ':TRIGger:A:EDGE:SLOpe',... 'format', '%s', ... 'value_list', {'RISe','FALL'}, ... 'default', 'RISe'); addCommand(this, 'trig_source', ':TRIGger:A:EDGE:SOUrce',... 'value_list', {'CH1','CH2','CH3','CH4', ... 'AUX','EXT','LINE'}, ... 'format', '%s', ... 'default', 'AUX'); addCommand(this, 'trig_mode', ':TRIGger:A:MODe', ... 'format', '%s', ... 'value_list', {'AUTO','NORMal'}, ... 'default', 'AUTO'); addCommand(this, 'acq_state', ':ACQuire:STATE', ... 'format', '%b',... 'info', 'State of data acquisition by the scope', ... 'default', true); 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'}, ... 'default', 'HIRes'); % Parametric commands for i = 1:this.channel_no i_str = num2str(i); addCommand(this,... ['cpl',i_str],[':CH',i_str,':COUP'],... 'default','DC', ... 'value_list', {'AC','DC','GND'},... 'format','%s',... 'info','Channel coupling: AC, DC or 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', {'FIFty','MEG'}, ... 'default', 'MEG'); % Offset addCommand(this, ... ['offset',i_str], [':CH',i_str,':OFFSet'], ... 'format', '%e', ... 'info', '(V)', ... 'default', 0); addCommand(this, ... ['scale',i_str], [':CH',i_str,':SCAle'], ... 'format', '%e', ... 'info', 'Channel y scale (V/div)', ... 'default', 1); addCommand(this,... ['enable',i_str], [':SEL:CH',i_str], ... 'format', '%b',... 'info', 'Channel enabled', ... 'default', true); end 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