mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +00:00
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:
@@ -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 ════════════════════════════════════════════════════
|
||||
|
||||
|
||||
Reference in New Issue
Block a user