Fix event system init and add SSP auto-accept for E2E testing

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.
This commit is contained in:
Ryan Malloy 2026-02-02 21:05:28 -07:00
parent 397b164eee
commit 5a853c15fc
3 changed files with 36 additions and 7 deletions

View File

@ -46,6 +46,7 @@ static struct {
esp_bt_io_cap_t io_cap; esp_bt_io_cap_t io_cap;
char pin_code[17]; char pin_code[17];
bool ssp_enabled; bool ssp_enabled;
bool auto_accept; /* Auto-confirm SSP pairing (for testing) */
/* Pending pairing state */ /* Pending pairing state */
char pending_pair_address[18]; char pending_pair_address[18];
char pending_pair_cmd_id[32]; 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); 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 */ /* Stash address so pair_respond can reply */
strncpy(classic_state.pending_pair_address, addr_str, strncpy(classic_state.pending_pair_address, addr_str,
sizeof(classic_state.pending_pair_address) - 1); sizeof(classic_state.pending_pair_address) - 1);
classic_state.pair_type = PAIR_TYPE_NUMERIC_COMPARISON; classic_state.pair_type = PAIR_TYPE_NUMERIC_COMPARISON;
}
break; 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, esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap,
sizeof(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 *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "mode", mode); cJSON_AddStringToObject(data, "mode", mode);
cJSON_AddNumberToObject(data, "io_cap", new_cap); cJSON_AddNumberToObject(data, "io_cap", new_cap);
cJSON_AddBoolToObject(data, "auto_accept", classic_state.auto_accept);
uart_send_response(id, STATUS_OK, data); uart_send_response(id, STATUS_OK, data);
} }

View File

@ -1,5 +1,6 @@
#include "protocol.h" #include "protocol.h"
#include "uart_handler.h" #include "uart_handler.h"
#include "event_reporter.h"
#include "cmd_dispatcher.h" #include "cmd_dispatcher.h"
#include "nvs_flash.h" #include "nvs_flash.h"
@ -57,6 +58,7 @@ void app_main(void)
} }
uart_handler_init(); uart_handler_init();
event_reporter_init();
send_boot_event(); send_boot_event();
cmd_dispatcher_init(); cmd_dispatcher_init();

View File

@ -74,7 +74,10 @@ def register_tools(mcp: FastMCP) -> None:
return {"error": str(e)} return {"error": str(e)}
@mcp.tool() @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. """Set the Secure Simple Pairing (SSP) mode on the ESP32.
SSP mode determines which pairing association model the ESP32 SSP mode determines which pairing association model the ESP32
@ -90,6 +93,11 @@ def register_tools(mcp: FastMCP) -> None:
io_cap). io_cap).
"passkey_display" ESP32 displays a passkey for the "passkey_display" ESP32 displays a passkey for the
remote device to enter. 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: Returns:
Response data confirming the new SSP mode, or an error dict 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: if mode not in valid_modes:
return {"error": f"Invalid SSP mode '{mode}'. Must be one of: {sorted(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: try:
client = get_client() 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: if resp.status == Status.OK:
return resp.data return resp.data
return {"error": resp.data.get("error", "Failed to set SSP mode")} return {"error": resp.data.get("error", "Failed to set SSP mode")}