Files
ZephCore/zephcore/helpers/NodePrefs.h
T
ZephCore T096 ec2a07dfa9 leds: persist brightness across reboots and firmware updates
leds.brightness (PWM-capable boards) was RAM-only since it was added: it
always reset to ZEPHCORE_LED_DEFAULT_BRIGHTNESS_PCT (10%) on every boot,
regardless of what a user had last set it to.

New field led_brightness on NodePrefs, following the exact same
append-only pattern as leds_radio_mode/leds_hb_mode added just before it:
offset 311 in RepeaterDataStore.cpp (repeater/room-server/observer,
shared file), offset 174 in ZephyrDataStore.cpp (companion). Both are
backward compatible by construction: a prefs file written before this
field existed simply leaves it at the initNodePrefs() default (10%),
which is exactly the behaviour every already-deployed node has today --
no special-casing needed to keep "10% on a new/not-yet-upgraded node" as
the effective default.

CommonCLI.cpp's "set leds.brightness" handler now writes the prefs field
and calls savePrefs(), same as the leds.radio/leds.hb handlers right
above it; "get leds.brightness" now reads from prefs instead of the RAM
getter directly, for the same reason those two already do (always in
sync, prefs is the source of truth). The four boot call sites
(main_companion.cpp, main_repeater.cpp, main_room_server.cpp,
main_observer.cpp) each gained a zephcore_led_set_brightness_pct() call
seeding the RAM cache from the persisted value, mirroring the existing
leds_radio_mode/leds_hb_mode lines right next to them.

Verified building for heltec_t096/nrf52840 (companion) and rak3401_1watt
(repeater), the two roles/datastores touched by this change.
2026-10-08 18:27:00 +02:00

454 lines
23 KiB
C++
Raw 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>
#include "LoRaConfig.h"
/* LEDS_RADIO_* / LEDS_HB_* mode values for the leds_radio_mode and leds_hb_mode
* fields below. They live in led_gate.h because the heartbeat consumers are C
* and this header is C++; see the note there. */
#include "led_gate.h"
/* ── Accepted radio parameter ranges ──
* One definition for everything that accepts or validates a radio preset
* (CLIs, the companion protocol, the load-time checks): a setter must never
* accept what the loader throws away. FREQ spans both LR2021 RF paths;
* BW_MAX is per build (only the LR2021 has the wide bandwidths). */
#define ZC_RADIO_FREQ_MIN_MHZ 150.0f
#define ZC_RADIO_FREQ_MAX_MHZ 2500.0f
#define ZC_RADIO_BW_MIN_KHZ 7.0f
#ifdef CONFIG_ZEPHCORE_RADIO_LR2021
#define ZC_RADIO_BW_MAX_KHZ 1000.0f
#define ZC_RADIO_BW_RANGE_STR "7-1000"
#else
#define ZC_RADIO_BW_MAX_KHZ 500.0f
#define ZC_RADIO_BW_RANGE_STR "7-500"
#endif
#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 detPeak offset range (levels from the family base): how far
* the staircase or a manual offset may roam. Must match CAD_LEVEL_MIN/MAX in
* adapters/radio/radio_common.h. */
#define CAD_OFFSET_MIN (-8)
#define CAD_OFFSET_MAX 12
/* leds_disabled in the server prefs layout sits in the byte that held
* agc_reset_interval (offset 120), so it is magic-encoded: any other value is
* a legacy AGC interval and decodes to LEDs on. */
#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
/* Display timezone offset range, whole hours from UTC. Matches upstream
* MeshCore's `set tz.offset` so the same command works against both trees. */
#define TZ_OFFSET_MIN (-12)
#define TZ_OFFSET_MAX 14
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)
uint8_t leds_radio_mode; // LEDS_RADIO_* — activity LED source (0 = TX, as before)
uint8_t leds_hb_mode; // LEDS_HB_* — heartbeat LED behaviour (0 = all, as before)
uint8_t led_brightness; // 0-100, PWM-capable boards only; ZEPHCORE_LED_DEFAULT_BRIGHTNESS_PCT on a new node
// Power saving
uint8_t powersaving_enabled;
/* 1 = powersaving_enabled is a real choice. The field was stored but
* did nothing before ESP32 light sleep honoured it (2026-09-26), so a
* stored 0 without this marker takes the build's default once
* (powersaving_upgrade). */
uint8_t powersaving_set;
// GPS settings
uint8_t gps_enabled;
/* Servers only: 1 = gps_enabled is a real choice. Servers before
* slice 10 ran the GPS whatever gps_enabled said, so a prefs file
* without this marker is upgraded to gps_enabled = 1 once. */
uint8_t gps_enabled_set;
uint32_t gps_interval; // in seconds
/* Every board: duty intervals up to this (seconds) keep the GPS module's
* state between fixes, longer ones power it off fully. 0 = always off. */
uint32_t gps_standby_max;
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. Still read and written at
* their offsets in both legacy layouts; do not reuse. */
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 ON)
int8_t cad_offset; // operating detPeak offset from family base (CAD_OFFSET_MIN..MAX, -8..+12)
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; // faint-tolerance / airtime cap: raise detPeak once more than this %
// of QUIET-MOMENT PROBES trip on a faint signal (0 = off, default 15).
// Not "% of TX attempts deferred" -- probes are prefiltered to quiet
// moments, so strong traffic never enters the statistic at all.
/* 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, both roles. display_rotate turns the panel
* 180 degrees where the panel supports it; input_rotate swaps the
* joystick/D-pad axes and is separate on purpose. */
uint8_t display_rotate; // 1 = panel rotated 180 degrees
uint8_t input_rotate; // 1 = joystick up/down and left/right swapped
/* Whole-hour offset from UTC, -12..+14, applied only when formatting a clock
* for the local display. It must never reach RTCClock or any timestamp that
* leaves this node. */
int8_t tz_offset;
/* The family base detPeak that cad_offset was learned against (0 = never
* stored). Lets the firmware re-anchor the offset at boot when the driver's
* base table changes (LoRaRadio::setCadParams). */
uint8_t cad_base;
/* ---- 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 10–100
uint8_t wake_on_msg; // 0 = don't wake display on message, 1 = wake (default)
uint16_t screen_off_secs; // 0 = default (Kconfig), else 5–300
uint16_t auto_shutdown_mv; // low-batt auto-shutdown threshold; 0 = off, else 2900–4200
/* 1 = auto_shutdown_mv is a choice made after the default moved from
* 3300 to 3200 mV; a stored 3300 without it is the old default and
* becomes 3200 once (PrefsJson, the legacy loader). */
uint8_t auto_shutdown_set;
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;
/* The WiFi companion's network (upstream's wifi_ssid / wifi_pwd /
* wifi_enabled). Stored by every companion build, WiFi or not, so moving
* between firmware never drops a saved network. */
char wifi_ssid[33];
char wifi_pwd[64];
uint8_t wifi_enabled; // 1 = join wifi_ssid when set (upstream default)
};
/* Range guards for prefs that came off flash: a blob can be intact and
* semantically wrong, and several fields make a node look bricked then.
* Called by both prefs decoders. Fields whose whole range is legal are 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; }
/* Same idea as saneBool for a small enum: anything outside 0..max carries no
* user intent, so fall back to 0 — which every LEDS_* enum defines as the
* behaviour the firmware had before the setting existed. */
static inline uint8_t saneEnum(uint8_t v, uint8_t max) { return (v <= max) ? v : 0; }
/* The GPS duty interval, in the one range every setter and the GPS manager
* share: 0 = always on, else 10 s to 1 week (safely below the ms overflow). */
/* The auto-shutdown default before 2026-09-25 (now 3200 mV): a stored
* 3300 without auto_shutdown_set is migrated (auto_shutdown_upgrade). */
#define AUTO_SHUTDOWN_OLD_DEFAULT_MV 3300
static inline void auto_shutdown_upgrade(NodePrefs *p) {
#if defined(CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS) && CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS > 0
if (!p->auto_shutdown_set && p->auto_shutdown_mv == AUTO_SHUTDOWN_OLD_DEFAULT_MV) {
p->auto_shutdown_mv = CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS;
}
#endif
p->auto_shutdown_set = 1;
}
/* `powersaving` gates ESP32 light sleep (helpers/pm_esp32_wake.c). On a
* light-sleep build the default is on: those builds slept unconditionally
* before the switch existed, and the stored 0 carried no intent. Elsewhere
* it is upstream's default, off. */
#if defined(CONFIG_PM) && defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
#define POWERSAVING_DEFAULT 1
#else
#define POWERSAVING_DEFAULT 0
#endif
static inline void powersaving_upgrade(NodePrefs *p) {
if (!p->powersaving_set && p->powersaving_enabled == 0) {
p->powersaving_enabled = POWERSAVING_DEFAULT;
}
p->powersaving_set = 1;
}
#define GPS_INTERVAL_MIN_SEC 10u
#define GPS_INTERVAL_MAX_SEC 604800u
#define GPS_STANDBY_MAX_DEFAULT_SEC 3600u
static inline uint32_t clampGpsInterval(uint32_t sec) {
if (sec == 0) return 0;
if (sec < GPS_INTERVAL_MIN_SEC) return GPS_INTERVAL_MIN_SEC;
return (sec > GPS_INTERVAL_MAX_SEC) ? GPS_INTERVAL_MAX_SEC : sec;
}
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->wifi_ssid[sizeof(p->wifi_ssid) - 1] = '\0';
p->wifi_pwd[sizeof(p->wifi_pwd) - 1] = '\0';
p->wifi_enabled = saneBool(p->wifi_enabled, (uint8_t)1);
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);
/* Range matches the CLI (0.0-2.0). 0.0 is a legal value meaning "reactive
* backoff off" and must survive: this guard exists for NaN and corruption
* only. RepeaterDataStore used to coerce 0.0 -> 0.2 here, which silently
* undid the documented way to disable the feature on every boot. */
p->backoff_multiplier = sanePrefFloat(p->backoff_multiplier, 0.0f, 2.0f, 0.2f);
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->gps_enabled_set = saneBool<uint8_t>(p->gps_enabled_set, 0);
p->auto_shutdown_set = saneBool<uint8_t>(p->auto_shutdown_set, 0);
p->rx_duty_cycle = saneBool<uint8_t>(p->rx_duty_cycle, 0);
p->leds_disabled = saneBool<uint8_t>(p->leds_disabled, 0);
p->leds_radio_mode = saneEnum(p->leds_radio_mode, LEDS_RADIO_MAX);
p->leds_hb_mode = saneEnum(p->leds_hb_mode, LEDS_HB_MAX);
p->led_brightness = clampPref<uint8_t>(p->led_brightness, 0, 100);
p->meshtimesync = saneBool<uint8_t>(p->meshtimesync, 0);
/* Fallback must be the initNodePrefs() default (ON). It was 0, so a byte
* that was neither 0 nor 1 silently switched adaptive CAD off instead of
* restoring the default — left over from when the default was dry-run. */
p->cad_auto = saneBool<uint8_t>(p->cad_auto, 1);
p->allow_read_only = saneBool<uint8_t>(p->allow_read_only, 0);
p->powersaving_enabled = saneBool<uint8_t>(p->powersaving_enabled, POWERSAVING_DEFAULT);
p->powersaving_set = saneBool<uint8_t>(p->powersaving_set, 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;
/* Fallback must be the initNodePrefs() default (1 = 2-byte hashes). It was
* 0, so a corrupt byte silently dropped the node to 1-byte path hashes --
* a value no role defaults to, and one the CLI would not report as the
* default either. */
if (p->path_hash_mode > 2) p->path_hash_mode = 1;
p->autoadd_max_hops = clampPref<uint8_t>(p->autoadd_max_hops, 0, 64);
p->flood_max = clampPref<uint8_t>(p->flood_max, 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;
/* UTC, not a boundary zone, for a byte that carries no user intent. */
if (p->tz_offset < TZ_OFFSET_MIN || p->tz_offset > TZ_OFFSET_MAX) p->tz_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;
/* 0 is always-on, so a corrupt value clamps to the week, not to 0. */
p->gps_interval = clampGpsInterval(p->gps_interval);
if (p->gps_standby_max > GPS_INTERVAL_MAX_SEC) p->gps_standby_max = GPS_INTERVAL_MAX_SEC;
/* 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 */
prefs->freq = mesh::LoRaConfig::FREQ_MHZ;
prefs->bw = mesh::LoRaConfig::BANDWIDTH_KHZ;
prefs->sf = mesh::LoRaConfig::SPREADING_FACTOR;
prefs->cr = mesh::LoRaConfig::CODING_RATE; /* 5-8 = CR 4/5 through 4/8 */
prefs->tx_power_dbm = mesh::LoRaConfig::TX_POWER_DBM;
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;
/* Must match mesh::ContentionTracker::DEFAULT_BACKOFF_MULT. Left at the
* memset 0 (= backoff disabled) until now, with RepeaterDataStore coercing
* 0.0 -> 0.2 on load to paper over it — which also undid a deliberate 0.0.
* Companions never apply this (only Repeater/RoomServer call
* setBackoffMultiplier), so setting it here changes no existing node. */
prefs->backoff_multiplier = 0.2f;
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)
/* 2-byte path hashes (mode + 1 = hash size) for everything this node
* originates. ZephCore's default for every role; upstream ships 0. Only
* affects a factory-fresh node. */
prefs->path_hash_mode = 1; // 2-byte path hashes
prefs->interference_threshold = 0;
prefs->leds_disabled = 0; // LEDs on
prefs->leds_radio_mode = LEDS_RADIO_TX; // activity LED on transmit, as before
prefs->leds_hb_mode = LEDS_HB_ALL; // heartbeat + unread, as before
prefs->led_brightness = ZEPHCORE_LED_DEFAULT_BRIGHTNESS_PCT; // 10%, new node
prefs->powersaving_enabled = POWERSAVING_DEFAULT;
prefs->powersaving_set = 1;
prefs->gps_enabled = 0;
prefs->gps_interval = 300; // 5 minutes
prefs->gps_standby_max = GPS_STANDBY_MAX_DEFAULT_SEC;
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 = 15; // back off detPeak once >15% of quiet-moment probes trip on faint signals
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
prefs->wifi_enabled = 1; // as upstream: WiFi still stays off until an SSID is set
/* Companion-only default, set here so every caller of initNodePrefs() sees
* it. 0 means disabled and is a legal stored value. */
#ifdef CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS
prefs->auto_shutdown_mv = CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS;
prefs->auto_shutdown_set = 1;
#else
prefs->auto_shutdown_mv = 0;
#endif
}