feat: chain wifi webui espnow and bt track execution
This commit is contained in:
@@ -116,8 +116,21 @@
|
|||||||
<input type="text" id="btHfpAddr" placeholder="MAC téléphone HFP (AA:BB:CC:DD:EE:FF)">
|
<input type="text" id="btHfpAddr" placeholder="MAC téléphone HFP (AA:BB:CC:DD:EE:FF)">
|
||||||
<button type="submit">HFP Connect</button>
|
<button type="submit">HFP Connect</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form id="btDialForm">
|
||||||
|
<input type="text" id="btDialNumber" placeholder="Numéro (ex: 0612345678)">
|
||||||
|
<button type="submit">Dial</button>
|
||||||
|
</form>
|
||||||
<div class="inline-actions">
|
<div class="inline-actions">
|
||||||
<button id="btHfpDisconnectBtn">HFP Disconnect</button>
|
<button id="btHfpDisconnectBtn">HFP Disconnect</button>
|
||||||
|
<button id="btRedialBtn">Redial</button>
|
||||||
|
<button id="btAnswerBtn">Answer</button>
|
||||||
|
<button id="btHangupBtn">Hangup</button>
|
||||||
|
<button id="btCallsBtn">Calls Query</button>
|
||||||
|
</div>
|
||||||
|
<div class="inline-actions">
|
||||||
|
<button id="btDiscoverableOnBtn">Discoverable ON</button>
|
||||||
|
<button id="btDiscoverableOffBtn">Discoverable OFF</button>
|
||||||
|
<button id="btPbapSyncBtn">PBAP Sync</button>
|
||||||
<button id="btBleStartBtn">BLE Start</button>
|
<button id="btBleStartBtn">BLE Start</button>
|
||||||
<button id="btBleStopBtn">BLE Stop</button>
|
<button id="btBleStopBtn">BLE Stop</button>
|
||||||
<button id="btRefreshBtn">Rafraîchir BT</button>
|
<button id="btRefreshBtn">Rafraîchir BT</button>
|
||||||
|
|||||||
+118
-1
@@ -80,9 +80,12 @@ async function refreshStatus() {
|
|||||||
const wifiState = status.wifi?.state || "n/a";
|
const wifiState = status.wifi?.state || "n/a";
|
||||||
const mqttConnected = status.mqtt?.connected ? "on" : "off";
|
const mqttConnected = status.mqtt?.connected ? "on" : "off";
|
||||||
const peers = status.espnow?.peer_count ?? 0;
|
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 =
|
line.textContent =
|
||||||
`board=${status.board_profile || "n/a"} telephony=${telephonyState} hook=${hook} ` +
|
`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);
|
setJson("statusJson", status);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
line.textContent = `Erreur statut: ${error.message}`;
|
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 () => {
|
document.getElementById("btBleStartBtn").addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
const result = await requestJson("/api/bluetooth/ble/start", {
|
const result = await requestJson("/api/bluetooth/ble/start", {
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ Chaque fiche détaille : rôle, API, notifications, points de validation, synerg
|
|||||||
| 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 |
|
| 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 |
|
| État des specs | SPECS_STATE.md |
|
||||||
|
|
||||||
## Procédures QA & hardware
|
## Procédures QA & hardware
|
||||||
|
|||||||
@@ -44,3 +44,47 @@
|
|||||||
- Écart documentation / exécution (doD non uniformément tracée)
|
- É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é)
|
- 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
|
- 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
|
||||||
|
|||||||
@@ -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é.
|
||||||
@@ -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.
|
||||||
@@ -46,3 +46,51 @@
|
|||||||
| `mqtt connect/disconnect/publish` | PASS | endpoints `/api/network/mqtt/*` presents front/backend |
|
| `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 |
|
| `espnow on/off/send/peer` | PASS | endpoints `/api/network/espnow/*` presents front/backend |
|
||||||
| `control actions` | PASS | endpoint `/api/control` present 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.
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ build_flags =
|
|||||||
${env.build_flags}
|
${env.build_flags}
|
||||||
-DBOARD_PROFILE_A252
|
-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]
|
[env:test]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp32dev
|
board = esp32dev
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#ifndef BLUETOOTH_I_BLUETOOTH_BACKEND_H
|
||||||
|
#define BLUETOOTH_I_BLUETOOTH_BACKEND_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#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
|
||||||
+80
-1
@@ -151,6 +151,55 @@ bool extractBridgeCommand(JsonVariantConst payload, String& out_cmd, uint8_t dep
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool buildRtcBlV1BridgeCommand(JsonVariantConst payload,
|
||||||
|
String& out_cmd,
|
||||||
|
String& out_request_id,
|
||||||
|
bool& out_is_v1) {
|
||||||
|
out_is_v1 = false;
|
||||||
|
if (!payload.is<JsonObjectConst>()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObjectConst obj = payload.as<JsonObjectConst>();
|
||||||
|
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 buildI2sConfig(const A252PinsConfig& pins_cfg, const A252AudioConfig& audio_cfg) {
|
||||||
AudioConfig cfg;
|
AudioConfig cfg;
|
||||||
cfg.port = I2S_NUM_0;
|
cfg.port = I2S_NUM_0;
|
||||||
@@ -780,7 +829,10 @@ void registerCommands() {
|
|||||||
|
|
||||||
void processInboundBridgeCommand(const String& source, const JsonVariantConst& payload) {
|
void processInboundBridgeCommand(const String& source, const JsonVariantConst& payload) {
|
||||||
String cmd;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,6 +846,33 @@ void processInboundBridgeCommand(const String& source, const JsonVariantConst& p
|
|||||||
String payload_json;
|
String payload_json;
|
||||||
serializeJson(event, payload_json);
|
serializeJson(event, payload_json);
|
||||||
g_mqtt.publish("event", payload_json, false);
|
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<JsonVariantConst>());
|
||||||
|
} 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() {
|
void printHelp() {
|
||||||
|
|||||||
@@ -244,6 +244,33 @@ void WebServerManager::registerRoutes() {
|
|||||||
});
|
});
|
||||||
server_.on("/api/bluetooth/hfp/disconnect", HTTP_POST,
|
server_.on("/api/bluetooth/hfp/disconnect", HTTP_POST,
|
||||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_HFP_DISCONNECT"); });
|
[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,
|
server_.on("/api/bluetooth/ble/start", HTTP_POST,
|
||||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_BLE_START"); });
|
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_BLE_START"); });
|
||||||
server_.on("/api/bluetooth/ble/stop", HTTP_POST,
|
server_.on("/api/bluetooth/ble/stop", HTTP_POST,
|
||||||
|
|||||||
Reference in New Issue
Block a user