Files
MeshCore/test/fixtures/morse_rx_cli.cpp
T
mikecarper 0be4c7488c Add bounded Morse receive and negotiated RS232 autosleep
Add native narrow-band CW capture, ACK-first remote sessions, retained LoRa-readable pages, checked restoration and transport-safe progress. Add stock-compatible negotiated heartbeat/autosleep and recovery. Include host regressions and identity-bound HIL runners; keep unverified faster tuning HIL-only and document outstanding hardware validation.
2026-10-13 01:24:39 -07:00

136 lines
7.3 KiB
C++

int main() {
TestRadio radio; radio.rx_supported = true;
char reply[160];
const auto run = [&](const char* command) {
memset(reply, '~', sizeof(reply));
assert(mesh::handleMorseCommand(&radio, command, reply, sizeof(reply)));
assert(memchr(reply, 0, sizeof(reply)));
return std::string(reply);
};
assert(run("get morse").find("captured=0") != std::string::npos);
assert(run("morse listen 5").find("OK - Morse RX") == 0);
assert(radio.rx_active && !radio.active && radio.rx_starts == 1 && !radio.starts);
assert(radio.frequency == 902.1f && radio.speed == 25 && radio.rx_duration == 300000);
assert(run("get morse").find("BW 7.81 kHz; 300.000 seconds left") != std::string::npos);
radio.capture = "CQ TEST SOS";
const auto saves = mesh::saves;
assert(run("set morse.freq 903.1").find("busy") != std::string::npos);
assert(run("set morse.wpm 30").find("busy") != std::string::npos);
assert(mesh::saves == saves);
assert(run("morse clear").find("Error: stop Morse") == 0 && radio.capture == "CQ TEST SOS");
assert(run("morse send E").find("busy") != std::string::npos && !radio.active && radio.rx_active);
assert(run("morse read") == "> 0000/0011 next=END truncated=0 CQ TEST SOS");
assert(run("morse read 3") == "> 0003/0011 next=END truncated=0 TEST SOS");
assert(run("morse read 11") == "> 0011/0011 next=END truncated=0 ");
assert(run("morse stats") == "> no Morse RX statistics");
radio.have_rx_diagnostics = true;
radio.rx_diagnostics.samples = 500;
radio.rx_diagnostics.noise_dbm = -110;
radio.rx_diagnostics.minimum_dbm = -112;
radio.rx_diagnostics.maximum_dbm = -80;
radio.rx_diagnostics.last_dbm = -109;
radio.rx_diagnostics.unit_ms = 48;
radio.rx_diagnostics.read_errors = 2;
assert(run("morse stats") == "> samples=500; noise=-110.0; min=-112.0; max=-80.0; last=-109.0 dBm; unit=48 ms; gap=0 ms; bad=2");
const auto saved_stats=radio.rx_diagnostics;
radio.rx_diagnostics.max_sample_gap_ms=0xffffffff;
radio.rx_diagnostics.samples=0xffffffff;
radio.rx_diagnostics.unit_ms=65535;
radio.rx_diagnostics.read_errors=255;
char reflected[161]; memset(reflected,'~',sizeof(reflected));
memcpy(reflected,"AB|",3);
assert(mesh::handleMorseCommand(&radio,"morse stats",reflected+3,157));
assert(reflected[160]=='~' && !memcmp(reflected,"AB|",3));
assert(memchr(reflected+3,0,157) && std::string(reflected+3).find("gap=4294967295 ms")!=std::string::npos);
radio.rx_diagnostics=saved_stats;
assert(run("morse stats E").find("Error:") == 0);
assert(run("morse stop").find("capture retained; receive resumed") != std::string::npos);
assert(!radio.rx_active && radio.rx_stops == 1 && radio.capture == "CQ TEST SOS");
assert(run("get morse").find("captured=11") != std::string::npos);
assert(run("morse read").find("CQ TEST SOS") != std::string::npos);
// A bounded LoRa CLI reply is a stable page, not a silently truncated message.
radio.capture.clear();
for (unsigned i = 0; i < mesh::MorseRx::CaptureCapacity; ++i) radio.capture += char('A' + i % 26);
radio.capture_truncated = true;
std::string reconstructed;
uint32_t offset = 0;
unsigned pages = 0;
while (offset < radio.capture.size()) {
const auto command = std::string("morse read ") + std::to_string(offset);
const auto page = run(command.c_str());
unsigned returned = 0, total = 0, truncated = 0;
char next[5]; int payload = 0;
assert(sscanf(page.c_str(), "> %u/%u next=%4s truncated=%u %n",
&returned, &total, next, &truncated, &payload) == 4);
assert(returned == offset && total == mesh::MorseRx::CaptureCapacity && truncated == 1 && payload > 0);
const auto text = page.substr(size_t(payload));
assert(!text.empty() && page.size() < sizeof(reply));
reconstructed += text;
if (!strcmp(next, "END")) offset = total;
else {
const auto following = uint32_t(std::strtoul(next, nullptr, 10));
assert(following == returned + text.size() && following > returned);
offset = following;
}
assert(++pages < 40);
}
assert(reconstructed == radio.capture && pages > 1);
for (const char* argument : {"-1", "4097", "1x", "1.0", "1e1", "4294967296", "99999999999999999999", "1 2"}) {
const auto command = std::string("morse read ") + argument;
assert(run(command.c_str()).find("Error:") == 0);
}
assert(run("morse read +0").find("> 0000/4096") == 0);
mesh::usable = false;
assert(run("morse read").find("> 0000/4096") == 0); // capture remains accessible on settings error
assert(run("morse stats").find("samples=500") != std::string::npos);
const auto sessions = radio.rx_starts;
assert(run("morse listen 1").find("saved Morse settings unavailable") != std::string::npos);
assert(radio.rx_starts == sessions);
assert(run("morse clear") == "OK - Morse capture cleared" && radio.capture.empty() && !radio.capture_truncated);
mesh::usable = true;
for (const char* duration : {"", "0", "-1", "5.001", "nan", "inf", "1e0", "1junk", "1 2", "0.01", "4294967296", "99999999999999999999"}) {
const auto starts = radio.rx_starts;
const auto command = std::string("morse listen ") + duration;
assert(run(command.c_str()).find("Error:") == 0 && radio.rx_starts == starts);
}
for (const char* duration : {"1", "5", ".5", ".0166667", "+2", "\t1\t"}) {
const auto command = std::string("morse listen ") + duration;
assert(run(command.c_str()).find("OK - Morse RX") == 0);
assert(radio.rx_duration >= 1000 && radio.rx_duration <= 300000 && !radio.active);
run("morse stop");
}
for (const char* invalid : {"get morse listen 1", "set morse listen 1", "morse clear 1", "morse stop 1"}) {
const auto starts = radio.rx_starts, stops = radio.rx_stops;
assert(run(invalid).find("Error:") == 0 && radio.rx_starts == starts && radio.rx_stops == stops);
}
radio.force_rx_result = true;
for (auto result : {mesh::MorseRxResult::Busy, mesh::MorseRxResult::Unsupported,
mesh::MorseRxResult::InvalidFrequency, mesh::MorseRxResult::Overlap,
mesh::MorseRxResult::InvalidSpeed, mesh::MorseRxResult::InvalidDuration,
mesh::MorseRxResult::Failed}) {
radio.forced_rx = result;
assert(run("morse listen 1").find("Error:") == 0);
}
radio.force_rx_result = false;
run("morse listen 1"); radio.capture = "LAST COPY";
radio.force_rx_result = true; radio.forced_rx = mesh::MorseRxResult::Failed;
assert(run("morse stop").find("recovery pending; capture retained") != std::string::npos);
assert(run("get morse").find("packet-radio recovery pending") != std::string::npos);
assert(run("morse read").find("LAST COPY") != std::string::npos);
radio.force_rx_result = false; run("morse stop");
radio.supported = false; // RX-only capability must not require a TX implementation
assert(run("morse listen 1").find("OK - Morse RX") == 0);
assert(run("morse send E").find("TX unsupported") != std::string::npos);
run("morse stop");
radio.rx_supported = false;
assert(run("morse listen 1").find("unsupported") != std::string::npos);
radio.supported = radio.rx_supported = true; radio.capture = std::string(mesh::MorseRx::CaptureCapacity, 'E');
for (size_t capacity = 1; capacity <= 160; ++capacity) {
char bounded[162]; memset(bounded, '~', sizeof(bounded));
assert(mesh::handleMorseCommand(&radio, "morse read 0", bounded, capacity));
assert(memchr(bounded, 0, capacity) && bounded[capacity] == '~');
}
}