Files
MeshCore/test/test_morse_raw_rssi.py
T

145 lines
7.8 KiB
Python

"""Bounded production-RF ESP32 raw RSSI diagnostics, without hardware or PIO."""
from pathlib import Path
import unittest
from test_morse_tx import compile_run, portable_radio_header
from test_morse_hil_production import GOOD_FLAGS
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
RAW_FLAGS = GOOD_FLAGS + ["-DESP32_PLATFORM", "-DMESH_MORSE_HIL_RAW_RSSI=1"]
class MorseRawRssiTests(unittest.TestCase):
def test_every_sample_including_arming_candidates_rollover_half_db_and_frozen_overflow(self):
compile_run(self, r'''
#include <helpers/MorseRx.h>
#include <cassert>
#include <memory>
#include <cstdio>
#include <initializer_list>
int main() {
using Trace = mesh::MorseRssiTrace;
static_assert(Trace::Capacity == 30001, "60s shipping-cadence prefix");
static_assert(sizeof(Trace) < 91000, "raw capture must remain admitted and bounded");
auto trace = std::unique_ptr<Trace>(new Trace);
mesh::MorseRx rx; rx.setTrace(trace.get());
const uint32_t start = 0xfffffff0U;
assert(rx.start(25, 300000, start) == mesh::MorseRxResult::Started);
for (uint32_t elapsed = 0; elapsed <= 60000; elapsed += 2) {
const float rssi = elapsed >= 600 && elapsed < 630 ? -60.5f : (elapsed % 4 ? -110.5f : -110.0f);
rx.sample(rssi, start + elapsed);
assert(trace->length() == elapsed / 2 + 1);
}
mesh::MorseRxDiagnostics stats; rx.diagnostics(stats);
assert(stats.samples == trace->total());
for (size_t index : {size_t(0),size_t(1),size_t(249),size_t(255),size_t(256),size_t(300),size_t(314),size_t(10000),size_t(30000)}) {
mesh::MorseRssiTraceEntry entry{}; assert(trace->read(index, entry));
const uint32_t elapsed = uint32_t(index * 2);
const float expected = elapsed >= 600 && elapsed < 630 ? -60.5f : (elapsed % 4 ? -110.5f : -110.0f);
assert(entry.millis == elapsed && entry.rssi_dbm == expected);
}
rx.sample(-100, start + 60002);
assert(trace->truncated() && trace->stopReason() == Trace::Window && trace->total() == 30002);
mesh::MorseRssiTraceEntry retained; assert(trace->read(0, retained) && retained.millis == 0 && retained.rssi_dbm == -110);
trace->clear(); assert(!trace->length() && !trace->total() && !trace->truncated());
trace->sample(-127.5f, 0, 0); trace->sample(0, 1, 0);
assert(trace->read(0, retained) && retained.rssi_dbm == -127.5f);
assert(trace->read(1, retained) && retained.rssi_dbm == 0);
trace->sample(-100.25f, 2, 0); assert(trace->stopReason() == Trace::Value && trace->length() == 2);
trace->clear(); trace->sample(-100, 10, 0); trace->sample(-100, 9, 0);
assert(trace->stopReason() == Trace::Gap && trace->length() == 1);
trace->clear();
for (size_t index = 0; index <= Trace::Capacity; ++index) trace->sample(-100, 0, 0);
assert(trace->stopReason() == Trace::Full && trace->length() == Trace::Capacity);
printf("raw trace bytes=%zu; all samples retained or explicitly truncated\n", sizeof(Trace));
}
''', flags=RAW_FLAGS)
def test_raw_pages_preserve_half_db_timestamps_five_digit_offsets_and_never_stream_during_rx(self):
first = (ROOT / "test/fixtures/morse_cli.cpp").read_text(encoding="ascii").split("int main() {", 1)[0]
code = first + r'''
#include <memory>
struct RawRadio : TestRadio {
std::unique_ptr<mesh::MorseRssiTrace> trace{new mesh::MorseRssiTrace};
size_t morseTraceLength() const override { return trace->length(); }
uint32_t morseTraceTotal() const override { return trace->total(); }
bool morseTraceTruncated() const override { return trace->truncated(); }
bool readMorseTrace(size_t offset, mesh::MorseRssiTraceEntry& out) const override { return trace->read(offset,out); }
};
int main() {
RawRadio radio; char reply[158];
for (uint32_t index = 0; index < 15001; ++index) radio.trace->sample(-100.5f,index*2,0);
radio.rx_active = true;
mesh::formatMorseTracePage(&radio,"0",reply,157);
assert(strstr(reply,"stop Morse RX"));
radio.rx_active = false; memset(reply,'~',sizeof(reply));
mesh::formatMorseTracePage(&radio,"10000",reply,157);
assert(reply[157] == '~' && strlen(reply) < 157);
assert(strstr(reply,"> morse.hil.raw-rssi-v1 10000/15001"));
assert(strstr(reply,"q=-0.5 20000,201;20002,201;"));
assert(!strstr(reply,"n=-----"));
mesh::formatMorseTracePage(&radio,"15000",reply,157);
assert(strstr(reply,"n=END ") && strstr(reply,"30000,201;"));
mesh::formatMorseTracePage(&radio,"15002",reply,157); assert(strstr(reply,"Error:"));
}
'''
fs = (ROOT / "test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h").read_text(encoding="ascii")
compile_run(self, code, flags=RAW_FLAGS + ["-Wno-unused-parameter"], headers={
"Dispatcher.h": portable_radio_header(), "helpers/IdentityStore.h": fs})
def test_raw_mode_requires_esp32_hil_and_all_shipping_rf_assertions(self):
fixture = "#include <helpers/MorseRx.h>\nint main() {}\n"
for missing in ("-DESP32_PLATFORM", "-DMESH_MORSE_HIL_PRODUCTION_RF=1", "-DMESH_MORSE_HIL=1"):
flags = [flag for flag in RAW_FLAGS if flag != missing]
with self.subTest(missing=missing), self.assertRaisesRegex(AssertionError, "Raw Morse RSSI requires|Production-equivalent HIL requires"):
compile_run(self, fixture, flags=flags)
for flag in ("-DMESH_MORSE_HIL_SAMPLE_INTERVAL_MS=1", "-DMESH_MORSE_HIL_PA_RAMP_US=800"):
name = flag.split("=",1)[0] + "="
flags = [item for item in RAW_FLAGS if not item.startswith(name)] + [flag]
with self.subTest(changed_rf=flag), self.assertRaisesRegex(AssertionError, "Production-equivalent HIL requires"):
compile_run(self, fixture, flags=flags)
for window in (29999,60001):
with self.subTest(window=window), self.assertRaisesRegex(AssertionError,"Raw Morse trace window must be"):
compile_run(self, fixture, flags=RAW_FLAGS+[f"-DMESH_MORSE_HIL_RAW_WINDOW_MS={window}"])
def test_shipping_and_default_hil_keep_their_existing_trace_layouts(self):
compile_run(self, "#include <helpers/MorseRx.h>\nint main(){static_assert(sizeof(mesh::MorseRx)==4172,\"shipping layout unchanged\");}\n")
compile_run(self, "#include <helpers/MorseRx.h>\nint main(){static_assert(sizeof(mesh::MorseRssiTrace)<2100,\"default HIL stays sparse\");}\n", flags=("-DMESH_MORSE_HIL=1",))
with self.assertRaisesRegex(AssertionError,"MorseRawRssiTrace"):
compile_run(self,"#include <helpers/MorseRx.h>\nint main(){mesh::MorseRawRssiTrace trace;}\n")
def test_actual_lazy_admission_accounts_for_whole_raw_buffer_and_contiguous_allocation(self):
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text(encoding="utf-8")
code = r'''
#include <helpers/MorseRx.h>
#include <cassert>
static size_t available = 0, largest = 0;
struct { size_t getFreeHeap() const { return available; } } ESP;
constexpr int MALLOC_CAP_8BIT = 1;
size_t heap_caps_get_largest_free_block(int) { return largest; }
struct RadioLibWrapper {
mesh::MorseRx* _morse_rx = nullptr;
mesh::MorseRssiTrace* _morse_trace = nullptr;
bool canAllocateMorseReceiver() const;
};
''' + method(source,"bool RadioLibWrapper::canAllocateMorseReceiver(") + r'''
int main() {
RadioLibWrapper radio;
const size_t raw = sizeof(mesh::MorseRssiTrace);
const size_t required = sizeof(mesh::MorseRx) + raw + 4096;
assert(raw > 90000);
available = required - 1; largest = raw; assert(!radio.canAllocateMorseReceiver());
available = required; largest = raw - 1; assert(!radio.canAllocateMorseReceiver());
largest = raw; assert(radio.canAllocateMorseReceiver());
radio._morse_rx = reinterpret_cast<mesh::MorseRx*>(1);
radio._morse_trace = reinterpret_cast<mesh::MorseRssiTrace*>(1);
available = 4095; largest = 0; assert(!radio.canAllocateMorseReceiver());
available = 4096; assert(radio.canAllocateMorseReceiver());
}
'''
compile_run(self, code, flags=RAW_FLAGS)
if __name__ == "__main__":
unittest.main()