feat(tel): call_manager local + PA single-owner

- Crée composant call_manager: FSM IDLE/DIALTONE/DIALING
- PA single-owner: es8388_init la coupe (niveau 0) au démarrage
- dtmf: #include dialer.h, volatile task_handle, got<0 vs got==0
- hook_monitor: guard double-start + stack 4096
- app_main: retire sondes + tonalité boot, câble call_manager_start
This commit is contained in:
clement
2026-06-19 11:00:19 +02:00
parent aec7599a22
commit 21ca98f869
9 changed files with 140 additions and 40 deletions
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "call_manager.c"
INCLUDE_DIRS "include"
REQUIRES hook_monitor dialer dtmf audio_router hal_i2s
)
+95
View File
@@ -0,0 +1,95 @@
/*
* call_manager.c — machine d'état d'appel LOCALE (sans Bluetooth).
* Adapté de plip_voice/main/conversation.c, dépouillé du routing/greet/scene.
*
* 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é
*/
#include "call_manager.h"
#include "hook_monitor.h"
#include "dialer.h"
#include "dtmf.h"
#include "audio_router.h"
#include "hal_i2s.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <stdbool.h>
#define TAG "call_manager"
#define DIAL_COMPLETE_SILENCE_MS 3000
typedef enum { ST_IDLE, ST_DIALTONE, ST_DIALING } call_state_t;
static volatile bool s_offhook = false;
static volatile bool s_hook_dirty = false;
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é)");
}
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;
}
}
if (st == ST_DIALTONE && !dialer_idle()) {
/* premier chiffre composé : couper la tonalité, passer en DIALING */
audio_router_tones_stop();
ESP_LOGI(TAG, "-> DIALING, numéro: \"%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. */
dtmf_stop();
st = ST_IDLE; /* on attend le raccroché pour revenir proprement */
}
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}
void call_manager_start(void)
{
ESP_ERROR_CHECK(hal_i2s_capture_begin());
if (hook_monitor_start(on_hook_change) != ESP_OK) {
ESP_LOGE(TAG, "hook_monitor_start a échoué");
return;
}
xTaskCreatePinnedToCore(call_task, "call_mgr", 4096, NULL, 5, NULL, 1);
ESP_LOGI(TAG, "call_manager démarré (mode local, sans BT)");
}
@@ -0,0 +1,16 @@
#pragma once
#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. */
void call_manager_start(void);
#ifdef __cplusplus
}
#endif
+1 -3
View File
@@ -1,7 +1,5 @@
idf_component_register(
SRCS "dtmf.c"
INCLUDE_DIRS "include"
REQUIRES hal_i2s
# NOTE Task 6 : ajouter "dialer" aux REQUIRES quand le composant existera.
# dialer_push_digit est appelé dans dtmf_task qui n'est pas encore activée.
REQUIRES hal_i2s dialer
)
+5 -11
View File
@@ -52,15 +52,8 @@
#include "esp_log.h"
#define TAG "dtmf"
/* NOTE Task 4 : dialer.h sera disponible quand le composant dialer existera.
* Pour l'instant, déclaration forward pour éviter une dépendance de build
* avant Task 4. L'include réel sera rétabli en Task 6 avec REQUIRES dialer. */
#ifdef CONFIG_RTC_DTMF_DIALER_READY
/* Task 6 : dialer est disponible — on inclut l'en-tête réel. */
#include "dialer.h"
#else
/* Stub forward — dialer_push_digit sera lié quand dialer existera (Task 4/6). */
void dialer_push_digit(int digit);
#endif
#endif /* DTMF_HOST_TEST */
@@ -248,8 +241,8 @@ 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;
static volatile bool s_armed_flag = false;
static volatile TaskHandle_t s_task_handle = NULL;
static void dtmf_task(void *arg)
{
@@ -279,7 +272,8 @@ static void dtmf_task(void *arg)
}
int got = hal_i2s_capture_read_frame(frame, DTMF_FRAME_SIZE, &rms_sq);
if (got <= 0) continue;
if (got < 0) { vTaskDelay(pdMS_TO_TICKS(20)); continue; }
if (got == 0) continue;
char sym = dtmf_detect_frame(frame, got);
if (sym == '\0') continue;
+2 -2
View File
@@ -235,9 +235,9 @@ esp_err_t es8388_init(void)
if (i2c_write_reg(ES8388_ADC_POWER, 0x00) != ESP_OK) return ESP_FAIL;
vTaskDelay(pdMS_TO_TICKS(10)); /* let ADC analog settle after full power-up */
/* 15. Enable PA (power amplifier for speaker). */
/* 15. Configure PA GPIO en sortie, coupé au repos (propriétaire unique : hal_audio_pa_set). */
gpio_set_direction(PLIP_PA_ENABLE, GPIO_MODE_OUTPUT);
gpio_set_level(PLIP_PA_ENABLE, 1);
gpio_set_level(PLIP_PA_ENABLE, 0); /* PA pilotée par call_manager (off au repos) */
/* Verify key register values to confirm ADC path is configured correctly. */
uint8_t ctl1=0, adcpwr=0, adcinsel=0, dacctl21=0, chippower=0, adcctl3=0;
+5 -2
View File
@@ -25,7 +25,8 @@
#define HANGUP_THRESHOLD_MS 2500 /* ouverture > seuil = vrai raccroché */
static hook_change_cb_t s_cb = NULL;
static volatile bool s_offhook = false;
static volatile bool s_offhook = false;
static TaskHandle_t s_task = NULL;
/* slic_is_offhook() renvoie true si la boucle est fermée (combiné décroché,
* cadran au repos). Pendant une impulsion, la boucle s'ouvre brièvement →
@@ -98,9 +99,11 @@ static void hook_task(void *arg)
esp_err_t hook_monitor_start(hook_change_cb_t cb)
{
if (s_task != NULL) return ESP_ERR_INVALID_STATE;
s_cb = cb;
if (xTaskCreatePinnedToCore(hook_task, "hook", 3072, NULL, 6, NULL, 1) != pdPASS) {
if (xTaskCreatePinnedToCore(hook_task, "hook", 4096, NULL, 6, &s_task, 1) != pdPASS) {
ESP_LOGE(TAG, "échec création tâche hook");
s_task = NULL;
return ESP_FAIL;
}
return ESP_OK;
+1 -1
View File
@@ -1,5 +1,5 @@
idf_component_register(
SRCS "app_main.c"
INCLUDE_DIRS "."
REQUIRES nvs_flash config_store hal_es8388 hal_i2s audio_router slic_ks0835 dialer
REQUIRES nvs_flash config_store hal_es8388 hal_i2s audio_router slic_ks0835 dtmf dialer hook_monitor call_manager
)
+10 -21
View File
@@ -5,33 +5,22 @@
#include "hal_i2s.h"
#include "audio_router.h"
#include "slic.h"
#include "dialer.h"
#include "call_manager.h"
static const char *TAG = "rtc_phone";
void app_main(void)
{
ESP_LOGI(TAG, "RTC BL PHONE — socle ESP-IDF v5.4 (esp32 classique)");
ESP_LOGI(TAG, "RTC BL PHONE — socle ESP-IDF v5.4");
ESP_ERROR_CHECK(config_store_init());
ESP_ERROR_CHECK(hal_i2s_init());
/* SLIC init AVANT le dial tone pour eviter la contention GPIO/mutex
* avec la tache tone (i2s_channel_write qui tourne en parallele). */
ESP_ERROR_CHECK(slic_init());
/* audio_router_init crée la tâche de tonalités SANS la démarrer.
* call_manager déclenchera audio_router_dialtone_start() au décroché. */
audio_router_init();
/* Sonde RMS micro (valide chemin LIN2/RIN2) — a retirer en Task 6. */
ESP_ERROR_CHECK(hal_i2s_capture_begin());
int16_t cap[320];
for (int i = 0; i < 20; i++) {
int64_t rms_sq = 0;
int got = hal_i2s_capture_read_frame(cap, 320, &rms_sq);
ESP_LOGI(TAG, "MIC sonde: offhook=%d frame=%d rms_sq=%lld",
(int)slic_is_offhook(), got, (long long)rms_sq);
vTaskDelay(pdMS_TO_TICKS(500));
}
audio_router_dialtone_start();
ESP_LOGI(TAG, "tonalite de numerotation demarree (combine decroche pour ecouter)");
ESP_LOGI(TAG, "boot OK — Phase 3 Task 2 capture micro hal_i2s");
while (true) {
ESP_LOGI(TAG, "heartbeat");
vTaskDelay(pdMS_TO_TICKS(5000));
}
/* SLIC init AVANT le démarrage des tâches audio lourdes (ordre anti-deadlock GPIO). */
ESP_ERROR_CHECK(slic_init());
dialer_init();
call_manager_start();
ESP_LOGI(TAG, "téléphonie locale prête — décroche pour composer");
}