mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
1023 lines
36 KiB
C
1023 lines
36 KiB
C
/*
|
|
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
|
*
|
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
|
*
|
|
* BLE UART — Bluedroid backend. Implements the lifecycle declared in
|
|
* ble_uart.h on top of the Bluedroid host using the service-table API
|
|
* (esp_ble_gatts_create_attr_tab). Active when
|
|
* CONFIG_BT_BLUEDROID_ENABLED=y; otherwise ble_uart_nimble.c is used.
|
|
*
|
|
* Differences vs the NimBLE backend:
|
|
* - PREP/EXEC long-write reassembly is open-coded (NimBLE does it
|
|
* for us; Bluedroid hands raw fragments to the application).
|
|
* - MTU is cached locally in s_local_mtu — no Bluedroid query API.
|
|
*/
|
|
|
|
#include "sdkconfig.h"
|
|
|
|
#if CONFIG_BT_BLUEDROID_ENABLED
|
|
|
|
#include "ble_uart.h"
|
|
|
|
#include <inttypes.h>
|
|
#include <string.h>
|
|
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
|
|
#include "esp_log.h"
|
|
#include "esp_bt.h"
|
|
#include "esp_bt_main.h"
|
|
#include "esp_bt_defs.h"
|
|
#include "esp_bt_device.h"
|
|
#include "esp_gap_ble_api.h"
|
|
#include "esp_gatt_common_api.h"
|
|
#include "esp_gatts_api.h"
|
|
|
|
static const char *TAG = "ble_uart";
|
|
|
|
/* ===== Backend constants ============================================== */
|
|
|
|
#define UART_APP_ID 0x55 /* arbitrary, must be unique per profile */
|
|
#define UART_SVC_INST_ID 0 /* single service instance */
|
|
|
|
/* RX scratch capacity. See ble_uart_nimble.c for rationale. */
|
|
#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
|
|
|
|
/* ===== UUIDs =========================================================== */
|
|
|
|
const ble_uart_uuid128_t ble_uart_service_uuid = {
|
|
.bytes = { 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0,
|
|
0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e },
|
|
};
|
|
|
|
static const uint8_t s_svc_uuid_bytes[16] = {
|
|
0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0,
|
|
0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e,
|
|
};
|
|
static const uint8_t s_chr_rx_uuid[16] = {
|
|
0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0,
|
|
0x93, 0xf3, 0xa3, 0xb5, 0x02, 0x00, 0x40, 0x6e,
|
|
};
|
|
static const uint8_t s_chr_tx_uuid[16] = {
|
|
0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0,
|
|
0x93, 0xf3, 0xa3, 0xb5, 0x03, 0x00, 0x40, 0x6e,
|
|
};
|
|
|
|
/* 16-bit UUIDs used in the attr table (declarations + CCCD). */
|
|
static const uint16_t s_pri_svc_uuid = ESP_GATT_UUID_PRI_SERVICE;
|
|
static const uint16_t s_char_decl_uuid = ESP_GATT_UUID_CHAR_DECLARE;
|
|
static const uint16_t s_cccd_uuid = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
|
|
|
|
static const uint8_t s_char_prop_write_nr =
|
|
ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_WRITE_NR;
|
|
static const uint8_t s_char_prop_notify = ESP_GATT_CHAR_PROP_BIT_NOTIFY;
|
|
|
|
static const uint8_t s_cccd_default[2] = {0x00, 0x00};
|
|
|
|
/* Placeholders for the value attributes — RX is RSP_BY_APP (we own
|
|
* the response), TX is set on demand by send_indicate(). */
|
|
static uint8_t s_rx_val_placeholder[1];
|
|
static uint8_t s_tx_val_placeholder[1];
|
|
|
|
/* ===== Service-table indices ========================================== */
|
|
|
|
enum {
|
|
NUS_IDX_SVC,
|
|
NUS_IDX_RX_DECL,
|
|
NUS_IDX_RX_VAL,
|
|
NUS_IDX_TX_DECL,
|
|
NUS_IDX_TX_VAL,
|
|
NUS_IDX_TX_CCCD,
|
|
NUS_IDX_NB,
|
|
};
|
|
|
|
/* ===== State =========================================================== */
|
|
|
|
static ble_uart_config_t s_cfg;
|
|
static esp_gatt_if_t s_gatts_if = ESP_GATT_IF_NONE;
|
|
static uint16_t s_handles[NUS_IDX_NB];
|
|
/* Volatile: written from BTC task, polled from caller task. */
|
|
static volatile uint16_t s_conn_id = 0xFFFF; /* invalid sentinel */
|
|
static esp_bd_addr_t s_remote_bda;
|
|
static uint16_t s_local_mtu = 23; /* spec default */
|
|
static bool s_subscribed;
|
|
static bool s_installed;
|
|
static bool s_opened;
|
|
static bool s_shutting_down;
|
|
static volatile bool s_attr_tab_ready;
|
|
static bool s_adv_active;
|
|
|
|
/* Two-bit latch driving the "configure adv data + scan rsp before
|
|
* start_advertising" sequence. start_advertising fires only when both
|
|
* SET_COMPLETE_EVT events have cleared their bit. */
|
|
#define ADV_CONFIG_FLAG (1 << 0)
|
|
#define SCAN_RSP_CONFIG_FLAG (1 << 1)
|
|
static uint8_t s_adv_config_done;
|
|
|
|
/* Long-write accumulator (Bluedroid doesn't reassemble for us). */
|
|
static uint8_t s_rx_buf[RX_SCRATCH];
|
|
static uint16_t s_prep_len;
|
|
static bool s_prep_bad;
|
|
|
|
/* ===== Backend rc → public rc ========================================= */
|
|
|
|
static int xlate_rc(esp_err_t rc)
|
|
{
|
|
switch (rc) {
|
|
case ESP_OK: return BLE_UART_OK;
|
|
case ESP_ERR_INVALID_ARG:
|
|
case ESP_ERR_INVALID_STATE: return BLE_UART_EINVAL;
|
|
case ESP_ERR_NO_MEM: return BLE_UART_ENOMEM;
|
|
default: return BLE_UART_EFAIL;
|
|
}
|
|
}
|
|
|
|
/* ===== GATT attribute table =========================================== */
|
|
|
|
/* Permissions are patched at install time depending on cfg.encrypted. */
|
|
static esp_gatts_attr_db_t s_nus_db[NUS_IDX_NB];
|
|
|
|
static void build_attr_table(bool encrypted)
|
|
{
|
|
const esp_gatt_perm_t r_perm = encrypted
|
|
? (ESP_GATT_PERM_READ_ENC_MITM)
|
|
: (ESP_GATT_PERM_READ);
|
|
const esp_gatt_perm_t w_perm = encrypted
|
|
? (ESP_GATT_PERM_WRITE_ENC_MITM)
|
|
: (ESP_GATT_PERM_WRITE);
|
|
|
|
/* [SVC] primary service declaration */
|
|
s_nus_db[NUS_IDX_SVC] = (esp_gatts_attr_db_t){
|
|
.attr_control = {ESP_GATT_AUTO_RSP},
|
|
.att_desc = {
|
|
.uuid_length = ESP_UUID_LEN_16,
|
|
.uuid_p = (uint8_t *)&s_pri_svc_uuid,
|
|
.perm = ESP_GATT_PERM_READ,
|
|
.max_length = sizeof(s_svc_uuid_bytes),
|
|
.length = sizeof(s_svc_uuid_bytes),
|
|
.value = (uint8_t *)s_svc_uuid_bytes,
|
|
},
|
|
};
|
|
|
|
/* [RX] characteristic declaration (Write + Write-no-rsp) */
|
|
s_nus_db[NUS_IDX_RX_DECL] = (esp_gatts_attr_db_t){
|
|
.attr_control = {ESP_GATT_AUTO_RSP},
|
|
.att_desc = {
|
|
.uuid_length = ESP_UUID_LEN_16,
|
|
.uuid_p = (uint8_t *)&s_char_decl_uuid,
|
|
.perm = ESP_GATT_PERM_READ,
|
|
.max_length = sizeof(s_char_prop_write_nr),
|
|
.length = sizeof(s_char_prop_write_nr),
|
|
.value = (uint8_t *)&s_char_prop_write_nr,
|
|
},
|
|
};
|
|
|
|
/* [RX value] — RSP_BY_APP because Bluedroid can't auto-respond
|
|
* to PREP_WRITE_REQ; we own the response path. */
|
|
s_nus_db[NUS_IDX_RX_VAL] = (esp_gatts_attr_db_t){
|
|
.attr_control = {ESP_GATT_RSP_BY_APP},
|
|
.att_desc = {
|
|
.uuid_length = ESP_UUID_LEN_128,
|
|
.uuid_p = (uint8_t *)s_chr_rx_uuid,
|
|
.perm = w_perm,
|
|
.max_length = RX_SCRATCH,
|
|
.length = sizeof(s_rx_val_placeholder),
|
|
.value = s_rx_val_placeholder,
|
|
},
|
|
};
|
|
|
|
/* [TX] characteristic declaration (Notify only) */
|
|
s_nus_db[NUS_IDX_TX_DECL] = (esp_gatts_attr_db_t){
|
|
.attr_control = {ESP_GATT_AUTO_RSP},
|
|
.att_desc = {
|
|
.uuid_length = ESP_UUID_LEN_16,
|
|
.uuid_p = (uint8_t *)&s_char_decl_uuid,
|
|
.perm = ESP_GATT_PERM_READ,
|
|
.max_length = sizeof(s_char_prop_notify),
|
|
.length = sizeof(s_char_prop_notify),
|
|
.value = (uint8_t *)&s_char_prop_notify,
|
|
},
|
|
};
|
|
|
|
/* [TX value] — TX characteristic is notify-only, so the prop above doesn't
|
|
* advertise READ; perm only matters if a client tries READ anyway. */
|
|
s_nus_db[NUS_IDX_TX_VAL] = (esp_gatts_attr_db_t){
|
|
.attr_control = {ESP_GATT_AUTO_RSP},
|
|
.att_desc = {
|
|
.uuid_length = ESP_UUID_LEN_128,
|
|
.uuid_p = (uint8_t *)s_chr_tx_uuid,
|
|
.perm = r_perm,
|
|
.max_length = RX_SCRATCH,
|
|
.length = sizeof(s_tx_val_placeholder),
|
|
.value = s_tx_val_placeholder,
|
|
},
|
|
};
|
|
|
|
/* [TX CCCD] — central writes 0x0001 to subscribe, 0x0000 to stop.
|
|
* w_perm enforces the encryption requirement. */
|
|
s_nus_db[NUS_IDX_TX_CCCD] = (esp_gatts_attr_db_t){
|
|
.attr_control = {ESP_GATT_AUTO_RSP},
|
|
.att_desc = {
|
|
.uuid_length = ESP_UUID_LEN_16,
|
|
.uuid_p = (uint8_t *)&s_cccd_uuid,
|
|
.perm = ESP_GATT_PERM_READ | w_perm,
|
|
.max_length = sizeof(uint16_t),
|
|
.length = sizeof(s_cccd_default),
|
|
.value = (uint8_t *)s_cccd_default,
|
|
},
|
|
};
|
|
}
|
|
|
|
/* ===== Advertising ==================================================== */
|
|
|
|
static esp_ble_adv_data_t s_adv_data = {
|
|
.set_scan_rsp = false,
|
|
.include_name = true,
|
|
.include_txpower = true,
|
|
.min_interval = 0,
|
|
.max_interval = 0,
|
|
.appearance = 0x00,
|
|
.manufacturer_len = 0,
|
|
.p_manufacturer_data = NULL,
|
|
.service_data_len = 0,
|
|
.p_service_data = NULL,
|
|
.service_uuid_len = 0,
|
|
.p_service_uuid = NULL,
|
|
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
|
|
};
|
|
|
|
/* Scan response = 128-bit UART service UUID. Splitting it off the primary payload
|
|
* leaves room for name + tx_pwr in the 31-byte primary. */
|
|
static esp_ble_adv_data_t s_scan_rsp_data = {
|
|
.set_scan_rsp = true,
|
|
.include_name = false,
|
|
.include_txpower = false,
|
|
.service_uuid_len = sizeof(s_svc_uuid_bytes),
|
|
.p_service_uuid = (uint8_t *)s_svc_uuid_bytes,
|
|
};
|
|
|
|
static esp_ble_adv_params_t s_adv_params = {
|
|
.adv_int_min = 0xa0, /* 100 ms */
|
|
.adv_int_max = 0xa0,
|
|
.adv_type = ADV_TYPE_IND,
|
|
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
|
|
.channel_map = ADV_CHNL_ALL,
|
|
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
|
|
};
|
|
|
|
static int start_advertising(void)
|
|
{
|
|
esp_err_t rc = esp_ble_gap_start_advertising(&s_adv_params);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "adv_start rc=%s", esp_err_to_name(rc));
|
|
return xlate_rc(rc);
|
|
}
|
|
s_adv_active = true;
|
|
return BLE_UART_OK;
|
|
}
|
|
|
|
/* Push adv data + scan response. start_advertising is triggered from
|
|
* the matching SET_COMPLETE_EVT once both halves are realised.
|
|
*
|
|
* On a sync failure of either config call, the matching SET_COMPLETE_EVT
|
|
* will NEVER fire — so we must wipe the latch entirely (not just clear
|
|
* one bit) to avoid (a) advertising silently lost, or (b) the other
|
|
* half's event firing start_advertising with a half-configured payload. */
|
|
static int configure_advertising(void)
|
|
{
|
|
s_adv_config_done = ADV_CONFIG_FLAG | SCAN_RSP_CONFIG_FLAG;
|
|
|
|
esp_err_t rc = esp_ble_gap_config_adv_data(&s_adv_data);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "config_adv_data rc=%s", esp_err_to_name(rc));
|
|
s_adv_config_done = 0;
|
|
return xlate_rc(rc);
|
|
}
|
|
rc = esp_ble_gap_config_adv_data(&s_scan_rsp_data);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "config_scan_rsp rc=%s", esp_err_to_name(rc));
|
|
/* adv_data is in flight; its SET_COMPLETE_EVT will hit the
|
|
* "stale event" branch in the GAP handler and bail. */
|
|
s_adv_config_done = 0;
|
|
return xlate_rc(rc);
|
|
}
|
|
return BLE_UART_OK;
|
|
}
|
|
|
|
/* ===== Passkey banner ================================================= */
|
|
|
|
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, "");
|
|
}
|
|
|
|
/* ===== Long-write accumulator ========================================= */
|
|
|
|
static void prep_reset(void)
|
|
{
|
|
s_prep_len = 0;
|
|
s_prep_bad = false;
|
|
}
|
|
|
|
/* Append one PREP_WRITE fragment. Bad fragments latch s_prep_bad; we
|
|
* still answer OK to the client so its prepare-queue advances, then
|
|
* reject the whole batch at EXEC time (BT Core Spec §3.4.6.3 model).
|
|
*
|
|
* Two checks, both required:
|
|
* 1. Bounds — would otherwise memcpy past s_rx_buf.
|
|
* 2. Strict ascending contiguity — the spec lets a hostile client
|
|
* send arbitrary offsets; if we accepted gaps, the bytes between
|
|
* the writes would leak from the previous transaction (s_rx_buf
|
|
* is BSS and prep_reset() doesn't zero it). All real-world BLE
|
|
* centrals send PREP fragments in strict order anyway. */
|
|
static void prep_append(uint16_t offset, const uint8_t *data, uint16_t len)
|
|
{
|
|
if (s_prep_bad) {
|
|
return;
|
|
}
|
|
if ((uint32_t)offset + len > sizeof(s_rx_buf)) {
|
|
ESP_LOGW(TAG, "prep_write overflow: offset=%u len=%u cap=%u",
|
|
offset, len, (unsigned)sizeof(s_rx_buf));
|
|
s_prep_bad = true;
|
|
return;
|
|
}
|
|
if (offset != s_prep_len) {
|
|
ESP_LOGW(TAG, "prep_write non-contiguous: offset=%u expected=%u",
|
|
offset, s_prep_len);
|
|
s_prep_bad = true;
|
|
return;
|
|
}
|
|
memcpy(s_rx_buf + offset, data, len);
|
|
s_prep_len = offset + len;
|
|
}
|
|
|
|
/* ===== GATT event handler ============================================= */
|
|
|
|
/* Reply to a PREP_WRITE_REQ by echoing the value (BT spec mandates). */
|
|
static void send_prep_write_response(uint16_t conn_id, uint32_t trans_id,
|
|
esp_gatt_status_t status,
|
|
uint16_t handle, uint16_t offset,
|
|
const uint8_t *value, uint16_t len)
|
|
{
|
|
esp_gatt_rsp_t rsp = {0};
|
|
rsp.attr_value.handle = handle;
|
|
rsp.attr_value.offset = offset;
|
|
rsp.attr_value.len = (len > sizeof(rsp.attr_value.value))
|
|
? sizeof(rsp.attr_value.value)
|
|
: len;
|
|
if (rsp.attr_value.len) {
|
|
memcpy(rsp.attr_value.value, value, rsp.attr_value.len);
|
|
}
|
|
esp_ble_gatts_send_response(s_gatts_if, conn_id, trans_id, status, &rsp);
|
|
}
|
|
|
|
static void handle_write(esp_ble_gatts_cb_param_t *p)
|
|
{
|
|
uint16_t handle = p->write.handle;
|
|
|
|
if (p->write.is_prep) {
|
|
/* Two-tier validation per BT Core Spec §3.4.6.1 / §3.4.6.3:
|
|
* - Attribute-level (wrong handle): error in the PREP_RSP.
|
|
* - Fragment-level (offset/oversize/non-contiguous): latched
|
|
* in s_prep_bad; PREP_RSP stays OK and the error is
|
|
* surfaced once at EXEC time. */
|
|
esp_gatt_status_t status = ESP_GATT_OK;
|
|
if (handle != s_handles[NUS_IDX_RX_VAL]) {
|
|
status = ESP_GATT_INVALID_HANDLE;
|
|
} else {
|
|
prep_append(p->write.offset, p->write.value, p->write.len);
|
|
/* status stays OK even if s_prep_bad got latched. */
|
|
}
|
|
if (p->write.need_rsp) {
|
|
send_prep_write_response(p->write.conn_id, p->write.trans_id,
|
|
status, handle, p->write.offset,
|
|
p->write.value, p->write.len);
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* Plain (non-prep) write. */
|
|
esp_gatt_status_t status = ESP_GATT_OK;
|
|
if (handle == s_handles[NUS_IDX_RX_VAL]) {
|
|
if (s_cfg.ble_uart_on_rx != NULL && p->write.len > 0) {
|
|
s_cfg.ble_uart_on_rx(p->write.value, p->write.len);
|
|
}
|
|
} else if (handle == s_handles[NUS_IDX_TX_CCCD]) {
|
|
/* CCCD must be exactly 2 bytes (BT Core §3.3.3.3). */
|
|
if (p->write.len != 2) {
|
|
ESP_LOGW(TAG, "malformed CCCD write len=%u", p->write.len);
|
|
status = ESP_GATT_INVALID_ATTR_LEN;
|
|
} else {
|
|
uint16_t cccd = (uint16_t)p->write.value[0]
|
|
| ((uint16_t)p->write.value[1] << 8);
|
|
s_subscribed = (cccd & 0x0001) != 0;
|
|
ESP_LOGI(TAG, "subscribe cccd=0x%04x sub=%d", cccd, s_subscribed);
|
|
}
|
|
}
|
|
|
|
if (p->write.need_rsp) {
|
|
esp_ble_gatts_send_response(s_gatts_if, p->write.conn_id,
|
|
p->write.trans_id, status, NULL);
|
|
}
|
|
}
|
|
|
|
static void handle_exec_write(esp_ble_gatts_cb_param_t *p)
|
|
{
|
|
/* CANCEL (flag=0x00) must always succeed; EXEC (flag=0x01) returns
|
|
* INVALID_ATTR_LEN if PREP latched an error. Either way, drop
|
|
* the queue. */
|
|
esp_gatt_status_t status = ESP_GATT_OK;
|
|
|
|
if (p->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
|
|
if (s_prep_bad) {
|
|
ESP_LOGW(TAG, "exec_write: rejecting bad prep buffer (len=%u)",
|
|
s_prep_len);
|
|
status = ESP_GATT_INVALID_ATTR_LEN;
|
|
} else if (s_cfg.ble_uart_on_rx != NULL && s_prep_len > 0) {
|
|
s_cfg.ble_uart_on_rx(s_rx_buf, s_prep_len);
|
|
}
|
|
}
|
|
prep_reset();
|
|
esp_ble_gatts_send_response(s_gatts_if, p->exec_write.conn_id,
|
|
p->exec_write.trans_id, status, NULL);
|
|
}
|
|
|
|
static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
|
|
esp_gatt_if_t gatts_if,
|
|
esp_ble_gatts_cb_param_t *param)
|
|
{
|
|
switch (event) {
|
|
|
|
case ESP_GATTS_REG_EVT:
|
|
ESP_LOGI(TAG, "gatts reg status=%d app_id=%u gatts_if=%u",
|
|
param->reg.status, param->reg.app_id, gatts_if);
|
|
if (param->reg.status != ESP_GATT_OK) {
|
|
return;
|
|
}
|
|
s_gatts_if = gatts_if;
|
|
if (s_cfg.device_name != NULL) {
|
|
esp_ble_gap_set_device_name(s_cfg.device_name);
|
|
/* Bluedroid copies internally; drop our pointer. */
|
|
s_cfg.device_name = NULL;
|
|
}
|
|
esp_ble_gatts_create_attr_tab(s_nus_db, gatts_if,
|
|
NUS_IDX_NB, UART_SVC_INST_ID);
|
|
break;
|
|
|
|
case ESP_GATTS_CREAT_ATTR_TAB_EVT:
|
|
if (param->add_attr_tab.status != ESP_GATT_OK
|
|
|| param->add_attr_tab.num_handle != NUS_IDX_NB) {
|
|
ESP_LOGE(TAG, "create_attr_tab failed status=%d num=%u (expected %u)",
|
|
param->add_attr_tab.status,
|
|
param->add_attr_tab.num_handle, NUS_IDX_NB);
|
|
return;
|
|
}
|
|
memcpy(s_handles, param->add_attr_tab.handles, sizeof(s_handles));
|
|
ESP_LOGI(TAG, "registered service svc_handle=%u rx=%u tx=%u cccd=%u",
|
|
s_handles[NUS_IDX_SVC], s_handles[NUS_IDX_RX_VAL],
|
|
s_handles[NUS_IDX_TX_VAL], s_handles[NUS_IDX_TX_CCCD]);
|
|
esp_ble_gatts_start_service(s_handles[NUS_IDX_SVC]);
|
|
s_attr_tab_ready = true;
|
|
/* If open() was waiting for the table, kick adv config now.
|
|
* On the BTC task — no caller to return errors to. */
|
|
if (s_opened && !s_adv_active) {
|
|
(void)configure_advertising();
|
|
}
|
|
break;
|
|
|
|
case ESP_GATTS_WRITE_EVT:
|
|
handle_write(param);
|
|
break;
|
|
|
|
case ESP_GATTS_EXEC_WRITE_EVT:
|
|
handle_exec_write(param);
|
|
break;
|
|
|
|
case ESP_GATTS_MTU_EVT:
|
|
s_local_mtu = param->mtu.mtu;
|
|
ESP_LOGI(TAG, "mtu=%u (conn=%u)", s_local_mtu, param->mtu.conn_id);
|
|
break;
|
|
|
|
case ESP_GATTS_CONNECT_EVT:
|
|
/* Bluedroid only fires this on a successful physical link;
|
|
* the param struct has no status field. */
|
|
s_conn_id = param->connect.conn_id;
|
|
s_subscribed = false;
|
|
/* Link-layer auto-stops undirected adv on connect (per BT spec)
|
|
* but Bluedroid doesn't deliver ADV_STOP_COMPLETE_EVT for that
|
|
* case — only for explicit stop_advertising(). Clear here so
|
|
* close() doesn't try to stop an already-stopped advertiser. */
|
|
s_adv_active = false;
|
|
memcpy(s_remote_bda, param->connect.remote_bda, sizeof(s_remote_bda));
|
|
ESP_LOGI(TAG, "connect conn_id=%u remote " ESP_BD_ADDR_STR,
|
|
s_conn_id, ESP_BD_ADDR_HEX(s_remote_bda));
|
|
if (s_cfg.encrypted) {
|
|
/* Kick pairing immediately rather than lazily on the
|
|
* first encrypted attribute access. */
|
|
esp_ble_set_encryption(param->connect.remote_bda,
|
|
ESP_BLE_SEC_ENCRYPT_MITM);
|
|
}
|
|
break;
|
|
|
|
case ESP_GATTS_DISCONNECT_EVT:
|
|
ESP_LOGI(TAG, "disconnect conn_id=%u reason=0x%x",
|
|
param->disconnect.conn_id, param->disconnect.reason);
|
|
s_conn_id = 0xFFFF;
|
|
s_subscribed = false;
|
|
/* MTU is per-connection: reset to the spec default 23 so the
|
|
* next peer (if it skips the MTU exchange) doesn't inherit
|
|
* the previous link's negotiated value and overflow tx chunks. */
|
|
s_local_mtu = 23;
|
|
prep_reset();
|
|
if (!s_shutting_down) {
|
|
start_advertising();
|
|
}
|
|
break;
|
|
|
|
case ESP_GATTS_CONF_EVT:
|
|
if (param->conf.status != ESP_GATT_OK) {
|
|
ESP_LOGW(TAG, "notify confirm status=%d handle=%u",
|
|
param->conf.status, param->conf.handle);
|
|
}
|
|
break;
|
|
|
|
case ESP_GATTS_READ_EVT:
|
|
ESP_LOGD(TAG, "read on handle=%u (ignored)", param->read.handle);
|
|
break;
|
|
|
|
case ESP_GATTS_UNREG_EVT:
|
|
s_gatts_if = ESP_GATT_IF_NONE;
|
|
s_attr_tab_ready = false;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ===== GAP event handler ============================================== */
|
|
|
|
static void gap_event_handler(esp_gap_ble_cb_event_t event,
|
|
esp_ble_gap_cb_param_t *param)
|
|
{
|
|
switch (event) {
|
|
|
|
/* Both SET_COMPLETE handlers below need to:
|
|
* 1) drop stale events (latch already zeroed by a sync failure);
|
|
* 2) drop async failures (status != SUCCESS) and wipe the latch
|
|
* so the other half can't satisfy the "==0 → start_adv" check
|
|
* and launch advertising with a malformed payload. */
|
|
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
|
|
if (!(s_adv_config_done & ADV_CONFIG_FLAG)) {
|
|
ESP_LOGD(TAG, "stale ADV_DATA_SET_COMPLETE_EVT ignored");
|
|
break;
|
|
}
|
|
if (param->adv_data_cmpl.status != ESP_BT_STATUS_SUCCESS) {
|
|
ESP_LOGE(TAG, "adv_data set failed status=0x%x",
|
|
param->adv_data_cmpl.status);
|
|
s_adv_config_done = 0;
|
|
break;
|
|
}
|
|
s_adv_config_done &= ~ADV_CONFIG_FLAG;
|
|
if (s_adv_config_done == 0 && s_opened && !s_shutting_down) {
|
|
start_advertising();
|
|
}
|
|
break;
|
|
|
|
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
|
|
if (!(s_adv_config_done & SCAN_RSP_CONFIG_FLAG)) {
|
|
ESP_LOGD(TAG, "stale SCAN_RSP_DATA_SET_COMPLETE_EVT ignored");
|
|
break;
|
|
}
|
|
if (param->scan_rsp_data_cmpl.status != ESP_BT_STATUS_SUCCESS) {
|
|
ESP_LOGE(TAG, "scan_rsp set failed status=0x%x",
|
|
param->scan_rsp_data_cmpl.status);
|
|
s_adv_config_done = 0;
|
|
break;
|
|
}
|
|
s_adv_config_done &= ~SCAN_RSP_CONFIG_FLAG;
|
|
if (s_adv_config_done == 0 && s_opened && !s_shutting_down) {
|
|
start_advertising();
|
|
}
|
|
break;
|
|
|
|
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
|
if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
|
|
ESP_LOGE(TAG, "adv_start failed status=0x%x",
|
|
param->adv_start_cmpl.status);
|
|
s_adv_active = false;
|
|
break;
|
|
}
|
|
ESP_LOGI(TAG, "advertising started");
|
|
break;
|
|
|
|
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
|
|
s_adv_active = false;
|
|
ESP_LOGI(TAG, "advertising stopped");
|
|
break;
|
|
|
|
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
|
|
show_passkey(param->ble_security.key_notif.passkey);
|
|
break;
|
|
|
|
case ESP_GAP_BLE_AUTH_CMPL_EVT: {
|
|
esp_ble_auth_cmpl_t *a = ¶m->ble_security.auth_cmpl;
|
|
if (a->success) {
|
|
ESP_LOGI(TAG, "pairing ok auth_mode=0x%x", a->auth_mode);
|
|
} else {
|
|
ESP_LOGW(TAG, "pairing failed reason=0x%x", a->fail_reason);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ESP_GAP_BLE_SEC_REQ_EVT:
|
|
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
|
|
break;
|
|
|
|
case ESP_GAP_BLE_KEY_EVT:
|
|
ESP_LOGD(TAG, "key event type=0x%x",
|
|
param->ble_security.ble_key.key_type);
|
|
break;
|
|
|
|
case ESP_GAP_BLE_NC_REQ_EVT:
|
|
/* Numeric Comparison shouldn't fire with our DisplayOnly IO
|
|
* (BT Core §2.3.5.1). Reject — accepting would flag the LTK
|
|
* as MITM-authenticated without any user actually comparing
|
|
* the digits, silently downgrading the security we asked for. */
|
|
esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, false);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ===== Public TX ====================================================== */
|
|
|
|
int ble_uart_tx(const uint8_t *data, size_t len)
|
|
{
|
|
/* Snapshot conn_id and MTU once: a peer-A→peer-B disconnect+connect
|
|
* race during a multi-chunk send could otherwise leak later chunks
|
|
* to peer B (send_indicate doesn't gate on the CCCD subscription).
|
|
* Stale conn_id → ESP_ERR_INVALID_ARG, we bail cleanly. Stale MTU
|
|
* is harmless (chunks would only be smaller, never too big). */
|
|
uint16_t conn_id = s_conn_id;
|
|
uint16_t mtu_snap = s_local_mtu;
|
|
if (conn_id == 0xFFFF) {
|
|
return BLE_UART_ENOTCONN;
|
|
}
|
|
if (data == NULL || len == 0) {
|
|
return BLE_UART_EINVAL;
|
|
}
|
|
|
|
size_t chunk = (mtu_snap > 3) ? (size_t)(mtu_snap - 3) : 20;
|
|
size_t sent = 0;
|
|
while (sent < len) {
|
|
size_t n = len - sent;
|
|
if (n > chunk) {
|
|
n = chunk;
|
|
}
|
|
esp_err_t rc = esp_ble_gatts_send_indicate(s_gatts_if, conn_id,
|
|
s_handles[NUS_IDX_TX_VAL],
|
|
(uint16_t)n,
|
|
(uint8_t *)(data + sent),
|
|
false); /* notify, not indicate */
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGW(TAG, "send_indicate rc=%s", esp_err_to_name(rc));
|
|
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_id != 0xFFFF; }
|
|
bool ble_uart_is_subscribed(void) { return s_subscribed; }
|
|
|
|
/* ===== Lifecycle ====================================================== */
|
|
|
|
static int configure_security(bool encrypted)
|
|
{
|
|
esp_ble_auth_req_t auth_req = encrypted ? ESP_LE_AUTH_REQ_SC_MITM_BOND
|
|
: ESP_LE_AUTH_NO_BOND;
|
|
esp_ble_io_cap_t iocap = encrypted ? ESP_IO_CAP_OUT
|
|
: ESP_IO_CAP_NONE;
|
|
uint8_t key_size = 16;
|
|
uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK;
|
|
uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK;
|
|
|
|
/* Apply each SM param individually and bail on the first failure.
|
|
* Do NOT collapse into `rc |= ...` — esp_err_t values aren't bit
|
|
* flags (e.g. NO_MEM|INVALID_ARG = INVALID_STATE), and a single
|
|
* ESP_FAIL=-1 sticks the accumulator. */
|
|
struct { esp_ble_sm_param_t id; const void *val; uint8_t len; const char *tag; } params[] = {
|
|
{ ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(auth_req), "AUTHEN_REQ_MODE" },
|
|
{ ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(iocap), "IOCAP_MODE" },
|
|
{ ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(key_size), "MAX_KEY_SIZE" },
|
|
{ ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(init_key), "SET_INIT_KEY" },
|
|
{ ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(rsp_key), "SET_RSP_KEY" },
|
|
};
|
|
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
|
esp_err_t rc = esp_ble_gap_set_security_param(params[i].id,
|
|
(void *)params[i].val,
|
|
params[i].len);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "set_security_param[%s] rc=%s",
|
|
params[i].tag, esp_err_to_name(rc));
|
|
return xlate_rc(rc);
|
|
}
|
|
}
|
|
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));
|
|
}
|
|
|
|
/* Free BR/EDR controller RAM we won't use (no-op on BLE-only chips). */
|
|
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
|
|
|
|
/* Bring up controller + Bluedroid step by step. Track `stage` so
|
|
* the fail label can unwind exactly the layers we allocated —
|
|
* otherwise a partial install would leave the controller enabled
|
|
* with no module state pointing at it, and the next install()
|
|
* would fail with INVALID_STATE. */
|
|
enum {
|
|
STAGE_NONE = 0,
|
|
STAGE_CTRL_INIT,
|
|
STAGE_CTRL_ENABLE,
|
|
STAGE_BLUEDROID_INIT,
|
|
STAGE_BLUEDROID_ENABLE,
|
|
STAGE_APP_REGISTER,
|
|
} stage = STAGE_NONE;
|
|
|
|
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
|
|
esp_err_t rc = esp_bt_controller_init(&bt_cfg);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "controller_init rc=%s", esp_err_to_name(rc));
|
|
goto fail;
|
|
}
|
|
stage = STAGE_CTRL_INIT;
|
|
|
|
rc = esp_bt_controller_enable(ESP_BT_MODE_BLE);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "controller_enable rc=%s", esp_err_to_name(rc));
|
|
goto fail;
|
|
}
|
|
stage = STAGE_CTRL_ENABLE;
|
|
|
|
rc = esp_bluedroid_init();
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "bluedroid_init rc=%s", esp_err_to_name(rc));
|
|
goto fail;
|
|
}
|
|
stage = STAGE_BLUEDROID_INIT;
|
|
|
|
rc = esp_bluedroid_enable();
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "bluedroid_enable rc=%s", esp_err_to_name(rc));
|
|
goto fail;
|
|
}
|
|
stage = STAGE_BLUEDROID_ENABLE;
|
|
|
|
rc = esp_ble_gatts_register_callback(gatts_profile_event_handler);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "gatts_register rc=%s", esp_err_to_name(rc));
|
|
goto fail;
|
|
}
|
|
rc = esp_ble_gap_register_callback(gap_event_handler);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "gap_register rc=%s", esp_err_to_name(rc));
|
|
goto fail;
|
|
}
|
|
|
|
/* SM must be configured before app_register so any incoming
|
|
* pairing request finds the right policy. */
|
|
int srv = configure_security(s_cfg.encrypted);
|
|
if (srv != BLE_UART_OK) {
|
|
ESP_LOGE(TAG, "security config failed rc=%d", srv);
|
|
rc = ESP_FAIL;
|
|
goto fail;
|
|
}
|
|
|
|
/* Bluedroid has no compile-time MTU Kconfig (NimBLE has
|
|
* CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU); set it at runtime here so
|
|
* both backends behave the same. Failure isn't fatal — we'd just
|
|
* fall back to the spec default 23. */
|
|
rc = esp_ble_gatt_set_local_mtu(512);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGW(TAG, "set_local_mtu(512) rc=%s; staying at default",
|
|
esp_err_to_name(rc));
|
|
}
|
|
|
|
build_attr_table(s_cfg.encrypted);
|
|
|
|
rc = esp_ble_gatts_app_register(UART_APP_ID);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "app_register rc=%s", esp_err_to_name(rc));
|
|
goto fail;
|
|
}
|
|
stage = STAGE_APP_REGISTER;
|
|
|
|
/* Wait briefly for CREAT_ATTR_TAB_EVT so the GATT table is in
|
|
* place before open() asks the stack to advertise (otherwise the
|
|
* peer would scan us with an empty database). 500 ms is well over
|
|
* the typical bring-up time (~150 ms on ESP32-S3). */
|
|
for (int i = 0; i < 50 && !s_attr_tab_ready; i++) {
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
if (!s_attr_tab_ready) {
|
|
ESP_LOGW(TAG, "attr table not ready after 500 ms; open() will retry");
|
|
}
|
|
|
|
s_installed = true;
|
|
return BLE_UART_OK;
|
|
|
|
fail:
|
|
/* Unwind exactly the layers we brought up; ignore secondary errors. */
|
|
switch (stage) {
|
|
case STAGE_APP_REGISTER:
|
|
if (s_gatts_if != ESP_GATT_IF_NONE) {
|
|
esp_ble_gatts_app_unregister(s_gatts_if);
|
|
s_gatts_if = ESP_GATT_IF_NONE;
|
|
}
|
|
/* fallthrough */
|
|
case STAGE_BLUEDROID_ENABLE:
|
|
esp_bluedroid_disable();
|
|
/* fallthrough */
|
|
case STAGE_BLUEDROID_INIT:
|
|
esp_bluedroid_deinit();
|
|
/* fallthrough */
|
|
case STAGE_CTRL_ENABLE:
|
|
esp_bt_controller_disable();
|
|
/* fallthrough */
|
|
case STAGE_CTRL_INIT:
|
|
esp_bt_controller_deinit();
|
|
/* fallthrough */
|
|
case STAGE_NONE:
|
|
break;
|
|
}
|
|
s_attr_tab_ready = false;
|
|
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;
|
|
}
|
|
s_opened = true;
|
|
|
|
/* If the GATT table is up, kick adv-config now; otherwise
|
|
* CREAT_ATTR_TAB_EVT will do it when it arrives. */
|
|
if (s_attr_tab_ready) {
|
|
int rc = configure_advertising();
|
|
if (rc != BLE_UART_OK) {
|
|
/* Roll back so the caller can retry open() without
|
|
* going through close + uninstall. */
|
|
s_opened = false;
|
|
return rc;
|
|
}
|
|
}
|
|
return BLE_UART_OK;
|
|
}
|
|
|
|
int ble_uart_close(void)
|
|
{
|
|
if (!s_opened) {
|
|
return BLE_UART_EALREADY;
|
|
}
|
|
|
|
/* Latch first so DISCONNECT_EVT skips the auto-readvertise. */
|
|
s_shutting_down = true;
|
|
|
|
if (s_adv_active) {
|
|
esp_ble_gap_stop_advertising();
|
|
}
|
|
|
|
/* Graceful disconnect: ask the controller to send LL_TERMINATE,
|
|
* then wait up to 500 ms for DISCONNECT_EVT to clear s_conn_id. */
|
|
if (s_conn_id != 0xFFFF) {
|
|
esp_err_t rc = esp_ble_gap_disconnect(s_remote_bda);
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGW(TAG, "gap_disconnect rc=%s", esp_err_to_name(rc));
|
|
}
|
|
for (int i = 0; i < 50 && s_conn_id != 0xFFFF; i++) {
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
if (s_conn_id != 0xFFFF) {
|
|
ESP_LOGW(TAG, "disconnect timed out; tearing down anyway");
|
|
}
|
|
}
|
|
|
|
s_conn_id = 0xFFFF;
|
|
s_subscribed = false;
|
|
/* Reset MTU here too in case DISCONNECT_EVT timed out above. */
|
|
s_local_mtu = 23;
|
|
/* Defensively clear the long-write accumulator: if DISCONNECT_EVT
|
|
* didn't fire within our wait window, prep_reset() in the event
|
|
* handler never ran, and stale s_prep_len/s_prep_bad would poison
|
|
* the first PREP_WRITE of the next session (offset 0 != stale len
|
|
* latches s_prep_bad and rejects the whole batch at EXEC time). */
|
|
prep_reset();
|
|
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. We MUST NOT early-return on a per-step
|
|
* failure: that would leave s_installed=true with the SDK in
|
|
* some half-torn-down state, blocking both re-install and retry.
|
|
* Mirror the install() goto-fail philosophy: record the first
|
|
* error, keep tearing down, and always wipe our state. */
|
|
esp_err_t first_err = ESP_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_err == ESP_OK) first_err = rc;
|
|
}
|
|
}
|
|
|
|
if (s_gatts_if != ESP_GATT_IF_NONE) {
|
|
esp_err_t rc = esp_ble_gatts_app_unregister(s_gatts_if);
|
|
if (rc != ESP_OK && first_err == ESP_OK) {
|
|
ESP_LOGE(TAG, "gatts_app_unregister rc=%s", esp_err_to_name(rc));
|
|
first_err = rc;
|
|
}
|
|
s_gatts_if = ESP_GATT_IF_NONE;
|
|
}
|
|
|
|
esp_err_t rc = esp_bluedroid_disable();
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "bluedroid_disable rc=%s", esp_err_to_name(rc));
|
|
if (first_err == ESP_OK) first_err = rc;
|
|
}
|
|
rc = esp_bluedroid_deinit();
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "bluedroid_deinit rc=%s", esp_err_to_name(rc));
|
|
if (first_err == ESP_OK) first_err = rc;
|
|
}
|
|
rc = esp_bt_controller_disable();
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "controller_disable rc=%s", esp_err_to_name(rc));
|
|
if (first_err == ESP_OK) first_err = rc;
|
|
}
|
|
rc = esp_bt_controller_deinit();
|
|
if (rc != ESP_OK) {
|
|
ESP_LOGE(TAG, "controller_deinit rc=%s", esp_err_to_name(rc));
|
|
if (first_err == ESP_OK) first_err = rc;
|
|
}
|
|
|
|
/* Wipe state unconditionally, even on partial failure. */
|
|
memset(&s_cfg, 0, sizeof(s_cfg));
|
|
memset(s_handles, 0, sizeof(s_handles));
|
|
memset(s_remote_bda, 0, sizeof(s_remote_bda));
|
|
s_conn_id = 0xFFFF;
|
|
s_local_mtu = 23;
|
|
s_subscribed = false;
|
|
s_installed = false;
|
|
s_opened = false;
|
|
s_shutting_down = false;
|
|
s_attr_tab_ready = false;
|
|
s_adv_active = false;
|
|
s_adv_config_done = 0;
|
|
prep_reset();
|
|
return first_err == ESP_OK ? BLE_UART_OK : xlate_rc(first_err);
|
|
}
|
|
|
|
#endif /* CONFIG_BT_BLUEDROID_ENABLED */
|