Pick up 97 commits from upstream/dev, resolving conflicts in CommonCLI prefs layout, board definitions, docs, and T-Beam Supreme platformio config. Co-authored-by: Cursor <cursoragent@cursor.com>
2345 lines
105 KiB
C++
2345 lines
105 KiB
C++
#include <Arduino.h>
|
|
#include "CommonCLI.h"
|
|
#include "TxtDataHelpers.h"
|
|
#include "AdvertDataHelpers.h"
|
|
#include "AlertReporter.h" // for alertReporterBannedChannelMatch()
|
|
#include <RTClib.h>
|
|
#include <Utils.h>
|
|
|
|
#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>
|
|
#endif
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
#include "bridges/MQTTBridge.h"
|
|
#include "MQTTDefaults.h"
|
|
|
|
// Helper function to calculate total size of MQTT fields for file format compatibility
|
|
// Uses NodePrefs struct to get accurate field sizes
|
|
static size_t getMQTTFieldsSize(const NodePrefs* prefs) {
|
|
return sizeof(prefs->mqtt_origin) + sizeof(prefs->mqtt_iata) +
|
|
sizeof(prefs->mqtt_status_enabled) + sizeof(prefs->mqtt_packets_enabled) +
|
|
sizeof(prefs->mqtt_raw_enabled) + sizeof(prefs->mqtt_tx_enabled) +
|
|
sizeof(prefs->mqtt_status_interval) + sizeof(prefs->wifi_ssid) +
|
|
sizeof(prefs->wifi_password) + sizeof(prefs->timezone_string) +
|
|
sizeof(prefs->timezone_offset) + sizeof(prefs->mqtt_slot_preset) +
|
|
sizeof(prefs->mqtt_slot_host) + sizeof(prefs->mqtt_slot_port) +
|
|
sizeof(prefs->mqtt_slot_username) + sizeof(prefs->mqtt_slot_password) +
|
|
sizeof(prefs->mqtt_owner_public_key) + sizeof(prefs->mqtt_email);
|
|
}
|
|
#endif
|
|
|
|
// Believe it or not, this std C function is busted on some platforms!
|
|
static uint32_t _atoi(const char* sp) {
|
|
uint32_t n = 0;
|
|
while (*sp && *sp >= '0' && *sp <= '9') {
|
|
n *= 10;
|
|
n += (*sp++ - '0');
|
|
}
|
|
return n;
|
|
}
|
|
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
static int getMQTTPresetNameCount() {
|
|
// Include virtual presets accepted by CLI parser.
|
|
return MQTT_PRESET_COUNT + 2; // built-ins + custom + none
|
|
}
|
|
|
|
static const char* getMQTTPresetNameByIndex(int index) {
|
|
if (index < MQTT_PRESET_COUNT) return MQTT_PRESETS[index].name;
|
|
if (index == MQTT_PRESET_COUNT) return MQTT_PRESET_CUSTOM;
|
|
if (index == MQTT_PRESET_COUNT + 1) return MQTT_PRESET_NONE;
|
|
return nullptr;
|
|
}
|
|
|
|
static void formatMQTTPresetListReply(char* reply, size_t reply_size, int start) {
|
|
if (!reply || reply_size == 0) return;
|
|
reply[0] = '\0';
|
|
|
|
const int total = getMQTTPresetNameCount();
|
|
if (start < 0 || start >= total) {
|
|
snprintf(reply, reply_size, "Error: preset list start must be 0-%d", total - 1);
|
|
return;
|
|
}
|
|
|
|
// Keep room for continuation marker and null terminator.
|
|
const size_t reserve_for_next = 18;
|
|
size_t used = 0;
|
|
bool wrote_any = false;
|
|
|
|
int index = start;
|
|
while (index < total) {
|
|
const char* name = getMQTTPresetNameByIndex(index);
|
|
if (!name) break;
|
|
size_t name_len = strlen(name);
|
|
size_t room = reply_size - used;
|
|
if (room <= reserve_for_next) break;
|
|
size_t needed = name_len + (wrote_any ? 1 : 0); // comma separator
|
|
if (needed >= room - reserve_for_next) break;
|
|
if (wrote_any) {
|
|
reply[used++] = ',';
|
|
}
|
|
memcpy(reply + used, name, name_len);
|
|
used += name_len;
|
|
reply[used] = '\0';
|
|
wrote_any = true;
|
|
index++;
|
|
}
|
|
|
|
if (!wrote_any) {
|
|
strcpy(reply, "Error: list page too small");
|
|
return;
|
|
}
|
|
|
|
if (index < total) {
|
|
snprintf(reply + used, reply_size - used, "... next:%d", index);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static bool isValidName(const char *n) {
|
|
while (*n) {
|
|
if (*n == '[' || *n == ']' || *n == '/' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
|
|
n++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#ifdef ESP_PLATFORM
|
|
// Optional embedded CA bundle symbols produced by board_build.embed_files.
|
|
// Weak linkage keeps non-bundle builds linkable.
|
|
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_src_certs_x509_crt_bundle_bin_start") __attribute__((weak));
|
|
extern const uint8_t rootca_crt_bundle_end[] asm("_binary_src_certs_x509_crt_bundle_bin_end") __attribute__((weak));
|
|
|
|
static bool parseTlsBundleTarget(const char* input, char* host_out, size_t host_out_size, uint16_t* port_out) {
|
|
if (!input || !host_out || host_out_size == 0 || !port_out) return false;
|
|
|
|
while (*input == ' ') input++;
|
|
if (*input == '\0') return false;
|
|
|
|
const char* start = input;
|
|
const char* scheme = strstr(input, "://");
|
|
if (scheme) start = scheme + 3;
|
|
|
|
const char* end = start;
|
|
while (*end && *end != '/' && *end != '?' && *end != '#') end++;
|
|
if (end <= start) return false;
|
|
|
|
uint16_t port = 443;
|
|
const char* host_start = start;
|
|
const char* host_end = end;
|
|
|
|
if (*host_start == '[') {
|
|
const char* close = (const char*)memchr(host_start, ']', host_end - host_start);
|
|
if (!close) return false;
|
|
if ((close + 1) < host_end && *(close + 1) == ':') {
|
|
int p = atoi(close + 2);
|
|
if (p <= 0 || p > 65535) return false;
|
|
port = (uint16_t)p;
|
|
}
|
|
host_start++;
|
|
host_end = close;
|
|
} else {
|
|
const char* colon = (const char*)memchr(host_start, ':', host_end - host_start);
|
|
if (colon) {
|
|
int p = atoi(colon + 1);
|
|
if (p <= 0 || p > 65535) return false;
|
|
port = (uint16_t)p;
|
|
host_end = colon;
|
|
}
|
|
}
|
|
|
|
size_t host_len = (size_t)(host_end - host_start);
|
|
if (host_len == 0 || host_len >= host_out_size) return false;
|
|
memcpy(host_out, host_start, host_len);
|
|
host_out[host_len] = '\0';
|
|
*port_out = port;
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
|
bool is_fresh_install = false;
|
|
bool is_upgrade = false;
|
|
|
|
if (fs->exists("/com_prefs")) {
|
|
loadPrefsInt(fs, "/com_prefs"); // new filename
|
|
} else if (fs->exists("/node_prefs")) {
|
|
loadPrefsInt(fs, "/node_prefs");
|
|
is_upgrade = true; // Migrating from old filename
|
|
savePrefs(fs); // save to new filename
|
|
fs->remove("/node_prefs"); // remove old
|
|
} else {
|
|
// File doesn't exist - set default bridge settings for fresh installs
|
|
is_fresh_install = true;
|
|
_prefs->bridge_pkt_src = 1; // Default to RX (logRx) for new installs
|
|
}
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// Load MQTT preferences from separate file
|
|
loadMQTTPrefs(fs);
|
|
// Sync MQTT prefs to NodePrefs so existing code (like MQTTBridge) can access them
|
|
syncMQTTPrefsToNodePrefs();
|
|
|
|
// For MQTT bridge, migrate bridge.source to RX (logRx) only on fresh installs or upgrades
|
|
// so legacy "tx" is not the default. mqtt.rx / mqtt.tx are separate (fresh default: advert for TX)
|
|
if ((is_fresh_install || is_upgrade) && _prefs->bridge_pkt_src == 0) {
|
|
MESH_DEBUG_PRINTLN("MQTT Bridge: Migrating bridge.source from tx to rx (MQTT bridge default)");
|
|
_prefs->bridge_pkt_src = 1; // Set to RX (logRx)
|
|
savePrefs(fs); // Save the updated preference
|
|
}
|
|
// mqtt_rx_enabled: new field appended to end of MQTTPrefs. On upgrade from older firmware,
|
|
// the shorter /mqtt_prefs file won't contain it, so it keeps the default value (1 = on)
|
|
// set by setMQTTPrefsDefaults(). No explicit migration needed.
|
|
#endif
|
|
}
|
|
|
|
void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
|
#if defined(RP2040_PLATFORM)
|
|
File file = fs->open(filename, "r");
|
|
#else
|
|
File file = fs->open(filename);
|
|
#endif
|
|
if (file) {
|
|
uint8_t pad[8];
|
|
|
|
file.read((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
|
|
file.read((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4
|
|
file.read(pad, 4); // 36
|
|
file.read((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
|
|
file.read((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
|
|
file.read((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56
|
|
file.read((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72
|
|
file.read((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
|
|
file.read((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
|
|
file.read((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
|
|
file.read(pad, 1); // 79 : 1 byte unused (was rx_boosted_gain in v1.14.1, moved to end for upgrade compat)
|
|
file.read((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
|
|
file.read((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
|
|
file.read((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
|
|
file.read((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
|
|
file.read(pad, 4); // 108
|
|
file.read((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112
|
|
file.read((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113
|
|
file.read((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
|
|
file.read((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
|
|
file.read((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116
|
|
file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
|
|
file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121
|
|
file.read((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122
|
|
file.read(pad, 1); // 123
|
|
file.read((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
|
|
file.read((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
|
|
file.read((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
|
|
file.read((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127
|
|
file.read((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128
|
|
file.read((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130
|
|
file.read((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131
|
|
file.read((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135
|
|
file.read((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136
|
|
file.read((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152
|
|
file.read(pad, 3); // 153
|
|
file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
|
|
file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
|
|
file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161
|
|
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
|
|
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
|
|
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
|
|
// MQTT settings - skip reading from main prefs file (now stored separately)
|
|
// For backward compatibility, we'll skip these bytes if they exist in old files
|
|
// The actual MQTT prefs will be loaded from /mqtt_prefs in loadMQTTPrefs()
|
|
// Skip MQTT fields for file format compatibility (whether MQTT bridge is enabled or not)
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
size_t mqtt_fields_size = getMQTTFieldsSize(_prefs);
|
|
#else
|
|
// If MQTT bridge not enabled, still skip these fields for file format compatibility
|
|
size_t mqtt_fields_size =
|
|
sizeof(_prefs->mqtt_origin) + sizeof(_prefs->mqtt_iata) +
|
|
sizeof(_prefs->mqtt_status_enabled) + sizeof(_prefs->mqtt_packets_enabled) +
|
|
sizeof(_prefs->mqtt_raw_enabled) + sizeof(_prefs->mqtt_tx_enabled) +
|
|
sizeof(_prefs->mqtt_status_interval) + sizeof(_prefs->wifi_ssid) +
|
|
sizeof(_prefs->wifi_password) + sizeof(_prefs->timezone_string) +
|
|
sizeof(_prefs->timezone_offset) + sizeof(_prefs->mqtt_slot_preset) +
|
|
sizeof(_prefs->mqtt_slot_host) + sizeof(_prefs->mqtt_slot_port) +
|
|
sizeof(_prefs->mqtt_slot_username) + sizeof(_prefs->mqtt_slot_password) +
|
|
sizeof(_prefs->mqtt_owner_public_key) + sizeof(_prefs->mqtt_email);
|
|
#endif
|
|
uint8_t skip_buffer[512]; // Large enough buffer
|
|
size_t remaining = mqtt_fields_size;
|
|
while (remaining > 0) {
|
|
size_t to_read = remaining > sizeof(skip_buffer) ? sizeof(skip_buffer) : remaining;
|
|
file.read(skip_buffer, to_read);
|
|
remaining -= to_read;
|
|
}
|
|
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
|
|
{
|
|
// Tail layout (current): 291-292 flood_max_*, 293+ snmp/alert fields.
|
|
// Legacy flex-branch files stored snmp at 291 without flood_max_* fields.
|
|
uint8_t byte291, byte292;
|
|
file.read(&byte291, 1);
|
|
file.read(&byte292, 1);
|
|
if (byte291 <= 1 && byte292 > 64) {
|
|
_prefs->snmp_enabled = byte291;
|
|
_prefs->snmp_community[0] = (char)byte292;
|
|
file.read((uint8_t *)&_prefs->snmp_community[1], sizeof(_prefs->snmp_community) - 1);
|
|
} else {
|
|
_prefs->flood_max_unscoped = byte291;
|
|
_prefs->flood_max_advert = byte292;
|
|
if (file.available() >= (int)sizeof(_prefs->snmp_enabled)) {
|
|
file.read((uint8_t *)&_prefs->snmp_enabled, sizeof(_prefs->snmp_enabled)); // 293
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->snmp_community)) {
|
|
file.read((uint8_t *)&_prefs->snmp_community, sizeof(_prefs->snmp_community)); // 294
|
|
}
|
|
}
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->radio_watchdog_minutes)) {
|
|
file.read((uint8_t *)&_prefs->radio_watchdog_minutes, sizeof(_prefs->radio_watchdog_minutes)); // 318
|
|
}
|
|
// Alert channel fields (appended; older files won't have them — defaults from MyMesh ctor remain)
|
|
if (file.available() >= (int)sizeof(_prefs->alert_enabled)) {
|
|
file.read((uint8_t *)&_prefs->alert_enabled, sizeof(_prefs->alert_enabled));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->alert_psk_hex)) {
|
|
file.read((uint8_t *)&_prefs->alert_psk_hex, sizeof(_prefs->alert_psk_hex));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->alert_wifi_minutes)) {
|
|
file.read((uint8_t *)&_prefs->alert_wifi_minutes, sizeof(_prefs->alert_wifi_minutes));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->alert_mqtt_minutes)) {
|
|
file.read((uint8_t *)&_prefs->alert_mqtt_minutes, sizeof(_prefs->alert_mqtt_minutes));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->alert_min_interval_min)) {
|
|
file.read((uint8_t *)&_prefs->alert_min_interval_min, sizeof(_prefs->alert_min_interval_min));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->alert_hashtag)) {
|
|
file.read((uint8_t *)&_prefs->alert_hashtag, sizeof(_prefs->alert_hashtag));
|
|
}
|
|
if (file.available() >= (int)sizeof(_prefs->alert_region)) {
|
|
file.read((uint8_t *)&_prefs->alert_region, sizeof(_prefs->alert_region));
|
|
}
|
|
// ensure null termination after raw read
|
|
_prefs->snmp_community[sizeof(_prefs->snmp_community) - 1] = '\0';
|
|
_prefs->alert_psk_hex[sizeof(_prefs->alert_psk_hex) - 1] = '\0';
|
|
_prefs->alert_hashtag[sizeof(_prefs->alert_hashtag) - 1] = '\0';
|
|
_prefs->alert_region[sizeof(_prefs->alert_region) - 1] = '\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);
|
|
_prefs->airtime_factor = constrain(_prefs->airtime_factor, 0, 9.0f);
|
|
_prefs->freq = constrain(_prefs->freq, 150.0f, 2500.0f);
|
|
_prefs->bw = constrain(_prefs->bw, 7.8f, 500.0f);
|
|
_prefs->sf = constrain(_prefs->sf, 5, 12);
|
|
_prefs->cr = constrain(_prefs->cr, 5, 8);
|
|
_prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, -9, 30);
|
|
_prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1);
|
|
_prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f);
|
|
_prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2); // NOTE: mode 3 reserved for future
|
|
_prefs->loop_detect = constrain(_prefs->loop_detect, 0, 3); // LOOP_DETECT_OFF..LOOP_DETECT_STRICT
|
|
|
|
// sanitise bad bridge pref values
|
|
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
|
|
_prefs->bridge_delay = constrain(_prefs->bridge_delay, 0, 10000);
|
|
_prefs->bridge_pkt_src = constrain(_prefs->bridge_pkt_src, 0, 1);
|
|
_prefs->bridge_baud = constrain(_prefs->bridge_baud, 9600, BRIDGE_MAX_BAUD);
|
|
_prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14);
|
|
|
|
_prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1);
|
|
|
|
_prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1);
|
|
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
|
|
|
|
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
|
|
_prefs->snmp_enabled = constrain(_prefs->snmp_enabled, 0, 1);
|
|
_prefs->snmp_community[sizeof(_prefs->snmp_community) - 1] = '\0'; // ensure null terminated
|
|
if (_prefs->radio_watchdog_minutes > 120) {
|
|
_prefs->radio_watchdog_minutes = 5;
|
|
}
|
|
|
|
file.close();
|
|
}
|
|
}
|
|
|
|
void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
fs->remove("/com_prefs");
|
|
File file = fs->open("/com_prefs", FILE_O_WRITE);
|
|
#elif defined(RP2040_PLATFORM)
|
|
File file = fs->open("/com_prefs", "w");
|
|
#else
|
|
File file = fs->open("/com_prefs", "w", true);
|
|
#endif
|
|
if (file) {
|
|
uint8_t pad[8];
|
|
memset(pad, 0, sizeof(pad));
|
|
|
|
file.write((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
|
|
file.write((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4
|
|
file.write(pad, 4); // 36
|
|
file.write((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
|
|
file.write((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
|
|
file.write((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56
|
|
file.write((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72
|
|
file.write((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
|
|
file.write((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
|
|
file.write((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
|
|
file.write(pad, 1); // 79 : 1 byte unused (rx_boosted_gain moved to end)
|
|
file.write((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
|
|
file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
|
|
file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
|
|
file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
|
|
file.write(pad, 4); // 108
|
|
file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112
|
|
file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113
|
|
file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
|
|
file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
|
|
file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116
|
|
file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
|
|
file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121
|
|
file.write((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122
|
|
file.write(pad, 1); // 123
|
|
file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
|
|
file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
|
|
file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
|
|
file.write((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127
|
|
file.write((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128
|
|
file.write((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130
|
|
file.write((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131
|
|
file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135
|
|
file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136
|
|
file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152
|
|
file.write(pad, 3); // 153
|
|
file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
|
|
file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
|
|
file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161
|
|
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
|
|
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
|
|
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
|
|
// MQTT settings - no longer saved here (stored in separate /mqtt_prefs file)
|
|
// Write zeros/padding to maintain file format compatibility
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
size_t mqtt_fields_size = getMQTTFieldsSize(_prefs);
|
|
#else
|
|
// If MQTT bridge not enabled, still write zeros for file format compatibility
|
|
size_t mqtt_fields_size =
|
|
sizeof(_prefs->mqtt_origin) + sizeof(_prefs->mqtt_iata) +
|
|
sizeof(_prefs->mqtt_status_enabled) + sizeof(_prefs->mqtt_packets_enabled) +
|
|
sizeof(_prefs->mqtt_raw_enabled) + sizeof(_prefs->mqtt_tx_enabled) +
|
|
sizeof(_prefs->mqtt_status_interval) + sizeof(_prefs->wifi_ssid) +
|
|
sizeof(_prefs->wifi_password) + sizeof(_prefs->timezone_string) +
|
|
sizeof(_prefs->timezone_offset) + sizeof(_prefs->mqtt_slot_preset) +
|
|
sizeof(_prefs->mqtt_slot_host) + sizeof(_prefs->mqtt_slot_port) +
|
|
sizeof(_prefs->mqtt_slot_username) + sizeof(_prefs->mqtt_slot_password) +
|
|
sizeof(_prefs->mqtt_owner_public_key) + sizeof(_prefs->mqtt_email);
|
|
#endif
|
|
memset(pad, 0, sizeof(pad));
|
|
size_t remaining = mqtt_fields_size;
|
|
while (remaining > 0) {
|
|
size_t to_write = remaining > sizeof(pad) ? sizeof(pad) : remaining;
|
|
file.write(pad, to_write);
|
|
remaining -= to_write;
|
|
}
|
|
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->snmp_enabled, sizeof(_prefs->snmp_enabled)); // 293
|
|
file.write((uint8_t *)&_prefs->snmp_community, sizeof(_prefs->snmp_community)); // 294
|
|
file.write((uint8_t *)&_prefs->radio_watchdog_minutes, sizeof(_prefs->radio_watchdog_minutes)); // 318
|
|
// Alert channel fields (appended)
|
|
file.write((uint8_t *)&_prefs->alert_enabled, sizeof(_prefs->alert_enabled));
|
|
file.write((uint8_t *)&_prefs->alert_psk_hex, sizeof(_prefs->alert_psk_hex));
|
|
file.write((uint8_t *)&_prefs->alert_wifi_minutes, sizeof(_prefs->alert_wifi_minutes));
|
|
file.write((uint8_t *)&_prefs->alert_mqtt_minutes, sizeof(_prefs->alert_mqtt_minutes));
|
|
file.write((uint8_t *)&_prefs->alert_min_interval_min, sizeof(_prefs->alert_min_interval_min));
|
|
file.write((uint8_t *)&_prefs->alert_hashtag, sizeof(_prefs->alert_hashtag));
|
|
file.write((uint8_t *)&_prefs->alert_region, sizeof(_prefs->alert_region));
|
|
|
|
file.close();
|
|
}
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// Save MQTT preferences to separate file
|
|
syncNodePrefsToMQTTPrefs(); // Sync any changes from NodePrefs to MQTTPrefs
|
|
saveMQTTPrefs(fs);
|
|
#endif
|
|
}
|
|
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// Set default values for MQTT preferences (used when file doesn't exist or is corrupted)
|
|
static void setMQTTPrefsDefaults(MQTTPrefs* prefs) {
|
|
applyMQTTDefaults(prefs);
|
|
}
|
|
|
|
void CommonCLI::loadMQTTPrefs(FILESYSTEM* fs) {
|
|
// Initialize with defaults first
|
|
setMQTTPrefsDefaults(&_mqtt_prefs);
|
|
|
|
bool file_existed = fs->exists("/mqtt_prefs");
|
|
if (file_existed) {
|
|
// Load from separate MQTT prefs file
|
|
#if defined(RP2040_PLATFORM)
|
|
File file = fs->open("/mqtt_prefs", "r");
|
|
#else
|
|
File file = fs->open("/mqtt_prefs");
|
|
#endif
|
|
if (file) {
|
|
size_t file_size = file.size();
|
|
|
|
// Detect old (pre-slot) format by file size.
|
|
// Old MQTTPrefs was ~472 bytes (no slot fields). New is ~1464 bytes.
|
|
// If the file is smaller than the new struct but close to OldMQTTPrefs size,
|
|
// read it with the old layout and migrate.
|
|
if (file_size > 0 && file_size <= sizeof(OldMQTTPrefs)) {
|
|
OldMQTTPrefs old_prefs;
|
|
memset(&old_prefs, 0, sizeof(old_prefs));
|
|
size_t bytes_read = file.read((uint8_t *)&old_prefs, file_size < sizeof(old_prefs) ? file_size : sizeof(old_prefs));
|
|
file.close();
|
|
|
|
if (bytes_read > 0) {
|
|
MESH_DEBUG_PRINTLN("MQTT: Migrating old-format prefs to slot-based layout");
|
|
|
|
// Copy common fields (identical layout at start of both structs)
|
|
memcpy(_mqtt_prefs.mqtt_origin, old_prefs.mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin));
|
|
memcpy(_mqtt_prefs.mqtt_iata, old_prefs.mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata));
|
|
_mqtt_prefs.mqtt_status_enabled = old_prefs.mqtt_status_enabled;
|
|
_mqtt_prefs.mqtt_packets_enabled = old_prefs.mqtt_packets_enabled;
|
|
_mqtt_prefs.mqtt_raw_enabled = old_prefs.mqtt_raw_enabled;
|
|
_mqtt_prefs.mqtt_tx_enabled = old_prefs.mqtt_tx_enabled;
|
|
_mqtt_prefs.mqtt_status_interval = old_prefs.mqtt_status_interval;
|
|
memcpy(_mqtt_prefs.wifi_ssid, old_prefs.wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid));
|
|
memcpy(_mqtt_prefs.wifi_password, old_prefs.wifi_password, sizeof(_mqtt_prefs.wifi_password));
|
|
_mqtt_prefs.wifi_power_save = old_prefs.wifi_power_save;
|
|
memcpy(_mqtt_prefs.timezone_string, old_prefs.timezone_string, sizeof(_mqtt_prefs.timezone_string));
|
|
_mqtt_prefs.timezone_offset = old_prefs.timezone_offset;
|
|
|
|
// Migrate shared auth fields
|
|
memcpy(_mqtt_prefs.mqtt_owner_public_key, old_prefs.mqtt_owner_public_key, sizeof(_mqtt_prefs.mqtt_owner_public_key));
|
|
memcpy(_mqtt_prefs.mqtt_email, old_prefs.mqtt_email, sizeof(_mqtt_prefs.mqtt_email));
|
|
|
|
// Migrate analyzer presets to slots
|
|
if (old_prefs.mqtt_analyzer_us_enabled == 1) {
|
|
strncpy(_mqtt_prefs.mqtt_slot_preset[0], "analyzer-us", sizeof(_mqtt_prefs.mqtt_slot_preset[0]) - 1);
|
|
} else {
|
|
strncpy(_mqtt_prefs.mqtt_slot_preset[0], "none", sizeof(_mqtt_prefs.mqtt_slot_preset[0]) - 1);
|
|
}
|
|
if (old_prefs.mqtt_analyzer_eu_enabled == 1) {
|
|
strncpy(_mqtt_prefs.mqtt_slot_preset[1], "analyzer-eu", sizeof(_mqtt_prefs.mqtt_slot_preset[1]) - 1);
|
|
} else {
|
|
strncpy(_mqtt_prefs.mqtt_slot_preset[1], "none", sizeof(_mqtt_prefs.mqtt_slot_preset[1]) - 1);
|
|
}
|
|
|
|
// Migrate custom server to slot 3
|
|
if (old_prefs.mqtt_server[0] != '\0' && old_prefs.mqtt_port > 0) {
|
|
strncpy(_mqtt_prefs.mqtt_slot_preset[2], "custom", sizeof(_mqtt_prefs.mqtt_slot_preset[2]) - 1);
|
|
strncpy(_mqtt_prefs.mqtt_slot_host[2], old_prefs.mqtt_server, sizeof(_mqtt_prefs.mqtt_slot_host[2]) - 1);
|
|
_mqtt_prefs.mqtt_slot_port[2] = old_prefs.mqtt_port;
|
|
strncpy(_mqtt_prefs.mqtt_slot_username[2], old_prefs.mqtt_username, sizeof(_mqtt_prefs.mqtt_slot_username[2]) - 1);
|
|
strncpy(_mqtt_prefs.mqtt_slot_password[2], old_prefs.mqtt_password, sizeof(_mqtt_prefs.mqtt_slot_password[2]) - 1);
|
|
} else {
|
|
strncpy(_mqtt_prefs.mqtt_slot_preset[2], "none", sizeof(_mqtt_prefs.mqtt_slot_preset[2]) - 1);
|
|
}
|
|
|
|
// Save migrated prefs in new format
|
|
saveMQTTPrefs(fs);
|
|
}
|
|
} else if (file_size > 0 && file_size <= sizeof(ThreeSlotMQTTPrefs)) {
|
|
// 3-slot format → 6-slot migration
|
|
// Array sizes changed from [3] to [6], shifting all field offsets.
|
|
// Read into old layout struct and field-copy to new layout.
|
|
ThreeSlotMQTTPrefs old3;
|
|
memset(&old3, 0, sizeof(old3));
|
|
size_t bytes_to_read = file_size < sizeof(old3) ? file_size : sizeof(old3);
|
|
size_t bytes_read = file.read((uint8_t *)&old3, bytes_to_read);
|
|
file.close();
|
|
|
|
if (bytes_read > 0) {
|
|
MESH_DEBUG_PRINTLN("MQTT: Migrating 3-slot prefs to 6-slot layout");
|
|
|
|
// Copy non-slot fields (identical layout)
|
|
memcpy(_mqtt_prefs.mqtt_origin, old3.mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin));
|
|
memcpy(_mqtt_prefs.mqtt_iata, old3.mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata));
|
|
_mqtt_prefs.mqtt_status_enabled = old3.mqtt_status_enabled;
|
|
_mqtt_prefs.mqtt_packets_enabled = old3.mqtt_packets_enabled;
|
|
_mqtt_prefs.mqtt_raw_enabled = old3.mqtt_raw_enabled;
|
|
_mqtt_prefs.mqtt_tx_enabled = old3.mqtt_tx_enabled;
|
|
_mqtt_prefs.mqtt_status_interval = old3.mqtt_status_interval;
|
|
memcpy(_mqtt_prefs.wifi_ssid, old3.wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid));
|
|
memcpy(_mqtt_prefs.wifi_password, old3.wifi_password, sizeof(_mqtt_prefs.wifi_password));
|
|
_mqtt_prefs.wifi_power_save = old3.wifi_power_save;
|
|
memcpy(_mqtt_prefs.timezone_string, old3.timezone_string, sizeof(_mqtt_prefs.timezone_string));
|
|
_mqtt_prefs.timezone_offset = old3.timezone_offset;
|
|
|
|
// Copy slot fields for indices 0-2 from old layout
|
|
for (int i = 0; i < 3; i++) {
|
|
memcpy(_mqtt_prefs.mqtt_slot_preset[i], old3.mqtt_slot_preset[i], sizeof(_mqtt_prefs.mqtt_slot_preset[i]));
|
|
memcpy(_mqtt_prefs.mqtt_slot_host[i], old3.mqtt_slot_host[i], sizeof(_mqtt_prefs.mqtt_slot_host[i]));
|
|
_mqtt_prefs.mqtt_slot_port[i] = old3.mqtt_slot_port[i];
|
|
memcpy(_mqtt_prefs.mqtt_slot_username[i], old3.mqtt_slot_username[i], sizeof(_mqtt_prefs.mqtt_slot_username[i]));
|
|
memcpy(_mqtt_prefs.mqtt_slot_password[i], old3.mqtt_slot_password[i], sizeof(_mqtt_prefs.mqtt_slot_password[i]));
|
|
memcpy(_mqtt_prefs.mqtt_slot_token[i], old3.mqtt_slot_token[i], sizeof(_mqtt_prefs.mqtt_slot_token[i]));
|
|
memcpy(_mqtt_prefs.mqtt_slot_topic[i], old3.mqtt_slot_topic[i], sizeof(_mqtt_prefs.mqtt_slot_topic[i]));
|
|
}
|
|
// Slots 3-5 keep defaults ("none") from setMQTTPrefsDefaults()
|
|
|
|
// Copy shared auth fields
|
|
memcpy(_mqtt_prefs.mqtt_owner_public_key, old3.mqtt_owner_public_key, sizeof(_mqtt_prefs.mqtt_owner_public_key));
|
|
memcpy(_mqtt_prefs.mqtt_email, old3.mqtt_email, sizeof(_mqtt_prefs.mqtt_email));
|
|
|
|
// Save migrated prefs in new 6-slot format
|
|
saveMQTTPrefs(fs);
|
|
}
|
|
} else if (file_size > 0) {
|
|
// 6-slot format: read directly
|
|
size_t bytes_to_read = file_size < sizeof(_mqtt_prefs) ? file_size : sizeof(_mqtt_prefs);
|
|
size_t bytes_read = file.read((uint8_t *)&_mqtt_prefs, bytes_to_read);
|
|
file.close();
|
|
if (bytes_read != bytes_to_read) {
|
|
setMQTTPrefsDefaults(&_mqtt_prefs);
|
|
}
|
|
} else {
|
|
file.close();
|
|
setMQTTPrefsDefaults(&_mqtt_prefs);
|
|
}
|
|
}
|
|
} else {
|
|
// No /mqtt_prefs file — defaults already set
|
|
// (Legacy /com_prefs migration removed: the old offset-based approach was fragile
|
|
// and the pre-MQTT firmware never wrote MQTT fields to /com_prefs anyway.)
|
|
}
|
|
}
|
|
|
|
void CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
fs->remove("/mqtt_prefs");
|
|
File file = fs->open("/mqtt_prefs", FILE_O_WRITE);
|
|
#elif defined(RP2040_PLATFORM)
|
|
File file = fs->open("/mqtt_prefs", "w");
|
|
#else
|
|
File file = fs->open("/mqtt_prefs", "w", true);
|
|
#endif
|
|
if (file) {
|
|
file.write((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs));
|
|
file.close();
|
|
}
|
|
}
|
|
|
|
void CommonCLI::syncMQTTPrefsToNodePrefs() {
|
|
// Copy MQTT prefs to NodePrefs so existing code can access them
|
|
// Use StrHelper::strncpy to ensure proper null termination
|
|
StrHelper::strncpy(_prefs->mqtt_origin, _mqtt_prefs.mqtt_origin, sizeof(_prefs->mqtt_origin));
|
|
StrHelper::strncpy(_prefs->mqtt_iata, _mqtt_prefs.mqtt_iata, sizeof(_prefs->mqtt_iata));
|
|
_prefs->mqtt_status_enabled = _mqtt_prefs.mqtt_status_enabled;
|
|
_prefs->mqtt_packets_enabled = _mqtt_prefs.mqtt_packets_enabled;
|
|
_prefs->mqtt_raw_enabled = _mqtt_prefs.mqtt_raw_enabled;
|
|
_prefs->mqtt_tx_enabled = _mqtt_prefs.mqtt_tx_enabled;
|
|
_prefs->mqtt_rx_enabled = _mqtt_prefs.mqtt_rx_enabled;
|
|
_prefs->mqtt_status_interval = _mqtt_prefs.mqtt_status_interval;
|
|
StrHelper::strncpy(_prefs->wifi_ssid, _mqtt_prefs.wifi_ssid, sizeof(_prefs->wifi_ssid));
|
|
StrHelper::strncpy(_prefs->wifi_password, _mqtt_prefs.wifi_password, sizeof(_prefs->wifi_password));
|
|
_prefs->wifi_power_save = _mqtt_prefs.wifi_power_save;
|
|
StrHelper::strncpy(_prefs->timezone_string, _mqtt_prefs.timezone_string, sizeof(_prefs->timezone_string));
|
|
_prefs->timezone_offset = _mqtt_prefs.timezone_offset;
|
|
// Slot-based fields
|
|
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_preset[i], _mqtt_prefs.mqtt_slot_preset[i], sizeof(_prefs->mqtt_slot_preset[i]));
|
|
StrHelper::strncpy(_prefs->mqtt_slot_host[i], _mqtt_prefs.mqtt_slot_host[i], sizeof(_prefs->mqtt_slot_host[i]));
|
|
_prefs->mqtt_slot_port[i] = _mqtt_prefs.mqtt_slot_port[i];
|
|
StrHelper::strncpy(_prefs->mqtt_slot_username[i], _mqtt_prefs.mqtt_slot_username[i], sizeof(_prefs->mqtt_slot_username[i]));
|
|
StrHelper::strncpy(_prefs->mqtt_slot_password[i], _mqtt_prefs.mqtt_slot_password[i], sizeof(_prefs->mqtt_slot_password[i]));
|
|
StrHelper::strncpy(_prefs->mqtt_slot_token[i], _mqtt_prefs.mqtt_slot_token[i], sizeof(_prefs->mqtt_slot_token[i]));
|
|
StrHelper::strncpy(_prefs->mqtt_slot_topic[i], _mqtt_prefs.mqtt_slot_topic[i], sizeof(_prefs->mqtt_slot_topic[i]));
|
|
StrHelper::strncpy(_prefs->mqtt_slot_audience[i], _mqtt_prefs.mqtt_slot_audience[i], sizeof(_prefs->mqtt_slot_audience[i]));
|
|
}
|
|
StrHelper::strncpy(_prefs->mqtt_owner_public_key, _mqtt_prefs.mqtt_owner_public_key, sizeof(_prefs->mqtt_owner_public_key));
|
|
StrHelper::strncpy(_prefs->mqtt_email, _mqtt_prefs.mqtt_email, sizeof(_prefs->mqtt_email));
|
|
}
|
|
|
|
void CommonCLI::syncNodePrefsToMQTTPrefs() {
|
|
// Copy NodePrefs to MQTT prefs (used when saving after changes via CLI)
|
|
// Use StrHelper::strncpy to ensure proper null termination
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_origin, _prefs->mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin));
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_iata, _prefs->mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata));
|
|
_mqtt_prefs.mqtt_status_enabled = _prefs->mqtt_status_enabled;
|
|
_mqtt_prefs.mqtt_packets_enabled = _prefs->mqtt_packets_enabled;
|
|
_mqtt_prefs.mqtt_raw_enabled = _prefs->mqtt_raw_enabled;
|
|
_mqtt_prefs.mqtt_tx_enabled = _prefs->mqtt_tx_enabled;
|
|
_mqtt_prefs.mqtt_rx_enabled = _prefs->mqtt_rx_enabled;
|
|
_mqtt_prefs.mqtt_status_interval = _prefs->mqtt_status_interval;
|
|
StrHelper::strncpy(_mqtt_prefs.wifi_ssid, _prefs->wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid));
|
|
StrHelper::strncpy(_mqtt_prefs.wifi_password, _prefs->wifi_password, sizeof(_mqtt_prefs.wifi_password));
|
|
_mqtt_prefs.wifi_power_save = _prefs->wifi_power_save;
|
|
StrHelper::strncpy(_mqtt_prefs.timezone_string, _prefs->timezone_string, sizeof(_mqtt_prefs.timezone_string));
|
|
_mqtt_prefs.timezone_offset = _prefs->timezone_offset;
|
|
// Slot-based fields
|
|
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[i], _prefs->mqtt_slot_preset[i], sizeof(_mqtt_prefs.mqtt_slot_preset[i]));
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_host[i], _prefs->mqtt_slot_host[i], sizeof(_mqtt_prefs.mqtt_slot_host[i]));
|
|
_mqtt_prefs.mqtt_slot_port[i] = _prefs->mqtt_slot_port[i];
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_username[i], _prefs->mqtt_slot_username[i], sizeof(_mqtt_prefs.mqtt_slot_username[i]));
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_password[i], _prefs->mqtt_slot_password[i], sizeof(_mqtt_prefs.mqtt_slot_password[i]));
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_token[i], _prefs->mqtt_slot_token[i], sizeof(_mqtt_prefs.mqtt_slot_token[i]));
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_topic[i], _prefs->mqtt_slot_topic[i], sizeof(_mqtt_prefs.mqtt_slot_topic[i]));
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_audience[i], _prefs->mqtt_slot_audience[i], sizeof(_mqtt_prefs.mqtt_slot_audience[i]));
|
|
}
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_owner_public_key, _prefs->mqtt_owner_public_key, sizeof(_mqtt_prefs.mqtt_owner_public_key));
|
|
StrHelper::strncpy(_mqtt_prefs.mqtt_email, _prefs->mqtt_email, sizeof(_mqtt_prefs.mqtt_email));
|
|
}
|
|
#endif
|
|
|
|
#define MIN_LOCAL_ADVERT_INTERVAL 60
|
|
|
|
void CommonCLI::savePrefs() {
|
|
uint8_t old_advert_interval = _prefs->advert_interval;
|
|
if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) {
|
|
_prefs->advert_interval = 0; // turn it off, now that device has been manually configured
|
|
}
|
|
// If advert_interval was changed, update the timer to reflect the change
|
|
if (old_advert_interval != _prefs->advert_interval) {
|
|
_callbacks->updateAdvertTimer();
|
|
}
|
|
_callbacks->savePrefs();
|
|
}
|
|
|
|
uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
|
|
if (_prefs->advert_loc_policy == ADVERT_LOC_NONE) {
|
|
AdvertDataBuilder builder(node_type, _prefs->node_name);
|
|
return builder.encodeTo(app_data);
|
|
} else if (_prefs->advert_loc_policy == ADVERT_LOC_SHARE) {
|
|
AdvertDataBuilder builder(node_type, _prefs->node_name, _sensors->node_lat, _sensors->node_lon);
|
|
return builder.encodeTo(app_data);
|
|
} else {
|
|
AdvertDataBuilder builder(node_type, _prefs->node_name, _prefs->node_lat, _prefs->node_lon);
|
|
return builder.encodeTo(app_data);
|
|
}
|
|
}
|
|
|
|
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
|
if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) {
|
|
_board->powerOff(); // doesn't return
|
|
} else if (memcmp(command, "reboot", 6) == 0) {
|
|
_board->reboot(); // doesn't return
|
|
} else if (memcmp(command, "clkreboot", 9) == 0) {
|
|
// Reset clock
|
|
getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm
|
|
_board->reboot(); // doesn't return
|
|
} else if (memcmp(command, "advert.zerohop", 14) == 0 && (command[14] == 0 || command[14] == ' ')) {
|
|
// send zerohop advert
|
|
_callbacks->sendSelfAdvertisement(1500, false); // longer delay, give CLI response time to be sent first
|
|
strcpy(reply, "OK - zerohop advert sent");
|
|
} else if (memcmp(command, "advert", 6) == 0) {
|
|
// send flood advert
|
|
_callbacks->sendSelfAdvertisement(1500, true); // longer delay, give CLI response time to be sent first
|
|
strcpy(reply, "OK - Advert sent");
|
|
} else if (memcmp(command, "clock sync", 10) == 0) {
|
|
uint32_t curr = getRTCClock()->getCurrentTime();
|
|
if (sender_timestamp > curr) {
|
|
getRTCClock()->setCurrentTime(sender_timestamp + 1);
|
|
uint32_t now = getRTCClock()->getCurrentTime();
|
|
DateTime dt = DateTime(now);
|
|
sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
|
|
} else {
|
|
strcpy(reply, "ERR: clock cannot go backwards");
|
|
}
|
|
} else if (memcmp(command, "memory", 6) == 0) {
|
|
sprintf(reply, "Free: %d, Min: %d, Max: %d, Queue: %d, IntFree: %d, IntMax: %d, PSRAM: %d/%d",
|
|
ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap(),
|
|
_callbacks->getQueueSize(),
|
|
(int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
|
|
(int)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL),
|
|
(int)heap_caps_get_free_size(MALLOC_CAP_SPIRAM),
|
|
(int)heap_caps_get_total_size(MALLOC_CAP_SPIRAM));
|
|
} else if (memcmp(command, "tls.bundletest ", 15) == 0) {
|
|
#ifdef ESP_PLATFORM
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
strcpy(reply, "ERR: WiFi not connected");
|
|
} else {
|
|
size_t bundle_len = 0;
|
|
if (rootca_crt_bundle_start != nullptr &&
|
|
rootca_crt_bundle_end != nullptr &&
|
|
rootca_crt_bundle_end > rootca_crt_bundle_start) {
|
|
bundle_len = static_cast<size_t>(rootca_crt_bundle_end - rootca_crt_bundle_start);
|
|
}
|
|
if (bundle_len == 0) {
|
|
strcpy(reply, "ERR: no embedded cert bundle");
|
|
} else {
|
|
char host[96];
|
|
uint16_t port = 443;
|
|
if (!parseTlsBundleTarget(command + 15, host, sizeof(host), &port)) {
|
|
strcpy(reply, "ERR: usage tls.bundletest <host[:port]|url>");
|
|
} else {
|
|
WiFiClientSecure client;
|
|
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
|
client.setCACertBundle(rootca_crt_bundle_start, bundle_len);
|
|
#else
|
|
client.setCACertBundle(rootca_crt_bundle_start);
|
|
#endif
|
|
client.setTimeout(8000);
|
|
bool ok = client.connect(host, port);
|
|
if (ok) {
|
|
client.stop();
|
|
snprintf(reply, 160, "OK: TLS bundle verified %s:%u", host, (unsigned)port);
|
|
} else {
|
|
snprintf(reply, 160, "ERR: TLS bundle failed %s:%u", host, (unsigned)port);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
strcpy(reply, "ERR: unsupported on this platform");
|
|
#endif
|
|
} else if (memcmp(command, "start ota", 9) == 0) {
|
|
if (!_board->startOTAUpdate(_prefs->node_name, reply)) {
|
|
strcpy(reply, "Error");
|
|
}
|
|
} else if (memcmp(command, "clock", 5) == 0) {
|
|
uint32_t now = getRTCClock()->getCurrentTime();
|
|
DateTime dt = DateTime(now);
|
|
sprintf(reply, "%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
|
|
} else if (memcmp(command, "time ", 5) == 0) { // set time (to epoch seconds)
|
|
uint32_t secs = _atoi(&command[5]);
|
|
uint32_t curr = getRTCClock()->getCurrentTime();
|
|
if (secs > curr) {
|
|
getRTCClock()->setCurrentTime(secs);
|
|
uint32_t now = getRTCClock()->getCurrentTime();
|
|
DateTime dt = DateTime(now);
|
|
sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
|
|
} else {
|
|
strcpy(reply, "(ERR: clock cannot go backwards)");
|
|
}
|
|
} else if (memcmp(command, "neighbors", 9) == 0) {
|
|
_callbacks->formatNeighborsReply(reply);
|
|
} else if (memcmp(command, "neighbor.remove ", 16) == 0) {
|
|
const char* hex = &command[16];
|
|
uint8_t pubkey[PUB_KEY_SIZE];
|
|
int hex_len = min((int)strlen(hex), PUB_KEY_SIZE*2);
|
|
int pubkey_len = hex_len / 2;
|
|
if (mesh::Utils::fromHex(pubkey, pubkey_len, hex)) {
|
|
_callbacks->removeNeighbor(pubkey, pubkey_len);
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "ERR: bad pubkey");
|
|
}
|
|
} else if (memcmp(command, "tempradio ", 10) == 0) {
|
|
strcpy(tmp, &command[10]);
|
|
const char *parts[5];
|
|
int num = mesh::Utils::parseTextParts(tmp, parts, 5);
|
|
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
|
|
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
|
|
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
|
|
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
|
|
int temp_timeout_mins = num > 4 ? atoi(parts[4]) : 0;
|
|
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f && temp_timeout_mins > 0) {
|
|
_callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins);
|
|
sprintf(reply, "OK - temp params for %d mins", temp_timeout_mins);
|
|
} else {
|
|
strcpy(reply, "Error, invalid params");
|
|
}
|
|
} else if (memcmp(command, "password ", 9) == 0) {
|
|
// change admin password
|
|
StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password));
|
|
savePrefs();
|
|
sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!!
|
|
} else if (memcmp(command, "clear stats", 11) == 0) {
|
|
_callbacks->clearStats();
|
|
strcpy(reply, "(OK - stats reset)");
|
|
} else if (memcmp(command, "alert test", 10) == 0 && (command[10] == 0 || command[10] == ' ')) {
|
|
// Send a one-off test alert on the configured alert channel.
|
|
const char* extra = command[10] == ' ' ? &command[11] : "";
|
|
char text[120];
|
|
if (*extra) {
|
|
snprintf(text, sizeof(text), "[test] %s", extra);
|
|
} else {
|
|
strcpy(text, "[test] alert channel ok");
|
|
}
|
|
if (!_prefs->alert_psk_hex[0]) {
|
|
strcpy(reply, "Error: alert channel not configured (set alert.psk or set alert.hashtag)");
|
|
} else {
|
|
bool ok = _callbacks->sendAlertText(text);
|
|
strcpy(reply, ok ? "OK - alert sent" : "Error: alert send failed (bad PSK or PUBLIC key refused?)");
|
|
}
|
|
} else if (memcmp(command, "get ", 4) == 0) {
|
|
handleGetCmd(sender_timestamp, command, reply);
|
|
} else if (memcmp(command, "set ", 4) == 0) {
|
|
handleSetCmd(sender_timestamp, command, reply);
|
|
} else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) {
|
|
bool s = _callbacks->formatFileSystem();
|
|
sprintf(reply, "File system erase: %s", s ? "OK" : "Err");
|
|
} else if (memcmp(command, "ver", 3) == 0) {
|
|
sprintf(reply, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate());
|
|
} else if (memcmp(command, "board", 5) == 0) {
|
|
sprintf(reply, "%s", _board->getManufacturerName());
|
|
} else if (memcmp(command, "sensor get ", 11) == 0) {
|
|
const char* key = command + 11;
|
|
const char* val = _sensors->getSettingByKey(key);
|
|
if (val != NULL) {
|
|
sprintf(reply, "> %s", val);
|
|
} else {
|
|
strcpy(reply, "null");
|
|
}
|
|
} else if (memcmp(command, "sensor set ", 11) == 0) {
|
|
strcpy(tmp, &command[11]);
|
|
const char *parts[2];
|
|
int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' ');
|
|
const char *key = (num > 0) ? parts[0] : "";
|
|
const char *value = (num > 1) ? parts[1] : "null";
|
|
if (_sensors->setSettingValue(key, value)) {
|
|
strcpy(reply, "ok");
|
|
} else {
|
|
strcpy(reply, "can't find custom var");
|
|
}
|
|
} else if (memcmp(command, "sensor list", 11) == 0) {
|
|
char* dp = reply;
|
|
int start = 0;
|
|
int end = _sensors->getNumSettings();
|
|
if (strlen(command) > 11) {
|
|
start = _atoi(command+12);
|
|
}
|
|
if (start >= end) {
|
|
strcpy(reply, "no custom var");
|
|
} else {
|
|
sprintf(dp, "%d vars\n", end);
|
|
dp = strchr(dp, 0);
|
|
int i;
|
|
for (i = start; i < end && (dp-reply < 134); i++) {
|
|
sprintf(dp, "%s=%s\n",
|
|
_sensors->getSettingName(i),
|
|
_sensors->getSettingValue(i));
|
|
dp = strchr(dp, 0);
|
|
}
|
|
if (i < end) {
|
|
sprintf(dp, "... next:%d", i);
|
|
} else {
|
|
*(dp-1) = 0; // remove last CR
|
|
}
|
|
}
|
|
} else if (memcmp(command, "region", 6) == 0) {
|
|
handleRegionCmd(command, reply);
|
|
#if ENV_INCLUDE_GPS == 1
|
|
} else if (memcmp(command, "gps on", 6) == 0) {
|
|
if (_sensors->setSettingValue("gps", "1")) {
|
|
_prefs->gps_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "ok");
|
|
} else {
|
|
strcpy(reply, "gps toggle not found");
|
|
}
|
|
} else if (memcmp(command, "gps off", 7) == 0) {
|
|
if (_sensors->setSettingValue("gps", "0")) {
|
|
_prefs->gps_enabled = 0;
|
|
savePrefs();
|
|
strcpy(reply, "ok");
|
|
} else {
|
|
strcpy(reply, "gps toggle not found");
|
|
}
|
|
} else if (memcmp(command, "gps sync", 8) == 0) {
|
|
LocationProvider * l = _sensors->getLocationProvider();
|
|
if (l != NULL) {
|
|
l->syncTime();
|
|
strcpy(reply, "ok");
|
|
} else {
|
|
strcpy(reply, "gps provider not found");
|
|
}
|
|
} else if (memcmp(command, "gps setloc", 10) == 0) {
|
|
_prefs->node_lat = _sensors->node_lat;
|
|
_prefs->node_lon = _sensors->node_lon;
|
|
savePrefs();
|
|
strcpy(reply, "ok");
|
|
} else if (memcmp(command, "gps advert", 10) == 0) {
|
|
if (strlen(command) == 10) {
|
|
switch (_prefs->advert_loc_policy) {
|
|
case ADVERT_LOC_NONE:
|
|
strcpy(reply, "> none");
|
|
break;
|
|
case ADVERT_LOC_PREFS:
|
|
strcpy(reply, "> prefs");
|
|
break;
|
|
case ADVERT_LOC_SHARE:
|
|
strcpy(reply, "> share");
|
|
break;
|
|
default:
|
|
strcpy(reply, "error");
|
|
}
|
|
} else if (memcmp(command+11, "none", 4) == 0) {
|
|
_prefs->advert_loc_policy = ADVERT_LOC_NONE;
|
|
savePrefs();
|
|
strcpy(reply, "ok");
|
|
} else if (memcmp(command+11, "share", 5) == 0) {
|
|
_prefs->advert_loc_policy = ADVERT_LOC_SHARE;
|
|
savePrefs();
|
|
strcpy(reply, "ok");
|
|
} else if (memcmp(command+11, "prefs", 5) == 0) {
|
|
_prefs->advert_loc_policy = ADVERT_LOC_PREFS;
|
|
savePrefs();
|
|
strcpy(reply, "ok");
|
|
} else {
|
|
strcpy(reply, "error");
|
|
}
|
|
} else if (memcmp(command, "gps", 3) == 0) {
|
|
LocationProvider * l = _sensors->getLocationProvider();
|
|
if (l != NULL) {
|
|
bool enabled = l->isEnabled(); // is EN pin on ?
|
|
bool fix = l->isValid(); // has fix ?
|
|
int sats = l->satellitesCount();
|
|
bool active = !strcmp(_sensors->getSettingByKey("gps"), "1");
|
|
if (enabled) {
|
|
sprintf(reply, "on, %s, %s, %d sats",
|
|
active?"active":"deactivated",
|
|
fix?"fix":"no fix",
|
|
sats);
|
|
} else {
|
|
strcpy(reply, "off");
|
|
}
|
|
} else {
|
|
strcpy(reply, "Can't find GPS");
|
|
}
|
|
#endif
|
|
} else if (memcmp(command, "powersaving on", 14) == 0) {
|
|
#if defined(NRF52_PLATFORM)
|
|
_prefs->powersaving_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "on - Immediate effect");
|
|
#elif defined(ESP32) && !defined(WITH_BRIDGE)
|
|
_prefs->powersaving_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "on - After 2 minutes");
|
|
#elif defined(WITH_BRIDGE)
|
|
strcpy(reply, "Bridge not supported");
|
|
#else
|
|
strcpy(reply, "Board not supported");
|
|
#endif
|
|
} else if (memcmp(command, "powersaving off", 15) == 0) {
|
|
_prefs->powersaving_enabled = 0;
|
|
savePrefs();
|
|
strcpy(reply, "off");
|
|
} else if (memcmp(command, "powersaving", 11) == 0) {
|
|
if (_prefs->powersaving_enabled) {
|
|
strcpy(reply, "on");
|
|
} else {
|
|
strcpy(reply, "off");
|
|
}
|
|
} else if (memcmp(command, "log start", 9) == 0) {
|
|
_callbacks->setLoggingOn(true);
|
|
strcpy(reply, " logging on");
|
|
} else if (memcmp(command, "log stop", 8) == 0) {
|
|
_callbacks->setLoggingOn(false);
|
|
strcpy(reply, " logging off");
|
|
} else if (memcmp(command, "log erase", 9) == 0) {
|
|
_callbacks->eraseLogFile();
|
|
strcpy(reply, " log erased");
|
|
} else if (sender_timestamp == 0 && memcmp(command, "log", 3) == 0) {
|
|
_callbacks->dumpLogFile();
|
|
strcpy(reply, " EOF");
|
|
} else if (sender_timestamp == 0 && memcmp(command, "stats-packets", 13) == 0 && (command[13] == 0 || command[13] == ' ')) {
|
|
_callbacks->formatPacketStatsReply(reply);
|
|
} else if (sender_timestamp == 0 && memcmp(command, "stats-radio-diag", 16) == 0 && (command[16] == 0 || command[16] == ' ')) {
|
|
_callbacks->formatRadioDiagReply(reply);
|
|
} else if (sender_timestamp == 0 && memcmp(command, "stats-radio", 11) == 0 && (command[11] == 0 || command[11] == ' ')) {
|
|
_callbacks->formatRadioStatsReply(reply);
|
|
} else if (sender_timestamp == 0 && memcmp(command, "stats-core", 10) == 0 && (command[10] == 0 || command[10] == ' ')) {
|
|
_callbacks->formatStatsReply(reply);
|
|
} else {
|
|
strcpy(reply, "Unknown command");
|
|
}
|
|
}
|
|
|
|
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
|
const char* config = &command[4];
|
|
if (memcmp(config, "dutycycle ", 10) == 0) {
|
|
float dc = atof(&config[10]);
|
|
if (dc < 1 || dc > 100) {
|
|
strcpy(reply, "ERROR: dutycycle must be 1-100");
|
|
} else {
|
|
_prefs->airtime_factor = (100.0f / dc) - 1.0f;
|
|
savePrefs();
|
|
float actual = 100.0f / (_prefs->airtime_factor + 1.0f);
|
|
int a_int = (int)actual;
|
|
int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
|
|
sprintf(reply, "OK - %d.%d%%", a_int, a_frac);
|
|
}
|
|
} else if (memcmp(config, "af ", 3) == 0) {
|
|
_prefs->airtime_factor = atof(&config[3]);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "int.thresh ", 11) == 0) {
|
|
_prefs->interference_threshold = atoi(&config[11]);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "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, "radio.watchdog ", 15) == 0) {
|
|
const char* val = &config[15];
|
|
if (*val == 0) {
|
|
strcpy(reply, "Error: missing radio.watchdog minutes");
|
|
return;
|
|
}
|
|
for (const char* sp = val; *sp; sp++) {
|
|
if (*sp < '0' || *sp > '9') {
|
|
strcpy(reply, "Error: radio.watchdog must be an integer 0-120");
|
|
return;
|
|
}
|
|
}
|
|
int mins = atoi(val);
|
|
if (mins > 120) {
|
|
strcpy(reply, "Error: radio.watchdog must be 0-120 minutes");
|
|
} else {
|
|
_prefs->radio_watchdog_minutes = (uint8_t)mins;
|
|
savePrefs();
|
|
if (mins == 0) {
|
|
strcpy(reply, "OK - radio watchdog disabled");
|
|
} else {
|
|
sprintf(reply, "OK - radio watchdog %d min", mins);
|
|
}
|
|
}
|
|
} else if (memcmp(config, "multi.acks ", 11) == 0) {
|
|
_prefs->multi_acks = atoi(&config[11]);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "allow.read.only ", 16) == 0) {
|
|
_prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
|
|
int hours = _atoi(&config[22]);
|
|
if ((hours > 0 && hours < 3) || (hours > 168)) {
|
|
strcpy(reply, "Error: interval range is 3-168 hours");
|
|
} else {
|
|
_prefs->flood_advert_interval = (uint8_t)(hours);
|
|
_callbacks->updateFloodAdvertTimer();
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else if (memcmp(config, "advert.interval ", 16) == 0) {
|
|
int mins = _atoi(&config[16]);
|
|
if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) {
|
|
sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL);
|
|
} else {
|
|
_prefs->advert_interval = (uint8_t)(mins / 2);
|
|
_callbacks->updateAdvertTimer();
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else if (memcmp(config, "guest.password ", 15) == 0) {
|
|
StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "prv.key ", 8) == 0) {
|
|
uint8_t prv_key[PRV_KEY_SIZE];
|
|
bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
|
|
// only allow rekey if key is valid
|
|
if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) {
|
|
mesh::LocalIdentity new_id;
|
|
new_id.readFrom(prv_key, PRV_KEY_SIZE);
|
|
_callbacks->saveIdentity(new_id);
|
|
strcpy(reply, "OK, reboot to apply! New pubkey: ");
|
|
mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE);
|
|
} else {
|
|
strcpy(reply, "Error, bad key");
|
|
}
|
|
} else if (memcmp(config, "name ", 5) == 0) {
|
|
if (isValidName(&config[5])) {
|
|
StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, bad chars");
|
|
}
|
|
} else if (memcmp(config, "repeat ", 7) == 0) {
|
|
_prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
|
|
savePrefs();
|
|
strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
|
|
#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110)
|
|
} else if (memcmp(config, "radio.rxgain ", 13) == 0) {
|
|
_prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0;
|
|
strcpy(reply, "OK");
|
|
savePrefs();
|
|
_callbacks->setRxBoostedGain(_prefs->rx_boosted_gain);
|
|
#endif
|
|
} else if (memcmp(config, "radio ", 6) == 0) {
|
|
strcpy(tmp, &config[6]);
|
|
const char *parts[4];
|
|
int num = mesh::Utils::parseTextParts(tmp, parts, 4);
|
|
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
|
|
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
|
|
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
|
|
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
|
|
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
|
|
_prefs->sf = sf;
|
|
_prefs->cr = cr;
|
|
_prefs->freq = freq;
|
|
_prefs->bw = bw;
|
|
_callbacks->savePrefs();
|
|
strcpy(reply, "OK - reboot to apply");
|
|
} else {
|
|
strcpy(reply, "Error, invalid radio params");
|
|
}
|
|
} else if (memcmp(config, "lat ", 4) == 0) {
|
|
_prefs->node_lat = atof(&config[4]);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "lon ", 4) == 0) {
|
|
_prefs->node_lon = atof(&config[4]);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "rxdelay ", 8) == 0) {
|
|
float db = atof(&config[8]);
|
|
if (db >= 0 && db <= 20.0f) {
|
|
_prefs->rx_delay_base = db;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 0-20");
|
|
}
|
|
} else if (memcmp(config, "txdelay ", 8) == 0) {
|
|
float f = atof(&config[8]);
|
|
if (f >= 0 && f <= 2.0f) {
|
|
_prefs->tx_delay_factor = f;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 0-2");
|
|
}
|
|
} else if (memcmp(config, "flood.max.unscoped ", 19) == 0) {
|
|
uint8_t m = atoi(&config[19]);
|
|
if (m <= 64) {
|
|
_prefs->flood_max_unscoped = m;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, max 64");
|
|
}
|
|
} else if (memcmp(config, "flood.max.advert ", 17) == 0) {
|
|
uint8_t m = atoi(&config[17]);
|
|
if (m <= 64) {
|
|
_prefs->flood_max_advert = m;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, max 64");
|
|
}
|
|
} else if (memcmp(config, "flood.max ", 10) == 0) {
|
|
uint8_t m = atoi(&config[10]);
|
|
if (m <= 64) {
|
|
_prefs->flood_max = m;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, max 64");
|
|
}
|
|
} else if (memcmp(config, "direct.txdelay ", 15) == 0) {
|
|
float f = atof(&config[15]);
|
|
if (f >= 0 && f <= 2.0f) {
|
|
_prefs->direct_tx_delay_factor = f;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 0-2");
|
|
}
|
|
} else if (memcmp(config, "owner.info ", 11) == 0) {
|
|
config += 11;
|
|
char *dp = _prefs->owner_info;
|
|
while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) {
|
|
*dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars
|
|
config++;
|
|
}
|
|
*dp = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "path.hash.mode ", 15) == 0) {
|
|
config += 15;
|
|
uint8_t mode = atoi(config);
|
|
if (mode < 3) {
|
|
_prefs->path_hash_mode = mode;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, must be 0,1, or 2");
|
|
}
|
|
} else if (memcmp(config, "loop.detect ", 12) == 0) {
|
|
config += 12;
|
|
uint8_t mode;
|
|
if (memcmp(config, "off", 3) == 0) {
|
|
mode = LOOP_DETECT_OFF;
|
|
} else if (memcmp(config, "minimal", 7) == 0) {
|
|
mode = LOOP_DETECT_MINIMAL;
|
|
} else if (memcmp(config, "moderate", 8) == 0) {
|
|
mode = LOOP_DETECT_MODERATE;
|
|
} else if (memcmp(config, "strict", 6) == 0) {
|
|
mode = LOOP_DETECT_STRICT;
|
|
} else {
|
|
mode = 0xFF;
|
|
strcpy(reply, "Error, must be: off, minimal, moderate, or strict");
|
|
}
|
|
if (mode != 0xFF) {
|
|
_prefs->loop_detect = mode;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else if (memcmp(config, "snmp.community ", 15) == 0) {
|
|
StrHelper::strncpy(_prefs->snmp_community, &config[15], sizeof(_prefs->snmp_community));
|
|
savePrefs();
|
|
strcpy(reply, "OK - restart to apply");
|
|
} else if (memcmp(config, "snmp ", 5) == 0) {
|
|
_prefs->snmp_enabled = memcmp(&config[5], "on", 2) == 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK - restart to apply");
|
|
} else if (memcmp(config, "tx ", 3) == 0) {
|
|
_prefs->tx_power_dbm = atoi(&config[3]);
|
|
savePrefs();
|
|
_callbacks->setTxPower(_prefs->tx_power_dbm);
|
|
strcpy(reply, "OK");
|
|
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
|
|
_prefs->freq = atof(&config[5]);
|
|
savePrefs();
|
|
strcpy(reply, "OK - reboot to apply");
|
|
#ifdef WITH_BRIDGE
|
|
} else if (memcmp(config, "bridge.enabled ", 15) == 0) {
|
|
_prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0;
|
|
_callbacks->setBridgeState(_prefs->bridge_enabled);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "bridge.delay ", 13) == 0) {
|
|
int delay = _atoi(&config[13]);
|
|
if (delay >= 0 && delay <= 10000) {
|
|
_prefs->bridge_delay = (uint16_t)delay;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: delay must be between 0-10000 ms");
|
|
}
|
|
} else if (memcmp(config, "bridge.source ", 14) == 0) {
|
|
_prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0;
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
if (_prefs->bridge_pkt_src == 1) {
|
|
_prefs->mqtt_rx_enabled = 1;
|
|
_prefs->mqtt_tx_enabled = 0;
|
|
} else {
|
|
_prefs->mqtt_rx_enabled = 0;
|
|
_prefs->mqtt_tx_enabled = 1;
|
|
}
|
|
#endif
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
#endif
|
|
#ifdef WITH_RS232_BRIDGE
|
|
} else if (memcmp(config, "bridge.baud ", 12) == 0) {
|
|
uint32_t baud = atoi(&config[12]);
|
|
if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) {
|
|
_prefs->bridge_baud = (uint32_t)baud;
|
|
_callbacks->restartBridge();
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD);
|
|
}
|
|
#endif
|
|
#ifdef WITH_ESPNOW_BRIDGE
|
|
} else if (memcmp(config, "bridge.channel ", 15) == 0) {
|
|
int ch = atoi(&config[15]);
|
|
if (ch > 0 && ch < 15) {
|
|
_prefs->bridge_channel = (uint8_t)ch;
|
|
_callbacks->restartBridge();
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: channel must be between 1-14");
|
|
}
|
|
} else if (memcmp(config, "bridge.secret ", 14) == 0) {
|
|
StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret));
|
|
_callbacks->restartBridge();
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
#endif
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
} else if (strcmp(config, "mqtt.origin") == 0) {
|
|
_prefs->mqtt_origin[0] = '\0';
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "mqtt.origin ", 12) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_origin, &config[12], sizeof(_prefs->mqtt_origin));
|
|
StrHelper::stripSurroundingQuotes(_prefs->mqtt_origin, sizeof(_prefs->mqtt_origin));
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "mqtt.iata ", 10) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_iata, &config[10], sizeof(_prefs->mqtt_iata));
|
|
for (int i = 0; _prefs->mqtt_iata[i]; i++) {
|
|
_prefs->mqtt_iata[i] = toupper(_prefs->mqtt_iata[i]);
|
|
}
|
|
savePrefs();
|
|
_callbacks->restartBridge();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "mqtt.status ", 12) == 0) {
|
|
_prefs->mqtt_status_enabled = memcmp(&config[12], "on", 2) == 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "mqtt.packets ", 13) == 0) {
|
|
_prefs->mqtt_packets_enabled = memcmp(&config[13], "on", 2) == 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "mqtt.raw ", 9) == 0) {
|
|
_prefs->mqtt_raw_enabled = memcmp(&config[9], "on", 2) == 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "mqtt.tx ", 8) == 0) {
|
|
if (memcmp(&config[8], "advert", 6) == 0) {
|
|
_prefs->mqtt_tx_enabled = 2;
|
|
} else {
|
|
_prefs->mqtt_tx_enabled = memcmp(&config[8], "on", 2) == 0 ? 1 : 0;
|
|
}
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "mqtt.rx ", 8) == 0) {
|
|
_prefs->mqtt_rx_enabled = memcmp(&config[8], "on", 2) == 0 ? 1 : 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "mqtt.interval ", 14) == 0) {
|
|
uint32_t minutes = _atoi(&config[14]);
|
|
if (minutes >= 1 && minutes <= 60) {
|
|
_prefs->mqtt_status_interval = minutes * 60000;
|
|
savePrefs();
|
|
_callbacks->restartBridge();
|
|
sprintf(reply, "OK - interval set to %u minutes (%lu ms), bridge restarted", minutes, (unsigned long)_prefs->mqtt_status_interval);
|
|
} else {
|
|
strcpy(reply, "Error: interval must be between 1-60 minutes");
|
|
}
|
|
} else if (memcmp(config, "wifi.ssid ", 10) == 0) {
|
|
StrHelper::strncpy(_prefs->wifi_ssid, &config[10], sizeof(_prefs->wifi_ssid));
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "wifi.pwd ", 9) == 0) {
|
|
StrHelper::strncpy(_prefs->wifi_password, &config[9], sizeof(_prefs->wifi_password));
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "wifi.powersave ", 15) == 0) {
|
|
const char* value = &config[15];
|
|
uint8_t ps_value;
|
|
bool valid = false;
|
|
if (memcmp(value, "min", 3) == 0 && (value[3] == 0 || value[3] == ' ')) {
|
|
ps_value = 0;
|
|
valid = true;
|
|
} else if (memcmp(value, "none", 4) == 0 && (value[4] == 0 || value[4] == ' ')) {
|
|
ps_value = 1;
|
|
valid = true;
|
|
} else if (memcmp(value, "max", 3) == 0 && (value[3] == 0 || value[3] == ' ')) {
|
|
ps_value = 2;
|
|
valid = true;
|
|
}
|
|
if (!valid) {
|
|
strcpy(reply, "Error: must be none, min, or max");
|
|
} else {
|
|
_prefs->wifi_power_save = ps_value;
|
|
savePrefs();
|
|
#ifdef ESP_PLATFORM
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
wifi_ps_type_t ps_mode = (ps_value == 1) ? WIFI_PS_NONE :
|
|
(ps_value == 2) ? WIFI_PS_MAX_MODEM : WIFI_PS_MIN_MODEM;
|
|
esp_err_t ps_result = esp_wifi_set_ps(ps_mode);
|
|
if (ps_result == ESP_OK) {
|
|
const char* ps_name = (ps_value == 1) ? "none" : (ps_value == 2) ? "max" : "min";
|
|
sprintf(reply, "OK - power save set to %s", ps_name);
|
|
} else {
|
|
sprintf(reply, "OK - saved, but failed to apply: %d", ps_result);
|
|
}
|
|
} else {
|
|
const char* ps_name = (ps_value == 1) ? "none" : (ps_value == 2) ? "max" : "min";
|
|
sprintf(reply, "OK - saved as %s (will apply on next WiFi connection)", ps_name);
|
|
}
|
|
#else
|
|
const char* ps_name = (ps_value == 1) ? "none" : (ps_value == 2) ? "max" : "min";
|
|
sprintf(reply, "OK - saved as %s", ps_name);
|
|
#endif
|
|
}
|
|
} else if (memcmp(config, "timezone ", 9) == 0) {
|
|
StrHelper::strncpy(_prefs->timezone_string, &config[9], sizeof(_prefs->timezone_string));
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "timezone.offset ", 16) == 0) {
|
|
int8_t offset = _atoi(&config[16]);
|
|
if (offset >= -12 && offset <= 14) {
|
|
_prefs->timezone_offset = offset;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: timezone offset must be between -12 and +14");
|
|
}
|
|
} else if (config[0] == 'm' && config[1] == 'q' && config[2] == 't' && config[3] == 't' &&
|
|
config[4] >= '1' && config[4] <= ('0' + MAX_MQTT_SLOTS) && config[5] == '.') {
|
|
// Slot-based commands: set mqtt1.preset <name>, set mqtt1.server <host>, etc.
|
|
int slot = config[4] - '1'; // 0-5
|
|
const char* subcmd = &config[6];
|
|
if (memcmp(subcmd, "preset ", 7) == 0) {
|
|
const char* preset_name = &subcmd[7];
|
|
// Validate preset name
|
|
if (findMQTTPreset(preset_name) != nullptr ||
|
|
strcmp(preset_name, MQTT_PRESET_CUSTOM) == 0 ||
|
|
strcmp(preset_name, MQTT_PRESET_NONE) == 0) {
|
|
// Reject duplicate presets (except "none" and "custom")
|
|
int dup_slot = -1;
|
|
if (findMQTTPreset(preset_name) != nullptr) {
|
|
for (int s = 0; s < MAX_MQTT_SLOTS; s++) {
|
|
if (s != slot && strcmp(_prefs->mqtt_slot_preset[s], preset_name) == 0) {
|
|
dup_slot = s;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (dup_slot >= 0) {
|
|
sprintf(reply, "Error: preset '%s' is already assigned to slot %d", preset_name, dup_slot + 1);
|
|
} else {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], preset_name, sizeof(_prefs->mqtt_slot_preset[slot]));
|
|
savePrefs();
|
|
_callbacks->restartBridgeSlot(slot);
|
|
// Check if the slot has everything it needs to connect
|
|
const MQTTPresetDef* p = findMQTTPreset(preset_name);
|
|
if (p && p->topic_style == MQTT_TOPIC_MESHRANK && _prefs->mqtt_slot_token[slot][0] == '\0') {
|
|
sprintf(reply, "OK - slot %d preset: %s (run 'set mqtt%d.token <your_token>' to connect)", slot + 1, preset_name, slot + 1);
|
|
} else if (p && p->topic_style == MQTT_TOPIC_MESHCORE &&
|
|
(strlen(_prefs->mqtt_iata) == 0 || strcmp(_prefs->mqtt_iata, "XXX") == 0)) {
|
|
sprintf(reply, "OK - slot %d preset: %s (run 'set mqtt.iata <airport_code>' to publish)", slot + 1, preset_name);
|
|
} else if (p && mqttPresetNeedsSlotCredentials(p) &&
|
|
(_prefs->mqtt_slot_username[slot][0] == '\0' ||
|
|
_prefs->mqtt_slot_password[slot][0] == '\0')) {
|
|
sprintf(reply,
|
|
"OK - slot %d preset: %s (run 'set mqtt%d.username <user>' and 'set mqtt%d.password <pass>' to connect)",
|
|
slot + 1, preset_name, slot + 1, slot + 1);
|
|
} else {
|
|
sprintf(reply, "OK - slot %d preset: %s", slot + 1, preset_name);
|
|
}
|
|
}
|
|
} else {
|
|
strcpy(reply, "Error: unknown preset. Use 'get mqtt.presets'");
|
|
}
|
|
} else if (memcmp(subcmd, "server ", 7) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_host[slot], &subcmd[7], sizeof(_prefs->mqtt_slot_host[slot]));
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(subcmd, "port ", 5) == 0) {
|
|
int port = atoi(&subcmd[5]);
|
|
if (port > 0 && port <= 65535) {
|
|
_prefs->mqtt_slot_port[slot] = port;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: port must be between 1 and 65535");
|
|
}
|
|
} else if (memcmp(subcmd, "username ", 9) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_username[slot], &subcmd[9], sizeof(_prefs->mqtt_slot_username[slot]));
|
|
savePrefs();
|
|
_callbacks->restartBridgeSlot(slot);
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(subcmd, "password ", 9) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_password[slot], &subcmd[9], sizeof(_prefs->mqtt_slot_password[slot]));
|
|
savePrefs();
|
|
_callbacks->restartBridgeSlot(slot);
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(subcmd, "token ", 6) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_token[slot], &subcmd[6], sizeof(_prefs->mqtt_slot_token[slot]));
|
|
savePrefs();
|
|
_callbacks->restartBridgeSlot(slot);
|
|
sprintf(reply, "OK - slot %d token set", slot + 1);
|
|
} else if (memcmp(subcmd, "topic ", 6) == 0) {
|
|
if (strcmp(_prefs->mqtt_slot_preset[slot], "custom") != 0) {
|
|
sprintf(reply, "Error: topic template only applies to custom preset slots");
|
|
} else {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_topic[slot], &subcmd[6], sizeof(_prefs->mqtt_slot_topic[slot]));
|
|
savePrefs();
|
|
_callbacks->restartBridgeSlot(slot);
|
|
sprintf(reply, "OK - slot %d topic: %s", slot + 1, _prefs->mqtt_slot_topic[slot]);
|
|
}
|
|
} else if (memcmp(subcmd, "audience ", 9) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_audience[slot], &subcmd[9], sizeof(_prefs->mqtt_slot_audience[slot]));
|
|
savePrefs();
|
|
_callbacks->restartBridgeSlot(slot);
|
|
if (_prefs->mqtt_slot_audience[slot][0] != '\0') {
|
|
sprintf(reply, "OK - slot %d JWT audience: %s", slot + 1, _prefs->mqtt_slot_audience[slot]);
|
|
} else {
|
|
sprintf(reply, "OK - slot %d JWT audience cleared (using username/password auth)", slot + 1);
|
|
}
|
|
} else if (memcmp(subcmd, "audience", 8) == 0 && subcmd[8] == '\0') {
|
|
// "set mqttN.audience" with no value — clear the audience
|
|
_prefs->mqtt_slot_audience[slot][0] = '\0';
|
|
savePrefs();
|
|
_callbacks->restartBridgeSlot(slot);
|
|
sprintf(reply, "OK - slot %d JWT audience cleared (using username/password auth)", slot + 1);
|
|
} else {
|
|
sprintf(reply, "unknown config: %s", config);
|
|
}
|
|
} else if (memcmp(config, "mqtt.analyzer.us ", 17) == 0) {
|
|
const int slot = 0;
|
|
if (memcmp(&config[17], "on", 2) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], "analyzer-us", sizeof(_prefs->mqtt_slot_preset[slot]));
|
|
} else {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], MQTT_PRESET_NONE, sizeof(_prefs->mqtt_slot_preset[slot]));
|
|
}
|
|
savePrefs();
|
|
_callbacks->restartBridgeSlot(slot);
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "mqtt.analyzer.eu ", 17) == 0) {
|
|
const int slot = 1;
|
|
if (memcmp(&config[17], "on", 2) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], "analyzer-eu", sizeof(_prefs->mqtt_slot_preset[slot]));
|
|
} else {
|
|
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], MQTT_PRESET_NONE, sizeof(_prefs->mqtt_slot_preset[slot]));
|
|
}
|
|
savePrefs();
|
|
_callbacks->restartBridgeSlot(slot);
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(config, "mqtt.owner ", 11) == 0) {
|
|
const char* owner_key = &config[11];
|
|
int key_len = strlen(owner_key);
|
|
if (key_len == 64) {
|
|
bool valid_key = true;
|
|
for (int i = 0; i < key_len; i++) {
|
|
if (!((owner_key[i] >= '0' && owner_key[i] <= '9') ||
|
|
(owner_key[i] >= 'A' && owner_key[i] <= 'F') ||
|
|
(owner_key[i] >= 'a' && owner_key[i] <= 'f'))) {
|
|
valid_key = false;
|
|
break;
|
|
}
|
|
}
|
|
if (valid_key) {
|
|
StrHelper::strncpy(_prefs->mqtt_owner_public_key, owner_key, sizeof(_prefs->mqtt_owner_public_key));
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: invalid hex characters in public key");
|
|
}
|
|
} else {
|
|
strcpy(reply, "Error: public key must be 64 hex characters (32 bytes)");
|
|
}
|
|
} else if (memcmp(config, "mqtt.email ", 11) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_email, &config[11], sizeof(_prefs->mqtt_email));
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
#endif
|
|
} else if (memcmp(config, "alert ", 6) == 0) {
|
|
// set alert on|off
|
|
const char* val = &config[6];
|
|
if (memcmp(val, "on", 2) == 0 && (val[2] == 0 || val[2] == ' ')) {
|
|
_prefs->alert_enabled = 1;
|
|
savePrefs();
|
|
_callbacks->onAlertConfigChanged();
|
|
strcpy(reply, "OK - alerts on");
|
|
} else if (memcmp(val, "off", 3) == 0 && (val[3] == 0 || val[3] == ' ')) {
|
|
_prefs->alert_enabled = 0;
|
|
savePrefs();
|
|
_callbacks->onAlertConfigChanged();
|
|
strcpy(reply, "OK - alerts off");
|
|
} else {
|
|
strcpy(reply, "Error: usage set alert on|off");
|
|
}
|
|
} else if (memcmp(config, "alert.psk", 9) == 0 && (config[9] == 0 || config[9] == ' ')) {
|
|
// `set alert.psk` with no argument clears the field (alerts then disabled
|
|
// until a new psk/hashtag is configured).
|
|
const char* val = (config[9] == ' ') ? &config[10] : "";
|
|
while (*val == ' ') val++;
|
|
size_t len = strlen(val);
|
|
if (len == 0) {
|
|
_prefs->alert_psk_hex[0] = '\0';
|
|
_prefs->alert_hashtag[0] = '\0';
|
|
savePrefs();
|
|
_callbacks->onAlertConfigChanged();
|
|
strcpy(reply, "OK - alert.psk cleared (alerts disabled until configured)");
|
|
} else if (val[0] == '#') {
|
|
strcpy(reply, "Error: use 'set alert.hashtag' for hashtag channels");
|
|
} else if (len != 32) {
|
|
// 16-byte channel secret = 32 hex chars. This is what the mobile app's
|
|
// "Share Channel" emits, what `set alert.hashtag` derives, and what the
|
|
// BANNED_ALERT_CHANNELS table holds. 32-byte channels aren't used
|
|
// anywhere in MeshCore practice.
|
|
strcpy(reply, "Error: PSK must be 32 hex chars (16-byte channel secret)");
|
|
} else {
|
|
// Validate all-hex, then normalize via fromHex/toHex so the stored
|
|
// form is always lowercase regardless of input case.
|
|
uint8_t raw[16];
|
|
bool all_hex = true;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (!mesh::Utils::isHexChar(val[i])) { all_hex = false; break; }
|
|
}
|
|
if (!all_hex || !mesh::Utils::fromHex(raw, 16, val)) {
|
|
strcpy(reply, "Error: PSK must be 32 hex chars (16-byte channel secret)");
|
|
} else {
|
|
char normalized[33];
|
|
mesh::Utils::toHex(normalized, raw, 16);
|
|
if (const char* banned = alertReporterBannedChannelMatchHex(normalized)) {
|
|
// Refuse any key on the banned channel list (Public PSK, well-known
|
|
// auto-responder hashtags like #test/#bot, etc.). Fault alerts on
|
|
// those channels would spam every node in the area.
|
|
sprintf(reply, "Error: refusing banned channel '%s'; pick a private key or hashtag", banned);
|
|
} else {
|
|
StrHelper::strncpy(_prefs->alert_psk_hex, normalized, sizeof(_prefs->alert_psk_hex));
|
|
// The new PSK is operator-supplied, so any previously-derived
|
|
// hashtag name is no longer accurate provenance — drop it.
|
|
_prefs->alert_hashtag[0] = '\0';
|
|
savePrefs();
|
|
_callbacks->onAlertConfigChanged();
|
|
strcpy(reply, "OK - alert.psk updated");
|
|
}
|
|
}
|
|
}
|
|
} else if (memcmp(config, "alert.hashtag", 13) == 0 && (config[13] == 0 || config[13] == ' ')) {
|
|
const char* val = (config[13] == ' ') ? &config[14] : "";
|
|
while (*val == ' ') val++;
|
|
size_t in_len = strlen(val);
|
|
if (in_len == 0) {
|
|
_prefs->alert_psk_hex[0] = '\0';
|
|
_prefs->alert_hashtag[0] = '\0';
|
|
savePrefs();
|
|
_callbacks->onAlertConfigChanged();
|
|
strcpy(reply, "OK - alert.hashtag cleared (alerts disabled until configured)");
|
|
} else {
|
|
// Canonical stored form is "#name" because the leading '#' is part of
|
|
// the sha256 input (matching the companion-app hashtag-channel
|
|
// derivation in docs/companion_protocol.md). Accept the user typing
|
|
// either "alerts" or "#alerts".
|
|
char hashtag[sizeof(_prefs->alert_hashtag)];
|
|
size_t need = (val[0] == '#') ? in_len : in_len + 1;
|
|
if (need >= sizeof(hashtag)) {
|
|
strcpy(reply, "Error: hashtag too long");
|
|
} else {
|
|
if (val[0] == '#') {
|
|
StrHelper::strncpy(hashtag, val, sizeof(hashtag));
|
|
} else {
|
|
hashtag[0] = '#';
|
|
StrHelper::strncpy(&hashtag[1], val, sizeof(hashtag) - 1);
|
|
}
|
|
|
|
// Derive the channel key once: first 16 bytes of sha256("#name"),
|
|
// store hex-encoded in alert_psk_hex. We don't re-derive on every
|
|
// send — operators can later override with `set alert.psk` without
|
|
// leaving stale hashtag text behind.
|
|
uint8_t digest[32];
|
|
mesh::Utils::sha256(digest, sizeof(digest),
|
|
(const uint8_t*)hashtag, (int)strlen(hashtag));
|
|
if (const char* banned = alertReporterBannedChannelMatch(digest)) {
|
|
// Hashtag derives to a banned key (e.g. `set alert.hashtag test`
|
|
// hits the #test entry). Refuse before clobbering existing config.
|
|
sprintf(reply, "Error: refusing banned channel '%s'", banned);
|
|
} else {
|
|
char hex[33];
|
|
mesh::Utils::toHex(hex, digest, 16);
|
|
StrHelper::strncpy(_prefs->alert_hashtag, hashtag, sizeof(_prefs->alert_hashtag));
|
|
StrHelper::strncpy(_prefs->alert_psk_hex, hex, sizeof(_prefs->alert_psk_hex));
|
|
savePrefs();
|
|
_callbacks->onAlertConfigChanged();
|
|
sprintf(reply, "OK - alert.hashtag: %s", _prefs->alert_hashtag);
|
|
}
|
|
}
|
|
}
|
|
} else if (memcmp(config, "alert.region", 12) == 0 && (config[12] == 0 || config[12] == ' ')) {
|
|
// `set alert.region <name>` overrides the repeater's default_scope for
|
|
// alert sends only. `set alert.region` (no arg) clears it. The name is
|
|
// looked up lazily via RegionMap at send time; we deliberately don't
|
|
// mutate the region map here, so naming an unknown region is allowed
|
|
// but will silently fall back to default_scope until the operator runs
|
|
// `region put` for it.
|
|
const char* val = (config[12] == ' ') ? &config[13] : "";
|
|
while (*val == ' ') val++;
|
|
size_t len = strlen(val);
|
|
if (len == 0) {
|
|
_prefs->alert_region[0] = '\0';
|
|
savePrefs();
|
|
_callbacks->onAlertConfigChanged();
|
|
strcpy(reply, "OK - alert.region cleared (using default scope)");
|
|
} else if (len >= sizeof(_prefs->alert_region)) {
|
|
strcpy(reply, "Error: alert.region too long");
|
|
} else {
|
|
StrHelper::strncpy(_prefs->alert_region, val, sizeof(_prefs->alert_region));
|
|
StrHelper::stripSurroundingQuotes(_prefs->alert_region, sizeof(_prefs->alert_region));
|
|
savePrefs();
|
|
_callbacks->onAlertConfigChanged();
|
|
sprintf(reply, "OK - alert.region: %s", _prefs->alert_region);
|
|
}
|
|
} else if (memcmp(config, "alert.wifi ", 11) == 0) {
|
|
int mins = (int)_atoi(&config[11]);
|
|
if (mins < 0 || mins > 1440) {
|
|
strcpy(reply, "Error: alert.wifi must be 0-1440 minutes (0=off)");
|
|
} else {
|
|
_prefs->alert_wifi_minutes = (uint16_t)mins;
|
|
savePrefs();
|
|
sprintf(reply, "OK - alert.wifi %d min%s", mins, mins == 0 ? " (disabled)" : "");
|
|
}
|
|
} else if (memcmp(config, "alert.mqtt ", 11) == 0) {
|
|
int mins = (int)_atoi(&config[11]);
|
|
if (mins < 0 || mins > 10080) {
|
|
strcpy(reply, "Error: alert.mqtt must be 0-10080 minutes (0=off)");
|
|
} else {
|
|
_prefs->alert_mqtt_minutes = (uint16_t)mins;
|
|
savePrefs();
|
|
sprintf(reply, "OK - alert.mqtt %d min%s", mins, mins == 0 ? " (disabled)" : "");
|
|
}
|
|
} else if (memcmp(config, "alert.interval ", 15) == 0) {
|
|
int mins = (int)_atoi(&config[15]);
|
|
// Floor at 60 min: faster re-fires would let a flapping link spam the
|
|
// mesh with a fresh GRP_TXT flood every minute — terrible for airtime.
|
|
if (mins < 60 || mins > 10080) {
|
|
strcpy(reply, "Error: alert.interval must be 60-10080 minutes");
|
|
} else {
|
|
_prefs->alert_min_interval_min = (uint16_t)mins;
|
|
savePrefs();
|
|
sprintf(reply, "OK - alert.interval %d min", mins);
|
|
}
|
|
} else if (memcmp(config, "adc.multiplier ", 15) == 0) {
|
|
_prefs->adc_multiplier = atof(&config[15]);
|
|
if (_board->setAdcMultiplier(_prefs->adc_multiplier)) {
|
|
savePrefs();
|
|
if (_prefs->adc_multiplier == 0.0f) {
|
|
strcpy(reply, "OK - using default board multiplier");
|
|
} else {
|
|
sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier);
|
|
}
|
|
} else {
|
|
_prefs->adc_multiplier = 0.0f;
|
|
strcpy(reply, "Error: unsupported by this board");
|
|
};
|
|
} else {
|
|
sprintf(reply, "unknown config: %s", config);
|
|
}
|
|
}
|
|
|
|
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
|
const char* config = &command[4];
|
|
if (memcmp(config, "dutycycle", 9) == 0) {
|
|
float dc = 100.0f / (_prefs->airtime_factor + 1.0f);
|
|
int dc_int = (int)dc;
|
|
int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f);
|
|
sprintf(reply, "> %d.%d%%", dc_int, dc_frac);
|
|
} else if (memcmp(config, "af", 2) == 0) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
|
|
} else if (memcmp(config, "int.thresh", 10) == 0) {
|
|
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
|
|
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
|
|
sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
|
|
} else if (memcmp(config, "radio.watchdog", 14) == 0) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->radio_watchdog_minutes);
|
|
} else if (memcmp(config, "multi.acks", 10) == 0) {
|
|
sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
|
|
} else if (memcmp(config, "allow.read.only", 15) == 0) {
|
|
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
|
|
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
|
|
sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
|
|
} else if (memcmp(config, "advert.interval", 15) == 0) {
|
|
sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
|
|
} else if (memcmp(config, "guest.password", 14) == 0) {
|
|
sprintf(reply, "> %s", _prefs->guest_password);
|
|
} else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only
|
|
uint8_t prv_key[PRV_KEY_SIZE];
|
|
int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
|
|
mesh::Utils::toHex(tmp, prv_key, len);
|
|
sprintf(reply, "> %s", tmp);
|
|
} else if (memcmp(config, "name", 4) == 0) {
|
|
sprintf(reply, "> %s", _prefs->node_name);
|
|
} else if (memcmp(config, "repeat", 6) == 0) {
|
|
sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on");
|
|
} else if (memcmp(config, "lat", 3) == 0) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
|
|
} else if (memcmp(config, "lon", 3) == 0) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
|
|
#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110)
|
|
} else if (memcmp(config, "radio.rxgain", 12) == 0) {
|
|
sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
|
|
#endif
|
|
} else if (memcmp(config, "radio", 5) == 0) {
|
|
char freq[16], bw[16];
|
|
strcpy(freq, StrHelper::ftoa(_prefs->freq));
|
|
strcpy(bw, StrHelper::ftoa3(_prefs->bw));
|
|
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
|
|
} else if (memcmp(config, "rxdelay", 7) == 0) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
|
|
} else if (memcmp(config, "txdelay", 7) == 0) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
|
|
} else if (memcmp(config, "flood.max.advert", 16) == 0) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert);
|
|
} else if (memcmp(config, "flood.max.unscoped", 18) == 0) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped);
|
|
} else if (memcmp(config, "flood.max", 9) == 0) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
|
|
} else if (memcmp(config, "direct.txdelay", 14) == 0) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
|
|
} else if (memcmp(config, "owner.info", 10) == 0) {
|
|
*reply++ = '>';
|
|
*reply++ = ' ';
|
|
const char* sp = _prefs->owner_info;
|
|
while (*sp) {
|
|
*reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|'
|
|
sp++;
|
|
}
|
|
*reply = 0; // set null terminator
|
|
} else if (memcmp(config, "path.hash.mode", 14) == 0) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
|
|
} else if (memcmp(config, "loop.detect", 11) == 0) {
|
|
if (_prefs->loop_detect == LOOP_DETECT_OFF) {
|
|
strcpy(reply, "> off");
|
|
} else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) {
|
|
strcpy(reply, "> minimal");
|
|
} else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) {
|
|
strcpy(reply, "> moderate");
|
|
} else {
|
|
strcpy(reply, "> strict");
|
|
}
|
|
} else if (memcmp(config, "snmp.community", 14) == 0) {
|
|
sprintf(reply, "> %s", _prefs->snmp_community);
|
|
} else if (memcmp(config, "snmp", 4) == 0 && (config[4] == '\0' || config[4] == '\n' || config[4] == '\r')) {
|
|
strcpy(reply, _prefs->snmp_enabled ? "> on" : "> off");
|
|
} else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
|
|
sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
|
|
} else if (memcmp(config, "freq", 4) == 0) {
|
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
|
|
} else if (memcmp(config, "public.key", 10) == 0) {
|
|
strcpy(reply, "> ");
|
|
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
|
|
} else if (memcmp(config, "role", 4) == 0) {
|
|
sprintf(reply, "> %s", _callbacks->getRole());
|
|
} else if (memcmp(config, "bridge.type", 11) == 0) {
|
|
sprintf(reply, "> %s",
|
|
#ifdef WITH_RS232_BRIDGE
|
|
"rs232"
|
|
#elif WITH_ESPNOW_BRIDGE
|
|
"espnow"
|
|
#else
|
|
"none"
|
|
#endif
|
|
);
|
|
#ifdef WITH_BRIDGE
|
|
} else if (memcmp(config, "bridge.enabled", 14) == 0) {
|
|
sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
|
|
} else if (memcmp(config, "bridge.delay", 12) == 0) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
|
|
} else if (memcmp(config, "bridge.source", 13) == 0) {
|
|
sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx");
|
|
#endif
|
|
#ifdef WITH_RS232_BRIDGE
|
|
} else if (memcmp(config, "bridge.baud", 11) == 0) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
|
|
#endif
|
|
#ifdef WITH_ESPNOW_BRIDGE
|
|
} else if (memcmp(config, "bridge.channel", 14) == 0) {
|
|
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
|
|
} else if (memcmp(config, "bridge.secret", 13) == 0) {
|
|
sprintf(reply, "> %s", _prefs->bridge_secret);
|
|
#endif
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
} else if (memcmp(config, "mqtt.origin", 11) == 0) {
|
|
char effective_origin[32];
|
|
MQTTBridge::getEffectiveMqttOrigin(_prefs, effective_origin, sizeof(effective_origin));
|
|
sprintf(reply, "> %s", effective_origin);
|
|
} else if (memcmp(config, "mqtt.iata", 9) == 0) {
|
|
sprintf(reply, "> %s", _prefs->mqtt_iata);
|
|
} else if (memcmp(config, "mqtt.presets", 12) == 0 && (config[12] == '\0' || config[12] == ' ')) {
|
|
int start = 0;
|
|
if (config[12] == ' ') {
|
|
const char* start_arg = &config[13];
|
|
if (*start_arg == '\0') {
|
|
strcpy(reply, "Error: usage get mqtt.presets [start]");
|
|
return;
|
|
}
|
|
for (const char* sp = start_arg; *sp; sp++) {
|
|
if (*sp < '0' || *sp > '9') {
|
|
strcpy(reply, "Error: usage get mqtt.presets [start]");
|
|
return;
|
|
}
|
|
}
|
|
start = (int)_atoi(start_arg);
|
|
}
|
|
formatMQTTPresetListReply(reply, 160, start);
|
|
} else if (memcmp(config, "mqtt.status", 11) == 0) {
|
|
MQTTBridge::formatMqttStatusReply(reply, 160, _prefs);
|
|
} else if (memcmp(config, "mqtt.packets", 12) == 0) {
|
|
sprintf(reply, "> %s", _prefs->mqtt_packets_enabled ? "on" : "off");
|
|
} else if (memcmp(config, "mqtt.raw", 8) == 0) {
|
|
sprintf(reply, "> %s", _prefs->mqtt_raw_enabled ? "on" : "off");
|
|
} else if (memcmp(config, "mqtt.tx", 7) == 0) {
|
|
const char* tx_str = _prefs->mqtt_tx_enabled == 2 ? "advert" : (_prefs->mqtt_tx_enabled ? "on" : "off");
|
|
sprintf(reply, "> %s", tx_str);
|
|
} else if (memcmp(config, "mqtt.rx", 7) == 0) {
|
|
sprintf(reply, "> %s", _prefs->mqtt_rx_enabled ? "on" : "off");
|
|
} else if (memcmp(config, "mqtt.interval", 13) == 0) {
|
|
uint32_t minutes = (_prefs->mqtt_status_interval + 29999) / 60000;
|
|
sprintf(reply, "> %u minutes (%lu ms)", minutes, (unsigned long)_prefs->mqtt_status_interval);
|
|
} else if (config[0] == 'm' && config[1] == 'q' && config[2] == 't' && config[3] == 't' &&
|
|
config[4] >= '1' && config[4] <= ('0' + MAX_MQTT_SLOTS) && config[5] == '.') {
|
|
// Slot-based commands: get mqtt1.preset, get mqtt1.server, etc.
|
|
int slot = config[4] - '1'; // 0-5
|
|
const char* subcmd = &config[6];
|
|
if (memcmp(subcmd, "preset", 6) == 0) {
|
|
sprintf(reply, "> %s", _prefs->mqtt_slot_preset[slot]);
|
|
} else if (memcmp(subcmd, "server", 6) == 0) {
|
|
sprintf(reply, "> %s", _prefs->mqtt_slot_host[slot]);
|
|
} else if (memcmp(subcmd, "port", 4) == 0) {
|
|
sprintf(reply, "> %d", _prefs->mqtt_slot_port[slot]);
|
|
} else if (memcmp(subcmd, "username", 8) == 0) {
|
|
sprintf(reply, "> %s", _prefs->mqtt_slot_username[slot]);
|
|
} else if (memcmp(subcmd, "password", 8) == 0) {
|
|
sprintf(reply, "> %s", _prefs->mqtt_slot_password[slot]);
|
|
} else if (memcmp(subcmd, "token", 5) == 0) {
|
|
if (_prefs->mqtt_slot_token[slot][0] != '\0') {
|
|
sprintf(reply, "> %s", _prefs->mqtt_slot_token[slot]);
|
|
} else {
|
|
strcpy(reply, "> (not set)");
|
|
}
|
|
} else if (memcmp(subcmd, "topic", 5) == 0) {
|
|
if (_prefs->mqtt_slot_topic[slot][0] != '\0') {
|
|
sprintf(reply, "> %s", _prefs->mqtt_slot_topic[slot]);
|
|
} else {
|
|
strcpy(reply, "> (default: meshcore/{iata}/{device}/{type})");
|
|
}
|
|
} else if (memcmp(subcmd, "audience", 8) == 0) {
|
|
if (_prefs->mqtt_slot_audience[slot][0] != '\0') {
|
|
sprintf(reply, "> %s", _prefs->mqtt_slot_audience[slot]);
|
|
} else {
|
|
strcpy(reply, "> (not set — custom slots use username/password auth)");
|
|
}
|
|
} else if (memcmp(subcmd, "diag", 4) == 0) {
|
|
MQTTBridge::formatSlotDiagReply(reply, 160, slot);
|
|
} else {
|
|
sprintf(reply, "??: %s", config);
|
|
}
|
|
} else if (memcmp(config, "wifi.ssid", 9) == 0) {
|
|
sprintf(reply, "> %s", _prefs->wifi_ssid);
|
|
} else if (memcmp(config, "wifi.pwd", 8) == 0) {
|
|
sprintf(reply, "> %s", _prefs->wifi_password);
|
|
} else if (memcmp(config, "wifi.status", 11) == 0) {
|
|
wl_status_t status = WiFi.status();
|
|
const char* status_str;
|
|
switch (status) {
|
|
case WL_CONNECTED: status_str = "connected"; break;
|
|
case WL_NO_SSID_AVAIL: status_str = "no_ssid"; break;
|
|
case WL_CONNECT_FAILED: status_str = "connect_failed"; break;
|
|
case WL_CONNECTION_LOST: status_str = "connection_lost"; break;
|
|
case WL_DISCONNECTED: status_str = "disconnected"; break;
|
|
case 255: status_str = "not_started"; break;
|
|
default: status_str = "unknown"; break;
|
|
}
|
|
if (status == WL_CONNECTED) {
|
|
sprintf(reply, "> %s, IP: %s, RSSI: %d dBm", status_str, WiFi.localIP().toString().c_str(), WiFi.RSSI());
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
unsigned long connect_at = MQTTBridge::getWifiConnectedAtMillis();
|
|
if (connect_at != 0) {
|
|
unsigned long uptime_ms = millis() - connect_at;
|
|
unsigned long uptime_sec = uptime_ms / 1000;
|
|
unsigned long d = uptime_sec / 86400;
|
|
unsigned long h = (uptime_sec % 86400) / 3600;
|
|
unsigned long m = (uptime_sec % 3600) / 60;
|
|
unsigned long s = uptime_sec % 60;
|
|
size_t len = strlen(reply);
|
|
const size_t reply_remaining = 128;
|
|
if (d > 0) {
|
|
snprintf(reply + len, reply_remaining, ", uptime: %lud %luh %lum %lus", d, h, m, s);
|
|
} else if (h > 0) {
|
|
snprintf(reply + len, reply_remaining, ", uptime: %luh %lum %lus", h, m, s);
|
|
} else if (m > 0) {
|
|
snprintf(reply + len, reply_remaining, ", uptime: %lum %lus", m, s);
|
|
} else {
|
|
snprintf(reply + len, reply_remaining, ", uptime: %lus", s);
|
|
}
|
|
}
|
|
#endif
|
|
} else {
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
uint8_t reason = MQTTBridge::getLastWifiDisconnectReason();
|
|
if (reason != 0) {
|
|
const char* desc = MQTTBridge::wifiReasonStr(reason);
|
|
if (desc) {
|
|
sprintf(reply, "> %s: %s (reason: %d)", status_str, desc, reason);
|
|
} else {
|
|
sprintf(reply, "> %s: reason %d", status_str, reason);
|
|
}
|
|
} else {
|
|
sprintf(reply, "> %s (code: %d)", status_str, status);
|
|
}
|
|
#else
|
|
sprintf(reply, "> %s (code: %d)", status_str, status);
|
|
#endif
|
|
}
|
|
} else if (memcmp(config, "wifi.powersave", 14) == 0) {
|
|
uint8_t ps = _prefs->wifi_power_save;
|
|
const char* ps_name = (ps == 1) ? "none" : (ps == 2) ? "max" : "min";
|
|
sprintf(reply, "> %s", ps_name);
|
|
} else if (memcmp(config, "timezone", 8) == 0) {
|
|
sprintf(reply, "> %s", _prefs->timezone_string);
|
|
} else if (memcmp(config, "timezone.offset", 15) == 0) {
|
|
sprintf(reply, "> %d", _prefs->timezone_offset);
|
|
} else if (memcmp(config, "mqtt.analyzer.us", 17) == 0) {
|
|
sprintf(reply, "> %s", strcmp(_prefs->mqtt_slot_preset[0], "analyzer-us") == 0 ? "on" : "off");
|
|
} else if (memcmp(config, "mqtt.analyzer.eu", 17) == 0) {
|
|
sprintf(reply, "> %s", strcmp(_prefs->mqtt_slot_preset[1], "analyzer-eu") == 0 ? "on" : "off");
|
|
} else if (sender_timestamp == 0 && memcmp(config, "mqtt.owner", 10) == 0) {
|
|
if (_prefs->mqtt_owner_public_key[0] != '\0') {
|
|
sprintf(reply, "> %s", _prefs->mqtt_owner_public_key);
|
|
} else {
|
|
strcpy(reply, "> (not set)");
|
|
}
|
|
} else if (sender_timestamp == 0 && memcmp(config, "mqtt.email", 10) == 0) {
|
|
if (_prefs->mqtt_email[0] != '\0') {
|
|
sprintf(reply, "> %s", _prefs->mqtt_email);
|
|
} else {
|
|
strcpy(reply, "> (not set)");
|
|
}
|
|
} else if (memcmp(config, "mqtt.config.valid", 17) == 0) {
|
|
bool valid = MQTTBridge::isConfigValid(_prefs);
|
|
sprintf(reply, "> %s", valid ? "valid" : "invalid");
|
|
#endif
|
|
} else if (memcmp(config, "bootloader.ver", 14) == 0) {
|
|
#ifdef NRF52_PLATFORM
|
|
char ver[32];
|
|
if (_board->getBootloaderVersion(ver, sizeof(ver))) {
|
|
sprintf(reply, "> %s", ver);
|
|
} else {
|
|
strcpy(reply, "> unknown");
|
|
}
|
|
#else
|
|
strcpy(reply, "ERROR: unsupported");
|
|
#endif
|
|
} else if (memcmp(config, "alert.hashtag", 13) == 0) {
|
|
sprintf(reply, "> %s", _prefs->alert_hashtag[0] ? _prefs->alert_hashtag : "(unset)");
|
|
} else if (sender_timestamp == 0 && memcmp(config, "alert.psk", 9) == 0) { // from serial command line only
|
|
sprintf(reply, "> %s", _prefs->alert_psk_hex[0] ? _prefs->alert_psk_hex : "(unset)");
|
|
} else if (memcmp(config, "alert.region", 12) == 0) {
|
|
sprintf(reply, "> %s", _prefs->alert_region[0] ? _prefs->alert_region : "(unset, using default scope)");
|
|
} else if (memcmp(config, "alert.wifi", 10) == 0) {
|
|
sprintf(reply, "> %u min%s", (unsigned)_prefs->alert_wifi_minutes,
|
|
_prefs->alert_wifi_minutes == 0 ? " (disabled)" : "");
|
|
} else if (memcmp(config, "alert.mqtt", 10) == 0) {
|
|
sprintf(reply, "> %u min%s", (unsigned)_prefs->alert_mqtt_minutes,
|
|
_prefs->alert_mqtt_minutes == 0 ? " (disabled)" : "");
|
|
} else if (memcmp(config, "alert.interval", 14) == 0) {
|
|
sprintf(reply, "> %u min", (unsigned)_prefs->alert_min_interval_min);
|
|
} else if (memcmp(config, "alert", 5) == 0 && (config[5] == 0 || config[5] == '\n' || config[5] == '\r')) {
|
|
sprintf(reply, "> %s", _prefs->alert_enabled ? "on" : "off");
|
|
} else if (memcmp(config, "adc.multiplier", 14) == 0) {
|
|
float adc_mult = _board->getAdcMultiplier();
|
|
if (adc_mult == 0.0f) {
|
|
strcpy(reply, "Error: unsupported by this board");
|
|
} else {
|
|
sprintf(reply, "> %.3f", adc_mult);
|
|
}
|
|
// Power management commands
|
|
} else if (memcmp(config, "pwrmgt.support", 14) == 0) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
strcpy(reply, "> supported");
|
|
#else
|
|
strcpy(reply, "> unsupported");
|
|
#endif
|
|
} else if (memcmp(config, "pwrmgt.source", 13) == 0) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
|
|
#else
|
|
strcpy(reply, "ERROR: Power management not supported");
|
|
#endif
|
|
} else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
sprintf(reply, "> Reset: %s; Shutdown: %s",
|
|
_board->getResetReasonString(_board->getResetReason()),
|
|
_board->getShutdownReasonString(_board->getShutdownReason()));
|
|
#else
|
|
strcpy(reply, "ERROR: Power management not supported");
|
|
#endif
|
|
} else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
sprintf(reply, "> %u mV", _board->getBootVoltage());
|
|
#else
|
|
strcpy(reply, "ERROR: Power management not supported");
|
|
#endif
|
|
} else {
|
|
sprintf(reply, "??: %s", config);
|
|
}
|
|
}
|
|
|
|
static char* skipSpaces(char* s) {
|
|
while (*s == ' ') s++;
|
|
return s;
|
|
}
|
|
|
|
static void rtrimSpaces(char* s) {
|
|
char* e = s + strlen(s);
|
|
while (e > s && e[-1] == ' ') *--e = '\0';
|
|
}
|
|
|
|
static char* takeToken(char** cursor) {
|
|
char* p = skipSpaces(*cursor);
|
|
if (*p == '\0') { *cursor = p; return nullptr; }
|
|
char* tok = p;
|
|
while (*p && *p != ' ') p++;
|
|
if (*p) *p++ = '\0';
|
|
*cursor = p;
|
|
return tok;
|
|
}
|
|
|
|
static char* splitNameJump(char* tok) {
|
|
for (char* q = tok; *q; q++) {
|
|
if (*q == '|' || *q == ',') {
|
|
*q = '\0';
|
|
char* jump = skipSpaces(q + 1);
|
|
rtrimSpaces(jump);
|
|
return jump;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static bool processRegionDefSegment(RegionMap* map, char* tok, RegionEntry** cursor, char* reply) {
|
|
char* jump = splitNameJump(tok);
|
|
char* name = skipSpaces(tok);
|
|
if (*name == '\0') { snprintf(reply, 160, "Err - empty name"); return false; }
|
|
if (jump && *jump == '\0') { snprintf(reply, 160, "Err - empty jump"); return false; }
|
|
|
|
RegionEntry* r = map->putRegion(name, (*cursor)->id);
|
|
if (r == NULL) { snprintf(reply, 160, "Err - put failed: %s", name); return false; }
|
|
r->flags = 0;
|
|
|
|
if (jump) {
|
|
RegionEntry* j = map->findByNamePrefix(jump);
|
|
if (j == NULL) { snprintf(reply, 160, "Err - unknown jump: %s", jump); return false; }
|
|
*cursor = j;
|
|
} else {
|
|
*cursor = r;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void CommonCLI::handleRegionCmd(char* command, char* reply) {
|
|
reply[0] = 0;
|
|
|
|
// `region def`: must run before parseTextParts mutates the buffer
|
|
char* cmd = skipSpaces(command);
|
|
if (strncmp(cmd, "region def", 10) == 0 && (cmd[10] == ' ' || cmd[10] == '\0')) {
|
|
char* payload = skipSpaces(cmd + 10);
|
|
rtrimSpaces(payload);
|
|
if (*payload == '\0') { snprintf(reply, 160, "Err - empty def"); return; }
|
|
|
|
RegionEntry* cursor = &_region_map->getWildcard();
|
|
for (char* tok; (tok = takeToken(&payload)) != nullptr; ) {
|
|
if (!processRegionDefSegment(_region_map, tok, &cursor, reply)) return;
|
|
}
|
|
_region_map->exportTo(reply, 160);
|
|
return;
|
|
}
|
|
|
|
const char* parts[4];
|
|
int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
|
|
if (n == 1) {
|
|
_region_map->exportTo(reply, 160);
|
|
} else if (n >= 2 && strcmp(parts[1], "load") == 0) {
|
|
_callbacks->startRegionsLoad();
|
|
} else if (n >= 2 && strcmp(parts[1], "save") == 0) {
|
|
_prefs->discovery_mod_timestamp = getRTCClock()->getCurrentTime(); // this node is now 'modified' (for discovery info)
|
|
savePrefs();
|
|
bool success = _callbacks->saveRegions();
|
|
strcpy(reply, success ? "OK" : "Err - save failed");
|
|
} else if (n >= 3 && strcmp(parts[1], "allowf") == 0) {
|
|
auto region = _region_map->findByNamePrefix(parts[2]);
|
|
if (region) {
|
|
region->flags &= ~REGION_DENY_FLOOD;
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Err - unknown region");
|
|
}
|
|
} else if (n >= 3 && strcmp(parts[1], "denyf") == 0) {
|
|
auto region = _region_map->findByNamePrefix(parts[2]);
|
|
if (region) {
|
|
region->flags |= REGION_DENY_FLOOD;
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Err - unknown region");
|
|
}
|
|
} else if (n >= 3 && strcmp(parts[1], "get") == 0) {
|
|
auto region = _region_map->findByNamePrefix(parts[2]);
|
|
if (region) {
|
|
auto parent = _region_map->findById(region->parent);
|
|
if (parent && parent->id != 0) {
|
|
sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
|
|
} else {
|
|
sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
|
|
}
|
|
} else {
|
|
strcpy(reply, "Err - unknown region");
|
|
}
|
|
} else if (n >= 3 && strcmp(parts[1], "home") == 0) {
|
|
auto home = _region_map->findByNamePrefix(parts[2]);
|
|
if (home) {
|
|
_region_map->setHomeRegion(home);
|
|
sprintf(reply, " home is now %s", home->name);
|
|
} else {
|
|
strcpy(reply, "Err - unknown region");
|
|
}
|
|
} else if (n == 2 && strcmp(parts[1], "home") == 0) {
|
|
auto home = _region_map->getHomeRegion();
|
|
sprintf(reply, " home is %s", home ? home->name : "*");
|
|
} else if (n >= 3 && strcmp(parts[1], "default") == 0) {
|
|
if (strcmp(parts[2], "<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) {
|
|
if (_region_map->removeRegion(*region)) {
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Err - not empty");
|
|
}
|
|
} else {
|
|
strcpy(reply, "Err - not found");
|
|
}
|
|
} else if (n >= 3 && strcmp(parts[1], "list") == 0) {
|
|
uint8_t mask = 0;
|
|
bool invert = false;
|
|
|
|
if (strcmp(parts[2], "allowed") == 0) {
|
|
mask = REGION_DENY_FLOOD;
|
|
invert = false; // list regions that DON'T have DENY flag
|
|
} else if (strcmp(parts[2], "denied") == 0) {
|
|
mask = REGION_DENY_FLOOD;
|
|
invert = true; // list regions that DO have DENY flag
|
|
} else {
|
|
strcpy(reply, "Err - use 'allowed' or 'denied'");
|
|
return;
|
|
}
|
|
|
|
int len = _region_map->exportNamesTo(reply, 160, mask, invert);
|
|
if (len == 0) {
|
|
strcpy(reply, "-none-");
|
|
}
|
|
} else {
|
|
strcpy(reply, "Err - ??");
|
|
}
|
|
}
|