Implement a new method `formatRadioDiagReply` in MyMesh to format and return radio diagnostic information. Update CommonCLI to handle the new command for retrieving radio diagnostics. Enhance the Dispatcher class with a radio watchdog feature to monitor and recover from silent radio states. Introduce new error flags and methods in the Dispatcher and RadioLibWrapper classes to support these diagnostics, improving overall radio management and troubleshooting capabilities.
1726 lines
85 KiB
C++
1726 lines
85 KiB
C++
#include <Arduino.h>
|
|
#include "CommonCLI.h"
|
|
#include "TxtDataHelpers.h"
|
|
#include "AdvertDataHelpers.h"
|
|
#include <RTClib.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"
|
|
|
|
// 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;
|
|
}
|
|
|
|
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
|
|
// This ensures new users get the correct default, but respects existing user choices
|
|
// MQTT bridge with TX requires mqtt.tx to be enabled (disabled by default),
|
|
// so RX is the sensible default for MQTT bridge installations
|
|
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
|
|
file.read((uint8_t *)&_prefs->snmp_enabled, sizeof(_prefs->snmp_enabled)); // 291
|
|
file.read((uint8_t *)&_prefs->snmp_community, sizeof(_prefs->snmp_community)); // 292
|
|
// next: 316
|
|
|
|
// 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
|
|
|
|
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->snmp_enabled, sizeof(_prefs->snmp_enabled)); // 291
|
|
file.write((uint8_t *)&_prefs->snmp_community, sizeof(_prefs->snmp_community)); // 292
|
|
// next: 316
|
|
|
|
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) {
|
|
memset(prefs, 0, sizeof(MQTTPrefs));
|
|
// Set sensible defaults matching MQTTBridge expectations
|
|
prefs->mqtt_status_enabled = 1; // enabled by default
|
|
prefs->mqtt_packets_enabled = 1; // enabled by default
|
|
prefs->mqtt_raw_enabled = 0; // disabled by default
|
|
prefs->mqtt_tx_enabled = 0; // disabled by default
|
|
prefs->mqtt_rx_enabled = 1; // RX packets enabled by default
|
|
prefs->mqtt_status_interval = 300000; // 5 minutes default
|
|
// Slot presets: analyzer-us and analyzer-eu enabled by default, rest = none
|
|
strncpy(prefs->mqtt_slot_preset[0], "analyzer-us", sizeof(prefs->mqtt_slot_preset[0]) - 1);
|
|
prefs->mqtt_slot_preset[0][sizeof(prefs->mqtt_slot_preset[0]) - 1] = '\0';
|
|
strncpy(prefs->mqtt_slot_preset[1], "analyzer-eu", sizeof(prefs->mqtt_slot_preset[1]) - 1);
|
|
prefs->mqtt_slot_preset[1][sizeof(prefs->mqtt_slot_preset[1]) - 1] = '\0';
|
|
for (int i = 2; i < MAX_MQTT_SLOTS; i++) {
|
|
strncpy(prefs->mqtt_slot_preset[i], "none", sizeof(prefs->mqtt_slot_preset[i]) - 1);
|
|
prefs->mqtt_slot_preset[i][sizeof(prefs->mqtt_slot_preset[i]) - 1] = '\0';
|
|
}
|
|
prefs->wifi_power_save = 1; // Default to none (0=min, 1=none, 2=max)
|
|
// String fields are already zero-initialized by memset
|
|
}
|
|
|
|
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, const 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)");
|
|
/*
|
|
* GET commands
|
|
*/
|
|
} else if (memcmp(command, "get ", 4) == 0) {
|
|
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, "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)
|
|
} 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", 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) {
|
|
sprintf(reply, "> %s", _prefs->mqtt_origin);
|
|
} else if (memcmp(config, "mqtt.iata", 9) == 0) {
|
|
sprintf(reply, "> %s", _prefs->mqtt_iata);
|
|
} 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) {
|
|
// Display interval in minutes (rounded)
|
|
uint32_t minutes = (_prefs->mqtt_status_interval + 29999) / 60000; // Round up
|
|
sprintf(reply, "> %u minutes (%lu ms)", minutes, _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; // WL_NO_SHIELD
|
|
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; // caller provides buffer (e.g. 161 bytes), leave headroom
|
|
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 (sender_timestamp == 0 && memcmp(config, "mqtt.owner", 10) == 0) { // from serial command line only
|
|
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) { // from serial command line only
|
|
if (_prefs->mqtt_email[0] != '\0') {
|
|
sprintf(reply, "> %s", _prefs->mqtt_email);
|
|
} else {
|
|
strcpy(reply, "> (not set)");
|
|
}
|
|
#endif
|
|
} else if (memcmp(config, "bootloader.ver", 14) == 0) {
|
|
#ifdef NRF52_PLATFORM
|
|
char ver[32];
|
|
if (_board->getBootloaderVersion(ver, sizeof(ver))) {
|
|
sprintf(reply, "> %s", ver);
|
|
} else {
|
|
strcpy(reply, "> unknown");
|
|
}
|
|
#else
|
|
strcpy(reply, "ERROR: unsupported");
|
|
#endif
|
|
} else if (memcmp(config, "adc.multiplier", 14) == 0) {
|
|
float adc_mult = _board->getAdcMultiplier();
|
|
if (adc_mult == 0.0f) {
|
|
strcpy(reply, "Error: unsupported by this board");
|
|
} else {
|
|
sprintf(reply, "> %.3f", adc_mult);
|
|
}
|
|
// Power management commands
|
|
} else if (memcmp(config, "pwrmgt.support", 14) == 0) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
strcpy(reply, "> supported");
|
|
#else
|
|
strcpy(reply, "> unsupported");
|
|
#endif
|
|
} else if (memcmp(config, "pwrmgt.source", 13) == 0) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
|
|
#else
|
|
strcpy(reply, "ERROR: Power management not supported");
|
|
#endif
|
|
} else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
sprintf(reply, "> Reset: %s; Shutdown: %s",
|
|
_board->getResetReasonString(_board->getResetReason()),
|
|
_board->getShutdownReasonString(_board->getShutdownReason()));
|
|
#else
|
|
strcpy(reply, "ERROR: Power management not supported");
|
|
#endif
|
|
} else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
sprintf(reply, "> %u mV", _board->getBootVoltage());
|
|
#else
|
|
strcpy(reply, "ERROR: Power management not supported");
|
|
#endif
|
|
} else {
|
|
sprintf(reply, "??: %s", config);
|
|
}
|
|
/*
|
|
* SET commands
|
|
*/
|
|
} else if (memcmp(command, "set ", 4) == 0) {
|
|
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, "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)
|
|
} 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) {
|
|
_prefs->rx_delay_base = db;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, cannot be negative");
|
|
}
|
|
} else if (memcmp(config, "txdelay ", 8) == 0) {
|
|
float f = atof(&config[8]);
|
|
if (f >= 0) {
|
|
_prefs->tx_delay_factor = f;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, cannot be negative");
|
|
}
|
|
} 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) {
|
|
_prefs->direct_tx_delay_factor = f;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, cannot be negative");
|
|
}
|
|
} 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;
|
|
// Also update mqtt.rx/mqtt.tx for MQTT bridge compatibility
|
|
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;
|
|
}
|
|
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 <= 115200) {
|
|
_prefs->bridge_baud = (uint32_t)baud;
|
|
_callbacks->restartBridge();
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: baud rate must be between 9600-115200");
|
|
}
|
|
#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 (memcmp(config, "mqtt.origin ", 12) == 0) {
|
|
StrHelper::strncpy(_prefs->mqtt_origin, &config[12], 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));
|
|
// Convert IATA code to uppercase (IATA codes are conventionally uppercase)
|
|
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) { // 1 minute to 60 minutes
|
|
_prefs->mqtt_status_interval = minutes * 60000; // Convert minutes to milliseconds
|
|
savePrefs();
|
|
// Restart bridge to pick up new interval value
|
|
_callbacks->restartBridge();
|
|
sprintf(reply, "OK - interval set to %u minutes (%lu ms), bridge restarted", minutes, _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();
|
|
|
|
// Apply immediately if WiFi is connected
|
|
#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 {
|
|
sprintf(reply, "OK - slot %d preset: %s", slot + 1, preset_name);
|
|
}
|
|
}
|
|
} else {
|
|
strcpy(reply, "Error: valid presets are: analyzer-us, analyzer-eu, meshmapper, meshrank, waev, meshomatic, cascadiamesh, tennmesh, custom, none");
|
|
}
|
|
} 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();
|
|
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();
|
|
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.owner ", 11) == 0) {
|
|
// Validate that it's a valid hex string of the correct length (64 hex chars = 32 bytes)
|
|
const char* owner_key = &config[11];
|
|
int key_len = strlen(owner_key);
|
|
if (key_len == 64) {
|
|
// Validate hex characters
|
|
bool valid = 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 = false;
|
|
break;
|
|
}
|
|
}
|
|
if (valid) {
|
|
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 {
|
|
sprintf(reply, "unknown config: %s", config);
|
|
}
|
|
} 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
|
|
}
|
|
}
|
|
#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) {
|
|
_prefs->powersaving_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "ok"); // TODO: to return Not supported if required
|
|
} else if (memcmp(command, "powersaving off", 15) == 0) {
|
|
_prefs->powersaving_enabled = 0;
|
|
savePrefs();
|
|
strcpy(reply, "ok");
|
|
} 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");
|
|
}
|
|
}
|