- DisplayDriver takes ColorVal; each driver's .cpp defines the UIColor roles
(window_bkg, primary_txt, ...) in its own values. ui-new draws in two:
window_bkg and primary_txt. OLED and e-ink look as before (sim screens
pixel-identical).
- E213, E290, LGFX, Null, ST7735, ST7789LCD, U8g2 taken whole from upstream
(with them the rewritten ST7735 driver and the Heltec V4 R8 TFT display).
- SH1106, SSD1306, ST7789, EinkGfxDisplay and the sim drivers converted.
- ui-orig, ui-tiny, simple_* get upstream's colour roles; the repeater's UI
taken from upstream (its power-off screen).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core: PathEditor results are links (node k hearing k-1 and back); toNode()
off traces only the repeaters (a companion answers only with repeat on);
probe() traces to each repeater in turn after no reply; failures carry a
reason; an empty list saves "direct". PingEngine takes over a slot left past
its timeout. A kept route: the dropped heard path isn't announced to the app
nor bumps lastmod; an app update keeps the flag only if it leaves the route.
Marker boxes sized for every dot and name (overflow).
ui-lvgl Route: one line per node with out / back dB coloured by quality, tap
a hop to move / remove, tap the node to include it, Keep in the header, Find
where beside a no-reply status. ui-new Path: To node / Find break rows,
result alerts, cursor kept on its action.
Channels: ui-new re-sorts the list on entering it, cursor on the same channel.
Docs: Path and trace, Channel order. Sim hooks: open route, add channel,
open messages.
Checked in the browser sim (L2 + ui-new peer + repeater); not on hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared chanctl::order() builds the channel list for ui-new and the L2:
favourites first, then by ch_sort. New ch_sort pref at the end of NodePrefs,
Settings > Contacts row and contacts.channel.order CLI key.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- L2 vector packs: a damaged count can't wrap the index size, and an
entry pointing past the file is refused.
- L2 map share-target list: sized for 40 long channel names and 16
favourites (it could overflow), off the stack.
- L2 SD history: a file with an unreadable header is set aside as .bad
instead of written over; Delete message history takes those too.
- Contact lookups by key or hash start after the reserved anon slots (an
empty one matched a 00 hop hash with a blank name); MessagesScreen uses
contactctl::hopName().
- Nearby keeps its selection by public key: deleting a contact shifts
the table.
- ui-new Live Share target cycle holds every channel and favourite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui-new's messagesInView() and ui-lvgl's threadInView() hold the
lit-and-unlocked test the viewing checks repeated; the keyboard's three
copies of the backspace call deleteBeforeCursor(). A stale comment on the
cursor-mode hint goes, onDisplayOn() sits after the view helpers it calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A conversation left open under the lock screen or a dark display no longer
counts as read: the counters and the lock screen's tally go up, and on
waking (L2: on unlocking) the conversation reopens on its first unread
under the "New" line, a typed draft kept.
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>
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>
- ui-lvgl: navTextEntry()/TextEntry replace eight copies of popup + field +
keyboard wiring; one byte-capped insert callback instead of two.
- ui-new: drawHeadedBubble()/BubbleMark replace the duplicated DM and channel
bubble drawing in MessagesScreen.
- GpsSky::halt(): stopping the receiver ends an unfinished search but keeps
the first-fix time; keyboard cursor blink restarts after a key.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VeBrxKyHHxWsmttVkUdBrj
/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>
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>
- 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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
DM and channel history each live in a fixed-size ring (32 DM / 48
channel entries). A conversation that outpaces reading can fill its
ring, evicting the oldest entries — including unread ones that were
never opened — so the unread badge silently caps at ring capacity
instead of reflecting how many messages actually arrived.
MessageHistory now tracks a sticky per-channel overflow flag, set at
the exact point an unread channel entry gets evicted (mirroring the
existing unread-decrement logic) and cleared once the count reads back
down to 0. DMUnreadEntry gains the same bit for DM contacts, detected
by comparing the raw per-contact counter against actual ring occupancy
on each new message (the DM ring, unlike the channel ring, doesn't
proactively decrement on eviction).
DisplayDriver::drawUnreadBadge/unreadBadgeWidth/fmtBadgeCount take an
optional overflow flag (default false, existing call sites unchanged)
that appends "+" to the badge. Wired into every place an unread count
is shown: DM/favourites and channel lists, the DM/Channels/Rooms
mode-select screen, favourite tiles, and the aggregate "Msgs" field on
the clock and lock dashboards plus the Messages home-carousel page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The marker was placed after the sender's full width while the name itself
was only ellipsized against the age, so a long name pushed the hop count
(worst with two digits) onto the age. The name now yields to the marker and
the age, the marker follows the width the name actually got, and a 2px gap
separates marker from age.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Message bubbles are capped at the row width minus a gutter (2 char widths,
min 8px) so a long message no longer fills the whole row and loses the
left/right anchoring that shows who sent it. Body wrapping uses the capped
width. DM and channel history.
- Docs: shared scope list (Settings, Repeater Extra scopes, channel Scope row),
contact Expire / Prune now, history hop count / wrap / scope tag, external
PA TX curve build flags.
- release-notes.md: v1.28 gains GAT562 30S TX power, Heltec V3/V4 joystick
Enter fix, prefs-migration removal note, and the bubble gutter fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the single device-wide Scope text field with a small named list
(wildcard "*" fixed first, plus a movable default): each channel picks one
scope of its own (matching the app's per-channel region picker, surfaced in
the channel-history title), and the repeater's Extra scopes multi-selects
from the same list instead of comma-typed names. Both pickers are popups
(PopupMenu gains addCheckItem()/a fillable-square checkbox glyph) over the
existing screen rather than a full-screen takeover.
Also fixes a stray-bits bug where a pre-existing prefs file's own sentinel
tail could read into the new repeat_extra_scope_mask/ch_scope_idx fields and
silently reappear as a real pick later once the scope list grew into that
range.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Unselected message bubbles no longer carry a stray light-filled header
strip behind the sender name -- the one place with a filled patch on an
otherwise plain row. Selected fill is unchanged.
- Received DMs and channel posts now show their hop count as the same tiny
digit icon a sent message already uses for its repeater/echo count, using
the hop path the mesh already records for incoming messages.
- DM/channel history list: UP at the oldest message now wraps straight to
the newest (not the compose row), closing the loop the same way every
other list in the UI does at its ends.
- A received message's age no longer goes blank when its timestamp reads
slightly ahead of the device's own clock (sender/receiver skew, or an
unsynced receiver) or is genuinely unknown -- both now show the receipt
time instead of leaving the row with just a name and hop count.
- release-notes.md: add the v1.28 section covering the above plus this
cycle's already-committed dev fixes (GPS altitude field, units
compliance, noise floor, Msg wake location, bot relay tracking, PR #34).
- Add .github/workflows/build-solo-sim.yml: builds the companion+repeater
WASM sim via Emscripten and attaches it as a fixed-name asset to the tag's
draft release, independent of the hardware firmware build job.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every other list in the UI (contacts, channels, message-pick, the
mode tabs) wraps at its ends; the DM and channel history lists were the
one exception -- UP at the oldest message (top of the list) just
stopped instead of wrapping to the compose row (the list's own
bottom-most stop), and DOWN from the compose row didn't wrap back to
the oldest either. Both directions now close the loop, same as
everywhere else.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two distinct bugs behind "Relayed by" scrolling looking broken:
1. The DM/channel history row underneath a context menu (Path/Relayed by,
Options, etc.) keeps rendering every frame with sel=true regardless of
the popup on top of it, and both it and the popup's own selected item
share DisplayDriver's single marquee slot. Whichever drew last each
frame kept is_new-resetting the other's animation -- looked like the
scroll "remembered" a stale, wrong position. Now suppressed via
`sel && !_ctx_menu.active` at all four call sites that had it (DM/
channel history bodies, contact/channel pick names).
2. PopupMenu::render() returns 50 (it wants a fast, smooth redraw), but
all 7 `_ctx_menu.render(display)` call sites discarded that return
value, so the enclosing phase's own return statement (500-2000ms)
governed the actual redraw cadence instead -- the popup's marquee only
ever advanced whenever the much slower underlying screen happened to
redraw. Now folded into the existing mq_delay accumulator, same as
every other marquee source in this file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
getContactByIdx() indexes the raw contacts[] table directly, whose first
MAX_ANON_CONTACTS (8) slots are reserved for anon requests -- getNumContacts()
already excludes them from its count, so real contacts start at index
MAX_ANON_CONTACTS, not 0 (NearbyScreen.h's own contact scan already applies
this offset; its comment documents why).
MessagesScreen.h's buildContactList() didn't, so its loop only ever read
the reserved anon slots (empty name, type 0) for any total at or under
MAX_ANON_CONTACTS -- e.g. a device with exactly one known contact would
show "SELECT CONTACT" / "No favourites" with an empty list forever,
regardless of the dm_show_all/fav_only setting or that contact's own
favourite flag. Reproduced live in the browser sim: a fresh companion_radio
instance with one real ADV_TYPE_CHAT contact showed nothing until this fix.
Also fixed _sorted[]'s stored index (was the bare loop counter, needed to
be the raw table index every other call site in the file already assumes)
and the same missing-offset bug in BotScreen.h's room-contact counter and
MessageHistory.h's pub_key-prefix contact lookup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hop_count was clamped to MAX_HIST_PATH_BYTES regardless of hash_size, but
path[] is only MAX_HIST_PATH_BYTES bytes total -- with hash_size>1 the old
clamp let i*hash_size run past the buffer in resolveHopName(). Not reachable
today since capturePath()/markChannelRelayed() already bound hop_count to
MAX_HIST_PATH_BYTES/hash_size on write, but the reader shouldn't rely on
writer discipline alone. Now clamps to the same MAX_HIST_PATH_BYTES/hash_size
bound.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found while re-reviewing this session's own commits: five screens each
hand-built the same 2-row Action/Cancel confirm popup, defaulting the
highlight to Cancel -- NearbyScreen's contact-delete, AdminScreen's
OTA-start, and the three just added (Trail's reset, Messages' channel
delete, RadioPresetPicker's preset delete). The plan that added those
three had already flagged this exact duplication without acting on it, so
it just tripled instead of getting fixed.
One PopupMenu::beginConfirm(title, action_label, cancel_label="Cancel")
replaces all five call sites, and makes "defaults to Cancel" a property of
the popup itself rather than something each new confirm has to remember.
Also drops two small redundancies spotted along the way: NearbyScreen's
_confirm.active = true, dead since begin() already sets it, and
RadioPresetPicker's deleting = false being set twice in a row (once inside
openConfirm(), once again by its only caller).
No behavior change; verified against the actual PopupMenu/menu-level state
machines in each of the five call sites before touching them.
Continuing the consistency review: sweep for the same three defect shapes
elsewhere in ui-new/ (own read pass plus two parallel research agents),
verified against source before acting.
Three destructive actions fired on a single Enter with no way back, unlike
contact-delete's existing confirm-defaulted-to-Cancel popup: Trail's "Reset
trail" (wipes the whole recorded route, no undo short of a prior manual
Save -- reuses Trail's own multi-level menu machinery, alongside its
GPS-off confirm), Messages' channel Delete, and RadioPresetPicker's saved-
preset delete (shared by Settings > Radio and Tools > Repeater, so one fix
covers both). All three now confirm the same way, defaulting to Cancel.
KeyboardWidget was the one place Hold-Enter still doubled as Cancel: Shift,
Backspace and a Latin letter's accent popup already have real, kept
meanings under a hold, but every other special-row cell (Space, OK/Done,
the {} placeholder) fell through to a bare CANCELLED, closing the keyboard
exactly like the real Cancel key. Now a no-op there too, matching the "only
Back closes it" rule already applied to popups and screens.
MessagesScreen defined the same two label arrays (Notif states, melody
slots) four times over, once per context-menu handler. Hoisted to one
pair of static class members -- constexpr wasn't enough to get the linker
to emit them on this toolchain, so they follow the same declare-in-class/
define-out-of-class shape NearbyScreen::FILTER_LABELS already uses.
Alert text: "Advert sent!"/"Advert failed.." and "Sent!" were the only
toasts anywhere with trailing punctuation; normalized to the plain style
every other confirmation uses. Unpinning from the Favourites Dial reported
the freed slot number from the Messages screens but not from Nodes or the
dial's own tile menu; now consistent everywhere pinning already was.
DiagnosticsScreen's Live/System/Font tab renderers hand-rolled the same
scroll-clamp/loop/indicator skeleton drawList() already bundles; switched
both to drawList (passing the screen's own _scroll as its `sel` too, since
neither tab has a row cursor -- makes drawList's internal clamp a no-op and
leaves clampScroll() as the only thing bounding it, unchanged). Pure
internal tidy, no behavior change.
Continuing the consistency pass: the four screens that show the
distance/bearing "navigate to a point" view (Nodes, Waypoints, Trail's
Track back, and navigating to a location shared in a message) had drifted
apart in three ways.
Only two of the four passed an EtaTracker to navview::draw(), so only
Nodes and Track back showed the closing-speed/ETA line -- navigating to a
waypoint or a shared location left it off for no reason. All four get one
now. They also left the view on three different key sets (Back alone,
Back+LEFT/RIGHT, Back+Enter); Back is now the only way out of any of them,
so a stray sideways nudge can't drop you out of a running track-back.
Messages' renderNav() also switched from reading node_prefs directly to
the shared useImperial() helper the other three already used.
Set as target -- the row Nodes and Waypoints both offer for a coordinate
-- was missing from the message-location Options menu; added alongside
Navigate and Save waypoint.
Nodes' own Set as target required a full 32-byte public key, which a
name-only live-track entry (someone sharing position on a channel who
isn't a saved contact -- the group-outing case this exists for) never
has. One flag was doing two jobs: "can be pinged" (needs the full key)
and "can be identified" (needs only the 6-byte prefix a person target
actually uses). Split into has_key/has_prefix; Set as target now only
needs a position, resolving to a person target (follows them) when a
prefix is available and a place target (pinned where they were) when it
isn't -- the same distinction Locator's own picker already draws.
Locator's target picker separately still listed the people pinned to the
Favourites Dial as its privileged top tier, which stopped making sense
once pinning and favouriting became separate concepts. It now leads with
favourites instead, matching every other list in the firmware.
Three interaction inconsistencies found while auditing the favourites work,
all of the same shape: the same gesture meaning different things depending
on which screen you were on.
Value rows in popup menus. Rows like "Notif: ON" or "Sort: Dist" show a
value the user steps through with LEFT/RIGHT, but Enter treated them as
ordinary menu picks and closed the popup, so changing two of them meant
reopening the menu in between. Trail's settings submenu was the lone
exception, working around it by rebuilding and re-selecting after each
Enter. PopupMenu now knows the difference: addValueItem() marks a row, and
Enter on it returns the new VALUE_NEXT instead of SELECTED, leaving the
menu open. Only Back closes a menu now. Applied to the Messages
contact/room/channel menus, Nodes, the Ringtone editor and Trail, which
drops its reopenSettingsAt() workaround. The LEFT/RIGHT cycling bodies
moved into one helper per menu, since Enter and RIGHT now share them.
Nodes' Fav row was the worst case: LEFT/RIGHT did nothing there at all, so
the only way to toggle a favourite was an Enter that dismissed the menu on
every flip. Its label moved to a member buffer (as the Pin row already had)
so it can be retitled in place.
Settings rows Auto-off, Low battery, GPS pwr and Battery ignored Enter,
though their options wrap exactly like the melody/keyboard/clock rows
beside them, where Enter has always stepped forward. They accept it now.
Rows that ramp between fixed ends (Brightness, Volume, TX Pwr, Timezone,
SF/BW/CR) stay LEFT/RIGHT-only -- there is nothing to wrap to.
Hold Enter no longer doubles as Back. It quietly meant "go back" on Tools,
Locator, Live Share, Repeater, Bot, Auto-Advert, GPIO, Compass, the
Dashboard config and the Messages navigate view, while elsewhere the same
long press opens a context menu. It now only ever opens a menu, or does
nothing where there is none. Same for dismissing an open popup, which it
used to do. Checked that this strands nobody: every board that can reach
these screens has a real Back key (back_btn on joystick boards, Esc on
CardKB/TCA8418/T-Deck). Single-button boards produce no KEY_ENTER at all,
so they never leave the home pages in the first place.
Three names had grown around one idea. "Favourite" was a filter in
Settings, an invisible app-only flag on a contact, a device-settable bit on
a channel, and — on the Nodes screen — a menu row that actually pinned to
the Favourites dial. Nothing marked a favourite on screen, and the dial
only took chat contacts.
A favourite is now the starred flag (ContactInfo::flags bit 0 for contacts
and rooms, ch_fav_bitmask for channels), settable on the device everywhere
via a Fav: ON/OFF row, marked with a star on its row, and sorted to the top
of the list — in Messages, Tools > Nodes and the Locator target picker.
Settings > Contacts > "Favs top" turns the sorting off; it defaults on, and
is stored inverted so an upgraded prefs file reads back as on rather than
off.
MyMesh::setContactFavourite() writes the same bit the app sets and bumps
lastmod, so the two stay in sync. The DMs/Rooms = Fav list filters no
longer depend on having starred someone in the app first.
Pinning is now separate and explicit, and the dial takes contacts, room
servers and channels. Slots carry a kind (NodePrefs::favourite_kinds,
schema 0xC0DE0029) — a channel slot holds an index, so emptiness is decided
by the kind first, since channel 0's payload is all zeroes. Choosing what
to pin reuses the Messages screen's own Direct/Channels/Rooms browse
instead of a second picker built on the dial, which drops that picker, its
key/label tables and the now-unused getRecentDMContacts(). A filled tile
gained Unpin/Replace under Hold Enter.
Fixes a room server being pinnable and then unremovable: the picker's
recent-conversations tier didn't filter by contact type and room posts
share the DM history, opening one from the dial skipped the login
handshake, and Unpin only existed in the chat contact list that rooms never
appear in.
Also: PopupMenu::_visible was written and never read (render recomputes the
cap from display height), which is why menus with more items than the
"visible" argument always worked; Settings' all/fav values and the
dm_show_all comment said things that were not true.
Built for Heltec V3, Wio Tracker L1, Wio Tracker L1 e-ink and T-Echo Card.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- RepeaterScreen: drop the redundant "Rpt " prefix on the profile fields
(Preset/Freq/SF/BW/CR), matching Settings > Radio's own terminology --
the screen is already dedicated to the repeater's own profile, so the
prefix disambiguated nothing.
- Channel context menu: "Fav: yes/no" -> "Fav: ON/OFF", matching every
other toggle in the app.
- Settings (System tab): six labels left as raw concatenated identifiers
(AutoOff/AutoLock/TimeZone/LowBat/BattDisp/BzrVol) now read like their
space-separated neighbours (Auto pwr/Pwr save/DM sound/GPS pwr), and
BzrVol no longer clashes with "Buzzer" one row above it for the same
feature. Three value-label arrays also had one mismatched-case entry
fixed to match its siblings: Auto-off's "never" -> "OFF", Batt display's
"icon" -> "Icon", Sound's "built-in" -> "Built-in".
- ToolsScreen: re-enabled the mini-icons next to each tool, which had
been commented out ("don't fit visually"). Root cause: the screen's own
drawIcon() centred against lineStep() (line height + inter-row gap)
instead of getLineHeight() alone, 2px too generous for a near-full-
height icon -- now delegates to the already-correct miniIconDraw() used
elsewhere (e.g. the message-list ack checkmark). Also gave Admin and
GPIO their own icons (padlock, 3-pin header) instead of both sharing
System's cog.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The plain ✓ on your own channel post only said "at least one repeater
heard it" -- now it shows how many distinct repeaters echoed it back,
drawn as tiny 3x5 digit icons (icons.h) rather than the normal font,
since the slot next to the sender name is icon-sized, not text-row-
sized. DM delivery ticks are untouched (no repeater-count concept
there, so they keep the plain checkmark).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An app-originated channel send (mirrored into the on-device history)
bumped that channel's unread badge whenever the device's own UI
wasn't already showing that exact channel -- unlike an on-device
compose, which sidesteps this by forcing itself into that channel's
view right before sending. Adds an explicit own_message flag through
addChannelMsg (MessageHistory -> AbstractUITask -> UITask ->
MessagesScreen) so an own post is never counted unread regardless of
what's on screen when it's sent.
Found the same bug in MyMeshBot.h's three auto-reply-into-channel call
sites (Remote Bot's own reply showing as unread on itself) and fixed
those with the same mechanism.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the existing single-boolean channel relay-echo marker into a
full count + list of distinct confirming repeaters, since each
repeater retransmit already appends its own identity hash to the
packet's path and the echo-matching hash deliberately ignores that
mutable path -- so every distinct repeater's echo of one send now
matches the same tracking slot instead of only the first.
Symmetrically captures the hop path a received DM/channel message
actually took, so a new "Path"/"Relayed by" row in the existing
Hold-Enter Options popup can show the resolved sequence of repeaters
(by contact name, or a hex fallback for an unknown one).
Also fixes a real bug caught during testing: the popup row's own
label ("Path (N hops)"/"Relayed by (N)") was built into a stack-local
buffer handed to PopupMenu, which only stores the pointer -- it
rendered as garbage once the building function returned. Moved to a
persistent member buffer.
Bumps the dev-build fallback version and adds release notes/docs
for this plus the two other 1.27 features already on this branch
(BLE retry backoff, marquee-scroll for selected long text).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>