Files
clement 4ac3981845
CI / platformio (pull_request) Failing after 4m3s
CI / platformio (push) Failing after 14m44s
fix(npc): gateway scene notify non-blocking
The scene->gateway POST ran synchronously (2 s timeout) on the scene
transition, so an offline gateway lagged EVERY scene change by 2 s.
Fire it on a detached one-shot task instead, so the game's scene
transitions never wait on the network. Frees the heap scene copy and
self-deletes. Builds clean.
2026-06-18 15:17:31 +02:00
..

idf_zacus/ — firmware master Zacus (ESP-IDF)

Firmware master du jeu Le Mystère du Professeur Zacus, pour la carte Freenove FNK0102H — ESP32-S3 WROOM N16R8 (16 MB flash, 8 MB PSRAM octale).

Ce n'est plus une slice de migration : c'est le master complet. Le firmware réunit aujourd'hui :

  • Voix NPC — capture I2S + esp-sr (AFE + WakeNet9, mot-clé placeholder wn9_hiesp / « Hi ESP »), pont WebSocket vers le voice-bridge MacStudio pour la STT et le retour TTS, plus un hook REST PLIP (téléphone rétro).
  • Énigmes locales caméra et micro résolues sur l'appareil (P1 son / P3 QR), agrégées en un code de sortie.
  • Écran LVGL (ST7796 320×480) : vue statut, vue scène, viewfinder caméra, shell Workbench, navigation 5 directions.
  • Surface REST de jeu (pilotage scénario, étapes, profil de groupe, relais ESP-NOW) et serveur OTA double-banque.

Le firmware Arduino historique (../ui_freenove_allinone/, PlatformIO) reste la source de vérité matérielle (pin-maps, drivers de référence), mais l'IDF est désormais le firmware master complet.

Prérequis, build, flash

ESP-IDF v5.4 installé sous ~/esp/esp-idf/.

QUIRK de cette machine — l'environnement Python par défaut d'IDF échoue ici. Il faut forcer le venv py3.11 avant de sourcer export.sh :

export IDF_PYTHON_ENV_PATH=$HOME/.espressif/python_env/idf5.4_py3.11_env
source ~/esp/esp-idf/export.sh

Build :

idf.py set-target esp32s3
idf.py build

Flash + monitor (le port de cette machine est fixe) :

idf.py -p /dev/cu.usbmodem5AB90753301 flash monitor

Sortie du monitor : Ctrl-].

Au premier idf.py build, le component manager clone LovyanGFX (tag 1.2.21, épinglé par SHA) depuis GitHub et récupère LVGL ~8.4.0, esp-sr, esp-dsp, mdns, esp_websocket_client et littlefs dans managed_components/. Les modèles esp-sr sont flashés automatiquement (srmodels.bin) dans la partition model.

Carte des composants

Tous les composants vivent sous components/ et sont des composants ESP-IDF standard (chacun avec son include/).

Composant Rôle
ota_server Serveur HTTP :80 : OTA double-banque (POST /ota, POST /ota/rollback), watchdog d'auto-rollback, GET /version / /status. Détient l'instance esp_http_server partagée par les autres endpoints.
game_endpoint Surface REST de jeu greffée sur le httpd d'ota_server : profil de groupe hints, pilotage scénario, étapes (/game/step), puzzle_state, relais ESP-NOW. Voir son README.md.
voice_pipeline Capture I2S + esp-sr (AFE AFE_TYPE_SR, WakeNet9), machine d'états idle/listening/speaking/muted, pont WebSocket vers le voice-bridge (STT/TTS), lecture TTS sur DAC. Inclut voice_dispatcher (routage STT → npc_engine, fast-path mots-clés).
voice_hook_endpoint Pont REST POST /voice/hook depuis le PLIP (téléphone Si3210) : off-hook arme la voix directement (bypass wake-word), on-hook ferme la session. Greffé sur le httpd d'ota_server.
npc_engine Port C du moteur de décision NPC Arduino : table de cues, humeur, déclenchement de cues via media_manager.
hints_client Client HTTP du moteur de hints (POST /hints/ask, /hints/puzzle_start, /hints/attempt_failed), profil de groupe (TECH / NON_TECH / MIXED / BOTH).
media_manager Port C du MediaManager Arduino : catalogue, play/stop, enregistrement WAV. Décodage MP3 et capture micro stubés ici (le micro réel passe par mic_broker).
local_puzzles Énigmes locales caméra/micro et leur câblage vers puzzle_state : qr_puzzle, sound_puzzle, mic_broker, validateurs seq_validator / melody_validator, pin-map board_pins_mediakit.h.
puzzle_state Agrégation master des énigmes résolues et assemblage du code de sortie (jusqu'à 8 énigmes, fragments → chiffres décimaux).
display_ui Écran LVGL 8.4 sur LovyanGFX : vues statut/scène, viewfinder caméra, effets, shell Workbench, browser de fichiers, intro cracktro ; boutons 5 directions (buttons_input).

Séquence de boot (main/main.c)

app_main exécute, dans l'ordre :

  1. NVS — init (efface + réinit si pages pleines / nouvelle version).
  2. Écrandisplay_ui_init() (splash tôt, non fatal). Si OK : enregistre display_ui_camera_frame comme callback de preview QR, puis buttons_input_init() (5 directions).
  3. Wi-Fi — lit les creds NVS (namespace wifi, clés ssid/pwd). Si présents : STA (attente GOT_IP, 8 retries, timeout 20 s). Sinon, ou échec : AP ouvert de secours zacus-setup (IP 192.168.4.1).
  4. mDNS — uniquement en STA : publie zacus-master.local et le service _zacus._tcp:80 (TXT path=/voice/hook). Sauté en AP-fallback.
  5. OTA serverota_server_init() (:80). Sur succès, greffe sur le même httpd : voice_hook_endpoint_init() puis game_endpoint_init() (qui monte aussi scenario_mesh / ESP-NOW), seed du registre de peers relais depuis NVS (namespace peers), et game_endpoint_set_puzzle_state(&s_pstate).
  6. LittleFS — monte la partition storage sur /littlefs (reformat si échec) et liste la racine. Puis, sous ce montage :
    • media_manager_init() (catalogues sur LittleFS) + smoke-test de lecture ;
    • npc_engine_init() (table de cues vide à ce stade) ;
    • hints_client_init() vers le backend hints, puis chargement du group_profile depuis NVS (namespace zacus, défaut MIXED) ;
    • voice_dispatcher_init() ;
    • puzzle_state_init() + local_puzzles_init() ;
    • mic_broker_init() sur les pins micro Media Kit (3/14/46, 16 kHz) avant voice_pipeline_init() (le pipeline no-op alors avec ESP_ERR_INVALID_STATE) ;
    • voice_pipeline_init() : wake-word + auto-start capture activés, URL du voice-bridge, lecture TTS activée, pins forcés aux valeurs Media Kit (mic 3/14/46, HP 42/41/1) pour éviter la collision avec les pins caméra.
  7. ota_server_mark_valid() — valide l'image (désamorce l'auto-rollback) une fois les sous-systèmes montés.
  8. Boucle de statut — réveil toutes les 500 ms : rafraîchit l'écran (IP, état wake-word, étape/armé, code assemblé, métadonnées de scène), traite les lancements d'apps du shell (/littlefs/apps/<id>/step.txtgame_endpoint_apply_step). Toutes les 60 s : heartbeat + media_manager_update
    • npc_engine_update.

URLs hardcodées (déplacées en NVS dans un suivi) : hints http://192.168.0.150:8302, voice-bridge WS ws://100.116.92.12:8200/voice/ws.

Énigmes locales (P1 son / P3 QR)

local_puzzles arme deux types d'énigmes et reporte leur fragment dans puzzle_state à la résolution :

  • P1 — son/mélodie (local_puzzles_arm_sound) : mic_broker (propriétaire unique du micro I2S RX) passe en mode MIC_P1_SOUND et route les trames PCM16 vers sound_puzzle, qui détecte les notes et les valide via melody_validator (notes MIDI + tolérance en demi-tons). Le même broker est partagé avec la voix (MIC_NPC_LISTEN) : un seul propriétaire des pins, un consommateur actif à la fois.
  • P3 — QR séquentiel (local_puzzles_arm_qr) : qr_puzzle initialise la caméra (QVGA 320×240 niveaux de gris), décode avec quirc, et valide l'ordre des codes via seq_validator. Les trames sont mirrorées au viewfinder de l'écran via le callback de preview. Teardown caméra asynchrone (réarmement à retenter après ~100 ms).

Pin-map Media Kit (board_pins_mediakit.h)

Freenove FNK0102H Media Kit v1.2, zéro collision entre caméra, micro, HP et écran :

  • Caméra (capteur réel : OV3660) — XCLK 15, SIOD/SIOC 4/5, VSYNC 6, HREF 7, PCLK 13, D0D7 = 11/9/8/10/12/18/17/16.
  • Micro I2S IN — BCLK 3, WS 14, DIN 46.
  • HP I2S OUT (MAX98357A) — BCLK 42, LRC 41, DOUT 1.
  • Écran ST7796 (SPI2/FSPI, 80 MHz) — SCK 47, MOSI 21, DC 45, RST 20, BL 2.

Les défauts du composant voice_pipeline (mic 14/15/22, HP 11/12/13) entrent en collision avec les pins caméra ; main.c les écrase par les valeurs Media Kit ci-dessus. Ne pas réintroduire les défauts.

Validateurs testables sur hôte

seq_validator et melody_validator sont de la logique pure, sans I/O, compilables et testables sur la machine de dev :

make -C idf_zacus/components/local_puzzles/test/host test

Écran (display_ui)

Pile LVGL 8.4 + LovyanGFX (driver ST7796, setAddrWindow / writePixels RGB565). Tâche de rendu dédiée (LovyanGFX n'est pas thread-safe inter-tâches) ; display_ui_set_status copie un snapshot sous mutex.

Vues et fonctions :

  • Vue statut — IP, wake-word, étape/armé, compte d'énigmes résolues, code.
  • Vue scène — titre/sous-titre/code de l'étape, avec effets (pulse / glitch / gyro / none) issus de l'IR de scène.
  • Viewfinder caméralv_canvas en PSRAM, alimenté par les trames QR (grayscale → RGB565, ~5 fps) quand la vue scène est active et le QR armé.
  • Shell Workbench — port d'ui_amiga_shell : tuiles statiques (Statut, Scene, Auto, Lumiere, Fichiers) + tuiles dynamiques d'apps lues dans /littlefs/apps/<id>/ (icon.png optionnel, step.txt = l'étape à armer).
  • Browser de fichiers — drawer LittleFS navigable.
  • Intro cracktro — port fidèle de ui_manager_intro (starfield parallaxe 3 couches, copper bars).

Navigation 5 directions (buttons_input, échelle analogique sur GPIO19, ADC2) : 1=SELECT 2=DOWN 3=MENU 4=LEFT/RIGHT 5=UP. Shell fermé : SELECT / LEFT-RIGHT bascule scène↔statut, MENU ouvre le shell, UP/DOWN règlent la luminosité. Shell ouvert : navigation grille, SELECT lance la tuile.

Surface REST de jeu

Tous les endpoints de jeu partagent l'instance esp_http_server d'ota_server (port 80, pas de second socket). Détail des routes (/game/group_profile, /game/step, /game/puzzle_state, /game/scenario, /game/scenario/relay…) et de la persistance NVS dans components/game_endpoint/README.md.

Tests hôte

Trois suites de logique pure tournent sur la machine de dev (Unity), sans matériel ni IDF flashé :

Suite Cas Commande
local_puzzles (validateurs séquence + mélodie) 6 make -C idf_zacus/components/local_puzzles/test/host test
puzzle_state (agrégation + assemblage du code) 2 make -C idf_zacus/components/puzzle_state/test/host test
game_endpoint (binding puzzle/scène) 10 make -C idf_zacus/components/game_endpoint/test/host test

Layout flash (partitions.csv)

Cible 16 MB (N16R8) :

Partition Type Taille
nvs data/nvs 24 KB
otadata data/ota 8 KB
phy_init data/phy 4 KB
factory / ota_0 / ota_1 app 3 MB chacune
model data/spiffs 1 MB (modèles esp-sr)
storage data/littlefs 5 MB (assets, scénario, apps)

La partition app est passée de 2 MB à 3 MB (2026-06-10) pour loger LVGL + LovyanGFX + polices + effets/preview. NVS/otadata/phy gardent leurs offsets (les creds Wi-Fi survivent au reflash) ; ota_0/ota_1/model/ storage se décalent, donc le contenu LittleFS est perdu au reflash (reformatage auto ; repousser le scénario via POST /game/scenario).

Limites connues / terrain

  • Décodage QR sur écran LCD émissif : lire un QR affiché à l'écran via la caméra ne fonctionne pas de façon fiable — quirc ne retrouve pas les motifs (contraste insuffisant sur un panneau émissif). Le QR imprimé reste la méthode recommandée pour P3.
  • Note repérée dans le code : le commentaire d'en-tête de qr_puzzle.h mentionne encore « OV2640 » ; le capteur réel de la carte est OV3660 (commentaire à corriger, sans impact fonctionnel sur le chemin grayscale/quirc).
  • Provisioning : creds Wi-Fi, URL hints et URL voice-bridge passent encore partiellement par des constantes / NVS pré-flashé ; le passage tout-NVS au runtime est un suivi.