# display_ui

Écran ST7796 (320×480, paysage) du master Freenove, piloté via **LovyanGFX** +
**LVGL 8.4**. Port fidèle de l'UI Arduino d'origine (`ui_freenove_allinone`).

Tous les appels LVGL et LovyanGFX s'exécutent exclusivement sur la
`display_task` ; `display_ui_set_status` copie l'état sous mutex et lève un flag
dirty que la tâche replie dans les labels.

## Pile

- **HAL panneau** — `Panel_ST7796` sur `Bus_SPI` (SPI2_HOST/FSPI, 80 MHz
  écriture / 20 MHz lecture). Configuration **identique** à l'UI Arduino
  d'origine (`FreenoveLgfxDevice` dans `ui_freenove_allinone`) : pins
  SCK=47/MOSI=21/DC=45/RST=20/BL=2, rotation 1, inversion on, ordre RGB.
- **Rétroéclairage PWM** — LEDC sur `LEDC_TIMER_1` / `LEDC_CHANNEL_1`. Le
  `LEDC_TIMER_0` / `LEDC_CHANNEL_0` est réservé au **XCLK de la caméra**
  (`qr_puzzle`). Duty exposé 0-100 % via `display_ui_set_brightness`.
- **Vue statut** + **vue scène** — la vue scène affiche le step courant et le
  code assemblé, avec un effet (**pulse** = respiration d'opacité, **glitch** =
  jitter + scintillement du titre, **gyro** = anneau-gyrophare rotatif) et un
  **viewfinder caméra** : `lv_canvas` PSRAM nourri par les trames grayscale du
  scan QR, converties en **RGB565** (~5 fps), visible seulement quand l'énigme
  QR est armée.
- **Boutons 5 directions** — échelle résistive sur ADC (GPIO19 → ADC2_CH8),
  seuils en mV repris de l'original (`{0, 447, 730, 1008, 1307, 1659}`, plancher
  « relâché » 2800 mV, décodage par milieu entre seuils adjacents).
- **Shell Workbench** — tuiles builtin **Statut / Scene / Auto / Lumiere /
  Fichiers**, plus des **apps dynamiques** chargées depuis
  `/littlefs/apps/<id>/` (`icon.png` optionnel + `step.txt` = action de l'app).
- **Browser de fichiers** — drawer parcourant `/littlefs`.
- **Intro cracktro** — port fidèle de la phase A d'origine : starfield parallaxe
  3 couches en **Q8.8**, copper bars, scrolltext en onde sinus, logo avec ombre
  portée.

## API publique (`display_ui.h`)

- `display_ui_init()` — init panneau + splash, spawn la tâche de refresh.
- `display_ui_set_status(s)` — copie un snapshot d'état et demande un redraw
  async (thread-safe ; NULL = no-op).
- `display_ui_set_brightness(pct)` — duty 0-100 % (défaut 100).
- `display_ui_camera_frame(gray, w, h)` — pousse une trame caméra pour le
  viewfinder (câblé comme preview callback de `qr_puzzle`) ; non-QVGA ignorée.
- `display_ui_handle_key(key)` — traite une touche 5 directions (1=SELECT
  2=DOWN 3=MENU 4=LEFT/RIGHT 5=UP).
- `display_ui_take_pending_launch(id_out, cap)` — récupère (one-shot) une
  demande de lancement d'app du shell dynamique.
