command fixes

This commit is contained in:
liquidraver
2026-08-30 09:27:20 +02:00
parent e1e5863688
commit ccb7fc748d
7 changed files with 323 additions and 137 deletions
+4 -3
View File
@@ -459,9 +459,10 @@ config ZEPHCORE_NOISE_FLOOR_INTERVAL_MS
raising it there would change their RF tracking for no power gain, since
their housekeeping timer wakes them at 5 s regardless.
Repeaters use 15 s, matching ZEPHCORE_CAD_PROBE_INTERVAL — the only other
recurring radio deadline — so the two do not interleave into separate
wakes any more often than they have to.
Repeaters use 15 s, matching the default probe.interval pref (set in
initNodePrefs(), helpers/NodePrefs.h) — the only other recurring radio
deadline — so the two do not interleave into separate wakes any more
often than they have to.
On a repeater this is one of the two shortest recurring deadlines, so it
bounds how long the SoC can sleep between wakes. Raising it stretches
+6 -4
View File
@@ -284,10 +284,12 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
* unwritten byte both mean "on". */
prefs.leds_disabled = (leds_byte == LEDS_PREF_OFF) ? 1 : 0;
/* Migrate uninitialized backoff_multiplier (0.0 or NaN) to default */
if (prefs.backoff_multiplier == 0.0f || prefs.backoff_multiplier != prefs.backoff_multiplier) {
prefs.backoff_multiplier = 0.2f;
}
/* The 0.0-or-NaN -> 0.2 coercion that used to live here is gone. It could
* not tell "field absent from an old file" from "the user set 0.0 to turn
* reactive backoff off", so the documented off switch never survived a
* reboot. Both cases are now handled properly: initNodePrefs() supplies
* 0.2 and a short-file fs_read() is a no-op that keeps it, while NaN and
* out-of-range values are caught by sanitizeNodePrefs(). */
LOG_INF("Loaded prefs from %s", path);
LOG_DBG(" name='%s' freq=%.3f sf=%u bw=%.1f tx_pwr=%d",
+220 -125
View File
@@ -40,6 +40,79 @@ static uint32_t _atoi(const char* sp) {
return n;
}
/* ---- "default" keyword + strict numeric parsing for the `set` path ----
*
* Bare atoi()/atof() fold every non-numeric string to 0, the word "default"
* included. On the knobs where 0 is itself legal and means "off" —
* probe.interval, cad.busycap, flood.max, rxduty — that silently switched the
* feature off and still answered OK. `set probe.interval default` disabling
* probing is the report that prompted this.
*
* Every `set` that has a default now takes the literal `default`, and rejects
* trailing garbage rather than turning it into a zero. */
/* The defaults are read out of initNodePrefs() itself rather than restated as
* constants here, so `set <x> default` cannot drift from what a factory-fresh
* node actually boots with. File-scope statics (not a function-local one) to
* avoid emitting a __cxa_guard for the lazy init. */
static NodePrefs s_cli_defaults;
static bool s_cli_defaults_ready = false;
static const NodePrefs* cliDefaults() {
if (!s_cli_defaults_ready) {
initNodePrefs(&s_cli_defaults);
s_cli_defaults_ready = true;
}
return &s_cli_defaults;
}
static const char* cliSkipSpace(const char* s) {
while (*s == ' ') s++;
return s;
}
static bool cliIsDefault(const char* arg) {
const char* s = cliSkipSpace(arg);
if (strncmp(s, "default", 7) != 0) return false;
return *cliSkipSpace(s + 7) == '\0';
}
/* Integer argument, or "default". false = unparseable; the caller reports
* usage rather than acting on a silently-zeroed value. */
static bool cliNum(const char* arg, long def_val, long* out) {
const char* s = cliSkipSpace(arg);
if (cliIsDefault(s)) { *out = def_val; return true; }
char* end = NULL;
long v = strtol(s, &end, 10);
if (end == s || *cliSkipSpace(end) != '\0') return false;
*out = v;
return true;
}
/* Float argument, or "default". NaN is rejected here so range checks in the
* callers (every comparison against NaN is false) cannot pass it through. */
static bool cliFloat(const char* arg, float def_val, float* out) {
const char* s = cliSkipSpace(arg);
if (cliIsDefault(s)) { *out = def_val; return true; }
char* end = NULL;
float v = strtof(s, &end);
if (end == s || *cliSkipSpace(end) != '\0') return false;
if (v != v) return false;
*out = v;
return true;
}
/* on / off / 1 / 0 / default -> 1 or 0; -1 when unrecognised. Replaces the
* hand-rolled copy of this ladder in a dozen setters, each of which accepted a
* bare leading '0'/'1' and ignored whatever followed. */
static int cliOnOff(const char* arg, int def_val) {
const char* s = cliSkipSpace(arg);
if (cliIsDefault(s)) return def_val;
if (strcmp(s, "on") == 0 || strcmp(s, "1") == 0) return 1;
if (strcmp(s, "off") == 0 || strcmp(s, "0") == 0) return 0;
return -1;
}
static bool isValidName(const char* n) {
while (*n) {
if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' ||
@@ -503,8 +576,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} 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) {
float dc;
if (!cliFloat(&config[10], 100.0f / (cliDefaults()->airtime_factor + 1.0f), &dc)) {
strcpy(reply, "ERROR: dutycycle must be 1-100, or default");
} else if (dc < 1 || dc > 100) {
strcpy(reply, "ERROR: dutycycle must be 1-100");
} else {
_prefs->airtime_factor = (100.0f / dc) - 1.0f;
@@ -515,9 +590,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
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");
float af;
if (!cliFloat(&config[3], cliDefaults()->airtime_factor, &af)) {
strcpy(reply, "Error: expected a number or default");
} else {
_prefs->airtime_factor = af;
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
@@ -525,25 +605,22 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
if (strcmp(_callbacks->getRole(), "companion") == 0) {
strcpy(reply, "Error: not supported on companion");
} else {
_prefs->interference_threshold = atoi(&config[11]);
savePrefs();
strcpy(reply, "OK");
long v;
if (!cliNum(&config[11], cliDefaults()->interference_threshold, &v)) {
strcpy(reply, "Error: expected a number or default");
} else {
_prefs->interference_threshold = (uint8_t)v;
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;
}
int on = cliOnOff(&config[5], cliDefaults()->leds_disabled ? 0 : 1);
if (on < 0) {
strcpy(reply, "Error: must be on or off");
strcpy(reply, "Error: must be on, off or default");
} else {
_prefs->leds_disabled = on ? 0 : 1;
zephcore_leds_set_disabled(_prefs->leds_disabled != 0);
@@ -565,11 +642,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
mode = ZEPHCORE_BUZZER_ON;
} else if (memcmp(val, "off", 3) == 0 || val[0] == '0') {
mode = ZEPHCORE_BUZZER_OFF;
} else if (cliIsDefault(val)) {
mode = zephcore_buzzer_mode_from_prefs(cliDefaults()->buzzer_quiet);
} else {
mode = -1;
}
if (mode < 0) {
strcpy(reply, "Error: 0 (silent), 1 (sound+vib), 2 (vibrate) or 3 (sound)");
strcpy(reply, "Error: 0 (silent), 1 (sound+vib), 2 (vibrate), 3 (sound) or default");
} 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");
@@ -591,17 +670,21 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
* existing node's prefs. */
strcpy(reply, "Removed - Automatic AGC reset is on");
} 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;
int on = cliOnOff(&config[9], cliDefaults()->cad_auto);
if (on >= 0) {
_prefs->cad_auto = (uint8_t)on;
_callbacks->applyCadPrefs();
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: must be on or off");
strcpy(reply, "Error: must be on, off or default");
}
} else if (memcmp(config, "cad.offset ", 11) == 0) {
int val = atoi(&config[11]);
if (val < CAD_OFFSET_MIN || val > CAD_OFFSET_MAX) {
long val;
if (!cliNum(&config[11], cliDefaults()->cad_offset, &val)) {
snprintf(reply, CLI_REPLY_SIZE, "Error: expected %d..%d or default",
CAD_OFFSET_MIN, CAD_OFFSET_MAX);
} else 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 {
@@ -613,8 +696,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
/* 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)) {
long val;
if (!cliNum(&config[15], cliDefaults()->probe_interval, &val)) {
strcpy(reply, "Error: interval is 0 (probing off), 10-255 seconds, or default");
} else 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;
@@ -623,8 +708,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(reply, "OK");
}
} else if (memcmp(config, "cad.busycap ", 12) == 0) {
int val = atoi(&config[12]);
if (val != 0 && (val < 10 || val > 90)) {
long val;
if (!cliNum(&config[12], cliDefaults()->cad_busycap, &val)) {
strcpy(reply, "Error: busycap is 0 (off), 10-90 percent, or default");
} else 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;
@@ -633,8 +720,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(reply, "OK");
}
} else if (memcmp(config, "cad.reset", 9) == 0) {
/* Clearing the probe statistics alone left the node re-converging
* FROM wherever the staircase had already walked detPeak, with no
* evidence left to justify sitting there — the opposite of a reset,
* and useless for the one job this command has (recovering after a
* base-table change). The operating offset lives in two places:
* _prefs->cad_offset, and _cad_offset inside the radio, which
* applyCadPrefs() reloads through setCadParams(). */
_prefs->cad_offset = cliDefaults()->cad_offset;
_callbacks->resetCadStats();
strcpy(reply, "OK - CAD probe stats cleared");
_callbacks->applyCadPrefs();
savePrefs();
snprintf(reply, CLI_REPLY_SIZE,
"OK - CAD probe stats cleared, detPeak offset reset to %d",
(int)_prefs->cad_offset);
} 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
@@ -662,32 +761,32 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
}
}
} else if (memcmp(config, "multi.acks ", 11) == 0) {
int val = atoi(&config[11]);
if (val == 0 || val == 1) {
long val;
if (cliNum(&config[11], cliDefaults()->multi_acks, &val) &&
(val == 0 || val == 1)) {
_prefs->multi_acks = (uint8_t)val;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: must be 0 or 1");
strcpy(reply, "Error: must be 0, 1 or default");
}
#ifdef CONFIG_ZEPHCORE_ROLE_ROOM_SERVER
/* Room server only -- see the matching guard on the `get` side. */
} 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;
int on = cliOnOff(&config[16], cliDefaults()->allow_read_only);
if (on >= 0) {
_prefs->allow_read_only = (uint8_t)on;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: must be on or off");
strcpy(reply, "Error: must be on, off or default");
}
#endif
} else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
int hours = _atoi(&config[22]);
if ((hours > 0 && hours < 3) || (hours > 168)) {
long hours;
if (!cliNum(&config[22], cliDefaults()->flood_advert_interval, &hours)) {
strcpy(reply, "Error: expected 0, 3-168 hours, or default");
} else if ((hours > 0 && hours < 3) || (hours > 168)) {
strcpy(reply, "Error: interval range is 3-168 hours");
} else {
_prefs->flood_advert_interval = (uint8_t)hours;
@@ -696,8 +795,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
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)) {
long mins;
if (!cliNum(&config[16], (long)cliDefaults()->advert_interval * 2, &mins)) {
snprintf(reply, CLI_REPLY_SIZE,
"Error: expected 0, %d-240 minutes, or default",
MIN_LOCAL_ADVERT_INTERVAL);
} else 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);
@@ -745,10 +848,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
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;
/* "set radio default" restores all four together — they are one
* interop-critical set and resetting them piecemeal can leave a
* node on a combination no other node uses. */
bool want_def = (num == 1 && cliIsDefault(parts[0]));
float freq = want_def ? cliDefaults()->freq : (num > 0 ? strtof(parts[0], nullptr) : 0.0f);
float bw = want_def ? cliDefaults()->bw : (num > 1 ? strtof(parts[1], nullptr) : 0.0f);
uint8_t sf = want_def ? cliDefaults()->sf : (num > 2 ? (uint8_t)atoi(parts[2]) : 0);
uint8_t cr = want_def ? cliDefaults()->cr : (num > 3 ? (uint8_t)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
@@ -768,53 +875,59 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_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");
strcpy(reply, "Error: freq 150-2500, bw 7-500, sf 5-12, cr 5-8, or default");
}
} else if (memcmp(config, "lat ", 4) == 0) {
_prefs->node_lat = atof(&config[4]);
_prefs->node_lat = cliIsDefault(&config[4]) ? cliDefaults()->node_lat
: atof(&config[4]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "lon ", 4) == 0) {
_prefs->node_lon = atof(&config[4]);
_prefs->node_lon = cliIsDefault(&config[4]) ? cliDefaults()->node_lon
: atof(&config[4]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "rxdelay ", 8) == 0) {
_prefs->rx_delay_base = atof(&config[8]);
_prefs->rx_delay_base = cliIsDefault(&config[8]) ? cliDefaults()->rx_delay_base
: (float)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]);
_prefs->tx_delay_factor = cliIsDefault(&config[8]) ? cliDefaults()->tx_delay_factor
: (float)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) {
long m;
if (cliNum(&config[17], cliDefaults()->flood_max_advert, &m) && m >= 0 && m <= 64) {
_prefs->flood_max_advert = (uint8_t)m;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: range 0-64");
strcpy(reply, "Error: range 0-64, or default");
}
} else if (memcmp(config, "flood.max.unscoped ", 19) == 0) {
int m = atoi(&config[19]);
if (m >= 0 && m <= 64) {
long m;
if (cliNum(&config[19], cliDefaults()->flood_max_unscoped, &m) && m >= 0 && m <= 64) {
_prefs->flood_max_unscoped = (uint8_t)m;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: range 0-64");
strcpy(reply, "Error: range 0-64, or default");
}
} else if (memcmp(config, "flood.max ", 10) == 0) {
int m = atoi(&config[10]);
if (m >= 0 && m <= 64) {
long m;
if (cliNum(&config[10], cliDefaults()->flood_max, &m) && m >= 0 && m <= 64) {
_prefs->flood_max = (uint8_t)m;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: range 0-64");
strcpy(reply, "Error: range 0-64, or default");
}
} else if (memcmp(config, "direct.txdelay ", 15) == 0) {
_prefs->direct_tx_delay_factor = atof(&config[15]);
_prefs->direct_tx_delay_factor = cliIsDefault(&config[15])
? cliDefaults()->direct_tx_delay_factor
: (float)atof(&config[15]);
savePrefs();
strcpy(reply, "OK (ignored: direct.txdelay is now adaptive)");
} else if (memcmp(config, "backoff.multiplier ", 19) == 0) {
@@ -824,8 +937,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
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) {
/* initNodePrefs() leaves this at 0.0 (memset) even though the
* live default is ContentionTracker::DEFAULT_BACKOFF_MULT and
* RepeaterDataStore migrates 0.0 -> 0.2 on load, so `default`
* uses the real value rather than the prefs blank. */
float f;
if (!cliFloat(&config[19], 0.2f, &f)) {
strcpy(reply, "Error, range 0.0-2.0, or default");
} else if (f >= 0.0f && f <= 2.0f) {
_prefs->backoff_multiplier = f;
_callbacks->setBackoffMultiplier(f);
savePrefs();
@@ -846,13 +965,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
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;
long mode;
if (cliNum(config, cliDefaults()->path_hash_mode, &mode) &&
mode >= 0 && mode < 3) {
_prefs->path_hash_mode = (uint8_t)mode;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0,1, or 2");
strcpy(reply, "Error, must be 0, 1, 2 or default");
}
} else if (memcmp(config, "loop.detect ", 12) == 0) {
/* Loop detection runs only in the Repeater/RoomServer forward
@@ -871,9 +991,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
mode = LOOP_DETECT_MODERATE;
} else if (memcmp(config, "strict", 6) == 0) {
mode = LOOP_DETECT_STRICT;
} else if (cliIsDefault(config)) {
mode = cliDefaults()->loop_detect;
} else {
mode = 0xFF;
strcpy(reply, "Error, must be: off, minimal, moderate, or strict");
strcpy(reply, "Error, must be: off, minimal, moderate, strict, or default");
}
if (mode != 0xFF) {
_prefs->loop_detect = mode;
@@ -881,14 +1003,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(reply, "OK");
}
} else if (memcmp(config, "tx ", 3) == 0) {
char *end = nullptr;
long parsed = strtol(&config[3], &end, 10);
long parsed;
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);
if (!cliNum(&config[3], cliDefaults()->tx_power_dbm, &parsed) ||
parsed < -9 || parsed > max_tx) {
snprintf(reply, CLI_REPLY_SIZE, "Error: range -9 to %d dBm, or default", max_tx);
} else {
_prefs->tx_power_dbm = (int8_t)parsed;
savePrefs();
@@ -897,8 +1019,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
(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 f;
if (cliFloat(&config[5], cliDefaults()->freq, &f) &&
f >= 150.0f && f <= 2500.0f) {
float old_freq = _prefs->freq;
_prefs->freq = f;
savePrefs();
@@ -909,7 +1032,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_callbacks->freezeRadioParams(old_freq, _prefs->bw, _prefs->sf, _prefs->cr);
strcpy(reply, "OK - reboot to apply");
} else {
strcpy(reply, "Error: range 150-2500 MHz");
strcpy(reply, "Error: range 150-2500 MHz, or default");
}
} else if (strcmp(config, "adc.multiplier target") == 0) {
strcpy(reply, "Error: need mV target (e.g. set adc.multiplier target 4173)");
@@ -960,15 +1083,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
}
} 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;
/* 0 is valid (resets to the DTS default). cliFloat() rejects the
* non-numeric input that atof() used to fold into a 0 here, so the
* old "distinguish from literal 0" dance is gone. Upper bound
* covers all real divider/reference combinations with margin. */
float val;
bool bad = !cliFloat(arg, cliDefaults()->adc_multiplier, &val);
if (!bad) bad = (val != 0.0f && val < 100.0f) || val > 30000.0f || val < 0.0f;
if (bad) {
strcpy(reply, "Error: invalid multiplier (0 to reset, or 100-30000)");
strcpy(reply, "Error: invalid multiplier (0 to reset, 100-30000, or default)");
} else if (_board->setAdcMultiplier(val)) {
_prefs->adc_multiplier = val;
savePrefs();
@@ -981,11 +1104,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(reply, "Error: unsupported by this board");
}
} else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) {
const char* arg = &config[17];
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);
int val = cliOnOff(&config[17], cliDefaults()->fem_rxgain);
if (val == 0 || val == 1) {
/* Same shape as radio.rxgain: always save, then apply live and
* report when the radio driver has no FEM gate. */
@@ -1000,11 +1119,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(reply, "Error: must be 0, 1, on, or off");
}
} 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);
int val = cliOnOff(&config[13], cliDefaults()->rx_boost);
if (val == 0 || val == 1) {
/* Always save (upstream f3d4d8cd), then apply live and
* report when the radio has no RX boost feature. */
@@ -1019,11 +1134,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
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);
int val = cliOnOff(&config[7], cliDefaults()->rx_duty_cycle);
if (val == 0 || val == 1) {
_prefs->rx_duty_cycle = (uint8_t)val;
savePrefs();
@@ -1035,11 +1146,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
// Rotate the panel 180 degrees for cases that mount it upside down.
// Applied immediately -- the SSD1306/SH1106 remap is two bytes on
// the wire and the next frame comes out flipped, no redraw needed.
const char* arg = &config[15];
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);
int val = cliOnOff(&config[15], cliDefaults()->display_rotate);
if (val != 0 && val != 1) {
strcpy(reply, "Error: must be 0, 1, on, or off");
} else {
@@ -1065,11 +1172,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
// Deliberately separate from display.rotate: a case can flip the
// screen without flipping the stick, and boards whose panel cannot
// rotate can still need the axis swap.
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);
int val = cliOnOff(&config[13], cliDefaults()->input_rotate);
if (val == 0 || val == 1) {
zephcore_input_set_flipped(val == 1);
_prefs->input_rotate = (uint8_t)val;
@@ -1081,12 +1184,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} 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);
/* Not persisted, so "default" means the boot state: off. */
int val = cliOnOff(config + 8, 0);
if (val == 0 || val == 1) {
gps_set_diag(val == 1);
if (val == 1) {
@@ -1127,19 +1226,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
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];
int on = cliOnOff(&config[13], cliDefaults()->meshtimesync);
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 if (on < 0) {
strcpy(reply, "Error: must be on, off or default");
} else {
strcpy(reply, "Error: must be on or off");
_prefs->meshtimesync = (uint8_t)on;
savePrefs();
snprintf(reply, CLI_REPLY_SIZE, "OK - meshtimesync %s", on ? "on" : "off");
}
} else {
snprintf(reply, CLI_REPLY_SIZE, "unknown config: %.230s", config);
+30 -2
View File
@@ -106,7 +106,7 @@ struct NodePrefs {
uint8_t _reserved_apc_enabled;
uint8_t _reserved_apc_margin;
uint8_t meshtimesync; // 1 = mesh time-sync clock correction on (default off)
uint8_t cad_auto; // 1 = adaptive-CAD staircase acts on probe stats (default off = dry-run)
uint8_t cad_auto; // 1 = adaptive-CAD staircase acts on probe stats (default ON)
int8_t cad_offset; // operating detPeak offset from family base (-4..4)
uint8_t probe_interval; // seconds between periodic radio measurements:
// one noise-floor sample, and the CAD probe that
@@ -202,6 +202,11 @@ static inline void sanitizeNodePrefs(NodePrefs* p) {
p->airtime_factor = sanePrefFloat(p->airtime_factor, 0.0f, 9.0f, 9.0f);
p->rx_delay_base = sanePrefFloat(p->rx_delay_base, 0.0f, 3600.0f, 0.0f);
p->adc_multiplier = sanePrefFloat(p->adc_multiplier, 0.0f, 30000.0f, 0.0f);
/* Range matches the CLI (0.0-2.0). 0.0 is a legal value meaning "reactive
* backoff off" and must survive: this guard exists for NaN and corruption
* only. RepeaterDataStore used to coerce 0.0 -> 0.2 here, which silently
* undid the documented way to disable the feature on every boot. */
p->backoff_multiplier = sanePrefFloat(p->backoff_multiplier, 0.0f, 2.0f, 0.2f);
p->node_lat = sanePrefFloat(p->node_lat, -90.0, 90.0, 0.0);
p->node_lon = sanePrefFloat(p->node_lon, -180.0, 180.0, 0.0);
@@ -226,7 +231,10 @@ static inline void sanitizeNodePrefs(NodePrefs* p) {
p->rx_duty_cycle = saneBool<uint8_t>(p->rx_duty_cycle, 0);
p->leds_disabled = saneBool<uint8_t>(p->leds_disabled, 0);
p->meshtimesync = saneBool<uint8_t>(p->meshtimesync, 0);
p->cad_auto = saneBool<uint8_t>(p->cad_auto, 0);
/* Fallback must be the initNodePrefs() default (ON). It was 0, so a byte
* that was neither 0 nor 1 silently switched adaptive CAD off instead of
* restoring the default — left over from when the default was dry-run. */
p->cad_auto = saneBool<uint8_t>(p->cad_auto, 1);
p->allow_read_only = saneBool<uint8_t>(p->allow_read_only, 0);
p->powersaving_enabled = saneBool<uint8_t>(p->powersaving_enabled, 0);
p->display_rotate = saneBool<uint8_t>(p->display_rotate, 0);
@@ -249,6 +257,7 @@ static inline void sanitizeNodePrefs(NodePrefs* p) {
if (p->loop_detect > LOOP_DETECT_STRICT) p->loop_detect = LOOP_DETECT_MINIMAL;
if (p->path_hash_mode > 2) p->path_hash_mode = 0;
p->autoadd_max_hops = clampPref<uint8_t>(p->autoadd_max_hops, 0, 64);
p->flood_max = clampPref<uint8_t>(p->flood_max, 0, 64);
p->flood_max_unscoped = clampPref<uint8_t>(p->flood_max_unscoped, 0, 64);
p->flood_max_advert = clampPref<uint8_t>(p->flood_max_advert, 0, 64);
p->cad_busycap = clampPref<uint8_t>(p->cad_busycap, 0, 90);
@@ -304,6 +313,12 @@ static inline void initNodePrefs(NodePrefs* prefs) {
prefs->advert_interval = 0; // 0 = periodic local advert off; else minutes = value * 2
prefs->flood_advert_interval = 47; // hours
prefs->rx_delay_base = 0.0f;
/* Must match mesh::ContentionTracker::DEFAULT_BACKOFF_MULT. Left at the
* memset 0 (= backoff disabled) until now, with RepeaterDataStore coercing
* 0.0 -> 0.2 on load to paper over it — which also undid a deliberate 0.0.
* Companions never apply this (only Repeater/RoomServer call
* setBackoffMultiplier), so setting it here changes no existing node. */
prefs->backoff_multiplier = 0.2f;
prefs->tx_delay_factor = 0.5f;
prefs->direct_tx_delay_factor = 0.3f;
prefs->allow_read_only = 0;
@@ -335,4 +350,17 @@ static inline void initNodePrefs(NodePrefs* prefs) {
prefs->input_rotate = 0; // Default OFF — joystick axes as the board wires them
prefs->v_contact_enabled = 1; // Default ON — v-contact loopback admin chat (companion)
prefs->v_battery_alert_mv = 0xFFFF; // Sentinel: derive from board auto-shutdown threshold
/* Companion-only feature, and main_companion.cpp used to assign this by
* hand right after calling us — so no node ever ran without it. It lives
* here now because a default listed only at one call site is invisible to
* every other caller of initNodePrefs(), which is the exact drift that
* zeroed probe_interval and cad_auto in the past. Matches what
* loadPrefs()'s absent-field fallback and sanitizeNodePrefs() already use.
* 0 means disabled and is a legal stored value, so sanitize passes it
* through untouched — this default only applies to a fresh prefs struct. */
#ifdef CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS
prefs->auto_shutdown_mv = CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS;
#else
prefs->auto_shutdown_mv = 0;
#endif
}
+6 -2
View File
@@ -1446,8 +1446,12 @@ int main(void)
* loadPrefs then keeps that 0 instead of the real default (this is what
* zeroed probe_interval / cad_auto and, earlier, the GPS settings). */
initNodePrefs(&companion_mesh.prefs);
/* Companion-specific overrides vs. initNodePrefs defaults: */
companion_mesh.prefs.auto_shutdown_mv = CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS; /* low-batt cutoff (0=off) */
/* Companion-specific overrides vs. initNodePrefs defaults. auto_shutdown_mv
* used to be set here too; it now comes from initNodePrefs() itself, which
* is what the comment above asks for — a value listed only here is invisible
* to every other caller of initNodePrefs(). gps_interval stays because
* initNodePrefs() hardcodes the 300 s companion figure while repeaters run a
* much longer duty; this line is how a board's Kconfig value wins. */
companion_mesh.prefs.gps_interval = CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC; /* 5-min duty cycle (0=always-on) */
/* Load prefs from storage */