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
740 lines
24 KiB
C
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);
|
|
}
|