mcbluetooth-esp32/firmware/main/cmd_dispatcher.c
Ryan Malloy ab699bbca3 Add HFP (Hands-Free Profile) support
Implement HFP client (Hands-Free Unit role) for the ESP32 test harness:

Firmware:
- bt_hfp.c/h: Full HFP client with call control, audio, volume, DTMF,
  voice recognition
- Enable HFP in sdkconfig.defaults with Wide Band Speech support
- Add HFP commands/events to protocol.h and cmd_dispatcher.c

Python MCP tools:
- 15 new tools: enable, connect, audio_connect, answer, reject, dial,
  send_dtmf, volume, voice_recognition_start/stop, query_calls, status
- Full protocol constants in protocol.py

Tested: HFP enable returns role='hands_free_unit', ready for AG pairing
2026-02-03 14:34:13 -07:00

229 lines
8.2 KiB
C

#include "cmd_dispatcher.h"
#include "protocol.h"
#include "uart_handler.h"
#include "bt_classic.h"
#include "bt_ble.h"
#include "bt_hid.h"
#include "bt_hfp.h"
#include "personas.h"
#include "esp_system.h"
#include "esp_chip_info.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_gap_bt_api.h"
#include "cJSON.h"
#include <string.h>
#define FW_VERSION "0.1.0"
static const char *TAG = "dispatch";
typedef void (*cmd_handler_t)(const char *id, cJSON *params);
typedef struct {
const char *name;
cmd_handler_t handler;
} cmd_entry_t;
/* --- System command handlers --- */
static void handle_ping(const char *id, cJSON *params)
{
(void)params;
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "pong", cJSON_True);
uart_send_response(id, STATUS_OK, data);
}
static void handle_reset(const char *id, cJSON *params)
{
(void)params;
uart_send_response(id, STATUS_OK, NULL);
vTaskDelay(pdMS_TO_TICKS(100)); /* let the response flush */
esp_restart();
}
static void handle_get_info(const char *id, cJSON *params)
{
(void)params;
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;
}
/* Decode feature flags */
cJSON *features = cJSON_CreateArray();
if (chip.features & CHIP_FEATURE_WIFI_BGN) cJSON_AddItemToArray(features, cJSON_CreateString("wifi"));
if (chip.features & CHIP_FEATURE_BT) cJSON_AddItemToArray(features, cJSON_CreateString("bt"));
if (chip.features & CHIP_FEATURE_BLE) cJSON_AddItemToArray(features, cJSON_CreateString("ble"));
/* BT MAC address */
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_BT);
char mac_str[18];
snprintf(mac_str, sizeof(mac_str), "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "chip_model", model);
cJSON_AddItemToObject(data, "features", features);
cJSON_AddNumberToObject(data, "revision", chip.revision);
cJSON_AddNumberToObject(data, "cores", chip.cores);
cJSON_AddStringToObject(data, "fw_version", FW_VERSION);
cJSON_AddNumberToObject(data, "free_heap", (double)esp_get_free_heap_size());
cJSON_AddStringToObject(data, "bt_mac", mac_str);
uart_send_response(id, STATUS_OK, data);
}
static void handle_get_status(const char *id, cJSON *params)
{
(void)params;
int64_t uptime_ms = esp_timer_get_time() / 1000;
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "uptime_ms", (double)uptime_ms);
cJSON_AddNumberToObject(data, "free_heap", (double)esp_get_free_heap_size());
cJSON_AddBoolToObject(data, "bt_enabled", bt_classic_is_enabled());
cJSON_AddBoolToObject(data, "ble_enabled", bt_ble_is_enabled());
uart_send_response(id, STATUS_OK, data);
}
/* --- Configure command: sets device name, IO capabilities, CoD --- */
static void handle_configure(const char *id, cJSON *params)
{
if (!params) {
cJSON *err = cJSON_CreateObject();
cJSON_AddStringToObject(err, "error", "params required");
uart_send_response(id, STATUS_ERROR, err);
return;
}
cJSON *applied = cJSON_CreateObject();
cJSON *name = cJSON_GetObjectItem(params, "name");
if (name && cJSON_IsString(name)) {
esp_bt_gap_set_device_name(name->valuestring);
cJSON_AddStringToObject(applied, "name", name->valuestring);
}
cJSON *io_cap = cJSON_GetObjectItem(params, "io_cap");
if (io_cap && cJSON_IsString(io_cap)) {
bt_classic_set_io_cap(io_cap->valuestring);
cJSON_AddStringToObject(applied, "io_cap", io_cap->valuestring);
}
cJSON *device_class = cJSON_GetObjectItem(params, "device_class");
if (device_class && cJSON_IsNumber(device_class)) {
bt_classic_set_device_class((uint32_t)device_class->valuedouble);
cJSON_AddNumberToObject(applied, "device_class", device_class->valuedouble);
}
uart_send_response(id, STATUS_OK, applied);
}
/* --- Command table --- */
static const cmd_entry_t cmd_table[] = {
/* System */
{ CMD_PING, handle_ping },
{ CMD_RESET, handle_reset },
{ CMD_GET_INFO, handle_get_info },
{ CMD_GET_STATUS, handle_get_status },
/* Configuration */
{ CMD_CONFIGURE, handle_configure },
{ CMD_LOAD_PERSONA, cmd_load_persona },
{ CMD_LIST_PERSONAS, cmd_list_personas },
/* Classic BT */
{ CMD_CLASSIC_ENABLE, cmd_classic_enable },
{ CMD_CLASSIC_DISABLE, cmd_classic_disable },
{ CMD_CLASSIC_SET_DISCOVERABLE,cmd_classic_set_discoverable },
{ CMD_CLASSIC_PAIR_RESPOND, cmd_classic_pair_respond },
{ CMD_CLASSIC_SET_SSP_MODE, cmd_classic_set_ssp_mode },
/* SPP */
{ CMD_SPP_SEND, cmd_spp_send },
{ CMD_SPP_DISCONNECT, cmd_spp_disconnect },
{ CMD_SPP_STATUS, cmd_spp_status },
/* HID */
{ CMD_HID_ENABLE, cmd_hid_enable },
{ CMD_HID_DISABLE, cmd_hid_disable },
{ CMD_HID_CONNECT, cmd_hid_connect },
{ CMD_HID_DISCONNECT, cmd_hid_disconnect },
{ CMD_HID_SEND_KEYBOARD, cmd_hid_send_keyboard },
{ CMD_HID_SEND_MOUSE, cmd_hid_send_mouse },
{ CMD_HID_STATUS, cmd_hid_status },
/* HFP */
{ CMD_HFP_ENABLE, cmd_hfp_enable },
{ CMD_HFP_DISABLE, cmd_hfp_disable },
{ CMD_HFP_CONNECT, cmd_hfp_connect },
{ CMD_HFP_DISCONNECT, cmd_hfp_disconnect },
{ CMD_HFP_AUDIO_CONNECT, cmd_hfp_audio_connect },
{ CMD_HFP_AUDIO_DISCONNECT, cmd_hfp_audio_disconnect },
{ CMD_HFP_ANSWER, cmd_hfp_answer },
{ CMD_HFP_REJECT, cmd_hfp_reject },
{ CMD_HFP_DIAL, cmd_hfp_dial },
{ CMD_HFP_SEND_DTMF, cmd_hfp_send_dtmf },
{ CMD_HFP_VOLUME, cmd_hfp_volume },
{ CMD_HFP_VOICE_RECOGNITION_START, cmd_hfp_voice_recognition_start },
{ CMD_HFP_VOICE_RECOGNITION_STOP, cmd_hfp_voice_recognition_stop },
{ CMD_HFP_QUERY_CALLS, cmd_hfp_query_calls },
{ CMD_HFP_STATUS, cmd_hfp_status },
/* BLE */
{ CMD_BLE_ENABLE, cmd_ble_enable },
{ CMD_BLE_DISABLE, cmd_ble_disable },
{ CMD_BLE_ADVERTISE, cmd_ble_advertise },
{ CMD_BLE_SET_ADV_DATA, cmd_ble_set_adv_data },
/* GATT */
{ CMD_GATT_ADD_SERVICE, cmd_gatt_add_service },
{ CMD_GATT_ADD_CHARACTERISTIC, cmd_gatt_add_characteristic },
{ CMD_GATT_SET_VALUE, cmd_gatt_set_value },
{ CMD_GATT_NOTIFY, cmd_gatt_notify },
{ CMD_GATT_CLEAR, cmd_gatt_clear },
{ NULL, NULL } /* sentinel */
};
void cmd_dispatcher_init(void)
{
ESP_LOGI(TAG, "command dispatcher ready (%d commands)",
(int)(sizeof(cmd_table) / sizeof(cmd_table[0])) - 1);
}
void cmd_dispatch(const char *id, const char *cmd, cJSON *params)
{
for (const cmd_entry_t *entry = cmd_table; entry->name != NULL; entry++) {
if (strcmp(entry->name, cmd) == 0) {
entry->handler(id, params);
return;
}
}
ESP_LOGW(TAG, "unknown command: %s", cmd);
cJSON *err = cJSON_CreateObject();
cJSON_AddStringToObject(err, "error", "unknown_command");
cJSON_AddStringToObject(err, "cmd", cmd);
uart_send_response(id, STATUS_ERROR, err);
}