Two fixes for the E2E test failures: 1. event_reporter_init() was never called in app_main(), so the FreeRTOS queue and reporter task were never created. Every BT event (pair_request, gatt_read, gatt_write, gatt_subscribe) was silently dropped at the NULL-queue guard. 2. SSP Numeric Comparison requires both sides to confirm, but bt_pair blocks until completion — creating a deadlock since the LLM can't send classic_pair_respond while waiting. Added auto_accept flag to set_ssp_mode that auto-confirms numeric comparison requests in the GAP callback.
658 lines
22 KiB
C
658 lines
22 KiB
C
/*
|
|
* bt_classic.c -- Classic Bluetooth GAP/SSP + minimal SPP peripheral.
|
|
*
|
|
* Provides pairing support across all four SSP association models
|
|
* (Just Works, Numeric Comparison, Passkey Display, Passkey Entry)
|
|
* plus legacy PIN for pre-2.1 devices. SPP is registered so remote
|
|
* devices have a service to connect to.
|
|
*/
|
|
|
|
#include "bt_classic.h"
|
|
#include "protocol.h"
|
|
#include "uart_handler.h"
|
|
#include "event_reporter.h"
|
|
|
|
#include "esp_bt.h"
|
|
#include "esp_bt_main.h"
|
|
#include "esp_gap_bt_api.h"
|
|
#include "esp_bt_device.h"
|
|
#include "esp_spp_api.h"
|
|
#include "esp_log.h"
|
|
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
static const char *TAG = "classic";
|
|
|
|
#define SPP_SERVER_NAME "mcbt_spp"
|
|
#define SPP_TAG "spp"
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Module state */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
typedef enum {
|
|
PAIR_TYPE_NONE = 0,
|
|
PAIR_TYPE_NUMERIC_COMPARISON,
|
|
PAIR_TYPE_PASSKEY_DISPLAY,
|
|
PAIR_TYPE_PASSKEY_ENTRY,
|
|
PAIR_TYPE_PIN_REQUEST,
|
|
} pair_type_t;
|
|
|
|
static struct {
|
|
bool enabled;
|
|
bool discoverable;
|
|
esp_bt_io_cap_t io_cap;
|
|
char pin_code[17];
|
|
bool ssp_enabled;
|
|
bool auto_accept; /* Auto-confirm SSP pairing (for testing) */
|
|
/* Pending pairing state */
|
|
char pending_pair_address[18];
|
|
char pending_pair_cmd_id[32];
|
|
bool pair_pending;
|
|
pair_type_t pair_type;
|
|
/* SPP handle for the listening server */
|
|
uint32_t spp_handle;
|
|
} classic_state = {
|
|
.io_cap = ESP_BT_IO_CAP_IO, /* display+yesno (numeric comparison) */
|
|
.ssp_enabled = true,
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Address helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static bool parse_bd_addr(const char *str, esp_bd_addr_t addr)
|
|
{
|
|
if (!str || strlen(str) < 17) {
|
|
return false;
|
|
}
|
|
unsigned int b[6];
|
|
int rc = sscanf(str, "%02x:%02x:%02x:%02x:%02x:%02x",
|
|
&b[0], &b[1], &b[2], &b[3], &b[4], &b[5]);
|
|
if (rc != 6) {
|
|
/* try uppercase */
|
|
rc = sscanf(str, "%02X:%02X:%02X:%02X:%02X:%02X",
|
|
&b[0], &b[1], &b[2], &b[3], &b[4], &b[5]);
|
|
}
|
|
if (rc != 6) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < 6; i++) {
|
|
addr[i] = (uint8_t)b[i];
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void bd_addr_to_str(const esp_bd_addr_t addr, char *str)
|
|
{
|
|
sprintf(str, "%02X:%02X:%02X:%02X:%02X:%02X",
|
|
addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* GAP callback */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
|
|
{
|
|
char addr_str[18];
|
|
|
|
switch (event) {
|
|
|
|
/* --- Pairing complete --- */
|
|
case ESP_BT_GAP_AUTH_CMPL_EVT: {
|
|
bd_addr_to_str(param->auth_cmpl.bda, addr_str);
|
|
bool ok = (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS);
|
|
|
|
ESP_LOGI(TAG, "auth_cmpl: %s %s (lk_type=%d)",
|
|
addr_str, ok ? "success" : "FAIL",
|
|
param->auth_cmpl.lk_type);
|
|
|
|
event_report_pair_complete(addr_str, ok);
|
|
|
|
/* If a pair_respond command is waiting, resolve it now. */
|
|
if (classic_state.pair_pending &&
|
|
strcmp(classic_state.pending_pair_address, addr_str) == 0) {
|
|
cJSON *d = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(d, "address", addr_str);
|
|
cJSON_AddBoolToObject(d, "success", ok);
|
|
uart_send_response(classic_state.pending_pair_cmd_id, STATUS_OK, d);
|
|
classic_state.pair_pending = false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
/* --- Numeric Comparison (SSP model 2) --- */
|
|
case ESP_BT_GAP_CFM_REQ_EVT: {
|
|
bd_addr_to_str(param->cfm_req.bda, addr_str);
|
|
uint32_t passkey = param->cfm_req.num_val;
|
|
|
|
ESP_LOGI(TAG, "cfm_req: %s passkey=%06" PRIu32, addr_str, passkey);
|
|
|
|
event_report_pair_request(addr_str, "numeric_comparison", (int)passkey);
|
|
|
|
if (classic_state.auto_accept) {
|
|
/* Auto-confirm for automated E2E testing */
|
|
esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
|
|
ESP_LOGI(TAG, "cfm_req: auto-accepted (auto_accept=true)");
|
|
} else {
|
|
/* Stash address so pair_respond can reply */
|
|
strncpy(classic_state.pending_pair_address, addr_str,
|
|
sizeof(classic_state.pending_pair_address) - 1);
|
|
classic_state.pair_type = PAIR_TYPE_NUMERIC_COMPARISON;
|
|
}
|
|
break;
|
|
}
|
|
|
|
/* --- Passkey Display (SSP model 3 -- we show, remote enters) --- */
|
|
case ESP_BT_GAP_KEY_NOTIF_EVT: {
|
|
bd_addr_to_str(param->key_notif.bda, addr_str);
|
|
uint32_t passkey = param->key_notif.passkey;
|
|
|
|
ESP_LOGI(TAG, "key_notif: %s passkey=%06" PRIu32, addr_str, passkey);
|
|
|
|
event_report_pair_request(addr_str, "passkey_display", (int)passkey);
|
|
|
|
/* Nothing to wait for -- the remote side enters the key. */
|
|
break;
|
|
}
|
|
|
|
/* --- Passkey Entry (SSP model 4 -- we must enter) --- */
|
|
case ESP_BT_GAP_KEY_REQ_EVT: {
|
|
bd_addr_to_str(param->key_req.bda, addr_str);
|
|
|
|
ESP_LOGI(TAG, "key_req: %s (waiting for passkey)", addr_str);
|
|
|
|
event_report_pair_request(addr_str, "passkey_entry", 0);
|
|
|
|
strncpy(classic_state.pending_pair_address, addr_str,
|
|
sizeof(classic_state.pending_pair_address) - 1);
|
|
classic_state.pair_type = PAIR_TYPE_PASSKEY_ENTRY;
|
|
break;
|
|
}
|
|
|
|
/* --- Legacy PIN request (pre-2.1 devices) --- */
|
|
case ESP_BT_GAP_PIN_REQ_EVT: {
|
|
bd_addr_to_str(param->pin_req.bda, addr_str);
|
|
bool min_16 = param->pin_req.min_16_digit;
|
|
|
|
ESP_LOGI(TAG, "pin_req: %s min_16=%d", addr_str, min_16);
|
|
|
|
/* Auto-reply if a PIN is pre-configured */
|
|
if (classic_state.pin_code[0] != '\0') {
|
|
uint8_t pin_len = (uint8_t)strlen(classic_state.pin_code);
|
|
esp_bt_pin_code_t pin;
|
|
memset(pin, 0, sizeof(pin));
|
|
memcpy(pin, classic_state.pin_code, pin_len);
|
|
esp_bt_gap_pin_reply(param->pin_req.bda, true, pin_len, pin);
|
|
|
|
ESP_LOGI(TAG, "pin_req: auto-replied with stored PIN");
|
|
|
|
/* Still report the event so the host sees what happened */
|
|
event_report_pair_request(addr_str, "pin_request", 0);
|
|
break;
|
|
}
|
|
|
|
/* No stored PIN -- ask the host */
|
|
event_report_pair_request(addr_str, "pin_request", 0);
|
|
|
|
strncpy(classic_state.pending_pair_address, addr_str,
|
|
sizeof(classic_state.pending_pair_address) - 1);
|
|
classic_state.pair_type = PAIR_TYPE_PIN_REQUEST;
|
|
break;
|
|
}
|
|
|
|
/* --- Discovery (we're a peripheral, not scanning -- just log) --- */
|
|
case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
|
|
ESP_LOGI(TAG, "discovery state changed: %d",
|
|
param->disc_st_chg.state);
|
|
break;
|
|
|
|
case ESP_BT_GAP_DISC_RES_EVT:
|
|
/* We don't initiate discovery, ignore. */
|
|
break;
|
|
|
|
default:
|
|
ESP_LOGD(TAG, "gap event %d (unhandled)", event);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* SPP callback (minimal -- just report connects/disconnects) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param)
|
|
{
|
|
char addr_str[18];
|
|
|
|
switch (event) {
|
|
|
|
case ESP_SPP_INIT_EVT:
|
|
if (param->init.status == ESP_SPP_SUCCESS) {
|
|
esp_spp_start_srv(ESP_SPP_SEC_AUTHENTICATE, ESP_SPP_ROLE_SLAVE,
|
|
0, SPP_SERVER_NAME);
|
|
ESP_LOGI(SPP_TAG, "SPP initialised, starting server");
|
|
} else {
|
|
ESP_LOGE(SPP_TAG, "SPP init failed: %d", param->init.status);
|
|
}
|
|
break;
|
|
|
|
case ESP_SPP_SRV_OPEN_EVT:
|
|
bd_addr_to_str(param->srv_open.rem_bda, addr_str);
|
|
classic_state.spp_handle = param->srv_open.handle;
|
|
ESP_LOGI(SPP_TAG, "SPP client connected: %s (handle=%" PRIu32 ")",
|
|
addr_str, param->srv_open.handle);
|
|
event_report_connect(addr_str, "classic");
|
|
break;
|
|
|
|
case ESP_SPP_CLOSE_EVT:
|
|
ESP_LOGI(SPP_TAG, "SPP disconnected (handle=%" PRIu32 ")",
|
|
param->close.handle);
|
|
/* We don't have the remote address in CLOSE_EVT on all IDF versions,
|
|
* so report with handle info. */
|
|
{
|
|
cJSON *d = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(d, "handle", (double)param->close.handle);
|
|
cJSON_AddStringToObject(d, "transport", "classic");
|
|
event_report(EVT_DISCONNECT, d);
|
|
}
|
|
if (classic_state.spp_handle == param->close.handle) {
|
|
classic_state.spp_handle = 0;
|
|
}
|
|
break;
|
|
|
|
case ESP_SPP_START_EVT:
|
|
if (param->start.status == ESP_SPP_SUCCESS) {
|
|
ESP_LOGI(SPP_TAG, "SPP server started (scn=%d)", param->start.scn);
|
|
} else {
|
|
ESP_LOGE(SPP_TAG, "SPP server start failed: %d",
|
|
param->start.status);
|
|
}
|
|
break;
|
|
|
|
case ESP_SPP_DATA_IND_EVT:
|
|
/* Data received over SPP -- log but don't process for now. */
|
|
ESP_LOGI(SPP_TAG, "SPP data: %d bytes on handle %" PRIu32,
|
|
param->data_ind.len, param->data_ind.handle);
|
|
break;
|
|
|
|
default:
|
|
ESP_LOGD(SPP_TAG, "spp event %d", event);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* IO capability string mapping */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static bool str_to_io_cap(const char *s, esp_bt_io_cap_t *out)
|
|
{
|
|
if (!s || !out) return false;
|
|
|
|
if (strcmp(s, IO_CAP_DISPLAY_ONLY) == 0) {
|
|
*out = ESP_BT_IO_CAP_OUT;
|
|
} else if (strcmp(s, IO_CAP_DISPLAY_YESNO) == 0) {
|
|
*out = ESP_BT_IO_CAP_IO;
|
|
} else if (strcmp(s, IO_CAP_KEYBOARD_ONLY) == 0) {
|
|
*out = ESP_BT_IO_CAP_IN;
|
|
} else if (strcmp(s, IO_CAP_NO_IO) == 0) {
|
|
*out = ESP_BT_IO_CAP_NONE;
|
|
} else if (strcmp(s, IO_CAP_KEYBOARD_DISPLAY) == 0) {
|
|
*out = ESP_BT_IO_CAP_IO; /* closest match in Bluedroid */
|
|
} else {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Command handlers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void cmd_classic_enable(const char *id, cJSON *params)
|
|
{
|
|
(void)params;
|
|
|
|
if (classic_state.enabled) {
|
|
uart_send_response(id, STATUS_OK, cJSON_CreateString("already enabled"));
|
|
return;
|
|
}
|
|
|
|
/* Release BLE-only memory when running dual-mode.
|
|
* esp_bt_controller_mem_release(ESP_BT_MODE_BLE) would be called
|
|
* if we only wanted Classic; here we keep both available.
|
|
* If the controller is already initialised (e.g. BLE brought it up),
|
|
* skip controller init. */
|
|
esp_bt_controller_status_t ctrl_status = esp_bt_controller_get_status();
|
|
|
|
if (ctrl_status == ESP_BT_CONTROLLER_STATUS_IDLE) {
|
|
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
|
|
esp_err_t err = esp_bt_controller_init(&bt_cfg);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "controller init failed: %s", esp_err_to_name(err));
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString(esp_err_to_name(err)));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) {
|
|
esp_err_t err = esp_bt_controller_enable(ESP_BT_MODE_BTDM);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "controller enable failed: %s", esp_err_to_name(err));
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString(esp_err_to_name(err)));
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Bluedroid init + enable */
|
|
esp_bluedroid_status_t bd_status = esp_bluedroid_get_status();
|
|
|
|
if (bd_status == ESP_BLUEDROID_STATUS_UNINITIALIZED) {
|
|
esp_err_t err = esp_bluedroid_init();
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "bluedroid init failed: %s", esp_err_to_name(err));
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString(esp_err_to_name(err)));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (esp_bluedroid_get_status() == ESP_BLUEDROID_STATUS_INITIALIZED) {
|
|
esp_err_t err = esp_bluedroid_enable();
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "bluedroid enable failed: %s", esp_err_to_name(err));
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString(esp_err_to_name(err)));
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Register GAP callback */
|
|
esp_bt_gap_register_callback(gap_cb);
|
|
|
|
/* Configure SSP IO capability */
|
|
esp_bt_sp_param_t iocap = classic_state.io_cap;
|
|
esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap,
|
|
sizeof(iocap));
|
|
ESP_LOGI(TAG, "SSP io_cap set to %d", classic_state.io_cap);
|
|
|
|
/* Initialise and register SPP */
|
|
esp_spp_register_callback(spp_cb);
|
|
|
|
esp_spp_cfg_t spp_cfg = {
|
|
.mode = ESP_SPP_MODE_CB,
|
|
.enable_l2cap_ertm = false,
|
|
};
|
|
esp_err_t err = esp_spp_enhanced_init(&spp_cfg);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG, "spp_enhanced_init: %s (trying legacy init)",
|
|
esp_err_to_name(err));
|
|
/* Fall back for older IDF builds that lack enhanced init */
|
|
esp_spp_init(ESP_SPP_MODE_CB);
|
|
}
|
|
|
|
classic_state.enabled = true;
|
|
|
|
ESP_LOGI(TAG, "Classic BT enabled");
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
}
|
|
|
|
void cmd_classic_disable(const char *id, cJSON *params)
|
|
{
|
|
(void)params;
|
|
|
|
if (!classic_state.enabled) {
|
|
uart_send_response(id, STATUS_OK, cJSON_CreateString("already disabled"));
|
|
return;
|
|
}
|
|
|
|
esp_spp_deinit();
|
|
esp_bluedroid_disable();
|
|
|
|
classic_state.enabled = false;
|
|
classic_state.discoverable = false;
|
|
classic_state.pair_pending = false;
|
|
classic_state.spp_handle = 0;
|
|
|
|
ESP_LOGI(TAG, "Classic BT disabled");
|
|
uart_send_response(id, STATUS_OK, NULL);
|
|
}
|
|
|
|
void cmd_classic_set_discoverable(const char *id, cJSON *params)
|
|
{
|
|
if (!classic_state.enabled) {
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString("classic BT not enabled"));
|
|
return;
|
|
}
|
|
|
|
bool discoverable = true;
|
|
const cJSON *j = cJSON_GetObjectItem(params, "discoverable");
|
|
if (cJSON_IsBool(j)) {
|
|
discoverable = cJSON_IsTrue(j);
|
|
}
|
|
|
|
if (discoverable) {
|
|
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE,
|
|
ESP_BT_GENERAL_DISCOVERABLE);
|
|
ESP_LOGI(TAG, "now discoverable + connectable");
|
|
} else {
|
|
esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE,
|
|
ESP_BT_NON_DISCOVERABLE);
|
|
ESP_LOGI(TAG, "now non-discoverable + non-connectable");
|
|
}
|
|
|
|
classic_state.discoverable = discoverable;
|
|
|
|
cJSON *data = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(data, "discoverable", discoverable);
|
|
uart_send_response(id, STATUS_OK, data);
|
|
}
|
|
|
|
void cmd_classic_pair_respond(const char *id, cJSON *params)
|
|
{
|
|
if (!classic_state.enabled) {
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString("classic BT not enabled"));
|
|
return;
|
|
}
|
|
|
|
const cJSON *j_addr = cJSON_GetObjectItem(params, "address");
|
|
const cJSON *j_accept = cJSON_GetObjectItem(params, "accept");
|
|
const cJSON *j_pass = cJSON_GetObjectItem(params, "passkey");
|
|
const cJSON *j_pin = cJSON_GetObjectItem(params, "pin");
|
|
|
|
if (!cJSON_IsString(j_addr)) {
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString("missing 'address'"));
|
|
return;
|
|
}
|
|
|
|
esp_bd_addr_t addr;
|
|
if (!parse_bd_addr(j_addr->valuestring, addr)) {
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString("invalid address format"));
|
|
return;
|
|
}
|
|
|
|
bool accept = cJSON_IsBool(j_accept) ? cJSON_IsTrue(j_accept) : true;
|
|
|
|
/* Determine which pairing reply to issue based on pending type */
|
|
switch (classic_state.pair_type) {
|
|
|
|
case PAIR_TYPE_NUMERIC_COMPARISON:
|
|
esp_bt_gap_ssp_confirm_reply(addr, accept);
|
|
ESP_LOGI(TAG, "ssp_confirm_reply: %s accept=%d",
|
|
j_addr->valuestring, accept);
|
|
break;
|
|
|
|
case PAIR_TYPE_PASSKEY_ENTRY:
|
|
if (!cJSON_IsNumber(j_pass)) {
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString("passkey_entry requires 'passkey'"));
|
|
return;
|
|
}
|
|
esp_bt_gap_ssp_passkey_reply(addr, accept,
|
|
(uint32_t)j_pass->valueint);
|
|
ESP_LOGI(TAG, "ssp_passkey_reply: %s accept=%d passkey=%d",
|
|
j_addr->valuestring, accept, j_pass->valueint);
|
|
break;
|
|
|
|
case PAIR_TYPE_PIN_REQUEST: {
|
|
if (!cJSON_IsString(j_pin)) {
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString("pin_request requires 'pin'"));
|
|
return;
|
|
}
|
|
const char *pin_str = j_pin->valuestring;
|
|
uint8_t pin_len = (uint8_t)strlen(pin_str);
|
|
if (pin_len == 0 || pin_len > 16) {
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString("pin must be 1-16 characters"));
|
|
return;
|
|
}
|
|
esp_bt_pin_code_t pin;
|
|
memset(pin, 0, sizeof(pin));
|
|
memcpy(pin, pin_str, pin_len);
|
|
esp_bt_gap_pin_reply(addr, accept, pin_len, pin);
|
|
ESP_LOGI(TAG, "pin_reply: %s accept=%d len=%d",
|
|
j_addr->valuestring, accept, pin_len);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString("no pending pair request"));
|
|
return;
|
|
}
|
|
|
|
/* Stash the command id so AUTH_CMPL can send the final response */
|
|
strncpy(classic_state.pending_pair_cmd_id, id,
|
|
sizeof(classic_state.pending_pair_cmd_id) - 1);
|
|
classic_state.pending_pair_cmd_id[sizeof(classic_state.pending_pair_cmd_id) - 1] = '\0';
|
|
classic_state.pair_pending = true;
|
|
|
|
/* Don't send the response yet -- AUTH_CMPL will do it. */
|
|
}
|
|
|
|
void cmd_classic_set_ssp_mode(const char *id, cJSON *params)
|
|
{
|
|
if (!classic_state.enabled) {
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString("classic BT not enabled"));
|
|
return;
|
|
}
|
|
|
|
const cJSON *j_mode = cJSON_GetObjectItem(params, "mode");
|
|
if (!cJSON_IsString(j_mode)) {
|
|
uart_send_response(id, STATUS_ERROR,
|
|
cJSON_CreateString("missing 'mode'"));
|
|
return;
|
|
}
|
|
|
|
const char *mode = j_mode->valuestring;
|
|
esp_bt_io_cap_t new_cap;
|
|
|
|
if (strcmp(mode, "just_works") == 0) {
|
|
new_cap = ESP_BT_IO_CAP_NONE;
|
|
} else if (strcmp(mode, "numeric_comparison") == 0) {
|
|
new_cap = ESP_BT_IO_CAP_IO;
|
|
} else if (strcmp(mode, "passkey_entry") == 0) {
|
|
new_cap = ESP_BT_IO_CAP_IN;
|
|
} else if (strcmp(mode, "passkey_display") == 0) {
|
|
new_cap = ESP_BT_IO_CAP_OUT;
|
|
} else {
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "error", "unknown mode");
|
|
cJSON_AddStringToObject(err, "valid",
|
|
"just_works, numeric_comparison, passkey_entry, passkey_display");
|
|
uart_send_response(id, STATUS_ERROR, err);
|
|
return;
|
|
}
|
|
|
|
classic_state.io_cap = new_cap;
|
|
|
|
esp_bt_sp_param_t iocap = new_cap;
|
|
esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap,
|
|
sizeof(iocap));
|
|
|
|
/* Optional auto_accept flag for automated testing */
|
|
const cJSON *j_auto = cJSON_GetObjectItem(params, "auto_accept");
|
|
if (cJSON_IsBool(j_auto)) {
|
|
classic_state.auto_accept = cJSON_IsTrue(j_auto);
|
|
}
|
|
|
|
ESP_LOGI(TAG, "SSP mode set to '%s' (io_cap=%d, auto_accept=%d)",
|
|
mode, new_cap, classic_state.auto_accept);
|
|
|
|
cJSON *data = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(data, "mode", mode);
|
|
cJSON_AddNumberToObject(data, "io_cap", new_cap);
|
|
cJSON_AddBoolToObject(data, "auto_accept", classic_state.auto_accept);
|
|
uart_send_response(id, STATUS_OK, data);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* State query */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
bool bt_classic_is_enabled(void)
|
|
{
|
|
return classic_state.enabled;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Configure helpers (called from the configure command handler) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void bt_classic_set_io_cap(const char *io_cap_str)
|
|
{
|
|
esp_bt_io_cap_t cap;
|
|
if (str_to_io_cap(io_cap_str, &cap)) {
|
|
classic_state.io_cap = cap;
|
|
ESP_LOGI(TAG, "io_cap configured: %s -> %d", io_cap_str, cap);
|
|
|
|
/* Apply immediately if already enabled */
|
|
if (classic_state.enabled) {
|
|
esp_bt_sp_param_t iocap = cap;
|
|
esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap,
|
|
sizeof(iocap));
|
|
}
|
|
} else {
|
|
ESP_LOGW(TAG, "unknown io_cap string: '%s'", io_cap_str);
|
|
}
|
|
}
|
|
|
|
void bt_classic_set_device_class(uint32_t cod_raw)
|
|
{
|
|
esp_bt_cod_t cod;
|
|
memset(&cod, 0, sizeof(cod));
|
|
cod.minor = (cod_raw >> 2) & 0x3F;
|
|
cod.major = (cod_raw >> 8) & 0x1F;
|
|
cod.service = (cod_raw >> 13) & 0x7FF;
|
|
esp_err_t err = esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_MAJOR_MINOR | ESP_BT_SET_COD_SERVICE_CLASS);
|
|
if (err == ESP_OK) {
|
|
ESP_LOGI(TAG, "CoD set to 0x%06" PRIx32, cod_raw);
|
|
} else {
|
|
ESP_LOGE(TAG, "set_cod failed: %s", esp_err_to_name(err));
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Init (called early, before any commands) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void bt_classic_init(void)
|
|
{
|
|
memset(&classic_state, 0, sizeof(classic_state));
|
|
classic_state.io_cap = ESP_BT_IO_CAP_IO;
|
|
classic_state.ssp_enabled = true;
|
|
ESP_LOGI(TAG, "Classic BT module ready");
|
|
}
|