Files
MeshCore-Solo/examples/companion_radio/NodePrefs.h

588 lines
32 KiB
C
Raw Normal View History

2025-06-01 09:25:17 -07:00
#pragma once
#include <cstdint>
#include <stdio.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
// Firmware's own boot-default radio params — used both as the companion's
// initial freq/sf/bw/cr (MyMesh.cpp) and, here, as the seed for a never-
// configured repeater profile (DataStore.cpp). Kept above any per-board
// target.h include so a board can still override via -D before this file
// is reached.
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 250
#endif
#ifndef LORA_SF
#define LORA_SF 10
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
// Bucket a companion frequency into whichever of the three license-exempt
// bands MeshCore's app-driven repeat toggle historically restricted to (see
// repeat_freq_ranges in MyMesh.cpp: 433.000 / 869.495 / 918.000 MHz). Used to
// seed a never-configured repeater profile in the same legal band as the
// companion's own network (MyMesh.cpp begin(), DataStore.cpp) — a flat
// single frequency could land outside the bands allowed where the operator
// actually lives.
static inline float defaultRepeaterFreqForBand(float companion_freq) {
if (companion_freq < 500.0f) return 433.000f;
if (companion_freq < 890.0f) return 869.495f;
return 918.000f;
}
#define TELEM_MODE_DENY 0
#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags
#define TELEM_MODE_ALLOW_ALL 2
#define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1
2026-06-07 10:53:17 +02:00
#define ADVERT_SOUND_SCOPE_ALL 0
#define ADVERT_SOUND_SCOPE_ZERO_HOP 1
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
float freq;
uint8_t sf;
uint8_t cr;
uint8_t multi_acks;
uint8_t manual_add_contacts;
float bw;
int8_t tx_power_dbm;
uint8_t telemetry_mode_base;
uint8_t telemetry_mode_loc;
uint8_t telemetry_mode_env;
float rx_delay_base;
uint32_t ble_pin;
uint8_t advert_loc_policy;
uint8_t buzzer_quiet;
uint8_t buzzer_volume; // 0=min..4=max, default 4
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval; // GPS read interval in seconds
uint8_t autoadd_config; // bitmask for auto-add contacts config
uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
2026-02-14 15:50:06 +11:00
uint8_t client_repeat;
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
char default_scope_name[31];
uint8_t default_scope_key[16];
uint8_t display_brightness; // 0=min..4=max, default 2 (medium)
uint16_t auto_off_secs; // display auto-off: 0=never, else seconds (default 15)
uint8_t auto_lock; // 0=disabled, 1=lock screen when display turns off
int8_t tz_offset_hours; // timezone offset from UTC, -12..+14 (default 0)
uint16_t low_batt_mv; // auto-shutdown threshold: 0=disabled, 3000-3500 mV
uint8_t batt_display_mode; // 0=icon, 1=percent, 2=voltage
char custom_msgs[10][140]; // user-defined quick messages (supports {loc}, {time})
uint64_t ch_notif_override; // bitmask: bit i = channel i has explicit notification setting
uint64_t ch_notif_muted; // bitmask: bit i = channel i muted (only if override bit set)
uint8_t dm_show_all; // 0=favourites only (default), 1=all chat contacts
uint8_t room_fav_only; // 0=all room servers (default), 1=favourites only
uint8_t ringtone_bpm_idx; // index into {60,90,120,150,180}
uint8_t ringtone_len; // number of notes in custom ringtone (0 = use default)
uint8_t ringtone_notes[32]; // packed: bits0-2=pitch, bits3-4=octave-4, bits5-6=dur_idx
uint16_t home_pages_mask; // bitmask of visible home pages (bit0=Clock..bit8=Shutdown); 0=all visible
feat(bot): room-server target, tab-carousel UI, per-target independence Auto-Reply Bot gains a third target (room servers, alongside DM and channel), new {name}/{hops} reply placeholders, and a DM all/favourites allow-list. BotScreen is redesigned as a circular tab carousel (Direct / Channel / Room / Other, same interaction as Nearby Nodes' filter tabs) instead of one long scrolling list, which also exposed and fixed a leftover coupling where Channel/Room trigger-replies and their !command handling secretly depended on the DM tab's Enable/Commands toggles — each target's Enable and Commands are now fully independent. - NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope, bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged at 2712 (new bytes absorbed existing padding, verified via a standalone host compile + offsetof check). - DataStore: persists all new fields; seeds bot_commands_ch/room from the old shared bot_commands_enabled on upgrade so existing channel/room command behaviour isn't silently lost. - MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's shape but post via sendMessage (room relays to members itself); requires an existing login session with that room, same as a manual post would. botDmSenderAllowed() gates DM trigger-reply/commands on the favourites bit when bot_dm_scope=Fav. - MsgExpand: {name}/{hops} as optional trailing params (default nullptr/-1, no existing caller affected) — deliberately not exposed on the general compose keyboard, only on bot Reply fields. - QuickMsgScreen/UITask: room-target picker (mirrors the channel picker), routing through the existing room-login prompt when there's no saved password yet. - BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves rows, Enter is now the only way to change a value); Enable split out of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode; Commands moved from a shared toggle into each tab. - docs/tools_screen.md updated for the new tab layout and behaviour. Not build-verified — no PlatformIO toolchain in this environment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
uint8_t bot_enabled; // 0=disabled, 1=DM trigger-reply active — DM only; channel/room have their own bot_channel_enabled/bot_room_enabled and don't depend on this
uint8_t bot_channel_enabled; // 0=disabled, 1=channel bot active for bot_channel_idx
uint8_t bot_channel_idx; // channel index for channel bot [del→onChannelRemoved]
feat(bot): hardening + auto-responder, query commands, quiet hours Reply-bot overhaul on top of the trigger/reply auto-reply: Robustness - single botTriggerMatches() shared by DM/channel paths; named constants (BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers. - per-contact DM throttle (8-entry ring) so a second sender isn't starved while one contact is on cooldown. - channel anti-loop: skip only when an incoming message equals our own reply (was: any reply containing the trigger word — which silently killed channel replies for the common "trigger word in reply" setup). Features - away / reply-to-all: a lone "*" trigger matches every message. - independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works on the channel too, bounded by cooldown + echo guard. - query commands: a DM or monitored-channel message is scanned for "!word" tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one combined reply. DM = per-contact throttle, ignores quiet hours; channel = broadcast, per-channel cooldown, respects quiet hours. !hops uses getPathHashCount() (0 = direct). - quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies. - reply counter shown in the BotScreen header. UI / storage - BotScreen: 9 rows, scrolling list (no more cramming), field I/O via fieldBuf()/fieldCap(). - prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration (bot_trigger_ch seeded from bot_trigger on upgrade). - docs: tools_screen bot section rewritten; FEATURES.md refreshed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
char bot_trigger[64]; // DM trigger phrase (case-insensitive contains; "*" = any DM)
char bot_reply_dm[140]; // auto-reply text for DM
char bot_reply_ch[140]; // auto-reply text for channel
feat(bot): hardening + auto-responder, query commands, quiet hours Reply-bot overhaul on top of the trigger/reply auto-reply: Robustness - single botTriggerMatches() shared by DM/channel paths; named constants (BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers. - per-contact DM throttle (8-entry ring) so a second sender isn't starved while one contact is on cooldown. - channel anti-loop: skip only when an incoming message equals our own reply (was: any reply containing the trigger word — which silently killed channel replies for the common "trigger word in reply" setup). Features - away / reply-to-all: a lone "*" trigger matches every message. - independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works on the channel too, bounded by cooldown + echo guard. - query commands: a DM or monitored-channel message is scanned for "!word" tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one combined reply. DM = per-contact throttle, ignores quiet hours; channel = broadcast, per-channel cooldown, respects quiet hours. !hops uses getPathHashCount() (0 = direct). - quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies. - reply counter shown in the BotScreen header. UI / storage - BotScreen: 9 rows, scrolling list (no more cramming), field I/O via fieldBuf()/fieldCap(). - prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration (bot_trigger_ch seeded from bot_trigger on upgrade). - docs: tools_screen bot section rewritten; FEATURES.md refreshed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
char bot_trigger_ch[64]; // channel trigger phrase (independent of DM; "*" = any channel msg)
feat(bot): room-server target, tab-carousel UI, per-target independence Auto-Reply Bot gains a third target (room servers, alongside DM and channel), new {name}/{hops} reply placeholders, and a DM all/favourites allow-list. BotScreen is redesigned as a circular tab carousel (Direct / Channel / Room / Other, same interaction as Nearby Nodes' filter tabs) instead of one long scrolling list, which also exposed and fixed a leftover coupling where Channel/Room trigger-replies and their !command handling secretly depended on the DM tab's Enable/Commands toggles — each target's Enable and Commands are now fully independent. - NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope, bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged at 2712 (new bytes absorbed existing padding, verified via a standalone host compile + offsetof check). - DataStore: persists all new fields; seeds bot_commands_ch/room from the old shared bot_commands_enabled on upgrade so existing channel/room command behaviour isn't silently lost. - MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's shape but post via sendMessage (room relays to members itself); requires an existing login session with that room, same as a manual post would. botDmSenderAllowed() gates DM trigger-reply/commands on the favourites bit when bot_dm_scope=Fav. - MsgExpand: {name}/{hops} as optional trailing params (default nullptr/-1, no existing caller affected) — deliberately not exposed on the general compose keyboard, only on bot Reply fields. - QuickMsgScreen/UITask: room-target picker (mirrors the channel picker), routing through the existing room-login prompt when there's no saved password yet. - BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves rows, Enter is now the only way to change a value); Enable split out of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode; Commands moved from a shared toggle into each tab. - docs/tools_screen.md updated for the new tab layout and behaviour. Not build-verified — no PlatformIO toolchain in this environment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
uint8_t bot_commands_enabled; // 0=off, 1=answer !ping/!batt/!loc/!time/!help DM commands — DM only, see bot_commands_ch/bot_commands_room below for the other two targets
feat(bot): hardening + auto-responder, query commands, quiet hours Reply-bot overhaul on top of the trigger/reply auto-reply: Robustness - single botTriggerMatches() shared by DM/channel paths; named constants (BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers. - per-contact DM throttle (8-entry ring) so a second sender isn't starved while one contact is on cooldown. - channel anti-loop: skip only when an incoming message equals our own reply (was: any reply containing the trigger word — which silently killed channel replies for the common "trigger word in reply" setup). Features - away / reply-to-all: a lone "*" trigger matches every message. - independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works on the channel too, bounded by cooldown + echo guard. - query commands: a DM or monitored-channel message is scanned for "!word" tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one combined reply. DM = per-contact throttle, ignores quiet hours; channel = broadcast, per-channel cooldown, respects quiet hours. !hops uses getPathHashCount() (0 = direct). - quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies. - reply counter shown in the BotScreen header. UI / storage - BotScreen: 9 rows, scrolling list (no more cramming), field I/O via fieldBuf()/fieldCap(). - prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration (bot_trigger_ch seeded from bot_trigger on upgrade). - docs: tools_screen bot section rewritten; FEATURES.md refreshed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
uint8_t bot_quiet_start; // quiet-hours start hour, local 0-23 (start==end → disabled)
uint8_t bot_quiet_end; // quiet-hours end hour, local 0-23
uint8_t clock_hide_seconds; // 0=show HH:MM:SS/refresh 1s (default), 1=hide/refresh 60s
uint8_t clock_12h; // 0=24h (default), 1=12h with AM/PM
uint8_t buzzer_auto; // 0=manual (default), 1=auto-mute when BT connected
struct DmNotifEntry { uint8_t prefix[4]; uint8_t state; }; // state: 0=default,1=muted,2=force-on
static const int DM_NOTIF_TABLE_MAX = 16;
DmNotifEntry dm_notif[DM_NOTIF_TABLE_MAX]; // 16*5 = 80 bytes [del→onContactRemoved]
uint8_t dashboard_fields[3]; // 0=None,1=Batt V,2=Temp,3=Hum,4=Pres,5=GPS,6=Alt,7=Lux,8=CO2,9=Nodes,10=Msgs,11=Batt %
uint32_t advert_auto_interval_sec; // periodic 0-hop advert with GPS: 0=off, else seconds
// Second melody slot (same packing as ringtone_*)
uint8_t ringtone2_bpm_idx;
uint8_t ringtone2_len;
uint8_t ringtone2_notes[32];
// Global melodies for notifications: 0=built-in, 1=melody1, 2=melody2, 3=none
uint8_t notif_melody_dm;
uint8_t notif_melody_ch;
2026-06-04 08:46:15 +02:00
uint8_t notif_melody_ad;
2026-06-07 10:53:17 +02:00
// Advert sound filter: 0=all adverts, 1=zero-hop only
uint8_t advert_sound_scope;
// Per-channel melody override (2 bitmasks, 1 bit per channel)
uint64_t ch_notif_melody_set; // bit i = channel i has explicit melody [del→onChannelRemoved]
uint64_t ch_notif_melody_2; // bit i = use melody 2 (else melody 1, when set bit is set)
// Per-DM melody table
struct DmMelodyEntry { uint8_t prefix[4]; uint8_t slot; }; // slot: 0=global,1=melody1,2=melody2
static const int DM_MELODY_TABLE_MAX = 16;
DmMelodyEntry dm_melody[DM_MELODY_TABLE_MAX]; // [del→onContactRemoved]
uint8_t use_lemon_font; // 0=default Adafruit font, 1=Lemon font (Unicode, pixel-accurate wrap)
uint8_t display_rotation; // 0-3; only used on e-ink displays
// Home screen page order: each byte = HomePageBit + 1. 0 terminates the list.
// Validity gated by page_order_set magic (see below) — not by entry value range,
// so a junk byte in 1..HPB_COUNT cannot trigger custom-order mode.
// Declared as a literal (not HPB_COUNT) so the field offset stays stable across
// builds that add HomePageBit entries. The first PAGE_ORDER_LEN_V1 bytes persist
// at this offset (backward-compatible); the remaining slots live at the file
// tail (see DataStore) so pre-0x0019 saves still load without shifting.
uint8_t page_order[13];
uint8_t joystick_rotation; // 0-3 steps CW; independent of display_rotation
uint8_t eink_full_refresh_every; // index into {0,5,10,20,30}: full refresh every N partials (0=off)
uint8_t page_order_set; // 0xA5 = page_order is user-configured; anything else = use default
static const uint8_t PAGE_ORDER_MAGIC = 0xA5;
feat(ui): Favourites home page (phase 1: storage + read-only grid) New home page rendering 6 pinned-contact slots. Grid transposes to orientation: landscape → 3×2, portrait → 2×3. Joystick claims UP/DOWN and inner LEFT/RIGHT for grid nav; edge LEFT/RIGHT fall through to page navigation. Selected tile inverts via drawSelectionRow, filled tile shows contact name + unread badge, empty tile shows "+". Storage: - NodePrefs::favourite_contacts[6][6] (6-byte pub_key prefix per slot, all-zero = empty; collision probability with a real key is 2^-48) - HomePageBit gains HPB_FAVOURITES = 11 (HPB_COUNT = 12), with a new HP_FAVOURITES mask bit so the page is toggleable in Settings - New PAGE_ORDER_LEN constant decouples the persisted array length (11) from the page-type count; loops over page_order now use it, value-range checks still use HPB_COUNT - homePageBit returns 0 for HPB_SETTINGS / HPB_QUICK_MSG explicitly rather than by bit-index range, so HPB_FAVOURITES (bit 11) is correctly treated as toggleable - Default order inserts FAVOURITES after CLOCK; SHUTDOWN drops to the fallback "missing pages" append (still reachable, lands at end) Schema sentinel bumped to 0xC0DE0002. DataStore::loadPrefsInt and savePrefs read/write the new field. Older saves leave the field zero-initialised (all slots empty), which is the natural starting state. Phase 2 (Pin from Contact options) and Phase 3 (Pin from empty-slot mini picker) wire interactions; this commit handles render + nav only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:00:27 +02:00
// Favourites dial: 6 pinned contacts, stored as first 6 bytes of pub_key per slot.
// All-zero = empty slot (probability a real pub_key starts with 6 zero bytes is 2^-48).
// Layout transposes between landscape (3×2) and portrait (2×3).
static const uint8_t FAVOURITES_COUNT = 6;
static const uint8_t FAVOURITE_PREFIX_LEN = 6;
uint8_t favourite_contacts[FAVOURITES_COUNT][FAVOURITE_PREFIX_LEN]; // [del→onContactRemoved]
feat(ui): Favourites home page (phase 1: storage + read-only grid) New home page rendering 6 pinned-contact slots. Grid transposes to orientation: landscape → 3×2, portrait → 2×3. Joystick claims UP/DOWN and inner LEFT/RIGHT for grid nav; edge LEFT/RIGHT fall through to page navigation. Selected tile inverts via drawSelectionRow, filled tile shows contact name + unread badge, empty tile shows "+". Storage: - NodePrefs::favourite_contacts[6][6] (6-byte pub_key prefix per slot, all-zero = empty; collision probability with a real key is 2^-48) - HomePageBit gains HPB_FAVOURITES = 11 (HPB_COUNT = 12), with a new HP_FAVOURITES mask bit so the page is toggleable in Settings - New PAGE_ORDER_LEN constant decouples the persisted array length (11) from the page-type count; loops over page_order now use it, value-range checks still use HPB_COUNT - homePageBit returns 0 for HPB_SETTINGS / HPB_QUICK_MSG explicitly rather than by bit-index range, so HPB_FAVOURITES (bit 11) is correctly treated as toggleable - Default order inserts FAVOURITES after CLOCK; SHUTDOWN drops to the fallback "missing pages" append (still reachable, lands at end) Schema sentinel bumped to 0xC0DE0002. DataStore::loadPrefsInt and savePrefs read/write the new field. Older saves leave the field zero-initialised (all slots empty), which is the natural starting state. Phase 2 (Pin from Contact options) and Phase 3 (Pin from empty-slot mini picker) wire interactions; this commit handles render + nav only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:00:27 +02:00
// GPS trail cadence. Logging on/off is a runtime state (Tools Trail),
// not a persisted preference.
uint8_t trail_interval_idx; // reserved — sampling cadence is now fixed at TrailStore::SAMPLING_SECS
uint8_t trail_min_delta_idx; // min-distance gate level (0=finest..3); metres or feet per units_imperial
uint8_t trail_units_idx; // legacy: old combined speed/pace+unit index (km/h, mph, min/km, min/mi)
2026-05-25 08:22:12 +02:00
uint64_t ch_fav_bitmask; // bit i = channel i is marked as favourite
uint8_t ch_fav_only; // 0=show all channels (default), 1=show favourites only
// Global measurement system for every distance/speed shown in the UI
// (Nearby, Trail, navigate-to-point). 0=metric (default), 1=imperial.
uint8_t units_imperial;
// Trail Summary readout: 0=speed (km/h or mph), 1=pace (min/km or min/mi).
// The km-vs-mi choice now comes from units_imperial, so this is just the mode.
uint8_t trail_show_pace;
// Hardware duty-cycle receive (battery saver): 0=continuous RX (default), 1=on.
// The SX126x cycles RX↔sleep on its own and wakes on a preamble — cuts average
// RX current at the cost of a little receive latency. See RadioLibWrapper
// power-save (startReceiveDutyCycleAuto).
uint8_t rx_powersave;
// Adaptive Power Control: 0=off (fixed tx_power_dbm, default), 1=on. When on,
// tx_power_dbm is treated as a ceiling and the radio's actual power is lowered
// at runtime on strong links (good ACK SNR), saving TX energy. Never persisted
// below the ceiling, so disabling restores the user's configured power.
uint8_t tx_apc;
2026-06-14 23:33:16 +02:00
// Auto-resend for on-device DMs: number of extra send attempts (0..5) made when
// no end-to-end ACK arrives before the deadline, before the delivery marker
// shows ✗. 0 = no auto-resend (single attempt). Default 2.
uint8_t dm_resend_count;
// User-saved radio presets, written by the "Save current..." entry in the
// shared preset picker (Settings > Radio and Tools > Repeater both populate
// these same slots). name[0] == '\0' marks an empty slot.
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
struct UserRadioPreset {
char name[16];
float freq;
float bw;
uint8_t sf;
uint8_t cr;
};
static const uint8_t USER_RADIO_PRESET_MAX = 4;
UserRadioPreset user_radio_presets[USER_RADIO_PRESET_MAX];
// Repeater forwarding filters — only consulted when client_repeat is on, via
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
// MyMesh::allowPacketForward(). Both default to off (0) so behaviour is
// unchanged until the user opts in (Tools > Repeater).
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
// repeat_skip_adverts: 1 = don't re-flood ADVERT packets (the highest-volume
// flood traffic); messages/acks still relay.
// repeat_max_hops: drop a flood packet once it has already travelled this
// many hops. 0 = no hop limit.
// repeat_delay_boost: extra retransmit-delay multiplier for FORWARDED floods
// only (own sends are unaffected) — a mobile companion yields to better-sited
// fixed repeaters. Effective delay = base * (1 + repeat_delay_boost). 0 = off.
// repeat_min_snr: drop a flood packet received below this SNR (dB), so marginal
// fringe traffic isn't re-flooded. REPEAT_SNR_DISABLED (-128) = off.
// repeat_suppress_dup: 1 = cancel a queued retransmit when the same flood is
// overheard from another node first (less redundant airtime in dense mesh).
uint8_t repeat_skip_adverts;
uint8_t repeat_max_hops;
uint8_t repeat_delay_boost;
int8_t repeat_min_snr;
static const int8_t REPEAT_SNR_DISABLED = -128;
uint8_t repeat_suppress_dup;
// Optional dedicated radio profile for repeater mode. When repeater_use_profile
// is 1, enabling the repeater switches the radio to repeater_freq/bw/sf/cr and
// disabling restores the companion's freq/bw/sf/cr (the fields above). 0 = the
// repeater stays on the current companion frequency. Default (a never-configured
// device) is 1, seeded via defaultRepeaterFreqForBand(freq) + LORA_SF/BW/CR —
// repeating on whatever private network the companion later joins isn't the
// community norm, and the seed stays in the same legal band as the companion's
// own network rather than a flat frequency.
uint8_t repeater_use_profile;
float repeater_freq;
float repeater_bw;
uint8_t repeater_sf;
uint8_t repeater_cr;
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
// Track positions shared by other nodes via [LOC] messages (LiveTrackStore).
// 0 = ignore shared positions (default), 1 = parse incoming DM/channel [LOC]
// shares and update the live-track table (Nearby "Live" view / map).
uint8_t track_shared_loc;
// Live location sharing — a message-based "beacon". When enabled, the device
// periodically sends a [LOC] message to the chosen target while it moves.
// Configured from the Map (Trail screen) "Live share" menu.
uint8_t loc_share_enabled; // 0=off (default), 1=auto-sharing on
uint8_t loc_share_target_type; // 0=channel, 1=DM contact
uint8_t loc_share_channel_idx; // target channel index (when target_type==0) [del→onChannelRemoved]
uint8_t loc_share_dm_prefix[6]; // target contact pubkey prefix (when target_type==1) [del→onContactRemoved]
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
uint8_t loc_share_move_idx; // movement gate level (index into locShareMoveMeters)
uint8_t loc_share_interval_idx; // min send interval (index into locShareIntervalSecs)
uint8_t loc_share_heartbeat_idx; // stationary heartbeat (index into locShareHeartbeatSecs)
// Locator — a single geofence around a saved point. When enabled the device
// watches its own GPS fix and beeps + shows an alert when it crosses into
// (arrive) or out of (leave) the radius. The target coordinate/label is a
// snapshot of a chosen waypoint, so the alert survives the waypoint being
// edited or deleted. Configured from Tools Locator.
uint8_t locator_enabled; // 0=off (default), 1=armed
uint8_t locator_has_target; // 0=no target chosen yet, 1=target set
uint8_t locator_radius_idx; // index into locatorRadiusMeters
uint8_t locator_mode; // 0=arrive, 1=leave, 2=both
int32_t locator_lat_1e6; // target latitude (1e6-scaled; last-known for a contact)
int32_t locator_lon_1e6; // target longitude (1e6-scaled; last-known for a contact)
char locator_label[12]; // target name for the alert text (WAYPOINT_LABEL_LEN)
// Target can be a static waypoint or a live contact: for a contact the engine
// re-reads the latest [LOC] position each evaluation (keyed by pubkey prefix),
// so the geofence follows a moving person ("alert when my friend is near").
uint8_t locator_target_kind; // 0=waypoint (static), 1=live contact
uint8_t locator_key[6]; // contact pubkey prefix when target_kind==1 [del→onContactRemoved]
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
// Trail auto-pause — when tracking, automatically freeze the trail (timer +
// sampling) after the device has sat still for this long, and resume on the
// next real movement. 0 = off. Index into trailAutoPauseSecs.
uint8_t trail_autopause_idx;
// Locator proximity beeper — when on (and the alert is armed with a target),
// the device ticks while inside the radius and shortens the gap between ticks
// the closer it gets to the target, like a homing beeper. Independent of the
// discrete arrive/leave alert (locator_mode).
uint8_t locator_beeper; // 0=off (default), 1=on
// GPS averaging for waypoint marking — when set, "Mark here" samples the GPS
// fix for this many seconds and stores the mean position, for a more accurate
// mark than a single instantaneous fix. 0 = off (instant mark, the default).
// Index into gpsAvgSecs(). [Tools Trail Settings Mark avg]
uint8_t gps_avg_idx;
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
// Alarm clock — a wake alarm, configured from the Clock page (Enter Alarm).
// Stored as a local time-of-day; the actual fire instant is (re)computed as an
// absolute time in tickBackground(), which is what makes it robust to RTC
// re-syncs: the mesh (every inbound packet), the companion app, GPS and the
// CLI can all jump getCurrentTime() at any moment, but an absolute target
// instant stays correct across small corrections and still fires (late) if
// the clock jumps over it. With alarm_repeat_mask == 0 it's one-shot: fires
// once, then alarm_on clears. A non-zero mask instead re-arms it for the next
// matching weekday (see UITask::computeAlarmNextFire/evaluateAlarm) and
// alarm_on stays set. The minutnik (countdown) and stoper (stopwatch) are
// runtime-only and not persisted.
uint8_t alarm_on; // 0=off (default), 1=armed
feat(companion): clock tools — alarm, countdown timer, stopwatch Add a Clock Tools screen (Enter on the home Clock page) with three time utilities, plus the engine that drives them from UITask::loop() so they work regardless of the current screen / display state: - Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump the clock) — small corrections still fire on time, a jump over the target still fires (late, up to 6 h). Disarms after firing. A bell icon signals an armed alarm on the clock face and the top status bar. - Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout, rings even when off-screen. - Stopwatch: millis-based, keeps running in the background. Numeric fields use the shared framework DigitEditor (digit-by-digit, min/max enforced). The alarm/timer/ring engine lives in UITask alongside the locator/live-share engines (no screen-cast for time-critical logic). E-ink: live readouts refresh coarsely (and on any key); timing is exact regardless, and the countdown's buzzer fires on time. Rings override mute and are dismissed by any key (auto-stop after 1 min). Cannot wake from a full Shutdown (CPU/RAM powered down). NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding, sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017, DataStore read/clamp/write in lockstep. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
uint8_t alarm_hour; // 0-23, local time
uint8_t alarm_min; // 0-59
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
// Repeat days as a bitmask, bit i = 1<<tm_wday (Sunday=0 .. Saturday=6, per
// struct tm — matches what computeAlarmNextFire() already reads from
// gmtime()). 0 = no repeat (one-shot, the original/default behaviour).
// Cycled through the presets below via the alarm's Repeat row.
static const uint8_t ALARM_REPEAT_NONE = 0x00; // one-shot (default)
static const uint8_t ALARM_REPEAT_DAILY = 0x7F; // every day
static const uint8_t ALARM_REPEAT_WEEKDAYS = 0x3E; // Mon-Fri
static const uint8_t ALARM_REPEAT_WEEKENDS = 0x41; // Sat+Sun
static const uint8_t ALARM_REPEAT_COUNT = 4;
static uint8_t alarmRepeatMaskForIdx(uint8_t idx) {
static const uint8_t M[ALARM_REPEAT_COUNT] =
{ ALARM_REPEAT_NONE, ALARM_REPEAT_DAILY, ALARM_REPEAT_WEEKDAYS, ALARM_REPEAT_WEEKENDS };
return M[idx < ALARM_REPEAT_COUNT ? idx : 0];
}
// Reverse lookup for display: an arbitrary mask (e.g. loaded from a future
// custom-day picker) that doesn't match a preset just reads as "Off" here —
// it still fires correctly, computeAlarmNextFire() reads the raw mask.
static uint8_t alarmRepeatIdxForMask(uint8_t mask) {
switch (mask) {
case ALARM_REPEAT_DAILY: return 1;
case ALARM_REPEAT_WEEKDAYS: return 2;
case ALARM_REPEAT_WEEKENDS: return 3;
default: return 0;
}
}
static const char* alarmRepeatLabel(uint8_t idx) {
static const char* L[ALARM_REPEAT_COUNT] = { "OFF", "Daily", "Weekdays", "Weekends" };
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
return L[idx < ALARM_REPEAT_COUNT ? idx : 0];
}
feat(companion): clock tools — alarm, countdown timer, stopwatch Add a Clock Tools screen (Enter on the home Clock page) with three time utilities, plus the engine that drives them from UITask::loop() so they work regardless of the current screen / display state: - Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump the clock) — small corrections still fire on time, a jump over the target still fires (late, up to 6 h). Disarms after firing. A bell icon signals an armed alarm on the clock face and the top status bar. - Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout, rings even when off-screen. - Stopwatch: millis-based, keeps running in the background. Numeric fields use the shared framework DigitEditor (digit-by-digit, min/max enforced). The alarm/timer/ring engine lives in UITask alongside the locator/live-share engines (no screen-cast for time-critical logic). E-ink: live readouts refresh coarsely (and on any key); timing is exact regardless, and the countdown's buzzer fires on time. Rings override mute and are dismissed by any key (auto-stop after 1 min). Cannot wake from a full Shutdown (CPU/RAM powered down). NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding, sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017, DataStore read/clamp/write in lockstep. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
// On-screen keyboard layout, shared across every text-entry screen (Settings >
// Keyboard). 0=ABC grid, alphabetical order (default), 1=T9 multi-tap
// (phone-keypad groups, cycled with repeated Enter presses — see KeyboardWidget.h).
uint8_t 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
// Additional (non-Latin) keyboard alphabet, orthogonal to keyboard_type above
// — either layout style (ABC grid or T9) can show any alphabet's characters.
// 0 = Latin only (default): the keyboard's existing #@/abc page-cycle key
// toggles between just the Latin letters page and the symbols page, as
// today. A non-zero value adds that alphabet's own page to the same cycle
// key (Latin → alphabet → symbols → Latin), so composing in it needs no new
// key, just Settings Keyboard Alphabet to pick which one is available.
// See KeyboardWidget.h for the per-alphabet grids (KB_CYRILLIC_CHARS etc.)
// and Lemon font (src/helpers/ui/LemonFont.h) for on-screen rendering —
// every alphabet here must be in Lemon's U+0020-04FF range.
feat(ui): on-device channel management, remote admin tool, per-language keyboards Messages: - Add/edit/delete channels on-device (new ChannelsView, owned by the renamed MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope). Channel secret entry supports a typed passphrase (SHA-256'd, same primitive the library already uses for the routing hash) or a raw 32-hex-char key. - MyMesh::setChannelLocal() factors out the setChannel/saveChannels/ onChannelRemoved sequence previously duplicated across the two CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths. Tools > Admin (new): - Log into a repeater/room server's admin account and send CLI commands, the on-device equivalent of the app's repeater-admin feature. - Commands are organised into category tabs (System/Radio/Routing/Actions) with common get/set fields (name, radio profile, tx power, repeat, advert intervals, ...) plus a free-text "Custom command..." fallback for anything else. A field row fetches the current value, opens it pre-filled for editing, and sends the change — falling back to a blank editor if the fetch fails or times out. - The admin password persists and self-heals exactly like room logins in Messages: saved on a confirmed admin-level login, forgotten on a failed one, left alone if merely under-privileged. - New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so a reply reaches the UI without touching the existing BLE/app CLI-terminal path (queueMessage's should_display gate is untouched). Shared TabBar.h extracted from NearbyScreen/BotScreen's independently duplicated tab-carousel rendering (now a third consumer via Admin) — also fixes neighbouring tabs vanishing outright when they didn't fully fit; they now truncate with an ellipsis instead. Keyboard: the combined "Ext.Latin" alphabet split into 8 separate, linguistically complete per-language keyboards (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug where tall accented glyphs overlapped the keyboard's separator line (SH1106's Lemon-font ascent constant was 2-3px short for them). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
//
// The Latin-diacritic entries (Polish..Nordic) replace what used to be a
// single combined "Ext.Latin" curated subset — each is now its own full,
// linguistically-correct set of that language's non-ASCII letters instead
// of a shared partial one. Not a schema change: keyboard_alt_alphabet is
// still just an index into keyboardAlphabetLabel()/KeyboardWidget's tables,
// so widening KB_ALPHABET_COUNT needs no persisted-data migration — only
// note that an index of 3 now means Polish, not the old combined Ext.Latin.
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
static const uint8_t KB_ALPHABET_LATIN_ONLY = 0;
static const uint8_t KB_ALPHABET_CYRILLIC = 1;
static const uint8_t KB_ALPHABET_GREEK = 2;
feat(ui): on-device channel management, remote admin tool, per-language keyboards Messages: - Add/edit/delete channels on-device (new ChannelsView, owned by the renamed MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope). Channel secret entry supports a typed passphrase (SHA-256'd, same primitive the library already uses for the routing hash) or a raw 32-hex-char key. - MyMesh::setChannelLocal() factors out the setChannel/saveChannels/ onChannelRemoved sequence previously duplicated across the two CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths. Tools > Admin (new): - Log into a repeater/room server's admin account and send CLI commands, the on-device equivalent of the app's repeater-admin feature. - Commands are organised into category tabs (System/Radio/Routing/Actions) with common get/set fields (name, radio profile, tx power, repeat, advert intervals, ...) plus a free-text "Custom command..." fallback for anything else. A field row fetches the current value, opens it pre-filled for editing, and sends the change — falling back to a blank editor if the fetch fails or times out. - The admin password persists and self-heals exactly like room logins in Messages: saved on a confirmed admin-level login, forgotten on a failed one, left alone if merely under-privileged. - New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so a reply reaches the UI without touching the existing BLE/app CLI-terminal path (queueMessage's should_display gate is untouched). Shared TabBar.h extracted from NearbyScreen/BotScreen's independently duplicated tab-carousel rendering (now a third consumer via Admin) — also fixes neighbouring tabs vanishing outright when they didn't fully fit; they now truncate with an ellipsis instead. Keyboard: the combined "Ext.Latin" alphabet split into 8 separate, linguistically complete per-language keyboards (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug where tall accented glyphs overlapped the keyboard's separator line (SH1106's Lemon-font ascent constant was 2-3px short for them). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
static const uint8_t KB_ALPHABET_POLISH = 3; // ą ć ę ł ń ó ś ź ż
static const uint8_t KB_ALPHABET_CZECH = 4; // á č ď é ě í ň ó ř š ť ú ů ý ž
static const uint8_t KB_ALPHABET_SLOVAK = 5; // á ä č ď é í ĺ ľ ň ó ô ŕ š ť ú ý ž
static const uint8_t KB_ALPHABET_GERMAN = 6; // ä ö ü ß
static const uint8_t KB_ALPHABET_FRENCH = 7; // à â ç é è ê ë î ï ô ù û ü ÿ œ
static const uint8_t KB_ALPHABET_SPANISH = 8; // á é í ñ ó ú ü
static const uint8_t KB_ALPHABET_PORTUGUESE = 9; // á à â ã ç é ê í ó ô õ ú
static const uint8_t KB_ALPHABET_NORDIC = 10; // å ä æ ö ø (Danish/Norwegian/Swedish share this set)
static const uint8_t KB_ALPHABET_COUNT = 11;
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
static const char* keyboardAlphabetLabel(uint8_t idx) {
feat(ui): on-device channel management, remote admin tool, per-language keyboards Messages: - Add/edit/delete channels on-device (new ChannelsView, owned by the renamed MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope). Channel secret entry supports a typed passphrase (SHA-256'd, same primitive the library already uses for the routing hash) or a raw 32-hex-char key. - MyMesh::setChannelLocal() factors out the setChannel/saveChannels/ onChannelRemoved sequence previously duplicated across the two CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths. Tools > Admin (new): - Log into a repeater/room server's admin account and send CLI commands, the on-device equivalent of the app's repeater-admin feature. - Commands are organised into category tabs (System/Radio/Routing/Actions) with common get/set fields (name, radio profile, tx power, repeat, advert intervals, ...) plus a free-text "Custom command..." fallback for anything else. A field row fetches the current value, opens it pre-filled for editing, and sends the change — falling back to a blank editor if the fetch fails or times out. - The admin password persists and self-heals exactly like room logins in Messages: saved on a confirmed admin-level login, forgotten on a failed one, left alone if merely under-privileged. - New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so a reply reaches the UI without touching the existing BLE/app CLI-terminal path (queueMessage's should_display gate is untouched). Shared TabBar.h extracted from NearbyScreen/BotScreen's independently duplicated tab-carousel rendering (now a third consumer via Admin) — also fixes neighbouring tabs vanishing outright when they didn't fully fit; they now truncate with an ellipsis instead. Keyboard: the combined "Ext.Latin" alphabet split into 8 separate, linguistically complete per-language keyboards (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug where tall accented glyphs overlapped the keyboard's separator line (SH1106's Lemon-font ascent constant was 2-3px short for them). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
static const char* L[KB_ALPHABET_COUNT] = { "Latin", "Cyrillic", "Greek", "Polish", "Czech",
"Slovak", "German", "French", "Spanish", "Portuguese", "Nordic" };
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
return L[idx < KB_ALPHABET_COUNT ? idx : 0];
}
// Auto-save the live GPS trail to /trail on shutdown (covers the low-battery
// auto-shutdown, which otherwise loses the whole route). Only writes when the
// trail has points and the toggle is on. 0 = off (default), 1 = on.
// [Tools Trail Settings Auto-save]
uint8_t trail_autosave_lowbatt;
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
// Appended at the tail (see the alarm doc comment above for the field itself
// and the serialization tripwire below for why new fields land here, not
// inline with alarm_on/hour/min).
uint8_t alarm_repeat_mask;
// Appended at the tail (see the keyboard_alt_alphabet doc comment above).
uint8_t keyboard_alt_alphabet;
feat(bot): room-server target, tab-carousel UI, per-target independence Auto-Reply Bot gains a third target (room servers, alongside DM and channel), new {name}/{hops} reply placeholders, and a DM all/favourites allow-list. BotScreen is redesigned as a circular tab carousel (Direct / Channel / Room / Other, same interaction as Nearby Nodes' filter tabs) instead of one long scrolling list, which also exposed and fixed a leftover coupling where Channel/Room trigger-replies and their !command handling secretly depended on the DM tab's Enable/Commands toggles — each target's Enable and Commands are now fully independent. - NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope, bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged at 2712 (new bytes absorbed existing padding, verified via a standalone host compile + offsetof check). - DataStore: persists all new fields; seeds bot_commands_ch/room from the old shared bot_commands_enabled on upgrade so existing channel/room command behaviour isn't silently lost. - MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's shape but post via sendMessage (room relays to members itself); requires an existing login session with that room, same as a manual post would. botDmSenderAllowed() gates DM trigger-reply/commands on the favourites bit when bot_dm_scope=Fav. - MsgExpand: {name}/{hops} as optional trailing params (default nullptr/-1, no existing caller affected) — deliberately not exposed on the general compose keyboard, only on bot Reply fields. - QuickMsgScreen/UITask: room-target picker (mirrors the channel picker), routing through the existing room-login prompt when there's no saved password yet. - BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves rows, Enter is now the only way to change a value); Enable split out of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode; Commands moved from a shared toggle into each tab. - docs/tools_screen.md updated for the new tab layout and behaviour. Not build-verified — no PlatformIO toolchain in this environment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
// Bot DM allow-list: who is allowed to trigger a DM auto-reply or run a
// command. 0 = all (default, matches the original bot_enabled behaviour).
// 1 = favourites only — gate on ContactInfo::flags bit 0, the same
// "favourite"/starred bit the Messages screen's DM list filter (dm_show_all)
// already reads, so no separate allow-list storage is needed.
uint8_t bot_dm_scope;
// Room-server auto-reply bot — same trigger/reply/command shape as the DM
// and channel bots above, but targets a single room server (like the
// channel bot targets a single channel) since posting requires that room's
// own login session (see MyMesh::sendRoomLogin/logoutRoom). If the device
// has never logged into this room, replies silently fail to send — same
// as a manual post would — so log in at least once from Messages > Rooms
// before relying on the bot there.
uint8_t bot_room_enabled; // 0=disabled, 1=room bot active for bot_room_prefix
uint8_t bot_room_prefix[6]; // target room's pubkey prefix [del→onContactRemoved]
char bot_trigger_room[64]; // room trigger phrase (independent of DM/channel; "*" = any post)
char bot_reply_room[140]; // auto-reply text for the room
// Per-target Commands toggle for channel/room, splitting what used to be
// one bot_commands_enabled shared across all three (see that field's doc
// comment) — e.g. answer !ping in DM but stay quiet on a public channel.
// Upgraders: DataStore seeds both from the old shared bot_commands_enabled
// on first load past the schema bump, so existing behaviour is preserved
// until the user deliberately splits them apart.
uint8_t bot_commands_ch;
uint8_t bot_commands_room;
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
// Single source of truth for the live-share option tables (shared by the Map
// UI labels and the auto-send engine in UITask).
static const uint8_t LOC_SHARE_MOVE_COUNT = 4;
static uint16_t locShareMoveMeters(uint8_t idx) {
static const uint16_t M[LOC_SHARE_MOVE_COUNT] = { 50, 100, 250, 500 };
return M[idx < LOC_SHARE_MOVE_COUNT ? idx : 1];
}
static const uint8_t LOC_SHARE_INTERVAL_COUNT = 4;
static uint16_t locShareIntervalSecs(uint8_t idx) {
static const uint16_t S[LOC_SHARE_INTERVAL_COUNT] = { 30, 60, 120, 300 };
return S[idx < LOC_SHARE_INTERVAL_COUNT ? idx : 1];
}
static const uint8_t LOC_SHARE_HEARTBEAT_COUNT = 3;
static uint16_t locShareHeartbeatSecs(uint8_t idx) {
static const uint16_t H[LOC_SHARE_HEARTBEAT_COUNT] = { 0, 300, 900 }; // off / 5 min / 15 min
return H[idx < LOC_SHARE_HEARTBEAT_COUNT ? idx : 0];
}
// Locator option tables (shared by the Tools Locator UI labels and the
// evaluation engine in UITask).
static const uint8_t LOCATOR_RADIUS_COUNT = 5;
static uint16_t locatorRadiusMeters(uint8_t idx) {
static const uint16_t R[LOCATOR_RADIUS_COUNT] = { 50, 100, 250, 500, 1000 };
return R[idx < LOCATOR_RADIUS_COUNT ? idx : 1];
}
static const uint8_t LOCATOR_MODE_COUNT = 3; // 0=arrive, 1=leave, 2=both
static const char* locatorModeLabel(uint8_t m) {
static const char* L[LOCATOR_MODE_COUNT] = { "Arrive", "Leave", "Both" };
return L[m < LOCATOR_MODE_COUNT ? m : 0];
}
// GPS-averaging durations for waypoint marking (seconds). 0 = off (instant).
static const uint8_t GPS_AVG_COUNT = 4;
static uint16_t gpsAvgSecs(uint8_t idx) {
static const uint16_t S[GPS_AVG_COUNT] = { 0, 5, 10, 30 };
return S[idx < GPS_AVG_COUNT ? idx : 0];
}
static const char* gpsAvgLabel(uint8_t idx) {
static const char* L[GPS_AVG_COUNT] = { "Off", "5s", "10s", "30s" };
return L[idx < GPS_AVG_COUNT ? idx : 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
// Trail auto-pause delays (seconds). 0 = off.
static const uint8_t TRAIL_AUTOPAUSE_COUNT = 4;
static uint16_t trailAutoPauseSecs(uint8_t idx) {
static const uint16_t S[TRAIL_AUTOPAUSE_COUNT] = { 0, 60, 120, 300 }; // off / 1 / 2 / 5 min
return S[idx < TRAIL_AUTOPAUSE_COUNT ? idx : 0];
}
static const char* trailAutoPauseLabel(uint8_t idx) {
static const char* L[TRAIL_AUTOPAUSE_COUNT] = { "Off", "1m", "2m", "5m" };
return L[idx < TRAIL_AUTOPAUSE_COUNT ? idx : 0];
}
// Movement under this many metres counts as "stationary" for auto-pause.
// Deliberately coarser than the trail min-delta gate (and independent of it)
// so GPS jitter while parked doesn't keep resetting the idle timer. Engine
// tuning only — not persisted.
static const uint16_t TRAIL_AUTOPAUSE_MOVE_M = 15;
// Tail sentinel written at the end of /new_prefs. Bump the low byte when
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
// older saves are detected on load and skipped (zero-init defaults kept).
// High 24 bits identify the file format; low byte is the schema revision.
feat(bot): room-server target, tab-carousel UI, per-target independence Auto-Reply Bot gains a third target (room servers, alongside DM and channel), new {name}/{hops} reply placeholders, and a DM all/favourites allow-list. BotScreen is redesigned as a circular tab carousel (Direct / Channel / Room / Other, same interaction as Nearby Nodes' filter tabs) instead of one long scrolling list, which also exposed and fixed a leftover coupling where Channel/Room trigger-replies and their !command handling secretly depended on the DM tab's Enable/Commands toggles — each target's Enable and Commands are now fully independent. - NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope, bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged at 2712 (new bytes absorbed existing padding, verified via a standalone host compile + offsetof check). - DataStore: persists all new fields; seeds bot_commands_ch/room from the old shared bot_commands_enabled on upgrade so existing channel/room command behaviour isn't silently lost. - MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's shape but post via sendMessage (room relays to members itself); requires an existing login session with that room, same as a manual post would. botDmSenderAllowed() gates DM trigger-reply/commands on the favourites bit when bot_dm_scope=Fav. - MsgExpand: {name}/{hops} as optional trailing params (default nullptr/-1, no existing caller affected) — deliberately not exposed on the general compose keyboard, only on bot Reply fields. - QuickMsgScreen/UITask: room-target picker (mirrors the channel picker), routing through the existing room-login prompt when there's no saved password yet. - BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves rows, Enter is now the only way to change a value); Enable split out of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode; Commands moved from a shared toggle into each tab. - docs/tools_screen.md updated for the new tab layout and behaviour. Not build-verified — no PlatformIO toolchain in this environment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
static const uint32_t SCHEMA_SENTINEL = 0xC0DE001F;
// Bit-index for each home page. Used by page_order (entries store bit+1) and
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
// and SettingsScreen::homePageBitIndex route their per-enum lookups through these.
enum HomePageBit : uint8_t {
feat(ui): Favourites home page (phase 1: storage + read-only grid) New home page rendering 6 pinned-contact slots. Grid transposes to orientation: landscape → 3×2, portrait → 2×3. Joystick claims UP/DOWN and inner LEFT/RIGHT for grid nav; edge LEFT/RIGHT fall through to page navigation. Selected tile inverts via drawSelectionRow, filled tile shows contact name + unread badge, empty tile shows "+". Storage: - NodePrefs::favourite_contacts[6][6] (6-byte pub_key prefix per slot, all-zero = empty; collision probability with a real key is 2^-48) - HomePageBit gains HPB_FAVOURITES = 11 (HPB_COUNT = 12), with a new HP_FAVOURITES mask bit so the page is toggleable in Settings - New PAGE_ORDER_LEN constant decouples the persisted array length (11) from the page-type count; loops over page_order now use it, value-range checks still use HPB_COUNT - homePageBit returns 0 for HPB_SETTINGS / HPB_QUICK_MSG explicitly rather than by bit-index range, so HPB_FAVOURITES (bit 11) is correctly treated as toggleable - Default order inserts FAVOURITES after CLOCK; SHUTDOWN drops to the fallback "missing pages" append (still reachable, lands at end) Schema sentinel bumped to 0xC0DE0002. DataStore::loadPrefsInt and savePrefs read/write the new field. Older saves leave the field zero-initialised (all slots empty), which is the natural starting state. Phase 2 (Pin from Contact options) and Phase 3 (Pin from empty-slot mini picker) wire interactions; this commit handles render + nav only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:00:27 +02:00
HPB_CLOCK = 0,
HPB_RECENT = 1,
HPB_RADIO = 2,
HPB_BLUETOOTH = 3,
HPB_ADVERT = 4,
HPB_GPS = 5,
HPB_SENSORS = 6,
HPB_TOOLS = 7,
HPB_SHUTDOWN = 8,
HPB_SETTINGS = 9,
HPB_QUICK_MSG = 10,
HPB_FAVOURITES = 11,
HPB_MAP = 12,
HPB_COUNT = 13,
};
// Number of usable page_order[] slots — one per home page (== HPB_COUNT), so
// every page can be reordered. Any page still missing from a saved order is
// appended at navigation time via buildVisibleOrder's fallback.
static const uint8_t PAGE_ORDER_LEN = 13;
// Bytes of page_order persisted at the original file offset. Slots beyond this
// (PAGE_ORDER_LEN - PAGE_ORDER_LEN_V1) are stored at the file tail, so a
// pre-0x0019 save — whose order was exactly this long — loads without shifting
// every field written after page_order.
static const uint8_t PAGE_ORDER_LEN_V1 = 11;
feat(ui): Favourites home page (phase 1: storage + read-only grid) New home page rendering 6 pinned-contact slots. Grid transposes to orientation: landscape → 3×2, portrait → 2×3. Joystick claims UP/DOWN and inner LEFT/RIGHT for grid nav; edge LEFT/RIGHT fall through to page navigation. Selected tile inverts via drawSelectionRow, filled tile shows contact name + unread badge, empty tile shows "+". Storage: - NodePrefs::favourite_contacts[6][6] (6-byte pub_key prefix per slot, all-zero = empty; collision probability with a real key is 2^-48) - HomePageBit gains HPB_FAVOURITES = 11 (HPB_COUNT = 12), with a new HP_FAVOURITES mask bit so the page is toggleable in Settings - New PAGE_ORDER_LEN constant decouples the persisted array length (11) from the page-type count; loops over page_order now use it, value-range checks still use HPB_COUNT - homePageBit returns 0 for HPB_SETTINGS / HPB_QUICK_MSG explicitly rather than by bit-index range, so HPB_FAVOURITES (bit 11) is correctly treated as toggleable - Default order inserts FAVOURITES after CLOCK; SHUTDOWN drops to the fallback "missing pages" append (still reachable, lands at end) Schema sentinel bumped to 0xC0DE0002. DataStore::loadPrefsInt and savePrefs read/write the new field. Older saves leave the field zero-initialised (all slots empty), which is the natural starting state. Phase 2 (Pin from Contact options) and Phase 3 (Pin from empty-slot mini picker) wire interactions; this commit handles render + nav only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:00:27 +02:00
// Bitmasks for home_pages_mask (bit=1 → page visible; 0 field = all visible).
// SETTINGS and QUICK_MSG have no mask bit — they're always visible.
feat(ui): Favourites home page (phase 1: storage + read-only grid) New home page rendering 6 pinned-contact slots. Grid transposes to orientation: landscape → 3×2, portrait → 2×3. Joystick claims UP/DOWN and inner LEFT/RIGHT for grid nav; edge LEFT/RIGHT fall through to page navigation. Selected tile inverts via drawSelectionRow, filled tile shows contact name + unread badge, empty tile shows "+". Storage: - NodePrefs::favourite_contacts[6][6] (6-byte pub_key prefix per slot, all-zero = empty; collision probability with a real key is 2^-48) - HomePageBit gains HPB_FAVOURITES = 11 (HPB_COUNT = 12), with a new HP_FAVOURITES mask bit so the page is toggleable in Settings - New PAGE_ORDER_LEN constant decouples the persisted array length (11) from the page-type count; loops over page_order now use it, value-range checks still use HPB_COUNT - homePageBit returns 0 for HPB_SETTINGS / HPB_QUICK_MSG explicitly rather than by bit-index range, so HPB_FAVOURITES (bit 11) is correctly treated as toggleable - Default order inserts FAVOURITES after CLOCK; SHUTDOWN drops to the fallback "missing pages" append (still reachable, lands at end) Schema sentinel bumped to 0xC0DE0002. DataStore::loadPrefsInt and savePrefs read/write the new field. Older saves leave the field zero-initialised (all slots empty), which is the natural starting state. Phase 2 (Pin from Contact options) and Phase 3 (Pin from empty-slot mini picker) wire interactions; this commit handles render + nav only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:00:27 +02:00
static const uint16_t HP_CLOCK = 1 << HPB_CLOCK;
static const uint16_t HP_RECENT = 1 << HPB_RECENT;
static const uint16_t HP_RADIO = 1 << HPB_RADIO;
static const uint16_t HP_BLUETOOTH = 1 << HPB_BLUETOOTH;
static const uint16_t HP_ADVERT = 1 << HPB_ADVERT;
static const uint16_t HP_GPS = 1 << HPB_GPS;
static const uint16_t HP_SENSORS = 1 << HPB_SENSORS;
static const uint16_t HP_TOOLS = 1 << HPB_TOOLS;
static const uint16_t HP_SHUTDOWN = 1 << HPB_SHUTDOWN;
static const uint16_t HP_FAVOURITES = 1 << HPB_FAVOURITES;
static const uint16_t HP_MAP = 1 << HPB_MAP;
static const uint16_t HP_ALL = 0x01FF | HP_FAVOURITES | HP_MAP;
// Factory-default carousel — the everyday pages only, so a fresh device isn't
// 13 pages to joystick through. Messages + Settings are always visible (no
// mask bit), so the mask covers: Clock, Tools, Shutdown, Favourites, Map.
// Recent / Radio / Bluetooth / Advert / GPS / Sensors are opt-in via
// Settings Home Pages. Existing users keep their saved mask (loaded from
// /new_prefs); this only seeds brand-new / factory-reset devices.
static const uint16_t HP_DEFAULT = HP_CLOCK | HP_TOOLS | HP_SHUTDOWN | HP_FAVOURITES | HP_MAP;
// Label for home page by bit-index; returns "" for out-of-range.
// Array indices match HomePageBit values.
static const char* homePageLabel(uint8_t bit) {
static const char* labels[HPB_COUNT] = {
"Clock", "Recent", "Radio", "Bluetooth", "Advert",
"GPS", "Sensors", "Tools", "Shutdown", "Settings", "Messages", "Favourites", "Map"
};
return (bit < HPB_COUNT) ? labels[bit] : "";
}
static void buildRTTTLString(const uint8_t* notes, uint8_t len, uint8_t bpm_idx,
char* buf, int size) {
static const uint16_t BPM_OPTS[] = { 60, 90, 120, 150, 180 };
static const uint8_t DUR_VALS[] = { 4, 8, 16, 32 };
static const char PITCHES[] = { 'p', 'c', 'd', 'e', 'f', 'g', 'a', 'b' };
if (len == 0) { buf[0] = '\0'; return; }
uint16_t bpm = BPM_OPTS[bpm_idx < 5 ? bpm_idx : 2];
int pos = snprintf(buf, size, "Ring:d=8,o=5,b=%u:", bpm);
for (int i = 0; i < len && pos < size - 8; i++) {
if (i > 0 && pos < size - 1) buf[pos++] = ',';
uint8_t pitch = notes[i] & 0x07;
uint8_t octave = ((notes[i] >> 3) & 0x03) + 4;
uint8_t dur_val = DUR_VALS[(notes[i] >> 5) & 0x03];
if (pitch == 0) pos += snprintf(buf + pos, size - pos, "%dp", dur_val);
else pos += snprintf(buf + pos, size - pos, "%d%c%d", dur_val, PITCHES[pitch], octave);
}
if (pos < size) buf[pos] = '\0';
}
2026-06-04 08:46:15 +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
// ── Serialization tripwire ───────────────────────────────────────────────────
// NodePrefs is written/read field-by-field, in order, by DataStore::savePrefs()
// and loadPrefsInt(); the on-disk format IS the struct's field layout. There is
// no automatic check that those two hand-written sequences match the struct, so
// a forgotten read/write silently misaligns EVERY field after it.
//
// This assert is the manual checkpoint. Changing a data member changes sizeof
// and trips it. When it trips, do ALL of the following, then update the number:
// 1. add the field's rd(...) in DataStore::loadPrefsInt(), in struct order
// 2. add the field's write(...) in DataStore::savePrefs(), in struct order
// 3. clamp it on load (an upgrader's file lacks it → stray bytes)
// 4. bump SCHEMA_SENTINEL's low byte
// (Padding can also shift sizeof; a "false" trip just means re-check + rebump.)
feat(bot): room-server target, tab-carousel UI, per-target independence Auto-Reply Bot gains a third target (room servers, alongside DM and channel), new {name}/{hops} reply placeholders, and a DM all/favourites allow-list. BotScreen is redesigned as a circular tab carousel (Direct / Channel / Room / Other, same interaction as Nearby Nodes' filter tabs) instead of one long scrolling list, which also exposed and fixed a leftover coupling where Channel/Room trigger-replies and their !command handling secretly depended on the DM tab's Enable/Commands toggles — each target's Enable and Commands are now fully independent. - NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope, bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged at 2712 (new bytes absorbed existing padding, verified via a standalone host compile + offsetof check). - DataStore: persists all new fields; seeds bot_commands_ch/room from the old shared bot_commands_enabled on upgrade so existing channel/room command behaviour isn't silently lost. - MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's shape but post via sendMessage (room relays to members itself); requires an existing login session with that room, same as a manual post would. botDmSenderAllowed() gates DM trigger-reply/commands on the favourites bit when bot_dm_scope=Fav. - MsgExpand: {name}/{hops} as optional trailing params (default nullptr/-1, no existing caller affected) — deliberately not exposed on the general compose keyboard, only on bot Reply fields. - QuickMsgScreen/UITask: room-target picker (mirrors the channel picker), routing through the existing room-login prompt when there's no saved password yet. - BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves rows, Enter is now the only way to change a value); Enable split out of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode; Commands moved from a shared toggle into each tab. - docs/tools_screen.md updated for the new tab layout and behaviour. Not build-verified — no PlatformIO toolchain in this environment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
static_assert(sizeof(NodePrefs) == 2712,
"NodePrefs layout changed — sync DataStore save/load + clamp, bump "
"SCHEMA_SENTINEL, then update this size (see steps above).");
// Bounds for a usable repeater radio profile. The frequency range is passed in
// by the caller from RadioLibWrapper::getFreqBounds() — the radio chip's own
// validated range — so a profile that passes here is guaranteed to actually take
// effect on the radio, instead of a hard-coded cap (was 150-960, the SX1262
// range) that would wrongly reject legal frequencies on any other chip. SF/BW/CR
// are the chip-independent discrete LoRa sets.
static inline bool isValidRepeaterProfile(float freq, float bw, uint8_t sf, uint8_t cr,
float min_freq, float max_freq) {
return freq >= min_freq && freq <= max_freq
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
&& sf >= 5 && sf <= 12
&& cr >= 5 && cr <= 8
&& bw >= 7.0f && bw <= 510.0f;
}
// Seed a never-configured repeater profile in the same band as the companion's
// own network (see defaultRepeaterFreqForBand() above for why).
static inline void seedDefaultRepeaterProfile(NodePrefs& prefs) {
prefs.repeater_use_profile = 1;
prefs.repeater_freq = defaultRepeaterFreqForBand(prefs.freq);
prefs.repeater_bw = LORA_BW;
prefs.repeater_sf = LORA_SF;
prefs.repeater_cr = LORA_CR;
}