fix(bt): RX exclusif dtmf/SCO + perte lien BT

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é.
This commit is contained in:
clement
2026-06-19 13:52:06 +02:00
parent 6b9ecbf2e4
commit 9cb31bdea9
3 changed files with 362 additions and 20 deletions
+294
View File
@@ -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: <nom_mobile>`
- 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 <numéro_appelant>` + 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` |
+16
View File
@@ -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;
}
+52 -20
View File
@@ -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 */