Mistake on this page?
Report an issue in GitHub or email us

GattClient

Note: Some functions, variables or types have been deprecated. Please see the class reference linked below for details.

You can use Generic Attribute Profile (GATT) to discover services, characteristics and descriptors and to perform operations on them. The interaction happens between two peers, one of which is the client (which initiates interactions) and the other is the server (which responds). You can use Attribute Protocol (ATT) to implement this interaction. GattClient defines procedures required for interacting with a remote GattServer.

Discovery procedures

A GattServer hosts a fixed set of services. These services are a logical composition of characteristics that may be discovered, read or written and can broadcast their state to a connected client. These characteristics may also contain metainformation and named characteristic descriptors. A characteristic descriptor may indicate the unit used for a characteristic value, describe the characteristic purpose in a textual form or allow a client to register for update notifications for the characteristic value.

Prior to any interaction with a server characteristic, a GattClient discovers the layout of the services and characteristics present on the server.

The layout of the descriptors of a characteristic may also be issued as an extra discovery step.

Attribute manipulation

As a result of the discovery process, the client can start interacting with the characteristic discovered. Depending on the characteristic properties (acquired during discovery), a client can read or write the value of a given characteristic.

Mbed BLE abstracts read and write operations to offer a single API that can be used to read or write characteristic values. The application code does not have to handle the necessary fragmentation or reassembly process if the attribute value to be transported can't fit in a single data packet.

Attribute Protocol Maximum Transmission Unit (ATT_MTU)

ATT_MTU is the maximum size of the attribute protocol packet. Operation on attributes too large to fit into a single packet are split across multiple operations.

This is independent of the data length, which controls the over-the-air packet payload size (which the GAP handles). An L2CAP packet containing the attribute protocol packet is fragmented over many packets if required.

Only GattClient can trigger the exchange of ATT_MTU between client and server. Depending on the implementation of the bluetooth stack, the client may trigger the exchange upon connection. You may also manually request the exchange using negotiateAttMtu. If an exchange happens, the biggest value possible across both devices will be used. Negotiation is only a best-effort process and does not guarantee a higher value being set.

ATT_MTU is at least 23 octets by default. If a larger size is negotiated, the user application will be informed through the onAttMtuChange function called in the GattClient::EventHandler. (GattServer::EventHandler is also informed.)

Server initiated events

When a server updates a characteristic value, it can forward the new value to any registered clients. Clients may register for these updates on a per-characteristic basis. The server sends the updates by using notifications (no confirmation from client) or indications (client confirms receipt). This mechanism minimizes the number of transactions between a client and a server by avoiding polling.

Clients register for these updates by setting the Client Characteristic Configuration Descriptor (CCCD) value. This is an attribute, and the client needs to discover its descriptor. It is present in the characteristic if its notify or indicate properties are set.

GattClient class reference

Data Structures
struct  EventHandler
 Definition of the general handler of GattClient related events. More...
Public Types
typedef FunctionPointerWithContext< const GattReadCallbackParams * > ReadCallback_t
 Attribute read event handler. More...
typedef CallChainOfFunctionPointersWithContext< const GattReadCallbackParams * > ReadCallbackChain_t
 Callchain of attribute read event handlers. More...
typedef FunctionPointerWithContext< const GattWriteCallbackParams * > WriteCallback_t
 Attribute write event handler. More...
typedef CallChainOfFunctionPointersWithContext< const GattWriteCallbackParams * > WriteCallbackChain_t
 Callchain of attribute write event handlers. More...
typedef FunctionPointerWithContext< const GattHVXCallbackParams * > HVXCallback_t
 Handle value notification/indication event handler. More...
typedef CallChainOfFunctionPointersWithContext< const GattHVXCallbackParams * > HVXCallbackChain_t
 Callchain of handle value notification/indication event handlers. More...
typedef FunctionPointerWithContext< const GattClient * > GattClientShutdownCallback_t
 Shutdown event handler. More...
typedef CallChainOfFunctionPointersWithContext< const GattClient * > GattClientShutdownCallbackChain_t
 Callchain of shutdown event handlers. More...
Public Member Functions
void setEventHandler (EventHandler *handler)
 Assign the event handler implementation that will be used by the module to signal events back to the application. More...
ble_error_t launchServiceDiscovery (ble::connection_handle_t connectionHandle, ServiceDiscovery::ServiceCallback_t sc=NULL, ServiceDiscovery::CharacteristicCallback_t cc=NULL, const UUID &matchingServiceUUID=UUID::ShortUUIDBytes_t(BLE_UUID_UNKNOWN), const UUID &matchingCharacteristicUUIDIn=UUID::ShortUUIDBytes_t(BLE_UUID_UNKNOWN))
 Launch the service and characteristic discovery procedure of a GATT server peer. More...
ble_error_t discoverServices (ble::connection_handle_t connectionHandle, ServiceDiscovery::ServiceCallback_t callback, const UUID &matchingServiceUUID=UUID::ShortUUIDBytes_t(BLE_UUID_UNKNOWN))
 Launch the service discovery procedure of a GATT server peer. More...
ble_error_t discoverServices (ble::connection_handle_t connectionHandle, ServiceDiscovery::ServiceCallback_t callback, GattAttribute::Handle_t startHandle, GattAttribute::Handle_t endHandle)
 Launch the service discovery procedure of a GATT server peer. More...
bool isServiceDiscoveryActive (void) const
 Check if the service discovery procedure is currently active. More...
void terminateServiceDiscovery (void)
 Terminate all ongoing service discovery procedures. More...
ble_error_t read (ble::connection_handle_t connHandle, GattAttribute::Handle_t attributeHandle, uint16_t offset) const
 Initiate the read procedure of an attribute handle. More...
ble_error_t write (GattClient::WriteOp_t cmd, ble::connection_handle_t connHandle, GattAttribute::Handle_t attributeHandle, size_t length, const uint8_t *value) const
 Initiate a write procedure on an attribute value. More...
void onDataRead (ReadCallback_t callback)
 Register an attribute read event handler. More...
ReadCallbackChain_tonDataRead ()
 Get the callchain of attribute read event handlers. More...
void onDataWritten (WriteCallback_t callback)
 Register an attribute write event handler. More...
WriteCallbackChain_tonDataWritten ()
 Get the callchain of attribute write event handlers. More...
void onDataWrite (WriteCallback_t callback)
 Register an attribute write event handler. More...
void onServiceDiscoveryTermination (ServiceDiscovery::TerminationCallback_t callback)
 Register a service discovery termination event handler. More...
ble_error_t discoverCharacteristicDescriptors (const DiscoveredCharacteristic &characteristic, const CharacteristicDescriptorDiscovery::DiscoveryCallback_t &discoveryCallback, const CharacteristicDescriptorDiscovery::TerminationCallback_t &terminationCallback)
 Initiate the descriptor discovery procedure for a given characteristic. More...
bool isCharacteristicDescriptorDiscoveryActive (const DiscoveredCharacteristic &characteristic) const
 Query status of the descriptor discovery procedure for a given characteristic. More...
void terminateCharacteristicDescriptorDiscovery (const DiscoveredCharacteristic &characteristic)
 Terminate an ongoing characteristic descriptor discovery procedure. More...
ble_error_t negotiateAttMtu (ble::connection_handle_t connection)
 Trigger MTU negotiation. More...
void onHVX (HVXCallback_t callback)
 Register an handler for Handle Value Notification/Indication events. More...
void onShutdown (const GattClientShutdownCallback_t &callback)
 Register a shutdown event handler. More...
template<typename T >
void onShutdown (T *objPtr, void(T::*memberPtr)(const GattClient *))
 Register a shutdown event handler. More...
GattClientShutdownCallbackChain_tonShutdown ()
 Get the callchain of shutdown event handlers. More...
HVXCallbackChain_tonHVX ()
 provide access to the callchain of HVX callbacks. More...
ble_error_t reset (void)
 Reset the state of the GattClient instance. More...
void processReadResponse (const GattReadCallbackParams *params)
 Forward an attribute read event to all registered handlers. More...
void processWriteResponse (const GattWriteCallbackParams *params)
 Forward an attribute written event to all registered handlers. More...
void processHVXEvent (const GattHVXCallbackParams *params)
 Forward a handle value notification or indication event to all registered handlers. More...
Protected Attributes
EventHandlereventHandler
 Event handler provided by the application. More...
ReadCallbackChain_t onDataReadCallbackChain
 Callchain containing all registered event handlers for data read events. More...
WriteCallbackChain_t onDataWriteCallbackChain
 Callchain containing all registered event handlers for data write events. More...
HVXCallbackChain_t onHVXCallbackChain
 Callchain containing all registered event handlers for update events. More...
GattClientShutdownCallbackChain_t shutdownCallChain
 Callchain containing all registered event handlers for shutdown events. More...

GattClient example


/* mbed Microcontroller Library
 * Copyright (c) 2006-2018 ARM Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <memory>
#include <new>
#include <stdio.h>

#include "events/EventQueue.h"
#include "platform/NonCopyable.h"

#include "ble/BLE.h"
#include "ble/Gap.h"
#include "ble/GattClient.h"
#include "ble/GapAdvertisingParams.h"
#include "ble/GapAdvertisingData.h"
#include "ble/GattClient.h"
#include "ble/DiscoveredService.h"
#include "ble/DiscoveredCharacteristic.h"
#include "ble/CharacteristicDescriptorDiscovery.h"

#include "BLEProcess.h"

/**
 * Handle discovery of the GATT server.
 *
 * First the GATT server is discovered in its entirety then each readable
 * characteristic is read and the client register to characteristic
 * notifications or indication when available. The client report server
 * indications and notification until the connection end.
 */
class GattClientProcess : private mbed::NonCopyable<GattClientProcess>,
                          public ble::Gap::EventHandler {

    // Internal typedef to this class type.
    // It is used as a shorthand to pass member function as callbacks.
    typedef GattClientProcess Self;

    typedef CharacteristicDescriptorDiscovery::DiscoveryCallbackParams_t
        DiscoveryCallbackParams_t;

    typedef CharacteristicDescriptorDiscovery::TerminationCallbackParams_t
        TerminationCallbackParams_t;

    typedef DiscoveredCharacteristic::Properties_t Properties_t;

public:

    /**
     * Construct an empty client process.
     *
     * The function start() shall be called to initiate the discovery process.
     */
    GattClientProcess() :
        _client(NULL),
        _connection_handle(),
        _characteristics(NULL),
        _it(NULL),
        _descriptor_handle(0),
        _ble_interface(NULL),
        _event_queue(NULL) {
    }

    ~GattClientProcess()
    {
        stop();
    }

    void init(BLE &ble_interface, events::EventQueue &event_queue)
    {
        _ble_interface = &ble_interface;
        _event_queue = &event_queue;
        _client = &_ble_interface->gattClient();

        _ble_interface->gap().setEventHandler(this);
    }

    /**
     * Start the discovery process.
     *
     * @param[in] client The GattClient instance which will discover the distant
     * GATT server.
     * @param[in] connection_handle Reference of the connection to the GATT
     * server which will be discovered.
     */
    void start()
    {
        // setup the event handlers called during the process
        _client->onDataWritten().add(as_cb(&Self::when_descriptor_written));
        _client->onHVX().add(as_cb(&Self::when_characteristic_changed));

        // The discovery process will invoke when_service_discovered when a
        // service is discovered, when_characteristic_discovered when a
        // characteristic is discovered and when_service_discovery_ends once the
        // discovery process has ended.
        _client->onServiceDiscoveryTermination(as_cb(&Self::when_service_discovery_ends));
        ble_error_t error = _client->launchServiceDiscovery(
            _connection_handle,
            as_cb(&Self::when_service_discovered),
            as_cb(&Self::when_characteristic_discovered)
        );

        if (error) {
            printf("Error %u returned by _client->launchServiceDiscovery.\r\n", error);
            return;
        }

        printf("Client process started: initiate service discovery.\r\n");
    }

    /**
     * Stop the discovery process and clean the instance.
     */
    void stop()
    {
        if (!_client) {
            return;
        }

        // unregister event handlers
        _client->onDataWritten().detach(as_cb(&Self::when_descriptor_written));
        _client->onHVX().detach(as_cb(&Self::when_characteristic_changed));
        _client->onServiceDiscoveryTermination(NULL);

        // remove discovered characteristics
        clear_characteristics();

        // clean up the instance
        _connection_handle = 0;
        _characteristics = NULL;
        _it = NULL;
        _descriptor_handle = 0;

        printf("Client process stopped.\r\n");
    }

private:
    /**
     * Event handler invoked when a connection is established.
     *
     * This function setup the connection handle to operate on then start the
     * discovery process.
     */
    virtual void onConnectionComplete(const ble::ConnectionCompleteEvent &event)
    {
        _connection_handle = event.getConnectionHandle();
        _event_queue->call(mbed::callback(this, &Self::start));
    }

    /**
     * Stop the discovery process and clean the instance.
     */
    virtual void onDisconnectionComplete(const ble::DisconnectionCompleteEvent &event)
    {
        if (_client && event.getConnectionHandle() == _connection_handle) {
            stop();
        }
    }

private:
////////////////////////////////////////////////////////////////////////////////
// Service and characteristic discovery process.

    /**
     * Handle services discovered.
     *
     * The GattClient invokes this function when a service has been discovered.
     *
     * @see GattClient::launchServiceDiscovery
     */
    void when_service_discovered(const DiscoveredService *discovered_service)
    {
        // print information of the service discovered
        printf("Service discovered: value = ");
        print_uuid(discovered_service->getUUID());
        printf(", start = %u, end = %u.\r\n",
            discovered_service->getStartHandle(),
            discovered_service->getEndHandle()
        );
    }

    /**
     * Handle characteristics discovered.
     *
     * The GattClient invoke this function when a characteristic has been
     * discovered.
     *
     * @see GattClient::launchServiceDiscovery
     */
    void when_characteristic_discovered(const DiscoveredCharacteristic *discovered_characteristic)
    {
        // print characteristics properties
        printf("\tCharacteristic discovered: uuid = ");
        print_uuid(discovered_characteristic->getUUID());
        printf(", properties = ");
        print_properties(discovered_characteristic->getProperties());
        printf(
            ", decl handle = %u, value handle = %u, last handle = %u.\r\n",
            discovered_characteristic->getDeclHandle(),
            discovered_characteristic->getValueHandle(),
            discovered_characteristic->getLastHandle()
        );

        // add the characteristic into the list of discovered characteristics
        bool success = add_characteristic(discovered_characteristic);
        if (!success) {
            printf("Error: memory allocation failure while adding the discovered characteristic.\r\n");
            _client->terminateServiceDiscovery();
            stop();
            return;
        }
    }

    /**
     * Handle termination of the service and characteristic discovery process.
     *
     * The GattClient invokes this function when the service and characteristic
     * discovery process ends.
     *
     * @see GattClient::onServiceDiscoveryTermination
     */
    void when_service_discovery_ends(Gap::Handle_t connection_handle)
    {
        if (!_characteristics) {
            printf("No characteristics discovered, end of the process.\r\n");
            return;
        }

        printf("All services and characteristics discovered, process them.\r\n");

        // reset iterator and start processing characteristics in order
        _it = NULL;
        _event_queue->call(mbed::callback(this, &Self::process_next_characteristic));
    }

////////////////////////////////////////////////////////////////////////////////
// Processing of characteristics based on their properties.

    /**
     * Process the characteristics discovered.
     *
     * - If the characteristic is readable then read its value and print it. Then
     * - If the characteristic can emit notification or indication then discover
     * the characteristic CCCD and subscribe to the server initiated event.
     * - Otherwise skip the characteristic processing.
     */
    void process_next_characteristic(void)
    {
        if (!_it) {
            _it = _characteristics;
        } else {
            _it = _it->next;
        }

        while (_it) {
            Properties_t properties = _it->value.getProperties();

            if (properties.read()) {
                read_characteristic(_it->value);
                return;
            } else if(properties.notify() || properties.indicate()) {
                discover_descriptors(_it->value);
                return;
            } else {
                printf(
                    "Skip processing of characteristic %u\r\n",
                    _it->value.getValueHandle()
                );
                _it = _it->next;
            }
        }

        printf("All characteristics discovered have been processed.\r\n");
    }

    /**
     * Initate the read of the characteristic in input.
     *
     * The completion of the operation will happens in when_characteristic_read()
     */
    void read_characteristic(const DiscoveredCharacteristic &characteristic)
    {
        printf("Initiating read at %u.\r\n", characteristic.getValueHandle());
        ble_error_t error = characteristic.read(
            0, as_cb(&Self::when_characteristic_read)
        );

        if (error) {
            printf(
                "Error: cannot initiate read at %u due to %u\r\n",
                characteristic.getValueHandle(), error
            );
            stop();
        }
    }

    /**
     * Handle the reception of a read response.
     *
     * If the characteristic can emit notification or indication then start the
     * discovery of the the characteristic descriptors then subscribe to the
     * server initiated event by writing the CCCD discovered. Otherwise start
     * the processing of the next characteristic discovered in the server.
     */
    void when_characteristic_read(const GattReadCallbackParams *read_event)
    {
        printf("\tCharacteristic value at %u equal to: ", read_event->handle);
        for (size_t i = 0; i <  read_event->len; ++i) {
            printf("0x%02X ", read_event->data[i]);
        }
        printf(".\r\n");

        Properties_t properties = _it->value.getProperties();

        if(properties.notify() || properties.indicate()) {
            discover_descriptors(_it->value);
        } else {
            process_next_characteristic();
        }
    }

    /**
     * Initiate the discovery of the descriptors of the characteristic in input.
     *
     * When a descriptor is discovered, the function when_descriptor_discovered
     * is invoked.
     */
    void discover_descriptors(const DiscoveredCharacteristic &characteristic)
    {
        printf("Initiating descriptor discovery of %u.\r\n", characteristic.getValueHandle());

        _descriptor_handle = 0;
        ble_error_t error = characteristic.discoverDescriptors(
            as_cb(&Self::when_descriptor_discovered),
            as_cb(&Self::when_descriptor_discovery_ends)
        );

        if (error) {
            printf(
                "Error: cannot initiate discovery of %04X due to %u.\r\n",
                characteristic.getValueHandle(), error
            );
            stop();
        }
    }

    /**
     * Handle the discovery of the characteristic descriptors.
     *
     * If the descriptor found is a CCCD then stop the discovery. Once the
     * process has ended subscribe to server initiated events by writing the
     * value of the CCCD.
     */
    void when_descriptor_discovered(const DiscoveryCallbackParams_t* event)
    {
        printf("\tDescriptor discovered at %u, UUID: ", event->descriptor.getAttributeHandle());
        print_uuid(event->descriptor.getUUID());
        printf(".\r\n");

        if (event->descriptor.getUUID() == BLE_UUID_DESCRIPTOR_CLIENT_CHAR_CONFIG) {
            _descriptor_handle = event->descriptor.getAttributeHandle();
            _client->terminateCharacteristicDescriptorDiscovery(
                event->characteristic
            );
        }
    }

    /**
     * If a CCCD has been found subscribe to server initiated events by writing
     * its value.
     */
    void when_descriptor_discovery_ends(const TerminationCallbackParams_t *event) {
        // shall never happen but happen with android devices ...
        // process the next charateristic
        if (!_descriptor_handle) {
            printf("\tWarning: characteristic with notify or indicate attribute without CCCD.\r\n");
            process_next_characteristic();
            return;
        }

        Properties_t properties = _it->value.getProperties();

        uint16_t cccd_value =
            (properties.notify() << 0) | (properties.indicate() << 1);

        ble_error_t error = _client->write(
            GattClient::GATT_OP_WRITE_REQ,
            _connection_handle,
            _descriptor_handle,
            sizeof(cccd_value),
            reinterpret_cast<uint8_t*>(&cccd_value)
        );

        if (error) {
            printf(
                "Error: cannot initiate write of CCCD %u due to %u.\r\n",
                _descriptor_handle, error
            );
            stop();
        }
    }

    /**
     * Called when the CCCD has been written.
     */
    void when_descriptor_written(const GattWriteCallbackParams* event)
    {
        // should never happen
        if (!_descriptor_handle) {
            printf("\tError: received write response to unsolicited request.\r\n");
            stop();
            return;
        }

        printf("\tCCCD at %u written.\r\n", _descriptor_handle);
        _descriptor_handle = 0;
        process_next_characteristic();
    }

    /**
     * Print the updated value of the characteristic.
     *
     * This function is called when the server emits a notification or an
     * indication of a characteristic value the client has subscribed to.
     *
     * @see GattClient::onHVX()
     */
    void when_characteristic_changed(const GattHVXCallbackParams* event)
    {
        printf("Change on attribute %u: new value = ", event->handle);
        for (size_t i = 0; i < event->len; ++i) {
            printf("0x%02X ", event->data[i]);
        }
        printf(".\r\n");
    }

    struct DiscoveredCharacteristicNode {
        DiscoveredCharacteristicNode(const DiscoveredCharacteristic &c) :
            value(c), next(NULL) { }

        DiscoveredCharacteristic value;
        DiscoveredCharacteristicNode *next;
    };

    /**
     * Add a discovered characteristic into the list of discovered characteristics.
     */
    bool add_characteristic(const DiscoveredCharacteristic *characteristic)
    {
        DiscoveredCharacteristicNode* new_node =
            new(std::nothrow) DiscoveredCharacteristicNode(*characteristic);

        if (new_node == false) {
            printf("Error while allocating a new characteristic.\r\n");
            return false;
        }

        if (_characteristics == NULL) {
            _characteristics = new_node;
        } else {
            DiscoveredCharacteristicNode* c = _characteristics;
            while(c->next) {
                c = c->next;
            }
            c->next = new_node;
        }

        return true;
    }

    /**
     * Clear the list of discovered characteristics.
     */
    void clear_characteristics(void)
    {
        DiscoveredCharacteristicNode *c= _characteristics;

        while (c) {
            DiscoveredCharacteristicNode *n = c->next;
            delete c;
            c = n;
        }
    }

    /**
     * Helper to construct an event handler from a member function of this
     * instance.
     */
    template<typename ContextType>
    FunctionPointerWithContext<ContextType> as_cb(
        void (Self::*member)(ContextType context)
    ) {
        return makeFunctionPointer(this, member);
    }

    /**
     * Print the value of a UUID.
     */
    static void print_uuid(const UUID &uuid)
    {
        const uint8_t *uuid_value = uuid.getBaseUUID();

        // UUIDs are in little endian, print them in big endian
        for (size_t i = 0; i < uuid.getLen(); ++i) {
            printf("%02X", uuid_value[(uuid.getLen() - 1) - i]);
        }
    }

    /**
     * Print the value of a characteristic properties.
     */
    static void print_properties(const Properties_t &properties)
    {
        const struct {
            bool (Properties_t::*fn)() const;
            const char* str;
        } prop_to_str[] = {
            { &Properties_t::broadcast, "broadcast" },
            { &Properties_t::read, "read" },
            { &Properties_t::writeWoResp, "writeWoResp" },
            { &Properties_t::write, "write" },
            { &Properties_t::notify, "notify" },
            { &Properties_t::indicate, "indicate" },
            { &Properties_t::authSignedWrite, "authSignedWrite" }
        };

        printf("[");
        for (size_t i = 0; i < (sizeof(prop_to_str) / sizeof(prop_to_str[0])); ++i) {
            if ((properties.*(prop_to_str[i].fn))()) {
                printf(" %s", prop_to_str[i].str);
            }
        }
        printf(" ]");
    }

    GattClient *_client;
    Gap::Handle_t _connection_handle;
    DiscoveredCharacteristicNode *_characteristics;
    DiscoveredCharacteristicNode *_it;
    GattAttribute::Handle_t _descriptor_handle;
    BLE *_ble_interface;
    events::EventQueue *_event_queue;
};


int main() {

    BLE &ble_interface = BLE::Instance();
    events::EventQueue event_queue;
    BLEProcess ble_process(event_queue, ble_interface);
    GattClientProcess gatt_client_process;

    // Register GattClientProcess::init in the ble_process; this function will
    // be called once the ble_interface is initialized.
    ble_process.on_init(
        mbed::callback(&gatt_client_process, &GattClientProcess::init)
    );

    // bind the event queue to the ble interface, initialize the interface
    // and start advertising
    ble_process.start();

    // Process the event queue.
    event_queue.dispatch_forever();

    return 0;
}

Important Information for this Arm website

This site uses cookies to store information on your computer. By continuing to use our site, you consent to our cookies. If you are not happy with the use of these cookies, please review our Cookie Policy to learn how they can be disabled. By disabling cookies, some features of the site will not work.