Commit Graph
18 Commits
Author SHA1 Message Date
Michael A. Cojocari 31a6a9798e fix(tdeck-pro): make console readable on e-paper
Use white paper and black ink from the first boot frame, commit sprite-backed input updates, and keep the e-paper cursor solid.

Fixes #562
2026-09-23 10:23:45 -04:00
Michael A. Cojocari 0aa2f28c16 feat(ui): add high-contrast day and night themes 2026-09-20 19:08:45 -04:00
Kaj SchittecatandClaude Opus 5 ce37110515 touch: web unlock, a way out of console mode, paste keys, M9 update notice
- Web interface: unlock a manually locked screen (#506). The device publishes
  its lock state over the mirror socket and the page shows an Unlock button
  while it is set. The lock screen absorbs taps by design and only a held
  trackball or BOOT press unlocks on the device, so a browser had no way back
  from a screen it had locked itself.
- Console mode: hold the panel for three seconds to leave it (#507). The
  banner and `help` both say so. Console mode also applies the "Older keyboard
  protocol" setting now: that is applied in the graphical startup path, which
  console mode returns before, so the console ran on protocol detection alone.
  On a T-Deck that needs the older protocol every keystroke there is garbage,
  which is why `ui` could not be typed and the reporter had to side-load a
  second firmware to get the device back.
- Console mode: a touch wakes a dark panel again on the touch-only boards,
  where the keyboard and button wake paths sit below the console branch's
  return. The waking press is swallowed so it cannot also type.
- Paste into a key field lifts the key out of the surrounding text (#526): a
  32 hex digit channel secret or a 64 hex digit public key, spaced keys
  included. Before, the field filled with prose and the length cap cut the key
  off.
- ThinkNode M9: a waiting firmware update is visible (#443). The red "!" over
  the bottom-bar gear is built in the #else of that board's block, so the M9
  had no update signal at all; it gets a third slot in its own notice row,
  steady amber rather than blinking. The Settings tile in the app drawer
  carries an "!" on every board.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:43:06 +02:00
Michael A. Cojocari 3a2781bb6e Work 2026-09-04 21:04:44 -04:00
Kaj SchittecatandClaude Opus 5 2a8b62c6a7 Merge PR #375: Day and Night themes (#296)
oumike. A firmware-wide appearance setting: Night stays the default and renders
exactly as before, Day is a low-glare light palette with its own semantic roles
for text, panels, fields, borders, controls, focus, charts, status colours and
chat surfaces. Selecting a mode restarts, so every LVGL object is rebuilt from
one coherent palette rather than half-repainted. There is a Night/Day selector
in Settings, Display, the Control Center Theme chip is now a direct toggle whose
sun/moon icon shows the active mode, and the standalone console UI has a
matching Day palette.

Main already carried the palette scaffolding for this, pinned to Night behind a
comment saying it was waiting on this branch, so the merge mostly replaces those
stubs with the real thing.

The prefs schema needed care. The branch appended theme_mode as v54, but v54 and
v55 were taken by boot_wifi_time/boot_wifi_open and loud_alerts before this
merged, so the field moves to the tail behind them and the version becomes v56,
with the trailing-field assert and the migration step moved to match. A packed
struct read back at the wrong offsets is the failure this schema's asserts exist
to prevent, so the invariants from both sides are kept rather than one replacing
the other.

That also surfaced a stale test: the v53 case asserted that v53 plus exactly two
bytes was the whole struct, which stopped being true when loud_alerts landed and
was not caught because the host test was not run then. It now counts every byte
appended since and asserts the new fields come back at their defaults. The
schema host tests pass. While there, the size comment said "about 500 bytes";
the struct is 114.

Built on all nine S3 envs and both ESP32-P4 targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 09:11:54 +02:00
Michael A. Cojocari 952075a22e Day/Night 2026-08-31 09:37:45 -04:00
Michael A. Cojocari 015a9cec4a mqtt: add connection status command 2026-08-30 15:05:09 -04:00
Kaj SchittecatandClaude Opus 5 6dd1e2514b console: the exit bridge must not sit behind CAP_TOUCH
The extern for consoleHostRebootToUi went inside the CAP_TOUCH block, so the
M9 (console, no touchscreen) stopped compiling. Every board with a console can
leave it; the declaration follows CAP_CONSOLE's reach, not the panel's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:51:32 +02:00
Kaj SchittecatandClaude Opus 5 e9bf6d9e17 console: make ui actually leave console mode
Typing `ui` or `exit` rebooted straight back into the console.

touchPrefsSetConsoleMode() does not write the pref, it QUEUES an A/B snapshot:
SdNvsPrefs deliberately keeps filesystem I/O off the calling thread, and
touchPrefsFlush() is what forces the queue out. The console called
ESP.restart() directly, so the queued write was discarded and the next boot
still read "console". The Settings toggle never showed this because it goes
through rebootDevice(), whose last act is that flush.

The console now exits through the same path, which also means it leaves on the
same terms as everything else: chat history, the Discovered ring and the sync
replay ring are persisted first. rebootDevice()'s save-failure branch called
lv_refr_now() unconditionally, which is not survivable with no LVGL, so that
one call is now conditional; showAlert was already console-aware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:34:50 +02:00
Kaj SchittecatandClaude Opus 5 93f1934889 console mode: the console was painting on a WHITE background
That is why the greens were unreadable, and it was my mistake at the root: I
built a light-on-dark palette and then painted it onto whatever UIColor
resolved to.

src/helpers/ui/UIColorPalette.cpp defines wadamesh's dark palette and its own
comment warns that the core's driver .cpps 'carry upstream definitions that stay
unlinked only while nothing references them'. The core's ST7789LCDDisplay.cpp
sets window_bkg = WHITE. The console referenced UIColor and got that one.

Fixed by not depending on which definition wins: the console now owns both ends,
painting its own near-black background and its own RGB565 palette, with the
values taken from the firmware's actual theme colours rather than invented neon.
Only the e-ink fallback still consults UIColor, where a single ink colour is the
correct answer.

Banner: the slash-and-underscore figlet art was a smear at a 6 px cell on a
240 px panel. Replaced with a ruled header, which reads at any size on any
board. A login banner exists to say what the machine is, legibly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:27:28 +02:00
Kaj SchittecatandClaude Opus 5 4b5b51ef01 console mode: three colours per line, trackball scroll, shell prompt, visible banner
Colour, properly this time. A line can carry THREE coloured segments, because
the three parts of a message answer different questions: WHERE it arrived
(channel, cyan), WHO said it (sender, yellow) and WHAT they said (grey). A DM
has no channel, so its sender takes the magenta slot and stands out in a busy
feed. Same treatment for stored history, contacts and unread.

The palette is explicit RGB565, deliberately NOT the UIColor theme names:
several of those resolve to the same value on a given board, so 'channel' and
'sender' would have come out identical. e-ink falls back to one ink colour,
where anything else would be invisible.

Banner: it WAS printing, just scrolling off. Boot output was seventeen lines on
a panel that fits fewer. The art is now three lines, identity is one, and the
menu is three-per-row instead of two, so the whole thing fits.

Prompt: 'node:recipient$' instead of a bare caret. Where you are and who you are
talking to is what a shell prompt is for, and 'to <name>' now visibly changes it.

Trackball scrolls the scrollback, accumulated so one flick does not race past
everything, and it wakes a dark screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:19:31 +02:00
Kaj SchittecatandClaude Opus 5 98e8e63af9 console mode: colour, a boot banner, a quick menu, and the audit's missing commands
Colour: each scrollback line carries a ConsoleColor, so meaning picks the colour
rather than the caller's taste. Incoming channel posts, DMs, your own echoed
command, successes, warnings and failures are now distinguishable at a glance.
The palette maps onto the core's UIColor names so the console tracks the rest of
the firmware instead of inventing values.

Banner: an ASCII mark, the node name and build, then the quick menu, in the
login-banner tradition. 19 columns wide so it does not break on the V4's 240 px
panel.

Quick menu: numbered, because typing 1 beats typing 'contacts' and because a
numbered list is how you find out what exists. A bare digit runs the item; it is
checked before everything else so the node CLI cannot shadow it.

The audit (full table in CONSOLE_MODE.md) compared every UI surface against what
the console offered. The node CLI already covers more than expected: advert,
clock, gps, region, get/set, log, neighbors, ota, power, sensor, time, ver. What
it does NOT cover, now added:
  chat <name>   read a thread (does NOT mark it read)
  stat          radio + traffic, the Signal page's numbers
  batt          battery
  wifi          radio state
  discover / discovered   the active probe, with BOTH link directions

luaHostBattery and luaHostMeshDiscover were CAP_LUA_SDK_EXT-gated, so the V4 --
the board this exists for -- failed to link. Widened to || CAP_CONSOLE, same as
the send path earlier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 20:50:59 +02:00
Kaj SchittecatandClaude Opus 5 5c93c05a71 console mode: a live message monitor that does not mark anything read
Incoming messages now appear in the console as they arrive, for every known
channel and DM, without opening anything. On by default, persisted (v52
console_monitor), toggled with 'monitor on|off'.

The unread requirement drove the design. The previous version returned early
from newMsgImpl and printed, which skipped ALL the bookkeeping: no store, no
unread, no filters. Now console mode allocates the same chat store the UI does
(allocMessageStore(), extracted from begin() so both paths share it) and the
message takes the NORMAL path — block list, 1-character spam filter, sender
parsing, appendMessage with mark_unread = true. The console print happens AFTER
that append.

So watching a message scroll past is not reading it. It is stored, it is
counted, and it stays unread until the thread is actually opened:
  unread        what is waiting, per thread. Listing does not clear it.
  read <name>   clear one thread, deliberately.

That also means the monitor inherits the spam and block filtering for free, and
history survives a switch back to the graphical UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 20:24:09 +02:00
Kaj SchittecatandClaude Opus 5 5b5e86c197 console mode: split CLI output on newlines, and self-heal after a panic
The overlap: the node CLI hands its whole reply to the terminal sink as ONE
buffer containing '\n' (its help text is a dozen lines in a single string).
consoleWriteLine stored that as one ring entry, so the text renderer drew the
breaks itself while our y-cursor still advanced by a single row, and every later
line landed on top of the one before it. Worsening down the screen, which is
what the photo shows. Now split on newlines first, then wrap each piece to the
panel width, so no stored line can be wider than the panel and the renderer
never wraps one on its own.

The bootloop: the device panicked, rebooted into console mode, and panicked
again, with no way back to the UI from the device. Console mode now checks
esp_reset_reason() and, if the previous boot panicked, comes up graphical and
clears the pref. That is the third way out the plan called for, and the only one
that needs the user to know nothing.

Honest about attribution: it boots clean now, but I cannot prove the newline bug
was the panic. The serial cut off right after 'serial_interface ok' because
those prints were not flushed, so the fault may have been later than it looked.
Boot breadcrumbs added (flushed) so the next one localises itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 19:53:14 +02:00
Kaj SchittecatandClaude Opus 5 51ea08743b console mode: stop the whole screen flashing on every blink and keystroke
startFrame() is display.fillScreen() and endFrame() is a no-op, so drawing is
direct to the panel with no buffer. render() did a full clear-and-repaint, and
it was called for the cursor blink twice a second AND for every keypress, so the
entire screen flashed continuously.

Split into three levels, cheapest that covers what changed:
* scrollback changed  -> full render (the only case that clears)
* typing              -> repaint the input row only, which never touches the
                         scrollback above it or the keypad below
* cursor blink        -> one fillRect on the cursor cell

An idle console now paints one character cell every half second instead of the
whole panel. Blink also moved to 530 ms so it does not beat against the loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 19:37:44 +02:00
Kaj SchittecatandClaude Opus 5 bc87645df7 console mode Phase 3: mem, contacts, chans, to, msg
* 'mem' prints free DRAM and PSRAM plus the largest PSRAM block. This is the
  number the whole feature is justified by, so it is a command rather than
  something you have to instrument a build to see: read it in the console,
  reboot into the UI, read it there. Phase 0, self-service.

* contacts / chans / to <name> / msg <text>.

The send path is REUSED, not reimplemented. luaHostMeshSendChannel matches the
channel by name at transmit time, and a cached slot index is how messages once
went out encrypted to the wrong channel; duplicating that logic here would have
been an invitation to reintroduce it. It was gated on CAP_LUA_SDK_EXT, which is
off on the V4 -- exactly the board console mode exists for -- so the guard is
now CAP_LUA_SDK_EXT || CAP_CONSOLE.

'to' stores a name rather than an index for the same reason: an index goes
stale the moment the contact list changes underneath it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 19:32:38 +02:00
Kaj SchittecatandClaude Opus 5 48dae32f8c console mode Phase 2: boot into it, and the way back out
* Prefs v51 adds console_mode. It goes at the ACTUAL tail of the struct, after
  lang_file. I first appended it after retry_echo, which is mid-struct and
  exactly the v44 mistake the schema comments warn about; the existing
  static_assert caught it at compile time. Added a second assert pinning
  console_mode immediately after lang_file, so the next person to append is told
  by the compiler rather than by a field of corrupted installs.

* The pref fails safe by construction: only the value 1 means console, so a
  corrupt or unreadable byte boots the graphical UI. The <51 migration forces it
  to 0 rather than inheriting whatever was in that position.

* UITask gains a headless personality rather than #if through main.cpp:
  begin() decides once from the pref and returns before lv_init() and before the
  draw buffer is allocated, loop() runs consoleLoop() instead of the graphical
  path, and showAlert() becomes a console line so main.cpp's ~20 Wi-Fi and
  Bluetooth reports keep working untouched.

* 'ui' or 'exit' in the console clears the pref and reboots. That is one of the
  three ways out in CONSOLE_MODE.md; the boot-key escape is next.

Also Phase 1's on-screen key grid, since the V4 cannot use console mode without
one: three layers (lower/upper/symbols) rather than a shift key that changes
every glyph, so a key's label is always what it types. Compiled out where
CAP_KEYBOARD is set.

All 9 S3 envs, the Tanmatsu and the T-Display P4 build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 18:29:26 +02:00
Kaj SchittecatandClaude Opus 5 e179653350 console mode Phase 1: the console front end (no LVGL)
src/ui-touch/ConsoleUI.{h,cpp}, new and self-contained. Draws straight to the
panel through DisplayDriver and reads input from the board drivers, so nothing
in it needs LVGL initialised.

What is here:
* Scrollback as a flat ring in PSRAM. No per-line allocation, so a chatty
  command cannot fragment the heap the way a strdup-per-line log would. Long
  replies wrap to the panel width rather than being truncated, because a reply
  that runs off the edge is the same as no reply on a screen this size.
* Render only when something changed. That is the point of the mode: an idle
  console costs a millis() comparison, not a render pass.
* Line height derived from the measured character cell (DisplayDriver has
  getTextWidth but no text height, and the drivers scale a fixed 6x8 cell), so
  it stays right at another scale instead of being a hardcoded guess.
* Commands go to the_mesh.runLocalCli(); replies arrive through the existing
  terminal sink. Only clear/help/ui are handled locally. Anything CommonCLI
  already answers belongs there, not reimplemented here.
* Touch: scroll strips wired against heltecV4CapTouchGetLive, guarded on
  CAP_TOUCH so the Tanmatsu (which excludes that driver entirely) still builds.
  The key grid is next.

CAP_CONSOLE is on wherever there is a DISPLAY_CLASS to draw on. It does NOT
imply booting into it; that is Phase 2.

Builds on all 5 S3 envs checked plus the Tanmatsu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:54:26 +02:00