75 Commits

Author SHA1 Message Date
clement d1db8862c1 docs: retire plan legacy ESP32-S3/AG1171S 2026-06-24 10:04:50 +02:00
clement e8efda1ce4 docs: retire la doc Arduino A252 legacy
Le repo gardait ~76 docs de l'ancien projet Arduino A252 (ESP-NOW, UI web,
WiFi, hotline WAV/FFat, ancienne archi RTOS/audio/SLIC en classes C++,
rapports d'audit/tests du code supprime, orchestration d'agents, CI web).

Garde la doc du firmware actuel : specs/plans de la migration ESP-IDF
(docs/superpowers/), specs Bluetooth HFP/PBAP, spec materielle carte A1S/
ES8388 (a252_board_spec.md + ai_thinker_esp32_a1s_*.yaml), artefacts de
debug d'appairage HFP.

Impact : docs/ aligne sur le firmware mains-libres Bluetooth ESP-IDF.
2026-06-22 08:30:43 +02:00
clement bef10a0a49 chore: retire test/scripts/tools/CI Arduino-era
Suite au retrait de l'arbre Arduino (src/, platformio.ini), les outils et la
CI qui en dependaient sont devenus morts ou cassants. Verifie : rien dans
components/main ne les reference, le build idf.py reste OK.

Supprime :
- test/ : tests Unity qui incluaient le code C++ supprime (DtmfDecoder,
  AudioCodec...), aucune infra de test IDF
- scripts/ : gate A252, controle ESP-NOW, flash FFat/USB-MSC, validation
  hardware A252, contrats runtime PlatformIO
- tools/dev/ (flasheur universel multi-cibles) + tools/repo_state/ + Makefile.flash
- .github/workflows/ : ci.yml et release.yml lancaient PlatformIO + gate a252
  sur du code supprime (CI cassee), repo_state.yml
- .github/agents/ : 19 docs decrivant src/ et PlatformIO
- .github/prompts/plan-roadmapHardwareFirmware : roadmap A252

Garde : tools/phone_tui.py, templates .github generiques (issue/PR/prompts).

Impact : plus aucun residu du projet Arduino dans le repo ; build inchange.
2026-06-22 08:20:39 +02:00
clement 1df3dc5971 docs: aligne README et CLAUDE.md sur le firmware ESP-IDF
Repo State / repo-state (push) Failing after 19s
Les deux fichiers decrivaient encore le projet Arduino d'origine (ESP32-S3,
hotline WAV, ESP-NOW, pio run). Reecrits pour la realite actuelle : kit
mains-libres Bluetooth HFP-HF sur ESP32 classique, build idf.py, CLI de
reglage audio, organisation en components/. CLAUDE.md desormais versionne.
2026-06-21 21:54:59 +02:00
clement dbd789f335 chore: retire l'arbre Arduino legacy, doc ESP-IDF
Repo State / repo-state (push) Failing after 24s
Contexte : le repo contenait deux projets superposés — le projet Arduino/
PlatformIO d'origine (ESP32-S3, hotline WAV, ESP-NOW, UI web) et le firmware
ESP-IDF actuel (kit mains-libres Bluetooth, ESP32 classique). Le code Arduino
n'est plus utilise par le build IDF (verifie : aucune reference dans
CMakeLists/main/components) et entretenait une dette de confusion.

Approche : suppression du code mort verifie independant du build, et reecriture
de la doc pour la realite ESP-IDF. Build IDF revalide apres coup (binaire OK).

Changements :
- supprime src/ (56 fichiers C++ Arduino : AudioCodec, AudioFilePlayer,
  SlicK50835F, web/, wifi/, usb/, ...)
- supprime platformio.ini (build Arduino) et data/webui/ (UI web abandonnee,
  aucune partition data)
- supprime les CLAUDE.md imbriques documentant ces dossiers
- README.md : reecrit pour le kit mains-libres BT HFP-HF / ESP-IDF
- CLAUDE.md : reecrit (Arduino/ESP32-S3 -> ESP-IDF/ESP32 classique) et versionne

Impact : un seul projet dans le repo, doc alignee sur le code, build inchange.
2026-06-21 21:54:15 +02:00
clement 8625665a41 refactor(audio): retire l'instrumentation SCO de debug 2026-06-21 21:48:36 +02:00
clement 8a2280edb0 feat(pwr): poll adaptatif des taches au repos
Repo State / repo-state (push) Failing after 27s
2026-06-20 00:33:10 +02:00
clement 216dc2523e feat(pwr): DFS 160/80 + light sleep (eco batterie)
Repo State / repo-state (push) Failing after 13m59s
2026-06-19 23:58:33 +02:00
clement 577a351124 feat(pwr): codec+I2S en veille au repos 2026-06-19 23:51:10 +02:00
clement 6d3454dffd feat(tools): TUI reglage audio HFP (serie)
Repo State / repo-state (push) Failing after 25s
2026-06-19 22:56:40 +02:00
clement 178931899a feat(audio): AEC NLMS adaptatif (mode aec 2)
Repo State / repo-state (push) Failing after 12m17s
2026-06-19 22:53:12 +02:00
clement baed6f4290 feat(audio): suppression d'echo (mic ducking)
Repo State / repo-state (push) Failing after 15s
2026-06-19 22:42:55 +02:00
clement fc67a1eef8 feat(audio): persiste gains/filtre en NVS
Repo State / repo-state (push) Failing after 17s
2026-06-19 22:10:00 +02:00
clement 2be3729824 tune(audio): gain ecouteur defaut a 8
Repo State / repo-state (push) Failing after 18s
2026-06-19 19:43:57 +02:00
clement b930411829 tune(audio): gain micro defaut a 12
Repo State / repo-state (push) Failing after 19s
2026-06-19 19:20:14 +02:00
clement 2eb97bc4de feat(audio): gains runtime + filtre voix + CLI
Repo State / repo-state (push) Failing after 17s
2026-06-19 17:50:41 +02:00
clement e4123bd32f tune(bt): gain micro a 5
Repo State / repo-state (push) Failing after 31s
2026-06-19 17:43:17 +02:00
clement 452f3efcf6 tune(bt): gain micro a 3
Repo State / repo-state (push) Failing after 31s
2026-06-19 17:41:31 +02:00
clement 935772c1b1 fix(bt): coupe DTMF sur appel entrant
Repo State / repo-state (push) Failing after 21s
2026-06-19 17:15:06 +02:00
clement f9283bc14f tune(bt): gain micro a 2
Repo State / repo-state (push) Failing after 18s
2026-06-19 15:43:18 +02:00
clement f8e3dfb442 feat(bt): reconnexion HFP auto au boot
Repo State / repo-state (push) Failing after 30s
2026-06-19 15:33:49 +02:00
clement 7956cebde1 fix(bt): dtmf_stop sans delete force du RX 2026-06-19 15:22:15 +02:00
clement 8f722db9f4 feat(bt): gain numerique SCO downlink+uplink
Repo State / repo-state (push) Failing after 17s
2026-06-19 15:01:44 +02:00
clement a978357a47 fix(bt): micro partiel + frames SCO completes 2026-06-19 14:57:55 +02:00
clement c382d4d8a1 fix(bt): cadence SCO + flux sonnerie + diag RX 2026-06-19 14:52:07 +02:00
clement 2102d14487 chore(bt): compteurs diag pont SCO (temp) 2026-06-19 14:30:18 +02:00
clement d95513481a fix(bt): appairage Just Works (IO_CAP_NONE) 2026-06-19 14:22:25 +02:00
clement 4d9c81918f fix(bt): mobile initie le SLC (anti-collision) 2026-06-19 14:18:55 +02:00
clement 4aedf7756d fix(bt): initie connexion HFP a l'appairage 2026-06-19 14:02:39 +02:00
clement 9cb31bdea9 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é.
2026-06-19 13:52:06 +02:00
clement 6b9ecbf2e4 fix(bt): file FreeRTOS pour evenements BT
Remplace s_bt_ev/s_bt_dirty (scalaire 1 slot) par une file
xQueueCreate(8) + xQueueSend non-bloquant + drain complet dans
call_task — evite de perdre un evenement si deux arrivent dans
la meme fenetre de 50 ms (ex. INCOMING suivi AUDIO_CONNECTED).
2026-06-19 13:40:27 +02:00
clement 24e82fbf3b 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()
2026-06-19 13:32:45 +02:00
clement 0ff2c5d4f1 feat(bt): pont data SCO HCI vers audio_router
Ajoute sco_incoming_cb et sco_outgoing_cb dans bt_hfp.c.
Enregistre les callbacks sur AUDIO_STATE connecte (CVSD/mSBC).
REQUIRES audio_router dans CMakeLists.txt.
Build vert, boot non regressé (flash + monitor 2026-06-19).
2026-06-19 13:25:48 +02:00
clement a430a24f39 fix(bt): sco_end synchrone + stack tone_task
- sco_end: s_mode=NONE avant s_sco_active=false pour que
  tone_task quitte la branche SCO avant l'arret du flag mic.
- Attente active s_sco_mic_task==NULL (max 200 ms, pas 5 ms).
- Settle 25 ms avant hal_audio_pa_set(false) evite le clic PA.
- tone_task stack: 3072 -> 4096 (buf stereo SCO 1280 octets).
2026-06-19 13:21:34 +02:00
clement 1695539493 feat(bt): audio_router mode SCO (pont PCM I2S)
- Ajoute TONE_SCO a l enum tone_mode_t
- Ring buffers FreeRTOS (play + cap, 4 KB chacun)
- sco_mic_task : capture I2S -> ring sortant (decimation CVSD 8k)
- tone_task : lit ring entrant, mono->stereo, underrun = silence
- 4 fonctions pub : sco_begin/end/feed_playback/take_capture
- CMakeLists.txt : + esp_ringbuf dans REQUIRES
- Header : + stdbool.h / stdint.h (types bool/uint8_t/uint32_t)
2026-06-19 13:14:57 +02:00
clement 578ef4ffb2 feat(bt): pile HFP-HF + evenements
- composant bt_hfp : init Bluedroid Classic, GAP, esp_hf_client
- garde SSP retiree (pas de CONFIG_EXAMPLE_SSP_ENABLED dans ce projet)
- evenements haut niveau : SLC, appel entrant, audio SCO
- cable dans app_main apres call_manager_start
- build vert 1349 steps, boot sans panique confirme sur materiel
2026-06-19 13:06:19 +02:00
clement eb2d7fb860 feat(bt): active HFP audio HCI + WBS mSBC 2026-06-19 12:58:53 +02:00
clement 8ca352d173 docs: decisions HFP + plan Phase 4 bluetooth 2026-06-19 12:56:03 +02:00
clement 55b54da086 fix(tel): coupe PA en DIALING + warning dtmf
- dtmf.c: retire volatile sur s_task_handle (meme coeur)
- call_manager.c: hal_audio_pa_set(false) en DIALING
- es8388.c: log init corrige (PA off par defaut)
- app_main.c: slic_init avant audio_router_init
2026-06-19 11:07:32 +02:00
clement 21ca98f869 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
2026-06-19 11:00:19 +02:00
clement aec7599a22 feat(tel): hook_monitor decroche + impulsions
Composant FreeRTOS qui surveille SHK via slic_is_offhook(),
decoode la numerotation par impulsions (cadran rotatif) et
pousse les chiffres au dialer. Build vert sans câblage main.
2026-06-19 10:51:01 +02:00
clement a89fa2b746 feat(tel): dialer accumulateur + test host
- components/dialer/{dialer.c,include/dialer.h,CMakeLists.txt}
- garde DIALER_HOST_TEST : stub esp_timer + ESP_LOGI no-op
- test host : 6/6 assertions OK (RED sans garde, GREEN avec)
- main/CMakeLists.txt : REQUIRES += dialer
- idf.py build : 1343/1343 OK
2026-06-19 10:43:30 +02:00
clement c5eabc44c5 feat(tel): DTMF Goertzel + test host
Context: Phase 3 migration ESP-IDF - detecteur DTMF par Goertzel.

Approach: Adapter dtmf.c/h de plip_voice pour le projet RTC_BL_PHONE
en isolant la partie pure (dtmf_detect_frame + Goertzel) de la partie
IDF (tache FreeRTOS, hal_i2s, dialer) via garde #ifndef DTMF_HOST_TEST.

Changes:
- components/dtmf/include/dtmf.h : header adapte, sans sdkconfig.h ni
  CONFIG_PLIP_DIAL_DTMF, gardes DTMF_HOST_TEST sur dtmf_start/stop
- components/dtmf/dtmf.c : remplace audio.h->hal_i2s.h,
  audio_capture_*->hal_i2s_capture_*, stub forward dialer_push_digit,
  corrige signature hal_i2s_capture_begin(void)
- components/dtmf/test_dtmf.c : TDD host, synth 6000+6000 par freq,
  3 assertions (1, silence, 9)
- components/dtmf/CMakeLists.txt : REQUIRES hal_i2s (dialer en Task 6)

Impact: TDD RED->GREEN 3/3. Build IDF vert. dtmf non reference dans
main pour l'instant (dialer manque) - sera tire par call_manager T6.
2026-06-19 10:35:02 +02:00
clement c11572307e fix(audio): distingue timeout/erreur capture RX 2026-06-19 10:30:09 +02:00
clement f3b92bf6e3 feat(audio): API capture micro RX hal_i2s
Ajoute hal_i2s_capture_begin/read_frame/end : scratch PSRAM,
downmix L+R->mono, energie RMS. Sonde app_main valide le canal.
2026-06-19 10:27:02 +02:00
clement 04a12e9351 feat(tel): SLIC KS0835 hook + sonnerie
Composant slic_ks0835: GPIO RM/FR/SHK/PD, tache sonnerie FT,
sonde polarite SHK. slic_init() place avant audio_router_init()
pour eviter la contention GPIO/mutex avec la tache tone I2S.
SLIC_SHK_OFFHOOK_LEVEL=1 confirme: SHK=0 au raccroché (on-hook).
2026-06-19 10:19:08 +02:00
clement 7844378e2c docs(plan): plan Phase 3 telephonie locale 2026-06-19 09:46:56 +02:00
clement 808f66595b fix(audio): monte niveau tonalite ecouteur
HAL_I2S_EARPIECE_VOL 60->95, TONE_AMPLITUDE 8000->14000.0f.
2026-06-19 09:36:46 +02:00
clement 22aca75e57 feat(audio): tonalite via audio_router
Composant audio_router cree : tache FreeRTOS 440 Hz,
machine d'etat DIAL/BUSY/RINGBACK. Cable dans app_main,
tonalite active au boot (PA ON confirme en serie).
2026-06-19 09:25:19 +02:00
clement aea32cfd41 fix(audio): borne amplitude tone_gen a int16
Sature s dans [-32768, 32767] avant le cast int16_t pour
eviter l'UB C99. Documente le contrat dans l'en-tete.
Ajoute assertion 5/5 : amplitude 40000 ne deborde pas.
2026-06-19 09:21:01 +02:00
clement 12363e9541 feat(audio): tone_gen sinus pur + test host
Composant tone_gen : generation sinus stereo entrelace (L==R),
sans dependance IDF, teste en host TDD (4/4 assertions OK).
Build IDF vert : libtone_gen.a compile en [1321/1335].
2026-06-19 09:17:15 +02:00
clement 46c03e1fef fix(audio): libere handle RX I2S si enable echoue
Sur echec de i2s_channel_enable(RX), le handle alloue par
i2s_new_channel n'etait pas libere : fuite de ressource IDF.
Appel i2s_del_channel() avant nullification du handle.
Log final rendu honnete : annonce TX-only si RX desactive.
2026-06-19 09:12:41 +02:00
clement c350304b23 feat(audio): I2S full-duplex i2s_std + ampli
Create component hal_i2s wrapping ES8388 init and i2s_std full-duplex
channel pair (TX speaker + RX mic, identical std_cfg to avoid clock
routing breakage). hal_i2s_init() replaces direct es8388_init() call
in app_main to prevent double codec init.
Verified on HW: ES8388 init OK then I2S init OK 16kHz 16-bit stereo.
2026-06-19 09:07:22 +02:00
clement f452387d92 fix(audio): error handling ES8388 + define DAC_CTL27 2026-06-19 08:57:06 +02:00
clement e14c912dac feat(audio): driver ES8388 I2C nouvelle API 2026-06-19 08:52:22 +02:00
clement 0d378c8cb2 docs: pivot plip_voice + plan Phase 2 audio 2026-06-19 08:46:13 +02:00
clement 8c23ce05c2 chore(idf): fige BLE off et vitesse PSRAM
Deux ajouts déclaratifs dans sdkconfig.defaults :
- CONFIG_BT_BLE_ENABLED=n : verrouille BR/EDR only (BLE
  inutile pour HFP, économie RAM).
- CONFIG_SPIRAM_SPEED_40M=y : fige la vitesse PSRAM détectée
  à 40 MHz, prévient toute dérive Kconfig future.

Build propre confirmé (Project build complete).
Boot non régressé : Found 8MB PSRAM, NVS prêt, heartbeat OK.
2026-06-19 08:28:10 +02:00
clement 1a6e3e95d9 chore(idf): trace decision PSRAM carte A1S
PSRAM 8MB détectée et confirmée par log de boot ESP-IDF (Found 8MB
PSRAM device + SPI SRAM memory test OK). Activée dans sdkconfig.defaults
pour dimensionner les buffers BT/audio SCO en Phase 4.
2026-06-19 08:20:45 +02:00
clement 43e2edf97d feat(idf): composant config_store init NVS
Referme le point de revue Task 1 : nvs_flash était dans REQUIRES mais
jamais consommé. Crée config_store encapsulant nvs_flash_init() avec
gestion NO_FREE_PAGES/NEW_VERSION_FOUND (effacement+réinit automatique).
Appelé via ESP_ERROR_CHECK dans app_main après la bannière de boot.
Testé sur matériel (ESP32-D0WD v3.1) : "NVS prêt" observé avant
les heartbeat.
2026-06-19 08:12:21 +02:00
clement f90c390595 feat(idf): config BT Classic HFP-HF + partitions
Ajoute sdkconfig.defaults (BT Bluedroid BR/EDR + HFP-HF data path HCI,
flash 4 MB) et partitions.csv custom 4 MB sans OTA. Les 5 clés BT cibles
valent =y après régénération et build réussi (Project build complete).
2026-06-19 08:07:22 +02:00
clement 4ac05ca1f0 feat(idf): socle projet ESP-IDF qui boote
Création du squelette ESP-IDF minimal coexistant avec le code
Arduino existant (src/, platformio.ini). Le build IDF ignore les
sources Arduino. app_main() log une bannière de boot et un heartbeat
toutes les 5 s. Validé sur carte ESP32 classique (A1S AudioKit) :
build OK (rtc_bl_phone.bin 158 KB), flash et boot observés en série.
2026-06-19 07:59:28 +02:00
clement d7ba42f5dd docs(plan): plan Phase 1 socle ESP-IDF 2026-06-19 07:52:25 +02:00
clement d63e9cafef docs(spec): conception migration ESP-IDF + HFP 2026-06-19 07:48:18 +02:00
L'électron rare 87bcb1610d fix(rtc-phone): enable web auth, secure defaults, path traversal, I2S mutex, yield loop
- Enable web authentication by default (kWebAuthEnabledByDefault = true)
- Change default web password from "admin" to "zacus-web-42"
- Add URL-decode checks for path traversal (%2e, %2f patterns)
- lockI2s() returns false with warning when mutex null (was silently succeeding)
- Replace blocking delay(1) with vTaskDelay(1) in main loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 06:47:08 +01:00
L'électron rare a638fad71b chore(firmware): stage local working changes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 00:04:10 +01:00
L'électron rare e719f54711 chore(quality): add DTMF silence test and submodule clean guard
Replace TODO-only DTMF test file with executable non-regression test and add script to fail fast on dirty/uninitialized submodule pointers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 00:04:10 +01:00
L'électron rare eb2f78d0c6 fix(audio): make playback timeout rollover-safe in AudioFilePlayer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 00:04:10 +01:00
Clément SAILLANT c93fe81592 feat: Add FFat image upload script and A252 hardware specifications
- Implemented `upload_ffat.py` script to create and flash FFat images for the A252 board.
- Added hardware specifications for the A252 board in `SKILL.md`, detailing memory, DIP semantics, and canonical pin mapping.
- Introduced audio chain verification skill in `rtc-a252-audio-chain-verificator/SKILL.md` for diagnosing audio playback issues.
- Created `ToneCatalog` for managing audio tones, including definitions for various tone events and profiles.
- Defined board pin mappings in `a1s_board_pins.h` for the ESP32-A1S module.
- Established media routing structures and functions in `MediaRouting.h` to handle audio playback sources and tone events.
2026-02-26 04:44:37 +01:00
Clément SAILLANT 2e4e90adef Add strict A252 gate and hardware/orchestrator validation 2026-02-25 14:34:24 +01:00
Clément SAILLANT d2befa2c8d feat: Refactor WebServerManager and remove unused web UI files
- Deleted WebServerManager.h and WebServerManager.cpp files to streamline the codebase.
- Updated WebServerManager.cpp to improve authentication handling and command validation.
- Added command validator functionality to WebServerManager.
- Removed old web UI files (index.html, script.js, style.css) as they are no longer needed.
- Modified WifiManager to open fallback AP by default to avoid lockout.
- Introduced a pre-merge script for automated checks and builds.
- Added runtime contract tests for firmware API guards to ensure robustness.
2026-02-25 10:58:48 +01:00
Clément SAILLANT 1ce018e6eb esp32-S3 2026-02-25 09:34:04 +01:00
Clément SAILLANT fca57a133d feat: ajouter la gestion des paramètres ADC DSP et FFT dans la configuration audio 2026-02-24 19:20:39 +01:00
Clément SAILLANT a50f3c656b add esp32-S3 2026-02-24 19:20:17 +01:00
Clément SAILLANT efeacb79f8 feat: ajouter la gestion des erreurs pour les commandes SCENE et WIFI_RECONNECT 2026-02-23 21:34:42 +01:00
Clément SAILLANT 5a2d86e253 Refactor audio engine and command dispatcher; update ESP-NOW handling
- Updated AudioEngine to integrate AudioTools and improve WAV playback handling.
- Modified A252ConfigStore to change default SLIC pin configurations.
- Enhanced CommandDispatcher to provide clearer error messages for unsupported commands.
- Removed unused Bluetooth and WiFi functionalities from main application logic.
- Implemented payload size and peer limits in EspNowBridge to improve stability.
- Added documentation for ESP-NOW contract to clarify message structure and supported commands.
2026-02-23 21:33:08 +01:00
226 changed files with 7586 additions and 14842 deletions
-30
View File
@@ -1,30 +0,0 @@
# Custom Agent Alignment Complete
## Scope
Final alignment tasks that ensure every agent contract, runbook, and onboarding doc stays in sync before a major phase or hand-off.
## Do
- Review `docs/AGENT_TODO.md`, `docs/TEST_SCRIPT_COORDINATOR.md`, and `docs/AGENTS_INDEX.md` to confirm the current state of gates, artifacts, and command registries.
- Ensure every folder-specific `AGENTS*.md` entry (global, firmware, tools, docs, etc.) matches the latest instructions in `.github/agents` briefs; note mismatches in the release log and update the relevant doc.
- Check that onboarding materials (`docs/QUICKSTART.md`, `docs/_generated/COCKPIT_COMMANDS.md`) reflect the expected workflows referenced by the new gate or automation plan.
- Record each alignment review, gate status and artifact path in `GIT_WRITE_OPS_FINAL_REPORT.md` or `docs/AGENT_TODO.md` for traceability.
## Must Not
- Deliver feature changes in this pass; the goal is coherence and evidence before launch.
- Skip the safety checkpoint or artifact-tracking mandate from `AGENTS.md`.
## References
- `docs/AGENT_TODO.md`
- `docs/TEST_SCRIPT_COORDINATOR.md`
- `docs/AGENTS_INDEX.md`
- `.github/agents/*.md`
## Plan daction
1. Revoir les TODO/runbooks centrés sur les gates.
- run: python3 tools/dev/gen_cockpit_docs.py
- run: git status -sb
2. Confirmer la cohérence des AGENT contracts et docs.
- run: rg -n 'AGENT' docs/AGENT_TODO.md
3. Capturer les artefacts/étapes dans les rapports.
- run: cat GIT_WRITE_OPS_FINAL_REPORT.md
-38
View File
@@ -1,38 +0,0 @@
# Référentiel de Conventions RTC_BL_PHONE
## Objectif
Fournir un socle commun de conventions pour le code, la documentation, le hardware et les workflows du projet RTC_BL_PHONE, inspiré des meilleures pratiques issues des sources web (Espressif, PlatformIO, Silvertel, Asterisk, etc.).
## 1. Conventions de code (firmware)
- Utiliser PlatformIO pour la gestion des environnements et des dépendances.
- Respecter la structure : `src/` pour le code principal, `include/` pour les headers, `lib/` pour les librairies additionnelles.
- Préférer les noms explicites pour les GPIO et les états (ex : `pinHookSense`, `PhoneState::OFF_HOOK`).
- Documenter chaque fonction critique avec un commentaire Doxygen minimal.
- Utiliser le logging série pour tout comportement anormal ou critique.
- Ne jamais committer de binaires ou de logs générés.
## 2. Conventions hardware
- Documenter tout schéma de câblage dans `docs/solutions_rtc_phone_esp32.md`.
- Préciser les variantes supportées (ESP32-DevKitC, Audio Kit, SLIC, etc.).
- Toujours vérifier la cohérence entre le schéma et la configuration firmware.
- Ajouter des remarques de sécurité (alimentation, isolation, ESD) dans la doc.
## 3. Conventions documentation
- Garder la documentation concise, structurée et à jour.
- Vérifier la validité de tous les liens relatifs.
- Archiver ou supprimer les sections obsolètes.
- Référencer explicitement ce référentiel dans chaque agent et plan projet.
## 4. Conventions CI/Workflows
- Garder les workflows GitHub Actions simples et adaptés à PlatformIO.
- Vérifier la syntaxe YAML et la validité des chemins à chaque commit.
- Documenter toute évolution CI dans le README ou un changelog.
## 5. Références web
- Espressif DevKitC : https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32/esp32-devkitc/index.html
- PlatformIO : https://docs.platformio.org/en/latest/boards/espressif32/esp32dev.html
- Silvertel SLIC : https://www.silvertel.com/
- Asterisk PBX : https://docs.asterisk.org/
## 6. Mise à jour
Ce référentiel doit être mis à jour à chaque évolution majeure du projet ou des pratiques de la communauté.
-59
View File
@@ -1,59 +0,0 @@
# Index des agents RTC_BL_PHONE
Chaque fichier `.github/agents/*.md` est une fiche de gouvernance ou de workflow: se référer à la section `## Plan daction` (voir `PLAN_TEMPLATE.md`) et au référentiel de conventions (`CONVENTIONS.md`).
## Catégories principales
### 1. Gouvernance et pilotage
| Fichier | Rôle |
|---|---|
| `global.md` | Cohérence projet, build, doc, sécurité, release, délégation agents |
| `ci.md` | Workflows CI, validation PlatformIO, reporting |
| `PLAN_TEMPLATE.md` | Modèle de plan daction pour tous les agents |
| `CONVENTIONS.md` | Référentiel de conventions (code, doc, hardware, CI) |
### Délégation agents
- Agent Firmware : modularisation, tests, intégration hardware/audio, CI.
- Agent Hardware : câblage, sécurité, compatibilité ESP32/ESP32-S3.
- Agent Audio : abstraction AudioCodec, routage, documentation audio.
- Agent Documentation : rédaction, mise à jour, cohérence documentation.
- Agent CI/QA : validation builds, tests automatisés, traçabilité.
- Agent Global/Conventions : audit, amélioration, cohérence.
Les agents travaillent en synergie pour garantir la qualité, la sécurité et la maintenabilité.
### 2. Firmware et tests
| Fichier | Rôle |
|---|---|
| `firmware_core.md` | Logique principale, build PlatformIO, doc firmware |
| `firmware_tests.md` | Tests unitaires/fonctionnels, vérification GPIO/audio |
| `firmware_tooling.md` | Outils de build/test, scripts CLI |
| `firmware_copilot.md` | Tâches spécifiques Copilot (I2S, artefacts, logs) |
| `firmware_docs.md` | Documentation firmware, onboarding |
### 3. Hardware et audio
| Fichier | Rôle |
|---|---|
| `hardware.md` | Schémas, câblage, sécurité, variantes matérielles |
| `audio.md` | Routage audio, I2S, ES8388, SLIC, tests audio |
### Audio
- Abstraction AudioCodec (I2S/I2C, ES8388, PCM5102, GenericCodec)
- Routage RTC/Bluetooth via setRoute
- Tests unitaires : mock, test_audio_codec.cpp
- Sécurité : mapping pins, alimentation, ESD
### 4. Documentation et outils
| Fichier | Rôle |
|---|---|
| `docs.md` | Documentation utilisateur/technique, structure, liens |
| `tools.md` | Scripts, helpers, conventions CLI |
### 5. Spécifiques projet ou phase
| Fichier | Rôle |
|---|---|
| `ALIGNMENT_COMPLETE.md` | Checklist de pré-lancement, conformité agents |
| `PHASE_LAUNCH_PLAN.md` | Plan de lancement de phase, gates, artefacts |
> Pour chaque fiche, consulter la section “Références” et se conformer à `CONVENTIONS.md`.
-370
View File
@@ -1,370 +0,0 @@
# Firmware Health Baseline Report
**Date**: 2026-02-16
**Phase**: Firmware Embedded Expert Phase 1 (Stabilize + Observe)
**Status**: PARTIAL FIX (build OK, mapping SoftwareSerial D4/D5 validé, écran/app screen refactorisé, flash/RC Live relancé)
---
## Executive Summary
This baseline captures the current health state of Story V2 firmware across all 5 build targets:
- ESP32 dev (primary)
- ESP32 release (optimized variant)
- ESP8266 OLED (legacy support)
- RP2040 TFT 9488
- RP2040 TFT 9486
**Key metrics (après correction):**
- Build ESP8266 OLED : OK (mapping SoftwareSerial D4/D5 validé)
- Logique écran/app screen : refactorisée, tous les types d'apps instanciés
- Flash + RC Live : relancé, artefacts générés
- Panic-free sessions: `10/10` (no panic markers detected)
- UI link : à revalider (connected=1 à vérifier sur prochain artefact)
- WiFi disconnect incidents: `unknown` (health endpoints failed)
- RTOS anomalies: `unknown` (health endpoints failed)
**Baseline log**: [logs/generate_baseline_20260216-063753.log](logs/generate_baseline_20260216-063753.log)
**Correction log**: [artifacts/rc_live/20260217-120000_logs/run_matrix_and_smoke_20260217-120000.log](artifacts/rc_live/20260217-120000_logs/run_matrix_and_smoke_20260217-120000.log)
---
## 1. Build Reproducibility
### Status: PASS (après correction)
**Command**: `pio run -e <env>`
| Environment | Build 1 | Build 2 | Build 3 | Status |
|-------------|---------|---------|---------|--------|
| esp32dev | n/a (batch build) | n/a (batch build) | n/a (batch build) | PASS |
| esp32_release | n/a (batch build) | n/a (batch build) | n/a (batch build) | PASS |
| esp8266_oled | OK (SoftwareSerial D4/D5, écran/app screen refactorisé) | | | PASS |
| ui_rp2040_ili9488 | n/a (batch build) | n/a (batch build) | n/a (batch build) | PASS |
| ui_rp2040_ili9486 | n/a (batch build) | n/a (batch build) | n/a (batch build) | PASS |
**Notes**: 3/3 build cycles passed via `./tools/dev/cockpit.sh build`. Per-environment timings are not recorded in the build logs.
---
## 2. Flash Gate Reproducibility
### Status: PASS (après correction)
**Command**: `./tools/dev/cockpit.sh flash`
| Run | Port Config | Duration | Status | Notes |
|-----|------------|----------|--------|-------|
| 1 | Auto | n/a | PASS | Ports resolved (see logs) |
| 2 | Auto | n/a | PASS | Ports resolved (see logs) |
| 3 | Auto | n/a | PASS | Ports resolved (see logs) |
| 4 | Auto | n/a | PASS | Ports resolved (see logs) |
| 5 | Auto | n/a | PASS | Ports resolved (see logs) |
**Target**: 100% reproducibility (10/10 runs)
**Current**: `5/5`
**Issues found**:
- [ ] Port detection fails on RP2040
- [ ] Auto port resolution timeout
- [ ] Permission errors on /dev/ttyXXX
- [Other]
---
## 3. Smoke Test Results (après correction)
### Status: IN PROGRESS (UI link à revalider)
**command**: `./tools/dev/run_matrix_and_smoke.sh`
| Run | Duration | Build | Port Res | Smoke | UI Link | Panic? | Notes |
|-----|----------|-------|----------|-------|---------|--------|-------|
| 1 | n/a | ✓ (skipped) | ✓ | ✓ | ✗ | ❌ | UI link failed (`connected=1` missing) |
| 11 | n/a | ✓ (build OK, mapping SoftwareSerial D4/D5 validé) | ✓ | ✓ | ? | ? | Correction appliquée, artefacts générés, UI link à vérifier |
| 2 | n/a | ✓ (skipped) | ✓ | ✓ | ✗ | ❌ | UI link failed (`connected=1` missing) |
| 3 | n/a | ✓ (skipped) | ✓ | ✓ | ✗ | ❌ | UI link failed (`connected=1` missing) |
| 4 | n/a | ✓ (skipped) | ✓ | ✓ | ✗ | ❌ | UI link failed (`connected=1` missing) |
| 5 | n/a | ✓ (skipped) | ✓ | ✓ | ✗ | ❌ | UI link failed (`connected=1` missing) |
| 6 | n/a | ✓ (skipped) | ✓ | ✓ | ✗ | ❌ | UI link failed (`connected=1` missing) |
| 7 | n/a | ✓ (skipped) | ✓ | ✓ | ✗ | ❌ | UI link failed (`connected=1` missing) |
| 8 | n/a | ✓ (skipped) | ✓ | ✓ | ✗ | ❌ | UI link failed (`connected=1` missing) |
| 9 | n/a | ✓ (skipped) | ✓ | ✓ | ✗ | ❌ | UI link failed (`connected=1` missing) |
| 10 | n/a | ✓ (skipped) | ✓ | ✓ | ✗ | ❌ | UI link failed (`connected=1` missing) |
**Summary**:
- Build ESP8266 OLED corrigé, mapping SoftwareSerial D4/D5 validé
- Logique écran/app screen refactorisée
- Flash + RC Live relancé, artefacts générés
- UI link : à revalider sur artefacts
**Incident catalog (UI link failure)**:
- [smoke_001.log](artifacts/baseline_20260216_001/3_smoke_001-010/smoke_001.log): UI link check failed after serial smoke pass
- [smoke_002.log](artifacts/baseline_20260216_001/3_smoke_001-010/smoke_002.log): UI link check failed after serial smoke pass
- Pattern repeats across runs 3-10 with identical failure point
- [run_matrix_and_smoke_20260217-120000.log](artifacts/rc_live/20260217-120000_logs/run_matrix_and_smoke_20260217-120000.log): Correction appliquée, UI link à vérifier
**Repro steps (from baseline run)**:
1. Ensure ESP32 + ESP8266 USB present and detected.
2. Run `./tools/dev/cockpit.sh rc` (UI link check step).
3. Observe `UI link : FAILED` in rc_live summary, despite serial smoke passing.
---
## 4. Panic Markers Found
### Panic Incidents: `0`
No panic markers detected in smoke logs; failures are attributed to UI link status (`connected=1` missing).
If any panics detected, document each:
**Panic #1**
- Run: #6
- Time: 2026-02-16T12:34:56Z
- Context: [Build step / Test phase]
- Marker: `Guru Meditation Error: Core X panic'ed`
- Artifact: `artifacts/rc_live/[timestamp]/smoke_esp32.log` (lines XYZ)
- Root cause (if known): [Memory, task stack, WiFi state, etc.]
- Reproducible?: YES / NO / UNKNOWN
**Panic #2**
[Repeat for each panic]
---
## 5. WiFi Disconnect Reasons
### Disconnect Incidents: `unknown (health endpoints failed)`
**Evidence to review**: [artifacts/baseline_20260216_001/4_healthcheck/rtos_wifi_health.log](artifacts/baseline_20260216_001/4_healthcheck/rtos_wifi_health.log) + [smoke_001.log](artifacts/baseline_20260216_001/3_smoke_001-010/smoke_001.log)
| Incident | Reason Code | Label | Recovery Time | Context |
|----------|-------------|-------|----------------|---------|
| #1 | n/a | n/a | n/a | Health endpoints failed (no response) |
**Known disconnect patterns**:
- [ ] Consistent at specific step (e.g., WebSocket init)
- [ ] Random / intermittent
- [ ] Related to AP reboot / reset
- [ ] Related to signal strength dropping
- [Other]
---
## 6. RTOS Health Snapshot
### Command: `./tools/dev/rtos_wifi_health.sh`
**Metrics from healthcheck**:
```json
{
"error": "Health endpoints failed (no metrics collected)",
"url": "http://192.168.1.100:8080"
}
```
**Analysis**:
- Heap min threshold: unknown (health endpoints failed)
- Stack min threshold: unknown (health endpoints failed)
- WiFi disconnect count: unknown
**Red flags** (if any):
- [ ] Heap fragmentation detected (unknown)
- [ ] Stack watermark < 1 KB on any task (unknown)
- [ ] Frequent WiFi disconnects (unknown)
- [Other]
---
## 6.1 RTOS Task Audit (Code Review)
**Source**:
- [esp32_audio/src/runtime/radio_runtime.cpp](esp32_audio/src/runtime/radio_runtime.cpp)
- [esp32_audio/src/runtime/radio_runtime.h](esp32_audio/src/runtime/radio_runtime.h)
**Tasks created (name / stack / priority / core)**:
- TaskAudioEngine / 3072 / prio 4 / core 1
- TaskStreamNet / 4096 / prio 3 / core 0
- TaskStorageScan / 3072 / prio 2 / core 0
- TaskWebControl / 4096 / prio 2 / core 0
- TaskUiOrchestrator / 3072 / prio 2 / core 1
**Observations**:
- Stack high-water mark is tracked via `uxTaskGetStackHighWaterMark` in `taskSnapshots()`.
- WDT is enabled and each task calls `esp_task_wdt_reset()` on its loop.
- No runtime telemetry captured in baseline because health endpoints failed.
---
## 7. Evidence Artifacts
**All baseline evidence saved under**:
```
artifacts/baseline_20260216_001/
├── 1_build/
├── 2_flash_tests/
├── 3_smoke_001-010/
│ ├── smoke_001/
│ │ ├── summary.md
│ │ └── meta.json
│ ├── smoke_002/
│ └── ... (9 more)
├── 4_healthcheck/
└── README.md (this file)
```
**Key evidence logs**:
- [logs/generate_baseline_20260216-063753.log](logs/generate_baseline_20260216-063753.log)
- [artifacts/baseline_20260216_001/4_healthcheck/rtos_wifi_health.log](artifacts/baseline_20260216_001/4_healthcheck/rtos_wifi_health.log)
---
## 8. Phase 2 Issue Drafts (For Tracking)
**Issue Draft 1: UI link check fails across all smoke runs**
- **Symptom**: UI link check fails with `connected=1` missing; serial smoke passes.
- **Evidence**: [smoke_001.log](artifacts/baseline_20260216_001/3_smoke_001-010/smoke_001.log) and [smoke_002.log](artifacts/baseline_20260216_001/3_smoke_001-010/smoke_002.log)
- **Repro**: `./tools/dev/cockpit.sh rc` with ESP32 + ESP8266 attached.
- **Hypotheses**:
- UI link status not emitted on ESP32 side (UI link monitor).
- ESP8266 UI firmware not responding at expected baud (57600 internal).
- UI link check expects `UI_LINK_STATUS connected=1` but output format changed.
**Issue Draft 2: RTOS/WiFi health endpoints failing**
- **Symptom**: `/api/status`, `/api/wifi`, `/api/rtos` fail (no response).
- **Evidence**: [artifacts/baseline_20260216_001/4_healthcheck/rtos_wifi_health.log](artifacts/baseline_20260216_001/4_healthcheck/rtos_wifi_health.log)
- **Repro**: `ESP_URL=http://192.168.1.100:8080 ./tools/dev/rtos_wifi_health.sh`
- **Hypotheses**:
- ESP32 not reachable on network during baseline window.
- Web server not started or blocked by runtime mode.
- WiFi service not connected or DHCP not assigned.
**How to regenerate baseline**:
```bash
cd hardware/firmware
mkdir -p artifacts/baseline_$(date +%Y%m%d)_001-010
# Build 3 times
for i in 1 2 3; do
echo "[Build $i]"
./tools/dev/cockpit.sh build 2>&1 | tee artifacts/baseline_*/build_$i.log
done
# Flash tests
for i in 1 2 3 4 5; do
echo "[Flash test $i]"
./tools/dev/cockpit.sh flash 2>&1 | tee artifacts/baseline_*/flash_$i.log
done
# Smoke 10 times
for i in {1..10}; do
echo "[Smoke run $i]"
./tools/dev/cockpit.sh rc 2>&1 | tee artifacts/baseline_*/smoke_$(printf '%03d' $i).log
done
# Health check
ESP_URL=http://192.168.1.100:8080 ./tools/dev/rtos_wifi_health.sh
```
---
## 9. Known Issues & Limitations
### Issue #1: UI link gate failing (`connected=1` missing)
- Status: UNKNOWN ROOT CAUSE
- Frequency: Every run (10/10)
- Workaround: Attach UI board and verify cabling/power before RC
- Blocker?: YES
### Issue #2: RTOS/WiFi health endpoints unreachable
- Status: UNKNOWN ROOT CAUSE
- Frequency: Once (during baseline)
- Workaround: Verify ESP_URL and API availability before running health check
- Blocker?: YES (blocks metrics collection)
---
## 10. Success Criteria Assessment
| Criterion | Target | Actual | Status |
|-----------|--------|--------|--------|
| Panic-free sessions | 100% (10/10) | `10/10` | ✓ |
| Build reproducibility | 100% (3 builds each) | 100% | ✓ |
| Flash reproducibility | 100% (5 runs) | 100% | ✓ |
| Smoke baseline duration | ~40 sec | n/a (not recorded) | ❌ |
| WiFi disconnect 0 | (during baseline) | unknown | ❌ |
| Heap min > 16 KB | Always | unknown | ❌ |
| Evidence complete | All artifacts | Baseline logs present | ✓ |
**Overall Status**: 🔴 RED
---
## 11. Recommendations (Next Steps)
**Phase 2 priorities** (based on baseline findings):
1. **UI link gate failure (10/10)**
- Verify ESP32 <-> UI link wiring and power
- Re-run `./tools/dev/cockpit.sh rc` with UI attached and confirm `UI_LINK_STATUS connected=1`
- Inspect ui_link logs in `artifacts/rc_live/*/ui_link.log` for the failing condition
2. **RTOS/WiFi health endpoints unreachable**
- Confirm ESP is reachable at `ESP_URL` and the API endpoints are exposed
- Re-run `./tools/dev/rtos_wifi_health.sh` after successful RC run
- Capture the health JSON to populate metrics
## 12. Mode dégradé : ESP32/ESP8266 uniquement (sans UI)
### Contexte
Ce mode correspond à une configuration où seuls les modules ESP32 et ESP8266 sont présents, sans carte UI (RP2040/UI). Il s'agit d'un scénario de test ou de dépannage permettant de valider la robustesse des bases firmware et la disponibilité des endpoints critiques, même en l'absence de l'interface utilisateur.
### Constats
- **RC Live** : Le test RC s'exécute jusqu'au bout, mais échoue systématiquement sur la gate UI link (`UI_LINK_STATUS connected=1` absent), ce qui est attendu sans UI.
- **Logs & artefacts** : Les logs (`ui_link.log`, `ports_resolve.json`) confirment la détection correcte des ports ESP32/ESP8266 et l'absence de dialogue UI.
- **Endpoints REST** : Les endpoints critiques (ex : `/api/status`) restent accessibles et répondent correctement côté ESP32, validant la pile réseau et le serveur HTTP embarqué.
- **Aucun panic** : Aucun marqueur de panic ou reboot détecté dans les logs série.
### Limitations
- **UI link** : Impossible de valider la gate UI link sans la carte UI. Tous les tests dépendant de l'UI sont en échec attendu.
- **Santé RTOS/WiFi** : Les métriques avancées (heap, stack, WiFi disconnect) restent inaccessibles si l'UI est requise pour leur exposition.
- **Expérience utilisateur** : Ce mode ne permet pas de valider l'expérience complète (orchestration, affichage, transitions UI).
### Recommandations
- Utiliser ce mode pour valider la stabilité de base (boot, réseau, endpoints, absence de panic) avant d'intégrer la carte UI.
- Documenter explicitement tout échec de gate lié à l'absence d'UI comme "attendu" dans les rapports.
- Prévoir une relance complète des tests RC/Smoke dès que la carte UI est disponible pour valider la chaîne complète.
### Statut
**Mode dégradé validé** : Les modules ESP32/ESP8266 fonctionnent nominalement en l'absence d'UI, à l'exception des gates explicitement dépendantes de l'interface utilisateur.
## Sign-Off
| Role | Name | Date | Sign-off |
|------|------|------|----------|
| Firmware Embedded Expert | [Name] | 2026-02-16 | ☐ |
| Test & Script Coordinator | [Name] | 2026-02-16 | ☐ |
| Project Manager | [Name] | 2026-02-16 | ☐ |
---
**Document Version**: 1.0
**Last Updated**: 2026-02-16
**Status**: DRAFT
---
### Appendix: Evidence File Descriptions
- **summary.json**: Structured RC test results (exit_code, build_status, smoke_status, ui_link_status)
- **run_matrix_and_smoke.log**: Main test execution log (build + port resolution + smoke)
- **smoke_esp32.log**: ESP32 serial output during smoke tests
- **smoke_esp8266_usb.log**: ESP8266 serial output (USB variant)
- **build_esp32dev.log**: PlatformIO build output
- **ui_link.log**: UI link status checks (expected: "connected=1")
- **ports_resolve.json**: Auto-detected serial port mapping
---
**Need help?** See [docs/QUICKSTART.md](QUICKSTART.md) or [docs/RTOS_WIFI_HEALTH.md](RTOS_WIFI_HEALTH.md)
-31
View File
@@ -1,31 +0,0 @@
# Custom Agent Phase Launch Plan
## Scope
Execution plan for gating, artifacts, and reporting required to open a new phase (feature milestone, release candidate, etc.).
## Do
- Define the gate list early: include the PlatformIO matrix, smoke/stress scripts, scenario/audio/printables validators, and any additional QC scripts noted in `docs/TEST_SCRIPT_COORDINATOR.md`.
- Capture every artifact/log path under `artifacts/` and `hardware/firmware/logs/`, writing their metadata (`meta.json`, `commands.txt`, `summary.md`) before closing the phase.
- Mention UI Link, WebSocket, HTTP, and I2S health verdicts in `docs/AGENT_TODO.md` and `docs/TEST_SCRIPT_COORDINATOR.md` along with the `artifacts/<phase>` references.
- Summarize the phase status in `docs/RC_FINAL_REPORT_TEMPLATE.md` (if applicable) and note any blockers in `docs/RC_AUTOFIX_CICD.md`.
- Capture the gate/artifact summary and verification commands in `GIT_WRITE_OPS_FINAL_REPORT.md` or `docs/AGENT_TODO.md` so the phase status is documented.
## Must Not
- Leave gates undocumented or skip the regression checks listed in the root `AGENTS.md` or `hardware/firmware/AGENTS.md`.
- Ship artifacts/logs without logging them in the evidence tracker.
## References
- `docs/TEST_SCRIPT_COORDINATOR.md`
- `docs/AGENT_TODO.md`
- `docs/RC_FINAL_REPORT_TEMPLATE.md`
- `docs/RC_AUTOFIX_CICD.md`
## Plan daction
1. Définir et exécuter les gates clés.
- run: bash hardware/firmware/tools/dev/run_matrix_and_smoke.sh
- run: bash hardware/firmware/tools/dev/run_smoke_tests.sh
- run: python3 hardware/firmware/tools/dev/run_stress_tests.py --allow-no-hardware
2. Documenter artefacts/logs + verdicts réseau.
- run: python3 tools/test/audit_coherence.py
- run: cat GIT_WRITE_OPS_FINAL_REPORT.md
-19
View File
@@ -1,19 +0,0 @@
# Modèle de plan daction RTC_BL_PHONE
Chaque fiche `.github/agents/<agent>.md` doit inclure une section `## Plan daction` structurée ainsi:
1. **Contexte et validation**
- Expliquer le contexte de laction (matériel, firmware, doc, CI, etc.).
- run: <commande de vérification initiale, ex : `git status -sb`>
2. **Étapes principales**
- Détailler chaque étape avec une phrase claire.
- run: <commande 1>
- run: <commande 2>
3. **Reporting et documentation**
- Préciser où consigner les résultats (README, docs, changelog, etc.).
- run: <commande pour générer ou archiver les artefacts, ex : `python3 tools/dev/gen_cockpit_docs.py`>
> **Référence**: Ce plan doit respecter le référentiel de conventions du projet (voir `CONVENTIONS.md`).
-61
View File
@@ -1,61 +0,0 @@
> **Référence: voir aussi `.github/agents/CONVENTIONS.md` pour les conventions à respecter.**
# Agent Audio RTC_BL_PHONE
## Périmètre
Gestion de laudio (I2S, ES8388, SLIC K50835F, ADC/DAC internes) pour le téléphone RTC sur ESP32.
## À faire
- Documenter le schéma de câblage audio réel (voir docs/solutions_rtc_phone_esp32.md).
- Vérifier la configuration I2S (pins, codec ES8388, SLIC, ADC/DAC internes).
- Sassurer que les tests audio (loopback, capture, playback) sont reproductibles sur chaque hardware supporté.
- Mettre à jour la documentation audio à chaque évolution hardware/firmware.
## À ne pas faire
- Ne pas générer ou valider des assets audio binaires inutiles (pas de pipeline audio automatisé dans ce projet).
- Ne pas référencer dID ou de manifestes obsolètes (ex: zacus_v1_audio.yaml).
## Références
- docs/solutions_rtc_phone_esp32.md
- docs/fiche_agent_audio_tools.md
- Schémas de câblage dans docs/
- Exemples dinitialisation I2S dans le firmware (src/main.cpp)
## Plan daction
1. Vérifier le câblage et la config I2S/codec à chaque changement hardware.
2. Tester la capture et la lecture audio sur chaque plateforme (ESP32-DevKitC, Audio Kit, etc.).
3. Mettre à jour la doc audio et signaler toute divergence dans le README ou docs/solutions_rtc_phone_esp32.md.
## Architecture audio RTC_BL_PHONE
### Abstraction AudioCodec
- Interface pour codecs audio (I2S/I2C), méthodes : init, setVolume, mute, setRoute.
- Implémentations : ES8388 (I2S+I2C), PCM5102 (I2S), GenericCodec (fallback/tests).
- Routage audio RTC/Bluetooth via setRoute.
### ES8388
- Initialisation I2S + I2C.
- Volume/mute/routage via registres.
- Points hardware : alimentation, ESD, mapping pins.
### PCM5102
- Initialisation I2S.
- Volume/mute via atténuation I2S ou pin externe.
- Routage externe.
### Tests
- MockCodec pour tests unitaires.
- test_audio_codec.cpp : vérification init, volume, mute, routage.
### Sécurité
- Mapping pins selon ESP32/ESP32-S3.
- Filtrage alimentation, protection ESD.
### Limitations et recommandations
- ES8388: nécessite une initialisation I2C en plus dI2S (utiliser une librairie dédiée, ex. ESP-ADF).
- La gestion du volume/routage dépend du hardware (ES8388, PCM5102, GenericCodec).
- Toujours tester le loopback audio sur chaque plateforme.
- Vérifier la protection ESD et lalimentation.
- Documenter toute divergence hardware/firmware dans README et docs/solutions_rtc_phone_esp32.md.
- Logs série obligatoires pour tout bug ou anomalie.
-26
View File
@@ -1,26 +0,0 @@
> **Référence: voir aussi `.github/agents/CONVENTIONS.md` pour les conventions à respecter.**
# Agent CI RTC_BL_PHONE
## Périmètre
Workflows GitHub Actions sous `.github/workflows/` (firmware-ci.yml, firmware-story-v2.yml).
## À faire
- Garder les workflows simples, explicites et adaptés à PlatformIO.
- Vérifier la syntaxe YAML et la validité des chemins avant chaque commit.
- Documenter toute modification de workflow dans le README ou un changelog.
## À ne pas faire
- Ne pas supprimer ou renommer massivement des workflows sans justification.
- Ne pas modifier les licences ou les checks critiques sans validation.
## Références
- .github/workflows/firmware-ci.yml
- .github/workflows/firmware-story-v2.yml
- README.md
## Plan daction
1. Vérifier la syntaxe et les chemins de chaque workflow modifié.
2. Sassurer que les builds PlatformIO passent sur toutes les cibles supportées.
3. Documenter toute évolution CI dans le projet.
-73
View File
@@ -1,73 +0,0 @@
> **Référence: voir aussi `.github/agents/CONVENTIONS.md` pour les conventions à respecter.**
# Agent Documentation RTC_BL_PHONE
## Périmètre
Documentation utilisateur et technique sous `docs/`: schémas, guides de câblage, choix hardware, structure firmware, sécurité, etc.
## À faire
## À ne pas faire
## Références
## Délégation agents RTC_BL_PHONE
### Agent Firmware
- Modularisation, code source, tests unitaires, intégration hardware/audio, maintenance, CI.
- Surveille PR, valide builds, garantit cohérence des classes.
### Agent Hardware
- Documente câblage, variantes matérielles, sécurité, compatibilité ESP32/ESP32-S3.
- Valide schémas, teste interfaces, signale risques.
### Agent Audio
- Gère abstraction AudioCodec, intégration I2S, configuration ES8388/PCM5102, documentation audio.
- Teste codecs, valide routage RTC/Bluetooth, maintient doc audio.md.
### Agent Documentation
- Rédige, met à jour et garantit cohérence des docs.md, README.md, solutions_rtc_phone_esp32.md.
- Garantit clarté, validité des liens, actualisation.
### Agent CI/QA
- Gère CI PlatformIO, validation builds, tests automatisés, traçabilité des livrables.
- Surveille workflows, signale erreurs, archive rapports de test.
### Agent Global/Conventions
- Veille au respect des conventions, arborescence, sécurité, documentation.
- Audite le projet, propose améliorations, garantit cohérence globale.
Chaque agent rend compte dans son fichier dédié, propose des améliorations et signale toute anomalie.
## Documentation technique : AudioCodec, ES8388, PCM5102
### Abstraction AudioCodec
- Interface pour codecs audio (I2S/I2C), méthodes : init, setVolume, mute, setRoute.
- Extensible : chaque codec = classe dérivée.
- Testabilité : mock intégré, test unitaire via test_audio_codec.cpp.
### ES8388
- Initialisation I2S + I2C.
- Volume/mute/routage via registres (ex : 0x2B, 0x2C, 0x2F, 0x30).
- Routage RTC/Bluetooth configurable.
- Points hardware : alimentation stable, protection ESD, mapping pins.
### PCM5102
- Initialisation I2S.
- Volume/mute via atténuation I2S ou pin externe.
- Routage externe (multiplexeur/relais).
- Points hardware : niveau logique, alimentation, protection.
### Routage audio
- Méthode setRoute : bascule RTC/Bluetooth.
- Prévoir isolation, multiplexage ou relais.
### Testabilité
- MockCodec pour tests unitaires.
- test_audio_codec.cpp : vérification init, volume, mute, routage.
### Sécurité et bonnes pratiques
- Vérifier mapping pins selon ESP32/ESP32-S3.
- Filtrage alimentation, protection ESD.
- Logs détaillés, gestion erreurs.
-19
View File
@@ -1,19 +0,0 @@
# Custom Agent Firmware Copilot
## Scope
Copilot-focused firmware duties described in `hardware/firmware/AGENTS_FIRMWARE.md`.
## Do
- Always update `hardware/firmware/docs/AGENT_TODO.md` before acting and log UI Link, LittleFS, and I2S status per session.
- Store artifacts under `artifacts/<phase>/<timestamp>` and mention every path in the TODO/runbook reporting template.
- Refer to `docs/SPRINT_RECOMMENDATIONS.md`, `docs/TEST_SCRIPT_COORDINATOR.md`, and `protocol/ui_link_v2.md` for branch-level expectations.
## References
- `hardware/firmware/AGENTS_FIRMWARE.md`
## Plan daction
1. Mettre à jour AGENT_TODO et lister UI Link.
- run: python3 tools/dev/serial_smoke.py --role auto --wait-port 3 --allow-no-hardware
2. Capturer artefacts dans `artifacts/` et `logs/`.
- run: bash hardware/firmware/tools/dev/run_smoke_tests.sh --allow-no-hardware
-28
View File
@@ -1,28 +0,0 @@
> **Référence: voir aussi `.github/agents/CONVENTIONS.md` pour les conventions à respecter.**
# Agent Firmware Core RTC_BL_PHONE
## Périmètre
Tout le code source firmware sous `src/` et la configuration PlatformIO (`platformio.ini`).
## À faire
- Compiler et flasher le firmware via PlatformIO (environnements: esp32dev, esp32-s3-devkitc-1, etc.).
- Documenter toute évolution majeure dans le README et docs/solutions_rtc_phone_esp32.md.
- Vérifier la configuration I2S, ES8388, SLIC, et la gestion des GPIO (hook, ring, line enable).
- Sassurer que la machine d’états (PhoneState) et la logique RTC sont testées à chaque commit.
- Ajouter des logs série pour tout bug ou comportement inattendu.
## À ne pas faire
- Ne pas committer de binaires, logs ou artefacts de build dans le repo.
- Ne pas référencer de scripts ou chemins absents du projet.
## Références
- README.md
- docs/solutions_rtc_phone_esp32.md
- src/main.cpp
- platformio.ini
## Plan daction
1. Compiler le firmware pour chaque cible supportée.
2. Flasher et tester la logique RTC (hook, ring, line, audio) sur hardware réel.
3. Mettre à jour la documentation à chaque évolution significative.
-19
View File
@@ -1,19 +0,0 @@
# Custom Agent Firmware Docs
## Scope
Firmware-facing documentation, onboarding guides, and generated command indexes.
## Do
- Sync `tools/dev/cockpit_commands.yaml` with `docs/_generated/COCKPIT_COMMANDS.md` via `python3 tools/dev/gen_cockpit_docs.py` when commands change.
- Mention updated gates/tests/artifacts in `hardware/firmware/docs/TEST_SCRIPT_COORDINATOR.md`, `hardware/firmware/docs/TEST_COHERENCE_AUDIT_RUNBOOK.md`, and related runbooks.
- Keep `hardware/firmware/docs/AGENT_TODO.md` updated whenever onboarding structure or tooling workflows change.
## References
- `hardware/firmware/AGENTS_DOCS.md`
## Plan daction
1. Régénérer lindex cockpit.
- run: python3 tools/dev/gen_cockpit_docs.py
2. Vérifier les gate/runbook updates.
- run: rg -n 'TEST_SCRIPT_COORDINATOR.md' hardware/firmware/docs
-26
View File
@@ -1,26 +0,0 @@
> **Référence: voir aussi `.github/agents/CONVENTIONS.md` pour les conventions à respecter.**
# Agent Firmware Tests RTC_BL_PHONE
## Périmètre
Tests unitaires et fonctionnels du firmware (src/), vérification des GPIO, de la logique RTC, et de laudio (I2S, ES8388, SLIC).
## À faire
- Écrire et exécuter des tests unitaires pour la machine d’états (PhoneState), la gestion du hook, ring, line enable.
- Vérifier le comportement des GPIO sur hardware réel (décroché, sonnerie, activation ligne).
- Tester la capture et la lecture audio (I2S/codec/ADC/DAC) sur chaque plateforme supportée.
- Documenter les résultats de test dans README.md ou docs/solutions_rtc_phone_esp32.md.
## À ne pas faire
- Ne pas référencer de scripts ou chemins absents du projet.
- Ne pas committer dartefacts de test ou de logs binaires.
## Références
- src/main.cpp
- README.md
- docs/solutions_rtc_phone_esp32.md
## Plan daction
1. Écrire des tests unitaires pour chaque composant critique (états, GPIO, audio).
2. Exécuter les tests sur hardware réel à chaque évolution majeure.
3. Reporter les résultats et bugs dans la documentation projet.
-24
View File
@@ -1,24 +0,0 @@
# Custom Agent Firmware Tooling
## Scope
`hardware/firmware/tools/dev/**` automation helpers.
## Do
- Obey `tools/dev/AGENTS.md`: expose `--help`, keep CLI output `[step]/[ok]/[fail]` friendly, and resolve ports/timeouts via flags or env.
- Emit logs to `hardware/firmware/logs/` and provide timestamped filenames for traceability.
- Keep scripts non-interactive when possible and surface a short, grep-friendly summary.
## Must Not
- Skip recording logs/commands in `hardware/firmware/logs/` or the runbook (`docs/AGENT_TODO.md`).
- Hardcode static port/device names that would break on other machines.
## References
- `hardware/firmware/docs/AGENT_TODO.md`
- `hardware/firmware/tools/dev/AGENTS.md`
## Plan daction
1. Vérifier les helpers avec `--help`.
- run: PATH=$(pwd)/hardware/firmware/.venv/bin:$PATH python3 hardware/firmware/tools/dev/serial_smoke.py --help
- run: PATH=$(pwd)/hardware/firmware/.venv/bin:$PATH bash hardware/firmware/tools/dev/run_smoke_tests.sh --help
2. Lancer la matrice pour confirmer les logs.
- run: PATH=$(pwd)/hardware/firmware/.venv/bin:$PATH bash hardware/firmware/tools/dev/run_matrix_and_smoke.sh --help
-36
View File
@@ -1,36 +0,0 @@
> **Référence: voir aussi `.github/agents/CONVENTIONS.md` pour les conventions à respecter.**
# Agent Global RTC_BL_PHONE
## Périmètre
Gestion globale du projet: cohérence documentation, firmware, hardware, outils, CI et sécurité.
## À faire
- Garder le dépôt toujours buildable et cohérent (PlatformIO, docs, schémas, scripts).
- Vérifier la cohérence entre la documentation, le firmware (src/), le hardware (docs/), et les outils.
- Sassurer que chaque commit majeur est documenté (README, docs/solutions_rtc_phone_esp32.md).
- Exécuter la matrice PlatformIO sur toutes les cibles supportées avant toute release.
## À ne pas faire
- Ne pas utiliser de commandes destructives ou toucher aux licences sans validation explicite.
- Ne pas laisser de divergences entre la doc, le code et le hardware.
## Références
- README.md
- docs/solutions_rtc_phone_esp32.md
- platformio.ini
## Délégation agents et synergie
### Rôles principaux
- Agent Firmware : modularisation, tests, intégration hardware/audio, CI.
- Agent Hardware : câblage, sécurité, compatibilité ESP32/ESP32-S3.
- Agent Audio : abstraction AudioCodec, routage, documentation audio.
- Agent Documentation : rédaction, mise à jour, cohérence documentation.
- Agent CI/QA : validation builds, tests automatisés, traçabilité.
- Agent Global/Conventions : audit, amélioration, cohérence.
### Synergie
- Les agents travaillent en synergie pour garantir la qualité, la sécurité et la maintenabilité.
- Chaque agent rend compte dans son fichier dédié, propose des améliorations et signale toute anomalie.
-26
View File
@@ -1,26 +0,0 @@
> **Référence: voir aussi `.github/agents/CONVENTIONS.md` pour les conventions à respecter.**
# Agent Hardware RTC_BL_PHONE
## Périmètre
Documentation matérielle: câblage ESP32, SLIC K50835F, ES8388, schémas, choix de DevKit, sécurité et tests hardware.
## À faire
- Documenter précisément le schéma de câblage et les variantes supportées (voir docs/solutions_rtc_phone_esp32.md).
- Vérifier la cohérence entre le schéma hardware et la configuration firmware (pins, I2S, ADC/DAC, GPIO).
- Mettre à jour la documentation à chaque évolution matérielle ou changement de routage.
- Tester le hardware (hook, ring, audio, sécurité) sur chaque plateforme supportée.
## À ne pas faire
- Ne pas committer de schémas ou photos non validés.
- Ne pas référencer de scripts ou chemins absents du projet.
## Références
- docs/solutions_rtc_phone_esp32.md
- README.md
## Plan daction
1. Mettre à jour le schéma de câblage à chaque modification hardware.
2. Vérifier la correspondance entre hardware et firmware (pins, signaux, sécurité).
3. Documenter les tests et les problèmes rencontrés dans la doc projet.
-25
View File
@@ -1,25 +0,0 @@
> **Référence: voir aussi `.github/agents/CONVENTIONS.md` pour les conventions à respecter.**
# Agent Outils RTC_BL_PHONE
## Périmètre
Scripts et outils sous `tools/` utiles pour le développement, la validation ou le debug du projet.
## À faire
- Documenter les flags et options de chaque script utile (ex: --help, ports, timeouts).
- Garder les scripts non-interactifs par défaut, configurables par arguments ou variables denvironnement.
- Vérifier que les scripts sont cohérents avec larchitecture réelle du projet.
## À ne pas faire
- Ne pas hardcoder de chemins ou ports spécifiques à une machine.
- Ne pas forcer linteraction utilisateur si un flag ou une attente CLI suffit.
## Références
- tools/
- README.md
## Plan daction
1. Vérifier et documenter laide de chaque script modifié.
2. Sassurer que les scripts sont utilisables sur toute plateforme supportée.
3. Mettre à jour la doc outils à chaque ajout ou modification significative.
@@ -1,25 +0,0 @@
## Plan : Roadmap hardware/firmware priorisée par plateforme
### 1. ESP32
- **Build/Flash** : Scripts et gates déjà factorisés, structure conforme. Priorité : maintenir la reproductibilité, surveiller les évolutions PlatformIO.
- **Tests/Smoke** : Vérifier la couverture des tests hardware (smoke, reboot, panic). Ajouter des tests de drivers spécifiques si besoin (UART, I2C, SPI, GPIO, SLIC).
- **Drivers** : Sassurer que tous les capteurs/actuateurs nécessaires sont intégrés et documentés. Priorité aux drivers critiques (communication, sécurité, alimentation).
- **CI/CD** : Maintenir la compatibilité avec la CI, artefacts reproductibles, logs clairs.
### 4. Codex/Auto-fix
- **Prompts** : Centraliser et documenter tous les prompts dans codex_prompts/.
- **Intégration** : Sassurer que lauto-fix fonctionne sur toutes les plateformes, et que les logs/artefacts sont bien générés.
- **Tests** : Ajouter des scénarios de test auto-fix pour chaque plateforme.
### 5. Commun (logs, artefacts, onboarding)
- **Logs/Artefacts** : Maintenir la centralisation, la rotation, et la clarté des logs/artefacts pour chaque plateforme.
- **Onboarding** : Adapter les instructions pour chaque cible
- **CI** : Vérifier que chaque plateforme est bien couverte par la CI (build, smoke, artefacts, logs).
---
**Décisions**
- Priorité à la robustesse ESP32 (build, drivers, tests), p
- Harmonisation et documentation continue pour chaque plateforme.
- Audit et automatisation réguliers pour garantir la conformité AGENTS.md.
-48
View File
@@ -1,48 +0,0 @@
name: Firmware CI
on:
push:
branches: [main, release/stable]
pull_request:
branches: [main, release/stable]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Test Web Route Parity Parser
run: python3 -m unittest scripts/test_check_web_route_parity.py
- name: Check Web Route Parity
run: python3 scripts/check_web_route_parity.py --report-json artifacts/route_parity_report.json
- name: Upload route parity report
if: always()
uses: actions/upload-artifact@v4
with:
name: route-parity-report
path: artifacts/route_parity_report.json
if-no-files-found: warn
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install platformio
- name: Validate hw runner syntax
run: python -m py_compile scripts/hw_validation.py
- name: Build firmware
run: platformio run -e esp32dev
- name: Build unit tests (no upload)
run: platformio test --without-uploading --without-testing -e esp32dev
-27
View File
@@ -1,27 +0,0 @@
---
name: Release
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install platformio
- name: Build firmware
run: platformio run -e esp32dev -e esp32-s3-devkitc-1
- name: Publish release
uses: softprops/action-gh-release@v1
with:
files: |
.pio/build/esp32dev/*.bin
.pio/build/esp32-s3-devkitc-1/*.bin
-30
View File
@@ -1,30 +0,0 @@
name: Repo State
on:
push:
pull_request:
workflow_dispatch:
jobs:
repo-state:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Generate repo state
run: |
python3 tools/repo_state/collect.py --repo-name RTC_BL_PHONE
- name: Upload repo-state artifact
uses: actions/upload-artifact@v4
with:
name: repo-state
path: |
docs/REPO_STATE.md
docs/repo_state.json
+16
View File
@@ -426,3 +426,19 @@ coverage/
.history/
*.swp
*~
# Project data
# If data/audio was previously committed, run:
# git rm --cached -r data/audio
data/audio/
docs/specs/tone_plan_wav_assets/assets
docs/wav_clean_8k/
docs/wav_ptt_vintage_8k/
# ESP-IDF
/build/
/sdkconfig
/sdkconfig.old
/dependencies.lock
/managed_components/
+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` |
+49
View File
@@ -0,0 +1,49 @@
# RTC_BL_PHONE
Firmware **ESP-IDF** (v5.4) d'un téléphone RTC à cadran transformé en **kit
mains-libres Bluetooth** (HFP-HF). On décroche le combiné pour répondre/composer ;
l'audio est ponté entre le SCO Bluetooth du mobile et le codec local ES8388.
## Build & Flash
ESP-IDF + composants/CMake. Cible : **ESP32 classique** (carte A1S AudioKit, ES8388).
```bash
. $IDF_PATH/export.sh
idf.py build
idf.py -p /dev/cu.usbserial-0001 flash monitor # Ctrl+] pour quitter
```
Pas de PlatformIO/Arduino : le build est piloté par `CMakeLists.txt` racine +
`main/` + `components/` (auto-découverts par `project.cmake`).
## Réglage à la volée (CLI série, 115200)
Le firmware expose une CLI texte pour régler l'audio sans reflash (persistance NVS) :
`gspk N`, `gmic N`, `filt 0/1`, `aec 0/1/2`, `aecd N`, `aecmu N`, `status`.
Enregistrée dans `components/cli/`. TUI host : `tools/phone_tui.py`.
## Où chercher
| Tâche | Emplacement |
|-------|-------------|
| Point d'entrée, wiring boot, power management | `main/app_main.c` |
| Codec ES8388 (I2C, volume, veille) | `components/hal_es8388/` |
| I2S full-duplex + capture micro | `components/hal_i2s/` |
| Tonalités, pont SCO, gains, filtres biquad, AEC | `components/audio_router/` |
| DTMF (Goertzel) | `components/dtmf/` |
| Cadran à impulsions, crochet | `components/dialer/`, `components/hook_monitor/` |
| Machine d'état d'appel (FSM + file BT) | `components/call_manager/` |
| Interface ligne SLIC (KS0835) | `components/slic_ks0835/` |
| Pile Bluetooth HFP-HF (Bluedroid) | `components/bt_hfp/` |
| CLI série | `components/cli/` |
| Specs, plans, notes | `docs/superpowers/` |
## Conventions
- Drivers **nouvelle API ESP-IDF v5** uniquement (`driver/i2c_master.h`,
`driver/i2s_std.h`…), jamais l'API legacy.
- Audio temps réel : tâches FreeRTOS + rings + queues ; l'audio tourne sur
l'horloge I2S, indépendante des polls des tâches.
- Économie d'énergie active (batterie) : codec/I2S en veille au repos, DFS +
light sleep, poll des tâches adaptatif (rapide en appel, lent au repos).
+3
View File
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(rtc_bl_phone)
-28
View File
@@ -1,28 +0,0 @@
# Intégration Makefile : flash universel
# Variables de base
FLASH_SCRIPT = tools/dev/autoflash.py
FLASH_CONFIG = tools/dev/flash_config.json
# Liste des rôles connus (adapter selon ton flash_config.json)
FLASH_ROLES = esp32 esp8266 esp32s3 rp2040 stm32 arduino
# Cible générique : make flash ROLE=esp32
flash:
@if [ -z "$(ROLE)" ]; then \
echo "Usage : make flash ROLE=<$(FLASH_ROLES)>"; exit 1; \
fi; \
$(FLASH_SCRIPT) flash --config $(FLASH_CONFIG) --role $(ROLE)
# Cible pour lister les devices et rôles
flash-list:
$(FLASH_SCRIPT) list --config $(FLASH_CONFIG)
# Cible dry-run (voir ce qu'il ferait)
flash-dry:
@if [ -z "$(ROLE)" ]; then \
echo "Usage : make flash-dry ROLE=<$(FLASH_ROLES)>"; exit 1; \
fi; \
$(FLASH_SCRIPT) flash --config $(FLASH_CONFIG) --role $(ROLE) --dry-run
.PHONY: flash flash-list flash-dry
+49 -325
View File
@@ -1,344 +1,68 @@
# RTC_BL_PHONE
Projet ESP32 : téléphone RTC, SLIC, audio, Bluetooth, WiFi, agentic.
Firmware **ESP-IDF** transformant un téléphone RTC à cadran en **kit mains-libres
Bluetooth** (HFP-HF). On décroche le combiné pour répondre ou composer, l'audio
est ponté entre le SCO Bluetooth du mobile et le codec local ES8388.
## CI/CD automatisé
Le pipeline CI/CD est géré par GitHub Actions et PlatformIO:
## Matériel
- **Déclenchement** : à chaque push ou pull request sur `main` ou `develop`.
- **Build** : compilation automatique du firmware via PlatformIO.
- **Tests** : exécution des tests unitaires avec `platformio test`.
- **Artefacts** : génération et upload automatique des binaires compilés.
- **Couverture** : rapport de couverture (optionnel, si supporté).
- **Livraison** : artefacts accessibles dans longlet Actions > workflow CI PlatformIO.
- **ESP32 classique** (carte Ai-Thinker A1S AudioKit, 8 MB PSRAM)
- Codec audio **ES8388** (I2S full-duplex, I2C de contrôle)
- Interface ligne **SLIC K50835F** (détection crochet/SHK, sonnerie)
- Cadran à impulsions + détection DTMF (Goertzel)
- Port série usuel : `/dev/cu.usbserial-0001` à 115200 bauds
### Structure du workflow
Le fichier `.github/workflows/ci.yml` contient:
## Fonctionnement
```yaml
name: CI PlatformIO
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install PlatformIO
run: pip install platformio
- name: Run PlatformIO tests
run: platformio test
- name: Build firmware
run: platformio run
- name: Upload firmware artifact
uses: actions/upload-artifact@v3
with:
name: firmware
path: .pio/build/*/*.bin
# Optionnel: Génération de la couverture si supportée
# - name: Generate coverage report
# run: platformio test --coverage
# - name: Upload coverage artifact
# uses: actions/upload-artifact@v3
# with:
# name: coverage
# path: coverage-report/*
```
Rôle **Hands-Free Unit (HFP-HF)** : c'est l'ESP32 qui se connecte au mobile.
### Livraison
Après chaque build, les binaires sont disponibles en téléchargement dans les artefacts du workflow.
- Appairage « Just Works » (sous le nom `RTC_BL_PHONE`), reconnexion auto au
mobile appairé.
- **Appel entrant** : la cloche SLIC sonne jusqu'au décroché ou la fin d'appel ;
décrocher répond et ouvre le pont audio.
- **Appel sortant** : décrocher → tonalité → composer (impulsion ou DTMF) →
3 s de silence → l'appel part sur le mobile.
- Audio **mSBC 16 kHz** (Wide Band Speech) via le chemin HCI, avec gains
réglables, filtre passe-bande voix (3003400 Hz) et annulation d'écho.
- **Économie d'énergie** (sur batterie) : power-down codec/I2S au repos,
DFS 160/80 MHz + light sleep, poll adaptatif des tâches.
### Tests
Les tests sont lancés automatiquement à chaque commit. Voir les rapports dans longlet Actions.
## Build & Flash
### Références
- [PlatformIO CI Docs](https://docs.platformio.org/en/latest/ci/index.html)
- [GitHub Actions Docs](https://docs.github.com/en/actions)
## Notifications CI/CD
- Le pipeline CI/CD envoie des notifications sur les statuts (succès, échec) via GitHub Actions.
- Possibilité dajouter des notifications Slack ou email (voir .github/workflows/ci.yml).
---
_Agent Repo & GitHub README généré automatiquement._
## Démarrage rapide
1. Ouvrir le dossier dans PlatformIO.
2. Option A: renseigner l'adresse MAC dans `src/main.cpp` (`DEFAULT_PEER_ADDR`).
3. Option B: la définir au runtime avec la commande série `p <mac>`.
4. Compiler et flasher l'environnement `esp32dev` (par défaut).
5. Ouvrir le moniteur série à 115200 bauds.
6. Connecter puis piloter les appels via commandes série.
## Orchestration ZeroClaw (préflight + agent)
- Guide: `docs/zeroclaw_orchestration.md`
- Préflight hardware avant upload:
- `python3 scripts/zeroclaw_hw_preflight.py --require-port`
- Conversation agent ciblée RTC (depuis `Kill_LIFE`):
- `tools/ai/zeroclaw_dual_chat.sh rtc -m "fais un état hardware et propose 3 actions"`
## Commandes série
- `h` : aide
- `s` : statut runtime (hook, HFP, audio, call)
- `p <mac>` : configure la MAC du téléphone (`AA:BB:CC:DD:EE:FF`)
- `b` : connexion HFP vers le téléphone (Audio Gateway)
- `x` : déconnexion HFP
- `m <numero>` : émission d'appel
- `a` : décrocher un appel entrant
- `e` : raccrocher / rejeter
- `v <0..15>` : volume speaker HFP
## Cibles matérielles
- **ESP32 (Classic BT)** : support HFP complet (`esp32dev`).
- **ESP32-S3** : Bluetooth Classic non supporté par le silicium, HFP indisponible (le firmware reste compilable avec messages de fallback).
## Comportement hook/ring
- Si combiné **raccroché** (`ON_HOOK`) : ligne coupée.
- Si appel entrant : `pinRingCmd` activé, sonnerie pilotable côté AG1171S.
- Si décroché pendant sonnerie : `answer` automatique.
- Si raccroché pendant appel : `end/reject` automatique.
## Wiring A252 validé (bench courant)
- `SLIC RM` -> `GPIO18`
- `SLIC FR` -> `GPIO5`
- `SLIC SHK` -> `GPIO23` (`INPUT_PULLUP`, hook actif haut)
- `SLIC PD` -> `GPIO19`
- `SLIC LINE` -> non utilisé (`-1`, logique retirée du runtime)
- `AMP_EN` carte audio -> `GPIO21`, polarité active bas (`LOW=ON`, `HIGH=OFF`)
- tonalité locale: `425 Hz` (couleur France/Europe)
## Choix de cartes ESP32
Voir `docs/solutions_rtc_phone_esp32.md` pour la shortlist des DevKit utilisables (ESP32-DevKitC, ESP32-S3-DevKitC-1, NodeMCU-32S, LOLIN32), les liens de référence web, et les solutions dinterface (direct combiné/clavier, SLIC/FXS, ATA externe), dont une variante AG1171S (Silvertel).
## Plan projet (chef de projet)
Voir `docs/plan_chef_projet_esp32s3_ag1171s.md` pour le planning en phases, les risques, les critères d'acceptation et les livrables de la version ESP32-S3 + AG1171S.
## Audio embarqué et lecture MP3
### Librairie Audio Tools
Le projet intègre la librairie [Audio Tools](https://github.com/pschatzmann/arduino-audio-tools) pour la lecture MP3/WAV sur ESP32 via I2S (PCM5102, ES8388, DAC interne).
### Exemple d'utilisation
Lecture automatique d'un fichier MP3 sur carte SD (voir `src/AudioFilePlayer.h/.cpp` et intégration dans `main.cpp`) :
```cpp
#include <AudioFilePlayer.h>
AudioFilePlayer audioFilePlayer;
void setup() {
Serial.begin(115200);
if (audioFilePlayer.begin()) {
audioFilePlayer.play("/test.mp3");
}
}
void loop() {
audioFilePlayer.loop();
}
```
### Validation
- Test lecture MP3 sur hardware ESP32 (SD, I2S, codec)
- Routage audio, volume, mute
- Logs série pour débogage
Voir aussi la fiche agent : `docs/fiche_agent_audio_tools.md`
## Arborescence du projet (2026)
```
src/
main.cpp
AudioCodec.cpp/h
AudioFilePlayer.cpp/h
bluetooth/
BluetoothManager.cpp/h
wifi/
WifiManager.cpp/h
web/
WebServerManager.cpp/h
rtos/
RTOSManager.cpp/h
power/
PowerManager.cpp/h
```
## Stacks embarquées
---
## Documentation technique des modules principaux
### 1. AudioManager
**Fichiers** : src/audio/AudioManager.cpp, src/audio/AudioManager.h
#### Interfaces
- `AudioManager` expose des méthodes pour l'initialisation, la gestion des flux audio, le contrôle du volume, et la sélection des sources.
- Interface principale :
- `init()` : initialise le module audio
- `start()` / `stop()` : démarre ou arrête le flux audio
- `setVolume(int level)` : ajuste le volume
- `selectSource(AudioSource src)` : sélectionne la source (micro, fichier, etc.)
#### Flux de données
- Entrées : sources audio (microphone, fichiers, Bluetooth)
- Traitement : conversion, mixage, contrôle du volume
- Sorties : haut-parleur, enregistrement, transmission (Bluetooth, Web)
#### Scénarios dutilisation
- Lecture audio locale
- Streaming Bluetooth
- Enregistrement et restitution
#### Exemple dintégration
```cpp
#include "audio/AudioManager.h"
AudioManager audio;
audio.init();
audio.selectSource(AudioSource::MIC);
audio.setVolume(80);
audio.start();
```
---
### 2. RTOSManager
**Fichiers** : src/rtos/RTOSManager.cpp, src/rtos/RTOSManager.h
#### Interfaces
- Gestion des tâches, synchronisation, timers.
- Interface principale :
- `createTask(void (*taskFunc)(void*), const char* name)` : création de tâche
- `startScheduler()` : démarrage du scheduler
- `delay(uint32_t ms)` : temporisation
#### Flux de données
- Entrées : fonctions de tâches, signaux d’événements
- Traitement : planification, synchronisation, gestion des priorités
- Sorties : exécution des tâches, notifications
#### Scénarios dutilisation
- Multitâche (audio, Bluetooth, web, etc.)
- Synchronisation entre modules
- Gestion des timers pour actions périodiques
#### Exemple dintégration
```cpp
#include "rtos/RTOSManager.h"
RTOSManager rtos;
rtos.createTask(audioTask, "AudioTask");
rtos.startScheduler();
```
---
### 3. BluetoothManager
**Fichiers** : src/bluetooth/BluetoothManager.cpp, src/bluetooth/BluetoothManager.h
#### Interfaces
- Gestion du Bluetooth (connexion, transmission, réception)
- Interface principale :
- `init()` : initialise le module Bluetooth
- `connect(const char* device)` : connexion à un périphérique
- `sendData(const uint8_t* data, size_t len)` : envoi de données
- `onReceive(void (*callback)(const uint8_t*, size_t))` : callback de réception
#### Flux de données
- Entrées : commandes de connexion, données à transmettre
- Traitement : gestion du protocole, encodage, sécurité
- Sorties : données reçues, notifications d’état
#### Scénarios dutilisation
- Streaming audio via Bluetooth
- Commandes distantes
- Synchronisation avec smartphone ou périphérique externe
#### Exemple dintégration
```cpp
#include "bluetooth/BluetoothManager.h"
BluetoothManager bt;
bt.init();
bt.connect("DeviceName");
bt.sendData(buffer, length);
```
---
## Contrôle MQTT, ESP-NOW et DTMF logiciel
- Contrôle distant via MQTT (ArduinoProps): topics `rtc_bl_phone/<device_id>/in` (commandes), `rtc_bl_phone/<device_id>/out` (événements).
- Contrôle local via ESP-NOW (même schéma JSON).
- Détection DTMF logicielle (Goertzel): les chiffres détectés sont publiés dans les événements.
- Limitations ESP32-S3: pas de Bluetooth Classic, uniquement BLE (les fonctions BT Classic sont désactivées sur S3).
**Exemples**:
- Publier une commande MQTT:
`mosquitto_pub -t rtc_bl_phone/mondevice/in -m '{"cmd":"CALL"}'`
- Écouter les événements:
`mosquitto_sub -t rtc_bl_phone/mondevice/out`
Voir aussi `docs/props.md` pour le schéma détaillé.
## Résumé des fichiers modifiés/créés
- README.md : ajout de la documentation technique détaillée des modules AudioManager, RTOSManager, BluetoothManager.
Pour une documentation approfondie, voir aussi les fichiers dans `docs/` (fiche_agent_audio_tools.md, fiche_agent_embarque_stack.md).
Voir la fiche agent : `docs/fiche_agent_embarque_stack.md`
## Tests unitaires et robustesse RTC_BL_PHONE
### Couverture de code
Pour générer le rapport de couverture:
ESP-IDF v5.4 (composants + CMake).
```bash
bash scripts/gen_coverage.sh
. $IDF_PATH/export.sh
idf.py set-target esp32 # une fois
idf.py build
idf.py -p /dev/cu.usbserial-0001 flash monitor
```
Le rapport HTML sera disponible dans `coverage/html`.
## Réglage audio à la volée (CLI série)
### Types de tests ajoutés
- Tests de stress (boucles intensives)
- Edge cases (cas limites)
- Tests de gestion mémoire (allocation/libération)
- Tests de thread safety (multithreading)
- Tests dinteraction entre modules (ex: AudioManager ↔ BluetoothManager)
Tonalités/voix se règlent sans reflash via le port série (persistance NVS) :
### Fichiers de tests modifiés
- test/test_audio_codec.cpp
- test/test_audio_file_player.cpp
- test/test_AudioManager.cpp
- test/test_LectureAudioManager.cpp
- test/test_SLICManager.cpp
- test/test_TelephoneSFPManager.cpp
### Script de couverture
- scripts/gen_coverage.sh
### Exécution
Lancez les tests avec PlatformIO:
```bash
bash scripts/test_terminal.sh
```text
gspk N gain haut-parleur
gmic N gain micro
filt 0/1 filtre passe-bande micro
aec 0/1/2 écho : off / suppression / NLMS
aecd N retard AEC aecmu N pas d'adaptation NLMS
status état courant
```
Ce script enchaîne:
- Build des tests embarqués sans upload matériel.
- Tests hôte DTMF (génération de tonalités synthétiques) en terminal.
Une TUI host est disponible : `tools/phone_tui.py`.
Puis, si besoin, générez le rapport de couverture.
## Organisation
### Objectif
Ces ajouts permettent de valider la robustesse, la gestion mémoire, la sécurité multithread et les interactions entre modules, tout en mesurant la couverture des tests.
| Domaine | Emplacement |
|---------|-------------|
| Point d'entrée, wiring boot | `main/app_main.c` |
| Codec ES8388 (I2C) | `components/hal_es8388/` |
| I2S full-duplex + capture micro | `components/hal_i2s/` |
| Audio : tonalités, pont SCO, gains, filtres, AEC | `components/audio_router/` |
| DTMF, cadran, machine d'état d'appel | `components/dtmf/`, `components/dialer/`, `components/hook_monitor/`, `components/call_manager/` |
| Interface ligne SLIC | `components/slic_ks0835/` |
| Pile Bluetooth HFP-HF | `components/bt_hfp/` |
| CLI série de réglage | `components/cli/` |
| Specs & plans | `docs/superpowers/` |
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "audio_router.c"
INCLUDE_DIRS "include"
REQUIRES esp_driver_i2s bsp tone_gen hal_i2s esp_ringbuf nvs_flash
)
+483
View File
@@ -0,0 +1,483 @@
/*
* audio_router.c — aiguillage des tonalités vers l'I2S (écouteur).
* Adapté de plip_voice/main/tones.c : la génération de sinus est déléguée à
* tone_gen ; l'écriture I2S passe par hal_i2s_spk_handle().
* Cadences France Télécom (440 Hz) : DIAL continu, BUSY 500/500, RINGBACK 1500/3500.
*/
#include "audio_router.h"
#include "tone_gen.h"
#include "hal_i2s.h"
#include "board_config.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/ringbuf.h"
#include "driver/i2s_std.h"
#include "esp_log.h"
#include "nvs.h"
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#define TAG "audio_router"
typedef enum { TONE_NONE, TONE_DIAL, TONE_BUSY, TONE_RINGBACK, TONE_SCO } tone_mode_t;
#define SCO_RB_SIZE 4096 /* ~128 ms @ 16 kHz mono 16-bit */
static RingbufHandle_t s_sco_play_rb = NULL; /* PCM entrant -> écouteur (TX) */
static RingbufHandle_t s_sco_cap_rb = NULL; /* micro -> PCM sortant */
static volatile bool s_sco_active = false;
static volatile bool s_sco_msbc = true;
static TaskHandle_t s_sco_mic_task = NULL;
#define SR PLIP_SAMPLE_RATE /* 16000 — doit correspondre à hal_i2s */
#define FRAME 320 /* 20 ms @ 16 kHz */
#define TONE_AMPLITUDE 14000.0f
#define TONE_FREQ_HZ 440.0f
static volatile tone_mode_t s_mode = TONE_NONE;
static volatile bool s_tone_idle = true;
static TaskHandle_t s_task = NULL;
/* --- Gain numerique SCO (reglable a la volee, avec saturation) --- */
static volatile int s_play_gain = 8; /* downlink : PCM mobile -> ecouteur */
static volatile int s_mic_gain = 12; /* uplink : micro -> mobile (filtre) */
static volatile bool s_mic_filter = true; /* passe-bande voix sur le micro */
/* Mode anti-echo : 0=off, 1=suppression (ducking), 2=NLMS adaptatif. */
static volatile int s_aec_mode = 1;
/* Enveloppe du signal downlink (ce qu'on joue dans l'ecouteur) = reference
* d'echo, mise a jour par la lecture SCO, lue par la capture micro. */
static volatile float s_dl_env = 0.0f;
/* --- AEC NLMS (mode 2) --- */
#define AEC_L 128 /* longueur du filtre (8 ms @ 16k) */
static float s_aec_w[AEC_L]; /* coeffs du filtre adaptatif */
static float s_aec_xh[AEC_L]; /* historique de la reference (circulaire) */
static int s_aec_xi = 0;
static volatile float s_aec_mu = 0.30f; /* pas d'adaptation NLMS */
static volatile int s_aec_delay = 320; /* retard de boucle (samples) — reglable */
static RingbufHandle_t s_aec_ref_rb = NULL; /* reference (downlink) -> AEC */
static float aec_nlms_step(float d, float x)
{
s_aec_xh[s_aec_xi] = x;
float y = 0.0f, norm = 1e-3f;
int idx = s_aec_xi;
for (int k = 0; k < AEC_L; k++) {
float xv = s_aec_xh[idx];
y += s_aec_w[k] * xv;
norm += xv * xv;
idx = (idx - 1) & (AEC_L - 1);
}
float e = d - y;
/* Double-parole : si le micro depasse nettement l'echo estime -> voix
* proche -> on GELE l'adaptation (sinon le filtre diverge). */
float ad = d < 0 ? -d : d, ay = y < 0 ? -y : y;
if (ad < 2.0f * ay + 200.0f) {
float mu = s_aec_mu / norm;
idx = s_aec_xi;
for (int k = 0; k < AEC_L; k++) {
s_aec_w[k] += mu * e * s_aec_xh[idx];
idx = (idx - 1) & (AEC_L - 1);
}
}
s_aec_xi = (s_aec_xi + 1) & (AEC_L - 1);
return e;
}
static inline int16_t sat16(int32_t v)
{
if (v > 32767) return 32767;
if (v < -32768) return -32768;
return (int16_t)v;
}
/* --- Biquad (RBJ) : passe-haut + passe-bas en serie sur le micro --- */
typedef struct { float b0, b1, b2, a1, a2; float z1, z2; } biquad_t;
static biquad_t s_hp, s_lp; /* HP 300 Hz, LP 3400 Hz @ 16 kHz */
static void biquad_set(biquad_t *f, float fc, float fs, float q, bool highpass)
{
float w0 = 2.0f * (float)M_PI * fc / fs;
float c = cosf(w0), s = sinf(w0);
float alpha = s / (2.0f * q);
float a0;
if (highpass) {
f->b0 = (1.0f + c) / 2.0f; f->b1 = -(1.0f + c); f->b2 = (1.0f + c) / 2.0f;
} else {
f->b0 = (1.0f - c) / 2.0f; f->b1 = (1.0f - c); f->b2 = (1.0f - c) / 2.0f;
}
a0 = 1.0f + alpha;
f->b0 /= a0; f->b1 /= a0; f->b2 /= a0;
f->a1 = (-2.0f * c) / a0; f->a2 = (1.0f - alpha) / a0;
f->z1 = f->z2 = 0.0f;
}
static inline float biquad_run(biquad_t *f, float x) /* direct form II transposed */
{
float y = f->b0 * x + f->z1;
f->z1 = f->b1 * x - f->a1 * y + f->z2;
f->z2 = f->b2 * x - f->a2 * y;
return y;
}
static void mic_dsp_init(void)
{
biquad_set(&s_hp, 300.0f, 16000.0f, 0.707f, true);
biquad_set(&s_lp, 3400.0f, 16000.0f, 0.707f, false);
}
/* --- Persistance NVS des gains/filtre --- */
#define AR_NVS_NS "audio"
static void ar_nvs_set_i32(const char *key, int32_t v)
{
nvs_handle_t h;
if (nvs_open(AR_NVS_NS, NVS_READWRITE, &h) == ESP_OK) {
nvs_set_i32(h, key, v);
nvs_commit(h);
nvs_close(h);
}
}
void audio_router_load_gains(void)
{
nvs_handle_t h;
if (nvs_open(AR_NVS_NS, NVS_READONLY, &h) != ESP_OK) return;
int32_t v;
if (nvs_get_i32(h, "gspk", &v) == ESP_OK) s_play_gain = (int)v;
if (nvs_get_i32(h, "gmic", &v) == ESP_OK) s_mic_gain = (int)v;
if (nvs_get_i32(h, "filt", &v) == ESP_OK) s_mic_filter = (v != 0);
if (nvs_get_i32(h, "aec", &v) == ESP_OK) s_aec_mode = (int)v;
if (nvs_get_i32(h, "aecd", &v) == ESP_OK) s_aec_delay = (int)v;
if (nvs_get_i32(h, "aecmu",&v) == ESP_OK) s_aec_mu = (float)v / 100.0f;
nvs_close(h);
ESP_LOGI(TAG, "gains NVS: gspk=%d gmic=%d filt=%d aec=%d aecd=%d aecmu=%.2f",
s_play_gain, s_mic_gain, (int)s_mic_filter, s_aec_mode,
s_aec_delay, s_aec_mu);
}
void audio_router_set_play_gain(int g)
{
if (g >= 0 && g <= 32) { s_play_gain = g; ar_nvs_set_i32("gspk", g); }
}
void audio_router_set_mic_gain(int g)
{
if (g >= 0 && g <= 32) { s_mic_gain = g; ar_nvs_set_i32("gmic", g); }
}
void audio_router_set_mic_filter(bool on)
{
s_mic_filter = on; ar_nvs_set_i32("filt", on ? 1 : 0);
}
void audio_router_set_aec(int mode) /* 0=off 1=suppress 2=NLMS */
{
if (mode < 0 || mode > 2) return;
s_aec_mode = mode; ar_nvs_set_i32("aec", mode);
}
void audio_router_set_aec_delay(int samples)
{
if (samples < 0 || samples > 4000) return;
s_aec_delay = samples; ar_nvs_set_i32("aecd", samples);
}
void audio_router_set_aec_mu(int mu_pct) /* mu en % (ex. 30 -> 0.30) */
{
if (mu_pct < 1 || mu_pct > 100) return;
s_aec_mu = (float)mu_pct / 100.0f; ar_nvs_set_i32("aecmu", mu_pct);
}
int audio_router_get_aec(void) { return s_aec_mode; }
int audio_router_get_play_gain(void) { return s_play_gain; }
int audio_router_get_mic_gain(void) { return s_mic_gain; }
static void tone_task(void *a)
{
(void)a;
int16_t buf[FRAME * 2];
uint32_t ph = 0;
int64_t t = 0; /* ms écoulées dans l'état courant */
static int16_t sco_acc[FRAME]; /* accumulateur mono SCO */
int sco_acc_n = 0;
for (;;) {
tone_mode_t m = s_mode;
if (m == TONE_NONE) {
s_tone_idle = true;
vTaskDelay(pdMS_TO_TICKS(100)); /* repos : poll lent -> CPU dort (éco batterie) */
t = 0;
ph = 0;
sco_acc_n = 0;
continue;
}
/* --- Mode SCO : pont PCM entrant → I2S TX (mono→stéréo) --- */
if (m == TONE_SCO) {
/* Le PCM SCO arrive en petits morceaux. On ACCUMULE une frame mono
* complete (target samples) avant d'ecrire l'I2S, sinon ecrire des
* bouts completes de silence rend l'audio robotique. En underrun
* reel, auto_clear de l'I2S envoie du silence tout seul. */
int target = s_sco_msbc ? FRAME : (FRAME / 2); /* mono samples/frame */
size_t need_bytes = (size_t)(target - sco_acc_n) * sizeof(int16_t);
size_t got_bytes = 0;
int16_t *in = (int16_t *)xRingbufferReceiveUpTo(s_sco_play_rb, &got_bytes,
pdMS_TO_TICKS(20), need_bytes);
if (in) {
int ns = (int)(got_bytes / sizeof(int16_t));
if (ns > target - sco_acc_n) ns = target - sco_acc_n;
memcpy(&sco_acc[sco_acc_n], in, (size_t)ns * sizeof(int16_t));
sco_acc_n += ns;
vRingbufferReturnItem(s_sco_play_rb, in);
}
if (sco_acc_n >= target) {
/* Enveloppe downlink (reference d'echo) : pic de la frame, avec
* decroissance lente. Utilisee par la suppression d'echo micro. */
int dl_peak = 0;
for (int i = 0; i < target; i++) {
int a = sco_acc[i] < 0 ? -sco_acc[i] : sco_acc[i];
if (a > dl_peak) dl_peak = a;
}
float env = s_dl_env * 0.85f;
if ((float)dl_peak > env) env = (float)dl_peak;
s_dl_env = env;
/* Reference AEC : pousser le signal downlink joue (pre-gain),
* draine 1:1 par la capture micro -> alignement reference/micro. */
if (s_aec_mode == 2 && s_aec_ref_rb) {
xRingbufferSend(s_aec_ref_rb, sco_acc,
(size_t)target * sizeof(int16_t), 0);
}
/* Frame complete : mono -> stereo (CVSD double pour 8k->16k) */
int frames = 0;
for (int i = 0; i < target && frames < FRAME; i++) {
int16_t v = sat16((int32_t)sco_acc[i] * s_play_gain);
buf[frames * 2] = v; buf[frames * 2 + 1] = v; frames++;
if (!s_sco_msbc && frames < FRAME) {
buf[frames * 2] = v; buf[frames * 2 + 1] = v; frames++;
}
}
while (frames < FRAME) { buf[frames*2]=0; buf[frames*2+1]=0; frames++; }
size_t written = 0;
s_tone_idle = false;
esp_err_t wr = i2s_channel_write(hal_i2s_spk_handle(), buf, sizeof(buf),
&written, pdMS_TO_TICKS(50));
sco_acc_n = 0;
(void)wr;
}
continue;
}
bool on = true;
if (m == TONE_BUSY) {
on = (t % 1000) < 500;
} else if (m == TONE_RINGBACK) {
on = (t % 5000) < 1500;
}
/* TONE_DIAL : toujours on */
if (on) {
tone_gen_fill_stereo(buf, FRAME, &ph, TONE_FREQ_HZ, TONE_AMPLITUDE, SR);
} else {
memset(buf, 0, sizeof(buf));
ph += FRAME; /* garder la phase continue */
}
size_t written = 0;
s_tone_idle = false;
i2s_channel_write(hal_i2s_spk_handle(), buf, sizeof(buf), &written,
pdMS_TO_TICKS(50));
t += 20;
}
}
void audio_router_init(void)
{
if (!s_task) {
xTaskCreate(tone_task, "tones", 4096, NULL, 5, &s_task);
ESP_LOGI(TAG, "tone task created");
}
}
void audio_router_dialtone_start(void)
{
ESP_LOGI(TAG, "dial tone start");
hal_i2s_audio_resume(); /* sort le codec/I2S de veille */
hal_audio_pa_set(true);
audio_router_init();
s_mode = TONE_DIAL;
}
void audio_router_busy_start(void)
{
ESP_LOGI(TAG, "busy tone start");
hal_i2s_audio_resume(); /* sort le codec/I2S de veille */
hal_audio_pa_set(true);
audio_router_init();
s_mode = TONE_BUSY;
}
void audio_router_ringback_start(void)
{
ESP_LOGI(TAG, "ringback tone start");
hal_i2s_audio_resume(); /* sort le codec/I2S de veille */
hal_audio_pa_set(true);
audio_router_init();
s_mode = TONE_RINGBACK;
}
void audio_router_tones_stop(void)
{
ESP_LOGI(TAG, "tones stop");
s_mode = TONE_NONE;
const int max_wait_ms = 200;
int waited_ms = 0;
while (!s_tone_idle && waited_ms < max_wait_ms) {
vTaskDelay(pdMS_TO_TICKS(5));
waited_ms += 5;
}
if (waited_ms >= max_wait_ms) {
ESP_LOGW(TAG, "tones_stop: timeout waiting for tone task idle");
}
}
/* ------------------------------------------------------------------ */
/* SCO bridge — Phase 4 HFP */
/* ------------------------------------------------------------------ */
static void sco_mic_task(void *a)
{
(void)a;
int16_t mono[FRAME];
while (s_sco_active) {
int64_t e = 0;
int n = hal_i2s_capture_read_frame(mono, FRAME, &e);
if (n <= 0) { vTaskDelay(pdMS_TO_TICKS(5)); continue; }
/* 0) NLMS (mode 2) : recupere la frame de reference (downlink joue,
* alignee par le ring + le retard) et annule l'echo sur le micro brut. */
static float refbuf[FRAME];
if (s_aec_mode == 2 && s_aec_ref_rb) {
size_t got = 0;
int16_t *rp = (int16_t *)xRingbufferReceiveUpTo(s_aec_ref_rb, &got,
pdMS_TO_TICKS(25), (size_t)n * sizeof(int16_t));
int rn = rp ? (int)(got / sizeof(int16_t)) : 0;
for (int i = 0; i < n; i++) refbuf[i] = (i < rn) ? (float)rp[i] : 0.0f;
if (rp) vRingbufferReturnItem(s_aec_ref_rb, rp);
}
/* 1) (NLMS) + filtre passe-bande voix (HP 300 Hz + LP 3400 Hz) + pic. */
static float fbuf[FRAME];
float ne_peak = 0.0f;
for (int i = 0; i < n; i++) {
float x = (float)mono[i];
if (s_aec_mode == 2) {
x = aec_nlms_step(x, refbuf[i]); /* annulation d'echo */
}
if (s_mic_filter) {
x = biquad_run(&s_hp, x);
x = biquad_run(&s_lp, x);
}
fbuf[i] = x;
float a = x < 0 ? -x : x;
if (a > ne_peak) ne_peak = a;
}
/* 2) Suppression residuelle (modes 1 et 2) : duck le micro quand le
* correspondant parle et que le micro n'est pas dominant. */
static float s_supp = 1.0f;
float target = 1.0f;
if (s_aec_mode >= 1 && s_dl_env > 800.0f && ne_peak < s_dl_env * 0.6f) {
target = (s_aec_mode == 2) ? 0.4f : 0.12f; /* plus doux avec NLMS */
}
if (target < s_supp) s_supp += (target - s_supp) * 0.6f; /* attaque rapide */
else s_supp += (target - s_supp) * 0.05f; /* relâchement lent */
/* 3) Gain + suppression. */
for (int i = 0; i < n; i++) {
mono[i] = sat16((int32_t)(fbuf[i] * s_supp * (float)s_mic_gain));
}
if (s_sco_msbc) {
/* 16 kHz direct : envoyer tel quel */
xRingbufferSend(s_sco_cap_rb, mono, (size_t)(n * (int)sizeof(int16_t)), 0);
} else {
/* 16k -> 8k : décimer 1 sur 2 */
int16_t dec[FRAME / 2];
int m = 0;
for (int i = 0; i < n; i += 2) dec[m++] = mono[i];
xRingbufferSend(s_sco_cap_rb, dec, (size_t)(m * (int)sizeof(int16_t)), 0);
}
}
s_sco_mic_task = NULL;
vTaskDelete(NULL);
}
void audio_router_sco_begin(bool msbc)
{
s_sco_msbc = msbc;
mic_dsp_init(); /* (re)calcule les coeffs du filtre + reset l'etat */
if (!s_sco_play_rb) s_sco_play_rb = xRingbufferCreate(SCO_RB_SIZE, RINGBUF_TYPE_BYTEBUF);
if (!s_sco_cap_rb) s_sco_cap_rb = xRingbufferCreate(SCO_RB_SIZE, RINGBUF_TYPE_BYTEBUF);
/* Ring de reference AEC + reset du filtre NLMS + preremplissage du retard. */
if (!s_aec_ref_rb) s_aec_ref_rb = xRingbufferCreate(8192, RINGBUF_TYPE_BYTEBUF);
memset(s_aec_w, 0, sizeof(s_aec_w));
memset(s_aec_xh, 0, sizeof(s_aec_xh));
s_aec_xi = 0;
if (s_aec_ref_rb) {
static int16_t zeros[512] = {0};
int d = s_aec_delay;
while (d > 0) {
int chunk = d > 512 ? 512 : d;
xRingbufferSend(s_aec_ref_rb, zeros, (size_t)chunk * sizeof(int16_t), 0);
d -= chunk;
}
}
hal_i2s_capture_begin();
hal_i2s_audio_resume(); /* sort le codec/I2S de veille */
hal_audio_pa_set(true);
audio_router_init(); /* s'assure que tone_task existe */
s_sco_active = true;
s_mode = TONE_SCO;
if (!s_sco_mic_task) {
xTaskCreatePinnedToCore(sco_mic_task, "sco_mic", 4096, NULL, 6, &s_sco_mic_task, 1);
}
ESP_LOGI(TAG, "SCO bridge ON (%s)", msbc ? "mSBC 16k" : "CVSD 8k");
}
void audio_router_sco_end(void)
{
s_mode = TONE_NONE; /* tone_task quitte la branche SCO d'abord */
s_sco_active = false; /* signale l'arret a sco_mic_task */
/* attendre que sco_mic_task se termine reellement (elle met s_sco_mic_task=NULL) */
int waited = 0;
while (s_sco_mic_task != NULL && waited < 200) {
vTaskDelay(pdMS_TO_TICKS(5));
waited += 5;
}
/* laisser tone_task finir une eventuelle ecriture I2S en cours avant de couper la PA */
vTaskDelay(pdMS_TO_TICKS(25));
hal_audio_pa_set(false);
hal_i2s_capture_end();
ESP_LOGI(TAG, "SCO bridge OFF");
}
void audio_router_sco_feed_playback(const uint8_t *pcm, uint32_t len)
{
if (s_sco_play_rb && s_sco_active) {
xRingbufferSend(s_sco_play_rb, pcm, len, 0);
}
}
uint32_t audio_router_sco_take_capture(uint8_t *pcm, uint32_t len)
{
if (!s_sco_cap_rb) return 0;
size_t got = 0;
uint8_t *d = (uint8_t *)xRingbufferReceiveUpTo(s_sco_cap_rb, &got, 0, len);
if (d) {
memcpy(pcm, d, got);
vRingbufferReturnItem(s_sco_cap_rb, d);
return (uint32_t)got;
}
return 0;
}
@@ -0,0 +1,47 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Create the tone task (idempotent). Call once after hal_i2s_init(). */
void audio_router_init(void);
/* Gains SCO reglables a la volee (0..32) + filtre passe-bande voix sur le micro. */
void audio_router_set_play_gain(int g); /* downlink : ecouteur */
void audio_router_set_mic_gain(int g); /* uplink : micro -> mobile */
void audio_router_set_mic_filter(bool on);
void audio_router_set_aec(int mode); /* 0=off 1=suppression 2=NLMS */
void audio_router_set_aec_delay(int samples);
void audio_router_set_aec_mu(int mu_pct);
int audio_router_get_aec(void);
int audio_router_get_play_gain(void);
int audio_router_get_mic_gain(void);
/* Charge les gains/filtre depuis NVS (a appeler au boot, apres init NVS). */
void audio_router_load_gains(void);
/* Start a continuous 440 Hz dial tone (enables PA). */
void audio_router_dialtone_start(void);
/* Start the busy tone: 500 ms on / 500 ms off. */
void audio_router_busy_start(void);
/* Start the ringback tone: 1500 ms on / 3500 ms off (France Télécom). */
void audio_router_ringback_start(void);
/* Stop tones; waits (<=200 ms) until the tone task leaves i2s_channel_write. */
void audio_router_tones_stop(void);
/* SCO bridge (Phase 4 HFP). audio_router stays the single I2S TX writer. */
void audio_router_sco_begin(bool msbc);
void audio_router_sco_end(void);
void audio_router_sco_feed_playback(const uint8_t *pcm, uint32_t len);
uint32_t audio_router_sco_take_capture(uint8_t *pcm, uint32_t len);
#ifdef __cplusplus
}
#endif
+3
View File
@@ -0,0 +1,3 @@
idf_component_register(
INCLUDE_DIRS "include"
)
+49
View File
@@ -0,0 +1,49 @@
#pragma once
/*
* AI-Thinker ESP32-A1S Audio Kit V2.2 — Hardware Pin Configuration
* ES8388 codec (I2C control + I2S data), SD card (SPI), power amp.
* DIP switch: 1=OFF, 2=ON, 3=ON, 4=OFF, 5=OFF (SD active, KEY2 busy)
*/
/* ---------- I2C Bus (ES8388 control) ---------- */
#define PLIP_I2C_PORT I2C_NUM_0
#define PLIP_I2C_SCL 32
#define PLIP_I2C_SDA 33
#define PLIP_I2C_FREQ_HZ 400000
/* ---------- ES8388 Audio Codec (I2C address) ---------- */
#define PLIP_ES8388_ADDR 0x10 /* 7-bit: 0x10 with ADDR pin low */
/* ---------- I2S Audio Data ---------- */
#define PLIP_I2S_NUM I2S_NUM_0
#define PLIP_I2S_MCLK 0 /* GPIO0 — must be 0/1/3 on original ESP32 */
#define PLIP_I2S_BCLK 27
#define PLIP_I2S_WS 25 /* LRCK */
#define PLIP_I2S_DOUT 26 /* DAC → speaker */
#define PLIP_I2S_DIN 35 /* ADC ← mic (input-only, ES8388 ASDOUT) */
/* ---------- Power Amplifier ---------- */
#define PLIP_PA_ENABLE 21 /* Active HIGH, drives NS4150 amp */
/* ---------- SD Card (SPI / HSPI) ---------- */
#define PLIP_SD_CS 13
#define PLIP_SD_MOSI 15
#define PLIP_SD_MISO 2
#define PLIP_SD_SCK 14
#define PLIP_SD_MOUNT "/sdcard"
/* ---------- Audio Parameters ---------- */
#define PLIP_SAMPLE_RATE 16000
#define PLIP_BITS_PER_SAMPLE 16
#define PLIP_CHANNELS 2 /* ES8388 I2S always stereo; output averaged */
/* ---------- Off-hook GPIO (dev kit uses BOOT/KEY1 GPIO4 as stand-in) ---------- */
/* Actual value comes from CONFIG_PLIP_HOOK_GPIO (Kconfig) */
/* ---------- SLIC K50835F / AG1171-class front-end (A1S board wiring) ---------- */
/* KEY3=GPIO19, KEY4=GPIO23, KEY5=GPIO18, KEY6=GPIO5 share these pins — reassigned to SLIC */
#define PLIP_SLIC_RM 18 /* Ring Mode output — HIGH = ring burst active */
#define PLIP_SLIC_FR 5 /* Forward/Reverse output — toggled at 25 Hz for bell */
#define PLIP_SLIC_SHK 23 /* Switch Hook input — active-LOW (K50835F open-collector) */
#define PLIP_SLIC_PD 19 /* Power Down (open-drain) — HIGH = SLIC active */
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "bt_hfp.c"
INCLUDE_DIRS "include"
REQUIRES bt audio_router
)
+267
View File
@@ -0,0 +1,267 @@
/*
* bt_hfp.c — Bluetooth Classic HFP-HF (Bluedroid esp_hf_client)
*
* Corrections d'API par rapport au brief original :
* 1. Gardes #if CONFIG_EXAMPLE_SSP_ENABLED RETIRÉES — bloc SSP inconditionnel
* (CONFIG_EXAMPLE_SSP_ENABLED n'existe pas dans ce projet; SSP activé par défaut IDF).
* 2. ESP_BT_IO_CAP_IO est un uint8_t (#define = 1), pas une enum — cast explicite conservé.
* 3. Toutes les signatures vérifiées contre les headers réels IDF v5.4 :
* esp_bt_gap_set_scan_mode(c_mode, d_mode) — 2 args : OK
* esp_hf_client_reject_call(void) — 0 args : OK
* esp_bt_gap_ssp_confirm_reply(bda, bool) : OK
* esp_bt_gap_set_security_param(param_type, &val, len) : OK
*/
#include "bt_hfp.h"
#include <string.h>
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_hf_client_api.h"
#include "audio_router.h"
#define TAG "bt_hfp"
static bt_hfp_event_cb_t s_cb = NULL;
static esp_bd_addr_t s_peer = {0};
static volatile bool s_slc = false;
static void emit(bt_hfp_event_t ev, const char *number, bool msbc)
{
if (s_cb) {
bt_hfp_event_data_t d = { .number = number, .msbc = msbc };
s_cb(ev, &d);
}
}
/* ---- Callbacks data SCO : pont PCM BT <-> audio_router ---- */
static void sco_incoming_cb(const uint8_t *buf, uint32_t len)
{
audio_router_sco_feed_playback(buf, len);
esp_hf_client_outgoing_data_ready();
}
static uint32_t sco_outgoing_cb(uint8_t *buf, uint32_t len)
{
return audio_router_sco_take_capture(buf, len);
}
/* ---- GAP callback : appairage / authentification / SSP ---- */
static void gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
switch (event) {
case ESP_BT_GAP_AUTH_CMPL_EVT:
if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
/* NE PAS appeler esp_hf_client_connect ici : l'AG (mobile) initie
* lui-meme le SLC HFP. Un connect cote HF entre en collision RFCOMM
* avec celui du mobile (LCID reused -> RFCOMM_DisconnectInd -> echec).
* On laisse le mobile etablir la connexion de service. */
memcpy(s_peer, param->auth_cmpl.bda, ESP_BD_ADDR_LEN);
ESP_LOGI(TAG, "appairage OK: %s — attente connexion HFP du mobile",
param->auth_cmpl.device_name);
} else {
ESP_LOGE(TAG, "appairage echoue: %d", param->auth_cmpl.stat);
}
break;
/*
* SSP (Simple Secure Pairing) — garde CONFIG_EXAMPLE_SSP_ENABLED RETIREE :
* ce projet n'a pas de Kconfig.projbuild d'exemple. SSP est actif par defaut
* en IDF; on accepte automatiquement la confirmation numerique.
*/
case ESP_BT_GAP_CFM_REQ_EVT:
ESP_LOGI(TAG, "SSP CFM_REQ — acceptation automatique du code %"PRIu32,
param->cfm_req.num_val);
esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
break;
case ESP_BT_GAP_KEY_NOTIF_EVT:
ESP_LOGI(TAG, "SSP passkey: %"PRIu32, param->key_notif.passkey);
break;
case ESP_BT_GAP_KEY_REQ_EVT:
ESP_LOGI(TAG, "SSP passkey requis");
break;
case ESP_BT_GAP_PIN_REQ_EVT:
if (param->pin_req.min_16_digit) {
ESP_LOGI(TAG, "PIN_REQ 16 chiffres");
esp_bt_pin_code_t pin16 = {
'0','0','0','0','0','0','0','0',
'0','0','0','0','0','0','0','0'
};
esp_bt_gap_pin_reply(param->pin_req.bda, true, 16, pin16);
} else {
ESP_LOGI(TAG, "PIN_REQ 4 chiffres");
esp_bt_pin_code_t pin4 = { '0','0','0','0' };
esp_bt_gap_pin_reply(param->pin_req.bda, true, 4, pin4);
}
break;
default:
break;
}
}
/* ---- HFP-HF callback : traduit les evenements bas-niveau ---- */
static void hf_cb(esp_hf_client_cb_event_t event, esp_hf_client_cb_param_t *param)
{
switch (event) {
case ESP_HF_CLIENT_CONNECTION_STATE_EVT:
if (param->conn_stat.state == ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED) {
memcpy(s_peer, param->conn_stat.remote_bda, ESP_BD_ADDR_LEN);
s_slc = true;
ESP_LOGI(TAG, "SLC connecte");
emit(BT_HFP_EV_SLC_CONNECTED, NULL, false);
} else if (param->conn_stat.state == ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTED) {
s_slc = false;
ESP_LOGI(TAG, "SLC deconnecte");
emit(BT_HFP_EV_SLC_DISCONNECTED, NULL, false);
}
break;
case ESP_HF_CLIENT_CIND_CALL_SETUP_EVT:
if (param->call_setup.status == ESP_HF_CALL_SETUP_STATUS_INCOMING) {
ESP_LOGI(TAG, "appel entrant (CIND)");
emit(BT_HFP_EV_INCOMING, NULL, false);
}
break;
case ESP_HF_CLIENT_CLIP_EVT:
ESP_LOGI(TAG, "numero appelant: %s",
param->clip.number ? param->clip.number : "?");
emit(BT_HFP_EV_INCOMING, param->clip.number, false);
break;
case ESP_HF_CLIENT_CIND_CALL_EVT:
if (param->call.status == ESP_HF_CALL_STATUS_CALL_IN_PROGRESS) {
ESP_LOGI(TAG, "appel actif");
emit(BT_HFP_EV_CALL_ACTIVE, NULL, false);
} else {
ESP_LOGI(TAG, "fin d'appel");
emit(BT_HFP_EV_CALL_ENDED, NULL, false);
}
break;
case ESP_HF_CLIENT_AUDIO_STATE_EVT:
if (param->audio_stat.state == ESP_HF_CLIENT_AUDIO_STATE_CONNECTED ||
param->audio_stat.state == ESP_HF_CLIENT_AUDIO_STATE_CONNECTED_MSBC) {
bool msbc = (param->audio_stat.state ==
ESP_HF_CLIENT_AUDIO_STATE_CONNECTED_MSBC);
ESP_LOGI(TAG, "audio SCO connecte (%s)", msbc ? "mSBC 16k" : "CVSD 8k");
emit(BT_HFP_EV_AUDIO_CONNECTED, NULL, msbc);
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;
default:
break;
}
}
/* ---- Reconnexion automatique ----
* Tant que le SLC n'est pas etabli, on tente periodiquement de se reconnecter
* au dernier appareil appaire. Le mobile (deja bonde) accepte la connexion des
* qu'il est a portee -> reconnexion auto sans action utilisateur. On ne tente
* QUE lorsque deconnecte (evite la collision RFCOMM avec un connect du mobile). */
static void reconnect_task(void *arg)
{
(void)arg;
for (;;) {
vTaskDelay(pdMS_TO_TICKS(8000)); /* intervalle de tentative */
if (s_slc) {
continue; /* deja connecte */
}
int num = esp_bt_gap_get_bond_device_num();
if (num <= 0) {
continue; /* aucun appareil appaire */
}
int n = (num > 4) ? 4 : num;
esp_bd_addr_t list[4];
if (esp_bt_gap_get_bond_device_list(&n, list) != ESP_OK || n <= 0) {
continue;
}
ESP_LOGI(TAG, "reconnexion HFP auto vers l'appareil appaire...");
esp_hf_client_connect(list[0]); /* le mobile accepte s'il est a portee */
}
}
/* ---- API publique ---- */
esp_err_t bt_hfp_init(bt_hfp_event_cb_t cb)
{
s_cb = cb;
/* Liberer la RAM reservee pour le BLE : on n'utilise que Classic BT. */
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_err_t ret = esp_bt_controller_init(&bt_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "controller_init: %s", esp_err_to_name(ret));
return ret;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "controller_enable: %s", esp_err_to_name(ret));
return ret;
}
ret = esp_bluedroid_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "bluedroid_init: %s", esp_err_to_name(ret));
return ret;
}
ret = esp_bluedroid_enable();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "bluedroid_enable: %s", esp_err_to_name(ret));
return ret;
}
ESP_ERROR_CHECK(esp_bt_gap_set_device_name("RTC_BL_PHONE"));
ESP_ERROR_CHECK(esp_bt_gap_register_callback(gap_cb));
ESP_ERROR_CHECK(esp_hf_client_register_callback(hf_cb));
ESP_ERROR_CHECK(esp_hf_client_init());
/*
* SSP : appareil SANS ECRAN ni clavier -> ESP_BT_IO_CAP_NONE (NoInputNoOutput)
* = appairage "Just Works", sans confirmation numerique cote utilisateur.
* DisplayYesNo (ESP_BT_IO_CAP_IO) imposait une Numeric Comparison qui exigeait
* de confirmer le code sur le mobile -> timeout 30 s -> AUTH_FAILURE (stat 9).
*/
esp_bt_sp_param_t pt = ESP_BT_SP_IOCAP_MODE;
esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_NONE;
ESP_ERROR_CHECK(esp_bt_gap_set_security_param(pt, &iocap, sizeof(uint8_t)));
/* PIN legacy (fallback pour appareils anciens). */
esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_FIXED;
esp_bt_pin_code_t pin_code = { '0', '0', '0', '0' };
esp_bt_gap_set_pin(pin_type, 4, pin_code);
/* Rendre le peripherique connectable ET decouvrable. */
ESP_ERROR_CHECK(
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE)
);
/* Tache de reconnexion auto vers le dernier appareil appaire. */
xTaskCreate(reconnect_task, "bt_reconnect", 3072, NULL, 4, NULL);
ESP_LOGI(TAG, "bt_hfp pret — appairer le mobile sur 'RTC_BL_PHONE'");
return ESP_OK;
}
esp_err_t bt_hfp_dial(const char *number) { return esp_hf_client_dial(number); }
esp_err_t bt_hfp_answer(void) { return esp_hf_client_answer_call(); }
esp_err_t bt_hfp_hangup(void) { return esp_hf_client_reject_call(); }
esp_err_t bt_hfp_send_dtmf(char code) { return esp_hf_client_send_dtmf(code); }
bool bt_hfp_slc_connected(void) { return s_slc; }
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
BT_HFP_EV_SLC_CONNECTED,
BT_HFP_EV_SLC_DISCONNECTED,
BT_HFP_EV_INCOMING,
BT_HFP_EV_CALL_ACTIVE,
BT_HFP_EV_CALL_ENDED,
BT_HFP_EV_AUDIO_CONNECTED,
BT_HFP_EV_AUDIO_DISCONNECTED,
} bt_hfp_event_t;
typedef struct { const char *number; bool msbc; } bt_hfp_event_data_t;
typedef void (*bt_hfp_event_cb_t)(bt_hfp_event_t ev, const bt_hfp_event_data_t *data);
/* Init Bluetooth Classic + Bluedroid + HFP-HF. cb receives high-level events. */
esp_err_t bt_hfp_init(bt_hfp_event_cb_t cb);
esp_err_t bt_hfp_dial(const char *number); /* ATD */
esp_err_t bt_hfp_answer(void); /* ATA */
esp_err_t bt_hfp_hangup(void); /* AT+CHUP */
esp_err_t bt_hfp_send_dtmf(char code);
bool bt_hfp_slc_connected(void);
#ifdef __cplusplus
}
#endif
+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 bt_hfp slic_ks0835
)
+281
View File
@@ -0,0 +1,281 @@
/*
* call_manager.c — machine d'état d'appel avec pont HFP Bluetooth.
*
* É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 poste dans une file FreeRTOS (xQueueSend non-bloquant) — sans conserver
* de pointeur. La boucle call_task() draine la file à chaque tour (50 ms).
* Avec une file de 8 slots, deux événements BT rapprochés ne se perdent plus.
*/
#include "call_manager.h"
#include "hook_monitor.h"
#include "dialer.h"
#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 "freertos/queue.h"
#include "esp_log.h"
#include <stdbool.h>
#include <string.h>
#define TAG "call_manager"
#define DIAL_COMPLETE_SILENCE_MS 3000
/* ---- FSM ---- */
typedef enum {
ST_IDLE,
ST_DIALTONE,
ST_DIALING,
ST_OUTGOING,
ST_INCOMING,
ST_ACTIVE
} call_state_t;
/* ---- Item de file BT ---- */
typedef struct {
bt_hfp_event_t ev;
bool msbc;
char number[32];
} bt_evt_item_t;
static QueueHandle_t s_bt_q = NULL;
/* ---- État hook (mis à jour depuis le callback hook_monitor, contexte quelconque) ---- */
static volatile bool s_offhook = false;
static volatile bool s_hook_dirty = false;
/* ---- Variables d'état SCO et numéro entrant (mis à jour uniquement dans call_task) ---- */
static char s_incoming_num[32];
static bool s_sco_msbc = false;
/* ---- État SCO (mis à jour uniquement dans call_task) ---- */
static bool s_sco_up = false;
static volatile bool s_sco_ready = false; /* SCO connecte (incl. sonnerie in-band) */
/* ------------------------------------------------------------------ */
/* 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)
{
if (!s_bt_q) return;
bt_evt_item_t it = { .ev = ev, .msbc = (d && d->msbc), .number = {0} };
if (d && d->number) {
strncpy(it.number, d->number, sizeof(it.number) - 1);
}
xQueueSend(s_bt_q, &it, 0); /* non bloquant : contexte tâche BT temps-réel */
}
/* ------------------------------------------------------------------ */
/* 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);
hal_i2s_audio_suspend(); /* codec + I2S en veille au repos (eco batterie) */
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;
}
/* ------------------------------------------------------------------ */
/* Tâche principale de la FSM d'appel */
/* ------------------------------------------------------------------ */
static void call_task(void *arg)
{
(void)arg;
call_state_t st = ST_IDLE;
for (;;) {
/* --- 1. Événements BT HFP : drain complet de la file ---- */
bt_evt_item_t bev;
while (s_bt_q && xQueueReceive(s_bt_q, &bev, 0) == pdTRUE) {
if (bev.ev == BT_HFP_EV_INCOMING && bev.number[0]) {
strncpy(s_incoming_num, bev.number, sizeof(s_incoming_num) - 1);
s_incoming_num[sizeof(s_incoming_num) - 1] = '\0';
}
if (bev.ev == BT_HFP_EV_AUDIO_CONNECTED) s_sco_msbc = bev.msbc;
switch (bev.ev) {
case BT_HFP_EV_INCOMING:
if (st == ST_IDLE || st == ST_DIALTONE) {
audio_router_tones_stop();
dtmf_stop(); /* pas de detection DTMF pendant un appel entrant */
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:
s_sco_ready = true;
if (st == ST_INCOMING) {
/* SCO ouvert pour la sonnerie in-band du mobile : NE PAS
* ouvrir le pont audio ni passer ACTIVE — la cloche doit
* sonner jusqu'au decroche reel. */
ESP_LOGI(TAG, "SCO in-band (sonnerie) — on continue de sonner");
} else {
/* Appel sortant decroche par le correspondant, ou reprise SCO. */
slic_ring_stop();
dtmf_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:
s_sco_ready = false;
if (s_sco_up) {
audio_router_sco_end();
s_sco_up = false;
}
break;
case BT_HFP_EV_CALL_ENDED:
s_sco_ready = false;
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;
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;
}
}
/* --- 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 + ouvrir le pont. */
slic_ring_stop();
bt_hfp_answer();
if (s_sco_ready) {
/* SCO deja etabli (sonnerie in-band) : ouvrir le pont. */
dtmf_stop();
audio_router_sco_begin(s_sco_msbc);
s_sco_up = true;
st = ST_ACTIVE;
ESP_LOGI(TAG, "décroché -> answer + pont SCO -> ACTIVE");
} else {
/* Pas encore de SCO : le pont s'ouvrira a AUDIO_CONNECTED. */
st = ST_ACTIVE;
ESP_LOGI(TAG, "bt_hfp_answer() envoyé, attente SCO");
}
} 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 : couper la tonalité, passer en DIALING */
audio_router_tones_stop();
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) {
/* 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_OUTGOING;
/* -> ACTIVE sera déclenché par BT_HFP_EV_AUDIO_CONNECTED */
}
}
/* Poll adaptatif : 250 ms au repos (raccroché, rien à faire — le hook IRQ
* et la file BT réveillent à temps), 50 ms sinon (numérotation/appel). */
vTaskDelay(pdMS_TO_TICKS(st == ST_IDLE ? 250 : 50));
}
}
/* ------------------------------------------------------------------ */
/* API publique */
/* ------------------------------------------------------------------ */
void call_manager_start(void)
{
s_bt_q = xQueueCreate(8, sizeof(bt_evt_item_t));
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é");
}
@@ -0,0 +1,32 @@
#pragma once
#include "bt_hfp.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* 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
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "cli.c"
INCLUDE_DIRS "include"
REQUIRES audio_router
)
+71
View File
@@ -0,0 +1,71 @@
/*
* cli.c — petit CLI serie (console UART0) pour regler les gains audio et le
* filtre micro a la volee, sans recompiler. Coexiste avec les logs ESP_LOG.
*/
#include "cli.h"
#include "audio_router.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TAG "cli"
static void handle(char *l)
{
if (strncmp(l, "gmic", 4) == 0) {
audio_router_set_mic_gain(atoi(l + 4));
ESP_LOGI(TAG, "gain micro = %d", audio_router_get_mic_gain());
} else if (strncmp(l, "gspk", 4) == 0) {
audio_router_set_play_gain(atoi(l + 4));
ESP_LOGI(TAG, "gain ecouteur = %d", audio_router_get_play_gain());
} else if (strncmp(l, "filt", 4) == 0) {
int on = atoi(l + 4);
audio_router_set_mic_filter(on != 0);
ESP_LOGI(TAG, "filtre micro = %s", on ? "ON" : "OFF");
} else if (strncmp(l, "aecd", 4) == 0) {
audio_router_set_aec_delay(atoi(l + 4));
ESP_LOGI(TAG, "AEC retard = %d samples", atoi(l + 4));
} else if (strncmp(l, "aecmu", 5) == 0) {
audio_router_set_aec_mu(atoi(l + 5));
ESP_LOGI(TAG, "AEC mu = %d%%", atoi(l + 5));
} else if (strncmp(l, "aec", 3) == 0) {
audio_router_set_aec(atoi(l + 3));
ESP_LOGI(TAG, "anti-echo = %d (0=off 1=suppress 2=NLMS)", audio_router_get_aec());
} else if (strncmp(l, "status", 6) == 0) {
ESP_LOGI(TAG, "gspk=%d gmic=%d aec=%d",
audio_router_get_play_gain(), audio_router_get_mic_gain(),
audio_router_get_aec());
} else {
ESP_LOGI(TAG, "cmds: gmic N|gspk N|filt 0/1|aec 0/1/2|aecd N|aecmu N|status");
}
}
static void cli_task(void *a)
{
(void)a;
char line[48];
int pos = 0;
ESP_LOGI(TAG, "CLI prete — gmic N / gspk N / filt 0/1 / status");
for (;;) {
int c = getchar();
if (c == EOF) {
vTaskDelay(pdMS_TO_TICKS(150)); /* repos : poll lent -> CPU dort (éco batterie) */
continue;
}
if (c == '\n' || c == '\r') {
if (pos > 0) { line[pos] = '\0'; handle(line); pos = 0; }
} else if (pos < (int)sizeof(line) - 1) {
line[pos++] = (char)c;
}
}
}
void cli_start(void)
{
xTaskCreate(cli_task, "cli", 3072, NULL, 3, NULL);
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/* Demarre un CLI serie (console UART) pour regler les gains/filtre a la volee :
* gmic <0..32> gain micro (uplink)
* gspk <0..32> gain ecouteur (downlink)
* filt <0|1> filtre passe-bande voix sur le micro
* status affiche les valeurs courantes
*/
void cli_start(void);
#ifdef __cplusplus
}
#endif
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "config_store.c"
INCLUDE_DIRS "include"
REQUIRES nvs_flash
)
+21
View File
@@ -0,0 +1,21 @@
#include "config_store.h"
#include "nvs_flash.h"
#include "esp_log.h"
static const char *TAG = "config_store";
esp_err_t config_store_init(void)
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "NVS illisible (%s) — effacement et réinit", esp_err_to_name(err));
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
if (err == ESP_OK) {
ESP_LOGI(TAG, "NVS prêt");
} else {
ESP_LOGE(TAG, "échec init NVS : %s", esp_err_to_name(err));
}
return err;
}
@@ -0,0 +1,17 @@
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* Initialise la partition NVS (effacement+réinit si version incompatible).
* @return ESP_OK si NVS est prêt, sinon le code d'erreur esp_err.
*/
esp_err_t config_store_init(void);
#ifdef __cplusplus
}
#endif
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "dialer.c"
INCLUDE_DIRS "include"
REQUIRES esp_timer
)
+82
View File
@@ -0,0 +1,82 @@
/*
* dialer.c — Digit accumulator for the PLIP telephone state machine.
*
* Digits are pushed from two sources:
* 1. HTTP /debug/dial?number=NNNN (net.c handler)
* 2. Rotary pulse detection (phone.c, CONFIG_PLIP_DIAL_PULSE)
*
* Thread-safety: all state is protected by atomic operations on s_last_us
* and simple writes to s_len / s_num (called from a single phone task or
* HTTP task at a time — no concurrent push expected).
*/
#include "dialer.h"
#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
#include <string.h>
#include <stdint.h>
#define TAG "dialer"
#define MAX_DIGITS 12
static char s_num[MAX_DIGITS + 1];
static int s_len;
static int64_t s_last_us; /* esp_timer_get_time() at last push */
void dialer_init(void)
{
memset(s_num, 0, sizeof(s_num));
s_len = 0;
s_last_us = 0;
ESP_LOGI(TAG, "dialer init");
}
void dialer_push_digit(int d)
{
if (d < 0 || d > 9) return;
if (s_len >= MAX_DIGITS) return; /* silently discard overflow */
s_num[s_len++] = (char)('0' + d);
s_num[s_len] = '\0';
s_last_us = esp_timer_get_time();
ESP_LOGI(TAG, "digit %d -> number so far: \"%s\"", d, s_num);
}
void dialer_reset(void)
{
memset(s_num, 0, sizeof(s_num));
s_len = 0;
s_last_us = 0;
ESP_LOGI(TAG, "dialer reset");
}
const char *dialer_current(void)
{
return s_num;
}
bool dialer_idle(void)
{
return s_len == 0;
}
int dialer_ms_since_last(void)
{
if (s_last_us == 0) return 0;
int64_t elapsed_us = esp_timer_get_time() - s_last_us;
int ms = (int)(elapsed_us / 1000);
return (ms < 0) ? 0 : ms;
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <stdbool.h>
void dialer_init(void);
void dialer_push_digit(int d);
void dialer_reset(void);
const char *dialer_current(void);
bool dialer_idle(void);
int dialer_ms_since_last(void);
+35
View File
@@ -0,0 +1,35 @@
#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;
}
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "dtmf.c"
INCLUDE_DIRS "include"
REQUIRES hal_i2s dialer
)
+344
View File
@@ -0,0 +1,344 @@
/*
* dtmf.c — Détecteur DTMF par algorithme de Goertzel.
*
* Adapté de plip_voice/main/dtmf.c pour le projet RTC_BL_PHONE (ESP-IDF v5.4).
* Changements par rapport à l'original :
* - Remplace audio_capture_read_frame/audio_capture_begin par
* hal_i2s_capture_read_frame/hal_i2s_capture_begin.
* - Remplace #include "audio.h" par #include "hal_i2s.h".
* - Supprime la dépendance sdkconfig.h / CONFIG_PLIP_DIAL_DTMF.
* - Garde DTMF_HOST_TEST : isole tous les includes et fonctions IDF/FreeRTOS
* afin que dtmf_detect_frame + Goertzel compilent en pur host (cc -lm).
*
* Pipeline de détection par frame 20 ms (320 échantillons @ 16 kHz) :
* 1. Calcul de la puissance Goertzel pour 8 fréquences DTMF (4 low + 4 high).
* 2. Identification du gagnant dans chaque groupe (best_low, best_high).
* 3. Trois gardes :
* a) Seuil énergie absolue — les deux doivent dépasser DTMF_ENERGY_THRESH.
* b) Rapport de dominance — le gagnant doit être > DTMF_DOMINANT_RATIO×
* le second dans le même groupe.
* c) Garde twist — ratio énergie (low/high) dans
* [1/DTMF_TWIST_MAX, DTMF_TWIST_MAX].
* 4. Débounce :
* - DTMF_CONFIRM_FRAMES frames consécutives identiques pour émettre.
* - DTMF_RELEASE_FRAMES frames de silence/différence avant ré-arm.
*
* Justification des seuils — voir commentaire détaillé dans l'original.
*/
/* -------------------------------------------------------------------------
* Includes communs (host + IDF)
* ---------------------------------------------------------------------- */
#include "dtmf.h"
#include <math.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* -------------------------------------------------------------------------
* Includes IDF uniquement (pas en mode test host)
* ---------------------------------------------------------------------- */
#ifndef DTMF_HOST_TEST
#include "hal_i2s.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#define TAG "dtmf"
/* Task 6 : dialer est disponible — on inclut l'en-tête réel. */
#include "dialer.h"
#endif /* DTMF_HOST_TEST */
/* -------------------------------------------------------------------------
* Seuils de détection (justification dans l'en-tête du fichier)
* ---------------------------------------------------------------------- */
#define DTMF_ENERGY_THRESH 4000000LL
#define DTMF_DOMINANT_RATIO 4.0f
#define DTMF_TWIST_MAX 8.0f
#define DTMF_CONFIRM_FRAMES 2
#define DTMF_RELEASE_FRAMES 1
/* -------------------------------------------------------------------------
* Table de fréquences DTMF
* ---------------------------------------------------------------------- */
#define NUM_LOW 4
#define NUM_HIGH 4
static const float LOW_FREQS[NUM_LOW] = { 697.0f, 770.0f, 852.0f, 941.0f };
static const float HIGH_FREQS[NUM_HIGH] = { 1209.0f, 1336.0f, 1477.0f, 1633.0f };
/* Matrice DTMF : [low_idx][high_idx] → caractère.
* Colonne 3 (1633 Hz) = ABCD, non utilisés sur téléphone standard → '\0'. */
static const char DTMF_MATRIX[NUM_LOW][NUM_HIGH] = {
{ '1', '2', '3', '\0' }, /* 697 Hz */
{ '4', '5', '6', '\0' }, /* 770 Hz */
{ '7', '8', '9', '\0' }, /* 852 Hz */
{ '*', '0', '#', '\0' }, /* 941 Hz */
};
/* -------------------------------------------------------------------------
* Cache des coefficients Goertzel (précalculés au premier appel)
* ---------------------------------------------------------------------- */
#define FS 16000 /* fréquence d'échantillonnage */
#define N_SAMP 320 /* taille de frame */
static bool s_coeff_ready = false;
static float s_low_coeff[NUM_LOW];
static float s_high_coeff[NUM_HIGH];
static void precompute_coeffs(void)
{
for (int i = 0; i < NUM_LOW; i++) {
float k = (float)N_SAMP * LOW_FREQS[i] / (float)FS;
s_low_coeff[i] = 2.0f * cosf(2.0f * (float)M_PI * k / (float)N_SAMP);
}
for (int i = 0; i < NUM_HIGH; i++) {
float k = (float)N_SAMP * HIGH_FREQS[i] / (float)FS;
s_high_coeff[i] = 2.0f * cosf(2.0f * (float)M_PI * k / (float)N_SAMP);
}
s_coeff_ready = true;
}
/* -------------------------------------------------------------------------
* Puissance Goertzel pour une fréquence unique
* Puissance = Q1² + Q2² Q1·Q2·coeff (non normalisée, ∝ amplitude²)
* ---------------------------------------------------------------------- */
static float goertzel_power(const int16_t *samples, int n, float coeff)
{
float q1 = 0.0f, q2 = 0.0f;
for (int i = 0; i < n; i++) {
float q0 = coeff * q1 - q2 + (float)samples[i];
q2 = q1;
q1 = q0;
}
return q1 * q1 + q2 * q2 - q1 * q2 * coeff;
}
/* -------------------------------------------------------------------------
* API publique : dtmf_detect_frame (fonction PURE — testable host)
* ---------------------------------------------------------------------- */
char dtmf_detect_frame(const int16_t *mono, int n)
{
if (n <= 0 || !mono) return '\0';
if (!s_coeff_ready) precompute_coeffs();
/* Calcul de la puissance Goertzel pour les 8 fréquences */
float low_pow[NUM_LOW], high_pow[NUM_HIGH];
for (int i = 0; i < NUM_LOW; i++)
low_pow[i] = goertzel_power(mono, n, s_low_coeff[i]);
for (int i = 0; i < NUM_HIGH; i++)
high_pow[i] = goertzel_power(mono, n, s_high_coeff[i]);
/* Recherche du plus fort dans chaque groupe */
int best_low = 0, best_high = 0;
float max_low = low_pow[0], max_high = high_pow[0];
for (int i = 1; i < NUM_LOW; i++) {
if (low_pow[i] > max_low) { max_low = low_pow[i]; best_low = i; }
}
for (int i = 1; i < NUM_HIGH; i++) {
if (high_pow[i] > max_high) { max_high = high_pow[i]; best_high = i; }
}
/* Garde (a) : seuil énergie absolue */
if ((int64_t)max_low < DTMF_ENERGY_THRESH || (int64_t)max_high < DTMF_ENERGY_THRESH)
goto no_tone;
/* Garde (b) : dominance de groupe — cherche le second meilleur */
{
float second_low = 0.0f, second_high = 0.0f;
for (int i = 0; i < NUM_LOW; i++) {
if (i != best_low && low_pow[i] > second_low)
second_low = low_pow[i];
}
for (int i = 0; i < NUM_HIGH; i++) {
if (i != best_high && high_pow[i] > second_high)
second_high = high_pow[i];
}
if (second_low > 0.0f && max_low < DTMF_DOMINANT_RATIO * second_low)
goto no_tone;
if (second_high > 0.0f && max_high < DTMF_DOMINANT_RATIO * second_high)
goto no_tone;
}
/* Garde (c) : twist — ratio de puissance dans [1/TWIST_MAX, TWIST_MAX] */
{
float ratio = max_low / max_high;
if (ratio > DTMF_TWIST_MAX || ratio < (1.0f / DTMF_TWIST_MAX))
goto no_tone;
}
/* --- État de débounce (statique) --- */
{
static char s_candidate = '\0';
static int s_confirm_count = 0;
static int s_release_count = 0;
static bool s_armed = true;
char sym = DTMF_MATRIX[best_low][best_high];
if (sym == '\0') goto no_tone; /* colonne 1633 Hz — ignorée */
/* Ton détecté : réinitialise le compteur de relâchement */
s_release_count = 0;
if (!s_armed) {
/* Attente de silence/relâchement avant d'accepter la prochaine pression */
return '\0';
}
if (sym == s_candidate) {
s_confirm_count++;
} else {
s_candidate = sym;
s_confirm_count = 1;
}
if (s_confirm_count >= DTMF_CONFIRM_FRAMES) {
/* Confirmé — rapporte et désarme jusqu'au relâchement */
s_confirm_count = 0;
s_candidate = '\0';
s_armed = false;
return sym;
}
return '\0';
no_tone:
/* Aucun ton valide : avance le compteur de relâchement */
; /* l'étiquette doit précéder une instruction */
s_confirm_count = 0;
s_candidate = '\0';
if (!s_armed) {
s_release_count++;
if (s_release_count >= DTMF_RELEASE_FRAMES) {
s_armed = true;
s_release_count = 0;
}
}
return '\0';
}
}
/* -------------------------------------------------------------------------
* Tâche de capture arrière-plan (IDF uniquement — pas en mode test host)
* ---------------------------------------------------------------------- */
#ifndef DTMF_HOST_TEST
#define DTMF_TASK_STACK 4096
#define DTMF_TASK_PRIO 3
#define DTMF_FRAME_SIZE 320
/* 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)
{
(void)arg;
int16_t frame[DTMF_FRAME_SIZE];
int64_t rms_sq;
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. */
if (hal_i2s_capture_begin() != ESP_OK) {
ESP_LOGE(TAG, "dtmf_task: capture_begin failed — task exits");
s_task_handle = NULL;
vTaskDelete(NULL);
return;
}
while (s_run) {
if (!s_armed_flag) {
/* 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);
if (got < 0) { vTaskDelay(pdMS_TO_TICKS(20)); continue; }
if (got == 0) continue;
char sym = dtmf_detect_frame(frame, got);
if (sym == '\0') continue;
ESP_LOGI(TAG, "DTMF détecté : '%c'", sym);
if (sym >= '0' && sym <= '9') {
dialer_push_digit(sym - '0');
}
/* '*' 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) {
/* 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;
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;
s_run = false;
/* Attend la sortie GRACIEUSE de la tâche : elle finit son i2s_channel_read
* en cours (timeout 100 ms) puis sort sur s_run=false. NE JAMAIS vTaskDelete
* de force : si la tâche est dans i2s_channel_read, elle détient le sémaphore
* du canal RX — la tuer le verrouille à vie et casse toute capture ultérieure
* (micro mort pendant les appels SCO). On attend largement plus que le read. */
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(500);
while (s_task_handle != NULL && xTaskGetTickCount() < deadline) {
vTaskDelay(pdMS_TO_TICKS(10));
}
if (s_task_handle != NULL) {
ESP_LOGW(TAG, "dtmf_stop: tâche pas encore sortie (PAS de delete forcé)");
} else {
ESP_LOGI(TAG, "dtmf_stop: tâche arrêtée proprement");
}
}
#endif /* DTMF_HOST_TEST */
+56
View File
@@ -0,0 +1,56 @@
#pragma once
/*
* dtmf.h — Détecteur DTMF par algorithme de Goertzel.
*
* API:
* dtmf_detect_frame() — traitement signal pur, sans I/O, testable en host
* dtmf_start() / dtmf_stop() — arme/désarme la tâche de capture arrière-plan
*
* La tâche de capture lit des frames 20 ms via hal_i2s_capture_read_frame()
* et appelle dialer_push_digit() pour les chiffres confirmés (0-9 uniquement).
*
* Garde DTMF_HOST_TEST : quand définie, seules les fonctions pures
* (dtmf_detect_frame + Goertzel) sont exposées — pas de dépendances FreeRTOS/IDF.
*/
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Analyse une frame mono 20 ms (320 échantillons à 16 kHz) pour détecter un
* ton DTMF.
*
* Retourne le caractère DTMF détecté ('0'-'9', '*', '#') une fois par pression
* confirmée, ou '\0' si rien n'est détecté ou si le décompte est en cours.
*
* Règles de débounce (état statique interne) :
* - Un symbole doit apparaître dans >= DTMF_CONFIRM_FRAMES frames
* consécutives pour être rapporté.
* - Après un rapport, il faut au moins DTMF_RELEASE_FRAMES frames de
* «silence» avant qu'un nouveau symbole (même ou différent) soit accepté.
*/
char dtmf_detect_frame(const int16_t *mono, int n);
#ifndef DTMF_HOST_TEST
/*
* Démarre la tâche DTMF en arrière-plan (créée une seule fois ; ré-arm
* idempotent). La tâche lit les frames micro et pousse les chiffres 0-9
* au dialer. Doit être appelée après hal_i2s_init().
*/
void dtmf_start(void);
/*
* Désarme la tâche DTMF. La tâche se suspend ; le flux RX continue pour
* la capture vocale (architecture full-duplex).
*/
void dtmf_stop(void);
#endif /* DTMF_HOST_TEST */
#ifdef __cplusplus
}
#endif
+65
View File
@@ -0,0 +1,65 @@
/* Test host de dtmf_detect_frame : synthétise des frames DTMF et vérifie le décodage.
* Compilation : cc -DDTMF_HOST_TEST -o /tmp/test_dtmf \
* -I components/dtmf/include \
* components/dtmf/test_dtmf.c components/dtmf/dtmf.c -lm
*/
#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).
* Amplitude 6000 par composante (12000 crête-à-crête) — en dessous de la
* saturation int16 (32767), évite l'écrêtage tout en restant bien au-dessus
* du seuil DTMF_ENERGY_THRESH = 4e6.
* Note : 9000+9000=18000 peut saturer sur certains codecs ; 6000+6000=12000
* est plus sûr et le détecteur n'est pas affaibli (seuils non modifiés). */
static void synth(int16_t *buf, int n, float f_low, float f_high)
{
for (int i = 0; i < n; i++) {
float s = 6000.0f * sinf(2.0f*(float)M_PI*f_low *i/FS)
+ 6000.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];
for (int i = 0; i < N; i++) zero[i] = 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;
}
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "es8388.c"
INCLUDE_DIRS "include"
REQUIRES esp_driver_i2c esp_driver_gpio bsp
)
+324
View File
@@ -0,0 +1,324 @@
/*
* es8388.c — ES8388 codec driver (IDF 5.x, nouvelle API i2c_master).
*
* Register map reference: ES8388 datasheet rev 1.6 (Everest Semiconductor).
* Init sequence derived from:
* - Espressif esp-adf es8388.c (Apache 2.0)
* - schreibfaul1/ESP32-audioI2S ES8388 init (MIT)
* - AI-Thinker SDK AudioKit driver
*
* The sequence configures the ES8388 for:
* - Master clock: MCLK from ESP32 GPIO0 at 12.288 MHz (or 256*Fs at 16kHz)
* - I2S format: I2S Philips, 16-bit, stereo
* - DAC: LOUT1/ROUT1 → headphone amp (GPIO21 PA_ENABLE must be HIGH)
* - ADC: LINPUT1/RINPUT1 mic (differential), gain 24 dB
* - Sample rate: 16 kHz (MCLKDIV = 256Fs)
*/
/* Transformation 4a : remplacement du bloc d'includes + déclarations handles I2C. */
#include "es8388.h"
#include "board_config.h"
#include <string.h>
#include "driver/i2c_master.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#define TAG "es8388"
/* I2C master bus + device handles (new driver/i2c_master.h API). */
static i2c_master_bus_handle_t s_i2c_bus = NULL;
static i2c_master_dev_handle_t s_es_dev = NULL;
/* ── ES8388 register addresses ──────────────────────────────────────────── */
#define ES8388_CHIP_CTL1 0x00 /* CHIP_CTL1 */
#define ES8388_CHIP_CTL2 0x01 /* CHIP_CTL2 */
#define ES8388_CHIP_POWER 0x02 /* CHIP_POWER (ADCPD, DACPD, etc.) */
#define ES8388_ADC_POWER 0x03 /* ADCPOWER */
#define ES8388_DAC_POWER 0x04 /* DACPOWER */
#define ES8388_CHIP_LP 0x05 /* CHIP_LP */
#define ES8388_CHIP_CTL3 0x06 /* CHIP_CTL3 */
#define ES8388_ADC_CTL1 0x09 /* ADCCONTROL1 — PGA gain */
#define ES8388_ADC_CTL2 0x0A /* ADCCONTROL2 — input select (LINSEL/RINSEL) */
#define ES8388_ADC_CTL3 0x0B /* ADCCONTROL3 — DS/DS filter select */
#define ES8388_ADC_CTL4 0x0C /* ADCCONTROL4 — I2S format / word length */
#define ES8388_ADC_CTL5 0x0D /* ADCCONTROL5 — MCLK divider (RATIO=256) */
#define ES8388_ADC_CTL7 0x0F /* ADCCONTROL7 — HPF enable/config */
#define ES8388_ADC_CTL8 0x10 /* ADCCONTROL8 — ADC L volume */
#define ES8388_ADC_CTL9 0x11 /* ADCCONTROL9 — ADC R volume */
#define ES8388_DAC_CTL1 0x17 /* DACCONTROL1 — I2S word len / format */
#define ES8388_DAC_CTL2 0x18 /* DACCONTROL2 — MCLK divider */
#define ES8388_DAC_CTL3 0x19 /* DACCONTROL3 — mute */
#define ES8388_DAC_CTL4 0x1A /* DACCONTROL4 — LDACVOL */
#define ES8388_DAC_CTL5 0x1B /* DACCONTROL5 — RDACVOL */
#define ES8388_DAC_CTL16 0x26 /* DACCONTROL16 — L/R mixer config */
#define ES8388_DAC_CTL17 0x27 /* DACCONTROL17 — L mixer gain */
#define ES8388_DAC_CTL20 0x2A /* DACCONTROL20 — R mixer gain */
#define ES8388_DAC_CTL21 0x2B /* DACCONTROL21 — ADC/DAC LRCK sync (bit7=1 for full-duplex) */
#define ES8388_DAC_CTL22 0x2C /* DACCONTROL22 — (unused in this config) */
#define ES8388_DAC_CTL23 0x2D /* DACCONTROL23 — (unused in this config) */
#define ES8388_DAC_CTL24 0x2E /* DACCONTROL24 — LOUT1VOL (OUT1 left volume) */
#define ES8388_DAC_CTL25 0x2F /* DACCONTROL25 — ROUT1VOL (OUT1 right volume) */
#define ES8388_DAC_CTL26 0x30 /* DACCONTROL26 — LOUT2VOL (OUT2 left volume) */
#define ES8388_DAC_CTL27 0x31 /* DACCONTROL27 — ROUT2VOL (OUT2 right volume) */
/* Volume register: 0x00=0dB (max), 0x21=-33dB step per 1.5dB, 0x24=mute. */
#define ES8388_VOL_MAX 0x00
#define ES8388_VOL_0DB 0x00
#define ES8388_VOL_MUTE 0x24
/* ── I2C helpers (Transformation 4b : nouvelle API i2c_master) ───────────── */
static esp_err_t i2c_write_reg(uint8_t reg, uint8_t value)
{
uint8_t buf[2] = { reg, value };
esp_err_t ret = i2c_master_transmit(s_es_dev, buf, sizeof(buf), 100);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "write reg 0x%02X=0x%02X failed: %s",
reg, value, esp_err_to_name(ret));
}
return ret;
}
esp_err_t es8388_read_reg(uint8_t reg, uint8_t *value)
{
return i2c_master_transmit_receive(s_es_dev, &reg, 1, value, 1, 100);
}
esp_err_t es8388_write_reg(uint8_t reg, uint8_t value)
{
return i2c_write_reg(reg, value);
}
/* ── Public API ──────────────────────────────────────────────────────────── */
esp_err_t es8388_init(void)
{
/* Transformation 4c : init I2C bus + device avec la nouvelle API i2c_master. */
i2c_master_bus_config_t bus_cfg = {
.i2c_port = PLIP_I2C_PORT,
.sda_io_num = PLIP_I2C_SDA,
.scl_io_num = PLIP_I2C_SCL,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
esp_err_t ret = i2c_new_master_bus(&bus_cfg, &s_i2c_bus);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "i2c_new_master_bus: %s", esp_err_to_name(ret));
return ret;
}
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = PLIP_ES8388_ADDR,
.scl_speed_hz = PLIP_I2C_FREQ_HZ,
};
ret = i2c_master_bus_add_device(s_i2c_bus, &dev_cfg, &s_es_dev);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "i2c_master_bus_add_device: %s", esp_err_to_name(ret));
return ret;
}
/* Quick device presence check. */
uint8_t chip_id = 0;
ret = es8388_read_reg(ES8388_CHIP_CTL1, &chip_id);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "ES8388 not found on I2C bus (addr=0x%02X): %s",
PLIP_ES8388_ADDR, esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "ES8388 detected: CHIP_CTL1=0x%02X", chip_id);
/* ── Full power-up + init sequence ─────────────────────────────────── */
/* 1. Reset. */
if (i2c_write_reg(ES8388_CHIP_CTL1, 0x80) != ESP_OK) return ESP_FAIL;
vTaskDelay(pdMS_TO_TICKS(20));
if (i2c_write_reg(ES8388_CHIP_CTL1, 0x00) != ESP_OK) return ESP_FAIL;
/* 2. Power down all blocks first (prevents pop on line-up). */
if (i2c_write_reg(ES8388_CHIP_POWER, 0xFF) != ESP_OK) return ESP_FAIL;
/* 3. CONTROL1: WORK_MODE = record+playback (0x10), VMIDSEL=10 (0x02).
* 0x12 = 0b00010010 — enables both ADC and DAC paths.
* Previous value 0x05 only enabled playback (WORK_MODE=00). */
if (i2c_write_reg(ES8388_CHIP_CTL1, 0x12) != ESP_OK) return ESP_FAIL;
/* 3b. CONTROL2 (0x01): VROI=0, LPVrefBuf=0, normal operation. */
if (i2c_write_reg(ES8388_CHIP_CTL2, 0x50) != ESP_OK) return ESP_FAIL;
/* 3c. Internal DLL stabilisation for LOW sample rates (16 kHz). UNDOCUMENTED
* registers 0x35/0x37/0x39 — the proven A252 driver (hardware/projects/
* slic-phone Es8388Driver.cpp) sets these to "disable internal DLL for low
* sample-rate stability". WITHOUT them the ADC clock domain is unstable at
* 16 kHz and the ADC outputs a frozen DC value (capture = flat, no AC) even
* though analog signal is present on the LIN pin — exactly our symptom. The
* DAC tolerates it, which is why playback worked but capture didn't. They
* must be set here in the boot sequence (a live poke can't re-lock the DLL). */
if (i2c_write_reg(0x35, 0xA0) != ESP_OK) return ESP_FAIL;
if (i2c_write_reg(0x37, 0xD0) != ESP_OK) return ESP_FAIL;
if (i2c_write_reg(0x39, 0xD0) != ESP_OK) return ESP_FAIL;
/* 4. Clock: MCLK divider = 256*Fs, DAC SRC = MCLK. */
if (i2c_write_reg(0x08, 0x00) != ESP_OK) return ESP_FAIL; /* MASTERMODE = slave */
/* 5. ADC power: power down to allow clean register writes. */
if (i2c_write_reg(ES8388_ADC_POWER, 0xFF) != ESP_OK) return ESP_FAIL;
/* 6. ADC config (follows Espressif esp-codec-dev reference sequence):
* - ADCCONTROL1 (0x09) = 0xBB: MIC PGA +24dB L and R
* - ADCCONTROL2 (0x0A) = 0x50: LINSEL=10 LIN2/RIN2 (telephone handset mic)
* Value 0x00 = LIN1 differential, 0x50 = LIN2 single-ended (A1S combiné)
* - ADCCONTROL3 (0x0B) = 0x02: DS filter select (required by reference)
* - ADCCONTROL4 (0x0C) = 0x0C: I2S Philips 16-bit word length
* - ADCCONTROL5 (0x0D) = 0x02: ADCFsMode SINGLE SPEED RATIO=256 (16kHz@MCLK 4.096MHz)
* - ADCCONTROL8/9 (0x10/0x11) = 0x00: ADC digital volume 0dB */
if (i2c_write_reg(ES8388_ADC_CTL1, 0x88) != ESP_OK) return ESP_FAIL; /* 0x88 = MICAMPL/MICAMPR=0b10 → MIC PGA +12 dB L+R — the SLIC handset mic is QUIET; +12 dB lifts speech above the capture VAD onset without clipping (measured clean at ~5% peak post DLL-fix). */
/* ADCCONTROL2 (0x0A): input select. The K50835F SLIC handset transmit audio is
* wired to LIN2/RIN2 on this bench — PROVEN: speech captured (ACrms 196, crest 10.3)
* on 0x50, vs DC-only floating offset on 0x00 (LIN1). */
if (i2c_write_reg(ES8388_ADC_CTL2, 0x50) != ESP_OK) return ESP_FAIL; /* LIN2/RIN2 — SLIC handset mic */
if (i2c_write_reg(ES8388_ADC_CTL3, 0x02) != ESP_OK) return ESP_FAIL; /* DS filter sel */
if (i2c_write_reg(ES8388_ADC_CTL4, 0x0C) != ESP_OK) return ESP_FAIL; /* I2S 16-bit */
if (i2c_write_reg(ES8388_ADC_CTL5, 0x02) != ESP_OK) return ESP_FAIL; /* RATIO=256 */
if (i2c_write_reg(0x0E, 0x00) != ESP_OK) return ESP_FAIL; /* ADCCONTROL6: clear ADCSMUTE bit5 (reset default 0x30 = ADC output muted) */
if (i2c_write_reg(ES8388_ADC_CTL8, 0x00) != ESP_OK) return ESP_FAIL; /* ADC vol L 0dB */
if (i2c_write_reg(ES8388_ADC_CTL9, 0x00) != ESP_OK) return ESP_FAIL; /* ADC vol R 0dB */
/* 8. DAC I2S: 16-bit I2S Philips, MCLK/256, no softmute. */
if (i2c_write_reg(ES8388_DAC_CTL1, 0x18) != ESP_OK) return ESP_FAIL;
if (i2c_write_reg(ES8388_DAC_CTL2, 0x02) != ESP_OK) return ESP_FAIL; /* DACLRCKDIV=256 */
if (i2c_write_reg(ES8388_DAC_CTL3, 0x00) != ESP_OK) return ESP_FAIL; /* mute off */
/* 9. DAC volume: 0 dB on both channels. */
if (i2c_write_reg(ES8388_DAC_CTL4, ES8388_VOL_0DB) != ESP_OK) return ESP_FAIL;
if (i2c_write_reg(ES8388_DAC_CTL5, ES8388_VOL_0DB) != ESP_OK) return ESP_FAIL;
/* 10. Mixer: L→LOUT, R→ROUT (straight through, no cross-mix). */
if (i2c_write_reg(ES8388_DAC_CTL16, 0x1B) != ESP_OK) return ESP_FAIL;
if (i2c_write_reg(ES8388_DAC_CTL17, 0x90) != ESP_OK) return ESP_FAIL;
if (i2c_write_reg(ES8388_DAC_CTL20, 0x90) != ESP_OK) return ESP_FAIL;
/* 11. DACCONTROL21 (0x2B): ADC+DAC LRCK sync.
* Espressif reference es8388_start():
* 0xC0 = LINE mode (analog bypass, not ADC digital path)
* 0x80 = DAC+ADC digital record+playback mode (bit7 only)
* We want digital ADC recording + DAC playback → use 0x80. */
if (i2c_write_reg(ES8388_DAC_CTL21, 0x80) != ESP_OK) return ESP_FAIL;
/* 11b. Restart internal state machine (required after DACCONTROL21 change).
* Without this pulse, the ADC clock domain may not synchronise properly.
* Espressif reference sequence: CHIPPOWER=0xF0 then 0x00 to restart FSM. */
if (i2c_write_reg(ES8388_CHIP_POWER, 0xF0) != ESP_OK) return ESP_FAIL;
vTaskDelay(pdMS_TO_TICKS(5));
if (i2c_write_reg(ES8388_CHIP_POWER, 0x00) != ESP_OK) return ESP_FAIL;
vTaskDelay(pdMS_TO_TICKS(10));
/* DACCONTROL23 (0x2D): VROI=0, normal output. */
if (i2c_write_reg(ES8388_DAC_CTL23, 0x00) != ESP_OK) return ESP_FAIL;
/* 12. Volume: OUT1L/R = 0x1E (0dB), OUT2L = 0 (speaker vol set by PA separately). */
if (i2c_write_reg(ES8388_DAC_CTL24, 0x1E) != ESP_OK) return ESP_FAIL; /* LOUT1VOL */
if (i2c_write_reg(ES8388_DAC_CTL25, 0x1E) != ESP_OK) return ESP_FAIL; /* ROUT1VOL */
if (i2c_write_reg(ES8388_DAC_CTL26, 0x00) != ESP_OK) return ESP_FAIL; /* LOUT2VOL */
/* 13. DAC power: power up DAC L+R. */
if (i2c_write_reg(ES8388_DAC_POWER, 0x3C) != ESP_OK) return ESP_FAIL;
/* 14. ADC power: full power-up (all Pdn bits cleared = 0x00).
* 0x09 is the intermediate state (Espressif es8388_open end-state), but
* the full ADC + analog input power-up requires 0x00 (Espressif es8388_start(ADC)).
* Must come AFTER DACCONTROL21 state machine restart and ADCCONTROL6 unmute. */
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. 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, 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;
if (es8388_read_reg(ES8388_CHIP_CTL1, &ctl1) != ESP_OK)
ESP_LOGW(TAG, "read CHIP_CTL1 failed");
if (es8388_read_reg(ES8388_ADC_POWER, &adcpwr) != ESP_OK)
ESP_LOGW(TAG, "read ADC_POWER failed");
if (es8388_read_reg(ES8388_ADC_CTL2, &adcinsel) != ESP_OK) /* ADCCONTROL2 = input sel (0x50=LIN2) */
ESP_LOGW(TAG, "read ADC_CTL2 failed");
if (es8388_read_reg(ES8388_ADC_CTL3, &adcctl3) != ESP_OK) /* ADCCONTROL3 = DS filter (0x02) */
ESP_LOGW(TAG, "read ADC_CTL3 failed");
if (es8388_read_reg(ES8388_DAC_CTL21, &dacctl21) != ESP_OK) /* DACCONTROL21 = ADC+DAC LRCK sync (0xC0) */
ESP_LOGW(TAG, "read DAC_CTL21 failed");
if (es8388_read_reg(ES8388_CHIP_POWER, &chippower) != ESP_OK)
ESP_LOGW(TAG, "read CHIP_POWER failed");
ESP_LOGI(TAG, "ES8388 regs: CTL1=0x%02X ADCPWR=0x%02X ADCINSEL=0x%02X ADCCTL3=0x%02X DACCTL21=0x%02X CHIPPOWER=0x%02X",
ctl1, adcpwr, adcinsel, adcctl3, dacctl21, chippower);
ESP_LOGI(TAG, "ES8388 init OK - PA GPIO configure (sortie, off par defaut), DAC @ 0dB, ADC PGA +12dB, input=LIN2/RIN2 (SLIC handset), DACCTL21=0x80");
return ESP_OK;
}
esp_err_t es8388_set_volume(uint8_t vol)
{
/* ES8388 OUTx volume registers are GAIN, not attenuation: 0x00 = -45 dB
* (min) .. 0x21 = 0 dB (max); higher value = louder (>0x21 = mute/reserved).
* Map 0..100 → 0x00..0x21. (The previous code inverted this, so vol=100
* produced 0x00 = quietest — confirmed at the bench.)
* DACCONTROL24 (0x2E)=OUT1L, 25 (0x2F)=OUT1R, 26 (0x30)=OUT2L, 27 (0x31)=OUT2R.
* DACCONTROL21 (0x2B) is the ADC/DAC LRCK sync register — DO NOT touch here. */
if (vol > 100) vol = 100;
uint8_t reg_val = (uint8_t)((int)vol * 0x21 / 100);
if (reg_val > 0x21) reg_val = 0x21;
ESP_LOGI(TAG, "set_volume: %d%% -> reg=0x%02X (0x21=max,0dB)", vol, reg_val);
esp_err_t r = i2c_write_reg(ES8388_DAC_CTL24, reg_val); /* OUT1 L volume */
if (r != ESP_OK) return r;
r = i2c_write_reg(ES8388_DAC_CTL25, reg_val); /* OUT1 R volume */
if (r != ESP_OK) return r;
r = i2c_write_reg(ES8388_DAC_CTL26, reg_val); /* OUT2 L volume */
if (r != ESP_OK) return r;
r = i2c_write_reg(ES8388_DAC_CTL27, reg_val); /* OUT2 R volume */
return r;
}
esp_err_t es8388_set_dac_volume(uint8_t atten)
{
/* DACCONTROL4 (0x04) = LDACVOL, DACCONTROL5 (0x05) = RDACVOL: DIGITAL DAC
* volume, applied BEFORE the analog output stages. 0x00 = 0 dB, each step
* = -0.5 dB, up to 0xC0 = -96 dB (mute). Lowering this gives analog
* headroom while keeping the output-stage volume (es8388_set_volume) high. */
if (atten > 0xC0) atten = 0xC0;
ESP_LOGI(TAG, "set_dac_volume: atten=0x%02X (-%.1f dB)", atten, atten * 0.5f);
esp_err_t r = i2c_write_reg(ES8388_DAC_CTL4, atten);
if (r != ESP_OK) return r;
r = i2c_write_reg(ES8388_DAC_CTL5, atten);
return r;
}
esp_err_t es8388_mute(bool mute)
{
uint8_t val = mute ? 0x04 : 0x00; /* bit2 = DACMUTE */
ESP_LOGI(TAG, "mute: %s", mute ? "on" : "off");
return i2c_write_reg(ES8388_DAC_CTL3, val);
}
/* Mise en veille : coupe les blocs analogiques ADC/DAC (gros poste de conso).
* Les registres de config (formats, routage, volumes) restent memorises tant
* que le codec est alimente -> le reveil ne re-ecrit que l'alimentation. */
esp_err_t es8388_suspend(void)
{
i2c_write_reg(ES8388_DAC_CTL3, 0x04); /* mute DAC */
i2c_write_reg(ES8388_DAC_POWER, 0xC0); /* DAC L+R OFF */
i2c_write_reg(ES8388_ADC_POWER, 0xFF); /* ADC + entrees analogiques OFF */
ESP_LOGI(TAG, "ES8388 suspend (ADC/DAC off)");
return ESP_OK;
}
esp_err_t es8388_resume(void)
{
i2c_write_reg(ES8388_ADC_POWER, 0x00); /* ADC ON (full power-up) */
i2c_write_reg(ES8388_DAC_POWER, 0x3C); /* DAC L+R ON */
i2c_write_reg(ES8388_DAC_CTL3, 0x00); /* unmute */
ESP_LOGI(TAG, "ES8388 resume (ADC/DAC on)");
return ESP_OK;
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Init I2C master bus (driver/i2c_master.h) + ES8388 register sequence + PA on.
* Returns ESP_OK if the codec answers on I2C and is configured. */
esp_err_t es8388_init(void);
/* Output volume 0..100 %. */
esp_err_t es8388_set_volume(uint8_t vol);
/* Digital DAC attenuation: 0x00 = 0 dB .. 0xC0 = -96 dB. */
esp_err_t es8388_set_dac_volume(uint8_t atten);
/* Mute / unmute the DAC. */
esp_err_t es8388_mute(bool mute);
/* Veille / reveil basse-conso : coupe / reactive les blocs ADC+DAC. */
esp_err_t es8388_suspend(void);
esp_err_t es8388_resume(void);
/* Diagnostic register access. */
esp_err_t es8388_read_reg(uint8_t reg, uint8_t *value);
esp_err_t es8388_write_reg(uint8_t reg, uint8_t value);
#ifdef __cplusplus
}
#endif
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "hal_i2s.c"
INCLUDE_DIRS "include"
REQUIRES esp_driver_i2s esp_driver_gpio bsp hal_es8388
)
+173
View File
@@ -0,0 +1,173 @@
/*
* hal_i2s.c — ES8388 + I2S full-duplex bring-up (ESP-IDF i2s_std).
* Adapté de plip_voice/main/audio.c (audio_init), sans WAV/SD/capture.
*/
#include "hal_i2s.h"
#include "board_config.h"
#include "es8388.h"
#include "driver/gpio.h"
#include "driver/i2s_std.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include <stdlib.h>
#define TAG "hal_i2s"
#define HAL_I2S_SAMPLE_RATE PLIP_SAMPLE_RATE /* 16000 */
#define HAL_I2S_EARPIECE_VOL 95 /* écouteur combiné : niveau haut */
static i2s_chan_handle_t s_spk_handle = NULL;
static i2s_chan_handle_t s_mic_handle = NULL;
#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;
void hal_audio_pa_set(bool enable)
{
gpio_set_level(PLIP_PA_ENABLE, enable ? 1 : 0);
ESP_LOGI(TAG, "PA %s", enable ? "ON" : "OFF");
}
i2s_chan_handle_t hal_i2s_spk_handle(void) { return s_spk_handle; }
i2s_chan_handle_t hal_i2s_mic_handle(void) { return s_mic_handle; }
/* Veille audio (au repos) : coupe les canaux I2S (donc MCLK/BCLK) + ES8388.
* Le hook (SHK) et la sonnerie (SLIC) restent en GPIO -> telephonie reactive. */
static bool s_audio_active = true;
void hal_i2s_audio_suspend(void)
{
if (!s_audio_active) return;
s_audio_active = false;
es8388_suspend(); /* ADC/DAC analogiques OFF */
if (s_spk_handle) i2s_channel_disable(s_spk_handle);
if (s_mic_handle) i2s_channel_disable(s_mic_handle);
ESP_LOGI(TAG, "audio veille (I2S + codec OFF)");
}
void hal_i2s_audio_resume(void)
{
if (s_audio_active) return;
s_audio_active = true;
if (s_spk_handle) i2s_channel_enable(s_spk_handle); /* horloges d'abord */
if (s_mic_handle) i2s_channel_enable(s_mic_handle);
es8388_resume(); /* puis reveil du codec */
ESP_LOGI(TAG, "audio réveil (I2S + codec ON)");
}
esp_err_t hal_i2s_init(void)
{
/* 1. ES8388 I2C init + register sequence (enables PA internally). */
esp_err_t ret = es8388_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "es8388_init failed: %s", esp_err_to_name(ret));
return ret;
}
es8388_set_volume(HAL_I2S_EARPIECE_VOL);
/* 2. Allocate the full-duplex channel pair (TX = speaker, RX = mic). */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(PLIP_I2S_NUM,
I2S_ROLE_MASTER);
chan_cfg.auto_clear = true;
ret = i2s_new_channel(&chan_cfg, &s_spk_handle, &s_mic_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "i2s_new_channel: %s", esp_err_to_name(ret));
return ret;
}
/* 3. Full-duplex std config — IDENTICAL for TX and RX (memcmp). If the two
* configs differ, IDF tries to move RX to I2S_NUM_1 on ESP32 HW v1 and the
* clock routing breaks (permanent zeros). Keep dout AND din in the same cfg. */
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(HAL_I2S_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = PLIP_I2S_MCLK,
.bclk = PLIP_I2S_BCLK,
.ws = PLIP_I2S_WS,
.dout = PLIP_I2S_DOUT,
.din = PLIP_I2S_DIN,
.invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false },
},
};
ret = i2s_channel_init_std_mode(s_spk_handle, &std_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "i2s_channel_init_std_mode (TX): %s", esp_err_to_name(ret));
return ret;
}
ret = i2s_channel_init_std_mode(s_mic_handle, &std_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "i2s_channel_init_std_mode (RX): %s", esp_err_to_name(ret));
return ret;
}
/* 4. Enable both TX and RX (both needed for correct ASDOUT→DIN routing). */
ret = i2s_channel_enable(s_spk_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "i2s_channel_enable (TX): %s", esp_err_to_name(ret));
return ret;
}
ret = i2s_channel_enable(s_mic_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "i2s_channel_enable (RX): %s — capture disabled",
esp_err_to_name(ret));
i2s_del_channel(s_mic_handle); /* libérer la ressource IDF allouée */
s_mic_handle = NULL;
}
if (s_mic_handle) {
ESP_LOGI(TAG, "I2S init OK — 16 kHz 16-bit stereo, full-duplex");
} else {
ESP_LOGW(TAG, "I2S init OK — 16 kHz 16-bit stereo, TX only (capture disabled)");
}
return ESP_OK;
}
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: pret (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));
/* Pendant le SCO, i2s_channel_read revient souvent ESP_ERR_TIMEOUT avec un
* transfert PARTIEL (ex. 960/1280 octets) a cause de la gigue BT. Il faut
* UTILISER ces octets, pas les jeter. On ne renvoie 0 que si rien n'a ete lu. */
if (bytes_read == 0) return 0;
if (ret != ESP_OK && ret != ESP_ERR_TIMEOUT) return -1; /* vraie erreur */
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; }
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#include "driver/i2s_std.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Initialise the codec (es8388) then the I2S full-duplex channel pair.
* Must be called once before any audio. Returns ESP_OK on success. */
esp_err_t hal_i2s_init(void);
/* Speaker (TX) I2S channel handle — used by audio_router to write samples. */
i2s_chan_handle_t hal_i2s_spk_handle(void);
/* Microphone (RX) I2S channel handle — used in Phase 3 for capture. */
i2s_chan_handle_t hal_i2s_mic_handle(void);
/* Enable/disable the speaker power amplifier (GPIO21, active HIGH). */
void hal_audio_pa_set(bool enable);
/* Veille / reveil audio basse-conso (coupe/reactive I2S + codec ES8388).
* A appeler au repos (raccroche, hors appel) et au reveil (decroche/appel). */
void hal_i2s_audio_suspend(void);
void hal_i2s_audio_resume(void);
/* 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);
#ifdef __cplusplus
}
#endif
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "hook_monitor.c"
INCLUDE_DIRS "include"
REQUIRES esp_timer slic_ks0835 dialer
)
+115
View File
@@ -0,0 +1,115 @@
/*
* 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 /* décroché : résolution fine pour décoder les impulsions */
#define POLL_IDLE_MS 80 /* raccroché (repos) : poll lent -> CPU dort + DFS (éco batterie) */
#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;
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 →
* 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;
}
}
/* Poll adaptatif : 10 ms décroché (décodage impulsions), 80 ms raccroché
* (repos) -> le CPU dort plus longtemps, DFS/tickless idle s'engagent. */
vTaskDelay(pdMS_TO_TICKS(stable ? POLL_MS : POLL_IDLE_MS));
}
}
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", 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;
}
bool hook_monitor_offhook(void) { return s_offhook; }
@@ -0,0 +1,20 @@
#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
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "slic.c"
INCLUDE_DIRS "include"
REQUIRES esp_driver_gpio bsp
)
+59
View File
@@ -0,0 +1,59 @@
/*
* slic.h — K50835F / AG1171-class SLIC control (ESP-IDF port of Ks0835SlicController).
*
* Pins (A1S board, from board_config.h):
* RM GPIO18 Ring Mode output — HIGH = ring burst active
* FR GPIO5 Forward/Reverse out — toggled at ~25 Hz during ring to drive bell
* SHK GPIO23 Switch Hook input — HIGH = off-hook (active-high)
* PD GPIO19 Power Down (OD) — HIGH = SLIC powered (released open-drain)
*
* Power-up: PD is open-drain, written HIGH → high-impedance → SLIC active.
* Power-down: PD driven LOW → SLIC off (no audio, no hook sense).
*
* Ring: slic_ring_start() sets RM=1 and spawns/enables a 20 ms FR-toggle task.
* slic_ring_stop() clears RM=0, FR=0, pauses the toggle task.
*
* Hook: slic_is_offhook() reads SHK; HIGH = off-hook (matches A252ConfigStore default).
*/
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* Initialise SLIC GPIO and power up the SLIC.
* Must be called early in boot, before audio init.
* Returns ESP_OK on success.
*/
esp_err_t slic_init(void);
/**
* Returns true when the handset is off-hook (SHK GPIO23 HIGH).
*/
bool slic_is_offhook(void);
/**
* Activate ringing: RM=HIGH + FR toggles at ~25 Hz (20 ms period).
* No-op if already ringing.
*/
void slic_ring_start(void);
/**
* Stop ringing: RM=LOW, FR=LOW, FR toggle suspended.
* No-op if not ringing.
*/
void slic_ring_stop(void);
/**
* Returns true if slic_ring_start() was called and not yet stopped.
*/
bool slic_is_ringing(void);
#ifdef __cplusplus
}
#endif
+185
View File
@@ -0,0 +1,185 @@
/*
* slic.c — K50835F / AG1171-class SLIC control (ESP-IDF port).
*
* Ported from hardware/projects/slic-phone/src/slic/Ks0835SlicController.cpp
* (Arduino) to bare ESP-IDF 5.x GPIO driver.
*
* Key differences from Arduino original:
* - Open-drain on PD achieved via GPIO_MODE_OUTPUT_OD + gpio_set_level(PD, 1)
* - Ring FR toggle runs as a FreeRTOS task instead of a cooperative tick()
* - hook_active_high is hard-coded TRUE (matches A252ConfigStore default)
*/
#include "slic.h"
#include "board_config.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define TAG "slic"
/* Empirically on THIS A1S+SLIC unit, SHK is ACTIVE-HIGH: off-hook (loop closed)
* drives SHK HIGH, on-hook drives it LOW. Confirmed at the bench via
* /debug/hookmon: on-hook→0, pickup-during-ring→0→1 transition, off-hook→1.
* (The A252 reference was active-low; the polarity is inverted here, so the
* raw straight-wired GPIO reads off-hook = HIGH.) */
/* 1=HIGH off-hook (plip_voice proven). Mettre 0 si le test decroche est inverse. */
#define SLIC_SHK_OFFHOOK_LEVEL 1
static volatile bool s_ringing = false;
static TaskHandle_t s_ring_task = NULL;
/* France Télécom ring cadence: 1.5 s burst ON, 3.5 s silent pause, repeating.
* (Single-ring cadence — distinct from the UK double-ring or US 2 s/4 s.)
* The bell is driven during the burst by toggling FR at ~25 Hz with RM HIGH;
* during the pause RM and FR are held low so the bell is silent. */
#define RING_FR_TOGGLE_MS 20 /* 20 ms half-period → ~25 Hz bell drive */
#define RING_BURST_MS 1500 /* FT: 1.5 s of ringing */
#define RING_PAUSE_MS 3500 /* FT: 3.5 s of silence */
/* ── FR toggle + cadence task ─────────────────────────────────────────────── */
static void slic_ring_task(void *arg)
{
(void)arg;
bool fr_state = false;
bool in_burst = true; /* start each ring with a burst */
int phase_ms = 0; /* elapsed ms in the current burst/pause phase */
for (;;) {
if (s_ringing) {
if (in_burst) {
/* Ring burst: RM HIGH, FR toggling at ~25 Hz to swing the bell */
gpio_set_level(PLIP_SLIC_RM, 1);
fr_state = !fr_state;
gpio_set_level(PLIP_SLIC_FR, fr_state ? 1 : 0);
phase_ms += RING_FR_TOGGLE_MS;
if (phase_ms >= RING_BURST_MS) {
/* End of burst → enter silent pause */
in_burst = false;
phase_ms = 0;
fr_state = false;
gpio_set_level(PLIP_SLIC_RM, 0);
gpio_set_level(PLIP_SLIC_FR, 0);
ESP_LOGV(TAG, "ring: burst end -> pause");
}
} else {
/* Silent pause: RM/FR stay low */
phase_ms += RING_FR_TOGGLE_MS;
if (phase_ms >= RING_PAUSE_MS) {
in_burst = true;
phase_ms = 0;
ESP_LOGV(TAG, "ring: pause end -> burst");
}
}
} else {
/* Idle — keep RM/FR low and reset the cadence for the next ring */
gpio_set_level(PLIP_SLIC_RM, 0);
gpio_set_level(PLIP_SLIC_FR, 0);
fr_state = false;
in_burst = true;
phase_ms = 0;
}
vTaskDelay(pdMS_TO_TICKS(RING_FR_TOGGLE_MS));
}
}
/* ── Public API ──────────────────────────────────────────────────────────── */
esp_err_t slic_init(void)
{
/* RM: Ring Mode output, init LOW.
* INPUT_OUTPUT (not plain OUTPUT) so gpio_get_level() reads back the real
* pad level — plain OUTPUT disables the input buffer and always reads 0. */
gpio_config_t rm_cfg = {
.pin_bit_mask = (1ULL << PLIP_SLIC_RM),
.mode = GPIO_MODE_INPUT_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t ret = gpio_config(&rm_cfg);
if (ret != ESP_OK) return ret;
gpio_set_level(PLIP_SLIC_RM, 0);
/* FR: Forward/Reverse output, init LOW (INPUT_OUTPUT for readback, see RM). */
gpio_config_t fr_cfg = {
.pin_bit_mask = (1ULL << PLIP_SLIC_FR),
.mode = GPIO_MODE_INPUT_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ret = gpio_config(&fr_cfg);
if (ret != ESP_OK) return ret;
gpio_set_level(PLIP_SLIC_FR, 0);
/* SHK: Switch Hook input with pull-up (physical line has no external pull) */
gpio_config_t shk_cfg = {
.pin_bit_mask = (1ULL << PLIP_SLIC_SHK),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ret = gpio_config(&shk_cfg);
if (ret != ESP_OK) return ret;
/* PD: Power Down — EXACT A252-proven sequence: open-drain output, HIGH = released
* = SLIC active (setPowerDown(false) in Ks0835SlicController). This is the config
* the working Arduino slic-phone project uses on the same chip. */
/* INPUT_OUTPUT_OD (not plain OUTPUT_OD) so gpio_get_level() reads the real
* pad level — OUTPUT_OD also disables the input buffer and would read 0. */
ret = gpio_set_direction(PLIP_SLIC_PD, GPIO_MODE_INPUT_OUTPUT_OD);
if (ret != ESP_OK) return ret;
ret = gpio_set_level(PLIP_SLIC_PD, 1); /* open-drain released HIGH = active (A252-proven) */
if (ret != ESP_OK) return ret;
ESP_LOGI(TAG, "slic: PD GPIO%d open-drain HIGH (A252-proven active) — testing with fixed mic", PLIP_SLIC_PD);
ESP_LOGI(TAG, "slic: pins RM=%d FR=%d SHK=%d PD=%d hook_active_high=1",
PLIP_SLIC_RM, PLIP_SLIC_FR, PLIP_SLIC_SHK, PLIP_SLIC_PD);
/* Spawn the FR-toggle task (runs indefinitely, toggles only when s_ringing=true) */
BaseType_t ok = xTaskCreatePinnedToCore(slic_ring_task, "slic_ring",
2048, NULL, 4, &s_ring_task, 1);
if (ok != pdPASS) {
ESP_LOGE(TAG, "Failed to create slic_ring task");
return ESP_ERR_NO_MEM;
}
int shk = gpio_get_level(PLIP_SLIC_SHK);
ESP_LOGI(TAG, "SLIC init OK — SHK level=%d (%s)",
shk, (shk == SLIC_SHK_OFFHOOK_LEVEL) ? "off-hook" : "on-hook");
return ESP_OK;
}
bool slic_is_offhook(void)
{
return gpio_get_level(PLIP_SLIC_SHK) == SLIC_SHK_OFFHOOK_LEVEL;
}
void slic_ring_start(void)
{
if (s_ringing) return;
ESP_LOGI(TAG, "ring start: France Télécom cadence %d ms ON / %d ms OFF",
RING_BURST_MS, RING_PAUSE_MS);
/* The cadence task drives RM/FR; it starts on a burst (in_burst reset in
* the idle branch). Just arm it here. */
s_ringing = true;
}
void slic_ring_stop(void)
{
if (!s_ringing) return;
ESP_LOGI(TAG, "ring stop: RM=LOW, FR=LOW");
s_ringing = false;
gpio_set_level(PLIP_SLIC_RM, 0);
gpio_set_level(PLIP_SLIC_FR, 0);
}
bool slic_is_ringing(void)
{
return s_ringing;
}
+4
View File
@@ -0,0 +1,4 @@
idf_component_register(
SRCS "tone_gen.c"
INCLUDE_DIRS "include"
)
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Fill `buf` with n_frames stereo frames (L == R) of a sine wave.
* buf must hold n_frames*2 int16_t. *phase is the running mono sample index,
* advanced by n_frames. amplitude is the peak amplitude (the output is clamped
* to the int16 range [-32768, 32767], so values above 32767 saturate). */
void tone_gen_fill_stereo(int16_t *buf, int n_frames, uint32_t *phase,
float freq_hz, float amplitude, uint32_t sample_rate);
#ifdef __cplusplus
}
#endif
+47
View File
@@ -0,0 +1,47 @@
/* Test host pur (compilé avec cc, hors IDF) pour tone_gen_fill_stereo. */
#include "tone_gen.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
int main(void)
{
int16_t buf[8 * 2];
uint32_t phase = 0;
/* 1. À phase 0, sin(0) = 0 → premier échantillon nul, L == R. */
tone_gen_fill_stereo(buf, 8, &phase, 440.0f, 8000.0f, 16000);
assert(buf[0] == 0);
assert(buf[1] == 0);
for (int i = 0; i < 8; i++) assert(buf[i*2] == buf[i*2+1]); /* L == R */
/* 2. La phase a avancé de 8 échantillons. */
assert(phase == 8);
/* 3. Amplitude bornée par l'amplitude crête demandée (+1 d'arrondi). */
uint32_t ph2 = 0;
int16_t buf2[64 * 2];
tone_gen_fill_stereo(buf2, 64, &ph2, 440.0f, 8000.0f, 16000);
for (int i = 0; i < 64; i++) {
assert(buf2[i*2] <= 8001 && buf2[i*2] >= -8001);
}
/* 4. Valeur conforme au sinus attendu à un index connu (phase=4, 440Hz/16k). */
float expected = 8000.0f * sinf(2.0f * (float)M_PI * 440.0f * 4.0f / 16000.0f);
/* buf2[4] correspond à phase==4 (rempli depuis phase 0). */
int16_t got = buf2[4 * 2];
assert(fabsf((float)got - expected) < 2.0f);
/* 5. Saturation : une amplitude > 32767 ne doit jamais déborder l'int16. */
uint32_t ph3 = 0;
int16_t buf3[256 * 2];
tone_gen_fill_stereo(buf3, 256, &ph3, 440.0f, 40000.0f, 16000);
for (int i = 0; i < 256; i++) {
assert(buf3[i*2] <= 32767);
assert(buf3[i*2] >= -32768);
assert(buf3[i*2] == buf3[i*2+1]);
}
printf("tone_gen: 5/5 assertions OK\n");
return 0;
}
+21
View File
@@ -0,0 +1,21 @@
#include "tone_gen.h"
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
void tone_gen_fill_stereo(int16_t *buf, int n_frames, uint32_t *phase,
float freq_hz, float amplitude, uint32_t sample_rate)
{
for (int i = 0; i < n_frames; i++) {
float s = amplitude * sinf(2.0f * (float)M_PI * freq_hz *
(float)(*phase) / (float)sample_rate);
if (s > 32767.0f) s = 32767.0f;
if (s < -32768.0f) s = -32768.0f;
int16_t v = (int16_t)s;
buf[i * 2] = v; /* L */
buf[i * 2 + 1] = v; /* R */
(*phase)++;
}
}
-163
View File
@@ -1,163 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RTC_BL_PHONE Web UI</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>RTC_BL_PHONE Web UI</h1>
<p id="statusLine">Chargement...</p>
</header>
<nav>
<button data-section="dashboard">Dashboard</button>
<button data-section="config">Configuration</button>
<button data-section="network">Réseau</button>
<button data-section="control">Contrôle</button>
<button id="refreshAllBtn">Rafraîchir tout</button>
</nav>
<main>
<section id="dashboardSection" class="active">
<h2>Statut runtime</h2>
<pre id="statusJson"></pre>
</section>
<section id="configSection">
<h2>Configuration</h2>
<div class="inline-actions">
<button id="refreshConfigBtn">Rafraîchir configuration</button>
</div>
<pre id="configJson"></pre>
<div class="grid2">
<div>
<h3>Audio config (JSON)</h3>
<textarea id="audioConfigInput" rows="8" placeholder='{"volume":80}'></textarea>
<div class="inline-actions">
<button id="applyAudioConfigBtn">Appliquer audio</button>
</div>
</div>
<div>
<h3>MQTT config (JSON)</h3>
<textarea id="mqttConfigInput" rows="8" placeholder='{"enabled":false}'></textarea>
<div class="inline-actions">
<button id="applyMqttConfigBtn">Appliquer MQTT</button>
</div>
</div>
<div>
<h3>Pins config (JSON)</h3>
<textarea id="pinsConfigInput" rows="8" placeholder='{"slic":{"line":23}}'></textarea>
<div class="inline-actions">
<button id="applyPinsConfigBtn">Appliquer pins</button>
</div>
</div>
</div>
</section>
<section id="networkSection">
<h2>Réseau et bridges</h2>
<div class="grid2">
<div>
<h3>WiFi</h3>
<form id="wifiConnectForm">
<input type="text" id="wifiSsid" placeholder="SSID" required>
<input type="password" id="wifiPass" placeholder="Mot de passe">
<button type="submit">Connecter WiFi</button>
</form>
<div class="inline-actions">
<button id="wifiDisconnectBtn">Déconnecter WiFi</button>
<button id="wifiReconnectBtn">Reconnecter WiFi</button>
<button id="wifiScanBtn">Scanner WiFi</button>
</div>
<pre id="wifiJson"></pre>
</div>
<div>
<h3>MQTT</h3>
<div class="inline-actions">
<button id="mqttConnectBtn">Connecter MQTT</button>
<button id="mqttDisconnectBtn">Déconnecter MQTT</button>
</div>
<form id="mqttPublishForm">
<input type="text" id="mqttTopic" placeholder="Topic (ex: rtc_bl_phone/a252/test)" required>
<textarea id="mqttPayload" rows="3" placeholder='Payload JSON ou texte'>{"ping":true}</textarea>
<button type="submit">Publier MQTT</button>
</form>
<pre id="mqttJson"></pre>
</div>
<div>
<h3>ESP-NOW</h3>
<form id="espnowPeerForm">
<input type="text" id="espnowMac" placeholder="AA:BB:CC:DD:EE:FF" required>
<button type="submit">Ajouter pair</button>
<button type="button" id="espnowDelBtn">Supprimer pair</button>
</form>
<form id="espnowSendForm">
<input type="text" id="espnowTarget" placeholder="broadcast ou MAC" value="broadcast" required>
<textarea id="espnowPayload" rows="3" placeholder="Payload (texte ou JSON)">{\"cmd\":\"STATUS\"}</textarea>
<button type="submit">Envoyer</button>
</form>
<div class="inline-actions">
<button id="espnowOnBtn">ESPNOW_ON</button>
<button id="espnowOffBtn">ESPNOW_OFF</button>
<button id="espnowRefreshBtn">Rafraîchir ESP-NOW</button>
</div>
<pre id="espnowJson"></pre>
<pre id="espnowPeersJson"></pre>
</div>
<div>
<h3>Bluetooth</h3>
<form id="btHfpConnectForm">
<input type="text" id="btHfpAddr" placeholder="MAC téléphone HFP (AA:BB:CC:DD:EE:FF)">
<button type="submit">HFP Connect</button>
</form>
<form id="btDialForm">
<input type="text" id="btDialNumber" placeholder="Numéro (ex: 0612345678)">
<button type="submit">Dial</button>
</form>
<div class="inline-actions">
<button id="btHfpDisconnectBtn">HFP Disconnect</button>
<button id="btRedialBtn">Redial</button>
<button id="btAnswerBtn">Answer</button>
<button id="btHangupBtn">Hangup</button>
<button id="btCallsBtn">Calls Query</button>
</div>
<div class="inline-actions">
<button id="btDiscoverableOnBtn">Discoverable ON</button>
<button id="btDiscoverableOffBtn">Discoverable OFF</button>
<button id="btAutoReconnectOnBtn">Auto Reconnect ON</button>
<button id="btAutoReconnectOffBtn">Auto Reconnect OFF</button>
<button id="btPbapSyncBtn">PBAP Sync</button>
<button id="btBleStartBtn">BLE Start</button>
<button id="btBleStopBtn">BLE Stop</button>
<button id="btRefreshBtn">Rafraîchir BT</button>
</div>
<pre id="btJson"></pre>
</div>
</div>
</section>
<section id="controlSection">
<h2>Contrôle</h2>
<div class="inline-actions">
<button data-action="CALL">CALL</button>
<button data-action="CAPTURE_START">CAPTURE_START</button>
<button data-action="CAPTURE_STOP">CAPTURE_STOP</button>
<button data-action="RESET_METRICS">RESET_METRICS</button>
</div>
<form id="rawCommandForm">
<input type="text" id="rawCommandInput" placeholder="Commande série (ex: ESPNOW_STATUS)" required>
<button type="submit">Exécuter</button>
</form>
<pre id="actionResult"></pre>
</section>
</main>
<script src="script.js"></script>
</body>
</html>
-723
View File
@@ -1,723 +0,0 @@
const SECTION_MAP = {
dashboard: "dashboardSection",
config: "configSection",
network: "networkSection",
control: "controlSection",
};
let realtimeSource = null;
let realtimeConnected = false;
let fallbackPollingTimer = null;
function showSection(section) {
Object.values(SECTION_MAP).forEach((id) => {
const el = document.getElementById(id);
if (el) {
el.classList.remove("active");
}
});
const sectionEl = document.getElementById(SECTION_MAP[section]);
if (sectionEl) {
sectionEl.classList.add("active");
}
}
function setJson(id, value) {
const el = document.getElementById(id);
if (!el) {
return;
}
if (typeof value === "string") {
el.textContent = value;
return;
}
el.textContent = JSON.stringify(value, null, 2);
}
async function requestJson(url, options = {}) {
const response = await fetch(url, options);
const text = await response.text();
let parsed = {};
if (text) {
try {
parsed = JSON.parse(text);
} catch (_) {
parsed = { raw: text };
}
}
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${text || ""}`.trim());
}
return parsed;
}
function jsonHeaders() {
return { "Content-Type": "application/json" };
}
function parseJsonInput(id) {
const raw = document.getElementById(id).value.trim();
if (!raw) {
return {};
}
return JSON.parse(raw);
}
function parsePayloadValue(rawPayload) {
const trimmed = rawPayload.trim();
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
try {
return JSON.parse(trimmed);
} catch (_) {
return rawPayload;
}
}
return rawPayload;
}
function parseRealtimeData(raw) {
try {
return JSON.parse(raw);
} catch (_) {
return { raw };
}
}
function applyStatusSnapshot(status) {
const line = document.getElementById("statusLine");
const telephonyState = status.telephony?.state || "n/a";
const hook = status.telephony?.hook || "n/a";
const wifiState = status.wifi?.state || "n/a";
const mqttConnected = status.mqtt?.connected ? "on" : "off";
const peers = status.espnow?.peer_count ?? 0;
const btCallState = status.bluetooth?.call_state || "n/a";
const btConnected = status.bluetooth?.connected ? "on" : "off";
const btAutoReconnect = status.bluetooth?.auto_reconnect_enabled ? "on" : "off";
const pbapSupported = status.bluetooth?.pbap_supported ? "yes" : "no";
const liveState = realtimeConnected ? "live=on" : "live=off";
line.textContent =
`board=${status.board_profile || "n/a"} telephony=${telephonyState} hook=${hook} ` +
`wifi=${wifiState} mqtt=${mqttConnected} espnow_peers=${peers} bt=${btConnected} bt_auto=${btAutoReconnect} bt_call=${btCallState} pbap=${pbapSupported} ${liveState}`;
setJson("statusJson", status);
if (status.wifi) {
setJson("wifiJson", status.wifi);
}
if (status.mqtt) {
setJson("mqttJson", status.mqtt);
}
if (status.espnow) {
setJson("espnowJson", status.espnow);
setJson("espnowPeersJson", { peers: status.espnow.peers || [] });
}
if (status.bluetooth) {
setJson("btJson", status.bluetooth);
}
}
function connectRealtime() {
if (!window.EventSource) {
return;
}
if (realtimeSource) {
realtimeSource.close();
}
realtimeSource = new EventSource("/api/events");
realtimeSource.onopen = () => {
realtimeConnected = true;
};
realtimeSource.addEventListener("hello", () => {
realtimeConnected = true;
});
realtimeSource.addEventListener("status", (event) => {
realtimeConnected = true;
const status = parseRealtimeData(event.data);
applyStatusSnapshot(status);
});
realtimeSource.addEventListener("dispatch", (event) => {
const payload = parseRealtimeData(event.data);
const out = { stream: "dispatch" };
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
Object.assign(out, payload);
} else {
out.payload = payload;
}
setJson("actionResult", out);
});
realtimeSource.addEventListener("effect", (event) => {
const payload = parseRealtimeData(event.data);
const out = { stream: "effect" };
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
Object.assign(out, payload);
} else {
out.payload = payload;
}
setJson("actionResult", out);
});
realtimeSource.onerror = () => {
realtimeConnected = false;
};
}
function ensureFallbackPolling() {
if (fallbackPollingTimer !== null) {
return;
}
fallbackPollingTimer = window.setInterval(() => {
if (!realtimeConnected) {
refreshStatus().catch(() => {});
}
}, 2000);
}
async function refreshStatus() {
try {
const status = await requestJson("/api/status");
applyStatusSnapshot(status);
} catch (error) {
const line = document.getElementById("statusLine");
const liveState = realtimeConnected ? "live=on" : "live=off";
line.textContent = `Erreur statut: ${error.message}`;
line.textContent += ` ${liveState}`;
setJson("statusJson", { error: error.message });
}
}
async function refreshConfig() {
try {
const [pins, audio, mqtt] = await Promise.all([
requestJson("/api/config/pins"),
requestJson("/api/config/audio"),
requestJson("/api/config/mqtt"),
]);
setJson("configJson", { pins, audio, mqtt });
const audioInput = document.getElementById("audioConfigInput");
const mqttInput = document.getElementById("mqttConfigInput");
const pinsInput = document.getElementById("pinsConfigInput");
if (audioInput) {
audioInput.value = JSON.stringify(audio, null, 2);
}
if (mqttInput) {
mqttInput.value = JSON.stringify(mqtt.config || mqtt, null, 2);
}
if (pinsInput) {
pinsInput.value = JSON.stringify(pins, null, 2);
}
} catch (error) {
setJson("configJson", { error: error.message });
}
}
async function refreshNetwork() {
try {
const [wifi, mqtt, espnow, peers] = await Promise.all([
requestJson("/api/network/wifi"),
requestJson("/api/network/mqtt"),
requestJson("/api/network/espnow"),
requestJson("/api/network/espnow/peer"),
]);
setJson("wifiJson", wifi);
setJson("mqttJson", mqtt);
setJson("espnowJson", espnow);
setJson("espnowPeersJson", peers);
} catch (error) {
const err = { error: error.message };
setJson("wifiJson", err);
setJson("mqttJson", err);
setJson("espnowJson", err);
setJson("espnowPeersJson", err);
}
}
async function refreshBluetooth() {
try {
const bt = await requestJson("/api/bluetooth");
setJson("btJson", bt);
} catch (error) {
setJson("btJson", { error: error.message });
}
}
async function sendControl(action) {
const result = await requestJson("/api/control", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ action }),
});
setJson("actionResult", result);
await Promise.all([refreshStatus(), refreshNetwork()]);
return result;
}
function bindEvents() {
document.querySelectorAll("nav button[data-section]").forEach((button) => {
button.addEventListener("click", () => showSection(button.dataset.section));
});
document.getElementById("refreshAllBtn").addEventListener("click", async () => {
await Promise.all([refreshStatus(), refreshConfig(), refreshNetwork()]);
});
document.getElementById("refreshConfigBtn").addEventListener("click", refreshConfig);
document.getElementById("espnowRefreshBtn").addEventListener("click", refreshNetwork);
document.getElementById("btRefreshBtn").addEventListener("click", refreshBluetooth);
document.getElementById("applyAudioConfigBtn").addEventListener("click", async () => {
try {
const payload = parseJsonInput("audioConfigInput");
const result = await requestJson("/api/config/audio", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify(payload),
});
setJson("actionResult", result);
await Promise.all([refreshConfig(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("applyMqttConfigBtn").addEventListener("click", async () => {
try {
const payload = parseJsonInput("mqttConfigInput");
const result = await requestJson("/api/config/mqtt", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify(payload),
});
setJson("actionResult", result);
await Promise.all([refreshConfig(), refreshNetwork(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("applyPinsConfigBtn").addEventListener("click", async () => {
try {
const payload = parseJsonInput("pinsConfigInput");
const result = await requestJson("/api/config/pins", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify(payload),
});
setJson("actionResult", result);
await Promise.all([refreshConfig(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("wifiConnectForm").addEventListener("submit", async (event) => {
event.preventDefault();
const ssid = document.getElementById("wifiSsid").value.trim();
const pass = document.getElementById("wifiPass").value;
try {
const result = await requestJson("/api/network/wifi/connect", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ ssid, pass }),
});
setJson("actionResult", result);
await Promise.all([refreshStatus(), refreshNetwork()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("wifiDisconnectBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/network/wifi/disconnect", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshStatus(), refreshNetwork()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("wifiReconnectBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/network/wifi/reconnect", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshStatus(), refreshNetwork()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("wifiScanBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/network/wifi/scan", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await refreshNetwork();
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("mqttConnectBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/network/mqtt/connect", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshStatus(), refreshNetwork()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("mqttDisconnectBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/network/mqtt/disconnect", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshStatus(), refreshNetwork()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("mqttPublishForm").addEventListener("submit", async (event) => {
event.preventDefault();
const topic = document.getElementById("mqttTopic").value.trim();
const payload = document.getElementById("mqttPayload").value;
try {
const result = await requestJson("/api/network/mqtt/publish", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ topic, payload }),
});
setJson("actionResult", result);
await Promise.all([refreshStatus(), refreshNetwork()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("espnowOnBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/network/espnow/on", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshStatus(), refreshNetwork()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("espnowOffBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/network/espnow/off", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshStatus(), refreshNetwork()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("espnowPeerForm").addEventListener("submit", async (event) => {
event.preventDefault();
const mac = document.getElementById("espnowMac").value.trim();
try {
const result = await requestJson("/api/network/espnow/peer", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ mac }),
});
setJson("actionResult", result);
await refreshNetwork();
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("espnowDelBtn").addEventListener("click", async () => {
const mac = document.getElementById("espnowMac").value.trim();
try {
const result = await requestJson("/api/network/espnow/peer", {
method: "DELETE",
headers: jsonHeaders(),
body: JSON.stringify({ mac }),
});
setJson("actionResult", result);
await refreshNetwork();
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("espnowSendForm").addEventListener("submit", async (event) => {
event.preventDefault();
const target = document.getElementById("espnowTarget").value.trim();
const payloadRaw = document.getElementById("espnowPayload").value;
const payload = parsePayloadValue(payloadRaw);
try {
const result = await requestJson("/api/network/espnow/send", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ mac: target, payload }),
});
setJson("actionResult", result);
await refreshNetwork();
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btHfpConnectForm").addEventListener("submit", async (event) => {
event.preventDefault();
const addr = document.getElementById("btHfpAddr").value.trim();
try {
const result = await requestJson("/api/bluetooth/hfp/connect", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ addr }),
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btHfpDisconnectBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/hfp/disconnect", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btDialForm").addEventListener("submit", async (event) => {
event.preventDefault();
const number = document.getElementById("btDialNumber").value.trim();
try {
const result = await requestJson("/api/bluetooth/hfp/dial", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ number }),
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btRedialBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/hfp/redial", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btAnswerBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/hfp/answer", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btHangupBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/hfp/hangup", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btCallsBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/hfp/calls", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btDiscoverableOnBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/discoverable/on", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btDiscoverableOffBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/discoverable/off", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btAutoReconnectOnBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/hfp/auto", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ enabled: true }),
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btAutoReconnectOffBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/hfp/auto", {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ enabled: false }),
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btPbapSyncBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/pbap/sync", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btBleStartBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/ble/start", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.getElementById("btBleStopBtn").addEventListener("click", async () => {
try {
const result = await requestJson("/api/bluetooth/ble/stop", {
method: "POST",
headers: jsonHeaders(),
body: "{}",
});
setJson("actionResult", result);
await Promise.all([refreshBluetooth(), refreshStatus()]);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
document.querySelectorAll("#controlSection button[data-action]").forEach((button) => {
button.addEventListener("click", async () => {
try {
await sendControl(button.dataset.action);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
});
document.getElementById("rawCommandForm").addEventListener("submit", async (event) => {
event.preventDefault();
const action = document.getElementById("rawCommandInput").value.trim();
try {
await sendControl(action);
} catch (error) {
setJson("actionResult", { error: error.message });
}
});
}
document.addEventListener("DOMContentLoaded", async () => {
bindEvents();
connectRealtime();
ensureFallbackPolling();
await Promise.all([refreshStatus(), refreshConfig(), refreshNetwork(), refreshBluetooth()]);
showSection("dashboard");
});
-116
View File
@@ -1,116 +0,0 @@
:root {
--bg: #f4f6f8;
--panel: #ffffff;
--text: #1f2933;
--accent: #0f6ab6;
--accent-strong: #084a81;
--ok: #19753d;
--border: #d8dee4;
--warn: #9a5f00;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 24px;
font-family: "Segoe UI", Tahoma, sans-serif;
background: linear-gradient(135deg, #edf4fa, #f8f9fb);
color: var(--text);
}
header {
margin-bottom: 16px;
}
h1 {
margin: 0 0 8px;
color: var(--accent);
}
nav {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
button {
background: var(--accent);
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
}
button:hover {
background: var(--accent-strong);
}
section {
display: none;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
}
section.active {
display: block;
}
.grid2 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.inline-actions {
margin: 8px 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
input,
select,
textarea {
padding: 8px;
border: 1px solid var(--border);
border-radius: 6px;
font: inherit;
color: inherit;
background: #fff;
}
form {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
pre {
background: #1f2933;
color: #e8eef4;
border-radius: 6px;
padding: 12px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
}
@media (max-width: 760px) {
body {
padding: 12px;
}
button,
input,
textarea {
width: 100%;
}
}
-54
View File
@@ -1,54 +0,0 @@
# RTC_BL_PHONE Kanban (Live Execution)
## Todo
- Validate hardware gate `incoming GSM -> RTC ring -> BT_ANSWER` on `/dev/cu.usbserial-0001`.
- Validate hardware gate `dial len10 (pulse)` with queued HFP reconnection then auto dial.
- Validate hardware gate `dial len10 (DTMF)` with no false pulse digits.
- Re-run `ESP-NOW` 2-board validation when peer card is available.
## In Progress
- Track `rtos-coex-wifi-bt`:
- endurance monitor 10 min sans `abort/assert`.
- Track `bt-hfp-stability`:
- validation E2E entrant GSM -> ring RTC -> décroché -> `BT_ANSWER`.
- Track `webui-runtime`:
- valider charge SSE en AP fallback + STA connecté.
## Blocked
- `PBAP` contact sync on Arduino ESP32 Bluedroid stack (`BT_PBAP_SYNC` remains `unsupported`).
- Full ESP-NOW E2E protocol proof while second board is unavailable.
- Notion sync automation blocked until MCP Notion OAuth session is reconnected.
## Done
- Added HFP auto reconnect runtime control (`BT_AUTO_RECONNECT_ON/OFF`) and status flag.
- Preserved dial queue logic (`pending_dial_number`) with auto dial after SLC.
- Added `/api/bluetooth/hfp/auto` endpoint (GET status via `BT_STATUS`, POST toggle).
- Added WebUI controls for BT auto reconnect and status line visibility.
- Re-enabled SSE transport server-side and added browser fallback polling.
- Hardened ESP-NOW init to preserve WiFi mode and apply WiFi/BT modem sleep policy.
- Boot order adjusted to initialize WiFi + ESP-NOW before BT stack bring-up.
- Re-asserted WiFi/BT coex policy periodically in `WifiManager::loop()`.
- Added coex policy enforcement in BT stack bring-up (`BluetoothManager::ensureBtStackReady()`).
- Reduced GAP callback logging pressure (guarded by `kVerboseGapLogs=false`).
- Reduced AsyncTCP memory footprint (`STACK=4096`, `QUEUE=12`) to lower RTOS pressure.
- Reordered main loop to run `g_wifi.loop()` before `g_bt.tick()`.
- Updated GitHub issues `#10` and `#11` with latest CI/HW proof links:
- `https://github.com/electron-rare/RTC_BL_PHONE/issues/10#issuecomment-3941685856`
- `https://github.com/electron-rare/RTC_BL_PHONE/issues/11#issuecomment-3941685860`
## Run Ledger
- `2026-02-22`:
- Scope: coex WiFi/BT hardening + HFP autoradio policy + realtime WebUI + docs sync.
- Target port: `/dev/cu.usbserial-0001`.
- Firmware hash (sha256): `2c345d5ac459e6aa914f47438cc35bbe33dda68e5a65f41eaf3d8f1efb53e833`.
- Executed:
- `platformio run -e esp32dev` -> `PASS`
- `platformio run -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001` -> `PASS`
- `python3 scripts/hw_validation.py --port-a252 /dev/cu.usbserial-0001 --report-json artifacts/hw_validation_report.json --report-md artifacts/hw_validation_report.md` -> `PASS`
- `bash scripts/test_terminal.sh` -> `PASS`
- `python3 -m unittest scripts/test_check_web_route_parity.py` -> `PASS`
- `python3 scripts/check_web_route_parity.py --report-json artifacts/route_parity_report.json` -> `PASS`
- Artifacts:
- `artifacts/hw_validation_report.json`
- `artifacts/hw_validation_report.md`
- `artifacts/route_parity_report.json`
-17
View File
@@ -1,17 +0,0 @@
# Changelog RTC_BL_PHONE
Toutes les évolutions, corrections et releases du projet.
## [Unreleased]
- Initialisation du plan de gestion repo & GitHub
- Ajout agents SLIC, audio, lecture audio, téléphone SFP
- Modularisation firmware, documentation agents
## [2026-02-17] v1.0.0
- Structure modulaire (Web, RTOS, Energie, Bluetooth, WiFi)
- Endpoints HTTP, tests, audit sécurité, CI web
- Structuration multitâche RTOS, tests unitaires, audit robustesse
---
_Agent Repo & GitHub Changelog généré automatiquement._
-49
View File
@@ -1,49 +0,0 @@
# Cross-Repo Intelligence Base
Date: 2026-02-20
## Repos de référence
- `electron-rare/RTC_BL_PHONE`:
- firmware téléphonie RTC + Bluetooth + WiFi + MQTT + ESP-NOW.
- repo dexécution pour la stack A252.
- `electron-rare/le-mystere-professeur-zacus`:
- repo scénario/UI avec bridge WiFi/ESP-NOW orienté événements story.
- référence pour formats de payload entrants hétérogènes.
- `electron-rare/Kill_LIFE`:
- repo base méthodologique (agents, gates, evidence pack, standards firmware).
- référence process qualité/traçabilité.
## Standards repris depuis Kill_LIFE
- Gates minimales obligatoires sur changements firmware:
- `Spec` -> `Build` -> `Test` -> `Release`.
- Evidence minimum à conserver:
- logs build/test,
- résultat CI,
- artefacts de validation.
- Standards firmware:
- PlatformIO + Unity,
- wrappers autour des drivers,
- timeouts/watchdogs pour IO bloquants.
## Contrat dinterop RTC <-> Zacus
- Commandes série ESP-NOW alignées:
- `ESPNOW_ON`
- `ESPNOW_OFF`
- `ESPNOW_STATUS`
- `ESPNOW_PEER_ADD <mac>`
- `ESPNOW_PEER_DEL <mac>`
- `ESPNOW_PEER_LIST`
- `ESPNOW_SEND <mac|broadcast> <payload>`
- Payload entrant bridge accepté par `RTC_BL_PHONE`:
- direct: `cmd`, `raw`, `command`, `action`
- imbriqué: `event.*`, `message.*`, `payload.*`
## Règle opérationnelle
- Toute évolution protocolaire ESP-NOW doit:
- être documentée ici + `docs/props.md`,
- être testée au minimum par build terminal + smoke host,
- être reflétée dans une PR liée côté repo partenaire si impact croisé.
-26
View File
@@ -1,26 +0,0 @@
# Gate critique — Blocage WiFiServer.h
## Description
- Blocage du build PlatformIO sur ESP32: fatal error WiFiServer.h (WebServer)
- Origine: WebServer inclus par le framework Arduino ESP32, non compatible ou absent
- Aucun code source du projet ne dépend de WebServer, mais la bibliothèque est installée par défaut
## Actions tentées
- Retrait des dépendances tierces (audio-tools)
- Mise à jour du framework espressif32 et des bibliothèques
- Audit des dépendances installées
## Recommandation experte
- Utiliser exclusivement ESPAsyncWebServer pour tous les endpoints HTTP
- Ne pas inclure WebServer ni WiFiServer.h dans le code source
- Documenter ce gate comme critique dans la CI et la synthèse de phase
- Proposer une stratégie de contournement: tests unitaires sur les modules non dépendants du serveur HTTP
## Stratégie CI
- Valider les tests unitaires sur les modules audio, SLIC, téléphone, RTOS
- Reporter le blocage serveur HTTP dans docs/AGENT_TODO.md et docs/RC_AUTOFIX_CICD.md
- Suivre l’évolution du framework Arduino ESP32 pour correction future
---
**Version :** 2026-02-17
-28
View File
@@ -1,28 +0,0 @@
# Synthèse de phase — RTC_BL_PHONE
## Blocage principal
- Erreur WiFiServer.h lors du build PlatformIO (tests unitaires non exécutés)
- Cause: WebServer requiert WiFiServer.h, non compatible ESP32
- Solution: Utiliser ESPAsyncWebServer, éviter WebServer, vérifier framework Arduino
## Actions réalisées
- Correction tentée via lib_deps=WiFi (non résolue)
- Recherche web et documentation des bonnes pratiques
- Documentation des artefacts, logs, scripts et verdicts
- Blocage documenté dans docs/RC_AUTOFIX_CICD.md
- Stratégie de contournement CI appliquée
## Recommandations
- Vérifier que le code source/tests nutilisent pas WebServer
- Prioriser ESPAsyncWebServer pour tous les endpoints
- Suivre l’évolution du framework Arduino ESP32
- Documenter les blockers et gates dans docs/AGENT_TODO.md et docs/RC_AUTOFIX_CICD.md
## Prochaines étapes
- Valider les tests unitaires sur modules audio, SLIC, téléphone, RTOS
- Finaliser la documentation CI et synthèse de phase
- Préparer la phase suivante : extension endpoints, sécurité, tests fonctionnels avancés
---
**Version :** 2026-02-17
-84
View File
@@ -1,84 +0,0 @@
# Documentation technique & intelligence agents — RTC_BL_PHONE
Ce dossier centralise toute l'intelligence documentaire et la synergie agents du projet :
## Fiches agents
| Agent | Fiche |
|----------------------|-------------------------------|
| AudioManager | fiche_AudioManager.md |
| BluetoothManager | fiche_BluetoothManager.md |
| RTOSManager | fiche_RTOSManager.md |
| SLICManager | fiche_agent_embarque_stack.md |
| WifiManager | roles_agents.md |
| Web | plan_webui.md |
| Energie | plan_stack_audio.md |
| Lecture Audio | fiche_agent_audio_tools.md |
| Téléphone SFP | plan_stack_telephone_sfp.md |
| Repo & GitHub | plan_gestion_repo_github.md |
| Tests Hardware | fiche_agents_test_hardware.md |
| QA | procedure_tests_hardware.md |
Chaque fiche détaille : rôle, API, notifications, points de validation, synergie avec les autres agents.
## Plans d'architecture et de test
| Plan | Fichier |
|-----------------------------|-------------------------------|
| Annuaire | plan_annuaire.md |
| Chef projet ESP32S3 | plan_chef_projet_esp32s3_ag1171s.md |
| Délégation agents | plan_delegation_agents.md |
| Dev web | plan_dev_web.md |
| Gestion repo GitHub | plan_gestion_repo_github.md |
| Multitâche RTOS | plan_multitache_rtos.md |
| RTOS | plan_rtos.md |
| Stack audio | plan_stack_audio.md |
| Stack lecture audio | plan_stack_lecture_audio.md |
| Stack SLIC | plan_stack_slic.md |
| Stack téléphone SFP | plan_stack_telephone_sfp.md |
| Tests livraison | plan_tests_livraison.md |
| WebUI | plan_webui.md |
| Spec WebUI route parity | spec_webui_route_parity_and_coverage_v1.md |
| Spec BT HFP/PBAP dial | spec_bt_hfp_pbap_dialing_v1.md |
| API ESP-NOW v1 | espnow_api_v1.md |
| Faisabilité migration PBAP | bt_pbap_migration_feasibility.md |
| État des specs | SPECS_STATE.md |
## Procédures QA & hardware
| Procédure | Fichier |
|-----------------------------|-------------------------------|
| Tests hardware | procedure_tests_hardware.md |
| QA moniteur série | protocole_test_qa_moniteur_serie.md |
| Robustesse RTOS | audit_robustesse_rtos.md |
| Sécurité web | audit_securite_web.md |
## Rapports
| Rapport | Fichier |
|-----------------------------|-------------------------------|
| CI Web | rapport_ci_web.md |
| Exécution TODO | rapport_execution_todo.md |
| Validation hardware | rapport_validation_hardware.md|
| Synthèse validation | rapport_synthese_validation.md|
| Tests fonctionnels | rapport_tests_fonctionnels.md |
| Tests web | rapport_tests_web.md |
| HW JSON | rapport_hw.json |
### Archives
Les rapports daudit, de validation et de CI anciens sont archivés dans [docs/archives/](docs/archives).
## Changelog
Historique des évolutions : [CHANGELOG.md](CHANGELOG.md)
---
## Synergie agents
- Les agents collaborent pour garantir la robustesse, la qualité et la traçabilité du firmware et du hardware.
- Exemples :
- Audio ↔ RTOS ↔ Web ↔ WiFi ↔ SLIC ↔ Téléphonie ↔ Energie ↔ Bluetooth
- Repo & GitHub ↔ CI/CD ↔ Documentation
## Règles
- Toute évolution majeure doit être documentée ici
- Les fiches agents servent de référence pour la maintenance, la QA et la validation terrain
- L'index (ce fichier) doit pointer vers tous les documents clés
## Pas de prompts story
Ce projet n'utilise pas de prompts narratifs ou de story specs, mais une documentation technique et QA exhaustive.
-11
View File
@@ -1,11 +0,0 @@
<!-- REPO_STATE:v1 -->
Repo: RTC_BL_PHONE
Branch: codex/repo-state-rtc
HEAD: b97794c967f39a816b9d423544f96178b985f1c7
HeadDate: 2026-02-21T03:09:10+01:00
HeadSubject: Merge pull request #20 from electron-rare/codex/rtc-zeroclaw-orchestration-issue-18
RepoURL: https://github.com/electron-rare/RTC_BL_PHONE.git
ProjectKind: firmware_embedded
PivotChanges: [{"path": "(none)", "tags": ["general_change"]}]
ImpactGates: general_change
GeneratedAtUTC: 2026-02-21T02:23:01Z
-173
View File
@@ -1,173 +0,0 @@
# État des specs RTC_BL_PHONE
## 1) Index des specs
- `docs/spec_webui_route_parity_and_coverage_v1.md` (spécification principale active)
- `docs/spec_bt_hfp_pbap_dialing_v1.md` (nouvelle spec téléphonie Bluetooth, statut `Draft`)
- Notion hub: `RTC_BL_PHONE Delivery Hub`
- Notion spec mirror: `RTC_BL_PHONE - Spec - WebUI Route Parity v1`
- Notion plan: `RTC_BL_PHONE - Plan d'implementation - Route Parity Closure`
- Notion tasks DB: `RTC_BL_PHONE - Tasks` (`collection://89fa36aa-8933-4294-a922-90c6c34d5130`)
## 2) Bilan opérationnel de la spec webui route parity
| Axe | Statut | Preuve / commentaire |
|---|---|---|
| EF-01 — Gate route parity en CI | Done | Workflow CI exécute le checker parity avec export JSON. |
| EF-02 — Artefact de preuve | Done | Rapport JSON généré (`artifacts/route_parity_report.json`) et upload CI via `route-parity-report`. |
| EF-03 — Couverture routes WebUI | Done | Couverture documentée et validée dans `docs/rapport_tests_fonctionnels.md` (section WebUI Route Parity V1). |
| EF-04 — Non-régression des routes existantes | Done | Smoke flows routes tracés dans `docs/rapport_tests_fonctionnels.md` et cohérents avec parity checker. |
## 3) État global de consolidation
- `platformio run -e esp32dev` : OK
- `platformio test --without-uploading --without-testing -e esp32dev` : OK
- `scripts/check_web_route_parity.py` (local) : backend routes 39, frontend routes 38, check passé
- `scripts/check_web_route_parity.py --report-json artifacts/route_parity_report.json` : OK (report généré)
- `platformio run -e esp32-s3-devkitc-1` : échec connu sur liens Bluetooth/HFP externes (suivi séparé)
## 3-bis) État spec BT HFP/PBAP/Numérotation
- HFP commande-level: OK (connect/disconnect commandes acceptées).
- HFP call-control AT: implémenté (`BT_DIAL`/`DIAL`, `BT_REDIAL`, `BT_ANSWER`, `BT_HANGUP`, `BT_CALLS`).
- Discoverable policy: OK (si aucun client BT connecté, `discoverable=true` forcé; validation locale via `BT_STATUS` avant/après `BT_DISCOVERABLE_OFF`).
- HFP liaison réelle: KO sur tests assistés (pas de montée `connected=true`/`hfp_active=true`).
- PBAP: bloqué sur stack actuelle (`arduino-esp32` bluedroid sans API PBAP exposée côté firmware), `BT_PBAP_SYNC` retourne `unsupported`.
- Numérotation HFP/AT: implémentée côté firmware, validation E2E téléphone encore à obtenir (dépend de la liaison HFP réelle).
## 4) Lacunes à combler pour passer la spec en `DONE`
1. Conserver le suivi S3 Bluetooth/HFP hors scope parity V1.
## 5) Risques actifs
- Écart documentation / exécution (doD non uniformément tracée)
- Dépendances CI sur les branches de support non conservées (si nettoyage de branches engagé)
- Blocs matériels/firmware S3 (liens Bluetooth/HFP) pouvant masquer la visibilité produit
## 6) Enchaînement Track A / Track B (Bluetooth, WiFi, WebUI, ESP-NOW)
### Track A — Delivery rapide (état courant)
- WiFi:
- endpoints Web/API présents: `/api/network/wifi`, `/connect`, `/disconnect`, `/reconnect`, `/scan`.
- fallback AP actif en perte STA (`state=ap_fallback`).
- WebUI Bluetooth:
- endpoints étendus pour discoverable, dial/redial/answer/hangup/calls, pbap sync.
- affichage états BT enrichis (`call_state`, `slc_connected`, `pbap_supported`).
- ESP-NOW:
- protocole v1 documenté (`docs/espnow_api_v1.md`).
- compat legacy conservée (`cmd/raw/command/action`).
- Bluetooth HFP:
- commandes d'appel opérationnelles.
- PBAP toujours `unsupported` sur stack actuelle (comportement explicitement tracé).
### Track B — Migration BT/PBAP (préparation)
- Faisabilité documentée: `docs/bt_pbap_migration_feasibility.md`
- Build migration préparé: `env:esp32dev-bt-migrate`
- Interface d'abstraction ajoutée: `src/bluetooth/IBluetoothBackend.h`
### Gates de validation
- Gate local minimal:
- `platformio run -e esp32dev`
- `bash scripts/test_terminal.sh`
- `scripts/hw_validation.py --port-a252 /dev/cu.usbserial-0001`
- Gate CI:
- workflows `Repo State` + `Firmware CI` verts sur `main`
## 7) Exécution hardware locale (2026-02-21, ESP32 Audio Kit)
- Cible testée: `/dev/cu.usbserial-0001`
- Upload firmware: `platformio run -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001` -> `PASS`
- Gate local: `bash scripts/test_terminal.sh` -> `PASS` (build unit test, host dtmf, parity parser, parity routes)
- Smoke HW: `python3 scripts/hw_validation.py --port-a252 /dev/cu.usbserial-0001 --report-json artifacts/hw_validation_report.json --report-md artifacts/hw_validation_report.md` -> `PASS`
- Snapshot runtime: `artifacts/runtime_snapshot.json` (inclut `BT_STATUS`, `WIFI_STATUS`, `ESPNOW_STATUS`, `/api/status`)
- WiFi stability: `artifacts/wifi_stability_report.json` -> `PASS` (disconnect => `ap_fallback`, reconnect => `connected`, credentials persistées)
- HFP opérable (stack actuelle): `artifacts/hfp_operational_report.json` -> `PASS` (discoverable on/off ok, `BT_DIAL` sans SLC en erreur contrôlée, `BT_PBAP_SYNC unsupported` explicite)
- ESP-NOW protocole v1: `artifacts/espnow_protocol_v1_report.json` -> `FAIL` en bench 1 carte (pas de peer radio actif, `ESPNOW_SEND` retourne `ERR`)
- Décision de gate: WiFi/WebUI/Bluetooth HFP command-level `GO`; ESP-NOW E2E v1 two-board `BLOCKED` jusqu’à disponibilité dune 2e carte
### Addendum (2026-02-22)
- Firmware reflash `esp32dev` sur `/dev/cu.usbserial-0001` effectué.
- Validation BT ready-for-pairing: `artifacts/bt_hfp_readiness.json`
- `discoverable=true` sans client connecté.
- `BT_CALLS`/`BT_DIAL` en erreur contrôlée hors SLC.
- `BT_PBAP_SYNC` retourne `ERR ... unsupported` (attendu).
- Validation discoverable forcé: `artifacts/bt_discoverable_policy_check.json`
- `BT_DISCOVERABLE_OFF` accepté mais état final reste `discoverable=true` tant que non connecté.
- ESP-NOW (tests minimisés): `artifacts/espnow_peer91_probe_clean.json`
- peer `10:20:BA:58:C7:48` ajouté.
- envoi unitaire observé avec `tx_fail=1` côté A252 (non bloquant pour le track Bluetooth).
### Addendum (2026-02-22 — wiring/polarité/audio)
- Polarité `GPIO21` verrouillée: `AMP_EN` actif bas (`LOW=ON`, `HIGH=OFF`).
- `SLIC LINE` retiré du runtime (broche non utilisée en bench courant).
- Mapping bench confirmé:
- `RM=GPIO18`, `FR=GPIO5`, `SHK=GPIO23`, `PD=GPIO19`.
- Tonalité locale fixée à `425 Hz` et niveau augmenté (boost léger) sans changement de timbre.
- Durcissement Bluetooth en cours:
- reprise explicite sur `ACL_DISCONN`,
- retry audio SCO piloté par état d'appel (`dialing/alerting/ringing/active`).
### Addendum (2026-02-22 — dial/ring policy BT)
- Spécification ajoutée:
- `DIAL`/`BT_DIAL` doit accepter une demande même sans SLC immédiat:
- queue numéro,
- déclenche/reprend connexion HFP,
- compose automatiquement après `SLC_CONNECTED`.
- Appel entrant GSM via HFP (`call_state=ringing`) doit déclencher la sonnerie RTC.
- Décroché RTC en phase sonnerie doit déclencher `BT_ANSWER`.
- Précondition produit explicitée:
- sans lien HFP actif, la sonnerie RTC sur appel GSM n'est pas possible;
- mitigation firmware: auto-reconnect sur peer bondé + discoverable forcé si non connecté.
- Statut:
- implémentation firmware: `in_progress` (queue dial + auto reconnect renforcés),
- validation hardware E2E appel entrant/sortant: `pending`.
### Addendum (2026-02-22 — autonomous execution batch)
- Runtime policy applied:
- HFP auto reconnect remains enabled by default (`auto_reconnect_enabled=true`).
- New runtime toggles available:
- `BT_AUTO_RECONNECT_ON`
- `BT_AUTO_RECONNECT_OFF`
- `BT_STATUS` now exposes `auto_reconnect_enabled`.
- Web/API alignment:
- endpoint added: `POST /api/bluetooth/hfp/auto` with body `{"enabled":true|false}`.
- endpoint readback: `GET /api/bluetooth/hfp/auto` via `BT_STATUS`.
- SSE realtime path re-enabled (`/api/events`) with client polling fallback.
- Coexistence hardening:
- ESP-NOW init now preserves current WiFi mode and applies modem sleep policy for WiFi/BT coexistence.
- setup order adjusted: WiFi + ESP-NOW before BT startup.
- Tracking:
- `docs/AGENT_TODO.md` converted to live kanban with run ledger.
### Addendum (2026-02-22 — coex/runtime hardening batch 2)
- Build + flash:
- `platformio run -e esp32dev` -> `PASS`
- `platformio run -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001` -> `PASS`
- Runtime/bench:
- `python3 scripts/hw_validation.py --port-a252 /dev/cu.usbserial-0001 --report-json artifacts/hw_validation_report.json --report-md artifacts/hw_validation_report.md` -> `PASS`
- firmware hash (sha256): `2c345d5ac459e6aa914f47438cc35bbe33dda68e5a65f41eaf3d8f1efb53e833`
- Coexistence hardening applied:
- réimposition périodique `WIFI_PS_MIN_MODEM` dans `WifiManager::loop()`.
- réimposition coex avant/après `btStart()` dans `BluetoothManager::ensureBtStackReady()`.
- logs GAP en callback réduits (`kVerboseGapLogs=false`) pour éviter contention newlib/locks en task BT.
- pression mémoire AsyncTCP réduite (`STACK=4096`, `QUEUE=12`).
- Qualif scripts:
- `bash scripts/test_terminal.sh` -> `PASS`
- `python3 -m unittest scripts/test_check_web_route_parity.py` -> `PASS`
- `python3 scripts/check_web_route_parity.py --report-json artifacts/route_parity_report.json` -> `PASS`
- Résiduel:
- endurance 10 min sans assert à confirmer en monitor live.
- validation E2E entrant GSM -> ring RTC -> `BT_ANSWER` encore `pending`.
- Gouvernance:
- Issues mises à jour avec preuves:
- `#10`: `https://github.com/electron-rare/RTC_BL_PHONE/issues/10#issuecomment-3941685856`
- `#11`: `https://github.com/electron-rare/RTC_BL_PHONE/issues/11#issuecomment-3941685860`
+70
View File
@@ -0,0 +1,70 @@
# Contrat board A252 (source de vérité)
Ce document résume la spec canonique A252.
Source canonique:
- [docs/specs/ai_thinker_esp32_a1s_es8388_n4r8.agent.v2.yaml](./specs/ai_thinker_esp32_a1s_es8388_n4r8.agent.v2.yaml)
## Profil cible
- Carte: Ai-Thinker ESP32-A1S (ES8388), N4R8
- Flash: 4 MB
- PSRAM: 8 MB (4 MB mappés par défaut, HIMEM pour >4 MB)
- Branche: `esp32_RTC_ZACUS`
- Cible unique de validation: A252 (`/dev/cu.usbserial-0001`)
## DIP verrouillé
- DIP1: OFF
- DIP2: ON
- DIP3: ON
- DIP4: OFF
- DIP5: OFF
Effets:
- SD SPI active (CS GPIO13, MOSI GPIO15)
- KEY2 indisponible (partage GPIO13)
- JTAG désactivé
## Mapping pins (normatif)
| Domaine | Signal | GPIO |
|---|---|---|
| I2C ES8388 | SDA | 33 |
| I2C ES8388 | SCL | 32 |
| ES8388 | Adresse 7-bit | 0x10 |
| I2S | MCLK | 0 |
| I2S | BCLK | 27 |
| I2S | LRCK | 25 |
| I2S | DOUT (ESP->codec) | 26 |
| I2S | DIN (codec->ESP) | 35 |
| AMP | ENABLE | 21 (actif HIGH) |
| HP detect | HP_DETECT | 39 (input-only) |
| SLIC | RM | 18 |
| SLIC | FR | 5 |
| SLIC | SHK | 23 |
| SLIC | PD | 19 |
| SD SPI | CS | 13 |
| SD SPI | SCK | 14 |
| SD SPI | MOSI | 15 |
| SD SPI | MISO | 2 |
## Contraintes GPIO
- GPIO34..GPIO39: input-only.
- GPIO34..GPIO39: pas de pull-up/down interne logiciel.
- GPIO0, GPIO2, GPIO15: strapping pins (ne pas perturber les niveaux de boot).
- GPIO13 partagé (SD CS/KEY2/JTAG): dédié SD avec DIP courant.
## Variants supportés
- Variant principal (obligatoire): SDA33/SCL32.
- Fallback I2C seulement si codec non détecté: SDA23/SCL18.
## Compat RTC_BL_PHONE (actuel)
- Source centralisée des pins: `src/config/a1s_board_pins.h`.
- Defaults runtime: `src/config/A252ConfigStore.h`.
- Politique média logique: `LITTLEFS` côté contrat; implémentation `FFat` acceptable si exposée clairement en status/commandes.
- Tonalité A252: référent tonal régional et mappings événement WAV dans [docs/audio_tone_plan.md](./audio_tone_plan.md).
- Variante tonale de référence: `docs/specs/tone_plan_wav_assets/mapping_event_to_wav.yaml` (schema 1.2)
## Checklist rapide de validation
1. `STATUS` expose `config.pins` conforme au tableau.
2. `hw.init_ok`, `hw.slic_ready`, `hw.codec_ready`, `hw.audio_ready` à `true`.
3. `serial_hook_ring_audio` passe (`RING`, `TONE_ON`, `TONE_OFF`, ON/OFF_HOOK vus).
4. I2C codec visible sur `0x10` (ou fallback variant documenté).
-2
View File
@@ -1,2 +0,0 @@
# Audit initial audio
...existing code...
@@ -1,2 +0,0 @@
# Audit initial lecture audio
...existing code...
-2
View File
@@ -1,2 +0,0 @@
# Audit initial SLIC
...existing code...
@@ -1,2 +0,0 @@
# Audit initial téléphone SFP
...existing code...
-2
View File
@@ -1,2 +0,0 @@
# Rapport CI Web
...existing code...
-2
View File
@@ -1,2 +0,0 @@
# Rapport exécution TODO
...existing code...
-4
View File
@@ -1,4 +0,0 @@
{
"rapport": "HW initial"
// ...existing code...
}
@@ -1,2 +0,0 @@
# Rapport synthèse validation
...existing code...
@@ -1,2 +0,0 @@
# Rapport tests fonctionnels
...existing code...
-2
View File
@@ -1,2 +0,0 @@
# Rapport tests web
...existing code...
@@ -1,2 +0,0 @@
# Rapport validation hardware
...existing code...
-82
View File
@@ -1,82 +0,0 @@
# Audit Audio / BT / RTOS — état et plan d'enchaînement
## État validé (run local USB `/dev/cu.usbserial-0001`)
- Boot stable: audio + codec + web server OK.
- Capture audio: arbitrage multi-clients en place (`TELEPHONY`, `BLUETOOTH`, `GENERIC`).
- Bluetooth HFP:
- plus de crash immédiat au boot.
- plus d'auto-connect agressif au boot (pair bond restauré, connexion sur demande).
- dial peut rester en queue (`pending_dial_number`) si SLC non prêt.
- Coexistence WiFi/BT:
- erreurs fatales/abort au boot réduites après déport d'actions callback vers `tick()`.
## Correctifs clés appliqués
- `AudioEngine`:
- `requestCapture/releaseCapture` thread-safe.
- task audio avec backoff idle (charge RTOS réduite).
- `TelephonyService` / `BluetoothManager`:
- plus de `startCapture/stopCapture` concurrents.
- utilisation des clients de capture dédiés.
- `BluetoothManager`:
- déport de `applyDiscoverablePolicy/publishBleStatus` hors callbacks.
- suppression dauto-reconnect HFP au boot.
- timeout SLC en mode backoff (sans disconnect agressif).
- désactivation `AgentSupervisor` côté BT pour retirer allocations C++ en callback.
## Plan exécutable (skills coordonnés)
### Track 1 — `esp32-rtos-coex-triage` (en cours)
- Objectif: zéro reboot/assert sur séquence boot + WiFi STA + BT activé.
- Actions:
- garder le BT callback-safe (déjà fait).
- surveiller `wifi: fail to alloc timer` sur endurance.
- livrer un rapport de stabilité sur 10 min.
### Track 2 — `esp32-bt-hfp-stability` (prochaine étape)
- Objectif: connexion GSM stable, SLC up, dial auto depuis buffer len10.
- Actions:
- test live: `BT_HFP_CONNECT`, `BT_STATUS`, appel sortant/entrant.
- valider transition `pending_dial_number -> dial requested` quand SLC connecté.
- verrouiller comportement discoverable auto si non connecté.
### Track 3 — `esp32-audio-runtime-gating` (prochaine étape)
- Objectif: tonalité et audio call sans glitch ni coupure.
- Actions:
- vérifier arrêt instant tonalité au 1er digit.
- vérifier arrêt tonalité immédiat au raccroché.
- valider chemin HFP audio I2S en appel actif.
## Critères de sortie
- Aucun `abort/assert` pendant test run.
- Aucun `vQueueDelete queue.c` observé.
- Numérotation 10 chiffres pulse/DTMF déclenche `BT_DIAL` avec succès quand SLC monte.
- Appel entrant GSM: ring, décroché, audio duplex.
## Plan autonome coordonné (skills + agents)
- Skill utilisé pour cadrage spec->implémentation: `notion-spec-to-implementation` (adapté en mode repo local).
- Agent `bt-hfp-stability`:
- garantir `DIAL => queue + connect HFP + auto-dial post-SLC`.
- garder `discoverable=true` si aucun client BT connecté.
- Agent `rtos-coex-wifi-bt`:
- forcer `WIFI_PS_MIN_MODEM` en STA/APSTA pour coexistence WiFi+BT.
- supprimer les reboots `Should enable WiFi modem sleep ...`.
- Agent `telephony-call-routing`:
- appel entrant HFP (`ringing`) => sonnerie RTC.
- décroché RTC en sonnerie => `BT_ANSWER`.
- Gate de validation utilisateur (bench USB):
- confirmer logs `dial_trigger ... ok=true` puis `hfp_slc_connected`/`hfp_dial_requested`.
- confirmer appel entrant GSM => sonnerie RTC immédiate.
## Exécution autonome (batch courant)
- `main.cpp`:
- ordre de boot ajusté pour réduire contention WiFi/BT (`WiFi/ESP-NOW` avant `BT begin`).
- commandes runtime ajoutées: `BT_AUTO_RECONNECT_ON`, `BT_AUTO_RECONNECT_OFF`.
- `BluetoothManager`:
- flag runtime `auto_reconnect_enabled` exposé dans `BT_STATUS`.
- reconnexion auto conditionnée par policy + queue dial.
- `WebServerManager` + WebUI:
- SSE réactivé (`/api/events`) + fallback polling navigateur.
- endpoint ajouté: `/api/bluetooth/hfp/auto`.
- `EspNowBridge`:
- init WiFi coex durci (mode préservé + modem sleep policy).
-14
View File
@@ -1,14 +0,0 @@
# Audit initial Lecture Audio
## Robustesse
- Instanciation, lecture fichier OK
- Aucun crash lors des appels de méthodes
## Fiabilité
- Méthodes stables, pas derreur détectée
## Points à surveiller
- Tests hardware à approfondir
---
_Audit généré automatiquement._
-14
View File
@@ -1,14 +0,0 @@
# Audit initial SLIC
## Robustesse
- Instanciation, monitoring ligne OK
- Aucun crash lors des appels de méthodes
## Fiabilité
- Méthodes stables, pas derreur détectée
## Points à surveiller
- Tests hardware à approfondir
---
_Audit généré automatiquement._
-14
View File
@@ -1,14 +0,0 @@
# Audit initial Téléphone SFP
## Robustesse
- Instanciation, gestion appel OK
- Aucun crash lors des appels de méthodes
## Fiabilité
- Méthodes stables, pas derreur détectée
## Points à surveiller
- Tests hardware à approfondir
---
_Audit généré automatiquement._
-30
View File
@@ -1,30 +0,0 @@
# Audit robustesse multitâche RTOS
## Objectif
Valider la robustesse, la résilience et la fiabilité des tâches FreeRTOS (audio, web, batterie) dans le firmware RTC_BL_PHONE.
## Méthodologie
- Analyse des priorités et synchronisation des tâches
- Tests de surcharge CPU (stress)
- Simulation de défaillances (watchdog, blocage, deadlock)
- Vérification de la reprise après erreur
- Monitoring des ressources (heap, stack, CPU)
## Résultats
- Les tâches audio, web et batterie fonctionnent en parallèle sans blocage.
- Aucun deadlock détecté lors des tests de surcharge.
- Le watchdog détecte et relance les tâches bloquées.
- La gestion des priorités permet une reprise fluide après interruption.
- La consommation mémoire reste stable sous stress.
## Points damélioration
- Ajouter logs détaillés sur les erreurs de synchronisation.
- Optimiser la gestion des priorités pour les tâches critiques (audio).
- Renforcer la surveillance du heap pour éviter fragmentation.
## Conclusion
La robustesse multitâche RTOS est validée. Les tâches sont résilientes, le système gère correctement les erreurs et la reprise. Prochaine étape : CI validation et stress tests automatisés.
---
_Agent RTOS Rapport généré automatiquement._
-30
View File
@@ -1,30 +0,0 @@
# Audit sécurité endpoints web — Agent Web
## Objectif
Analyser les risques et proposer des mesures pour sécuriser les endpoints HTTP.
---
### Points vérifiés
- Validation des entrées (pas de POST, GET simple)
- Pas de buffer overflow possible (ESPAsyncWebServer gère la taille)
- Pas dinjection (pas de paramètre dynamique pour linstant)
- Pas dauthentification (à ajouter si besoin)
- Pas de données sensibles exposées
- Logs non dynamiques (pas de fuite)
### Recommandations
- Ajouter validation stricte des paramètres pour endpoints dynamiques
- Implémenter authentification (token, basic auth) pour `/config`, `/logs`
- Limiter accès à `/logs` (IP, token)
- Ajouter rate limiting (anti-DOS)
- Journaliser les accès critiques
### Priorité
- Authentification endpoints critiques
- Validation entrées POST/GET
- Rate limiting
---
**Version :** 2026-02-17
-92
View File
@@ -1,92 +0,0 @@
# ESP-NOW API v1 (enveloppe `msg_id/seq/type/payload/ack`)
Date: 2026-02-21
Scope: contrat d'échange entre `RTC_BL_PHONE` (A252) et la seconde carte.
## 1. Objectif
Normaliser les trames ESP-NOW pour:
- corréler requête/réponse,
- conserver compatibilité legacy,
- simplifier l'intégration du second repo.
## 2. Requête v1 (nouveau format recommandé)
```json
{
"proto": "rtcbl/1",
"msg_id": "req-001",
"seq": 1,
"type": "command",
"ack": true,
"payload": {
"cmd": "STATUS",
"args": {}
}
}
```
Règles:
- `proto=rtcbl/1` recommandé (toléré absent pour compat).
- `msg_id` sert à corréler la réponse.
- `seq` est un compteur local de trame (recommandé monotone par source).
- `type=command|request|cmd` déclenche l'exécution côté firmware.
- `ack=true` demande une réponse corrélée.
- `payload.cmd` obligatoire pour une commande dispatcher.
- `payload.args` optionnel; sérialisé puis passé au dispatcher.
## 3. Réponse v1 (ack corrélée)
```json
{
"proto": "rtcbl/1",
"msg_id": "req-001",
"seq": 1,
"type": "ack",
"ack": true,
"payload": {
"ok": true,
"code": "STATUS",
"data": {},
"error": ""
}
}
```
Règles:
- `msg_id` et `seq` reprennent la requête.
- `payload.ok=false` => `payload.error` non vide.
- `payload.data` contient le JSON de la commande si disponible.
- fallback possible `payload.data_raw` si la réponse n'est pas JSON.
## 4. Compatibilité legacy
Le firmware continue d'accepter les formats existants:
- `{"cmd":"..."}`
- `{"raw":"..."}`
- `{"command":"..."}`
- `{"action":"..."}`
- variantes imbriquées via `event`, `message`, `payload`
- format historique `rtcbl/1`:
- `{"proto":"rtcbl/1","id":"...","cmd":"...","args":{}}`
## 5. Commandes recommandées v1
- `STATUS`
- `RING`
- `CALL`
- `HOOK`
- `LIST_FILES`
- `PLAY_FILE`
- `WIFI_STATUS`
- `MQTT_STATUS`
- `ESPNOW_STATUS`
- `BT_STATUS`
## 6. Intégration second repo
Checklist minimum côté seconde carte:
1. Émettre `msg_id` unique + `seq` monotone.
2. Positionner `type=command` et `ack=true` pour obtenir une réponse.
3. Implémenter timeout de réponse (2-5s) et corréler sur `msg_id`.
4. Prévoir fallback legacy (`rtcbl/1` + formats `cmd/raw/command/action`) pour compat.
-21
View File
@@ -1,21 +0,0 @@
# Fiche technique AudioManager
## Interface
- Méthodes principales : init(), play(), stop(), setVolume(), getStatus()
- Gestion des flux audio (lecture, enregistrement)
## Flux de données
- Entrée : fichiers audio, flux PCM
- Sortie : DAC, I2S, logs
## Scénarios dutilisation
- Lecture de fichier audio
- Contrôle du volume
- Gestion des erreurs
## Exemple dintégration
```cpp
AudioManager audio;
audio.init();
audio.play("test.wav");
```
-21
View File
@@ -1,21 +0,0 @@
# Fiche technique RTOSManager
## Interface
- Méthodes principales : start(), stop(), createTask(), audit(), getStatus()
- Gestion multitâche, watchdog
## Flux de données
- Entrée : tâches, signaux
- Sortie : logs, états
## Scénarios dutilisation
- Création de tâches
- Audit du système
- Gestion du watchdog
## Exemple dintégration
```cpp
RTOSManager rtos;
rtos.start();
rtos.createTask(myTask);
```

Some files were not shown because too many files have changed in this diff Show More