mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-11-08 14:08:25 +00:00
The 45 s expiry moves into src/helpers/UiRequestSlot.h, shared by MyMesh::uiRequestTick and the web stand-in, so one constant and one rule cover both (firmware behaviour unchanged). test/test_ui_request_slot.cpp: the boundary (free at 45000 ms, not 44999), millis() wrap, an unanswered ask holding the admin app's STATUS until the expiry, an answered one clearing at once, a late reply matching nothing, the deferred login's own 45 s, and a source check that every slot taker in MyMesh.h and WebMesh.cpp stamps its time. web/scripts/ui-slot-expiry.wada: the Discord report end to end in the host build. WADA_DEMO_SILENT_TELEMETRY=1 makes the demo repeater lose its telemetry reply; the contact sheet's Telemetry (no deadline without SD) takes the slot, the admin app logs in and Status stays blank until 45 s after the ask, then the queued STATUS goes out and Status fills. With the expiry disabled the same run stays blank. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
6.5 KiB
C++
156 lines
6.5 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// Host test for the touch UI's single-flight request slot expiry (cec1431c):
|
|
// src/helpers/UiRequestSlot.h, used by MyMesh::uiRequestTick (src/MyMesh.h) and the
|
|
// web build's stand-in (web/mesh/WebMesh.cpp). Run from the repo root (it also reads
|
|
// src/MyMesh.h and web/mesh/WebMesh.cpp to check every slot taker stamps the time):
|
|
//
|
|
// c++ -std=c++17 -Wall -Wextra -I src test/test_ui_request_slot.cpp -o /tmp/test_ui_request_slot \
|
|
// && /tmp/test_ui_request_slot
|
|
//
|
|
// The end-to-end flow (a lost telemetry reply holds the slot, the admin app's queued
|
|
// STATUS goes out after the expiry) runs in the web host build:
|
|
// web/scripts/ui-slot-expiry.wada with WADA_DEMO_SILENT_TELEMETRY=1.
|
|
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "helpers/UiRequestSlot.h"
|
|
|
|
// The slot as MyMesh keeps it: the first 4 bytes of the key (0 = free), when it was
|
|
// taken, and the deferred guest login with its own stamp. tick() is uiRequestTick's
|
|
// expiry part; reply() is onContactResponse's tag match.
|
|
struct Slot {
|
|
uint32_t key = 0, at = 0, tag = 0;
|
|
uint32_t login_then = 0, login_then_at = 0;
|
|
void take(uint32_t k, uint32_t t, uint32_t now) { key = k; tag = t; at = now; }
|
|
void armLogin(uint32_t k, uint32_t now) { login_then = k; login_then_at = now; }
|
|
void tick(uint32_t now) {
|
|
if (key && UiRequestSlot::expired(now, at)) { key = 0; tag = 0; }
|
|
if (login_then && UiRequestSlot::expired(now, login_then_at)) login_then = 0;
|
|
}
|
|
bool reply(uint32_t k, uint32_t t) {
|
|
if (!key || key != k || t != tag) return false;
|
|
key = 0; tag = 0; login_then = 0;
|
|
return true;
|
|
}
|
|
bool pending() const { return key != 0 || login_then != 0; } // hasUIRequestPending
|
|
};
|
|
|
|
// The admin app's pump (RepeaterAdminApp.h raPump): a queued STATUS waits while the
|
|
// slot is held, then goes out and takes it.
|
|
struct Admin {
|
|
bool status_queued = false, status_sent = false;
|
|
uint32_t sent_at = 0;
|
|
void pump(Slot& s, uint32_t now) {
|
|
if (!status_queued || s.pending()) return;
|
|
status_queued = false; status_sent = true; sent_at = now;
|
|
s.take(0xA1A1A1A1u, 77, now);
|
|
}
|
|
};
|
|
|
|
static std::string slurp(const char* path) {
|
|
std::ifstream f(path);
|
|
if (!f) { fprintf(stderr, "cannot read %s (run from the repo root)\n", path); assert(false); }
|
|
std::stringstream ss; ss << f.rdbuf(); return ss.str();
|
|
}
|
|
|
|
// Every statement that takes the slot must stamp its time within the next few lines,
|
|
// or the slot carries a stale stamp and expires at once (or 45 s too late).
|
|
static int checkStamps(const char* path, const char* take, const char* stamp) {
|
|
const std::string src = slurp(path);
|
|
std::vector<std::string> lines;
|
|
{ std::stringstream ss(src); for (std::string l; std::getline(ss, l);) lines.push_back(l); }
|
|
int takers = 0;
|
|
for (size_t i = 0; i < lines.size(); ++i) {
|
|
if (lines[i].find(take) == std::string::npos) continue;
|
|
++takers;
|
|
bool ok = false;
|
|
for (size_t j = i; j < lines.size() && j <= i + 4; ++j) if (lines[j].find(stamp) != std::string::npos) ok = true;
|
|
if (!ok) { fprintf(stderr, "%s:%zu takes the slot without %s\n", path, i + 1, stamp); assert(false); }
|
|
}
|
|
return takers;
|
|
}
|
|
|
|
int main() {
|
|
const uint32_t T = 1000;
|
|
|
|
// ---- the boundary: free at exactly 45 s, not a millisecond before ----
|
|
assert(UiRequestSlot::MAX_MS == 45000);
|
|
assert(!UiRequestSlot::expired(T, T));
|
|
assert(!UiRequestSlot::expired(T + 44999, T));
|
|
assert(UiRequestSlot::expired(T + 45000, T));
|
|
assert(UiRequestSlot::expired(T + 3600000, T));
|
|
|
|
// ---- millis() wraps after ~49.7 days: the age is still right across it ----
|
|
const uint32_t W = 0xFFFFFFFFu - 10000; // taken 10 s before the wrap
|
|
assert(!UiRequestSlot::expired(W + 20000, W)); // 20 s later, after the wrap
|
|
assert(!UiRequestSlot::expired(W + 44999, W));
|
|
assert(UiRequestSlot::expired(W + 45000, W));
|
|
assert(UiRequestSlot::expired((uint32_t)(W + 60000), W));
|
|
|
|
// ---- an unanswered telemetry ask holds the slot; the admin STATUS waits, then goes ----
|
|
{
|
|
Slot s; Admin a;
|
|
s.take(0x11223344u, 5, T); // contact sheet Telemetry / auto-poll / Home widget
|
|
a.status_queued = true; // admin logs in 5 s later
|
|
for (uint32_t now = T; now <= T + 60000; now += 200) { s.tick(now); a.pump(s, now); }
|
|
assert(a.status_sent);
|
|
assert(a.sent_at >= T + 45000 && a.sent_at < T + 45200); // first tick after the expiry
|
|
}
|
|
|
|
// ---- an answered request clears at once: the STATUS goes on the next pump ----
|
|
{
|
|
Slot s; Admin a;
|
|
s.take(0x11223344u, 5, T);
|
|
a.status_queued = true;
|
|
s.tick(T + 1200); a.pump(s, T + 1200);
|
|
assert(!a.status_sent);
|
|
assert(!s.reply(0x11223344u, 6)); // a frame with another tag is not the answer
|
|
assert(!s.reply(0x55667788u, 5)); // nor one from another node
|
|
assert(s.reply(0x11223344u, 5));
|
|
s.tick(T + 1400); a.pump(s, T + 1400);
|
|
assert(a.status_sent && a.sent_at == T + 1400);
|
|
}
|
|
|
|
// ---- a reply that comes after the expiry matches nothing (the tag was dropped) ----
|
|
{
|
|
Slot s;
|
|
s.take(0x11223344u, 5, T);
|
|
s.tick(T + 45000);
|
|
assert(!s.pending());
|
|
assert(!s.reply(0x11223344u, 5));
|
|
}
|
|
|
|
// ---- the deferred guest login has its own 45 s, from when it was armed ----
|
|
{
|
|
Slot s;
|
|
s.take(0x11223344u, 5, T); // direct-first request
|
|
s.armLogin(0x11223344u, T + 12000); // fallback login 12 s later (no path)
|
|
s.tick(T + 45000);
|
|
assert(s.key == 0 && s.login_then != 0 && s.pending());
|
|
s.tick(T + 56999);
|
|
assert(s.pending());
|
|
s.tick(T + 57000);
|
|
assert(!s.pending());
|
|
}
|
|
|
|
// ---- every slot taker in the firmware and the web stand-in stamps the time ----
|
|
const int fw = checkStamps("src/MyMesh.h", "memcpy(&_ui_pending_status,", "_ui_pending_at");
|
|
const int fw_login = checkStamps("src/MyMesh.h", "memcpy(&_ui_login_then,", "_ui_login_then_at");
|
|
const int web = checkStamps("web/mesh/WebMesh.cpp", "memcpy(_ui_pending_key,", "_ui_pending_at");
|
|
assert(fw >= 4 && fw_login >= 1);
|
|
assert(web >= 4);
|
|
// Both expiries go through the shared helper (one constant, one rule).
|
|
assert(slurp("src/MyMesh.h").find("UiRequestSlot::expired(now, _ui_pending_at)") != std::string::npos);
|
|
assert(slurp("src/MyMesh.h").find("UiRequestSlot::expired(now, _ui_login_then_at)") != std::string::npos);
|
|
assert(slurp("web/mesh/WebMesh.cpp").find("UiRequestSlot::expired(now, _ui_pending_at)") != std::string::npos);
|
|
|
|
printf("test_ui_request_slot: ok (%d firmware slot takers, %d login arms checked)\n", fw, fw_login);
|
|
return 0;
|
|
}
|