Files
2026-07-13 13:04:25 +08:00

710 lines
30 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*
* ESP-BLE-UART — turnkey serial-over-BLE peripheral.
*
* Implements the de-facto BLE UART-over-GATT layout (RX write, TX notify;
* fixed 128-bit UUIDs below) on top of either NimBLE or Bluedroid; the
* backend is picked at compile time via CONFIG_BT_NIMBLE_ENABLED /
* CONFIG_BT_BLUEDROID_ENABLED.
*
* Lifecycle — bring-up:
*
* 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
*
* 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):
*
* Service: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
* RX : 6e400002-b5a3-f393-e0a9-e50e24dcca9e write
* TX : 6e400003-b5a3-f393-e0a9-e50e24dcca9e notify
*
* See PORTING.md in this component directory for the integration guide.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ----- Return codes --------------------------------------------------- */
/** All ble_uart_* APIs return one of these stack-neutral codes. */
#define BLE_UART_OK 0 /* Success */
#define BLE_UART_EINVAL -1 /* Bad argument or unsupported op */
#define BLE_UART_ENOTCONN -2 /* No central currently connected */
#define BLE_UART_ENOMEM -3 /* Out of mbufs / send queue full */
#define BLE_UART_EALREADY -4 /* Lifecycle already in this state */
#define BLE_UART_EFAIL -5 /* Backend internal failure (see logs) */
/* ----- 128-bit UUID helper -------------------------------------------- */
/** Stack-agnostic 128-bit UUID in little-endian (wire) order. */
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
* arrive on the RX characteristic. The buffer is owned by the stack
* and reused after return — copy what you need to keep.
*
* Don't block here; offload heavy work to your own task.
*
* Long-write (PREP/EXEC) reassembly is handled transparently — you
* always see one contiguous payload, capped by
* CONFIG_BLE_UART_RX_SCRATCH_SIZE (default 1024). Oversized writes
* 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 {
/** 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;
/** 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, 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);
/** Start advertising. NimBLE: spawns the host task and primes the bond
* store; advertising begins once the controller signals ready.
* Bluedroid: triggers adv-data + scan-response config; advertising
* begins once the stack acknowledges both.
*
* Returns immediately; the ESP-BLE-UART then runs autonomously
* (connect, pairing, passkey display, RX delivery all via internal
* callbacks). Single-shot. */
int ble_uart_open(void);
/** Counterpart to ble_uart_open(). Stops advertising, gracefully
* disconnects (waits up to 500 ms for LL_TERMINATE_IND ack), and
* quiesces the host. install state is preserved — call open() again
* to resume.
*
* 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 (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
* notifications, fragmented to fit the live ATT MTU. Safe from any
* FreeRTOS task; not safe from ISR.
*
* Returns BLE_UART_ENOTCONN when no peer is connected (this is
* normal — typically just ignore). */
int ble_uart_tx(const uint8_t *data, size_t len);
/* ----- Status (best-effort, optional) -------------------------------- */
/** True when a central is connected (link may not yet be encrypted).
* Best-effort snapshot; production callers should rely on the return
* code of ble_uart_tx() instead. */
bool ble_uart_is_connected(void);
/** True when the central has subscribed to TX notifications.
* ble_uart_tx() does NOT gate on this (bonded reconnects often skip
* 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.
* The two characteristic UUIDs are private to the backend. */
extern const ble_uart_uuid128_t ble_uart_service_uuid;
#ifdef __cplusplus
}
#endif