Commit Graph
991 Commits
Author SHA1 Message Date
JakubandClaude Sonnet 5 f589b9b2d1 feat(companion): on-device scope + repeater scope filtering; fix CAD, UTF-8 truncation, Public channel, Nodes list, keyboard cursor
- 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>
2026-08-21 10:40:50 +02:00
MarekZegare4 68527e7ba0 Merge upstream companion-v1.17.1 into power-saving
Pulls in real fixes: scoped-reply routing, RX boosted-gain restored
correctly after AGC reset, T-Echo Lite/Card SPI pin corrections
(unused pins now map to NRFX_SPIM_PIN_NOT_USED via 0 instead of -1)
and TCXO voltage fix, Heltec T096/T1 and MeshPocket pin fixes,
T-beam Supreme S3 display fix, LR2021 preamble/IRQ timeout handling.

None of the touched variants overlap with our active Heltec V3/V4,
Cardputer ADV, GAT562, or WioTracker builds.

# Conflicts:
#	examples/companion_radio/MyMesh.cpp
#	examples/companion_radio/MyMesh.h
#	examples/companion_radio/NodePrefs.h
#	src/helpers/radiolib/CustomSX1262Wrapper.h
#	src/helpers/radiolib/RadioLibWrappers.cpp
#	src/helpers/ui/SH1106Display.cpp
#	variants/lilygo_techo_lite/platformio.ini
#	variants/lilygo_techo_lite/variant.h
2026-08-14 15:58:23 +02:00
Scott Powell d929643524 * version 1.17.1 2026-08-14 22:19:19 +10:00
Scott Powell 890a2e2cff * commenting out the load/save of the fem_ properties, until they can be set 2026-08-14 16:30:12 +10:00
MarekZegare4 64d4b2982b fix(ui): wake display on T-Echo KeyShield Home key, drop dead code
Home key toggles the keyboard backlight but wasn't going through
checkDisplayOn() like every other TCA8418 key, so it couldn't wake a
sleeping display or extend the auto-off timer.

Also: removed a no-op #elif branch in ST7789Display.cpp (same values as
the #else it duplicated), and ENABLE_SCREENSHOT on the Cardputer ADV
solo env, which does nothing since ST7789Display has no getBuffer().
2026-08-14 01:07:06 +02:00
MarekZegare4 55cb5d25d3 fix(gps): hold GPS awake during waypoint nav/avg/track-back and message-location navigate
These views run the same live bearing/distance readout as Compass/Nearby's
navigate mode, which already held GPS awake -- these three didn't, so
duty-cycling could leave them stuck on a stale fix until the next scheduled
wake (up to the configured sleep interval).
2026-08-14 00:40:50 +02:00
MarekZegare4 cc591e8cee chore: bump dev-fallback version to v1.25-dev, fix stale comment casing 2026-08-12 23:58:03 +02:00
MarekZegare4andClaude Sonnet 5 6c7c9f3f83 fix(prefs): snap legacy gps_interval values to a valid duty-cycle preset
The pre-v1.13 "GPS Interval" setting (hidden from Settings ever since,
but its byte kept "for backwards compatibility") used a different option
set than today's duty-cycle presets -- its old 30s choice isn't one of
them. A device that had it set to 30 would load that value straight into
the new duty-cycle scheduler while "GPS pwr" in Settings showed OFF
(gpsDutyIndex() found no matching preset), silently cycling GPS on a
setting nobody could see or change. Unrecognised values now reset to OFF
on load, same as the existing out-of-range clamp this replaces.

Also refreshes MyMesh.h's FIRMWARE_VERSION/FIRMWARE_BUILD_DATE fallback
(only ever used by a `pio run` that bypasses build.sh entirely) -- it
was still "v1.17-solo.0" from 12 tags ago.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 22:43:15 +02:00
MarekZegare4andClaude Sonnet 5 721f892307 fix(ui): unify "off" casing to OFF across all screens
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>
2026-08-12 22:08:20 +02:00
MarekZegare4andClaude Sonnet 5 c045df1da5 fix(power): stop GPS duty-cycling from starving !gps fix and fighting manual toggles
Two races between the new GPS duty-cycle scheduler and code that changes
GPS state independently of it:

- gpsDutyCycleLoop() capped every "GPS on" phase at a fixed 60s and would
  stop_gps() as soon as a fix went valid, with no awareness of an
  in-flight "!gps fix" bot request -- so a fix's own up-to-300s acquire
  window (and its 10s averaging phase) could get cut short by the
  scheduler shutting GPS off mid-request. MyMesh::isGpsFixPending() now
  feeds into UITask's existing "is anything live using GPS right now"
  hold, alongside trail/live-share/locator/nearby.

- setSettingValue("gps", ...) (Settings toggle, bot !gps on/off, CLI)
  starts/stops GPS directly without resetting the scheduler's own phase
  timer, so a manual toggle could land on a stale, already-expired
  deadline left over from before -- immediately re-stopping GPS a tick
  after turning it on. The phase timer now resets on every external
  change, so the next duty-cycle tick re-arms fresh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 22:08:13 +02:00
MarekZegare4andClaude Sonnet 5 22b05ada52 fix(ui): show unread count on the locked-screen Messages dashboard field
formatDashVal(), the locked-screen counterpart to the unlocked clock's
dashboard rendering, never learned about DASH_MSGS and fell through to
an empty string for it, so Messages was the one CLOCK FIELDS choice
that showed nothing at all once the screen locked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 22:08:03 +02:00
ripplebizandGitHub a4ab7f0e59 Merge pull request #3137 from agessaman/feat/station-g3-fem-prefs
Station G3: Expose, persist, and apply FEM gain preferences
2026-08-12 16:11:23 +10:00
JakubandClaude Sonnet 5 7699d8ffad Merge upstream companion-v1.17.0 (CAD) into power-saving
Adopts hardware Channel Activity Detection (wired into
RadioLibWrapper::isChannelActive() alongside our RSSI-threshold check
and RX duty-cycle power-save), MCU temperature telemetry, LR2021
standby workaround, DISPLAY_SCALE/FLIP overrides, NRF52Board
shutdownPeripherals() refactor, and misc upstream fixes.

Declines upstream's ConfigSerializer-based NodePrefs rewrite,
MultiSerialInterface/interface_manager, and UIColor palette system —
each would have broken large parts of the Solo-specific feature set
(NodePrefs fields, per-variant single serial_interface, enum-based
DisplayDriver::Color). Flagged as candidate follow-up migrations, not
permanent no's.

Also fixes several pre-existing bugs surfaced while chasing silent
merge breaks (stale newMsg() override signature in ui-tiny/ui-orig,
dead UIEventType::newContactMessage case, missing ContactsIterator
init), bumps FIRMWARE_VERSION/MESHCORE_VERSION to 1.17, and fixes a
missing <cstdlib> include that broke the native ConfigSerializer unit
tests.

Verified via 13+ pio run builds across ESP32/nRF52, all 3 companion UI
variants, and 7 display drivers, plus the full native unit test suite
(33/33 passing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 17:25:35 +02:00
agessaman e2aa7b98f9 feat(companion_radio): add external FEM gain preferences for RX and TX for companions
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.
2026-08-10 11:59:26 -07:00
Scott Powell 93b2db63de * version 1.17.0 2026-08-09 15:14:46 +10:00
JakubandClaude Opus 5 05e57357d3 feat(boards): add M5Stack Cardputer ADV and LilyGO T-Echo Lite KeyShield
New M5Stack Cardputer ADV variant (ESP32-S3, ST7789 TFT, built-in TCA8418
QWERTY keyboard, PI4IOE5V6408 LoRa-cap IO-expander autodetect), and a
KeyShield accessory variant for the existing LilyGO T-Echo Lite (external
TCA8418 T9 keypad + AW21009 backlight driver). Both keyboards share one
ENV_USE_TCA8418 polling block in UITask.cpp::loop(), coexisting with the
unrelated CardKB support (different chip/address/flag).

Fixes carried in from the contributed T-Echo Lite code: swapped GPS RX/TX
pins, TX-LED hooks, TCXO voltage, missing GxEPD2_122_T61 panel include.
Fixed during integration: I2C bus was probed for an RTC before Wire.begin()
configured its pins on Cardputer ADV (silent RTC autodetect failure).

Added dedicated *_solo_dual release envs for both boards (auto-picked up by
the solo-firmware release workflow). Gave the T-Echo Lite KeyShield solo
build -Os/-Ofast-unflag like every other nRF52 solo build (was missing,
cut flash usage from 90.7% to 61.4%).

Ported the shared misc-fixed 6x9 font (full Latin/Greek/Cyrillic, opt-in via
OLED_MISC_FIXED_FONT) to ST7789Display for the Cardputer's on-screen
keyboard. ST7789Spi isn't Adafruit_GFX-based like the other single-font
drivers, and this panel's logical->physical scale is non-integer, so glyphs
are re-packed to XBM and blitted through the existing drawXbm(), which
already does correct fractional-scale boundary math, rather than
duplicating that logic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 01:06:24 +02: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
Hacuchino-hash 2af4b3c14a Fix X1 charge status LED always showing charged 2026-08-07 08:08:08 -05:00
ripplebizandGitHub 47154f02cb Merge pull request #3122 from Hacuchino-hash/x1-notifications
Add RGB LED and haptic notifications for SenseCAP MeshTracker X1
2026-08-07 15:26:33 +10:00
JakubandClaude Sonnet 5 2bde84a573 feat(heltec): solo dual-transport builds for Heltec V3/V4
Port the Wio Tracker L1 solo firmware (full on-device UI, dual BLE/USB
companion transport) to Heltec V3 and V4 OLED boards. Neither board has
a joystick or CardKB on-board, so each new env wires up both as optional
peripherals with default pins from what the board leaves free, gated
behind the existing UI_HAS_JOYSTICK/ENV_PIN_SDA+SCL flags.

DUAL_SERIAL was nRF52-only; added an ESP32 helpers/esp32/DualSerialInterface.h
counterpart so the flag isn't silently ignored on these boards. On V4's
native USB CDC, isClientConnected() also honours (bool)Serial (real DTR),
same as the nRF52 version; V3 has no native CDC so it stays BLE-only there.

Screen (SDA 17/SCL 18) and CardKB (SDA 3/SCL 4) confirmed working on real
V4 hardware.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 20:08:31 +02:00
Hacuchino-hash 335ebf546d Add SPA06 barometer telemetry and charge status LED for MeshTracker X1 2026-08-06 07:27:05 -05:00
Hacuchino-hash 3abe415cf7 Add RGB LED and haptic notifications for SenseCAP MeshTracker X1 2026-08-06 07:27:05 -05:00
liamcottle dee133d647 disable cad on companion until it's configurable 2026-08-07 00:19:18 +12:00
JakubandClaude Opus 5 b36cc7730b fix(ui): honour newlines in message text instead of overdrawing lines
A message containing a line break drew two words on top of each other in
the fullscreen reader. wrapLines() treated '\n' as an ordinary character:
it measured it via getCodepointWidth() -- which reports a full 6px cell
for it, since 0x0A sits below the font's first glyph -- and copied it into
the wrapped line. Both display drivers' print() then acts on '\n' by
resetting the cursor to x=0 and stepping down one row, so the tail of that
line was drawn straight over the following one.

wrapLines() now ends the line at '\n'/'\r' (CRLF counts as one break) and
consumes the byte rather than emitting it, preserving blank lines the
sender typed while still skipping degenerate empty wrap segments so the
loop can't stall. This covers the fullscreen view and the history list's
portrait bubbles, which share the function.

drawTextEllipsized() folds newlines into spaces for the same reason: it
draws one line clipped to max_width, and the compact one-line message
previews in the landscape list feed it raw message bodies. A space keeps
the words apart and measures the same, so the ellipsis maths is unchanged;
for names and labels it's a no-op.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 13:54:34 +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 Opus 5 e558ea0b86 fix(keyboard): joystick-free Compact mode for external keyboards
Compact mode (Settings > Keyboard's "Ext. KB" row) is meant to guarantee
operation with no joystick at all, but it was still half-tied to the
on-screen grid it hides:

- arrows now move the text cursor directly instead of a grid selection
  nobody can see, and Tab opens the placeholder picker directly instead of
  the row/col-dependent Hold-Enter dispatch
- plain Enter submits the field (there's no grid cell to have deliberately
  landed on), same as Fn+Enter
- Fn+letter's accent popup no longer gates on the grid's script/T9
  settings -- CardKB always types plain Latin regardless of them, so the
  gate only made the gesture silently stop working
- the whole status line is gone: nothing it showed (script, T9-vs-ABC,
  caps) is actionable from an external keyboard. The freed height goes to
  message-preview lines, floored at the smallest grid's footprint so
  cursor mode's own hint block still fits
- the accent popup gets a fixed slot instead of anchoring on a `row` that
  is never deliberately navigated to in this mode

Also fixes a text-corrupting invariant break: moveCursorDirect() and
openPlaceholders() move the cursor without finalizing a pending T9
multi-tap cycle, so a later tap on the same cell within the timeout
overwrote an unrelated character. Every other cursor-moving path already
cleared it.

Fn+Tab is dropped as a separate shortcut -- plain Tab already covered
every case it did. Fn+Enter no longer reads as a dead key in cursor mode.

Direct typing moves into insertTyped(), one translation point documenting
what a future relabelled-keycap layout (Cyrillic/Greek) would need.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 23:14:57 +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 Opus 5 6470afaf56 fix(ui): cap channel unread badge to what the message ring still holds
The per-channel unread counter was independent of the ring's actual
contents, so the two drifted apart:

- Opening a channel whose entries had been evicted left the badge
  claiming messages the list could no longer show. The viewing-session
  bookkeeping computes the count from an _unread_at_entry snapshot, and
  with an empty list _hist_visible is 0, so entering only knocked the
  count down by one instead of clearing it (badge "7", empty list, then
  "6").
- Eviction from a full ring decremented the counter for any dropped
  entry, including already-read ones, undercounting the newer unread
  messages the counter actually refers to.

chUnread()/getTotalChannelUnread() now clamp to the channel's ring
occupancy, so the badge can never promise more than the history holds
whatever the raw counter says, and eviction only decrements when the
entry being dropped was itself unread.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 18:05:47 +02:00
liamcottle dd778e2189 fix bluetooth toggle on ui tiny 2026-07-28 03:50:08 +12:00
liamcottle 203fd4e407 refactor rak13800 to new ethernet interface class 2026-07-28 03:18:54 +12:00
liamcottle e672679d6e refactored companion interfaces to allow for multiple active connection modes 2026-07-28 03:18:54 +12:00
liamcottle b4143a4402 rename class 2026-07-28 03:18:54 +12:00
liamcottle 5de857f8f8 initial ch390 ethernet support for thinknode m7 companion 2026-07-28 03:18:54 +12:00
ripplebizandGitHub c86e63d2f5 Merge pull request #2982 from meshcore-dev/config-serializer
Config serializer
2026-07-27 17:58:03 +10:00
JakubandClaude Opus 4.8 5844fa2390 refactor(bot,ui): dedupe bot token parsing, drop redundant turnOn, fix comment
Post-review cleanups, no behaviour change:
- botScanCommands() parsed the command name and its two args with three
  near-identical read-token loops; extracted a single readToken() lambda.
- Fn+Esc lock branch turned the display on twice (the unlock arm repeated
  what the branch head already did); dropped the redundant call.
- setGpioMode()'s comment said "Cycle" (cycling lives in GpioScreen); now
  describes what it actually does — set a specific mode + persist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 00:00:07 +02:00
JakubandClaude Sonnet 5 0a61b7ef15 fix(bot,ui): defer bot actions past throttle, fix CardKB lock bypass, add !gps fix timeout arg
- Bot Actions (!buzz/!gps/!advert/!gpio1-4) ran their side effect
  immediately during botScanCommands(), before quiet-hours/cooldown/
  per-contact throttle were checked -- those gates only suppressed the
  reply text, not the actual buzz/GPS toggle/advert/pin write. botCommandReply()
  now only records what was requested; applyPendingBotActions() runs the
  deferred effects once a wrapper's throttle checks pass and the ack sent,
  mirroring the existing _locfix_requested pattern. resetPendingBotActions()
  clears everything on every throttled/aborted path.

- CardKB's Fn+<letter> accent-popup shortcut bypassed the locked-input gate
  (it called into KeyboardWidget directly instead of through the
  enqueueKey()/dequeue path every other key uses, so it wasn't discarded
  while _locked). Now checks _locked itself.

- Since a locked device now correctly ignores CardKB entirely, Fn+Esc
  (single press) is added as CardKB's own lock/unlock gesture -- otherwise
  a CardKB-only setup had no way to unlock. Esc rather than the adjacent
  Fn+Backspace, to avoid an accidental press.

- botScanCommands() now parses up to two arguments per command instead of
  one. Used by "!gps fix [seconds]" to override the default 90s timeout
  (clamped 15-300s) for a poor sky view where 90s isn't always enough to
  reach isLocFixReady()'s HDOP/satellite bar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 23:51:31 +02:00
JakubandClaude Sonnet 5 7b6ae8e2cb feat(gps): use HDOP for !gps fix readiness, satellite count as fallback
Satellite count alone is a poor proxy for fix quality -- few satellites
in good geometry can beat many in poor geometry. LocationProvider now
exposes getHDOP() (default -1 = unsupported); MicroNMEA implements it.
isLocFixReady() prefers HDOP <= 2.0 when available, falling back to the
old >=8 satellite threshold for providers that don't report it (e.g.
RAK12500/u-blox).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 22:49:17 +02:00
JakubandClaude Sonnet 5 2c656f5af7 fix(ui): a stale login reply for a different node could be accepted
Same fix as hotfix/admin-login-timeout (96b44460). AdminScreen's only
guard was "_phase == LOGIN" (true for any node sat at the login
screen), not that the reply actually named _target. Now also checks
pub_key against _target.id.pub_key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 18:27:02 +02:00
JakubandClaude Sonnet 5 9932fb01df fix(ui): late Admin login reply could be misrouted after giving up
Same fix as hotfix/admin-login-timeout (5a5ebe9f). UITask::onRoomLoginResult()
dispatches by whichever screen is currently shown, not by who sent the
request, so a reply arriving after AdminScreen gave up (Cancel or the
timeout fix) could land on MessagesScreen instead and persist its own
unrelated _login_pw as the "confirmed" password for that pubkey.
MyMesh::cancelUiPendingLogin(pub_key) stops tracking the request on
give-up so a late reply matches nothing instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 18:21:31 +02:00
JakubandClaude Sonnet 5 0ab74bdd41 fix(ui): Admin password change didn't update the saved login copy
Same fix as hotfix/admin-login-timeout (05609019). Tools > Admin >
System > "Admin password" changes the remote's admin credential but
never updated this device's saved copy, so the next login retried the
password just replaced -- likely the actual trigger behind the
"stuck on Logging in..." report. Parses CommonCLI's "password now: <v>"
success echo and saves that as the new on-device password.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 18:13:33 +02:00
JakubandClaude Sonnet 5 220d46de8e feat(bot): !gps fix -- single-shot GPS location
Turns GPS on (if it wasn't already), waits for a stabilised fix
(isValid() + >=8 satellites, then averages 10s of readings), sends the
position, and restores GPS to whatever state it was in before -- up to
a 90s timeout, after which it reports a partial fix (if it got any
samples) or plain failure.

Replies in two parts since a fix takes seconds-to-minutes, unlike every
other bot command here: an immediate "acquiring fix..." ack (through
the existing synchronous command path), then the actual position as a
separate follow-up message once ready, delivered to whichever
destination (DM/room/channel) the request came from. Only one fix can
be in flight at a time -- a second request while one is pending gets an
immediate "already pending" instead of silently replacing it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 17:57:05 +02:00
JakubandClaude Sonnet 5 176094d32e fix(ui): Admin login could hang forever on "Logging in..."
AdminScreen's LOGIN phase had no timeout, unlike its COMMAND phase
(_cmd_deadline_ms). If a login reply never arrived -- most commonly a
saved password gone stale after the remote node's password changed,
silently dropped instead of nacked -- the screen stayed stuck with only
a manual Cancel to escape.

sendRoomLogin() now returns the same est_timeout sendAdminCommand()
already exposes; AdminScreen uses it to arm a deadline (poll(),
mirroring the COMMAND-phase pattern) that forgets the stale password
and returns to the picker on expiry, same as an explicit login
rejection already does.

Same fix as hotfix/admin-login-timeout (23f43cac), split out of this
branch's other in-progress work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 17:56:46 +02:00
JakubandClaude Sonnet 5 a0b1c80c85 feat(ui): compact on-screen keyboard toggle for external keyboards
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>
2026-07-24 15:51:51 +02:00
Scott Powell f83ad03e5e * T114 display fix 2026-07-24 18:35:56 +10:00
Scott Powell ee79df4c4f Merge branch 'dev' into config-serializer
# Conflicts:
#	test/mocks/Arduino.h
#	test/mocks/Stream.h
2026-07-24 15:06:48 +10: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
Scott Powell b8504e55c6 * refactor of Color mapping in UITasks / DisplayDrivers
* color displays now with a new light theme
2026-07-22 16:03:33 +10: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 a57d5d67bb rename(ui): Auto-Reply Bot -> Remote Bot
The bot outgrew "auto-reply" once it gained Actions (!buzz/!gps/!advert/
!gpio1-4) that control device hardware remotely, not just answer messages.
Renames the Tools screen entry and all doc cross-references; already-shipped
release notes (v1.23) are left as-is to match what actually shipped under
that name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 23:15:41 +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