feat(bt): call_manager HFP appels entrant/sortant

- FSM étendue : ST_OUTGOING, ST_INCOMING, ST_ACTIVE
- call_manager_bt_event() : pont léger BT→FSM (flags + strncpy)
- Transitions hook : answer/hangup/dial HFP câblés
- go_idle() coupe SCO si actif
- bt_hfp.c : désenregistrement SCO sur AUDIO_DISCONNECTED
- CMake REQUIRES += bt_hfp slic_ks0835
- app_main : call_manager_start() avant bt_hfp_init()
This commit is contained in:
clement
2026-06-19 13:32:45 +02:00
parent 0ff2c5d4f1
commit 24e82fbf3b
5 changed files with 199 additions and 52 deletions
+3
View File
@@ -149,6 +149,9 @@ static void hf_cb(esp_hf_client_cb_event_t event, esp_hf_client_cb_param_t *para
esp_hf_client_register_data_callback(sco_incoming_cb, sco_outgoing_cb);
} else if (param->audio_stat.state == ESP_HF_CLIENT_AUDIO_STATE_DISCONNECTED) {
ESP_LOGI(TAG, "audio SCO deconnecte");
/* Désenregistrement propre des callbacks data SCO avant d'émettre
* l'événement — évite des appels fantômes après fin de SCO. */
esp_hf_client_register_data_callback(NULL, NULL);
emit(BT_HFP_EV_AUDIO_DISCONNECTED, NULL, false);
}
break;
+1 -1
View File
@@ -1,5 +1,5 @@
idf_component_register(
SRCS "call_manager.c"
INCLUDE_DIRS "include"
REQUIRES hook_monitor dialer dtmf audio_router hal_i2s
REQUIRES hook_monitor dialer dtmf audio_router hal_i2s bt_hfp slic_ks0835
)
+168 -35
View File
@@ -1,10 +1,18 @@
/*
* call_manager.c — machine d'état d'appel LOCALE (sans Bluetooth).
* Adapté de plip_voice/main/conversation.c, dépouillé du routing/greet/scene.
* call_manager.c — machine d'état d'appel avec pont HFP Bluetooth.
*
* IDLE : raccroché — silence, PA coupée
* DIALTONE : décroché — tonalité de numérotation, attente du 1er chiffre
* DIALING : chiffres en cours — attente de 3 s de silence -> numéro figé/logué
* États :
* IDLE raccroché — silence, PA coupée
* DIALTONE décroché au repos — tonalité + attente premier chiffre
* DIALING chiffres en cours — 3 s silence → bt_hfp_dial() → OUTGOING
* OUTGOING appel sortant émis — attente AUDIO_CONNECTED → ACTIVE
* INCOMING appel entrant BT — cloche sonne + attente décroché
* ACTIVE audio SCO bidirectionnel — pont I2S↔BT
*
* Thread-safety :
* call_manager_bt_event() est appelé depuis la tâche BT (contexte temps-réel).
* Elle pose uniquement des flags volatils et copie le numéro via strncpy.
* La boucle call_task() consomme ces flags dans son propre contexte.
*/
#include "call_manager.h"
#include "hook_monitor.h"
@@ -12,71 +20,193 @@
#include "dtmf.h"
#include "audio_router.h"
#include "hal_i2s.h"
#include "slic.h"
#include "bt_hfp.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <stdbool.h>
#include <string.h>
#define TAG "call_manager"
#define DIAL_COMPLETE_SILENCE_MS 3000
typedef enum { ST_IDLE, ST_DIALTONE, ST_DIALING } call_state_t;
/* ---- FSM ---- */
typedef enum {
ST_IDLE,
ST_DIALTONE,
ST_DIALING,
ST_OUTGOING,
ST_INCOMING,
ST_ACTIVE
} call_state_t;
/* ---- État hook (mis à jour depuis le callback hook_monitor, contexte quelconque) ---- */
static volatile bool s_offhook = false;
static volatile bool s_hook_dirty = false;
/* ---- Pont BT : flags posés par call_manager_bt_event() (contexte tâche BT) ---- */
static volatile bool s_bt_dirty = false;
static volatile bt_hfp_event_t s_bt_ev = BT_HFP_EV_SLC_DISCONNECTED;
static char s_incoming_num[32]; /* copié avec strncpy — jamais stocker le ptr */
static volatile bool s_sco_msbc = false;
/* ---- État SCO (mis à jour uniquement dans call_task) ---- */
static bool s_sco_up = false;
/* ------------------------------------------------------------------ */
/* Callback appelé depuis la tâche BT — DOIT rester léger */
/* ------------------------------------------------------------------ */
void call_manager_bt_event(bt_hfp_event_t ev, const bt_hfp_event_data_t *d)
{
s_bt_ev = ev;
if (ev == BT_HFP_EV_INCOMING && d && d->number) {
strncpy(s_incoming_num, d->number, sizeof(s_incoming_num) - 1);
s_incoming_num[sizeof(s_incoming_num) - 1] = '\0';
}
if (ev == BT_HFP_EV_AUDIO_CONNECTED && d) {
s_sco_msbc = d->msbc;
}
s_bt_dirty = true;
}
/* ------------------------------------------------------------------ */
/* go_idle : retour à l'état de repos (coupe tout) */
/* ------------------------------------------------------------------ */
static void go_idle(void)
{
audio_router_tones_stop();
if (s_sco_up) {
audio_router_sco_end();
s_sco_up = false;
}
dtmf_stop();
slic_ring_stop();
hal_audio_pa_set(false);
dialer_reset();
ESP_LOGI(TAG, "-> IDLE");
}
/* ------------------------------------------------------------------ */
/* callback changement d'état du crochet */
/* ------------------------------------------------------------------ */
static void on_hook_change(bool offhook)
{
s_offhook = offhook;
s_hook_dirty = true;
}
static void go_idle(void)
{
audio_router_tones_stop();
dtmf_stop();
hal_audio_pa_set(false);
dialer_reset();
ESP_LOGI(TAG, "-> IDLE (raccroché)");
}
/* ------------------------------------------------------------------ */
/* Tâche principale de la FSM d'appel */
/* ------------------------------------------------------------------ */
static void call_task(void *arg)
{
(void)arg;
call_state_t st = ST_IDLE;
for (;;) {
if (s_hook_dirty) {
s_hook_dirty = false;
if (!s_offhook) {
go_idle();
st = ST_IDLE;
} else if (st == ST_IDLE) {
/* décroché depuis le repos : tonalité + armement numérotation */
dialer_reset();
audio_router_dialtone_start(); /* active la PA en interne */
dtmf_start(); /* arme la capture DTMF */
ESP_LOGI(TAG, "-> DIALTONE (décroché)");
st = ST_DIALTONE;
/* --- 1. Événements BT HFP ---- */
if (s_bt_dirty) {
s_bt_dirty = false;
switch (s_bt_ev) {
case BT_HFP_EV_INCOMING:
if (st == ST_IDLE || st == ST_DIALTONE) {
audio_router_tones_stop();
slic_ring_start(); /* fait sonner la cloche */
st = ST_INCOMING;
ESP_LOGI(TAG, "-> INCOMING %s", s_incoming_num);
}
break;
case BT_HFP_EV_AUDIO_CONNECTED:
slic_ring_stop();
audio_router_sco_begin(s_sco_msbc);
s_sco_up = true;
st = ST_ACTIVE;
ESP_LOGI(TAG, "-> ACTIVE (audio SCO %s)",
s_sco_msbc ? "mSBC 16k" : "CVSD 8k");
break;
case BT_HFP_EV_AUDIO_DISCONNECTED:
if (s_sco_up) {
audio_router_sco_end();
s_sco_up = false;
}
break;
case BT_HFP_EV_CALL_ENDED:
slic_ring_stop();
if (s_sco_up) {
audio_router_sco_end();
s_sco_up = false;
}
/* Si encore décroché : tonalité d'occupation */
if (hook_monitor_offhook()) {
audio_router_busy_start();
st = ST_DIALTONE; /* attente du raccroché */
} else {
st = ST_IDLE;
}
ESP_LOGI(TAG, "-> appel terminé");
break;
default:
break;
}
}
/* --- 2. Événements hook (crochet) ---- */
if (s_hook_dirty) {
s_hook_dirty = false;
if (!s_offhook) {
/* Raccroché : terminer l'appel BT si actif */
if (st == ST_ACTIVE || st == ST_OUTGOING || st == ST_INCOMING) {
bt_hfp_hangup();
}
go_idle();
st = ST_IDLE;
} else {
/* Décroché */
if (st == ST_INCOMING) {
/* Décroché pendant sonnerie : répondre */
slic_ring_stop();
bt_hfp_answer();
/* -> ACTIVE sera déclenché par BT_HFP_EV_AUDIO_CONNECTED */
ESP_LOGI(TAG, "bt_hfp_answer() envoyé");
} else if (st == ST_IDLE) {
/* Décroché au repos : tonalité + armement DTMF */
dialer_reset();
audio_router_dialtone_start();
dtmf_start();
ESP_LOGI(TAG, "-> DIALTONE (décroché)");
st = ST_DIALTONE;
}
/* ST_ACTIVE, ST_OUTGOING : décroché redondant, ignorer */
}
}
/* --- 3. Progression de la numérotation ---- */
if (st == ST_DIALTONE && !dialer_idle()) {
/* premier chiffre composé : couper la tonalité, passer en DIALING */
/* Premier chiffre : couper la tonalité, passer en DIALING */
audio_router_tones_stop();
hal_audio_pa_set(false); /* silence pendant la numerotation (DTMF = micro) */
ESP_LOGI(TAG, "-> DIALING, numéro: \"%s\"", dialer_current());
hal_audio_pa_set(false);
ESP_LOGI(TAG, "-> DIALING, numéro en cours: \"%s\"", dialer_current());
st = ST_DIALING;
} else if (st == ST_DIALING) {
if (!dialer_idle() && dialer_ms_since_last() > DIAL_COMPLETE_SILENCE_MS) {
ESP_LOGI(TAG, "NUMÉRO COMPOSÉ: \"%s\" (impulsion + DTMF)",
dialer_current());
/* Phase 4 : ici on lancera l'appel HFP. Pour l'instant on fige. */
/* 3 s de silence : numéro complet → appel BT */
ESP_LOGI(TAG, "appel sortant: \"%s\"", dialer_current());
bt_hfp_dial(dialer_current());
dtmf_stop();
st = ST_IDLE; /* on attend le raccroché pour revenir proprement */
st = ST_OUTGOING;
/* -> ACTIVE sera déclenché par BT_HFP_EV_AUDIO_CONNECTED */
}
}
@@ -84,6 +214,9 @@ static void call_task(void *arg)
}
}
/* ------------------------------------------------------------------ */
/* API publique */
/* ------------------------------------------------------------------ */
void call_manager_start(void)
{
ESP_ERROR_CHECK(hal_i2s_capture_begin());
@@ -92,5 +225,5 @@ void call_manager_start(void)
return;
}
xTaskCreatePinnedToCore(call_task, "call_mgr", 4096, NULL, 5, NULL, 1);
ESP_LOGI(TAG, "call_manager démarré (mode local, sans BT)");
ESP_LOGI(TAG, "call_manager démarré");
}
+21 -5
View File
@@ -1,16 +1,32 @@
#pragma once
#include "bt_hfp.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Start the local call state machine (no Bluetooth yet):
* on-hook -> idle (silence, PA off)
* off-hook -> dial tone + collect digits (pulse + DTMF)
* number complete (3 s silence) -> log number
* Call once after audio + slic + dialer + dtmf + hook_monitor are ready. */
/**
* Démarre la machine d'état d'appel (hook_monitor + call_task).
* Doit être appelé AVANT bt_hfp_init() pour que le callback soit prêt.
*
* États FSM :
* IDLE raccroché — silence, PA coupée
* DIALTONE décroché au repos — tonalité + attente premier chiffre
* DIALING chiffres en cours — 3 s silence → bt_hfp_dial() → OUTGOING
* OUTGOING appel sortant BT — attente AUDIO_CONNECTED → ACTIVE
* INCOMING appel entrant BT — cloche + attente décroché → bt_hfp_answer()
* ACTIVE audio SCO actif — pont bidirectionnel I2S↔BT
*/
void call_manager_start(void);
/**
* Callback BT HFP à passer à bt_hfp_init().
* Appelé depuis la tâche BT temps-réel → LÉGER : flags + strncpy uniquement.
* Ne jamais stocker le pointeur d->number.
*/
void call_manager_bt_event(bt_hfp_event_t ev, const bt_hfp_event_data_t *d);
#ifdef __cplusplus
}
#endif
+6 -11
View File
@@ -11,14 +11,6 @@
static const char *TAG = "rtc_phone";
/* Handler de log temporaire pour les evenements BT HFP (Task 2).
* Remplace par call_manager en Task 5. */
static void bt_log_cb(bt_hfp_event_t ev, const bt_hfp_event_data_t *d)
{
ESP_LOGI(TAG, "BT event %d (number=%s msbc=%d)", (int)ev,
d->number ? d->number : "-", (int)d->msbc);
}
void app_main(void)
{
ESP_LOGI(TAG, "RTC BL PHONE — socle ESP-IDF v5.4");
@@ -30,8 +22,11 @@ void app_main(void)
* call_manager déclenchera audio_router_dialtone_start() au décroché. */
audio_router_init();
dialer_init();
/* call_manager_start() AVANT bt_hfp_init() : le callback doit être
* enregistré et prêt à recevoir des événements BT dès l'init. */
call_manager_start();
/* Pile BT HFP-HF — NVS déjà initialisé par config_store_init(). */
ESP_ERROR_CHECK(bt_hfp_init(bt_log_cb));
ESP_LOGI(TAG, "telephonie locale + BT HFP prets");
/* Pile BT HFP-HF — NVS déjà initialisé par config_store_init().
* call_manager_bt_event remplace le handler de log temporaire (Task 2). */
ESP_ERROR_CHECK(bt_hfp_init(call_manager_bt_event));
ESP_LOGI(TAG, "call_manager + bt_hfp prets — appairer sur 'RTC_BL_PHONE'");
}