# ESP-NOW commandes & événements — design v1 (2026-06-11)

## Problème

Les 4 blocs Blockly ESP-NOW compilent vers des actions IR
(`espnow_register_peer`, `espnow_send`, `espnow_broadcast`, `espnow_wait`)
que **personne n'exécute** : le gateway ne sait que relayer des IR complets
(`/game/scenario/relay`), et le firmware master ignore les actions des steps.
La pile radio existe pourtant (`ESP32_ZACUS/lib/scenario_mesh/`, partagée
master/annexes) mais ne transporte que des chunks d'IR.

## Décisions de cadrage (validées)

1. **Rôle** : commandes ET événements — le scénario commande les annexes et
   attend leurs événements.
2. **Orchestration** : **master autonome** — le firmware Freenove
   (`idf_zacus`) exécute lui-même les actions espnow des steps ; pas de
   dépendance au gateway pendant la partie.
3. **Périmètre** : bout-en-bout avec la BOX-3 (`box3_voice`) comme annexe.
4. **Sémantique `espnow_wait`** : action **bloquante** (événement ou timeout,
   puis le déroulé continue ; timeout → log, pas d'échec de step).
5. **Côté annexe** : handler **ouvert** — la commande est une chaîne opaque
   passée à un callback enregistrable ; l'annexe répond l'événement de son
   choix. Démo livrée : `ping` → `pong`, branchement optionnel `stim:*`.

## Architecture (approche A retenue)

### 1. Protocole — `lib/scenario_mesh` (composant partagé)

- Les frames gagnent un **type** : `IR_CHUNK` (existant), `CMD`, `EVT`.
- Payload CMD/EVT : chaîne UTF-8 opaque ≤ 200 octets.
- API additive :
  - `esp_err_t scenario_mesh_send_text(const uint8_t mac[6], uint8_t kind, const char *text);`
  - `void scenario_mesh_set_text_cb(scenario_mesh_text_cb_t cb);`
    (callback : kind, MAC source, texte ; dispatché hors contexte WiFi via la
    queue/tâche existante).
- Pas d'ack applicatif en v1 : statut d'envoi radio natif + confirmation
  logique par `wait`.

### 2. Master — `idf_zacus`

- À l'application d'un step, exécution séquentielle des actions `espnow_*`
  (les autres kinds restent ignorés) :
  - `espnow_register_peer {alias, mac}` → `scenario_mesh_register_peer`.
  - `espnow_send {peer, command}` → résolution alias → frame CMD.
  - `espnow_broadcast {command}` → frame CMD vers FF:FF:FF:FF:FF:FF.
  - `espnow_wait {command, timeout_ms}` → sémaphore + timeout, débloqué par
    un EVT dont le texte égale `command` ; timeout → `ESP_LOGW` et on
    continue.
- Endpoint debug : `POST /game/espnow/cmd {peer|broadcast, command}` pour
  tester le tuyau sans scénario.

### 3. Annexe — `box3_voice`

- Enregistre le callback texte : sur CMD reçue, passe la chaîne au handler
  ouvert du firmware.
- Handler de démo : `ping` → répond EVT `pong` au MAC source ; préfixe
  `stim:` → déclenche le générateur de stimulus existant (`melody`, `qr`)
  puis répond `done:<cmd>` (automatisera les tests physiques P1/P3).

### 4. Tests

- Export Blockly→IR : déjà couvert (`test_blocks_export_espnow.py`).
- Master seul : `POST /game/espnow/cmd` vers un peer absent → statut radio
  loggé ; step avec `espnow_wait` sans annexe → timeout propre.
- Bout-en-bout 2 cartes : step `espnow_send box3 "ping"` +
  `espnow_wait "pong" 10s` → logs série des deux côtés, wait débloqué.

## Hors scope v1

Retries/ack applicatif, chiffrement, événements→transitions IR, file
d'événements persistante, exécution des autres kinds d'actions par le master.

## Découpage

1. **Commit 1** : protocole CMD/EVT + exécuteur master + endpoint debug,
   testé sur la Freenove seule (timeout, statuts radio).
2. **Commit 2** : handler BOX-3 (`ping`/`pong`, `stim:*`) + test 2 cartes.
