5661 lines
243 KiB
C++
5661 lines
243 KiB
C++
#include <Arduino.h>
|
|
#include "CommonCLI.h"
|
|
#include "PrefsSaveReplyGuard.h"
|
|
#include "UsbLoggingWatchdog.h"
|
|
#include "HilStartupTrace.h"
|
|
#include <helpers/ui/DisplayPowerSettings.h>
|
|
#include "CLICommandUtils.h"
|
|
#include "GpsPowerPolicy.h"
|
|
#include "FloodAdvertCLI.h"
|
|
#include "StorageLayout.h"
|
|
#include "radiolib/RadioPowerLimits.h"
|
|
#include "radiolib/LR2021SideDetectorConfig.h"
|
|
#include "TxtDataHelpers.h"
|
|
#include "AdvertDataHelpers.h"
|
|
#include "AlertReporter.h" // for alertReporterBannedChannelMatch()
|
|
#include "sensors/EnvironmentI2CConfig.h"
|
|
#if defined(NRF52_POWER_MANAGEMENT)
|
|
#include "NRF52VoltagePolicy.h"
|
|
#endif
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
#include "AtomicFileWriter.h"
|
|
#elif defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
|
#include "ContactFileTransaction.h"
|
|
#endif
|
|
#include <RTClib.h>
|
|
#include <ctype.h>
|
|
#include <Utils.h>
|
|
#include <math.h>
|
|
#include <new>
|
|
#include <stddef.h>
|
|
|
|
#define STR_HELPER(x) #x
|
|
#define STR(x) STR_HELPER(x)
|
|
#if defined(ENABLE_OTA)
|
|
#include "ota/OtaCli.h"
|
|
#include "ota/OtaContext.h" // persist/sync OTA policy + signer allowlist with NodePrefs
|
|
#include "ota/OtaConfigState.h"
|
|
#include "ota/OtaSpeedConfig.h"
|
|
#endif
|
|
|
|
#ifndef BRIDGE_MAX_BAUD
|
|
#define BRIDGE_MAX_BAUD 115200
|
|
#endif
|
|
#ifdef ESP_PLATFORM
|
|
#include <WiFi.h>
|
|
#include <WiFiClientSecure.h>
|
|
#include <esp_wifi.h>
|
|
#include <esp_heap_caps.h>
|
|
#else
|
|
#include <malloc.h> // mallinfo() for the `memory` command on nRF52/RP2040
|
|
#endif
|
|
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
|
|
#include <helpers/esp32/WiFiRadioPolicy.h>
|
|
#endif
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
#include "bridges/MQTTBridge.h"
|
|
#include "CommonPrefsRecovery.h"
|
|
#include "FilePresence.h"
|
|
#include "MQTTDefaults.h"
|
|
#include "MQTTPrefsAtomicStore.h"
|
|
#include "MQTTPrefsCodec.h"
|
|
#include "MQTTPrefsJsonImport.h"
|
|
#include "MQTTPrefsRecovery.h"
|
|
#endif
|
|
|
|
#define RECENT_REPEATER_PREFIX_MAX_BYTES 3
|
|
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
static const uint32_t SDCARD_ACTION_COOLDOWN_MILLIS = 5UL * 60UL * 1000UL;
|
|
|
|
static void formatSdCardElapsed(char* out, size_t out_len, bool recorded,
|
|
uint32_t ran_at, uint32_t now) {
|
|
if (!recorded) {
|
|
snprintf(out, out_len, "never");
|
|
return;
|
|
}
|
|
|
|
const uint32_t elapsed_secs = (now - ran_at) / 1000UL;
|
|
if (elapsed_secs < 60UL) {
|
|
snprintf(out, out_len, "%lus ago", (unsigned long)elapsed_secs);
|
|
} else if (elapsed_secs < 60UL * 60UL) {
|
|
snprintf(out, out_len, "%lum %lus ago",
|
|
(unsigned long)(elapsed_secs / 60UL),
|
|
(unsigned long)(elapsed_secs % 60UL));
|
|
} else if (elapsed_secs < 24UL * 60UL * 60UL) {
|
|
snprintf(out, out_len, "%luh %lum ago",
|
|
(unsigned long)(elapsed_secs / 3600UL),
|
|
(unsigned long)((elapsed_secs / 60UL) % 60UL));
|
|
} else {
|
|
snprintf(out, out_len, "%lud %luh ago",
|
|
(unsigned long)(elapsed_secs / 86400UL),
|
|
(unsigned long)((elapsed_secs / 3600UL) % 24UL));
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
#include <helpers/radiolib/RXPowerSaving.h>
|
|
|
|
// 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;
|
|
}
|
|
|
|
#if defined(ENABLE_OTA)
|
|
static bool commandTokenMatches(const char* command, const char* token) {
|
|
const size_t len = strlen(token);
|
|
return strncmp(command, token, len) == 0 && (command[len] == 0 || command[len] == ' ');
|
|
}
|
|
|
|
static bool otaCommandNeedsTempRadio(const char* command) {
|
|
const char* action = command + 3;
|
|
while (*action == ' ') action++;
|
|
|
|
return commandTokenMatches(action, "neighbors")
|
|
|| commandTokenMatches(action, "nbrs")
|
|
|| commandTokenMatches(action, "updates")
|
|
|| commandTokenMatches(action, "ls")
|
|
|| commandTokenMatches(action, "n")
|
|
|| commandTokenMatches(action, "pull")
|
|
|| commandTokenMatches(action, "get")
|
|
|| commandTokenMatches(action, "download")
|
|
|| commandTokenMatches(action, "announce")
|
|
|| commandTokenMatches(action, "adv")
|
|
|| commandTokenMatches(action, "folder on")
|
|
|| commandTokenMatches(action, "folder off")
|
|
|| commandTokenMatches(action, "fold on")
|
|
|| commandTokenMatches(action, "fold off")
|
|
|| commandTokenMatches(action, "dev announce");
|
|
}
|
|
#endif
|
|
|
|
static bool isValidName(const char *n) {
|
|
while (*n) {
|
|
if (*n == '[' || *n == ']' || *n == '/' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
|
|
n++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
|
static bool isGpioConfig(const char* config) {
|
|
static const char expected[] = "gpio";
|
|
for (size_t i = 0; i < sizeof(expected) - 1; i++) {
|
|
if (config[i] == '\0' || tolower((unsigned char)config[i]) != expected[i]) return false;
|
|
}
|
|
return config[sizeof(expected) - 1] == '\0' || config[sizeof(expected) - 1] == ' ';
|
|
}
|
|
#endif
|
|
|
|
void CommonCLI::loop() {
|
|
_radio_profiles.loop();
|
|
loopManagement();
|
|
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
|
_user_gpio.loop();
|
|
UserGpio::Completion completion;
|
|
while (_user_gpio.takeCompletion(completion)) {
|
|
_callbacks->onUserGpioTimerCompleted(completion.pin, (uint8_t)completion.state,
|
|
completion.request_id);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool CommonCLI::hasActiveUserGpioTimer() const {
|
|
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
|
return _user_gpio.hasActiveTimer();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
// 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)
|
|
|
|
static bool looksNumeric(const char* s) {
|
|
if (s == NULL) return false;
|
|
while (*s == ' ') s++;
|
|
if (*s == '-' || *s == '+') s++;
|
|
bool saw_digit = false;
|
|
bool saw_dot = false;
|
|
while (*s) {
|
|
if (*s >= '0' && *s <= '9') {
|
|
saw_digit = true;
|
|
} else if (*s == '.' && !saw_dot) {
|
|
saw_dot = true;
|
|
} else if (*s == ' ') {
|
|
while (*s == ' ') s++;
|
|
return saw_digit && *s == 0;
|
|
} else {
|
|
break;
|
|
}
|
|
s++;
|
|
}
|
|
return saw_digit && *s == 0;
|
|
}
|
|
|
|
static bool looksUnsignedInteger(const char* s) {
|
|
if (s == NULL) return false;
|
|
while (*s == ' ') s++;
|
|
bool saw_digit = false;
|
|
while (*s) {
|
|
if (*s >= '0' && *s <= '9') {
|
|
saw_digit = true;
|
|
} else if (*s == ' ') {
|
|
while (*s == ' ') s++;
|
|
return saw_digit && *s == 0;
|
|
} else {
|
|
return false;
|
|
}
|
|
s++;
|
|
}
|
|
return saw_digit;
|
|
}
|
|
|
|
static bool configKeyMatches(const char* config, const char* key) {
|
|
if (config == NULL || key == NULL) return false;
|
|
size_t key_len = strlen(key);
|
|
return strncmp(config, key, key_len) == 0
|
|
&& (config[key_len] == 0 || config[key_len] == ' ' || config[key_len] == '.');
|
|
}
|
|
|
|
static bool configKeyEquals(const char* config, const char* key) {
|
|
return mesh::cli::matchNoArgCommand(config, key)
|
|
== mesh::cli::NoArgCommandMatch::Exact;
|
|
}
|
|
|
|
static bool isAdvancedRetryConfig(const char* config) {
|
|
return configKeyMatches(config, "direct.retry.heard")
|
|
|| configKeyMatches(config, "direct.retry.margin")
|
|
|| configKeyMatches(config, "flood.retry.prefixes")
|
|
|| configKeyMatches(config, "flood.retry.ignore")
|
|
|| configKeyMatches(config, "flood.retry.bridge")
|
|
|| configKeyMatches(config, "flood.retry.bucket")
|
|
|| configKeyMatches(config, "recent.repeater")
|
|
|| configKeyMatches(config, "recent.repeaters");
|
|
}
|
|
|
|
static bool isBasicRetryConfig(const char* config) {
|
|
return configKeyMatches(config, "retry.preset")
|
|
|| configKeyMatches(config, "direct.retry")
|
|
|| configKeyMatches(config, "flood.retry.count")
|
|
|| configKeyMatches(config, "flood.retry.path")
|
|
|| configKeyMatches(config, "flood.retry.group.path")
|
|
|| configKeyMatches(config, "flood.retry.advert");
|
|
}
|
|
|
|
static bool parseUint8Strict(const char* value, uint8_t min_value, uint8_t max_value, uint8_t& result) {
|
|
if (value == NULL || *value == 0) {
|
|
return false;
|
|
}
|
|
|
|
uint16_t parsed = 0;
|
|
const char* sp = value;
|
|
while (*sp) {
|
|
if (*sp < '0' || *sp > '9') {
|
|
return false;
|
|
}
|
|
parsed = (uint16_t)((parsed * 10) + (*sp - '0'));
|
|
if (parsed > max_value) {
|
|
return false;
|
|
}
|
|
sp++;
|
|
}
|
|
if (parsed < min_value) {
|
|
return false;
|
|
}
|
|
result = (uint8_t)parsed;
|
|
return true;
|
|
}
|
|
|
|
static bool parseOnOffStrict(const char* value, bool& enabled) {
|
|
if (strcmp(value, "on") == 0) {
|
|
enabled = true;
|
|
return true;
|
|
}
|
|
if (strcmp(value, "off") == 0) {
|
|
enabled = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool bwMatches(float bw, float allowed) {
|
|
float diff = bw - allowed;
|
|
if (diff < 0.0f) diff = -diff;
|
|
return diff <= 0.001f;
|
|
}
|
|
|
|
static bool isValidLoRaBandwidth(float bw) {
|
|
#if defined(USE_LR1110)
|
|
return bwMatches(bw, 62.5f)
|
|
|| bwMatches(bw, 125.0f)
|
|
|| bwMatches(bw, 250.0f)
|
|
|| bwMatches(bw, 500.0f);
|
|
#elif defined(USE_LLCC68) || defined(USE_SX1272)
|
|
return bwMatches(bw, 125.0f)
|
|
|| bwMatches(bw, 250.0f)
|
|
|| bwMatches(bw, 500.0f);
|
|
#else
|
|
return bwMatches(bw, 7.8f)
|
|
|| bwMatches(bw, 10.4f)
|
|
|| bwMatches(bw, 15.6f)
|
|
|| bwMatches(bw, 20.8f)
|
|
|| bwMatches(bw, 31.25f)
|
|
|| bwMatches(bw, 41.7f)
|
|
|| bwMatches(bw, 62.5f)
|
|
|| bwMatches(bw, 125.0f)
|
|
|| bwMatches(bw, 250.0f)
|
|
|| bwMatches(bw, 500.0f);
|
|
#endif
|
|
}
|
|
|
|
static float defaultLoRaBandwidth() {
|
|
#ifdef LORA_BW
|
|
if (isValidLoRaBandwidth((float)LORA_BW)) {
|
|
return (float)LORA_BW;
|
|
}
|
|
#endif
|
|
return 125.0f;
|
|
}
|
|
|
|
static const char* skipSpacesConst(const char* s) {
|
|
while (s != NULL && *s == ' ') s++;
|
|
return s;
|
|
}
|
|
|
|
static bool parseUint32Strict(const char* s, uint32_t& out) {
|
|
if (!looksUnsignedInteger(s)) {
|
|
return false;
|
|
}
|
|
|
|
uint64_t n = 0;
|
|
s = skipSpacesConst(s);
|
|
while (*s >= '0' && *s <= '9') {
|
|
n = (n * 10) + (uint32_t)(*s - '0');
|
|
if (n > 0xFFFFFFFFULL) {
|
|
return false;
|
|
}
|
|
s++;
|
|
}
|
|
out = (uint32_t)n;
|
|
return true;
|
|
}
|
|
|
|
static int countSeparatedParts(const char* s, char separator) {
|
|
if (s == NULL || *s == 0) {
|
|
return 0;
|
|
}
|
|
|
|
int count = 1;
|
|
while (*s) {
|
|
if (*s++ == separator) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static bool parseScheduledRadioArgs(const char* args, bool temporary, float& freq, float& bw,
|
|
uint8_t& sf, uint8_t& cr, uint32_t& start_time,
|
|
uint32_t& end_time, uint32_t now) {
|
|
const int expected_parts = temporary ? 6 : 5;
|
|
args = skipSpacesConst(args);
|
|
if (countSeparatedParts(args, ',') != expected_parts) {
|
|
return false;
|
|
}
|
|
char local[96];
|
|
if (strlen(args) >= sizeof(local)) {
|
|
return false;
|
|
}
|
|
StrHelper::strncpy(local, args, sizeof(local));
|
|
const char* parts[6];
|
|
int num = mesh::Utils::parseTextParts(local, parts, expected_parts, ',');
|
|
if (num != expected_parts) {
|
|
return false;
|
|
}
|
|
|
|
uint32_t sf_u32 = 0;
|
|
uint32_t cr_u32 = 0;
|
|
if (!mesh::cli::parseDecimalStrict(parts[0], freq)
|
|
|| !mesh::cli::parseDecimalStrict(parts[1], bw)
|
|
|| !parseUint32Strict(parts[2], sf_u32)
|
|
|| !parseUint32Strict(parts[3], cr_u32)
|
|
|| !mesh::cli::parseRadioScheduleTime(parts[4], now, start_time)) {
|
|
return false;
|
|
}
|
|
if (sf_u32 > 255 || cr_u32 > 255) {
|
|
return false;
|
|
}
|
|
|
|
sf = (uint8_t)sf_u32;
|
|
cr = (uint8_t)cr_u32;
|
|
if (temporary && !mesh::cli::parseRadioScheduleTime(parts[5], now, end_time)) {
|
|
return false;
|
|
}
|
|
if (!temporary) {
|
|
end_time = 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static int16_t parseSnrDbX4(const char* s) {
|
|
float db = 0.0f;
|
|
mesh::cli::parseDecimalStrict(s, db);
|
|
return (int16_t)(db * 4.0f + (db >= 0.0f ? 0.5f : -0.5f));
|
|
}
|
|
|
|
static void formatSnrDbX4(char* dest, size_t dest_len, int16_t snr_x4) {
|
|
int16_t v = snr_x4;
|
|
const char* sign = "";
|
|
if (v < 0) {
|
|
sign = "-";
|
|
v = -v;
|
|
}
|
|
snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25);
|
|
}
|
|
|
|
static const char* retryPresetName(uint8_t preset) {
|
|
switch (preset) {
|
|
case RETRY_PRESET_INFRA: return "infra";
|
|
case RETRY_PRESET_ROOFTOP: return "rooftop";
|
|
case RETRY_PRESET_MOBILE: return "mobile";
|
|
default: return "custom";
|
|
}
|
|
}
|
|
|
|
bool CommonCLI::calculateRxPowerSavingLevel(uint32_t level, uint8_t sf, float bw, uint32_t preamble,
|
|
uint32_t* rx_us, uint32_t* sleep_us) {
|
|
if (level < 1 || level > 10 || preamble > 0xFF
|
|
|| !isRxPowerSavingPreamble((uint8_t)preamble)) {
|
|
return false;
|
|
}
|
|
return calcRxPowerSavingLevelAtOrAbove(
|
|
(uint8_t)level, sf, bw, (uint8_t)preamble,
|
|
rxPowerSavingDefaultTransitionUs(), rx_us, sleep_us);
|
|
}
|
|
|
|
static void appendRxPowerSavingAdjustmentNote(char* reply, const NodePrefs* prefs,
|
|
uint8_t sf, float bw) {
|
|
if (!reply || !prefs || !prefs->rx_powersaving_enabled
|
|
|| prefs->rx_ps_level < 1 || prefs->rx_ps_level > 10) {
|
|
return;
|
|
}
|
|
|
|
uint32_t rx_us = 0;
|
|
uint32_t sleep_us = 0;
|
|
uint8_t effective_level = 0;
|
|
uint8_t effective_preamble = 0;
|
|
if (!recalcRxPowerSavingFromLevel(
|
|
prefs->rx_ps_level, sf, bw, prefs->rx_ps_preamble,
|
|
&rx_us, &sleep_us, &effective_level, &effective_preamble)) {
|
|
return;
|
|
}
|
|
|
|
char note[96] = {};
|
|
if (rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)) {
|
|
snprintf(note, sizeof(note),
|
|
"; RXPS continuous-fast (requested level %u)",
|
|
(unsigned)prefs->rx_ps_level);
|
|
} else {
|
|
const uint8_t requested_preamble = prefs->rx_ps_preamble == 0
|
|
? rxPowerSavingPreambleForParams(sf, bw) : prefs->rx_ps_preamble;
|
|
if (effective_level == prefs->rx_ps_level
|
|
&& effective_preamble == requested_preamble) {
|
|
return;
|
|
}
|
|
snprintf(note, sizeof(note),
|
|
"; RXPS effective level %u, preamble %u (saved minimum %u)",
|
|
(unsigned)effective_level, (unsigned)effective_preamble,
|
|
(unsigned)prefs->rx_ps_level);
|
|
}
|
|
|
|
const size_t used = strlen(reply);
|
|
if (used < 159) {
|
|
snprintf(reply + used, 160 - used, "%s", note);
|
|
}
|
|
}
|
|
|
|
// Recomputes rx_ps_rx_us/rx_ps_sleep_us from the stored level and the current
|
|
// radio SF/BW. No-op (returns false) for manual timings (rx_ps_level == 0).
|
|
// Lets level-based RX powersaving auto-retune when SF/BW change.
|
|
bool CommonCLI::recalculateRxPowerSavingFromLevel(NodePrefs* prefs) {
|
|
return recalcRxPowerSavingFromLevel(prefs->rx_ps_level, prefs->sf, prefs->bw,
|
|
prefs->rx_ps_preamble, &prefs->rx_ps_rx_us,
|
|
&prefs->rx_ps_sleep_us);
|
|
}
|
|
|
|
static void markDirectRetryPrefsValid(NodePrefs* prefs) {
|
|
prefs->direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0;
|
|
prefs->direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1;
|
|
}
|
|
|
|
static void applyFloodRetryPreset(NodePrefs* prefs, uint8_t preset) {
|
|
if (preset == RETRY_PRESET_INFRA) {
|
|
prefs->flood_retry_attempts = FLOOD_RETRY_INFRA_COUNT;
|
|
prefs->flood_retry_max_path = FLOOD_RETRY_INFRA_MAX_PATH;
|
|
} else if (preset == RETRY_PRESET_MOBILE) {
|
|
prefs->flood_retry_attempts = FLOOD_RETRY_MOBILE_COUNT;
|
|
prefs->flood_retry_max_path = FLOOD_RETRY_MOBILE_MAX_PATH;
|
|
} else {
|
|
prefs->flood_retry_attempts = FLOOD_RETRY_ROOFTOP_COUNT;
|
|
prefs->flood_retry_max_path = FLOOD_RETRY_ROOFTOP_MAX_PATH;
|
|
}
|
|
prefs->flood_retry_group_max_path = prefs->flood_retry_max_path == 0
|
|
? FLOOD_RETRY_PATH_GATE_DISABLED
|
|
: FLOOD_RETRY_GROUP_MAX_PATH_DEFAULT;
|
|
}
|
|
|
|
static bool parseFloodRetryPathGate(const char* value, uint8_t& path_gate) {
|
|
if (value == NULL) {
|
|
return false;
|
|
}
|
|
if (strcmp(value, "off") == 0 || strcmp(value, "disabled") == 0 || strcmp(value, "disable") == 0) {
|
|
path_gate = FLOOD_RETRY_PATH_GATE_DISABLED;
|
|
return true;
|
|
}
|
|
return parseUint8Strict(value, 0, 63, path_gate);
|
|
}
|
|
|
|
static void formatFloodRetryPathGate(char* dest, uint8_t path_gate) {
|
|
if (path_gate == FLOOD_RETRY_PATH_GATE_DISABLED) {
|
|
strcpy(dest, "off");
|
|
} else {
|
|
sprintf(dest, "%u", (unsigned int)path_gate);
|
|
}
|
|
}
|
|
|
|
static bool parseFloodChannelHops(const char* value, uint8_t& max_hops) {
|
|
if (value == NULL) {
|
|
return false;
|
|
}
|
|
value = skipSpacesConst(value);
|
|
if (strcmp(value, "all") == 0) {
|
|
max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
|
return true;
|
|
}
|
|
return parseUint8Strict(value, 1, 7, max_hops);
|
|
}
|
|
|
|
static void formatFloodChannelHops(char* dest, uint8_t max_hops) {
|
|
if (max_hops == FLOOD_CHANNEL_HOPS_ALL) {
|
|
strcpy(dest, "h=all");
|
|
} else {
|
|
sprintf(dest, "h>%u", (unsigned int)max_hops);
|
|
}
|
|
}
|
|
|
|
static void formatFloodRetryPrefixList(char* dest, const uint8_t prefixes[][FLOOD_RETRY_PREFIX_LEN],
|
|
uint8_t max_prefixes) {
|
|
char* out = dest;
|
|
bool first = true;
|
|
for (int i = 0; i < max_prefixes; i++) {
|
|
const uint8_t* prefix = prefixes[i];
|
|
if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) {
|
|
continue;
|
|
}
|
|
if (!first) {
|
|
*out++ = ',';
|
|
}
|
|
mesh::Utils::toHex(out, prefix, FLOOD_RETRY_PREFIX_LEN);
|
|
out += FLOOD_RETRY_PREFIX_LEN * 2;
|
|
first = false;
|
|
}
|
|
*out = 0;
|
|
}
|
|
|
|
static bool parseFloodRetryPrefixList(uint8_t dest[][FLOOD_RETRY_PREFIX_LEN], uint8_t max_prefixes, const char* value) {
|
|
if (max_prefixes > FLOOD_RETRY_LIST_PREFIXES) {
|
|
return false;
|
|
}
|
|
uint8_t parsed[FLOOD_RETRY_LIST_PREFIXES][FLOOD_RETRY_PREFIX_LEN];
|
|
memset(parsed, 0, sizeof(parsed));
|
|
if (value == NULL || value[0] == 0 || strcmp(value, "none") == 0 || strcmp(value, "off") == 0) {
|
|
memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN);
|
|
return true;
|
|
}
|
|
|
|
char local[FLOOD_RETRY_LIST_TEXT_MAX];
|
|
StrHelper::strncpy(local, value, sizeof(local));
|
|
const char* parts[FLOOD_RETRY_LIST_PREFIXES + 1];
|
|
int num = mesh::Utils::parseTextParts(local, parts, FLOOD_RETRY_LIST_PREFIXES + 1);
|
|
if (num > max_prefixes) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < num; i++) {
|
|
if (strlen(parts[i]) != FLOOD_RETRY_PREFIX_LEN * 2) {
|
|
return false;
|
|
}
|
|
for (int j = 0; j < FLOOD_RETRY_PREFIX_LEN * 2; j++) {
|
|
if (!mesh::Utils::isHexChar(parts[i][j])) {
|
|
return false;
|
|
}
|
|
}
|
|
if (!mesh::Utils::fromHex(parsed[i], FLOOD_RETRY_PREFIX_LEN, parts[i])
|
|
|| (parsed[i][0] == 0 && parsed[i][1] == 0 && parsed[i][2] == 0)) {
|
|
return false;
|
|
}
|
|
}
|
|
memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN);
|
|
return true;
|
|
}
|
|
|
|
static const uint8_t* floodRetryBucketPrefixAt(const NodePrefs* prefs, uint8_t bucket, uint8_t index) {
|
|
if (bucket < FLOOD_RETRY_BRIDGE_BUCKETS && index < FLOOD_RETRY_BUCKET_PREFIXES) {
|
|
return prefs->flood_retry_bridge_buckets[bucket][index];
|
|
}
|
|
if (bucket == FLOOD_RETRY_BRIDGE_BUCKETS && index < FLOOD_RETRY_PREFIX_SLOTS) {
|
|
return prefs->flood_retry_prefixes[index];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static uint8_t floodRetryBucketPrefixCount(uint8_t bucket) {
|
|
return bucket < FLOOD_RETRY_BRIDGE_BUCKETS ? FLOOD_RETRY_BUCKET_PREFIXES : FLOOD_RETRY_PREFIX_SLOTS;
|
|
}
|
|
|
|
static bool findFloodRetryFirstByteCollision(const NodePrefs* prefs, uint8_t& first_byte,
|
|
uint8_t& first_bucket, uint8_t& second_bucket) {
|
|
for (uint8_t bucket_a = 0; bucket_a <= FLOOD_RETRY_BRIDGE_BUCKETS; bucket_a++) {
|
|
for (uint8_t bucket_b = bucket_a + 1; bucket_b <= FLOOD_RETRY_BRIDGE_BUCKETS; bucket_b++) {
|
|
for (uint8_t a = 0; a < floodRetryBucketPrefixCount(bucket_a); a++) {
|
|
const uint8_t* prefix_a = floodRetryBucketPrefixAt(prefs, bucket_a, a);
|
|
if (prefix_a == NULL || (prefix_a[0] == 0 && prefix_a[1] == 0 && prefix_a[2] == 0)) {
|
|
continue;
|
|
}
|
|
for (uint8_t b = 0; b < floodRetryBucketPrefixCount(bucket_b); b++) {
|
|
const uint8_t* prefix_b = floodRetryBucketPrefixAt(prefs, bucket_b, b);
|
|
if (prefix_b != NULL && (prefix_b[0] != 0 || prefix_b[1] != 0 || prefix_b[2] != 0)
|
|
&& prefix_a[0] == prefix_b[0]) {
|
|
first_byte = prefix_a[0];
|
|
first_bucket = bucket_a;
|
|
second_bucket = bucket_b;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool formatFloodRetryBucketCollisionWarning(char* reply, const NodePrefs* prefs, const char* prefix) {
|
|
uint8_t first_byte, first_bucket, second_bucket;
|
|
if (!findFloodRetryFirstByteCollision(prefs, first_byte, first_bucket, second_bucket)) {
|
|
return false;
|
|
}
|
|
if (second_bucket == FLOOD_RETRY_BRIDGE_BUCKETS) {
|
|
snprintf(reply, 160, "%sWARNING: 1-byte %02X matches buckets %u and 7 (other)", prefix,
|
|
(unsigned int)first_byte, (unsigned int)first_bucket + 1U);
|
|
} else {
|
|
snprintf(reply, 160, "%sWARNING: 1-byte %02X matches buckets %u and %u", prefix,
|
|
(unsigned int)first_byte, (unsigned int)first_bucket + 1U,
|
|
(unsigned int)second_bucket + 1U);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) {
|
|
prefs->retry_preset = preset;
|
|
if (preset == RETRY_PRESET_INFRA) {
|
|
prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT;
|
|
prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS;
|
|
prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS;
|
|
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_INFRA_MARGIN_X4;
|
|
} else if (preset == RETRY_PRESET_MOBILE) {
|
|
prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT;
|
|
prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS;
|
|
prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS;
|
|
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_MOBILE_MARGIN_X4;
|
|
} else {
|
|
prefs->retry_preset = RETRY_PRESET_ROOFTOP;
|
|
prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT;
|
|
prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS;
|
|
prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS;
|
|
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4;
|
|
}
|
|
applyFloodRetryPreset(prefs, prefs->retry_preset);
|
|
markDirectRetryPrefsValid(prefs);
|
|
}
|
|
|
|
static void setDefaultDirectRetryPrefs(NodePrefs* prefs) {
|
|
applyDirectRetryPreset(prefs, RETRY_PRESET_ROOFTOP);
|
|
prefs->direct_retry_cr_enabled = 1;
|
|
prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT;
|
|
prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT;
|
|
prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT;
|
|
prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT;
|
|
prefs->direct_retry_enabled = 1;
|
|
prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT;
|
|
markDirectRetryPrefsValid(prefs);
|
|
}
|
|
|
|
static bool directRetryPrefsValid(const NodePrefs* prefs) {
|
|
return prefs->direct_retry_prefs_magic[0] == DIRECT_RETRY_PREFS_MAGIC_0
|
|
&& prefs->direct_retry_prefs_magic[1] == DIRECT_RETRY_PREFS_MAGIC_1;
|
|
}
|
|
|
|
static bool parseRetryPreset(const char* s, uint8_t& preset) {
|
|
if (strcmp(s, "infra") == 0 || strcmp(s, "0") == 0) {
|
|
preset = RETRY_PRESET_INFRA;
|
|
return true;
|
|
}
|
|
if (strcmp(s, "rooftop") == 0 || strcmp(s, "1") == 0) {
|
|
preset = RETRY_PRESET_ROOFTOP;
|
|
return true;
|
|
}
|
|
if (strcmp(s, "mobile") == 0 || strcmp(s, "2") == 0) {
|
|
preset = RETRY_PRESET_MOBILE;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool parseHashPrefix(const char* text, uint8_t* prefix, uint8_t& prefix_len) {
|
|
size_t hex_len = strlen(text);
|
|
if (hex_len == 0 || (hex_len & 1) || hex_len > RECENT_REPEATER_PREFIX_MAX_BYTES * 2) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < hex_len; i++) {
|
|
if (!mesh::Utils::isHexChar(text[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
prefix_len = hex_len / 2;
|
|
return mesh::Utils::fromHex(prefix, prefix_len, text);
|
|
}
|
|
|
|
static void formatSnrDbX4Short(char* dest, size_t dest_len, int16_t snr_x4) {
|
|
formatSnrDbX4(dest, dest_len, snr_x4);
|
|
size_t len = strlen(dest);
|
|
if (len > 3 && dest[len - 1] == '0') {
|
|
dest[len - 1] = 0;
|
|
}
|
|
}
|
|
|
|
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
|
mesh::hilStartupTrace("prefs_profiles_begin");
|
|
_radio_profiles.begin(fs, _callbacks->getProfileRadio(), _rtc, true);
|
|
mesh::hilStartupTrace("prefs_profiles_ready");
|
|
_prefs->primary_radio_preamble = _radio_profiles.primaryPreamble();
|
|
#if !defined(WITH_MQTT_BRIDGE) && (defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM))
|
|
mesh::hilStartupTrace("prefs_recovery_begin");
|
|
mesh::ContactFileTransaction::recover(fs, "/com_prefs");
|
|
mesh::hilStartupTrace("prefs_recovery_ready");
|
|
#endif
|
|
#if defined(ENABLE_OTA)
|
|
mesh::hilStartupTrace("prefs_ota_speed_begin");
|
|
mesh::ota::beginSpeedConfig(fs);
|
|
mesh::hilStartupTrace("prefs_ota_speed_ready");
|
|
#endif
|
|
mesh::hilStartupTrace("prefs_display_begin");
|
|
const bool display_settings_loaded = mesh::ui::loadDisplayPowerSettings(fs, false);
|
|
mesh::hilStartupTrace("prefs_display_ready");
|
|
(void)display_settings_loaded;
|
|
bool is_fresh_install = false;
|
|
bool is_upgrade = false;
|
|
bool loaded = false;
|
|
|
|
// The hardware main-loop watchdog is opt-out. Older preference files do not
|
|
// contain its appended byte, so they safely inherit the enabled default.
|
|
_prefs->system_watchdog_enabled = 1;
|
|
memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf));
|
|
_prefs->usb_logging_enabled = 1;
|
|
_prefs->usb_debug_enabled = 0;
|
|
_prefs->trace_when_repeat_off = 0;
|
|
_prefs->ota_channel = 0;
|
|
#ifdef WITH_RS232_BRIDGE
|
|
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
|
|
#else
|
|
_prefs->bridge_uart = 0;
|
|
#endif
|
|
_prefs->bridge_format = mesh::bridge::ESPNOW_FORMAT_WRAPPED;
|
|
_prefs->gps_sync_interval_hours = 0;
|
|
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
bool node_prefs_needs_migration = false;
|
|
mesh::hilStartupTrace("prefs_recovery_begin");
|
|
if (!recoverCommonPrefsFiles(fs)) {
|
|
MESH_DEBUG_PRINTLN("Prefs: common preference recovery is incomplete");
|
|
}
|
|
mesh::hilStartupTrace("prefs_recovery_ready");
|
|
#endif
|
|
|
|
mesh::hilStartupTrace("prefs_common_begin");
|
|
if (fs->exists("/com_prefs")) {
|
|
loadPrefsInt(fs, "/com_prefs"); loaded = true; // new filename
|
|
} else if (fs->exists("/node_prefs")) {
|
|
loadPrefsInt(fs, "/node_prefs");
|
|
loaded = true;
|
|
is_upgrade = true; // Migrating from old filename
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// Wait for loadMQTTPrefs() to persist any observer tail captured from this
|
|
// old file before replacing or removing its only on-flash copy.
|
|
node_prefs_needs_migration = true;
|
|
#else
|
|
savePrefs(fs);
|
|
if (fs->exists("/com_prefs")) {
|
|
fs->remove("/node_prefs");
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("Prefs: preserving legacy /node_prefs because /com_prefs was not created");
|
|
}
|
|
_com_prefs_needs_upgrade = false;
|
|
#endif
|
|
} else if (fs->exists("/prefs.json")) {
|
|
// Stock 1.17.1 stored NodePrefs as JSON. A partition migration preserves
|
|
// that file byte-for-byte; import it before writing the current binary
|
|
// image so node name, passwords and primary radio survive the Full handoff.
|
|
#if defined(NRF52_PLATFORM)
|
|
File legacy(*fs);
|
|
legacy.open("/prefs.json", FILE_O_READ);
|
|
#elif defined(STM32_PLATFORM)
|
|
File legacy = fs->open("/prefs.json", FILE_O_READ);
|
|
#else
|
|
File legacy = fs->open("/prefs.json", "r");
|
|
#endif
|
|
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
|
|
&& !defined(RS232_BRIDGE_DEFAULT_ON)
|
|
const uint8_t previous_bridge_uart = _prefs->bridge_uart;
|
|
const uint8_t previous_bridge_enabled = _prefs->bridge_enabled;
|
|
// JSON profiles can contain the unused enabled flag without any UART.
|
|
// Require an explicit nonzero UART before treating it as bridge intent.
|
|
_prefs->bridge_uart = 0;
|
|
#endif
|
|
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
|
const uint8_t previous_espnow_enabled = _prefs->espnow_bridge_enabled;
|
|
#if !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
|
const uint8_t previous_bridge_enabled = _prefs->bridge_enabled;
|
|
#endif
|
|
#endif
|
|
if (legacy && _prefs->loadSerial(legacy)) {
|
|
loaded = true;
|
|
is_upgrade = true;
|
|
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
|
|
&& !defined(RS232_BRIDGE_DEFAULT_ON)
|
|
if (_prefs->bridge_uart == 0) _prefs->bridge_enabled = 0;
|
|
if (_prefs->bridge_uart != WITH_RS232_BRIDGE_UART
|
|
#ifdef WITH_RS232_BRIDGE_ALT
|
|
&& _prefs->bridge_uart != WITH_RS232_BRIDGE_ALT_UART
|
|
#endif
|
|
) {
|
|
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
|
|
}
|
|
#endif
|
|
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
|
// The legacy JSON schema has no independent ESP-NOW intent marker.
|
|
_prefs->espnow_bridge_enabled = 0;
|
|
#if !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
|
_prefs->bridge_enabled = 0;
|
|
#endif
|
|
#elif defined(WITH_ESPNOW_BRIDGE) && defined(ESPNOW_BRIDGE_MERGED) \
|
|
&& !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
|
// An explicitly default-on compatibility profile imports its legacy
|
|
// single-bridge intent into the new independent ESP-NOW setting.
|
|
_prefs->espnow_bridge_enabled = _prefs->bridge_enabled;
|
|
#endif
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
node_prefs_needs_migration = true;
|
|
#else
|
|
savePrefs(fs);
|
|
#endif
|
|
} else {
|
|
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
|
|
&& !defined(RS232_BRIDGE_DEFAULT_ON)
|
|
_prefs->bridge_uart = previous_bridge_uart;
|
|
_prefs->bridge_enabled = previous_bridge_enabled;
|
|
#endif
|
|
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
|
_prefs->espnow_bridge_enabled = previous_espnow_enabled;
|
|
#if !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
|
_prefs->bridge_enabled = previous_bridge_enabled;
|
|
#endif
|
|
#endif
|
|
}
|
|
if (legacy) legacy.close();
|
|
} else {
|
|
// File doesn't exist - set defaults for a fresh install. Dual R1/R2
|
|
// scanning keeps the node awake, so only that configuration starts with
|
|
// device power saving off. A saved preference is never rewritten here.
|
|
is_fresh_install = true;
|
|
mesh::Radio* profile_radio = _callbacks->getProfileRadio();
|
|
const bool dual_radio_active = profile_radio != NULL
|
|
&& profile_radio->profiles() != NULL
|
|
&& profile_radio->profiles()->enabled();
|
|
_prefs->powersaving_enabled = DEFAULT_POWERSAVING_ENABLED && !dual_radio_active ? 1 : 0;
|
|
_prefs->bridge_pkt_src = 1; // Default to RX (logRx) for new installs
|
|
}
|
|
mesh::hilStartupTrace("prefs_common_ready");
|
|
#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.
|
|
MQTTPrefsAtomicStore::LegacyUpgradeGate legacy_upgrade(
|
|
_com_prefs_needs_upgrade || node_prefs_needs_migration);
|
|
mesh::hilStartupTrace("prefs_mqtt_begin");
|
|
loadMQTTPrefs(fs, &legacy_upgrade);
|
|
mesh::hilStartupTrace("prefs_mqtt_ready");
|
|
if (!display_settings_loaded && _mqtt_prefs.display_timeout_secs != DISPLAY_TIMEOUT_DEFAULT_SECS)
|
|
mesh::ui::migrateLegacyDisplayTimeout(_mqtt_prefs.display_timeout_secs);
|
|
if (_mqtt_prefs_hold) legacy_upgrade.holdMqttSource();
|
|
|
|
// 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) {
|
|
if (legacy_upgrade.blocksComPrefsRewrite()) {
|
|
MESH_DEBUG_PRINTLN("MQTT Bridge: deferring bridge.source migration until legacy prefs are preserved");
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("MQTT Bridge: Migrating bridge.source from tx to rx (MQTT bridge default)");
|
|
_prefs->bridge_pkt_src = 1; // Set to RX (logRx)
|
|
if (node_prefs_needs_migration) {
|
|
// The atomic /node_prefs -> /com_prefs handoff below persists this
|
|
// in-memory change. Do not publish /com_prefs before that transaction.
|
|
MESH_DEBUG_PRINTLN("MQTT Bridge: bridge.source will be saved with node prefs migration");
|
|
} else {
|
|
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
|
|
|
|
mesh::hilStartupTrace("prefs_migration_begin");
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
if (node_prefs_needs_migration) {
|
|
if (legacy_upgrade.mayRewriteComPrefs()) {
|
|
// The MQTT image (and any tail from /node_prefs) is committed, so it is
|
|
// now safe to publish the replacement name. Keep /node_prefs until the
|
|
// complete /com_prefs image is closed and atomically renamed into place.
|
|
if (saveCommonPrefsImageAtomically(fs)) {
|
|
fs->remove("/node_prefs");
|
|
legacy_upgrade.recordComPrefsRewrite();
|
|
_com_prefs_needs_upgrade = false;
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("MQTT: preserving legacy /node_prefs until /com_prefs migration commits");
|
|
}
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("MQTT: preserving legacy /node_prefs until /mqtt_prefs migration commits");
|
|
}
|
|
} else 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.
|
|
if (legacy_upgrade.mayRewriteComPrefs()) {
|
|
// loadMQTTPrefs has already committed the full MQTT payload (including
|
|
// any recovered observer tail), so compact only /com_prefs now.
|
|
savePrefs(fs, PrefsSaveRouting::Scope::Common);
|
|
legacy_upgrade.recordComPrefsRewrite();
|
|
_com_prefs_needs_upgrade = false;
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("MQTT: preserving legacy /com_prefs until /mqtt_prefs migration commits");
|
|
}
|
|
}
|
|
#else
|
|
if (_com_prefs_needs_upgrade) {
|
|
savePrefs(fs);
|
|
_com_prefs_needs_upgrade = false;
|
|
}
|
|
#endif
|
|
mesh::hilStartupTrace("prefs_migration_ready");
|
|
#if defined(ENABLE_OTA)
|
|
if (loaded) syncOtaConfigFromPrefs(); // persisted OTA policy/keys -> OtaContext (else keep safe defaults)
|
|
#endif
|
|
mesh::hilStartupTrace("prefs_apply_begin");
|
|
#if MESH_USB_LOGGING_AVAILABLE
|
|
mesh::setUsbDebugEnabled(_prefs->usb_debug_enabled != 0);
|
|
mesh::setUsbLoggingEnabled(_prefs->usb_logging_enabled != 0);
|
|
#endif
|
|
_radio_profiles.adoptPrimaryPreamble(_prefs->primary_radio_preamble);
|
|
_radio_profiles.stagePrimary(_prefs->primary_radio_preamble, false);
|
|
mesh::hilStartupTrace("prefs_apply_ready");
|
|
}
|
|
|
|
#if defined(ENABLE_OTA)
|
|
// Register the existing preferences as the source of OTA policy. Registration
|
|
// does not need a workspace; a later allocation retry must still restore keys.
|
|
void CommonCLI::syncOtaConfigFromPrefs() {
|
|
// Mesh owns one process-wide OTA context. Its role's NodePrefs outlives that
|
|
// context and already holds the durable policy, so retain only its pointer.
|
|
static const NodePrefs* ota_prefs = nullptr;
|
|
ota_prefs = _prefs;
|
|
mesh::ota::ota_set_context_config_loader([](mesh::ota::OtaConfigState& state) {
|
|
state.autofetch = ota_prefs->ota_autofetch;
|
|
state.checkpoint = ota_prefs->ota_checkpoint_blocks;
|
|
state.advert = ota_prefs->ota_advert_interval;
|
|
state.hops = ota_prefs->ota_max_hops;
|
|
state.autoinstall = ota_prefs->ota_autoinstall;
|
|
state.allow.clear();
|
|
for (uint8_t i = 0; i < ota_prefs->ota_signer_count && i < MAX_OTA_SIGNERS; i++)
|
|
state.allow.add(ota_prefs->ota_signers[i]);
|
|
return true;
|
|
});
|
|
}
|
|
#endif
|
|
|
|
void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
|
mesh::hilStartupTrace("prefs_image_open_begin");
|
|
// Older or truncated preference images must never inherit these opt-ins
|
|
// from a previous load of a newer image.
|
|
_prefs->usb_debug_enabled = 0;
|
|
_prefs->trace_when_repeat_off = 0;
|
|
_prefs->gps_sync_interval_hours = 0;
|
|
_prefs->ota_channel = 0;
|
|
#if defined(RP2040_PLATFORM)
|
|
File file = fs->open(filename, "r");
|
|
#else
|
|
File file = fs->open(filename);
|
|
#endif
|
|
mesh::hilStartupTrace("prefs_image_open_ready");
|
|
if (file) {
|
|
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
|
bool has_runtime_espnow_intent = false;
|
|
#endif
|
|
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
|
|
&& !defined(RS232_BRIDGE_DEFAULT_ON)
|
|
bool has_runtime_bridge_uart = false;
|
|
#endif
|
|
// Every supported layout contains the fixed 290-byte common core. Reject
|
|
// a truncated in-place write before it can leave strings unterminated or
|
|
// feed partial radio values into startup. loadPrefs() rewrites the image.
|
|
if (file.size() < 290) {
|
|
MESH_DEBUG_PRINTLN("Prefs: %s is truncated (%u bytes); using defaults",
|
|
filename, (unsigned)file.size());
|
|
file.close();
|
|
_com_prefs_needs_upgrade = true;
|
|
return;
|
|
}
|
|
uint8_t pad[8];
|
|
|
|
mesh::hilStartupTrace("prefs_image_core_begin");
|
|
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((uint8_t *)&_prefs->reboot_interval, sizeof(_prefs->reboot_interval)); // 153
|
|
file.read(pad, 2); // 154
|
|
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
|
|
mesh::hilStartupTrace("prefs_image_core_ready");
|
|
_prefs->node_name[sizeof(_prefs->node_name) - 1] = '\0';
|
|
_prefs->password[sizeof(_prefs->password) - 1] = '\0';
|
|
_prefs->guest_password[sizeof(_prefs->guest_password) - 1] = '\0';
|
|
_prefs->bridge_secret[sizeof(_prefs->bridge_secret) - 1] = '\0';
|
|
_prefs->owner_info[sizeof(_prefs->owner_info) - 1] = '\0';
|
|
// 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.
|
|
// Build-profile defaults - overwritten below when the saved field is present.
|
|
_prefs->radio_fem_rxgain = 1;
|
|
_prefs->radio_fem_txgain = 0;
|
|
_prefs->cad_enabled = DEFAULT_CAD_ENABLED;
|
|
memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf));
|
|
_prefs->rx_powersaving_enabled = 0;
|
|
_prefs->rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
|
|
_prefs->rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
|
|
_prefs->rx_ps_level = 0;
|
|
_prefs->rx_ps_preamble = 0;
|
|
#if defined(ENABLE_OTA)
|
|
// OTA settings were appended after Keymind's retry/flood tail. Initialize them
|
|
// before reading so older and legacy preference files remain conservative.
|
|
_prefs->ota_autofetch = 0;
|
|
_prefs->ota_autoinstall = 0;
|
|
_prefs->ota_signer_count = 0;
|
|
memset(_prefs->ota_signers, 0, sizeof(_prefs->ota_signers));
|
|
_prefs->ota_checkpoint_blocks = 4;
|
|
_prefs->ota_advert_interval = 0;
|
|
_prefs->ota_max_hops = 3;
|
|
#endif
|
|
_prefs->telemetry_access = TELEMETRY_ACCESS_ALL;
|
|
_prefs->flood_retry_group_max_path = FLOOD_RETRY_GROUP_MAX_PATH_DEFAULT;
|
|
_prefs->rx_watchdog_enabled = mesh::RepeaterRadioTiming::DEFAULT_RX_WATCHDOG_ENABLED;
|
|
_prefs->system_watchdog_enabled = 1;
|
|
// A remainder larger than the smallest legacy MQTT gap (864) means an old fork
|
|
// file with the zero-filled gap; detect and recover it below. Anything smaller
|
|
// (upstream/flex 5-byte tails, or the ~384-byte keymind retry tail) takes the
|
|
// normal path: guarded 290-294 reads, then the keymind retry tail whose absence
|
|
// in shorter files is detected via direct_retry_prefs_magic.
|
|
size_t extra = file.available();
|
|
bool has_flood_retry_prefs = false; // set when the keymind flood-retry tail is read below
|
|
if (extra > LEGACY_MQTT_GAP_3SLOT) {
|
|
_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));
|
|
}
|
|
// next: 295
|
|
file.read((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295
|
|
file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296
|
|
file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297
|
|
file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299
|
|
file.read((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301
|
|
file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303
|
|
file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304
|
|
file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305
|
|
file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306
|
|
file.read((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307
|
|
file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308
|
|
file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309
|
|
memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes));
|
|
_prefs->flood_retry_bridge_enabled = 0;
|
|
memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets));
|
|
memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes));
|
|
_prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT;
|
|
_prefs->battery_alert_enabled = 0;
|
|
_prefs->battery_alert_low_percent = BATTERY_ALERT_LOW_PERCENT_DEFAULT;
|
|
_prefs->battery_alert_critical_percent = BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT;
|
|
memset(_prefs->battery_alert_region, 0, sizeof(_prefs->battery_alert_region));
|
|
_prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT;
|
|
_prefs->flood_channel_data_enabled = 1;
|
|
_prefs->legacy_flood_channel_block_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
|
_prefs->flood_channel_data_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
|
has_flood_retry_prefs = file.available() >= 2;
|
|
if (has_flood_retry_prefs) {
|
|
file.read((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 311
|
|
file.read((uint8_t *)&_prefs->flood_retry_max_path, sizeof(_prefs->flood_retry_max_path)); // 312
|
|
if (file.available() >= (int)sizeof(_prefs->flood_retry_prefixes)) {
|
|
file.read((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->flood_retry_bridge_enabled)) {
|
|
file.read((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->flood_retry_bridge_buckets)) {
|
|
file.read((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->flood_retry_ignore_prefixes)) {
|
|
file.read((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], sizeof(_prefs->flood_retry_ignore_prefixes));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->flood_retry_advert_enabled)) {
|
|
file.read((uint8_t *)&_prefs->flood_retry_advert_enabled, sizeof(_prefs->flood_retry_advert_enabled));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->battery_alert_enabled)) {
|
|
file.read((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->battery_alert_low_percent)) {
|
|
file.read((uint8_t *)&_prefs->battery_alert_low_percent, sizeof(_prefs->battery_alert_low_percent));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->battery_alert_critical_percent)) {
|
|
file.read((uint8_t *)&_prefs->battery_alert_critical_percent, sizeof(_prefs->battery_alert_critical_percent));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->direct_retry_recent_enabled)) {
|
|
file.read((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->flood_channel_data_enabled)) {
|
|
file.read((uint8_t *)&_prefs->flood_channel_data_enabled, sizeof(_prefs->flood_channel_data_enabled));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->legacy_flood_channel_block_max_hops)) {
|
|
file.read((uint8_t *)&_prefs->legacy_flood_channel_block_max_hops,
|
|
sizeof(_prefs->legacy_flood_channel_block_max_hops));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->flood_channel_data_max_hops)) {
|
|
file.read((uint8_t *)&_prefs->flood_channel_data_max_hops, sizeof(_prefs->flood_channel_data_max_hops));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->telemetry_access)) {
|
|
file.read((uint8_t *)&_prefs->telemetry_access, sizeof(_prefs->telemetry_access));
|
|
}
|
|
}
|
|
#if defined(ENABLE_OTA)
|
|
// OTA config starts at 675, after telemetry_access. Guard every appended field so older files keep defaults.
|
|
if (file.available() >= (int)sizeof(_prefs->ota_autofetch)) {
|
|
file.read((uint8_t *)&_prefs->ota_autofetch, sizeof(_prefs->ota_autofetch));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->ota_autoinstall)) {
|
|
file.read((uint8_t *)&_prefs->ota_autoinstall, sizeof(_prefs->ota_autoinstall));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->ota_signer_count)) {
|
|
file.read((uint8_t *)&_prefs->ota_signer_count, sizeof(_prefs->ota_signer_count));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->ota_signers)) {
|
|
file.read((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->ota_checkpoint_blocks)) {
|
|
file.read((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->ota_advert_interval)) {
|
|
file.read((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->ota_max_hops)) {
|
|
file.read((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops));
|
|
}
|
|
// next: 811
|
|
#endif
|
|
// RXPS is stored at a fixed offset after the reserved OTA tail. This keeps
|
|
// both old OTA and non-OTA /com_prefs files unambiguous.
|
|
bool has_rxps_tail = file.available() >= 11;
|
|
#if !defined(ENABLE_OTA)
|
|
const size_t ota_tail_size = 136;
|
|
const size_t padded_rxps_tail_size = sizeof(_prefs->rx_powersaving_enabled)
|
|
+ sizeof(_prefs->rx_ps_rx_us) + sizeof(_prefs->rx_ps_sleep_us)
|
|
+ sizeof(_prefs->rx_ps_level) + sizeof(_prefs->rx_ps_preamble);
|
|
if (file.available() >= (int)(ota_tail_size + padded_rxps_tail_size)) {
|
|
size_t remaining = ota_tail_size;
|
|
while (remaining > 0) {
|
|
size_t n = remaining > sizeof(pad) ? sizeof(pad) : remaining;
|
|
file.read(pad, n);
|
|
remaining -= n;
|
|
}
|
|
has_rxps_tail = true;
|
|
} else {
|
|
has_rxps_tail = false;
|
|
}
|
|
#endif
|
|
const size_t rxps_tail_size = sizeof(_prefs->rx_powersaving_enabled)
|
|
+ sizeof(_prefs->rx_ps_rx_us) + sizeof(_prefs->rx_ps_sleep_us)
|
|
+ sizeof(_prefs->rx_ps_level) + sizeof(_prefs->rx_ps_preamble);
|
|
if (has_rxps_tail && file.available() >= (int)rxps_tail_size) {
|
|
file.read((uint8_t *)&_prefs->rx_powersaving_enabled, sizeof(_prefs->rx_powersaving_enabled));
|
|
file.read((uint8_t *)&_prefs->rx_ps_rx_us, sizeof(_prefs->rx_ps_rx_us));
|
|
file.read((uint8_t *)&_prefs->rx_ps_sleep_us, sizeof(_prefs->rx_ps_sleep_us));
|
|
file.read((uint8_t *)&_prefs->rx_ps_level, sizeof(_prefs->rx_ps_level));
|
|
file.read((uint8_t *)&_prefs->rx_ps_preamble, sizeof(_prefs->rx_ps_preamble));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->battery_alert_region)) {
|
|
file.read((uint8_t *)_prefs->battery_alert_region, sizeof(_prefs->battery_alert_region));
|
|
_prefs->battery_alert_region[sizeof(_prefs->battery_alert_region) - 1] = '\0';
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->flood_retry_group_max_path)) {
|
|
file.read((uint8_t *)&_prefs->flood_retry_group_max_path,
|
|
sizeof(_prefs->flood_retry_group_max_path));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->rx_watchdog_enabled)) {
|
|
file.read((uint8_t *)&_prefs->rx_watchdog_enabled,
|
|
sizeof(_prefs->rx_watchdog_enabled));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->system_watchdog_enabled)) {
|
|
file.read((uint8_t *)&_prefs->system_watchdog_enabled,
|
|
sizeof(_prefs->system_watchdog_enabled));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->extra_sf)) {
|
|
file.read((uint8_t *)_prefs->extra_sf, sizeof(_prefs->extra_sf));
|
|
if (file.available() >= (int)sizeof(_prefs->radio_fem_txgain)) {
|
|
file.read((uint8_t *)&_prefs->radio_fem_txgain,
|
|
sizeof(_prefs->radio_fem_txgain));
|
|
if (file.available() >= (int)sizeof(_prefs->usb_logging_enabled)) {
|
|
file.read((uint8_t *)&_prefs->usb_logging_enabled,
|
|
sizeof(_prefs->usb_logging_enabled));
|
|
if (file.available() >= (int)sizeof(_prefs->bridge_uart)) {
|
|
file.read((uint8_t *)&_prefs->bridge_uart,
|
|
sizeof(_prefs->bridge_uart));
|
|
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
|
|
&& !defined(RS232_BRIDGE_DEFAULT_ON)
|
|
has_runtime_bridge_uart = true;
|
|
#endif
|
|
if (file.available() >= (int)sizeof(_prefs->bridge_format)) {
|
|
file.read((uint8_t *)&_prefs->bridge_format,
|
|
sizeof(_prefs->bridge_format));
|
|
if (file.available() >= (int)sizeof(_prefs->primary_radio_preamble)) {
|
|
uint16_t preamble = 0;
|
|
if (file.read((uint8_t *)&preamble, sizeof(preamble)) == sizeof(preamble)
|
|
&& (preamble == 0 || (preamble >= 8
|
|
&& preamble <= mesh::RadioProfiles::MaxPreamble))) {
|
|
_prefs->primary_radio_preamble = preamble;
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->espnow_bridge_enabled)) {
|
|
file.read((uint8_t *)&_prefs->espnow_bridge_enabled,
|
|
sizeof(_prefs->espnow_bridge_enabled));
|
|
if (file.available() >= (int)sizeof(_prefs->gps_sync_interval_hours)) {
|
|
uint16_t hours = 0;
|
|
if (file.read((uint8_t *)&hours, sizeof(hours)) == sizeof(hours)) {
|
|
_prefs->gps_sync_interval_hours = hours;
|
|
if (file.available() >= (int)sizeof(_prefs->usb_debug_enabled)) {
|
|
file.read((uint8_t *)&_prefs->usb_debug_enabled,
|
|
sizeof(_prefs->usb_debug_enabled));
|
|
if (file.available() >= (int)sizeof(_prefs->trace_when_repeat_off)) {
|
|
file.read((uint8_t *)&_prefs->trace_when_repeat_off,
|
|
sizeof(_prefs->trace_when_repeat_off));
|
|
if (file.available() >= (int)sizeof(_prefs->ota_channel)) {
|
|
file.read((uint8_t *)&_prefs->ota_channel,
|
|
sizeof(_prefs->ota_channel));
|
|
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
|
uint8_t bridge_profile = 0;
|
|
if (file.read(&bridge_profile, sizeof(bridge_profile)) == sizeof(bridge_profile)) {
|
|
has_runtime_espnow_intent = bridge_profile == 0xA1;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (file.available() > 0) {
|
|
// Never accept a torn append as a partial detector list.
|
|
_com_prefs_needs_upgrade = true;
|
|
}
|
|
}
|
|
|
|
// Sanitize non-finite values before constrain(), whose comparisons leave
|
|
// NaN unchanged. Safe defaults keep a damaged image from reaching radio
|
|
// initialization or advert coordinate encoding.
|
|
if (!isfinite(_prefs->airtime_factor)) _prefs->airtime_factor = 1.0f;
|
|
#ifdef DEFAULT_RX_DELAY_BASE
|
|
if (!isfinite(_prefs->rx_delay_base)) _prefs->rx_delay_base = DEFAULT_RX_DELAY_BASE;
|
|
#else
|
|
if (!isfinite(_prefs->rx_delay_base)) _prefs->rx_delay_base = 0.0f;
|
|
#endif
|
|
if (!isfinite(_prefs->tx_delay_factor)) _prefs->tx_delay_factor = 0.5f;
|
|
if (!isfinite(_prefs->direct_tx_delay_factor)) _prefs->direct_tx_delay_factor = 0.3f;
|
|
#ifdef LORA_FREQ
|
|
if (!isfinite(_prefs->freq)) _prefs->freq = (float)LORA_FREQ;
|
|
#else
|
|
if (!isfinite(_prefs->freq)) _prefs->freq = 915.0f;
|
|
#endif
|
|
if (!isfinite(_prefs->bw)) _prefs->bw = defaultLoRaBandwidth();
|
|
if (!isfinite(_prefs->adc_multiplier)) _prefs->adc_multiplier = 0.0f;
|
|
if (!isfinite(_prefs->node_lat) || _prefs->node_lat < -90.0 || _prefs->node_lat > 90.0) {
|
|
_prefs->node_lat = 0.0;
|
|
}
|
|
if (!isfinite(_prefs->node_lon) || _prefs->node_lon < -180.0 || _prefs->node_lon > 180.0) {
|
|
_prefs->node_lon = 0.0;
|
|
}
|
|
|
|
// 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);
|
|
// Duty cycles down to 1% are accepted and persist factor 99, even though
|
|
// the legacy `set af` command retains its narrower input range.
|
|
_prefs->airtime_factor = constrain(_prefs->airtime_factor, 0, 99.0f);
|
|
_prefs->freq = constrain(_prefs->freq, 150.0f, 2500.0f);
|
|
_prefs->bw = isValidLoRaBandwidth(_prefs->bw) ? _prefs->bw : defaultLoRaBandwidth();
|
|
_prefs->sf = constrain(_prefs->sf, 5, 12);
|
|
_prefs->cr = constrain(_prefs->cr, 5, 8);
|
|
uint8_t extra_sf_count = 0;
|
|
if (!mesh::lr2021::storedSideDetectorCount(_prefs->extra_sf, extra_sf_count)
|
|
|| !mesh::lr2021::validateSideDetectorSFs(
|
|
_prefs->extra_sf, extra_sf_count, _prefs->sf, _prefs->bw)) {
|
|
memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf));
|
|
_com_prefs_needs_upgrade = true;
|
|
}
|
|
_prefs->tx_power_dbm = mesh::clampLoRaTxPower(
|
|
_prefs->tx_power_dbm, _prefs->freq);
|
|
_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
|
|
|
|
// sanitise bad bridge pref values
|
|
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
|
if (!has_runtime_espnow_intent) {
|
|
// Older profiles saved an unused ESP-NOW flag as on. Only the appended
|
|
// combined-profile marker proves that this was an explicit setting.
|
|
_prefs->espnow_bridge_enabled = 0;
|
|
_com_prefs_needs_upgrade = true;
|
|
}
|
|
#endif
|
|
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
|
|
&& !defined(RS232_BRIDGE_DEFAULT_ON)
|
|
if (!has_runtime_bridge_uart || _prefs->bridge_uart == 0) {
|
|
// Pre-merge normal repeaters persisted bridge_enabled=1 even though no
|
|
// bridge was compiled, with either no UART tail or the no-bridge zero
|
|
// sentinel. Fail safe before normalizing the UART instead of claiming
|
|
// it unexpectedly; the user can explicitly enable the bridge.
|
|
_prefs->bridge_enabled = 0;
|
|
_com_prefs_needs_upgrade = true;
|
|
}
|
|
#endif
|
|
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
|
|
_prefs->espnow_bridge_enabled = constrain(_prefs->espnow_bridge_enabled, 0, 1);
|
|
#if defined(WITH_ESPNOW_BRIDGE) && defined(ESPNOW_BRIDGE_MERGED) \
|
|
&& !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
|
// An unrelated legacy primary bridge flag cannot enable this new mode.
|
|
// Only the separately persisted and marker-validated ESP-NOW intent wins.
|
|
_prefs->bridge_enabled = _prefs->espnow_bridge_enabled;
|
|
#endif
|
|
_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);
|
|
#ifdef WITH_RS232_BRIDGE
|
|
if (_prefs->bridge_uart != WITH_RS232_BRIDGE_UART
|
|
#ifdef WITH_RS232_BRIDGE_ALT
|
|
&& _prefs->bridge_uart != WITH_RS232_BRIDGE_ALT_UART
|
|
#endif
|
|
) {
|
|
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
|
|
_com_prefs_needs_upgrade = true;
|
|
}
|
|
#else
|
|
_prefs->bridge_uart = 0;
|
|
#endif
|
|
if (!mesh::bridge::isValidEspNowFormat(_prefs->bridge_format)) {
|
|
_prefs->bridge_format = mesh::bridge::ESPNOW_FORMAT_WRAPPED;
|
|
_com_prefs_needs_upgrade = true;
|
|
}
|
|
#ifdef WITH_ESPNOW_BRIDGE
|
|
if (!mesh::bridge::isValidEspNowBridgeChannel(
|
|
_prefs->bridge_channel, _prefs->bridge_format)) {
|
|
_prefs->bridge_channel = 1;
|
|
_com_prefs_needs_upgrade = true;
|
|
}
|
|
#endif
|
|
|
|
_prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1);
|
|
_prefs->reboot_interval = constrain(_prefs->reboot_interval, 0, 255);
|
|
|
|
_prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1);
|
|
_prefs->gps_sync_interval_hours = constrain(_prefs->gps_sync_interval_hours,
|
|
0, mesh::gps::MAX_SYNC_INTERVAL_HOURS);
|
|
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
|
|
|
|
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
|
|
_prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean
|
|
_prefs->radio_fem_txgain = constrain(_prefs->radio_fem_txgain, 0, 1); // boolean
|
|
_prefs->usb_logging_enabled = constrain(_prefs->usb_logging_enabled, 0, 1); // boolean
|
|
// Only a deliberately saved 1 enables diagnostics; corrupted bytes are OFF.
|
|
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
|
|
// Corrupt or unknown values must not enable a repeat-off exception.
|
|
_prefs->trace_when_repeat_off = _prefs->trace_when_repeat_off == 1 ? 1 : 0;
|
|
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
|
|
if (!directRetryPrefsValid(_prefs)) {
|
|
setDefaultDirectRetryPrefs(_prefs);
|
|
memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes));
|
|
_prefs->flood_retry_bridge_enabled = 0;
|
|
memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets));
|
|
memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes));
|
|
_prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT;
|
|
} else if (!has_flood_retry_prefs) {
|
|
applyFloodRetryPreset(_prefs, _prefs->retry_preset);
|
|
}
|
|
if (_prefs->retry_preset > RETRY_PRESET_MOBILE && _prefs->retry_preset != RETRY_PRESET_CUSTOM) {
|
|
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
|
}
|
|
_prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, 1, 15);
|
|
_prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, 10, 5000);
|
|
_prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, 0, 5000);
|
|
_prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160);
|
|
_prefs->direct_retry_enabled = constrain(_prefs->direct_retry_enabled, 0, 1);
|
|
_prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1);
|
|
_prefs->flood_retry_attempts = constrain(_prefs->flood_retry_attempts, 0, 15);
|
|
if (_prefs->flood_retry_max_path != FLOOD_RETRY_PATH_GATE_DISABLED) {
|
|
_prefs->flood_retry_max_path = constrain(_prefs->flood_retry_max_path, 0, 63);
|
|
}
|
|
if (_prefs->flood_retry_group_max_path != FLOOD_RETRY_PATH_GATE_DISABLED) {
|
|
_prefs->flood_retry_group_max_path = constrain(_prefs->flood_retry_group_max_path, 0, 63);
|
|
}
|
|
if (_prefs->flood_retry_max_path == 0) {
|
|
_prefs->flood_retry_group_max_path = FLOOD_RETRY_PATH_GATE_DISABLED;
|
|
}
|
|
_prefs->flood_retry_bridge_enabled = constrain(_prefs->flood_retry_bridge_enabled, 0, 1);
|
|
_prefs->flood_retry_advert_enabled = constrain(_prefs->flood_retry_advert_enabled, 0, 1);
|
|
_prefs->battery_alert_enabled = constrain(_prefs->battery_alert_enabled, 0, 1);
|
|
_prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1);
|
|
_prefs->flood_channel_data_enabled = constrain(_prefs->flood_channel_data_enabled, 0, 1);
|
|
_prefs->telemetry_access = constrain(_prefs->telemetry_access, 0, 1);
|
|
if (_prefs->ota_channel > 2) _prefs->ota_channel = 0;
|
|
if (_prefs->legacy_flood_channel_block_max_hops != FLOOD_CHANNEL_HOPS_ALL
|
|
&& (_prefs->legacy_flood_channel_block_max_hops < 1
|
|
|| _prefs->legacy_flood_channel_block_max_hops > 7)) {
|
|
_prefs->legacy_flood_channel_block_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
|
}
|
|
if (_prefs->flood_channel_data_max_hops != FLOOD_CHANNEL_HOPS_ALL
|
|
&& (_prefs->flood_channel_data_max_hops < 1 || _prefs->flood_channel_data_max_hops > 7)) {
|
|
_prefs->flood_channel_data_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
|
}
|
|
if (_prefs->battery_alert_low_percent < 1
|
|
|| _prefs->battery_alert_low_percent > 100
|
|
|| _prefs->battery_alert_critical_percent >= _prefs->battery_alert_low_percent) {
|
|
_prefs->battery_alert_low_percent = BATTERY_ALERT_LOW_PERCENT_DEFAULT;
|
|
_prefs->battery_alert_critical_percent = BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT;
|
|
}
|
|
#if defined(ENABLE_OTA)
|
|
_prefs->ota_autofetch = constrain(_prefs->ota_autofetch, 0, 2);
|
|
_prefs->ota_autoinstall = constrain(_prefs->ota_autoinstall, 0, 1);
|
|
if (_prefs->ota_checkpoint_blocks > 4096) _prefs->ota_checkpoint_blocks = 4; // 0=never; cap absurd
|
|
if (_prefs->ota_advert_interval > 10080) _prefs->ota_advert_interval = 1440; // 0=off; cap at 7 days
|
|
if (_prefs->ota_max_hops > 8) _prefs->ota_max_hops = 3; // 0=direct only; cap absurd reach
|
|
if (_prefs->ota_signer_count > 4) _prefs->ota_signer_count = 0; // corrupt count -> drop keys
|
|
#endif
|
|
_prefs->rx_powersaving_enabled = constrain(_prefs->rx_powersaving_enabled, 0, 1);
|
|
_prefs->rx_watchdog_enabled = constrain(_prefs->rx_watchdog_enabled, 0, 1);
|
|
_prefs->system_watchdog_enabled = constrain(_prefs->system_watchdog_enabled, 0, 1);
|
|
_prefs->rx_ps_level = constrain(_prefs->rx_ps_level, 0, 10);
|
|
if (_prefs->rx_ps_preamble != 16 && _prefs->rx_ps_preamble != 32) {
|
|
_prefs->rx_ps_preamble = 0; // 0 = auto (derive from SF)
|
|
}
|
|
ensureRxPowerSavingDefaults(&_prefs->rx_ps_rx_us, &_prefs->rx_ps_sleep_us);
|
|
recalcRxPowerSavingFromLevel(_prefs->rx_ps_level, _prefs->sf, _prefs->bw, _prefs->rx_ps_preamble,
|
|
&_prefs->rx_ps_rx_us,
|
|
&_prefs->rx_ps_sleep_us); // retune level-based timings to the loaded SF/BW
|
|
|
|
file.close();
|
|
}
|
|
}
|
|
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// Keep the byte layout in one place for the observer's coordinated atomic
|
|
// legacy migration. Ordinary builds use savePrefs() directly, avoiding a
|
|
// duplicate writer on flash-constrained targets.
|
|
template <typename Writer>
|
|
static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
|
|
uint8_t pad[8];
|
|
memset(pad, 0, sizeof(pad));
|
|
|
|
#define WRITE_COMMON_PREFS(value) \
|
|
do { \
|
|
if (writer.write((const uint8_t *)(value), sizeof(*(value))) != sizeof(*(value))) return false; \
|
|
} while (0)
|
|
#define WRITE_COMMON_PREFS_BYTES(value, size) \
|
|
do { \
|
|
if (writer.write((const uint8_t *)(value), (size)) != (size)) return false; \
|
|
} while (0)
|
|
|
|
WRITE_COMMON_PREFS(&prefs->airtime_factor); // 0
|
|
WRITE_COMMON_PREFS(&prefs->node_name); // 4
|
|
WRITE_COMMON_PREFS_BYTES(pad, 4); // 36
|
|
WRITE_COMMON_PREFS(&prefs->node_lat); // 40
|
|
WRITE_COMMON_PREFS(&prefs->node_lon); // 48
|
|
WRITE_COMMON_PREFS_BYTES(prefs->password, sizeof(prefs->password)); // 56
|
|
WRITE_COMMON_PREFS(&prefs->freq); // 72
|
|
WRITE_COMMON_PREFS(&prefs->tx_power_dbm); // 76
|
|
WRITE_COMMON_PREFS(&prefs->disable_fwd); // 77
|
|
WRITE_COMMON_PREFS(&prefs->advert_interval); // 78
|
|
WRITE_COMMON_PREFS_BYTES(pad, 1); // 79
|
|
WRITE_COMMON_PREFS(&prefs->rx_delay_base); // 80
|
|
WRITE_COMMON_PREFS(&prefs->tx_delay_factor); // 84
|
|
WRITE_COMMON_PREFS_BYTES(prefs->guest_password, sizeof(prefs->guest_password)); // 88
|
|
WRITE_COMMON_PREFS(&prefs->direct_tx_delay_factor); // 104
|
|
WRITE_COMMON_PREFS_BYTES(pad, 4); // 108
|
|
WRITE_COMMON_PREFS(&prefs->sf); // 112
|
|
WRITE_COMMON_PREFS(&prefs->cr); // 113
|
|
WRITE_COMMON_PREFS(&prefs->allow_read_only); // 114
|
|
WRITE_COMMON_PREFS(&prefs->multi_acks); // 115
|
|
WRITE_COMMON_PREFS(&prefs->bw); // 116
|
|
WRITE_COMMON_PREFS(&prefs->agc_reset_interval); // 120
|
|
WRITE_COMMON_PREFS(&prefs->path_hash_mode); // 121
|
|
WRITE_COMMON_PREFS(&prefs->loop_detect); // 122
|
|
WRITE_COMMON_PREFS_BYTES(pad, 1); // 123
|
|
WRITE_COMMON_PREFS(&prefs->flood_max); // 124
|
|
WRITE_COMMON_PREFS(&prefs->flood_advert_interval); // 125
|
|
WRITE_COMMON_PREFS(&prefs->interference_threshold); // 126
|
|
WRITE_COMMON_PREFS(&prefs->bridge_enabled); // 127
|
|
WRITE_COMMON_PREFS(&prefs->bridge_delay); // 128
|
|
WRITE_COMMON_PREFS(&prefs->bridge_pkt_src); // 130
|
|
WRITE_COMMON_PREFS(&prefs->bridge_baud); // 131
|
|
WRITE_COMMON_PREFS(&prefs->bridge_channel); // 135
|
|
WRITE_COMMON_PREFS_BYTES(prefs->bridge_secret, sizeof(prefs->bridge_secret)); // 136
|
|
WRITE_COMMON_PREFS(&prefs->powersaving_enabled); // 152
|
|
WRITE_COMMON_PREFS(&prefs->reboot_interval); // 153
|
|
WRITE_COMMON_PREFS_BYTES(pad, 2); // 154
|
|
WRITE_COMMON_PREFS(&prefs->gps_enabled); // 156
|
|
WRITE_COMMON_PREFS(&prefs->gps_interval); // 157
|
|
WRITE_COMMON_PREFS(&prefs->advert_loc_policy); // 161
|
|
WRITE_COMMON_PREFS(&prefs->discovery_mod_timestamp); // 162
|
|
WRITE_COMMON_PREFS(&prefs->adc_multiplier); // 166
|
|
WRITE_COMMON_PREFS_BYTES(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.
|
|
WRITE_COMMON_PREFS(&prefs->rx_boosted_gain); // 290
|
|
WRITE_COMMON_PREFS(&prefs->flood_max_unscoped); // 291
|
|
WRITE_COMMON_PREFS(&prefs->flood_max_advert); // 292
|
|
WRITE_COMMON_PREFS(&prefs->radio_fem_rxgain); // 293
|
|
WRITE_COMMON_PREFS(&prefs->cad_enabled); // 294
|
|
|
|
markDirectRetryPrefsValid(prefs);
|
|
WRITE_COMMON_PREFS(&prefs->retry_preset); // 295
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_attempts); // 296
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_base_ms); // 297
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_step_ms); // 299
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_snr_margin_x4); // 301
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_cr4_snr_x4); // 303
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_cr5_snr_x4); // 304
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_cr7_snr_x4); // 305
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_cr8_snr_x4); // 306
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_enabled); // 307
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_cr_enabled); // 308
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_prefs_magic); // 309
|
|
WRITE_COMMON_PREFS(&prefs->flood_retry_attempts); // 311
|
|
WRITE_COMMON_PREFS(&prefs->flood_retry_max_path); // 312
|
|
WRITE_COMMON_PREFS(&prefs->flood_retry_prefixes); // 313
|
|
WRITE_COMMON_PREFS(&prefs->flood_retry_bridge_enabled);
|
|
WRITE_COMMON_PREFS(&prefs->flood_retry_bridge_buckets);
|
|
WRITE_COMMON_PREFS(&prefs->flood_retry_ignore_prefixes);
|
|
WRITE_COMMON_PREFS(&prefs->flood_retry_advert_enabled);
|
|
WRITE_COMMON_PREFS(&prefs->battery_alert_enabled);
|
|
WRITE_COMMON_PREFS(&prefs->battery_alert_low_percent);
|
|
WRITE_COMMON_PREFS(&prefs->battery_alert_critical_percent);
|
|
WRITE_COMMON_PREFS(&prefs->direct_retry_recent_enabled);
|
|
WRITE_COMMON_PREFS(&prefs->flood_channel_data_enabled);
|
|
WRITE_COMMON_PREFS(&prefs->legacy_flood_channel_block_max_hops);
|
|
WRITE_COMMON_PREFS(&prefs->flood_channel_data_max_hops);
|
|
WRITE_COMMON_PREFS(&prefs->telemetry_access); // 674
|
|
#if defined(ENABLE_OTA)
|
|
WRITE_COMMON_PREFS(&prefs->ota_autofetch); // 675
|
|
WRITE_COMMON_PREFS(&prefs->ota_autoinstall); // 676
|
|
WRITE_COMMON_PREFS(&prefs->ota_signer_count); // 677
|
|
WRITE_COMMON_PREFS(&prefs->ota_signers); // 678
|
|
WRITE_COMMON_PREFS(&prefs->ota_checkpoint_blocks); // 806
|
|
WRITE_COMMON_PREFS(&prefs->ota_advert_interval); // 808
|
|
WRITE_COMMON_PREFS(&prefs->ota_max_hops); // 810
|
|
#else
|
|
// Reserve the OTA tail so RXPS has the same offset in every build.
|
|
WRITE_COMMON_PREFS_BYTES(pad, 3);
|
|
for (size_t remaining = 128; remaining > 0; ) {
|
|
const size_t n = remaining > sizeof(pad) ? sizeof(pad) : remaining;
|
|
WRITE_COMMON_PREFS_BYTES(pad, n);
|
|
remaining -= n;
|
|
}
|
|
const uint16_t ota_checkpoint_blocks = 4;
|
|
const uint16_t ota_advert_interval = 0;
|
|
const uint8_t ota_max_hops = 3;
|
|
WRITE_COMMON_PREFS(&ota_checkpoint_blocks);
|
|
WRITE_COMMON_PREFS(&ota_advert_interval);
|
|
WRITE_COMMON_PREFS(&ota_max_hops);
|
|
#endif
|
|
WRITE_COMMON_PREFS(&prefs->rx_powersaving_enabled); // 811
|
|
WRITE_COMMON_PREFS(&prefs->rx_ps_rx_us); // 812
|
|
WRITE_COMMON_PREFS(&prefs->rx_ps_sleep_us); // 816
|
|
WRITE_COMMON_PREFS(&prefs->rx_ps_level); // 820
|
|
WRITE_COMMON_PREFS(&prefs->rx_ps_preamble); // 821
|
|
WRITE_COMMON_PREFS(&prefs->battery_alert_region); // 822
|
|
WRITE_COMMON_PREFS(&prefs->flood_retry_group_max_path); // 853
|
|
WRITE_COMMON_PREFS(&prefs->rx_watchdog_enabled); // 854
|
|
WRITE_COMMON_PREFS(&prefs->system_watchdog_enabled); // 855
|
|
WRITE_COMMON_PREFS(&prefs->extra_sf); // 856
|
|
WRITE_COMMON_PREFS(&prefs->radio_fem_txgain); // 860
|
|
WRITE_COMMON_PREFS(&prefs->usb_logging_enabled); // 861
|
|
WRITE_COMMON_PREFS(&prefs->bridge_uart); // 862
|
|
WRITE_COMMON_PREFS(&prefs->bridge_format); // 863
|
|
WRITE_COMMON_PREFS(&prefs->primary_radio_preamble); // appended primary tuple field
|
|
WRITE_COMMON_PREFS(&prefs->espnow_bridge_enabled); // appended Full ESP-NOW intent
|
|
WRITE_COMMON_PREFS(&prefs->gps_sync_interval_hours); // published GPS sync cadence
|
|
WRITE_COMMON_PREFS(&prefs->usb_debug_enabled); // appended USB debug intent
|
|
WRITE_COMMON_PREFS(&prefs->trace_when_repeat_off); // appended repeater trace exception
|
|
WRITE_COMMON_PREFS(&prefs->ota_channel); // appended WiFi OTA channel; never shift older fields
|
|
#ifdef ESPNOW_BRIDGE_MERGED
|
|
const uint8_t bridge_profile = 0xA1;
|
|
WRITE_COMMON_PREFS(&bridge_profile); // independent ESP-NOW intent belongs to this profile
|
|
#endif
|
|
|
|
#undef WRITE_COMMON_PREFS_BYTES
|
|
#undef WRITE_COMMON_PREFS
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
|
const PrefsSaveRouting::Plan plan = PrefsSaveRouting::planFor(scope);
|
|
if (plan.common) {
|
|
_common_save_result_known = true;
|
|
_common_save_succeeded = false;
|
|
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
|
|
_prefs->trace_when_repeat_off = _prefs->trace_when_repeat_off == 1 ? 1 : 0;
|
|
}
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// Observer builds use a verified temp/backup transaction for common prefs.
|
|
// Radio and bridge changes must never leave a truncated boot-time image.
|
|
if (plan.common) _common_save_succeeded = saveCommonPrefsImageAtomically(fs);
|
|
if (plan.observer) {
|
|
_observer_save_result_known = true;
|
|
_observer_save_succeeded = saveMQTTPrefs(fs);
|
|
}
|
|
#else
|
|
// Observer-only saves are a no-op on roles with no observer preference image.
|
|
if (!plan.common) return;
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
mesh::AtomicFileWriter file(fs, "/com_prefs");
|
|
#else
|
|
mesh::ContactFileTransaction file(fs, "/com_prefs");
|
|
#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((uint8_t *)&_prefs->reboot_interval, sizeof(_prefs->reboot_interval)); // 153
|
|
file.write(pad, 2); // 154
|
|
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. /com_prefs
|
|
// holds the upstream fields plus the keymind retry tail below.
|
|
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
|
|
// next: 295
|
|
markDirectRetryPrefsValid(_prefs);
|
|
file.write((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295
|
|
file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296
|
|
file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297
|
|
file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299
|
|
file.write((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301
|
|
file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303
|
|
file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304
|
|
file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305
|
|
file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306
|
|
file.write((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307
|
|
file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308
|
|
file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309
|
|
file.write((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 311
|
|
file.write((uint8_t *)&_prefs->flood_retry_max_path, sizeof(_prefs->flood_retry_max_path)); // 312
|
|
file.write((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 313
|
|
file.write((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled));
|
|
file.write((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets));
|
|
file.write((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], sizeof(_prefs->flood_retry_ignore_prefixes));
|
|
file.write((uint8_t *)&_prefs->flood_retry_advert_enabled, sizeof(_prefs->flood_retry_advert_enabled));
|
|
file.write((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled));
|
|
file.write((uint8_t *)&_prefs->battery_alert_low_percent, sizeof(_prefs->battery_alert_low_percent));
|
|
file.write((uint8_t *)&_prefs->battery_alert_critical_percent, sizeof(_prefs->battery_alert_critical_percent));
|
|
file.write((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled));
|
|
file.write((uint8_t *)&_prefs->flood_channel_data_enabled, sizeof(_prefs->flood_channel_data_enabled));
|
|
file.write((uint8_t *)&_prefs->legacy_flood_channel_block_max_hops,
|
|
sizeof(_prefs->legacy_flood_channel_block_max_hops));
|
|
file.write((uint8_t *)&_prefs->flood_channel_data_max_hops, sizeof(_prefs->flood_channel_data_max_hops));
|
|
file.write((uint8_t *)&_prefs->telemetry_access, sizeof(_prefs->telemetry_access)); // 674
|
|
#if defined(ENABLE_OTA)
|
|
file.write((uint8_t *)&_prefs->ota_autofetch, sizeof(_prefs->ota_autofetch)); // 675
|
|
file.write((uint8_t *)&_prefs->ota_autoinstall, sizeof(_prefs->ota_autoinstall)); // 676
|
|
file.write((uint8_t *)&_prefs->ota_signer_count, sizeof(_prefs->ota_signer_count)); // 677
|
|
file.write((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers)); // 678
|
|
file.write((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks)); // 806
|
|
file.write((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 808
|
|
file.write((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops)); // 810
|
|
// next: 811
|
|
#else
|
|
// Reserve the OTA tail so RXPS has the same offset in every build. Write
|
|
// OTA's normal defaults so a later OTA-enabled firmware reads sane values.
|
|
file.write(pad, 3); // autofetch, autoinstall, signer_count
|
|
for (size_t remaining = 128; remaining > 0; ) {
|
|
size_t n = remaining > sizeof(pad) ? sizeof(pad) : remaining;
|
|
file.write(pad, n);
|
|
remaining -= n;
|
|
}
|
|
const uint16_t ota_checkpoint_blocks = 4;
|
|
const uint16_t ota_advert_interval = 0;
|
|
const uint8_t ota_max_hops = 3;
|
|
file.write((uint8_t *)&ota_checkpoint_blocks, sizeof(ota_checkpoint_blocks));
|
|
file.write((uint8_t *)&ota_advert_interval, sizeof(ota_advert_interval));
|
|
file.write((uint8_t *)&ota_max_hops, sizeof(ota_max_hops));
|
|
#endif
|
|
file.write((uint8_t *)&_prefs->rx_powersaving_enabled, sizeof(_prefs->rx_powersaving_enabled)); // 811
|
|
file.write((uint8_t *)&_prefs->rx_ps_rx_us, sizeof(_prefs->rx_ps_rx_us)); // 812
|
|
file.write((uint8_t *)&_prefs->rx_ps_sleep_us, sizeof(_prefs->rx_ps_sleep_us)); // 816
|
|
file.write((uint8_t *)&_prefs->rx_ps_level, sizeof(_prefs->rx_ps_level)); // 820
|
|
file.write((uint8_t *)&_prefs->rx_ps_preamble, sizeof(_prefs->rx_ps_preamble)); // 821
|
|
file.write((uint8_t *)_prefs->battery_alert_region, sizeof(_prefs->battery_alert_region)); // 822
|
|
file.write((uint8_t *)&_prefs->flood_retry_group_max_path,
|
|
sizeof(_prefs->flood_retry_group_max_path)); // 853
|
|
file.write((uint8_t *)&_prefs->rx_watchdog_enabled,
|
|
sizeof(_prefs->rx_watchdog_enabled)); // 854
|
|
file.write((uint8_t *)&_prefs->system_watchdog_enabled,
|
|
sizeof(_prefs->system_watchdog_enabled)); // 855
|
|
file.write((uint8_t *)_prefs->extra_sf, sizeof(_prefs->extra_sf)); // 856
|
|
file.write((uint8_t *)&_prefs->radio_fem_txgain, sizeof(_prefs->radio_fem_txgain)); // 860
|
|
file.write((uint8_t *)&_prefs->usb_logging_enabled, sizeof(_prefs->usb_logging_enabled)); // 861
|
|
file.write((uint8_t *)&_prefs->bridge_uart, sizeof(_prefs->bridge_uart)); // 862
|
|
file.write((uint8_t *)&_prefs->bridge_format, sizeof(_prefs->bridge_format)); // 863
|
|
file.write((uint8_t *)&_prefs->primary_radio_preamble, sizeof(_prefs->primary_radio_preamble)); // appended
|
|
file.write((uint8_t *)&_prefs->espnow_bridge_enabled, sizeof(_prefs->espnow_bridge_enabled)); // appended
|
|
file.write((uint8_t *)&_prefs->gps_sync_interval_hours, sizeof(_prefs->gps_sync_interval_hours)); // published GPS sync cadence
|
|
file.write((uint8_t *)&_prefs->usb_debug_enabled, sizeof(_prefs->usb_debug_enabled)); // appended
|
|
file.write((uint8_t *)&_prefs->trace_when_repeat_off, sizeof(_prefs->trace_when_repeat_off)); // appended
|
|
file.write((uint8_t *)&_prefs->ota_channel, sizeof(_prefs->ota_channel)); // appended WiFi OTA channel
|
|
#ifdef ESPNOW_BRIDGE_MERGED
|
|
const uint8_t bridge_profile = 0xA1;
|
|
file.write(&bridge_profile, sizeof(bridge_profile)); // independent ESP-NOW intent belongs to this profile
|
|
#endif
|
|
|
|
_common_save_succeeded = file.commit();
|
|
if (!_common_save_succeeded) {
|
|
MESH_DEBUG_PRINTLN("ERROR: savePrefs atomic commit failed");
|
|
}
|
|
}
|
|
#endif
|
|
#if MESH_USB_LOGGING_AVAILABLE
|
|
if (plan.common && _common_save_succeeded) {
|
|
// Dynamic config imports use this path too. Do not enable diagnostics
|
|
// until the corresponding common preference image is committed.
|
|
mesh::setUsbDebugEnabled(_prefs->usb_debug_enabled != 0);
|
|
mesh::setUsbLoggingEnabled(_prefs->usb_logging_enabled != 0);
|
|
}
|
|
#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, const char* path = "/mqtt_prefs") {
|
|
#if defined(RP2040_PLATFORM)
|
|
return fs->open(path, "r");
|
|
#else
|
|
return fs->open(path);
|
|
#endif
|
|
}
|
|
|
|
static bool mqttJsonImportPresetValid(const char* preset) {
|
|
return findMQTTPreset(preset) != nullptr;
|
|
}
|
|
|
|
static MQTTPrefsJsonImport::Result loadMqttJsonImport(
|
|
FILESYSTEM* fs, const MQTTPrefs& defaults, MQTTPrefs* candidate) {
|
|
File file = openMqttPrefsRead(fs, "/mqtt.json");
|
|
if (!file) return MQTTPrefsJsonImport::Result::InvalidSyntax;
|
|
if (file.size() > MQTTPrefsJsonImport::kMaximumFileSize) {
|
|
file.close();
|
|
return MQTTPrefsJsonImport::Result::TooLarge;
|
|
}
|
|
const MQTTPrefsJsonImport::Result result =
|
|
MQTTPrefsJsonImport::decode(
|
|
file, defaults, candidate, mqttJsonImportPresetValid);
|
|
file.close();
|
|
return result;
|
|
}
|
|
|
|
static const char* mqttJsonImportResultName(MQTTPrefsJsonImport::Result result) {
|
|
switch (result) {
|
|
case MQTTPrefsJsonImport::Result::Loaded: return "loaded";
|
|
case MQTTPrefsJsonImport::Result::LoadedWithRepairs: return "loaded with repairs";
|
|
case MQTTPrefsJsonImport::Result::InvalidSyntax: return "invalid ConfigSerializer syntax";
|
|
case MQTTPrefsJsonImport::Result::InvalidSchema: return "invalid v1 schema";
|
|
case MQTTPrefsJsonImport::Result::UnsupportedVersion: return "unsupported version";
|
|
case MQTTPrefsJsonImport::Result::TooLarge: return "file too large";
|
|
}
|
|
return "unknown error";
|
|
}
|
|
|
|
static MQTTPrefsRecovery::FileState mqttPrefsFileState(FILESYSTEM* fs, const char* path) {
|
|
bool present = false;
|
|
if (!mesh::filePresence(fs, path, present)) return MQTTPrefsRecovery::FileState::Preserve;
|
|
if (!present) return MQTTPrefsRecovery::FileState::Missing;
|
|
File file = openMqttPrefsRead(fs, path);
|
|
if (!file) return MQTTPrefsRecovery::FileState::Preserve;
|
|
const size_t file_size = file.size();
|
|
uint8_t prefix[sizeof(MQTTPrefsHeader)] = {};
|
|
const size_t prefix_size = file_size < sizeof(prefix) ? file_size : sizeof(prefix);
|
|
const size_t prefix_read = file.read(prefix, prefix_size);
|
|
file.close();
|
|
return MQTTPrefsCodec::classify(prefix, prefix_read, file_size).preserve_file
|
|
? MQTTPrefsRecovery::FileState::Preserve
|
|
: MQTTPrefsRecovery::FileState::Usable;
|
|
}
|
|
|
|
// Restore the only usable image before the normal loader inspects /mqtt_prefs.
|
|
// SPIFFS cannot rename over an existing destination, so publishing moves the
|
|
// old primary to .bak before moving the verified temp into the empty name.
|
|
// The decision helper deliberately treats unsupported/corrupt files as opaque:
|
|
// no recovery path overwrites one with an older layout.
|
|
static bool recoverMqttPrefsFiles(FILESYSTEM* fs) {
|
|
const MQTTPrefsRecovery::FileState primary = mqttPrefsFileState(fs, "/mqtt_prefs");
|
|
const MQTTPrefsRecovery::FileState temp = mqttPrefsFileState(fs, "/mqtt_prefs.tmp");
|
|
const MQTTPrefsRecovery::FileState backup = mqttPrefsFileState(fs, "/mqtt_prefs.bak");
|
|
const MQTTPrefsRecovery::Action action = MQTTPrefsRecovery::select(primary, temp, backup);
|
|
|
|
if (action == MQTTPrefsRecovery::Action::KeepPrimary) {
|
|
// A current/known legacy primary has already published. Every transaction
|
|
// artifact is therefore unpublished or stale, including a partial temp
|
|
// left by a reset during write(), and can be discarded. Preserve artifacts
|
|
// only when the primary itself is opaque (the branch above still keeps it).
|
|
if (primary == MQTTPrefsRecovery::FileState::Usable) {
|
|
if (temp != MQTTPrefsRecovery::FileState::Missing) fs->remove("/mqtt_prefs.tmp");
|
|
if (backup != MQTTPrefsRecovery::FileState::Missing) fs->remove("/mqtt_prefs.bak");
|
|
}
|
|
return primary == MQTTPrefsRecovery::FileState::Preserve;
|
|
}
|
|
if (action == MQTTPrefsRecovery::Action::PromoteBackup) {
|
|
if (fs->rename("/mqtt_prefs.bak", "/mqtt_prefs")) {
|
|
// Symmetric case: a usable backup is now primary, so any interrupted
|
|
// temp is no longer authoritative and must not block a later save.
|
|
if (backup == MQTTPrefsRecovery::FileState::Usable &&
|
|
temp != MQTTPrefsRecovery::FileState::Missing) {
|
|
fs->remove("/mqtt_prefs.tmp");
|
|
}
|
|
MESH_DEBUG_PRINTLN("MQTT: recovered /mqtt_prefs from transaction backup");
|
|
return backup == MQTTPrefsRecovery::FileState::Preserve;
|
|
}
|
|
MESH_DEBUG_PRINTLN("MQTT: could not recover /mqtt_prefs backup; files preserved");
|
|
return true;
|
|
}
|
|
if (action == MQTTPrefsRecovery::Action::DiscardTemp) {
|
|
// No published image exists. A first-save candidate is still uncommitted,
|
|
// even when fully written, and must not become active after a rejected save.
|
|
return !fs->remove("/mqtt_prefs.tmp");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Filesystem adapter for MQTTPrefsAtomicStore. It writes the new image to
|
|
// /mqtt_prefs.tmp and verifies its size. Publishing is a recoverable SPIFFS
|
|
// transaction: primary -> .bak, then tmp -> primary, then best-effort backup
|
|
// cleanup. A power loss at every boundary leaves at least one recoverable file.
|
|
class MQTTPrefsFileStore {
|
|
public:
|
|
explicit MQTTPrefsFileStore(FILESYSTEM* fs)
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
: _fs(fs), _file(*fs) {}
|
|
#else
|
|
: _fs(fs) {}
|
|
#endif
|
|
|
|
bool begin() {
|
|
_finished = false;
|
|
_open = false;
|
|
_owns_temp = false;
|
|
_bytes_written = 0;
|
|
// Retry interrupted cleanup/rollback without requiring a reboot. Recovery
|
|
// never publishes a rejected candidate or replaces an opaque primary.
|
|
if (recoverMqttPrefsFiles(_fs)) return false;
|
|
if (_fs->exists("/mqtt_prefs.tmp") || _fs->exists("/mqtt_prefs.bak")) return false;
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
_file = _fs->open("/mqtt_prefs.tmp", FILE_O_WRITE);
|
|
#elif defined(RP2040_PLATFORM)
|
|
_file = _fs->open("/mqtt_prefs.tmp", "w");
|
|
#else
|
|
_file = _fs->open("/mqtt_prefs.tmp", "w", true);
|
|
#endif
|
|
_open = _file;
|
|
_owns_temp = _open;
|
|
return _open;
|
|
}
|
|
|
|
size_t write(const uint8_t* bytes, size_t size) {
|
|
if (!_open) return 0;
|
|
const size_t written = _file.write(bytes, size);
|
|
_bytes_written += written;
|
|
return written;
|
|
}
|
|
|
|
bool finish() {
|
|
if (!_open) return false;
|
|
_file.close();
|
|
_open = false;
|
|
#if defined(RP2040_PLATFORM)
|
|
File verify = _fs->open("/mqtt_prefs.tmp", "r");
|
|
#else
|
|
File verify = _fs->open("/mqtt_prefs.tmp");
|
|
#endif
|
|
if (!verify) return false;
|
|
const bool complete = verify.size() == _bytes_written;
|
|
verify.close();
|
|
if (!complete) return false;
|
|
_finished = true;
|
|
return true;
|
|
}
|
|
|
|
bool commit() {
|
|
if (!_finished) return false;
|
|
// SPIFFS refuses rename(tmp, existing_dest). Move the existing image to a
|
|
// recoverable backup first, then publish temp into the now-empty primary.
|
|
// A rejected publication must keep the previous image authoritative.
|
|
if (_fs->exists("/mqtt_prefs.bak")) return false;
|
|
if (_fs->exists("/mqtt_prefs") && !_fs->rename("/mqtt_prefs", "/mqtt_prefs.bak")) {
|
|
return false;
|
|
}
|
|
if (!_fs->rename("/mqtt_prefs.tmp", "/mqtt_prefs")) {
|
|
if (_fs->exists("/mqtt_prefs.bak")) {
|
|
_fs->rename("/mqtt_prefs.bak", "/mqtt_prefs");
|
|
}
|
|
return false;
|
|
}
|
|
// Cleanup failure is non-fatal: the new primary is published and recovery
|
|
// will remove a known-good stale backup on a later boot.
|
|
if (_fs->exists("/mqtt_prefs.bak")) _fs->remove("/mqtt_prefs.bak");
|
|
return true;
|
|
}
|
|
|
|
void abort() {
|
|
if (_open) _file.close();
|
|
_open = false;
|
|
// Explicit rejection is not a successful commit. If cleanup or rollback
|
|
// also fails, boot recovery still chooses the old backup, never this temp.
|
|
if (_owns_temp && _fs->exists("/mqtt_prefs.tmp")) {
|
|
_fs->remove("/mqtt_prefs.tmp");
|
|
}
|
|
_finished = false;
|
|
_owns_temp = false;
|
|
}
|
|
|
|
private:
|
|
FILESYSTEM* _fs;
|
|
File _file;
|
|
bool _open = false;
|
|
bool _finished = false;
|
|
bool _owns_temp = false;
|
|
size_t _bytes_written = 0;
|
|
};
|
|
|
|
#endif // WITH_MQTT_BRIDGE
|
|
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// Common preferences use the same verified temp/backup transaction shape as
|
|
// MQTT preferences. This protects both old-name migration and normal runtime
|
|
// radio/bridge changes from power loss between truncate, write, and close.
|
|
class CommonPrefsFileStore {
|
|
public:
|
|
explicit CommonPrefsFileStore(FILESYSTEM* fs)
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
: _fs(fs), _file(*fs) {}
|
|
#else
|
|
: _fs(fs) {}
|
|
#endif
|
|
|
|
bool begin() {
|
|
_finished = false;
|
|
_open = false;
|
|
_owns_temp = false;
|
|
_bytes_written = 0;
|
|
if (_fs->exists("/com_prefs.bak")) return false;
|
|
if (_fs->exists("/com_prefs.tmp")) {
|
|
_fs->remove("/com_prefs.tmp");
|
|
if (_fs->exists("/com_prefs.tmp")) return false; // could not clear it
|
|
}
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
_file = _fs->open("/com_prefs.tmp", FILE_O_WRITE);
|
|
#elif defined(RP2040_PLATFORM)
|
|
_file = _fs->open("/com_prefs.tmp", "w");
|
|
#else
|
|
_file = _fs->open("/com_prefs.tmp", "w", true);
|
|
#endif
|
|
_open = _file;
|
|
_owns_temp = _open;
|
|
return _open;
|
|
}
|
|
|
|
size_t write(const uint8_t* bytes, size_t size) {
|
|
if (!_open) return 0;
|
|
const size_t written = _file.write(bytes, size);
|
|
_bytes_written += written;
|
|
return written;
|
|
}
|
|
|
|
bool finish() {
|
|
if (!_open) return false;
|
|
_file.close();
|
|
_open = false;
|
|
#if defined(RP2040_PLATFORM)
|
|
File verify = _fs->open("/com_prefs.tmp", "r");
|
|
#else
|
|
File verify = _fs->open("/com_prefs.tmp");
|
|
#endif
|
|
if (!verify) return false;
|
|
const bool complete = verify.size() == _bytes_written;
|
|
verify.close();
|
|
if (!complete) return false;
|
|
_finished = true;
|
|
return true;
|
|
}
|
|
|
|
bool commit() {
|
|
if (!_finished) return false;
|
|
if (_fs->exists("/com_prefs.bak")) return false;
|
|
if (_fs->exists("/com_prefs")
|
|
&& !_fs->rename("/com_prefs", "/com_prefs.bak")) {
|
|
return false;
|
|
}
|
|
if (!_fs->rename("/com_prefs.tmp", "/com_prefs")) {
|
|
// An explicit failure must retain the previous durable configuration.
|
|
// If restoration also fails, recovery keeps the backup authoritative.
|
|
if (_fs->exists("/com_prefs.bak")) {
|
|
_fs->rename("/com_prefs.bak", "/com_prefs");
|
|
}
|
|
return false;
|
|
}
|
|
if (_fs->exists("/com_prefs.bak")) _fs->remove("/com_prefs.bak");
|
|
return true;
|
|
}
|
|
|
|
void abort() {
|
|
if (_open) _file.close();
|
|
_open = false;
|
|
// abort() is an explicit rejected write, unlike a power loss. Never leave
|
|
// its candidate eligible to become the committed primary on a later boot.
|
|
if (_owns_temp && _fs->exists("/com_prefs.tmp")) {
|
|
_fs->remove("/com_prefs.tmp");
|
|
}
|
|
_finished = false;
|
|
_owns_temp = false;
|
|
}
|
|
|
|
private:
|
|
FILESYSTEM* _fs;
|
|
File _file;
|
|
bool _open = false;
|
|
bool _finished = false;
|
|
bool _owns_temp = false;
|
|
size_t _bytes_written = 0;
|
|
};
|
|
|
|
static const char* commonPrefsSaveResultName(MQTTPrefsAtomicStore::ImageResult result) {
|
|
switch (result) {
|
|
case MQTTPrefsAtomicStore::ImageResult::BeginFailed: return "begin";
|
|
case MQTTPrefsAtomicStore::ImageResult::WriteFailed: return "write";
|
|
case MQTTPrefsAtomicStore::ImageResult::FinishFailed: return "close";
|
|
case MQTTPrefsAtomicStore::ImageResult::CommitFailed: return "rename";
|
|
case MQTTPrefsAtomicStore::ImageResult::Committed: return "committed";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
bool CommonCLI::recoverCommonPrefsFiles(FILESYSTEM* fs) {
|
|
using CommonPrefsRecovery::Action;
|
|
const Action action = CommonPrefsRecovery::select(
|
|
fs->exists("/com_prefs"),
|
|
fs->exists("/com_prefs.tmp"),
|
|
fs->exists("/com_prefs.bak"));
|
|
|
|
switch (action) {
|
|
case Action::KeepPrimary:
|
|
if (fs->exists("/com_prefs.tmp")) fs->remove("/com_prefs.tmp");
|
|
if (fs->exists("/com_prefs.bak")) fs->remove("/com_prefs.bak");
|
|
return !fs->exists("/com_prefs.tmp") && !fs->exists("/com_prefs.bak");
|
|
|
|
case Action::PromoteBackup:
|
|
// A complete temp was not necessarily accepted by the caller. Roll back
|
|
// an interrupted or rejected commit to the last published image.
|
|
if (!fs->rename("/com_prefs.bak", "/com_prefs")) return false;
|
|
if (fs->exists("/com_prefs.tmp")) fs->remove("/com_prefs.tmp");
|
|
return !fs->exists("/com_prefs.tmp");
|
|
|
|
case Action::DiscardTemp:
|
|
// With no backup, a reset may have interrupted the very first write
|
|
// before finish(). Keep defaults or /node_prefs and retry the save.
|
|
fs->remove("/com_prefs.tmp");
|
|
return !fs->exists("/com_prefs.tmp");
|
|
|
|
case Action::None:
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CommonCLI::saveCommonPrefsImageAtomically(FILESYSTEM* fs) {
|
|
if (!recoverCommonPrefsFiles(fs)) {
|
|
MESH_DEBUG_PRINTLN("Prefs: refusing save while recovery is incomplete");
|
|
return false;
|
|
}
|
|
CommonPrefsFileStore store(fs);
|
|
const MQTTPrefsAtomicStore::ImageResult result = MQTTPrefsAtomicStore::writeImage(
|
|
store, [this](CommonPrefsFileStore& target) {
|
|
return writeCommonPrefsImage(target, _prefs);
|
|
});
|
|
if (!MQTTPrefsAtomicStore::imageCommitted(result)) {
|
|
MESH_DEBUG_PRINTLN("Prefs: atomic /com_prefs save failed at %s; previous image preserved",
|
|
commonPrefsSaveResultName(result));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static const char* mqttPrefsSaveResultName(MQTTPrefsAtomicStore::Result result) {
|
|
switch (result) {
|
|
case MQTTPrefsAtomicStore::Result::BeginFailed: return "begin";
|
|
case MQTTPrefsAtomicStore::Result::HeaderWriteFailed: return "header write";
|
|
case MQTTPrefsAtomicStore::Result::PayloadWriteFailed: return "payload write";
|
|
case MQTTPrefsAtomicStore::Result::FinishFailed: return "close";
|
|
case MQTTPrefsAtomicStore::Result::CommitFailed: return "rename";
|
|
case MQTTPrefsAtomicStore::Result::Committed: return "committed";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
void CommonCLI::loadMQTTPrefs(
|
|
FILESYSTEM* fs, MQTTPrefsAtomicStore::LegacyUpgradeGate* legacy_upgrade) {
|
|
setMQTTPrefsDefaults(&_mqtt_prefs);
|
|
// Complete or preserve an interrupted SPIFFS transaction before decoding.
|
|
// A failed recovery leaves the artifacts untouched and blocks this boot from
|
|
// replacing them with defaults through a later CLI save.
|
|
_mqtt_prefs_hold = recoverMqttPrefsFiles(fs);
|
|
|
|
// `/mqtt.json` was the authoritative observer-firmware-dev format. Import it
|
|
// exactly once, but only when binary recovery found no primary or recovery
|
|
// artifact at all. A valid, default-valued, corrupt, unreadable, or future
|
|
// binary image always wins because neither format carries a generation that
|
|
// could prove the JSON is newer.
|
|
const MQTTPrefsJsonImport::Decision json_import =
|
|
MQTTPrefsJsonImport::select(
|
|
_mqtt_prefs_hold,
|
|
fs->exists("/mqtt_prefs"), fs->exists("/mqtt_prefs.tmp"),
|
|
fs->exists("/mqtt_prefs.bak"), fs->exists("/mqtt.json"),
|
|
fs->exists("/mqtt.json.tmp"), fs->exists("/mqtt.json.bak"));
|
|
if (json_import == MQTTPrefsJsonImport::Decision::HoldJsonArtifacts) {
|
|
_mqtt_prefs_hold = true;
|
|
_legacy_tail.valid = false;
|
|
MESH_DEBUG_PRINTLN(
|
|
"MQTT: unresolved /mqtt.json transaction files; defaults active and all sources preserved");
|
|
return;
|
|
}
|
|
if (json_import == MQTTPrefsJsonImport::Decision::ImportPrimary) {
|
|
MQTTPrefs* candidate = new (std::nothrow) MQTTPrefs;
|
|
if (candidate == nullptr) {
|
|
_mqtt_prefs_hold = true;
|
|
_legacy_tail.valid = false;
|
|
MESH_DEBUG_PRINTLN(
|
|
"MQTT: no memory to import /mqtt.json; defaults active and source preserved");
|
|
return;
|
|
}
|
|
setMQTTPrefsDefaults(candidate);
|
|
const MQTTPrefsJsonImport::Result result =
|
|
loadMqttJsonImport(fs, _mqtt_prefs, candidate);
|
|
if (!MQTTPrefsJsonImport::loaded(result)) {
|
|
delete candidate;
|
|
_mqtt_prefs_hold = true;
|
|
_legacy_tail.valid = false;
|
|
MESH_DEBUG_PRINTLN(
|
|
"MQTT: /mqtt.json import refused (%s); defaults active and source preserved",
|
|
mqttJsonImportResultName(result));
|
|
return;
|
|
}
|
|
|
|
_mqtt_prefs = *candidate;
|
|
delete candidate;
|
|
legacy_upgrade->requireMqttRewrite();
|
|
if (saveMQTTPrefs(fs)) {
|
|
legacy_upgrade->recordMqttSave(true);
|
|
// JSON contains the full observer group. Returning prevents a stale
|
|
// /com_prefs observer tail from being overlaid after the committed import.
|
|
_legacy_tail.valid = false;
|
|
MESH_DEBUG_PRINTLN(
|
|
"MQTT: imported /mqtt.json v1 into atomic /mqtt_prefs (%s); JSON preserved",
|
|
mqttJsonImportResultName(result));
|
|
return;
|
|
}
|
|
|
|
// Do not expose a partially imported runtime state after a failed publish.
|
|
// The JSON remains untouched, and binary recovery owns any completed temp.
|
|
setMQTTPrefsDefaults(&_mqtt_prefs);
|
|
_mqtt_prefs_hold = true;
|
|
_legacy_tail.valid = false;
|
|
legacy_upgrade->recordMqttSave(false);
|
|
MESH_DEBUG_PRINTLN(
|
|
"MQTT: /mqtt.json decoded but binary commit failed; defaults active and sources preserved");
|
|
return;
|
|
}
|
|
|
|
bool has_observer_fields = false;
|
|
bool mqtt_rewrite_pending = false;
|
|
bool migrated_legacy_mqtt = false;
|
|
|
|
if (fs->exists("/mqtt_prefs")) {
|
|
File file = openMqttPrefsRead(fs);
|
|
if (file) {
|
|
const size_t file_size = file.size();
|
|
uint8_t prefix[sizeof(MQTTPrefsHeader)] = {};
|
|
const size_t prefix_size = file_size < sizeof(prefix) ? file_size : sizeof(prefix);
|
|
const size_t prefix_read = file.read(prefix, prefix_size);
|
|
file.close();
|
|
|
|
const MQTTPrefsCodec::DecodePlan plan =
|
|
MQTTPrefsCodec::classify(prefix, prefix_read, file_size);
|
|
if (plan.preserve_file) {
|
|
_mqtt_prefs_hold = true;
|
|
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs is unsupported or corrupt, using defaults (file preserved)");
|
|
} else if (plan.source == MQTTPrefsCodec::Source::Current) {
|
|
file = openMqttPrefsRead(fs);
|
|
MQTTPrefsHeader header;
|
|
if (!file || file.read((uint8_t *)&header, sizeof(header)) != sizeof(header) ||
|
|
file.read((uint8_t *)&_mqtt_prefs, plan.payload_len) != plan.payload_len) {
|
|
setMQTTPrefsDefaults(&_mqtt_prefs);
|
|
_mqtt_prefs_hold = true;
|
|
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs read failed, using defaults (file preserved)");
|
|
} else {
|
|
has_observer_fields = plan.observer_fields_present;
|
|
// Written by a later build with appended fields. Everything this
|
|
// binary knows loaded normally; say so, because the next `set` will
|
|
// rewrite the file at this length and drop the newer settings.
|
|
if (file_size - sizeof(MQTTPrefsHeader) > plan.payload_len) {
|
|
MESH_DEBUG_PRINTLN(
|
|
"MQTT: /mqtt_prefs written by newer firmware (%u > %u bytes); "
|
|
"config loaded, newer settings ignored and dropped on next save",
|
|
(unsigned)(file_size - sizeof(MQTTPrefsHeader)),
|
|
(unsigned)plan.payload_len);
|
|
}
|
|
}
|
|
if (file) file.close();
|
|
} else if (plan.rewrite_legacy) {
|
|
bool migrated = false;
|
|
file = openMqttPrefsRead(fs);
|
|
if (file) {
|
|
switch (plan.source) {
|
|
case MQTTPrefsCodec::Source::LegacyPreSlot: {
|
|
union {
|
|
OldMQTTPrefs post_wifi_power;
|
|
PreWifiPowerOldMQTTPrefs pre_wifi_power;
|
|
} old_prefs = {};
|
|
if (file.read((uint8_t *)&old_prefs, sizeof(old_prefs)) == sizeof(old_prefs)) {
|
|
if (MQTTPrefsCodec::isPlausibleLegacy(plan.source,
|
|
(const uint8_t *)&old_prefs, sizeof(old_prefs))) {
|
|
if (MQTTPrefsCodec::looksLikePreWifiPower((uint8_t *)&old_prefs, sizeof(old_prefs))) {
|
|
MQTTPrefsCodec::migratePreWifiPower(old_prefs.pre_wifi_power, &_mqtt_prefs);
|
|
} else {
|
|
MQTTPrefsCodec::migratePreSlot(old_prefs.post_wifi_power, &_mqtt_prefs);
|
|
}
|
|
migrated = true;
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs legacy content failed plausibility checks");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case MQTTPrefsCodec::Source::LegacyThreeSlotBase: {
|
|
ThreeSlotBaseMQTTPrefs old_prefs = {};
|
|
if (file.read((uint8_t *)&old_prefs, sizeof(old_prefs)) == sizeof(old_prefs)) {
|
|
if (MQTTPrefsCodec::isPlausibleLegacy(plan.source,
|
|
(const uint8_t *)&old_prefs, sizeof(old_prefs))) {
|
|
MQTTPrefsCodec::migrateThreeSlot(old_prefs, &_mqtt_prefs);
|
|
migrated = true;
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs legacy content failed plausibility checks");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case MQTTPrefsCodec::Source::LegacyThreeSlot: {
|
|
ThreeSlotMQTTPrefs old_prefs = {};
|
|
if (file.read((uint8_t *)&old_prefs, sizeof(old_prefs)) == sizeof(old_prefs)) {
|
|
if (MQTTPrefsCodec::isPlausibleLegacy(plan.source,
|
|
(const uint8_t *)&old_prefs, sizeof(old_prefs))) {
|
|
MQTTPrefsCodec::migrateThreeSlot(old_prefs, &_mqtt_prefs);
|
|
migrated = true;
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs legacy content failed plausibility checks");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case MQTTPrefsCodec::Source::LegacySixSlotBase:
|
|
case MQTTPrefsCodec::Source::LegacySixSlotAudience:
|
|
case MQTTPrefsCodec::Source::LegacySixSlotAudienceRx:
|
|
case MQTTPrefsCodec::Source::LegacySixSlot: {
|
|
Legacy6SlotMQTTPrefs old_prefs = {};
|
|
if (file.read((uint8_t *)&old_prefs, plan.payload_len) == plan.payload_len) {
|
|
if (MQTTPrefsCodec::isPlausibleLegacy(plan.source,
|
|
(const uint8_t *)&old_prefs, plan.payload_len)) {
|
|
MQTTPrefsCodec::migrateLegacySixSlot(old_prefs, plan.source, &_mqtt_prefs);
|
|
migrated = true;
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs legacy content failed plausibility checks");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
file.close();
|
|
}
|
|
if (migrated) {
|
|
// Do not save yet: a legacy /com_prefs observer tail may still need
|
|
// to be overlaid below. Publish the complete v1 image once, after it.
|
|
mqtt_rewrite_pending = true;
|
|
migrated_legacy_mqtt = true;
|
|
} else {
|
|
setMQTTPrefsDefaults(&_mqtt_prefs);
|
|
_mqtt_prefs_hold = true;
|
|
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs legacy read failed, using defaults (file preserved)");
|
|
}
|
|
}
|
|
} else {
|
|
_mqtt_prefs_hold = true;
|
|
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs could not be opened, using defaults (file preserved)");
|
|
}
|
|
} 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.)
|
|
}
|
|
|
|
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));
|
|
mqtt_rewrite_pending = true;
|
|
MESH_DEBUG_PRINTLN("MQTT: Migrated observer settings from legacy /com_prefs trailing block");
|
|
}
|
|
|
|
// Keep persisted values inside the signed-delta millis() scheduling window.
|
|
// This also repairs any manually-written or experimental value from firmware
|
|
// that briefly accepted intervals longer than the supported two-week cap.
|
|
if (_mqtt_prefs.mqtt_neighbors_interval < MQTT_NEIGHBORS_MIN_INTERVAL_MS ||
|
|
_mqtt_prefs.mqtt_neighbors_interval > MQTT_NEIGHBORS_MAX_INTERVAL_MS) {
|
|
_mqtt_prefs.mqtt_neighbors_interval = MQTT_NEIGHBORS_DEFAULT_INTERVAL_MS;
|
|
// Persist the repair so a corrupt flash value is not re-clamped every boot.
|
|
// Skip when hold is set so we never overwrite a deliberately preserved file.
|
|
if (!_mqtt_prefs_hold) {
|
|
mqtt_rewrite_pending = true;
|
|
}
|
|
MESH_DEBUG_PRINTLN("MQTT: invalid neighbors interval reset to %u hours",
|
|
(unsigned)MQTT_NEIGHBORS_DEFAULT_INTERVAL_HOURS);
|
|
}
|
|
if (MQTTPrefsCodec::repairDisplayPrefs(&_mqtt_prefs)) {
|
|
// Old payloads retain valid defaults because the object was defaulted before
|
|
// their shorter prefix was copied. This branch repairs only a full/newer
|
|
// payload carrying invalid display values.
|
|
if (!_mqtt_prefs_hold) {
|
|
mqtt_rewrite_pending = true;
|
|
}
|
|
MESH_DEBUG_PRINTLN("MQTT: invalid display preferences reset to defaults");
|
|
}
|
|
_legacy_tail.valid = false;
|
|
|
|
if (mqtt_rewrite_pending) {
|
|
legacy_upgrade->requireMqttRewrite();
|
|
if (migrated_legacy_mqtt) {
|
|
MESH_DEBUG_PRINTLN("MQTT: Migrating headerless /mqtt_prefs to versioned layout");
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("MQTT: Persisting observer tail into /mqtt_prefs before /com_prefs compaction");
|
|
}
|
|
if (saveMQTTPrefs(fs)) {
|
|
legacy_upgrade->recordMqttSave(true);
|
|
} else {
|
|
// The legacy source(s) remain intact because the failed transaction never
|
|
// published its temp file. Hold this boot so loadPrefs leaves /com_prefs
|
|
// untouched; the next boot can recover the tail and retry the transaction.
|
|
_mqtt_prefs_hold = true;
|
|
legacy_upgrade->recordMqttSave(false);
|
|
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs migration save failed; legacy files preserved and held");
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) {
|
|
if (_mqtt_prefs_hold) {
|
|
// Loading deliberately preserved the source file. Do not replace it with this
|
|
// boot's defaults after an unsupported, corrupt, or temporarily failed read.
|
|
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs held, not overwriting");
|
|
return false;
|
|
}
|
|
|
|
// Write header and payload sequentially so the transaction needs no second
|
|
// full-size (2.8 KiB) staging buffer on constrained targets. The length is
|
|
// the shortest that still round-trips this config, so a node with default
|
|
// packet filters keeps writing a payload older firmware can read.
|
|
const size_t payload_len = MQTTPrefsCodec::payloadLenFor(_mqtt_prefs);
|
|
const MQTTPrefsHeader header = MQTTPrefsCodec::makeHeader(payload_len);
|
|
MQTTPrefsFileStore store(fs);
|
|
const MQTTPrefsAtomicStore::Result result = MQTTPrefsAtomicStore::write(
|
|
store, (const uint8_t *)&header, sizeof(header),
|
|
(const uint8_t *)&_mqtt_prefs, payload_len);
|
|
if (!MQTTPrefsAtomicStore::committed(result)) {
|
|
MESH_DEBUG_PRINTLN("MQTT: atomic /mqtt_prefs save failed at %s; source preserved",
|
|
mqttPrefsSaveResultName(result));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
#define MIN_LOCAL_ADVERT_INTERVAL 60
|
|
|
|
void CommonCLI::savePrefs(PrefsSaveRouting::Scope scope) {
|
|
if (!trySavePrefs(scope)) {
|
|
++_prefs_save_failures;
|
|
if (PrefsSaveRouting::planFor(scope).common
|
|
&& (!_common_save_result_known || !_common_save_succeeded)) {
|
|
_prefs->markUnsaved();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CommonCLI::trySavePrefs(PrefsSaveRouting::Scope scope) {
|
|
const PrefsSaveRouting::Plan plan = PrefsSaveRouting::planFor(scope);
|
|
const uint8_t old_advert_interval = _prefs->advert_interval;
|
|
if (plan.common && _prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) {
|
|
_prefs->advert_interval = 0; // turn it off, now that device has been manually configured
|
|
}
|
|
// A previous successful transaction must not mask a callback that did not
|
|
// save this time. Both images have independent commit results.
|
|
if (plan.common) {
|
|
_common_save_result_known = false;
|
|
_common_save_succeeded = false;
|
|
}
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
if (plan.observer) {
|
|
_observer_save_result_known = false;
|
|
_observer_save_succeeded = false;
|
|
}
|
|
#endif
|
|
_callbacks->savePrefs(scope);
|
|
const bool common_saved = !plan.common
|
|
|| (_common_save_result_known && _common_save_succeeded);
|
|
if (plan.common) {
|
|
if (common_saved) {
|
|
_prefs->clearDirty();
|
|
if (old_advert_interval != _prefs->advert_interval) {
|
|
_callbacks->updateAdvertTimer();
|
|
}
|
|
} else {
|
|
_prefs->advert_interval = old_advert_interval;
|
|
}
|
|
}
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
return common_saved && (!plan.observer
|
|
|| (_observer_save_result_known && _observer_save_succeeded));
|
|
#else
|
|
return common_saved;
|
|
#endif
|
|
}
|
|
|
|
bool CommonCLI::saveObserverPrefs() {
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
_observer_save_result_known = false;
|
|
_observer_save_succeeded = false;
|
|
_callbacks->savePrefs(PrefsSaveRouting::Scope::Observer);
|
|
const bool saved = _observer_save_result_known && _observer_save_succeeded;
|
|
if (!saved) ++_prefs_save_failures;
|
|
return saved;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool CommonCLI::saveCommonPrefs() {
|
|
_common_save_result_known = false;
|
|
_common_save_succeeded = false;
|
|
_callbacks->savePrefs(PrefsSaveRouting::Scope::Common);
|
|
return _common_save_result_known && _common_save_succeeded;
|
|
}
|
|
|
|
bool CommonCLI::savePrimaryRadioParams(float freq, float bw, uint8_t sf,
|
|
uint8_t cr, uint16_t preamble) {
|
|
if (!isfinite(freq) || !isfinite(bw) || freq < 150.0f || freq > 2500.0f
|
|
|| sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)
|
|
|| !_radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) return false;
|
|
const float old_freq = _prefs->freq, old_bw = _prefs->bw;
|
|
const uint8_t old_sf = _prefs->sf, old_cr = _prefs->cr;
|
|
const int8_t old_power = _prefs->tx_power_dbm;
|
|
const uint32_t old_rx = _prefs->rx_ps_rx_us, old_sleep = _prefs->rx_ps_sleep_us;
|
|
const uint16_t old_preamble = _prefs->primary_radio_preamble;
|
|
_prefs->freq = freq; _prefs->bw = bw; _prefs->sf = sf; _prefs->cr = cr;
|
|
_prefs->primary_radio_preamble = preamble;
|
|
_prefs->tx_power_dbm = mesh::clampLoRaTxPower(old_power, freq);
|
|
recalculateRxPowerSavingFromLevel(_prefs);
|
|
if (!saveCommonPrefs()) {
|
|
_prefs->freq = old_freq; _prefs->bw = old_bw;
|
|
_prefs->sf = old_sf; _prefs->cr = old_cr;
|
|
_prefs->tx_power_dbm = old_power;
|
|
_prefs->rx_ps_rx_us = old_rx; _prefs->rx_ps_sleep_us = old_sleep;
|
|
_prefs->primary_radio_preamble = old_preamble;
|
|
return false;
|
|
}
|
|
_radio_profiles.adoptPrimaryPreamble(preamble);
|
|
return true;
|
|
}
|
|
|
|
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) {
|
|
PrefsSaveReplyGuard save_reply(_prefs_save_failures, reply);
|
|
mesh::cli::normalizeCommandVerb(command);
|
|
if (mesh::handleUsbLoggingWatchdogCommand(command, reply, 160)) return;
|
|
if (handleManagementCommand(command, reply)) return;
|
|
if (mesh::wireless::control().handle(command, reply, 160, millis(),
|
|
_callbacks->wirelessCommandSource(sender_timestamp))) return;
|
|
if (_radio_profiles.handle(command, reply, 160, sender_timestamp != 0)) return;
|
|
#if defined(ENABLE_OTA)
|
|
const auto* ota_context = mesh::ota::ota_context_if_active();
|
|
const float adaptive_ota_pace = ota_context
|
|
? ota_context->manager.adaptivePacketSpeed() : mesh::ota::OTA_SPEED_DEFAULT;
|
|
if (mesh::ota::handleSpeedCommand(command, reply, 160, adaptive_ota_pace)) return;
|
|
#endif
|
|
if (strncmp(command, "set tempradio ", 14) == 0) {
|
|
handleCommand(sender_timestamp, command + 4, reply);
|
|
return;
|
|
}
|
|
if (!strcmp(command, "get tempradio") || !strcmp(command, "tempradio")) {
|
|
const auto* radio = _callbacks->getProfileRadio();
|
|
if (!radio || !radio->profiles() || !radio->profiles()->primary_temporary) strcpy(reply, "> off");
|
|
else {
|
|
const auto& p = radio->profiles()->primary;
|
|
snprintf(reply, 160, "> %.3f,%.3f,%u,%u,%u", p.freq, p.bw, p.sf, p.cr, radio->profilePreamble(0));
|
|
}
|
|
return;
|
|
}
|
|
if (mesh::ui::handleDisplayPowerCommand(command, reply, 160)) return;
|
|
|
|
// 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 (mesh::cli::handleFloodAdvertGet(_callbacks->getFloodAdvertLimiter(), command, reply, millis())) return;
|
|
if (mesh::cli::handleFloodAdvertClear(_callbacks->getFloodAdvertLimiter(), 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 (sender_timestamp == 0
|
|
&& mesh::cli::isUf2ResetCommand(command)) {
|
|
if (!_board->rebootToUf2Bootloader()) {
|
|
strcpy(reply, "ERR: unsupported");
|
|
}
|
|
} 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);
|
|
_callbacks->onManualClockSet();
|
|
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");
|
|
}
|
|
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
|
} else if (memcmp(command, "start webconfig", 15) == 0
|
|
&& (command[15] == 0 || command[15] == ' ')) {
|
|
const bool force_ap = command[15] == ' ' && strcmp(&command[16], "ap") == 0;
|
|
if (command[15] == ' ' && !force_ap) {
|
|
strcpy(reply, "ERR: usage start webconfig [ap]");
|
|
} else if (!_callbacks->startWebConfig(force_ap, reply)) {
|
|
strcpy(reply, "ERR: webconfig not supported on this build");
|
|
}
|
|
} else if (strcmp(command, "stop webconfig") == 0) {
|
|
if (!_callbacks->stopWebConfig(reply)) {
|
|
strcpy(reply, "ERR: webconfig not supported on this build");
|
|
}
|
|
#endif
|
|
} else if (memcmp(command, "memory", 6) == 0) {
|
|
#ifdef ESP_PLATFORM
|
|
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
|
|
struct mallinfo mi = mallinfo();
|
|
sprintf(reply, "Heap: free=%d, used=%d", (int)mi.fordblks, (int)mi.uordblks);
|
|
#endif
|
|
} else if (memcmp(command, "start ota", 9) == 0 && (command[9] == 0 || command[9] == ' ')) {
|
|
// Manual OTA: bring up the board's browser server for a hand-uploaded binary.
|
|
const bool force_ap = command[9] == ' ' && strcmp(&command[10], "ap") == 0;
|
|
if (command[9] == ' ' && !force_ap) {
|
|
strcpy(reply, "ERR: usage start ota [ap]");
|
|
return;
|
|
}
|
|
bool webconfig_stopped = false;
|
|
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
|
if (_callbacks->isWebConfigActive()) {
|
|
if (!_callbacks->stopWebConfigForOTA(reply)) {
|
|
if (!reply[0]) strcpy(reply, "ERR: could not stop WebConfig for OTA");
|
|
return;
|
|
}
|
|
webconfig_stopped = true;
|
|
}
|
|
#endif
|
|
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
|
// Browser OTA owns the shared WiFi driver. An independent UART bridge
|
|
// can keep running throughout the upload.
|
|
const bool espnow_paused = _callbacks->isEspNowBridgeRunning();
|
|
if (espnow_paused && !_callbacks->setEspNowBridgeState(false)) {
|
|
strcpy(reply, "ERR: could not pause ESP-NOW for OTA");
|
|
return;
|
|
}
|
|
#endif
|
|
reply[0] = 0;
|
|
if (!_board->startOTAUpdate(_prefs->node_name, reply, force_ap)) {
|
|
if (!reply[0]) strcpy(reply, "Error");
|
|
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
|
if (espnow_paused && !_callbacks->setEspNowBridgeState(true)) {
|
|
const size_t used = strlen(reply);
|
|
snprintf(reply + used, 160 - used, "; ESP-NOW resume failed");
|
|
}
|
|
#endif
|
|
}
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(LIGHTWEIGHT_WIFI_OTA)
|
|
else {
|
|
// Keep WiFi up, but release MQTT/TLS heap while the browser uploader runs.
|
|
_callbacks->setBridgeState(false);
|
|
}
|
|
#endif
|
|
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
|
else if (espnow_paused) {
|
|
const size_t used = strlen(reply);
|
|
snprintf(reply + used, 160 - used, "; ESP-NOW paused");
|
|
}
|
|
#endif
|
|
if (webconfig_stopped) {
|
|
const size_t used = strlen(reply);
|
|
snprintf(reply + used, 160 - used, "; WebConfig stopped");
|
|
}
|
|
} else if (memcmp(command, "stop ota", 8) == 0 && (command[8] == 0 || command[8] == ' ')) {
|
|
if (!_board->stopOTAUpdate(reply)) {
|
|
if (!reply[0]) strcpy(reply, "Error");
|
|
}
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(LIGHTWEIGHT_WIFI_OTA)
|
|
else if (_prefs->bridge_enabled
|
|
#ifdef WITH_ESPNOW_BRIDGE
|
|
|| _prefs->espnow_bridge_enabled
|
|
#endif
|
|
) {
|
|
_callbacks->setBridgeState(true);
|
|
}
|
|
#endif
|
|
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
|
else if (
|
|
#if defined(WITH_RS232_BRIDGE) || defined(ESPNOW_BRIDGE_MERGED)
|
|
_prefs->espnow_bridge_enabled
|
|
#else
|
|
_prefs->bridge_enabled
|
|
#endif
|
|
) {
|
|
const bool resumed = _callbacks->setEspNowBridgeState(true);
|
|
const size_t used = strlen(reply);
|
|
snprintf(reply + used, 160 - used, resumed
|
|
? "; ESP-NOW resumed" : "; ESP-NOW resume failed");
|
|
}
|
|
#endif
|
|
} 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);
|
|
_callbacks->onManualClockSet();
|
|
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 (strncmp(command, "neighbor.remove", 15) == 0
|
|
&& (command[15] == 0 || command[15] == ' ' || command[15] == '\t')) {
|
|
const char* hex = &command[15];
|
|
while (*hex == ' ' || *hex == '\t' || *hex == '\r' || *hex == '\n') hex++;
|
|
uint8_t pubkey[PUB_KEY_SIZE];
|
|
size_t hex_len = strlen(hex);
|
|
int pubkey_len = (int)(hex_len / 2);
|
|
if (hex_len == 0) {
|
|
// Mobile clients clear all neighbors with an empty prefix. The
|
|
// repeater's command normalization may already have trimmed its space.
|
|
_callbacks->removeNeighbor(NULL, 0);
|
|
strcpy(reply, "OK");
|
|
} else if (hex_len <= PUB_KEY_SIZE * 2 && (hex_len & 1) == 0
|
|
&& mesh::Utils::fromHex(pubkey, pubkey_len, hex)) {
|
|
_callbacks->removeNeighbor(pubkey, pubkey_len);
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "ERR: bad pubkey");
|
|
}
|
|
} else if (strcmp(command, "normalradio") == 0) {
|
|
if (_callbacks->scheduleNormalRadio()) {
|
|
strcpy(reply, "OK - normal radio restore scheduled");
|
|
} else {
|
|
strcpy(reply, "Error: unsupported");
|
|
}
|
|
} else if (memcmp(command, "tempradio ", 10) == 0) {
|
|
uint16_t preamble = 0;
|
|
if (!mesh::RadioProfileCLI::parseSuffix(command + 10, 5, tmp, sizeof(tmp), preamble)) {
|
|
strcpy(reply, "Error, use: tempradio f,bw,sf,cr,minutes[,preamble]"); return;
|
|
}
|
|
const char *parts[5];
|
|
int num = mesh::Utils::parseTextParts(tmp, parts, 5);
|
|
float freq = 0.0f;
|
|
float bw = 0.0f;
|
|
uint32_t sf = 0, cr = 0;
|
|
uint32_t temp_timeout_mins = 0;
|
|
if (num == 5
|
|
&& parseUint32Strict(parts[2], sf) && parseUint32Strict(parts[3], cr)
|
|
&& parseUint32Strict(parts[4], temp_timeout_mins)
|
|
&& temp_timeout_mins > 0
|
|
&& (uint64_t)getRTCClock()->getCurrentTime() + 2
|
|
+ (uint64_t)temp_timeout_mins * 60 <= UINT32_MAX
|
|
&& mesh::cli::parseDecimalStrict(parts[0], freq)
|
|
&& mesh::cli::parseDecimalStrict(parts[1], bw)
|
|
&& freq >= 150.0f && freq <= 2500.0f
|
|
&& sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8
|
|
&& isValidLoRaBandwidth(bw)
|
|
&& _radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
|
|
_callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins, preamble);
|
|
char duration[64];
|
|
const uint32_t seconds = (uint32_t)temp_timeout_mins * 60UL;
|
|
mesh::RepeaterRadioTiming::formatDuration(duration, sizeof(duration), seconds);
|
|
snprintf(reply, 160, "OK - temp params for %s", duration);
|
|
_radio_profiles.appendPrimaryChirpWarning(reply, 160, sf, bw, preamble);
|
|
_callbacks->appendTempRadioTimingNote(reply, 160, seconds);
|
|
appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw);
|
|
} else {
|
|
strcpy(reply, "Error, invalid params");
|
|
}
|
|
} else if (memcmp(command, "password ", 9) == 0) {
|
|
// change admin password
|
|
char previous[sizeof(_prefs->password)];
|
|
memcpy(previous, _prefs->password, sizeof(previous));
|
|
StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password));
|
|
if (!trySavePrefs()) {
|
|
memcpy(_prefs->password, previous, sizeof(previous));
|
|
strcpy(reply, "Error: password not saved; unchanged");
|
|
return;
|
|
}
|
|
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, "clear recent.repeater", 21) == 0 && (command[21] == 0 || command[21] == ' ')) {
|
|
if (_callbacks->supportsAdvancedRetryConfig()) {
|
|
_callbacks->clearRecentRepeaters();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, unsupported on this role");
|
|
}
|
|
} 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 (memcmp(command, "del ", 4) == 0) {
|
|
handleDelCmd(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());
|
|
#if defined(ENABLE_OTA)
|
|
} else if (memcmp(command, "ota", 3) == 0 && (command[3] == 0 || command[3] == ' ')) {
|
|
if (!_callbacks->isTempRadioActive() && !_radio_profiles.secondaryTemporary()
|
|
&& otaCommandNeedsTempRadio(command)) {
|
|
strcpy(reply, "LoRa OTA needs temp radio on every node. Run: tempradio 909.950,250,5,5,120");
|
|
} else {
|
|
if (!mesh::ota::ota_acquire_context(reply, 160)) return;
|
|
const auto previous = mesh::ota::OtaConfigState::capture(mesh::ota::ota_ctx());
|
|
mesh::ota::handle_ota_command(command, reply, *_board);
|
|
if (mesh::ota::ota_context_if_active()
|
|
&& mesh::ota::ota_ctx().config_dirty) { // a policy/key changed via the CLI -> persist it
|
|
mesh::ota::OtaContext& c = mesh::ota::ota_ctx();
|
|
const auto copy_policy = [this, &c]() {
|
|
_prefs->ota_autofetch = c.manager.autofetch();
|
|
_prefs->ota_checkpoint_blocks = c.manager.checkpoint_blocks();
|
|
_prefs->ota_advert_interval = c.manager.advert_mins();
|
|
_prefs->ota_max_hops = c.manager.max_hops();
|
|
_prefs->ota_autoinstall = c.autoinstall;
|
|
_prefs->ota_signer_count = c.allow.count();
|
|
memset(_prefs->ota_signers, 0, sizeof(_prefs->ota_signers));
|
|
for (uint8_t i = 0; i < c.allow.count() && i < MAX_OTA_SIGNERS; i++)
|
|
memcpy(_prefs->ota_signers[i], c.allow.get(i), 32);
|
|
};
|
|
copy_policy();
|
|
if (saveCommonPrefs()) {
|
|
c.config_dirty = false;
|
|
} else {
|
|
previous.apply(c);
|
|
copy_policy();
|
|
strcpy(reply, "ERR OTA configuration could not be saved");
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
} else if (memcmp(command, "ota", 3) == 0 && (command[3] == 0 || command[3] == ' ')) {
|
|
strcpy(reply, "LoRa OTA is not included in this build; tempradio cannot enable it. Use an OTA-enabled repeater firmware");
|
|
#endif
|
|
} else if (strcmp(command, "power") == 0) {
|
|
const char* usb_state = _board->hasUsbPowerDetect()
|
|
? (_board->isUsbPowered() ? "yes" : "no")
|
|
: "n/a";
|
|
const char* solar_state = _board->hasSolarChargerDetect()
|
|
? (_board->isSolarChargerActive() ? "yes" : "no")
|
|
: "n/a";
|
|
snprintf(reply, 160, "batt:%u mV usb:%s solar_chg:%s ext:%s charger:%s",
|
|
(unsigned)_board->getBattMilliVolts(),
|
|
usb_state,
|
|
solar_state,
|
|
_board->isExternalPowered() ? "yes" : "no",
|
|
_board->isChargerActive() ? "yes" : "no");
|
|
} 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";
|
|
uint32_t gps_interval = 0;
|
|
const bool is_gps_interval = strcmp(key, "gps_interval") == 0;
|
|
const bool is_gps_toggle = strcmp(key, "gps") == 0;
|
|
const bool valid_gps_interval = !is_gps_interval
|
|
|| (parseUint32Strict(value, gps_interval)
|
|
&& gps_interval <= mesh::gps::MAX_UPDATE_INTERVAL_SEC);
|
|
const bool valid_gps_toggle = !is_gps_toggle
|
|
|| strcmp(value, "0") == 0 || strcmp(value, "1") == 0;
|
|
|
|
if (!valid_gps_interval) {
|
|
strcpy(reply, "gps_interval must be 0..86400 seconds");
|
|
} else if (!valid_gps_toggle) {
|
|
strcpy(reply, "gps must be 0 or 1");
|
|
}
|
|
else if (_sensors->setSettingValue(key, value)) {
|
|
if (is_gps_interval) {
|
|
_prefs->gps_interval = gps_interval;
|
|
savePrefs();
|
|
} else if (is_gps_toggle) {
|
|
_prefs->gps_enabled = strcmp(value, "1") == 0 ? 1 : 0;
|
|
savePrefs();
|
|
}
|
|
#ifdef WITH_RS232_BRIDGE
|
|
if (is_gps_toggle && strcmp(value, "1") == 0
|
|
&& _callbacks->isBridgeRunning()
|
|
&& _sensors->gpsUsesSerialUart(_prefs->bridge_uart)) {
|
|
strcpy(reply, "saved; UART GPS paused while bridge is enabled");
|
|
} else
|
|
#endif
|
|
{
|
|
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();
|
|
|
|
if (_callbacks->isBridgeRunning()
|
|
#ifdef WITH_RS232_BRIDGE
|
|
&& _sensors->gpsUsesSerialUart(_prefs->bridge_uart)
|
|
#else
|
|
&& false
|
|
#endif
|
|
) {
|
|
strcpy(reply, "saved; UART GPS paused while bridge is enabled");
|
|
} else if (_prefs->powersaving_enabled) { // Power Saving
|
|
strcpy(reply, "on (powersaving)");
|
|
} else { // Normal mode
|
|
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 (!_prefs->gps_enabled) {
|
|
strcpy(reply, "gps is off");
|
|
#ifdef WITH_RS232_BRIDGE
|
|
} else if (_callbacks->isBridgeRunning()
|
|
&& _sensors->gpsUsesSerialUart(_prefs->bridge_uart)) {
|
|
strcpy(reply, "gps paused by RS232 bridge");
|
|
#endif
|
|
} else if (l != NULL) {
|
|
l->syncTime();
|
|
strcpy(reply, "scheduled");
|
|
} else {
|
|
strcpy(reply, "gps provider not found");
|
|
}
|
|
} else if (strcmp(command, "gps diag") == 0) {
|
|
LocationProvider * l = _sensors->getLocationProvider();
|
|
if (l != NULL) {
|
|
const char* setting = _sensors->getSettingByKey("gps");
|
|
bool requested = setting != NULL && strcmp(setting, "1") == 0;
|
|
int prefix_len = snprintf(reply, 160, "req:%u ", requested ? 1U : 0U);
|
|
if (prefix_len < 0 || prefix_len >= 160) {
|
|
reply[159] = 0;
|
|
} else {
|
|
l->formatDiagnostics(reply + prefix_len, 160 - prefix_len);
|
|
}
|
|
} 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();
|
|
const char* gps_setting = _sensors->getSettingByKey("gps");
|
|
bool active = gps_setting != NULL && strcmp(gps_setting, "1") == 0;
|
|
|
|
if (!active) {
|
|
snprintf(reply, 160, "off, %s", enabled ? "powered" : "unpowered");
|
|
} else if (_prefs->powersaving_enabled && l->getGPSPowerSaving()) { // GPS Power Saving
|
|
unsigned long now = millis();
|
|
unsigned long next_off = l->getNextGPSOff();
|
|
unsigned long deadline = next_off != 0 ? next_off : l->getNextGPSOn();
|
|
long remaining_ms = deadline == 0 ? 0 : (long)(deadline - now);
|
|
unsigned long mins = remaining_ms > 0 ? (unsigned long)remaining_ms / 60000UL : 0;
|
|
if (next_off != 0) {
|
|
snprintf(reply, 160, "on, %s (powersaving, sleep in %luh %lum), %s, %d sats",
|
|
enabled ? "powered" : "unpowered", mins / 60UL, mins % 60UL,
|
|
fix ? "fix" : "no fix", sats);
|
|
} else {
|
|
snprintf(reply, 160, "on, %s (powersaving, wake in %luh %lum)",
|
|
enabled ? "powered" : "unpowered", mins / 60UL, mins % 60UL);
|
|
}
|
|
|
|
// "last sync" from GPS
|
|
unsigned long last_sync = l->getLastValidTimeSync();
|
|
size_t used = strlen(reply);
|
|
if (last_sync == 0) {
|
|
snprintf(reply + used, 160 - used, ", last sync: none");
|
|
} else {
|
|
DateTime dt = DateTime(last_sync);
|
|
snprintf(reply + used, 160 - used, ", last sync: %02d:%02d - %d/%d/%d UTC",
|
|
dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
|
|
}
|
|
} else { // Normal mode
|
|
snprintf(reply, 160, "on, %s, %s, %d sats",
|
|
enabled ? "powered" : "unpowered",
|
|
fix ? "fix" : "no fix", sats);
|
|
}
|
|
} else {
|
|
strcpy(reply, "Can't find GPS");
|
|
}
|
|
#endif
|
|
} else if (strcmp(command, "powersaving on") == 0 || strcmp(command, "powersaving off") == 0) {
|
|
char canonical[24];
|
|
snprintf(canonical, sizeof(canonical), "set %s", command);
|
|
handleSetCmd(sender_timestamp, canonical, reply);
|
|
} else if (strcmp(command, "powersaving") == 0) {
|
|
char canonical[] = "get powersaving";
|
|
handleGetCmd(sender_timestamp, canonical, reply);
|
|
} else if (memcmp(command, "sensor", 6) == 0) {
|
|
// I2C
|
|
#if ENV_HAS_SECONDARY_I2C
|
|
sprintf(reply, "I2C Wire1: SDA=%s,SCL=%s\r\n", STR(ENV_PIN_SDA), STR(ENV_PIN_SCL));
|
|
#elif defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
|
|
sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n", STR(PIN_BOARD_SDA), STR(PIN_BOARD_SCL));
|
|
#elif defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL)
|
|
sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n", STR(PIN_WIRE_SDA), STR(PIN_WIRE_SCL));
|
|
#else
|
|
sprintf(reply, "I2C GPIOs not defined\r\n");
|
|
#endif
|
|
|
|
// GPS
|
|
#if defined(PIN_GPS_RX) && defined(PIN_GPS_TX)
|
|
sprintf(reply + strlen(reply), "GPS Serial: RX=%s, TX=%s", STR(PIN_GPS_RX), STR(PIN_GPS_TX));
|
|
#if defined(ENV_INCLUDE_GPS) && ENV_INCLUDE_GPS > 0
|
|
sprintf(reply + strlen(reply), ". Configured");
|
|
#else
|
|
sprintf(reply + strlen(reply), ". Not configured");
|
|
#endif
|
|
#else
|
|
sprintf(reply + strlen(reply), "GPS Serial not defined");
|
|
#endif
|
|
} else if (memcmp(command, "powerlog", 8) == 0) {
|
|
sprintf(reply, "Last reset reason: %s", _board->getResetReasonString(_board->getResetReason()));
|
|
#if defined(NRF52_PLATFORM)
|
|
sprintf(reply + strlen(reply), "\r\nLast shutdown reason: %s",
|
|
_board->getShutdownReasonString(_board->getShutdownReason()));
|
|
sprintf(reply + strlen(reply), "\r\nLast boot voltage: %u mV", _board->getBootVoltage());
|
|
#endif
|
|
} 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 if (_board->handleCommand(command, sender_timestamp, reply)) {
|
|
if (_prefs->isDirty()) savePrefs();
|
|
} else {
|
|
strcpy(reply, "Unknown command");
|
|
}
|
|
}
|
|
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
bool CommonCLI::handleSdCardSetCmd(const char* config, char* reply) {
|
|
if (strncmp(config, "sdcard", 6) != 0 ||
|
|
(config[6] != 0 && config[6] != ' ')) {
|
|
return false;
|
|
}
|
|
|
|
const char* action_args = skipSpacesConst(config + 6);
|
|
bool is_format;
|
|
bool force = false;
|
|
if (strcmp(action_args, "format") == 0) {
|
|
is_format = true;
|
|
} else if (strcmp(action_args, "format --force") == 0) {
|
|
is_format = true;
|
|
force = true;
|
|
} else if (strcmp(action_args, "erase") == 0) {
|
|
is_format = false;
|
|
} else if (strcmp(action_args, "erase --force") == 0) {
|
|
is_format = false;
|
|
force = true;
|
|
} else {
|
|
strcpy(reply, "Error: use set sdcard format|erase [--force]");
|
|
return true;
|
|
}
|
|
|
|
const bool recorded = is_format ? _sdcard_format_recorded : _sdcard_erase_recorded;
|
|
const uint32_t ran_at = is_format ? _sdcard_format_at : _sdcard_erase_at;
|
|
const char* action = is_format ? "format" : "erase";
|
|
const uint32_t now = millis();
|
|
if (!force && recorded && now - ran_at < SDCARD_ACTION_COOLDOWN_MILLIS) {
|
|
char age[32];
|
|
formatSdCardElapsed(age, sizeof(age), true, ran_at, now);
|
|
snprintf(reply, 160,
|
|
"Error: SD card %s ran %s; use set sdcard %s --force",
|
|
action, age, action);
|
|
return true;
|
|
}
|
|
|
|
mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
|
|
if (context.apply_pending) {
|
|
strcpy(reply, "Error: OTA apply is pending");
|
|
return true;
|
|
}
|
|
#if defined(OTA_TOWER_AUTO_STORE)
|
|
// Card maintenance must never abandon a fallback update stored internally.
|
|
if (context.fetch_store.usesInternal() &&
|
|
(context.manager.fetchState() != mesh::ota::OtaManager::IDLE ||
|
|
context.fetch_store.staged_size() != 0)) {
|
|
strcpy(reply, "Error: internal OTA slot is occupied; use ota cancel first");
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
mesh::ota::OtaStoreSdNrf52& card = context.sdStagingStore();
|
|
|
|
// A card-wide destructive operation invalidates any staged fetch. First let
|
|
// an archive capture checkpoint and close its shared card handles.
|
|
context.prepareSdCardReset();
|
|
context.manager.reset_session();
|
|
context.manager.want(0);
|
|
context.manager.want_mid(nullptr);
|
|
context.session_started_ms = 0;
|
|
context.prev_fstate = mesh::ota::OtaManager::IDLE;
|
|
|
|
if (is_format) {
|
|
if (!card.formatCard(*_board)) {
|
|
char error[80];
|
|
strncpy(error, card.last_error(), sizeof(error) - 1);
|
|
error[sizeof(error) - 1] = 0;
|
|
context.finishSdCardReset(millis());
|
|
snprintf(reply, 160, "Error: SD card format failed: %s",
|
|
error);
|
|
return true;
|
|
}
|
|
context.finishSdCardReset(millis());
|
|
_sdcard_format_recorded = true;
|
|
_sdcard_format_at = millis();
|
|
strcpy(reply, "OK - SD card format complete");
|
|
return true;
|
|
}
|
|
|
|
if (!card.eraseCard(*_board)) {
|
|
char error[80];
|
|
strncpy(error, card.last_error(), sizeof(error) - 1);
|
|
error[sizeof(error) - 1] = 0;
|
|
context.finishSdCardReset(millis());
|
|
snprintf(reply, 160, "Error: SD card erase failed: %s",
|
|
error);
|
|
return true;
|
|
}
|
|
_sdcard_erase_recorded = true;
|
|
_sdcard_erase_at = millis();
|
|
|
|
if (!card.formatCard(*_board)) {
|
|
char error[80];
|
|
strncpy(error, card.last_error(), sizeof(error) - 1);
|
|
error[sizeof(error) - 1] = 0;
|
|
context.finishSdCardReset(millis());
|
|
snprintf(reply, 160, "Error: SD card erased but format failed: %s",
|
|
error);
|
|
return true;
|
|
}
|
|
_sdcard_format_recorded = true;
|
|
_sdcard_format_at = millis();
|
|
context.finishSdCardReset(millis());
|
|
strcpy(reply, "OK - SD card erase and format complete");
|
|
return true;
|
|
}
|
|
|
|
bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
|
|
if (strncmp(config, "sdcard", 6) != 0 ||
|
|
(config[6] != 0 && config[6] != ' ')) {
|
|
return false;
|
|
}
|
|
|
|
const char* query = skipSpacesConst(config + 6);
|
|
const uint32_t now = millis();
|
|
if (*query == 0 || strcmp(query, "*") == 0) {
|
|
char format_age[32];
|
|
char erase_age[32];
|
|
formatSdCardElapsed(format_age, sizeof(format_age),
|
|
_sdcard_format_recorded, _sdcard_format_at, now);
|
|
formatSdCardElapsed(erase_age, sizeof(erase_age),
|
|
_sdcard_erase_recorded, _sdcard_erase_at, now);
|
|
snprintf(reply, 160, "> format: %s, erase: %s", format_age, erase_age);
|
|
} else if (strcmp(query, "format") == 0) {
|
|
char age[32];
|
|
formatSdCardElapsed(age, sizeof(age), _sdcard_format_recorded,
|
|
_sdcard_format_at, now);
|
|
snprintf(reply, 160, "> format: %s", age);
|
|
} else if (strcmp(query, "erase") == 0) {
|
|
char age[32];
|
|
formatSdCardElapsed(age, sizeof(age), _sdcard_erase_recorded,
|
|
_sdcard_erase_at, now);
|
|
snprintf(reply, 160, "> erase: %s", age);
|
|
} else if (strcmp(query, "free") == 0) {
|
|
uint64_t used_bytes = 0;
|
|
uint64_t free_bytes = 0;
|
|
mesh::ota::OtaStoreSdNrf52& store = mesh::ota::ota_ctx().sdStagingStore();
|
|
if (!store.getSpace(*_board, used_bytes, free_bytes)) {
|
|
snprintf(reply, 160, "Error: SD card space query failed: %s",
|
|
store.last_error());
|
|
} else {
|
|
char used[24];
|
|
char free[24];
|
|
mesh::cli::formatStorageBytes(used, sizeof(used), used_bytes);
|
|
mesh::cli::formatStorageBytes(free, sizeof(free), free_bytes);
|
|
snprintf(reply, 160, "> used: %s, free: %s", used, free);
|
|
}
|
|
} else if (strncmp(query, "ls", 2) == 0 || strncmp(query, "dir", 3) == 0) {
|
|
const bool is_ls = strncmp(query, "ls", 2) == 0;
|
|
const size_t word_len = is_ls ? 2 : 3;
|
|
if (query[word_len] != 0 && query[word_len] != ' ') {
|
|
strcpy(reply, "Error: use get sdcard ls|dir [page]");
|
|
return true;
|
|
}
|
|
const char* page_arg = skipSpacesConst(query + word_len);
|
|
uint32_t page = 1;
|
|
if (*page_arg) {
|
|
char* end = nullptr;
|
|
unsigned long parsed = strtoul(page_arg, &end, 10);
|
|
if (parsed == 0 || parsed > 65535 || !end || *skipSpacesConst(end) != 0) {
|
|
strcpy(reply, "Error: use get sdcard ls|dir [page]");
|
|
return true;
|
|
}
|
|
page = parsed;
|
|
}
|
|
mesh::ota::OtaStoreSdNrf52& store = mesh::ota::ota_ctx().sdStagingStore();
|
|
if (!store.listFiles(*_board, (uint16_t)page, reply, 160)) {
|
|
snprintf(reply, 160, "Error: SD card list failed: %s", store.last_error());
|
|
}
|
|
} else {
|
|
strcpy(reply, "Error: use get sdcard [format|erase|free|ls|dir [page]|*]");
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
|
const char* config = &command[4];
|
|
if (strncmp(config, "repeat.trace", 12) == 0
|
|
&& (config[12] == 0 || config[12] == ' ' || config[12] == '\t')) {
|
|
if (strcmp(_callbacks->getRole(), "repeater") != 0) {
|
|
strcpy(reply, "Error: repeat.trace is only supported by repeaters");
|
|
return;
|
|
}
|
|
const char* value = config + 12;
|
|
while (*value == ' ' || *value == '\t') ++value;
|
|
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
|
|
strcpy(reply, "Error: usage set repeat.trace on|off");
|
|
return;
|
|
}
|
|
const uint8_t previous = _prefs->trace_when_repeat_off;
|
|
_prefs->trace_when_repeat_off = strcmp(value, "on") == 0 ? 1 : 0;
|
|
if (!trySavePrefs()) {
|
|
_prefs->trace_when_repeat_off = previous;
|
|
strcpy(reply, "Error: Repeat trace not saved; unchanged");
|
|
return;
|
|
}
|
|
_callbacks->onRetryConfigChanged();
|
|
snprintf(reply, 160, "OK - repeat.trace %s (saved)", value);
|
|
return;
|
|
}
|
|
#if defined(NRF52_POWER_MANAGEMENT)
|
|
if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) return;
|
|
#endif
|
|
// Infrastructure roles dispatch here rather than through CommonRadioPrefs.
|
|
if (strncmp(config, "path.hash.mode", 14) == 0
|
|
&& (config[14] == 0 || config[14] == ' ' || config[14] == '\t')) {
|
|
uint32_t mode = 0;
|
|
if (mesh::cli::parseUnsignedIntegerStrict(config + 14, mode) && mode <= 2) {
|
|
_prefs->path_hash_mode = static_cast<uint8_t>(mode);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 0,1, or 2");
|
|
}
|
|
return;
|
|
}
|
|
#if ENV_INCLUDE_GPS == 1
|
|
if (strncmp(config, "gps.sync.interval", 17) == 0
|
|
&& (config[17] == 0 || config[17] == ' ' || config[17] == '\t')) {
|
|
const char* value = config + 17;
|
|
while (*value == ' ' || *value == '\t') ++value;
|
|
uint16_t hours = 0;
|
|
if (!mesh::gps::parseSyncIntervalHours(value, hours)) {
|
|
strcpy(reply, "Error: GPS sync interval must be 1..336 hours");
|
|
} else if (_sensors->getLocationProvider() == nullptr) {
|
|
strcpy(reply, "Error: GPS unavailable");
|
|
} else {
|
|
const uint16_t previous = _prefs->gps_sync_interval_hours;
|
|
_prefs->gps_sync_interval_hours = hours;
|
|
if (!trySavePrefs()) {
|
|
_prefs->gps_sync_interval_hours = previous;
|
|
strcpy(reply, "Error: GPS sync interval could not be saved");
|
|
} else {
|
|
_sensors->applyGpsTimeSyncInterval(hours);
|
|
snprintf(reply, 160, "OK - GPS sync interval %u hours (saved)", (unsigned)hours);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (strncmp(config, "gps ", 4) == 0) {
|
|
if (strcmp(config + 4, "on") != 0 && strcmp(config + 4, "off") != 0) {
|
|
strcpy(reply, "Error: use set gps on|off");
|
|
} else {
|
|
char gps_command[8];
|
|
snprintf(gps_command, sizeof(gps_command), "%s", config);
|
|
handleCommand(sender_timestamp, gps_command, reply);
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
|
|
if (strncmp(config, "espnow.channel", 14) == 0
|
|
&& (config[14] == 0 || config[14] == ' ' || config[14] == '\t')) {
|
|
const char* value = config + 14;
|
|
while (*value == ' ' || *value == '\t') value++;
|
|
uint8_t channel = 0;
|
|
if (!mesh::wifi::parseEspNowChannel(value, channel)) {
|
|
strcpy(reply, "Error: ESP-NOW channel must be 1-13");
|
|
} else {
|
|
// Snapshot the boot channel before writing NVS. The saved setting must
|
|
// never become the active value in this boot merely because this is the
|
|
// first policy helper reached by a CLI-only image.
|
|
const uint8_t active = mesh::wifi::activeEspNowChannel();
|
|
if (!mesh::wifi::saveConfiguredEspNowChannel(channel)) {
|
|
strcpy(reply, "Error: failed to save ESP-NOW channel");
|
|
} else if (channel == active) {
|
|
snprintf(reply, 160,
|
|
"OK - ESP-NOW channel %u saved and active",
|
|
(unsigned)channel);
|
|
} else {
|
|
snprintf(reply, 160,
|
|
"OK - ESP-NOW channel %u saved; active %u; reboot required",
|
|
(unsigned)channel, (unsigned)active);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
#if MESH_USB_LOGGING_AVAILABLE
|
|
if (strncmp(config, "usb.logging", 11) == 0
|
|
&& (config[11] == 0 || config[11] == ' ' || config[11] == '\t')) {
|
|
const char* value = &config[11];
|
|
while (*value == ' ' || *value == '\t') value++;
|
|
bool enabled = false, reboot_if_needed = false;
|
|
if (mesh::cli::parseLoggingToggle(value, enabled, reboot_if_needed)) {
|
|
const uint8_t previous = _prefs->usb_logging_enabled;
|
|
_prefs->usb_logging_enabled = enabled ? 1 : 0;
|
|
if (!trySavePrefs()) {
|
|
_prefs->usb_logging_enabled = previous;
|
|
strcpy(reply, "Error: USB logging not saved; unchanged");
|
|
return;
|
|
}
|
|
// Commit before changing the stream's live protocol ownership.
|
|
mesh::setUsbLoggingEnabled(enabled);
|
|
snprintf(reply, 160, "OK - USB logging %s (saved)", enabled ? "on" : "off");
|
|
} else {
|
|
strcpy(reply, "Error: usage set usb.logging on|off");
|
|
}
|
|
return;
|
|
}
|
|
if (strncmp(config, "usb.debug", 9) == 0
|
|
&& (config[9] == 0 || config[9] == ' ' || config[9] == '\t')) {
|
|
const char* value = &config[9];
|
|
while (*value == ' ' || *value == '\t') value++;
|
|
bool enabled = false, reboot_if_needed = false;
|
|
if (mesh::cli::parseLoggingToggle(value, enabled, reboot_if_needed)
|
|
&& !reboot_if_needed) {
|
|
const uint8_t previous = _prefs->usb_debug_enabled;
|
|
_prefs->usb_debug_enabled = enabled ? 1 : 0;
|
|
if (!trySavePrefs()) {
|
|
_prefs->usb_debug_enabled = previous;
|
|
strcpy(reply, "Error: USB debug not saved; unchanged");
|
|
return;
|
|
}
|
|
mesh::setUsbDebugEnabled(enabled);
|
|
snprintf(reply, 160, "OK - USB debug %s (saved)", enabled ? "on" : "off");
|
|
} else {
|
|
strcpy(reply, "Error: usage set usb.debug on|off");
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
#if MESH_USB_LOGGING_AVAILABLE && defined(WITH_MQTT_BRIDGE)
|
|
if (strncmp(config, "logging.output", 14) == 0
|
|
&& (config[14] == 0 || config[14] == ' ' || config[14] == '\t')) {
|
|
const char* value = &config[14];
|
|
while (*value == ' ' || *value == '\t') value++;
|
|
bool usb_enabled;
|
|
bool wifi_enabled;
|
|
if (!mesh::cli::parseLoggingOutput(value, usb_enabled, wifi_enabled)) {
|
|
strcpy(reply, "Error: usage set logging.output off|usb|wifi|both");
|
|
return;
|
|
}
|
|
const uint8_t previous_usb = _prefs->usb_logging_enabled;
|
|
const uint8_t previous_bridge = _prefs->bridge_enabled;
|
|
_prefs->usb_logging_enabled = usb_enabled ? 1 : 0;
|
|
_prefs->bridge_enabled = wifi_enabled ? 1 : 0;
|
|
if (!trySavePrefs()) {
|
|
_prefs->usb_logging_enabled = previous_usb;
|
|
_prefs->bridge_enabled = previous_bridge;
|
|
strcpy(reply, "Error: logging output not saved; unchanged");
|
|
return;
|
|
}
|
|
mesh::setUsbLoggingEnabled(usb_enabled);
|
|
if (!_callbacks->setMqttBridgeState(wifi_enabled)) {
|
|
strcpy(reply, "Error: logging output saved, but MQTT runtime change failed");
|
|
return;
|
|
}
|
|
snprintf(reply, 160, "OK - logging.output %s (saved)", value);
|
|
return;
|
|
}
|
|
#endif
|
|
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
|
if (isGpioConfig(config)) {
|
|
const UserGpio::SetResult result = _user_gpio.handleSet(
|
|
config + 4, reply, 160, sender_timestamp,
|
|
_callbacks->getUserGpioRequestSource());
|
|
if (result.outcome == UserGpio::SetResult::TIMER_STARTED) {
|
|
_callbacks->onUserGpioTimerScheduled(result.pin, sender_timestamp);
|
|
} else if (result.outcome == UserGpio::SetResult::APPLIED &&
|
|
result.cancelled_timer) {
|
|
_callbacks->onUserGpioTimerCancelled(result.pin);
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
|
if (memcmp(config, "webui ", 6) == 0) {
|
|
const char* value = &config[6];
|
|
bool enabled;
|
|
if (strcmp(value, "on") == 0) enabled = true;
|
|
else if (strcmp(value, "off") == 0) enabled = false;
|
|
else {
|
|
strcpy(reply, "Error: usage set webui on|off");
|
|
return;
|
|
}
|
|
if (!_callbacks->setWebUIEnabled(enabled, reply)) {
|
|
strcpy(reply, "Error: webui not supported on this build");
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
|
const char* cli_value = nullptr;
|
|
if (mesh::cli::classifyStandaloneWiFiSet(config, &cli_value) ==
|
|
mesh::cli::StandaloneWiFiKey::CLI) {
|
|
if (!_callbacks->setWiFiCLI(cli_value, reply)) {
|
|
strcpy(reply, "Error: WiFi CLI unavailable on this build");
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// bridge_enabled is stored with the common node preferences. Keep its
|
|
// MQTT-specific switch here rather than in the observer preferences
|
|
// handler, which must only write its own store.
|
|
if (strncmp(config, "mqtt.enabled ", 13) == 0) {
|
|
const char* value = config + 13;
|
|
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
|
|
strcpy(reply, "Error: use set mqtt.enabled on|off");
|
|
} else {
|
|
const bool enable = strcmp(value, "on") == 0;
|
|
_prefs->bridge_enabled = enable ? 1 : 0;
|
|
const bool applied = _callbacks->setMqttBridgeState(enable);
|
|
savePrefs();
|
|
strcpy(reply, applied ? "OK"
|
|
: "Error: MQTT runtime change failed; setting saved");
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
|
|
if (handleObserverSetCmd(sender_timestamp, config, reply)) return;
|
|
#if defined(WITH_ESPNOW_BRIDGE)
|
|
if (memcmp(config, "espnow.enabled ", 15) == 0) {
|
|
const char* value = config + 15;
|
|
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
|
|
strcpy(reply, "Error: use set espnow.enabled on|off");
|
|
} else {
|
|
const bool enable = strcmp(value, "on") == 0;
|
|
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE) \
|
|
|| defined(ESPNOW_BRIDGE_MERGED)
|
|
_prefs->espnow_bridge_enabled = enable;
|
|
#endif
|
|
#if !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
|
_prefs->bridge_enabled = enable;
|
|
#endif
|
|
const bool applied = _callbacks->setEspNowBridgeState(enable);
|
|
savePrefs();
|
|
strcpy(reply, applied ? "OK"
|
|
: "Error: ESP-NOW runtime change failed; setting saved");
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
if (handleSdCardSetCmd(config, reply)) return;
|
|
#endif
|
|
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && \
|
|
!defined(WEBCONFIG_DISABLED) && !defined(WITH_MQTT_BRIDGE)
|
|
const char* wifi_value = nullptr;
|
|
const mesh::cli::StandaloneWiFiKey wifi_key =
|
|
mesh::cli::classifyStandaloneWiFiSet(config, &wifi_value);
|
|
bool wifi_supported = true;
|
|
switch (wifi_key) {
|
|
case mesh::cli::StandaloneWiFiKey::SSID:
|
|
wifi_supported = _callbacks->setWiFiSSID(wifi_value, reply);
|
|
break;
|
|
case mesh::cli::StandaloneWiFiKey::Password:
|
|
wifi_supported = _callbacks->setWiFiPassword(wifi_value, reply);
|
|
break;
|
|
case mesh::cli::StandaloneWiFiKey::PowerSave:
|
|
wifi_supported = _callbacks->setWiFiPowerSave(wifi_value, reply);
|
|
break;
|
|
case mesh::cli::StandaloneWiFiKey::CLI:
|
|
wifi_supported = _callbacks->setWiFiCLI(wifi_value, reply);
|
|
break;
|
|
default:
|
|
wifi_supported = false;
|
|
break;
|
|
}
|
|
if (wifi_key != mesh::cli::StandaloneWiFiKey::None) {
|
|
if (!wifi_supported) {
|
|
strcpy(reply, "Error: standalone WiFi settings unavailable on this build");
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
if (isAdvancedRetryConfig(config) && !_callbacks->supportsAdvancedRetryConfig()) {
|
|
strcpy(reply, "Error, unsupported on this role");
|
|
return;
|
|
}
|
|
if (isBasicRetryConfig(config) && !_callbacks->supportsBasicRetryConfig()) {
|
|
strcpy(reply, "Error, retry configuration unsupported on this role");
|
|
return;
|
|
}
|
|
if (memcmp(config, "dutycycle ", 10) == 0) {
|
|
float dc = 0.0f;
|
|
if (!mesh::cli::parseDecimalStrict(&config[10], dc)
|
|
|| dc < 1.0f || dc > 100.0f) {
|
|
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, "system.watchdog ", 16) == 0) {
|
|
#if defined(NRF52_PLATFORM)
|
|
const char* value = &config[16];
|
|
if (strcmp(value, "on") == 0) {
|
|
_prefs->system_watchdog_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "OK - system watchdog enabled");
|
|
} else if (strcmp(value, "off") == 0) {
|
|
_prefs->system_watchdog_enabled = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK - disabled; restarting within 60s");
|
|
} else {
|
|
strcpy(reply, "Error: use set system.watchdog on|off");
|
|
}
|
|
#else
|
|
strcpy(reply, "Error: unsupported on this platform");
|
|
#endif
|
|
} else if (memcmp(config, "af ", 3) == 0) {
|
|
float factor = 0.0f;
|
|
if (mesh::cli::parseDecimalStrict(&config[3], factor)) {
|
|
_prefs->airtime_factor = factor;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, invalid airtime factor");
|
|
}
|
|
} 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) {
|
|
const char* value = &config[4];
|
|
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
|
|
strcpy(reply, "Error: use set cad on|off");
|
|
} else {
|
|
_prefs->cad_enabled = strcmp(value, "on") == 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
} 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, "telemetry.access ", 17) == 0) {
|
|
if (strcmp(&config[17], "all") == 0) {
|
|
_prefs->telemetry_access = TELEMETRY_ACCESS_ALL;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (strcmp(&config[17], "acl") == 0) {
|
|
_prefs->telemetry_access = TELEMETRY_ACCESS_ACL;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "ERROR: telemetry.access must be all or acl");
|
|
}
|
|
} else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
|
|
uint32_t hours = 0;
|
|
if (!parseUint32Strict(&config[22], hours)
|
|
|| (hours != 0 && (hours < 3 || hours > 168))) {
|
|
strcpy(reply, "Error: interval must be 0 (off) or 3-168 hours");
|
|
} else {
|
|
const uint8_t previous = _prefs->flood_advert_interval;
|
|
_prefs->flood_advert_interval = (uint8_t)(hours);
|
|
if (!trySavePrefs()) {
|
|
_prefs->flood_advert_interval = previous;
|
|
strcpy(reply, "Error: interval not saved; unchanged");
|
|
return;
|
|
}
|
|
_callbacks->updateFloodAdvertTimer();
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else if (memcmp(config, "advert.interval ", 16) == 0) {
|
|
uint32_t mins = 0;
|
|
if (!parseUint32Strict(&config[16], mins)
|
|
|| (mins != 0 && (mins < MIN_LOCAL_ADVERT_INTERVAL || mins > 240))) {
|
|
sprintf(reply, "Error: interval must be 0 (off) or %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL);
|
|
} else {
|
|
const uint8_t previous = _prefs->advert_interval;
|
|
_prefs->advert_interval = (uint8_t)(mins / 2);
|
|
if (!trySavePrefs()) {
|
|
_prefs->advert_interval = previous;
|
|
strcpy(reply, "Error: interval not saved; unchanged");
|
|
return;
|
|
}
|
|
_callbacks->updateAdvertTimer();
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else if (memcmp(config, "guest.password ", 15) == 0) {
|
|
char previous[sizeof(_prefs->guest_password)];
|
|
memcpy(previous, _prefs->guest_password, sizeof(previous));
|
|
StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
|
|
if (!trySavePrefs()) {
|
|
memcpy(_prefs->guest_password, previous, sizeof(previous));
|
|
strcpy(reply, "Error: guest password not saved; unchanged");
|
|
return;
|
|
}
|
|
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);
|
|
if (!_callbacks->saveIdentity(new_id)) {
|
|
strcpy(reply, "Error: identity not saved; current key unchanged");
|
|
return;
|
|
}
|
|
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) {
|
|
if (strcmp(&config[7], "on") == 0 || strcmp(&config[7], "off") == 0) {
|
|
_prefs->disable_fwd = strcmp(&config[7], "off") == 0;
|
|
savePrefs();
|
|
_callbacks->onRetryConfigChanged();
|
|
strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
|
|
} else {
|
|
strcpy(reply, "Error, must be on or off");
|
|
}
|
|
} else if (memcmp(config, "radio.rxgain ", 13) == 0) {
|
|
bool enabled = memcmp(&config[13], "on", 2) == 0;
|
|
if (_callbacks->setRxBoostedGain(enabled)) {
|
|
_prefs->rx_boosted_gain = enabled;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: unsupported");
|
|
}
|
|
} else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) {
|
|
if (!_board->canControlLoRaFemLna()) {
|
|
strcpy(reply, "Error: unsupported");
|
|
} else if (memcmp(&config[17], "on", 2) == 0) {
|
|
bool changed = !_board->isLoRaFemLnaEnabled();
|
|
if (_board->setLoRaFemLnaEnabled(true)) {
|
|
if (changed) _callbacks->recalibrateNoiseFloor();
|
|
_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) {
|
|
bool changed = _board->isLoRaFemLnaEnabled();
|
|
if (_board->setLoRaFemLnaEnabled(false)) {
|
|
if (changed) _callbacks->recalibrateNoiseFloor();
|
|
_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, "radio.fem.txgain ", 17) == 0) {
|
|
if (!_board->canControlLoRaFemPaGain()) {
|
|
strcpy(reply, "Error: unsupported");
|
|
} else if (memcmp(&config[17], "on", 2) == 0) {
|
|
if (_board->setLoRaFemPaGainEnabled(true)) {
|
|
_prefs->radio_fem_txgain = 1;
|
|
savePrefs();
|
|
strcpy(reply, "OK - LoRa FEM TX gain on");
|
|
} else {
|
|
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
|
|
}
|
|
} else if (memcmp(&config[17], "off", 3) == 0) {
|
|
if (_board->setLoRaFemPaGainEnabled(false)) {
|
|
_prefs->radio_fem_txgain = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK - LoRa FEM TX gain off");
|
|
} else {
|
|
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
|
|
}
|
|
} else {
|
|
strcpy(reply, "Error: state must be on or off");
|
|
}
|
|
} else if (strncmp(config, "radio.rxps.rfrx_disabled ", 25) == 0) {
|
|
const char* value = &config[25];
|
|
bool disabled;
|
|
if (strcmp(value, "on") == 0) {
|
|
disabled = true;
|
|
} else if (strcmp(value, "off") == 0) {
|
|
disabled = false;
|
|
} else {
|
|
strcpy(reply, "Error: state must be on or off");
|
|
return;
|
|
}
|
|
|
|
if (!_callbacks->setRxPowerSavingRfRxDisabled(disabled)) {
|
|
strcpy(reply, "Error: unsupported");
|
|
} else {
|
|
sprintf(reply, "OK - radio.rxps.rfrx_disabled %s", disabled ? "on" : "off");
|
|
}
|
|
} else if (memcmp(config, "radio.rxps ", 11) == 0) { // RX PowerSaving
|
|
const char* value = &config[11];
|
|
uint8_t enable = _prefs->rx_powersaving_enabled;
|
|
uint32_t rx_us = _prefs->rx_ps_rx_us;
|
|
uint32_t sleep_us = _prefs->rx_ps_sleep_us;
|
|
uint8_t level = 0;
|
|
uint8_t preamble = rxPowerSavingPreambleForParams(_prefs->sf, _prefs->bw);
|
|
bool level_requested = false;
|
|
bool preamble_overridden = false;
|
|
bool manual_timings_requested = false;
|
|
|
|
ensureRxPowerSavingDefaults(&_prefs->rx_ps_rx_us, &_prefs->rx_ps_sleep_us);
|
|
rx_us = _prefs->rx_ps_rx_us;
|
|
sleep_us = _prefs->rx_ps_sleep_us;
|
|
|
|
mesh::cli::RxPowerSavingArguments parsed = {};
|
|
if (!mesh::cli::parseRxPowerSavingArgumentsStrict(value, parsed)) {
|
|
strcpy(reply, "ERROR: use off|on|conservative|balanced|max|overdrive|riskyWorkingMax|level <1-10>|<rx_us> <sleep_us>");
|
|
return;
|
|
}
|
|
using mesh::cli::RxPowerSavingArgumentMode;
|
|
if (parsed.mode == RxPowerSavingArgumentMode::Off) {
|
|
enable = 0;
|
|
} else if (parsed.mode == RxPowerSavingArgumentMode::Manual) {
|
|
rx_us = parsed.rx_us;
|
|
sleep_us = parsed.sleep_us;
|
|
enable = 1;
|
|
manual_timings_requested = true;
|
|
} else {
|
|
uint32_t requested_level = parsed.level;
|
|
uint32_t requested_preamble = parsed.preamble;
|
|
if (parsed.mode == RxPowerSavingArgumentMode::Conservative
|
|
|| parsed.mode == RxPowerSavingArgumentMode::Balanced) {
|
|
requested_level = parsed.mode == RxPowerSavingArgumentMode::Conservative
|
|
? RX_POWERSAVING_CONSERVATIVE_LEVEL : RX_POWERSAVING_BALANCED_LEVEL;
|
|
requested_preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
|
|
}
|
|
if (requested_level < 1 || requested_level > 10
|
|
|| (requested_preamble != 0 && requested_preamble != 16 && requested_preamble != 32)) {
|
|
strcpy(reply, "ERROR: level range is 1-10; preamble is 16 or 32");
|
|
return;
|
|
}
|
|
level = requested_level;
|
|
if (requested_preamble != 0) preamble = requested_preamble;
|
|
preamble_overridden = requested_preamble != 0;
|
|
level_requested = true;
|
|
enable = 1;
|
|
}
|
|
|
|
if (level_requested && preamble_overridden
|
|
&& preamble != 16 && preamble != 32) {
|
|
strcpy(reply, "ERROR: level range is 1-10; preamble is 16 or 32");
|
|
return;
|
|
}
|
|
|
|
if (level_requested && !calculateRxPowerSavingLevel(level, _prefs->sf, _prefs->bw, preamble, &rx_us, &sleep_us)) {
|
|
strcpy(reply, "ERROR: level range is 1-10; preamble is 16 or 32");
|
|
return;
|
|
}
|
|
|
|
if (manual_timings_requested
|
|
&& (rx_us < RX_POWERSAVING_MIN_MANUAL_PERIOD_US
|
|
|| sleep_us < RX_POWERSAVING_MIN_MANUAL_PERIOD_US)) {
|
|
sprintf(reply, "ERROR: range is %lu-%lu us",
|
|
(unsigned long)RX_POWERSAVING_MIN_MANUAL_PERIOD_US,
|
|
(unsigned long)RX_POWERSAVING_MAX_PERIOD_US);
|
|
return;
|
|
}
|
|
|
|
if (!isValidRxPowerSavingPeriod(rx_us) || !isValidRxPowerSavingPeriod(sleep_us)) {
|
|
sprintf(reply, "ERROR: range is %lu-%lu us",
|
|
(unsigned long)RX_POWERSAVING_MIN_MANUAL_PERIOD_US,
|
|
(unsigned long)RX_POWERSAVING_MAX_PERIOD_US);
|
|
return;
|
|
}
|
|
|
|
if (!_callbacks->setRxPowerSaving(enable, rx_us, sleep_us)) {
|
|
strcpy(reply, "ERROR: RX powersaving unsupported");
|
|
return;
|
|
}
|
|
|
|
_prefs->rx_powersaving_enabled = enable;
|
|
_prefs->rx_ps_rx_us = rx_us;
|
|
_prefs->rx_ps_sleep_us = sleep_us;
|
|
if (level_requested) {
|
|
// Remember the intent so the timings can auto-retune when SF/BW change.
|
|
_prefs->rx_ps_level = level;
|
|
_prefs->rx_ps_preamble = preamble_overridden ? preamble : 0; // 0 = auto (derive from SF)
|
|
} else if (parsed.mode != RxPowerSavingArgumentMode::Off) {
|
|
// manual <rx_us> <sleep_us> timings are fixed, not level-derived
|
|
// (the named profiles set level_requested and are handled above)
|
|
_prefs->rx_ps_level = 0;
|
|
_prefs->rx_ps_preamble = 0;
|
|
}
|
|
savePrefs();
|
|
if (level_requested) {
|
|
sprintf(reply, "OK - level %lu,%s,%lu,%lu,preamble=%lu",
|
|
(unsigned long)level,
|
|
enable ? "on" : "off",
|
|
(unsigned long)rx_us,
|
|
(unsigned long)sleep_us,
|
|
(unsigned long)preamble);
|
|
appendRxPowerSavingAdjustmentNote(
|
|
reply, _prefs, _prefs->sf, _prefs->bw);
|
|
} else {
|
|
sprintf(reply, "OK - %s,%lu,%lu", enable ? "on" : "off",
|
|
(unsigned long)rx_us, (unsigned long)sleep_us);
|
|
}
|
|
} else if (strcmp(config, "powersaving on") == 0
|
|
|| strcmp(config, "powersaving off") == 0) {
|
|
const bool enabled = strcmp(&config[12], "on") == 0;
|
|
_prefs->powersaving_enabled = enabled ? 1 : 0;
|
|
_sensors->setPowerSavingEnabled(enabled);
|
|
savePrefs();
|
|
sprintf(reply, "OK - powersaving %s", enabled ? "on" : "off");
|
|
} else if (memcmp(config, "radio ", 6) == 0) {
|
|
float freq = 0.0f;
|
|
float bw = 0.0f;
|
|
uint8_t sf = 0;
|
|
uint8_t cr = 0;
|
|
uint16_t preamble = 0;
|
|
char legacy[96];
|
|
if (mesh::RadioProfileCLI::parseSuffix(&config[6], 4, legacy, sizeof(legacy), preamble)
|
|
&& mesh::cli::parseRadioTupleStrict(legacy, freq, bw, sf, cr)
|
|
&& freq >= 150.0f && freq <= 2500.0f
|
|
&& sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8
|
|
&& isValidLoRaBandwidth(bw)
|
|
&& _radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
|
|
const int8_t previous_power = _prefs->tx_power_dbm;
|
|
if (!savePrimaryRadioParams(freq, bw, sf, cr, preamble)) {
|
|
strcpy(reply, "Error: radio settings could not be saved"); return;
|
|
}
|
|
if (_prefs->tx_power_dbm != previous_power) {
|
|
sprintf(reply, "OK - reboot to apply; TX power limited to %d dBm",
|
|
(int)_prefs->tx_power_dbm);
|
|
} else {
|
|
strcpy(reply, "OK - reboot to apply");
|
|
}
|
|
appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw);
|
|
_radio_profiles.appendPrimaryChirpWarning(reply, 160, sf, bw, preamble);
|
|
} else {
|
|
strcpy(reply, "Error, invalid radio params");
|
|
}
|
|
} else if (memcmp(config, "radioat ", 8) == 0) {
|
|
float freq, bw;
|
|
uint8_t sf, cr;
|
|
uint32_t start_time, end_time;
|
|
uint16_t preamble = 0;
|
|
char legacy[120];
|
|
if (!mesh::RadioProfileCLI::parseSuffix(&config[8], 5, legacy, sizeof(legacy), preamble)
|
|
|| !parseScheduledRadioArgs(legacy, false, freq, bw, sf, cr, start_time, end_time, _rtc->getCurrentTime())) {
|
|
strcpy(reply, "Error, use: set radioat f,bw,sf,cr,start (UTC or +minutes)");
|
|
} else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)
|
|
|| !_radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
|
|
strcpy(reply, "Error, invalid radio params");
|
|
} else {
|
|
_callbacks->addScheduledRadioParams(false, freq, bw, sf, cr, start_time, end_time, reply, preamble);
|
|
if (strncmp(reply, "OK", 2) == 0) {
|
|
_radio_profiles.appendPrimaryChirpWarning(reply, 160, sf, bw, preamble);
|
|
appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw);
|
|
}
|
|
}
|
|
} else if (memcmp(config, "tempradioat ", 12) == 0) {
|
|
float freq, bw;
|
|
uint8_t sf, cr;
|
|
uint32_t start_time, end_time;
|
|
uint16_t preamble = 0;
|
|
char legacy[120];
|
|
if (!mesh::RadioProfileCLI::parseSuffix(&config[12], 6, legacy, sizeof(legacy), preamble)
|
|
|| !parseScheduledRadioArgs(legacy, true, freq, bw, sf, cr, start_time, end_time, _rtc->getCurrentTime())) {
|
|
strcpy(reply, "Error, use: set tempradioat f,bw,sf,cr,start,end (UTC or +minutes)");
|
|
} else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)
|
|
|| !_radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
|
|
strcpy(reply, "Error, invalid radio params");
|
|
} else {
|
|
_callbacks->addScheduledRadioParams(true, freq, bw, sf, cr, start_time, end_time, reply, preamble);
|
|
if (strncmp(reply, "OK", 2) == 0) {
|
|
_radio_profiles.appendPrimaryChirpWarning(reply, 160, sf, bw, preamble);
|
|
appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw);
|
|
}
|
|
}
|
|
} else if (memcmp(config, "lat ", 4) == 0) {
|
|
float latitude = 0.0f;
|
|
if (mesh::cli::parseDecimalStrict(&config[4], latitude)) {
|
|
_prefs->node_lat = latitude;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, invalid latitude");
|
|
}
|
|
} else if (memcmp(config, "lon ", 4) == 0) {
|
|
float longitude = 0.0f;
|
|
if (mesh::cli::parseDecimalStrict(&config[4], longitude)) {
|
|
_prefs->node_lon = longitude;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, invalid longitude");
|
|
}
|
|
} else if (memcmp(config, "rxdelay ", 8) == 0) {
|
|
float db = 0.0f;
|
|
if (mesh::cli::parseDecimalStrict(&config[8], db)
|
|
&& db >= 0.0f && 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 = 0.0f;
|
|
if (mesh::cli::parseDecimalStrict(&config[8], f)
|
|
&& f >= 0.0f && 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) {
|
|
uint32_t m;
|
|
if (mesh::cli::parseUnsignedIntegerStrict(&config[19], m) && 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) {
|
|
uint32_t m;
|
|
if (mesh::cli::parseUnsignedIntegerStrict(&config[17], m) && 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) {
|
|
uint32_t m;
|
|
if (mesh::cli::parseUnsignedIntegerStrict(&config[10], m) && 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 = 0.0f;
|
|
if (mesh::cli::parseDecimalStrict(&config[15], f)
|
|
&& f >= 0.0f && 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, "retry.preset ", 13) == 0) {
|
|
uint8_t preset;
|
|
if (parseRetryPreset(&config[13], preset)) {
|
|
applyDirectRetryPreset(_prefs, preset);
|
|
savePrefs();
|
|
sprintf(reply, "OK - %s", retryPresetName(_prefs->retry_preset));
|
|
} else {
|
|
strcpy(reply, "Error, must be infra, rooftop, or mobile");
|
|
}
|
|
} else if (memcmp(config, "direct.retry ", 13) == 0) {
|
|
if (strcmp(&config[13], "on") == 0) {
|
|
_prefs->direct_retry_enabled = 1;
|
|
savePrefs();
|
|
_callbacks->onRetryConfigChanged();
|
|
strcpy(reply, "OK");
|
|
} else if (strcmp(&config[13], "off") == 0) {
|
|
_prefs->direct_retry_enabled = 0;
|
|
savePrefs();
|
|
_callbacks->onRetryConfigChanged();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be on or off");
|
|
}
|
|
} else if (memcmp(config, "direct.retry.heard ", 19) == 0) {
|
|
if (strcmp(&config[19], "on") == 0) {
|
|
_prefs->direct_retry_recent_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (strcmp(&config[19], "off") == 0) {
|
|
_prefs->direct_retry_recent_enabled = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be on or off");
|
|
}
|
|
} else if (memcmp(config, "direct.retry.margin ", 20) == 0) {
|
|
if (!looksNumeric(&config[20])) {
|
|
strcpy(reply, "Error, must be 0-40 dB");
|
|
} else {
|
|
int16_t margin_x4 = parseSnrDbX4(&config[20]);
|
|
if (margin_x4 >= 0 && margin_x4 <= 160) {
|
|
_prefs->direct_retry_snr_margin_x4 = (uint16_t)margin_x4;
|
|
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 0-40 dB");
|
|
}
|
|
}
|
|
} else if (memcmp(config, "direct.retry.count ", 19) == 0) {
|
|
int attempts = looksUnsignedInteger(&config[19]) ? _atoi(&config[19]) : -1;
|
|
if (attempts >= 1 && attempts <= 15) {
|
|
_prefs->direct_retry_attempts = (uint8_t)attempts;
|
|
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 1-15");
|
|
}
|
|
} else if (memcmp(config, "direct.retry.base ", 18) == 0) {
|
|
int base_ms = looksUnsignedInteger(&config[18]) ? _atoi(&config[18]) : -1;
|
|
if (base_ms >= 10 && base_ms <= 5000) {
|
|
_prefs->direct_retry_base_ms = (uint16_t)base_ms;
|
|
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 10-5000 ms");
|
|
}
|
|
} else if (memcmp(config, "direct.retry.step ", 18) == 0) {
|
|
int step_ms = looksUnsignedInteger(&config[18]) ? _atoi(&config[18]) : -1;
|
|
if (step_ms >= 0 && step_ms <= 5000) {
|
|
_prefs->direct_retry_step_ms = (uint16_t)step_ms;
|
|
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 0-5000 ms");
|
|
}
|
|
} else if (memcmp(config, "flood.channel.data.hops ", 24) == 0) {
|
|
uint8_t max_hops;
|
|
if (parseFloodChannelHops(&config[24], max_hops)) {
|
|
_prefs->flood_channel_data_max_hops = max_hops;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be all or 1-7");
|
|
}
|
|
} else if (memcmp(config, "flood.channel.data ", 19) == 0) {
|
|
if (strcmp(&config[19], "on") == 0) {
|
|
_prefs->flood_channel_data_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (strcmp(&config[19], "off") == 0) {
|
|
_prefs->flood_channel_data_enabled = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be on or off");
|
|
}
|
|
} else if (memcmp(config, "flood.retry.count ", 18) == 0) {
|
|
int attempts = looksUnsignedInteger(&config[18]) ? _atoi(&config[18]) : -1;
|
|
if (attempts >= 0 && attempts <= 15) {
|
|
_prefs->flood_retry_attempts = (uint8_t)attempts;
|
|
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
|
savePrefs();
|
|
_callbacks->onRetryConfigChanged();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 0-15");
|
|
}
|
|
} else if (memcmp(config, "flood.retry.path ", 17) == 0) {
|
|
uint8_t path_gate;
|
|
if (parseFloodRetryPathGate(&config[17], path_gate)) {
|
|
_prefs->flood_retry_max_path = path_gate;
|
|
if (path_gate == 0) {
|
|
_prefs->flood_retry_group_max_path = FLOOD_RETRY_PATH_GATE_DISABLED;
|
|
}
|
|
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 0-63 or off");
|
|
}
|
|
} else if (memcmp(config, "flood.retry.group.path ", 23) == 0) {
|
|
uint8_t path_gate;
|
|
if (parseFloodRetryPathGate(&config[23], path_gate)) {
|
|
_prefs->flood_retry_group_max_path = _prefs->flood_retry_max_path == 0
|
|
? FLOOD_RETRY_PATH_GATE_DISABLED
|
|
: path_gate;
|
|
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 0-63 or off");
|
|
}
|
|
} else if (memcmp(config, "flood.retry.prefixes ", 21) == 0) {
|
|
if (parseFloodRetryPrefixList(_prefs->flood_retry_prefixes, FLOOD_RETRY_PREFIX_SLOTS, &config[21])) {
|
|
savePrefs();
|
|
if (!formatFloodRetryBucketCollisionWarning(reply, _prefs, "OK - ")) {
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else {
|
|
sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes",
|
|
(unsigned int)FLOOD_RETRY_PREFIX_SLOTS);
|
|
}
|
|
} else if (memcmp(config, "flood.retry.ignore ", 19) == 0) {
|
|
if (parseFloodRetryPrefixList(_prefs->flood_retry_ignore_prefixes,
|
|
FLOOD_RETRY_IGNORE_PREFIXES, &config[19])) {
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes",
|
|
(unsigned int)FLOOD_RETRY_IGNORE_PREFIXES);
|
|
}
|
|
} else if (memcmp(config, "flood.retry.advert ", 19) == 0) {
|
|
if (strcmp(&config[19], "on") == 0) {
|
|
_prefs->flood_retry_advert_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (strcmp(&config[19], "off") == 0) {
|
|
_prefs->flood_retry_advert_enabled = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be on or off");
|
|
}
|
|
} else if (memcmp(config, "flood.retry.bridge ", 19) == 0) {
|
|
if (strcmp(&config[19], "on") == 0) {
|
|
_prefs->flood_retry_bridge_enabled = 1;
|
|
savePrefs();
|
|
if (!formatFloodRetryBucketCollisionWarning(reply, _prefs, "OK - bucket 7=prefixes; ")) {
|
|
strcpy(reply, "OK - flood.retry.prefixes acts as bucket 7 (other); multi-try bridge routing enabled");
|
|
}
|
|
} else if (strcmp(&config[19], "off") == 0) {
|
|
_prefs->flood_retry_bridge_enabled = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be on or off");
|
|
}
|
|
} else if (memcmp(config, "flood.retry.bucket ", 19) == 0) {
|
|
const char* params = &config[19];
|
|
uint8_t bucket = atoi(params);
|
|
const char* list = strchr(params, ' ');
|
|
if (bucket < 1 || bucket > FLOOD_RETRY_BRIDGE_BUCKETS || list == NULL || *(list + 1) == 0) {
|
|
sprintf(reply, "Error, usage: set flood.retry.bucket <1-%d> <prefixes|none>", FLOOD_RETRY_BRIDGE_BUCKETS);
|
|
} else if (parseFloodRetryPrefixList(_prefs->flood_retry_bridge_buckets[bucket - 1],
|
|
FLOOD_RETRY_BUCKET_PREFIXES, list + 1)) {
|
|
savePrefs();
|
|
if (!formatFloodRetryBucketCollisionWarning(reply, _prefs, "OK - ")) {
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else {
|
|
sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes",
|
|
(unsigned int)FLOOD_RETRY_BUCKET_PREFIXES);
|
|
}
|
|
} else if (memcmp(config, "direct.retry.cr ", 16) == 0) {
|
|
if (strcmp(&config[16], "off") == 0) {
|
|
_prefs->direct_retry_cr_enabled = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (!_callbacks->supportsAdvancedRetryConfig()) {
|
|
if (strcmp(&config[16], "on") == 0) {
|
|
_prefs->direct_retry_cr_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, use on or off on this role");
|
|
}
|
|
} else {
|
|
strcpy(tmp, &config[16]);
|
|
const char *parts[4];
|
|
int num = mesh::Utils::parseTextParts(tmp, parts, 4, ',');
|
|
if (num == 4 && looksNumeric(parts[0]) && looksNumeric(parts[1]) && looksNumeric(parts[2]) && looksNumeric(parts[3])) {
|
|
int16_t cr4 = parseSnrDbX4(parts[0]);
|
|
int16_t cr5 = parseSnrDbX4(parts[1]);
|
|
int16_t cr7 = parseSnrDbX4(parts[2]);
|
|
int16_t cr8 = parseSnrDbX4(parts[3]);
|
|
if (cr4 >= -128 && cr4 <= 127 && cr5 >= -128 && cr5 <= 127 && cr7 >= -128 && cr7 <= 127 && cr8 >= -128 && cr8 <= 127) {
|
|
_prefs->direct_retry_cr4_snr_x4 = (int8_t)cr4;
|
|
_prefs->direct_retry_cr5_snr_x4 = (int8_t)cr5;
|
|
_prefs->direct_retry_cr7_snr_x4 = (int8_t)cr7;
|
|
_prefs->direct_retry_cr8_snr_x4 = (int8_t)cr8;
|
|
_prefs->direct_retry_cr_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, SNR must fit -32.00..31.75 dB");
|
|
}
|
|
} else {
|
|
strcpy(reply, "Error, use CR4,CR5,CR7,CR8 SNRs or off");
|
|
}
|
|
}
|
|
} else if (memcmp(config, "recent.repeater ", 16) == 0) {
|
|
strcpy(tmp, &config[16]);
|
|
const char *parts[2];
|
|
int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' ');
|
|
uint8_t prefix[MAX_HASH_SIZE];
|
|
uint8_t prefix_len = 0;
|
|
int16_t snr_x4 = 12; // default to +3.0 dB when omitted or invalid
|
|
if (num < 1 || !parseHashPrefix(parts[0], prefix, prefix_len)) {
|
|
strcpy(reply, "Error, use: set recent.repeater <hex> [snr_db]");
|
|
} else if (num > 1 && !looksNumeric(parts[1])) {
|
|
strcpy(reply, "Error, SNR must be numeric");
|
|
} else {
|
|
if (num > 1) {
|
|
snr_x4 = parseSnrDbX4(parts[1]);
|
|
}
|
|
if (snr_x4 < -128 || snr_x4 > 127) {
|
|
strcpy(reply, "Error, SNR must fit -32.00..31.75 dB");
|
|
} else if (_callbacks->setRecentRepeater(prefix, prefix_len, (int8_t)snr_x4)) {
|
|
char prefix_hex[RECENT_REPEATER_PREFIX_MAX_BYTES * 2 + 1];
|
|
char snr[12];
|
|
mesh::Utils::toHex(prefix_hex, prefix, prefix_len);
|
|
prefix_hex[prefix_len * 2] = 0;
|
|
formatSnrDbX4Short(snr, sizeof(snr), snr_x4);
|
|
sprintf(reply, "OK - set %s at %s SNR", prefix_hex, snr);
|
|
} else {
|
|
strcpy(reply, "Error, table rejected prefix");
|
|
}
|
|
}
|
|
} 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, "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) {
|
|
int32_t power = 0;
|
|
const int8_t minimum = mesh::minLoRaTxPowerForFrequency(_prefs->freq);
|
|
const int8_t maximum = mesh::maxLoRaTxPowerForFrequency(_prefs->freq);
|
|
if (!mesh::cli::parseIntegerStrict(&config[3], power)) {
|
|
strcpy(reply, "Error: invalid TX power");
|
|
} else if (!mesh::isLoRaTxPowerValid(power, _prefs->freq)) {
|
|
sprintf(reply, "Error: TX power range is %d to %d dBm",
|
|
(int)minimum, (int)maximum);
|
|
} else if (!_callbacks->setTxPower(static_cast<int8_t>(power))) {
|
|
strcpy(reply, "Error: radio busy or TX power rejected");
|
|
} else {
|
|
_prefs->tx_power_dbm = static_cast<int8_t>(power);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else if (memcmp(config, "freq ", 5) == 0) {
|
|
float freq = 0.0f;
|
|
if (mesh::cli::parseDecimalStrict(&config[5], freq)
|
|
&& freq >= 150.0f && freq <= 2500.0f) {
|
|
const int8_t previous_power = _prefs->tx_power_dbm;
|
|
_prefs->freq = freq;
|
|
_prefs->tx_power_dbm = mesh::clampLoRaTxPower(
|
|
_prefs->tx_power_dbm, _prefs->freq);
|
|
savePrefs();
|
|
if (_prefs->tx_power_dbm != previous_power) {
|
|
sprintf(reply, "OK - reboot to apply; TX power limited to %d dBm",
|
|
(int)_prefs->tx_power_dbm);
|
|
} else {
|
|
strcpy(reply, "OK - reboot to apply");
|
|
}
|
|
} else {
|
|
strcpy(reply, "Error, invalid frequency");
|
|
}
|
|
#ifdef WITH_BRIDGE
|
|
} else if (memcmp(config, "bridge.enabled ", 15) == 0) {
|
|
bool enable = false;
|
|
if (!parseOnOffStrict(&config[15], enable)) {
|
|
strcpy(reply, "Error: usage set bridge.enabled on|off");
|
|
} else {
|
|
#if defined(WITH_ESPNOW_BRIDGE) \
|
|
&& (defined(WITH_MQTT_BRIDGE) \
|
|
|| (defined(ESPNOW_BRIDGE_MERGED) && !defined(WITH_RS232_BRIDGE)))
|
|
// bridge.enabled retains its ESP-NOW meaning; mqtt.enabled is the
|
|
// independent MQTT switch when the profile also has that transport.
|
|
_prefs->espnow_bridge_enabled = enable;
|
|
#if !defined(WITH_MQTT_BRIDGE)
|
|
_prefs->bridge_enabled = enable;
|
|
#endif
|
|
const bool applied = _callbacks->setEspNowBridgeState(enable);
|
|
savePrefs();
|
|
strcpy(reply, applied ? "OK"
|
|
: "Error: ESP-NOW runtime change failed; setting saved");
|
|
#else
|
|
#ifdef WITH_RS232_BRIDGE
|
|
if (enable
|
|
&& _sensors->gpsSerialTransportMayConflict(_prefs->bridge_uart)
|
|
&& (!_sensors->gpsUsesSerialUart(_prefs->bridge_uart)
|
|
|| !_sensors->gpsSerialTransportCanYield(_prefs->bridge_uart))) {
|
|
strcpy(reply, "Error: UART may be driven by GPS; use UART 2 or a no-GPS build");
|
|
return;
|
|
}
|
|
const uint8_t previous_enabled = _prefs->bridge_enabled;
|
|
const bool previous_running = _callbacks->isBridgeRunning();
|
|
_prefs->bridge_enabled = enable;
|
|
const bool applied = _callbacks->setBridgeState(enable);
|
|
if (applied) {
|
|
savePrefs();
|
|
#ifdef WITH_RS232_BRIDGE
|
|
if (enable && _sensors->gpsUsesSerialUart(_prefs->bridge_uart)) {
|
|
strcpy(reply, "OK - UART GPS paused");
|
|
} else
|
|
#endif
|
|
{
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else {
|
|
_prefs->bridge_enabled = previous_enabled;
|
|
bool restored = true;
|
|
if (_callbacks->isBridgeRunning() != previous_running
|
|
|| !previous_running) {
|
|
// The false/false case still calls disable to delete a failed,
|
|
// non-running heap-backed bridge instance.
|
|
restored = _callbacks->setBridgeState(previous_running);
|
|
}
|
|
strcpy(reply, restored
|
|
? "Error: bridge state change failed; setting unchanged"
|
|
: "Error: bridge state change failed; previous runtime state could not be restored");
|
|
}
|
|
#else
|
|
// MQTT and ESP-NOW preserve configured intent across missing WiFi
|
|
// credentials or transient initialization failure. Their runtime can be
|
|
// retried later, so do not apply the stricter RS-232 rollback policy.
|
|
_prefs->bridge_enabled = enable;
|
|
const bool applied = _callbacks->setBridgeState(enable);
|
|
savePrefs();
|
|
strcpy(reply, applied ? "OK"
|
|
: "Error: bridge runtime change failed; setting saved");
|
|
#endif
|
|
#endif
|
|
}
|
|
} else if (memcmp(config, "bridge.delay ", 13) == 0) {
|
|
int32_t bridge_delay = 0;
|
|
if (mesh::cli::parseIntegerStrict(&config[13], bridge_delay)
|
|
&& bridge_delay >= 0 && bridge_delay <= 10000) {
|
|
_prefs->bridge_delay = (uint16_t)bridge_delay;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: delay must be between 0-10000 ms");
|
|
}
|
|
} else if (memcmp(config, "bridge.source ", 14) == 0) {
|
|
const char* source = &config[14];
|
|
if (strcmp(source, "rx") != 0 && strcmp(source, "tx") != 0) {
|
|
strcpy(reply, "Error: usage set bridge.source rx|tx");
|
|
return;
|
|
}
|
|
_prefs->bridge_pkt_src = strcmp(source, "rx") == 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;
|
|
}
|
|
savePrefs(PrefsSaveRouting::Scope::Both);
|
|
#else
|
|
savePrefs();
|
|
#endif
|
|
strcpy(reply, "OK");
|
|
#endif
|
|
#ifdef WITH_RS232_BRIDGE
|
|
} else if (memcmp(config, "bridge.baud ", 12) == 0) {
|
|
uint32_t baud = 0;
|
|
if (mesh::cli::parseUnsignedIntegerStrict(&config[12], baud)
|
|
&& baud >= 9600 && baud <= BRIDGE_MAX_BAUD) {
|
|
const uint32_t previous_baud = _prefs->bridge_baud;
|
|
const bool previous_running = _callbacks->isBridgeRunning();
|
|
_prefs->bridge_baud = baud;
|
|
const bool applied =
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
|
|
#else
|
|
!_prefs->bridge_enabled || _callbacks->restartBridge();
|
|
#endif
|
|
if (applied) {
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
_prefs->bridge_baud = previous_baud;
|
|
const bool restored = previous_running
|
|
? _callbacks->restartBridge()
|
|
: _callbacks->setBridgeState(false);
|
|
strcpy(reply, restored
|
|
? "Error: bridge failed to restart; baud unchanged"
|
|
: "Error: bridge failed to restart; previous baud could not be restored");
|
|
}
|
|
} else {
|
|
sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD);
|
|
}
|
|
} else if (memcmp(config, "bridge.uart ", 12) == 0) {
|
|
int32_t uart = 0;
|
|
const bool parsed = mesh::cli::parseIntegerStrict(&config[12], uart);
|
|
const bool supported = uart == WITH_RS232_BRIDGE_UART
|
|
#ifdef WITH_RS232_BRIDGE_ALT
|
|
|| uart == WITH_RS232_BRIDGE_ALT_UART
|
|
#endif
|
|
;
|
|
if (!parsed || !supported) {
|
|
#ifdef WITH_RS232_BRIDGE_ALT
|
|
sprintf(reply, "Error: UART must be %d or %d",
|
|
WITH_RS232_BRIDGE_UART, WITH_RS232_BRIDGE_ALT_UART);
|
|
#else
|
|
sprintf(reply, "Error: UART is fixed at %d", WITH_RS232_BRIDGE_UART);
|
|
#endif
|
|
}
|
|
else {
|
|
if (_prefs->bridge_enabled
|
|
&& _sensors->gpsSerialTransportMayConflict((uint8_t)uart)
|
|
&& (!_sensors->gpsUsesSerialUart((uint8_t)uart)
|
|
|| !_sensors->gpsSerialTransportCanYield((uint8_t)uart))) {
|
|
strcpy(reply, "Error: UART may be driven by GPS; keep UART 2 or use a no-GPS build");
|
|
return;
|
|
}
|
|
const uint8_t previous_uart = _prefs->bridge_uart;
|
|
const bool previous_running = _callbacks->isBridgeRunning();
|
|
_prefs->bridge_uart = (uint8_t)uart;
|
|
const bool applied =
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
|
|
#else
|
|
!_prefs->bridge_enabled || _callbacks->restartBridge();
|
|
#endif
|
|
if (!applied) {
|
|
_prefs->bridge_uart = previous_uart;
|
|
const bool restored = previous_running
|
|
? _callbacks->restartBridge()
|
|
: _callbacks->setBridgeState(false);
|
|
strcpy(reply, restored
|
|
? "Error: bridge failed to restart; UART unchanged"
|
|
: "Error: bridge failed to restart; previous UART could not be restored");
|
|
} else if (_prefs->bridge_enabled
|
|
&& _sensors->gpsUsesSerialUart(_prefs->bridge_uart)) {
|
|
savePrefs();
|
|
strcpy(reply, "OK - UART GPS paused");
|
|
} else {
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
}
|
|
#endif
|
|
#ifdef WITH_ESPNOW_BRIDGE
|
|
} else if (memcmp(config, "bridge.channel ", 15) == 0) {
|
|
uint8_t channel = 0;
|
|
const uint8_t max_channel =
|
|
mesh::bridge::espNowMaxChannel(_prefs->bridge_format);
|
|
if (mesh::bridge::parseEspNowBridgeChannel(
|
|
&config[15], _prefs->bridge_format, channel)) {
|
|
_prefs->bridge_channel = channel;
|
|
const bool applied =
|
|
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
|
|
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
|
|
#else
|
|
!_prefs->bridge_enabled || _callbacks->restartBridge();
|
|
#endif
|
|
savePrefs();
|
|
strcpy(reply, applied ? "OK"
|
|
: "Error: setting saved; bridge failed to restart");
|
|
} else {
|
|
sprintf(reply, "Error: channel must be an integer between 1-%u for %s format",
|
|
(unsigned)max_channel,
|
|
mesh::bridge::espNowFormatName(_prefs->bridge_format));
|
|
}
|
|
} else if (memcmp(config, "bridge.secret ", 14) == 0) {
|
|
const char* secret = &config[14];
|
|
if (secret[0] == 0 || strlen(secret) >= sizeof(_prefs->bridge_secret)) {
|
|
sprintf(reply, "Error: secret must be 1-%u characters", (unsigned)(sizeof(_prefs->bridge_secret) - 1));
|
|
} else {
|
|
StrHelper::strncpy(_prefs->bridge_secret, secret, sizeof(_prefs->bridge_secret));
|
|
const bool applied =
|
|
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
|
|
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
|
|
#else
|
|
!_prefs->bridge_enabled || _callbacks->restartBridge();
|
|
#endif
|
|
savePrefs();
|
|
strcpy(reply, applied ? "OK"
|
|
: "Error: setting saved; bridge failed to restart");
|
|
}
|
|
} else if (memcmp(config, "bridge.format ", 14) == 0) {
|
|
uint8_t format = mesh::bridge::ESPNOW_FORMAT_WRAPPED;
|
|
if (!mesh::bridge::parseEspNowFormat(&config[14], format)) {
|
|
strcpy(reply, "Error: format must be wrapped or raw");
|
|
} else if (!mesh::bridge::isValidEspNowBridgeChannel(
|
|
_prefs->bridge_channel, format)) {
|
|
strcpy(reply, "Error: set bridge.channel to 1-13 before changing format");
|
|
} else {
|
|
_prefs->bridge_format = format;
|
|
const bool applied =
|
|
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
|
|
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
|
|
#else
|
|
!_prefs->bridge_enabled || _callbacks->restartBridge();
|
|
#endif
|
|
savePrefs();
|
|
if (!applied) {
|
|
strcpy(reply, "Error: setting saved; bridge failed to restart");
|
|
} else {
|
|
strcpy(reply, format == mesh::bridge::ESPNOW_FORMAT_RAW
|
|
? "OK - raw MeshCore frames; bridge.secret is ignored"
|
|
: "OK - wrapped bridge frames");
|
|
}
|
|
}
|
|
#endif
|
|
} else if (memcmp(config, "adc.multiplier ", 15) == 0) {
|
|
float multiplier = 0.0f;
|
|
if (!mesh::cli::parseDecimalStrict(&config[15], multiplier)) {
|
|
strcpy(reply, "Error: invalid multiplier");
|
|
} else if (_board->setAdcMultiplier(multiplier)) {
|
|
_prefs->adc_multiplier = 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 {
|
|
strcpy(reply, "Error: unsupported");
|
|
};
|
|
} else if (memcmp(config, "reboot.interval ", 16) == 0) {
|
|
int hours = _atoi(&config[16]);
|
|
if (hours == 0) {
|
|
_prefs->reboot_interval = 0;
|
|
savePrefs();
|
|
strcpy(reply, "reboot.interval disabled");
|
|
} else if (hours < 1 || 255 < hours) {
|
|
strcpy(reply, "Error: interval range is 1-255 hours");
|
|
} else {
|
|
_prefs->reboot_interval = hours;
|
|
savePrefs();
|
|
sprintf(reply, "OK - reboot.interval set to %d", _prefs->reboot_interval);
|
|
}
|
|
} else if (strcmp(config, "extra.sf") == 0 || memcmp(config, "extra.sf ", 9) == 0) {
|
|
#if defined(USE_LR2021)
|
|
uint8_t sideDetSFs[mesh::lr2021::STORED_SIDE_DETECTOR_BYTES] = {};
|
|
uint8_t num = 0;
|
|
const char* value = config[8] == '\0' ? "" : &config[9];
|
|
if (strcmp(value, "none") == 0 || strcmp(value, "off") == 0) value = "";
|
|
if (!mesh::lr2021::parseSideDetectorSFList(value, sideDetSFs, num)
|
|
|| !mesh::lr2021::validateSideDetectorSFs(
|
|
sideDetSFs, num, _prefs->sf, _prefs->bw)) {
|
|
strcpy(reply, "Invalid extra SF config");
|
|
} else {
|
|
if (_callbacks->configSideDetectors(sideDetSFs, num, _prefs->bw)) {
|
|
memcpy(_prefs->extra_sf, sideDetSFs, sizeof(_prefs->extra_sf));
|
|
savePrefs();
|
|
strcpy(reply, num == 0 ? "OK - extra SFs cleared" : "OK - extra SFs set");
|
|
} else {
|
|
strcpy(reply, "Invalid extra SF config");
|
|
}
|
|
}
|
|
#else
|
|
strcpy(reply, "Error: extra.sf requires an LR2021 radio");
|
|
#endif
|
|
} else {
|
|
sprintf(reply, "unknown config: %s", config);
|
|
}
|
|
}
|
|
|
|
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
|
const char* config = &command[4];
|
|
if (strncmp(config, "repeat.trace", 12) == 0
|
|
&& (config[12] == 0 || config[12] == ' ' || config[12] == '\t')) {
|
|
if (strcmp(_callbacks->getRole(), "repeater") != 0) {
|
|
strcpy(reply, "Error: repeat.trace is only supported by repeaters");
|
|
} else if (strcmp(config, "repeat.trace") != 0) {
|
|
strcpy(reply, "Error: usage get repeat.trace");
|
|
} else {
|
|
snprintf(reply, 160, "> %s", _prefs->trace_when_repeat_off == 1 ? "on" : "off");
|
|
}
|
|
return;
|
|
}
|
|
#if defined(NRF52_POWER_MANAGEMENT)
|
|
if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) return;
|
|
#endif
|
|
if (strcmp(config, "password") == 0) {
|
|
if (sender_timestamp == 0) {
|
|
snprintf(reply, 160, "> %s", _prefs->password);
|
|
} else {
|
|
strcpy(reply, "Error: get password requires a local connection");
|
|
}
|
|
return;
|
|
}
|
|
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED) && !defined(WITH_MQTT_BRIDGE)
|
|
if (strcmp(config, "wifi.pwd") == 0) {
|
|
if (sender_timestamp != 0) {
|
|
strcpy(reply, "> ******** (local connection only)");
|
|
} else if (!_callbacks->getWiFiPassword(reply)) {
|
|
strcpy(reply, "Error: WiFi password unavailable on this build");
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
if (strcmp(config, "powersaving") == 0) {
|
|
snprintf(reply, 160, "> %s", _prefs->powersaving_enabled ? "on" : "off");
|
|
return;
|
|
}
|
|
#if ENV_INCLUDE_GPS == 1
|
|
if (strcmp(config, "gps.sync.interval") == 0) {
|
|
auto* provider = _sensors->getLocationProvider();
|
|
if (provider == nullptr) {
|
|
strcpy(reply, "Error: GPS unavailable");
|
|
} else {
|
|
const uint16_t hours = provider->getTimeSyncIntervalHours();
|
|
if (hours == 0) strcpy(reply, "> default (board GPS sync policy)");
|
|
else snprintf(reply, 160, "> %u hours", (unsigned)hours);
|
|
}
|
|
return;
|
|
}
|
|
if (strcmp(config, "gps") == 0) {
|
|
char gps_command[] = "gps";
|
|
handleCommand(sender_timestamp, gps_command, reply);
|
|
return;
|
|
}
|
|
#endif
|
|
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
|
|
if (strcmp(config, "espnow.channel") == 0) {
|
|
const uint8_t saved = mesh::wifi::loadConfiguredEspNowChannel();
|
|
const uint8_t active = mesh::wifi::activeEspNowChannel();
|
|
if (saved == active) {
|
|
snprintf(reply, 160, "> %u (saved and active)", (unsigned)saved);
|
|
} else {
|
|
snprintf(reply, 160, "> saved %u, active %u; reboot required",
|
|
(unsigned)saved, (unsigned)active);
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
#if MESH_USB_LOGGING_AVAILABLE
|
|
if (strcmp(config, "usb.logging") == 0) {
|
|
snprintf(reply, 160, "> %s",
|
|
mesh::isUsbLoggingEnabled() ? "on" : "off");
|
|
return;
|
|
}
|
|
if (strcmp(config, "usb.debug") == 0) {
|
|
snprintf(reply, 160, "> %s",
|
|
mesh::isUsbDebugEnabled() ? "on" : "off");
|
|
return;
|
|
}
|
|
#endif
|
|
#if MESH_USB_LOGGING_AVAILABLE && defined(WITH_MQTT_BRIDGE)
|
|
if (strcmp(config, "logging.output") == 0) {
|
|
const bool usb_enabled = mesh::isUsbLoggingEnabled();
|
|
const bool wifi_enabled = _prefs->bridge_enabled != 0;
|
|
const char* mode = mesh::cli::loggingOutputName(usb_enabled, wifi_enabled);
|
|
snprintf(reply, 160, "> %s", mode);
|
|
return;
|
|
}
|
|
#endif
|
|
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
|
if (isGpioConfig(config)) {
|
|
_user_gpio.handleGet(config + 4, reply, 160);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
|
if (strcmp(config, "webui") == 0) {
|
|
if (!_callbacks->getWebUIStatus(reply)) {
|
|
strcpy(reply, "Error: webui not supported on this build");
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
|
if (mesh::cli::classifyStandaloneWiFiGet(config) ==
|
|
mesh::cli::StandaloneWiFiKey::CLI) {
|
|
if (!_callbacks->getWiFiCLI(reply)) {
|
|
strcpy(reply, "Error: WiFi CLI unavailable on this build");
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
|
|
if (handleObserverGetCmd(sender_timestamp, config, reply)) return;
|
|
if (mesh::cli::handleStorageLayoutGet(config, *_board, reply, 160)) return;
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
if (handleSdCardGetCmd(config, reply)) return;
|
|
#endif
|
|
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
|
// Non-MQTT FULL repeater/room-server builds keep WiFi only for WebConfig.
|
|
// Their credentials live in the standalone WebConfig NVS namespace rather
|
|
// than MQTTPrefs, so expose the WiFi configuration through the role.
|
|
const mesh::cli::StandaloneWiFiKey wifi_key =
|
|
mesh::cli::classifyStandaloneWiFiGet(config);
|
|
bool wifi_supported = true;
|
|
switch (wifi_key) {
|
|
case mesh::cli::StandaloneWiFiKey::SSID:
|
|
wifi_supported = _callbacks->getWiFiSSID(reply);
|
|
break;
|
|
case mesh::cli::StandaloneWiFiKey::Status:
|
|
wifi_supported = _callbacks->getWiFiStatus(reply);
|
|
break;
|
|
case mesh::cli::StandaloneWiFiKey::PowerSave:
|
|
wifi_supported = _callbacks->getWiFiPowerSave(reply);
|
|
break;
|
|
case mesh::cli::StandaloneWiFiKey::CLI:
|
|
wifi_supported = _callbacks->getWiFiCLI(reply);
|
|
break;
|
|
default:
|
|
wifi_supported = false;
|
|
break;
|
|
}
|
|
if (wifi_key != mesh::cli::StandaloneWiFiKey::None) {
|
|
if (!wifi_supported) {
|
|
strcpy(reply, "Error: WiFi information unavailable on this build");
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
if (isAdvancedRetryConfig(config) && !_callbacks->supportsAdvancedRetryConfig()) {
|
|
strcpy(reply, "Error, unsupported on this role");
|
|
return;
|
|
}
|
|
if (isBasicRetryConfig(config) && !_callbacks->supportsBasicRetryConfig()) {
|
|
strcpy(reply, "Error, retry configuration unsupported on this role");
|
|
return;
|
|
}
|
|
mesh::cli::RecentRepeaterGetQuery recent_query;
|
|
const mesh::cli::RecentRepeaterGetMatch recent_query_match =
|
|
mesh::cli::parseRecentRepeaterGet(config, recent_query);
|
|
if (configKeyEquals(config, "dutycycle")) {
|
|
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 (strcmp(config, "system.watchdog") == 0) {
|
|
#if defined(NRF52_PLATFORM)
|
|
sprintf(reply, "> %s", _prefs->system_watchdog_enabled ? "on" : "off");
|
|
#else
|
|
strcpy(reply, "Error: unsupported on this platform");
|
|
#endif
|
|
} else if (configKeyEquals(config, "af")) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
|
|
} else if (configKeyEquals(config, "int.thresh")) {
|
|
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
|
|
} else if (configKeyEquals(config, "cad")) {
|
|
sprintf(reply, "> %s. # channel busy: %u", _prefs->cad_enabled ? "on" : "off", _board->n_cad_busy);
|
|
} else if (configKeyEquals(config, "agc.reset.interval")) {
|
|
sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
|
|
} else if (configKeyEquals(config, "multi.acks")) {
|
|
sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
|
|
} else if (configKeyEquals(config, "allow.read.only")) {
|
|
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
|
|
} else if (configKeyEquals(config, "telemetry.access")) {
|
|
sprintf(reply, "> %s", _prefs->telemetry_access == TELEMETRY_ACCESS_ACL ? "acl" : "all");
|
|
} else if (configKeyEquals(config, "flood.advert.interval")) {
|
|
mesh::RepeaterRadioTiming timing;
|
|
timing.setTempDuration(_callbacks->getTempRadioDurationSeconds());
|
|
sprintf(reply, "> %lu", (unsigned long)timing.floodAdvertHours(_prefs->flood_advert_interval));
|
|
} else if (configKeyEquals(config, "advert.interval")) {
|
|
mesh::RepeaterRadioTiming timing;
|
|
timing.setTempDuration(_callbacks->getTempRadioDurationSeconds());
|
|
sprintf(reply, "> %lu", (unsigned long)timing.localAdvertMinutes((uint32_t)_prefs->advert_interval * 2));
|
|
} else if (configKeyEquals(config, "guest.password")) {
|
|
sprintf(reply, "> %s", _prefs->guest_password);
|
|
} else if (sender_timestamp == 0 && configKeyEquals(config, "prv.key")) { // local connections 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 (configKeyEquals(config, "name")) {
|
|
sprintf(reply, "> %s", _prefs->node_name);
|
|
} else if (configKeyEquals(config, "repeat")) {
|
|
sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on");
|
|
} else if (configKeyEquals(config, "lat")) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
|
|
} else if (configKeyEquals(config, "lon")) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
|
|
} else if (configKeyEquals(config, "radio.rxgain")) {
|
|
sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
|
|
} else if (configKeyEquals(config, "radio.fem.rxgain")) {
|
|
if (!_board->canControlLoRaFemLna()) {
|
|
strcpy(reply, "Error: unsupported");
|
|
} else {
|
|
sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off");
|
|
}
|
|
} else if (configKeyEquals(config, "radio.fem.txgain")) {
|
|
if (!_board->canControlLoRaFemPaGain()) {
|
|
strcpy(reply, "Error: unsupported");
|
|
} else {
|
|
sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off");
|
|
}
|
|
} else if (memcmp(config, "tempradioat", 11) == 0 && (config[11] == 0 || config[11] == ' ')) {
|
|
_callbacks->formatScheduledRadioParams(true, skipSpacesConst(&config[11]), reply);
|
|
} else if (memcmp(config, "radioat", 7) == 0 && (config[7] == 0 || config[7] == ' ')) {
|
|
_callbacks->formatScheduledRadioParams(false, skipSpacesConst(&config[7]), reply);
|
|
} else if (strcmp(config, "radio.rxps.rfrx_disabled") == 0) {
|
|
if (!_callbacks->supportsRxPowerSavingRfRxDisable()) {
|
|
strcpy(reply, "Error: unsupported");
|
|
} else {
|
|
sprintf(reply, "> %s", _callbacks->isRxPowerSavingRfRxDisabled() ? "on" : "off");
|
|
}
|
|
} else if (strcmp(config, "radio.rxps.config") == 0) {
|
|
ensureRxPowerSavingDefaults(&_prefs->rx_ps_rx_us, &_prefs->rx_ps_sleep_us);
|
|
sprintf(reply, "> %s,level=%u,preamble=%u,rx=%lu,sleep=%lu",
|
|
_prefs->rx_powersaving_enabled ? "on" : "off",
|
|
(unsigned)_prefs->rx_ps_level,
|
|
(unsigned)_prefs->rx_ps_preamble,
|
|
(unsigned long)_prefs->rx_ps_rx_us,
|
|
(unsigned long)_prefs->rx_ps_sleep_us);
|
|
} else if (configKeyEquals(config, "radio.rxps")) { // RX PowerSaving
|
|
ensureRxPowerSavingDefaults(&_prefs->rx_ps_rx_us, &_prefs->rx_ps_sleep_us);
|
|
sprintf(reply, "> %s,%lu,%lu", _prefs->rx_powersaving_enabled ? "on" : "off",
|
|
(unsigned long)_prefs->rx_ps_rx_us, (unsigned long)_prefs->rx_ps_sleep_us);
|
|
} else if (configKeyEquals(config, "rxps.wd")) {
|
|
uint32_t wd_soft, wd_hard;
|
|
_callbacks->getRxPsWatchdogCounts(&wd_soft, &wd_hard);
|
|
sprintf(reply, "> soft=%lu,hard=%lu", (unsigned long)wd_soft, (unsigned long)wd_hard);
|
|
} else if (configKeyEquals(config, "radio")) {
|
|
char freq[16], bw[16];
|
|
strcpy(freq, StrHelper::ftoa(_prefs->freq));
|
|
strcpy(bw, StrHelper::ftoa3(_prefs->bw));
|
|
// The official app parses this reply as exactly four CSV fields.
|
|
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
|
|
} else if (configKeyEquals(config, "rxdelay")) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
|
|
} else if (configKeyEquals(config, "txdelay")) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
|
|
} else if (strcmp(config, "flood.channel.data.hops") == 0) {
|
|
char hops[8];
|
|
formatFloodChannelHops(hops, _prefs->flood_channel_data_max_hops);
|
|
sprintf(reply, "> %s", hops);
|
|
} else if (strcmp(config, "flood.channel.data") == 0) {
|
|
char hops[8];
|
|
formatFloodChannelHops(hops, _prefs->flood_channel_data_max_hops);
|
|
sprintf(reply, "> %s %s", _prefs->flood_channel_data_enabled ? "on" : "off", hops);
|
|
} else if (configKeyEquals(config, "flood.max.advert")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert);
|
|
} else if (configKeyEquals(config, "flood.max.unscoped")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped);
|
|
} else if (configKeyEquals(config, "flood.max")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
|
|
} else if (configKeyEquals(config, "direct.txdelay")) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
|
|
} else if (configKeyEquals(config, "retry.preset")) {
|
|
sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset));
|
|
} else if (configKeyEquals(config, "direct.retry")) {
|
|
sprintf(reply, "> %s", _prefs->direct_retry_enabled ? "on" : "off");
|
|
} else if (configKeyEquals(config, "direct.retry.heard")) {
|
|
sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off");
|
|
} else if (configKeyEquals(config, "direct.retry.margin")) {
|
|
char margin[12];
|
|
formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4);
|
|
sprintf(reply, "> %s", margin);
|
|
} else if (configKeyEquals(config, "direct.retry.count")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts);
|
|
} else if (configKeyEquals(config, "direct.retry.base")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms);
|
|
} else if (configKeyEquals(config, "direct.retry.step")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_step_ms);
|
|
} else if (configKeyEquals(config, "flood.retry.count")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->flood_retry_attempts);
|
|
} else if (configKeyEquals(config, "flood.retry.path")) {
|
|
char path_gate[8];
|
|
formatFloodRetryPathGate(path_gate, _prefs->flood_retry_max_path);
|
|
sprintf(reply, "> %s", path_gate);
|
|
} else if (configKeyEquals(config, "flood.retry.group.path")) {
|
|
char path_gate[8];
|
|
formatFloodRetryPathGate(path_gate, _prefs->flood_retry_group_max_path);
|
|
sprintf(reply, "> %s", path_gate);
|
|
} else if (configKeyEquals(config, "flood.retry.prefixes")) {
|
|
formatFloodRetryPrefixList(tmp, _prefs->flood_retry_prefixes, FLOOD_RETRY_PREFIX_SLOTS);
|
|
sprintf(reply, "> %s", tmp[0] ? tmp : "none");
|
|
} else if (configKeyEquals(config, "flood.retry.ignore")) {
|
|
formatFloodRetryPrefixList(tmp, _prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES);
|
|
sprintf(reply, "> %s", tmp[0] ? tmp : "none");
|
|
} else if (configKeyEquals(config, "flood.retry.advert")) {
|
|
sprintf(reply, "> %s", _prefs->flood_retry_advert_enabled ? "on" : "off");
|
|
} else if (configKeyEquals(config, "flood.retry.bridge")) {
|
|
sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off");
|
|
} else if (memcmp(config, "flood.retry.bucket.", 19) == 0) {
|
|
int32_t bucket = 0;
|
|
if (mesh::cli::parseIntegerStrict(&config[19], bucket)
|
|
&& bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) {
|
|
formatFloodRetryPrefixList(tmp, _prefs->flood_retry_bridge_buckets[bucket - 1], FLOOD_RETRY_BUCKET_PREFIXES);
|
|
sprintf(reply, "> %s", tmp[0] ? tmp : "none");
|
|
} else {
|
|
sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS);
|
|
}
|
|
} else if (configKeyEquals(config, "direct.retry.cr")) {
|
|
if (!_prefs->direct_retry_cr_enabled) {
|
|
strcpy(reply, "> off");
|
|
} else if (!_callbacks->supportsAdvancedRetryConfig()) {
|
|
strcpy(reply, "> on");
|
|
} else {
|
|
char cr4[12], cr5[12], cr7[12], cr8[12];
|
|
formatSnrDbX4(cr4, sizeof(cr4), _prefs->direct_retry_cr4_snr_x4);
|
|
formatSnrDbX4(cr5, sizeof(cr5), _prefs->direct_retry_cr5_snr_x4);
|
|
formatSnrDbX4(cr7, sizeof(cr7), _prefs->direct_retry_cr7_snr_x4);
|
|
formatSnrDbX4(cr8, sizeof(cr8), _prefs->direct_retry_cr8_snr_x4);
|
|
sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8);
|
|
}
|
|
} else if (recent_query_match == mesh::cli::RecentRepeaterGetMatch::Valid) {
|
|
_callbacks->formatRecentRepeatersReply(
|
|
reply, recent_query.page,
|
|
recent_query.search_prefix_len == 0 ? NULL : recent_query.search_prefix,
|
|
recent_query.search_prefix_len);
|
|
} else if (recent_query_match == mesh::cli::RecentRepeaterGetMatch::Invalid) {
|
|
strcpy(reply, "Error, use: get recent.repeaters [page] | search <2|4|6 hex> [page]");
|
|
} else if (configKeyEquals(config, "owner.info")) {
|
|
*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 (configKeyEquals(config, "path.hash.mode")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
|
|
} else if (configKeyEquals(config, "loop.detect")) {
|
|
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 (configKeyEquals(config, "tx")) {
|
|
sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
|
|
} else if (configKeyEquals(config, "freq")) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
|
|
} else if (configKeyEquals(config, "public.key")) {
|
|
strcpy(reply, "> ");
|
|
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
|
|
} else if (configKeyEquals(config, "role")) {
|
|
sprintf(reply, "> %s", _callbacks->getRole());
|
|
} else if (configKeyEquals(config, "bridge.type")) {
|
|
sprintf(reply, "> %s",
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
"mqtt+espnow"
|
|
#elif defined(WITH_RS232_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
"rs232+espnow"
|
|
#elif defined(WITH_RS232_BRIDGE)
|
|
"rs232"
|
|
#elif WITH_ESPNOW_BRIDGE
|
|
"espnow"
|
|
#elif WITH_MQTT_BRIDGE
|
|
"mqtt"
|
|
#else
|
|
"none"
|
|
#endif
|
|
);
|
|
#ifdef WITH_BRIDGE
|
|
} else if (configKeyEquals(config, "bridge.enabled")) {
|
|
sprintf(reply, "> %s",
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
_prefs->espnow_bridge_enabled ? "on" : "off");
|
|
#else
|
|
_prefs->bridge_enabled ? "on" : "off");
|
|
#endif
|
|
} else if (configKeyEquals(config, "bridge.running")) {
|
|
sprintf(reply, "> %s",
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
_callbacks->isEspNowBridgeRunning() ? "on" : "off");
|
|
#else
|
|
_callbacks->isBridgeRunning() ? "on" : "off");
|
|
#endif
|
|
#if defined(WITH_ESPNOW_BRIDGE)
|
|
} else if (configKeyEquals(config, "espnow.enabled")) {
|
|
sprintf(reply, "> %s",
|
|
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE) \
|
|
|| defined(ESPNOW_BRIDGE_MERGED)
|
|
_prefs->espnow_bridge_enabled ? "on" : "off");
|
|
#else
|
|
_prefs->bridge_enabled ? "on" : "off");
|
|
#endif
|
|
} else if (configKeyEquals(config, "espnow.running")) {
|
|
sprintf(reply, "> %s", _callbacks->isEspNowBridgeRunning() ? "on" : "off");
|
|
#endif
|
|
} else if (configKeyEquals(config, "bridge.delay")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
|
|
} else if (configKeyEquals(config, "bridge.source")) {
|
|
sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "rx" : "tx");
|
|
#endif
|
|
#ifdef WITH_RS232_BRIDGE
|
|
} else if (configKeyEquals(config, "bridge.baud")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
|
|
} else if (configKeyEquals(config, "bridge.uart")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_uart);
|
|
#endif
|
|
#ifdef WITH_ESPNOW_BRIDGE
|
|
} else if (configKeyEquals(config, "bridge.channel")) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
|
|
} else if (configKeyEquals(config, "bridge.secret")) {
|
|
sprintf(reply, "> %s", _prefs->bridge_secret);
|
|
} else if (configKeyEquals(config, "bridge.format")) {
|
|
sprintf(reply, "> %s",
|
|
mesh::bridge::espNowFormatName(_prefs->bridge_format));
|
|
#endif
|
|
} else if (configKeyEquals(config, "bootloader.ver")) {
|
|
#ifdef NRF52_PLATFORM
|
|
char ver[128]; // preserve preview/fork/build suffixes without truncation
|
|
if (_board->getBootloaderVersion(ver, sizeof(ver))) {
|
|
sprintf(reply, "> %s", ver);
|
|
} else {
|
|
strcpy(reply, "> unknown");
|
|
}
|
|
#else
|
|
strcpy(reply, "Error: unsupported");
|
|
#endif
|
|
} else if (configKeyEquals(config, "adc.multiplier")) {
|
|
float adc_mult = _board->getAdcMultiplier();
|
|
if (adc_mult == 0.0f) {
|
|
strcpy(reply, "Error: unsupported");
|
|
} else {
|
|
sprintf(reply, "> %.3f", adc_mult);
|
|
}
|
|
// Power management commands
|
|
} else if (configKeyEquals(config, "pwrmgt.support")) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
strcpy(reply, "> supported");
|
|
#else
|
|
strcpy(reply, "> unsupported");
|
|
#endif
|
|
} else if (configKeyEquals(config, "pwrmgt.source")) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
|
|
#else
|
|
strcpy(reply, "ERROR: Power management not supported");
|
|
#endif
|
|
} else if (configKeyEquals(config, "pwrmgt.bootreason")) {
|
|
sprintf(reply, "> Reset: %s; Shutdown: %s",
|
|
_board->getResetReasonString(_board->getResetReason()),
|
|
_board->getShutdownReasonString(_board->getShutdownReason()));
|
|
} else if (configKeyEquals(config, "pwrmgt.bootmv")) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
sprintf(reply, "> %u mV", _board->getBootVoltage());
|
|
#else
|
|
strcpy(reply, "ERROR: Power management not supported");
|
|
#endif
|
|
} else if (configKeyEquals(config, "reboot.interval")) {
|
|
if (_prefs->reboot_interval == 0) {
|
|
strcpy(reply, "disabled");
|
|
} else {
|
|
sprintf(reply, "> %d", (uint8_t)_prefs->reboot_interval);
|
|
}
|
|
} else if (strcmp(config, "extra.sf") == 0) {
|
|
#if defined(USE_LR2021)
|
|
char* tmp = reply;
|
|
for (int i = 0; i < 3 && _prefs->extra_sf[i] != 0; i++) {
|
|
tmp += sprintf(tmp, "%s%d", (i == 0) ? "" : ",", _prefs->extra_sf[i]);
|
|
}
|
|
if (tmp == reply) {
|
|
strcpy(reply, "No extra SF configured");
|
|
}
|
|
#else
|
|
strcpy(reply, "Error: extra.sf requires an LR2021 radio");
|
|
#endif
|
|
} else {
|
|
mesh::cli::formatUnknownSetting(reply, 160, config);
|
|
}
|
|
}
|
|
|
|
void CommonCLI::handleDelCmd(char* command, char* reply) {
|
|
const char* config = &command[4];
|
|
if (memcmp(config, "tempradioat", 11) == 0 && (config[11] == 0 || config[11] == ' ')) {
|
|
_callbacks->deleteScheduledRadioParams(true, skipSpacesConst(&config[11]), reply);
|
|
} else if (memcmp(config, "radioat", 7) == 0 && (config[7] == 0 || config[7] == ' ')) {
|
|
_callbacks->deleteScheduledRadioParams(false, skipSpacesConst(&config[7]), reply);
|
|
} else {
|
|
strcpy(reply, "unknown del: ");
|
|
StrHelper::strncpy(&reply[13], config, 160 - 14);
|
|
}
|
|
}
|
|
|
|
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 or ambiguous 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; }
|
|
|
|
// Build the complete definition on a staged copy. A bad name, jump, table
|
|
// overflow, or cycle leaves the live hierarchy completely unchanged.
|
|
RegionMap staged(*_region_map);
|
|
RegionEntry* cursor = &staged.getWildcard();
|
|
for (char* tok; (tok = takeToken(&payload)) != nullptr; ) {
|
|
if (!processRegionDefSegment(&staged, tok, &cursor, reply)) return;
|
|
}
|
|
*_region_map = staged;
|
|
_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], "<null>") == 0) {
|
|
_region_map->setDefaultRegion(NULL);
|
|
_callbacks->onDefaultRegionChanged(NULL);
|
|
_callbacks->saveRegions(); // persist in one atomic step
|
|
sprintf(reply, " default scope is now <null>");
|
|
} 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 : "<null>");
|
|
} 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) {
|
|
RegionEntry* current_default = _region_map->getDefaultRegion();
|
|
bool removed_default = current_default != NULL && current_default->id == region->id;
|
|
if (_region_map->removeRegion(*region)) {
|
|
if (removed_default) _callbacks->onDefaultRegionChanged(NULL);
|
|
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 - ??");
|
|
}
|
|
}
|