Files
HaloKeymind/src/helpers/CommonCLI.h
T

800 lines
31 KiB
C++

#pragma once
#include "Mesh.h"
#include <helpers/IdentityStore.h>
#include <helpers/SensorManager.h>
#include <helpers/ClientACL.h>
#include <helpers/MQTTPresets.h> // For MAX_MQTT_SLOTS (used in NodePrefs struct layout)
#include <helpers/MQTTPrefs.h>
#include <helpers/PrefsSaveRouting.h>
#include <helpers/RegionMap.h>
#include <helpers/ConfigSerializer.h>
#include <helpers/bridges/ESPNowBridgeFormat.h>
#include <helpers/CommonRadioPrefs.h>
#include <helpers/DynamicConfigSerializer.h>
#ifndef DEFAULT_CAD_ENABLED
#define DEFAULT_CAD_ENABLED 0
#endif
#ifndef DEFAULT_POWERSAVING_ENABLED
#define DEFAULT_POWERSAVING_ENABLED 0
#endif
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
#include <helpers/UserGpio.h>
#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 = 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;
// 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;
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;
// 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 = 0; // repeater RX-inactivity reboot watchdog (boolean)
uint8_t system_watchdog_enabled = 0; // nRF52 main-loop hardware watchdog (boolean; default on)
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(); }
uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; }
void setAgcResetInt(uint8_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);
}
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("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("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("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(); }
};
#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 {
public:
// 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;
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 void 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) = 0;
// 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) {
(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; }
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 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 getWiFiSSID(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 "<server> 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
class CommonCLI {
mesh::RTCClock* _rtc;
NodePrefs* _prefs;
CommonCLICallbacks* _callbacks;
mesh::MainBoard* _board;
#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
mesh::RTCClock* getRTCClock() { return _rtc; }
void savePrefs(
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 -> running 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:
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(&region_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<MQTTPrefs*>(&_mqtt_prefs); }
#endif
};