From 9cb31bdea96fd01d968b34998cb89c03dc23b8ec Mon Sep 17 00:00:00 2001 From: clement Date: Fri, 19 Jun 2026 13:52:06 +0200 Subject: [PATCH] fix(bt): RX exclusif dtmf/SCO + perte lien BT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problème 1 : dtmf_task lisait le canal I2S RX après dtmf_stop(), provoquant une double lecture concurrente avec sco_mic_task (même buffer scratch -> corruption micro). - dtmf.c : ajout de s_run (volatile bool) séparé de s_armed_flag. dtmf_task() tourne dans while(s_run) ; quand s_armed_flag=false, break immédiat -> s_task_handle=NULL -> vTaskDelete(NULL). Plus aucun appel hal_i2s_capture_read_frame après dtmf_stop(). - dtmf_stop() : pose s_run=false, attend (poll 5 ms, timeout 100 ms) que s_task_handle==NULL, sinon vTaskDelete forcé + LOGW. - dtmf_start() : recrée la tâche si handle==NULL. Correction 1b (call_manager) : appel idempotent dtmf_stop() AVANT audio_router_sco_begin() dans BT_HFP_EV_AUDIO_CONNECTED. Problème 2 : BT_HFP_EV_SLC_DISCONNECTED non géré -> perte du lien BT silencieuse (SCO et sonnerie restaient actifs). - call_manager.c : case BT_HFP_EV_SLC_DISCONNECTED : coupe SCO si actif, slic_ring_stop(), go_idle(), st=ST_IDLE + LOGW. Tests : host dtmf 3/3 OK, build vert, boot propre flashé. --- .superpowers/sdd/p4-task-5-report.md | 294 +++++++++++++++++++++++++ components/call_manager/call_manager.c | 16 ++ components/dtmf/dtmf.c | 72 ++++-- 3 files changed, 362 insertions(+), 20 deletions(-) create mode 100644 .superpowers/sdd/p4-task-5-report.md diff --git a/.superpowers/sdd/p4-task-5-report.md b/.superpowers/sdd/p4-task-5-report.md new file mode 100644 index 0000000..adedba5 --- /dev/null +++ b/.superpowers/sdd/p4-task-5-report.md @@ -0,0 +1,294 @@ +# Phase 4 — Task 5 : call_manager HFP intégration complète + +**Date :** 2026-06-19 +**Commit :** `24e82fb feat(bt): call_manager HFP appels entrant/sortant` (47 car.) +**Status :** DONE_WITH_CONCERNS (test appel réel nécessite utilisateur + mobile) + +--- + +## 1. Extensions FSM — call_manager.c + +### Nouveaux états ajoutés +```c +typedef enum { + ST_IDLE, // raccroché — silence, PA coupée + ST_DIALTONE, // décroché — tonalité + attente chiffres + ST_DIALING, // numérotation — attente 3 s silence + ST_OUTGOING, // appel BT émis — attente AUDIO_CONNECTED + ST_INCOMING, // appel entrant — cloche + attente décroché + ST_ACTIVE // SCO bidirectionnel actif +} call_state_t; +``` + +### Variables pont BT (léger depuis tâche BT temps-réel) +```c +static volatile bool s_bt_dirty; +static volatile bt_hfp_event_t s_bt_ev; +static char s_incoming_num[32]; // strncpy dans callback +static volatile bool s_sco_msbc; +static bool s_sco_up; // géré uniquement dans call_task +``` + +### Callback call_manager_bt_event() +Posé uniquement des flags + strncpy du numéro CLIP. Jamais de blocage, jamais de stockage du pointeur `d->number` au-delà du strncpy. + +### Transitions implémentées +| Événement / Geste | État source | Action | État dest | +|---|---|---|---| +| BT_HFP_EV_INCOMING | IDLE/DIALTONE | slic_ring_start() | INCOMING | +| BT_HFP_EV_AUDIO_CONNECTED | tout | slic_ring_stop + sco_begin + s_sco_up=true | ACTIVE | +| BT_HFP_EV_AUDIO_DISCONNECTED | ACTIVE | audio_router_sco_end() | (inchangé) | +| BT_HFP_EV_CALL_ENDED | tout | slic_ring_stop + sco_end + busy si décroché | IDLE/DIALTONE | +| Raccroché en ACTIVE/OUTGOING/INCOMING | * | bt_hfp_hangup() + go_idle() | IDLE | +| Décroché en INCOMING | INCOMING | slic_ring_stop + bt_hfp_answer() | (attend AUDIO_CONNECTED) | +| Décroché en IDLE | IDLE | dialtone + dtmf_start | DIALTONE | +| 3 s silence après DIALING | DIALING | bt_hfp_dial() + dtmf_stop | OUTGOING | + +### go_idle() mis à jour +Coupe SCO si actif : `audio_router_sco_end()` + `s_sco_up = false`. + +--- + +## 2. Fix désenregistrement SCO — bt_hfp.c + +Dans le case `ESP_HF_CLIENT_AUDIO_STATE_DISCONNECTED`, **avant** `emit(...)` : +```c +esp_hf_client_register_data_callback(NULL, NULL); +``` +Cela désenregistre proprement les callbacks data SCO et évite des appels fantômes vers `audio_router_sco_feed_playback` / `audio_router_sco_take_capture` après la fin du canal SCO. + +--- + +## 3. Modifications CMakeLists.txt + +```cmake +idf_component_register( + SRCS "call_manager.c" + INCLUDE_DIRS "include" + REQUIRES hook_monitor dialer dtmf audio_router hal_i2s bt_hfp slic_ks0835 +) +``` +Ajout : `bt_hfp` (pour `bt_hfp_dial/answer/hangup` et les types `bt_hfp_event_t`) et `slic_ks0835` (pour `slic_ring_start/stop`). + +--- + +## 4. Modifications app_main.c + +- Retiré le handler temporaire `bt_log_cb` (Task 2). +- Ordre conservé : `call_manager_start()` AVANT `bt_hfp_init(call_manager_bt_event)`. +- Nouveau message de boot : `call_manager + bt_hfp prets — appairer sur 'RTC_BL_PHONE'`. + +--- + +## 5. Résultat build + +``` +Project build complete. +rtc_bl_phone.bin binary size 0xca170 bytes. +Smallest app partition is 0x2f0000 bytes. 0x225e90 bytes (73%) free. +``` +**Build vert, 0 warning, 0 erreur.** + +--- + +## 6. Logs de boot propre (capture série) + +``` +I (1449) rtc_phone: RTC BL PHONE — socle ESP-IDF v5.4 +I (1489) config_store: NVS prêt +I (1529) hal_i2s: I2S init OK — 16 kHz 16-bit stereo, full-duplex +I (1589) slic: SLIC init OK — SHK level=0 (on-hook) +I (1589) audio_router: tone task created +I (1599) dialer: dialer init +I (1609) call_manager: call_manager démarré +I (1639) call_manager: -> IDLE +I (2319) bt_hfp: bt_hfp pret — appairer le mobile sur 'RTC_BL_PHONE' +I (2319) rtc_phone: call_manager + bt_hfp prets — appairer sur 'RTC_BL_PHONE' +``` +PA off au boot confirmé (pas de `PA ON` dans les logs). + +--- + +## 7. Checklist de test utilisateur + +### Préparation +- [ ] Appareil flashé, branché, monitor série 115200 ouvert +- [ ] Mobile (iOS/Android) Bluetooth activé +- [ ] Appairer : Bluetooth → `RTC_BL_PHONE` → confirmer le code si demandé (SSP) + - Log attendu : `I (xxx) bt_hfp: appairage OK: ` + - Log attendu : `I (xxx) bt_hfp: SLC connecte` + +### Parcours A : Appel SORTANT +1. Décrocher le combiné + - Log : `-> DIALTONE (décroché)` +2. Composer un numéro (cadran à impulsions ou DTMF) + - Log : `-> DIALING, numéro en cours: "..."` +3. Attendre 3 secondes de silence + - Log : `appel sortant: "0612345678"` (ou autre) + - Le mobile doit composer le numéro +4. Le correspondant décroche + - Log : `I (xxx) bt_hfp: audio SCO connecte (mSBC 16k)` ou `(CVSD 8k)` + - Log : `-> ACTIVE (audio SCO ...)` + - **Audio bidirectionnel : parler dans le combiné, entendre la réponse** +5. Raccrocher le combiné + - Log : `-> IDLE` + - Appel BT terminé côté mobile + +### Parcours B : Appel ENTRANT +1. Appeler le mobile depuis un autre téléphone (le mobile doit être connecté en HFP) + - Log : `-> INCOMING ` + cloche du combiné sonne +2. Décrocher le combiné + - Log : `bt_hfp_answer() envoyé` + - Log : `-> ACTIVE (audio SCO ...)` + - **Audio bidirectionnel** +3. Raccrocher le combiné + - Log : `-> IDLE` + +### En cas de problème +- **Appairage coince (code redemandé en boucle)** : dans `components/bt_hfp/bt_hfp.c`, changer `ESP_BT_IO_CAP_IO` → `ESP_BT_IO_CAP_NONE` (pas de confirmation, appairage automatique). +- **Décroché ne déclenche rien** : vérifier la polarité SHK. Dans le header `board_config.h` ou la config SLIC, inverser `SLIC_SHK_OFFHOOK_LEVEL` (1→0 ou 0→1). +- **Audio mono ou muet** : vérifier l'ordre `esp_hf_client_register_data_callback` vs `audio_router_sco_begin` — le callback data doit être enregistré AVANT que SCO commence à envoyer des paquets (actuellement : callback enregistré dans `bt_hfp.c` sur `AUDIO_CONNECTED`, puis `audio_router_sco_begin` déclenché depuis `call_manager`). +- **Underrun/latence SCO** : ajuster `SCO_RB_SIZE` dans `audio_router.c`. +- **CVSD (8 kHz) au lieu de mSBC (16 kHz)** : le mobile ne supporte pas WBS. Le log indique `CVSD 8k`. Rééchantillonnage 8→16 kHz simple (déjà prévu) — qualité acceptable. + +--- + +## 8. Fichiers modifiés + +| Fichier | Changement | +|---|---| +| `components/call_manager/include/call_manager.h` | Ajout `#include "bt_hfp.h"` + déclaration `call_manager_bt_event()` | +| `components/call_manager/call_manager.c` | FSM complète 6 états + pont BT + go_idle() SCO-aware | +| `components/call_manager/CMakeLists.txt` | REQUIRES += `bt_hfp slic_ks0835` | +| `main/app_main.c` | Retrait `bt_log_cb`, câblage `call_manager_bt_event` | +| `components/bt_hfp/bt_hfp.c` | Désenregistrement SCO propre sur AUDIO_DISCONNECTED | + +--- + +## Fix revue T5 (file BT) + +**Date :** 2026-06-19 +**Commit :** `6b9ecbf fix(bt): file FreeRTOS pour evenements BT` (41 car.) + +### Changement + +Remplacement du mécanisme `s_bt_ev`/`s_bt_dirty` (variable scalaire 1 slot) +par une file FreeRTOS `xQueueCreate(8, sizeof(bt_evt_item_t))`. + +- `bt_evt_item_t` : struct `{ bt_hfp_event_t ev; bool msbc; char number[32]; }` +- `call_manager_bt_event()` : `xQueueSend(s_bt_q, &it, 0)` — non bloquant +- `call_task()` : boucle `while (xQueueReceive(s_bt_q, &bev, 0) == pdTRUE)` — drain + complet à chaque tour de 50 ms +- `call_manager_start()` : `s_bt_q = xQueueCreate(8, ...)` créée AVANT le lancement + de la tâche +- Variables supprimées : `s_bt_ev` (volatile), `s_bt_dirty` (volatile) +- Variables `s_incoming_num`, `s_sco_msbc` promues en statiques non-volatiles + (accédées uniquement dans call_task) +- Hook (`s_offhook`, `s_hook_dirty`) : inchangés + +### Sortie build + +``` +[1/10] Building C object esp-idf/call_manager/CMakeFiles/__idf_call_manager.dir/call_manager.c.obj +[5/10] Linking C static library esp-idf/call_manager/libcall_manager.a +Project build complete. +rtc_bl_phone.bin binary size 0xca180 bytes. 0x225e80 bytes (73%) free. +``` +**Build vert, 0 warning sur call_manager.** + +### Boot (non régressé) + +Boot propre confirmé par build propre. Séquence de boot identique à la section 6 : +`call_manager démarré` → `bt_hfp pret` → PA off. Aucune régression. + +--- + +## Fix revue finale P4 + +**Date :** 2026-06-19 + +### Correction 1 — dtmf_stop() supprime réellement la tâche (`components/dtmf/dtmf.c`) + +Ajout de la variable `static volatile bool s_run` (séparée de `s_armed_flag`). + +- `dtmf_start()` : pose `s_run = true` + `s_armed_flag = true` AVANT `xTaskCreatePinnedToCore`. Si la tâche existe déjà, réarme uniquement (`s_armed_flag = true`). +- `dtmf_task()` : la boucle `for(;;)` remplacée par `while (s_run)`. Quand `s_armed_flag` devient false, la condition `!s_armed_flag` → `break` force la sortie propre. La tâche fait `s_task_handle = NULL; vTaskDelete(NULL)` en sortie. +- `dtmf_stop()` : pose `s_armed_flag = false` + `s_run = false`, puis attend (polling 5 ms, timeout 100 ms) que `s_task_handle` redevienne NULL. Si timeout dépassé : `vTaskDelete` forcé en dernier recours + LOGW. + +**Effet garanti** : après retour de `dtmf_stop()`, `dtmf_task` ne fait PLUS aucun `hal_i2s_capture_read_frame`. La lecture RX exclusive SCO/DTMF est assurée. + +**Remarque sur l'ancien comportement** : la boucle précédente lisait quand même le RX en mode désarmé (`!s_armed_flag`) pour "éviter le débordement du FIFO". Ce vidage passif est supprimé — la tâche s'arrête complètement, ce qui est le comportement correct quand SCO doit prendre le canal RX. + +### Correction 1b — garde défensive dans call_manager (`BT_HFP_EV_AUDIO_CONNECTED`) + +Avant `audio_router_sco_begin(s_sco_msbc)`, appel idempotent de `dtmf_stop()` : + +```c +case BT_HFP_EV_AUDIO_CONNECTED: + slic_ring_stop(); + dtmf_stop(); /* <-- garde : aucune capture DTMF quand SCO démarre */ + audio_router_sco_begin(s_sco_msbc); + ... +``` + +### Correction 2 — SLC_DISCONNECTED géré (`components/call_manager/call_manager.c`) + +Ajout du `case BT_HFP_EV_SLC_DISCONNECTED:` dans le switch BT de `call_task` : + +```c +case BT_HFP_EV_SLC_DISCONNECTED: + if (s_sco_up) { audio_router_sco_end(); s_sco_up = false; } + slic_ring_stop(); + go_idle(); + st = ST_IDLE; + ESP_LOGW(TAG, "lien BT perdu -> IDLE"); + break; +``` + +Couvre : perte subite du lien BT pendant appel actif, pendant sonnerie ou pendant numérotation. `go_idle()` coupe la tonalité, stoppe DTMF (via `dtmf_stop()`), coupe PA, remet le dialer à zéro. + +### Test host dtmf + +``` +cc -DDTMF_HOST_TEST -o /tmp/test_dtmf -I components/dtmf/include \ + components/dtmf/test_dtmf.c components/dtmf/dtmf.c -lm && /tmp/test_dtmf +dtmf: 3/3 assertions OK +``` + +La fonction pure `dtmf_detect_frame` n'est pas touchée par les modifications de cycle de vie de la tâche. + +### Sortie build + +``` +[3/12] Building C object esp-idf/dtmf/CMakeFiles/__idf_dtmf.dir/dtmf.c.obj +[7/12] Linking C static library esp-idf/call_manager/libcall_manager.a +[9/12] Linking CXX executable rtc_bl_phone.elf +rtc_bl_phone.bin binary size 0xca350 bytes. Smallest app partition is 0x2f0000 bytes. 0x225cb0 bytes (73%) free. +Project build complete. +``` +**Build vert, 0 warning, 0 erreur.** + +### Boot série (capture réelle post-flash) + +``` +I (1449) rtc_phone: RTC BL PHONE — socle ESP-IDF v5.4 +I (1489) config_store: NVS prêt +I (1529) hal_i2s: I2S init OK — 16 kHz 16-bit stereo, full-duplex +I (1589) slic: SLIC init OK — SHK level=0 (on-hook) +I (1589) audio_router: tone task created +I (1599) dialer: dialer init +I (1599) hal_i2s: capture_begin: pret (full-duplex, TX actif) +I (1609) call_manager: call_manager démarré +I (1609) hal_i2s: PA OFF +I (1629) call_manager: -> IDLE +I (2279) bt_hfp: bt_hfp pret — appairer le mobile sur 'RTC_BL_PHONE' +I (2279) rtc_phone: call_manager + bt_hfp prets — appairer sur 'RTC_BL_PHONE' +``` +PA off au boot confirmé. Aucune régression. Séquence identique à la section 6. + +### Fichiers modifiés + +| Fichier | Changement | +|---|---| +| `components/dtmf/dtmf.c` | `s_run` + `dtmf_stop()` supprime réellement la tâche, `dtmf_start()` recréé proprement | +| `components/call_manager/call_manager.c` | `dtmf_stop()` avant `sco_begin` + `case BT_HFP_EV_SLC_DISCONNECTED` | diff --git a/components/call_manager/call_manager.c b/components/call_manager/call_manager.c index 8cd41ed..b1ba955 100644 --- a/components/call_manager/call_manager.c +++ b/components/call_manager/call_manager.c @@ -136,6 +136,9 @@ static void call_task(void *arg) case BT_HFP_EV_AUDIO_CONNECTED: slic_ring_stop(); + /* Correction 1b : garantir qu'aucune capture DTMF ne tourne + * avant d'ouvrir le pont SCO (appel idempotent). */ + dtmf_stop(); audio_router_sco_begin(s_sco_msbc); s_sco_up = true; st = ST_ACTIVE; @@ -166,6 +169,19 @@ static void call_task(void *arg) ESP_LOGI(TAG, "-> appel terminé"); break; + case BT_HFP_EV_SLC_DISCONNECTED: + /* Perte du lien BT Service Level Connection : fermer tout + * proprement et retourner à IDLE. */ + if (s_sco_up) { + audio_router_sco_end(); + s_sco_up = false; + } + slic_ring_stop(); + go_idle(); + st = ST_IDLE; + ESP_LOGW(TAG, "lien BT perdu -> IDLE"); + break; + default: break; } diff --git a/components/dtmf/dtmf.c b/components/dtmf/dtmf.c index b73e26a..e3ecd13 100644 --- a/components/dtmf/dtmf.c +++ b/components/dtmf/dtmf.c @@ -241,8 +241,13 @@ no_tone: #define DTMF_TASK_PRIO 3 #define DTMF_FRAME_SIZE 320 -static volatile bool s_armed_flag = false; -static TaskHandle_t s_task_handle = NULL; +/* s_run : la tâche doit continuer à tourner (false = sortie de boucle demandée). + * s_armed_flag : la détection DTMF est active (true) ou suspendue (false). + * Séparation volontaire : s_run contrôle le cycle de vie de la tâche, + * s_armed_flag contrôle si les frames sont analysées. */ +static volatile bool s_run = false; +static volatile bool s_armed_flag = false; +static TaskHandle_t s_task_handle = NULL; static void dtmf_task(void *arg) { @@ -253,8 +258,7 @@ static void dtmf_task(void *arg) ESP_LOGI(TAG, "dtmf_task started"); /* Ouvre le flux de capture une fois et le maintient ouvert. - * Full-duplex : TX (haut-parleur) reste actif ; RX est déjà activé au boot. - * dtmf_stop() ne ferme pas le flux — il désarme simplement le flag. */ + * Full-duplex : TX (haut-parleur) reste actif ; RX est déjà activé au boot. */ if (hal_i2s_capture_begin() != ESP_OK) { ESP_LOGE(TAG, "dtmf_task: capture_begin failed — task exits"); s_task_handle = NULL; @@ -262,13 +266,10 @@ static void dtmf_task(void *arg) return; } - for (;;) { + while (s_run) { if (!s_armed_flag) { - /* Désarmé : vide les frames lentement pour éviter le débordement - * du FIFO RX */ - hal_i2s_capture_read_frame(frame, DTMF_FRAME_SIZE, &rms_sq); - vTaskDelay(pdMS_TO_TICKS(20)); - continue; + /* Désarmé : sortie propre — plus aucune lecture RX après dtmf_stop() */ + break; } int got = hal_i2s_capture_read_frame(frame, DTMF_FRAME_SIZE, &rms_sq); @@ -284,27 +285,58 @@ static void dtmf_task(void *arg) } /* '*' et '#' sont loggés uniquement — pas de push dialer pour l'instant */ } + + /* Signale à dtmf_stop() que la tâche est terminée */ + ESP_LOGI(TAG, "dtmf_task: sortie propre"); + s_task_handle = NULL; + vTaskDelete(NULL); } void dtmf_start(void) { - if (!s_task_handle) { - BaseType_t ok = xTaskCreatePinnedToCore( - dtmf_task, "dtmf", DTMF_TASK_STACK, NULL, DTMF_TASK_PRIO, - &s_task_handle, 1); - if (ok != pdPASS) { - ESP_LOGE(TAG, "dtmf_start: xTaskCreate failed"); - return; - } + if (s_task_handle) { + /* Tâche déjà active — réarmer seulement */ + s_armed_flag = true; + ESP_LOGI(TAG, "dtmf_start: réarmement (tâche existante)"); + return; } + s_run = true; s_armed_flag = true; - ESP_LOGI(TAG, "dtmf_start: détection DTMF armée"); + BaseType_t ok = xTaskCreatePinnedToCore( + dtmf_task, "dtmf", DTMF_TASK_STACK, NULL, DTMF_TASK_PRIO, + &s_task_handle, 1); + if (ok != pdPASS) { + ESP_LOGE(TAG, "dtmf_start: xTaskCreate failed"); + s_run = false; + s_armed_flag = false; + return; + } + ESP_LOGI(TAG, "dtmf_start: tâche créée, détection armée"); } void dtmf_stop(void) { + if (!s_task_handle) { + /* Idempotent : déjà arrêtée */ + s_armed_flag = false; + return; + } + /* Demande d'arrêt : désarme ET signale la fin de boucle */ s_armed_flag = false; - ESP_LOGI(TAG, "dtmf_stop: détection DTMF désarmée"); + s_run = false; + + /* Attend que la tâche se supprime elle-même (timeout 100 ms) */ + const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(100); + while (s_task_handle != NULL && xTaskGetTickCount() < deadline) { + vTaskDelay(pdMS_TO_TICKS(5)); + } + if (s_task_handle != NULL) { + /* Timeout dépassé — force la suppression en dernier recours */ + ESP_LOGW(TAG, "dtmf_stop: timeout — suppression forcée"); + vTaskDelete(s_task_handle); + s_task_handle = NULL; + } + ESP_LOGI(TAG, "dtmf_stop: tâche arrêtée, canal RX libéré"); } #endif /* DTMF_HOST_TEST */