docs(solo): bring the docs up to date with the L1 refresh

- Home pages as they are now (Status, quick panels, Map page), the Status
  screen's tabs instead of Diagnostics / GPS / Sensors, lock clock Big /
  Compact and the hint on a key press, battery curve on L1 and L2, "+ Send"
  and a failed send, trail pin, vector map behind -D MAP_VECTOR.
- Developer guide: drawScreenHeader, soft frames, InfoKit, TabBar, a status
  bar without blinking, toast and millis() rules; ui-core status; build
  flags (GPS_SKYVIEW, MAP_VECTOR, HIST_ARCHIVE added, ui-tiny-only ones out).
- New simulator screenshots: the clock page and the Status page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-02 14:23:15 +02:00
co-authored by Claude Opus 5.5
parent 3631e0ab85
commit a733b8bcf5
13 changed files with 157 additions and 84 deletions
+4 -6
View File
@@ -31,7 +31,7 @@ per-user. Listed here so the rest of this page can assume them.
| `TRAIL_CAPACITY=<n>` | Points the GPS trail holds (16 B each). Default 512 for the nRF52's RAM; the L2 sets 32768 in PSRAM. At most 65535 (the save format's count). |
| `TRAIL_SIMPLIFY=0` | Keeps every trail sample instead of storing straight stretches as their two ends. For boards with the memory for it (the L2). |
| `TRAIL_FIXED_MIN_DELTA_M=<m>` | A fixed distance between trail points instead of the Point spacing setting, which is then hidden (the L2: 5 m, against GPS jitter). |
| `UI_SENSORS_PAGE=1` | Enables the on-device sensors dashboard page. |
| `UI_SENSORS_PAGE=1` | Only the default Home page order in the solo UI: sensor readings are on the Status screen's Power tab whenever the board has sensors. |
| `BLE_PIN_CODE=<n>` | See below — not a plain fixed value in practice. |
| `DISPLAY_CLASS=<Class>` | Selects the display driver (e.g. `SSD1306Display`, `GxEPDDisplay`, `ST7789Display` — see `src/helpers/ui/` for the full set). Fixed by whatever panel the board actually has; only relevant if you're wiring on a *different* display than stock, in which case the matching driver's `.cpp` also needs adding to `build_src_filter`. |
@@ -56,7 +56,6 @@ existing `solo/<board>/platformio.ini` for what's already claimed).
| `UI_HAS_JOYSTICK=1` + `UI_HAS_JOYSTICK_UPDOWN=1` (optional) + `JOYSTICK_UP` / `JOYSTICK_DOWN` / `JOYSTICK_LEFT` / `JOYSTICK_RIGHT` + `PIN_USER_BTN` + `PIN_BACK_BTN` | A wired joystick (four direction contacts + a press contact for Enter). Replaces single-button navigation entirely once enabled. See [External Keyboard & Joystick](../hardware.md#external-keyboard-and-joystick). |
| `PIN_GPIO1` .. `PIN_GPIO4` | Up to four general-purpose pins, each independently switchable between Off / Input / Output (GPIO1/GPIO2 also get an Analog step, if wired to an ADC-capable pin) from Tools › GPIO, and via the `!gpio1`..`!gpio4` bot commands. Not restricted to any particular board — works anywhere the pins are actually free. See [Tools › GPIO](../tools.md#gpio). |
| `PIN_HALL_SENSOR` + `HALL_ACTIVE_HIGH=1` (optional) | A Hall-effect or reed sensor for a magnetic flip cover: closing locks and blanks the screen instantly, opening unlocks and wakes it, no combo either way. `HALL_ACTIVE_HIGH` is only for a module wired to pull the pin high (rather than low) when the magnet is near. See [Screen lock › Magnetic cover](../lock.md#magnetic-cover). |
| `PIN_GPS_SWITCH` | A physical on/off switch for GPS power, read alongside the software GPS toggle — the Tools screen shows `gps off(hw)` / `gps off(sw)` when the two disagree, instead of silently trusting one over the other. |
---
@@ -75,9 +74,7 @@ existing `solo/<board>/platformio.ini` for what's already claimed).
| --- | --- |
| `ENV_INCLUDE_GPS=1` | Compiles in GPS support (NMEA parsing, location provider) at all. |
| `GPS_BAUD_RATE=<n>` | Baud rate for the GPS module's serial link (`Serial1`). Match your module's default. |
`PIN_GPS_SWITCH` (above) is independent of both — it's a hardware kill switch
layered on top of GPS support, not a requirement for it.
| `GPS_SKYVIEW` | Keeps every satellite in view (constellation, elevation, azimuth, signal) for the Status screen's Sky tab and the L2's GPS app (~1.5 KB RAM). Set on the Wio Tracker L1 and L2. |
---
@@ -90,7 +87,6 @@ layered on top of GPS support, not a requirement for it.
| `ENABLE_SCREENSHOT` | Lets the site's [USB tools](https://solo.marekzegarek.com/#pc-tools) (or `tools/screenshot.py`) capture the screen over USB. A colour screen (the L2) renders a fresh frame and sends it as RGB565. |
| `KEEP_DISPLAY_ON_USB` | Refreshes the auto-off deadline continuously while externally (USB) powered, so the auto-off timer only starts counting once power is actually removed. Off by default because OLED panels burn in quickly with a permanently-lit screen — only worth enabling for an LCD/e-ink target, or a display you don't mind replacing. |
| `AUTO_OFF_MILLIS=<ms>` | How long the display stays on before auto-off. Default 15000 (15s); `0` disables auto-off entirely. |
| `UI_RECENT_LIST_SIZE=<n>` | How many entries the recent-activity lists show before scrolling. Default 4. |
---
@@ -116,4 +112,6 @@ values above that are set from the phone app or the CLI.
| Flag | Meaning |
| --- | --- |
| `ADVERT_NAME='"name"'` | Sets the default node name baked into a fresh device, instead of the hex of the first 4 bytes of its public key. Note the doubled quoting — it's a C string literal passed through a build flag. |
| `MAP_VECTOR` | Wio Tracker L2: turns on the unfinished vector map (regions from `tools/maps/osm_vector.py`). Without it the vector map stays off whatever was saved. |
| `HIST_ARCHIVE` | Wio Tracker L2: message history also kept on the SD card (`ui-lvgl/HistoryStore.h`). Set in its solo build. |
| `MESHCORE_VERSION='"x.y"'` | Overrides the upstream MeshCore base-version string shown in diagnostics, for boards whose port hasn't been rebased onto the latest yet. Cosmetic only — doesn't change protocol behaviour. |
File diff suppressed because one or more lines are too long
+62 -28
View File
@@ -63,7 +63,9 @@ public:
Only screens needing a *parameter* at entry add a typed call after it (e.g.
`gotoRingtoneEditor` → `selectSlot(slot)`, `gotoMapScreen` → `showMapView()`).
4. Reach it from somewhere — usually a row in `ToolsScreen.h` (add an `Action`
enum value, a row in the right section table, and a `dispatch()` case).
enum value, a row in the right section table, and a `run()` case). The
Tools home page lists the last two tools used; give the new one a state
in `HomeScreen::toolState()` (`UITask.cpp`) if it has one worth showing.
That's the whole contract. Steps 1, 3 and 4 are compiler-checked (a mismatch
won't link); only a forgotten step 2 can slip through — every screen pointer is
@@ -95,14 +97,19 @@ pixel sizes** — derive everything from these:
Drawing helpers (all clip/measure for you):
- `drawCenteredHeader(title, menu_hint=false, menu_open=false)` — plain centered
title + separator.
- `drawInvertedHeader(label, menu_hint=false, menu_open=false)` — filled title
bar (used by detail views).
- Both take an optional `menu_hint`: pass `true` on a screen with a Hold-Enter
context menu to reserve a `≡` glyph (`menuHintWidth()`/`drawContextMenuHint()`)
in the header, so the menu is discoverable without already knowing the
shortcut; `menu_open` highlights it while the menu is actually up.
- `drawScreenHeader(d, title, page=-1, pages=0, menu_hint=false, menu_open=false)`
(`ui-new/icons.h`) — every screen's title bar: the title (title case, no
icon) centred over the separator, page dots at the right for a screen with
views (`page` of `pages`, e.g. Trail's Summary / Map / List), and the `≡`
hint when the screen has a Hold-Enter menu, so the menu is discoverable
without knowing the shortcut; `menu_open` highlights it while the menu is
up. A title too long to centre goes left and ends in an ellipsis.
- `drawInvertedHeader(label, menu_hint=false, menu_open=false)` — a filled
title bar, kept for the node detail and Navigate views.
- `drawSoftRect` / `fillSoftRect` — a box with its corner pixels left out;
every frame and fill in the UI uses them (selections, pills, buttons).
`drawPanel(x, y, w, h)` is a box floating over the view (alert, popup,
hint): it clears a 1 px moat, fills dark and draws the soft border.
- `drawSelectionRow(x, y, w, h, sel)` — the highlight bar behind a list row.
- `drawTextEllipsized(x, y, max_w, str, selected=false)` — truncates with `…`;
**use this for any user string** (names, labels) so long/UTF-8 text can't
@@ -160,6 +167,8 @@ calculator (`scrollIndicatorReserve`) are exposed for hand-laid lists.
| `DigitEditor` | `DigitEditor.h` | scroll-edit one number, digit by digit |
| `FullscreenMsgView` | `FullscreenMsgView.h` | scrollable full-message reader + word wrap |
| `NavView` | `NavView.h` | bearing/distance/ETA "navigate to a point" view |
| `TabBar` | `TabBar.h` | a row of tabs: the current one a soft pill, whole tabs only, ◂ ▸ where more are off screen |
| `info::` rows and blocks | `InfoKit.h` | settings rows, data screens (below) |
All follow the same shape: a `begin(...)` to open, an `active` flag, a
`handleInput(c)` returning a small `Result` enum, and a `render()`/`draw()`.
@@ -211,6 +220,24 @@ to pick, Enter to insert via the shared `insertGlyph()` helper, Cancel to
dismiss. Holding a letter with no variants, or any T9/alt-alphabet/symbols
cell, is a no-op.
**InfoKit** (`ui-new/InfoKit.h`, namespace `info`) is the shared look of
settings and data screens. Rows (drawn after `drawRowSelection()`, in its
ink): `valueRow(d, y, label, value, sel, reserve, x0)` — label left, value
flush right with its unit after a space ("50 m"), cut and marquee-scrolled
when selected; `switchRow` — a yes/no setting as an on/off switch
(`toggle()`); `listRow` — picks between the two for a formatter that returns
exactly "ON" / "OFF". `rowWrapped` gives a long value its own line instead of
an ellipsis nobody can scroll. Blocks for data screens: `section` (a centred
"··· Font ···" divider), `meter`, `battery`, `chip`, `big` (a 2× number with
its unit), `History<N>` + `spark` (a sparkline), `rule` / `vrule`. `Flow`
places blocks top-down, skips the ones scrolled off and tells whether more
are below, which gives the scroll indicator for free (see `StatusScreen.h`).
Other drawing helpers in `icons.h`: `drawButton` (a push button, soft pill,
filled while selected), `drawDisclosure` (▸ / ▾ on an accordion section),
`drawPageDots`, `drawLoadingDots` (the waiting animation), `drawSignalBars`
(SNR as four bars).
`FullscreenMsgView::wrapLines()` is a standalone pixel-accurate word-wrapper
(O(n), variable-width-font aware) reusable by any multi-line layout; it writes
into the shared `s_wrap_trans` / `s_wrap_lines` scratch (single-threaded render,
@@ -260,21 +287,19 @@ MINI_ICON(ICON_FOO, 5,
```
Draw with `miniIconDraw(display, x, topY, ICON_FOO)` (auto-scaled & centered),
`miniIconDrawTop` (exact placement), or the boxed/slot variants
(`drawBoxedIcon` = lit when active, `drawSlotIcon` = plain). Bigger page glyphs
use `BIG_ICON` / `bigIconDraw`. The home status bar composes these right-to-left
with a `blinkOn()` cadence for "leave it on and forget" broadcasts (auto-advert,
Live Share, trail, repeater) — follow that pattern when adding an indicator:
always shown on e-ink, blinking on OLED.
`miniIconDrawTop` (exact placement), `miniIconDrawCentered`, or `drawSlotIcon`
(centred in a status-bar slot). Use icons sparingly: headers and the Tools
list have none, the Home page row is the main place for them.
Icons are drawn from a fixed priority-ordered table (`HomeScreen::renderBatteryIndicator()`,
`UITask.cpp`); once the row runs out of horizontal space the loop just stops,
so the lowest-priority icons silently drop first rather than the whole bar
crushing the node name. A blinking icon still reserves its width on the
off-phase of its blink, so the row's layout can't visibly shift width as icons
blink in and out.
The home status bar (`HomeScreen::renderBatteryIndicator()`, `UITask.cpp`)
draws plain glyphs right to left from a fixed, priority-ordered table, with
no boxes and no blinking: each icon tells a state by its shape (Bluetooth only
while a phone is connected, the GPS reticle broken until a fix, a pin while
the trail records). Once the row runs out of room the loop stops, so the
lowest-priority icons drop first instead of the bar crushing the time. Add an
indicator as one more row in that table, at its priority.
Screens with a Hold-Enter context menu (Nodes, Bot, Admin, Diagnostics, …) pass
Screens with a Hold-Enter context menu (Nodes, Messages, Bot, Admin, …) pass
`menu_hint=true` to their header call (see §2) so a `≡` glyph advertises the
menu; `KEY_CONTEXT_MENU` (Hold-Enter) opens it.
@@ -363,7 +388,13 @@ with `resolvePersonPos()` (live `[LOC]` share, else last-advertised fix).
`chanctl::onRemoved()` (ui-core/ChannelControl.h). New per-contact or
per-channel state should clear there too.
- **Toasts:** `_task->showAlert("msg", duration_ms)` overlays a transient banner
over any screen; no redraw plumbing needed.
over any screen (not over the keyboard); no redraw plumbing needed. Use one
only for what the screen doesn't already show: a failure ("Send failed",
"Contacts full") or an outcome somewhere else. No echo of an action whose
result is in view ("Sent", "Deleted", "Counters reset").
- **Time:** `millis()` wraps after ~49.7 days. Compare deadlines as
`(int32_t)(millis() - deadline) >= 0`, never `millis() >= deadline`, and use
0 as "now / none" (`UITask::refreshDue()`, `alertShowing()`).
- **Strings:** always `strncpy`+NUL or `snprintf`; treat every name/label as
untrusted-length and render through `drawTextEllipsized`.
@@ -374,7 +405,8 @@ with `resolvePersonPos()` (live `[LOC]` share, else last-advertised fix).
```cpp
// ui-new/MyToolScreen.h — included by UITask.cpp near the other screens
#pragma once
#include "icons.h" // drawList + mini-icons
#include "icons.h" // drawList, drawScreenHeader, mini-icons
#include "InfoKit.h" // valueRow / switchRow
#include "../NodePrefs.h"
class MyToolScreen : public UIScreen {
@@ -389,13 +421,15 @@ public:
int render(DisplayDriver& d) override {
d.setTextSize(1);
d.drawCenteredHeader("MY TOOL");
drawScreenHeader(d, "My tool");
int next = 500;
drawList(d, ROWS, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
drawRowSelection(d, y, sel, reserve);
d.setCursor(4, y);
d.print(i == 0 ? "Alpha" : i == 1 ? "Bravo" : "Charlie");
static const char* const L[ROWS] = { "Alpha", "Bravo", "Charlie" };
const int m = info::valueRow(d, y, L[i], "50 m", sel, reserve); // value flush right
if (m > 0 && m < next) next = m; // a marquee moving
});
return 500;
return next;
}
bool handleInput(char c) override {