#include #include "CommonCLI.h" #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include "AlertReporter.h" // for alertReporterBannedChannelMatch() #include #include #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 #endif #ifdef ESP_PLATFORM #include #include #include #include #endif #ifdef WITH_MQTT_BRIDGE #include "bridges/MQTTBridge.h" #include "MQTTDefaults.h" #endif // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; while (*sp && *sp >= '0' && *sp <= '9') { n *= 10; n += (*sp++ - '0'); } return n; } static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '/' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; n++; } return true; } // Old fork firmware persisted the (since removed) NodePrefs MQTT fields to /com_prefs // as a zero-filled gap between owner_info (which ends at offset 290) and a trailing // observer block (rx_boosted_gain, flood_max_*, snmp/watchdog/alert settings). // The gap size depended on MAX_MQTT_SLOTS at the time: 306 bytes of non-slot fields // plus 186 bytes per slot (preset 24 + host 64 + port 2 + username 32 + password 64). // loadPrefsInt() uses the file size to tell the eras apart and recover the tail. static const size_t LEGACY_MQTT_GAP_6SLOT = 306 + 6 * 186; // 1422 static const size_t LEGACY_MQTT_GAP_3SLOT = 306 + 3 * 186; // 864 static const size_t LEGACY_OBS_TAIL_MAX = 124; // rx_boosted(1) + flood(2) + snmp(25) + watchdog(1) + alert block(95) // Bytes savePrefs() writes after owner_info (offsets 290-294): rx_boosted_gain, // flood_max_unscoped, flood_max_advert, radio_fem_rxgain, cad_enabled. loadPrefsInt() // treats any larger remainder as a legacy MQTT-gap file — keep this in sync with the // trailing writes in savePrefs() whenever an upstream merge appends /com_prefs fields. static const size_t COM_PREFS_TAIL_BYTES = 5; void CommonCLI::loadPrefs(FILESYSTEM* fs) { bool is_fresh_install = false; bool is_upgrade = false; if (fs->exists("/com_prefs")) { loadPrefsInt(fs, "/com_prefs"); // new filename } else if (fs->exists("/node_prefs")) { loadPrefsInt(fs, "/node_prefs"); is_upgrade = true; // Migrating from old filename savePrefs(fs); // save to new filename fs->remove("/node_prefs"); // remove old } else { // File doesn't exist - set default bridge settings for fresh installs is_fresh_install = true; _prefs->bridge_pkt_src = 1; // Default to RX (logRx) for new installs } #ifdef WITH_MQTT_BRIDGE // Load observer preferences (MQTT/WiFi/timezone/SNMP/alert) from /mqtt_prefs. // Readers (MQTTBridge, AlertReporter, observer CLI) use _mqtt_prefs directly — // these fields no longer exist in NodePrefs, so there is nothing to sync. loadMQTTPrefs(fs); // For MQTT bridge, migrate bridge.source to RX (logRx) only on fresh installs or upgrades // so legacy "tx" is not the default. mqtt.rx / mqtt.tx are separate (fresh default: advert for TX) if ((is_fresh_install || is_upgrade) && _prefs->bridge_pkt_src == 0) { MESH_DEBUG_PRINTLN("MQTT Bridge: Migrating bridge.source from tx to rx (MQTT bridge default)"); _prefs->bridge_pkt_src = 1; // Set to RX (logRx) savePrefs(fs); // Save the updated preference } // mqtt_rx_enabled: new field appended to end of MQTTPrefs. On upgrade from older firmware, // the shorter /mqtt_prefs file won't contain it, so it keeps the default value (1 = on) // set by setMQTTPrefsDefaults(). No explicit migration needed. #endif if (_com_prefs_needs_upgrade) { // Old-format /com_prefs (legacy MQTT gap + trailing observer block) was detected: // rewrite the prefs files in the current layout, one time. This persists the // recovered rx_boosted_gain/flood_max_* values and (on MQTT builds) the observer // settings that loadMQTTPrefs carried over into /mqtt_prefs. savePrefs(fs); _com_prefs_needs_upgrade = false; } } void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { #if defined(RP2040_PLATFORM) File file = fs->open(filename, "r"); #else File file = fs->open(filename); #endif if (file) { uint8_t pad[8]; file.read((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0 file.read((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4 file.read(pad, 4); // 36 file.read((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40 file.read((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48 file.read((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56 file.read((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72 file.read((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76 file.read((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77 file.read((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78 file.read(pad, 1); // 79 : 1 byte unused (was rx_boosted_gain in v1.14.1, moved to end for upgrade compat) file.read((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80 file.read((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 file.read((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 file.read((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 file.read(pad, 4); // 108 file.read((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.read((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.read((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 file.read((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 file.read((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 file.read((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122 file.read(pad, 1); // 123 file.read((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.read((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.read((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 file.read((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127 file.read((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128 file.read((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130 file.read((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131 file.read((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.read((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.read((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 file.read(pad, 3); // 153 file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161 file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 // MQTT/observer settings are no longer stored in /com_prefs — they live in // /mqtt_prefs (loaded by loadMQTTPrefs). Old fork firmware wrote a zero-filled // MQTT gap here followed by a trailing observer block; detect that layout by the // extra length, skip the gap, and recover the tail so those settings survive // the upgrade (the file is rewritten in the new layout by loadPrefs afterwards). // Defaults for the trailing fields that older/shorter files may not contain. // (upstream defaults: FEM RX gain on, CAD off) — overwritten below if present. _prefs->radio_fem_rxgain = 1; _prefs->cad_enabled = 0; // A remainder larger than the new-format tail means an old fork file with the // legacy MQTT gap; detect and recover it below. size_t extra = file.available(); if (extra > COM_PREFS_TAIL_BYTES) { _com_prefs_needs_upgrade = true; size_t gap = 0; if (extra > LEGACY_MQTT_GAP_6SLOT && extra <= LEGACY_MQTT_GAP_6SLOT + LEGACY_OBS_TAIL_MAX) { gap = LEGACY_MQTT_GAP_6SLOT; } else if (extra > LEGACY_MQTT_GAP_3SLOT && extra <= LEGACY_MQTT_GAP_3SLOT + LEGACY_OBS_TAIL_MAX) { gap = LEGACY_MQTT_GAP_3SLOT; } // Unrecognized legacy sizes (e.g. pre-slot-era files) leave gap == 0: the tail // is not read and everything past owner_info degrades to defaults. if (gap > 0) { uint8_t skip_buf[64]; size_t remaining = gap; while (remaining > 0) { size_t n = remaining > sizeof(skip_buf) ? sizeof(skip_buf) : remaining; file.read(skip_buf, n); remaining -= n; } file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // Tail layout: flood_max_unscoped, flood_max_advert, then the snmp fields — // except legacy flex-branch files where snmp starts right after // rx_boosted_gain (no flood_max_*). Same heuristic the old firmware used: // snmp_enabled is 0/1 and the first community char is printable (> 64). uint8_t b1 = 0, b2 = 0; bool have_flood_bytes = file.available() >= 2; if (have_flood_bytes) { file.read(&b1, 1); file.read(&b2, 1); } #ifdef WITH_MQTT_BRIDGE // Pre-fill with the same defaults applyMQTTDefaults() uses, so fields a // shorter (older) tail doesn't contain degrade to defaults when applied. memset(&_legacy_tail, 0, sizeof(_legacy_tail)); strncpy(_legacy_tail.snmp_community, "public", sizeof(_legacy_tail.snmp_community) - 1); _legacy_tail.radio_watchdog_minutes = 5; _legacy_tail.alert_wifi_minutes = 30; _legacy_tail.alert_mqtt_minutes = 240; _legacy_tail.alert_min_interval_min = 60; #endif if (have_flood_bytes && b1 <= 1 && b2 > 64) { // Legacy variant: no flood_max_* — b1/b2 are snmp_enabled + community[0] #ifdef WITH_MQTT_BRIDGE _legacy_tail.snmp_enabled = b1; _legacy_tail.snmp_community[0] = (char) b2; if (file.available() >= (int)(sizeof(_legacy_tail.snmp_community) - 1)) { file.read((uint8_t *)&_legacy_tail.snmp_community[1], sizeof(_legacy_tail.snmp_community) - 1); } #endif } else if (have_flood_bytes) { _prefs->flood_max_unscoped = b1; _prefs->flood_max_advert = b2; #ifdef WITH_MQTT_BRIDGE if (file.available() >= (int)sizeof(_legacy_tail.snmp_enabled)) { file.read((uint8_t *)&_legacy_tail.snmp_enabled, sizeof(_legacy_tail.snmp_enabled)); } if (file.available() >= (int)sizeof(_legacy_tail.snmp_community)) { file.read((uint8_t *)&_legacy_tail.snmp_community, sizeof(_legacy_tail.snmp_community)); } #endif } #ifdef WITH_MQTT_BRIDGE if (file.available() >= (int)sizeof(_legacy_tail.radio_watchdog_minutes)) { file.read((uint8_t *)&_legacy_tail.radio_watchdog_minutes, sizeof(_legacy_tail.radio_watchdog_minutes)); } if (file.available() >= (int)sizeof(_legacy_tail.alert_enabled)) { file.read((uint8_t *)&_legacy_tail.alert_enabled, sizeof(_legacy_tail.alert_enabled)); } if (file.available() >= (int)sizeof(_legacy_tail.alert_psk_hex)) { file.read((uint8_t *)&_legacy_tail.alert_psk_hex, sizeof(_legacy_tail.alert_psk_hex)); } if (file.available() >= (int)sizeof(_legacy_tail.alert_wifi_minutes)) { file.read((uint8_t *)&_legacy_tail.alert_wifi_minutes, sizeof(_legacy_tail.alert_wifi_minutes)); } if (file.available() >= (int)sizeof(_legacy_tail.alert_mqtt_minutes)) { file.read((uint8_t *)&_legacy_tail.alert_mqtt_minutes, sizeof(_legacy_tail.alert_mqtt_minutes)); } if (file.available() >= (int)sizeof(_legacy_tail.alert_min_interval_min)) { file.read((uint8_t *)&_legacy_tail.alert_min_interval_min, sizeof(_legacy_tail.alert_min_interval_min)); } if (file.available() >= (int)sizeof(_legacy_tail.alert_hashtag)) { file.read((uint8_t *)&_legacy_tail.alert_hashtag, sizeof(_legacy_tail.alert_hashtag)); } if (file.available() >= (int)sizeof(_legacy_tail.alert_region)) { file.read((uint8_t *)&_legacy_tail.alert_region, sizeof(_legacy_tail.alert_region)); } _legacy_tail.snmp_enabled = constrain(_legacy_tail.snmp_enabled, 0, 1); _legacy_tail.radio_watchdog_minutes = constrain(_legacy_tail.radio_watchdog_minutes, 0, 120); _legacy_tail.alert_enabled = constrain(_legacy_tail.alert_enabled, 0, 1); _legacy_tail.snmp_community[sizeof(_legacy_tail.snmp_community) - 1] = '\0'; _legacy_tail.alert_psk_hex[sizeof(_legacy_tail.alert_psk_hex) - 1] = '\0'; _legacy_tail.alert_hashtag[sizeof(_legacy_tail.alert_hashtag) - 1] = '\0'; _legacy_tail.alert_region[sizeof(_legacy_tail.alert_region) - 1] = '\0'; _legacy_tail.valid = true; #endif } } else { if (file.available() >= (int)sizeof(_prefs->rx_boosted_gain)) { file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); } if (file.available() >= (int)sizeof(_prefs->flood_max_unscoped)) { file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); } if (file.available() >= (int)sizeof(_prefs->flood_max_advert)) { file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); } if (file.available() >= (int)sizeof(_prefs->radio_fem_rxgain)) { // 293 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); } if (file.available() >= (int)sizeof(_prefs->cad_enabled)) { // 294 file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); } } // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); _prefs->tx_delay_factor = constrain(_prefs->tx_delay_factor, 0, 2.0f); _prefs->direct_tx_delay_factor = constrain(_prefs->direct_tx_delay_factor, 0, 2.0f); _prefs->airtime_factor = constrain(_prefs->airtime_factor, 0, 9.0f); _prefs->freq = constrain(_prefs->freq, 150.0f, 2500.0f); _prefs->bw = constrain(_prefs->bw, 7.8f, 500.0f); _prefs->sf = constrain(_prefs->sf, 5, 12); _prefs->cr = constrain(_prefs->cr, 5, 8); _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, -9, 30); _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); _prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2); // NOTE: mode 3 reserved for future _prefs->loop_detect = constrain(_prefs->loop_detect, 0, 3); // LOOP_DETECT_OFF..LOOP_DETECT_STRICT _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); _prefs->bridge_delay = constrain(_prefs->bridge_delay, 0, 10000); _prefs->bridge_pkt_src = constrain(_prefs->bridge_pkt_src, 0, 1); _prefs->bridge_baud = constrain(_prefs->bridge_baud, 9600, BRIDGE_MAX_BAUD); _prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14); _prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1); _prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1); _prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2); _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean file.close(); } } void CommonCLI::savePrefs(FILESYSTEM* fs) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) fs->remove("/com_prefs"); File file = fs->open("/com_prefs", FILE_O_WRITE); #elif defined(RP2040_PLATFORM) File file = fs->open("/com_prefs", "w"); #else File file = fs->open("/com_prefs", "w", true); #endif if (file) { uint8_t pad[8]; memset(pad, 0, sizeof(pad)); file.write((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0 file.write((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4 file.write(pad, 4); // 36 file.write((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40 file.write((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48 file.write((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56 file.write((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72 file.write((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76 file.write((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77 file.write((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78 file.write(pad, 1); // 79 : 1 byte unused (rx_boosted_gain moved to end) file.write((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80 file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 file.write(pad, 4); // 108 file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 file.write((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122 file.write(pad, 1); // 123 file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 file.write((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127 file.write((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128 file.write((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130 file.write((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131 file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 file.write(pad, 3); // 153 file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 // MQTT/observer settings are stored in /mqtt_prefs, not here. No zero-gap is // written anymore — /com_prefs holds only the (non-observer) fields below. // These trailing writes are COM_PREFS_TAIL_BYTES; keep the two in sync. file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 file.close(); } #ifdef WITH_MQTT_BRIDGE // Observer config (MQTT/WiFi/timezone/SNMP/alert) is persisted separately. The // observer CLI writes _mqtt_prefs directly, so no NodePrefs->MQTTPrefs sync runs. saveMQTTPrefs(fs); #endif } #ifdef WITH_MQTT_BRIDGE // Set default values for MQTT preferences (used when file doesn't exist or is corrupted) static void setMQTTPrefsDefaults(MQTTPrefs* prefs) { applyMQTTDefaults(prefs); } static File openMqttPrefsRead(FILESYSTEM* fs) { #if defined(RP2040_PLATFORM) return fs->open("/mqtt_prefs", "r"); #else return fs->open("/mqtt_prefs"); #endif } void CommonCLI::loadMQTTPrefs(FILESYSTEM* fs) { // Initialize with defaults first setMQTTPrefsDefaults(&_mqtt_prefs); _mqtt_prefs_hold = false; // Whether the loaded /mqtt_prefs already contained the observer fields (snmp/ // watchdog/alert) appended in Phase 2 — if not, they may be carried over from an // old-format /com_prefs trailing block below. bool has_observer_fields = false; bool file_existed = fs->exists("/mqtt_prefs"); if (file_existed) { // First, peek the header to see if this is a versioned file. File file = openMqttPrefsRead(fs); bool versioned = false; if (file) { size_t file_size = file.size(); if (file_size >= sizeof(MQTTPrefsHeader)) { MQTTPrefsHeader hdr; if (file.read((uint8_t *)&hdr, sizeof(hdr)) == sizeof(hdr) && memcmp(hdr.magic, MQTT_PREFS_MAGIC, sizeof(hdr.magic)) == 0) { versioned = true; if (hdr.version == MQTT_PREFS_VERSION) { // Current version: the payload follows the header. Read up to // sizeof(MQTTPrefs); a shorter payload (an earlier v1 firmware that // hadn't appended a tail field) leaves the trailing fields at their // defaults, and a longer one (a future append) is truncated harmlessly. size_t payload_avail = file_size - sizeof(hdr); size_t to_read = payload_avail < sizeof(_mqtt_prefs) ? payload_avail : sizeof(_mqtt_prefs); size_t got = file.read((uint8_t *)&_mqtt_prefs, to_read); if (got != to_read) { setMQTTPrefsDefaults(&_mqtt_prefs); } else { has_observer_fields = true; // observer tail is part of the v1 payload } } else { // Unknown (newer) version: don't risk misreading a layout we don't know. // Keep defaults for this boot and hold the file so later savePrefs() // calls can't overwrite the newer config (no downgrade). _mqtt_prefs_hold = true; MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs version unsupported, using defaults (file preserved)"); } } } file.close(); } if (!versioned) { // Headerless (legacy) file. Detect the historical on-disk layout by size, // migrate it into the compact versioned struct, and re-save — which adds the // header and drops the vestigial `_legacy_*` fields. Reopen because the peek // above advanced the read cursor past the (non-matching) leading bytes. File file = openMqttPrefsRead(fs); if (file) { size_t file_size = file.size(); // Detect old (pre-slot) format by file size. // Old MQTTPrefs was ~472 bytes (no slot fields). // If the file is smaller than the new struct but close to OldMQTTPrefs size, // read it with the old layout and migrate. if (file_size > 0 && file_size <= sizeof(OldMQTTPrefs)) { OldMQTTPrefs old_prefs; memset(&old_prefs, 0, sizeof(old_prefs)); size_t bytes_read = file.read((uint8_t *)&old_prefs, file_size < sizeof(old_prefs) ? file_size : sizeof(old_prefs)); file.close(); if (bytes_read > 0) { MESH_DEBUG_PRINTLN("MQTT: Migrating old-format prefs to versioned layout"); // Copy common fields (identical layout at start of both structs) memcpy(_mqtt_prefs.mqtt_origin, old_prefs.mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin)); memcpy(_mqtt_prefs.mqtt_iata, old_prefs.mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata)); _mqtt_prefs.mqtt_status_enabled = old_prefs.mqtt_status_enabled; _mqtt_prefs.mqtt_packets_enabled = old_prefs.mqtt_packets_enabled; _mqtt_prefs.mqtt_raw_enabled = old_prefs.mqtt_raw_enabled; _mqtt_prefs.mqtt_tx_enabled = old_prefs.mqtt_tx_enabled; _mqtt_prefs.mqtt_status_interval = old_prefs.mqtt_status_interval; memcpy(_mqtt_prefs.wifi_ssid, old_prefs.wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid)); memcpy(_mqtt_prefs.wifi_password, old_prefs.wifi_password, sizeof(_mqtt_prefs.wifi_password)); _mqtt_prefs.wifi_power_save = old_prefs.wifi_power_save; memcpy(_mqtt_prefs.timezone_string, old_prefs.timezone_string, sizeof(_mqtt_prefs.timezone_string)); _mqtt_prefs.timezone_offset = old_prefs.timezone_offset; // Migrate shared auth fields memcpy(_mqtt_prefs.mqtt_owner_public_key, old_prefs.mqtt_owner_public_key, sizeof(_mqtt_prefs.mqtt_owner_public_key)); memcpy(_mqtt_prefs.mqtt_email, old_prefs.mqtt_email, sizeof(_mqtt_prefs.mqtt_email)); // Migrate analyzer presets to slots if (old_prefs.mqtt_analyzer_us_enabled == 1) { strncpy(_mqtt_prefs.mqtt_slot_preset[0], "analyzer-us", sizeof(_mqtt_prefs.mqtt_slot_preset[0]) - 1); } else { strncpy(_mqtt_prefs.mqtt_slot_preset[0], "none", sizeof(_mqtt_prefs.mqtt_slot_preset[0]) - 1); } if (old_prefs.mqtt_analyzer_eu_enabled == 1) { strncpy(_mqtt_prefs.mqtt_slot_preset[1], "analyzer-eu", sizeof(_mqtt_prefs.mqtt_slot_preset[1]) - 1); } else { strncpy(_mqtt_prefs.mqtt_slot_preset[1], "none", sizeof(_mqtt_prefs.mqtt_slot_preset[1]) - 1); } // Migrate custom server to slot 3 if (old_prefs.mqtt_server[0] != '\0' && old_prefs.mqtt_port > 0) { strncpy(_mqtt_prefs.mqtt_slot_preset[2], "custom", sizeof(_mqtt_prefs.mqtt_slot_preset[2]) - 1); strncpy(_mqtt_prefs.mqtt_slot_host[2], old_prefs.mqtt_server, sizeof(_mqtt_prefs.mqtt_slot_host[2]) - 1); _mqtt_prefs.mqtt_slot_port[2] = old_prefs.mqtt_port; strncpy(_mqtt_prefs.mqtt_slot_username[2], old_prefs.mqtt_username, sizeof(_mqtt_prefs.mqtt_slot_username[2]) - 1); strncpy(_mqtt_prefs.mqtt_slot_password[2], old_prefs.mqtt_password, sizeof(_mqtt_prefs.mqtt_slot_password[2]) - 1); } else { strncpy(_mqtt_prefs.mqtt_slot_preset[2], "none", sizeof(_mqtt_prefs.mqtt_slot_preset[2]) - 1); } // Save migrated prefs in the versioned format saveMQTTPrefs(fs); } } else if (file_size > 0 && file_size <= sizeof(ThreeSlotMQTTPrefs)) { // 3-slot format → compact 6-slot migration // Array sizes changed from [3] to [6], shifting all field offsets. // Read into old layout struct and field-copy to new layout. ThreeSlotMQTTPrefs old3; memset(&old3, 0, sizeof(old3)); size_t bytes_to_read = file_size < sizeof(old3) ? file_size : sizeof(old3); size_t bytes_read = file.read((uint8_t *)&old3, bytes_to_read); file.close(); if (bytes_read > 0) { MESH_DEBUG_PRINTLN("MQTT: Migrating 3-slot prefs to versioned layout"); // Copy non-slot fields (identical layout) memcpy(_mqtt_prefs.mqtt_origin, old3.mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin)); memcpy(_mqtt_prefs.mqtt_iata, old3.mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata)); _mqtt_prefs.mqtt_status_enabled = old3.mqtt_status_enabled; _mqtt_prefs.mqtt_packets_enabled = old3.mqtt_packets_enabled; _mqtt_prefs.mqtt_raw_enabled = old3.mqtt_raw_enabled; _mqtt_prefs.mqtt_tx_enabled = old3.mqtt_tx_enabled; _mqtt_prefs.mqtt_status_interval = old3.mqtt_status_interval; memcpy(_mqtt_prefs.wifi_ssid, old3.wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid)); memcpy(_mqtt_prefs.wifi_password, old3.wifi_password, sizeof(_mqtt_prefs.wifi_password)); _mqtt_prefs.wifi_power_save = old3.wifi_power_save; memcpy(_mqtt_prefs.timezone_string, old3.timezone_string, sizeof(_mqtt_prefs.timezone_string)); _mqtt_prefs.timezone_offset = old3.timezone_offset; // Copy slot fields for indices 0-2 from old layout for (int i = 0; i < 3; i++) { memcpy(_mqtt_prefs.mqtt_slot_preset[i], old3.mqtt_slot_preset[i], sizeof(_mqtt_prefs.mqtt_slot_preset[i])); memcpy(_mqtt_prefs.mqtt_slot_host[i], old3.mqtt_slot_host[i], sizeof(_mqtt_prefs.mqtt_slot_host[i])); _mqtt_prefs.mqtt_slot_port[i] = old3.mqtt_slot_port[i]; memcpy(_mqtt_prefs.mqtt_slot_username[i], old3.mqtt_slot_username[i], sizeof(_mqtt_prefs.mqtt_slot_username[i])); memcpy(_mqtt_prefs.mqtt_slot_password[i], old3.mqtt_slot_password[i], sizeof(_mqtt_prefs.mqtt_slot_password[i])); memcpy(_mqtt_prefs.mqtt_slot_token[i], old3.mqtt_slot_token[i], sizeof(_mqtt_prefs.mqtt_slot_token[i])); memcpy(_mqtt_prefs.mqtt_slot_topic[i], old3.mqtt_slot_topic[i], sizeof(_mqtt_prefs.mqtt_slot_topic[i])); } // Slots 3-5 keep defaults ("none") from setMQTTPrefsDefaults() // Copy shared auth fields memcpy(_mqtt_prefs.mqtt_owner_public_key, old3.mqtt_owner_public_key, sizeof(_mqtt_prefs.mqtt_owner_public_key)); memcpy(_mqtt_prefs.mqtt_email, old3.mqtt_email, sizeof(_mqtt_prefs.mqtt_email)); // Save migrated prefs in the versioned format saveMQTTPrefs(fs); } } else if (file_size > 0) { // Headerless 6-slot layout as shipped on mqtt-bridge-implementation-flex // (the deployed fleet). Same field order as the compact struct but with the // vestigial `_legacy_*` block mid-struct and no observer tail — so read it // into Legacy6SlotMQTTPrefs and field-copy across, dropping `_legacy_*`. Legacy6SlotMQTTPrefs old6; memset(&old6, 0, sizeof(old6)); size_t bytes_to_read = file_size < sizeof(old6) ? file_size : sizeof(old6); size_t bytes_read = file.read((uint8_t *)&old6, bytes_to_read); file.close(); if (bytes_read > 0) { MESH_DEBUG_PRINTLN("MQTT: Migrating headerless 6-slot prefs to versioned layout"); memcpy(_mqtt_prefs.mqtt_origin, old6.mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin)); memcpy(_mqtt_prefs.mqtt_iata, old6.mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata)); _mqtt_prefs.mqtt_status_enabled = old6.mqtt_status_enabled; _mqtt_prefs.mqtt_packets_enabled = old6.mqtt_packets_enabled; _mqtt_prefs.mqtt_raw_enabled = old6.mqtt_raw_enabled; _mqtt_prefs.mqtt_tx_enabled = old6.mqtt_tx_enabled; _mqtt_prefs.mqtt_status_interval = old6.mqtt_status_interval; memcpy(_mqtt_prefs.wifi_ssid, old6.wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid)); memcpy(_mqtt_prefs.wifi_password, old6.wifi_password, sizeof(_mqtt_prefs.wifi_password)); _mqtt_prefs.wifi_power_save = old6.wifi_power_save; memcpy(_mqtt_prefs.timezone_string, old6.timezone_string, sizeof(_mqtt_prefs.timezone_string)); _mqtt_prefs.timezone_offset = old6.timezone_offset; memcpy(_mqtt_prefs.mqtt_slot_preset, old6.mqtt_slot_preset, sizeof(_mqtt_prefs.mqtt_slot_preset)); memcpy(_mqtt_prefs.mqtt_slot_host, old6.mqtt_slot_host, sizeof(_mqtt_prefs.mqtt_slot_host)); memcpy(_mqtt_prefs.mqtt_slot_port, old6.mqtt_slot_port, sizeof(_mqtt_prefs.mqtt_slot_port)); memcpy(_mqtt_prefs.mqtt_slot_username, old6.mqtt_slot_username, sizeof(_mqtt_prefs.mqtt_slot_username)); memcpy(_mqtt_prefs.mqtt_slot_password, old6.mqtt_slot_password, sizeof(_mqtt_prefs.mqtt_slot_password)); memcpy(_mqtt_prefs.mqtt_owner_public_key, old6.mqtt_owner_public_key, sizeof(_mqtt_prefs.mqtt_owner_public_key)); memcpy(_mqtt_prefs.mqtt_email, old6.mqtt_email, sizeof(_mqtt_prefs.mqtt_email)); // `_legacy_*` fields are intentionally dropped here. memcpy(_mqtt_prefs.mqtt_slot_token, old6.mqtt_slot_token, sizeof(_mqtt_prefs.mqtt_slot_token)); memcpy(_mqtt_prefs.mqtt_slot_topic, old6.mqtt_slot_topic, sizeof(_mqtt_prefs.mqtt_slot_topic)); memcpy(_mqtt_prefs.mqtt_slot_audience, old6.mqtt_slot_audience, sizeof(_mqtt_prefs.mqtt_slot_audience)); _mqtt_prefs.mqtt_rx_enabled = old6.mqtt_rx_enabled; memcpy(_mqtt_prefs.mqtt_ntp_server, old6.mqtt_ntp_server, sizeof(_mqtt_prefs.mqtt_ntp_server)); // Observer tail (snmp/watchdog/alert) keeps defaults; if this device is // also upgrading across the NodePrefs split, loadPrefsInt captured those // values from /com_prefs and they are applied below. saveMQTTPrefs(fs); } } else { file.close(); setMQTTPrefsDefaults(&_mqtt_prefs); } } } } else { // No /mqtt_prefs file — defaults already set. (MQTT slot/WiFi settings from // pre-/mqtt_prefs-split fork firmware are NOT recovered from /com_prefs — that // offset-based migration was fragile and was removed; those users re-enter // their MQTT config. The observer trailing block IS recovered, below.) } // One-time upgrade path: if loadPrefsInt captured the trailing observer block of // an old-format /com_prefs and this /mqtt_prefs predates the appended observer // fields (or doesn't exist), carry the settings over so SNMP, radio-watchdog and // fault-alert config survive the firmware upgrade. loadPrefs() persists both // files in the new layout right after this. if (_legacy_tail.valid && !has_observer_fields) { _mqtt_prefs.snmp_enabled = _legacy_tail.snmp_enabled; memcpy(_mqtt_prefs.snmp_community, _legacy_tail.snmp_community, sizeof(_mqtt_prefs.snmp_community)); _mqtt_prefs.radio_watchdog_minutes = _legacy_tail.radio_watchdog_minutes; _mqtt_prefs.alert_enabled = _legacy_tail.alert_enabled; memcpy(_mqtt_prefs.alert_psk_hex, _legacy_tail.alert_psk_hex, sizeof(_mqtt_prefs.alert_psk_hex)); _mqtt_prefs.alert_wifi_minutes = _legacy_tail.alert_wifi_minutes; _mqtt_prefs.alert_mqtt_minutes = _legacy_tail.alert_mqtt_minutes; _mqtt_prefs.alert_min_interval_min = _legacy_tail.alert_min_interval_min; memcpy(_mqtt_prefs.alert_hashtag, _legacy_tail.alert_hashtag, sizeof(_mqtt_prefs.alert_hashtag)); memcpy(_mqtt_prefs.alert_region, _legacy_tail.alert_region, sizeof(_mqtt_prefs.alert_region)); MESH_DEBUG_PRINTLN("MQTT: Migrated observer settings from legacy /com_prefs trailing block"); } _legacy_tail.valid = false; } void CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) { if (_mqtt_prefs_hold) { // /mqtt_prefs was written by newer firmware; overwriting it here (v1 header + // this boot's defaults) would destroy that config. Observer settings changed // this boot are not persisted until current-or-older firmware is flashed. MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs from newer firmware, not overwriting"); return; } #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) fs->remove("/mqtt_prefs"); File file = fs->open("/mqtt_prefs", FILE_O_WRITE); #elif defined(RP2040_PLATFORM) File file = fs->open("/mqtt_prefs", "w"); #else File file = fs->open("/mqtt_prefs", "w", true); #endif if (file) { // Versioned format: 8-byte header followed by the raw MQTTPrefs payload. MQTTPrefsHeader hdr; memcpy(hdr.magic, MQTT_PREFS_MAGIC, sizeof(hdr.magic)); hdr.version = MQTT_PREFS_VERSION; hdr.payload_len = (uint16_t)sizeof(_mqtt_prefs); file.write((uint8_t *)&hdr, sizeof(hdr)); file.write((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs)); file.close(); } } #endif #define MIN_LOCAL_ADVERT_INTERVAL 60 void CommonCLI::savePrefs() { uint8_t old_advert_interval = _prefs->advert_interval; if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) { _prefs->advert_interval = 0; // turn it off, now that device has been manually configured } // If advert_interval was changed, update the timer to reflect the change if (old_advert_interval != _prefs->advert_interval) { _callbacks->updateAdvertTimer(); } _callbacks->savePrefs(); } uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { if (_prefs->advert_loc_policy == ADVERT_LOC_NONE) { AdvertDataBuilder builder(node_type, _prefs->node_name); return builder.encodeTo(app_data); } else if (_prefs->advert_loc_policy == ADVERT_LOC_SHARE) { AdvertDataBuilder builder(node_type, _prefs->node_name, _sensors->node_lat, _sensors->node_lon); return builder.encodeTo(app_data); } else { AdvertDataBuilder builder(node_type, _prefs->node_name, _prefs->node_lat, _prefs->node_lon); return builder.encodeTo(app_data); } } void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { // Observer-only top-level commands (ota check/update, tls.bundletest, alert test) // live in CommonCLI_Observer.cpp. if (handleObserverCommand(sender_timestamp, command, reply)) return; if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) { _board->powerOff(); // doesn't return } else if (memcmp(command, "reboot", 6) == 0) { _board->reboot(); // doesn't return } else if (memcmp(command, "clkreboot", 9) == 0) { // Reset clock getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm _board->reboot(); // doesn't return } else if (memcmp(command, "advert.zerohop", 14) == 0 && (command[14] == 0 || command[14] == ' ')) { // send zerohop advert _callbacks->sendSelfAdvertisement(1500, false); // longer delay, give CLI response time to be sent first strcpy(reply, "OK - zerohop advert sent"); } else if (memcmp(command, "advert", 6) == 0) { // send flood advert _callbacks->sendSelfAdvertisement(1500, true); // longer delay, give CLI response time to be sent first strcpy(reply, "OK - Advert sent"); } else if (memcmp(command, "clock sync", 10) == 0) { uint32_t curr = getRTCClock()->getCurrentTime(); if (sender_timestamp > curr) { getRTCClock()->setCurrentTime(sender_timestamp + 1); uint32_t now = getRTCClock()->getCurrentTime(); DateTime dt = DateTime(now); sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); } else { strcpy(reply, "ERR: clock cannot go backwards"); } } else if (memcmp(command, "memory", 6) == 0) { sprintf(reply, "Free: %d, Min: %d, Max: %d, Queue: %d, IntFree: %d, IntMax: %d, PSRAM: %d/%d", ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap(), _callbacks->getQueueSize(), (int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL), (int)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL), (int)heap_caps_get_free_size(MALLOC_CAP_SPIRAM), (int)heap_caps_get_total_size(MALLOC_CAP_SPIRAM)); } else if (memcmp(command, "start ota", 9) == 0) { // Manual OTA: bring up the ElegantOTA SoftAP for a hand-uploaded binary. if (!_board->startOTAUpdate(_prefs->node_name, reply)) { strcpy(reply, "Error"); } } else if (memcmp(command, "clock", 5) == 0) { uint32_t now = getRTCClock()->getCurrentTime(); DateTime dt = DateTime(now); sprintf(reply, "%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); } else if (memcmp(command, "time ", 5) == 0) { // set time (to epoch seconds) uint32_t secs = _atoi(&command[5]); uint32_t curr = getRTCClock()->getCurrentTime(); if (secs > curr) { getRTCClock()->setCurrentTime(secs); uint32_t now = getRTCClock()->getCurrentTime(); DateTime dt = DateTime(now); sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); } else { strcpy(reply, "(ERR: clock cannot go backwards)"); } } else if (memcmp(command, "neighbors", 9) == 0) { _callbacks->formatNeighborsReply(reply); } else if (memcmp(command, "neighbor.remove ", 16) == 0) { const char* hex = &command[16]; uint8_t pubkey[PUB_KEY_SIZE]; int hex_len = min((int)strlen(hex), PUB_KEY_SIZE*2); int pubkey_len = hex_len / 2; if (mesh::Utils::fromHex(pubkey, pubkey_len, hex)) { _callbacks->removeNeighbor(pubkey, pubkey_len); strcpy(reply, "OK"); } else { strcpy(reply, "ERR: bad pubkey"); } } else if (memcmp(command, "tempradio ", 10) == 0) { strcpy(tmp, &command[10]); const char *parts[5]; int num = mesh::Utils::parseTextParts(tmp, parts, 5); float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; uint8_t sf = num > 2 ? atoi(parts[2]) : 0; uint8_t cr = num > 3 ? atoi(parts[3]) : 0; int temp_timeout_mins = num > 4 ? atoi(parts[4]) : 0; if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f && temp_timeout_mins > 0) { _callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins); sprintf(reply, "OK - temp params for %d mins", temp_timeout_mins); } else { strcpy(reply, "Error, invalid params"); } } else if (memcmp(command, "password ", 9) == 0) { // change admin password StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password)); savePrefs(); sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!! } else if (memcmp(command, "clear stats", 11) == 0) { _callbacks->clearStats(); strcpy(reply, "(OK - stats reset)"); } else if (memcmp(command, "get ", 4) == 0) { handleGetCmd(sender_timestamp, command, reply); } else if (memcmp(command, "set ", 4) == 0) { handleSetCmd(sender_timestamp, command, reply); } else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) { bool s = _callbacks->formatFileSystem(); sprintf(reply, "File system erase: %s", s ? "OK" : "Err"); } else if (memcmp(command, "ver", 3) == 0) { sprintf(reply, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate()); } else if (memcmp(command, "board", 5) == 0) { sprintf(reply, "%s", _board->getManufacturerName()); } else if (memcmp(command, "sensor get ", 11) == 0) { const char* key = command + 11; const char* val = _sensors->getSettingByKey(key); if (val != NULL) { sprintf(reply, "> %s", val); } else { strcpy(reply, "null"); } } else if (memcmp(command, "sensor set ", 11) == 0) { strcpy(tmp, &command[11]); const char *parts[2]; int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' '); const char *key = (num > 0) ? parts[0] : ""; const char *value = (num > 1) ? parts[1] : "null"; if (_sensors->setSettingValue(key, value)) { strcpy(reply, "ok"); } else { strcpy(reply, "can't find custom var"); } } else if (memcmp(command, "sensor list", 11) == 0) { char* dp = reply; int start = 0; int end = _sensors->getNumSettings(); if (strlen(command) > 11) { start = _atoi(command+12); } if (start >= end) { strcpy(reply, "no custom var"); } else { sprintf(dp, "%d vars\n", end); dp = strchr(dp, 0); int i; for (i = start; i < end && (dp-reply < 134); i++) { sprintf(dp, "%s=%s\n", _sensors->getSettingName(i), _sensors->getSettingValue(i)); dp = strchr(dp, 0); } if (i < end) { sprintf(dp, "... next:%d", i); } else { *(dp-1) = 0; // remove last CR } } } else if (memcmp(command, "region", 6) == 0) { handleRegionCmd(command, reply); #if ENV_INCLUDE_GPS == 1 } else if (memcmp(command, "gps on", 6) == 0) { if (_sensors->setSettingValue("gps", "1")) { _prefs->gps_enabled = 1; savePrefs(); strcpy(reply, "ok"); } else { strcpy(reply, "gps toggle not found"); } } else if (memcmp(command, "gps off", 7) == 0) { if (_sensors->setSettingValue("gps", "0")) { _prefs->gps_enabled = 0; savePrefs(); strcpy(reply, "ok"); } else { strcpy(reply, "gps toggle not found"); } } else if (memcmp(command, "gps sync", 8) == 0) { LocationProvider * l = _sensors->getLocationProvider(); if (l != NULL) { l->syncTime(); strcpy(reply, "ok"); } else { strcpy(reply, "gps provider not found"); } } else if (memcmp(command, "gps setloc", 10) == 0) { _prefs->node_lat = _sensors->node_lat; _prefs->node_lon = _sensors->node_lon; savePrefs(); strcpy(reply, "ok"); } else if (memcmp(command, "gps advert", 10) == 0) { if (strlen(command) == 10) { switch (_prefs->advert_loc_policy) { case ADVERT_LOC_NONE: strcpy(reply, "> none"); break; case ADVERT_LOC_PREFS: strcpy(reply, "> prefs"); break; case ADVERT_LOC_SHARE: strcpy(reply, "> share"); break; default: strcpy(reply, "error"); } } else if (memcmp(command+11, "none", 4) == 0) { _prefs->advert_loc_policy = ADVERT_LOC_NONE; savePrefs(); strcpy(reply, "ok"); } else if (memcmp(command+11, "share", 5) == 0) { _prefs->advert_loc_policy = ADVERT_LOC_SHARE; savePrefs(); strcpy(reply, "ok"); } else if (memcmp(command+11, "prefs", 5) == 0) { _prefs->advert_loc_policy = ADVERT_LOC_PREFS; savePrefs(); strcpy(reply, "ok"); } else { strcpy(reply, "error"); } } else if (memcmp(command, "gps", 3) == 0) { LocationProvider * l = _sensors->getLocationProvider(); if (l != NULL) { bool enabled = l->isEnabled(); // is EN pin on ? bool fix = l->isValid(); // has fix ? int sats = l->satellitesCount(); bool active = !strcmp(_sensors->getSettingByKey("gps"), "1"); if (enabled) { sprintf(reply, "on, %s, %s, %d sats", active?"active":"deactivated", fix?"fix":"no fix", sats); } else { strcpy(reply, "off"); } } else { strcpy(reply, "Can't find GPS"); } #endif } else if (memcmp(command, "powersaving on", 14) == 0) { #if defined(NRF52_PLATFORM) _prefs->powersaving_enabled = 1; savePrefs(); strcpy(reply, "on - Immediate effect"); #elif defined(ESP32) && !defined(WITH_BRIDGE) _prefs->powersaving_enabled = 1; savePrefs(); strcpy(reply, "on - After 2 minutes"); #elif defined(WITH_BRIDGE) strcpy(reply, "Bridge not supported"); #else strcpy(reply, "Board not supported"); #endif } else if (memcmp(command, "powersaving off", 15) == 0) { _prefs->powersaving_enabled = 0; savePrefs(); strcpy(reply, "off"); } else if (memcmp(command, "powersaving", 11) == 0) { if (_prefs->powersaving_enabled) { strcpy(reply, "on"); } else { strcpy(reply, "off"); } } else if (memcmp(command, "log start", 9) == 0) { _callbacks->setLoggingOn(true); strcpy(reply, " logging on"); } else if (memcmp(command, "log stop", 8) == 0) { _callbacks->setLoggingOn(false); strcpy(reply, " logging off"); } else if (memcmp(command, "log erase", 9) == 0) { _callbacks->eraseLogFile(); strcpy(reply, " log erased"); } else if (sender_timestamp == 0 && memcmp(command, "log", 3) == 0) { _callbacks->dumpLogFile(); strcpy(reply, " EOF"); } else if (sender_timestamp == 0 && memcmp(command, "stats-packets", 13) == 0 && (command[13] == 0 || command[13] == ' ')) { _callbacks->formatPacketStatsReply(reply); } else if (sender_timestamp == 0 && memcmp(command, "stats-radio-diag", 16) == 0 && (command[16] == 0 || command[16] == ' ')) { _callbacks->formatRadioDiagReply(reply); } else if (sender_timestamp == 0 && memcmp(command, "stats-radio", 11) == 0 && (command[11] == 0 || command[11] == ' ')) { _callbacks->formatRadioStatsReply(reply); } else if (sender_timestamp == 0 && memcmp(command, "stats-core", 10) == 0 && (command[10] == 0 || command[10] == ' ')) { _callbacks->formatStatsReply(reply); } else { strcpy(reply, "Unknown command"); } } void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; // Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp. if (handleObserverSetCmd(sender_timestamp, config, reply)) return; if (memcmp(config, "dutycycle ", 10) == 0) { float dc = atof(&config[10]); if (dc < 1 || dc > 100) { strcpy(reply, "ERROR: dutycycle must be 1-100"); } else { _prefs->airtime_factor = (100.0f / dc) - 1.0f; savePrefs(); float actual = 100.0f / (_prefs->airtime_factor + 1.0f); int a_int = (int)actual; int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); sprintf(reply, "OK - %d.%d%%", a_int, a_frac); } } else if (memcmp(config, "af ", 3) == 0) { _prefs->airtime_factor = atof(&config[3]); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "int.thresh ", 11) == 0) { _prefs->interference_threshold = atoi(&config[11]); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "cad ", 4) == 0) { _prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0; savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); } else if (memcmp(&config[17], "on", 2) == 0) { if (_board->setLoRaFemLnaEnabled(true)) { _prefs->radio_fem_rxgain = 1; savePrefs(); strcpy(reply, "OK - LoRa FEM RX gain on"); } else { strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); } } else if (memcmp(&config[17], "off", 3) == 0) { if (_board->setLoRaFemLnaEnabled(false)) { _prefs->radio_fem_rxgain = 0; savePrefs(); strcpy(reply, "OK - LoRa FEM RX gain off"); } else { strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); } } else { strcpy(reply, "Error: state must be on or off"); } } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { _prefs->agc_reset_interval = atoi(&config[19]) / 4; savePrefs(); sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4); } else if (memcmp(config, "multi.acks ", 11) == 0) { _prefs->multi_acks = atoi(&config[11]); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "allow.read.only ", 16) == 0) { _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "flood.advert.interval ", 22) == 0) { int hours = _atoi(&config[22]); if ((hours > 0 && hours < 3) || (hours > 168)) { strcpy(reply, "Error: interval range is 3-168 hours"); } else { _prefs->flood_advert_interval = (uint8_t)(hours); _callbacks->updateFloodAdvertTimer(); savePrefs(); strcpy(reply, "OK"); } } else if (memcmp(config, "advert.interval ", 16) == 0) { int mins = _atoi(&config[16]); if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) { sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL); } else { _prefs->advert_interval = (uint8_t)(mins / 2); _callbacks->updateAdvertTimer(); savePrefs(); strcpy(reply, "OK"); } } else if (memcmp(config, "guest.password ", 15) == 0) { StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password)); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "prv.key ", 8) == 0) { uint8_t prv_key[PRV_KEY_SIZE]; bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]); // only allow rekey if key is valid if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) { mesh::LocalIdentity new_id; new_id.readFrom(prv_key, PRV_KEY_SIZE); _callbacks->saveIdentity(new_id); strcpy(reply, "OK, reboot to apply! New pubkey: "); mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE); } else { strcpy(reply, "Error, bad key"); } } else if (memcmp(config, "name ", 5) == 0) { if (isValidName(&config[5])) { StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, bad chars"); } } else if (memcmp(config, "repeat ", 7) == 0) { _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; savePrefs(); strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); #if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) } else if (memcmp(config, "radio.rxgain ", 13) == 0) { _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; strcpy(reply, "OK"); savePrefs(); _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); #endif } else if (memcmp(config, "radio ", 6) == 0) { strcpy(tmp, &config[6]); const char *parts[4]; int num = mesh::Utils::parseTextParts(tmp, parts, 4); float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; uint8_t sf = num > 2 ? atoi(parts[2]) : 0; uint8_t cr = num > 3 ? atoi(parts[3]) : 0; if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { _prefs->sf = sf; _prefs->cr = cr; _prefs->freq = freq; _prefs->bw = bw; _callbacks->savePrefs(); strcpy(reply, "OK - reboot to apply"); } else { strcpy(reply, "Error, invalid radio params"); } } else if (memcmp(config, "lat ", 4) == 0) { _prefs->node_lat = atof(&config[4]); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "lon ", 4) == 0) { _prefs->node_lon = atof(&config[4]); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "rxdelay ", 8) == 0) { float db = atof(&config[8]); if (db >= 0 && db <= 20.0f) { _prefs->rx_delay_base = db; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-20"); } } else if (memcmp(config, "txdelay ", 8) == 0) { float f = atof(&config[8]); if (f >= 0 && f <= 2.0f) { _prefs->tx_delay_factor = f; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-2"); } } else if (memcmp(config, "flood.max.unscoped ", 19) == 0) { uint8_t m = atoi(&config[19]); if (m <= 64) { _prefs->flood_max_unscoped = m; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, max 64"); } } else if (memcmp(config, "flood.max.advert ", 17) == 0) { uint8_t m = atoi(&config[17]); if (m <= 64) { _prefs->flood_max_advert = m; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, max 64"); } } else if (memcmp(config, "flood.max ", 10) == 0) { uint8_t m = atoi(&config[10]); if (m <= 64) { _prefs->flood_max = m; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, max 64"); } } else if (memcmp(config, "direct.txdelay ", 15) == 0) { float f = atof(&config[15]); if (f >= 0 && f <= 2.0f) { _prefs->direct_tx_delay_factor = f; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-2"); } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) { *dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars config++; } *dp = 0; savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "path.hash.mode ", 15) == 0) { config += 15; uint8_t mode = atoi(config); if (mode < 3) { _prefs->path_hash_mode = mode; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0,1, or 2"); } } else if (memcmp(config, "loop.detect ", 12) == 0) { config += 12; uint8_t mode; if (memcmp(config, "off", 3) == 0) { mode = LOOP_DETECT_OFF; } else if (memcmp(config, "minimal", 7) == 0) { mode = LOOP_DETECT_MINIMAL; } else if (memcmp(config, "moderate", 8) == 0) { mode = LOOP_DETECT_MODERATE; } else if (memcmp(config, "strict", 6) == 0) { mode = LOOP_DETECT_STRICT; } else { mode = 0xFF; strcpy(reply, "Error, must be: off, minimal, moderate, or strict"); } if (mode != 0xFF) { _prefs->loop_detect = mode; savePrefs(); strcpy(reply, "OK"); } } else if (memcmp(config, "tx ", 3) == 0) { _prefs->tx_power_dbm = atoi(&config[3]); savePrefs(); _callbacks->setTxPower(_prefs->tx_power_dbm); strcpy(reply, "OK"); } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { _prefs->freq = atof(&config[5]); savePrefs(); strcpy(reply, "OK - reboot to apply"); #ifdef WITH_BRIDGE } else if (memcmp(config, "bridge.enabled ", 15) == 0) { _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0; _callbacks->setBridgeState(_prefs->bridge_enabled); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "bridge.delay ", 13) == 0) { int delay = _atoi(&config[13]); if (delay >= 0 && delay <= 10000) { _prefs->bridge_delay = (uint16_t)delay; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error: delay must be between 0-10000 ms"); } } else if (memcmp(config, "bridge.source ", 14) == 0) { _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0; #ifdef WITH_MQTT_BRIDGE if (_prefs->bridge_pkt_src == 1) { _mqtt_prefs.mqtt_rx_enabled = 1; _mqtt_prefs.mqtt_tx_enabled = 0; } else { _mqtt_prefs.mqtt_rx_enabled = 0; _mqtt_prefs.mqtt_tx_enabled = 1; } #endif savePrefs(); strcpy(reply, "OK"); #endif #ifdef WITH_RS232_BRIDGE } else if (memcmp(config, "bridge.baud ", 12) == 0) { uint32_t baud = atoi(&config[12]); if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) { _prefs->bridge_baud = (uint32_t)baud; _callbacks->restartBridge(); savePrefs(); strcpy(reply, "OK"); } else { sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD); } #endif #ifdef WITH_ESPNOW_BRIDGE } else if (memcmp(config, "bridge.channel ", 15) == 0) { int ch = atoi(&config[15]); if (ch > 0 && ch < 15) { _prefs->bridge_channel = (uint8_t)ch; _callbacks->restartBridge(); savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error: channel must be between 1-14"); } } else if (memcmp(config, "bridge.secret ", 14) == 0) { StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret)); _callbacks->restartBridge(); savePrefs(); strcpy(reply, "OK"); #endif } else if (memcmp(config, "adc.multiplier ", 15) == 0) { _prefs->adc_multiplier = atof(&config[15]); if (_board->setAdcMultiplier(_prefs->adc_multiplier)) { savePrefs(); if (_prefs->adc_multiplier == 0.0f) { strcpy(reply, "OK - using default board multiplier"); } else { sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier); } } else { _prefs->adc_multiplier = 0.0f; strcpy(reply, "Error: unsupported by this board"); }; } else { sprintf(reply, "unknown config: %s", config); } } void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; // Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp. if (handleObserverGetCmd(sender_timestamp, config, reply)) return; if (memcmp(config, "dutycycle", 9) == 0) { float dc = 100.0f / (_prefs->airtime_factor + 1.0f); int dc_int = (int)dc; int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); sprintf(reply, "> %d.%d%%", dc_int, dc_frac); } else if (memcmp(config, "af", 2) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); } else if (memcmp(config, "cad", 3) == 0) { sprintf(reply, "> %s", _prefs->cad_enabled ? "on" : "off"); } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); } else { sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); } } else if (memcmp(config, "agc.reset.interval", 18) == 0) { sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); } else if (memcmp(config, "multi.acks", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks); } else if (memcmp(config, "allow.read.only", 15) == 0) { sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off"); } else if (memcmp(config, "flood.advert.interval", 21) == 0) { sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval)); } else if (memcmp(config, "advert.interval", 15) == 0) { sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2); } else if (memcmp(config, "guest.password", 14) == 0) { sprintf(reply, "> %s", _prefs->guest_password); } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only uint8_t prv_key[PRV_KEY_SIZE]; int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE); mesh::Utils::toHex(tmp, prv_key, len); sprintf(reply, "> %s", tmp); } else if (memcmp(config, "name", 4) == 0) { sprintf(reply, "> %s", _prefs->node_name); } else if (memcmp(config, "repeat", 6) == 0) { sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on"); } else if (memcmp(config, "lat", 3) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); } else if (memcmp(config, "lon", 3) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); #if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) } else if (memcmp(config, "radio.rxgain", 12) == 0) { sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); #endif } else if (memcmp(config, "radio", 5) == 0) { char freq[16], bw[16]; strcpy(freq, StrHelper::ftoa(_prefs->freq)); strcpy(bw, StrHelper::ftoa3(_prefs->bw)); sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr); } else if (memcmp(config, "rxdelay", 7) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base)); } else if (memcmp(config, "txdelay", 7) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (memcmp(config, "flood.max.advert", 16) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert); } else if (memcmp(config, "flood.max.unscoped", 18) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; const char* sp = _prefs->owner_info; while (*sp) { *reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|' sp++; } *reply = 0; // set null terminator } else if (memcmp(config, "path.hash.mode", 14) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); } else if (memcmp(config, "loop.detect", 11) == 0) { if (_prefs->loop_detect == LOOP_DETECT_OFF) { strcpy(reply, "> off"); } else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) { strcpy(reply, "> minimal"); } else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) { strcpy(reply, "> moderate"); } else { strcpy(reply, "> strict"); } } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); } else if (memcmp(config, "freq", 4) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); } else if (memcmp(config, "public.key", 10) == 0) { strcpy(reply, "> "); mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); } else if (memcmp(config, "role", 4) == 0) { sprintf(reply, "> %s", _callbacks->getRole()); } else if (memcmp(config, "bridge.type", 11) == 0) { sprintf(reply, "> %s", #ifdef WITH_RS232_BRIDGE "rs232" #elif WITH_ESPNOW_BRIDGE "espnow" #else "none" #endif ); #ifdef WITH_BRIDGE } else if (memcmp(config, "bridge.enabled", 14) == 0) { sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off"); } else if (memcmp(config, "bridge.delay", 12) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay); } else if (memcmp(config, "bridge.source", 13) == 0) { sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx"); #endif #ifdef WITH_RS232_BRIDGE } else if (memcmp(config, "bridge.baud", 11) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud); #endif #ifdef WITH_ESPNOW_BRIDGE } else if (memcmp(config, "bridge.channel", 14) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel); } else if (memcmp(config, "bridge.secret", 13) == 0) { sprintf(reply, "> %s", _prefs->bridge_secret); #endif } else if (memcmp(config, "bootloader.ver", 14) == 0) { #ifdef NRF52_PLATFORM char ver[32]; if (_board->getBootloaderVersion(ver, sizeof(ver))) { sprintf(reply, "> %s", ver); } else { strcpy(reply, "> unknown"); } #else strcpy(reply, "ERROR: unsupported"); #endif } else if (memcmp(config, "adc.multiplier", 14) == 0) { float adc_mult = _board->getAdcMultiplier(); if (adc_mult == 0.0f) { strcpy(reply, "Error: unsupported by this board"); } else { sprintf(reply, "> %.3f", adc_mult); } // Power management commands } else if (memcmp(config, "pwrmgt.support", 14) == 0) { #ifdef NRF52_POWER_MANAGEMENT strcpy(reply, "> supported"); #else strcpy(reply, "> unsupported"); #endif } else if (memcmp(config, "pwrmgt.source", 13) == 0) { #ifdef NRF52_POWER_MANAGEMENT strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery"); #else strcpy(reply, "ERROR: Power management not supported"); #endif } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) { #ifdef NRF52_POWER_MANAGEMENT sprintf(reply, "> Reset: %s; Shutdown: %s", _board->getResetReasonString(_board->getResetReason()), _board->getShutdownReasonString(_board->getShutdownReason())); #else strcpy(reply, "ERROR: Power management not supported"); #endif } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) { #ifdef NRF52_POWER_MANAGEMENT sprintf(reply, "> %u mV", _board->getBootVoltage()); #else strcpy(reply, "ERROR: Power management not supported"); #endif } else { sprintf(reply, "??: %s", config); } } static char* skipSpaces(char* s) { while (*s == ' ') s++; return s; } static void rtrimSpaces(char* s) { char* e = s + strlen(s); while (e > s && e[-1] == ' ') *--e = '\0'; } static char* takeToken(char** cursor) { char* p = skipSpaces(*cursor); if (*p == '\0') { *cursor = p; return nullptr; } char* tok = p; while (*p && *p != ' ') p++; if (*p) *p++ = '\0'; *cursor = p; return tok; } static char* splitNameJump(char* tok) { for (char* q = tok; *q; q++) { if (*q == '|' || *q == ',') { *q = '\0'; char* jump = skipSpaces(q + 1); rtrimSpaces(jump); return jump; } } return nullptr; } static bool processRegionDefSegment(RegionMap* map, char* tok, RegionEntry** cursor, char* reply) { char* jump = splitNameJump(tok); char* name = skipSpaces(tok); if (*name == '\0') { snprintf(reply, 160, "Err - empty name"); return false; } if (jump && *jump == '\0') { snprintf(reply, 160, "Err - empty jump"); return false; } RegionEntry* r = map->putRegion(name, (*cursor)->id); if (r == NULL) { snprintf(reply, 160, "Err - put failed: %s", name); return false; } r->flags = 0; if (jump) { RegionEntry* j = map->findByNamePrefix(jump); if (j == NULL) { snprintf(reply, 160, "Err - unknown jump: %s", jump); return false; } *cursor = j; } else { *cursor = r; } return true; } void CommonCLI::handleRegionCmd(char* command, char* reply) { reply[0] = 0; // `region def`: must run before parseTextParts mutates the buffer char* cmd = skipSpaces(command); if (strncmp(cmd, "region def", 10) == 0 && (cmd[10] == ' ' || cmd[10] == '\0')) { char* payload = skipSpaces(cmd + 10); rtrimSpaces(payload); if (*payload == '\0') { snprintf(reply, 160, "Err - empty def"); return; } RegionEntry* cursor = &_region_map->getWildcard(); for (char* tok; (tok = takeToken(&payload)) != nullptr; ) { if (!processRegionDefSegment(_region_map, tok, &cursor, reply)) return; } _region_map->exportTo(reply, 160); return; } const char* parts[4]; int n = mesh::Utils::parseTextParts(command, parts, 4, ' '); if (n == 1) { _region_map->exportTo(reply, 160); } else if (n >= 2 && strcmp(parts[1], "load") == 0) { _callbacks->startRegionsLoad(); } else if (n >= 2 && strcmp(parts[1], "save") == 0) { _prefs->discovery_mod_timestamp = getRTCClock()->getCurrentTime(); // this node is now 'modified' (for discovery info) savePrefs(); bool success = _callbacks->saveRegions(); strcpy(reply, success ? "OK" : "Err - save failed"); } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) { auto region = _region_map->findByNamePrefix(parts[2]); if (region) { region->flags &= ~REGION_DENY_FLOOD; strcpy(reply, "OK"); } else { strcpy(reply, "Err - unknown region"); } } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) { auto region = _region_map->findByNamePrefix(parts[2]); if (region) { region->flags |= REGION_DENY_FLOOD; strcpy(reply, "OK"); } else { strcpy(reply, "Err - unknown region"); } } else if (n >= 3 && strcmp(parts[1], "get") == 0) { auto region = _region_map->findByNamePrefix(parts[2]); if (region) { auto parent = _region_map->findById(region->parent); if (parent && parent->id != 0) { sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); } else { sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); } } else { strcpy(reply, "Err - unknown region"); } } else if (n >= 3 && strcmp(parts[1], "home") == 0) { auto home = _region_map->findByNamePrefix(parts[2]); if (home) { _region_map->setHomeRegion(home); sprintf(reply, " home is now %s", home->name); } else { strcpy(reply, "Err - unknown region"); } } else if (n == 2 && strcmp(parts[1], "home") == 0) { auto home = _region_map->getHomeRegion(); sprintf(reply, " home is %s", home ? home->name : "*"); } else if (n >= 3 && strcmp(parts[1], "default") == 0) { if (strcmp(parts[2], "") == 0) { _region_map->setDefaultRegion(NULL); _callbacks->onDefaultRegionChanged(NULL); _callbacks->saveRegions(); // persist in one atomic step sprintf(reply, " default scope is now "); } else { auto def = _region_map->findByNamePrefix(parts[2]); if (def == NULL) { def = _region_map->putRegion(parts[2], 0); // auto-create the default region } if (def) { def->flags = 0; // make sure allow flood enabled _region_map->setDefaultRegion(def); _callbacks->onDefaultRegionChanged(def); _callbacks->saveRegions(); // persist in one atomic step sprintf(reply, " default scope is now %s", def->name); } else { strcpy(reply, "Err - region table full"); } } } else if (n == 2 && strcmp(parts[1], "default") == 0) { auto def = _region_map->getDefaultRegion(); sprintf(reply, " default scope is %s", def ? def->name : ""); } else if (n >= 3 && strcmp(parts[1], "put") == 0) { auto parent = n >= 4 ? _region_map->findByNamePrefix(parts[3]) : &(_region_map->getWildcard()); if (parent == NULL) { strcpy(reply, "Err - unknown parent"); } else { auto region = _region_map->putRegion(parts[2], parent->id); if (region == NULL) { strcpy(reply, "Err - unable to put"); } else { region->flags = 0; // New default: enable flood strcpy(reply, "OK - (flood allowed)"); } } } else if (n >= 3 && strcmp(parts[1], "remove") == 0) { auto region = _region_map->findByName(parts[2]); if (region) { if (_region_map->removeRegion(*region)) { strcpy(reply, "OK"); } else { strcpy(reply, "Err - not empty"); } } else { strcpy(reply, "Err - not found"); } } else if (n >= 3 && strcmp(parts[1], "list") == 0) { uint8_t mask = 0; bool invert = false; if (strcmp(parts[2], "allowed") == 0) { mask = REGION_DENY_FLOOD; invert = false; // list regions that DON'T have DENY flag } else if (strcmp(parts[2], "denied") == 0) { mask = REGION_DENY_FLOOD; invert = true; // list regions that DO have DENY flag } else { strcpy(reply, "Err - use 'allowed' or 'denied'"); return; } int len = _region_map->exportNamesTo(reply, 160, mask, invert); if (len == 0) { strcpy(reply, "-none-"); } } else { strcpy(reply, "Err - ??"); } }