diff --git a/CODEOWNERS b/CODEOWNERS index 5aca9f1521..64588aedf4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,10 +49,12 @@ /lib/bluetooth/ble_adv/ @nrfconnect/ncs-bm /lib/bluetooth/ble_conn_params/ @nrfconnect/ncs-bm /lib/bluetooth/ble_conn_state/ @nrfconnect/ncs-bm +/lib/bluetooth/ble_db_discovery/ @nrfconnect/ncs-bm /lib/bluetooth/ble_gq/ @nrfconnect/ncs-bm /lib/bluetooth/ble_qwr/ @nrfconnect/ncs-bm /lib/bluetooth/ble_racp/ @nrfconnect/ncs-bm /lib/bluetooth/ble_radio_notification/ @nrfconnect/ncs-bm +/lib/bluetooth/ble_scan/ @nrfconnect/ncs-bm /lib/bluetooth/peer_manager/ @nrfconnect/ncs-bm /lib/bm_buttons/ @nrfconnect/ncs-bm /lib/bm_timer/ @nrfconnect/ncs-bm @@ -100,9 +102,11 @@ /tests/lib/bluetooth/ble_conn_params/ @nrfconnect/ncs-bm /tests/lib/bluetooth/ble_conn_state/ @nrfconnect/ncs-bm /tests/lib/bluetooth/ble_gq/ @nrfconnect/ncs-bm +/tests/lib/bluetooth/ble_db_discovery/ @nrfconnect/ncs-bm /tests/lib/bluetooth/ble_qwr/ @nrfconnect/ncs-bm /tests/lib/bluetooth/ble_racp/ @nrfconnect/ncs-bm /tests/lib/bluetooth/ble_radio_notif/ @nrfconnect/ncs-bm +/tests/lib/bluetooth/ble_scan/ @nrfconnect/ncs-bm /tests/subsys/bluetooth/services/ @nrfconnect/ncs-bm @nrfconnect/ncs-bm-test /tests/subsys/fs/bm_zms/ @nrfconnect/ncs-bm @rghaddab /tests/subsys/kmu/ @nrfconnect/ncs-eris diff --git a/doc/nrf-bm/api/api.rst b/doc/nrf-bm/api/api.rst index 5460930ac0..27e92641dc 100644 --- a/doc/nrf-bm/api/api.rst +++ b/doc/nrf-bm/api/api.rst @@ -48,6 +48,15 @@ Bluetooth LE Connection State library :inner: :members: +.. _api_ble_db_discovery: + +Bluetooth LE Database Discovery library +======================================= + +.. doxygengroup:: ble_db_discovery + :inner: + :members: + .. _api_ble_radio_notif: Bluetooth LE Radio Notification library @@ -57,6 +66,15 @@ Bluetooth LE Radio Notification library :inner: :members: +.. _api_ble_scan: + +Bluetooth LE Scan library +========================= + +.. doxygengroup:: ble_scan + :inner: + :members: + .. _api_bm_buttons: Bare Metal Buttons library diff --git a/doc/nrf-bm/doxygen/nrf-bm.doxyfile b/doc/nrf-bm/doxygen/nrf-bm.doxyfile index 61c5aa6471..b9ec86c8cd 100644 --- a/doc/nrf-bm/doxygen/nrf-bm.doxyfile +++ b/doc/nrf-bm/doxygen/nrf-bm.doxyfile @@ -2402,6 +2402,7 @@ INCLUDE_FILE_PATTERNS = PREDEFINED = __DOXYGEN__ \ CONFIG_BLE_QWR_MAX_ATTR=1 \ + CONFIG_BLE_SCAN_FILTER=1 \ # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/doc/nrf-bm/libraries/bluetooth/ble_db_discovery.rst b/doc/nrf-bm/libraries/bluetooth/ble_db_discovery.rst new file mode 100644 index 0000000000..d06579a5f1 --- /dev/null +++ b/doc/nrf-bm/libraries/bluetooth/ble_db_discovery.rst @@ -0,0 +1,58 @@ +.. _lib_ble_db_discovery: + +Bluetooth: Database Discovery library +############################## + +.. contents:: + :local: + :depth: 2 + +This module contains the APIs and types exposed by the Database Discovery module. These APIs and types can be used by the application to perform discovery of a service and its characteristics at the peer server. This module can also be used to discover the desired services in multiple remote devices. + +Overview +******** + +The library allows registration of callbacks that trigger upon discoveering selected services and characteristics in the peer device. + +.. _lib_ble_db_discovery_configure: + +Configuration +************* + +The library is enabled and configured using the Kconfig system: + +* :kconfig:option:`CONFIG_BLE_DB_DISCOVERY` - Enables the database discovery library. +* :kconfig:option:`CONFIG_BLE_GATT_DB_MAX_CHARS` - Sets the maximum number of database characteristics. +* :kconfig:option:`CONFIG_BLE_DB_DISCOVERY_MAX_SRV`- Sets the maximum number of services that can be discovered. +* :kconfig:option:`SRV_DISC_START_HANDLE`- Sets the start value used durning discovery. + + +Initialization +============== + +The library is initialized by calling the :c:func:`ble_db_discovery_init` function. +See the :c:struct:`ble_db_discovery_config` struct for configuration details. + +Usage +***** + +After initialization, use :c:func:`ble_db_discovery_service_register` to register a callback that triggers when a service is discovered with a specific UUID. +To start discovering from a peer, you must use :c:func:`ble_db_discovery_start` with the connection handle of the peer device. +For example, you can call :c:func:`ble_db_discovery_start` when the ble event :c:enum:`BLE_GAP_EVT_CONNECTED` is triggered and use the connection handle from the event as the second argument in :c:func:`ble_db_discovery_start`. + + +Dependencies +************ + +This library uses the following |BMshort| libraries: + +* SoftDevice - :kconfig:option:`CONFIG_SOFTDEVICE` +* SoftDevice handler - :kconfig:option:`CONFIG_NRF_SDH` + +API documentation +***************** + +| Header file: :file:`include/bm/bluetooth/ble_db_discovery.h` +| Source files: :file:`lib/bluetooth/ble_db_discovery/` + +:ref:`Bluetooth LE Database Discovery library API reference ` diff --git a/doc/nrf-bm/libraries/bluetooth/ble_scan.rst b/doc/nrf-bm/libraries/bluetooth/ble_scan.rst new file mode 100644 index 0000000000..2e58d0f1b8 --- /dev/null +++ b/doc/nrf-bm/libraries/bluetooth/ble_scan.rst @@ -0,0 +1,264 @@ +.. _lib_ble_scan: + +Bluetooth: Scan +############### + +.. contents:: + :local: + :depth: 2 + +The Bluetooth® Low Energy Scan library handles scanning for advertising Bluetooth LE devices. +You can use it to find an advertising device and establish a connection with it. +You can narrow down the scan to a device of a specific type using scan filters or the whitelist. + +Overview +******** + +You can configure this library in one of the following modes: + +* Simple mode without using filters or the whitelist. +* Advanced mode that allows you to use advanced filters and the whitelist. + +The library can automatically establish a connection on a filter match or when identifying a whitelisted device. + +The library registers as a Bluetooth LE event observer using the :c:macro:`NRF_SDH_BLE_OBSERVER` macro and handles the relevant Bluetooth LE events from the SoftDevice. + +.. note:: + + This library require a SoftDevice with central support. + +Configuration +************* + +To enable the library, set the :kconfig:option:`CONFIG_BLE_SCAN` Kconfig option. + +The library provides the following Kconfig configuration options: + +* :kconfig:option:`CONFIG_BLE_SCAN_BUFFER_SIZE` - Maximum size of an advertising event. +* :kconfig:option:`CONFIG_BLE_SCAN_NAME_MAX_LEN` - Maximum size of the name to search for in the advertisement report. +* :kconfig:option:`CONFIG_BLE_SCAN_SHORT_NAME_MAX_LEN` - Maximum size of the short name to search for in the advertisement report. +* :kconfig:option:`CONFIG_BLE_SCAN_FILTER` - Enabling filters for the scan library. +* :kconfig:option:`CONFIG_BLE_SCAN_NAME_COUNT` - Maximum number of name filters. +* :kconfig:option:`CONFIG_BLE_SCAN_APPEARANCE_COUNT` - Maximum number of appearance filters. +* :kconfig:option:`CONFIG_BLE_SCAN_ADDRESS_COUNT` - Maximum number of address filters. +* :kconfig:option:`CONFIG_BLE_SCAN_SHORT_NAME_COUNT` - Maximum number of short name filters. +* :kconfig:option:`CONFIG_BLE_SCAN_UUID_COUNT` - Maximum number of filters for UUIDs. +* :kconfig:option:`CONFIG_BLE_SCAN_INTERVAL` - Determines the scan interval in units of 0.625 ms. +* :kconfig:option:`CONFIG_BLE_SCAN_DURATION` - Duration of a scanning session in units of 10 ms, if set to 0, the scanning continues until it is explicitly disabled. +* :kconfig:option:`CONFIG_BLE_SCAN_WINDOW` - Determines the scanning window in units of 0.625 ms. +* :kconfig:option:`CONFIG_BLE_SCAN_SLAVE_LATENCY` - Determines the slave latency in counts of connection events. +* :kconfig:option:`CONFIG_BLE_SCAN_MIN_CONNECTION_INTERVAL` - Determines the minimum connection interval in units of 1.25 ms. +* :kconfig:option:`CONFIG_BLE_SCAN_MAX_CONNECTION_INTERVAL` - Determines the maximum connection interval in units of 1.25 ms. +* :kconfig:option:`CONFIG_BLE_SCAN_SUPERVISION_TIMEOUT` - Determines the supervision time-out in units of 10 ms. + +Initialization +============== + +The library is initialized by calling the :c:func:`ble_scan_init` function. +The application can provide an event handler to receive events to inform about a filter or whitelist match, or about a connection error during the automatic connection. + +Simple initialization +===================== + +You can use the simple initialization with the default scanning and connection parameters when you want the library to work in the simple mode. + + +.. code-block:: c + + BLE_SCAN_DEF(ble_scan); + + void scan_event_handler_func(struct ble_scan_evt const *scan_evt); + + uint32_t nrf_err; + struct ble_scan_config scan_cfg = { + .scan_params = BLE_SCAN_SCAN_PARAMS_DEFAULT, + .conn_params = BLE_SCAN_CONN_PARAMS_DEFAULT, + .evt_handler = scan_event_handler_func, + }; + + nrf_err = ble_scan_init(&ble_scan, &scan_cfg); + if (nrf_err) { + LOG_ERR("Failed to initialie scan library, nrf_error %#x", nrf_err); + } + +Advanced initialization +======================= + +The advanced initialization provides a larger configuration set for the library. +It is required when using scan filters or the whitelist. + +.. code-block:: c + + BLE_SCAN_DEF(ble_scan); + + void scan_event_handler_func(struct ble_scan_evt const *scan_evt); + + uint32_t nrf_err; + struct ble_scan_config scan_cfg = { + .scan_params = { + .active = 0x01, + .interval = NRF_BLE_SCAN_INTERVAL, + .window = NRF_BLE_SCAN_WINDOW, + .filter_policy = BLE_GAP_SCAN_FP_WHITELIST, + .timeout = SCAN_DURATION_WHITELIST, + .scan_phys = BLE_GAP_PHY_1MBPS, + .extended = true, + }, + .conn_params = BLE_SCAN_CONN_PARAMS_DEFAULT, + .connect_if_match = true, + .conn_cfg_tag = CONFIG_NRF_SDH_BLE_CONN_TAG, + .evt_handler = scan_event_handler_func, + }; + + nrf_err = ble_scan_init(&ble_scan, &scan_cfg); + if (nrf_err) { + LOG_ERR("Failed to initialize scan library, nrf_error %#x", nrf_err); + } + +.. note:: + + When setting connection-specific configurations with the :c:func:`sd_ble_cfg_set` function, you must create a tag for each configuration. + If your application uses the library with automatic connection, this tag must be provided when calling the :c:func:`sd_ble_gap_scan_start` or the :c:func:`sd_ble_gap_connect` function. + +Usage +***** + +The application can start scanning by calling the :c:func:`ble_scan_start` function. + +The scan parameters can be updated by calling the :c:func:`ble_scan_params_set` function. + +To stop scanning, call the :c:func:`ble_scan_stop` function. + +The library resumes scanning after receiving advertising reports. +Scanning stops if the library establishes a connection automatically, or if the application calls the :c:func:`ble_scan_stop` or the :c:func:`sd_ble_gap_connect` function. + +.. note:: + + When you use the :c:func:`ble_scan_params_set` function during the ongoing scan, scanning is stopped. + To resume scanning, use the :c:func:`ble_scan_start` function. + +Whitelist +========= + +The whitelist stores information about all the device connections and bonding. +If you enable the whitelist, the application receives advertising packets only from the devices that are on the whitelist. +An advertising package from a whitelisted device generates the :c:macro:`NRF_BLE_SCAN_EVT_WHITELIST_ADV_REPORT` event. + +.. note:: + + When using the whitelist, filters are inactive. + +.. caution:: + + If you use the whitelist, you must pass the event handler during the library initialization. + The initial scanning with whitelist generates a :c:macro:`NRF_BLE_SCAN_EVT_WHITELIST_REQUEST` event. + The application must react to this event by either setting up the whitelist or switching off the whitelist scan. + Otherwise, an error is reported when the scan starts. + +Filters +======= + +The library can set scanning filters of different type and mode. +When a filter is matched, it generates a :c:macro:`NRF_BLE_SCAN_EVT_FILTER_MATCH` event to the main application. +If the filter matching is enabled and no filter is matched, a :c:macro:`NRF_BLE_SCAN_EVT_NOT_FOUND` event is generated. + +The available filter types are: + +* **Name** - Filter set to the target name. + The maximum length of the name corresponds to :kconfig:option:`CONFIG_BLE_SCAN_NAME_MAX_LEN`. + The maximum number of filters of this type corresponds to :kconfig:option:`CONFIG_BLE_SCAN_NAME_COUNT`. +* **Short name** - Filter set to the short target name. + The maximum length of the name corresponds to :kconfig:option:`CONFIG_BLE_SCAN_SHORT_NAME_MAX_LEN`. + The maximum number of filters of this type corresponds to :kconfig:option:`CONFIG_BLE_SCAN_SHORT_NAME_COUNT`. +* **Address** - Filter set to the target address. + The maximum number of filters of this type corresponds to :kconfig:option:`CONFIG_BLE_SCAN_ADDRESS_COUNT`. +* **UUID** - Filter set to the target UUID. + The maximum number of filters of this type corresponds to :kconfig:option:`CONFIG_BLE_SCAN_UUID_COUNT`. +* **Appearance** - Filter set to the target appearance. + The maximum number of filters of this type corresponds to :kconfig:option:`CONFIG_BLE_SCAN_APPEARANCE_COUNT`. + +The following two filter modes are available: + +* **Normal** - Only one of the filters set, regardless of the type, must be matched to generate an event. +* **Multifilter** - At least one filter from each filter type you set must be matched to generate an event. + For UUID filters, all specified UUIDs must match in this mode. + Enable multifilter by setting the :c:macro:`match_all` argument to true when calling the :c:func:`ble_scan_filters_enable` function. + +Multifilter example: + +Several filters are set for name, address, UUID, and appearance. +To generate the :c:macro:`NRF_BLE_SCAN_EVT_FILTER_MATCH` event, the following types must match: + +* One of the address filters. +* One of the name filters. +* One of the appearance filters. +* All UUID filters. + +Otherwise, the :c:macro:`NRF_BLE_SCAN_EVT_NOT_FOUND` event is generated. + +You can enable filters by calling the :c:func:`ble_scan_filters_enable` function after initialization. +You can activate filters for one filter type, or for a combination of several filter types. + +.. code-block:: c + + BLE_SCAN_DEF(ble_scan); + + uint8_t addr[BLE_GAP_ADDR_LEN] = {0xa, 0xd, 0xd, 0x4, 0xe, 0x5}; + char *device_name = "my_device"; + + /* See above code snippet for initialization */ + + /* Enable filter for name and address in normal mode */ + nrf_err = ble_scan_filters_enable(&ble_scan, BLE_SCAN_NAME_FILTER | BLE_SCAN_ADDR_FILTER, false); + if (nrf_err) { + LOG_ERR("Failed to enable scan filters, nrf_error %#x", nrf_err); + } + + /* Add address to scan filter */ + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_ADDR_FILTER, addr); + if (nrf_err) { + LOG_ERR("Failed to add address scan filter, nrf_error %#x", nrf_err); + } + + /* Add name to scan filter */ + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_NAME_FILTER, device_name); + if (nrf_err) { + LOG_ERR("Failed to add name scan filter, nrf_error %#x", nrf_err); + } + + /* Start scanning */ + nrf_err = ble_scan_start(&ble_scan); + if (nrf_err) { + LOG_ERR("Failed to start scan, nrf_error %#x", nrf_err); + } + + /* Start scanning */ + ble_scan_stop(&ble_scan); + + /* Disable filters */ + nrf_err = ble_scan_filters_disable(&ble_scan); + if (nrf_err) { + LOG_ERR("Failed to disable scan filters, nrf_error %#x", nrf_err); + } + + /* Remove all scan filters */ + nrf_err = ble_scan_all_filter_remove(&ble_scan); + if (nrf_err) { + LOG_ERR("Failed to remove scan filters, nrf_error %#x", nrf_err); + } + +Dependencies +************ + +This library uses the following |BMshort| libraries: + +* SoftDevice - :kconfig:option:`CONFIG_SOFTDEVICE` +* SoftDevice handler - :kconfig:option:`CONFIG_NRF_SDH` + +API documentation +***************** + +| Header file: :file:`include/bm/bluetooth/ble_scan.h` +| Source files: :file:`lib/bluetooth/ble_scan/` + +:ref:`Bluetooth LE Scan library API reference ` diff --git a/include/bm/bluetooth/ble_db_discovery.h b/include/bm/bluetooth/ble_db_discovery.h new file mode 100644 index 0000000000..83b23f3f0f --- /dev/null +++ b/include/bm/bluetooth/ble_db_discovery.h @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2013 - 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/** @file + * + * @defgroup ble_db_discovery BLE Nordic database discovery library + * @{ + * @brief Library for discovery of a service and its characteristics at the peer server. + */ + +#include +#include +#include +#include +#include + +#ifndef BLE_DB_DISCOVERY_H__ +#define BLE_DB_DISCOVERY_H__ + +/** + * @brief Macro for defining a ble_db_discovery instance. + * + * @param _name Name of the instance. + */ +#define BLE_DB_DISCOVERY_DEF(_name) \ + static struct ble_db_discovery _name = {.discovery_in_progress = 0, \ + .conn_handle = BLE_CONN_HANDLE_INVALID}; \ + NRF_SDH_BLE_OBSERVER(_name##_obs, ble_db_discovery_on_ble_evt, &_name, HIGH); + +/** + * @brief BLE database discovery event type. + */ +enum ble_db_discovery_evt_type { + /** + * @brief Event indicating that the discovery of one service is complete. + */ + BLE_DB_DISCOVERY_COMPLETE, + /** + * @brief Event indicating that the service was not found at the peer. + */ + BLE_DB_DISCOVERY_SRV_NOT_FOUND, + /** + * @brief Event indicating that the DB discovery instance is available. + */ + BLE_DB_DISCOVERY_AVAILABLE, + /** + * @brief Event indicating that an internal error has occurred in the DB Discovery module. + * + * This could typically be because of the SoftDevice API returning an error code during the + * DB discover. + */ + BLE_DB_DISCOVERY_ERROR, +}; + +/** + * @brief BLE database discovery event. + */ +struct ble_db_discovery_evt { + /** + * @brief Type of event. + */ + enum ble_db_discovery_evt_type evt_type; + /** + * @brief Handle of the connection for which this event has occurred. + */ + uint16_t conn_handle; + union { + /** + * @brief Structure containing the information about the GATT Database at the + * server. + * + * This will be filled when the event type is @ref BLE_DB_DISCOVERY_COMPLETE. The + * UUID field of this will be filled when the event type is @ref + * BLE_DB_DISCOVERY_SRV_NOT_FOUND. + */ + struct ble_gatt_db_srv discovered_db; + /** + * @brief nRF Error code indicating the type of error which occurred in the DB + * Discovery module. + * + * This will be filled when the event type is @ref BLE_DB_DISCOVERY_ERROR. + */ + struct { + /** + * @brief nRF Error code indicating the type of error which occurred in the + * DB Discovery module. + */ + uint32_t reason; + } error; + } params; +}; + +/* Forward declaration. */ +struct ble_db_discovery; + +/** + * @brief DB discovery event handler type. + */ +typedef void (*ble_db_discovery_evt_handler)(struct ble_db_discovery *db_discovery, + struct ble_db_discovery_evt *evt); + +/** + * @brief BLE database discovery configuration. + */ +struct ble_db_discovery_config { + /** + * @brief Event handler to be called by the DB Discovery module. + */ + ble_db_discovery_evt_handler evt_handler; + /** + * @brief Pointer to BLE GATT Queue instance. + */ + const struct ble_gq *gatt_queue; +}; + +/** + * @brief BLE database discovery user event. + */ +struct ble_db_discovery_user_evt { + /** + * @brief Pending event. + */ + struct ble_db_discovery_evt evt; + /** + * @brief Event handler which should be called to raise this event. + */ + ble_db_discovery_evt_handler evt_handler; +}; + +/** + * @brief BLE database discovery. + */ +struct ble_db_discovery { + /** + * @brief Information related to the current service being discovered. + * + * This is intended for internal use during service discovery. + */ + struct ble_gatt_db_srv services[CONFIG_BLE_DB_DISCOVERY_MAX_SRV]; + /** + * @brief UUID of registered handlers + */ + ble_uuid_t registered_uuids[CONFIG_BLE_DB_DISCOVERY_MAX_SRV]; + /** + * @brief Instance event handler. + */ + ble_db_discovery_evt_handler evt_handler; + /** + * @brief BLE GATT Queue instance. + */ + const struct ble_gq *gatt_queue; + /** + * @brief The number of UUIDs registered with the DB Discovery module. + */ + uint32_t num_registered_uuids; + /** + * @brief Number of services at the peer's GATT database. + */ + uint8_t srv_count; + /** + * @brief Index of the current characteristic being discovered. + * + * This is intended for internal use during service discovery. + */ + uint8_t curr_char_ind; + /** + * @brief Index of the current service being discovered. + * + * This is intended for internal use during service discovery. + */ + uint8_t curr_srv_ind; + /** + * @brief Number of service discoveries made, both successful and unsuccessful. + */ + uint8_t discoveries_count; + /** + * @brief Variable to indicate whether there is a service discovery in progress. + */ + bool discovery_in_progress; + /** + * @brief Connection handle on which the discovery is started. + */ + uint16_t conn_handle; + /** + * @brief The index to the pending user event array, pointing to the last added pending user + * event. + */ + uint32_t pending_usr_evt_index; + /** + * @brief Whenever a discovery related event is to be raised to a user module, it is stored + * in this array first. + * + * When all expected services have been discovered, all pending events are sent to the + * corresponding user modules. + */ + struct ble_db_discovery_user_evt pending_usr_evts[CONFIG_BLE_DB_DISCOVERY_MAX_SRV]; +}; + +/** + * @brief Initialize the DB Discovery module. + * + * @param[in] db_discovery BLE DB discovery instance. + * @param[in] db_config DB discovery initialization structure. + * + * @retval NRF_SUCCESS On successful initialization. + * @retval NRF_ERROR_NULL If the initialization structure was NULL or + * the structure content is empty. + */ +uint32_t ble_db_discovery_init(struct ble_db_discovery *db_discovery, + struct ble_db_discovery_config *db_config); + +/** + * @brief Start the discovery of the GATT database at the server. + * + * @param[out] db_discovery BLE DB Discovery instance. + * @param[in] conn_handle The handle of the connection for which the discovery should be + * started. + * + * @retval NRF_SUCCESS Operation success. + * @retval NRF_ERROR_NULL When a NULL pointer is passed as input. + * @retval NRF_ERROR_INVALID_STATE If this function is called without calling the + * @ref ble_db_discovery_init, or without calling + * @ref ble_db_discovery_service_register. + * @retval NRF_ERROR_BUSY If a discovery is already in progress using @p db_discovery. + * Use a different @ref struct ble_db_discovery structure, + * or wait for a DB Discovery event before retrying. + * @return This API propagates the error code returned by functions: + * @ref nrf_ble_gq_conn_handle_register and @ref nrf_ble_gq_item_add. + */ +uint32_t ble_db_discovery_start(struct ble_db_discovery *db_discovery, uint16_t conn_handle); + +/** + * @brief Register service with the DB Discovery module. + * + * @details The application can use this function to inform which service it is interested in + * discovering at the server. + * + * @param[in] db_discovery BLE DB discovery instance. + * @param[in] uuid UUID of the service to be discovered at the server. + * + * @note The total number of services that can be discovered by this module is @ref + * CONFIG_BLE_DB_DISCOVERY_MAX_SRV. This effectively means that the maximum number of + * registrations possible is equal to the @ref CONFIG_BLE_DB_DISCOVERY_MAX_SRV. + * + * @retval NRF_SUCCESS Operation success. + * @retval NRF_ERROR_NULL When a NULL pointer is passed as input. + * @retval NRF_ERROR_INVALID_STATE If this function is called without calling the + * @ref ble_db_discovery_init. + * @retval NRF_ERROR_NO_MEM The maximum number of registrations allowed by this module + * has been reached. + */ +uint32_t ble_db_discovery_service_register(struct ble_db_discovery *db_discovery, + const ble_uuid_t *const uuid); + +/** + * @brief Application's BLE Stack event handler. + * + * @param[in] ble_evt BLE event received. + * @param[in,out] context BLE DB discovery instance. + */ +void ble_db_discovery_on_ble_evt(ble_evt_t const *ble_evt, void *context); + +#endif /* BLE_DB_DISCOVERY_H__ */ + +/** @} */ diff --git a/include/bm/bluetooth/peer_manager/ble_gatt_db.h b/include/bm/bluetooth/ble_gatt_db.h similarity index 100% rename from include/bm/bluetooth/peer_manager/ble_gatt_db.h rename to include/bm/bluetooth/ble_gatt_db.h diff --git a/include/bm/bluetooth/ble_scan.h b/include/bm/bluetooth/ble_scan.h new file mode 100644 index 0000000000..bb12fc6fe8 --- /dev/null +++ b/include/bm/bluetooth/ble_scan.h @@ -0,0 +1,570 @@ +/* + * Copyright (c) 2018 - 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/** + * @file + * + * @defgroup ble_scan Scan Library + * @{ + * @brief Library for handling the BLE scanning. + * + * @details The Scan Library handles the BLE scanning for your application. + * The library offers several criteria for filtering the devices available for connection, + * and it can also work in the simple mode without using the filtering. + * If an event handler is provided, your main application can react to a filter match or to + * the need of setting the whitelist. The library can also be configured to automatically + * connect after it matches a filter or a device from the whitelist. + * + * @note The Scan library also supports applications with a multicentral link. + */ + +#ifndef BLE_SCAN_H__ +#define BLE_SCAN_H__ + +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Macro for defining a ble_scan instance. + * + * @param _name Name of the instance. + * @hideinitializer + */ +#define BLE_SCAN_DEF(_name) \ + static struct ble_scan _name; \ + extern void ble_scan_on_ble_evt(const ble_evt_t *ble_evt, void *ctx); \ + NRF_SDH_BLE_OBSERVER(_name##_obs, ble_scan_on_ble_evt, &_name, HIGH) + +/** + * @brief Scan events. + * + * @details These events are propagated to the main application if a handler is provided during the + * initialization of the Scan library. + * + * @note @ref BLE_SCAN_EVT_WHITELIST_REQUEST cannot be ignored if whitelist is used. + */ +enum ble_scan_evt_type { + /** + * @brief A filter is matched or all filters are matched in the multifilter mode. + */ + BLE_SCAN_EVT_FILTER_MATCH, + /** + * @brief Request the whitelist from the main application. + * + * For whitelist scanning to work, the whitelist must be set when this event occurs. + */ + BLE_SCAN_EVT_WHITELIST_REQUEST, + /** + * @brief Send notification to the main application when a device from the whitelist is + * found. + */ + BLE_SCAN_EVT_WHITELIST_ADV_REPORT, + /** + * @brief The filter was not matched for the scan data. + */ + BLE_SCAN_EVT_NOT_FOUND, + /** + * @brief Scan timeout. + */ + BLE_SCAN_EVT_SCAN_TIMEOUT, + /** + * @brief Error occurred when establishing the connection. + * + * In this event, an error is passed from the function call @ref sd_ble_gap_connect. + */ + BLE_SCAN_EVT_CONNECTING_ERROR, + /** + * @brief Connected to device. + */ + BLE_SCAN_EVT_CONNECTED, + /** + * @brief Error. + */ + BLE_SCAN_EVT_ERROR, +}; + +/** + * @defgroup ble_scan_filter_type Filter types + * @{ + */ + +/** Filters the device name. */ + #define BLE_SCAN_NAME_FILTER (0x01) +/** Filters the device address. */ +#define BLE_SCAN_ADDR_FILTER (0x02) +/** Filters the UUID. */ +#define BLE_SCAN_UUID_FILTER (0x04) +/** Filters the appearance. */ +#define BLE_SCAN_APPEARANCE_FILTER (0x08) +/** Filters the device short name. */ +#define BLE_SCAN_SHORT_NAME_FILTER (0x10) +/** @} */ + +/** + * @brief Scan short name. + */ +struct ble_scan_short_name { + /** + * @brief Pointer to the short name. + */ + char const *short_name; + /** + * @brief Minimum length of the short name. + */ + uint8_t short_name_min_len; +}; + +/** + * @brief Filter status. + */ +struct ble_scan_filter_match { + /** Set to 1 if name filter is matched. */ + uint8_t name_filter_match: 1; + /** Set to 1 if address filter is matched. */ + uint8_t address_filter_match: 1; + /** Set to 1 if uuid filter is matched. */ + uint8_t uuid_filter_match: 1; + /** Set to 1 if appearance filter is matched. */ + uint8_t appearance_filter_match: 1; + /** Set to 1 if short name filter is matched. */ + uint8_t short_name_filter_match: 1; +}; + +/** + * @brief Scan library event. + * + * @details This is used to send library event data to the main application when an event occurs. + */ +struct ble_scan_evt { + /** Type of event. */ + enum ble_scan_evt_type evt_type; + /** GAP scanning parameters. These parameter are needed to establish connection. */ + ble_gap_scan_params_t const *scan_params; + union { + /** Scan filter match. */ + struct { + /** Event structure for @ref BLE_GAP_EVT_ADV_REPORT. This data + * allows the main application to establish connection. + */ + ble_gap_evt_adv_report_t const *adv_report; + /** Matching filters. Information about matched filters. */ + struct ble_scan_filter_match filter_match; + } filter_match; + /** Timeout event parameters. */ + ble_gap_evt_timeout_t timeout; + /** Advertising report event parameters for whitelist. */ + struct { + /** Advertising report */ + ble_gap_evt_adv_report_t const *report; + } whitelist_adv_report; + /** Advertising report event parameters when filter is not found. */ + struct { + /** Advertising report */ + ble_gap_evt_adv_report_t const *report; + } not_found; + /** Connected event parameters. */ + struct { + /** Connected event parameters. */ + ble_gap_evt_connected_t const *connected; + /** Connection handle of the device on which the event occurred. */ + uint16_t conn_handle; + } connected; + /** Error event when connecting. Propagates the error code returned by + * @ref sd_ble_gap_scan_start. + */ + struct { + /** Indicates success or failure of an API procedure. In case of + * failure, a comprehensive error code indicating the cause or reason + * for failure is provided. + */ + int reason; + } connecting_err; + /** Error event. */ + struct { + /** Error reason */ + uint32_t reason; + } error; + } params; +}; + +/** + * @brief BLE Scan event handler type. + */ +typedef void (*ble_scan_evt_handler_t)(struct ble_scan_evt const *scan_evt); + +#if defined(CONFIG_BLE_SCAN_FILTER) + +#if (CONFIG_BLE_SCAN_NAME_COUNT > 0) +/** Scan name filter */ +struct ble_scan_name_filter { + /** Names that the main application will scan for, + * and that will be advertised by the peripherals. + */ + char target_name[CONFIG_BLE_SCAN_NAME_COUNT][CONFIG_BLE_SCAN_NAME_MAX_LEN]; + /** Number of target names. */ + uint8_t name_cnt; + /** Flag to inform about enabling or disabling this filter. */ + bool name_filter_enabled; +}; +#endif + +#if (CONFIG_BLE_SCAN_SHORT_NAME_COUNT > 0) +/** Scan short name filter */ +struct ble_scan_short_name_filter { + struct { + /** Short names that the main application will scan for, and that will be + * advertised by the peripherals. + */ + char short_target_name[CONFIG_BLE_SCAN_SHORT_NAME_MAX_LEN]; + /** Minimum length of the short name. */ + uint8_t short_name_min_len; + } short_name[CONFIG_BLE_SCAN_SHORT_NAME_COUNT]; + /** Number of short target names. */ + uint8_t name_cnt; + /** Flag to inform about enabling or disabling this filter. */ + bool short_name_filter_enabled; +}; +#endif + +#if (CONFIG_BLE_SCAN_ADDRESS_COUNT > 0) +/** Scan address filter */ +struct ble_scan_addr_filter { + /** Addresses in the same format as the format used by the SoftDevice that the + * main application will scan for, and that will be advertised by the peripherals. + */ + ble_gap_addr_t target_addr[CONFIG_BLE_SCAN_ADDRESS_COUNT]; + /** Number of target addresses. */ + uint8_t addr_cnt; + /** Flag to inform about enabling or disabling this filter. */ + bool addr_filter_enabled; +}; +#endif + +#if (CONFIG_BLE_SCAN_UUID_COUNT > 0) +/** Scan UUID filter */ +struct ble_scan_uuid_filter { + /** UUIDs that the main application will scan for, and that will be advertised by + * the peripherals. + */ + ble_uuid_t uuid[CONFIG_BLE_SCAN_UUID_COUNT]; + /** Number of target UUIDs in list. */ + uint8_t uuid_cnt; + /** Flag to inform about enabling or disabling this filter. */ + bool uuid_filter_enabled; +}; +#endif + +#if (CONFIG_BLE_SCAN_APPEARANCE_COUNT > 0) +/** Scan appearance filter */ +struct ble_scan_appearance_filter { + /** Apperances that the main application will scan for, and that will be advertised by the + * peripherals. + */ + uint16_t appearance[CONFIG_BLE_SCAN_APPEARANCE_COUNT]; + /** Number of appearances in list. */ + uint8_t appearance_cnt; + /** Flag to inform about enabling or disabling this filter.*/ + bool appearance_filter_enabled; +}; +#endif + +/** + * @brief Filter data. + * + * @details This contains all filter data and the information about enabling and disabling + * any type of filters. Flag all_filter_mode informs about the filter mode. If this flag is set, + * then all types of enabled filters must be matched for the library to send a notification to the + * main application. Otherwise, it is enough to match one of the filters to send a notification. + */ +struct ble_scan_filters { +#if (CONFIG_BLE_SCAN_NAME_COUNT > 0) + /** Name filter data. */ + struct ble_scan_name_filter name_filter; +#endif +#if (CONFIG_BLE_SCAN_SHORT_NAME_COUNT > 0) + /** Short name filter data. */ + struct ble_scan_short_name_filter short_name_filter; +#endif +#if (CONFIG_BLE_SCAN_ADDRESS_COUNT > 0) + /** Address filter data. */ + struct ble_scan_addr_filter addr_filter; +#endif +#if (CONFIG_BLE_SCAN_UUID_COUNT > 0) + /** UUID filter data. */ + struct ble_scan_uuid_filter uuid_filter; +#endif +#if (CONFIG_BLE_SCAN_APPEARANCE_COUNT > 0) + /** Appearance filter data. */ + struct ble_scan_appearance_filter appearance_filter; +#endif + /** Filter mode. If true, all set filters must be matched to generate an event. */ + bool all_filters_mode; +}; + +#endif /* CONFIG_BLE_SCAN_FILTER */ + +#define BLE_SCAN_SCAN_PARAMS_DEFAULT \ +{ \ + .active = 1, \ + .interval = CONFIG_BLE_SCAN_INTERVAL, \ + .window = CONFIG_BLE_SCAN_WINDOW, \ + .timeout = CONFIG_BLE_SCAN_DURATION, \ + .filter_policy = BLE_GAP_SCAN_FP_ACCEPT_ALL, \ + .scan_phys = BLE_GAP_PHY_1MBPS, \ +} + +#define BLE_SCAN_CONN_PARAMS_DEFAULT \ +{ \ + .conn_sup_timeout = BLE_GAP_CP_CONN_SUP_TIMEOUT_MIN, \ + .min_conn_interval = CONFIG_BLE_SCAN_MIN_CONNECTION_INTERVAL, \ + .max_conn_interval = CONFIG_BLE_SCAN_MAX_CONNECTION_INTERVAL, \ + .slave_latency = (uint16_t)CONFIG_BLE_SCAN_SLAVE_LATENCY, \ +} + +/** + * @brief Scan instance configuration. + */ +struct ble_scan_config { + /* BLE GAP scan parameters required to initialize the module. */ + ble_gap_scan_params_t scan_params; + /* If set to true, the module automatically connects after a filter + * match or successful identification of a device from the whitelist. + */ + bool connect_if_match; + /* Connection parameters. */ + ble_gap_conn_params_t conn_params; + /* Variable to keep track of what connection settings will be used + * if a filer match or a whitelist match results in a connection. + */ + uint8_t conn_cfg_tag; + /** Handler for the scanning events. */ + ble_scan_evt_handler_t evt_handler; +}; + +/** + * @brief Scan library instance with options for the different scanning modes. + * + * @details This structure stores all library settings. It is used to enable or disable scanning + * modes and to configure filters. + */ +struct ble_scan { +#if defined(CONFIG_BLE_SCAN_FILTER) + /** Filter data. */ + struct ble_scan_filters scan_filters; +#endif + /** If set to true, the library automatically connects after a filter + * match or successful identification of a device from the whitelist. + */ + bool connect_if_match; + /** Connection parameters. */ + ble_gap_conn_params_t conn_params; + /** Variable to keep track of what connection settings will be used + * if a filer match or a whitelist match results in a connection. + */ + uint8_t conn_cfg_tag; + /** GAP scanning parameters. */ + ble_gap_scan_params_t scan_params; + /** Handler for the scanning events. */ + ble_scan_evt_handler_t evt_handler; + /** Buffer where advertising reports will be stored by the SoftDevice. */ + uint8_t scan_buffer_data[CONFIG_BLE_SCAN_BUFFER_SIZE]; + /** Structure-stored pointer to the buffer where advertising + * reports will be stored by the SoftDevice. + */ + ble_data_t scan_buffer; +}; + +/** + * @brief Check if the whitelist is used. + * + * @param[in] scan_ctx Scan library instance. + * + * @return true if whitelist is used. + */ +bool is_whitelist_used(struct ble_scan const *const scan_ctx); + +/** + * @brief Initialize the Scan library. + * + * @param[out] scan Scan library instance. This structure must be supplied by the application. + * It is initialized by this function and is later used to identify this + * particular library instance. + * + * @param[in] config Configuration parameters. + * + * @retval NRF_SUCCESS If initialization was successful. + * @retval NRF_ERROR_NULL When a NULL pointer is passed as input. + */ +int ble_scan_init(struct ble_scan *scan, struct ble_scan_config *config); + +/** + * @brief Start scanning. + * + * @details This function starts the scanning according to the configuration set during the + * initialization. + * + * @param[in] scan_ctx Scan library instance. + * + * @retval NRF_SUCCESS If scanning started. Otherwise, an error code is returned. + * @retval NRF_ERROR_NULL If NULL pointer is passed as input. + * + * @return This API propagates the error code returned by the + * SoftDevice API @ref sd_ble_gap_scan_start. + */ +int ble_scan_start(struct ble_scan const *const scan_ctx); + +/** + * @brief Stop scanning. + * + * @param[in] scan_ctx Scan library instance. + */ +void ble_scan_stop(struct ble_scan const *const scan_ctx); + +#if defined(CONFIG_BLE_SCAN_FILTER) + +/** + * @brief Enable filtering. + * + * @details The filters can be combined with each other. For example, you can enable one filter or + * several filters. For example, (BLE_SCAN_NAME_FILTER | BLE_SCAN_UUID_FILTER) + * enables UUID and name filters. + * + * @param[in] mode Filter mode: @c ble_scan_filter_type. + * @param[in] match_all If this flag is set, all types of enabled filters must be + * matched before generating @ref BLE_SCAN_EVT_FILTER_MATCH to the main + * application. Otherwise, it is enough to match one filter to trigger the + * filter match event. + * @param[in] scan_ctx Scan library instance. + * + * @retval NRF_SUCCESS If the filters are enabled successfully. + * @retval NRF_ERROR_INVALID_PARAM If the filter mode is invalid. + * @retval NRF_ERROR_NULL If a NULL pointer is passed as input. + */ +int ble_scan_filters_enable(struct ble_scan *const scan_ctx, uint8_t mode, bool match_all); + +/** + * @brief Disable filtering. + * + * @details Disable all filters. + * Even if the automatic connection establishing is enabled, + * the connection will not be established with the first device found after + * this function is called. + * + * @param[in] scan_ctx Scan library instance. + * + * @retval NRF_SUCCESS If filters are disabled successfully. + * @retval NRF_ERROR_NULL If a NULL pointer is passed as input. + */ +int ble_scan_filters_disable(struct ble_scan *const scan_ctx); + +/** + * @brief Get filter status. + * + * @details This function returns the filter setting and whether it is enabled or disabled. + + * @param[out] status Filter status. + * @param[in] scan_ctx Scan library instance. + * + * @retval NRF_SUCCESS If filter status is returned. + * @retval NRF_ERROR_NULL If a NULL pointer is passed as input. + */ +int ble_scan_filter_get(struct ble_scan *const scan_ctx, struct ble_scan_filters *status); + +/** + * @brief Add scan filter. + * + * @details This function adds a new filter by type @ref ble_scan_filter_type_t. + * The filter will be added if the number of filters of a given type does not exceed @ref + * CONFIG_BLE_SCAN_UUID_COUNT, @ref CONFIG_BLE_SCAN_NAME_COUNT, @ref + * CONFIG_BLE_SCAN_ADDRESS_COUNT, or @ref CONFIG_BLE_SCAN_APPEARANCE_COUNT, depending on + * the filter type, and if the same filter has not already been set. + * + * @param[in,out] scan_ctx Scan library instance. + * @param[in] type Filter type. + * @param[in] data The filter data to add. + * + * @retval NRF_SUCCESS If the filter is added successfully. + * @retval NRF_ERROR_NULL If a NULL pointer is passed as input. + * @retval NRF_ERROR_DATA_SIZE If the name filter length is too long. Maximum name filter + * length corresponds to @ref NRF_BLE_SCAN_NAME_MAX_LEN. + * @retval NRF_ERROR_NO_MEMORY If the number of available filters is exceeded. + * @retval NRF_ERROR_INVALID_PARAM If the filter type is incorrect. Available filter types: + * @ref ble_scan_filter_type_t. + * @retval BLE_ERROR_GAP_INVALID_BLE_ADDR If the BLE address type is invalid. + */ +int ble_scan_filter_set(struct ble_scan *const scan_ctx, uint8_t type, + void const *data); + +/** + * @brief Remove all filters. + * + * @details The function removes all previously set filters. + * + * @note After using this function the filters are still enabled. + * + * @param[in,out] scan_ctx Scan library instance. + * + * @retval NRF_SUCCESS If all filters are removed successfully. + */ +int ble_scan_all_filter_remove(struct ble_scan *const scan_ctx); + +#endif /* CONFIG_BLE_SCAN_FILTER */ + +/** + * @brief Set the scanning parameters. + * + * @details Use this function to change scanning parameters. During the parameter change + * the scan is stopped. To resume scanning, use @ref ble_scan_start. + * + * @param[in,out] scan_ctx Scan library instance. + * @param[in] scan_params GAP scanning parameters. Can be initialized as NULL. + * + * @retval NRF_SUCCESS If parameters are changed successfully. + * @retval NRF_ERROR_NULL If a NULL pointer is passed as input. + */ +int ble_scan_params_set(struct ble_scan *const scan_ctx, ble_gap_scan_params_t const *scan_params); + +/** + * @brief Handler for BLE stack events. + * + * @param[in] ble_evt BLE event. + * @param[in,out] scan Scan library instance. + */ +void ble_scan_on_ble_evt(ble_evt_t const *ble_evt, void *scan); + +/** + * @brief Convert the raw address to the SoftDevice GAP address. + * + * @details This function inverts the byte order in the address. If you enter the address as it is + * displayed (for example, on a phone screen from left to right), you must use + * this function to convert the address to the SoftDevice address type. + * + * @note This function does not decode an address type. + * + * @param[out] gap_addr The Bluetooth Low Energy address. + * @param[in] addr Address to be converted to the SoftDevice address. + * + * @retval NRF_ERROR_NULL If a NULL pointer is passed as input. + * @retval NRF_SUCCESS If the address is copied and converted successfully. + */ +int ble_scan_copy_addr_to_sd_gap_addr(ble_gap_addr_t *gap_addr, + uint8_t const addr[BLE_GAP_ADDR_LEN]); + +#ifdef __cplusplus +} +#endif + +#endif /* BLE_SCAN_H__ */ + +/** @} */ diff --git a/include/bm/bluetooth/peer_manager/peer_manager_types.h b/include/bm/bluetooth/peer_manager/peer_manager_types.h index 8219c6edfa..5fffb973db 100644 --- a/include/bm/bluetooth/peer_manager/peer_manager_types.h +++ b/include/bm/bluetooth/peer_manager/peer_manager_types.h @@ -20,7 +20,7 @@ #include #include #include -#include +#include #ifdef __cplusplus extern "C" { diff --git a/include/bm/bluetooth/services/ble_hrs_c.h b/include/bm/bluetooth/services/ble_hrs_c.h new file mode 100644 index 0000000000..328cbf2f14 --- /dev/null +++ b/include/bm/bluetooth/services/ble_hrs_c.h @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2012 - 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ +/** + * @file + * + * @defgroup ble_hrs_c Heart Rate Service Client + * @{ + * @brief Heart Rate Service Client. + * + * @details This library contains the APIs and types exposed by the Heart Rate Service Client + * library. The application can use these APIs and types to perform the discovery of + * Heart Rate Service at the peer and to interact with it. + * + * @warning Currently, this library only supports the Heart Rate Measurement characteristic. This + * means that it is able to enable notification of the characteristic at the peer and + * is able to receive Heart Rate Measurement notifications from the peer. It does not + * support the Body Sensor Location and the Heart Rate Control Point characteristics. + * When a Heart Rate Measurement is received, this library decodes only the + * Heart Rate Measurement value field (both 8-bit and 16-bit) and provides it to + * the application. + */ + +#ifndef BLE_HRS_C_H__ +#define BLE_HRS_C_H__ + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Macro for defining a ble_hrs_c instance. + * + * @param _name Name of the instance. + * @hideinitializer + */ +#define BLE_HRS_C_DEF(_name) \ + static struct ble_hrs_c _name; \ + NRF_SDH_BLE_OBSERVER(_name##_obs, ble_hrs_c_on_ble_evt, &_name, USER_LOW) + +/** + * @brief HRS Client event type. + */ +enum ble_hrs_c_evt_type { + /** Event indicating that the Heart Rate Service was discovered at the peer. */ + BLE_HRS_C_EVT_DISCOVERY_COMPLETE, + /** Event indicating that a notification of the Heart Rate Measurement characteristic was + * received from the peer. + */ + BLE_HRS_C_EVT_HRM_NOTIFICATION, + /** Error. */ + BLE_HRS_C_EVT_ERROR, +}; + +/** + * @brief Heart Rate Measurement received from the peer. + */ +struct ble_hrm { + /** Heart Rate Value. */ + uint16_t hr_value; + /** Number of RR intervals. */ + uint8_t rr_intervals_cnt; + /** RR intervals. */ + uint16_t rr_intervals[CONFIG_BLE_HRS_C_RR_INTERVALS_MAX_COUNT]; +}; + +/** + * @brief Database for handles related to the Heart Rate Service found on the peer. + */ +struct hrs_db { + /** Handle of the CCCD of the Heart Rate Measurement characteristic. */ + uint16_t hrm_cccd_handle; + /** Handle of the Heart Rate Measurement characteristic, as provided by the SoftDevice. */ + uint16_t hrm_handle; +}; + +/** + * @brief Heart Rate Event. + */ +struct ble_hrs_c_evt { + /** Type of the event. */ + enum ble_hrs_c_evt_type evt_type; + /** Connection handle on which the Heart Rate service was discovered on the peer device. */ + uint16_t conn_handle; + union { + /** Handles related to the Heart Rate, found on the peer device. + * This is filled if the evt_type is @ref BLE_HRS_C_EVT_DISCOVERY_COMPLETE. + */ + struct hrs_db peer_db; + /** Heart Rate Measurement received. This is filled if the evt_type + * is @ref BLE_HRS_C_EVT_HRM_NOTIFICATION. + */ + struct ble_hrm hrm; + + struct { + /* Error reason */ + uint32_t reason; + } error; + } params; +}; + +/** Forward declaration. */ +struct ble_hrs_c; + +/** + * @brief Event handler type. + * + * @details This is the type of the event handler that is to be provided by the application + * of this module to receive events. + */ +typedef void (*ble_hrs_c_evt_handler_t)(struct ble_hrs_c *ble_hrs_c, struct ble_hrs_c_evt *evt); + +/** + * @brief Heart Rate Client. + */ +struct ble_hrs_c { + /** Connection handle, as provided by the SoftDevice. */ + uint16_t conn_handle; + /** Handles related to HRS on the peer. */ + struct hrs_db peer_hrs_db; + /** Application event handler to be called when there + * is an event related to the Heart Rate Service. + */ + ble_hrs_c_evt_handler_t evt_handler; + /** Pointer to the BLE GATT Queue instance. */ + const struct ble_gq *gatt_queue; +}; + +/** + * @brief Heart Rate Client configuration structure. + */ +struct ble_hrs_c_config { + /** Event handler to be called by the Heart Rate Client module when + * there is an event related to the Heart Rate Service. + */ + ble_hrs_c_evt_handler_t evt_handler; + /** Pointer to the BLE GATT Queue instance. */ + const struct ble_gq *gatt_queue; + /** Database discovery instance. */ + struct ble_db_discovery *db_discovery; +}; + +/** + * @brief Initialize the Heart Rate Client module. + * + * @details This function registers with the Database Discovery module for the Heart Rate Service. + * The module looks for the presence of a Heart Rate Service instance at the peer + * when a discovery is started. + * + * @param[in] ble_hrs_c Pointer to the Heart Rate Client structure. + * @param[in] ble_hrs_c_init Pointer to the Heart Rate initialization structure that + * contains the initialization information. + * + * @retval NRF_SUCCESS On successful initialization. + * @return Otherwise, this function propagates the error code returned by the + * Database Discovery module API @ref ble_db_discovery_service_register. + */ +uint32_t ble_hrs_c_init(struct ble_hrs_c *ble_hrs_c, struct ble_hrs_c_config *ble_hrs_c_init); + +/** + * @brief Handle BLE events from the SoftDevice. + * + * @details This function handles the BLE events received from the SoftDevice. If a BLE + * event is relevant to the Heart Rate Client module, the function uses the + * event's data to update interval variables and, if necessary, send events to the + * application. + * + * @param[in] ble_evt Pointer to the BLE event. + * @param[in] ctx Pointer to the Heart Rate Client structure. + */ +void ble_hrs_c_on_ble_evt(ble_evt_t const *ble_evt, void *ctx); + +/** + * @brief Request the peer to start sending notification of Heart Rate Measurement. + * + * @details This function enables notification of the Heart Rate Measurement at the peer + * by writing to the CCCD of the Heart Rate Measurement characteristic. + * + * @param ble_hrs_c Pointer to the Heart Rate Client structure. + * + * @retval NRF_SUCCESS If the SoftDevice is requested to write to the CCCD of the peer. + * @return Error code returned by the SoftDevice API @ref sd_ble_gattc_write. + */ +uint32_t ble_hrs_c_hrm_notif_enable(struct ble_hrs_c *ble_hrs_c); + +/** + * @brief Handle events from the Database Discovery module. + * + * @details Call this function when you get a callback event from the Database Discovery + * module. This function handles an event from the Database Discovery module and determines + * whether it relates to the discovery of Heart Rate Service at the peer. If it + * does, the function calls the application's event handler to indicate that the Heart Rate + * Service was discovered at the peer. The function also populates the event with + * service-related information before providing it to the application. + * + * @param[in] ble_hrs_c Pointer to the Heart Rate Client structure instance for + * associating the link. + * @param[in] evt Pointer to the event received from the Database Discovery module. + * + */ +void ble_hrs_on_db_disc_evt(struct ble_hrs_c *ble_hrs_c, + const struct ble_db_discovery_evt *evt); + +/** + * @brief Assign handles to an instance of hrs_c. + * + * @details Call this function when a link has been established with a peer to + * associate the link to this instance of the module. This association makes it + * possible to handle several links and associate each link to a particular + * instance of this module. The connection handle and attribute handles are + * provided from the discovery event @ref BLE_HRS_C_EVT_DISCOVERY_COMPLETE. + * + * @param[in] ble_hrs_c Pointer to the Heart Rate Client structure instance for + * associating the link. + * @param[in] conn_handle Connection handle to associate with the given Heart Rate + * Client Instance. + * @param[in] peer_hrs_handles Attribute handles for the HRS server you want this HRS_C + * client to interact with. + */ +uint32_t ble_hrs_c_handles_assign(struct ble_hrs_c *ble_hrs_c, uint16_t conn_handle, + const struct hrs_db *peer_hrs_handles); + +#ifdef __cplusplus +} +#endif + +#endif /* BLE_HRS_C_H__ */ + +/** @} */ diff --git a/lib/bluetooth/CMakeLists.txt b/lib/bluetooth/CMakeLists.txt index a29e625f37..8edff1a4e0 100644 --- a/lib/bluetooth/CMakeLists.txt +++ b/lib/bluetooth/CMakeLists.txt @@ -7,8 +7,10 @@ add_subdirectory_ifdef(CONFIG_BLE_ADV ble_adv) add_subdirectory_ifdef(CONFIG_BLE_CONN_PARAMS ble_conn_params) add_subdirectory_ifdef(CONFIG_BLE_CONN_STATE ble_conn_state) +add_subdirectory_ifdef(CONFIG_BLE_DB_DISCOVERY ble_db_discovery) add_subdirectory_ifdef(CONFIG_BLE_GATT_QUEUE ble_gq) add_subdirectory_ifdef(CONFIG_BLE_RACP ble_racp) add_subdirectory_ifdef(CONFIG_BLE_RADIO_NOTIFICATION ble_radio_notification) +add_subdirectory_ifdef(CONFIG_BLE_SCAN ble_scan) add_subdirectory_ifdef(CONFIG_BLE_QWR ble_qwr) add_subdirectory_ifdef(CONFIG_PEER_MANAGER peer_manager) diff --git a/lib/bluetooth/Kconfig b/lib/bluetooth/Kconfig index 4e7e7007f6..e28314bc03 100644 --- a/lib/bluetooth/Kconfig +++ b/lib/bluetooth/Kconfig @@ -8,9 +8,11 @@ menu "Bluetooth libraries" rsource "ble_adv/Kconfig" rsource "ble_conn_params/Kconfig" rsource "ble_conn_state/Kconfig" +rsource "ble_db_discovery/Kconfig" rsource "ble_gq/Kconfig" rsource "ble_racp/Kconfig" rsource "ble_radio_notification/Kconfig" +rsource "ble_scan/Kconfig" rsource "ble_qwr/Kconfig" rsource "peer_manager/Kconfig" diff --git a/lib/bluetooth/ble_db_discovery/CMakeLists.txt b/lib/bluetooth/ble_db_discovery/CMakeLists.txt new file mode 100644 index 0000000000..e333a0936d --- /dev/null +++ b/lib/bluetooth/ble_db_discovery/CMakeLists.txt @@ -0,0 +1,9 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +zephyr_library() +zephyr_library_sources( + ble_db_discovery.c +) diff --git a/lib/bluetooth/ble_db_discovery/Kconfig b/lib/bluetooth/ble_db_discovery/Kconfig new file mode 100644 index 0000000000..6c89ad93df --- /dev/null +++ b/lib/bluetooth/ble_db_discovery/Kconfig @@ -0,0 +1,42 @@ +# +# Copyright (c) 2025 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig BLE_DB_DISCOVERY + bool "BLE DB discovery" + depends on SOFTDEVICE + depends on BLE_GATT_QUEUE + select EXPERIMENTAL + help + database discovery. + +if BLE_DB_DISCOVERY + +config BLE_GATT_DB_MAX_CHARS + int "Database max characteristics" + range 1 6 + default 6 + help + The maximum number of database characteristics. + +config BLE_DB_DISCOVERY_MAX_SRV + int "Max discovered services" + range 1 6 + default 6 + help + The total number of services that can be discovered by this module. + +config SRV_DISC_START_HANDLE + hex "Start handle value during discovery" + range 0x0000 0x0EFF + default 0x0001 + help + The start handle value used during service discovery. + +module=BLE_DB_DISCOVERY +module-str=BLE DB Discovery +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +endif # BLE_DB_DISCOVERY diff --git a/lib/bluetooth/ble_db_discovery/ble_db_discovery.c b/lib/bluetooth/ble_db_discovery/ble_db_discovery.c new file mode 100644 index 0000000000..c18c7788c8 --- /dev/null +++ b/lib/bluetooth/ble_db_discovery/ble_db_discovery.c @@ -0,0 +1,748 @@ +/* + * Copyright (c) 2013 - 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(ble_db_disc, CONFIG_BLE_DB_DISCOVERY_LOG_LEVEL); + +static ble_db_discovery_evt_handler registered_uuid_handler_get( + struct ble_db_discovery *db_discovery, ble_uuid_t const *srv_uuid) +{ + for (uint32_t i = 0; i < db_discovery->num_registered_uuids; i++) { + if (BLE_UUID_EQ(&(db_discovery->registered_uuids[i]), srv_uuid)) { + return db_discovery->evt_handler; + } + } + + return NULL; +} + +static uint32_t uuid_register(struct ble_db_discovery *db_discovery, + ble_uuid_t const *srv_uuid) +{ + if (registered_uuid_handler_get(db_discovery, srv_uuid) != NULL) { + return NRF_SUCCESS; + } + + if (db_discovery->num_registered_uuids < CONFIG_BLE_DB_DISCOVERY_MAX_SRV) { + db_discovery->registered_uuids[db_discovery->num_registered_uuids] = *srv_uuid; + db_discovery->num_registered_uuids++; + + return NRF_SUCCESS; + } else { + return NRF_ERROR_NO_MEM; + } +} + +static void pending_user_events_send(struct ble_db_discovery *db_discovery) +{ + for (uint32_t i = 0; i < db_discovery->pending_usr_evt_index; i++) { + /* Pass the event to the corresponding event handler. */ + db_discovery->pending_usr_evts[i].evt_handler( + db_discovery, &(db_discovery->pending_usr_evts[i].evt)); + } + + db_discovery->pending_usr_evt_index = 0; +} + +static void discovery_available_evt_trigger(struct ble_db_discovery *const db_discovery, + uint16_t const conn_handle) +{ + struct ble_db_discovery_evt evt = {0}; + + evt.conn_handle = conn_handle; + evt.evt_type = BLE_DB_DISCOVERY_AVAILABLE; + + if (db_discovery->evt_handler) { + db_discovery->evt_handler(db_discovery, &evt); + } +} + +static void discovery_error_evt_trigger(struct ble_db_discovery *db_discovery, uint32_t nrf_err, + uint16_t conn_handle) +{ + ble_db_discovery_evt_handler evt_handler; + struct ble_gatt_db_srv *srv_being_discovered; + + srv_being_discovered = &(db_discovery->services[db_discovery->curr_srv_ind]); + + evt_handler = registered_uuid_handler_get(db_discovery, &(srv_being_discovered->srv_uuid)); + + if (evt_handler) { + struct ble_db_discovery_evt evt = { + .conn_handle = conn_handle, + .evt_type = BLE_DB_DISCOVERY_ERROR, + .params.error.reason = nrf_err, + }; + + db_discovery->evt_handler(db_discovery, &evt); + } +} + +static void discovery_error_handler(uint16_t conn_handle, uint32_t nrf_error, void *ctx) +{ + struct ble_db_discovery *db_discovery = (struct ble_db_discovery *)ctx; + + db_discovery->discovery_in_progress = false; + + discovery_error_evt_trigger(db_discovery, nrf_error, conn_handle); + discovery_available_evt_trigger(db_discovery, conn_handle); +} + +static void discovery_gq_event_handler(const struct ble_gq_req *req, struct ble_gq_evt *evt) +{ + if (evt->evt_type != BLE_GQ_EVT_ERROR) { + return; + } + + discovery_error_handler(evt->conn_handle, evt->error.reason, req->ctx); +} + +static void discovery_complete_evt_trigger(struct ble_db_discovery *db_discovery, bool is_srv_found, + uint16_t conn_handle) +{ + ble_db_discovery_evt_handler evt_handler; + struct ble_gatt_db_srv *srv_being_discovered; + + srv_being_discovered = &(db_discovery->services[db_discovery->curr_srv_ind]); + + evt_handler = registered_uuid_handler_get(db_discovery, &(srv_being_discovered->srv_uuid)); + + if (evt_handler) { + if (db_discovery->pending_usr_evt_index < CONFIG_BLE_DB_DISCOVERY_MAX_SRV) { + /* Insert an event into the pending event list. */ + db_discovery->pending_usr_evts[db_discovery->pending_usr_evt_index] + .evt.conn_handle = conn_handle; + db_discovery->pending_usr_evts[db_discovery->pending_usr_evt_index] + .evt.params.discovered_db = *srv_being_discovered; + + if (is_srv_found) { + db_discovery->pending_usr_evts[db_discovery->pending_usr_evt_index] + .evt.evt_type = BLE_DB_DISCOVERY_COMPLETE; + } else { + db_discovery->pending_usr_evts[db_discovery->pending_usr_evt_index] + .evt.evt_type = BLE_DB_DISCOVERY_SRV_NOT_FOUND; + } + + db_discovery->pending_usr_evts[db_discovery->pending_usr_evt_index] + .evt_handler = evt_handler; + db_discovery->pending_usr_evt_index++; + + if (db_discovery->pending_usr_evt_index == + db_discovery->num_registered_uuids) { + /* All registered modules have pending events. Send all pending + * events to the user modules. + */ + pending_user_events_send(db_discovery); + } else { + /* Too many events pending. Do nothing. (Ideally this should not + * happen.) + */ + } + } + } +} + +static void on_srv_disc_completion(struct ble_db_discovery *db_discovery, uint16_t conn_handle) +{ + uint32_t nrf_err; + struct ble_gq_req db_srv_disc_req = {0}; + struct ble_gatt_db_srv *srv_being_discovered; + + db_discovery->discoveries_count++; + + /* Check if more services need to be discovered. */ + if (db_discovery->discoveries_count < db_discovery->num_registered_uuids) { + /* Reset the current characteristic index since a new service discovery is about to + * start. + */ + db_discovery->curr_char_ind = 0; + + /* Initiate discovery of the next service. */ + db_discovery->curr_srv_ind++; + + srv_being_discovered = &(db_discovery->services[db_discovery->curr_srv_ind]); + + srv_being_discovered->srv_uuid = + db_discovery->registered_uuids[db_discovery->curr_srv_ind]; + + /* Reset the characteristic count in the current service to zero since a new + * service discovery is about to start. + */ + srv_being_discovered->char_count = 0; + + LOG_DBG("Starting discovery of service with UUID 0x%x on connection handle 0x%x.", + srv_being_discovered->srv_uuid.uuid, conn_handle); + + db_srv_disc_req.type = BLE_GQ_REQ_SRV_DISCOVERY; + db_srv_disc_req.gattc_srv_disc.start_handle = CONFIG_SRV_DISC_START_HANDLE; + db_srv_disc_req.gattc_srv_disc.srvc_uuid = srv_being_discovered->srv_uuid; + db_srv_disc_req.ctx = db_discovery; + db_srv_disc_req.evt_handler = discovery_gq_event_handler; + + nrf_err = ble_gq_item_add(db_discovery->gatt_queue, &db_srv_disc_req, conn_handle); + + if (nrf_err) { + discovery_error_handler(conn_handle, nrf_err, db_discovery); + + return; + } + } else { + /* No more service discovery is needed. */ + db_discovery->discovery_in_progress = false; + + discovery_available_evt_trigger(db_discovery, conn_handle); + } +} + +static bool is_char_discovery_reqd(struct ble_db_discovery *db_discovery, + ble_gattc_char_t *after_char) +{ + if (after_char->handle_value < + db_discovery->services[db_discovery->curr_srv_ind].handle_range.end_handle) { + /* Handle value of the characteristic being discovered is less than the end handle + * of the service being discovered. There is a possibility of more characteristics + * being present. Hence a characteristic discovery is required. + */ + return true; + } + + return false; +} + +static bool is_desc_discovery_reqd(struct ble_db_discovery *db_discovery, + struct ble_gatt_db_char *curr_char, + struct ble_gatt_db_char *next_char, + ble_gattc_handle_range_t *handle_range) +{ + if (next_char == NULL) { + /* Current characteristic is the last characteristic in the service. Check if the + * value handle of the current characteristic is equal to the service end handle. + */ + if (curr_char->characteristic.handle_value == + db_discovery->services[db_discovery->curr_srv_ind].handle_range.end_handle) { + /* No descriptors can be present for the current characteristic. curr_char + * is the last characteristic with no descriptors. + */ + return false; + } + + handle_range->start_handle = curr_char->characteristic.handle_value + 1; + + /* Since the current characteristic is the last characteristic in the service, the + * end handle should be the end handle of the service. + */ + handle_range->end_handle = + db_discovery->services[db_discovery->curr_srv_ind].handle_range.end_handle; + + return true; + } + + /* next_char != NULL. Check for existence of descriptors between the current and the next + * characteristic. + */ + if ((curr_char->characteristic.handle_value + 1) == next_char->characteristic.handle_decl) { + /* No descriptors can exist between the two characteristic. */ + return false; + } + + handle_range->start_handle = curr_char->characteristic.handle_value + 1; + handle_range->end_handle = next_char->characteristic.handle_decl - 1; + + return true; +} + +static uint32_t characteristics_discover(struct ble_db_discovery *db_discovery, + uint16_t conn_handle) +{ + struct ble_gatt_db_srv *srv_being_discovered; + ble_gattc_handle_range_t handle_range = {0}; + struct ble_gq_req db_char_disc_req = {0}; + + srv_being_discovered = &(db_discovery->services[db_discovery->curr_srv_ind]); + + if (db_discovery->curr_char_ind != 0) { + /* This is not the first characteristic being discovered. Hence the 'start handle' + * to be used must be computed using the handle_value of the previous + * characteristic. + */ + ble_gattc_char_t *prev_char; + uint8_t prev_char_ind = db_discovery->curr_char_ind - 1; + + srv_being_discovered = &(db_discovery->services[db_discovery->curr_srv_ind]); + + prev_char = &(srv_being_discovered->charateristics[prev_char_ind].characteristic); + + handle_range.start_handle = prev_char->handle_value + 1; + } else { + /* This is the first characteristic of this service being discovered. */ + handle_range.start_handle = srv_being_discovered->handle_range.start_handle; + } + + handle_range.end_handle = srv_being_discovered->handle_range.end_handle; + + db_char_disc_req.type = BLE_GQ_REQ_CHAR_DISCOVERY; + db_char_disc_req.gattc_char_disc = handle_range; + db_char_disc_req.ctx = db_discovery; + db_char_disc_req.evt_handler = discovery_gq_event_handler; + + return ble_gq_item_add(db_discovery->gatt_queue, &db_char_disc_req, conn_handle); +} + +static uint32_t descriptors_discover(struct ble_db_discovery *db_discovery, + bool *raise_discov_complete, uint16_t conn_handle) +{ + ble_gattc_handle_range_t handle_range; + struct ble_gatt_db_char *curr_char_being_discovered; + struct ble_gatt_db_srv *srv_being_discovered; + struct ble_gq_req db_desc_disc_req = {0}; + bool is_discovery_reqd = false; + + srv_being_discovered = &(db_discovery->services[db_discovery->curr_srv_ind]); + + curr_char_being_discovered = + &(srv_being_discovered->charateristics[db_discovery->curr_char_ind]); + + if ((db_discovery->curr_char_ind + 1) == srv_being_discovered->char_count) { + /* This is the last characteristic of this service. */ + is_discovery_reqd = is_desc_discovery_reqd(db_discovery, curr_char_being_discovered, + NULL, &handle_range); + } else { + uint8_t i; + struct ble_gatt_db_char *next_char; + + for (i = db_discovery->curr_char_ind; i < srv_being_discovered->char_count; i++) { + if (i == (srv_being_discovered->char_count - 1)) { + /* The current characteristic is the last characteristic in the + * service. + */ + next_char = NULL; + } else { + next_char = &(srv_being_discovered->charateristics[i + 1]); + } + + /* Check if it is possible for the current characteristic to have a + * descriptor. + */ + if (is_desc_discovery_reqd(db_discovery, curr_char_being_discovered, + next_char, &handle_range)) { + is_discovery_reqd = true; + break; + } + /* No descriptors can exist. */ + curr_char_being_discovered = next_char; + db_discovery->curr_char_ind++; + } + } + + if (!is_discovery_reqd) { + /* No more descriptor discovery required. Discovery is complete. + * This informs the caller that a discovery complete event can be triggered. + */ + *raise_discov_complete = true; + + return NRF_SUCCESS; + } + + *raise_discov_complete = false; + + db_desc_disc_req.type = BLE_GQ_REQ_DESC_DISCOVERY; + db_desc_disc_req.gattc_desc_disc = handle_range; + db_desc_disc_req.ctx = db_discovery; + db_desc_disc_req.evt_handler = discovery_gq_event_handler; + + return ble_gq_item_add(db_discovery->gatt_queue, &db_desc_disc_req, conn_handle); +} + +static void on_primary_srv_discovery_rsp(struct ble_db_discovery *db_discovery, + ble_gattc_evt_t const *ble_gattc_evt) +{ + struct ble_gatt_db_srv *srv_being_discovered; + + srv_being_discovered = &(db_discovery->services[db_discovery->curr_srv_ind]); + + if (ble_gattc_evt->conn_handle != db_discovery->conn_handle) { + return; + } + + if (ble_gattc_evt->gatt_status == BLE_GATT_STATUS_SUCCESS) { + uint32_t nrf_err; + ble_gattc_evt_prim_srvc_disc_rsp_t const *prim_srvc_disc_rsp_evt; + + LOG_DBG("Found service UUID 0x%x.", srv_being_discovered->srv_uuid.uuid); + + prim_srvc_disc_rsp_evt = &(ble_gattc_evt->params.prim_srvc_disc_rsp); + + srv_being_discovered->handle_range = + prim_srvc_disc_rsp_evt->services[0].handle_range; + + /* Number of services, previously discovered. */ + uint8_t num_srv_previous_disc = db_discovery->srv_count; + + /* Number of services, currently discovered. */ + uint8_t current_srv_disc = prim_srvc_disc_rsp_evt->count; + + if ((num_srv_previous_disc + current_srv_disc) <= CONFIG_BLE_DB_DISCOVERY_MAX_SRV) { + db_discovery->srv_count += current_srv_disc; + } else { + db_discovery->srv_count = CONFIG_BLE_DB_DISCOVERY_MAX_SRV; + + LOG_WRN("Not enough space for services."); + LOG_WRN("Increase CONFIG_BLE_DB_DISCOVERY_MAX_SRV to be able to store more " + "services!"); + } + + nrf_err = characteristics_discover(db_discovery, ble_gattc_evt->conn_handle); + + if (nrf_err) { + discovery_error_handler(ble_gattc_evt->conn_handle, nrf_err, db_discovery); + } + } else { + LOG_DBG("Service UUID 0x%x not found.", srv_being_discovered->srv_uuid.uuid); + /* Trigger Service Not Found event to the application. */ + discovery_complete_evt_trigger(db_discovery, false, ble_gattc_evt->conn_handle); + on_srv_disc_completion(db_discovery, ble_gattc_evt->conn_handle); + } +} + +static void on_characteristic_discovery_rsp(struct ble_db_discovery *db_discovery, + ble_gattc_evt_t const *ble_gattc_evt) +{ + uint32_t nrf_err; + struct ble_gatt_db_srv *srv_being_discovered; + bool perform_desc_discov = false; + + if (ble_gattc_evt->conn_handle != db_discovery->conn_handle) { + return; + } + + srv_being_discovered = &(db_discovery->services[db_discovery->curr_srv_ind]); + + if (ble_gattc_evt->gatt_status == BLE_GATT_STATUS_SUCCESS) { + ble_gattc_evt_char_disc_rsp_t const *char_disc_rsp_evt; + + char_disc_rsp_evt = &(ble_gattc_evt->params.char_disc_rsp); + + /* Find out the number of characteristics that were previously discovered (in + * earlier characteristic discovery responses, if any). + */ + uint8_t num_chars_prev_disc = srv_being_discovered->char_count; + + /* Find out the number of characteristics that are currently discovered (in the + * characteristic discovery response being handled). + */ + uint8_t num_chars_curr_disc = char_disc_rsp_evt->count; + + /* Check if the total number of discovered characteristics are supported by this + * module. + */ + if ((num_chars_prev_disc + num_chars_curr_disc) <= CONFIG_BLE_GATT_DB_MAX_CHARS) { + /* Update the characteristics count. */ + srv_being_discovered->char_count += num_chars_curr_disc; + } else { + /* The number of characteristics discovered at the peer is more than the + * supported maximum. This module will store only the characteristics found + * up to this point. + */ + srv_being_discovered->char_count = CONFIG_BLE_GATT_DB_MAX_CHARS; + LOG_WRN("Not enough space for characteristics associated with " + "service 0x%04X !", + srv_being_discovered->srv_uuid.uuid); + LOG_WRN("Increase CONFIG_BLE_GATT_DB_MAX_CHARS to be able to store more " + "characteristics for each service!"); + } + + uint32_t i; + uint32_t j; + + for (i = num_chars_prev_disc, j = 0; i < srv_being_discovered->char_count; + i++, j++) { + srv_being_discovered->charateristics[i].characteristic = + char_disc_rsp_evt->chars[j]; + + srv_being_discovered->charateristics[i].cccd_handle = + BLE_GATT_HANDLE_INVALID; + srv_being_discovered->charateristics[i].ext_prop_handle = + BLE_GATT_HANDLE_INVALID; + srv_being_discovered->charateristics[i].user_desc_handle = + BLE_GATT_HANDLE_INVALID; + srv_being_discovered->charateristics[i].report_ref_handle = + BLE_GATT_HANDLE_INVALID; + } + + ble_gattc_char_t *last_known_char; + + last_known_char = &(srv_being_discovered->charateristics[i - 1].characteristic); + + /* If no more characteristic discovery is required, or if the maximum number of + * supported characteristic per service has been reached, descriptor discovery will + * be performed. + */ + if (!is_char_discovery_reqd(db_discovery, last_known_char) || + (srv_being_discovered->char_count == CONFIG_BLE_GATT_DB_MAX_CHARS)) { + perform_desc_discov = true; + } else { + /* Update the current characteristic index. */ + db_discovery->curr_char_ind = srv_being_discovered->char_count; + + /* Perform another round of characteristic discovery. */ + nrf_err = characteristics_discover(db_discovery, + ble_gattc_evt->conn_handle); + if (nrf_err) { + discovery_error_handler(ble_gattc_evt->conn_handle, nrf_err, + db_discovery); + + return; + } + } + } else { + /* The previous characteristic discovery resulted in no characteristics. + * descriptor discovery should be performed. + */ + perform_desc_discov = true; + } + + if (perform_desc_discov) { + bool raise_discov_complete; + + db_discovery->curr_char_ind = 0; + + nrf_err = descriptors_discover(db_discovery, &raise_discov_complete, + ble_gattc_evt->conn_handle); + + if (nrf_err) { + discovery_error_handler(ble_gattc_evt->conn_handle, nrf_err, db_discovery); + + return; + } + if (raise_discov_complete) { + /* No more characteristics and descriptors need to be discovered. Discovery + * is complete. Send a discovery complete event to the user application. + */ + LOG_DBG("Discovery of service with UUID 0x%x completed with success" + " on connection handle 0x%x.", + srv_being_discovered->srv_uuid.uuid, ble_gattc_evt->conn_handle); + + discovery_complete_evt_trigger(db_discovery, true, + ble_gattc_evt->conn_handle); + on_srv_disc_completion(db_discovery, ble_gattc_evt->conn_handle); + } + } +} + +static void on_descriptor_discovery_rsp(struct ble_db_discovery *const db_discovery, + const ble_gattc_evt_t *const ble_gattc_evt) +{ + const ble_gattc_evt_desc_disc_rsp_t *desc_disc_rsp_evt; + struct ble_gatt_db_srv *srv_being_discovered; + + if (ble_gattc_evt->conn_handle != db_discovery->conn_handle) { + return; + } + + srv_being_discovered = &(db_discovery->services[db_discovery->curr_srv_ind]); + + desc_disc_rsp_evt = &(ble_gattc_evt->params.desc_disc_rsp); + + struct ble_gatt_db_char *char_being_discovered = + &(srv_being_discovered->charateristics[db_discovery->curr_char_ind]); + + if (ble_gattc_evt->gatt_status == BLE_GATT_STATUS_SUCCESS) { + /* The descriptor was found at the peer. + * Iterate through and collect CCCD, Extended Properties, + * User Description & Report Reference descriptor handles. + */ + for (uint32_t i = 0; i < desc_disc_rsp_evt->count; i++) { + switch (desc_disc_rsp_evt->descs[i].uuid.uuid) { + case BLE_UUID_DESCRIPTOR_CLIENT_CHAR_CONFIG: + char_being_discovered->cccd_handle = + desc_disc_rsp_evt->descs[i].handle; + break; + + case BLE_UUID_DESCRIPTOR_CHAR_EXT_PROP: + char_being_discovered->ext_prop_handle = + desc_disc_rsp_evt->descs[i].handle; + break; + + case BLE_UUID_DESCRIPTOR_CHAR_USER_DESC: + char_being_discovered->user_desc_handle = + desc_disc_rsp_evt->descs[i].handle; + break; + + case BLE_UUID_REPORT_REF_DESCR: + char_being_discovered->report_ref_handle = + desc_disc_rsp_evt->descs[i].handle; + break; + } + + /* Break if we've found all the descriptors we are looking for. */ + if (char_being_discovered->cccd_handle != BLE_GATT_HANDLE_INVALID && + char_being_discovered->ext_prop_handle != BLE_GATT_HANDLE_INVALID && + char_being_discovered->user_desc_handle != BLE_GATT_HANDLE_INVALID && + char_being_discovered->report_ref_handle != BLE_GATT_HANDLE_INVALID) { + break; + } + } + } + + bool raise_discov_complete = false; + + if ((db_discovery->curr_char_ind + 1) == srv_being_discovered->char_count) { + /* No more characteristics and descriptors need to be discovered. Discovery is + * complete. Send a discovery complete event to the user application. + */ + raise_discov_complete = true; + } else { + /* Begin discovery of descriptors for the next characteristic.*/ + uint32_t nrf_err; + + db_discovery->curr_char_ind++; + + nrf_err = descriptors_discover(db_discovery, &raise_discov_complete, + ble_gattc_evt->conn_handle); + + if (nrf_err) { + discovery_error_handler(ble_gattc_evt->conn_handle, nrf_err, db_discovery); + + return; + } + } + + if (raise_discov_complete) { + LOG_DBG("Discovery of service with UUID 0x%x completed with success" + " on connection handle 0x%x.", + srv_being_discovered->srv_uuid.uuid, ble_gattc_evt->conn_handle); + + discovery_complete_evt_trigger(db_discovery, true, ble_gattc_evt->conn_handle); + on_srv_disc_completion(db_discovery, ble_gattc_evt->conn_handle); + } +} + +static uint32_t discovery_start(struct ble_db_discovery *const db_discovery, uint16_t conn_handle) +{ + int nrf_err; + struct ble_gatt_db_srv *srv_being_discovered; + struct ble_gq_req db_srv_disc_req = {0}; + + nrf_err = ble_gq_conn_handle_register(db_discovery->gatt_queue, conn_handle); + if (nrf_err) { + return nrf_err; + } + + db_discovery->conn_handle = conn_handle; + + srv_being_discovered = &(db_discovery->services[db_discovery->curr_srv_ind]); + srv_being_discovered->srv_uuid = + db_discovery->registered_uuids[db_discovery->curr_srv_ind]; + + LOG_DBG("Starting discovery of service with UUID 0x%x on connection handle 0x%x.", + srv_being_discovered->srv_uuid.uuid, conn_handle); + + db_srv_disc_req.type = BLE_GQ_REQ_SRV_DISCOVERY; + db_srv_disc_req.gattc_srv_disc.start_handle = CONFIG_SRV_DISC_START_HANDLE; + db_srv_disc_req.gattc_srv_disc.srvc_uuid = srv_being_discovered->srv_uuid; + db_srv_disc_req.ctx = db_discovery; + db_srv_disc_req.evt_handler = discovery_gq_event_handler; + + nrf_err = ble_gq_item_add(db_discovery->gatt_queue, &db_srv_disc_req, conn_handle); + if (nrf_err) { + return nrf_err; + } + + db_discovery->discovery_in_progress = true; + + return NRF_SUCCESS; +} + +uint32_t ble_db_discovery_init(struct ble_db_discovery *db_discovery, + struct ble_db_discovery_config *db_config) +{ + if (!db_discovery || !db_config || !(db_config->evt_handler) || !(db_config->gatt_queue)) { + return NRF_ERROR_NULL; + } + + db_discovery->num_registered_uuids = 0; + db_discovery->evt_handler = db_config->evt_handler; + db_discovery->gatt_queue = db_config->gatt_queue; + + return NRF_SUCCESS; +} + +uint32_t ble_db_discovery_start(struct ble_db_discovery *const db_discovery, uint16_t conn_handle) +{ + if (!db_discovery) { + return NRF_ERROR_NULL; + } + if (!db_discovery->gatt_queue) { + return NRF_ERROR_INVALID_STATE; + } + + if (db_discovery->num_registered_uuids == 0) { + /* No user modules were registered. There are no services to discover. */ + return NRF_ERROR_INVALID_STATE; + } + + if (db_discovery->discovery_in_progress) { + return NRF_ERROR_BUSY; + } + + return discovery_start(db_discovery, conn_handle); +} + +uint32_t ble_db_discovery_service_register(struct ble_db_discovery *db_discovery, + ble_uuid_t const *uuid) +{ + if (!db_discovery || !uuid) { + return NRF_ERROR_NULL; + } + if (!db_discovery->gatt_queue) { + return NRF_ERROR_INVALID_STATE; + } + + return uuid_register(db_discovery, uuid); +} + +static void on_disconnected(struct ble_db_discovery *db_discovery, ble_gap_evt_t const *evt) +{ + if (evt->conn_handle == db_discovery->conn_handle) { + db_discovery->discovery_in_progress = false; + db_discovery->conn_handle = BLE_CONN_HANDLE_INVALID; + } +} + +void ble_db_discovery_on_ble_evt(ble_evt_t const *ble_evt, void *context) +{ + struct ble_db_discovery *db_discovery = (struct ble_db_discovery *)context; + + if (!ble_evt || !context || !db_discovery->gatt_queue) { + return; + } + + switch (ble_evt->header.evt_id) { + case BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP: + on_primary_srv_discovery_rsp(db_discovery, &(ble_evt->evt.gattc_evt)); + break; + + case BLE_GATTC_EVT_CHAR_DISC_RSP: + on_characteristic_discovery_rsp(db_discovery, &(ble_evt->evt.gattc_evt)); + break; + + case BLE_GATTC_EVT_DESC_DISC_RSP: + on_descriptor_discovery_rsp(db_discovery, &(ble_evt->evt.gattc_evt)); + break; + + case BLE_GAP_EVT_DISCONNECTED: + on_disconnected(db_discovery, &(ble_evt->evt.gap_evt)); + break; + + default: + break; + } +} diff --git a/lib/bluetooth/ble_scan/CMakeLists.txt b/lib/bluetooth/ble_scan/CMakeLists.txt new file mode 100644 index 0000000000..394b629e20 --- /dev/null +++ b/lib/bluetooth/ble_scan/CMakeLists.txt @@ -0,0 +1,9 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +zephyr_library() +zephyr_library_sources( + ble_scan.c +) diff --git a/lib/bluetooth/ble_scan/Kconfig b/lib/bluetooth/ble_scan/Kconfig new file mode 100644 index 0000000000..a505e23a27 --- /dev/null +++ b/lib/bluetooth/ble_scan/Kconfig @@ -0,0 +1,123 @@ +# +# Copyright (c) 2025 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig BLE_SCAN + bool "BLE Scan" + depends on SOFTDEVICE_CENTRAL + select EXPERIMENTAL + +if BLE_SCAN + +config BLE_SCAN_BUFFER_SIZE + int "Scan buffer size" + default 32 + help + Maximum size for an advertising set. + +config BLE_SCAN_NAME_MAX_LEN + int "Scan name maximum length" + default 32 + help + Maximum size for the name to search in the advertisement report. + +config BLE_SCAN_SHORT_NAME_MAX_LEN + int "Scan short name maximum length" + default 32 + help + Maximum size of the short name to search for in the advertisement report. + +config BLE_SCAN_FILTER + bool "Scan filter" + default y + help + Enabling filters for the scanning module. + +if BLE_SCAN_FILTER + +config BLE_SCAN_NAME_COUNT + int "Scan name count" + default 1 + help + Maximum number of name filters. + +config BLE_SCAN_APPEARANCE_COUNT + int "Scan appearance count" + default 0 + help + Maximum number of appearance filters. + +config BLE_SCAN_ADDRESS_COUNT + int "Scan address count" + default 0 + help + Maximum number of address filters. + +config BLE_SCAN_SHORT_NAME_COUNT + int "Scan short name count" + default 0 + help + Maximum number of short name filters. + +config BLE_SCAN_UUID_COUNT + int "Scan UUID count" + default 0 + help + Maximum number of filters for UUIDs. + +endif # BLE_SCAN_FILTER + +config BLE_SCAN_INTERVAL + int "Scanning interval" + default 160 + help + Determines the scan interval in units of 0.625 millisecond. + +config BLE_SCAN_DURATION + int "Scan duration" + default 0 + range 0 65535 + help + Duration of a scanning session in units of 10 ms. + If set to 0, the scanning continues until it is explicitly disabled. + +config BLE_SCAN_WINDOW + int "Scanning window" + default 80 + help + Determines the scanning window in units of 0.625 milliseconds. + +config BLE_SCAN_SLAVE_LATENCY + int "Scan slave latency" + default 0 + help + Determines the slave latency in counts of connection events. + +config BLE_SCAN_MIN_CONNECTION_INTERVAL + int "Minimum connection interval" + default 6 + range 6 3200 + help + Determines the minimum connection interval in units of 1.25 milliseconds. + +config BLE_SCAN_MAX_CONNECTION_INTERVAL + int "Maximum connection interval" + default 24 + range 6 3200 + help + Determines the maximum connection interval in units of 1.25 milliseconds. + +config BLE_SCAN_SUPERVISION_TIMEOUT + int "Scan supervision timeout" + default 3200 + range 10 3200 + help + Determines the supervision time-out in units of 10 millisecond. + +module=BLE_SCAN +module-str=BLE Scan +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +endif # BLE_SCAN diff --git a/lib/bluetooth/ble_scan/ble_scan.c b/lib/bluetooth/ble_scan/ble_scan.c new file mode 100644 index 0000000000..c01a000435 --- /dev/null +++ b/lib/bluetooth/ble_scan/ble_scan.c @@ -0,0 +1,894 @@ +/* + * Copyright (c) 2015 - 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include + +#include +#include +#include + +LOG_MODULE_REGISTER(ble_scan, CONFIG_BLE_SCAN_LOG_LEVEL); + +int ble_scan_copy_addr_to_sd_gap_addr(ble_gap_addr_t *gap_addr, + const uint8_t addr[BLE_GAP_ADDR_LEN]) +{ + if (!gap_addr) { + return NRF_ERROR_NULL; + } + + for (uint8_t i = 0; i < BLE_GAP_ADDR_LEN; ++i) { + gap_addr->addr[i] = addr[BLE_GAP_ADDR_LEN - (i + 1)]; + } + + return NRF_SUCCESS; +} + +static void ble_scan_connect_with_target(struct ble_scan const *const scan, + ble_gap_evt_adv_report_t const *const adv_report) +{ + uint32_t nrf_err; + struct ble_scan_evt scan_evt = { + .evt_type = BLE_SCAN_EVT_CONNECTING_ERROR, + }; + + /* Return if the automatic connection is disabled. */ + if (!scan->connect_if_match) { + return; + } + + /* Stop scanning. */ + ble_scan_stop(scan); + + /* Establish connection. */ + nrf_err = sd_ble_gap_connect(&adv_report->peer_addr, &scan->scan_params, + &scan->conn_params, scan->conn_cfg_tag); + if (nrf_err) { + LOG_ERR("Connection failed, nrf_error %#x", nrf_err); + if (scan->evt_handler) { + scan_evt.params.connecting_err.reason = nrf_err; + scan->evt_handler(&scan_evt); + } + } +} + +#if CONFIG_BLE_SCAN_FILTER + +#if (CONFIG_BLE_SCAN_ADDRESS_COUNT > 0) +static bool find_peer_addr(ble_gap_evt_adv_report_t const *const adv_report, + ble_gap_addr_t const *addr) +{ + ble_gap_addr_t const *peer_addr = &adv_report->peer_addr; + + /* Compare addresses. */ + if (memcmp(addr->addr, peer_addr->addr, sizeof(peer_addr->addr)) == 0) { + return true; + } + + return false; +} + +static bool adv_addr_compare(ble_gap_evt_adv_report_t const *const adv_report, + struct ble_scan const *const scan) +{ + ble_gap_addr_t const *addr = scan->scan_filters.addr_filter.target_addr; + + for (uint8_t i = 0; i < scan->scan_filters.addr_filter.addr_cnt; i++) { + /* Search for address. */ + if (find_peer_addr(adv_report, &addr[i])) { + return true; + } + } + + return false; +} + +static int ble_scan_addr_filter_add(struct ble_scan *const scan, uint8_t const *addr) +{ + ble_gap_addr_t *addr_filter = scan->scan_filters.addr_filter.target_addr; + uint8_t *counter = &scan->scan_filters.addr_filter.addr_cnt; + + /* If no memory for filter. */ + if (*counter >= CONFIG_BLE_SCAN_ADDRESS_COUNT) { + return NRF_ERROR_NO_MEM; + } + + /* Check for duplicated filter. */ + for (uint8_t i = 0; i < CONFIG_BLE_SCAN_ADDRESS_COUNT; i++) { + if (!memcmp(addr_filter[i].addr, addr, BLE_GAP_ADDR_LEN)) { + return NRF_SUCCESS; + } + } + + for (uint8_t i = 0; i < BLE_GAP_ADDR_LEN; i++) { + addr_filter[*counter].addr[i] = addr[i]; + } + + /* Address type is not used so set it to 0. */ + addr_filter[*counter].addr_type = 0; + + LOG_HEXDUMP_DBG(addr_filter[*counter].addr, BLE_GAP_ADDR_LEN, "Filter set on address"); + + /* Increase the address filter counter. */ + *counter += 1; + + return NRF_SUCCESS; +} +#endif /* CONFIG_BLE_SCAN_ADDRESS_COUNT */ + +#if (CONFIG_BLE_SCAN_NAME_COUNT > 0) +static uint16_t advdata_search(uint8_t const *encoded_data, uint16_t data_len, uint16_t *offset, + uint8_t ad_type) +{ + uint16_t i = 0; + uint16_t new_offset; + uint16_t len; + + if (!encoded_data || !offset) { + return 0; + } + + while ((i + 1 < data_len) && ((i < *offset) || (encoded_data[i + 1] != ad_type))) { + /* Jump to next data. */ + i += (encoded_data[i] + 1); + } + + if (i >= data_len) { + return 0; + } + + new_offset = i + 2; + len = encoded_data[i] ? (encoded_data[i] - 1) : 0; + + if (!len || ((new_offset + len) > data_len)) { + /* Malformed. Zero length or extends beyond provided data. */ + return 0; + } + + *offset = new_offset; + + return len; +} + +static bool advdata_name_find(uint8_t const *encoded_data, uint16_t data_len, + char const *target_name) +{ + uint16_t parsed_name_len; + uint8_t const *parsed_name; + uint16_t data_offset = 0; + + if (!target_name) { + return false; + } + + parsed_name_len = advdata_search(encoded_data, data_len, &data_offset, + BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME); + + parsed_name = &encoded_data[data_offset]; + + if ((data_offset != 0) && (parsed_name_len != 0) && + (strlen(target_name) == parsed_name_len) && + (memcmp(target_name, parsed_name, parsed_name_len) == 0)) { + return true; + } + + return false; +} + +static bool adv_name_compare(ble_gap_evt_adv_report_t const *adv_report, + struct ble_scan const *const scan) +{ + struct ble_scan_name_filter const *name_filter = &scan->scan_filters.name_filter; + uint16_t data_len = adv_report->data.len; + + /* Compare the name found with the name filter. */ + for (uint8_t i = 0; i < scan->scan_filters.name_filter.name_cnt; i++) { + if (advdata_name_find(adv_report->data.p_data, data_len, + name_filter->target_name[i])) { + return true; + } + } + + return false; +} + +static int ble_scan_name_filter_add(struct ble_scan *const scan, char const *name) +{ + uint8_t *counter = &scan->scan_filters.name_filter.name_cnt; + uint8_t name_len = strlen(name); + + /* Check the name length. */ + if ((name_len == 0) || (name_len > CONFIG_BLE_SCAN_NAME_MAX_LEN)) { + return NRF_ERROR_DATA_SIZE; + } + + /* If no memory for filter. */ + if (*counter >= CONFIG_BLE_SCAN_NAME_COUNT) { + return NRF_ERROR_NO_MEM; + } + + /* Check for duplicated filter. */ + for (uint8_t i = 0; i < CONFIG_BLE_SCAN_NAME_COUNT; i++) { + if (!strcmp(scan->scan_filters.name_filter.target_name[i], name)) { + return NRF_SUCCESS; + } + } + + /* Add name to filter. */ + memcpy(scan->scan_filters.name_filter.target_name[(*counter)++], name, strlen(name)); + + LOG_DBG("Adding filter on %s name", name); + + return NRF_SUCCESS; +} +#endif /* CONFIG_BLE_SCAN_NAME_COUNT */ + +#if (CONFIG_BLE_SCAN_SHORT_NAME_COUNT > 0) +static bool adv_short_name_compare(ble_gap_evt_adv_report_t const *const adv_report, + struct ble_scan const *const scan) +{ + struct ble_scan_short_name_filter const *name_filter = + &scan->scan_filters.short_name_filter; + uint16_t data_len = adv_report->data.len; + + /* Compare the name found with the name filters. */ + for (uint8_t i = 0; i < scan->scan_filters.short_name_filter.name_cnt; i++) { + if (ble_adv_data_short_name_find( + adv_report->data.p_data, data_len, + name_filter->short_name[i].short_target_name, + name_filter->short_name[i].short_name_min_len)) { + return true; + } + } + + return false; +} + +static int ble_scan_short_name_filter_add(struct ble_scan *const scan, + struct ble_scan_short_name const *short_name) +{ + uint8_t *counter = &scan->scan_filters.short_name_filter.name_cnt; + struct ble_scan_short_name_filter *short_name_filter = + &scan->scan_filters.short_name_filter; + uint8_t name_len = strlen(short_name->short_name); + + /* Check the name length. */ + if ((name_len == 0) || (name_len > CONFIG_BLE_SCAN_SHORT_NAME_MAX_LEN)) { + return NRF_ERROR_DATA_SIZE; + } + + /* If no memory for filter. */ + if (*counter >= CONFIG_BLE_SCAN_SHORT_NAME_COUNT) { + return NRF_ERROR_NO_MEM; + } + + /* Check for duplicated filter. */ + for (uint8_t i = 0; i < CONFIG_BLE_SCAN_SHORT_NAME_COUNT; i++) { + if (!strcmp(short_name_filter->short_name[i].short_target_name, + short_name->short_name)) { + return NRF_SUCCESS; + } + } + + /* Add name to the filter. */ + short_name_filter->short_name[(*counter)].short_name_min_len = + short_name->short_name_min_len; + memcpy(short_name_filter->short_name[(*counter)++].short_target_name, + short_name->short_name, strlen(short_name->short_name)); + + LOG_DBG("Adding filter on %s name", short_name->short_name); + + return NRF_SUCCESS; +} +#endif /* CONFIG_BLE_SCAN_SHORT_NAME_COUNT */ + +#if (CONFIG_BLE_SCAN_UUID_COUNT > 0) +static bool adv_uuid_compare(ble_gap_evt_adv_report_t const *const adv_report, + struct ble_scan const *const scan) +{ + struct ble_scan_uuid_filter const *uuid_filter = &scan->scan_filters.uuid_filter; + bool const all_filters_mode = scan->scan_filters.all_filters_mode; + uint16_t data_len = adv_report->data.len; + uint8_t uuid_match_cnt = 0; + + for (uint8_t i = 0; i < scan->scan_filters.uuid_filter.uuid_cnt; i++) { + + if (ble_adv_data_uuid_find(adv_report->data.p_data, data_len, + &uuid_filter->uuid[i])) { + uuid_match_cnt++; + + /* In the normal filter mode, only one UUID is needed to match. */ + if (!all_filters_mode) { + break; + } + } else if (all_filters_mode) { + break; + } + } + + /* In the multifilter mode, all UUIDs must be found in the advertisement packets. */ + if ((all_filters_mode && (uuid_match_cnt == scan->scan_filters.uuid_filter.uuid_cnt)) || + ((!all_filters_mode) && (uuid_match_cnt > 0))) { + return true; + } + + return false; +} + +static int ble_scan_uuid_filter_add(struct ble_scan *const scan, ble_uuid_t const *uuid) +{ + ble_uuid_t *uuid_filter = scan->scan_filters.uuid_filter.uuid; + uint8_t *counter = &scan->scan_filters.uuid_filter.uuid_cnt; + + /* If no memory. */ + if (*counter >= CONFIG_BLE_SCAN_UUID_COUNT) { + return NRF_ERROR_NO_MEM; + } + + /* Check for duplicated filter.*/ + for (uint8_t i = 0; i < CONFIG_BLE_SCAN_UUID_COUNT; i++) { + if (uuid_filter[i].uuid == uuid->uuid) { + return NRF_SUCCESS; + } + } + + /* Add UUID to the filter. */ + uuid_filter[(*counter)++] = *uuid; + LOG_DBG("Added filter on UUID %#x", uuid->uuid); + + return NRF_SUCCESS; +} +#endif /* CONFIG_BLE_SCAN_UUID_COUNT */ + +#if (CONFIG_BLE_SCAN_APPEARANCE_COUNT) +static bool adv_appearance_compare(ble_gap_evt_adv_report_t const *const adv_report, + struct ble_scan const *const scan) +{ + struct ble_scan_appearance_filter const *appearance_filter = + &scan->scan_filters.appearance_filter; + uint16_t data_len = adv_report->data.len; + + /* Verify if the advertised appearance matches the provided appearance. */ + for (uint8_t i = 0; i < scan->scan_filters.appearance_filter.appearance_cnt; i++) { + if (ble_adv_data_appearance_find(adv_report->data.p_data, data_len, + &appearance_filter->appearance[i])) { + return true; + } + } + return false; +} + +static int ble_scan_appearance_filter_add(struct ble_scan *const scan, uint16_t appearance) +{ + uint16_t *appearance_filter = scan->scan_filters.appearance_filter.appearance; + uint8_t *counter = &scan->scan_filters.appearance_filter.appearance_cnt; + + /* If no memory. */ + if (*counter >= CONFIG_BLE_SCAN_APPEARANCE_COUNT) { + return NRF_ERROR_NO_MEM; + } + + /* Check for duplicated filter. */ + for (uint8_t i = 0; i < CONFIG_BLE_SCAN_APPEARANCE_COUNT; i++) { + if (appearance_filter[i] == appearance) { + return NRF_SUCCESS; + } + } + + /* Add appearance to the filter. */ + appearance_filter[(*counter)++] = appearance; + LOG_DBG("Added filter on appearance %#x", appearance); + return NRF_SUCCESS; +} +#endif /* CONFIG_BLE_SCAN_APPEARANCE_COUNT */ + +int ble_scan_filter_set(struct ble_scan *const scan, uint8_t type, + void const *data) +{ + if (!scan || !data) { + return NRF_ERROR_NULL; + } + + switch (type) { +#if (CONFIG_BLE_SCAN_NAME_COUNT > 0) + case BLE_SCAN_NAME_FILTER: { + char *name = (char *)data; + + return ble_scan_name_filter_add(scan, name); + } +#endif + +#if (CONFIG_BLE_SCAN_SHORT_NAME_COUNT > 0) + case BLE_SCAN_SHORT_NAME_FILTER: { + struct ble_scan_short_name *short_name = (struct ble_scan_short_name *)data; + + return ble_scan_short_name_filter_add(scan, short_name); + } +#endif + +#if (CONFIG_BLE_SCAN_ADDRESS_COUNT > 0) + case BLE_SCAN_ADDR_FILTER: { + uint8_t *addr = (uint8_t *)data; + + return ble_scan_addr_filter_add(scan, addr); + } +#endif + +#if (CONFIG_BLE_SCAN_UUID_COUNT > 0) + case BLE_SCAN_UUID_FILTER: { + ble_uuid_t *uuid = (ble_uuid_t *)data; + + return ble_scan_uuid_filter_add(scan, uuid); + } +#endif + +#if (CONFIG_BLE_SCAN_APPEARANCE_COUNT > 0) + case BLE_SCAN_APPEARANCE_FILTER: { + uint16_t appearance = *((uint16_t *)data); + + return ble_scan_appearance_filter_add(scan, appearance); + } +#endif + + default: + return NRF_ERROR_INVALID_PARAM; + } +} + +int ble_scan_all_filter_remove(struct ble_scan *const scan) +{ +#if (CONFIG_BLE_SCAN_NAME_COUNT > 0) + struct ble_scan_name_filter *name_filter = &scan->scan_filters.name_filter; + + memset(name_filter->target_name, 0, sizeof(name_filter->target_name)); + name_filter->name_cnt = 0; +#endif + +#if (CONFIG_BLE_SCAN_SHORT_NAME_COUNT > 0) + struct ble_scan_short_name_filter *short_name_filter = + &scan->scan_filters.short_name_filter; + + memset(short_name_filter->short_name, 0, sizeof(short_name_filter->short_name)); + short_name_filter->name_cnt = 0; +#endif + +#if (CONFIG_BLE_SCAN_ADDRESS_COUNT > 0) + struct ble_scan_addr_filter *addr_filter = &scan->scan_filters.addr_filter; + + memset(addr_filter->target_addr, 0, sizeof(addr_filter->target_addr)); + addr_filter->addr_cnt = 0; +#endif + +#if (CONFIG_BLE_SCAN_UUID_COUNT > 0) + struct ble_scan_uuid_filter *uuid_filter = &scan->scan_filters.uuid_filter; + + memset(uuid_filter->uuid, 0, sizeof(uuid_filter->uuid)); + uuid_filter->uuid_cnt = 0; +#endif + +#if (CONFIG_BLE_SCAN_APPEARANCE_COUNT > 0) + struct ble_scan_appearance_filter *appearance_filter = + &scan->scan_filters.appearance_filter; + + memset(appearance_filter->appearance, 0, sizeof(appearance_filter->appearance)); + appearance_filter->appearance_cnt = 0; +#endif + + return NRF_SUCCESS; +} + +int ble_scan_filters_enable(struct ble_scan *const scan, uint8_t mode, bool match_all) +{ + int nrf_err; + struct ble_scan_filters *filters; + + if (!scan) { + return NRF_ERROR_NULL; + } + + /* Check if the mode is correct. */ + if ((!(mode & BLE_SCAN_ADDR_FILTER)) && + (!(mode & BLE_SCAN_NAME_FILTER)) && + (!(mode & BLE_SCAN_UUID_FILTER)) && + (!(mode & BLE_SCAN_SHORT_NAME_FILTER)) && + (!(mode & BLE_SCAN_APPEARANCE_FILTER))) { + return NRF_ERROR_INVALID_PARAM; + } + + /* Disable filters.*/ + nrf_err = ble_scan_filters_disable(scan); + if (nrf_err) { + return nrf_err; + } + + filters = &scan->scan_filters; + + /* Turn on the filters of your choice. */ +#if (CONFIG_BLE_SCAN_ADDRESS_COUNT > 0) + if (mode & BLE_SCAN_ADDR_FILTER) { + filters->addr_filter.addr_filter_enabled = true; + } +#endif + +#if (CONFIG_BLE_SCAN_NAME_COUNT > 0) + if (mode & BLE_SCAN_NAME_FILTER) { + filters->name_filter.name_filter_enabled = true; + } +#endif + +#if (CONFIG_BLE_SCAN_SHORT_NAME_COUNT > 0) + if (mode & BLE_SCAN_SHORT_NAME_FILTER) { + filters->short_name_filter.short_name_filter_enabled = true; + } +#endif + +#if (CONFIG_BLE_SCAN_UUID_COUNT > 0) + if (mode & BLE_SCAN_UUID_FILTER) { + filters->uuid_filter.uuid_filter_enabled = true; + } +#endif + +#if (CONFIG_BLE_SCAN_APPEARANCE_COUNT > 0) + if (mode & BLE_SCAN_APPEARANCE_FILTER) { + filters->appearance_filter.appearance_filter_enabled = true; + } +#endif + + /* Select the filter mode. */ + filters->all_filters_mode = match_all; + + return NRF_SUCCESS; +} + +int ble_scan_filters_disable(struct ble_scan *const scan) +{ + if (!scan) { + return NRF_ERROR_NULL; + } + + /* Disable all filters.*/ +#if (CONFIG_BLE_SCAN_NAME_COUNT > 0) + bool *name_filter_enabled = &scan->scan_filters.name_filter.name_filter_enabled; + *name_filter_enabled = false; +#endif + +#if (CONFIG_BLE_SCAN_ADDRESS_COUNT > 0) + bool *addr_filter_enabled = &scan->scan_filters.addr_filter.addr_filter_enabled; + *addr_filter_enabled = false; +#endif + +#if (CONFIG_BLE_SCAN_UUID_COUNT > 0) + bool *uuid_filter_enabled = &scan->scan_filters.uuid_filter.uuid_filter_enabled; + *uuid_filter_enabled = false; +#endif + +#if (CONFIG_BLE_SCAN_APPEARANCE_COUNT > 0) + bool *appearance_filter_enabled = + &scan->scan_filters.appearance_filter.appearance_filter_enabled; + *appearance_filter_enabled = false; +#endif + + return NRF_SUCCESS; +} + +int ble_scan_filter_get(struct ble_scan *const scan, struct ble_scan_filters *status) +{ + if (!scan || !status) { + return NRF_ERROR_NULL; + } + + *status = scan->scan_filters; + + return NRF_SUCCESS; +} + +#endif /* CONFIG_BLE_SCAN_FILTER */ + +bool is_whitelist_used(struct ble_scan const *const scan) +{ + if (scan->scan_params.filter_policy == BLE_GAP_SCAN_FP_WHITELIST || + scan->scan_params.filter_policy == + BLE_GAP_SCAN_FP_WHITELIST_NOT_RESOLVED_DIRECTED) { + return true; + } + + return false; +} + +int ble_scan_init(struct ble_scan *scan, struct ble_scan_config *config) +{ + if (!scan || !config) { + return NRF_ERROR_NULL; + } + + scan->evt_handler = config->evt_handler; + +#if CONFIG_BLE_SCAN_FILTER + /* Disable all scanning filters. */ + memset(&scan->scan_filters, 0, sizeof(scan->scan_filters)); +#endif + + scan->connect_if_match = config->connect_if_match; + scan->conn_cfg_tag = config->conn_cfg_tag; + scan->scan_params = config->scan_params; + scan->conn_params = config->conn_params; + + /* Assign a buffer where the advertising reports are to be stored by the SoftDevice. */ + scan->scan_buffer.p_data = scan->scan_buffer_data; + scan->scan_buffer.len = CONFIG_BLE_SCAN_BUFFER_SIZE; + + return NRF_SUCCESS; +} + +int ble_scan_params_set(struct ble_scan *const scan, + ble_gap_scan_params_t const *const scan_params) +{ + if (!scan | !scan_params) { + return NRF_ERROR_NULL; + } + + ble_scan_stop(scan); + + /* Assign new scanning parameters. */ + scan->scan_params = *scan_params; + + LOG_DBG("Scanning parameters have been changed successfully"); + + return NRF_SUCCESS; +} + +int ble_scan_start(struct ble_scan const *const scan) +{ + uint32_t nrf_err; + struct ble_scan_evt scan_evt = { + .evt_type = BLE_SCAN_EVT_WHITELIST_REQUEST, + }; + + if (!scan) { + return NRF_ERROR_NULL; + } + + ble_scan_stop(scan); + + /** If the whitelist is used and the event handler is not NULL, send the whitelist request + * to the main application. + */ + if (is_whitelist_used(scan)) { + if (scan->evt_handler) { + scan->evt_handler(&scan_evt); + } + } + + /* Start the scanning. */ + nrf_err = sd_ble_gap_scan_start(&scan->scan_params, &scan->scan_buffer); + + /* It is okay to ignore NRF_ERROR_INVALID_STATE, because the scan stopped earlier. */ + if (nrf_err && (nrf_err != NRF_ERROR_INVALID_STATE)) { + LOG_ERR("sd_ble_gap_scan_start returned nrf_error %#x", nrf_err); + return nrf_err; + } + LOG_DBG("Scanning"); + + return NRF_SUCCESS; +} + +void ble_scan_stop(struct ble_scan const *const scan) +{ + ARG_UNUSED(scan); + /** It is ok to ignore the function return value here, because this function can return + * NRF_SUCCESS or NRF_ERROR_INVALID_STATE, when app is not in the scanning state. + */ + (void)sd_ble_gap_scan_stop(); +} + +static void ble_scan_on_adv_report(struct ble_scan const *const scan, + ble_gap_evt_adv_report_t const *const adv_report) +{ + struct ble_scan_evt scan_evt = { + .scan_params = &scan->scan_params, + }; + +#if CONFIG_BLE_SCAN_FILTER + uint8_t filter_cnt = 0; + uint8_t filter_match_cnt = 0; +#endif + + /* If the whitelist is used, do not check the filters and return. */ + if (is_whitelist_used(scan)) { + scan_evt.evt_type = BLE_SCAN_EVT_WHITELIST_ADV_REPORT; + scan_evt.params.whitelist_adv_report.report = adv_report; + scan->evt_handler(&scan_evt); + + sd_ble_gap_scan_start(NULL, &scan->scan_buffer); + ble_scan_connect_with_target(scan, adv_report); + + return; + } + +#if CONFIG_BLE_SCAN_FILTER + bool const all_filter_mode = scan->scan_filters.all_filters_mode; + bool is_filter_matched = false; + +#if (CONFIG_BLE_SCAN_ADDRESS_COUNT > 0) + bool const addr_filter_enabled = scan->scan_filters.addr_filter.addr_filter_enabled; +#endif + +#if (CONFIG_BLE_SCAN_NAME_COUNT > 0) + bool const name_filter_enabled = scan->scan_filters.name_filter.name_filter_enabled; +#endif + +#if (CONFIG_BLE_SCAN_SHORT_NAME_COUNT > 0) + bool const short_name_filter_enabled = + scan->scan_filters.short_name_filter.short_name_filter_enabled; +#endif + +#if (CONFIG_BLE_SCAN_UUID_COUNT > 0) + bool const uuid_filter_enabled = scan->scan_filters.uuid_filter.uuid_filter_enabled; +#endif + +#if (CONFIG_BLE_SCAN_APPEARANCE_COUNT > 0) + bool const appearance_filter_enabled = + scan->scan_filters.appearance_filter.appearance_filter_enabled; +#endif + +#if (CONFIG_BLE_SCAN_ADDRESS_COUNT > 0) + /* Check the address filter. */ + if (addr_filter_enabled) { + /* Number of active filters. */ + filter_cnt++; + if (adv_addr_compare(adv_report, scan)) { + /* Number of filters matched. */ + filter_match_cnt++; + /* Information about the filters matched. */ + scan_evt.params.filter_match.filter_match.address_filter_match = true; + is_filter_matched = true; + } + } +#endif + +#if (CONFIG_BLE_SCAN_NAME_COUNT > 0) + /* Check the name filter. */ + if (name_filter_enabled) { + filter_cnt++; + if (adv_name_compare(adv_report, scan)) { + filter_match_cnt++; + + /* Information about the filters matched. */ + scan_evt.params.filter_match.filter_match.name_filter_match = true; + is_filter_matched = true; + } + } +#endif + +#if (CONFIG_BLE_SCAN_SHORT_NAME_COUNT > 0) + if (short_name_filter_enabled) { + filter_cnt++; + if (adv_short_name_compare(adv_report, scan)) { + filter_match_cnt++; + + /* Information about the filters matched. */ + scan_evt.params.filter_match.filter_match.short_name_filter_match = true; + is_filter_matched = true; + } + } +#endif + +#if (CONFIG_BLE_SCAN_UUID_COUNT > 0) + /* Check the UUID filter. */ + if (uuid_filter_enabled) { + filter_cnt++; + if (adv_uuid_compare(adv_report, scan)) { + filter_match_cnt++; + /* Information about the filters matched. */ + scan_evt.params.filter_match.filter_match.uuid_filter_match = true; + is_filter_matched = true; + } + } +#endif + +#if (CONFIG_BLE_SCAN_APPEARANCE_COUNT > 0) + /* Check the appearance filter. */ + if (appearance_filter_enabled) { + filter_cnt++; + if (adv_appearance_compare(adv_report, scan)) { + filter_match_cnt++; + /* Information about the filters matched. */ + scan_evt.params.filter_match.filter_match.appearance_filter_match = true; + is_filter_matched = true; + } + } + + scan_evt.evt_type = BLE_SCAN_EVT_NOT_FOUND; +#endif + + scan_evt.params.filter_match.adv_report = adv_report; + + /** In the multifilter mode, the number of the active filters must equal the number of the + * filters matched to generate the notification. + */ + if (all_filter_mode && (filter_match_cnt == filter_cnt)) { + scan_evt.evt_type = BLE_SCAN_EVT_FILTER_MATCH; + ble_scan_connect_with_target(scan, adv_report); + } + /** In the normal filter mode, only one filter match is needed to generate the notification + * to the main application. + */ + else if ((!all_filter_mode) && is_filter_matched) { + scan_evt.evt_type = BLE_SCAN_EVT_FILTER_MATCH; + ble_scan_connect_with_target(scan, adv_report); + } else { + scan_evt.evt_type = BLE_SCAN_EVT_NOT_FOUND; + scan_evt.params.not_found.report = adv_report; + } + + /* If the event handler is not NULL, notify the main application. */ + if (scan->evt_handler) { + scan->evt_handler(&scan_evt); + } + +#endif /* CONFIG_BLE_SCAN_FILTER*/ + + /* Resume the scanning. */ + (void)sd_ble_gap_scan_start(NULL, &scan->scan_buffer); +} + +static void ble_scan_on_timeout(struct ble_scan const *const scan, + ble_gap_evt_t const *const gap) +{ + ble_gap_evt_timeout_t const *timeout = &gap->params.timeout; + struct ble_scan_evt scan_evt = { + .evt_type = BLE_SCAN_EVT_SCAN_TIMEOUT, + .scan_params = &scan->scan_params, + .params.timeout.src = timeout->src, + }; + + if (timeout->src == BLE_GAP_TIMEOUT_SRC_SCAN) { + LOG_DBG("BLE_GAP_SCAN_TIMEOUT"); + if (scan->evt_handler) { + scan->evt_handler(&scan_evt); + } + } +} + +static void ble_scan_on_connected_evt(struct ble_scan const *const scan, + ble_gap_evt_t const *const gap_evt) +{ + struct ble_scan_evt scan_evt = { + .evt_type = BLE_SCAN_EVT_CONNECTED, + .params.connected.connected = &gap_evt->params.connected, + .params.connected.conn_handle = gap_evt->conn_handle, + .scan_params = &scan->scan_params, + }; + + if (scan->evt_handler) { + scan->evt_handler(&scan_evt); + } +} + +void ble_scan_on_ble_evt(ble_evt_t const *ble_evt, void *contex) +{ + struct ble_scan *scan_data = (struct ble_scan *)contex; + ble_gap_evt_adv_report_t const *adv_report = &ble_evt->evt.gap_evt.params.adv_report; + ble_gap_evt_t const *gap_evt = &ble_evt->evt.gap_evt; + + switch (ble_evt->header.evt_id) { + case BLE_GAP_EVT_ADV_REPORT: + ble_scan_on_adv_report(scan_data, adv_report); + break; + + case BLE_GAP_EVT_TIMEOUT: + ble_scan_on_timeout(scan_data, gap_evt); + break; + + case BLE_GAP_EVT_CONNECTED: + ble_scan_on_connected_evt(scan_data, gap_evt); + break; + + default: + break; + } +} diff --git a/samples/bluetooth/ble_hrs_c/CMakeLists.txt b/samples/bluetooth/ble_hrs_c/CMakeLists.txt new file mode 100644 index 0000000000..034a420691 --- /dev/null +++ b/samples/bluetooth/ble_hrs_c/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ble_hrs) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/bluetooth/ble_hrs_c/Kconfig b/samples/bluetooth/ble_hrs_c/Kconfig new file mode 100644 index 0000000000..d95aff01e1 --- /dev/null +++ b/samples/bluetooth/ble_hrs_c/Kconfig @@ -0,0 +1,40 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "BLE HRS Central sample" + +config APP_USE_TARGET_PERIPH_NAME + bool "Use target peripheral name" + default y + +if APP_USE_TARGET_PERIPH_NAME + +config APP_TARGET_PERIPH_NAME + string "Target peripheral name" + default "nRF_BM_HRS" + +endif # APP_USE_TARGET_PERIPH_NAME + +config APP_USE_TARGET_PERIPH_ADDR + bool "Use target peripheral address" + +if APP_USE_TARGET_PERIPH_ADDR + +config APP_TARGET_PERIPH_ADDR + hex "Target peripheral address" + range 0x0 0xffffffffffff + default 0x78E7F806C5D8 + +endif # APP_USE_TARGET_PERIPH_ADDR + +module=APP_BLE_HRS_C_SAMPLE +module-dep=LOG +module-str=BLE Heart Rate Central Service Sample +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +endmenu # "BLE HRS sample" + +source "Kconfig.zephyr" diff --git a/samples/bluetooth/ble_hrs_c/README.rst b/samples/bluetooth/ble_hrs_c/README.rst new file mode 100644 index 0000000000..103b279b47 --- /dev/null +++ b/samples/bluetooth/ble_hrs_c/README.rst @@ -0,0 +1,122 @@ +.. _ble_hrs_central_sample: + +Bluetooth: Heart Rate Service Central +##################################### + +.. contents:: + :local: + :depth: 2 + +The Heart Rate Service Central sample demonstrates how you can implement the Heart Rate profile as a central using |BMlong|. + +Requirements +************ + +The sample supports the following development kits: + +.. tabs:: + + .. group-tab:: Simple board variants + + The following board variants do **not** have DFU capabilities. + + .. list-table:: + :header-rows: 1 + + * - Hardware platform + - PCA + - SoftDevice + - Board target + * - `nRF54L15 DK`_ + - PCA10156 + - S145 + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice + * - `nRF54L15 DK`_ (emulating nRF54L10) + - PCA10156 + - S145 + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice + * - `nRF54L15 DK`_ (emulating nRF54L05) + - PCA10156 + - S145 + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice + + .. group-tab:: MCUboot board variants + + The following board variants have DFU capabilities. + + .. list-table:: + :header-rows: 1 + + * - Hardware platform + - PCA + - SoftDevice + - Board target + * - `nRF54L15 DK`_ + - PCA10156 + - S145 + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice/mcuboot + * - `nRF54L15 DK`_ (emulating nRF54L10) + - PCA10156 + - S145 + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice/mcuboot + * - `nRF54L15 DK`_ (emulating nRF54L05) + - PCA10156 + - S145 + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice/mcuboot + +Overview +******** + +When the application starts, three timers are started. +These timers control the generation of various parts of the Heart Rate Measurement characteristic value: + +* Heart Rate +* RR Interval +* Sensor Contact Detected + +A timer for generating battery measurements is also started. + +The sensor measurements are simulated the following way: + +* Heart Rate +* RR Interval +* Sensor Contact +* Battery Level + +When notification of Heart Rate Measurement characteristic is enabled, the Heart Rate Measurement, containing the current value for all the components of the Heart Rate Measurement characteristic, is notified each time the Heart Rate measurement timer expires. +When notification of Battery Level characteristic is enabled, the Battery Level is notified each time the Battery Level measurement timer expires. + +Programming the S115 SoftDevice +******************************* + +.. include:: /includes/softdevice_flash.txt + +.. _ble_hrs_sample_testing: + +Building and running +******************** + +This sample can be found under :file:`samples/bluetooth/ble_hrs/` in the |BMshort| folder structure. + +.. include:: /includes/create_sample.txt + +.. include:: /includes/configure_and_build_sample.txt + +.. include:: /includes/program_sample.txt + +Testing +======= + +You can test this sample using `nRF Connect for Desktop`_ with the `Bluetooth Low Energy app`_ and the `Serial Terminal app`_. +Make sure that these are installed before starting the testing procedure. + +1. Compile and program the application. +#. In the Serial Terminal, observe that the ``BLE HRS sample started`` message is printed. +#. Observe that the ``Advertising as nRF_BM_HRS`` message is printed. +#. In nRF Connect for Desktop, scan for advertising devices. + Your device should be advertising as ``nRF_BM_HRS``. + If the device is not advertising, you might need to use the :guilabel:`Reset Board` option in |VSC|. +#. :guilabel:`Connect` to your device. + The terminal output in |VSC| indicates ``Peer connected``. +#. Observe that the services are shown in the connected device and that you can start receiving values for the Heart Rate and the Battery Service by clicking the Play button. + Heart Rate notifications are received every second, and Battery Level notifications are received every two seconds. diff --git a/samples/bluetooth/ble_hrs_c/prj.conf b/samples/bluetooth/ble_hrs_c/prj.conf new file mode 100644 index 0000000000..456787e8d2 --- /dev/null +++ b/samples/bluetooth/ble_hrs_c/prj.conf @@ -0,0 +1,46 @@ +CONFIG_LOG=y +CONFIG_LOG_BACKEND_BM_UARTE=y + +CONFIG_SOFTDEVICE=y +CONFIG_NRF_SDH_BLE_TOTAL_LINK_COUNT=2 +CONFIG_NRF_SDH_BLE_CENTRAL_LINK_COUNT=1 + +CONFIG_BM_BUTTONS=y +CONFIG_BM_TIMER=y + +# Enable RNG +CONFIG_NRF_SECURITY=y +CONFIG_MBEDTLS_PSA_CRYPTO_C=y +CONFIG_PSA_WANT_GENERATE_RANDOM=y + +# BLE Heart rate client +CONFIG_BLE_HRS_C=y + +# BLE connection parameter +CONFIG_BLE_CONN_PARAMS=y + +# BLE database discovery +CONFIG_BLE_DB_DISCOVERY=y +CONFIG_BLE_GATT_QUEUE=y + +# BLE scan +CONFIG_BLE_SCAN=y +CONFIG_BLE_SCAN_UUID_COUNT=2 +CONFIG_BLE_SCAN_ADDRESS_COUNT=1 +CONFIG_BLE_ADV=y + +# Peer manager +CONFIG_PEER_MANAGER=y +CONFIG_PM_LESC=y +CONFIG_BM_ZMS=y +CONFIG_BLE_CONN_STATE=y + +# Enable Crypto functionality required by LE Secure Connection +CONFIG_PSA_WANT_ALG_ECDH=y +CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_GENERATE=y +CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT=y +CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_EXPORT=y +CONFIG_PSA_WANT_ECC_SECP_R1_256=y +CONFIG_MBEDTLS_PSA_STATIC_KEY_SLOTS=y +CONFIG_MBEDTLS_PSA_KEY_SLOT_COUNT=1 +CONFIG_MBEDTLS_PSA_STATIC_KEY_SLOT_BUFFER_SIZE=65 diff --git a/samples/bluetooth/ble_hrs_c/sample.yaml b/samples/bluetooth/ble_hrs_c/sample.yaml new file mode 100644 index 0000000000..fd41b3361a --- /dev/null +++ b/samples/bluetooth/ble_hrs_c/sample.yaml @@ -0,0 +1,30 @@ +sample: + name: Bluetooth LE Heart Rate Central Service Sample +tests: + sample.ble_hrs_c: + build_only: true + integration_platforms: + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice/mcuboot + platform_allow: + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice/mcuboot + tags: ci_build + sample.ble_hrs_c.logging_w_hwfc_parity: + build_only: true + integration_platforms: + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice + platform_allow: + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice + extra_configs: + - CONFIG_BM_UARTE_CONSOLE_UARTE_USE_HWFC=y + - CONFIG_BM_UARTE_CONSOLE_UARTE_PARITY_INCLUDED=y + tags: ci_build diff --git a/samples/bluetooth/ble_hrs_c/src/main.c b/samples/bluetooth/ble_hrs_c/src/main.c new file mode 100644 index 0000000000..3de02279d7 --- /dev/null +++ b/samples/bluetooth/ble_hrs_c/src/main.c @@ -0,0 +1,673 @@ +/* + * Copyright (c) 2014-2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include "ble.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +LOG_MODULE_REGISTER(app, CONFIG_APP_BLE_HRS_C_SAMPLE_LOG_LEVEL); + +/* Perform bonding. */ +#define SEC_PARAM_BOND 1 +/* Man In The Middle protection not required. */ +#define SEC_PARAM_MITM 0 +/* LE Secure Connections enabled. */ +#define SEC_PARAM_LESC 1 +/* Keypress notifications not enabled. */ +#define SEC_PARAM_KEYPRESS 0 +/* No I/O capabilities. */ +#define SEC_PARAM_IO_CAPABILITIES BLE_GAP_IO_CAPS_NONE +/* Out Of Band data not available. */ +#define SEC_PARAM_OOB 0 +/* Minimum encryption key size in octets. */ +#define SEC_PARAM_MIN_KEY_SIZE 7 +/* Maximum encryption key size in octets. */ +#define SEC_PARAM_MAX_KEY_SIZE 16 + +/* Macro to unpack 16bit unsigned UUID from octet stream. */ +#define UUID16_EXTRACT(DST, SRC) \ + do { \ + (*(DST)) = (SRC)[1]; \ + (*(DST)) <<= 8; \ + (*(DST)) |= (SRC)[0]; \ + } while (0) + +/* Structure used to identify the heart rate client module. */ +BLE_HRS_C_DEF(ble_hrs_c); +/* Gatt queue instance. */ +BLE_GQ_DEF(ble_gq); +/* DB discovery module instance. */ +BLE_DB_DISCOVERY_DEF(ble_db_disc); +/* Scanning module instance. */ +BLE_SCAN_DEF(ble_scan); + +/* Current connection handle. */ +static uint16_t conn_handle; +/* True if whitelist has been temporarily disabled. */ +static bool whitelist_disabled; + +#if defined(CONFIG_APP_USE_TARGET_PERIPH_ADDR) +uint8_t target_periph_addr[BLE_GAP_ADDR_LEN] = { + (CONFIG_APP_TARGET_PERIPH_ADDR >> 40) & 0xff, + (CONFIG_APP_TARGET_PERIPH_ADDR >> 32) & 0xff, + (CONFIG_APP_TARGET_PERIPH_ADDR >> 24) & 0xff, + (CONFIG_APP_TARGET_PERIPH_ADDR >> 16) & 0xff, + (CONFIG_APP_TARGET_PERIPH_ADDR >> 8) & 0xff, + (CONFIG_APP_TARGET_PERIPH_ADDR) & 0xff, +}; +#endif /* CONFIG_APP_USE_TARGET_PERIPH_ADDR */ + +static uint32_t scan_start(bool erase_bonds); + +static void db_disc_handler(struct ble_db_discovery *db_discovery, + struct ble_db_discovery_evt *evt) +{ + ble_hrs_on_db_disc_evt(&ble_hrs_c, evt); +} + +static void pm_evt_handler(struct pm_evt const *evt) +{ + pm_handler_on_pm_evt(evt); + pm_handler_disconnect_on_sec_failure(evt); + pm_handler_flash_clean(evt); + + switch (evt->evt_id) { + case PM_EVT_PEERS_DELETE_SUCCEEDED: + scan_start(false); + break; + + default: + break; + } +} + +static void on_ble_evt(ble_evt_t const *ble_evt, void *ctx) +{ + uint32_t nrf_err; + ble_gap_evt_t const *gap_evt = &ble_evt->evt.gap_evt; + + switch (ble_evt->header.evt_id) { + case BLE_GAP_EVT_CONNECTED: + LOG_INF("Connected"); + conn_handle = ble_evt->evt.gap_evt.conn_handle; + + nrf_err = ble_db_discovery_start(&ble_db_disc, ble_evt->evt.gap_evt.conn_handle); + if (nrf_err != 0) { + LOG_ERR("db discovery start failed, nrf_error %#x", nrf_err); + } + + if (ble_conn_state_central_conn_count() < CONFIG_NRF_SDH_BLE_CENTRAL_LINK_COUNT) { + scan_start(false); + } + + break; + + case BLE_GAP_EVT_DISCONNECTED: + LOG_INF("Disconnected, reason %#x", gap_evt->params.disconnected.reason); + + if (ble_conn_state_central_conn_count() < CONFIG_NRF_SDH_BLE_CENTRAL_LINK_COUNT) { + scan_start(false); + } + + break; + + case BLE_GAP_EVT_TIMEOUT: + if (gap_evt->params.timeout.src == BLE_GAP_TIMEOUT_SRC_CONN) { + LOG_INF("Connection Request timed out"); + } + break; + + case BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST: + LOG_INF("ble gap event connection parameter update request"); + nrf_err = sd_ble_gap_conn_param_update( + gap_evt->conn_handle, + &gap_evt->params.conn_param_update_request.conn_params); + if (nrf_err) { + LOG_ERR("Failed to update connection params, nrf_error %#x", nrf_err); + } + break; + + case BLE_GATTC_EVT_TIMEOUT: + LOG_INF("GATT Client Timeout."); + nrf_err = sd_ble_gap_disconnect(ble_evt->evt.gattc_evt.conn_handle, + BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION); + if (nrf_err) { + LOG_ERR("Failed to disconnect, nrf_error %#x", nrf_err); + } + break; + + case BLE_GATTS_EVT_TIMEOUT: + LOG_INF("GATT Server Timeout."); + nrf_err = sd_ble_gap_disconnect(ble_evt->evt.gatts_evt.conn_handle, + BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION); + if (nrf_err) { + LOG_ERR("Failed to disconnect, nrf_error %#x", nrf_err); + } + break; + + default: + break; + } +} +NRF_SDH_BLE_OBSERVER(sdh_ble, on_ble_evt, NULL, USER_LOW); + +static uint32_t peer_manager_init(void) +{ + uint32_t nrf_err; + ble_gap_sec_params_t sec_param = { + .bond = SEC_PARAM_BOND, + .mitm = SEC_PARAM_MITM, + .lesc = SEC_PARAM_LESC, + .keypress = SEC_PARAM_KEYPRESS, + .io_caps = SEC_PARAM_IO_CAPABILITIES, + .oob = SEC_PARAM_OOB, + .min_key_size = SEC_PARAM_MIN_KEY_SIZE, + .max_key_size = SEC_PARAM_MAX_KEY_SIZE, + .kdist_own.enc = 1, + .kdist_own.id = 1, + .kdist_peer.enc = 1, + .kdist_peer.id = 1, + }; + + nrf_err = pm_init(); + if (nrf_err) { + LOG_ERR("PM init failed, nrf_error %#x", nrf_err); + return nrf_err; + } + + nrf_err = pm_sec_params_set(&sec_param); + if (nrf_err) { + LOG_ERR("Failed to set PM sec params, nrf_error %#x", nrf_err); + return nrf_err; + } + + nrf_err = pm_register(pm_evt_handler); + if (nrf_err) { + LOG_ERR("PM register failed, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} +static uint32_t delete_bonds(void) +{ + uint32_t nrf_err; + + LOG_INF("Erase bonds!"); + + nrf_err = pm_peers_delete(); + if (nrf_err) { + LOG_ERR("Failed to delete bonds, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} + +static void whitelist_disable(void) +{ + if (!whitelist_disabled) { + LOG_INF("Whitelist temporarily disabled."); + whitelist_disabled = true; + ble_scan_stop(&ble_scan); + scan_start(false); + } +} + +static void button_handler_whitelist_off(uint8_t pin, uint8_t action) +{ + LOG_INF("Button whitelist off"); + whitelist_disable(); +} + +static void button_handler_disconnect(uint8_t pin, uint8_t action) +{ + LOG_INF("Button disconnect"); + + uint32_t nrf_err = sd_ble_gap_disconnect(conn_handle, + BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION); + if (nrf_err != 0) { + LOG_ERR("ble gap disconnect failed, nrf_error %#x", nrf_err); + } +} + +static void hrs_c_evt_handler(struct ble_hrs_c *hrs, struct ble_hrs_c_evt *evt) +{ + + uint32_t nrf_err; + + switch (evt->evt_type) { + case BLE_HRS_C_EVT_DISCOVERY_COMPLETE: + LOG_INF("Heart rate service discovered."); + + nrf_err = ble_hrs_c_handles_assign(hrs, evt->conn_handle, &evt->params.peer_db); + if (nrf_err != 0) { + LOG_ERR("ble_hrs_c_handles_assign failed, nrf_error %#x", nrf_err); + } + + /* Heart rate service discovered. Enable notification of Heart Rate Measurement. */ + nrf_err = ble_hrs_c_hrm_notif_enable(hrs); + if (nrf_err != 0) { + LOG_ERR("ble_hrs_c_hrm_notif_enable failed, nrf_error %#x", nrf_err); + } + + break; + + case BLE_HRS_C_EVT_HRM_NOTIFICATION: + LOG_INF("Heart Rate = %d.", evt->params.hrm.hr_value); + if (evt->params.hrm.rr_intervals_cnt != 0) { + uint32_t rr_avg = 0; + + for (uint32_t i = 0; i < evt->params.hrm.rr_intervals_cnt; i++) { + rr_avg += evt->params.hrm.rr_intervals[i]; + } + rr_avg = rr_avg / evt->params.hrm.rr_intervals_cnt; + LOG_INF("rr_interval (avg) = %d.", rr_avg); + } + break; + + default: + LOG_WRN("Unhandled hrs event %d", evt->evt_type); + break; + } +} + +static uint32_t hrs_c_init(void) +{ + uint32_t nrf_err; + struct ble_hrs_c_config hrs_c_cfg = { + .evt_handler = hrs_c_evt_handler, + .gatt_queue = &ble_gq, + .db_discovery = &ble_db_disc + }; + + nrf_err = ble_hrs_c_init(&ble_hrs_c, &hrs_c_cfg); + if (nrf_err) { + LOG_ERR("Failed to init HRS central, nrf_error %#x", nrf_err); + } + + return nrf_err; +} + +static uint32_t db_discovery_init(void) +{ + uint32_t nrf_err; + struct ble_db_discovery_config db_init = {0}; + + db_init.evt_handler = db_disc_handler; + db_init.gatt_queue = &ble_gq; + + nrf_err = ble_db_discovery_init(&ble_db_disc, &db_init); + if (nrf_err) { + LOG_ERR("db discovery init failed, nrf_error %#x", nrf_err); + } + + return nrf_err; +} + +static void peer_list_get(uint16_t *peers, uint32_t *size) +{ + uint16_t peer_id; + uint32_t peers_to_copy; + + peers_to_copy = (*size < BLE_GAP_WHITELIST_ADDR_MAX_COUNT) + ? *size + : BLE_GAP_WHITELIST_ADDR_MAX_COUNT; + + peer_id = pm_next_peer_id_get(PM_PEER_ID_INVALID); + *size = 0; + + while ((peer_id != PM_PEER_ID_INVALID) && (peers_to_copy--)) { + peers[(*size)++] = peer_id; + peer_id = pm_next_peer_id_get(peer_id); + } +} + +static uint32_t whitelist_load(void) +{ + uint32_t nrf_err; + uint16_t peers[8]; + uint32_t peer_cnt; + + memset(peers, PM_PEER_ID_INVALID, sizeof(peers)); + peer_cnt = (sizeof(peers) / sizeof(*peers)); + + peer_list_get(peers, &peer_cnt); + + nrf_err = pm_whitelist_set(peers, peer_cnt); + if (nrf_err) { + return nrf_err; + } + + nrf_err = pm_device_identities_list_set(peers, peer_cnt); + if (nrf_err != NRF_ERROR_NOT_SUPPORTED) { + return nrf_err; + } + + return NRF_SUCCESS; +} + +static uint32_t on_whitelist_req(void) +{ + uint32_t nrf_err; + + ble_gap_addr_t whitelist_addrs[8] = {0}; + ble_gap_irk_t whitelist_irks[8] = {0}; + + uint32_t addr_cnt = (sizeof(whitelist_addrs) / sizeof(ble_gap_addr_t)); + uint32_t irk_cnt = (sizeof(whitelist_irks) / sizeof(ble_gap_irk_t)); + + nrf_err = whitelist_load(); + if (nrf_err) { + return nrf_err; + } + + nrf_err = pm_whitelist_get(whitelist_addrs, &addr_cnt, whitelist_irks, &irk_cnt); + if (nrf_err) { + return nrf_err; + } + + if (((addr_cnt == 0) && (irk_cnt == 0)) || (whitelist_disabled)) { + /* Don't use whitelist.*/ + nrf_err = ble_scan_params_set(&ble_scan, NULL); + if (nrf_err) { + return nrf_err; + } + } + + return NRF_SUCCESS; +} + +static uint32_t scan_start(bool erase_bonds) +{ + uint32_t nrf_err; + + if (erase_bonds) { + /* Scan is started by the PM_EVT_PEERS_DELETE_SUCCEEDED event. */ + delete_bonds(); + } else { + nrf_err = ble_scan_start(&ble_scan); + if (nrf_err) { + LOG_ERR("ble_scan_start failed, nrf_error %#x", nrf_err); + return nrf_err; + } + } + + return NRF_SUCCESS; +} + +static void conn_params_evt_handler(const struct ble_conn_params_evt *evt) +{ + switch (evt->evt_type) { + case BLE_CONN_PARAMS_EVT_ATT_MTU_UPDATED: + LOG_INF("GATT ATT MTU on connection 0x%x changed to %d.", evt->conn_handle, + evt->att_mtu); + break; + + case BLE_CONN_PARAMS_EVT_DATA_LENGTH_UPDATED: + LOG_INF("Data length for connection 0x%x updated to %d.", evt->conn_handle, + evt->data_length.rx); + break; + + default: + LOG_WRN("unhandled conn params event %d", evt->evt_type); + break; + } +} + +static void scan_evt_handler(struct ble_scan_evt const *scan_evt) +{ + uint32_t nrf_err; + + switch (scan_evt->evt_type) { + case BLE_SCAN_EVT_NOT_FOUND: + /* ignore */ + break; + + case BLE_SCAN_EVT_WHITELIST_REQUEST: + on_whitelist_req(); + whitelist_disabled = false; + LOG_INF("Whitelist request."); + break; + + case BLE_SCAN_EVT_CONNECTING_ERROR: + nrf_err = scan_evt->params.connecting_err.reason; + LOG_INF("Scan connecting error"); + break; + + case BLE_SCAN_EVT_SCAN_TIMEOUT: + LOG_INF("Scan timed out."); + scan_start(false); + break; + + case BLE_SCAN_EVT_FILTER_MATCH: + LOG_INF("Scan filter match"); + break; + + case BLE_SCAN_EVT_WHITELIST_ADV_REPORT: + LOG_INF("Whitelist advertise report."); + break; + + case BLE_SCAN_EVT_CONNECTED: { + ble_gap_evt_connected_t const *p_connected = scan_evt->params.connected.connected; + + LOG_INF("Connecting to target %02x%02x%02x%02x%02x%02x", + p_connected->peer_addr.addr[0], p_connected->peer_addr.addr[1], + p_connected->peer_addr.addr[2], p_connected->peer_addr.addr[3], + p_connected->peer_addr.addr[4], p_connected->peer_addr.addr[5]); + } break; + + default: + LOG_WRN("Unhandled scan event %d", scan_evt->evt_type); + break; + } +} + +static uint32_t gatt_init(void) +{ + uint32_t nrf_err = ble_conn_params_evt_handler_set(conn_params_evt_handler); + + if (nrf_err) { + LOG_ERR("ble_conn_params_evt_handler_set failed, nrf_error %#x", nrf_err); + } + + return nrf_err; +} + +static uint32_t scan_init(void) +{ + uint32_t nrf_err; + struct ble_scan_config scan_cfg = { + .scan_params = { + .active = 0x01, + .interval = BLE_GAP_SCAN_INTERVAL_US_MIN * 6, + .window = BLE_GAP_SCAN_WINDOW_US_MIN * 6, + + .filter_policy = BLE_GAP_SCAN_FP_ACCEPT_ALL, + .timeout = BLE_GAP_SCAN_TIMEOUT_UNLIMITED, + .scan_phys = BLE_GAP_PHY_AUTO, + }, + .conn_params = BLE_SCAN_CONN_PARAMS_DEFAULT, + .connect_if_match = true, + .conn_cfg_tag = CONFIG_NRF_SDH_BLE_CONN_TAG, + .evt_handler = scan_evt_handler, + }; + + ble_gap_scan_params_t scan_params = BLE_SCAN_SCAN_PARAMS_DEFAULT; + + nrf_err = ble_scan_params_set(&ble_scan, &scan_params); + if (nrf_err) { + LOG_ERR("nrf_ble_scan_params_set failed, nrf_error %#x", nrf_err); + } + + nrf_err = ble_scan_init(&ble_scan, &scan_cfg); + if (nrf_err) { + LOG_ERR("nrf_ble_scan_init failed, nrf_error %#x", nrf_err); + } + + ble_uuid_t uuid = { + .uuid = BLE_UUID_HEART_RATE_SERVICE, + .type = BLE_UUID_TYPE_BLE, + }; + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_UUID_FILTER, &uuid); + if (nrf_err) { + LOG_ERR("nrf_ble_scan_filter_set uuid failed, nrf_error %#x", nrf_err); + } + +#if defined(CONFIG_APP_USE_TARGET_PERIPH_NAME) + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_NAME_FILTER, + CONFIG_APP_TARGET_PERIPH_NAME); + if (nrf_err) { + LOG_ERR("nrf_ble_scan_filter_set name failed, nrf_error %#x", nrf_err); + } +#endif /* CONFIG_APP_USE_TARGET_PERIPH_NAME */ + +#if defined(CONFIG_APP_USE_TARGET_PERIPH_ADDR) + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_ADDR_FILTER, target_periph_addr); + if (nrf_err) { + LOG_ERR("nrf_ble_scan_filter_set address failed, nrf_error %#x", nrf_err); + } +#endif /* CONFIG_APP_USE_TARGET_PERIPH_ADDR */ + + nrf_err = ble_scan_filters_enable(&ble_scan, BLE_SCAN_UUID_FILTER | + BLE_SCAN_NAME_FILTER | + BLE_SCAN_ADDR_FILTER, false); + if (nrf_err) { + LOG_ERR("Failed to enable scan filters, nrf_error %#x", nrf_err); + } + + return NRF_SUCCESS; +} + +int main(void) +{ + int err; + uint32_t nrf_err; + static struct bm_buttons_config configs[] = { + { + .pin_number = BOARD_PIN_BTN_0, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler_whitelist_off, + }, + { + .pin_number = BOARD_PIN_BTN_1, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler_disconnect, + }, + }; + + LOG_INF("BLE HRS Central sample started."); + + nrf_gpio_cfg_output(BOARD_PIN_LED_0); + + err = bm_buttons_init(configs, ARRAY_SIZE(configs), BM_BUTTONS_DETECTION_DELAY_MIN_US); + if (err) { + LOG_ERR("Failed to initialize buttons, err %d", err); + goto idle; + } + + err = bm_buttons_enable(); + if (err) { + LOG_ERR("Failed to enable buttons, err %d", err); + goto idle; + } + + const bool erase_bonds = bm_buttons_is_pressed(BOARD_PIN_BTN_1); + + err = nrf_sdh_enable_request(); + if (err) { + LOG_ERR("Failed to enable SoftDevice, err %d", err); + goto idle; + } + + LOG_INF("SoftDevice enabled"); + + err = nrf_sdh_ble_enable(CONFIG_NRF_SDH_BLE_CONN_TAG); + if (err) { + LOG_ERR("Failed to enable BLE, err %d", err); + goto idle; + } + + LOG_INF("Bluetooth enabled"); + + nrf_err = gatt_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize gatt, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = peer_manager_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize peer manager, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = db_discovery_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize db discovery, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = hrs_c_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize HRS central, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = scan_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize scan library, nrf_error %#x", nrf_err); + goto idle; + } + + scan_start(erase_bonds); + + while (true) { +#if defined(CONFIG_PM_LESC) + (void)nrf_ble_lesc_request_handler(); +#endif +idle: + while (LOG_PROCESS()) { + } + + /* Wait for an event. */ + __WFE(); + + /* Clear Event Register */ + __SEV(); + __WFE(); + } +} diff --git a/subsys/bluetooth/services/CMakeLists.txt b/subsys/bluetooth/services/CMakeLists.txt index fed1f48964..624c604b43 100644 --- a/subsys/bluetooth/services/CMakeLists.txt +++ b/subsys/bluetooth/services/CMakeLists.txt @@ -11,3 +11,4 @@ add_subdirectory_ifdef(CONFIG_BLE_HRS ble_hrs) add_subdirectory_ifdef(CONFIG_BLE_LBS ble_lbs) add_subdirectory_ifdef(CONFIG_BLE_NUS ble_nus) add_subdirectory_ifdef(CONFIG_BLE_MCUMGR ble_mcumgr) +add_subdirectory_ifdef(CONFIG_BLE_HRS_C ble_hrs_c) diff --git a/subsys/bluetooth/services/Kconfig b/subsys/bluetooth/services/Kconfig index fcc06c2773..229d3e1c61 100644 --- a/subsys/bluetooth/services/Kconfig +++ b/subsys/bluetooth/services/Kconfig @@ -8,6 +8,7 @@ rsource "ble_cgms/Kconfig" rsource "ble_dis/Kconfig" rsource "ble_hids/Kconfig" rsource "ble_hrs/Kconfig" +rsource "ble_hrs_c/Kconfig" rsource "ble_lbs/Kconfig" rsource "ble_nus/Kconfig" rsource "ble_mcumgr/Kconfig" diff --git a/subsys/bluetooth/services/ble_hrs_c/CMakeLists.txt b/subsys/bluetooth/services/ble_hrs_c/CMakeLists.txt new file mode 100644 index 0000000000..985c9327c8 --- /dev/null +++ b/subsys/bluetooth/services/ble_hrs_c/CMakeLists.txt @@ -0,0 +1,7 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +zephyr_library() +zephyr_library_sources(ble_hrs_c.c) diff --git a/subsys/bluetooth/services/ble_hrs_c/Kconfig b/subsys/bluetooth/services/ble_hrs_c/Kconfig new file mode 100644 index 0000000000..24609cc47a --- /dev/null +++ b/subsys/bluetooth/services/ble_hrs_c/Kconfig @@ -0,0 +1,27 @@ +# +# Copyright (c) 2025 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig BLE_HRS_C + bool "BLE Heart rate service central" + depends on SOFTDEVICE + select EXPERIMENTAL + help + Heart rate service central library. + +if BLE_HRS_C + +config BLE_HRS_C_RR_INTERVALS_MAX_COUNT + int "Maximum number of RR intervals per HRM notification" + default 20 + help + Maximum number of RR intervals to be decoded for each HRM notifications + (any extra RR intervals are ignored). + +module=BLE_HRS_C +module-str=BLE Heart rate client +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +endif # BLE_HRS_C diff --git a/subsys/bluetooth/services/ble_hrs_c/ble_hrs_c.c b/subsys/bluetooth/services/ble_hrs_c/ble_hrs_c.c new file mode 100644 index 0000000000..cd9ebf6b43 --- /dev/null +++ b/subsys/bluetooth/services/ble_hrs_c/ble_hrs_c.c @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2012 - 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include + +#include + +LOG_MODULE_REGISTER(ble_hrs_c, CONFIG_BLE_HRS_C_LOG_LEVEL); + +/* Bit mask used to extract the type of heart rate value. This is used to + * find if the received heart rate is a 16 bit value or an 8 bit value. + */ +#define HRM_FLAG_MASK_HR_16BIT (0x01 << 0) + +/* Bit mask used to extract the presence of RR_INTERVALS. This is used to + * find if the received measurement includes RR_INTERVALS. + */ +#define HRM_FLAG_MASK_HR_RR_INT (0x01 << 4) + +static void gatt_error_handler(const struct ble_gq_req *req, struct ble_gq_evt *gq_evt) +{ + struct ble_hrs_c *ble_hrs_c = (struct ble_hrs_c *)req->ctx; + struct ble_hrs_c_evt evt = { + .evt_type = BLE_HRS_C_EVT_ERROR, + .conn_handle = gq_evt->conn_handle, + .params.error.reason = gq_evt->error.reason + }; + + LOG_DBG("A GATT Client error has occurred on conn_handle 0X%X, nrf_error %#x", + gq_evt->conn_handle, gq_evt->error.reason); + + if (ble_hrs_c->evt_handler) { + ble_hrs_c->evt_handler(ble_hrs_c, &evt); + } +} + +static void on_hvx(struct ble_hrs_c *ble_hrs_c, const ble_evt_t *ble_evt) +{ + uint32_t index = 0; + const ble_gattc_evt_hvx_t *hvx = &ble_evt->evt.gattc_evt.params.hvx; + struct ble_hrs_c_evt ble_hrs_c_evt = { + .evt_type = BLE_HRS_C_EVT_HRM_NOTIFICATION, + .conn_handle = ble_hrs_c->conn_handle, + .params.hrm.rr_intervals_cnt = 0, + }; + + /* Check if the event is on the link for this instance. */ + if (ble_hrs_c->conn_handle != ble_evt->evt.gattc_evt.conn_handle) { + LOG_DBG("Received HVX on link 0x%x, not associated to this instance. Ignore.", + ble_evt->evt.gattc_evt.conn_handle); + return; + } + + /* Check if this is a heart rate notification. */ + if (hvx->handle != ble_hrs_c->peer_hrs_db.hrm_handle) { + return; + } + + LOG_DBG("Received HVX on link 0x%x, hrm_handle 0x%x", + hvx->handle, ble_hrs_c->peer_hrs_db.hrm_handle); + + if (!(hvx->data[index++] & HRM_FLAG_MASK_HR_16BIT)) { + /* 8-bit heart rate value received. */ + ble_hrs_c_evt.params.hrm.hr_value = + hvx->data[index++]; + } else { + /* 16-bit heart rate value received. */ + ble_hrs_c_evt.params.hrm.hr_value = + (((uint16_t)((uint8_t *)(&(hvx->data[index])))[0])) | + (((uint16_t)((uint8_t *)(&(hvx->data[index])))[1]) << 8); + index += sizeof(uint16_t); + } + + if ((hvx->data[0] & HRM_FLAG_MASK_HR_RR_INT)) { + ble_hrs_c_evt.params.hrm.rr_intervals_cnt = CONFIG_BLE_HRS_C_RR_INTERVALS_MAX_COUNT; + + for (uint32_t i = 0; i < CONFIG_BLE_HRS_C_RR_INTERVALS_MAX_COUNT; i++) { + if (index >= hvx->len) { + ble_hrs_c_evt.params.hrm.rr_intervals_cnt = (uint8_t)i; + break; + } + ble_hrs_c_evt.params.hrm.rr_intervals[i] = + (((uint16_t)((uint8_t *)(&(hvx->data[index])))[0])) | + (((uint16_t)((uint8_t *)(&(hvx->data[index])))[1]) << 8); + index += sizeof(uint16_t); + } + } + + ble_hrs_c->evt_handler(ble_hrs_c, &ble_hrs_c_evt); +} + +static void on_disconnected(struct ble_hrs_c *ble_hrs_c, const ble_evt_t *ble_evt) +{ + if (ble_hrs_c->conn_handle == ble_evt->evt.gap_evt.conn_handle) { + ble_hrs_c->conn_handle = BLE_CONN_HANDLE_INVALID; + ble_hrs_c->peer_hrs_db.hrm_cccd_handle = BLE_GATT_HANDLE_INVALID; + ble_hrs_c->peer_hrs_db.hrm_handle = BLE_GATT_HANDLE_INVALID; + } +} + +void ble_hrs_on_db_disc_evt(struct ble_hrs_c *ble_hrs_c, const struct ble_db_discovery_evt *evt) +{ + const struct ble_gatt_db_char *db_char; + struct ble_hrs_c_evt hrs_c_evt = { + .evt_type = BLE_HRS_C_EVT_DISCOVERY_COMPLETE, + .conn_handle = evt->conn_handle, + }; + + /* Check if the Heart Rate Service was discovered. */ + if (evt->evt_type == BLE_DB_DISCOVERY_COMPLETE && + evt->params.discovered_db.srv_uuid.uuid == BLE_UUID_HEART_RATE_SERVICE && + evt->params.discovered_db.srv_uuid.type == BLE_UUID_TYPE_BLE) { + /* Find the CCCD Handle of the Heart Rate Measurement characteristic. */ + for (uint32_t i = 0; i < evt->params.discovered_db.char_count; i++) { + db_char = &evt->params.discovered_db.charateristics[i]; + + if (db_char->characteristic.uuid.uuid == + BLE_UUID_HEART_RATE_MEASUREMENT_CHAR) { + /* Found Heart Rate characteristic. Store CCCD handle and break. */ + hrs_c_evt.params.peer_db.hrm_cccd_handle = + db_char->cccd_handle; + hrs_c_evt.params.peer_db.hrm_handle = + db_char->characteristic.handle_value; + break; + } + } + + LOG_DBG("Heart Rate Service discovered at peer."); + /* If the instance has been assigned prior to db_discovery, + * assign the db_handles. + */ + if (ble_hrs_c->conn_handle != BLE_CONN_HANDLE_INVALID) { + if ((ble_hrs_c->peer_hrs_db.hrm_cccd_handle == BLE_GATT_HANDLE_INVALID) && + (ble_hrs_c->peer_hrs_db.hrm_handle == BLE_GATT_HANDLE_INVALID)) { + ble_hrs_c->peer_hrs_db = hrs_c_evt.params.peer_db; + } + } + + ble_hrs_c->evt_handler(ble_hrs_c, &hrs_c_evt); + } +} + +uint32_t ble_hrs_c_init(struct ble_hrs_c *ble_hrs_c, struct ble_hrs_c_config *ble_hrs_c_init) +{ + ble_uuid_t hrs_uuid = { + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_HEART_RATE_SERVICE, + }; + + if (!ble_hrs_c || !ble_hrs_c_init) { + return NRF_ERROR_NULL; + } + + ble_hrs_c->evt_handler = ble_hrs_c_init->evt_handler; + ble_hrs_c->gatt_queue = ble_hrs_c_init->gatt_queue; + ble_hrs_c->conn_handle = BLE_CONN_HANDLE_INVALID; + ble_hrs_c->peer_hrs_db.hrm_cccd_handle = BLE_GATT_HANDLE_INVALID; + ble_hrs_c->peer_hrs_db.hrm_handle = BLE_GATT_HANDLE_INVALID; + + return ble_db_discovery_service_register(ble_hrs_c_init->db_discovery, &hrs_uuid); +} + +void ble_hrs_c_on_ble_evt(ble_evt_t const *ble_evt, void *ctx) +{ + struct ble_hrs_c *ble_hrs_c = (struct ble_hrs_c *)ctx; + + if (!ble_hrs_c || !ble_evt) { + return; + } + + switch (ble_evt->header.evt_id) { + case BLE_GATTC_EVT_HVX: + on_hvx(ble_hrs_c, ble_evt); + break; + case BLE_GAP_EVT_DISCONNECTED: + on_disconnected(ble_hrs_c, ble_evt); + break; + default: + break; + } +} + +static uint32_t cccd_configure(struct ble_hrs_c *ble_hrs_c, bool enable) +{ + LOG_DBG("Configuring CCCD. CCCD Handle = %d, Connection Handle = %d", + ble_hrs_c->peer_hrs_db.hrm_cccd_handle, ble_hrs_c->conn_handle); + + uint16_t cccd_val = enable ? BLE_GATT_HVX_NOTIFICATION : 0; + uint8_t cccd[BLE_CCCD_VALUE_LEN] = { + (cccd_val & 0x00FF), + (cccd_val & 0xFF00) >> 8 + }; + struct ble_gq_req hrs_c_req = { + .type = BLE_GQ_REQ_GATTC_WRITE, + .evt_handler = gatt_error_handler, + .ctx = ble_hrs_c, + .gattc_write.handle = ble_hrs_c->peer_hrs_db.hrm_cccd_handle, + .gattc_write.len = BLE_CCCD_VALUE_LEN, + .gattc_write.p_value = cccd, + .gattc_write.write_op = BLE_GATT_OP_WRITE_REQ, + }; + + return ble_gq_item_add(ble_hrs_c->gatt_queue, &hrs_c_req, ble_hrs_c->conn_handle); +} + +uint32_t ble_hrs_c_hrm_notif_enable(struct ble_hrs_c *ble_hrs_c) +{ + if (!ble_hrs_c) { + return NRF_ERROR_NULL; + } + + return cccd_configure(ble_hrs_c, true); +} + +uint32_t ble_hrs_c_handles_assign(struct ble_hrs_c *ble_hrs_c, uint16_t conn_handle, + const struct hrs_db *p_peer_hrs_handles) +{ + if (!ble_hrs_c) { + return NRF_ERROR_NULL; + } + + ble_hrs_c->conn_handle = conn_handle; + + if (p_peer_hrs_handles) { + ble_hrs_c->peer_hrs_db = *p_peer_hrs_handles; + } + + return ble_gq_conn_handle_register(ble_hrs_c->gatt_queue, conn_handle); +} +/** @} */ diff --git a/tests/lib/bluetooth/ble_db_discovery/CMakeLists.txt b/tests/lib/bluetooth/ble_db_discovery/CMakeLists.txt new file mode 100644 index 0000000000..00e09f3499 --- /dev/null +++ b/tests/lib/bluetooth/ble_db_discovery/CMakeLists.txt @@ -0,0 +1,59 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +project(unit_test_ble_db_discovery) + +set(SOFTDEVICE_VARIANT "s145") +set(SOFTDEVICE_INCLUDE_DIR "${ZEPHYR_NRF_BM_MODULE_DIR}/components/softdevice/${SOFTDEVICE_VARIANT}/${SOFTDEVICE_VARIANT}_API/include") + +cmock_handle(${SOFTDEVICE_INCLUDE_DIR}/ble.h) +cmock_handle(${SOFTDEVICE_INCLUDE_DIR}/ble_gatts.h + WORD_EXCLUDE + "__STATIC_INLINE" +) +cmock_handle(${SOFTDEVICE_INCLUDE_DIR}/ble_gattc.h + WORD_EXCLUDE + "__STATIC_INLINE" +) +cmock_handle(${SOFTDEVICE_INCLUDE_DIR}/ble_gap.h) +cmock_handle(${ZEPHYR_NRF_BM_MODULE_DIR}/include/bm/bluetooth/ble_gq.h) +cmock_handle(${ZEPHYR_NRF_BM_MODULE_DIR}/include/bm/softdevice_handler/nrf_sdh_ble.h) + +zephyr_linker_sources(SECTIONS evt_obs.ld) + +add_compile_definitions( + SVCALL_AS_NORMAL_FUNCTION=1 + NRF54L15_XXAA) + +target_compile_definitions( app PRIVATE + CONFIG_NRF_SDH_BLE_TOTAL_LINK_COUNT=1 + CONFIG_BLE_DB_DISCOVERY_MAX_SRV=6 + CONFIG_BLE_GQ_QUEUE_SIZE=8 + CONFIG_BLE_GQ_HEAP_SIZE=256 + CONFIG_BLE_GQ_MAX_CONNECTIONS=1 + CONFIG_SRV_DISC_START_HANDLE=1 + CONFIG_BLE_GATT_DB_MAX_CHARS=6 +) + +# Generate and add test file +test_runner_generate(src/unity_test.c) +target_sources(app PRIVATE src/unity_test.c) + +target_include_directories(app PRIVATE ${ZEPHYR_NRF_BM_MODULE_DIR}/include) +target_include_directories(app PRIVATE ${SOFTDEVICE_INCLUDE_DIR}) +target_include_directories(app PRIVATE ${ZEPHYR_HAL_NORDIC_MODULE_DIR}/nrfx/mdk) +target_include_directories(app PRIVATE ${ZEPHYR_CMSIS_MODULE_DIR}/CMSIS/Core/Include) + +target_sources(app + PRIVATE + ${ZEPHYR_NRF_BM_MODULE_DIR}/lib/bluetooth/ble_db_discovery/ble_db_discovery.c +) + +zephyr_include_directories(${ZEPHYR_NRF_BM_MODULE_DIR}/include) diff --git a/tests/lib/bluetooth/ble_db_discovery/evt_obs.ld b/tests/lib/bluetooth/ble_db_discovery/evt_obs.ld new file mode 100644 index 0000000000..ba91196245 --- /dev/null +++ b/tests/lib/bluetooth/ble_db_discovery/evt_obs.ld @@ -0,0 +1 @@ +ITERABLE_SECTION_ROM(nrf_sdh_ble_evt_observers, 4) diff --git a/tests/lib/bluetooth/ble_db_discovery/prj.conf b/tests/lib/bluetooth/ble_db_discovery/prj.conf new file mode 100644 index 0000000000..a8788847da --- /dev/null +++ b/tests/lib/bluetooth/ble_db_discovery/prj.conf @@ -0,0 +1,6 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +CONFIG_UNITY=y diff --git a/tests/lib/bluetooth/ble_db_discovery/src/unity_test.c b/tests/lib/bluetooth/ble_db_discovery/src/unity_test.c new file mode 100644 index 0000000000..001430f681 --- /dev/null +++ b/tests/lib/bluetooth/ble_db_discovery/src/unity_test.c @@ -0,0 +1,392 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include + +#include "cmock_ble.h" +#include "cmock_ble_gap.h" +#include "cmock_ble_gattc.h" +#include "cmock_ble_gq.h" + +BLE_GQ_DEF(ble_gatt_queue); +BLE_DB_DISCOVERY_DEF(db_discovery) + +static struct ble_db_discovery_evt db_evt; +static struct ble_db_discovery_evt db_evt_prev; + +void ble_evt_send(const ble_evt_t *evt) +{ + TYPE_SECTION_FOREACH(struct nrf_sdh_ble_evt_observer, nrf_sdh_ble_evt_observers, obs) + { + obs->handler(evt, obs->context); + } +} + +static void db_discovery_evt_handler(struct ble_db_discovery *db_discovery, + struct ble_db_discovery_evt *evt) +{ + db_evt_prev = db_evt; + db_evt = *evt; +} + +void test_ble_adv_init_error_null(void) +{ + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + int ret; + + ret = ble_db_discovery_init(NULL, &config); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, ret); + ret = ble_db_discovery_init(&db_discovery, NULL); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, ret); + config.evt_handler = NULL; + ret = ble_db_discovery_init(&db_discovery, &config); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, ret); +} + +void test_ble_db_discovery_init(void) +{ + int ret; + + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + ret = ble_db_discovery_init(&db_discovery, &config); + TEST_ASSERT_EQUAL(NRF_SUCCESS, ret); + TEST_ASSERT_EQUAL(0, db_discovery.pending_usr_evt_index); + TEST_ASSERT_NOT_EQUAL(NULL, db_discovery.evt_handler); + TEST_ASSERT_NOT_EQUAL(NULL, db_discovery.gatt_queue); +} + +void test_ble_db_discovery_service_register_null(void) +{ + uint32_t ret; + + struct ble_db_discovery db_discovery = {0}; + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + ble_uuid_t hrs_uuid = {.type = BLE_UUID_TYPE_BLE, .uuid = BLE_UUID_HEART_RATE_SERVICE}; + + ret = ble_db_discovery_init(&db_discovery, &config); + TEST_ASSERT_EQUAL(NRF_SUCCESS, ret); + + ret = ble_db_discovery_service_register(&db_discovery, NULL); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, ret); + ret = ble_db_discovery_service_register(NULL, &hrs_uuid); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, ret); +} + +void test_ble_db_discovery_service_register_invalid_state(void) +{ + + ble_uuid_t hrs_uuid = {.type = BLE_UUID_TYPE_BLE, .uuid = BLE_UUID_HEART_RATE_SERVICE}; + + TEST_ASSERT_EQUAL(NRF_ERROR_INVALID_STATE, + ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); +} + +void test_ble_db_discovery_service_register_no_mem(void) +{ + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_init(&db_discovery, &config)); + + ble_uuid_t hrs_uuid = {.type = BLE_UUID_TYPE_BLE, .uuid = BLE_UUID_IMMEDIATE_ALERT_SERVICE}; + + for (size_t i = 0; i < CONFIG_BLE_DB_DISCOVERY_MAX_SRV; ++i) { + TEST_ASSERT_EQUAL(NRF_SUCCESS, + ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); + hrs_uuid.uuid++; + } + TEST_ASSERT_EQUAL(CONFIG_BLE_DB_DISCOVERY_MAX_SRV, db_discovery.num_registered_uuids); + TEST_ASSERT_EQUAL(NRF_ERROR_NO_MEM, + ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); +} + +void test_ble_db_discovery_service_register(void) +{ + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_init(&db_discovery, &config)); + + ble_uuid_t hrs_uuid = {.type = BLE_UUID_TYPE_BLE, .uuid = BLE_UUID_HEART_RATE_SERVICE}; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); +} + +void test_ble_db_discovery_start_null(void) +{ + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_init(&db_discovery, &config)); + + ble_uuid_t hrs_uuid = {.type = BLE_UUID_TYPE_BLE, .uuid = BLE_UUID_HEART_RATE_SERVICE}; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); + + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, ble_db_discovery_start(NULL, 0)); +} + +void test_ble_db_discovery_start_invalid_state(void) +{ + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + TEST_ASSERT_EQUAL(NRF_ERROR_INVALID_STATE, ble_db_discovery_start(&db_discovery, 0)); + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_init(&db_discovery, &config)); + + TEST_ASSERT_EQUAL(NRF_ERROR_INVALID_STATE, ble_db_discovery_start(&db_discovery, 0)); +} + +void test_ble_db_discovery_start_busy(void) +{ + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_init(&db_discovery, &config)); + + ble_uuid_t hrs_uuid = {.type = BLE_UUID_TYPE_BLE, .uuid = BLE_UUID_HEART_RATE_SERVICE}; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); + + __cmock_ble_gq_conn_handle_register_ExpectAnyArgsAndReturn(NRF_SUCCESS); + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_start(&db_discovery, 0)); + + TEST_ASSERT_EQUAL(NRF_ERROR_BUSY, ble_db_discovery_start(&db_discovery, 0)); +} + +void test_ble_db_discovery_start_no_mem(void) +{ + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_init(&db_discovery, &config)); + + ble_uuid_t hrs_uuid = {.type = BLE_UUID_TYPE_BLE, .uuid = BLE_UUID_HEART_RATE_SERVICE}; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); + + __cmock_ble_gq_conn_handle_register_ExpectAndReturn(&ble_gatt_queue, 8, NRF_ERROR_NO_MEM); + TEST_ASSERT_EQUAL(NRF_ERROR_NO_MEM, ble_db_discovery_start(&db_discovery, 8)); +} + +void test_ble_db_discovery_start(void) +{ + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_init(&db_discovery, &config)); + + ble_uuid_t hrs_uuid = {.type = BLE_UUID_TYPE_BLE, .uuid = BLE_UUID_HEART_RATE_SERVICE}; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); + + __cmock_ble_gq_conn_handle_register_ExpectAndReturn(&ble_gatt_queue, 8, NRF_SUCCESS); + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_start(&db_discovery, 8)); +} + +void test_ble_db_discovery_on_ble_evt(void) +{ + ble_evt_t evt; + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + __cmock_ble_gq_conn_handle_register_ExpectAndReturn(&ble_gatt_queue, 8, NRF_SUCCESS); + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_init(&db_discovery, &config)); + + ble_uuid_t hrs_uuid = {.type = BLE_UUID_TYPE_BLE, .uuid = BLE_UUID_HEART_RATE_SERVICE}; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); + + hrs_uuid.uuid = BLE_UUID_HEALTH_THERMOMETER_SERVICE; + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_start(&db_discovery, 8)); + + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.header.evt_id = BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP; + evt.evt.gattc_evt.conn_handle = 8; + evt.evt.gattc_evt.params.prim_srvc_disc_rsp.services[0].uuid.uuid = + BLE_UUID_HEART_RATE_SERVICE; + evt.evt.gattc_evt.gatt_status = BLE_GATT_STATUS_SUCCESS; + evt.evt.gattc_evt.params.prim_srvc_disc_rsp.services[0].uuid.type = BLE_UUID_TYPE_BLE; + evt.evt.gattc_evt.params.prim_srvc_disc_rsp.count = 1; + ble_evt_send(&evt); + TEST_ASSERT_NOT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt.evt_type); + + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.evt.gattc_evt.gatt_status = BLE_GATT_STATUS_UNKNOWN; + ble_evt_send(&evt); + TEST_ASSERT_NOT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt.evt_type); + + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.header.evt_id = BLE_GATTC_EVT_CHAR_DISC_RSP; + evt.evt.gattc_evt.conn_handle = 8; + evt.evt.gattc_evt.gatt_status = BLE_GATT_STATUS_SUCCESS; + evt.evt.gattc_evt.params.char_disc_rsp.count = 1; + evt.evt.gattc_evt.params.char_disc_rsp.chars[0].uuid.uuid = + BLE_UUID_HEART_RATE_MEASUREMENT_CHAR; + evt.evt.gattc_evt.params.char_disc_rsp.chars[0].uuid.type = BLE_UUID_TYPE_BLE; + ble_evt_send(&evt); + TEST_ASSERT_NOT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt.evt_type); + + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.evt.gattc_evt.params.char_disc_rsp.chars[0].uuid.uuid = + BLE_UUID_HEART_RATE_CONTROL_POINT_CHAR; + ble_evt_send(&evt); + TEST_ASSERT_NOT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt.evt_type); + + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.evt.gattc_evt.gatt_status = BLE_GATT_STATUS_UNKNOWN; + ble_evt_send(&evt); + TEST_ASSERT_NOT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt.evt_type); + + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.header.evt_id = BLE_GATTC_EVT_DESC_DISC_RSP; + evt.evt.gattc_evt.conn_handle = 8; + evt.evt.gattc_evt.gatt_status = BLE_GATT_STATUS_SUCCESS; + evt.evt.gattc_evt.params.desc_disc_rsp.count = 1; + evt.evt.gattc_evt.params.desc_disc_rsp.descs[0].handle = 8; + evt.evt.gattc_evt.params.desc_disc_rsp.descs[0].uuid.uuid = + BLE_UUID_DESCRIPTOR_CHAR_USER_DESC; + evt.evt.gattc_evt.params.desc_disc_rsp.descs[0].uuid.type = BLE_UUID_TYPE_BLE; + ble_evt_send(&evt); + TEST_ASSERT_NOT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt.evt_type); + + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.evt.gattc_evt.params.desc_disc_rsp.descs[0].uuid.uuid = + BLE_UUID_DESCRIPTOR_CHAR_EXT_PROP; + ble_evt_send(&evt); + TEST_ASSERT_NOT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt.evt_type); + + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.evt.gattc_evt.params.desc_disc_rsp.descs[0].uuid.uuid = + BLE_UUID_DESCRIPTOR_CLIENT_CHAR_CONFIG; + ble_evt_send(&evt); + TEST_ASSERT_NOT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt.evt_type); + + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.evt.gattc_evt.params.desc_disc_rsp.descs[0].uuid.uuid = BLE_UUID_REPORT_REF_DESCR; + ble_evt_send(&evt); + TEST_ASSERT_NOT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt.evt_type); + + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.header.evt_id = BLE_GAP_EVT_DISCONNECTED; + evt.evt.gap_evt.params.disconnected.reason = BLE_HCI_CONNECTION_TIMEOUT; + ble_evt_send(&evt); + TEST_ASSERT_NOT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt.evt_type); +} + +static uint32_t ble_gq_item_add_no_mem_stub(const struct ble_gq *gatt_queue, struct ble_gq_req *req, + uint16_t conn_handle, int cmock_num_calls) +{ + struct ble_gq_evt evt = {.evt_type = BLE_GQ_EVT_ERROR, .error.reason = NRF_ERROR_NO_MEM}; + + req->evt_handler(req, &evt); + + return NRF_ERROR_NO_MEM; +} + +void test_ble_db_discovery_on_ble_evt_no_mem(void) +{ + ble_evt_t evt; + struct ble_db_discovery_config config = { + .gatt_queue = &ble_gatt_queue, + .evt_handler = db_discovery_evt_handler, + }; + + __cmock_ble_gq_conn_handle_register_ExpectAndReturn(&ble_gatt_queue, 4, NRF_SUCCESS); + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_init(&db_discovery, &config)); + + ble_uuid_t hrs_uuid = {.type = BLE_UUID_TYPE_BLE, .uuid = BLE_UUID_HEART_RATE_SERVICE}; + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_service_register(&db_discovery, &hrs_uuid)); + + TEST_ASSERT_EQUAL(NRF_SUCCESS, ble_db_discovery_start(&db_discovery, 4)); + + __cmock_ble_gq_item_add_ExpectAnyArgsAndReturn(NRF_SUCCESS); + __cmock_ble_gq_item_add_StubWithCallback(ble_gq_item_add_no_mem_stub); + + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.header.evt_id = BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP; + evt.evt.gattc_evt.conn_handle = 4; + evt.evt.gattc_evt.gatt_status = BLE_GATT_STATUS_SUCCESS; + evt.evt.gattc_evt.params.prim_srvc_disc_rsp.services[0].uuid.uuid = + BLE_UUID_HEART_RATE_SERVICE; + evt.evt.gattc_evt.params.prim_srvc_disc_rsp.services[0].uuid.type = BLE_UUID_TYPE_BLE; + evt.evt.gattc_evt.params.prim_srvc_disc_rsp.count = 1; + ble_evt_send(&evt); + TEST_ASSERT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt_prev.evt_type); + TEST_ASSERT_EQUAL(NRF_ERROR_NO_MEM, db_evt_prev.params.error.reason); + + __cmock_ble_gq_on_ble_evt_ExpectAnyArgs(); + evt.header.evt_id = BLE_GATTC_EVT_CHAR_DISC_RSP; + evt.evt.gattc_evt.conn_handle = 8; + evt.evt.gattc_evt.gatt_status = BLE_GATT_STATUS_SUCCESS; + evt.evt.gattc_evt.params.char_disc_rsp.count = 1; + evt.evt.gattc_evt.params.char_disc_rsp.chars[0].uuid.uuid = BLE_UUID_BATTERY_LEVEL_CHAR; + evt.evt.gattc_evt.params.char_disc_rsp.chars[0].uuid.type = BLE_UUID_TYPE_BLE; + ble_evt_send(&evt); + TEST_ASSERT_EQUAL(BLE_DB_DISCOVERY_ERROR, db_evt_prev.evt_type); + TEST_ASSERT_EQUAL(NRF_ERROR_NO_MEM, db_evt_prev.params.error.reason); +} + +void setUp(void) +{ + memset(&db_discovery, 0, sizeof(db_discovery)); +} +void tearDown(void) +{ +} + +extern int unity_main(void); + +int main(void) +{ + return unity_main(); +} diff --git a/tests/lib/bluetooth/ble_db_discovery/testcase.yaml b/tests/lib/bluetooth/ble_db_discovery/testcase.yaml new file mode 100644 index 0000000000..f37b29eb5e --- /dev/null +++ b/tests/lib/bluetooth/ble_db_discovery/testcase.yaml @@ -0,0 +1,4 @@ +tests: + lib.ble_db_discovery: + platform_allow: native_sim + tags: unittest diff --git a/tests/lib/bluetooth/ble_scan/CMakeLists.txt b/tests/lib/bluetooth/ble_scan/CMakeLists.txt new file mode 100644 index 0000000000..f084425ec1 --- /dev/null +++ b/tests/lib/bluetooth/ble_scan/CMakeLists.txt @@ -0,0 +1,35 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +project(unit_test_ble_scan) + +zephyr_compile_definitions( + SVCALL_AS_NORMAL_FUNCTION + SUPPRESS_INLINE_IMPLEMENTATION + NRF54L15_XXAA +) + +set(SOFTDEVICE_VARIANT "s145") +set(SOFTDEVICE_INCLUDE_DIR "${ZEPHYR_NRF_BM_MODULE_DIR}/components/softdevice/\ +${SOFTDEVICE_VARIANT}/${SOFTDEVICE_VARIANT}_API/include") + +cmock_handle(${SOFTDEVICE_INCLUDE_DIR}/ble_gap.h) +cmock_handle(${SOFTDEVICE_INCLUDE_DIR}/ble_gatts.h) +cmock_handle(${SOFTDEVICE_INCLUDE_DIR}/ble_gattc.h) +cmock_handle(${ZEPHYR_NRF_BM_MODULE_DIR}/include/bm/softdevice_handler/nrf_sdh_ble.h) +cmock_handle(${ZEPHYR_NRF_BM_MODULE_DIR}/include/bm/bluetooth/ble_adv_data.h) + +zephyr_linker_sources(SECTIONS evt_obs.ld) + +zephyr_include_directories(${ZEPHYR_NRF_BM_MODULE_DIR}/include) +zephyr_include_directories(${SOFTDEVICE_INCLUDE_DIR}) + +test_runner_generate(src/unity_test.c) +target_sources(app PRIVATE src/unity_test.c) diff --git a/tests/lib/bluetooth/ble_scan/Kconfig b/tests/lib/bluetooth/ble_scan/Kconfig new file mode 100644 index 0000000000..f25a9362a2 --- /dev/null +++ b/tests/lib/bluetooth/ble_scan/Kconfig @@ -0,0 +1,6 @@ +# Clear dependencies for BLE_SCAN and enable it to allow +# testing the features without enabling the library. +config BLE_SCAN + default y + +source "Kconfig.zephyr" diff --git a/tests/lib/bluetooth/ble_scan/evt_obs.ld b/tests/lib/bluetooth/ble_scan/evt_obs.ld new file mode 100644 index 0000000000..ba91196245 --- /dev/null +++ b/tests/lib/bluetooth/ble_scan/evt_obs.ld @@ -0,0 +1 @@ +ITERABLE_SECTION_ROM(nrf_sdh_ble_evt_observers, 4) diff --git a/tests/lib/bluetooth/ble_scan/prj.conf b/tests/lib/bluetooth/ble_scan/prj.conf new file mode 100644 index 0000000000..b3c153cf9e --- /dev/null +++ b/tests/lib/bluetooth/ble_scan/prj.conf @@ -0,0 +1,23 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +CONFIG_UNITY=y + +CONFIG_BLE_SCAN_BUFFER_SIZE=31 +CONFIG_BLE_SCAN_NAME_MAX_LEN=32 +CONFIG_BLE_SCAN_SHORT_NAME_MAX_LEN=32 +CONFIG_BLE_SCAN_NAME_COUNT=1 +CONFIG_BLE_SCAN_APPEARANCE_COUNT=1 +CONFIG_BLE_SCAN_ADDRESS_COUNT=1 +CONFIG_BLE_SCAN_SHORT_NAME_COUNT=2 +CONFIG_BLE_SCAN_UUID_COUNT=1 +CONFIG_BLE_SCAN_INTERVAL=160 +CONFIG_BLE_SCAN_DURATION=0 +CONFIG_BLE_SCAN_WINDOW=80 +CONFIG_BLE_SCAN_SLAVE_LATENCY=0 +CONFIG_BLE_SCAN_MIN_CONNECTION_INTERVAL=6 +CONFIG_BLE_SCAN_MAX_CONNECTION_INTERVAL=24 +CONFIG_BLE_SCAN_SUPERVISION_TIMEOUT=3200 +CONFIG_BLE_SCAN_FILTER=y diff --git a/tests/lib/bluetooth/ble_scan/src/unity_test.c b/tests/lib/bluetooth/ble_scan/src/unity_test.c new file mode 100644 index 0000000000..c73a6c7dc6 --- /dev/null +++ b/tests/lib/bluetooth/ble_scan/src/unity_test.c @@ -0,0 +1,994 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "cmock_ble_gap.h" +#include "cmock_ble_gatts.h" +#include "cmock_ble_gattc.h" +#include "cmock_nrf_sdh_ble.h" +#include "cmock_ble_adv_data.h" + +#define CONN_HANDLE 1 +#define DEVICE_NAME "my_device" + +#define UUID 0x2312 + +BLE_SCAN_DEF(ble_scan); + +static struct ble_scan_evt scan_event; +static struct ble_scan_evt scan_event_prev; + +/* Invoke the BLE event handlers in ble_conn_params, passing BLE event 'evt'. */ +void ble_evt_send(const ble_evt_t *evt) +{ + TYPE_SECTION_FOREACH(struct nrf_sdh_ble_evt_observer, nrf_sdh_ble_evt_observers, obs) { + obs->handler(evt, obs->context); + } +} + +void scan_event_handler_func(struct ble_scan_evt const *scan_evt) +{ + scan_event_prev = scan_event; + scan_event = *scan_evt; +} + +void test_ble_scan_init_error_null(void) +{ + uint32_t nrf_err; + + struct ble_scan_config scan_cfg = { + .evt_handler = scan_event_handler_func, + }; + + nrf_err = ble_scan_init(NULL, NULL); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); + + nrf_err = ble_scan_init(&ble_scan, NULL); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); + + nrf_err = ble_scan_init(NULL, &scan_cfg); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); +} + +void test_ble_scan_init(void) +{ + uint32_t nrf_err; + + struct ble_scan_config scan_cfg = { + .evt_handler = scan_event_handler_func, + }; + + struct ble_scan_config scan_cfg_no_handler = { + .evt_handler = scan_event_handler_func, + }; + + struct ble_scan_config scan_cfg_with_params = { + .scan_params = { + .extended = 1, + .report_incomplete_evts = 1, + .active = 1, + .filter_policy = BLE_GAP_SCAN_FP_ACCEPT_ALL, + .scan_phys = BLE_GAP_PHY_1MBPS, + .interval = CONFIG_BLE_SCAN_INTERVAL, + .window = CONFIG_BLE_SCAN_WINDOW, + .timeout = CONFIG_BLE_SCAN_DURATION, + .channel_mask = {1, 1, 1, 1, 1}, + }, + .conn_params = { + .min_conn_interval = 1, + .max_conn_interval = 10, + .slave_latency = CONFIG_BLE_SCAN_SLAVE_LATENCY, + .conn_sup_timeout = BLE_GAP_CP_CONN_SUP_TIMEOUT_MIN, + }, + .evt_handler = scan_event_handler_func, + }; + + nrf_err = ble_scan_init(&ble_scan, &scan_cfg); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + nrf_err = ble_scan_init(&ble_scan, &scan_cfg_no_handler); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + nrf_err = ble_scan_init(&ble_scan, &scan_cfg_with_params); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_start_error_null(void) +{ + uint32_t nrf_err; + + test_ble_scan_init(); + + nrf_err = ble_scan_start(NULL); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); +} + +void test_ble_scan_start(void) +{ + uint32_t nrf_err; + + test_ble_scan_init(); + + __cmock_sd_ble_gap_scan_stop_IgnoreAndReturn(NRF_SUCCESS); + __cmock_sd_ble_gap_scan_start_ExpectAndReturn(&ble_scan.scan_params, + &ble_scan.scan_buffer, NRF_SUCCESS); + nrf_err = ble_scan_start(&ble_scan); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_filters_enable_error_invalid_param(void) +{ + uint32_t nrf_err; + + test_ble_scan_init(); + + nrf_err = ble_scan_filters_enable(&ble_scan, 0x20, true); + TEST_ASSERT_EQUAL(NRF_ERROR_INVALID_PARAM, nrf_err); +} + +void test_ble_scan_filters_enable_error_null(void) +{ + uint32_t nrf_err; + + test_ble_scan_init(); + + nrf_err = ble_scan_filters_enable(NULL, BLE_SCAN_ADDR_FILTER, true); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); +} + +void test_ble_scan_filters_enable_all(void) +{ + uint32_t nrf_err; + + test_ble_scan_init(); + + nrf_err = ble_scan_filters_enable(&ble_scan, (BLE_SCAN_NAME_FILTER | + BLE_SCAN_SHORT_NAME_FILTER | + BLE_SCAN_ADDR_FILTER | + BLE_SCAN_UUID_FILTER | + BLE_SCAN_APPEARANCE_FILTER), true); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_filters_disable_error_null(void) +{ + uint32_t nrf_err; + + test_ble_scan_init(); + + nrf_err = ble_scan_filters_disable(NULL); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); +} + +void test_ble_scan_filters_disable(void) +{ + uint32_t nrf_err; + + test_ble_scan_init(); + + nrf_err = ble_scan_filters_disable(&ble_scan); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_filter_set_get(void) +{ + uint32_t nrf_err; + + struct ble_scan_filters ble_scan_filter_data = {0}; + char *device_name = "generic_device"; + + test_ble_scan_init(); + + nrf_err = ble_scan_filter_get(&ble_scan, &ble_scan_filter_data); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + TEST_ASSERT_EQUAL(0, ble_scan_filter_data.name_filter.name_cnt); + TEST_ASSERT_FALSE(ble_scan_filter_data.name_filter.name_filter_enabled); + + ble_scan_filters_enable(&ble_scan, (BLE_SCAN_NAME_FILTER | + BLE_SCAN_SHORT_NAME_FILTER | + BLE_SCAN_ADDR_FILTER | + BLE_SCAN_UUID_FILTER | + BLE_SCAN_APPEARANCE_FILTER), true); + + nrf_err = ble_scan_filter_get(&ble_scan, &ble_scan_filter_data); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + TEST_ASSERT_EQUAL(0, ble_scan_filter_data.name_filter.name_cnt); + TEST_ASSERT_TRUE(ble_scan_filter_data.name_filter.name_filter_enabled); + + ble_scan_filter_set(&ble_scan, BLE_SCAN_NAME_FILTER, device_name); + + nrf_err = ble_scan_filter_get(&ble_scan, &ble_scan_filter_data); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + TEST_ASSERT_EQUAL(1, ble_scan_filter_data.name_filter.name_cnt); + TEST_ASSERT_TRUE(ble_scan_filter_data.name_filter.name_filter_enabled); + TEST_ASSERT_EQUAL_MEMORY(device_name, ble_scan_filter_data.name_filter.target_name[0], + sizeof(device_name)); +} + +void test_ble_scan_filter_set_error_null(void) +{ + uint32_t nrf_err; + char *device_name = "generic_device"; + + test_ble_scan_init(); + + ble_scan_filters_enable(&ble_scan, (BLE_SCAN_NAME_FILTER), true); + + nrf_err = ble_scan_filter_set(NULL, (BLE_SCAN_NAME_FILTER), device_name); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); + + nrf_err = ble_scan_filter_set(&ble_scan, (BLE_SCAN_NAME_FILTER), NULL); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); +} + +void test_ble_scan_filter_set_error_invalid_param(void) +{ + uint32_t nrf_err; + char *device_name = DEVICE_NAME; + + test_ble_scan_init(); + + ble_scan_filters_enable(&ble_scan, BLE_SCAN_NAME_FILTER, true); + + nrf_err = ble_scan_filter_set(&ble_scan, 0, device_name); + TEST_ASSERT_EQUAL(NRF_ERROR_INVALID_PARAM, nrf_err); +} + +void test_ble_scan_filter_set_name(void) +{ + uint32_t nrf_err; + char *device_name = DEVICE_NAME; + + test_ble_scan_init(); + + ble_scan_filters_enable(&ble_scan, BLE_SCAN_NAME_FILTER, true); + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_NAME_FILTER, device_name); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_filter_set_addr(void) +{ + uint32_t nrf_err; + uint8_t addr[BLE_GAP_ADDR_LEN] = {0xa, 0xd, 0xd, 0x4, 0xe, 0x5}; + + test_ble_scan_init(); + + ble_scan_filters_enable(&ble_scan, BLE_SCAN_ADDR_FILTER, true); + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_ADDR_FILTER, addr); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_filter_set_addr_enomem(void) +{ + uint32_t nrf_err; + uint8_t addr[BLE_GAP_ADDR_LEN] = {0xa, 0xd, 0xd, 0x4, 0xe, 0x5}; + + test_ble_scan_filter_set_addr(); + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_ADDR_FILTER, addr); + TEST_ASSERT_EQUAL(NRF_ERROR_NO_MEM, nrf_err); +} + +void test_ble_scan_filter_set_uuid(void) +{ + uint32_t nrf_err; + ble_uuid_t uuid = { + .uuid = UUID, + .type = BLE_UUID_TYPE_BLE, + }; + + test_ble_scan_init(); + + ble_scan_filters_enable(&ble_scan, BLE_SCAN_UUID_FILTER, true); + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_UUID_FILTER, &uuid); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_filter_set_uuid_error_no_mem(void) +{ + uint32_t nrf_err; + ble_uuid_t uuid; + + test_ble_scan_filter_set_uuid(); + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_UUID_FILTER, &uuid); + TEST_ASSERT_EQUAL(NRF_ERROR_NO_MEM, nrf_err); +} + +void test_ble_scan_filter_set_appearance(void) +{ + uint32_t nrf_err; + uint16_t appearance = 0xa44e; + + test_ble_scan_init(); + + ble_scan_filters_enable(&ble_scan, BLE_SCAN_APPEARANCE_FILTER, true); + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_APPEARANCE_FILTER, &appearance); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_filter_set_appearance_error_no_mem(void) +{ + uint32_t nrf_err; + uint16_t appearance = 0xa44e; + + test_ble_scan_filter_set_appearance(); + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_APPEARANCE_FILTER, &appearance); + TEST_ASSERT_EQUAL(NRF_ERROR_NO_MEM, nrf_err); +} + +void test_ble_scan_filter_set_short_name(void) +{ + uint32_t nrf_err; + struct ble_scan_short_name short_name = { + .short_name = "dev", + .short_name_min_len = 2, + }; + + test_ble_scan_init(); + + ble_scan_filters_enable(&ble_scan, BLE_SCAN_SHORT_NAME_FILTER, true); + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_SHORT_NAME_FILTER, &short_name); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_filter_set_short_name_error_no_mem(void) +{ + uint32_t nrf_err; + struct ble_scan_short_name short_name = { + .short_name = "dev", + .short_name_min_len = 2, + }; + struct ble_scan_short_name short_name2 = { + .short_name = "dev2", + .short_name_min_len = 2, + }; + + test_ble_scan_filter_set_short_name(); + + /* duplicate filter does not increase count, so the next will also succeed. */ + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_SHORT_NAME_FILTER, &short_name); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + /* We accept two short names so the second will succeed */ + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_SHORT_NAME_FILTER, &short_name2); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_SHORT_NAME_FILTER, &short_name2); + TEST_ASSERT_EQUAL(NRF_ERROR_NO_MEM, nrf_err); + +} + +void test_is_whitelist_used(void) +{ + bool nrf_err; + + nrf_err = is_whitelist_used(&ble_scan); + TEST_ASSERT_FALSE(nrf_err); + + ble_scan.scan_params.filter_policy = BLE_GAP_SCAN_FP_WHITELIST; + + nrf_err = is_whitelist_used(&ble_scan); + TEST_ASSERT_TRUE(nrf_err); +} + +void test_ble_scan_all_filter_remove(void) +{ + uint32_t nrf_err; + + nrf_err = ble_scan_all_filter_remove(&ble_scan); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_params_set_error_null(void) +{ + uint32_t nrf_err; + ble_gap_scan_params_t scan_params = BLE_SCAN_SCAN_PARAMS_DEFAULT; + + nrf_err = ble_scan_params_set(NULL, NULL); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); + + nrf_err = ble_scan_params_set(&ble_scan, NULL); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); + + nrf_err = ble_scan_params_set(NULL, &scan_params); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); + +} + +void test_ble_scan_params_set(void) +{ + uint32_t nrf_err; + ble_gap_scan_params_t scan_params = BLE_SCAN_SCAN_PARAMS_DEFAULT; + + __cmock_sd_ble_gap_scan_stop_IgnoreAndReturn(NRF_SUCCESS); + nrf_err = ble_scan_params_set(&ble_scan, &scan_params); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_copy_addr_to_sd_gap_addr_error_null(void) +{ + uint32_t nrf_err; + + nrf_err = ble_scan_copy_addr_to_sd_gap_addr(NULL, 0); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); +} + +void test_ble_scan_copy_addr_to_sd_gap_addr(void) +{ + uint32_t nrf_err; + uint8_t address[BLE_GAP_ADDR_LEN] = {0}; + ble_gap_addr_t gap_address = {0}; + + nrf_err = ble_scan_copy_addr_to_sd_gap_addr(&gap_address, + address); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_on_ble_evt_adv_report_empty(void) +{ + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = {}, + }, + }; + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn( + NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_evt_send(&ble_evt); + + test_ble_scan_init(); +} + +void test_ble_scan_on_ble_evt_adv_report_device_name_bad_data(void) +{ + char bad_data[] = "baddata"; + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .data = { + .p_data = bad_data, + .len = sizeof(bad_data), + }, + + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_name(); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn( + NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_NOT_FOUND, scan_event.evt_type); + +} + +void test_ble_scan_on_ble_evt_adv_report_device_address_not_found(void) +{ + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .peer_addr = { + .addr = {0xb, 0xa, 0xd, 0x4, 0xd, 0xd}, + }, + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_addr(); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn( + NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_NOT_FOUND, scan_event.evt_type); +} + +void test_ble_scan_on_ble_evt_adv_report_device_address(void) +{ + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .peer_addr = { + .addr = {0xa, 0xd, 0xd, 0x4, 0xe, 0x5}, + }, + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_addr(); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn( + NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_FILTER_MATCH, scan_event.evt_type); + TEST_ASSERT_EQUAL(1, scan_event.params.filter_match.filter_match.address_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.name_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.short_name_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.appearance_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, + scan_event.params.filter_match.adv_report); +} + +void test_ble_scan_on_ble_evt_adv_report_device_name_not_found(void) +{ + uint8_t device_name_data[] = { + 10, BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME, + 'n', 'o', 't', '_', 'm', 'y', '_', 'd', 'e', 'v', 'i', 'c', 'e', + }; + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .data = { + .p_data = device_name_data, + .len = sizeof(device_name_data), + }, + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_name(); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn( + NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_NOT_FOUND, scan_event.evt_type); +} + +void test_ble_scan_on_ble_evt_adv_report_device_name(void) +{ + uint8_t device_name_data[] = { + 10, BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME, + 'm', 'y', '_', 'd', 'e', 'v', 'i', 'c', 'e', + }; + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .data = { + .p_data = device_name_data, + .len = sizeof(device_name_data), + }, + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_name(); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn( + NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_FILTER_MATCH, scan_event.evt_type); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.address_filter_match); + TEST_ASSERT_EQUAL(1, scan_event.params.filter_match.filter_match.name_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.short_name_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.appearance_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, + scan_event.params.filter_match.adv_report); +} + +void test_ble_scan_on_ble_evt_adv_report_device_short_name_not_found(void) +{ + static const char short_name_exp[] = "dev"; + const uint8_t min_len_exp = 2; + uint8_t dummy_data[] = "hello"; + + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .data = { + .p_data = dummy_data, + .len = sizeof(dummy_data), + }, + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_short_name(); + + __cmock_ble_adv_data_short_name_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + short_name_exp, min_len_exp, false); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn( + NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_NOT_FOUND, scan_event.evt_type); +} + +void test_ble_scan_on_ble_evt_adv_report_device_short_name(void) +{ + static const char short_name_exp[] = "dev"; + const uint8_t min_len_exp = 2; + uint8_t dummy_data[] = "hello"; + + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .data = { + .p_data = dummy_data, + .len = sizeof(dummy_data), + }, + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_short_name(); + + __cmock_ble_adv_data_short_name_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + short_name_exp, min_len_exp, true); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn( + NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_FILTER_MATCH, scan_event.evt_type); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.address_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.name_filter_match); + TEST_ASSERT_EQUAL(1, scan_event.params.filter_match.filter_match.short_name_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.appearance_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, + scan_event.params.filter_match.adv_report); +} + +void test_ble_scan_on_ble_evt_adv_report_device_appearance_not_found(void) +{ + uint16_t appearance_exp = 0xa44e; + uint8_t dummy_data[] = "hello"; + + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .data = { + .p_data = dummy_data, + .len = sizeof(dummy_data), + }, + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_appearance(); + + __cmock_ble_adv_data_appearance_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + &appearance_exp, 1, false); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn(NULL, &ble_scan.scan_buffer, + NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_NOT_FOUND, scan_event.evt_type); +} + +void test_ble_scan_on_ble_evt_adv_report_device_appearance(void) +{ + uint16_t appearance_exp = 0xa44e; + uint8_t dummy_data[] = "hello"; + + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .data = { + .p_data = dummy_data, + .len = sizeof(dummy_data), + }, + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_appearance(); + + __cmock_ble_adv_data_appearance_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + &appearance_exp, 1, true); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn(NULL, &ble_scan.scan_buffer, + NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_FILTER_MATCH, scan_event.evt_type); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.address_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.name_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.short_name_filter_match); + TEST_ASSERT_EQUAL(1, scan_event.params.filter_match.filter_match.appearance_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, + scan_event.params.filter_match.adv_report); +} + +void test_ble_scan_on_ble_evt_adv_report_device_uuid_not_found(void) +{ + const ble_uuid_t uuid_exp = { + .uuid = UUID, + .type = BLE_UUID_TYPE_BLE, + }; + uint8_t dummy_data[] = "hello"; + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .data = { + .p_data = dummy_data, + .len = sizeof(dummy_data), + }, + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_uuid(); + + __cmock_ble_adv_data_uuid_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + &uuid_exp, 1, false); + /* Size of ble_uuid_t is 4, though only 3 is used, so last byte will fail to compare. */ + __cmock_ble_adv_data_uuid_find_IgnoreArg_uuid(); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn(NULL, &ble_scan.scan_buffer, + NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_NOT_FOUND, scan_event.evt_type); +} + +void test_ble_scan_on_ble_evt_adv_report_device_uuid(void) +{ + const ble_uuid_t uuid_exp = { + .uuid = UUID, + .type = BLE_UUID_TYPE_BLE, + }; + uint8_t dummy_data[] = "hello"; + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .data = { + .p_data = dummy_data, + .len = sizeof(dummy_data), + }, + }, + }, + }; + + test_ble_scan_init(); + test_ble_scan_filter_set_uuid(); + + __cmock_ble_adv_data_uuid_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + &uuid_exp, 1, true); + /* Size of ble_uuid_t is 4, though only 3 is used, so last byte will fail to compare. */ + __cmock_ble_adv_data_uuid_find_IgnoreArg_uuid(); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn(NULL, &ble_scan.scan_buffer, + NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_FILTER_MATCH, scan_event.evt_type); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.address_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.name_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.short_name_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.params.filter_match.filter_match.appearance_filter_match); + TEST_ASSERT_EQUAL(1, scan_event.params.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, + scan_event.params.filter_match.adv_report); +} + +void test_ble_scan_on_ble_evt_adv_report_device_uuid_connect(void) +{ + uint32_t nrf_err; + ble_uuid_t uuid = { + .uuid = UUID, + .type = BLE_UUID_TYPE_BLE, + }; + const ble_uuid_t uuid_exp = { + .uuid = UUID, + .type = BLE_UUID_TYPE_BLE, + }; + uint8_t dummy_data[] = "hello"; + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_ADV_REPORT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.adv_report = { + .data = { + .p_data = dummy_data, + .len = sizeof(dummy_data), + }, + .peer_addr = { + .addr = {0xa, 0xd, 0xd, 0x4, 0xe, 0x5}, + .addr_type = BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE, + } + }, + }, + }; + + struct ble_scan_config scan_cfg_with_params = { + .scan_params = { + .extended = 1, + .report_incomplete_evts = 1, + .active = 1, + .filter_policy = BLE_GAP_SCAN_FP_ACCEPT_ALL, + .scan_phys = BLE_GAP_PHY_1MBPS, + .interval = CONFIG_BLE_SCAN_INTERVAL, + .window = CONFIG_BLE_SCAN_WINDOW, + .timeout = CONFIG_BLE_SCAN_DURATION, + .channel_mask = {1, 1, 1, 1, 1}, + }, + .conn_params = { + .min_conn_interval = 1, + .max_conn_interval = 10, + .slave_latency = CONFIG_BLE_SCAN_SLAVE_LATENCY, + .conn_sup_timeout = BLE_GAP_CP_CONN_SUP_TIMEOUT_MIN, + }, + .evt_handler = scan_event_handler_func, + .connect_if_match = true, + .conn_cfg_tag = 5, + }; + + nrf_err = ble_scan_init(&ble_scan, &scan_cfg_with_params); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + ble_scan_filters_enable(&ble_scan, BLE_SCAN_UUID_FILTER, true); + + nrf_err = ble_scan_filter_set(&ble_scan, BLE_SCAN_UUID_FILTER, &uuid); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + __cmock_ble_adv_data_uuid_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + &uuid_exp, 1, true); + /* Size of ble_uuid_t is 4, though only 3 is used, so last byte will fail to compare. */ + __cmock_ble_adv_data_uuid_find_IgnoreArg_uuid(); + + __cmock_sd_ble_gap_scan_stop_ExpectAndReturn(NRF_SUCCESS); + __cmock_sd_ble_gap_connect_ExpectWithArrayAndReturn( + &ble_evt.evt.gap_evt.params.adv_report.peer_addr, 1, + &scan_cfg_with_params.scan_params, 1, + &scan_cfg_with_params.conn_params, 1, + scan_cfg_with_params.conn_cfg_tag, NRF_SUCCESS); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn(NULL, &ble_scan.scan_buffer, + NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_FILTER_MATCH, scan_event.evt_type); + + __cmock_ble_adv_data_uuid_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + &uuid_exp, 1, true); + /* Size of ble_uuid_t is 4, though only 3 is used, so last byte will fail to compare. */ + __cmock_ble_adv_data_uuid_find_IgnoreArg_uuid(); + + __cmock_sd_ble_gap_scan_stop_ExpectAndReturn(NRF_SUCCESS); + __cmock_sd_ble_gap_connect_ExpectWithArrayAndReturn( + &ble_evt.evt.gap_evt.params.adv_report.peer_addr, 1, + &scan_cfg_with_params.scan_params, 1, + &scan_cfg_with_params.conn_params, 1, + scan_cfg_with_params.conn_cfg_tag, NRF_ERROR_BUSY); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn(NULL, &ble_scan.scan_buffer, + NRF_SUCCESS); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_CONNECTING_ERROR, scan_event_prev.evt_type); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_FILTER_MATCH, scan_event.evt_type); +} + +void test_ble_scan_on_ble_evt_timeout(void) +{ + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_TIMEOUT, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.timeout = { + .src = BLE_GAP_TIMEOUT_SRC_SCAN, + }, + }, + }; + + test_ble_scan_init(); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_SCAN_TIMEOUT, scan_event.evt_type); + TEST_ASSERT_EQUAL(BLE_GAP_TIMEOUT_SRC_SCAN, scan_event.params.timeout.src); +} + +void test_ble_scan_on_ble_evt_connected(void) +{ + ble_evt_t ble_evt = { + .header.evt_id = BLE_GAP_EVT_CONNECTED, + .evt.gap_evt = { + .conn_handle = CONN_HANDLE, + .params.connected = { + .role = 1, + }, + }, + }; + + test_ble_scan_init(); + + ble_evt_send(&ble_evt); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_CONNECTED, scan_event.evt_type); + TEST_ASSERT_EQUAL(1, scan_event.params.connected.connected->role); + TEST_ASSERT_EQUAL(CONN_HANDLE, scan_event.params.connected.conn_handle); +} + +void setUp(void) +{ + memset(&ble_scan, 0, sizeof(ble_scan)); +} + +void tearDown(void) +{ +} + +extern int unity_main(void); + +int main(void) +{ + return unity_main(); +} diff --git a/tests/lib/bluetooth/ble_scan/testcase.yaml b/tests/lib/bluetooth/ble_scan/testcase.yaml new file mode 100644 index 0000000000..6b52d2e9d9 --- /dev/null +++ b/tests/lib/bluetooth/ble_scan/testcase.yaml @@ -0,0 +1,4 @@ +tests: + lib.ble_scan: + platform_allow: native_sim + tags: unittest