feat: app-only settings on the device: privacy, adding contacts, advanced radio

- Settings schema gains Privacy (position in adverts, who can ask for
  status / location / sensors), Adding contacts (all or by type, max
  hops, replace oldest) and Advanced radio (path hash size, double ACK,
  listen before talk, interference threshold, RX boost, RX delay,
  airtime factor); both UIs build their rows from it
- Auto-advert carries position only when Position in adverts is on
- ui-new accordion holds 16 sections and 128 rows
- docs: settings, tools, feature matrix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-10 00:47:03 +02:00
co-authored by Claude Opus 5.5
parent a0a76fbc60
commit 6fba3dfca7
10 changed files with 181 additions and 30 deletions
+2 -1
View File
@@ -3484,7 +3484,8 @@ void MyMesh::loop() {
}
if (_prefs.advert_auto_interval_sec > 0 && millisHasNowPassed(_next_auto_advert_ms)) {
mesh::Packet* pkt = (sensors.node_lat != 0 || sensors.node_lon != 0)
// The position only where Privacy lets adverts carry it, and once there is one.
mesh::Packet* pkt = (_prefs.advert_loc_policy != ADVERT_LOC_NONE && (sensors.node_lat != 0 || sensors.node_lon != 0))
? createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon)
: createSelfAdvert(_prefs.node_name);
if (pkt) sendZeroHop(pkt);
@@ -9,7 +9,9 @@
// uint8_t holding that index; with a `values` table the field (1, 2 or 4 bytes,
// `size`) holds values[index] instead -- seconds, millivolts, an hour offset,
// or {1, 0} for a switch whose field is stored inverted. count == 2 with no
// option labeller is an on/off switch.
// option labeller is an on/off switch. A `bit` mask makes it a switch over that
// one bit of a uint8_t field; size FLOAT_X100 is a float field with values in
// hundredths.
#include <stddef.h>
#include "../NodePrefs.h"
@@ -21,28 +23,33 @@ namespace settings {
// Pages (one screen each) and the sections on them, in display order.
// PG_NAV is the Map's options (its trail / live share / target tools),
// PG_ADVERT sits in Nearby's advert popup; the rest are Settings pages.
enum Page : uint8_t { PG_NAV, PG_ADVERT, PG_DEVICE, PG_MESSAGES, PG_SOUND, PG_COUNT };
// PG_PRIVACY and PG_RADIO (Radio's advanced options) are Settings pages too.
enum Page : uint8_t { PG_NAV, PG_ADVERT, PG_DEVICE, PG_MESSAGES, PG_SOUND, PG_PRIVACY, PG_RADIO, PG_COUNT };
enum Section : uint8_t { SEC_TRAIL, SEC_LIVE_SHARE, SEC_LOCATOR, SEC_ADVERT,
SEC_DISPLAY, SEC_POWER, SEC_TIME, SEC_UNITS,
SEC_MESSAGES, SEC_CONTACTS,
SEC_SOUND, SEC_SOUND_FOR, SEC_QUIET, SEC_COUNT };
SEC_MESSAGES, SEC_CONTACTS, SEC_AUTO_ADD,
SEC_SOUND, SEC_SOUND_FOR, SEC_QUIET,
SEC_PRIVACY, SEC_TELEMETRY, SEC_RADIO_ADV, SEC_COUNT };
static const char* pageTitle(uint8_t p) {
static const char* T[PG_COUNT] = { "Map options", "Advert", "Display & power", "Messages & contacts", "Sound" };
static const char* T[PG_COUNT] = { "Map options", "Advert", "Display & power", "Messages & contacts", "Sound",
"Privacy", "Advanced radio" };
return p < PG_COUNT ? T[p] : "";
}
static const char* sectionTitle(uint8_t s) {
static const char* T[SEC_COUNT] = { "TRAIL", "LIVE SHARE", "ARRIVAL ALERT", "AUTOMATIC",
"DISPLAY", "POWER", "TIME", "UNITS",
"MESSAGES", "CONTACTS",
"SOUND", "PLAYS FOR", "QUIET HOURS" };
"MESSAGES", "CONTACTS", "ADDING CONTACTS",
"SOUND", "PLAYS FOR", "QUIET HOURS",
"ADVERTS", "WHO CAN ASK FOR", "ADVANCED" };
return s < SEC_COUNT ? T[s] : "";
}
static uint8_t sectionPage(uint8_t s) {
static const uint8_t P[SEC_COUNT] = { PG_NAV, PG_NAV, PG_NAV, PG_ADVERT,
PG_DEVICE, PG_DEVICE, PG_DEVICE, PG_DEVICE,
PG_MESSAGES, PG_MESSAGES,
PG_SOUND, PG_SOUND, PG_SOUND };
PG_MESSAGES, PG_MESSAGES, PG_MESSAGES,
PG_SOUND, PG_SOUND, PG_SOUND,
PG_PRIVACY, PG_PRIVACY, PG_RADIO };
return s < SEC_COUNT ? P[s] : PG_DEVICE;
}
@@ -57,8 +64,10 @@ struct Setting {
// Side effect after a change (nullptr: none). The caller saves the prefs.
void (*changed)(UiCore& core);
const int32_t* values; // stored value per index; nullptr: the index itself
uint8_t size; // field bytes (1, 2, 4)
uint8_t size; // field bytes (1, 2, 4), or FLOAT_X100
uint8_t bit; // nonzero: a switch over this bit of a uint8_t field
};
static const uint8_t FLOAT_X100 = 0x84; // size: a float, values in hundredths
// ── Value labels ──────────────────────────────────────────────────────────────
#ifndef TRAIL_FIXED_MIN_DELTA_M
@@ -152,12 +161,40 @@ static void optAdvertScope(uint8_t v, char* b, int n, const NodePrefs&) {
snprintf(b, n, "%s", v == ADVERT_SOUND_SCOPE_ZERO_HOP ? "Direct" : "All");
}
static void optTelemetry(uint8_t v, char* b, int n, const NodePrefs&) {
static const char* const L[] = { "Nobody", "Allowed", "Everyone" }; // TELEM_MODE_*
snprintf(b, n, "%s", L[v < 3 ? v : 0]);
}
static void optAutoAdd(uint8_t v, char* b, int n, const NodePrefs&) { snprintf(b, n, "%s", v ? "By type" : "All"); }
// autoadd_max_hops: 0 = any, 1 = direct only, N = up to N-1 hops.
static const int32_t MAX_HOPS[] = { 0, 1, 2, 3, 4, 6, 9 };
static void optMaxHops(uint8_t v, char* b, int n, const NodePrefs&) {
int32_t h = MAX_HOPS[v];
if (h == 0) snprintf(b, n, "Any");
else if (h == 1) snprintf(b, n, "Direct");
else snprintf(b, n, "%ld hop%s", (long)(h - 1), h == 2 ? "" : "s");
}
static void optHashSize(uint8_t v, char* b, int n, const NodePrefs&) { snprintf(b, n, "%u byte%s", v + 1, v ? "s" : ""); }
static const int32_t INT_THRESH[] = { 0, 8, 10, 12, 14, 16, 20 };
static void optIntThresh(uint8_t v, char* b, int n, const NodePrefs&) {
if (INT_THRESH[v] == 0) snprintf(b, n, "Off"); else snprintf(b, n, "%ld dB", (long)INT_THRESH[v]);
}
static const int32_t RX_DELAY[] = { 0, 200, 500, 1000, 2000 }; // rx_delay_base, x100
static void optRxDelay(uint8_t v, char* b, int n, const NodePrefs&) {
if (RX_DELAY[v] == 0) snprintf(b, n, "Off"); else snprintf(b, n, "%ld", (long)(RX_DELAY[v] / 100));
}
static const int32_t AIRTIME[] = { 0, 50, 100, 200, 300, 500, 900 }; // airtime_factor, x100
static void optAirtime(uint8_t v, char* b, int n, const NodePrefs&) {
snprintf(b, n, "%ld.%ld x", (long)(AIRTIME[v] / 100), (long)(AIRTIME[v] % 100 / 10));
}
// ── Side effects ──────────────────────────────────────────────────────────────
static void rearmLocator(UiCore& c) { c.locator.reset(); }
static void restartShareClock(UiCore& c) { c.live_share.restartClock(); } // a new length starts over
static void applyDisplay(UiCore& c) { c.host()->applyDisplayPrefs(); }
static void applyGpsDuty(UiCore& c) { c.applyGpsInterval(); }
static void applySound(UiCore& c) { c.host()->applySoundPrefs(); }
static void applyRxGain(UiCore& c) { if (c.prefs()) radio_driver.setRxBoostedGainMode(c.prefs()->rx_boosted_gain); }
#define NP_OFF(f) (uint16_t)offsetof(NodePrefs, f)
#define NP_SIZE(f) (uint8_t)sizeof(((NodePrefs*)0)->f)
@@ -166,6 +203,8 @@ static void applySound(UiCore& c) { c.host()->applySoundPrefs(); }
#define IDX(label, hint, sec, f, count, opt, chg) { label, hint, sec, NP_OFF(f), count, opt, chg, nullptr, 1 }
#define SW(label, hint, sec, f, chg) { label, hint, sec, NP_OFF(f), 2, nullptr, chg, nullptr, 1 }
#define MAP(label, hint, sec, f, vals, opt, chg) { label, hint, sec, NP_OFF(f), COUNT_OF(vals), opt, chg, vals, NP_SIZE(f) }
#define MAPF(label, hint, sec, f, vals, opt) { label, hint, sec, NP_OFF(f), COUNT_OF(vals), opt, nullptr, vals, FLOAT_X100 }
#define BIT(label, hint, sec, f, mask) { label, hint, sec, NP_OFF(f), 2, nullptr, nullptr, nullptr, 1, mask }
static const Setting ALL[] = {
#ifndef TRAIL_FIXED_MIN_DELTA_M // else every sample past a fixed jitter gate is kept
IDX("Point spacing", "Between trail points", SEC_TRAIL, trail_min_delta_idx,
@@ -194,7 +233,7 @@ static const Setting ALL[] = {
NodePrefs::LOCATOR_MODE_COUNT, optLocMode, rearmLocator),
SW("Proximity beeper", "Ticks faster when closer", SEC_LOCATOR, locator_beeper, nullptr),
MAP("Auto-advert", "Advert with your position", SEC_ADVERT, advert_auto_interval_sec, AUTO_ADVERT,
MAP("Auto-advert", "Your advert, on a timer", SEC_ADVERT, advert_auto_interval_sec, AUTO_ADVERT,
optAutoAdvert, nullptr),
IDX("Brightness", nullptr, SEC_DISPLAY, display_brightness, 5, optBrightness, applyDisplay),
@@ -221,6 +260,15 @@ static const Setting ALL[] = {
NodePrefs::CONTACT_EXPIRY_COUNT, optExpiry, nullptr),
SW("Favourites first", "In contact and node lists", SEC_CONTACTS, fav_sort, nullptr),
// manual_add_contacts bit 0: 0 adds every node heard, 1 only the types below.
IDX("Add heard nodes", "Every node, or only these types", SEC_AUTO_ADD, manual_add_contacts, 2, optAutoAdd, nullptr),
BIT("Companions", nullptr, SEC_AUTO_ADD, autoadd_config, 0x02),
BIT("Repeaters", nullptr, SEC_AUTO_ADD, autoadd_config, 0x04),
BIT("Rooms", nullptr, SEC_AUTO_ADD, autoadd_config, 0x08),
BIT("Sensors", nullptr, SEC_AUTO_ADD, autoadd_config, 0x10),
MAP("Up to", "How far away a node may be", SEC_AUTO_ADD, autoadd_max_hops, MAX_HOPS, optMaxHops, nullptr),
BIT("Replace oldest", "When the list is full; not favourites", SEC_AUTO_ADD, autoadd_config, 0x01),
// On / Off / Auto spans two fields (soundctl::setMode): the frontend's own row.
IDX("Volume", nullptr, SEC_SOUND, buzzer_volume, 5, optVolume, applySound),
SW("Quiet hours", "Muted, screen dark; alarms ring", SEC_QUIET, quiet_hours, nullptr),
@@ -231,6 +279,23 @@ static const Setting ALL[] = {
IDX("Channels", nullptr, SEC_SOUND_FOR, notif_melody_ch, soundctl::SOUND_COUNT, optSound, nullptr),
IDX("Adverts", "A node announcing itself", SEC_SOUND_FOR, notif_melody_ad, soundctl::SOUND_COUNT, optSound, nullptr),
IDX("Advert sound for", "Direct: no repeaters", SEC_SOUND_FOR, advert_sound_scope, 2, optAdvertScope, nullptr),
SW("Position in adverts", "Your adverts carry your location", SEC_PRIVACY, advert_loc_policy, nullptr),
// TELEM_MODE_*: "Allowed" is the contacts given that permission.
IDX("Status", "Battery; the two below need it", SEC_TELEMETRY, telemetry_mode_base, 3, optTelemetry, nullptr),
IDX("Location", nullptr, SEC_TELEMETRY, telemetry_mode_loc, 3, optTelemetry, nullptr),
IDX("Sensors", nullptr, SEC_TELEMETRY, telemetry_mode_env, 3, optTelemetry, nullptr),
IDX("Path hash size", "Bytes per repeater in a path", SEC_RADIO_ADV, path_hash_mode, 3, optHashSize, nullptr),
SW("Double ACK", "Send each acknowledgement twice", SEC_RADIO_ADV, multi_acks, nullptr),
SW("Listen before talk", "Wait for a quiet channel (CAD)", SEC_RADIO_ADV, cad_enabled, nullptr),
MAP("Interference", "Busy above the noise floor by", SEC_RADIO_ADV, interference_threshold, INT_THRESH,
optIntThresh, nullptr),
#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR2021)
SW("Boosted RX gain", "Hears more, uses a little more power", SEC_RADIO_ADV, rx_boosted_gain, applyRxGain),
#endif
MAPF("RX delay", "Repeaters wait to pick the best path", SEC_RADIO_ADV, rx_delay_base, RX_DELAY, optRxDelay),
MAPF("Airtime factor", "Pause after sending, times airtime", SEC_RADIO_ADV, airtime_factor, AIRTIME, optAirtime),
};
static const int COUNT = (int)(sizeof(ALL) / sizeof(ALL[0]));
@@ -249,8 +314,19 @@ static const struct { uint16_t offset; const char* text; } SHORT_LABELS[] = {
{ NP_OFF(quiet_to), " until" }, { NP_OFF(notif_melody_dm), "DM sound" },
{ NP_OFF(notif_melody_ch), "Ch sound" }, { NP_OFF(notif_melody_ad), "AD sound" },
{ NP_OFF(advert_sound_scope), "AD scope" },
{ NP_OFF(manual_add_contacts), "Auto-add" }, { NP_OFF(autoadd_max_hops), " up to" },
{ NP_OFF(advert_loc_policy), "Advert pos" }, { NP_OFF(telemetry_mode_base), "Ask status" },
{ NP_OFF(telemetry_mode_loc), "Ask loc" }, { NP_OFF(telemetry_mode_env), "Ask sens" },
{ NP_OFF(path_hash_mode), "Hash size" }, { NP_OFF(multi_acks), "Double ACK" },
{ NP_OFF(cad_enabled), "LBT (CAD)" }, { NP_OFF(interference_threshold), "Interf." },
{ NP_OFF(rx_boosted_gain), "RX boost" }, { NP_OFF(rx_delay_base), "RX delay" },
{ NP_OFF(airtime_factor), "Airtime" },
};
static const char* shortLabel(const Setting& s) {
if (s.bit) { // the auto-add types share one field
static const char* const B[] = { "Replace old", " companions", " repeaters", " rooms", " sensors" };
for (int i = 0; i < 5; i++) if (s.bit == (1 << i)) return B[i];
}
for (auto& l : SHORT_LABELS) if (l.offset == s.offset) return l.text;
return s.label;
}
@@ -262,12 +338,16 @@ static int indexOf(uint16_t offset) {
#undef IDX
#undef SW
#undef MAP
#undef MAPF
#undef BIT
#undef COUNT_OF
#undef NP_SIZE
#undef NP_OFF
static int32_t readRaw(const NodePrefs& p, const Setting& s) {
const uint8_t* f = (const uint8_t*)&p + s.offset;
if (s.bit) return (f[0] & s.bit) ? 1 : 0;
if (s.size == FLOAT_X100) { float v; memcpy(&v, f, 4); return (int32_t)(v * 100 + (v < 0 ? -0.5f : 0.5f)); }
bool sgn = false; // a table with negative values is over a signed field
for (uint8_t i = 0; s.values && i < s.count; i++) if (s.values[i] < 0) sgn = true;
if (s.size == 2) { uint16_t v; memcpy(&v, f, 2); return sgn ? (int32_t)(int16_t)v : (int32_t)v; }
@@ -276,6 +356,8 @@ static int32_t readRaw(const NodePrefs& p, const Setting& s) {
}
static void writeRaw(NodePrefs& p, const Setting& s, int32_t v) {
uint8_t* f = (uint8_t*)&p + s.offset;
if (s.bit) { f[0] = v ? (f[0] | s.bit) : (f[0] & ~s.bit); return; }
if (s.size == FLOAT_X100) { float w = v / 100.0f; memcpy(f, &w, 4); return; }
if (s.size == 2) { uint16_t w = (uint16_t)v; memcpy(f, &w, 2); }
else if (s.size == 4) { uint32_t w = (uint32_t)v; memcpy(f, &w, 4); }
else f[0] = (uint8_t)v;
@@ -152,6 +152,7 @@ public:
}
UiCoreHost* host() { return _host; }
NodePrefs* prefs() { return _prefs; }
// ════ MyMesh::Listener ════════════════════════════════════════════════════
@@ -96,13 +96,19 @@ void UITask::buildRadio() {
paramRows(g, p, radioctl::currentPreset(p), p->freq, p->sf, p->bw, p->cr, onRadioDropdown, IDS, onRadioFreq,
"Higher = longer range");
groupNote(body, "Everyone you talk to needs the same settings.");
fill(&UITask::radioGroup, 1, 3); // the rest as the loop goes
fill(&UITask::radioGroup, 1, 4); // the rest as the loop goes
}
void UITask::radioGroup(int i) {
using namespace radioview;
NodePrefs* p = _prefs;
if (i == 2) { buildRadioExtras(_body); return; } // my presets, scopes (RadioExtras.h)
if (i == 3) { // the schema's advanced options, a page of their own
lv_obj_t* g = group(_body, nullptr);
listRow(g, LV_SYMBOL_SETTINGS " Advanced", "Path hash size, ACKs, listen before talk, delays",
onOpenSchemaPage, (void*)(uintptr_t)settings::PG_RADIO);
return;
}
lv_obj_t* g = group(_body, "TRANSMIT");
int o = 0;
for (int v = TX_MIN; v <= TX_MAX; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > TX_MIN ? "\n%d dBm" : "%d dBm", v);
+9 -2
View File
@@ -2399,6 +2399,8 @@ void UITask::back() {
if (_settings_page == settings::PG_NAV && s_opts_from_map) {
openMap(true);
if (s_nav_section >= 0) navToolsPopup(); // one section's options: back into Map tools
} else if (_settings_page == settings::PG_RADIO) {
showRadio();
} else {
showSettings();
}
@@ -3548,6 +3550,7 @@ static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub,
#include "MapRegions.h"
#include "HomeScreen.h"
#include "ClockScreen.h"
static void onOpenSchemaPage(lv_event_t* e); // Radio > Advanced
#include "RadioScreen.h"
#include "WifiScreen.h"
#include "ChannelScreen.h"
@@ -4536,11 +4539,13 @@ void UITask::buildAboutPage(lv_obj_t* body) {
// (fill); a popup's list gets them all now.
static lv_obj_t* s_schema_body = nullptr;
static bool s_schema_one = false; // one section: its own page, no heading
static bool s_schema_bare = false; // no section headings
void UITask::schemaRows(lv_obj_t* body, uint8_t page) {
for (lv_obj_t*& c : s_sec_card) c = nullptr;
s_schema_body = body;
s_schema_one = page == settings::PG_NAV && s_nav_section >= 0;
s_schema_bare = s_schema_one || page == settings::PG_RADIO; // one section: the page title says it
uint8_t sec = 0xFF;
bool first = true;
for (int i = 0; i < settings::COUNT; i++) {
@@ -4557,7 +4562,7 @@ void UITask::schemaRows(lv_obj_t* body, uint8_t page) {
// The group of the section setting i starts, with its rows.
void UITask::schemaSection(int i) {
uint8_t sec = settings::ALL[i].section;
lv_obj_t* card = s_sec_card[sec] = group(s_schema_body, s_schema_one ? nullptr : settings::sectionTitle(sec));
lv_obj_t* card = s_sec_card[sec] = group(s_schema_body, s_schema_bare ? nullptr : settings::sectionTitle(sec));
if (sec == settings::SEC_SOUND) buildSoundRows(card, true); // On / Off / Auto
for (int k = i; k < settings::COUNT && settings::ALL[k].section == sec; k++) {
if (settings::ALL[k].offset == offsetof(NodePrefs, lock_compact)) continue; // ui-new's lock look; the slide card has one
@@ -4684,7 +4689,7 @@ void UITask::settingsGroup(int i) {
snprintf(sub, sizeof(sub), aa == ma ? "%s" : "%s + %s", SCRIPT[ma], SCRIPT[aa]);
}
listRow(g, LV_SYMBOL_KEYBOARD " Keyboard", sub, onOpenSchemaPage, (void*)(uintptr_t)PG_KEYBOARD);
listRow(g, LV_SYMBOL_ENVELOPE " Messages & contacts", "Resend, expiry, quick messages",
listRow(g, LV_SYMBOL_ENVELOPE " Messages & contacts", "Resend, adding, expiry, quick messages",
onOpenSchemaPage, (void*)(uintptr_t)settings::PG_MESSAGES);
break;
@@ -4696,6 +4701,8 @@ void UITask::settingsGroup(int i) {
if (pi >= 0) radioctl::presetAt(_prefs, pi, pn, f, b, sf, cr);
snprintf(sub, sizeof(sub), "%s - %.3f MHz, %d dBm", pn, _prefs->freq, _prefs->tx_power_dbm);
listRow(g, UI_SYMBOL_RADIO " Radio", sub, onOpenRadio, NULL);
listRow(g, LV_SYMBOL_EYE_CLOSE " Privacy", "Position in adverts, who can ask for telemetry",
onOpenSchemaPage, (void*)(uintptr_t)settings::PG_PRIVACY);
}
bluetoothRow(g);
wifiRow(g);
@@ -22,15 +22,15 @@ public:
enum Result { IGNORED, HANDLED, ACTIVATED };
static const int MAX_SECTIONS = 8; // collapse bitmask is 8 bits
static const int MAX_ROWS = 64;
static const int MAX_SECTIONS = 16; // collapse bitmask is 16 bits
static const int MAX_ROWS = 128;
// (Re)initialise from per-section item counts. `collapsed` sets the initial
// fold state for every section (Settings opens folded, so default true).
void begin(const uint8_t* section_sizes, int section_count, bool collapsed = true) {
_count = section_count > MAX_SECTIONS ? MAX_SECTIONS : section_count;
for (int i = 0; i < _count; i++) _sizes[i] = section_sizes[i];
_collapsed = collapsed ? 0xFF : 0x00;
_collapsed = collapsed ? 0xFFFF : 0x0000;
_sel = 0; _scroll = 0;
rebuild();
}
@@ -68,7 +68,7 @@ public:
// the cursor never jumps to an unrelated row when items appear/disappear.
void toggle(int sec) {
if (sec < 0 || sec >= _count) return;
_collapsed ^= (uint8_t)(1u << sec);
_collapsed ^= (uint16_t)(1u << sec);
rebuild();
for (int i = 0; i < _vis_count; i++)
if (_rows[i].sec == sec && _rows[i].item < 0) { _sel = i; break; }
@@ -77,7 +77,7 @@ public:
private:
uint8_t _sizes[MAX_SECTIONS];
int _count = 0;
uint8_t _collapsed = 0xFF;
uint16_t _collapsed = 0xFFFF;
Row _rows[MAX_ROWS];
int _vis_count = 0;
int _sel = 0, _scroll = 0;
@@ -46,6 +46,7 @@ class SettingsScreen : public UIScreen {
#endif
TX_APC,
SCOPE_NAME,
SCHEMA_RADIO, // the advanced radio options
// System section
SECTION_SYSTEM,
DEVICE_NAME,
@@ -61,7 +62,9 @@ class SettingsScreen : public UIScreen {
KEYBOARD_CARDKB_COMPACT,
#endif
// Contacts section
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER, SCHEMA_CONTACTS, PRUNE_NOW,
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER, SCHEMA_CONTACTS, PRUNE_NOW, SCHEMA_AUTO_ADD,
// Privacy section: what adverts carry, who may ask for telemetry
SECTION_PRIVACY, SCHEMA_PRIVACY,
// Messages section
SECTION_MESSAGES,
SCHEMA_MESSAGES,
@@ -83,8 +86,8 @@ class SettingsScreen : public UIScreen {
bool _keep_place = false; // set when opening a screen from a row, so Back lands on it
AccordionList _acc;
static const int NUM_SECTIONS = 8;
static const int MAX_PER_SEC = 16;
static const int NUM_SECTIONS = 9;
static const int MAX_PER_SEC = 20;
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
@@ -136,7 +139,8 @@ class SettingsScreen : public UIScreen {
item == SECTION_HOME_PAGES ||
item == SECTION_RADIO || item == SECTION_SYSTEM ||
item == SECTION_KEYBOARD ||
item == SECTION_CONTACTS || item == SECTION_MESSAGES;
item == SECTION_CONTACTS || item == SECTION_MESSAGES ||
item == SECTION_PRIVACY;
}
const char* sectionName(int item) const {
@@ -148,6 +152,7 @@ class SettingsScreen : public UIScreen {
if (item == SECTION_KEYBOARD) return "Keyboard";
if (item == SECTION_CONTACTS) return "Contacts";
if (item == SECTION_MESSAGES) return "Messages";
if (item == SECTION_PRIVACY) return "Privacy";
return "";
}
@@ -178,7 +183,8 @@ class SettingsScreen : public UIScreen {
static bool isSchemaGroup(int item) {
return item == SCHEMA_DISPLAY || item == SCHEMA_SOUND || item == SCHEMA_SYSTEM ||
item == SCHEMA_CONTACTS || item == SCHEMA_MESSAGES;
item == SCHEMA_CONTACTS || item == SCHEMA_MESSAGES || item == SCHEMA_RADIO || item == SCHEMA_AUTO_ADD ||
item == SCHEMA_PRIVACY;
}
// Which schema sections a placeholder stands for.
static bool schemaIn(int group, uint8_t sec) {
@@ -188,6 +194,9 @@ class SettingsScreen : public UIScreen {
case SCHEMA_SOUND: return sec == SEC_SOUND || sec == SEC_QUIET || sec == SEC_SOUND_FOR;
case SCHEMA_SYSTEM: return sec == SEC_POWER || sec == SEC_UNITS;
case SCHEMA_CONTACTS: return sec == SEC_CONTACTS;
case SCHEMA_AUTO_ADD: return sec == SEC_AUTO_ADD;
case SCHEMA_RADIO: return sec == SEC_RADIO_ADV;
case SCHEMA_PRIVACY: return sec == SEC_PRIVACY || sec == SEC_TELEMETRY;
case SCHEMA_MESSAGES: return sec == SEC_MESSAGES;
}
return false;