#include #include "CommonCLI.h" #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include "TxtDataHelpers.h" #include #if defined(NRF52_PLATFORM) #include #include #ifndef DFU_MAGIC_UF2_RESET #define DFU_MAGIC_UF2_RESET 0x57 #endif static void resetToUf2Bootloader() { uint8_t sd_enabled = 0; sd_softdevice_is_enabled(&sd_enabled); if (sd_enabled) { sd_power_gpregret_clr(0, 0xFF); sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET); } else { NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET; } NVIC_SystemReset(); } #endif #define STR_HELPER(x) #x #define STR(x) STR_HELPER(x) #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 #endif #define RECENT_REPEATER_PREFIX_MAX_BYTES 3 // 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 parseRecentRepeaterGet(const char* config, int& page) { if (strncmp(config, "recent.repeater", 15) != 0) { return false; } const char* cursor = &config[15]; if (*cursor == 's') { cursor++; } if (*cursor != 0 && *cursor != ' ') { return false; } while (*cursor == ' ') cursor++; if (strncmp(cursor, "page", 4) == 0 && (cursor[4] == 0 || cursor[4] == ' ')) { cursor += 4; while (*cursor == ' ') cursor++; } page = 1; if (*cursor) page = _atoi(cursor); if (page < 1) page = 1; return true; } static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; n++; } return true; } static bool looksNumeric(const char* s) { if (s == NULL) return false; while (*s == ' ') s++; if (*s == '-' || *s == '+') s++; bool saw_digit = false; bool saw_dot = false; while (*s) { if (*s >= '0' && *s <= '9') { saw_digit = true; } else if (*s == '.' && !saw_dot) { saw_dot = true; } else if (*s == ' ') { while (*s == ' ') s++; return saw_digit && *s == 0; } else { break; } s++; } return saw_digit && *s == 0; } static bool looksUnsignedInteger(const char* s) { if (s == NULL) return false; while (*s == ' ') s++; bool saw_digit = false; while (*s) { if (*s >= '0' && *s <= '9') { saw_digit = true; } else if (*s == ' ') { while (*s == ' ') s++; return saw_digit && *s == 0; } else { return false; } s++; } return saw_digit; } static bool parseUint8Strict(const char* value, uint8_t min_value, uint8_t max_value, uint8_t& result) { if (value == NULL || *value == 0) { return false; } uint16_t parsed = 0; const char* sp = value; while (*sp) { if (*sp < '0' || *sp > '9') { return false; } parsed = (uint16_t)((parsed * 10) + (*sp - '0')); if (parsed > max_value) { return false; } sp++; } if (parsed < min_value) { return false; } result = (uint8_t)parsed; return true; } static bool bwMatches(float bw, float allowed) { float diff = bw - allowed; if (diff < 0.0f) diff = -diff; return diff <= 0.001f; } static bool isValidLoRaBandwidth(float bw) { #if defined(USE_LR1110) return bwMatches(bw, 62.5f) || bwMatches(bw, 125.0f) || bwMatches(bw, 250.0f) || bwMatches(bw, 500.0f); #elif defined(USE_LLCC68) || defined(USE_SX1272) return bwMatches(bw, 125.0f) || bwMatches(bw, 250.0f) || bwMatches(bw, 500.0f); #else return bwMatches(bw, 7.8f) || bwMatches(bw, 10.4f) || bwMatches(bw, 15.6f) || bwMatches(bw, 20.8f) || bwMatches(bw, 31.25f) || bwMatches(bw, 41.7f) || bwMatches(bw, 62.5f) || bwMatches(bw, 125.0f) || bwMatches(bw, 250.0f) || bwMatches(bw, 500.0f); #endif } static float defaultLoRaBandwidth() { #ifdef LORA_BW if (isValidLoRaBandwidth((float)LORA_BW)) { return (float)LORA_BW; } #endif return 125.0f; } static const char* skipSpacesConst(const char* s) { while (s != NULL && *s == ' ') s++; return s; } static bool parseUint32Strict(const char* s, uint32_t& out) { if (!looksUnsignedInteger(s)) { return false; } uint64_t n = 0; s = skipSpacesConst(s); while (*s >= '0' && *s <= '9') { n = (n * 10) + (uint32_t)(*s - '0'); if (n > 0xFFFFFFFFULL) { return false; } s++; } out = (uint32_t)n; return true; } static int countSeparatedParts(const char* s, char separator) { if (s == NULL || *s == 0) { return 0; } int count = 1; while (*s) { if (*s++ == separator) { count++; } } return count; } static bool parseScheduledRadioArgs(const char* args, bool temporary, float& freq, float& bw, uint8_t& sf, uint8_t& cr, uint32_t& start_time, uint32_t& end_time) { const int expected_parts = temporary ? 6 : 5; args = skipSpacesConst(args); if (countSeparatedParts(args, ',') != expected_parts) { return false; } char local[96]; if (strlen(args) >= sizeof(local)) { return false; } StrHelper::strncpy(local, args, sizeof(local)); const char* parts[6]; int num = mesh::Utils::parseTextParts(local, parts, expected_parts, ','); if (num != expected_parts) { return false; } uint32_t sf_u32 = 0; uint32_t cr_u32 = 0; if (!looksNumeric(parts[0]) || !looksNumeric(parts[1]) || !parseUint32Strict(parts[2], sf_u32) || !parseUint32Strict(parts[3], cr_u32) || !parseUint32Strict(parts[4], start_time)) { return false; } if (sf_u32 > 255 || cr_u32 > 255) { return false; } freq = atof(parts[0]); bw = atof(parts[1]); sf = (uint8_t)sf_u32; cr = (uint8_t)cr_u32; if (temporary && !parseUint32Strict(parts[5], end_time)) { return false; } if (!temporary) { end_time = 0; } return true; } static int16_t parseSnrDbX4(const char* s) { float db = atof(s); return (int16_t)(db * 4.0f + (db >= 0.0f ? 0.5f : -0.5f)); } static void formatSnrDbX4(char* dest, size_t dest_len, int16_t snr_x4) { int16_t v = snr_x4; const char* sign = ""; if (v < 0) { sign = "-"; v = -v; } snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25); } static const char* retryPresetName(uint8_t preset) { switch (preset) { case RETRY_PRESET_INFRA: return "infra"; case RETRY_PRESET_ROOFTOP: return "rooftop"; case RETRY_PRESET_MOBILE: return "mobile"; default: return "custom"; } } static void markDirectRetryPrefsValid(NodePrefs* prefs) { prefs->direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0; prefs->direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1; } static void applyFloodRetryPreset(NodePrefs* prefs, uint8_t preset) { if (preset == RETRY_PRESET_INFRA) { prefs->flood_retry_attempts = FLOOD_RETRY_INFRA_COUNT; prefs->flood_retry_max_path = FLOOD_RETRY_INFRA_MAX_PATH; } else if (preset == RETRY_PRESET_MOBILE) { prefs->flood_retry_attempts = FLOOD_RETRY_MOBILE_COUNT; prefs->flood_retry_max_path = FLOOD_RETRY_MOBILE_MAX_PATH; } else { prefs->flood_retry_attempts = FLOOD_RETRY_ROOFTOP_COUNT; prefs->flood_retry_max_path = FLOOD_RETRY_ROOFTOP_MAX_PATH; } } static bool parseFloodRetryPathGate(const char* value, uint8_t& path_gate) { if (value == NULL) { return false; } if (strcmp(value, "off") == 0 || strcmp(value, "disabled") == 0 || strcmp(value, "disable") == 0) { path_gate = FLOOD_RETRY_PATH_GATE_DISABLED; return true; } return parseUint8Strict(value, 0, 63, path_gate); } static void formatFloodRetryPathGate(char* dest, uint8_t path_gate) { if (path_gate == FLOOD_RETRY_PATH_GATE_DISABLED) { strcpy(dest, "off"); } else { sprintf(dest, "%u", (unsigned int)path_gate); } } static bool parseFloodChannelBlockHops(const char* value, uint8_t& max_hops) { if (value == NULL) { return false; } value = skipSpacesConst(value); if (strcmp(value, "all") == 0) { max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL; return true; } return parseUint8Strict(value, 1, 7, max_hops); } static bool parseFloodChannelBlockRowHops(const char* value, uint8_t& max_hops) { if (value == NULL) { return false; } value = skipSpacesConst(value); if (strcmp(value, "default") == 0 || strcmp(value, "def") == 0 || strcmp(value, "inherit") == 0) { max_hops = FLOOD_CHANNEL_BLOCK_HOPS_INHERIT; return true; } return parseFloodChannelBlockHops(value, max_hops); } static bool parseFloodChannelBlockHopAssignment(const char* text, bool allow_bare, uint8_t& max_hops) { char token[16]; text = skipSpacesConst(text); if (text == NULL || *text == 0) { return false; } size_t len = 0; while (text[len] && text[len] != ' ' && len + 1 < sizeof(token)) { token[len] = text[len]; len++; } token[len] = 0; const char* value = NULL; if (strncmp(token, "h=", 2) == 0) { value = token + 2; } else if (strncmp(token, "hops=", 5) == 0) { value = token + 5; } else if (allow_bare) { value = token; } else { return false; } return parseFloodChannelBlockRowHops(value, max_hops); } static bool looksFloodChannelBlockHopAssignment(const char* text) { text = skipSpacesConst(text); if (text == NULL || *text == 0) { return false; } return (*text >= '0' && *text <= '9') || strncmp(text, "h=", 2) == 0 || strncmp(text, "hops=", 5) == 0 || strncmp(text, "all", 3) == 0 || strncmp(text, "def", 3) == 0 || strncmp(text, "default", 7) == 0 || strncmp(text, "inherit", 7) == 0; } static bool trimFloodChannelBlockHopSuffix(char* name, uint8_t& max_hops) { size_t len = strlen(name); while (len > 0 && name[len - 1] == ' ') { name[--len] = 0; } char* token = strrchr(name, ' '); if (token == NULL) { return true; } if (strncmp(token + 1, "h=", 2) != 0 && strncmp(token + 1, "hops=", 5) != 0) { return true; } if (!parseFloodChannelBlockHopAssignment(token + 1, false, max_hops)) { return false; } *token = 0; return strlen(name) > 0; } static void formatFloodChannelBlockHops(char* dest, uint8_t max_hops) { if (max_hops == FLOOD_CHANNEL_BLOCK_HOPS_ALL) { strcpy(dest, "h=all"); } else { sprintf(dest, "h>%u", (unsigned int)max_hops); } } static void formatFloodRetryPrefixList(char* dest, const uint8_t prefixes[][FLOOD_RETRY_PREFIX_LEN], uint8_t max_prefixes) { char* out = dest; bool first = true; for (int i = 0; i < max_prefixes; i++) { const uint8_t* prefix = prefixes[i]; if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { continue; } if (!first) { *out++ = ','; } mesh::Utils::toHex(out, prefix, FLOOD_RETRY_PREFIX_LEN); out += FLOOD_RETRY_PREFIX_LEN * 2; first = false; } *out = 0; } static bool parseFloodRetryPrefixList(uint8_t dest[][FLOOD_RETRY_PREFIX_LEN], uint8_t max_prefixes, const char* value) { if (max_prefixes > FLOOD_RETRY_LIST_PREFIXES) { return false; } uint8_t parsed[FLOOD_RETRY_LIST_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; memset(parsed, 0, sizeof(parsed)); if (value == NULL || value[0] == 0 || strcmp(value, "none") == 0 || strcmp(value, "off") == 0) { memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN); return true; } char local[FLOOD_RETRY_LIST_TEXT_MAX]; StrHelper::strncpy(local, value, sizeof(local)); const char* parts[FLOOD_RETRY_LIST_PREFIXES + 1]; int num = mesh::Utils::parseTextParts(local, parts, FLOOD_RETRY_LIST_PREFIXES + 1); if (num > max_prefixes) { return false; } for (int i = 0; i < num; i++) { if (strlen(parts[i]) != FLOOD_RETRY_PREFIX_LEN * 2) { return false; } for (int j = 0; j < FLOOD_RETRY_PREFIX_LEN * 2; j++) { if (!mesh::Utils::isHexChar(parts[i][j])) { return false; } } if (!mesh::Utils::fromHex(parsed[i], FLOOD_RETRY_PREFIX_LEN, parts[i]) || (parsed[i][0] == 0 && parsed[i][1] == 0 && parsed[i][2] == 0)) { return false; } } memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN); return true; } static bool parseFloodChannelBlockKey(const char* text, uint8_t secret[PUB_KEY_SIZE], uint8_t& key_len) { if (text == NULL || text[0] == 0) { return false; } memset(secret, 0, PUB_KEY_SIZE); if (text[0] == '#') { if (!isValidName(text)) { return false; } mesh::Utils::sha256(secret, CIPHER_KEY_SIZE, (const uint8_t*)text, strlen(text)); key_len = CIPHER_KEY_SIZE; return true; } size_t hex_len = strlen(text); if (!(hex_len == CIPHER_KEY_SIZE * 2 || hex_len == PUB_KEY_SIZE * 2)) { return false; } for (size_t i = 0; i < hex_len; i++) { if (!mesh::Utils::isHexChar(text[i])) { return false; } } key_len = (uint8_t)(hex_len / 2); return mesh::Utils::fromHex(secret, key_len, text); } static bool parseFloodChannelBlockDotIndex(const char*& cursor, int& index) { if (*cursor != '.') { index = 0; return true; } cursor++; if (*cursor < '0' || *cursor > '9') { return false; } int value = 0; while (*cursor >= '0' && *cursor <= '9') { value = (value * 10) + (*cursor - '0'); if (value > FLOOD_CHANNEL_BLOCK_SLOTS) { return false; } cursor++; } if (value < 1) { return false; } index = value; return true; } static void copyTrimmedFloodChannelBlockName(char* dest, size_t dest_len, const char* src) { src = skipSpacesConst(src); StrHelper::strncpy(dest, src, dest_len); size_t len = strlen(dest); while (len > 0 && dest[len - 1] == ' ') { dest[--len] = 0; } } static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { prefs->retry_preset = preset; if (preset == RETRY_PRESET_INFRA) { prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT; prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS; prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS; prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_INFRA_MARGIN_X4; } else if (preset == RETRY_PRESET_MOBILE) { prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT; prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS; prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS; prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_MOBILE_MARGIN_X4; } else { prefs->retry_preset = RETRY_PRESET_ROOFTOP; prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4; } applyFloodRetryPreset(prefs, prefs->retry_preset); markDirectRetryPrefsValid(prefs); } static void setDefaultDirectRetryPrefs(NodePrefs* prefs) { applyDirectRetryPreset(prefs, RETRY_PRESET_ROOFTOP); prefs->direct_retry_cr_enabled = 1; prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; prefs->direct_retry_enabled = 1; prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT; markDirectRetryPrefsValid(prefs); } static bool directRetryPrefsValid(const NodePrefs* prefs) { return prefs->direct_retry_prefs_magic[0] == DIRECT_RETRY_PREFS_MAGIC_0 && prefs->direct_retry_prefs_magic[1] == DIRECT_RETRY_PREFS_MAGIC_1; } static bool parseRetryPreset(const char* s, uint8_t& preset) { if (strcmp(s, "infra") == 0 || strcmp(s, "0") == 0) { preset = RETRY_PRESET_INFRA; return true; } if (strcmp(s, "rooftop") == 0 || strcmp(s, "1") == 0) { preset = RETRY_PRESET_ROOFTOP; return true; } if (strcmp(s, "mobile") == 0 || strcmp(s, "2") == 0) { preset = RETRY_PRESET_MOBILE; return true; } return false; } static bool parseHashPrefix(const char* text, uint8_t* prefix, uint8_t& prefix_len) { size_t hex_len = strlen(text); if (hex_len == 0 || (hex_len & 1) || hex_len > RECENT_REPEATER_PREFIX_MAX_BYTES * 2) { return false; } for (size_t i = 0; i < hex_len; i++) { if (!mesh::Utils::isHexChar(text[i])) { return false; } } prefix_len = hex_len / 2; return mesh::Utils::fromHex(prefix, prefix_len, text); } static void formatSnrDbX4Short(char* dest, size_t dest_len, int16_t snr_x4) { formatSnrDbX4(dest, dest_len, snr_x4); size_t len = strlen(dest); if (len > 3 && dest[len - 1] == '0') { dest[len - 1] = 0; } } void CommonCLI::loadPrefs(FILESYSTEM* fs) { if (fs->exists("/com_prefs")) { loadPrefsInt(fs, "/com_prefs"); // new filename } else if (fs->exists("/node_prefs")) { loadPrefsInt(fs, "/node_prefs"); savePrefs(fs); // save to new filename fs->remove("/node_prefs"); // remove old } } 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 : 4 bytes unused file.read((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.read((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.read((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 file.read((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 file.read((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 file.read((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122 file.read(pad, 1); // 123 file.read((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.read((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.read((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 file.read((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127 file.read((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128 file.read((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130 file.read((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131 file.read((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.read((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.read((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 file.read((uint8_t *)&_prefs->reboot_interval, sizeof(_prefs->reboot_interval)); // 153 file.read(pad, 2); // 154 file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161 file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 // next: 295 file.read((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295 file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296 file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297 file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299 file.read((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301 file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303 file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 file.read((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307 file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308 file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309 memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes)); _prefs->flood_retry_bridge_enabled = 0; memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets)); memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes)); _prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT; _prefs->battery_alert_enabled = 0; _prefs->battery_alert_low_percent = BATTERY_ALERT_LOW_PERCENT_DEFAULT; _prefs->battery_alert_critical_percent = BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT; _prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT; _prefs->flood_channel_data_enabled = 1; _prefs->flood_channel_block_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL; bool has_flood_retry_prefs = file.available() >= 2; if (has_flood_retry_prefs) { file.read((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 311 file.read((uint8_t *)&_prefs->flood_retry_max_path, sizeof(_prefs->flood_retry_max_path)); // 312 if (file.available() >= (int)sizeof(_prefs->flood_retry_prefixes)) { file.read((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); } if (file.available() >= (int)sizeof(_prefs->flood_retry_bridge_enabled)) { file.read((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); } if (file.available() >= (int)sizeof(_prefs->flood_retry_bridge_buckets)) { file.read((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); } if (file.available() >= (int)sizeof(_prefs->flood_retry_ignore_prefixes)) { file.read((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], sizeof(_prefs->flood_retry_ignore_prefixes)); } if (file.available() >= (int)sizeof(_prefs->flood_retry_advert_enabled)) { file.read((uint8_t *)&_prefs->flood_retry_advert_enabled, sizeof(_prefs->flood_retry_advert_enabled)); } if (file.available() >= (int)sizeof(_prefs->battery_alert_enabled)) { file.read((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled)); } if (file.available() >= (int)sizeof(_prefs->battery_alert_low_percent)) { file.read((uint8_t *)&_prefs->battery_alert_low_percent, sizeof(_prefs->battery_alert_low_percent)); } if (file.available() >= (int)sizeof(_prefs->battery_alert_critical_percent)) { file.read((uint8_t *)&_prefs->battery_alert_critical_percent, sizeof(_prefs->battery_alert_critical_percent)); } if (file.available() >= (int)sizeof(_prefs->direct_retry_recent_enabled)) { file.read((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled)); } if (file.available() >= (int)sizeof(_prefs->flood_channel_data_enabled)) { file.read((uint8_t *)&_prefs->flood_channel_data_enabled, sizeof(_prefs->flood_channel_data_enabled)); } if (file.available() >= (int)sizeof(_prefs->flood_channel_block_max_hops)) { file.read((uint8_t *)&_prefs->flood_channel_block_max_hops, sizeof(_prefs->flood_channel_block_max_hops)); } } // next: 674 // 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 = isValidLoRaBandwidth(_prefs->bw) ? _prefs->bw : defaultLoRaBandwidth(); _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 // 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->reboot_interval = constrain(_prefs->reboot_interval, 0, 255); _prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1); _prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2); // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean if (!directRetryPrefsValid(_prefs)) { setDefaultDirectRetryPrefs(_prefs); memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes)); _prefs->flood_retry_bridge_enabled = 0; memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets)); memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes)); _prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT; } else if (!has_flood_retry_prefs) { applyFloodRetryPreset(_prefs, _prefs->retry_preset); } if (_prefs->retry_preset > RETRY_PRESET_MOBILE && _prefs->retry_preset != RETRY_PRESET_CUSTOM) { _prefs->retry_preset = RETRY_PRESET_CUSTOM; } _prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, 1, 15); _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, 10, 5000); _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, 0, 5000); _prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160); _prefs->direct_retry_enabled = constrain(_prefs->direct_retry_enabled, 0, 1); _prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1); _prefs->flood_retry_attempts = constrain(_prefs->flood_retry_attempts, 0, 15); if (_prefs->flood_retry_max_path != FLOOD_RETRY_PATH_GATE_DISABLED) { _prefs->flood_retry_max_path = constrain(_prefs->flood_retry_max_path, 0, 63); } _prefs->flood_retry_bridge_enabled = constrain(_prefs->flood_retry_bridge_enabled, 0, 1); _prefs->flood_retry_advert_enabled = constrain(_prefs->flood_retry_advert_enabled, 0, 1); _prefs->battery_alert_enabled = constrain(_prefs->battery_alert_enabled, 0, 1); _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); _prefs->flood_channel_data_enabled = constrain(_prefs->flood_channel_data_enabled, 0, 1); if (_prefs->flood_channel_block_max_hops != FLOOD_CHANNEL_BLOCK_HOPS_ALL && (_prefs->flood_channel_block_max_hops < 1 || _prefs->flood_channel_block_max_hops > 7)) { _prefs->flood_channel_block_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL; } if (_prefs->battery_alert_low_percent < 1 || _prefs->battery_alert_low_percent > 100 || _prefs->battery_alert_critical_percent >= _prefs->battery_alert_low_percent) { _prefs->battery_alert_low_percent = BATTERY_ALERT_LOW_PERCENT_DEFAULT; _prefs->battery_alert_critical_percent = BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT; } 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 : 4 byte unused file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 file.write((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122 file.write(pad, 1); // 123 file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 file.write((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127 file.write((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128 file.write((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130 file.write((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131 file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 file.write((uint8_t *)&_prefs->reboot_interval, sizeof(_prefs->reboot_interval)); // 153 file.write(pad, 2); // 154 file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 // next: 295 markDirectRetryPrefsValid(_prefs); file.write((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295 file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296 file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297 file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299 file.write((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301 file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303 file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 file.write((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307 file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308 file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309 file.write((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 311 file.write((uint8_t *)&_prefs->flood_retry_max_path, sizeof(_prefs->flood_retry_max_path)); // 312 file.write((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 313 file.write((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); file.write((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); file.write((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], sizeof(_prefs->flood_retry_ignore_prefixes)); file.write((uint8_t *)&_prefs->flood_retry_advert_enabled, sizeof(_prefs->flood_retry_advert_enabled)); file.write((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled)); file.write((uint8_t *)&_prefs->battery_alert_low_percent, sizeof(_prefs->battery_alert_low_percent)); file.write((uint8_t *)&_prefs->battery_alert_critical_percent, sizeof(_prefs->battery_alert_critical_percent)); file.write((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled)); file.write((uint8_t *)&_prefs->flood_channel_data_enabled, sizeof(_prefs->flood_channel_data_enabled)); file.write((uint8_t *)&_prefs->flood_channel_block_max_hops, sizeof(_prefs->flood_channel_block_max_hops)); // next: 674 file.close(); } } #define MIN_LOCAL_ADVERT_INTERVAL 60 void CommonCLI::savePrefs() { if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) { _prefs->advert_interval = 0; // turn it off, now that device has been manually configured } _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 (sender_timestamp == 0 && memcmp(command, "uf2reset", 8) == 0 && (command[8] == 0 || command[8] == ' ')) { #if defined(NRF52_PLATFORM) resetToUf2Bootloader(); // doesn't return #else strcpy(reply, "ERR: unsupported"); #endif } 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, "start ota", 9) == 0 && (command[9] == 0 || command[9] == ' ')) { if (!_board->startOTAUpdate(_prefs->node_name, reply)) { strcpy(reply, "Error"); } } else if (memcmp(command, "stop ota", 8) == 0 && (command[8] == 0 || command[8] == ' ')) { if (!_board->stopOTAUpdate(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 && isValidLoRaBandwidth(bw) && 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: "); StrHelper::strncpy(&reply[14], _prefs->password, 160-15); // echo back just to let admin know for sure!! } else if (memcmp(command, "clear stats", 11) == 0) { _callbacks->clearStats(); strcpy(reply, "(OK - stats reset)"); } else if (memcmp(command, "clear recent.repeater", 21) == 0 && (command[21] == 0 || command[21] == ' ')) { _callbacks->clearRecentRepeaters(); strcpy(reply, "OK"); } else if (memcmp(command, "get ", 4) == 0) { handleGetCmd(sender_timestamp, command, reply); } else if (memcmp(command, "set ", 4) == 0) { handleSetCmd(sender_timestamp, command, reply); } else if (memcmp(command, "del ", 4) == 0) { handleDelCmd(command, reply); } else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) { bool s = _callbacks->formatFileSystem(); sprintf(reply, "File system erase: %s", s ? "OK" : "Err"); } else if (memcmp(command, "ver", 3) == 0) { sprintf(reply, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate()); } else if (memcmp(command, "board", 5) == 0) { sprintf(reply, "%s", _board->getManufacturerName()); } 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) if (sender_timestamp == 0 || _board->isUsbDataConnected()) { strcpy(reply, "Error: USB serial connected"); } else { _prefs->powersaving_enabled = 1; savePrefs(); strcpy(reply, "on - Immediate effect"); } #elif defined(ESP32) && !defined(WITH_BRIDGE) if (sender_timestamp == 0 || _board->isUsbDataConnected()) { strcpy(reply, "Error: USB serial connected"); } else { _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, "sensor", 6) == 0) { // I2C #if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL) sprintf(reply, "I2C Wire1: SDA=%s,SCL=%s\r\n", STR(ENV_PIN_SDA), STR(ENV_PIN_SCL)); #elif defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n", STR(PIN_BOARD_SDA), STR(PIN_BOARD_SCL)); #elif defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n", STR(PIN_WIRE_SDA), STR(PIN_WIRE_SCL)); #else sprintf(reply, "I2C GPIOs not defined\r\n"); #endif // GPS #if defined(PIN_GPS_RX) && defined(PIN_GPS_TX) sprintf(reply + strlen(reply), "GPS Serial: RX=%s, TX=%s", STR(PIN_GPS_RX), STR(PIN_GPS_TX)); #if defined(ENV_INCLUDE_GPS) && ENV_INCLUDE_GPS > 0 sprintf(reply + strlen(reply), ". Configured"); #else sprintf(reply + strlen(reply), ". Not configured"); #endif #else sprintf(reply + strlen(reply), "GPS Serial not defined"); #endif } else if (memcmp(command, "powerlog", 8) == 0) { sprintf(reply, "Last reset reason: %s", _board->getResetReasonString(_board->getResetReason())); #if defined(NRF52_PLATFORM) sprintf(reply + strlen(reply), "\r\nLast shutdown reason: %s", _board->getShutdownReasonString(_board->getShutdownReason())); sprintf(reply + strlen(reply), "\r\nLast boot voltage: %u mV", _board->getBootVoltage()); #endif } else if (memcmp(command, "log start", 9) == 0) { _callbacks->setLoggingOn(true); strcpy(reply, " logging on"); } else if (memcmp(command, "log stop", 8) == 0) { _callbacks->setLoggingOn(false); strcpy(reply, " logging off"); } else if (memcmp(command, "log erase", 9) == 0) { _callbacks->eraseLogFile(); strcpy(reply, " log erased"); } else if (sender_timestamp == 0 && memcmp(command, "log", 3) == 0) { _callbacks->dumpLogFile(); strcpy(reply, " EOF"); } else if (sender_timestamp == 0 && memcmp(command, "stats-packets", 13) == 0 && (command[13] == 0 || command[13] == ' ')) { _callbacks->formatPacketStatsReply(reply); } else if (sender_timestamp == 0 && memcmp(command, "stats-radio", 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, "cad ", 4) == 0) { _prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0; savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { _prefs->agc_reset_interval = atoi(&config[19]) / 4; savePrefs(); sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4); } else if (memcmp(config, "multi.acks ", 11) == 0) { _prefs->multi_acks = atoi(&config[11]); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "allow.read.only ", 16) == 0) { _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "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"); } else if (memcmp(config, "radio.rxgain ", 13) == 0) { bool enabled = memcmp(&config[13], "on", 2) == 0; _prefs->rx_boosted_gain = enabled; savePrefs(); if (_callbacks->setRxBoostedGain(enabled)) { strcpy(reply, "OK"); } else { strcpy(reply, "Error: unsupported"); } } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); } else if (memcmp(&config[17], "on", 2) == 0) { if (_board->setLoRaFemLnaEnabled(true)) { _prefs->radio_fem_rxgain = 1; savePrefs(); strcpy(reply, "OK - LoRa FEM RX gain on"); } else { strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); } } else if (memcmp(&config[17], "off", 3) == 0) { if (_board->setLoRaFemLnaEnabled(false)) { _prefs->radio_fem_rxgain = 0; savePrefs(); strcpy(reply, "OK - LoRa FEM RX gain off"); } else { strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); } } else { strcpy(reply, "Error: state must be on or off"); } } else if (memcmp(config, "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 && isValidLoRaBandwidth(bw)) { _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, "radioat ", 8) == 0) { float freq, bw; uint8_t sf, cr; uint32_t start_time, end_time; if (!parseScheduledRadioArgs(&config[8], false, freq, bw, sf, cr, start_time, end_time)) { strcpy(reply, "Error, use: set radioat f,bw,sf,cr,start"); } else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)) { strcpy(reply, "Error, invalid radio params"); } else { _callbacks->addScheduledRadioParams(false, freq, bw, sf, cr, start_time, end_time, reply); } } else if (memcmp(config, "tempradioat ", 12) == 0) { float freq, bw; uint8_t sf, cr; uint32_t start_time, end_time; if (!parseScheduledRadioArgs(&config[12], true, freq, bw, sf, cr, start_time, end_time)) { strcpy(reply, "Error, use: set tempradioat f,bw,sf,cr,start,end"); } else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)) { strcpy(reply, "Error, invalid radio params"); } else { _callbacks->addScheduledRadioParams(true, freq, bw, sf, cr, start_time, end_time, reply); } } 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, "retry.preset ", 13) == 0) { uint8_t preset; if (parseRetryPreset(&config[13], preset)) { applyDirectRetryPreset(_prefs, preset); savePrefs(); sprintf(reply, "OK - %s", retryPresetName(_prefs->retry_preset)); } else { strcpy(reply, "Error, must be infra, rooftop, or mobile"); } } else if (memcmp(config, "direct.retry ", 13) == 0) { if (strcmp(&config[13], "on") == 0) { _prefs->direct_retry_enabled = 1; savePrefs(); strcpy(reply, "OK"); } else if (strcmp(&config[13], "off") == 0) { _prefs->direct_retry_enabled = 0; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be on or off"); } } else if (memcmp(config, "direct.retry.heard ", 19) == 0) { if (strcmp(&config[19], "on") == 0) { _prefs->direct_retry_recent_enabled = 1; savePrefs(); strcpy(reply, "OK"); } else if (strcmp(&config[19], "off") == 0) { _prefs->direct_retry_recent_enabled = 0; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be on or off"); } } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { if (!looksNumeric(&config[20])) { strcpy(reply, "Error, must be 0-40 dB"); } else { int16_t margin_x4 = parseSnrDbX4(&config[20]); if (margin_x4 >= 0 && margin_x4 <= 160) { _prefs->direct_retry_snr_margin_x4 = (uint16_t)margin_x4; _prefs->retry_preset = RETRY_PRESET_CUSTOM; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-40 dB"); } } } else if (memcmp(config, "direct.retry.count ", 19) == 0) { int attempts = looksUnsignedInteger(&config[19]) ? _atoi(&config[19]) : -1; if (attempts >= 1 && attempts <= 15) { _prefs->direct_retry_attempts = (uint8_t)attempts; _prefs->retry_preset = RETRY_PRESET_CUSTOM; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 1-15"); } } else if (memcmp(config, "direct.retry.base ", 18) == 0) { int base_ms = looksUnsignedInteger(&config[18]) ? _atoi(&config[18]) : -1; if (base_ms >= 10 && base_ms <= 5000) { _prefs->direct_retry_base_ms = (uint16_t)base_ms; _prefs->retry_preset = RETRY_PRESET_CUSTOM; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 10-5000 ms"); } } else if (memcmp(config, "direct.retry.step ", 18) == 0) { int step_ms = looksUnsignedInteger(&config[18]) ? _atoi(&config[18]) : -1; if (step_ms >= 0 && step_ms <= 5000) { _prefs->direct_retry_step_ms = (uint16_t)step_ms; _prefs->retry_preset = RETRY_PRESET_CUSTOM; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-5000 ms"); } } else if (memcmp(config, "flood.channel.data ", 19) == 0) { if (strcmp(&config[19], "on") == 0) { _prefs->flood_channel_data_enabled = 1; savePrefs(); strcpy(reply, "OK"); } else if (strcmp(&config[19], "off") == 0) { _prefs->flood_channel_data_enabled = 0; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be on or off"); } } else if (memcmp(config, "flood.channel.block.hops ", 25) == 0) { uint8_t max_hops; if (parseFloodChannelBlockHops(&config[25], max_hops)) { _prefs->flood_channel_block_max_hops = max_hops; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be all or 1-7"); } } else if (memcmp(config, "flood.channel.block", 19) == 0 && (config[19] == ' ' || config[19] == '.')) { const char* cursor = &config[19]; int index = 0; if (!parseFloodChannelBlockDotIndex(cursor, index)) { sprintf(reply, "Error, index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS); return; } cursor = skipSpacesConst(cursor); const char* key_start = cursor; while (*cursor && *cursor != ' ') cursor++; size_t key_len_text = cursor - key_start; char key_text[PUB_KEY_SIZE * 2 + 1]; if (key_len_text == 0 || key_len_text >= sizeof(key_text)) { strcpy(reply, "Error, use: set flood.channel.block[.n] |#channel"); return; } memcpy(key_text, key_start, key_len_text); key_text[key_len_text] = 0; char name[FLOOD_CHANNEL_BLOCK_NAME_LEN]; uint8_t block_hops = FLOOD_CHANNEL_BLOCK_HOPS_INHERIT; if (key_text[0] == '#') { StrHelper::strncpy(name, key_text, sizeof(name)); const char* extra = skipSpacesConst(cursor); if (*extra && looksFloodChannelBlockHopAssignment(extra) && !parseFloodChannelBlockHopAssignment(extra, true, block_hops)) { strcpy(reply, "Error, hops must be all, default, or 1-7"); return; } } else { copyTrimmedFloodChannelBlockName(name, sizeof(name), cursor); if (!trimFloodChannelBlockHopSuffix(name, block_hops)) { strcpy(reply, "Error, bad name or hops"); return; } } uint8_t secret[PUB_KEY_SIZE]; uint8_t decoded_key_len = 0; if (!parseFloodChannelBlockKey(key_text, secret, decoded_key_len)) { strcpy(reply, "Error, key must be 128/256-bit hex or #channel"); } else if (name[0] == 0 || !isValidName(name)) { strcpy(reply, "Error, bad name"); } else { _callbacks->setFloodChannelBlock(index, secret, decoded_key_len, name, block_hops, reply); } } else if (memcmp(config, "flood.retry.count ", 18) == 0) { int attempts = looksUnsignedInteger(&config[18]) ? _atoi(&config[18]) : -1; if (attempts >= 0 && attempts <= 15) { _prefs->flood_retry_attempts = (uint8_t)attempts; _prefs->retry_preset = RETRY_PRESET_CUSTOM; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-15"); } } else if (memcmp(config, "flood.retry.path ", 17) == 0) { uint8_t path_gate; if (parseFloodRetryPathGate(&config[17], path_gate)) { _prefs->flood_retry_max_path = path_gate; _prefs->retry_preset = RETRY_PRESET_CUSTOM; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-63 or off"); } } else if (memcmp(config, "flood.retry.prefixes ", 21) == 0) { if (parseFloodRetryPrefixList(_prefs->flood_retry_prefixes, FLOOD_RETRY_PREFIX_SLOTS, &config[21])) { savePrefs(); strcpy(reply, "OK"); } else { sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", (unsigned int)FLOOD_RETRY_PREFIX_SLOTS); } } else if (memcmp(config, "flood.retry.ignore ", 19) == 0) { if (parseFloodRetryPrefixList(_prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES, &config[19])) { savePrefs(); strcpy(reply, "OK"); } else { sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", (unsigned int)FLOOD_RETRY_IGNORE_PREFIXES); } } else if (memcmp(config, "flood.retry.advert ", 19) == 0) { if (strcmp(&config[19], "on") == 0) { _prefs->flood_retry_advert_enabled = 1; savePrefs(); strcpy(reply, "OK"); } else if (strcmp(&config[19], "off") == 0) { _prefs->flood_retry_advert_enabled = 0; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be on or off"); } } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { if (strcmp(&config[19], "on") == 0) { _prefs->flood_retry_bridge_enabled = 1; savePrefs(); strcpy(reply, "OK"); } else if (strcmp(&config[19], "off") == 0) { _prefs->flood_retry_bridge_enabled = 0; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be on or off"); } } else if (memcmp(config, "flood.retry.bucket ", 19) == 0) { const char* params = &config[19]; uint8_t bucket = atoi(params); const char* list = strchr(params, ' '); if (bucket < 1 || bucket > FLOOD_RETRY_BRIDGE_BUCKETS || list == NULL || *(list + 1) == 0) { sprintf(reply, "Error, usage: set flood.retry.bucket <1-%d> ", FLOOD_RETRY_BRIDGE_BUCKETS); } else if (parseFloodRetryPrefixList(_prefs->flood_retry_bridge_buckets[bucket - 1], FLOOD_RETRY_BUCKET_PREFIXES, list + 1)) { savePrefs(); strcpy(reply, "OK"); } else { sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", (unsigned int)FLOOD_RETRY_BUCKET_PREFIXES); } } else if (memcmp(config, "direct.retry.cr ", 16) == 0) { if (strcmp(&config[16], "off") == 0) { _prefs->direct_retry_cr_enabled = 0; savePrefs(); strcpy(reply, "OK"); } else { strcpy(tmp, &config[16]); const char *parts[4]; int num = mesh::Utils::parseTextParts(tmp, parts, 4, ','); if (num == 4 && looksNumeric(parts[0]) && looksNumeric(parts[1]) && looksNumeric(parts[2]) && looksNumeric(parts[3])) { int16_t cr4 = parseSnrDbX4(parts[0]); int16_t cr5 = parseSnrDbX4(parts[1]); int16_t cr7 = parseSnrDbX4(parts[2]); int16_t cr8 = parseSnrDbX4(parts[3]); if (cr4 >= -128 && cr4 <= 127 && cr5 >= -128 && cr5 <= 127 && cr7 >= -128 && cr7 <= 127 && cr8 >= -128 && cr8 <= 127) { _prefs->direct_retry_cr4_snr_x4 = (int8_t)cr4; _prefs->direct_retry_cr5_snr_x4 = (int8_t)cr5; _prefs->direct_retry_cr7_snr_x4 = (int8_t)cr7; _prefs->direct_retry_cr8_snr_x4 = (int8_t)cr8; _prefs->direct_retry_cr_enabled = 1; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, SNR must fit -32.00..31.75 dB"); } } else { strcpy(reply, "Error, use CR4,CR5,CR7,CR8 SNRs or off"); } } } else if (memcmp(config, "recent.repeater ", 16) == 0) { strcpy(tmp, &config[16]); const char *parts[2]; int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' '); uint8_t prefix[MAX_HASH_SIZE]; uint8_t prefix_len = 0; int16_t snr_x4 = 12; // default to +3.0 dB when omitted or invalid if (num < 1 || !parseHashPrefix(parts[0], prefix, prefix_len)) { strcpy(reply, "Error, use: set recent.repeater [snr_db]"); } else if (num > 1 && !looksNumeric(parts[1])) { strcpy(reply, "Error, SNR must be numeric"); } else { if (num > 1) { snr_x4 = parseSnrDbX4(parts[1]); } if (snr_x4 < -128 || snr_x4 > 127) { strcpy(reply, "Error, SNR must fit -32.00..31.75 dB"); } else if (_callbacks->setRecentRepeater(prefix, prefix_len, (int8_t)snr_x4)) { char prefix_hex[RECENT_REPEATER_PREFIX_MAX_BYTES * 2 + 1]; char snr[12]; mesh::Utils::toHex(prefix_hex, prefix, prefix_len); prefix_hex[prefix_len * 2] = 0; formatSnrDbX4Short(snr, sizeof(snr), snr_x4); sprintf(reply, "OK - set %s at %s SNR", prefix_hex, snr); } else { strcpy(reply, "Error, table rejected prefix"); } } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) { *dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars config++; } *dp = 0; savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "path.hash.mode ", 15) == 0) { config += 15; uint8_t mode = atoi(config); if (mode < 3) { _prefs->path_hash_mode = mode; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0,1, or 2"); } } else if (memcmp(config, "loop.detect ", 12) == 0) { config += 12; uint8_t mode; if (memcmp(config, "off", 3) == 0) { mode = LOOP_DETECT_OFF; } else if (memcmp(config, "minimal", 7) == 0) { mode = LOOP_DETECT_MINIMAL; } else if (memcmp(config, "moderate", 8) == 0) { mode = LOOP_DETECT_MODERATE; } else if (memcmp(config, "strict", 6) == 0) { mode = LOOP_DETECT_STRICT; } else { mode = 0xFF; strcpy(reply, "Error, must be: off, minimal, moderate, or strict"); } if (mode != 0xFF) { _prefs->loop_detect = mode; savePrefs(); strcpy(reply, "OK"); } } else if (memcmp(config, "tx ", 3) == 0) { _prefs->tx_power_dbm = atoi(&config[3]); savePrefs(); _callbacks->setTxPower(_prefs->tx_power_dbm); strcpy(reply, "OK"); } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { _prefs->freq = atof(&config[5]); savePrefs(); strcpy(reply, "OK - reboot to apply"); #ifdef WITH_BRIDGE } else if (memcmp(config, "bridge.enabled ", 15) == 0) { _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0; _callbacks->setBridgeState(_prefs->bridge_enabled); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "bridge.delay ", 13) == 0) { int delay = _atoi(&config[13]); if (delay >= 0 && delay <= 10000) { _prefs->bridge_delay = (uint16_t)delay; savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error: delay must be between 0-10000 ms"); } } else if (memcmp(config, "bridge.source ", 14) == 0) { _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0; savePrefs(); strcpy(reply, "OK"); #endif #ifdef WITH_RS232_BRIDGE } else if (memcmp(config, "bridge.baud ", 12) == 0) { uint32_t baud = atoi(&config[12]); if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) { _prefs->bridge_baud = (uint32_t)baud; _callbacks->restartBridge(); savePrefs(); strcpy(reply, "OK"); } else { sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD); } #endif #ifdef WITH_ESPNOW_BRIDGE } else if (memcmp(config, "bridge.channel ", 15) == 0) { int ch = atoi(&config[15]); if (ch > 0 && ch < 15) { _prefs->bridge_channel = (uint8_t)ch; _callbacks->restartBridge(); savePrefs(); strcpy(reply, "OK"); } else { strcpy(reply, "Error: channel must be between 1-14"); } } else if (memcmp(config, "bridge.secret ", 14) == 0) { StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret)); _callbacks->restartBridge(); savePrefs(); strcpy(reply, "OK"); #endif } else if (memcmp(config, "adc.multiplier ", 15) == 0) { _prefs->adc_multiplier = atof(&config[15]); if (_board->setAdcMultiplier(_prefs->adc_multiplier)) { savePrefs(); if (_prefs->adc_multiplier == 0.0f) { strcpy(reply, "OK - using default board multiplier"); } else { sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier); } } else { _prefs->adc_multiplier = 0.0f; strcpy(reply, "Error: unsupported"); }; } else if (memcmp(config, "reboot.interval ", 16) == 0) { int hours = _atoi(&config[16]); if (hours == 0) { _prefs->reboot_interval = 0; savePrefs(); strcpy(reply, "reboot.interval disabled"); } else if (hours < 1 || 255 < hours) { strcpy(reply, "Error: interval range is 1-255 hours"); } else { _prefs->reboot_interval = hours; savePrefs(); sprintf(reply, "OK - reboot.interval set to %d", _prefs->reboot_interval); } } else { strcpy(reply, "unknown config: "); StrHelper::strncpy(&reply[16], config, 160-17); } } void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; int recent_page = 1; if (memcmp(config, "dutycycle", 9) == 0) { float dc = 100.0f / (_prefs->airtime_factor + 1.0f); int dc_int = (int)dc; int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); sprintf(reply, "> %d.%d%%", dc_int, dc_frac); } else if (memcmp(config, "af", 2) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); } else if (memcmp(config, "cad", 3) == 0) { sprintf(reply, "> %s. # channel busy: %u", _prefs->cad_enabled ? "on" : "off", _board->n_cad_busy); } 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)); } else if (memcmp(config, "radio.rxgain", 12) == 0) { sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); } else { sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); } } else if (memcmp(config, "tempradioat", 11) == 0 && (config[11] == 0 || config[11] == ' ')) { _callbacks->formatScheduledRadioParams(true, skipSpacesConst(&config[11]), reply); } else if (memcmp(config, "radioat", 7) == 0 && (config[7] == 0 || config[7] == ' ')) { _callbacks->formatScheduledRadioParams(false, skipSpacesConst(&config[7]), reply); } else if (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.channel.data", 18) == 0) { char hops[8]; formatFloodChannelBlockHops(hops, _prefs->flood_channel_block_max_hops); sprintf(reply, "> %s %s", _prefs->flood_channel_data_enabled ? "on" : "off", hops); } else if (memcmp(config, "flood.channel.block.hops", 24) == 0) { char hops[8]; formatFloodChannelBlockHops(hops, _prefs->flood_channel_block_max_hops); sprintf(reply, "> %s", hops); } else if (memcmp(config, "flood.channel.block", 19) == 0 && (config[19] == 0 || config[19] == ' ' || config[19] == '.')) { const char* cursor = &config[19]; int index = 0; if (!parseFloodChannelBlockDotIndex(cursor, index)) { sprintf(reply, "Error, index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS); return; } cursor = skipSpacesConst(cursor); char selector[8]; if (index > 0 && *cursor != 0) { strcpy(reply, "Error, use index or selector"); return; } if (index > 0) { snprintf(selector, sizeof(selector), "%d", index); _callbacks->formatFloodChannelBlocks(selector, reply); } else { _callbacks->formatFloodChannelBlocks(cursor, reply); } } 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, "retry.preset", 12) == 0) { sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset)); } else if (memcmp(config, "direct.retry", 12) == 0 && (config[12] == 0 || config[12] == ' ')) { sprintf(reply, "> %s", _prefs->direct_retry_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.heard", 18) == 0) { sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { char margin[12]; formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4); sprintf(reply, "> %s", margin); } else if (memcmp(config, "direct.retry.count", 18) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); } else if (memcmp(config, "direct.retry.base", 17) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); } else if (memcmp(config, "direct.retry.step", 17) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_step_ms); } else if (memcmp(config, "flood.retry.count", 17) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_retry_attempts); } else if (memcmp(config, "flood.retry.path", 16) == 0) { char path_gate[8]; formatFloodRetryPathGate(path_gate, _prefs->flood_retry_max_path); sprintf(reply, "> %s", path_gate); } else if (memcmp(config, "flood.retry.prefixes", 20) == 0) { formatFloodRetryPrefixList(tmp, _prefs->flood_retry_prefixes, FLOOD_RETRY_PREFIX_SLOTS); sprintf(reply, "> %s", tmp[0] ? tmp : "none"); } else if (memcmp(config, "flood.retry.ignore", 18) == 0) { formatFloodRetryPrefixList(tmp, _prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES); sprintf(reply, "> %s", tmp[0] ? tmp : "none"); } else if (memcmp(config, "flood.retry.advert", 18) == 0) { sprintf(reply, "> %s", _prefs->flood_retry_advert_enabled ? "on" : "off"); } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { uint8_t bucket = atoi(&config[19]); if (bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) { formatFloodRetryPrefixList(tmp, _prefs->flood_retry_bridge_buckets[bucket - 1], FLOOD_RETRY_BUCKET_PREFIXES); sprintf(reply, "> %s", tmp[0] ? tmp : "none"); } else { sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS); } } else if (memcmp(config, "direct.retry.cr", 15) == 0) { if (!_prefs->direct_retry_cr_enabled) { strcpy(reply, "> off"); } else { char cr4[12], cr5[12], cr7[12], cr8[12]; formatSnrDbX4(cr4, sizeof(cr4), _prefs->direct_retry_cr4_snr_x4); formatSnrDbX4(cr5, sizeof(cr5), _prefs->direct_retry_cr5_snr_x4); formatSnrDbX4(cr7, sizeof(cr7), _prefs->direct_retry_cr7_snr_x4); formatSnrDbX4(cr8, sizeof(cr8), _prefs->direct_retry_cr8_snr_x4); sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8); } } else if (parseRecentRepeaterGet(config, recent_page)) { _callbacks->formatRecentRepeatersReply(reply, recent_page); } else if (memcmp(config, "owner.info", 10) == 0) { auto start = reply; *reply++ = '>'; *reply++ = ' '; const char* sp = _prefs->owner_info; while (*sp && reply - start < 159) { *reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|' sp++; } *reply = 0; // set null terminator } else if (memcmp(config, "path.hash.mode", 14) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); } else if (memcmp(config, "loop.detect", 11) == 0) { if (_prefs->loop_detect == LOOP_DETECT_OFF) { strcpy(reply, "> off"); } else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) { strcpy(reply, "> minimal"); } else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) { strcpy(reply, "> moderate"); } else { strcpy(reply, "> strict"); } } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); } else if (memcmp(config, "freq", 4) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); } else if (memcmp(config, "public.key", 10) == 0) { strcpy(reply, "> "); mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); } else if (memcmp(config, "role", 4) == 0) { sprintf(reply, "> %s", _callbacks->getRole()); } else if (memcmp(config, "bridge.type", 11) == 0) { sprintf(reply, "> %s", #ifdef WITH_RS232_BRIDGE "rs232" #elif WITH_ESPNOW_BRIDGE "espnow" #else "none" #endif ); #ifdef WITH_BRIDGE } else if (memcmp(config, "bridge.enabled", 14) == 0) { sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off"); } else if (memcmp(config, "bridge.delay", 12) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay); } else if (memcmp(config, "bridge.source", 13) == 0) { sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx"); #endif #ifdef WITH_RS232_BRIDGE } else if (memcmp(config, "bridge.baud", 11) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud); #endif #ifdef WITH_ESPNOW_BRIDGE } else if (memcmp(config, "bridge.channel", 14) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel); } else if (memcmp(config, "bridge.secret", 13) == 0) { sprintf(reply, "> %s", _prefs->bridge_secret); #endif } else if (memcmp(config, "bootloader.ver", 14) == 0) { #ifdef NRF52_PLATFORM char ver[32]; if (_board->getBootloaderVersion(ver, sizeof(ver))) { sprintf(reply, "> %s", ver); } else { strcpy(reply, "> unknown"); } #else strcpy(reply, "Error: unsupported"); #endif } else if (memcmp(config, "adc.multiplier", 14) == 0) { float adc_mult = _board->getAdcMultiplier(); if (adc_mult == 0.0f) { strcpy(reply, "Error: unsupported"); } 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) { sprintf(reply, "> Reset: %s; Shutdown: %s", _board->getResetReasonString(_board->getResetReason()), _board->getShutdownReasonString(_board->getShutdownReason())); } 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 if (memcmp(config, "reboot.interval", 15) == 0) { if (_prefs->reboot_interval == 0) { strcpy(reply, "disabled"); } else { sprintf(reply, "> %d", (uint8_t)_prefs->reboot_interval); } } else { sprintf(reply, "??: %s", config); } } void CommonCLI::handleDelCmd(char* command, char* reply) { const char* config = &command[4]; if (memcmp(config, "tempradioat", 11) == 0 && (config[11] == 0 || config[11] == ' ')) { _callbacks->deleteScheduledRadioParams(true, skipSpacesConst(&config[11]), reply); } else if (memcmp(config, "radioat", 7) == 0 && (config[7] == 0 || config[7] == ' ')) { _callbacks->deleteScheduledRadioParams(false, skipSpacesConst(&config[7]), reply); } else if (memcmp(config, "flood.channel.block", 19) == 0 && (config[19] == ' ' || config[19] == '.')) { const char* cursor = &config[19]; int index = 0; if (!parseFloodChannelBlockDotIndex(cursor, index)) { sprintf(reply, "Error, index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS); return; } cursor = skipSpacesConst(cursor); char selector[8]; if (index > 0 && *cursor != 0) { strcpy(reply, "Error, use index or selector"); return; } if (index > 0) { snprintf(selector, sizeof(selector), "%d", index); _callbacks->deleteFloodChannelBlock(selector, reply); } else if (*cursor != 0) { _callbacks->deleteFloodChannelBlock(cursor, reply); } else { strcpy(reply, "Error, use: del flood.channel.block "); } } else { strcpy(reply, "unknown del: "); StrHelper::strncpy(&reply[13], config, 160 - 14); } } static char* skipSpaces(char* s) { while (*s == ' ') s++; return s; } static void rtrimSpaces(char* s) { char* e = s + strlen(s); while (e > s && e[-1] == ' ') *--e = '\0'; } static char* takeToken(char** cursor) { char* p = skipSpaces(*cursor); if (*p == '\0') { *cursor = p; return nullptr; } char* tok = p; while (*p && *p != ' ') p++; if (*p) *p++ = '\0'; *cursor = p; return tok; } static char* splitNameJump(char* tok) { for (char* q = tok; *q; q++) { if (*q == '|' || *q == ',') { *q = '\0'; char* jump = skipSpaces(q + 1); rtrimSpaces(jump); return jump; } } return nullptr; } static bool processRegionDefSegment(RegionMap* map, char* tok, RegionEntry** cursor, char* reply) { char* jump = splitNameJump(tok); char* name = skipSpaces(tok); if (*name == '\0') { snprintf(reply, 160, "Err - empty name"); return false; } if (jump && *jump == '\0') { snprintf(reply, 160, "Err - empty jump"); return false; } RegionEntry* r = map->putRegion(name, (*cursor)->id); if (r == NULL) { snprintf(reply, 160, "Err - put failed: %s", name); return false; } r->flags = 0; if (jump) { RegionEntry* j = map->findByNamePrefix(jump); if (j == NULL) { snprintf(reply, 160, "Err - unknown jump: %s", jump); return false; } *cursor = j; } else { *cursor = r; } return true; } void CommonCLI::handleRegionCmd(char* command, char* reply) { reply[0] = 0; // `region def`: must run before parseTextParts mutates the buffer char* cmd = skipSpaces(command); if (strncmp(cmd, "region def", 10) == 0 && (cmd[10] == ' ' || cmd[10] == '\0')) { char* payload = skipSpaces(cmd + 10); rtrimSpaces(payload); if (*payload == '\0') { snprintf(reply, 160, "Err - empty def"); return; } RegionEntry* cursor = &_region_map->getWildcard(); for (char* tok; (tok = takeToken(&payload)) != nullptr; ) { if (!processRegionDefSegment(_region_map, tok, &cursor, reply)) return; } _region_map->exportTo(reply, 160); return; } const char* parts[4]; int n = mesh::Utils::parseTextParts(command, parts, 4, ' '); if (n == 1) { _region_map->exportTo(reply, 160); } else if (n >= 2 && strcmp(parts[1], "load") == 0) { _callbacks->startRegionsLoad(); } else if (n >= 2 && strcmp(parts[1], "save") == 0) { _prefs->discovery_mod_timestamp = getRTCClock()->getCurrentTime(); // this node is now 'modified' (for discovery info) savePrefs(); bool success = _callbacks->saveRegions(); strcpy(reply, success ? "OK" : "Err - save failed"); } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) { auto region = _region_map->findByNamePrefix(parts[2]); if (region) { region->flags &= ~REGION_DENY_FLOOD; strcpy(reply, "OK"); } else { strcpy(reply, "Err - unknown region"); } } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) { auto region = _region_map->findByNamePrefix(parts[2]); if (region) { region->flags |= REGION_DENY_FLOOD; strcpy(reply, "OK"); } else { strcpy(reply, "Err - unknown region"); } } else if (n >= 3 && strcmp(parts[1], "get") == 0) { auto region = _region_map->findByNamePrefix(parts[2]); if (region) { auto parent = _region_map->findById(region->parent); if (parent && parent->id != 0) { sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); } else { sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); } } else { strcpy(reply, "Err - unknown region"); } } else if (n >= 3 && strcmp(parts[1], "home") == 0) { auto home = _region_map->findByNamePrefix(parts[2]); if (home) { _region_map->setHomeRegion(home); sprintf(reply, " home is now %s", home->name); } else { strcpy(reply, "Err - unknown region"); } } else if (n == 2 && strcmp(parts[1], "home") == 0) { auto home = _region_map->getHomeRegion(); sprintf(reply, " home is %s", home ? home->name : "*"); } else if (n >= 3 && strcmp(parts[1], "default") == 0) { if (strcmp(parts[2], "") == 0) { _region_map->setDefaultRegion(NULL); _callbacks->onDefaultRegionChanged(NULL); _callbacks->saveRegions(); // persist in one atomic step sprintf(reply, " default scope is now "); } else { auto def = _region_map->findByNamePrefix(parts[2]); if (def == NULL) { def = _region_map->putRegion(parts[2], 0); // auto-create the default region } if (def) { def->flags = 0; // make sure allow flood enabled _region_map->setDefaultRegion(def); _callbacks->onDefaultRegionChanged(def); _callbacks->saveRegions(); // persist in one atomic step sprintf(reply, " default scope is now %s", def->name); } else { strcpy(reply, "Err - region table full"); } } } else if (n == 2 && strcmp(parts[1], "default") == 0) { auto def = _region_map->getDefaultRegion(); sprintf(reply, " default scope is %s", def ? def->name : ""); } else if (n >= 3 && strcmp(parts[1], "put") == 0) { auto parent = n >= 4 ? _region_map->findByNamePrefix(parts[3]) : &(_region_map->getWildcard()); if (parent == NULL) { strcpy(reply, "Err - unknown parent"); } else { auto region = _region_map->putRegion(parts[2], parent->id); if (region == NULL) { strcpy(reply, "Err - unable to put"); } else { region->flags = 0; // New default: enable flood strcpy(reply, "OK - (flood allowed)"); } } } else if (n >= 3 && strcmp(parts[1], "remove") == 0) { auto region = _region_map->findByName(parts[2]); if (region) { if (_region_map->removeRegion(*region)) { strcpy(reply, "OK"); } else { strcpy(reply, "Err - not empty"); } } else { strcpy(reply, "Err - not found"); } } else if (n >= 3 && strcmp(parts[1], "list") == 0) { uint8_t mask = 0; bool invert = false; if (strcmp(parts[2], "allowed") == 0) { mask = REGION_DENY_FLOOD; invert = false; // list regions that DON'T have DENY flag } else if (strcmp(parts[2], "denied") == 0) { mask = REGION_DENY_FLOOD; invert = true; // list regions that DO have DENY flag } else { strcpy(reply, "Err - use 'allowed' or 'denied'"); return; } int len = _region_map->exportNamesTo(reply, 160, mask, invert); if (len == 0) { strcpy(reply, "-none-"); } } else { strcpy(reply, "Err - ??"); } }