From 7ff338d37cd612cb0a75cb36e640dfc0bc94b24f Mon Sep 17 00:00:00 2001 From: Martynas Smilingis Date: Tue, 28 Apr 2026 16:06:58 +0200 Subject: [PATCH 1/3] bluetooth: ble_adv_data: add manufacturer data find helper Add ble_adv_data_manufacturer_data_find() to locate manufacturer-specific data in an advertising payload and prefix-match it against a target value. Follows the pattern of existing _find helpers for name, uuid and appearance. Modified unit tests to align with changes. Added changelog. Signed-off-by: Martynas Smilingis --- .../release_notes/release_notes_changelog.rst | 4 + include/bm/bluetooth/ble_adv_data.h | 17 ++ lib/bluetooth/ble_adv/ble_adv_data.c | 29 +++ .../bluetooth/ble_adv_data/src/unity_test.c | 180 +++++++++++++++++- 4 files changed, 228 insertions(+), 2 deletions(-) diff --git a/doc/nrf-bm/release_notes/release_notes_changelog.rst b/doc/nrf-bm/release_notes/release_notes_changelog.rst index 6e6ef0adff..a051675a7e 100644 --- a/doc/nrf-bm/release_notes/release_notes_changelog.rst +++ b/doc/nrf-bm/release_notes/release_notes_changelog.rst @@ -81,6 +81,10 @@ Libraries * An issue where the :c:func:`ble_conn_params_phy_radio_mode_get` function would incorrectly return the PHY mode mask of a pending update rather than the currently active PHY mode if a PHY update initiated by the :c:func:`ble_conn_params_phy_radio_mode_set` function was still in progress. * An issue where the SoftDevice define :c:macro:`BLE_GAP_PHYS_SUPPORTED` was used instead of the PHY preferences set with Kconfig when initiating or responding to a PHY update procedure. +* :ref:`lib_ble_adv` library: + + * Added the :c:func:`ble_adv_data_manufacturer_data_find` function to locate manufacturer-specific data in an advertising payload and prefix-match it against a target value. + Bluetooth LE Services --------------------- diff --git a/include/bm/bluetooth/ble_adv_data.h b/include/bm/bluetooth/ble_adv_data.h index 0fde8953b2..a5272f5108 100644 --- a/include/bm/bluetooth/ble_adv_data.h +++ b/include/bm/bluetooth/ble_adv_data.h @@ -308,6 +308,23 @@ bool ble_adv_data_uuid_find(const uint8_t *buf, uint16_t len, const ble_uuid_t * */ bool ble_adv_data_appearance_find(const uint8_t *buf, uint16_t len, const uint16_t *appearance); +/** + * @brief Search encoded Advertising data for manufacturer-specific data. + * + * @param[in] buf Encoded advertising data. + * @param[in] len Buffer length. + * @param[in] target_data Target manufacturer data to match (prefix). Starts with the + * 2-byte little-endian company identifier followed by optional + * vendor payload. + * @param[in] target_data_len Length of the target manufacturer data. + * + * @retval true If manufacturer-specific data in which the first @p target_data_len bytes matches + * @p target_data was found among @p buf. + * @retval false If no match was found among @p buf, or if @p buf or @p target_data was @c NULL. + */ +bool ble_adv_data_manufacturer_data_find(const uint8_t *buf, uint16_t len, + const uint8_t *target_data, uint8_t target_data_len); + #ifdef __cplusplus } #endif diff --git a/lib/bluetooth/ble_adv/ble_adv_data.c b/lib/bluetooth/ble_adv/ble_adv_data.c index 8faa7635c2..1d0f1ed963 100644 --- a/lib/bluetooth/ble_adv/ble_adv_data.c +++ b/lib/bluetooth/ble_adv/ble_adv_data.c @@ -742,3 +742,32 @@ bool ble_adv_data_appearance_find(const uint8_t *data, uint16_t data_len, return false; } + +bool ble_adv_data_manufacturer_data_find(const uint8_t *data, uint16_t data_len, + const uint8_t *target_data, uint8_t target_data_len) +{ + uint16_t data_offset; + uint16_t parsed_len; + + if (!data || !target_data || target_data_len == 0) { + return false; + } + + data_offset = 0; + parsed_len = ble_adv_data_search(data, data_len, &data_offset, + BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA); + + if (data_offset == 0 || parsed_len == 0) { + return false; + } + + if (target_data_len > parsed_len) { + return false; + } + + if (memcmp(&data[data_offset], target_data, target_data_len) == 0) { + return true; + } + + return false; +} diff --git a/tests/unit/lib/bluetooth/ble_adv_data/src/unity_test.c b/tests/unit/lib/bluetooth/ble_adv_data/src/unity_test.c index 758ad5e5c7..e72a2d4892 100644 --- a/tests/unit/lib/bluetooth/ble_adv_data/src/unity_test.c +++ b/tests/unit/lib/bluetooth/ble_adv_data/src/unity_test.c @@ -196,11 +196,17 @@ static void encode_128bit_uuid_test_data(void) */ uint32_t nrf_err; ble_uuid_t uuids[] = { - {.uuid = TEST_UUID_128_VAL, .type = TEST_UUID_128_TYPE}, + { + .uuid = TEST_UUID_128_VAL, + .type = TEST_UUID_128_TYPE + }, }; struct ble_adv_data adv_data = { .flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE, - .uuid_lists.complete = {.uuid = uuids, .len = 1}, + .uuid_lists.complete = { + .uuid = uuids, + .len = 1 + }, }; __cmock_sd_ble_uuid_encode_Stub(stub_uuid_encode); @@ -209,6 +215,30 @@ static void encode_128bit_uuid_test_data(void) TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); } +static void encode_manufacturer_test_data(const uint8_t *payload, uint8_t payload_len) +{ + /* + * Separate from common_test_data_encode() because that does not encode a + * manufacturer-specific data AD field, which is required to test + * ble_adv_data_manufacturer_data_find(). This encodes the Nordic company + * identifier followed by an optional caller-provided payload as a + * MANUFACTURER_SPECIFIC_DATA AD field. + */ + uint32_t nrf_err; + struct ble_adv_data_manufacturer manuf = { + .company_identifier = BLE_COMPANY_ID_NORDIC, + .data = (uint8_t *)payload, + .len = payload_len, + }; + struct ble_adv_data adv_data = { + .flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE, + .manufacturer_data = &manuf, + }; + + nrf_err = ble_adv_data_encode(&adv_data, buf, &len); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + /* ble_adv_data_encode() => Generic Unit Tests */ void test_ble_adv_data_encode_error_null(void) { @@ -1944,6 +1974,152 @@ void test_ble_adv_data_appearance_find_zero_length_data(void) TEST_ASSERT_FALSE(found); } +/* ble_adv_data_manufacturer_data_find() Unit Tests */ +void test_ble_adv_data_manufacturer_data_find_company_id_only(void) +{ + /* Encoded manufacturer data contains only the company ID, + * search with the same 2-byte company ID matches. + */ + bool found; + const uint8_t target[] = {0x59, 0x00}; /* Nordic company ID, little-endian */ + + encode_manufacturer_test_data(NULL, 0); + + found = ble_adv_data_manufacturer_data_find(buf, len, target, sizeof(target)); + TEST_ASSERT_TRUE(found); +} + +void test_ble_adv_data_manufacturer_data_find_full_match(void) +{ + /* Exact match of company ID + full payload. */ + bool found; + const uint8_t payload[] = {0xDE, 0xAD, 0xBE, 0xEF}; + const uint8_t target[] = {0x59, 0x00, 0xDE, 0xAD, 0xBE, 0xEF}; + + encode_manufacturer_test_data(payload, sizeof(payload)); + + found = ble_adv_data_manufacturer_data_find(buf, len, target, sizeof(target)); + TEST_ASSERT_TRUE(found); +} + +void test_ble_adv_data_manufacturer_data_find_prefix_match(void) +{ + /* Target is a prefix of the encoded manufacturer data, prefix match succeeds. */ + bool found; + const uint8_t payload[] = {0xDE, 0xAD, 0xBE, 0xEF}; + const uint8_t target[] = {0x59, 0x00, 0xDE, 0xAD}; + + encode_manufacturer_test_data(payload, sizeof(payload)); + + found = ble_adv_data_manufacturer_data_find(buf, len, target, sizeof(target)); + TEST_ASSERT_TRUE(found); +} + +void test_ble_adv_data_manufacturer_data_find_wrong_company_id(void) +{ + /* Different company ID, no match. */ + bool found; + const uint8_t payload[] = {0xDE, 0xAD}; + const uint8_t target[] = {0x4C, 0x00, 0xDE, 0xAD}; /* Random company ID */ + + encode_manufacturer_test_data(payload, sizeof(payload)); + + found = ble_adv_data_manufacturer_data_find(buf, len, target, sizeof(target)); + TEST_ASSERT_FALSE(found); +} + +void test_ble_adv_data_manufacturer_data_find_wrong_payload(void) +{ + /* Company ID matches but payload differs. */ + bool found; + const uint8_t payload[] = {0xDE, 0xAD, 0xBE, 0xEF}; + const uint8_t target[] = {0x59, 0x00, 0x12, 0x34}; + + encode_manufacturer_test_data(payload, sizeof(payload)); + + found = ble_adv_data_manufacturer_data_find(buf, len, target, sizeof(target)); + TEST_ASSERT_FALSE(found); +} + +void test_ble_adv_data_manufacturer_data_find_target_longer_than_encoded(void) +{ + /* Target length exceeds encoded manufacturer data length. */ + bool found; + const uint8_t payload[] = {0xDE, 0xAD}; + const uint8_t target[] = {0x59, 0x00, 0xDE, 0xAD, 0xBE, 0xEF}; + + encode_manufacturer_test_data(payload, sizeof(payload)); + + found = ble_adv_data_manufacturer_data_find(buf, len, target, sizeof(target)); + TEST_ASSERT_FALSE(found); +} + +void test_ble_adv_data_manufacturer_data_find_no_manuf_data_in_data(void) +{ + /* Encoded data has no manufacturer-specific data AD field. */ + bool found; + const uint8_t target[] = {0x59, 0x00}; + + common_test_data_encode(); + + found = ble_adv_data_manufacturer_data_find(buf, len, target, sizeof(target)); + TEST_ASSERT_FALSE(found); +} + +void test_ble_adv_data_manufacturer_data_find_null_data(void) +{ + const uint8_t target[] = {0x59, 0x00}; + + bool found = ble_adv_data_manufacturer_data_find(NULL, 10, target, sizeof(target)); + + TEST_ASSERT_FALSE(found); +} + +void test_ble_adv_data_manufacturer_data_find_null_target(void) +{ + bool found; + + encode_manufacturer_test_data(NULL, 0); + + found = ble_adv_data_manufacturer_data_find(buf, len, NULL, 2); + TEST_ASSERT_FALSE(found); +} + +void test_ble_adv_data_manufacturer_data_find_zero_target_len(void) +{ + bool found; + const uint8_t target[] = {0x59, 0x00}; + + encode_manufacturer_test_data(NULL, 0); + + found = ble_adv_data_manufacturer_data_find(buf, len, target, 0); + TEST_ASSERT_FALSE(found); +} + +void test_ble_adv_data_manufacturer_data_find_zero_length_data(void) +{ + const uint8_t target[] = {0x59, 0x00}; + + bool found = ble_adv_data_manufacturer_data_find(buf, 0, target, sizeof(target)); + + TEST_ASSERT_FALSE(found); +} + +void test_ble_adv_data_manufacturer_data_find_only_other_ad_types(void) +{ + /* Encoded data contains appearance, flags, tx_power, UUID, conn_int and name + * but no manufacturer-specific data AD field. Searching for manufacturer data + * should not match any of the other AD fields. + */ + bool found; + const uint8_t target[] = {0x59, 0x00}; + + common_test_data_encode(); + + found = ble_adv_data_manufacturer_data_find(buf, len, target, sizeof(target)); + TEST_ASSERT_FALSE(found); +} + /* Unit Test Setup */ void setUp(void) { From fafb892c735f5b41d30be4e8b3da8cedc7d44d74 Mon Sep 17 00:00:00 2001 From: Martynas Smilingis Date: Tue, 28 Apr 2026 16:15:03 +0200 Subject: [PATCH 2/3] bluetooth: ble_scan: clarify scan timing in adv report handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand the comment around the match_all cache logic to explain the on-air ADV → SCAN_REQ → SCAN_RSP exchange and why the paired SCAN_RSP is guaranteed to arrive before an ADV from another device. Also clarify the purpose of the buffer swap. No functional changes. Signed-off-by: Martynas Smilingis --- lib/bluetooth/ble_scan/ble_scan.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/bluetooth/ble_scan/ble_scan.c b/lib/bluetooth/ble_scan/ble_scan.c index 9262272bcb..dcb9e10401 100644 --- a/lib/bluetooth/ble_scan/ble_scan.c +++ b/lib/bluetooth/ble_scan/ble_scan.c @@ -683,8 +683,12 @@ static void ble_scan_on_adv_report(struct ble_scan *scan, LOG_INF("Incomplete, expecting more data!"); } - /* If active scanning where we need to match all we need the scan response for the same - * address before processing the filters. + /* Active scanning with match_all mode: filter data may be split between the ADV packet and + * the SCAN_RSP packet, so we need both before evaluating filters. + * + * Cache this ADV and swap scan buffers so the next report the SoftDevice delivers + * (the SCAN_RSP) is written into a different buffer, leaving adv_data valid until we can + * evaluate the filters. */ if (active_match_all && !adv_report->type.scan_response) { /* Store what we have as advertising data and continue for the scan response. */ From d71f75fbf3e20be52ce70128dbd42a8399d1fb12 Mon Sep 17 00:00:00 2001 From: Martynas Smilingis Date: Tue, 28 Apr 2026 16:16:31 +0200 Subject: [PATCH 3/3] bluetooth: ble_scan: add manufacturer data filter support Port the manufacturer data scan filter from NCS to Bare Metal. Adds BLE_SCAN_MANUFACTURER_DATA_FILTER type, manufacturer_data_filter_match flag in ble_scan_filter_match, and plumbing through ble_scan_filter_add, ble_scan_filters_enable, ble_scan_filters_disable, ble_scan_all_filter_remove and the adv report event handler. New Kconfig options: - CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT - CONFIG_BLE_SCAN_MANUFACTURER_DATA_MAX_LEN Modified unit tests to align with changes. Added changelog. Signed-off-by: Martynas Smilingis --- .../release_notes/release_notes_changelog.rst | 7 + include/bm/bluetooth/ble_scan.h | 36 +- lib/bluetooth/ble_scan/Kconfig | 12 + lib/bluetooth/ble_scan/ble_scan.c | 104 +++++- tests/unit/lib/bluetooth/ble_scan/prj.conf | 2 + .../lib/bluetooth/ble_scan/src/unity_test.c | 307 +++++++++++++++++- 6 files changed, 459 insertions(+), 9 deletions(-) diff --git a/doc/nrf-bm/release_notes/release_notes_changelog.rst b/doc/nrf-bm/release_notes/release_notes_changelog.rst index a051675a7e..cf6b089c6a 100644 --- a/doc/nrf-bm/release_notes/release_notes_changelog.rst +++ b/doc/nrf-bm/release_notes/release_notes_changelog.rst @@ -85,6 +85,13 @@ Libraries * Added the :c:func:`ble_adv_data_manufacturer_data_find` function to locate manufacturer-specific data in an advertising payload and prefix-match it against a target value. +* :ref:`lib_ble_scan` library: + + * Added: + + * Support for filtering by manufacturer-specific data using the :c:macro:`BLE_SCAN_MANUFACTURER_DATA_FILTER` filter type. + * The :kconfig:option:`CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT` and :kconfig:option:`CONFIG_BLE_SCAN_MANUFACTURER_DATA_MAX_LEN` Kconfig options to configure the manufacturer data filter capacity and maximum payload length. + Bluetooth LE Services --------------------- diff --git a/include/bm/bluetooth/ble_scan.h b/include/bm/bluetooth/ble_scan.h index 11c26af8fa..7e15433ada 100644 --- a/include/bm/bluetooth/ble_scan.h +++ b/include/bm/bluetooth/ble_scan.h @@ -106,6 +106,8 @@ enum ble_scan_evt_type { #define BLE_SCAN_APPEARANCE_FILTER (0x08) /** Filters the device short name. */ #define BLE_SCAN_SHORT_NAME_FILTER (0x10) +/** Filters the manufacturer data. */ +#define BLE_SCAN_MANUFACTURER_DATA_FILTER (0x20) /** @} */ /** @@ -136,6 +138,13 @@ struct ble_scan_filter_data { /** Minimum length of the short name to be matched. */ uint8_t short_name_min_len; } short_name_filter; + /** Manufacturer filter data */ + struct { + /** Pointer to the manufacturer data. */ + uint8_t *data; + /** Manufacturer data length. */ + uint8_t data_len; + } manufacturer_data_filter; }; }; @@ -153,6 +162,8 @@ struct ble_scan_filter_match { uint8_t appearance_filter_match: 1; /** Set to 1 if short name filter is matched. */ uint8_t short_name_filter_match: 1; + /** Set to 1 if manufacturer data filter is matched. */ + uint8_t manufacturer_data_filter_match: 1; }; /** @@ -291,6 +302,22 @@ struct ble_scan_appearance_filter { bool appearance_filter_enabled; }; +/** Scan manufacturer data filter. */ +struct ble_scan_manufacturer_data_filter { + struct { + /** Manufacturer data that the main application will scan for, and that will be + * advertised by the peripherals. + */ + uint8_t data[CONFIG_BLE_SCAN_MANUFACTURER_DATA_MAX_LEN]; + /** Length of the manufacturer data. */ + uint8_t data_len; + } manufacturer_data[CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT]; + /** Number of manufacturer data in list. */ + uint8_t manufacturer_data_cnt; + /** Flag to inform about enabling or disabling this filter. */ + bool manufacturer_data_filter_enabled; +}; + /** * @brief Filter data. * @@ -320,6 +347,10 @@ struct ble_scan_filters { #if CONFIG_BLE_SCAN_APPEARANCE_COUNT > 0 /** Appearance filter data. */ struct ble_scan_appearance_filter appearance_filter; +#endif +#if CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT > 0 + /** Manufacturer filter data. */ + struct ble_scan_manufacturer_data_filter manufacturer_data_filter; #endif /** Filter mode. If true, all set filters must be matched to generate an event. */ bool all_filters_mode; @@ -517,8 +548,9 @@ uint32_t ble_scan_filter_get(const struct ble_scan *scan, struct ble_scan_filter * This function adds a new filter by type @ref ble_scan_filter_type. * 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. + * CONFIG_BLE_SCAN_ADDRESS_COUNT, @ref CONFIG_BLE_SCAN_APPEARANCE_COUNT, + * or @ref BLE_SCAN_MANUFACTURER_DATA_COUNT, depending on the filter type, + * and if the same filter has not already been set. * * @param[in,out] scan Scan library instance. * @param[in] type Filter type. diff --git a/lib/bluetooth/ble_scan/Kconfig b/lib/bluetooth/ble_scan/Kconfig index 92973e3b6f..0c149553ef 100644 --- a/lib/bluetooth/ble_scan/Kconfig +++ b/lib/bluetooth/ble_scan/Kconfig @@ -30,6 +30,12 @@ config BLE_SCAN_SHORT_NAME_MAX_LEN help Maximum size of the short name to search for in the advertisement report. +config BLE_SCAN_MANUFACTURER_DATA_MAX_LEN + int "Scan manufacturer data maximum length" + default 32 + help + Maximum size for the manufacturer data to search in the advertisement report. + config BLE_SCAN_FILTER bool "Scan filter" default y @@ -68,6 +74,12 @@ config BLE_SCAN_UUID_COUNT help Maximum number of filters for UUIDs. +config BLE_SCAN_MANUFACTURER_DATA_COUNT + int "Scan manufacturer data count" + default 0 + help + Maximum number of manufacturer data filters. + endif # BLE_SCAN_FILTER config BLE_SCAN_INTERVAL diff --git a/lib/bluetooth/ble_scan/ble_scan.c b/lib/bluetooth/ble_scan/ble_scan.c index dcb9e10401..9174857127 100644 --- a/lib/bluetooth/ble_scan/ble_scan.c +++ b/lib/bluetooth/ble_scan/ble_scan.c @@ -362,6 +362,68 @@ static int appearance_filter_add(struct ble_scan *scan, const struct ble_scan_fi } #endif /* CONFIG_BLE_SCAN_APPEARANCE_COUNT */ +#if (CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT > 0) +static bool adv_manufacturer_data_compare(const struct ble_scan *scan, + uint8_t *data, uint16_t len) +{ + const struct ble_scan_manufacturer_data_filter *md_filter = + &scan->scan_filters.manufacturer_data_filter; + + /* Match the adv packet against each configured manufacturer data filter. */ + for (uint8_t i = 0; i < md_filter->manufacturer_data_cnt; i++) { + if (ble_adv_data_manufacturer_data_find(data, len, + md_filter->manufacturer_data[i].data, + md_filter->manufacturer_data[i].data_len)) { + return true; + } + } + return false; +} + +static int manufacturer_data_filter_add(struct ble_scan *scan, + const struct ble_scan_filter_data *data) +{ + struct ble_scan_manufacturer_data_filter *md_filter = + &scan->scan_filters.manufacturer_data_filter; + uint8_t *counter = &md_filter->manufacturer_data_cnt; + uint8_t md_len = data->manufacturer_data_filter.data_len; + uint8_t *md_data = data->manufacturer_data_filter.data; + + /* Validate length. */ + if ((md_len == 0) || (md_len > CONFIG_BLE_SCAN_MANUFACTURER_DATA_MAX_LEN)) { + return NRF_ERROR_DATA_SIZE; + } + + /* Validate md_data. */ + if (md_data == NULL) { + return NRF_ERROR_NULL; + } + + /* Check for duplicated filter. */ + for (uint8_t i = 0; i < *counter; i++) { + if ((md_filter->manufacturer_data[i].data_len == md_len) && + (memcmp(md_filter->manufacturer_data[i].data, md_data, md_len) == 0)) { + return NRF_SUCCESS; + } + } + + /* Check for free slot. */ + if (*counter >= CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT) { + return NRF_ERROR_NO_MEM; + } + + /* Add manufacturer data to filter. */ + memcpy(md_filter->manufacturer_data[*counter].data, md_data, md_len); + md_filter->manufacturer_data[*counter].data_len = md_len; + (*counter)++; + + LOG_DBG("Added manufacturer data filter"); + LOG_HEXDUMP_DBG(md_data, md_len, "Manufacturer data"); + + return NRF_SUCCESS; +} +#endif /* CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT */ + uint32_t ble_scan_filter_add(struct ble_scan *scan, uint8_t type, const struct ble_scan_filter_data *data) { @@ -400,6 +462,12 @@ uint32_t ble_scan_filter_add(struct ble_scan *scan, uint8_t type, } #endif +#if (CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT > 0) + case BLE_SCAN_MANUFACTURER_DATA_FILTER: { + return manufacturer_data_filter_add(scan, data); + } +#endif + default: return NRF_ERROR_INVALID_PARAM; } @@ -444,6 +512,14 @@ uint32_t ble_scan_all_filter_remove(struct ble_scan *scan) appearance_filter->appearance_cnt = 0; #endif +#if (CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT > 0) + struct ble_scan_manufacturer_data_filter *md_filter = + &scan->scan_filters.manufacturer_data_filter; + + memset(md_filter->manufacturer_data, 0, sizeof(md_filter->manufacturer_data)); + md_filter->manufacturer_data_cnt = 0; +#endif + return NRF_SUCCESS; } @@ -461,7 +537,8 @@ uint32_t ble_scan_filters_enable(struct ble_scan *scan, uint8_t mode, bool match (!(mode & BLE_SCAN_NAME_FILTER)) && (!(mode & BLE_SCAN_UUID_FILTER)) && (!(mode & BLE_SCAN_SHORT_NAME_FILTER)) && - (!(mode & BLE_SCAN_APPEARANCE_FILTER))) { + (!(mode & BLE_SCAN_APPEARANCE_FILTER)) && + (!(mode & BLE_SCAN_MANUFACTURER_DATA_FILTER))) { return NRF_ERROR_INVALID_PARAM; } @@ -502,6 +579,12 @@ uint32_t ble_scan_filters_enable(struct ble_scan *scan, uint8_t mode, bool match } #endif +#if (CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT > 0) + if (mode & BLE_SCAN_MANUFACTURER_DATA_FILTER) { + filters->manufacturer_data_filter.manufacturer_data_filter_enabled = true; + } +#endif + /* Select the filter mode. */ filters->all_filters_mode = match_all; @@ -539,6 +622,10 @@ uint32_t ble_scan_filters_disable(struct ble_scan *scan) filters->appearance_filter.appearance_filter_enabled = false; #endif +#if (CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT > 0) + filters->manufacturer_data_filter.manufacturer_data_filter_enabled = false; +#endif + return NRF_SUCCESS; } @@ -771,6 +858,21 @@ static void ble_scan_on_adv_report(struct ble_scan *scan, } #endif /* CONFIG_BLE_SCAN_APPEARANCE_COUNT */ +#if (CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT > 0) + /* Check the manufacturer data filter. */ + if (scan->scan_filters.manufacturer_data_filter.manufacturer_data_filter_enabled) { + filter_cnt++; + if (adv_manufacturer_data_compare(scan, adv_report->data.p_data, + adv_report->data.len) || + (active_match_all && + adv_manufacturer_data_compare(scan, adv_data, adv_data_len))) { + filter_match_cnt++; + + scan_evt.filter_match.filter_match.manufacturer_data_filter_match = true; + } + } +#endif /* CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT */ + /* In the multifilter mode, the number of the active filters must equal the number of the * filters matched to generate the notification. */ diff --git a/tests/unit/lib/bluetooth/ble_scan/prj.conf b/tests/unit/lib/bluetooth/ble_scan/prj.conf index 6e4a8778ad..cf20024db1 100644 --- a/tests/unit/lib/bluetooth/ble_scan/prj.conf +++ b/tests/unit/lib/bluetooth/ble_scan/prj.conf @@ -3,11 +3,13 @@ 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_MANUFACTURER_DATA_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_MANUFACTURER_DATA_COUNT=2 CONFIG_BLE_SCAN_INTERVAL=160 CONFIG_BLE_SCAN_DURATION=0 CONFIG_BLE_SCAN_WINDOW=80 diff --git a/tests/unit/lib/bluetooth/ble_scan/src/unity_test.c b/tests/unit/lib/bluetooth/ble_scan/src/unity_test.c index 570c1468a3..0596baef99 100644 --- a/tests/unit/lib/bluetooth/ble_scan/src/unity_test.c +++ b/tests/unit/lib/bluetooth/ble_scan/src/unity_test.c @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include "cmock_ble_gap.h" #include "cmock_ble_gatts.h" @@ -21,6 +24,9 @@ #define UUID 0x2312 +/* Nordic Semiconductor company identifier (Bluetooth SIG assigned). */ +#define BLE_COMPANY_ID_NORDIC 0x0059 + BLE_SCAN_DEF(ble_scan); static struct ble_scan_evt scan_event; @@ -136,7 +142,7 @@ void test_ble_scan_filters_enable_error_invalid_param(void) test_ble_scan_init(); - nrf_err = ble_scan_filters_enable(&ble_scan, 0x20, true); + nrf_err = ble_scan_filters_enable(&ble_scan, 0x40, true); TEST_ASSERT_EQUAL(NRF_ERROR_INVALID_PARAM, nrf_err); } @@ -160,7 +166,8 @@ void test_ble_scan_filters_enable_all(void) BLE_SCAN_SHORT_NAME_FILTER | BLE_SCAN_ADDR_FILTER | BLE_SCAN_UUID_FILTER | - BLE_SCAN_APPEARANCE_FILTER), true); + BLE_SCAN_APPEARANCE_FILTER | + BLE_SCAN_MANUFACTURER_DATA_FILTER), true); TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); } @@ -216,10 +223,11 @@ void test_ble_scan_filter_get(void) 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); + BLE_SCAN_SHORT_NAME_FILTER | + BLE_SCAN_ADDR_FILTER | + BLE_SCAN_UUID_FILTER | + BLE_SCAN_APPEARANCE_FILTER | + BLE_SCAN_MANUFACTURER_DATA_FILTER), true); nrf_err = ble_scan_filter_get(&ble_scan, &ble_scan_filter_data); TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); @@ -571,6 +579,137 @@ void test_ble_scan_filter_add_short_name_error_no_mem(void) TEST_ASSERT_EQUAL(NRF_ERROR_NO_MEM, nrf_err); } +/* Manufacturer data filter Unit Tests */ +void test_ble_scan_filter_add_manufacturer_data(void) +{ + /* Happy path: add a valid manufacturer data filter containing a 2-byte little-endian + * company identifier. Expected to succeed and accept the filter. + */ + uint32_t nrf_err; + uint8_t manuf_data[2]; + struct ble_scan_filter_data filter_data = { + .manufacturer_data_filter = { + .data = manuf_data, + .data_len = sizeof(manuf_data), + }, + }; + + /* Encode the company identifier in little-endian as required by the BLE spec. */ + sys_put_le16(BLE_COMPANY_ID_NORDIC, manuf_data); + + test_ble_scan_init(); + ble_scan_filters_enable(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, true); + + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, &filter_data); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); +} + +void test_ble_scan_filter_add_manufacturer_data_error_data_size(void) +{ + /* Boundary validation: a zero length filter and a filter larger than + * CONFIG_BLE_SCAN_MANUFACTURER_DATA_MAX_LEN must both be rejected with + * NRF_ERROR_DATA_SIZE. + */ + uint32_t nrf_err; + uint8_t manuf_data[2]; + uint8_t manuf_data_too_long[CONFIG_BLE_SCAN_MANUFACTURER_DATA_MAX_LEN + 1] = {0}; + struct ble_scan_filter_data filter_data_zero_len = { + .manufacturer_data_filter = { + .data = manuf_data, + .data_len = 0, + }, + }; + struct ble_scan_filter_data filter_data_too_long = { + .manufacturer_data_filter = { + .data = manuf_data_too_long, + .data_len = sizeof(manuf_data_too_long), + }, + }; + + /* Encode the company identifier in little-endian as required by the BLE spec. */ + sys_put_le16(BLE_COMPANY_ID_NORDIC, manuf_data); + + test_ble_scan_init(); + ble_scan_filters_enable(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, true); + + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, + &filter_data_zero_len); + TEST_ASSERT_EQUAL(NRF_ERROR_DATA_SIZE, nrf_err); + + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, + &filter_data_too_long); + TEST_ASSERT_EQUAL(NRF_ERROR_DATA_SIZE, nrf_err); +} + +void test_ble_scan_filter_add_manufacturer_data_error_null(void) +{ + /* Verify that a valid-length filter with a NULL data pointer is rejected with + * NRF_ERROR_NULL, ensuring the inner pointer is validated before dereference. + */ + uint32_t nrf_err; + struct ble_scan_filter_data filter_data = { + .manufacturer_data_filter = { + .data = NULL, + .data_len = 2, + }, + }; + + test_ble_scan_init(); + ble_scan_filters_enable(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, true); + + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, &filter_data); + TEST_ASSERT_EQUAL(NRF_ERROR_NULL, nrf_err); +} + +void test_ble_scan_filter_add_manufacturer_data_error_no_mem(void) +{ + /* Capacity check: duplicates must not consume a slot, two unique filters must fit, + * and a third unique filter must be rejected with NRF_ERROR_NO_MEM. + * Assumes CONFIG_BLE_SCAN_MANUFACTURER_DATA_COUNT is 2. + */ + uint32_t nrf_err; + uint8_t manuf_data[2]; + uint8_t manuf_data_second[] = {0x4C, 0x00}; + uint8_t manuf_data_third[] = {0x06, 0x00}; + struct ble_scan_filter_data filter_data = { + .manufacturer_data_filter = { + .data = manuf_data, .data_len = sizeof(manuf_data), + }, + }; + struct ble_scan_filter_data filter_data_second = { + .manufacturer_data_filter = { + .data = manuf_data_second, .data_len = sizeof(manuf_data_second), + }, + }; + struct ble_scan_filter_data filter_data_third = { + .manufacturer_data_filter = { + .data = manuf_data_third, .data_len = sizeof(manuf_data_third), + }, + }; + + sys_put_le16(BLE_COMPANY_ID_NORDIC, manuf_data); + + test_ble_scan_init(); + ble_scan_filters_enable(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, true); + + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, &filter_data); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + /* Duplicate filter is accepted without increasing the counter. */ + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, &filter_data); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + /* Second unique filter fits. */ + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, + &filter_data_second); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + /* Third unique filter exceeds capacity. */ + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, + &filter_data_third); + TEST_ASSERT_EQUAL(NRF_ERROR_NO_MEM, nrf_err); +} + void test_ble_scan_is_allow_list_used(void) { bool nrf_err; @@ -731,6 +870,7 @@ void test_ble_scan_on_ble_evt_adv_report_device_address(void) TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.short_name_filter_match); TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.appearance_filter_match); TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.manufacturer_data_filter_match); TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, scan_event.filter_match.adv_report); } @@ -794,6 +934,7 @@ void test_ble_scan_on_ble_evt_adv_report_device_name(void) TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.short_name_filter_match); TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.appearance_filter_match); TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.manufacturer_data_filter_match); TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, scan_event.filter_match.adv_report); } @@ -867,6 +1008,7 @@ void test_ble_scan_on_ble_evt_adv_report_device_short_name(void) TEST_ASSERT_EQUAL(1, scan_event.filter_match.filter_match.short_name_filter_match); TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.appearance_filter_match); TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.manufacturer_data_filter_match); TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, scan_event.filter_match.adv_report); } @@ -938,6 +1080,7 @@ void test_ble_scan_on_ble_evt_adv_report_device_appearance(void) TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.short_name_filter_match); TEST_ASSERT_EQUAL(1, scan_event.filter_match.filter_match.appearance_filter_match); TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.manufacturer_data_filter_match); TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, scan_event.filter_match.adv_report); } @@ -1017,6 +1160,7 @@ void test_ble_scan_on_ble_evt_adv_report_device_name_and_appearance(void) TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.short_name_filter_match); TEST_ASSERT_EQUAL(1, scan_event.filter_match.filter_match.appearance_filter_match); TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.manufacturer_data_filter_match); } void test_ble_scan_on_ble_evt_adv_report_device_uuid_not_found(void) @@ -1094,6 +1238,7 @@ void test_ble_scan_on_ble_evt_adv_report_device_uuid(void) TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.short_name_filter_match); TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.appearance_filter_match); TEST_ASSERT_EQUAL(1, scan_event.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.manufacturer_data_filter_match); TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, scan_event.filter_match.adv_report); } @@ -1201,6 +1346,156 @@ void test_ble_scan_on_ble_evt_adv_report_device_uuid_connect(void) TEST_ASSERT_EQUAL(BLE_SCAN_EVT_FILTER_MATCH, scan_event.evt_type); } +void test_ble_scan_on_ble_evt_adv_report_device_manufacturer_data_not_found(void) +{ + /* Advertising packet does not contain manufacturer-specific data matching the configured + * filter. The mocked find helper returns false, so the scan event handler must receive + * BLE_SCAN_EVT_NOT_FOUND. + */ + uint8_t manuf_data_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), + }, + }, + }, + }; + + /* Encode the company identifier in little-endian as required by the BLE spec. */ + sys_put_le16(BLE_COMPANY_ID_NORDIC, manuf_data_exp); + + test_ble_scan_filter_add_manufacturer_data(); + + __cmock_ble_adv_data_manufacturer_data_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + manuf_data_exp, sizeof(manuf_data_exp), sizeof(manuf_data_exp), + false); /* Mock "no manufacturer data match" -> expect BLE_SCAN_EVT_NOT_FOUND */ + __cmock_sd_ble_gap_scan_start_ExpectAndReturn(NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_scan_on_ble_evt(&ble_evt, &ble_scan); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_NOT_FOUND, scan_event.evt_type); +} + +void test_ble_scan_on_ble_evt_adv_report_device_manufacturer_data(void) +{ + /* Advertising packet contains manufacturer-specific data matching the configured filter. + * The mocked find helper returns true, so the scan event handler must receive + * BLE_SCAN_EVT_FILTER_MATCH with only manufacturer_data_filter_match set. + */ + uint8_t manuf_data_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), + }, + }, + }, + }; + + /* Encode the company identifier in little-endian as required by the BLE spec. */ + sys_put_le16(BLE_COMPANY_ID_NORDIC, manuf_data_exp); + + test_ble_scan_filter_add_manufacturer_data(); + + __cmock_ble_adv_data_manufacturer_data_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + manuf_data_exp, sizeof(manuf_data_exp), sizeof(manuf_data_exp), + true); /* Mock "manufacturer data match" -> expect BLE_SCAN_EVT_FILTER_MATCH */ + __cmock_sd_ble_gap_scan_start_ExpectAndReturn(NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_scan_on_ble_evt(&ble_evt, &ble_scan); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_FILTER_MATCH, scan_event.evt_type); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.address_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.name_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.short_name_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.appearance_filter_match); + TEST_ASSERT_EQUAL(0, scan_event.filter_match.filter_match.uuid_filter_match); + TEST_ASSERT_EQUAL(1, scan_event.filter_match.filter_match.manufacturer_data_filter_match); + TEST_ASSERT_EQUAL_PTR(&ble_evt.evt.gap_evt.params.adv_report, + scan_event.filter_match.adv_report); +} + +void test_ble_scan_on_ble_evt_adv_report_device_multiple_manufacturer_data(void) +{ + /* Multiple manufacturer data filters registered. The library iterates the list and stops + * on the first match. Verify that an ADV packet matching the *second* registered filter + * still produces BLE_SCAN_EVT_FILTER_MATCH with manufacturer_data_filter_match set. + */ + uint32_t nrf_err; + uint8_t manuf_data_first[2]; + uint8_t manuf_data_second[] = {0x4C, 0x00}; /* Different company ID */ + 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), + }, + }, + }, + }; + struct ble_scan_filter_data filter_data_first = { + .manufacturer_data_filter = { + .data = manuf_data_first, + .data_len = sizeof(manuf_data_first), + }, + }; + struct ble_scan_filter_data filter_data_second = { + .manufacturer_data_filter = { + .data = manuf_data_second, + .data_len = sizeof(manuf_data_second), + }, + }; + + sys_put_le16(BLE_COMPANY_ID_NORDIC, manuf_data_first); + + test_ble_scan_init(); + + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, + &filter_data_first); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, + &filter_data_second); + TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); + + ble_scan_filters_enable(&ble_scan, BLE_SCAN_MANUFACTURER_DATA_FILTER, false); + + /* First registered filter is checked and does not match. */ + __cmock_ble_adv_data_manufacturer_data_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + manuf_data_first, sizeof(manuf_data_first), sizeof(manuf_data_first), false); + + /* Second registered filter is checked and matches. */ + __cmock_ble_adv_data_manufacturer_data_find_ExpectWithArrayAndReturn( + ble_evt.evt.gap_evt.params.adv_report.data.p_data, 1, + ble_evt.evt.gap_evt.params.adv_report.data.len, + manuf_data_second, sizeof(manuf_data_second), sizeof(manuf_data_second), true); + + __cmock_sd_ble_gap_scan_start_ExpectAndReturn(NULL, &ble_scan.scan_buffer, NRF_SUCCESS); + + ble_scan_on_ble_evt(&ble_evt, &ble_scan); + TEST_ASSERT_EQUAL(BLE_SCAN_EVT_FILTER_MATCH, scan_event.evt_type); + TEST_ASSERT_EQUAL(1, scan_event.filter_match.filter_match.manufacturer_data_filter_match); +} + void test_ble_scan_on_ble_evt_timeout(void) { ble_evt_t ble_evt = {