Ryan Malloy ab699bbca3 Add HFP (Hands-Free Profile) support
Implement HFP client (Hands-Free Unit role) for the ESP32 test harness:

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

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

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

740 lines
24 KiB
C

/**
* @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);
}