ui-new: Status › System, Enter shows it full screen. L2: Settings › About.
One precomputed 25 x 25 matrix (ui-core/SiteQr.h) for both, so no QR
library; the URL in upper case keeps it at version 2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hashtag channel names already start with '#'; the tiles added another
("##test"). chanctl::bareName() is now the one place that strips it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both UIs: a channel, DM or room with unread messages opens on the oldest of
them (MessageHistory::firstUnreadChannel / firstUnreadDm, own messages not
counted), a "New" line above it, so it reads down to the newest. Nothing
unread: the newest, as before.
L1: the first unread is selected and scrolled up top with the line in view;
the channel's unread count now counts down as you read towards the newest.
L2: the page holding it, scrolled to the line; an incoming message scrolls
down only when the list is already at its end. Sim hooks: sim_unread (L1),
sim_open("unread") (L2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A pending DM shows a dot per send, as on the L1. The thread is read from
the SD archive, which a resend never updated: it does now, and the
thread's signature and patch state count the attempt.
- Nearby redraws when a node goes live, changes type or the units change;
the scan popup when a name or the remote SNR arrives.
- The user LED is active high: it was lit all the time and went dark
while transmitting.
- Sim hooks "dm" and "send" for testing a DM thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Display > Message alert: Normal keeps the sliding banner; Compact shows an
envelope with the unread count beside the battery (in the header line where
there's no status row), blinking for 2 s; Off shows nothing. The blink only
ever brings the next frame earlier, so it no longer holds back the UI's
animations while it runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The limit is what the send leaves of MAX_TEXT_LEN: a channel post carries
"<node name>: ", a direct message resent more than three times keeps two
bytes spare, and placeholders count as they expand (msgtext::limit/sentLen).
Bytes, not characters. The path and packet header sit outside the payload;
static_asserts keep a full message within the radio frame on the longest path.
From 40 bytes left the count shows: on the L2 as a tag on the field's top
edge, on OLED/e-ink at the right of the preview's last line with the text
wrapped short of it. Input stops at the limit; a send still over (a
placeholder grew) is held. Sim hooks to open a compose with text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/prefs holds a header (magic, format version, size, CRC-32), the node's
position and NodePrefs as it is in memory, in place of /new_prefs' two
hand-kept field lists and 0xC0DE00xx schema bumps. A new field goes at the
end of the struct; moving one bumps PREFS_VERSION. sanitize() keeps every
value in range.
The first boot after an update from before 2.0 imports only the first
block of the old file, laid out the same in upstream MeshCore and Solo
(name, position, radio, BLE PIN, scope), then deletes it; everything else
starts from the defaults. Contacts, channels, scopes, waypoints and trails
are untouched.
Gone: the page-order and GPS-interval migrations, the unused
trail_interval_idx / trail_units_idx / repeat_extra_scopes; fav_sort_off
and msg_wake_screen_off become fav_sort and msg_wake (on by default).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The history of the noise floor, battery, traffic and SNR, a trail's height
profile (with a sampler for trails read point by point off the card) and a
month's calendar, moved out of ui-new so ui-lvgl can draw the same.
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>
- 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 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>
- 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>
- 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>
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>
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>
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 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>
- 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>
- Message history archived per conversation on the SD card (HIST_ARCHIVE):
restored on boot, Older/Newer paging in threads, kept count selectable
- Settings > System > Storage: card and flash usage, breakdown by maps,
messages, trails and other, kept-per-chat setting, delete history;
reads the mounted volume and warns when the FAT partition is much
smaller than the card
- Saved trails on the card: every Save is a new file, Load opens a list
with load (framed on the map), GPX export and delete
- Diagnostics: GPS data rows, last start reason, last crash summary
- Messages restored before the clock is set show their date instead of 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Map tools hold trail, live share and arrival alert, with their options
(schema page "Map options") behind one button; back returns to the map
- Nearby gets an advert button: send now (nearby / everyone) + auto-advert
- Home page 3: Repeater, Admin (list of repeaters and room servers),
Diagnostics
- Settings: display, sound, radio (Bluetooth, WiFi), system, keyboard,
contacts & messages; units moved to Display & power
- Status bar: Bluetooth, GPS, alarm, mute, then auto-advert, trail, live
share, repeater, arrival alert in the accent colour
- Fonts: key and route glyphs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- genericBuzzer I2S path (BUZZER_I2S): an audio task synthesises the notes
with eased edges and keeps the DMA queue fed with silence between sounds;
ES8311 setup; amp powered through the board only around sounds
- ui-core/SoundControl.h (mode, volume, built-in sounds, melody model),
SoundNotifier moved to the Core, chanctl::melody, schema Sound page
- L2: notifications, alarm / timer ring, locator sounds, Settings > Sound,
melody editor with the playing note lit, per-chat sound, mute icon
- noteIndex() on every buzzer backend; scrolling popups past screen height
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BotConfig (tabs, rows, the NodePrefs field behind each, reply placeholders)
moves into ui-core; ui-new's Tools > Bot renders it. L2: Home > Bot with
switches, list pickers, text popups with placeholder buttons, quiet hours.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AdminSession (login with saved password, CLI round trips, timeouts, field
table, typed values) moves into ui-core; UiCore offers login answers and
CLI replies to it first. ui-new's Tools > Admin becomes a view over it.
L2: node detail > Admin with tabs and per-field popups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ChannelControl (add Public/Hashtag/Private, rename, delete, notif/fav/scope)
and Favourites slots move into ui-core; ui-new uses them. Contact / channel
pref cleanup moves from ui-new's UITask into UiCore so it runs on every
frontend (L2 had none). L2: channel options popup (hold a row or the gear
in a channel) and the Add / Edit channel form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared by ui-new (L1) and ui-lvgl (L2) so both firmwares have the same core
features:
- WaypointModel (/waypoints, UTF-8-safe labels, [WAY] share text),
EtaTracker, GpsAverager, TrackBack; TrailEngine save/load/reset actions;
UiCore::setGpsEnabled / applyGpsInterval.
- Locator target kind 2: a channel live share followed by sender name
(LiveTrackStore::activeByName).
- SettingsSchema: pages (navigation, display & power, messages & contacts),
value tables over 1/2/4-byte NodePrefs fields, side effects via UiCore.
- RadioControl: apply params / TX power / APC and the preset list; ui-new's
Settings > Radio now goes through it.
- NodePrefs::display_brightness_pct (sentinel 0xC0DE002F, size unchanged)
for frontends with a brightness slider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui-core/NearbyModel.h builds the Nearby list (contacts + live [LOC] shares
+ heard adverts, or discover-scan results) with type filter and sort,
moved verbatim out of ui-new's NearbyScreen, which now inherits it.
ui-lvgl: Home is three tiles (Messages, Nearby, Settings). Nearby lists
known nodes with filter chips and a Dist/Recent toggle; tapping a row opens
node detail (distance/bearing, position, last heard, ID) with Message,
Ping, favourite, Add and two-tap Delete. Scan is a popup over the list
with its own model: nodes in range now, not the known set. Fonts gain the
FontAwesome star for favourites. The LVGL sim page adds an optional
repeater so Scan has something to find.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fonts: Noto Sans generated with lv_font_conv (ui-lvgl/fonts/generate.sh,
OFL) replaces ASCII-only Montserrat: European Latin (Latin-1, Extended-A/B),
Greek, Cyrillic + supplement, typographic punctuation, currency, LVGL
symbols + globe; 12/14/16/20 px, clock 40 px digits only (RLE, so
LV_USE_FONT_COMPRESSED is on).
Keyboard (ui-lvgl/Keyboard.h): one layout per script (Latin QWERTY,
Cyrillic, Greek) picked by the same keyboard_main/alt_alphabet prefs as
ui-new, globe key to switch, hold a key for its variants in a popup above
it, one-shot shift, symbols page; compose limit counted in UTF-8 bytes.
First Settings screen (Keyboard: Main / Additional).
Long-press variants and UTF-8 case mapping move to ui-core/KeyboardData.h
and are shared with ui-new, whose Hold-Enter accents gain Romanian,
Turkish, Hungarian, Baltic and Icelandic letters (and ș/ț, Ukrainian /
Serbian letters now capitalise).
Verified on L2 hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New examples/companion_radio/ui-lvgl/ over the shared UI Core (env
Wio_Tracker_L2_companion_solo_lvgl, LVGL 9.2.2, pool in PSRAM):
status bar, home (clock, unread), conversation list (channels + recent
DMs with unread badges), contact picker, conversation view with bubbles,
delivery / relay markers and on-screen keyboard, toasts for Core events,
display auto-off with wake on touch / button. Board glue (LovyanGFX
flush + GT911 touch) isolated in LvglPort.h; design tokens in Theme.h.
UiCore gains the first frontend actions, sendDirectText() and
sendChannelText(), and drives DM auto-resend + unread reconcile from its
own loop (removed from ui-new's loop). MessageHistory exposes newest-first
DM ring iteration and a public contactByPrefix().
Verified on L2 hardware: navigation, send/receive, sleep/wake.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui-new hands MyMesh the UI Core (UITaskBase::meshListener()). The Core
applies the display filter, labels room posts, files history, keeps DM /
channel / room unread and the queue count, and reports incoming messages
and adverts as MessageArrived / AdvertHeard events. UITask now derives
UITaskBase + UiCoreHost: it answers "is this conversation on screen" and
keeps the selection after an insert, forwards not-yet-extracted parts
(room login / admin, bot device actions, removal cleanup, shutdown), and
turns events into alert / wake / sound, drained at the start and end of
its loop(). Mesh callbacks no longer touch the display or buzzer.
AbstractUITask is split into UITaskBase (lifecycle, link state, helpers)
and the Listener glue ui-orig / ui-tiny keep using unchanged. Screenshot
handler reaches ui_task directly instead of casting the Listener.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TrailStore, background sampling, auto-pause and the pre-shutdown
auto-save move from UITask to ui-core/TrailEngine.h. TrailScreen still
drives the store directly. The 512-point store now lives on the heap
with the rest of UiCore (static RAM -8 KB on Wio L1, same total).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Active target (set/clear/resolve person position), geofence crossing
state machine and proximity beeper move from UITask to
ui-core/LocatorEngine.h, emitting LocatorCrossed / LocatorBeep events.
UiEvent gains a flag and a 24-byte text. tickCore() now runs at the end
of UITask::loop(), where the moved engines used to run, so a fresh GPS
wake re-seeds the locator before it evaluates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auto-[LOC] session (duration, movement/heartbeat gate, scoped send) and
the peers' LiveTrackStore + expiry move from UITask to
ui-core/LiveShareEngine.h. "Live share ended" becomes a
UiEventType::LiveShareEnded event; UITask keeps thin wrappers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
COG ring sampling (every ~1 s from UiCore::loop), outlier rejection and
currentCourse/currentLocation move from UITask to
ui-core/CourseEngine.h. UITask keeps one-line wrappers; screens unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Single in-flight ping + last result move from UITask to
ui-core/PingEngine.h; MyMesh's ping callback routes to the engine
instead of casting the Listener back to UITask. UITask keeps thin
wrappers (and the "not supported" alert), NearbyScreen is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alarm / countdown / ring logic moves from UITask to ui-core/ClockEngine.h
unchanged. New ui-core/UiEvents.h: fixed-size Core -> frontend event
queue; UITask::tickCore() runs UiCore::loop() and reacts to ClockAlert /
ClockRingEnded (wake, alert overlay, melody). ClockToolsScreen API unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New examples/companion_radio/ui-core/ (header-only for now, reached from
ui-new/UITask.cpp by relative include, so no platformio.ini changes):
UiCore facade, MessageHistory (moved verbatim), DmUnreadTable (extracted
from UITask). UITask heap-allocates the Core in begin(); MessagesScreen
binds to core().history. Behaviour unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>