Files
MeshCore/test/fixtures/radio_profiles/cli_test.cpp
T
mikecarper a2a7d3b168 Order radio schedules and preserve temporary lease deadlines
Apply due radio2 settings by requested start time and expire every old temporary
window before starting its successor. Anchor temporary deadlines to command
acceptance so idle time before a command cannot shorten the new lease.

Keep failed radio2 saves in their original order and retry without rewriting
their scheduled timestamps. Give repeater persistence its own backoff and cache
the next actionable deadline, preventing repeated writes while allowing
temporary windows to start and expire on time.

Exercise all 24 slot insertion orders with punctual and delayed servicing, plus
adjacent windows, storage failures, command-time anchoring, and timer rollover.
2026-09-15 08:59:10 -07:00

490 lines
22 KiB
C++

#include <Arduino.h>
#include <helpers/RadioProfileCLI.h>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <algorithm>
struct Clock : mesh::RTCClock {
uint32_t epoch = 1700000000;
uint32_t getCurrentTime() override { return epoch; }
void setCurrentTime(uint32_t time) override { epoch = time; }
};
struct Radio : mesh::Radio {
mesh::RadioProfiles p;
Radio() { p.primary.freq=909.5; p.primary.bw=62.5; p.primary.sf=7; p.primary.cr=5; }
mesh::RadioProfiles* profiles() override { return &p; }
const mesh::RadioProfiles* profiles() const override { return &p; }
bool validateProfile(const mesh::RadioProfileParams& v) const override { return mesh::RadioProfiles::valid(v) && v.bw<=500; }
uint16_t profilePreamble(uint8_t profile) const override { return p.preamble(profile, 32); }
int recvRaw(uint8_t*,int) override { return 0; }
uint32_t getEstAirtimeFor(int) override { return 10; }
float packetScore(float,int) override { return 0; }
bool startSendRaw(const uint8_t*,int) override { return true; }
bool isSendComplete() override { return true; }
void onSendFinished() override {}
bool isInRecvMode() const override { return true; }
};
struct Fixture {
MemoryFS fs;
Clock clock;
Radio radio;
mesh::RadioProfileCLI cli;
char reply[160];
Fixture(bool infrastructure = false) { g_mock_millis=0; cli.begin(&fs,&radio,&clock,infrastructure); }
const char* cmd(const char* text, bool ok=true) {
memset(reply,0x55,sizeof(reply));
assert(cli.handle(text,reply,sizeof(reply)));
assert(memchr(reply,0,sizeof(reply)));
if (strncmp(text,"get ",4) && strncmp(text,"del ",4)) {
if ((!strncmp(reply,"OK",2))!=ok) { fprintf(stderr,"%s -> %s\n",text,reply); assert(false); }
}
return reply;
}
void advance(uint32_t ms, bool epoch=true) { g_mock_millis+=ms; if(epoch)clock.epoch+=ms/1000; cli.loop(); }
};
static void permanentScheduleOrder() {
for (bool fail_first_save : {false, true}) {
Fixture f;
char command[160];
snprintf(command, sizeof(command), "set radioat2 912.5,500,8,5,rx,%lu,80",
(unsigned long)(f.clock.epoch + 120));
f.cmd(command);
snprintf(command, sizeof(command), "set radioat2 910.5,500,8,5,rx,%lu,80",
(unsigned long)(f.clock.epoch + 60));
f.cmd(command);
f.fs.fail_write = fail_first_save;
f.advance(120000); // both are due after sleep or a forward clock correction
if (fail_first_save) {
f.fs.fail_write = false;
f.advance(60000);
}
assert(f.radio.p.secondary.params.freq == 912.5f);
Radio reboot; mesh::RadioProfileCLI restored;
restored.begin(&f.fs, &reboot, &f.clock);
assert(reboot.p.secondary.params.freq == 912.5f);
assert(!strcmp(f.cmd("get radioat2 all"), "> slots: "));
}
}
static void adjacentTemporarySchedules() {
for (bool later_first : {false, true}) {
Fixture f;
const uint32_t epoch = f.clock.epoch;
for (unsigned i = 0; i < 2; ++i) {
const bool later = later_first ? i == 0 : i == 1;
char command[160];
snprintf(command, sizeof(command), "set tempradioat2 %.1f,500,8,5,rxtx,%lu,%lu,80",
later ? 912.5 : 910.5, (unsigned long)(epoch + (later ? 60 : 1)),
(unsigned long)(epoch + (later ? 120 : 60)));
f.cmd(command);
}
f.advance(1000);
assert(f.radio.p.secondary_temporary && f.radio.p.secondary.params.freq == 910.5f);
f.advance(59000);
assert(f.radio.p.secondary_temporary && f.radio.p.secondary.params.freq == 912.5f);
f.advance(59000);
assert(f.radio.p.secondary_temporary && f.radio.p.secondary.params.freq == 912.5f);
f.advance(1000);
assert(!f.radio.p.enabled());
}
}
static void scheduledLeaseStartsAtCommandTime() {
for (bool rollover : {false, true}) {
Fixture f;
if (rollover) { g_mock_millis = UINT32_MAX - 30000; f.cli.loop(); }
g_mock_millis += 10000; f.clock.epoch += 10; // command arrives between loop calls
char command[160];
snprintf(command, sizeof(command), "set tempradioat2 910.5,500,8,5,rx,%lu,%lu,80",
(unsigned long)(f.clock.epoch + 1), (unsigned long)(f.clock.epoch + 60));
f.cmd(command);
f.advance(1000);
assert(f.radio.p.secondary_temporary);
f.advance(49000);
assert(f.radio.p.secondary_temporary); // idle time before the command is not charged to it
f.advance(9000);
assert(f.radio.p.secondary_temporary);
f.advance(1000);
assert(!f.radio.p.enabled());
}
}
static void schedulePermutations() {
int order[] = {0, 1, 2, 3};
do {
for (bool temporary : {false, true}) for (bool late : {false, true}) {
Fixture f;
const uint32_t epoch = f.clock.epoch;
for (int entry : order) {
char command[160];
if (temporary) {
snprintf(command, sizeof(command), "set tempradioat2 %.1f,500,8,5,rx,%lu,%lu,80",
910.5 + entry, (unsigned long)(epoch + 1 + entry * 60),
(unsigned long)(epoch + 61 + entry * 60));
} else {
snprintf(command, sizeof(command), "set radioat2 %.1f,500,8,5,rx,%lu,80",
910.5 + entry, (unsigned long)(epoch + (entry + 1) * 60));
}
f.cmd(command);
}
if (late) {
f.advance(temporary ? 181000 : 240000);
assert(f.radio.p.secondary.params.freq == 913.5f);
} else {
for (int entry = 0; entry < 4; ++entry) {
f.advance(temporary && entry == 0 ? 1000 : 60000);
assert(f.radio.p.secondary.params.freq == 910.5f + entry);
}
}
assert(f.radio.p.secondary_temporary == temporary);
if (temporary) { f.advance(60000); assert(!f.radio.p.enabled()); }
}
} while (std::next_permutation(order, order + 4));
}
static void storageBackoffDoesNotDelayTemporaryWindows() {
Fixture f;
f.cmd("set radio2 910.5,500,8,5,rx,80"); f.advance(2000);
char command[160];
snprintf(command, sizeof(command), "set radioat2 912.5,500,8,5,rx,%lu,80", (unsigned long)(f.clock.epoch + 2));
f.cmd(command);
snprintf(command, sizeof(command), "set tempradioat2 911.5,500,8,5,rx,%lu,%lu,80",
(unsigned long)(f.clock.epoch + 30), (unsigned long)(f.clock.epoch + 90));
f.cmd(command);
f.fs.fail_write = true;
f.advance(2000);
f.advance(28000);
assert(f.radio.p.secondary_temporary && f.radio.p.secondary.params.freq == 911.5f);
f.advance(60000);
assert(!f.radio.p.secondary_temporary && f.radio.p.secondary.params.freq == 910.5f);
f.fs.fail_write = false;
f.advance(60000);
assert(f.radio.p.secondary.params.freq == 912.5f);
}
static void scheduledSaveFailureKeepsOriginalTimes() {
Fixture f;
char command[160];
const uint32_t start = f.clock.epoch + 60;
snprintf(command, sizeof(command), "set radioat2 910.5,500,8,5,rx,%lu,80", (unsigned long)start);
f.cmd(command);
f.fs.fail_write = true;
f.advance(60000);
char timestamp[32]; snprintf(timestamp, sizeof(timestamp), "@%lu", (unsigned long)start);
assert(strstr(f.cmd("get radioat2 all"), timestamp));
f.fs.fail_write = false;
f.advance(59000);
assert(!f.radio.p.enabled());
f.advance(1000);
assert(f.radio.p.secondary.params.freq == 910.5f);
}
int main(int argc, char** argv) {
if (argc == 2) {
if (!strcmp(argv[1], "permanent_order")) permanentScheduleOrder();
else if (!strcmp(argv[1], "temporary_boundary")) adjacentTemporarySchedules();
else if (!strcmp(argv[1], "lease_creation")) scheduledLeaseStartsAtCommandTime();
else if (!strcmp(argv[1], "save_failure")) scheduledSaveFailureKeepsOriginalTimes();
else if (!strcmp(argv[1], "schedule_permutations")) schedulePermutations();
else if (!strcmp(argv[1], "storage_isolation")) storageBackoffDoesNotDelayTemporaryWindows();
else return 2;
return 0;
}
for (bool damaged_primary : {false, true}) {
Fixture f(true);
f.cmd("set radio2 910.5,500,8,5,rxtx,80");
f.cmd("set radio2.cross off");
f.cmd("set tx.reply radio2 force");
assert(f.cli.savePrimaryPreamble(120));
assert(f.fs.rename("/radio_profiles", "/radio_profiles.bak"));
const auto backup = f.fs.files["/radio_profiles.bak"];
if (damaged_primary) {
f.fs.files["/radio_profiles"] = {0};
f.fs.fail_remove = true;
} else {
f.fs.fail_rename = 1;
}
Radio radio; mesh::RadioProfileCLI restored;
restored.begin(&f.fs, &radio, &f.clock, true);
// The verified backup stays usable even when storage cannot repair it.
assert(radio.p.enabled());
assert(radio.p.secondary.params.freq == 910.5f);
assert(radio.p.secondary.params.preamble == 80);
assert(restored.primaryPreamble() == 120);
assert(radio.p.cross == mesh::RadioCrossMode::Off);
assert(radio.p.reply_tx == mesh::RADIO_TX_SECONDARY && radio.p.reply_force);
f.fs.fail_remove = false;
assert(restored.handle("set radio2 off", f.reply));
assert(strstr(f.reply, "Error"));
assert(f.fs.files["/radio_profiles.bak"] == backup);
}
{
Fixture f;
f.cmd("set radio2 910.5,500,8,5,rxtx,80"); f.advance(2000);
const auto committed = f.fs.files["/radio_profiles"];
f.fs.fail_rename_from = {"/radio_profiles.tmp", "/radio_profiles.bak"};
f.cmd("set radio2 off", false); // both publication and rollback fail
assert(f.radio.p.enabled());
assert(f.fs.files["/radio_profiles.bak"] == committed);
f.fs.fail_rename_from.clear();
f.cmd("set radio2.cross on", false); // protect the sole committed copy
assert(f.fs.files["/radio_profiles.bak"] == committed);
Radio radio; mesh::RadioProfileCLI restored;
restored.begin(&f.fs, &radio, &f.clock);
assert(radio.p.enabled() && radio.p.secondary.params.preamble == 80);
assert(restored.handle("set radio2 off", f.reply));
assert(!strncmp(f.reply, "OK", 2)); // reboot repairs storage and releases the hold
}
{
Fixture f(true);
f.cmd("set tx.reply off");
// Simulate an image whose CRC is valid but whose saved mode is invalid.
auto& image=f.fs.files["/radio_profiles"];
image[3]=9;
uint32_t crc=0xffffffffU;
for (size_t i=0;i<20;++i) {
crc ^= image[i];
for (unsigned bit=0;bit<8;++bit) crc=(crc>>1)^((crc&1)?0xedb88320U:0);
}
memcpy(image.data()+20,&crc,4);
const auto damaged=image;
Radio radio; mesh::RadioProfileCLI restored;
restored.begin(&f.fs,&radio,&f.clock,true);
restored.loop();
assert(radio.p.reply_tx==mesh::RADIO_TX_BOTH && !radio.p.reply_force);
assert(!radio.p.enabled());
assert(restored.handle("set tx.reply radio",f.reply));
assert(strstr(f.reply,"Error"));
assert(f.fs.files["/radio_profiles"]==damaged);
}
{
Fixture companion;
assert(companion.radio.p.reply_tx==mesh::RADIO_TX_AUTO);
assert(!companion.cli.handle("set tx.reply both force",companion.reply));
Fixture f(true);
assert(strstr(f.cmd("get tx.reply"),"> both; radio2 TX unavailable"));
f.cmd("set radio2 910.5,500,7,5,rx"); f.advance(2000);
f.cmd("set tx.reply both force");
assert(!strcmp(f.cmd("get tx.reply"),"> both force"));
assert(f.radio.p.reply_tx==mesh::RADIO_TX_BOTH && f.radio.p.reply_force);
assert(f.radio.p.secondary.mode==mesh::RadioProfileMode::Rx);
assert(f.fs.files["/radio_profiles"].size()==24);
// Every legal choice survives reboot, including explicit AUTO (distinct
// from the zero reserved byte in firmware predating reply routing).
const char* modes[]={"auto","radio","radio2","both","off","radio2 force","both force"};
const uint8_t policies[]={0,1,2,3,4,2,3};
for (unsigned i=0;i<7;++i) {
char command[64]; snprintf(command,sizeof(command),"set tx.reply %s",modes[i]);
f.cmd(command);
Radio reboot; mesh::RadioProfileCLI restored;
restored.begin(&f.fs,&reboot,&f.clock,true);
assert(reboot.p.reply_tx==policies[i] && reboot.p.reply_force==(i>=5));
}
f.fs.fail_write=true;
f.cmd("set tx.reply off",false);
assert(f.radio.p.reply_tx==mesh::RADIO_TX_BOTH && f.radio.p.reply_force);
f.fs.fail_write=false; f.fs.fail_rename=2;
f.cmd("set tx.reply off",false);
Radio reboot; mesh::RadioProfileCLI restored;
restored.begin(&f.fs,&reboot,&f.clock,true);
assert(reboot.p.reply_tx==mesh::RADIO_TX_BOTH && reboot.p.reply_force);
const char* invalid[]={"", "both junk", "auto force", "radio force", "off force",
"both force extra", "force", "bothforce", "radio2345678901234567"};
for (const char* args:invalid) {
char command[80]; snprintf(command,sizeof(command),"set tx.reply %s",args);
f.cmd(command,false);
assert(f.radio.p.reply_tx==mesh::RADIO_TX_BOTH && f.radio.p.reply_force);
}
assert(strstr(f.cmd("get tx.reply extra"),"Error"));
// A Companion neither activates nor erases infrastructure reply choices
// when it saves another profile setting.
Radio other; mesh::RadioProfileCLI other_cli;
other_cli.begin(&f.fs,&other,&f.clock);
assert(other.p.reply_tx==mesh::RADIO_TX_AUTO && !other.p.reply_force);
assert(other_cli.handle("set radio2.cross on",f.reply));
Radio back; mesh::RadioProfileCLI back_cli;
back_cli.begin(&f.fs,&back,&f.clock,true);
assert(back.p.reply_tx==mesh::RADIO_TX_BOTH && back.p.reply_force);
// Old images have a zero reserved byte and adopt BOTH on infrastructure.
Fixture legacy; legacy.cmd("set radio2 910.5,500,7,5,rxtx");
assert(legacy.fs.files["/radio_profiles"][19]==0);
Radio upgrade; mesh::RadioProfileCLI upgraded;
upgraded.begin(&legacy.fs,&upgrade,&legacy.clock,true);
assert(upgrade.p.reply_tx==mesh::RADIO_TX_BOTH && !upgrade.p.reply_force);
}
{
Fixture f;
assert(!strcmp(f.cmd("get radio2"),"> off"));
assert(!strcmp(f.cmd("get radio2.cross"),"> auto"));
assert(strstr(f.cmd("set radio2 910.5,500,7,5,rxtx"),"WARN recommended preamble: radio2=64"));
assert(!f.radio.p.enabled()); f.advance(2000); assert(f.radio.p.enabled());
assert(strstr(f.cmd("get radio2"),"rxtx,64 (auto)"));
assert(strstr(f.cmd("get radio2.scan"),"slow=radio; listen_us=9421,21847"));
assert(strstr(f.cmd("get radio2.timing"),"chirps=4.60,85.34; need=32,64"));
assert(strstr(f.reply,"switch=600us"));
assert(strstr(f.reply,"loop=300us"));
assert(strstr(f.reply,"estimate"));
assert(strstr(f.reply,"WARN recommended preamble: radio2=64"));
char timing_reply[160]; strcpy(timing_reply, f.reply);
assert(!strcmp(f.cmd("get radio.timing"),timing_reply));
f.cmd("set tempradio2 911.5,500,8,5,rx,2,64");
assert(!f.radio.p.secondary_temporary); f.advance(2000);
assert(f.radio.p.secondary_temporary && f.radio.p.secondary.params.freq==911.5f);
assert(!f.radio.p.canCross());
assert(strstr(f.cmd("get tempradio2"),"0d0h2m"));
f.cmd("set radio2.cross on"); assert(f.radio.p.canCross());
f.cmd("set radio2.cross off"); assert(!f.radio.p.canCross());
f.cmd("set tempradio2 off"); f.advance(2000);
assert(!f.radio.p.secondary_temporary && f.radio.p.secondary.params.freq==910.5f);
f.cmd("set radio2 off"); f.advance(2000); assert(!f.radio.p.enabled());
f.cmd("get radio2.status");
}
{
Fixture f;
const char* invalid[] = {"910.5,500,7,5", "NaN,500,7,5,rx", "910.5,0,7,5,rx",
"910.5,500,256,5,rx", "910.5,500,7,256,rx", "910.5,500,7,5,invalid",
"910.5,500,7,5,rx,7", "910.5,500,7,5,rx,65535", "910.5,500,7,5,rx,32,junk",
"910.5,500,7,5,rx,32,", "910.5,500,7,5,rx,32junk"};
char text[160];
for (auto args : invalid) { snprintf(text,sizeof(text),"set radio2 %s",args); f.cmd(text,false); assert(!f.radio.p.enabled()); }
f.cmd("set tempradio2 910.5,500,7,5,rxtx,0",false);
f.cmd("set tempradio2 910.5,500,7,5,rxtx,10081",false);
f.cmd("set radio2.status 1",false);
f.cmd("set radio2.timing 1",false);
f.cmd("set radio.timing 1",false);
assert(strstr(f.cmd("get radio2.timing extra"),"read-only"));
assert(!f.cli.handle("set radio2junk 910.5",f.reply));
}
{
Fixture f;
assert(!strcmp(f.cmd("get radio2.timing"),"> off"));
// A short explicit preamble is not rewritten, and cannot hide a warning.
assert(strstr(f.cmd("set radio2 910.5,500,7,5,rx,32"),"WARN recommended preamble: radio2=64"));
assert(strstr(f.reply,"short override: radio2"));
f.advance(2000);
assert(f.radio.p.secondary.params.preamble==32);
assert(strstr(f.cmd("get radio2"),"short override: radio2"));
f.radio.p.longest_switch_us=8428;
assert(strstr(f.cmd("get radio2.timing"),"switch=8428us"));
assert(strstr(f.reply,"WARN recommended preamble: radio="));
assert(f.radio.p.secondary.params.preamble==32);
// Primary getters use the saved primary's SF/BW instead of a temporary one.
f.radio.p.primary.sf=10; f.radio.p.primary.bw=125;
strcpy(f.reply,"> radio");
f.cli.appendSavedPreamble(f.reply,sizeof(f.reply),7,62.5);
assert(strstr(f.reply,"WARN recommended preamble: radio="));
// Bounded appenders cannot corrupt neighboring bytes, even with tiny replies.
for (size_t n=1;n<=160;++n) {
char bytes[162]; memset(bytes,0x55,sizeof(bytes)); bytes[0]=0;
mesh::RadioProfileCLI::appendChirpWarning(bytes,n,f.radio.p);
assert(memchr(bytes,0,n)); assert(bytes[n]==0x55);
}
f.radio.p.primary.sf=10;f.radio.p.primary.bw=125;
f.radio.p.secondary.params.sf=10;f.radio.p.secondary.params.bw=125;
f.radio.p.longest_switch_us=0;
assert(!strstr(f.cmd("get radio2.timing"),"WARN"));
// A selected value below 32 still gets the actual recommendation, not
// merely a warning label with no preamble to use.
assert(strstr(f.cmd("set radio2 910.5,125,10,5,rx,16"),"WARN recommended preamble: radio2=32"));
assert(strstr(f.reply,"short override: radio2"));
}
{
Fixture f;
f.radio.p.primary.sf=7;f.radio.p.primary.bw=500;
assert(strstr(f.cmd("set radio2 910.5,62.5,7,5,rx"),"WARN recommended preamble: radio=64"));
f.advance(2000);
assert(strstr(f.cmd("get radio2.timing"),"need=64,32"));
strcpy(f.reply,"OK - reboot to apply");
f.cli.appendPrimaryChirpWarning(f.reply,sizeof(f.reply),7,500,32);
assert(strstr(f.reply,"WARN recommended preamble: radio=64"));
assert(strstr(f.reply,"short override: radio"));
strcpy(f.reply,"OK - reboot to apply");
f.cli.appendPrimaryChirpWarning(f.reply,sizeof(f.reply),7,62.5,0);
assert(!strstr(f.reply,"WARN")); // Preview requested tuple, not the live one.
f.radio.p.primary.sf=5;f.radio.p.primary.bw=500;
f.radio.p.secondary.params.sf=5;f.radio.p.secondary.params.bw=500;
assert(strstr(f.cmd("get radio2.timing"),"WARN recommended preamble: radio="));
assert(strstr(f.reply,",radio2="));
}
{
Fixture f;
// The tested pair is allocated automatically without changing explicit 32.
assert(strstr(f.cmd("set radio2 910.5,500,8,5,rx,32"),"WARN recommended preamble: radio2=88"));
f.advance(2000);
assert(f.radio.p.secondary.params.preamble==32);
assert(strstr(f.cmd("get radio2.scan"),"slow=radio; listen_us=9421,21847"));
assert(strstr(f.cmd("get radio2.timing"),"chirps=4.60,42.67; need=32,88"));
assert(strstr(f.reply,"loop=300us"));
}
{
Fixture f;
f.cmd("set radio2 910.5,500,9,5,rxtx,40"); f.advance(2000);
f.fs.fail_write=true;
f.cmd("set radio2 off",false); assert(f.radio.p.enabled());
f.fs.fail_write=false;
f.fs.fail_rename=2; // publishing temp fails after moving old image to backup
f.cmd("set radio2 off",false); assert(f.radio.p.enabled());
Radio reboot; mesh::RadioProfileCLI restored; restored.begin(&f.fs,&reboot,&f.clock);
assert(reboot.p.enabled() && reboot.p.secondary.params.preamble==40);
f.cmd("set tempradio2 911.5,500,7,5,rxtx,1"); f.advance(2000);
Radio again; mesh::RadioProfileCLI restored_again; restored_again.begin(&f.fs,&again,&f.clock);
assert(!again.p.secondary_temporary && again.p.secondary.params.freq==910.5f);
}
{
Fixture f;
f.cmd("set tempradio2 910.5,500,7,5,rxtx,1"); f.advance(2000);
f.clock.epoch-=3600; f.advance(60000,false);
assert(!f.radio.p.enabled());
g_mock_millis=UINT32_MAX-1000; f.cli.loop();
f.cmd("set tempradio2 910.5,500,7,5,rxtx,1"); f.advance(2000,false);
assert(f.radio.p.secondary_temporary); f.advance(60000,false); assert(!f.radio.p.enabled());
}
{
Fixture f;
assert(f.cli.savePrimaryPreamble(48));
f.radio.p.primary.preamble=96; f.radio.p.primary_temporary=true;
strcpy(f.reply, "> 909.5,62.5,7,5");
f.cli.appendSavedPreamble(f.reply, sizeof(f.reply), 7, 62.5);
assert(strstr(f.reply, "preamble=48")); // saved getter cannot display temporary preamble
assert(!f.cli.savePrimaryPreamble(7));
f.cmd("set tempradio2 910.5,500,8,5,rx,1");
f.advance(65000); // servicing a late start cannot extend its lease
assert(!f.radio.p.enabled());
f.cmd("set tempradio2 910.5,500,8,5,rx,2");
char text[160];
snprintf(text, sizeof(text), "set tempradioat2 910.5,500,8,5,rx,%lu,%lu",
(unsigned long)(f.clock.epoch+60),(unsigned long)(f.clock.epoch+120));
f.cmd(text, false); // overlaps the pending session, before it starts
}
{
Fixture f;
f.cmd("set radio2 910.5,7.8,12,8,rx,65528",false); // driver airtime overflow
f.cmd("set radio2 910.5,500,8,5,rx,48");
assert(f.fs.rename("/radio_profiles", "/radio_profiles.bak"));
Radio radio; mesh::RadioProfileCLI restored;
restored.begin(&f.fs, &radio, &f.clock);
assert(radio.p.enabled()); // interrupted publication recovered
f.fs.files["/radio_profiles"][2]=99;
Radio corrupt; mesh::RadioProfileCLI invalid;
invalid.begin(&f.fs, &corrupt, &f.clock);
assert(!corrupt.p.enabled());
assert(invalid.handle("set radio2 off", f.reply));
assert(strstr(f.reply,"Error")); // newer/corrupt image is not overwritten
}
{
Fixture f;
char text[160];
snprintf(text,sizeof(text),"set tempradioat2 910.5,500,8,5,rxtx,%lu,%lu,48",
(unsigned long)(f.clock.epoch+60),(unsigned long)(f.clock.epoch+120));
f.cmd(text); assert(strstr(f.cmd("get tempradioat2 1"),"48"));
f.cmd(text,false); // overlap
f.advance(60000); assert(f.radio.p.secondary_temporary);
f.clock.epoch-=3600; f.advance(60000,false); assert(!f.radio.p.enabled());
snprintf(text,sizeof(text),"set radioat2 910.5,500,9,5,rx,%lu",(unsigned long)(f.clock.epoch+60));
f.cmd(text); f.advance(60000); assert(f.radio.p.secondary.mode==mesh::RadioProfileMode::Rx);
f.cmd("del radioat2 all"); f.cmd("del tempradioat2 all");
}
puts("RadioProfileCLI: command, persistence, failure, schedule and rollover tests passed");
}