diff --git a/docs/README.md b/docs/README.md index 69ae10d..6cf75c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -37,6 +37,7 @@ Chaque fiche détaille : rôle, API, notifications, points de validation, synerg | Tests livraison | plan_tests_livraison.md | | WebUI | plan_webui.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 | ## Procédures QA & hardware diff --git a/docs/SPECS_STATE.md b/docs/SPECS_STATE.md index 446761d..9d99bda 100644 --- a/docs/SPECS_STATE.md +++ b/docs/SPECS_STATE.md @@ -3,6 +3,7 @@ ## 1) Index des specs - `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 spec mirror: `RTC_BL_PHONE - Spec - WebUI Route Parity v1` - 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é) - `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` 1. Mettre à jour les issues de pilotage (`#10`, `#11`) avec les liens vers les runs CI et l’artefact. diff --git a/docs/spec_bt_hfp_pbap_dialing_v1.md b/docs/spec_bt_hfp_pbap_dialing_v1.md new file mode 100644 index 0000000..66c4b60 --- /dev/null +++ b/docs/spec_bt_hfp_pbap_dialing_v1.md @@ -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 ` / alias `DIAL ` +- `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 `). +- 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. diff --git a/src/bluetooth/BluetoothManager.cpp b/src/bluetooth/BluetoothManager.cpp index 0f0ee85..1ef2918 100644 --- a/src/bluetooth/BluetoothManager.cpp +++ b/src/bluetooth/BluetoothManager.cpp @@ -14,6 +14,7 @@ #include #include +#include #include namespace { @@ -43,6 +44,94 @@ String errToString(esp_err_t err) { 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(number.length()); ++i) { + if (!isDialableChar(number[static_cast(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) { if (g_manager == nullptr || param == nullptr) { return; @@ -144,11 +233,18 @@ BluetoothManager::BluetoothManager() ble_client_connected_(false), connected_(false), hfp_active_(false), + slc_connected_(false), ble_active_(false), + discoverable_(false), security_enabled_(false), + pbap_supported_(false), + pbap_synced_(false), peer_mac_(""), peer_addr_{0}, 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_ble_event_("idle"), last_error_(""), @@ -159,10 +255,15 @@ bool BluetoothManager::begin(BoardProfile profile) { features_ = getFeatureMatrix(profile); connected_ = false; hfp_active_ = false; + slc_connected_ = false; ble_active_ = false; + discoverable_ = false; + pbap_synced_ = false; ble_client_connected_ = false; peer_mac_ = ""; peer_addr_valid_ = false; + call_state_ = "idle"; + last_dialed_number_ = ""; memset(peer_addr_, 0, sizeof(peer_addr_)); last_error_ = ""; last_hfp_event_ = "initialized"; @@ -363,10 +464,17 @@ void BluetoothManager::statusToJson(JsonObject obj) const { obj["stack_ready"] = stack_ready_; obj["connected"] = connected_; obj["hfp_active"] = hfp_active_; + obj["slc_connected"] = slc_connected_; obj["hfp_requested"] = hfp_requested_; + obj["call_state"] = call_state_; + obj["last_dialed_number"] = last_dialed_number_; obj["ble_active"] = ble_active_; + obj["discoverable"] = discoverable_; obj["ble_client_connected"] = ble_client_connected_; 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["last_hfp_event"] = last_hfp_event_; obj["last_ble_event"] = last_ble_event_; @@ -379,6 +487,137 @@ void BluetoothManager::setSecurity(bool 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 handler) { ble_command_handler_ = std::move(handler); } @@ -395,6 +634,18 @@ bool BluetoothManager::isBleActive() const { 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 { return peer_mac_; } @@ -446,7 +697,14 @@ bool BluetoothManager::ensureBtStackReady() { memcpy(pin_code, kLegacyPinCode, strlen(kLegacyPinCode)); 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. - 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; notifyBluetooth("stack_ready"); @@ -483,6 +741,26 @@ bool BluetoothManager::ensureHfpClientReady() { 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 { if (out == nullptr) { return false; @@ -561,24 +839,29 @@ void BluetoothManager::handleHfpEvent(int event, const void* raw_param) { case ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTED: connected_ = false; hfp_active_ = false; + slc_connected_ = false; + call_state_ = "idle"; last_hfp_event_ = "disconnected"; notifyBluetooth("hfp_disconnected"); break; case ESP_HF_CLIENT_CONNECTION_STATE_CONNECTING: connected_ = false; hfp_active_ = false; + slc_connected_ = false; last_hfp_event_ = "connecting"; notifyBluetooth("hfp_connecting"); break; case ESP_HF_CLIENT_CONNECTION_STATE_CONNECTED: connected_ = true; hfp_active_ = false; + slc_connected_ = false; last_hfp_event_ = "rfcomm_connected"; notifyBluetooth("hfp_rfcomm_connected"); break; case ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED: connected_ = true; hfp_active_ = true; + slc_connected_ = true; last_hfp_event_ = "slc_connected"; notifyBluetooth("hfp_slc_connected"); if (hfp_requested_ && peer_addr_valid_) { @@ -586,6 +869,7 @@ void BluetoothManager::handleHfpEvent(int event, const void* raw_param) { } break; case ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTING: + slc_connected_ = false; last_hfp_event_ = "disconnecting"; notifyBluetooth("hfp_disconnecting"); break; @@ -620,8 +904,50 @@ void BluetoothManager::handleHfpEvent(int event, const void* raw_param) { } 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: last_hfp_event_ = "ring"; + call_state_ = "ringing"; notifyBluetooth("hfp_ring"); break; default: diff --git a/src/bluetooth/BluetoothManager.h b/src/bluetooth/BluetoothManager.h index d1cd2f1..c97c7b2 100644 --- a/src/bluetooth/BluetoothManager.h +++ b/src/bluetooth/BluetoothManager.h @@ -18,6 +18,13 @@ public: bool stopHFP(); bool startBLE(); 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 statusToJson(JsonObject obj) const; void setSecurity(bool enabled); @@ -29,11 +36,15 @@ public: bool isSecurityEnabled() const; bool isHfpActive() const; bool isBleActive() const; + bool isDiscoverable() const; + bool isPbapSupported() const; + String callState() const; String peerMac() const; private: bool ensureBtStackReady(); bool ensureHfpClientReady(); + bool requireCallControlReady(const char* operation); bool parseMac(const String& mac, uint8_t out[6]) const; String formatMac(const uint8_t* mac) const; @@ -46,11 +57,18 @@ private: bool ble_client_connected_; bool connected_; bool hfp_active_; + bool slc_connected_; bool ble_active_; + bool discoverable_; bool security_enabled_; + bool pbap_supported_; + bool pbap_synced_; String peer_mac_; uint8_t peer_addr_[6]; bool peer_addr_valid_; + String call_state_; + String last_dialed_number_; + String pbap_last_error_; String last_hfp_event_; String last_ble_event_; String last_error_; diff --git a/src/main.cpp b/src/main.cpp index 1b45b97..8b4329e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -646,6 +646,44 @@ void registerCommands() { 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&) { return makeResponse(g_bt.startBLE(), "BT_BLE_START"); }); @@ -654,6 +692,14 @@ void registerCommands() { 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&) { JsonDocument doc; g_bt.statusToJson(doc.to());