Files
ZephCore/zephcore/helpers/CommonCLI.cpp
T

719 lines
33 KiB
C++

/*
* SPDX-License-Identifier: Apache-2.0
* CommonCLI - Common CLI command handlers for repeaters
*/
#include "CommonCLI.h"
#include <helpers/TxtDataHelpers.h>
#include <helpers/AdvertDataHelpers.h>
#include <adapters/board/ZephyrBoard.h>
#include <zephyr/fs/fs.h>
#include <zephyr/logging/log.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
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;
}
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];
fs_read(&file, &_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
fs_read(&file, &_prefs->node_name, sizeof(_prefs->node_name)); // 4
fs_read(&file, pad, 4); // 36
fs_read(&file, &_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
fs_read(&file, &_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
fs_read(&file, &_prefs->password[0], sizeof(_prefs->password)); // 56
fs_read(&file, &_prefs->freq, sizeof(_prefs->freq)); // 72
fs_read(&file, &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
fs_read(&file, &_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
fs_read(&file, &_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
fs_read(&file, pad, 1); // 79
fs_read(&file, &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
fs_read(&file, &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
fs_read(&file, &_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
fs_read(&file, &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
fs_read(&file, pad, 4); // 108
fs_read(&file, &_prefs->sf, sizeof(_prefs->sf)); // 112
fs_read(&file, &_prefs->cr, sizeof(_prefs->cr)); // 113
fs_read(&file, &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
fs_read(&file, &_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
fs_read(&file, &_prefs->bw, sizeof(_prefs->bw)); // 116
fs_read(&file, &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
fs_read(&file, pad, 3); // 121
fs_read(&file, &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
fs_read(&file, &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
fs_read(&file, &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
fs_read(&file, pad, 1); // skip bridge_enabled // 127
fs_read(&file, pad, 2); // skip bridge_delay // 128
fs_read(&file, pad, 1); // skip bridge_pkt_src // 130
fs_read(&file, pad, 4); // skip bridge_baud // 131
fs_read(&file, pad, 1); // skip bridge_channel // 135
fs_read(&file, pad, 16); // skip bridge_secret // 136
fs_read(&file, &_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152
fs_read(&file, pad, 3); // 153
fs_read(&file, &_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
fs_read(&file, &_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
fs_read(&file, &_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161
fs_read(&file, &_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
fs_read(&file, &_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
fs_read(&file, _prefs->owner_info, sizeof(_prefs->owner_info)); // 170
fs_read(&file, &_prefs->rx_boost, sizeof(_prefs->rx_boost)); // 290
fs_close(&file);
// 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 old AF multiplier (0-9) to duty cycle percentage (0-99) */
if (_prefs->airtime_factor > 0.0f && _prefs->airtime_factor <= 9.0f) {
_prefs->airtime_factor *= 10.0f;
}
_prefs->airtime_factor = constrain(_prefs->airtime_factor, 0.0f, 99.0f);
_prefs->freq = constrain(_prefs->freq, 400.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, (uint8_t)1, (uint8_t)30);
#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM
if (_prefs->tx_power_dbm > CONFIG_ZEPHCORE_MAX_TX_POWER_DBM) {
_prefs->tx_power_dbm = (uint8_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, 10.0f);
_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);
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, pad, 4);
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));
fs_write(&file, &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval));
fs_write(&file, pad, 3);
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_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));
}
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) {
/* Reboot into Adafruit BLE OTA DFU mode */
strcpy(reply, "OK - rebooting to BLE OTA DFU");
scheduleReboot(REBOOT_OTA);
} 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", 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);
uint32_t now = getRTCClock()->getCurrentTime();
// Simple time formatting
snprintf(reply, CLI_REPLY_SIZE, "OK - clock set to %u", now);
} else {
strcpy(reply, "ERR: clock cannot go backwards");
}
} else if (memcmp(command, "clock", 5) == 0) {
uint32_t now = getRTCClock()->getCurrentTime();
snprintf(reply, CLI_REPLY_SIZE, "Clock: %u", now);
} else if (memcmp(command, "time ", 5) == 0) {
uint32_t secs = _atoi(&command[5]);
uint32_t curr = getRTCClock()->getCurrentTime();
if (secs > curr) {
getRTCClock()->setCurrentTime(secs);
snprintf(reply, CLI_REPLY_SIZE, "OK - clock set to %u", secs);
} 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) {
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 >= 300.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, invalid params");
}
} else if (memcmp(command, "password ", 9) == 0) {
StrHelper::strncpy(_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, "af", 2) == 0) {
int dc = (_prefs->airtime_factor == 0.0f) ? 100 : (int)_prefs->airtime_factor;
snprintf(reply, CLI_REPLY_SIZE, "> Duty Cycle: %d%%", dc);
} else if (memcmp(config, "int.thresh", 10) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->interference_threshold);
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %u", ((uint32_t)_prefs->agc_reset_interval) * 4);
} 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", 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, "> %.2f", (double)_prefs->rx_delay_base);
} else if (memcmp(config, "txdelay", 7) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %.2f", (double)_prefs->tx_delay_factor);
} 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, "> %.2f", (double)_prefs->direct_tx_delay_factor);
} else if (memcmp(config, "owner.info", 10) == 0) {
*reply++ = '>';
*reply++ = ' ';
const char* sp = _prefs->owner_info;
while (*sp) {
*reply++ = (*sp == '\n') ? '|' : *sp;
sp++;
}
*reply = 0;
} else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_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, "adc.multiplier", 14) == 0) {
float adc_mult = _board->getAdcMultiplier();
if (adc_mult == 0.0f) {
strcpy(reply, "Error: unsupported by this board");
} else {
snprintf(reply, CLI_REPLY_SIZE, "> %.3f", (double)adc_mult);
}
} 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, "af ", 3) == 0) {
int val = atoi(&config[3]);
if (val <= 0 || val >= 100) {
_prefs->airtime_factor = 0.0f;
} else {
_prefs->airtime_factor = (float)val;
}
savePrefs();
int dc = (_prefs->airtime_factor == 0.0f) ? 100 : (int)_prefs->airtime_factor;
snprintf(reply, CLI_REPLY_SIZE, "Duty Cycle set: %d%%", dc);
} else if (memcmp(config, "int.thresh ", 11) == 0) {
_prefs->interference_threshold = atoi(&config[11]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
savePrefs();
snprintf(reply, CLI_REPLY_SIZE, "OK - interval rounded to %u", ((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)) {
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::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]);
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 ", 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 >= 300.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 &&
cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
_prefs->sf = sf;
_prefs->cr = cr;
_prefs->freq = freq;
_prefs->bw = bw;
_callbacks->savePrefs();
strcpy(reply, "OK - reboot to apply");
} else {
strcpy(reply, "Error, invalid radio params");
}
} else if (memcmp(config, "lat ", 4) == 0) {
_prefs->node_lat = atof(&config[4]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "lon ", 4) == 0) {
_prefs->node_lon = atof(&config[4]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "rxdelay ", 8) == 0) {
float db = atof(&config[8]);
if (db >= 0) {
_prefs->rx_delay_base = db;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, cannot be negative");
}
} else if (memcmp(config, "txdelay ", 8) == 0) {
float f = atof(&config[8]);
if (f >= 0) {
_prefs->tx_delay_factor = f;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, cannot be negative");
}
} else if (memcmp(config, "flood.max ", 10) == 0) {
uint8_t m = atoi(&config[10]);
if (m <= 64) {
_prefs->flood_max = m;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, max 64");
}
} else if (memcmp(config, "direct.txdelay ", 15) == 0) {
float f = atof(&config[15]);
if (f >= 0) {
_prefs->direct_tx_delay_factor = f;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, cannot be negative");
}
} else if (memcmp(config, "owner.info ", 11) == 0) {
config += 11;
char* dp = _prefs->owner_info;
while (*config && dp - _prefs->owner_info < (int)sizeof(_prefs->owner_info) - 1) {
*dp++ = (*config == '|') ? '\n' : *config;
config++;
}
*dp = 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "tx ", 3) == 0) {
int val = atoi(&config[3]);
#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM
if (val > CONFIG_ZEPHCORE_MAX_TX_POWER_DBM) {
val = CONFIG_ZEPHCORE_MAX_TX_POWER_DBM;
}
#endif
_prefs->tx_power_dbm = (uint8_t)val;
savePrefs();
_callbacks->setTxPower(_prefs->tx_power_dbm);
snprintf(reply, CLI_REPLY_SIZE, "OK - tx power=%u dBm", (uint32_t)_prefs->tx_power_dbm);
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
_prefs->freq = atof(&config[5]);
savePrefs();
strcpy(reply, "OK - reboot to apply");
} 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 {
snprintf(reply, CLI_REPLY_SIZE, "OK - multiplier set to %.3f", (double)_prefs->adc_multiplier);
}
} else {
_prefs->adc_multiplier = 0.0f;
strcpy(reply, "Error: unsupported by this board");
}
} else if (memcmp(config, "rxboost ", 8) == 0) {
_prefs->rx_boost = atoi(&config[8]) ? 1 : 0;
savePrefs();
snprintf(reply, CLI_REPLY_SIZE, "OK - rxboost=%d (reboot to apply)", _prefs->rx_boost);
} else if (memcmp(config, "rxboost", 7) == 0 && (config[7] == 0 || config[7] == ' ')) {
snprintf(reply, CLI_REPLY_SIZE, "> %d", _prefs->rx_boost);
} else {
snprintf(reply, CLI_REPLY_SIZE, "unknown config: %s", config);
}
} else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) {
bool s = _callbacks->formatFileSystem();
snprintf(reply, CLI_REPLY_SIZE, "File system erase: %s", s ? "OK" : "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) {
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 (_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", 4) == 0) {
_prefs->advert_loc_policy = ADVERT_LOC_PREFS;
savePrefs();
strcpy(reply, "ok");
} else {
strcpy(reply, "error");
}
} else if (memcmp(command, "gps", 3) == 0) {
if (_callbacks->isGpsEnabled()) {
strcpy(reply, "on");
} else {
strcpy(reply, "off");
}
} else if (memcmp(command, "powersaving on", 14) == 0) {
_prefs->powersaving_enabled = 1;
savePrefs();
strcpy(reply, "ok");
} else if (memcmp(command, "powersaving off", 15) == 0) {
_prefs->powersaving_enabled = 0;
savePrefs();
strcpy(reply, "ok");
} else if (memcmp(command, "powersaving", 11) == 0) {
strcpy(reply, _prefs->powersaving_enabled ? "on" : "off");
} else if (memcmp(command, "log start", 9) == 0) {
_callbacks->setLoggingOn(true);
strcpy(reply, " logging on");
} else if (memcmp(command, "log stop", 8) == 0) {
_callbacks->setLoggingOn(false);
strcpy(reply, " logging off");
} else if (memcmp(command, "log erase", 9) == 0) {
_callbacks->eraseLogFile();
strcpy(reply, " log erased");
} else if (sender_timestamp == 0 && memcmp(command, "log", 3) == 0) {
_callbacks->dumpLogFile();
strcpy(reply, " EOF");
} else if (sender_timestamp == 0 && memcmp(command, "stats-packets", 13) == 0 &&
(command[13] == 0 || command[13] == ' ')) {
_callbacks->formatPacketStatsReply(reply);
} else if (sender_timestamp == 0 && memcmp(command, "stats-radio", 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");
}
}