Files
MeshCore/test/test_radio_profile_runtime_recommendation.py
T

152 lines
7.5 KiB
Python

"""Delay displayed advice without changing the scanner's measured-max policy."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
import test_radio_profile_scan as scan
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
class RuntimeRecommendationTest(unittest.TestCase):
def compile_run(self, source):
with tempfile.TemporaryDirectory() as folder:
cpp, binary = Path(folder) / "test.cpp", Path(folder) / "test"
(Path(folder) / "Arduino.h").write_text("#pragma once\n#include <cstdint>\n#include <cmath>\n")
cpp.write_text(source, encoding="ascii")
built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
"-Wall", "-Wextra", "-fsanitize=address,undefined", "-no-pie",
"-fno-omit-frame-pointer", "-I", folder, "-I", str(ROOT / "src"),
str(cpp), "-o", str(binary)], capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(ran.returncode, 0, ran.stderr)
def test_clock_boundaries_resets_and_unchanged_maximum_budget(self):
self.compile_run(r'''
#include <RadioProfiles.h>
#include <cassert>
#include <cstdint>
#include <initializer_list>
using P=mesh::RadioProfiles;
P pair() {
P p;p.primary={909.5f,500,0,7,5};p.secondary.params=p.primary;
p.secondary.params.freq=910.5f;p.secondary.mode=mesh::RadioProfileMode::Rx;
return p;
}
void train(P& p,uint32_t now,uint32_t a=100,uint32_t b=100) {
for(unsigned n=0;n<P::SwitchTestSamplesPerDirection;++n) {
p.sampleSwitch(0,1,a,now);p.sampleSwitch(1,0,b,now);
}
assert(p.switchTestReady() && !p.recommendationReady());
assert(p.switch_test_samples[0]==4 && p.switch_test_samples[1]==4);
}
void same(const P& a,const P& b) {
assert(a.switch_runtime_started==b.switch_runtime_started);
assert(a.switch_runtime_ready==b.switch_runtime_ready);
assert(a.switch_runtime_started_ms==b.switch_runtime_started_ms);
for(unsigned i=0;i<2;++i) {
assert(a.switch_test_samples[i]==b.switch_test_samples[i]);
assert(a.switch_test_max_us[i]==b.switch_test_max_us[i]);
}
}
int main() {
// Zero is a valid real start, and unsigned millisecond wrap is harmless.
for(uint32_t start:{0u,1234u,UINT32_MAX-60000u}) {
auto p=pair();assert(p.recommendationRemainingMs(start)==120000);
train(p,start);assert(p.switch_runtime_started && p.switch_runtime_started_ms==start);
assert(p.switchBudgetUs()==110 && p.preamble(0,32)==32);
const auto wire_a=p.preamble(0,32),wire_b=p.preamble(1,32);
const auto dwell_a=p.listenUs(0),dwell_b=p.listenUs(1);
p.sampleSwitch(0,1,100,start+119999u);
assert(!p.recommendationReady() && p.recommendationRemainingMs(start+119999u)==1);
p.sampleSwitch(1,0,100,start+120000u);
assert(p.recommendationReady() && p.recommendationRemainingMs(start+120000u)==0);
assert(p.switchBudgetUs()==110);
assert(p.preamble(0,32)==wire_a && p.preamble(1,32)==wire_b);
assert(p.listenUs(0)==dwell_a && p.listenUs(1)==dwell_b);
p.sampleSwitch(0,1,8428,start+120001u);
assert(p.switch_test_max_us[0]==8428 && p.switchBudgetUs()==9271);
assert(p.preamble(0,32)==168); // late maximum still affects real scheduling
assert(p.switch_test_samples[0]==4 && p.switch_test_samples[1]==4);
p.sampleSwitch(1,0,UINT32_MAX,start+120002u);
assert(p.switchBudgetUs()==UINT32_MAX); // existing overflow-safe guard
}
// The old provisional maximum adapts immediately BEFORE two minutes too.
{
auto p=pair();train(p,0);p.sampleSwitch(0,1,8428,1000);
assert(!p.recommendationReady() && p.switchBudgetUs()==9271);
assert(p.preamble(0,32)==168);
p.sampleSwitch(1,0,8428,120000);
assert(p.recommendationReady() && p.switchBudgetUs()==9271);
}
// Invalid work cannot start the clock; elapsed time without the four good
// samples in EACH direction is not recommendation readiness.
{
auto p=pair();p.sampleSwitch(0,0,100,120000);p.sampleSwitch(0,1,0,120000);
p.sampleSwitch(2,0,100,120000);p.sampleSwitch(0,2,100,120000);
assert(!p.switch_runtime_started && !p.recommendationReady());
p.sampleSwitch(0,1,100,0);p.sampleSwitch(0,1,100,120000);
assert(!p.switchTestReady() && !p.recommendationReady());
assert(p.recommendationRemainingMs(120000)==0);
for(unsigned n=0;n<4;++n) {p.sampleSwitch(0,1,100,120001);p.sampleSwitch(1,0,100,120001);}
assert(p.switchTestReady() && p.recommendationReady());
}
// Pair/lease changes reset the complete timer. No-op publication and a
// whole-object rollback preserve it, together with the unchanged maxima.
{
auto p=pair();train(p,0);p.sampleSwitch(0,1,100,120000);const auto old=p;
p.setPrimary(p.primary,p.primary_temporary);p.setSecondary(p.secondary,p.secondary_temporary);same(p,old);
auto changed=p.primary;changed.freq+=1;p.setPrimary(changed,false);
assert(!p.switchTestReady() && !p.recommendationReady() && !p.switch_runtime_started);
assert(p.switchBudgetUs()==0 && p.recommendationRemainingMs(120000)==120000);
p=old;same(p,old);p.setSecondary(p.secondary,!p.secondary_temporary);
assert(!p.recommendationReady() && !p.switch_runtime_started);
p=old;auto disabled=p.secondary;disabled.mode=mesh::RadioProfileMode::Off;
p.setSecondary(disabled,false);p.sampleSwitch(0,1,100,120000);
assert(!p.switch_runtime_started && !p.recommendationReady());
}
}
''')
def test_actual_scanner_keeps_bounded_startup_and_natural_visits(self):
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
signatures = [("uint8_t", "beginReconfigure"), ("void", "endReconfigure"),
("void", "serviceProfileScan")]
signatures += [("mesh::RadioParamApplyResult", name) for name in (
"tuneProfile", "prepareTransmitProfile", "tryRestoreCodingRate", "trySetParams", "trySetPrimaryParams")]
methods = "\n".join(method(source, f"{kind} RadioLibWrapper::{name}(") for kind, name in signatures)
methods += "\n" + method(source, "mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile, bool")
wrapper = (ROOT / "src/helpers/radiolib/CustomSX1262Wrapper.h").read_text()
methods += "\n" + method(wrapper, "void setProfileStandbyWarm(").replace(
"void setProfileStandbyWarm(bool enabled) override",
"void RadioLibWrapper::setProfileStandbyWarm(bool enabled)")
fixture = scan.HARNESS[:scan.HARNESS.index("int main()")].replace("@METHODS@", methods)
fixture += r'''
int main() {
RadioLibWrapper w;w.enable();
assert(w._profiles.switchTestReady() && !w._profiles.recommendationReady());
const auto applies=w.applies;
for(unsigned i=0;i<16;++i) w.serviceProfileScan();
assert(w.applies==applies); // not repeated startup bursts while advice waits
const auto started=w._profiles.switch_runtime_started_ms;
elapsed_us=(uint64_t(started)+119999)*1000;
w.serviceProfileScan();
assert(w._profiles.recommendationReady() && w.applies==applies+1);
assert(w._profiles.switchBudgetUs()==1320);
const auto old=w._profiles;
w.packet=true;auto requested=w._profiles.primary;requested.preamble=64;
assert(w.trySetPrimaryParams(requested,true)==mesh::RadioParamApplyResult::BUSY);
assert(w._profiles.recommendationReady() && w._profiles.switch_runtime_started_ms==old.switch_runtime_started_ms);
}
'''
self.compile_run(fixture)
if __name__ == "__main__":
unittest.main()