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

1445 lines
54 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 — NimBLE backend. Implements the lifecycle declared in
* ble_uart.h on top of the NimBLE host. Active when
* CONFIG_BT_NIMBLE_ENABLED=y; otherwise ble_uart_bluedroid.c is used.
*/
#include "sdkconfig.h"
#if CONFIG_BT_NIMBLE_ENABLED
#include "ble_uart.h"
#include <assert.h>
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_random.h"
#include "nimble/ble.h"
#include "host/ble_att.h"
#include "host/ble_gap.h"
#include "host/ble_gatt.h"
#include "host/ble_hs.h"
#include "host/ble_hs_mbuf.h"
#include "host/ble_sm.h"
#include "host/ble_uuid.h"
#include "host/util/util.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
/* tx path needs notifications; the disabled-path in
* ble_gatts_notify_custom() leaks the caller's mbuf. */
#if !MYNEWT_VAL(BLE_GATT_NOTIFY)
#error "ble_uart NimBLE backend requires MYNEWT_VAL(BLE_GATT_NOTIFY)=1"
#endif
static const char *TAG = "ble_uart";
/* Map NimBLE rc → public BLE_UART_E* code; unknown rcs → EFAIL. */
static int xlate_rc(int nimble_rc)
{
switch (nimble_rc) {
case 0: return BLE_UART_OK;
case BLE_HS_EINVAL: return BLE_UART_EINVAL;
case BLE_HS_ENOTCONN: return BLE_UART_ENOTCONN;
case BLE_HS_ENOMEM: return BLE_UART_ENOMEM;
case BLE_HS_EALREADY: return BLE_UART_EALREADY;
default: return BLE_UART_EFAIL;
}
}
/* Collapse NimBLE's 4-value peer addr type (03) into our public
* 2-value enum (identity types map onto public/random). */
static uint8_t nimble_peer_type_to_uart(uint8_t nimble_type)
{
return (nimble_type == BLE_ADDR_PUBLIC ||
nimble_type == BLE_ADDR_PUBLIC_ID)
? BLE_UART_ADDR_TYPE_PUBLIC
: BLE_UART_ADDR_TYPE_RANDOM;
}
/* Marshal a NimBLE ble_addr_t into our public ble_uart_addr_t. */
static void from_nimble_addr(const ble_addr_t *src, ble_uart_addr_t *dst)
{
dst->type = nimble_peer_type_to_uart(src->type);
for (int i = 0; i < 6; i++) {
dst->bytes[i] = src->val[5 - i];
}
}
/* Provided by NimBLE's `store/config` lib. */
extern void ble_store_config_init(void);
/* ===== UUIDs =========================================================== */
/* BLE UART profile UUIDs in little-endian byte order. */
#define NUS_SVC_BYTES 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, \
0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e
#define NUS_RX_BYTES 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, \
0x93, 0xf3, 0xa3, 0xb5, 0x02, 0x00, 0x40, 0x6e
#define NUS_TX_BYTES 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, \
0x93, 0xf3, 0xa3, 0xb5, 0x03, 0x00, 0x40, 0x6e
const ble_uart_uuid128_t ble_uart_service_uuid = { .bytes = { NUS_SVC_BYTES } };
static const ble_uuid128_t s_svc_uuid = BLE_UUID128_INIT(NUS_SVC_BYTES);
static const ble_uuid128_t s_chr_rx_uuid = BLE_UUID128_INIT(NUS_RX_BYTES);
static const ble_uuid128_t s_chr_tx_uuid = BLE_UUID128_INIT(NUS_TX_BYTES);
/* ===== State =========================================================== */
/* RX scratch capacity. Tunable via menuconfig (Component config → ESP-BLE-UART
* library); fall
* back to 1024 if CONFIG_BLE_UART_RX_SCRATCH_SIZE is absent. */
#ifndef CONFIG_BLE_UART_RX_SCRATCH_SIZE
#define CONFIG_BLE_UART_RX_SCRATCH_SIZE 1024
#endif
#define RX_SCRATCH CONFIG_BLE_UART_RX_SCRATCH_SIZE
/* Cached device name. Avoids ble_svc_gap_device_name() which returns
* NULL when CONFIG_BT_NIMBLE_GAP_SERVICE=n (would NULL-deref). The
* cap is BLE_UART_DEVICE_NAME_MAX (validated in install) + NUL; we
* round up for safety margin. */
#define DEV_NAME_MAX (BLE_UART_DEVICE_NAME_MAX + 2)
static ble_uart_config_t s_cfg;
static char s_dev_name[DEV_NAME_MAX];
static uint16_t s_tx_val_handle;
/* Volatile: written from NimBLE host task, polled from caller task. */
static volatile uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
static bool s_subscribed;
static bool s_installed;
static bool s_opened;
/* After close(), ble_gatts_stop() drops svc-def pointers; open() must
* count/add again before the next ble_hs_start(). Cleared on install. */
static bool s_gatts_needs_readd;
#if MYNEWT_VAL(BLE_HS_AUTO_START)
/* Set in install() when nimble_port_init() queues the one-shot AUTO_START
* event; cleared on first open() so we don't also ble_hs_sched_start() and
* trip assert(rc==0) in ble_hs_event_start_stage2 (BLE_HS_EALREADY). */
static bool s_hs_auto_start_pending;
#endif
static bool s_shutting_down; /* gates auto-readvertise during close */
/* Set by ble_uart_close_async() when its worker task is in flight,
* cleared by the worker just before it exits. uninstall() polls this
* to drain a pending async close before tearing the port down. */
static volatile bool s_closing;
static uint8_t s_own_addr_type;
/* Resolved security policy. Computed once in install() from cfg.encrypted
* + the per-feature overrides (cfg.sc/bonding/mitm/io_cap), then read
* in the GAP event handler and the GATT-table builder.
* s_link_encrypted = true if any of {sc, bonding, mitm} resolved ON
* → kick pairing on connect, require encryption on chars
* s_mitm_required = resolved mitm bit
* → require AUTHEN flag on chars (Just-Works peer cannot read/write) */
static bool s_link_encrypted;
static bool s_mitm_required;
/* Pending Passkey-Entry / Numeric-Comparison request awaiting an
* application reply via ble_uart_passkey_reply / ble_uart_compare_reply.
*
* s_pending_io_conn — conn_handle the SM is asking about, or
* BLE_HS_CONN_HANDLE_NONE if no request is in
* flight. Set in PASSKEY_ACTION, cleared on
* reply, on disconnect, and on enc_change.
* s_pending_io_action — BLE_SM_IOACT_INPUT or BLE_SM_IOACT_NUMCMP;
* used to reject mismatched reply calls (e.g.
* passkey_reply during NUMCMP).
*
* No FreeRTOS lock — both fields are written only from the host task,
* and the reply API is the only outside reader. The reader takes a
* local snapshot before injecting. */
static volatile uint16_t s_pending_io_conn = BLE_HS_CONN_HANDLE_NONE;
static volatile uint8_t s_pending_io_action;
/* Optional user-supplied advertising payloads. When `s_adv_data_len`
* is non-zero we feed `s_adv_data` straight to ble_gap_adv_set_data;
* the buffer always carries the 3-byte Flags AD element we built in
* install() followed by the user's bytes. Same for the scan response
* (no Flags element there). Zero length means "use the default
* field-builder path in start_advertising()". */
static uint8_t s_adv_data[3 + BLE_UART_ADV_DATA_MAX];
static uint8_t s_adv_data_len;
static uint8_t s_scan_rsp_data[BLE_UART_SCAN_RSP_DATA_MAX];
static uint8_t s_scan_rsp_data_len;
static int gap_event(struct ble_gap_event *event, void *arg);
static int start_advertising(void);
/* ===== GATT (BLE UART service) ========================================= */
static int chr_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
switch (ctxt->op) {
case BLE_GATT_ACCESS_OP_WRITE_CHR: {
/* File-scope (BSS) — host task is single-threaded so no reentry. */
static uint8_t s_rx_buf[RX_SCRATCH];
uint16_t total = OS_MBUF_PKTLEN(ctxt->om);
if (total > sizeof(s_rx_buf)) {
ESP_LOGW(TAG, "rx oversize: %u > %u, rejecting",
(unsigned)total, (unsigned)sizeof(s_rx_buf));
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
uint16_t copied = 0;
int rc = ble_hs_mbuf_to_flat(ctxt->om, s_rx_buf, total, &copied);
if (rc != 0) {
return BLE_ATT_ERR_UNLIKELY;
}
if (s_cfg.ble_uart_on_rx != NULL && copied > 0) {
s_cfg.ble_uart_on_rx(s_rx_buf, copied);
}
return 0;
}
case BLE_GATT_ACCESS_OP_READ_CHR:
return BLE_ATT_ERR_READ_NOT_PERMITTED;
default:
return BLE_ATT_ERR_UNLIKELY;
}
}
/* Encryption-required flag masks. NimBLE derives CCCD permissions from
* NOTIFY_INDICATE_* (not from READ/WRITE_*), so notify-only chars need
* the NOTIFY_INDICATE mask, not just the RW mask — otherwise an
* unpaired central could subscribe and receive notifications over the
* unencrypted link (see ble_gatts.c:ble_gatts_chr_clt_cfg_flags_from_chr_flags).
*
* The _ENC and _AUTHEN halves are split so an encrypted-but-unauthenticated
* (Just Works) link still passes when mitm=OFF — _AUTHEN gates on the
* link's authenticated bit which Just Works doesn't set. */
#define CHR_FLAG_RW_ENC (BLE_GATT_CHR_F_READ_ENC | \
BLE_GATT_CHR_F_WRITE_ENC)
#define CHR_FLAG_RW_AUTHEN (BLE_GATT_CHR_F_READ_AUTHEN | \
BLE_GATT_CHR_F_WRITE_AUTHEN)
#define CHR_FLAG_NOTIFY_ENC (BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC)
#define CHR_FLAG_NOTIFY_AUTHEN (BLE_GATT_CHR_F_NOTIFY_INDICATE_AUTHEN)
static struct ble_gatt_chr_def s_chr_defs[3];
static struct ble_gatt_svc_def s_svc_defs[2];
static void build_gatt_table(bool link_enc, bool mitm)
{
/* `ble_gatt_chr_flags` is uint32_t — match width here so the
* 0x10000-and-above NOTIFY_INDICATE flags don't get truncated. */
ble_gatt_chr_flags rw_enc = 0;
ble_gatt_chr_flags notify_enc = 0;
if (link_enc) {
rw_enc |= CHR_FLAG_RW_ENC;
notify_enc |= CHR_FLAG_NOTIFY_ENC;
if (mitm) {
rw_enc |= CHR_FLAG_RW_AUTHEN;
notify_enc |= CHR_FLAG_NOTIFY_AUTHEN;
}
}
s_chr_defs[0] = (struct ble_gatt_chr_def){
.uuid = &s_chr_rx_uuid.u,
.access_cb = chr_access,
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP | rw_enc,
};
s_chr_defs[1] = (struct ble_gatt_chr_def){
.uuid = &s_chr_tx_uuid.u,
.access_cb = chr_access,
.flags = BLE_GATT_CHR_F_NOTIFY | notify_enc,
.val_handle = &s_tx_val_handle,
};
s_chr_defs[2] = (struct ble_gatt_chr_def){0};
s_svc_defs[0] = (struct ble_gatt_svc_def){
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = &s_svc_uuid.u,
.characteristics = s_chr_defs,
};
s_svc_defs[1] = (struct ble_gatt_svc_def){0};
}
static int register_uart_gatt_svc(void)
{
build_gatt_table(s_link_encrypted, s_mitm_required);
int rc = ble_gatts_count_cfg(s_svc_defs);
if (rc != 0) {
ESP_LOGE(TAG, "ble_gatts_count_cfg rc=%d", rc);
return xlate_rc(rc);
}
rc = ble_gatts_add_svcs(s_svc_defs);
if (rc != 0) {
ESP_LOGE(TAG, "ble_gatts_add_svcs rc=%d", rc);
return xlate_rc(rc);
}
return BLE_UART_OK;
}
/* Re-queue std + UART svc defs after close(). Host stop + gatts_reset clear
* the ATT table and free the svc-def pointer array; only re-adding UART would
* leave GAP/GATT (and anything else added at install) off-air on the next
* ble_hs_start(). ble_svc_*_init() is safe to recall on ESP-IDF: SYSINIT_ASSERT
* is a no-op and gap name storage is not reallocated if already present. */
static int reregister_gatt_svcs_after_close(void)
{
#if NIMBLE_BLE_CONNECT
ble_svc_gap_init();
#endif
ble_svc_gatt_init();
return register_uart_gatt_svc();
}
static void register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg)
{
char buf[BLE_UUID_STR_LEN];
switch (ctxt->op) {
case BLE_GATT_REGISTER_OP_SVC:
ESP_LOGD(TAG, "registered service %s handle=%d",
ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf), ctxt->svc.handle);
break;
case BLE_GATT_REGISTER_OP_CHR:
ESP_LOGD(TAG, "registered chr %s def=%d val=%d",
ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf),
ctxt->chr.def_handle, ctxt->chr.val_handle);
break;
default:
break;
}
}
/* ===== TX ============================================================== */
int ble_uart_tx(const uint8_t *data, size_t len)
{
/* Snapshot conn_handle once: a peer-A→peer-B disconnect+connect
* race during a multi-chunk send could otherwise leak later chunks
* to peer B (notify_custom doesn't gate on the CCCD subscription).
* Stale handle → BLE_HS_ENOTCONN, we bail cleanly. */
uint16_t conn_handle = s_conn_handle;
if (conn_handle == BLE_HS_CONN_HANDLE_NONE) {
return BLE_UART_ENOTCONN;
}
if (data == NULL || len == 0) {
return BLE_UART_EINVAL;
}
uint16_t mtu = ble_att_mtu(conn_handle);
size_t chunk = (mtu > 3) ? (size_t)(mtu - 3) : 20;
size_t sent = 0;
while (sent < len) {
size_t n = len - sent;
if (n > chunk) {
n = chunk;
}
struct os_mbuf *om = ble_hs_mbuf_from_flat(data + sent, n);
if (om == NULL) {
return BLE_UART_ENOMEM;
}
int rc = ble_gatts_notify_custom(conn_handle, s_tx_val_handle, om);
if (rc != 0) {
ESP_LOGW(TAG, "notify failed: rc=%d", rc);
/* Callee frees om on every failure path EXCEPT the
* BLE_GATT_NOTIFY-disabled early-return (BLE_HS_ENOTSUP).
* Freeing on any other rc would be a double free. */
if (rc == BLE_HS_ENOTSUP) {
os_mbuf_free_chain(om);
}
return xlate_rc(rc);
}
sent += n;
}
return BLE_UART_OK;
}
/* Best-effort snapshots; see header for threading caveat. */
bool ble_uart_is_connected(void) { return s_conn_handle != BLE_HS_CONN_HANDLE_NONE; }
bool ble_uart_is_subscribed(void) { return s_subscribed; }
/* ===== Event dispatch ================================================= */
/* Forward a tagged event to the application callback. NULL-safe so all
* call sites stay one-liners; runs on the NimBLE host task — caller
* must keep the local `ble_uart_evt_t` alive across the call (we do
* via stack/compound literal at each site). */
static void emit_evt(const ble_uart_evt_t *evt)
{
if (s_cfg.on_event != NULL) {
s_cfg.on_event(evt);
}
}
/* ===== Advertising ==================================================== */
static int start_advertising(void)
{
int rc;
/* Two paths: raw-bytes (when the app provided its own payload)
* and field-builder (default). Mixing is allowed — e.g. raw
* adv_data + default scan_rsp.
*
* Default primary payload: Flags AD + Complete Local Name. The
* 128-bit service UUID lives in the scan response (the 31-byte
* primary packet can't hold name + 128-bit UUID together). */
if (s_adv_data_len > 0) {
rc = ble_gap_adv_set_data(s_adv_data, s_adv_data_len);
if (rc != 0) {
ESP_LOGE(TAG, "adv_set_data rc=%d", rc);
return rc;
}
} else {
const char *name = s_dev_name;
size_t name_len = strlen(name);
struct ble_hs_adv_fields adv = {
.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP,
/* If no name was set, advertise without one (NimBLE accepts
* NULL+0); the service UUID in scan rsp still identifies us. */
.name = name_len > 0 ? (uint8_t *)name : NULL,
.name_len = name_len,
.name_is_complete = name_len > 0 ? 1 : 0,
};
rc = ble_gap_adv_set_fields(&adv);
if (rc != 0) {
ESP_LOGE(TAG, "adv_set_fields rc=%d (name too long?)", rc);
return rc;
}
}
if (s_scan_rsp_data_len > 0) {
rc = ble_gap_adv_rsp_set_data(s_scan_rsp_data, s_scan_rsp_data_len);
if (rc != 0) {
ESP_LOGE(TAG, "adv_rsp_set_data rc=%d", rc);
return rc;
}
} else {
struct ble_hs_adv_fields rsp = {
.uuids128 = &s_svc_uuid,
.num_uuids128 = 1,
.uuids128_is_complete = 1,
};
rc = ble_gap_adv_rsp_set_fields(&rsp);
if (rc != 0) {
ESP_LOGE(TAG, "adv_rsp_set_fields rc=%d", rc);
return rc;
}
}
struct ble_gap_adv_params params = {
.conn_mode = BLE_GAP_CONN_MODE_UND,
.disc_mode = BLE_GAP_DISC_MODE_GEN,
};
rc = ble_gap_adv_start(s_own_addr_type, NULL, BLE_HS_FOREVER,
&params, gap_event, NULL);
if (rc != 0) {
ESP_LOGE(TAG, "adv_start rc=%d", rc);
return rc;
}
/* "advertising as '<name>'" only makes sense when ble_uart owns
* the primary payload — with a caller-supplied adv_data the name
* the scanner sees is whatever bytes the caller put in there, not
* s_dev_name (which is only exposed via the GAP-service Device
* Name characteristic, post-connect). Pick the wording per path. */
if (s_adv_data_len > 0 || s_scan_rsp_data_len > 0) {
ESP_LOGI(TAG, "advertising with custom payload "
"(adv=%u B, scan_rsp=%u B; GAP-service name='%s')",
(unsigned)s_adv_data_len,
(unsigned)s_scan_rsp_data_len,
s_dev_name[0] ? s_dev_name : "<unset>");
} else {
ESP_LOGI(TAG, "advertising as '%s'",
s_dev_name[0] ? s_dev_name : "<no name>");
}
return 0;
}
/* ===== GAP event handler ============================================== */
static void show_passkey(uint32_t passkey)
{
ESP_LOGW(TAG, "");
ESP_LOGW(TAG, " +-----------------------------+");
ESP_LOGW(TAG, " | BLE PAIRING PASSKEY: |");
ESP_LOGW(TAG, " | %06" PRIu32 " |", passkey);
ESP_LOGW(TAG, " +-----------------------------+");
ESP_LOGW(TAG, "");
}
static int gap_event(struct ble_gap_event *event, void *arg)
{
struct ble_gap_conn_desc desc;
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
ESP_LOGI(TAG, "connect %s status=%d handle=%d",
event->connect.status == 0 ? "ok" : "failed",
event->connect.status,
event->connect.conn_handle);
if (event->connect.status == 0) {
s_conn_handle = event->connect.conn_handle;
s_subscribed = false;
/* Look up the peer's address; on first pair this equals
* peer_ota_addr, on bonded reconnect this is the resolved
* identity address. ble_gap_conn_find should never fail
* for a just-arrived connect event, but guard anyway —
* a zero-address payload is preferable to a stale stack
* read. */
ble_uart_evt_t e = { .id = BLE_UART_EVT_CONNECTED };
struct ble_gap_conn_desc d;
if (ble_gap_conn_find(event->connect.conn_handle, &d) == 0) {
from_nimble_addr(&d.peer_id_addr, &e.connected.peer);
}
emit_evt(&e);
/* Start pairing immediately (rather than lazily on the
* first encrypted attribute access). Resolved policy:
* any of {sc, bonding, mitm} ON → pairing required. */
if (s_link_encrypted) {
ble_gap_security_initiate(event->connect.conn_handle);
}
} else if (!s_shutting_down) {
start_advertising();
}
return 0;
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(TAG, "disconnect reason=%d", event->disconnect.reason);
s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
s_subscribed = false;
/* Drop any pending Passkey-Entry / NC reply; pairing was
* cancelled along with the link. A stale value here would
* make the next reply call inject into a closed conn. */
s_pending_io_conn = BLE_HS_CONN_HANDLE_NONE;
emit_evt(&(ble_uart_evt_t){
.id = BLE_UART_EVT_DISCONNECTED,
.disconnected = { .reason = event->disconnect.reason },
});
if (!s_shutting_down) {
start_advertising();
}
return 0;
case BLE_GAP_EVENT_CONN_UPDATE:
ESP_LOGI(TAG, "conn_update status=%d", event->conn_update.status);
return 0;
case BLE_GAP_EVENT_ADV_COMPLETE:
ESP_LOGI(TAG, "adv_complete reason=%d", event->adv_complete.reason);
/* Don't auto-restart while a connection is up. Undirected adv
* auto-stops at the LL on connect (BT Core spec), and an
* explicit start while connected would either fail (single-
* conn build, the default) or accept a second peripheral
* link we don't want to handle here. ADV_COMPLETE can still
* arrive in connected state via NimBLE-internal cleanup
* (e.g. resolving-list updates after bonding); ignore it. */
if (!s_shutting_down && s_conn_handle == BLE_HS_CONN_HANDLE_NONE) {
start_advertising();
}
return 0;
case BLE_GAP_EVENT_ENC_CHANGE:
/* Pairing has resolved one way or the other; clear any pending
* Passkey-Entry / NC request so the next pairing starts fresh. */
s_pending_io_conn = BLE_HS_CONN_HANDLE_NONE;
if (ble_gap_conn_find(event->enc_change.conn_handle, &desc) == 0) {
ESP_LOGI(TAG, "enc_change status=%d encrypted=%d authenticated=%d bonded=%d",
event->enc_change.status,
desc.sec_state.encrypted,
desc.sec_state.authenticated,
desc.sec_state.bonded);
/* Dispatch on the actual sec_state, not the rc — bonded
* reconnects can finish with status=BLE_HS_ETIMEOUT (13)
* while encrypted=1 thanks to a benign race with the
* peer's auto-encrypt; reporting that as PAIRING_FAILED
* would be wrong. */
if (desc.sec_state.encrypted) {
emit_evt(&(ble_uart_evt_t){
.id = BLE_UART_EVT_LINK_SECURE,
.link_secure = {
.encrypted = (bool)desc.sec_state.encrypted,
.authenticated = (bool)desc.sec_state.authenticated,
.bonded = (bool)desc.sec_state.bonded,
.key_size = (uint8_t)desc.sec_state.key_size,
},
});
} else {
emit_evt(&(ble_uart_evt_t){
.id = BLE_UART_EVT_PAIRING_FAILED,
.pairing_failed = { .reason = event->enc_change.status },
});
}
}
return 0;
case BLE_GAP_EVENT_REPEAT_PAIRING:
/* Drop old keys + retry rather than reject. */
if (ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc) == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
return BLE_GAP_REPEAT_PAIRING_RETRY;
case BLE_GAP_EVENT_PASSKEY_ACTION:
switch (event->passkey.params.action) {
case BLE_SM_IOACT_DISP: {
/* Rejection sampling avoids the modulo bias of
* `esp_random() % 1000000` (2^32 % 1e6 != 0). */
const uint32_t passkey_max = 1000000U;
const uint32_t reject_above = UINT32_MAX -
(UINT32_MAX % passkey_max);
uint32_t r;
do {
r = esp_random();
} while (r >= reject_above);
struct ble_sm_io pkey = {
.action = BLE_SM_IOACT_DISP,
.passkey = r % passkey_max,
};
/* Banner stays for backward compat with log-scraping
* tests; on_event is additive. */
show_passkey(pkey.passkey);
emit_evt(&(ble_uart_evt_t){
.id = BLE_UART_EVT_PASSKEY_DISPLAY,
.passkey = { .passkey = pkey.passkey },
});
int rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
if (rc != 0) {
ESP_LOGW(TAG, "ble_sm_inject_io(DISP) rc=%d", rc);
}
break;
}
case BLE_SM_IOACT_INPUT:
/* Central displays a passkey, user reads it from there
* and types it into our device. We can't inject anything
* yet — wait for ble_uart_passkey_reply(). */
ESP_LOGI(TAG, "passkey entry requested (conn=%d)",
event->passkey.conn_handle);
s_pending_io_conn = event->passkey.conn_handle;
s_pending_io_action = BLE_SM_IOACT_INPUT;
emit_evt(&(ble_uart_evt_t){ .id = BLE_UART_EVT_PASSKEY_REQUEST });
break;
case BLE_SM_IOACT_NUMCMP:
/* Both sides should display the same 6-digit value; user
* confirms match. The value is in `numcmp` (already a
* decimal 0..999999, computed by the SM). */
ESP_LOGI(TAG, "numeric compare %06" PRIu32 " (conn=%d)",
event->passkey.params.numcmp,
event->passkey.conn_handle);
s_pending_io_conn = event->passkey.conn_handle;
s_pending_io_action = BLE_SM_IOACT_NUMCMP;
emit_evt(&(ble_uart_evt_t){
.id = BLE_UART_EVT_NUMERIC_COMPARE,
.numeric_compare = { .passkey = event->passkey.params.numcmp },
});
break;
case BLE_SM_IOACT_OOB:
/* OOB plumbing is intentionally not exposed. Letting the
* SM hang here would surface as a pairing timeout — log
* loudly and let it. */
ESP_LOGW(TAG, "OOB pairing requested but not implemented");
break;
default:
ESP_LOGW(TAG, "unexpected passkey action %d",
event->passkey.params.action);
break;
}
return 0;
case BLE_GAP_EVENT_MTU:
ESP_LOGI(TAG, "mtu=%d (conn=%d)",
event->mtu.value, event->mtu.conn_handle);
return 0;
case BLE_GAP_EVENT_SUBSCRIBE:
ESP_LOGI(TAG, "subscribe attr=%d cur_notify=%d",
event->subscribe.attr_handle, event->subscribe.cur_notify);
if (event->subscribe.attr_handle == s_tx_val_handle) {
bool sub = (event->subscribe.cur_notify != 0);
/* Edge-trigger so a redundant CCCD write (same value
* twice) doesn't fire two SUBSCRIBED events. */
if (sub != s_subscribed) {
s_subscribed = sub;
emit_evt(&(ble_uart_evt_t){
.id = BLE_UART_EVT_SUBSCRIBED,
.subscribed = { .subscribed = sub },
});
}
}
return 0;
default:
return 0;
}
}
/* ===== Pairing replies ================================================ */
/* Shared body for both reply APIs — looks at the pending state, builds
* the matching ble_sm_io payload, and injects it. `expected_action` is
* BLE_SM_IOACT_INPUT for passkey_reply and BLE_SM_IOACT_NUMCMP for
* compare_reply; calling the wrong API for the in-flight request
* returns ENOTCONN (treated as "no such request waiting"). */
static int do_pairing_reply(uint8_t expected_action,
uint32_t passkey,
bool numcmp_accept)
{
/* Snapshot the volatile fields once. The host task may clear them
* at any moment (disconnect / enc_change), and we want a coherent
* decision below. */
uint16_t conn = s_pending_io_conn;
uint8_t action = s_pending_io_action;
if (conn == BLE_HS_CONN_HANDLE_NONE || action != expected_action) {
return BLE_UART_ENOTCONN;
}
struct ble_sm_io io = { .action = expected_action };
if (expected_action == BLE_SM_IOACT_INPUT) {
io.passkey = passkey;
} else { /* BLE_SM_IOACT_NUMCMP */
io.numcmp_accept = numcmp_accept ? 1 : 0;
}
/* Clear pending BEFORE inject so a re-entrant on_event triggered
* by inject_io doesn't see stale state. If inject fails the
* request is gone anyway (the SM will time out from the central's
* side), so leaving it cleared is the right move. */
s_pending_io_conn = BLE_HS_CONN_HANDLE_NONE;
int rc = ble_sm_inject_io(conn, &io);
if (rc != 0) {
ESP_LOGW(TAG, "ble_sm_inject_io(%s) rc=%d",
expected_action == BLE_SM_IOACT_INPUT ? "INPUT" : "NUMCMP",
rc);
/* ENOTCONN from NimBLE means the conn vanished between the
* snapshot and the inject — surface that to the caller as
* such; everything else is a stack-internal failure. */
return (rc == BLE_HS_ENOTCONN) ? BLE_UART_ENOTCONN : BLE_UART_EFAIL;
}
return BLE_UART_OK;
}
int ble_uart_passkey_reply(uint32_t passkey)
{
if (passkey > 999999) {
return BLE_UART_EINVAL;
}
return do_pairing_reply(BLE_SM_IOACT_INPUT, passkey, false);
}
int ble_uart_compare_reply(bool match)
{
return do_pairing_reply(BLE_SM_IOACT_NUMCMP, 0, match);
}
/* ===== Host plumbing =================================================== */
static void on_reset(int reason)
{
ESP_LOGE(TAG, "Resetting NimBLE state; reason=%d", reason);
}
static void on_sync(void)
{
int rc = ble_hs_util_ensure_addr(0);
assert(rc == 0);
rc = ble_hs_id_infer_auto(0, &s_own_addr_type);
if (rc != 0) {
ESP_LOGE(TAG, "infer addr type rc=%d", rc);
return;
}
uint8_t addr[6] = {0};
ble_hs_id_copy_addr(s_own_addr_type, addr, NULL);
ESP_LOGI(TAG, "addr=%02x:%02x:%02x:%02x:%02x:%02x",
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
start_advertising();
}
static void nimble_host_task(void *param)
{
(void)param;
ESP_LOGI(TAG, "BLE host task started");
nimble_port_run();
/* Self-delete instead of nimble_port_freertos_deinit(): do_close()'s
* nimble_port_stop() returns once port_run() exits in this task, but
* this task is still running. A quick ble_uart_open() may already have
* updated the port layer's host_task_h; freertos_deinit() would
* vTaskDelete(host_task_h) and kill the new host task. */
vTaskDelete(NULL);
}
/* ===== Public lifecycle ================================================ */
/* Resolved view of cfg.encrypted + the per-feature overrides
* (cfg.sc / cfg.bonding / cfg.mitm / cfg.io_cap). Used in install()
* to drive both ble_hs_cfg.sm_* and the GATT-table builder. */
struct sec_policy {
bool sc; /* LE Secure Connections */
bool bonding; /* persist LTK in NVS */
bool mitm; /* require authentication */
bool link_enc; /* derived: sc || bonding || mitm */
uint8_t sm_io_cap; /* NimBLE BLE_HS_IO_* */
};
static int resolve_sec_policy(const ble_uart_config_t *cfg,
struct sec_policy *out)
{
const ble_uart_security_t *sec = &cfg->security;
/* Range-check the public enums up front. Accepting (say) a
* dangling 99 here would propagate to ble_hs_cfg as garbage and
* the SM would refuse pairing for non-obvious reasons. */
if ((unsigned)sec->sc > BLE_UART_SEC_ON
|| (unsigned)sec->bonding > BLE_UART_SEC_ON
|| (unsigned)sec->mitm > BLE_UART_SEC_ON
|| (unsigned)sec->io_cap > BLE_UART_IO_CAP_KEYBOARD_DISPLAY) {
return BLE_UART_EINVAL;
}
/* Input-capable IO caps fire BLE_UART_EVT_PASSKEY_REQUEST or
* BLE_UART_EVT_NUMERIC_COMPARE and need an application reply via
* ble_uart_passkey_reply / ble_uart_compare_reply. With on_event
* NULL the caller would never see the request and pairing would
* silently stall until the SM times out — fail synchronously.
*
* This checks the *configured* io_cap, not the value resolved
* below. AUTO and DISPLAY_ONLY are excluded on purpose: AUTO with
* mitm=ON becomes DisplayOnly; Passkey Display (BLE_SM_IOACT_DISP)
* is satisfied inside gap_event via ble_sm_inject_io with no app
* reply. BLE_UART_EVT_PASSKEY_DISPLAY is additive when on_event is
* set; with on_event NULL emit_evt() drops it and pairing still
* completes. */
if (cfg->on_event == NULL
&& (sec->io_cap == BLE_UART_IO_CAP_KEYBOARD_ONLY
|| sec->io_cap == BLE_UART_IO_CAP_DISPLAY_YES_NO
|| sec->io_cap == BLE_UART_IO_CAP_KEYBOARD_DISPLAY)) {
return BLE_UART_EINVAL;
}
/* AUTO inherits the bit from cfg.encrypted; OFF / ON override. */
bool preset = cfg->encrypted;
out->sc = (sec->sc == BLE_UART_SEC_AUTO) ? preset
: (sec->sc == BLE_UART_SEC_ON);
out->bonding = (sec->bonding == BLE_UART_SEC_AUTO) ? preset
: (sec->bonding == BLE_UART_SEC_ON);
out->mitm = (sec->mitm == BLE_UART_SEC_AUTO) ? preset
: (sec->mitm == BLE_UART_SEC_ON);
/* "Link will be encrypted" iff the SM runs at all — and the SM
* runs whenever any of these three bits is set. Pairing-without-
* bonding still encrypts the live link with a session LTK. */
out->link_enc = out->sc || out->bonding || out->mitm;
/* IO capability: AUTO picks the minimum that lets the resolved
* MITM bit succeed; the explicit values map straight to the
* NimBLE BLE_HS_IO_* constants used by the SM. */
switch (sec->io_cap) {
case BLE_UART_IO_CAP_DISPLAY_ONLY:
out->sm_io_cap = BLE_HS_IO_DISPLAY_ONLY;
break;
case BLE_UART_IO_CAP_NO_INPUT_OUTPUT:
out->sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT;
break;
case BLE_UART_IO_CAP_KEYBOARD_ONLY:
out->sm_io_cap = BLE_HS_IO_KEYBOARD_ONLY;
break;
case BLE_UART_IO_CAP_DISPLAY_YES_NO:
out->sm_io_cap = BLE_HS_IO_DISPLAY_YESNO;
break;
case BLE_UART_IO_CAP_KEYBOARD_DISPLAY:
out->sm_io_cap = BLE_HS_IO_KEYBOARD_DISPLAY;
break;
case BLE_UART_IO_CAP_AUTO:
default:
out->sm_io_cap = out->mitm ? BLE_HS_IO_DISPLAY_ONLY
: BLE_HS_IO_NO_INPUT_OUTPUT;
break;
}
/* Just Works (NoInputNoOutput) cannot satisfy MITM — the SM
* would reject pairing in flight. Catch it synchronously here. */
if (out->mitm && out->sm_io_cap == BLE_HS_IO_NO_INPUT_OUTPUT) {
return BLE_UART_EINVAL;
}
return BLE_UART_OK;
}
int ble_uart_install(const ble_uart_config_t *cfg)
{
if (s_installed) {
ESP_LOGW(TAG, "ble_uart_install called twice; ignoring");
return BLE_UART_EALREADY;
}
if (cfg != NULL) {
s_cfg = *cfg;
} else {
memset(&s_cfg, 0, sizeof(s_cfg));
}
/* Validate device_name length up front. Beyond
* BLE_UART_DEVICE_NAME_MAX the default-path advertising would
* silently fail at adv_set_fields time; surfacing the error here
* is much friendlier. strnlen with cap+1 also stops a missing-NUL
* caller buffer from running into uninitialised memory. */
if (s_cfg.device_name != NULL) {
size_t nlen = strnlen(s_cfg.device_name, BLE_UART_DEVICE_NAME_MAX + 1);
if (nlen > BLE_UART_DEVICE_NAME_MAX) {
ESP_LOGE(TAG, "device_name too long: > %u bytes",
(unsigned)BLE_UART_DEVICE_NAME_MAX);
memset(&s_cfg, 0, sizeof(s_cfg));
return BLE_UART_EINVAL;
}
}
/* Validate caller-supplied advertising payloads up front so an
* oversized buffer fails the install instead of corrupting the
* adv packet at start time (where errors only surface in logs).
* (NULL + len>0 is also rejected — almost always a caller bug.) */
if (s_cfg.adv_data_len > BLE_UART_ADV_DATA_MAX
|| (s_cfg.adv_data == NULL && s_cfg.adv_data_len > 0)) {
ESP_LOGE(TAG, "bad adv_data: ptr=%p len=%u (max=%u)",
s_cfg.adv_data,
(unsigned)s_cfg.adv_data_len,
(unsigned)BLE_UART_ADV_DATA_MAX);
memset(&s_cfg, 0, sizeof(s_cfg));
return BLE_UART_EINVAL;
}
if (s_cfg.scan_rsp_data_len > BLE_UART_SCAN_RSP_DATA_MAX
|| (s_cfg.scan_rsp_data == NULL && s_cfg.scan_rsp_data_len > 0)) {
ESP_LOGE(TAG, "bad scan_rsp_data: ptr=%p len=%u (max=%u)",
s_cfg.scan_rsp_data,
(unsigned)s_cfg.scan_rsp_data_len,
(unsigned)BLE_UART_SCAN_RSP_DATA_MAX);
memset(&s_cfg, 0, sizeof(s_cfg));
return BLE_UART_EINVAL;
}
/* Resolve cfg.encrypted + per-feature overrides into a flat
* policy. Validation (out-of-range enums + impossible MITM/IO
* combination) happens up front so install() rejects bad cfgs
* synchronously, before any host-stack resources are allocated. */
struct sec_policy pol;
int srv = resolve_sec_policy(&s_cfg, &pol);
if (srv != BLE_UART_OK) {
ESP_LOGE(TAG, "bad security cfg: encrypted=%d sc=%d bonding=%d "
"mitm=%d io_cap=%d",
(int)s_cfg.encrypted,
(int)s_cfg.security.sc, (int)s_cfg.security.bonding,
(int)s_cfg.security.mitm, (int)s_cfg.security.io_cap);
memset(&s_cfg, 0, sizeof(s_cfg));
return srv;
}
s_link_encrypted = pol.link_enc;
s_mitm_required = pol.mitm;
/* Copy raw payloads now (caller's pointers may not outlive install).
* For adv_data we also prepend the 3-byte Flags AD ourselves —
* the controller-visible Flags element is library-controlled and
* not part of what the application owns. */
s_adv_data_len = 0;
s_scan_rsp_data_len = 0;
if (s_cfg.adv_data != NULL && s_cfg.adv_data_len > 0) {
s_adv_data[0] = 0x02; /* AD length */
s_adv_data[1] = 0x01; /* AD type: Flags */
s_adv_data[2] = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
memcpy(s_adv_data + 3, s_cfg.adv_data, s_cfg.adv_data_len);
s_adv_data_len = (uint8_t)(3 + s_cfg.adv_data_len);
}
if (s_cfg.scan_rsp_data != NULL && s_cfg.scan_rsp_data_len > 0) {
memcpy(s_scan_rsp_data, s_cfg.scan_rsp_data, s_cfg.scan_rsp_data_len);
s_scan_rsp_data_len = (uint8_t)s_cfg.scan_rsp_data_len;
}
/* Drop the pointers — install must not retain caller buffers. */
s_cfg.adv_data = NULL;
s_cfg.scan_rsp_data = NULL;
esp_err_t err = nimble_port_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "nimble_port_init rc=%d", err);
return BLE_UART_EFAIL;
}
#if MYNEWT_VAL(BLE_HS_AUTO_START)
s_hs_auto_start_pending = true;
#endif
/* From here every failure must `goto fail` so nimble_port_deinit()
* runs — leaving the port allocated breaks the next install(). */
ble_hs_cfg.reset_cb = on_reset;
ble_hs_cfg.sync_cb = on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
ble_hs_cfg.gatts_register_cb = register_cb;
/* Apply the resolved security policy. NimBLE checks sm_bonding
* before consulting the key-distribution masks, so it's safe to
* leave them set unconditionally — they're a no-op when bonding=0. */
if (pol.link_enc) {
ble_hs_cfg.sm_io_cap = pol.sm_io_cap;
ble_hs_cfg.sm_sc = pol.sc ? 1 : 0;
ble_hs_cfg.sm_bonding = pol.bonding ? 1 : 0;
ble_hs_cfg.sm_mitm = pol.mitm ? 1 : 0;
ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
} else {
/* Fully plaintext: SM disabled, no keys exchanged. */
ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT;
ble_hs_cfg.sm_sc = 0;
ble_hs_cfg.sm_bonding = 0;
ble_hs_cfg.sm_mitm = 0;
}
#if NIMBLE_BLE_CONNECT
ble_svc_gap_init();
#endif
ble_svc_gatt_init();
/* Cache the device name into our own buffer (caller's pointer may
* not outlive this call; also avoids the GAP-service stub path
* which returns NULL from ble_svc_gap_device_name()). */
int rc = 0;
if (s_cfg.device_name != NULL) {
strncpy(s_dev_name, s_cfg.device_name, sizeof(s_dev_name) - 1);
s_dev_name[sizeof(s_dev_name) - 1] = '\0';
s_cfg.device_name = NULL;
/* Best-effort: also set in the GAP service for peer reads.
* Returns -1 on the stub path — fine, we already cached locally. */
rc = ble_svc_gap_device_name_set(s_dev_name);
if (rc != 0) {
ESP_LOGI(TAG, "ble_svc_gap_device_name_set rc=%d (GAP service stubbed?)",
rc);
rc = 0;
}
} else {
s_dev_name[0] = '\0';
}
rc = register_uart_gatt_svc();
if (rc != BLE_UART_OK) {
goto fail;
}
s_gatts_needs_readd = false;
/* Wire up the NVS bond store (requires CONFIG_BT_NIMBLE_NVS_PERSIST=y).
* Done here — not in open() — so bond-management APIs
* (ble_uart_get_bond_count / clear_bonds / remove_peer) work
* between install and open, letting callers wipe stale bonds
* before the first advertising window opens. */
ble_store_config_init();
s_installed = true;
return BLE_UART_OK;
fail:
#if MYNEWT_VAL(BLE_HS_AUTO_START)
s_hs_auto_start_pending = false;
#endif
nimble_port_deinit();
memset(&s_cfg, 0, sizeof(s_cfg));
return xlate_rc(rc);
}
int ble_uart_open(void)
{
if (!s_installed) {
ESP_LOGE(TAG, "ble_uart_open before ble_uart_install");
return BLE_UART_EINVAL;
}
if (s_opened) {
ESP_LOGW(TAG, "ble_uart_open called twice; ignoring");
return BLE_UART_EALREADY;
}
if (s_gatts_needs_readd) {
int grc = reregister_gatt_svcs_after_close();
if (grc != BLE_UART_OK) {
return grc;
}
s_gatts_needs_readd = false;
}
/* Spawn host task, then queue host start. on_sync() starts advertising
* once the controller sync completes.
*
* With BLE_HS_AUTO_START (default), install()'s nimble_port_init()
* already queued a one-shot start event — open() must not sched_start()
* again or ble_hs_start() returns BLE_HS_EALREADY and the host task
* asserts. After close()'s nimble_port_stop() the host is OFF and no
* AUTO_START event remains, so every later open() must sched_start(). */
nimble_port_freertos_init(nimble_host_task);
#if MYNEWT_VAL(BLE_HS_AUTO_START)
if (!s_hs_auto_start_pending) {
ble_hs_sched_start();
}
s_hs_auto_start_pending = false;
#else
ble_hs_sched_start();
#endif
s_opened = true;
return BLE_UART_OK;
}
/* Body of ble_uart_close(); also called directly by the close-async
* worker, which has already latched s_closing itself. The public
* wrapper below uses s_closing to reject a sync close that races
* with an in-flight async close. */
static int do_close(void)
{
if (!s_opened) {
return BLE_UART_EALREADY;
}
/* Latch first so GAP events stop re-arming advertising. */
s_shutting_down = true;
/* Only stop adv if it's still running. Undirected adv auto-stops
* at the LL on connect, so calling adv_stop while connected just
* burns one HCI cmd that NimBLE answers with BLE_HS_EALREADY.
* Mirrors the Bluedroid backend's `if (s_adv_active)` gate. */
int rc = 0;
if (ble_gap_adv_active()) {
rc = ble_gap_adv_stop();
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGW(TAG, "adv_stop rc=%d", rc);
}
}
/* Graceful disconnect: wait up to 500 ms for the disconnect event
* so the peer sees a proper LL_TERMINATE_IND, not a controller-yank. */
if (s_conn_handle != BLE_HS_CONN_HANDLE_NONE) {
rc = ble_gap_terminate(s_conn_handle, BLE_ERR_REM_USER_CONN_TERM);
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGW(TAG, "ble_gap_terminate rc=%d", rc);
}
for (int i = 0; i < 50 && s_conn_handle != BLE_HS_CONN_HANDLE_NONE; i++) {
vTaskDelay(pdMS_TO_TICKS(10));
}
if (s_conn_handle != BLE_HS_CONN_HANDLE_NONE) {
ESP_LOGW(TAG, "disconnect timed out; tearing down anyway");
}
}
/* nimble_port_stop() waits for port_run() to exit in the host task;
* the host then vTaskDelete(NULL) — no join after stop returns. */
rc = nimble_port_stop();
if (rc != 0) {
ESP_LOGE(TAG, "nimble_port_stop rc=%d", rc);
s_shutting_down = false;
return BLE_UART_EFAIL;
}
/* Host stop frees the svc-def pointer array; stale ATT rows can remain
* until cleared. Use the public ble_gatts_reset() only (no NimBLE
* source edits) and re-queue svc defs on the next open(). */
rc = ble_gatts_reset();
if (rc != 0) {
ESP_LOGW(TAG, "ble_gatts_reset rc=%d", rc);
}
s_tx_val_handle = 0;
s_gatts_needs_readd = true;
s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
s_subscribed = false;
s_opened = false;
s_shutting_down = false;
return BLE_UART_OK;
}
int ble_uart_close(void)
{
/* If a ble_uart_close_async() worker is in flight, the close
* sequence is already running on the worker's task — let it
* finish rather than racing it from here. The worker drives
* s_opened to false on its own, so the next sync close after
* the worker drains will get the natural !s_opened EALREADY. */
if (s_closing) {
return BLE_UART_EALREADY;
}
return do_close();
}
/* ===== Async close ==================================================== */
/* Background worker spawned by ble_uart_close_async(). Lives just
* long enough to run the synchronous close path (which itself can
* block on the disconnect timeout and on nimble_port_stop()), then
* fires the completion event and self-deletes.
*
* Spawned as a separate task — not a deferred ble_npl callout — so
* that nimble_port_stop() can join the host task without us being
* the host task. */
static void close_async_task(void *arg)
{
(void)arg;
/* Bypass the s_closing gate in ble_uart_close(): we ARE the
* in-flight async close that gate is meant to protect against. */
int rc = do_close();
if (rc != BLE_UART_OK && rc != BLE_UART_EALREADY) {
ESP_LOGW(TAG, "close_async: do_close rc=%d", rc);
}
/* Deliver CLOSED on the worker task. Applications must defer
* ble_uart_uninstall() to another task (PORTING.md §5.3.2).
* Concurrent uninstall() may clear s_cfg while we read on_event. */
emit_evt(&(ble_uart_evt_t){
.id = BLE_UART_EVT_CLOSED,
.closed = { .status = rc },
});
s_closing = false;
vTaskDelete(NULL);
}
int ble_uart_close_async(void)
{
/* Same-state checks as the synchronous variant: nothing to close
* if we never opened, and no point spawning a second worker if
* the first hasn't drained yet. */
if (!s_opened || s_closing) {
return BLE_UART_EALREADY;
}
/* Latch BEFORE spawning so a racing caller (different task) sees
* the in-flight state immediately and gets EALREADY. */
s_closing = true;
/* 3 KB is comfortably more than the close path uses (mostly small
* GAP/HCI helpers + a 50×10ms vTaskDelay loop); bump if you wedge
* a heavy on_event handler between adv_stop and CLOSED. */
BaseType_t ok = xTaskCreate(close_async_task, "ble_close",
3072, NULL,
tskIDLE_PRIORITY + 2, NULL);
if (ok != pdPASS) {
s_closing = false;
return BLE_UART_ENOMEM;
}
return BLE_UART_OK;
}
/* ===== Bond management ================================================ */
/* Public API uses big-endian bytes (bytes[0] = MSB) but NimBLE stores
* addresses little-endian (val[0] = LSB). Caller must supply
* BLE_UART_ADDR_TYPE_PUBLIC/RANDOM (0/1); the bond store keys on
* those same identity types. */
static void to_nimble_addr(const ble_uart_addr_t *src, ble_addr_t *dst)
{
dst->type = src->type;
for (int i = 0; i < 6; i++) {
dst->val[i] = src->bytes[5 - i];
}
}
#if MYNEWT_VAL(BLE_STORE_MAX_BONDS) > 0
/* ble_store_util_bonded_peers enumerates OUR_SEC (unique peer_addr).
* Heap-allocate the scratch buffer so callers on small-stack tasks are
* safe regardless of CONFIG_BT_NIMBLE_MAX_BONDS. */
static int bonded_peers_unique_count(size_t *out_count)
{
const int max_peers = MYNEWT_VAL(BLE_STORE_MAX_BONDS);
ble_addr_t *peer_addrs = calloc((size_t)max_peers, sizeof(*peer_addrs));
if (peer_addrs == NULL) {
return BLE_UART_ENOMEM;
}
int num_peers = 0;
int rc = ble_store_util_bonded_peers(peer_addrs, &num_peers, max_peers);
free(peer_addrs);
if (rc != 0) {
ESP_LOGW(TAG, "ble_store_util_bonded_peers rc=%d", rc);
return xlate_rc(rc);
}
*out_count = (size_t)num_peers;
return BLE_UART_OK;
}
#endif
int ble_uart_get_bond_count(size_t *out_count)
{
if (out_count == NULL || !s_installed) {
return BLE_UART_EINVAL;
}
#if MYNEWT_VAL(BLE_STORE_MAX_BONDS) > 0
return bonded_peers_unique_count(out_count);
#else
*out_count = 0;
return BLE_UART_OK;
#endif
}
int ble_uart_get_bonded_peers(ble_uart_addr_t *out, size_t cap, size_t *out_count)
{
if (out_count == NULL || !s_installed
|| (out == NULL && cap > 0)) {
return BLE_UART_EINVAL;
}
#if MYNEWT_VAL(BLE_STORE_MAX_BONDS) <= 0
*out_count = 0;
return BLE_UART_OK;
#else
/* ble_store_util_bonded_peers enumerates OUR_SEC (unique peer_addr).
* Size the buffer to BLE_STORE_MAX_BONDS — the compile-time cap —
* not PEER_SEC/OUR_SEC raw entry counts (they can disagree). */
const int max_peers = MYNEWT_VAL(BLE_STORE_MAX_BONDS);
if (cap == 0) {
return bonded_peers_unique_count(out_count);
}
int n_our = 0;
int rc = ble_store_util_count(BLE_STORE_OBJ_TYPE_OUR_SEC, &n_our);
if (rc != 0) {
ESP_LOGW(TAG, "ble_store_util_count rc=%d", rc);
return xlate_rc(rc);
}
if (n_our <= 0) {
*out_count = 0;
return BLE_UART_OK;
}
/* Bonded peers in NimBLE's native LE byte order. Heap-allocate to
* keep the host task's stack untouched even when many peers exist. */
ble_addr_t *tmp = calloc((size_t)max_peers, sizeof(*tmp));
if (tmp == NULL) {
return BLE_UART_ENOMEM;
}
int got = 0;
rc = ble_store_util_bonded_peers(tmp, &got, max_peers);
if (rc != 0) {
ESP_LOGW(TAG, "ble_store_util_bonded_peers rc=%d", rc);
free(tmp);
return xlate_rc(rc);
}
/* Copy at most cap entries into the caller's buffer, flipping
* NimBLE's LE byte order back to our public big-endian convention
* and narrowing addr types to BLE_UART_ADDR_TYPE_* . */
size_t to_copy = ((size_t)got < cap) ? (size_t)got : cap;
for (size_t i = 0; i < to_copy; i++) {
from_nimble_addr(&tmp[i], &out[i]);
}
free(tmp);
*out_count = (size_t)got;
return BLE_UART_OK;
#endif
}
int ble_uart_remove_peer(const ble_uart_addr_t *peer)
{
if (peer == NULL || !s_installed) {
return BLE_UART_EINVAL;
}
ble_addr_t addr;
to_nimble_addr(peer, &addr);
int rc = ble_store_util_delete_peer(&addr);
if (rc != 0) {
ESP_LOGW(TAG, "ble_store_util_delete_peer rc=%d", rc);
return xlate_rc(rc);
}
return BLE_UART_OK;
}
int ble_uart_clear_bonds(void)
{
if (!s_installed) {
return BLE_UART_EINVAL;
}
/* Wipes peer LTK + our LTK + persisted CCCD (and a few NimBLE
* internal records). Doesn't touch our s_cfg or any other NVS
* namespace. */
int rc = ble_store_clear();
if (rc != 0) {
ESP_LOGW(TAG, "ble_store_clear rc=%d", rc);
return xlate_rc(rc);
}
return BLE_UART_OK;
}
/* ===== Uninstall ====================================================== */
int ble_uart_uninstall(void)
{
if (!s_installed) {
return BLE_UART_EALREADY;
}
/* If a ble_uart_close_async() worker is still draining, poll s_closing
* for up to ~5 s before touching shared state. On timeout, teardown
* continues anyway — applications must follow PORTING.md §5.3.2 so
* uninstall runs only after the worker has finished. */
for (int i = 0; i < 500 && s_closing; i++) {
vTaskDelay(pdMS_TO_TICKS(10));
}
if (s_closing) {
ESP_LOGW(TAG, "uninstall: close_async worker still running, "
"tearing down anyway");
}
/* Best-effort cleanup. Do NOT early-return on a per-step failure:
* leaving s_installed=true with partially torn-down NimBLE state
* makes the module unrecoverable (can't re-install, can't retry
* uninstall cleanly). Mirror the Bluedroid backend: record the
* first error, keep tearing down, and always wipe our state. */
int first_rc = BLE_UART_OK;
if (s_opened) {
int rc = ble_uart_close();
if (rc != BLE_UART_OK && rc != BLE_UART_EALREADY) {
ESP_LOGE(TAG, "ble_uart_close rc=%d", rc);
if (first_rc == BLE_UART_OK) {
first_rc = rc;
}
}
}
/* Best-effort: even if port_deinit fails, wipe our state anyway —
* otherwise s_installed stays true and the module is unrecoverable
* (can't re-install, can't retry uninstall cleanly). */
esp_err_t err = nimble_port_deinit();
if (err != ESP_OK) {
ESP_LOGE(TAG, "nimble_port_deinit rc=%d", err);
if (first_rc == BLE_UART_OK) {
first_rc = BLE_UART_EFAIL;
}
}
memset(&s_cfg, 0, sizeof(s_cfg));
s_dev_name[0] = '\0';
s_tx_val_handle = 0;
s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
s_subscribed = false;
s_own_addr_type = 0;
s_shutting_down = false;
s_closing = false;
s_installed = false;
s_opened = false;
s_gatts_needs_readd = false;
#if MYNEWT_VAL(BLE_HS_AUTO_START)
s_hs_auto_start_pending = false;
#endif
s_adv_data_len = 0;
s_scan_rsp_data_len = 0;
s_link_encrypted = false;
s_mitm_required = false;
s_pending_io_conn = BLE_HS_CONN_HANDLE_NONE;
s_pending_io_action = 0;
return first_rc;
}
#endif /* CONFIG_BT_NIMBLE_ENABLED */