A screen with room for nine lines or more shows each message bubble
fully wrapped instead of one line. The bubble's height is measured at
the width it is drawn at.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wrapLines carried the overflowing space's word to the next line too
early, so bubbles wrapped short of their width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each point keeps the GPS altitude in what was padding, so a point stays
16 bytes. The save format goes to version 2; version 1 files still load,
with the altitude unknown. GPX export writes <ele>.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SIM_EINK=landscape|portrait builds the e-ink firmware for the browser,
drawn by the same EinkGfxDisplay as the panel. index.html and mesh.html
pick a build from the URL. The simulated sensors get a GPS switch, so the
UI's GPS toggle works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The L1 e-ink solo build splits in two, one per screen shape, so each
screen can be laid out for a single shape. Settings > Rotation now only
turns the screen upside down, and the joystick turns with it.
Landscape writes in misc-fixed 8x13 at the panel's own resolution
(EINK_LARGE_FONT); portrait keeps the 6x9. All drawing moves into
EinkGfxDisplay, which the panel driver and the simulator share; the
FreeSans fonts and the CRC32 library go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
The Big / Compact lock look is ui-new's; the shared schema row did nothing
on the L2's slide-to-unlock card.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ui-core: Diagnostics::systemLines, NearbyModel::typeShort,
rptctl::currentPresetName, RoomSessions::pendingKey, settings::isSwitch,
UiCore::reconcileDMUnread.
- ui-new: the retired Recent page, fmtAlt, the battMvToPercent wrapper,
setFavouriteChannelSlot, AccordionList onHeader/visibleCount, drawBoxedIcon
and seven unused icons; info::row folded into info::valueRow.
- ui-lvgl: onOpenNav, TileDownloader::withTrails; the trail header size is
one constant (TRAIL_HDR) for live and saved trails.
- Home quick panels save prefs only when a value changed.
- Comments brought in line with the code (status bar reserve, settings rows,
page order, SH1106 frame skip, UiCore header).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The redraw, alert, splash and sensor timers compare millis() wrap-safe
(refreshDue(), alertShowing(); 0 = now / none), so the screen keeps
refreshing past the 49.7-day rollover.
- A failed send leaves the keyboard with its text ("Not sent" in the field
label, as alerts don't show over it) or the quick-message list with
"Send failed", instead of dropping to Home. No "Sent" echo.
- The dashboard's Sats field shows "--" with the GPS off, not a stale count.
- Brightness and volume in Settings stop at both ends instead of wrapping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Home clock page draws HH:MM in the boot splash's slanted digits, with a
seconds bar, a centred date and the clock fields as columns. The lock screen
keeps the status bar, puts the big clock under it and shows unread messages
in a pill; the unlock hint appears for a few seconds after a key press on the
lit screen (not the waking one). Settings > Display > Lock clock adds a
compact look: a small clock with the date beside it and the clock fields as
rows (NodePrefs lock_compact, schema 0xC0DE0033). 12-hour time sets AM / PM
beside the digits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Battery curve: NodePrefs batt_curve_mv[11] (voltage at 0..100 %, all
zeros = built-in LiPo curve, sentinel 0xC0DE0032). ui-core/Battery.h
uses it for every percentage. L1: Settings > System > Batt curve list
(Left/Right 10 mV, Reset); L2: Settings > Power > Battery curve chart
with draggable points and a line at the cell's voltage.
- L2: the Nodes map is the full navigation map plus the nodes, Back
returns to Nodes; the favourite button is lit while it is one; pinning
to the Home dial uses a thumbtack (fonts regenerated); minimap dots
follow the accent; arranging tiles shows where they land while dragging;
emoji row in Diagnostics > Font.
- Sim hooks: sim_open nodesmap / accent@N / homeedit / battcurve.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base class matched an ACK carried in a PATH packet without going
through onAckRecv(), so the device UI's delivery marker never saw it and
kept resending the DM until a plain ACK arrived. MyMesh now passes it on
to the listener as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Icon pages become quick panels: two or three rows (Settings, Messages,
Bluetooth, Advert, Tools, Power) inset from the edges, the last one
opening the full screen. Brightness and auto-advert are edited in place
(Enter, Left/Right, Enter/Back saves once).
- Tools panel shows the two tools used last with their live state.
- Map page: preview on the left, GPS / trail / live / target on the right.
- Favourites as soft cards; Status tile with a battery spark line.
- Status bar without boxes or blinking; BT only when connected, GPS
search glyph without a fix.
- Redrawn mini icons (advert waves, cog, wrench, messages); big hovering
icons removed. Tools list is text only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
drawScreenHeader replaces drawCenteredHeader on every ui-new screen: the
title in normal case, page dots instead of "TRAIL 1/3", the menu hint, no
icon (the screen was just picked by one). Accordions in Settings and Tools
show a disclosure triangle instead of +/-, Tools sections drop their icons.
Form "[Save]" rows and "+ Send" share drawButton; form fields use valueRow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InfoKit gains valueRow (value at the right edge, cut and scrolling when
selected), switchRow and listRow (exact ON/OFF becomes a switch). Settings,
Live Share, Locator, Repeater, Bot, Auto-Advert, alarm, GPIO and popup value
rows use them; units after a space, "Off" in title case, Trail summary as
blocks with a big distance and a points meter, "+ Send" as a soft pill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home's Radio, GPS and Sensors pages become one Status page: four tiles split
by dotted rules (radio, GPS, battery, nodes), each a headline over a detail.
Enter opens the Status screen, tabs Radio / GPS / Sky / Power / Mesh /
System, which also takes over Tools > Diagnostics and Tools > Satellites
(the sky plot now lives in SkyView.h). GPS on/off moves to Enter on its tab.
New InfoKit.h holds the shared pieces, sized from the font: label/value row
(a long value takes its own line), centred section title, meter, chip, big
number with unit, switch, history line (one sample a minute) and a block
flow that scrolls a tab by whole blocks.
The Status page keeps the old Radio bit and shows when any of the three old
pages did; the save format is unchanged. The L1 degree sign was a cp437
byte the UTF-8 renderer drew as a box; now U+00B0. The perf walk covers
every Home page and Status tab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DisplayDriver gains slideBegin()/slideCompose(): the driver keeps the frame
on screen and, in the next one, moves it out while the new rows follow.
SH1106 does it on its 1 KB buffer (+1 KB RAM for the copy), the sim on its
canvas; drivers that can't read their frame back keep cutting over (e-ink).
Home slides the content below the page-icon row in 220 ms with an ease-out
(the whole screen to and from the header-less clock), and the current-page
pill glides between icons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Splash: the L2 wordmark and lettering, shared through ui-core/Lettering.h,
rise in with loading dots under the version line (static frame on e-ink).
- Home: the header shows the time once the clock is set; icon pages carry
only their title. One 32 px icon family (3 px strokes) bobs 2 px over a
dithered blob shadow that shrinks as it rises; the unread badge rides on
the Messages bubble; Bluetooth off is the icon at half density.
- Soft corners on every selection, the page pill, tabs and popups;
dotted scrollbar track with a rounded thumb; the tab bar lays out whole
tabs and shows edge arrows only in free space.
- DM bubbles drop the sender row and wrap to a second line, with the
delivery mark and age closing it.
- Keyboards name what is typed (channel, contact, Password, Scope...)
and show characters left near the limit; PIN entry has its own preview.
- Loading dots for waits (login, scan, GPS) and signal bars in place of
raw RSSI in Nearby.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DisplayDriver gets drawSoftRect / fillSoftRect / drawPanel (the unread badge's
corner knockout; a panel also clears a 1 px moat so lines below stop short of
its border). Used by alerts, popups, the accent picker, the lock hint, chat
bubbles and the send button.
Fixes: no "Msg:" toast for a message landing in the open thread; Nodes scan
text fits the screen; the PIN pad skips the blanks beside 0; Auto-Advert is a
plain value row; the Admin picker says what it needs when empty; the GPS page
shows "-" instead of 999 before a first fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opens each screen directly, types keys, and prints per step over USB: frames
drawn/sent, render and flush time (CPU cycle counter), heap used/free/largest
block, every task's stack headroom, and a screen dump. Measurement only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each frame is hashed per 32-column block of each 8-px page and only changed
blocks go over I2C. A full frame was 1 KB / 31 ms on every change; a moved
selection, a scrolling name or a clock tick is now 1-8 ms. 128 B of hashes,
no frame copy; full sends stay for wake/clear and SPI panels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New message, Nodes (and its Scan popup), Admin and Messages show their
items through one virtual list: a screenful of rows, handed to the next
item as one scrolls out, the scroll extent from the items' heights. 256
contacts: 522 objects and ~950 ms of filling in (frames up to 96 ms)
before, 19 objects at once now; a scrolled frame 28.5 -> 11.7 ms. Nodes
257 -> 49 objects. A new message rewrites Messages' rows in place
(46 -> 20 ms) instead of building the screen again; it's rebuilt only
when "Read all" comes or goes. A held row keeps what it shows until let
go. Perf tools: -D PERF_LISTS, -D PERF_LIST_MUL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows still to build wait while a screen slides in, so its frames aren't
a fill budget apart (Nodes: 12 frames of 24 ms instead of 7 of 33, up to
65). The crash dump in flash is read at boot, not on the first
Diagnostics (build 54 -> 20 ms). The perf tools time Nodes too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel takes each 24-line band by DMA while LVGL draws the next one
into the other buffer (two 15 KB buffers in internal RAM; LovyanGFX's
4092-byte DMA descriptors leave PSRAM unaligned for the S3's DMA, which
showed as shifted and smeared bands). LVGL draws in the panel's byte
order, and so do map tiles and the frozen screen under a popup, so
nothing is swapped on the way out. Full frames ~10-25% faster (page1
19.9 -> 14.7 ms, Home 21.4 -> 18.2 ms). Without the internal RAM the
screen is sent as before. The arduino2 env keeps the old way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recording / live share and the download stack in a column at the top
centre, empty when nothing runs; the recording pill is plain text with
only its mark coloured (red stop, accent pause / live) -- all-red text
was hard to read over the map. The credit, scale bar and zoom share one
pill bottom left (a tap shows the credit). Every map pill is 22 px with
round ends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A popup over a still screen is frozen and dimmed as it opens, so the
panel's rise redraws only the panel: 6-8 frames of 19-25 ms instead of
3-4 of 45-65 ms. Picker options are one label each (time zone list
97 -> 52 ms to build). The bottom popup no longer animates a dim of 0.
Repeater updates its hint in place (130 ms rebuild gone); Auto power,
Imperial units and Relay on rebuild the screen after the knob has
moved (rebuildSoon). -D PERF_CONTROLS times every control on the
settings screens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 404/503 answer (OpenTopoMap renders on demand) went to an immediate
2 s retry and counted as network loss, so each tile then waited up to
60 s. Answered errors now go straight to the retry list; long pauses
only when there is no answer at all. Three retry rounds (20/60/120 s)
for the tiles still missing, transfers cut after 30 s, failures logged
on serial.
Download popup tidied: title + floating close, percentage instead of
tile counts (also on the map pill), no dead zoom row, no finish toast
after Stop or while the popup shows the result.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A group row's text column (and an info row's key) grows into what the
controls on its right leave, and LVGL lays out children before their
parent, so its labels were first measured 1 px wide; LVGL also works out a
new label's height at its old width (0) before the real one. Each height
that changed then took another pass up the screen: 4-6 passes per screen.
rowsFit(), run before a frame's layout (LV_EVENT_REFR_START) and by
layoutNow() in place of every explicit lv_obj_update_layout(), gives each
new row's growing child the width the flex will give it, worked out from
the other children's styles and text, and labelFit() measures wrapping
labels at their real width up front. The grow stays, so a control that
can't be sized ahead only falls back to the old passes.
Layout on the device: Display page 34 -> 24 ms, Bot 33 -> 18, Quick
37 -> 22, Radio 26 -> 18, Diagnostics 20 -> 12. Sim screenshots of 26
screens unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An infinite lv_roller measures its whole option text (repeated three
times) on every style change, layout pass and frame. The hand-drawn wheel
draws only the visible rows, with the same look, drag, throw, tap and
wrap-around. Clock first frame 99 -> 54 ms (layout 42 -> 12 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The framework libs are rebuilt for the L2's own ESP-IDF options
(solo/wio-tracker-l2/idf/sdkconfig): a 32 KB instruction cache and
64-byte data cache lines make frames ~35% faster (map 45 -> 30 ms,
settings ~36 -> 22 ms). mbedTLS and the NimBLE host allocate in PSRAM,
so internal RAM ends up with more free than before (103 KB, largest
block 57 KB; was 89.6 / 45).
Wio_Tracker_L2_idf_libs does the rebuild; it has to run before the
firmware env, also in CI. idf/arduino_tinyusb gives the rebuild the
TinyUSB options and the framework's prebuilt library, which it would
otherwise lose.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UiCore::setGpsEnabled() wrote the prefs to flash at once, which stalled
the loop mid-animation. It takes save = false now (default true, the L1
unchanged); the L2 marks the prefs changed and writes them on leaving
the screen, as its other switches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vector map isn't finished: without the flag it stays off whatever
was saved, and its switch and the Vector regions row are gone from the
map tools. The code stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by counting flushes on idle screens in the sim:
- The compose field's cursor blinked (a redraw every 400 ms) as soon as
a conversation opened, the keyboard hidden: lv_keyboard_set_textarea()
focuses the field, and LVGL only stops a blink when the cursor moves.
fieldFocus() sets the state and nudges the blink; also used for the
WiFi and channel fields, which left theirs running.
- lv_obj_align_to() restyles on every call, so the status bar's icons
redrew every second; placeLeftOf() moves them only when they move.
- Text colours set every second (nav bar, GPS status, Diag errors) go
through setTextColor(), which skips an unchanged colour.
- A LV_LABEL_LONG_DOT label writes its "..." into its own text, so
setText() never matched and rewrote it every refresh (the nav bar's
hint); labelShows() reads past the dots.
- The map area, the area-outline, trail and vector-label layers were
invalidated on every map refresh (every 3 s, a full-map redraw); now
only when the view or what they draw changed.
Idle map: 17 -> 1 flush in 5 s; list and settings screens: 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor has its own 80 % dim layer, not a popupOpen() one, so every
key press redrew the list under it and blended the dim over it. It now
freezes the screen like the popups (freeze::now); the frozen image takes
the dim layer's own level instead of assuming 60 %.
Keyboard redraw 38.7 -> 29.7 ms, full frame 46.3 -> 39.7 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The map screen's own fill is only painted where the map area doesn't
cover it (the strip it uncovers as it drifts in); the area and the
opaque status bar hide the rest. ~2 ms a frame.
- With every visible cell holding a tile, the map area drops its fill and
grid, which lie under the tiles anyway, and still reports that it
covers what's below (coversOwnArea, shared with the frozen popup
image). The Home minimap drops them the same way. ~5 ms a frame.
Full map frame ~50 -> 46-48 ms on the L2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kept for the optimisation pass before a release:
- perfDrawProfile(): a full redraw timed per object (its own drawing),
totals by kind and the costliest listed -- on settings pages, Contacts,
Home, the map and under a frozen popup; the screen's entrance frames too.
- Per screen: build, layout, first draw and redraw apart.
- A popup over Home: a full frame dimmed live and frozen.
- The reset reason and a core dump summary at start.
- -D PERF_WRAP_TEXT with -Wl,--wrap= of lv_text_get_size_attributes,
lv_layout_apply, lv_obj_get_style_prop_internal and lv_obj_send_event:
text measured, flex runs, style reads and events per screen.
- -D PERF_KEEP_USB leaves the USB popup up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once a popup's dim is up, the screen below is drawn once into an image
(LV_USE_SNAPSHOT), darkened as the dim layer darkens it, and shown opaque
over the screen with the dim layer cleared. LVGL then starts drawing from
that image; an lv_image with no background never reports that it covers,
so it answers the cover check itself. A full frame under a popup: 45.7 ->
32.6 ms; freezing costs ~29 ms once, as the popup settles.
One popup at a time (a second one dims live), a strip keeps the status
bar dimmed under top-layer popups, the image goes with its popup, and the
popup dims live again if its screen goes first. POP_BOTTOM (over a live
map) isn't frozen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LVGL lays out children before their parent, and a new box's size is only
known after a pass, so every level sized in % of the one above cost a whole
extra layout pass over the screen. innerW() works the width out from the
styles instead; fillWidth() sets it on infoCard, infoLine (every row),
noteLabel and scrollList, and the screen body is px-wide.
Display & power page: 101 -> 76 flex runs, layout 68 -> 54 ms, first
frame 161 -> 146 ms; the other settings pages and Radio / Bot / Quick
alike. Four passes remain: a row's text column grows beside its control,
so its width is only known after the row's own layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on the L2 (steady full frames): Settings page 37.7 -> 31.4 ms,
Display 41.5 -> 37.9, Contacts 34.6 -> 32.9, Home 37.3 -> 32.4.
- styleSurface() leaves boxes in the page colour unpainted; styleOpaque()
for the screen and the status bar, which things slide under.
- group() cards clip their corners only while a row is held
(rowPressable(), shared by the tappable rows).
- rowSlider(): the track an opaque tint (theme::mix(), also behind
ACCENT_DIM) instead of 20 % accent blended every frame.
- Map buttons, pills, scale, nav bar and the Home minimap labels opaque.
- Screens drift in without the fading cover: its full-screen blend was in
every entrance frame (page 39 -> 29 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PNGdec (bitbank2, 1.1.6) decodes a tile a row at a time straight into
RGB565, without lodepng's 256 KB RGBA buffer; the trail overlay goes through
it too, blended by hand for palette-with-alpha and RGBA. It lives in its own
file (ui-lvgl/TilePng.cpp) because its zlib headers put DONE/EXTRA in the
global namespace. lodepng stays the fallback and the sim's decoder.
Same 20 tiles: decode 78 -> 50 ms, trails 42 -> 17.5 ms, a whole tile
170 -> 105 ms; the map with trails is pixel-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list fill becomes a small job queue (fill/then/fillStart, fillTick with a
25 ms budget per pass). Settings pages build section by section in
schemaRows, with the trailing extras (melodies, prune, quick messages) queued
after them so the order stays; Radio and Repeater build by group, Admin's
repeater pick like the contact list, Diag's Live tab by section. Rebuilds
that restore the scroll flush the queue first. Screens went from 220-360 ms
to 80-175 ms (build + first frame).
Diag's crash line read the core dump in flash every second (~44 ms each);
it is read once now. The perf run times every page and dismisses the USB
popup, which had covered all earlier measurements.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A widget costs ~1 ms on the L2, so Nodes with 48 rows held the screen for
~600 ms and a full contact list for over a second. fillStart() builds the
first screenful at once and fillTick() adds the rest ~25 ms a pass.
- Nodes: 6 rows first; the 2 s refresh rewrites built rows in place
- New message: contacts filled the same way
- Settings: a group at a time (all at once when coming back scrolled)
- Nodes 671 -> ~200 ms, Settings 376 -> ~190 ms longest UI stall
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- TileCache: request() hands z/x/y to a task on core 0, which renders into
a spare buffer; poll() swaps it into the LRU slot (stale results dropped
after drop()/invalidate()); the sim runs the job at once
- MapScreen / Home map place finished tiles as they arrive; the longest
UI stall with the map opening goes from ~340 to ~170 ms
- LVGL allocates from the PSRAM heap (lv_mem_psram.h), safe from both
tasks; LVGL's own pool is gone
- lv_conf: notes why LV_OS_FREERTOS with two draw units stays off
(measured slower: drawing is bound by PSRAM bandwidth)
- UI_PERF_TEST: the map (tile read/decode, pan, redraw), build time and
the longest loop pass per screen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trail on the L2: 32768 points every 5 m, no simplification, no point
spacing setting; copied to the card every minute and restored at power-on.
The line keeps up with the position dot (incremental sync, pending point).
Map: a tap's finger jitter no longer turns follow off; the GPS button
shows when the map follows. The trail is drawn as one polyline with
breaks and off-view chunks skipped (an lv_line queued a draw task per
point).
Fewer rebuilds: conversation bubbles patched (new messages, delivery
marks), home mini-map dots reused, Nearby rows rewritten in place,
history read in runs and each file opened once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
503/429 slow the pace down, transport errors retry after 2/8/30 s, failed
tiles get a second pass; the download only gives up after 10 minutes
without a single tile or on repeated 403s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CMD_GET_SCREENSHOT on colour screens: display type 2 (RGB565 LE),
32-bit payload size; ui-lvgl captureFrame() shared with the SD BMP
- ENABLE_SCREENSHOT in the L2 build; tools/screenshot.py decodes type 2
- docs and READMEs point to solo.marekzegarek.com/#pc-tools
- USB popup: "Keep card" instead of "Charge only" (only the card is lent;
charging and USB serial work either way)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CPU at 80 MHz with the screen on after 2 s without touch or animation;
a touch restores the full clock before its frame is drawn
- setText(): periodic refreshes no longer redraw unchanged labels
(status bar, Home, lock, Diag, GPS, compass, clock, map, nav, nearby)
- map markers kept across the 3 s rebuild when unchanged
- app connected: the loop blocks until a BLE frame arrives
(serialFrameArrived / hasPendingFrames) instead of polling every 2 ms
- PSRAM: boot frame sprite and the HTTP response buffer released
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The WiFi and Bluetooth rows rebuilt the whole Settings page to update
their hint, cutting the switch's animation short; the hint is now
rewritten in place (rowHintSet). The "Bluetooth on" echo toast is gone.
- Map download: a 404 gets the same one retry as other errors
(OpenTopoMap sends the odd 404 for a tile not rendered yet), and a
download that ends with gaps says "Done, N missing - download again".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- build-solo-sim.yml clones LVGL at the version pinned in
solo/wio-tracker-l2/platformio.ini and builds the SIM_UI=lvgl sim into
wasm/lvgl/ of solo-sim-wasm.zip, for the site's device switch.
- build_wasm.sh: LVGL_DIR can be overridden (CI has no .pio libdeps).
- sim_test_set_node_name(): names a sim node, for site screenshots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Short pages by feature (getting started, messages, contacts, navigation,
tools, settings, lock, hardware) instead of one page per OLED screen, with
Wio Tracker L2 differences as notes; facts checked against the code
(keyboard scripts vs. hold-for-accents, {batt}/{dist}, L2 limits and
settings layout). Developer guides and build flags under docs/solo/developer.
Old docs/solo_features and its ~50 screenshots removed; a hero banner and
three simulator screenshots added; links updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Working plans (docs/development, docs/design redesign notes) removed; the
two developer guides moved to docs/developer/. README cut to devices,
flashing, links and building (Wio Tracker L2 added); its feature list and
doc tables moved to docs/solo_features/README.md. RELEASE.md describes the
Solo release flow. Upstream's GitHub Pages workflow (docs.meshcore.io)
dropped, CLAUDE.md untracked, Buy Me a Coffee back in FUNDING.yml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The phone app's offline queue took 256 x 177 B = 45 KB, which with the
on-device UI left the L1 ~5 KB of heap from boot (the sign buffer of #38
never fit). The UI keeps its own message history, and a full queue drops
the oldest channel messages first, so 128 is plenty: the L1 heap now has
~27 KB free. L1 OLED / e-ink, ProMicro, T-Echo Lite, both GAT562; the
upstream variants/ are unchanged (build_unflags in solo/).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SD card (message history) could be lent to a computer from the lock
card before the PIN was entered. While locked with a PIN, usbPoll() no
longer reacts; a cable plugged in meanwhile is offered once unlocked.
The plain-text NVS PIN never shipped, so there's nothing to migrate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since bf1e6fca kbApplyCapsUtf8 wanted 4 bytes free before each codepoint;
the ABC grid passes a 3-byte cell buffer, so every cell came out empty (T9
and CardKB Compact draw no such cells, so it went unnoticed). A codepoint
is now copied when it fits with the NUL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contributor's feature -- a PIN asked before the screen unlocks, on
boot, and when the magnet cover opens, stored as a salted SHA-256 -- on
top of this branch, with:
- ui-core/ScreenLock.h: set / check / isSet and the tries limit, shared by
L1 and L2. isSet looks at every byte (the PR tested only the first, so one
PIN in 256 silently disabled the lock). 5 misses pause entry for 30 s.
- NodePrefs: the fields at the tail after quiet hours, sentinel 0xC0DE0031,
sizeof 2880; an older file's bytes there are cleared. A sentinel bump no
longer resets display_brightness_pct from a file that already has it.
- DataStore: no AGPL header (the project is MIT), no whitespace churn.
- Keyboard: the number pad is a widget flag (pin_mode) with a prompt in the
preview ("New PIN", "Again", "PIN"), not keyboard_type = 2 written into the
prefs and restored after.
- L1 Settings: "Lock PIN" right after the schema's "Lock screen" row, typed
twice, at least 4 characters, saved at once (a power-off right after
setting it kept no PIN). Home is told about a boot-time lock once it
exists (also fixes booting with the cover closed).
- L2: the screen PIN moves from plain text in NVS to the same hash in
NodePrefs; the old one is migrated on first boot and removed. The pad
unlocks as soon as the hash matches from 4 digits on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CMD_SIGN_START promised the app 8K before a malloc it never checked. On
the L1 the nRF52 heap has ~5K free from boot, so the malloc always failed
and every CMD_SIGN_DATA answered ERR_CODE_BAD_STATE. Now it allocates
first, halving down to 256 B, and reports the size it got; nothing at all
is an error on SIGN_START itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- L1's MessagesScreen sends through UiCore::sendDirectText / sendChannelText
(its own copy of send + file + ACK tracking gone).
- ui-core/Telemetry.h: the dashboard fields (numbers, names, formats) and
sensor readings as text, each frontend passing its Style; L2 Home, L1's
clock page, lock screen, sensors page and GPS altitude use it.
- L2: the side button held in the first 8 s is the CLI rescue, as on L1.
- L1's repeater radio goes through rptctl.
- L1 Settings are built from SettingsSchema: short labels for small
screens, SCHEMA_* placeholders expand to the schema's sections, one
generic row (switch ON/OFF, bars for brightness / volume). 20 hand-written
rows and their tables gone; L1 implements applyDisplayPrefs /
applySoundPrefs. settings::text / step / indexOf shared with L2.
- Schema: auto-off gains 5 s; the advert sound scope reads "Direct".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dead code gone: anim::fadeHide, radioctl::presetCount, the L2 board's
setSdPower / setGrovePower / expanderOK (awRead, gnssReset now private).
- buttonBar() / barButton() replace 7 copies of the popup button row.
- confirmBody(): one red-button confirm for reboot / power off, reset
counters, admin commands, preset and scope delete.
- tapConfirmed(): the 11 "tap again" buttons share one mechanism, 3 s
everywhere, the label restored when left alone.
- noteLabel() for ~30 wrapped notes.
- localTm() in NodePrefs.h and one MONTHS table: lvgl, MsgExpand, the L1
clock and lock screen.
- The sim keeps UI settings in an in-memory nvs::, so one set of load /
save helpers serves both.
- Asleep, the loop wakes every 50 ms (the button / touch poll), not 20.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings > Sound > Quiet hours (L2: its own group; L1: Quiet hrs / from /
until): from quiet_from to quiet_to (local hours, wrapping past midnight)
the buzzer is muted and a message doesn't wake the screen; the clock alarm
still rings. A manual mute or unmute inside the window stands until the
window ends. Off with the clock unset.
NodePrefs gains quiet_hours / quiet_from / quiet_to (appended to the file,
sentinel 0xC0DE0030; sizeof 2832 on nRF52 and ESP32), off by default,
22:00-07:00. hourInWindow() / localHour() are shared with the bot's quiet
hours. soundctl::tick() replaces autoTick() (Auto and quiet hours).
Also fixes the L2 side-button mute: it went by the mode, not by whether
the buzzer was muted, so with Auto + an app connected (and now quiet hours)
the first press muted instead of unmuting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Noise tab held the L2's interference hunt: parts of the board off one
by one, a slow cycle for a second radio, a spike hunt and three sweeps
(~90 s, L2 only). It is now one Measure: the noise floor on the mesh
frequency (median and 10th percentile of 5 s) and a +-1.1 MHz sweep round
it, ~12 s, with a short legend for reading it. Works wherever the variant
defines the radio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Map tools: TRAIL, LIVE SHARE and ARRIVAL ALERT each with their own options
page (that section of Map options; back returns to Map tools), the arrival
alert switch where its state is shown, OFFLINE MAPS (Map areas, Live
tiles; the separate Download an area is gone -- a running or unfinished
download shows at the top of Map areas) and LAYERS.
Map areas: picking one frames it on the map above a small sheet (no Show
button, the map above isn't dimmed); closing the sheet, however it's
closed, puts the map back where it was and drops the outline -- it used
to stay on the map. New popup kind POP_BOTTOM.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI runs on Arduino's loop task, whose stock 8 KB stack had 340 B left
at its deepest point; a screenshot's file write went past it and reset the
device. The loop stack is now 16 KB (measured peak ~7.8 KB) and the
screenshot's line buffer is static. Diagnostics names the row
"UI stack free (lowest)".
With a cable in at boot, the USB popup waited for nothing and showed over
the splash; it now comes once the splash has faded.
Adds docs/development/plan-next.md, the plan up to the merge into dev.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hold an app on Home (or Settings > Home apps) to arrange them: every app
is shown, hidden ones dimmed with a +; a drag drops the app into the slot
under the finger, held at a screen edge it goes to that page; a tap hides
or shows it. Settings stays shown. The order is kept in NVS (letters per
app, lower case = hidden), saved on Done or on leaving Home.
Roadmap: note the speaker-knock experiment (DAC ramp didn't help).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- USB through TinyUSB (ARDUINO_USB_MODE=0) instead of USB-Serial/JTAG: the
serial port stays, and plugged into a computer (enumerated, the bus not
suspended -- on battery an unplug only shows as a suspend) the device asks
"Charge only / USB drive". As a drive the firmware lets go of the card and
the computer gets its sectors; ejected or unplugged, the device restarts.
- Incoming messages: a banner slides down from under the status bar (icon,
sender or channel, two lines of the text from history); a tap opens the
conversation (not while locked), a swipe up dismisses it, not shown for
the conversation already open. Toasts moved up there too, under the banner
when both are up; the ones only echoing a switch or a choice are gone.
- Storage > Format card: a popup saying what goes, then Format tapped twice;
one FAT32 volume over the whole card (readable or not), then a restart.
- A deliberate restart shows a full "Restarting..." screen with the reason
instead of the screen just freezing.
- The screenshot flash is a thin frame round the edges: a full-screen fade
redrew all 320x240 each step and tore.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two views, swapped with left / right: a polar sky plot (filled = used in the
fix, hollow = tracked, a pixel = no signal) beside fix, used / in view, HDOP,
PDOP and time to first fix; and a signal bar per satellite (used first, a
dotted 30 dB-Hz line) under per-constellation counts. The GPS stays awake
while it's open. Data from GpsSky (-D GPS_SKYVIEW, now on the L1 solo build).
The simulator's made-up sky moves from the L2's GpsScreen into
helpers/sensors/GpsSkySim.h, so both sims show it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Settings > WiFi keeps up to 8 networks (NVS "s0".."s7" / "p0".."p7"; the
single one older builds saved moves over). A tap loads one to change its
password, Forget asks with a second tap, a scan marks the known ones.
- Map downloads, live tiles and updates join the strongest saved network in
range (a short scan first when more than one is saved), else the newest
one -- which also covers a hidden network.
- Save joins the network once: "password OK", turned away (forgotten again,
the form keeps it to fix) or not in range (kept, checked later).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Side button held + top button saves the screen, popups and toasts included,
to /sdcard/screenshots/scr_NNNN.bmp (24-bit BMP). The flush callbacks copy
one forced full re-render into a PSRAM buffer. A white flash and a toast
confirm it. The side button's mute now fires on release, so holding it a
little long before the top button doesn't also mute. In the sim, the page's
Screenshot button downloads the file.
- The lock screen goes dark after 30 s even with auto-off at Never (a boot
PIN, a pocket wake-up, a message), and sooner if auto-off is shorter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
helpers/esp32/InternalFS.h picks the filesystem: -D ESP32_LITTLEFS mounts
LittleFS on the same "spiffs" partition, otherwise SPIFFS as before. Only the
L2 LVGL envs set it; every other ESP32 board is unchanged. A settings save
drops from ~0.5 s to ~80 ms and the files survive power loss mid-write.
No migration: the first boot formats the partition (fresh identity).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every toggle / stepper wrote the prefs file straight away (0.1-0.6 s on the
ESP32's SPIFFS), which stuttered the UI and wore the flash. prefsSave() now
only marks them dirty; they're written once when the screen is left, when the
display goes off, or after 60 s at the latest -- the L1's approach. Shutdown
and the pre-restart OTA save still write immediately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It was a muted prefix on the date line, easy to miss. clockFace() -- the
digits and a small AM / PM on their baseline -- is shared by Home and both
lock screens (the PIN card showed no AM / PM at all). The date loses the
prefix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The triangle draw descriptor's colour fields lost their bg_ prefix and style
getters take an lv_part_t. 9.6 sizes flex containers through the layout, so
popups no longer shrink when pulled past their end: lvgl_patches.py goes.
The sim wipes its LVGL object cache when the library version changes.
Flash +73 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While nothing is connected, every 5 s: if advertising has stopped (a host
reset ends it with no disconnect to restart it), start it again. A link up
for 60 s without pairing is dropped, so advertising comes back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LVGL 9.2.2 measures a SIZE_CONTENT box from its children as scrolled, so an
elastic pull past the end of a capped popup list shrank it by the overshoot,
again on every pull. lvgl_patches.py fixes calc_content_height in the
downloaded library (a build middleware for PlatformIO, called directly by the
sim's build_wasm.sh).
The five popups that each built the same panel now share popupOpen() (fit to
content / full height / under the status bar).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spike (a throwaway ui-oled frontend, not kept) ran in the simulator but
never got past building its screens on the L1: LVGL's pool and stack sit
outside the heap and UiCore needs 27 KB of it in one piece. Flash was fine.
Decision: no LVGL on the nRF52 boards; the v2 look goes into ui-new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GPS trouble on the cable was the I2S MCLK comb (fixed in b50ecc83):
fix times match with and without USB now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- BLE (ESP32): fast advertising (20-30 ms) for 30 s, then 152.5 ms, as the
nRF52 interface; the library default kept 30-60 ms forever
- Live map tiles: the WiFi drops after a minute with nothing missing
- ES8311 to standby while the I2S clocks are off, set up again before the
amp comes on
- Buzzer: no channel (DMA out of memory) waits out the samples instead of
spinning above the UI loop
- OTA: the image header check waits for its 16 bytes
- Comments: P10 is just the LED; the LCD SPI clock is 40 MHz on the wire
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The LoRa noise floor with the board's parts off one at a time, sweeps of
850-930 MHz, the mesh channel and a 7.8 kHz zoom on its loudest point; a
slow cycle of the same states for a second radio beside the device; and
a spike hunt over 864-876 MHz listing the lines over the floor and their
spacing (how the I2S MCLK comb was found).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The speaker's I2S channel was only disabled when idle, which leaves MCLK
running. Its fractional divider (160 MHz / 39 1/16 = 4.096 MHz) makes a
comb of lines every 256 kHz: one at 869.632 MHz, in the EU narrow mesh
channel, lifted the LoRa noise floor from ~-102 to ~-85 dBm, and others
land near GPS L1 and BeiDou B1. The channel is now deleted and its pins
held low after each sound, and made again for the next one. Measured:
noise floor -102 dBm, outdoor first fix 87 s (was 620 s and more).
Also: TCXO at 3.0 V (the L2's part, as Meshtastic has it); GLONASS
enabled on the L76K after each power-up; begin() on a running receiver
no longer resets it (a reset threw away the acquisition in progress).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One .vpk file per region in /vmap (header, sorted index of tiles and
points, then the data), read in place with the index in PSRAM; loose
{dz}/{x}/{y}.vt/.vp files still work. osm_vector.py writes packs
(--pack/--name), reads OSM PBF extracts for a box (--pbf/--bbox, via
pyosmium) and picks label languages (--lang). Map tools > Vector regions
lists the packs on the card: show one on the map, delete it. No
on-device download: packs are made with the tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
osm_vector.py --dem adds contours from the Terrain Tiles on AWS Open Data
(SRTM, ~25 m; tools/maps/dem.py, standard library only: PNG decode,
smoothing, marching squares, line joining). Every 100 m from zoom 12,
every 20 m from zoom 15; the credit names the elevation source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hiking routes are stored per stretch with the waymark colours of every
route along it and drawn as side-by-side stripes on a white band (half
the route ways in the Tatra sample carry two or more routes), less
simplified; demanding / alpine paths are dotted. Named points (places,
peaks with height, huts, passes, springs, viewpoints, caves) come in
small .vp files and are placed by priority as a layer over the tiles,
clear of each other and of the map controls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Points as zigzag varint deltas, a byte length per feature to skip it,
natural land cover simplified harder: the Tatra sample goes from 4.4 MB
to 2.3 MB, panning at z14 from ~63 to ~45 ms per tile on the L2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/maps/osm_vector.py turns Overpass JSON into VT2 tiles (data zooms
10/12/14, per-feature bbox, simplified); VectorTileProvider draws them
into 256 px tiles (scanline polygons, thick lines with round joins) with
a small PSRAM cache of data files. Behind Map tools > Vector map (test),
raster where there is no vector data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Download an area: a frame with four corner handles on the map (kept in
world coordinates, the map pans / zooms under it); a bar gives its size,
zoom range, tile count and size, the trails switch and Get; areas on the
card are outlined
- Map tools > Map areas: the downloaded areas (map/AreaStore.h,
/sdcard/maps/areas.txt) -- show (framed, outlined), rename, fill gaps,
refresh (force re-download), trails on / off, delete (in steps, keeping
tiles another area covers)
- TileDownloader: force and per-job trails options
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A square grid drawn under the tiles (map, Nodes map, Home minimap), its step
a round distance for the zoom in metric or imperial units, with a scale bar
one step long; gives position and scale where no tile is loaded. The
missing-tiles hint moves up, off own position.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Waymarked Trails' transparent hiking layer, kept in /sdcard/maps-trails and
blended over the base tile at render time (Map tools > Hiking trails, NVS).
An area download fetches each tile's trails after its base tile, so
re-running an area on the card adds just the trails; with live tiles on, a
shown tile missing its trails has them fetched. Tiles without a trail are
stored as 0-byte files (no cluster on FAT).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Messages: New message moves to the header; Channels / Direct / Rooms fold
on a tap of their title (kept in NVS), a folded one shows its unread count
- Storage: the card's bar is split into one coloured segment per category
(plus the uncounted rest), each category row keyed with its colour
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unread badge on Home tiles sits in the top-right corner, clear of the icon
- Minimap with no GPS fix: the saved position (hollow dot), else where the
map was left, else the default view; a hint when no tiles cover the area
- Screen header 38 px, so the back and header buttons get a margin
- Bluetooth / WiFi connected shown in the accent colour; charging bolt green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home pages: favourites, the clock (tap for Clock) with three telemetry
fields picked by holding one (shared dashboard_fields), a minimap that
frames you, live shares and the target (tap for the Navigation map), and
the apps in 3x2 pages. The side button now returns to the clock page with
a cross-fade instead of going back. Home moves to HomeScreen.h.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
group() / settingRow / listRow / actionRow / choiceRow / groupNote: one card
per section, hairlines between rows, hints that wrap. Dropdowns replaced by
a centred picker list. Settings regrouped (Device, Connections, Map & data,
System); Display & power split into Display, Power and Time on L2 only
(the shared schema is unchanged), Keyboard and About get their own pages.
Radio and Repeater share one LoRa parameter block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Info cards drop the default 8 px row gap so text sits centred between
hairlines. The alarm Repeat list is compacted to fit above the dropdown.
creditText() keeps "(c) Name" and licence names like CC-BY-SA together
in About and the map credits toast.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared infoCard/infoRow/infoNote helpers: muted label left, value right,
hairlines between rows, empty rows hidden. Diagnostics grouped into
sections with a received/sent packet table; GPS gets a position card and
constellation pills; About, OTA, Storage and map popups use the same
cards. Text no longer breaks after '.', clock alarm hint unclipped.
Sim-only sim_open() hook for jumping to a screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Power: the UI loop sleeps until something is due (never with packets
queued); screen off puts the CPU at 80 MHz, the GT911 to sleep (unless a
tap wakes the screen) and the LP5814 in standby. GPS off now cuts the
L76K's rail and UART on the L2 (L2GpsProvider). The battery divider is
powered only for a reading (measured: settles at once); readings within
2 s are reused. The SD mount is retried at most every 5 s.
Fixes: WiFi left on after leaving a scan, live tiles' WiFi kept with the
screen off on the map, an ADC read every second for the status bar, touch
polled on every pass while dark, a scan cutting an update's WiFi, relay
count not refreshing in a channel thread, raw @[nick] in room previews.
Labels skip unchanged text (no redraw).
Helpers: scrollList, dimOverlay, settingRow, flexBox, nvs::get/put,
trailFilePath (a second save in the same minute no longer overwrites),
unreadTotal. Dead code removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mentions: "@[nick]" in message text becomes "@nick" in the accent colour
(a span group); a mention of us is underlined and its bubble outlined.
Chat previews and the message popup's quote show "@nick" as well.
Emoji: every single-codepoint Twemoji and every flag, 16 px RGB565A8 in
flash, as the fallback font of each text size (fonts/ui_emoji.c, data
generated by fonts/emoji.py). Joiners, selectors and skin tones take no
space; a regional-indicator pair is swapped for the flag's private
codepoint before text reaches a label. Twemoji credit in Settings > About.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>