Files
2026-08-25 15:19:11 +02:00

339 lines
17 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SPDX-License-Identifier: MIT
* NodePrefs - persisted node configuration (unified for all roles)
*
* Serialized field-by-field, not raw memcpy; struct layout does
* not affect on-disk compatibility.
*/
#pragma once
#include <stdint.h>
#include <string.h>
#define TELEM_MODE_DENY 0
#define TELEM_MODE_ALLOW_FLAGS 1
#define TELEM_MODE_ALLOW_ALL 2
#define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1
#define ADVERT_LOC_PREFS 2
#define LOOP_DETECT_OFF 0
#define LOOP_DETECT_MINIMAL 1
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
/* Adaptive-CAD operating detPeak offset range (levels from the family base).
* Wide on purpose: a dense hilltop can need a much higher detPeak than a quiet
* valley node. The per-family absolute clamp inside the driver (SX126x 15-40,
* LR11xx/LR20xx 48-90) is a firmware guardrail, NOT a chip limit — cadDetPeak
* is a full uint8_t (0-255). It just keeps the staircase from wandering into
* "CAD never fires" (too high) or "CAD always busy" (too low) territory. This
* offset range limits how far the staircase / manual offset may roam; MUST
* match CAD_LEVEL_MIN/MAX in adapters/radio/radio_common.h (they index the
* per-level stats array). */
#define CAD_OFFSET_MIN (-8)
#define CAD_OFFSET_MAX 12
/* leds_disabled, as stored in the repeater/room-server/observer prefs layout.
*
* It occupies the byte that used to hold agc_reset_interval (offset 120),
* retired when periodic AGC recalibration was removed. That byte is NOT
* reusable as a plain 0/1 boolean: the old command stored seconds/4, so a node
* upgrading from a build that had it configured has an arbitrary small integer
* sitting there, and a bare non-zero test would silently kill its LEDs. Hence
* a magic encoding — anything that is not one of these two values is a legacy
* AGC interval and decodes to the default (LEDs on). The first savePrefs()
* claims the byte for good.
*
* The companion layout is unaffected: it has always stored leds_disabled as a
* plain 0/1 at its own offset 93. */
#define LEDS_PREF_ON 0xA0
#define LEDS_PREF_OFF 0xA1
/* Number of LR2021 side-detector slots stored in prefs. Matches the chip's
* ConfigureSideDetectors limit of 3. */
#define EXTRA_SF_MAX 3
struct NodePrefs {
/* ---- Common fields (both roles) ---- */
float airtime_factor;
char node_name[32];
double node_lat, node_lon;
char password[16];
float freq;
int8_t tx_power_dbm;
uint8_t disable_fwd; // repeater: disable forwarding
uint8_t advert_interval; // stored as minutes / 2
uint8_t flood_advert_interval; // hours
float rx_delay_base;
float tx_delay_factor;
char guest_password[16];
float direct_tx_delay_factor;
float backoff_multiplier; // per-dupe reactive backoff (0.0 = disabled)
uint32_t guard;
uint8_t sf;
uint8_t cr;
uint8_t allow_read_only;
uint8_t multi_acks;
float bw;
uint8_t flood_max;
uint8_t flood_max_unscoped; // hop limit for un-scoped (ROUTE_TYPE_FLOOD) floods
uint8_t flood_max_advert; // hop limit for ADVERT floods (curbs advert churn)
uint8_t interference_threshold;
uint8_t leds_disabled; // 1 = all LEDs off (heartbeat, unread, LoRa TX)
// Power saving
uint8_t powersaving_enabled;
// GPS settings
uint8_t gps_enabled;
uint32_t gps_interval; // in seconds
uint8_t advert_loc_policy;
uint32_t discovery_mod_timestamp;
float adc_multiplier;
char owner_info[120];
uint8_t rx_boost; // 1 = boosted RX gain (+3dB), 0 = power save
uint8_t fem_rxgain; // 1 = external FEM LNA active during RX, 0 = off (power save)
uint8_t rx_duty_cycle; // 1 = RX duty cycle, 0 = continuous RX
/* RESERVED — formerly apc_enabled / apc_margin (Adaptive Power Control,
* removed in 1.16.6). These two bytes are still read and written at their
* original offsets in all three prefs serializers (companion new_prefs 94/95,
* repeater prefs 292/293, RepeaterDataStore) because every field after them
* is positional: dropping them would shift the rest of the layout and make
* every already-deployed node misparse its saved prefs on upgrade.
* Do not reuse for a new setting — an upgraded node still has the old APC
* values sitting in these bytes. */
uint8_t _reserved_apc_enabled;
uint8_t _reserved_apc_margin;
uint8_t meshtimesync; // 1 = mesh time-sync clock correction on (default off)
uint8_t cad_auto; // 1 = adaptive-CAD staircase acts on probe stats (default off = dry-run)
int8_t cad_offset; // operating detPeak offset from family base (-4..4)
uint8_t probe_interval; // seconds between periodic radio measurements:
// one noise-floor sample, and the CAD probe that
// consumes it (0 = CAD probing off, default 15)
uint8_t cad_busycap; // airtime-protection: max % of TX attempts deferred before backing off detPeak (0 = off, default 25)
/* LR2021 side detectors: extra spreading factors received concurrently
* with `sf`, on the same bandwidth. Zero-terminated list — the first 0
* ends it, so an all-zero array means the feature is off (which is what
* every node upgrading from a build without this field has). Every
* entry must be greater than `sf`, all distinct, spread <= 4; the
* driver rejects anything else. Ignored on non-LR2021 radios. */
uint8_t extra_sf[EXTRA_SF_MAX];
/* Physical mounting orientation. Common to both roles: a repeater board
* with an OLED (RAK4631, Heltec) can be mounted upside down just as a
* companion can, and the joystick boards ship a repeater artifact too.
*
* display_rotate rotates the panel 180 degrees in hardware and is only
* honoured on SSD1306/SH1106 (see MC_DISPLAY_ROTATE_SUPPORTED); other
* panels report it unsupported rather than silently ignoring it.
* input_rotate swaps the joystick/D-pad axes to match, and is kept
* separate because the two are not always wanted together — a screen can
* be remounted without moving the stick. */
uint8_t display_rotate; // 1 = panel rotated 180 degrees
uint8_t input_rotate; // 1 = joystick up/down and left/right swapped
/* ---- Companion-only fields ---- */
uint8_t manual_add_contacts;
uint8_t telemetry_mode_base;
uint8_t telemetry_mode_loc;
uint8_t telemetry_mode_env;
uint32_t ble_pin;
uint8_t buzzer_quiet;
uint8_t autoadd_config;
uint8_t client_repeat; // 1 = offgrid mode (forward packets)
uint8_t path_hash_mode; // path mode 0-2
uint8_t autoadd_max_hops; // 0 = no limit, N = up to N-1 hops
uint8_t loop_detect; // LOOP_DETECT_{OFF,MINIMAL,MODERATE,STRICT}
char default_scope_name[31]; // companion: default flood scope region name ("" = null)
uint8_t default_scope_key[16]; // companion: default flood scope TransportKey
uint8_t ble_disabled; // 1 = BLE advertising off
uint8_t display_brightness; // 0 = default (100%), else 10100
uint8_t wake_on_msg; // 0 = don't wake display on message, 1 = wake (default)
uint16_t screen_off_secs; // 0 = default (Kconfig), else 5300
uint16_t auto_shutdown_mv; // low-batt auto-shutdown threshold; 0 = off, else 29004200
uint8_t v_contact_enabled; // v-contact (loopback admin chat via BLE/USB); 1 = on (default)
uint16_t v_battery_alert_mv; // 0 = alert off; 0xFFFF = board default (auto_shutdown+200); else mV
/* App-owned ContactInfo.flags byte for the v-contact. The v-contact never
* enters the contacts table, so it has no record to hold the flags the app
* sets via CMD_ADD_UPDATE_CONTACT -- bit 0 is the 'favourite' star, the
* upper bits are telemetry permissions. Kept here so a toggle survives
* reconnects and reboots instead of being echoed back as 0. */
uint8_t v_contact_flags;
};
/* Default prefs -- must match LoRaConfig.h defaults for radio interop. */
/* Range guards for prefs that came off flash.
*
* The atomic replace in every savePrefs() plus littlefs's own CRCs make a torn
* write impossible, so this is not about power loss — it is about a blob that
* is structurally intact and semantically wrong. Several of these fields make
* a node look bricked when they are: auto_shutdown_mv powers it off seconds
* after boot, ble_pin locks pairing out, and the char fields are stored as
* fixed-size blocks with no terminator in the file format, so an unterminated
* one runs every later %s off the end of the struct.
*
* Called at the end of each role's loadPrefs(). Fields whose whole range is
* legal (autoadd_config bitmask, discovery_mod_timestamp, the v_contact_*
* sentinels) are deliberately left alone. */
template <typename T>
static inline T clampPref(T v, T lo, T hi) { return v < lo ? lo : (v > hi ? hi : v); }
/* Floats need the NaN test spelled out: every comparison against NaN is false,
* so a plain clamp passes it straight through. */
template <typename T>
static inline T sanePrefFloat(T v, T lo, T hi, T fallback) {
if (v != v || v < lo || v > hi) return fallback;
return v;
}
/* A byte that is neither 0 nor 1 carries no user intent — take the default
* rather than clamping, which would round every garbage value up to "on". */
template <typename T>
static inline T saneBool(T v, T fallback) { return (v == 0 || v == 1) ? v : fallback; }
static inline void sanitizeNodePrefs(NodePrefs* p) {
p->node_name[sizeof(p->node_name) - 1] = '\0';
p->password[sizeof(p->password) - 1] = '\0';
p->guest_password[sizeof(p->guest_password) - 1] = '\0';
p->owner_info[sizeof(p->owner_info) - 1] = '\0';
p->default_scope_name[sizeof(p->default_scope_name) - 1] = '\0';
p->airtime_factor = sanePrefFloat(p->airtime_factor, 0.0f, 9.0f, 9.0f);
p->rx_delay_base = sanePrefFloat(p->rx_delay_base, 0.0f, 3600.0f, 0.0f);
p->adc_multiplier = sanePrefFloat(p->adc_multiplier, 0.0f, 30000.0f, 0.0f);
p->node_lat = sanePrefFloat(p->node_lat, -90.0, 90.0, 0.0);
p->node_lon = sanePrefFloat(p->node_lon, -180.0, 180.0, 0.0);
p->cr = clampPref<uint8_t>(p->cr, 5, 8);
p->tx_power_dbm = clampPref<int8_t>(p->tx_power_dbm, -9, 30);
#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM
if (p->tx_power_dbm > CONFIG_ZEPHCORE_MAX_TX_POWER_DBM) {
p->tx_power_dbm = (int8_t)CONFIG_ZEPHCORE_MAX_TX_POWER_DBM;
}
#endif
/* Booleans and enums fall back to a safe value rather than being clamped
* to their maximum: clamping is the wrong direction for anything that
* grants a permission, hides the node, or turns a radio mode on. A byte
* that is neither 0 nor 1 carries no user intent, so the fallback is
* simply the field's own default. */
p->client_repeat = saneBool<uint8_t>(p->client_repeat, 0);
p->manual_add_contacts = saneBool<uint8_t>(p->manual_add_contacts, 0);
p->multi_acks = saneBool<uint8_t>(p->multi_acks, 0);
p->buzzer_quiet = saneBool<uint8_t>(p->buzzer_quiet, 0);
p->gps_enabled = saneBool<uint8_t>(p->gps_enabled, 0);
p->rx_duty_cycle = saneBool<uint8_t>(p->rx_duty_cycle, 0);
p->leds_disabled = saneBool<uint8_t>(p->leds_disabled, 0);
p->meshtimesync = saneBool<uint8_t>(p->meshtimesync, 0);
p->cad_auto = saneBool<uint8_t>(p->cad_auto, 0);
p->allow_read_only = saneBool<uint8_t>(p->allow_read_only, 0);
p->powersaving_enabled = saneBool<uint8_t>(p->powersaving_enabled, 0);
p->display_rotate = saneBool<uint8_t>(p->display_rotate, 0);
p->input_rotate = saneBool<uint8_t>(p->input_rotate, 0);
/* Defaults that are on, not off. */
p->rx_boost = saneBool<uint8_t>(p->rx_boost, 1);
p->fem_rxgain = saneBool<uint8_t>(p->fem_rxgain, 1);
p->wake_on_msg = saneBool<uint8_t>(p->wake_on_msg, 1);
p->v_contact_enabled = saneBool<uint8_t>(p->v_contact_enabled, 1);
/* Never let a corrupt byte take the radio off the air or hide BLE — both
* remove the only ways left to fix the node. */
p->ble_disabled = saneBool<uint8_t>(p->ble_disabled, 0);
/* Permission and privacy enums: out of range means deny/withhold. */
if (p->telemetry_mode_base > TELEM_MODE_ALLOW_ALL) p->telemetry_mode_base = TELEM_MODE_DENY;
if (p->telemetry_mode_loc > TELEM_MODE_ALLOW_ALL) p->telemetry_mode_loc = TELEM_MODE_DENY;
if (p->telemetry_mode_env > TELEM_MODE_ALLOW_ALL) p->telemetry_mode_env = TELEM_MODE_DENY;
if (p->advert_loc_policy > ADVERT_LOC_PREFS) p->advert_loc_policy = ADVERT_LOC_NONE;
if (p->loop_detect > LOOP_DETECT_STRICT) p->loop_detect = LOOP_DETECT_MINIMAL;
if (p->path_hash_mode > 2) p->path_hash_mode = 0;
p->autoadd_max_hops = clampPref<uint8_t>(p->autoadd_max_hops, 0, 64);
p->flood_max_unscoped = clampPref<uint8_t>(p->flood_max_unscoped, 0, 64);
p->flood_max_advert = clampPref<uint8_t>(p->flood_max_advert, 0, 64);
p->cad_busycap = clampPref<uint8_t>(p->cad_busycap, 0, 90);
/* Neutral, not the boundary — a garbage offset is not a request to run
* the CAD staircase at one end of its range. */
if (p->cad_offset < CAD_OFFSET_MIN || p->cad_offset > CAD_OFFSET_MAX) p->cad_offset = 0;
if (p->probe_interval != 0 && p->probe_interval < 10) p->probe_interval = 10;
/* 6-digit BLE passkey — anything else is rejected at pairing time and
* leaves no way back in over BLE. */
if (p->ble_pin > 999999) p->ble_pin = 0;
/* Seconds; 0 = off. A year is already far past any sane setting. */
if (p->gps_interval > 31536000UL) p->gps_interval = 0;
/* 0 = board/Kconfig default on both, else the range the UI offers. */
if (p->display_brightness != 0) p->display_brightness = clampPref<uint8_t>(p->display_brightness, 10, 100);
if (p->screen_off_secs != 0) p->screen_off_secs = clampPref<uint16_t>(p->screen_off_secs, 5, 300);
/* 0 = off. Outside the cell range it either never fires or powers the
* node off the moment it boots — the one that looks like a dead device. */
if (p->auto_shutdown_mv != 0 &&
(p->auto_shutdown_mv < 2900 || p->auto_shutdown_mv > 4200)) {
#ifdef CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS
p->auto_shutdown_mv = CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS;
#else
p->auto_shutdown_mv = 0;
#endif
}
}
static inline void initNodePrefs(NodePrefs* prefs) {
memset(prefs, 0, sizeof(NodePrefs));
prefs->airtime_factor = 9.0f; /* Arduino formula: duty% = 100 / (af + 1) → 10% */
prefs->node_lat = 0.0;
prefs->node_lon = 0.0;
#ifdef CONFIG_ZEPHCORE_ADMIN_PASSWORD
strncpy(prefs->password, CONFIG_ZEPHCORE_ADMIN_PASSWORD, sizeof(prefs->password) - 1);
#else
strcpy(prefs->password, "password");
#endif
#ifdef CONFIG_ZEPHCORE_GUEST_PASSWORD
strncpy(prefs->guest_password, CONFIG_ZEPHCORE_GUEST_PASSWORD, sizeof(prefs->guest_password) - 1);
#endif
/* Radio params - MUST match LoRaConfig.h for interop with companion nodes */
prefs->freq = 869.618f; // LoRaConfig::FREQ_HZ / 1000000.0
prefs->bw = 62.5f; // LoRaConfig::BANDWIDTH
prefs->sf = 7; // LoRaConfig::SPREADING_FACTOR
prefs->cr = 5; // CR 4/5 (MeshCore uses 5-8 for CR 4/5 through 4/8)
#ifdef CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM
prefs->tx_power_dbm = CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM;
#else
prefs->tx_power_dbm = 22; // LoRaConfig::TX_POWER_DBM
#endif
prefs->disable_fwd = 0;
prefs->advert_interval = 0; // 0 = periodic local advert off; else minutes = value * 2
prefs->flood_advert_interval = 47; // hours
prefs->rx_delay_base = 0.0f;
prefs->tx_delay_factor = 0.5f;
prefs->direct_tx_delay_factor = 0.3f;
prefs->allow_read_only = 0;
prefs->multi_acks = 0;
prefs->flood_max = 64; // max hops for flood packets (0 = blocking all!)
prefs->flood_max_unscoped = 64; // un-scoped flood hop limit (defaults to flood_max)
prefs->flood_max_advert = 8; // ADVERT flood hop limit (upstream default)
prefs->interference_threshold = 0;
prefs->leds_disabled = 0; // LEDs on
prefs->powersaving_enabled = 0;
prefs->gps_enabled = 0;
prefs->gps_interval = 300; // 5 minutes
prefs->advert_loc_policy = ADVERT_LOC_NONE;
prefs->adc_multiplier = 0.0f;
prefs->rx_boost = 1; // Default to boosted RX for better sensitivity
/* Default ON: the FEM's chip-enable has always been asserted for RX on the
* boards that have one, so 1 is the historical behaviour and the only safe
* default -- 0 costs the FEM's RX gain (~16 dB on SKY66122). */
prefs->fem_rxgain = 1;
prefs->rx_duty_cycle = 0; // Default OFF — continuous RX for best reliability
prefs->_reserved_apc_enabled = 0; // reserved (was APC), see NodePrefs
prefs->_reserved_apc_margin = 0; // reserved (was APC), see NodePrefs
prefs->cad_auto = 1; // Default ON — adaptive staircase acts on probe stats
prefs->cad_offset = 0; // Start at family base detPeak (SF+13 on SX126x)
prefs->probe_interval = 15; // floor sample + CAD probe; staircase responds in ~1-2 h
prefs->cad_busycap = 25; // back off detPeak once >25% of TX attempts are deferred
prefs->wake_on_msg = 1; // Default ON — wake display when message arrives
prefs->display_rotate = 0; // Default OFF — panel in the stock case orientation
prefs->input_rotate = 0; // Default OFF — joystick axes as the board wires them
prefs->v_contact_enabled = 1; // Default ON — v-contact loopback admin chat (companion)
prefs->v_battery_alert_mv = 0xFFFF; // Sentinel: derive from board auto-shutdown threshold
}