diff --git a/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/CMakeLists.txt b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/CMakeLists.txt new file mode 100644 index 00000000000..f30129c3669 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/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.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) +project(ble50_dual_identity_server) diff --git a/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/README.md b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/README.md new file mode 100644 index 00000000000..2ff1c63885f --- /dev/null +++ b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/README.md @@ -0,0 +1,77 @@ +| Supported Targets | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-S3 | +| ----------------- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | + +# BLE 5.0 Dual Local-Identity Security Server + +This example demonstrates **two local identities advertising and being connected/encrypted at the same time** on a BLE 5.0 peripheral, with **isolated bonds per identity**. + +It relies on the Host feature `CONFIG_BT_BLE_PERIPH_PSEUDO_ADDR_BOND`: the Bluedroid Host derives a per-link **pseudo address** from `f(local_identity, peer)`, so that one phone connecting through two different local identities is treated as two independent peers (separate device record, LTK and NVS bond section). + +## What it does + +* Advertises **two connectable extended advertising sets** simultaneously: + * Adv set 0 — **Public** address (identity A, name `ESP_DUAL_PUBLIC_A`) + * Adv set 1 — **fixed Static Random** address (identity B, name `ESP_DUAL_RANDOM_B`) +* Runs a minimal GATT server with one characteristic that requires an **encrypted** link to read/write. +* On each connection it starts pairing (`SC + MITM + BOND`, static passkey `123456`). +* On `AUTH_CMPL` it prints the link's **real peer** and **local identity** via `esp_ble_gap_get_conn_identity()`, and lists bonded devices. + +## Key points for the application + +* The `remote_bda` reported in `ESP_GATTS_CONNECT_EVT` is a **Host pseudo address**, not the phone's real MAC. The **same phone shows up as two different addresses**, one per identity. +* **Always use `conn_id` as the link key.** Do not use `remote_bda` to tell links apart. +* Use `esp_ble_gap_get_conn_identity(pseudo, &id)` to recover the real peer MAC and the local identity. +* Use `esp_ble_gap_remove_bond_for_identity(local, local_type, peer, peer_type)` to delete a single identity's bond without affecting the other. +* The Static Random address used for identity B is **hard-coded and fixed** (`s_identity_b_addr`) so the per-identity bond bucket survives reboots. If you randomize it per boot, reconnection cannot match the stored bond. + +### Which address to pass to GAP/GATTS APIs + +* **Link/bond operations** (`esp_ble_set_encryption`, `esp_ble_gap_disconnect`, `esp_ble_gap_security_rsp`, `esp_ble_confirm_reply`, `esp_ble_passkey_reply`, `esp_ble_gap_update_conn_params`, `esp_ble_gap_read_rssi`, `esp_ble_remove_bond_device`): pass back **exactly the `remote_bda` the event gave you** (the pseudo). Never build the real MAC yourself. +* **Telling links apart / GATTS data** (`esp_ble_gatts_send_indicate`, `send_response`, `close`, …): use **`conn_id`**, not the address. +* **Controller-level operations** (`esp_ble_gap_update_whitelist`, resolving list, directed advertising, `esp_ble_gatts_open`): use the **real peer Identity/RPA**, never the pseudo (the pseudo never goes on air). +* **Need the real MAC**: call `esp_ble_gap_get_real_peer_addr()` / `esp_ble_gap_get_conn_identity()`. + +See the Pseudo design guide **§8.2.2** for the full per-API table. + +## How to test + +1. `idf.py set-target esp32c3` (or s3/c6/h2), then `idf.py flash monitor`. +2. On a phone, scan: you will see **two devices** — `ESP_DUAL_PUBLIC_A` and `ESP_DUAL_RANDOM_B`. +3. Connect and pair to `..._A` (passkey `123456`). Read/write the characteristic — succeeds because the link is encrypted. +4. While still connected to A, connect and pair to `..._B` from the **same phone**. +5. Observe in the log that both links have the **same real peer** but **different pseudo** addresses and **different local identities**, and that `Bonded devices` shows **two** independent entries. +6. Disconnect/reconnect either identity — it re-encrypts from its own stored LTK independently. +7. Remove one bond (e.g. forget `..._B` on the phone, or call `esp_ble_gap_remove_bond_for_identity` for B) — the other identity's bond and encrypted reconnect are unaffected. + +## Example log (abridged) + +``` +I (xxx) DUAL_ID: Pseudo-address dual-identity bond isolation ENABLED +I (xxx) DUAL_ID: Advertising as two identities: Public (A) + Static Random c1:22:33:44:55:66 (B) +[PSEUDO] conn_complete[sync]: keyed handle=0x0 real= -> pseudo= +I (xxx) DUAL_ID: CONNECT conn_id 0, remote(pseudo) c4:..:a +I (xxx) DUAL_ID: AUTH_CMPL identity: real peer , local +[PSEUDO] conn_complete[sync]: keyed handle=0x1 real= -> pseudo= +I (xxx) DUAL_ID: CONNECT conn_id 1, remote(pseudo) fb:..:b +I (xxx) DUAL_ID: AUTH_CMPL identity: real peer , local c1:22:33:44:55:66 +I (xxx) DUAL_ID: Bonded devices: 2 (each entry is a (local,peer) identity = one pseudo) +``` + +The `[PSEUDO] ...` lines come from the Host feature's debug logging, emitted through the standard BTM trace at debug level. They make it easy to follow the dual-identity flow during bring-up; raise the Bluetooth log level (or lower the BTM trace level) to suppress them. + +## Notes + +* Requires a BLE 5.0 capable chip (Extended Advertising). ESP32 (original) does not support it. +* If `CONFIG_BT_BLE_PERIPH_PSEUDO_ADDR_BOND` is disabled, the example still builds and runs but the two identities of the same phone will **share one bond** and overwrite each other — a startup warning is printed. + +## Verified scenarios (ESP32-C3, Android phone using RPA) + +* Connect Public_A and Static-Random_B from the same phone, pair/bond each → `Bonded devices: 2` (two isolated pseudo bonds). +* Delete both bonds on the phone, reconnect A and B → fresh re-pairing succeeds for both, still two isolated bonds. +* Disconnect all, reconnect A → **re-encrypts directly from the stored LTK (no re-pairing)**; the two identities' bonds never interfere. + +The Host-side feature internals and the fixes behind these scenarios (peer-RPA Identity derivation, SC `active_remote_addr` using the real on-air RPA, disabling same-identity NVS de-dup, duplicate device-record cleanup, BTA `device_list` removal by handle) are documented in the Pseudo design guide, section **§24 (implementation notes)**: `docs/zh_CN/api-guides/ble/bluedroid-periph-pseudo-addr-dev-guide-v0.10.md`. + +## Known limitation + +The two bonds share the same peer IRK, so adding that IRK to the controller resolving list a second time is rejected (`Add resolving list error 18`). This does **not** affect encrypted reconnection here (RPA resolution is done in the Host), but it matters if you rely on the controller resolving list, white list, or directed advertising. diff --git a/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/main/CMakeLists.txt b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/main/CMakeLists.txt new file mode 100644 index 00000000000..4ed4bbf0cbb --- /dev/null +++ b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "ble50_dual_identity_server.c" + PRIV_REQUIRES bt nvs_flash + INCLUDE_DIRS ".") diff --git a/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/main/ble50_dual_identity_server.c b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/main/ble50_dual_identity_server.c new file mode 100644 index 00000000000..7a8b6d590a8 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/main/ble50_dual_identity_server.c @@ -0,0 +1,488 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* + * BLE 5.0 dual local-identity GATT server. + * + * The device advertises TWO connectable extended advertising sets at the same + * time, each using a different local identity: + * + * - Adv set 0 : Public address -> identity A + * - Adv set 1 : fixed Static Random addr -> identity B + * + * A single phone can connect to BOTH identities simultaneously. With + * CONFIG_BT_BLE_PERIPH_PSEUDO_ADDR_BOND enabled, the Host derives a distinct + * pseudo address per (local_identity, peer) pair, so the two links get + * independent device records, LTKs and NVS bond sections. Deleting one + * identity's bond never affects the other. + * + * IMPORTANT for the application: + * - remote_bda reported here is the Host PSEUDO (the same phone shows up as + * two different addresses, one per identity). Use conn_id as the link key. + * - Call esp_ble_gap_get_conn_identity(pseudo, &id) to recover the real peer + * MAC and the local identity that the link was established with. + */ + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_bt.h" + +#include "esp_gap_ble_api.h" +#include "esp_gatts_api.h" +#include "esp_bt_defs.h" +#include "esp_bt_main.h" +#include "ble50_dual_identity_server.h" + +#define TAG "DUAL_ID" + +#define ESP_APP_ID 0x55 +#define SVC_INST_ID 0 +#define GATTS_DEMO_CHAR_VAL_LEN_MAX 0x40 +#define MAX_CONN 3 +#define NOTIFY_ENABLE 0x0001 + +/* Two simultaneously-advertising connectable extended advertising sets. */ +#define ADV_HANDLE_PUBLIC 0 /* identity A : Public */ +#define ADV_HANDLE_RANDOM 1 /* identity B : Static Random */ +#define NUM_ADV_SET 2 + +#ifndef MIN +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#endif + +/* A FIXED static random address for identity B. It MUST be persistent across + * reboots (top two bits = 0b11) so the per-identity bond bucket matches on + * reconnect. Do NOT generate it randomly at every boot. */ +static esp_bd_addr_t s_identity_b_addr = {0xC1, 0x22, 0x33, 0x44, 0x55, 0x66}; + +static uint16_t s_handle_table[HRS_IDX_NB]; + +/* Track which advertising sets are currently on-air so that on a disconnect we + * only restart the set(s) that had been consumed by a connection. */ +static bool s_adv_on_air[NUM_ADV_SET]; + +/* ---- advertising data: one human-readable name per identity ---- */ +static uint8_t adv_data_public[] = { + 0x02, ESP_BLE_AD_TYPE_FLAG, 0x06, + 0x12, ESP_BLE_AD_TYPE_NAME_CMPL, + 'E', 'S', 'P', '_', 'D', 'U', 'A', 'L', '_', 'P', 'U', 'B', 'L', 'I', 'C', '_', 'A', +}; + +static uint8_t adv_data_random[] = { + 0x02, ESP_BLE_AD_TYPE_FLAG, 0x06, + 0x12, ESP_BLE_AD_TYPE_NAME_CMPL, + 'E', 'S', 'P', '_', 'D', 'U', 'A', 'L', '_', 'R', 'A', 'N', 'D', 'O', 'M', '_', 'B', +}; + +static esp_ble_gap_ext_adv_t s_ext_adv[NUM_ADV_SET] = { + [0] = {ADV_HANDLE_PUBLIC, 0, 0}, + [1] = {ADV_HANDLE_RANDOM, 0, 0}, +}; + +static esp_ble_gap_ext_adv_params_t s_adv_params_public = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(40), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(40), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = 0, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .tx_power = EXT_ADV_TX_PWR_NO_PREFERENCE, +}; + +static esp_ble_gap_ext_adv_params_t s_adv_params_random = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(40), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(40), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = 1, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_RANDOM, + .tx_power = EXT_ADV_TX_PWR_NO_PREFERENCE, +}; + +/* ---- minimal GATT database (one read/write/notify characteristic) ---- */ +static const uint16_t primary_service_uuid = ESP_GATT_UUID_PRI_SERVICE; +static const uint16_t character_declaration_uuid = ESP_GATT_UUID_CHAR_DECLARE; +static const uint16_t character_client_config_uuid = ESP_GATT_UUID_CHAR_CLIENT_CONFIG; +static const uint16_t GATTS_SERVICE_UUID_TEST = 0x00FF; +static const uint16_t GATTS_CHAR_UUID_TEST_A = 0xFF01; +static const uint8_t char_prop_read_write_notify = ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_NOTIFY; +static const uint8_t ccc[2] = {0x00, 0x00}; +static const uint8_t char_value[4] = {0x11, 0x22, 0x33, 0x44}; +#define CHAR_DECLARATION_SIZE (sizeof(uint8_t)) + +static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] = { + [IDX_SVC] = + {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ, + sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}}, + + [IDX_CHAR_A] = + {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ, + CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_write_notify}}, + + /* Require encryption to read/write so the link must be paired/encrypted. */ + [IDX_CHAR_VAL_A] = + {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_A, + ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED, + GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(char_value), (uint8_t *)char_value}}, + + [IDX_CHAR_CFG_A] = + {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, + ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, + sizeof(uint16_t), sizeof(ccc), (uint8_t *)ccc}}, +}; + +static esp_gatt_if_t s_gatts_if = ESP_GATT_IF_NONE; + +/* Per-link notify subscription (CCC). Indexed by conn_id. */ +static bool s_notify_enabled[MAX_CONN]; + +/* -------------------------------------------------------------------------- */ + +static const char *auth_req_to_str(esp_ble_auth_req_t auth_req) +{ + switch (auth_req) { + case ESP_LE_AUTH_NO_BOND: return "NO_BOND"; + case ESP_LE_AUTH_BOND: return "BOND"; + case ESP_LE_AUTH_REQ_MITM: return "MITM"; + case ESP_LE_AUTH_REQ_BOND_MITM: return "BOND_MITM"; + case ESP_LE_AUTH_REQ_SC_ONLY: return "SC_ONLY"; + case ESP_LE_AUTH_REQ_SC_BOND: return "SC_BOND"; + case ESP_LE_AUTH_REQ_SC_MITM: return "SC_MITM"; + case ESP_LE_AUTH_REQ_SC_MITM_BOND: return "SC_MITM_BOND"; + default: return "INVALID"; + } +} + +static void show_bonded_devices(void) +{ + int dev_num = esp_ble_get_bond_device_num(); + if (dev_num <= 0) { + ESP_LOGI(TAG, "Bonded devices: 0"); + return; + } + esp_ble_bond_dev_t *list = malloc(sizeof(esp_ble_bond_dev_t) * dev_num); + if (!list) { + return; + } + esp_ble_get_bond_device_list(&dev_num, list); + ESP_LOGI(TAG, "Bonded devices: %d (each entry is a (local,peer) identity = one pseudo)", dev_num); + for (int i = 0; i < dev_num; i++) { + ESP_LOGI(TAG, " [%d] pseudo "ESP_BD_ADDR_STR"", i, ESP_BD_ADDR_HEX(list[i].bd_addr)); + } + free(list); +} + +#if CONFIG_BT_BLE_PERIPH_PSEUDO_ADDR_BOND +static void log_conn_identity(const char *tag, esp_bd_addr_t pseudo) +{ + esp_ble_conn_identity_t id; + if (esp_ble_gap_get_conn_identity(pseudo, &id) == ESP_OK) { + ESP_LOGI(TAG, "%s identity: real peer "ESP_BD_ADDR_STR" (type %u), local "ESP_BD_ADDR_STR" (type %u)", + tag, + ESP_BD_ADDR_HEX(id.peer_addr), id.peer_addr_type, + ESP_BD_ADDR_HEX(id.local_addr), id.local_addr_type); + } else { + ESP_LOGW(TAG, "%s identity: pseudo not finalized yet (link may still be on real peer)", tag); + } +} +#endif + +static void request_ext_adv_start(uint8_t inst) +{ + if (inst >= NUM_ADV_SET || s_adv_on_air[inst]) { + return; + } + esp_err_t ret = esp_ble_gap_ext_adv_start(1, &s_ext_adv[inst]); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "ext_adv_start enqueue failed for instance %u: %s", inst, esp_err_to_name(ret)); + } +} + +static void start_idle_adv_sets(void) +{ + for (int i = 0; i < NUM_ADV_SET; i++) { + request_ext_adv_start(i); + } +} + +static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_EXT_ADV_SET_RAND_ADDR_COMPLETE_EVT: + ESP_LOGI(TAG, "Set random addr (identity B) done, status %d", param->ext_adv_set_rand_addr.status); + if (param->ext_adv_set_rand_addr.status == ESP_BT_STATUS_SUCCESS) { + esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE_RANDOM, sizeof(adv_data_random), adv_data_random); + } else { + ESP_LOGE(TAG, "Identity B random addr failed, skip adv data/start"); + } + break; + case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT: + ESP_LOGI(TAG, "Ext adv params set, instance %u, status %d", + param->ext_adv_set_params.instance, param->ext_adv_set_params.status); + if (param->ext_adv_set_params.status != ESP_BT_STATUS_SUCCESS) { + ESP_LOGE(TAG, "Ext adv params failed for instance %u, skip downstream setup", + param->ext_adv_set_params.instance); + break; + } + if (param->ext_adv_set_params.instance == ADV_HANDLE_PUBLIC) { + esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE_PUBLIC, sizeof(adv_data_public), adv_data_public); + esp_ble_gap_ext_adv_set_params(ADV_HANDLE_RANDOM, &s_adv_params_random); + } else if (param->ext_adv_set_params.instance == ADV_HANDLE_RANDOM) { + esp_ble_gap_ext_adv_set_rand_addr(ADV_HANDLE_RANDOM, s_identity_b_addr); + } + break; + case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT: + ESP_LOGI(TAG, "Ext adv data set, status %d, instance %u", + param->ext_adv_data_set.status, param->ext_adv_data_set.instance); + if (param->ext_adv_data_set.status == ESP_BT_STATUS_SUCCESS) { + request_ext_adv_start(param->ext_adv_data_set.instance); + } + break; + case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT: + ESP_LOGI(TAG, "Ext adv start, status %d, instance_num %u", + param->ext_adv_start.status, param->ext_adv_start.instance_num); + for (uint8_t j = 0; j < param->ext_adv_start.instance_num; j++) { + uint8_t inst = param->ext_adv_start.instance[j]; + if (inst >= NUM_ADV_SET) { + continue; + } + if (param->ext_adv_start.status == ESP_BT_STATUS_SUCCESS) { + s_adv_on_air[inst] = true; + } else { + s_adv_on_air[inst] = false; + ESP_LOGE(TAG, "Ext adv start failed for instance %u", inst); + } + } + break; + case ESP_GAP_BLE_ADV_TERMINATED_EVT: + ESP_LOGI(TAG, "Adv terminated: instance %u status 0x%x conn_idx %u", + param->adv_terminate.adv_instance, param->adv_terminate.status, + param->adv_terminate.conn_idx); + if (param->adv_terminate.adv_instance < NUM_ADV_SET) { + /* This set produced a connection -> it stopped advertising. */ + s_adv_on_air[param->adv_terminate.adv_instance] = false; + } + break; + case ESP_GAP_BLE_SEC_REQ_EVT: + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + ESP_LOGI(TAG, "Passkey notify: %06" PRIu32, param->ble_security.key_notif.passkey); + break; + case ESP_GAP_BLE_KEY_EVT: + ESP_LOGI(TAG, "Key exchanged on link "ESP_BD_ADDR_STR" type %d", + ESP_BD_ADDR_HEX(param->ble_security.ble_key.bd_addr), + param->ble_security.ble_key.key_type); + break; + case ESP_GAP_BLE_AUTH_CMPL_EVT: { + esp_bd_addr_t pseudo; + memcpy(pseudo, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); + if (param->ble_security.auth_cmpl.success) { + ESP_LOGI(TAG, "Pairing OK on link "ESP_BD_ADDR_STR", auth %s", + ESP_BD_ADDR_HEX(pseudo), + auth_req_to_str(param->ble_security.auth_cmpl.auth_mode)); +#if CONFIG_BT_BLE_PERIPH_PSEUDO_ADDR_BOND + log_conn_identity("AUTH_CMPL", pseudo); +#endif + } else { + ESP_LOGE(TAG, "Pairing FAILED on link "ESP_BD_ADDR_STR", reason 0x%x", + ESP_BD_ADDR_HEX(pseudo), param->ble_security.auth_cmpl.fail_reason); + } + show_bonded_devices(); + break; + } + case ESP_GAP_BLE_REMOVE_BOND_DEV_COMPLETE_EVT: + ESP_LOGI(TAG, "Bond removed, status %d, pseudo "ESP_BD_ADDR_STR"", + param->remove_bond_dev_cmpl.status, + ESP_BD_ADDR_HEX(param->remove_bond_dev_cmpl.bd_addr)); + break; + case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: + ESP_LOGI(TAG, "Conn params update, link "ESP_BD_ADDR_STR", status %d, " + "conn_int %u, latency %u, timeout %u", + ESP_BD_ADDR_HEX(param->update_conn_params.bda), + param->update_conn_params.status, + param->update_conn_params.conn_int, + param->update_conn_params.latency, + param->update_conn_params.timeout); + break; + case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: + ESP_LOGI(TAG, "Data length update, status %d, rx %u, tx %u", + param->pkt_data_length_cmpl.status, + param->pkt_data_length_cmpl.params.rx_len, + param->pkt_data_length_cmpl.params.tx_len); + break; + default: + break; + } +} + +static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) +{ + switch (event) { + case ESP_GATTS_REG_EVT: + if (param->reg.status == ESP_GATT_OK) { + s_gatts_if = gatts_if; + esp_ble_gatts_create_attr_tab(gatt_db, gatts_if, HRS_IDX_NB, SVC_INST_ID); + } + break; + case ESP_GATTS_CREAT_ATTR_TAB_EVT: + if (param->add_attr_tab.status == ESP_GATT_OK && param->add_attr_tab.num_handle == HRS_IDX_NB) { + memcpy(s_handle_table, param->add_attr_tab.handles, sizeof(s_handle_table)); + esp_ble_gatts_start_service(s_handle_table[IDX_SVC]); + } else { + ESP_LOGE(TAG, "Create attr table failed"); + } + break; + case ESP_GATTS_CONNECT_EVT: + ESP_LOGI(TAG, "CONNECT conn_id %u, remote(pseudo) "ESP_BD_ADDR_STR"", + param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda)); + ESP_LOGI(TAG, " -> use conn_id %u as the link key (remote_bda may be a Host pseudo)", + param->connect.conn_id); + /* Trigger pairing / encryption for this link. */ + esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT_MITM); + break; + case ESP_GATTS_WRITE_EVT: + if (param->write.is_prep) { + break; + } + ESP_LOGI(TAG, "WRITE conn_id %u handle %u len %d", + param->write.conn_id, param->write.handle, param->write.len); + if (param->write.handle == s_handle_table[IDX_CHAR_CFG_A] && param->write.len == 2) { + uint16_t descr_value = (param->write.value[1] << 8) | param->write.value[0]; + if (param->write.conn_id < MAX_CONN) { + if (descr_value == NOTIFY_ENABLE) { + s_notify_enabled[param->write.conn_id] = true; + ESP_LOGI(TAG, "Notify enabled on conn_id %u", param->write.conn_id); + } else if (descr_value == 0x0000) { + s_notify_enabled[param->write.conn_id] = false; + ESP_LOGI(TAG, "Notify disabled on conn_id %u", param->write.conn_id); + } + } + } else if (param->write.handle == s_handle_table[IDX_CHAR_VAL_A]) { + ESP_LOG_BUFFER_HEX(TAG, param->write.value, param->write.len); + if (param->write.conn_id < MAX_CONN && s_notify_enabled[param->write.conn_id] && + param->write.len > 0) { + esp_err_t ret = esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, + s_handle_table[IDX_CHAR_VAL_A], + param->write.len, param->write.value, false); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Notify echo failed on conn_id %u: %s", + param->write.conn_id, esp_err_to_name(ret)); + } else { + ESP_LOGI(TAG, "Notify echo sent on conn_id %u, len %d", + param->write.conn_id, param->write.len); + } + } + } + break; + case ESP_GATTS_READ_EVT: + ESP_LOGI(TAG, "READ conn_id %u (link is encrypted)", param->read.conn_id); + break; + case ESP_GATTS_MTU_EVT: + ESP_LOGI(TAG, "MTU exchange, conn_id %u, mtu %u", + param->mtu.conn_id, param->mtu.mtu); + break; + case ESP_GATTS_CONF_EVT: + ESP_LOGI(TAG, "Notify/indicate confirm, conn_id %u, status %d, handle %u", + param->conf.conn_id, param->conf.status, param->conf.handle); + break; + case ESP_GATTS_DISCONNECT_EVT: + ESP_LOGI(TAG, "DISCONNECT conn_id %u, remote(pseudo) "ESP_BD_ADDR_STR", reason 0x%x", + param->disconnect.conn_id, ESP_BD_ADDR_HEX(param->disconnect.remote_bda), + param->disconnect.reason); + if (param->disconnect.conn_id < MAX_CONN) { + s_notify_enabled[param->disconnect.conn_id] = false; + } + /* Re-advertise only the set(s) that are not currently on-air. */ + start_idle_adv_sets(); + break; + default: + break; + } +} + +static void setup_advertising(void) +{ + /* Kick off the event-driven setup chain: + * PUBLIC params -> PUBLIC data -> RANDOM params -> RANDOM rand_addr -> RANDOM data. + * Each step is gated on the previous COMPLETE event status. */ + esp_err_t ret = esp_ble_gap_ext_adv_set_params(ADV_HANDLE_PUBLIC, &s_adv_params_public); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "ext_adv_set_params(PUBLIC) enqueue failed: %s", esp_err_to_name(ret)); + } +} + +void app_main(void) +{ + 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); + + ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); + + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg)); + ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE)); + + esp_bluedroid_config_t cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_bluedroid_init_with_cfg(&cfg)); + ESP_ERROR_CHECK(esp_bluedroid_enable()); + + ESP_ERROR_CHECK(esp_ble_gatts_register_callback(gatts_event_handler)); + ESP_ERROR_CHECK(esp_ble_gap_register_callback(gap_event_handler)); + ESP_ERROR_CHECK(esp_ble_gatts_app_register(ESP_APP_ID)); + + /* Security: bond + MITM + Secure Connections, static passkey. */ + esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_MITM_BOND; + esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; + uint8_t key_size = 16; + uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint32_t passkey = 123456; + uint8_t auth_option = ESP_BLE_ONLY_ACCEPT_SPECIFIED_AUTH_DISABLE; + uint8_t oob_support = ESP_BLE_OOB_DISABLE; + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &passkey, sizeof(uint32_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_ONLY_ACCEPT_SPECIFIED_SEC_AUTH, &auth_option, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_OOB_SUPPORT, &oob_support, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(uint8_t)); + +#if !CONFIG_BT_BLE_PERIPH_PSEUDO_ADDR_BOND + ESP_LOGW(TAG, "CONFIG_BT_BLE_PERIPH_PSEUDO_ADDR_BOND is DISABLED:"); + ESP_LOGW(TAG, " two identities of the same phone will share one bond and overwrite each other."); + ESP_LOGW(TAG, " Enable it to get isolated bonds per local identity."); +#else + ESP_LOGI(TAG, "Pseudo-address dual-identity bond isolation ENABLED"); +#endif + + setup_advertising(); + ESP_LOGI(TAG, "Advertising as two identities: Public (A) + Static Random "ESP_BD_ADDR_STR" (B)", + ESP_BD_ADDR_HEX(s_identity_b_addr)); +} diff --git a/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/main/ble50_dual_identity_server.h b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/main/ble50_dual_identity_server.h new file mode 100644 index 00000000000..7351aae3600 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/main/ble50_dual_identity_server.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#ifndef BLE50_DUAL_IDENTITY_SERVER_H +#define BLE50_DUAL_IDENTITY_SERVER_H + +#include +#include +#include + +/* Attributes State Machine */ +enum { + IDX_SVC, + IDX_CHAR_A, + IDX_CHAR_VAL_A, + IDX_CHAR_CFG_A, + + HRS_IDX_NB, +}; + +#endif // BLE50_DUAL_IDENTITY_SERVER_H diff --git a/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/sdkconfig.defaults b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/sdkconfig.defaults new file mode 100644 index 00000000000..85a3ad3d4ec --- /dev/null +++ b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/sdkconfig.defaults @@ -0,0 +1,11 @@ +CONFIG_BT_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_BLE_42_FEATURES_SUPPORTED=n +CONFIG_BT_BLE_SMP_ENABLE=y +CONFIG_BT_BLE_PERIPH_PSEUDO_ADDR_BOND=y +CONFIG_BT_ACL_CONNECTIONS=2 +CONFIG_BT_GATTS_ENABLE=y +# CONFIG_BT_GATTC_ENABLE is not set +# CONFIG_BT_BLE_50_DTM_TEST_EN is not set +# CONFIG_BT_BLE_50_PERIODIC_ADV_EN is not set +# CONFIG_BT_BLE_50_EXTEND_SCAN_EN is not set diff --git a/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/sdkconfig.defaults.esp32c3 b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/sdkconfig.defaults.esp32c3 new file mode 100644 index 00000000000..b4f291d7790 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/sdkconfig.defaults.esp32c3 @@ -0,0 +1,2 @@ +CONFIG_IDF_TARGET="esp32c3" +CONFIG_BT_ENABLED=y diff --git a/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/sdkconfig.defaults.esp32s3 b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/sdkconfig.defaults.esp32s3 new file mode 100644 index 00000000000..f38423a524f --- /dev/null +++ b/examples/bluetooth/bluedroid/ble_50/ble50_dual_identity_server/sdkconfig.defaults.esp32s3 @@ -0,0 +1,2 @@ +CONFIG_IDF_TARGET="esp32s3" +CONFIG_BT_ENABLED=y