mirror of
https://github.com/espressif/esp-idf.git
synced 2026-07-23 03:13:01 +03:00
654 lines
22 KiB
C
654 lines
22 KiB
C
/*
|
|
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
|
*
|
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
|
*
|
|
* 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 <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;
|
|
}
|
|
}
|
|
|
|
/* 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 → 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). 32B
|
|
* covers the BLE 31-byte adv-payload limit + NUL. */
|
|
#define DEV_NAME_MAX 32
|
|
|
|
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;
|
|
static bool s_shutting_down; /* gates auto-readvertise during close */
|
|
static uint8_t s_own_addr_type;
|
|
|
|
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). */
|
|
#define CHR_FLAG_RW_ENC (BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_READ_AUTHEN | \
|
|
BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_WRITE_AUTHEN)
|
|
#define CHR_FLAG_NOTIFY_ENC (BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \
|
|
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 encrypted)
|
|
{
|
|
/* `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 = encrypted ? CHR_FLAG_RW_ENC : 0;
|
|
ble_gatt_chr_flags notify_enc = encrypted ? CHR_FLAG_NOTIFY_ENC : 0;
|
|
|
|
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 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_LOGI(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_LOGI(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; }
|
|
|
|
/* ===== Advertising ==================================================== */
|
|
|
|
static int start_advertising(void)
|
|
{
|
|
/* 31-byte primary adv can't hold flags + tx_pwr + name + 128-bit
|
|
* UUID together, so split: primary = flags+tx_pwr+name,
|
|
* scan rsp = 128-bit service UUID. */
|
|
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,
|
|
.tx_pwr_lvl_is_present = 1,
|
|
.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO,
|
|
/* 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,
|
|
};
|
|
int rc = ble_gap_adv_set_fields(&adv);
|
|
if (rc != 0) {
|
|
ESP_LOGE(TAG, "adv_set_fields rc=%d (name too long?)", rc);
|
|
return rc;
|
|
}
|
|
|
|
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,
|
|
¶ms, gap_event, NULL);
|
|
if (rc != 0) {
|
|
ESP_LOGE(TAG, "adv_start rc=%d", rc);
|
|
return rc;
|
|
}
|
|
ESP_LOGI(TAG, "advertising as '%s'", name_len > 0 ? 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;
|
|
/* Start pairing immediately (rather than lazily on the
|
|
* first encrypted attribute access). */
|
|
if (s_cfg.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;
|
|
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);
|
|
if (!s_shutting_down) {
|
|
start_advertising();
|
|
}
|
|
return 0;
|
|
|
|
case BLE_GAP_EVENT_ENC_CHANGE:
|
|
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);
|
|
}
|
|
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:
|
|
if (event->passkey.params.action == 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,
|
|
};
|
|
show_passkey(pkey.passkey);
|
|
int rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
|
|
if (rc != 0) {
|
|
ESP_LOGW(TAG, "ble_sm_inject_io rc=%d", rc);
|
|
}
|
|
} else {
|
|
ESP_LOGW(TAG, "passkey action %d not handled (DisplayOnly only)",
|
|
event->passkey.params.action);
|
|
}
|
|
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) {
|
|
s_subscribed = (event->subscribe.cur_notify != 0);
|
|
}
|
|
return 0;
|
|
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* ===== 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)
|
|
{
|
|
ESP_LOGI(TAG, "BLE host task started");
|
|
nimble_port_run();
|
|
nimble_port_freertos_deinit();
|
|
}
|
|
|
|
/* ===== Public lifecycle ================================================ */
|
|
|
|
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));
|
|
}
|
|
|
|
esp_err_t err = nimble_port_init();
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "nimble_port_init rc=%d", err);
|
|
return BLE_UART_EFAIL;
|
|
}
|
|
/* 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;
|
|
|
|
/* Encrypted = LE Secure Connections + Bonding + MITM, DisplayOnly.
|
|
* Plaintext = SM disabled. */
|
|
if (s_cfg.encrypted) {
|
|
ble_hs_cfg.sm_io_cap = BLE_HS_IO_DISPLAY_ONLY;
|
|
ble_hs_cfg.sm_sc = 1;
|
|
ble_hs_cfg.sm_bonding = 1;
|
|
ble_hs_cfg.sm_mitm = 1;
|
|
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 {
|
|
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;
|
|
}
|
|
|
|
ble_svc_gap_init();
|
|
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';
|
|
}
|
|
|
|
build_gatt_table(s_cfg.encrypted);
|
|
|
|
rc = ble_gatts_count_cfg(s_svc_defs);
|
|
if (rc != 0) {
|
|
ESP_LOGE(TAG, "ble_gatts_count_cfg rc=%d", rc);
|
|
goto fail;
|
|
}
|
|
rc = ble_gatts_add_svcs(s_svc_defs);
|
|
if (rc != 0) {
|
|
ESP_LOGE(TAG, "ble_gatts_add_svcs rc=%d", rc);
|
|
goto fail;
|
|
}
|
|
|
|
s_installed = true;
|
|
return BLE_UART_OK;
|
|
|
|
fail:
|
|
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;
|
|
}
|
|
|
|
/* NVS bond store (requires CONFIG_BT_NIMBLE_NVS_PERSIST=y). */
|
|
ble_store_config_init();
|
|
|
|
/* Spawn host task; on_sync starts advertising once controller is ready. */
|
|
nimble_port_freertos_init(nimble_host_task);
|
|
s_opened = true;
|
|
return BLE_UART_OK;
|
|
}
|
|
|
|
int ble_uart_close(void)
|
|
{
|
|
if (!s_opened) {
|
|
return BLE_UART_EALREADY;
|
|
}
|
|
|
|
/* Latch first so GAP events stop re-arming advertising. */
|
|
s_shutting_down = true;
|
|
|
|
int 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_host_task self-cleans (port_freertos_deinit + delete) when
|
|
* port_run returns, so no explicit join. */
|
|
rc = nimble_port_stop();
|
|
if (rc != 0) {
|
|
ESP_LOGE(TAG, "nimble_port_stop rc=%d", rc);
|
|
s_shutting_down = false;
|
|
return BLE_UART_EFAIL;
|
|
}
|
|
|
|
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_uninstall(void)
|
|
{
|
|
if (!s_installed) {
|
|
return BLE_UART_EALREADY;
|
|
}
|
|
|
|
/* 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_installed = false;
|
|
s_opened = false;
|
|
return first_rc;
|
|
}
|
|
|
|
#endif /* CONFIG_BT_NIMBLE_ENABLED */
|