mcbluetooth-esp32/firmware/main/event_reporter.c
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

173 lines
5.3 KiB
C

/*
* event_reporter.c -- Drain BT events from a FreeRTOS queue, emit NDJSON.
*/
#include "event_reporter.h"
#include "protocol.h"
#include "uart_handler.h"
#include <string.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "cJSON.h"
#include "esp_log.h"
#define TAG "event"
#define EVENT_QUEUE_DEPTH 32
#define REPORTER_STACK 4096
#define REPORTER_PRIORITY 5
#define EVENT_NAME_MAX 32
/* ------------------------------------------------------------------ */
/* Queue payload */
/* ------------------------------------------------------------------ */
typedef struct {
char event_name[EVENT_NAME_MAX];
cJSON *data; /* ownership transfers to uart_send_event */
} event_msg_t;
static QueueHandle_t s_event_queue;
/* ------------------------------------------------------------------ */
/* Reporter task */
/* ------------------------------------------------------------------ */
static void reporter_task(void *arg)
{
(void)arg;
event_msg_t msg;
for (;;) {
if (xQueueReceive(s_event_queue, &msg, portMAX_DELAY) == pdTRUE) {
/*
* uart_send_event() takes ownership of msg.data via
* cJSON_AddItemToObject -- the root (and therefore data)
* is freed inside uart_send_event. No delete here.
*/
uart_send_event(msg.event_name, msg.data);
}
}
}
/* ------------------------------------------------------------------ */
/* Public API */
/* ------------------------------------------------------------------ */
void event_reporter_init(void)
{
s_event_queue = xQueueCreate(EVENT_QUEUE_DEPTH, sizeof(event_msg_t));
if (!s_event_queue) {
ESP_LOGE(TAG, "failed to create event queue");
return;
}
BaseType_t rc = xTaskCreate(reporter_task, "evt_rpt",
REPORTER_STACK, NULL,
REPORTER_PRIORITY, NULL);
if (rc != pdPASS) {
ESP_LOGE(TAG, "failed to create reporter task");
}
}
void event_report(const char *event_name, cJSON *data)
{
if (!s_event_queue) {
ESP_LOGW(TAG, "reporter not initialised, dropping %s", event_name);
cJSON_Delete(data);
return;
}
event_msg_t msg;
memset(&msg, 0, sizeof(msg));
strncpy(msg.event_name, event_name, EVENT_NAME_MAX - 1);
msg.data = data;
if (xQueueSend(s_event_queue, &msg, 0) != pdTRUE) {
ESP_LOGW(TAG, "event queue full, dropping %s", event_name);
cJSON_Delete(data);
}
}
/* ------------------------------------------------------------------ */
/* Convenience helpers */
/* ------------------------------------------------------------------ */
void event_report_pair_request(const char *address, const char *type,
int passkey)
{
cJSON *d = cJSON_CreateObject();
cJSON_AddStringToObject(d, "address", address);
cJSON_AddStringToObject(d, "type", type);
cJSON_AddNumberToObject(d, "passkey", passkey);
event_report(EVT_PAIR_REQUEST, d);
}
void event_report_pair_complete(const char *address, bool success)
{
cJSON *d = cJSON_CreateObject();
cJSON_AddStringToObject(d, "address", address);
cJSON_AddBoolToObject(d, "success", success);
event_report(EVT_PAIR_COMPLETE, d);
}
void event_report_connect(const char *address, const char *transport)
{
cJSON *d = cJSON_CreateObject();
cJSON_AddStringToObject(d, "address", address);
cJSON_AddStringToObject(d, "transport", transport);
event_report(EVT_CONNECT, d);
}
void event_report_disconnect(const char *address, const char *transport)
{
cJSON *d = cJSON_CreateObject();
cJSON_AddStringToObject(d, "address", address);
cJSON_AddStringToObject(d, "transport", transport);
event_report(EVT_DISCONNECT, d);
}
void event_report_gatt_read(uint16_t char_handle, const char *address)
{
cJSON *d = cJSON_CreateObject();
cJSON_AddNumberToObject(d, "handle", char_handle);
cJSON_AddStringToObject(d, "address", address);
event_report(EVT_GATT_READ, d);
}
void event_report_gatt_write(uint16_t char_handle, const char *address,
const uint8_t *value, uint16_t len)
{
cJSON *d = cJSON_CreateObject();
cJSON_AddNumberToObject(d, "handle", char_handle);
cJSON_AddStringToObject(d, "address", address);
/* Encode raw bytes as a hex string (2 chars per byte + NUL). */
char *hex = malloc((size_t)len * 2 + 1);
if (hex) {
for (uint16_t i = 0; i < len; i++) {
sprintf(hex + i * 2, "%02x", value[i]);
}
hex[len * 2] = '\0';
cJSON_AddStringToObject(d, "value", hex);
free(hex);
} else {
cJSON_AddStringToObject(d, "value", "");
ESP_LOGW(TAG, "hex alloc failed for %u bytes", len);
}
cJSON_AddNumberToObject(d, "length", len);
event_report(EVT_GATT_WRITE, d);
}
void event_report_gatt_subscribe(uint16_t char_handle, bool subscribed)
{
cJSON *d = cJSON_CreateObject();
cJSON_AddNumberToObject(d, "handle", char_handle);
cJSON_AddBoolToObject(d, "subscribed", subscribed);
event_report(EVT_GATT_SUBSCRIBE, d);
}