classdef SerialReceive < matlab.System
    % SerialRxObj = matlabshared.seriallib.internal.system.SerialReceive
    % creates a Serial Receive system object.

    % Copyright 2020-2023 The MathWorks, Inc.

    % Public, non-tunable properties
    properties(Nontunable)
        % Communication port
        PortSel = '<Select a port...>';
        % Data type
        DataType = 'uint8';
        % SaveDataType
        SaveDataType = 'uint8';
        % Header
        Header double;
        Terminator = '<none>';
        % Custom terminator
        CustomTerminator double;
        DataSize = [1 1];
        % Action when data is not available
        ActionWhenDataUnavailable = 'Output last received value';
        % Custom value
        CustomValue = 0;
        % Block sample time
        SampleTime = 0.01;
        ComPort = '<Select a port...>';
        InputFormat = 'Column major';    

        ToggleHeader (1, 1) logical = false
        ToggleTerminator (1, 1) logical = false
        ToggleDataSize (1, 1) logical = true
        % Enable blocking mode
        EnableBlockingMode (1, 1) logical = true
    end

    properties (Hidden)
        ActionWhenDataUnavailableSet = matlab.system.StringSet( ...
            cellstr(["Output last received value", "Output custom value", "Error"]))
        TerminatorSet = matlab.system.StringSet(cellstr(["<none>", "CR ('\r')", ...
            "LF ('\n')", "CR/LF ('\r\n')", "NULL ('\0')", "Custom terminator"]))
        DataTypeSet = matlab.system.StringSet(cellstr(["single", "double", "int8", ...
            "uint8", "int16", "uint16", "int32", "uint32"]))
        InputFormatSet = matlab.system.StringSet({'Row major', 'Column major'});
    end

    %#codegen
    properties (Access = 'private')
        SerialObj;
        SaveOutput;
    end

    methods
        %% Constructor
        function obj = SerialReceive(varargin)
            % Support name-value pair arguments when constructing object
            setProperties(obj, nargin, varargin{:})
        end

        %% Set functions to validate and set the value.
        function set.DataSize(obj, value)
            if ~isempty(value)
                if ~strcmpi(obj.DataType, 'string')
                    validateattributes(value, {'numeric'}, ...
                        { '>', 0, 'real', 'nonnan', 'integer', 'row'}, ...
                        '', 'Data size')
                else
                    validateattributes(value, {'numeric'}, ...
                        { '>', 0, 'real', 'nonnan', 'integer', 'row', 'ncols', 1}, ...
                        '', 'Data size')
                end
            end
            obj.DataSize = value;
        end

        function set.CustomTerminator(obj, value)
            if ~isempty(value)
                validateattributes(value, {'numeric'}, ...
                    { '>=', 0, '<=', 255, 'real', 'nonnan', 'integer', 'row'}, ...
                    '', 'Terminator')
            end
            obj.CustomTerminator = value;
        end

        function set.Header(obj, value)
            if ~isempty(value)
                validateattributes(value, {'numeric'}, ...
                    { '>=', 0, '<=', 255, 'real', 'nonnan', 'integer', 'row'}, ...
                    '', 'Header')
            end
            obj.Header = value;
        end

        function set.CustomValue(obj, value)
            validateattributes(value, {'numeric'}, ...
                { 'real', 'nonnan', 'finite'}, ...
                '', 'Custom value')
            if ~isscalar(value) && ...
                    ( numel(value) ~= prod((obj.DataSize)) )
                coder.internal.error('instrument:instrumentblks:invalidCustomValue');
            end
            obj.CustomValue = value;
        end

        function set.SampleTime(obj, value)
            if ~isscalar(value) || isempty(value) || ~isnumeric(value) || ...
                    isnan(value) || isinf(value) || (value <= 0 && value ~= -1)
                coder.internal.error('instrument:instrumentblks:invalidReceiveSampleTime');
            end
            obj.SampleTime = value;
        end

    end

    methods(Access = protected)
        %% Algorithm implementation
        function [out, varargout] = stepImpl(obj)

            % Set default value of dataAvailable to true. The dataAvailable variable
            % will be set to false in onDataNotAvailable() when no new data is received.
            dataAvailable = true;

            % Call the function to get the data
            out = getSerialData();

            % If new data is available, save the data, to output in next
            % timestep when new data is not available.
            if strcmpi(obj.ActionWhenDataUnavailable, 'Output last received value')
                if dataAvailable
                    obj.SaveOutput = out;
                end
            end

            % Set the status port in Non-blocking mode based on if new data
            % is received or not.
            if ~obj.EnableBlockingMode
                if dataAvailable
                    varargout{1} = true;
                else
                    varargout{1} = false;
                end
            end

            function out =  getSerialData()
                % Set deafult value of Data frame property's status.
                hasHeader = true;
                hasTerminator = true;
                hasDataSize = true;

                % If data size is scalar, system block will output data
                % with size [Datasize x 1].
                if isscalar(obj.DataSize)
                    outDataSize = [obj.DataSize 1];
                else
                    outDataSize = obj.DataSize;
                end

                % Check if user provided Header.
                if obj.ToggleHeader == 0 || isempty(obj.Header)
                    hasHeader = false;
                end

                % Check if user provided Terminator.
                if obj.ToggleTerminator == 0 || strcmpi(obj.Terminator, '<none>') ||  ...
                       obj.ToggleTerminator == 1 && strcmpi(obj.Terminator, 'Custom terminator') && isempty(obj.CustomTerminator)
                    hasTerminator = false;
                elseif ~strcmpi(obj.Terminator, 'Custom terminator')
                    index = strfind(obj.Terminator, '(');
                    terminatorVal = uint8(sprintf(obj.Terminator(index+2 : end-2)));
                else
                    terminatorVal = obj.CustomTerminator;
                end

                % Check if user provided DataSize
                if obj.ToggleDataSize == 0 || isempty(obj.DataSize)
                    hasDataSize = false;
                end
                
                dataTypeSize = sizeOfDatatype();
                expectedDataSize = dataTypeSize*prod(outDataSize);

                % If user provided Header, look for Header in the buffer.
                % In Blocking mode wait till Header is found until Timeout.
                if hasHeader
                    % If user provides both Terminator and Header as well, get
                    % the data till the terminator. If Datasize matches, return
                    % the data else return as per the action mentioned.
                    if hasTerminator && hasDataSize
                        % check if the property is present in the buffer
                        % if yes, then read until the value, else return.
                        if ~peakInBufferAndRead(obj.Header)
                            out = onDataNotAvailable;
                            return
                        end
                        [status, indices] = peekStatus_terminator(obj.SerialObj, terminatorVal, obj.EnableBlockingMode, expectedDataSize+1);
                        % Check if terminator is found at the Data size position
                        % given by the user on the block. Terminator has to
                        % be present at the next location after Data (Data size + 1).
                        if ~status || ~any(ismember(expectedDataSize+1, indices))
                            out = onDataNotAvailable; % output custom value
                            return
                        end

                        % Terminator found, now read the data.
                        bytesToRead = numel(terminatorVal) + expectedDataSize;
                        dataPlus = obj.SerialObj.read(bytesToRead);

                        % Output the data after reshaping with correct data type
                        sizedataPlus = size(dataPlus, 2);
                        sizeterminatorVal = size(terminatorVal, 2);
                        terminatorPositionOffset = sizedataPlus- sizeterminatorVal;
                        outval = dataPlus(1: terminatorPositionOffset);
                        data = typecast(outval, obj.DataType);
                        if strcmpi(obj.InputFormat,'Column major')
                            out = reshape(data, outDataSize);
                        else
                            out = (reshape(data, fliplr(outDataSize)))';
                        end

                    elseif hasDataSize
                        % check if the property is present in the buffer
                        % if yes, then read until the value, else return.
                        if ~peakInBufferAndRead(obj.Header)
                            out = onDataNotAvailable;
                            return
                        end
                        % If user provides Datasize as well, read until Datasize
                        % and return. In blocking mode wait till the size of
                        % Datasize is reached.
                        data = readData(obj.SerialObj, outDataSize, obj.EnableBlockingMode);
                        if isempty(data)
                            out = onDataNotAvailable; % output custom value
                            return
                        end
                        data = reshape(data, 1, numel(data));
                        outputVal = typecast(data, obj.DataType);
                        if strcmpi(obj.InputFormat,'Column major')
                            out = reshape(outputVal, outDataSize);
                        else
                            out = (reshape(outputVal, fliplr(outDataSize)))';
                        end
                    end

                elseif hasTerminator
                    % If user provided Terminator, check if Terminator is available
                    % in the buffer. In Blocking mode wait till Terminator is found
                    % until Timeout.
                    [status, ~] = peekStatus(obj.SerialObj, terminatorVal, obj.EnableBlockingMode);
                    if ~status
                        out = onDataNotAvailable; % output custom value
                        return
                    end
                    dataPlus = obj.SerialObj.readUntil(uint8(terminatorVal));
                    % If user provided Datasize as well, return Datasize data before the terminator
                    if hasDataSize
                        sizedataPlus = size(dataPlus, 2);
                        sizeterminatorVal = size(terminatorVal, 2);
                        terminatorPositionOffset = sizedataPlus- sizeterminatorVal;
                        if expectedDataSize > terminatorPositionOffset
                            out = onDataNotAvailable;
                            return
                        end
                        data = dataPlus(sizedataPlus - expectedDataSize - sizeterminatorVal + 1 : terminatorPositionOffset);
                        outputVal = typecast(data, obj.DataType);
                        if strcmpi(obj.InputFormat,'Column major')
                            out = reshape(outputVal, outDataSize);
                        else
                            out = (reshape(outputVal, fliplr(outDataSize)))';
                        end
                    end

                elseif hasDataSize
                    % If user provides only Datasize, check if Datasize data is
                    % available in buffer.If not available return as per the action
                    % mentioned. In Blocking mode wait until Datasize data
                    % is available.
                    data = readData(obj.SerialObj, outDataSize, obj.EnableBlockingMode);
                    if isempty(data)
                        out = onDataNotAvailable;
                        return
                    end
                    out = getOutputOnType(data);
                end
                
                % Function to calculate output based on dataType
                function out = getOutputOnType(data)
                    if isscalar(obj.DataSize)
                        outSizeVal = [obj.DataSize 1];
                    else
                        outSizeVal = obj.DataSize;
                    end
                    if ~strcmpi(obj.DataType, 'string')
                        data = reshape(data, numel(data), 1);
                        outpVal = typecast(data, obj.DataType);
                        if strcmpi(obj.InputFormat,'Column major')
                            out = reshape(outpVal, outSizeVal);
                        else
                            out = (reshape(outpVal, fliplr(outSizeVal)))';
                        end
                        return
                    end
                    % To output string Datatype. This feature is not
                    % supported in 21a. Adding this code to implement in
                    % upcoming releases.
                    out = convertCharsToStrings(char(data));
                end
                
                % Peek for the property in buffer and if found, then read
                % it.
                function found = peakInBufferAndRead(property)
                    found = true;
                    [status,~] = peekStatus(obj.SerialObj, property, obj.EnableBlockingMode);
                    if ~status
                        found = false;
                        return
                    end
                    % Read until the header from the buffer
                    obj.SerialObj.readUntil(uint8(property));
                end

                % PeekStatus function to peek until the property(Header or Terminator)
                % is found.
                function [status, varargout] = peekStatus(serialObject, propertyVal, enableBlockingMode)
                    varargout{1} = 0;
                    if ~enableBlockingMode
                        [status, varargout{1}] = serialObject.peekUntil(uint8(propertyVal));
                        return
                    end
                    status = false;
                    % In blocking mode, wait until the property is found till
                    % Timeout.
                    tic;
                    while toc < serialObject.Timeout
                        [status, varargout{1}] = serialObject.peekUntil(uint8(propertyVal)) ;
                        if status ~= false
                            status = true;
                            break
                        end
                        pause(0.001);
                    end
                end

                % PeekStatus function to peek until the property(Terminator)
                % is found.
                function [status, varargout] = peekStatus_terminator(serialObject, propertyVal, enableBlockingMode, expectedTerminatorIndex)
                    varargout{1} = 0;
                    if ~enableBlockingMode
                        [status, varargout{1}] = serialObject.peekUntil(uint8(propertyVal));
                        return
                    end
                    status = false;
                    foundIndex = 0;
                    % In blocking mode, we wait for data in buffer. Then start
                    % peeking for terminator within a range in buffer and return the status and indices.
                    tic;
                    while toc < serialObject.Timeout
                        varargout{1} = serialObject.peekBytesFromEnd(foundIndex, uint8(propertyVal));
                        if indexFound(varargout{1}, expectedTerminatorIndex)
                            status = true;
                            break
                        end
                        pause(0.001);
                    end
                end

                function flag = indexFound(idx, expectedTerminatorIndex)
                    % Check if the terminator is present at the expected location
                    % idx: Holds the array of indices where terminators are located within the data buffer.
                    flag = ~isempty(idx) && ismember(expectedTerminatorIndex, idx);
                end

                % readData function is to read the dataSizeVal data from the
                % buffer. In blocking mode wait until dataSizeVal data is found
                % until Timeout.
                function data = readData(serialObject, dataSizeVal, enableBlockingMode)
                    data = uint8([]);
                    typeSize = sizeOfDatatype();

                    if ~enableBlockingMode
                        if serialObject.NumBytesAvailable >= typeSize*prod(dataSizeVal)
                            data = serialObject.read(typeSize*prod(dataSizeVal),'uint8');
                        end
                        return
                    end
                    tic;
                    while toc < serialObject.Timeout
                        if serialObject.NumBytesAvailable >= typeSize*prod(dataSizeVal)
                            data = serialObject.read(typeSize*prod(dataSizeVal),'uint8');
                            return
                        end
                        pause(0.001);
                    end

                end

                %% Function to return the size of selected Data type
                function typeSize = sizeOfDatatype()
                    switch obj.DataType
                        case {'int8', 'uint8'}
                            typeSize = 1;
                        case {'int16', 'uint16'}
                            typeSize = 2;
                        case {'int32', 'uint32', 'single'}
                            typeSize = 4;
                        case 'double'
                            typeSize = 8;
                        case 'string'
                            typeSize = 1;
                        otherwise
                            typeSize = 1;
                    end
                end

                %% Function that gets called when no new data/ expected data is available.
                function out = onDataNotAvailable()
                    % Set the dataAvailable flag to false if there is no
                    % new data.
                    dataAvailable = false;

                    if isscalar(obj.DataSize)
                        outSizeVal = [obj.DataSize 1];
                    else
                        outSizeVal = obj.DataSize;
                    end

                    % Takes corresponding action based on Timeout received.
                    if strcmpi(obj.ActionWhenDataUnavailable, 'Output last received value')
                        % Set data to last received value.
                        out = obj.SaveOutput;
                    elseif strcmpi(obj.ActionWhenDataUnavailable, 'Output custom value')
                        % Set the data to custom value .
                        if isscalar(obj.CustomValue)
                            val = double(obj.CustomValue(ones(outSizeVal)));
                            outVal = cast(val, obj.DataType);
                        else
                            outVal = cast(obj.CustomValue, obj.DataType);
                        end
                        if(strcmpi(obj.InputFormat,'Column major'))
                            out = reshape(outVal, outSizeVal);
                        else
                            out = (reshape(outVal, fliplr(outSizeVal)))';
                        end
                    elseif strcmpi(obj.ActionWhenDataUnavailable, 'Error')
                        % Throw an error.
                        coder.internal.error('instrument:instrumentblks:timeouterror');
                    end
                end
            end
        end

        function setupImpl(obj)

            % Warn if custom value cannot be represented in output datatype.
            if any((cast(obj.CustomValue, obj.DataType) ~=  obj.CustomValue),'all')
                coder.internal.warning('instrument:instrumentblks:invalidCustomValueType');
            end

            if isscalar(obj.DataSize)
                outDataSize = [obj.DataSize 1];
            else
                outDataSize = obj.DataSize;
            end

            % Construct default value of SaveOutput property with DataSize
            % and DataType.
            obj.SaveOutput = zeros(outDataSize, obj.DataType);

            if strcmpi(obj.PortSel, '<Select a port...>')
                coder.internal.error('instrument:instrumentblks:noPortsSelected');
            end

            % Check if the parameter value is a variable. If yes, resolve
            % the variable and use its value. If parameter value is an
            % existing serial port, do not resolve.
            portValue = obj.PortSel;
            % Use coder.extrinsic to call the function in MATLAB rather than generating
            %code for it. This is done for functions that doesnt support codegen.
            coder.extrinsic('serialportlist');
            coder.extrinsic('slResolve');
            coder.extrinsic('gcb');
            coder.extrinsic('ismember');
            % Use coder.const to evaluate function call at compile time instead of run time.
            blockPath = coder.const(gcb);
            slList = coder.const(serialportlist);
            portInList = coder.const(~ismember(obj.PortSel, slList));
            if portInList
                [portValue, portStatus]= coder.const(@slResolve,obj.PortSel, blockPath);
                if ~portStatus
                    portValue = obj.PortSel;
                end
            end

            obj.SerialObj = matlabshared.seriallib.internal.Serial(portValue, 'IsSharingPort', true, ...
                'IsSharingExistingTimeout', true, 'IsWriteOnly', false);
            obj.SerialObj.connect;

            % Check if the Serial object has initiaization access. If yes, the port
            % was not configured by the Serial object created in Serial configuration system object.
            if obj.SerialObj.InitAccess
                coder.internal.error('instrument:instrumentblks:portNotConfigured');
            end
        end

        function [out,varargout]  = getOutputSizeImpl(obj)
            if isscalar(obj.DataSize)
                out = [obj.DataSize 1];
            else
                out = obj.DataSize;
            end

            if ~obj.EnableBlockingMode
                varargout{1} = [1 1];
            end
        end

        function releaseImpl(obj)
            % Release resources, such as file handles
            obj.SerialObj.disconnect;
        end

        function validatePropertiesImpl(obj)
            if isempty(obj.DataSize)
                coder.internal.error('instrument:instrumentblks:emptyDatasize');
            end

            if isempty(obj.CustomValue)
                coder.internal.error('instrument:instrumentblks:emptyCustomValue');
            end

            if any((cast(obj.CustomValue, obj.DataType) ~=  obj.CustomValue),'all')
                coder.internal.warning('instrument:instrumentblks:invalidCustomValueType');
            end
        end

        function num = getNumInputsImpl(~)
            % Define total number of inputs for system with optional inputs
            num = 0;
        end

        function num = getNumOutputsImpl(obj)
            % Define total number of outputs for system
            num = 1;
            if ~obj.EnableBlockingMode
                num = 2;
            end
        end

        function [name,varargout] = getOutputNamesImpl(obj)
            % Return output port names for System block
            name = 'Data';
            if ~obj.EnableBlockingMode
                varargout{1} = 'Status';
            end
        end

        function [out,varargout] = getOutputDataTypeImpl(obj)
            % Return data type for each output port
            out = obj.DataType;
            if ~obj.EnableBlockingMode
                varargout{1} = 'logical';
            end
        end
        %
        function [out,varargout] = isOutputComplexImpl(obj)
            % Return true for each output port with complex data
            out = false;
            if ~obj.EnableBlockingMode
                varargout{1} = false;
            end
        end

        function [out,varargout] = isOutputFixedSizeImpl(obj)
            % Return true for each output port with fixed size
            out = true;
            if ~obj.EnableBlockingMode
                varargout{1} = true;
            end
        end

        function sts = getSampleTimeImpl(obj)
            if isequal(obj.SampleTime, -1)
                sts = obj.createSampleTime("Type", "Inherited");
            else
                sts = obj.createSampleTime("Type", "Discrete", ...
                    "SampleTime", obj.SampleTime);
            end
        end

    end
end