classdef bleChannelSelection < matlab.System
%bleChannelSelection Bluetooth LE channel index selection
%   CSA = bleChannelSelection creates a default Bluetooth(R) low energy
%   (LE) channel selection System object. This object enables you to select
%   a channel index for connection events, periodic advertising events, and
%   isochronous events.
%
%   CSA = bleChannelSelection(Name, Value) creates a System object for
%   Bluetooth LE channel selection, CSA, 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).
%
%   Step method syntax:
%
%   CHANNELINDEX = step(CSA) selects a channel index based on the
%   Algorithm, UsedChannels, and these initialization parameters:
%
%   1) HopIncrement for connection event channel selection using 'Algorithm
%   #1'
%   2) AccessAddress for connection, periodic advertising, or isochronous
%   event channel selection using 'Algorithm #2'
%   3) IsochronousEventCounter, SubeventChannelSelection, and
%   SubeventNumber for isochronous event channel selection using 'Algorithm
%   #2'. During each call of this step function, update the values of
%   IsochronousEventCounter and SubeventNumber. During each call of this
%   step function, the SubeventNumber must be updated sequentially.
%
%   CHANNELINDEX is the selected channel index, returned as a double value.
%
%   System objects may be called directly like a function instead of using
%   the step method. For example, y = step(csa) and y = csa() are
%   equivalent.
%
%   bleChannelSelection methods:
%
%   step     - Select a channel index using the specified algorithm
%   reset    - Reset channel selection to the beginning of connection
%              or periodic advertising or isochronous events
%   clone    - Create the System object with same property values
%   isLocked - Locked status (logical)
%
%   bleChannelSelection properties:
%
%   Algorithm                - Type of channel selection algorithm
%   HopIncrement             - Hop increment count
%   AccessAddress            - Unique connection address
%   UsedChannels             - List of used (good) data channels
%   SubeventChannelSelection - Flag to enable or disable isochronous event
%                              channel selection
%   IsochronousEventCounter  - Isochronous event counter
%   SubeventNumber           - Subevent counter
%
%   Read-only properties:
%
%   ChannelIndex - Selected channel index for current event
%   EventCounter - Counter of the current connection event or periodic
%                  advertising event for which the channel index is
%                  selected
%
%   % Example 1:
%   % Select a channel index with 'Algorithm1 #1'.
%
%   csa = bleChannelSelection;
%   csa.Algorithm     = 1;
%   csa.HopIncrement  = 7;
%   csa.UsedChannels  = [0, 2, 24, 6, 10, 14, 26, 30, 34, 36];
%   channelIndex = csa()
%
%   % Example 2:
%   % Select a channel index for connection event with 'Algorithm #2'.
%
%   csa = bleChannelSelection;
%   csa.Algorithm     = 2;
%   csa.AccessAddress = '8E89BED6';
%   csa.UsedChannels  = [9, 10, 21, 22, 23, 33, 34, 35, 36];
%   channelIndex = csa()
%
%   % Example 3:
%   % Select channel indices for isochronous events with 'Algorithm #2'.
%
%   csa = bleChannelSelection;
%   csa.Algorithm     = 2;
%   csa.AccessAddress = '8E89BED6';
%   csa.UsedChannels  = [9, 10, 21, 22, 23, 33, 34, 35, 36];
%   csa.SubeventChannelSelection = true;
%   % Select a channel index for first subevent in the first isochronous
%   % event
%   csa.IsochronousEventCounter = 0;
%   csa.SubeventNumber = 1;
%   channelIndex = csa()
%   % Select a channel index for second subevent in the first isochronous
%   % event
%   csa.SubeventNumber = 2;
%   channelIndex = csa()
%
%   See also bluetoothFrequencyHop.

%   Copyright 2018-2021 The MathWorks, Inc.

%#codegen

properties
    %Algorithm Type of channel selection algorithm
    %   Specify the algorithm as 1 or 2 representing 'Algorithm #1' or
    %   'Algorithm #2', respectively. 'Algorithm #1' supports channel
    %   selection for connection events. 'Algorithm #2' supports channel
    %   selection for connection events, periodic advertising events, and
    %   LE isochronous events. The System object uses this property to
    %   select the channel index. The default value is 1.
    Algorithm = 1

    %HopIncrement Hop increment count
    %   Specify the hop increment as an integer in the range [5, 16]. This
    %   property specifies the hop increment count to hop between data
    %   channels. The System object uses this property as an input to
    %   'Algorithm #1'. The default value is 5.
    HopIncrement = 5

    %AccessAddress Unique connection address
    %   Specify the access address as an 8-element character vector or a
    %   string scalar denoting a 4-octet hexadecimal value. This property
    %   specifies a unique 32-bit address for the Link Layer connection
    %   between the two devices. The System object uses this property as an
    %   input to 'Algorithm #2'. The default value is '8E89BED6'.
    AccessAddress = '8E89BED6'
    
    %SubeventChannelSelection Flag to enable or disable subevent channel
    %selection
    %   Specify this property as a scalar logical. A true value specifies,
    %   that the channel selection is for isochronous events using
    %   'Algorithm #2'. The default value is false.
    SubeventChannelSelection (1, 1) logical = false
    
    %IsochronousEventCounter Isochronous event counter
    %   Specify the isochronous event counter as an integer in the range
    %   [0, 65535]. This property specifies the counter for each
    %   isochronous event, applicable only when <a
    %   href="matlab:help('bleChannelSelection.SubeventChannelSelection')">SubeventChannelSelection</a>
    %   is enabled. The default value is 0 (first isochronous event).
    IsochronousEventCounter = 0
    
    %SubeventNumber Subevent counter
    %   Specify the subevent counter as an integer in the range [1, 31].
    %   This property specifies the counter for each subevent, applicable
    %   only when <a
    %   href="matlab:help('bleChannelSelection.SubeventChannelSelection')">SubeventChannelSelection</a>
    %   is enabled. The default value is 1 (first subevent).
    SubeventNumber = 1
end

properties (Dependent)
    %UsedChannels List of used (good) data channels
    %   Specify the assigned data channel indices as an integer vector with
    %   element values in the range [0, 36]. The vector length must be
    %   greater than 1. At least two channels must be set as used (good)
    %   channels. This property indicates the set of good channels
    %   classified by the Central. The default value is a row vector
    %   containing all the channel indices [0:36].
    UsedChannels
end

properties (Access = protected)
    %Channel map indicating good (1) and bad (0) data channels
    ChannelMap = ones(1, 37)
end

properties (Access = private)
    %pEventCounter Counter for the next connection events or periodic
    %advertising event
    pEventCounter = 0
    
    %pLastUnmappedChannel Unmapped channel selected for the previous
    %event
    pLastUnmappedChannel = 0
    
    %pPRNSubeventSE Random number generated using subevent pseudo-random
    %number (PRN) generator
    pPRNSubeventSE = 0
    
    %pPRNSubeventLU Random number generated using subevent pseudo-random
    %number generator
    pPRNSubeventLU = 0

    %pSubeventMappingIndex Index used to map a used channel index
    pSubeventMappingIndex = 0
    
    %pPrevSubeventNumber Previous subevent number for which the channel
    %index is selected
    pPrevSubeventNumber = 0

    %pDistance Minimum distance between the channel indices used in
    %consecutive subevents
    pDistance = 1

    %pNumUsedChannels Number of used channels
    pNumUsedChannels = 37

    %pUsedChannels List of used channels
    pUsedChannels = 0:36
end

properties (SetAccess = private)
    %ChannelIndex Selected channel index for current event
    ChannelIndex = 0
end

properties (SetAccess = private, Dependent)
    %EventCounter Counter of the current connection event or periodic
    %advertising event, for which the channel index is selected
    EventCounter
end

methods
    % Constructor
    function obj = bleChannelSelection(varargin)
        % Apply constructor Name-value pairs
        for i = 1 : 2 : nargin
            obj.(varargin{i}) = varargin{i+1};
        end
    end
    
    function set.Algorithm(obj, value)
        % Accept either 1 or 2
        coder.internal.errorIf(~ismember(value, [1 2]), ...
            'bluetooth:bleLL:InvalidChSelAlgorithm');
        obj.Algorithm = value;
        % Reset the stored context
        reset(obj);
    end
    
    function set.HopIncrement(obj, value)
        validateattributes(value, {'numeric'}, {'scalar', 'integer'...
            '>=', 5, '<=', 16}, mfilename, 'HopIncrement');
        obj.HopIncrement = value;
    end
    
    function set.AccessAddress(obj, value)
        ble.internal.validateHex(value, 8, 'AccessAddress');
        obj.AccessAddress = char(value);
    end
    
    % Set channel map from given used channels list
    function set.UsedChannels(obj, value)
        validateattributes(value, {'numeric'}, {'vector', 'nonnegative', ...
            'integer', '<=', 36}, mfilename, 'UsedChannels');
        value = unique(value);
        % At least two channels must be set as used (good) channels as
        % specified in Bluetooth Core Specification v5.2 | Vol 6, Part B,
        % Section 4.5.8.1
        obj.pNumUsedChannels = numel(value);
        coder.internal.errorIf((obj.pNumUsedChannels < 2), ...
            'bluetooth:bleLL:InvalidUsedChannels');
        obj.ChannelMap = zeros(1, 37);
        obj.ChannelMap(value+1) = 1;
        % Distance between consecutive subevents as specified in Bluetooth
        % Core Specification v5.2 | Vol 6, Part B, Section 4.5.8.3.6
        obj.pDistance = max(1, max(min(3, obj.pNumUsedChannels-5), ...
            min(11, floor((obj.pNumUsedChannels-10)/2))));
        obj.pUsedChannels = value;
    end

    function set.SubeventChannelSelection(obj, value)
        obj.SubeventChannelSelection = value;
        % Reset the stored context
        reset(obj);
    end
    
    function set.IsochronousEventCounter(obj, value)
        validateattributes(value, {'numeric'}, {'scalar', 'integer', ...
            'nonnegative', '<=', 65535}, mfilename, 'IsochronousEventCounter');
        obj.IsochronousEventCounter = value;
    end
    
    function set.SubeventNumber(obj, value)
        validateattributes(value, {'numeric'}, {'scalar', 'integer', ...
            '>=', 1, '<=', 31}, mfilename, 'SubeventNumber');
        obj.SubeventNumber = value;
    end
    
    % Get used channels list from channel map
    function usedChannels = get.UsedChannels(obj)
        usedChannels = obj.pUsedChannels;
    end
    
    % Get current event counter
    function eventCounter = get.EventCounter(obj)
        if obj.pEventCounter ~= 0
            eventCounter = obj.pEventCounter - 1;
        else
            eventCounter = 0;
        end
    end
end

methods (Access = protected)
    % Step implementation
    function channelIndex = stepImpl(obj)
        % Generate channel index
        channelIndex = nextHop(obj);
    end
    
    % Reset implementation
    function resetImpl(obj)
        % To start channel selection from first event, reset private
        % context to default.
        obj.ChannelIndex = 0;
        obj.pEventCounter = 0;
        obj.pLastUnmappedChannel = 0;
        obj.pPRNSubeventSE = 0;
        obj.pPRNSubeventLU = 0;
        obj.pSubeventMappingIndex = 0;
        obj.pPrevSubeventNumber = 0;
    end
    
    % Save object
    function s = saveObjectImpl(obj)
        s = saveObjectImpl@matlab.System(obj);
        s.UsedChannels = obj.UsedChannels;
        if isLocked(obj)
            s.ChannelIndex = obj.ChannelIndex;
            s.pEventCounter = obj.pEventCounter;
            s.pLastUnmappedChannel = obj.pLastUnmappedChannel;
            s.pPRNSubeventSE = obj.pPRNSubeventSE;
            s.pPRNSubeventLU = obj.pPRNSubeventLU;
            s.pSubeventMappingIndex = obj.pSubeventMappingIndex;
            s.pPrevSubeventNumber = obj.pPrevSubeventNumber;
        end
    end
    
    % Load object
    function loadObjectImpl(obj, s, wasLocked)
        if wasLocked
            obj.ChannelIndex = s.ChannelIndex;
            obj.pEventCounter = s.pEventCounter;
            obj.pLastUnmappedChannel = s.pLastUnmappedChannel;
            obj.pPRNSubeventSE = s.pPRNSubeventSE;
            obj.pPRNSubeventLU = s.pPRNSubeventLU;
            obj.pSubeventMappingIndex = s.pSubeventMappingIndex;
            obj.pPrevSubeventNumber = s.pPrevSubeventNumber;
        end
        obj.UsedChannels = s.UsedChannels;
        loadObjectImpl@matlab.System(obj, s);
    end
    
    % Conditional display
    function flag = isInactivePropertyImpl(obj, prop)
        flag = true;
        % Applicable for both the algorithms
        if any(strcmp(prop, {'Algorithm', 'UsedChannels', ...
                'ChannelIndex'}))
            flag = false;
            return;
        end
        
        % Based on algorithm
        switch obj.Algorithm
            % Algorithm #1
            case 1
                flag = ~any(strcmp(prop, {'HopIncrement', 'EventCounter'}));
            % Algorithm #2
            case 2
                % Applicable properties for isochronous events
                if obj.SubeventChannelSelection
                    flag = ~any(strcmp(prop, {'AccessAddress', ...
                        'SubeventChannelSelection', 'IsochronousEventCounter', ...
                        'SubeventNumber'}));
                % Applicable properties for connection events and periodic
                % advertising events
                else
                    flag = ~any(strcmp(prop, {'AccessAddress', ...
                        'SubeventChannelSelection', 'EventCounter'}));
                end
        end
    end
end

methods (Access = private)
    % Select next channel index
    function channelIndex = nextHop(obj)
        % Initialize
        channelIndex = 0;

        % Applicable for connection events and periodic advertising events
        if ~obj.SubeventChannelSelection
            % Wrap around
            if (obj.pEventCounter > intmax('uint16'))
                obj.pEventCounter = 0;
            end
        end
        
        % Based on algorithm
        switch (obj.Algorithm)
            case 1 % Algorithm #1
                % Calculate unmapped channel
                unmappedChannel = mod((...
                    obj.pLastUnmappedChannel + obj.HopIncrement), 37);
                % If the unmapped channel is an unusable channel, map to
                % one of the usable channels from the remapping table
                if ~obj.ChannelMap(unmappedChannel + 1)
                    % Construct the remapping table containing usable
                    % channel indices
                    remapTable = obj.pUsedChannels;
                    
                    % Calculate the remapping index. Add 1 since remapTable
                    % indexing starts from 1.
                    remapIndex = mod(unmappedChannel, nnz(obj.ChannelMap)) + 1;
                    
                    % Map the unmapped channel to a usable channel using
                    % remapping table
                    channelIndex = remapTable(remapIndex);
                else
                    % If the unmapped channel is a usable channel,
                    % remapping is not required
                    channelIndex = unmappedChannel;
                end
                
                % Retain the last unmapped channel and current channel
                % index
                obj.pLastUnmappedChannel = unmappedChannel;
                obj.ChannelIndex = channelIndex;
            
            case 2 % Algorithm #2
                % Convert hexadecimal to binary
                binAccessAddress = ...
                    int2bit(hex2dec(obj.AccessAddress), 32, 0)';
                
                % Calculate channel identifier from the given Access
                % Address
                channelIdentifier = ...
                    xor(binAccessAddress(1:16), binAccessAddress(17:32));
                
                % Channel selection for connection events, periodic
                % advertising events and first isochronous subevent
                if ~obj.SubeventChannelSelection || ...
                        obj.SubeventChannelSelection && (obj.SubeventNumber == 1)
                    % Use isochronous event counter
                    if obj.SubeventChannelSelection
                        eventCounter = obj.IsochronousEventCounter;
                        % Update current subevent number
                        obj.pPrevSubeventNumber = obj.SubeventNumber;
                    % Use connection event or periodic advertising event
                    % counter
                    else
                        eventCounter = obj.pEventCounter;
                    end
                    
                    % Generate a pseudo-random number as specified in
                    % Bluetooth Core Specification v5.2 | Vol 6, Part B,
                    % Section 4.5.8.3.3
                    [obj.pPRNSubeventLU, prnE] = ...
                        obj.generatePseudoRandomNumber(...
                        int2bit(eventCounter, 16, 0)', channelIdentifier);
                    
                    % Calculate unmapped channel index using the
                    % pseudo-random number
                    unmappedChannel = mod(prnE, 37);
                    
                    % Select channel index and retain the remap index of
                    % last used channel, as specified in Bluetooth Core
                    % Specification v5.2 | Vol 6, Part B, Section
                    % 4.5.8.3.4
                    [channelIndex, obj.pSubeventMappingIndex] = ...
                        obj.eventMappingToUsedChannelIndex(unmappedChannel, prnE);
                    
                % Channel selection for rest of the isochronous subevents
                else
                    % Select channel index for next subevent (sequential)
                    if obj.SubeventNumber == (obj.pPrevSubeventNumber + 1)
                        % Generate a pseudo-random number for subevents as
                        % specified in Bluetooth Core Specification v5.2 |
                        % Vol 6, Part B, Section 4.5.8.3.5
                        [obj.pPRNSubeventSE, obj.pPRNSubeventLU] = ...
                            obj.generateSubEventPseudoRandomNumber(...
                            obj.pPRNSubeventLU, channelIdentifier);
                        
                        % Select channel index and retain the remap index
                        % of last used channel, as specified in Bluetooth
                        % Core Specification v5.2 | Vol 6, Part B, Section
                        % 4.5.8.3.6
                        [channelIndex, obj.pSubeventMappingIndex] = ...
                            obj.subEventMappingToUsedChannelIndex(...
                            obj.pSubeventMappingIndex, obj.pPRNSubeventSE);
                        
                        % Update current subevent number
                        obj.pPrevSubeventNumber = obj.SubeventNumber;
                    else
                        % Channel index, if the same subevent number is
                        % given as input
                        if obj.SubeventNumber == obj.pPrevSubeventNumber
                            channelIndex = obj.ChannelIndex;
                        % If subevent channel selection is not called
                        % sequentially
                        else
                            coder.internal.error('bluetooth:bleLL:InvalidSubeventNumber', ...
                                obj.pPrevSubeventNumber+1, obj.SubeventNumber);
                        end
                    end
                end
                obj.ChannelIndex = channelIndex;
        end
        
        % Applicable for connection events and periodic advertising events
        if ~obj.SubeventChannelSelection
            % Increment the event counter
            obj.pEventCounter = obj.pEventCounter + 1;
        end
    end
    
    % Generate pseudo-random numbers
    function [prnS, prnE] = generatePseudoRandomNumber(...
            obj, counter, channelIdentifier)
        tmp = double(xor(counter, channelIdentifier));
        % Generate a pseudo-random number (Refer Figure 4.36 in Bluetooth
        % Core Specification v5.2 | Vol 6, Part B, Section 4.5.8.3.3)
        for i = 1 : 3
            % Permutation operation as specified in Bluetooth Core
            % Specification v5.2 | Vol 6, Part B, Section 4.5.8.3.2
            tmp = obj.permutationOp(tmp);
            
            % MAM (Multiply, Addition, Modulo) operation as specified in
            % Bluetooth Core Specification v5.2 | Vol 6, Part B, Section
            % 4.5.8.3.2
            tmp = obj.mamOp(...
                bit2int(tmp', numel(tmp), 0), ...
                bit2int(channelIdentifier', numel(channelIdentifier), 0));
            prnS = bit2int(tmp', numel(tmp), 0);
        end
        prnE = xor(tmp, channelIdentifier);
        prnE = bit2int(prnE', numel(prnE), 0);
    end
    
    % Select channel index and retain remap index of last used channel
    function [channelIndex, eventIndex] = eventMappingToUsedChannelIndex(...
            obj, unmappedChannel, randomNumber)
        % Construct the remapping table containing usable
        % channel indices
        remapTable = obj.pUsedChannels;
        
        % If the unmapped channel is an unusable channel, map to
        % one of the usable channels from the remapping table
        if ~obj.ChannelMap(unmappedChannel + 1)
            % Calculate the remapping index. Add 1 since remapTable
            % indexing starts from 1
            remapIndex = floor(...
                (nnz(obj.ChannelMap)*randomNumber)/(2^16)) + 1;
            
            % Map the unmapped channel to a usable channel using
            % remapping table
            channelIndex = remapTable(remapIndex);
        else
            % If the unmapped channel is a usable channel,
            % remapping is not required
            channelIndex = unmappedChannel;
        end
        eventIndex = find(remapTable == channelIndex);
    end
    
    % Generate pseudo-random numbers for subevent channel selection
    function [prnSubeventSE, prnSubeventLU] = generateSubEventPseudoRandomNumber(...
            obj, prn, channelIdentifier)
        % Convert random number to binary
        prnBin = int2bit(prn, 16, 0)';
        % Permutation operation as specified in Bluetooth Core
        % Specification v5.2 | Vol 6, Part B, Section 4.5.8.3.2
        tmp = obj.permutationOp(prnBin);
        % MAM (Multiply, Addition, Modulo) operation as specified in
        % Bluetooth Core Specification v5.2 | Vol 6, Part B, Section
        % 4.5.8.3.2
        prnSubeventLU = obj.mamOp(...
            bit2int(tmp', numel(tmp), 0), ...
            bit2int(channelIdentifier', numel(channelIdentifier), 0));
        % Generate prnSubeventSE
        prnSubeventSE = xor(prnSubeventLU, channelIdentifier);
        prnSubeventSE = bit2int(prnSubeventSE', numel(prnSubeventSE), 0);
        % Generate prnSubeventLU
        prnSubeventLU = bit2int(prnSubeventLU', numel(prnSubeventLU), 0);
    end
    
    % Select channel index and retain remap index of last used channel for
    % subevents
    function [channelIndex, subEventIndex] = subEventMappingToUsedChannelIndex(...
            obj, indexOfLastUsedChannel, prnSubeventSE)
        subEventIndex = mod((indexOfLastUsedChannel-1)+obj.pDistance+...
            floor(prnSubeventSE*((obj.pNumUsedChannels-2*obj.pDistance+1)/2^16)), obj.pNumUsedChannels);
        subEventIndex = subEventIndex + 1;
        % Select channel index
        channelIndex = obj.pUsedChannels(subEventIndex);
        channelIndex = channelIndex(1); % For codegen
    end
    
    % Permutation operation as mentioned in the specification
    function out = permutationOp(~, num)
        out = zeros(1, 16);
        out(1:8) = num(8:-1:1);
        out(9:16) = num(16:-1:9);
    end
    
    % MAM (Multiply, Addition, Modulo) operation as mentioned in the
    % specification
    function out = mamOp(~, a, b)
        out = mod(((17*a) + b), 2^16);
        out = int2bit(out, 16, 0)';
    end
end
end

% LocalWords:  csa isochronous
