mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
feat(bt/ble_uart): Support ble uart more interfaces
- Added tagged event API (ble_uart_evt_t / on_event)
- Added bond management APIs
- Supported custom adv_data / scan_rsp_data
- Validate device_name length synchronously
- Added ble_uart_close_async() and EVT_CLOSED
- Added granular security config (security struct)
- Supported Passkey Entry and Numeric Comparison
(cherry picked from commit f1d9c994d2)
Co-authored-by: zhiweijian <zhiweijian@espressif.com>
This commit is contained in:
@@ -15,6 +15,14 @@ examples/bluetooth:
|
||||
disable:
|
||||
- if: SOC_BT_SUPPORTED != 1
|
||||
|
||||
examples/bluetooth/ble_uart_service:
|
||||
<<: *bt_default_depends
|
||||
disable:
|
||||
- if: SOC_BLE_SUPPORTED != 1
|
||||
depends_filepatterns:
|
||||
- examples/bluetooth/common/ble_uart/**/*
|
||||
- examples/bluetooth/ble_uart_service/**/*
|
||||
|
||||
examples/bluetooth/bluedroid/ble:
|
||||
<<: *bt_default_depends
|
||||
disable:
|
||||
|
||||
@@ -24,12 +24,19 @@ ble_uart_install(&cfg); // NimBLE host + BLE UART GATT service
|
||||
ble_uart_open(); // start advertising + auto-encrypt
|
||||
```
|
||||
|
||||
…and two matching tear-down calls if your app ever needs to power
|
||||
BLE off at runtime:
|
||||
If your app powers BLE off at runtime, use **one** of the release paths
|
||||
in [PORTING.md §5.3](../common/ble_uart/PORTING.md#53-lifecycle--bring-up-and-release)
|
||||
(this example uses Path A from `app_main`):
|
||||
|
||||
| Path | When | Calls |
|
||||
| --- | --- | --- |
|
||||
| **A — sync** (default) | Shutdown from a normal task (button, Wi-Fi, `app_main`) | `ble_uart_close()` → `ble_uart_uninstall()` |
|
||||
| **B — async** | Shutdown triggered inside `on_event` / `on_rx` | `close_async()` in callback → `CLOSED` sets flag → **`uninstall()` on a separate app task** (not inside `CLOSED`) |
|
||||
|
||||
```c
|
||||
ble_uart_close(); // stop advertising / disconnect / halt host
|
||||
ble_uart_uninstall(); // free the NimBLE port + reset state
|
||||
/* Path A — this example style */
|
||||
ble_uart_close();
|
||||
ble_uart_uninstall();
|
||||
```
|
||||
|
||||
When a central connects, the firmware automatically initiates LE Secure
|
||||
@@ -47,49 +54,135 @@ back with `ble_uart_tx()`.
|
||||
| TX (out) | `6e400003-b5a3-f393-e0a9-e50e24dcca9e` | Notify (auto-CCCD) | encrypted, authenticated |
|
||||
|
||||
The `_ENC | _AUTHEN` flags are turned on only when `cfg.encrypted = true`
|
||||
(the default in this example).
|
||||
(the default in this example). The two flags can be controlled
|
||||
independently via `cfg.security.mitm` (drops `_AUTHEN`) and the
|
||||
combined `cfg.security.{sc,bonding,mitm}` set (all OFF drops `_ENC`
|
||||
too) — see PORTING.md §5.6.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Lines | Role |
|
||||
| --- | ---: | --- |
|
||||
| `main/main.c` | ~70 | NVS init, MAC-derived device name, install + open, RX echo handler. Identical for both backends. |
|
||||
| `main/main.c` | ~200 | NVS init, install + open with `<prefix>-XXXX` device name (Kconfig prefix + BT MAC suffix), RX echo handler, lifecycle/link-state event sink, bonded-peer dump on boot. Identical for both backends. |
|
||||
| `main/Kconfig.projbuild` | ~50 | Example-local `EXAMPLE_CUSTOM_ADV_DATA` switch — toggles the `ble_uart_config_t::adv_data` demo path in `main.c`. |
|
||||
| `CMakeLists.txt` (root) | ~15 | `list(APPEND EXTRA_COMPONENT_DIRS .../common/ble_uart)` before `project()` so `main` can `REQUIRES ble_uart`. |
|
||||
| `../common/ble_uart/ble_uart.h` | ~155 | Stack-agnostic public API: 3-field config + 4 lifecycle functions + TX/status + UUID + `BLE_UART_E*` return codes. No NimBLE / Bluedroid types leak through. |
|
||||
| `../common/ble_uart/ble_uart_nimble.c` | ~650 | NimBLE backend: host bring-up, BLE UART GATT service via `ble_gatts_add_svcs`, advertising, pairing, install/open/close/uninstall. Active when `CONFIG_BT_NIMBLE_ENABLED=y`. |
|
||||
| `../common/ble_uart/ble_uart_bluedroid.c` | ~1020 | Bluedroid backend: controller + host enable, BLE UART GATT service via `esp_ble_gatts_create_attr_tab` (service-table API), advertising, pairing, full PREP/EXEC long-write reassembly, install/open/close/uninstall. Active when `CONFIG_BT_BLUEDROID_ENABLED=y`. |
|
||||
| `../common/ble_uart/Kconfig` | ~30 | Device-name prefix + RX scratch size (`menuconfig → Component configuration → ESP-BLE-UART library`). |
|
||||
| `../common/ble_uart/PORTING.md` | ~724 | Porting and API guide (integration, CMake, sdkconfig, thread safety). |
|
||||
| `../common/ble_uart/ble_uart.h` | ~640 | Stack-agnostic public API: configuration struct (preset + per-feature security overrides + custom adv payload + RX/event callbacks) + lifecycle (install/open/close/close_async/uninstall) + TX + pairing replies + bond-management + status + UUID + `BLE_UART_E*` return codes. No NimBLE / Bluedroid types leak through. |
|
||||
| `../common/ble_uart/ble_uart_nimble.c` | ~1290 | NimBLE backend: host bring-up, BLE UART GATT service via `ble_gatts_add_svcs`, advertising (default + raw), pairing (incl. Passkey Entry / Numeric Comparison), bond store, async close, install/open/close/uninstall. Active when `CONFIG_BT_NIMBLE_ENABLED=y`. |
|
||||
| `../common/ble_uart/ble_uart_bluedroid.c` | ~1660 | Bluedroid backend: controller + host enable, BLE UART GATT service via `esp_ble_gatts_create_attr_tab` (service-table API), advertising (default + raw), pairing (incl. Passkey Entry / Numeric Comparison), bond store, async close, full PREP/EXEC long-write reassembly, install/open/close/uninstall. Active when `CONFIG_BT_BLUEDROID_ENABLED=y`. |
|
||||
| `../common/ble_uart/Kconfig` | ~30 | Device name prefix + RX scratch size (`menuconfig → Component configuration → ESP-BLE-UART library`). |
|
||||
| `../common/ble_uart/PORTING.md` | ~1300 | Porting and API guide (integration, CMake, sdkconfig, security model, custom advertising, bond management, thread safety). |
|
||||
| `sdkconfig.defaults` | — | Default: NimBLE backend, MTU 512, SC + bonding + persistent NVS. |
|
||||
| `sdkconfig.bluedroid` | — | Overlay: switch to Bluedroid backend (used via `-D SDKCONFIG_DEFAULTS=...`, see "Choosing the host stack" below). |
|
||||
|
||||
## Public API
|
||||
|
||||
```c
|
||||
typedef void (*ble_uart_rx_cb_t)(const uint8_t *data, size_t len);
|
||||
typedef void (*ble_uart_rx_cb_t) (const uint8_t *data, size_t len);
|
||||
typedef void (*ble_uart_evt_cb_t)(const ble_uart_evt_t *evt);
|
||||
|
||||
typedef struct {
|
||||
bool encrypted; /* SC + Bonding + MITM in one knob */
|
||||
const char *device_name;
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;
|
||||
ble_uart_sec_t sc; /* AUTO / OFF / ON */
|
||||
ble_uart_sec_t bonding;
|
||||
ble_uart_sec_t mitm;
|
||||
ble_uart_io_cap_t io_cap; /* AUTO / NO_INPUT_OUTPUT / DISPLAY_ONLY /
|
||||
KEYBOARD_ONLY / DISPLAY_YES_NO /
|
||||
KEYBOARD_DISPLAY */
|
||||
} ble_uart_security_t;
|
||||
|
||||
typedef struct {
|
||||
bool encrypted; /* preset: SC + Bonding + MITM + DisplayOnly */
|
||||
ble_uart_security_t security; /* per-feature overrides; see PORTING.md §5.6 */
|
||||
|
||||
const char *device_name; /* ≤ BLE_UART_DEVICE_NAME_MAX (26) */
|
||||
/* Optional: raw advertising / scan-response bytes (NULL → defaults).
|
||||
* Limits: adv_data_len ≤ BLE_UART_ADV_DATA_MAX (28),
|
||||
* scan_rsp_data_len ≤ BLE_UART_SCAN_RSP_DATA_MAX (31).
|
||||
* The 3-byte Flags AD element is prepended automatically — don't
|
||||
* include it in adv_data. See PORTING.md §5.9 for examples. */
|
||||
const uint8_t *adv_data;
|
||||
size_t adv_data_len;
|
||||
const uint8_t *scan_rsp_data;
|
||||
size_t scan_rsp_data_len;
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;
|
||||
ble_uart_evt_cb_t on_event; /* lifecycle / link-state events; NULL drops */
|
||||
} ble_uart_config_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t bytes[6]; /* big-endian: bytes[0] is the MSB (AA:BB:CC:DD:EE:FF) */
|
||||
uint8_t type; /* BLE_UART_ADDR_TYPE_PUBLIC | _RANDOM */
|
||||
} ble_uart_addr_t;
|
||||
|
||||
/* Lifecycle */
|
||||
int ble_uart_install(const ble_uart_config_t *cfg); /* NimBLE host + GATT */
|
||||
int ble_uart_install(const ble_uart_config_t *cfg); /* host + GATT */
|
||||
int ble_uart_open(void); /* host task + advertising */
|
||||
int ble_uart_close(void); /* stop adv / disconnect / halt host */
|
||||
int ble_uart_uninstall(void); /* free NimBLE port + reset state */
|
||||
int ble_uart_close_async(void); /* same, fire-and-forget; safe from inside on_event/on_rx */
|
||||
int ble_uart_uninstall(void); /* free port + reset state */
|
||||
|
||||
/* Data path */
|
||||
int ble_uart_tx(const uint8_t *data, size_t len);
|
||||
|
||||
/* Pairing replies (call from on_event for input-capable IO caps) */
|
||||
int ble_uart_passkey_reply(uint32_t passkey); /* answer PASSKEY_REQUEST */
|
||||
int ble_uart_compare_reply(bool match); /* answer NUMERIC_COMPARE */
|
||||
|
||||
/* Status (best-effort snapshot) */
|
||||
bool ble_uart_is_connected(void);
|
||||
bool ble_uart_is_subscribed(void);
|
||||
|
||||
/* Bond management (works after install()) */
|
||||
int ble_uart_get_bond_count(size_t *out_count);
|
||||
int ble_uart_get_bonded_peers(ble_uart_addr_t *out, size_t cap, size_t *out_count);
|
||||
int ble_uart_remove_peer(const ble_uart_addr_t *peer);
|
||||
int ble_uart_clear_bonds(void);
|
||||
|
||||
extern const ble_uart_uuid128_t ble_uart_service_uuid;
|
||||
```
|
||||
|
||||
### Event callback
|
||||
|
||||
`on_event` is invoked on the BLE host task (same context as `ble_uart_on_rx`)
|
||||
with a tagged `ble_uart_evt_t`. Use `LINK_SECURE` — not `is_connected()` —
|
||||
to gate any application logic that requires the channel to be encrypted /
|
||||
authenticated:
|
||||
|
||||
| `evt->id` | Payload | Fires when |
|
||||
| ------------------------------- | ------------------------------------------------------- | ---------- |
|
||||
| `BLE_UART_EVT_CONNECTED` | `connected.peer` | Physical link up |
|
||||
| `BLE_UART_EVT_DISCONNECTED` | `disconnected.reason` (int, stack-specific) | Physical link down — Bluedroid: `esp_gatt_conn_reason_t`; NimBLE: BLE host return code (`BLE_HS_HCI_ERR()` for HCI) |
|
||||
| `BLE_UART_EVT_SUBSCRIBED` | `subscribed.subscribed` | Central writes CCCD on TX (edge-triggered) |
|
||||
| `BLE_UART_EVT_LINK_SECURE` | `link_secure.{encrypted,authenticated,bonded,key_size}` | Pairing or bonded reconnect succeeds |
|
||||
| `BLE_UART_EVT_PASSKEY_DISPLAY` | `passkey.passkey` (0..999999) | SM asks the device to show a passkey |
|
||||
| `BLE_UART_EVT_PASSKEY_REQUEST` | — | SM asks the user to enter a passkey shown by the central — answer with `ble_uart_passkey_reply()` |
|
||||
| `BLE_UART_EVT_NUMERIC_COMPARE` | `numeric_compare.passkey` (0..999999) | SM asks the user to confirm both sides display the same value — answer with `ble_uart_compare_reply()` |
|
||||
| `BLE_UART_EVT_PAIRING_FAILED` | `pairing_failed.reason` (stack-specific) | Pairing rejected or timed out |
|
||||
| `BLE_UART_EVT_CLOSED` | `closed.status` (`BLE_UART_*`) | `ble_uart_close_async()` worker has finished; `BLE_UART_OK` means tear-down succeeded |
|
||||
|
||||
The default passkey UART banner still prints; the callback is additive so
|
||||
log-scraping tests stay compatible. Don't block in the callback.
|
||||
|
||||
**Callback rules:**
|
||||
|
||||
- Do **not** call `ble_uart_close()` or `ble_uart_uninstall()` from
|
||||
`on_event` / `on_rx` (host task — deadlocks).
|
||||
- To start teardown from a callback, call `ble_uart_close_async()` only.
|
||||
- Call `ble_uart_uninstall()` from a **normal app task** after
|
||||
`BLE_UART_EVT_CLOSED` with `closed.status == BLE_UART_OK` (see
|
||||
[PORTING.md §5.3.2](../common/ble_uart/PORTING.md#532-path-b--release-after-a-ble-event-close_async)).
|
||||
|
||||
Path B sketch (full code in PORTING.md):
|
||||
|
||||
```c
|
||||
case BLE_UART_EVT_PAIRING_FAILED:
|
||||
ble_uart_close_async();
|
||||
break;
|
||||
case BLE_UART_EVT_CLOSED:
|
||||
if (e->closed.status == BLE_UART_OK) {
|
||||
s_ble_closed_ok = true; /* app task calls uninstall */
|
||||
}
|
||||
break;
|
||||
```
|
||||
|
||||
## Choosing the host stack
|
||||
|
||||
The same `ble_uart.h` API is implemented twice — once on top of NimBLE
|
||||
@@ -134,8 +227,10 @@ When neither is enabled the build fails up-front with a clear error.
|
||||
idf.py set-target esp32c3 # or esp32, esp32s3, esp32c6, esp32h2 ...
|
||||
idf.py menuconfig # optional
|
||||
# Component configuration -> ESP-BLE-UART library
|
||||
# - BLE device name prefix (default: BleUart)
|
||||
# - BLE device name prefix (default: BleUart; example appends -XXXX from BT MAC)
|
||||
# - RX scratch buffer size (default: 1024 bytes)
|
||||
# BLE UART service example
|
||||
# - Use custom advertising data (default: off)
|
||||
```
|
||||
|
||||
Those `BLE_UART_*` options are defined in **`../common/ble_uart/Kconfig`**
|
||||
@@ -143,6 +238,27 @@ Those `BLE_UART_*` options are defined in **`../common/ble_uart/Kconfig`**
|
||||
build (this example pulls it in via `EXTRA_COMPONENT_DIRS` in the root
|
||||
`CMakeLists.txt`).
|
||||
|
||||
`EXAMPLE_CUSTOM_ADV_DATA` is example-local (`main/Kconfig.projbuild`)
|
||||
and demonstrates `ble_uart_config_t::adv_data` — the field that lets
|
||||
the application fully control the over-the-air advertising payload
|
||||
instead of using the library default.
|
||||
|
||||
When the option is on, `app_main` hands a static byte array
|
||||
(`example_adv_payload[]`, top of `main.c`) to `ble_uart_install()`.
|
||||
The array is just a sequence of `[length][AD type][value]` triplets;
|
||||
edit it directly to advertise whatever you want — a different Local
|
||||
Name, Manufacturer Specific Data, custom Service Data, additional
|
||||
Service UUIDs, etc. The only hard rule is total length ≤
|
||||
`BLE_UART_ADV_DATA_MAX` (28); the 3-byte Flags AD is added by the
|
||||
library and does not count against that budget.
|
||||
|
||||
The GAP-service Device Name (set via `device_name` in the same
|
||||
config struct) is independent and is what connected centrals read
|
||||
post-pair, regardless of `adv_data`.
|
||||
|
||||
With the option off the library default is used (Complete Local Name
|
||||
in the primary packet, 128-bit Service UUID in the scan response).
|
||||
|
||||
The two security knobs are set in `sdkconfig.defaults`:
|
||||
|
||||
```ini
|
||||
@@ -154,6 +270,17 @@ Disable `cfg.encrypted` in `main.c` (set it to `false`) for plaintext
|
||||
operation in the lab — the GATT characteristics drop their `_ENC`
|
||||
flags accordingly. Production firmware should keep encryption on.
|
||||
|
||||
For finer control without going all-or-nothing — e.g. a displayless
|
||||
gateway that wants encryption + bonding but no passkey UI, or a
|
||||
device with a keypad that wants Passkey Entry / Numeric Comparison —
|
||||
keep `cfg.encrypted = true` and override individual bits via
|
||||
`cfg.security.{sc,bonding,mitm,io_cap}`. The input-capable IO caps
|
||||
(`KEYBOARD_ONLY`, `DISPLAY_YES_NO`, `KEYBOARD_DISPLAY`) require an
|
||||
`on_event` handler that wires `BLE_UART_EVT_PASSKEY_REQUEST` /
|
||||
`NUMERIC_COMPARE` to `ble_uart_passkey_reply()` /
|
||||
`ble_uart_compare_reply()`. See PORTING.md §5.6 for the full matrix
|
||||
and worked examples.
|
||||
|
||||
### Build & flash
|
||||
|
||||
```bash
|
||||
@@ -170,7 +297,7 @@ I (xxx) ble_uart: registered chr 6e400002-... def=15 val=16
|
||||
I (xxx) ble_uart: registered chr 6e400003-... def=17 val=18
|
||||
I (xxx) ble_uart: addr=80:7d:3a:11:22:33
|
||||
I (xxx) ble_uart: BLE host task started
|
||||
I (xxx) ble_uart: advertising as 'BleUart-XXXX'
|
||||
I (xxx) ble_uart: advertising as 'BleUart-2233'
|
||||
```
|
||||
|
||||
Expected boot log (Bluedroid backend):
|
||||
@@ -186,8 +313,10 @@ I (xxx) ble_uart: advertising started
|
||||
1. On a phone, install **a BLE GATT client app** that supports scanning,
|
||||
pairing, characteristic write, and notify/CCCD (many mobile “BLE tools”
|
||||
or serial-over-BLE utilities qualify).
|
||||
2. Scan, tap **Connect** on `BleUart-XXXX`. The phone prompts for a
|
||||
6-digit code.
|
||||
2. Scan, tap **Connect** on `BleUart-XXXX` (prefix from
|
||||
`CONFIG_BLE_UART_DEVICE_NAME_PREFIX`, `XXXX` = last two BT MAC
|
||||
bytes). The phone prompts for a 6-digit
|
||||
code.
|
||||
3. The device prints a fresh code in a banner on UART:
|
||||
|
||||
```
|
||||
@@ -204,8 +333,13 @@ I (xxx) ble_uart: advertising started
|
||||
6. Disconnect and reconnect: no passkey prompt — the bond resumes
|
||||
automatically.
|
||||
|
||||
To wipe the bond and force a fresh passkey, run `idf.py erase-flash`
|
||||
and re-flash.
|
||||
To wipe the bond and force a fresh passkey there are three options:
|
||||
|
||||
- Call `ble_uart_clear_bonds()` from your app (preserves the rest of NVS)
|
||||
- Call `ble_uart_remove_peer(&addr)` to drop one peer (use the address
|
||||
reported in `BLE_UART_EVT_CONNECTED`, or any address you happen to
|
||||
have stored — Bluedroid matches by address only, NimBLE by identity)
|
||||
- Run `idf.py erase-flash` and re-flash (also wipes WiFi creds, NVS, etc.)
|
||||
|
||||
## Adapting to your application
|
||||
|
||||
|
||||
47
examples/bluetooth/ble_uart_service/main/Kconfig.projbuild
Normal file
47
examples/bluetooth/ble_uart_service/main/Kconfig.projbuild
Normal file
@@ -0,0 +1,47 @@
|
||||
menu "BLE UART service example"
|
||||
|
||||
config EXAMPLE_CUSTOM_ADV_DATA
|
||||
bool "Use custom advertising data"
|
||||
default n
|
||||
help
|
||||
Demonstrates `ble_uart_config_t::adv_data` — the field that
|
||||
lets the application fully control the advertising payload
|
||||
instead of relying on the library default.
|
||||
|
||||
When enabled, the example passes a static byte array
|
||||
(`example_adv_payload[]` defined at the top of `main.c`) to
|
||||
`ble_uart_install()`. Edit that array to broadcast anything
|
||||
you want: a different Local Name, Manufacturer Specific
|
||||
Data, custom Service Data, multiple Service UUIDs, etc.
|
||||
|
||||
Format
|
||||
The array is a sequence of standard Bluetooth Core "AD
|
||||
structure" triplets:
|
||||
|
||||
[length(1)] [AD type(1)] [value(length-1)]
|
||||
|
||||
See the Bluetooth Assigned Numbers (Generic Access
|
||||
Profile) document for the full type list.
|
||||
|
||||
Length budget
|
||||
Total bytes in the array must be
|
||||
≤ BLE_UART_ADV_DATA_MAX (28). The 3-byte mandatory
|
||||
Flags AD element is prepended automatically by
|
||||
ble_uart and does NOT count against this budget. An
|
||||
oversized buffer makes `ble_uart_install()` fail with
|
||||
BLE_UART_EINVAL.
|
||||
|
||||
Scope
|
||||
Only affects the over-the-air advertising payload.
|
||||
The GAP-service Device Name (UUID 0x2A00, set via
|
||||
`device_name` in the same struct) is independent and
|
||||
stays whatever the application configured — connected
|
||||
centrals read that name regardless of what is in
|
||||
`adv_data`.
|
||||
|
||||
Default value
|
||||
Off. The library default is used (Complete Local Name
|
||||
in the primary packet, 128-bit Service UUID in the
|
||||
scan response).
|
||||
|
||||
endmenu
|
||||
@@ -8,6 +8,7 @@
|
||||
* writes to the RX characteristic is echoed back over TX.
|
||||
*/
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
@@ -17,6 +18,41 @@
|
||||
|
||||
#include "ble_uart.h"
|
||||
|
||||
#if CONFIG_EXAMPLE_CUSTOM_ADV_DATA
|
||||
/* Sample advertising payload demonstrating ble_uart_config_t::adv_data.
|
||||
* Replace these bytes with whatever your product needs (a different
|
||||
* Local Name, Manufacturer Specific Data, custom Service Data,
|
||||
* additional Service UUIDs, ...) — ble_uart broadcasts them verbatim.
|
||||
*
|
||||
* Format: a sequence of standard BT Core "AD structure" triplets,
|
||||
* [length(1)] [AD type(1)] [value(length-1)].
|
||||
*
|
||||
* Length budget: total ≤ BLE_UART_ADV_DATA_MAX (28). The mandatory
|
||||
* 3-byte Flags AD is prepended by ble_uart and does NOT count against
|
||||
* this budget; oversize fails ble_uart_install() with EINVAL.
|
||||
*
|
||||
* The current contents (purely illustrative — edit freely):
|
||||
*
|
||||
* Layout bytes
|
||||
* -------------------------------------- -----
|
||||
* Complete Local Name AD "BleUart" 1 + 1 + 7 = 9
|
||||
* Complete 128-bit UUID AD 1 + 1 + 16 = 18
|
||||
* -------------------------------------- -----
|
||||
* total 27 (≤ 28)
|
||||
*/
|
||||
static const uint8_t example_adv_payload[] = {
|
||||
/* AD type 0x09: Complete Local Name */
|
||||
0x08, 0x09, 'B', 'l', 'e', 'U', 'a', 'r', 't',
|
||||
|
||||
/* AD type 0x07: Complete List of 128-bit Service UUIDs.
|
||||
* UUID bytes are in over-the-air (little-endian) order, matching
|
||||
* ble_uart_service_uuid.bytes[]. */
|
||||
0x11, 0x07,
|
||||
0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0,
|
||||
0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e,
|
||||
};
|
||||
#endif
|
||||
|
||||
static const char *TAG = "app";
|
||||
|
||||
static void ble_uart_on_rx(const uint8_t *data, size_t len)
|
||||
@@ -29,6 +65,83 @@ static void ble_uart_on_rx(const uint8_t *data, size_t len)
|
||||
ble_uart_tx(data, len); /* echo back */
|
||||
}
|
||||
|
||||
/* Lifecycle / link-state event sink. Runs on the BLE host task —
|
||||
* keep it short, never call ble_uart_close()/uninstall() from here.
|
||||
*
|
||||
* For production code: gate any sensitive TX on
|
||||
* BLE_UART_EVT_LINK_SECURE (encrypted+authenticated) instead of just
|
||||
* "connected"; ble_uart_is_connected() returns true while the link is
|
||||
* still plaintext during the pairing window. */
|
||||
static void ble_uart_on_event(const ble_uart_evt_t *e)
|
||||
{
|
||||
switch (e->id) {
|
||||
case BLE_UART_EVT_CONNECTED: {
|
||||
const uint8_t *b = e->connected.peer.bytes;
|
||||
ESP_LOGI(TAG,
|
||||
"evt: connected peer=%02x:%02x:%02x:%02x:%02x:%02x type=%u",
|
||||
b[0], b[1], b[2], b[3], b[4], b[5], e->connected.peer.type);
|
||||
break;
|
||||
}
|
||||
case BLE_UART_EVT_DISCONNECTED:
|
||||
ESP_LOGI(TAG, "evt: disconnected reason=0x%x",
|
||||
e->disconnected.reason);
|
||||
break;
|
||||
case BLE_UART_EVT_SUBSCRIBED:
|
||||
ESP_LOGI(TAG, "evt: %ssubscribed",
|
||||
e->subscribed.subscribed ? "" : "un");
|
||||
break;
|
||||
case BLE_UART_EVT_LINK_SECURE:
|
||||
ESP_LOGI(TAG, "evt: link_secure enc=%d auth=%d bond=%d ks=%u",
|
||||
e->link_secure.encrypted, e->link_secure.authenticated,
|
||||
e->link_secure.bonded, e->link_secure.key_size);
|
||||
break;
|
||||
case BLE_UART_EVT_PASSKEY_DISPLAY:
|
||||
ESP_LOGI(TAG, "evt: passkey=%06" PRIu32, e->passkey.passkey);
|
||||
break;
|
||||
case BLE_UART_EVT_PASSKEY_REQUEST:
|
||||
/* Fires only when cfg.security.io_cap is KEYBOARD_ONLY or
|
||||
* KEYBOARD_DISPLAY (this example leaves io_cap at AUTO →
|
||||
* DisplayOnly, so it should not fire). For a real keypad
|
||||
* product, prompt the user for the 6 digits the central
|
||||
* displayed and feed them in:
|
||||
*
|
||||
* ble_uart_passkey_reply(digits);
|
||||
*
|
||||
* See PORTING.md §5.6.1 for the full pattern. */
|
||||
ESP_LOGW(TAG, "evt: passkey entry requested — no UI wired in this "
|
||||
"example (see PORTING.md §5.6.1)");
|
||||
break;
|
||||
case BLE_UART_EVT_NUMERIC_COMPARE:
|
||||
/* Fires only when cfg.security.io_cap is DISPLAY_YES_NO or
|
||||
* KEYBOARD_DISPLAY (likewise dormant in this example). For a
|
||||
* product with a yes/no control, surface the digits to the
|
||||
* user and resolve the comparison:
|
||||
*
|
||||
* ble_uart_compare_reply(user_says_match);
|
||||
*
|
||||
* See PORTING.md §5.6.1. */
|
||||
ESP_LOGW(TAG, "evt: numeric compare %06" PRIu32
|
||||
" — no yes/no UI wired (see PORTING.md §5.6.1)",
|
||||
e->numeric_compare.passkey);
|
||||
break;
|
||||
case BLE_UART_EVT_PAIRING_FAILED:
|
||||
ESP_LOGW(TAG, "evt: pairing failed reason=0x%x",
|
||||
e->pairing_failed.reason);
|
||||
break;
|
||||
case BLE_UART_EVT_CLOSED:
|
||||
/* Only after ble_uart_close_async(). This example does not use
|
||||
* close_async; do not ble_uart_uninstall() here — defer to an
|
||||
* app task (PORTING.md §5.3.2). Kept for -Wswitch. */
|
||||
if (e->closed.status == BLE_UART_OK) {
|
||||
ESP_LOGI(TAG, "evt: closed (async-close succeeded)");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "evt: closed async-close failed status=%d",
|
||||
e->closed.status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/* NVS is required by the BT controller (PHY calibration) and the
|
||||
@@ -49,15 +162,51 @@ void app_main(void)
|
||||
ESP_LOGW(TAG, "esp_read_mac(BT) failed (%s); device name suffix will be 0000",
|
||||
esp_err_to_name(mac_err));
|
||||
}
|
||||
char name[24];
|
||||
char name[BLE_UART_DEVICE_NAME_MAX + 1];
|
||||
snprintf(name, sizeof(name), "%s-%02X%02X",
|
||||
CONFIG_BLE_UART_DEVICE_NAME_PREFIX, mac[4], mac[5]);
|
||||
|
||||
ESP_ERROR_CHECK(ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.device_name = name,
|
||||
#if CONFIG_EXAMPLE_CUSTOM_ADV_DATA
|
||||
/* Hand the application-defined bytes to ble_uart. Whatever
|
||||
* the array contains is broadcast verbatim; what `device_name`
|
||||
* (above) holds is exposed via the GAP service for connected
|
||||
* centrals to read — independent paths. */
|
||||
.adv_data = example_adv_payload,
|
||||
.adv_data_len = sizeof(example_adv_payload),
|
||||
/* scan_rsp_data is left at its default (NULL) → ble_uart still
|
||||
* sends its built-in scan response. Override it the same way
|
||||
* if you want to control those bytes too. */
|
||||
#endif
|
||||
.ble_uart_on_rx = ble_uart_on_rx,
|
||||
.on_event = ble_uart_on_event,
|
||||
}));
|
||||
|
||||
/* Demonstrate the bond-management API: list every bonded peer
|
||||
* already on flash. Replace the log with `ble_uart_clear_bonds()`
|
||||
* to wipe them at boot (e.g. when a "factory reset" GPIO is held);
|
||||
* use `ble_uart_remove_peer(&list[i])` to target one specifically. */
|
||||
size_t total = 0;
|
||||
ble_uart_addr_t list[8];
|
||||
int rc = ble_uart_get_bonded_peers(list, sizeof(list) / sizeof(list[0]),
|
||||
&total);
|
||||
if (rc == 0) {
|
||||
ESP_LOGI(TAG, "%u peer(s) currently bonded", (unsigned)total);
|
||||
size_t shown = total < sizeof(list) / sizeof(list[0])
|
||||
? total : sizeof(list) / sizeof(list[0]);
|
||||
for (size_t i = 0; i < shown; i++) {
|
||||
const uint8_t *b = list[i].bytes;
|
||||
ESP_LOGI(TAG, " [%u] %02x:%02x:%02x:%02x:%02x:%02x type=%u",
|
||||
(unsigned)i,
|
||||
b[0], b[1], b[2], b[3], b[4], b[5], list[i].type);
|
||||
}
|
||||
if (total > shown) {
|
||||
ESP_LOGI(TAG, " (%u more not shown)",
|
||||
(unsigned)(total - shown));
|
||||
}
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(ble_uart_open());
|
||||
}
|
||||
|
||||
24
examples/bluetooth/ble_uart_service/sdkconfig.ci.bluedroid
Normal file
24
examples/bluetooth/ble_uart_service/sdkconfig.ci.bluedroid
Normal file
@@ -0,0 +1,24 @@
|
||||
# CI build overlay: Bluedroid host (sdkconfig.defaults selects NimBLE).
|
||||
# Mirrors sdkconfig.bluedroid; kept in sync for idf-build-apps CONFIG_NAME=bluedroid.
|
||||
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_ENABLED=y
|
||||
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_BLUEDROID_ENABLED=y
|
||||
|
||||
CONFIG_BT_BLE_SMP_ENABLE=y
|
||||
|
||||
|
||||
CONFIG_BT_GATTS_ENABLE=y
|
||||
|
||||
# CONFIG_BT_GATTC_ENABLE is not set
|
||||
|
||||
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
|
||||
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
|
||||
|
||||
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
|
||||
|
||||
CONFIG_BT_BLE_42_ADV_EN=y
|
||||
|
||||
# CONFIG_BT_BLE_42_SCAN_EN is not set
|
||||
7
examples/bluetooth/ble_uart_service/sdkconfig.ci.nimble
Normal file
7
examples/bluetooth/ble_uart_service/sdkconfig.ci.nimble
Normal file
@@ -0,0 +1,7 @@
|
||||
# CI build overlay: NimBLE host (sdkconfig.defaults is NimBLE-first).
|
||||
# Explicit config so idf-build-apps builds both nimble and bluedroid in CI.
|
||||
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_SM_SC=y
|
||||
CONFIG_BT_NIMBLE_NVS_PERSIST=y
|
||||
@@ -8,9 +8,10 @@ This document lives in **`examples/bluetooth/common/ble_uart/`** next to the
|
||||
**Reference application:** use the **`examples/bluetooth/ble_uart_service`**
|
||||
example as the working template. Its root `CMakeLists.txt` appends this
|
||||
directory to **`EXTRA_COMPONENT_DIRS`** so `main` can `REQUIRES ble_uart`;
|
||||
`main/main.c` initializes NVS and a MAC-derived GAP name, calls
|
||||
`ble_uart_install()` / `ble_uart_open()` with the default encrypted UART-over-BLE echo
|
||||
path, and the tree ships `sdkconfig.defaults` plus the Bluedroid overlay
|
||||
`main/main.c` initializes NVS, calls `ble_uart_install()` /
|
||||
`ble_uart_open()` with the Kconfig-supplied GAP name and the default
|
||||
encrypted UART-over-BLE echo path, and the tree ships
|
||||
`sdkconfig.defaults` plus the Bluedroid overlay
|
||||
(`sdkconfig.bluedroid`). Clone or diff that project when adapting to a new
|
||||
target or host stack.
|
||||
|
||||
@@ -61,7 +62,7 @@ is entirely up to you**.
|
||||
|
||||
Canonical sources live under **`$IDF_PATH/examples/bluetooth/common/ble_uart/`**
|
||||
(component name `ble_uart`): `ble_uart.h`, `ble_uart_nimble.c`,
|
||||
`ble_uart_bluedroid.c`, `CMakeLists.txt`, and `Kconfig` (prefix + RX scratch;
|
||||
`ble_uart_bluedroid.c`, `CMakeLists.txt`, and `Kconfig` (device name + RX scratch;
|
||||
`menuconfig → Component configuration → ESP-BLE-UART library`). When reusing
|
||||
outside this tree, copy the whole `common/ble_uart/` directory or at least
|
||||
merge `Kconfig` into your component so the same `CONFIG_BLE_UART_*` symbols
|
||||
@@ -264,17 +265,32 @@ back.
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
bool encrypted; /* Master switch for SC + Bonding + MITM */
|
||||
const char *device_name; /* GAP device name; NULL uses the NimBLE default */
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;/* RX byte callback */
|
||||
bool encrypted; /* Preset shortcut for SC + Bonding + MITM */
|
||||
ble_uart_security_t security; /* Per-feature overrides — see §5.6 */
|
||||
|
||||
const char *device_name; /* GAP service device name (UUID 0x2A00) */
|
||||
|
||||
/* Custom advertising bytes — see §5.9. NULL keeps the default
|
||||
* payload. ble_uart prepends the 3-byte Flags AD itself; you don't. */
|
||||
const uint8_t *adv_data;
|
||||
size_t adv_data_len; /* ≤ BLE_UART_ADV_DATA_MAX (28) */
|
||||
const uint8_t *scan_rsp_data;
|
||||
size_t scan_rsp_data_len;/* ≤ BLE_UART_SCAN_RSP_DATA_MAX (31) */
|
||||
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;/* RX byte callback */
|
||||
ble_uart_evt_cb_t on_event; /* Lifecycle / link-state events */
|
||||
} ble_uart_config_t;
|
||||
```
|
||||
|
||||
| Field | Type | Required | Default / meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `encrypted` | `bool` | yes | `true` = SC + Bonding + MITM + DisplayOnly + encrypted GATT chars; `false` = fully plaintext (sniffable, lab use only) |
|
||||
| `device_name` | `const char *` | recommended | Any string. Mind the 31-byte primary advertising packet limit: flags(3) + tx_pwr(3) + name(2 + length) + 128-bit UUID(18) → keep the name ≤ 8 bytes |
|
||||
| `encrypted` | `bool` | yes | One-line preset for the override fields under `security`: `true` = SC + Bonding + MITM + DisplayOnly + encrypted+authenticated GATT chars; `false` = fully plaintext (sniffable, lab use only). Override individual bits via `security.*` — see §5.6. |
|
||||
| `security` | `ble_uart_security_t` | optional | A zero-initialised member (`security.{sc,bonding,mitm,io_cap} = AUTO`) inherits everything from `encrypted`. Set any sub-field to `OFF`/`ON` (or pick a specific `io_cap`) to override just that bit. Out-of-range enum values, or impossible combos like `mitm=ON` with `io_cap=NO_INPUT_OUTPUT`, fail `ble_uart_install()` with `BLE_UART_EINVAL`. Full reference in §5.6. |
|
||||
| `device_name` | `const char *` | recommended | Set as the GAP-service Device Name (UUID 0x2A00). With the **default** advertising payload it is also placed in the primary adv as the Complete Local Name; with custom `adv_data` (see §5.9) it is **not** auto-included — the application owns the adv bytes. Length must be ≤ **`BLE_UART_DEVICE_NAME_MAX` = 26** (sized so the default Flags + Name AD layout always fits in a 31-byte primary packet). Longer names fail `ble_uart_install()` synchronously with `BLE_UART_EINVAL`. |
|
||||
| `adv_data` / `adv_data_len` | bytes + length | optional | Application-controlled raw advertisement data. NULL keeps the built-in default (Complete Local Name only). Max length **`BLE_UART_ADV_DATA_MAX` = 28** (the 31-byte primary packet minus our 3-byte Flags AD). Buffer is copied in `install`; the pointer doesn't need to outlive the call. See §5.9. |
|
||||
| `scan_rsp_data` / `scan_rsp_data_len` | bytes + length | optional | Application-controlled raw scan-response data. NULL keeps the built-in default (128-bit BLE UART service UUID). Max length **`BLE_UART_SCAN_RSP_DATA_MAX` = 31** (no Flags element here). Same copy semantics as `adv_data`. |
|
||||
| `ble_uart_on_rx` | callback | optional | `NULL` discards every received byte |
|
||||
| `on_event` | callback | optional | `NULL` drops every event (see §5.2.1). **Not required** for the default preset (`encrypted=true`, all `security.*` AUTO → Passkey Display): the port logs the 6-digit passkey to UART and completes pairing without a callback. **Required** when `io_cap` is `KEYBOARD_ONLY`, `DISPLAY_YES_NO`, or `KEYBOARD_DISPLAY` — otherwise `ble_uart_install()` returns `BLE_UART_EINVAL`. |
|
||||
|
||||
### 5.2 RX callback signature
|
||||
|
||||
@@ -290,50 +306,322 @@ static void my_handler(const uint8_t *data, size_t len)
|
||||
|
||||
**Caveats**:
|
||||
|
||||
- The callback runs in the **NimBLE host task** context — **do not
|
||||
block**; offload heavy work to your own task.
|
||||
- The callback runs on the BLE host task (NimBLE host task /
|
||||
Bluedroid BTC task) — **do not block**; offload heavy work to your
|
||||
own task.
|
||||
- A single callback may carry only **part** of an upper-layer frame
|
||||
(the central slices on ATT MTU). Framing logic (line / TLV /
|
||||
length-prefixed) is your responsibility.
|
||||
- The data carries **no `ctx` argument**. If your callback needs state,
|
||||
use a file-scope `static` or a global.
|
||||
|
||||
### 5.3 Lifecycle functions
|
||||
### 5.2.1 Event callback
|
||||
|
||||
```c
|
||||
typedef void (*ble_uart_evt_cb_t)(const ble_uart_evt_t *evt);
|
||||
|
||||
static void on_event(const ble_uart_evt_t *e)
|
||||
{
|
||||
switch (e->id) {
|
||||
case BLE_UART_EVT_CONNECTED: /* link up */ break;
|
||||
case BLE_UART_EVT_DISCONNECTED: /* e->disconnected.reason */ break;
|
||||
case BLE_UART_EVT_SUBSCRIBED: /* e->subscribed.subscribed */ break;
|
||||
case BLE_UART_EVT_LINK_SECURE:
|
||||
if (e->link_secure.encrypted && e->link_secure.authenticated) {
|
||||
/* Safe to forward sensitive payloads now */
|
||||
}
|
||||
break;
|
||||
case BLE_UART_EVT_PASSKEY_DISPLAY: /* e->passkey.passkey */ break;
|
||||
case BLE_UART_EVT_PASSKEY_REQUEST: /* user types peer's 6-digit;
|
||||
ble_uart_passkey_reply(d) */ break;
|
||||
case BLE_UART_EVT_NUMERIC_COMPARE: /* e->numeric_compare.passkey,
|
||||
ble_uart_compare_reply(b) */ break;
|
||||
case BLE_UART_EVT_PAIRING_FAILED: /* e->pairing_failed.reason */ break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| `evt->id` | Payload (anonymous-union member) | Fires when |
|
||||
| --- | --- | --- |
|
||||
| `BLE_UART_EVT_CONNECTED` | — | Physical link up |
|
||||
| `BLE_UART_EVT_DISCONNECTED` | `disconnected.reason` (int, stack-specific) | Physical link down — Bluedroid: `esp_gatt_conn_reason_t`; NimBLE: BLE host return code (`BLE_HS_HCI_ERR()` for HCI) |
|
||||
| `BLE_UART_EVT_SUBSCRIBED` | `subscribed.subscribed` | CCCD on TX changed (edge-triggered) |
|
||||
| `BLE_UART_EVT_LINK_SECURE` | `link_secure.{encrypted,authenticated,bonded,key_size}` | Pairing or bonded reconnect succeeds |
|
||||
| `BLE_UART_EVT_PASSKEY_DISPLAY` | `passkey.passkey` (0..999999) | SM generated a passkey for the central to type (Passkey Display). **Optional** — the port always prints a banner to UART; with `on_event == NULL` the event is dropped and pairing still completes (NimBLE injects the passkey internally; Bluedroid needs no app reply). Register `on_event` only if you want a custom UI in addition to the log line. |
|
||||
| `BLE_UART_EVT_PASSKEY_REQUEST` | — | SM asks the user to enter a passkey shown by the central — application **must** reply via `ble_uart_passkey_reply()` (see §5.6.1). Requires `on_event != NULL` at install time. |
|
||||
| `BLE_UART_EVT_NUMERIC_COMPARE` | `numeric_compare.passkey` (0..999999) | SM asks the user to confirm the displayed value matches the central — application **must** reply via `ble_uart_compare_reply()` (see §5.6.1). Requires `on_event != NULL` at install time. |
|
||||
| `BLE_UART_EVT_PAIRING_FAILED` | `pairing_failed.reason` | Pairing rejected or timed out (including no application reply for `PASSKEY_REQUEST` / `NUMERIC_COMPARE` before the SM's pairing timeout) |
|
||||
| `BLE_UART_EVT_CLOSED` | `closed.status` (`BLE_UART_*` from the worker's `ble_uart_close()`) | `ble_uart_close_async()` worker finished — then `uninstall` on an app task (§5.3.2) |
|
||||
|
||||
**Use `LINK_SECURE`, not `is_connected()`, to gate any logic that
|
||||
requires the link to be encrypted / authenticated** — bare
|
||||
`is_connected()` returns `true` while the link is still plaintext, and
|
||||
inferring security from `encrypted` / `authenticated` separately on the
|
||||
caller side is exactly the kind of leak the callback is designed to
|
||||
plug.
|
||||
|
||||
**Threading**: same context and rules as `ble_uart_on_rx` (NimBLE host
|
||||
task / Bluedroid BTC task). Don't block, don't call `ble_uart_close` /
|
||||
`ble_uart_uninstall` from inside the callback — use
|
||||
`ble_uart_close_async()` (§5.3.2) if you need to teardown in response
|
||||
to an event.
|
||||
|
||||
**Exception — `BLE_UART_EVT_CLOSED`**: this single event fires from
|
||||
the close-async worker task instead of the BLE host task; by the time
|
||||
it runs the host task is already gone. Keep the handler short: set a
|
||||
flag or notify an app task — do **not** call `ble_uart_uninstall()`
|
||||
here (see §5.3.2). The worker clears `s_closing` only after your
|
||||
handler returns.
|
||||
|
||||
**Ordering contracts (both backends)**:
|
||||
|
||||
- A single CCCD value change fires exactly one `SUBSCRIBED` event
|
||||
(edge-triggered — repeating the same write is a no-op).
|
||||
- If the central was subscribed at the moment the link drops, you get
|
||||
`SUBSCRIBED(false)` **before** `DISCONNECTED`. NimBLE does this
|
||||
natively (`BLE_GAP_SUBSCRIBE_REASON_TERM`); the Bluedroid backend
|
||||
synthesizes the same sequence so consumers can write a single state
|
||||
machine that works on either host.
|
||||
- `LINK_SECURE` always arrives after `CONNECTED` — pairing can't run
|
||||
without a link.
|
||||
- `BLE_UART_EVT_CLOSED` always arrives **after**
|
||||
`BLE_UART_EVT_DISCONNECTED` (when there was a peer) — the
|
||||
close-async worker calls the same disconnect+wait sequence as the
|
||||
synchronous `ble_uart_close()` before firing CLOSED.
|
||||
|
||||
**Backend differences**:
|
||||
|
||||
- `BLE_UART_EVT_LINK_SECURE.key_size`: NimBLE reports the negotiated
|
||||
size (7..16); Bluedroid surfaces a fixed 16 — Bluedroid sets
|
||||
`ESP_BLE_SM_MAX_KEY_SIZE=16` at install time and does not expose the
|
||||
negotiated size on `AUTH_CMPL`.
|
||||
- Bonded reconnects: NimBLE re-fires `LINK_SECURE` on every encryption
|
||||
change; Bluedroid only fires `AUTH_CMPL_EVT` when the SM exchange
|
||||
actually runs, so a pure LTK-restart may not refire the event.
|
||||
- CCCD persistence on bonded reconnect: NimBLE re-fires
|
||||
`SUBSCRIBED(true)` automatically (via `BLE_GAP_SUBSCRIBE_REASON_RESTORE`)
|
||||
when the bonded peer reconnects; Bluedroid does not persist CCCD
|
||||
across connections, so the central has to write CCCD again to
|
||||
resubscribe.
|
||||
|
||||
### 5.3 Lifecycle — bring-up and release
|
||||
|
||||
#### API summary
|
||||
|
||||
```c
|
||||
int ble_uart_install(const ble_uart_config_t *cfg);
|
||||
int ble_uart_open(void);
|
||||
int ble_uart_close(void);
|
||||
int ble_uart_close_async(void); /* fire-and-forget, see §5.3.2–5.3.4 */
|
||||
int ble_uart_uninstall(void);
|
||||
```
|
||||
|
||||
| Function | What it does (NimBLE) | What it does (Bluedroid) | When to call | Blocking? |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `install` | `nimble_port_init` + `ble_hs_cfg` + SM + SIG services + UART GATT | `controller_init/enable` + `bluedroid_init/enable` + SM + `esp_ble_gatts_create_attr_tab` (waits ≤500 ms for the attr-table event) | After `nvs_flash_init()`, before `open` | No, ~50 ms (NimBLE) / ~150 ms (Bluedroid) |
|
||||
| `open` | Bond store + spawn host task + start advertising once synced | Configure adv data + scan rsp + start advertising | After `install` | No, host runs in the background |
|
||||
| `close` | Stop adv → graceful disconnect (LL_TERMINATE_IND, waits ≤500 ms for the disconnect event) → `nimble_port_stop()` | Stop adv → graceful disconnect (`esp_ble_gap_disconnect`, waits ≤500 ms) | After `open`, before `uninstall` | Yes, up to ~500 ms while waiting for the peer disconnect |
|
||||
| `uninstall` | Calls `close` if still open, then `nimble_port_deinit()` and resets module state | Calls `close` if still open, then `bluedroid_disable+deinit` + `controller_disable+deinit` | After `close` (or directly — `uninstall` cascades into `close` on its own) | Yes, follows the same wait window as `close` |
|
||||
| `open` | Spawn host task + `ble_hs_start` (first time via `BLE_HS_AUTO_START`, later via `ble_hs_sched_start`) + advertising once synced; after a prior `close`, re-queues GAP/GATT/UART svc defs (§5.3.1a) | Configure adv data + scan rsp + start advertising (GATT table from `install` stays up) | After `install` | No, host runs in the background |
|
||||
| `close` | Stop adv → graceful disconnect (≤500 ms) → `nimble_port_stop()` → `ble_gatts_reset()` | Stop adv → graceful disconnect (≤500 ms); host + GATT table stay up | After `open`, before `uninstall`; **not** from host-task callbacks (§5.3.2) | Yes, up to ~500 ms (NimBLE) |
|
||||
| `close_async` | Worker runs the same body as `close`, then `BLE_UART_EVT_CLOSED` | Same | From `on_event` / `on_rx` (host task) when sync `close` would deadlock | No (returns once worker is spawned) |
|
||||
| `uninstall` | `close` if still open (+ poll in-flight `close_async` ≤~5 s), then `nimble_port_deinit`, wipe module state | Same + controller deinit | After the radio is fully closed (§5.3.2); **not** from host-task callbacks | Yes |
|
||||
|
||||
Call order:
|
||||
**Bring-up** (every product):
|
||||
|
||||
```
|
||||
nvs_flash_init
|
||||
└── ble_uart_install
|
||||
└── ble_uart_open ← BLE is live
|
||||
└── ble_uart_close
|
||||
└── ble_uart_uninstall ← clean state, can install again
|
||||
```text
|
||||
nvs_flash_init()
|
||||
└── ble_uart_install(&cfg) /* once per uninstall cycle */
|
||||
└── ble_uart_open() /* advertising + pairing; BLE is live */
|
||||
```
|
||||
|
||||
Each call returns `BLE_HS_EALREADY` if the corresponding state is
|
||||
already true (e.g. `open` called twice, or `close` called when the
|
||||
radio is already down). It is therefore safe to call `close` /
|
||||
`uninstall` defensively at shutdown without checking the current state
|
||||
yourself.
|
||||
Run-forever firmware can stop here — no `close` / `uninstall` required.
|
||||
|
||||
**Do NOT call `close` / `uninstall` from inside `ble_uart_on_rx`** —
|
||||
that callback runs on the NimBLE host task, and `close` blocks on
|
||||
`nimble_port_stop()` which expects the host task to exit. Self-stop
|
||||
deadlocks. Forward the request to a normal FreeRTOS task instead.
|
||||
**Release** — pick **one** path below. `close` stops the radio but keeps
|
||||
`install` state (you can `open()` again). `uninstall` tears the host +
|
||||
controller down so `install()` can run from scratch.
|
||||
|
||||
| Goal | Call sequence | Who calls `close` / `uninstall` |
|
||||
| --- | --- | --- |
|
||||
| Power BLE off from a **normal app task** (button, Wi-Fi, `app_main` shutdown) | `ble_uart_close()` → `ble_uart_uninstall()` | That app task only |
|
||||
| Power BLE off **because of a BLE event** (RX command, failed pairing, policy) | `ble_uart_close_async()` in `on_event` / `on_rx` → wait for `BLE_UART_EVT_CLOSED` → `ble_uart_uninstall()` on an **app task** (§5.3.2) | `close_async` in callback; `uninstall` deferred |
|
||||
|
||||
Each API returns `BLE_UART_EALREADY` when the module is already in the
|
||||
target state, so defensive `close` / `uninstall` at shutdown without
|
||||
manual state checks is fine **as long as** you follow the release path
|
||||
for your scenario.
|
||||
|
||||
#### 5.3.1 Path A — synchronous release (recommended default)
|
||||
|
||||
Use when teardown is **not** triggered from inside `on_event` /
|
||||
`on_rx` (NimBLE host task / Bluedroid BTC task). This is what the
|
||||
`ble_uart_service` example does.
|
||||
|
||||
```c
|
||||
void shutdown_ble_from_app_task(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
rc = ble_uart_close();
|
||||
if (rc != BLE_UART_OK && rc != BLE_UART_EALREADY) {
|
||||
ESP_LOGE(TAG, "ble_uart_close rc=%d", rc);
|
||||
}
|
||||
|
||||
rc = ble_uart_uninstall();
|
||||
if (rc != BLE_UART_OK && rc != BLE_UART_EALREADY) {
|
||||
ESP_LOGE(TAG, "ble_uart_uninstall rc=%d", rc);
|
||||
}
|
||||
/* BLE UART fully released — safe to ble_uart_install() again */
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
ble_uart_open() /* running */
|
||||
│
|
||||
▼
|
||||
ble_uart_close() /* same app task; not from on_event / on_rx */
|
||||
│
|
||||
▼
|
||||
ble_uart_uninstall()
|
||||
```
|
||||
|
||||
- `uninstall` may call `close` internally if you skipped `close` — still
|
||||
call both explicitly so return codes are obvious in your logs.
|
||||
- Do **not** call `close` or `uninstall` from `on_event` / `on_rx` — use
|
||||
Path B instead.
|
||||
|
||||
#### 5.3.1a Pausing and resuming (`close` then `open` again)
|
||||
|
||||
`install` state is preserved across `close()` — you may call `open()`
|
||||
again without `uninstall()`. This is what the `ble_uart_service` example
|
||||
exercises in `app_main` (open → close → open) to prove the cycle.
|
||||
|
||||
**NimBLE backend**
|
||||
|
||||
| Topic | Behaviour |
|
||||
| --- | --- |
|
||||
| GATT services | Same set as after `install`: GAP (`0x1800`), GATT (`0x1801`), BLE UART (NUS). `close()` calls the public `ble_gatts_reset()`; the next `open()` re-runs `ble_svc_gap_init()`, `ble_svc_gatt_init()`, and re-adds the UART service. |
|
||||
| ATT handles | **Not stable** — centrals must run a full service discovery after each reconnect; do not cache handles across a `close`/`open` cycle. |
|
||||
| Subscriptions | Cleared — the central must re-enable TX notifications (CCCD). |
|
||||
| Bonds | NVS bond store is unchanged (still configured at `install()`). |
|
||||
| First vs later `open` | With default `BLE_HS_AUTO_START`, the first `open()` consumes the one-shot auto-start queued by `nimble_port_init()`; every later `open()` must call `ble_hs_sched_start()` (handled inside `ble_uart_open()`). |
|
||||
|
||||
**Bluedroid backend**
|
||||
|
||||
`close()` only stops advertising and disconnects; the host and attribute
|
||||
table created at `install()` stay registered. A second `open()` restarts
|
||||
advertising. GATT handles are typically unchanged.
|
||||
|
||||
**Extra GATT services (§6.3)**
|
||||
|
||||
Services you register with `ble_gatts_add_svcs()` / `ble_svc_*_init()`
|
||||
at `install()` time are **not** automatically re-registered by
|
||||
`ble_uart` on a later `open()` after `close()` (NimBLE only re-adds
|
||||
GAP, GATT, and UART). Either call your init/add functions again inside
|
||||
your own `open()` hook after `ble_uart_close()`, or use
|
||||
`close()` → `uninstall()` → `install()` → `open()` for a full rebuild.
|
||||
|
||||
#### 5.3.2 Path B — release after a BLE event (`close_async`)
|
||||
|
||||
Use when the **reason** to shut down arrives on the host task (e.g.
|
||||
`BLE_UART_EVT_PAIRING_FAILED`, an RX “power off” byte, or
|
||||
`LINK_SECURE` policy). Synchronous `close()` deadlocks there; use
|
||||
`close_async()` and **defer** `uninstall()` to a normal task.
|
||||
|
||||
```c
|
||||
static volatile bool s_ble_closed_ok;
|
||||
|
||||
static void on_event(const ble_uart_evt_t *e)
|
||||
{
|
||||
switch (e->id) {
|
||||
case BLE_UART_EVT_PAIRING_FAILED:
|
||||
ble_uart_close_async(); /* OK: host-task context */
|
||||
break;
|
||||
|
||||
case BLE_UART_EVT_CLOSED:
|
||||
/* Runs on the close-async worker — keep this short. Do NOT call
|
||||
* ble_uart_uninstall() here (s_closing is still set; see §5.3.3). */
|
||||
if (e->closed.status == BLE_UART_OK) {
|
||||
s_ble_closed_ok = true; /* or xTaskNotifyGive / queue */
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ble_shutdown_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
if (s_ble_closed_ok) {
|
||||
s_ble_closed_ok = false;
|
||||
ble_uart_uninstall(); /* normal app task */
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
on_event / on_rx (host task):
|
||||
ble_uart_close_async()
|
||||
│
|
||||
▼
|
||||
[worker: do_close ≈ sync close]
|
||||
│
|
||||
├── BLE_UART_EVT_DISCONNECTED (if peer was connected)
|
||||
└── BLE_UART_EVT_CLOSED (worker task; set flag only)
|
||||
│
|
||||
▼
|
||||
app task (not host, not inside CLOSED handler):
|
||||
ble_uart_uninstall()
|
||||
```
|
||||
|
||||
- `close_async` returns `BLE_UART_OK` once the worker is **spawned**, not
|
||||
when close finishes.
|
||||
- Only `BLE_UART_EVT_CLOSED` with `.closed.status == BLE_UART_OK` means
|
||||
the same quiesced state as `ble_uart_close()` — then it is safe to
|
||||
`uninstall()` from your app task.
|
||||
- On failure (`BLE_UART_EFAIL`, etc.) the port may still be open; retry
|
||||
`ble_uart_close()` / `ble_uart_close_async()` from an app task.
|
||||
|
||||
#### 5.3.3 `close_async` + `uninstall` — rules and pitfalls
|
||||
|
||||
`ble_uart_uninstall()` **polls** an in-flight `close_async` worker for
|
||||
up to **~5 s**. If the worker has not exited it logs
|
||||
`uninstall: close_async worker still running, tearing down anyway` and
|
||||
continues anyway — treat that as an application bug, not a supported
|
||||
path.
|
||||
|
||||
| Do | Don't |
|
||||
| --- | --- |
|
||||
| `close_async()` in `on_event` / `on_rx`; `uninstall()` later on **one** app task after `CLOSED` + `BLE_UART_OK` | `uninstall()` in the same task right after `close_async()` without waiting |
|
||||
| Set a flag / queue in `BLE_UART_EVT_CLOSED`; return immediately | `ble_uart_uninstall()` **inside** `BLE_UART_EVT_CLOSED` (worker still holds `s_closing`) |
|
||||
| Sync `close` + `uninstall` from a button / network task | `close` / `uninstall` from host-task callbacks |
|
||||
| Keep `on_event` / `on_rx` short while a close is in flight | Multi-second blocking in callbacks during `close_async` |
|
||||
| After a timeout log, fix ordering before `install()` again | Immediate `install()` + `open()` + `close_async()` after a wedged teardown |
|
||||
|
||||
If you see `uninstall: close_async worker still running, tearing down
|
||||
anyway`, fix call ordering (§5.3.2) before calling `install()` again.
|
||||
|
||||
#### 5.3.4 `ble_uart_close_async()` — reference
|
||||
|
||||
Some applications need to teardown the radio in response to a BLE
|
||||
event — examples: a "shutdown" command on RX, a `LINK_SECURE` whose
|
||||
flags don't meet the application's policy, or a `PAIRING_FAILED` from
|
||||
a peer that's been blacklisted. Because the synchronous `close()` is
|
||||
called *from* the host task it would normally run on, calling it
|
||||
inline would deadlock. `close_async()` papers over that: it spawns a
|
||||
small worker task (~3 KB stack, idle+2 priority) that runs the same
|
||||
close body, then signals completion via the event callback.
|
||||
|
||||
**Behaviour** (see §5.3.2 for the full release flow):
|
||||
|
||||
- `close_async` returns `BLE_UART_OK` once the worker has been spawned.
|
||||
- `BLE_UART_EVT_DISCONNECTED` (if connected) then `BLE_UART_EVT_CLOSED`
|
||||
with `.closed.status` — same ≤500 ms disconnect window as sync `close`.
|
||||
- Second call while draining → `BLE_UART_EALREADY`; before `open` →
|
||||
`BLE_UART_EALREADY`; spawn failure → `BLE_UART_ENOMEM` (latch reset).
|
||||
|
||||
### 5.4 TX interface
|
||||
|
||||
@@ -354,11 +642,11 @@ ble_uart_tx((const uint8_t *)line, (size_t)n);
|
||||
|
||||
| Return | Meaning |
|
||||
| --- | --- |
|
||||
| `0` | Success (notification handed to the stack) |
|
||||
| `BLE_HS_ENOTCONN` | No central connected; **this is normal — typically ignore** |
|
||||
| `BLE_HS_EINVAL` | `data == NULL` or `len == 0` |
|
||||
| `BLE_HS_ENOMEM` | Stack mbuf pool exhausted |
|
||||
| other | Internal stack error — see `ble_hs.h` |
|
||||
| `BLE_UART_OK` | Success (notification handed to the stack) |
|
||||
| `BLE_UART_ENOTCONN` | No central connected; **this is normal — typically ignore** |
|
||||
| `BLE_UART_EINVAL` | `data == NULL` or `len == 0` |
|
||||
| `BLE_UART_ENOMEM` | Stack mbuf pool exhausted |
|
||||
| `BLE_UART_EFAIL` | Internal stack error — see logs |
|
||||
|
||||
**Calling context**: any FreeRTOS task at any priority. **Not callable
|
||||
from an ISR** — push the data to a queue from the ISR and let a task
|
||||
@@ -382,16 +670,425 @@ bool ble_uart_is_subscribed(void);
|
||||
You usually **don't need** to query these up-front — `ble_uart_tx`
|
||||
returns `ENOTCONN` to tell you.
|
||||
|
||||
### 5.6 Service UUID constant
|
||||
### 5.6 Security configuration
|
||||
|
||||
`cfg.encrypted` is a one-line **preset** that turns on every part of
|
||||
the stack's security toolbox at once — LE Secure Connections, bonding
|
||||
(LTK persisted in NVS), MITM protection, DisplayOnly IO, and the
|
||||
`_ENC | _AUTHEN` flags on the GATT characteristics. It maps to the
|
||||
older two-state behaviour and is what the "secure by default" template
|
||||
in §4.4 picks.
|
||||
|
||||
For applications that need finer control — a displayless gateway that
|
||||
still wants encrypted bonding, a one-shot encrypted session that
|
||||
doesn't keep an LTK, an interop test build that disables only MITM —
|
||||
each component of the preset can be flipped individually through the
|
||||
`cfg.security` sub-struct:
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
BLE_UART_SEC_AUTO = 0, /* follow cfg.encrypted */
|
||||
BLE_UART_SEC_OFF = 1,
|
||||
BLE_UART_SEC_ON = 2,
|
||||
} ble_uart_sec_t;
|
||||
|
||||
typedef enum {
|
||||
BLE_UART_IO_CAP_AUTO = 0, /* DisplayOnly when MITM is on;
|
||||
NoInputNoOutput when off.
|
||||
Passkey Display needs no on_event */
|
||||
BLE_UART_IO_CAP_NO_INPUT_OUTPUT = 1, /* Just Works only */
|
||||
BLE_UART_IO_CAP_DISPLAY_ONLY = 2, /* Passkey Display — UART banner
|
||||
+ optional PASSKEY_DISPLAY;
|
||||
no on_event required */
|
||||
BLE_UART_IO_CAP_KEYBOARD_ONLY = 3, /* Passkey Entry — PASSKEY_REQUEST;
|
||||
on_event required */
|
||||
BLE_UART_IO_CAP_DISPLAY_YES_NO = 4, /* Numeric Comparison;
|
||||
on_event required */
|
||||
BLE_UART_IO_CAP_KEYBOARD_DISPLAY = 5, /* PASSKEY_REQUEST or NUMERIC_COMPARE;
|
||||
on_event required */
|
||||
} ble_uart_io_cap_t;
|
||||
|
||||
typedef struct {
|
||||
ble_uart_sec_t sc; /* tri-state */
|
||||
ble_uart_sec_t bonding; /* tri-state */
|
||||
ble_uart_sec_t mitm; /* tri-state */
|
||||
ble_uart_io_cap_t io_cap; /* AUTO + the five IO caps above */
|
||||
} ble_uart_security_t;
|
||||
```
|
||||
|
||||
Each of `cfg.security.{sc,bonding,mitm}` is a tri-state. `AUTO`
|
||||
(the value of any zero-initialised member) inherits from
|
||||
`cfg.encrypted`; `OFF` / `ON` override that specific bit only. The
|
||||
resolution table:
|
||||
|
||||
| `cfg.encrypted` | Override field | Resolved bit |
|
||||
| --- | --- | --- |
|
||||
| `true` | `AUTO` | ON |
|
||||
| `true` | `OFF` | OFF |
|
||||
| `true` | `ON` | ON |
|
||||
| `false` | `AUTO` | OFF |
|
||||
| `false` | `OFF` | OFF |
|
||||
| `false` | `ON` | ON |
|
||||
|
||||
`cfg.security.io_cap` follows the same `AUTO` / explicit pattern.
|
||||
The application picks an IO cap matching its UI; the SM combines it
|
||||
with the central's IO cap to elect the pairing model (see BT Core
|
||||
Spec §2.3.5.1) and ble_uart fires the matching event:
|
||||
|
||||
| Pairing model | Trigger event | Application response |
|
||||
| --- | --- | --- |
|
||||
| Just Works | (none — pairs silently) | — |
|
||||
| Passkey Display (we show) | `BLE_UART_EVT_PASSKEY_DISPLAY` (optional; UART banner always) | (none — port handles SM reply; central types the digits) |
|
||||
| Passkey Entry (user types)| `BLE_UART_EVT_PASSKEY_REQUEST` | `ble_uart_passkey_reply(d)` — **`on_event` required** |
|
||||
| Numeric Comparison | `BLE_UART_EVT_NUMERIC_COMPARE` | `ble_uart_compare_reply(b)` — **`on_event` required** |
|
||||
|
||||
Numeric Comparison additionally requires LE Secure Connections on
|
||||
both sides (legacy SM doesn't support it); against a legacy peer a
|
||||
`DISPLAY_YES_NO` / `KEYBOARD_DISPLAY` IO cap falls back to either
|
||||
Passkey Entry (with our keypad) or Just Works.
|
||||
|
||||
#### What is checked synchronously
|
||||
|
||||
`ble_uart_install()` rejects the following with `BLE_UART_EINVAL`
|
||||
**before** bringing the host stack up, so misconfigured applications
|
||||
fail fast and predictably:
|
||||
|
||||
- `cfg.security.{sc,bonding,mitm}` outside `{AUTO, OFF, ON}`
|
||||
- `cfg.security.io_cap` outside the six values listed above
|
||||
- Resolved `mitm == ON` together with resolved
|
||||
`io_cap == NO_INPUT_OUTPUT` — Just Works cannot satisfy MITM and
|
||||
the SM would otherwise reject pairing in flight
|
||||
- `cfg.on_event == NULL` together with a **configured** (not resolved)
|
||||
input-capable `io_cap` — only `KEYBOARD_ONLY`, `DISPLAY_YES_NO`, and
|
||||
`KEYBOARD_DISPLAY`. Without an event sink the application would never
|
||||
see `PASSKEY_REQUEST` / `NUMERIC_COMPARE` and pairing would silently
|
||||
stall until the SM times out. **`AUTO` (even when it resolves to
|
||||
DisplayOnly because `mitm=ON`), `DISPLAY_ONLY`, and `NO_INPUT_OUTPUT`
|
||||
do not require `on_event`** — Passkey Display is satisfied inside the
|
||||
port (UART log + internal SM reply); `PASSKEY_DISPLAY` via `on_event`
|
||||
is additive only.
|
||||
|
||||
#### How the resolved policy is applied
|
||||
|
||||
| Component | Effect |
|
||||
| --- | --- |
|
||||
| Resolved `sc` / `bonding` / `mitm` (any ON) | SM is enabled; `ble_gap_security_initiate` (NimBLE) / `esp_ble_set_encryption` (Bluedroid) runs on connect |
|
||||
| Resolved `mitm` | `ESP_BLE_SEC_ENCRYPT_MITM` vs `_NO_MITM` (Bluedroid); `_AUTHEN` flag added to GATT chars |
|
||||
| Any of the three on | Encrypted GATT permission flags (`_ENC` on NimBLE, `_ENCRYPTED` on Bluedroid) |
|
||||
| All three off | Plain `READ`/`WRITE` permissions; SM disabled |
|
||||
| Resolved `io_cap` | `BLE_HS_IO_*` (NimBLE) / `ESP_IO_CAP_*` (Bluedroid) |
|
||||
|
||||
#### Common combinations
|
||||
|
||||
```c
|
||||
/* (a) Default — secure-by-default UART. SC + Bonding + MITM, DisplayOnly.
|
||||
* on_event may be NULL: passkey is printed to UART and pairing
|
||||
* completes without PASSKEY_DISPLAY / reply callbacks. */
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
/* security.{sc,bonding,mitm,io_cap} all AUTO → all ON. */
|
||||
/* .on_event = NULL — valid for this preset */
|
||||
});
|
||||
|
||||
/* (b) Displayless gateway. SC + Bonding + Just Works (no passkey UI). */
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.security = {
|
||||
.mitm = BLE_UART_SEC_OFF,
|
||||
.io_cap = BLE_UART_IO_CAP_NO_INPUT_OUTPUT,
|
||||
},
|
||||
});
|
||||
|
||||
/* (c) Encrypted but ephemeral. Re-pair every reconnect, no NVS bond. */
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.security = { .bonding = BLE_UART_SEC_OFF },
|
||||
});
|
||||
|
||||
/* (d) Plaintext lab build. */
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = false,
|
||||
/* security.* all AUTO → all OFF. */
|
||||
});
|
||||
|
||||
/* (e) Interop test — keep encryption + bonding, drop MITM only. */
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.security = { .mitm = BLE_UART_SEC_OFF },
|
||||
/* security.io_cap AUTO → NoInputNoOutput once MITM is gone. */
|
||||
});
|
||||
|
||||
/* (f) Passkey Entry — peripheral has a keypad, central has a display.
|
||||
* User reads the 6-digit code off the central and types it here.
|
||||
* on_event MUST be set; the application wires PASSKEY_REQUEST to
|
||||
* a UI prompt and feeds the digits to ble_uart_passkey_reply(). */
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.security = { .io_cap = BLE_UART_IO_CAP_KEYBOARD_ONLY },
|
||||
.on_event = on_event,
|
||||
...
|
||||
});
|
||||
|
||||
/* (g) Numeric Comparison — peripheral has display + yes/no button.
|
||||
* Both sides see the same 6-digit value; user confirms match.
|
||||
* Requires LE Secure Connections (so .sc must be ON, which it is
|
||||
* by default with .encrypted=true). on_event MUST be set. */
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.security = { .io_cap = BLE_UART_IO_CAP_DISPLAY_YES_NO },
|
||||
.on_event = on_event,
|
||||
...
|
||||
});
|
||||
|
||||
/* (h) Touchscreen UI — full keypad+display. The SM elects either
|
||||
* Passkey Entry or Numeric Comparison depending on the central;
|
||||
* wire BOTH events. */
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.security = { .io_cap = BLE_UART_IO_CAP_KEYBOARD_DISPLAY },
|
||||
.on_event = on_event,
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
#### 5.6.1 Pairing reply API
|
||||
|
||||
**Passkey Display (default / `DISPLAY_ONLY` / `AUTO` + `mitm=ON`)** does
|
||||
not use the reply APIs. The port generates the 6-digit value, logs it,
|
||||
and drives the SM (NimBLE: `ble_sm_inject_io` on `BLE_SM_IOACT_DISP`;
|
||||
Bluedroid: no `esp_ble_passkey_reply` needed on `PASSKEY_NOTIF`). You
|
||||
only need `ble_uart_passkey_reply()` / `ble_uart_compare_reply()` for
|
||||
the interactive models below.
|
||||
|
||||
`Passkey Entry` and `Numeric Comparison` are interactive — the SM
|
||||
suspends pairing until the application reports the user's input.
|
||||
`ble_uart` exposes one reply call per flavour:
|
||||
|
||||
```c
|
||||
int ble_uart_passkey_reply(uint32_t passkey); /* 0..999999 */
|
||||
int ble_uart_compare_reply(bool match);
|
||||
```
|
||||
|
||||
Both are safe from any task, return immediately, and accept exactly
|
||||
one reply per request. Subsequent calls (or calls with no request in
|
||||
flight) return `BLE_UART_ENOTCONN`. `passkey > 999999` returns
|
||||
`BLE_UART_EINVAL`. If the user fails to reply before the SM's pairing
|
||||
timeout (controller default ≈ 30 s), the link surfaces
|
||||
`BLE_UART_EVT_PAIRING_FAILED` and any later reply is silently dropped.
|
||||
|
||||
```c
|
||||
static void on_event(const ble_uart_evt_t *e)
|
||||
{
|
||||
switch (e->id) {
|
||||
case BLE_UART_EVT_PASSKEY_REQUEST:
|
||||
/* Prompt the user; once digits are entered: */
|
||||
ble_uart_passkey_reply(user_input); /* 0..999999 */
|
||||
break;
|
||||
|
||||
case BLE_UART_EVT_NUMERIC_COMPARE:
|
||||
ESP_LOGI(TAG, "compare %06" PRIu32, e->numeric_compare.passkey);
|
||||
/* Once the user confirms: */
|
||||
ble_uart_compare_reply(true /* or false on mismatch */);
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A `false` reply to `compare_reply()` makes pairing fail with a
|
||||
numeric-comparison mismatch — surfaced as
|
||||
`BLE_UART_EVT_PAIRING_FAILED`. To cancel `PASSKEY_REQUEST` without a
|
||||
mismatch event, just don't call `passkey_reply()`; the SM will time
|
||||
out the pairing.
|
||||
|
||||
#### Backend differences
|
||||
|
||||
- **Passkey Display without `on_event`**: both backends complete pairing;
|
||||
only `PASSKEY_DISPLAY` is suppressed when the callback is `NULL`. The
|
||||
UART banner (`show_passkey`) is always emitted for log-scraping tests.
|
||||
- **Numeric Comparison edge case**: if `io_cap` resolved to DisplayOnly
|
||||
but the central still negotiates NC (rare), Bluedroid rejects the
|
||||
request when `on_event == NULL`; NimBLE may stall until the SM times
|
||||
out — use `DISPLAY_YES_NO` / `KEYBOARD_DISPLAY` with a registered
|
||||
`on_event` if you need NC.
|
||||
- **NimBLE** lets the application observe the negotiated `key_size`
|
||||
on `BLE_UART_EVT_LINK_SECURE`; **Bluedroid** surfaces a fixed `16`
|
||||
(the value forced via `ESP_BLE_SM_MAX_KEY_SIZE` at install time —
|
||||
Bluedroid does not expose the negotiated size on `AUTH_CMPL`).
|
||||
- With `mitm=OFF`, NimBLE pairs with `_AUTHEN` permissions still
|
||||
off on the chars; Bluedroid uses `ESP_GATT_PERM_*_ENCRYPTED`
|
||||
(the encryption-without-MITM tier) to match.
|
||||
- `cfg.encrypted=false` plus any `cfg.security.*=ON` override is
|
||||
allowed — it partially enables the SM, e.g.
|
||||
`cfg.encrypted=false, cfg.security.sc=ON` is "SC pairing without
|
||||
MITM and without persisted bond". Useful only for lab interop
|
||||
tests; production firmware should keep `cfg.encrypted = true` and
|
||||
only override surgically.
|
||||
|
||||
### 5.7 Bond management
|
||||
|
||||
```c
|
||||
/* All three are usable as soon as ble_uart_install() returns; they
|
||||
* do not require ble_uart_open() to have been called yet — clearing
|
||||
* stale bonds before the first advertising window is the canonical
|
||||
* use case. */
|
||||
int ble_uart_get_bond_count(size_t *out_count);
|
||||
int ble_uart_get_bonded_peers(ble_uart_addr_t *out, size_t cap, size_t *out_count);
|
||||
int ble_uart_remove_peer(const ble_uart_addr_t *peer);
|
||||
int ble_uart_clear_bonds(void);
|
||||
|
||||
/* Address type used by remove_peer and BLE_UART_EVT_CONNECTED. */
|
||||
typedef struct {
|
||||
uint8_t bytes[6]; /* big-endian: bytes[0] is the MSB octet */
|
||||
uint8_t type; /* BLE_UART_ADDR_TYPE_PUBLIC or _RANDOM */
|
||||
} ble_uart_addr_t;
|
||||
```
|
||||
|
||||
| Function | Effect |
|
||||
| --- | --- |
|
||||
| `ble_uart_get_bond_count` | Number of peers in the persistent store; 0 means "no bonds yet". Pass `cap == 0` to `get_bonded_peers` for the same count without an address buffer. |
|
||||
| `ble_uart_get_bonded_peers` | List bonded peer addresses; writes up to `cap`, reports total in `*out_count` (caller may re-call with a larger buffer if total > cap). `cap == 0` returns the count only. |
|
||||
| `ble_uart_remove_peer` | Drop one peer's LTK / IRK / persisted CCCD. **Idempotent** — returns `BLE_UART_OK` even when the peer is not in the store (NimBLE: `ble_store_util_delete_peer` treats `BLE_HS_ENOENT` as success; Bluedroid: `esp_ble_remove_bond_device` does not fail on a missing entry). Call `get_bonded_peers()` first if you need to tell "removed" from "was never bonded". |
|
||||
| `ble_uart_clear_bonds` | Drop *all* of the above; equivalent to a factory reset of the bond store, but does not touch any other NVS namespace |
|
||||
|
||||
`ble_uart_remove_peer` and `ble_uart_clear_bonds` do **not** actively
|
||||
disconnect the current link (encrypted or not). Call `ble_uart_close()`
|
||||
first if you need an immediate disconnect and re-pair.
|
||||
|
||||
**Where do I get the address?** From `BLE_UART_EVT_CONNECTED.connected.peer`
|
||||
(see §5.2.1). Save it from your event handler the first time you see
|
||||
each new peer, then pass it to `ble_uart_remove_peer` later when you
|
||||
want to forget it.
|
||||
|
||||
**Backend notes**:
|
||||
|
||||
- Bluedroid matches bonds by BD address alone — `peer->type` is
|
||||
ignored by `remove_peer`. If the peer first connected as
|
||||
`address_A` and bonding succeeded, CONNECT and `get_bonded_peers()`
|
||||
keep reporting `address_A` on later reconnects even when the
|
||||
peer's over-the-air address has changed (e.g. a new RPA).
|
||||
- NimBLE matches by `(type, identity-address)` — for an RPA peer this
|
||||
is the resolved identity, **not** the random address you saw on the
|
||||
wire. `BLE_UART_EVT_CONNECTED` reports the resolved identity when
|
||||
it's known (post-pairing reconnect of a bonded RPA peer); on first
|
||||
pair it equals the OTA random address, so the bond is recorded
|
||||
under that random address and `remove_peer` works either way.
|
||||
- Neither backend reports "peer not found" from `remove_peer` — a
|
||||
wrong `(type, bytes)` pair that does not match any stored bond
|
||||
still returns `BLE_UART_OK`. This mirrors the underlying stacks'
|
||||
delete-if-present semantics, not a lookup-then-delete API.
|
||||
- `ble_uart_clear_bonds` on Bluedroid iterates the bond list and
|
||||
removes each entry; on NimBLE it calls `ble_store_clear()`, which
|
||||
also wipes the local LTK and any persisted CCCD.
|
||||
- **NimBLE** `get_bond_count` / `get_bonded_peers(cap=0)` heap-allocate a
|
||||
scratch buffer sized to `BLE_STORE_MAX_BONDS` (not the caller's stack),
|
||||
so they are safe from small-stack tasks regardless of
|
||||
`CONFIG_BT_NIMBLE_MAX_BONDS`.
|
||||
|
||||
### 5.8 Service UUID constant
|
||||
|
||||
```c
|
||||
extern const ble_uart_uuid128_t ble_uart_service_uuid;
|
||||
```
|
||||
|
||||
Always `6e400001-b5a3-f393-e0a9-e50e24dcca9e` (the de-facto BLE UART service UUID). It is
|
||||
already inserted into the scan response, so the **application normally
|
||||
does not touch it**. You only need it if you take over advertising
|
||||
yourself (see 6.3).
|
||||
already inserted into the scan response **by the default payload**, so
|
||||
the application only needs to reference it when it takes over the adv
|
||||
bytes itself (see §5.9) or otherwise replaces our advertising (see §6.3).
|
||||
|
||||
### 5.9 Custom advertising payloads
|
||||
|
||||
`ble_uart` builds a sensible default for both the primary advertisement
|
||||
and the scan response:
|
||||
|
||||
| Packet | Default content | Why |
|
||||
| --- | --- | --- |
|
||||
| Primary adv (31 B max) | Flags AD + Complete Local Name (`device_name`) | Phones show the name; everything else in the 31 bytes is left for the application to add via `adv_data` |
|
||||
| Scan response (31 B max) | Complete 128-bit BLE UART service UUID (18 B element) | The 128-bit UUID alone is too big to share the primary packet with a typical name |
|
||||
|
||||
Set `adv_data` / `scan_rsp_data` in the config to override **everything
|
||||
the application sees** — only the 3-byte Flags AD element of the primary
|
||||
packet stays library-controlled (the BT spec mandates a Flags element,
|
||||
and a few of its bits — General Discoverable / BR-EDR Not Supported —
|
||||
are state we already negotiated with the controller).
|
||||
|
||||
```c
|
||||
/* +-- 31-byte primary advertisement packet ---------------------+
|
||||
* | [02 01 06] ← Flags AD prepended by ble_uart (3 bytes) |
|
||||
* | <up to BLE_UART_ADV_DATA_MAX = 28 bytes from your buffer> |
|
||||
* +-------------------------------------------------------------+
|
||||
*
|
||||
* +-- 31-byte scan-response packet -----------------------------+
|
||||
* | <up to BLE_UART_SCAN_RSP_DATA_MAX = 31 bytes from your buf> |
|
||||
* +-------------------------------------------------------------+
|
||||
*/
|
||||
```
|
||||
|
||||
`adv_data_len` is checked at install time; oversized buffers fail with
|
||||
`BLE_UART_EINVAL`. Both buffers are copied into module-private storage,
|
||||
so the caller's pointers do not need to outlive the call.
|
||||
|
||||
**Format**: a sequence of standard BT Core "AD structure" triplets —
|
||||
`[length(1)] [AD type(1)] [value(length-1)]`. AD-type values are
|
||||
defined in the *Bluetooth Assigned Numbers* document
|
||||
([Generic Access Profile, §1](https://www.bluetooth.com/specifications/assigned-numbers/)).
|
||||
Common ones:
|
||||
|
||||
| Type | Name | Value format |
|
||||
| --- | --- | --- |
|
||||
| `0x09` | Complete Local Name | UTF-8 bytes |
|
||||
| `0x08` | Shortened Local Name | UTF-8 bytes |
|
||||
| `0x0A` | TX Power Level | 1 signed byte (dBm) |
|
||||
| `0x07` | Complete List of 128-bit Service UUIDs | 16 bytes per UUID |
|
||||
| `0xFF` | Manufacturer Specific Data | 2-byte company ID + payload |
|
||||
|
||||
**Example — replace the default with name + UUID + 4 bytes of vendor data**
|
||||
|
||||
```c
|
||||
static const uint8_t adv_payload[] = {
|
||||
/* Complete Local Name "MyDev" (1 + 1 + 5 = 7 bytes) */
|
||||
0x06, 0x09, 'M', 'y', 'D', 'e', 'v',
|
||||
|
||||
/* Complete 128-bit Service UUID — bytes are little-endian on air,
|
||||
* matching ble_uart_service_uuid.bytes[]. (1 + 1 + 16 = 18 bytes) */
|
||||
0x11, 0x07,
|
||||
0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0,
|
||||
0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e,
|
||||
/* total = 7 + 18 = 25 bytes (≤ BLE_UART_ADV_DATA_MAX = 28) */
|
||||
};
|
||||
|
||||
static const uint8_t scan_rsp_payload[] = {
|
||||
/* Manufacturer Specific Data: Espressif Systems (0x02E5) + 4 bytes */
|
||||
0x07, 0xFF, 0xE5, 0x02, 0xDE, 0xAD, 0xBE, 0xEF,
|
||||
};
|
||||
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.device_name = "MyDev", /* GAP service value, peer-readable */
|
||||
.adv_data = adv_payload,
|
||||
.adv_data_len = sizeof(adv_payload),
|
||||
.scan_rsp_data = scan_rsp_payload,
|
||||
.scan_rsp_data_len = sizeof(scan_rsp_payload),
|
||||
.ble_uart_on_rx = on_rx,
|
||||
.on_event = on_event,
|
||||
});
|
||||
```
|
||||
|
||||
**Notes**:
|
||||
|
||||
- `device_name` and `adv_data` are independent. The first is the GAP
|
||||
service value that any connected peer can read over GATT; the second
|
||||
is what scanners see before connecting. If you want the device name
|
||||
visible during a scan, include a Complete-Local-Name AD element
|
||||
(`0x09`) in `adv_data` yourself — providing custom `adv_data`
|
||||
disables the auto-include path.
|
||||
- The 31-byte packet limit is BLE 4.x legacy advertising. Extended
|
||||
advertising (BLE 5.0) is **not** wired through this API — both
|
||||
backends fall back to legacy advertising for portability.
|
||||
- Set only one half if you want the other to keep its default — e.g.
|
||||
custom `adv_data` with `scan_rsp_data = NULL` keeps the default
|
||||
service-UUID scan response.
|
||||
|
||||
---
|
||||
|
||||
@@ -467,8 +1164,12 @@ Effect:
|
||||
|
||||
`ble_uart` registers its own service; you can call `ble_gatts_add_svcs()`
|
||||
**multiple times** and NimBLE will build all of them into the GATT
|
||||
table. **Caveat**: this must happen before `ble_uart_open()`, otherwise
|
||||
the host task is already running and the GATT table is locked.
|
||||
table. **Caveat**: this must happen before the **first** `ble_uart_open()`
|
||||
for that `install()` cycle, otherwise the host task is already running
|
||||
and the GATT table is locked. If you use `ble_uart_close()` and later
|
||||
`ble_uart_open()` without `uninstall()`, you must call your extra
|
||||
`ble_svc_*_init()` / `ble_gatts_add_svcs()` again before that second
|
||||
`open()` — see §5.3.1a.
|
||||
|
||||
```c
|
||||
ble_uart_install(&cfg);
|
||||
@@ -488,15 +1189,20 @@ ble_uart_open();
|
||||
> call `ble_gap_adv_start` yourself. In that case, just fork
|
||||
> `ble_uart_nimble.c` (or the matching `ble_uart_bluedroid.c`).
|
||||
|
||||
### 6.4 Configuring the device-name prefix via Kconfig
|
||||
### 6.4 Configuring the device name via Kconfig
|
||||
|
||||
If you use the shared `ble_uart` component, options are already in
|
||||
`menuconfig → Component configuration → ESP-BLE-UART library`. If you copied only
|
||||
the `.c` / `.h` files into `main/`, copy `Kconfig` from `common/ble_uart/` as
|
||||
well (or merge its symbols into your own `Kconfig.projbuild`), then:
|
||||
|
||||
The bundled example builds a per-unit name as `<prefix>-XXXX` where
|
||||
`XXXX` is the last two BT MAC bytes in hex:
|
||||
|
||||
```c
|
||||
char name[24];
|
||||
uint8_t mac[6] = {0};
|
||||
esp_read_mac(mac, ESP_MAC_BT);
|
||||
char name[BLE_UART_DEVICE_NAME_MAX + 1];
|
||||
snprintf(name, sizeof(name), "%s-%02X%02X",
|
||||
CONFIG_BLE_UART_DEVICE_NAME_PREFIX, mac[4], mac[5]);
|
||||
|
||||
@@ -507,8 +1213,11 @@ ble_uart_install(&(ble_uart_config_t){
|
||||
});
|
||||
```
|
||||
|
||||
Edit the default through `menuconfig → Component configuration → ESP-BLE-UART
|
||||
library → BLE device name prefix`.
|
||||
Edit the prefix through `menuconfig → Component configuration →
|
||||
ESP-BLE-UART library → BLE device name prefix`.
|
||||
|
||||
For a fixed name on every unit, skip the MAC suffix and pass any
|
||||
string ≤ `BLE_UART_DEVICE_NAME_MAX` directly to `device_name`.
|
||||
|
||||
### 6.5 Pushing data proactively
|
||||
|
||||
@@ -531,7 +1240,7 @@ static void sensor_task(void *arg)
|
||||
xTaskCreate(sensor_task, "sensor", 3072, NULL, 5, NULL);
|
||||
```
|
||||
|
||||
When nobody is subscribed, `ble_uart_tx` returns `BLE_HS_ENOTCONN` —
|
||||
When nobody is subscribed, `ble_uart_tx` returns `BLE_UART_ENOTCONN` —
|
||||
**just ignore it**.
|
||||
|
||||
---
|
||||
@@ -543,10 +1252,11 @@ When nobody is subscribed, `ble_uart_tx` returns `BLE_HS_ENOTCONN` —
|
||||
| `ble_uart_install` | Any task; once per uninstall cycle | One-shot until `uninstall` |
|
||||
| `ble_uart_open` | Any task; after `install` | One-shot until `close` |
|
||||
| `ble_uart_close` | Any task **except the BLE host task** (NimBLE host task / Bluedroid BTC task) | Idempotent; second call returns `EALREADY` |
|
||||
| `ble_uart_uninstall` | Any task **except the BLE host task** | Idempotent; cascades into `close` if needed |
|
||||
| `ble_uart_close_async` | Any task — including the BLE host task (use this from inside `on_rx` / `on_event`) | Idempotent; second call while a worker is draining returns `EALREADY` |
|
||||
| `ble_uart_uninstall` | Any task **except the BLE host task** | Idempotent; see §5.3 release paths; polls in-flight `close_async` ≤~5 s. Best-effort teardown: returns the **first** `BLE_UART_*` failure (`ble_uart_close` or translated `esp_err_t`) but always wipes module state so a retry is possible. |
|
||||
| `ble_uart_tx` | Any FreeRTOS task | Yes — multi-task concurrent |
|
||||
| `ble_uart_is_connected` / `is_subscribed` | Any context | Yes (bool read; best-effort snapshot) |
|
||||
| `ble_uart_on_rx` callback | BLE host task (NimBLE host task / Bluedroid BTC task) | Your code must not block, **must not call `close` / `uninstall`** |
|
||||
| `ble_uart_on_rx` / `on_event` callback | BLE host task (NimBLE host task / Bluedroid BTC task); **`BLE_UART_EVT_CLOSED` is the lone exception — fires on the close-async worker task** | Your code must not block, **must not call `close` / `uninstall`** — use `ble_uart_close_async()` instead |
|
||||
| **Calling any `ble_uart` API from an ISR** | not allowed | Neither host stack supports it |
|
||||
|
||||
---
|
||||
@@ -575,7 +1285,7 @@ Measured throughput (ESP32-S3, iPhone 14 Pro central, MTU 247):
|
||||
| --- | --- |
|
||||
| `nimble_port_init rc=...` | NVS not initialised, or BT controller not enabled |
|
||||
| Compile error: `host/ble_hs.h` not found | `REQUIRES bt` is missing from CMakeLists |
|
||||
| Device not discoverable | Device name exceeds the advertising packet limit (drop the tx_pwr field or shorten the name) |
|
||||
| `ble_uart_install()` returns `BLE_UART_EINVAL` | Buffer too long (`device_name` / `adv_data` / `scan_rsp` limits in §5.9), impossible security (`mitm=ON` + `io_cap=NO_INPUT_OUTPUT`), **`io_cap` in `{KEYBOARD_ONLY, DISPLAY_YES_NO, KEYBOARD_DISPLAY}` with `on_event=NULL`** (note: default `AUTO` + `encrypted=true` and explicit `DISPLAY_ONLY` **do** allow `on_event=NULL`), or out-of-range `sc`/`bonding`/`mitm`/`io_cap`. See §5.6. |
|
||||
| Pairing fails | Central uses "Just Works" but we require MITM (`encrypted=true`). Use a central that supports passkey entry |
|
||||
| `enc_change status=13 encrypted=1 bonded=1` | `13 = BLE_HS_ETIMEOUT`. Bonded-reconnect race; **the link is actually encrypted — safe to ignore** |
|
||||
| Notifications missing after a reconnect | Bonded centrals often skip the CCCD write; our TX path doesn't gate on subscription state, so notifications still go out — make sure the central side has its callback registered |
|
||||
@@ -613,19 +1323,42 @@ If you **start from an empty project**:
|
||||
#include "ble_uart.h"
|
||||
|
||||
/* === Types === */
|
||||
typedef void (*ble_uart_rx_cb_t)(const uint8_t *data, size_t len);
|
||||
typedef void (*ble_uart_rx_cb_t) (const uint8_t *data, size_t len);
|
||||
typedef void (*ble_uart_evt_cb_t)(const ble_uart_evt_t *evt);
|
||||
|
||||
typedef struct {
|
||||
bool encrypted;
|
||||
const char *device_name;
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;
|
||||
ble_uart_sec_t sc; /* AUTO / OFF / ON — follow `encrypted` when AUTO */
|
||||
ble_uart_sec_t bonding;
|
||||
ble_uart_sec_t mitm;
|
||||
ble_uart_io_cap_t io_cap; /* AUTO / NO_INPUT_OUTPUT / DISPLAY_ONLY /
|
||||
KEYBOARD_ONLY / DISPLAY_YES_NO /
|
||||
KEYBOARD_DISPLAY */
|
||||
} ble_uart_security_t;
|
||||
|
||||
typedef struct {
|
||||
bool encrypted; /* preset: SC + Bonding + MITM + DisplayOnly */
|
||||
ble_uart_security_t security; /* per-feature overrides; see §5.6 */
|
||||
|
||||
const char *device_name; /* ≤ BLE_UART_DEVICE_NAME_MAX (26) */
|
||||
/* Custom adv payloads (NULL → defaults).
|
||||
* Limits: adv_data_len ≤ BLE_UART_ADV_DATA_MAX (28),
|
||||
* scan_rsp_data_len ≤ BLE_UART_SCAN_RSP_DATA_MAX (31). */
|
||||
const uint8_t *adv_data;
|
||||
size_t adv_data_len;
|
||||
const uint8_t *scan_rsp_data;
|
||||
size_t scan_rsp_data_len;
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;
|
||||
ble_uart_evt_cb_t on_event; /* optional for default Passkey Display;
|
||||
required for KEYBOARD_ONLY /
|
||||
DISPLAY_YES_NO / KEYBOARD_DISPLAY */
|
||||
} ble_uart_config_t;
|
||||
|
||||
/* === Lifecycle === */
|
||||
int ble_uart_install(const ble_uart_config_t *cfg); /* host + GATT */
|
||||
int ble_uart_open(void); /* start advertising (NimBLE: spawn host task) */
|
||||
int ble_uart_close(void); /* stop adv / disconnect / quiesce host */
|
||||
int ble_uart_uninstall(void); /* tear down host + reset state */
|
||||
int ble_uart_close_async(void); /* same, fire-and-forget; signals BLE_UART_EVT_CLOSED on completion */
|
||||
int ble_uart_uninstall(void); /* best-effort teardown; first error, state always cleared */
|
||||
|
||||
/* === Send (callable from any task) === */
|
||||
int ble_uart_tx(const uint8_t *data, size_t len);
|
||||
@@ -634,10 +1367,25 @@ int ble_uart_tx(const uint8_t *data, size_t len);
|
||||
/* Via the cfg.ble_uart_on_rx callback, signature:
|
||||
* void cb(const uint8_t *data, size_t len); */
|
||||
|
||||
/* === Pairing replies (PASSKEY_REQUEST / NUMERIC_COMPARE only) === */
|
||||
int ble_uart_passkey_reply(uint32_t passkey); /* answer PASSKEY_REQUEST */
|
||||
int ble_uart_compare_reply(bool match); /* answer NUMERIC_COMPARE */
|
||||
|
||||
/* === Status === */
|
||||
bool ble_uart_is_connected(void);
|
||||
bool ble_uart_is_subscribed(void);
|
||||
|
||||
/* === Bond management (works after install) === */
|
||||
typedef struct {
|
||||
uint8_t bytes[6]; /* big-endian: bytes[0] is MSB */
|
||||
uint8_t type; /* BLE_UART_ADDR_TYPE_PUBLIC or _RANDOM */
|
||||
} ble_uart_addr_t;
|
||||
|
||||
int ble_uart_get_bond_count(size_t *out_count);
|
||||
int ble_uart_get_bonded_peers(ble_uart_addr_t *out, size_t cap, size_t *out_count);
|
||||
int ble_uart_remove_peer(const ble_uart_addr_t *peer);
|
||||
int ble_uart_clear_bonds(void);
|
||||
|
||||
/* === Service UUID (for advertising; usually no need to touch) === */
|
||||
extern const ble_uart_uuid128_t ble_uart_service_uuid;
|
||||
```
|
||||
|
||||
@@ -10,16 +10,24 @@
|
||||
* backend is picked at compile time via CONFIG_BT_NIMBLE_ENABLED /
|
||||
* CONFIG_BT_BLUEDROID_ENABLED.
|
||||
*
|
||||
* Lifecycle:
|
||||
* Lifecycle — bring-up:
|
||||
*
|
||||
* ble_uart_install(&cfg); // host + GATT service
|
||||
* ble_uart_open(); // start advertising + auto-encrypt
|
||||
* ...
|
||||
* nvs_flash_init();
|
||||
* ble_uart_install(&cfg); // host + GATT service (once per uninstall)
|
||||
* ble_uart_open(); // advertising + pairing
|
||||
*
|
||||
* Run-forever apps stop after open().
|
||||
*
|
||||
* Lifecycle — release (pick one path; see PORTING.md §5.3):
|
||||
*
|
||||
* Path A — from a normal app task (not on_event / on_rx):
|
||||
* ble_uart_close(); // stop adv / disconnect / halt host
|
||||
* ble_uart_uninstall(); // free port + reset state
|
||||
*
|
||||
* Run-forever apps only need install + open. close / uninstall is
|
||||
* for apps that need to power BLE off at runtime.
|
||||
* Path B — teardown triggered by a BLE event on the host task:
|
||||
* ble_uart_close_async(); // in on_event / on_rx only
|
||||
* // wait for BLE_UART_EVT_CLOSED (.closed.status == BLE_UART_OK)
|
||||
* ble_uart_uninstall(); // on an app task — NOT inside CLOSED
|
||||
*
|
||||
* GATT layout (UUIDs are the widely used fixed 128-bit values):
|
||||
*
|
||||
@@ -57,6 +65,23 @@ typedef struct {
|
||||
uint8_t bytes[16];
|
||||
} ble_uart_uuid128_t;
|
||||
|
||||
/* ----- BLE address ---------------------------------------------------- */
|
||||
|
||||
/** Address type, mirroring the BT Core spec values. */
|
||||
#define BLE_UART_ADDR_TYPE_PUBLIC 0
|
||||
#define BLE_UART_ADDR_TYPE_RANDOM 1
|
||||
|
||||
/** 6-octet BLE device address.
|
||||
*
|
||||
* `bytes` is in big-endian order — `bytes[0]` is the MSB octet, the
|
||||
* way addresses are usually printed (`AA:BB:CC:DD:EE:FF`). Both
|
||||
* backends marshal between this representation and their own native
|
||||
* byte order internally, so callers never need to flip bytes. */
|
||||
typedef struct {
|
||||
uint8_t bytes[6];
|
||||
uint8_t type; /* BLE_UART_ADDR_TYPE_PUBLIC or _RANDOM */
|
||||
} ble_uart_addr_t;
|
||||
|
||||
/* ----- Configuration -------------------------------------------------- */
|
||||
|
||||
/** RX byte callback. Invoked from the BLE host task whenever bytes
|
||||
@@ -71,27 +96,427 @@ typedef struct {
|
||||
* are rejected with ATT error 0x0d. */
|
||||
typedef void (*ble_uart_rx_cb_t)(const uint8_t *data, size_t len);
|
||||
|
||||
/* ----- Event callback ------------------------------------------------- */
|
||||
|
||||
/** Lifecycle / link-state events delivered to ble_uart_config_t::on_event.
|
||||
*
|
||||
* All events fire from the BLE host task context (NimBLE host task /
|
||||
* Bluedroid BTC task), with one documented exception:
|
||||
* BLE_UART_EVT_CLOSED is fired by the close-async worker task, after
|
||||
* the host stack has been torn down — there is no host task left to
|
||||
* deliver it from. See ble_uart_close_async().
|
||||
*
|
||||
* The same threading rules as ble_uart_on_rx apply: don't block, and
|
||||
* don't call ble_uart_close() / ble_uart_uninstall() (use the async
|
||||
* variant if you need to teardown from inside an event handler). */
|
||||
typedef enum {
|
||||
/** Physical link established. Payload: .connected.peer.
|
||||
* Type is always BLE_UART_ADDR_TYPE_PUBLIC or _RANDOM (each
|
||||
* backend's wider addr-type enum is collapsed before delivery).
|
||||
*
|
||||
* Backend semantics differ:
|
||||
* - NimBLE: peer identity address (`peer_id_addr`). On first
|
||||
* connect this equals the over-the-air address; on a bonded
|
||||
* RPA reconnect it is the resolved identity, not the random
|
||||
* address currently on the wire.
|
||||
* - Bluedroid: the BD address recorded at bond time. If the
|
||||
* peer connected as address_A and bonding succeeded, later
|
||||
* reconnects still report address_A in CONNECT even when the
|
||||
* peer's over-the-air address has changed (e.g. a new RPA).
|
||||
* Matches `get_bonded_peers()` / `remove_peer` (`bytes` only). */
|
||||
BLE_UART_EVT_CONNECTED,
|
||||
|
||||
/** Physical link torn down. Payload: .disconnected.reason
|
||||
* (stack-specific disconnect code — esp_gatt_conn_reason_t on
|
||||
* Bluedroid, NimBLE BLE host return code on NimBLE; see
|
||||
* BLE_HS_HCI_ERR() / BLE_HS_ERR_HCI_BASE for HCI encoding). */
|
||||
BLE_UART_EVT_DISCONNECTED,
|
||||
|
||||
/** CCCD on the TX characteristic changed. Payload:
|
||||
* .subscribed.subscribed (true = notifications enabled). */
|
||||
BLE_UART_EVT_SUBSCRIBED,
|
||||
|
||||
/** Link reached the encrypted+authenticated state requested at
|
||||
* install time. Payload: .link_secure.{encrypted, authenticated,
|
||||
* bonded, key_size}. Use this — not is_connected() — to gate any
|
||||
* application logic that requires the channel to be secure. */
|
||||
BLE_UART_EVT_LINK_SECURE,
|
||||
|
||||
/** SM asks the application to display a 6-digit passkey.
|
||||
* Payload: .passkey.passkey (0..999999). The default banner on
|
||||
* UART still prints; this callback is additive so a UI / test
|
||||
* harness can avoid scraping logs. */
|
||||
BLE_UART_EVT_PASSKEY_DISPLAY,
|
||||
|
||||
/** SM asks the application to collect a 6-digit passkey from the
|
||||
* user (the central displays it; the user types it into this
|
||||
* device). No payload.
|
||||
*
|
||||
* The application MUST respond by calling ble_uart_passkey_reply()
|
||||
* with the 6 digits the user entered. Until the reply arrives —
|
||||
* or until the SM's pairing timeout fires (the controller's
|
||||
* default ~30 s) — pairing is suspended; on timeout the link
|
||||
* surfaces BLE_UART_EVT_PAIRING_FAILED.
|
||||
*
|
||||
* Only fires when cfg.security.io_cap is one of the input-capable
|
||||
* values (KEYBOARD_ONLY / KEYBOARD_DISPLAY) and the central asks
|
||||
* for Passkey Entry. */
|
||||
BLE_UART_EVT_PASSKEY_REQUEST,
|
||||
|
||||
/** SM asks the application to display a 6-digit value and let the
|
||||
* user confirm whether the same value appears on the central.
|
||||
* Payload: .numeric_compare.passkey (0..999999).
|
||||
*
|
||||
* The application MUST respond by calling ble_uart_compare_reply()
|
||||
* with the user's verdict (true = match). Same suspend-and-time-
|
||||
* out semantics as BLE_UART_EVT_PASSKEY_REQUEST.
|
||||
*
|
||||
* Only fires when cfg.security.io_cap is one of the
|
||||
* comparison-capable values (DISPLAY_YES_NO / KEYBOARD_DISPLAY)
|
||||
* and the central asks for Numeric Comparison (which itself
|
||||
* requires LE Secure Connections on both sides). */
|
||||
BLE_UART_EVT_NUMERIC_COMPARE,
|
||||
|
||||
/** Pairing failed or was rejected. Payload: .pairing_failed.reason
|
||||
* (NimBLE BLE_HS_E* / Bluedroid esp_ble_auth_fail_rsn_t). */
|
||||
BLE_UART_EVT_PAIRING_FAILED,
|
||||
|
||||
/** Async-close completion — fired only by ble_uart_close_async()
|
||||
* after the worker task has finished the equivalent of a
|
||||
* synchronous ble_uart_close(). Payload: .closed.status — the
|
||||
* return code from that close (BLE_UART_OK on success).
|
||||
*
|
||||
* When .closed.status is BLE_UART_OK the host stack is fully
|
||||
* quiesced — same state as right after ble_uart_close() returns.
|
||||
* Defer ble_uart_uninstall() to a normal app task (set a flag /
|
||||
* queue here); do not call uninstall from this handler — see
|
||||
* PORTING.md §5.3.2. On failure (e.g. BLE_UART_EFAIL) the port
|
||||
* may still be open; retry ble_uart_close() / ble_uart_close_async()
|
||||
* from an app task.
|
||||
*
|
||||
* Unlike every other event in this enum, this one runs on the
|
||||
* close-async worker task, NOT on the BLE host task — by the
|
||||
* time it fires the host task is already gone. Keep the handler
|
||||
* short; the worker clears s_closing after it returns. */
|
||||
BLE_UART_EVT_CLOSED,
|
||||
} ble_uart_evt_id_t;
|
||||
|
||||
/** Tagged union delivered to ble_uart_config_t::on_event. */
|
||||
typedef struct {
|
||||
ble_uart_evt_id_t id;
|
||||
union {
|
||||
struct {
|
||||
ble_uart_addr_t peer;
|
||||
} connected;
|
||||
|
||||
struct {
|
||||
int reason; /* stack-specific disconnect code */
|
||||
} disconnected;
|
||||
|
||||
struct {
|
||||
bool subscribed;
|
||||
} subscribed;
|
||||
|
||||
struct {
|
||||
bool encrypted; /* 1 = link is AES-CCM encrypted */
|
||||
bool authenticated; /* 1 = pairing used MITM protection */
|
||||
bool bonded; /* 1 = LTK persisted in NVS */
|
||||
uint8_t key_size; /* 7..16 (octets) */
|
||||
} link_secure;
|
||||
|
||||
struct {
|
||||
uint32_t passkey; /* 0..999999 */
|
||||
} passkey;
|
||||
|
||||
struct {
|
||||
uint32_t passkey; /* 0..999999 — the value to display */
|
||||
} numeric_compare;
|
||||
|
||||
struct {
|
||||
int reason; /* stack-specific status code */
|
||||
} pairing_failed;
|
||||
|
||||
struct {
|
||||
int status; /* BLE_UART_* from async close worker */
|
||||
} closed;
|
||||
};
|
||||
} ble_uart_evt_t;
|
||||
|
||||
/** Event callback. May be NULL — events are silently dropped then. */
|
||||
typedef void (*ble_uart_evt_cb_t)(const ble_uart_evt_t *evt);
|
||||
|
||||
/* ----- Security configuration ---------------------------------------- */
|
||||
|
||||
/** Tri-state knob for the per-feature security overrides in
|
||||
* ble_uart_config_t (`sc`, `bonding`, `mitm`).
|
||||
*
|
||||
* AUTO (= 0, the value of a zero-initialised struct member) means
|
||||
* "use whatever cfg.encrypted implies":
|
||||
*
|
||||
* encrypted = true → AUTO behaves as ON
|
||||
* encrypted = false → AUTO behaves as OFF
|
||||
*
|
||||
* OFF / ON force the bit regardless of the preset, letting the
|
||||
* caller mix the preset with one or two surgical overrides without
|
||||
* spelling out every other field. */
|
||||
typedef enum {
|
||||
BLE_UART_SEC_AUTO = 0,
|
||||
BLE_UART_SEC_OFF = 1,
|
||||
BLE_UART_SEC_ON = 2,
|
||||
} ble_uart_sec_t;
|
||||
|
||||
/** SM Input/Output capability — combines with the central's IO cap and
|
||||
* the resolved `mitm` bit to pick the pairing model (Just Works /
|
||||
* Passkey Display / Passkey Entry / Numeric Comparison — see BT Core
|
||||
* Spec §2.3.5.1). The application doesn't decide the method directly;
|
||||
* it picks the IO cap that matches its UI and ble_uart fires the right
|
||||
* event when the SM negotiates a method.
|
||||
*
|
||||
* Passing an out-of-range integer makes ble_uart_install() return
|
||||
* BLE_UART_EINVAL. Only the input-capable values (KEYBOARD_ONLY,
|
||||
* DISPLAY_YES_NO, KEYBOARD_DISPLAY) require cfg.on_event to be
|
||||
* non-NULL — pairing would otherwise stall on unanswered
|
||||
* BLE_UART_EVT_PASSKEY_REQUEST / NUMERIC_COMPARE. AUTO (resolves to
|
||||
* DisplayOnly when MITM is ON), DISPLAY_ONLY, and NO_INPUT_OUTPUT do
|
||||
* not require on_event; Passkey Display is handled internally. */
|
||||
typedef enum {
|
||||
/** Default: DisplayOnly when the resolved MITM bit is ON;
|
||||
* NoInputNoOutput when it is OFF. */
|
||||
BLE_UART_IO_CAP_AUTO = 0,
|
||||
|
||||
/** Device has no UI; pairing always uses Just Works. Cannot
|
||||
* satisfy MITM — combining this with mitm=ON makes
|
||||
* ble_uart_install() return BLE_UART_EINVAL. */
|
||||
BLE_UART_IO_CAP_NO_INPUT_OUTPUT = 1,
|
||||
|
||||
/** Device shows a 6-digit passkey on a display; the central
|
||||
* enters it. Generates a fresh passkey for every pairing,
|
||||
* surfaced via BLE_UART_EVT_PASSKEY_DISPLAY (no reply call
|
||||
* needed — the central does the typing). */
|
||||
BLE_UART_IO_CAP_DISPLAY_ONLY = 2,
|
||||
|
||||
/** Device has keys (or some other way to feed digits to the
|
||||
* library) but no display; the central displays a 6-digit
|
||||
* passkey, the user reads it from there and types it in.
|
||||
*
|
||||
* ble_uart fires BLE_UART_EVT_PASSKEY_REQUEST and waits for
|
||||
* ble_uart_passkey_reply(). Requires cfg.on_event != NULL. */
|
||||
BLE_UART_IO_CAP_KEYBOARD_ONLY = 3,
|
||||
|
||||
/** Device has a display + a yes/no confirmation control. With a
|
||||
* similarly-equipped LE Secure Connections central this elects
|
||||
* Numeric Comparison: ble_uart fires BLE_UART_EVT_NUMERIC_COMPARE
|
||||
* with the 6-digit value to display, and waits for
|
||||
* ble_uart_compare_reply().
|
||||
*
|
||||
* Falls back to Just Works against legacy or NoInput peers.
|
||||
* Requires cfg.on_event != NULL. */
|
||||
BLE_UART_IO_CAP_DISPLAY_YES_NO = 4,
|
||||
|
||||
/** Device has a display AND a keypad (covers both Numeric
|
||||
* Comparison and Passkey Entry). Best fit for a touchscreen UI
|
||||
* that wants to handle every MITM-capable peer.
|
||||
*
|
||||
* ble_uart fires either BLE_UART_EVT_PASSKEY_REQUEST or
|
||||
* BLE_UART_EVT_NUMERIC_COMPARE depending on what the SM
|
||||
* negotiates with the central; respond with the matching reply
|
||||
* API. Requires cfg.on_event != NULL. */
|
||||
BLE_UART_IO_CAP_KEYBOARD_DISPLAY = 5,
|
||||
} ble_uart_io_cap_t;
|
||||
|
||||
/** Per-feature security overrides, embedded in ble_uart_config_t.
|
||||
*
|
||||
* Each tri-state field defaults to AUTO (= 0, the value of any
|
||||
* zero-initialised member), inheriting its bit from
|
||||
* ble_uart_config_t::encrypted:
|
||||
*
|
||||
* encrypted = true → AUTO behaves as ON
|
||||
* encrypted = false → AUTO behaves as OFF
|
||||
*
|
||||
* Set any field to OFF / ON to override that single bit while the
|
||||
* rest still follow the preset. Common patterns are listed in
|
||||
* PORTING.md §5.6 (e.g. encrypted=true with mitm=OFF +
|
||||
* io_cap=NO_INPUT_OUTPUT for a displayless gateway).
|
||||
*
|
||||
* Combinations the SM cannot satisfy — io_cap=NO_INPUT_OUTPUT
|
||||
* together with the resolved mitm=ON, or an out-of-range enum value
|
||||
* — make ble_uart_install() return BLE_UART_EINVAL up front, before
|
||||
* the host stack is brought up. */
|
||||
typedef struct {
|
||||
/** Override LE Secure Connections (the BT 4.2+ pairing method
|
||||
* that uses ECDH for the LTK). */
|
||||
ble_uart_sec_t sc;
|
||||
|
||||
/** Override bonding (persistence of the LTK / IRK / persisted
|
||||
* CCCD in NVS). With bonding=OFF the link is still encrypted
|
||||
* (if sc/mitm are on) but every reconnect re-pairs. */
|
||||
ble_uart_sec_t bonding;
|
||||
|
||||
/** Override MITM protection (man-in-the-middle: link
|
||||
* authentication via passkey display / entry / numeric
|
||||
* comparison). With mitm=OFF the link pairs via Just Works,
|
||||
* which is encrypted but unauthenticated; the GATT permission
|
||||
* flags drop their _AUTHEN bit so a Just-Works peer can
|
||||
* read/write the UART characteristics. */
|
||||
ble_uart_sec_t mitm;
|
||||
|
||||
/** SM IO capability — controls which pairing model is chosen
|
||||
* alongside `mitm`. AUTO picks DisplayOnly when the resolved
|
||||
* MITM bit is ON, NoInputNoOutput when it is OFF. */
|
||||
ble_uart_io_cap_t io_cap;
|
||||
} ble_uart_security_t;
|
||||
|
||||
/* ----- Advertising payload limits ------------------------------------ */
|
||||
|
||||
/** Maximum bytes the application may put in `adv_data`.
|
||||
*
|
||||
* BLE 4.x legacy primary advertising packets are capped at 31 bytes
|
||||
* total. Of those, the 3-byte Flags AD element (length+type+value)
|
||||
* is always added by ble_uart, leaving 31 − 3 = 28 bytes for the
|
||||
* application. */
|
||||
#define BLE_UART_ADV_DATA_MAX 28
|
||||
|
||||
/** Maximum bytes the application may put in `scan_rsp_data`.
|
||||
*
|
||||
* Scan response packets are also capped at 31 bytes, with no
|
||||
* mandatory AD elements — the entire 31 bytes belong to the
|
||||
* application. */
|
||||
#define BLE_UART_SCAN_RSP_DATA_MAX 31
|
||||
|
||||
/** Maximum length (bytes, excluding NUL terminator) of `device_name`.
|
||||
*
|
||||
* Sized so that the *default* advertising payload — Flags AD +
|
||||
* Complete Local Name AD — always fits in the 31-byte primary packet:
|
||||
*
|
||||
* 31 − 3 (Flags AD) − 2 (Name AD header) = 26
|
||||
*
|
||||
* Names that exceed this length make `ble_uart_install()` return
|
||||
* `BLE_UART_EINVAL` synchronously, instead of silently failing later
|
||||
* in the host stack when advertising starts.
|
||||
*
|
||||
* This applies regardless of whether `adv_data` is set — the GAP
|
||||
* service Device Name characteristic (UUID 0x2A00) reports the same
|
||||
* string. Apps that need a longer GAP-service name with a shorter
|
||||
* advertised name should keep `device_name` ≤ this limit and use
|
||||
* `adv_data` to broadcast a shortened/different name instead. */
|
||||
#define BLE_UART_DEVICE_NAME_MAX 26
|
||||
|
||||
/** Configuration handed to ble_uart_install(). */
|
||||
typedef struct {
|
||||
/** True = LE Secure Connections + Bonding + MITM, DisplayOnly IO,
|
||||
* encrypted RX/TX chars, bond persisted in NVS (NimBLE: requires
|
||||
* CONFIG_BT_NIMBLE_NVS_PERSIST=y; Bluedroid: default).
|
||||
* False = plaintext (lab debugging only — sniffable). */
|
||||
/** Security preset (a one-line shortcut for the four override
|
||||
* fields under `security` below).
|
||||
*
|
||||
* True = LE Secure Connections + Bonding + MITM, DisplayOnly IO,
|
||||
* encrypted+authenticated RX/TX chars, bond persisted in
|
||||
* NVS (NimBLE: requires CONFIG_BT_NIMBLE_NVS_PERSIST=y;
|
||||
* Bluedroid: default).
|
||||
* False = plaintext (lab debugging only — sniffable).
|
||||
*
|
||||
* Every member of `security` defaults to AUTO, meaning "follow
|
||||
* this preset". Override individual bits there; see
|
||||
* ble_uart_security_t for the resolution rules. */
|
||||
bool encrypted;
|
||||
|
||||
/** GAP device name. NULL keeps the host stack default. Mind the
|
||||
* 31-byte primary advertising limit (≤ 8 bytes recommended). */
|
||||
/** Per-feature security overrides. A zero-initialised value
|
||||
* (every field AUTO) inherits everything from `encrypted`, so
|
||||
* callers that just want the secure-by-default preset can leave
|
||||
* this field unset:
|
||||
*
|
||||
* ble_uart_install(&(ble_uart_config_t){
|
||||
* .encrypted = true, // sc/bonding/mitm/io_cap all AUTO
|
||||
* ...
|
||||
* });
|
||||
*
|
||||
* Surgical override:
|
||||
*
|
||||
* ble_uart_install(&(ble_uart_config_t){
|
||||
* .encrypted = true,
|
||||
* .security = { .mitm = BLE_UART_SEC_OFF }, // SC + Bonding, no MITM
|
||||
* ...
|
||||
* });
|
||||
*
|
||||
* See ble_uart_security_t for the full per-field docs. */
|
||||
ble_uart_security_t security;
|
||||
|
||||
/** GAP device name (peer-readable via the GAP service, UUID 0x2A00).
|
||||
* NULL keeps the host-stack default.
|
||||
*
|
||||
* Length must be ≤ BLE_UART_DEVICE_NAME_MAX (26) — over-long
|
||||
* strings make ble_uart_install() return BLE_UART_EINVAL.
|
||||
*
|
||||
* This is NOT automatically inserted into the advertising payload
|
||||
* when `adv_data` (below) is non-NULL — if you want the name to
|
||||
* appear in scans without connecting, include a Complete Local
|
||||
* Name AD element (type 0x09) in your `adv_data` bytes yourself. */
|
||||
const char *device_name;
|
||||
|
||||
/** Optional raw advertising data — everything that goes after the
|
||||
* 3-byte Flags AD element in the primary advertising packet. The
|
||||
* Flags element is built by ble_uart and is NOT part of these
|
||||
* bytes (don't include it).
|
||||
*
|
||||
* Format: standard BT Core "AD structure" sequence — repeating
|
||||
* `[length(1)][AD type(1)][value(length-1)]` triplets. See the
|
||||
* Bluetooth Assigned Numbers (Generic Access Profile) document
|
||||
* for the full type list.
|
||||
*
|
||||
* Length must be ≤ BLE_UART_ADV_DATA_MAX (28). The buffer is
|
||||
* copied at install time; the pointer does not need to outlive
|
||||
* the call.
|
||||
*
|
||||
* Set to NULL (with adv_data_len=0) to keep the built-in default,
|
||||
* which advertises only the Complete Local Name (taken from
|
||||
* device_name). */
|
||||
const uint8_t *adv_data;
|
||||
size_t adv_data_len;
|
||||
|
||||
/** Optional raw scan response data — entire 31-byte payload is at
|
||||
* the application's disposal; ble_uart adds nothing.
|
||||
*
|
||||
* Same `[len][type][value]` format and copy semantics as
|
||||
* adv_data. Length must be ≤ BLE_UART_SCAN_RSP_DATA_MAX (31).
|
||||
*
|
||||
* Set to NULL (with scan_rsp_data_len=0) to keep the built-in
|
||||
* default, which advertises the 128-bit BLE UART service UUID. */
|
||||
const uint8_t *scan_rsp_data;
|
||||
size_t scan_rsp_data_len;
|
||||
|
||||
/** Byte handler for RX writes. NULL discards incoming data. */
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;
|
||||
|
||||
/** Lifecycle / link-state event sink. NULL drops every event.
|
||||
* See ble_uart_evt_id_t for the supported events; runs on the
|
||||
* BLE host task with the same caveats as ble_uart_on_rx. */
|
||||
ble_uart_evt_cb_t on_event;
|
||||
} ble_uart_config_t;
|
||||
|
||||
/* ----- Lifecycle ------------------------------------------------------ */
|
||||
|
||||
/** Bring up host stack + Security Manager + SIG services + BLE UART GATT
|
||||
* service. Caller must have already called nvs_flash_init().
|
||||
* cfg->device_name is copied; doesn't need to outlive the call.
|
||||
*
|
||||
* cfg->device_name, cfg->adv_data and cfg->scan_rsp_data are all
|
||||
* copied internally; the caller's buffers don't need to outlive the
|
||||
* call. Returns BLE_UART_EINVAL if any of these checks fail:
|
||||
* strlen(cfg->device_name) > BLE_UART_DEVICE_NAME_MAX (26)
|
||||
* cfg->adv_data_len > BLE_UART_ADV_DATA_MAX (28)
|
||||
* cfg->scan_rsp_data_len > BLE_UART_SCAN_RSP_DATA_MAX (31)
|
||||
* cfg->security.{sc,bonding,mitm} outside BLE_UART_SEC_{AUTO,OFF,ON}
|
||||
* cfg->security.io_cap outside BLE_UART_IO_CAP_{AUTO,
|
||||
* NO_INPUT_OUTPUT,DISPLAY_ONLY,
|
||||
* KEYBOARD_ONLY,DISPLAY_YES_NO,
|
||||
* KEYBOARD_DISPLAY}
|
||||
* resolved mitm=ON + io_cap=NO_INPUT_OUTPUT
|
||||
* (Just Works can never satisfy MITM)
|
||||
* io_cap requires user input (KEYBOARD_ONLY, DISPLAY_YES_NO,
|
||||
* KEYBOARD_DISPLAY) but cfg->on_event
|
||||
* is NULL — the application would have
|
||||
* no way to receive PASSKEY_REQUEST /
|
||||
* NUMERIC_COMPARE and answer it
|
||||
* (io_cap=AUTO with resolved mitm=ON, or DISPLAY_ONLY, does not
|
||||
* need on_event — equivalent to Passkey Display handled inside
|
||||
* the port; PASSKEY_DISPLAY via on_event is optional)
|
||||
*
|
||||
* Single-shot until ble_uart_uninstall(); a second call returns
|
||||
* BLE_UART_EALREADY. */
|
||||
int ble_uart_install(const ble_uart_config_t *cfg);
|
||||
@@ -111,16 +536,84 @@ int ble_uart_open(void);
|
||||
* quiesces the host. install state is preserved — call open() again
|
||||
* to resume.
|
||||
*
|
||||
* Don't call from the BLE host task (i.e. from ble_uart_on_rx). */
|
||||
* NimBLE: also resets the local GATT server; the next open() re-adds
|
||||
* GAP/GATT/UART. Service UUIDs are unchanged but ATT handles may
|
||||
* differ — centrals must rediscover and re-subscribe (PORTING.md
|
||||
* §5.3.1a). Bluedroid: host and GATT table stay up; open() only
|
||||
* restarts advertising.
|
||||
*
|
||||
* Don't call from the BLE host task (i.e. from ble_uart_on_rx or
|
||||
* ble_uart_evt_cb_t) — it would deadlock waiting for the disconnect
|
||||
* event that the host task itself is supposed to deliver. Use
|
||||
* ble_uart_close_async() in those contexts instead. */
|
||||
int ble_uart_close(void);
|
||||
|
||||
/** Fire-and-forget variant of ble_uart_close(). Returns immediately
|
||||
* after spawning a small worker task that runs the regular close
|
||||
* sequence in the background; safe from ANY task — including the
|
||||
* BLE host task (i.e. from inside ble_uart_on_rx or on_event), where
|
||||
* the synchronous variant deadlocks.
|
||||
*
|
||||
* Completion is reported on the on_event callback as
|
||||
* BLE_UART_EVT_CLOSED with .closed.status set to the worker's
|
||||
* ble_uart_close() result. When status is BLE_UART_OK the host stack
|
||||
* is fully torn down — then uninstall on an app task after
|
||||
* BLE_UART_EVT_CLOSED (PORTING.md §5.3.2 Path B).
|
||||
* (BLE_UART_EVT_DISCONNECTED is also delivered, ahead of CLOSED, if
|
||||
* there was a peer.)
|
||||
*
|
||||
* Idempotent in the harmless sense: calling it before
|
||||
* ble_uart_open() has succeeded, or while a previous async close
|
||||
* is still draining, returns BLE_UART_EALREADY without spawning a
|
||||
* second worker. Returns BLE_UART_ENOMEM if FreeRTOS can't
|
||||
* allocate the worker task. */
|
||||
int ble_uart_close_async(void);
|
||||
|
||||
/** Counterpart to ble_uart_install(). Force-closes if still open,
|
||||
* then tears down the host stack + controller. After this returns,
|
||||
* install() can run from scratch.
|
||||
*
|
||||
* Don't call from the BLE host task. */
|
||||
* Don't call from the BLE host task (NimBLE host / Bluedroid BTC).
|
||||
* If a ble_uart_close_async() worker is still running, this call
|
||||
* polls for up to ~5 s and then proceeds with teardown anyway if the
|
||||
* worker has not exited — do not call uninstall from another task
|
||||
* while a close_async is in flight unless you follow PORTING.md §5.3:
|
||||
* Path A — ble_uart_close() then uninstall from an app task; or
|
||||
* Path B — close_async, then uninstall on an app task after
|
||||
* BLE_UART_EVT_CLOSED with .closed.status == BLE_UART_OK (never
|
||||
* call uninstall from inside the CLOSED handler). */
|
||||
int ble_uart_uninstall(void);
|
||||
|
||||
/* ----- Pairing replies ----------------------------------------------- */
|
||||
|
||||
/** Answer an in-flight BLE_UART_EVT_PASSKEY_REQUEST.
|
||||
*
|
||||
* `passkey` is the 6-digit value the user read off the central's
|
||||
* display and entered on this device — must be in 0..999999.
|
||||
*
|
||||
* Safe from any task. Returns:
|
||||
* BLE_UART_OK reply was injected into the SM
|
||||
* BLE_UART_EINVAL passkey > 999999
|
||||
* BLE_UART_ENOTCONN no PASSKEY_REQUEST is currently pending
|
||||
* (link dropped, pairing already timed out,
|
||||
* or the SM asked for something else)
|
||||
* BLE_UART_EFAIL backend rejected the inject
|
||||
*
|
||||
* Each PASSKEY_REQUEST event accepts exactly one reply; subsequent
|
||||
* calls return BLE_UART_ENOTCONN until the next request. */
|
||||
int ble_uart_passkey_reply(uint32_t passkey);
|
||||
|
||||
/** Answer an in-flight BLE_UART_EVT_NUMERIC_COMPARE.
|
||||
*
|
||||
* `match` is the user's verdict: true if the 6-digit values shown
|
||||
* on this device and on the central are identical, false otherwise.
|
||||
* A `false` reply makes pairing fail with a numeric-comparison
|
||||
* mismatch, surfaced as BLE_UART_EVT_PAIRING_FAILED.
|
||||
*
|
||||
* Same threading semantics and return codes as
|
||||
* ble_uart_passkey_reply(). */
|
||||
int ble_uart_compare_reply(bool match);
|
||||
|
||||
/* ----- TX ------------------------------------------------------------- */
|
||||
|
||||
/** Send raw bytes to the connected central as one or more TX
|
||||
@@ -143,6 +636,68 @@ bool ble_uart_is_connected(void);
|
||||
* the CCCD write); exposed for diagnostics only. */
|
||||
bool ble_uart_is_subscribed(void);
|
||||
|
||||
/* ----- Bond management ----------------------------------------------- */
|
||||
|
||||
/** Number of bonded peers in the persistent store.
|
||||
*
|
||||
* Requires ble_uart_install() to have run; works whether or not
|
||||
* ble_uart_open() has been called. *out_count is left untouched on
|
||||
* failure. Safe from any task. */
|
||||
int ble_uart_get_bond_count(size_t *out_count);
|
||||
|
||||
/** List the bonded peers' addresses.
|
||||
*
|
||||
* Up to `cap` entries are written to `out`; on success *out_count
|
||||
* receives the **total** number of bonds (which may exceed `cap`).
|
||||
* When *out_count > cap the caller may allocate a larger buffer
|
||||
* and re-call to read the rest.
|
||||
*
|
||||
* `out` may be NULL if `cap` is 0 — useful as a preflight to size
|
||||
* an exactly-fitting buffer (although ble_uart_get_bond_count()
|
||||
* does the same with one less argument).
|
||||
*
|
||||
* Safe from any task. Requires ble_uart_install() to have run. */
|
||||
int ble_uart_get_bonded_peers(ble_uart_addr_t *out,
|
||||
size_t cap,
|
||||
size_t *out_count);
|
||||
|
||||
/** Drop the bond (LTK / IRK / persisted CCCD) for one peer.
|
||||
*
|
||||
* Does not actively disconnect the current link (encrypted or not).
|
||||
* Call ble_uart_close() first if you need an immediate disconnect
|
||||
* and re-pair.
|
||||
*
|
||||
* `peer` is matched against the identity address in the bond store.
|
||||
* Backend matching:
|
||||
* - NimBLE: `(type, bytes)`. `BLE_UART_EVT_CONNECTED` and
|
||||
* `get_bonded_peers()` both yield identity addresses suitable
|
||||
* for this call (first connect: same as over-the-air; bonded RPA
|
||||
* reconnect: resolved identity, not the random on the wire).
|
||||
* - Bluedroid: `bytes` only — `type` is ignored. The bond store
|
||||
* and CONNECT both use the address seen when bonding was
|
||||
* established (address_A); later over-the-air changes are not
|
||||
* reflected in either API.
|
||||
*
|
||||
* Idempotent: returns BLE_UART_OK whether or not the peer was bonded
|
||||
* (both backends treat "already absent" as success — NimBLE's
|
||||
* ble_store_util_delete_peer maps BLE_HS_ENOENT to 0). Use
|
||||
* ble_uart_get_bonded_peers() first if you need to distinguish
|
||||
* "removed" from "was never bonded".
|
||||
*
|
||||
* Returns BLE_UART_EINVAL if peer is NULL or ble_uart_install() has
|
||||
* not run. Safe from any task. */
|
||||
int ble_uart_remove_peer(const ble_uart_addr_t *peer);
|
||||
|
||||
/** Drop ALL bonded peers — equivalent to a factory reset of the bond
|
||||
* store, but does not touch any other NVS namespace.
|
||||
*
|
||||
* Does not actively disconnect the current link (encrypted or not).
|
||||
* Call ble_uart_close() first if you need an immediate disconnect
|
||||
* and re-pair.
|
||||
*
|
||||
* Returns BLE_UART_OK if the store was cleared. Safe from any task. */
|
||||
int ble_uart_clear_bonds(void);
|
||||
|
||||
/* ----- Service UUID -------------------------------------------------- */
|
||||
|
||||
/** The BLE UART service UUID, exposed for custom advertising payloads.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user