/* * 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 #include #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) // 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 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 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 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(p->cr, 5, 8); p->tx_power_dbm = clampPref(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(p->client_repeat, 0); p->manual_add_contacts = saneBool(p->manual_add_contacts, 0); p->multi_acks = saneBool(p->multi_acks, 0); p->buzzer_quiet = saneBool(p->buzzer_quiet, 0); p->gps_enabled = saneBool(p->gps_enabled, 0); p->gps_enabled_set = saneBool(p->gps_enabled_set, 0); p->auto_shutdown_set = saneBool(p->auto_shutdown_set, 0); p->rx_duty_cycle = saneBool(p->rx_duty_cycle, 0); p->leds_disabled = saneBool(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->meshtimesync = saneBool(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(p->cad_auto, 1); p->allow_read_only = saneBool(p->allow_read_only, 0); p->powersaving_enabled = saneBool(p->powersaving_enabled, POWERSAVING_DEFAULT); p->powersaving_set = saneBool(p->powersaving_set, 0); p->display_rotate = saneBool(p->display_rotate, 0); p->input_rotate = saneBool(p->input_rotate, 0); /* Defaults that are on, not off. */ p->rx_boost = saneBool(p->rx_boost, 1); p->fem_rxgain = saneBool(p->fem_rxgain, 1); p->wake_on_msg = saneBool(p->wake_on_msg, 1); p->v_contact_enabled = saneBool(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(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(p->autoadd_max_hops, 0, 64); p->flood_max = clampPref(p->flood_max, 0, 64); p->flood_max_unscoped = clampPref(p->flood_max_unscoped, 0, 64); p->flood_max_advert = clampPref(p->flood_max_advert, 0, 64); p->cad_busycap = clampPref(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(p->display_brightness, 10, 100); if (p->screen_off_secs != 0) p->screen_off_secs = clampPref(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->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 }