2026-05-17 00:12:40 +02:00
|
|
|
|
#pragma once
|
|
|
|
|
|
// Custom screen — not part of upstream UITask.cpp
|
|
|
|
|
|
// Included by UITask.cpp after SensorPlaceholders.h is defined.
|
|
|
|
|
|
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#include "../Features.h"
|
2026-06-20 09:07:00 +02:00
|
|
|
|
#include "../RadioPresets.h"
|
2026-06-21 23:09:35 +02:00
|
|
|
|
#include "RadioParamsEditor.h"
|
2026-06-21 22:32:47 +02:00
|
|
|
|
#include "RadioPresetPicker.h"
|
2026-06-22 18:13:33 +02:00
|
|
|
|
#include "AccordionList.h"
|
2026-05-24 22:20:42 +02:00
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
class SettingsScreen : public UIScreen {
|
|
|
|
|
|
UITask* _task;
|
|
|
|
|
|
|
|
|
|
|
|
enum SettingItem {
|
|
|
|
|
|
// Display section
|
|
|
|
|
|
SECTION_DISPLAY,
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_BRIGHTNESS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
BRIGHTNESS,
|
2026-05-22 18:34:20 +02:00
|
|
|
|
#endif
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
AUTO_OFF,
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-17 09:55:51 +02:00
|
|
|
|
AUTO_LOCK,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
BATT_DISPLAY,
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_CLOCK_SECONDS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
CLOCK_SECONDS,
|
2026-05-22 10:47:19 +02:00
|
|
|
|
#endif
|
2026-05-21 21:04:43 +02:00
|
|
|
|
CLOCK_FORMAT,
|
2026-05-20 09:29:38 +02:00
|
|
|
|
FONT,
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_DISPLAY_ROTATION_SETTING
|
2026-05-20 21:53:40 +02:00
|
|
|
|
ROTATION,
|
2026-05-23 15:20:40 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_JOYSTICK_ROTATION_SETTING
|
2026-05-23 15:20:40 +02:00
|
|
|
|
JOY_ROTATION,
|
2026-05-24 10:22:03 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_FULL_REFRESH_SETTING
|
2026-05-24 10:22:03 +02:00
|
|
|
|
EINK_FULL_REFRESH,
|
2026-05-20 21:53:40 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// Sound section
|
|
|
|
|
|
SECTION_SOUND,
|
|
|
|
|
|
BUZZER,
|
|
|
|
|
|
BUZZER_VOLUME,
|
|
|
|
|
|
DM_MELODY,
|
|
|
|
|
|
CH_MELODY,
|
2026-06-04 08:46:15 +02:00
|
|
|
|
AD_SOUND,
|
2026-06-07 10:53:17 +02:00
|
|
|
|
AD_SOUND_SCOPE,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// Home pages section
|
|
|
|
|
|
SECTION_HOME_PAGES,
|
2026-05-24 23:00:27 +02:00
|
|
|
|
HOME_CLOCK, HOME_FAVOURITES, HOME_RECENT, HOME_RADIO, HOME_BT, HOME_ADVERT,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
HOME_GPS,
|
|
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
|
|
|
|
|
HOME_SENSORS,
|
|
|
|
|
|
#endif
|
2026-05-22 23:33:06 +02:00
|
|
|
|
HOME_SETTINGS, HOME_QUICK_MSG,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
HOME_TOOLS, HOME_SHUTDOWN,
|
|
|
|
|
|
// Radio section
|
|
|
|
|
|
SECTION_RADIO,
|
|
|
|
|
|
TX_POWER,
|
2026-06-20 09:07:00 +02:00
|
|
|
|
RADIO_PRESET,
|
|
|
|
|
|
CUSTOM_FREQ, CUSTOM_SF, CUSTOM_BW, CUSTOM_CR,
|
2026-06-10 23:30:43 +02:00
|
|
|
|
POWER_SAVE,
|
|
|
|
|
|
TX_APC,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// System section
|
|
|
|
|
|
SECTION_SYSTEM,
|
|
|
|
|
|
TIMEZONE,
|
|
|
|
|
|
LOW_BAT,
|
2026-06-04 00:44:56 +02:00
|
|
|
|
UNITS,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// Contacts section
|
2026-05-27 17:33:52 +02:00
|
|
|
|
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
// Messages section
|
|
|
|
|
|
SECTION_MESSAGES,
|
2026-06-14 23:33:16 +02:00
|
|
|
|
DM_RESEND,
|
2026-05-17 00:12:40 +02:00
|
|
|
|
MSG_SLOT_0, MSG_SLOT_1, MSG_SLOT_2, MSG_SLOT_3, MSG_SLOT_4,
|
|
|
|
|
|
MSG_SLOT_5, MSG_SLOT_6, MSG_SLOT_7, MSG_SLOT_8, MSG_SLOT_9,
|
|
|
|
|
|
Count
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-22 18:13:33 +02:00
|
|
|
|
// Cursor + scroll, fold state and the flattened visible list are owned by the
|
|
|
|
|
|
// shared AccordionList helper. We keep only the section→SettingItem mapping it
|
|
|
|
|
|
// needs (sections are walked once from the enum, honouring the #if guards).
|
|
|
|
|
|
int _selected = 0; // SettingItem under the cursor, resolved per input/render
|
|
|
|
|
|
int _reserve = 0; // right-edge px reserved for the scrollbar (0 when list fits)
|
|
|
|
|
|
bool _dirty = false;
|
|
|
|
|
|
|
|
|
|
|
|
AccordionList _acc;
|
|
|
|
|
|
static const int NUM_SECTIONS = 7;
|
|
|
|
|
|
static const int MAX_PER_SEC = 16;
|
|
|
|
|
|
uint8_t _sec_items[NUM_SECTIONS][MAX_PER_SEC]; // SettingItem per (section, row)
|
|
|
|
|
|
uint8_t _sec_count[NUM_SECTIONS];
|
|
|
|
|
|
uint8_t _sec_header[NUM_SECTIONS]; // the SECTION_* enum for each section
|
|
|
|
|
|
int _num_sections = 0;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
static const uint16_t AUTO_OFF_OPTS[5];
|
|
|
|
|
|
static const char* AUTO_OFF_LABELS[5];
|
|
|
|
|
|
static const int AUTO_OFF_COUNT = 5;
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-25 10:48:03 +02:00
|
|
|
|
// GPS update interval tables are no longer surfaced in Settings — the sensor
|
|
|
|
|
|
// manager defaults to 1 s when nothing else sets it. Pref byte _prefs.gps_interval
|
|
|
|
|
|
// is retained for backwards compatibility.
|
2026-05-17 00:12:40 +02:00
|
|
|
|
static const uint16_t LOW_BAT_OPTS[7];
|
|
|
|
|
|
static const char* LOW_BAT_LABELS[7];
|
|
|
|
|
|
static const int LOW_BAT_COUNT = 7;
|
|
|
|
|
|
static const char* BATT_DISPLAY_LABELS[3];
|
|
|
|
|
|
static const int BATT_DISPLAY_COUNT = 3;
|
2026-06-07 09:13:02 +02:00
|
|
|
|
static const char* SOUND_LABELS[4];
|
|
|
|
|
|
static const int SOUND_COUNT = 4;
|
2026-06-07 10:53:17 +02:00
|
|
|
|
static const char* AD_SCOPE_LABELS[2];
|
|
|
|
|
|
static const int AD_SCOPE_COUNT = 2;
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_FULL_REFRESH_SETTING
|
2026-05-24 10:22:03 +02:00
|
|
|
|
static const char* EINK_FULL_REFRESH_LABELS[5];
|
|
|
|
|
|
static const int EINK_FULL_REFRESH_COUNT = 5;
|
|
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
|
|
int lowBatIndex() {
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
if (!p) return 0;
|
|
|
|
|
|
for (int i = 0; i < LOW_BAT_COUNT; i++)
|
|
|
|
|
|
if (LOW_BAT_OPTS[i] == p->low_batt_mv) return i;
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 22:32:47 +02:00
|
|
|
|
// The companion's own radio fields, as the shared preset picker's target
|
|
|
|
|
|
// (Tools › Repeater points the same picker at the dedicated repeater profile).
|
|
|
|
|
|
RadioPresetPicker::Target radioTarget(NodePrefs* p) const {
|
|
|
|
|
|
return { &p->freq, &p->bw, &p->sf, &p->cr };
|
2026-06-20 09:07:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 16:45:28 +02:00
|
|
|
|
// Value column start, pulled left by the scrollbar gutter so right-side
|
|
|
|
|
|
// values never render under the indicator when the list scrolls.
|
|
|
|
|
|
int valCol(DisplayDriver& display) const { return display.valCol() - _reserve; }
|
|
|
|
|
|
|
2026-06-15 21:23:00 +02:00
|
|
|
|
// Shared 0/90/180/270 labels for display + joystick rotation.
|
|
|
|
|
|
static const char* rotLabel(uint8_t r) {
|
|
|
|
|
|
static const char* const L[] = { "0 deg", "90 deg", "180 deg", "270 deg" };
|
|
|
|
|
|
return L[r & 3];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
void renderBar(DisplayDriver& display, int x, int y, int value, int max_val) {
|
2026-05-22 19:11:25 +02:00
|
|
|
|
const int gap = 2;
|
2026-06-15 14:41:46 +02:00
|
|
|
|
const int avail = display.width() - x - _reserve;
|
2026-05-22 19:11:25 +02:00
|
|
|
|
const int raw = (avail - (max_val - 1) * gap) / max_val;
|
|
|
|
|
|
const int cap = display.getLineHeight() - 2;
|
|
|
|
|
|
const int box_h = raw < cap ? (raw < 2 ? 2 : raw) : cap;
|
|
|
|
|
|
const int box_w = box_h;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
for (int i = 0; i < max_val; i++) {
|
|
|
|
|
|
int bx = x + i * (box_w + gap);
|
|
|
|
|
|
display.drawRect(bx, y, box_w, box_h);
|
|
|
|
|
|
if (i < value)
|
|
|
|
|
|
display.fillRect(bx + 1, y + 1, box_w - 2, box_h - 2);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
int autoOffIndex() {
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
if (!p) return 1;
|
|
|
|
|
|
for (int i = 0; i < AUTO_OFF_COUNT; i++)
|
|
|
|
|
|
if (AUTO_OFF_OPTS[i] == p->auto_off_secs) return i;
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool isSection(int item) const {
|
|
|
|
|
|
return item == SECTION_DISPLAY || item == SECTION_SOUND ||
|
|
|
|
|
|
item == SECTION_HOME_PAGES ||
|
|
|
|
|
|
item == SECTION_RADIO || item == SECTION_SYSTEM ||
|
2026-05-25 13:01:06 +02:00
|
|
|
|
item == SECTION_CONTACTS || item == SECTION_MESSAGES;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const char* sectionName(int item) const {
|
|
|
|
|
|
if (item == SECTION_DISPLAY) return "Display";
|
|
|
|
|
|
if (item == SECTION_SOUND) return "Sound";
|
|
|
|
|
|
if (item == SECTION_HOME_PAGES) return "Home Pages";
|
|
|
|
|
|
if (item == SECTION_RADIO) return "Radio";
|
|
|
|
|
|
if (item == SECTION_SYSTEM) return "System";
|
|
|
|
|
|
if (item == SECTION_CONTACTS) return "Contacts";
|
|
|
|
|
|
if (item == SECTION_MESSAGES) return "Messages";
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 18:13:33 +02:00
|
|
|
|
// Walk the SettingItem enum once, bucketing items under their section header.
|
|
|
|
|
|
// #if-guarded items need no special handling — they simply aren't in the enum.
|
|
|
|
|
|
void buildSections() {
|
|
|
|
|
|
int cur = -1;
|
2026-05-26 15:49:50 +02:00
|
|
|
|
for (int i = 0; i < (int)Count; i++) {
|
|
|
|
|
|
if (isSection(i)) {
|
2026-06-22 18:13:33 +02:00
|
|
|
|
if (++cur >= NUM_SECTIONS) break;
|
|
|
|
|
|
_sec_header[cur] = (uint8_t)i;
|
|
|
|
|
|
_sec_count[cur] = 0;
|
|
|
|
|
|
} else if (cur >= 0 && _sec_count[cur] < MAX_PER_SEC) {
|
|
|
|
|
|
_sec_items[cur][_sec_count[cur]++] = (uint8_t)i;
|
2026-05-26 15:49:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-22 18:13:33 +02:00
|
|
|
|
_num_sections = cur + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// (Re)load the section sizes into the accordion (folds all, resets the cursor).
|
|
|
|
|
|
void resetList() {
|
|
|
|
|
|
uint8_t sizes[NUM_SECTIONS];
|
|
|
|
|
|
for (int i = 0; i < _num_sections; i++) sizes[i] = _sec_count[i];
|
|
|
|
|
|
_acc.begin(sizes, _num_sections);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Resolve the accordion's selected row to a SettingItem (header → SECTION_*).
|
|
|
|
|
|
int currentItem() const {
|
|
|
|
|
|
const AccordionList::Row& r = _acc.selected();
|
|
|
|
|
|
return (r.item < 0) ? _sec_header[r.sec] : _sec_items[r.sec][r.item];
|
2026-05-26 15:49:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
bool isHomePage(int item) const {
|
2026-05-24 23:00:27 +02:00
|
|
|
|
return item == HOME_CLOCK || item == HOME_RECENT || item == HOME_RADIO ||
|
|
|
|
|
|
item == HOME_BT || item == HOME_ADVERT || item == HOME_TOOLS ||
|
|
|
|
|
|
item == HOME_SHUTDOWN || item == HOME_SETTINGS || item == HOME_QUICK_MSG ||
|
|
|
|
|
|
item == HOME_FAVOURITES
|
2026-05-17 00:12:40 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
|| item == HOME_GPS
|
|
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
|
|
|
|
|
|| item == HOME_SENSORS
|
|
|
|
|
|
#endif
|
|
|
|
|
|
;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uint16_t homePageBit(int item) const {
|
2026-05-23 20:03:45 +02:00
|
|
|
|
int bit = homePageBitIndex(item);
|
2026-05-24 23:00:27 +02:00
|
|
|
|
// SETTINGS and QUICK_MSG are always visible (no mask bit). All other pages
|
|
|
|
|
|
// — including FAVOURITES — toggle via home_pages_mask.
|
|
|
|
|
|
if (bit < 0 || bit == NodePrefs::HPB_SETTINGS || bit == NodePrefs::HPB_QUICK_MSG) return 0;
|
|
|
|
|
|
return (uint16_t)(1 << bit);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const char* homePageLabel(int item) const {
|
2026-05-23 20:03:45 +02:00
|
|
|
|
int bit = homePageBitIndex(item);
|
2026-05-24 22:26:56 +02:00
|
|
|
|
return NodePrefs::homePageLabel((uint8_t)(bit >= 0 ? bit : NodePrefs::HPB_COUNT));
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool homePageVisible(int item, const NodePrefs* p) const {
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if (item == HOME_SETTINGS || item == HOME_QUICK_MSG) return true;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
uint16_t bit = homePageBit(item);
|
|
|
|
|
|
if (!bit) return false;
|
2026-05-23 20:03:45 +02:00
|
|
|
|
uint16_t mask = (p && p->home_pages_mask) ? p->home_pages_mask : NodePrefs::HP_ALL;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
return (mask & bit) != 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:33:06 +02:00
|
|
|
|
bool homePageToggleable(int item) const {
|
|
|
|
|
|
return item != HOME_SETTINGS && item != HOME_QUICK_MSG;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 22:26:56 +02:00
|
|
|
|
// Returns the bit-index used in page_order for this SettingItem, or -1.
|
|
|
|
|
|
// Bit-index values are defined once in NodePrefs::HomePageBit.
|
2026-05-22 23:33:06 +02:00
|
|
|
|
int homePageBitIndex(int item) const {
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_CLOCK) return NodePrefs::HPB_CLOCK;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
if (item == HOME_FAVOURITES) return NodePrefs::HPB_FAVOURITES;
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_RECENT) return NodePrefs::HPB_RECENT;
|
|
|
|
|
|
if (item == HOME_RADIO) return NodePrefs::HPB_RADIO;
|
|
|
|
|
|
if (item == HOME_BT) return NodePrefs::HPB_BLUETOOTH;
|
|
|
|
|
|
if (item == HOME_ADVERT) return NodePrefs::HPB_ADVERT;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_GPS) return NodePrefs::HPB_GPS;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_SENSORS) return NodePrefs::HPB_SENSORS;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#endif
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (item == HOME_TOOLS) return NodePrefs::HPB_TOOLS;
|
|
|
|
|
|
if (item == HOME_SHUTDOWN) return NodePrefs::HPB_SHUTDOWN;
|
|
|
|
|
|
if (item == HOME_SETTINGS) return NodePrefs::HPB_SETTINGS;
|
|
|
|
|
|
if (item == HOME_QUICK_MSG) return NodePrefs::HPB_QUICK_MSG;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Returns 1-based position of item in page_order, or 0 if no custom order / not found.
|
|
|
|
|
|
int homePagePosition(int item, const NodePrefs* p) const {
|
2026-05-24 19:53:05 +02:00
|
|
|
|
if (!p || p->page_order_set != NodePrefs::PAGE_ORDER_MAGIC) return 0;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
int bit = homePageBitIndex(item);
|
|
|
|
|
|
if (bit < 0) return 0;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
for (int i = 0; i < NodePrefs::PAGE_ORDER_LEN; i++) {
|
2026-05-22 23:33:06 +02:00
|
|
|
|
uint8_t v = p->page_order[i];
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (v < 1 || v > NodePrefs::HPB_COUNT) break;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if ((int)(v - 1) == bit) return i + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Initialises page_order to the default display sequence if not already set.
|
2026-05-26 16:08:01 +02:00
|
|
|
|
// Also repairs a partially-initialised order where CLOCK is absent; migrates
|
|
|
|
|
|
// older orders by inserting FAVOURITES after CLOCK; and appends any pages that
|
|
|
|
|
|
// are absent from a stale saved order (e.g. TOOLS/MESSAGES added by later firmware).
|
2026-05-22 23:33:06 +02:00
|
|
|
|
void ensurePageOrderInit(NodePrefs* p) const {
|
|
|
|
|
|
if (!p) return;
|
2026-05-24 19:53:05 +02:00
|
|
|
|
if (p->page_order_set == NodePrefs::PAGE_ORDER_MAGIC) {
|
2026-05-24 23:17:48 +02:00
|
|
|
|
bool has_clock = false;
|
|
|
|
|
|
bool has_fav = false;
|
|
|
|
|
|
int len = 0;
|
|
|
|
|
|
int clock_at = -1;
|
|
|
|
|
|
for (int i = 0; i < NodePrefs::PAGE_ORDER_LEN; i++) {
|
2026-05-23 23:50:25 +02:00
|
|
|
|
uint8_t v = p->page_order[i];
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (v < 1 || v > NodePrefs::HPB_COUNT) break;
|
2026-05-24 23:17:48 +02:00
|
|
|
|
if ((int)(v - 1) == NodePrefs::HPB_CLOCK) { has_clock = true; clock_at = i; }
|
|
|
|
|
|
if ((int)(v - 1) == NodePrefs::HPB_FAVOURITES) { has_fav = true; }
|
|
|
|
|
|
len = i + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!has_clock) {
|
|
|
|
|
|
// Corrupted/partial — full re-init below.
|
|
|
|
|
|
memset(p->page_order, 0, sizeof(p->page_order));
|
|
|
|
|
|
} else {
|
2026-05-24 23:24:13 +02:00
|
|
|
|
if (!has_fav) {
|
|
|
|
|
|
// Insert FAVOURITES right after CLOCK. If the array is full, the last
|
|
|
|
|
|
// entry (typically SHUTDOWN, which still appears via the missing-page
|
|
|
|
|
|
// fallback at end of nav) is overwritten to make room.
|
2026-05-24 23:17:48 +02:00
|
|
|
|
int insert_at = clock_at + 1;
|
2026-05-24 23:24:13 +02:00
|
|
|
|
int tail = (len < NodePrefs::PAGE_ORDER_LEN) ? len : NodePrefs::PAGE_ORDER_LEN - 1;
|
|
|
|
|
|
for (int i = tail; i > insert_at; i--) p->page_order[i] = p->page_order[i - 1];
|
2026-05-24 23:17:48 +02:00
|
|
|
|
p->page_order[insert_at] = NodePrefs::HPB_FAVOURITES + 1;
|
|
|
|
|
|
}
|
2026-05-26 16:08:01 +02:00
|
|
|
|
// Append any pages that are absent from the saved order (e.g. added by a
|
|
|
|
|
|
// later firmware version). Recount first since the block above may have
|
|
|
|
|
|
// just inserted FAVOURITES.
|
|
|
|
|
|
{
|
|
|
|
|
|
uint16_t present = 0;
|
|
|
|
|
|
int cur_len = 0;
|
|
|
|
|
|
for (int i = 0; i < NodePrefs::PAGE_ORDER_LEN; i++) {
|
|
|
|
|
|
uint8_t v = p->page_order[i];
|
|
|
|
|
|
if (v < 1 || v > NodePrefs::HPB_COUNT) break;
|
|
|
|
|
|
present |= (uint16_t)(1u << (v - 1));
|
|
|
|
|
|
cur_len++;
|
|
|
|
|
|
}
|
|
|
|
|
|
static const uint8_t REQUIRED[] = {
|
|
|
|
|
|
NodePrefs::HPB_CLOCK, NodePrefs::HPB_FAVOURITES,
|
|
|
|
|
|
NodePrefs::HPB_RECENT, NodePrefs::HPB_RADIO,
|
|
|
|
|
|
NodePrefs::HPB_BLUETOOTH, NodePrefs::HPB_ADVERT,
|
|
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
NodePrefs::HPB_GPS,
|
|
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
|
|
|
|
|
NodePrefs::HPB_SENSORS,
|
|
|
|
|
|
#endif
|
|
|
|
|
|
NodePrefs::HPB_SETTINGS, NodePrefs::HPB_TOOLS, NodePrefs::HPB_QUICK_MSG,
|
|
|
|
|
|
};
|
2026-05-26 16:18:17 +02:00
|
|
|
|
// If any required page is missing, evict SHUTDOWN to make room — it is
|
|
|
|
|
|
// handled by buildVisibleOrder's fallback and need not be in the explicit
|
|
|
|
|
|
// list. This frees a slot regardless of whether the array is full or not,
|
|
|
|
|
|
// so multiple missing pages (e.g. TOOLS + QUICK_MSG) can all be appended.
|
|
|
|
|
|
bool any_missing = false;
|
|
|
|
|
|
for (int ri = 0; ri < (int)(sizeof(REQUIRED)/sizeof(REQUIRED[0])); ri++) {
|
|
|
|
|
|
if (!(present & (uint16_t)(1u << REQUIRED[ri]))) { any_missing = true; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (any_missing && (present & (uint16_t)(1u << NodePrefs::HPB_SHUTDOWN))) {
|
|
|
|
|
|
for (int i = 0; i < cur_len; i++) {
|
|
|
|
|
|
if (p->page_order[i] == NodePrefs::HPB_SHUTDOWN + 1) {
|
|
|
|
|
|
for (int j = i; j < cur_len - 1; j++) p->page_order[j] = p->page_order[j + 1];
|
|
|
|
|
|
p->page_order[--cur_len] = 0;
|
|
|
|
|
|
present &= ~(uint16_t)(1u << NodePrefs::HPB_SHUTDOWN);
|
|
|
|
|
|
break;
|
2026-05-26 16:13:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (int ri = 0; ri < (int)(sizeof(REQUIRED)/sizeof(REQUIRED[0])); ri++) {
|
|
|
|
|
|
uint8_t bit = REQUIRED[ri];
|
2026-05-26 16:08:01 +02:00
|
|
|
|
if (!(present & (uint16_t)(1u << bit)) && cur_len < NodePrefs::PAGE_ORDER_LEN)
|
|
|
|
|
|
p->page_order[cur_len++] = bit + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-24 23:17:48 +02:00
|
|
|
|
return;
|
2026-05-23 23:50:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-24 23:00:27 +02:00
|
|
|
|
// Default: CLOCK FAVOURITES RECENT RADIO BT ADVERT [GPS] [SENSORS] SETTINGS TOOLS MESSAGES
|
|
|
|
|
|
// SHUTDOWN is omitted from the explicit list (PAGE_ORDER_LEN = 11 leaves room for the
|
|
|
|
|
|
// common case GPS+SENSORS+all-others) and appended by buildVisibleOrder's missing-page
|
|
|
|
|
|
// fallback at the end of the navigation sequence.
|
2026-05-22 23:33:06 +02:00
|
|
|
|
int j = 0;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_CLOCK + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_FAVOURITES + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_RECENT + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_RADIO + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_BLUETOOTH + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_ADVERT + 1;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
2026-05-24 23:00:27 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_GPS + 1;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
2026-05-24 23:00:27 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_SENSORS + 1;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#endif
|
2026-05-24 23:00:27 +02:00
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_SETTINGS + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_TOOLS + 1;
|
|
|
|
|
|
p->page_order[j++] = NodePrefs::HPB_QUICK_MSG + 1;
|
|
|
|
|
|
while (j < NodePrefs::PAGE_ORDER_LEN) p->page_order[j++] = 0;
|
2026-05-24 19:53:05 +02:00
|
|
|
|
p->page_order_set = NodePrefs::PAGE_ORDER_MAGIC;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Swaps item's page_order slot with its neighbour in the given direction (-1=earlier, +1=later).
|
|
|
|
|
|
void movePageInOrder(int item, int delta, NodePrefs* p) {
|
|
|
|
|
|
ensurePageOrderInit(p);
|
|
|
|
|
|
int bit = homePageBitIndex(item);
|
|
|
|
|
|
if (bit < 0) return;
|
|
|
|
|
|
int cur = -1, total = 0;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
for (int i = 0; i < NodePrefs::PAGE_ORDER_LEN; i++) {
|
2026-05-22 23:33:06 +02:00
|
|
|
|
uint8_t v = p->page_order[i];
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (v < 1 || v > NodePrefs::HPB_COUNT) break;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if ((int)(v - 1) == bit) cur = i;
|
|
|
|
|
|
total++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cur < 0) return;
|
|
|
|
|
|
int next = cur + delta;
|
|
|
|
|
|
if (next < 0 || next >= total) return;
|
|
|
|
|
|
uint8_t tmp = p->page_order[cur];
|
|
|
|
|
|
p->page_order[cur] = p->page_order[next];
|
|
|
|
|
|
p->page_order[next] = tmp;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
bool isMsgSlot(int item) const {
|
|
|
|
|
|
return item >= MSG_SLOT_0 && item <= MSG_SLOT_9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int msgSlotIndex(int item) const {
|
|
|
|
|
|
return item - MSG_SLOT_0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 18:13:33 +02:00
|
|
|
|
void renderItem(DisplayDriver& display, int item, int y, bool sel) {
|
2026-05-17 00:12:40 +02:00
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
|
2026-06-16 08:54:11 +02:00
|
|
|
|
display.drawSelectionRow(0, y - 1, display.width() - _reserve, display.lineStep() - 1, sel);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
2026-06-15 14:41:46 +02:00
|
|
|
|
display.setCursor(2, y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_BRIGHTNESS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (item == BRIGHTNESS) {
|
|
|
|
|
|
display.print("Bright");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
renderBar(display, valCol(display), y, (p ? p->display_brightness : 2) + 1, 5);
|
2026-05-22 18:34:20 +02:00
|
|
|
|
} else
|
|
|
|
|
|
#endif
|
|
|
|
|
|
if (item == BUZZER) {
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print("Buzzer");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
{ static const char* labels[] = { "ON", "OFF", "Auto" };
|
|
|
|
|
|
int m = _task->getBuzzerMode();
|
|
|
|
|
|
display.print(labels[m < 3 ? m : 0]); }
|
|
|
|
|
|
#else
|
|
|
|
|
|
display.print("N/A");
|
|
|
|
|
|
#endif
|
|
|
|
|
|
} else if (item == BUZZER_VOLUME) {
|
|
|
|
|
|
display.print("BzrVol");
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
2026-06-15 16:45:28 +02:00
|
|
|
|
renderBar(display, valCol(display), y, _task->getBuzzerVolume() + 1, 5);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
#else
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print("N/A");
|
|
|
|
|
|
#endif
|
|
|
|
|
|
} else if (item == DM_MELODY) {
|
|
|
|
|
|
display.print("DM sound");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-07 09:13:02 +02:00
|
|
|
|
{ uint8_t v = p ? p->notif_melody_dm : 0;
|
|
|
|
|
|
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == CH_MELODY) {
|
|
|
|
|
|
display.print("Ch sound");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-07 09:13:02 +02:00
|
|
|
|
{ uint8_t v = p ? p->notif_melody_ch : 0;
|
|
|
|
|
|
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
|
2026-06-04 08:46:15 +02:00
|
|
|
|
} else if (item == AD_SOUND) {
|
|
|
|
|
|
display.print("AD sound");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-07 09:13:02 +02:00
|
|
|
|
{ uint8_t v = p ? p->notif_melody_ad : 0;
|
|
|
|
|
|
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
|
2026-06-07 10:53:17 +02:00
|
|
|
|
} else if (item == AD_SOUND_SCOPE) {
|
|
|
|
|
|
display.print("AD scope");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-07 10:53:17 +02:00
|
|
|
|
{ uint8_t v = p ? p->advert_sound_scope : ADVERT_SOUND_SCOPE_ALL;
|
|
|
|
|
|
display.print(AD_SCOPE_LABELS[v < AD_SCOPE_COUNT ? v : 0]); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (isHomePage(item)) {
|
2026-05-23 23:50:25 +02:00
|
|
|
|
if (p) ensurePageOrderInit(p);
|
2026-05-22 23:33:06 +02:00
|
|
|
|
int pos = homePagePosition(item, p);
|
|
|
|
|
|
if (pos > 0) {
|
2026-05-23 09:46:28 +02:00
|
|
|
|
char pb[5]; snprintf(pb, sizeof(pb), "%2d ", pos);
|
2026-05-22 23:33:06 +02:00
|
|
|
|
display.print(pb);
|
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print(homePageLabel(item));
|
2026-06-15 14:41:46 +02:00
|
|
|
|
display.setCursor(display.width() - 6 * display.getCharWidth() - _reserve, y);
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if (!homePageToggleable(item))
|
|
|
|
|
|
display.print("always");
|
|
|
|
|
|
else
|
|
|
|
|
|
display.print(homePageVisible(item, p) ? "ON" : "OFF");
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == TX_POWER) {
|
|
|
|
|
|
display.print("TX Pwr");
|
|
|
|
|
|
char buf[8];
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(buf, sizeof(buf),"%ddBm", p ? p->tx_power_dbm : 0);
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print(buf);
|
2026-06-20 09:07:00 +02:00
|
|
|
|
} else if (item == RADIO_PRESET) {
|
|
|
|
|
|
display.print("Preset");
|
2026-06-21 22:32:47 +02:00
|
|
|
|
const char* name = p ? _picker.currentName(p, radioTarget(p)) : "Custom";
|
2026-06-20 09:07:00 +02:00
|
|
|
|
int xc = valCol(display);
|
|
|
|
|
|
display.drawTextEllipsized(xc, y, display.width() - xc - _reserve, name);
|
|
|
|
|
|
} else if (item == CUSTOM_FREQ) {
|
|
|
|
|
|
display.print("Freq");
|
|
|
|
|
|
int xc = valCol(display);
|
2026-06-21 23:09:35 +02:00
|
|
|
|
if (sel && _editor.active()) {
|
|
|
|
|
|
_editor.render(display, xc, y);
|
2026-06-20 09:07:00 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
char buf[10];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%.3f", p ? p->freq : 0.0f);
|
|
|
|
|
|
display.setCursor(xc, y);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (item == CUSTOM_SF) {
|
|
|
|
|
|
display.print("SF");
|
|
|
|
|
|
char buf[6];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%d", p ? (int)p->sf : 0);
|
|
|
|
|
|
display.setCursor(valCol(display), y);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
} else if (item == CUSTOM_BW) {
|
|
|
|
|
|
display.print("BW");
|
|
|
|
|
|
char buf[10];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%.1f", p ? p->bw : 0.0f);
|
|
|
|
|
|
display.setCursor(valCol(display), y);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
} else if (item == CUSTOM_CR) {
|
|
|
|
|
|
display.print("CR");
|
|
|
|
|
|
char buf[6];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%d", p ? (int)p->cr : 0);
|
|
|
|
|
|
display.setCursor(valCol(display), y);
|
|
|
|
|
|
display.print(buf);
|
2026-06-10 23:30:43 +02:00
|
|
|
|
} else if (item == POWER_SAVE) {
|
|
|
|
|
|
display.print("Pwr save");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-20 09:07:00 +02:00
|
|
|
|
// Forced off (and locked) while the repeater is on — it must hear all traffic.
|
|
|
|
|
|
if (p && p->client_repeat) display.print("--");
|
|
|
|
|
|
else display.print((p && p->rx_powersave) ? "ON" : "OFF");
|
2026-06-10 23:30:43 +02:00
|
|
|
|
} else if (item == TX_APC) {
|
|
|
|
|
|
display.print("Auto pwr");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-20 09:07:00 +02:00
|
|
|
|
// Suppressed (and locked) while repeating — a repeater holds full TX power.
|
|
|
|
|
|
if (p && p->client_repeat) display.print("--");
|
|
|
|
|
|
else display.print((p && p->tx_apc) ? "ON" : "OFF");
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == AUTO_OFF) {
|
|
|
|
|
|
display.print("AutoOff");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print(AUTO_OFF_LABELS[autoOffIndex()]);
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-17 09:55:51 +02:00
|
|
|
|
} else if (item == AUTO_LOCK) {
|
|
|
|
|
|
display.print("AutoLock");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 09:55:51 +02:00
|
|
|
|
display.print((p && p->auto_lock) ? "ON" : "OFF");
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == TIMEZONE) {
|
|
|
|
|
|
display.print("TimeZone");
|
|
|
|
|
|
char buf[8];
|
|
|
|
|
|
int8_t tz = p ? p->tz_offset_hours : 0;
|
2026-05-24 20:35:49 +02:00
|
|
|
|
if (tz >= 0) snprintf(buf, sizeof(buf),"UTC+%d", (int)tz);
|
|
|
|
|
|
else snprintf(buf, sizeof(buf),"UTC%d", (int)tz);
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print(buf);
|
|
|
|
|
|
} else if (item == LOW_BAT) {
|
|
|
|
|
|
display.print("LowBat");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print(LOW_BAT_LABELS[lowBatIndex()]);
|
2026-06-04 00:44:56 +02:00
|
|
|
|
} else if (item == UNITS) {
|
|
|
|
|
|
display.print("Units");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-04 00:44:56 +02:00
|
|
|
|
display.print((p && p->units_imperial) ? "Imperial" : "Metric");
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == BATT_DISPLAY) {
|
|
|
|
|
|
display.print("BattDisp");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
uint8_t mode = p ? p->batt_display_mode : 0;
|
|
|
|
|
|
display.print(BATT_DISPLAY_LABELS[mode < BATT_DISPLAY_COUNT ? mode : 0]);
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_CLOCK_SECONDS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == CLOCK_SECONDS) {
|
|
|
|
|
|
display.print("Seconds");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print((p && p->clock_hide_seconds) ? "OFF" : "ON");
|
2026-05-22 10:47:19 +02:00
|
|
|
|
#endif
|
2026-05-21 21:04:43 +02:00
|
|
|
|
} else if (item == CLOCK_FORMAT) {
|
|
|
|
|
|
display.print("Format");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-21 21:04:43 +02:00
|
|
|
|
display.print((p && p->clock_12h) ? "12h" : "24h");
|
2026-05-20 09:29:38 +02:00
|
|
|
|
} else if (item == FONT) {
|
|
|
|
|
|
display.print("Font");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-20 09:29:38 +02:00
|
|
|
|
display.print((p && p->use_lemon_font) ? "Lemon" : "Default");
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_DISPLAY_ROTATION_SETTING
|
2026-05-20 21:53:40 +02:00
|
|
|
|
} else if (item == ROTATION) {
|
|
|
|
|
|
display.print("Rotation");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-15 21:23:00 +02:00
|
|
|
|
display.print(rotLabel(p ? p->display_rotation : 0));
|
2026-05-23 15:20:40 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_JOYSTICK_ROTATION_SETTING
|
2026-05-23 15:20:40 +02:00
|
|
|
|
} else if (item == JOY_ROTATION) {
|
|
|
|
|
|
display.print("Joystick");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-15 21:23:00 +02:00
|
|
|
|
display.print(rotLabel(p ? p->joystick_rotation : 0));
|
2026-05-24 10:22:03 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_FULL_REFRESH_SETTING
|
2026-05-24 10:22:03 +02:00
|
|
|
|
} else if (item == EINK_FULL_REFRESH) {
|
|
|
|
|
|
display.print("Full rfsh");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-24 10:22:03 +02:00
|
|
|
|
{ uint8_t idx = p ? p->eink_full_refresh_every : 0;
|
|
|
|
|
|
if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0;
|
|
|
|
|
|
display.print(EINK_FULL_REFRESH_LABELS[idx]); }
|
2026-05-20 21:53:40 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == DM_FILTER) {
|
|
|
|
|
|
display.print("DM");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print((p && p->dm_show_all) ? "all" : "fav");
|
2026-05-27 17:33:52 +02:00
|
|
|
|
} else if (item == CH_FILTER) {
|
|
|
|
|
|
display.print("Channels");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-27 17:33:52 +02:00
|
|
|
|
display.print((p && p->ch_fav_only) ? "fav" : "all");
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (item == ROOM_FILTER) {
|
|
|
|
|
|
display.print("Rooms");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
display.print((p && p->room_fav_only) ? "fav" : "all");
|
2026-06-14 23:33:16 +02:00
|
|
|
|
} else if (item == DM_RESEND) {
|
|
|
|
|
|
display.print("Resend");
|
2026-06-15 16:45:28 +02:00
|
|
|
|
display.setCursor(valCol(display), y);
|
2026-06-14 23:33:16 +02:00
|
|
|
|
uint8_t n = p ? p->dm_resend_count : 0;
|
|
|
|
|
|
if (n == 0) display.print("OFF");
|
|
|
|
|
|
else { char buf[6]; snprintf(buf, sizeof(buf), "%ux", (unsigned)n); display.print(buf); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
} else if (isMsgSlot(item)) {
|
|
|
|
|
|
int slot = msgSlotIndex(item);
|
|
|
|
|
|
char label[5];
|
|
|
|
|
|
snprintf(label, sizeof(label), "Q%d:", slot + 1);
|
|
|
|
|
|
display.print(label);
|
|
|
|
|
|
const char* tmpl = (p && p->custom_msgs[slot][0]) ? p->custom_msgs[slot] : "(empty)";
|
2026-05-22 18:05:09 +02:00
|
|
|
|
int xm = 8 + display.getCharWidth() * 4;
|
2026-06-15 14:41:46 +02:00
|
|
|
|
display.drawTextEllipsized(xm, y, display.width() - xm - _reserve, tmpl);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Keyboard state for editing message slots
|
2026-06-22 18:13:33 +02:00
|
|
|
|
int _edit_slot = -1; // -1 = not editing, 0..9 = slot being edited
|
2026-06-08 20:42:18 +02:00
|
|
|
|
KeyboardWidget* _kb;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
2026-06-20 09:07:00 +02:00
|
|
|
|
// Radio preset picker — names are too long for the value column, so Enter on
|
2026-06-21 22:32:47 +02:00
|
|
|
|
// RADIO_PRESET opens it as a full-width scrollable list instead of cycling.
|
|
|
|
|
|
// Shared with Tools › Repeater (see RadioPresetPicker.h). _picker.saving means
|
|
|
|
|
|
// _kb is open to name a new preset, not a message slot.
|
|
|
|
|
|
RadioPresetPicker _picker;
|
2026-06-20 09:07:00 +02:00
|
|
|
|
|
2026-06-21 23:09:35 +02:00
|
|
|
|
// Manual radio-parameter editing (digit-by-digit Freq editor + SF/BW/CR
|
|
|
|
|
|
// stepping), shared with Tools › Repeater — see RadioParamsEditor.h.
|
|
|
|
|
|
RadioParamsEditor _editor;
|
2026-06-20 09:07:00 +02:00
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
public:
|
2026-06-08 20:42:18 +02:00
|
|
|
|
SettingsScreen(UITask* task, KeyboardWidget* kb)
|
2026-06-22 18:13:33 +02:00
|
|
|
|
: _task(task), _kb(kb) {
|
|
|
|
|
|
buildSections();
|
|
|
|
|
|
resetList();
|
2026-05-26 15:49:50 +02:00
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 15:49:50 +02:00
|
|
|
|
void markClean() {
|
|
|
|
|
|
_dirty = false;
|
2026-06-22 18:13:33 +02:00
|
|
|
|
resetList();
|
2026-06-21 23:09:35 +02:00
|
|
|
|
_editor.freq.active = false;
|
2026-05-26 15:49:50 +02:00
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
|
|
int render(DisplayDriver& display) override {
|
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
|
2026-06-21 22:32:47 +02:00
|
|
|
|
if (_edit_slot >= 0 || _picker.saving) {
|
2026-06-08 20:42:18 +02:00
|
|
|
|
return _kb->render(display);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 21:23:00 +02:00
|
|
|
|
display.drawCenteredHeader("SETTINGS");
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
2026-06-22 18:13:33 +02:00
|
|
|
|
_acc.render(display,
|
|
|
|
|
|
// Section header: "[+/-] Name"
|
|
|
|
|
|
[&](int sec, int y, bool sel, int reserve, bool collapsed) {
|
|
|
|
|
|
_reserve = reserve;
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
display.drawSelectionRow(0, y - 1, display.width() - reserve, display.lineStep() - 1, sel);
|
|
|
|
|
|
display.setCursor(2, y);
|
|
|
|
|
|
display.print(collapsed ? "+" : "-");
|
|
|
|
|
|
display.print(" ");
|
|
|
|
|
|
display.print(sectionName(_sec_header[sec]));
|
|
|
|
|
|
},
|
|
|
|
|
|
// Item row
|
|
|
|
|
|
[&](int sec, int item, int y, bool sel, int reserve) {
|
|
|
|
|
|
_reserve = reserve;
|
|
|
|
|
|
renderItem(display, _sec_items[sec][item], y, sel);
|
|
|
|
|
|
});
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
2026-06-21 22:32:47 +02:00
|
|
|
|
if (_picker.menu.active) _picker.menu.render(display);
|
2026-06-20 09:07:00 +02:00
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
return 2000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool handleInput(char c) override {
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
|
|
|
|
|
|
// Keyboard editing mode for message slots
|
|
|
|
|
|
if (_edit_slot >= 0) {
|
2026-06-08 20:42:18 +02:00
|
|
|
|
auto res = _kb->handleInput(c);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (res == KeyboardWidget::DONE) {
|
|
|
|
|
|
if (p) {
|
2026-06-08 20:42:18 +02:00
|
|
|
|
strncpy(p->custom_msgs[_edit_slot], _kb->buf, sizeof(p->custom_msgs[0]) - 1);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
p->custom_msgs[_edit_slot][sizeof(p->custom_msgs[0]) - 1] = '\0';
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
_edit_slot = -1;
|
|
|
|
|
|
} else if (res == KeyboardWidget::CANCELLED) {
|
|
|
|
|
|
_edit_slot = -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 09:07:00 +02:00
|
|
|
|
// Digit-by-digit Freq editor
|
2026-06-21 23:09:35 +02:00
|
|
|
|
if (_editor.active()) {
|
|
|
|
|
|
if (_editor.handleFreqInput(c) && p) { _task->applyRadioParams(); _dirty = true; }
|
2026-06-20 09:07:00 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Keyboard editing mode for naming a new saved preset
|
2026-06-21 22:32:47 +02:00
|
|
|
|
if (_picker.saving) {
|
2026-06-20 09:07:00 +02:00
|
|
|
|
auto res = _kb->handleInput(c);
|
|
|
|
|
|
if (res == KeyboardWidget::DONE) {
|
2026-06-21 22:32:47 +02:00
|
|
|
|
if (p && _picker.save(p, _kb->buf, radioTarget(p))) {
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
_task->showAlert("Preset saved", 800);
|
|
|
|
|
|
}
|
|
|
|
|
|
_picker.saving = false;
|
2026-06-20 09:07:00 +02:00
|
|
|
|
} else if (res == KeyboardWidget::CANCELLED) {
|
2026-06-21 22:32:47 +02:00
|
|
|
|
_picker.saving = false;
|
2026-06-20 09:07:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Radio preset popup (and its "delete a saved preset" sub-list)
|
2026-06-21 22:32:47 +02:00
|
|
|
|
if (_picker.menu.active) {
|
|
|
|
|
|
auto res = _picker.menu.handleInput(c);
|
2026-06-20 09:07:00 +02:00
|
|
|
|
if (res == PopupMenu::SELECTED && p) {
|
2026-06-21 22:32:47 +02:00
|
|
|
|
switch (_picker.onSelected(_picker.menu.selectedIndex(), p, radioTarget(p))) {
|
|
|
|
|
|
case RadioPresetPicker::START_SAVE:
|
|
|
|
|
|
_kb->begin("", (int)sizeof(p->user_radio_presets[0].name) - 1);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case RadioPresetPicker::APPLIED:
|
|
|
|
|
|
_task->applyRadioParams();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case RadioPresetPicker::DELETED:
|
2026-06-20 09:07:00 +02:00
|
|
|
|
_dirty = true;
|
|
|
|
|
|
_task->showAlert("Preset deleted", 800);
|
2026-06-21 22:32:47 +02:00
|
|
|
|
break;
|
|
|
|
|
|
case RadioPresetPicker::NONE:
|
|
|
|
|
|
break;
|
2026-06-20 09:07:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
} else if (res == PopupMenu::CANCELLED) {
|
2026-06-21 22:32:47 +02:00
|
|
|
|
_picker.deleting = false;
|
2026-06-20 09:07:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (c == KEY_CANCEL) {
|
|
|
|
|
|
if (_dirty) the_mesh.savePrefs();
|
|
|
|
|
|
_task->gotoHomeScreen();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 18:13:33 +02:00
|
|
|
|
// Up/down navigation and section fold/unfold live in the shared helper.
|
|
|
|
|
|
// Enter on an item returns ACTIVATED and falls through to the per-item logic
|
|
|
|
|
|
// below; left/right are ignored by the helper and likewise fall through.
|
|
|
|
|
|
AccordionList::Result ar = _acc.handleInput(c);
|
|
|
|
|
|
if (ar == AccordionList::HANDLED) return true;
|
|
|
|
|
|
_selected = currentItem();
|
|
|
|
|
|
|
2026-06-15 21:23:00 +02:00
|
|
|
|
bool right = keyIsNext(c);
|
|
|
|
|
|
bool left = keyIsPrev(c);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
bool enter = (c == KEY_ENTER);
|
|
|
|
|
|
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_BRIGHTNESS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == BRIGHTNESS) {
|
|
|
|
|
|
uint8_t lvl = _task->getBrightnessLevel();
|
|
|
|
|
|
if (right && lvl < 4) { _task->setBrightnessLevel(lvl + 1); _dirty = true; return true; }
|
|
|
|
|
|
if (left && lvl > 0) { _task->setBrightnessLevel(lvl - 1); _dirty = true; return true; }
|
|
|
|
|
|
return right || left;
|
|
|
|
|
|
}
|
2026-05-22 18:34:20 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == BUZZER && (left || right || enter)) {
|
|
|
|
|
|
_task->cycleBuzzerMode();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == BUZZER_VOLUME) {
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
uint8_t lvl = _task->getBuzzerVolume();
|
|
|
|
|
|
if (right && lvl < 4) { _task->setBuzzerVolumeLevel(lvl + 1); _dirty = true; return true; }
|
|
|
|
|
|
if (left && lvl > 0) { _task->setBuzzerVolumeLevel(lvl - 1); _dirty = true; return true; }
|
|
|
|
|
|
#endif
|
|
|
|
|
|
return right || left;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == DM_MELODY && p && (left || right || enter)) {
|
2026-06-07 09:13:02 +02:00
|
|
|
|
p->notif_melody_dm = (p->notif_melody_dm + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
_dirty = true; return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == CH_MELODY && p && (left || right || enter)) {
|
2026-06-07 09:13:02 +02:00
|
|
|
|
p->notif_melody_ch = (p->notif_melody_ch + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
_dirty = true; return true;
|
|
|
|
|
|
}
|
2026-06-04 08:46:15 +02:00
|
|
|
|
if (_selected == AD_SOUND && p && (left || right || enter)) {
|
2026-06-07 09:13:02 +02:00
|
|
|
|
p->notif_melody_ad = (p->notif_melody_ad + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT;
|
2026-06-04 08:46:15 +02:00
|
|
|
|
_dirty = true; return true;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
2026-06-07 10:53:17 +02:00
|
|
|
|
if (_selected == AD_SOUND_SCOPE && p && (left || right || enter)) {
|
|
|
|
|
|
p->advert_sound_scope ^= 1;
|
|
|
|
|
|
_dirty = true; return true;
|
|
|
|
|
|
}
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if (isHomePage(_selected) && p) {
|
|
|
|
|
|
if (left || right) {
|
|
|
|
|
|
movePageInOrder(_selected, left ? -1 : 1, p);
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (enter && homePageToggleable(_selected)) {
|
2026-05-23 20:03:45 +02:00
|
|
|
|
if (!p->home_pages_mask) p->home_pages_mask = NodePrefs::HP_ALL;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
p->home_pages_mask ^= homePageBit(_selected);
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return enter;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
if (_selected == TX_POWER && p) {
|
|
|
|
|
|
if (right && p->tx_power_dbm < 22) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; }
|
|
|
|
|
|
if (left && p->tx_power_dbm > 2) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; }
|
|
|
|
|
|
}
|
2026-06-20 09:07:00 +02:00
|
|
|
|
if (_selected == RADIO_PRESET && p && enter) {
|
2026-06-21 22:32:47 +02:00
|
|
|
|
_picker.open(p, radioTarget(p), "Radio Preset");
|
2026-06-20 09:07:00 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-06-21 23:09:35 +02:00
|
|
|
|
// Enter Freq's digit-by-digit editor. Bounds come from the radio driver
|
|
|
|
|
|
// itself (RadioLib's own validated range for this chip) so a digit can never
|
|
|
|
|
|
// be nudged to a value setFrequency() would reject.
|
2026-06-20 09:07:00 +02:00
|
|
|
|
if (_selected == CUSTOM_FREQ && p && enter) {
|
|
|
|
|
|
float min_mhz, max_mhz;
|
|
|
|
|
|
radio_driver.getFreqBounds(min_mhz, max_mhz);
|
2026-06-21 23:09:35 +02:00
|
|
|
|
_editor.beginFreq(p->freq, min_mhz, max_mhz);
|
2026-06-20 09:07:00 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-06-21 23:09:35 +02:00
|
|
|
|
int dir = right ? 1 : (left ? -1 : 0);
|
|
|
|
|
|
if (_selected == CUSTOM_SF && p && dir && RadioParamsEditor::stepSF(p->sf, dir)) { _task->applyRadioParams(); _dirty = true; return true; }
|
|
|
|
|
|
if (_selected == CUSTOM_BW && p && dir && RadioParamsEditor::stepBW(p->bw, dir)) { _task->applyRadioParams(); _dirty = true; return true; }
|
|
|
|
|
|
if (_selected == CUSTOM_CR && p && dir && RadioParamsEditor::stepCR(p->cr, dir)) { _task->applyRadioParams(); _dirty = true; return true; }
|
2026-06-10 23:30:43 +02:00
|
|
|
|
if (_selected == POWER_SAVE && p && (left || right || enter)) {
|
2026-06-20 09:07:00 +02:00
|
|
|
|
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
|
2026-06-10 23:30:43 +02:00
|
|
|
|
p->rx_powersave ^= 1;
|
|
|
|
|
|
_task->applyPowerSave();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == TX_APC && p && (left || right || enter)) {
|
2026-06-20 09:07:00 +02:00
|
|
|
|
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
|
2026-06-10 23:30:43 +02:00
|
|
|
|
p->tx_apc ^= 1;
|
|
|
|
|
|
_task->applyApc();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == AUTO_OFF && p) {
|
|
|
|
|
|
int idx = autoOffIndex();
|
|
|
|
|
|
if (right) idx = (idx + 1) % AUTO_OFF_COUNT;
|
|
|
|
|
|
if (left) idx = (idx + AUTO_OFF_COUNT - 1) % AUTO_OFF_COUNT;
|
|
|
|
|
|
if (left || right) { p->auto_off_secs = AUTO_OFF_OPTS[idx]; _dirty = true; return true; }
|
|
|
|
|
|
}
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-17 09:55:51 +02:00
|
|
|
|
if (_selected == AUTO_LOCK && p && (left || right || enter)) {
|
|
|
|
|
|
p->auto_lock ^= 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == TIMEZONE && p) {
|
|
|
|
|
|
if (right && p->tz_offset_hours < 14) { p->tz_offset_hours++; _dirty = true; return true; }
|
|
|
|
|
|
if (left && p->tz_offset_hours > -12) { p->tz_offset_hours--; _dirty = true; return true; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_selected == LOW_BAT && p) {
|
|
|
|
|
|
int idx = lowBatIndex();
|
|
|
|
|
|
if (right) idx = (idx + 1) % LOW_BAT_COUNT;
|
|
|
|
|
|
if (left) idx = (idx + LOW_BAT_COUNT - 1) % LOW_BAT_COUNT;
|
|
|
|
|
|
if (left || right) { p->low_batt_mv = LOW_BAT_OPTS[idx]; _dirty = true; return true; }
|
|
|
|
|
|
}
|
2026-06-04 00:44:56 +02:00
|
|
|
|
if (_selected == UNITS && p && (left || right || enter)) {
|
|
|
|
|
|
p->units_imperial ^= 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-06-14 23:33:16 +02:00
|
|
|
|
if (_selected == DM_RESEND && p) {
|
|
|
|
|
|
int n = p->dm_resend_count;
|
|
|
|
|
|
if (right || enter) n = (n + 1) % 6; // 0..5, wraps
|
|
|
|
|
|
else if (left) n = (n + 5) % 6;
|
|
|
|
|
|
if (left || right || enter) { p->dm_resend_count = (uint8_t)n; _dirty = true; return true; }
|
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == BATT_DISPLAY && p) {
|
|
|
|
|
|
int idx = p->batt_display_mode < BATT_DISPLAY_COUNT ? p->batt_display_mode : 0;
|
|
|
|
|
|
if (right) idx = (idx + 1) % BATT_DISPLAY_COUNT;
|
|
|
|
|
|
if (left) idx = (idx + BATT_DISPLAY_COUNT - 1) % BATT_DISPLAY_COUNT;
|
|
|
|
|
|
if (left || right) { p->batt_display_mode = idx; _dirty = true; return true; }
|
|
|
|
|
|
}
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_CLOCK_SECONDS_SETTING
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == CLOCK_SECONDS && p && (left || right || enter)) {
|
|
|
|
|
|
p->clock_hide_seconds ^= 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-22 10:47:19 +02:00
|
|
|
|
#endif
|
2026-05-21 21:04:43 +02:00
|
|
|
|
if (_selected == CLOCK_FORMAT && p && (left || right || enter)) {
|
|
|
|
|
|
p->clock_12h ^= 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-20 09:29:38 +02:00
|
|
|
|
if (_selected == FONT && p && (left || right || enter)) {
|
2026-06-15 16:45:28 +02:00
|
|
|
|
// Normalise (not ^=1): a stale value >1 from an older build with extra
|
|
|
|
|
|
// font modes would otherwise toggle 2<->3 and stay stuck on Lemon.
|
|
|
|
|
|
p->use_lemon_font = p->use_lemon_font ? 0 : 1;
|
2026-05-20 09:29:38 +02:00
|
|
|
|
_task->applyFont();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_DISPLAY_ROTATION_SETTING
|
2026-05-20 21:53:40 +02:00
|
|
|
|
if (_selected == ROTATION && p && (left || right || enter)) {
|
|
|
|
|
|
p->display_rotation = (p->display_rotation + (left ? 3 : 1)) & 3;
|
|
|
|
|
|
_task->applyRotation();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-23 15:20:40 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_JOYSTICK_ROTATION_SETTING
|
2026-05-23 15:20:40 +02:00
|
|
|
|
if (_selected == JOY_ROTATION && p && (left || right || enter)) {
|
|
|
|
|
|
p->joystick_rotation = (p->joystick_rotation + (left ? 3 : 1)) & 3;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-24 10:22:03 +02:00
|
|
|
|
#endif
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_FULL_REFRESH_SETTING
|
2026-05-24 10:22:03 +02:00
|
|
|
|
if (_selected == EINK_FULL_REFRESH && p && (left || right || enter)) {
|
|
|
|
|
|
int idx = p->eink_full_refresh_every;
|
|
|
|
|
|
if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0;
|
|
|
|
|
|
idx = (idx + (left ? EINK_FULL_REFRESH_COUNT - 1 : 1)) % EINK_FULL_REFRESH_COUNT;
|
|
|
|
|
|
p->eink_full_refresh_every = idx;
|
|
|
|
|
|
_task->applyFullRefreshInterval();
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-20 21:53:40 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == DM_FILTER && p && (left || right || enter)) {
|
|
|
|
|
|
p->dm_show_all = p->dm_show_all ? 0 : 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-27 17:33:52 +02:00
|
|
|
|
if (_selected == CH_FILTER && p && (left || right || enter)) {
|
|
|
|
|
|
p->ch_fav_only = p->ch_fav_only ? 0 : 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
if (_selected == ROOM_FILTER && p && (left || right || enter)) {
|
|
|
|
|
|
p->room_fav_only = p->room_fav_only ? 0 : 1;
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isMsgSlot(_selected) && enter) {
|
|
|
|
|
|
int slot = msgSlotIndex(_selected);
|
|
|
|
|
|
_edit_slot = slot;
|
2026-06-04 12:15:30 +02:00
|
|
|
|
// Bound to the custom_msgs store (140 B) so the wider keyboard buffer
|
|
|
|
|
|
// can't overflow it on save.
|
2026-06-08 20:42:18 +02:00
|
|
|
|
_kb->begin(p ? p->custom_msgs[slot] : "",
|
2026-06-04 12:15:30 +02:00
|
|
|
|
p ? (int)sizeof(p->custom_msgs[slot]) - 1 : KB_MAX_LEN);
|
2026-06-09 15:09:57 +02:00
|
|
|
|
kbAddSensorPlaceholders(*_kb, &sensors);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-17 00:12:40 +02:00
|
|
|
|
const uint16_t SettingsScreen::AUTO_OFF_OPTS[5] = { 5, 15, 30, 60, 0 };
|
|
|
|
|
|
const char* SettingsScreen::AUTO_OFF_LABELS[5] = { "5s", "15s", "30s", "60s", "never" };
|
2026-05-22 23:13:27 +02:00
|
|
|
|
#endif
|
2026-05-17 00:12:40 +02:00
|
|
|
|
const uint16_t SettingsScreen::LOW_BAT_OPTS[7] = { 0, 3000, 3100, 3200, 3300, 3400, 3500 };
|
|
|
|
|
|
const char* SettingsScreen::LOW_BAT_LABELS[7] = { "off", "3.0V", "3.1V", "3.2V", "3.3V", "3.4V", "3.5V" };
|
|
|
|
|
|
const char* SettingsScreen::BATT_DISPLAY_LABELS[3] = { "icon", "%", "V" };
|
2026-06-07 09:13:02 +02:00
|
|
|
|
const char* SettingsScreen::SOUND_LABELS[4] = { "built-in", "M1", "M2", "None" };
|
2026-06-07 10:53:17 +02:00
|
|
|
|
const char* SettingsScreen::AD_SCOPE_LABELS[2] = { "All", "Zero-hop" };
|
2026-05-24 22:20:42 +02:00
|
|
|
|
#if FEAT_FULL_REFRESH_SETTING
|
2026-05-24 10:22:03 +02:00
|
|
|
|
const char* SettingsScreen::EINK_FULL_REFRESH_LABELS[5] = { "off", "5", "10", "20", "30" };
|
|
|
|
|
|
#endif
|