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

101 lines
2.9 KiB
C

#include "protocol.h"
#include "uart_handler.h"
#include "cmd_dispatcher.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_chip_info.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "cJSON.h"
#include <string.h>
#include <stdlib.h>
#define FW_VERSION "0.1.0"
static const char *TAG = "main";
static void send_boot_event(void)
{
esp_chip_info_t chip;
esp_chip_info(&chip);
const char *model;
switch (chip.model) {
case CHIP_ESP32: model = "ESP32"; break;
case CHIP_ESP32S2: model = "ESP32-S2"; break;
case CHIP_ESP32S3: model = "ESP32-S3"; break;
case CHIP_ESP32C3: model = "ESP32-C3"; break;
case CHIP_ESP32H2: model = "ESP32-H2"; break;
default: model = "unknown"; break;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "fw_version", FW_VERSION);
cJSON_AddStringToObject(data, "chip_model", model);
cJSON_AddNumberToObject(data, "cores", chip.cores);
cJSON_AddNumberToObject(data, "revision", chip.revision);
cJSON_AddNumberToObject(data, "free_heap", (double)esp_get_free_heap_size());
uart_send_event(EVT_BOOT, data);
}
void app_main(void)
{
/* NVS -- needed for Bluetooth bonding storage */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "NVS partition issue, erasing and re-initializing");
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
} else {
ESP_ERROR_CHECK(ret);
}
uart_handler_init();
send_boot_event();
cmd_dispatcher_init();
ESP_LOGI(TAG, "mcbluetooth-esp32 v%s ready", FW_VERSION);
/* Main command loop */
for (;;) {
char *line = uart_read_line();
if (!line) {
continue;
}
cJSON *root = cJSON_Parse(line);
if (!root) {
ESP_LOGE(TAG, "bad JSON: %.64s", line);
/* best-effort error -- no id available */
uart_send_response("?", STATUS_ERROR,
cJSON_CreateString("invalid JSON"));
free(line);
continue;
}
const cJSON *j_id = cJSON_GetObjectItem(root, "id");
const cJSON *j_cmd = cJSON_GetObjectItem(root, "cmd");
cJSON *j_params = cJSON_GetObjectItem(root, "params");
const char *id = cJSON_IsString(j_id) ? j_id->valuestring : "?";
const char *cmd = cJSON_IsString(j_cmd) ? j_cmd->valuestring : NULL;
if (!cmd) {
ESP_LOGE(TAG, "missing 'cmd' field");
uart_send_response(id, STATUS_ERROR,
cJSON_CreateString("missing 'cmd' field"));
} else {
cmd_dispatch(id, cmd, j_params);
}
cJSON_Delete(root);
free(line);
}
}