docs(plan): plan Phase 1 socle ESP-IDF

This commit is contained in:
clement
2026-06-19 07:52:25 +02:00
parent d63e9cafef
commit d7ba42f5dd
@@ -0,0 +1,374 @@
# Phase 1 — Socle ESP-IDF : Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Obtenir un projet ESP-IDF natif minimal qui build, flashe et boote sur l'ESP32-A1S (ESP32 classique), avec logs série, Bluetooth Classic activé dans la config, et NVS initialisé — sans aucun driver métier encore.
**Architecture :** Nouveau projet ESP-IDF à la racine du dépôt (`CMakeLists.txt`, `main/`, `components/`), coexistant avec le code Arduino existant (`src/`, `platformio.ini`) que le build IDF ignore. La cible est `esp32`. Le `sdkconfig` est généré depuis un `sdkconfig.defaults` versionné qui active Bluedroid BR/EDR + HFP-HF (chemin data HCI). NVS est initialisé au boot via un composant `config_store` embryonnaire.
**Tech Stack :** ESP-IDF v5.4 (`/Users/electron/esp/esp-idf`), CMake, Bluedroid (Bluetooth Classic), NVS, esp_log.
## Global Constraints
- ESP-IDF v5.4 — activer l'environnement dans CHAQUE shell avant tout `idf.py` : `. /Users/electron/esp/esp-idf/export.sh`
- Cible unique : `esp32` (classique, module A1S) — PAS de S3.
- Port série par défaut : `/dev/cu.usbserial-0001` (peut varier ; vérifier avec `ls /dev/cu.*`).
- Aucune dépendance Arduino dans le projet IDF.
- Logs via `ESP_LOGx` uniquement (jamais `Serial`/`printf` brut).
- Le `build/` et le `sdkconfig` généré ne sont PAS versionnés ; seul `sdkconfig.defaults` l'est.
- Bluetooth : Bluedroid, mode **BR/EDR only** (pas de BLE — économie RAM), **HFP-HF** activé, chemin audio **HCI** (`CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI`).
- Flash 4 MB (cf. module A1S) ; partitions sans OTA.
- PSRAM : ne PAS activer aveuglément (variante A1S incertaine) — détecter puis décider (Task 4).
---
### Task 1 : Squelette projet IDF qui boote
**Files:**
- Create: `CMakeLists.txt` (racine)
- Create: `main/CMakeLists.txt`
- Create: `main/app_main.c`
- Modify: `.gitignore` (ajouter `build/`, `sdkconfig`, `sdkconfig.old`, `dependencies.lock`, `managed_components/`)
**Interfaces:**
- Consumes: rien (point de départ).
- Produces: une fonction d'entrée `void app_main(void)` qui log une bannière de boot. Les phases suivantes y câbleront leurs `*_init()`.
- [ ] **Step 1 : Créer le `CMakeLists.txt` racine**
```cmake
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(rtc_bl_phone)
```
- [ ] **Step 2 : Créer `main/CMakeLists.txt`**
```cmake
idf_component_register(
SRCS "app_main.c"
INCLUDE_DIRS "."
REQUIRES nvs_flash
)
```
- [ ] **Step 3 : Créer `main/app_main.c`**
```c
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "rtc_phone";
void app_main(void)
{
ESP_LOGI(TAG, "RTC BL PHONE — socle ESP-IDF v5.4 (esp32 classique)");
ESP_LOGI(TAG, "boot OK — aucun driver chargé (Phase 1)");
while (true) {
ESP_LOGI(TAG, "heartbeat");
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
```
- [ ] **Step 4 : Mettre à jour `.gitignore`**
Ajouter ces lignes à `.gitignore` (les laisser groupées sous un commentaire) :
```
# ESP-IDF
/build/
/sdkconfig
/sdkconfig.old
/dependencies.lock
/managed_components/
```
- [ ] **Step 5 : Activer l'environnement et fixer la cible**
Run (dans le shell, une fois) :
```bash
. /Users/electron/esp/esp-idf/export.sh
cd /Users/electron/RTC_BL_PHONE
idf.py set-target esp32
```
Expected : `Building ESP-IDF components for target esp32` puis génération du `sdkconfig`, se termine sans erreur. Un fichier `sdkconfig` apparaît.
- [ ] **Step 6 : Builder**
Run : `idf.py build`
Expected : se termine par `Project build complete.` et affiche la taille binaire (`rtc_bl_phone.bin`). Aucune erreur de compilation.
- [ ] **Step 7 : Flasher et observer le boot**
Run : `idf.py -p /dev/cu.usbserial-0001 flash monitor`
Expected : après le reset, les lignes de log apparaissent :
```
I (xxx) rtc_phone: RTC BL PHONE — socle ESP-IDF v5.4 (esp32 classique)
I (xxx) rtc_phone: boot OK — aucun driver chargé (Phase 1)
I (xxx) rtc_phone: heartbeat
```
La ligne `heartbeat` se répète toutes les 5 s. Quitter le monitor avec `Ctrl+]`.
(Si pas de matériel branché : valider au minimum que `idf.py build` réussit — le flash/monitor sera fait par l'utilisateur.)
- [ ] **Step 8 : Commit**
```bash
git add CMakeLists.txt main/CMakeLists.txt main/app_main.c .gitignore
git commit -m "feat(idf): socle projet ESP-IDF qui boote"
```
(Sujet ≤ 50 car., sans attribution IA — un hook local le vérifie.)
---
### Task 2 : Config Bluetooth Classic + table de partitions
**Files:**
- Create: `sdkconfig.defaults`
- Create: `partitions.csv`
**Interfaces:**
- Consumes: le projet de Task 1.
- Produces: un `sdkconfig` régénéré où `CONFIG_BT_ENABLED`, `CONFIG_BT_CLASSIC_ENABLED`, `CONFIG_BT_HFP_CLIENT_ENABLE` et `CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI` valent `y`. Phase 4 (`bt_hfp`) en dépend.
- [ ] **Step 1 : Créer `partitions.csv` (4 MB, sans OTA)**
```csv
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 0x2F0000,
```
- [ ] **Step 2 : Créer `sdkconfig.defaults`**
```
# Cible
CONFIG_IDF_TARGET="esp32"
# Partitions
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
# Bluetooth : Bluedroid, BR/EDR only (pas de BLE), HFP-HF, data path HCI
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=y
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y
CONFIG_BT_HFP_ENABLE=y
CONFIG_BT_HFP_CLIENT_ENABLE=y
CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI=y
# Confort logs
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
# Watchdog : tolérer la boucle de heartbeat
CONFIG_ESP_TASK_WDT_INIT=n
```
- [ ] **Step 3 : Régénérer la config et builder**
Run :
```bash
rm -f sdkconfig
idf.py build
```
Expected : build OK. Note : la première reconfig peut afficher des avertissements de dépendances Kconfig BT — acceptable tant que le build se termine.
- [ ] **Step 4 : Vérifier que les clés BT sont bien appliquées**
Run :
```bash
grep -E "CONFIG_BT_CLASSIC_ENABLED=|CONFIG_BT_HFP_CLIENT_ENABLE=|CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI=|CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=" sdkconfig
```
Expected (4 lignes, toutes `=y`) :
```
CONFIG_BT_CLASSIC_ENABLED=y
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y
CONFIG_BT_HFP_CLIENT_ENABLE=y
CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI=y
```
Si une clé manque ou vaut `n` : ouvrir `idf.py menuconfig``Component config → Bluetooth` pour repérer le nom exact dans cette build d'IDF, corriger `sdkconfig.defaults`, puis reprendre au Step 3.
- [ ] **Step 5 : Commit**
```bash
git add sdkconfig.defaults partitions.csv
git commit -m "feat(idf): config Bluetooth Classic HFP-HF + partitions"
```
---
### Task 3 : Composant `config_store` (init NVS)
**Files:**
- Create: `components/config_store/CMakeLists.txt`
- Create: `components/config_store/include/config_store.h`
- Create: `components/config_store/config_store.c`
- Modify: `main/CMakeLists.txt` (ajouter `config_store` aux `REQUIRES`)
- Modify: `main/app_main.c` (appeler `config_store_init()`)
**Interfaces:**
- Consumes: NVS (`nvs_flash`), le squelette de Task 1.
- Produces:
- `esp_err_t config_store_init(void);` — initialise NVS (gère `ESP_ERR_NVS_NO_FREE_PAGES` / `ESP_ERR_NVS_NEW_VERSION_FOUND` par effacement+réinit), retourne `ESP_OK` si la partition NVS est prête. Les phases ultérieures stockeront leur config via ce composant.
- [ ] **Step 1 : Créer `components/config_store/CMakeLists.txt`**
```cmake
idf_component_register(
SRCS "config_store.c"
INCLUDE_DIRS "include"
REQUIRES nvs_flash
)
```
- [ ] **Step 2 : Créer `components/config_store/include/config_store.h`**
```c
#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
```
- [ ] **Step 3 : Créer `components/config_store/config_store.c`**
```c
#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;
}
```
- [ ] **Step 4 : Référencer le composant dans `main/CMakeLists.txt`**
Remplacer le contenu par :
```cmake
idf_component_register(
SRCS "app_main.c"
INCLUDE_DIRS "."
REQUIRES nvs_flash config_store
)
```
- [ ] **Step 5 : Appeler `config_store_init()` dans `main/app_main.c`**
Insérer l'include en tête et l'appel au début de `app_main`, avant la boucle :
```c
#include "config_store.h"
```
et, juste après la première ligne de log de bannière :
```c
ESP_ERROR_CHECK(config_store_init());
```
- [ ] **Step 6 : Builder**
Run : `idf.py build`
Expected : `Project build complete.` sans erreur.
- [ ] **Step 7 : Flasher et vérifier le log NVS**
Run : `idf.py -p /dev/cu.usbserial-0001 flash monitor`
Expected : présence de la ligne :
```
I (xxx) config_store: NVS prêt
```
avant les `heartbeat`. (Sans matériel : valider que `idf.py build` réussit.)
- [ ] **Step 8 : Commit**
```bash
git add components/config_store main/CMakeLists.txt main/app_main.c
git commit -m "feat(idf): composant config_store init NVS"
```
---
### Task 4 : Détection PSRAM et décision d'activation
**Files:**
- Modify: `sdkconfig.defaults` (uniquement si PSRAM détectée)
- Create: `docs/superpowers/notes/2026-06-19-psram-a1s.md` (consigner le résultat)
**Interfaces:**
- Consumes: le binaire bootable des tâches précédentes.
- Produces: une décision tracée (PSRAM présente → activée ; absente → laissée off avec justification). Phase 4 s'appuiera dessus pour dimensionner les buffers BT/audio.
- [ ] **Step 1 : Identifier la variante de flash/PSRAM de la carte**
Run : `esptool.py --port /dev/cu.usbserial-0001 flash_id`
Expected : affiche `Detected flash size` (ex. `4MB`). Noter la valeur.
Pour la PSRAM, observer le log de boot complet : au démarrage l'IDF logue la présence/absence de SPIRAM. Run `idf.py -p /dev/cu.usbserial-0001 monitor` et chercher une ligne `spiram` / `SPI RAM` pendant le boot du second-stage bootloader.
- [ ] **Step 2 : Consigner le résultat**
Créer `docs/superpowers/notes/2026-06-19-psram-a1s.md` avec :
```markdown
# Variante carte A1S — flash & PSRAM (2026-06-19)
- Flash détectée : <valeur de flash_id>
- PSRAM : <présente | absente> (preuve : <ligne de log boot>)
- Décision : <PSRAM activée dans sdkconfig.defaults | laissée désactivée>
- Raison : <justification>
```
- [ ] **Step 3 : (Conditionnel) Activer la PSRAM si présente**
UNIQUEMENT si le Step 1 confirme la présence de PSRAM, ajouter à `sdkconfig.defaults` :
```
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=n
```
puis `rm -f sdkconfig && idf.py build` et vérifier `grep CONFIG_SPIRAM= sdkconfig``CONFIG_SPIRAM=y`, et que le boot ne régresse pas (`idf.py flash monitor` → toujours `NVS prêt` + `heartbeat`).
Si PSRAM absente : ne rien changer au `sdkconfig.defaults` (laisser off).
- [ ] **Step 4 : Commit**
```bash
git add docs/superpowers/notes/2026-06-19-psram-a1s.md sdkconfig.defaults
git commit -m "chore(idf): trace decision PSRAM carte A1S"
```
(`sdkconfig.defaults` n'est ré-ajouté que s'il a changé au Step 3.)
---
## Critère de sortie de Phase 1
Le firmware ESP-IDF natif **build, flashe et boote** sur l'ESP32-A1S, logue une bannière + `NVS prêt` + `heartbeat` en série, avec Bluetooth Classic / HFP-HF activé dans la config (vérifié par `grep` sur `sdkconfig`) et la décision PSRAM tracée. Aucun driver métier n'est encore présent — c'est l'objet des Phases 2-5.
## Notes sur les tests
La Phase 1 est du scaffolding : la « vérification » est le **succès du build** + l'**observation du log de boot**, pas des tests unitaires. Les tests unitaires (Unity host) arrivent en Phases 2-3 sur les composants de logique pure (DTMF Goertzel, décodeur d'impulsion, machine d'état SLIC, générateur de tonalités), conformément au spec.