classdef helperBLEChannelClassification < ble.internal.ConfigBase
%helperBLEChannelClassification Create an object to classify Bluetooth Low
%Energy channels
%   CFGCLASSIFY = helperBLEChannelClassification creates an object,
%   CFGCLASSIFY with default values, for classifying the BLE (Bluetooth Low
%   Energy) channels. This object can be used to classify the BLE channels
%   into 'good' (used) or 'bad' (unused) channels based on the statistics
%   of each channel.
%
%   CFGCLASSIFY = helperBLEChannelClassification(Name, Value) creates a BLE
%   channel classification object, CFGCLASSIFY, with the specified property
%   Name set to the specified Value. You can specify additional name-value
%   pair arguments in any order as (Name1, Value1, ..., NameN, ValueN).
%
%   helperBLEChannelClassification properties:
% 
%   PERThreshold                   - Packet error rate (PER) threshold
%   ChannelBusyCountThreshold      - Channel busy count threshold
%   ClassificationInterval         - Periodicity of channel classification
%   RxStatusCount                  - Received packets status count
%   EDIndicationsCount             - Energy detection (ED) indications
%                                    status count
%   MinRxCountToClassify           - Minimum number of received packets
%                                    status to classify the channels
%   PreferredMinimumGoodChannels   - Preferred number of good channels
%   BadChannelClassificationMethod - Method for bad channel classification

%   Copyright 2018-2019 The MathWorks, Inc.

properties
    %PERThreshold Packet error rate (PER) threshold
    %   Specify the packet error rate (PER) threshold as integer in the
    %   range of [1, 100] percentage. It is the threshold value of the PER
    %   which is used to classify the channels. The default value is 60.
    PERThreshold = 60
    
    %ChannelBusyCountThreshold Channel busy count threshold
    %   Specify the channel busy count threshold as integer in the range of
    %   [1, 100] percentage. It is the threshold value of channel busy
    %   count which is used to classify the channels. It is used only when
    %   <a
    %   href="matlab:help('helperBLEChannelClassification.BadChannelClassificationMethod')">BadChannelClassificationMethod</a>
    %   set to 'Using energy detections'. The default value is 60.
    ChannelBusyCountThreshold = 60
    
    %ClassificationInterval Periodicity of channel classification
    %   Specify the classification interval as an integer in the range
    %   [100, 5000]. It defines the number of connection events after which
    %   channels will be classified and the updated channel map sent to all
    %   the connected Slaves from the Master. The default classification
    %   interval is taken as 4 times the number of data channels (37). This
    %   is based on a assumption that for every classification majority of
    %   the channels should transmit at least 4 packets. The default value
    %   is 148 (37*4).
    ClassificationInterval = 148
    
    %RxStatusCount Received packets status count
    %   Specify the Rx status count as an integer in the range [10, 200].
    %   It defines the number of previous received packets status to be
    %   used for channel classification. The default value is 10.
    RxStatusCount = 10
    
    %EDIndicationsCount Energy detection (ED) indications status count
    %   Specify the ED indications count as an integer in the range [10,
    %   200]. These indications are observed for bad channels when device
    %   is in idle state. The default value is 10.
    EDIndicationsCount = 10
    
    %MinRxCountToClassify Minimum number of received packets status to
    %classify the channels
    %   Specify the minimum Rx count as an integer in the range [4, 200].
    %   It defines the minimum number of received packets status those are
    %   used to classify the channels. The default is 10.
    MinRxCountToClassify = 10
    
    %BadChannelClassificationMethod Method for bad channel classification
    %   Specify the bad channel classification method as one of 'Using
    %   energy detections' | 'Reset all the bad channels'. The option
    %   'Using energy detections' converts the bad channels to good
    %   channels based on energy detections observed on that channel, when
    %   the device is idle. The option 'Reset all the bad channels' resets
    %   all channels as good channels. The default value is 'Reset all the
    %   bad channels'.
    BadChannelClassificationMethod = 'Reset all the bad channels'
    
    %PreferredMinimumGoodChannels Preferred number of good channels
    %   Specify the preferred minimum good channels as an integer in the
    %   range [2, 37]. This property indicates the preferred number of
    %   minimum good channels that need to be maintained for the BLE device
    %   operation. The default value is 2.
    PreferredMinimumGoodChannels = 2
end

properties(Hidden, Constant)
    BadChannelClassificationMethod_Values = {'Using energy detections', ...
        'Reset all the bad channels'}
end

methods
    % Constructor
    function obj = helperBLEChannelClassification(varargin)
        obj@ble.internal.ConfigBase(varargin{:});
    end
    
    function obj = set.PERThreshold(obj, value)
        validateattributes(value, {'numeric'}, {'scalar', 'integer', ...
            '>=', 1, '<=', 100}, mfilename, 'PERThreshold');
        obj.PERThreshold = value;
    end
    
    function obj = set.ChannelBusyCountThreshold(obj, value)
        validateattributes(value, {'numeric'}, {'scalar', 'integer', ...
            '>=', 1, '<=', 100}, mfilename, 'ChannelBusyCountThreshold');
        obj.ChannelBusyCountThreshold = value;
    end
    
    function obj = set.ClassificationInterval(obj, value)
        validateattributes(value, {'numeric'}, {'nrows', 1, 'integer', ...
            '>=', 100, '<=', 5000}, mfilename, 'ClassificationInterval');
        obj.ClassificationInterval = value;
    end
    
    function obj = set.RxStatusCount(obj, value)
        validateattributes(value, {'numeric'}, {'scalar', 'integer', ...
            '>=', 10, '<=', 200}, mfilename, 'RxStatusCount');
        obj.RxStatusCount = value;
    end
    
    function obj = set.EDIndicationsCount(obj, value)
        validateattributes(value, {'numeric'}, {'scalar', 'integer', ...
            '>=', 10, '<=', 200}, mfilename, 'EDIndicationsCount');
        obj.EDIndicationsCount = value;
    end
    
    function obj = set.MinRxCountToClassify(obj, value)
        validateattributes(value, {'numeric'}, {'scalar', 'integer', ...
            '>=', 4, '<=', 200}, mfilename, 'MinRxCountToClassify');
        obj.MinRxCountToClassify = value;
    end
    
    function obj = set.PreferredMinimumGoodChannels(obj, value)
        validateattributes(value, {'numeric'}, {'scalar', 'integer', ...
            '>=', 2, '<=', 37}, mfilename, 'PreferredMinimumGoodChannels');
        obj.PreferredMinimumGoodChannels = value;
    end
    
    function obj = set.BadChannelClassificationMethod(obj, value)
        obj.BadChannelClassificationMethod = validatestring(value, ...
            obj.BadChannelClassificationMethod_Values, mfilename, ...
            'BadChannelClassificationMethod');
    end
end

methods
    function classifyChannels(obj, connectionConfig)
        %classifyChannels Classifies the channels based on connection
        %statistics such as packet failures
        
        for idx = 1:connectionConfig.DataChannelsCount
            % Total number of received packets in the channel 'idx'
            totalRxPackets = nnz(connectionConfig.LastNRxStatus(idx, :) ~= -1);
            
            % Ignore if the channel is already a bad channel or minimum
            % number of packets were not yet received in that channel
            if (connectionConfig.ChannelMap(idx) == false) || ...
                    (totalRxPackets < obj.MinRxCountToClassify)
                continue;
            end
            
            % Calculate PER
            per = ...
                (nnz(~connectionConfig.LastNRxStatus(idx, :))/totalRxPackets)*100;
            
            % Compare the calculated PER with threshold value to classify
            % the channel
            if (per > obj.PERThreshold)
                connectionConfig.ChannelMap(idx) = false;
            end
        end
        
        % If the number of good channels is less than
        % 'PreferredMinimumGoodChannels', classify bad channels. This might
        % avoid the permanent blocking of a bad channel.
        if (nnz(connectionConfig.ChannelMap) < obj.PreferredMinimumGoodChannels)
            getGoodChannels(obj, connectionConfig);
        end
        
        % Get the number of good channels after classification
        goodChannelsCount = nnz(connectionConfig.ChannelMap);
        
        % Channel classification algorithm should return minimum two good
        % channels. Reference: Volume 6, Part B, Section 4.5.8.1 of
        % Bluetooth core specification v5.0, Dec. 06, 2016
        if (goodChannelsCount < 2)
            goodChannels = getMinimumGoodChannels(obj, ...
                2-goodChannelsCount, connectionConfig);
            % Maintain minimum 2 good channels in the channel map
            connectionConfig.ChannelMap(goodChannels) = 1;
            
            % Reset the counters and indexes of Tx and Rx of the good
            % channels
            connectionConfig.RxIdx(goodChannels) = 1;
            connectionConfig.TxIdx(goodChannels) = 1;
        end
    end
end

methods(Access = private)
    function goodChannels = getMinimumGoodChannels(~, n, connectionConfig)
        %getMinimumGoodChannels Returns the best n channels from all the
        %bad channels
        
        % Get all the bad channels of the peer
        badChannels = find(~connectionConfig.ChannelMap);
        
        % Calculate the failures in all bad channels separately
        failureCount = ...
            sum(~connectionConfig.LastNRxStatus(badChannels, :), 2);
        
        % Add a column of channel numbers corresponding to the failures
        failureCount = [failureCount badChannels'];
        
        % Sort the bad channels based on failure count
        failureCount = sortrows(failureCount);
        
        % Return 'n' bad channels those having minimum number of failures
        % among all bad channels
        goodChannels = failureCount(1:n, 2);
    end
    
    function getGoodChannels(obj, connectionConfig)
        %getGoodChannels classifies the bad channels based on energy
        %indications observed when the device is in idle state
        
        if strcmp(obj.BadChannelClassificationMethod, 'Using energy detections')
            for idx = 1:connectionConfig.DataChannelsCount
                % Ignore if the channel is already a good channel
                if(connectionConfig.ChannelMap(idx) == true)
                    continue;
                end
                
                % Total energy indications observed till now
                channelBusyStatusCount = ...
                    nnz(connectionConfig.ChannelBusyStatus(idx, :) ~= -1);
                
                % Calculate channel busy time
                idleTime = (nnz(~connectionConfig.ChannelBusyStatus(idx, :))/ ...
                    channelBusyStatusCount)*100;
                
                if idleTime >= obj.ChannelBusyCountThreshold
                    
                    % Classify the channel as good if the busy time is less
                    % than the specified threshold
                    connectionConfig.ChannelMap(idx) = true;
                    
                    % Reset LastNRxStatus of the channel 'idx'
                    connectionConfig.LastNRxStatus(idx, :) = -1;
                    
                    % Because bad channels are converted to good channel,
                    % add 50% failures initially to classify them using
                    % fewer trails than original good channel
                    failuresCount = ceil(obj.MinRxCountToClassify/2);
                    
                    connectionConfig.LastNRxStatus(idx, 1:failuresCount) = 0;
                    
                    % Reset the indexes of Rx of the channel
                    connectionConfig.RxIdx(idx) = failuresCount+1;
                end
            end
        else % Make all bad to good
            for idx = 1:connectionConfig.DataChannelsCount
                % Ignore if the channel is already a good channel
                if(connectionConfig.ChannelMap(idx) == true)
                    continue;
                end
                
                % Convert the bad channel a good channel
                connectionConfig.ChannelMap(idx) = true;
                
                % Reset all the statistics of the channel except recent
                % (10%) statistics
                
                % Total transmissions and receptions in the channel
                totalRxPackets = ...
                    nnz(connectionConfig.LastNRxStatus(idx, :) ~= -1);
                rxCount = ceil(totalRxPackets/10);
                
                % Reset Rx statistics
                rxIndexes = (connectionConfig.RxIdx(idx)-rxCount:connectionConfig.RxIdx(idx)-1);
                
                for j = 1:numel(rxIndexes)
                    if rxIndexes(j) <= 0
                        rxIndexes(j) = rxIndexes(j) + obj.RxStatusCount;
                    end
                end
                
                temp = connectionConfig.LastNRxStatus(idx, rxIndexes);
                connectionConfig.LastNRxStatus(idx, :) = -1;
                connectionConfig.LastNRxStatus(idx, rxIndexes) = temp;
            end
        end
    end
end
end
