- Switch UART0 for protocol I/O (USB bridge), disable ESP-IDF console - Wire all 21 command handlers into dispatch table (was only 4) - Add configure command handler (name, io_cap, device_class) - Add bt_classic_is_enabled()/bt_ble_is_enabled() for live status - Fix cJSON_False misuse in get_status (type constant, not boolean) - Fix esp_bt_gap_set_cod() to use esp_bt_cod_t bitfield struct - Fix auth_cmpl.auth_mode → lk_type for ESP-IDF v5.3 - Replace deprecated esp_bt_dev_set_device_name with stack-specific API - Remove unused bytes_to_hex, obsolete kconfig symbols - Use large partition table (1.5MB) for dual-mode BT stack Verified on ESP32-D0WD-V3 rev 3.1, /dev/ttyUSB4, all commands tested.
905 lines
30 KiB
C
905 lines
30 KiB
C
/*
|
|
* bt_ble.c -- BLE peripheral using Bluedroid GATTS (ESP-IDF v5.x).
|
|
*
|
|
* Provides GATT server with up to 8 services, each with up to 8
|
|
* characteristics. Commands arrive as NDJSON from the host over UART;
|
|
* events (connect, read, write, subscribe) are pushed back asynchronously
|
|
* via the event_reporter queue.
|
|
*/
|
|
|
|
#include "bt_ble.h"
|
|
#include "protocol.h"
|
|
#include "uart_handler.h"
|
|
#include "event_reporter.h"
|
|
|
|
#include "esp_bt.h"
|
|
#include "esp_gap_ble_api.h"
|
|
#include "esp_gatts_api.h"
|
|
#include "esp_bt_main.h"
|
|
#include "esp_gatt_common_api.h"
|
|
#include "esp_log.h"
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
|
|
#define TAG "ble"
|
|
|
|
#define MAX_SERVICES 8
|
|
#define MAX_CHARS_PER_SVC 8
|
|
#define MAX_CHAR_VALUE_LEN 512
|
|
#define GATTS_APP_ID 0
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Data structures */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
typedef struct {
|
|
uint16_t handle;
|
|
uint8_t uuid[16];
|
|
uint8_t value[MAX_CHAR_VALUE_LEN];
|
|
uint16_t value_len;
|
|
uint8_t properties;
|
|
uint16_t cccd_handle;
|
|
bool notifications_enabled;
|
|
} char_entry_t;
|
|
|
|
typedef struct {
|
|
uint16_t handle;
|
|
uint8_t uuid[16];
|
|
bool primary;
|
|
int num_chars;
|
|
char_entry_t chars[MAX_CHARS_PER_SVC];
|
|
} service_entry_t;
|
|
|
|
static struct {
|
|
bool enabled;
|
|
bool advertising;
|
|
int num_services;
|
|
service_entry_t services[MAX_SERVICES];
|
|
esp_gatt_if_t gatts_if;
|
|
uint16_t conn_id;
|
|
bool connected;
|
|
} s_ble;
|
|
|
|
/*
|
|
* Async response plumbing: GATTS create/add_char events need to reply to the
|
|
* command that triggered them. Commands are processed sequentially on the
|
|
* main UART task, so a single pending slot is safe.
|
|
*/
|
|
static char s_pending_cmd_id[64];
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static int parse_uuid(const char *uuid_str, uint8_t *uuid_out)
|
|
{
|
|
if (!uuid_str) return -1;
|
|
|
|
/* Strip hyphens into a 32-char hex string */
|
|
char hex[33];
|
|
int h = 0;
|
|
for (int i = 0; uuid_str[i] && h < 32; i++) {
|
|
if (uuid_str[i] != '-') {
|
|
hex[h++] = uuid_str[i];
|
|
}
|
|
}
|
|
hex[h] = '\0';
|
|
|
|
if (h != 32) {
|
|
ESP_LOGE(TAG, "UUID must be 32 hex digits (got %d)", h);
|
|
return -1;
|
|
}
|
|
|
|
/* Parse 16 bytes, store in little-endian order for ESP BLE stack */
|
|
for (int i = 0; i < 16; i++) {
|
|
unsigned int byte;
|
|
if (sscanf(hex + (15 - i) * 2, "%2x", &byte) != 1) return -1;
|
|
uuid_out[i] = (uint8_t)byte;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int hex_to_bytes(const char *hex, uint8_t *out, int max_len)
|
|
{
|
|
if (!hex) return 0;
|
|
int len = (int)strlen(hex) / 2;
|
|
if (len > max_len) len = max_len;
|
|
for (int i = 0; i < len; i++) {
|
|
unsigned int b;
|
|
if (sscanf(hex + i * 2, "%2x", &b) != 1) return i;
|
|
out[i] = (uint8_t)b;
|
|
}
|
|
return len;
|
|
}
|
|
|
|
static uint8_t properties_from_json(cJSON *arr)
|
|
{
|
|
uint8_t props = 0;
|
|
if (!cJSON_IsArray(arr)) return props;
|
|
|
|
cJSON *item;
|
|
cJSON_ArrayForEach(item, arr) {
|
|
if (!cJSON_IsString(item)) continue;
|
|
const char *s = item->valuestring;
|
|
if (strcmp(s, "read") == 0) props |= ESP_GATT_CHAR_PROP_BIT_READ;
|
|
else if (strcmp(s, "write") == 0) props |= ESP_GATT_CHAR_PROP_BIT_WRITE;
|
|
else if (strcmp(s, "write_nr") == 0) props |= ESP_GATT_CHAR_PROP_BIT_WRITE_NR;
|
|
else if (strcmp(s, "notify") == 0) props |= ESP_GATT_CHAR_PROP_BIT_NOTIFY;
|
|
else if (strcmp(s, "indicate") == 0) props |= ESP_GATT_CHAR_PROP_BIT_INDICATE;
|
|
}
|
|
return props;
|
|
}
|
|
|
|
static service_entry_t *find_service_by_handle(uint16_t handle)
|
|
{
|
|
for (int i = 0; i < s_ble.num_services; i++) {
|
|
if (s_ble.services[i].handle == handle)
|
|
return &s_ble.services[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static char_entry_t *find_char_by_handle(uint16_t handle)
|
|
{
|
|
for (int i = 0; i < s_ble.num_services; i++) {
|
|
service_entry_t *svc = &s_ble.services[i];
|
|
for (int j = 0; j < svc->num_chars; j++) {
|
|
if (svc->chars[j].handle == handle)
|
|
return &svc->chars[j];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static char_entry_t *find_char_by_cccd(uint16_t cccd_handle)
|
|
{
|
|
for (int i = 0; i < s_ble.num_services; i++) {
|
|
service_entry_t *svc = &s_ble.services[i];
|
|
for (int j = 0; j < svc->num_chars; j++) {
|
|
if (svc->chars[j].cccd_handle == cccd_handle)
|
|
return &svc->chars[j];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void send_pending_response(const char *status, cJSON *data)
|
|
{
|
|
if (s_pending_cmd_id[0] == '\0') return;
|
|
uart_send_response(s_pending_cmd_id, status, data);
|
|
s_pending_cmd_id[0] = '\0';
|
|
}
|
|
|
|
static void set_pending(const char *id)
|
|
{
|
|
strncpy(s_pending_cmd_id, id, sizeof(s_pending_cmd_id) - 1);
|
|
s_pending_cmd_id[sizeof(s_pending_cmd_id) - 1] = '\0';
|
|
}
|
|
|
|
/* BDA to string */
|
|
static void bda_to_str(const uint8_t *bda, char *out)
|
|
{
|
|
sprintf(out, "%02X:%02X:%02X:%02X:%02X:%02X",
|
|
bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Advertising params */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static esp_ble_adv_params_t s_adv_params = {
|
|
.adv_int_min = 0x20, /* 20 ms */
|
|
.adv_int_max = 0x40, /* 40 ms */
|
|
.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,
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* GAP event handler */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void gap_event_handler(esp_gap_ble_cb_event_t event,
|
|
esp_ble_gap_cb_param_t *param)
|
|
{
|
|
switch (event) {
|
|
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
|
|
esp_ble_gap_start_advertising(&s_adv_params);
|
|
break;
|
|
|
|
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
|
if (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS) {
|
|
ESP_LOGI(TAG, "advertising started");
|
|
s_ble.advertising = true;
|
|
} else {
|
|
ESP_LOGE(TAG, "advertising start failed: %d",
|
|
param->adv_start_cmpl.status);
|
|
}
|
|
break;
|
|
|
|
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
|
|
ESP_LOGI(TAG, "advertising stopped");
|
|
s_ble.advertising = false;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* GATTS event handler */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void gatts_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:
|
|
if (param->reg.status == ESP_GATT_OK) {
|
|
s_ble.gatts_if = gatts_if;
|
|
ESP_LOGI(TAG, "GATTS app registered, if=%d", gatts_if);
|
|
} else {
|
|
ESP_LOGE(TAG, "GATTS register failed: %d", param->reg.status);
|
|
}
|
|
break;
|
|
|
|
case ESP_GATTS_CREATE_EVT: {
|
|
uint16_t svc_handle = param->create.service_handle;
|
|
ESP_LOGI(TAG, "service created, handle=%d", svc_handle);
|
|
|
|
/* Store handle in the most recently added service entry */
|
|
if (s_ble.num_services > 0) {
|
|
service_entry_t *svc = &s_ble.services[s_ble.num_services - 1];
|
|
svc->handle = svc_handle;
|
|
}
|
|
|
|
esp_ble_gatts_start_service(svc_handle);
|
|
|
|
cJSON *data = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(data, "service_handle", svc_handle);
|
|
send_pending_response(STATUS_OK, data);
|
|
break;
|
|
}
|
|
|
|
case ESP_GATTS_ADD_CHAR_EVT: {
|
|
if (param->add_char.status != ESP_GATT_OK) {
|
|
ESP_LOGE(TAG, "add char failed: %d", param->add_char.status);
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "add_char_failed");
|
|
send_pending_response(STATUS_ERROR, err);
|
|
break;
|
|
}
|
|
|
|
uint16_t char_handle = param->add_char.attr_handle;
|
|
ESP_LOGI(TAG, "characteristic added, handle=%d", char_handle);
|
|
|
|
/* Find the service and update the most recently added char */
|
|
service_entry_t *svc = find_service_by_handle(param->add_char.service_handle);
|
|
if (svc && svc->num_chars > 0) {
|
|
char_entry_t *ch = &svc->chars[svc->num_chars - 1];
|
|
ch->handle = char_handle;
|
|
}
|
|
|
|
cJSON *data = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(data, "char_handle", char_handle);
|
|
send_pending_response(STATUS_OK, data);
|
|
break;
|
|
}
|
|
|
|
case ESP_GATTS_ADD_CHAR_DESCR_EVT: {
|
|
if (param->add_char_descr.status != ESP_GATT_OK) {
|
|
ESP_LOGE(TAG, "add descriptor failed: %d", param->add_char_descr.status);
|
|
break;
|
|
}
|
|
|
|
/* Store CCCD handle on the owning characteristic */
|
|
uint16_t descr_handle = param->add_char_descr.attr_handle;
|
|
service_entry_t *svc = find_service_by_handle(param->add_char_descr.service_handle);
|
|
if (svc && svc->num_chars > 0) {
|
|
svc->chars[svc->num_chars - 1].cccd_handle = descr_handle;
|
|
}
|
|
ESP_LOGI(TAG, "CCCD descriptor added, handle=%d", descr_handle);
|
|
break;
|
|
}
|
|
|
|
case ESP_GATTS_READ_EVT: {
|
|
char addr_str[18];
|
|
bda_to_str(param->read.bda, addr_str);
|
|
event_report_gatt_read(param->read.handle, addr_str);
|
|
|
|
char_entry_t *ch = find_char_by_handle(param->read.handle);
|
|
|
|
esp_gatt_rsp_t rsp;
|
|
memset(&rsp, 0, sizeof(rsp));
|
|
rsp.attr_value.handle = param->read.handle;
|
|
|
|
if (ch) {
|
|
uint16_t send_len = ch->value_len;
|
|
if (send_len > ESP_GATT_MAX_ATTR_LEN)
|
|
send_len = ESP_GATT_MAX_ATTR_LEN;
|
|
rsp.attr_value.len = send_len;
|
|
memcpy(rsp.attr_value.value, ch->value, send_len);
|
|
}
|
|
|
|
esp_ble_gatts_send_response(gatts_if, param->read.conn_id,
|
|
param->read.trans_id,
|
|
ESP_GATT_OK, &rsp);
|
|
break;
|
|
}
|
|
|
|
case ESP_GATTS_WRITE_EVT: {
|
|
char addr_str[18];
|
|
bda_to_str(param->write.bda, addr_str);
|
|
|
|
/* CCCD write -- subscription toggle */
|
|
char_entry_t *cccd_ch = find_char_by_cccd(param->write.handle);
|
|
if (cccd_ch) {
|
|
bool subscribed = false;
|
|
if (param->write.len == 2) {
|
|
uint16_t val = param->write.value[0] | (param->write.value[1] << 8);
|
|
subscribed = (val != 0);
|
|
}
|
|
cccd_ch->notifications_enabled = subscribed;
|
|
event_report_gatt_subscribe(cccd_ch->handle, subscribed);
|
|
|
|
if (param->write.need_rsp) {
|
|
esp_ble_gatts_send_response(gatts_if, param->write.conn_id,
|
|
param->write.trans_id,
|
|
ESP_GATT_OK, NULL);
|
|
}
|
|
break;
|
|
}
|
|
|
|
/* Normal characteristic write */
|
|
char_entry_t *ch = find_char_by_handle(param->write.handle);
|
|
if (ch && param->write.len <= MAX_CHAR_VALUE_LEN) {
|
|
memcpy(ch->value, param->write.value, param->write.len);
|
|
ch->value_len = param->write.len;
|
|
}
|
|
|
|
event_report_gatt_write(param->write.handle, addr_str,
|
|
param->write.value, param->write.len);
|
|
|
|
if (param->write.need_rsp) {
|
|
esp_ble_gatts_send_response(gatts_if, param->write.conn_id,
|
|
param->write.trans_id,
|
|
ESP_GATT_OK, NULL);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ESP_GATTS_CONNECT_EVT: {
|
|
s_ble.conn_id = param->connect.conn_id;
|
|
s_ble.connected = true;
|
|
char addr_str[18];
|
|
bda_to_str(param->connect.remote_bda, addr_str);
|
|
ESP_LOGI(TAG, "connected: %s conn_id=%d", addr_str, param->connect.conn_id);
|
|
event_report_connect(addr_str, "ble");
|
|
break;
|
|
}
|
|
|
|
case ESP_GATTS_DISCONNECT_EVT: {
|
|
s_ble.connected = false;
|
|
char addr_str[18];
|
|
bda_to_str(param->disconnect.remote_bda, addr_str);
|
|
ESP_LOGI(TAG, "disconnected: %s reason=0x%x", addr_str, param->disconnect.reason);
|
|
event_report_disconnect(addr_str, "ble");
|
|
|
|
/* Restart advertising if we were advertising before the connection */
|
|
if (s_ble.advertising) {
|
|
esp_ble_gap_start_advertising(&s_adv_params);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ESP_GATTS_START_EVT:
|
|
ESP_LOGI(TAG, "service started, handle=%d status=%d",
|
|
param->start.service_handle, param->start.status);
|
|
break;
|
|
|
|
case ESP_GATTS_STOP_EVT:
|
|
ESP_LOGI(TAG, "service stopped, handle=%d", param->stop.service_handle);
|
|
break;
|
|
|
|
case ESP_GATTS_DELETE_EVT:
|
|
ESP_LOGI(TAG, "service deleted, handle=%d", param->del.service_handle);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* State query */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
bool bt_ble_is_enabled(void)
|
|
{
|
|
return s_ble.enabled;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Command handlers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void cmd_ble_enable(const char *id, cJSON *params)
|
|
{
|
|
(void)params;
|
|
|
|
if (s_ble.enabled) {
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
return;
|
|
}
|
|
|
|
/* Release classic BT memory if we only need BLE */
|
|
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
|
|
esp_err_t ret = esp_bt_controller_init(&bt_cfg);
|
|
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
|
|
ESP_LOGE(TAG, "controller init failed: %s", esp_err_to_name(ret));
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", esp_err_to_name(ret));
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
|
|
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
|
|
ESP_LOGE(TAG, "controller enable failed: %s", esp_err_to_name(ret));
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", esp_err_to_name(ret));
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
ret = esp_bluedroid_init();
|
|
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
|
|
ESP_LOGE(TAG, "bluedroid init failed: %s", esp_err_to_name(ret));
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", esp_err_to_name(ret));
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
ret = esp_bluedroid_enable();
|
|
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
|
|
ESP_LOGE(TAG, "bluedroid enable failed: %s", esp_err_to_name(ret));
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", esp_err_to_name(ret));
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
esp_ble_gap_register_callback(gap_event_handler);
|
|
esp_ble_gatts_register_callback(gatts_event_handler);
|
|
|
|
ret = esp_ble_gatts_app_register(GATTS_APP_ID);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "GATTS app register failed: %s", esp_err_to_name(ret));
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", esp_err_to_name(ret));
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
esp_ble_gatt_set_local_mtu(517);
|
|
|
|
s_ble.enabled = true;
|
|
ESP_LOGI(TAG, "BLE enabled");
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
}
|
|
|
|
void cmd_ble_disable(const char *id, cJSON *params)
|
|
{
|
|
(void)params;
|
|
|
|
if (!s_ble.enabled) {
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
return;
|
|
}
|
|
|
|
if (s_ble.advertising) {
|
|
esp_ble_gap_stop_advertising();
|
|
s_ble.advertising = false;
|
|
}
|
|
|
|
esp_ble_gatts_app_unregister(s_ble.gatts_if);
|
|
esp_bluedroid_disable();
|
|
esp_bluedroid_deinit();
|
|
esp_bt_controller_disable();
|
|
esp_bt_controller_deinit();
|
|
|
|
s_ble.enabled = false;
|
|
s_ble.gatts_if = 0;
|
|
ESP_LOGI(TAG, "BLE disabled");
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
}
|
|
|
|
void cmd_ble_advertise(const char *id, cJSON *params)
|
|
{
|
|
if (!s_ble.enabled) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "ble_not_enabled");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
bool enable = true;
|
|
cJSON *j_enable = cJSON_GetObjectItem(params, "enable");
|
|
if (cJSON_IsBool(j_enable)) {
|
|
enable = cJSON_IsTrue(j_enable);
|
|
}
|
|
|
|
cJSON *j_interval = cJSON_GetObjectItem(params, "interval_ms");
|
|
if (cJSON_IsNumber(j_interval)) {
|
|
/* Convert ms to 0.625ms units */
|
|
uint16_t raw = (uint16_t)(j_interval->valuedouble / 0.625);
|
|
if (raw < 0x20) raw = 0x20;
|
|
if (raw > 0x4000) raw = 0x4000;
|
|
s_adv_params.adv_int_min = raw;
|
|
s_adv_params.adv_int_max = raw;
|
|
}
|
|
|
|
if (enable) {
|
|
esp_err_t ret = esp_ble_gap_start_advertising(&s_adv_params);
|
|
if (ret != ESP_OK) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", esp_err_to_name(ret));
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
s_ble.advertising = true;
|
|
} else {
|
|
esp_ble_gap_stop_advertising();
|
|
s_ble.advertising = false;
|
|
}
|
|
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
}
|
|
|
|
void cmd_ble_set_adv_data(const char *id, cJSON *params)
|
|
{
|
|
if (!s_ble.enabled) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "ble_not_enabled");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
cJSON *j_name = cJSON_GetObjectItem(params, "name");
|
|
if (cJSON_IsString(j_name)) {
|
|
esp_ble_gap_set_device_name(j_name->valuestring);
|
|
}
|
|
|
|
esp_ble_adv_data_t adv_data = {
|
|
.set_scan_rsp = false,
|
|
.include_name = true,
|
|
.include_txpower = true,
|
|
.min_interval = 0x0006,
|
|
.max_interval = 0x0010,
|
|
.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),
|
|
};
|
|
|
|
/* Service UUIDs */
|
|
uint8_t svc_uuid_buf[16 * 8]; /* up to 8 UUIDs */
|
|
int svc_uuid_len = 0;
|
|
cJSON *j_uuids = cJSON_GetObjectItem(params, "service_uuids");
|
|
if (cJSON_IsArray(j_uuids)) {
|
|
cJSON *j_uuid;
|
|
cJSON_ArrayForEach(j_uuid, j_uuids) {
|
|
if (cJSON_IsString(j_uuid) && svc_uuid_len < (int)sizeof(svc_uuid_buf) - 16) {
|
|
if (parse_uuid(j_uuid->valuestring, svc_uuid_buf + svc_uuid_len) == 0) {
|
|
svc_uuid_len += 16;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (svc_uuid_len > 0) {
|
|
adv_data.service_uuid_len = svc_uuid_len;
|
|
adv_data.p_service_uuid = svc_uuid_buf;
|
|
}
|
|
|
|
/* Manufacturer data */
|
|
uint8_t mfr_buf[32];
|
|
int mfr_len = 0;
|
|
cJSON *j_mfr = cJSON_GetObjectItem(params, "manufacturer_data");
|
|
if (cJSON_IsString(j_mfr)) {
|
|
mfr_len = hex_to_bytes(j_mfr->valuestring, mfr_buf, sizeof(mfr_buf));
|
|
}
|
|
if (mfr_len > 0) {
|
|
adv_data.manufacturer_len = mfr_len;
|
|
adv_data.p_manufacturer_data = mfr_buf;
|
|
}
|
|
|
|
esp_err_t ret = esp_ble_gap_config_adv_data(&adv_data);
|
|
if (ret != ESP_OK) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", esp_err_to_name(ret));
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
}
|
|
|
|
void cmd_gatt_add_service(const char *id, cJSON *params)
|
|
{
|
|
if (!s_ble.enabled) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "ble_not_enabled");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
if (s_ble.num_services >= MAX_SERVICES) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "max_services_reached");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
cJSON *j_uuid = cJSON_GetObjectItem(params, "uuid");
|
|
if (!cJSON_IsString(j_uuid)) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "missing uuid");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
bool primary = true;
|
|
cJSON *j_primary = cJSON_GetObjectItem(params, "primary");
|
|
if (cJSON_IsBool(j_primary)) {
|
|
primary = cJSON_IsTrue(j_primary);
|
|
}
|
|
|
|
/* Prepare the service entry (handle filled in by CREATE_EVT) */
|
|
service_entry_t *svc = &s_ble.services[s_ble.num_services];
|
|
memset(svc, 0, sizeof(*svc));
|
|
svc->primary = primary;
|
|
if (parse_uuid(j_uuid->valuestring, svc->uuid) != 0) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "invalid uuid");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
s_ble.num_services++;
|
|
|
|
/* Build service ID for ESP API */
|
|
esp_gatt_srvc_id_t srvc_id;
|
|
memset(&srvc_id, 0, sizeof(srvc_id));
|
|
srvc_id.is_primary = primary;
|
|
srvc_id.id.inst_id = 0;
|
|
srvc_id.id.uuid.len = ESP_UUID_LEN_128;
|
|
memcpy(srvc_id.id.uuid.uuid.uuid128, svc->uuid, 16);
|
|
|
|
/* num_handle: 1 (svc) + per char: 1 (char decl) + 1 (char val) + 1 (CCCD) = 3 each, + 1 spare */
|
|
uint16_t num_handle = 1 + MAX_CHARS_PER_SVC * 3 + 1;
|
|
|
|
set_pending(id);
|
|
esp_err_t ret = esp_ble_gatts_create_service(s_ble.gatts_if, &srvc_id, num_handle);
|
|
if (ret != ESP_OK) {
|
|
s_ble.num_services--;
|
|
s_pending_cmd_id[0] = '\0';
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", esp_err_to_name(ret));
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
}
|
|
}
|
|
|
|
void cmd_gatt_add_characteristic(const char *id, cJSON *params)
|
|
{
|
|
if (!s_ble.enabled) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "ble_not_enabled");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
cJSON *j_svc_handle = cJSON_GetObjectItem(params, "service_handle");
|
|
cJSON *j_uuid = cJSON_GetObjectItem(params, "uuid");
|
|
cJSON *j_props = cJSON_GetObjectItem(params, "properties");
|
|
|
|
if (!cJSON_IsNumber(j_svc_handle) || !cJSON_IsString(j_uuid)) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "missing service_handle or uuid");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
uint16_t svc_handle = (uint16_t)j_svc_handle->valuedouble;
|
|
service_entry_t *svc = find_service_by_handle(svc_handle);
|
|
if (!svc) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "unknown service_handle");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
if (svc->num_chars >= MAX_CHARS_PER_SVC) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "max_chars_reached");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
/* Prepare char entry (handle filled in by ADD_CHAR_EVT) */
|
|
char_entry_t *ch = &svc->chars[svc->num_chars];
|
|
memset(ch, 0, sizeof(*ch));
|
|
|
|
if (parse_uuid(j_uuid->valuestring, ch->uuid) != 0) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "invalid uuid");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
ch->properties = properties_from_json(j_props);
|
|
|
|
/* Initial value */
|
|
cJSON *j_value = cJSON_GetObjectItem(params, "value");
|
|
if (cJSON_IsString(j_value)) {
|
|
ch->value_len = hex_to_bytes(j_value->valuestring,
|
|
ch->value, MAX_CHAR_VALUE_LEN);
|
|
}
|
|
|
|
svc->num_chars++;
|
|
|
|
/* Build UUID */
|
|
esp_bt_uuid_t char_uuid;
|
|
char_uuid.len = ESP_UUID_LEN_128;
|
|
memcpy(char_uuid.uuid.uuid128, ch->uuid, 16);
|
|
|
|
/* Permission flags derived from properties */
|
|
esp_gatt_perm_t perm = 0;
|
|
if (ch->properties & ESP_GATT_CHAR_PROP_BIT_READ)
|
|
perm |= ESP_GATT_PERM_READ;
|
|
if (ch->properties & (ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_WRITE_NR))
|
|
perm |= ESP_GATT_PERM_WRITE;
|
|
|
|
/* Auto-add CCCD if notify or indicate */
|
|
esp_attr_control_t auto_rsp = { .auto_rsp = ESP_GATT_RSP_BY_APP };
|
|
|
|
set_pending(id);
|
|
esp_err_t ret = esp_ble_gatts_add_char(svc_handle, &char_uuid,
|
|
perm, ch->properties,
|
|
NULL, &auto_rsp);
|
|
if (ret != ESP_OK) {
|
|
svc->num_chars--;
|
|
s_pending_cmd_id[0] = '\0';
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", esp_err_to_name(ret));
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
/* If the char supports notify/indicate, add a CCCD descriptor */
|
|
if (ch->properties & (ESP_GATT_CHAR_PROP_BIT_NOTIFY | ESP_GATT_CHAR_PROP_BIT_INDICATE)) {
|
|
esp_bt_uuid_t cccd_uuid;
|
|
cccd_uuid.len = ESP_UUID_LEN_16;
|
|
cccd_uuid.uuid.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
|
|
|
|
esp_ble_gatts_add_char_descr(svc_handle, &cccd_uuid,
|
|
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
|
|
NULL, NULL);
|
|
}
|
|
}
|
|
|
|
void cmd_gatt_set_value(const char *id, cJSON *params)
|
|
{
|
|
cJSON *j_handle = cJSON_GetObjectItem(params, "char_handle");
|
|
cJSON *j_value = cJSON_GetObjectItem(params, "value");
|
|
|
|
if (!cJSON_IsNumber(j_handle) || !cJSON_IsString(j_value)) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "missing char_handle or value");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
uint16_t handle = (uint16_t)j_handle->valuedouble;
|
|
char_entry_t *ch = find_char_by_handle(handle);
|
|
if (!ch) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "unknown char_handle");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
ch->value_len = hex_to_bytes(j_value->valuestring, ch->value, MAX_CHAR_VALUE_LEN);
|
|
|
|
esp_ble_gatts_set_attr_value(handle, ch->value_len, ch->value);
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
}
|
|
|
|
void cmd_gatt_notify(const char *id, cJSON *params)
|
|
{
|
|
if (!s_ble.connected) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "not_connected");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
cJSON *j_handle = cJSON_GetObjectItem(params, "char_handle");
|
|
if (!cJSON_IsNumber(j_handle)) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "missing char_handle");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
uint16_t handle = (uint16_t)j_handle->valuedouble;
|
|
char_entry_t *ch = find_char_by_handle(handle);
|
|
if (!ch) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "unknown char_handle");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
if (!ch->notifications_enabled) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "notifications_not_enabled");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
bool need_confirm = (ch->properties & ESP_GATT_CHAR_PROP_BIT_INDICATE) != 0;
|
|
esp_err_t ret = esp_ble_gatts_send_indicate(s_ble.gatts_if, s_ble.conn_id,
|
|
handle, ch->value_len,
|
|
ch->value, need_confirm);
|
|
if (ret != ESP_OK) {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", esp_err_to_name(ret));
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
}
|
|
|
|
void cmd_gatt_clear(const char *id, cJSON *params)
|
|
{
|
|
(void)params;
|
|
|
|
/* Stop and delete all services */
|
|
for (int i = 0; i < s_ble.num_services; i++) {
|
|
service_entry_t *svc = &s_ble.services[i];
|
|
if (svc->handle != 0) {
|
|
esp_ble_gatts_stop_service(svc->handle);
|
|
esp_ble_gatts_delete_service(svc->handle);
|
|
}
|
|
}
|
|
|
|
memset(s_ble.services, 0, sizeof(s_ble.services));
|
|
s_ble.num_services = 0;
|
|
|
|
ESP_LOGI(TAG, "GATT services cleared");
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Init */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void bt_ble_init(void)
|
|
{
|
|
memset(&s_ble, 0, sizeof(s_ble));
|
|
s_pending_cmd_id[0] = '\0';
|
|
ESP_LOGI(TAG, "BLE subsystem ready (max %d services, %d chars each)",
|
|
MAX_SERVICES, MAX_CHARS_PER_SVC);
|
|
}
|