diff --git a/components/bt_hfp/CMakeLists.txt b/components/bt_hfp/CMakeLists.txt new file mode 100644 index 0000000..6471ac8 --- /dev/null +++ b/components/bt_hfp/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "bt_hfp.c" + INCLUDE_DIRS "include" + REQUIRES bt +) diff --git a/components/bt_hfp/bt_hfp.c b/components/bt_hfp/bt_hfp.c new file mode 100644 index 0000000..60ad159 --- /dev/null +++ b/components/bt_hfp/bt_hfp.c @@ -0,0 +1,213 @@ +/* + * bt_hfp.c — Bluetooth Classic HFP-HF (Bluedroid esp_hf_client) + * + * Corrections d'API par rapport au brief original : + * 1. Gardes #if CONFIG_EXAMPLE_SSP_ENABLED RETIRÉES — bloc SSP inconditionnel + * (CONFIG_EXAMPLE_SSP_ENABLED n'existe pas dans ce projet; SSP activé par défaut IDF). + * 2. ESP_BT_IO_CAP_IO est un uint8_t (#define = 1), pas une enum — cast explicite conservé. + * 3. Toutes les signatures vérifiées contre les headers réels IDF v5.4 : + * esp_bt_gap_set_scan_mode(c_mode, d_mode) — 2 args : OK + * esp_hf_client_reject_call(void) — 0 args : OK + * esp_bt_gap_ssp_confirm_reply(bda, bool) : OK + * esp_bt_gap_set_security_param(param_type, &val, len) : OK + */ + +#include "bt_hfp.h" +#include +#include "esp_log.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" + +#define TAG "bt_hfp" + +static bt_hfp_event_cb_t s_cb = NULL; +static esp_bd_addr_t s_peer = {0}; +static volatile bool s_slc = false; + +static void emit(bt_hfp_event_t ev, const char *number, bool msbc) +{ + if (s_cb) { + bt_hfp_event_data_t d = { .number = number, .msbc = msbc }; + s_cb(ev, &d); + } +} + +/* ---- GAP callback : appairage / authentification / SSP ---- */ +static void gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) +{ + switch (event) { + case ESP_BT_GAP_AUTH_CMPL_EVT: + if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) { + ESP_LOGI(TAG, "appairage OK: %s", param->auth_cmpl.device_name); + } else { + ESP_LOGE(TAG, "appairage echoue: %d", param->auth_cmpl.stat); + } + break; + + /* + * SSP (Simple Secure Pairing) — garde CONFIG_EXAMPLE_SSP_ENABLED RETIREE : + * ce projet n'a pas de Kconfig.projbuild d'exemple. SSP est actif par defaut + * en IDF; on accepte automatiquement la confirmation numerique. + */ + case ESP_BT_GAP_CFM_REQ_EVT: + ESP_LOGI(TAG, "SSP CFM_REQ — acceptation automatique du code %"PRIu32, + param->cfm_req.num_val); + esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true); + break; + + case ESP_BT_GAP_KEY_NOTIF_EVT: + ESP_LOGI(TAG, "SSP passkey: %"PRIu32, param->key_notif.passkey); + break; + + case ESP_BT_GAP_KEY_REQ_EVT: + ESP_LOGI(TAG, "SSP passkey requis"); + break; + + case ESP_BT_GAP_PIN_REQ_EVT: + if (param->pin_req.min_16_digit) { + ESP_LOGI(TAG, "PIN_REQ 16 chiffres"); + esp_bt_pin_code_t pin16 = { + '0','0','0','0','0','0','0','0', + '0','0','0','0','0','0','0','0' + }; + esp_bt_gap_pin_reply(param->pin_req.bda, true, 16, pin16); + } else { + ESP_LOGI(TAG, "PIN_REQ 4 chiffres"); + esp_bt_pin_code_t pin4 = { '0','0','0','0' }; + esp_bt_gap_pin_reply(param->pin_req.bda, true, 4, pin4); + } + break; + + default: + break; + } +} + +/* ---- HFP-HF callback : traduit les evenements bas-niveau ---- */ +static void hf_cb(esp_hf_client_cb_event_t event, esp_hf_client_cb_param_t *param) +{ + switch (event) { + case ESP_HF_CLIENT_CONNECTION_STATE_EVT: + if (param->conn_stat.state == ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED) { + memcpy(s_peer, param->conn_stat.remote_bda, ESP_BD_ADDR_LEN); + s_slc = true; + ESP_LOGI(TAG, "SLC connecte"); + emit(BT_HFP_EV_SLC_CONNECTED, NULL, false); + } else if (param->conn_stat.state == ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTED) { + s_slc = false; + ESP_LOGI(TAG, "SLC deconnecte"); + emit(BT_HFP_EV_SLC_DISCONNECTED, NULL, false); + } + break; + + case ESP_HF_CLIENT_CIND_CALL_SETUP_EVT: + if (param->call_setup.status == ESP_HF_CALL_SETUP_STATUS_INCOMING) { + ESP_LOGI(TAG, "appel entrant (CIND)"); + emit(BT_HFP_EV_INCOMING, NULL, false); + } + break; + + case ESP_HF_CLIENT_CLIP_EVT: + ESP_LOGI(TAG, "numero appelant: %s", + param->clip.number ? param->clip.number : "?"); + emit(BT_HFP_EV_INCOMING, param->clip.number, false); + break; + + case ESP_HF_CLIENT_CIND_CALL_EVT: + if (param->call.status == ESP_HF_CALL_STATUS_CALL_IN_PROGRESS) { + ESP_LOGI(TAG, "appel actif"); + emit(BT_HFP_EV_CALL_ACTIVE, NULL, false); + } else { + ESP_LOGI(TAG, "fin d'appel"); + emit(BT_HFP_EV_CALL_ENDED, NULL, false); + } + break; + + case ESP_HF_CLIENT_AUDIO_STATE_EVT: + if (param->audio_stat.state == ESP_HF_CLIENT_AUDIO_STATE_CONNECTED || + param->audio_stat.state == ESP_HF_CLIENT_AUDIO_STATE_CONNECTED_MSBC) { + bool msbc = (param->audio_stat.state == + ESP_HF_CLIENT_AUDIO_STATE_CONNECTED_MSBC); + ESP_LOGI(TAG, "audio SCO connecte (%s)", msbc ? "mSBC 16k" : "CVSD 8k"); + emit(BT_HFP_EV_AUDIO_CONNECTED, NULL, msbc); + } else if (param->audio_stat.state == ESP_HF_CLIENT_AUDIO_STATE_DISCONNECTED) { + ESP_LOGI(TAG, "audio SCO deconnecte"); + emit(BT_HFP_EV_AUDIO_DISCONNECTED, NULL, false); + } + break; + + default: + break; + } +} + +/* ---- API publique ---- */ + +esp_err_t bt_hfp_init(bt_hfp_event_cb_t cb) +{ + s_cb = cb; + + /* Liberer la RAM reservee pour le BLE : on n'utilise que Classic BT. */ + ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE)); + + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + esp_err_t ret = esp_bt_controller_init(&bt_cfg); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "controller_init: %s", esp_err_to_name(ret)); + return ret; + } + + ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "controller_enable: %s", esp_err_to_name(ret)); + return ret; + } + + ret = esp_bluedroid_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "bluedroid_init: %s", esp_err_to_name(ret)); + return ret; + } + + ret = esp_bluedroid_enable(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "bluedroid_enable: %s", esp_err_to_name(ret)); + return ret; + } + + ESP_ERROR_CHECK(esp_bt_gap_set_device_name("RTC_BL_PHONE")); + ESP_ERROR_CHECK(esp_bt_gap_register_callback(gap_cb)); + ESP_ERROR_CHECK(esp_hf_client_register_callback(hf_cb)); + ESP_ERROR_CHECK(esp_hf_client_init()); + + /* + * SSP : configuration du mode IO. Garde CONFIG_EXAMPLE_SSP_ENABLED RETIREE. + * ESP_BT_IO_CAP_IO (= 1, uint8_t) = DisplayYesNo — l'ESP affiche un code, + * l'utilisateur confirme sur le mobile. Acceptation automatique dans gap_cb. + */ + esp_bt_sp_param_t pt = ESP_BT_SP_IOCAP_MODE; + esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_IO; + ESP_ERROR_CHECK(esp_bt_gap_set_security_param(pt, &iocap, sizeof(uint8_t))); + + /* PIN legacy (fallback pour appareils anciens). */ + esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_FIXED; + esp_bt_pin_code_t pin_code = { '0', '0', '0', '0' }; + esp_bt_gap_set_pin(pin_type, 4, pin_code); + + /* Rendre le peripherique connectable ET decouvrable. */ + ESP_ERROR_CHECK( + esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE) + ); + + ESP_LOGI(TAG, "bt_hfp pret — appairer le mobile sur 'RTC_BL_PHONE'"); + return ESP_OK; +} + +esp_err_t bt_hfp_dial(const char *number) { return esp_hf_client_dial(number); } +esp_err_t bt_hfp_answer(void) { return esp_hf_client_answer_call(); } +esp_err_t bt_hfp_hangup(void) { return esp_hf_client_reject_call(); } +esp_err_t bt_hfp_send_dtmf(char code) { return esp_hf_client_send_dtmf(code); } +bool bt_hfp_slc_connected(void) { return s_slc; } diff --git a/components/bt_hfp/include/bt_hfp.h b/components/bt_hfp/include/bt_hfp.h new file mode 100644 index 0000000..fd3c4cf --- /dev/null +++ b/components/bt_hfp/include/bt_hfp.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + BT_HFP_EV_SLC_CONNECTED, + BT_HFP_EV_SLC_DISCONNECTED, + BT_HFP_EV_INCOMING, + BT_HFP_EV_CALL_ACTIVE, + BT_HFP_EV_CALL_ENDED, + BT_HFP_EV_AUDIO_CONNECTED, + BT_HFP_EV_AUDIO_DISCONNECTED, +} bt_hfp_event_t; + +typedef struct { const char *number; bool msbc; } bt_hfp_event_data_t; +typedef void (*bt_hfp_event_cb_t)(bt_hfp_event_t ev, const bt_hfp_event_data_t *data); + +/* Init Bluetooth Classic + Bluedroid + HFP-HF. cb receives high-level events. */ +esp_err_t bt_hfp_init(bt_hfp_event_cb_t cb); + +esp_err_t bt_hfp_dial(const char *number); /* ATD */ +esp_err_t bt_hfp_answer(void); /* ATA */ +esp_err_t bt_hfp_hangup(void); /* AT+CHUP */ +esp_err_t bt_hfp_send_dtmf(char code); +bool bt_hfp_slc_connected(void); + +#ifdef __cplusplus +} +#endif diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b8e76b5..e8a0fa4 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRCS "app_main.c" INCLUDE_DIRS "." - REQUIRES nvs_flash config_store hal_es8388 hal_i2s audio_router slic_ks0835 dtmf dialer hook_monitor call_manager + REQUIRES nvs_flash config_store hal_es8388 hal_i2s audio_router slic_ks0835 dtmf dialer hook_monitor call_manager bt_hfp ) diff --git a/main/app_main.c b/main/app_main.c index 99d44dd..77261cc 100644 --- a/main/app_main.c +++ b/main/app_main.c @@ -7,9 +7,18 @@ #include "slic.h" #include "dialer.h" #include "call_manager.h" +#include "bt_hfp.h" static const char *TAG = "rtc_phone"; +/* Handler de log temporaire pour les evenements BT HFP (Task 2). + * Remplace par call_manager en Task 5. */ +static void bt_log_cb(bt_hfp_event_t ev, const bt_hfp_event_data_t *d) +{ + ESP_LOGI(TAG, "BT event %d (number=%s msbc=%d)", (int)ev, + d->number ? d->number : "-", (int)d->msbc); +} + void app_main(void) { ESP_LOGI(TAG, "RTC BL PHONE — socle ESP-IDF v5.4"); @@ -22,5 +31,7 @@ void app_main(void) audio_router_init(); dialer_init(); call_manager_start(); - ESP_LOGI(TAG, "téléphonie locale prête — décroche pour composer"); + /* Pile BT HFP-HF — NVS déjà initialisé par config_store_init(). */ + ESP_ERROR_CHECK(bt_hfp_init(bt_log_cb)); + ESP_LOGI(TAG, "telephonie locale + BT HFP prets"); }