From 2d1544b863e47baaf44890ce41806d8ce70b24b8 Mon Sep 17 00:00:00 2001 From: Shen Weilong Date: Fri, 14 Jul 2023 17:06:47 +0800 Subject: [PATCH] feat(ble): Added ble examples for multiple connections --- components/bt/controller/esp32c6/Kconfig.in | 2 +- components/bt/controller/esp32h2/Kconfig.in | 2 +- components/bt/host/nimble/Kconfig.in | 12 + components/bt/host/nimble/nimble | 2 +- .../host/nimble/port/include/esp_nimble_cfg.h | 8 + .../esp32c6/include/soc/Kconfig.soc_caps.in | 4 + components/soc/esp32c6/include/soc/soc_caps.h | 1 + .../esp32h2/include/soc/Kconfig.soc_caps.in | 4 + components/soc/esp32h2/include/soc/soc_caps.h | 1 + examples/bluetooth/.build-test-rules.yml | 10 + .../ble_multi_conn_cent/CMakeLists.txt | 8 + .../ble_multi_conn_cent/README.md | 76 +++ .../ble_multi_conn_cent/main/CMakeLists.txt | 5 + .../main/Kconfig.projbuild | 4 + .../main/ble_multi_conn_cent.h | 20 + .../ble_multi_conn_cent/main/gatt_svr.c | 178 +++++++ .../ble_multi_conn_cent/main/main.c | 469 ++++++++++++++++++ .../ble_multi_conn_cent/sdkconfig.defaults | 13 + .../sdkconfig.defaults.esp32h2 | 6 + ...Connections_Central_Example_Walkthrough.md | 242 +++++++++ .../tutorial/phone_screenshot.png | Bin 0 -> 91492 bytes .../ble_multi_conn_prph/CMakeLists.txt | 8 + .../ble_multi_conn_prph/README.md | 114 +++++ .../ble_multi_conn_prph/main/CMakeLists.txt | 5 + .../main/Kconfig.projbuild | 20 + .../main/ble_multi_conn_prph.h | 24 + .../ble_multi_conn_prph/main/gatt_svr.c | 131 +++++ .../ble_multi_conn_prph/main/main.c | 323 ++++++++++++ .../ble_multi_conn_prph/sdkconfig.defaults | 11 + .../sdkconfig.defaults.esp32h2 | 5 + ...nections_Peripheral_Example_Walkthrough.md | 234 +++++++++ .../common/nimble_central_utils/esp_central.h | 17 +- .../nimble/common/nimble_central_utils/peer.c | 48 +- .../nimble_peripheral_utils/esp_peripheral.h | 1 + .../common/nimble_peripheral_utils/misc.c | 15 +- 35 files changed, 2017 insertions(+), 6 deletions(-) create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/CMakeLists.txt create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/README.md create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/CMakeLists.txt create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/Kconfig.projbuild create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/ble_multi_conn_cent.h create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/gatt_svr.c create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/main.c create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/sdkconfig.defaults create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/sdkconfig.defaults.esp32h2 create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/tutorial/Ble_Multiple_Connections_Central_Example_Walkthrough.md create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/tutorial/phone_screenshot.png create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/CMakeLists.txt create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/README.md create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/CMakeLists.txt create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/Kconfig.projbuild create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/ble_multi_conn_prph.h create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/gatt_svr.c create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/main.c create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/sdkconfig.defaults create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/sdkconfig.defaults.esp32h2 create mode 100644 examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/tutorial/Ble_Multiple_Connections_Peripheral_Example_Walkthrough.md diff --git a/components/bt/controller/esp32c6/Kconfig.in b/components/bt/controller/esp32c6/Kconfig.in index dde650827c..4732abb74a 100644 --- a/components/bt/controller/esp32c6/Kconfig.in +++ b/components/bt/controller/esp32c6/Kconfig.in @@ -340,7 +340,7 @@ config BT_LE_LL_SCA config BT_LE_MAX_CONNECTIONS int "Maximum number of concurrent connections" depends on !BT_NIMBLE_ENABLED - range 1 9 + range 1 70 default 3 help Defines maximum number of concurrent BLE connections. For ESP32, user diff --git a/components/bt/controller/esp32h2/Kconfig.in b/components/bt/controller/esp32h2/Kconfig.in index 7d8d7463d4..b836ff7c6f 100644 --- a/components/bt/controller/esp32h2/Kconfig.in +++ b/components/bt/controller/esp32h2/Kconfig.in @@ -340,7 +340,7 @@ config BT_LE_LL_SCA config BT_LE_MAX_CONNECTIONS int "Maximum number of concurrent connections" depends on !BT_NIMBLE_ENABLED - range 1 9 + range 1 35 default 3 help Defines maximum number of concurrent BLE connections. For ESP32, user diff --git a/components/bt/host/nimble/Kconfig.in b/components/bt/host/nimble/Kconfig.in index 869ccad1f1..d76edd00ee 100644 --- a/components/bt/host/nimble/Kconfig.in +++ b/components/bt/host/nimble/Kconfig.in @@ -65,6 +65,8 @@ config BT_NIMBLE_LOG_LEVEL config BT_NIMBLE_MAX_CONNECTIONS int "Maximum number of concurrent connections" range 1 2 if IDF_TARGET_ESP32C2 + range 1 70 if IDF_TARGET_ESP32C6 + range 1 35 if IDF_TARGET_ESP32H2 range 1 9 default 2 if IDF_TARGET_ESP32C2 default 3 @@ -667,3 +669,13 @@ config BT_NIMBLE_VS_SUPPORT help This option is used to enable support for sending Vendor Specific HCI commands and handling Vendor Specific HCI Events. + +config BT_NIMBLE_OPTIMIZE_MULTI_CONN + bool "Enable the optimization of multi-connection" + depends on SOC_BLE_MULTI_CONN_OPTIMIZATION + select BT_NIMBLE_VS_SUPPORT + default n + help + This option enables the use of vendor-specific APIs for multi-connections, which can + greatly enhance the stability of coexistence between numerous central and peripheral + devices. It will prohibit the usage of standard APIs. diff --git a/components/bt/host/nimble/nimble b/components/bt/host/nimble/nimble index ec23739a74..859cddf1f9 160000 --- a/components/bt/host/nimble/nimble +++ b/components/bt/host/nimble/nimble @@ -1 +1 @@ -Subproject commit ec23739a744aff94762d5013fc60d50275ca797a +Subproject commit 859cddf1f9d987d8c31a8b27688714463ed76c99 diff --git a/components/bt/host/nimble/port/include/esp_nimble_cfg.h b/components/bt/host/nimble/port/include/esp_nimble_cfg.h index 66d85102c4..9c60bc2412 100644 --- a/components/bt/host/nimble/port/include/esp_nimble_cfg.h +++ b/components/bt/host/nimble/port/include/esp_nimble_cfg.h @@ -1688,4 +1688,12 @@ #define MYNEWT_VAL_BLE_HCI_VS (0) #endif +#ifndef MYNEWT_VAL_OPTIMIZE_MULTI_CONN +#ifdef CONFIG_BT_NIMBLE_OPTIMIZE_MULTI_CONN +#define MYNEWT_VAL_OPTIMIZE_MULTI_CONN CONFIG_BT_NIMBLE_OPTIMIZE_MULTI_CONN +#else +#define MYNEWT_VAL_OPTIMIZE_MULTI_CONN (0) +#endif +#endif + #endif diff --git a/components/soc/esp32c6/include/soc/Kconfig.soc_caps.in b/components/soc/esp32c6/include/soc/Kconfig.soc_caps.in index b1ace25e8c..df7328d98a 100644 --- a/components/soc/esp32c6/include/soc/Kconfig.soc_caps.in +++ b/components/soc/esp32c6/include/soc/Kconfig.soc_caps.in @@ -1271,6 +1271,10 @@ config SOC_BLUFI_SUPPORTED bool default y +config SOC_BLE_MULTI_CONN_OPTIMIZATION + bool + default y + config SOC_BLE_USE_WIFI_PWR_CLK_WORKAROUND bool default y diff --git a/components/soc/esp32c6/include/soc/soc_caps.h b/components/soc/esp32c6/include/soc/soc_caps.h index e5208eeaf0..f96df405d5 100644 --- a/components/soc/esp32c6/include/soc/soc_caps.h +++ b/components/soc/esp32c6/include/soc/soc_caps.h @@ -520,5 +520,6 @@ #define SOC_BLE_DEVICE_PRIVACY_SUPPORTED (1) /*!< Support BLE device privacy mode */ #define SOC_BLE_POWER_CONTROL_SUPPORTED (1) /*!< Support Bluetooth Power Control */ #define SOC_BLUFI_SUPPORTED (1) /*!< Support BLUFI */ +#define SOC_BLE_MULTI_CONN_OPTIMIZATION (1) /*!< Support multiple connections optimization */ #define SOC_BLE_USE_WIFI_PWR_CLK_WORKAROUND (1) diff --git a/components/soc/esp32h2/include/soc/Kconfig.soc_caps.in b/components/soc/esp32h2/include/soc/Kconfig.soc_caps.in index 314956ccf3..0775719f4a 100644 --- a/components/soc/esp32h2/include/soc/Kconfig.soc_caps.in +++ b/components/soc/esp32h2/include/soc/Kconfig.soc_caps.in @@ -1190,3 +1190,7 @@ config SOC_BLE_DEVICE_PRIVACY_SUPPORTED config SOC_BLE_POWER_CONTROL_SUPPORTED bool default y + +config SOC_BLE_MULTI_CONN_OPTIMIZATION + bool + default y diff --git a/components/soc/esp32h2/include/soc/soc_caps.h b/components/soc/esp32h2/include/soc/soc_caps.h index a5bd04e645..2fe2e09e78 100644 --- a/components/soc/esp32h2/include/soc/soc_caps.h +++ b/components/soc/esp32h2/include/soc/soc_caps.h @@ -489,3 +489,4 @@ #define SOC_BLE_50_SUPPORTED (1) /*!< Support Bluetooth 5.0 */ #define SOC_BLE_DEVICE_PRIVACY_SUPPORTED (1) /*!< Support BLE device privacy mode */ #define SOC_BLE_POWER_CONTROL_SUPPORTED (1) /*!< Support Bluetooth Power Control */ +#define SOC_BLE_MULTI_CONN_OPTIMIZATION (1) /*!< Support multiple connections optimization */ diff --git a/examples/bluetooth/.build-test-rules.yml b/examples/bluetooth/.build-test-rules.yml index 6f36563323..c49ba165ab 100644 --- a/examples/bluetooth/.build-test-rules.yml +++ b/examples/bluetooth/.build-test-rules.yml @@ -162,6 +162,16 @@ examples/bluetooth/nimble/ble_multi_adv: temporary: true reason: The runner doesn't support yet +examples/bluetooth/nimble/ble_multi_conn: + enable: + - if: IDF_TARGET in ["esp32c6", "esp32h2"] + temporary: true + reason: the other targets are not tested yet + disable_test: + - if: IDF_TARGET in ["esp32c6", "esp32h2"] + temporary: true + reason: The runner doesn't support yet + examples/bluetooth/nimble/ble_periodic_adv: enable: - if: IDF_TARGET in ["esp32c2", "esp32c3", "esp32c6" , "esp32s3", "esp32h2" ] diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/CMakeLists.txt b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/CMakeLists.txt new file mode 100644 index 0000000000..06382888f1 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/bluetooth/nimble/common/nimble_central_utils) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(blecent) diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/README.md b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/README.md new file mode 100644 index 0000000000..7b0958fe98 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/README.md @@ -0,0 +1,76 @@ +| Supported Targets | ESP32-C6 | ESP32-H2 | +| ----------------- | -------- | -------- | + +# BLE Multiple Connection Central Example + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +Please check the [tutorial](tutorial/Ble_Multiple_Connections_Central_Example_Walkthrough.md) for more information about this example. + +## How to Use Example + +Before project configuration and build, be sure to set the correct chip target using: + +```bash +idf.py set-target +``` + +### Hardware Required + +* At least two development board with ESP32-C6/ESP32-H2 SoC (e.g., ESP32-C6-DevKitC, ESP32-H2-DevKitC, etc.) +* USB cable for Power supply and programming + +See [Development Boards](https://www.espressif.com/en/products/devkits) for more information about it. + +### Build and Flash + +Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +This is the console output on successful connection: + +``` +controller lib commit: [5cacafa] +I (564) ESP_MULTI_CONN_EXT: BLE Host Task Started +I (564) main_task: Returned from app_main() +I (604) ESP_MULTI_CONN_EXT: Create connection. -> peer addr e0:e1:f5:6f:ec:9d +I (1234) ESP_MULTI_CONN_EXT: Connection established. Handle:1, Total:1 +I (2754) ESP_MULTI_CONN_EXT: Create connection. -> peer addr ee:16:69:80:72:d5 +I (3394) ESP_MULTI_CONN_EXT: Connection established. Handle:2, Total:2 +I (4454) ESP_MULTI_CONN_EXT: Create connection. -> peer addr ef:1a:6e:d6:64:44 +I (5094) ESP_MULTI_CONN_EXT: Connection established. Handle:3, Total:3 +I (6144) ESP_MULTI_CONN_EXT: Create connection. -> peer addr cb:f4:5d:b2:c8:1d +I (6244) ESP_MULTI_CONN_EXT: Connection established. Handle:4, Total:4 +I (6444) ESP_MULTI_CONN_EXT: Create connection. -> peer addr e8:08:e5:ad:61:f6 +I (7394) ESP_MULTI_CONN_EXT: Connection established. Handle:5, Total:5 +I (8504) ESP_MULTI_CONN_EXT: Create connection. -> peer addr c1:53:a8:6f:2a:b4 +I (9124) ESP_MULTI_CONN_EXT: Connection established. Handle:6, Total:6 +I (9274) ESP_MULTI_CONN_EXT: Create connection. -> peer addr dd:fb:5b:13:6a:20 +I (9904) ESP_MULTI_CONN_EXT: Connection established. Handle:7, Total:7 +I (10934) ESP_MULTI_CONN_EXT: Create connection. -> peer addr d5:71:9c:fe:4f:6e +I (11574) ESP_MULTI_CONN_EXT: Connection established. Handle:8, Total:8 +I (12264) ESP_MULTI_CONN_EXT: Create connection. -> peer addr d9:56:91:21:d4:25 +I (12884) ESP_MULTI_CONN_EXT: Connection established. Handle:9, Total:9 +I (13084) ESP_MULTI_CONN_EXT: Create connection. -> peer addr f7:f9:b1:73:38:13 +I (13704) ESP_MULTI_CONN_EXT: Connection established. Handle:10, Total:10 +I (14724) ESP_MULTI_CONN_EXT: Create connection. -> peer addr e7:e5:94:d0:32:78 +I (15354) ESP_MULTI_CONN_EXT: Connection established. Handle:11, Total:11 +I (16404) ESP_MULTI_CONN_EXT: Create connection. -> peer addr fb:c6:f9:46:11:dc +I (17344) ESP_MULTI_CONN_EXT: Connection established. Handle:12, Total:12 +I (18434) ESP_MULTI_CONN_EXT: Create connection. -> peer addr c0:e3:ef:80:e6:fd +I (19374) ESP_MULTI_CONN_EXT: Connection established. Handle:13, Total:13 +I (20484) ESP_MULTI_CONN_EXT: Create connection. -> peer addr d8:9d:6d:b8:c9:40 +I (21104) ESP_MULTI_CONN_EXT: Connection established. Handle:14, Total:14 +I (30814) ESP_MULTI_CONN_EXT: Connected to central. Handle:15 +I (39624) BLE_CENT_SVC: Characteristic write; conn_handle=15 +I (39624) BLE_CENT_SVC: 12 +``` + +## Troubleshooting + +For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/CMakeLists.txt b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/CMakeLists.txt new file mode 100644 index 0000000000..9e539a9fc0 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/CMakeLists.txt @@ -0,0 +1,5 @@ +set(srcs "main.c" + "gatt_svr.c") + +idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS ".") diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/Kconfig.projbuild b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/Kconfig.projbuild new file mode 100644 index 0000000000..5d868c75d0 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/Kconfig.projbuild @@ -0,0 +1,4 @@ +menu "Example Configuration" + + +endmenu diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/ble_multi_conn_cent.h b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/ble_multi_conn_cent.h new file mode 100644 index 0000000000..8c5174e295 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/ble_multi_conn_cent.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef H_BLECENT_ +#define H_BLECENT_ + +#include "esp_central.h" +#ifdef __cplusplus +extern "C" { +#endif + +int gatt_svr_init(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/gatt_svr.c b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/gatt_svr.c new file mode 100644 index 0000000000..05cd00b7ef --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/gatt_svr.c @@ -0,0 +1,178 @@ +/* + * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "esp_log.h" +#include "host/ble_hs.h" +#include "host/ble_uuid.h" +#include "services/gap/ble_svc_gap.h" +#include "services/gatt/ble_svc_gatt.h" +#include "ble_multi_conn_cent.h" + +#define TAG "BLE_MUTLI_CONN_CENT_SVC" + +static const ble_uuid128_t gatt_svr_svc_uuid = + BLE_UUID128_INIT(0x2d, 0x71, 0xa2, 0x59, 0xb4, 0x58, 0xc8, 0x12, + 0x99, 0x99, 0x43, 0x95, 0x12, 0x2f, 0x46, 0x59); + +static uint16_t gatt_svr_chr_val_handle; +static const ble_uuid128_t gatt_svr_chr_uuid = + BLE_UUID128_INIT(0x00, 0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, + 0x22, 0x22, 0x22, 0x22, 0x33, 0x33, 0x33, 0x33); + +static int +gatt_svc_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, + void *arg); + +static const struct ble_gatt_svc_def gatt_svr_svcs[] = { + { + /*** Service ***/ + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &gatt_svr_svc_uuid.u, + .characteristics = (struct ble_gatt_chr_def[]) + { { + .uuid = &gatt_svr_chr_uuid.u, + .access_cb = gatt_svc_access, + .flags = BLE_GATT_CHR_F_WRITE, + .val_handle = &gatt_svr_chr_val_handle, + }, { + 0, /* No more characteristics in this service. */ + } + }, + }, + + { + 0, /* No more services. */ + }, +}; + +static int +gatt_svc_send_to_peers(const struct peer *peer, void *arg) +{ + int rc; + const struct peer_chr *chr; + struct os_mbuf *src_om = arg; + struct os_mbuf *dst_om = NULL; + + chr = peer_chr_find_uuid(peer, (const ble_uuid_t *)&gatt_svr_svc_uuid, + (const ble_uuid_t *)&gatt_svr_chr_uuid); + if (!chr) { + ESP_LOGE(TAG, "Didn't find the characteristic, handle:%d", peer->conn_handle); + goto failed_to_send; + } + + dst_om = ble_hs_mbuf_att_pkt(); + if (!dst_om) { + ESP_LOGE(TAG, "No enough buffer, handle:%d", peer->conn_handle); + goto failed_to_send; + } + + rc = os_mbuf_appendfrom(dst_om, src_om, 0, os_mbuf_len(src_om)); + if (rc) { + ESP_LOGE(TAG, "Failed to copy data, handle:%d, rc:%d", peer->conn_handle, rc); + goto failed_to_send; + } + + rc = ble_gattc_write(peer->conn_handle, chr->chr.val_handle, dst_om, NULL, NULL); + if (rc) { + ESP_LOGE(TAG, "Failed to write, handle:%d, rc:%d", peer->conn_handle, rc); + /* The dst_om has already been freed in ble_gattc_write. */ + dst_om = NULL; + goto failed_to_send; + } + + return 0; + +failed_to_send: + if (dst_om) { + os_mbuf_free_chain(dst_om); + } + + return -1; +} + +static int +gatt_svc_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + uint8_t data[10]; + uint8_t len; + struct os_mbuf *om; + + switch (ctxt->op) { + case BLE_GATT_ACCESS_OP_WRITE_CHR: + ESP_LOGI(TAG, "Characteristic write; conn_handle=%d", conn_handle); + if (attr_handle == gatt_svr_chr_val_handle) { + om = ctxt->om; + len = os_mbuf_len(om); + len = len < sizeof(data) ? len : sizeof(data); + assert(os_mbuf_copydata(om, 0, len, data) == 0); + ESP_LOG_BUFFER_HEX(TAG, data, len); + /* Send the received data to all of peers. */ + peer_traverse_all(gatt_svc_send_to_peers, om); + return 0; + } + goto unknown; + + default: + goto unknown; + } + +unknown: + return BLE_ATT_ERR_UNLIKELY; +} + +void +gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg) +{ + char buf[BLE_UUID_STR_LEN]; + + switch (ctxt->op) { + case BLE_GATT_REGISTER_OP_SVC: + ESP_LOGD(TAG, "registered service %s with handle=%d\n", + ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf), ctxt->svc.handle); + break; + + case BLE_GATT_REGISTER_OP_CHR: + ESP_LOGD(TAG, "registering characteristic %s with def_handle=%d val_handle=%d\n", + ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf), ctxt->chr.def_handle, + ctxt->chr.val_handle); + break; + + case BLE_GATT_REGISTER_OP_DSC: + ESP_LOGD(TAG, "registering descriptor %s with handle=%d\n", + ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf), ctxt->dsc.handle); + break; + + default: + assert(0); + break; + } +} + +int +gatt_svr_init(void) +{ + int rc; + + ble_svc_gap_init(); + ble_svc_gatt_init(); + + rc = ble_gatts_count_cfg(gatt_svr_svcs); + if (rc != 0) { + return rc; + } + + rc = ble_gatts_add_svcs(gatt_svr_svcs); + if (rc != 0) { + + return rc; + } + + return 0; +} diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/main.c b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/main.c new file mode 100644 index 0000000000..e0fdf4c520 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/main/main.c @@ -0,0 +1,469 @@ +/* + * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "esp_log.h" +#include "nvs_flash.h" +/* BLE */ +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "host/ble_hs.h" +#include "host/util/util.h" +#include "services/gap/ble_svc_gap.h" +#include "ble_multi_conn_cent.h" + +#define BLE_PEER_NAME "esp-multi-conn" +#define BLE_PEER_MAX_NUM (MYNEWT_VAL(BLE_MAX_CONNECTIONS) - 1) +#define BLE_PREF_EVT_LEN_MS (5) +#define BLE_PREF_CONN_ITVL_MS (BLE_PEER_MAX_NUM * BLE_PREF_EVT_LEN_MS) + +static const char *TAG = "ESP_MULTI_CONN_CENT"; + +static const ble_uuid_t *remote_svc_uuid = + BLE_UUID128_DECLARE(0x2d, 0x71, 0xa2, 0x59, 0xb4, 0x58, 0xc8, 0x12, + 0x99, 0x99, 0x43, 0x95, 0x12, 0x2f, 0x46, 0x59); + +static uint8_t ext_adv_pattern_1[] = { + 0x02, 0x01, 0x06, + 0x14, 0X09, 'e', 's', 'p', '-', 'b', 'l', 'e', '-', 'r', 'o', 'l', 'e', '-', 'c', 'o', 'e', 'x', '-', 'e', +}; + +void ble_store_config_init(void); +static void ble_cent_advertise(void); +static void ble_cent_scan(void); +static void ble_cent_connect(void *disc); + +static uint8_t s_ble_multi_conn_num = 0; + +/** + * Called when service discovery of the specified peer has completed. + */ +static void +ble_cent_on_disc_complete(const struct peer *peer, int status, void *arg) +{ + + if (status != 0) { + /* Service discovery failed. Terminate the connection. */ + ESP_LOGE(TAG, "Error: Service discovery failed; status=%d conn_handle=%d", status, + peer->conn_handle); + ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM); + return; + } + + /* Service discovery has completed successfully. Now we have a complete + * list of services, characteristics, and descriptors that the peer + * supports. + */ + ESP_LOGD(TAG, "Service discovery complete; status=%d conn_handle=%d\n", status, + peer->conn_handle); +} + + +/** + * The nimble host executes this callback when a GAP event occurs. The application associates a GAP + * event callback with each connection that is established. This callback will be only used by the + * central. + * + * @param event The event being signalled. + * @param arg Application-specified argument; unused by + * blecent. + * + * @return 0 if the application successfully handled the + * event; nonzero on failure. The semantics + * of the return code is specific to the + * particular GAP event being signalled. + */ +static int +ble_cent_client_gap_event(struct ble_gap_event *event, void *arg) +{ + struct ble_hs_adv_fields fields; + int rc; + + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + rc = ble_hs_adv_parse_fields(&fields, event->ext_disc.data, event->ext_disc.length_data); + + /* An advertisment report was received during GAP discovery. */ + if ((rc == 0) && fields.name && (fields.name_len >= strlen(BLE_PEER_NAME)) && + !strncmp((const char *)fields.name, BLE_PEER_NAME, strlen(BLE_PEER_NAME))) { + ble_cent_connect(&event->ext_disc); + } + + return 0; + + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + ESP_LOGI(TAG, "Connection established. Handle:%d, Total:%d", event->connect.conn_handle, + ++s_ble_multi_conn_num); + /* Remember peer. */ + rc = peer_add(event->connect.conn_handle); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to add peer; rc=%d\n", rc); + } else { + /* Perform service discovery */ + rc = peer_disc_svc_by_uuid(event->connect.conn_handle, remote_svc_uuid, + ble_cent_on_disc_complete, NULL); + if(rc != 0) { + ESP_LOGE(TAG, "Failed to discover services; rc=%d\n", rc); + } + } + } else { + /* Connection attempt failed; resume scanning. */ + ESP_LOGE(TAG, "Central: Connection failed; status=0x%x\n", event->connect.status); + } + ble_cent_scan(); + return 0; + + case BLE_GAP_EVENT_DISCONNECT: + /* Connection terminated. */ + print_conn_desc(&event->disconnect.conn); + + /* Forget about peer. */ + peer_delete(event->disconnect.conn.conn_handle); + + ESP_LOGI(TAG, "Central disconnected; Handle:%d, Reason=%d, Total:%d", + event->disconnect.conn.conn_handle, event->disconnect.reason, + --s_ble_multi_conn_num); + + /* Resume scanning. */ + ble_cent_scan(); + return 0; + + case BLE_GAP_EVENT_DISC_COMPLETE: + ESP_LOGI(TAG, "discovery complete; reason=%d\n", event->disc_complete.reason); + return 0; + +#if MYNEWT_VAL(BLE_POWER_CONTROL) + case BLE_GAP_EVENT_TRANSMIT_POWER: + ESP_LOGD(TAG, "Transmit power event : status=%d conn_handle=%d reason=%d phy=%d " + "power_level=%x power_level_flag=%d delta=%d", event->transmit_power.status, + event->transmit_power.conn_handle, event->transmit_power.reason, + event->transmit_power.phy, event->transmit_power.transmit_power_level, + event->transmit_power.transmit_power_level_flag, event->transmit_power.delta); + return 0; + + case BLE_GAP_EVENT_PATHLOSS_THRESHOLD: + ESP_LOGD(TAG, "Pathloss threshold event : conn_handle=%d current path loss=%d " + "zone_entered =%d", event->pathloss_threshold.conn_handle, + event->pathloss_threshold.current_path_loss, event->pathloss_threshold.zone_entered); + return 0; +#endif + + default: + return 0; + } +} + +/** + * The nimble host executes this callback when a GAP event occurs. The application associates a GAP + * event callback with each connection that is established. This callback will be only used by the + * peripheral. + * + * @param event The event being signalled. + * @param arg Application-specified argument; unused by + * blecent. + * + * @return 0 if the application successfully handled the + * event; nonzero on failure. The semantics + * of the return code is specific to the + * particular GAP event being signalled. + */ +static int +ble_cent_server_gap_event(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + /* The connectable adv has been established. We will act as the peripheral. */ + if (event->connect.status == 0) { + ESP_LOGI(TAG, "Peripheral connected to central. Handle:%d", event->connect.conn_handle); + } else { + ESP_LOGE(TAG, "Peripheral: Connection failed; status=0x%x\n", event->connect.status); + ble_cent_advertise(); + } + return 0; + + case BLE_GAP_EVENT_DISCONNECT: + ESP_LOGI(TAG, "Peripheral disconnected; Handle:%d, Reason=%d", + event->disconnect.conn.conn_handle, event->disconnect.reason); + ble_cent_advertise(); + return 0; + +#if MYNEWT_VAL(BLE_POWER_CONTROL) + case BLE_GAP_EVENT_TRANSMIT_POWER: + ESP_LOGD(TAG, "Transmit power event : status=%d conn_handle=%d reason=%d phy=%d " + "power_level=%x power_level_flag=%d delta=%d", event->transmit_power.status, + event->transmit_power.conn_handle, event->transmit_power.reason, + event->transmit_power.phy, event->transmit_power.transmit_power_level, + event->transmit_power.transmit_power_level_flag, event->transmit_power.delta); + return 0; + + case BLE_GAP_EVENT_PATHLOSS_THRESHOLD: + ESP_LOGD(TAG, "Pathloss threshold event : conn_handle=%d current path loss=%d " + "zone_entered =%d", event->pathloss_threshold.conn_handle, + event->pathloss_threshold.current_path_loss, event->pathloss_threshold.zone_entered); + return 0; +#endif + + default: + return 0; + } +} + +/** + * Enables advertising with the following parameters: + * o General discoverable mode. + * o Undirected connectable mode. + */ +static void +ble_cent_advertise(void) +{ + int rc; + struct ble_gap_ext_adv_params params; + struct os_mbuf *data; + uint8_t instance = 0; + + /* First check if any instance is already active */ + if(ble_gap_ext_adv_active(instance)) { + return; + } + + memset (¶ms, 0, sizeof(params)); + + /* Enable connectable advertising */ + params.connectable = 1; + + params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + params.primary_phy = BLE_HCI_LE_PHY_1M; + params.secondary_phy = BLE_HCI_LE_PHY_1M; + params.tx_power = 127; + params.sid = 1; + params.itvl_min = BLE_GAP_ADV_ITVL_MS(300); + params.itvl_max = BLE_GAP_ADV_ITVL_MS(300); + + rc = ble_gap_ext_adv_configure(instance, ¶ms, NULL, + ble_cent_server_gap_event, NULL); + assert(rc == 0); + + /* Get mbuf for adv data */ + data = os_msys_get_pkthdr(sizeof(ext_adv_pattern_1), 0); + assert(data); + rc = os_mbuf_append(data, ext_adv_pattern_1, sizeof(ext_adv_pattern_1)); + assert(rc == 0); + + rc = ble_gap_ext_adv_set_data(instance, data); + assert(rc == 0); + + /* Start advertising */ + rc = ble_gap_ext_adv_start(instance, 0, 0); + assert(rc == 0); + + if (rc) { + ESP_LOGE(TAG, "Failed to enable advertisement; rc=%d\n", rc); + return; + } +} + +/** + * Initiates the GAP general discovery procedure. + */ +static void +ble_cent_scan(void) +{ + int rc; + + if (ble_gap_disc_active()) { + return; + } + + struct ble_gap_ext_disc_params uncoded_disc_params; + struct ble_gap_ext_disc_params coded_disc_params; + + /* Perform a passive scan. I.e., don't send follow-up scan requests to + * each advertiser. + */ + uncoded_disc_params.passive = 1; + uncoded_disc_params.itvl = BLE_GAP_SCAN_ITVL_MS(500); + uncoded_disc_params.window = BLE_GAP_SCAN_WIN_MS(200); + + coded_disc_params.passive = 1; + coded_disc_params.itvl = BLE_GAP_SCAN_ITVL_MS(500); + coded_disc_params.window = BLE_GAP_SCAN_WIN_MS(300); + + /* Tell the controller to filter duplicates; we don't want to process + * repeated advertisements from the same device. + */ + rc = ble_gap_ext_disc(BLE_OWN_ADDR_PUBLIC, 0, 0, 1, 0, 0, &uncoded_disc_params, + &coded_disc_params, ble_cent_client_gap_event, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Error initiating GAP discovery procedure; rc=%d\n", rc); + } +} + +/** + * Connects to the sender of the specified advertisement.The advertisement must contain its full + * name which we will compare with 'BLE_PEER_NAME'. + */ +static void +ble_cent_connect(void *disc) +{ + ble_addr_t own_addr; + ble_addr_t *peer_addr; + struct ble_gap_multi_conn_params multi_conn_params; + struct ble_gap_conn_params uncoded_conn_param; + struct ble_gap_conn_params coded_conn_param; + int rc; + + if (s_ble_multi_conn_num >= BLE_PEER_MAX_NUM) { + return; + } + + /* Scanning must be stopped before a connection can be initiated. */ + rc = ble_gap_disc_cancel(); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to cancel scan; rc=%d\n", rc); + return; + } + + /* We won't connect to the same device. Change our static random address to simulate + * multi-connection with only one central and one peripheral. + */ + rc = ble_hs_id_gen_rnd(0, &own_addr); + assert(rc == 0); + rc = ble_hs_id_set_rnd(own_addr.val); + assert(rc == 0); + + peer_addr = &((struct ble_gap_ext_disc_desc *)disc)->addr; + + /* The connection and scan parameters for uncoded phy (1M & 2M). */ + uncoded_conn_param.scan_itvl = BLE_GAP_SCAN_ITVL_MS(300); + uncoded_conn_param.scan_window = BLE_GAP_SCAN_WIN_MS(100); + uncoded_conn_param.itvl_min = BLE_GAP_CONN_ITVL_MS(BLE_PREF_CONN_ITVL_MS); + uncoded_conn_param.itvl_max = BLE_GAP_CONN_ITVL_MS(BLE_PREF_CONN_ITVL_MS); + uncoded_conn_param.latency = 0; + uncoded_conn_param.supervision_timeout = BLE_GAP_SUPERVISION_TIMEOUT_MS(BLE_PREF_CONN_ITVL_MS * 30); + uncoded_conn_param.min_ce_len = 0; + uncoded_conn_param.max_ce_len = BLE_GAP_CONN_ITVL_MS(BLE_PREF_CONN_ITVL_MS); + + /* The connection and scan parameters for coded phy (125k & 500k) */ + coded_conn_param.scan_itvl = BLE_GAP_SCAN_ITVL_MS(300); + coded_conn_param.scan_window = BLE_GAP_SCAN_WIN_MS(200); + coded_conn_param.itvl_min = BLE_GAP_CONN_ITVL_MS(BLE_PREF_CONN_ITVL_MS); + coded_conn_param.itvl_max = BLE_GAP_CONN_ITVL_MS(BLE_PREF_CONN_ITVL_MS); + coded_conn_param.latency = 0; + coded_conn_param.supervision_timeout = BLE_GAP_SUPERVISION_TIMEOUT_MS(BLE_PREF_CONN_ITVL_MS * 30); + coded_conn_param.min_ce_len = 0; + coded_conn_param.max_ce_len = BLE_GAP_CONN_ITVL_MS(BLE_PREF_CONN_ITVL_MS); + + /* The parameters for multi-connect. We expect that this connection has at least + * BLE_PREF_EVT_LEN_MS every interval to Rx and Tx. + */ + multi_conn_params.scheduling_len_us = BLE_PREF_EVT_LEN_MS * 1000; + multi_conn_params.own_addr_type = BLE_OWN_ADDR_RANDOM; + multi_conn_params.peer_addr = peer_addr; + multi_conn_params.duration_ms = 8000; + multi_conn_params.phy_mask = BLE_GAP_LE_PHY_1M_MASK | BLE_GAP_LE_PHY_2M_MASK | + BLE_GAP_LE_PHY_CODED_MASK; + multi_conn_params.phy_1m_conn_params = &uncoded_conn_param; + multi_conn_params.phy_2m_conn_params = &uncoded_conn_param; + multi_conn_params.phy_coded_conn_params = &coded_conn_param; + + rc = ble_gap_multi_connect(&multi_conn_params, ble_cent_client_gap_event, NULL); + + if (rc) { + ESP_LOGE(TAG, "Error: Failed to connect to device; addr_type=%d addr=%s; rc=%d\n", + peer_addr->type, addr_str(peer_addr->val), rc); + } else { + ESP_LOGI(TAG, "Create connection. -> peer addr %s", addr_str(peer_addr->val)); + } +} + +static void +blecent_on_reset(int reason) +{ + ESP_LOGE(TAG, "Resetting state; reason=%d\n", reason); +} + +static void +blecent_on_sync(void) +{ + int rc; + + /* + * To improve both throughput and stability, it is recommended to set the connection interval + * as an integer multiple of the `MINIMUM_CONN_INTERVAL`. This `MINIMUM_CONN_INTERVAL` should + * be calculated based on the total number of connections and the Transmitter/Receiver phy. + * + * Note that the `MINIMUM_CONN_INTERVAL` value should meet the condition that: + * MINIMUM_CONN_INTERVAL > ((MAX_TIME_OF_PDU * 2) + 150us) * CONN_NUM. + * + * For example, if we have 10 connections, maxmum TX/RX length is 251 and the phy is 1M, then + * the `MINIMUM_CONN_INTERVAL` should be greater than ((261 * 8us) * 2 + 150us) * 10 = 43260us. + * + */ + rc = ble_gap_common_factor_set(true, (BLE_PREF_CONN_ITVL_MS * 1000) / 625); + assert(rc == 0); + + /* Make sure we have proper identity address set (public preferred) */ + rc = ble_hs_util_ensure_addr(0); + assert(rc == 0); + + /* We will function as both the central and peripheral device, connecting to all peripherals + * with the name of BLE_PEER_NAME. Meanwhile, a connectable advertising will be enabled. + * In this example, we register two gap callback functions. + * - ble_cent_client_gap_event: Used by the central. + * - ble_cent_server_gap_event: Used by the peripheral. + */ + ble_cent_advertise(); + ble_cent_scan(); +} + +void blecent_host_task(void *param) +{ + ESP_LOGI(TAG, "BLE Host Task Started"); + /* This function will return only when nimble_port_stop() is executed */ + nimble_port_run(); + + nimble_port_freertos_deinit(); +} + +void +app_main(void) +{ + int rc; + /* Initialize NVS — it is used to store PHY calibration data */ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ret = nimble_port_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to init nimble %d ", ret); + return; + } + + /* Configure the host. */ + ble_hs_cfg.reset_cb = blecent_on_reset; + ble_hs_cfg.sync_cb = blecent_on_sync; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + + /* Initialize data structures to track connected peers. */ + rc = peer_init(BLE_PEER_MAX_NUM, BLE_PEER_MAX_NUM, BLE_PEER_MAX_NUM, BLE_PEER_MAX_NUM); + assert(rc == 0); + + /* Set the default device name. We will act as both central and peripheral. */ + rc = ble_svc_gap_device_name_set("esp-ble-role-coex"); + assert(rc == 0); + + rc = gatt_svr_init(); + assert(rc == 0); + + /* XXX Need to have template for store */ + ble_store_config_init(); + + nimble_port_freertos_init(blecent_host_task); +} diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/sdkconfig.defaults b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/sdkconfig.defaults new file mode 100644 index 0000000000..762eb4d35e --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/sdkconfig.defaults @@ -0,0 +1,13 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_HCI_EVT_BUF_SIZE=70 +CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=70 +CONFIG_BT_NIMBLE_GATT_MAX_PROCS=70 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=100 +CONFIG_BT_NIMBLE_OPTIMIZE_MULTI_CONN=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_BLE_POWER_CONTROL=y diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/sdkconfig.defaults.esp32h2 b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/sdkconfig.defaults.esp32h2 new file mode 100644 index 0000000000..e30be10c13 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/sdkconfig.defaults.esp32h2 @@ -0,0 +1,6 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=35 +CONFIG_BT_NIMBLE_GATT_MAX_PROCS=35 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=50 diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/tutorial/Ble_Multiple_Connections_Central_Example_Walkthrough.md b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/tutorial/Ble_Multiple_Connections_Central_Example_Walkthrough.md new file mode 100644 index 0000000000..5a69870230 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/tutorial/Ble_Multiple_Connections_Central_Example_Walkthrough.md @@ -0,0 +1,242 @@ +# BLE Multiple Connections Central Example Walkthrough + +## Introduction + +In this tutorial, the multiple connection example code for the espressif chipsets with BLE5.0 support is reviewed. The example demonstrates a scenario where dozens of connections are working simultaneously to showcase how to invoke vendor APIs to establish connections and enhance connection stability. While acting as a central device to connect multiple peripherals, it also broadcasts itself as connectable and can be connected by a phone. Once the phone successfully connects, it can perform write operations on all connected devices. + +To minimize the number of development boards, the central and peripheral devices can simulate multiple connections by changing their static random addresses. Therefore, this example only requires two Espressif development boards. Multiple development boards can also be used to simulate a real-world usage scenario. + +## Includes + +This example is located in the examples folder of the ESP-IDF under the [ble_multi_conn_cent/main](../main/). The [main.c](../main/main.c) file located in the main folder contains all the functionality that we are going to review. The header files contained in [main.c](../main/main.c) are: + +```c +#include +#include "esp_log.h" +#include "nvs_flash.h" +/* BLE */ +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "host/ble_hs.h" +#include "host/util/util.h" +#include "services/gap/ble_svc_gap.h" +#include "ble_multi_conn_cent.h" +``` + +These `includes` are required for the FreeRTOS and underlying system components to run, including the logging functionality and a library to store data in non-volatile flash memory. We are interested in `“nimble_port.h”`, `“nimble_port_freertos.h”`, `"ble_hs.h"` and `“ble_svc_gap.h”`, `“ble_multi_conn_cent.h”` which expose the BLE APIs required to implement this example. + +* `nimble_port.h`: Includes the declaration of functions required for the initialization of the nimble stack. +* `nimble_port_freertos.h`: Initializes and enables nimble host task. +* `ble_hs.h`: Defines the functionalities to handle the host event +* `ble_svc_gap.h`:Defines the macros for device name and device appearance and declares the function to set them. +* `ble_multi_conn_cent.h`: Defines the functions used for multiple connections. + +## Main Entry Point + +The program’s entry point is the app_main() function: + +```c +void +app_main(void) +{ + int rc; + /* Initialize NVS — it is used to store PHY calibration data */ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ret = nimble_port_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to init nimble %d ", ret); + return; + } + + /* Configure the host. */ + ble_hs_cfg.reset_cb = blecent_on_reset; + ble_hs_cfg.sync_cb = blecent_on_sync; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + + /* Initialize data structures to track connected peers. */ + rc = peer_init(BLE_PEER_MAX_NUM, BLE_PEER_MAX_NUM, BLE_PEER_MAX_NUM, BLE_PEER_MAX_NUM); + assert(rc == 0); + + /* Set the default device name. We will act as both central and peripheral. */ + rc = ble_svc_gap_device_name_set("esp-ble-role-coex"); + assert(rc == 0); + + rc = gatt_svr_init(); + assert(rc == 0); + + /* XXX Need to have template for store */ + ble_store_config_init(); + + nimble_port_freertos_init(blecent_host_task); +} +``` + +The main function starts by initializing the non-volatile storage library. This library allows us to save the key-value pairs in flash memory.`nvs_flash_init()` stores the PHY calibration data. + +```c +esp_err_t ret = nvs_flash_init(); +if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); +} +ESP_ERROR_CHECK( ret ); +``` + +## BT Controller and Stack Initialization + +The main function calls `nimble_port_init()` to initialize BT Controller and nimble stack. This function initializes the BT controller by first creating its configuration structure named `esp_bt_controller_config_t` with default settings generated by the `BT_CONTROLLER_INIT_CONFIG_DEFAULT()` macro. It implements the Host Controller Interface (HCI) on the controller side, the Link Layer (LL), and the Physical Layer (PHY). The BT Controller is invisible to the user applications and deals with the lower layers of the BLE stack. The controller configuration includes setting the BT controller stack size, priority. With the settings created, the BT controller is initialized and enabled with the `esp_bt_controller_init()` and `esp_bt_controller_enable()` functions: + +```c +esp_bt_controller_config_t config_opts = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); +ret = esp_bt_controller_init(&config_opts); +``` + +Next, the controller is enabled in BLE Mode. + +```c +ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); +``` +>The controller should be enabled in `ESP_BT_MODE_BLE` if you want to use the BLE mode. + +There are four Bluetooth modes supported: + +1. `ESP_BT_MODE_IDLE`: Bluetooth not running +2. `ESP_BT_MODE_BLE`: BLE mode +3. `ESP_BT_MODE_CLASSIC_BT`: BT Classic mode +4. `ESP_BT_MODE_BTDM`: Dual mode (BLE + BT Classic) + +After the initialization of the BT controller, the nimble stack, which includes the common definitions and APIs for BLE, is initialized by using `esp_nimble_init()`: + +```c +esp_err_t esp_nimble_init(void) +{ +#if !SOC_ESP_NIMBLE_CONTROLLER + /* Initialize the function pointers for OS porting */ + npl_freertos_funcs_init(); + + npl_freertos_mempool_init(); + + if(esp_nimble_hci_init() != ESP_OK) { + ESP_LOGE(NIMBLE_PORT_LOG_TAG, "hci inits failed\n"); + return ESP_FAIL; + } + + /* Initialize default event queue */ + ble_npl_eventq_init(&g_eventq_dflt); + /* Initialize the global memory pool */ + os_mempool_module_init(); + os_msys_init(); + +#endif + /* Initialize the host */ + ble_transport_hs_init(); + + return ESP_OK; +} +``` + +The host is configured by setting up the callbacks on Stack-reset, Stack-sync, registration of each GATT resource, and storage status. + +```c + ble_hs_cfg.reset_cb = ble_multi_adv_on_reset; + ble_hs_cfg.sync_cb = ble_multi_adv_on_sync; + ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; +``` + +The main function calls `ble_svc_gap_device_name_set()` to set the default device name. 'esp-ble-role-coex' is passed as the default device name to this function. + +```c +rc = ble_svc_gap_device_name_set("esp-ble-role-coex"); +``` + +main function calls `ble_store_config_init()` to configure the host by setting up the storage callbacks which handle the read, write, and deletion of security material. + +```c +/* XXX Need to have a template for store */ +ble_store_config_init(); +``` + +The main function ends by creating a task where nimble will run using `nimble_port_freertos_init()`. This enables the nimble stack by using `esp_nimble_enable()`. + +```c +nimble_port_freertos_init(ble_multi_adv_host_task); +``` + +`esp_nimble_enable()` create a task where the nimble host will run. It is not strictly necessary to have a separate task for the nimble host, but since something needs to handle the default queue, it is easier to create a separate task. + +## Multiple Connections + +This example will be executed according to the following steps: + +* Call the vendor APIs to set a common factor. All subsequent connections need to set the interval as a multiple of this number to reduce mutual interference between different connections. + + ```c + /* + * To improve both throughput and stability, it is recommended to set the connection interval + * as an integer multiple of the `MINIMUM_CONN_INTERVAL`. This `MINIMUM_CONN_INTERVAL` should + * be calculated based on the total number of connections and the Transmitter/Receiver phy. + * + * Note that the `MINIMUM_CONN_INTERVAL` value should meet the condition that: + * MINIMUM_CONN_INTERVAL > ((MAX_TIME_OF_PDU * 2) + 150us) * CONN_NUM. + * + * For example, if we have 10 connections, maxmum TX/RX length is 251 and the phy is 1M, then + * the `MINIMUM_CONN_INTERVAL` should be greater than ((261 * 8us) * 2 + 150us) * 10 = 43260us. + * + */ + rc = ble_gap_common_factor_set(true, (BLE_PREF_CONN_ITVL_MS * 1000) / 625); + ``` + +* Enable both scan and adv simultaneously. + + ```c + /* We will function as both the central and peripheral device, connecting to all peripherals + * with the name of BLE_PEER_NAME. Meanwhile, a connectable advertising will be enabled. + * In this example, we register two gap callback functions. + * - ble_cent_client_gap_event: Used by the central. + * - ble_cent_server_gap_event: Used by the peripheral. + */ + ble_cent_advertise(); + ble_cent_scan(); + ``` + +* In the callback function of the scan, compare the name from the received adv. If it is the device you want to connect to, initiate the connection with the specified connection parameters. + + > When sending multiple connect requests to the same device, the peer may reject duplicate connections. Therefore, before initiating a connection, it is necessary to change your own static random address. + + ```c + /* The parameters for multi-connect. We expect that this connection has at least + * BLE_PREF_EVT_LEN_MS every interval to Rx and Tx. + */ + multi_conn_params.scheduling_len_us = BLE_PREF_EVT_LEN_MS * 1000; + multi_conn_params.own_addr_type = BLE_OWN_ADDR_RANDOM; + multi_conn_params.peer_addr = peer_addr; + multi_conn_params.duration_ms = 3000; + multi_conn_params.phy_mask = BLE_GAP_LE_PHY_1M_MASK | BLE_GAP_LE_PHY_2M_MASK | + BLE_GAP_LE_PHY_CODED_MASK; + multi_conn_params.phy_1m_conn_params = &uncoded_conn_param; + multi_conn_params.phy_2m_conn_params = &uncoded_conn_param; + multi_conn_params.phy_coded_conn_params = &coded_conn_param; + + rc = ble_gap_multi_connect(&multi_conn_params, ble_cent_client_gap_event, NULL); + ``` + + + +* The connection will be automatically established. When the maximum number of connections is reached, you can connect to a device named "esp-ble-role-coex-e" using your mobile phone (with `nRF Connect` APP). After writing to its characteristic , the central device will forward the received data to all peripherals. + + +![The screenshot of the APP](./phone_screenshot.png) + + + +## Conclusion + +Users can use this example to understand how to use the vendor APIs and experience the stability of multiple connections it brings. + diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/tutorial/phone_screenshot.png b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_cent/tutorial/phone_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..46cb389987ed05769c594f02b03fe1eb0a217ff0 GIT binary patch literal 91492 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�WO3ysIQyt8{h`j8!EV99dc}Ty4b4Z}Y|36o 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�^v3{ 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 +``` + +### Hardware Required + +* At least two development board with ESP32-C6/ESP32-H2 SoC (e.g., ESP32-C6-DevKitC, ESP32-H2-DevKitC, etc.) +* USB cable for Power supply and programming + +See [Development Boards](https://www.espressif.com/en/products/devkits) for more information about it. + +### Build and Flash + +Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +This is the console output on successful connection: + +``` +controller lib commit: [5cacafa] +I (444) ESP_MULTI_CONN_EXT: BLE Host Task Started +I (1854) ESP_MULTI_CONN_EXT: Started adv, Device Address e0:e1:f5:6f:ec:9d +I (6254) ESP_MULTI_CONN_EXT: Connection established. Handle:1. Total:1 +I (6254) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (6504) ESP_MULTI_CONN_EXT: Started adv, Device Address ee:16:69:80:72:d5 +I (8414) ESP_MULTI_CONN_EXT: Connection established. Handle:2. Total:2 +I (8414) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (8654) ESP_MULTI_CONN_EXT: Started adv, Device Address ef:1a:6e:d6:64:44 +I (10124) ESP_MULTI_CONN_EXT: Connection established. Handle:3. Total:3 +I (10124) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (10434) ESP_MULTI_CONN_EXT: Started adv, Device Address cb:f4:5d:b2:c8:1d +I (11264) ESP_MULTI_CONN_EXT: Connection established. Handle:4. Total:4 +I (11264) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (11464) ESP_MULTI_CONN_EXT: Started adv, Device Address e8:08:e5:ad:61:f6 +I (12414) ESP_MULTI_CONN_EXT: Connection established. Handle:5. Total:5 +I (12414) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (12794) ESP_MULTI_CONN_EXT: Started adv, Device Address c1:53:a8:6f:2a:b4 +I (14154) ESP_MULTI_CONN_EXT: Connection established. Handle:6. Total:6 +I (14154) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (14294) ESP_MULTI_CONN_EXT: Started adv, Device Address dd:fb:5b:13:6a:20 +I (14934) ESP_MULTI_CONN_EXT: Connection established. Handle:7. Total:7 +I (14934) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (15324) ESP_MULTI_CONN_EXT: Started adv, Device Address d5:71:9c:fe:4f:6e +I (16594) ESP_MULTI_CONN_EXT: Connection established. Handle:8. Total:8 +I (16594) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (16974) ESP_MULTI_CONN_EXT: Started adv, Device Address d9:56:91:21:d4:25 +I (17904) ESP_MULTI_CONN_EXT: Connection established. Handle:9. Total:9 +I (17914) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (18104) ESP_MULTI_CONN_EXT: Started adv, Device Address f7:f9:b1:73:38:13 +I (18734) ESP_MULTI_CONN_EXT: Connection established. Handle:10. Total:10 +I (18734) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (19004) ESP_MULTI_CONN_EXT: Started adv, Device Address e7:e5:94:d0:32:78 +I (20374) ESP_MULTI_CONN_EXT: Connection established. Handle:11. Total:11 +I (20374) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (20684) ESP_MULTI_CONN_EXT: Started adv, Device Address fb:c6:f9:46:11:dc +I (22374) ESP_MULTI_CONN_EXT: Connection established. Handle:12. Total:12 +I (22374) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (22594) ESP_MULTI_CONN_EXT: Started adv, Device Address c0:e3:ef:80:e6:fd +I (24394) ESP_MULTI_CONN_EXT: Connection established. Handle:13. Total:13 +I (24404) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (24674) ESP_MULTI_CONN_EXT: Started adv, Device Address d8:9d:6d:b8:c9:40 +I (26134) ESP_MULTI_CONN_EXT: Connection established. Handle:14. Total:14 +I (26134) ESP_MULTI_CONN_EXT: advertisement completed. Reason=0. +I (44654) BLE_PRPH_SVC: Characteristic write; conn_handle=1 +I (44654) BLE_PRPH_SVC: 12 +I (44674) BLE_PRPH_SVC: Characteristic write; conn_handle=2 +I (44674) BLE_PRPH_SVC: 12 +I (44684) BLE_PRPH_SVC: Characteristic write; conn_handle=3 +I (44684) BLE_PRPH_SVC: 12 +I (44694) BLE_PRPH_SVC: Characteristic write; conn_handle=4 +I (44694) BLE_PRPH_SVC: 12 +I (44724) BLE_PRPH_SVC: Characteristic write; conn_handle=6 +I (44724) BLE_PRPH_SVC: 12 +I (44734) BLE_PRPH_SVC: Characteristic write; conn_handle=7 +I (44734) BLE_PRPH_SVC: 12 +I (44754) BLE_PRPH_SVC: Characteristic write; conn_handle=8 +I (44754) BLE_PRPH_SVC: 12 +I (44764) BLE_PRPH_SVC: Characteristic write; conn_handle=9 +I (44764) BLE_PRPH_SVC: 12 +I (44784) BLE_PRPH_SVC: Characteristic write; conn_handle=10 +I (44784) BLE_PRPH_SVC: 12 +I (44804) BLE_PRPH_SVC: Characteristic write; conn_handle=12 +I (44804) BLE_PRPH_SVC: 12 +I (44814) BLE_PRPH_SVC: Characteristic write; conn_handle=13 +I (44814) BLE_PRPH_SVC: 12 +I (44834) BLE_PRPH_SVC: Characteristic write; conn_handle=14 +I (44834) BLE_PRPH_SVC: 12 +I (44914) BLE_PRPH_SVC: Characteristic write; conn_handle=5 +I (44914) BLE_PRPH_SVC: 12 +I (45194) BLE_PRPH_SVC: Characteristic write; conn_handle=11 +I (45194) BLE_PRPH_SVC: 12 +``` + +## Troubleshooting + +For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/CMakeLists.txt b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/CMakeLists.txt new file mode 100644 index 0000000000..9e539a9fc0 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/CMakeLists.txt @@ -0,0 +1,5 @@ +set(srcs "main.c" + "gatt_svr.c") + +idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS ".") diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/Kconfig.projbuild b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/Kconfig.projbuild new file mode 100644 index 0000000000..0662463dcb --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/Kconfig.projbuild @@ -0,0 +1,20 @@ +menu "Example Configuration" + + config EXAMPLE_EXTENDED_ADV + bool + depends on SOC_BLE_50_SUPPORTED + default y if SOC_ESP_NIMBLE_CONTROLLER + select BT_NIMBLE_EXT_ADV + prompt "Enable Extended Adv" + help + Use this option to enable extended advertising in the example + + config EXAMPLE_RESTART_ADV_AFTER_CONNECTED + bool + default y + prompt "Restart advertisement when connected" + help + To simulate multiple connections with only one device, + restart the advertisement once a connection has been established. + +endmenu diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/ble_multi_conn_prph.h b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/ble_multi_conn_prph.h new file mode 100644 index 0000000000..c253a63f31 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/ble_multi_conn_prph.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef H_BLEPRPH_ +#define H_BLEPRPH_ + +#include +#include "nimble/ble.h" +#include "modlog/modlog.h" +#include "esp_peripheral.h" +#ifdef __cplusplus +extern "C" { +#endif + +void gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg); +int gatt_svr_init(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/gatt_svr.c b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/gatt_svr.c new file mode 100644 index 0000000000..0e7e982b80 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/gatt_svr.c @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include "esp_log.h" +#include "host/ble_hs.h" +#include "host/ble_uuid.h" +#include "services/gap/ble_svc_gap.h" +#include "services/gatt/ble_svc_gatt.h" +#include "ble_multi_conn_prph.h" + +#define TAG "BLE_MUTLI_CONN_PRPH_SVC" + +static const ble_uuid128_t gatt_svr_svc_uuid = + BLE_UUID128_INIT(0x2d, 0x71, 0xa2, 0x59, 0xb4, 0x58, 0xc8, 0x12, + 0x99, 0x99, 0x43, 0x95, 0x12, 0x2f, 0x46, 0x59); + +/* A characteristic that can be subscribed to */ +static uint16_t gatt_svr_chr_val_handle; +static const ble_uuid128_t gatt_svr_chr_uuid = + BLE_UUID128_INIT(0x00, 0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, + 0x22, 0x22, 0x22, 0x22, 0x33, 0x33, 0x33, 0x33); + + +static int +gatt_svc_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, + void *arg); + +static const struct ble_gatt_svc_def gatt_svr_svcs[] = { + { + /*** Service ***/ + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &gatt_svr_svc_uuid.u, + .characteristics = (struct ble_gatt_chr_def[]) + { { + .uuid = &gatt_svr_chr_uuid.u, + .access_cb = gatt_svc_access, + .flags = BLE_GATT_CHR_F_WRITE, + .val_handle = &gatt_svr_chr_val_handle, + }, { + 0, /* No more characteristics in this service. */ + } + }, + }, + + { + 0, /* No more services. */ + }, +}; + +static int +gatt_svc_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + uint8_t data[10]; + uint8_t len; + struct os_mbuf *om; + + switch (ctxt->op) { + case BLE_GATT_ACCESS_OP_WRITE_CHR: + ESP_LOGI(TAG, "Characteristic write; conn_handle=%d", conn_handle); + if (attr_handle == gatt_svr_chr_val_handle) { + om = ctxt->om; + len = os_mbuf_len(om); + len = len < sizeof(data) ? len : sizeof(data); + assert(os_mbuf_copydata(om, 0, len, data) == 0); + ESP_LOG_BUFFER_HEX(TAG, data, len); + return 0; + } + goto unknown; + + default: + goto unknown; + } + +unknown: + return BLE_ATT_ERR_UNLIKELY; +} + +void +gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg) +{ + char buf[BLE_UUID_STR_LEN]; + + switch (ctxt->op) { + case BLE_GATT_REGISTER_OP_SVC: + ESP_LOGD(TAG, "registered service %s with handle=%d\n", + ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf), ctxt->svc.handle); + break; + + case BLE_GATT_REGISTER_OP_CHR: + ESP_LOGD(TAG, "registering characteristic %s with def_handle=%d val_handle=%d\n", + ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf), ctxt->chr.def_handle, + ctxt->chr.val_handle); + break; + + case BLE_GATT_REGISTER_OP_DSC: + ESP_LOGD(TAG, "registering descriptor %s with handle=%d\n", + ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf), ctxt->dsc.handle); + break; + + default: + assert(0); + break; + } +} + +int +gatt_svr_init(void) +{ + int rc; + + ble_svc_gap_init(); + ble_svc_gatt_init(); + + rc = ble_gatts_count_cfg(gatt_svr_svcs); + if (rc != 0) { + return rc; + } + + rc = ble_gatts_add_svcs(gatt_svr_svcs); + if (rc != 0) { + return rc; + } + + return 0; +} diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/main.c b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/main.c new file mode 100644 index 0000000000..e067f618ff --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/main/main.c @@ -0,0 +1,323 @@ +/* + * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_random.h" +/* BLE */ +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "host/ble_hs.h" +#include "services/gap/ble_svc_gap.h" +#include "ble_multi_conn_prph.h" + +#if CONFIG_EXAMPLE_EXTENDED_ADV +static uint8_t ext_adv_pattern_1[] = { + 0x02, 0x01, 0x06, + 0x03, 0x03, 0xab, 0xcd, + 0x03, 0x03, 0x18, 0x11, + 0x11, 0X09, 'e', 's', 'p', '-', 'm', 'u', 'l', 't', 'i', '-', 'c', 'o', 'n', 'n', '-', 'e', +}; +#endif + +static const char *TAG = "ESP_MULTI_CONN_PRPH"; +static uint8_t s_ble_prph_conn_num = 0; +static SemaphoreHandle_t s_sem_restart_adv = NULL; + +static int ble_prph_gap_event(struct ble_gap_event *event, void *arg); +void ble_store_config_init(void); + +/** + * Enables advertising with the following parameters: + * o General discoverable mode. + * o Undirected connectable mode. + */ +static void +ble_prph_advertise(void) +{ + int rc; + ble_addr_t addr; + + if (s_ble_prph_conn_num >= CONFIG_BT_NIMBLE_MAX_CONNECTIONS) { + return; + } + +#if CONFIG_EXAMPLE_EXTENDED_ADV + struct ble_gap_ext_adv_params params; + struct os_mbuf *data; + uint8_t instance = 0; + + /* First check if any instance is already active */ + if(ble_gap_ext_adv_active(instance)) { + return; + } + + memset (¶ms, 0, sizeof(params)); + + /* enable connectable advertising */ + params.connectable = 1; + + /* advertise using random addr */ + params.own_addr_type = BLE_OWN_ADDR_RANDOM; + params.primary_phy = BLE_HCI_LE_PHY_1M; + params.secondary_phy = BLE_HCI_LE_PHY_2M; + params.tx_power = 127; + params.sid = 1; + params.itvl_min = BLE_GAP_ADV_ITVL_MS(100); + params.itvl_max = BLE_GAP_ADV_ITVL_MS(100); + + rc = ble_gap_ext_adv_configure(instance, ¶ms, NULL, + ble_prph_gap_event, NULL); + assert(rc == 0); + + data = os_msys_get_pkthdr(sizeof(ext_adv_pattern_1), 0); + assert(data); + rc = os_mbuf_append(data, ext_adv_pattern_1, sizeof(ext_adv_pattern_1)); + assert(rc == 0); + rc = ble_gap_ext_adv_set_data(instance, data); + assert(rc == 0); + + /* We won't connect to a connected device. Change our static random address to simulate + * multi-connection with only one central and one peripheral. + */ + rc = ble_hs_id_gen_rnd(0, &addr); + assert(rc == 0); + + rc = ble_gap_ext_adv_set_addr(instance, &addr); + assert(rc == 0); + + /* start advertising */ + rc = ble_gap_ext_adv_start(instance, 0, 0); + assert(rc == 0); +#else + struct ble_gap_adv_params adv_params; + struct ble_hs_adv_fields fields; + const char *name; + + if (ble_gap_adv_active()) { + return; + } + + /* We won't connect to a connected device. Change our static random address to simulate + * multi-connection with only one central and one peripheral. + */ + rc = ble_hs_id_gen_rnd(0, &addr); + assert(rc == 0); + /* set generated address */ + rc = ble_hs_id_set_rnd(addr.val); + assert(rc == 0); + + /** + * Set the advertisement data included in our advertisements: + * o Flags (indicates advertisement type and other general info). + * o Advertising tx power. + * o Device name. + */ + memset(&fields, 0, sizeof fields); + + /* Advertise two flags: + * o Discoverability in forthcoming advertisement (general) + * o BLE-only (BR/EDR unsupported). + */ + fields.flags = BLE_HS_ADV_F_DISC_GEN | + BLE_HS_ADV_F_BREDR_UNSUP; + + /* Indicate that the TX power level field should be included; have the + * stack fill this value automatically. This is done by assigning the + * special value BLE_HS_ADV_TX_PWR_LVL_AUTO. + */ + fields.tx_pwr_lvl_is_present = 1; + fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; + + name = ble_svc_gap_device_name(); + fields.name = (uint8_t *)name; + fields.name_len = strlen(name); + fields.name_is_complete = 1; + + rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + ESP_LOGE(TAG, "failed to set advertisement data; rc=%d\n", rc); + return; + } + + /* Begin advertising. */ + memset(&adv_params, 0, sizeof adv_params); + adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; + adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; + rc = ble_gap_adv_start(BLE_OWN_ADDR_RANDOM, NULL, BLE_HS_FOREVER, + &adv_params, ble_prph_gap_event, NULL); +#endif // CONFIG_EXAMPLE_EXTENDED_ADV + + if (rc) { + ESP_LOGE(TAG, "Failed to enable advertisement; rc=%d\n", rc); + return; + } else { + ESP_LOGI(TAG, "Started adv, Device Address %s", addr_str(addr.val)); + } +} + +static void +ble_prph_restart_adv(void) +{ +#if CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED + if (!xSemaphoreGive(s_sem_restart_adv)) { + ESP_LOGE(TAG, "Failed to give Semaphor"); + } +#else + ble_prph_advertise(); +#endif // CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED +} + +/** + * The nimble host executes this callback when a GAP event occurs. The + * application associates a GAP event callback with each connection that forms. + * bleprph uses the same callback for all connections. + * + * @param event The type of event being signalled. + * @param ctxt Various information pertaining to the event. + * @param arg Application-specified argument; unused by + * bleprph. + * + * @return 0 if the application successfully handled the + * event; nonzero on failure. The semantics + * of the return code is specific to the + * particular GAP event being signalled. + */ +static int +ble_prph_gap_event(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + /* A new connection was established. */ + ESP_LOGI(TAG, "Connection established. Handle:%d. Total:%d", event->connect.conn_handle, + ++s_ble_prph_conn_num); +#if !CONFIG_EXAMPLE_EXTENDED_ADV && CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED + ble_prph_restart_adv(); +#endif // !CONFIG_EXAMPLE_EXTENDED_ADV && CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED + } else { + /* Restart the advertising */ + ble_prph_restart_adv(); + } + return 0; + + case BLE_GAP_EVENT_DISCONNECT: + ESP_LOGI(TAG, "Disconnect. Handle:%d. Reason=%d. Total:%d", + event->disconnect.conn.conn_handle, event->disconnect.reason, --s_ble_prph_conn_num); + + /* Connection terminated; resume advertising. */ + ble_prph_restart_adv(); + return 0; + +#if CONFIG_EXAMPLE_EXTENDED_ADV + case BLE_GAP_EVENT_ADV_COMPLETE: + ESP_LOGI(TAG, "advertisement completed. Reason=%d.",event->adv_complete.reason); +#if CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED + ble_prph_restart_adv(); +#endif // CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED + return 0; +#endif // CONFIG_EXAMPLE_EXTENDED_ADV + +#if MYNEWT_VAL(BLE_POWER_CONTROL) + case BLE_GAP_EVENT_TRANSMIT_POWER: + ESP_LOGD(TAG, "Transmit power event : status=%d conn_handle=%d reason=%d " + "phy=%d power_level=%x power_level_flag=%d delta=%d", + event->transmit_power.status, event->transmit_power.conn_handle, + event->transmit_power.reason, event->transmit_power.phy, + event->transmit_power.transmit_power_level, + event->transmit_power.transmit_power_level_flag, event->transmit_power.delta); + return 0; + + case BLE_GAP_EVENT_PATHLOSS_THRESHOLD: + ESP_LOGD(TAG, "Pathloss threshold event : conn_handle=%d current path loss=%d " + "zone_entered =%d", event->pathloss_threshold.conn_handle, + event->pathloss_threshold.current_path_loss, event->pathloss_threshold.zone_entered); + return 0; +#endif + } + + return 0; +} + +static void +bleprph_on_reset(int reason) +{ + ESP_LOGE(TAG, "Resetting state; reason=%d\n", reason); +} + +static void +bleprph_on_sync(void) +{ + /* Begin advertising. */ + ble_prph_advertise(); +} + +void bleprph_host_task(void *param) +{ + ESP_LOGI(TAG, "BLE Host Task Started"); + /* This function will return only when nimble_port_stop() is executed */ + nimble_port_run(); + + nimble_port_freertos_deinit(); +} + +void +app_main(void) +{ + int rc; + + /* Initialize NVS — it is used to store PHY calibration data */ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + s_sem_restart_adv = xSemaphoreCreateBinary(); + assert(s_sem_restart_adv); + + ret = nimble_port_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to init nimble %d ", ret); + return; + } + /* Initialize the NimBLE host configuration. */ + ble_hs_cfg.reset_cb = bleprph_on_reset; + ble_hs_cfg.sync_cb = bleprph_on_sync; + ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + + rc = gatt_svr_init(); + assert(rc == 0); + + /* Set the default device name. */ + rc = ble_svc_gap_device_name_set("esp-multi-conn"); + assert(rc == 0); + + /* XXX Need to have template for store */ + ble_store_config_init(); + + nimble_port_freertos_init(bleprph_host_task); + +#if CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED + int delay_ms; + + /* Restart the advertising if the connection has been established successfully. This can + * help to simulate multiple devices with only one peripheral development board. + */ + while (true) + { + if (xSemaphoreTake(s_sem_restart_adv, portMAX_DELAY)) { + /* Delay a random time to increase the randomness of the test. */ + delay_ms = (esp_random() % 300) + 100; + vTaskDelay(pdMS_TO_TICKS(delay_ms)); + ble_prph_advertise(); + } else { + ESP_LOGE(TAG, "Failed to take Semaphor"); + } + } +#endif // CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED +} diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/sdkconfig.defaults b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/sdkconfig.defaults new file mode 100644 index 0000000000..6040051b36 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/sdkconfig.defaults @@ -0,0 +1,11 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_HCI_EVT_BUF_SIZE=70 +CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=69 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=100 +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_BLE_POWER_CONTROL=y diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/sdkconfig.defaults.esp32h2 b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/sdkconfig.defaults.esp32h2 new file mode 100644 index 0000000000..f0cb07854e --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/sdkconfig.defaults.esp32h2 @@ -0,0 +1,5 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=34 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=50 diff --git a/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/tutorial/Ble_Multiple_Connections_Peripheral_Example_Walkthrough.md b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/tutorial/Ble_Multiple_Connections_Peripheral_Example_Walkthrough.md new file mode 100644 index 0000000000..3e15818237 --- /dev/null +++ b/examples/bluetooth/nimble/ble_multi_conn/ble_multi_conn_prph/tutorial/Ble_Multiple_Connections_Peripheral_Example_Walkthrough.md @@ -0,0 +1,234 @@ +# BLE Multiple Connections Peripheral Example Walkthrough + +## Introduction + +In this tutorial, the multiple connection example code for the espressif chipsets with BLE5.0 support is reviewed. The example demonstrates a scenario where dozens of connections are working simultaneously to showcase how to invoke vendor APIs to establish connections and enhance connection stability. While acting as a central device to connect multiple peripherals, it also broadcasts itself as connectable and can be connected by a phone. Once the phone successfully connects, it can perform write operations on all connected devices. + +To minimize the number of development boards, the central and peripheral devices can simulate multiple connections by changing their static random addresses. Therefore, this example only requires two Espressif development boards. Multiple development boards can also be used to simulate a real-world usage scenario. + +## Includes + +This example is located in the examples folder of the ESP-IDF under the [ble_multi_conn_prph/main](../main/). The [main.c](../main/main.c) file located in the main folder contains all the functionality that we are going to review. The header files contained in [main.c](../main/main.c) are: + +```c +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_random.h" +/* BLE */ +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "host/ble_hs.h" +#include "services/gap/ble_svc_gap.h" +#include "ble_multi_conn_prph.h" +``` + +These `includes` are required for the FreeRTOS and underlying system components to run, including the logging functionality and a library to store data in non-volatile flash memory. We are interested in `“nimble_port.h”`, `“nimble_port_freertos.h”`, `"ble_hs.h"` and `“ble_svc_gap.h”`, `“ble_multi_conn_prph.h”` which expose the BLE APIs required to implement this example. + +* `nimble_port.h`: Includes the declaration of functions required for the initialization of the nimble stack. +* `nimble_port_freertos.h`: Initializes and enables nimble host task. +* `ble_hs.h`: Defines the functionalities to handle the host event +* `ble_svc_gap.h`:Defines the macros for device name and device appearance and declares the function to set them. +* `ble_multi_conn_prph.h`: Defines the functions used for multiple connections. + +## Main Entry Point + +The program’s entry point is the app_main() function: + +```c +void +app_main(void) +{ + int rc; + + /* Initialize NVS — it is used to store PHY calibration data */ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + s_sem_restart_adv = xSemaphoreCreateBinary(); + assert(s_sem_restart_adv); + + ret = nimble_port_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to init nimble %d ", ret); + return; + } + /* Initialize the NimBLE host configuration. */ + ble_hs_cfg.reset_cb = bleprph_on_reset; + ble_hs_cfg.sync_cb = bleprph_on_sync; + ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + + rc = gatt_svr_init(); + assert(rc == 0); + + /* Set the default device name. */ + rc = ble_svc_gap_device_name_set("esp-multi-conn"); + assert(rc == 0); + + /* XXX Need to have template for store */ + ble_store_config_init(); + + nimble_port_freertos_init(bleprph_host_task); + +#if CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED + int delay_ms; + + /* Restart the advertising if the connection has been established successfully. This can + * help to simulate multiple devices with only one peripheral development board. + */ + while (true) + { + if (xSemaphoreTake(s_sem_restart_adv, portMAX_DELAY)) { + /* Delay a random time to increase the randomness of the test. */ + delay_ms = (esp_random() % 300) + 100; + vTaskDelay(pdMS_TO_TICKS(delay_ms)); + ble_prph_advertise(); + } else { + ESP_LOGE(TAG, "Failed to take Semaphor"); + } + } +#endif // CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED +} +``` + +The main function starts by initializing the non-volatile storage library. This library allows us to save the key-value pairs in flash memory.`nvs_flash_init()` stores the PHY calibration data. + +```c +esp_err_t ret = nvs_flash_init(); +if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); +} +ESP_ERROR_CHECK( ret ); +``` + +## BT Controller and Stack Initialization + +The main function calls `nimble_port_init()` to initialize BT Controller and nimble stack. This function initializes the BT controller by first creating its configuration structure named `esp_bt_controller_config_t` with default settings generated by the `BT_CONTROLLER_INIT_CONFIG_DEFAULT()` macro. It implements the Host Controller Interface (HCI) on the controller side, the Link Layer (LL), and the Physical Layer (PHY). The BT Controller is invisible to the user applications and deals with the lower layers of the BLE stack. The controller configuration includes setting the BT controller stack size, priority. With the settings created, the BT controller is initialized and enabled with the `esp_bt_controller_init()` and `esp_bt_controller_enable()` functions: + +```c +esp_bt_controller_config_t config_opts = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); +ret = esp_bt_controller_init(&config_opts); +``` + +Next, the controller is enabled in BLE Mode. + +```c +ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); +``` +>The controller should be enabled in `ESP_BT_MODE_BLE` if you want to use the BLE mode. + +There are four Bluetooth modes supported: + +1. `ESP_BT_MODE_IDLE`: Bluetooth not running +2. `ESP_BT_MODE_BLE`: BLE mode +3. `ESP_BT_MODE_CLASSIC_BT`: BT Classic mode +4. `ESP_BT_MODE_BTDM`: Dual mode (BLE + BT Classic) + +After the initialization of the BT controller, the nimble stack, which includes the common definitions and APIs for BLE, is initialized by using `esp_nimble_init()`: + +```c +esp_err_t esp_nimble_init(void) +{ +#if !SOC_ESP_NIMBLE_CONTROLLER + /* Initialize the function pointers for OS porting */ + npl_freertos_funcs_init(); + + npl_freertos_mempool_init(); + + if(esp_nimble_hci_init() != ESP_OK) { + ESP_LOGE(NIMBLE_PORT_LOG_TAG, "hci inits failed\n"); + return ESP_FAIL; + } + + /* Initialize default event queue */ + ble_npl_eventq_init(&g_eventq_dflt); + /* Initialize the global memory pool */ + os_mempool_module_init(); + os_msys_init(); + +#endif + /* Initialize the host */ + ble_transport_hs_init(); + + return ESP_OK; +} +``` + +The host is configured by setting up the callbacks on Stack-reset, Stack-sync, registration of each GATT resource, and storage status. + +```c + ble_hs_cfg.reset_cb = ble_multi_adv_on_reset; + ble_hs_cfg.sync_cb = ble_multi_adv_on_sync; + ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; +``` + +The main function calls `ble_svc_gap_device_name_set()` to set the default device name. 'esp-multi-conn' is passed as the default device name to this function. + +```c +rc = ble_svc_gap_device_name_set("esp-multi-conn"); +``` + +main function calls `ble_store_config_init()` to configure the host by setting up the storage callbacks which handle the read, write, and deletion of security material. + +```c +/* XXX Need to have a template for store */ +ble_store_config_init(); +``` + +The main function ends by creating a task where nimble will run using `nimble_port_freertos_init()`. This enables the nimble stack by using `esp_nimble_enable()`. + +```c +nimble_port_freertos_init(ble_multi_adv_host_task); +``` + +`esp_nimble_enable()` create a task where the nimble host will run. It is not strictly necessary to have a separate task for the nimble host, but since something needs to handle the default queue, it is easier to create a separate task. + +## Multiple Connections + +This example will be executed according to the following steps: + +* Set the advertising data (Adv data) and enable connectable advertising + + > Set the “name” field in the Adv data as “esp-multi-conn” to allow the counterpart devices to recognize and initiate a connection. + +* After a successful connection, call "ble_prph_gap_event" and send a semaphore to the main task. + +* To simulate a multi-connection scenario using a development board, "app_main" function is modified to receive a semaphore and perform a random delay. This approach will increase the randomness of the example and better simulate real-world usage scenarios. + + You can enable or disable this functionality through the "EXAMPLE_RESTART_ADV_AFTER_CONNECTED" in the menuconfig. Additionally, using multiple development boards will further enhance the simulation of real-world usage scenarios. + + ```c + int delay_ms; + + /* Restart the advertising if the connection has been established successfully. This can + * help to simulate multiple devices with only one peripheral development board. + */ + while (true) + { + if (xSemaphoreTake(s_sem_restart_adv, portMAX_DELAY)) { + /* Delay a random time to increase the randomness of the test. */ + delay_ms = (esp_random() % 300) + 100; + vTaskDelay(pdMS_TO_TICKS(delay_ms)); + ble_prph_advertise(); + } else { + ESP_LOGE(TAG, "Failed to take Semaphor"); + } + } + ``` + +* Connections will be created automatically. When the maximum number of connections is reached, sending connectable advertisements will be stopped. + +* You can use a mobile phone to connect to the central device and perform write operations on all local connections. + + > For more details, please check [Central tutorial](../../ble_multi_conn_cent/tutorial/Ble_Multiple_Connections_Central_Example_Walkthrough.md) + +## Conclusion + +Users can use this example to understand how to use the vendor APIs and experience the stability of multiple connections it brings. + diff --git a/examples/bluetooth/nimble/common/nimble_central_utils/esp_central.h b/examples/bluetooth/nimble/common/nimble_central_utils/esp_central.h index 740275d426..573f2a62c3 100644 --- a/examples/bluetooth/nimble/common/nimble_central_utils/esp_central.h +++ b/examples/bluetooth/nimble/common/nimble_central_utils/esp_central.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -48,6 +48,16 @@ SLIST_HEAD(peer_svc_list, peer_svc); struct peer; typedef void peer_disc_fn(const struct peer *peer, int status, void *arg); +/** + * @brief The callback function for the devices traversal. + * + * @param peer + * @param arg + * @return int 0, continue; Others, stop the traversal. + * + */ +typedef int peer_traverse_fn(const struct peer *peer, void *arg); + struct peer { SLIST_ENTRY(peer) next; @@ -65,6 +75,11 @@ struct peer { void *disc_cb_arg; }; +void peer_traverse_all(peer_traverse_fn *trav_cb, void *arg); + +int peer_disc_svc_by_uuid(uint16_t conn_handle, const ble_uuid_t *uuid, peer_disc_fn *disc_cb, + void *disc_cb_arg); + int peer_disc_all(uint16_t conn_handle, peer_disc_fn *disc_cb, void *disc_cb_arg); const struct peer_dsc * diff --git a/examples/bluetooth/nimble/common/nimble_central_utils/peer.c b/examples/bluetooth/nimble/common/nimble_central_utils/peer.c index 30cb1338af..80515884ba 100644 --- a/examples/bluetooth/nimble/common/nimble_central_utils/peer.c +++ b/examples/bluetooth/nimble/common/nimble_central_utils/peer.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -619,6 +619,36 @@ peer_svc_disced(uint16_t conn_handle, const struct ble_gatt_error *error, return rc; } +int +peer_disc_svc_by_uuid(uint16_t conn_handle, const ble_uuid_t *uuid, peer_disc_fn *disc_cb, + void *disc_cb_arg) +{ + struct peer_svc *svc; + struct peer *peer; + int rc; + + peer = peer_find(conn_handle); + if (peer == NULL) { + return BLE_HS_ENOTCONN; + } + + /* Undiscover everything first. */ + while ((svc = SLIST_FIRST(&peer->svcs)) != NULL) { + SLIST_REMOVE_HEAD(&peer->svcs, next); + peer_svc_delete(svc); + } + + peer->disc_prev_chr_val = 1; + peer->disc_cb = disc_cb; + peer->disc_cb_arg = disc_cb_arg; + + rc = ble_gattc_disc_svc_by_uuid(conn_handle, uuid, peer_svc_disced, peer); + if (rc != 0) { + return rc; + } + + return 0; +} int peer_disc_all(uint16_t conn_handle, peer_disc_fn *disc_cb, void *disc_cb_arg) @@ -702,6 +732,22 @@ peer_add(uint16_t conn_handle) return 0; } +void +peer_traverse_all(peer_traverse_fn *trav_cb, void *arg) +{ + struct peer *peer; + + if (!trav_cb) { + return; + } + + SLIST_FOREACH(peer, &peers, next) { + if (trav_cb(peer, arg)) { + return; + } + } +} + static void peer_free_mem(void) { diff --git a/examples/bluetooth/nimble/common/nimble_peripheral_utils/esp_peripheral.h b/examples/bluetooth/nimble/common/nimble_peripheral_utils/esp_peripheral.h index 9a68ee7166..be2018a8dc 100644 --- a/examples/bluetooth/nimble/common/nimble_peripheral_utils/esp_peripheral.h +++ b/examples/bluetooth/nimble/common/nimble_peripheral_utils/esp_peripheral.h @@ -21,6 +21,7 @@ int scli_receive_key(int *key); /** Misc. */ void print_bytes(const uint8_t *bytes, int len); void print_addr(const void *addr); +char *addr_str(const void *addr); #ifdef __cplusplus } diff --git a/examples/bluetooth/nimble/common/nimble_peripheral_utils/misc.c b/examples/bluetooth/nimble/common/nimble_peripheral_utils/misc.c index 543bfe7f6c..962eb4da72 100644 --- a/examples/bluetooth/nimble/common/nimble_peripheral_utils/misc.c +++ b/examples/bluetooth/nimble/common/nimble_peripheral_utils/misc.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -28,3 +28,16 @@ print_addr(const void *addr) MODLOG_DFLT(INFO, "%02x:%02x:%02x:%02x:%02x:%02x", u8p[5], u8p[4], u8p[3], u8p[2], u8p[1], u8p[0]); } + +char * +addr_str(const void *addr) +{ + static char buf[6 * 2 + 5 + 1]; + const uint8_t *u8p; + + u8p = addr; + sprintf(buf, "%02x:%02x:%02x:%02x:%02x:%02x", + u8p[5], u8p[4], u8p[3], u8p[2], u8p[1], u8p[0]); + + return buf; +}