From d7ba42f5dd1b27dbdbfdc1cf92a53cbf80403e6c Mon Sep 17 00:00:00 2001 From: clement Date: Fri, 19 Jun 2026 07:52:25 +0200 Subject: [PATCH] docs(plan): plan Phase 1 socle ESP-IDF --- .../plans/2026-06-19-phase1-socle-idf.md | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-19-phase1-socle-idf.md diff --git a/docs/superpowers/plans/2026-06-19-phase1-socle-idf.md b/docs/superpowers/plans/2026-06-19-phase1-socle-idf.md new file mode 100644 index 0000000..bf15cb8 --- /dev/null +++ b/docs/superpowers/plans/2026-06-19-phase1-socle-idf.md @@ -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 : +- PSRAM : (preuve : ) +- Décision : +- Raison : +``` + +- [ ] **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.