# DSL Blockly honnête & couplé hardware — Design

**Date** : 2026-06-14
**Statut** : design approuvé (hybride : honnête + roadmap ; priorités binding carte / média SD / transitions réelles)
**Périmètre** : `frontend-v3/packages/shared/blockly`, `frontend-v3/apps/atelier`, `tools/zacus-gateway`, `tools/scenario`

## 1. Problème

Le DSL Blockly actuel (64 blocs, source unique `frontend-v3/packages/shared/blockly/zacus_blocks.mjs`) est ~70 % **fantôme** : la plupart des blocs sont validés par le gateway puis **ignorés par le firmware**. Le firmware Freenove master sait réellement faire :
- **Énigmes** : `qr` + `sound` uniquement (`puzzle_binding.h` enum `PB_NONE/PB_QR/PB_SOUND`) ;
- **Écran** : step-extra `scene` (title≤47 / subtitle≤63 / symbol≤15 / effect∈pulse|glitch|gyro|none) ;
- **Média** : WAV + MP3 par **chemin** (`media_manager`, `POST /game/media/play {path}`), résolu sur `/sdcard/...` ou `/littlefs/...` ;
- **Relais ESP-NOW** : IR chunké (`/game/scenario/relay {peers,ir}`) + canal texte libre ≤200 o (sans consommateur structuré) ;
- **Voix** : déléguée au voice-bridge :8200 ;
- **PAS de moteur de transitions on-device** : le firmware est piloté pas-à-pas par `POST /game/step {step_id}` ; la logique vit côté gateway/web.

Sans support firmware : servo, relais, buzzer, NFC, morse/radio/LED/keypad, cartes M5/BOX-3/PLIP, couche GM. Non exposées en blocs alors que réelles : **binding à une carte précise**, **chemin média SD réel**, **transitions `button/serial/audio_done/unlock/espnow`**, **branchement sur l'état des énigmes** (`puzzle_state`).

## 2. Intention

Rendre le DSL **honnête** (refléter ce que la carte fait) et **réellement couplé au hardware**, en gardant les blocs non encore supportés comme **roadmap balisée** (hybride). Priorités : binding carte, média SD réel, transitions réelles.

## 3. Architecture cible

### 3.1 Deux voies de blocs
- **✅ RÉELLE** (exécutée on-device) : `sceneStart/End/Goto/Branch`, `scene`, `puzzleQR`, `puzzleSound`, média réel, relais ESP-NOW, `npcSay`/`npcIntentMatch`, transitions réelles.
- **🔬 HARDWARE FUTUR** (validée, non exécutée) : servo, relais, buzzer, NFC, `puzzleLED/Radio/Morse/NFC/Coffre`, M5/BOX-3/PLIP, GM. Catégorie(s) Blockly distincte(s), couleur/badge « non câblé », et un avertissement non bloquant à la compilation (warning, pas erreur).

La distinction vit dans `zacus_blocks.mjs` : un set `ZACUS_REAL_KINDS` (firmware-backed) ; `ZACUS_TOOLBOX_CATEGORIES` réorganisé pour grouper réel vs futur. La toolbox de l'atelier (`apps/atelier/src/components/toolbox.ts`) est générée depuis ces catégories — donc se met à jour automatiquement.

### 3.2 Binding carte
- Cartes connues : `master`, `box3`, `plip`, `p7` (cf. `tools/zacus-gateway/boards.yaml`).
- **Champ `board`** (défaut `master`) sur les blocs énigme (`puzzleQR`, `puzzleSound`) et un bloc contexte **`hwOnBoard`** (« ▸ sur la carte X { … } », c-block) qui cible les actions/cues qu'il contient.
- Compilation : le `board` est porté sur le step (`step.board`) et/ou l'action (`action.board`). Le gateway route :
  - énigme/scénario d'une carte ≠ master → `POST /game/scenario/relay {peers:[board], ir}` ;
  - cue média → `POST /game/media/play` **sur l'IP/relais de la bonne carte**.
- `board` validé contre `boards.yaml` (gateway) ; carte inconnue → erreur de compile claire.

### 3.3 Média réel (SD)
- Bloc **`mediaPlay`** : `path` (chemin SD réel, ex. `zacus/<board>/<file>.wav`, piochable dans le pool stagé P4) + `board` (hérité de `hwOnBoard` si présent) + `loop?`.
- Remplace l'usage opaque de `hwSoundPlay`(`asset`). Compile → action `sound_play{path,board}`.
- Le gateway expose le routage : action `sound_play` d'un step joué → `POST http://<ip(board)>/game/media/play {path}` (résolution IP/relais via `boards.yaml`). Cohérent avec le staging P4 (`/sdcard/zacus/{board}/`).

### 3.4 Transitions réelles
- Nouveaux blocs « **Quand …** » (hat/stack attachés à un step) produisant une `RuntimeTransition` avec le vrai `event_type` :
  - `onButton(button)` → `event_type: button`, `event_name: <button>` ;
  - `onAudioDone()` → `audio_done` ;
  - `onUnlock(source)` → `unlock` ;
  - `onSerial(token)` → `serial` ;
  - `onEspnow(command)` → `espnow` ;
  - `onTimer(seconds)` → `timer` (`after_ms`) — existe déjà via `logicTimer`, à aligner ;
  - `onPuzzleSolved(puzzleId)` → branchement sur `puzzle_state` (résolu) ; `onCodeAssembled(code)` → idem code assemblé.
- Chaque bloc « Quand » porte une cible `target` (step id). Compilation dans `blocks_to_runtime3.py` : émettre la transition avec `event_type`/`event_name`/`target_step_id` corrects (aujourd'hui seuls `action`/`timer` sortent). `runtime3_common.py::EVENT_TYPES` déjà = `{button,serial,timer,audio_done,unlock,espnow,action}` → pas d'extension du schéma, juste de l'émission.

### 3.5 Contrat executor annexe (roadmap, doc seulement)
- Spécifier un **schéma CMD ESP-NOW structuré** (JSON compact ≤200 o) remplaçant le texte libre : `{op, args}` (ex. `{"op":"play","args":{"path":"…"}}`, `{"op":"led","args":{"pattern":"…"}}`). Documente le contrat (`docs/` ou spec firmware) pour que les cartes annexes (M5/BOX-3/PLIP) le décodent **plus tard** ; aucun firmware annexe écrit ici.

## 4. Composants & fichiers

| Fichier | Changement |
|---|---|
| `frontend-v3/packages/shared/blockly/zacus_blocks.mjs` | `ZACUS_REAL_KINDS` ; champ `board` sur puzzles ; blocs `hwOnBoard`, `mediaPlay`, `onButton/onAudioDone/onUnlock/onSerial/onEspnow/onPuzzleSolved/onCodeAssembled` ; réorg `ZACUS_TOOLBOX_CATEGORIES` (réel vs futur) |
| `frontend-v3/apps/atelier/src/components/toolbox.ts` | (auto depuis catégories) — vérifier le rendu 2 voies |
| `frontend-v3/apps/atelier/src/lib/yaml-export.ts` | sérialiser les nouveaux params (`board`, `path`, event targets) |
| `tools/zacus-gateway/blocks_to_runtime3.py` | compiler `board`, `mediaPlay`→`sound_play{path,board}`, blocs `onX`→transitions `event_type` réelles |
| `tools/scenario/runtime3_common.py` | validation `board`∈boards, `sound_play.path`, warning « bloc hardware-futur » |
| `tools/zacus-gateway/main.py` | routage : `sound_play`/relais vers la bonne carte (IP/relais via `boards.yaml`) ; endpoint de « run d'un step » si nécessaire |
| `docs/…/esp-now-cmd-contract.md` | contrat CMD structuré (roadmap) |

## 5. Découpage d'implémentation (phases)
- **D1** — Réorg honnête : `ZACUS_REAL_KINDS` + catégories toolbox 2 voies + badge/warning compile « hardware futur ». Web + compile, risque nul (aucune sémantique cassée). Régénérer le catalogue hub (`tools/dev/sync_blockly_catalog.sh` si présent).
- **D2** — Binding carte : champ `board` + bloc `hwOnBoard` + compile (`step.board`/`action.board`) + validation `boards.yaml` + routage gateway relais.
- **D3** — Média réel SD : bloc `mediaPlay(path,board,loop)` + compile `sound_play{path,board}` + routage `/game/media/play` sur la bonne carte.
- **D4** — Transitions réelles : blocs `onX` → `RuntimeTransition` avec `event_type` réels + branchement `puzzle_state`.
- **D5** — Doc contrat executor annexe ESP-NOW.

## 6. Tests & risques
- **Pipeline** : `make content-checks` / `make runtime3-*`, `python3 -m unittest discover -s tests`, et un round-trip blocs→IR validé (compile + validators) à chaque phase.
- **Frontend** : `tsc --noEmit` + `vite build` verts (toolbox).
- **Parité TS↔PY** : `yaml-export.ts` (serialize) et `blocks_to_runtime3.py` (compile) doivent rester en miroir — tout nouveau param sérialisé côté TS doit être lu côté PY.
- **E2E hardware** : routage gateway→carte (relais, media/play) testable seulement avec les cartes — hardware-gated ; sinon stub/contrat.
- **Risque principal** : la double implémentation TS (sérialisation/preview) + Python (compile/validation) — un champ ajouté d'un seul côté = silencieusement perdu. Chaque phase ajoute le champ des DEUX côtés + un test de parité.

## 7. Hors périmètre
- Profils de groupe (TECH/NON_TECH/MIXED/BOTH) — écartés pour cette itération.
- Écriture de firmware annexe (M5/BOX-3/PLIP) — seulement le contrat documenté (D5).
- Implémentation firmware des énigmes manquantes (morse/NFC/…) — restent « hardware futur ».
- Le graphe spatial FlowGraph (déjà fait, branche séparée).
