docs(plan): plan Phase 3 telephonie locale
This commit is contained in:
@@ -0,0 +1,849 @@
|
||||
# Phase 3 — Téléphonie locale (SLIC + DTMF + numérotation) : Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Au décroché du combiné, jouer la tonalité de numérotation ; capturer le numéro composé au cadran à impulsions ET au clavier DTMF ; le journaliser ; au raccroché, couper le son et l'ampli. Sans Bluetooth (Phase 4).
|
||||
|
||||
**Architecture :** On adapte les modules natifs éprouvés de `plip_voice` (`/Users/electron/code/zacus/ESP32_ZACUS/plip_voice/main`) en composants ESP-IDF : `slic_ks0835` (GPIO hook/sonnerie), capture micro ajoutée à `hal_i2s` (lecture RX), `dtmf` (Goertzel, testé host), `dialer` (accumulateur de chiffres, testé host), `hook_monitor` (surveillance décroché + impulsions cadran), `call_manager` (machine d'état IDLE→DIALTONE→DIALING). On intègre aussi deux nettoyages reportés de la Phase 2 : PA à propriétaire unique et tâche de capture micro.
|
||||
|
||||
**Tech Stack :** ESP-IDF v5.4, `driver/gpio.h`, `driver/i2s_std.h`, `esp_timer.h`, FreeRTOS, codec ES8388 / SLIC K50835F, carte AI-Thinker ESP32-A1S.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- ESP-IDF v5.4 — `. /Users/electron/esp/esp-idf/export.sh` dans CHAQUE shell. Si `idf.py` échoue sur l'interpréteur, utiliser `python3` (venv py3.14 sur ce poste).
|
||||
- Cible `esp32`. Port `/dev/cu.usbserial-0001`. Matériel branché. Monitor lu en non-bloquant (pyserial ~15 s ; pas de `timeout` sur ce macOS).
|
||||
- API drivers neuves uniquement (`driver/gpio.h`, `driver/i2s_std.h`). Logs `ESP_LOGx`.
|
||||
- Source d'adaptation : `/Users/electron/code/zacus/ESP32_ZACUS/plip_voice/main/{slic.c,slic.h,dtmf.c,dtmf.h,dialer.c,dialer.h,phone.c}`. Code éprouvé banc — préserver la logique/valeurs, n'adapter que ce que le plan indique (suppression du couplage WiFi/conversation/scene/BT).
|
||||
- Pinout SLIC (depuis `components/bsp/include/board_config.h`, déjà en place) : RM=18, FR=5, SHK=23, PD=19. Cadence sonnerie France Télécom : burst 1500 ms / pause 3500 ms, FR togglé ~25 Hz (demi-période 20 ms).
|
||||
- **Polarité SHK ambiguë** : la table fournie dit actif-BAS, le code plip_voice utilise actif-HAUT. On démarre avec la valeur plip_voice (`SLIC_SHK_OFFHOOK_LEVEL = 1`, HIGH=décroché) et on tranche EMPIRIQUEMENT au test « décroché » (Task 1) ; si inversé, basculer le seul `#define` à `0`.
|
||||
- DTMF/audio : 16 kHz, frame 320 samples (20 ms), mono après downmix L+R. Goertzel sur basses {697,770,852,941} / hautes {1209,1336,1477,1633}.
|
||||
- Hook git : sujet ≤ 50 car., sans attribution IA, corps ≤ 72 car./ligne. `sdkconfig`/`build/` non versionnés.
|
||||
- Le firmware actuel joue la tonalité au boot (Phase 2) — `call_manager` (Task 6) remplacera ce comportement : tonalité au décroché, pas au boot.
|
||||
|
||||
---
|
||||
|
||||
### Task 1 : `slic_ks0835` (GPIO hook + sonnerie)
|
||||
|
||||
**Files:**
|
||||
- Create: `components/slic_ks0835/include/slic.h`
|
||||
- Create: `components/slic_ks0835/slic.c`
|
||||
- Create: `components/slic_ks0835/CMakeLists.txt`
|
||||
- Modify: `main/CMakeLists.txt` (REQUIRES += `slic_ks0835`)
|
||||
- Modify: `main/app_main.c` (appeler `slic_init()` + log temporaire de l'état hook pour valider la polarité)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes : `board_config.h` (bsp) — pins SLIC.
|
||||
- Produces :
|
||||
- `esp_err_t slic_init(void);` — config GPIO RM/FR/SHK/PD + crée la tâche de sonnerie.
|
||||
- `bool slic_is_offhook(void);` — true si décroché (selon `SLIC_SHK_OFFHOOK_LEVEL`).
|
||||
- `void slic_ring_start(void);` / `void slic_ring_stop(void);` / `bool slic_is_ringing(void);`
|
||||
|
||||
- [ ] **Step 1 : Copier `slic.h` et `slic.c` depuis plip_voice**
|
||||
|
||||
Copier `/Users/electron/code/zacus/ESP32_ZACUS/plip_voice/main/slic.h` → `components/slic_ks0835/include/slic.h` et `/Users/electron/code/zacus/ESP32_ZACUS/plip_voice/main/slic.c` → `components/slic_ks0835/slic.c` (verbatim). Ils n'ont pas de dépendance WiFi/conversation — adaptation minimale.
|
||||
|
||||
- [ ] **Step 2 : Vérifier l'include du pinout dans `slic.c`**
|
||||
|
||||
`slic.c` inclut `"board_config.h"`. Comme ce header vit maintenant dans le composant `bsp`, aucun changement de chemin n'est nécessaire (les composants exposent leurs `include/`), mais le CMake devra requérir `bsp`. Vérifier que `slic.c` contient bien `#define SLIC_SHK_OFFHOOK_LEVEL 1` (sinon l'ajouter en tête, avec le commentaire : `/* 1=HIGH off-hook (plip_voice proven). Mettre 0 si le test decroche est inverse. */`).
|
||||
|
||||
- [ ] **Step 3 : Créer `components/slic_ks0835/CMakeLists.txt`**
|
||||
|
||||
```cmake
|
||||
idf_component_register(
|
||||
SRCS "slic.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_gpio bsp
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Référencer dans `main/CMakeLists.txt`**
|
||||
|
||||
Ajouter `slic_ks0835` à la liste `REQUIRES` de `main` (conserver les composants existants : `nvs_flash config_store hal_es8388 hal_i2s audio_router`).
|
||||
|
||||
- [ ] **Step 5 : Câbler `slic_init` + sonde de polarité dans `main/app_main.c`**
|
||||
|
||||
Ajouter `#include "slic.h"`. Après `audio_router_dialtone_start();` (existant), ajouter :
|
||||
```c
|
||||
ESP_ERROR_CHECK(slic_init());
|
||||
/* Sonde temporaire de polarite hook (a retirer en Task 6). */
|
||||
for (int i = 0; i < 20; i++) {
|
||||
ESP_LOGI(TAG, "HOOK sonde: offhook=%d (niveau brut SHK)", (int)slic_is_offhook());
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6 : Builder, flasher, VALIDER LA POLARITÉ**
|
||||
|
||||
Run : `. /Users/electron/esp/esp-idf/export.sh && idf.py build && idf.py -p /dev/cu.usbserial-0001 flash`, puis lire ~12 s de série en **manipulant le combiné** (raccroché puis décroché).
|
||||
Expected : `slic init` OK, puis la sonde logue `offhook=0` quand le combiné est RACCROCHÉ et `offhook=1` quand il est DÉCROCHÉ.
|
||||
**Si c'est inversé** (offhook=1 au raccroché) : éditer `slic.c`, passer `#define SLIC_SHK_OFFHOOK_LEVEL 0`, rebuild+reflash, re-vérifier. Documenter la valeur retenue dans le rapport.
|
||||
|
||||
- [ ] **Step 7 : Commit**
|
||||
|
||||
```bash
|
||||
git add components/slic_ks0835 main/CMakeLists.txt main/app_main.c
|
||||
git commit -m "feat(tel): SLIC KS0835 hook + sonnerie"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2 : Capture micro (API RX dans `hal_i2s`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `components/hal_i2s/include/hal_i2s.h` (ajouter l'API capture)
|
||||
- Modify: `components/hal_i2s/hal_i2s.c` (implémenter la capture)
|
||||
- Modify: `main/app_main.c` (sonde temporaire RMS micro)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes : `hal_i2s_mic_handle()` (Phase 2).
|
||||
- Produces :
|
||||
- `esp_err_t hal_i2s_capture_begin(void);` — alloue le buffer scratch RX (PSRAM si dispo). Idempotent.
|
||||
- `int hal_i2s_capture_read_frame(int16_t *mono_out, int n_samples, int64_t *rms_sq_out);` — lit une frame 20 ms du micro, downmixe L+R→mono dans `mono_out` (n_samples, attendu 320), renseigne `*rms_sq_out` (énergie moyenne). Retourne le nb de samples (0 = timeout, -1 = erreur).
|
||||
- `void hal_i2s_capture_end(void);` — libère le scratch.
|
||||
|
||||
- [ ] **Step 1 : Ajouter l'API capture à `components/hal_i2s/include/hal_i2s.h`**
|
||||
|
||||
Avant le `#ifdef __cplusplus` de fermeture, ajouter :
|
||||
```c
|
||||
/* Microphone capture (RX). capture_begin allocates a scratch buffer; read_frame
|
||||
* reads one 20 ms frame, downmixes L+R to mono, and reports mean energy. */
|
||||
esp_err_t hal_i2s_capture_begin(void);
|
||||
int hal_i2s_capture_read_frame(int16_t *mono_out, int n_samples, int64_t *rms_sq_out);
|
||||
void hal_i2s_capture_end(void);
|
||||
```
|
||||
|
||||
- [ ] **Step 2 : Implémenter dans `components/hal_i2s/hal_i2s.c`**
|
||||
|
||||
Ajouter les includes en tête (après les existants) :
|
||||
```c
|
||||
#include "esp_heap_caps.h"
|
||||
#include <stdlib.h>
|
||||
```
|
||||
Ajouter une variable statique près de `s_mic_handle` :
|
||||
```c
|
||||
#define HAL_I2S_CAP_FRAME_SAMPLES 320
|
||||
#define HAL_I2S_CAP_IN_BYTES (HAL_I2S_CAP_FRAME_SAMPLES * 2 * (int)sizeof(int16_t))
|
||||
static int16_t *s_rx_scratch = NULL;
|
||||
```
|
||||
Ajouter les trois fonctions (adaptées de plip_voice audio.c:764-830) :
|
||||
```c
|
||||
esp_err_t hal_i2s_capture_begin(void)
|
||||
{
|
||||
if (!s_mic_handle) {
|
||||
ESP_LOGE(TAG, "capture_begin: pas de canal RX");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (s_rx_scratch) return ESP_OK; /* idempotent */
|
||||
s_rx_scratch = heap_caps_malloc(HAL_I2S_CAP_IN_BYTES, MALLOC_CAP_SPIRAM);
|
||||
if (!s_rx_scratch) s_rx_scratch = malloc(HAL_I2S_CAP_IN_BYTES);
|
||||
if (!s_rx_scratch) return ESP_ERR_NO_MEM;
|
||||
ESP_LOGI(TAG, "capture_begin: prêt (full-duplex, TX actif)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int hal_i2s_capture_read_frame(int16_t *mono_out, int n_samples, int64_t *rms_sq_out)
|
||||
{
|
||||
if (!s_rx_scratch || !s_mic_handle) return -1;
|
||||
size_t bytes_read = 0;
|
||||
esp_err_t ret = i2s_channel_read(s_mic_handle, s_rx_scratch,
|
||||
HAL_I2S_CAP_IN_BYTES, &bytes_read,
|
||||
pdMS_TO_TICKS(100));
|
||||
if (ret != ESP_OK || bytes_read == 0) return 0; /* timeout */
|
||||
int n = (int)(bytes_read / (2 * sizeof(int16_t)));
|
||||
if (n > n_samples) n = n_samples;
|
||||
int64_t rms_sq = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int32_t l = s_rx_scratch[i * 2];
|
||||
int32_t r = s_rx_scratch[i * 2 + 1];
|
||||
int16_t mono = (int16_t)((l + r) / 2);
|
||||
mono_out[i] = mono;
|
||||
rms_sq += (int64_t)mono * mono;
|
||||
}
|
||||
if (rms_sq_out) *rms_sq_out = (n > 0) ? rms_sq / n : 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
void hal_i2s_capture_end(void)
|
||||
{
|
||||
if (s_rx_scratch) { free(s_rx_scratch); s_rx_scratch = NULL; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Sonde RMS micro temporaire dans `main/app_main.c`**
|
||||
|
||||
Remplacer la sonde hook de Task 1 (la boucle `HOOK sonde`) par une sonde combinée hook + micro (pour valider le chemin micro LIN2/RIN2) :
|
||||
```c
|
||||
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));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Builder, flasher, valider le micro**
|
||||
|
||||
Run : build + flash, lire ~12 s en **parlant/soufflant dans le micro du combiné décroché**.
|
||||
Expected : `capture_begin: prêt`, puis `rms_sq` proche de 0 au silence et nettement plus élevé (milliers+) quand on parle dans le micro. Confirme que le chemin micro LIN2/RIN2 capte du signal.
|
||||
Si `rms_sq` reste figé à ~0 même en parlant : le DLL fix / la sélection LIN2 est en cause — reporter DONE_WITH_CONCERNS avec les valeurs.
|
||||
|
||||
- [ ] **Step 5 : Commit**
|
||||
|
||||
```bash
|
||||
git add components/hal_i2s main/app_main.c
|
||||
git commit -m "feat(audio): API capture micro RX hal_i2s"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3 : `dtmf` (détecteur Goertzel, testé host)
|
||||
|
||||
**Files:**
|
||||
- Create: `components/dtmf/include/dtmf.h`
|
||||
- Create: `components/dtmf/dtmf.c`
|
||||
- Create: `components/dtmf/CMakeLists.txt`
|
||||
- Test: `components/dtmf/test_dtmf.c` (host, `cc`)
|
||||
- Modify: `main/CMakeLists.txt` (REQUIRES += `dtmf`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes : `hal_i2s_capture_read_frame()` (Task 2) ; `dialer_push_digit()` (Task 4 — déclaré mais la tâche dtmf n'est démarrée qu'en Task 6).
|
||||
- Produces :
|
||||
- `char dtmf_detect_frame(const int16_t *mono, int n);` — analyse une frame 20 ms mono ; retourne le caractère DTMF confirmé (`'0'`-`'9'`, `'*'`, `'#'`) ou `'\0'`. **Fonction pure, testable host.**
|
||||
- `void dtmf_start(void);` / `void dtmf_stop(void);` — arme/désarme la tâche de capture→décodage (utilisée en Task 6).
|
||||
|
||||
- [ ] **Step 1 : Copier `dtmf.h` et `dtmf.c` depuis plip_voice**
|
||||
|
||||
Copier `/Users/electron/code/zacus/ESP32_ZACUS/plip_voice/main/dtmf.h` → `components/dtmf/include/dtmf.h` et `dtmf.c` → `components/dtmf/dtmf.c` (verbatim). `dtmf_detect_frame` et le Goertzel sont purs.
|
||||
|
||||
- [ ] **Step 2 : Adapter la source de samples dans `dtmf.c`**
|
||||
|
||||
Dans `dtmf.c`, la tâche `dtmf_task` appelle `audio_capture_read_frame(...)` (API plip_voice). Remplacer chaque appel `audio_capture_read_frame(` par `hal_i2s_capture_read_frame(`, et tout appel `audio_capture_begin()` par `hal_i2s_capture_begin()`. Remplacer l'include `#include "audio.h"` par `#include "hal_i2s.h"`. La tâche appelle `dialer_push_digit(sym - '0')` sur un chiffre 0-9 — garder ; ajouter `#include "dialer.h"`. NE PAS modifier `dtmf_detect_frame` ni le Goertzel.
|
||||
|
||||
- [ ] **Step 3 : Écrire le test host qui échoue — `components/dtmf/test_dtmf.c`**
|
||||
|
||||
```c
|
||||
/* Test host de dtmf_detect_frame : synthétise des frames DTMF et vérifie le décodage. */
|
||||
#include "dtmf.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
#define FS 16000
|
||||
#define N 320
|
||||
|
||||
/* Remplit une frame mono 20 ms avec la somme de deux sinus (low+high). */
|
||||
static void synth(int16_t *buf, int n, float f_low, float f_high)
|
||||
{
|
||||
for (int i = 0; i < n; i++) {
|
||||
float s = 9000.0f * sinf(2.0f*(float)M_PI*f_low *i/FS)
|
||||
+ 9000.0f * sinf(2.0f*(float)M_PI*f_high*i/FS);
|
||||
buf[i] = (int16_t)s;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int16_t buf[N];
|
||||
char d = '\0';
|
||||
|
||||
/* '1' = 697 + 1209. Le détecteur confirme après DTMF_CONFIRM_FRAMES (2)
|
||||
* frames soutenues ; on l'alimente plusieurs fois puis on lit le résultat. */
|
||||
for (int k = 0; k < 5; k++) {
|
||||
synth(buf, N, 697.0f, 1209.0f);
|
||||
char c = dtmf_detect_frame(buf, N);
|
||||
if (c != '\0') d = c;
|
||||
}
|
||||
assert(d == '1');
|
||||
|
||||
/* Silence → aucun chiffre. */
|
||||
int16_t zero[N] = {0};
|
||||
char s = '\0';
|
||||
for (int k = 0; k < 3; k++) { char c = dtmf_detect_frame(zero, N); if (c) s = c; }
|
||||
assert(s == '\0');
|
||||
|
||||
/* '9' = 852 + 1477 après ré-arm (silence intercalé ci-dessus). */
|
||||
char d9 = '\0';
|
||||
for (int k = 0; k < 5; k++) {
|
||||
synth(buf, N, 852.0f, 1477.0f);
|
||||
char c = dtmf_detect_frame(buf, N);
|
||||
if (c != '\0') d9 = c;
|
||||
}
|
||||
assert(d9 == '9');
|
||||
|
||||
printf("dtmf: 3/3 assertions OK\n");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Compiler le test → RED**
|
||||
|
||||
Run :
|
||||
```bash
|
||||
cc -o /tmp/test_dtmf -I /Users/electron/RTC_BL_PHONE/components/dtmf/include \
|
||||
/Users/electron/RTC_BL_PHONE/components/dtmf/test_dtmf.c \
|
||||
/Users/electron/RTC_BL_PHONE/components/dtmf/dtmf.c -lm 2>&1 | head -20
|
||||
```
|
||||
Expected : ÉCHEC de compilation — `dtmf.c` référence des symboles IDF (FreeRTOS, ESP_LOG, hal_i2s) absents en host. C'est attendu : on ne peut pas lier la tâche IDF en host. **Adapter la stratégie de test** : compiler UNIQUEMENT la fonction pure. Pour cela, le test doit être lié à un `dtmf.c` dont seule `dtmf_detect_frame` + le Goertzel sont host-compatibles.
|
||||
|
||||
→ Solution : isoler la partie pure. Si `dtmf.c` mélange `dtmf_detect_frame` (pur) et `dtmf_task` (IDF), encadrer la section IDF (`dtmf_task`, `dtmf_start`, `dtmf_stop`, includes FreeRTOS/esp_log/hal_i2s/dialer) par `#ifndef DTMF_HOST_TEST ... #endif`. Le test host compile avec `-DDTMF_HOST_TEST`. La commande RED devient :
|
||||
```bash
|
||||
cc -DDTMF_HOST_TEST -o /tmp/test_dtmf -I /Users/electron/RTC_BL_PHONE/components/dtmf/include \
|
||||
/Users/electron/RTC_BL_PHONE/components/dtmf/test_dtmf.c \
|
||||
/Users/electron/RTC_BL_PHONE/components/dtmf/dtmf.c -lm
|
||||
```
|
||||
Avant d'ajouter les gardes, lancer cette commande pour observer l'échec initial (symboles IDF non définis), ce qui motive l'ajout des `#ifndef DTMF_HOST_TEST`.
|
||||
|
||||
- [ ] **Step 5 : Ajouter les gardes `#ifndef DTMF_HOST_TEST` dans `dtmf.c`**
|
||||
|
||||
Encadrer par `#ifndef DTMF_HOST_TEST` / `#endif` : les includes IDF (`freertos/*`, `esp_log.h`, `hal_i2s.h`, `dialer.h`), et les fonctions `dtmf_task`, `dtmf_start`, `dtmf_stop`. Laisser HORS garde : les includes `<math.h>`/`<stdint.h>`, les constantes (fréquences, seuils), `goertzel_power`, et `dtmf_detect_frame`. Remplacer les `ESP_LOGx` éventuels DANS `dtmf_detect_frame` par rien (ou un `#define` no-op sous DTMF_HOST_TEST) si présents.
|
||||
|
||||
- [ ] **Step 6 : Compiler le test → GREEN**
|
||||
|
||||
Run :
|
||||
```bash
|
||||
cc -DDTMF_HOST_TEST -o /tmp/test_dtmf -I /Users/electron/RTC_BL_PHONE/components/dtmf/include \
|
||||
/Users/electron/RTC_BL_PHONE/components/dtmf/test_dtmf.c \
|
||||
/Users/electron/RTC_BL_PHONE/components/dtmf/dtmf.c -lm && /tmp/test_dtmf
|
||||
```
|
||||
Expected : `dtmf: 3/3 assertions OK`. Si une assertion échoue (chiffre non détecté), NE pas affaiblir le test : vérifier l'amplitude de synthèse (9000+9000 peut saturer — réduire à 6000 chacun si besoin) et les seuils ; documenter tout ajustement de l'amplitude de test (pas des seuils du détecteur).
|
||||
|
||||
- [ ] **Step 7 : Créer `components/dtmf/CMakeLists.txt`**
|
||||
|
||||
```cmake
|
||||
idf_component_register(
|
||||
SRCS "dtmf.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES hal_i2s dialer
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 8 : Référencer `dtmf` dans `main/CMakeLists.txt` et builder**
|
||||
|
||||
Ajouter `dtmf` aux REQUIRES de `main`. Run `idf.py build` → `Project build complete.` (le composant `dtmf` compile la version IDF, sans `DTMF_HOST_TEST`).
|
||||
|
||||
- [ ] **Step 9 : Commit**
|
||||
|
||||
```bash
|
||||
git add components/dtmf main/CMakeLists.txt
|
||||
git commit -m "feat(tel): DTMF Goertzel + test host"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4 : `dialer` (accumulateur de chiffres, testé host)
|
||||
|
||||
**Files:**
|
||||
- Create: `components/dialer/include/dialer.h`
|
||||
- Create: `components/dialer/dialer.c`
|
||||
- Create: `components/dialer/CMakeLists.txt`
|
||||
- Test: `components/dialer/test_dialer.c` (host, `cc`)
|
||||
- Modify: `main/CMakeLists.txt` (REQUIRES += `dialer`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes : `esp_timer_get_time()` (IDF) en cible ; en host, une implémentation de substitution sous garde.
|
||||
- Produces :
|
||||
- `void dialer_init(void);` `void dialer_reset(void);`
|
||||
- `void dialer_push_digit(int d);` — ajoute un chiffre 0-9 (ignore hors plage / au-delà de 12 chiffres).
|
||||
- `const char *dialer_current(void);` — numéro accumulé (C-string).
|
||||
- `bool dialer_idle(void);` — true si aucun chiffre encore.
|
||||
- `int dialer_ms_since_last(void);` — ms depuis le dernier chiffre.
|
||||
|
||||
- [ ] **Step 1 : Copier `dialer.h`/`dialer.c` depuis plip_voice**
|
||||
|
||||
Copier `/Users/electron/code/zacus/ESP32_ZACUS/plip_voice/main/dialer.h` → `components/dialer/include/dialer.h` et `dialer.c` → `components/dialer/dialer.c` (verbatim).
|
||||
|
||||
- [ ] **Step 2 : Rendre `dialer.c` testable host (substitution du temps + log)**
|
||||
|
||||
En tête de `dialer.c`, encadrer pour le host : remplacer l'include `esp_timer.h`/`esp_log.h` par une abstraction. Ajouter au début :
|
||||
```c
|
||||
#ifdef DIALER_HOST_TEST
|
||||
#include <stdint.h>
|
||||
static int64_t g_fake_us = 0;
|
||||
static int64_t esp_timer_get_time(void) { return g_fake_us; }
|
||||
void dialer_test_set_time_us(int64_t us); /* déclaré pour le test */
|
||||
void dialer_test_set_time_us(int64_t us) { g_fake_us = us; }
|
||||
#define ESP_LOGI(tag, ...) ((void)0)
|
||||
#else
|
||||
#include "esp_timer.h"
|
||||
#include "esp_log.h"
|
||||
#endif
|
||||
```
|
||||
(Si `dialer.c` a un `#define TAG`/`static const char *TAG`, le laisser ; sous host le `ESP_LOGI` no-op l'ignore.) Le reste de `dialer.c` reste inchangé.
|
||||
|
||||
- [ ] **Step 3 : Écrire le test host — `components/dialer/test_dialer.c`**
|
||||
|
||||
```c
|
||||
#include "dialer.h"
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
void dialer_test_set_time_us(int64_t us); /* fourni par dialer.c sous DIALER_HOST_TEST */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
dialer_init();
|
||||
assert(dialer_idle()); /* vide au départ */
|
||||
|
||||
dialer_test_set_time_us(1000000); /* t = 1 s */
|
||||
dialer_push_digit(0);
|
||||
dialer_push_digit(1);
|
||||
dialer_push_digit(4);
|
||||
assert(!dialer_idle());
|
||||
assert(strcmp(dialer_current(), "014") == 0);
|
||||
|
||||
/* ms depuis le dernier chiffre */
|
||||
dialer_test_set_time_us(1000000 + 1500000); /* +1.5 s */
|
||||
assert(dialer_ms_since_last() == 1500);
|
||||
|
||||
/* chiffres hors plage ignorés */
|
||||
dialer_push_digit(-1);
|
||||
dialer_push_digit(10);
|
||||
assert(strcmp(dialer_current(), "014") == 0);
|
||||
|
||||
dialer_reset();
|
||||
assert(dialer_idle());
|
||||
assert(strcmp(dialer_current(), "") == 0);
|
||||
|
||||
printf("dialer: 6/6 assertions OK\n");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : RED**
|
||||
|
||||
Run :
|
||||
```bash
|
||||
cc -DDIALER_HOST_TEST -o /tmp/test_dialer -I /Users/electron/RTC_BL_PHONE/components/dialer/include \
|
||||
/Users/electron/RTC_BL_PHONE/components/dialer/test_dialer.c \
|
||||
/Users/electron/RTC_BL_PHONE/components/dialer/dialer.c
|
||||
```
|
||||
Expected : si la garde host de Step 2 n'est pas encore en place, échec de compilation (esp_timer.h introuvable). C'est le RED qui motive la garde. Une fois la garde en place, recompiler.
|
||||
|
||||
- [ ] **Step 5 : GREEN**
|
||||
|
||||
Après la garde (Step 2), run la même commande + exécution :
|
||||
```bash
|
||||
cc -DDIALER_HOST_TEST -o /tmp/test_dialer -I /Users/electron/RTC_BL_PHONE/components/dialer/include \
|
||||
/Users/electron/RTC_BL_PHONE/components/dialer/test_dialer.c \
|
||||
/Users/electron/RTC_BL_PHONE/components/dialer/dialer.c && /tmp/test_dialer
|
||||
```
|
||||
Expected : `dialer: 6/6 assertions OK`. (Si `MAX_DIGITS` ou le format diffère, ajuster le test aux constantes réelles de `dialer.c`, sans changer la logique.)
|
||||
|
||||
- [ ] **Step 6 : Créer `components/dialer/CMakeLists.txt`**
|
||||
|
||||
```cmake
|
||||
idf_component_register(
|
||||
SRCS "dialer.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_timer
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 7 : Référencer dans `main/CMakeLists.txt` et builder**
|
||||
|
||||
Ajouter `dialer` aux REQUIRES de `main`. `idf.py build` → vert.
|
||||
|
||||
- [ ] **Step 8 : Commit**
|
||||
|
||||
```bash
|
||||
git add components/dialer main/CMakeLists.txt
|
||||
git commit -m "feat(tel): dialer accumulateur + test host"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5 : `hook_monitor` (surveillance décroché + impulsions cadran)
|
||||
|
||||
**Files:**
|
||||
- Create: `components/hook_monitor/include/hook_monitor.h`
|
||||
- Create: `components/hook_monitor/hook_monitor.c`
|
||||
- Create: `components/hook_monitor/CMakeLists.txt`
|
||||
- Modify: `main/CMakeLists.txt` (REQUIRES += `hook_monitor`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes : `slic_is_offhook()` (Task 1) ; `dialer_push_digit()` (Task 4).
|
||||
- Produces :
|
||||
- `typedef void (*hook_change_cb_t)(bool offhook);`
|
||||
- `esp_err_t hook_monitor_start(hook_change_cb_t cb);` — lance la tâche de surveillance ; appelle `cb(offhook)` sur chaque transition décroché/raccroché stable, et `dialer_push_digit()` sur chaque chiffre composé au cadran (impulsions).
|
||||
- `bool hook_monitor_offhook(void);` — état hook débouncé courant.
|
||||
|
||||
- [ ] **Step 1 : Créer `components/hook_monitor/include/hook_monitor.h`**
|
||||
|
||||
```c
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*hook_change_cb_t)(bool offhook);
|
||||
|
||||
/* Start the hook-monitor task. Calls cb(offhook) on each stable hook
|
||||
* transition, and dialer_push_digit() for each rotary-dialed digit. */
|
||||
esp_err_t hook_monitor_start(hook_change_cb_t cb);
|
||||
|
||||
/* Current debounced off-hook state. */
|
||||
bool hook_monitor_offhook(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
> **Risque & repli :** la logique d'impulsion ci-dessous est une version simplifiée. La détection d'impulsion du cadran est sensible aux timings réels de la boucle SLIC. Si la validation matérielle (Task 6 Step 7) montre des chiffres manqués/parasites, le repli est de porter plus fidèlement la logique éprouvée de `/Users/electron/code/zacus/ESP32_ZACUS/plip_voice/main/phone.c` (vérification multi-échantillons du raccroché `HANGUP_VERIFY_COUNT`, resync auto-correctif `RESYNC_PICKUP/HANGUP_MS`, filtrage glitch). Conserver l'interface `hook_monitor_start(cb)` / `hook_monitor_offhook()` inchangée.
|
||||
|
||||
- [ ] **Step 2 : Créer `components/hook_monitor/hook_monitor.c`** (adapté de plip_voice phone.c, dépouillé du couplage conversation/audio/ring)
|
||||
|
||||
```c
|
||||
/*
|
||||
* hook_monitor.c — surveille le crochet (SHK via slic) et décode la
|
||||
* numérotation par impulsions du cadran. Adapté de plip_voice/main/phone.c
|
||||
* (logique de pulse/hangup) sans le couplage conversation/audio.
|
||||
*
|
||||
* Cadran à impulsions : pendant que le combiné est décroché, le cadran ouvre
|
||||
* la boucle N fois pour le chiffre N (10 impulsions = 0). Un raccroché réel se
|
||||
* distingue d'une impulsion par sa durée (> HANGUP_THRESHOLD_MS).
|
||||
*/
|
||||
#include "hook_monitor.h"
|
||||
#include "slic.h"
|
||||
#include "dialer.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#define TAG "hook_monitor"
|
||||
|
||||
#define POLL_MS 10
|
||||
#define DEBOUNCE_MS 30
|
||||
#define PULSE_MIN_WIDTH_MS 20 /* filtre anti-glitch */
|
||||
#define INTER_DIGIT_GAP_MS 200 /* fin d'un chiffre au cadran */
|
||||
#define HANGUP_THRESHOLD_MS 2500 /* ouverture > seuil = vrai raccroché */
|
||||
|
||||
static hook_change_cb_t s_cb = NULL;
|
||||
static volatile bool s_offhook = false;
|
||||
|
||||
/* 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 →
|
||||
* slic_is_offhook() repasse false un court instant. On distingue :
|
||||
* - impulsion : ouverture courte (~60 ms) répétée, puis gap inter-chiffre
|
||||
* - raccroché : ouverture longue (> HANGUP_THRESHOLD_MS) */
|
||||
static void hook_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
bool stable = slic_is_offhook();
|
||||
s_offhook = stable;
|
||||
if (s_cb) s_cb(stable);
|
||||
|
||||
int pulse_count = 0;
|
||||
bool in_open = false;
|
||||
int64_t open_start_us = 0;
|
||||
int64_t last_close_us = esp_timer_get_time();
|
||||
|
||||
for (;;) {
|
||||
bool raw = slic_is_offhook();
|
||||
int64_t now = esp_timer_get_time();
|
||||
|
||||
if (!stable) {
|
||||
/* Combiné raccroché : n'attendre qu'un décroché franc et débouncé. */
|
||||
if (raw) {
|
||||
vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS));
|
||||
if (slic_is_offhook()) {
|
||||
stable = true; s_offhook = true;
|
||||
pulse_count = 0; in_open = false;
|
||||
last_close_us = esp_timer_get_time();
|
||||
dialer_reset();
|
||||
if (s_cb) s_cb(true);
|
||||
ESP_LOGI(TAG, "décroché");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Combiné décroché : décoder impulsions + détecter raccroché. */
|
||||
if (!raw && !in_open) {
|
||||
in_open = true; open_start_us = now; /* début ouverture */
|
||||
} else if (raw && in_open) {
|
||||
in_open = false; /* fin ouverture */
|
||||
int open_ms = (int)((now - open_start_us) / 1000);
|
||||
if (open_ms >= PULSE_MIN_WIDTH_MS && open_ms < HANGUP_THRESHOLD_MS) {
|
||||
pulse_count++; /* impulsion valide */
|
||||
}
|
||||
last_close_us = now;
|
||||
} else if (!raw && in_open) {
|
||||
/* ouverture en cours : vérifier un raccroché (ouverture longue) */
|
||||
int open_ms = (int)((now - open_start_us) / 1000);
|
||||
if (open_ms >= HANGUP_THRESHOLD_MS) {
|
||||
stable = false; s_offhook = false;
|
||||
in_open = false; pulse_count = 0;
|
||||
if (s_cb) s_cb(false);
|
||||
ESP_LOGI(TAG, "raccroché");
|
||||
}
|
||||
}
|
||||
|
||||
/* Fin d'un chiffre : gap inter-impulsions dépassé avec des pulses. */
|
||||
if (!in_open && pulse_count > 0 &&
|
||||
(int)((now - last_close_us) / 1000) >= INTER_DIGIT_GAP_MS) {
|
||||
int digit = (pulse_count >= 10) ? 0 : pulse_count;
|
||||
ESP_LOGI(TAG, "cadran: %d impulsions -> chiffre %d", pulse_count, digit);
|
||||
dialer_push_digit(digit);
|
||||
pulse_count = 0;
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(POLL_MS));
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t hook_monitor_start(hook_change_cb_t cb)
|
||||
{
|
||||
s_cb = cb;
|
||||
if (xTaskCreatePinnedToCore(hook_task, "hook", 3072, NULL, 6, NULL, 1) != pdPASS) {
|
||||
ESP_LOGE(TAG, "échec création tâche hook");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool hook_monitor_offhook(void) { return s_offhook; }
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Créer `components/hook_monitor/CMakeLists.txt`**
|
||||
|
||||
```cmake
|
||||
idf_component_register(
|
||||
SRCS "hook_monitor.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_timer slic_ks0835 dialer
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Référencer dans `main/CMakeLists.txt` et builder**
|
||||
|
||||
Ajouter `hook_monitor` aux REQUIRES de `main`. `idf.py build` → vert. (Pas de câblage app_main ici ; ce sera fait en Task 6.)
|
||||
|
||||
- [ ] **Step 5 : Commit**
|
||||
|
||||
```bash
|
||||
git add components/hook_monitor main/CMakeLists.txt
|
||||
git commit -m "feat(tel): hook_monitor decroche + impulsions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6 : `call_manager` + intégration (critère de phase)
|
||||
|
||||
**Files:**
|
||||
- Create: `components/call_manager/include/call_manager.h`
|
||||
- Create: `components/call_manager/call_manager.c`
|
||||
- Create: `components/call_manager/CMakeLists.txt`
|
||||
- Modify: `components/hal_es8388/es8388.c` (retirer le forçage PA — propriétaire unique)
|
||||
- Modify: `main/CMakeLists.txt` (REQUIRES += `call_manager`)
|
||||
- Modify: `main/app_main.c` (retirer tonalité-au-boot + sondes temporaires ; lancer `call_manager_start()`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes : `hook_monitor_start()` + `hook_monitor_offhook()` (Task 5) ; `dialer_*` (Task 4) ; `dtmf_start/stop` (Task 3) ; `audio_router_dialtone_start/tones_stop/busy_start` + `hal_audio_pa_set` (Phase 2).
|
||||
- Produces :
|
||||
- `void call_manager_start(void);` — lance la machine d'état d'appel locale (sans BT).
|
||||
|
||||
- [ ] **Step 1 : Retirer le forçage PA de `es8388_init` (propriétaire unique)**
|
||||
|
||||
Dans `components/hal_es8388/es8388.c`, supprimer les deux lignes qui forcent la PA en fin d'init :
|
||||
```c
|
||||
gpio_set_direction(PLIP_PA_ENABLE, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(PLIP_PA_ENABLE, 1);
|
||||
```
|
||||
et les remplacer par la seule configuration en sortie (sans l'activer) :
|
||||
```c
|
||||
gpio_set_direction(PLIP_PA_ENABLE, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(PLIP_PA_ENABLE, 0); /* PA pilotée par call_manager (off au repos) */
|
||||
```
|
||||
Ainsi `hal_audio_pa_set` (hal_i2s) devient le pilote unique de la PA, et l'ampli démarre coupé.
|
||||
|
||||
- [ ] **Step 2 : Créer `components/call_manager/include/call_manager.h`**
|
||||
|
||||
```c
|
||||
#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
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Créer `components/call_manager/call_manager.c`** (adapté de plip_voice conversation.c, sans routing/greet/BT/WiFi)
|
||||
|
||||
```c
|
||||
/*
|
||||
* 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 */
|
||||
} else {
|
||||
/* log incrémental du numéro en cours (chiffres additionnels) */
|
||||
}
|
||||
}
|
||||
|
||||
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)");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Créer `components/call_manager/CMakeLists.txt`**
|
||||
|
||||
```cmake
|
||||
idf_component_register(
|
||||
SRCS "call_manager.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES hook_monitor dialer dtmf audio_router hal_i2s
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 5 : Nettoyer `main/app_main.c` et lancer `call_manager`**
|
||||
|
||||
Dans `main/app_main.c` :
|
||||
1. RETIRER la tonalité jouée au boot (lignes `audio_router_init(); audio_router_dialtone_start(); ESP_LOGI(... tonalite de numerotation demarree ...)`) — la tonalité est désormais déclenchée au décroché.
|
||||
2. RETIRER les sondes temporaires (boucle `MIC sonde`/`HOOK sonde` + `hal_i2s_capture_begin` de la sonde) ajoutées en Tasks 1-2.
|
||||
3. Conserver `ESP_ERROR_CHECK(hal_i2s_init());` et `ESP_ERROR_CHECK(slic_init());`. Ajouter `audio_router_init();` (création de la tâche de tonalités, sans la démarrer).
|
||||
4. Ajouter les includes `#include "call_manager.h"` et `#include "dialer.h"`, et après `slic_init()` :
|
||||
```c
|
||||
dialer_init();
|
||||
call_manager_start();
|
||||
ESP_LOGI(TAG, "téléphonie locale prête — décroche pour composer");
|
||||
```
|
||||
5. Mettre à jour `main/CMakeLists.txt` : REQUIRES final = `nvs_flash config_store hal_es8388 hal_i2s audio_router slic_ks0835 dtmf dialer hook_monitor call_manager`.
|
||||
|
||||
- [ ] **Step 6 : Builder**
|
||||
|
||||
Run : `. /Users/electron/esp/esp-idf/export.sh && idf.py build` → `Project build complete.`
|
||||
|
||||
- [ ] **Step 7 : Flasher et VALIDER LE CRITÈRE DE PHASE (matériel)**
|
||||
|
||||
Run : flash, puis lire la série en **manipulant le combiné** :
|
||||
1. **Décrocher** → log `-> DIALTONE (décroché)` + tonalité audible dans l'écouteur + `PA ON`.
|
||||
2. **Composer au cadran** (impulsions) quelques chiffres → logs `cadran: N impulsions -> chiffre X` puis `-> DIALING, numéro: "..."`.
|
||||
3. **Composer au clavier DTMF** (si combiné à touches) → les chiffres DTMF s'ajoutent au numéro (logs dialer `digit ...`).
|
||||
4. Attendre 3 s → log `NUMÉRO COMPOSÉ: "..." (impulsion + DTMF)`.
|
||||
5. **Raccrocher** → log `-> IDLE (raccroché)`, tonalité coupée, `PA OFF`.
|
||||
Expected : la séquence complète fonctionne ; le numéro journalisé correspond aux chiffres composés (cadran et/ou clavier). C'est le critère de sortie de la Phase 3.
|
||||
Reporter DONE_WITH_CONCERNS avec les logs si un chemin (impulsion ou DTMF) ne capture pas correctement — c'est une validation matérielle pouvant nécessiter l'utilisateur.
|
||||
|
||||
- [ ] **Step 8 : Commit**
|
||||
|
||||
```bash
|
||||
git add components/call_manager components/hal_es8388/es8388.c main/CMakeLists.txt main/app_main.c
|
||||
git commit -m "feat(tel): call_manager local + PA single-owner"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Critère de sortie de Phase 3
|
||||
|
||||
Au décroché : tonalité de numérotation ; la numérotation au **cadran à impulsions** ET au **clavier DTMF** est capturée et le numéro composé est journalisé ; au raccroché : silence et ampli coupé. PA à propriétaire unique. Aucun Bluetooth — c'est l'objet de la Phase 4 (le numéro figé sera passé à `esp_hf_client_dial`).
|
||||
|
||||
## Notes sur les tests
|
||||
|
||||
- `dtmf` (`dtmf_detect_frame`) et `dialer` sont couverts par des **tests host** (`cc`, synthèse de tonalités DTMF / séquences de chiffres) — vrai TDD sur la logique pure.
|
||||
- `slic_ks0835`, capture micro, `hook_monitor`, `call_manager` sont validés par **build + flash + manipulation du combiné** (logs série + écoute). La polarité SHK est tranchée empiriquement en Task 1. La validation finale du parcours décroché→numérotation→raccroché nécessite l'utilisateur sur le matériel.
|
||||
Reference in New Issue
Block a user