classdef helperBLEGATTServer
%helperBLEGATTServer Stores the attributes database and provides methods to
%access and update the database. It also provides methods corresponding to
%the GATT (Generic Attribute Profile) server features defined in Bluetooth
%core specification.
%   SERVER = helperBLEGATTServer creates a GATT server object.
%
%   See also helperBLEGATTClient.

%   Copyright 2018 The MathWorks, Inc.
    
properties(Access = private)
    % Attributes data base
    AttributeDB = table(...
        ...% Attribute Type(ID)
        {'2800'; '2803'; '2A37'; '2902'; '2803'; '2A38'; '2800'; '2803'; ...
        '2A47'; '2803'; '2A46'; '2902'; '2803'; '2A48'; '2803'; '2A45'; ...
        '2902'; '2803'; '2A44'}, ...
        ... % Attribute Permissions
        {{'Read'}; {'Read'}; {'Notify'}; {'Read', 'Write'}; ...
        {'Read'}; {'Read'}; {'Read'}; {'Read'}; {'Read'}; {'Read'}; ...
        {'Notify'}; {'Read', 'Write'}; {'Read'}; {'Read'}; {'Read'}; ...
        {'Notify'}; {'Read', 'Write'}; {'Read'}; {'Write'}},...
        ... % Attribute Value (Must be in LSB First)
        {'0D18'; '100300372A'; '0032'; '0000'; '020600382A'; ...
        '02'; '1118'; '020900472A'; '01'; '100B00462A'; '0000'; '0000'; ...
        '020D00482A'; '01'; '100F00452A'; '0000'; '0000'; '041100442A'; ...
        '0000'}, ...
        'VariableNames', {'ID', 'Permissions', 'Value'}, ...
        ... % Attribute Handles
        'RowNames', {'0001'; '0002'; '0003'; '0004'; '0005'; '0006'; '0007'; ...
        '0008'; '0009'; '000A'; '000B'; '000C'; '000D'; '000E'; '000F'; ...
        '0010'; '0011'; '0012'; '0013'});
end

methods
    function [startHandle, endHandle, attributeValue] = getPrimaryService(obj, id, reqStartHandle, reqEndHandle)

        % Converting hexadecimal octets into decimal number (For comparison)
        reqStartHandleDec = hex2dec(reqStartHandle);
        reqEndHandleDec = hex2dec(reqEndHandle);

        % Fetch the Attribute database with the given handle range
        endhandle = reqEndHandleDec;
        if reqEndHandleDec > hex2dec(obj.AttributeDB.Properties.RowNames{end})
            endhandle = hex2dec(obj.AttributeDB.Properties.RowNames{end});
        end

        if reqStartHandleDec > endhandle
            startHandle = '0000';
            endHandle = '0000';
            attributeValue = '0000';
            return;
        end

        tempAttributeDB = obj.AttributeDB(reqStartHandleDec:endhandle, :);

        % Fetch handles of attributes, which attribute IDs are matched
        matchedHandles = tempAttributeDB.Properties.RowNames(strcmpi(tempAttributeDB.ID, id));

        % Only one match found, then return the
        if (numel(matchedHandles) == 1)
            startHandle = matchedHandles{1};
            endHandle = obj.AttributeDB.Properties.RowNames{end};
            % More than one match found
        elseif (numel(matchedHandles) > 1)
            startHandle = matchedHandles{1};
            endHandle = dec2hex((hex2dec(matchedHandles{2}) - 1), 4);
            % No match found
        else
            startHandle = '0000';
            endHandle = '0000';
            attributeValue = '0000';
            return;
        end

        % Fetch attribute value
        attributeValue = obj.AttributeDB.Value{...
            strcmpi(obj.AttributeDB.Properties.RowNames, startHandle)};
    end

    function maxHandle = getMaxHandle(obj)
        maxHandle = numel(obj.AttributeDB.Properties.RowNames);
    end

    function [handle, value] = getCharacteristic(obj, id, startHandle, endHandle)
        % Converting hexadecimal octets into decimal number (For comparison)
        startHandleDec = hex2dec(startHandle);
        endHandleDec = hex2dec(endHandle);

        % Fetch the Attribute database with the given handle range
        endhandle = endHandleDec;
        if endHandleDec > hex2dec(obj.AttributeDB.Properties.RowNames{end})
            endhandle = hex2dec(obj.AttributeDB.Properties.RowNames{end});
        end

        if startHandleDec > endhandle
            handle = '0000';
            value = '0000';
            return;
        end

        tempAttributeDB = obj.AttributeDB(startHandleDec:endhandle, :);

        % Fetch handles of attributes, which attribute IDs are matched
        matchedHandles = tempAttributeDB.Properties.RowNames(strcmpi(tempAttributeDB.ID, id));

        if (numel(matchedHandles) >= 1)
            handle = matchedHandles{1};
            value = tempAttributeDB.Value{strcmpi(tempAttributeDB.Properties.RowNames, handle)};
        else
            handle = '0000';
            value = '0000';
        end
    end

    function [handle, value, descriptors] = getCharacteristicDescriptor(obj, startHandle, endHandle)
        % As of now only "Client characteristic configuration descriptor"
        % (CCCD) is defined in attribute database
        descriptors = 'Client characteristic configuration descriptor';

        value = '0000';

        % Get handles of the descriptors
        id = helperBluetoothID.getBluetoothID(descriptors);
        startHandleDec = hex2dec(startHandle);
        endHandleDec = hex2dec(endHandle);

        % Fetch the Attribute database with the given handle range
        endhandle = endHandleDec;
        if endHandleDec > hex2dec(obj.AttributeDB.Properties.RowNames{end})
            endhandle = hex2dec(obj.AttributeDB.Properties.RowNames{end});
        end

        if startHandleDec > endhandle
            handle = '0000';
            return;
        end

        tempAttributeDB = obj.AttributeDB(startHandleDec:endhandle, :);

        matchedHandles = tempAttributeDB.Properties.RowNames(strcmpi(tempAttributeDB.ID, id));

        if (numel(matchedHandles) >= 1)
            handle = matchedHandles{1};
            value = tempAttributeDB.Value{strcmpi(tempAttributeDB.Properties.RowNames, handle)};
        else
            handle = '0000';
        end
    end

    function handles = getHandleByID(obj, id)
        %getHandleByID returns the handles of the attributes with given id
        handles = [];
        index = strcmpi(obj.AttributeDB.ID, id);
        if any(index)
            handles = obj.AttributeDB.Properties.RowNames(index);
        end
    end

    function values = getValueByID(obj, id)
        %getValueByID returns the value of the attribute with given id
        values = [];
        index = strcmpi(obj.AttributeDB.ID, id);
        if any(index)
            values = obj.AttributeDB.Value(index);
        end
    end

    function value = getValueByHandle(obj, attributeHandle)
        %getValueByHandle returns the value of the attribute with given handle
        value = [];
        index = strcmpi(obj.AttributeDB.Properties.RowNames, attributeHandle);
        if any(index)
            value = obj.AttributeDB.Value{index};
        end
    end

    function permissions = getAttributePermissionsByHandle(obj, attributeHandle)
        %getAttributePermissionsByHandle returns the permissions of the
        %attribute with given handle
        permissions = cell(0, 0);
        index = strcmpi(obj.AttributeDB.Properties.RowNames, ...
            dec2hex(attributeHandle, 4));
        if any(index)
            permissions = obj.AttributeDB.Permissions{index};
        end
    end

    function attributeType = getAttributeTypeByHandle(obj, attributeHandle)
        %getAttributePermissionsByHandle returns the permissions of the
        %attribute with given handle
        attributeType = '';
        index = strcmpi(obj.AttributeDB.Properties.RowNames, attributeHandle);
        if any(index)
            attributeType = helperBluetoothID.getBluetoothName(obj.AttributeDB.ID{index});
        end
    end

    function obj = updateAttributeValueByHandle(obj, attributeHandle, value)
        %updateAttributeValueByHandle updates the value of the attribute with
        %given value
        if isrow(value)
            obj.AttributeDB(attributeHandle, :).Value = {value};
        else
            obj.AttributeDB(attributeHandle, :).Value = {reshape(value', 1, [])};
        end
    end

    function attPDU = handleValueNotifiaction(obj, attributeHandle)
        %handleValueNotification returns a notification ATT PDU with attribute value
        % corresponding to given attribute handle
        attributeValue = getValueByHandle(obj, attributeHandle);

        if isempty(attributeValue)
            error(['Given attribute handle is not found in the attribute database. ' ...
                'Use attribute handles obtained from the GATT characteristic or service discovery.']);
        end

        if any(strcmp(obj.AttributeDB(attributeHandle, :).Permissions{:}, 'Notify'))
            cfg = bleATTPDUConfig();
            cfg.Opcode = 'Handle value notification';
            cfg.AttributeValue = attributeValue;
            cfg.AttributeHandle = attributeHandle;
            attPDU = bleATTPDU(cfg);
        else
            error(['Client did not subscribe for notifications of this attribute value. ' ...
                'In order to subscribe, GATT client must update the ''Client Characteristic ' ...
                'Configuration Descriptor (CCCD)'' attribute value to 1 or 3 at the server.']);
        end
    end

    function [obj, notificationPDU] = notifyHeartRateMeasurement(obj, heartRateMeasurementValue)
        % Get heart rate measurement characteristic ID and handle from attribute
        % database
        id = helperBluetoothID.getBluetoothID('Heart rate measurement');
        handles = getHandleByID(obj, id);
        handle = handles{1};
        
        heartRateMeasurement = ...
            helperBLEAttributeValueConfig(...
            'AttributeType', 'Heart rate measurement');
        heartRateMeasurement.HeartRateValueFormat = 'UINT8';
        heartRateMeasurement.SensorContactStatus = 'Contact detected';
        heartRateMeasurement.EnergyExpendedFieldFlag = 'Present, Units: Kilo Joules';
        heartRateMeasurement.RRIntervalFieldFlag = 'Present';
        heartRateMeasurement.EnergyExpended = 100;
        heartRateMeasurement.RRInterval = 10;
        heartRateMeasurement.HeartRateValue = heartRateMeasurementValue;
        
        % Prepare heart rate measurement characteristic value
        value = helperBLEGenerateAttributeValue(heartRateMeasurement);
        
        % Update heart rate measurement characteristic value at server
        obj = updateAttributeValueByHandle(obj, handle, value);
        
        % SERVER: Prepare notification PDU
        notificationPDU = handleValueNotifiaction(obj, handle);
    end
    
    function [respPDU, reqCfg, obj] = receiveData(obj, rxPDU)
        % [respPDU, reqCfg, obj] = receiveData(obj, rxPDU) decodes the
        % given rxPDU as an ATT PDU and returns the corresponding
        % configuration object to reqCfg, response PDU to respPDU and
        % updated GATTServer object to obj. Empty array will be returned if
        % response is not applicable.

        respPDU = [];
        [status, reqCfg] = bleATTPDUDecode(rxPDU);

        if (status == blePacketDecodeStatus.Success)
            errRespPDU = validateATTPDU(obj, reqCfg);
        end

        if ~isempty(errRespPDU) && (status == blePacketDecodeStatus.Success)
            respPDU = errRespPDU;
        else
            switch reqCfg.Opcode
                case 'Read request'
                    respPDU = readCharacteristicValue(obj, ...
                        reqCfg.AttributeHandle);

                case 'Read by type request'
                    attributeType = helperBluetoothID.getBluetoothName(reqCfg.AttributeType);
                    if strcmp(attributeType, 'Characteristic')
                        respPDU = readUsingCharacteristicID(obj, reqCfg);
                    else
                        disp(['Response frame for "Read by type request" with ' ...
                            'AttributeType "' helperBluetoothID.getBluetoothName(reqCfg.AttributeType) ...
                            '" is not supported in the GATT server.\n']);
                    end

                case 'Read by group type request'
                    attributeType = helperBluetoothID.getBluetoothName(reqCfg.AttributeType);
                    if strcmpi(attributeType, 'Primary Service')
                        respPDU = discoverAllPrimaryServices(obj, reqCfg);
                    else
                        disp(['There is no response frame for "Read by group type request" with ' ...
                            'AttributeType "' attributeType '" from the GATT server.\n']);
                    end

                case 'Information request'
                    respPDU = discoverAllDescriptors(obj, reqCfg);

                case 'Write request'
                    [respPDU, obj] = writeValue(obj, reqCfg.AttributeHandle, reqCfg.AttributeValue);
            end
        end
    end
end

methods(Access = private)
    function attPDU = readCharacteristicValue(obj, attributeHandle)
        cfg = bleATTPDUConfig();
        cfg.Opcode = 'Read response';
        value = getValueByHandle(obj, attributeHandle);
        cfg.AttributeValue = value;
        attPDU = bleATTPDU(cfg);
    end

    function [attPDU, obj] = writeValue(obj, handle, value)
        % Write attribute value into database

        cfg = bleATTPDUConfig();
        cfg.Opcode = 'Write response';

        obj = updateAttributeValueByHandle(obj, handle, value);

        attPDU = bleATTPDU(cfg);
    end

    function attPDU = readUsingCharacteristicID(obj, reqCfg)
        id = helperBluetoothID.getBluetoothID('Characteristic');
        cfg = bleATTPDUConfig();
        cfg.Opcode = 'Read by type response';
        [cfg.AttributeHandle, cfg.AttributeValue] = getCharacteristic(obj, id, ...
            reqCfg.StartHandle, reqCfg.EndHandle);

        if strcmp(cfg.AttributeHandle, '0000') || strcmp(cfg.AttributeValue, '0000')
            cfg.Opcode = 'Error response';
            cfg.ErrorMessage = 'Attribute not found';
            cfg.AttributeHandle = reqCfg.StartHandle;
            cfg.RequestedOpcode = reqCfg.Opcode;
        end
        attPDU = bleATTPDU(cfg);
    end

    function attPDU = discoverAllDescriptors(obj, reqCfg)

        cfg = bleATTPDUConfig();

        [handle, value, descriptorType] = getCharacteristicDescriptor(obj, ...
            reqCfg.StartHandle, reqCfg.EndHandle);

        if strcmp(handle, '0000')
            cfg.Opcode = 'Error response';
            cfg.ErrorMessage = 'Attribute not found';
            cfg.AttributeHandle = reqCfg.StartHandle;
            cfg.RequestedOpcode = reqCfg.Opcode;
        else
            cfg.Opcode = 'Information response';
            cfg.Format = '16 bit';
            cfg.AttributeHandle = handle;
            % Client characteristic configuration descriptor
            cfg.AttributeType = helperBluetoothID.getBluetoothID(descriptorType);

            % Converting AttributeValue, in the form of binary column vector
            cfg.AttributeValue = value;
        end

        attPDU = bleATTPDU(cfg);
    end

    function attPDU = discoverAllPrimaryServices(obj, reqCfg)
        cfg = bleATTPDUConfig();
        cfg.Opcode = 'Read by group type response';

        [cfg.StartHandle, cfg.EndHandle, attributeValue] = ...
            obj.getPrimaryService(helperBluetoothID.getBluetoothID('Primary service'), ...
            reqCfg.StartHandle, reqCfg.EndHandle);

        if strcmp(cfg.StartHandle, '0000') || strcmp(cfg.EndHandle, '0000')
            cfg.Opcode = 'Error response';
            cfg.ErrorMessage = 'Attribute not found';
            cfg.AttributeHandle = reqCfg.StartHandle;
            cfg.RequestedOpcode = reqCfg.Opcode;
        else
            % Converting AttributeValue, in the form of binary column vector
            cfg.AttributeValue = attributeValue;
        end

        attPDU = bleATTPDU(cfg);
    end

    function errRespPDU = validateATTPDU(obj, reqCfg)
        errRespPDU = [];
        switch reqCfg.Opcode
            case {'Read request', 'Write request'}

                attributeHandle = hex2dec(reqCfg.AttributeHandle);

                % prepare error response configuration object
                cfg = bleATTPDUConfig;
                cfg.Opcode = 'Error response';
                cfg.RequestedOpcode = reqCfg.Opcode;
                cfg.AttributeHandle = reqCfg.AttributeHandle;

                if ((attributeHandle == 0) || (attributeHandle > obj.getMaxHandle))
                    % Attribute handle in request PDU is invalid
                    cfg.ErrorMessage = 'Invalid handle';
                    errRespPDU = bleATTPDU(cfg);

                elseif strcmp(reqCfg.Opcode, 'Read request') && ...
                        ~any(strcmp(getAttributePermissionsByHandle(obj, attributeHandle), ...
                        'Read'))
                    % Requested attribute doesn't have read permissions
                    cfg.ErrorMessage = 'Read not permitted';
                    errRespPDU = bleATTPDU(cfg);

                elseif strcmp(reqCfg.Opcode, 'Write request') && ...
                        ~any(strcmp(getAttributePermissionsByHandle(obj, attributeHandle), ...
                        'Write'))
                    % Requested attribute doesn't have write permissions
                    cfg.ErrorMessage = 'Write not permitted';
                    errRespPDU = bleATTPDU(cfg);
                end

            case {'Read by group type request', 'Read by type request'}

                attributeType = reqCfg.AttributeType;
                startHandle = hex2dec(reqCfg.StartHandle);
                endHandle = hex2dec(reqCfg.EndHandle);

                % prepare error response configuration object
                cfg = bleATTPDUConfig;
                cfg.Opcode = 'Error response';
                cfg.RequestedOpcode = reqCfg.Opcode;
                cfg.AttributeHandle = reqCfg.StartHandle;

                if ((startHandle == 0) || (startHandle > endHandle))
                    % Attribute handle in request PDU is invalid
                    cfg.ErrorMessage = 'Invalid handle';
                    errRespPDU = bleATTPDU(cfg);

                    % If requested attribute type is not found in attribute database
                elseif isempty(attributeType)
                    if strcmp(reqCfg.Opcode, 'Read by group type request')
                        cfg.ErrorMessage = 'Unsupported group type';
                    else
                        cfg.ErrorMessage = 'Attribute not found';
                    end
                    errRespPDU = bleATTPDU(cfg);

                    % The requested attribute type is found, but its handle is
                    % outside the specified handle range
                elseif (all(hex2dec(getHandleByID(obj, attributeType)) < startHandle) || ...
                        all(hex2dec(getHandleByID(obj, attributeType)) > endHandle))
                    cfg.ErrorMessage = 'Attribute not found';
                    errRespPDU = bleATTPDU(cfg);
                end
        end
    end
end
end

