classdef ardulogparse < handle
    %ardulogparse Summary of this class goes here
    %   Detailed explanation goes here

    %   Copyright 2023-2024 The MathWorks, Inc.

    properties

        %FileHandle Handle to log file
        FileHandle

        %FileDataArray Array of bytes containing log data in the input file
        FileDataArray

        %MsgFormat Format of the message
        MsgFormat

        %Header Message header
        Header = [0xA3 0x95]

        %FormatMsgId Id of the format message (FMT)
        FormatMsgId = 128

        %FormatMsgLen Length of the format message
        FormatMsgLen = 89

        %FormatMessages Format structure of all the messages in the log
        %file
        FormatMessages

        %FormatMap Map object for the format key and format value pairs
        FormatMap

        %UnitMap Map between units characters and unit values
        UnitMap

        %DataTypeMap Map between C and MATLAB datatypes
        DataTypeMap

        %MsgTypeNameMap Map between message type to message name
        MsgTypeNameMap

        %MsgNameTypeMap Map between message name to message type
        MsgNameTypeMap

        %MultMap Map between multiplier key and values
        MultMap

        %FormatMsgUnit The format of FMTU message
        FormatMsgUnit

        %MessageData The message data of the entire log file
        MessageData

        %MultIdToValueMap - Map between multiplier Id and value
        MultIdToValueMap

        %UnitToValueMap - Map between units Id and value
        UnitToValueMap

        %HeaderIdx Header indices of messages in the log file
        HeaderIdx

        %AvailableMsgs Table containing summary of all the available
        %message
        AvailableMsgs

        %MsgsTable Table of all the messages in the log file
        MsgsTable

        %MultMsg Multiplier message (MULT) in log file
        MultMsg

        %UnitMsg Unit message (UNIT) in log file
        UnitMsg

    end

    properties(Constant)

        %FormatKey Format characters in the format string of log message
        FormatKey = {'a','b','B','h','H','i','I','f','d','n','N','Z','c','C','e',...
            'E', 'L', 'M', 'q', 'Q'};

        %FormatValue Value of the format characters
        FormatValue = {{32*2,'int16_t'},{1,'int8_t'},{1,'uint8_t'},{2,'int16_t'},...
            {2,'uint16_t'},{4,'int32_t'},{4,'uint32_t'},{4,'float'},...
            {8, 'double'},{4,'char'},{16,'char'},{64,'char'},{2,'int16_t'},...
            {2,'uint16_t'},{4,'int32_t'},{4,'uint32_t'},{4,'int32_t'},{1,'uint8_t'},...
            {8,'int64_t'},{8,'uint64_t'}};

        %UnitKey Units characters in the log message
        UnitKey = {'-','?','A','a','d','b','k','D','e','E','G','h','i','J','l',...
            'L','m','n','N','o','O','%','S','s','q','r','U','u','v','P','w','W',...
            'X','y','Y','z','#'};

        %UnitValue Value for the units characters
        UnitValue = {"","UNKNOWN","A","Ah","deg","B","deg/s","deglatitude","deg/s/s",...
            "rad/s","Gauss","degheading","A.s","W.s","l","rad/s/s","m","m/s",...
            "N","m/s/s","degC","%","satellites","s","rpm","rad","deglongitude",...
            "ppm","V","Pa","Ohm","Watt","W.h","l/s","us","Hz","instance"};

        %MultKey Key values of the multipliers
        MultKey = {'-','?','2','1','0','A','B','C','D','E','F','G',...
            'I','!','/'};

        %MultValue Multiplier values
        MultValue = {0,1,1e02,1e01,1e0,1e-1,1e-2,1e-3,1e-4,1e-5,1e-6,...
            1e-7,1e-9,3.6,3600};

        %DataTypeKey Datatype in C
        DataTypeKey =  {'int8_t','uint8_t','int16_t','uint16_t','int32_t', ...
            'uint32_t', 'int64_t', 'uint64_t', 'float', 'double', ...
            'bool', 'char'};
        %DataTypeValue MATLAB datatype values for the C datatypes
        DataTypeValue = {'int8','uint8','int16','uint16','int32','uint32', ...
            'int64', 'uint64', 'single', 'double', 'int8', 'char'};

    end


    methods
        function obj = ardulogparse(logFile)
            %ardulogparse

            % Create maps

            % Map between format key and C datatype and size in bytes
            obj.FormatMap = containers.Map(obj.FormatKey,obj.FormatValue);

            % Map between unit key and value
            obj.UnitMap = containers.Map(obj.UnitKey,obj.UnitValue);

            % Map between multiplier key and values
            obj.MultMap = containers.Map(obj.MultKey,obj.MultValue);

            % Map between C and MATLAB datatype
            obj.DataTypeMap = containers.Map(obj.DataTypeKey, obj.DataTypeValue);

            % Load the log file for parsing
            obj.loadFile(logFile);

        end

        function loadFile(obj,logFile)
            %This function loads the input file and calls functions to parse
            %the header, definition and data sections of the file.
            %
            % loadFile(OBJ, LOGFILE) loads the input file, and calls functions
            % readFileHeader, readFileDefinitions, and readFileData for parsing
            % the file. Input OBJ is an object of class ulogparse and input
            % LOGFILE is a string or character vector storing the file path of
            % the input file.

            validateattributes(logFile, {'string', 'char'}, ...
                {'scalartext', 'nonempty'}, 'ardupilotreader', 'filepath');
            robotics.internal.validation.findFilePath(convertStringsToChars ...
                (logFile), 'ardupilotreader', 'filepath');

            obj.FileHandle = fopen(logFile,'r');

            if obj.FileHandle == -1
                error(message('uav:robotuav:ardupilotreader:FileHandle', logFile));
            end

            try
                %read file data
                % FileDataArray contains the data of the file in a uint8
                % array
                obj.FileDataArray = fread(obj.FileHandle,'*uint8');
                obj.FileDataArray = uint8(obj.FileDataArray');
                fclose(obj.FileHandle);
            catch err
                fclose(obj.FileHandle);
                throw(err);
            end

            % Find message headers in log file
            obj.HeaderIdx = strfind(obj.FileDataArray, [obj.Header]);

            if isempty(obj.HeaderIdx)
                error(message('uav:robotuav:ardupilotreader:InvalidLogFile'));
            end

            % Read format messages
            msgs = readMsgsFormat(obj);
            obj.FormatMessages = msgs;
            % Read FMTU messages to get units and multipliers
            obj.FormatMsgUnit = readFMTU(obj);
            % Read the "MULT" and "UNIT" messages to get multiplier and
            % units information
            obj.MultIdToValueMap = readMultMsg(obj);
            obj.UnitToValueMap = readUnitMsg(obj);

            % Read all remaining messages
            msgTypeList = obj.FormatMessages.Type;
            obj.MessageData = obj.createMsgsTable(msgTypeList);

            % Select relevant fields "MsgName", "InstanceID",
            % "StartTimestamp", "lastTimestamp", "NumMessages"
            obj.AvailableMsgs = obj.MessageData(:,[1 2 3 4 7]);
            obj.AvailableMsgs.Properties.RowNames = {};

        end

        function [startTime, endTime] = getStartEndTime(~,msgDataTable)
            %getStartEndTime Get start and end time of logging in the log
            %file

            ignoredMsgs = ["FMT","FMTU","MULT","UNIT"];
            validRows = ~any(msgDataTable.MsgName == ignoredMsgs,2);
            msgDataTbl = msgDataTable(validRows,:);
            startTime = min(msgDataTbl.StartTimestamp);
            endTime = max(msgDataTbl.LastTimestamp);
        end

        function msgsTable = createMsgsTable(obj,msgTypeList)
            %createMsgsTable Create message table for the log file

            rowNames = [];
            for k = 1:numel(msgTypeList)
                [msgName, msgInstance, startTimeStamp, lastTimeStamp, ...
                    msgDataTable,msgFormatTable,newMsgName, numMsg] = parseMsg(obj, msgTypeList(k));
                % The message names will be same for the different message
                % instances but instance value will be different.
                if ~isempty(msgDataTable)
                    obj.MsgsTable = [obj.MsgsTable;table(msgName',msgInstance',startTimeStamp', lastTimeStamp', ...
                        msgDataTable, msgFormatTable, numMsg')];
                    % The row names need to be unique hence adding newMsgName.
                    % row names are included to enable query by message name.
                    rowNames = [rowNames;newMsgName'];
                end
            end
            msgsTable = obj.MsgsTable;
            msgsTable.Properties.VariableNames = {'MsgName','InstanceID',...
                'StartTimestamp', 'LastTimestamp', 'MsgData','MsgFormat','NumMessages'};
            msgsTable.Properties.RowNames = rowNames;
        end

        function [unit, mult] =  msgToUnitMult(obj, msgName)
            %msgToUnitMult Get the units and multiplier values of the
            %message

            % Get units and multipliers from FMTU message
            unitIdsRaw = obj.FormatMsgUnit(msgName,:).UnitIds;
            multIdsRaw = obj.FormatMsgUnit(msgName,:).MultIds;
            unitIds = deblank(unitIdsRaw{:});
            multIds = deblank(multIdsRaw{:});
            unit = values(obj.UnitToValueMap, cellstr(unitIds(:)));
            mult = values(obj.MultIdToValueMap, cellstr(multIds(:)));

        end

        function bMsgTypeIdx = validMsgHeaderIdx(~, logData, headerIdx, msgType)
            %validMsgHeaderIdx Get valid header indices (logical array) for
            % a given message type. Handle end of array edge case

            % The message type value is present after the last header index
            % [163 149 Message type]. Check for existence of messsage type 
            % Find header index that does not exceed size of log data
            headerIdxLast = numel(headerIdx);
            while headerIdx(headerIdxLast) + 2 > numel(logData)
                headerIdxLast = headerIdxLast - 1;
            end
            bMsgTypeIdx = (logData(headerIdx(1:headerIdxLast)+2) == msgType);          
           
        end

        function msgs = readMsgsFormat(obj)
            %readMsgsFormat Read the format messages. This contains
            %information to decode all the messages in the log file

            % Find the format messages. Format message Id = 128.
            % logical indices array for the message type
            bMsgTypeIdx = obj.validMsgHeaderIdx(obj.FileDataArray, obj.HeaderIdx, obj.FormatMsgId);

            % Valid message indices
            fmtMsgIdx = obj.HeaderIdx(bMsgTypeIdx);
            fmtMsgLen = obj.FormatMsgLen;
            % Validate format message indices
            fmtMsgIdx = obj.validateMsgIndex(obj.FileDataArray, fmtMsgIdx, fmtMsgLen);

            if isempty(fmtMsgIdx)
                error(message('uav:robotuav:ardupilotreader:NoFormatMsg'));
            else
                % Get format messages. Decoding format = 'BBnNZ' - 'Type',
                % 'Length','Name', Format', 'Columns'
                % B - 'unit8', n - Char[4], N - Char[16], Z - Char[64]
                % Initialize arrays
                msgId = uint8(zeros(numel(fmtMsgIdx),1));
                msgType = uint8(zeros(numel(fmtMsgIdx),1));
                msgLength = uint8(zeros(numel(fmtMsgIdx),1));
                msgName = strings(numel(fmtMsgIdx),1);
                msgFormat = strings(numel(fmtMsgIdx),1);
                msgLabels = strings(numel(fmtMsgIdx),1);
                for k = 1:numel(fmtMsgIdx)
                    % Note: first 3 bytes are header + message Id
                    msgId(k) = obj.FileDataArray(fmtMsgIdx(k) + 2);
                    msgType(k) = obj.FileDataArray(fmtMsgIdx(k) + 3);
                    msgLength(k) = obj.FileDataArray(fmtMsgIdx(k) + 4);
                    msgName(k) = string(char(obj.FileDataArray(fmtMsgIdx(k) + 5:fmtMsgIdx(k)+ 8)));
                    msgFormat(k) = string(char(obj.FileDataArray(fmtMsgIdx(k) + 9:fmtMsgIdx(k)+ 24)));
                    msgLabels(k) = string(char(obj.FileDataArray(fmtMsgIdx(k) + 25:fmtMsgIdx(k)+ 88)));
                end
            end
            msg.Name = deblank(msgName);
            msg.Id = msgId;
            msg.Type = msgType;
            msg.Length = msgLength;
            msg.Format = deblank(msgFormat);
            msg.Labels = deblank(msgLabels);
            % Create format message table
            % Check for duplicate messages. Ignore duplicate messages.
            [dupMsg, errMsg, warningState] = verifyDuplicateMsg(obj,msg);

            switch warningState
                case "Duplicate"
                    msgStr = strjoin(dupMsg);
                    warning(message('uav:robotuav:ardupilotreader:DuplicateWarning', msgStr))
                    [msgName, uniqueIdx] = unique(msgName,'stable');
                    msgType = msgType(uniqueIdx);
                    msgLength = msgLength(uniqueIdx);
                    msgFormat = msgFormat(uniqueIdx);
                    msgLabels = msgLabels(uniqueIdx);
                case "Error"
                    msgStr = strjoin(errMsg);
                    error(message('uav:robotuav:ardupilotreader:DuplicateError', msgStr));
            end

            % Create table with unique message ids
            msgs = table(msgType,msgLength,msgName,msgFormat,msgLabels);

            msgs.Properties.VariableNames = {'Type','Length','Name','Format','Labels'};
            msgs.Properties.RowNames = deblank(msgName');

            %create map between message types and message names
            msgTypeKey = msgType;
            msgNameValue = deblank(msgName);

            obj.MsgTypeNameMap = containers.Map(msgTypeKey, msgNameValue);
            obj.MsgNameTypeMap = containers.Map(msgNameValue, msgTypeKey);

        end

        function [dupMsg, errMsg, warningState] = verifyDuplicateMsg(~,msg)
            %verifyDuplicateMsg Message FMT is duplicate when all the
            %fields like Id, Type, Format, length, labels are all same.
            %Duplicate message is invalid or wrong when at least one of the
            %message field is wrong.

            % Create message name and message info map
            map = containers.Map;
            % No Warning - log file is valid with no duplicate message
            % Warning - log file is valid but with duplicate message
            % Error - log file is invalid
            warningState = "No Warning";
            dupMsgCount = 1;
            errMsgCount = 1;
            errMsg = strings;
            dupMsg = strings;

            for k = 1:numel(msg.Name)

                % Check for duplicate messages in the map
                if isKey(map,msg.Name(k))
                    val = values(map, {msg.Name(k)});
                    existingMsg = val{:};
                    currentMsg = {msg.Id(k) msg.Type(k) msg.Length(k) msg.Format(k) msg.Labels(k)};
                    % Check for equality of message. Message is equal if all the fields
                    % name, id, type, length, format and labels are same
                    msgEqual = all(arrayfun(@(existingMsg,currentMsg ) ...
                        isequal(existingMsg,currentMsg ),existingMsg,currentMsg ));
                    if msgEqual
                        % The new message and existing message are same. Hence
                        % duplicate message
                        warningState = "Duplicate";
                        dupMsg(dupMsgCount) = msg.Name(k);
                        dupMsgCount = dupMsgCount + 1;
                    else
                        % The messages differ in at least one field
                        warningState = "Error";
                        errMsg(errMsgCount) = msg.Name(k);
                        errMsgCount = errMsgCount + 1;
                    end
                else
                    % Add message to the map
                    map(msg.Name(k)) = {msg.Id(k) msg.Type(k) msg.Length(k) msg.Format(k) msg.Labels(k)};
                end
            end
        end

        function msgData = readFMTU(obj)
            %readFMTU Read the format message with multipliers and units.
            %Create a map between message and its units and multipliers

            % Get message type from message name
            % "FMTU" definition exits in FMT message. We are assuming the
            % "FMTU" of all messages exist in the log file.
            if isKey(obj.MsgNameTypeMap,"FMTU")
                msgType = obj.MsgNameTypeMap("FMTU");
                [~, ~, ~, ~, mDataTable, ~, ~] = parseMsg(obj, msgType);
            else
                % Only "FMTU" definition exists but no message data
                mDataTable = table;
            end

            if ~isempty(mDataTable)
                % Extract message data
                msgData = mDataTable{1};
                % Fields of the FMTU message - 'TimeUS', 'FmtType', 'UnitIds',
                % 'MultsIds'
                % Get units and multipliers of the messages                
                % Remove duplicate FmtType from msgData table
                [msgTypeKey, idx] = unique(msgData.FmtType,'stable');
                % Update table
                msgData = msgData(idx,:); 
                msgNames = values(obj.MsgTypeNameMap,num2cell(msgTypeKey));
                msgData.Properties.RowNames = msgNames;
            else
                msgData = table;
            end
        end

        function map = readMultMsg(obj)
            %readMultMsg Read "MULT" message and create map between
            % multiplier Id and its value

            if isKey(obj.MsgNameTypeMap,"MULT")
                msgType = obj.MsgNameTypeMap("MULT");
                [~, ~, ~, ~, mDataTable, ~, ~] = parseMsg(obj, msgType);
            else
                mDataTable = table;
            end

            if ~isempty(mDataTable)
                msgData = mDataTable{1};
                % Get units of messages with units
                multId = cellstr(char(msgData.Id));
                multValue = msgData.Mult;
                % Map multiplier Id to value
                map = containers.Map(multId,multValue);
            else
                % Use default multiplier mapping
                map = obj.MultMap;
            end
        end

        function map = readUnitMsg(obj)
            %readUnitMsg Read "UNIT" message and create map between units
            %id and value

            if isKey(obj.MsgNameTypeMap,"UNIT")
                msgType = obj.MsgNameTypeMap("UNIT");
                [~, ~, ~, ~, mDataTable, ~, ~] = parseMsg(obj, msgType);
            else
                mDataTable = table;
            end

            if ~isempty(mDataTable)
                msgData = mDataTable{1};
                % Get units of messages with units
                msgUnitIds = cellstr(char(msgData.Id));
                msgUnitValue = cellstr(msgData.Label);
                map = containers.Map(msgUnitIds,msgUnitValue);
            else
                % Use default units mapping
                map = obj.UnitMap;
            end
        end

        function msgData = readMessages(obj)
            %readMsgData Extract the message data given the message type

            % Exclude "NumMessages" field
            msgData = obj.MessageData(:,1:end-1);
            msgData.Properties.RowNames = {};

        end

        function unitValues = unitKeyToVal(obj,unitKey)
            %unitKeyToVal Get the unit values

            units = cellstr(unitKey(:));
            unitValues = values(obj.UnitMap, units);

        end

        function loggedMsg = readLoggedOutput(obj, varargin)
            %readLoggedOutput Read the logged messages as a table

            % Check if log message "MSG" definition exits in log file
            if isKey(obj.MsgNameTypeMap,"MSG")
                msgType = obj.MsgNameTypeMap("MSG");
                [~, ~, ~, ~, logMsgTbl, ~, ~] = parseMsg(obj, msgType);
            else
                % Create empty table
                logMsgTbl = table;
            end

            % Check if log message exists in log file
            if isempty(logMsgTbl)
                error(message('uav:robotuav:ardupilotreader:NoLogMsg'));
            else
                loggedMsg = logMsgTbl{1};
            end

        end

        function paramMsg = readParameters(obj, varargin)
            %readParameters Read the parameter messages

            % Check if PARM message definition exits in log file
            if isKey(obj.MsgNameTypeMap,"PARM")
                msgType = obj.MsgNameTypeMap("PARM");
                [~, ~, ~, ~, paramTbl, ~, ~] = parseMsg(obj, msgType);
            else
                % Create empty table
                paramTbl = table;
            end

            % Check if PARM message exists in log file
            if isempty(paramTbl)
                error(message('uav:robotuav:ardupilotreader:NoParamMsg'));
            else
                paramMsg = paramTbl{1};
            end

        end

        function [msgName, msgInstance, startTimeStamp, lastTimeStamp, ...
                mDataTable, mFormatTable, newMsgName, numMsg] = parseMsg(obj, msgTypes)
            %parseMsg Parse the message contained in the log file

            % Message header index
            headerIdx =  obj.HeaderIdx;

            % Initialize
            msgName = [];
            startTimeStamp = [];
            lastTimeStamp = [];

            mDataTable = [];
            mFormatTable = [];
            newMsgName = [];
            msgInstance = 0;
            numMsg = 0;

            for msgType = msgTypes'

                % Message contains time field
                isTimeField = false;
                % Get message name from the format message (FMT)
                msgName = deblank(obj.FormatMessages.Name(obj.FormatMessages.Type == msgType));
                msgFormat = struct;
                % Message header to search in log file
                headerWithMsgType = [obj.Header msgType];

                % Find row containing format of message
                rows = obj.FormatMessages.Type == msgType;
                % Get the message labels/fields
                labels = deblank(strsplit(obj.FormatMessages.Labels(rows),','));

                % Get the format of the message e.g. 'QNf'
                msgTypeFormat = deblank(char(obj.FormatMessages.Format(rows)));

                % logical indices array for the message type
                bMsgTypeIdx = obj.validMsgHeaderIdx(obj.FileDataArray, headerIdx, msgType);
               
                % Valid message indices for the message type
                msgIdx = headerIdx(bMsgTypeIdx);

                % Length of message including header (3 bytes)
                msgLen = obj.FormatMessages.Length(rows);

                % Remove invalid message indices
                msgIdx = validateMsgIndex(obj, obj.FileDataArray, msgIdx, double(msgLen));

                %Initialize the message labels/fields
                msgCols = cell(1,numel(labels));

                % read all data of a message type
                if ~isempty(msgIdx)

                    actMsgLen = double(msgLen) - numel(headerWithMsgType);
                    % Get the message data in matrix form. The first three bytes are
                    % the header [163 149] and the message Id.
                    msgIdxArray = msgIdx(:) + (0:actMsgLen-1) + numel(headerWithMsgType);

                    % Get a chunk of the message data. Each row contains
                    % message data for the message type a particular
                    % timestamp
                    msgArray = obj.FileDataArray(msgIdxArray);
                    numMsg = size(msgArray,1);
                    labelIdx = 1;

                    % populate the labels/fields
                    for numLabel = 1:numel(labels)

                        % Get the format key for the label/field
                        formatKey = msgTypeFormat(numLabel);

                        % Get the format value and the size
                        labelData = obj.FormatMap(formatKey);

                        % Get the MATLAB data type
                        mDataType = obj.DataTypeMap(labelData{2});

                        % Length in bytes of the label/field
                        labelLen = labelData{1};
                        % Name of the field
                        labelName = labels(numLabel);

                        % Initialize multiplier values
                        multValue = num2cell(ones(numel(labels),1));

                        % Update the message format
                        msgFormat.Labels{numLabel} = labels(numLabel);
                        msgFormat.Type{numLabel} = labelData{2};
                        msgFormat.Size{numLabel} = labelLen;
                        msgFormatTable = table(msgFormat.Labels', msgFormat.Type', ...
                            msgFormat.Size','Variablename',{'Label','Type','Size'});

                        % Units and multipliers for these messages are
                        % ignored
                        ignoredMsgs = {'FMT', 'FMTU', 'MULT','UNIT'};
                        % Not any of the above message type
                        if ~any(msgName == ignoredMsgs)
                            % Get units and multipliers of the message
                            % fields/labels
                            [unit, multValue] = obj.unitsMultValues(msgName,labels,numLabel);

                            msgFormat.Unit{numLabel} = unit{numLabel};
                            msgFormatTable = table(msgFormat.Labels', msgFormat.Type', ...
                                msgFormat.Size', msgFormat.Unit', 'Variablename',{'Label','Type','Size', 'Units'});
                        end
                        % Decode the message field
                        msgCols(:,numLabel) = obj.decodeFormat(labelLen, ...
                            mDataType, msgArray, labelIdx, multValue{numLabel});

                        % Convert time to duration type
                        if strcmp(labelName,'TimeUS')
                            isTimeField = true;
                            if any(msgName == ignoredMsgs)
                                % Use explicit multiplier value as the
                                % multipliers are not known till the "FMTU"
                                % , "MULT" messages are decoded.
                                multiplier = 1e-06;
                                timeUS = obj.decodeFormat(labelLen, mDataType, ...
                                    msgArray, labelIdx, multiplier);
                            else
                                timeUS = obj.decodeFormat(labelLen, mDataType, ...
                                    msgArray, labelIdx, multValue{numLabel});
                            end

                            durationTimeUS = obj.timestampToDuration(timeUS{:});
                            msgCols(:,numLabel) = {durationTimeUS};
                        end

                        % go to next label/field
                        labelIdx = labelIdx + labelLen;

                    end

                    % Create message data table/timetable
                    msgDataTable = table(msgCols{:},'Variablename',labels);

                    if isTimeField && ~any(msgName == ignoredMsgs)
                        % Message has time information
                        msgDataTable = table2timetable(msgDataTable,'RowTimes',msgDataTable.TimeUS);
                        msgDataTable.Properties.DimensionNames{1} = 'timestamp';
                    end

                    if istimetable(msgDataTable)
                        % Update start and end timestamp for timetable
                        % message
                        startTimeStamp = msgDataTable.Properties.RowTimes(1);
                        lastTimeStamp = msgDataTable.Properties.RowTimes(end);
                    else
                        startTimeStamp = 0;
                        lastTimeStamp = 0;
                    end

                    % Convert to cell
                    mDataTable = {msgDataTable};
                    mFormatTable = {msgFormatTable};
                    newMsgName = msgName;

                    % check if instance message and split into respective
                    % instances
                    msgInstance = 0;
                    [isInstanceMsg, unitIdx] = obj.isInstMessage(msgName);
                    if  isInstanceMsg
                        % Form table for each message instance
                        [mDataTable, msgName, newMsgName, msgInstance, instanceLabel,...
                            startTimeStamp, lastTimeStamp, mFormatTable, numMsg] = ...
                            obj.splitTable(msgDataTable, labels, msgName, msgFormatTable, unitIdx);
                    end
                end
            end
        end

        function [unit, multValue] = unitsMultValues(obj, msgName, labels, numLabel)
            %unitsMultValues Use default values when FMTU message is not
            %present in the log file. This happens for older versions of log
            %files. Use values present in the FMTU message when available.

            if isempty(obj.FormatMsgUnit)
                unit = cellstr(strings(numel(labels),1));
                multValue = repmat({1},numel(labels),1);
                if labels(numLabel) == "TimeUS"
                    multValue{numLabel} = 1e-06;
                end
            else
                [unit, multValue] = obj.msgToUnitMult(msgName);
            end
        end

        function msgIdx = validateMsgIndex(obj, logData, msgIdx, msgLen)
            %validateMsgIndex Remove invalid message indices. A message is
            %valid if there is a valid message header after it. Message
            %index is the index of the first header value (163).

            % Next message has no headers (end of log file) or one header
            % or both headers
            msgIdx = msgIdx(msgIdx + double(msgLen) - 1 <= numel(logData));

            % Next message has one header or two headers
            % msgIdx = msgIdx(msgIdx + double(msgLen) <= numel(logData));

            % Handle two headers existing in next message
            if msgIdx + double(msgLen) + 1 <= numel(logData)
                invalid_header1 = find(logData(msgIdx + msgLen) ~= obj.Header(1));
                invalid_header2 = find(logData(msgIdx + msgLen + 1) ~= obj.Header(2));
                invalidIndex = union(invalid_header1,invalid_header2);
                msgIdx(invalidIndex) = [];
            elseif msgIdx + double(msgLen) <= numel(logData)
                % Only one header in next message hence invalid message
                % index
                msgIdx(end) = [];
            end

        end

        function [bInstMsg, unitIdx] = isInstMessage(obj, msgName)
            %isInstMessage Check if the message is an instance message. The
            %instance message will have a units field with label
            %"instance".

            % These messages don't have an instance field
            ignoredMsgs = {'FMT','FMTU','MULT','UNIT'};
            % Not any of the above message type
            if any(msgName == ignoredMsgs) || isempty(obj.FormatMsgUnit)
                bInstMsg = 0;
                unitIdx = [];
            else
                unitIds = deblank(obj.FormatMsgUnit(msgName,:).UnitIds);
                unitValues = obj.unitKeyToVal(unitIds{:});
                bInstMsg = any([unitValues{:}] == "instance");
                unitIdx = find([unitValues{:}] == "instance");
            end
        end

        function [newtbl, messageName, newMsgName, msgInstance, ...
                instanceLabel, startTimeStamp, lastTimeStamp, msgFormatTable, numMsg] ...
                = splitTable(~,tbl,labels, msgName, msgFmtTable, unitIdx)
            %splitTable Split the message table according to the instances

            msgInstance = [];
            isTimeUS = any(contains(tbl.Properties.VariableNames,"TimeUS"));
            % Label that contains instance information
            instanceLabel = labels(unitIdx);

            y = tbl.(instanceLabel);
            % Find instance groups
            [~,groupId] = findgroups(y);

            % Iterate over number of instance groups
            for k = 1:numel(groupId)
                rows = tbl.(instanceLabel)==groupId(k);
                % Create new instance group table
                newtbl{k} = tbl(rows,:);
                if isTimeUS
                    startTimeStamp(k) = newtbl{k}.TimeUS(1);
                    lastTimeStamp(k) = newtbl{k}.TimeUS(end);
                else
                    startTimeStamp(k) = 0;
                    lastTimeStamp(k) = 0;
                end
                % Add instance ID to the message
                msgInstance(k) = newtbl{k}.(instanceLabel)(1);
                % Add a new message name to index table using message name
                newMsgName{k} = [char(msgName) '_' num2str(k-1)];
                messageName(k) = msgName;
                msgFormatTable{k} = msgFmtTable;
                numMsg(k) = height(newtbl{k});
            end
            newMsgName{end} = char(msgName);
            newtbl = newtbl';
            msgFormatTable = msgFormatTable';
        end

        function msg = decodeFormat(~,len, mDataType, msgArray, labelIdx, multValue)
            %decodeFormat Decode bytes in the log file according to the
            %datatype specified

            % Read data of the label/field
            data = msgArray(:,labelIdx:labelIdx + len - 1);
            if strcmp(mDataType,'char')
                msg = {string(cellstr(char(data)))};
            else
                % multiplier value of 0 implies no multiplication
                if multValue == 0
                    multValue = 1;
                end
                msg = {double(typecast(reshape(data',1,[]),mDataType))*multValue};
                % Reshape message              
                msg = {reshape(msg{1}, [], size(data,1))'};
            end
        end

        function durationTime = timestampToDuration(~,tStampSeconds)
            %timestampToDuration Convert time from seconds to duration

            durationTime = seconds(tStampSeconds(:));
            durationTime.Format = 'hh:mm:ss.SSSSSS';

        end
    end
end
