Files
HaloKeymind/test/test_repeater_radio_timing_integration.py
T

623 lines
27 KiB
Python

#!/usr/bin/env python3
"""Execute production repeater scheduling, watchdog and advert service methods."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def method(text, signature):
start = text.index(signature)
end = text.index("{", start) + 1
depth = 1
while depth:
depth += (text[end] == "{") - (text[end] == "}")
end += 1
return text[start:end]
HARNESS = r'''
#include <cassert>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <helpers/RepeaterRadioTiming.h>
#include <helpers/TempRadioLeaseDeadline.h>
#define MESH_DEBUG_PRINTLN(...) ((void)0)
#define MESH_ENABLE_TELEMETRY_HISTORY 0
#define MAX_SCHEDULED_RADIO_SETTINGS 8
#define MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE 4
#define SCHEDULED_RADIO_CLOCK_CHECKPOINT_SECS 60
#define RADIO_APPLY_RETRY_INTERVAL_MILLIS 100
static uint32_t now_ms = 1;
uint32_t millis() { return now_ms; }
namespace mesh {
struct Packet {};
int clampLoRaTxPower(int power, float) { return power; }
namespace lr2021 {
bool storedSideDetectorCount(const uint8_t*, uint8_t& count) { count = 0; return true; }
bool validateSideDetectorSFs(const uint8_t*, uint8_t, uint8_t, float) { return true; }
}
}
struct CommonCLI {
template <typename T> static void recalculateRxPowerSavingFromLevel(T*) {}
};
uint32_t nextRadioApplyRetryDelay(uint8_t& failures) { ++failures; return 100; }
struct RadioDriver {
bool gain_supported = true, gain_success = true, power_success = true;
bool side_detector_success = true;
unsigned gain_calls = 0, power_calls = 0;
bool active_gain = false;
int active_power = 0;
bool supportsRxBoostedGainMode() const { return gain_supported; }
bool setRxBoostedGainMode(bool gain) {
++gain_calls;
if (!gain_success) return false;
active_gain = gain;
return true;
}
bool setTxPower(int power) {
++power_calls;
if (!power_success) return false;
active_power = power;
return true;
}
bool configSideDetectors(const uint8_t*, uint8_t, float) { return side_detector_success; }
} radio_driver;
struct RTC {
uint32_t now = 100000;
uint32_t getCurrentTime() const { return now; }
};
struct RadioPrefs {
bool rx_watchdog_enabled = true, rx_boosted_gain = true;
uint8_t advert_interval = 60, flood_advert_interval = 24, path_hash_mode = 0;
float freq = 909.5f, bw = 62.5f;
uint8_t sf = 7, cr = 5;
uint8_t extra_sf[4] = {};
int tx_power_dbm = 20;
};
struct Board { int reboots = 0; void reboot() { ++reboots; } };
struct CLI {
Board board; Board* getBoard() { return &board; }
CLI& radioProfiles() { return *this; }
RadioPrefs* prefs = nullptr;
int* saves = nullptr;
bool save_success = true;
unsigned save_attempts = 0;
unsigned fail_on_save = 0;
uint16_t saved_preamble = 0;
bool savePrimaryRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, uint16_t preamble) {
++save_attempts;
if (!save_success || save_attempts == fail_on_save) return false;
++*saves;
prefs->freq = freq; prefs->bw = bw; prefs->sf = sf; prefs->cr = cr;
saved_preamble = preamble;
return true;
}
uint16_t primaryPreamble() const { return saved_preamble; }
bool hasReplyMutation() const { return false; }
bool finishReplyMutation(bool) { return true; }
};
struct Radio { uint32_t last_rx = 0; uint32_t getLastRecvMillis() { return last_rx; } };
struct Barrier { bool held = false; bool waiting() const { return held; } void clear() { held = false; } };
struct ScheduledRadioSetting {
bool active = false, temporary = false, started = false;
float freq = 0, bw = 0;
uint8_t sf = 0, cr = 0;
uint16_t preamble = 0;
uint32_t start_time = 0, end_time = 0;
uint64_t hard_end_uptime_millis = 0;
};
class MyMesh {
public:
RadioPrefs _prefs;
CLI _cli;
Radio radio; Radio* _radio = &radio;
uint32_t last_meshcore_rx = 0;
uint32_t getLastMeshCoreRecvMillis() const { return last_meshcore_rx; }
RTC rtc;
Barrier temp_radio_reply_barrier;
uint32_t primary_radio_mutation_generation = 0, radio_reply_deadline = 0;
bool primary_radio_mutation_starts_temp = false;
bool radio_reply_secondary = false;
mesh::RxInactivityWatchdog rx_inactivity_watchdog;
mesh::RepeaterRadioTiming radio_timing;
ScheduledRadioSetting scheduled_radio_settings[MAX_SCHEDULED_RADIO_SETTINGS];
uint64_t uptime_millis = 1;
uint32_t last_millis = 1;
uint32_t next_local_advert = 0, next_flood_advert = 0;
bool saved_radio_apply_pending = false, temp_radio_handoff_pending = false;
bool temp_radio_applied = false, scheduled_temp_radio_started = false;
uint32_t next_scheduled_radio_time = 0, next_scheduled_radio_check_at = 0;
uint32_t scheduled_temp_radio_end_time = 0, scheduled_temp_radio_end_check_at = 0;
bool scheduled_temp_radio_end_check_final = false;
uint32_t scheduled_radio_retry_at = 0;
uint32_t scheduled_radio_save_retry_at = 0;
uint8_t scheduled_radio_retry_failures = 0;
bool pending_self_advert = false, pending_self_advert_flood = false;
uint32_t pending_self_advert_delay = 0;
bool outbound = false, apply_success = true, restore_success = true;
int saves = 0, applies = 0, restores = 0, local_adverts = 0, flood_adverts = 0;
int default_scope = 0;
mesh::Packet packet;
MyMesh() {
_cli.prefs = &_prefs; _cli.saves = &saves;
radio_driver = {}; now_ms = 1; updateAdvertTimer(); updateFloodAdvertTimer();
}
RTC* getRTCClock() const { return const_cast<RTC*>(&rtc); }
uint32_t futureMillis(uint32_t delay) const { return now_ms + delay; }
bool millisHasNowPassed(uint32_t deadline) const { return (int32_t)(now_ms - deadline) >= 0; }
bool hasOutbound() const { return outbound; }
bool hasStartedScheduledTempRadio() const { return scheduled_temp_radio_started; }
bool applyRadioParams(float, float, uint8_t, uint8_t, uint16_t = 0, bool temporary = false) {
if (temporary) { ++applies; return apply_success; }
++restores; return restore_success;
}
bool applySavedRadioParams();
bool isValidScheduledRadioParams(float, float, uint8_t, uint8_t) { return true; }
void savePrefs() { ++saves; }
int getScheduledRadioSettingIndex(bool, int slot) const { return slot + 1; }
void formatScheduledRadioDuration(char* dest, size_t size, uint32_t when) const {
snprintf(dest, size, "%lus", (unsigned long)(when - rtc.now));
}
void appendTempRadioTimingNote(char* reply, size_t size, uint32_t seconds) const {
mesh::RepeaterRadioTiming::appendTempWatchdogNote(reply, size, seconds);
}
void advance(uint32_t seconds) {
now_ms += seconds * 1000UL;
uptime_millis += (uint64_t)seconds * 1000UL;
last_millis = now_ms;
rtc.now += seconds;
}
mesh::Packet* createSelfAdvert() { return &packet; }
void sendFloodScoped(int, mesh::Packet*, uint32_t, int) { ++flood_adverts; }
void sendZeroHop(mesh::Packet*) { ++local_adverts; }
void sendSelfAdvertisementNow(uint32_t, bool) {}
void checkBatteryAlert() {}
void expireRecentRepeatersIfDue() {}
void checkRxInactivityWatchdog();
void setTempRadioTiming(uint32_t);
void updateAdvertTimer();
void updateFloodAdvertTimer();
void queueSavedRadioApply();
void refreshScheduledRadioState();
void processScheduledRadioSettings();
void applyTempRadioParams(float, float, uint8_t, uint8_t, int, uint16_t = 0);
bool scheduleNormalRadio();
void clearScheduledRadioSetting(int, bool);
int findFreeScheduledRadioSlot() const;
int countScheduledRadioSettings(bool) const;
bool scheduledRadioConflicts(bool, uint32_t, uint32_t) const;
void addScheduledRadioParams(bool, float, float, uint8_t, uint8_t, uint32_t, uint32_t, char*, uint16_t = 0);
void servicePostMeshLoop();
};
@METHODS@
static constexpr uint32_t HOUR = 3600;
static void startTemp(MyMesh& m, int minutes) {
m.applyTempRadioParams(910, 250, 5, 5, minutes);
m.advance(2);
m.servicePostMeshLoop();
assert(m.temp_radio_applied);
}
int main() {
for (unsigned failure = 0; failure <
#ifdef USE_LR2021
4
#else
3
#endif
; ++failure) {
// A failure at any independent hardware step retains the saved recovery
// work and its backoff until the complete configuration succeeds.
MyMesh m;
if (failure == 0) radio_driver.gain_success = false;
if (failure == 1) m.restore_success = false;
if (failure == 2) radio_driver.power_success = false;
if (failure == 3) radio_driver.side_detector_success = false;
m.queueSavedRadioApply();
m.processScheduledRadioSettings();
assert(m.saved_radio_apply_pending && m.scheduled_radio_retry_at);
const unsigned gain_calls = radio_driver.gain_calls;
const unsigned power_calls = radio_driver.power_calls;
const unsigned restores = m.restores;
for (unsigned i = 0; i < 10; ++i) m.processScheduledRadioSettings();
assert(radio_driver.gain_calls == gain_calls);
assert(radio_driver.power_calls == power_calls && m.restores == (int)restores);
radio_driver.gain_success = radio_driver.power_success = true;
radio_driver.side_detector_success = true;
m.restore_success = true;
m.advance(1); m.processScheduledRadioSettings();
assert(!m.saved_radio_apply_pending && !m.scheduled_radio_retry_at);
assert(radio_driver.active_gain == m._prefs.rx_boosted_gain);
assert(radio_driver.active_power == m._prefs.tx_power_dbm);
}
{ // A later failed due entry must not undo an earlier durable change.
MyMesh m;
char reply[160];
m.addScheduledRadioParams(false, 911, 250, 5, 5, m.rtc.now + 2, 0, reply, 48);
m.addScheduledRadioParams(false, 912, 125, 6, 6, m.rtc.now + 3, 0, reply, 80);
m._cli.fail_on_save = 2;
m.advance(3); m.servicePostMeshLoop();
assert(m._cli.save_attempts == 2 && m.saves == 1);
assert(m.countScheduledRadioSettings(false) == 1);
assert(m._prefs.freq == 911 && m._cli.saved_preamble == 48);
assert(!m.saved_radio_apply_pending);
m.advance(60); m.servicePostMeshLoop();
assert(m._cli.save_attempts == 3 && m.saves == 2);
assert(m.countScheduledRadioSettings(false) == 0);
assert(m._prefs.freq == 912 && m._cli.saved_preamble == 80);
assert(!m.saved_radio_apply_pending);
}
{ // Radios without boosted gain must not become stuck in recovery.
MyMesh m;
radio_driver.gain_supported = false;
radio_driver.gain_success = false;
m.queueSavedRadioApply();
m.processScheduledRadioSettings();
assert(!m.saved_radio_apply_pending && radio_driver.gain_calls == 0);
assert(radio_driver.active_power == m._prefs.tx_power_dbm);
}
{ // Boot uses the same complete apply result to arm recovery.
MyMesh m;
radio_driver.power_success = false;
m.saved_radio_apply_pending = !m.applySavedRadioParams();
assert(m.saved_radio_apply_pending);
radio_driver.power_success = true;
m.processScheduledRadioSettings();
assert(!m.saved_radio_apply_pending);
}
{ // Expiring one scheduled lease must retain a later temporary entry.
MyMesh m;
char reply[160];
m.addScheduledRadioParams(true, 910, 250, 5, 5, m.rtc.now + 2, m.rtc.now + 62, reply);
m.addScheduledRadioParams(true, 911, 250, 5, 5, m.rtc.now + 122, m.rtc.now + 182, reply);
assert(m.countScheduledRadioSettings(true) == 2);
m.advance(2); m.servicePostMeshLoop(); assert(m.temp_radio_applied);
m.advance(60); m.servicePostMeshLoop();
assert(m.countScheduledRadioSettings(true) == 1 && !m.temp_radio_applied);
m.advance(60); m.servicePostMeshLoop(); assert(m.temp_radio_applied);
const auto before = m.primary_radio_mutation_generation;
m.queueSavedRadioApply();
assert(m.primary_radio_mutation_generation != before);
}
for (bool rollover : {false, true}) {
MyMesh m;
if (rollover) { now_ms = UINT32_MAX - 30000; m.last_millis = now_ms; }
char reply[160];
m.addScheduledRadioParams(false, 912.5, 250, 5, 5, m.rtc.now + 2, 0, reply, 48);
assert(!strncmp(reply, "OK", 2));
m._cli.save_success = false;
m.advance(2); m.servicePostMeshLoop();
assert(m._cli.save_attempts == 1 && m.saves == 0);
assert(m.next_scheduled_radio_check_at == m.scheduled_radio_save_retry_at);
assert(m._prefs.freq == 909.5f && m._cli.saved_preamble == 0);
assert(m.countScheduledRadioSettings(false) == 1);
for (unsigned i = 0; i < 10; ++i) m.servicePostMeshLoop();
assert(m._cli.save_attempts == 1); // storage backoff survives the scheduler's cleanup
m._cli.save_success = true;
m.advance(59); m.servicePostMeshLoop();
assert(m._cli.save_attempts == 1);
m.advance(1); m.servicePostMeshLoop();
assert(m._cli.save_attempts == 2 && m.saves == 1);
assert(m._prefs.freq == 912.5f && !m.saved_radio_apply_pending);
assert(m._cli.saved_preamble == 48 && m.countScheduledRadioSettings(false) == 0);
}
{ // Backing off a failed permanent save must not delay temporary start/end.
MyMesh m;
char reply[160];
m.addScheduledRadioParams(false, 912.5, 250, 5, 5, m.rtc.now + 2, 0, reply, 48);
m.addScheduledRadioParams(true, 911.5, 250, 5, 5, m.rtc.now + 30, m.rtc.now + 90, reply, 80);
m._cli.save_success = false;
m.advance(2); m.servicePostMeshLoop();
assert(m._cli.save_attempts == 1);
m.advance(28); m.servicePostMeshLoop();
assert(m.temp_radio_applied && m.radio_timing.isTemporary());
assert(m._cli.save_attempts == 1);
m.advance(60); m.servicePostMeshLoop();
assert(!m.temp_radio_applied && !m.radio_timing.isTemporary());
assert(m._prefs.freq == 909.5f);
}
{ // Pending schedules do not change current timings, even on an apply failure.
MyMesh m;
const uint32_t normal_local = m.next_local_advert;
char reply[160];
m.addScheduledRadioParams(true, 910, 250, 5, 5, m.rtc.now + 60,
m.rtc.now + 60 + 24 * HOUR, reply);
assert(strstr(reply, "1440 mins (1d0h0m); rx.watchdog=12hours"));
assert(!m.radio_timing.isTemporary());
assert(m.next_local_advert == normal_local);
m.apply_success = false;
m.advance(60); m.servicePostMeshLoop();
assert(!m.radio_timing.isTemporary());
m.apply_success = true;
m.advance(1); m.servicePostMeshLoop();
assert(m.radio_timing.tempDuration() == 24 * HOUR);
assert(m.next_local_advert == now_ms + HOUR * 1000);
assert(m.next_flood_advert == now_ms + 3 * HOUR * 1000);
assert(m.saves == 0);
assert(m._prefs.advert_interval == 60 && m._prefs.flood_advert_interval == 24);
}
{ // The exact-reply barrier must still delay activation and its overrides.
MyMesh m;
m.applyTempRadioParams(910, 250, 5, 5, 1440);
m.temp_radio_reply_barrier.held = true;
m.advance(3); m.servicePostMeshLoop();
assert(!m.radio_timing.isTemporary());
m.temp_radio_reply_barrier.clear();
m.advance(1); m.servicePostMeshLoop();
assert(m.radio_timing.tempDuration() == 24 * HOUR);
}
{ // Both adverts are sent at hour three; hourly local adverts cannot vanish.
MyMesh m; startTemp(m, 1440);
for (int hour = 1; hour <= 6; ++hour) { m.advance(HOUR); m.servicePostMeshLoop(); }
assert(m.local_adverts == 6 && m.flood_adverts == 2);
assert(m._cli.board.reboots == 0 && m.saves == 0);
}
{ // A short session keeps the normal flood setting, including disabled.
MyMesh m;
m._prefs.flood_advert_interval = 0;
m._prefs.advert_interval = 0;
startTemp(m, 180);
assert(m.next_flood_advert == 0);
assert(m.radio_timing.watchdogMillis(true) == 0);
m.advance(HOUR); m.servicePostMeshLoop();
assert(m.local_adverts == 1 && m.flood_adverts == 0);
m.advance(2 * HOUR); m.servicePostMeshLoop();
assert(!m.radio_timing.isTemporary());
assert(m.next_local_advert == 0 && m.next_flood_advert == 0);
assert(m.radio_timing.watchdogMillis(true) == 24 * HOUR * 1000);
}
{ // At exactly 12 hours, expiry wins over the temporary watchdog deadline.
MyMesh m; startTemp(m, 720);
m.advance(12 * HOUR); m.servicePostMeshLoop();
assert(!m.radio_timing.isTemporary());
assert(m._cli.board.reboots == 0);
m.advance(24 * HOUR); m.servicePostMeshLoop();
assert(m._cli.board.reboots == 1);
}
{ // A longer session really does reboot after a 12-hour no-RX window.
MyMesh m; startTemp(m, 1440);
m.advance(12 * HOUR); m.servicePostMeshLoop();
assert(m._cli.board.reboots == 1);
}
{ // Replacing an active session retains old policy until the new tuple applies.
MyMesh m; startTemp(m, 1440);
m.applyTempRadioParams(911, 250, 5, 5, 60);
assert(m.radio_timing.tempDuration() == 24 * HOUR);
m.advance(2); m.servicePostMeshLoop();
assert(m.radio_timing.tempDuration() == HOUR);
assert(m.radio_timing.watchdogMillis(true) == 0);
}
{ // Failed restoration must not leave a short session's watchdog disabled.
MyMesh m; startTemp(m, 60);
m.restore_success = false;
m.advance(HOUR); m.servicePostMeshLoop();
assert(m.temp_radio_applied); // hardware restoration is still pending
assert(!m.radio_timing.isTemporary());
assert(m.radio_timing.watchdogMillis(true) == 24 * HOUR * 1000);
m.restore_success = true;
m.advance(1); m.servicePostMeshLoop();
assert(!m.temp_radio_applied && m.saves == 0);
}
{ // Manual cancellation restores saved timings without saving temp overrides.
MyMesh m; startTemp(m, 1440);
assert(m.scheduleNormalRadio());
assert(!m.radio_timing.isTemporary());
m.servicePostMeshLoop();
assert(!m.temp_radio_applied && m.saves == 0);
assert(m.next_local_advert == now_ms + 2 * HOUR * 1000);
assert(m.next_flood_advert == now_ms + 24 * HOUR * 1000);
}
{ // Deleting an active scheduled entry has the same restoration behavior.
MyMesh m; startTemp(m, 1440);
m.clearScheduledRadioSetting(0, true);
assert(!m.radio_timing.isTemporary());
m.servicePostMeshLoop();
assert(!m.temp_radio_applied && m.saves == 0);
}
{ // Adjacent scheduled windows adopt the new duration at the boundary.
MyMesh m;
char reply[160];
uint32_t start = m.rtc.now + 2;
m.addScheduledRadioParams(true, 910, 250, 5, 5, start, start + HOUR, reply);
m.addScheduledRadioParams(true, 911, 250, 5, 5, start + HOUR, start + 25 * HOUR, reply);
m.advance(2); m.servicePostMeshLoop();
assert(m.radio_timing.tempDuration() == HOUR);
m.advance(HOUR); m.servicePostMeshLoop();
assert(m.radio_timing.tempDuration() == 24 * HOUR);
assert(m._cli.board.reboots == 0 && m.saves == 0);
}
{ // Raw driver receptions must not keep the whole-board watchdog alive.
MyMesh m; m.servicePostMeshLoop();
m.advance(23 * HOUR);
m.radio.last_rx = now_ms;
m.servicePostMeshLoop();
m.advance(HOUR);
m.radio.last_rx = now_ms;
m.servicePostMeshLoop();
assert(m._cli.board.reboots == 1);
}
{ // A parsed MeshCore reception moves the actual repeater reboot deadline.
MyMesh m; m.servicePostMeshLoop();
m.advance(23 * HOUR);
m.last_meshcore_rx = now_ms;
m.servicePostMeshLoop();
m.advance(HOUR); m.servicePostMeshLoop();
assert(m._cli.board.reboots == 0);
m.advance(23 * HOUR); m.servicePostMeshLoop();
assert(m._cli.board.reboots == 1);
}
puts("Production scheduler, storage-backoff, watchdog and advert scenarios passed");
}
'''
CLI_HARNESS = r'''
#include <cassert>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <helpers/RepeaterRadioTiming.h>
#include <helpers/CLICommandUtils.h>
#include <helpers/RadioProfileCommandUtils.h>
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 acceptsPrimary(float, float, uint8_t, uint8_t, uint16_t) { return true; }
void appendPrimaryChirpWarning(char*, size_t, uint8_t, float, uint16_t) const {}
};
class Utils { public: static int parseTextParts(char*, const char*[], int, char separator=','); };
@PARTS@
}
@PARSERS@
struct RTC { uint32_t getCurrentTime() const { return 1789160000UL; } } rtc;
RTC* getRTCClock() { return &rtc; }
struct Callbacks {
int calls = 0;
uint32_t duration = 0;
void applyTempRadioParams(float, float, uint8_t, uint8_t, int minutes, uint16_t) {
++calls; duration = (uint32_t)minutes * 60;
}
void appendTempRadioTimingNote(char* reply, size_t size, uint32_t seconds) {
mesh::RepeaterRadioTiming::appendTempWatchdogNote(reply, size, seconds);
}
} callbacks;
void appendRxPowerSavingAdjustmentNote(char*, const void*, uint8_t, float) {}
void handle(const char* command, char* reply) {
mesh::RadioProfileCLI _radio_profiles;
char tmp[160];
Callbacks* _callbacks = &callbacks;
const void* _prefs = nullptr;
if (false) {
@BRANCH@
}
}
int main() {
struct Reply { char text[160]; char guard = '!'; } reply;
handle("tempradio 910,250,5,5,1440", reply.text);
assert(!strcmp(reply.text, "OK - temp params for 1440 mins (1d0h0m); rx.watchdog=12hours"));
assert(!strncmp(reply.text, "OK - temp params for ", 21)); // exact reply-barrier marker
handle("tempradio 910,250,5,5,90", reply.text);
assert(!strcmp(reply.text, "OK - temp params for 90 mins (0d1h30m); rx.watchdog=off (temp<12hours)"));
assert(callbacks.calls == 2 && callbacks.duration == 5400);
for (const char* minutes : {"0", "-1", "12oops", "1.5", "4294967295", "4294967296"}) {
char command[100];
snprintf(command, sizeof(command), "tempradio 910,250,5,5,%s", minutes);
handle(command, reply.text);
assert(!strcmp(reply.text, "Error, invalid params"));
assert(callbacks.calls == 2);
}
assert(reply.guard == '!');
puts("production TempRadio replies and duration validation passed");
}
'''
class RepeaterRadioTimingIntegrationTest(unittest.TestCase):
def compile_and_run(self, source):
with tempfile.TemporaryDirectory(prefix="meshcore-temp-timing-") as temp:
cpp = Path(temp) / "test.cpp"
exe = Path(temp) / ("test.exe" if os.name == "nt" else "test")
cpp.write_text(source, encoding="utf-8")
subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17", "-I", str(ROOT / "src"),
str(cpp), "-o", str(exe)], check=True)
subprocess.run([str(exe)], check=True)
def test_production_scheduler_and_services(self):
source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="utf-8")
signatures = [
"void MyMesh::checkRxInactivityWatchdog()",
"void MyMesh::setTempRadioTiming(",
"void MyMesh::updateAdvertTimer()",
"void MyMesh::updateFloodAdvertTimer()",
"void MyMesh::queueSavedRadioApply()",
"bool MyMesh::applySavedRadioParams()",
"void MyMesh::refreshScheduledRadioState()",
"void MyMesh::processScheduledRadioSettings()",
"void MyMesh::applyTempRadioParams(",
"bool MyMesh::scheduleNormalRadio()",
"void MyMesh::clearScheduledRadioSetting(",
"int MyMesh::findFreeScheduledRadioSlot() const",
"int MyMesh::countScheduledRadioSettings(",
"bool MyMesh::scheduledRadioConflicts(",
"void MyMesh::addScheduledRadioParams(",
]
methods = "\n".join(method(source, signature) for signature in signatures)
post = method(source, "void __attribute__((noinline)) MyMesh::servicePostMeshLoop()")
# The remaining service tail handles unrelated MQTT/OTA/peripheral work.
post = post[:post.index("#if defined(WITH_MQTT_BRIDGE) && defined(OTA_MANIFEST_BASE)")] + "}\n"
methods += "\n" + post
for lr2021 in (False, True):
with self.subTest(lr2021=lr2021):
defines = "#define USE_LR2021 1\n" if lr2021 else ""
self.compile_and_run(defines + HARNESS.replace("@METHODS@", methods))
def test_scheduled_radio_relative_and_absolute_times(self):
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="utf-8")
parsers = "\n".join(method(source, signature) for signature in [
"static bool looksUnsignedInteger(", "static const char* skipSpacesConst(",
"static bool parseUint32Strict(", "static int countSeparatedParts(",
"static bool parseScheduledRadioArgs(",
])
utils = (ROOT / "src/Utils.cpp").read_text(encoding="utf-8")
parts = method(utils, "int Utils::parseTextParts(")
self.compile_and_run(r'''
#include <cassert>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <helpers/CLICommandUtils.h>
struct StrHelper {
static void strncpy(char* dst, const char* src, size_t n) { snprintf(dst, n, "%s", src); }
};
namespace mesh {
struct Utils { static int parseTextParts(char*, const char*[], int, char); };
@PARTS@
}
@PARSERS@
int main() {
const uint32_t now = 1700000000;
float freq, bw; uint8_t sf, cr; uint32_t start, end;
assert(parseScheduledRadioArgs("910.5,500,8,5,+5,+15", true, freq,bw,sf,cr,start,end,now));
assert(start == now + 300 && end == now + 900);
assert(parseScheduledRadioArgs("910.5,500,8,5,1700000300,1700000900", true, freq,bw,sf,cr,start,end,now));
assert(start == now + 300 && end == now + 900);
assert(parseScheduledRadioArgs("910.5,500,8,5,1700000300,+15", true, freq,bw,sf,cr,start,end,now));
assert(start == now + 300 && end == now + 900);
assert(parseScheduledRadioArgs("910.5,500,8,5,+5", false, freq,bw,sf,cr,start,end,now));
assert(start == now + 300 && end == 0);
for (const char* invalid : {"+0", "+", "++1", "+ 1", "-1", "+1.5", "+1x", "+4294967295", "4294967296"}) {
start = 42;
assert(!mesh::cli::parseRadioScheduleTime(invalid, now, start));
assert(start == 42);
}
assert(mesh::cli::parseRadioScheduleTime(" +1 ", now, start) && start == now + 60);
assert(mesh::cli::parseRadioScheduleTime("+1", UINT32_MAX - 60, start) && start == UINT32_MAX);
assert(!mesh::cli::parseRadioScheduleTime("+1", UINT32_MAX - 59, start));
}
'''.replace("@PARTS@", parts).replace("@PARSERS@", parsers))
def test_production_cli_replies_and_duration_validation(self):
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="utf-8")
parsers = "\n".join(method(source, signature) for signature in [
"static bool looksUnsignedInteger(", "static const char* skipSpacesConst(",
"static bool parseUint32Strict(", "static bool bwMatches(",
"static bool isValidLoRaBandwidth(",
])
start = source.index(' } else if (memcmp(command, "tempradio ", 10) == 0)')
end = source.index(' } else if (memcmp(command, "password ", 9) == 0)', start)
utils = (ROOT / "src/Utils.cpp").read_text(encoding="utf-8")
parts = method(utils, "int Utils::parseTextParts(")
self.compile_and_run(CLI_HARNESS.replace("@PARSERS@", parsers)
.replace("@BRANCH@", source[start:end])
.replace("@PARTS@", parts))
if __name__ == "__main__":
unittest.main()