Files
MeshCore/test/fixtures/companion_ui_deadlines.cpp
T

477 lines
18 KiB
C++

// Compile the complete production UITask::loop() with a true 32-bit clock.
// Screen rendering, radio timing, display and unrelated services are adapters;
// deadline, popup, polling and scheduling decisions remain production source.
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <initializer_list>
static uint32_t now_ms = 0;
static uint32_t millis() { return now_ms; }
static void delay(uint32_t duration) { now_ms += duration; }
static constexpr uint32_t BOOT_SCREEN_MILLIS = 3000;
#define MESH_DEBUG_PRINTLN(...) do {} while (0)
struct Radio { bool timing = false; } radio_driver;
struct TestMesh { unsigned rescues = 0; void enterCLIRescue() { ++rescues; } } the_mesh;
namespace mesh {
static bool morseTimingActive(const Radio* radio) { return radio && radio->timing; }
namespace ui { enum class DisplayWake { Button }; }
}
namespace UIColor { constexpr int popup_bkg = 0, popup_txt = 1, warning_txt = 2; }
struct Board { bool isExternalPowered() const { return false; } } board;
struct Display {
bool on = true;
unsigned frames = 0, popups = 0;
bool isOn() const { return on; }
bool isEink() const { return false; }
int height() const { return 64; }
int width() const { return 128; }
void wake(mesh::ui::DisplayWake) { on = true; }
void startFrame() { ++frames; }
void endFrame() {}
void setTextSize(int) {}
void setColor(int) {}
void fillRect(int, int, int, int) {}
void drawRect(int, int, int, int) {}
void drawTextCentered(int, int, const char* text) { if (!std::strcmp(text, "ALERT")) ++popups; }
};
struct UIScreen {
unsigned renders = 0, polls = 0;
int delay_ms = 1000;
int render(Display&) { ++renders; return delay_ms; }
void poll() { ++polls; }
void handleInput(char) {}
};
struct HomeScreen : UIScreen { void showFirstPage() {} };
struct StatusBar {
void update(Display&, const char*, uint16_t, bool, bool, bool) {}
bool needsRedraw() const { return false; }
void render(Display&) {}
};
struct UITask {
Display display;
Display* _display = &display;
HomeScreen screen;
UIScreen* curr = &screen;
UIScreen* home = &screen;
UIScreen preview;
UIScreen* msg_preview = &preview;
StatusBar _statusBar;
struct Prefs { const char* node_name = "test"; } prefs;
Prefs* _node_prefs = &prefs;
// Arduino unsigned long is 32 bits; the host unsigned long is NOT used.
uint32_t _next_refresh = 0, _alert_expiry = 0, ui_started_at = 0, next_batt_chck = 0;
uint32_t _pairing_screen_until = 0;
bool _pairing_from_button = false;
bool _startup_complete = false;
uint16_t _cached_batt_mv = 4000, voltage = 4000;
bool gesture_pending = false;
bool _need_refresh = true;
unsigned battery_reads = 0, shutdowns = 0, profile_services = 0;
char _alert[80] = {}, _origin[80] = {}, _msg[80] = {};
void servicePairingState() {}
void serviceWiFiToggleButton() {}
bool isPairingScreenActive() const { return false; }
bool isButtonGesturePending() const { return gesture_pending; }
bool isBuzzerQuiet() const { return true; }
bool getGPSState() const { return false; }
bool isBluetoothEnabled() const { return false; }
void userLedHandler() {}
void renderPairingBanner() {}
void serviceRadioProfileDisplayPage() { ++profile_services; }
void renderCurrScreen() { ++screen.renders; _need_refresh = false; }
uint16_t getBattMilliVolts() { ++battery_reads; return voltage; }
void shutdown() { ++shutdowns; }
void showAlert(const char* text, int duration_millis);
void setCurrScreen(UIScreen* screen);
void loop();
void handleButtonLongPress();
char handleLongPress(char);
};
@LOOP@
@ALERT@
@SCREEN@
static void forceCurrentScreen(UITask& ui) {
#if UI_ORIG
// Orig has no screen selector. Dirty is a real incoming-event condition;
// retain its already-due refresh so the test never invents a comparator.
ui._need_refresh = true;
#else
ui.setCurrScreen(ui.home); // Actual source's force-refresh operation.
#endif
}
int main(int argc, char** argv) {
setbuf(stdout, nullptr);
assert(argc == 2);
const char* scenario = argv[1];
UITask ui;
if (!std::strcmp(scenario, "startup-first-draw")) {
#if UI_ORIG
ui.ui_started_at = now_ms = 1000;
ui._need_refresh = false;
ui.display.on = false;
ui.loop();
now_ms = 9000; ui.loop();
for (uint64_t day = 1; day <= 49; ++day) {
now_ms = uint32_t(1000u + day * 86400000ULL);
ui.loop();
}
now_ms = 2000; ui.loop(); // Full-wrap alias, startup already observed closed.
ui.display.on = true; ui.loop();
assert(ui.display.frames == 1 && "deferred first paint must remain post-startup after full wrap");
#endif
std::puts("PASS: startup-first-draw");
return 0;
}
if (!std::strcmp(scenario, "startup-one-shot")) {
for (uint32_t started : {0u, 1000u, UINT32_MAX - 4000u}) {
for (unsigned gate = 0; gate < 4; ++gate) {
UITask window;
window.ui_started_at = started;
window._next_refresh = now_ms = started;
window.display.on = (gate & 1) == 0;
radio_driver.timing = (gate & 2) != 0;
window.loop();
now_ms = started + 7999u; window.loop();
const auto before = the_mesh.rescues;
#if UI_ORIG
window.handleButtonLongPress();
#else
assert(window.handleLongPress('x') == 0);
#endif
assert(the_mesh.rescues == before + 1 && !window._startup_complete);
now_ms = started + 8000u; window.loop();
#if UI_ORIG
window.handleButtonLongPress();
assert(window.shutdowns == 1);
#else
assert(window.handleLongPress('x') == 'x');
#endif
assert(the_mesh.rescues == before + 1); // Exact 8-second cutoff.
for (uint64_t day = 1; day <= 49; ++day) {
now_ms = uint32_t(started + day * 86400000ULL);
window.loop();
}
// Live observation of expiry precedes the complete-counter alias.
now_ms = started + 1000u; window.loop();
window.display.on = true; radio_driver.timing = false;
const auto profiles = window.profile_services;
window.loop();
#if UI_ORIG
window.handleButtonLongPress();
#else
(void)profiles;
const char returned = window.handleLongPress('x');
#endif
assert(the_mesh.rescues == before + 1 && window._startup_complete
&& "expired startup rescue must never reopen after full wrap");
#if UI_ORIG
assert(window.shutdowns == 2 && window.profile_services == profiles + 1);
#else
assert(returned == 'x');
#endif
}
}
std::puts("PASS: startup-one-shot");
return 0;
}
if (!std::strcmp(scenario, "orig-long-clean-idle")) {
#if UI_ORIG
now_ms = 0x90001000u;
const uint32_t origin = now_ms;
ui.ui_started_at = now_ms - BOOT_SCREEN_MILLIS;
ui._next_refresh = now_ms;
// A persistent message preview disables automatic radio-page dirtiness.
std::strcpy(ui._origin, "sender");
std::strcpy(ui._msg, "retained preview");
ui.loop();
assert(ui.display.frames == 1 && !ui._need_refresh);
for (uint32_t day = 1; day <= 26; ++day) {
now_ms = origin + day * 86400000u;
ui.loop(); // Every individual elapsed step is far below half a wrap.
assert(ui.display.frames == 1 && !ui._need_refresh);
}
ui._need_refresh = true; // A fresh message finally makes the view dirty.
ui.loop();
std::printf("orig after 26 clean days: frames=%u next=%u now=%u\n",
ui.display.frames, ui._next_refresh, now_ms);
assert(ui.display.frames == 2 && "a clean interval cannot age a due deadline back into the future");
#else
assert(false && "orig-only scenario");
#endif
} else if (!std::strcmp(scenario, "display-off-long-idle")) {
#if !UI_ORIG
now_ms = 0x90001000u;
const uint32_t origin = now_ms;
ui.ui_started_at = now_ms - BOOT_SCREEN_MILLIS;
ui._next_refresh = now_ms;
ui.next_batt_chck = now_ms;
ui.display.on = false;
ui.loop();
for (uint32_t day = 1; day <= 26; ++day) {
now_ms = origin + day * 86400000u;
ui.loop();
assert(ui.display.frames == 0);
}
ui.display.on = true; // Do not inject a force-refresh assignment on wake.
ui.loop();
std::printf("wake after 26 display-off days: frames=%u next=%u now=%u\n",
ui.display.frames, ui._next_refresh, now_ms);
assert(ui.display.frames == 1 && "live loops must preserve due refresh status while display is off");
#else
assert(false && "screened layouts only");
#endif
} else if (!std::strcmp(scenario, "fresh-dirty-throttle")) {
#if UI_ORIG
now_ms = 0x90001000u;
ui.ui_started_at = now_ms - BOOT_SCREEN_MILLIS;
ui._next_refresh = now_ms;
std::strcpy(ui._origin, "sender");
std::strcpy(ui._msg, "preview");
ui.loop();
assert(ui.display.frames == 1 && !ui._need_refresh);
++now_ms;
ui._need_refresh = true; // Fresh incoming dirtiness, not a force operation.
ui.loop();
assert(ui.display.frames == 1 && "normal fresh dirtiness still respects the one-second throttle");
now_ms += 998u;
ui.loop();
assert(ui.display.frames == 1);
++now_ms;
ui.loop();
assert(ui.display.frames == 2);
#else
assert(false && "orig-only dirty-flag scenario");
#endif
} else if (!std::strcmp(scenario, "cache-battery-long-idle")) {
#if UI_TINY_CACHE
now_ms = 0x90001000u;
const uint32_t origin = now_ms;
ui.ui_started_at = now_ms - BOOT_SCREEN_MILLIS;
ui._next_refresh = now_ms;
ui.next_batt_chck = now_ms;
ui.display.on = false;
ui.loop();
for (uint32_t day = 1; day <= 26; ++day) {
now_ms = origin + day * 86400000u;
ui.loop();
assert(ui.battery_reads == 0 && ui.display.frames == 0);
}
ui.display.on = true;
radio_driver.timing = true;
ui.loop();
assert(ui.battery_reads == 0 && ui.display.frames == 0);
radio_driver.timing = false;
++now_ms;
ui.loop();
std::printf("cache ADC after 26 off days and Morse release: reads=%u next=%u now=%u\n",
ui.battery_reads, ui.next_batt_chck, now_ms);
assert(ui.battery_reads == 1 && "cached battery poll stays due while off but samples only on safe wake");
assert(ui.display.frames == 1);
#else
assert(false && "tiny no-auto-shutdown cache-only scenario");
#endif
} else if (!std::strcmp(scenario, "cache-battery-wrapped-future")) {
#if UI_TINY_CACHE
now_ms = UINT32_MAX - 7999u;
ui.next_batt_chck = now_ms;
ui.loop();
assert(ui.battery_reads == 1 && ui.next_batt_chck == 0);
++now_ms;
ui.loop();
assert(ui.battery_reads == 1 && "future wrapped zero cannot be clamped into an immediate cache poll");
ui.display.on = false;
now_ms = 1u;
ui.loop();
assert(ui.battery_reads == 1);
ui.display.on = true;
radio_driver.timing = true;
ui.loop();
assert(ui.battery_reads == 1);
radio_driver.timing = false;
ui.loop();
assert(ui.battery_reads == 2 && "due cached ADC waits for both display and Morse gates");
#else
assert(false && "tiny cache-only scenario");
#endif
} else if (!std::strcmp(scenario, "healthy") || !std::strcmp(scenario, "upper-half")) {
now_ms = !std::strcmp(scenario, "healthy") ? 10000u : 0x80001000u;
ui.ui_started_at = now_ms - BOOT_SCREEN_MILLIS;
ui._next_refresh = now_ms + 1000u;
ui.loop();
assert(ui.display.frames == 0);
now_ms += 999u;
ui.loop();
assert(ui.display.frames == 0);
++now_ms;
ui.loop();
assert(ui.display.frames == 1 && ui.screen.renders == 1);
} else if (!std::strcmp(scenario, "force-upper-half")) {
#if UI_ORIG
assert(false && "orig has no setCurrScreen operation");
#else
now_ms = 0x80001000u;
ui._next_refresh = now_ms + 1000u;
forceCurrentScreen(ui);
ui.loop();
assert(ui.display.frames == 1 && "screen change must redraw immediately in upper half");
#endif
} else if (!std::strcmp(scenario, "deferred-across-wrap")) {
now_ms = UINT32_MAX - 1500u;
ui.ui_started_at = now_ms - BOOT_SCREEN_MILLIS;
ui._next_refresh = UINT32_MAX - 1000u;
ui.loop();
assert(ui.display.frames == 0);
radio_driver.timing = true;
now_ms = UINT32_MAX - 500u;
ui.loop();
now_ms = 100u;
ui.loop();
assert(ui.display.frames == 0);
radio_driver.timing = false;
ui.loop();
std::printf("after wrapped Morse deferral: frames=%u next=%u now=%u\n",
ui.display.frames, ui._next_refresh, now_ms);
assert(ui.display.frames == 1 && "overdue refresh must run after Morse releases across wrap");
} else if (!std::strcmp(scenario, "wrapped-future-deadline")) {
now_ms = UINT32_MAX - 999u;
ui.ui_started_at = now_ms - BOOT_SCREEN_MILLIS;
ui._next_refresh = now_ms;
ui.loop();
assert(ui.display.frames == 1 && ui._next_refresh == 0);
++now_ms;
// Orig correctly clears dirty after render; model a fresh incoming event.
// It still must respect the real loop's one-second redraw throttle.
ui._need_refresh = true;
ui.loop();
std::printf("future deadline wrapped to zero: frames=%u now=%u\n", ui.display.frames, now_ms);
assert(ui.display.frames == 1 && "a future wrapped deadline is not an immediate force sentinel");
now_ms = 0;
ui.loop();
assert(ui.display.frames == 2);
}
#if !UI_ORIG
else if (!std::strcmp(scenario, "battery-healthy") || !std::strcmp(scenario, "battery-upper-half")) {
ui.display.on = false;
now_ms = !std::strcmp(scenario, "battery-healthy") ? 10000u : 0x80001000u;
const uint32_t due = now_ms + 1000u;
ui.next_batt_chck = due;
ui.loop();
assert(ui.battery_reads == 0 && ui.shutdowns == 0);
now_ms = due + 1u;
ui.loop();
assert(ui.battery_reads == 1 && ui.shutdowns == 0);
now_ms += 7999u;
ui.loop();
assert(ui.battery_reads == 1);
now_ms += 2u;
ui.loop();
assert(ui.battery_reads == 2 && ui.shutdowns == 0);
} else if (!std::strcmp(scenario, "alert-upper-half")) {
now_ms = 0x80001000u;
ui.showAlert("ALERT", 1000);
forceCurrentScreen(ui);
ui.loop();
assert(ui.display.popups == 1);
now_ms += 999u;
forceCurrentScreen(ui);
ui.loop();
assert(ui.display.popups == 2);
now_ms += 2u;
forceCurrentScreen(ui);
ui.loop();
assert(ui.display.popups == 2 && "healthy upper-half alert must expire normally");
} else if (!std::strcmp(scenario, "battery-deferred-wrap")) {
ui.display.on = false;
radio_driver.timing = true;
ui.voltage = 3350;
now_ms = UINT32_MAX - 1500u;
ui.next_batt_chck = UINT32_MAX - 1000u;
ui.loop();
assert(ui.battery_reads == 0 && ui.shutdowns == 0);
now_ms = 100u; // The main loop crossed wrap before the protective poll ran.
ui.loop();
std::printf("low-battery poll across wrap: reads=%u shutdowns=%u next=%u\n",
ui.battery_reads, ui.shutdowns, ui.next_batt_chck);
assert(ui.battery_reads == 1 && ui.shutdowns == 1
&& "overdue protective battery sample must not be postponed another full wrap");
} else if (!std::strcmp(scenario, "battery-wrapped-future")) {
ui.display.on = false;
now_ms = UINT32_MAX - 7999u;
ui.next_batt_chck = now_ms - 1u;
ui.loop();
assert(ui.battery_reads == 1 && ui.next_batt_chck == 0);
++now_ms;
ui.loop();
std::printf("future battery poll wrapped to zero: reads=%u next=%u\n",
ui.battery_reads, ui.next_batt_chck);
assert(ui.battery_reads == 1 && "eight-second battery poll must not run again one ms later");
now_ms = 1u;
ui.loop();
assert(ui.battery_reads == 2 && ui.shutdowns == 0);
} else if (!std::strcmp(scenario, "expire-with-display-off")
|| !std::strcmp(scenario, "expire-during-morse")
|| !std::strcmp(scenario, "expire-during-gesture")) {
now_ms = 10000u;
ui.showAlert("ALERT", 1000);
forceCurrentScreen(ui);
ui.loop();
assert(ui.display.popups == 1);
const unsigned before_frames = ui.display.frames;
now_ms = 11001u;
if (!std::strcmp(scenario, "expire-with-display-off")) ui.display.on = false;
if (!std::strcmp(scenario, "expire-during-morse")) radio_driver.timing = true;
if (!std::strcmp(scenario, "expire-during-gesture")) ui.gesture_pending = true;
ui.loop();
assert(ui.display.frames == before_frames);
std::printf("popup expiry while redraw gated: text_active=%u frames=%u\n",
unsigned(ui._alert[0] != 0), ui.display.frames);
assert(!ui._alert[0] && "expired popup text must be consumed even when redraw is suppressed");
ui.display.on = true;
radio_driver.timing = false;
ui.gesture_pending = false;
now_ms = 0x80002af9u;
forceCurrentScreen(ui);
ui.loop();
assert(ui.display.popups == 1 && "expired popup cannot revive after half a wrap");
}
else if (!std::strcmp(scenario, "alert-across-wrap") || !std::strcmp(scenario, "alert-zero-deadline")) {
const bool zero_deadline = !std::strcmp(scenario, "alert-zero-deadline");
now_ms = zero_deadline ? uint32_t(0u - 1000u) : UINT32_MAX - 500u;
ui.ui_started_at = now_ms - BOOT_SCREEN_MILLIS;
ui.showAlert("ALERT", 1000);
forceCurrentScreen(ui);
ui.loop();
std::printf("new wrapped alert: popups=%u expiry=%u now=%u\n",
ui.display.popups, ui._alert_expiry, now_ms);
assert(ui.display.popups == 1 && "new popup must display before a wrapped expiry, including zero");
now_ms = ui._alert_expiry;
forceCurrentScreen(ui);
ui.loop();
assert(ui.display.popups == 1 && "popup expires at its exact deadline");
} else if (!std::strcmp(scenario, "expired-after-wrap") || !std::strcmp(scenario, "expired-after-halfwrap")) {
now_ms = 10000u;
ui.showAlert("ALERT", 1000);
forceCurrentScreen(ui);
ui.loop();
assert(ui.display.popups == 1);
now_ms = 11001u;
forceCurrentScreen(ui);
ui.loop();
assert(ui.display.popups == 1);
// It was observed expired. A stale expiry must never reactivate it later.
now_ms = !std::strcmp(scenario, "expired-after-wrap") ? 100u : 0x80002af9u;
forceCurrentScreen(ui);
ui.loop();
std::printf("previously expired alert: popups=%u expiry=%u now=%u\n",
ui.display.popups, ui._alert_expiry, now_ms);
assert(ui.display.popups == 1 && "observed expired popup must never reactivate");
}
#endif
else assert(false && "unknown scenario");
std::printf("PASS: %s\n", scenario);
}