2026-10-13 00:56:32 +02:00
|
|
|
// Tests for the ESP32 logic (src/esp32/logic), run on the PC: pio test -e test_esp32
|
|
|
|
|
//
|
|
|
|
|
// The display controller is tested with a fake power switch and fake frames that record
|
|
|
|
|
// what the controller asks for.
|
|
|
|
|
|
|
|
|
|
#include <unity.h>
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cstdio>
|
2026-10-13 01:21:04 +02:00
|
|
|
#include <cstring>
|
2026-10-13 00:56:32 +02:00
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
#include "config.h"
|
2026-10-13 01:52:34 +02:00
|
|
|
#include "logic/bitmap_format.h"
|
2026-10-13 01:05:06 +02:00
|
|
|
#include "logic/boot_policy.h"
|
2026-10-13 00:56:32 +02:00
|
|
|
#include "logic/display_controller.h"
|
|
|
|
|
#include "logic/power_timer.h"
|
|
|
|
|
#include "logic/schedule.h"
|
2026-10-13 01:52:34 +02:00
|
|
|
#include "logic/settings.h"
|
2026-10-13 00:56:32 +02:00
|
|
|
#include "logic/status_led.h"
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Fakes
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class FakePower : public DisplayPower
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
bool is_on = false;
|
|
|
|
|
int on_count = 0;
|
|
|
|
|
int off_count = 0;
|
|
|
|
|
|
|
|
|
|
bool isOn() override { return is_on; }
|
|
|
|
|
void on() override
|
|
|
|
|
{
|
|
|
|
|
is_on = true;
|
|
|
|
|
on_count++;
|
|
|
|
|
}
|
|
|
|
|
void off() override
|
|
|
|
|
{
|
|
|
|
|
is_on = false;
|
|
|
|
|
off_count++;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class FakeFrames : public Frames
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
std::vector<std::string> shown;
|
|
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
void clock(int hour, int minute, bool evening) override
|
2026-10-13 00:56:32 +02:00
|
|
|
{
|
2026-10-13 01:52:34 +02:00
|
|
|
char s[24];
|
|
|
|
|
snprintf(s, sizeof(s), "clock %02d:%02d%s", hour, minute, evening ? " evening" : "");
|
2026-10-13 00:56:32 +02:00
|
|
|
shown.push_back(s);
|
|
|
|
|
}
|
2026-10-13 01:52:34 +02:00
|
|
|
void page() override { shown.push_back("page"); }
|
|
|
|
|
void live() override { shown.push_back("live"); }
|
2026-10-13 01:34:04 +02:00
|
|
|
void noWifi(int hour, int minute) override
|
|
|
|
|
{
|
|
|
|
|
char s[16];
|
|
|
|
|
snprintf(s, sizeof(s), "nowifi %02d:%02d", hour, minute);
|
|
|
|
|
shown.push_back(s);
|
|
|
|
|
}
|
2026-10-15 01:29:01 +02:00
|
|
|
void off() override { shown.push_back("off"); }
|
2026-10-13 00:56:32 +02:00
|
|
|
|
|
|
|
|
std::string last() const { return shown.empty() ? "" : shown.back(); }
|
|
|
|
|
};
|
|
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
static const Settings defaults;
|
|
|
|
|
|
2026-10-13 00:56:32 +02:00
|
|
|
static FakePower power;
|
|
|
|
|
static FakeFrames frames;
|
|
|
|
|
static DisplayController *controller;
|
|
|
|
|
static uint32_t now;
|
2026-10-13 01:34:04 +02:00
|
|
|
static bool wifi;
|
2026-10-13 00:56:32 +02:00
|
|
|
|
|
|
|
|
void setUp()
|
|
|
|
|
{
|
|
|
|
|
power = FakePower();
|
|
|
|
|
frames = FakeFrames();
|
|
|
|
|
delete controller;
|
|
|
|
|
controller = new DisplayController(power, frames);
|
|
|
|
|
now = 1000;
|
2026-10-13 01:34:04 +02:00
|
|
|
wifi = true;
|
2026-10-13 00:56:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void tearDown() {}
|
|
|
|
|
|
|
|
|
|
// Run the controller at a time of day; `ms` passes in steps like the real loop
|
|
|
|
|
static void run(int hour, int minute, uint32_t ms = 0)
|
|
|
|
|
{
|
|
|
|
|
uint32_t end = now + ms;
|
|
|
|
|
do
|
|
|
|
|
{
|
2026-10-13 01:34:04 +02:00
|
|
|
controller->update(now, true, hour, minute, wifi);
|
2026-10-13 00:56:32 +02:00
|
|
|
now += 100;
|
|
|
|
|
} while ((int32_t)(end - now) >= 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Schedule
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void test_schedule_night_until_8()
|
|
|
|
|
{
|
2026-10-13 01:52:34 +02:00
|
|
|
TEST_ASSERT_TRUE(scheduleFor(0, 0, defaults.schedule) == ScheduleMode::Night);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(7, 0, defaults.schedule) == ScheduleMode::Night);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(7, 59, defaults.schedule) == ScheduleMode::Night);
|
2026-10-13 00:56:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_schedule_clock_from_8_to_22()
|
|
|
|
|
{
|
2026-10-13 01:52:34 +02:00
|
|
|
TEST_ASSERT_TRUE(scheduleFor(8, 0, defaults.schedule) == ScheduleMode::Clock);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(12, 34, defaults.schedule) == ScheduleMode::Clock);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(21, 59, defaults.schedule) == ScheduleMode::Clock);
|
2026-10-13 00:56:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_schedule_quarter_hours_until_23_30()
|
|
|
|
|
{
|
2026-10-13 01:52:34 +02:00
|
|
|
TEST_ASSERT_TRUE(scheduleFor(22, 0, defaults.schedule) == ScheduleMode::Clock);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(22, 1, defaults.schedule) == ScheduleMode::Standby);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(22, 14, defaults.schedule) == ScheduleMode::Standby);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(22, 15, defaults.schedule) == ScheduleMode::Clock);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(22, 45, defaults.schedule) == ScheduleMode::Clock);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(23, 0, defaults.schedule) == ScheduleMode::Clock);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(23, 29, defaults.schedule) == ScheduleMode::Standby);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(23, 30, defaults.schedule) == ScheduleMode::Clock);
|
2026-10-13 00:56:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_schedule_night_from_23_31()
|
|
|
|
|
{
|
2026-10-13 01:52:34 +02:00
|
|
|
TEST_ASSERT_TRUE(scheduleFor(23, 31, defaults.schedule) == ScheduleMode::Night);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(23, 45, defaults.schedule) == ScheduleMode::Night);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(23, 59, defaults.schedule) == ScheduleMode::Night);
|
2026-10-13 00:56:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_evening_from_17()
|
|
|
|
|
{
|
2026-10-13 01:52:34 +02:00
|
|
|
TEST_ASSERT_FALSE(isEvening(16, 59, defaults.clock));
|
|
|
|
|
TEST_ASSERT_TRUE(isEvening(17, 0, defaults.clock));
|
|
|
|
|
TEST_ASSERT_TRUE(isEvening(23, 0, defaults.clock));
|
2026-10-13 00:56:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Power timer
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void test_timer_expires_once_after_delay()
|
|
|
|
|
{
|
|
|
|
|
PowerTimer t;
|
|
|
|
|
t.request(1000, 500);
|
|
|
|
|
TEST_ASSERT_FALSE(t.expired(1499));
|
|
|
|
|
TEST_ASSERT_TRUE(t.expired(1500));
|
|
|
|
|
TEST_ASSERT_FALSE(t.expired(1600));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_timer_keeps_earlier_deadline()
|
|
|
|
|
{
|
|
|
|
|
PowerTimer t;
|
|
|
|
|
t.request(0, 1000);
|
|
|
|
|
t.request(500, 1000); // later deadline: ignored
|
|
|
|
|
TEST_ASSERT_TRUE(t.expired(1000));
|
|
|
|
|
|
|
|
|
|
t.request(0, 30000);
|
|
|
|
|
t.request(100, 1000); // earlier deadline: used
|
|
|
|
|
TEST_ASSERT_TRUE(t.expired(1100));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_timer_cancel()
|
|
|
|
|
{
|
|
|
|
|
PowerTimer t;
|
|
|
|
|
t.request(0, 100);
|
|
|
|
|
t.cancel();
|
|
|
|
|
TEST_ASSERT_FALSE(t.pending());
|
|
|
|
|
TEST_ASSERT_FALSE(t.expired(1000));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_timer_across_millis_overflow()
|
|
|
|
|
{
|
|
|
|
|
PowerTimer t;
|
|
|
|
|
t.request(0xFFFFFF00u, 0x200); // deadline wraps past 0
|
|
|
|
|
TEST_ASSERT_FALSE(t.expired(0xFFFFFFFFu));
|
|
|
|
|
TEST_ASSERT_FALSE(t.expired(0x00000050u));
|
|
|
|
|
TEST_ASSERT_TRUE(t.expired(0x00000100u));
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:05:06 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Boot policy (keeping OTA alive)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void test_power_on_starts_normally_and_clears_crashes()
|
|
|
|
|
{
|
|
|
|
|
BootDecision d = decideBoot(ResetKind::PowerOn, 2);
|
|
|
|
|
TEST_ASSERT_TRUE(d.mode == BootMode::Normal);
|
|
|
|
|
TEST_ASSERT_EQUAL(0, d.crash_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_software_reset_starts_normally_and_keeps_count()
|
|
|
|
|
{
|
|
|
|
|
// e.g. the restart after an OTA update
|
|
|
|
|
BootDecision d = decideBoot(ResetKind::Software, 1);
|
|
|
|
|
TEST_ASSERT_TRUE(d.mode == BootMode::Normal);
|
|
|
|
|
TEST_ASSERT_EQUAL(1, d.crash_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_other_reset_starts_normally_and_keeps_count()
|
|
|
|
|
{
|
|
|
|
|
BootDecision d = decideBoot(ResetKind::Other, 2);
|
|
|
|
|
TEST_ASSERT_TRUE(d.mode == BootMode::Normal);
|
|
|
|
|
TEST_ASSERT_EQUAL(2, d.crash_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_first_crashes_give_a_panic_window()
|
|
|
|
|
{
|
|
|
|
|
BootDecision d = decideBoot(ResetKind::Crash, 0);
|
|
|
|
|
TEST_ASSERT_TRUE(d.mode == BootMode::PanicWindow);
|
|
|
|
|
TEST_ASSERT_EQUAL(1, d.crash_count);
|
|
|
|
|
d = decideBoot(ResetKind::Crash, d.crash_count);
|
|
|
|
|
TEST_ASSERT_TRUE(d.mode == BootMode::PanicWindow);
|
|
|
|
|
TEST_ASSERT_EQUAL(2, d.crash_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_repeated_crashes_give_safe_mode()
|
|
|
|
|
{
|
|
|
|
|
uint8_t count = 0;
|
|
|
|
|
BootDecision d = {BootMode::Normal, 0};
|
|
|
|
|
for (int i = 0; i < SAFE_MODE_CRASHES; i++)
|
|
|
|
|
{
|
|
|
|
|
d = decideBoot(ResetKind::Crash, count);
|
|
|
|
|
count = d.crash_count;
|
|
|
|
|
}
|
|
|
|
|
TEST_ASSERT_TRUE(d.mode == BootMode::SafeMode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_after_safe_mode_one_more_crash_gives_safe_mode_again()
|
|
|
|
|
{
|
|
|
|
|
uint8_t count = crashCountAfterSafeMode();
|
|
|
|
|
TEST_ASSERT_TRUE(decideBoot(ResetKind::Software, count).mode == BootMode::Normal);
|
|
|
|
|
TEST_ASSERT_TRUE(decideBoot(ResetKind::Crash, count).mode == BootMode::SafeMode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_crash_count_does_not_overflow()
|
|
|
|
|
{
|
|
|
|
|
BootDecision d = decideBoot(ResetKind::Crash, 255);
|
|
|
|
|
TEST_ASSERT_EQUAL(255, d.crash_count);
|
|
|
|
|
TEST_ASSERT_TRUE(d.mode == BootMode::SafeMode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_new_firmware_needs_time_and_wifi_to_be_accepted()
|
|
|
|
|
{
|
|
|
|
|
TEST_ASSERT_FALSE(shouldMarkFirmwareValid(FIRMWARE_VALID_TIME - 1, true));
|
|
|
|
|
TEST_ASSERT_FALSE(shouldMarkFirmwareValid(FIRMWARE_VALID_TIME, false));
|
|
|
|
|
TEST_ASSERT_TRUE(shouldMarkFirmwareValid(FIRMWARE_VALID_TIME, true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_crash_count_cleared_after_running_long_enough()
|
|
|
|
|
{
|
|
|
|
|
TEST_ASSERT_FALSE(shouldClearCrashCount(CRASH_FREE_TIME - 1));
|
|
|
|
|
TEST_ASSERT_TRUE(shouldClearCrashCount(CRASH_FREE_TIME));
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:21:04 +02:00
|
|
|
void test_quick_power_cycles_request_recovery()
|
|
|
|
|
{
|
|
|
|
|
for (uint8_t count = 0; count < POWER_CYCLES_FOR_RECOVERY; count++)
|
|
|
|
|
TEST_ASSERT_FALSE(powerCyclesRequestRecovery(count));
|
|
|
|
|
TEST_ASSERT_TRUE(powerCyclesRequestRecovery(POWER_CYCLES_FOR_RECOVERY));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_recovery_for_wifi_starts_clock_when_wifi_works()
|
|
|
|
|
{
|
|
|
|
|
TEST_ASSERT_FALSE(recoveryShouldStartMain(RecoveryReason::Wifi, 0, false, true));
|
|
|
|
|
TEST_ASSERT_FALSE(recoveryShouldStartMain(RecoveryReason::Wifi, RECOVERY_TIME * 10, false, true));
|
|
|
|
|
TEST_ASSERT_FALSE(recoveryShouldStartMain(RecoveryReason::Wifi, RECOVERY_MIN_TIME - 1, true, true));
|
|
|
|
|
TEST_ASSERT_TRUE(recoveryShouldStartMain(RecoveryReason::Wifi, RECOVERY_MIN_TIME, true, true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_recovery_for_other_reasons_waits_recovery_time()
|
|
|
|
|
{
|
|
|
|
|
const RecoveryReason reasons[] = {RecoveryReason::Crash, RecoveryReason::Manual,
|
|
|
|
|
RecoveryReason::PowerCycle, RecoveryReason::None};
|
|
|
|
|
for (RecoveryReason reason : reasons)
|
|
|
|
|
{
|
|
|
|
|
TEST_ASSERT_FALSE(recoveryShouldStartMain(reason, RECOVERY_TIME - 1, true, true));
|
|
|
|
|
TEST_ASSERT_TRUE(recoveryShouldStartMain(reason, RECOVERY_TIME, true, true));
|
|
|
|
|
// The time counts, not the WiFi: an OTA upload over the setup WiFi is possible
|
|
|
|
|
TEST_ASSERT_TRUE(recoveryShouldStartMain(reason, RECOVERY_TIME, false, true));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_recovery_never_starts_missing_clock_firmware()
|
|
|
|
|
{
|
|
|
|
|
TEST_ASSERT_FALSE(recoveryShouldStartMain(RecoveryReason::Wifi, RECOVERY_TIME, true, false));
|
|
|
|
|
TEST_ASSERT_FALSE(recoveryShouldStartMain(RecoveryReason::Manual, RECOVERY_TIME, true, false));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_every_recovery_reason_has_a_text()
|
|
|
|
|
{
|
|
|
|
|
for (uint8_t r = 0; r <= (uint8_t)RecoveryReason::PowerCycle; r++)
|
2026-10-13 01:34:04 +02:00
|
|
|
{
|
2026-10-13 01:21:04 +02:00
|
|
|
TEST_ASSERT_TRUE(strlen(recoveryReasonText((RecoveryReason)r)) > 0);
|
2026-10-13 01:34:04 +02:00
|
|
|
// fits on one line of the display
|
|
|
|
|
TEST_ASSERT_TRUE(strlen(recoveryReasonShort((RecoveryReason)r)) > 0);
|
|
|
|
|
TEST_ASSERT_TRUE(strlen(recoveryReasonShort((RecoveryReason)r)) <= 14);
|
|
|
|
|
}
|
2026-10-13 01:21:04 +02:00
|
|
|
TEST_ASSERT_TRUE(strcmp(recoveryReasonText(RecoveryReason::Wifi),
|
|
|
|
|
recoveryReasonText(RecoveryReason::Crash)) != 0);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 00:56:32 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Status LED
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void test_led_off_while_display_off()
|
|
|
|
|
{
|
|
|
|
|
for (uint32_t t = 0; t < 2000; t += 50)
|
|
|
|
|
TEST_ASSERT_FALSE(statusLedLevel(t, true, false));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_led_blinks_slow_with_wifi_and_fast_without()
|
|
|
|
|
{
|
|
|
|
|
TEST_ASSERT_TRUE(statusLedLevel(0, true, true));
|
|
|
|
|
TEST_ASSERT_TRUE(statusLedLevel(499, true, true));
|
|
|
|
|
TEST_ASSERT_FALSE(statusLedLevel(500, true, true));
|
|
|
|
|
|
|
|
|
|
TEST_ASSERT_TRUE(statusLedLevel(0, false, true));
|
|
|
|
|
TEST_ASSERT_FALSE(statusLedLevel(100, false, true));
|
|
|
|
|
TEST_ASSERT_TRUE(statusLedLevel(200, false, true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Display controller
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void test_nothing_happens_until_time_is_synced()
|
|
|
|
|
{
|
|
|
|
|
power.is_on = true; // showing "Connecting..."
|
2026-10-13 01:34:04 +02:00
|
|
|
controller->update(now, false, 0, 0, true);
|
2026-10-13 00:56:32 +02:00
|
|
|
TEST_ASSERT_EQUAL(0, power.off_count);
|
|
|
|
|
TEST_ASSERT_EQUAL(0, (int)frames.shown.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_daytime_powers_on_and_shows_clock()
|
|
|
|
|
{
|
|
|
|
|
run(12, 34);
|
|
|
|
|
TEST_ASSERT_TRUE(power.is_on);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 12:34", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_clock_is_only_sent_when_the_minute_changes()
|
|
|
|
|
{
|
|
|
|
|
run(12, 34, 5000);
|
|
|
|
|
TEST_ASSERT_EQUAL(1, (int)frames.shown.size());
|
|
|
|
|
run(12, 35);
|
|
|
|
|
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 12:35", frames.last().c_str());
|
|
|
|
|
TEST_ASSERT_EQUAL(1, power.on_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_night_clears_then_powers_off_after_delay()
|
|
|
|
|
{
|
|
|
|
|
run(23, 30);
|
|
|
|
|
run(23, 31, POWER_OFF_DELAY - 200);
|
2026-10-15 01:29:01 +02:00
|
|
|
TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str());
|
2026-10-13 00:56:32 +02:00
|
|
|
TEST_ASSERT_TRUE(power.is_on);
|
|
|
|
|
run(23, 31, 300);
|
|
|
|
|
TEST_ASSERT_FALSE(power.is_on);
|
|
|
|
|
TEST_ASSERT_EQUAL(1, power.off_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_night_does_not_power_on()
|
|
|
|
|
{
|
|
|
|
|
run(3, 0, 60000);
|
|
|
|
|
TEST_ASSERT_EQUAL(0, power.on_count);
|
|
|
|
|
TEST_ASSERT_EQUAL(0, (int)frames.shown.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_standby_keeps_the_last_time_and_powers_off()
|
|
|
|
|
{
|
|
|
|
|
run(22, 0);
|
|
|
|
|
run(22, 1, POWER_OFF_DELAY + 200);
|
|
|
|
|
TEST_ASSERT_FALSE(power.is_on);
|
2026-10-15 01:29:01 +02:00
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 22:00 evening", frames.last().c_str()); // no off screen
|
2026-10-13 00:56:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_quarter_hour_powers_on_again_and_redraws()
|
|
|
|
|
{
|
|
|
|
|
run(22, 1, POWER_OFF_DELAY + 200);
|
|
|
|
|
run(22, 14);
|
|
|
|
|
TEST_ASSERT_FALSE(power.is_on);
|
|
|
|
|
run(22, 15);
|
|
|
|
|
TEST_ASSERT_TRUE(power.is_on);
|
2026-10-13 01:52:34 +02:00
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 22:15 evening", frames.last().c_str());
|
2026-10-13 00:56:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_clock_cancels_pending_power_off()
|
|
|
|
|
{
|
|
|
|
|
run(22, 0);
|
|
|
|
|
run(22, 14, 10000); // power off countdown running
|
|
|
|
|
TEST_ASSERT_TRUE(power.is_on);
|
|
|
|
|
run(22, 15, POWER_OFF_DELAY);
|
|
|
|
|
TEST_ASSERT_TRUE(power.is_on);
|
|
|
|
|
TEST_ASSERT_EQUAL(0, power.off_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_power_switch_off_clears_and_powers_off_quickly()
|
|
|
|
|
{
|
|
|
|
|
run(12, 0);
|
|
|
|
|
run(22, 14, 1000); // 30 s countdown already running
|
|
|
|
|
controller->setPowerEnabled(false);
|
|
|
|
|
run(22, 14, CLEAR_BEFORE_OFF_TIME + 200);
|
2026-10-15 01:29:01 +02:00
|
|
|
TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str());
|
|
|
|
|
TEST_ASSERT_FALSE(power.is_on);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_power_switch_off_waits_for_the_refresh_effect()
|
|
|
|
|
{
|
|
|
|
|
controller->setEffectTime(3000);
|
|
|
|
|
run(12, 0);
|
|
|
|
|
controller->setPowerEnabled(false);
|
|
|
|
|
run(12, 0, CLEAR_BEFORE_OFF_TIME + 2000);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str());
|
|
|
|
|
TEST_ASSERT_TRUE(power.is_on); // the effect is still flipping dots
|
|
|
|
|
run(12, 0, 1400);
|
2026-10-13 00:56:32 +02:00
|
|
|
TEST_ASSERT_FALSE(power.is_on);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_power_switch_off_keeps_display_off_during_the_day()
|
|
|
|
|
{
|
|
|
|
|
controller->setPowerEnabled(false);
|
|
|
|
|
run(12, 0, 60000);
|
|
|
|
|
TEST_ASSERT_EQUAL(0, power.on_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_power_switch_on_again_shows_clock()
|
|
|
|
|
{
|
|
|
|
|
run(12, 0);
|
|
|
|
|
controller->setPowerEnabled(false);
|
|
|
|
|
run(12, 0, CLEAR_BEFORE_OFF_TIME + 200);
|
|
|
|
|
controller->setPowerEnabled(true);
|
|
|
|
|
run(12, 1);
|
|
|
|
|
TEST_ASSERT_TRUE(power.is_on);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 12:01", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_redraws_after_power_was_switched_off_elsewhere()
|
|
|
|
|
{
|
|
|
|
|
// e.g. after flashing the Nano: the Nano restarts without a frame
|
|
|
|
|
run(12, 0);
|
|
|
|
|
power.off();
|
|
|
|
|
run(12, 0);
|
|
|
|
|
TEST_ASSERT_TRUE(power.is_on);
|
|
|
|
|
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:34:04 +02:00
|
|
|
void test_no_wifi_is_shown_after_a_minute_without_wifi()
|
|
|
|
|
{
|
|
|
|
|
run(12, 0);
|
|
|
|
|
wifi = false;
|
|
|
|
|
run(12, 0, NO_WIFI_SHOW_TIME - 200);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str());
|
|
|
|
|
run(12, 1, 300);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("nowifi 12:01", frames.last().c_str());
|
|
|
|
|
TEST_ASSERT_TRUE(power.is_on);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_no_wifi_screen_keeps_the_time_running()
|
|
|
|
|
{
|
|
|
|
|
wifi = false;
|
|
|
|
|
run(12, 0, NO_WIFI_SHOW_TIME + 100);
|
|
|
|
|
run(12, 1);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("nowifi 12:01", frames.last().c_str());
|
|
|
|
|
int frames_before = (int)frames.shown.size();
|
|
|
|
|
run(12, 1, 5000);
|
|
|
|
|
TEST_ASSERT_EQUAL(frames_before, (int)frames.shown.size()); // only on change
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_clock_returns_when_wifi_is_back()
|
|
|
|
|
{
|
|
|
|
|
wifi = false;
|
|
|
|
|
run(12, 0, NO_WIFI_SHOW_TIME + 100);
|
|
|
|
|
wifi = true;
|
|
|
|
|
run(12, 0);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_short_wifi_drop_does_not_change_the_display()
|
|
|
|
|
{
|
|
|
|
|
run(12, 0);
|
|
|
|
|
wifi = false;
|
|
|
|
|
run(12, 0, 10000);
|
|
|
|
|
wifi = true;
|
|
|
|
|
run(12, 0, NO_WIFI_SHOW_TIME);
|
|
|
|
|
TEST_ASSERT_EQUAL(1, (int)frames.shown.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_night_without_wifi_still_powers_off()
|
|
|
|
|
{
|
|
|
|
|
run(23, 30);
|
|
|
|
|
wifi = false;
|
|
|
|
|
run(23, 31, POWER_OFF_DELAY + 200);
|
|
|
|
|
TEST_ASSERT_FALSE(power.is_on);
|
2026-10-15 01:29:01 +02:00
|
|
|
TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str());
|
2026-10-13 01:34:04 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Settings
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void test_default_settings_are_valid()
|
|
|
|
|
{
|
|
|
|
|
TEST_ASSERT_NULL(validateSettings(defaults));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_settings_times_must_be_in_order()
|
|
|
|
|
{
|
|
|
|
|
Settings s;
|
|
|
|
|
s.schedule.late_start = 7 * 60; // before day_start
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
s = Settings();
|
|
|
|
|
s.schedule.night_start = 21 * 60; // before late_start
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
s = Settings();
|
|
|
|
|
s.schedule.night_start = MINUTES_PER_DAY; // never night: fine
|
|
|
|
|
TEST_ASSERT_NULL(validateSettings(s));
|
|
|
|
|
s.schedule.night_start = MINUTES_PER_DAY + 1;
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 01:29:01 +02:00
|
|
|
void test_settings_drive_ranges()
|
|
|
|
|
{
|
|
|
|
|
Settings s;
|
|
|
|
|
TEST_ASSERT_NULL(validateSettings(s));
|
|
|
|
|
s.drive.flip_gap = 5001;
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
s = Settings();
|
|
|
|
|
s.drive.pulses = 0;
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
s.drive.pulses = 4;
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
s = Settings();
|
|
|
|
|
s.drive.effect = DRIVE_EFFECT_COUNT;
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
s = Settings();
|
|
|
|
|
s.drive.effect_time = 99;
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
s.drive.effect_time = 10001;
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_effect_time_is_zero_when_instant()
|
|
|
|
|
{
|
|
|
|
|
DriveSettings d;
|
|
|
|
|
TEST_ASSERT_EQUAL(0, (int)effectTime(d)); // default: instant
|
|
|
|
|
d.effect = 1;
|
|
|
|
|
d.effect_time = 2500;
|
|
|
|
|
TEST_ASSERT_EQUAL(2500, (int)effectTime(d));
|
|
|
|
|
d.effect = DRIVE_EFFECT_PUSH; // can take much longer
|
|
|
|
|
TEST_ASSERT_EQUAL(2500 + (int)DRIVE_PUSH_EXTRA_TIME, (int)effectTime(d));
|
|
|
|
|
d.effect = DRIVE_EFFECT_RANDOM; // can be a push
|
|
|
|
|
TEST_ASSERT_EQUAL(2500 + (int)DRIVE_PUSH_EXTRA_TIME, (int)effectTime(d));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_settings_off_page_blank_or_bitmap_name()
|
|
|
|
|
{
|
|
|
|
|
Settings s;
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("", s.off_page); // default: blank
|
|
|
|
|
TEST_ASSERT_NULL(validateSettings(s));
|
|
|
|
|
strcpy(s.off_page, "Nintendo");
|
|
|
|
|
TEST_ASSERT_NULL(validateSettings(s));
|
|
|
|
|
strcpy(s.off_page, "../x");
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
memset(s.off_page, 'a', sizeof(s.off_page)); // not terminated
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_changed_off_image_is_redrawn_while_the_display_is_on()
|
|
|
|
|
{
|
|
|
|
|
run(23, 30);
|
|
|
|
|
run(23, 31, 1000);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str());
|
|
|
|
|
controller->pageChanged();
|
|
|
|
|
run(23, 31, 1000);
|
|
|
|
|
TEST_ASSERT_EQUAL(2, (int)std::count(frames.shown.begin(), frames.shown.end(), std::string("off")));
|
|
|
|
|
TEST_ASSERT_TRUE(power.is_on);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
void test_settings_flip_time_and_port_ranges()
|
|
|
|
|
{
|
|
|
|
|
Settings s;
|
|
|
|
|
s.flip_time = 49;
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
s.flip_time = 2001;
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
s = Settings();
|
|
|
|
|
s.home_assistant.port = 0;
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_settings_page_must_be_clock_or_bitmap_name()
|
|
|
|
|
{
|
|
|
|
|
Settings s;
|
|
|
|
|
strcpy(s.page, "My bitmap");
|
|
|
|
|
TEST_ASSERT_NULL(validateSettings(s));
|
|
|
|
|
strcpy(s.page, "../evil");
|
|
|
|
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_time_parse_and_format()
|
|
|
|
|
{
|
|
|
|
|
uint16_t m = 0;
|
|
|
|
|
TEST_ASSERT_TRUE(parseTime("08:30", m));
|
|
|
|
|
TEST_ASSERT_EQUAL(510, m);
|
|
|
|
|
TEST_ASSERT_TRUE(parseTime("24:00", m));
|
|
|
|
|
TEST_ASSERT_EQUAL(MINUTES_PER_DAY, m);
|
|
|
|
|
TEST_ASSERT_FALSE(parseTime("24:01", m));
|
|
|
|
|
TEST_ASSERT_FALSE(parseTime("12:60", m));
|
|
|
|
|
TEST_ASSERT_FALSE(parseTime("noon", m));
|
|
|
|
|
TEST_ASSERT_FALSE(parseTime("12:30x", m));
|
|
|
|
|
TEST_ASSERT_FALSE(parseTime(nullptr, m));
|
|
|
|
|
char out[6];
|
|
|
|
|
formatTime(23 * 60 + 31, out);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("23:31", out);
|
|
|
|
|
formatTime(MINUTES_PER_DAY, out);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("24:00", out);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Bitmap format
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void test_bitmap_is_84_by_19_in_drawbitmap_format()
|
|
|
|
|
{
|
|
|
|
|
TEST_ASSERT_EQUAL(11, BITMAP_ROW_BYTES);
|
|
|
|
|
TEST_ASSERT_EQUAL(209, BITMAP_BYTES);
|
|
|
|
|
uint8_t b[BITMAP_BYTES] = {};
|
|
|
|
|
setBitmapPixel(b, 0, 0, true);
|
|
|
|
|
TEST_ASSERT_EQUAL_HEX8(0x80, b[0]); // leftmost dot = most significant bit
|
|
|
|
|
setBitmapPixel(b, 83, 18, true);
|
|
|
|
|
TEST_ASSERT_EQUAL_HEX8(0x10, b[18 * 11 + 10]); // x = 83: bit 3 of byte 10
|
|
|
|
|
TEST_ASSERT_TRUE(bitmapPixel(b, 83, 18));
|
|
|
|
|
TEST_ASSERT_FALSE(bitmapPixel(b, 84, 0)); // outside
|
|
|
|
|
setBitmapPixel(b, 0, 0, false);
|
|
|
|
|
TEST_ASSERT_EQUAL_HEX8(0x00, b[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_bitmap_hex_round_trip()
|
|
|
|
|
{
|
|
|
|
|
uint8_t a[BITMAP_BYTES], b[BITMAP_BYTES];
|
|
|
|
|
for (int i = 0; i < BITMAP_BYTES; i++)
|
|
|
|
|
a[i] = (uint8_t)(i * 37);
|
|
|
|
|
char hex[BITMAP_HEX_LENGTH + 1];
|
|
|
|
|
bitmapToHex(a, hex);
|
|
|
|
|
TEST_ASSERT_EQUAL(BITMAP_HEX_LENGTH, (int)strlen(hex));
|
|
|
|
|
TEST_ASSERT_TRUE(bitmapFromHex(hex, b));
|
|
|
|
|
TEST_ASSERT_EQUAL_MEMORY(a, b, BITMAP_BYTES);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_bitmap_hex_rejects_wrong_input()
|
|
|
|
|
{
|
|
|
|
|
uint8_t b[BITMAP_BYTES];
|
|
|
|
|
TEST_ASSERT_FALSE(bitmapFromHex("abcd", b));
|
|
|
|
|
TEST_ASSERT_FALSE(bitmapFromHex(nullptr, b));
|
|
|
|
|
std::string bad(BITMAP_HEX_LENGTH, 'f');
|
|
|
|
|
bad[100] = 'x';
|
|
|
|
|
TEST_ASSERT_FALSE(bitmapFromHex(bad.c_str(), b));
|
|
|
|
|
std::string upper(BITMAP_HEX_LENGTH, 'F');
|
|
|
|
|
TEST_ASSERT_TRUE(bitmapFromHex(upper.c_str(), b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_bitmap_names()
|
|
|
|
|
{
|
|
|
|
|
TEST_ASSERT_TRUE(validBitmapName("Nintendo"));
|
|
|
|
|
TEST_ASSERT_TRUE(validBitmapName("Happy Birthday 2"));
|
|
|
|
|
TEST_ASSERT_TRUE(validBitmapName("a-b_c"));
|
|
|
|
|
TEST_ASSERT_FALSE(validBitmapName(""));
|
|
|
|
|
TEST_ASSERT_FALSE(validBitmapName(" lead"));
|
|
|
|
|
TEST_ASSERT_FALSE(validBitmapName("trail "));
|
|
|
|
|
TEST_ASSERT_FALSE(validBitmapName("../etc"));
|
|
|
|
|
TEST_ASSERT_FALSE(validBitmapName("a/b"));
|
|
|
|
|
TEST_ASSERT_FALSE(validBitmapName("Clock")); // the clock page
|
|
|
|
|
TEST_ASSERT_TRUE(validBitmapName("123456789012345678901234"));
|
|
|
|
|
TEST_ASSERT_FALSE(validBitmapName("1234567890123456789012345"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Schedule with settings
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void test_schedule_disabled_is_always_on()
|
|
|
|
|
{
|
|
|
|
|
ScheduleSettings s;
|
|
|
|
|
s.enabled = false;
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(3, 0, s) == ScheduleMode::Clock);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(22, 7, s) == ScheduleMode::Clock);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_schedule_custom_times()
|
|
|
|
|
{
|
|
|
|
|
ScheduleSettings s;
|
|
|
|
|
s.day_start = 6 * 60 + 30;
|
|
|
|
|
s.late_start = 20 * 60;
|
|
|
|
|
s.night_start = 21 * 60;
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(6, 29, s) == ScheduleMode::Night);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(6, 30, s) == ScheduleMode::Clock);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(20, 1, s) == ScheduleMode::Standby);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(20, 30, s) == ScheduleMode::Clock);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(21, 0, s) == ScheduleMode::Night);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_schedule_without_quarter_hours_stays_on_until_night()
|
|
|
|
|
{
|
|
|
|
|
ScheduleSettings s;
|
|
|
|
|
s.quarter_hours = false;
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(22, 7, s) == ScheduleMode::Clock);
|
|
|
|
|
TEST_ASSERT_TRUE(scheduleFor(23, 31, s) == ScheduleMode::Night);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_evening_can_be_disabled()
|
|
|
|
|
{
|
|
|
|
|
ClockSettings c;
|
|
|
|
|
c.evening_enabled = false;
|
|
|
|
|
TEST_ASSERT_FALSE(isEvening(20, 0, c));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Pages and live mode
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void test_bitmap_page_is_shown_once()
|
|
|
|
|
{
|
|
|
|
|
controller->setPage(Page::Bitmap);
|
|
|
|
|
run(12, 0);
|
|
|
|
|
run(12, 1, 5000);
|
|
|
|
|
TEST_ASSERT_EQUAL(1, (int)frames.shown.size());
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("page", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_bitmap_page_redrawn_when_changed()
|
|
|
|
|
{
|
|
|
|
|
controller->setPage(Page::Bitmap);
|
|
|
|
|
run(12, 0);
|
|
|
|
|
controller->pageChanged();
|
|
|
|
|
run(12, 0);
|
|
|
|
|
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_switching_back_to_the_clock_page()
|
|
|
|
|
{
|
|
|
|
|
controller->setPage(Page::Bitmap);
|
|
|
|
|
run(12, 0);
|
|
|
|
|
controller->setPage(Page::Clock);
|
|
|
|
|
run(12, 0);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_bitmap_page_follows_the_schedule()
|
|
|
|
|
{
|
|
|
|
|
controller->setPage(Page::Bitmap);
|
|
|
|
|
run(23, 30);
|
|
|
|
|
run(23, 31, POWER_OFF_DELAY + 200);
|
2026-10-15 01:29:01 +02:00
|
|
|
TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str());
|
2026-10-13 01:52:34 +02:00
|
|
|
TEST_ASSERT_FALSE(power.is_on);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_evening_clock_redrawn_when_the_evening_starts()
|
|
|
|
|
{
|
|
|
|
|
run(16, 59);
|
|
|
|
|
run(17, 0);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 17:00 evening", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_settings_change_redraws()
|
|
|
|
|
{
|
|
|
|
|
run(18, 0);
|
|
|
|
|
ClockSettings c;
|
|
|
|
|
c.evening_enabled = false;
|
|
|
|
|
controller->setSettings(defaults.schedule, c);
|
|
|
|
|
run(18, 0);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 18:00", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_live_is_shown_right_away_even_at_night()
|
|
|
|
|
{
|
|
|
|
|
run(3, 0);
|
|
|
|
|
controller->showLive(now);
|
|
|
|
|
run(3, 0);
|
|
|
|
|
TEST_ASSERT_TRUE(power.is_on);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("live", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_live_works_before_the_time_is_synced()
|
|
|
|
|
{
|
|
|
|
|
controller->showLive(now);
|
|
|
|
|
controller->update(now, false, 0, 0, true);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("live", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_each_live_image_is_drawn()
|
|
|
|
|
{
|
|
|
|
|
controller->showLive(now);
|
|
|
|
|
run(12, 0);
|
|
|
|
|
controller->showLive(now);
|
|
|
|
|
run(12, 0);
|
|
|
|
|
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_stop_live_returns_to_the_page()
|
|
|
|
|
{
|
|
|
|
|
controller->showLive(now);
|
|
|
|
|
run(12, 0);
|
|
|
|
|
controller->stopLive();
|
|
|
|
|
run(12, 0);
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str());
|
|
|
|
|
TEST_ASSERT_FALSE(controller->liveActive());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_live_ends_after_timeout()
|
|
|
|
|
{
|
|
|
|
|
controller->showLive(now);
|
|
|
|
|
run(12, 0, LIVE_TIMEOUT + 200);
|
|
|
|
|
TEST_ASSERT_FALSE(controller->liveActive());
|
|
|
|
|
TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_live_respects_the_power_switch()
|
|
|
|
|
{
|
|
|
|
|
controller->setPowerEnabled(false);
|
|
|
|
|
controller->showLive(now);
|
|
|
|
|
run(12, 0, 5000);
|
|
|
|
|
TEST_ASSERT_FALSE(power.is_on);
|
|
|
|
|
TEST_ASSERT_EQUAL(0, (int)std::count(frames.shown.begin(), frames.shown.end(), std::string("live")));
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 00:56:32 +02:00
|
|
|
int main()
|
|
|
|
|
{
|
|
|
|
|
UNITY_BEGIN();
|
|
|
|
|
|
|
|
|
|
RUN_TEST(test_schedule_night_until_8);
|
|
|
|
|
RUN_TEST(test_schedule_clock_from_8_to_22);
|
|
|
|
|
RUN_TEST(test_schedule_quarter_hours_until_23_30);
|
|
|
|
|
RUN_TEST(test_schedule_night_from_23_31);
|
|
|
|
|
RUN_TEST(test_evening_from_17);
|
|
|
|
|
|
|
|
|
|
RUN_TEST(test_timer_expires_once_after_delay);
|
|
|
|
|
RUN_TEST(test_timer_keeps_earlier_deadline);
|
|
|
|
|
RUN_TEST(test_timer_cancel);
|
|
|
|
|
RUN_TEST(test_timer_across_millis_overflow);
|
|
|
|
|
|
2026-10-13 01:05:06 +02:00
|
|
|
RUN_TEST(test_power_on_starts_normally_and_clears_crashes);
|
|
|
|
|
RUN_TEST(test_software_reset_starts_normally_and_keeps_count);
|
|
|
|
|
RUN_TEST(test_other_reset_starts_normally_and_keeps_count);
|
|
|
|
|
RUN_TEST(test_first_crashes_give_a_panic_window);
|
|
|
|
|
RUN_TEST(test_repeated_crashes_give_safe_mode);
|
|
|
|
|
RUN_TEST(test_after_safe_mode_one_more_crash_gives_safe_mode_again);
|
|
|
|
|
RUN_TEST(test_crash_count_does_not_overflow);
|
|
|
|
|
RUN_TEST(test_new_firmware_needs_time_and_wifi_to_be_accepted);
|
|
|
|
|
RUN_TEST(test_crash_count_cleared_after_running_long_enough);
|
|
|
|
|
|
2026-10-13 01:21:04 +02:00
|
|
|
RUN_TEST(test_quick_power_cycles_request_recovery);
|
|
|
|
|
RUN_TEST(test_recovery_for_wifi_starts_clock_when_wifi_works);
|
|
|
|
|
RUN_TEST(test_recovery_for_other_reasons_waits_recovery_time);
|
|
|
|
|
RUN_TEST(test_recovery_never_starts_missing_clock_firmware);
|
|
|
|
|
RUN_TEST(test_every_recovery_reason_has_a_text);
|
|
|
|
|
|
2026-10-13 00:56:32 +02:00
|
|
|
RUN_TEST(test_led_off_while_display_off);
|
|
|
|
|
RUN_TEST(test_led_blinks_slow_with_wifi_and_fast_without);
|
|
|
|
|
|
|
|
|
|
RUN_TEST(test_nothing_happens_until_time_is_synced);
|
|
|
|
|
RUN_TEST(test_daytime_powers_on_and_shows_clock);
|
|
|
|
|
RUN_TEST(test_clock_is_only_sent_when_the_minute_changes);
|
|
|
|
|
RUN_TEST(test_night_clears_then_powers_off_after_delay);
|
|
|
|
|
RUN_TEST(test_night_does_not_power_on);
|
|
|
|
|
RUN_TEST(test_standby_keeps_the_last_time_and_powers_off);
|
|
|
|
|
RUN_TEST(test_quarter_hour_powers_on_again_and_redraws);
|
|
|
|
|
RUN_TEST(test_clock_cancels_pending_power_off);
|
|
|
|
|
RUN_TEST(test_power_switch_off_clears_and_powers_off_quickly);
|
2026-10-15 01:29:01 +02:00
|
|
|
RUN_TEST(test_power_switch_off_waits_for_the_refresh_effect);
|
2026-10-13 00:56:32 +02:00
|
|
|
RUN_TEST(test_power_switch_off_keeps_display_off_during_the_day);
|
|
|
|
|
RUN_TEST(test_power_switch_on_again_shows_clock);
|
|
|
|
|
RUN_TEST(test_redraws_after_power_was_switched_off_elsewhere);
|
2026-10-13 01:34:04 +02:00
|
|
|
RUN_TEST(test_no_wifi_is_shown_after_a_minute_without_wifi);
|
|
|
|
|
RUN_TEST(test_no_wifi_screen_keeps_the_time_running);
|
|
|
|
|
RUN_TEST(test_clock_returns_when_wifi_is_back);
|
|
|
|
|
RUN_TEST(test_short_wifi_drop_does_not_change_the_display);
|
|
|
|
|
RUN_TEST(test_night_without_wifi_still_powers_off);
|
2026-10-13 00:56:32 +02:00
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
RUN_TEST(test_default_settings_are_valid);
|
|
|
|
|
RUN_TEST(test_settings_times_must_be_in_order);
|
2026-10-15 01:29:01 +02:00
|
|
|
RUN_TEST(test_settings_drive_ranges);
|
|
|
|
|
RUN_TEST(test_effect_time_is_zero_when_instant);
|
|
|
|
|
RUN_TEST(test_settings_off_page_blank_or_bitmap_name);
|
|
|
|
|
RUN_TEST(test_changed_off_image_is_redrawn_while_the_display_is_on);
|
2026-10-13 01:52:34 +02:00
|
|
|
RUN_TEST(test_settings_flip_time_and_port_ranges);
|
|
|
|
|
RUN_TEST(test_settings_page_must_be_clock_or_bitmap_name);
|
|
|
|
|
RUN_TEST(test_time_parse_and_format);
|
|
|
|
|
RUN_TEST(test_bitmap_is_84_by_19_in_drawbitmap_format);
|
|
|
|
|
RUN_TEST(test_bitmap_hex_round_trip);
|
|
|
|
|
RUN_TEST(test_bitmap_hex_rejects_wrong_input);
|
|
|
|
|
RUN_TEST(test_bitmap_names);
|
|
|
|
|
RUN_TEST(test_schedule_disabled_is_always_on);
|
|
|
|
|
RUN_TEST(test_schedule_custom_times);
|
|
|
|
|
RUN_TEST(test_schedule_without_quarter_hours_stays_on_until_night);
|
|
|
|
|
RUN_TEST(test_evening_can_be_disabled);
|
|
|
|
|
RUN_TEST(test_bitmap_page_is_shown_once);
|
|
|
|
|
RUN_TEST(test_bitmap_page_redrawn_when_changed);
|
|
|
|
|
RUN_TEST(test_switching_back_to_the_clock_page);
|
|
|
|
|
RUN_TEST(test_bitmap_page_follows_the_schedule);
|
|
|
|
|
RUN_TEST(test_evening_clock_redrawn_when_the_evening_starts);
|
|
|
|
|
RUN_TEST(test_settings_change_redraws);
|
|
|
|
|
RUN_TEST(test_live_is_shown_right_away_even_at_night);
|
|
|
|
|
RUN_TEST(test_live_works_before_the_time_is_synced);
|
|
|
|
|
RUN_TEST(test_each_live_image_is_drawn);
|
|
|
|
|
RUN_TEST(test_stop_live_returns_to_the_page);
|
|
|
|
|
RUN_TEST(test_live_ends_after_timeout);
|
|
|
|
|
RUN_TEST(test_live_respects_the_power_switch);
|
|
|
|
|
|
2026-10-13 00:56:32 +02:00
|
|
|
return UNITY_END();
|
|
|
|
|
}
|