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