gusucode.com > vnt工具箱matlab源码程序 > vnt/vntguis/+can/Tool.m

    classdef (Hidden) Tool < handle
% Tool Class that implements the VNT CAN Tool.
%
%   The Tool class contains a full implementation of the CAN Tool feature.
%   It contains properties and methods to use the tool. The class is hidden
%   to prevent direct user access. We expects user to access Tool through
%   our factory function.
%
%   See also VNT.
%

% Copyright 2010-2016 The MathWorks, Inc.

properties
    % BusSpeed - Stores the desired bus speed value provided by the user.
    BusSpeed
    % Channel - A handle to the channel to which the tool is currently
    %   connected when runnning.
    Channel
    % ChannelInfoAll - An array of structures containing constructor 
    %   parameters to build a channel for all available channels.
    ChannelInfoAll
    % ChannelInfoCurrent - A structure containing constructor parameters to
    %   build a channel for the current selection.
    ChannelInfoCurrent
    % Database - A handle to a database object to use for message decoding.
    Database
    % FigureWindow - Handle for the figure window represented by this class.
    FigureWindow
    % MaximumMessageCount - The maximum number of messages to appear in the
    %   tool display.
    MaximumMessageCount = 2500;
    % MenuChildren - Handles to all of the child options on the menu bars.
    MenuChildren
    % MenuConfigure - The menu bar for configuring the tool.
    MenuConfigure
    % MenuFile - The menu bar for operating the tool.
    MenuFile
    % MenuHelp - The menu bar for accessing tool help.
    MenuHelp
    % MenuRun - The menu bar for running the tool.
    MenuRun
    % MenuView - The menu bar for customizing tool view options.
    MenuView
    % MessageTable - Handle to the primary message table of the tool.
    MessageTable
    % MessageTableCellData - A cell array that contains the data currently
    %   shown on the tool's table.
    MessageTableCellData
    % MessageTableHandle - A handle to the primary message table.
    MessageTableHandle
    % MessageLog - An array of message objects that is the current
    %   complete message history logged by the tool.
    MessageLog
    % MessageLogPaused - An array of message objects that is the
    %   frozen set of messages shown on pause.
    MessageLogPaused
    % MessageLogUnique - An array of message objects that contains
    %   only the most recent instance of each unique message to appear in
    %   the tool since last start.
    MessageLogUnique
    % MessageLogUniquePaused - An array of message objects that
    %   contains a frozen set of unique messages shown on pause.
    MessageLogUniquePaused
    % MsgFilterStd - Handle to a message filter object for standard IDs.
    %   This object stores filter settings for the given ID type.
    MsgFilterStd
    % MsgFilterExt - Handle to a message filter object for extended IDs.
    %   This object stores filter settings for the given ID type.
    MsgFilterExt
    % StateNumberFormat - Indicates what numerical format the tool is 
    %   using to display information.
    StateNumberFormat
    % StateShowUniqueMessages - Indicates the current state of the show 
    %   unique message option.
    StateShowUniqueMessages
    % StateTool - Indicates the current run state of the tool.
    StateTool
    % Toolbar - A toolbar for operating the tool.
    Toolbar
    % ToolbarButtons - A structure of handles to toolbar buttons.
    ToolbarButtons
    % UpdateTimer - The timer controlling when the display is updated.
    UpdateTimer
    % MsgInfoMapStd - A map of standard message IDs to message information
    %   stored locally for faster access.
    MsgInfoMapStd
    % MsgInfoMapExt - A map of extended message IDs to message information
    %   stored locally for faster access.
    MsgInfoMapExt
end

properties (Constant)
    % ALLOWED_MESSAGE_COUNT_MAX - The maximum count of messages that
    %   the tool allows the user to display.
    ALLOWED_MESSAGE_COUNT_MAX = 5000;
    % ALLOWED_MESSAGE_COUNT_MIN - The minimum count of messages that
    %   the tool allows the user to display.
    ALLOWED_MESSAGE_COUNT_MIN = 100;
    
    % FIGURE_HEIGHT - The height of the figure in pixels.
    FIGURE_HEIGHT = 670;    
    % FIGURE_WIDTH - The width of the figure in pixels.
    FIGURE_WIDTH = 600;
    
    % FILTERSPEC_DATABASE - The file filter specification to use when
    %   browsing for CANdb files.
    FILTERSPEC_DATABASE = {'*.dbc', 'CANdb Database (*.dbc)'};
    
    % FORMAT_HEXADECIMAL - Indicates that the tool is displaying 
    %   hexadecimal format values.
    FORMAT_HEXADECIMAL = 0;    
    % FORMAT_DECIMAL - Indicates that the tool is displaying decimal 
    %   format values.
    FORMAT_DECIMAL = 1;
    
    % SIGNAL_TABLE_COLUMN_NAMES - Columns names for signal tables.
    SIGNAL_TABLE_COLUMN_NAMES = {'Signal Name' 'Physical Value'};
    
    % STATE_STARTED - Indicates that the tool is started.
    STATE_STARTED = 0;
    % STATE_PAUSED - Indicates that the tool is paused.
    STATE_PAUSED = 1;
    % STATE_STOPPED - Indicates that the tools is stopped.
    STATE_STOPPED = 2;

    % TABLE_BLANK_ROW  - A cell array of values that creates a blank row on
    %   the tool's table.
    TABLE_BLANK_ROW = {' ', ' ', ' ', ' ', ' '};
    
    % UPDATE_RATE - The rate at which the tool display is updated once
    %   started. Units are in seconds.
    UPDATE_RATE = 0.500;
    
    % EMPTY_MSG_STRUCT - Defines default empty structure for messages.
    EMPTY_MSG_STRUCT = struct('ID', {}, 'Timestamp', {}, 'Data', {}, ...
        'Extended', {}, 'Remote', {}, 'Error', {});
end

methods
    
    function obj = Tool()
        % Check for no access to any devices on the system to prevent
        % launching of CAN Tool.
        info = can.HardwareInfo();
        if isempty(info.VendorInfo)
            % Error if no devices are available.
            error(message('vnt:Tool:NoDevicesAvailable'));
        end
        
        % Create the figure according to the defined properties.
        pos = get(0, 'ScreenSize');
        obj.FigureWindow = figure( ...
            'CloseRequestFcn', @obj.closeFigure, ...
            'MenuBar', 'none', ...
            'Name', 'Vehicle CAN Bus Monitor', ...
            'NextPlot', 'new', ...
            'NumberTitle', 'off', ...
            'Position', [(pos(3) - obj.FIGURE_WIDTH) / 2, ...
                (pos(4) - obj.FIGURE_HEIGHT) / 2, ...
                obj.FIGURE_WIDTH, obj.FIGURE_HEIGHT], ...
            'ResizeFcn', @obj.resizeFigure, ...
            'ToolBar', 'none', ...
            'Units', 'pixels', ...
            'Visible', 'off');
        
        % Add the menu bar for tool operation.
        obj.MenuFile = uimenu( ...
            obj.FigureWindow, ...
            'Label', 'File');
        
        obj.MenuChildren.Save = uimenu(obj.MenuFile, ...
            'Accelerator', 'S', ...
            'Callback', @obj.save, ...
            'Label', 'Save Messages');
        
        obj.MenuChildren.Clear = uimenu(obj.MenuFile, ...
            'Accelerator', 'X', ...
            'Callback', @obj.clear, ...
            'Label', 'Clear Messages');
        
        obj.MenuChildren.Exit = uimenu(obj.MenuFile, ...
            'Accelerator', 'Q', ...
            'Callback', @obj.exit, ...
            'Label', 'Exit', ...
            'Separator', 'on');
        
        % Add the menu bar for configuration.
        obj.MenuConfigure = uimenu( ...
            obj.FigureWindow, ...
            'Label', 'Configure');
        
        obj.MenuChildren.Channel = uimenu(obj.MenuConfigure, ...
            'Label', 'Channel', ...
            'CreateFcn', @obj.initChannels);

        obj.MenuChildren.BusSpeed = uimenu(obj.MenuConfigure, ...
            'Label', 'Bus Speed...', ...
            'Callback', @obj.configBusSpeed);
        
        obj.MenuChildren.Filtering = uimenu(obj.MenuConfigure, ...
            'Label', 'Message Filters...', ...
            'Callback', @obj.configFiltering);
        
        obj.MenuChildren.Database = uimenu(obj.MenuConfigure, ...
            'Label', 'Database...', ...
            'Callback', @obj.configDatabase);

        % Add the menu bar for run control.
        obj.MenuRun = uimenu( ...
            obj.FigureWindow, ...
            'Label', 'Run');
        
        obj.MenuChildren.Start = uimenu(obj.MenuRun, ...
            'Accelerator', 'R', ...
            'Callback', @obj.start, ...
            'Label', 'Start', ...
            'Separator', 'on');
        
        obj.MenuChildren.Pause = uimenu(obj.MenuRun, ...
            'Accelerator', 'P', ...
            'Callback', @obj.pause, ...
            'Label', 'Pause');
        
        obj.MenuChildren.Stop = uimenu(obj.MenuRun, ...
            'Accelerator', 'T', ...
            'Callback', @obj.stop, ...
            'Label', 'Stop');

        % Add the menu bar for view control.
        obj.MenuView = uimenu( ...
            obj.FigureWindow, ...
            'Label', 'View');
        
        obj.MenuChildren.MaximumMessageCount = uimenu(obj.MenuView, ...
            'Label', 'Maximum Message Count...', ...
            'Callback', @obj.configMaximumMessageCount);

        obj.MenuChildren.NumberFormat = uimenu(obj.MenuView, ...
            'Label', 'Number Format');
        uimenu(obj.MenuChildren.NumberFormat, ...
            'Callback', @obj.changeNumberFormat, ...
            'Label', 'Hexadecimal', ...
            'Checked', 'on');
        uimenu(obj.MenuChildren.NumberFormat, ...
            'Callback', @obj.changeNumberFormat, ...
            'Label', 'Decimal');
        
        obj.MenuChildren.ShowUniqueMessages = uimenu(obj.MenuView, ...
            'Accelerator', 'U', ...
            'Callback', @obj.showUniqueMessages, ...
            'Checked', 'off', ...
            'Label', 'Show Unique Messages');

        % Add the menu bar for help.
        obj.MenuHelp = uimenu( ...
            obj.FigureWindow, ...
            'Label', 'Help');
        
        obj.MenuChildren.ToolHelp = uimenu(obj.MenuHelp, ...
            'Label', 'Documentation', ...
            'Callback', @obj.launchToolHelp);

        obj.MenuChildren.ToolAbout = uimenu(obj.MenuHelp, ...
            'Label', 'About Vehicle Network Toolbox', ...
            'Callback', @obj.launchToolAbout);

        % Add the toolbar.
        obj.Toolbar = uitoolbar(obj.FigureWindow);
        
        obj.ToolbarButtons.Start = uipushtool( ...
            obj.Toolbar, ...
            'CData', imread(fullfile(toolboxdir('vnt'), 'vntguis', 'Start.png')), ...
            'TooltipString', 'Start', ...
            'ClickedCallback', @obj.start);            
        
        obj.ToolbarButtons.Pause = uipushtool( ...
            obj.Toolbar, ...
            'CData', imread(fullfile(toolboxdir('vnt'), 'vntguis', 'Pause.png')), ...
            'TooltipString', 'Pause', ...
            'ClickedCallback', @obj.pause);            

        obj.ToolbarButtons.Stop = uipushtool( ...
            obj.Toolbar, ...
            'CData', imread(fullfile(toolboxdir('vnt'), 'vntguis', 'Stop.png')), ...
            'TooltipString', 'Stop', ...
            'ClickedCallback', @obj.stop);            

        obj.ToolbarButtons.Save = uipushtool( ...
            obj.Toolbar, ...
            'CData', imread(fullfile(toolboxdir('vnt'), 'vntguis', 'Save.png')), ...
            'TooltipString', 'Save Messages', ...
            'ClickedCallback', @obj.save, ...
            'Separator', 'on');            

        obj.ToolbarButtons.Clear = uipushtool( ...
            obj.Toolbar, ...
            'CData', imread(fullfile(toolboxdir('vnt'), 'vntguis', 'Clear.png')), ...
            'TooltipString', 'Clear Messages', ...
            'ClickedCallback', @obj.clear);            

        obj.ToolbarButtons.ShowUniqueMessages = uitoggletool( ...
            obj.Toolbar, ...
            'CData', imread(fullfile(toolboxdir('vnt'), 'vntguis', 'ShowUnique.png')), ...
            'TooltipString', 'Show Unique Messages', ...
            'ClickedCallback', @obj.showUniqueMessages, ...
            'Separator', 'on');            

        % Build a table on the tool.
        obj.MessageTable = internal.ui.widget.TablePanelController( ...
            obj.TABLE_BLANK_ROW, ...
            {'Timestamp', 'ID', 'Name', 'Length', 'Data'}, ...
            [false false false false false], ...
            'QETestMessageTable', ...
            'Hierarchical');
        
        [~, obj.MessageTableHandle] = ...
            javacomponent(obj.MessageTable.getJavaComponent(), ...
                [0, 0, obj.FIGURE_WIDTH, obj.FIGURE_HEIGHT], ...
            obj.FigureWindow);
        
        % Initialize the signal tables with blank information.
        clearSignalTableData(obj);            
        
        % Set initial tool states.
        obj.StateTool = obj.STATE_STOPPED;
        obj.StateNumberFormat = obj.FORMAT_HEXADECIMAL;
        obj.StateShowUniqueMessages = false;

        % Create message filter objects.
        obj.MsgFilterStd = can.MessageFilter('Standard');
        obj.MsgFilterExt = can.MessageFilter('Extended');
        
        % Initialize the internal message logs.
        obj.MessageLog = obj.EMPTY_MSG_STRUCT;
        obj.MessageLogPaused = obj.EMPTY_MSG_STRUCT;
        obj.MessageLogUnique = obj.EMPTY_MSG_STRUCT;
        obj.MessageLogUniquePaused = obj.EMPTY_MSG_STRUCT;   
        
        % Make figure visible.
        set(obj.FigureWindow, 'Visible', 'on');
        
        % Update the figure.
        updateFigure(obj);
    end

    
    function closeFigure(obj, ~, ~)
    % closeFigure Prepares the tool to close.
    %
    %   This function is called when the user clicks close on the tool
    %   window. It cleans up the object and removes the display window.
    
        % Clear the timer.
        if ~isempty(obj.UpdateTimer)
            % Stop the timer.
            if obj.UpdateTimer.Running
                stop(obj.UpdateTimer);
            end
            
            % Delete the timer.
            delete(obj.UpdateTimer);
        end
        obj.UpdateTimer = [];
        
        % Clear the channel.
        obj.Channel = [];
        
        % Clear the internal message logs.
        obj.MessageLog = [];
        obj.MessageLogUnique = [];
        obj.MessageLogPaused = [];
        obj.MessageLogUniquePaused = [];
        
        % Remove the figure.
        delete(obj.FigureWindow);
        obj.FigureWindow = [];
    end
    
    function exit(obj, ~, ~)
    % exit Closes the tool.
    %
    %   This function responds to user closing of the tool.
    
        % Close the figure to shut down the tool.
        close(obj.FigureWindow);
    end

    
    function resizeFigure(obj, ~, ~)
    % resizeFigure Manages figure on resize operation.
    %
    %   This function redraws the figure controls to match any window
    %   resize events.
    
        % Get the new position information.
        pos = get(obj.FigureWindow, 'Position');

        % Resize the message table to completely fill the figure.
        javacomponent(obj.MessageTable.getJavaComponent(), ...
            [0, 0, pos(3), pos(4)], ...
            obj.FigureWindow);
        
        % Draw now to force the resize.
        drawnow limitrate;
    end

    
    function initChannels(obj, handle, ~)
    % initChannels Create function for channel options.
    %
    %   This function is called on launch of the tool to build the
    %   list of devices and channels.
        
        % Get hardware information for all devices on the system.
        hwInfo = canHWInfo();

        % If no devices were found, error.
        if isempty(hwInfo.VendorInfo)
            return;
        end
        
        % Initialize the constructor information structure.
        obj.ChannelInfoAll = struct([]);

        % Build the channel options.
        for vi = 1:numel(hwInfo.VendorInfo)
            for ci = 1:numel(hwInfo.VendorInfo(vi).ChannelInfo)
                % Build a string representing this channel.
                switch hwInfo.VendorInfo(vi).VendorName
                    case {'Kvaser', 'Vector','MathWorks'}
                        selectionString = [hwInfo.VendorInfo(vi).VendorName ' ' ...
                            hwInfo.VendorInfo(vi).ChannelInfo(ci).Device ' ' ...
                            '(Channel ' num2str(hwInfo.VendorInfo(vi).ChannelInfo(ci).DeviceChannelIndex) ')'];
                    case {'NI', 'PEAK-System'}
                        selectionString = [hwInfo.VendorInfo(vi).VendorName ' ' ...
                            hwInfo.VendorInfo(vi).ChannelInfo(ci).DeviceType ' ' ...
                            '(' hwInfo.VendorInfo(vi).ChannelInfo(ci).Device ')'];
                end                         
                
                % Set the string as available to the user.
                uimenu(handle, ...
                    'Label', selectionString, ...
                    'Callback', @obj.changeChannel);

                % Build a corresponding structure containing the information for
                % each CAN channel as needed to construct it later. This is done
                % for efficiency rather than parsing the values later out of the
                % selection strings.
                index = numel(obj.ChannelInfoAll) + 1;
                obj.ChannelInfoAll(index).Vendor = ...
                    hwInfo.VendorInfo(vi).VendorName;
                obj.ChannelInfoAll(index).Device = ...
                    hwInfo.VendorInfo(vi).ChannelInfo(ci).Device;
                obj.ChannelInfoAll(index).DeviceChannelIndex = ...
                    hwInfo.VendorInfo(vi).ChannelInfo(ci).DeviceChannelIndex;
                
                if strcmpi(obj.ChannelInfoAll(index).Vendor, 'NI') || ...
                   strcmpi(obj.ChannelInfoAll(index).Vendor, 'PEAK-System')
                    obj.ChannelInfoAll(index).DeviceType = ...
                        hwInfo.VendorInfo(vi).ChannelInfo(ci).DeviceType;
                else
                    obj.ChannelInfoAll(index).DeviceType = '';
                end
            end
        end
        
        % Set the active channel to the first option in the list as the
        % default case by checking it.
        hInfo = get(handle);
        set(hInfo.Children(end), 'Checked', 'on');

        % Set the current information structure to reference the active
        % channel as well.
        obj.ChannelInfoCurrent.Vendor = obj.ChannelInfoAll(1).Vendor;
        obj.ChannelInfoCurrent.Device = obj.ChannelInfoAll(1).Device;
        obj.ChannelInfoCurrent.DeviceChannelIndex = ...
            obj.ChannelInfoAll(1).DeviceChannelIndex;
        obj.ChannelInfoCurrent.DeviceType = ...
            obj.ChannelInfoAll(1).DeviceType;
    end
    
    function changeChannel(obj, handle, ~)
    % changeChannel Callback function for changing channel selection.
    %
    %   This function is called when the user selects a new device channel.
    %   It will ensure only the selected channel is checked active,
    %   internally configure the tool to use the new channel, and take
    %   care of any visible management required on the tool.
        
        % If the user is reselecting the already set option, do nothing.
        if strcmpi(get(handle, 'Checked'), 'on')
            return;
        end

        % If the tool is started, prompt to stop in order to allow
        % configuration of this option.
        if promptForCancel(obj)
            return;
        end
    
        % Get the info on the menu option.
        hInfo = get(obj.MenuChildren.Channel);

        % Uncheck all of the suboptions.
        set(hInfo.Children, 'Checked', 'off');
        % Check the currently selected option.
        set(handle, 'Checked', 'on');
        
        % Set the new channel's constructor info using logical
        % indexing to find the current channel. Note that the order of the
        % channel info array is opposite of the numbering used by the menu
        % system, so the array must be flipped for proper referencing.
        index = flipud(hInfo.Children) == handle;
        obj.ChannelInfoCurrent.Vendor = obj.ChannelInfoAll(index).Vendor;
        obj.ChannelInfoCurrent.Device = obj.ChannelInfoAll(index).Device;
        obj.ChannelInfoCurrent.DeviceChannelIndex = ...
            obj.ChannelInfoAll(index).DeviceChannelIndex;
        obj.ChannelInfoCurrent.DeviceType = ...
            obj.ChannelInfoAll(index).DeviceType;
    
        % Clear all message information in the tool.
        clearMessages(obj);
        
        % Update the figure.
        updateFigure(obj);
    end
    
    function configBusSpeed(obj, ~, ~)
    % configBusSpeed Allows changing of the bus speed.
    %
    %   This function provides the interface that allows the user to change
    %   the bus speed of the currently selected channel. It will launch a
    %   bus speed specific configuration window and listen for any changes
    %   made there.
    
        % If the tool is started, prompt to stop in order to allow
        % configuration of this option.
        if promptForCancel(obj)
            return;
        end
    
        % Construct a temporary channel for validation.
        if strcmpi(obj.ChannelInfoCurrent.Vendor, 'NI') || ...
           strcmpi(obj.ChannelInfoCurrent.Vendor, 'PEAK-System')
            try
                channel = canChannel(obj.ChannelInfoCurrent.Vendor, obj.ChannelInfoCurrent.Device);
            catch err %#ok<NASGU>
                % Throw an error describing the case that bus speed is not
                % currently settable because we cannot open a channel to
                % validate a new value.
                text = getString(message('vnt:Tool:BusSpeedNotConfigurable'));
                e = errordlg(text, 'Error', 'modal');
                set(e, 'Tag', 'BusSpeedNotConfigurable');
                uiwait(e);
                return;
            end
        else
            channel = canChannel( ...
                obj.ChannelInfoCurrent.Vendor, ...
                obj.ChannelInfoCurrent.Device, ...
                obj.ChannelInfoCurrent.DeviceChannelIndex);
        end

        % Check for initialization access as without it, we do not allow
        % configuring the bus speed.
        if ~channel.InitializationAccess
            % Throw an error describing the init access situation.
            text = getString(message('vnt:Tool:BusSpeedNoInitAccess'));
            e = errordlg(text, 'Error', 'modal');
            set(e, 'Tag', 'BusSpeedNoInitAccess');
            uiwait(e);
            return;
        end
        
        % Build an input dialog box.
        text = getString(message('vnt:Tool:BusSpeedInputDialogText'));
        header = getString(message('vnt:Tool:BusSpeedInputDialogHeader'));
        numLines = 1;

        % Set the default configuration value for the dialog.
        if isempty(obj.BusSpeed)
            % If no desired bus speed value exists, populate the default
            % setting with the value currently defined on the channel.
            defaultAnswer = {num2str(channel.BusSpeed)};
        else
            % Otherwise, use the desired value.
            defaultAnswer = {num2str(obj.BusSpeed)};
        end
        
        % Query for user input.
        answerStr = inputdlg(text, header, numLines, defaultAnswer);
        
        % Check for an empty answer indicating that the user closed or
        % cancelled the dialog box. We have nothing more to do then.
        if isempty(answerStr)
            return;
        end
        
        % Convert the answer string to a number.
        answer = str2double(answerStr);
        
        % Validate that the user entered a valid number.
        if isnan(answer)
            throwInvalidBusSpeedError(obj, answerStr);
            return;
        end
        
        % Compare the new value against the currently configured value. If
        % they are equal, we do not need to validate as we already know the
        % value is good. Just set the desired option and exit.
        if answer == channel.BusSpeed
            obj.BusSpeed = answer;
            return;
        end        
        
        % Validate the new bus speed by setting it in the temporary channel.
        try
            % If the set succeeded, we have a validated bus speed.
            configBusSpeed(channel, answer);
            obj.BusSpeed = answer;
            
        catch err %#ok<NASGU>
            throwInvalidBusSpeedError(obj, answerStr);
            return;
        end
        
        function throwInvalidBusSpeedError(obj, answerStr) %#ok<INUSD>
        % throwInvalidBusSpeedError Throws a configuration error.
        %
        %   This local function is used to throw errors when the 
        %   configuration option is not set correctly.
            
            % Clear the validation channel.
            channel = [];
            
            % Error.
            text = getString(message('vnt:Tool:BusSpeedInvalidValue'));
            e = errordlg(text, 'Error', 'modal');
            set(e, 'Tag', 'BusSpeedInvalidValue');
            uiwait(e);
            
            % Relaunch the configuration dialog.
            configBusSpeed(obj);
        end
    end
    
    function configFiltering(obj, ~, ~)
    % configFiltering Allows changing of message filtering settings.
    %
    %   This function presents the user with a figure allowing for
    %   configuration of standard and extended CAN message identifier
    %   filters.
    
        % If the tool is started, prompt to stop in order to allow
        % configuration of this option.
        if promptForCancel(obj)
            return;
        end
        
        % Launch a filtering configuration window.
        filterWindow = can.ToolFilter( ...
            obj.MsgFilterStd, ...
            obj.MsgFilterExt, ...
            get(obj.FigureWindow, 'Position'));
        % Wait until the window is closed.
        uiwait(filterWindow.FigureWindow);
    end
        
    function configDatabase(obj, ~, ~)
    % configDatabase Allows changing of the option.
    %
    %   This function presents the user with a file selection dialog box
    %   allowing them to choose a CANdb database file to be used for
    %   message/signal decoding by the tool.
    
        % If the tool is started, prompt to stop in order to allow
        % configuration of this option.
        if promptForCancel(obj)
            return;
        end
    
        % Launch a file dialog for the user to select the database file.
        [fName, fPath] = uigetfile(obj.FILTERSPEC_DATABASE);
        
        % Exit if no database file was selected.
        if fName == 0
            return;
        end
        
        % Set the cursor mode.
        set(obj.FigureWindow, 'Pointer', 'watch');
        drawnow limitrate;
        cleanupObj = onCleanup(@()set(obj.FigureWindow, 'Pointer', 'arrow'));
        
        try
            % Open the database file and set it on the object.
            obj.Database = canDatabase([fPath fName]);
            
        catch err %#ok<NASGU>
            % If opening the file fails, inform the user.
            text = getString(message('vnt:Tool:DatabaseUnableToOpen'));
            e = errordlg(text, 'Error', 'modal');
            set(e, 'Tag', 'DatabaseUnableToOpen');
            uiwait(e);

            % Relaunch the configuration dialog.
            configDatabase(obj);
        end
    end
    
    function set.Database(obj, value)
    % set.Database Set the property.
    %
    %   This function is a custom setter for the Database property. It sets
    %   the received handle to a database objects and forces any changes
    %   required in the tool as a result of adding database decoding.
    %
    %   NOTE: Having this logic in a custom setter is not inherently
    %   necessary. In some respects, it is better left in the function that
    %   responds to the user inputting a database from the file dialog.
    %   This code was relocated here for testing purposes. It allows the
    %   tool tests to directly set a database object into the tool without
    %   the limitations of needing to interact with the file dialog window.
    
        % Set the property value.
        obj.Database = value;
        
        % Initialize the message information maps.
        obj.MsgInfoMapStd = containers.Map( ...
            'KeyType', 'double', 'ValueType', 'any'); %#ok<MCSUP>
        obj.MsgInfoMapExt = containers.Map( ...
            'KeyType', 'double', 'ValueType', 'any'); %#ok<MCSUP>
        
        % Reset the table data.
        updateTableMessageLog(obj);
    end
    
    function configMaximumMessageCount(obj, ~, ~)
    % configMaximumMessageCount Allows changing of the option.
    %
    %   This function provides the interface that allows the user to change
    %   an option of the tool. It will launch a configuration window and 
    %   listen for any changes made there.
    
        % Build an input dialog box.
        text = getString(message('vnt:Tool:MaximumMessageCountDialogText', ...
            obj.ALLOWED_MESSAGE_COUNT_MIN, ...
            obj.ALLOWED_MESSAGE_COUNT_MAX));
        header = getString(message('vnt:Tool:MaximumMessageCountDialogHeader'));
        numLines = 1;
        defaultAnswer = {num2str(obj.MaximumMessageCount)};
        
        % Query for user input.
        answerStr = inputdlg(text, header, numLines, defaultAnswer);
        
        % Check for an empty answer indicating that the user closed or
        % cancelled the dialog box. We have nothing more to do then.
        if isempty(answerStr)
            return;
        end
        
        % Convert the answer string to a number.
        answer = str2double(answerStr);
        
        try
            % Validate the input.
            validateattributes(answer, {'numeric'}, ...
                {'positive', '<=', obj.ALLOWED_MESSAGE_COUNT_MAX, ...
                '>=', obj.ALLOWED_MESSAGE_COUNT_MIN, 'integer', 'scalar'});
            
            % Set the new value.
            obj.MaximumMessageCount = answer;
            
            % Trim the message logs.
            if numel(obj.MessageLog) > obj.MaximumMessageCount
                obj.MessageLog = obj.MessageLog(1:obj.MaximumMessageCount);
            end
            
            if numel(obj.MessageLogPaused) > obj.MaximumMessageCount
                obj.MessageLogPaused = obj.MessageLogPaused(1:obj.MaximumMessageCount);
            end
            
            % Update the message table for the change in view only if the
            % tool is not started. When started, the change will happen
            % automatically on periodic update of the display.
            if obj.StateTool ~= obj.STATE_STARTED
                updateTableMessageLog(obj);
            end
            
        catch err %#ok<NASGU>
            % Error.
            text = getString(message('vnt:Tool:MaximumMessageCountInvalidValue', ...
                obj.ALLOWED_MESSAGE_COUNT_MIN, ...
                obj.ALLOWED_MESSAGE_COUNT_MAX));
            e = errordlg(text, 'Error', 'modal');
            set(e, 'Tag', 'MaximumMessageCountInvalidValue');
            uiwait(e);

            % Relaunch the configuration dialog.
            configMaximumMessageCount(obj);
            return;
        end        
    end
    
    function changeNumberFormat(obj, handle, ~)
    % changeNumberFormat Callback function for changing number format.
    %
    %   This function is called when the user changes the number format
    %   used in the display of the tool (for example from hex to dec). It
    %   will update the number format control, set the internal tool state,
    %   and manage any visible changes required on the tool.
        
        % If the user is reselecting the already set option, do nothing.
        if strcmpi(get(handle, 'Checked'), 'on')
            return;
        end
        
        % Set the cursor mode.
        set(obj.FigureWindow, 'Pointer', 'watch');
        drawnow limitrate;
        cleanupObj = onCleanup(@()set(obj.FigureWindow, 'Pointer', 'arrow'));
        
        % Get the info on the menu option.
        hInfo = get(obj.MenuChildren.NumberFormat);

        % Set the new number format value on the tool.
        switch get(handle, 'Label')
            case 'Hexadecimal'
                obj.StateNumberFormat = obj.FORMAT_HEXADECIMAL;
            case 'Decimal'
                obj.StateNumberFormat = obj.FORMAT_DECIMAL;
        end
        
        % Uncheck all of the suboptions.
        set(hInfo.Children, 'Checked', 'off');
        % Check the currently selected option.
        set(handle, 'Checked', 'on');
        
        % Update the message table for the change in view.
        updateTableMessageLog(obj);
    end
    
    function showUniqueMessages(obj, ~, ~)
    % showUniqueMessages Callback function for changing the show unique option.
    %
    %   This function is called when the show unique option is changed. It
    %   will set the internal state on the tool and manage any visible
    %   changes required for the table.
    
        % Set the cursor mode.
        set(obj.FigureWindow, 'Pointer', 'watch');
        drawnow limitrate;
        cleanupObj = onCleanup(@()set(obj.FigureWindow, 'Pointer', 'arrow'));
        
        % Adjust the show unique message state and controls.
        if obj.StateShowUniqueMessages
            obj.StateShowUniqueMessages = false;
            set(obj.MenuChildren.ShowUniqueMessages, 'Checked', 'off');
            set(obj.ToolbarButtons.ShowUniqueMessages, 'State', 'off');
        else
            obj.StateShowUniqueMessages = true;
            set(obj.MenuChildren.ShowUniqueMessages, 'Checked', 'on');
            set(obj.ToolbarButtons.ShowUniqueMessages, 'State', 'on');
        end            
    
        % Update the message table for the change in show unique messages.
        updateTableMessageLog(obj);
    end
    
    function launchToolHelp(obj, ~, ~) %#ok<INUSD>
    % launchToolHelp Shows tool help.
    %
    %   This function launches tool help.
    
        % Show the help for CAN Tool.
        helpview(fullfile(docroot, 'toolbox', 'vnt', 'vnt.map'), 'cantool');
    end
    
    function launchToolAbout(obj, ~, ~) %#ok<INUSD>
    % launchToolAbout Shows the About dialog.
    %
    %   This function launches a dialog box showing information about the
    %   toolbox.
    
        % Build a display string with toolbox version information.
        verInfo = ver('vnt');
        text = getString(message('vnt:Tool:AboutMessageBoxText', ...
            verInfo.Name, ...
            verInfo.Version, ...
            verInfo.Release));
        header = getString(message('vnt:Tool:AboutMessageBoxHeader'));
        
        % Display a message dialog with the about information.
        msgbox(text, header, 'modal');
    end
    
    function start(obj, ~, ~)
    % start Callback function for starting the tool.
    %
    %   This function is called when tool is started. It will build and
    %   activate the currently selected channel as well as manage the
    %   internal tool state and visible changes required on start. Note
    %   that start can also mean resume if start is engaged when the tool
    %   is paused.
    
        % Add a protection against double push of the start button. This
        % can happen due to lag and if allowed results in double timers
        % and problems operating the tool.
        if obj.StateTool == obj.STATE_STARTED
            return;
        end
    
        % Check if we are resuming (i.e. restarting from pause).
        if obj.StateTool == obj.STATE_PAUSED
            % Set the cursor mode.
            set(obj.FigureWindow, 'Pointer', 'watch');
            drawnow limitrate;
            cleanupObj = onCleanup(@()set(obj.FigureWindow, 'Pointer', 'arrow'));

            % Set the new tool state.
            obj.StateTool = obj.STATE_STARTED;
            updateFigure(obj);

            % Update the message table to restore to the an active state
            % from the pause state.
            updateTableMessageLog(obj);
            return;
        end
        
        % Clear any messages.
        clearMessages(obj);
        
        % Build a Channel connected to the currently selected device.
        switch obj.ChannelInfoCurrent.Vendor
            case {'Vector', 'Kvaser','MathWorks'}
                obj.Channel = canChannel( ...
                    obj.ChannelInfoCurrent.Vendor, ...
                    obj.ChannelInfoCurrent.Device, ...
                    obj.ChannelInfoCurrent.DeviceChannelIndex);
            case {'NI', 'PEAK-System'}
                try
                    obj.Channel = canChannel( ...
                        obj.ChannelInfoCurrent.Vendor, ...
                        obj.ChannelInfoCurrent.Device);
                catch err
                    % If the channel fails to create due to another active
                    % connection already existing, inform the user with a
                    % dialog box.
                    if strcmpi(err.identifier, 'vnt:Channel:ActiveConnectionExists')
                        text = getString(message('vnt:Tool:ActiveConnectionExists'));
                        e = errordlg(text, 'Error', 'modal');
                        set(e, 'Tag', 'ActiveConnectionExists');
                        uiwait(e);
                        return;
                    else
                        % Rethrow any other error.
                        rethrow(err);
                    end
                end
        end
        
        % If a database is configured for the tool, set it on the channel.
        if ~isempty(obj.Database)
            obj.Channel.Database = obj.Database;
        end
        
        % Check the desired bus speed against the actual value
        % currently set on the channel.
        if obj.BusSpeed ~= obj.Channel.BusSpeed
            try
                % Setting the bus speed is done in a try/catch to protect the
                % figure from an improper value. No action is taken on error.
                % The bus speed will revert back to its previous state.
                configBusSpeed(obj.Channel, obj.BusSpeed);
                
            catch err %#ok<NASGU>
                % Warn that we cannot change the bus speed.
                text = getString(message('vnt:Tool:BusSpeedUnableToSetOnStart', obj.Channel.BusSpeed));
                w = errordlg(text, 'Warning', 'modal');
                set(w, 'Tag', 'BusSpeedUnableToSetOnStart');
                uiwait(w);
            end
        end
        
        % Set up any custom filters for the channel.
        can.Tool.setMessageFilter(obj.Channel, obj.MsgFilterStd);
        can.Tool.setMessageFilter(obj.Channel, obj.MsgFilterExt);

        % Start the channel.
        start(obj.Channel);
        
        % Configure and start a timer for updating the message table. 
        obj.UpdateTimer = timer(...
            'Period', obj.UPDATE_RATE,...
            'ExecutionMode', 'fixedSpacing', ...
            'TimerFcn', @obj.updateDisplay);
        start(obj.UpdateTimer);

        % Set the new tool state.
        obj.StateTool = obj.STATE_STARTED;
        updateFigure(obj);
    end
    
    function pause(obj, ~, ~)
    % pause Callback function for pausing the tool.
    %
    %   This function is called the tool is paused. It will freeze the
    %   message display with the current values and manage the internal
    %   state of the tool.

        % Set the new tool state.
        obj.StateTool = obj.STATE_PAUSED;
        updateFigure(obj);

        % Copy the current message logs into paused logs. The reason for this
        % is because messages will continue to process behind the scenes even
        % when the display is paused. If a user then saves messages or changes
        % show unique messages, we want them to save or view messages in the 
        % snapshot of time when they hit the pause button. Unpausing will allow
        % a return to the "real time" view of message traffic.
        obj.MessageLogPaused = obj.MessageLog;
        obj.MessageLogUniquePaused = obj.MessageLogUnique;

        % Update the table to show signal tables.
        updateTableMessageLog(obj);
    end
    
    function stop(obj, ~, ~)
    % stop Callback function for stopping the tool.
    %
    %   This function is called when the tool is stopped. It will shut down
    %   update operations and release the channel. Note that we do not
    %   clear messages though. They must be kept so the user can
    %   view/analyze them.

        % Set the cursor mode.
        set(obj.FigureWindow, 'Pointer', 'watch');
        drawnow limitrate;
        cleanupObj = onCleanup(@()set(obj.FigureWindow, 'Pointer', 'arrow'));
        
        % Set the new tool state.
        obj.StateTool = obj.STATE_STOPPED;
        updateFigure(obj);

        % Clear the timer.
        stop(obj.UpdateTimer);
        delete(obj.UpdateTimer);
        obj.UpdateTimer = [];

        % Clear the channel.
        obj.Channel = [];

        % Update the table to show signal tables.
        updateTableMessageLog(obj);
    end
    
    function save(obj, ~, ~)
    % save Saves the messages stored in the tool to a file.
    %
    %   This function saves the currently logged messages to file.
    
        % Get the file information of the current folder.
        files = dir;
        % Get the names of the files in the current folder.
        fileNames = {files.name};
        % Find any message logs in the file list.
        fileMatches = strmatch('VNT CAN Log', fileNames);
        fileMatchCount = numel(fileMatches);

        % Set the log file enumeration into the file name.
        if fileMatchCount == 0
            % Set a blank enumeration if no log files currently exist.
            saveFileString = '';
        else
            % Get the logs files from the full list.
            logFiles = files(fileMatches);
            % Get the matched file names.
            logFileNames = {logFiles.name};

            % Loop through each log file and extract its enumeration value.
            for fi = 1:numel(logFileNames)
                % Use the parentheses as a token to get to the enum.
                [~, enumString] = strtok(char(logFileNames(fi)), '(');
                % Before extracting the enum value, check that the string was
                % parsable with the parentheses token.
                if isempty(enumString)
                    % If the string is empty, this is the first log file present.
                    enumValues(fi) = 1; %#ok<AGROW>
                else
                    % Otherwise, get the value from the string.
                    enumValues(fi) = str2double(enumString(:, 2:end - 5)); %#ok<AGROW>
                end
            end

            % Get the highest current enum value and add one to get the new
            % value for the new log file.
            newEnumValue = max(enumValues) + 1;
            % Set the new value into a string for the file name.
            saveFileString = [' (' num2str(newEnumValue) ')'];
        end
        
        % Complete building of the log file name.
        logFileName = ['VNT CAN Log' saveFileString];
        
        % Give the user a file save dialog with the determined name as
        % the default save file name.
        [fName, fPath] = uiputfile({[logFileName '.mat']}, 'Save');
        % If the user cancels the file selection dialog, then cancel the
        % save operation.
        if fName == 0
            return;
        end
        % Set the final save file name and path based on the user selection.
        logFileName = [fPath fName];

        % Set the cursor mode.
        set(obj.FigureWindow, 'Pointer', 'watch');
        drawnow limitrate;
        cleanupObj = onCleanup(@()set(obj.FigureWindow, 'Pointer', 'arrow'));
        
        % Check the pre-save state of the figure. 
        if obj.StateTool == obj.STATE_PAUSED
            % When paused, save the messages frozen on the display.
            saveMessageLog = obj.MessageLogPaused;
        else
            % Otherwise save the active log.
            saveMessageLog = obj.MessageLog;
        end
        
        % Reverse the order of the message log to save. We store messages
        % inside the tool with the most recent first. We normally keep them
        % with the most recent as the last index though. This is how they
        % should be saved.
        saveMessageLog = saveMessageLog(end:-1:1); 

        % Convert to the structures to objects to save.
        if isempty(obj.Database)
            saveMessageLog = can.Message(saveMessageLog); %#ok<NASGU>
        else
            saveMessageLog = can.Message(saveMessageLog, obj.Database); %#ok<NASGU>
        end
        
        % Save the message log to the current working directory.
        save(logFileName, 'saveMessageLog');
    end
    
    function clear(obj, ~, ~)
    % clear Clears all messages.
    %
    %   This function clears all message information from the tool.
    
        % Delegate the clear messages method.
        clearMessages(obj);
        
        % Update the figure controls. This is done primarily to control
        % the state of the save and clear controls. With message
        % information removed from the tool, both of these controls should
        % be disabled. The control function will take care of this as well
        % as other special state management.
        updateFigure(obj);
    end

    
    function updateDisplay(obj, ~, ~)
    % updateDisplay Timer callback function to update the display.
    %
    %   This function is the processing function for CAN Tool. It runs off
    %   the timer when the tool is active to receive messages and update
    %   the display.

        % Exit immediately if there is no channel to use. It can happen
        % on close/delete of the tool where the timer callback will run
        % as the tool is being deconstructed.
        if isempty(obj.Channel)
            return;
        end

        % Return immediately if no messages are available.
        if obj.Channel.MessagesAvailable == 0
            return;
        end
        
        % Get all available messages from the channel.
        msgRx = receiveRaw(obj.Channel);

        % Enable the clear controls when messages are received.
        set(obj.MenuChildren.Clear, 'Enable', 'On');
        set(obj.ToolbarButtons.Clear, 'Enable', 'On');
        
        % Flip the order of the new message array. We do this to put the
        % most recent messages in the tool on top of the list.
        msgRx = msgRx(end:-1:1);
        
        % Perform an extract recent. This is done whether we are in unique
        % display mode or not in order to maximize efficiency. The
        % extraction is performed on the results of the last extraction
        % plus the new messages received instead of the entire log to save
        % time. It is preferable to do smaller extractions each time
        % interval than to risk doing large extractions occasionally which
        % would stutter the figure.
        obj.MessageLogUnique = obj.extractRecent([obj.MessageLogUnique msgRx]);

        % Add the new messages onto the front of the message log.
        obj.MessageLog = [msgRx obj.MessageLog];

        % The log must be clipped if the total count of existing messages 
        % plus the newly received ones are over the log size threshold.
        obj.MessageLog = obj.MessageLog(1:min(end, obj.MaximumMessageCount));

        % Return without updating the display when paused.
        if obj.StateTool == obj.STATE_PAUSED
            return;
        end
        
        % Display on the table based on the setting of show unique messages.
        if obj.StateShowUniqueMessages
            % Update the table with a full rewrite.
            updateTableMessageLog(obj);
        else
            % PERFORMANCE: It would be cleanest and easiest here to just use
            % the updateTableMessageLog method for all cases; however, when
            % the tool is running and showing all messages, we can not
            % afford the time to reconvert every message every iteration.

            % If more new messages have arrived than will fit on the table,
            % we can discard the extra messages.
            msgRx = msgRx(1:min(end, obj.MaximumMessageCount)); 

            % Create the cell data for the new messages.
            newTableData = obj.convertMessageObjectsToTable(msgRx);
            
            % Concatenate the new messages with the messages already in
            % the table to make a single list with the newest messages on
            % top for proper order.
            obj.MessageTableCellData = cat(1, newTableData, obj.MessageTableCellData);
            
            % Cut off any messages in the concatenated table over the maximum
            % threshold. We remove from the bottom of the array to get rid
            % of the oldest messages first.
            obj.MessageTableCellData = obj.MessageTableCellData( ...
                1:min(size(obj.MessageTableCellData, 1), obj.MaximumMessageCount), :);
            
            % Set the data into the table.
            setTableData(obj.MessageTable, obj.MessageTableCellData, uint16(1));
        end

        % Force a redraw on the figure to update the table.
        drawnow limitrate;
    end

    function clearMessages(obj)
    % clearMessages Removes all message history from the tool.
    %
    %   This function is used to delete any currently stored messages
    %   history from the tool. This includes both the internally stored
    %   messages logs as well as clearing the visible message table.
    
        % Clear the internal message logs.
        obj.MessageLog = obj.EMPTY_MSG_STRUCT;
        obj.MessageLogUnique = obj.EMPTY_MSG_STRUCT;
        obj.MessageLogPaused = obj.EMPTY_MSG_STRUCT;
        obj.MessageLogUniquePaused = obj.EMPTY_MSG_STRUCT;

        % Clear any existing signal table data.
        clearSignalTableData(obj);

        % Clear the message table by setting blank information into it.
        obj.MessageTableCellData = [];
        setTableData(obj.MessageTable, obj.TABLE_BLANK_ROW, uint16(1));
    end
    
    function msgCell = convertMessageObjectsToTable(obj, msg)
    % convertMessageObjectsToTable Converts message object to table cell data.
    %
    %   This function extracts displayed message information from the
    %   message objects and converts it into cell array format for display
    %   on the table.

        % Check for empty and return a blank set of data if so.
        if isempty(msg)
            msgCell = obj.TABLE_BLANK_ROW;
            return;
        end
        
        % Call to the MEX helper function to process the message structure
        % to string table data conversion. This is done for performance.
        % The call differs whether a database is being used or not as with
        % a database we get the message names.
        if isempty(obj.Database)
            msgCell = mexCANToolTableConverter( ...
                msg, ...
                obj.StateNumberFormat, ...
                false);
        else
            msgCell = mexCANToolTableConverter( ...
                msg, ...
                obj.StateNumberFormat, ...
                true, ...
                obj.Database.Path, ...
                obj.Database.HandleName);
        end
    end
    
    function updateTableMessageLog(obj)
    % updateTableMessageLog Displays the correct current message log.
    %
    %   This function determines which message log is currently viewable on
    %   the tool table given the current state of the tool. It will then
    %   update the table accordingly.
    
        % The correct message log to display on the table is dependent on
        % whether the tool is paused or not and also whether the show
        % unique messages option is active or not.
        if obj.StateTool == obj.STATE_PAUSED
            if obj.StateShowUniqueMessages
                % Unique + paused.
                targetLog = obj.MessageLogUniquePaused;
                % Update the signal table data.
                setSignalTableData(obj, targetLog);
            else
                % Normal + Paused.
                targetLog = obj.MessageLogPaused;
                % Update the signal table data.
                setSignalTableData(obj, targetLog);
            end
        else
            if obj.StateShowUniqueMessages
                % Unique + Started.
                targetLog = obj.MessageLogUnique;
                % Update the signal table data.
                setSignalTableData(obj, targetLog);
            else
                % Normal + Started.
                targetLog = obj.MessageLog;
                
                % Update the signal table data if the tool is stopped.
                if obj.StateTool == obj.STATE_STOPPED
                    setSignalTableData(obj, targetLog);
                else
                    clearSignalTableData(obj);
                end
            end
        end
        
        % Turn the new target message log into table cell data.
        obj.MessageTableCellData = ...
            convertMessageObjectsToTable(obj, targetLog);
        % Set the data on the table. Note that the final argument indicates
        % the first table row for the data and must be casted for the
        % interface to work.
        setTableData(obj.MessageTable, obj.MessageTableCellData, uint16(1));
    end
    
    
    function setSignalTableData(obj, msg)
    % setSignalTableData Builds new signal tables.
    %
    %   This function builds tables of signal information that are child
    %   tables of each row containing the decoded signal data for the
    %   message contained in a given row.
        
        % Do nothing if no database is present.
        if isempty(obj.Database)
            return;
        end
        
        % Set the column names.
        columnNames = obj.SIGNAL_TABLE_COLUMN_NAMES;
        
        % Build a blank table for rows without signal data.
        blankData = cell(1, 2);
        blankData{1, 1} = ' ';
        blankData{1, 2} = ' ';
        
        % Build the cell data for each message.
        for ii = 1:numel(msg)
            % Look up the message.
            msgInfo = obj.getMessageInfo(msg(ii));
            % If there is no signal data to set, send a blank table.
            if isempty(msgInfo)
                % Add the blank signal data to the table.
                addChildData(obj.MessageTable, ii, columnNames, blankData);
                continue;
            end
            
            % Get the signal names for this message.
            signalNames = msgInfo.Signals;
            
            % Initialize the cell data to the appropriate signal number.
            cellData = cell(numel(signalNames), 2);
            
            % PERFORMANCE: Get the signals structure from the message
            % object into temporary storage for faster access.
            [~, ~, signalValues] = obj.Database.getSignalValueAll(msgInfo.Name, msg(ii).Data);
            
            % Loop through the signals and add to the cell data. We
            % maintain a separate index for loading versus looping in order
            % to get the signals to show up in proper alphabetical order.
            idx = 1;
            for jj = numel(signalNames):-1:1
                % Load the signal name.
                cellData{idx, 1} = signalNames{jj};
                % Load the value.
                cellData{idx, 2} = sprintf('%f', signalValues.(signalNames{jj}));
                % Increment the loading index.
                idx = idx + 1;
            end
            
            % Add the signal data to the table.
            addChildData(obj.MessageTable, ii, columnNames, cellData);
        end
    end
    
    function clearSignalTableData(obj)
    % clearSignalTableData Blanks out signal table data.
    %
    %   This function is used to write blanks to all of the signal table
    %   data in the tool.
    
        % Build the blank table.
        cellData = cell(1, 2);
        cellData{1, 1} = 'Unavailable';
        cellData{1, 2} = 'Unavailable';
        
        % Loop through each table entry and blank it.
        for ii = 1:obj.MaximumMessageCount
            % Add the blank signal data to the table.
            addChildData(obj.MessageTable, ii, obj.SIGNAL_TABLE_COLUMN_NAMES, cellData);
        end
    end

    
    function updateFigure(obj)
    % updateFigure Reconfigures the tool figure in response to state change.
    %
    %   This function is used to modify the current active/inactive status
    %   of the tool. This includes enabling or disabling menu and/or
    %   toolbar controls as well managing the text shown on the figure
    %   header.

        % Enable/disable some controls based on message count.
        if isempty(obj.MessageLog)
            % Disable save.
            set(obj.MenuChildren.Save,'Enable', 'Off');
            set(obj.ToolbarButtons.Save,'Enable', 'Off');
            % Disable clear.
            set(obj.MenuChildren.Clear,'Enable', 'Off');
            set(obj.ToolbarButtons.Clear,'Enable', 'Off');
        else
            % Enable save.
            set(obj.MenuChildren.Save,'Enable', 'On');
            set(obj.ToolbarButtons.Save,'Enable', 'On');
            % Enable clear.
            set(obj.MenuChildren.Clear,'Enable', 'On');
            set(obj.ToolbarButtons.Clear,'Enable', 'On');
        end
        
        % Enable/disable controls based on the tool state.
        switch obj.StateTool
            case obj.STATE_STARTED
                set(obj.MenuChildren.Start, 'Enable', 'Off');
                set(obj.MenuChildren.Pause, 'Enable', 'On');
                set(obj.MenuChildren.Stop,  'Enable', 'On');
                set(obj.ToolbarButtons.Start, 'Enable', 'Off');
                set(obj.ToolbarButtons.Pause, 'Enable', 'On');
                set(obj.ToolbarButtons.Stop,  'Enable', 'On');

                % Disable save.
                set(obj.MenuChildren.Save,'Enable', 'Off');
                set(obj.ToolbarButtons.Save,'Enable', 'Off');
                
            case obj.STATE_PAUSED
                set(obj.MenuChildren.Start, 'Enable', 'On');
                set(obj.MenuChildren.Pause, 'Enable', 'Off');
                set(obj.MenuChildren.Stop,  'Enable', 'On');
                set(obj.ToolbarButtons.Start, 'Enable', 'On');
                set(obj.ToolbarButtons.Pause, 'Enable', 'Off');
                set(obj.ToolbarButtons.Stop,  'Enable', 'On');

            case obj.STATE_STOPPED
                set(obj.MenuChildren.Start, 'Enable', 'On');
                set(obj.MenuChildren.Pause, 'Enable', 'Off');
                set(obj.MenuChildren.Stop,  'Enable', 'Off');
                set(obj.ToolbarButtons.Start, 'Enable', 'On');
                set(obj.ToolbarButtons.Pause, 'Enable', 'Off');
                set(obj.ToolbarButtons.Stop,  'Enable', 'Off');
        end

        % Build a new header string for the tool. It is expected that prior
        % to calling this method that the current channel info structure
        % has been populated with new information.
        switch obj.ChannelInfoCurrent.Vendor
            case {'Kvaser', 'Vector','MathWorks'}
                headerStr = sprintf('Vehicle CAN Bus Monitor  |  %s %s (Channel %d)', ...
                    obj.ChannelInfoCurrent.Vendor, ...
                    obj.ChannelInfoCurrent.Device, ...
                    obj.ChannelInfoCurrent.DeviceChannelIndex);
            case {'NI', 'PEAK-System'}
                headerStr = sprintf('Vehicle CAN Bus Monitor  |  %s %s (%s)', ...
                    obj.ChannelInfoCurrent.Vendor, ...
                    obj.ChannelInfoCurrent.DeviceType, ...
                    obj.ChannelInfoCurrent.Device);
        end
        
        % Add additional information based on tool state.
        switch obj.StateTool
            case obj.STATE_STARTED
                headerStr = [headerStr '  |  Started'];
                headerStr = sprintf([headerStr ' - %d kbps'], ...
                    obj.Channel.BusSpeed / 1000);

            case obj.STATE_PAUSED
                headerStr = [headerStr '  |  Paused'];
                headerStr = sprintf([headerStr ' - %d kbps'], ...
                    obj.Channel.BusSpeed / 1000);

            case obj.STATE_STOPPED
                headerStr = [headerStr '  |  Stopped'];
        end
        
        % Set the new information into the header.
        set(obj.FigureWindow, 'Name', headerStr);
        drawnow limitrate;
    end
    
    function out = promptForCancel(obj)
    % promptForCancel Queries the user to cancel an operation based on state.
    %
    %   This function prompts the user for input on whether to stop the 
    %   tool if that is required to perform some action.
        
        % Initialize the output to ensure operation continues as the
        % default case. This covers the tool to act appropriately when 
        % this method is called and the tool is already stopped.
        out = false;
        
        % If the tool not stopped, prompt if the user would like to stop.
        if obj.StateTool ~= obj.STATE_STOPPED
            % Prompt the user.
            text = getString(message('vnt:Tool:PromptForCancelDialogText'));
            buttonName = questdlg(text, 'Continue?', 'Yes', 'No', 'No');
        
            % Check the user input.
            if strcmpi(buttonName, 'Yes')
                % Stop the tool if requested.
                stop(obj);
                out = false;
            else
                % Return true as the user chose to cancel the action and
                % leave the tool running instead of forcing the tool to
                % stop.
                out = true;
            end
        end
    end
    
    function extracted = extractRecent(obj, msg)
    % extractRecent Returns the most recent occurrence of the specified message(s).
    %
    %   This method takes an array of message structures and returns an
    %   array of message structures that is a reduced set. The reduced set
    %   contains only one entry for each unique ID found in the input.
        
        % Get the ID and extended values.
        msgDetails = [msg.ID]';
        msgDetails(:,2) = [msg.Extended]';

        % Find all unique ID/Extended in the message array.
        msgU = unique(msgDetails, 'rows');

        % Initialize the output in case no messages are found.
        extracted = obj.EMPTY_MSG_STRUCT;

        % Loop through each ID/Extended requested for extraction.
        for idi = 1:size(msgU, 1)
            % Using logical indexing, obtain matches of all messages having
            % matching ID/Extended properties.
            msgByID = msg((msgU(idi,1) == msgDetails(:,1)) & (msgU(idi,2) == msgDetails(:,2)));

            % Note that an implementation was tested using findobj in place of
            % logical indexing; however, logical indexing proved to be much
            % faster when extracting with a large number of target ID/Extended
            % values.The findobj code is as follows:
            %   msgByID = findobj(obj, 'ID', uint32(ID(idi)), 'Extended', extended(idi));

            % Check for matches.
            if ~isempty(msgByID)
                % Get the timestamp values.
                timestamps = [msgByID.Timestamp];
                % Get the latest occurring message of the matches.
                latestMessage = msgByID(timestamps == max(timestamps));
                % Set the actual extracted message as the last one in
                % the array. This addresses an issue should multiple
                % messages have the same timestamp because they were
                % received faster than the resolution of the timestamp.
                % This is a possible scenario with virtual channels.
                extracted(end + 1) = latestMessage(end); %#ok<AGROW>
            end
        end
    end
   
    function info = getMessageInfo(obj, msg)
    % getMessageInfo Get database message information for a message.
    %
    %   This method returns a messsage information structure for the input
    %   message structure representing a message. It does a database search
    %   for the information if the information is not already stored
    %   locally in the tool.
    
        % Use try/catch for better performance than isKey for the map.
        try
            % Check the appropriate map depending on if the message has a
            % standard or extended ID.
            if msg.Extended
                info = obj.MsgInfoMapExt(msg.ID);
            else
                info = obj.MsgInfoMapStd(msg.ID);
            end
        catch err %#ok<NASGU>
            % The message information was not already in the map, so look
            % it up in the database.
            info = obj.Database.privateMessageInfoByID(msg.ID, msg.Extended);
            
            % Add the information to the appropriate map.
            if msg.Extended
                obj.MsgInfoMapExt(msg.ID) = info;
            else
                obj.MsgInfoMapStd(msg.ID) = info;
            end
        end
    end
    
end

methods (Static)
    
    function setMessageFilter(channel, filterSettings)
    % setMessageFilter Configures filtering settings on a channel.
    %
    %   This method configures filter settings according to the given input
    %   on the channel also provided as input. Depending on the state of
    %   the filter, the channel is configured accordingly.
    
        % Set the filter based on the configured state.
        switch filterSettings.State
            case can.FilterStates.AllowAll
                channel.filterAllowAll(filterSettings.Type);
            case can.FilterStates.BlockAll
                channel.filterBlockAll(filterSettings.Type);
            case can.FilterStates.AllowOnly
                channel.filterAllowOnly(filterSettings.AllowOnlyNumeric, filterSettings.Type);
        end
    end
    
end

end