notify_gateway_scene() + hints puzzle_start lived only in npc_engine_set_step(), which had NO callers — the runtime->gateway sync was dead code, so scripted incoming calls would never fire in a real game. POST /game/step goes through game_endpoint_apply_step(), which now resolves the step's scene_id (new field on scene_binding, parsed straight from the IR) and calls the new npc_engine_set_scene_by_id(). Hardware-validated: STEP_WARNING -> scene 3 -> gateway notify -> auto-ring Professeur Zacus on the PLIP.
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 :
- NVS — init (efface + réinit si pages pleines / nouvelle version).
- Écran —
display_ui_init()(splash tôt, non fatal). Si OK : enregistredisplay_ui_camera_framecomme callback de preview QR, puisbuttons_input_init()(5 directions). - Wi-Fi — lit les creds NVS (namespace
wifi, clésssid/pwd). Si présents : STA (attenteGOT_IP, 8 retries, timeout 20 s). Sinon, ou échec : AP ouvert de secourszacus-setup(IP192.168.4.1). - mDNS — uniquement en STA : publie
zacus-master.localet le service_zacus._tcp:80(TXTpath=/voice/hook). Sauté en AP-fallback. - OTA server —
ota_server_init()(:80). Sur succès, greffe sur le même httpd :voice_hook_endpoint_init()puisgame_endpoint_init()(qui monte aussiscenario_mesh/ ESP-NOW), seed du registre de peers relais depuis NVS (namespacepeers), etgame_endpoint_set_puzzle_state(&s_pstate). - LittleFS — monte la partition
storagesur/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 dugroup_profiledepuis NVS (namespacezacus, défautMIXED) ;voice_dispatcher_init();puzzle_state_init()+local_puzzles_init();mic_broker_init()sur les pins micro Media Kit (3/14/46, 16 kHz) avantvoice_pipeline_init()(le pipeline no-op alors avecESP_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.
ota_server_mark_valid()— valide l'image (désamorce l'auto-rollback) une fois les sous-systèmes montés.- 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.txt→game_endpoint_apply_step). Toutes les 60 s : heartbeat +media_manager_updatenpc_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 modeMIC_P1_SOUNDet route les trames PCM16 verssound_puzzle, qui détecte les notes et les valide viamelody_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_puzzleinitialise la caméra (QVGA 320×240 niveaux de gris), décode avec quirc, et valide l'ordre des codes viaseq_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, D0–D7 = 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.cles é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éra —
lv_canvasen 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.pngoptionnel,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.hmentionne 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.