mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-09 06:31:35 -04:00
* fix(heltec-t1): sleep the panel on screen-off to stop image retention The T1 is excluded from the LovyanGFX sleep()/wakeup() calls because it uses TFT_eSPI, so DISPLAYOFF only dropped the backlight and the ST7735 kept driving the last frame unlit for the whole screen-off timeout. That constant static image is what burns ghost pixels into the panel. Add opt-in TFT_SLEEP_WHEN_OFF for the TFT_eSPI path: DISPOFF + SLPIN on screen-off, SLPOUT (120 ms) + DISPON on wake, issued as raw MIPI DCS since TFT_eSPI exposes no sleep API. Frame memory survives sleep-in, so the previous frame reappears and the dirty-window diff continues unchanged. The sleep flag keeps the double displayOn() in Screen::handleSetOn() from paying the delay twice. * fix(heltec-t096): sleep the panel on screen-off to stop image retention The T096 sits behind the same TFT_eSPI exclusion as the T1, so DISPLAYOFF only dropped its backlight while the ST7735S kept driving the last frame unlit for the whole screen-off timeout. Same panel, same bus, same burn-in. Opt in to TFT_SLEEP_WHEN_OFF; the TFTDisplay side of the fix is already generic. * fix(tft): harden the TFT_SLEEP_WHEN_OFF wake/sleep sequence Drive VTFT_CTRL LOW before SLPOUT, so the rail is up before the panel is addressed. Wait out the remainder of the 120 ms the controller needs after SLPIN before sending SLPOUT, so a wake landing as the screen timeout fires is not dropped. Guard DISPLAYOFF on panelAsleep to match DISPLAYON. Correct the T1 VTFT_CTRL comment: LOW enables the rail, not HIGH. * fix(tft): wait out only what is left of the sleep-in window before SLPOUT Throttle::isWithinTimespanMs() is true for the whole 120 ms after SLPIN, so the wake path paid a fresh 120 ms on top of however much had already elapsed. A wake 119 ms after the SLPIN waited ~120 ms rather than ~1 ms, up to 119 ms of avoidable latency on every quick off/on. Add Throttle::remainingMs(), which returns what is left of the interval and saturates at 0 instead of underflowing to a ~49 day wait. It reads the clock once, so a caller that tests and then waits cannot be preempted between the two and land on that underflow - which a separate isWithinTimespanMs() plus subtraction at the call site could. heltec-mesh-node-t1 and -t096 both build; neither is board_level = pr, so CI does not compile this path. Docker native suite green, 1515/1515. * trunk --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
294 lines
11 KiB
C++
294 lines
11 KiB
C++
// Unit tests for src/mesh/Throttle.{h,cpp} - the firmware's elapsed-time and deadline helpers.
|
|
//
|
|
// These drive the injected clock across the 32-bit millis() wrap, which is not otherwise reachable
|
|
// in a test, and which every caller of these helpers depends on being handled correctly.
|
|
#include "Arduino.h"
|
|
#include "TestUtil.h"
|
|
#include "UptimeClock.h"
|
|
#include "mesh/Throttle.h"
|
|
#include <cstdint>
|
|
#include <unity.h>
|
|
|
|
void setUp(void) {}
|
|
void tearDown(void)
|
|
{
|
|
Time::useRealClock(); // don't leak the fake clock into other suites
|
|
}
|
|
|
|
// --- basic window semantics ---
|
|
|
|
void test_isWithinTimespan_true_inside_window()
|
|
{
|
|
Time::setTestMillis(10000);
|
|
TEST_ASSERT_TRUE(Throttle::isWithinTimespanMs(9500, 1000)); // 500ms elapsed of a 1000ms window
|
|
}
|
|
|
|
void test_isWithinTimespan_false_outside_window()
|
|
{
|
|
Time::setTestMillis(10000);
|
|
TEST_ASSERT_FALSE(Throttle::isWithinTimespanMs(8000, 1000)); // 2000ms elapsed
|
|
}
|
|
|
|
// The boundary is exclusive: elapsed == interval is NOT "within".
|
|
void test_isWithinTimespan_boundary_is_exclusive()
|
|
{
|
|
Time::setTestMillis(10000);
|
|
TEST_ASSERT_FALSE(Throttle::isWithinTimespanMs(9000, 1000)); // exactly 1000ms elapsed
|
|
TEST_ASSERT_TRUE(Throttle::isWithinTimespanMs(9001, 1000)); // 999ms elapsed
|
|
}
|
|
|
|
// --- hasElapsed is the exact complement ---
|
|
|
|
void test_hasElapsed_is_complement_of_isWithinTimespan()
|
|
{
|
|
Time::setTestMillis(10000);
|
|
const uint32_t cases[][2] = {{9500, 1000}, {8000, 1000}, {9000, 1000}, {10000, 1}, {0, 5000}};
|
|
for (auto &c : cases) {
|
|
TEST_ASSERT_EQUAL(!Throttle::isWithinTimespanMs(c[0], c[1]), Throttle::hasElapsed(c[0], c[1]));
|
|
}
|
|
}
|
|
|
|
void test_hasElapsed_boundary_is_inclusive()
|
|
{
|
|
Time::setTestMillis(10000);
|
|
TEST_ASSERT_TRUE(Throttle::hasElapsed(9000, 1000)); // exactly 1000ms elapsed
|
|
TEST_ASSERT_FALSE(Throttle::hasElapsed(9001, 1000)); // 999ms elapsed
|
|
}
|
|
|
|
// --- remainingMs() ---
|
|
|
|
// The point of the helper: what is left of the window, not the whole window. A caller that waits
|
|
// out the remainder must not pay again for time that has already gone by.
|
|
void test_remainingMs_returns_only_what_is_left()
|
|
{
|
|
Time::setTestMillis(10000);
|
|
TEST_ASSERT_EQUAL_UINT32(500, Throttle::remainingMs(9500, 1000)); // 500ms elapsed of a 1000ms window
|
|
TEST_ASSERT_EQUAL_UINT32(1, Throttle::remainingMs(9001, 1000)); // 999ms elapsed
|
|
TEST_ASSERT_EQUAL_UINT32(1000, Throttle::remainingMs(10000, 1000)); // nothing elapsed yet
|
|
}
|
|
|
|
// Saturates at 0 rather than underflowing to a ~49 day wait, which is what a bare
|
|
// intervalMs - elapsed would produce once the interval has passed.
|
|
void test_remainingMs_saturates_at_zero_once_elapsed()
|
|
{
|
|
Time::setTestMillis(10000);
|
|
TEST_ASSERT_EQUAL_UINT32(0, Throttle::remainingMs(9000, 1000)); // exactly the interval
|
|
TEST_ASSERT_EQUAL_UINT32(0, Throttle::remainingMs(8000, 1000)); // well past it
|
|
TEST_ASSERT_EQUAL_UINT32(0, Throttle::remainingMs(0, 1000));
|
|
}
|
|
|
|
// Nonzero exactly while isWithinTimespanMs() is true, so `if (remainingMs(...))` is a drop-in for
|
|
// the predicate at a call site that then waits out the rest.
|
|
void test_remainingMs_is_nonzero_exactly_within_the_window()
|
|
{
|
|
Time::setTestMillis(10000);
|
|
const uint32_t cases[][2] = {{9500, 1000}, {8000, 1000}, {9000, 1000}, {9001, 1000}, {10000, 1}, {0, 5000}};
|
|
for (auto &c : cases) {
|
|
TEST_ASSERT_EQUAL(Throttle::isWithinTimespanMs(c[0], c[1]), Throttle::remainingMs(c[0], c[1]) != 0);
|
|
}
|
|
}
|
|
|
|
void test_remainingMs_survives_millis_wrap()
|
|
{
|
|
const uint32_t lastRun = 0xFFFFFF00u; // 256ms before the wrap
|
|
Time::setTestMillis(lastRun);
|
|
|
|
Time::advanceTestMillis(100); // still before the wrap
|
|
TEST_ASSERT_EQUAL_UINT32(900, Throttle::remainingMs(lastRun, 1000));
|
|
|
|
Time::advanceTestMillis(200); // wraps to 0x0000002C - 300ms elapsed in total
|
|
TEST_ASSERT_EQUAL_UINT32(700, Throttle::remainingMs(lastRun, 1000));
|
|
|
|
Time::advanceTestMillis(800); // 1100ms elapsed, past both the window and the wrap
|
|
TEST_ASSERT_EQUAL_UINT32(0, Throttle::remainingMs(lastRun, 1000));
|
|
}
|
|
|
|
// --- rollover: the headline property ---
|
|
|
|
// A window opened just before the 32-bit wrap must still close correctly after it.
|
|
void test_isWithinTimespan_survives_millis_wrap()
|
|
{
|
|
const uint32_t lastRun = 0xFFFFFF00u; // 256ms before the wrap
|
|
Time::setTestMillis(lastRun);
|
|
|
|
Time::advanceTestMillis(100); // 0xFFFFFF64 - still before the wrap
|
|
TEST_ASSERT_TRUE(Throttle::isWithinTimespanMs(lastRun, 1000));
|
|
|
|
Time::advanceTestMillis(200); // wraps to 0x0000002C - 300ms elapsed in total
|
|
TEST_ASSERT_TRUE(Throttle::isWithinTimespanMs(lastRun, 1000));
|
|
TEST_ASSERT_FALSE(Throttle::hasElapsed(lastRun, 1000));
|
|
|
|
Time::advanceTestMillis(800); // 1100ms elapsed in total, well past the wrap
|
|
TEST_ASSERT_FALSE(Throttle::isWithinTimespanMs(lastRun, 1000));
|
|
TEST_ASSERT_TRUE(Throttle::hasElapsed(lastRun, 1000));
|
|
}
|
|
|
|
// The long-interval end of the range: a 24h window (the longest in the tree) across the wrap.
|
|
void test_long_interval_survives_wrap()
|
|
{
|
|
const uint32_t dayMs = 24u * 60u * 60u * 1000u; // 86,400,000
|
|
const uint32_t lastRun = 0xFFFFFF00u;
|
|
Time::setTestMillis(lastRun);
|
|
|
|
Time::advanceTestMillis(dayMs - 1);
|
|
TEST_ASSERT_TRUE(Throttle::isWithinTimespanMs(lastRun, dayMs));
|
|
|
|
Time::advanceTestMillis(1); // exactly one day elapsed
|
|
TEST_ASSERT_TRUE(Throttle::hasElapsed(lastRun, dayMs));
|
|
}
|
|
|
|
// --- deadlinePassed() ---
|
|
|
|
void test_deadlinePassed_basic()
|
|
{
|
|
Time::setTestMillis(10000);
|
|
TEST_ASSERT_FALSE(Throttle::deadlinePassed(10001)); // 1ms in the future
|
|
TEST_ASSERT_TRUE(Throttle::deadlinePassed(10000)); // exactly now counts as passed
|
|
TEST_ASSERT_TRUE(Throttle::deadlinePassed(9999)); // 1ms in the past
|
|
}
|
|
|
|
// The property the naive `millis() > deadline` compare fails: a deadline set before the wrap must
|
|
// fire once, and only once, after the wrap.
|
|
void test_deadlinePassed_survives_millis_wrap()
|
|
{
|
|
Time::setTestMillis(0xFFFFFF00u); // 256ms before the wrap
|
|
const uint32_t deadline = 0xFFFFFF00u + 500;
|
|
|
|
TEST_ASSERT_FALSE(Throttle::deadlinePassed(deadline)); // not yet
|
|
Time::advanceTestMillis(400); // 0x00000090 - wrapped, still not due
|
|
TEST_ASSERT_FALSE(Throttle::deadlinePassed(deadline));
|
|
Time::advanceTestMillis(100); // exactly due, past the wrap
|
|
TEST_ASSERT_TRUE(Throttle::deadlinePassed(deadline));
|
|
Time::advanceTestMillis(60000); // stays passed
|
|
TEST_ASSERT_TRUE(Throttle::deadlinePassed(deadline));
|
|
}
|
|
|
|
// The naive compare's actual failure mode, pinned so a regression is unmistakable: before the wrap
|
|
// the deadline is numerically smaller than now, so `millis() > deadline` would fire it early.
|
|
void test_deadlinePassed_does_not_fire_early_when_deadline_wraps()
|
|
{
|
|
Time::setTestMillis(0xFFFFFF00u);
|
|
const uint32_t deadline = 0xFFFFFF00u + 1000; // wraps to 0x000002E8
|
|
|
|
TEST_ASSERT_TRUE(deadline < Time::getMillis()); // the naive compare would fire here
|
|
TEST_ASSERT_FALSE(Throttle::deadlinePassed(deadline));
|
|
}
|
|
|
|
// deadlinePassedAt() judges against a caller-supplied now, so a loop that snapshots the clock once
|
|
// gets one instant for every entry - including across the wrap, where the clock has moved on.
|
|
void test_deadlinePassedAt_uses_the_supplied_now()
|
|
{
|
|
Time::setTestMillis(0xFFFFFF00u);
|
|
const uint32_t now = Time::getMillis();
|
|
const uint32_t deadline = 0xFFFFFF00u + 500; // wraps to 0x000000F4
|
|
|
|
TEST_ASSERT_FALSE(Throttle::deadlinePassedAt(now, deadline));
|
|
TEST_ASSERT_TRUE(Throttle::deadlinePassedAt(deadline, deadline)); // inclusive boundary
|
|
TEST_ASSERT_TRUE(Throttle::deadlinePassedAt(deadline + 1, deadline)); // past the wrap
|
|
Time::advanceTestMillis(60000); // clock moved, snapshot did not
|
|
TEST_ASSERT_FALSE(Throttle::deadlinePassedAt(now, deadline));
|
|
TEST_ASSERT_TRUE(Throttle::deadlinePassed(deadline));
|
|
}
|
|
|
|
// deadlinePassed() cannot know about sentinels, so it reports them as passed. This pins that
|
|
// contract, since callers relying on it must test armed-ness first.
|
|
void test_deadlinePassed_reads_disarmed_sentinels_as_passed()
|
|
{
|
|
Time::setTestMillis(6247);
|
|
|
|
TEST_ASSERT_TRUE(Throttle::deadlinePassed(0)); // "inactive" for rebootAtMsec et al
|
|
|
|
// UINT32_MAX is not a usable "far future" either - at a low uptime it is a hair BEHIND now, so
|
|
// it reads as passed like any other past value. ExternalNotificationModule used to reserve it
|
|
// for "unarmed" and now keeps that state in its isNagging flag instead.
|
|
TEST_ASSERT_TRUE(Throttle::deadlinePassed(UINT32_MAX));
|
|
|
|
// The guarded form every caller must use.
|
|
const uint32_t disarmed = 0;
|
|
TEST_ASSERT_FALSE(disarmed && Throttle::deadlinePassed(disarmed));
|
|
|
|
// And it still holds after a wrap.
|
|
Time::setTestMillis(0xFFFFFF00u);
|
|
Time::advanceTestMillis(1000);
|
|
TEST_ASSERT_FALSE(disarmed && Throttle::deadlinePassed(disarmed));
|
|
}
|
|
|
|
// --- execute() ---
|
|
|
|
static int executeCount = 0;
|
|
static int deferCount = 0;
|
|
static void countExecute()
|
|
{
|
|
executeCount++;
|
|
}
|
|
static void countDefer()
|
|
{
|
|
deferCount++;
|
|
}
|
|
|
|
void test_execute_runs_first_time_then_throttles()
|
|
{
|
|
executeCount = 0;
|
|
deferCount = 0;
|
|
Time::setTestMillis(5000);
|
|
|
|
uint32_t last = 0; // 0 means "never run" to execute()
|
|
TEST_ASSERT_TRUE(Throttle::execute(&last, 1000, countExecute, countDefer));
|
|
TEST_ASSERT_EQUAL(1, executeCount);
|
|
|
|
// Immediately again: deferred.
|
|
TEST_ASSERT_FALSE(Throttle::execute(&last, 1000, countExecute, countDefer));
|
|
TEST_ASSERT_EQUAL(1, executeCount);
|
|
TEST_ASSERT_EQUAL(1, deferCount);
|
|
|
|
// After the interval: runs again.
|
|
Time::advanceTestMillis(1000);
|
|
TEST_ASSERT_TRUE(Throttle::execute(&last, 1000, countExecute, countDefer));
|
|
TEST_ASSERT_EQUAL(2, executeCount);
|
|
}
|
|
|
|
void test_execute_survives_millis_wrap()
|
|
{
|
|
executeCount = 0;
|
|
Time::setTestMillis(0xFFFFFF00u);
|
|
|
|
uint32_t last = 0;
|
|
TEST_ASSERT_TRUE(Throttle::execute(&last, 1000, countExecute)); // arms at 0xFFFFFF00
|
|
TEST_ASSERT_EQUAL(1, executeCount);
|
|
|
|
Time::advanceTestMillis(500); // wraps past 0
|
|
TEST_ASSERT_FALSE(Throttle::execute(&last, 1000, countExecute)); // not due yet
|
|
TEST_ASSERT_EQUAL(1, executeCount);
|
|
|
|
Time::advanceTestMillis(600); // 1100ms total
|
|
TEST_ASSERT_TRUE(Throttle::execute(&last, 1000, countExecute));
|
|
TEST_ASSERT_EQUAL(2, executeCount);
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_isWithinTimespan_true_inside_window);
|
|
RUN_TEST(test_isWithinTimespan_false_outside_window);
|
|
RUN_TEST(test_isWithinTimespan_boundary_is_exclusive);
|
|
RUN_TEST(test_hasElapsed_is_complement_of_isWithinTimespan);
|
|
RUN_TEST(test_hasElapsed_boundary_is_inclusive);
|
|
RUN_TEST(test_remainingMs_returns_only_what_is_left);
|
|
RUN_TEST(test_remainingMs_saturates_at_zero_once_elapsed);
|
|
RUN_TEST(test_remainingMs_is_nonzero_exactly_within_the_window);
|
|
RUN_TEST(test_remainingMs_survives_millis_wrap);
|
|
RUN_TEST(test_isWithinTimespan_survives_millis_wrap);
|
|
RUN_TEST(test_long_interval_survives_wrap);
|
|
RUN_TEST(test_deadlinePassed_basic);
|
|
RUN_TEST(test_deadlinePassed_survives_millis_wrap);
|
|
RUN_TEST(test_deadlinePassed_does_not_fire_early_when_deadline_wraps);
|
|
RUN_TEST(test_deadlinePassedAt_uses_the_supplied_now);
|
|
RUN_TEST(test_deadlinePassed_reads_disarmed_sentinels_as_passed);
|
|
RUN_TEST(test_execute_runs_first_time_then_throttles);
|
|
RUN_TEST(test_execute_survives_millis_wrap);
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|