#pragma once #include "Mesh.h" #include #include #include #include // For MAX_MQTT_SLOTS (used in NodePrefs struct layout) #include #include #include #include #include #include #include #include #include #include #include #include #if MESH_ENABLE_FLEET_CONTROL #include #endif #ifndef DEFAULT_CAD_ENABLED #define DEFAULT_CAD_ENABLED 0 #endif #ifndef DEFAULT_POWERSAVING_ENABLED #define DEFAULT_POWERSAVING_ENABLED 1 #endif #if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL) #include #endif #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) || defined(WITH_MQTT_BRIDGE) #define WITH_BRIDGE #endif #if defined(WITH_RS232_BRIDGE) && !defined(WITH_RS232_BRIDGE_UART) #define WITH_RS232_BRIDGE_UART 1 #endif #define ADVERT_LOC_NONE 0 #define ADVERT_LOC_SHARE 1 #define ADVERT_LOC_PREFS 2 static constexpr uint8_t DEFAULT_ADVERT_LOC_POLICY = ADVERT_LOC_PREFS; #define LOOP_DETECT_OFF 0 #define LOOP_DETECT_MINIMAL 1 #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 #define RETRY_PRESET_INFRA 0 #define RETRY_PRESET_ROOFTOP 1 #define RETRY_PRESET_MOBILE 2 #define RETRY_PRESET_CUSTOM 0xFF #define DIRECT_RETRY_INFRA_BASE_MS 275 #define DIRECT_RETRY_INFRA_COUNT 4 #define DIRECT_RETRY_INFRA_STEP_MS 150 #define DIRECT_RETRY_INFRA_MARGIN_X4 60 #define DIRECT_RETRY_ROOFTOP_BASE_MS 175 #define DIRECT_RETRY_ROOFTOP_COUNT 15 #define DIRECT_RETRY_ROOFTOP_STEP_MS 100 #define DIRECT_RETRY_ROOFTOP_MARGIN_X4 20 #define DIRECT_RETRY_MOBILE_BASE_MS 175 #define DIRECT_RETRY_MOBILE_COUNT 15 #define DIRECT_RETRY_MOBILE_STEP_MS 50 #define DIRECT_RETRY_MOBILE_MARGIN_X4 0 #define DIRECT_RETRY_RECENT_DEFAULT 1 #define FLOOD_RETRY_INFRA_COUNT 1 #define FLOOD_RETRY_INFRA_MAX_PATH 1 #define FLOOD_RETRY_ROOFTOP_COUNT 3 #define FLOOD_RETRY_ROOFTOP_MAX_PATH 2 #define FLOOD_RETRY_MOBILE_COUNT 15 #define FLOOD_RETRY_MOBILE_MAX_PATH 1 #define FLOOD_RETRY_ADVERT_DEFAULT 0 #define FLOOD_RETRY_GROUP_MAX_PATH_DEFAULT 1 #define BATTERY_ALERT_LOW_PERCENT_DEFAULT 20 #define BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT 10 #ifndef FLOOD_RETRY_PREFIX_SLOTS #define FLOOD_RETRY_PREFIX_SLOTS 8 #endif #ifndef FLOOD_RETRY_PREFIX_LEN #define FLOOD_RETRY_PREFIX_LEN 3 #endif #ifndef FLOOD_RETRY_BRIDGE_BUCKETS #define FLOOD_RETRY_BRIDGE_BUCKETS 6 #endif #ifndef FLOOD_RETRY_BUCKET_PREFIXES #define FLOOD_RETRY_BUCKET_PREFIXES 17 #endif #ifndef FLOOD_RETRY_IGNORE_PREFIXES #define FLOOD_RETRY_IGNORE_PREFIXES 8 #endif #ifndef FLOOD_RETRY_LIST_PREFIXES #define FLOOD_RETRY_LIST_PREFIXES ((FLOOD_RETRY_IGNORE_PREFIXES > FLOOD_RETRY_BUCKET_PREFIXES) ? FLOOD_RETRY_IGNORE_PREFIXES : FLOOD_RETRY_BUCKET_PREFIXES) #endif #ifndef FLOOD_RETRY_LIST_TEXT_MAX #define FLOOD_RETRY_LIST_TEXT_MAX (FLOOD_RETRY_LIST_PREFIXES * FLOOD_RETRY_PREFIX_LEN * 2 + FLOOD_RETRY_LIST_PREFIXES) #endif #ifndef COMMON_CLI_TMP_LEN #define COMMON_CLI_TMP_LEN ((FLOOD_RETRY_LIST_TEXT_MAX > (PRV_KEY_SIZE * 2 + 4)) ? FLOOD_RETRY_LIST_TEXT_MAX : (PRV_KEY_SIZE * 2 + 4)) #endif #ifndef FLOOD_CHANNEL_KEY_PREFIX_LEN #define FLOOD_CHANNEL_KEY_PREFIX_LEN 4 #endif #define FLOOD_CHANNEL_HOPS_ALL 0xFF #define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40 #define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30 #define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10 #define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10 #define DIRECT_RETRY_PREFS_MAGIC_0 0xD1 #define DIRECT_RETRY_PREFS_MAGIC_1 0x52 #define TELEMETRY_ACCESS_ALL 0 #define TELEMETRY_ACCESS_ACL 1 class NodePrefs : public ConfigSerializer { // persisted to file public: float airtime_factor = 0; char node_name[32] = {}; double node_lat = 0, node_lon = 0; char password[16] = {}; float freq = 0; int8_t tx_power_dbm = 0; uint8_t disable_fwd = 0; uint8_t advert_interval = 0; // minutes / 2 uint8_t flood_advert_interval = 0; // hours float rx_delay_base = 0; float tx_delay_factor = 0; char guest_password[16] = {}; float direct_tx_delay_factor = 0; uint32_t guard = 0; uint8_t sf = 0; uint8_t cr = 0; uint8_t allow_read_only = 0; uint8_t multi_acks = 0; float bw = 0; uint8_t flood_max = 0; uint8_t flood_max_unscoped = 0; uint8_t flood_max_advert = 0; uint8_t interference_threshold = 0; uint8_t agc_reset_interval = 0; // secs / 4 // Bridge settings uint8_t bridge_enabled = 0; // boolean uint16_t bridge_delay = 0; // milliseconds (default 500 ms) uint8_t bridge_pkt_src = 0; // 0 = logTx, 1 = logRx (default logRx) uint32_t bridge_baud = 0; // 9600, 19200, 38400, 57600, 115200 (default 115200) uint8_t bridge_channel = 0; // 1-13 (ESP-NOW only) char bridge_secret[16] = {}; // for XOR encryption of bridge packets (ESP-NOW only) // Power setting uint8_t powersaving_enabled = DEFAULT_POWERSAVING_ENABLED ? 1 : 0; // boolean uint8_t reboot_interval = 0; // hours, 0-255 (default 0=disable) // Gps settings uint8_t gps_enabled = 0; uint32_t gps_interval = 0; // seconds; 0 uses the 1-second default uint8_t advert_loc_policy = DEFAULT_ADVERT_LOC_POLICY; uint32_t discovery_mod_timestamp = 0; float adc_multiplier = 0; char owner_info[120] = {}; // NOTE: member order below matches mcarper/keymindCascade (upstream/dev order). // It is in-memory only - /com_prefs is read/written field-by-field in loadPrefsInt/ // savePrefs, whose canonical file order (identical to the flex fleet's through // offset 294, keymind retry tail at 295+) is what devices actually persist. uint8_t rx_boosted_gain = 0; // power settings uint8_t radio_fem_rxgain = 0; // LoRa FEM RX gain setting uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t loop_detect = 0; uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean) // LR2021 side-detector SFs are appended at /com_prefs offset 856. Their // in-memory placement here does not shift the established binary layout. uint8_t extra_sf[4] = {}; uint8_t radio_fem_txgain = 0; // LoRa FEM TX gain; persisted at /com_prefs offset 860 // Runtime USB packet output gate. Appended at /com_prefs offset 861 so // older images remain readable and merged standard builds keep the former // logging artifact's enabled-at-first-boot behavior. uint8_t usb_logging_enabled = 1; // USB diagnostics are opt-in independently of the packet output gate. // Appended after GPS sync cadence in /com_prefs, not beside usb_log. uint8_t usb_debug_enabled = 0; // Repeater-only opt-in for routed trace diagnostics while repeat is off. // Appended after usb_debug in /com_prefs; existing images stay disabled. uint8_t trace_when_repeat_off = 0; // Runtime UART choice for merged RS-232 repeater artifacts. Appended at // /com_prefs offset 862; single-UART builds keep their compiled port here. uint8_t bridge_uart = 0; // ESP-NOW bridge wire format. Appended at /com_prefs offset 863; zero keeps // existing installations on the original wrapped/checksummed/XOR format. uint8_t bridge_format = mesh::bridge::ESPNOW_FORMAT_WRAPPED; // Appended after the existing /com_prefs tail. Keep the preamble in the same // transaction as frequency/modulation; old images adopt /radio_profiles. uint16_t primary_radio_preamble = 0; // Full ESP32 images can carry another bridge and ESP-NOW simultaneously. Keep // ESP-NOW intent separately so either transport can be selected at runtime. // Sole merged ESP-NOW profiles also use this intent and mirror bridge_enabled // for their legacy single-bridge lifecycle and CLI alias. // Old preference images did not have this byte; they default to enabled to // retain the previous MQTT combined-Full behavior after an upgrade. Merged // default-off profiles require their appended profile marker instead. uint8_t espnow_bridge_enabled = 1; // Independent UART intent in MQTT-combined profiles. Non-MQTT UART roles // retain bridge_enabled as their legacy alias and mirror it on save. // Appended after the profile markers, never beside the legacy bridge byte. uint8_t rs232_bridge_enabled = 0; // Default standby is negotiated by a valid heartbeat reply. Stock peers // never arm it. Explicit force (2) also permits unconfirmed-link standby. // Stored in a checked append-only tail, never in the legacy bridge layout. uint8_t rs232_autosleep = 1; uint8_t rs232_heartbeat = 1; uint16_t rs232_idle_seconds = 300; // Appended after the ESP-NOW intent. Zero keeps the board's legacy cadence. uint16_t gps_sync_interval_hours = 0; uint8_t retry_preset = 0; uint8_t direct_retry_attempts = 0; uint16_t direct_retry_base_ms = 0; uint16_t direct_retry_step_ms = 0; uint16_t direct_retry_snr_margin_x4 = 0; int8_t direct_retry_cr4_snr_x4 = 0; int8_t direct_retry_cr5_snr_x4 = 0; int8_t direct_retry_cr7_snr_x4 = 0; int8_t direct_retry_cr8_snr_x4 = 0; uint8_t direct_retry_enabled = 0; uint8_t direct_retry_cr_enabled = 0; uint8_t direct_retry_prefs_magic[2] = {}; uint8_t flood_retry_attempts = 0; uint8_t flood_retry_max_path = 0; uint8_t flood_retry_prefixes[FLOOD_RETRY_PREFIX_SLOTS][FLOOD_RETRY_PREFIX_LEN] = {}; uint8_t flood_retry_bridge_enabled = 0; uint8_t flood_retry_bridge_buckets[FLOOD_RETRY_BRIDGE_BUCKETS][FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN] = {}; uint8_t flood_retry_ignore_prefixes[FLOOD_RETRY_IGNORE_PREFIXES][FLOOD_RETRY_PREFIX_LEN] = {}; uint8_t flood_retry_advert_enabled = 0; uint8_t battery_alert_enabled = 0; uint8_t battery_alert_low_percent = 0; uint8_t battery_alert_critical_percent = 0; uint8_t direct_retry_recent_enabled = 0; uint8_t flood_channel_data_enabled = 0; // Retains the removed flood.channel.block byte at its established file // offset so every following preference remains upgrade-compatible. uint8_t legacy_flood_channel_block_max_hops = 0; uint8_t flood_channel_data_max_hops = 0; uint8_t telemetry_access = 0; uint8_t ota_channel = 0; // WiFi OTA release channel: 0=native, 1=prod, 2=beta // NOTE: observer settings (MQTT/WiFi/timezone/SNMP/alert) were moved out of // NodePrefs into MQTTPrefs (persisted to /mqtt_prefs) so this struct stays // aligned with upstream (plus the keymind retry/flood/telemetry tail above). See // helpers/MQTTPrefs.h. #if defined(ENABLE_OTA) // OTA config (persisted; synced to OtaContext on load, written on change). 0 = conservative defaults. uint8_t ota_autofetch = 0; // OtaManager AUTOFETCH_* (0=off, 1=any-compatible, 2=signed-only) uint8_t ota_autoinstall = 0; // OtaContext AUTOINSTALL_* (0=off, 1=trusted-only) uint8_t ota_signer_count = 0; // # of allowlisted signer pubkeys below uint8_t ota_signers[4][32] = {}; // trusted Ed25519 signer pubkeys (== MAX_OTA_SIGNERS) uint16_t ota_checkpoint_blocks = 0; // resume checkpoint cadence (blocks); 0=never. Default 4 (runtime-tunable) uint16_t ota_advert_interval = 0; // OTA beacon re-advertise cadence (mins); 0=off. Default 1440 (24h, tunable) uint8_t ota_max_hops = 0; // OTA flood reach in hops; 0=direct only. Default 3 (runtime-tunable) #endif uint8_t rx_powersaving_enabled = 0; // boolean uint32_t rx_ps_rx_us = 0; uint32_t rx_ps_sleep_us = 0; uint8_t rx_ps_level = 0; // 0 = manual/explicit us timings; 1..10 = level-derived (auto-retunes on SF/BW change) uint8_t rx_ps_preamble = 0; // 0 = auto (derive from SF); else 16 or 32 = explicit override for level calc char battery_alert_region[31] = {}; // named scope for low-battery floods; empty = no alert scope uint8_t flood_retry_group_max_path = 0; // PAYLOAD_TYPE_GRP_DATA retry path gate; 0xFF = use only the general gate uint8_t rx_watchdog_enabled = mesh::RepeaterRadioTiming::DEFAULT_RX_WATCHDOG_ENABLED; uint8_t system_watchdog_enabled = 0; // nRF52 main-loop hardware watchdog (boolean; default on) #if defined(RAK_4631) // Checked append-only file footer; never shifts existing preference offsets. uint8_t peripherals_shared = 0; // 0=GPS-only, 1=shared; applied only at boot #endif private: class RadioPrefs : public CommonRadioPrefs { NodePrefs* _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); } public: explicit RadioPrefs(NodePrefs* parent) : _parent(parent) { } // CommonRadioPrefs interface float getFreq() const override { return _parent->freq; } void setFreq(float f) override { _parent->freq = f; markDirty(); } float getBandwidth() const override { return _parent->bw; } void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } uint8_t getSpreadFactor() const override { return _parent->sf; } void setSpreadFactor(uint8_t sf) override { _parent->sf = sf; markDirty(); } uint8_t getCodingRate() const override { return _parent->cr; } void setCodingRate(uint8_t cr) override { _parent->cr = cr; 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 g) override { _parent->rx_boosted_gain = g; markDirty(); } int8_t getTxPower() const override { return _parent->tx_power_dbm; } void setTxPower(int8_t dbm) override { _parent->tx_power_dbm = dbm; 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 m) override { _parent->path_hash_mode = m; 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 g) override { _parent->radio_fem_rxgain = g; markDirty(); } uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; } void setFEMTxGain(uint8_t g) override { _parent->radio_fem_txgain = g; markDirty(); } }; RadioPrefs radio; class BridgePrefs : public ConfigSerializer { NodePrefs* _parent; protected: void structure() override { def("en", _parent->bridge_enabled); def("delay", _parent->bridge_delay); def("src", _parent->bridge_pkt_src); def("baud", _parent->bridge_baud); def("uart", _parent->bridge_uart); def("ch", _parent->bridge_channel); def("secret", _parent->bridge_secret, sizeof(_parent->bridge_secret)); def("format", _parent->bridge_format); def("usb_log", _parent->usb_logging_enabled); def("usb_dbg", _parent->usb_debug_enabled); #ifdef WITH_RS232_BRIDGE def("rs232_heartbeat", _parent->rs232_heartbeat); def("rs232_autosleep", _parent->rs232_autosleep); def("rs232_idle", _parent->rs232_idle_seconds); #endif } public: explicit BridgePrefs(NodePrefs* parent) : _parent(parent) { } }; BridgePrefs bridge; class GPSPrefs : public ConfigSerializer { NodePrefs* _parent; protected: void structure() override { def("en", _parent->gps_enabled); def("int", _parent->gps_interval); def("sync_hours", _parent->gps_sync_interval_hours); def("adv_loc", _parent->advert_loc_policy); } public: explicit GPSPrefs(NodePrefs* parent) : _parent(parent) { } }; GPSPrefs gps; class PowerPrefs : public ConfigSerializer { NodePrefs* _parent; protected: void structure() override { def("adc_mult", _parent->adc_multiplier); def("pwr_sav_en", _parent->powersaving_enabled); } public: explicit PowerPrefs(NodePrefs* parent) : _parent(parent) { } }; PowerPrefs power; class RepeatPrefs : public ConfigSerializer { NodePrefs* _parent; protected: void structure() override { def("disable", _parent->disable_fwd); def("trace_off", _parent->trace_when_repeat_off); def("f_max", _parent->flood_max); def("f_max_uns", _parent->flood_max_unscoped); def("f_max_adv", _parent->flood_max_advert); def("loop", _parent->loop_detect); } public: explicit RepeatPrefs(NodePrefs* parent) : _parent(parent) { } }; RepeatPrefs repeat; class RoomPrefs : public ConfigSerializer { NodePrefs* _parent; protected: void structure() override { def("rd_only", _parent->allow_read_only); } public: explicit RoomPrefs(NodePrefs* parent) : _parent(parent) { } }; RoomPrefs room; DynamicConfigSerializer custom; protected: void structure() override { def("name", node_name, sizeof(node_name)); def("pass", password, sizeof(password)); def("guest", guest_password, sizeof(guest_password)); def("owner", owner_info, sizeof(owner_info)); def("adv_int", advert_interval); def("f_adv_int", flood_advert_interval); def("lat", node_lat); def("lon", node_lon); def("disc_mod", discovery_mod_timestamp); def("ota_ch", ota_channel); // WiFi OTA release channel selector def("radio", radio); def("bridge", bridge); def("gps", gps); def("repeat", repeat); def("room", room); def("power", power); def("custom", custom); } public: NodePrefs() : ConfigSerializer(), radio(this), bridge(this), gps(this), power(this), repeat(this), room(this), custom(&radio) { } CommonRadioPrefs* getRadioPrefs() { return &radio; } KeyValueStore* getCustom() { return &custom; } bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty() || custom.isDirty(); } void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); custom.clearDirty(); } void markUnsaved() { markDirty(); } }; #ifdef WITH_MQTT_BRIDGE static_assert(MQTT_PREFS_SLOT_COUNT == MAX_MQTT_SLOTS, "MQTT prefs layout and slot count must change together"); // Observer settings captured from the trailing block of an old-format /com_prefs // (fork firmware that predates the NodePrefs -> MQTTPrefs split). loadPrefsInt() // fills this in when it detects the old file layout; loadMQTTPrefs() then applies // the values one-time if the loaded /mqtt_prefs predates the appended observer // fields, so SNMP/watchdog/alert config survives the firmware upgrade. struct LegacyObserverTail { bool valid = false; uint8_t snmp_enabled; char snmp_community[24]; uint8_t radio_watchdog_minutes; uint8_t alert_enabled; char alert_psk_hex[33]; uint16_t alert_wifi_minutes; uint16_t alert_mqtt_minutes; uint16_t alert_min_interval_min; char alert_hashtag[24]; char alert_region[31]; }; #endif class CommonCLICallbacks #if MESH_ENABLE_FLEET_CONTROL : public mesh::FleetReplyHooks #endif { public: virtual bool supportsFleetControl() const { return false; } // True only after an actual NTP response this boot, not a plausible RTC. virtual bool managementNtpSynced() const { return false; } virtual mesh::Radio* getProfileRadio() { return nullptr; } virtual mesh::FloodAdvertLimiter* getFloodAdvertLimiter() { return nullptr; } // Ordinary CommonCLI setters mutate NodePrefs and therefore save only the // common image. Observer setters explicitly request Observer; only // cross-file migration and mixed-owner setters request Both. virtual void savePrefs( PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common) = 0; // Called only after a CLI command successfully changes the RTC. Roles that // derive time from untrusted radio traffic can use this to prefer the manual // value for the remainder of the boot. virtual void onManualClockSet() { } // GPIO timers use the active authenticated CLI client as a deduplication // source and to route the asynchronous final-transition report. Local CLI // commands keep the default source of zero and do not get a network report. virtual uint32_t getUserGpioRequestSource() const { return 0; } virtual void onUserGpioTimerScheduled(uint8_t /*pin*/, uint32_t /*request_id*/) { } virtual void onUserGpioTimerCancelled(uint8_t /*pin*/) { } virtual void onUserGpioTimerCompleted(uint8_t /*pin*/, uint8_t /*state*/, uint32_t /*request_id*/) { } virtual const char* getFirmwareVer() = 0; virtual const char* getBuildDate() = 0; virtual const char* getRole() = 0; virtual bool formatFileSystem() = 0; virtual void sendSelfAdvertisement(int delay_millis, bool flood) = 0; virtual void updateAdvertTimer() = 0; virtual void updateFloodAdvertTimer() = 0; virtual void setLoggingOn(bool enable) = 0; virtual void eraseLogFile() = 0; virtual void dumpLogFile() = 0; virtual bool setTxPower(int8_t power_dbm) = 0; virtual void formatNeighborsReply(char *reply) = 0; // A zero-length prefix clears the whole table; pubkey may be null then. // Nonempty prefixes must contain 1..PUB_KEY_SIZE bytes. virtual void removeNeighbor(const uint8_t* pubkey, int key_len) { // no op by default }; virtual void formatStatsReply(char *reply) = 0; virtual void formatRadioStatsReply(char *reply) = 0; virtual void formatRadioDiagReply(char *reply) { strcpy(reply, "Not supported"); } virtual void formatPacketStatsReply(char *reply) = 0; virtual void formatRecentRepeatersReply(char *reply, int page, const uint8_t* search_prefix = NULL, uint8_t search_prefix_len = 0) { (void)page; (void)search_prefix; (void)search_prefix_len; if (reply != NULL) reply[0] = 0; } virtual bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { (void)prefix; (void)prefix_len; (void)snr_x4; return false; } virtual void clearRecentRepeaters() { } // Basic retry controls are portable across roles (enable/count/timing, // flood count/path/advert, and the current-CR retry progression). Advanced // controls require the repeater's recent-SNR and bridge-routing tables. virtual bool supportsBasicRetryConfig() const { return false; } virtual bool supportsAdvancedRetryConfig() const { return false; } virtual void onRetryConfigChanged() { } virtual mesh::LocalIdentity& getSelfId() = 0; virtual bool saveIdentity(const mesh::LocalIdentity& new_id) = 0; virtual void clearStats() = 0; virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) = 0; virtual uint32_t getTempRadioDurationSeconds() const { return 0; } virtual void appendTempRadioTimingNote(char* reply, size_t size, uint32_t seconds) const { } // Cancel pending/active temporary-radio windows and restore the saved tuple // after the command reply has drained on the current channel. virtual bool scheduleNormalRadio() { return false; } #if defined(ENABLE_OTA) virtual bool isTempRadioActive() const { return false; } #endif virtual void addScheduledRadioParams(bool temporary, float freq, float bw, uint8_t sf, uint8_t cr, uint32_t start_time, uint32_t end_time, char* reply, uint16_t preamble = 0) { (void)preamble; (void)temporary; (void)freq; (void)bw; (void)sf; (void)cr; (void)start_time; (void)end_time; strcpy(reply, "Error: unsupported"); } virtual void formatScheduledRadioParams(bool temporary, const char* selector, char* reply) { (void)temporary; (void)selector; strcpy(reply, "Error: unsupported"); } virtual void deleteScheduledRadioParams(bool temporary, const char* selector, char* reply) { (void)temporary; (void)selector; strcpy(reply, "Error: unsupported"); } virtual void startRegionsLoad() { // no op by default } virtual bool saveRegions() { return false; } virtual void onDefaultRegionChanged(const RegionEntry* r) { // no op by default } virtual bool setBridgeState(bool enable) { // no op by default (void)enable; return false; }; virtual bool restartBridge() { // no op by default return false; }; virtual bool isBridgeRunning() const { return false; } // Most roles have one bridge, so the transport-specific methods retain the // legacy bridge behavior by default. Combined bridge roles override // them to let each transport be started, stopped, and queried independently. virtual bool setMqttBridgeState(bool enable) { return setBridgeState(enable); } // CLI disable may acknowledge before worker-owned TLS teardown finishes. // Ownership barriers continue to use the synchronous state setter. virtual bool requestMqttBridgeStop() { return setMqttBridgeState(false); } virtual bool isMqttBridgeStopping() { return false; } virtual bool setEspNowBridgeState(bool enable) { return setBridgeState(enable); } virtual bool setRs232BridgeState(bool enable) { return setBridgeState(enable); } virtual bool restartMqttBridge() { return restartBridge(); } virtual bool restartEspNowBridge() { return restartBridge(); } virtual bool restartRs232Bridge() { return restartBridge(); } virtual bool isEspNowBridgeRunning() { return isBridgeRunning(); } virtual bool isRs232BridgeRunning() const { return isBridgeRunning(); } virtual void formatRs232Status(char* reply) { strcpy(reply, "RS232 unavailable"); } virtual void restartBridgeSlot(int slot) { // Default: fall back to full restart restartBridge(); }; // Schedule a pull-OTA firmware update to run shortly (from the app loop), after // the "Beginning update..." CLI reply has been transmitted. Deferred because the // flash blocks the loop and then reboots, so it can't run inline with the reply. // Returns true if scheduled. Default: not supported. virtual bool beginDeferredOtaUpdate() { return false; }; // Browser-based configuration portal. ESP32 infrastructure roles override // these; force_ap=true asks for the captive SoftAP even when WiFi is set. virtual bool startWebConfig(bool force_ap, char* reply) { (void)force_ap; (void)reply; return false; }; virtual bool stopWebConfig(char* reply) { (void)reply; return false; }; virtual bool stopWebConfigForOTA(char* reply) { (void)reply; return false; }; virtual bool isWebConfigActive() const { return false; }; virtual bool setWebUIEnabled(bool enabled, char* reply) { (void)enabled; (void)reply; return false; }; virtual bool getWebUIStatus(char* reply) const { (void)reply; return false; }; virtual bool usesCanonicalWiFi() const { return false; } virtual bool getWiFiSSID(char* reply) const { (void)reply; return false; }; virtual uint8_t wirelessCommandSource(uint32_t sender_timestamp) const { if (!sender_timestamp) return 0; #if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW return mesh::wireless::EspNow; #else return mesh::wireless::Independent; // Authenticated remote CLI over LoRa. #endif } virtual bool getWiFiPassword(char* reply) const { (void)reply; return false; }; virtual bool getWiFiStatus(char* reply) const { (void)reply; return false; }; virtual bool getWiFiPowerSave(char* reply) const { (void)reply; return false; }; virtual bool getWiFiCLI(char* reply) const { (void)reply; return false; }; virtual bool setWiFiSSID(const char* value, char* reply) { (void)value; (void)reply; return false; }; virtual bool setWiFiPassword(const char* value, char* reply) { (void)value; (void)reply; return false; }; virtual bool setWiFiPowerSave(const char* value, char* reply) { (void)value; (void)reply; return false; }; virtual bool setWiFiCLI(const char* value, char* reply) { (void)value; (void)reply; return false; }; virtual int getQueueSize() { return 0; // no op by default }; virtual bool syncMqttNtp() { return false; // WITH_MQTT_BRIDGE builds override }; virtual bool isMqttBridgeRunning() { return false; }; // Probe all configured NTP servers for connectivity (verbose=serial console gets a // detailed table; otherwise reply gets a compact " ok|fail" list). virtual bool runMqttNtpDiag(char* reply, size_t reply_size, bool verbose) { return false; // WITH_MQTT_BRIDGE builds override }; virtual bool setRxBoostedGain(bool enable) { return false; // CommonCLI reports unsupported if not overridden by wrapper }; #if defined(USE_LR2021) virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) { return false; // Override in wrapper } #endif virtual void recalibrateNoiseFloor() { // no op by default for radios without a noise-floor estimator }; // Fault-alert channel hooks (see NodePrefs::alert_*). The default no-op // implementations keep CLI commands harmless on builds that don't wire up // an AlertReporter. virtual void onAlertConfigChanged() { // no op by default } virtual bool sendAlertText(const char* /*text*/) { return false; // no op by default } // Resolve the TransportKey scope to use for outgoing fault-alert floods. // Implementations should consult NodePrefs::alert_region first (look up via // RegionMap), then fall back to the repeater's default_scope, then return // false if neither yields a usable key. AlertReporter falls back to an // unscoped flood when this returns false. virtual bool resolveAlertScope(TransportKey& /*dest*/) { return false; // no op by default } virtual bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) { return !enable; }; virtual bool supportsRxPowerSavingRfRxDisable() const { return false; } virtual bool setRxPowerSavingRfRxDisabled(bool disabled) { (void)disabled; return false; } virtual bool isRxPowerSavingRfRxDisabled() const { return false; } virtual void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) { *soft = 0; *hard = 0; }; }; #ifdef WITH_MQTT_BRIDGE namespace MQTTPrefsAtomicStore { class LegacyUpgradeGate; } #endif namespace mesh { class ManagementReporter; struct DataRouteState; class BeaconManager; } class CommonCLI { mesh::ManagementReporter* _management = nullptr; mesh::DataRouteState* _data_route = nullptr; mesh::Mesh* _management_mesh = nullptr; FILESYSTEM* _management_fs = nullptr; uint64_t _management_uptime_ms = 0; uint32_t _management_last_ms = 0; bool handleManagementCommand(char* command, char* reply); bool handleDataTxCommand(char* command, char* reply); mesh::RTCClock* _rtc; NodePrefs* _prefs; CommonCLICallbacks* _callbacks; mesh::MainBoard* _board; #if defined(WITH_ESPNOW_BRIDGE) \ || (defined(WITH_MQTT_BRIDGE) && defined(LIGHTWEIGHT_WIFI_OTA)) // Browser OTA restores only transports it actually paused, not saved // defaults suspended by an unconfigured first-boot setup session. bool _wifi_ota_resume_mqtt = false; bool _wifi_ota_resume_espnow = false; #endif #if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL) UserGpio _user_gpio; #endif SensorManager* _sensors; RegionMap* _region_map; ClientACL* _acl; char tmp[COMMON_CLI_TMP_LEN]; #if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) bool _sdcard_format_recorded = false; bool _sdcard_erase_recorded = false; uint32_t _sdcard_format_at = 0; uint32_t _sdcard_erase_at = 0; #endif #ifdef WITH_MQTT_BRIDGE MQTTPrefs _mqtt_prefs; LegacyObserverTail _legacy_tail; // /mqtt_prefs is newer, corrupt, or temporarily unreadable. The in-memory prefs // run on defaults and saveMQTTPrefs() must not overwrite the source file. bool _mqtt_prefs_hold = false; // CommonCLICallbacks::savePrefs() has a stable void ABI. Observer setters use // these synchronous result latches to learn whether the callback's nested // CommonCLI::savePrefs(fs, Observer) transaction actually committed. bool _observer_save_result_known = false; bool _observer_save_succeeded = false; #endif bool _com_prefs_needs_upgrade = false; // old-format legacy prefs detected; rewrite once after load bool _common_save_result_known = false; bool _common_save_succeeded = false; uint32_t _prefs_save_failures = 0; mesh::RadioProfileCLI _radio_profiles; #if MESH_ENABLE_TX_BEACONS mesh::BeaconManager* _beacons = nullptr; void beginBeacons(); void loopBeacons(); void suspendBeacons(); bool handleBeaconCommand(const char* command, char* reply, size_t capacity); #endif mesh::MorseReplyHandoff _morse_reply; #if MESH_ENABLE_FLEET_CONTROL mesh::FleetChannel* _fleet_channel = nullptr; // allocate only when enrolled bool handleFleetCommand(const char* command, char* reply); #endif mesh::RTCClock* getRTCClock() { return _rtc; } void savePrefs( PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common); bool trySavePrefs( PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common); bool saveObserverPrefs(); void loadPrefsInt(FILESYSTEM* _fs, const char* filename); #ifdef WITH_MQTT_BRIDGE bool recoverCommonPrefsFiles(FILESYSTEM* fs); bool saveCommonPrefsImageAtomically(FILESYSTEM* fs); void loadMQTTPrefs(FILESYSTEM* fs, MQTTPrefsAtomicStore::LegacyUpgradeGate* legacy_upgrade); bool saveMQTTPrefs(FILESYSTEM* fs); #endif #if defined(ENABLE_OTA) void syncOtaConfigFromPrefs(); // persisted OTA policy + signer allowlist -> idle/active OtaContext #endif void handleRegionCmd(char* command, char* reply); void handleGetCmd(uint32_t sender_timestamp, char* command, char* reply); void handleSetCmd(uint32_t sender_timestamp, char* command, char* reply); void handleDelCmd(char* command, char* reply); #if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) bool handleSdCardGetCmd(const char* config, char* reply); bool handleSdCardSetCmd(const char* config, char* reply); #endif // Observer/MQTT/WiFi/timezone/alert/SNMP CLI handling lives in the fork-owned // CommonCLI_Observer.cpp to keep these branches out of the upstream-tracked // CommonCLI.cpp. Each returns true if it recognized (handled) the command, or // false to fall through to the base get/set parsing. bool handleObserverSetCmd(uint32_t sender_timestamp, const char* config, char* reply); bool handleObserverGetCmd(uint32_t sender_timestamp, const char* config, char* reply); // Observer-only top-level commands (ota check/update, tls.bundletest, alert test) // also live in CommonCLI_Observer.cpp; returns true if it handled the command. bool handleObserverCommand(uint32_t sender_timestamp, char* command, char* reply); public: void beginManagement(mesh::Mesh& mesh, FILESYSTEM* fs); void loopManagement(); // Shared delivery configuration for scheduled/background data producers. // Fresh installs use a zero-hop direct path and automatic region scope; // producers retain independent disabled-by-default schedules. bool getDataTxPath(const uint8_t*& path, uint8_t& path_len) const; bool resolveDataTxScope(TransportKey& scope, char* resolved_name = nullptr, size_t resolved_name_size = 0, bool* ambiguous = nullptr) const; // True if a saved shared route already takes precedence or the legacy path // was committed. False means migration failed and reporting must stop. bool adoptLegacyDataTxPath(const uint8_t* path, uint8_t path_len); bool setDataTxPath(const char* spec, char* reply, size_t reply_size); bool saveCommonPrefs(); bool savePrimaryRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, uint16_t preamble); mesh::RadioProfileCLI& radioProfiles() { return _radio_profiles; } const mesh::RadioProfileCLI& radioProfiles() const { return _radio_profiles; } mesh::MorseReplyHandoff& morseReply() { return _morse_reply; } const mesh::MorseReplyHandoff& morseReply() const { return _morse_reply; } #if MESH_ENABLE_FLEET_CONTROL mesh::FleetChannel* fleetChannel() { return _fleet_channel; } #endif static bool calculateRxPowerSavingLevel(uint32_t level, uint8_t sf, float bw, uint32_t preamble, uint32_t* rx_us, uint32_t* sleep_us); static bool recalculateRxPowerSavingFromLevel(NodePrefs* prefs); CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks) : _rtc(&rtc), _prefs(prefs), _callbacks(callbacks), _board(&board), #if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL) _user_gpio(board), #endif _sensors(&sensors), _region_map(®ion_map), _acl(&acl) { } void loadPrefs(FILESYSTEM* _fs); void savePrefs( FILESYSTEM* _fs, PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Both); void loop(); bool hasActiveUserGpioTimer() const; void handleCommand(uint32_t sender_timestamp, char* command, char* reply); mesh::MainBoard* getBoard() { return _board; } uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data); #ifdef WITH_MQTT_BRIDGE // Observer config (MQTT/WiFi/timezone/SNMP/alert), persisted to /mqtt_prefs. // Exposed so the app can hand it to MQTTBridge/AlertReporter, which read these // fields directly (they no longer live in NodePrefs). MQTTPrefs* getObserverPrefs() const { return const_cast(&_mqtt_prefs); } #endif };