From 5a853c15fc37720bec6f486bd892535a6519f570 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 2 Feb 2026 21:05:28 -0700 Subject: [PATCH] Fix event system init and add SSP auto-accept for E2E testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- firmware/main/bt_classic.c | 25 +++++++++++++++++++----- firmware/main/main.c | 2 ++ src/mcbluetooth_esp32/tools/configure.py | 16 +++++++++++++-- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/firmware/main/bt_classic.c b/firmware/main/bt_classic.c index f9673af..45c4b5d 100644 --- a/firmware/main/bt_classic.c +++ b/firmware/main/bt_classic.c @@ -46,6 +46,7 @@ static struct { 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]; @@ -132,10 +133,16 @@ static void gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) event_report_pair_request(addr_str, "numeric_comparison", (int)passkey); - /* 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; + 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; } @@ -575,11 +582,19 @@ void cmd_classic_set_ssp_mode(const char *id, cJSON *params) esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap, sizeof(iocap)); - ESP_LOGI(TAG, "SSP mode set to '%s' (io_cap=%d)", mode, new_cap); + /* 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); } diff --git a/firmware/main/main.c b/firmware/main/main.c index 4401757..1644a05 100644 --- a/firmware/main/main.c +++ b/firmware/main/main.c @@ -1,5 +1,6 @@ #include "protocol.h" #include "uart_handler.h" +#include "event_reporter.h" #include "cmd_dispatcher.h" #include "nvs_flash.h" @@ -57,6 +58,7 @@ void app_main(void) } uart_handler_init(); + event_reporter_init(); send_boot_event(); cmd_dispatcher_init(); diff --git a/src/mcbluetooth_esp32/tools/configure.py b/src/mcbluetooth_esp32/tools/configure.py index 4e9bd30..e7480e4 100644 --- a/src/mcbluetooth_esp32/tools/configure.py +++ b/src/mcbluetooth_esp32/tools/configure.py @@ -74,7 +74,10 @@ def register_tools(mcp: FastMCP) -> None: return {"error": str(e)} @mcp.tool() - async def esp32_set_ssp_mode(mode: str) -> dict[str, Any]: + async def esp32_set_ssp_mode( + mode: str, + auto_accept: bool = False, + ) -> dict[str, Any]: """Set the Secure Simple Pairing (SSP) mode on the ESP32. SSP mode determines which pairing association model the ESP32 @@ -90,6 +93,11 @@ def register_tools(mcp: FastMCP) -> None: io_cap). "passkey_display" — ESP32 displays a passkey for the remote device to enter. + auto_accept: When true, the ESP32 automatically confirms + numeric comparison pairing requests without waiting for + a classic_pair_respond command. Useful for automated + E2E testing where both sides need to confirm but the + MCP tool calls are sequential. Returns: Response data confirming the new SSP mode, or an error dict @@ -99,9 +107,13 @@ def register_tools(mcp: FastMCP) -> None: if mode not in valid_modes: return {"error": f"Invalid SSP mode '{mode}'. Must be one of: {sorted(valid_modes)}"} + params: dict[str, Any] = {"mode": mode} + if auto_accept: + params["auto_accept"] = True + try: client = get_client() - resp = await client.send_command(CMD_CLASSIC_SET_SSP_MODE, {"mode": mode}) + resp = await client.send_command(CMD_CLASSIC_SET_SSP_MODE, params) if resp.status == Status.OK: return resp.data return {"error": resp.data.get("error", "Failed to set SSP mode")}