2026-05-17 00:12:40 +02:00
|
|
|
|
#pragma once
|
|
|
|
|
|
// Custom screen — not part of upstream UITask.cpp
|
|
|
|
|
|
// Included by UITask.cpp after SensorPlaceholders.h is defined.
|
|
|
|
|
|
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#include "../Features.h"
|
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
|
|
|
|
#include "../RadioPresets.h"
|
2026-06-21 23:09:35 +02:00
|
|
|
|
#include "RadioParamsEditor.h"
|
2026-06-21 22:32:47 +02:00
|
|
|
|
#include "RadioPresetPicker.h"
|
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
|
|
|
|
#include "AccordionList.h"
|
2026-05-24 22:20:42 +02:00
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
class SettingsScreen : public UIScreen {
|
|
|
|
|
|
UITask* _task;
|
|
|
|
|
|
|
|
|
|
|
|
enum SettingItem {
|
|
|
|
|
|
// Display section
|
|
|
|
|
|
SECTION_DISPLAY,
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_BRIGHTNESS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
BRIGHTNESS,
|
2026-05-22 18:34:20 +02:00
|
|
|
|
#endif
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
AUTO_OFF,
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-17 09:55:51 +02:00
|
|
|
|
AUTO_LOCK,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
BATT_DISPLAY,
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_CLOCK_SECONDS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
CLOCK_SECONDS,
|
2026-05-22 10:47:19 +02:00
|
|
|
|
#endif
|
2026-05-21 21:04:43 +02:00
|
|
|
|
CLOCK_FORMAT,
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_DISPLAY_ROTATION_SETTING
|
2026-05-20 21:53:40 +02:00
|
|
|
|
ROTATION,
|
2026-05-23 15:20:40 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_JOYSTICK_ROTATION_SETTING
|
2026-05-23 15:20:40 +02:00
|
|
|
|
JOY_ROTATION,
|
2026-05-24 10:22:03 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_FULL_REFRESH_SETTING
|
2026-05-24 10:22:03 +02:00
|
|
|
|
EINK_FULL_REFRESH,
|
2026-05-20 21:53:40 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// Sound section
|
|
|
|
|
|
SECTION_SOUND,
|
|
|
|
|
|
BUZZER,
|
|
|
|
|
|
BUZZER_VOLUME,
|
|
|
|
|
|
DM_MELODY,
|
|
|
|
|
|
CH_MELODY,
|
2026-06-04 08:46:15 +02:00
|
|
|
|
AD_SOUND,
|
2026-06-07 10:53:17 +02:00
|
|
|
|
AD_SOUND_SCOPE,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// Home pages section
|
|
|
|
|
|
SECTION_HOME_PAGES,
|
2026-07-07 01:53:02 +02:00
|
|
|
|
HOME_CLOCK, HOME_FAVOURITES, HOME_RADIO, HOME_BT, HOME_ADVERT,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
HOME_GPS,
|
|
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
|
|
|
|
|
HOME_SENSORS,
|
|
|
|
|
|
#endif
|
2026-05-22 23:33:06 +02:00
|
|
|
|
HOME_SETTINGS, HOME_QUICK_MSG,
|
2026-07-02 10:34:38 +02:00
|
|
|
|
HOME_TOOLS, HOME_SHUTDOWN, HOME_MAP,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// Radio section
|
|
|
|
|
|
SECTION_RADIO,
|
|
|
|
|
|
TX_POWER,
|
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
|
|
|
|
RADIO_PRESET,
|
|
|
|
|
|
CUSTOM_FREQ, CUSTOM_SF, CUSTOM_BW, CUSTOM_CR,
|
2026-06-10 23:30:43 +02:00
|
|
|
|
POWER_SAVE,
|
|
|
|
|
|
TX_APC,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// System section
|
|
|
|
|
|
SECTION_SYSTEM,
|
2026-07-13 09:10:24 +02:00
|
|
|
|
DEVICE_NAME,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
TIMEZONE,
|
|
|
|
|
|
LOW_BAT,
|
2026-06-04 00:44:56 +02:00
|
|
|
|
UNITS,
|
2026-07-13 09:10:24 +02:00
|
|
|
|
REBOOT,
|
2026-07-02 12:21:52 +02:00
|
|
|
|
// Keyboard section
|
|
|
|
|
|
SECTION_KEYBOARD,
|
|
|
|
|
|
KEYBOARD_TYPE,
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
KEYBOARD_ALPHABET,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// Contacts section
|
2026-05-27 17:33:52 +02:00
|
|
|
|
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// Messages section
|
|
|
|
|
|
SECTION_MESSAGES,
|
2026-06-14 23:33:16 +02:00
|
|
|
|
DM_RESEND,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
MSG_SLOT_0, MSG_SLOT_1, MSG_SLOT_2, MSG_SLOT_3, MSG_SLOT_4,
|
|
|
|
|
|
MSG_SLOT_5, MSG_SLOT_6, MSG_SLOT_7, MSG_SLOT_8, MSG_SLOT_9,
|
|
|
|
|
|
Count
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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
|
|
|
|
// Cursor + scroll, fold state and the flattened visible list are owned by the
|
|
|
|
|
|
// shared AccordionList helper. We keep only the section→SettingItem mapping it
|
|
|
|
|
|
// needs (sections are walked once from the enum, honouring the #if guards).
|
|
|
|
|
|
int _selected = 0; // SettingItem under the cursor, resolved per input/render
|
|
|
|
|
|
int _reserve = 0; // right-edge px reserved for the scrollbar (0 when list fits)
|
|
|
|
|
|
bool _dirty = false;
|
|
|
|
|
|
|
|
|
|
|
|
AccordionList _acc;
|
2026-07-02 12:21:52 +02:00
|
|
|
|
static const int NUM_SECTIONS = 8;
|
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
|
|
|
|
static const int MAX_PER_SEC = 16;
|
|
|
|
|
|
uint8_t _sec_items[NUM_SECTIONS][MAX_PER_SEC]; // SettingItem per (section, row)
|
|
|
|
|
|
uint8_t _sec_count[NUM_SECTIONS];
|
|
|
|
|
|
uint8_t _sec_header[NUM_SECTIONS]; // the SECTION_* enum for each section
|
|
|
|
|
|
int _num_sections = 0;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
static const uint16_t AUTO_OFF_OPTS[5];
|
|
|
|
|
|
static const char* AUTO_OFF_LABELS[5];
|
|
|
|
|
|
static const int AUTO_OFF_COUNT = 5;
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-25 10:48:03 +02:00
|
|
|
|
// GPS update interval tables are no longer surfaced in Settings — the sensor
|
|
|
|
|
|
// manager defaults to 1 s when nothing else sets it. Pref byte _prefs.gps_interval
|
|
|
|
|
|
// is retained for backwards compatibility.
|
2026-05-17 00:12:40 +02:00
|
|
|
|
static const uint16_t LOW_BAT_OPTS[7];
|
|
|
|
|
|
static const char* LOW_BAT_LABELS[7];
|
|
|
|
|
|
static const int LOW_BAT_COUNT = 7;
|
|
|
|
|
|
static const char* BATT_DISPLAY_LABELS[3];
|
|
|
|
|
|
static const int BATT_DISPLAY_COUNT = 3;
|
2026-06-07 09:13:02 +02:00
|
|
|
|
static const char* SOUND_LABELS[4];
|
|
|
|
|
|
static const int SOUND_COUNT = 4;
|
2026-06-07 10:53:17 +02:00
|
|
|
|
static const char* AD_SCOPE_LABELS[2];
|
|
|
|
|
|
static const int AD_SCOPE_COUNT = 2;
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_FULL_REFRESH_SETTING
|
2026-05-24 10:22:03 +02:00
|
|
|
|
static const char* EINK_FULL_REFRESH_LABELS[5];
|
|
|
|
|
|
static const int EINK_FULL_REFRESH_COUNT = 5;
|
|
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
|
|
int lowBatIndex() {
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
if (!p) return 0;
|
|
|
|
|
|
for (int i = 0; i < LOW_BAT_COUNT; i++)
|
|
|
|
|
|
if (LOW_BAT_OPTS[i] == p->low_batt_mv) return i;
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 22:32:47 +02:00
|
|
|
|
// The companion's own radio fields, as the shared preset picker's target
|
|
|
|
|
|
// (Tools › Repeater points the same picker at the dedicated repeater profile).
|
|
|
|
|
|
RadioPresetPicker::Target radioTarget(NodePrefs* p) const {
|
|
|
|
|
|
return { &p->freq, &p->bw, &p->sf, &p->cr };
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 16:45:28 +02:00
|
|
|
|
// Value column start, pulled left by the scrollbar gutter so right-side
|
|
|
|
|
|
// values never render under the indicator when the list scrolls.
|
|
|
|
|
|
int valCol(DisplayDriver& display) const { return display.valCol() - _reserve; }
|
|
|
|
|
|
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
|
// Shared 0/90/180/270 labels for display + joystick rotation.
|
|
|
|
|
|
static const char* rotLabel(uint8_t r) {
|
|
|
|
|
|
static const char* const L[] = { "0 deg", "90 deg", "180 deg", "270 deg" };
|
|
|
|
|
|
return L[r & 3];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
void renderBar(DisplayDriver& display, int x, int y, int value, int max_val) {
|
2026-05-22 19:11:25 +02:00
|
|
|
|
const int gap = 2;
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
|
const int avail = display.width() - x - _reserve;
|
2026-05-22 19:11:25 +02:00
|
|
|
|
const int raw = (avail - (max_val - 1) * gap) / max_val;
|
|
|
|
|
|
const int cap = display.getLineHeight() - 2;
|
|
|
|
|
|
const int box_h = raw < cap ? (raw < 2 ? 2 : raw) : cap;
|
|
|
|
|
|
const int box_w = box_h;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
for (int i = 0; i < max_val; i++) {
|
|
|
|
|
|
int bx = x + i * (box_w + gap);
|
|
|
|
|
|
display.drawRect(bx, y, box_w, box_h);
|
|
|
|
|
|
if (i < value)
|
|
|
|
|
|
display.fillRect(bx + 1, y + 1, box_w - 2, box_h - 2);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
int autoOffIndex() {
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
if (!p) return 1;
|
|
|
|
|
|
for (int i = 0; i < AUTO_OFF_COUNT; i++)
|
|
|
|
|
|
if (AUTO_OFF_OPTS[i] == p->auto_off_secs) return i;
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool isSection(int item) const {
|
|
|
|
|
|
return item == SECTION_DISPLAY || item == SECTION_SOUND ||
|
|
|
|
|
|
item == SECTION_HOME_PAGES ||
|
|
|
|
|
|
item == SECTION_RADIO || item == SECTION_SYSTEM ||
|
2026-07-02 12:21:52 +02:00
|
|
|
|
item == SECTION_KEYBOARD ||
|
2026-05-25 13:01:06 +02:00
|
|
|
|
item == SECTION_CONTACTS || item == SECTION_MESSAGES;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const char* sectionName(int item) const {
|
|
|
|
|
|
if (item == SECTION_DISPLAY) return "Display";
|
|
|
|
|
|
if (item == SECTION_SOUND) return "Sound";
|
|
|
|
|
|
if (item == SECTION_HOME_PAGES) return "Home Pages";
|
|
|
|
|
|
if (item == SECTION_RADIO) return "Radio";
|
|
|
|
|
|
if (item == SECTION_SYSTEM) return "System";
|
2026-07-02 12:21:52 +02:00
|
|
|
|
if (item == SECTION_KEYBOARD) return "Keyboard";
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (item == SECTION_CONTACTS) return "Contacts";
|
|
|
|
|
|
if (item == SECTION_MESSAGES) return "Messages";
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// Walk the SettingItem enum once, bucketing items under their section header.
|
|
|
|
|
|
// #if-guarded items need no special handling — they simply aren't in the enum.
|
|
|
|
|
|
void buildSections() {
|
|
|
|
|
|
int cur = -1;
|
2026-05-26 15:49:50 +02:00
|
|
|
|
for (int i = 0; i < (int)Count; i++) {
|
|
|
|
|
|
if (isSection(i)) {
|
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
|
|
|
|
if (++cur >= NUM_SECTIONS) break;
|
|
|
|
|
|
_sec_header[cur] = (uint8_t)i;
|
|
|
|
|
|
_sec_count[cur] = 0;
|
|
|
|
|
|
} else if (cur >= 0 && _sec_count[cur] < MAX_PER_SEC) {
|
|
|
|
|
|
_sec_items[cur][_sec_count[cur]++] = (uint8_t)i;
|
2026-05-26 15:49:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
_num_sections = cur + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// (Re)load the section sizes into the accordion (folds all, resets the cursor).
|
|
|
|
|
|
void resetList() {
|
|
|
|
|
|
uint8_t sizes[NUM_SECTIONS];
|
|
|
|
|
|
for (int i = 0; i < _num_sections; i++) sizes[i] = _sec_count[i];
|
|
|
|
|
|
_acc.begin(sizes, _num_sections);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Resolve the accordion's selected row to a SettingItem (header → SECTION_*).
|
|
|
|
|
|
int currentItem() const {
|
|
|
|
|
|
const AccordionList::Row& r = _acc.selected();
|
|
|
|
|
|
return (r.item < 0) ? _sec_header[r.sec] : _sec_items[r.sec][r.item];
|
2026-05-26 15:49:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
bool isHomePage(int item) const {
|
2026-07-07 01:53:02 +02:00
|
|
|
|
return item == HOME_CLOCK || item == HOME_RADIO || item == HOME_BT ||
|
|
|
|
|
|
item == HOME_ADVERT || item == HOME_TOOLS ||
|
2026-05-24 23:00:27 +02:00
|
|
|
|
item == HOME_SHUTDOWN || item == HOME_SETTINGS || item == HOME_QUICK_MSG ||
|
2026-07-02 10:34:38 +02:00
|
|
|
|
item == HOME_FAVOURITES || item == HOME_MAP
|
2026-05-17 00:12:40 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
|| item == HOME_GPS
|
|
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
|
|
|
|
|
|| item == HOME_SENSORS
|
|
|
|
|
|
#endif
|
|
|
|
|
|
;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uint16_t homePageBit(int item) const {
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
int bit = homePageBitIndex(item);
|
2026-05-24 23:00:27 +02:00
|
|
|
|
// SETTINGS and QUICK_MSG are always visible (no mask bit). All other pages
|
|
|
|
|
|
// — including FAVOURITES — toggle via home_pages_mask.
|
|
|
|
|
|
if (bit < 0 || bit == NodePrefs::HPB_SETTINGS || bit == NodePrefs::HPB_QUICK_MSG) return 0;
|
|
|
|
|
|
return (uint16_t)(1 << bit);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const char* homePageLabel(int item) const {
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
int bit = homePageBitIndex(item);
|
2026-05-24 22:26:56 +02:00
|
|
|
|
return NodePrefs::homePageLabel((uint8_t)(bit >= 0 ? bit : NodePrefs::HPB_COUNT));
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool homePageVisible(int item, const NodePrefs* p) const {
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if (item == HOME_SETTINGS || item == HOME_QUICK_MSG) return true;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
uint16_t bit = homePageBit(item);
|
|
|
|
|
|
if (!bit) return false;
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
uint16_t mask = (p && p->home_pages_mask) ? p->home_pages_mask : NodePrefs::HP_ALL;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
return (mask & bit) != 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:33:06 +02:00
|
|
|
|
bool homePageToggleable(int item) const {
|
|
|
|
|
|
return item != HOME_SETTINGS && item != HOME_QUICK_MSG;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 22:26:56 +02:00
|
|
|
|
// Returns the bit-index used in page_order for this SettingItem, or -1.
|
|
|
|
|
|
// Bit-index values are defined once in NodePrefs::HomePageBit.
|
2026-05-22 23:33:06 +02:00
|
|
|
|
int homePageBitIndex(int item) const {
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_CLOCK) return NodePrefs::HPB_CLOCK;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
if (item == HOME_FAVOURITES) return NodePrefs::HPB_FAVOURITES;
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_RADIO) return NodePrefs::HPB_RADIO;
|
|
|
|
|
|
if (item == HOME_BT) return NodePrefs::HPB_BLUETOOTH;
|
|
|
|
|
|
if (item == HOME_ADVERT) return NodePrefs::HPB_ADVERT;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_GPS) return NodePrefs::HPB_GPS;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_SENSORS) return NodePrefs::HPB_SENSORS;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#endif
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_TOOLS) return NodePrefs::HPB_TOOLS;
|
|
|
|
|
|
if (item == HOME_SHUTDOWN) return NodePrefs::HPB_SHUTDOWN;
|
2026-07-02 10:34:38 +02:00
|
|
|
|
if (item == HOME_MAP) return NodePrefs::HPB_MAP;
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_SETTINGS) return NodePrefs::HPB_SETTINGS;
|
|
|
|
|
|
if (item == HOME_QUICK_MSG) return NodePrefs::HPB_QUICK_MSG;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Returns 1-based position of item in page_order, or 0 if no custom order / not found.
|
|
|
|
|
|
int homePagePosition(int item, const NodePrefs* p) const {
|
2026-05-24 19:53:05 +02:00
|
|
|
|
if (!p || p->page_order_set != NodePrefs::PAGE_ORDER_MAGIC) return 0;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
int bit = homePageBitIndex(item);
|
|
|
|
|
|
if (bit < 0) return 0;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
for (int i = 0; i < NodePrefs::PAGE_ORDER_LEN; i++) {
|
2026-05-22 23:33:06 +02:00
|
|
|
|
uint8_t v = p->page_order[i];
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (v < 1 || v > NodePrefs::HPB_COUNT) break;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if ((int)(v - 1) == bit) return i + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Initialises page_order to the default display sequence if not already set.
|
2026-05-26 16:08:01 +02:00
|
|
|
|
// Also repairs a partially-initialised order where CLOCK is absent; migrates
|
|
|
|
|
|
// older orders by inserting FAVOURITES after CLOCK; and appends any pages that
|
|
|
|
|
|
// are absent from a stale saved order (e.g. TOOLS/MESSAGES added by later firmware).
|
2026-05-22 23:33:06 +02:00
|
|
|
|
void ensurePageOrderInit(NodePrefs* p) const {
|
|
|
|
|
|
if (!p) return;
|
2026-05-24 19:53:05 +02:00
|
|
|
|
if (p->page_order_set == NodePrefs::PAGE_ORDER_MAGIC) {
|
2026-05-24 23:17:48 +02:00
|
|
|
|
bool has_clock = false;
|
|
|
|
|
|
bool has_fav = false;
|
|
|
|
|
|
int len = 0;
|
|
|
|
|
|
int clock_at = -1;
|
|
|
|
|
|
for (int i = 0; i < NodePrefs::PAGE_ORDER_LEN; i++) {
|
2026-05-23 23:50:25 +02:00
|
|
|
|
uint8_t v = p->page_order[i];
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (v < 1 || v > NodePrefs::HPB_COUNT) break;
|
2026-05-24 23:17:48 +02:00
|
|
|
|
if ((int)(v - 1) == NodePrefs::HPB_CLOCK) { has_clock = true; clock_at = i; }
|
|
|
|
|
|
if ((int)(v - 1) == NodePrefs::HPB_FAVOURITES) { has_fav = true; }
|
|
|
|
|
|
len = i + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!has_clock) {
|
|
|
|
|
|
// Corrupted/partial — full re-init below.
|
|
|
|
|
|
memset(p->page_order, 0, sizeof(p->page_order));
|
|
|
|
|
|
} else {
|
2026-05-24 23:24:13 +02:00
|
|
|
|
if (!has_fav) {
|
2026-07-02 12:01:40 +02:00
|
|
|
|
// Insert FAVOURITES right after CLOCK. Real orders are shorter than
|
|
|
|
|
|
// PAGE_ORDER_LEN so there's room; only a pathologically full order would
|
|
|
|
|
|
// drop its last entry, which buildVisibleOrder's fallback re-appends.
|
2026-05-24 23:17:48 +02:00
|
|
|
|
int insert_at = clock_at + 1;
|
2026-05-24 23:24:13 +02:00
|
|
|
|
int tail = (len < NodePrefs::PAGE_ORDER_LEN) ? len : NodePrefs::PAGE_ORDER_LEN - 1;
|
|
|
|
|
|
for (int i = tail; i > insert_at; i--) p->page_order[i] = p->page_order[i - 1];
|
2026-05-24 23:17:48 +02:00
|
|
|
|
p->page_order[insert_at] = NodePrefs::HPB_FAVOURITES + 1;
|
|
|
|
|
|
}
|
2026-05-26 16:08:01 +02:00
|
|
|
|
// Append any pages that are absent from the saved order (e.g. added by a
|
|
|
|
|
|
// later firmware version). Recount first since the block above may have
|
|
|
|
|
|
// just inserted FAVOURITES.
|
|
|
|
|
|
{
|
|
|
|
|
|
uint16_t present = 0;
|
|
|
|
|
|
int cur_len = 0;
|
|
|
|
|
|
for (int i = 0; i < NodePrefs::PAGE_ORDER_LEN; i++) {
|
|
|
|
|
|
uint8_t v = p->page_order[i];
|
|
|
|
|
|
if (v < 1 || v > NodePrefs::HPB_COUNT) break;
|
|
|
|
|
|
present |= (uint16_t)(1u << (v - 1));
|
|
|
|
|
|
cur_len++;
|
|
|
|
|
|
}
|
2026-07-02 12:01:40 +02:00
|
|
|
|
// Every page has a slot now (PAGE_ORDER_LEN == HPB_COUNT), so all pages
|
|
|
|
|
|
// are required — any missing from a stale saved order (SHUTDOWN and MAP
|
|
|
|
|
|
// for pre-0x0019 upgraders) is appended into the free tail slots below.
|
2026-05-26 16:08:01 +02:00
|
|
|
|
static const uint8_t REQUIRED[] = {
|
|
|
|
|
|
NodePrefs::HPB_CLOCK, NodePrefs::HPB_FAVOURITES,
|
|
|
|
|
|
NodePrefs::HPB_RECENT, NodePrefs::HPB_RADIO,
|
|
|
|
|
|
NodePrefs::HPB_BLUETOOTH, NodePrefs::HPB_ADVERT,
|
|
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
NodePrefs::HPB_GPS,
|
|
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
|
|
|
|
|
NodePrefs::HPB_SENSORS,
|
|
|
|
|
|
#endif
|
2026-07-02 12:01:40 +02:00
|
|
|
|
NodePrefs::HPB_SETTINGS, NodePrefs::HPB_MAP, NodePrefs::HPB_TOOLS,
|
|
|
|
|
|
NodePrefs::HPB_QUICK_MSG, NodePrefs::HPB_SHUTDOWN,
|
2026-05-26 16:08:01 +02:00
|
|
|
|
};
|
2026-05-26 16:13:07 +02:00
|
|
|
|
for (int ri = 0; ri < (int)(sizeof(REQUIRED)/sizeof(REQUIRED[0])); ri++) {
|
|
|
|
|
|
uint8_t bit = REQUIRED[ri];
|
2026-05-26 16:08:01 +02:00
|
|
|
|
if (!(present & (uint16_t)(1u << bit)) && cur_len < NodePrefs::PAGE_ORDER_LEN)
|
|
|
|
|
|
p->page_order[cur_len++] = bit + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-24 23:17:48 +02:00
|
|
|
|
return;
|
2026-05-23 23:50:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-02 12:01:40 +02:00
|
|
|
|
// Default: CLOCK FAVOURITES RECENT RADIO BT ADVERT [GPS] [SENSORS] SETTINGS
|
|
|
|
|
|
// MAP TOOLS MESSAGES SHUTDOWN — mirrors the home-carousel enum order. Every
|
|
|
|
|
|
// page has an explicit slot now (PAGE_ORDER_LEN == HPB_COUNT).
|
2026-05-22 23:33:06 +02:00
|
|
|
|
int j = 0;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_CLOCK + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_FAVOURITES + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_RECENT + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_RADIO + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_BLUETOOTH + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_ADVERT + 1;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
2026-05-24 23:00:27 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_GPS + 1;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
2026-05-24 23:00:27 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_SENSORS + 1;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#endif
|
2026-05-24 23:00:27 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_SETTINGS + 1;
|
2026-07-02 12:01:40 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_MAP + 1;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_TOOLS + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_QUICK_MSG + 1;
|
2026-07-02 12:01:40 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_SHUTDOWN + 1;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
while (j < NodePrefs::PAGE_ORDER_LEN) p->page_order[j++] = 0;
|
2026-05-24 19:53:05 +02:00
|
|
|
|
p->page_order_set = NodePrefs::PAGE_ORDER_MAGIC;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Swaps item's page_order slot with its neighbour in the given direction (-1=earlier, +1=later).
|
|
|
|
|
|
void movePageInOrder(int item, int delta, NodePrefs* p) {
|
|
|
|
|
|
ensurePageOrderInit(p);
|
|
|
|
|
|
int bit = homePageBitIndex(item);
|
|
|
|
|
|
if (bit < 0) return;
|
|
|
|
|
|
int cur = -1, total = 0;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
for (int i = 0; i < NodePrefs::PAGE_ORDER_LEN; i++) {
|
2026-05-22 23:33:06 +02:00
|
|
|
|
uint8_t v = p->page_order[i];
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (v < 1 || v > NodePrefs::HPB_COUNT) break;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if ((int)(v - 1) == bit) cur = i;
|
|
|
|
|
|
total++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cur < 0) return;
|
|
|
|
|
|
int next = cur + delta;
|
|
|
|
|
|
if (next < 0 || next >= total) return;
|
|
|
|
|
|
uint8_t tmp = p->page_order[cur];
|
|
|
|
|
|
p->page_order[cur] = p->page_order[next];
|
|
|
|
|
|
p->page_order[next] = tmp;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
bool isMsgSlot(int item) const {
|
|
|
|
|
|
return item >= MSG_SLOT_0 && item <= MSG_SLOT_9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int msgSlotIndex(int item) const {
|
|
|
|
|
|
return item - MSG_SLOT_0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
void renderItem(DisplayDriver& display, int item, int y, bool sel) {
|
2026-05-17 00:12:40 +02:00
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
|
2026-06-29 18:53:18 +02:00
|
|
|
|
drawRowSelection(display, y, sel, _reserve);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
|
display.setCursor(2, y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_BRIGHTNESS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (item == BRIGHTNESS) {
|
|
|
|
|
|
display.print("Bright");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
renderBar(display, valCol(display), y, (p ? p->display_brightness : 2) + 1, 5);
|
2026-05-22 18:34:20 +02:00
|
|
|
|
} else
|
|
|
|
|
|
#endif
|
|
|
|
|
|
if (item == BUZZER) {
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print("Buzzer");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
{ static const char* labels[] = { "ON", "OFF", "Auto" };
|
|
|
|
|
|
int m = _task->getBuzzerMode();
|
|
|
|
|
|
display.print(labels[m < 3 ? m : 0]); }
|
|
|
|
|
|
#else
|
|
|
|
|
|
display.print("N/A");
|
|
|
|
|
|
#endif
|
|
|
|
|
|
} else if (item == BUZZER_VOLUME) {
|
|
|
|
|
|
display.print("BzrVol");
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
2026-06-15 16:45:28 +02:00
|
|
|
|
renderBar(display, valCol(display), y, _task->getBuzzerVolume() + 1, 5);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
#else
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print("N/A");
|
|
|
|
|
|
#endif
|
|
|
|
|
|
} else if (item == DM_MELODY) {
|
|
|
|
|
|
display.print("DM sound");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-07 09:13:02 +02:00
|
|
|
|
{ uint8_t v = p ? p->notif_melody_dm : 0;
|
|
|
|
|
|
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == CH_MELODY) {
|
|
|
|
|
|
display.print("Ch sound");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-07 09:13:02 +02:00
|
|
|
|
{ uint8_t v = p ? p->notif_melody_ch : 0;
|
|
|
|
|
|
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
|
2026-06-04 08:46:15 +02:00
|
|
|
|
} else if (item == AD_SOUND) {
|
|
|
|
|
|
display.print("AD sound");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-07 09:13:02 +02:00
|
|
|
|
{ uint8_t v = p ? p->notif_melody_ad : 0;
|
|
|
|
|
|
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
|
2026-06-07 10:53:17 +02:00
|
|
|
|
} else if (item == AD_SOUND_SCOPE) {
|
|
|
|
|
|
display.print("AD scope");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-07 10:53:17 +02:00
|
|
|
|
{ uint8_t v = p ? p->advert_sound_scope : ADVERT_SOUND_SCOPE_ALL;
|
|
|
|
|
|
display.print(AD_SCOPE_LABELS[v < AD_SCOPE_COUNT ? v : 0]); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (isHomePage(item)) {
|
2026-05-23 23:50:25 +02:00
|
|
|
|
if (p) ensurePageOrderInit(p);
|
2026-05-22 23:33:06 +02:00
|
|
|
|
int pos = homePagePosition(item, p);
|
|
|
|
|
|
if (pos > 0) {
|
2026-05-23 09:46:28 +02:00
|
|
|
|
char pb[5]; snprintf(pb, sizeof(pb), "%2d ", pos);
|
2026-05-22 23:33:06 +02:00
|
|
|
|
display.print(pb);
|
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print(homePageLabel(item));
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
|
display.setCursor(display.width() - 6 * display.getCharWidth() - _reserve, y);
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if (!homePageToggleable(item))
|
|
|
|
|
|
display.print("always");
|
|
|
|
|
|
else
|
|
|
|
|
|
display.print(homePageVisible(item, p) ? "ON" : "OFF");
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == TX_POWER) {
|
|
|
|
|
|
display.print("TX Pwr");
|
|
|
|
|
|
char buf[8];
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(buf, sizeof(buf),"%ddBm", p ? p->tx_power_dbm : 0);
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print(buf);
|
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
|
|
|
|
} else if (item == RADIO_PRESET) {
|
|
|
|
|
|
display.print("Preset");
|
2026-06-21 22:32:47 +02:00
|
|
|
|
const char* name = p ? _picker.currentName(p, radioTarget(p)) : "Custom";
|
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
|
|
|
|
int xc = valCol(display);
|
|
|
|
|
|
display.drawTextEllipsized(xc, y, display.width() - xc - _reserve, name);
|
|
|
|
|
|
} else if (item == CUSTOM_FREQ) {
|
|
|
|
|
|
display.print("Freq");
|
|
|
|
|
|
int xc = valCol(display);
|
2026-06-21 23:09:35 +02:00
|
|
|
|
if (sel && _editor.active()) {
|
|
|
|
|
|
_editor.render(display, xc, y);
|
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
|
|
|
|
} else {
|
|
|
|
|
|
char buf[10];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%.3f", p ? p->freq : 0.0f);
|
|
|
|
|
|
display.setCursor(xc, y);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (item == CUSTOM_SF) {
|
|
|
|
|
|
display.print("SF");
|
|
|
|
|
|
char buf[6];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%d", p ? (int)p->sf : 0);
|
|
|
|
|
|
display.setCursor(valCol(display), y);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
} else if (item == CUSTOM_BW) {
|
|
|
|
|
|
display.print("BW");
|
|
|
|
|
|
char buf[10];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%.1f", p ? p->bw : 0.0f);
|
|
|
|
|
|
display.setCursor(valCol(display), y);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
} else if (item == CUSTOM_CR) {
|
|
|
|
|
|
display.print("CR");
|
|
|
|
|
|
char buf[6];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%d", p ? (int)p->cr : 0);
|
|
|
|
|
|
display.setCursor(valCol(display), y);
|
|
|
|
|
|
display.print(buf);
|
2026-06-10 23:30:43 +02:00
|
|
|
|
} else if (item == POWER_SAVE) {
|
|
|
|
|
|
display.print("Pwr save");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
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
|
|
|
|
// Forced off (and locked) while the repeater is on — it must hear all traffic.
|
|
|
|
|
|
if (p && p->client_repeat) display.print("--");
|
|
|
|
|
|
else display.print((p && p->rx_powersave) ? "ON" : "OFF");
|
2026-06-10 23:30:43 +02:00
|
|
|
|
} else if (item == TX_APC) {
|
|
|
|
|
|
display.print("Auto pwr");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
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
|
|
|
|
// Suppressed (and locked) while repeating — a repeater holds full TX power.
|
|
|
|
|
|
if (p && p->client_repeat) display.print("--");
|
|
|
|
|
|
else display.print((p && p->tx_apc) ? "ON" : "OFF");
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == AUTO_OFF) {
|
|
|
|
|
|
display.print("AutoOff");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print(AUTO_OFF_LABELS[autoOffIndex()]);
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-17 09:55:51 +02:00
|
|
|
|
} else if (item == AUTO_LOCK) {
|
|
|
|
|
|
display.print("AutoLock");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 09:55:51 +02:00
|
|
|
|
display.print((p && p->auto_lock) ? "ON" : "OFF");
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == TIMEZONE) {
|
|
|
|
|
|
display.print("TimeZone");
|
|
|
|
|
|
char buf[8];
|
|
|
|
|
|
int8_t tz = p ? p->tz_offset_hours : 0;
|
2026-05-24 20:35:49 +02:00
|
|
|
|
if (tz >= 0) snprintf(buf, sizeof(buf),"UTC+%d", (int)tz);
|
|
|
|
|
|
else snprintf(buf, sizeof(buf),"UTC%d", (int)tz);
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print(buf);
|
|
|
|
|
|
} else if (item == LOW_BAT) {
|
|
|
|
|
|
display.print("LowBat");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print(LOW_BAT_LABELS[lowBatIndex()]);
|
2026-06-04 00:44:56 +02:00
|
|
|
|
} else if (item == UNITS) {
|
|
|
|
|
|
display.print("Units");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-04 00:44:56 +02:00
|
|
|
|
display.print((p && p->units_imperial) ? "Imperial" : "Metric");
|
2026-07-13 09:10:24 +02:00
|
|
|
|
} else if (item == DEVICE_NAME) {
|
|
|
|
|
|
display.print("Name");
|
|
|
|
|
|
int vx = valCol(display);
|
|
|
|
|
|
display.drawTextEllipsized(vx, y, display.width() - vx - _reserve, the_mesh.getNodeName());
|
|
|
|
|
|
} else if (item == REBOOT) {
|
|
|
|
|
|
display.print("Reboot"); // action row: Enter reboots this device
|
2026-07-02 12:21:52 +02:00
|
|
|
|
} else if (item == KEYBOARD_TYPE) {
|
|
|
|
|
|
display.print("Type");
|
|
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-07-02 18:33:40 +02:00
|
|
|
|
display.print((p && p->keyboard_type) ? "T9" : "ABC");
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
} else if (item == KEYBOARD_ALPHABET) {
|
|
|
|
|
|
display.print("Alphabet");
|
|
|
|
|
|
display.setCursor(valCol(display), y);
|
|
|
|
|
|
display.print(NodePrefs::keyboardAlphabetLabel(p ? p->keyboard_alt_alphabet : 0));
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == BATT_DISPLAY) {
|
|
|
|
|
|
display.print("BattDisp");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
uint8_t mode = p ? p->batt_display_mode : 0;
|
|
|
|
|
|
display.print(BATT_DISPLAY_LABELS[mode < BATT_DISPLAY_COUNT ? mode : 0]);
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_CLOCK_SECONDS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == CLOCK_SECONDS) {
|
|
|
|
|
|
display.print("Seconds");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print((p && p->clock_hide_seconds) ? "OFF" : "ON");
|
2026-05-22 10:47:19 +02:00
|
|
|
|
#endif
|
2026-05-21 21:04:43 +02:00
|
|
|
|
} else if (item == CLOCK_FORMAT) {
|
|
|
|
|
|
display.print("Format");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-21 21:04:43 +02:00
|
|
|
|
display.print((p && p->clock_12h) ? "12h" : "24h");
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_DISPLAY_ROTATION_SETTING
|
2026-05-20 21:53:40 +02:00
|
|
|
|
} else if (item == ROTATION) {
|
|
|
|
|
|
display.print("Rotation");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
|
display.print(rotLabel(p ? p->display_rotation : 0));
|
2026-05-23 15:20:40 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_JOYSTICK_ROTATION_SETTING
|
2026-05-23 15:20:40 +02:00
|
|
|
|
} else if (item == JOY_ROTATION) {
|
|
|
|
|
|
display.print("Joystick");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
|
display.print(rotLabel(p ? p->joystick_rotation : 0));
|
2026-05-24 10:22:03 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_FULL_REFRESH_SETTING
|
2026-05-24 10:22:03 +02:00
|
|
|
|
} else if (item == EINK_FULL_REFRESH) {
|
|
|
|
|
|
display.print("Full rfsh");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-24 10:22:03 +02:00
|
|
|
|
{ uint8_t idx = p ? p->eink_full_refresh_every : 0;
|
|
|
|
|
|
if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0;
|
|
|
|
|
|
display.print(EINK_FULL_REFRESH_LABELS[idx]); }
|
2026-05-20 21:53:40 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == DM_FILTER) {
|
|
|
|
|
|
display.print("DM");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print((p && p->dm_show_all) ? "all" : "fav");
|
2026-05-27 17:33:52 +02:00
|
|
|
|
} else if (item == CH_FILTER) {
|
|
|
|
|
|
display.print("Channels");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-27 17:33:52 +02:00
|
|
|
|
display.print((p && p->ch_fav_only) ? "fav" : "all");
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == ROOM_FILTER) {
|
|
|
|
|
|
display.print("Rooms");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print((p && p->room_fav_only) ? "fav" : "all");
|
2026-06-14 23:33:16 +02:00
|
|
|
|
} else if (item == DM_RESEND) {
|
|
|
|
|
|
display.print("Resend");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-14 23:33:16 +02:00
|
|
|
|
uint8_t n = p ? p->dm_resend_count : 0;
|
|
|
|
|
|
if (n == 0) display.print("OFF");
|
|
|
|
|
|
else { char buf[6]; snprintf(buf, sizeof(buf), "%ux", (unsigned)n); display.print(buf); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (isMsgSlot(item)) {
|
|
|
|
|
|
int slot = msgSlotIndex(item);
|
|
|
|
|
|
char label[5];
|
|
|
|
|
|
snprintf(label, sizeof(label), "Q%d:", slot + 1);
|
|
|
|
|
|
display.print(label);
|
|
|
|
|
|
const char* tmpl = (p && p->custom_msgs[slot][0]) ? p->custom_msgs[slot] : "(empty)";
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
int xm = 8 + display.getCharWidth() * 4;
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
|
display.drawTextEllipsized(xm, y, display.width() - xm - _reserve, tmpl);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Keyboard state for editing message slots
|
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
|
|
|
|
int _edit_slot = -1; // -1 = not editing, 0..9 = slot being edited
|
2026-07-13 09:10:24 +02:00
|
|
|
|
bool _edit_name = false; // editing DEVICE_NAME via the keyboard
|
2026-06-08 20:42:18 +02:00
|
|
|
|
KeyboardWidget* _kb;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
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
|
|
|
|
// Radio preset picker — names are too long for the value column, so Enter on
|
2026-06-21 22:32:47 +02:00
|
|
|
|
// RADIO_PRESET opens it as a full-width scrollable list instead of cycling.
|
|
|
|
|
|
// Shared with Tools › Repeater (see RadioPresetPicker.h). _picker.saving means
|
|
|
|
|
|
// _kb is open to name a new preset, not a message slot.
|
|
|
|
|
|
RadioPresetPicker _picker;
|
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
|
|
|
|
|
2026-06-21 23:09:35 +02:00
|
|
|
|
// Manual radio-parameter editing (digit-by-digit Freq editor + SF/BW/CR
|
|
|
|
|
|
// stepping), shared with Tools › Repeater — see RadioParamsEditor.h.
|
|
|
|
|
|
RadioParamsEditor _editor;
|
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
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
public:
|
2026-06-08 20:42:18 +02:00
|
|
|
|
SettingsScreen(UITask* task, KeyboardWidget* kb)
|
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
|
|
|
|
: _task(task), _kb(kb) {
|
|
|
|
|
|
buildSections();
|
|
|
|
|
|
resetList();
|
2026-05-26 15:49:50 +02:00
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-29 18:37:04 +02:00
|
|
|
|
void onShow() override {
|
2026-05-26 15:49:50 +02:00
|
|
|
|
_dirty = false;
|
2026-07-13 09:10:24 +02:00
|
|
|
|
_edit_name = false;
|
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
|
|
|
|
resetList();
|
2026-06-21 23:09:35 +02:00
|
|
|
|
_editor.freq.active = false;
|
2026-05-26 15:49:50 +02:00
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
|
|
int render(DisplayDriver& display) override {
|
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
|
2026-07-13 09:10:24 +02:00
|
|
|
|
if (_edit_slot >= 0 || _edit_name || _picker.saving) {
|
2026-06-08 20:42:18 +02:00
|
|
|
|
return _kb->render(display);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
|
display.drawCenteredHeader("SETTINGS");
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
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
|
|
|
|
_acc.render(display,
|
|
|
|
|
|
// Section header: "[+/-] Name"
|
|
|
|
|
|
[&](int sec, int y, bool sel, int reserve, bool collapsed) {
|
|
|
|
|
|
_reserve = reserve;
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-06-29 18:53:18 +02:00
|
|
|
|
drawRowSelection(display, y, sel, reserve);
|
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
|
|
|
|
display.setCursor(2, y);
|
|
|
|
|
|
display.print(collapsed ? "+" : "-");
|
|
|
|
|
|
display.print(" ");
|
|
|
|
|
|
display.print(sectionName(_sec_header[sec]));
|
|
|
|
|
|
},
|
|
|
|
|
|
// Item row
|
|
|
|
|
|
[&](int sec, int item, int y, bool sel, int reserve) {
|
|
|
|
|
|
_reserve = reserve;
|
|
|
|
|
|
renderItem(display, _sec_items[sec][item], y, sel);
|
|
|
|
|
|
});
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
2026-06-21 22:32:47 +02:00
|
|
|
|
if (_picker.menu.active) _picker.menu.render(display);
|
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
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
return 2000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool handleInput(char c) override {
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
|
|
|
|
|
|
// Keyboard editing mode for message slots
|
|
|
|
|
|
if (_edit_slot >= 0) {
|
2026-06-08 20:42:18 +02:00
|
|
|
|
auto res = _kb->handleInput(c);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (res == KeyboardWidget::DONE) {
|
|
|
|
|
|
if (p) {
|
2026-06-08 20:42:18 +02:00
|
|
|
|
strncpy(p->custom_msgs[_edit_slot], _kb->buf, sizeof(p->custom_msgs[0]) - 1);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
p->custom_msgs[_edit_slot][sizeof(p->custom_msgs[0]) - 1] = '\0';
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
_edit_slot = -1;
|
|
|
|
|
|
} else if (res == KeyboardWidget::CANCELLED) {
|
|
|
|
|
|
_edit_slot = -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 09:10:24 +02:00
|
|
|
|
// Keyboard editing mode for the device name
|
|
|
|
|
|
if (_edit_name) {
|
|
|
|
|
|
auto res = _kb->handleInput(c);
|
|
|
|
|
|
if (res == KeyboardWidget::DONE) {
|
|
|
|
|
|
if (p) {
|
|
|
|
|
|
strncpy(p->node_name, _kb->buf, sizeof(p->node_name) - 1);
|
|
|
|
|
|
p->node_name[sizeof(p->node_name) - 1] = '\0';
|
|
|
|
|
|
_dirty = true; // savePrefsIfDirty on exit; getNodeName()/self-advert read node_name live
|
|
|
|
|
|
}
|
|
|
|
|
|
_edit_name = false;
|
|
|
|
|
|
} else if (res == KeyboardWidget::CANCELLED) {
|
|
|
|
|
|
_edit_name = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// Digit-by-digit Freq editor
|
2026-06-21 23:09:35 +02:00
|
|
|
|
if (_editor.active()) {
|
|
|
|
|
|
if (_editor.handleFreqInput(c) && p) { _task->applyRadioParams(); _dirty = true; }
|
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
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Keyboard editing mode for naming a new saved preset
|
2026-06-21 22:32:47 +02:00
|
|
|
|
if (_picker.saving) {
|
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
|
|
|
|
auto res = _kb->handleInput(c);
|
|
|
|
|
|
if (res == KeyboardWidget::DONE) {
|
2026-06-21 22:32:47 +02:00
|
|
|
|
if (p && _picker.save(p, _kb->buf, radioTarget(p))) {
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
_task->showAlert("Preset saved", 800);
|
|
|
|
|
|
}
|
|
|
|
|
|
_picker.saving = false;
|
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
|
|
|
|
} else if (res == KeyboardWidget::CANCELLED) {
|
2026-06-21 22:32:47 +02:00
|
|
|
|
_picker.saving = false;
|
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
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Radio preset popup (and its "delete a saved preset" sub-list)
|
2026-06-21 22:32:47 +02:00
|
|
|
|
if (_picker.menu.active) {
|
|
|
|
|
|
auto res = _picker.menu.handleInput(c);
|
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
|
|
|
|
if (res == PopupMenu::SELECTED && p) {
|
2026-06-21 22:32:47 +02:00
|
|
|
|
switch (_picker.onSelected(_picker.menu.selectedIndex(), p, radioTarget(p))) {
|
|
|
|
|
|
case RadioPresetPicker::START_SAVE:
|
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
|
|
|
|
_kb->begin("", (int)sizeof(p->user_radio_presets[0].name) - 1);
|
2026-07-05 11:02:43 +02:00
|
|
|
|
_kb->clearPlaceholders(); // {loc}/{time} are for messages, not preset names
|
2026-06-21 22:32:47 +02:00
|
|
|
|
break;
|
|
|
|
|
|
case RadioPresetPicker::APPLIED:
|
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
|
|
|
|
_task->applyRadioParams();
|
|
|
|
|
|
_dirty = true;
|
2026-06-21 22:32:47 +02:00
|
|
|
|
break;
|
|
|
|
|
|
case RadioPresetPicker::DELETED:
|
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
|
|
|
|
_dirty = true;
|
2026-06-21 22:32:47 +02:00
|
|
|
|
_task->showAlert("Preset deleted", 800);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case RadioPresetPicker::NONE:
|
|
|
|
|
|
break;
|
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
|
|
|
|
}
|
|
|
|
|
|
} else if (res == PopupMenu::CANCELLED) {
|
2026-06-21 22:32:47 +02:00
|
|
|
|
_picker.deleting = false;
|
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
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (c == KEY_CANCEL) {
|
2026-06-29 18:47:51 +02:00
|
|
|
|
_task->savePrefsIfDirty(_dirty);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
_task->gotoHomeScreen();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// Up/down navigation and section fold/unfold live in the shared helper.
|
|
|
|
|
|
// Enter on an item returns ACTIVATED and falls through to the per-item logic
|
|
|
|
|
|
// below; left/right are ignored by the helper and likewise fall through.
|
|
|
|
|
|
AccordionList::Result ar = _acc.handleInput(c);
|
|
|
|
|
|
if (ar == AccordionList::HANDLED) return true;
|
|
|
|
|
|
_selected = currentItem();
|
|
|
|
|
|
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
|
bool right = keyIsNext(c);
|
|
|
|
|
|
bool left = keyIsPrev(c);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
bool enter = (c == KEY_ENTER);
|
|
|
|
|
|
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_BRIGHTNESS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == BRIGHTNESS) {
|
|
|
|
|
|
uint8_t lvl = _task->getBrightnessLevel();
|
|
|
|
|
|
if (right && lvl < 4) { _task->setBrightnessLevel(lvl + 1); _dirty = true; return true; }
|
|
|
|
|
|
if (left && lvl > 0) { _task->setBrightnessLevel(lvl - 1); _dirty = true; return true; }
|
|
|
|
|
|
return right || left;
|
|
|
|
|
|
}
|
2026-05-22 18:34:20 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == BUZZER && (left || right || enter)) {
|
|
|
|
|
|
_task->cycleBuzzerMode();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == BUZZER_VOLUME) {
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
uint8_t lvl = _task->getBuzzerVolume();
|
|
|
|
|
|
if (right && lvl < 4) { _task->setBuzzerVolumeLevel(lvl + 1); _dirty = true; return true; }
|
|
|
|
|
|
if (left && lvl > 0) { _task->setBuzzerVolumeLevel(lvl - 1); _dirty = true; return true; }
|
|
|
|
|
|
#endif
|
|
|
|
|
|
return right || left;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == DM_MELODY && p && (left || right || enter)) {
|
2026-06-07 09:13:02 +02:00
|
|
|
|
p->notif_melody_dm = (p->notif_melody_dm + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
_dirty = true; return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == CH_MELODY && p && (left || right || enter)) {
|
2026-06-07 09:13:02 +02:00
|
|
|
|
p->notif_melody_ch = (p->notif_melody_ch + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
_dirty = true; return true;
|
|
|
|
|
|
}
|
2026-06-04 08:46:15 +02:00
|
|
|
|
if (_selected == AD_SOUND && p && (left || right || enter)) {
|
2026-06-07 09:13:02 +02:00
|
|
|
|
p->notif_melody_ad = (p->notif_melody_ad + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT;
|
2026-06-04 08:46:15 +02:00
|
|
|
|
_dirty = true; return true;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
2026-06-07 10:53:17 +02:00
|
|
|
|
if (_selected == AD_SOUND_SCOPE && p && (left || right || enter)) {
|
|
|
|
|
|
p->advert_sound_scope ^= 1;
|
|
|
|
|
|
_dirty = true; return true;
|
|
|
|
|
|
}
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if (isHomePage(_selected) && p) {
|
|
|
|
|
|
if (left || right) {
|
|
|
|
|
|
movePageInOrder(_selected, left ? -1 : 1, p);
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (enter && homePageToggleable(_selected)) {
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
if (!p->home_pages_mask) p->home_pages_mask = NodePrefs::HP_ALL;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
p->home_pages_mask ^= homePageBit(_selected);
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return enter;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
if (_selected == TX_POWER && p) {
|
|
|
|
|
|
if (right && p->tx_power_dbm < 22) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; }
|
|
|
|
|
|
if (left && p->tx_power_dbm > 2) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; }
|
|
|
|
|
|
}
|
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
|
|
|
|
if (_selected == RADIO_PRESET && p && enter) {
|
2026-06-21 22:32:47 +02:00
|
|
|
|
_picker.open(p, radioTarget(p), "Radio Preset");
|
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
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-06-21 23:09:35 +02:00
|
|
|
|
// Enter Freq's digit-by-digit editor. Bounds come from the radio driver
|
|
|
|
|
|
// itself (RadioLib's own validated range for this chip) so a digit can never
|
|
|
|
|
|
// be nudged to a value setFrequency() would reject.
|
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
|
|
|
|
if (_selected == CUSTOM_FREQ && p && enter) {
|
|
|
|
|
|
float min_mhz, max_mhz;
|
|
|
|
|
|
radio_driver.getFreqBounds(min_mhz, max_mhz);
|
2026-06-21 23:09:35 +02:00
|
|
|
|
_editor.beginFreq(p->freq, min_mhz, max_mhz);
|
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
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-06-21 23:09:35 +02:00
|
|
|
|
int dir = right ? 1 : (left ? -1 : 0);
|
|
|
|
|
|
if (_selected == CUSTOM_SF && p && dir && RadioParamsEditor::stepSF(p->sf, dir)) { _task->applyRadioParams(); _dirty = true; return true; }
|
|
|
|
|
|
if (_selected == CUSTOM_BW && p && dir && RadioParamsEditor::stepBW(p->bw, dir)) { _task->applyRadioParams(); _dirty = true; return true; }
|
|
|
|
|
|
if (_selected == CUSTOM_CR && p && dir && RadioParamsEditor::stepCR(p->cr, dir)) { _task->applyRadioParams(); _dirty = true; return true; }
|
2026-06-10 23:30:43 +02:00
|
|
|
|
if (_selected == POWER_SAVE && p && (left || right || enter)) {
|
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
|
|
|
|
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
|
2026-06-10 23:30:43 +02:00
|
|
|
|
p->rx_powersave ^= 1;
|
|
|
|
|
|
_task->applyPowerSave();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == TX_APC && p && (left || right || enter)) {
|
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
|
|
|
|
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
|
2026-06-10 23:30:43 +02:00
|
|
|
|
p->tx_apc ^= 1;
|
|
|
|
|
|
_task->applyApc();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == AUTO_OFF && p) {
|
|
|
|
|
|
int idx = autoOffIndex();
|
|
|
|
|
|
if (right) idx = (idx + 1) % AUTO_OFF_COUNT;
|
|
|
|
|
|
if (left) idx = (idx + AUTO_OFF_COUNT - 1) % AUTO_OFF_COUNT;
|
|
|
|
|
|
if (left || right) { p->auto_off_secs = AUTO_OFF_OPTS[idx]; _dirty = true; return true; }
|
|
|
|
|
|
}
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-17 09:55:51 +02:00
|
|
|
|
if (_selected == AUTO_LOCK && p && (left || right || enter)) {
|
|
|
|
|
|
p->auto_lock ^= 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == TIMEZONE && p) {
|
|
|
|
|
|
if (right && p->tz_offset_hours < 14) { p->tz_offset_hours++; _dirty = true; return true; }
|
|
|
|
|
|
if (left && p->tz_offset_hours > -12) { p->tz_offset_hours--; _dirty = true; return true; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == LOW_BAT && p) {
|
|
|
|
|
|
int idx = lowBatIndex();
|
|
|
|
|
|
if (right) idx = (idx + 1) % LOW_BAT_COUNT;
|
|
|
|
|
|
if (left) idx = (idx + LOW_BAT_COUNT - 1) % LOW_BAT_COUNT;
|
|
|
|
|
|
if (left || right) { p->low_batt_mv = LOW_BAT_OPTS[idx]; _dirty = true; return true; }
|
|
|
|
|
|
}
|
2026-06-04 00:44:56 +02:00
|
|
|
|
if (_selected == UNITS && p && (left || right || enter)) {
|
|
|
|
|
|
p->units_imperial ^= 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-07-13 09:10:24 +02:00
|
|
|
|
if (_selected == DEVICE_NAME && p && enter) {
|
|
|
|
|
|
_edit_name = true;
|
|
|
|
|
|
_kb->begin(the_mesh.getNodeName(), (int)sizeof(p->node_name) - 1);
|
|
|
|
|
|
_kb->clearPlaceholders(); // a device name is literal, not a message
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == REBOOT && enter) {
|
|
|
|
|
|
_task->savePrefsIfDirty(_dirty); // don't lose pending edits across the restart
|
|
|
|
|
|
_task->showAlert("Rebooting...", 800);
|
|
|
|
|
|
board.reboot();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-07-02 12:21:52 +02:00
|
|
|
|
if (_selected == KEYBOARD_TYPE && p && (left || right || enter)) {
|
|
|
|
|
|
p->keyboard_type ^= 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
if (_selected == KEYBOARD_ALPHABET && p && (left || right || enter)) {
|
|
|
|
|
|
int idx = p->keyboard_alt_alphabet;
|
|
|
|
|
|
if (right || enter) idx = (idx + 1) % NodePrefs::KB_ALPHABET_COUNT;
|
|
|
|
|
|
else if (left) idx = (idx + NodePrefs::KB_ALPHABET_COUNT - 1) % NodePrefs::KB_ALPHABET_COUNT;
|
|
|
|
|
|
p->keyboard_alt_alphabet = (uint8_t)idx;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-06-14 23:33:16 +02:00
|
|
|
|
if (_selected == DM_RESEND && p) {
|
|
|
|
|
|
int n = p->dm_resend_count;
|
|
|
|
|
|
if (right || enter) n = (n + 1) % 6; // 0..5, wraps
|
|
|
|
|
|
else if (left) n = (n + 5) % 6;
|
|
|
|
|
|
if (left || right || enter) { p->dm_resend_count = (uint8_t)n; _dirty = true; return true; }
|
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == BATT_DISPLAY && p) {
|
|
|
|
|
|
int idx = p->batt_display_mode < BATT_DISPLAY_COUNT ? p->batt_display_mode : 0;
|
|
|
|
|
|
if (right) idx = (idx + 1) % BATT_DISPLAY_COUNT;
|
|
|
|
|
|
if (left) idx = (idx + BATT_DISPLAY_COUNT - 1) % BATT_DISPLAY_COUNT;
|
|
|
|
|
|
if (left || right) { p->batt_display_mode = idx; _dirty = true; return true; }
|
|
|
|
|
|
}
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_CLOCK_SECONDS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == CLOCK_SECONDS && p && (left || right || enter)) {
|
|
|
|
|
|
p->clock_hide_seconds ^= 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-22 10:47:19 +02:00
|
|
|
|
#endif
|
2026-05-21 21:04:43 +02:00
|
|
|
|
if (_selected == CLOCK_FORMAT && p && (left || right || enter)) {
|
|
|
|
|
|
p->clock_12h ^= 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_DISPLAY_ROTATION_SETTING
|
2026-05-20 21:53:40 +02:00
|
|
|
|
if (_selected == ROTATION && p && (left || right || enter)) {
|
|
|
|
|
|
p->display_rotation = (p->display_rotation + (left ? 3 : 1)) & 3;
|
|
|
|
|
|
_task->applyRotation();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-23 15:20:40 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_JOYSTICK_ROTATION_SETTING
|
2026-05-23 15:20:40 +02:00
|
|
|
|
if (_selected == JOY_ROTATION && p && (left || right || enter)) {
|
|
|
|
|
|
p->joystick_rotation = (p->joystick_rotation + (left ? 3 : 1)) & 3;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-24 10:22:03 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_FULL_REFRESH_SETTING
|
2026-05-24 10:22:03 +02:00
|
|
|
|
if (_selected == EINK_FULL_REFRESH && p && (left || right || enter)) {
|
|
|
|
|
|
int idx = p->eink_full_refresh_every;
|
|
|
|
|
|
if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0;
|
|
|
|
|
|
idx = (idx + (left ? EINK_FULL_REFRESH_COUNT - 1 : 1)) % EINK_FULL_REFRESH_COUNT;
|
|
|
|
|
|
p->eink_full_refresh_every = idx;
|
|
|
|
|
|
_task->applyFullRefreshInterval();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-20 21:53:40 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == DM_FILTER && p && (left || right || enter)) {
|
|
|
|
|
|
p->dm_show_all = p->dm_show_all ? 0 : 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-27 17:33:52 +02:00
|
|
|
|
if (_selected == CH_FILTER && p && (left || right || enter)) {
|
|
|
|
|
|
p->ch_fav_only = p->ch_fav_only ? 0 : 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == ROOM_FILTER && p && (left || right || enter)) {
|
|
|
|
|
|
p->room_fav_only = p->room_fav_only ? 0 : 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isMsgSlot(_selected) && enter) {
|
|
|
|
|
|
int slot = msgSlotIndex(_selected);
|
|
|
|
|
|
_edit_slot = slot;
|
2026-06-04 12:15:30 +02:00
|
|
|
|
// Bound to the custom_msgs store (140 B) so the wider keyboard buffer
|
|
|
|
|
|
// can't overflow it on save.
|
2026-06-08 20:42:18 +02:00
|
|
|
|
_kb->begin(p ? p->custom_msgs[slot] : "",
|
2026-06-04 12:15:30 +02:00
|
|
|
|
p ? (int)sizeof(p->custom_msgs[slot]) - 1 : KB_MAX_LEN);
|
2026-06-09 15:09:57 +02:00
|
|
|
|
kbAddSensorPlaceholders(*_kb, &sensors);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
const uint16_t SettingsScreen::AUTO_OFF_OPTS[5] = { 5, 15, 30, 60, 0 };
|
|
|
|
|
|
const char* SettingsScreen::AUTO_OFF_LABELS[5] = { "5s", "15s", "30s", "60s", "never" };
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
const uint16_t SettingsScreen::LOW_BAT_OPTS[7] = { 0, 3000, 3100, 3200, 3300, 3400, 3500 };
|
|
|
|
|
|
const char* SettingsScreen::LOW_BAT_LABELS[7] = { "off", "3.0V", "3.1V", "3.2V", "3.3V", "3.4V", "3.5V" };
|
|
|
|
|
|
const char* SettingsScreen::BATT_DISPLAY_LABELS[3] = { "icon", "%", "V" };
|
2026-06-07 09:13:02 +02:00
|
|
|
|
const char* SettingsScreen::SOUND_LABELS[4] = { "built-in", "M1", "M2", "None" };
|
2026-06-07 10:53:17 +02:00
|
|
|
|
const char* SettingsScreen::AD_SCOPE_LABELS[2] = { "All", "Zero-hop" };
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_FULL_REFRESH_SETTING
|
2026-05-24 10:22:03 +02:00
|
|
|
|
const char* SettingsScreen::EINK_FULL_REFRESH_LABELS[5] = { "off", "5", "10", "20", "30" };
|
|
|
|
|
|
#endif
|