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
This commit is contained in:
Ryan Malloy 2026-02-03 14:34:13 -07:00
parent 9a8eae1d2f
commit ab699bbca3
9 changed files with 1308 additions and 1 deletions

View File

@ -6,6 +6,7 @@ idf_component_register(
"bt_classic.c" "bt_classic.c"
"bt_ble.c" "bt_ble.c"
"bt_hid.c" "bt_hid.c"
"bt_hfp.c"
"personas.c" "personas.c"
"event_reporter.c" "event_reporter.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."

739
firmware/main/bt_hfp.c Normal file
View File

@ -0,0 +1,739 @@
/**
* @file bt_hfp.c
* @brief HFP (Hands-Free Profile) Client implementation.
*
* Implements the Hands-Free Unit role - the ESP32 acts as a Bluetooth headset
* that can connect to phones/computers for call control and audio.
*/
#include "bt_hfp.h"
#include "protocol.h"
#include "uart_handler.h"
#include "event_reporter.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_hf_client_api.h"
#include "esp_log.h"
#include <string.h>
#include <stdio.h>
static const char *TAG = "bt_hfp";
/* ---------------------------------------------------------------------------
* State
* --------------------------------------------------------------------------- */
static struct {
bool enabled;
bool connected;
bool audio_connected;
bool call_active;
uint8_t remote_addr[6];
char remote_addr_str[18];
char operator_name[32];
int signal_strength;
int battery_level;
int speaker_volume;
int mic_volume;
esp_hf_client_connection_state_t conn_state;
esp_hf_client_audio_state_t audio_state;
esp_hf_call_status_t call_status;
esp_hf_call_setup_status_t call_setup_status;
} s_hfp = {0};
/* ---------------------------------------------------------------------------
* Helpers
* --------------------------------------------------------------------------- */
static void addr_to_str(const uint8_t *addr, char *str)
{
snprintf(str, 18, "%02X:%02X:%02X:%02X:%02X:%02X",
addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
}
static bool str_to_addr(const char *str, uint8_t *addr)
{
unsigned int tmp[6];
if (sscanf(str, "%02X:%02X:%02X:%02X:%02X:%02X",
&tmp[0], &tmp[1], &tmp[2], &tmp[3], &tmp[4], &tmp[5]) != 6) {
return false;
}
for (int i = 0; i < 6; i++) {
addr[i] = (uint8_t)tmp[i];
}
return true;
}
/* ---------------------------------------------------------------------------
* HFP Client Callback
* --------------------------------------------------------------------------- */
static void hfp_client_cb(esp_hf_client_cb_event_t event, esp_hf_client_cb_param_t *param)
{
cJSON *data;
switch (event) {
case ESP_HF_CLIENT_CONNECTION_STATE_EVT:
s_hfp.conn_state = param->conn_stat.state;
memcpy(s_hfp.remote_addr, param->conn_stat.remote_bda, 6);
addr_to_str(s_hfp.remote_addr, s_hfp.remote_addr_str);
switch (param->conn_stat.state) {
case ESP_HF_CLIENT_CONNECTION_STATE_CONNECTED:
ESP_LOGI(TAG, "HFP connected (RFCOMM) to %s", s_hfp.remote_addr_str);
break;
case ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED:
ESP_LOGI(TAG, "HFP SLC connected to %s (peer_feat=0x%04lx)",
s_hfp.remote_addr_str, (unsigned long)param->conn_stat.peer_feat);
s_hfp.connected = true;
data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "address", s_hfp.remote_addr_str);
cJSON_AddNumberToObject(data, "peer_features", param->conn_stat.peer_feat);
event_report(EVT_HFP_CONNECT, data);
break;
case ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTED:
ESP_LOGI(TAG, "HFP disconnected from %s", s_hfp.remote_addr_str);
s_hfp.connected = false;
s_hfp.audio_connected = false;
data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "address", s_hfp.remote_addr_str);
event_report(EVT_HFP_DISCONNECT, data);
break;
default:
break;
}
break;
case ESP_HF_CLIENT_AUDIO_STATE_EVT:
s_hfp.audio_state = param->audio_stat.state;
switch (param->audio_stat.state) {
case ESP_HF_CLIENT_AUDIO_STATE_CONNECTED:
ESP_LOGI(TAG, "HFP audio connected (CVSD)");
s_hfp.audio_connected = true;
data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "codec", "cvsd");
event_report(EVT_HFP_AUDIO_CONNECT, data);
break;
case ESP_HF_CLIENT_AUDIO_STATE_CONNECTED_MSBC:
ESP_LOGI(TAG, "HFP audio connected (mSBC)");
s_hfp.audio_connected = true;
data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "codec", "msbc");
event_report(EVT_HFP_AUDIO_CONNECT, data);
break;
case ESP_HF_CLIENT_AUDIO_STATE_DISCONNECTED:
ESP_LOGI(TAG, "HFP audio disconnected");
s_hfp.audio_connected = false;
event_report(EVT_HFP_AUDIO_DISCONNECT, NULL);
break;
default:
break;
}
break;
case ESP_HF_CLIENT_CIND_CALL_EVT:
s_hfp.call_status = param->call.status;
ESP_LOGI(TAG, "Call indicator: %d", param->call.status);
if (param->call.status == ESP_HF_CALL_STATUS_NO_CALLS) {
s_hfp.call_active = false;
} else {
s_hfp.call_active = true;
}
data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "call_status", param->call.status);
cJSON_AddBoolToObject(data, "active", s_hfp.call_active);
event_report(EVT_HFP_CALL_STATUS, data);
break;
case ESP_HF_CLIENT_CIND_CALL_SETUP_EVT:
s_hfp.call_setup_status = param->call_setup.status;
ESP_LOGI(TAG, "Call setup: %d", param->call_setup.status);
data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "call_setup", param->call_setup.status);
event_report(EVT_HFP_CALL_SETUP, data);
break;
case ESP_HF_CLIENT_RING_IND_EVT:
ESP_LOGI(TAG, "Incoming call - RING");
event_report(EVT_HFP_RING, NULL);
break;
case ESP_HF_CLIENT_CLIP_EVT:
ESP_LOGI(TAG, "Caller ID: %s", param->clip.number ? param->clip.number : "unknown");
data = cJSON_CreateObject();
if (param->clip.number) {
cJSON_AddStringToObject(data, "number", param->clip.number);
}
event_report(EVT_HFP_CLIP, data);
break;
case ESP_HF_CLIENT_CIND_SIGNAL_STRENGTH_EVT:
s_hfp.signal_strength = param->signal_strength.value;
ESP_LOGD(TAG, "Signal strength: %d", param->signal_strength.value);
break;
case ESP_HF_CLIENT_CIND_BATTERY_LEVEL_EVT:
s_hfp.battery_level = param->battery_level.value;
ESP_LOGD(TAG, "Battery level: %d", param->battery_level.value);
break;
case ESP_HF_CLIENT_COPS_CURRENT_OPERATOR_EVT:
if (param->cops.name) {
strncpy(s_hfp.operator_name, param->cops.name, sizeof(s_hfp.operator_name) - 1);
ESP_LOGI(TAG, "Operator: %s", param->cops.name);
}
break;
case ESP_HF_CLIENT_VOLUME_CONTROL_EVT:
if (param->volume_control.type == ESP_HF_VOLUME_CONTROL_TARGET_SPK) {
s_hfp.speaker_volume = param->volume_control.volume;
ESP_LOGI(TAG, "Speaker volume: %d", param->volume_control.volume);
} else {
s_hfp.mic_volume = param->volume_control.volume;
ESP_LOGI(TAG, "Mic volume: %d", param->volume_control.volume);
}
data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "type",
param->volume_control.type == ESP_HF_VOLUME_CONTROL_TARGET_SPK ? "speaker" : "microphone");
cJSON_AddNumberToObject(data, "volume", param->volume_control.volume);
event_report(EVT_HFP_VOLUME, data);
break;
case ESP_HF_CLIENT_BVRA_EVT:
ESP_LOGI(TAG, "Voice recognition: %s",
param->bvra.value == ESP_HF_VR_STATE_ENABLED ? "enabled" : "disabled");
data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "active", param->bvra.value == ESP_HF_VR_STATE_ENABLED);
event_report(EVT_HFP_VOICE_RECOGNITION, data);
break;
case ESP_HF_CLIENT_CLCC_EVT:
ESP_LOGI(TAG, "Call list entry: idx=%d dir=%d status=%d",
param->clcc.idx, param->clcc.dir, param->clcc.status);
data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "index", param->clcc.idx);
cJSON_AddNumberToObject(data, "direction", param->clcc.dir);
cJSON_AddNumberToObject(data, "status", param->clcc.status);
cJSON_AddNumberToObject(data, "mode", param->clcc.mpty);
if (param->clcc.number) {
cJSON_AddStringToObject(data, "number", param->clcc.number);
}
event_report(EVT_HFP_CALL_LIST, data);
break;
case ESP_HF_CLIENT_AT_RESPONSE_EVT:
if (param->at_response.code != ESP_HF_AT_RESPONSE_CODE_OK) {
ESP_LOGW(TAG, "AT response: code=%d cme=%d",
param->at_response.code, param->at_response.cme);
}
break;
default:
ESP_LOGD(TAG, "HFP event: %d", event);
break;
}
}
/* ---------------------------------------------------------------------------
* Public API
* --------------------------------------------------------------------------- */
void bt_hfp_init(void)
{
ESP_LOGI(TAG, "HFP module initialized");
}
bool bt_hfp_is_enabled(void)
{
return s_hfp.enabled;
}
bool bt_hfp_is_connected(void)
{
return s_hfp.connected;
}
bool bt_hfp_is_audio_connected(void)
{
return s_hfp.audio_connected;
}
/* ---------------------------------------------------------------------------
* Command Handlers
* --------------------------------------------------------------------------- */
void cmd_hfp_enable(const char *id, cJSON *params)
{
(void)params;
if (s_hfp.enabled) {
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "enabled", true);
cJSON_AddStringToObject(data, "note", "already enabled");
uart_send_response(id, STATUS_OK, data);
return;
}
esp_err_t err;
/* Register callback */
err = esp_hf_client_register_callback(hfp_client_cb);
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
/* Initialize HFP client */
err = esp_hf_client_init();
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
s_hfp.enabled = true;
s_hfp.speaker_volume = 10;
s_hfp.mic_volume = 10;
ESP_LOGI(TAG, "HFP Client enabled");
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "enabled", true);
cJSON_AddStringToObject(data, "role", "hands_free_unit");
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_disable(const char *id, cJSON *params)
{
(void)params;
if (!s_hfp.enabled) {
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "enabled", false);
cJSON_AddStringToObject(data, "note", "already disabled");
uart_send_response(id, STATUS_OK, data);
return;
}
/* Disconnect if connected */
if (s_hfp.connected) {
esp_hf_client_disconnect(s_hfp.remote_addr);
}
esp_err_t err = esp_hf_client_deinit();
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
s_hfp.enabled = false;
s_hfp.connected = false;
s_hfp.audio_connected = false;
ESP_LOGI(TAG, "HFP Client disabled");
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "enabled", false);
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_connect(const char *id, cJSON *params)
{
if (!s_hfp.enabled) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "HFP not enabled");
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *addr_json = cJSON_GetObjectItem(params, "address");
if (!addr_json || !cJSON_IsString(addr_json)) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "address required");
uart_send_response(id, STATUS_ERROR, e);
return;
}
uint8_t addr[6];
if (!str_to_addr(addr_json->valuestring, addr)) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "invalid address format");
uart_send_response(id, STATUS_ERROR, e);
return;
}
ESP_LOGI(TAG, "Connecting HFP to %s", addr_json->valuestring);
esp_err_t err = esp_hf_client_connect(addr);
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "status", "connecting");
cJSON_AddStringToObject(data, "address", addr_json->valuestring);
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_disconnect(const char *id, cJSON *params)
{
(void)params;
if (!s_hfp.connected) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "not connected");
uart_send_response(id, STATUS_ERROR, e);
return;
}
esp_err_t err = esp_hf_client_disconnect(s_hfp.remote_addr);
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "status", "disconnecting");
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_audio_connect(const char *id, cJSON *params)
{
(void)params;
if (!s_hfp.connected) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "not connected");
uart_send_response(id, STATUS_ERROR, e);
return;
}
if (s_hfp.audio_connected) {
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "audio_connected", true);
cJSON_AddStringToObject(data, "note", "already connected");
uart_send_response(id, STATUS_OK, data);
return;
}
esp_err_t err = esp_hf_client_connect_audio(s_hfp.remote_addr);
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "status", "connecting_audio");
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_audio_disconnect(const char *id, cJSON *params)
{
(void)params;
if (!s_hfp.audio_connected) {
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "audio_connected", false);
cJSON_AddStringToObject(data, "note", "already disconnected");
uart_send_response(id, STATUS_OK, data);
return;
}
esp_err_t err = esp_hf_client_disconnect_audio(s_hfp.remote_addr);
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "status", "disconnecting_audio");
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_answer(const char *id, cJSON *params)
{
(void)params;
if (!s_hfp.connected) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "not connected");
uart_send_response(id, STATUS_ERROR, e);
return;
}
esp_err_t err = esp_hf_client_answer_call();
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "action", "answered");
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_reject(const char *id, cJSON *params)
{
(void)params;
if (!s_hfp.connected) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "not connected");
uart_send_response(id, STATUS_ERROR, e);
return;
}
esp_err_t err = esp_hf_client_reject_call();
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "action", "rejected");
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_dial(const char *id, cJSON *params)
{
if (!s_hfp.connected) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "not connected");
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *number_json = cJSON_GetObjectItem(params, "number");
if (!number_json || !cJSON_IsString(number_json)) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "number required");
uart_send_response(id, STATUS_ERROR, e);
return;
}
esp_err_t err = esp_hf_client_dial(number_json->valuestring);
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "action", "dialing");
cJSON_AddStringToObject(data, "number", number_json->valuestring);
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_send_dtmf(const char *id, cJSON *params)
{
if (!s_hfp.connected) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "not connected");
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *code_json = cJSON_GetObjectItem(params, "code");
if (!code_json || !cJSON_IsString(code_json) || strlen(code_json->valuestring) != 1) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "single character code required");
uart_send_response(id, STATUS_ERROR, e);
return;
}
char code = code_json->valuestring[0];
esp_err_t err = esp_hf_client_send_dtmf(code);
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
char code_str[2] = {code, '\0'};
cJSON_AddStringToObject(data, "dtmf", code_str);
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_volume(const char *id, cJSON *params)
{
if (!s_hfp.connected) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "not connected");
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *type_json = cJSON_GetObjectItem(params, "type");
cJSON *volume_json = cJSON_GetObjectItem(params, "volume");
if (!type_json || !cJSON_IsString(type_json)) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "type required (speaker/microphone)");
uart_send_response(id, STATUS_ERROR, e);
return;
}
if (!volume_json || !cJSON_IsNumber(volume_json)) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "volume required (0-15)");
uart_send_response(id, STATUS_ERROR, e);
return;
}
int volume = (int)volume_json->valuedouble;
if (volume < 0 || volume > 15) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "volume must be 0-15");
uart_send_response(id, STATUS_ERROR, e);
return;
}
esp_hf_volume_control_target_t target;
if (strcmp(type_json->valuestring, "speaker") == 0) {
target = ESP_HF_VOLUME_CONTROL_TARGET_SPK;
s_hfp.speaker_volume = volume;
} else if (strcmp(type_json->valuestring, "microphone") == 0) {
target = ESP_HF_VOLUME_CONTROL_TARGET_MIC;
s_hfp.mic_volume = volume;
} else {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "type must be speaker or microphone");
uart_send_response(id, STATUS_ERROR, e);
return;
}
esp_err_t err = esp_hf_client_volume_update(target, volume);
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "type", type_json->valuestring);
cJSON_AddNumberToObject(data, "volume", volume);
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_voice_recognition_start(const char *id, cJSON *params)
{
(void)params;
if (!s_hfp.connected) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "not connected");
uart_send_response(id, STATUS_ERROR, e);
return;
}
esp_err_t err = esp_hf_client_start_voice_recognition();
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "action", "voice_recognition_started");
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_voice_recognition_stop(const char *id, cJSON *params)
{
(void)params;
if (!s_hfp.connected) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "not connected");
uart_send_response(id, STATUS_ERROR, e);
return;
}
esp_err_t err = esp_hf_client_stop_voice_recognition();
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "action", "voice_recognition_stopped");
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_query_calls(const char *id, cJSON *params)
{
(void)params;
if (!s_hfp.connected) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", "not connected");
uart_send_response(id, STATUS_ERROR, e);
return;
}
esp_err_t err = esp_hf_client_query_current_calls();
if (err != ESP_OK) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "error", esp_err_to_name(err));
uart_send_response(id, STATUS_ERROR, e);
return;
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "action", "querying_calls");
cJSON_AddStringToObject(data, "note", "results via hfp_call_list events");
uart_send_response(id, STATUS_OK, data);
}
void cmd_hfp_status(const char *id, cJSON *params)
{
(void)params;
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "enabled", s_hfp.enabled);
cJSON_AddBoolToObject(data, "connected", s_hfp.connected);
cJSON_AddBoolToObject(data, "audio_connected", s_hfp.audio_connected);
if (s_hfp.connected) {
cJSON_AddStringToObject(data, "remote_address", s_hfp.remote_addr_str);
cJSON_AddBoolToObject(data, "call_active", s_hfp.call_active);
cJSON_AddNumberToObject(data, "signal_strength", s_hfp.signal_strength);
cJSON_AddNumberToObject(data, "battery_level", s_hfp.battery_level);
cJSON_AddNumberToObject(data, "speaker_volume", s_hfp.speaker_volume);
cJSON_AddNumberToObject(data, "mic_volume", s_hfp.mic_volume);
if (strlen(s_hfp.operator_name) > 0) {
cJSON_AddStringToObject(data, "operator", s_hfp.operator_name);
}
}
uart_send_response(id, STATUS_OK, data);
}

122
firmware/main/bt_hfp.h Normal file
View File

@ -0,0 +1,122 @@
/**
* @file bt_hfp.h
* @brief HFP (Hands-Free Profile) Client module for ESP32 test harness.
*
* Implements HFP Hands-Free Unit (HF) role - acts as a Bluetooth headset.
* Connects to phones/computers (Audio Gateways) for call control and audio.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "cJSON.h"
/**
* @brief Initialize HFP module (register callbacks, but don't init stack yet).
*/
void bt_hfp_init(void);
/* Command handlers (dispatched from cmd_dispatcher) */
/**
* @brief Enable HFP Client (Hands-Free Unit).
*
* Initializes the HFP client stack and registers the device as a headset.
* Classic Bluetooth must be enabled first.
*/
void cmd_hfp_enable(const char *id, cJSON *params);
/**
* @brief Disable HFP Client.
*
* Disconnects any active connection and deinitializes the HFP stack.
*/
void cmd_hfp_disable(const char *id, cJSON *params);
/**
* @brief Connect to an Audio Gateway (phone/computer).
*
* Params:
* - address: Bluetooth address of the AG (required)
*/
void cmd_hfp_connect(const char *id, cJSON *params);
/**
* @brief Disconnect from the current Audio Gateway.
*/
void cmd_hfp_disconnect(const char *id, cJSON *params);
/**
* @brief Connect SCO audio channel.
*
* Opens the audio link for voice. Requires an active SLC connection.
*/
void cmd_hfp_audio_connect(const char *id, cJSON *params);
/**
* @brief Disconnect SCO audio channel.
*
* Closes the audio link but keeps the control connection open.
*/
void cmd_hfp_audio_disconnect(const char *id, cJSON *params);
/**
* @brief Answer an incoming call.
*/
void cmd_hfp_answer(const char *id, cJSON *params);
/**
* @brief Reject an incoming call or hang up an active call.
*/
void cmd_hfp_reject(const char *id, cJSON *params);
/**
* @brief Dial a phone number.
*
* Params:
* - number: Phone number to dial (required)
*/
void cmd_hfp_dial(const char *id, cJSON *params);
/**
* @brief Send DTMF tone during an active call.
*
* Params:
* - code: Single DTMF character (0-9, *, #, A-D)
*/
void cmd_hfp_send_dtmf(const char *id, cJSON *params);
/**
* @brief Adjust speaker or microphone volume.
*
* Params:
* - type: "speaker" or "microphone"
* - volume: 0-15
*/
void cmd_hfp_volume(const char *id, cJSON *params);
/**
* @brief Start voice recognition (Siri/Google Assistant).
*/
void cmd_hfp_voice_recognition_start(const char *id, cJSON *params);
/**
* @brief Stop voice recognition.
*/
void cmd_hfp_voice_recognition_stop(const char *id, cJSON *params);
/**
* @brief Query current call list from AG.
*/
void cmd_hfp_query_calls(const char *id, cJSON *params);
/**
* @brief Get HFP status (enabled, connected, audio state, call info).
*/
void cmd_hfp_status(const char *id, cJSON *params);
/* State query functions */
bool bt_hfp_is_enabled(void);
bool bt_hfp_is_connected(void);
bool bt_hfp_is_audio_connected(void);

View File

@ -4,6 +4,7 @@
#include "bt_classic.h" #include "bt_classic.h"
#include "bt_ble.h" #include "bt_ble.h"
#include "bt_hid.h" #include "bt_hid.h"
#include "bt_hfp.h"
#include "personas.h" #include "personas.h"
#include "esp_system.h" #include "esp_system.h"
@ -171,6 +172,23 @@ static const cmd_entry_t cmd_table[] = {
{ CMD_HID_SEND_MOUSE, cmd_hid_send_mouse }, { CMD_HID_SEND_MOUSE, cmd_hid_send_mouse },
{ CMD_HID_STATUS, cmd_hid_status }, { 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 */ /* BLE */
{ CMD_BLE_ENABLE, cmd_ble_enable }, { CMD_BLE_ENABLE, cmd_ble_enable },
{ CMD_BLE_DISABLE, cmd_ble_disable }, { CMD_BLE_DISABLE, cmd_ble_disable },

View File

@ -50,6 +50,23 @@
#define CMD_HID_SEND_MOUSE "hid_send_mouse" #define CMD_HID_SEND_MOUSE "hid_send_mouse"
#define CMD_HID_STATUS "hid_status" #define CMD_HID_STATUS "hid_status"
/* HFP (Hands-Free Profile) commands */
#define CMD_HFP_ENABLE "hfp_enable"
#define CMD_HFP_DISABLE "hfp_disable"
#define CMD_HFP_CONNECT "hfp_connect"
#define CMD_HFP_DISCONNECT "hfp_disconnect"
#define CMD_HFP_AUDIO_CONNECT "hfp_audio_connect"
#define CMD_HFP_AUDIO_DISCONNECT "hfp_audio_disconnect"
#define CMD_HFP_ANSWER "hfp_answer"
#define CMD_HFP_REJECT "hfp_reject"
#define CMD_HFP_DIAL "hfp_dial"
#define CMD_HFP_SEND_DTMF "hfp_send_dtmf"
#define CMD_HFP_VOLUME "hfp_volume"
#define CMD_HFP_VOICE_RECOGNITION_START "hfp_voice_recognition_start"
#define CMD_HFP_VOICE_RECOGNITION_STOP "hfp_voice_recognition_stop"
#define CMD_HFP_QUERY_CALLS "hfp_query_calls"
#define CMD_HFP_STATUS "hfp_status"
/* BLE commands */ /* BLE commands */
#define CMD_BLE_ENABLE "ble_enable" #define CMD_BLE_ENABLE "ble_enable"
#define CMD_BLE_DISABLE "ble_disable" #define CMD_BLE_DISABLE "ble_disable"
@ -83,6 +100,20 @@
#define EVT_HID_DISCONNECT "hid_disconnect" #define EVT_HID_DISCONNECT "hid_disconnect"
#define EVT_HID_DATA "hid_data" #define EVT_HID_DATA "hid_data"
/* HFP events */
#define EVT_HFP_CONNECT "hfp_connect"
#define EVT_HFP_DISCONNECT "hfp_disconnect"
#define EVT_HFP_AUDIO_CONNECT "hfp_audio_connect"
#define EVT_HFP_AUDIO_DISCONNECT "hfp_audio_disconnect"
#define EVT_HFP_RING "hfp_ring"
#define EVT_HFP_CALL_STATUS "hfp_call_status"
#define EVT_HFP_CALL_SETUP "hfp_call_setup"
#define EVT_HFP_CLIP "hfp_clip"
#define EVT_HFP_VOLUME "hfp_volume"
#define EVT_HFP_VOLUME_CHANGE "hfp_volume_change"
#define EVT_HFP_CALL_LIST "hfp_call_list"
#define EVT_HFP_VOICE_RECOGNITION "hfp_voice_recognition"
/* SSP IO capabilities */ /* SSP IO capabilities */
#define IO_CAP_DISPLAY_ONLY "display_only" #define IO_CAP_DISPLAY_ONLY "display_only"
#define IO_CAP_DISPLAY_YESNO "display_yesno" #define IO_CAP_DISPLAY_YESNO "display_yesno"

View File

@ -10,6 +10,13 @@ CONFIG_BT_HID_ENABLED=y
CONFIG_BT_HID_DEVICE_ENABLED=y CONFIG_BT_HID_DEVICE_ENABLED=y
CONFIG_BT_A2DP_ENABLE=n CONFIG_BT_A2DP_ENABLE=n
# HFP (Hands-Free Profile) - headset role
CONFIG_BT_HFP_ENABLE=y
CONFIG_BT_HFP_CLIENT_ENABLE=y
CONFIG_BT_HFP_AG_ENABLE=n
CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI=y
CONFIG_BT_HFP_WBS_ENABLE=y
# GAP & GATTS # GAP & GATTS
CONFIG_BT_GATTS_ENABLE=y CONFIG_BT_GATTS_ENABLE=y
CONFIG_BT_GATTC_ENABLE=n CONFIG_BT_GATTC_ENABLE=n

View File

@ -74,6 +74,23 @@ CMD_HID_SEND_KEYBOARD = "hid_send_keyboard"
CMD_HID_SEND_MOUSE = "hid_send_mouse" CMD_HID_SEND_MOUSE = "hid_send_mouse"
CMD_HID_STATUS = "hid_status" CMD_HID_STATUS = "hid_status"
# HFP (Hands-Free Profile)
CMD_HFP_ENABLE = "hfp_enable"
CMD_HFP_DISABLE = "hfp_disable"
CMD_HFP_CONNECT = "hfp_connect"
CMD_HFP_DISCONNECT = "hfp_disconnect"
CMD_HFP_AUDIO_CONNECT = "hfp_audio_connect"
CMD_HFP_AUDIO_DISCONNECT = "hfp_audio_disconnect"
CMD_HFP_ANSWER = "hfp_answer"
CMD_HFP_REJECT = "hfp_reject"
CMD_HFP_DIAL = "hfp_dial"
CMD_HFP_SEND_DTMF = "hfp_send_dtmf"
CMD_HFP_VOLUME = "hfp_volume"
CMD_HFP_VOICE_RECOGNITION_START = "hfp_voice_recognition_start"
CMD_HFP_VOICE_RECOGNITION_STOP = "hfp_voice_recognition_stop"
CMD_HFP_QUERY_CALLS = "hfp_query_calls"
CMD_HFP_STATUS = "hfp_status"
# BLE # BLE
CMD_BLE_ENABLE = "ble_enable" CMD_BLE_ENABLE = "ble_enable"
CMD_BLE_DISABLE = "ble_disable" CMD_BLE_DISABLE = "ble_disable"
@ -107,6 +124,20 @@ EVT_HID_CONNECT = "hid_connect"
EVT_HID_DISCONNECT = "hid_disconnect" EVT_HID_DISCONNECT = "hid_disconnect"
EVT_HID_DATA = "hid_data" EVT_HID_DATA = "hid_data"
# HFP Events
EVT_HFP_CONNECT = "hfp_connect"
EVT_HFP_DISCONNECT = "hfp_disconnect"
EVT_HFP_AUDIO_CONNECT = "hfp_audio_connect"
EVT_HFP_AUDIO_DISCONNECT = "hfp_audio_disconnect"
EVT_HFP_RING = "hfp_ring"
EVT_HFP_CALL_STATUS = "hfp_call_status"
EVT_HFP_CALL_SETUP = "hfp_call_setup"
EVT_HFP_CLIP = "hfp_clip"
EVT_HFP_VOLUME = "hfp_volume"
EVT_HFP_VOLUME_CHANGE = "hfp_volume_change"
EVT_HFP_CALL_LIST = "hfp_call_list"
EVT_HFP_VOICE_RECOGNITION = "hfp_voice_recognition"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Protocol constants # Protocol constants
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@ -8,7 +8,7 @@ from fastmcp import FastMCP
from .resources import register_resources from .resources import register_resources
from .serial_client import init_client from .serial_client import init_client
from .tools import ble, classic, configure, connection, events, hid, persona from .tools import ble, classic, configure, connection, events, hfp, hid, persona
mcp = FastMCP( mcp = FastMCP(
name="mcbluetooth-esp32", name="mcbluetooth-esp32",
@ -28,6 +28,7 @@ Configure: esp32_configure, esp32_set_ssp_mode
Classic BT: esp32_classic_enable, esp32_classic_set_discoverable, esp32_classic_pair_respond Classic BT: esp32_classic_enable, esp32_classic_set_discoverable, esp32_classic_pair_respond
SPP: esp32_spp_send, esp32_spp_disconnect, esp32_spp_status SPP: esp32_spp_send, esp32_spp_disconnect, esp32_spp_status
HID: esp32_hid_enable, esp32_hid_send_keyboard, esp32_hid_send_mouse, esp32_hid_status HID: esp32_hid_enable, esp32_hid_send_keyboard, esp32_hid_send_mouse, esp32_hid_status
HFP: esp32_hfp_enable, esp32_hfp_connect, esp32_hfp_audio_connect, esp32_hfp_answer, esp32_hfp_dial, esp32_hfp_volume, esp32_hfp_status
BLE: esp32_ble_advertise, esp32_gatt_add_service, esp32_gatt_add_characteristic, esp32_gatt_set_value, esp32_gatt_notify BLE: esp32_ble_advertise, esp32_gatt_add_service, esp32_gatt_add_characteristic, esp32_gatt_set_value, esp32_gatt_notify
Personas: esp32_load_persona, esp32_list_personas Personas: esp32_load_persona, esp32_list_personas
Events: esp32_get_events, esp32_wait_event, esp32_clear_events Events: esp32_get_events, esp32_wait_event, esp32_clear_events
@ -46,6 +47,7 @@ connection.register_tools(mcp)
configure.register_tools(mcp) configure.register_tools(mcp)
classic.register_tools(mcp) classic.register_tools(mcp)
hid.register_tools(mcp) hid.register_tools(mcp)
hfp.register_tools(mcp)
ble.register_tools(mcp) ble.register_tools(mcp)
persona.register_tools(mcp) persona.register_tools(mcp)
events.register_tools(mcp) events.register_tools(mcp)

View File

@ -0,0 +1,356 @@
"""HFP (Hands-Free Profile) tools for ESP32 MCP server.
Provides headset/hands-free functionality for call control and audio over Classic Bluetooth.
The ESP32 acts as a Hands-Free Unit (HF) that connects to phones/computers (Audio Gateways).
"""
from __future__ import annotations
from typing import Any, Literal
from fastmcp import FastMCP
from ..protocol import (
CMD_HFP_ENABLE,
CMD_HFP_DISABLE,
CMD_HFP_CONNECT,
CMD_HFP_DISCONNECT,
CMD_HFP_AUDIO_CONNECT,
CMD_HFP_AUDIO_DISCONNECT,
CMD_HFP_ANSWER,
CMD_HFP_REJECT,
CMD_HFP_DIAL,
CMD_HFP_SEND_DTMF,
CMD_HFP_VOLUME,
CMD_HFP_VOICE_RECOGNITION_START,
CMD_HFP_VOICE_RECOGNITION_STOP,
CMD_HFP_QUERY_CALLS,
CMD_HFP_STATUS,
Status,
)
from ..serial_client import get_client
def register_tools(mcp: FastMCP) -> None:
"""Register HFP tools with the MCP server."""
@mcp.tool()
async def esp32_hfp_enable() -> dict[str, Any]:
"""Enable HFP (Hands-Free Profile) on the ESP32.
Initializes the ESP32 as a Hands-Free Unit (headset role) that can
connect to phones/computers (Audio Gateways) for call control and audio.
Classic Bluetooth must be enabled first via esp32_classic_enable.
Supports:
- Wide Band Speech (mSBC codec) for HD voice
- Call control (answer, reject, dial, DTMF)
- Volume control (speaker and microphone)
- Voice recognition activation (Siri, Google Assistant)
Returns:
Response with enabled status.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_ENABLE)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_disable() -> dict[str, Any]:
"""Disable HFP on the ESP32.
Disconnects any active HFP connection and deinitializes the HFP stack.
Returns:
Response confirming HFP is disabled.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_DISABLE)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_connect(address: str) -> dict[str, Any]:
"""Connect to an Audio Gateway (phone/computer).
Establishes an HFP Service Level Connection (SLC) to the specified
device. The device must support the Audio Gateway role (typically
phones and computers).
Args:
address: Bluetooth address of the AG (e.g., "AA:BB:CC:DD:EE:FF").
Returns:
Response with connection status.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_CONNECT, {"address": address})
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_disconnect() -> dict[str, Any]:
"""Disconnect from the current Audio Gateway.
Terminates the HFP connection including any active audio link.
Returns:
Response confirming disconnection.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_DISCONNECT)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_audio_connect() -> dict[str, Any]:
"""Connect SCO audio channel.
Opens the synchronous audio link for voice transmission. Requires an
active SLC connection (established via esp32_hfp_connect).
Audio routing:
- With CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI: Audio data flows through
the host controller interface (can be processed by ESP32).
- Without: Audio is routed directly through the Bluetooth chip.
Returns:
Response with audio connection status and codec info.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_AUDIO_CONNECT)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_audio_disconnect() -> dict[str, Any]:
"""Disconnect SCO audio channel.
Closes the audio link but keeps the control connection (SLC) open.
Useful for temporarily muting the audio path without fully disconnecting.
Returns:
Response confirming audio disconnection.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_AUDIO_DISCONNECT)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_answer() -> dict[str, Any]:
"""Answer an incoming call.
Sends the ATA command to the Audio Gateway to accept an incoming call.
The call must be in the ringing state.
Returns:
Response with call answer status.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_ANSWER)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_reject() -> dict[str, Any]:
"""Reject an incoming call or hang up an active call.
Sends the AT+CHUP command to the Audio Gateway. Works for:
- Rejecting an incoming/waiting call
- Ending an active call
- Terminating an outgoing call (before it's answered)
Returns:
Response with call rejection/hangup status.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_REJECT)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_dial(number: str) -> dict[str, Any]:
"""Dial a phone number.
Initiates an outgoing call through the connected Audio Gateway.
Args:
number: Phone number to dial.
Returns:
Response with dial status.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_DIAL, {"number": number})
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_send_dtmf(code: str) -> dict[str, Any]:
"""Send a DTMF tone during an active call.
Used for navigating phone menus, entering PIN codes, etc.
Args:
code: Single DTMF character. Valid values: 0-9, *, #, A-D.
Returns:
Response with DTMF send status.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_SEND_DTMF, {"code": code})
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_volume(
type: Literal["speaker", "microphone"],
volume: int,
) -> dict[str, Any]:
"""Adjust speaker or microphone volume.
Sets the volume level and notifies the Audio Gateway of the change.
Args:
type: Volume type - "speaker" for output, "microphone" for input.
volume: Volume level 0-15 (0 = muted, 15 = maximum).
Returns:
Response with applied volume settings.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_VOLUME, {"type": type, "volume": volume})
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_voice_recognition_start() -> dict[str, Any]:
"""Start voice recognition (Siri/Google Assistant).
Requests the Audio Gateway to activate its voice recognition feature.
The AG must support this feature (most modern phones do).
Returns:
Response with voice recognition activation status.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_VOICE_RECOGNITION_START)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_voice_recognition_stop() -> dict[str, Any]:
"""Stop voice recognition.
Requests the Audio Gateway to deactivate voice recognition.
Returns:
Response with voice recognition deactivation status.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_VOICE_RECOGNITION_STOP)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_query_calls() -> dict[str, Any]:
"""Query current call list from the Audio Gateway.
Retrieves information about all current calls including:
- Call index and direction (incoming/outgoing)
- Call status (active, held, dialing, alerting, incoming, waiting)
- Phone number and type
Returns:
Response with list of current calls.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_QUERY_CALLS)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}
@mcp.tool()
async def esp32_hfp_status() -> dict[str, Any]:
"""Get the current HFP status.
Returns comprehensive status including:
- enabled: Whether HFP stack is initialized
- connected: Whether SLC is established
- audio_connected: Whether SCO audio link is active
- remote_address: Connected AG address
- call_status: Current call state
- signal_strength: AG signal strength (0-5)
- battery_level: AG battery level (0-5)
- speaker_volume: Current speaker volume (0-15)
- microphone_volume: Current mic volume (0-15)
Returns:
Response with HFP status details.
"""
try:
client = get_client()
response = await client.send_command(CMD_HFP_STATUS)
if response.status == Status.OK:
return {"status": "ok", **response.data}
return {"status": "error", "error": response.data.get("error", "unknown error")}
except Exception as exc:
return {"error": str(exc)}