%% Mapping MDF Channels to Simulink Model Input Ports
% This example shows you how to programmatically map MDF channels and consume their data via input ports of a Simulink model. It performs the gathering of input port names of a Simulink model and correlates them to the contents of a given MDF file. A linkage between them is then created which consumes channel data sourced from the MDF file when the model runs.
%
% Table of Contents
% Acquire Model Details
% Obtain Model Input Port Names
% Investigate the MDF File
% Construct a Table to Manage Items of Interest
% Perform Input Port to Channel Matching
% Populate the Simulink Dataset Object with MDF Channel Data
% Enable the Dataset as Input to the Simulink Model
% Run the Model
% Helper Functions
%
%% Acquire Model Details
% Define the example model name and open it.

mdlName = 'ModelForMDFInput';
open_system(mdlName);

%
% Use the createInputDataset function to obtain overall information about the model and its inputs.

dsObj = createInputDataset(mdlName)

%
%% Obtain Model Input Port Names
% This model has both a bus and an individual input port. The helperGetMdlInputNames function demonstrates how to get the name of all the model inputs regardless of how they are defined in the model.

mdlInputNames = helperGetMdlInputNames(mdlName)

%
%% Investigate the MDF File
% Now that you have the input port names of the model, you can see what channels exist in the MDF file so you can attempt to match them. The channelList function of the MDF feature allows quick access to the available channels present in an MDF file.

mdfName = 'CANape.MF4';
mdfObj = mdf(mdfName);
mdfChannelInfo = channelList(mdfObj)

%
%% Construct a Table to Manage Items of Interest
% Use a table to map the model input ports to MDF channels.

channelTable = table();
channelTable.PortNames = mdlInputNames;
n = size(channelTable.PortNames,1);
channelTable.ChGrpNum = NaN(n,1);
channelTable.ChNameActual = strings(n,1);
channelTable

%
%% Perform Input Port to Channel Matching
% The helperReportChannelInfo function searches the MDF file for channel names that match the model input port names. When found, the details of the channel are recorded in the table. Specifically, the channel group number where the given channel is in the file and its actual defined name. Note that the actual channel names are not exact matches to the model port names. In this example, the channel name matching is performed case-insensitive and ignores the underscore characters. This algorithm can be adapted as needed based on application-specific matching criteria.

channelTable = helperReportChannelInfo(channelTable, mdfChannelInfo)

%% Populate the Simulink Dataset Object with MDF Channel Data
% The dataset object created earlier contains both a single timeseries object and a structure of timeseries objects. This makes assigning data back to them somewhat challenging. Things to keep in mind include:
% When specifying 'TimeSeries' as the return type from the MDF read function, you must call read separately for each channel.
% Because the dataset object has dissimilar elements (a scalar timeseries and a scalar structure of timeseries objects), you need to manually manage the collection and make sure you are writing to the correct location.

for ii = 1:dsObj.numElements
    switch ii
        case {1} % [1x1 timeseries], triangle
            % Read the input port data from the MDF file one channel at a time.
            mdfData = read(mdfObj, channelTable.ChGrpNum(ii), channelTable.ChNameActual(ii), 'OutputFormat', 'TimeSeries');
            % Populate the dataset object.
            dsObj{ii} = mdfData;
            
        case {2} % [1x1 struct], busInput
            for jj = 1:numel(fieldnames(dsObj.getElement(ii)))
                % Read the input port data from the MDF file one channel at a time.
                mdfData = read(mdfObj, channelTable.ChGrpNum(jj+1), channelTable.ChNameActual(jj+1), 'OutputFormat', 'TimeSeries');
                % Populate the dataset object.
                dsObj{ii}.(channelTable.PortNames{jj+1}) = mdfData;                
            end
    end
end
dsObj

%
%% Enable the Dataset as Input to the Simulink Model

set_param(mdlName, 'LoadExternalInput', 'on');
set_param(mdlName, 'ExternalInput', 'dsObj');

%
%% Run the Model
% Upon executing the model, note that the MDF channel data properly maps to the designated input ports and plots through Simulink as expected.

open_system(mdlName);
bp = find_system(mdlName, 'BlockType', 'Scope');
open_system(bp);
pause(1)
set_param(mdlName, 'SimulationCommand', 'start');

%
%
%% Helper Functions

function mdlInputNames = helperGetMdlInputNames(mdlName)
% helperGetMdlInputNames Find input port names of a Simulink model.
%
% This function takes in the name of a Simulink model and returns the names of each model input. This specific model has 
% both a bus and a stand-alone input port going into it. To drive an input port that expects a bus means you need to supply 
% the signals as timeseries objects in a struct that matches the structure of the bus object attached to the input port.

% Test to see if the model is currently loaded in memory.
isLoaded = bdIsLoaded(matlab.lang.makeValidName(mdlName));

% If the model is not open then load it.
if ~isLoaded
    load_system(mdlName);
end

dsObj = createInputDataset(mdlName);
numElements = dsObj.numElements;
isStruct = zeros(1:numElements);

% Check to see if any of the elements in the returned dataset object are
% structs. If they are, assume they are for an input port that accepts a bus.
for elementIdx = 1:numElements
    isStruct(elementIdx) = isa(dsObj.getElement(elementIdx),'struct');
end

% For a port that accepts a bus, the data to be loaded must be arranged in a struct
% that matches the structure of the bus object attached to the input port.
busInportIdx = 1;
for idx = 1:numElements
    if isStruct(idx)
        % Get names of signals from a bus input port.
        inPortsBus(busInportIdx, :) = string(fieldnames(dsObj.getElement(idx)));
    else
        % Get signal name from a non-bus input port.
        inPorts(idx) = string(dsObj.getElement(idx).Name);
    end
end

mdlInputNames = [inPorts, inPortsBus]';
end


function channelTableOut = helperReportChannelInfo(channelTableIn, mdfChannelInfo)
% channelTableOut Reports if a channel is present in a set of channel names.

% Assign the output data.
channelTableOut = channelTableIn;

% Remove underscores and make everything lowercase for matching.
inPortChannelNames = lower(erase(channelTableIn.PortNames,'_'));
mdfChannelNames = lower(erase(mdfChannelInfo.ChannelName,'_'));

% Match the input channel names to the channel names in the MDF file.
[~, inPortidx] = ismember(inPortChannelNames, mdfChannelNames);

% Assign the relevant information back to the channel table.
channelTableOut.ChGrpNum = mdfChannelInfo{(inPortidx), {'ChannelGroupNumber'}};
channelTableOut.ChNameActual = mdfChannelInfo{(inPortidx), {'ChannelName'}};
end

% Copyright 2018 The MathWorks, Inc.