Ryan Malloy 6398a5223a ESP32 Bluetooth test harness MCP server
UART-controlled ESP32 peripheral for automated E2E Bluetooth testing.
Dual-mode (Classic BT + BLE) via Bluedroid on original ESP32.

Firmware (ESP-IDF v5.x, 2511 lines C):
- NDJSON protocol over UART1 (115200 baud)
- System commands: ping, reset, get_info, get_status
- Classic BT: GAP, SPP, all 4 SSP pairing modes
- BLE: GATTS, advertising, GATT service/characteristic management
- 6 device personas: headset, speaker, keyboard, sensor, phone, bare
- Event reporter: thread-safe async event queue to host

Python MCP server (FastMCP, 1626 lines):
- Async serial client with command/response correlation
- Event queue with wait_for pattern matching
- Tools: connection, configure, classic, ble, persona, events
- MCP resources: esp32://status, esp32://events, esp32://personas

Tests: 74 unit tests passing, 5 integration test stubs (skip without hardware)
2026-02-02 15:12:28 -07:00

271 lines
9.4 KiB
C

/*
* personas.c -- Device persona presets for BT test harness.
*
* Each persona configures the ESP32 to emulate a particular class of
* Bluetooth device (headset, keyboard, sensor, etc.) by setting the
* device name, IO capability, Class of Device, and advertising the
* expected GATT services.
*/
#include "personas.h"
#include "protocol.h"
#include "uart_handler.h"
#include "bt_classic.h"
#include "bt_ble.h"
#include "esp_bt_device.h"
#include "esp_gap_ble_api.h"
#include "esp_log.h"
#include "cJSON.h"
#include <string.h>
static const char *TAG = "personas";
/* ------------------------------------------------------------------ */
/* BT SIG base UUID: 0000xxxx-0000-1000-8000-00805f9b34fb */
/* ------------------------------------------------------------------ */
/* Pre-built 128-bit UUID strings for standard services */
static const char UUID_BATTERY[] = "0000180f-0000-1000-8000-00805f9b34fb";
static const char UUID_DEVICE_INFO[] = "0000180a-0000-1000-8000-00805f9b34fb";
static const char UUID_HID[] = "00001812-0000-1000-8000-00805f9b34fb";
static const char UUID_ENV_SENSE[] = "0000181a-0000-1000-8000-00805f9b34fb";
static const char UUID_PHONEBOOK[] = "00001130-0000-1000-8000-00805f9b34fb";
/* ------------------------------------------------------------------ */
/* Per-persona service UUID lists (NULL-terminated) */
/* ------------------------------------------------------------------ */
static const char *svc_headset[] = { UUID_BATTERY, UUID_DEVICE_INFO, NULL };
static const char *svc_speaker[] = { UUID_BATTERY, UUID_DEVICE_INFO, NULL };
static const char *svc_keyboard[] = { UUID_HID, UUID_BATTERY, NULL };
static const char *svc_sensor[] = { UUID_ENV_SENSE, UUID_BATTERY, NULL };
static const char *svc_phone[] = { UUID_PHONEBOOK, UUID_DEVICE_INFO, NULL };
static const char *svc_bare[] = { NULL };
/* ------------------------------------------------------------------ */
/* Persona table */
/* ------------------------------------------------------------------ */
typedef struct {
const char *name;
const char *device_name;
const char *io_cap;
uint32_t device_class;
bool classic_enabled;
bool ble_enabled;
const char **service_uuids;
} persona_t;
static const persona_t personas[] = {
{
.name = "headset",
.device_name = "BT Headset",
.io_cap = IO_CAP_NO_IO,
.device_class = 0x200404, /* Audio, Headset */
.classic_enabled = true,
.ble_enabled = true,
.service_uuids = svc_headset,
},
{
.name = "speaker",
.device_name = "BT Speaker",
.io_cap = IO_CAP_NO_IO,
.device_class = 0x200414, /* Audio, Loudspeaker */
.classic_enabled = true,
.ble_enabled = true,
.service_uuids = svc_speaker,
},
{
.name = "keyboard",
.device_name = "BT Keyboard",
.io_cap = IO_CAP_KEYBOARD_ONLY,
.device_class = 0x002540, /* Peripheral, Keyboard */
.classic_enabled = true,
.ble_enabled = true,
.service_uuids = svc_keyboard,
},
{
.name = "sensor",
.device_name = "Environment Sensor",
.io_cap = IO_CAP_NO_IO,
.device_class = 0, /* BLE only, no CoD */
.classic_enabled = false,
.ble_enabled = true,
.service_uuids = svc_sensor,
},
{
.name = "phone",
.device_name = "Test Phone",
.io_cap = IO_CAP_KEYBOARD_DISPLAY,
.device_class = 0x5A020C, /* Phone, Smart Phone */
.classic_enabled = true,
.ble_enabled = true,
.service_uuids = svc_phone,
},
{
.name = "bare",
.device_name = "ESP32-Test",
.io_cap = IO_CAP_DISPLAY_YESNO,
.device_class = 0x1F00, /* Uncategorized */
.classic_enabled = true,
.ble_enabled = true,
.service_uuids = svc_bare,
},
};
#define NUM_PERSONAS (sizeof(personas) / sizeof(personas[0]))
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
static const persona_t *find_persona(const char *name)
{
for (size_t i = 0; i < NUM_PERSONAS; i++) {
if (strcmp(personas[i].name, name) == 0) {
return &personas[i];
}
}
return NULL;
}
/*
* Build a short-form UUID string ("0x180F") from a full 128-bit UUID.
* Caller must provide a buffer of at least 7 bytes.
*/
static void uuid128_to_short(const char *uuid128, char *out, size_t out_sz)
{
/* Full form: "0000xxxx-0000-1000-8000-00805f9b34fb" */
/* The 16-bit part sits at characters 4..7 */
if (strlen(uuid128) >= 8) {
snprintf(out, out_sz, "0x%.4s", uuid128 + 4);
} else {
snprintf(out, out_sz, "0x????");
}
}
/* ------------------------------------------------------------------ */
/* cmd_load_persona */
/* ------------------------------------------------------------------ */
void cmd_load_persona(const char *id, cJSON *params)
{
const cJSON *j_name = cJSON_GetObjectItem(params, "persona");
if (!cJSON_IsString(j_name)) {
cJSON *err = cJSON_CreateObject();
cJSON_AddStringToObject(err, "error", "missing 'persona' param");
uart_send_response(id, STATUS_ERROR, err);
return;
}
const char *req_name = j_name->valuestring;
const persona_t *p = find_persona(req_name);
if (!p) {
cJSON *err = cJSON_CreateObject();
cJSON_AddStringToObject(err, "error", "unknown persona");
cJSON_AddStringToObject(err, "persona", req_name);
uart_send_response(id, STATUS_ERROR, err);
return;
}
ESP_LOGI(TAG, "loading persona: %s (%s)", p->name, p->device_name);
/* --- Set device name on both stacks --- */
if (p->classic_enabled) {
esp_bt_dev_set_device_name(p->device_name);
}
if (p->ble_enabled) {
esp_ble_gap_set_device_name(p->device_name);
}
/* --- IO capability (Classic BT SSP) --- */
if (p->classic_enabled) {
bt_classic_set_io_cap(p->io_cap);
}
/* --- Class of Device --- */
if (p->classic_enabled && p->device_class != 0) {
bt_classic_set_device_class(p->device_class);
}
/* --- Build response --- */
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "persona", p->name);
cJSON_AddStringToObject(data, "device_name", p->device_name);
cJSON_AddStringToObject(data, "io_cap", p->io_cap);
cJSON_AddBoolToObject(data, "classic", p->classic_enabled);
cJSON_AddBoolToObject(data, "ble", p->ble_enabled);
if (p->device_class != 0) {
char cod_str[12];
snprintf(cod_str, sizeof(cod_str), "0x%06X", (unsigned)p->device_class);
cJSON_AddStringToObject(data, "device_class", cod_str);
}
/*
* Include service UUIDs so the MCP client can set up GATT services
* via gatt_add_service / gatt_add_characteristic commands.
*/
cJSON *svc_arr = cJSON_CreateArray();
for (const char **u = p->service_uuids; u && *u; u++) {
cJSON_AddItemToArray(svc_arr, cJSON_CreateString(*u));
}
cJSON_AddItemToObject(data, "services", svc_arr);
uart_send_response(id, STATUS_OK, data);
}
/* ------------------------------------------------------------------ */
/* cmd_list_personas */
/* ------------------------------------------------------------------ */
void cmd_list_personas(const char *id, cJSON *params)
{
(void)params;
cJSON *arr = cJSON_CreateArray();
for (size_t i = 0; i < NUM_PERSONAS; i++) {
const persona_t *p = &personas[i];
cJSON *obj = cJSON_CreateObject();
cJSON_AddStringToObject(obj, "name", p->name);
cJSON_AddStringToObject(obj, "device_name", p->device_name);
cJSON_AddStringToObject(obj, "io_cap", p->io_cap);
cJSON_AddBoolToObject(obj, "classic", p->classic_enabled);
cJSON_AddBoolToObject(obj, "ble", p->ble_enabled);
if (p->device_class != 0) {
char cod_str[12];
snprintf(cod_str, sizeof(cod_str), "0x%06X", (unsigned)p->device_class);
cJSON_AddStringToObject(obj, "device_class", cod_str);
}
/* Short-form service UUIDs for readability */
cJSON *svc_arr = cJSON_CreateArray();
for (const char **u = p->service_uuids; u && *u; u++) {
char short_uuid[8];
uuid128_to_short(*u, short_uuid, sizeof(short_uuid));
cJSON_AddItemToArray(svc_arr, cJSON_CreateString(short_uuid));
}
cJSON_AddItemToObject(obj, "services", svc_arr);
cJSON_AddItemToArray(arr, obj);
}
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "personas", arr);
uart_send_response(id, STATUS_OK, data);
}
/* ------------------------------------------------------------------ */
/* Init */
/* ------------------------------------------------------------------ */
void personas_init(void)
{
ESP_LOGI(TAG, "%d persona presets available", (int)NUM_PERSONAS);
}