diff --git a/data/webui/index.html b/data/webui/index.html
index bef7f97..88dc947 100644
--- a/data/webui/index.html
+++ b/data/webui/index.html
@@ -116,8 +116,21 @@
+
+
+
+
diff --git a/data/webui/script.js b/data/webui/script.js
index 5fc4943..65ed287 100644
--- a/data/webui/script.js
+++ b/data/webui/script.js
@@ -80,9 +80,12 @@ async function refreshStatus() {
const wifiState = status.wifi?.state || "n/a";
const mqttConnected = status.mqtt?.connected ? "on" : "off";
const peers = status.espnow?.peer_count ?? 0;
+ const btCallState = status.bluetooth?.call_state || "n/a";
+ const btConnected = status.bluetooth?.connected ? "on" : "off";
+ const pbapSupported = status.bluetooth?.pbap_supported ? "yes" : "no";
line.textContent =
`board=${status.board_profile || "n/a"} telephony=${telephonyState} hook=${hook} ` +
- `wifi=${wifiState} mqtt=${mqttConnected} espnow_peers=${peers}`;
+ `wifi=${wifiState} mqtt=${mqttConnected} espnow_peers=${peers} bt=${btConnected} bt_call=${btCallState} pbap=${pbapSupported}`;
setJson("statusJson", status);
} catch (error) {
line.textContent = `Erreur statut: ${error.message}`;
@@ -424,6 +427,120 @@ function bindEvents() {
}
});
+ document.getElementById("btDialForm").addEventListener("submit", async (event) => {
+ event.preventDefault();
+ const number = document.getElementById("btDialNumber").value.trim();
+ try {
+ const result = await requestJson("/api/bluetooth/hfp/dial", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: JSON.stringify({ number }),
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshBluetooth(), refreshStatus()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("btRedialBtn").addEventListener("click", async () => {
+ try {
+ const result = await requestJson("/api/bluetooth/hfp/redial", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshBluetooth(), refreshStatus()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("btAnswerBtn").addEventListener("click", async () => {
+ try {
+ const result = await requestJson("/api/bluetooth/hfp/answer", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshBluetooth(), refreshStatus()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("btHangupBtn").addEventListener("click", async () => {
+ try {
+ const result = await requestJson("/api/bluetooth/hfp/hangup", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshBluetooth(), refreshStatus()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("btCallsBtn").addEventListener("click", async () => {
+ try {
+ const result = await requestJson("/api/bluetooth/hfp/calls", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshBluetooth(), refreshStatus()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("btDiscoverableOnBtn").addEventListener("click", async () => {
+ try {
+ const result = await requestJson("/api/bluetooth/discoverable/on", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshBluetooth(), refreshStatus()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("btDiscoverableOffBtn").addEventListener("click", async () => {
+ try {
+ const result = await requestJson("/api/bluetooth/discoverable/off", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshBluetooth(), refreshStatus()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("btPbapSyncBtn").addEventListener("click", async () => {
+ try {
+ const result = await requestJson("/api/bluetooth/pbap/sync", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshBluetooth(), refreshStatus()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
document.getElementById("btBleStartBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/ble/start", {
diff --git a/docs/README.md b/docs/README.md
index 6cf75c0..9fdb839 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -38,6 +38,8 @@ Chaque fiche détaille : rôle, API, notifications, points de validation, synerg
| 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 |
+| API ESP-NOW v1 | espnow_api_v1.md |
+| Faisabilité migration PBAP | bt_pbap_migration_feasibility.md |
| État des specs | SPECS_STATE.md |
## Procédures QA & hardware
diff --git a/docs/SPECS_STATE.md b/docs/SPECS_STATE.md
index 9d99bda..15f4bac 100644
--- a/docs/SPECS_STATE.md
+++ b/docs/SPECS_STATE.md
@@ -44,3 +44,47 @@
- Écart documentation / exécution (doD non uniformément tracée)
- Dépendances CI sur les branches de support non conservées (si nettoyage de branches engagé)
- Blocs matériels/firmware S3 (liens Bluetooth/HFP) pouvant masquer la visibilité produit
+
+## 6) Enchaînement Track A / Track B (Bluetooth, WiFi, WebUI, ESP-NOW)
+
+### Track A — Delivery rapide (état courant)
+
+- WiFi:
+ - endpoints Web/API présents: `/api/network/wifi`, `/connect`, `/disconnect`, `/reconnect`, `/scan`.
+ - fallback AP actif en perte STA (`state=ap_fallback`).
+- WebUI Bluetooth:
+ - endpoints étendus pour discoverable, dial/redial/answer/hangup/calls, pbap sync.
+ - affichage états BT enrichis (`call_state`, `slc_connected`, `pbap_supported`).
+- ESP-NOW:
+ - protocole v1 documenté (`docs/espnow_api_v1.md`).
+ - compat legacy conservée (`cmd/raw/command/action`).
+- Bluetooth HFP:
+ - commandes d'appel opérationnelles.
+ - PBAP toujours `unsupported` sur stack actuelle (comportement explicitement tracé).
+
+### Track B — Migration BT/PBAP (préparation)
+
+- Faisabilité documentée: `docs/bt_pbap_migration_feasibility.md`
+- Build migration préparé: `env:esp32dev-bt-migrate`
+- Interface d'abstraction ajoutée: `src/bluetooth/IBluetoothBackend.h`
+
+### Gates de validation
+
+- Gate local minimal:
+ - `platformio run -e esp32dev`
+ - `bash scripts/test_terminal.sh`
+ - `scripts/hw_validation.py --port-a252 /dev/cu.usbserial-0001`
+- Gate CI:
+ - workflows `Repo State` + `Firmware CI` verts sur `main`
+
+## 7) Exécution hardware locale (2026-02-21, ESP32 Audio Kit)
+
+- Cible testée: `/dev/cu.usbserial-0001`
+- Upload firmware: `platformio run -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001` -> `PASS`
+- Gate local: `bash scripts/test_terminal.sh` -> `PASS` (build unit test, host dtmf, parity parser, parity routes)
+- Smoke HW: `python3 scripts/hw_validation.py --port-a252 /dev/cu.usbserial-0001 --report-json artifacts/hw_validation_report.json --report-md artifacts/hw_validation_report.md` -> `PASS`
+- Snapshot runtime: `artifacts/runtime_snapshot.json` (inclut `BT_STATUS`, `WIFI_STATUS`, `ESPNOW_STATUS`, `/api/status`)
+- WiFi stability: `artifacts/wifi_stability_report.json` -> `PASS` (disconnect => `ap_fallback`, reconnect => `connected`, credentials persistées)
+- HFP opérable (stack actuelle): `artifacts/hfp_operational_report.json` -> `PASS` (discoverable on/off ok, `BT_DIAL` sans SLC en erreur contrôlée, `BT_PBAP_SYNC unsupported` explicite)
+- ESP-NOW protocole v1: `artifacts/espnow_protocol_v1_report.json` -> `FAIL` en bench 1 carte (pas de peer radio actif, `ESPNOW_SEND` retourne `ERR`)
+- Décision de gate: WiFi/WebUI/Bluetooth HFP command-level `GO`; ESP-NOW E2E v1 two-board `BLOCKED` jusqu’à disponibilité d’une 2e carte
diff --git a/docs/bt_pbap_migration_feasibility.md b/docs/bt_pbap_migration_feasibility.md
new file mode 100644
index 0000000..9bef5a7
--- /dev/null
+++ b/docs/bt_pbap_migration_feasibility.md
@@ -0,0 +1,61 @@
+# Faisabilité migration Bluetooth PBAP
+
+Date: 2026-02-21
+Repo: `RTC_BL_PHONE`
+
+## Résumé exécutif
+
+- Constats actuels:
+ - HFP est opérable côté commandes firmware.
+ - PBAP n'est pas exposé par la stack Arduino-ESP32/Bluedroid utilisée actuellement.
+- Décision court terme:
+ - conserver Track A livrable (HFP + WebUI + WiFi + ESP-NOW),
+ - lancer Track B migration stack pour PBAP.
+
+## 1) État technique actuel
+
+- Stack firmware active: Arduino ESP32 + Bluedroid (HFP client).
+- PBAP: API indisponible dans ce périmètre actuel.
+- Symptôme attendu: `BT_PBAP_SYNC` retourne `unsupported` avec cause explicite.
+
+## 2) Cibles étudiées
+
+1. Arduino-ESP32 actuel (baseline):
+ - Avantage: stable, faible risque régression.
+ - Limite: PBAP absent.
+2. Mode mixte Arduino + ESP-IDF (backend BT dédié):
+ - Avantage: accès plus bas niveau profils BT.
+ - Risque: complexité build/intégration.
+3. Backend BT alternatif (abstraction `IBluetoothBackend`):
+ - Avantage: migration progressive sans casser Track A.
+ - Risque: surcharge maintenance temporaire.
+
+## 3) Critère GO / NOGO Track B
+
+GO si:
+- PBAP sync fonctionne réellement (au moins 1 contact avec `display_name`, `phone_number`).
+- Build reproductible via `platformio.ini` (`env:esp32dev-bt-migrate`).
+- Pas de régression des commandes HFP existantes.
+
+NOGO si:
+- PBAP non atteignable en 2 itérations de migration contrôlée.
+- Régressions HFP non résolues.
+
+## 4) Plan de pivot
+
+Si NOGO:
+1. marquer PBAP `blocked by stack`,
+2. conserver Track A en production,
+3. ouvrir lot dédié migration profonde (estimation + risques + dépendances).
+
+## 5) Implémentation préparatoire déjà posée
+
+- Environnement de build migration ajouté:
+ - `env:esp32dev-bt-migrate` (`-DBT_BACKEND_EXPERIMENTAL=1`)
+- Interface d'abstraction introduite:
+ - `src/bluetooth/IBluetoothBackend.h`
+
+## 6) Risques
+
+- Risque principal: coût de migration stack BT supérieur au bénéfice court terme.
+- Risque secondaire: divergence comportements HFP entre backend baseline et backend migré.
diff --git a/docs/espnow_api_v1.md b/docs/espnow_api_v1.md
new file mode 100644
index 0000000..224b464
--- /dev/null
+++ b/docs/espnow_api_v1.md
@@ -0,0 +1,76 @@
+# ESP-NOW API v1 (`rtcbl/1`)
+
+Date: 2026-02-21
+Scope: contrat d'échange entre `RTC_BL_PHONE` (A252) et la seconde carte.
+
+## 1. Objectif
+
+Normaliser les trames ESP-NOW pour:
+- corréler requête/réponse,
+- conserver compatibilité legacy,
+- simplifier l'intégration du second repo.
+
+## 2. Requête v1
+
+```json
+{
+ "proto": "rtcbl/1",
+ "id": "req-001",
+ "ts": 1730000000,
+ "source": "a252",
+ "target": "peer|broadcast",
+ "cmd": "STATUS|CALL|WIFI_STATUS|BT_STATUS|...",
+ "args": {}
+}
+```
+
+Règles:
+- `proto` obligatoire pour activer le mode v1.
+- `id` recommandé (corrélation réponse).
+- `cmd` obligatoire.
+- `args` optionnel. S'il est présent, il est sérialisé et transmis au dispatcher.
+
+## 3. Réponse v1
+
+```json
+{
+ "proto": "rtcbl/1",
+ "id": "req-001",
+ "ok": true,
+ "code": "STATUS",
+ "data": {},
+ "error": ""
+}
+```
+
+Règles:
+- `id` reprend la requête.
+- `ok=false` => `error` non vide.
+- `data` contient le JSON de la commande si disponible.
+- fallback possible `data_raw` si la réponse n'est pas JSON.
+
+## 4. Compatibilité legacy
+
+Le firmware continue d'accepter les formats existants:
+- `{"cmd":"..."}`
+- `{"raw":"..."}`
+- `{"command":"..."}`
+- `{"action":"..."}`
+- variantes imbriquées via `event`, `message`, `payload`
+
+## 5. Commandes recommandées v1
+
+- `STATUS`
+- `CALL`
+- `WIFI_STATUS`
+- `MQTT_STATUS`
+- `ESPNOW_STATUS`
+- `BT_STATUS`
+
+## 6. Intégration second repo
+
+Checklist minimum côté seconde carte:
+1. Émettre `proto=rtcbl/1` + `id` unique par requête.
+2. Implémenter timeout de réponse (2-5s).
+3. Corréler sur `id`.
+4. Prévoir fallback legacy si `proto` absent en réponse.
diff --git a/docs/rapport_tests_fonctionnels.md b/docs/rapport_tests_fonctionnels.md
index 6304487..541d9be 100644
--- a/docs/rapport_tests_fonctionnels.md
+++ b/docs/rapport_tests_fonctionnels.md
@@ -46,3 +46,51 @@
| `mqtt connect/disconnect/publish` | PASS | endpoints `/api/network/mqtt/*` presents front/backend |
| `espnow on/off/send/peer` | PASS | endpoints `/api/network/espnow/*` presents front/backend |
| `control actions` | PASS | endpoint `/api/control` present front/backend |
+
+## Enchaînement Track A — Bluetooth / WiFi / WebUI / ESP-NOW
+
+- Date UTC: 2026-02-21T22:30:00Z
+- Scope: stabilisation continue Track A + préparation Track B PBAP.
+
+### Vérifications ajoutées
+
+| Domaine | Vérification | Verdict |
+|---|---|---|
+| Bluetooth Web/API | endpoints discoverable/dial/redial/answer/hangup/calls/pbap_sync exposés | PASS |
+| WebUI BT | actions UI alignées sur endpoints backend | PASS |
+| WiFi | endpoints connect/disconnect/reconnect/scan inchangés et disponibles | PASS |
+| ESP-NOW | contrat v1 documenté + compat legacy conservée | PASS |
+| PBAP | retourne explicitement `unsupported` tant que migration non finalisée | PASS |
+
+### Artefacts de référence
+
+- `artifacts/route_parity_report.json`
+- `artifacts/webui_bt_parity_report.json`
+- `artifacts/wifi_stability_report.json`
+- `artifacts/espnow_protocol_v1_report.json`
+- `artifacts/hfp_operational_report.json`
+
+## Exécution locale consolidée (ESP32 Audio Kit, 2026-02-21)
+
+- Port test: `/dev/cu.usbserial-0001`
+- Upload firmware: `platformio run -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001` -> `PASS`
+- Gate local: `bash scripts/test_terminal.sh` -> `PASS`
+- Validation HW: `scripts/hw_validation.py --port-a252 /dev/cu.usbserial-0001 --report-json artifacts/hw_validation_report.json --report-md artifacts/hw_validation_report.md` -> `PASS`
+- Snapshot runtime: `artifacts/runtime_snapshot.json`
+- WiFi stability (`artifacts/wifi_stability_report.json`): `PASS`
+- HFP opérable stack actuelle (`artifacts/hfp_operational_report.json`): `PASS`
+- ESP-NOW protocole v1 (`artifacts/espnow_protocol_v1_report.json`): `FAIL` sur bench 1 carte (absence de peer, `ESPNOW_SEND` en erreur contrôlée)
+
+### Détails clés observés
+
+- WiFi:
+ - `WIFI_DISCONNECT` force `state=ap_fallback` avec AP actif.
+ - `WIFI_RECONNECT` restaure `state=connected` en STA et coupe l’AP fallback.
+ - SSID restauré: `Les cils`; credentials persistées.
+- Bluetooth/HFP:
+ - `BT_DISCOVERABLE_ON/OFF`: `OK`.
+ - `BT_DIAL` sans SLC connecté: erreur contrôlée (attendue).
+ - `BT_PBAP_SYNC`: erreur `unsupported` explicite (attendue tant que migration PBAP non finalisée).
+- ESP-NOW:
+ - `ESPNOW_STATUS.ready=true`.
+ - `ESPNOW_SEND` v1 et legacy en échec sur banc mono-carte (pas de pair receveur actif), à retester en configuration 2 cartes.
diff --git a/platformio.ini b/platformio.ini
index 40ef130..db1ee2a 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -29,6 +29,14 @@ build_flags =
${env.build_flags}
-DBOARD_PROFILE_A252
+[env:esp32dev-bt-migrate]
+board = esp32dev
+board_build.partitions = huge_app.csv
+build_flags =
+ ${env.build_flags}
+ -DBOARD_PROFILE_A252
+ -DBT_BACKEND_EXPERIMENTAL=1
+
[env:test]
platform = espressif32
board = esp32dev
diff --git a/src/bluetooth/IBluetoothBackend.h b/src/bluetooth/IBluetoothBackend.h
new file mode 100644
index 0000000..7ed0bdc
--- /dev/null
+++ b/src/bluetooth/IBluetoothBackend.h
@@ -0,0 +1,32 @@
+#ifndef BLUETOOTH_I_BLUETOOTH_BACKEND_H
+#define BLUETOOTH_I_BLUETOOTH_BACKEND_H
+
+#include
+
+#include "core/PlatformProfile.h"
+
+struct PbapContact {
+ String display_name;
+ String phone_number;
+};
+
+class IBluetoothBackend {
+public:
+ virtual ~IBluetoothBackend() = default;
+
+ virtual bool begin(BoardProfile profile) = 0;
+ virtual bool connect(const String& mac) = 0;
+ virtual bool disconnect() = 0;
+
+ virtual bool setDiscoverable(bool enabled) = 0;
+ virtual bool dial(const String& number) = 0;
+ virtual bool redial() = 0;
+ virtual bool answer() = 0;
+ virtual bool hangup() = 0;
+ virtual bool queryCalls() = 0;
+
+ virtual bool syncPbap() = 0;
+ virtual bool isPbapSupported() const = 0;
+};
+
+#endif // BLUETOOTH_I_BLUETOOTH_BACKEND_H
diff --git a/src/main.cpp b/src/main.cpp
index de129d3..d8db072 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -151,6 +151,55 @@ bool extractBridgeCommand(JsonVariantConst payload, String& out_cmd, uint8_t dep
return false;
}
+bool buildRtcBlV1BridgeCommand(JsonVariantConst payload,
+ String& out_cmd,
+ String& out_request_id,
+ bool& out_is_v1) {
+ out_is_v1 = false;
+ if (!payload.is()) {
+ return false;
+ }
+
+ JsonObjectConst obj = payload.as();
+ const String proto = obj["proto"] | "";
+ if (!proto.equalsIgnoreCase("rtcbl/1")) {
+ return false;
+ }
+
+ const String cmd = obj["cmd"] | "";
+ if (cmd.isEmpty()) {
+ return false;
+ }
+
+ out_cmd = cmd;
+ out_cmd.trim();
+ if (out_cmd.isEmpty()) {
+ return false;
+ }
+
+ out_request_id = obj["id"] | "";
+ out_is_v1 = true;
+
+ if (obj["args"].isNull()) {
+ return true;
+ }
+
+ String args;
+ serializeJson(obj["args"], args);
+ args.trim();
+ if (!args.isEmpty() && args != "null") {
+ out_cmd += " ";
+ out_cmd += args;
+ }
+
+ return true;
+}
+
+bool isMacAddressString(const String& value) {
+ uint8_t mac[6] = {0};
+ return A252ConfigStore::parseMac(value, mac);
+}
+
AudioConfig buildI2sConfig(const A252PinsConfig& pins_cfg, const A252AudioConfig& audio_cfg) {
AudioConfig cfg;
cfg.port = I2S_NUM_0;
@@ -780,7 +829,10 @@ void registerCommands() {
void processInboundBridgeCommand(const String& source, const JsonVariantConst& payload) {
String cmd;
- if (!extractBridgeCommand(payload, cmd)) {
+ String request_id;
+ bool is_rtcbl_v1 = false;
+ if (!buildRtcBlV1BridgeCommand(payload, cmd, request_id, is_rtcbl_v1) &&
+ !extractBridgeCommand(payload, cmd)) {
return;
}
@@ -794,6 +846,33 @@ void processInboundBridgeCommand(const String& source, const JsonVariantConst& p
String payload_json;
serializeJson(event, payload_json);
g_mqtt.publish("event", payload_json, false);
+
+ if (!is_rtcbl_v1 || !isMacAddressString(source)) {
+ return;
+ }
+
+ JsonDocument response;
+ response["proto"] = "rtcbl/1";
+ response["id"] = request_id;
+ response["ok"] = result.ok;
+ response["code"] = result.code;
+ response["error"] = result.ok ? "" : (result.code.isEmpty() ? result.raw : result.code);
+
+ if (!result.json.isEmpty()) {
+ JsonDocument parsed;
+ if (deserializeJson(parsed, result.json) == DeserializationError::Ok) {
+ JsonVariant data = response["data"];
+ data.set(parsed.as());
+ } else {
+ response["data_raw"] = result.json;
+ }
+ } else if (!result.raw.isEmpty()) {
+ response["data_raw"] = result.raw;
+ }
+
+ String response_payload;
+ serializeJson(response, response_payload);
+ g_espnow.sendJson(source, response_payload);
}
void printHelp() {
diff --git a/src/web/WebServerManager.cpp b/src/web/WebServerManager.cpp
index 191cb99..35db57d 100644
--- a/src/web/WebServerManager.cpp
+++ b/src/web/WebServerManager.cpp
@@ -244,6 +244,33 @@ void WebServerManager::registerRoutes() {
});
server_.on("/api/bluetooth/hfp/disconnect", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_HFP_DISCONNECT"); });
+ server_.on("/api/bluetooth/discoverable/on", HTTP_POST,
+ [this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_DISCOVERABLE_ON"); });
+ server_.on("/api/bluetooth/discoverable/off", HTTP_POST,
+ [this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_DISCOVERABLE_OFF"); });
+ server_.on("/api/bluetooth/hfp/dial", HTTP_POST, [this](AsyncWebServerRequest* request) {
+ JsonDocument doc;
+ if (!extractJsonBody(request, doc)) {
+ request->send(400, "application/json", "{\"error\":\"invalid json body\"}");
+ return;
+ }
+ const String number = doc["number"] | "";
+ if (!isValidInput(number, 32)) {
+ request->send(400, "application/json", "{\"error\":\"invalid number\"}");
+ return;
+ }
+ handleDispatch(request, "BT_DIAL " + number);
+ });
+ server_.on("/api/bluetooth/hfp/redial", HTTP_POST,
+ [this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_REDIAL"); });
+ server_.on("/api/bluetooth/hfp/answer", HTTP_POST,
+ [this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_ANSWER"); });
+ server_.on("/api/bluetooth/hfp/hangup", HTTP_POST,
+ [this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_HANGUP"); });
+ server_.on("/api/bluetooth/hfp/calls", HTTP_POST,
+ [this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_CALLS"); });
+ server_.on("/api/bluetooth/pbap/sync", HTTP_POST,
+ [this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_PBAP_SYNC"); });
server_.on("/api/bluetooth/ble/start", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_BLE_START"); });
server_.on("/api/bluetooth/ble/stop", HTTP_POST,