refactor(prefs): settings saved as /prefs.json, upstream's ConfigSerializer

NodePrefs is now NodePrefsFields (the fields) plus how they're saved: one
JSON key per field, upstream's keys under "radio", "gps", "repeat", "comp"
and "custom" (so either firmware reads what the other wrote of them),
ours under "solo". Saved atomically (temp file, then renamed), through a
buffer. 2.0-rc's binary /prefs is removed, not converted: 2.0 starts from
the defaults; /new_prefs from before 2.0 is still imported.

From upstream with it: txdelay / direct.txdelay / agc.reset.interval
(prefs and CLI), and the board's own settings through getCustom()
(Board::attachDynamicPrefs) in place of the merge's stopgap adapter.
ConfigSerializer reads values up to 191 characters (was 127): 140-byte
texts, an 80-byte table in hex.

Test: every field round-trips through the JSON byte for byte; the alarm
label tests expect "Once" (the label changed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-15 13:25:58 +02:00
co-authored by Claude Opus 5.5
parent 28be1c6206
commit bd64c203c7
9 changed files with 505 additions and 111 deletions
+23 -1
View File
@@ -21,7 +21,10 @@ bool MyMesh::handleCliCommand(const char* command, char* reply, int n) {
command += 3;
}
if (cliRadioCommand(command, reply, n)) return true;
if (n >= 64 && board.handleCommand(command, 0, reply)) return true; // the board's own (radio.fem.*); short replies
if (n >= 64 && board.handleCommand(command, 0, reply)) { // the board's own (radio.fem.*); short replies
if (_prefs.isDirty()) savePrefs();
return true;
}
if (strcmp(command, "reboot") == 0 || strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) {
bool restart = command[0] == 'r';
@@ -185,6 +188,25 @@ bool MyMesh::cliRadioCommand(const char* command, char* reply, int n) {
else { _prefs.rx_delay_base = d; savePrefs(); snprintf(reply, n, "OK"); }
return true;
}
if (strcmp(command, "get txdelay") == 0) { snprintf(reply, n, "> %s", StrHelper::ftoa(_prefs.tx_delay_factor)); return true; }
if (strcmp(command, "get direct.txdelay") == 0) { snprintf(reply, n, "> %s", StrHelper::ftoa(_prefs.direct_tx_delay_factor)); return true; }
if (memcmp(command, "set txdelay ", 12) == 0 || memcmp(command, "set direct.txdelay ", 19) == 0) {
const bool direct = command[4] == 'd';
float f = atof(&command[direct ? 19 : 12]);
if (!(f >= 0 && f <= 2.0f)) { snprintf(reply, n, "Error, must be 0-2"); return true; }
(direct ? _prefs.direct_tx_delay_factor : _prefs.tx_delay_factor) = f;
savePrefs();
snprintf(reply, n, "OK");
return true;
}
if (strcmp(command, "get agc.reset.interval") == 0) { snprintf(reply, n, "> %d", _prefs.agc_reset_interval * 4); return true; }
if (memcmp(command, "set agc.reset.interval ", 23) == 0) {
int secs = constrain(atoi(&command[23]), 0, 255 * 4);
_prefs.agc_reset_interval = (uint8_t)(secs / 4);
savePrefs();
snprintf(reply, n, "OK - interval rounded to %d", _prefs.agc_reset_interval * 4);
return true;
}
if (strcmp(command, "get path.hash.mode") == 0) { snprintf(reply, n, "> %d", (int)_prefs.path_hash_mode); return true; }
if (memcmp(command, "set path.hash.mode ", 19) == 0) {
int m = atoi(&command[19]);