% Class to store data series versus time % Metadata for this class is stored independently classdef MyLog < matlab.mixin.Copyable properties (Access=public) % Save time as posixtime up to ms precision time_fmt = '%14.3f' % Save data as reals with up to 14 decimal digits. Trailing zeros % are removed by %g data_fmt = '%.14g' % Data columns are separated by this symbol data_column_sep = '\t' % File extension that is appended by default when saving the log % if a different one is not specified explicitly data_file_ext = '.log' % File extension for metadata meta_file_ext = '.meta' file_name = '' % Used to save or load the data data_headers = {} % Cell array of column headers length_lim = Inf % Keep the log length below this limit end properties (SetAccess=public, GetAccess=public) timestamps % Times at which data was aqcuired data % Array of measurements TimeLabels % Structure array to store labeled time marks hlines={}; %Cell that contains handles the log is plotted in % Information about the log in saveable format, % including time labels and data headers Metadata end properties (Dependent=true) data_line_fmt % Format specifier for one data line column_headers % Data headers + time header data_file_name % File name with extension for data saving meta_file_name % File name with extension for metadata saving end methods (Access=public) %% Constructo and destructor methods function this = MyLog(varargin) P=MyClassParser(this); processInputs(P, this, varargin{:}); this.Metadata=MyMetadata(P.unmatched_nv{:}); % Create an empty structure array of time labels this.TimeLabels=struct(... 'time',{},... % datetime object 'time_str',{},... % time in text format 'text_str',{},... % message string 'isdispl',{},... % if this label is to be displayed 'text_handles',{}); % handle to the plotted text object % Load the data from file if the file name was provided if ~ismember('file_name', P.UsingDefaults) load(this); %#ok end end %% Save and load functionality % save the entire data record function save(this, fname) % Verify that the data can be saved assertDataMatrix(this); % File name can be either supplied explicitly or given as the % file_name property if nargin()<2 fname = this.file_name; else this.file_name=fname; end assert(~isempty(fname), 'File name is not provided.'); datfname=this.data_file_name; metfname=this.meta_file_name; try stat=createFile(datfname); if stat % Save time labels in separate file save(this.Metadata, metfname, 'overwrite', true); fid = fopen(datfname,'w'); printDataHeaders(this, datfname); % Write data body fmt=this.data_line_fmt; % Convert time stamps to numbers if necessary if isa(this.timestamps,'datetime') time_num_arr=posixtime(this.timestamps); else time_num_arr=this.timestamps; end for i=1:length(this.timestamps) fprintf(fid, fmt, time_num_arr(i), this.data(i,:)); end fclose(fid); end catch warning('Log was not saved'); % Try closing fid in case it is still open try fclose(fid); catch end end end % Load log from file function load(this, fname) if nargin()==2 this.file_name=fname; end assert(~isempty(this.file_name), 'File name is not provided.'); load(this.Metadata, this.meta_file_name); % Convert time to datetime if needed fid=fopen(this.data_file_name,'r'); col_heads=strsplit(fgetl(fid),this.data_column_sep); if length(col_heads)>1 this.data_headers=col_heads(2:end); end % Read data as delimiter-separated values and convert to cell % array, skip the first line containing column headers fulldata = dlmread(this.data_file_name, this.column_sep, 1, 0); this.data = fulldata(:,2:end); this.timestamps = fulldata(:,1); % Convert to datetime if the time column format is posixtime if ~isempty(col_heads) && ... contains(col_heads{1},'posix','IgnoreCase',true) this.timestamps=datetime(this.timestamps); end % Process metadata. if ismember('ColumnNames', this.Metadata.field_names) && ... length(col_heads)<=1 % Assign column headers from metadata here, % if no values are found in the data file this.data_headers=this.Metadata.ColumnNames.Name.value; end if ismember('TimeLabels', this.Metadata.field_names) Lbl=this.Metadata.TimeLabels.Lbl.value; for i=1:length(Lbl) this.TimeLabels(i).time_str=Lbl(i).time_str; this.TimeLabels(i).time=datetime(Lbl(i).time_str); this.TimeLabels(i).text_str=Lbl(i).text_str; end end end %% Plotting % Plot the log data with time labels function plot(this, varargin) % Verify that the data can be plotted assertDataMatrix(this); p=inputParser(); addOptional(p, 'Ax', gca(),... @(x)assert(isa(x,'axes')||isa(x,'uiaxes'),... 'Argument must be axes or uiaxes.')); addParameter(p, 'time_labels', true); addParameter(p, 'legend', true); parse(p, varargin{:}); Ax=p.Results.Ax; [~, ncols] = size(this.data); % Plot data pl_args=cell(1,2*ncols); for i=1:ncols pl_args{2*i-1}=this.timestamps; pl_args{2*i}=this.data(:,i); end plot(Ax, pl_args{:}); % Plot time labels and legend if (p.Results.time_labels) plotTimeLabels(this); end if (p.Results.legend) plotLegend(this); end end function plotTimeLabels(this, Ax) hold(Ax,'on') % Marker line for time labels spans over the entire plot [ymin,ymax]=ylim(Ax); markline = linspace(ymin, ymax, 10); for i=1:length(this.TimeLabels) t=this.TimeLabels(i); % Add line to plot plot(Ax, t.time, markline); % Add text label to plot str=t.text_str; txt_h=text(Ax, posixtime(t.time), 1, str); t.text_handles=[t.text_handles,txt_h]; end hold(Ax,'off') end function plotLegend(this, Ax) % Add legend if n>=1 && ~isempty(this.data_headers{:}) legend(Ax, this.data_headers{:},'Location','northeastoutside'); end end %% Manipulations with log data % Append data points to the log function appendData(this, time, val, varargin) p=inputParser(); addParameter(p, 'save', false, @islogical); parse(p, varargin{:}); % Format checks on the input data assert(isa(time,'datetime')||isnumeric(time),... ['''time'' argument must be numeric or ',... 'of the class datetime.']); assert(iscolumn(time),'Time and array must be column'); assert(ismatrix(val),['Value array must be matrix. ',... 'Use cells to enclose arrays with more dimensions.']) [nrows, ~]=size(val); assert(length(time)==nrows,... 'Lengths of the time and value arrays do not match'); % Append new data and time stamps this.timestamps=[this.timestamps; time]; this.data=[this.data; val]; % Ensure the log length is within the length limit trim(this); % Optionally save the new data to file if p.Results.save try exstat = exist(this.file_name,'file'); if exstat==0 % if the file does not exist, create it and write % the metadata createFile(this.file_name); printAllHeaders(this.Metadata, this.file_name); fid = fopen(this.file_name,'a'); else % otherwise open for appending fid = fopen(this.file_name,'a'); end % Print new values to the file for i=1:length(time) fprintf(fid, this.data_line_fmt, ... posixtime(time(i)), val(i)); end fclose(fid); catch warning(['Logger cannot save data at time = ',... datestr(time(1))]); % Try closing fid in case it is still open try fclose(fid); catch end end end end % Add label to the metadata function addTimeLabel(this, time, str) if nargin()<3 % Invoke a dialog to add the label time and name answ = inputdlg({'Label text', 'Time'},'Add time label',... [2 40; 1 40],{'',datestr(datetime('now'))}); if isempty(answ)||isempty(answ{1}) return else % Conversion of the inputed value to datetime to % ensure proper format time=datetime(answ{2}); str=answ{1}; end end % Need to calculate length explicitly as using 'end' fails % for empty array l=length(this.TimeLabels); this.TimeLabels(l+1).time=time; this.TimeLabels(l+1).time_str=datestr(time); this.TimeLabels(l+1).text_str=str; this.TimeLabels(l+1).isdispl=true; this.TimeLabels(l+1).mark_arr=calcTimeLabelLine(this, time); end % Clear log data and time labels function clear(this) % Clear with preserving the array type this.TimeLabels(:)=[]; this.data_headers(:)=[]; % Clear data and its type this.timestamps = []; this.data = []; clearFields(this.Metadata); end % Verify that the data can be saved or plotted function assertDataMatrix(this) assert(ismatrix(this.data)&&isnumeric(this.data),... ['Data is not a numeric matrix, saving in '... 'text format is not possible.']); end end methods (Access=private) %% Auxiliary private functions % Ensure the log length is within length limit function trim(this) l=length(this.timestamps); if l>this.length_lim dn=l-this.length_lim; this.timestamps(1:dn)=[]; this.data(1:dn)=[]; end end function mark_arr = calcTimeLabelLine(this, time) % Define the extent of marker line to cover the data range % at the point nearest to the time label if isempty(this.timestamps)||this.timestamps(end)