Files
MeshCore/examples/companion_radio/NodePrefs.h
T
mikecarper 7088096abf Merge upstream dev through 428a95c64
Integrate fractional Companion timezones and conditional Bluetooth pages while preserving the existing preference byte layout, transactional saves, and Full transport selector. Add migration, rollback, invalid-value, and UI profile regressions.
2026-10-18 08:30:50 -07:00

357 lines
14 KiB
C++

#pragma once
#if defined(ENABLE_WIFI_INTERFACE) && !defined(WIFI_SSID)
#define WIFI_SSID ""
#endif
#include <Arduino.h>
#include <cstdint>
#include <helpers/BluetoothMac.h>
#include <helpers/CommonRadioPrefs.h>
#include <helpers/DynamicConfigSerializer.h>
#include <helpers/CompanionTrafficPrefs.h>
#include <helpers/CompanionTimezonePrefs.h>
#include "BluetoothName.h"
#define TELEM_MODE_DENY 0
#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags
#define TELEM_MODE_ALLOW_ALL 2
#define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1
// Increment when a release must migrate the persisted Companion device-power
// policy. Version 1 corrects the short-lived default-off regression: old
// preference files are upgraded to power saving on exactly once, after which
// an explicit user choice remains persistent.
#define COMPANION_POWERSAVING_POLICY_VERSION 1
// Companion preferences remain stored by DataStore's explicit, append-only
// binary field list. The adapter objects are appended after every persisted
// field, and node latitude/longitude deliberately remain separate DataStore
// arguments, so the established field offsets and on-device format do not move.
class CompanionNodePrefs {
public:
float airtime_factor = 0;
char node_name[32] = {};
float freq = 0;
uint8_t sf = 0;
uint8_t cr = 0;
uint8_t multi_acks = 0;
uint8_t manual_add_contacts = 0;
float bw = 0;
int8_t tx_power_dbm = 0;
uint8_t telemetry_mode_base = 0;
uint8_t telemetry_mode_loc = 0;
uint8_t telemetry_mode_env = 0;
float rx_delay_base = 0;
uint32_t ble_pin = 0;
uint8_t advert_loc_policy = 0;
uint8_t buzzer_quiet = 0;
uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval = 0; // seconds; 0 uses the 1-second default
uint8_t autoadd_config = 0; // bitmask for auto-add contacts config
uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode
uint8_t radio_fem_rxgain = 0; // external LoRa FEM RX gain
uint8_t client_repeat = 0;
uint8_t path_hash_mode = 0;
uint8_t autoadd_max_hops = 0;
char default_scope_name[31] = {};
uint8_t default_scope_key[16] = {};
// Append-only binary preference tail. Do not insert persisted fields above
// or between these entries without updating DataStore's compatibility map.
uint8_t radio_fem_rxgain_override = 0;
uint8_t vibe_quiet = 0;
uint8_t radio_fem_txgain = 0;
uint8_t rx_powersaving_enabled = 0;
uint32_t rx_ps_rx_us = 0;
uint32_t rx_ps_sleep_us = 0;
uint8_t rx_ps_level = 0;
uint8_t rx_ps_preamble = 0;
uint8_t powersaving_enabled = 0;
uint8_t wifi_enabled = 0;
uint8_t powersaving_policy_version = 0;
uint8_t usb_logging_enabled = 0; // 0=off, 1=separate where supported, 2=primary packet stream
char bluetooth_name[mesh::companion::BLUETOOTH_NAME_SIZE] = {};
uint16_t display_rotation_degrees = 0;
uint8_t cad_enabled = 0;
uint16_t cad_scan_timeout_ms = 0;
uint16_t cad_retry_delay_ms = 0;
uint16_t cad_max_duration_ms = 0;
uint8_t bluetooth_mac_mode = mesh::companion::BLUETOOTH_MAC_DEFAULT;
uint8_t bluetooth_mac[mesh::companion::BLUETOOTH_MAC_BYTES] = {};
uint8_t bluetooth_stealth_peer_type =
mesh::companion::BLUETOOTH_PEER_ADDRESS_NONE;
uint8_t bluetooth_stealth_peer[mesh::companion::BLUETOOTH_MAC_BYTES] = {};
uint8_t bluetooth_stealth_mode = mesh::companion::BLUETOOTH_STEALTH_OFF;
float tx_delay_factor = 0.5f;
float direct_tx_delay_factor = 0.2f;
uint8_t interference_threshold = 0;
uint8_t agc_reset_interval = 0; // seconds / 4
// Storage-only shadow for the published one-byte integer timezone slot.
// Fractional precision lives in the checked footer, never in this prefix.
int8_t tz_offset_legacy = 0;
#ifdef TBEAM_1W
char fan_mode[5] = "auto";
uint8_t fan_lo = 30;
uint8_t fan_hi = 36;
#endif
#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
char wifi_ssid[33] = {};
char wifi_pwd[64] = {};
#endif
// Keep these after every board-specific field: all existing prefs images
// must retain their exact offsets. Defaults reproduce the core Companion
// retry policy for older files that have no retry tail.
uint8_t flood_retry_attempts = 15;
uint8_t flood_retry_max_path = 1;
uint8_t flood_retry_group_max_path = 0xFF;
uint8_t flood_retry_advert_enabled = 1;
// Appended persisted field: old preferences images default to consent required.
uint8_t one_key_dm_enabled = 0;
// Appended after all existing fields so older images keep Bluetooth enabled.
uint8_t bluetooth_enabled = 1;
// Published tail: zero retains the board's legacy GPS sync cadence.
uint16_t gps_sync_interval_hours = 0;
// Debug verbosity is independent of the packet/output master switch. Keep
// this after the published GPS cadence so legacy images default to quiet.
uint8_t usb_debug_enabled = 0;
// Appended opt-in private-message response: 0=off, 1=no, 2=yes.
uint8_t lost_reply = 0;
#if defined(RAK_4631)
// A checked footer follows every existing on-flash field, never an insertion.
uint8_t peripherals_shared = 0; // applied at boot, not on a live CLI save
#endif
// Checked footer after all previously published fields/board footers.
uint8_t tx_media = mesh::RADIO_TX_AUTO;
uint8_t tx_ssh = mesh::RADIO_TX_AUTO;
private:
class RadioPrefs : public CommonRadioPrefs {
CompanionNodePrefs* _parent;
protected:
void structure() override {
def("freq", _parent->freq);
def("bw", _parent->bw);
def("sf", _parent->sf);
def("cr", _parent->cr);
def("cad", _parent->cad_enabled);
def("int_thr", _parent->interference_threshold);
def("rxgain", _parent->rx_boosted_gain);
def("fem_rxgain", _parent->radio_fem_rxgain);
def("fem_txgain", _parent->radio_fem_txgain);
def("tx", _parent->tx_power_dbm);
def("af", _parent->airtime_factor);
def("rxdelay", _parent->rx_delay_base);
def("f_txdelay", _parent->tx_delay_factor);
def("d_txdelay", _parent->direct_tx_delay_factor);
def("agc_int", _parent->agc_reset_interval);
def("hash_mode", _parent->path_hash_mode);
def("multi_ack", _parent->multi_acks);
def("usb_debug", _parent->usb_debug_enabled);
#ifdef TBEAM_1W
def("fan", _parent->fan_mode, sizeof(_parent->fan_mode));
def("fan_lo", _parent->fan_lo);
def("fan_hi", _parent->fan_hi);
#endif
}
public:
explicit RadioPrefs(CompanionNodePrefs* parent) : _parent(parent) { }
bool getByKey(const char* key, char* value, size_t max_len) override {
if (key != nullptr && strcmp(key, "usb_debug") == 0) {
if (value == nullptr || max_len == 0) return false;
if (max_len < 2) {
value[0] = 0;
return false;
}
value[0] = _parent->usb_debug_enabled == 1 ? '1' : '0';
value[1] = 0;
return true;
}
return CommonRadioPrefs::getByKey(key, value, max_len);
}
bool setByKey(const char* key, const char* value) override {
if (key != nullptr && strcmp(key, "usb_debug") == 0) {
if (value == nullptr
|| (strcmp(value, "0") != 0 && strcmp(value, "1") != 0)) {
return false;
}
_parent->usb_debug_enabled = value[0] == '1' ? 1 : 0;
markDirty();
return true;
}
return CommonRadioPrefs::setByKey(key, value);
}
float getFreq() const override { return _parent->freq; }
void setFreq(float value) override {
_parent->freq = value;
markDirty();
}
float getBandwidth() const override { return _parent->bw; }
void setBandwidth(float value) override {
_parent->bw = value;
markDirty();
}
uint8_t getSpreadFactor() const override { return _parent->sf; }
void setSpreadFactor(uint8_t value) override {
_parent->sf = value;
markDirty();
}
uint8_t getCodingRate() const override { return _parent->cr; }
void setCodingRate(uint8_t value) override {
_parent->cr = value;
markDirty();
}
float getAirtimeFactor() const override { return _parent->airtime_factor; }
void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); }
bool isCadEnabled() const override { return _parent->cad_enabled; }
void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); }
uint8_t getIntThresh() const override { return _parent->interference_threshold; }
void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); }
uint8_t getRxGain() const override { return _parent->rx_boosted_gain; }
void setRxGain(uint8_t value) override {
_parent->rx_boosted_gain = value;
markDirty();
}
int8_t getTxPower() const override { return _parent->tx_power_dbm; }
void setTxPower(int8_t value) override {
_parent->tx_power_dbm = value;
markDirty();
}
float getRxDelay() const override { return _parent->rx_delay_base; }
void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); }
uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; }
void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); }
uint8_t getHashMode() const override { return _parent->path_hash_mode; }
void setHashMode(uint8_t value) override {
_parent->path_hash_mode = value;
markDirty();
}
uint8_t getMultiAcks() const override { return _parent->multi_acks; }
void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); }
float getFloodTxDelay() const override { return _parent->tx_delay_factor; }
void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); }
float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; }
void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); }
uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; }
void setFEMRxGain(uint8_t value) override {
_parent->radio_fem_rxgain = value;
_parent->radio_fem_rxgain_override = 1;
markDirty();
}
uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; }
void setFEMTxGain(uint8_t value) override {
_parent->radio_fem_txgain = value;
markDirty();
}
};
// Runtime-only adapters must remain after the persisted field block above.
RadioPrefs radio;
DynamicConfigSerializer custom;
public:
// Keep the widened runtime value after the adapters: widening the historic
// one-byte member in place would move the compact ARM preference offsets.
float tz_offset = 0;
CompanionNodePrefs() : radio(this), custom(&radio) { }
// Snapshot the scalar/array prefix and the separate runtime timezone.
// Copying the complete object also copies adapter owner/fallback pointers
// and costs two large memberwise copies in the transactional loader. Keep
// both live adapters untouched and a scratch snapshot's adapters local.
bool copyPersistedValuesFrom(const CompanionNodePrefs& source) {
if (&source == this) return true;
const auto& last_value = tx_ssh;
static_assert(sizeof(tx_ssh) == sizeof(uint8_t),
"The last persisted traffic policy is one byte");
const uintptr_t first = reinterpret_cast<uintptr_t>(&airtime_factor);
const uintptr_t last = reinterpret_cast<uintptr_t>(&last_value);
if (last < first
|| last - first > sizeof(*this) - sizeof(tx_ssh)) {
return false;
}
const size_t size = static_cast<size_t>(last - first)
+ sizeof(tx_ssh);
const auto bounded = [size](const CompanionNodePrefs& values) -> bool {
const uintptr_t object = reinterpret_cast<uintptr_t>(&values);
const uintptr_t begin =
reinterpret_cast<uintptr_t>(&values.airtime_factor);
const uintptr_t end = reinterpret_cast<uintptr_t>(
&values.tx_ssh
);
const uintptr_t radio_begin = reinterpret_cast<uintptr_t>(&values.radio);
const uintptr_t custom_begin = reinterpret_cast<uintptr_t>(&values.custom);
// Subtractions happen only after the corresponding ordering checks.
// Never include either runtime adapter, even if this layout is changed.
return begin >= object && begin - object <= sizeof(values)
&& size <= sizeof(values) - static_cast<size_t>(begin - object)
&& end >= begin
&& end - begin == size - sizeof(values.tx_ssh)
&& radio_begin >= begin && size <= radio_begin - begin
&& custom_begin >= begin && size <= custom_begin - begin;
};
if (!bounded(*this) || !bounded(source)) return false;
memcpy(&airtime_factor, &source.airtime_factor, size);
tz_offset = source.tz_offset;
return true;
}
bool isRepeatEn() const { return client_repeat != 0; }
void setRepeatEn(bool enabled) { client_repeat = enabled ? 1 : 0; }
CommonRadioPrefs* getRadioPrefs() { return &radio; }
KeyValueStore* getCustom() { return &custom; }
bool isDirty() const {
return radio.isDirty() || custom.isDirty();
}
void clearDirty() {
radio.clearDirty();
custom.clearDirty();
}
};
inline bool migrateCompanionPowerSavingDefault(CompanionNodePrefs& prefs) {
if (prefs.powersaving_policy_version == COMPANION_POWERSAVING_POLICY_VERSION) {
return false;
}
prefs.powersaving_enabled = 1;
prefs.powersaving_policy_version = COMPANION_POWERSAVING_POLICY_VERSION;
return true;
}
inline void clearCompanionBluetoothStealthPeer(CompanionNodePrefs& prefs) {
if (mesh::companion::bluetoothStealthEnabled(prefs.bluetooth_stealth_mode)) {
prefs.bluetooth_stealth_mode = mesh::companion::BLUETOOTH_STEALTH_PAIRING;
}
prefs.bluetooth_stealth_peer_type =
mesh::companion::BLUETOOTH_PEER_ADDRESS_NONE;
memset(prefs.bluetooth_stealth_peer, 0, sizeof(prefs.bluetooth_stealth_peer));
}
// An on/off flag must not change the address policy or erase a valid bond
// when the same value is submitted repeatedly.
inline bool setCompanionBluetoothStealth(CompanionNodePrefs& prefs,
bool enabled) {
if (mesh::companion::bluetoothStealthEnabled(prefs.bluetooth_stealth_mode)
== enabled) {
return false;
}
prefs.bluetooth_stealth_mode = enabled
? mesh::companion::BLUETOOTH_STEALTH_PAIRING
: mesh::companion::BLUETOOTH_STEALTH_OFF;
clearCompanionBluetoothStealthPeer(prefs);
return true;
}