feat(bt): add HFP call controls and PBAP capability status

This commit is contained in:
Clément SAILLANT
2026-02-21 22:51:25 +01:00
parent 1f277f8598
commit 52c4aba36b
6 changed files with 492 additions and 1 deletions
+1
View File
@@ -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
+9
View File
@@ -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 lartefact.
+91
View File
@@ -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 dimplé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.
+327 -1
View File
@@ -14,6 +14,7 @@
#include <esp_gap_bt_api.h>
#include <esp_hf_client_api.h>
#include <cctype>
#include <cstring>
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<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) {
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<String(const String&)> 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:
+18
View File
@@ -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_;
+46
View File
@@ -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<JsonObject>());