405 lines
12 KiB
C
405 lines
12 KiB
C
/**
|
|
* Bluetooth HFP - Hands-Free Profile Audio Gateway
|
|
*
|
|
* ESP32 agiert als Audio Gateway (AG) - die Rolle einer Telefonanlage
|
|
* Headset ist das HF (Hands-Free) Device
|
|
*
|
|
* Angepasst für ESP-IDF 5.x
|
|
* Nur auf ESP32 (nicht S3/C3) verfügbar
|
|
*/
|
|
|
|
#include <string.h>
|
|
#include "bt_hfp.h"
|
|
#include "bt_manager.h"
|
|
#include "esp_log.h"
|
|
#include "sdkconfig.h"
|
|
|
|
static const char* TAG = "BT_HFP";
|
|
|
|
#if CONFIG_BT_ENABLED
|
|
|
|
#include "esp_hf_ag_api.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "freertos/ringbuf.h"
|
|
|
|
// Audio Buffer
|
|
#define AUDIO_RINGBUF_SIZE (8 * 1024)
|
|
static RingbufHandle_t s_audio_out_ringbuf = NULL;
|
|
|
|
// State
|
|
static bool s_initialized = false;
|
|
static bool s_audio_connected = false;
|
|
static esp_bd_addr_t s_connected_peer;
|
|
static bool s_service_connected = false;
|
|
|
|
// External notifications (definiert in bt_manager.c)
|
|
extern void bt_manager_notify_connected(const esp_bd_addr_t address);
|
|
extern void bt_manager_notify_disconnected(const esp_bd_addr_t address);
|
|
extern void bt_manager_notify_button(bt_button_event_t event);
|
|
extern void bt_manager_notify_audio_data(const uint8_t* data, size_t len);
|
|
|
|
// HFP AG Callback
|
|
static void hf_ag_callback(esp_hf_cb_event_t event, esp_hf_cb_param_t *param)
|
|
{
|
|
switch (event) {
|
|
case ESP_HF_CONNECTION_STATE_EVT:
|
|
if (param->conn_stat.state == ESP_HF_CONNECTION_STATE_CONNECTED) {
|
|
ESP_LOGI(TAG, "HFP Service verbunden");
|
|
memcpy(s_connected_peer, param->conn_stat.remote_bda, ESP_BD_ADDR_LEN);
|
|
s_service_connected = true;
|
|
bt_manager_notify_connected(param->conn_stat.remote_bda);
|
|
} else if (param->conn_stat.state == ESP_HF_CONNECTION_STATE_DISCONNECTED) {
|
|
ESP_LOGI(TAG, "HFP Service getrennt");
|
|
s_service_connected = false;
|
|
s_audio_connected = false;
|
|
bt_manager_notify_disconnected(param->conn_stat.remote_bda);
|
|
} else if (param->conn_stat.state == ESP_HF_CONNECTION_STATE_SLC_CONNECTED) {
|
|
ESP_LOGI(TAG, "HFP SLC verbunden (Service Level Connection)");
|
|
}
|
|
break;
|
|
|
|
case ESP_HF_AUDIO_STATE_EVT:
|
|
if (param->audio_stat.state == ESP_HF_AUDIO_STATE_CONNECTED) {
|
|
ESP_LOGI(TAG, "HFP Audio verbunden (SCO)");
|
|
s_audio_connected = true;
|
|
} else if (param->audio_stat.state == ESP_HF_AUDIO_STATE_DISCONNECTED) {
|
|
ESP_LOGI(TAG, "HFP Audio getrennt");
|
|
s_audio_connected = false;
|
|
}
|
|
break;
|
|
|
|
case ESP_HF_VOLUME_CONTROL_EVT:
|
|
ESP_LOGI(TAG, "Volume %s: %d",
|
|
param->volume_control.type == ESP_HF_VOLUME_TYPE_SPK ?
|
|
"Speaker" : "Mic",
|
|
param->volume_control.volume);
|
|
break;
|
|
|
|
case ESP_HF_UNAT_RESPONSE_EVT:
|
|
// Unknown AT Command
|
|
ESP_LOGD(TAG, "Unbekannter AT Command: %s", param->unat_rep.unat);
|
|
break;
|
|
|
|
case ESP_HF_CIND_RESPONSE_EVT:
|
|
// Indicator Status Request
|
|
ESP_LOGD(TAG, "CIND Request");
|
|
// Antworten mit Standard-Werten
|
|
esp_hf_ag_cind_response(s_connected_peer,
|
|
1, // call (0=no call, 1=call)
|
|
0, // call_setup (0=none, 1=incoming, 2=outgoing)
|
|
1, // service (0=no service, 1=service)
|
|
5, // signal (0-5)
|
|
0, // roam (0=not roaming, 1=roaming)
|
|
5, // batt (0-5)
|
|
0); // call_held (0=none, 1=held, 2=hold+active)
|
|
break;
|
|
|
|
case ESP_HF_CLCC_RESPONSE_EVT:
|
|
// Call List Request - keine aktiven Anrufe melden
|
|
ESP_LOGD(TAG, "CLCC Request");
|
|
// ESP-IDF 5: esp_hf_ag_clcc_response hat mehr Parameter
|
|
esp_hf_ag_clcc_response(s_connected_peer, 0,
|
|
ESP_HF_CURRENT_CALL_DIRECTION_INCOMING,
|
|
ESP_HF_CURRENT_CALL_STATUS_ACTIVE,
|
|
ESP_HF_CURRENT_CALL_MODE_VOICE,
|
|
ESP_HF_CURRENT_CALL_MPTY_TYPE_SINGLE,
|
|
NULL, ESP_HF_CALL_ADDR_TYPE_UNKNOWN);
|
|
break;
|
|
|
|
case ESP_HF_COPS_RESPONSE_EVT:
|
|
// Network Operator Request
|
|
ESP_LOGD(TAG, "COPS Request");
|
|
esp_hf_ag_cops_response(s_connected_peer, "SIP Phone");
|
|
break;
|
|
|
|
case ESP_HF_CNUM_RESPONSE_EVT:
|
|
// Subscriber Number Request
|
|
ESP_LOGD(TAG, "CNUM Request");
|
|
// ESP-IDF 5: esp_hf_ag_cnum_response hat mehr Parameter
|
|
esp_hf_ag_cnum_response(s_connected_peer, NULL,
|
|
ESP_HF_CALL_ADDR_TYPE_UNKNOWN,
|
|
ESP_HF_SUBSCRIBER_SERVICE_TYPE_VOICE);
|
|
break;
|
|
|
|
case ESP_HF_VTS_RESPONSE_EVT:
|
|
// DTMF Tone
|
|
ESP_LOGI(TAG, "DTMF: %s", param->vts_rep.code);
|
|
break;
|
|
|
|
case ESP_HF_NREC_RESPONSE_EVT:
|
|
// Noise Reduction / Echo Cancellation
|
|
ESP_LOGI(TAG, "NREC: %s", param->nrec.state ? "an" : "aus");
|
|
break;
|
|
|
|
case ESP_HF_ATA_RESPONSE_EVT:
|
|
// Answer Call (ATA)
|
|
ESP_LOGI(TAG, "Headset: Anruf annehmen");
|
|
bt_manager_notify_button(BT_BUTTON_ANSWER);
|
|
break;
|
|
|
|
case ESP_HF_CHUP_RESPONSE_EVT:
|
|
// Hangup Call (AT+CHUP)
|
|
ESP_LOGI(TAG, "Headset: Auflegen");
|
|
bt_manager_notify_button(BT_BUTTON_HANGUP);
|
|
break;
|
|
|
|
case ESP_HF_DIAL_EVT:
|
|
// Dial (ATD, ATD>, ATD>mem)
|
|
if (param->out_call.type == ESP_HF_DIAL_MEM) {
|
|
ESP_LOGI(TAG, "Headset: Wähle Speicher %s", param->out_call.num_or_loc);
|
|
} else if (param->out_call.type == ESP_HF_DIAL_NUM) {
|
|
ESP_LOGI(TAG, "Headset: Wähle %s", param->out_call.num_or_loc);
|
|
} else {
|
|
ESP_LOGI(TAG, "Headset: Wahlwiederholung");
|
|
bt_manager_notify_button(BT_BUTTON_REDIAL);
|
|
}
|
|
break;
|
|
|
|
case ESP_HF_WBS_RESPONSE_EVT:
|
|
// Wide Band Speech (mSBC Codec)
|
|
ESP_LOGI(TAG, "WBS Codec: %s",
|
|
param->wbs_rep.codec == ESP_HF_WBS_YES ? "mSBC" : "CVSD");
|
|
break;
|
|
|
|
case ESP_HF_BCS_RESPONSE_EVT:
|
|
// Codec Selection
|
|
ESP_LOGI(TAG, "Codec Selected: %d", param->bcs_rep.mode);
|
|
break;
|
|
|
|
default:
|
|
ESP_LOGD(TAG, "HFP Event: %d", event);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Audio Data Callback (eingehend vom Headset - Mikrofon)
|
|
// ESP-IDF 5: Signatur geändert zu void
|
|
static void hf_ag_incoming_data_callback(const uint8_t *buf, uint32_t len)
|
|
{
|
|
// Audio-Daten vom Headset-Mikrofon weiterleiten
|
|
bt_manager_notify_audio_data(buf, len);
|
|
}
|
|
|
|
// Audio Data Request (ausgehend zum Headset - Speaker)
|
|
// ESP-IDF 5: Signatur geändert zu uint32_t return
|
|
static uint32_t hf_ag_outgoing_data_callback(uint8_t *buf, uint32_t len)
|
|
{
|
|
if (s_audio_out_ringbuf) {
|
|
size_t item_size;
|
|
uint8_t* data = xRingbufferReceiveUpTo(s_audio_out_ringbuf, &item_size, 0, len);
|
|
if (data && item_size > 0) {
|
|
memcpy(buf, data, item_size);
|
|
vRingbufferReturnItem(s_audio_out_ringbuf, data);
|
|
|
|
// Rest mit Stille füllen
|
|
if (item_size < len) {
|
|
memset(buf + item_size, 0, len - item_size);
|
|
}
|
|
return len;
|
|
}
|
|
}
|
|
// Keine Daten - Stille senden
|
|
memset(buf, 0, len);
|
|
return len;
|
|
}
|
|
|
|
esp_err_t bt_hfp_init(void)
|
|
{
|
|
if (s_initialized) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Initialisiere HFP Audio Gateway");
|
|
|
|
// Audio Output Buffer erstellen
|
|
s_audio_out_ringbuf = xRingbufferCreate(AUDIO_RINGBUF_SIZE, RINGBUF_TYPE_BYTEBUF);
|
|
if (!s_audio_out_ringbuf) {
|
|
ESP_LOGE(TAG, "Ringbuffer erstellen fehlgeschlagen");
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
// HFP AG initialisieren
|
|
esp_err_t ret = esp_hf_ag_init();
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "HFP AG init failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
// Callback registrieren
|
|
ret = esp_hf_ag_register_callback(hf_ag_callback);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "HFP AG callback register failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
// Audio Data Callbacks registrieren
|
|
esp_hf_ag_register_data_callback(hf_ag_incoming_data_callback,
|
|
hf_ag_outgoing_data_callback);
|
|
|
|
s_initialized = true;
|
|
ESP_LOGI(TAG, "HFP Audio Gateway initialisiert");
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t bt_hfp_deinit(void)
|
|
{
|
|
if (!s_initialized) return ESP_OK;
|
|
|
|
ESP_LOGI(TAG, "Deinitalisiere HFP");
|
|
|
|
if (s_audio_connected) {
|
|
bt_hfp_audio_disconnect();
|
|
}
|
|
|
|
if (s_service_connected) {
|
|
esp_hf_ag_slc_disconnect(s_connected_peer);
|
|
}
|
|
|
|
esp_hf_ag_deinit();
|
|
|
|
if (s_audio_out_ringbuf) {
|
|
vRingbufferDelete(s_audio_out_ringbuf);
|
|
s_audio_out_ringbuf = NULL;
|
|
}
|
|
|
|
s_initialized = false;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t bt_hfp_connect(const esp_bd_addr_t address)
|
|
{
|
|
if (!s_initialized) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
char addr_str[18];
|
|
bt_addr_to_str(address, addr_str, sizeof(addr_str));
|
|
ESP_LOGI(TAG, "HFP Connect: %s", addr_str);
|
|
|
|
// Cast um const zu entfernen (ESP-IDF API erwartet non-const)
|
|
esp_bd_addr_t addr_copy;
|
|
memcpy(addr_copy, address, ESP_BD_ADDR_LEN);
|
|
return esp_hf_ag_slc_connect(addr_copy);
|
|
}
|
|
|
|
esp_err_t bt_hfp_disconnect(const esp_bd_addr_t address)
|
|
{
|
|
if (!s_initialized || !s_service_connected) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
if (s_audio_connected) {
|
|
bt_hfp_audio_disconnect();
|
|
}
|
|
|
|
// Cast um const zu entfernen
|
|
esp_bd_addr_t addr_copy;
|
|
memcpy(addr_copy, address, ESP_BD_ADDR_LEN);
|
|
return esp_hf_ag_slc_disconnect(addr_copy);
|
|
}
|
|
|
|
esp_err_t bt_hfp_audio_connect(void)
|
|
{
|
|
if (!s_initialized || !s_service_connected) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
if (s_audio_connected) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Starte SCO Audio...");
|
|
return esp_hf_ag_audio_connect(s_connected_peer);
|
|
}
|
|
|
|
esp_err_t bt_hfp_audio_disconnect(void)
|
|
{
|
|
if (!s_initialized || !s_audio_connected) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Stoppe SCO Audio...");
|
|
return esp_hf_ag_audio_disconnect(s_connected_peer);
|
|
}
|
|
|
|
esp_err_t bt_hfp_send_audio(const uint8_t* data, size_t len)
|
|
{
|
|
if (!s_initialized || !s_audio_connected || !s_audio_out_ringbuf) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
// In Ringbuffer schreiben
|
|
if (xRingbufferSend(s_audio_out_ringbuf, data, len, 0) != pdTRUE) {
|
|
// Buffer voll - alte Daten verwerfen
|
|
ESP_LOGW(TAG, "Audio buffer overflow");
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
bool bt_hfp_is_connected(void)
|
|
{
|
|
return s_service_connected;
|
|
}
|
|
|
|
bool bt_hfp_is_audio_connected(void)
|
|
{
|
|
return s_audio_connected;
|
|
}
|
|
|
|
#else // CONFIG_BT_ENABLED not set - Stub-Implementierungen
|
|
|
|
esp_err_t bt_hfp_init(void)
|
|
{
|
|
ESP_LOGW(TAG, "HFP nicht verfügbar (Bluetooth Classic deaktiviert)");
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
esp_err_t bt_hfp_deinit(void)
|
|
{
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t bt_hfp_connect(const uint8_t* address)
|
|
{
|
|
(void)address;
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
esp_err_t bt_hfp_disconnect(const uint8_t* address)
|
|
{
|
|
(void)address;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t bt_hfp_audio_connect(void)
|
|
{
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
esp_err_t bt_hfp_audio_disconnect(void)
|
|
{
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t bt_hfp_send_audio(const uint8_t* data, size_t len)
|
|
{
|
|
(void)data;
|
|
(void)len;
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
bool bt_hfp_is_connected(void)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool bt_hfp_is_audio_connected(void)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#endif // CONFIG_BT_ENABLED
|