diff --git a/@MyCollector/MyCollector.m b/@MyCollector/MyCollector.m index f28cd77..9c27565 100644 --- a/@MyCollector/MyCollector.m +++ b/@MyCollector/MyCollector.m @@ -1,205 +1,205 @@ classdef MyCollector < MySingleton & matlab.mixin.Copyable properties (Access = public, SetObservable = true) InstrList = struct() % Structure accomodating instruments InstrProps = struct() % Properties of instruments collect_flag = true end properties (Access = private) Listeners = struct() end properties (Dependent = true) running_instruments end events NewDataWithHeaders end methods (Access = private) % Constructor of a singleton class must be private function this = MyCollector(varargin) p = inputParser; addParameter(p,'InstrHandles',{}); parse(p,varargin{:}); if ~isempty(p.Results.InstrHandles) cellfun(@(x) addInstrument(this,x),p.Results.InstrHandles); end end end methods (Access=public) function delete(this) cellfun(@(x) deleteListeners(this,x), this.running_instruments); end function addInstrument(this, name, Instrument) assert(isvarname(name), ['Instrument name must be a valid ' ... 'MATLAB variable name.']) assert(~ismember(name, this.running_instruments), ... ['Instrument ' name ' is already present in the ' ... 'collector. Delete the existing instrument before ' ... 'adding a new one with the same name.']) if ismethod(Instrument, 'readSettings') %Defaults to read header this.InstrProps.(name).header_flag = true; else % If the class does not have a header generation function, % it can still be added to the collector and transfer data % to Daq this.InstrProps.(name).header_flag = false; warning(['%s does not have a readSettings function, ',... 'measurement headers will not be collected from ',... 'this instrument.'],name) end this.InstrList.(name) = instr_handle; % If the added instrument has a newdata event, we add a % listener for it. if ismember('NewData', events(this.InstrList.(name))) this.Listeners.(name).NewData=... addlistener(this.InstrList.(name),'NewData',... @(~, EventData) acquireData(this, name, EventData)); end %Cleans up if the instrument is closed this.Listeners.(name).Deletion=... addlistener(this.InstrList.(name),'ObjectBeingDestroyed',... @(~,~) deleteInstrument(this,name)); end % Store instrument GUI function addInstrumentGui(this, instr_name, Gui) assert(ismember(instr_name, this.running_instruments), ... 'Name must correspond to one of the running instruments.') this.InstrProps.(name).Gui = Gui; end % Store instrument GUI function Gui = getInstrumentGui(this, instr_name) assert(ismember(instr_name, this.running_instruments), ... 'Name must correspond to one of the running instruments.') if isfield(this.InstrProps.(name), 'Gui') && ... isvalid(this.InstrProps.(name).Gui) Gui = this.InstrProps.(name).Gui; else Gui = []; end end function acquireData(this, name, InstrEventData) src = InstrEventData.Source; % Check that event data object is MyNewDataEvent, % and fix otherwise if ~isa(InstrEventData,'MyNewDataEvent') InstrEventData = MyNewDataEvent(); InstrEventData.new_header = true; InstrEventData.Trace = copy(src.Trace); end % Add instrument name InstrEventData.src_name = name; % Collect the headers if the flag is on and if the triggering % instrument does not request suppression of header collection if this.collect_flag && InstrEventData.new_header % Add field indicating the time when the trace was acquired TimeMdt = MyMetadata.time('title', 'AcquisitionTime'); AcqInstrMdt = MyMetadata('title', 'AcquiringInstrument'); addParam(AcqInstrMdt, 'Name', InstrEventData.src_name); Mdt = mdt2str([AcqInstrMdt,TimeMdt,acquireHeaders(this)]); %We copy the MeasHeaders to both copies of the trace - the %one that is with the source and the one that is forwarded %to Daq. - InstrEventData.Trace.MeasHeaders = Mdt; + InstrEventData.Trace.MeasHeaders = copy(Mdt); src.Trace.MeasHeaders = copy(Mdt); end triggerNewDataWithHeaders(this, InstrEventData); end % Collects headers for open instruments with the header flag on function Mdt = acquireHeaders(this) Mdt = MyMetadata.empty(); for i=1:length(this.running_instruments) name = this.running_instruments{i}; if this.InstrProps.(name).header_flag try TmpMdt = readSettings(this.InstrList.(name)); TmpMdt.title = name; Mdt = [Mdt, TmpMdt]; %#ok catch warning(['Error while reading metadata from %s. '... 'Measurement header collection is switched '... 'off for this instrument.'], name) this.InstrProps.(name).header_flag = false; end end end end function bool = isrunning(this, name) assert(~isempty(name),'Instrument name must be specified') assert(ischar(name)&&isvector(name),... 'Instrument name must be a character vector, not %s',... class(name)); bool=ismember(name,this.running_instruments); end function deleteInstrument(this,name) if isrunning(this,name) %We remove the instrument this.InstrList=rmfield(this.InstrList,name); this.InstrProps=rmfield(this.InstrProps,name); deleteListeners(this,name); end end end methods(Static) % Concrete implementation of the singletone constructor. function this = instance() persistent UniqueInstance if isempty(UniqueInstance)||(~isvalid(UniqueInstance)) disp('Creating new instance of MyCollector') this = MyCollector(); UniqueInstance = this; else this = UniqueInstance; end end end methods (Access=private) function triggerNewDataWithHeaders(this,InstrEventData) notify(this,'NewDataWithHeaders',InstrEventData); end %deleteListeners is in a separate file deleteListeners(this, obj_name); end methods function running_instruments = get.running_instruments(this) running_instruments = fieldnames(this.InstrList); end end end diff --git a/@MyMetadata/MyMetadata.m b/@MyMetadata/MyMetadata.m index 65ad2f2..a8b4aa7 100644 --- a/@MyMetadata/MyMetadata.m +++ b/@MyMetadata/MyMetadata.m @@ -1,437 +1,440 @@ % MyMetadata stores parameter-value pairs and can be saved in a readable % format. % % Metadata parameters can be strings, numerical values or cells, as well as % any arrays and structures of such with arbitrary nesting. Sub-indices are % automatically expanded when saving. classdef MyMetadata < handle & matlab.mixin.CustomDisplay & ... matlab.mixin.SetGet & matlab.mixin.Copyable properties (Access = public) % Header sections are separated by [hdr_spec, title, hdr_spec] title = '' hdr_spec = '==' % Columns are separated by this symbol (space-tab by default) column_sep = ' \t' % Comments start from this symbol comment_sep = '%' line_sep = '\r\n' % Limit for column padding. Variables which take more space than % this limit are ignored when calculating the padding length. pad_lim = 15 end properties (GetAccess = public, SetAccess = protected) ParamList = struct() % Values of parameters ParamOptList = struct() % Options for parameters end methods (Access = public) function this = MyMetadata(varargin) P = MyClassParser(this); processInputs(P, this, varargin{:}); end % Adds a new metadata parameter. function addParam(this, param_name, value, varargin) assert(isvarname(param_name), ['Parameter name must be a ' ... 'valid variable name.']); p = inputParser(); % Format specifier for printing the value addParameter(p, 'fmt_spec', '', @ischar); % Comment to be added to the line addParameter(p, 'comment', '', @ischar); + addParameter(p, 'SubStruct', struct('type',{},'subs',{}),... @isstruct) parse(p, varargin{:}); - - % Initialize property with an empty variable of the same class - % as value, this is important if SubStruct is an array index. - this.ParamList.(param_name) = feval([class(value),'.empty']); % Make sure that the comment does not contain new line or % carriage return characters, which would mess up formating % when saving the metadata [comment, is_mod] = toSingleLine(p.Results.comment); this.ParamOptList.(param_name).comment = comment; if is_mod warning(['Comment string for ''%s'' has been ' ... 'converted to single line.'], param_name); end this.ParamOptList.(param_name).fmt_spec = p.Results.fmt_spec; S = p.Results.SubStruct; if isempty(S) this.ParamList.(param_name) = value; else + % Initialize property with an empty variable of the same + % class as value, this is important if SubStruct is an + % array index. + this.ParamList.(param_name) = ... + feval([class(value),'.empty']); + % Construct full subscript reference with respect to 'this' S = [struct('type', '.', 'subs', param_name), S]; % Assign the value of parameter this.ParamList = subsasgn(this.ParamList, S, value); end end function bool = isparam(this, param_name) bool = isfield(param_name, this.ParamList); end % Alias for addParam that is useful to ensure the correspondence % between metadata parameter names and object property names. function addObjProp(this, Obj, tag, varargin) addParam(this, tag, Obj.(tag), varargin{:}); end % Print metadata in a readable form function str = mdt2str(this) % Make the function spannable over arrays if isempty(this) str = ''; return elseif length(this) > 1 str_arr = arrayfun(@(x)mdt2str(x), this, ... 'UniformOutput', false); str = [str_arr{:}]; return end % Compose the list of parameter names expanded over subscripts % except for those which are already character arrays par_names = fieldnames(this.ParamList); % Expand parameters over subscripts, except for the arrays of % characters exp_par_names = cell(1, length(par_names)); max_nm_arr = zeros(1, length(par_names)); for i=1:length(par_names) % Expand parameter subscripts exp_par_names{i} = printSubs( ... this.ParamList.(par_names{i}), ... 'own_name', par_names{i}, ... 'expansion_test', @(y) ~ischar(y)); % Max name length for this parameter including subscripts max_nm_arr(i) = max(cellfun(@(x) length(x), ... exp_par_names{i})); end % Calculate width of the name column name_pad_length = min(max(max_nm_arr), this.pad_lim); % Compose a list of parameter values converted to char strings val_strs = cell(1, length(par_names)); % Width of the values column will be the maximum parameter % string width val_pad_length = 0; for i=1:length(par_names) tmp_nm = par_names{i}; % Iterate over parameter indices for j=1:length(exp_par_names{i}) tmp_exp_nm = exp_par_names{i}{j}; TmpS = str2substruct(tmp_exp_nm); % Get the indexed value of parameter TmpS = [struct('type','.','subs', tmp_nm), TmpS]; %#ok tmp_val = subsref(this.ParamList, TmpS); %Do the check to detect unsupported data type if ischar(tmp_val) && ~isvector(tmp_val) && ... ~isempty(tmp_val) warning(['Argument ''%s'' is a multi-dimensional ',... 'character array. It will be converted to ',... 'single string during saving. Use cell',... 'arrays to save data as a set of separate ',... 'strings.'], tmp_exp_nm) % Flatten tmp_val = tmp_val(:); end % Check for new line symbols in strings if (ischar(tmp_val) || isstring(tmp_val)) && ... any(ismember({newline, sprintf('\r')},tmp_val)) warning(['String values must not contain ' ... '''\\n'' and ''\\r'' symbols, replacing ' ... 'them with '' ''.']); tmp_val = replace(tmp_val, ... {newline, sprintf('\r')}, ' '); end fmt_spec = this.ParamOptList.(tmp_nm).fmt_spec; if isempty(fmt_spec) % Convert to string with format specifier % extracted from the varaible calss val_strs{i}{j} = var2str(tmp_val); else val_strs{i}{j} = sprintf(fmt_spec, tmp_val); end % Find maximum length to determine the colum width, % but, for beauty, do not account for variables with % excessively long value strings tmplen = length(val_strs{i}{j}); if (val_pad_length else str = [str, sprintf([par_fmt_spec, ls],... exp_par_names{i}{j}, val_strs{i}{j})]; %#ok end end end % Prints an extra line separator at the end str = [str, sprintf(ls)]; end % Save metadata to a file function save(this, filename) fileID = fopen(filename,'a'); fprintf(fileID, mdt2str(this)); fclose(fileID); end % Create a structure from metadata array function MdtList = arrToStruct(this) MdtList = struct(); for i = 1:length(this) fn = matlab.lang.makeValidName(this(i).title); MdtList.(fn) = this(i); end end end methods (Access = public, Static = true) % Create metadata indicating the present moment of time function TimeMdt = time(varargin) TimeMdt = MyMetadata(varargin{:}); if isempty(TimeMdt.title) TimeMdt.title = 'Time'; end dv = datevec(datetime('now')); addParam(TimeMdt, 'Year', dv(1), 'fmt_spec','%i'); addParam(TimeMdt, 'Month', dv(2), 'fmt_spec','%i'); addParam(TimeMdt, 'Day', dv(3), 'fmt_spec','%i'); addParam(TimeMdt, 'Hour', dv(4), 'fmt_spec','%i'); addParam(TimeMdt, 'Minute', dv(5), 'fmt_spec','%i'); addParam(TimeMdt, 'Second', floor(dv(6)), 'fmt_spec','%i'); addParam(TimeMdt, 'Millisecond',... round(1000*(dv(6)-floor(dv(6)))),'fmt_spec','%i'); end % Load metadata from file. Return all the entries found and % the number of the last line read. function [MdtArr, n_end_line] = load(filename, varargin) fileID = fopen(filename,'r'); MasterMdt = MyMetadata(varargin{:}); % Loop initialization MdtArr = MyMetadata.empty(); line_no = 0; % Loop continues until we reach the next header or we reach % the end of the file while ~feof(fileID) % Grabs the current line curr_line = fgetl(fileID); % Give a warning if the file is empty, i.e. if fgetl % returns -1 if curr_line == -1 disp(['Read empty file ', filename, '.']); break end % Skips if the current line is empty if isempty(deblank(curr_line)) continue end S = parseLine(MasterMdt, curr_line); switch S.type case 'title' % Add new a new metadata to the output list TmpMdt = MyMetadata(varargin{:}, 'title', S.match); MdtArr = [MdtArr, TmpMdt]; %#ok case 'paramval' % Add a new parameter-value pair to the current % metadata [name, value, comment, Subs] = S.match{:}; if ~isparam(TmpMdt, name) % Add new parameter addParam(TmpMdt, name, value, ... 'SubStruct', Subs, 'comment', comment); else % Assign the value to a new subscript of % an existing parameter Subs = [struct('type','.','subs',name), Subs]; %#ok this.ParamList = subsasgn(this.ParamList, ... Subs, value); end otherwise % Exit break end % Increment the counter line_no = line_no+1; end n_end_line = line_no; fclose(fileID); end end methods (Access = protected) % Parse string and determine the type of string function S = parseLine(this, str) S = struct( ... 'type', '', ... % title, paramval, other 'match', []); % parsed output % Check if the line contains a parameter - value pair. % First separate the comment if present pv_token = regexp(str, this.comment_sep, 'split', 'once'); if length(pv_token)>1 comment = pv_token{2}; % There is a comment else comment = ''; % There is no comment end % Then process name-value pair. Regard everything after % the first column separator as value. pv_token = regexp(pv_token{1}, this.column_sep, 'split', ... 'once'); % Remove leading and trailing blanks pv_token = strtrim(pv_token); if length(pv_token)>=2 % A candidate for parameter-value pair is found. Infer % the variable name and subscript reference. [Subs, name] = str2substruct(pv_token{1}); if isvarname(name) % Attempt converting the value to a number value = str2doubleHedged(pv_token{2}); S.type = 'paramval'; S.match = {name, value, comment, Subs}; return end end % Check if the line contains a title title_exp = [this.hdr_spec, '(\w.*)', this.hdr_spec]; title_token = regexp(str, title_exp, 'once', 'tokens'); if ~isempty(title_token) % Title expression found S.type = 'title'; S.match = title_token{1}; return end % No match found S.type = 'other'; S.match = {}; end % Make custom footer for command line display % (see matlab.mixin.CustomDisplay) function str = getFooter(this) if length(this) == 1 % For a single object display its properties str = ['Content:', newline, newline, ... replace(mdt2str(this), sprintf(this.line_sep), ... newline)]; elseif length(this) > 1 % For a non-empty array of objects display titles str = sprintf('\tTitles:\n\n'); for i = 1:length(this) str = [str, sprintf('%i\t%s\n', i, this(i).title)]; %#ok end str = [str, newline]; else % For an empty array display nothing str = ''; end end end end diff --git a/@MyScpiInstrument/MyScpiInstrument.m b/@MyScpiInstrument/MyScpiInstrument.m index b732191..146dedf 100644 --- a/@MyScpiInstrument/MyScpiInstrument.m +++ b/@MyScpiInstrument/MyScpiInstrument.m @@ -1,251 +1,263 @@ % 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; 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); 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 %% 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.']) 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 + + 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