Files
HaloKeymind/test/fixtures/cli_settings/main.cpp
T
mikecarper 24b1b1167f Fix storage recovery and deferred runtime state
Recover usable ESP32 preferences and channels before permitting replacement; keep identity and ACL persistence transactional and complete.

Preserve MQTT commit boundaries, restore OTA identity and policy after deferred allocation, retain radio gain retries, and reject invalid flood limits. Add production-path fault regressions and CI coverage.
2026-09-15 16:46:17 -07:00

200 lines
9.1 KiB
C++

#include <cassert>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <initializer_list>
#include <helpers/CLICommandUtils.h>
#include <helpers/RadioProfileCommandUtils.h>
// Persistence has its own production-parser tests with an in-memory FS.
// This fixture isolates the actual CommonCLI setting branches and callbacks.
namespace mesh {
struct RadioProfileCLI {
static bool parseSuffix(const char* input, unsigned fields, char* legacy,
size_t size, uint16_t& preamble) {
return cli::parseRadioPreambleSuffix(input, fields, legacy, size, preamble);
}
bool savePrimaryPreamble(uint16_t) { return true; }
void appendPreamble(char*, size_t) {}
void appendSavedPreamble(char*, size_t, uint8_t, float) {}
mutable unsigned warning_calls = 0;
mutable uint8_t warning_sf = 0;
mutable float warning_bw = 0;
mutable uint16_t warning_preamble = 0;
void appendPrimaryChirpWarning(char* reply, size_t capacity, uint8_t sf, float bw, uint16_t preamble) const {
++warning_calls; warning_sf = sf; warning_bw = bw; warning_preamble = preamble;
const size_t used = strlen(reply);
if (used < capacity) snprintf(reply + used, capacity - used, "; WARN recommended preamble: radio=32");
}
bool acceptsPrimary(float, float, uint8_t, uint8_t, uint16_t) const { return true; }
};
}
#define MIN_LORA_TX_POWER -9
#define MAX_LORA_TX_POWER 22
#include <helpers/radiolib/RadioPowerLimits.h>
#include <helpers/radiolib/LR2021SideDetectorConfig.h>
struct StrHelper {
static const char* ftoa(float value) {
static char buffer[24]; snprintf(buffer, sizeof(buffer), "%.3f", double(value)); return buffer;
}
static const char* ftoa3(float value) { return ftoa(value); }
};
struct Prefs {
float freq=909.5f, bw=62.5f, airtime_factor=1, rx_delay_base=0;
float tx_delay_factor=0, direct_tx_delay_factor=0;
uint8_t sf=7, cr=5, interference_threshold=0, agc_reset_interval=0;
uint8_t path_hash_mode=0, multi_acks=0, rx_ps_level=0, rx_ps_preamble=0;
int8_t tx_power_dbm=10;
uint32_t rx_ps_rx_us=1000, rx_ps_sleep_us=1000;
bool cad_enabled=false, rx_boosted_gain=false;
uint8_t extra_sf[4]={};
uint8_t flood_max=16, flood_max_advert=16, flood_max_unscoped=16;
};
struct Callbacks {
unsigned saves=0, tx_calls=0, gain_calls=0;
bool accept=true;
int8_t power=0;
bool gain=false;
void savePrefs() { ++saves; }
bool setTxPower(int8_t value) { ++tx_calls; if (!accept) return false; power=value; return true; }
bool setRxBoostedGain(bool value) { ++gain_calls; if (!accept) return false; gain=value; return true; }
bool configSideDetectors(const uint8_t*,uint8_t,float) { return accept; }
};
struct Board { unsigned n_cad_busy=0; };
bool isValidLoRaBandwidth(float bw) { return bw==125 || bw==62.5f; }
void recalcRxPowerSavingFromLevel(uint8_t,uint8_t,float,uint8_t,uint32_t*,uint32_t*) {}
void appendRxPowerSavingAdjustmentNote(char*,Prefs*,uint8_t,float) {}
@KEY_EQUALS@
class CLI {
public:
Prefs prefs;
Callbacks callbacks;
Board board;
mesh::RadioProfileCLI _radio_profiles;
Prefs* _prefs=&prefs;
Callbacks* _callbacks=&callbacks;
Board* _board=&board;
void savePrefs() { callbacks.savePrefs(); }
bool savePrimaryRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, uint16_t) {
prefs.freq=freq; prefs.bw=bw; prefs.sf=sf; prefs.cr=cr;
prefs.tx_power_dbm=mesh::clampLoRaTxPower(prefs.tx_power_dbm,freq);
callbacks.savePrefs(); return true;
}
void handleSetCmd(uint32_t, char* command, char* reply) {
const char* config=command+4;
@SET@
else strcpy(reply,"unknown config");
}
void handleGetCmd(uint32_t, char* command, char* reply) {
const char* config=command+4;
@GET@
else strcpy(reply,"unknown config");
}
void command(uint32_t sender_timestamp, char* command, char* reply) {
@DISPATCH@
else strcpy(reply,"unknown command");
}
void call(uint32_t sender, const char* text, char* reply) {
char command[160]; snprintf(command,sizeof(command),"%s",text);
this->command(sender,command,reply);
}
};
struct Case { const char *key, *value; double expected; };
int main() {
// Local USB and authenticated on-air calls use the same infrastructure CLI.
for (uint32_t sender : {0u, 123456u}) {
CLI cli; char text[160], reply[160];
for (const Case& c : {
Case{"freq","915.5",915.5}, {"af","2.5",2.5}, {"dutycycle","25",25},
{"int.thresh","128",128}, {"tx","14",14}, {"rxdelay","2.5",2.5},
{"agc.reset.interval","19",16}, {"path.hash.mode","2",2},
{"multi.acks","1",1}, {"txdelay","1.5",1.5}, {"direct.txdelay","0.5",0.5}}) {
unsigned saves=cli.callbacks.saves;
snprintf(text,sizeof(text),"set %s %s",c.key,c.value);
cli.call(sender,text,reply);
if (strncmp(reply,"OK",2)) { fprintf(stderr,"%s -> %s\n",text,reply); return 1; }
assert(cli.callbacks.saves==saves+1);
snprintf(text,sizeof(text),"get %s",c.key);
cli.call(sender,text,reply);
double actual=0;
assert(sscanf(reply,"> %lf",&actual)==1 && fabs(actual-c.expected)<0.001);
assert(cli.callbacks.saves==saves+1); // reading must not persist
}
for (const char* key : {"cad","radio.rxgain"}) {
for (const char* state : {"on","off"}) {
unsigned saves=cli.callbacks.saves;
snprintf(text,sizeof(text),"set %s %s",key,state); cli.call(sender,text,reply);
assert(!strcmp(reply,"OK") && cli.callbacks.saves==saves+1);
snprintf(text,sizeof(text),"get %s",key); cli.call(sender,text,reply);
assert(!strncmp(reply+2,state,strlen(state)));
}
}
unsigned saves=cli.callbacks.saves;
cli.call(sender,"set radio 916,125,8,6",reply);
assert(!strncmp(reply,"OK",2) && cli.callbacks.saves==saves+1);
assert(strstr(reply,"WARN recommended preamble: radio=32"));
assert(cli._radio_profiles.warning_calls==1 && cli._radio_profiles.warning_sf==8);
assert(cli._radio_profiles.warning_bw==125 && cli._radio_profiles.warning_preamble==0);
cli.call(sender,"get radio",reply);
float freq=0,bw=0; int sf=0,cr=0;
assert(sscanf(reply,"> %f,%f,%d,%d",&freq,&bw,&sf,&cr)==4);
assert(freq==916 && bw==125 && sf==8 && cr==6);
cli.call(sender,"set radio 916,125,8,6,16",reply);
assert(strstr(reply,"WARN recommended preamble: radio=32"));
assert(cli._radio_profiles.warning_calls==2 && cli._radio_profiles.warning_preamble==16);
cli.call(sender,"set radio 916,125,13,6",reply);
assert(!strncmp(reply,"Error",5) && !strstr(reply,"WARN"));
assert(cli._radio_profiles.warning_calls==2);
saves=cli.callbacks.saves;
cli.call(sender,"set extra.sf 9,10",reply);
#if defined(USE_LR2021)
assert(!strncmp(reply,"OK",2) && cli.callbacks.saves==saves+1);
assert(cli.prefs.extra_sf[0]==9 && cli.prefs.extra_sf[1]==10 && cli.prefs.extra_sf[2]==0);
cli.call(sender,"get extra.sf",reply); assert(!strcmp(reply,"9,10"));
saves=cli.callbacks.saves;
cli.call(sender,"set extra.sf 8",reply);
assert(strstr(reply,"Invalid") && cli.callbacks.saves==saves && cli.prefs.extra_sf[0]==9);
cli.call(sender,"set extra.sf none",reply);
assert(!strncmp(reply,"OK",2) && cli.prefs.extra_sf[0]==0);
cli.call(sender,"get extra.sf",reply); assert(!strcmp(reply,"No extra SF configured"));
#else
assert(strstr(reply,"requires an LR2021") && cli.callbacks.saves==saves);
cli.call(sender,"get extra.sf",reply);
assert(strstr(reply,"requires an LR2021") && cli.prefs.extra_sf[0]==0);
#endif
// A failed hardware apply must not persist or advertise a changed setting.
cli.callbacks.accept=false;
const int8_t old_power=cli.prefs.tx_power_dbm;
const bool old_gain=cli.prefs.rx_boosted_gain;
saves=cli.callbacks.saves;
cli.call(sender,"set tx 16",reply);
assert(!strncmp(reply,"Error",5) && cli.prefs.tx_power_dbm==old_power);
cli.call(sender,"set radio.rxgain on",reply);
assert(!strncmp(reply,"Error",5) && cli.prefs.rx_boosted_gain==old_gain);
assert(cli.callbacks.saves==saves);
cli.call(sender,"set unknown.setting 1",reply);
assert(!strcmp(reply,"unknown config") && cli.callbacks.saves==saves);
for (const char* key : {"flood.max", "flood.max.advert", "flood.max.unscoped"}) {
for (const char* invalid : {"", "invalid", "65", "256", "-1", "1tail",
"1.5", "4294967296", "999999999999999999999"}) {
saves=cli.callbacks.saves;
snprintf(text,sizeof(text),"set %s %s",key,invalid); cli.call(sender,text,reply);
assert(!strncmp(reply,"Error",5) && cli.callbacks.saves==saves);
assert(cli.prefs.flood_max==16 && cli.prefs.flood_max_advert==16
&& cli.prefs.flood_max_unscoped==16);
}
}
for (const char* key : {"flood.max", "flood.max.advert", "flood.max.unscoped"}) {
for (unsigned value : {0u, 1u, 64u}) {
saves=cli.callbacks.saves;
snprintf(text,sizeof(text),"set %s %u",key,value); cli.call(sender,text,reply);
assert(!strcmp(reply,"OK") && cli.callbacks.saves==saves+1);
snprintf(text,sizeof(text),"get %s",key); cli.call(sender,text,reply);
unsigned actual=1000; assert(sscanf(reply,"> %u",&actual)==1 && actual==value);
}
}
}
}