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

137 lines
3.5 KiB
C

#include "uart_handler.h"
#include "protocol.h"
#include "driver/uart.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "cJSON.h"
#include <string.h>
#include <stdlib.h>
static const char *TAG = "uart";
static SemaphoreHandle_t tx_mutex;
/* Pin assignments -- keep UART0 free for ESP-IDF console/logging */
#define UART_TX_PIN GPIO_NUM_4
#define UART_RX_PIN GPIO_NUM_5
void uart_handler_init(void)
{
uart_config_t cfg = {
.baud_rate = PROTO_BAUD_RATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
ESP_ERROR_CHECK(uart_param_config(PROTO_UART_NUM, &cfg));
ESP_ERROR_CHECK(uart_set_pin(PROTO_UART_NUM, UART_TX_PIN, UART_RX_PIN,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_driver_install(PROTO_UART_NUM,
PROTO_RX_BUF_SIZE, PROTO_TX_BUF_SIZE,
0, NULL, 0));
tx_mutex = xSemaphoreCreateMutex();
assert(tx_mutex);
ESP_LOGI(TAG, "UART%d ready (TX=%d RX=%d @ %d baud)",
PROTO_UART_NUM, UART_TX_PIN, UART_RX_PIN, PROTO_BAUD_RATE);
}
char *uart_read_line(void)
{
static char buf[PROTO_MAX_LINE];
int pos = 0;
for (;;) {
uint8_t byte;
int n = uart_read_bytes(PROTO_UART_NUM, &byte, 1, portMAX_DELAY);
if (n <= 0) {
continue;
}
if (byte == '\n') {
if (pos == 0) {
continue; /* skip empty lines */
}
buf[pos] = '\0';
return strdup(buf);
}
if (byte == '\r') {
continue; /* ignore carriage returns */
}
if (pos < PROTO_MAX_LINE - 1) {
buf[pos++] = (char)byte;
} else {
/* line too long -- discard and reset */
ESP_LOGE(TAG, "line overflow (%d bytes), discarding", pos);
pos = 0;
}
}
}
void uart_send_line(const char *json_line)
{
if (!json_line) {
return;
}
xSemaphoreTake(tx_mutex, portMAX_DELAY);
size_t len = strlen(json_line);
uart_write_bytes(PROTO_UART_NUM, json_line, len);
uart_write_bytes(PROTO_UART_NUM, "\n", 1);
xSemaphoreGive(tx_mutex);
}
void uart_send_response(const char *id, const char *status, cJSON *data)
{
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", MSG_TYPE_RESP);
cJSON_AddStringToObject(root, "id", id);
cJSON_AddStringToObject(root, "status", status);
if (data) {
cJSON_AddItemToObject(root, "data", data);
} else {
cJSON_AddObjectToObject(root, "data");
}
char *out = cJSON_PrintUnformatted(root);
uart_send_line(out);
free(out);
cJSON_Delete(root);
}
void uart_send_event(const char *event_name, cJSON *data)
{
int64_t ts_ms = esp_timer_get_time() / 1000;
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", MSG_TYPE_EVENT);
cJSON_AddStringToObject(root, "event", event_name);
if (data) {
cJSON_AddItemToObject(root, "data", data);
} else {
cJSON_AddObjectToObject(root, "data");
}
cJSON_AddNumberToObject(root, "ts", (double)ts_ms);
char *out = cJSON_PrintUnformatted(root);
uart_send_line(out);
free(out);
cJSON_Delete(root);
}