Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1db8862c1 | |||
| e8efda1ce4 | |||
| bef10a0a49 | |||
| 1df3dc5971 | |||
| dbd789f335 | |||
| 8625665a41 | |||
| 8a2280edb0 | |||
| 216dc2523e | |||
| 577a351124 | |||
| 6d3454dffd | |||
| 178931899a | |||
| baed6f4290 | |||
| fc67a1eef8 | |||
| 2be3729824 | |||
| b930411829 | |||
| 2eb97bc4de | |||
| e4123bd32f | |||
| 452f3efcf6 | |||
| 935772c1b1 | |||
| f9283bc14f | |||
| f8e3dfb442 | |||
| 7956cebde1 | |||
| 8f722db9f4 | |||
| a978357a47 | |||
| c382d4d8a1 | |||
| 2102d14487 | |||
| d95513481a | |||
| 4d9c81918f | |||
| 4aedf7756d | |||
| 9cb31bdea9 | |||
| 6b9ecbf2e4 | |||
| 24e82fbf3b | |||
| 0ff2c5d4f1 | |||
| a430a24f39 | |||
| 1695539493 | |||
| 578ef4ffb2 | |||
| eb2d7fb860 | |||
| 8ca352d173 | |||
| 55b54da086 | |||
| 21ca98f869 | |||
| aec7599a22 | |||
| a89fa2b746 | |||
| c5eabc44c5 | |||
| c11572307e | |||
| f3b92bf6e3 | |||
| 04a12e9351 | |||
| 7844378e2c | |||
| 808f66595b | |||
| 22aca75e57 | |||
| aea32cfd41 | |||
| 12363e9541 | |||
| 46c03e1fef | |||
| c350304b23 | |||
| f452387d92 | |||
| e14c912dac | |||
| 0d378c8cb2 | |||
| 8c23ce05c2 | |||
| 1a6e3e95d9 | |||
| 43e2edf97d | |||
| f90c390595 | |||
| 4ac05ca1f0 | |||
| d7ba42f5dd | |||
| d63e9cafef | |||
| 87bcb1610d | |||
| a638fad71b | |||
| e719f54711 | |||
| eb2f78d0c6 | |||
| c93fe81592 | |||
| 2e4e90adef | |||
| d2befa2c8d | |||
| 1ce018e6eb | |||
| fca57a133d | |||
| a50f3c656b | |||
| efeacb79f8 | |||
| 5a2d86e253 |
@@ -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 d’action
|
||||
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
|
||||
|
||||
@@ -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é.
|
||||
@@ -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 d’action` (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 d’action 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`.
|
||||
@@ -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)
|
||||
@@ -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 d’action
|
||||
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
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
|
||||
# Modèle de plan d’action – RTC_BL_PHONE
|
||||
|
||||
Chaque fiche `.github/agents/<agent>.md` doit inclure une section `## Plan d’action` structurée ainsi :
|
||||
|
||||
1. **Contexte et validation**
|
||||
- Expliquer le contexte de l’action (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`).
|
||||
@@ -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 l’audio (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).
|
||||
- S’assurer 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 d’ID 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 d’initialisation I2S dans le firmware (src/main.cpp)
|
||||
|
||||
## Plan d’action
|
||||
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 d’I2S (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 l’alimentation.
|
||||
- Documenter toute divergence hardware/firmware dans README et docs/solutions_rtc_phone_esp32.md.
|
||||
- Logs série obligatoires pour tout bug ou anomalie.
|
||||
|
||||
@@ -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 d’action
|
||||
1. Vérifier la syntaxe et les chemins de chaque workflow modifié.
|
||||
2. S’assurer que les builds PlatformIO passent sur toutes les cibles supportées.
|
||||
3. Documenter toute évolution CI dans le projet.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 d’action
|
||||
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
|
||||
|
||||
@@ -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).
|
||||
- S’assurer 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 d’action
|
||||
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.
|
||||
@@ -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 d’action
|
||||
1. Régénérer l’index 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
|
||||
|
||||
@@ -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 l’audio (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 d’artefacts de test ou de logs binaires.
|
||||
|
||||
## Références
|
||||
- src/main.cpp
|
||||
- README.md
|
||||
- docs/solutions_rtc_phone_esp32.md
|
||||
|
||||
## Plan d’action
|
||||
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.
|
||||
@@ -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 d’action
|
||||
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
|
||||
@@ -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.
|
||||
- S’assurer 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.
|
||||
@@ -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 d’action
|
||||
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.
|
||||
|
||||
@@ -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 d’environnement.
|
||||
- Vérifier que les scripts sont cohérents avec l’architecture réelle du projet.
|
||||
|
||||
## À ne pas faire
|
||||
- Ne pas hardcoder de chemins ou ports spécifiques à une machine.
|
||||
- Ne pas forcer l’interaction utilisateur si un flag ou une attente CLI suffit.
|
||||
|
||||
## Références
|
||||
- tools/
|
||||
- README.md
|
||||
|
||||
## Plan d’action
|
||||
1. Vérifier et documenter l’aide de chaque script modifié.
|
||||
2. S’assurer 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** : S’assurer 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** : S’assurer que l’auto-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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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/
|
||||
|
||||
|
||||
@@ -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` |
|
||||
@@ -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).
|
||||
@@ -0,0 +1,3 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(rtc_bl_phone)
|
||||
@@ -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
|
||||
@@ -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 l’onglet 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 (300–3400 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 l’onglet 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é d’ajouter 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 d’interface (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 d’utilisation
|
||||
- Lecture audio locale
|
||||
- Streaming Bluetooth
|
||||
- Enregistrement et restitution
|
||||
|
||||
#### Exemple d’inté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 d’utilisation
|
||||
- Multitâche (audio, Bluetooth, web, etc.)
|
||||
- Synchronisation entre modules
|
||||
- Gestion des timers pour actions périodiques
|
||||
|
||||
#### Exemple d’inté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 d’utilisation
|
||||
- Streaming audio via Bluetooth
|
||||
- Commandes distantes
|
||||
- Synchronisation avec smartphone ou périphérique externe
|
||||
|
||||
#### Exemple d’inté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 d’interaction 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/` |
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
idf_component_register(
|
||||
INCLUDE_DIRS "include"
|
||||
)
|
||||
@@ -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 */
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "bt_hfp.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES bt audio_router
|
||||
)
|
||||
@@ -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; }
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "cli.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES audio_router
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "config_store.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES nvs_flash
|
||||
)
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "dialer.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_timer
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "dtmf.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES hal_i2s dialer
|
||||
)
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "es8388.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_i2c esp_driver_gpio bsp
|
||||
)
|
||||
@@ -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, ®, 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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "hal_i2s.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_i2s esp_driver_gpio bsp hal_es8388
|
||||
)
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "hook_monitor.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_timer slic_ks0835 dialer
|
||||
)
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "slic.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_gpio bsp
|
||||
)
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
idf_component_register(
|
||||
SRCS "tone_gen.c"
|
||||
INCLUDE_DIRS "include"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)++;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
@@ -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._
|
||||
@@ -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 d’exé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 d’interop 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é.
|
||||
@@ -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
|
||||
@@ -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 n’utilisent 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
|
||||
@@ -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 d’audit, 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.
|
||||
@@ -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
|
||||
@@ -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é d’une 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`
|
||||
@@ -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é).
|
||||
@@ -1,2 +0,0 @@
|
||||
# Audit initial audio
|
||||
...existing code...
|
||||
@@ -1,2 +0,0 @@
|
||||
# Audit initial lecture audio
|
||||
...existing code...
|
||||
@@ -1,2 +0,0 @@
|
||||
# Audit initial SLIC
|
||||
...existing code...
|
||||
@@ -1,2 +0,0 @@
|
||||
# Audit initial téléphone SFP
|
||||
...existing code...
|
||||
@@ -1,2 +0,0 @@
|
||||
# Rapport CI Web
|
||||
...existing code...
|
||||
@@ -1,2 +0,0 @@
|
||||
# Rapport exécution TODO
|
||||
...existing code...
|
||||
@@ -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...
|
||||
@@ -1,2 +0,0 @@
|
||||
# Rapport tests web
|
||||
...existing code...
|
||||
@@ -1,2 +0,0 @@
|
||||
# Rapport validation hardware
|
||||
...existing code...
|
||||
@@ -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 d’auto-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).
|
||||
@@ -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 d’erreur détectée
|
||||
|
||||
## Points à surveiller
|
||||
- Tests hardware à approfondir
|
||||
|
||||
---
|
||||
_Audit généré automatiquement._
|
||||
@@ -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 d’erreur détectée
|
||||
|
||||
## Points à surveiller
|
||||
- Tests hardware à approfondir
|
||||
|
||||
---
|
||||
_Audit généré automatiquement._
|
||||
@@ -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 d’erreur détectée
|
||||
|
||||
## Points à surveiller
|
||||
- Tests hardware à approfondir
|
||||
|
||||
---
|
||||
_Audit généré automatiquement._
|
||||
@@ -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 d’amé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._
|
||||
@@ -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 d’injection (pas de paramètre dynamique pour l’instant)
|
||||
- Pas d’authentification (à 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
|
||||
@@ -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.
|
||||
@@ -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 d’utilisation
|
||||
- Lecture de fichier audio
|
||||
- Contrôle du volume
|
||||
- Gestion des erreurs
|
||||
|
||||
## Exemple d’intégration
|
||||
```cpp
|
||||
AudioManager audio;
|
||||
audio.init();
|
||||
audio.play("test.wav");
|
||||
```
|
||||
@@ -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 d’utilisation
|
||||
- Création de tâches
|
||||
- Audit du système
|
||||
- Gestion du watchdog
|
||||
|
||||
## Exemple d’inté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
Reference in New Issue
Block a user