# LCD Expressions This document describes how LCD expressions are owned, requested, played, and extended. ## Ownership Model LCD expressions are split into semantic intent and low-level playback: - `control_plane` decides when an expression should be requested. - `domain` owns semantic expression IDs and maps them to playback requests. - `platform` owns LCD presentation primitives and LVGL playback. Do not bypass this path from upper layers. The expected flow is: ```text control_plane condition -> domain_runtime_request_expression() -> platform_lcd_request_presentation() -> LCD render task / LVGL ``` `control_plane` must not call `platform_lcd_*` APIs directly. `platform` must not expose domain expression IDs. ## Current Expressions | Domain expression | Trigger owner | Playback kind | Behavior | | --- | --- | --- | --- | | `DOMAIN_EXPRESSION_IDLE` | LCD startup / restore path | `PLATFORM_LCD_PRESENTATION_IDLE` | Shows the default cat idle screen. | | `DOMAIN_EXPRESSION_SERVER_ERROR` | Multimodal server failure events | `PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE` | Plays `server_error` for 5 cycles, then returns to idle. | | `DOMAIN_EXPRESSION_SLOT_REELS` | Slot feature switch worker | `PLATFORM_LCD_PRESENTATION_SLOT_REELS` | Plays the procedural slot reels presentation. | | `DOMAIN_EXPRESSION_WIFI_DISCONNECTED` | Wi-Fi state monitor worker | `PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE` | Loops `wifi_disconnected` while STA is unavailable; saved credentials are retried with backoff, then idle is restored after reconnect. | | `DOMAIN_EXPRESSION_WEALTH_SPIN` | Model-command direction worker | `PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE` | Plays `spin` exactly twice as a fixed intro while the cat stays still; the worker blocks until it completes, then the reveal and motor rotation start together. | | `DOMAIN_EXPRESSION_WEALTH_DIRECTION` | Model-command direction worker | `PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE` | Plays the matched direction's reveal once, then returns to idle. Parametric on `direction_index`; the asset directory is resolved from the domain direction catalog order. | Server error currently triggers only from: - `CONTROL_PLANE_MULTIMODAL_EVENT_TASK_FAILED` - `CONTROL_PLANE_MULTIMODAL_EVENT_OUTPUT_ERROR` Server error expression requests are suppressed while Wi-Fi STA is disconnected; the Wi-Fi disconnected expression owns that state because server reachability is not meaningful without Wi-Fi connectivity. Transport disconnect/error events are not server error expressions. ## Asset Management Expression assets live under: ```text components/platform/assets/lvgl_animations// ``` The platform build copies all `.bin` files under this tree into the `gifpack` LittleFS partition while preserving relative directories. Runtime LVGL paths are resolved as: ```text A:/littlefs//.bin ``` Asset rules: - Use LVGL `.bin` images compatible with the active LCD color configuration. - RGB565 LZ4 compressed assets are supported by the existing decoder path when `LV_BIN_DECODER_RAM_LOAD` and `LV_USE_LZ4` are enabled. - File discovery is lexicographic. Name files with zero-padded numeric suffixes. - If an expression needs a non-linear playback sequence, define `frame_order` in the domain catalog instead of duplicating files. - Keep `asset_dir` names short enough for `PLATFORM_LCD_PRESENTATION_ASSET_DIR_MAX`. The current server error expression uses: ```text components/platform/assets/lvgl_animations/server_error/ ``` Sorted source frames: ```text TQ_Cat_Server_Error_0000.bin TQ_Cat_Server_Error_0001.bin TQ_Cat_Server_Error_0002.bin ``` Playback order is declared in the domain catalog as: ```c {0, 1, 2, 1, 0} ``` The Wi-Fi disconnected expression uses: ```text components/platform/assets/lvgl_animations/wifi_disconnected/ ``` Sorted source frames: ```text TQ_Cat_wifi_disconnected_0000.bin TQ_Cat_wifi_disconnected_0001.bin TQ_Cat_wifi_disconnected_0002.bin TQ_Cat_wifi_disconnected_0003.bin ``` Playback order is declared in the domain catalog as: ```c {0, 1, 2, 3, 2, 1, 0} ``` The wealth-direction reveal uses one shared spin intro plus one body per direction: ```text components/platform/assets/lvgl_animations/spin/ (fixed two-cycle spin intro) components/platform/assets/lvgl_animations/south/ components/platform/assets/lvgl_animations/southwest/ components/platform/assets/lvgl_animations/west/ components/platform/assets/lvgl_animations/northwest/ components/platform/assets/lvgl_animations/north/ components/platform/assets/lvgl_animations/northeast/ components/platform/assets/lvgl_animations/east/ components/platform/assets/lvgl_animations/southeast/ ``` These play all frames in sorted order (`frame_order = NULL`). The direction body asset is resolved from `direction_index` via `s_wealth_direction_asset_dirs[]` in `components/domain/src/runtime/domain_runtime_adapters.c`, whose order must match the direction catalog (`components/domain/src/direction/domain_direction_catalog.c`). ## Requesting Expressions Use `domain_runtime_request_expression()` from orchestration code: ```c const domain_expression_request_t request = { .id = DOMAIN_EXPRESSION_SERVER_ERROR, .trigger = DOMAIN_EXPRESSION_TRIGGER_SERVER_TASK_FAILED, }; (void)domain_runtime_request_expression(&request); ``` Trigger IDs should describe why the expression was requested. They are useful for logs and future policy decisions, but the playback mapping is still owned by the domain catalog. Slot reel playback uses the same expression path: `domain_runtime_request_expression()` with `DOMAIN_EXPRESSION_SLOT_REELS`. ## Playback Scheduling The platform LCD presentation scheduler is non-blocking: - Requests are copied at API entry. - LVGL objects are created, deleted, and mutated only inside the LCD render task. - Same active or pending requests are coalesced. - Higher-priority interrupting requests replace the active presentation. - Lower-priority requests are ignored while a higher-priority presentation is active or pending. - Asset-backed expressions are cached after their frames are decoded. The LCD task preheats the `spin` asset after startup and preheats the selected wealth direction reveal while the fixed spin intro is playing. - Preheat requests do not change the active display. They decode incrementally during the render loop, and playback finishes any remaining decode work synchronously if a reveal is requested before preheat completes. Current priorities: | Presentation | Priority | Interrupts current | | --- | --- | --- | | Wi-Fi disconnected / idle restore | `120` | Idle restore interrupts Wi-Fi disconnected. | | Wealth spin / wealth-direction reveal | `110` | Yes (spin interrupts current; the reveal follows after the spin completes). | | Server error | `100` | Yes | | Slot reels | `20` | No | The wealth spin/reveal pair is requested by the model-command direction worker (`control_plane_model_command_task`): the spin plays exactly twice as a blocking intro while the selected reveal is preheated, then the reveal is requested and the motor rotation starts together. At priority `110` a Wi-Fi-disconnected screen (`120`) still wins, so the reveal is suppressed while STA is down — consistent with the server-error suppression rule above. ## Adding a New Expression Follow this checklist for every new expression: 1. Add a semantic ID to `domain_expression_id_t` in `components/domain/include/domain.h`. 2. Add a trigger reason to `domain_expression_trigger_t` if the new expression needs a new trigger source. 3. Add a catalog entry in `components/domain/src/runtime/domain_runtime_adapters.c`. 4. For asset-backed expressions, place `.bin` frames under `components/platform/assets/lvgl_animations//`. 5. If the expression needs a custom frame order, define a static `uint8_t` frame-order array in the domain adapter and reference it from the catalog. 6. Add or update the `control_plane` rule that creates `domain_expression_request_t`. 7. Build with ESP-IDF v5.5.3 and confirm the LittleFS image includes the asset directory. Do not add one-off APIs such as `domain_runtime_show_happy()` or `platform_lcd_show_error()`. Keep extension through the expression catalog and the unified presentation request path. ## Validation Checklist Before merging expression changes: - Run `idf.py build` through the repository-approved ESP-IDF activation path. - Confirm new asset files appear in the `gifpack` build output. - Verify `control_plane` does not include `platform.h` or call `platform_lcd_*`. - Verify platform code does not refer to `DOMAIN_EXPRESSION_*`. - For finite asset sequences, verify completion returns to idle when `return_to_idle` is set. - For interrupting expressions, verify the active lower-priority presentation is cancelled cleanly. - For wealth-direction playback, verify default -> spin and spin -> reveal transitions use cached/preheated assets without visible transition stalls.