diff --git a/docs/development/ui-core.md b/docs/development/ui-core.md new file mode 100644 index 00000000..380a70b6 --- /dev/null +++ b/docs/development/ui-core.md @@ -0,0 +1,158 @@ +# UI Core + frontends β€” design + +Status: πŸ“‹ draft for review (2026-09-24). Branch: `wio-tracker-l2`. + +## Why + +The Wio Tracker L2 (320Γ—240 colour touch LCD, no joystick) gets a new LVGL 9 +interface. Rather than a second, parallel copy of everything Solo does, the +application logic moves out of `ui-new` into a hardware-independent **UI Core** +that every frontend shares: + +| Frontend | Devices | Toolkit | Input | +|---|---|---|---| +| `ui-lvgl` (rich) | Wio Tracker L2 first; later other ESP32-S3 colour boards | LVGL 9 | pointer (touch) + focus (buttons, CardKB) | +| `ui-new` (lite) | OLED / e-ink / nRF52 (Wio L1, T-Echo Lite, Heltec, …) | `DisplayDriver` | focus (joystick, buttons, CardKB) | + +A feature written once in the Core (live share, locator, unread tracking, a new +setting) then shows up on both. `ui-new` is not rewritten: it is re-pointed at +the Core and stays the lite frontend. + +Non-goals: changing the mesh protocol, `MyMesh`, persistence formats +(`NodePrefs` layout, `/scopes1`, trail files), or the companion app protocol. + +## Current state + +`ui-new` is 18.5k lines. `UITask` implements `AbstractUITask` (the interface +`MyMesh` calls into) *and* hosts the screens, so logic and drawing are +interleaved. Classified: + +**Models β€” already UI-free, move as-is** +- `MessageHistory.h` β€” channel (48) / DM (32) rings, unread counters + overflow flags. +- `Trail.h` / `TrailStore`, waypoints (`WaypointsView` storage half), `ScopeList.h`. + +**Engines β€” logic living inside `UITask.cpp` / `UITask.h`** +- Unread tracking β€” DM unread table (`_dm_unread_table`, `newMsg`, `addDMMsg`, `reconcileDMUnread`), room unread. +- Notifications β€” `showAlert`, `notify`, `SoundNotifier`, LED (`userLedHandler`), wake-on-message (`checkDisplayOn`, auto-off). +- Live share β€” session timer, movement/heartbeat gate, `sendLocationShare`, scope guard, `onSharedLocation`/live-track expiry. +- Locator β€” geofence state machine, proximity beeper (`evaluateLocator`, `fireLocator`, targets). +- Trail β€” sampling, auto-pause, low-battery auto-save. +- Course over ground β€” `pushCogFix`, `currentCourse`, `currentLocation`. +- Clock tools β€” alarm / countdown / ring (`evaluateAlarm`, `tickClockTools`, `fireClockAlert`). +- Ping β€” `startPing`, `handlePingResult`. +- Device controls β€” GPS on/off, GPIO (`setGpioMode`, bot GPIO), buzzer mode/volume, brightness, radio apply (`applyTxPower`, `applyApc`, `applyRadioParams`, …). +- Bot hooks β€” `botSetGPS`, `botBuzz`, `botSetGPIO`, … + +**Screen-embedded logic (harder to extract)** +- `SettingsScreen.h` (1.3k lines) β€” ~40 items, each with inline get/format/step/apply code. +- `NearbyScreen.h` β€” contact discovery/sorting; `AdminScreen.h` β€” repeater admin session; `BotScreen.h`; `RepeaterScreen.h`; `MessagesScreen.h` (2.6k lines) β€” list ordering, favourites, context-menu actions mixed with rendering. + +**Pure view / widgets β€” stay in `ui-new`** +- Rendering of every screen, `KeyboardWidget`, `PopupMenu`, `AccordionList`, `TabBar`, `icons.h`, `GfxUtils.h`, marquee, badges. + +## Architecture + +``` + MyMesh ──(AbstractUITask callbacks)──► UiCore ──events──► Frontend (ui-new | ui-lvgl) + β–² β”‚ β–² β”‚ + └──────────── mesh actions β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ └──── actions β”€β”€β”€β”€β”€β”˜ + engines, models, settings schema + Platform services (per board): Sound, DisplayPower, Input sources, Led +``` + +### 1. `UiCore` owns the mesh-facing interface + +`UiCore` implements `AbstractUITask`. `MyMesh` keeps calling exactly the same +methods; they land in the Core, which updates models/engines and emits events. +The frontend no longer implements `AbstractUITask` at all. This is the single +point that makes both frontends receive identical behaviour. + +### 2. Engines + +One class per engine from the inventory above, each with `begin(prefs, …)`, +`loop(now_ms)` and a small public API. No drawing, no `DisplayDriver`, no +screen pointers. Engines that need to show something emit an event +(`AlertRequested{text, ms}`, `LocShareEnded`, `AlarmFired`, …) instead of +calling `showAlert()` directly. + +Constraints (the lite frontend runs on nRF52 with ~215 KB flash / ~65 KB RAM +free): no heap allocation, no RTTI/exceptions, no STL containers, fixed-size +buffers β€” the same rules `ui-new` follows today. Extraction must be roughly +size-neutral on `WioTrackerL1_companion_solo_dual`. + +### 3. Events: Core β†’ frontend + +A small fixed-size queue of tagged events plus coarse dirty flags +(`DIRTY_MESSAGES`, `DIRTY_CONTACTS`, `DIRTY_STATUS`, …). The frontend drains +the queue in its `loop()` and redraws what is dirty. No callbacks into the +frontend from inside mesh processing β€” which also keeps the e-ink busy-wait +pump rule ("never re-enter UI code from the radio path") trivially true. + +### 4. Actions: frontend β†’ Core + +Plain methods on the Core facade: `sendDM`, `sendChannelMsg`, `markRead`, +`setLiveShare`, `setLocatorTarget`, `toggleGps`, `startPing`, … Both frontends +call the same ones, so e.g. "marking a channel read" has one implementation. + +### 5. Declarative settings schema + +Replaces the per-item code in `SettingsScreen`: + +```cpp +struct SettingDef { + const char* id; // stable key, also used for search / CLI + const char* label; + uint8_t section; // Radio / System / Display / … + SettingType type; // Bool, Enum, Int, Text, Action + // accessors over NodePrefs; enum labels; min/max/step + int (*get)(const NodePrefs&); + void (*set)(NodePrefs&, int); + const char* (*label_for)(int v); // Enum/Int formatting + bool (*visible)(const NodePrefs&); // build- or state-dependent rows + bool (*locked)(const NodePrefs&, const char** why); // "Off while repeating" + void (*apply)(UiCore&); // side effect after set (radio, display…) +}; +``` + +`ui-new` renders it as today's accordion list; `ui-lvgl` as switches, dropdowns +and sliders. Build-time gating (`FEAT_*`) stays in the table via `#if`, so a +hidden row costs no flash. + +### 6. Platform services + +Per-board implementations behind small interfaces: `Sound` (buzzer PWM today; +I2S ES8311 tone generator on L2), `DisplayPower` (auto-off, brightness), `Led`, +and input sources. Input reaches frontends in two forms: **focus keys** (the +existing `KEY_*` codes β€” joystick, buttons, CardKB) and **pointer events** +(`down/move/up` with coordinates β€” touch). LVGL consumes both natively; `ui-new` +consumes keys only. + +## `ui-lvgl` specifics + +- LVGL 9 (pinned), memory in a PSRAM pool; partial-render buffers in internal RAM, flush over QSPI via LovyanGFX. +- Theme tokens (colours, spacing, typography) in one place β€” the first chance to realise the "Amber Trace" direction in colour. +- Fonts with full Latin/Polish/Cyrillic coverage. +- Runs in the WASM sim at 320Γ—240 with mouse as touch, so most UI work needs no flashing. +- PR #3381's LVGL UI is a reference for platform glue (flush, GT911, PSRAM pool) only; its screens are not adopted. + +## Migration plan + +Each step keeps `WioTrackerL1_companion_solo_dual` behaviour identical and is +checked in the sim plus on L1 hardware before the next one. + +1. **Skeleton.** `examples/companion_radio/ui-core/`; add it to every solo env's `build_src_filter` / include path. Move `MessageHistory` + DM unread tracking into it; `ui-new` uses them through the Core. +2. **Engines, one per commit.** Clock tools β†’ ping β†’ course-over-ground β†’ live share β†’ locator β†’ trail β†’ notifications. `UITask` shrinks to screen management + drawing. +3. **Flip the interface.** `UiCore` implements `AbstractUITask`; `ui-new`'s `UITask` becomes a frontend fed by events. +4. **Settings schema.** Convert `SettingsScreen` section by section. +5. **`ui-lvgl` skeleton** for L2 + sim target: boot, home, message list/conversation, keyboard. Then screens by priority. +6. Contacts/Nearby, Admin, Bot logic extraction as the LVGL screens for them are built. + +## Decisions + +- **Upstream merge cost accepted** (2026-09-24). Moving code out of `ui-new` + will conflict with upstream edits to the same files; this fork's `ui-new` is + already heavily diverged, and upstream is itself working toward a larger UI + abstraction β€” revisit alignment when that lands. +- **Scope is UI only** (2026-09-24). The settings schema and event queue serve + the on-device frontends; CLI and companion-app settings/push paths are + untouched for now.