Files
lisael-box/docs/superpowers/specs/2026-06-15-lisael-box-tower-autopush-design.md
Clément Saillant c46a2e268b docs: Spec A autopush design + plan
Design and implementation plan for Tower auto-push: registration-
triggered, weekly, podcasts only. Ignore __pycache__ and *.real
editor backups.
2026-06-15 07:45:15 +02:00

6.1 KiB
Raw Permalink Blame History

Spec A — Tower pousse les podcasts automatiquement (design)

Date: 2026-06-15 Statut: validé (brainstorming), prêt pour le plan. Projet: Lisael Box (ESP32-S3-BOX-3) + Tower (clems@192.168.0.120).

But

Garder les podcasts de la box à jour automatiquement, sans MSC ni intervention : Tower (toujours allumé) rafraîchit les derniers épisodes Radio France chaque semaine et les pousse vers la box dès qu'elle se signale en ligne. La box ne fait jamais de téléchargement lourd (elle reçoit en HTTP simple via son file-server :8080 déjà en place).

Décisions (validées)

  • Auto-refresh : podcasts seulement (épisodes + pochettes + manifeste).
  • Découverte/déclenchement : enregistrement — la box s'annonce à Tower, qui pousse alors le delta (fichiers que la box n'a pas).
  • Fréquence du refresh Tower : hebdomadaire.
  • Port serveur Tower : :9555 ; id box fixe : lisael-box.

Architecture

Tower (192.168.0.120)                              Box (Les cils, DHCP)
┌─────────────────────────────┐                    ┌────────────────────────┐
│ lisael-refresh.timer (hebdo)│                    │ net_task @ WiFi connect │
│   → lisael_refresh.py       │                    │   lit /sdcard/podcasts  │
│   RSS RF → épisodes+cover    │                    │   POST /register ───────┼──┐
│   → /home/clems/lisael-      │                    └────────────────────────┘  │
│      content/podcasts/ (stg)│                                                  │
│                              │   ◄──── POST /register {id,ip,have} ───────────┘
│ lisael-content.service :9555 │
│   /register → diff(stg,have) │   ──── POST :8080/put?name=podcasts/<f> ───►  file-server
│   → push delta + manifest    │        (épisodes manquants + manifest.json)    → /sdcard/podcasts
└─────────────────────────────┘

Composants

Tower — lisael_refresh.py (timer hebdo)

  • Pour chaque show (Oli rss_19721, Les Odyssées rss_20108, Salut l'info rss_20689, Bestioles proxycast) : fetch RSS, prendre les N derniers épisodes (N=3 configurable), télécharger l'enclosure (HTTPS), récupérer <itunes:image> → redimensionner 160×160 → .bin LVGL RGB565 via LVGLImage.py. Noms ASCII key_NN.<ext> / key.bin.
  • Reconstruire manifest.json v2 = [{show,cover,episodes:[{file,title}]}].
  • Écrire le tout dans le staging /home/clems/lisael-content/podcasts/ (atomique : écrire dans un tmp puis renommer). Reprend la logique éprouvée de tools/gen_covers.py. Idempotent (skip si déjà présent).

Tower — lisael_server.py (service :9555, toujours actif)

  • POST /register corps JSON { "id": "lisael-box", "have": ["oli_01.m4a", ...] }. L'IP de la box = l'IP source de la requête (client_address), pas besoin que la box l'envoie.
  • À la réception : calcule le delta = (fichiers du staging) (have). Pour chaque fichier manquant (audio + .bin dont la pochette), POST http://<ip>:8080/put?name=podcasts/<f> (corps = octets du fichier). Puis pousse toujours manifest.json en dernier (il reflète le staging courant).
  • Journalise (/var/log ou journald) : ip, nb fichiers poussés, erreurs.
  • Stdlib only (http.server + urllib pour le push) — pas de dépendance.

Box — main/net/box_register.c / .h

  • void lisael_box_register(void); : lit /sdcard/podcasts/*.m4a|*.mp3 (basenames) → construit un JSON {id, have:[...]}POST http://192.168.0.120:9555/register via esp_http_client (HTTP simple, court timeout, non bloquant pour l'UI). Pas besoin d'envoyer l'IP (Tower lit la source). Échec → silencieux (best-effort).
  • Appelé depuis app_main.c net_task, dans le chemin connecté (après lisael_file_server_start()), une fois.

Données

  • Register (box→Tower) : {id:string, have:[string]} (l'IP = source de la requête).
  • Staging Tower : /home/clems/lisael-content/podcasts/{<key>_<NN>.<ext>, <key>.bin, manifest.json} — miroir exact de ce que la box doit avoir sous /sdcard/podcasts/.
  • Push (Tower→box) : POST :8080/put?name=podcasts/<basename> (handler existant, crée /sdcard/podcasts/ au besoin).

Gestion d'erreurs / robustesse

  • Box éteinte → pas de /register → pas de push ; rattrapé au prochain boot.
  • RSS/HTTPS RF en échec pendant le refresh → garder l'ancien staging (écriture atomique), réessayer la semaine suivante.
  • Push interrompu (box se déconnecte) → état partiel sur la box ; le prochain /register renverra un have réduit → Tower repousse le manquant (delta auto-réparant).
  • Tower indisponible au boot box → /register échoue silencieusement, la box fonctionne avec son contenu actuel.

Sécurité

LAN domestique : pas d'auth (comme le file-server :8080). Le serveur :9555 n'accepte que /register ; le push ne touche que /sdcard/podcasts/.

Hors-scope v1

  • Rafraîchir l'écran Histoires sans reboot (le manifeste est lu à l'ouverture ; les nouveaux épisodes apparaissent au prochain boot).
  • Multi-box (l'id permet d'évoluer), auth, contenu Lire/webradios auto.

Tests (acceptation)

  1. lisael_refresh.py à la main sur Tower → staging peuplé (épisodes + .bin + manifest v2 cohérent).
  2. lisael-content.service up ; curl -XPOST :9555/register -d '{...,"have":[]}' depuis le Mac → Tower pousse tout vers une box de test (ou un faux file-server).
  3. Box réelle : reboot → elle s'enregistre (log Tower montre son ip + un delta) → après le push, /sdcard/podcasts/ contient les nouveaux épisodes (vérif via le prochain boot : ils apparaissent dans Histoires).
  4. Re-boot sans nouvel épisode → have complet → delta vide → aucun push.
  5. Couper Tower → boot box → pas d'erreur, contenu inchangé.