feat(bt): add HFP call controls and PBAP capability status
This commit is contained in:
@@ -37,6 +37,7 @@ Chaque fiche détaille : rôle, API, notifications, points de validation, synerg
|
|||||||
| Tests livraison | plan_tests_livraison.md |
|
| Tests livraison | plan_tests_livraison.md |
|
||||||
| WebUI | plan_webui.md |
|
| WebUI | plan_webui.md |
|
||||||
| Spec WebUI route parity | spec_webui_route_parity_and_coverage_v1.md |
|
| Spec WebUI route parity | spec_webui_route_parity_and_coverage_v1.md |
|
||||||
|
| Spec BT HFP/PBAP dial | spec_bt_hfp_pbap_dialing_v1.md |
|
||||||
| État des specs | SPECS_STATE.md |
|
| État des specs | SPECS_STATE.md |
|
||||||
|
|
||||||
## Procédures QA & hardware
|
## Procédures QA & hardware
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
## 1) Index des specs
|
## 1) Index des specs
|
||||||
|
|
||||||
- `docs/spec_webui_route_parity_and_coverage_v1.md` (spécification principale active)
|
- `docs/spec_webui_route_parity_and_coverage_v1.md` (spécification principale active)
|
||||||
|
- `docs/spec_bt_hfp_pbap_dialing_v1.md` (nouvelle spec téléphonie Bluetooth, statut `Draft`)
|
||||||
- Notion hub: `RTC_BL_PHONE Delivery Hub`
|
- Notion hub: `RTC_BL_PHONE Delivery Hub`
|
||||||
- Notion spec mirror: `RTC_BL_PHONE - Spec - WebUI Route Parity v1`
|
- Notion spec mirror: `RTC_BL_PHONE - Spec - WebUI Route Parity v1`
|
||||||
- Notion plan: `RTC_BL_PHONE - Plan d'implementation - Route Parity Closure`
|
- Notion plan: `RTC_BL_PHONE - Plan d'implementation - Route Parity Closure`
|
||||||
@@ -25,6 +26,14 @@
|
|||||||
- `scripts/check_web_route_parity.py --report-json artifacts/route_parity_report.json` : OK (report généré)
|
- `scripts/check_web_route_parity.py --report-json artifacts/route_parity_report.json` : OK (report généré)
|
||||||
- `platformio run -e esp32-s3-devkitc-1` : échec connu sur liens Bluetooth/HFP externes (suivi séparé)
|
- `platformio run -e esp32-s3-devkitc-1` : échec connu sur liens Bluetooth/HFP externes (suivi séparé)
|
||||||
|
|
||||||
|
## 3-bis) État spec BT HFP/PBAP/Numérotation
|
||||||
|
|
||||||
|
- HFP commande-level: OK (connect/disconnect commandes acceptées).
|
||||||
|
- HFP call-control AT: implémenté (`BT_DIAL`/`DIAL`, `BT_REDIAL`, `BT_ANSWER`, `BT_HANGUP`, `BT_CALLS`).
|
||||||
|
- HFP liaison réelle: KO sur tests assistés (pas de montée `connected=true`/`hfp_active=true`).
|
||||||
|
- PBAP: bloqué sur stack actuelle (`arduino-esp32` bluedroid sans API PBAP exposée côté firmware), `BT_PBAP_SYNC` retourne `unsupported`.
|
||||||
|
- Numérotation HFP/AT: implémentée côté firmware, validation E2E téléphone encore à obtenir (dépend de la liaison HFP réelle).
|
||||||
|
|
||||||
## 4) Lacunes à combler pour passer la spec en `DONE`
|
## 4) Lacunes à combler pour passer la spec en `DONE`
|
||||||
|
|
||||||
1. Mettre à jour les issues de pilotage (`#10`, `#11`) avec les liens vers les runs CI et l’artefact.
|
1. Mettre à jour les issues de pilotage (`#10`, `#11`) avec les liens vers les runs CI et l’artefact.
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Spécification Bluetooth Téléphonie — HFP + PBAP + Numérotation (v1)
|
||||||
|
|
||||||
|
Date: 2026-02-21
|
||||||
|
Repo: `electron-rare/RTC_BL_PHONE`
|
||||||
|
|
||||||
|
## 1. Contexte
|
||||||
|
|
||||||
|
Le firmware expose des commandes Bluetooth (`BT_HFP_CONNECT`, `BT_STATUS`, etc.) mais la liaison HFP réelle n'est pas encore validée de bout en bout avec un téléphone.
|
||||||
|
Le besoin produit inclut explicitement:
|
||||||
|
- audio d'appel + états d'appel (HFP),
|
||||||
|
- synchronisation des contacts (PBAP),
|
||||||
|
- numérotation depuis le firmware (AT/HFP) avec session HFP active.
|
||||||
|
|
||||||
|
## 2. Objectif
|
||||||
|
|
||||||
|
Livrer une stack Bluetooth téléphonie réellement opérationnelle sur ESP32 Audio Kit:
|
||||||
|
1. HFP réel fonctionnel (pas seulement commandes firmware),
|
||||||
|
2. synchronisation contacts via PBAP,
|
||||||
|
3. numérotation sortante via commandes HFP/AT.
|
||||||
|
|
||||||
|
## 2-bis. État d’implémentation (snapshot 2026-02-21)
|
||||||
|
|
||||||
|
- HFP commandes firmware: implémenté (`BT_HFP_CONNECT`, `BT_HFP_DISCONNECT`, `BT_DISCOVERABLE_ON/OFF`, `BT_STATUS`).
|
||||||
|
- Numérotation/contrôle appel via HFP AT: implémenté côté firmware:
|
||||||
|
- `BT_DIAL <number>` / alias `DIAL <number>`
|
||||||
|
- `BT_REDIAL`
|
||||||
|
- `BT_ANSWER`
|
||||||
|
- `BT_HANGUP`
|
||||||
|
- `BT_CALLS` (query `AT+CLCC`)
|
||||||
|
- `BT_STATUS` expose maintenant `slc_connected`, `call_state`, `last_dialed_number`.
|
||||||
|
- PBAP (contacts): non disponible sur la stack actuelle `arduino-esp32` (Bluedroid exposé sans API PBAP côté firmware). La commande `BT_PBAP_SYNC` retourne explicitement `unsupported`.
|
||||||
|
|
||||||
|
## 3. Périmètre
|
||||||
|
|
||||||
|
In-scope:
|
||||||
|
- Profil HFP (connexion RFCOMM/SLC + audio link),
|
||||||
|
- Profil PBAP (récupération carnet/adresses utiles),
|
||||||
|
- Commandes de numérotation et contrôle d'appel via HFP/AT,
|
||||||
|
- Validation réelle sur téléphone (iOS/macOS/Android selon disponibilité banc).
|
||||||
|
|
||||||
|
Out-of-scope v1:
|
||||||
|
- streaming musique A2DP,
|
||||||
|
- UI avancée carnet (recherche/favoris), hors MVP téléphonie.
|
||||||
|
|
||||||
|
## 4. Exigences fonctionnelles
|
||||||
|
|
||||||
|
### EF-01 — HFP réel
|
||||||
|
- Le firmware DOIT établir une session HFP réelle avec un téléphone compatible.
|
||||||
|
- `BT_STATUS` DOIT refléter:
|
||||||
|
- `connected=true` après connexion RFCOMM/SLC,
|
||||||
|
- `hfp_active=true` lorsque l'audio HFP est actif.
|
||||||
|
- Les commandes `BT_HFP_CONNECT` et `BT_HFP_DISCONNECT` DOIVENT piloter la session réelle.
|
||||||
|
|
||||||
|
### EF-02 — PBAP contacts
|
||||||
|
- Le firmware DOIT récupérer un sous-ensemble de contacts via PBAP.
|
||||||
|
- Les métadonnées minimales DOIVENT inclure: `display_name`, `phone_number`.
|
||||||
|
- La synchro DOIT gérer:
|
||||||
|
- premier chargement,
|
||||||
|
- rafraîchissement,
|
||||||
|
- erreur d'accès/pairing (message explicite).
|
||||||
|
|
||||||
|
### EF-03 — Numérotation
|
||||||
|
- Le firmware DOIT exposer une commande de numérotation sortante (ex: `DIAL <number>`).
|
||||||
|
- La numérotation DOIT échouer proprement si HFP non connecté.
|
||||||
|
- Les transitions d'appel DOIVENT être visibles dans `STATUS`/`BT_STATUS` (idle, dialing, ringing, active, ended).
|
||||||
|
|
||||||
|
### EF-04 — Compatibilité pairing
|
||||||
|
- L'appareil DOIT être détectable quand requis pour jumelage.
|
||||||
|
- Le flux de jumelage DOIT être documenté et testable depuis iPhone/Mac.
|
||||||
|
- Les erreurs de profil non supporté DOIVENT être tracées avec cause exploitable.
|
||||||
|
|
||||||
|
## 5. Exigences non fonctionnelles
|
||||||
|
|
||||||
|
- Temps de connexion HFP cible: < 15s dans des conditions normales.
|
||||||
|
- Aucun crash du firmware en cas d'échec pairing/profil.
|
||||||
|
- Logs série actionnables pour chaque étape: connect, slc, audio, pbap sync, dial.
|
||||||
|
|
||||||
|
## 6. Critères d'acceptation (DoD)
|
||||||
|
|
||||||
|
- [ ] HFP réel validé sur hardware (preuve `connected=true` et `hfp_active=true`).
|
||||||
|
- [ ] PBAP contacts synchronisés avec au moins un contact lisible (bloqué stack actuelle, nécessite changement de stack BT).
|
||||||
|
- [ ] Numérotation sortante validée sur téléphone réel depuis commande firmware (`BT_DIAL`).
|
||||||
|
- [ ] Rapport de test hardware mis à jour avec preuves (logs + JSON).
|
||||||
|
- [ ] Documentation opératoire de pairing/mise en service publiée.
|
||||||
|
|
||||||
|
## 7. Preuves attendues
|
||||||
|
|
||||||
|
- Rapport JSON/Markdown de tests hardware Bluetooth téléphonie.
|
||||||
|
- Captures ou logs montrant la session HFP active.
|
||||||
|
- Extrait de contacts PBAP récupérés.
|
||||||
|
- Trace de numérotation et état d'appel.
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <esp_gap_bt_api.h>
|
#include <esp_gap_bt_api.h>
|
||||||
#include <esp_hf_client_api.h>
|
#include <esp_hf_client_api.h>
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -43,6 +44,94 @@ String errToString(esp_err_t err) {
|
|||||||
return String(buf);
|
return String(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isDialableChar(char c) {
|
||||||
|
return (c >= '0' && c <= '9') || c == '+' || c == '*' || c == '#' || c == ',' || c == 'p' || c == 'P' ||
|
||||||
|
c == 'w' || c == 'W';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValidDialNumber(const String& number) {
|
||||||
|
if (number.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < static_cast<size_t>(number.length()); ++i) {
|
||||||
|
if (!isDialableChar(number[static_cast<unsigned int>(i)])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* callSetupStateToString(esp_hf_call_setup_status_t status) {
|
||||||
|
switch (status) {
|
||||||
|
case ESP_HF_CALL_SETUP_STATUS_IDLE:
|
||||||
|
return "idle";
|
||||||
|
case ESP_HF_CALL_SETUP_STATUS_INCOMING:
|
||||||
|
return "ringing";
|
||||||
|
case ESP_HF_CALL_SETUP_STATUS_OUTGOING_DIALING:
|
||||||
|
return "dialing";
|
||||||
|
case ESP_HF_CALL_SETUP_STATUS_OUTGOING_ALERTING:
|
||||||
|
return "alerting";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* callHeldStateToString(esp_hf_call_held_status_t status) {
|
||||||
|
switch (status) {
|
||||||
|
case ESP_HF_CALL_HELD_STATUS_NONE:
|
||||||
|
return "active";
|
||||||
|
case ESP_HF_CALL_HELD_STATUS_HELD_AND_ACTIVE:
|
||||||
|
return "held_active";
|
||||||
|
case ESP_HF_CALL_HELD_STATUS_HELD:
|
||||||
|
return "held";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* clccStateToString(esp_hf_current_call_status_t status) {
|
||||||
|
switch (status) {
|
||||||
|
case ESP_HF_CURRENT_CALL_STATUS_ACTIVE:
|
||||||
|
return "active";
|
||||||
|
case ESP_HF_CURRENT_CALL_STATUS_HELD:
|
||||||
|
return "held";
|
||||||
|
case ESP_HF_CURRENT_CALL_STATUS_DIALING:
|
||||||
|
return "dialing";
|
||||||
|
case ESP_HF_CURRENT_CALL_STATUS_ALERTING:
|
||||||
|
return "alerting";
|
||||||
|
case ESP_HF_CURRENT_CALL_STATUS_INCOMING:
|
||||||
|
case ESP_HF_CURRENT_CALL_STATUS_WAITING:
|
||||||
|
return "ringing";
|
||||||
|
case ESP_HF_CURRENT_CALL_STATUS_HELD_BY_RESP_HOLD:
|
||||||
|
return "held";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* atResponseCodeToString(esp_hf_at_response_code_t code) {
|
||||||
|
switch (code) {
|
||||||
|
case ESP_HF_AT_RESPONSE_CODE_OK:
|
||||||
|
return "ok";
|
||||||
|
case ESP_HF_AT_RESPONSE_CODE_ERR:
|
||||||
|
return "error";
|
||||||
|
case ESP_HF_AT_RESPONSE_CODE_NO_CARRIER:
|
||||||
|
return "no_carrier";
|
||||||
|
case ESP_HF_AT_RESPONSE_CODE_BUSY:
|
||||||
|
return "busy";
|
||||||
|
case ESP_HF_AT_RESPONSE_CODE_NO_ANSWER:
|
||||||
|
return "no_answer";
|
||||||
|
case ESP_HF_AT_RESPONSE_CODE_DELAYED:
|
||||||
|
return "delayed";
|
||||||
|
case ESP_HF_AT_RESPONSE_CODE_BLACKLISTED:
|
||||||
|
return "blacklisted";
|
||||||
|
case ESP_HF_AT_RESPONSE_CODE_CME:
|
||||||
|
return "cme";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void btGapCallback(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t* param) {
|
void btGapCallback(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t* param) {
|
||||||
if (g_manager == nullptr || param == nullptr) {
|
if (g_manager == nullptr || param == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -144,11 +233,18 @@ BluetoothManager::BluetoothManager()
|
|||||||
ble_client_connected_(false),
|
ble_client_connected_(false),
|
||||||
connected_(false),
|
connected_(false),
|
||||||
hfp_active_(false),
|
hfp_active_(false),
|
||||||
|
slc_connected_(false),
|
||||||
ble_active_(false),
|
ble_active_(false),
|
||||||
|
discoverable_(false),
|
||||||
security_enabled_(false),
|
security_enabled_(false),
|
||||||
|
pbap_supported_(false),
|
||||||
|
pbap_synced_(false),
|
||||||
peer_mac_(""),
|
peer_mac_(""),
|
||||||
peer_addr_{0},
|
peer_addr_{0},
|
||||||
peer_addr_valid_(false),
|
peer_addr_valid_(false),
|
||||||
|
call_state_("idle"),
|
||||||
|
last_dialed_number_(""),
|
||||||
|
pbap_last_error_("pbap_not_available_on_esp32_arduino_bluedroid"),
|
||||||
last_hfp_event_("idle"),
|
last_hfp_event_("idle"),
|
||||||
last_ble_event_("idle"),
|
last_ble_event_("idle"),
|
||||||
last_error_(""),
|
last_error_(""),
|
||||||
@@ -159,10 +255,15 @@ bool BluetoothManager::begin(BoardProfile profile) {
|
|||||||
features_ = getFeatureMatrix(profile);
|
features_ = getFeatureMatrix(profile);
|
||||||
connected_ = false;
|
connected_ = false;
|
||||||
hfp_active_ = false;
|
hfp_active_ = false;
|
||||||
|
slc_connected_ = false;
|
||||||
ble_active_ = false;
|
ble_active_ = false;
|
||||||
|
discoverable_ = false;
|
||||||
|
pbap_synced_ = false;
|
||||||
ble_client_connected_ = false;
|
ble_client_connected_ = false;
|
||||||
peer_mac_ = "";
|
peer_mac_ = "";
|
||||||
peer_addr_valid_ = false;
|
peer_addr_valid_ = false;
|
||||||
|
call_state_ = "idle";
|
||||||
|
last_dialed_number_ = "";
|
||||||
memset(peer_addr_, 0, sizeof(peer_addr_));
|
memset(peer_addr_, 0, sizeof(peer_addr_));
|
||||||
last_error_ = "";
|
last_error_ = "";
|
||||||
last_hfp_event_ = "initialized";
|
last_hfp_event_ = "initialized";
|
||||||
@@ -363,10 +464,17 @@ void BluetoothManager::statusToJson(JsonObject obj) const {
|
|||||||
obj["stack_ready"] = stack_ready_;
|
obj["stack_ready"] = stack_ready_;
|
||||||
obj["connected"] = connected_;
|
obj["connected"] = connected_;
|
||||||
obj["hfp_active"] = hfp_active_;
|
obj["hfp_active"] = hfp_active_;
|
||||||
|
obj["slc_connected"] = slc_connected_;
|
||||||
obj["hfp_requested"] = hfp_requested_;
|
obj["hfp_requested"] = hfp_requested_;
|
||||||
|
obj["call_state"] = call_state_;
|
||||||
|
obj["last_dialed_number"] = last_dialed_number_;
|
||||||
obj["ble_active"] = ble_active_;
|
obj["ble_active"] = ble_active_;
|
||||||
|
obj["discoverable"] = discoverable_;
|
||||||
obj["ble_client_connected"] = ble_client_connected_;
|
obj["ble_client_connected"] = ble_client_connected_;
|
||||||
obj["security_enabled"] = security_enabled_;
|
obj["security_enabled"] = security_enabled_;
|
||||||
|
obj["pbap_supported"] = pbap_supported_;
|
||||||
|
obj["pbap_synced"] = pbap_synced_;
|
||||||
|
obj["pbap_last_error"] = pbap_last_error_;
|
||||||
obj["peer"] = peer_mac_;
|
obj["peer"] = peer_mac_;
|
||||||
obj["last_hfp_event"] = last_hfp_event_;
|
obj["last_hfp_event"] = last_hfp_event_;
|
||||||
obj["last_ble_event"] = last_ble_event_;
|
obj["last_ble_event"] = last_ble_event_;
|
||||||
@@ -379,6 +487,137 @@ void BluetoothManager::setSecurity(bool enabled) {
|
|||||||
security_enabled_ = enabled;
|
security_enabled_ = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool BluetoothManager::setDiscoverable(bool enabled) {
|
||||||
|
if (!ensureBtStackReady()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const esp_bt_discovery_mode_t mode =
|
||||||
|
enabled ? ESP_BT_GENERAL_DISCOVERABLE : ESP_BT_NON_DISCOVERABLE;
|
||||||
|
const esp_err_t err = esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, mode);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
last_error_ = "bt_discoverable_failed:" + errToString(err);
|
||||||
|
notifyBluetooth("discoverable_failed", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
discoverable_ = enabled;
|
||||||
|
last_error_ = "";
|
||||||
|
notifyBluetooth(enabled ? "discoverable_on" : "discoverable_off");
|
||||||
|
publishBleStatus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluetoothManager::dial(const String& number) {
|
||||||
|
if (!requireCallControlReady("dial")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String dialed = number;
|
||||||
|
dialed.trim();
|
||||||
|
if (!isValidDialNumber(dialed)) {
|
||||||
|
last_error_ = "dial_invalid_number";
|
||||||
|
notifyBluetooth("hfp_dial_failed", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esp_err_t err = esp_hf_client_dial(dialed.c_str());
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
last_error_ = "hfp_dial_failed:" + errToString(err);
|
||||||
|
notifyBluetooth("hfp_dial_failed", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_dialed_number_ = dialed;
|
||||||
|
call_state_ = "dialing";
|
||||||
|
last_hfp_event_ = "dial_requested";
|
||||||
|
notifyBluetooth("hfp_dial_requested");
|
||||||
|
publishBleStatus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluetoothManager::redial() {
|
||||||
|
if (!requireCallControlReady("redial")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esp_err_t err = esp_hf_client_dial(nullptr);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
last_error_ = "hfp_redial_failed:" + errToString(err);
|
||||||
|
notifyBluetooth("hfp_redial_failed", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
call_state_ = "dialing";
|
||||||
|
last_hfp_event_ = "redial_requested";
|
||||||
|
notifyBluetooth("hfp_redial_requested");
|
||||||
|
publishBleStatus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluetoothManager::answerCall() {
|
||||||
|
if (!requireCallControlReady("answer")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esp_err_t err = esp_hf_client_answer_call();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
last_error_ = "hfp_answer_failed:" + errToString(err);
|
||||||
|
notifyBluetooth("hfp_answer_failed", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
call_state_ = "active";
|
||||||
|
last_hfp_event_ = "answer_requested";
|
||||||
|
notifyBluetooth("hfp_answer_requested");
|
||||||
|
publishBleStatus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluetoothManager::hangupCall() {
|
||||||
|
if (!requireCallControlReady("hangup")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esp_err_t err = esp_hf_client_reject_call();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
last_error_ = "hfp_hangup_failed:" + errToString(err);
|
||||||
|
notifyBluetooth("hfp_hangup_failed", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
call_state_ = "ending";
|
||||||
|
last_hfp_event_ = "hangup_requested";
|
||||||
|
notifyBluetooth("hfp_hangup_requested");
|
||||||
|
publishBleStatus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluetoothManager::queryCurrentCalls() {
|
||||||
|
if (!requireCallControlReady("calls_query")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esp_err_t err = esp_hf_client_query_current_calls();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
last_error_ = "hfp_calls_query_failed:" + errToString(err);
|
||||||
|
notifyBluetooth("hfp_calls_query_failed", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_hfp_event_ = "calls_query_requested";
|
||||||
|
notifyBluetooth("hfp_calls_query_requested");
|
||||||
|
publishBleStatus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluetoothManager::syncPbapContacts() {
|
||||||
|
pbap_synced_ = false;
|
||||||
|
pbap_last_error_ = "pbap_not_available_on_esp32_arduino_bluedroid";
|
||||||
|
last_error_ = pbap_last_error_;
|
||||||
|
notifyBluetooth("pbap_sync_unsupported", pbap_last_error_.c_str());
|
||||||
|
publishBleStatus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void BluetoothManager::setBleCommandHandler(std::function<String(const String&)> handler) {
|
void BluetoothManager::setBleCommandHandler(std::function<String(const String&)> handler) {
|
||||||
ble_command_handler_ = std::move(handler);
|
ble_command_handler_ = std::move(handler);
|
||||||
}
|
}
|
||||||
@@ -395,6 +634,18 @@ bool BluetoothManager::isBleActive() const {
|
|||||||
return ble_active_;
|
return ble_active_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool BluetoothManager::isDiscoverable() const {
|
||||||
|
return discoverable_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluetoothManager::isPbapSupported() const {
|
||||||
|
return pbap_supported_;
|
||||||
|
}
|
||||||
|
|
||||||
|
String BluetoothManager::callState() const {
|
||||||
|
return call_state_;
|
||||||
|
}
|
||||||
|
|
||||||
String BluetoothManager::peerMac() const {
|
String BluetoothManager::peerMac() const {
|
||||||
return peer_mac_;
|
return peer_mac_;
|
||||||
}
|
}
|
||||||
@@ -446,7 +697,14 @@ bool BluetoothManager::ensureBtStackReady() {
|
|||||||
memcpy(pin_code, kLegacyPinCode, strlen(kLegacyPinCode));
|
memcpy(pin_code, kLegacyPinCode, strlen(kLegacyPinCode));
|
||||||
esp_bt_gap_set_pin(ESP_BT_PIN_TYPE_FIXED, 4, pin_code);
|
esp_bt_gap_set_pin(ESP_BT_PIN_TYPE_FIXED, 4, pin_code);
|
||||||
// Keep classic BT connectable for outbound HFP while avoiding random inbound ACL grabs.
|
// Keep classic BT connectable for outbound HFP while avoiding random inbound ACL grabs.
|
||||||
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
|
const esp_err_t scan_err =
|
||||||
|
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
|
||||||
|
if (scan_err != ESP_OK) {
|
||||||
|
last_error_ = "gap_scan_mode_failed:" + errToString(scan_err);
|
||||||
|
notifyBluetooth("stack_failed", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
discoverable_ = false;
|
||||||
|
|
||||||
stack_ready_ = true;
|
stack_ready_ = true;
|
||||||
notifyBluetooth("stack_ready");
|
notifyBluetooth("stack_ready");
|
||||||
@@ -483,6 +741,26 @@ bool BluetoothManager::ensureHfpClientReady() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool BluetoothManager::requireCallControlReady(const char* operation) {
|
||||||
|
if (!features_.has_hfp) {
|
||||||
|
last_error_ = "hfp_not_supported";
|
||||||
|
notifyBluetooth("hfp_call_control_unavailable", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ensureHfpClientReady()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connected_ || !slc_connected_) {
|
||||||
|
last_error_ = String(operation) + ":hfp_not_connected";
|
||||||
|
notifyBluetooth("hfp_call_control_unavailable", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool BluetoothManager::parseMac(const String& mac, uint8_t out[6]) const {
|
bool BluetoothManager::parseMac(const String& mac, uint8_t out[6]) const {
|
||||||
if (out == nullptr) {
|
if (out == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
@@ -561,24 +839,29 @@ void BluetoothManager::handleHfpEvent(int event, const void* raw_param) {
|
|||||||
case ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTED:
|
case ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTED:
|
||||||
connected_ = false;
|
connected_ = false;
|
||||||
hfp_active_ = false;
|
hfp_active_ = false;
|
||||||
|
slc_connected_ = false;
|
||||||
|
call_state_ = "idle";
|
||||||
last_hfp_event_ = "disconnected";
|
last_hfp_event_ = "disconnected";
|
||||||
notifyBluetooth("hfp_disconnected");
|
notifyBluetooth("hfp_disconnected");
|
||||||
break;
|
break;
|
||||||
case ESP_HF_CLIENT_CONNECTION_STATE_CONNECTING:
|
case ESP_HF_CLIENT_CONNECTION_STATE_CONNECTING:
|
||||||
connected_ = false;
|
connected_ = false;
|
||||||
hfp_active_ = false;
|
hfp_active_ = false;
|
||||||
|
slc_connected_ = false;
|
||||||
last_hfp_event_ = "connecting";
|
last_hfp_event_ = "connecting";
|
||||||
notifyBluetooth("hfp_connecting");
|
notifyBluetooth("hfp_connecting");
|
||||||
break;
|
break;
|
||||||
case ESP_HF_CLIENT_CONNECTION_STATE_CONNECTED:
|
case ESP_HF_CLIENT_CONNECTION_STATE_CONNECTED:
|
||||||
connected_ = true;
|
connected_ = true;
|
||||||
hfp_active_ = false;
|
hfp_active_ = false;
|
||||||
|
slc_connected_ = false;
|
||||||
last_hfp_event_ = "rfcomm_connected";
|
last_hfp_event_ = "rfcomm_connected";
|
||||||
notifyBluetooth("hfp_rfcomm_connected");
|
notifyBluetooth("hfp_rfcomm_connected");
|
||||||
break;
|
break;
|
||||||
case ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED:
|
case ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED:
|
||||||
connected_ = true;
|
connected_ = true;
|
||||||
hfp_active_ = true;
|
hfp_active_ = true;
|
||||||
|
slc_connected_ = true;
|
||||||
last_hfp_event_ = "slc_connected";
|
last_hfp_event_ = "slc_connected";
|
||||||
notifyBluetooth("hfp_slc_connected");
|
notifyBluetooth("hfp_slc_connected");
|
||||||
if (hfp_requested_ && peer_addr_valid_) {
|
if (hfp_requested_ && peer_addr_valid_) {
|
||||||
@@ -586,6 +869,7 @@ void BluetoothManager::handleHfpEvent(int event, const void* raw_param) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTING:
|
case ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTING:
|
||||||
|
slc_connected_ = false;
|
||||||
last_hfp_event_ = "disconnecting";
|
last_hfp_event_ = "disconnecting";
|
||||||
notifyBluetooth("hfp_disconnecting");
|
notifyBluetooth("hfp_disconnecting");
|
||||||
break;
|
break;
|
||||||
@@ -620,8 +904,50 @@ void BluetoothManager::handleHfpEvent(int event, const void* raw_param) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case ESP_HF_CLIENT_CIND_CALL_SETUP_EVT:
|
||||||
|
if (param != nullptr) {
|
||||||
|
call_state_ = callSetupStateToString(param->call_setup.status);
|
||||||
|
}
|
||||||
|
last_hfp_event_ = "call_setup";
|
||||||
|
notifyBluetooth("hfp_call_setup");
|
||||||
|
break;
|
||||||
|
case ESP_HF_CLIENT_CIND_CALL_EVT:
|
||||||
|
if (param != nullptr) {
|
||||||
|
if (param->call.status == ESP_HF_CALL_STATUS_CALL_IN_PROGRESS) {
|
||||||
|
call_state_ = "active";
|
||||||
|
} else if (call_state_ == "ending" || call_state_ == "active" || call_state_ == "dialing" ||
|
||||||
|
call_state_ == "alerting" || call_state_ == "ringing") {
|
||||||
|
call_state_ = "idle";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_hfp_event_ = "call_status";
|
||||||
|
notifyBluetooth("hfp_call_status");
|
||||||
|
break;
|
||||||
|
case ESP_HF_CLIENT_CIND_CALL_HELD_EVT:
|
||||||
|
if (param != nullptr) {
|
||||||
|
call_state_ = callHeldStateToString(param->call_held.status);
|
||||||
|
}
|
||||||
|
last_hfp_event_ = "call_held";
|
||||||
|
notifyBluetooth("hfp_call_held");
|
||||||
|
break;
|
||||||
|
case ESP_HF_CLIENT_CLCC_EVT:
|
||||||
|
if (param != nullptr) {
|
||||||
|
call_state_ = clccStateToString(param->clcc.status);
|
||||||
|
if (param->clcc.number != nullptr && strlen(param->clcc.number) > 0) {
|
||||||
|
last_dialed_number_ = String(param->clcc.number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_hfp_event_ = "clcc";
|
||||||
|
notifyBluetooth("hfp_clcc");
|
||||||
|
break;
|
||||||
|
case ESP_HF_CLIENT_AT_RESPONSE_EVT:
|
||||||
|
if (param != nullptr && param->at_response.code != ESP_HF_AT_RESPONSE_CODE_OK) {
|
||||||
|
last_error_ = "hfp_at_response:" + String(atResponseCodeToString(param->at_response.code));
|
||||||
|
}
|
||||||
|
break;
|
||||||
case ESP_HF_CLIENT_RING_IND_EVT:
|
case ESP_HF_CLIENT_RING_IND_EVT:
|
||||||
last_hfp_event_ = "ring";
|
last_hfp_event_ = "ring";
|
||||||
|
call_state_ = "ringing";
|
||||||
notifyBluetooth("hfp_ring");
|
notifyBluetooth("hfp_ring");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ public:
|
|||||||
bool stopHFP();
|
bool stopHFP();
|
||||||
bool startBLE();
|
bool startBLE();
|
||||||
bool stopBLE();
|
bool stopBLE();
|
||||||
|
bool setDiscoverable(bool enabled);
|
||||||
|
bool dial(const String& number);
|
||||||
|
bool redial();
|
||||||
|
bool answerCall();
|
||||||
|
bool hangupCall();
|
||||||
|
bool queryCurrentCalls();
|
||||||
|
bool syncPbapContacts();
|
||||||
void logStatus() const;
|
void logStatus() const;
|
||||||
void statusToJson(JsonObject obj) const;
|
void statusToJson(JsonObject obj) const;
|
||||||
void setSecurity(bool enabled);
|
void setSecurity(bool enabled);
|
||||||
@@ -29,11 +36,15 @@ public:
|
|||||||
bool isSecurityEnabled() const;
|
bool isSecurityEnabled() const;
|
||||||
bool isHfpActive() const;
|
bool isHfpActive() const;
|
||||||
bool isBleActive() const;
|
bool isBleActive() const;
|
||||||
|
bool isDiscoverable() const;
|
||||||
|
bool isPbapSupported() const;
|
||||||
|
String callState() const;
|
||||||
String peerMac() const;
|
String peerMac() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool ensureBtStackReady();
|
bool ensureBtStackReady();
|
||||||
bool ensureHfpClientReady();
|
bool ensureHfpClientReady();
|
||||||
|
bool requireCallControlReady(const char* operation);
|
||||||
bool parseMac(const String& mac, uint8_t out[6]) const;
|
bool parseMac(const String& mac, uint8_t out[6]) const;
|
||||||
String formatMac(const uint8_t* mac) const;
|
String formatMac(const uint8_t* mac) const;
|
||||||
|
|
||||||
@@ -46,11 +57,18 @@ private:
|
|||||||
bool ble_client_connected_;
|
bool ble_client_connected_;
|
||||||
bool connected_;
|
bool connected_;
|
||||||
bool hfp_active_;
|
bool hfp_active_;
|
||||||
|
bool slc_connected_;
|
||||||
bool ble_active_;
|
bool ble_active_;
|
||||||
|
bool discoverable_;
|
||||||
bool security_enabled_;
|
bool security_enabled_;
|
||||||
|
bool pbap_supported_;
|
||||||
|
bool pbap_synced_;
|
||||||
String peer_mac_;
|
String peer_mac_;
|
||||||
uint8_t peer_addr_[6];
|
uint8_t peer_addr_[6];
|
||||||
bool peer_addr_valid_;
|
bool peer_addr_valid_;
|
||||||
|
String call_state_;
|
||||||
|
String last_dialed_number_;
|
||||||
|
String pbap_last_error_;
|
||||||
String last_hfp_event_;
|
String last_hfp_event_;
|
||||||
String last_ble_event_;
|
String last_ble_event_;
|
||||||
String last_error_;
|
String last_error_;
|
||||||
|
|||||||
@@ -646,6 +646,44 @@ void registerCommands() {
|
|||||||
return makeResponse(ok, "BT_HFP_DISCONNECT");
|
return makeResponse(ok, "BT_HFP_DISCONNECT");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
g_dispatcher.registerCommand("BT_DIAL", [](const String& args) {
|
||||||
|
String number = args;
|
||||||
|
number.trim();
|
||||||
|
if (number.isEmpty()) {
|
||||||
|
return makeResponse(false, "BT_DIAL invalid_number");
|
||||||
|
}
|
||||||
|
return makeResponse(g_bt.dial(number), "BT_DIAL");
|
||||||
|
});
|
||||||
|
|
||||||
|
g_dispatcher.registerCommand("DIAL", [](const String& args) {
|
||||||
|
String number = args;
|
||||||
|
number.trim();
|
||||||
|
if (number.isEmpty()) {
|
||||||
|
return makeResponse(false, "DIAL invalid_number");
|
||||||
|
}
|
||||||
|
return makeResponse(g_bt.dial(number), "DIAL");
|
||||||
|
});
|
||||||
|
|
||||||
|
g_dispatcher.registerCommand("BT_REDIAL", [](const String&) {
|
||||||
|
return makeResponse(g_bt.redial(), "BT_REDIAL");
|
||||||
|
});
|
||||||
|
|
||||||
|
g_dispatcher.registerCommand("BT_ANSWER", [](const String&) {
|
||||||
|
return makeResponse(g_bt.answerCall(), "BT_ANSWER");
|
||||||
|
});
|
||||||
|
|
||||||
|
g_dispatcher.registerCommand("BT_HANGUP", [](const String&) {
|
||||||
|
return makeResponse(g_bt.hangupCall(), "BT_HANGUP");
|
||||||
|
});
|
||||||
|
|
||||||
|
g_dispatcher.registerCommand("BT_CALLS", [](const String&) {
|
||||||
|
return makeResponse(g_bt.queryCurrentCalls(), "BT_CALLS");
|
||||||
|
});
|
||||||
|
|
||||||
|
g_dispatcher.registerCommand("BT_PBAP_SYNC", [](const String&) {
|
||||||
|
return makeResponse(g_bt.syncPbapContacts(), "BT_PBAP_SYNC unsupported");
|
||||||
|
});
|
||||||
|
|
||||||
g_dispatcher.registerCommand("BT_BLE_START", [](const String&) {
|
g_dispatcher.registerCommand("BT_BLE_START", [](const String&) {
|
||||||
return makeResponse(g_bt.startBLE(), "BT_BLE_START");
|
return makeResponse(g_bt.startBLE(), "BT_BLE_START");
|
||||||
});
|
});
|
||||||
@@ -654,6 +692,14 @@ void registerCommands() {
|
|||||||
return makeResponse(g_bt.stopBLE(), "BT_BLE_STOP");
|
return makeResponse(g_bt.stopBLE(), "BT_BLE_STOP");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
g_dispatcher.registerCommand("BT_DISCOVERABLE_ON", [](const String&) {
|
||||||
|
return makeResponse(g_bt.setDiscoverable(true), "BT_DISCOVERABLE_ON");
|
||||||
|
});
|
||||||
|
|
||||||
|
g_dispatcher.registerCommand("BT_DISCOVERABLE_OFF", [](const String&) {
|
||||||
|
return makeResponse(g_bt.setDiscoverable(false), "BT_DISCOVERABLE_OFF");
|
||||||
|
});
|
||||||
|
|
||||||
g_dispatcher.registerCommand("BT_STATUS", [](const String&) {
|
g_dispatcher.registerCommand("BT_STATUS", [](const String&) {
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
g_bt.statusToJson(doc.to<JsonObject>());
|
g_bt.statusToJson(doc.to<JsonObject>());
|
||||||
|
|||||||
Reference in New Issue
Block a user