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)
173 lines
5.3 KiB
C
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);
|
|
}
|