Channel options > Save keeps a busy channel's new messages off the flash, so
it leaves the room to the others; Keep messages stays the master switch.
Off now only stops the saving, so a value stepped past costs nothing and the
wait for the delete comes at a start. Delete history stays immediate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Past the oldest message in a conversation, Up reads the next 12 from the log.
Pages, not a longer list: the messages screen measures its whole list every
frame. Also from the full-screen view, and for a conversation the ring has lost.
Settings > Messages > Delete after hides older messages at once and frees the
flash a segment at a time, never with the clock unset or behind the log; Delete
history clears the log and the rings. A batch is also written after 12 new
messages, as a room's backlog could leave the ring unsaved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings > Messages > Keep messages (Off, 500, 1000, 2500 in all; off by
default; HIST_FLASH, ui-core/HistoryLog.h). Two append-only logs on the QSPI
flash, channels and DMs, restored into the RAM rings at the start. A batch is
written a minute after the first unsaved change and at shutdown, as the L1's
power switch gives no warning. Each segment has a directory of its own: a
LittleFS 1.x commit erases one of a directory's two fixed blocks every time.
E-ink panels keep their last frame, so a shutdown now leaves "Powered off" on
it and puts the controller into deep sleep.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Bluetooth + USB (+ WiFi/Ethernet/UART per build) in one interface_manager,
as upstream; frames go to every enabled link, USB is read while BT is
connected. DUAL_SERIAL now means ENABLE_USB_INTERFACE.
- WiFi interface with upstream's wifi_* prefs and set/get wifi.* CLI.
- isClientConnected moved into ArduinoSerialInterface (DTR on nRF52 and
ESP32 native CDC).
- UI uses isBluetoothEnabled/enableBluetooth/disableBluetooth; turning BT
off leaves USB and the other links on.
- DualSerialInterface removed.
- Sim build gains DynamicConfigSerializer.cpp (broken since bd64c203).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NodePrefs is now NodePrefsFields (the fields) plus how they're saved: one
JSON key per field, upstream's keys under "radio", "gps", "repeat", "comp"
and "custom" (so either firmware reads what the other wrote of them),
ours under "solo". Saved atomically (temp file, then renamed), through a
buffer. 2.0-rc's binary /prefs is removed, not converted: 2.0 starts from
the defaults; /new_prefs from before 2.0 is still imported.
From upstream with it: txdelay / direct.txdelay / agc.reset.interval
(prefs and CLI), and the board's own settings through getCustom()
(Board::attachDynamicPrefs) in place of the merge's stopgap adapter.
ConfigSerializer reads values up to 191 characters (was 127): 140-byte
texts, an 80-byte table in hex.
Test: every field round-trips through the JSON byte for byte; the alarm
label tests expect "Once" (the label changed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every packet heard runs from its origin through the repeaters with a
position to this device; a packet sent is a ring. ui-core/PacketFlow.h
resolves them (UiCore::flow, MyMesh logRx/logTx -> Listener). ui-lvgl
draws them on the map's marker layer and repaints only their area while
they move; ui-new draws the last minute on the Nodes map ("Flow" in its
options). Setting: Map options > On the map > Packet flow, CLI map.flow.
Off by default. Sim-checked only.
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>
PREFS_VERSION 2: settings saved by 2.0-rc1..rc3 are not read, the device
starts from the defaults (v1.x /new_prefs is still imported as before).
The fields appended at the end during the rc cycle move into their
sections (BLE off, battery curve, text/icon size, message alert, advert
position precision, lock screen look). ch_scope_follow and its rc1
conversion and the unused use_lemon_font are removed. Stale comments in
NodePrefs.h and Trail.h fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up:
- The rows exist only where the build asks for them: Text size with
EINK_FONT_SCALE, Icon size with STATUS_ICON_SCALE. The stock 2.13", the
portrait builds and the L2 don't get them -- their screens are laid out
for one cell size. The text override also applies in landscape only.
- Stored in NodePrefs (ui_text_scale, ui_icon_scale) instead of a
/ui_scale file, as rows in the schema's Display section: the usual row,
save path and factory reset, and console keys display.text.size /
display.icon.size. The hand-written Settings rows and the load/save code
are gone; UITask::applyUiScale() applies the prefs at boot and on change
(applyDisplayPrefs). On opt-in builds ui_text_scale starts at the
build's EINK_FONT_SCALE (DataStore sanitize), so the row shows the scale
in use.
- EINK_FONT_SCALE, when set, now scales whichever landscape font the build
picks (the 6x9, or the EINK_LARGE_FONT one); unset, it is stock as before.
- setUiScale() skips the re-layout and full redraw when nothing changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings > Privacy > Precision: Exact / ~100 m / ~500 m / ~1 km, beside
"Position in adverts", which stays the on/off. With the position shared,
it is snapped to a fixed grid (0.0009 / 0.0045 / 0.009 degrees) rather
than jittered, so repeated adverts from one spot send the same value and
can't be averaged back to the exact fix.
A schema row (NodePrefs::advert_loc_prec), so both UIs show it, factory
reset clears it and the console has it as advert.loc.precision.
Every self-advert -- manual, flood, auto, the phone app's and contact
export -- now goes through MyMesh::createOwnAdvert(), so the precision
applies everywhere.
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>
/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 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>
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>
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>
- 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>
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>
- Live Share gets a "Scope" row (Target / * / any scope from the shared
list). It applies to [LOC] sends only, beats the app's send_scope and a
channel's own pick, and never touches normal messages. Target keeps the
previous behaviour. removeScope() fixes the stored index up.
- Auto share now always ends: "Stop after" 1/2/4/8/12 h (default 1 h, no
unlimited option). On expiry it switches itself off and shows "Live share
ended". The session clock is RAM-only, so a reboot starts a new session.
- NodePrefs: loc_share_scope + loc_share_duration_idx appended, sentinel
0xC0DE002C -> 0xC0DE002E, sizeof unchanged (2824).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
loadPrefsInt()'s "sentinel mismatch" block had accumulated ~62 lines of
one-shot backfills tied to specific old NodePrefs::SCHEMA_SENTINEL values
(favourites/map home-page bits, split bot-command toggles, trail_units_idx,
bot_trigger_ch seed, scope mask/index zeroing). Reduce it to just the
sentinel read + mismatch log line -- an upgrade from a version old enough
to hit any of these now lands on today's plain defaults for that field
instead of a silently-seeded value, same outcome a fresh device already
gets. The ongoing per-field range clamps (defensive against any corrupt or
out-of-range byte, not version-specific) are untouched.
Same treatment for DataStore::loadScopeList()'s one-time synthesis of a
scope-list entry from the old single default_scope_name/key field: removed,
along with the scope_migrated_legacy plumbing in MyMesh::begin() that only
ran once that migration had just fired. loadScopeList() drops its now-
unused NodePrefs parameter and bool return.
Also trimmed ~25 per-field comments that over-specified which exact old
sentinel produced which stray byte, down to what the clamp actually
defends against -- except where the hex value explained a genuinely
non-obvious byte-aliasing interaction, which stays. One comment (scope
mask/index) was flagged and rewritten because it described the very
zeroing logic this change removes.
Drive-by: two comments still said Settings > Sound > "Msg wake" -- it's
been under Settings > Display for a while, per the enum's section grouping
in SettingsScreen.h.
Verified: sim_companion_radio and WioTrackerL1_companion_solo_dual build
clean, RAM/Flash usage unchanged (pure logic removal, no schema/layout
change).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Settings > Contacts gains "Expire" (Off/7d/30d/90d) and a "Prune now" action
that counts first and asks before removing anything. A contact with no
advert/update within the threshold is eligible; favourites are always kept.
Thresholds and their labels live in one NodePrefs table so the age shown and
the age enforced can't drift. SCHEMA_SENTINEL -> 0xC0DE002C (sizeof unchanged
at 2824, confirmed on native and a real WioTrackerL1 build).
Also fixes four bugs in the scope list from 89c02eea:
- removeScope() saved /scopes1 but never savePrefs(), so the ch_scope_idx[]
and repeat_extra_scope_mask fix-ups it makes were lost on reboot, leaving
shifted entries against unshifted indices.
- CMD_SET_DEFAULT_FLOOD_SCOPE wrote the legacy fields directly instead of
going through setPrimaryScope(), so the app's default-scope setting had
nothing reading it once sends resolved through the list.
- The reverse direction was stale too: an on-device "Set as default" never
refreshed default_scope_name/key, so CMD_GET_DEFAULT_FLOOD_SCOPE reported
a scope the device had stopped using. New syncLegacyDefaultScope().
- Upgrading from the old single Scope field set the default (so DMs kept it)
but left every channel unscoped, since "*" means unscoped, not "inherit".
loadScopeList() now reports when it migrated and begin() seeds the channels
that already exist, leaving empty slots alone.
"Set default" -> "Set as default", and setting it now says what it governs
("Default: DMs + relay") rather than leaving the [default] marker to imply
more than it does.
Co-Authored-By: Claude Opus 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>
Two independent changes, requested together:
1. Settings > Sound > "Msg wake" (NodePrefs::msg_wake_screen_off,
0xC0DE002A) lets a user disable UITask::newMsg()'s existing behaviour
of turning the display on for an incoming message when it was off and
no companion app is connected. Stored inverted so both a fresh device
and an existing saved-prefs file default to "on" (today's behaviour).
New sim_test_set_msg_wake_disabled() hook verifies it without scripting
Settings navigation -- confirmed end to end via a real A<->R<->B mesh
in Playwright: message delivered while B's screen is off either wakes
it (enabled) or doesn't (disabled), checked via real canvas pixels.
2. A brand-new or factory-reset device now shows ALL home pages by
default instead of a curated 5-page carousel (MyMesh.cpp used to seed
home_pages_mask = HP_DEFAULT; now seeds 0, which every other read site
already treated as "all visible" -- HP_DEFAULT is now unused, removed).
Existing users' saved masks are untouched. New sim_test_get_home_pages_mask()
getter confirms a fresh instance reads back 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
NodePrefs.h's field declaration order used to just be historical append
order (on-disk format is defined solely by DataStore's explicit rd()/wr()
sequence, not struct layout), making the file hard to navigate. Reordered
fields into thematic groups (radio, repeater, bot, GPS/trail/location,
display/keyboard, etc.) with no on-disk/schema change; fixed two comments
that had gone stale (favourite_contacts/_kinds' [del→...] tags only named
one of the two handlers that actually clear them; dashboard_fields was
miscategorized under favourites). sizeof(NodePrefs) shifted twice as a
side effect of packing (2760→2752→2760) — verified via real builds on all
four canonical envs and re-checked against the serialization tripwire.
Also replaced test_companion_node_prefs.cpp's dead body (a disabled test
against a saveSerial/loadSerial API this struct never got) with real
coverage of the pure helper functions NodePrefs.h already carries -- band
bucketing, repeater-profile bounds, alarm-repeat round-trip, and every
option-lookup table, including their inconsistent out-of-range fallback
behaviour.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Follow-up review of f589b9b2 -- five defects in that commit's own changes.
- repeat_scope_only + repeat_extra_scopes were read/written in the MIDDLE of
the prefs stream, beside their repeat_* siblings. loadPrefsInt()'s rd() is a
plain sequential reader gated only on file.available(), with no per-field
versioning, so on any pre-existing file those 25 bytes were taken from the
fields that follow, shifting EVERY later field: repeater profile (incl. a
float freq), track_shared_loc, all of loc_share_*, trail, bot, GPIO modes.
Moved to the struct/file tail, sentinel bumped to 0xC0DE0027 with 0xC0DE0026
marked burned. sizeof stays 2752 (confirmed by build); the tripwire procedure
now spells out the append-only rule that "in struct order" left implicit.
- rebuildRepeatScopes() called getAutoKeyFor() with id 0 for every entry, but
that cache is keyed on the id alone and ignores the name on a hit -- so every
extra scope after the first silently got the first one's key, making the
comma-separated list do nothing. Distinct id per scope now.
- interference_threshold had no load clamp, so an upgrader read 0x23 (35) out
of the old file's sentinel tail instead of 0.
- CMD_SET_DEFAULT_FLOOD_SCOPE wrote default_scope_key without rebuilding the
relay filter, so setting or clearing the scope from the app left the repeater
filtering on the previous key until reboot. The on-device path already did.
- The keyboard preview derived the cursor's row a second time from byte
offsets, disagreeing with the cursor_line the scroll window already computes:
it pinned the cursor to the end of a full line (drawing '_' one character
past the display width) at every wrap boundary. Use cursor_line directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Settings > Radio > Scope: type a community/region name on-device (derives
the shared key the same "#name" -> SHA256 way as DEFAULT_FLOOD_SCOPE_NAME),
previously only settable from a connected app.
- Tools > Repeater > Scope only + Extra scopes: only relay flood traffic
matching the device's own scope or a comma-separated list of additional
scopes, without changing what scope the device's own messages send under.
No-op while unconfigured.
- getCADEnabled()/getInterferenceThreshold() were hardcoded off on
companion_radio; CAD now auto-enables whenever RX power-save (duty-cycle)
is active, since the noise floor isn't kept fresh during duty-cycle sleep.
- Message truncation to fit the send frame could split a multi-byte UTF-8
character in half; now stops at the last complete character.
- The default "Public" channel was unconditionally re-added at every boot
before the saved channel list was loaded, so deleting it never stuck.
Only seeded now on a genuinely fresh device (no channel file yet).
- Tools > Nodes read contacts from the wrong starting offset, landing on
internally-reserved bookkeeping slots instead of real contacts -- showed
as blank "Unknown" rows and silently dropped that many real contacts off
the end of the list.
- resetContacts() only cleared the first few reserved slots, not the whole
contact table, contrary to its own comment; only reachable today via
private-key import, fixed to match stated intent regardless.
- Keyboard's multi-line text preview could render the cursor on an empty
line below short typed text instead of right after it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every genuine on/off toggle already agreed on ON/OFF, but the disabled
point of several value pickers didn't: Settings' LowBat/GPS pwr/e-ink
full-refresh options and the auto-advert interval showed lowercase
"off", GPIO's mode row showed "Off" right above its own State row's
"OFF", and the GPS-averaging/trail-autopause pickers showed "Off" where
the alarm-repeat picker already said "OFF". All display-only label
arrays, no behaviour change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Introduced consistent preferences for external LoRa FEM RX and TX gain settings in NodePrefs. Updated companion MyMesh to apply these settings during initialization and transmission. Added unit tests to verify the round-trip serialization of these new preferences.
Three power-saving additions, prompted by comparing this fork's existing
RX duty-cycle support against IoTThinks/EasySkyMesh:
- RX duty-cycle watchdog: the SX126x's hardware RX<->sleep sequencer runs
with no MCU polling, so a desync (a known failure mode) previously had
nothing watching for it. A new watchdog samples the BUSY pin every tick;
no transition for too long triggers a soft re-arm, then a full chip
reset (with cached radio params reapplied, since std_init() resets to
compiled firmware defaults) if that doesn't clear it. Soft/hard recovery
counts surface on Tools > Diagnostics > Live as "RXPS wd s/h".
- Noise-floor recalibration during power-save: sampling was previously
skipped entirely while duty-cycling, freezing int.thresh interference
detection at whatever the floor was when power-save turned on. Now
borrows a brief continuous-RX window once a minute to take a fresh
reading before re-arming duty-cycle.
- GPS duty-cycling (Settings > System > "GPS pwr"): cycles GPS off between
fixes instead of running it continuously. Each wake waits for a fix
(capped at 60s) before sleeping again for the configured interval.
Repurposes the long-dead NodePrefs::gps_interval byte rather than adding
a new persisted field. A "is anything live using GPS right now" hold in
UITask keeps GPS continuously on whenever trail recording, live-share,
an armed Locator, or the Compass/Nearby-navigate view actually need a
live fix, so none of those features degrade. Locator crossing-state is
reset on each wake so a still-settling first fix can't read as a false
geofence crossing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add Settings > Keyboard "Ext. KB" row (boards with a CardKB-capable I2C
bus only): switching it to Compact hides the letter grid and special-row
icons in favour of a one-line status (script/page, caps) plus a Fn-shortcut
reminder, since an external-keyboard typist never looks at the on-screen
grid. Accent/placeholder popups still render as before. Off by default.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Auto-Reply Bot gains Actions (!buzz/!gps/!advert) behind a new per-target
toggle nested under Commands (bot_actions_dm/ch/room); off by default.
- Bot Trigger fields accept comma-separated multiple phrases, matching any
one fires the reply.
- New user-assignable GPIO feature (Wio Tracker L1): !gpio1..!gpio4 bot
commands plus a Tools > GPIO screen. Each pin cycles Off/Input/Output;
GPIO1/GPIO2 (P0.02/P0.29, the nRF52840's AIN0/AIN5) also offer a read-only
Analog mode via direct SAADC access. GPIO3/GPIO4 (P0.09/P0.10) are the
chip's NFC1/NFC2 pins, repurposed as plain GPIO via a one-time UICR
NFCPINS bit-clear in initVariant() (adapted from Adafruit's own
nfc_to_gpio example) -- confirmed working on real hardware.
- Fix: DM/room reply-prefix ("@[nick] ") stripping happened at the wrong
layer, hiding the "To:" header on DM replies and leaking the raw prefix
into room messages' list view; a related mismatch had the history
scrollbar's sizing pass wrap room messages with the sender name still
attached, disagreeing with the actual rendered text.
Build-verified: WioTrackerL1_companion_solo_dual and
WioTrackerL1Eink_companion_solo_dual both compile and link clean
(sizeof(NodePrefs) confirmed 2720 via real build, not guessed).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>