docs(routines): design spec for routine mode

Matin/Soir illustrated step routine: SD manifest pushed by Merlin Studio/Tower (Twemoji .bin + fr TTS .mp3), firmware mode with voice announce + done button + final cheer.
This commit is contained in:
L'électron rare
2026-06-21 08:37:13 +02:00
parent ea19aba53d
commit 3c6302bf0e
@@ -0,0 +1,173 @@
# Lisael Box — Mode « Routines » illustrées (design)
- **Date** : 2026-06-21
- **Repo** : `electron-rare/lisael-box` (firmware) + Tower `lisael-content` + Merlin Studio (`merlin.saillant.cc`)
- **Statut** : design validé, prêt pour le plan d'implémentation
## 1. But
Donner à Lisael (6 ans) un mode **Routine illustrée à étapes** : une séquence de
pictogrammes (s'habiller → petit-déj → dents → cartable…) qu'il avance une par une,
avec une **voix qui annonce chaque étape** et un **« Bravo ! » final**. Deux routines
au lancement : **Matin** et **Soir**. Le contenu (étapes, emojis, textes, ordre) est
édité par un parent depuis **Merlin Studio** (web) et poussé sur la box en WiFi —
aucun reflash pour changer une routine.
## 2. Non-objectifs (YAGNI)
- Pas de **minuteur** par étape ni d'avance automatique (décision : avance manuelle).
- Pas de **routines libres** au-delà de Matin/Soir (deux routines fixes).
- Pas de **logique horaire** (la box ne devine pas matin/soir : l'enfant choisit).
- Pas de **photos perso** (emojis Twemoji uniquement).
- Pas de **persistance de progression** entre sessions (chaque ouverture repart au début).
- Pas de **généralisation** du pipeline de contenu Merlin (on étend, on ne refactore pas
`lisael_content.py` en système de types génériques).
## 3. Décisions de cadrage (verrouillées)
| Sujet | Décision |
|---|---|
| Concept | Routine illustrée à étapes (gros picto + nom plein écran) |
| Routines | Matin + Soir (2, fixes) |
| Source du contenu | Pilotée par Merlin Studio (web) → manifest SD + assets poussés en WiFi |
| Interaction | Gros bouton « C'est fait ✓ » + voix TTS par étape + son de validation + Bravo final |
| Déclenchement | Tuile « Routine » à l'accueil → choix Matin/Soir |
| Pictogrammes | Twemoji choisis dans Merlin → `.bin` RGB565A8 généré par Tower |
| Édition web | Éditer les étapes de Matin/Soir : texte + emoji, ajouter/supprimer/réordonner |
| Voix | `fr` (= « siwis » de la gateway, voix validée pour le jeu Lire) |
| Reset | Repart de l'étape 1 à chaque ouverture, pas de persistance |
| Cap | ~12 étapes / routine, structures en PSRAM |
| Livraison | **Tout d'un bloc** (firmware + Tower + web), test final global |
## 4. Architecture d'ensemble
```
Merlin Studio (web) Tower (lisael-content) Box (firmware)
merlin.saillant.cc clems@192.168.0.120 ESP32-S3-BOX-3
───────────────── ────────────────────── ─────────────────
Onglet "Routines" ──POST──▶ routines.json (vérité)
édite Matin/Soir + Twemoji→.bin (RGB565A8)
(texte+emoji, + TTS→.mp3 (voix "fr")
ordre, +/) + manifest SD
───push WiFi (:8080/put)──▶ /sdcard/routines/*
(delta, comme podcasts) routines.json + .bin + .mp3
mode Routine lit le manifest
```
Réutilise trois briques déjà en prod : le jeu **Lire** (pictos Twemoji `.bin` +
TTS `.mp3` + cache PSRAM), l'**autopush Tower** (`box_register` + `compute_delta`
+ `push_batch` vers `:8080/put`), et **Merlin Studio** (UI + API + Basic Auth).
## 5. Contenu sur la SD — `/sdcard/routines/`
Manifest unique + assets, tout poussé par Tower :
```json
{ "routines": [
{ "id":"matin", "title":"Le matin", "icon":"matin.bin", "steps":[
{"text":"Habille-toi", "icon":"matin_01.bin", "audio":"matin_01.mp3"},
{"text":"Petit-déjeuner", "icon":"matin_02.bin", "audio":"matin_02.mp3"},
{"text":"Brosse tes dents", "icon":"matin_03.bin", "audio":"matin_03.mp3"},
{"text":"Prépare ton cartable", "icon":"matin_04.bin", "audio":"matin_04.mp3"} ]},
{ "id":"soir", "title":"Le soir", "icon":"soir.bin", "steps":[ ... ] }
]}
```
Règles :
- pictos `.bin` **RGB565A8** (même format/pipeline que Lire), audio `.mp3` **mono**.
- noms de fichiers **ASCII** (`matin_01`) ; les libellés accentués vivent dans le JSON
(UTF-8, rendu par Fredoka) — leçon LFN/codepage FAT du jeu Lire.
- `icon` de routine = vignette du choix Matin/Soir (Twemoji ☀️ / 🌙).
## 6. Firmware — mode `LISAEL_MODE_ROUTINE`
### Câblage
- `main/ui/ui.h` : ajouter `LISAEL_MODE_ROUTINE` à l'enum + `lv_obj_t *lisael_screen_routine(void);`.
- `main/modes/routine.c` : nouvel écran.
- `main/ui/assets/lisael_ic_routine.c` : icône de tuile (Twemoji bakée, ✅/🗓️, RGB565A8).
- `main/modes/home.c` : ajouter la tuile au carrousel d'accueil + route dans `ui.c`.
### Écrans
1. **Choix** : deux grosses cartes *Matin* (☀️) / *Soir* (🌙), tap → ouvre la routine.
2. **Étape plein écran** : gros picto (cache PSRAM + FS `S:`, comme `lire.c`) + nom
(Fredoka) + indicateur de progression **3/6** (pastilles) + bouton géant
**« C'est fait ✓ »** + bouton retour Accueil.
- à l'arrivée sur une étape : **annonce vocale** (`aac_player` lit le `.mp3`, type MP3).
- tap « C'est fait » : **son de validation** (court) → étape suivante.
- dernière étape validée : écran **Bravo** (animation étoiles `lisael_ic_star` + son) →
retour Accueil.
### Données & RAM
- `main/audio/routine_manifest.{c,h}` : parse `/sdcard/routines/routines.json`
structures `lisael_routine_t` / `lisael_step_t`, allouées en **PSRAM**
(`heap_caps_*(MALLOC_CAP_SPIRAM)`), caps **2 routines × 12 étapes**.
- Cache picto **PSRAM** : lire chaque `.bin` une fois en PSRAM et le présenter comme
`lv_image_dsc_t` RAM (data = buf+12), pour éviter la relecture SD à chaque frame
(piège perf corrigé dans `lire.c`).
- Reset : `lisael_screen_routine()` repart toujours de l'étape 0.
## 7. Tower — génération + push
Dans `tower/lisael_content.py` (module partagé), ajouter le support « routines » :
- **vérité** : `routines.json` côté Tower (`/home/clems/lisael-content/`), édité par l'API web.
- **pictos** : pour chaque étape, télécharger le Twemoji de l'emoji → resize →
`.bin` RGB565A8 via `LVGLImage` (pipeline du jeu Lire).
- **voix** : annonce TTS via la gateway (`POST <gateway>/v1/audio/speech`,
`model=tts-1`, `voice=fr`) → `.mp3` mono. Gateway = origine tailnet Tower `:9300`
(⚠️ Cloudflare bloque l'UA `Python-urllib` sur les domaines publics → appel direct
tailnet, ou `User-Agent: Mozilla/5.0`).
- **manifest SD** : composer `routines.json` (chemins `.bin`/`.mp3` relatifs).
- **push** : étendre `box_register` pour annoncer aussi `routines/*` dans `have`, et
réutiliser `compute_delta` + `push_batch` vers `:8080/put?name=routines/<f>`.
- **cache** : ne régénérer un `.bin`/`.mp3` que si l'emoji/texte a changé (hash du
couple emoji+texte dans le nom ou un index), comme le suffixe `_a48` des podcasts.
## 8. Merlin Studio — onglet « Routines »
- `tower/merlin_ui.html` : nouvel onglet, 2 sections **Matin** / **Soir**. Par étape :
champ **texte** + **sélecteur d'emoji** + bouton supprimer ; **+ ajouter une étape** ;
réordonner (flèches ↑↓) ; bouton **Enregistrer & pousser**.
- `tower/lisael_server.py` : `GET /api/routines` (lit `routines.json`),
`POST /api/routines` (sauve + régénère pictos/TTS + met à jour le manifest staging),
`POST /api/routines/push` (push delta à la box). Sous la **Basic Auth** existante.
- Sélecteur d'emoji : palette d'emojis enfant courants (dents/habits/repas/bain/dodo/
cartable/jouets…) + saisie libre d'un emoji.
## 9. Gestion d'erreurs (dégradés gracieux)
- pas de `routines.json` sur SD → écran « Demande à un parent de préparer tes routines ».
- picto manquant → case colorée avec 1ʳᵉ lettre (fallback du jeu Lire).
- audio manquant pour une étape → étape affichée sans voix (non bloquant).
- TTS gateway indisponible → Tower garde texte+picto, audio absent (non bloquant).
- push échoué (WiFi faible) → réessai au prochain `register` (pattern existant) ;
fichier partiel supprimé côté box (`unlink`, durcissement file-server existant).
## 10. Tests
- **Hôte** : `test/host/test_routine_manifest.c` (JSON valide / malformé / vide /
caps dépassés) ; test Tower de génération + delta (calqué sur `test_delta.py`).
- **Sur carte** (box branchée en USB) : boot-burst USB-JTAG + écoute UDP —
manifest chargé, pictos affichés, voix jouée par étape, son de validation, Bravo final.
## 11. Pré-requis build (dette à régler avant de compiler)
Le `sdkconfig` (gitigné) a été perdu avec `/tmp`. `sdkconfig.defaults*` ne contient
PAS tous les réglages critiques. Avant le premier build neuf, reconfigurer (ou
remonter dans `sdkconfig.defaults`, recommandé) :
`FATFS_LFN_HEAP` + `FATFS_MAX_LFN=255`, `LV_USE_FS_STDIO` + lettre `S` (83),
`MBEDTLS_EXTERNAL_MEM_ALLOC` + `MBEDTLS_DYNAMIC_BUFFER`, `ESP_WIFI_TX_BUFFER_TYPE=1`,
`SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY`, + les creds WiFi (ou override NVS). Sans ces
réglages : manifest illisible, pictos invisibles, reset TLS. À traiter dans le plan
(ce sera la première tâche, sinon rien ne tourne).
## 12. Critères d'acceptation
1. Une tuile « Routine » apparaît au carrousel d'accueil ; tap → choix Matin/Soir.
2. Choisir Matin enchaîne les étapes : gros picto + nom, voix qui annonce, bouton
« C'est fait ✓ », son de validation, jusqu'au Bravo final.
3. Le contenu vient de `/sdcard/routines/routines.json` (aucun contenu en dur figé).
4. Depuis Merlin, ajouter/supprimer/réordonner une étape + changer texte/emoji, puis
« Enregistrer & pousser » → la box reflète le changement après push (au boot/register).
5. Dégradés gracieux vérifiés : pas de manifest, picto/audio manquant, push partiel.
6. Tests hôte verts (parser + delta Tower).