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.
191 lines
9.6 KiB
C++
191 lines
9.6 KiB
C++
using Handoff = mesh::MorseReplyHandoff;
|
|
using RXResult = mesh::MorseRxResult;
|
|
static uint8_t actor[32] = {1, 2, 3, 4};
|
|
|
|
static void stage(Handoff& handoff, uint32_t now = 100) {
|
|
handoff.beginCommand(actor);
|
|
assert(handoff.remoteCommand());
|
|
assert(handoff.stage(902100, 25, 60000, now));
|
|
handoff.endCommand();
|
|
assert(handoff.pending() && !handoff.remoteCommand());
|
|
assert(!memcmp(handoff.actor(), actor, 32));
|
|
}
|
|
static void queued(Handoff& h, const void* packet, const void* copy = nullptr) {
|
|
h.prepareReply(packet);
|
|
if (copy) h.copy(packet, copy);
|
|
assert(h.ownsReply(packet) && (!copy || h.ownsReply(copy)));
|
|
h.armReply(packet, true);
|
|
}
|
|
static bool service(Handoff& h, TestRadio& radio, uint32_t now = 100,
|
|
bool authorized = true, bool usable = true,
|
|
uint32_t frequency = 902100, uint8_t wpm = 25) {
|
|
return h.service(&radio, now, authorized, usable, frequency, wpm);
|
|
}
|
|
|
|
static void exact_phy_barrier() {
|
|
for (unsigned success = 0; success < 4; ++success) {
|
|
Handoff handoff; TestRadio radio; radio.rx_supported = true;
|
|
int packet, copy, unrelated;
|
|
stage(handoff); queued(handoff, &packet, ©);
|
|
handoff.complete(&unrelated); handoff.fail(&unrelated);
|
|
assert(!service(handoff, radio) && radio.rx_starts == 0 && handoff.pending());
|
|
if (success & 1) handoff.complete(&packet); else handoff.fail(&packet);
|
|
assert(!handoff.ownsReply(&packet) && handoff.ownsReply(©)); // pool slot may now be reused
|
|
assert(!service(handoff, radio) && radio.rx_starts == 0 && handoff.pending());
|
|
if (success & 2) handoff.complete(©); else handoff.fail(©);
|
|
assert(service(handoff, radio) == (success != 0));
|
|
assert(!handoff.pending() && radio.rx_starts == unsigned(success != 0));
|
|
assert(!handoff.ownsReply(&packet) && !handoff.ownsReply(©));
|
|
handoff.complete(&packet); handoff.complete(©);
|
|
assert(!service(handoff, radio) && radio.rx_starts == unsigned(success != 0));
|
|
}
|
|
{
|
|
Handoff h; TestRadio radio; int packet;
|
|
assert(!h.stage(902100, 25, 60000, 100)); // timestamps do not authorize remote staging
|
|
h.beginCommand(nullptr); assert(!h.stage(902100, 25, 60000, 100));
|
|
stage(h);
|
|
const auto generation = h.generation();
|
|
h.beginCommand(actor); assert(!h.stage(902100, 25, 60000, 100)); h.endCommand();
|
|
assert(h.generation() == generation);
|
|
h.prepareReply(&packet);
|
|
h.fail(&packet); h.complete(&packet); // admission callbacks before arm prove nothing
|
|
assert(h.pending() && !service(h, radio) && !radio.rx_starts);
|
|
h.armReply(&packet, false); assert(!h.pending());
|
|
}
|
|
{
|
|
Handoff h; TestRadio radio; int packet;
|
|
stage(h); h.prepareReply(&packet); h.armReply(nullptr, true);
|
|
assert(!h.pending() && !service(h, radio) && !radio.rx_starts);
|
|
}
|
|
}
|
|
|
|
static void revocation_settings_cancel_and_replays() {
|
|
for (int reason = 0; reason < 5; ++reason) {
|
|
Handoff h; TestRadio radio; int packet;
|
|
stage(h); queued(h, &packet); h.complete(&packet);
|
|
if (reason == 4) h.cancel();
|
|
assert(!service(h, radio, 100, reason != 0, reason != 1,
|
|
reason == 2 ? 902101 : 902100, reason == 3 ? 26 : 25));
|
|
assert(!h.pending() && !radio.rx_starts);
|
|
}
|
|
{
|
|
Handoff h; TestRadio radio; int old_packet, new_packet;
|
|
stage(h); queued(h, &old_packet); const auto previous = h.generation(); h.cancel();
|
|
stage(h); queued(h, &new_packet);
|
|
assert(h.generation() != previous && !h.contains(&old_packet));
|
|
h.complete(&old_packet); h.fail(&old_packet);
|
|
assert(!service(h, radio) && !radio.rx_starts);
|
|
h.complete(&new_packet); assert(service(h, radio) && radio.rx_starts == 1);
|
|
}
|
|
{
|
|
Handoff h; uint8_t original[32]; memcpy(original, actor, 32);
|
|
stage(h); actor[31] ^= 1;
|
|
assert(!memcmp(h.actor(), original, 32)); // full identity is owned, not an ACL index/pointer
|
|
actor[31] ^= 1; h.cancel();
|
|
}
|
|
}
|
|
|
|
static void deadlines_busy_retry_and_hard_failure() {
|
|
for (uint32_t now : {100U, 0xfffffff0U}) {
|
|
Handoff h; TestRadio radio; int packet;
|
|
stage(h, now); queued(h, &packet);
|
|
assert(!service(h, radio, now + Handoff::ReplyDeadlineMillis - 1) && h.pending());
|
|
assert(!service(h, radio, now + Handoff::ReplyDeadlineMillis) && !h.pending());
|
|
h.complete(&packet); assert(!service(h, radio, now + Handoff::ReplyDeadlineMillis + 1) && !radio.rx_starts);
|
|
}
|
|
for (auto outcome : {RXResult::Unsupported, RXResult::InvalidFrequency,
|
|
RXResult::InvalidSpeed, RXResult::InvalidDuration, RXResult::Failed}) {
|
|
Handoff h; TestRadio radio; int packet;
|
|
stage(h); queued(h, &packet); h.complete(&packet);
|
|
radio.force_rx_result = true; radio.forced_rx = outcome;
|
|
assert(!service(h, radio) && !h.pending() && radio.rx_starts == 1);
|
|
assert(!service(h, radio, 200) && radio.rx_starts == 1);
|
|
}
|
|
for (uint32_t now : {100U, 0xfffffff0U, UINT32_MAX - 99U, UINT32_MAX - 9999U}) {
|
|
Handoff h; TestRadio radio; int packet;
|
|
stage(h, now); queued(h, &packet); h.complete(&packet);
|
|
radio.force_rx_result = true; radio.forced_rx = RXResult::Busy;
|
|
assert(!service(h, radio, now) && h.pending() && radio.rx_starts == 1);
|
|
assert(!service(h, radio, now) && radio.rx_starts == 1);
|
|
assert(!service(h, radio, now + 99) && radio.rx_starts == 1);
|
|
assert(!service(h, radio, now + 100) && radio.rx_starts == 2);
|
|
radio.force_rx_result = false;
|
|
assert(service(h, radio, now + 200) && !h.pending() && radio.rx_starts == 3);
|
|
assert(!service(h, radio, now + 300) && radio.rx_starts == 3);
|
|
}
|
|
{
|
|
const uint32_t now = UINT32_MAX - 9999U; // exactly zero wrapped start deadline
|
|
Handoff h; TestRadio radio; int packet;
|
|
stage(h, now); queued(h, &packet); h.complete(&packet);
|
|
radio.force_rx_result = true; radio.forced_rx = RXResult::Busy;
|
|
assert(!service(h, radio, now));
|
|
assert(!service(h, radio, now + Handoff::StartDeadlineMillis - 1) && h.pending());
|
|
assert(!service(h, radio, now + Handoff::StartDeadlineMillis) && !h.pending());
|
|
}
|
|
{
|
|
Handoff h; TestRadio radio; int packet;
|
|
stage(h); queued(h, &packet); h.complete(&packet);
|
|
radio.force_rx_result = true; radio.forced_rx = RXResult::Busy;
|
|
assert(!service(h, radio) && h.pending());
|
|
assert(!service(h, radio, 101, false) && !h.pending()); // revoke permission during Busy retry
|
|
assert(radio.rx_starts == 1);
|
|
}
|
|
}
|
|
|
|
static void actual_shared_remote_parser() {
|
|
Handoff h; TestRadio radio; radio.rx_supported = true;
|
|
char reply[160];
|
|
const auto command = [&](const char* text, bool remote) {
|
|
if (remote) h.beginCommand(actor);
|
|
const bool intercepted = mesh::handleMorseReplyCommand(h, &radio, text, reply, sizeof(reply), 100);
|
|
h.endCommand();
|
|
if (!intercepted) assert(mesh::handleMorseCommand(&radio, text, reply, sizeof(reply)));
|
|
return std::string(reply);
|
|
};
|
|
assert(command("morse listen 1", false).find("OK - Morse RX") == 0);
|
|
assert(radio.rx_active && radio.rx_starts == 1 && !h.pending());
|
|
command("morse stop", false);
|
|
assert(command("morse listen 1", true).find("starts after this reply transmits") != std::string::npos);
|
|
assert(h.pending() && !radio.rx_active && radio.rx_starts == 1);
|
|
const auto generation = h.generation();
|
|
assert(command("morse listen 1", true).find("handoff pending") != std::string::npos && h.generation() == generation);
|
|
assert(command("get morse", false).find("waiting for command reply") != std::string::npos);
|
|
assert(command("set morse.freq 903", false).find("handoff pending") != std::string::npos);
|
|
assert(command("cw on", false).find("handoff pending") != std::string::npos);
|
|
assert(command("morse send E", false).find("handoff pending") != std::string::npos);
|
|
assert(command("morse read", false).find("> 0000/0000") == 0);
|
|
assert(command("morse stop", false).find("pending Morse RX cancelled") != std::string::npos && !h.pending());
|
|
assert(radio.rx_starts == 1);
|
|
for (const char* spelling : {"morse\tlisten 1", "morse listen\t1", "morse listen +.5\t",
|
|
"morse\t\tlisten .0166667"}) {
|
|
assert(command(spelling, true).find("starts after this reply transmits") != std::string::npos);
|
|
assert(h.pending() && radio.rx_starts == 1 && !radio.rx_active);
|
|
assert(command("morse\tstop\t ", false).find("pending Morse RX cancelled") != std::string::npos);
|
|
assert(!h.pending());
|
|
}
|
|
for (bool remote : {false, true}) {
|
|
for (const char* stop : {"morse stop ", "morse stop ", "morse\tstop", "morse\t\tstop\t "}) {
|
|
assert(command("morse listen 1", true).find("starts after this reply transmits") != std::string::npos);
|
|
assert(command(stop, remote).find("pending Morse RX cancelled") != std::string::npos);
|
|
assert(!h.pending() && !radio.rx_active && radio.rx_starts == 1);
|
|
}
|
|
}
|
|
for (const char* invalid : {"morse listen", "morse listen 0", "morse listen 6",
|
|
"morse listen 1x", "morse listen nan", "morse listen 1e0", "morse listen .01"}) {
|
|
assert(command(invalid, true).find("Error:") == 0 && !h.pending() && radio.rx_starts == 1);
|
|
}
|
|
mesh::usable = false;
|
|
assert(command("morse listen 1", true).find("saved Morse settings unavailable") != std::string::npos && !h.pending());
|
|
mesh::usable = true;
|
|
radio.rx_supported = false;
|
|
assert(command("morse listen 1", true).find("unsupported") != std::string::npos && !h.pending());
|
|
assert(!mesh::handleMorseReplyCommand(h, &radio, nullptr, reply, sizeof(reply), 100));
|
|
assert(!mesh::handleMorseReplyCommand(h, &radio, "get morse", nullptr, 160, 100));
|
|
assert(!mesh::handleMorseReplyCommand(h, &radio, "get morse", reply, 0, 100));
|
|
}
|
|
|
|
int main() {
|
|
exact_phy_barrier(); revocation_settings_cancel_and_replays();
|
|
deadlines_busy_retry_and_hard_failure(); actual_shared_remote_parser();
|
|
}
|