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:
parent
9a8eae1d2f
commit
ab699bbca3
@ -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
739
firmware/main/bt_hfp.c
Normal 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
122
firmware/main/bt_hfp.h
Normal 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);
|
||||||
@ -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 },
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
356
src/mcbluetooth_esp32/tools/hfp.py
Normal file
356
src/mcbluetooth_esp32/tools/hfp.py
Normal 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)}
|
||||||
Loading…
x
Reference in New Issue
Block a user