mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 23:08:19 +00:00
1349 lines
67 KiB
C++
1349 lines
67 KiB
C++
/*
|
|
* SPDX-License-Identifier: MIT
|
|
* CommonCLI - Common CLI command handlers for repeaters
|
|
*/
|
|
|
|
#include "CommonCLI.h"
|
|
#include "battery_curve.h"
|
|
#include "led_gate.h"
|
|
#include "buzzer_gate.h"
|
|
#include <helpers/ui/ui_task.h>
|
|
#include <helpers/MeshTimeSync.h>
|
|
#include <helpers/time_sync.h>
|
|
#include <adapters/clock/ZephyrRTCDiscover.h>
|
|
#include <helpers/TxtDataHelpers.h>
|
|
#include <helpers/AdvertDataHelpers.h>
|
|
#include <adapters/board/ZephyrBoard.h>
|
|
#include <adapters/gps/ZephyrGPSManager.h>
|
|
#include <zephyr/fs/fs.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <time.h>
|
|
|
|
#if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA)
|
|
#include "wifi_ota.h"
|
|
#endif
|
|
|
|
LOG_MODULE_REGISTER(zephcore_cli, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL);
|
|
|
|
// Helper: robust atoi
|
|
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 == '*') return false;
|
|
n++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Constrain helper
|
|
template<typename T>
|
|
static T constrain(T value, T min_val, T max_val) {
|
|
if (value < min_val) return min_val;
|
|
if (value > max_val) return max_val;
|
|
return value;
|
|
}
|
|
|
|
/* Read exactly 'len' bytes; short/error read stops the chain via ok flag */
|
|
static inline bool prefs_read(struct fs_file_t *f, void *dest, size_t len) {
|
|
return fs_read(f, dest, len) == (ssize_t)len;
|
|
}
|
|
|
|
void CommonCLI::loadPrefs(const char* path) {
|
|
struct fs_file_t file;
|
|
fs_file_t_init(&file);
|
|
|
|
if (fs_open(&file, path, FS_O_READ) < 0) {
|
|
LOG_DBG("No prefs file at %s, using defaults", path);
|
|
return;
|
|
}
|
|
|
|
uint8_t pad[8];
|
|
uint8_t leds_byte = 0;
|
|
bool ok = true;
|
|
|
|
/* Read fields in Arduino-compatible binary order.
|
|
* On truncated file, short-circuit at first failure
|
|
* so remaining fields keep their default values. */
|
|
ok = ok && prefs_read(&file, &_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
|
|
ok = ok && prefs_read(&file, &_prefs->node_name, sizeof(_prefs->node_name)); // 4
|
|
ok = ok && prefs_read(&file, pad, 4); // 36
|
|
ok = ok && prefs_read(&file, &_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
|
|
ok = ok && prefs_read(&file, &_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
|
|
ok = ok && prefs_read(&file, &_prefs->password[0], sizeof(_prefs->password)); // 56
|
|
ok = ok && prefs_read(&file, &_prefs->freq, sizeof(_prefs->freq)); // 72
|
|
ok = ok && prefs_read(&file, &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
|
|
ok = ok && prefs_read(&file, &_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
|
|
ok = ok && prefs_read(&file, &_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
|
|
ok = ok && prefs_read(&file, pad, 1); // 79
|
|
ok = ok && prefs_read(&file, &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
|
|
ok = ok && prefs_read(&file, &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
|
|
ok = ok && prefs_read(&file, &_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
|
|
ok = ok && prefs_read(&file, &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
|
|
ok = ok && prefs_read(&file, &_prefs->backoff_multiplier, sizeof(_prefs->backoff_multiplier)); // 108
|
|
ok = ok && prefs_read(&file, &_prefs->sf, sizeof(_prefs->sf)); // 112
|
|
ok = ok && prefs_read(&file, &_prefs->cr, sizeof(_prefs->cr)); // 113
|
|
ok = ok && prefs_read(&file, &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
|
|
ok = ok && prefs_read(&file, &_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
|
|
ok = ok && prefs_read(&file, &_prefs->bw, sizeof(_prefs->bw)); // 116
|
|
/* 120: leds_disabled, magic-encoded. Formerly agc_reset_interval — see the
|
|
* LEDS_PREF_* comment in NodePrefs.h for why this is not a bare 0/1. */
|
|
ok = ok && prefs_read(&file, &leds_byte, sizeof(leds_byte)); // 120
|
|
ok = ok && prefs_read(&file, &_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121
|
|
ok = ok && prefs_read(&file, &_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122
|
|
ok = ok && prefs_read(&file, pad, 1); // 123
|
|
ok = ok && prefs_read(&file, &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
|
|
ok = ok && prefs_read(&file, &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
|
|
ok = ok && prefs_read(&file, &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
|
|
ok = ok && prefs_read(&file, pad, 1); // skip bridge_enabled // 127
|
|
ok = ok && prefs_read(&file, pad, 2); // skip bridge_delay // 128
|
|
ok = ok && prefs_read(&file, pad, 1); // skip bridge_pkt_src // 130
|
|
ok = ok && prefs_read(&file, pad, 4); // skip bridge_baud // 131
|
|
ok = ok && prefs_read(&file, pad, 1); // skip bridge_channel // 135
|
|
ok = ok && prefs_read(&file, pad, 16); // skip bridge_secret // 136
|
|
ok = ok && prefs_read(&file, &_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152
|
|
ok = ok && prefs_read(&file, pad, 3); // 153
|
|
ok = ok && prefs_read(&file, &_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
|
|
ok = ok && prefs_read(&file, &_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
|
|
ok = ok && prefs_read(&file, &_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161
|
|
ok = ok && prefs_read(&file, &_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
|
|
ok = ok && prefs_read(&file, &_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
|
|
ok = ok && prefs_read(&file, _prefs->owner_info, sizeof(_prefs->owner_info)); // 170
|
|
ok = ok && prefs_read(&file, &_prefs->rx_boost, sizeof(_prefs->rx_boost)); // 290
|
|
ok = ok && prefs_read(&file, &_prefs->rx_duty_cycle, sizeof(_prefs->rx_duty_cycle)); // 291
|
|
/* 292-293: RESERVED — formerly apc_enabled / apc_margin (APC, removed in
|
|
* 1.16.6). Still read so offset 294 onward stays where deployed nodes
|
|
* wrote it; the values are ignored. */
|
|
ok = ok && prefs_read(&file, &_prefs->_reserved_apc_enabled, sizeof(_prefs->_reserved_apc_enabled)); // 292
|
|
ok = ok && prefs_read(&file, &_prefs->_reserved_apc_margin, sizeof(_prefs->_reserved_apc_margin)); // 293
|
|
ok = ok && prefs_read(&file, &_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 294
|
|
ok = ok && prefs_read(&file, &_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 295
|
|
ok = ok && prefs_read(&file, &_prefs->meshtimesync, sizeof(_prefs->meshtimesync)); // 296
|
|
ok = ok && prefs_read(&file, &_prefs->cad_auto, sizeof(_prefs->cad_auto)); // 297
|
|
ok = ok && prefs_read(&file, &_prefs->cad_offset, sizeof(_prefs->cad_offset)); // 298
|
|
ok = ok && prefs_read(&file, &_prefs->probe_interval, sizeof(_prefs->probe_interval)); // 299
|
|
ok = ok && prefs_read(&file, &_prefs->cad_busycap, sizeof(_prefs->cad_busycap)); // 300
|
|
ok = ok && prefs_read(&file, _prefs->extra_sf, sizeof(_prefs->extra_sf)); // 301-303
|
|
|
|
if (!ok) {
|
|
LOG_WRN("Prefs file %s truncated, some fields use defaults", path);
|
|
}
|
|
|
|
fs_close(&file);
|
|
|
|
/* Only the explicit "off" magic disables LEDs; a legacy AGC interval, an
|
|
* unwritten byte, or a truncated file all mean "on". */
|
|
_prefs->leds_disabled = (leds_byte == LEDS_PREF_OFF) ? 1 : 0;
|
|
|
|
// Sanitise bad pref values
|
|
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0.0f, 20.0f);
|
|
_prefs->tx_delay_factor = constrain(_prefs->tx_delay_factor, 0.0f, 2.0f);
|
|
_prefs->direct_tx_delay_factor = constrain(_prefs->direct_tx_delay_factor, 0.0f, 2.0f);
|
|
/* Migrate uninitialized pad bytes: NaN or out-of-range → default 0.2.
|
|
* 0.0 is valid (disables reactive backoff). Old firmware upgrading
|
|
* with zeroed pad bytes will get 0.0 = disabled; user can set explicitly. */
|
|
if (_prefs->backoff_multiplier != _prefs->backoff_multiplier ||
|
|
_prefs->backoff_multiplier < 0.0f || _prefs->backoff_multiplier > 10.0f) {
|
|
_prefs->backoff_multiplier = 0.2f;
|
|
}
|
|
_prefs->backoff_multiplier = constrain(_prefs->backoff_multiplier, 0.0f, 2.0f);
|
|
/* af is the Arduino airtime budget factor: duty% = 100 / (af + 1).
|
|
* Range matches upstream (0..9). Values >9 (from a previous build that
|
|
* stored af as a percentage) get clamped to 9 → 10% effective. */
|
|
_prefs->airtime_factor = constrain(_prefs->airtime_factor, 0.0f, 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, (uint8_t)5, (uint8_t)12);
|
|
_prefs->cr = constrain(_prefs->cr, (uint8_t)5, (uint8_t)8);
|
|
_prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, (int8_t)-9, (int8_t)30);
|
|
#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM
|
|
if (_prefs->tx_power_dbm > CONFIG_ZEPHCORE_MAX_TX_POWER_DBM) {
|
|
_prefs->tx_power_dbm = (int8_t)CONFIG_ZEPHCORE_MAX_TX_POWER_DBM;
|
|
}
|
|
#endif
|
|
_prefs->multi_acks = constrain(_prefs->multi_acks, (uint8_t)0, (uint8_t)1);
|
|
_prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 30000.0f);
|
|
_prefs->path_hash_mode = constrain(_prefs->path_hash_mode, (uint8_t)0, (uint8_t)2);
|
|
_prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, (uint8_t)0, (uint8_t)1);
|
|
_prefs->gps_enabled = constrain(_prefs->gps_enabled, (uint8_t)0, (uint8_t)1);
|
|
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, (uint8_t)0, (uint8_t)2);
|
|
_prefs->rx_boost = constrain(_prefs->rx_boost, (uint8_t)0, (uint8_t)1);
|
|
_prefs->rx_duty_cycle = constrain(_prefs->rx_duty_cycle, (uint8_t)0, (uint8_t)1);
|
|
_prefs->flood_max_unscoped = constrain(_prefs->flood_max_unscoped, (uint8_t)0, (uint8_t)64);
|
|
_prefs->flood_max_advert = constrain(_prefs->flood_max_advert, (uint8_t)0, (uint8_t)64);
|
|
_prefs->meshtimesync = constrain(_prefs->meshtimesync, (uint8_t)0, (uint8_t)1);
|
|
_prefs->cad_auto = constrain(_prefs->cad_auto, (uint8_t)0, (uint8_t)1);
|
|
_prefs->cad_offset = constrain(_prefs->cad_offset, (int8_t)CAD_OFFSET_MIN, (int8_t)CAD_OFFSET_MAX);
|
|
if (_prefs->probe_interval != 0 && _prefs->probe_interval < 10) {
|
|
_prefs->probe_interval = 10;
|
|
}
|
|
_prefs->cad_busycap = constrain(_prefs->cad_busycap, (uint8_t)0, (uint8_t)90);
|
|
|
|
LOG_INF("Loaded prefs from %s", path);
|
|
}
|
|
|
|
void CommonCLI::savePrefs(const char* path) {
|
|
// Remove old file first
|
|
fs_unlink(path);
|
|
|
|
struct fs_file_t file;
|
|
fs_file_t_init(&file);
|
|
|
|
if (fs_open(&file, path, FS_O_CREATE | FS_O_WRITE) < 0) {
|
|
LOG_ERR("Failed to open %s for write", path);
|
|
return;
|
|
}
|
|
|
|
uint8_t pad[16];
|
|
memset(pad, 0, sizeof(pad));
|
|
|
|
fs_write(&file, &_prefs->airtime_factor, sizeof(_prefs->airtime_factor));
|
|
fs_write(&file, &_prefs->node_name, sizeof(_prefs->node_name));
|
|
fs_write(&file, pad, 4);
|
|
fs_write(&file, &_prefs->node_lat, sizeof(_prefs->node_lat));
|
|
fs_write(&file, &_prefs->node_lon, sizeof(_prefs->node_lon));
|
|
fs_write(&file, &_prefs->password[0], sizeof(_prefs->password));
|
|
fs_write(&file, &_prefs->freq, sizeof(_prefs->freq));
|
|
fs_write(&file, &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm));
|
|
fs_write(&file, &_prefs->disable_fwd, sizeof(_prefs->disable_fwd));
|
|
fs_write(&file, &_prefs->advert_interval, sizeof(_prefs->advert_interval));
|
|
fs_write(&file, pad, 1);
|
|
fs_write(&file, &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base));
|
|
fs_write(&file, &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor));
|
|
fs_write(&file, &_prefs->guest_password[0], sizeof(_prefs->guest_password));
|
|
fs_write(&file, &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor));
|
|
fs_write(&file, &_prefs->backoff_multiplier, sizeof(_prefs->backoff_multiplier));
|
|
fs_write(&file, &_prefs->sf, sizeof(_prefs->sf));
|
|
fs_write(&file, &_prefs->cr, sizeof(_prefs->cr));
|
|
fs_write(&file, &_prefs->allow_read_only, sizeof(_prefs->allow_read_only));
|
|
fs_write(&file, &_prefs->multi_acks, sizeof(_prefs->multi_acks));
|
|
fs_write(&file, &_prefs->bw, sizeof(_prefs->bw));
|
|
/* 120: leds_disabled, magic-encoded (was agc_reset_interval). */
|
|
{
|
|
uint8_t leds_byte = _prefs->leds_disabled ? LEDS_PREF_OFF : LEDS_PREF_ON;
|
|
fs_write(&file, &leds_byte, sizeof(leds_byte));
|
|
}
|
|
fs_write(&file, &_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode));
|
|
fs_write(&file, &_prefs->loop_detect, sizeof(_prefs->loop_detect));
|
|
fs_write(&file, pad, 1);
|
|
fs_write(&file, &_prefs->flood_max, sizeof(_prefs->flood_max));
|
|
fs_write(&file, &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval));
|
|
fs_write(&file, &_prefs->interference_threshold, sizeof(_prefs->interference_threshold));
|
|
fs_write(&file, pad, 1); // bridge_enabled
|
|
fs_write(&file, pad, 2); // bridge_delay
|
|
fs_write(&file, pad, 1); // bridge_pkt_src
|
|
fs_write(&file, pad, 4); // bridge_baud
|
|
fs_write(&file, pad, 1); // bridge_channel
|
|
fs_write(&file, pad, 16); // bridge_secret
|
|
fs_write(&file, &_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled));
|
|
fs_write(&file, pad, 3);
|
|
fs_write(&file, &_prefs->gps_enabled, sizeof(_prefs->gps_enabled));
|
|
fs_write(&file, &_prefs->gps_interval, sizeof(_prefs->gps_interval));
|
|
fs_write(&file, &_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy));
|
|
fs_write(&file, &_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp));
|
|
fs_write(&file, &_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier));
|
|
fs_write(&file, _prefs->owner_info, sizeof(_prefs->owner_info));
|
|
fs_write(&file, &_prefs->rx_boost, sizeof(_prefs->rx_boost));
|
|
fs_write(&file, &_prefs->rx_duty_cycle, sizeof(_prefs->rx_duty_cycle));
|
|
/* 292-293: RESERVED — formerly APC, written back unchanged. */
|
|
fs_write(&file, &_prefs->_reserved_apc_enabled, sizeof(_prefs->_reserved_apc_enabled));
|
|
fs_write(&file, &_prefs->_reserved_apc_margin, sizeof(_prefs->_reserved_apc_margin));
|
|
fs_write(&file, &_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped));
|
|
fs_write(&file, &_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert));
|
|
fs_write(&file, &_prefs->meshtimesync, sizeof(_prefs->meshtimesync));
|
|
fs_write(&file, &_prefs->cad_auto, sizeof(_prefs->cad_auto));
|
|
fs_write(&file, &_prefs->cad_offset, sizeof(_prefs->cad_offset));
|
|
fs_write(&file, &_prefs->probe_interval, sizeof(_prefs->probe_interval));
|
|
fs_write(&file, &_prefs->cad_busycap, sizeof(_prefs->cad_busycap));
|
|
fs_write(&file, _prefs->extra_sf, sizeof(_prefs->extra_sf));
|
|
|
|
fs_close(&file);
|
|
LOG_INF("Saved prefs to %s", path);
|
|
}
|
|
|
|
#define MIN_LOCAL_ADVERT_INTERVAL 60
|
|
|
|
void CommonCLI::savePrefs() {
|
|
if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) {
|
|
_prefs->advert_interval = 0; // turn 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,
|
|
_callbacks->getNodeLat(), _callbacks->getNodeLon());
|
|
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::rebootWorkHandler(struct k_work *work)
|
|
{
|
|
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
|
|
CommonCLI *self = CONTAINER_OF(dwork, CommonCLI, _reboot_work);
|
|
|
|
switch (self->_pending_reboot) {
|
|
case REBOOT_DFU:
|
|
static_cast<mesh::ZephyrBoard*>(self->_board)->rebootToBootloader();
|
|
break;
|
|
case REBOOT_OTA:
|
|
/* reply already sent; startOTAUpdate will reset */
|
|
char dummy[80];
|
|
self->_board->startOTAUpdate(self->_prefs->node_name, dummy);
|
|
break;
|
|
case REBOOT_NORMAL:
|
|
default:
|
|
self->_board->reboot();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CommonCLI::scheduleReboot(uint8_t type)
|
|
{
|
|
_pending_reboot = type;
|
|
/* 2 second delay - enough for LoRa reply to be transmitted */
|
|
k_work_schedule(&_reboot_work, K_SECONDS(2));
|
|
}
|
|
|
|
/* CLI commands are case-sensitive, matching upstream Arduino MeshCore.
|
|
*
|
|
* A case-insensitive normalizer lived here from 2026-07-12 until 2026-07-19.
|
|
* It lowercased the first two whitespace-delimited tokens before matching, on
|
|
* the assumption that a value never appears before the third token. That is
|
|
* false for "password <value>", whose value IS token 1 -- so any admin
|
|
* password containing uppercase was silently stored folded to lowercase and
|
|
* could never be used to log in again. ("set guest.password <value>" was
|
|
* unaffected: three tokens.)
|
|
*
|
|
* Do not reintroduce input folding here. Any scheme that rewrites the buffer
|
|
* before dispatch has to guess where keywords end and arguments begin, and
|
|
* that guess is what broke. If case-insensitivity is wanted again, do it at
|
|
* the comparison sites so argument bytes are never touched.
|
|
*/
|
|
void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
|
|
if (strcmp(command, "start dfu") == 0) {
|
|
/* Reboot into UF2 bootloader for firmware update */
|
|
strcpy(reply, "OK - rebooting to UF2 DFU");
|
|
scheduleReboot(REBOOT_DFU);
|
|
} else if (memcmp(command, "start ota", 9) == 0) {
|
|
#if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA)
|
|
/* ESP32: Start WiFi AP + HTTP OTA server (no reboot) */
|
|
int ota_ret = wifi_ota_start(_prefs->node_name, _board->getManufacturerName());
|
|
if (ota_ret == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "Started: http://%s/update",
|
|
CONFIG_ZEPHCORE_OTA_AP_IP);
|
|
} else if (ota_ret == -EALREADY) {
|
|
strcpy(reply, "OTA already active");
|
|
} else {
|
|
snprintf(reply, CLI_REPLY_SIZE, "Error starting OTA: %d", ota_ret);
|
|
}
|
|
#else
|
|
/* nRF52: Reboot into Adafruit BLE OTA DFU mode */
|
|
strcpy(reply, "OK - rebooting to BLE OTA DFU");
|
|
scheduleReboot(REBOOT_OTA);
|
|
#endif
|
|
} else if (memcmp(command, "stop ota", 8) == 0) {
|
|
#if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA)
|
|
if (wifi_ota_is_active()) {
|
|
wifi_ota_stop();
|
|
strcpy(reply, "OTA stopped");
|
|
} else {
|
|
strcpy(reply, "OTA not active");
|
|
}
|
|
#else
|
|
strcpy(reply, "Not supported");
|
|
#endif
|
|
} else if (memcmp(command, "reboot", 6) == 0) {
|
|
strcpy(reply, "OK - rebooting");
|
|
scheduleReboot(REBOOT_NORMAL);
|
|
} else if (memcmp(command, "clkreboot", 9) == 0) {
|
|
getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm
|
|
_board->reboot();
|
|
} else if (memcmp(command, "advert.zerohop", 14) == 0) {
|
|
_callbacks->sendSelfAdvertisement(1500, false); // 0-hop (direct) advert
|
|
strcpy(reply, "OK - zerohop advert sent");
|
|
} else if (memcmp(command, "advert", 6) == 0) {
|
|
_callbacks->sendSelfAdvertisement(1500, true);
|
|
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);
|
|
time_sync_report(TIME_SYNC_CLI);
|
|
zephcore_rtc_save(sender_timestamp + 1); /* persist to hardware RTC */
|
|
MeshTimeSync* ts = _callbacks->getMeshTimeSync();
|
|
if (ts) ts->noteManualSync((uint32_t)(k_uptime_get() / 1000));
|
|
uint32_t now = getRTCClock()->getCurrentTime();
|
|
time_t t = (time_t)now;
|
|
struct tm *tm = gmtime(&t);
|
|
snprintf(reply, CLI_REPLY_SIZE, "OK - clock set: %02d:%02d - %d/%d/%d UTC",
|
|
tm->tm_hour, tm->tm_min, tm->tm_mday, tm->tm_mon + 1, tm->tm_year + 1900);
|
|
} else {
|
|
strcpy(reply, "ERR: clock cannot go backwards");
|
|
}
|
|
} else if (memcmp(command, "clock", 5) == 0) {
|
|
uint32_t now = getRTCClock()->getCurrentTime();
|
|
time_t t = (time_t)now;
|
|
struct tm *tm = gmtime(&t);
|
|
snprintf(reply, CLI_REPLY_SIZE, "Clock: %02d:%02d - %d/%d/%d UTC",
|
|
tm->tm_hour, tm->tm_min, tm->tm_mday, tm->tm_mon + 1, tm->tm_year + 1900);
|
|
} else if (memcmp(command, "time ", 5) == 0) {
|
|
uint32_t secs = _atoi(&command[5]);
|
|
uint32_t curr = getRTCClock()->getCurrentTime();
|
|
if (secs > curr) {
|
|
getRTCClock()->setCurrentTime(secs);
|
|
time_sync_report(TIME_SYNC_CLI);
|
|
zephcore_rtc_save(secs); /* persist to hardware RTC */
|
|
MeshTimeSync* ts = _callbacks->getMeshTimeSync();
|
|
if (ts) ts->noteManualSync((uint32_t)(k_uptime_get() / 1000));
|
|
time_t t = (time_t)secs;
|
|
struct tm *tm = gmtime(&t);
|
|
snprintf(reply, CLI_REPLY_SIZE, "OK - clock set: %02d:%02d - %d/%d/%d UTC",
|
|
tm->tm_hour, tm->tm_min, tm->tm_mday, tm->tm_mon + 1, tm->tm_year + 1900);
|
|
} 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 = strlen(hex);
|
|
if (hex_len > PUB_KEY_SIZE * 2) hex_len = 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) {
|
|
snprintf(tmp, sizeof(tmp), "%.*s", (int)(sizeof(tmp) - 1), &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);
|
|
snprintf(reply, CLI_REPLY_SIZE, "OK - temp params for %d mins", temp_timeout_mins);
|
|
} else {
|
|
strcpy(reply, "Error: freq 150-2500, bw 7-500, sf 5-12, cr 5-8, timeout>0");
|
|
}
|
|
} else if (memcmp(command, "password ", 9) == 0) {
|
|
StrHelper::strzcpy(_prefs->password, &command[9], sizeof(_prefs->password));
|
|
savePrefs();
|
|
snprintf(reply, CLI_REPLY_SIZE, "password now: %s", _prefs->password);
|
|
} 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);
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %d.%d%%", dc_int, dc_frac);
|
|
} else if (memcmp(config, "af", 2) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %.2f", (double)_prefs->airtime_factor);
|
|
} else if (memcmp(config, "int.thresh", 10) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->interference_threshold);
|
|
} else if (memcmp(config, "leds", 4) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->leds_disabled ? "off" : "on");
|
|
#ifndef ZEPHCORE_REPEATER
|
|
} else if (memcmp(config, "buzzer", 6) == 0) {
|
|
uint8_t mode = zephcore_buzzer_mode_from_prefs(_prefs->buzzer_quiet);
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %u (%s)", mode,
|
|
zephcore_buzzer_mode_name(mode));
|
|
#endif
|
|
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
|
|
strcpy(reply, "Removed - use rxduty instead");
|
|
} else if (memcmp(config, "multi.acks", 10) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->multi_acks);
|
|
} else if (memcmp(config, "allow.read.only", 15) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->allow_read_only ? "on" : "off");
|
|
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->flood_advert_interval);
|
|
} else if (memcmp(config, "advert.interval", 15) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %u", ((uint32_t)_prefs->advert_interval) * 2);
|
|
} else if (memcmp(config, "guest.password", 14) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->guest_password);
|
|
} else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) {
|
|
uint8_t prv_key[PRV_KEY_SIZE];
|
|
int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
|
|
mesh::Utils::toHex(tmp, prv_key, len);
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %s", tmp);
|
|
} else if (memcmp(config, "name", 4) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->node_name);
|
|
} else if (memcmp(config, "repeat", 6) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->disable_fwd ? "off" : "on");
|
|
} else if (memcmp(config, "lat", 3) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %.6f", _prefs->node_lat);
|
|
} else if (memcmp(config, "lon", 3) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %.6f", _prefs->node_lon);
|
|
} else if (memcmp(config, "radio.rxgain", 12) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->rx_boost);
|
|
} else if (memcmp(config, "radio", 5) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %.3f,%.1f,%u,%u",
|
|
(double)_prefs->freq, (double)_prefs->bw,
|
|
(uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
|
|
} else if (memcmp(config, "rxdelay", 7) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> adaptive (rxdelay deprecated)");
|
|
} else if (memcmp(config, "txdelay", 7) == 0) {
|
|
float est = _callbacks->getContentionEstimate();
|
|
float ff = _callbacks->getFloodDelayFactor();
|
|
snprintf(reply, CLI_REPLY_SIZE, "> adaptive (est=%.1f flood=%.2f)",
|
|
(double)est, (double)ff);
|
|
} else if (memcmp(config, "flood.max.advert", 16) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->flood_max_advert);
|
|
} else if (memcmp(config, "flood.max.unscoped", 18) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->flood_max_unscoped);
|
|
} else if (memcmp(config, "flood.max", 9) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->flood_max);
|
|
} else if (memcmp(config, "direct.txdelay", 14) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> adaptive (direct.txdelay deprecated)");
|
|
} else if (memcmp(config, "backoff.multiplier", 18) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %.2f", (double)_prefs->backoff_multiplier);
|
|
} else if (memcmp(config, "owner.info", 10) == 0) {
|
|
*reply++ = '>';
|
|
*reply++ = ' ';
|
|
const char* sp = _prefs->owner_info;
|
|
while (*sp) {
|
|
*reply++ = (*sp == '\n') ? '|' : *sp;
|
|
sp++;
|
|
}
|
|
*reply = 0;
|
|
} else if (memcmp(config, "path.hash.mode", 14) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %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 (strcmp(config, "tx") == 0) {
|
|
/* Plain number, matching upstream Arduino MeshCore's "> %d". */
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->tx_power_dbm);
|
|
} else if (memcmp(config, "freq", 4) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %.3f", (double)_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) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %s", _callbacks->getRole());
|
|
} else if (memcmp(config, "bootloader.ver", 14) == 0) {
|
|
char ver[32];
|
|
if (_board->getBootloaderVersion(ver, sizeof(ver))) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %s", ver);
|
|
} else {
|
|
strcpy(reply, "> unknown");
|
|
}
|
|
} 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 {
|
|
uint16_t mv = _board->getBattMilliVolts();
|
|
uint16_t target_mv = battery_curve_default.ocv_mv[0];
|
|
if (mv > 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %.3f (%u mV, target >= %u mV for 100%%)",
|
|
(double)adc_mult, mv, target_mv);
|
|
} else {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %.3f (no ADC reading)", (double)adc_mult);
|
|
}
|
|
}
|
|
} else if (memcmp(config, "rxduty", 6) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->rx_duty_cycle);
|
|
} else if (memcmp(config, "gps diag", 8) == 0) {
|
|
// What the last module-configuration attempt actually did.
|
|
reply[0] = '>'; reply[1] = ' ';
|
|
gps_get_diag_report(reply + 2, CLI_REPLY_SIZE - 2);
|
|
} else if (memcmp(config, "gps duty", 8) == 0) {
|
|
uint32_t s = gps_get_poll_interval_sec(); // now-effective value
|
|
if (s == 0) strcpy(reply, "> always on (0)");
|
|
else snprintf(reply, CLI_REPLY_SIZE, "> %u", (unsigned)s);
|
|
} else if (memcmp(config, "dc.restarts", 11) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %u",
|
|
(uint32_t)_callbacks->getDutyCycleTimeoutRestarts());
|
|
} else if (memcmp(config, "probe.interval", 14) == 0) {
|
|
/* Seconds between periodic radio measurements — the noise-floor
|
|
* sample and the CAD probe that consumes it. 0 = probing off. */
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %u",
|
|
(uint32_t)_prefs->probe_interval);
|
|
} else if (memcmp(config, "cad", 3) == 0) {
|
|
/* Runtime state + per-level probe stats live in the radio.
|
|
* Remote replies get the truncated buffer like meshtimesync. */
|
|
size_t cap = (sender_timestamp == 0) ? CLI_REPLY_SIZE
|
|
: CLI_REMOTE_REPLY_SIZE;
|
|
int n = snprintf(reply, cap, "> ");
|
|
if (_callbacks->formatCadStatus(reply + n, (int)cap - n) == 0) {
|
|
strcpy(reply, "not available");
|
|
}
|
|
} else if (memcmp(config, "extra.sf", 8) == 0) {
|
|
char* dp = reply;
|
|
dp += sprintf(dp, "> ");
|
|
int shown = 0;
|
|
for (int i = 0; i < EXTRA_SF_MAX && _prefs->extra_sf[i] != 0; i++) {
|
|
dp += sprintf(dp, "%s%u", shown++ ? "," : "",
|
|
(unsigned)_prefs->extra_sf[i]);
|
|
}
|
|
if (shown == 0) {
|
|
strcpy(reply, "> none");
|
|
}
|
|
} else if (memcmp(config, "meshtimesync", 12) == 0) {
|
|
MeshTimeSync* ts = _callbacks->getMeshTimeSync();
|
|
if (ts == nullptr) {
|
|
strcpy(reply, "not available");
|
|
} else {
|
|
/* Only the local USB CLI (sender_timestamp == 0) gets the
|
|
* full evidence table; remote replies are truncated to the
|
|
* packet buffer. */
|
|
size_t cap = (sender_timestamp == 0) ? CLI_REPLY_SIZE
|
|
: CLI_REMOTE_REPLY_SIZE;
|
|
ts->formatStatus(reply, cap, getRTCClock()->getCurrentTime(),
|
|
(uint32_t)(k_uptime_get() / 1000),
|
|
_prefs->meshtimesync != 0);
|
|
}
|
|
} else {
|
|
snprintf(reply, CLI_REPLY_SIZE, "??: %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);
|
|
snprintf(reply, CLI_REPLY_SIZE, "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) {
|
|
/* Companion runtime never reads this (getInterferenceThreshold is
|
|
* only overridden in Repeater/RoomServer) — reject instead of a
|
|
* false OK. */
|
|
if (strcmp(_callbacks->getRole(), "companion") == 0) {
|
|
strcpy(reply, "Error: not supported on companion");
|
|
} else {
|
|
_prefs->interference_threshold = atoi(&config[11]);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else if (memcmp(config, "leds ", 5) == 0) {
|
|
/* Master switch for every LED on the node: heartbeat, unread-message
|
|
* and LoRa TX activity, plus the message and shutdown flashes. Not
|
|
* the display backlight — that has its own UI brightness setting. */
|
|
const char* val = &config[5];
|
|
int on;
|
|
if (memcmp(val, "on", 2) == 0 || val[0] == '1') {
|
|
on = 1;
|
|
} else if (memcmp(val, "off", 3) == 0 || val[0] == '0') {
|
|
on = 0;
|
|
} else {
|
|
on = -1;
|
|
}
|
|
if (on < 0) {
|
|
strcpy(reply, "Error: must be on or off");
|
|
} else {
|
|
_prefs->leds_disabled = on ? 0 : 1;
|
|
zephcore_leds_set_disabled(_prefs->leds_disabled != 0);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
#ifndef ZEPHCORE_REPEATER
|
|
} else if (memcmp(config, "buzzer ", 7) == 0) {
|
|
/* 0 = silent, 1 = sound + vibration, 2 = vibration only,
|
|
* 3 = sound only. Modes 2 and 3 only mean something on a board
|
|
* with a vibration motor, which most boards don't have. */
|
|
const char* val = &config[7];
|
|
int mode;
|
|
if (memcmp(val, "vibrate", 7) == 0 || val[0] == '2') {
|
|
mode = ZEPHCORE_BUZZER_VIBRATE;
|
|
} else if (memcmp(val, "sound", 5) == 0 || val[0] == '3') {
|
|
mode = ZEPHCORE_BUZZER_SOUND;
|
|
} else if (memcmp(val, "on", 2) == 0 || val[0] == '1') {
|
|
mode = ZEPHCORE_BUZZER_ON;
|
|
} else if (memcmp(val, "off", 3) == 0 || val[0] == '0') {
|
|
mode = ZEPHCORE_BUZZER_OFF;
|
|
} else {
|
|
mode = -1;
|
|
}
|
|
if (mode < 0) {
|
|
strcpy(reply, "Error: 0 (silent), 1 (sound+vib), 2 (vibrate) or 3 (sound)");
|
|
} else if ((mode == ZEPHCORE_BUZZER_VIBRATE || mode == ZEPHCORE_BUZZER_SOUND) &&
|
|
!zephcore_buzzer_has_vibrate()) {
|
|
strcpy(reply, "Error: no vibration motor on this board - use 0 or 1");
|
|
} else {
|
|
_prefs->buzzer_quiet = zephcore_buzzer_prefs_from_mode((uint8_t)mode);
|
|
zephcore_buzzer_set_mode((uint8_t)mode, false);
|
|
ui_set_buzzer_mode((uint8_t)mode);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
#endif
|
|
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
|
|
/* Periodic AGC recalibration was removed: it reset the noise floor
|
|
* to its unseeded sentinel on every fire, forcing a fresh seed and
|
|
* a full EMA warmup, and it was already forced off under RX duty
|
|
* cycle. RX duty cycle is the supported way to cut RX current.
|
|
* The prefs BYTE is retained (read/written, never acted on) — the
|
|
* on-disk layout is byte-exact and shifting it would corrupt every
|
|
* existing node's prefs. */
|
|
strcpy(reply, "Removed - use rxduty instead");
|
|
} else if (memcmp(config, "cad.auto ", 9) == 0) {
|
|
if (memcmp(&config[9], "on", 2) == 0 || memcmp(&config[9], "off", 3) == 0) {
|
|
_prefs->cad_auto = (config[9] == 'o' && config[10] == 'n') ? 1 : 0;
|
|
_callbacks->applyCadPrefs();
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: must be on or off");
|
|
}
|
|
} else if (memcmp(config, "cad.offset ", 11) == 0) {
|
|
int val = atoi(&config[11]);
|
|
if (val < CAD_OFFSET_MIN || val > CAD_OFFSET_MAX) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "Error: offset range is %d..%d",
|
|
CAD_OFFSET_MIN, CAD_OFFSET_MAX);
|
|
} else {
|
|
_prefs->cad_offset = (int8_t)val;
|
|
_callbacks->applyCadPrefs();
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
/* Governs every periodic radio measurement, not just CAD — the
|
|
* noise-floor sampler and the CAD probe share one reading. */
|
|
} else if (memcmp(config, "probe.interval ", 15) == 0) {
|
|
int val = atoi(&config[15]);
|
|
if (val != 0 && (val < 10 || val > 255)) {
|
|
strcpy(reply, "Error: interval is 0 (probing off) or 10-255 seconds");
|
|
} else {
|
|
_prefs->probe_interval = (uint8_t)val;
|
|
_callbacks->applyCadPrefs();
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else if (memcmp(config, "cad.busycap ", 12) == 0) {
|
|
int val = atoi(&config[12]);
|
|
if (val != 0 && (val < 10 || val > 90)) {
|
|
strcpy(reply, "Error: busycap is 0 (off) or 10-90 percent");
|
|
} else {
|
|
_prefs->cad_busycap = (uint8_t)val;
|
|
_callbacks->applyCadPrefs();
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
}
|
|
} else if (memcmp(config, "cad.reset", 9) == 0) {
|
|
_callbacks->resetCadStats();
|
|
strcpy(reply, "OK - CAD probe stats cleared");
|
|
} else if (memcmp(config, "extra.sf ", 9) == 0) {
|
|
/* LR2021 side detectors: up to 3 extra SFs received alongside
|
|
* `sf`. "0" / "off" clears the set. The chip-side constraints
|
|
* (each > sf, distinct, spread <= 4, BW>=500 caps the count) are
|
|
* enforced in the driver, so an accepted set is a valid one. */
|
|
char tmp[32];
|
|
const char* parts[EXTRA_SF_MAX + 1];
|
|
uint8_t sfs[EXTRA_SF_MAX] = {0};
|
|
StrHelper::strncpy(tmp, &config[9], sizeof(tmp));
|
|
int num = mesh::Utils::parseTextParts(tmp, parts, EXTRA_SF_MAX + 1, ' ');
|
|
if (num == 1 && (strcmp(parts[0], "0") == 0 || strcmp(parts[0], "off") == 0)) {
|
|
num = 0;
|
|
}
|
|
if (num > EXTRA_SF_MAX) {
|
|
sprintf(reply, "Error: at most %d extra SFs", EXTRA_SF_MAX);
|
|
} else {
|
|
for (int i = 0; i < num; i++) sfs[i] = (uint8_t)atoi(parts[i]);
|
|
if (_callbacks->configSideDetectors(sfs, (uint8_t)num)) {
|
|
memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf));
|
|
for (int i = 0; i < num; i++) _prefs->extra_sf[i] = sfs[i];
|
|
savePrefs();
|
|
strcpy(reply, num ? "OK - extra SFs set" : "OK - extra SFs cleared");
|
|
} else {
|
|
strcpy(reply, "Error: unsupported or invalid extra SF config");
|
|
}
|
|
}
|
|
} else if (memcmp(config, "multi.acks ", 11) == 0) {
|
|
int val = atoi(&config[11]);
|
|
if (val == 0 || val == 1) {
|
|
_prefs->multi_acks = (uint8_t)val;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: must be 0 or 1");
|
|
}
|
|
} else if (memcmp(config, "allow.read.only ", 16) == 0) {
|
|
if (memcmp(&config[16], "on", 2) == 0) {
|
|
_prefs->allow_read_only = 1;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else if (memcmp(&config[16], "off", 3) == 0) {
|
|
_prefs->allow_read_only = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: must be on or off");
|
|
}
|
|
} 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)) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "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::strzcpy(_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]);
|
|
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: name cannot contain [ ] \\ : , ? *");
|
|
}
|
|
} else if (memcmp(config, "repeat ", 7) == 0) {
|
|
if (memcmp(&config[7], "on", 2) == 0) {
|
|
_prefs->disable_fwd = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK - repeat is now ON");
|
|
} else if (memcmp(&config[7], "off", 3) == 0) {
|
|
_prefs->disable_fwd = 1;
|
|
savePrefs();
|
|
strcpy(reply, "OK - repeat is now OFF");
|
|
} else {
|
|
strcpy(reply, "Error: must be on or off");
|
|
}
|
|
} else if (memcmp(config, "radio ", 6) == 0) {
|
|
snprintf(tmp, sizeof(tmp), "%.*s", (int)(sizeof(tmp) - 1), &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) {
|
|
/* Snapshot old params, then mutate _prefs and save so later
|
|
* savePrefs() calls (set af, set name, ...) don't clobber
|
|
* the new values with stale RAM. Freeze the running radio on
|
|
* the old params via override so the on-air config doesn't
|
|
* change until reboot. */
|
|
float old_freq = _prefs->freq;
|
|
float old_bw = _prefs->bw;
|
|
uint8_t old_sf = _prefs->sf;
|
|
uint8_t old_cr = _prefs->cr;
|
|
_prefs->freq = freq;
|
|
_prefs->bw = bw;
|
|
_prefs->sf = sf;
|
|
_prefs->cr = cr;
|
|
_callbacks->savePrefs();
|
|
_callbacks->freezeRadioParams(old_freq, old_bw, old_sf, old_cr);
|
|
strcpy(reply, "OK - reboot to apply");
|
|
} else {
|
|
strcpy(reply, "Error: freq 150-2500, bw 7-500, sf 5-12, cr 5-8");
|
|
}
|
|
} 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) {
|
|
_prefs->rx_delay_base = atof(&config[8]);
|
|
savePrefs();
|
|
strcpy(reply, "OK (ignored: rxdelay is now adaptive)");
|
|
} else if (memcmp(config, "txdelay ", 8) == 0) {
|
|
_prefs->tx_delay_factor = atof(&config[8]);
|
|
savePrefs();
|
|
strcpy(reply, "OK (ignored: txdelay is now adaptive)");
|
|
} else if (memcmp(config, "flood.max.advert ", 17) == 0) {
|
|
int m = atoi(&config[17]);
|
|
if (m >= 0 && m <= 64) {
|
|
_prefs->flood_max_advert = (uint8_t)m;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: range 0-64");
|
|
}
|
|
} else if (memcmp(config, "flood.max.unscoped ", 19) == 0) {
|
|
int m = atoi(&config[19]);
|
|
if (m >= 0 && m <= 64) {
|
|
_prefs->flood_max_unscoped = (uint8_t)m;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: range 0-64");
|
|
}
|
|
} else if (memcmp(config, "flood.max ", 10) == 0) {
|
|
int m = atoi(&config[10]);
|
|
if (m >= 0 && m <= 64) {
|
|
_prefs->flood_max = (uint8_t)m;
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error: range 0-64");
|
|
}
|
|
} else if (memcmp(config, "direct.txdelay ", 15) == 0) {
|
|
_prefs->direct_tx_delay_factor = atof(&config[15]);
|
|
savePrefs();
|
|
strcpy(reply, "OK (ignored: direct.txdelay is now adaptive)");
|
|
} else if (memcmp(config, "backoff.multiplier ", 19) == 0) {
|
|
/* Companion's setBackoffMultiplier callback is the base-class
|
|
* no-op and the value isn't restored at boot — reject instead of
|
|
* a false OK. */
|
|
if (strcmp(_callbacks->getRole(), "companion") == 0) {
|
|
strcpy(reply, "Error: not supported on companion");
|
|
} else {
|
|
float f = atof(&config[19]);
|
|
if (f >= 0.0f && f <= 2.0f) {
|
|
_prefs->backoff_multiplier = f;
|
|
_callbacks->setBackoffMultiplier(f);
|
|
savePrefs();
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Error, range 0.0-2.0");
|
|
}
|
|
}
|
|
} else if (memcmp(config, "owner.info ", 11) == 0) {
|
|
config += 11;
|
|
char* dp = _prefs->owner_info;
|
|
while (*config && dp - _prefs->owner_info < (int)sizeof(_prefs->owner_info) - 1) {
|
|
*dp++ = (*config == '|') ? '\n' : *config;
|
|
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) {
|
|
/* Loop detection runs only in the Repeater/RoomServer forward
|
|
* path — companions never consult loop_detect. */
|
|
if (strcmp(_callbacks->getRole(), "companion") == 0) {
|
|
strcpy(reply, "Error: not supported on companion");
|
|
return;
|
|
}
|
|
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) {
|
|
char *end = nullptr;
|
|
long parsed = strtol(&config[3], &end, 10);
|
|
int max_tx = 30;
|
|
#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM
|
|
max_tx = CONFIG_ZEPHCORE_MAX_TX_POWER_DBM;
|
|
#endif
|
|
if (end == &config[3] || *end != '\0' || parsed < -9 || parsed > max_tx) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "Error: range -9 to %d dBm", max_tx);
|
|
} else {
|
|
_prefs->tx_power_dbm = (int8_t)parsed;
|
|
savePrefs();
|
|
_callbacks->setTxPower(_prefs->tx_power_dbm);
|
|
snprintf(reply, CLI_REPLY_SIZE, "OK - tx power=%d dBm",
|
|
(int)_prefs->tx_power_dbm);
|
|
}
|
|
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
|
|
float f = atof(&config[5]);
|
|
if (f >= 150.0f && f <= 2500.0f) {
|
|
float old_freq = _prefs->freq;
|
|
_prefs->freq = f;
|
|
savePrefs();
|
|
/* Keep _prefs->freq = f in RAM so a later savePrefs() (from any
|
|
* other "set" command before reboot) can't rewrite the old freq
|
|
* back; freeze the running radio on the old freq until reboot,
|
|
* mirroring the "set radio" handler above. */
|
|
_callbacks->freezeRadioParams(old_freq, _prefs->bw, _prefs->sf, _prefs->cr);
|
|
strcpy(reply, "OK - reboot to apply");
|
|
} else {
|
|
strcpy(reply, "Error: range 150-2500 MHz");
|
|
}
|
|
} else if (strcmp(config, "adc.multiplier target") == 0) {
|
|
strcpy(reply, "Error: need mV target (e.g. set adc.multiplier target 4173)");
|
|
} else if (memcmp(config, "adc.multiplier target ", 22) == 0) {
|
|
/* Calibrate against a known voltage measured with a multimeter. */
|
|
uint16_t target_mv = (uint16_t)atoi(&config[22]);
|
|
uint16_t current_mv = _board->getBattMilliVolts();
|
|
if (current_mv == 0) {
|
|
strcpy(reply, "Error: no ADC reading on this board");
|
|
} else if (target_mv < 3000 || target_mv > 4400) {
|
|
strcpy(reply, "Error: target out of range (3000-4400 mV)");
|
|
} else {
|
|
float current_mult = _board->getAdcMultiplier();
|
|
float new_mult = current_mult * (float)target_mv / (float)current_mv;
|
|
_prefs->adc_multiplier = new_mult;
|
|
if (_board->setAdcMultiplier(new_mult)) {
|
|
savePrefs();
|
|
snprintf(reply, CLI_REPLY_SIZE,
|
|
"OK - multiplier %.3f -> %.3f (%u -> %u mV)",
|
|
(double)current_mult, (double)new_mult,
|
|
current_mv, target_mv);
|
|
} else {
|
|
_prefs->adc_multiplier = 0.0f;
|
|
strcpy(reply, "Error: unsupported by this board");
|
|
}
|
|
}
|
|
} else if (memcmp(config, "adc.multiplier full", 19) == 0) {
|
|
/* Calibrate: board must be on a full charge. Scales the current
|
|
* multiplier so the ADC reads the board's curve 100% point. */
|
|
uint16_t current_mv = _board->getBattMilliVolts();
|
|
if (current_mv == 0) {
|
|
strcpy(reply, "Error: no ADC reading on this board");
|
|
} else {
|
|
uint16_t target_mv = battery_curve_default.ocv_mv[0];
|
|
float current_mult = _board->getAdcMultiplier();
|
|
float new_mult = current_mult * (float)target_mv / (float)current_mv;
|
|
_prefs->adc_multiplier = new_mult;
|
|
if (_board->setAdcMultiplier(new_mult)) {
|
|
savePrefs();
|
|
snprintf(reply, CLI_REPLY_SIZE,
|
|
"OK - multiplier %.3f -> %.3f (%u -> %u mV)",
|
|
(double)current_mult, (double)new_mult,
|
|
current_mv, target_mv);
|
|
} else {
|
|
_prefs->adc_multiplier = 0.0f;
|
|
strcpy(reply, "Error: unsupported by this board");
|
|
}
|
|
}
|
|
} else if (memcmp(config, "adc.multiplier ", 15) == 0) {
|
|
const char *arg = &config[15];
|
|
float val = atof(arg);
|
|
/* Reject non-numeric, NaN, inf, negative, and out-of-range values.
|
|
* 0 is valid (resets to DTS default). Upper bound covers all real
|
|
* divider/reference combinations with margin. */
|
|
bool bad = (val != 0.0f && val < 100.0f) || val > 30000.0f || val < 0.0f;
|
|
/* atof returns 0 for non-numeric strings — distinguish from literal "0" */
|
|
if (val == 0.0f && arg[0] != '0') bad = true;
|
|
if (bad) {
|
|
strcpy(reply, "Error: invalid multiplier (0 to reset, or 100-30000)");
|
|
} else if (_board->setAdcMultiplier(val)) {
|
|
_prefs->adc_multiplier = val;
|
|
savePrefs();
|
|
if (val == 0.0f) {
|
|
strcpy(reply, "OK - using default board multiplier");
|
|
} else {
|
|
snprintf(reply, CLI_REPLY_SIZE, "OK - multiplier set to %.3f", (double)val);
|
|
}
|
|
} else {
|
|
strcpy(reply, "Error: unsupported by this board");
|
|
}
|
|
} else if (memcmp(config, "radio.rxgain ", 13) == 0) {
|
|
const char* arg = &config[13];
|
|
int val = -1;
|
|
if (memcmp(arg, "on", 2) == 0) val = 1;
|
|
else if (memcmp(arg, "off", 3) == 0) val = 0;
|
|
else if (arg[0] == '0' || arg[0] == '1') val = atoi(arg);
|
|
if (val == 0 || val == 1) {
|
|
/* Always save (upstream f3d4d8cd), then apply live and
|
|
* report when the radio has no RX boost feature. */
|
|
_prefs->rx_boost = (uint8_t)val;
|
|
savePrefs();
|
|
if (_callbacks->setRxBoostedGain(val == 1)) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "OK - radio.rxgain=%d", _prefs->rx_boost);
|
|
} else {
|
|
strcpy(reply, "Error: unsupported");
|
|
}
|
|
} else {
|
|
strcpy(reply, "Error: must be 0, 1, on, or off");
|
|
}
|
|
} else if (memcmp(config, "rxduty ", 7) == 0) {
|
|
const char* arg = &config[7];
|
|
int val = -1;
|
|
if (memcmp(arg, "on", 2) == 0) val = 1;
|
|
else if (memcmp(arg, "off", 3) == 0) val = 0;
|
|
else if (arg[0] == '0' || arg[0] == '1') val = atoi(arg);
|
|
if (val == 0 || val == 1) {
|
|
_prefs->rx_duty_cycle = (uint8_t)val;
|
|
savePrefs();
|
|
snprintf(reply, CLI_REPLY_SIZE, "OK - rxduty=%d (reboot to apply)", _prefs->rx_duty_cycle);
|
|
} else {
|
|
strcpy(reply, "Error: must be 0, 1, on, or off");
|
|
}
|
|
} else if (memcmp(config, "gps diag", 8) == 0) {
|
|
// set gps diag <0|1|on|off> — arm module-configuration reporting.
|
|
// Not persisted: clears on reboot, by design.
|
|
const char* arg = config + 8;
|
|
while (*arg == ' ') arg++;
|
|
int val = -1;
|
|
if (memcmp(arg, "on", 2) == 0) val = 1;
|
|
else if (memcmp(arg, "off", 3) == 0) val = 0;
|
|
else if (arg[0] == '0' || arg[0] == '1') val = atoi(arg);
|
|
if (val == 0 || val == 1) {
|
|
gps_set_diag(val == 1);
|
|
if (val == 1) {
|
|
strcpy(reply, "OK - gps diag on; run 'gps off' then 'gps on', "
|
|
"then 'get gps diag'");
|
|
} else {
|
|
strcpy(reply, "OK - gps diag off");
|
|
}
|
|
} else {
|
|
strcpy(reply, "usage: set gps diag <0|1|on|off>");
|
|
}
|
|
} else if (memcmp(config, "gps duty", 8) == 0) {
|
|
// set gps duty <seconds> | default (0 = always on)
|
|
const char* arg = config + 8;
|
|
while (*arg == ' ') arg++;
|
|
uint32_t val = 0;
|
|
bool ok = true;
|
|
if (*arg == '\0') {
|
|
ok = false;
|
|
} else if (strcmp(arg, "default") == 0) {
|
|
val = _callbacks->getDefaultGpsIntervalSec();
|
|
} else {
|
|
char* end = NULL;
|
|
unsigned long parsed = strtoul(arg, &end, 10);
|
|
// reject non-numeric, fractions, or trailing garbage
|
|
if (end == arg || *end != '\0') ok = false;
|
|
else val = (uint32_t)parsed;
|
|
}
|
|
if (!ok) {
|
|
strcpy(reply, "usage: set gps duty <seconds> | default (0 = always on)");
|
|
} else {
|
|
if (val > 604800UL) val = 604800UL; // cap at 1 week
|
|
else if (val != 0 && val < 10) val = 10; // floor 10s (0 = always on)
|
|
_prefs->gps_interval = val;
|
|
gps_set_poll_interval_sec(val); // apply live
|
|
savePrefs();
|
|
if (val == 0) strcpy(reply, "OK - gps duty=0 (always on)");
|
|
else snprintf(reply, CLI_REPLY_SIZE, "OK - gps duty=%u s", (unsigned)val);
|
|
}
|
|
} else if (memcmp(config, "meshtimesync ", 13) == 0) {
|
|
const char* arg = &config[13];
|
|
if (_callbacks->getMeshTimeSync() == nullptr) {
|
|
strcpy(reply, "not available");
|
|
} else if (memcmp(arg, "on", 2) == 0) {
|
|
_prefs->meshtimesync = 1;
|
|
savePrefs();
|
|
strcpy(reply, "OK - meshtimesync on");
|
|
} else if (memcmp(arg, "off", 3) == 0) {
|
|
_prefs->meshtimesync = 0;
|
|
savePrefs();
|
|
strcpy(reply, "OK - meshtimesync off");
|
|
} else {
|
|
strcpy(reply, "Error: must be on or off");
|
|
}
|
|
} else {
|
|
snprintf(reply, CLI_REPLY_SIZE, "unknown config: %.230s", config);
|
|
}
|
|
} else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) {
|
|
bool s = _callbacks->formatFileSystem();
|
|
if (s) {
|
|
/* formatFileSystem() flattens the mounted NVS bonds partition,
|
|
* leaving stale in-RAM bond state. Reboot (deferred so this
|
|
* reply transmits first) so NVS + the BT stack re-init cleanly. */
|
|
snprintf(reply, CLI_REPLY_SIZE, "File system erase: OK - rebooting");
|
|
scheduleReboot(REBOOT_NORMAL);
|
|
} else {
|
|
snprintf(reply, CLI_REPLY_SIZE, "File system erase: Err");
|
|
}
|
|
} else if (memcmp(command, "ver", 3) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate());
|
|
} else if (memcmp(command, "board", 5) == 0) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "%s", _board->getManufacturerName());
|
|
} else if (memcmp(command, "sensor get ", 11) == 0) {
|
|
const char* key = command + 11;
|
|
const char* val = _callbacks->getSensorSettingByKey(key);
|
|
if (val != nullptr) {
|
|
snprintf(reply, CLI_REPLY_SIZE, "> %s", val);
|
|
} else {
|
|
strcpy(reply, "null");
|
|
}
|
|
} else if (memcmp(command, "sensor set ", 11) == 0) {
|
|
snprintf(tmp, sizeof(tmp), "%.*s", (int)(sizeof(tmp) - 1), &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 (_callbacks->setSensorSettingValue(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 = _callbacks->getNumSensorSettings();
|
|
if (strlen(command) > 11) {
|
|
start = _atoi(command + 12);
|
|
}
|
|
if (start >= end) {
|
|
strcpy(reply, "no custom var");
|
|
} else {
|
|
snprintf(dp, CLI_REPLY_SIZE - (dp - reply), "%d vars\n", end);
|
|
dp = strchr(dp, 0);
|
|
int i;
|
|
for (i = start; i < end && (dp - reply < 134); i++) {
|
|
snprintf(dp, CLI_REPLY_SIZE - (dp - reply), "%s=%s\n",
|
|
_callbacks->getSensorSettingName(i),
|
|
_callbacks->getSensorSettingValue(i));
|
|
dp = strchr(dp, 0);
|
|
}
|
|
if (i < end) {
|
|
snprintf(dp, CLI_REPLY_SIZE - (dp - reply), "... next:%d", i);
|
|
} else {
|
|
*(dp - 1) = 0; // remove last CR
|
|
}
|
|
}
|
|
} else if (memcmp(command, "gps on", 6) == 0) {
|
|
if (_callbacks->setGpsEnabled(true)) {
|
|
_prefs->gps_enabled = 1;
|
|
savePrefs();
|
|
strcpy(reply, "ok");
|
|
} else {
|
|
strcpy(reply, "gps toggle not found");
|
|
}
|
|
} else if (memcmp(command, "gps off", 7) == 0) {
|
|
if (_callbacks->setGpsEnabled(false)) {
|
|
_prefs->gps_enabled = 0;
|
|
savePrefs();
|
|
strcpy(reply, "ok");
|
|
} else {
|
|
strcpy(reply, "gps toggle not found");
|
|
}
|
|
} else if (memcmp(command, "gps setloc", 10) == 0) {
|
|
_prefs->node_lat = _callbacks->getNodeLat();
|
|
_prefs->node_lon = _callbacks->getNodeLon();
|
|
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) {
|
|
_callbacks->formatGpsStatsReply(reply);
|
|
} else if (memcmp(command, "powersaving", 11) == 0) {
|
|
strcpy(reply, "Not implemented");
|
|
} 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");
|
|
}
|
|
}
|