mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-09-25 19:54:58 +00:00
383 lines
17 KiB
Markdown
383 lines
17 KiB
Markdown
# Console mode
|
|
|
|
A boot mode where the device runs with **no LVGL**: a text console driven by
|
|
typed commands, with core mesh functionality and Lua apps. For boards that
|
|
struggle under the UI, and for people who would rather type than tap.
|
|
|
|
Regular mode is unaffected and you can go back at any time.
|
|
|
|
---
|
|
|
|
## Short answer
|
|
|
|
**Possible: yes, and considerably more of it already exists than you would
|
|
expect.** Four of the five pieces are built and shipping today.
|
|
|
|
**A lot of work: no, not for a usable console.** The work is not the console
|
|
itself, it is everything that currently assumes the UI is there. Realistic
|
|
shape: a useful console in a few days of work, a polished one that runs Lua
|
|
apps in a couple of weeks.
|
|
|
|
**Saves resources: yes, but not the resource you might think.** See
|
|
[What it actually saves](#what-it-actually-saves) below, because this changes
|
|
how the feature should be sold and to whom.
|
|
|
|
---
|
|
|
|
## What already exists
|
|
|
|
This is the reason the estimate is not larger.
|
|
|
|
| Piece | Where | State |
|
|
|---|---|---|
|
|
| **Command surface** | `CommonCLI::handleCommand` (MeshCore core) | Done. The full command set the node already answers over serial and over the mesh. |
|
|
| **Run a command locally, capture its output** | `MyMesh::runLocalCli()` + `MyMesh::setTerminalSink()` | Done. Feed it a line, get reply lines back through a callback. |
|
|
| **Text rendering with no LVGL** | `DisplayDriver` (core): `startFrame`, `setCursor`, `print`, `printWordWrap`, `getTextWidth`, `fillRect`, `drawTextEllipsized`, `endFrame`, UTF-8 to blocks | Done, and linked into the touch build already. |
|
|
| **A mode where LVGL does not drive the panel** | Remote mode (`s_remote_mode` in `UITask.cpp`) | Done and shipping. It paints the physical screen with `display.drawTextCentered(...)` directly while LVGL renders elsewhere. This is the exact precedent. |
|
|
| **An on-device terminal** | `homeTerminalCb` / `s_term_log_box` in `UITask.cpp` | Done, but built out of LVGL widgets. The *behaviour* is the model; the widgets are what console mode replaces. |
|
|
|
|
So the console does not need a new command language, a new renderer, or a new
|
|
way of talking to the mesh. It needs a different **front end** over machinery
|
|
that is already there and already tested.
|
|
|
|
---
|
|
|
|
## What it actually saves
|
|
|
|
Worth being precise, because the obvious assumption is wrong in one direction
|
|
and right in another.
|
|
|
|
### PSRAM: a real saving, and it is the object tree, not the draw buffer
|
|
|
|
Everything LVGL allocates is PSRAM: `include/lv_conf.h` sets `LV_MEM_CUSTOM 1`
|
|
with `LV_MEM_CUSTOM_ALLOC lvglPsramAlloc`, so every object, style and font cache
|
|
entry is a PSRAM allocation, and the draw buffer is `MALLOC_CAP_SPIRAM` too.
|
|
|
|
**The draw buffer is not the prize.** `LV_DRAW_BUF_LINES` is 24, so it is
|
|
37.5 KB on the 800 px panels and **11.2 KB on the V4**. Worth having back, not
|
|
worth building a mode for.
|
|
|
|
The saving is the **object tree**: every screen, tab, list row, label, style and
|
|
cached glyph, all of it live for the life of the session. That is the number
|
|
Phase 0 exists to measure, because it is the one nobody here can quote from
|
|
memory.
|
|
|
|
For scale on the V4, its PSRAM is also carrying up to four 128 KB map tiles, so
|
|
whatever LVGL is holding sits alongside that. Console mode frees the LVGL half
|
|
outright.
|
|
|
|
**This matters most exactly where you said it hurts.** The Heltec V4 has 2 MB of
|
|
PSRAM, and PSRAM pressure there is a recurring source of real bugs: map tiles
|
|
fragmenting the heap, the tile pool needing a cull and a cap, the extended Lua
|
|
SDK gated off the board entirely for want of memory. Console mode gives that
|
|
board its PSRAM back.
|
|
|
|
### Internal DRAM: a smaller saving than it looks
|
|
|
|
The scarce resource on these boards is internal DRAM, and LVGL is largely not
|
|
in it. What *is* in it is UITask's static data: the contact cache, message
|
|
rings, layout tables. Most of that is data the console still needs to do the
|
|
same job, so it does not simply disappear.
|
|
|
|
Two caveats worth keeping honest:
|
|
|
|
- There is a fallback at boot: if the PSRAM draw-buffer allocation fails,
|
|
`malloc()` puts it in internal DRAM instead. On a board already under
|
|
pressure that is exactly when it happens, so for the worst-off devices the
|
|
DRAM saving is real after all.
|
|
- A meaningful DRAM number needs measuring, not guessing. See Phase 0.
|
|
|
|
### CPU: a real saving, and probably the one users feel
|
|
|
|
`lv_timer_handler()` runs on every pass of the main loop, alongside input-device
|
|
polling and any animation or scroll in flight, and a redraw blits through SPI.
|
|
A console redraws only when a line is added.
|
|
|
|
This is also the mechanism behind several bugs we have already fixed: the loop
|
|
being stalled by the UI is what starved the GPS UART ring, and what made SPIFFS
|
|
garbage collection visible as a freeze. Less time in the UI is less exposure to
|
|
that whole class of problem.
|
|
|
|
**How to describe the feature honestly:** it buys back PSRAM and CPU. On the
|
|
V4 that is the difference between comfortable and not. It is not a fix for
|
|
internal-DRAM exhaustion on its own.
|
|
|
|
---
|
|
|
|
## The hard parts
|
|
|
|
Not the console. These three.
|
|
|
|
### 1. Everything that assumes the UI exists
|
|
|
|
`src/main.cpp` calls `ui_task.showAlert(...)` in roughly twenty places to report
|
|
Wi-Fi and Bluetooth outcomes, and `ui_task.loop()` unconditionally. UITask is
|
|
also the owner of things the console still wants: the storage root, the Lua app
|
|
host, the update check.
|
|
|
|
The wrong fix is `#if` sprinkled through main. The right one is that UITask
|
|
keeps its role as the front end and gains a **headless personality**: the same
|
|
entry points, LVGL never initialised, `showAlert` becoming a console line.
|
|
|
|
### 2. Lua apps: the `wada.ui` translator
|
|
|
|
Decided: `wada.ui` gets a **console backend** behind the same names, so an app
|
|
keeps working without knowing which mode it is in.
|
|
|
|
This turns out to be far more achievable than "no UI" suggests, because console
|
|
mode is **not a text-only terminal**. It still owns a real pixel panel. The
|
|
thing being removed is LVGL's object tree, not the ability to draw.
|
|
|
|
The enabler is a raw framebuffer blit that already exists outside LVGL:
|
|
|
|
```
|
|
ST7789LCDDisplay::writePixelsRGB565(x, y, w, h, const uint16_t* pixels)
|
|
```
|
|
|
|
Present today on `ST7789LCDDisplay` (V4 TFT, T-Deck), `TanmatsuDisplay`,
|
|
`RM69A10Display` and `HI8561Display`. Missing only on `LGFXDisplay` (V4-R8) and
|
|
`ST7796LCDDisplay` (the pagers), and LovyanGFX has `pushImage` underneath, so
|
|
those are a wrapper method rather than new work.
|
|
|
|
That changes the answer on the two APIs that looked impossible:
|
|
|
|
| API | Console backend | Why it works |
|
|
|---|---|---|
|
|
| `label` | Text at a cursor. | `print` / `printWordWrap` / `drawTextEllipsized`. |
|
|
| `button` | A highlighted line with a selection index; activated by key or tap. | Text plus `fillRect` for the highlight. |
|
|
| `list` | Numbered lines, one selected. | Same. |
|
|
| `input` | A prompt, using the console's own keypad. | Phase 1 already builds it. |
|
|
| `text_w` / `text_lines` | `getTextWidth`, honestly measured. | Already the right shape. |
|
|
| `chart` | Drawn directly. | It is bars and lines; `fillRect` does it. |
|
|
| **`canvas`** | **Works.** The app already gets an RGB565 buffer and draws into it; console mode blits that buffer instead of handing it to LVGL. | `writePixelsRGB565`. |
|
|
| **`map`** | **Works.** Tiles are already decoded to RGB565 buffers (`MapTile.rgb565`); the projection and cache are ours and have no LVGL in them. | Same blit, plus markers as `fillRect`. |
|
|
| Everything non-UI | Unchanged. | `mesh`, `fs`, `net`, `crypto`, `geo`, `sys`, `timer`, `store` never touched LVGL. |
|
|
|
|
**What is genuinely lost is interaction, not drawing.** LVGL supplies scrolling
|
|
containers, a focus group, flex layout and event bubbling. The console backend
|
|
has to provide its own much simpler version: a line cursor, a selection index,
|
|
a scroll offset. That is the real work in this phase, and it is bounded.
|
|
|
|
So the honest expectation is that **most apps work**, including the graphical
|
|
ones, and a few that lean on LVGL behaviour we do not reimplement will not.
|
|
Apps already feature-detect through `wada.sys.caps()`, so anything that cannot
|
|
be supported is reported rather than broken, and `caps().console` lets an app
|
|
adapt its layout deliberately.
|
|
|
|
### 3. Getting back out
|
|
|
|
A boot flag that lands you in a console with no way back is a device that feels
|
|
bricked, and it will happen to somebody. This needs to be designed, not bolted
|
|
on. See [Safety](#safety).
|
|
|
|
---
|
|
|
|
## Plan
|
|
|
|
Each phase is shippable and useful on its own. Stop after any of them and
|
|
nothing is half-built.
|
|
|
|
### Phase 0: measure first
|
|
|
|
Before building anything, get the numbers the feature is being justified by.
|
|
|
|
- Free internal DRAM and free PSRAM at the home screen, on a V4 and a T-Deck.
|
|
- The same with `lv_timer_handler()` stubbed out and nothing built.
|
|
- Loop iterations per second, both ways.
|
|
|
|
Cheap, and it either confirms the case or changes what to build. If the DRAM
|
|
saving turns out to be negligible on the V4 and the PSRAM saving large, that is
|
|
worth knowing before the docs promise anything.
|
|
|
|
**Output:** real numbers in this file.
|
|
|
|
### Phase 1: the console front end
|
|
|
|
A `ConsoleUI` that owns the panel through `DisplayDriver`, with no LVGL.
|
|
|
|
- Scrollback ring in PSRAM, N lines, prune oldest.
|
|
- Prompt line, cursor, word wrap via `printWordWrap`.
|
|
- Input from the hardware keyboard where there is one.
|
|
- **An on-screen keypad on touch boards.** Decided: the V4 is the board that
|
|
most needs its resources back, so console mode has to be usable there without
|
|
a keyboard. The existing on-screen keyboard is LVGL and cannot be reused, so
|
|
the console draws its own: `fillRect` + `drawRect` + `print` for the keys, and
|
|
touch read straight from the board driver (`heltecV4CapTouchCheck()` and its
|
|
equivalents), which is already independent of LVGL. `lvglTouchRead` is only an
|
|
adapter feeding LVGL, so nothing needs to be untangled first.
|
|
- Commands go straight to `the_mesh.runLocalCli()`; output arrives through
|
|
`setTerminalSink`. This part is close to free.
|
|
- One touch affordance, as you said: a back/exit target.
|
|
|
|
At the end of this phase the device is usable over the mesh from a console.
|
|
|
|
### Phase 2: boot into it, and back out
|
|
|
|
- A pref, `touchPrefsGetBootMode()`, read early in `setup()`.
|
|
- In console mode: never call `lv_init()`, never allocate the draw buffer, never
|
|
build the UI. This is where the saving comes from, so it has to be a genuine
|
|
skip and not a hidden LVGL instance.
|
|
- `ui_task.showAlert()` and friends route to the console.
|
|
- Commands to switch: `ui` reboots into the graphical mode, `console` reboots
|
|
into this one. Also a Settings toggle in normal mode.
|
|
|
|
### Phase 3: the things the CLI does not cover
|
|
|
|
`CommonCLI` is a node CLI, not a messenger. Console mode needs conversational
|
|
commands, some of which the existing on-device terminal already has (`to <name>`
|
|
sets a recipient):
|
|
|
|
- `msg`, `to`, `reply`, unread counts, reading a thread's scrollback
|
|
- `contacts`, `chans`, `discover` with the results table
|
|
- `apps`, `run <id>`
|
|
|
|
Most of this is a thin layer over calls that already exist for the UI.
|
|
|
|
### Phase 4: Lua apps in the console
|
|
|
|
- A `WADA_UI_CONSOLE` backend behind the same `wada.ui` names, per the table
|
|
above.
|
|
- `caps()` gains `console`, `canvas`, `map` so apps can adapt, and the docs say
|
|
which apps work.
|
|
- Store apps audited; those that cannot work say so on launch rather than
|
|
failing oddly.
|
|
|
|
### Phase 5: polish
|
|
|
|
- Command history and tab completion.
|
|
- Colour, where the panel has it.
|
|
- A `help` that is actually good, since there is no discoverable UI.
|
|
- Docs page, and a section in the user guide.
|
|
|
|
---
|
|
|
|
## Safety
|
|
|
|
The failure that matters is a device that boots to a console the owner cannot
|
|
leave. Three independent ways out, because one is not enough:
|
|
|
|
1. **A command.** `ui` then reboot.
|
|
2. **A key held at boot.** Any boot with the key down forces graphical mode
|
|
regardless of the pref, the same shape as the existing recovery paths.
|
|
3. **The web flasher and the serial CLI** can always clear the pref.
|
|
|
|
And the pref should be stored so that a corrupt or unreadable value means
|
|
*graphical*, never console. Fail toward the mode everyone can use.
|
|
|
|
---
|
|
|
|
## Risks
|
|
|
|
| Risk | Containment |
|
|
|---|---|
|
|
| Console mode rots because nobody builds it | It compiles into every build from Phase 1, and SDK Test runs in it. A broken console fails the build, not a user. |
|
|
| Two front ends drift apart | The console consumes the same `runLocalCli` and the same MyMesh accessors the UI does. Do not let it grow a private copy of anything. |
|
|
| Touch-only boards get a console they cannot type into | Decide in Phase 1. Requiring a keyboard is a legitimate answer. |
|
|
| It quietly degrades normal mode | Everything new is behind the boot pref, and the graphical path keeps its current code path unchanged. Phase 0's numbers are the regression check. |
|
|
| Lua apps half-work and look broken | `caps()` first, refuse to launch second, document third. Less likely now that canvas and map can actually be drawn. |
|
|
| Two display drivers lack a raw blit | `LGFXDisplay` and `ST7796LCDDisplay` need a `writePixelsRGB565`; LovyanGFX's `pushImage` does the work. Until then `caps().canvas` is false on those boards and apps adapt. |
|
|
|
|
---
|
|
|
|
## Effort
|
|
|
|
Honest, assuming no surprises:
|
|
|
|
| Phase | Effort |
|
|
|---|---|
|
|
| 0 measure | Half a day |
|
|
| 1 console front end (incl. the drawn keypad) | 3 to 4 days |
|
|
| 2 boot mode and back out | 1 to 2 days |
|
|
| 3 messaging commands | 2 to 3 days |
|
|
| 4 `wada.ui` console backend | 4 to 6 days |
|
|
| 5 polish and docs | 2 days |
|
|
|
|
**A console you can actually use on the mesh: Phases 0 to 3, about a week.**
|
|
Phase 4 is the one that can grow, because it is the one with a design question
|
|
in it rather than just code.
|
|
|
|
---
|
|
|
|
## Open decisions
|
|
|
|
Worth settling before Phase 1 rather than during it.
|
|
|
|
1. ~~Keyboard required?~~ **Decided: no.** Touch boards get a drawn on-screen
|
|
keypad, because the V4 is exactly the board this feature is for.
|
|
2. **Do Lua apps matter in v1?** Phases 0 to 3 are worth shipping without them.
|
|
The translator is now a known quantity rather than a question mark, so it can
|
|
follow safely.
|
|
3. **Does this replace the Terminal app** in graphical mode eventually, or do
|
|
both stay?
|
|
|
|
---
|
|
|
|
## Audit: every UI surface, and its console equivalent
|
|
|
|
Done by enumerating the real thing rather than from memory: the tab list, the
|
|
app-drawer action enum (`APPACT_*`), the 19 settings categories, and the node
|
|
CLI's own command table in `CommonCLI.cpp`.
|
|
|
|
**The single most useful finding: the node CLI already covers far more than
|
|
expected.** `advert`, `board`, `clock`, `clock sync`, `erase`, `get`/`set`,
|
|
`gps` (on/off/sync/setloc/advert), `log`, `neighbors`, `ota url`, `password`,
|
|
`poweroff`, `powersaving`, `reboot`, `region`, `sensor list/get/set`,
|
|
`shutdown`, `start ota`, `tempradio`, `time`, `ver`. All of that arrives free,
|
|
which is why the console needed far fewer new commands than the UI has screens.
|
|
|
|
### Tabs and apps
|
|
|
|
| UI surface | Console | State |
|
|
|---|---|---|
|
|
| Chats: read a thread | `chat <name>` | **done** |
|
|
| Chats: send | `to <name>` + `msg <text>` | **done** |
|
|
| Chats: unread badges | `unread`, `read <name>` | **done**, and the monitor never clears them |
|
|
| Live incoming messages | the monitor, `monitor on\|off` | **done** |
|
|
| Contacts list | `contacts` | **done** |
|
|
| Contacts: favourite / block / delete | none | gap |
|
|
| Channels list | `chans` | **done** |
|
|
| Discover | `discover`, `discovered` | **done**, with both link directions |
|
|
| Advertise | CLI `advert` | free |
|
|
| Signal / traffic | `stat` | **done** |
|
|
| Map | none | needs the `wada.map` blit path; not console-shaped yet |
|
|
| Terminal | this *is* the console | n/a |
|
|
| Files | none | gap |
|
|
| Spectrum | none | gap, and heavy |
|
|
| Store / Lua apps | none | Phase 4 |
|
|
| Web reader | none | gap |
|
|
| VNC / Remote | none | deliberate: another front end, not a console feature |
|
|
| Power menu | CLI `poweroff` / `reboot` | free |
|
|
| Mentions | `unread` shows counts | partial |
|
|
|
|
### Settings, all 19 categories
|
|
|
|
| Category | Console | State |
|
|
|---|---|---|
|
|
| Radio & Mesh | CLI `get`/`set`, `region`, `tempradio` | free |
|
|
| GPS | CLI `gps *` | free |
|
|
| Clock & time | CLI `clock`, `time` | free |
|
|
| Sensors | CLI `sensor *` | free |
|
|
| About | CLI `ver`, `board`; `mem` | free |
|
|
| Battery | `batt` | **done** |
|
|
| Wi-Fi | `wifi` (state only) | partial |
|
|
| Bluetooth | none | gap |
|
|
| Auto-add | none | gap |
|
|
| Display / Keyboard / Sound / Lock screen | none | not meaningful without the UI |
|
|
| Quick replies | none | gap |
|
|
| Backups | none | gap |
|
|
| Language | none | gap |
|
|
| MQTT bridge | `mqtt status` | **done** (diagnostics only) |
|
|
| App permissions | none | with Phase 4 |
|
|
| General | `console`/`ui` toggle | partial |
|
|
|
|
### What the gaps have in common
|
|
|
|
Almost everything still missing is either **Phase 4** (anything Lua), **not
|
|
console-shaped** (Display, Keyboard, Lock screen: settings for a UI that is not
|
|
running), or **a list-and-act flow** (files, backups, quick replies, contact
|
|
actions) that all wants the same small pattern: number the list, act on the
|
|
number. That pattern is worth building once rather than six times.
|
|
|
|
The two genuinely interesting gaps are **Files** and **contact actions**
|
|
(favourite, block, delete), because both are things people do reach for.
|