Commit Graph
100 Commits
Author SHA1 Message Date
JakubandClaude Sonnet 5 dd170874d2 Merge pull request #32 from 3urobeat/feat/lock-screen-page-type
feat: Make lock screen a page type and display title bar on it

Moves lock-screen rendering out of a dedicated branch in UITask::loop()
into a proper HomePage::LOCK page in HomeScreen (appended after Count,
so no existing HomePage/home_pages_mask bit shifts), adds a title bar to
the lock screen (node name omitted, intentionally, for at-a-glance
anonymity), and makes drawClockTime()'s center-alignment opt-in so the
lock screen can left-align its clock. Reviewed: the two existing
drawClockTime() call sites are both updated to keep their prior
alignment, and this doesn't interact with our home_pages_mask=0 default,
msg_wake_screen_off, or the shutdown()-flush fix landed this cycle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 10:41:18 +02:00
3urobeat 376fcd6bdd refactor: Make screen lock a dedicated page
This commit
- adds LOCK to the HomePage enum
- moves the lock page code from loop() to the selected page handler
- adds a forward declaration for formatDashVal to make it callable from within the selected page handler
- adds a syncLockToHome function that triggers a (un)lock page switch
2026-09-05 20:40:13 +02:00
JakubandClaude Sonnet 5 b2cf459460 fix(sim): text-width/render bugs, splash version, wasm-fetch error, battery lag
Code-review pass over the buzzer/sim commits turned up several real bugs,
plus two issues found afterward from manual browser testing:

Rendering (SimDisplayDriverCanvas, variants/sim/SimDisplayDriver.h + target.cpp):
- getTextWidth() measured UTF-8 BYTES (strlen()*6), not codepoints. Since
  b067e95b stopped stripping accents, any accented string now measures
  double its real width -- mis-centred titles, premature ellipsis/marquee,
  badges pushed off-screen. Now uses the real MiscFixedRenderer measurement
  (miscFixedTextWidth()), same as SH1106Display/SSD1306Display.
- Added the matching getCodepointWidth() override (O(1) single-glyph
  advance), same pattern as SSD1306Display.
- isSingleFont() was left at the base class's `false`, though this backend
  only ever renders MiscFixed -- UITask.cpp's status-bar indicator height
  keys off this (`lh-2` vs `lh`), so the sim drew it 2px taller than a real
  board.
- print() blitted the full 128x64 canvas on every call (dozens per frame,
  60fps) -- now tracks a dirty bounding box and only clears/blits the
  region actually touched.

Web Audio (buzzer bridge, index.html + mesh.html):
- No AudioContext.resume() -- a context created (or later suspended) in the
  'suspended' state (Safari/Firefox, or any browser backgrounding the tab)
  stayed silent forever. Now resumed on every gesture.
- linearRampToValueAtTime with no anchoring setValueAtTime interpolates
  from the LAST scheduled event, not "now" -- so the anti-click ramps could
  effectively snap instead of fading. Fixed with cancelScheduledValues +
  setValueAtTime(current) before each ramp.
- mesh.html: a gesture only armed the clicked instance's audio. Click A,
  send A->B, and B (the one actually meant to beep on receipt) stayed
  silent. Now any gesture arms both A and B.
- RTTTL rests (freq=0, still "playing") now explicitly hold pitch and drop
  gain instead of it happening to work by coincidence.

Misc: sim_test_get_num_contacts() was missing the g_sim_ready gate every
other sim_test_* hook has, so it could return a bogus negative count before
setup() finishes seeding num_contacts.

Splash screen missing "Solo <version>" bar: variants/sim never defined
FIRMWARE_SOLO_BUILD (every real Solo board does), so SplashScreen silently
skipped that whole line -- the sim looked like a plain non-Solo companion
build. Added -D FIRMWARE_SOLO_BUILD=1 to platformio.ini and build_wasm.sh.
Verified on a real canvas screenshot: "MESHCORE 1.17.1 / 19 Aug 2026 /
Solo v1.27".

Wasm-fetch error message: "failed to start: RuntimeError: Aborted(both
async and sync fetching of the wasm failed)" is Emscripten's own opaque
message for the single most common real cause -- the page opened via
file://...index.html instead of served over http(s) (fetch() on a local
file is blocked by CORS in both Chrome and Safari, confirmed by reproducing
the exact same error/stack via file://). Both harnesses now detect
location.protocol === 'file:' and show an actionable message with the
one-line fix instead of the raw stack trace.

Battery-set latency: SimMainBoard's battery value is an exact, instantaneous
JS-set integer (see sim_battery_set_mv()), but UITask's battery-check code
polls it every 8s and runs it through an EMA (alpha=0.2) meant to smooth a
REAL board's noisy ADC -- so a value typed into the demo UI could take tens
of seconds to visibly settle. SIM_PLATFORM now checks every 250ms and skips
the EMA (nothing to smooth), since the reading is already clean. Measured
on real canvas pixels: indicator update now lands within one screen-refresh
cycle instead of up to 8s+.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 19:32:23 +02:00
JakubandClaude Sonnet 5 f61d6832a7 feat(sim): real buzzer/RTTTL sound via Web Audio
genericBuzzer (src/helpers/ui/buzzer.h/.cpp) gets a third platform branch,
#elif defined(SIM_PLATFORM), alongside the existing NRF52 (direct PWM) and
NonBlockingRtttl paths -- purely additive, no changes to either real-hardware
branch. It reuses the NRF52 branch's already hardware-free RTTTL parser
(_parseHeader/_parseNext/_noteFreq, now shared via a widened guard) but
tracks (current frequency, note-end-time) instead of touching real PWM/timer
registers, advancing on plain millis() polling from loop() -- same
non-blocking shape UITask already drives every tick.

Wired into the sim build the same way every real board sets its buzzer pin
(-D PIN_BUZZER=<n> in build_flags/DEFINES; here it's a dummy sentinel since
there's no real pin, just something to activate the existing #ifdef
PIN_BUZZER guards in UITask.h/.cpp/SoundNotifier.h unchanged), plus two new
small UITask accessors (isBuzzerPlaying/buzzerFreqHz/buzzerVolume) and three
EMSCRIPTEN_KEEPALIVE exports so a host page can poll the buzzer's state.

Browser side: one Web Audio oscillator+gain per companion instance (index.html
single-instance; mesh.html per A/B, not R which is headless), created lazily
on the first real user gesture (AudioContext autoplay policy), polled every
20ms and mapped to the oscillator frequency/gain -- so every notification
sound, ringtone, alarm, and volume-blip that already worked on real hardware
now actually produces audio in the browser, unchanged at the call-site level.

Verified end-to-end with real RTTTL playback traces (not just "no errors"):
the startup jingle's exact note frequencies (C6/E6/G6) and a real DM-received
notification triggering the receiving instance's Web Audio gain node from 0
to its mapped volume and back, matching the actual "MsgRcv3" melody's notes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 13:59:26 +02:00
JakubandClaude Sonnet 5 725e3b715e feat(sim): long-press context menu + pixel-perfect real font rendering
Two prototype polish fixes ahead of the website embed work:

1. Holding Enter (the sim's main way into "the rest of the options") did
   nothing -- the JS bridge's sim_enqueue_key() went straight to
   enqueueKey(), bypassing UITask::handleLongPress() entirely, so
   KEY_CONTEXT_MENU could never be reached. Add injectSimKeyLongPress()/
   sim_enqueue_key_longpress(), which does route through the real
   handleLongPress() (same code a real MomentaryButton(pin, 1000, ...)
   reaches), and wire up press-and-hold (buttons + Enter/Space key) in both
   web harnesses with the same 1000ms threshold real hardware uses.

2. SimDisplayDriverCanvas::print() drew text with the browser's own system
   font (ctx.fillText, '8px monospace') instead of the real bitmap font a
   MeshCore-Solo board renders with OLED_MISC_FIXED_FONT=1 (see
   solo/heltec_v3/platformio.ini). Vendor the real Adafruit_GFX (unmodified,
   from the same PlatformIO registry package a real board build pulls) into
   variants/sim/thirdparty/gfx/, and render print() through the real,
   shared src/helpers/ui/MiscFixedRenderer.h + MiscFixedFont.h -- byte-
   identical glyphs to real hardware, not a look-alike.

Verified in a real Chromium (Playwright): short Enter -> "CLOCK TOOLS",
held Enter -> "CLOCK FIELDS" (gotoDashboardConfig(), proving
KEY_CONTEXT_MENU is really reached); font renders as hard square pixels,
inverse/selected-row text still punches correctly through a filled bar.
Full regression re-run clean: all 3 native envs, wasm companion_radio, and
the two-instance + repeater mesh demo (relay routing, DM delivery).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 10:06:44 +02:00
Jakub 8f4c92a217 feat(sim): add variants/sim/ — real companion_radio firmware on native + Emscripten
New board variant compiling the unmodified MyMesh/UITask/DataStore app
logic against real mesh::Radio/MainBoard/RTCClock/RNG interfaces, for
running the actual firmware outside embedded hardware:

- Native (plain g++, platform = native): ASCII-art display over stdout,
  stdin-driven input, local-disk-backed DataStore/IdentityStore.
- Emscripten/WASM (variants/sim/build_wasm.sh, since PlatformIO's native
  platform force-overrides any CC/CXX toolchain override back to system
  clang++): canvas-backed display, IDBFS-backed persistence across page
  reloads, JS-callable input via sim_enqueue_key(), emscripten_set_main_loop.

Real rweather/Crypto (AES128/SHA256/Ed25519) vendored unmodified and
proven working on both targets. variants/sim/web/index.html is a bare
verification harness, not the polished website embed.
2026-09-03 00:46:47 +02:00
JakubandClaude Opus 5 57851627cf feat(ui): make favourites mean one thing across every list
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>
2026-08-31 15:02:01 +02:00
JakubandClaude Sonnet 5 ba3c3cc91f fix(mesh): don't mark our own channel post as unread
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>
2026-08-30 20:26:57 +02:00
JakubandClaude Sonnet 5 bdd2ed379d feat(mesh): show which repeaters relayed/confirmed a message's path
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>
2026-08-30 18:04:06 +02:00
JakubandClaude Sonnet 5 389f3f7a36 feat(mesh): mirror app-originated DM/channel sends into on-device history
CMD_SEND_TXT_MSG and CMD_SEND_CHANNEL_TXT_MSG (the phone app's send path)
transmitted over the mesh but never touched the device's own MessagesScreen
history, unlike a message composed on-device (MessagesScreen::afterSend) --
so a DM/channel post sent from the app was invisible if that same
conversation was later opened on the device's own screen. Both handlers now
also call into the same history-store entry points incoming messages use.

Also wires up delivery-status parity with an on-device send, not just the
raw text:
- Channels: arms the existing "relayed into mesh" repeater-echo tracker
  (trackRelaySend()/armChannelRelay()) on the new entry -- sendGroupMessage
  already runs that tracker regardless of who originated the send, this
  just attaches it to the right history entry. Required threading a ring
  position back out through AbstractUITask::addChannelMsg (now returns int)
  and a new armChannelRelay() passthrough.
- DMs: addDMMsg gained ack_tag/ack_deadline_ms/resends params (threaded
  through MessageHistory -> MessagesScreen -> AbstractUITask/UITask) so an
  app-sent DM gets the same pending -> \xe2\x9c\x93/\xe2\x9c\x97 status the on-device compose
  path shows. resends stays 0 deliberately: the app owns its own retry
  decision, so this only drives the on-screen status, never a second,
  independent auto-resend from the device itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 17:35:13 +02:00
Jakub ca1192c491 merge: PR #31 (CardKB via shared I2C bus for ProMicro) into 1.26 2026-08-29 10:14:15 +02:00
tchellow 8fdb03bd9a Rebase onto main, move CARDKB_I2C out of class body, resolve via platformio.ini 2026-08-28 10:55:52 -03:00
JakubandClaude Sonnet 5 b71b46fd84 fix(ui): debounce the Hall sensor poll against reed-switch contact bounce
pollHallSensor() acted on the raw pin reading immediately, unlike every other
physical-input path in this file (MomentaryButton, pollCardKB()'s own
last-raw edge check). A cheap mechanical reed switch -- one of the two
sensor types the docs explicitly recommend wiring here, alongside a
solid-state Hall IC -- can chatter for a few ms while the magnet crosses the
trigger distance, so a poll every loop() tick during that window could flip
_locked and fire _display->turnOff()/turnOn() repeatedly in that short span:
wasted work on any panel, and a real cost on e-ink where each is a slow
full-panel operation.

A raw reading now has to hold steady for HALL_DEBOUNCE_MS (25ms, same
threshold as MomentaryButton's ISR_DEBOUNCE_MS) before it replaces
_hall_magnet_present and triggers the lock/unlock actions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 10:22:50 +02:00
JakubandClaude Sonnet 5 58e6bfcec5 feat(ui): optional magnetic flip-cover screen lock via user-wired Hall sensor
No board in this repo has one built in, and no default pin is assumed
anywhere -- whoever wires a Hall-effect or reed sensor to a free GPIO sets
PIN_HALL_SENSOR (and HALL_ACTIVE_HIGH, for a sensor that pulls the pin high
rather than low on presence) as a build_flag on their own env. Entirely
opt-in and a no-op elsewhere, same pattern as PIN_GPIO1..4/ADC_MULTIPLIER/
CARDKB_ENABLE.

Level-triggered polling (like pollCardKB()) rather than an edge interrupt --
a magnet held near the sensor reads the same way every tick, so the new
pollHallSensor() only acts on the two transitions. Closing locks and blanks
the display with no wake grace (the cover is physically over the screen, so
there's nothing to show); opening unlocks and wakes it, with no key combo
either way. Both are independent of the Auto-lock setting, which is a
timeout, not a physical event.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 13:48:33 +02:00
tchellow 55203c2ddd Add CardKB support for ProMicro via shared I2C bus 2026-08-23 21:44:17 -03:00
JakubandClaude Opus 5 73744175b0 feat(power): RX duty-cycle watchdog, noise-floor recal, GPS duty-cycling
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>
2026-08-07 22:14:19 +02:00
JakubandClaude Opus 5 07c80cd548 fix(keyboard): count preview line breaks in codepoints, mark caps-lock
The text preview was the last part of the keyboard still working in bytes
rather than codepoints. cpl is how many characters physically fit on a
line, so dividing byte offsets by it counted every 2-byte Cyrillic/Greek/
accented character as two: lines held half the text they had room for, and
a break could land inside a codepoint. Both display drivers are
permanently single-font, so translateUTF8ToBlocks() passes UTF-8 straight
through -- the truncated sequence reached print() and drew as garbage on
both sides of the break. Line boundaries now walk the buffer with the same
kbUtf8*() helpers insertion/backspace/T9 already use, and the per-line
buffers are sized for a full line of 2-byte characters.

Caps-lock also gets an underline on the shift key: it sets caps too, so
the highlight alone made a one-shot Shift and a held lock indistinguishable
despite capitalising one letter vs. every following one.

Drops UITask::applyFont() -- setSingleFont() is a no-op on both drivers
since they were pinned to misc-fixed, so it did nothing, and use_lemon_font
has had no Settings row for a while. The pref itself stays: it's part of
the on-disk layout. Retires the matching stale rationale on scriptHint().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 23:24:42 +02:00
JakubandClaude Sonnet 5 8e5f083e2e fix(ui,bot): resolve remaining findings from the 2026-07-26 screen review
- DM/room unread badges could claim messages the ring no longer held
  (same class as the channel fix in 6470afaf, not covered by it):
  getDMUnread()/getDMUnreadTotal() now clamp to dmHistCountForContact(),
  and a new reconcileDMUnread() (called once per loop()) frees any
  _dm_unread_table slot whose ring occupancy has dropped to 0, so a
  17th sender isn't starved by stale entries. onContactRemoved() now
  also clears _dm_unread_table -- the one per-contact table it was
  missing.
- Shift didn't capitalise ł/ń/ź/ż (+ĺ/ľ/ň/ž): the Latin Extended-A
  case-pairing rule assumed a single parity for the whole block, but it
  flips around the unpaired codepoints ĸ/ʼn/Ÿ. Fixed with four
  sub-ranges, verified exhaustively over U+0100-U+017F.
- Triple-click could still toggle the buzzer while locked on
  PIN_USER_BTN/PIN_USER_BTN_ANA boards (joystick path already guarded
  this).
- millis() wraparound: 4 absolute comparisons in UITask.cpp (battery
  poll, auto-off, lock-wake, backlight) converted to the existing
  (int32_t)(millis()-deadline)>=0 idiom; MyMeshBot.h's DM-throttle
  eviction now picks the oldest slot by elapsed time instead of raw
  t_ms, which picked the wrong slot right after a rollover.
- Long-press bypassed checkDisplayOn() on all 5 call sites -- neither
  woke the display nor extended auto-off, and could deliver
  KEY_CONTEXT_MENU to the invisible screen. Moved the gate inside
  handleLongPress() itself instead of patching each site.
- CardKB's backspace/printable-insert branches didn't reset t9_cell,
  so typing right after a T9 cycle tap could get silently overwritten
  by a same-cell re-tap within the T9 timeout.
- buildContactList()'s counts[MAX_CONTACTS] was a 1400 B int array on
  the 4 KB loop() stack; values are bounded by DM_HIST_MAX (32), so
  now uint8_t.
- ACK table treated ack==0 as a wildcard: isAckPending(0) matched any
  free slot, and processAck() with an all-zero ACK matched the first
  free slot and returned its stale contact pointer. Both now skip/reject
  ack==0, and the matched slot's contact pointer is cleared alongside
  its ack hash.
- ensurePageOrderInit() could write one byte past page_order[13] when
  migrating a saved order with all 13 slots full and CLOCK last --
  guarded on insert_at < PAGE_ORDER_LEN.

Two findings from the same review were resolved as no-op decisions,
not code changes: !buzz over DM ignoring quiet hours is intentional
(the pull exemption is meant to cover the buzzer), and the offline
queue's full-queue drop-newest behaviour is upstream code, left alone.

Build-verified green on WioTrackerL1_companion_solo_dual (RAM 71.1%,
Flash 66.6%) and WioTrackerL1Eink_companion_solo_dual (RAM 73.0%,
Flash 67.9%).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 22:05:52 +02:00
JakubandClaude Sonnet 5 ad4668242b fix(ui): CardKB key handling - debounce, Fn modifier for submit/accents
CardKB is level-triggered (repeats the held byte every poll) and its Enter
key collided with the on-screen keyboard grid's own commit action, causing
duplicate characters and accidental message sends. Debounce polling and use
the CardKB v1.1 Fn modifier (confirmed working on real hardware) instead of
tracking navigation state: plain Enter now behaves like the physical centre
button, Fn+Enter submits, Fn+Tab opens the Hold-Enter equivalent, and
Fn+<letter> opens that letter's accent popup directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 17:49:04 +02:00
JakubandClaude Sonnet 5 7cae6470bf feat(ui): CardKB (I2C keyboard) support with full keyboard-only navigation
Auto-detected at boot on Wire1/Grove (addr 0x5F) -- no setting to flip, and a
no-op on boards without that bus or with nothing attached. This UI's key
codes (KEY_LEFT/UP/DOWN/RIGHT/ENTER/CANCEL) already match CardKB's own byte
protocol, so most input needs zero translation and flows through the same
key queue as physical buttons.

Two bytes get remapped in UITask::pollCardKB():
- Enter, only when the on-screen keyboard's plain grid state is active (no
  placeholder/accent popup, not in cursor-mode), becomes a new KEY_KB_ENTER
  sentinel meaning "submit the field" -- reusing plain KEY_ENTER there would
  insert a stray character, since a CardKB typist's row/col never reflect an
  intentional grid selection. Everywhere else Enter is untouched, so
  selecting a placeholder or committing an accent still works normally.
- Tab (otherwise unused) becomes KEY_CONTEXT_MENU, standing in for the
  "Hold-Enter" long-press gesture CardKB has no way to produce -- without it,
  ~30 context menus across the UI (message reply/navigate, Bot/Admin/
  Repeater, ...) would be unreachable from the keyboard alone.

KeyboardWidget gains a direct-typing path: printable ASCII inserts straight
at the cursor bypassing the grid, Backspace deletes, KEY_KB_ENTER submits.

Build-verified: WioTrackerL1_companion_solo_dual and
WioTrackerL1Eink_companion_solo_dual both compile and link clean; also
smoke-tested Heltec_mesh_solar_companion_radio_ble (no ENV_PIN_SDA/SCL) to
confirm zero regression on boards without the feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 23:43:33 +02:00
JakubandClaude Sonnet 5 5bfebc6559 feat(bot): Actions commands, multi-trigger, and user GPIO pins
- 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>
2026-07-21 20:30:03 +02:00
JakubandClaude Sonnet 5 453dc5e570 feat(admin): typed Radio/Routing field editors; pre-release audit fixes
Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string
field (free-text keyboard) into 4 independent, type-appropriate rows --
Frequency (same digit-cursor DigitEditor Settings/Repeater use locally),
Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping),
plus TX power and the 3 Routing numeric fields as number steppers and
Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per
keystroke); only Enter sends one combined `set`, Cancel sends nothing.

Full pre-release audit of all 16 commits since v1.22 (5 parallel focus
areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels,
UITask+NodePrefs schema) turned up and fixed:

- Admin: fetched FK_NUMBER values weren't clamped to the field's range,
  so a value already out-of-range could get stuck unreachable; fixed
  commit-time float format noise (%.6f -> %.3f); Cancel/failed-login
  always returned to the Nodes picker even when Admin was opened
  directly from a node's Hold-Enter action -- now returns to wherever
  it was actually opened from (AdminScreen::_from_picker).
- Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap,
  so cycling to the 2nd/3rd candidate always came out lowercase --
  fixed by caching the cycle's caps state (t9_caps).
- Messages: history is numbered newest-first, so a message arriving
  while scrolled up to an older one silently relabeled the view onto a
  different message -- selection now shifts with the insert.
- Channels: onChannelRemoved() didn't clear ch_notif_override/
  ch_notif_muted/ch_fav_bitmask despite its own contract comment
  requiring it (now far more reachable via the on-device Delete);
  an all-zero hex secret silently self-deleted the channel it was
  just saved into (collides with the empty-slot sentinel) -- rejected.
- Room login: isRoomLoggedIn() indexed the login-tracking ring
  directly instead of via its head offset, silently wrong once the
  ring wraps (8+ rooms/session) -- the new on-device Logout depends
  on this being right.
- Font/display: removed the now fully-inert Settings > Display > Font
  toggle and the dead LemonFont.h (retired by the earlier misc-fixed
  font unification, zero remaining includes); fixed a copy-pasted
  "5x7" comment (font is 6x9), two meaningless dead ternaries, and an
  OLED/e-ink inconsistency in the undefined-glyph fallback box offset.
- AdminField's `kind`/bounds fields no longer rely on default member
  initializers inside aggregate-init: this toolchain's actual nRF52
  build (unlike env:native) has no explicit -std= override, so it
  predates C++14's aggregate-with-default-member-initializer rule.
  Given an explicit constructor instead -- portable regardless of
  standard, all existing field-table literals unchanged.

Docs updated to match: tools_screen.md (Diagnostics as a 3-tab
carousel, was documented as one flat screen), message_screen.md
(chat bubbles, newest-at-bottom, cursor mode, secret validation),
settings_screen.md (dropped the dead Font row), solo_ui_framework.md
(header menu_hint signatures, icon priority-drop, KeyboardWidget's
T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section
covering all of the above plus the other 15 commits since v1.22.

Build-verified on WioTrackerL1_companion_solo_dual.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
JakubandClaude Opus 4.8 56aaff1f76 refactor(admin): make Admin remote-only, move device settings to Settings
A device administering itself via the Admin tool read as awkward, so the
local mode is gone and its options relocated to Settings.

- AdminScreen is remote-only: drop the CHOOSER phase, _local_mode, the
  LocalField/LocalKind model and activateLocalField/commitLocalField.
  Phases collapse to LOGIN/COMMAND/REPLY, entered only via startFor().
- Tools > Admin opens straight to the Nodes picker (ACT_ADMIN ->
  pickAdminTarget()). Backing out of a command screen returns to that
  picker; the picker's Cancel returns to Tools. gotoAdminScreen() removed.
- Settings > System gains Name (keyboard-edited node_name) and Reboot
  (action row, placed last so it isn't the default cursor). Radio + TX
  power were already in Settings; Send advert is the home ADVERT page.

Docs: tools_screen.md Admin section rewritten remote-only; settings_screen.md
System table gains Name + Reboot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 09:10:24 +02:00
JakubandClaude Opus 4.8 9bfccf4cf4 feat(admin): local-device mode + reuse Nodes screen for target picking
Tools > Admin now opens on a This-device / Remote-node chooser. "This
device" is a login-free 2-tab carousel (System: Name/Radio/TX power/
Lat/Lon, Actions: Send advert/Reboot) mapped onto NodePrefs/sensors and
reusing Settings' own apply chains -- no CommonCLI port. "Remote node..."
opens Tools > Nodes in a pick-mode (borrow-another-screen's-list idiom),
and Nodes gains an "Admin" Hold-Enter action; both converge on the single
canonical UITask::openAdminFor() -> AdminScreen::startFor().

Also:
- KeyboardWidget: opt-in PlaceholderRefreshFn hook for contextual {}-key
  CLI command-name completion (word-replacing); default behaviour and all
  other keyboard uses unchanged.
- TabBar: ellipsize() re-measures the real "text..." candidate per step
  (+ width clamp) so dots can't spill into a neighbour/reserved icon.
- ChannelsView: form rows truncate instead of wrapping.
- Reboot moved off the default-selected Actions row (fires with no confirm).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:32:04 +02:00
JakubandClaude Sonnet 5 f399298fa6 feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
  MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
  Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
  the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
  onChannelRemoved sequence previously duplicated across the two
  CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.

Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
  the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
  with common get/set fields (name, radio profile, tx power, repeat, advert
  intervals, ...) plus a free-text "Custom command..." fallback for anything
  else. A field row fetches the current value, opens it pre-filled for
  editing, and sends the change — falling back to a blank editor if the
  fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
  Messages: saved on a confirmed admin-level login, forgotten on a failed
  one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
  a reply reaches the UI without touching the existing BLE/app CLI-terminal
  path (queueMessage's should_display gate is untouched).

Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.

Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
JakubandClaude Sonnet 5 afd7c0ee78 feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.

- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
  bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
  DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
  at 2712 (new bytes absorbed existing padding, verified via a
  standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
  the old shared bot_commands_enabled on upgrade so existing
  channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
  shape but post via sendMessage (room relays to members itself);
  requires an existing login session with that room, same as a manual
  post would. botDmSenderAllowed() gates DM trigger-reply/commands on
  the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
  nullptr/-1, no existing caller affected) — deliberately not exposed
  on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
  picker), routing through the existing room-login prompt when there's
  no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
  rows, Enter is now the only way to change a value); Enable split out
  of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
  Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.

Not build-verified — no PlatformIO toolchain in this environment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
JakubandClaude Fable 5 9a71ee14b6 fix(solo): six fixes from the 2026-07-05 UI audit
- locked device: a ringing alarm/timer was invisible — wakeForAlarm() now
  holds the lock wake window for the whole ring, the lock screen draws the
  alert overlay, and a key-dismissed ring falls back to a 5 s glance
- status bar: A / live-share / trail / repeater icons no longer vanish when
  Bluetooth is off (moved outside the isSerialEnabled() gate)
- alert overlay: long text wraps to up to 3 lines inside the box instead of
  overflowing the border (new UITask::renderAlertOverlay, shared with lock)
- MyMesh: force NUL on contact.name copied from an app frame (unterminated
  name could overrun the AdvertPath strcpy)
- clock tools: ring/timer deadline compares now wrap-safe (signed diff),
  matching the trail/loc-share timers
- messages: channel context menu freezes its target channel at open; fav
  toggle no longer retargets the menu when the fav-only filter drops the row

All three solo envs build green (WioTrackerL1 OLED/Eink, GAT562-30S).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:08:44 +02:00
MarekZegare4 78dba5e807 Merge branch 'main' into fix/eink-button-irq-edge-capture
# Conflicts:
#	examples/companion_radio/ui-new/UITask.cpp
2026-06-30 22:13:18 +02:00
MarekZegare4andClaude Opus 4.8 8c70acc3ef fix(eink): queue input so a burst of taps survives a blocking refresh
The joystick directions and Back were never begin()'d, so on the e-ink build
they never claimed a GPIOTE channel — edges landing during a blocking panel
refresh were lost and the IRQ-capture work didn't reach them. begin() them.

Even with edges captured, rapid taps of one direction replayed into a single
check() and collapsed into a double/triple-click event the navigation handler
ignores, and the loop only ever dispatched one key per render. So:

- MomentaryButton: for multiclick=false buttons emit one CLICK per completed
  release (one per check() call) instead of collapsing — each tap stays a
  discrete key. multiclick=true buttons (double/triple) are unchanged.
- UITask: add a key FIFO; drain each direction fully into it, apply the whole
  queued burst, then redraw once. N taps captured during a refresh become N
  navigation steps at the cost of a single refresh. Also fixes losing a key
  when two buttons fire in the same loop iteration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:10:18 +02:00
MarekZegare4andClaude Opus 4.8 9ce04835da feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:

- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
  time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
  from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
  the clock) — small corrections still fire on time, a jump over the
  target still fires (late, up to 6 h). Disarms after firing. A bell
  icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
  rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.

Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).

E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).

NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:01:03 +02:00
MarekZegare4andClaude Opus 4.8 0e6bd743a6 refactor(companion): unify screen save-on-exit via UITask::savePrefsIfDirty()
The "_dirty bool, then if (_dirty) the_mesh.savePrefs() on exit" pattern was
duplicated across 9 screens with subtle divergence: some cleared the flag
after saving, some left it set and relied on onShow() to reset. Replace all
12 exit sites with a single _task->savePrefsIfDirty(flag) helper that saves
once only if dirty and always clears the flag, so the "did we touch flash?"
answer lives in one place and the reset is consistent.

Edit sites still mark the flag manually (inherent to change tracking); only
the persist-on-exit boilerplate is centralised.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:47:51 +02:00
MarekZegare4andClaude Opus 4.8 15716d2b03 fix(companion): nullptr-init screen pointers so a forgotten new() fails safe
Adding a screen touches 4 sites; 3 (member decl, gotoX decl, gotoX def) are
compile-checked, but a missed `new XScreen()` in begin() left the pointer
uninitialised and crashed at first navigation. Give every screen member an
in-class nullptr initialiser and bail early in setCurrScreen(nullptr) so the
mistake is an inert no-op instead of a null deref. Document the 4-site
registration contract on the member block.

A full registry table was considered and rejected: the named gotoXScreen()
methods are a depended-upon API (~30 call sites, menu dispatch + back-nav),
so a table would add an enum + indirection without removing them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:42:59 +02:00
MarekZegare4andClaude Sonnet 4.6 7a5247d5d4 fix(companion): show the sender's own timestamp for synced/queued messages
DM, room and channel history always stamped a message with receipt time
(rtc_clock.getCurrentTime()), even though the sender's real timestamp was
already available (and, for DMs/rooms, already threaded through to
addDMMsg -- just never used for display). Live-received messages hid this
because receipt lags origination by only seconds, but a room-sync replay or
an offline-queued message held by a repeater can arrive long after it was
sent, so a burst of backlog messages all showed as "just now".

storeDMMsg() now prefers msg_ts (falling back to receipt time only when
unknown). addChannelMsg() gained a timestamp parameter, threaded from
onChannelMessageRecv() down through AbstractUITask/UITask, with the same
fallback. The DM dedup check and outgoing-message timestamps are unaffected
(they use msg_ts directly, already correct).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 11:00:45 +02:00
MarekZegare4andClaude Sonnet 4.6 6a394e2d7a fix(companion): clear stale channel-index refs when a channel is removed
CMD_SET_CHANNEL clearing a slot (empty secret) left bot_channel_idx,
loc_share_channel_idx, and the per-channel melody bitmasks pointing at that
index. A new channel added later at the same slot would then silently
inherit the old one's bot target, Live Share target, or notification
melody. New onChannelRemoved() hook, mirroring onContactRemoved(), turns
the bot/Live Share channel target off (fail closed) and clears the melody
bits for that index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 00:39:10 +02:00
MarekZegare4andClaude Sonnet 4.6 5c4607cca9 fix(companion): clear stale Locator/Live Share/favourite refs on delete
Deleting a waypoint left the Locator pointed at coordinates that no longer
existed (it's a coordinate snapshot, so nothing noticed). Removing a contact
was worse: nothing cleared its favourite slot, its Locator/Live Share target,
or its per-contact mute/melody entry, so all four kept referencing a pubkey
that no longer resolved to anything.

- WaypointsView's Delete now calls UITask::clearTargetIfWaypoint() first.
- New AbstractUITask::onContactRemoved() hook, called from MyMesh.cpp's
  CMD_REMOVE_CONTACT handler, clears the favourite slot, the Locator target,
  and dm_notif/dm_melody entries for that pubkey. Live Share's DM target
  turns auto-share off instead of guessing a new recipient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 17:48:05 +02:00
MarekZegare4andClaude Opus 4.8 5d71fde980 fix(companion): prompt to enable GPS when starting a trail with GPS off
Starting trail recording with GPS switched off used to call setActive(true)
immediately -- the session timer ran while the screen sat on "Waiting for
GPS fix" forever and recorded nothing, with no hint that GPS was the
problem. Choosing "Start tracking" now opens a "GPS is off" confirmation
(Enable GPS & start / Cancel); confirming enables GPS and starts the
session. Behaviour is unchanged when GPS is already on.

Gated on a new UITask::hasGPS() so the prompt only appears on boards that
expose a toggleable GPS, not where GPS is simply absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 14:11:48 +02:00
MarekZegare4andClaude Opus 4.8 be9f3db8c6 feat(companion): on-device room login with saved passwords
Log in to a room server from the device UI with no phone app: picking a
room prompts for its password (blank allowed for open rooms), and a
context-menu "Login..." allows re-login. Successful passwords are
persisted to a dedicated /room_pw file so a previously-used room logs
back in after reboot without retyping; a failed login forgets the
(now-stale) saved password so the next attempt prompts again.

The room-password file is written via a temp file + atomic rename
(new DataStore::commitFile helper), matching the crash-safety of
contacts/channels persistence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 10:55:24 +02:00
MarekZegare4andClaude Opus 4.8 57774d41f3 feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).

Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
  messages to a channel or contact; live shares show as map pins with
  distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
  parsed in DMs, channel messages and room messages; DM shares name the
  sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
  (live [LOC] or last-known position), alert on arrive/leave or near/far,
  with an optional homing beeper (gated to arrive/both modes). Arm from
  Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
  favourites first, clearable via a "None" entry. Active target is drawn
  as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
  resolver (resolvePersonPos / activeTargetPos) that prefers a live
  [LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
  as they move and adds an ETA line; quick-share your own position from
  the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
  tick, and a connected trail line (was disconnected dots); status line
  shows tracked-node count, an arrow + distance to the active Locator/Nav
  target (falling back to the nearest live-tracked contact); GPS fix icon
  in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
  breaking the map line across the idle gap) and resumes on movement
  without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
  via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
  runs collapse to their endpoints, curves stay bounded to the Min-dist
  tolerance, so the 512-point buffer covers a far longer route than a
  flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
  like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
  digit by digit.

Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
  message-history scrollback rings (recovering ~14 KB free heap) and
  made contacts/channels/prefs persistence atomic (temp file + rename),
  so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
  shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
  v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
  index on load.

Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
JakubandClaude Sonnet 4.6 32c50d1cfe feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.

Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.

A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.

Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
JakubandClaude Sonnet 4.6 327e659d51 feat(ui): add on-device diagnostics screen (Tools > Diagnostics)
Shows packet counts by category (RX/TX), radio noise floor/RSSI/SNR,
packet-pool free count and outbound queue length, uptime, and heap/stack
headroom on a single scrollable screen.

Adds generic per-payload-type RX/TX counters to Dispatcher (mesh layer),
plus pool-free/queue-length getters, and a new DeviceDiag helper for
nRF52 heap (linker-symbol + sbrk) and stack (FreeRTOS high-water-mark)
stats — the only platform needed for the 3 in-scope boards (Wio Tracker
L1 OLED/Eink, GAT562 30S), all nRF52840.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:34:52 +02:00
MarekZegare4andClaude Opus 4.8 8c0ccaed73 feat(ui): keyboard overhaul — shared instance, icon keys, symbols page
- Unify on the shared keyboard: WaypointsView drives UITask::keyboard()
  instead of its own instance; openKb() clears {loc}/{time} for literal
  fields (labels, coordinates). Reclaims the duplicate widget from the heap.
- Icon glyphs on the special row: caps ⇧, space ⎵ (two halves), delete ⌫,
  OK ✓ (reuses ICON_CHECK); {} and the page toggle stay text.
- Second character page (symbols), toggled by a "#@"/"abc" key. Tidy the
  letters page: drop the duplicate grid space, add the comma, group
  punctuation as . , ! ?
- Harden the preview buffers (KB_PREVIEW_CAP) against very wide displays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:47:50 +02:00
MarekZegare4andClaude Opus 4.8 bcf8774962 feat(ui): message delivery status (DM ACK + channel relay)
End-to-end delivery indicators on the device UI, drawn next to outgoing
messages and auto-scaled to the font (legible on landscape e-ink).

DM (and room servers, which share the DM path):
- Pending / delivered / failed marker driven by the real end-to-end ACK.
  Pending shows a row of dots — one per send — so auto-resend progress is
  visible before it resolves to ✓ / ✗.
- Auto-resend: new pref dm_resend_count (0-5, default 2), Settings ›
  Messages › Resend. A pending DM whose ACK times out is re-sent (reusing
  the original timestamp) until resends run out, then ✗. Driven from
  UITask::loop so it completes in the background, independent of screen.
- Incoming dedup: a retry reuses the sender timestamp + text but carries a
  fresh packet hash, so addDMMsg drops copies matching prefix+ts+text.

Channels (flood, no recipient ACK):
- ✓ only once a repeater echo confirms the send was relayed into the mesh;
  no echo is normal, not a failure (no pending/fail shown). A small ring
  tracks a burst of sends so each matches its echo. Receive-path hashing is
  gated so the hot flood path is untouched when idle.

Shared:
- Markers shown in both the history list and the fullscreen message view.
- Reusable scalable mini-icon facility in icons.h (bitmap + auto-scale);
  adding a new status icon is a bitmap plus one draw call.

No changes to the upstream mesh library (src/).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:33:16 +02:00
MarekZegare4andClaude Sonnet 4.6 151892e1ce refactor(ui): extract shared helpers, dedup screens, GPS shutdown via provider
Pull repeated UI idioms into DisplayDriver and remove duplicated logic:

- DisplayDriver: add drawScrollArrows() and drawInvertedHeader(); replaces 11
  copy-pasted scroll-arrow blocks across 5 screens and 3 inverted title-bar
  blocks (NearbyScreen x2, NavView). Standardises the detail-view separator.
- useImperial(): single source on UITask; NearbyScreen/TrailScreen delegate
  instead of each re-reading NodePrefs.units_imperial.
- NearbyScreen: extract saveSelectedWaypoint() (two identical blocks -> one);
  drop the redundant local label buffer (WaypointStore truncates).
- UITask::shutdown(): power GPS off through LocationProvider::stop() instead of
  poking PIN_GPS_EN directly — centralises enable/reset-pin + active-level logic.

Net -58 lines. Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo
builds compile clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 13:21:59 +02:00
MarekZegare4andClaude Sonnet 4.6 ec4b7d625f fix(ui): force immediate refresh when BLE disconnects
UITask.onBLEDisconnected() sets _next_refresh=0 so the BLE icon
updates instantly instead of waiting for the next polling cycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 12:59:41 +02:00
JakubandClaude Opus 4.8 5b58049139 feat(power): battery saving — hardware duty-cycle RX + adaptive TX power
Two independent, default-off toggles under Settings › Radio.

Pwr save: hardware RX duty-cycle (SX126x SetRxDutyCycle via
startReceiveDutyCycleAuto). The chip cycles RX↔sleep and wakes on a preamble —
no MCU state machine; recvRaw reads the packet exactly as in continuous RX.
Falls back to continuous RX on non-SX126x. (Replaces an earlier software-CAD
state machine that fought the hardware: polling a warm-sleeping chip gave a
phantom-busy channel that stalled TX ~4 s and dropped ACKs in the scan gaps.)

Auto pwr: Adaptive Power Control. tx_power_dbm becomes a ceiling; actual TX
power tracks the reverse-link SNR margin (measured above the per-SF demod floor,
EWMA-smoothed, proportional step with a deadband). Feedback comes from direct /
room-server ACKs and, for channels (no ACK), from hearing a repeater rebroadcast
our own flood; a lost confirmation ramps power back up so channel sends can't get
stranded below what the repeaters can hear.

Prefs schema 0xC0DE0009 (rx_powersave, tx_apc). Radio page / name bar show the
live TX power; noise floor reads n/a while duty-cycling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 08:15:42 +02:00
JakubandClaude Sonnet 4.6 e6f8fb99c7 feat(NearbyScreen): waypoint/navigate from list, TIME filter, addWaypoint helper
NearbyScreen:
- Hold Enter on a list row now shows Navigate and Save waypoint directly,
  without entering the detail view first
- TIME added as the last filter slot (LEFT/RIGHT cycles Fav→ALL→…→Snsr→TIME);
  time-sort shows age in right column and sorts by last-heard descending
- Save waypoint also available from detail view Options menu

UITask:
- Extract addWaypoint() helper (two overloads: with/without explicit timestamp)
  consolidating the full()+add()+saveWaypoints()+alert pattern; TrailScreen
  and QuickMsgScreen updated to use it

Fixes:
- SoundNotifier.h: correct NodePrefs.h include path (../NodePrefs.h)
- kbAddSensorPlaceholders: dereference _kb pointer in SettingsScreen,
  QuickMsgScreen and BotScreen (signature changed to KeyboardWidget&)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:09:57 +02:00
JakubandClaude Sonnet 4.6 c339dc7d97 refactor(ui): share single KeyboardWidget instance across screens
Moves KeyboardWidget from a per-screen value member to a single instance
owned by UITask, passed to SettingsScreen, QuickMsgScreen, and BotScreen
as a pointer. Only one screen is ever active at a time, so the shared
state is safe. Saves ~1.5 KB of always-resident heap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:42:18 +02:00
JakubandClaude Opus 4.8 75f9ccb34a feat(nav): share a waypoint as a message
Waypoints list → Hold Enter → Send hands the point to the Messages screen
as "[WAY]<lat>,<lon> <label>" (same text format geo::parseLatLon already
reads). The user picks a contact or channel, the text lands prefilled in
the keyboard to confirm/edit, then sends — closing the loop with the
Navigate / Save waypoint actions on the receiving end.

- QuickMsgScreen: share-compose mode (startShare/beginShareCompose). Picking
  a recipient jumps straight to the prefilled keyboard; cancel returns home;
  afterSend clears the mode.
- UITask::shareToMessage hands off from TrailScreen to the Messages screen.
- TrailScreen: "Send" added to the waypoint Rename/Delete popup; builds the
  [WAY] payload at the {loc} precision (5 dp).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:38:12 +02:00
JakubandClaude Opus 4.8 55d164c074 refactor(nav): single GPS-position accessor + reset COG window after a gap
UITask::currentLocation() becomes the one place that reads the
LocationProvider fix. TrailScreen::ownPos, CompassScreen::gpsValid and
NearbyScreen::refresh now delegate to it instead of each duplicating the
getLocationProvider/isValid dance (and the ENV_INCLUDE_GPS guard).

pushCogFix now restarts the course-over-ground ring when the gap since the
previous fix exceeds 15 s: after losing GPS for a while, a window spanning
the stale old fixes would compute a bogus "teleport" heading. The
last-good heading is kept for display continuity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:12:15 +02:00
JakubandClaude Opus 4.8 8bf050ddb4 feat(nav): standalone Compass in Tools
Tools › Compass shows the device's course over ground (from the GPS COG
ring — no magnetometer) as a rotating arrow in a ring with a north tick,
plus a large degrees + cardinal readout. "No GPS fix" without a fix;
"move to set heading" when stationary (course undefined). Reuses
UITask::currentCourse, so it works without recording a trail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:07:53 +02:00