
%

%   Copyright 2012 The MathWorks, Inc.

classdef BufferDetection < coder.rtiostreamtest.Test
    %BUFFERDETECTION Detects the available buffer size on the target
    %   The CPU of the target is overwhelmed by issuing the sleep command
    %   for more time than is required to send a certain
    %   amount of data. If the buffer has overflowed during that time, the 
    %   device will report an error and the first position of the error 
    %   in the data is assumed to be the size of the available buffer.
    %
    %   Note that after this test the device might be left in an
    %   unresponsive state and a reset might be needed.
    %   
    %   Parameters:
    %
    %       sizeB -
    %           The maximum amount of data that will be used for probing
    %           the buffer size. If device's buffer size is bigger than
    %           that, this test will fail to detect the buffer.
    %       targetSize -
    %           How much data is the target supposed to receive by invoking
    %           the rtiostream read in one go.
    %       pcSize -
    %           How much data is the PC supposed to send in one go.
    %       fractTimeToSleep -
    %           How much of the time required for the data to be sent to
    %           the device should the device actually spend "sleeping".
    %       incrementalStart -
    %           Whether to start the probing from a small buffer size and
    %           doubling the data that is send every test iteration. This
    %           is helpful for devices that stop responding after their
    %           buffer overflows.
    %       verbose -
    %           Whether to enable verbose output
    
    % Available parameters defined here
    properties (Constant, GetAccess = 'private')
        param_maxDataByte       = 'sizeB';
        param_deviceReadStep    = 'targetSize';
        param_fracTimeToSleep   = 'fracTimeToSleep';
        param_incrementalStart  = 'incrementalStart';
        param_verbose           = 'verbose';
        param_pcSendStep        = 'pcSize';
        param_skip              = 'skip';
        
        % Default values for the parameters
        default_maxDataByte      = 1 * 1024;	% 1 KB
        default_deviceReadStep   = 1;           % 1 chunk at a time
        default_fracTimeToSleep  = 2;           % sleep factor (ensure we sleep for longer than data transmission)
        default_incrementalStart = true;        % Incrementally probe
        default_verbose          = false;       % No verbose output
        default_pcSendStep       = 64;          % up to 64 chunks at a time
    end
    
    properties (GetAccess = 'protected', SetAccess = 'protected')
        parameters;
    end
    
    methods (Static)
        
        function [maxDataByte, deviceReadStep, pcSendStep, fracTimeToSleep, incrementalStart, verbose] = getParameters(parameters)
            % GETPARAMETERS parse parameters into local variables
            
            import coder.rtiostreamtest.*;
            
            maxDataByte = Test.getSetting(parameters, BufferDetection.param_maxDataByte, BufferDetection.default_maxDataByte);
            deviceReadStep = Test.getSetting(parameters, BufferDetection.param_deviceReadStep, BufferDetection.default_deviceReadStep);
            fracTimeToSleep = Test.getSetting(parameters, BufferDetection.param_fracTimeToSleep, BufferDetection.default_fracTimeToSleep);
            incrementalStart = Test.getSetting(parameters, BufferDetection.param_incrementalStart, BufferDetection.default_incrementalStart);
            verbose = Test.getSetting(parameters, BufferDetection.param_verbose, BufferDetection.default_verbose);
            pcSendStep = Test.getSetting(parameters, BufferDetection.param_pcSendStep, BufferDetection.default_pcSendStep);
        end
        
    end
    
    methods
        
        function this = BufferDetection(parameters)
            % PARAMETERS would be used as initial parameters
            
            this = this@coder.rtiostreamtest.Test(coder.rtiostreamtest.Test.message('TitleBufferDetection'));
            this.skip = coder.rtiostreamtest.Test.getSetting(parameters, coder.rtiostreamtest.BufferDetection.param_skip, this.skip);
            this.parameters = [];
            if nargin == 1, this.parameters = parameters; end;
        end
        
        function runTest(this , connection, parameters)
            
            % Create a list of the initial parameters and the supplied ones
            parameters = [this.parameters parameters];
            
            % Parse the parameters into local variables
            [maxDataByte, deviceReadStep, pcSendStep, fracTimeToSleep, ...
                incrementalStart, verbose] = coder.rtiostreamtest.BufferDetection.getParameters(parameters);
            
            % Probe the outgoing bandwidth for the given pair of pc/target
            % chunk sizes
            input = coder.rtiostreamtest.BandwidthOutgoingTest();
            throughputB = input.runTest(connection, {...
                'pcSize', pcSendStep, ...
                'targetSize', deviceReadStep, ...
                'size', 32 * max(pcSendStep, deviceReadStep), ...
                }) * connection.memUnitSize;
            
            % Invoke the buffer detector with the parameters
            this.bufferDetectionLegacy(connection, maxDataByte, deviceReadStep, pcSendStep, ...
                fracTimeToSleep, incrementalStart, throughputB, verbose);
            
        end
        
        function bufferDetectionLegacy(this, connection, maxDataByte, deviceReadStep, pcSendStep, ...
                fracTimeToSleep, incrementalStart, throughputB, verbose)
            
            % Print verbose information
            coder.rtiostreamtest.Test.lnverboseln(verbose, 'VerboseBufferInfo', ...
                coder.rtiostreamtest.Test.displayDataSize(maxDataByte), ...
                deviceReadStep, fracTimeToSleep*100);
            
            % make sure we restore recv timeout
            origRecvTimeout = connection.getRecvTimeout;
             recvTimeoutCleanup = onCleanup(@()setRecvTimeout(connection, origRecvTimeout));
            
            if incrementalStart
                coder.rtiostreamtest.Test.verboseln(verbose, 'VerboseBufferIncremental');
            else
                coder.rtiostreamtest.Test.verboseln(verbose, 'VerboseBufferNonIncremental');
            end
            
            % Generate data
            dataWordSize = maxDataByte / connection.memUnitSize;
            data = coder.rtiostreamtest.ServerConnection.generateSlowData(dataWordSize);
            
            % Initialize the data to be sent during the first iteration
            if incrementalStart
                dataInBToTransmit = connection.memUnitSize;
            else
                dataInBToTransmit = maxDataByte;
            end                       
            
            % Initialize progress bar
            this.resetProgress();
            this.progress(0);
            
            % Calculate number of runs for correct percentage indicator by emulating
            % main the loop
            i = dataInBToTransmit;
            numberOfRuns = 0;
            while true
                numberOfRuns = numberOfRuns + 1;
                if i ~= maxDataByte
                    i = i * 2;
                    if i > maxDataByte
                        i = maxDataByte;
                    end
                else
                    break;
                end
            end
            runs = 0;
            prevDataSize = 0;
            
            % Do the probing
            while true                                
                % Data to be transmitted in chunks
                dataWordSize = uint32(round(dataInBToTransmit / connection.memUnitSize));               
                
                % Time to transmit
                time = double(dataInBToTransmit) / double(throughputB);                
                % add 1s to increase reliability when dataInBToTransmit
                % is small
                time = time + 1;                
                          
                % make sure we sleep for much longer than the estimated
                % data transmission time
                targetSleepTime = fracTimeToSleep*time;
                
                % Send macro
                connection.sendCommandAndAck(connection.op_macro_pause, ...
                    connection.secondsToTics(targetSleepTime));              
                connection.sendCommandAndAck(connection.op_macro_sendWakeup);
                connection.sendCommandAndAck(connection.op_macro_repeat_start);
                connection.sendCommandAndAck(connection.op_macro_blockRcvSlow, deviceReadStep);
                connection.sendCommandAndAck(connection.op_macro_repeat, idivide(dataWordSize, deviceReadStep));
                
                % If data does not exactly divide
                remainder = mod(dataWordSize, deviceReadStep);
                if remainder ~= 0
                    connection.sendCommandAndAck( ...
                        connection.op_macro_blockRcvFast, remainder);
                end
                
                % Initialize buffer size holder
                detectedSize = 0;
                
                % Start
                connection.sendCommand(connection.op_macro_run);
                % in case of flow control, the send timeout needs to be 
                % long enough such that it will not timeout before the
                % target wakes up and clears its receive buffer which will
                % make space for the send to complete
                connection.setSendTimeout(targetSleepTime * 5);
                
                earlyTargetWakeup = false;
                
                sendTimeStart = tic;                
                % Send data
                for k = 1 : pcSendStep : dataWordSize                    
                    % check for early wake-up message which implies either:
                    %
                    % a) sleep not calibrated correctly and target woke up
                    % too early
                    %
                    % b) rtIOStreamSend implementation inside blockingSend,
                    % or blockingSend itself, blocked for too long on 
                    % previous loop iteration and now the target is awake: 
                    % this may occur if the underlying rtiostream
                    % has a flow control mechanism                   
                    %
                    if ~earlyTargetWakeup
                        wakeupMessage = connection.recv(1);
                        if ~isempty(wakeupMessage)                      
                            % flag early wake-up but continue with the
                            % buffer detection protocol so we can always
                            % complete
                            earlyTargetWakeup = true;                            
                        end                    
                    end
                    
                    % Make sure we don't try to send more than we need to
                    last = k + pcSendStep - 1;
                    if last > dataWordSize
                        last = dataWordSize;
                    end
                    
                    % Send data
                    connection.blockingSend(data(k : last));                    
                    
                    % Update progress bar
                    this.progress(double(runs)/double(numberOfRuns) ...
                        + (double(k)/double(dataWordSize))/double(numberOfRuns));
                end                
                sendTime = toc(sendTimeStart);                
                remainingTargetSleepTime = targetSleepTime - sendTime;
                % wait for target sleep to complete, then rely on
                % blockingRecv timeout to give extra leeway
                if remainingTargetSleepTime > 0
                    pause(remainingTargetSleepTime);
                end
                            
                if ~earlyTargetWakeup
                    % wait for the wake up message which is sent after sleep
                    % terminates
                    connection.setRecvTimeout(10);
                    connection.blockingRecv(1);                                   
                end
                
                % Try to receive an ACK from device
                %
                % If the target buffer overflows and data is
                % lost, the target will be blocking
                % trying to receive the outstanding data and we
                % will time out waiting for an ack.    
                %
                % In this case, we deliberately
                % inject an error in to the stream to force the
                % target to terminate.
                %
                % Worst case is that we need to send two padding
                % bytes (255) to be sure of injecting a failure.   Hence,
                % we must loop 3 times.
                %
                % Reduce timeout so we don't hang for a long time
                connection.setRecvTimeout(5);                
                numTries = 3;
                ackReceivedOnRetry = false;
                ackReceived = false;
                for tryNum=1:numTries
                    try                        
                        [ACKcode, arg0] = connection.waitForAck(true);
                        ackReceived = true;
                        if tryNum > 1
                            ackReceivedOnRetry = true;
                        end
                        % ack received, stop looping
                        break;
                    catch err
                        if strcmp(err.identifier, 'Connectivity:target:ErrorRecvTimeout')                            
                            if tryNum ~= numTries
                                % send padding byte to wake up target
                                connection.blockingSend(uint8(255));
                            end
                        else
                            rethrow(err);
                        end
                    end
                end                                                   
                % process ack & derive errorAt
                endProbing = false;
                errorAt = [];
                if ~ackReceived
                    % no response from target (timeout)
                    endProbing = true; 
                else
                    switch ACKcode
                        case coder.rtiostreamtest.ServerConnection.ACK_SUCCESS
                            if ackReceivedOnRetry
                                % If the rogue byte made the target
                                % return success, then the buffer is
                                % exactly the size we are testing
                                endProbing = true;
                                errorAt = dataWordSize;
                            end
                        case coder.rtiostreamtest.ServerConnection.ACK_DATA_INTEGRITY_ERROR
                            endProbing = true;
                            errorAt = arg0;
                        case coder.rtiostreamtest.ServerConnection.ACK_RTIOSTREAM_ERROR
                            % Some targets (e.g. C2000) report an error
                            % when reading a buffer that has overflowed.
                            % This can occur even when attempting to read
                            % the first byte of the buffer.
                            % 
                            % In this case, we end testing and report the
                            % detected range to the user; assume that
                            % unrelated rtiostream errors would have
                            % already been captured by earlier tests.
                            endProbing = true;
                            % set errorAt as zero to trigger the correct
                            % reporting option below.
                            errorAt = 0;
                        otherwise
                            % throw unexpected error
                            connection.throwACKError(ACKcode, arg0)
                    end
                end
                if earlyTargetWakeup
                    % don't continue if we didn't sleep for long enough or
                    % we hit a flow control stall
                    endProbing = true; 
                end
                                                                                                      
                if endProbing                                                    
                    this.progress('done');
                    fprintf('\n');
                    
                    if ~isempty(errorAt) && errorAt ~= 0
                        % If the device knows exactly where the error
                        % occurred, report back to user                        
                        detectedSize = errorAt*connection.memUnitSize;
                        coder.rtiostreamtest.Test.print('BufferTargetOverflow', ...
                            coder.rtiostreamtest.Test.displayDataSize(detectedSize));                        
                    else
                        % If the device does not know where the error
                        % occurred or if a timeout occurred, try to guess the
                        % boundaries of the buffer                        
                        detectedSize = dataInBToTransmit;
                        
                        if incrementalStart && prevDataSize ~= 0
                            % If we have a successful test before the one
                            % that failed, report that the buffer size is
                            % between the last successful value and the
                            % current one which fails                            
                            coder.rtiostreamtest.Test.print('BufferDeviceErroredAccurate', ...
                                coder.rtiostreamtest.Test.displayDataSize(prevDataSize), ...
                                coder.rtiostreamtest.Test.displayDataSize(detectedSize));
                        else
                            % If the very first test fails, we can't do
                            % much about determining the size of the
                            % buffer. We know that it is definitely less
                            % than the current value.                            
                            coder.rtiostreamtest.Test.print('BufferDeviceErroredInaccurate', ...
                                coder.rtiostreamtest.Test.displayDataSize(detectedSize));
                        end                        
                    end                                                            
                    % endProbing
                    break;                    
                end
                
                % Value that would be used for estimating buffer in case
                % that a concrete value is not reported
                prevDataSize = dataInBToTransmit;
                
                % Increment the data size or stop the test
                if dataInBToTransmit ~= maxDataByte
                    dataInBToTransmit = dataInBToTransmit * 2;
                    if dataInBToTransmit > maxDataByte
                        dataInBToTransmit = maxDataByte;
                    end
                else
                    break;
                end
                
                % Count the number of runs for correct progress bar display
                runs = runs + 1;
            end
            
            % If the whole test has finished but no buffer size was
            % detected, report that.
            if detectedSize == 0
                this.progress('done');
                fprintf('\n');
                coder.rtiostreamtest.Test.print('BufferNotDetected', ...
                    coder.rtiostreamtest.Test.displayDataSize(prevDataSize));
            end
            if earlyTargetWakeup 
                % tag on a message about early wake-up / flow control.
                fprintf(' ');
                coder.rtiostreamtest.Test.print('EarlyTargetWakeup');                
            end
        end
        
    end
end

