diff --git a/include/zephyr/bluetooth/gap.h b/include/zephyr/bluetooth/gap.h index b446ad521d4c9..5f1f3e72e65d2 100644 --- a/include/zephyr/bluetooth/gap.h +++ b/include/zephyr/bluetooth/gap.h @@ -26,6 +26,15 @@ extern "C" { * @{ */ +/** + * @brief Default GAP service name + * + * Use this name as a first argument for BT_GATT_SERVICE_DEFINE when creating + * GAP service implementation. + * This ensures that the GAP service would be placed just after BT_UUID_GATT. + */ +#define BT_GATT_GAP_SVC_DEFAULT_NAME _2_gap_svc + /** * @name Company Identifiers (see Bluetooth Assigned Numbers) * @{ diff --git a/samples/bluetooth/peripheral_gap_svc/CMakeLists.txt b/samples/bluetooth/peripheral_gap_svc/CMakeLists.txt new file mode 100644 index 0000000000000..831b26a3a756d --- /dev/null +++ b/samples/bluetooth/peripheral_gap_svc/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(peripheral_gap_svc) + +target_sources(app PRIVATE + src/main.c +) diff --git a/samples/bluetooth/peripheral_gap_svc/README.rst b/samples/bluetooth/peripheral_gap_svc/README.rst new file mode 100644 index 0000000000000..ea6439831a26d --- /dev/null +++ b/samples/bluetooth/peripheral_gap_svc/README.rst @@ -0,0 +1,29 @@ +.. zephyr:code-sample:: ble_peripheral_gap_svc + :name: Peripheral GAP Server non default implementation + :relevant-api: bluetooth + + Implement a dummy peripheral with GAP Server that limits the accepted names + +Overview +******** + +This sample demonstrates the implementation of the GAP service + + + + +This sample demonstrates the usage of the NUS service (Nordic UART Service) as a serial +endpoint to exchange data. In this case, the sample assumes the data is UTF-8 encoded, +but it may be binary data. Once the user connects to the device and subscribes to the TX +characteristic, it will start receiving periodic notifications with "Hello World!\n". + +Requirements +************ + +* BlueZ running on the host, or +* A board with Bluetooth LE support + +Building and Running +******************** + +See :zephyr:code-sample-category:`bluetooth` samples for details. diff --git a/samples/bluetooth/peripheral_gap_svc/prj.conf b/samples/bluetooth/peripheral_gap_svc/prj.conf new file mode 100644 index 0000000000000..4f9beebd4c063 --- /dev/null +++ b/samples/bluetooth/peripheral_gap_svc/prj.conf @@ -0,0 +1,14 @@ +CONFIG_LOG=y +CONFIG_BT=y +CONFIG_BT_SMP=y +CONFIG_BT_PERIPHERAL=y + +# Testing locally redefined SVC implementation +CONFIG_BT_GAP_SVC_DEFAULT_IMPL=n + +CONFIG_BT_DEVICE_NAME="Zephyr GAP service" +CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_DEVICE_NAME_GATT_WRITABLE=y + +CONFIG_BT_DEVICE_APPEARANCE_DYNAMIC=n +CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE=n diff --git a/samples/bluetooth/peripheral_gap_svc/sample.yaml b/samples/bluetooth/peripheral_gap_svc/sample.yaml new file mode 100644 index 0000000000000..e0a6f36fd0088 --- /dev/null +++ b/samples/bluetooth/peripheral_gap_svc/sample.yaml @@ -0,0 +1,14 @@ +sample: + name: Bluetooth Peripheral GAP service non default implementation + description: Demonstrates the GAP service implementation on application side. +tests: + sample.bluetooth.peripheral_gap_svc: + harness: bluetooth + platform_allow: + - qemu_cortex_m3 + - qemu_x86 + - nrf52840dk/nrf52840 + - ophelia4ev/nrf54l15/cpuapp + integration_platforms: + - qemu_cortex_m3 + tags: bluetooth diff --git a/samples/bluetooth/peripheral_gap_svc/src/main.c b/samples/bluetooth/peripheral_gap_svc/src/main.c new file mode 100644 index 0000000000000..9482fe94bd9d7 --- /dev/null +++ b/samples/bluetooth/peripheral_gap_svc/src/main.c @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2025 Koppel Electronic + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + + +#define DEVICE_NAME CONFIG_BT_DEVICE_NAME +#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1) + +static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN), +}; + +/* ----------------------------------------------------------------------------- + * Local implementation of GAP service + */ + +static ssize_t read_name(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + const char *name = bt_get_name(); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, name, + strlen(name)); +} + +static ssize_t write_name(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, + uint16_t len, uint16_t offset, uint8_t flags) +{ + /* adding one to fit the terminating null character */ + char value[CONFIG_BT_DEVICE_NAME_MAX + 1] = {}; + + if (offset != 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (offset + len > CONFIG_BT_DEVICE_NAME_MAX) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + memcpy(value, buf, len); + + value[len] = '\0'; + + /* Check if the name starts with capital letter */ + if (value[0] < 'A' || value[0] > 'Z') { + printk("Rejected name change to \"%s\": must start with capital letter\n", value); + return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED); + } + + bt_set_name(value); + + printk("Name changed to \"%s\"\n", value); + + return len; +} + +static ssize_t read_appearance(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint16_t appearance = sys_cpu_to_le16(bt_get_appearance()); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &appearance, sizeof(appearance)); +} + +BT_GATT_SERVICE_DEFINE(BT_GATT_GAP_SVC_DEFAULT_NAME, + BT_GATT_PRIMARY_SERVICE(BT_UUID_GAP), + /* Require pairing for writes to device name */ + BT_GATT_CHARACTERISTIC(BT_UUID_GAP_DEVICE_NAME, + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE, + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, + read_name, write_name, NULL), + BT_GATT_CHARACTERISTIC(BT_UUID_GAP_APPEARANCE, + BT_GATT_CHRC_READ, + BT_GATT_PERM_READ, + read_appearance, NULL, NULL), +); + +/* End of local implementation of GAP service + * --------------------------------------------------------------------------- + */ + +static void connected(struct bt_conn *conn, uint8_t conn_err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + if (conn_err) { + printk("Failed to connect to %s (%u)\n", addr, conn_err); + return; + } + + printk("Connected: %s\n", addr); +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + printk("Disconnected: %s (reason 0x%02x)\n", addr, reason); +} + +static int start_advertising(void) +{ + int err; + + err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), NULL, 0); + if (err) { + printk("Advertising failed to start (err %d)\n", err); + } + + return err; +} + +static void recycled(void) +{ + start_advertising(); +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = connected, + .disconnected = disconnected, + .recycled = recycled, +}; + +int main(void) +{ + int err; + + printk("Sample - Bluetooth Peripheral GAP service\n"); + + err = bt_enable(NULL); + if (err) { + printk("Failed to enable bluetooth: %d\n", err); + return err; + } + + err = start_advertising(); + if (err) { + return err; + } + + printk("Initialization complete\n"); + + return 0; +} diff --git a/subsys/bluetooth/host/CMakeLists.txt b/subsys/bluetooth/host/CMakeLists.txt index 0911c22c8f866..39ce620a53490 100644 --- a/subsys/bluetooth/host/CMakeLists.txt +++ b/subsys/bluetooth/host/CMakeLists.txt @@ -49,6 +49,11 @@ if(CONFIG_BT_HCI_HOST) gatt.c ) + zephyr_library_sources_ifdef( + CONFIG_GATT_GAP_SVC_VALIDATE + gatt_gap_svc_validate.c + ) + if(CONFIG_BT_SMP) zephyr_library_sources( smp.c diff --git a/subsys/bluetooth/host/Kconfig.gatt b/subsys/bluetooth/host/Kconfig.gatt index a04241a3e94f6..2d55220d17278 100644 --- a/subsys/bluetooth/host/Kconfig.gatt +++ b/subsys/bluetooth/host/Kconfig.gatt @@ -259,62 +259,6 @@ config BT_PERIPHERAL_PREF_TIMEOUT Range 3200 to 65534 is invalid. 65535 represents no specific value. endif # BT_GAP_PERIPHERAL_PREF_PARAMS -config BT_DEVICE_NAME_GATT_WRITABLE - bool "Allow to write device name by remote GATT clients" - depends on BT_DEVICE_NAME_DYNAMIC - default y - help - Enabling this option allows remote GATT clients to write to device - name GAP characteristic. - -if BT_DEVICE_NAME_GATT_WRITABLE -choice BT_DEVICE_NAME_GATT_WRITABLE_SECURITY - prompt "Security requirements" - default DEVICE_NAME_GATT_WRITABLE_ENCRYPT - help - Select security requirements for writing device name by remote GATT - clients. - -config DEVICE_NAME_GATT_WRITABLE_NONE - bool "No requirements" - -config DEVICE_NAME_GATT_WRITABLE_ENCRYPT - bool "Encryption required" - -config DEVICE_NAME_GATT_WRITABLE_AUTHEN - bool "Encryption and authentication required" - -endchoice #BT_DEVICE_NAME_GATT_WRITABLE_SECURITY -endif #BT_DEVICE_NAME_GATT_WRITABLE - -config BT_DEVICE_APPEARANCE_GATT_WRITABLE - bool "Allow to write GAP Appearance by remote GATT clients" - depends on BT_DEVICE_APPEARANCE_DYNAMIC - default y - help - Enabling this option allows remote GATT clients to write to device - appearance GAP characteristic. - -if BT_DEVICE_APPEARANCE_GATT_WRITABLE -choice BT_DEVICE_APPEARANCE_GATT_WRITABLE - prompt "Security requirements" - default DEVICE_APPEARANCE_GATT_WRITABLE_AUTHEN - help - Select security requirements for writing device name by remote GATT - clients. - -config BT_DEVICE_APPEARANCE_GATT_WRITABLE_NONE - bool "No requirements" - -config BT_DEVICE_APPEARANCE_GATT_WRITABLE_ENCRYPT - bool "Encryption required" - -config DEVICE_APPEARANCE_GATT_WRITABLE_AUTHEN - bool "Encryption and authentication required" - -endchoice #BT_DEVICE_APPEARANCE_GATT_WRITABLE -endif #BT_DEVICE_APPEARANCE_GATT_WRITABLE - config BT_GATT_AUTHORIZATION_CUSTOM bool "Custom authorization of GATT operations" help @@ -323,4 +267,5 @@ config BT_GATT_AUTHORIZATION_CUSTOM with the bt_gatt_authorization_cb_register API. See the API documentation for more details. +rsource 'Kconfig.gatt_gap_svc_validate' endmenu diff --git a/subsys/bluetooth/host/Kconfig.gatt_gap_svc_validate b/subsys/bluetooth/host/Kconfig.gatt_gap_svc_validate new file mode 100644 index 0000000000000..61bfd96477258 --- /dev/null +++ b/subsys/bluetooth/host/Kconfig.gatt_gap_svc_validate @@ -0,0 +1,21 @@ +# Bluetooth GATT GAP service validation + +# Copyright (c) 2025 Koppel Electronic +# SPDX-License-Identifier: Apache-2.0 + +menuconfig GATT_GAP_SVC_VALIDATE + bool "GATT GAP Service validation" + depends on BT_CONN + default y + help + Validate if in the GATT database exactly one GAP service is present. + This helps to debug any issues related to GAP service that may make + the Bluetooth implementation not compatible with the standard. + +if GATT_GAP_SVC_VALIDATE + +module = GATT_GAP_SVC_VALIDATE +module-str = gatt-gap-validate +source "subsys/logging/Kconfig.template.log_config" + +endif # GATT_GAP_SVC_VALIDATE diff --git a/subsys/bluetooth/host/conn.c b/subsys/bluetooth/host/conn.c index 3bb442e3f1859..0ba63dce9d20e 100644 --- a/subsys/bluetooth/host/conn.c +++ b/subsys/bluetooth/host/conn.c @@ -53,6 +53,7 @@ #include "common/bt_str.h" #include "conn_internal.h" #include "direction_internal.h" +#include "gatt_gap_svc_validate.h" #include "hci_core.h" #include "id.h" #include "iso_internal.h" @@ -4428,6 +4429,14 @@ int bt_conn_init(void) bt_att_init(); + if (IS_ENABLED(CONFIG_GATT_GAP_SVC_VALIDATE)) { + err = gatt_gap_svc_validate(); + if (err) { + LOG_ERR("GATT GAP service validation failed (err %d)", err); + return err; + } + } + err = bt_smp_init(); if (err) { return err; diff --git a/subsys/bluetooth/host/gatt.c b/subsys/bluetooth/host/gatt.c index 4e8b7c78244c7..bbae64420adcb 100644 --- a/subsys/bluetooth/host/gatt.c +++ b/subsys/bluetooth/host/gatt.c @@ -104,99 +104,6 @@ enum gatt_global_flags { static ATOMIC_DEFINE(gatt_flags, GATT_NUM_FLAGS); -static ssize_t read_name(struct bt_conn *conn, const struct bt_gatt_attr *attr, - void *buf, uint16_t len, uint16_t offset) -{ - const char *name = bt_get_name(); - - return bt_gatt_attr_read(conn, attr, buf, len, offset, name, - strlen(name)); -} - -#if defined(CONFIG_BT_DEVICE_NAME_GATT_WRITABLE) - -static ssize_t write_name(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, - uint16_t len, uint16_t offset, uint8_t flags) -{ - /* adding one to fit the terminating null character */ - char value[CONFIG_BT_DEVICE_NAME_MAX + 1] = {}; - - if (offset >= CONFIG_BT_DEVICE_NAME_MAX) { - return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); - } - - if (offset + len > CONFIG_BT_DEVICE_NAME_MAX) { - return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); - } - - memcpy(value, buf, len); - - value[len] = '\0'; - - bt_set_name(value); - - return len; -} - -#endif /* CONFIG_BT_DEVICE_NAME_GATT_WRITABLE */ - -static ssize_t read_appearance(struct bt_conn *conn, - const struct bt_gatt_attr *attr, void *buf, - uint16_t len, uint16_t offset) -{ - uint16_t appearance = sys_cpu_to_le16(bt_get_appearance()); - - return bt_gatt_attr_read(conn, attr, buf, len, offset, &appearance, - sizeof(appearance)); -} - -#if defined(CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE) -static ssize_t write_appearance(struct bt_conn *conn, const struct bt_gatt_attr *attr, - const void *buf, uint16_t len, uint16_t offset, - uint8_t flags) -{ - uint16_t appearance_le = sys_cpu_to_le16(bt_get_appearance()); - char * const appearance_le_bytes = (char *)&appearance_le; - uint16_t appearance; - int err; - - if (offset >= sizeof(appearance_le)) { - return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); - } - - if ((offset + len) > sizeof(appearance_le)) { - return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); - } - - memcpy(&appearance_le_bytes[offset], buf, len); - appearance = sys_le16_to_cpu(appearance_le); - - err = bt_set_appearance(appearance); - - if (err) { - return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); - } - - return len; -} -#endif /* CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE */ - -#if defined(CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE) - #define GAP_APPEARANCE_PROPS (BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE) -#if defined(CONFIG_DEVICE_APPEARANCE_GATT_WRITABLE_AUTHEN) - #define GAP_APPEARANCE_PERMS (BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_AUTHEN) -#elif defined(CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE_ENCRYPT) - #define GAP_APPEARANCE_PERMS (BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT) -#else - #define GAP_APPEARANCE_PERMS (BT_GATT_PERM_READ | BT_GATT_PERM_WRITE) -#endif - #define GAP_APPEARANCE_WRITE_HANDLER write_appearance -#else - #define GAP_APPEARANCE_PROPS BT_GATT_CHRC_READ - #define GAP_APPEARANCE_PERMS BT_GATT_PERM_READ - #define GAP_APPEARANCE_WRITE_HANDLER NULL -#endif - #if defined (CONFIG_BT_GAP_PERIPHERAL_PREF_PARAMS) /* This checks if the range entered is valid */ BUILD_ASSERT(!(CONFIG_BT_PERIPHERAL_PREF_MIN_INT > 3200 && @@ -211,72 +118,7 @@ BUILD_ASSERT((CONFIG_BT_PERIPHERAL_PREF_MIN_INT == 0xffff) || BUILD_ASSERT((CONFIG_BT_PERIPHERAL_PREF_TIMEOUT * 4U) > ((1U + CONFIG_BT_PERIPHERAL_PREF_LATENCY) * CONFIG_BT_PERIPHERAL_PREF_MAX_INT)); - -static ssize_t read_ppcp(struct bt_conn *conn, const struct bt_gatt_attr *attr, - void *buf, uint16_t len, uint16_t offset) -{ - struct __packed { - uint16_t min_int; - uint16_t max_int; - uint16_t latency; - uint16_t timeout; - } ppcp; - - ppcp.min_int = sys_cpu_to_le16(CONFIG_BT_PERIPHERAL_PREF_MIN_INT); - ppcp.max_int = sys_cpu_to_le16(CONFIG_BT_PERIPHERAL_PREF_MAX_INT); - ppcp.latency = sys_cpu_to_le16(CONFIG_BT_PERIPHERAL_PREF_LATENCY); - ppcp.timeout = sys_cpu_to_le16(CONFIG_BT_PERIPHERAL_PREF_TIMEOUT); - - return bt_gatt_attr_read(conn, attr, buf, len, offset, &ppcp, - sizeof(ppcp)); -} #endif - -#if defined(CONFIG_BT_CENTRAL) && defined(CONFIG_BT_PRIVACY) -static ssize_t read_central_addr_res(struct bt_conn *conn, - const struct bt_gatt_attr *attr, void *buf, - uint16_t len, uint16_t offset) -{ - uint8_t central_addr_res = BT_GATT_CENTRAL_ADDR_RES_SUPP; - - return bt_gatt_attr_read(conn, attr, buf, len, offset, - ¢ral_addr_res, sizeof(central_addr_res)); -} -#endif /* CONFIG_BT_CENTRAL && CONFIG_BT_PRIVACY */ - -BT_GATT_SERVICE_DEFINE(_2_gap_svc, - BT_GATT_PRIMARY_SERVICE(BT_UUID_GAP), -#if defined(CONFIG_BT_DEVICE_NAME_GATT_WRITABLE) - /* Require pairing for writes to device name */ - BT_GATT_CHARACTERISTIC(BT_UUID_GAP_DEVICE_NAME, - BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE, - BT_GATT_PERM_READ | -#if defined(CONFIG_DEVICE_NAME_GATT_WRITABLE_AUTHEN) - BT_GATT_PERM_WRITE_AUTHEN, -#elif defined(CONFIG_DEVICE_NAME_GATT_WRITABLE_ENCRYPT) - BT_GATT_PERM_WRITE_ENCRYPT, -#else - BT_GATT_PERM_WRITE, -#endif - read_name, write_name, bt_dev.name), -#else - BT_GATT_CHARACTERISTIC(BT_UUID_GAP_DEVICE_NAME, BT_GATT_CHRC_READ, - BT_GATT_PERM_READ, read_name, NULL, NULL), -#endif /* CONFIG_BT_DEVICE_NAME_GATT_WRITABLE */ - BT_GATT_CHARACTERISTIC(BT_UUID_GAP_APPEARANCE, GAP_APPEARANCE_PROPS, - GAP_APPEARANCE_PERMS, read_appearance, - GAP_APPEARANCE_WRITE_HANDLER, NULL), -#if defined(CONFIG_BT_CENTRAL) && defined(CONFIG_BT_PRIVACY) - BT_GATT_CHARACTERISTIC(BT_UUID_CENTRAL_ADDR_RES, - BT_GATT_CHRC_READ, BT_GATT_PERM_READ, - read_central_addr_res, NULL, NULL), -#endif /* CONFIG_BT_CENTRAL && CONFIG_BT_PRIVACY */ -#if defined(CONFIG_BT_GAP_PERIPHERAL_PREF_PARAMS) - BT_GATT_CHARACTERISTIC(BT_UUID_GAP_PPCP, BT_GATT_CHRC_READ, - BT_GATT_PERM_READ, read_ppcp, NULL, NULL), -#endif -); - struct sc_data { uint16_t start; uint16_t end; diff --git a/subsys/bluetooth/host/gatt_gap_svc_validate.c b/subsys/bluetooth/host/gatt_gap_svc_validate.c new file mode 100644 index 0000000000000..518cdd607f448 --- /dev/null +++ b/subsys/bluetooth/host/gatt_gap_svc_validate.c @@ -0,0 +1,55 @@ +/* gatt_gap_svc_validate.c - GAP service inside GATT validation functions */ + +/* + * Copyright (c) 2025 Koppel Electronic + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + + +#define MODULE gatt_gap_validate + +LOG_MODULE_REGISTER(MODULE, CONFIG_GATT_GAP_SVC_VALIDATE_LOG_LEVEL); + + +static uint8_t gap_service_check_cb(const struct bt_gatt_attr *attr, + uint16_t handle, + void *user_data) +{ + size_t *cnt = user_data; + const struct bt_uuid *svc_uuid = attr->user_data; + + if (svc_uuid && bt_uuid_cmp(svc_uuid, BT_UUID_GAP) == 0) { + (*cnt)++; + LOG_DBG("GAP service found at handle: %u", handle); + if (*cnt > 1) { + LOG_ERR("Multiple (%zu) GAP services found at handle: %u", + *cnt, handle); + } + } + + return BT_GATT_ITER_CONTINUE; +} + +int gatt_gap_svc_validate(void) +{ + size_t gap_svc_count = 0; + + bt_gatt_foreach_attr_type( + BT_ATT_FIRST_ATTRIBUTE_HANDLE, + BT_ATT_LAST_ATTRIBUTE_HANDLE, + BT_UUID_GATT_PRIMARY, + NULL, 0, + gap_service_check_cb, + &gap_svc_count); + + if (gap_svc_count != 1) { + LOG_ERR("GAP service count invalid: %zu", gap_svc_count); + return -EINVAL; + } + return 0; +} diff --git a/subsys/bluetooth/host/gatt_gap_svc_validate.h b/subsys/bluetooth/host/gatt_gap_svc_validate.h new file mode 100644 index 0000000000000..c6a07ce617199 --- /dev/null +++ b/subsys/bluetooth/host/gatt_gap_svc_validate.h @@ -0,0 +1,27 @@ +/** @file + * @brief Internal API GATT GAP service validation. + */ + +/* + * Copyright (c) 2025 Koppel Electronic + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __BT_GATT_GAP_SVC_VALIDATE_H +#define __BT_GATT_GAP_SVC_VALIDATE_H + + +/** @brief Validate GATT database contains exactly one GAP service. + * + * This function iterates through the GATT attribute database and verifies + * that exactly one Generic Access Profile (GAP) service is present. The + * Bluetooth specification requires that a GATT server expose exactly one + * GAP service. + * + * @return 0 on success (exactly one GAP service found). + * @return -EINVAL if zero or multiple GAP services are found. + */ +int gatt_gap_svc_validate(void); + +#endif /* __BT_GATT_GAP_SVC_VALIDATE_H */ diff --git a/subsys/bluetooth/host/gatt_internal.h b/subsys/bluetooth/host/gatt_internal.h index dbe3736cb0fc8..67640b63f627e 100644 --- a/subsys/bluetooth/host/gatt_internal.h +++ b/subsys/bluetooth/host/gatt_internal.h @@ -14,9 +14,6 @@ #include #include -#define BT_GATT_CENTRAL_ADDR_RES_NOT_SUPP 0 -#define BT_GATT_CENTRAL_ADDR_RES_SUPP 1 - #define BT_GATT_PERM_READ_MASK (BT_GATT_PERM_READ | \ BT_GATT_PERM_READ_ENCRYPT | \ BT_GATT_PERM_READ_AUTHEN | \ diff --git a/subsys/bluetooth/services/CMakeLists.txt b/subsys/bluetooth/services/CMakeLists.txt index f17f683d7186c..75ad5fa3fad5d 100644 --- a/subsys/bluetooth/services/CMakeLists.txt +++ b/subsys/bluetooth/services/CMakeLists.txt @@ -4,6 +4,8 @@ zephyr_sources_ifdef(CONFIG_BT_ANS ans.c) zephyr_sources_ifdef(CONFIG_BT_DIS dis.c) +zephyr_sources_ifdef(CONFIG_BT_GAP_SVC_DEFAULT_IMPL gap_svc_default.c) + zephyr_sources_ifdef(CONFIG_BT_CTS cts.c) zephyr_sources_ifdef(CONFIG_BT_HRS hrs.c) diff --git a/subsys/bluetooth/services/Kconfig b/subsys/bluetooth/services/Kconfig index cad7dc5a72589..75547b8552847 100644 --- a/subsys/bluetooth/services/Kconfig +++ b/subsys/bluetooth/services/Kconfig @@ -10,6 +10,8 @@ rsource "Kconfig.ans" rsource "Kconfig.dis" +rsource "Kconfig.gap_svc" + rsource "Kconfig.cts" rsource "Kconfig.hrs" diff --git a/subsys/bluetooth/services/Kconfig.gap_svc b/subsys/bluetooth/services/Kconfig.gap_svc new file mode 100644 index 0000000000000..6451edf5e5d4d --- /dev/null +++ b/subsys/bluetooth/services/Kconfig.gap_svc @@ -0,0 +1,76 @@ +# GATT Generic Access Service - default implementation +# +# Copyright (c) 2019 Intel Corporation +# Copyright (c) 2025 Koppel Electronic +# +# SPDX-License-Identifier: Apache-2.0 + +menu "GATT Generic Access Service" + +config BT_GAP_SVC_DEFAULT_IMPL + bool "GATT Generic Access Service - default implementation" + default y + help + Enable the default GATT Generic Access Service. + Enabled by default provides basic GAP functionality. + If disabled, it is the application responsibility to provide its own + implementation of GAP service, otherwise the system is not Bluetooth + qualification compliant. + +config BT_DEVICE_NAME_GATT_WRITABLE + bool "Allow to write device name by remote GATT clients" + depends on BT_DEVICE_NAME_DYNAMIC + default y + help + Enabling this option allows remote GATT clients to write to device + name GAP characteristic. + +if BT_DEVICE_NAME_GATT_WRITABLE +choice BT_DEVICE_NAME_GATT_WRITABLE_SECURITY + prompt "Security requirements" + default DEVICE_NAME_GATT_WRITABLE_ENCRYPT + help + Select security requirements for writing device name by remote GATT + clients. + +config DEVICE_NAME_GATT_WRITABLE_NONE + bool "No requirements" + +config DEVICE_NAME_GATT_WRITABLE_ENCRYPT + bool "Encryption required" + +config DEVICE_NAME_GATT_WRITABLE_AUTHEN + bool "Encryption and authentication required" + +endchoice #BT_DEVICE_NAME_GATT_WRITABLE_SECURITY +endif #BT_DEVICE_NAME_GATT_WRITABLE + +config BT_DEVICE_APPEARANCE_GATT_WRITABLE + bool "Allow to write GAP Appearance by remote GATT clients" + depends on BT_DEVICE_APPEARANCE_DYNAMIC + default y + help + Enabling this option allows remote GATT clients to write to device + appearance GAP characteristic. + +if BT_DEVICE_APPEARANCE_GATT_WRITABLE +choice BT_DEVICE_APPEARANCE_GATT_WRITABLE + prompt "Security requirements" + default DEVICE_APPEARANCE_GATT_WRITABLE_AUTHEN + help + Select security requirements for writing device appearance by remote + GATT clients. + +config BT_DEVICE_APPEARANCE_GATT_WRITABLE_NONE + bool "No requirements" + +config BT_DEVICE_APPEARANCE_GATT_WRITABLE_ENCRYPT + bool "Encryption required" + +config DEVICE_APPEARANCE_GATT_WRITABLE_AUTHEN + bool "Encryption and authentication required" + +endchoice #BT_DEVICE_APPEARANCE_GATT_WRITABLE +endif #BT_DEVICE_APPEARANCE_GATT_WRITABLE + +endmenu diff --git a/subsys/bluetooth/services/gap_svc_default.c b/subsys/bluetooth/services/gap_svc_default.c new file mode 100644 index 0000000000000..8bef494d647bc --- /dev/null +++ b/subsys/bluetooth/services/gap_svc_default.c @@ -0,0 +1,175 @@ +/** @file + * @brief GATT Generic Access Service - default implementation + */ +/* + * Copyright (c) 2015-2016 Intel Corporation + * Copyright (c) 2023 Nordic Semiconductor ASA + * Copyright (c) 2025 Koppel Electronic + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#define BT_GATT_CENTRAL_ADDR_RES_NOT_SUPP 0 +#define BT_GATT_CENTRAL_ADDR_RES_SUPP 1 + + +static ssize_t read_name(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + const char *name = bt_get_name(); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, name, + strlen(name)); +} + +#if defined(CONFIG_BT_DEVICE_NAME_GATT_WRITABLE) + +static ssize_t write_name(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, + uint16_t len, uint16_t offset, uint8_t flags) +{ + /* adding one to fit the terminating null character */ + char value[CONFIG_BT_DEVICE_NAME_MAX + 1] = {}; + + if (offset > 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (offset + len > CONFIG_BT_DEVICE_NAME_MAX) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + memcpy(value, buf, len); + + value[len] = '\0'; + + bt_set_name(value); + + return len; +} + +#endif /* CONFIG_BT_DEVICE_NAME_GATT_WRITABLE */ + +static ssize_t read_appearance(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint16_t appearance = sys_cpu_to_le16(bt_get_appearance()); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &appearance, + sizeof(appearance)); +} + +#if defined(CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE) +static ssize_t write_appearance(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) +{ + uint16_t appearance; + int err; + + if (offset > 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (len != sizeof(appearance)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + appearance = sys_get_le16(buf); + + err = bt_set_appearance(appearance); + + if (err) { + return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); + } + + return len; +} +#endif /* CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE */ + +#if defined(CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE) + #define GAP_APPEARANCE_PROPS (BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE) +#if defined(CONFIG_DEVICE_APPEARANCE_GATT_WRITABLE_AUTHEN) + #define GAP_APPEARANCE_PERMS (BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_AUTHEN) +#elif defined(CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE_ENCRYPT) + #define GAP_APPEARANCE_PERMS (BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT) +#else + #define GAP_APPEARANCE_PERMS (BT_GATT_PERM_READ | BT_GATT_PERM_WRITE) +#endif + #define GAP_APPEARANCE_WRITE_HANDLER write_appearance +#else + #define GAP_APPEARANCE_PROPS BT_GATT_CHRC_READ + #define GAP_APPEARANCE_PERMS BT_GATT_PERM_READ + #define GAP_APPEARANCE_WRITE_HANDLER NULL +#endif + +#if defined(CONFIG_BT_GAP_PERIPHERAL_PREF_PARAMS) +static ssize_t read_ppcp(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + struct __packed { + uint16_t min_int; + uint16_t max_int; + uint16_t latency; + uint16_t timeout; + } ppcp; + + ppcp.min_int = sys_cpu_to_le16(CONFIG_BT_PERIPHERAL_PREF_MIN_INT); + ppcp.max_int = sys_cpu_to_le16(CONFIG_BT_PERIPHERAL_PREF_MAX_INT); + ppcp.latency = sys_cpu_to_le16(CONFIG_BT_PERIPHERAL_PREF_LATENCY); + ppcp.timeout = sys_cpu_to_le16(CONFIG_BT_PERIPHERAL_PREF_TIMEOUT); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &ppcp, + sizeof(ppcp)); +} +#endif + +#if defined(CONFIG_BT_CENTRAL) && defined(CONFIG_BT_PRIVACY) +static ssize_t read_central_addr_res(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint8_t central_addr_res = BT_GATT_CENTRAL_ADDR_RES_SUPP; + + return bt_gatt_attr_read(conn, attr, buf, len, offset, + ¢ral_addr_res, sizeof(central_addr_res)); +} +#endif /* CONFIG_BT_CENTRAL && CONFIG_BT_PRIVACY */ + +BT_GATT_SERVICE_DEFINE(BT_GATT_GAP_SVC_DEFAULT_NAME, + BT_GATT_PRIMARY_SERVICE(BT_UUID_GAP), +#if defined(CONFIG_BT_DEVICE_NAME_GATT_WRITABLE) + /* Require pairing for writes to device name */ + BT_GATT_CHARACTERISTIC(BT_UUID_GAP_DEVICE_NAME, + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE, + BT_GATT_PERM_READ | +#if defined(CONFIG_DEVICE_NAME_GATT_WRITABLE_AUTHEN) + BT_GATT_PERM_WRITE_AUTHEN, +#elif defined(CONFIG_DEVICE_NAME_GATT_WRITABLE_ENCRYPT) + BT_GATT_PERM_WRITE_ENCRYPT, +#else + BT_GATT_PERM_WRITE, +#endif + read_name, write_name, NULL), +#else + BT_GATT_CHARACTERISTIC(BT_UUID_GAP_DEVICE_NAME, BT_GATT_CHRC_READ, + BT_GATT_PERM_READ, read_name, NULL, NULL), +#endif /* CONFIG_BT_DEVICE_NAME_GATT_WRITABLE */ + BT_GATT_CHARACTERISTIC(BT_UUID_GAP_APPEARANCE, GAP_APPEARANCE_PROPS, + GAP_APPEARANCE_PERMS, read_appearance, + GAP_APPEARANCE_WRITE_HANDLER, NULL), +#if defined(CONFIG_BT_CENTRAL) && defined(CONFIG_BT_PRIVACY) + BT_GATT_CHARACTERISTIC(BT_UUID_CENTRAL_ADDR_RES, + BT_GATT_CHRC_READ, BT_GATT_PERM_READ, + read_central_addr_res, NULL, NULL), +#endif /* CONFIG_BT_CENTRAL && CONFIG_BT_PRIVACY */ +#if defined(CONFIG_BT_GAP_PERIPHERAL_PREF_PARAMS) + BT_GATT_CHARACTERISTIC(BT_UUID_GAP_PPCP, BT_GATT_CHRC_READ, + BT_GATT_PERM_READ, read_ppcp, NULL, NULL), +#endif +); diff --git a/tests/bluetooth/host/conn/prj.conf b/tests/bluetooth/host/conn/prj.conf index eb8d5a0fdbf79..c199e15baf832 100644 --- a/tests/bluetooth/host/conn/prj.conf +++ b/tests/bluetooth/host/conn/prj.conf @@ -9,6 +9,7 @@ CONFIG_BT_CENTRAL=y CONFIG_BT_EXT_ADV=y CONFIG_BT_PER_ADV=y CONFIG_BT_PER_ADV_SYNC=y +CONFIG_GATT_GAP_SVC_VALIDATE=n CONFIG_BT_MAX_CONN=1 CONFIG_BT_L2CAP_TX_MTU=23 diff --git a/tests/bsim/bluetooth/host/gatt/gap_svc/CMakeLists.txt b/tests/bsim/bluetooth/host/gatt/gap_svc/CMakeLists.txt new file mode 100644 index 0000000000000..9f183ca632c95 --- /dev/null +++ b/tests/bsim/bluetooth/host/gatt/gap_svc/CMakeLists.txt @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +project(gap_svc) + +add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/common/testlib testlib) +target_link_libraries(app PRIVATE testlib) + +add_subdirectory(${ZEPHYR_BASE}/tests/bsim/babblekit babblekit) +target_link_libraries(app PRIVATE babblekit) + +zephyr_include_directories( + ${BSIM_COMPONENTS_PATH}/libUtilv1/src/ + ${BSIM_COMPONENTS_PATH}/libPhyComv1/src/ +) + +target_sources(app PRIVATE + src/main.c + src/central.c + src/peripheral.c +) diff --git a/tests/bsim/bluetooth/host/gatt/gap_svc/prj.conf b/tests/bsim/bluetooth/host/gatt/gap_svc/prj.conf new file mode 100644 index 0000000000000..6bbd0817d1a2a --- /dev/null +++ b/tests/bsim/bluetooth/host/gatt/gap_svc/prj.conf @@ -0,0 +1,42 @@ +CONFIG_BT_TESTING=y + +CONFIG_BT=y +CONFIG_BT_DEVICE_NAME="GapSvc" +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_HRS=y +# Dependency of testlib/adv and testlib/scan. +CONFIG_BT_EXT_ADV=y + +CONFIG_BT_AUTO_PHY_UPDATE=n +CONFIG_BT_GATT_AUTO_UPDATE_MTU=n +CONFIG_BT_AUTO_DATA_LEN_UPDATE=n +CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=n + +CONFIG_BT_GATT_AUTO_DISCOVER_CCC=y +CONFIG_BT_GATT_AUTO_RESUBSCRIBE=n + +CONFIG_BT_SMP=y +CONFIG_BT_GATT_CLIENT=y + + +# Testing locally redefined SVC implementation +CONFIG_BT_GAP_SVC_DEFAULT_IMPL=n + +CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_DEVICE_NAME_GATT_WRITABLE=y + +CONFIG_BT_DEVICE_APPEARANCE_DYNAMIC=y +CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE=y +# Do not care about paramsters below - simpler GAP implementation +CONFIG_BT_PRIVACY=n +CONFIG_BT_GAP_PERIPHERAL_PREF_PARAMS=n + +# Other libraries +CONFIG_LOG=y +CONFIG_ASSERT=y + +CONFIG_THREAD_NAME=y +CONFIG_LOG_THREAD_ID_PREFIX=y + +CONFIG_ARCH_POSIX_TRAP_ON_FATAL=y diff --git a/tests/bsim/bluetooth/host/gatt/gap_svc/src/central.c b/tests/bsim/bluetooth/host/gatt/gap_svc/src/central.c new file mode 100644 index 0000000000000..91187ff9af996 --- /dev/null +++ b/tests/bsim/bluetooth/host/gatt/gap_svc/src/central.c @@ -0,0 +1,359 @@ +/** @file + * @brief Test local GATT Generic Access Service - central role + * + * @note Most of the original code from "../device_name/client.c" used here. + */ +/* + * Copyright (c) 2025 Koppel Electronic + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "testlib/att.h" +#include "testlib/att_read.h" +#include "testlib/att_write.h" +#include "testlib/conn.h" + +#include "babblekit/testcase.h" + +LOG_MODULE_REGISTER(central, LOG_LEVEL_DBG); + +/* Wait time in microseconds for the test to be finished */ +#define WAIT_TIME 10e6 + +static struct bt_conn *default_conn; +static struct bt_conn *connected_conn; +struct k_sem connected_sem; + +static void start_scan(void); + + +static bool eir_found(struct bt_data *data, void *user_data) +{ + bt_addr_le_t *addr = user_data; + + printk("[AD]: %u data_len %u\n", data->type, data->data_len); + + switch (data->type) { + case BT_DATA_UUID16_SOME: + case BT_DATA_UUID16_ALL: + if (data->data_len % sizeof(uint16_t) != 0U) { + printk("AD malformed\n"); + return true; + } + + for (int i = 0; i < data->data_len; i += sizeof(uint16_t)) { + struct bt_conn_le_create_param *create_param; + struct bt_le_conn_param *param; + const struct bt_uuid *uuid; + uint16_t u16; + int err; + + memcpy(&u16, &data->data[i], sizeof(u16)); + uuid = BT_UUID_DECLARE_16(sys_le16_to_cpu(u16)); + if (bt_uuid_cmp(uuid, BT_UUID_HRS)) { + continue; + } + + err = bt_le_scan_stop(); + if (err) { + printk("Stop LE scan failed (err %d)\n", err); + continue; + } + + printk("Creating connection with Coded PHY support\n"); + param = BT_LE_CONN_PARAM_DEFAULT; + create_param = BT_CONN_LE_CREATE_CONN; + create_param->options |= BT_CONN_LE_OPT_CODED; + err = bt_conn_le_create(addr, create_param, param, + &default_conn); + if (err) { + printk("Create connection with Coded PHY support failed (err %d)\n", + err); + + printk("Creating non-Coded PHY connection\n"); + create_param->options &= ~BT_CONN_LE_OPT_CODED; + err = bt_conn_le_create(addr, create_param, + param, &default_conn); + if (err) { + printk("Create connection failed (err %d)\n", err); + start_scan(); + } + } + + return false; + } + default: + break; + } + + return true; +} + +static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, + struct net_buf_simple *ad) +{ + char dev[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(addr, dev, sizeof(dev)); + printk("[DEVICE]: %s, AD evt type %u, AD data len %u, RSSI %i\n", + dev, type, ad->len, rssi); + + /* We're only interested in legacy connectable events or + * possible extended advertising that are connectable. + */ + if (type == BT_GAP_ADV_TYPE_ADV_IND || + type == BT_GAP_ADV_TYPE_ADV_DIRECT_IND || + type == BT_GAP_ADV_TYPE_EXT_ADV) { + bt_data_parse(ad, eir_found, (void *)addr); + } +} + +static void start_scan(void) +{ + int err; + + /* Use active scanning and disable duplicate filtering to handle any + * devices that might update their advertising data at runtime. + */ + struct bt_le_scan_param scan_param = { + .type = BT_LE_SCAN_TYPE_ACTIVE, + .options = BT_LE_SCAN_OPT_CODED, + .interval = BT_GAP_SCAN_FAST_INTERVAL, + .window = BT_GAP_SCAN_FAST_WINDOW, + }; + + err = bt_le_scan_start(&scan_param, device_found); + if (err) { + printk("Scanning with Coded PHY support failed (err %d)\n", err); + + printk("Scanning without Coded PHY\n"); + scan_param.options &= ~BT_LE_SCAN_OPT_CODED; + err = bt_le_scan_start(&scan_param, device_found); + if (err) { + printk("Scanning failed to start (err %d)\n", err); + return; + } + } + + printk("Scanning successfully started\n"); +} + + +static void connected(struct bt_conn *conn, uint8_t conn_err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + if (conn_err) { + printk("Failed to connect to %s (%u)\n", addr, conn_err); + + bt_conn_unref(default_conn); + default_conn = NULL; + + start_scan(); + return; + } + + printk("Connected: %s\n", addr); + + if (conn == default_conn) { + connected_conn = bt_conn_ref(conn); + k_sem_give(&connected_sem); + } +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + printk("Disconnected: %s, reason 0x%02x %s\n", addr, reason, bt_hci_err_to_str(reason)); + + if (default_conn != conn) { + return; + } + + struct bt_conn *conn_to_unref; + + conn_to_unref = connected_conn; + connected_conn = NULL; + bt_conn_unref(conn_to_unref); + + conn_to_unref = default_conn; + default_conn = NULL; + bt_conn_unref(conn_to_unref); + + start_scan(); +} + +static struct bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, +}; + +static void test_gap_name(struct bt_conn *conn) +{ + int err; + char server_new_name[CONFIG_BT_DEVICE_NAME_MAX] = CONFIG_BT_DEVICE_NAME"-up"; + uint16_t chrc_handle; + + NET_BUF_SIMPLE_DEFINE(attr_value_buf, BT_ATT_MAX_ATTRIBUTE_LEN); + + err = bt_testlib_gatt_discover_characteristic(&chrc_handle, + NULL, NULL, conn, + BT_UUID_GAP_DEVICE_NAME, + BT_ATT_FIRST_ATTRIBUTE_HANDLE, + BT_ATT_LAST_ATTRIBUTE_HANDLE); + TEST_ASSERT(err == 0, "Device Name characteristic not found (err %d)", err); + + LOG_DBG("Device Name characteristic found at handle %u", chrc_handle); + + /* Read Device name */ + err = bt_testlib_att_read_by_handle_sync(&attr_value_buf, NULL, NULL, conn, + BT_ATT_CHAN_OPT_UNENHANCED_ONLY, chrc_handle, 0); + TEST_ASSERT(err == 0, "Failed to read characteristic (err %d)", err); + + LOG_DBG("Device Name of the server: %.*s", attr_value_buf.len, attr_value_buf.data); + + net_buf_simple_reset(&attr_value_buf); + + /* Write new Device name */ + err = bt_testlib_att_write(conn, + BT_ATT_CHAN_OPT_UNENHANCED_ONLY, + chrc_handle, + server_new_name, + sizeof(server_new_name)); + TEST_ASSERT(err == BT_ATT_ERR_SUCCESS, "Got ATT error: %d", err); + + /* Verify new Device name */ + err = bt_testlib_att_read_by_handle_sync(&attr_value_buf, NULL, NULL, conn, + BT_ATT_CHAN_OPT_UNENHANCED_ONLY, chrc_handle, 0); + TEST_ASSERT(err == 0, "Failed to read characteristic (err %d)", err); + + TEST_ASSERT(attr_value_buf.len == strlen(server_new_name), + "Unexpected Device Name length: %u (!=%u)", + attr_value_buf.len, sizeof(server_new_name)); + TEST_ASSERT(memcmp(attr_value_buf.data, server_new_name, attr_value_buf.len) == 0, + "Unexpected Device Name value: %.*s", + attr_value_buf.len, attr_value_buf.data); + + net_buf_simple_reset(&attr_value_buf); +} + +static void test_gap_appearance(struct bt_conn *conn) +{ + int err; + uint16_t chrc_handle; + uint16_t appearance; + + NET_BUF_SIMPLE_DEFINE(attr_value_buf, BT_ATT_MAX_ATTRIBUTE_LEN); + + err = bt_testlib_gatt_discover_characteristic(&chrc_handle, + NULL, NULL, conn, + BT_UUID_GAP_APPEARANCE, + BT_ATT_FIRST_ATTRIBUTE_HANDLE, + BT_ATT_LAST_ATTRIBUTE_HANDLE); + TEST_ASSERT(err == 0, "Device Appearance characteristic not found (err %d)", err); + + LOG_DBG("Device Appearance characteristic found at handle %u", chrc_handle); + + /* Read Device appearance */ + err = bt_testlib_att_read_by_handle_sync(&attr_value_buf, NULL, NULL, conn, + BT_ATT_CHAN_OPT_UNENHANCED_ONLY, chrc_handle, 0); + TEST_ASSERT(err == 0, "Failed to read characteristic (err %d)", err); + TEST_ASSERT(attr_value_buf.len == sizeof(appearance), + "Unexpected Appearance length: %u (!=%u)", + attr_value_buf.len, sizeof(uint16_t)); + appearance = sys_le16_to_cpu(*(uint16_t *)attr_value_buf.data); + LOG_DBG("Device Appearance of the server: %.4x", appearance); + net_buf_simple_reset(&attr_value_buf); + + /* Write new Device appearance */ + appearance += 0x100; + err = bt_testlib_att_write(conn, BT_ATT_CHAN_OPT_UNENHANCED_ONLY, chrc_handle, + (char *)&appearance, sizeof(appearance)); + TEST_ASSERT(err == BT_ATT_ERR_SUCCESS, "Got ATT error: %d", err); +} + +static void test_local_gap_svc_central_main(void) +{ + int err; + struct bt_conn *conn; + + k_sem_init(&connected_sem, 0, 1); + bt_conn_cb_register(&conn_callbacks); + + err = bt_enable(NULL); + TEST_ASSERT(err == 0, "Cannot enable Bluetooth (err %d)", err); + + LOG_INF("Bluetooth initialized"); + + start_scan(); + + /* Wait for connection */ + k_sem_take(&connected_sem, K_FOREVER); + + conn = bt_conn_ref(connected_conn); + + err = bt_testlib_att_exchange_mtu(conn); + TEST_ASSERT(err == 0, "Failed to update MTU (err %d)", err); + + test_gap_name(conn); + test_gap_appearance(conn); + + bt_conn_unref(conn); + + TEST_PASS("client"); +} + +static void test_local_gap_svc_central_init(void) +{ + bst_ticker_set_next_tick_absolute(WAIT_TIME); + TEST_START("test_local_gap_svc_central"); +} + +static void test_local_gap_svc_central_tick(bs_time_t HW_device_time) +{ + /* + * If in WAIT_TIME seconds the testcase did not already pass + * (and finish) we consider it failed + */ + if (bst_result != Passed) { + TEST_FAIL("test_local_gap_svc_central failed (not passed after %d seconds)", + (int)(WAIT_TIME / 1e6)); + } +} + + + +static const struct bst_test_instance test_central[] = { + { + .test_id = "central", + .test_descr = "GAP service local reimplementation - central role.", + .test_main_f = test_local_gap_svc_central_main, + .test_pre_init_f = test_local_gap_svc_central_init, + .test_tick_f = test_local_gap_svc_central_tick, + }, + BSTEST_END_MARKER +}; + +struct bst_test_list *test_local_gap_svc_central_install(struct bst_test_list *tests) +{ + tests = bst_add_tests(tests, test_central); + return tests; +} diff --git a/tests/bsim/bluetooth/host/gatt/gap_svc/src/main.c b/tests/bsim/bluetooth/host/gatt/gap_svc/src/main.c new file mode 100644 index 0000000000000..61ca999b2cf2b --- /dev/null +++ b/tests/bsim/bluetooth/host/gatt/gap_svc/src/main.c @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 Koppel Electronic + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "bstests.h" + +extern struct bst_test_list *test_local_gap_svc_central_install(struct bst_test_list *tests); +extern struct bst_test_list *test_local_gap_svc_peripheral_install(struct bst_test_list *tests); + +bst_test_install_t test_installers[] = { + test_local_gap_svc_central_install, + test_local_gap_svc_peripheral_install, + NULL +}; + +int main(void) +{ + bst_main(); + return 0; +} diff --git a/tests/bsim/bluetooth/host/gatt/gap_svc/src/peripheral.c b/tests/bsim/bluetooth/host/gatt/gap_svc/src/peripheral.c new file mode 100644 index 0000000000000..9c4c37d54298b --- /dev/null +++ b/tests/bsim/bluetooth/host/gatt/gap_svc/src/peripheral.c @@ -0,0 +1,256 @@ +/** @file + * @brief Test local GATT Generic Access Service - peripheral role + */ +/* + * Copyright (c) 2025 Koppel Electronic + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include + +#include "babblekit/testcase.h" + + +LOG_MODULE_REGISTER(peripheral, LOG_LEVEL_DBG); + +BUILD_ASSERT(IS_ENABLED(CONFIG_BT_DEVICE_NAME_GATT_WRITABLE), + "This test requires BT_DEVICE_NAME_GATT_WRITABLE to be enabled"); +BUILD_ASSERT(IS_ENABLED(CONFIG_BT_DEVICE_APPEARANCE_GATT_WRITABLE), + "This test requires BT_DEVICE_APPEARANCE_GATT_WRITABLE to be enabled"); +BUILD_ASSERT(!IS_ENABLED(CONFIG_BT_GAP_SVC_DEFAULT_IMPL), + "This test requires BT_GAP_SVC_DEFAULT_IMPL to be disabled"); +BUILD_ASSERT(!IS_ENABLED(CONFIG_BT_PRIVACY), + "Simplified GAP implementation - BT_PRIVACY not implemented"); +BUILD_ASSERT(!IS_ENABLED(CONFIG_BT_GAP_PERIPHERAL_PREF_PARAMS), + "Simplified GAP implementation - BT_GAP_PERIPHERAL_PREF_PARAMS not implemented"); + +/* Wait time in microseconds for the name and appearance to be changed */ +#define WAIT_TIME 10e6 +/* Name changed called in the test */ +bool gap_svc_name_changed; +/* Appearance changed called in the test */ +bool gap_svc_appearance_changed; + +/* Advertising data */ +static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA_BYTES(BT_DATA_UUID16_ALL, + BT_UUID_16_ENCODE(BT_UUID_HRS_VAL)), +}; + +/* ----------------------------------------------------------------------------- + * Local implementation of GAP service + */ + +static ssize_t read_name(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + LOG_DBG("Name read called"); + + const char *name = bt_get_name(); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, name, + strlen(name)); +} + +static ssize_t write_name(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, + uint16_t len, uint16_t offset, uint8_t flags) +{ + LOG_DBG("Name changed called"); + /* adding one to fit the terminating null character */ + char value[CONFIG_BT_DEVICE_NAME_MAX + 1] = {}; + + if (offset != 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (offset + len > CONFIG_BT_DEVICE_NAME_MAX) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + memcpy(value, buf, len); + + value[len] = '\0'; + + bt_set_name(value); + + LOG_INF("Name changed to %s", value); + gap_svc_name_changed = true; + if (gap_svc_appearance_changed) { + TEST_PASS("GAP service name and appearance changed successfully"); + } + + return len; +} + +static ssize_t read_appearance(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint16_t appearance = sys_cpu_to_le16(bt_get_appearance()); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &appearance, sizeof(appearance)); +} + +static ssize_t write_appearance(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) +{ + LOG_DBG("Appearance write called"); + + uint16_t appearance; + int err; + + if (offset > 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (len != sizeof(appearance)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + appearance = sys_get_le16(buf); + + err = bt_set_appearance(appearance); + + if (err) { + return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); + } + + LOG_INF("Appearance changed to 0x%04x", appearance); + gap_svc_appearance_changed = true; + if (gap_svc_name_changed) { + TEST_PASS("GAP service name and appearance changed successfully"); + } + + return len; +} + +BT_GATT_SERVICE_DEFINE(BT_GATT_GAP_SVC_DEFAULT_NAME, + BT_GATT_PRIMARY_SERVICE(BT_UUID_GAP), + /* Require pairing for writes to device name */ + BT_GATT_CHARACTERISTIC(BT_UUID_GAP_DEVICE_NAME, + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE, + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, + read_name, write_name, NULL), + BT_GATT_CHARACTERISTIC(BT_UUID_GAP_APPEARANCE, + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE, + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, + read_appearance, write_appearance, NULL), +); + +/* End of local implementation of GAP service + * --------------------------------------------------------------------------- + */ + +static void connected(struct bt_conn *conn, uint8_t conn_err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + if (conn_err) { + TEST_FAIL("Failed to connect to %s (%u)", addr, conn_err); + return; + } + + gap_svc_name_changed = false; + gap_svc_appearance_changed = false; + + LOG_INF("Connected: %s", addr); +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + LOG_INF("Disconnected: %s (reason 0x%02x)", addr, reason); +} + +static int start_advertising(void) +{ + int err; + + err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), NULL, 0); + if (err) { + TEST_FAIL("Advertising failed to start (err %d)", err); + } + + return err; +} + +static void recycled(void) +{ + start_advertising(); +} + +static struct bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, + .recycled = recycled, +}; + +static void test_local_gap_svc_peripheral_main(void) +{ + int err; + + bt_conn_cb_register(&conn_callbacks); + + err = bt_enable(NULL); + + if (err) { + TEST_FAIL("Bluetooth init failed (err %d)", err); + return; + } + + LOG_INF("Peripheral Bluetooth initialized"); + err = start_advertising(); + if (err) { + return; + } + LOG_INF("Advertising successfully started"); +} + +static void test_local_gap_svc_peripheral_init(void) +{ + bst_ticker_set_next_tick_absolute(WAIT_TIME); + TEST_START("test_local_gap_svc_peripheral"); +} + +static void test_local_gap_svc_peripheral_tick(bs_time_t HW_device_time) + +{ + /* + * If in WAIT_TIME seconds the testcase did not already pass + * (and finish) we consider it failed + */ + if (bst_result != Passed) { + TEST_FAIL("test_local_gap_svc_peripheral failed (not passed after %d seconds)", + (int)(WAIT_TIME / 1e6)); + } +} + +static const struct bst_test_instance test_peripheral[] = { + { + .test_id = "peripheral", + .test_descr = "GAP service local reimplementation - peripheral role.", + .test_main_f = test_local_gap_svc_peripheral_main, + .test_pre_init_f = test_local_gap_svc_peripheral_init, + .test_tick_f = test_local_gap_svc_peripheral_tick, + }, + BSTEST_END_MARKER +}; + +struct bst_test_list *test_local_gap_svc_peripheral_install(struct bst_test_list *tests) +{ + tests = bst_add_tests(tests, test_peripheral); + return tests; +} diff --git a/tests/bsim/bluetooth/host/gatt/gap_svc/test_scripts/run_gap_svc.sh b/tests/bsim/bluetooth/host/gatt/gap_svc/test_scripts/run_gap_svc.sh new file mode 100755 index 0000000000000..0ba0bb939601f --- /dev/null +++ b/tests/bsim/bluetooth/host/gatt/gap_svc/test_scripts/run_gap_svc.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Copyright 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 +set -eu + +source ${ZEPHYR_BASE}/tests/bsim/sh_common.source + +test_name="$(guess_test_long_name)" +simulation_id=${test_name} + +verbosity_level=2 + +EXECUTE_TIMEOUT=120 + +SIM_LEN_US=$((10 * 1000 * 1000)) + +test_exe="${BSIM_OUT_PATH}/bin/bs_${BOARD_TS}_${test_name}_prj_conf" + +cd ${BSIM_OUT_PATH}/bin + +Execute ./bs_2G4_phy_v1 -v=${verbosity_level} -s=${simulation_id} -D=2 -sim_length=${SIM_LEN_US} $@ + +Execute "${test_exe}" -v=${verbosity_level} -s=${simulation_id} -d=0 -rs=420 -testid=peripheral \ + -RealEncryption=1 +Execute "${test_exe}" -v=${verbosity_level} -s=${simulation_id} -d=1 -rs=69 -testid=central \ + -RealEncryption=1 + +wait_for_background_jobs diff --git a/tests/bsim/bluetooth/host/gatt/gap_svc/testcase.yaml b/tests/bsim/bluetooth/host/gatt/gap_svc/testcase.yaml new file mode 100644 index 0000000000000..d48960efda12e --- /dev/null +++ b/tests/bsim/bluetooth/host/gatt/gap_svc/testcase.yaml @@ -0,0 +1,10 @@ +tests: + bluetooth.host.gatt.gap_svc: + build_only: true + tags: + - bluetooth + platform_allow: + - nrf52_bsim/native + harness: bsim + harness_config: + bsim_exe_name: tests_bsim_bluetooth_host_gatt_gap_svc_prj_conf