Public Access
Add dynamic pages with clock, date, Home Assistant values and fonts
The hardcoded clock is now the built-in dynamic page "Clock". A dynamic page has a background bitmap per time of day and up to 8 elements (clock, date, Home Assistant value, text), each with position, alignment, font and overlay. Pages are drawn on the ESP32, also for the web previews. - Pages tab in the web UI: page editor with drag and drop, font upload (TTF/OTF converted in the browser to the FDF1 dot font format) - Built-in fonts Small, Normal, Large and Full height (generated by tools/make_builtin_fonts.py), built-in bitmaps Pikachu and Pikachu yawning - Home Assistant values over the REST API (URL and token in the HA tab) - Settings version 4: evening settings removed (now time slots of the Clock page), old records are converted - HA Mode dropdown lists the dynamic pages and bitmaps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d3b084de1
commit
0411804a2f
49 files changed
+3058
-331
No files matched your search
+406
-34
@@ -13,8 +13,13 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "logic/bitmap_format.h"
|
||||
#include "logic/builtin_fonts.h"
|
||||
#include "logic/builtin_pages.h"
|
||||
#include "logic/font.h"
|
||||
#include "logic/boot_policy.h"
|
||||
#include "logic/display_controller.h"
|
||||
#include "logic/page.h"
|
||||
#include "logic/page_render.h"
|
||||
#include "logic/power_timer.h"
|
||||
#include "logic/schedule.h"
|
||||
#include "logic/settings.h"
|
||||
@@ -49,10 +54,10 @@ class FakeFrames : public Frames
|
||||
public:
|
||||
std::vector<std::string> shown;
|
||||
|
||||
void clock(int hour, int minute, bool evening) override
|
||||
void dynamicPage(const LocalTime &time) override
|
||||
{
|
||||
char s[24];
|
||||
snprintf(s, sizeof(s), "clock %02d:%02d%s", hour, minute, evening ? " evening" : "");
|
||||
snprintf(s, sizeof(s), "clock %02d:%02d", time.hour, time.minute);
|
||||
shown.push_back(s);
|
||||
}
|
||||
void page() override { shown.push_back("page"); }
|
||||
@@ -94,7 +99,10 @@ static void run(int hour, int minute, uint32_t ms = 0)
|
||||
uint32_t end = now + ms;
|
||||
do
|
||||
{
|
||||
controller->update(now, true, hour, minute, wifi);
|
||||
LocalTime time;
|
||||
time.hour = hour;
|
||||
time.minute = minute;
|
||||
controller->update(now, true, time, wifi);
|
||||
now += 100;
|
||||
} while ((int32_t)(end - now) >= 0);
|
||||
}
|
||||
@@ -136,13 +144,6 @@ void test_schedule_night_from_23_31()
|
||||
TEST_ASSERT_TRUE(scheduleFor(23, 59, defaults.schedule) == ScheduleMode::Night);
|
||||
}
|
||||
|
||||
void test_evening_from_17()
|
||||
{
|
||||
TEST_ASSERT_FALSE(isEvening(16, 59, defaults.clock));
|
||||
TEST_ASSERT_TRUE(isEvening(17, 0, defaults.clock));
|
||||
TEST_ASSERT_TRUE(isEvening(23, 0, defaults.clock));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Power timer
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -336,7 +337,7 @@ void test_led_blinks_slow_with_wifi_and_fast_without()
|
||||
void test_nothing_happens_until_time_is_synced()
|
||||
{
|
||||
power.is_on = true; // showing "Connecting..."
|
||||
controller->update(now, false, 0, 0, true);
|
||||
controller->update(now, false, LocalTime(), true);
|
||||
TEST_ASSERT_EQUAL(0, power.off_count);
|
||||
TEST_ASSERT_EQUAL(0, (int)frames.shown.size());
|
||||
}
|
||||
@@ -381,7 +382,7 @@ 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);
|
||||
TEST_ASSERT_EQUAL_STRING("clock 22:00 evening", frames.last().c_str()); // no off screen
|
||||
TEST_ASSERT_EQUAL_STRING("clock 22:00", frames.last().c_str()); // no off screen
|
||||
}
|
||||
|
||||
void test_quarter_hour_powers_on_again_and_redraws()
|
||||
@@ -391,7 +392,7 @@ void test_quarter_hour_powers_on_again_and_redraws()
|
||||
TEST_ASSERT_FALSE(power.is_on);
|
||||
run(22, 15);
|
||||
TEST_ASSERT_TRUE(power.is_on);
|
||||
TEST_ASSERT_EQUAL_STRING("clock 22:15 evening", frames.last().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("clock 22:15", frames.last().c_str());
|
||||
}
|
||||
|
||||
void test_clock_cancels_pending_power_off()
|
||||
@@ -599,7 +600,7 @@ void test_settings_flip_time_and_port_ranges()
|
||||
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
||||
}
|
||||
|
||||
void test_settings_page_must_be_clock_or_bitmap_name()
|
||||
void test_settings_page_must_be_a_name()
|
||||
{
|
||||
Settings s;
|
||||
strcpy(s.page, "My bitmap");
|
||||
@@ -680,7 +681,8 @@ void test_bitmap_names()
|
||||
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(sameName("Clock", "clock"));
|
||||
TEST_ASSERT_FALSE(sameName("Clock", "Clocks"));
|
||||
TEST_ASSERT_TRUE(validBitmapName("123456789012345678901234"));
|
||||
TEST_ASSERT_FALSE(validBitmapName("1234567890123456789012345"));
|
||||
}
|
||||
@@ -718,13 +720,6 @@ void test_schedule_without_quarter_hours_stays_on_until_night()
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -751,7 +746,7 @@ void test_switching_back_to_the_clock_page()
|
||||
{
|
||||
controller->setPage(Page::Bitmap);
|
||||
run(12, 0);
|
||||
controller->setPage(Page::Clock);
|
||||
controller->setPage(Page::Dynamic);
|
||||
run(12, 0);
|
||||
TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str());
|
||||
}
|
||||
@@ -765,21 +760,22 @@ void test_bitmap_page_follows_the_schedule()
|
||||
TEST_ASSERT_FALSE(power.is_on);
|
||||
}
|
||||
|
||||
void test_evening_clock_redrawn_when_the_evening_starts()
|
||||
void test_dynamic_page_redrawn_when_its_content_changes()
|
||||
{
|
||||
run(16, 59);
|
||||
run(17, 0);
|
||||
TEST_ASSERT_EQUAL_STRING("clock 17:00 evening", frames.last().c_str());
|
||||
run(18, 0);
|
||||
controller->pageChanged(); // e.g. a Home Assistant value changed
|
||||
run(18, 0);
|
||||
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
||||
}
|
||||
|
||||
void test_settings_change_redraws()
|
||||
{
|
||||
run(18, 0);
|
||||
ClockSettings c;
|
||||
c.evening_enabled = false;
|
||||
c.no_wifi_screen = false;
|
||||
controller->setSettings(defaults.schedule, c);
|
||||
run(18, 0);
|
||||
TEST_ASSERT_EQUAL_STRING("clock 18:00", frames.last().c_str());
|
||||
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
||||
}
|
||||
|
||||
void test_live_is_shown_right_away_even_at_night()
|
||||
@@ -794,7 +790,7 @@ void test_live_is_shown_right_away_even_at_night()
|
||||
void test_live_works_before_the_time_is_synced()
|
||||
{
|
||||
controller->showLive(now);
|
||||
controller->update(now, false, 0, 0, true);
|
||||
controller->update(now, false, LocalTime(), true);
|
||||
TEST_ASSERT_EQUAL_STRING("live", frames.last().c_str());
|
||||
}
|
||||
|
||||
@@ -834,6 +830,367 @@ void test_live_respects_the_power_switch()
|
||||
TEST_ASSERT_EQUAL(0, (int)std::count(frames.shown.begin(), frames.shown.end(), std::string("live")));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fonts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static Font fontNamed(const char *name)
|
||||
{
|
||||
const BuiltinFont *b = findBuiltinFont(name);
|
||||
TEST_ASSERT_NOT_NULL(b);
|
||||
return builtinFont(*b);
|
||||
}
|
||||
|
||||
void test_builtin_fonts_are_valid()
|
||||
{
|
||||
for (int i = 0; i < BUILTIN_FONT_COUNT; i++)
|
||||
TEST_ASSERT_TRUE_MESSAGE(validFont(BUILTIN_FONTS[i].data, BUILTIN_FONTS[i].size), BUILTIN_FONTS[i].name);
|
||||
TEST_ASSERT_NOT_NULL(findBuiltinFont("normal"));
|
||||
TEST_ASSERT_NULL(findBuiltinFont("Comic Sans"));
|
||||
TEST_ASSERT_EQUAL(2, fontNamed("Large").scale);
|
||||
}
|
||||
|
||||
void test_font_sizes()
|
||||
{
|
||||
// Monospaced digits: the clock doesn't move when the time changes
|
||||
TEST_ASSERT_EQUAL(29, textWidth(fontNamed("Normal"), "12:34"));
|
||||
TEST_ASSERT_EQUAL(29, textWidth(fontNamed("Normal"), "00:00"));
|
||||
TEST_ASSERT_EQUAL(58, textWidth(fontNamed("Large"), "12:34"));
|
||||
TEST_ASSERT_EQUAL(17, textWidth(fontNamed("Small"), "12:34"));
|
||||
TEST_ASSERT_EQUAL(51, textWidth(fontNamed("Full height"), "12:34"));
|
||||
TEST_ASSERT_EQUAL(8, fontHeight(fontNamed("Normal")));
|
||||
TEST_ASSERT_EQUAL(16, fontHeight(fontNamed("Large")));
|
||||
TEST_ASSERT_EQUAL(6, fontHeight(fontNamed("Small")));
|
||||
TEST_ASSERT_EQUAL(19, fontHeight(fontNamed("Full height")));
|
||||
// Characters that are not in the font are skipped
|
||||
TEST_ASSERT_EQUAL(10, textWidth(fontNamed("Full height"), "a1b"));
|
||||
TEST_ASSERT_EQUAL(0, textWidth(fontNamed("Normal"), ""));
|
||||
}
|
||||
|
||||
void test_draw_text_uses_the_classic_glyphs()
|
||||
{
|
||||
uint8_t b[BITMAP_BYTES] = {};
|
||||
drawText(b, fontNamed("Normal"), 10, 2, "1", DrawMode::Set);
|
||||
// glcdfont '1': 0x00, 0x42, 0x7F, 0x40, 0x00 (columns, bit 0 = top)
|
||||
for (int row = 0; row < 7; row++)
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 12, 2 + row));
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 12, 9));
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 11, 3));
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 11, 2));
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 13, 8));
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 10, 5));
|
||||
}
|
||||
|
||||
void test_draw_text_scaled_and_inverted()
|
||||
{
|
||||
uint8_t b[BITMAP_BYTES];
|
||||
memset(b, 0xFF, sizeof(b));
|
||||
drawText(b, fontNamed("Large"), 0, 0, "1", DrawMode::Invert);
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 4, 0)); // column 2 doubled: 4 and 5
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 5, 13));
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 4, 14));
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 0, 0));
|
||||
}
|
||||
|
||||
void test_draw_text_is_clipped()
|
||||
{
|
||||
uint8_t b[BITMAP_BYTES] = {};
|
||||
drawText(b, fontNamed("Full height"), 80, 5, "88", DrawMode::Set); // must not write outside
|
||||
drawText(b, fontNamed("Full height"), -5, -10, "88", DrawMode::Set);
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 81, 6));
|
||||
}
|
||||
|
||||
void test_invalid_fonts_are_rejected()
|
||||
{
|
||||
const BuiltinFont *n = findBuiltinFont("Normal");
|
||||
std::vector<uint8_t> f(n->data, n->data + n->size);
|
||||
TEST_ASSERT_TRUE(validFont(f.data(), f.size()));
|
||||
TEST_ASSERT_FALSE(validFont(f.data(), f.size() - 1));
|
||||
f.push_back(0);
|
||||
TEST_ASSERT_FALSE(validFont(f.data(), f.size()));
|
||||
f.pop_back();
|
||||
f[0] = 'X';
|
||||
TEST_ASSERT_FALSE(validFont(f.data(), f.size()));
|
||||
f[0] = 'F';
|
||||
f[4] = 20; // too high
|
||||
TEST_ASSERT_FALSE(validFont(f.data(), f.size()));
|
||||
TEST_ASSERT_FALSE(validFont(nullptr, 0));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dynamic pages
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static DynamicPage twoSlotPage()
|
||||
{
|
||||
DynamicPage page;
|
||||
page.slot_count = 2;
|
||||
page.slots[0].start = 8 * 60;
|
||||
strcpy(page.slots[0].bitmap, "Day");
|
||||
page.slots[1].start = 17 * 60;
|
||||
strcpy(page.slots[1].bitmap, "Evening");
|
||||
return page;
|
||||
}
|
||||
|
||||
void test_active_slot_follows_the_time()
|
||||
{
|
||||
DynamicPage page = twoSlotPage();
|
||||
TEST_ASSERT_EQUAL_STRING("Evening", activeSlot(page, 7 * 60)->bitmap); // from the day before
|
||||
TEST_ASSERT_EQUAL_STRING("Day", activeSlot(page, 8 * 60)->bitmap);
|
||||
TEST_ASSERT_EQUAL_STRING("Day", activeSlot(page, 17 * 60 - 1)->bitmap);
|
||||
TEST_ASSERT_EQUAL_STRING("Evening", activeSlot(page, 17 * 60)->bitmap);
|
||||
TEST_ASSERT_NULL(activeSlot(DynamicPage(), 600));
|
||||
}
|
||||
|
||||
void test_page_validation()
|
||||
{
|
||||
DynamicPage page = twoSlotPage();
|
||||
TEST_ASSERT_NULL(validatePage(page));
|
||||
page.slots[1].start = 8 * 60;
|
||||
TEST_ASSERT_NOT_NULL(validatePage(page)); // not in order
|
||||
page = twoSlotPage();
|
||||
page.slots[1].start = 24 * 60;
|
||||
TEST_ASSERT_NOT_NULL(validatePage(page));
|
||||
page = twoSlotPage();
|
||||
strcpy(page.slots[0].bitmap, "../x");
|
||||
TEST_ASSERT_NOT_NULL(validatePage(page));
|
||||
|
||||
page = DynamicPage();
|
||||
page.element_count = 1;
|
||||
PageElement &e = page.elements[0];
|
||||
TEST_ASSERT_NULL(validatePage(page));
|
||||
e.type = ElementType::HaValue;
|
||||
TEST_ASSERT_NOT_NULL(validatePage(page)); // needs an entity
|
||||
strcpy(e.entity, "sensor.outside_temperature");
|
||||
TEST_ASSERT_NULL(validatePage(page));
|
||||
strcpy(e.attribute, "bad attr");
|
||||
TEST_ASSERT_NOT_NULL(validatePage(page));
|
||||
e.attribute[0] = '\0';
|
||||
strcpy(e.text, "caf\xc3\xa9");
|
||||
TEST_ASSERT_NOT_NULL(validatePage(page)); // the fonts have no accents
|
||||
e.text[0] = '\0';
|
||||
e.decimals = 5;
|
||||
TEST_ASSERT_NOT_NULL(validatePage(page));
|
||||
e.decimals = 1;
|
||||
e.type = ElementType::Date;
|
||||
e.format = DATE_FORMAT_COUNT;
|
||||
TEST_ASSERT_NOT_NULL(validatePage(page));
|
||||
page.element_count = PAGE_ELEMENTS + 1;
|
||||
TEST_ASSERT_NOT_NULL(validatePage(page));
|
||||
|
||||
TEST_ASSERT_TRUE(validEntityId("weather.home"));
|
||||
TEST_ASSERT_FALSE(validEntityId("Sensor.x"));
|
||||
TEST_ASSERT_FALSE(validEntityId("sensor"));
|
||||
TEST_ASSERT_FALSE(validEntityId("a.b.c"));
|
||||
TEST_ASSERT_FALSE(validEntityId(".x"));
|
||||
}
|
||||
|
||||
static std::string textOf(const PageElement &e, const char *ha_value = nullptr)
|
||||
{
|
||||
LocalTime t; // Thursday 15 October 2026, 08:05
|
||||
t.year = 2026;
|
||||
t.month = 10;
|
||||
t.day = 15;
|
||||
t.weekday = 4;
|
||||
t.hour = 8;
|
||||
t.minute = 5;
|
||||
char out[64];
|
||||
elementText(e, t, ha_value, out, sizeof(out));
|
||||
return out;
|
||||
}
|
||||
|
||||
void test_clock_and_date_texts()
|
||||
{
|
||||
PageElement e;
|
||||
TEST_ASSERT_EQUAL_STRING("08:05", textOf(e).c_str());
|
||||
e.format = CLOCK_H_MM;
|
||||
TEST_ASSERT_EQUAL_STRING("8:05", textOf(e).c_str());
|
||||
|
||||
e.type = ElementType::Date;
|
||||
const char *dutch[] = {"do 15 okt", "15 okt", "15 oktober", "15-10", "15-10-2026", "donderdag", "do"};
|
||||
const char *english[] = {"Thu 15 Oct", "15 Oct", "15 October", "15-10", "15-10-2026", "Thursday", "Thu"};
|
||||
for (uint8_t f = 0; f < DATE_FORMAT_COUNT; f++)
|
||||
{
|
||||
e.format = f;
|
||||
e.language = Language::Dutch;
|
||||
TEST_ASSERT_EQUAL_STRING(dutch[f], textOf(e).c_str());
|
||||
e.language = Language::English;
|
||||
TEST_ASSERT_EQUAL_STRING(english[f], textOf(e).c_str());
|
||||
}
|
||||
|
||||
e = PageElement();
|
||||
e.type = ElementType::Text;
|
||||
strcpy(e.text, "Hallo");
|
||||
TEST_ASSERT_EQUAL_STRING("Hallo", textOf(e).c_str());
|
||||
}
|
||||
|
||||
void test_home_assistant_value_texts()
|
||||
{
|
||||
PageElement e;
|
||||
e.type = ElementType::HaValue;
|
||||
strcpy(e.text, "T ");
|
||||
strcpy(e.suffix, "C");
|
||||
TEST_ASSERT_EQUAL_STRING("T 21.46C", textOf(e, "21.46").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("T --C", textOf(e, nullptr).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("T --C", textOf(e, "unavailable").c_str());
|
||||
e.decimals = 1;
|
||||
TEST_ASSERT_EQUAL_STRING("T 21.5C", textOf(e, "21.46").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("T 0.0C", textOf(e, "-0.04").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("T -1.0C", textOf(e, "-1").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("T onC", textOf(e, "on").c_str()); // not a number: as is
|
||||
e.decimals = 0;
|
||||
TEST_ASSERT_EQUAL_STRING("T 21C", textOf(e, "21.46").c_str());
|
||||
}
|
||||
|
||||
// Backgrounds: "Full" is all dots on, "Day"/"Evening" have one dot at x 0 / x 1
|
||||
class FakeResources : public PageResources
|
||||
{
|
||||
public:
|
||||
const char *value = "21.5";
|
||||
|
||||
bool background(const char *name, uint8_t *bitmap) override
|
||||
{
|
||||
memset(bitmap, 0, BITMAP_BYTES);
|
||||
if (strcmp(name, "Full") == 0)
|
||||
memset(bitmap, 0xFF, BITMAP_BYTES);
|
||||
else if (strcmp(name, "Day") == 0)
|
||||
setBitmapPixel(bitmap, 0, 0, true);
|
||||
else if (strcmp(name, "Evening") == 0)
|
||||
setBitmapPixel(bitmap, 1, 0, true);
|
||||
else
|
||||
{
|
||||
memset(bitmap, 0xAA, BITMAP_BYTES); // garbage: must be cleared
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Font font(const char *name) override
|
||||
{
|
||||
const BuiltinFont *b = findBuiltinFont(name);
|
||||
return b ? builtinFont(*b) : Font();
|
||||
}
|
||||
const char *haValue(const PageElement &) override { return value; }
|
||||
};
|
||||
|
||||
static LocalTime at(int hour, int minute)
|
||||
{
|
||||
LocalTime t;
|
||||
t.hour = hour;
|
||||
t.minute = minute;
|
||||
return t;
|
||||
}
|
||||
|
||||
void test_render_background_follows_the_slots()
|
||||
{
|
||||
FakeResources res;
|
||||
DynamicPage page = twoSlotPage();
|
||||
uint8_t b[BITMAP_BYTES];
|
||||
renderPage(page, at(9, 0), res, b);
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 0, 0));
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 1, 0));
|
||||
renderPage(page, at(18, 0), res, b);
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 1, 0));
|
||||
strcpy(page.slots[1].bitmap, "Gone");
|
||||
renderPage(page, at(18, 0), res, b);
|
||||
for (int i = 0; i < BITMAP_BYTES; i++)
|
||||
TEST_ASSERT_EQUAL_HEX8(0, b[i]); // blank
|
||||
}
|
||||
|
||||
void test_render_overlays()
|
||||
{
|
||||
FakeResources res;
|
||||
DynamicPage page;
|
||||
page.slot_count = 1;
|
||||
strcpy(page.slots[0].bitmap, "Full");
|
||||
page.element_count = 1;
|
||||
PageElement &e = page.elements[0];
|
||||
e.type = ElementType::Text;
|
||||
strcpy(e.text, "1");
|
||||
e.x = 10;
|
||||
e.y = 2;
|
||||
|
||||
uint8_t b[BITMAP_BYTES];
|
||||
e.overlay = Overlay::OnTop; // on a full background nothing changes
|
||||
renderPage(page, at(12, 0), res, b);
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 10, 2));
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 12, 2));
|
||||
|
||||
e.overlay = Overlay::Box; // cleared area with one dot around it, then the text
|
||||
renderPage(page, at(12, 0), res, b);
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 9, 1));
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 10, 2));
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 12, 2));
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 15, 10));
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 16, 10));
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 8, 2));
|
||||
|
||||
e.overlay = Overlay::Invert;
|
||||
renderPage(page, at(12, 0), res, b);
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 10, 2));
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 12, 2));
|
||||
}
|
||||
|
||||
void test_render_alignment_and_boxes()
|
||||
{
|
||||
FakeResources res;
|
||||
DynamicPage page;
|
||||
page.element_count = 2;
|
||||
page.elements[0].x = 84;
|
||||
page.elements[0].align = Align::Right;
|
||||
page.elements[1].type = ElementType::HaValue;
|
||||
strcpy(page.elements[1].entity, "sensor.t");
|
||||
page.elements[1].x = 42;
|
||||
page.elements[1].y = 11;
|
||||
page.elements[1].align = Align::Center;
|
||||
ElementBox boxes[2];
|
||||
uint8_t b[BITMAP_BYTES];
|
||||
renderPage(page, at(12, 34), res, b, boxes);
|
||||
TEST_ASSERT_EQUAL(84 - 29, boxes[0].x);
|
||||
TEST_ASSERT_EQUAL(29, boxes[0].width);
|
||||
TEST_ASSERT_EQUAL(8, boxes[0].height);
|
||||
TEST_ASSERT_EQUAL(23, boxes[1].width); // "21.5"
|
||||
TEST_ASSERT_EQUAL(42 - 11, boxes[1].x);
|
||||
TEST_ASSERT_EQUAL(11, boxes[1].y);
|
||||
}
|
||||
|
||||
void test_render_unknown_font_uses_normal()
|
||||
{
|
||||
FakeResources res;
|
||||
DynamicPage page;
|
||||
page.element_count = 1;
|
||||
strcpy(page.elements[0].font, "Gone");
|
||||
ElementBox box;
|
||||
uint8_t b[BITMAP_BYTES];
|
||||
renderPage(page, at(12, 34), res, b, &box);
|
||||
TEST_ASSERT_EQUAL(29, box.width);
|
||||
}
|
||||
|
||||
void test_builtin_clock_page_is_the_old_clock()
|
||||
{
|
||||
DynamicPage page;
|
||||
TEST_ASSERT_TRUE(builtinPage("clock", page)); // old settings stored "clock"
|
||||
TEST_ASSERT_NULL(validatePage(page));
|
||||
TEST_ASSERT_EQUAL_STRING("Pikachu", activeSlot(page, 16 * 60 + 59)->bitmap);
|
||||
TEST_ASSERT_EQUAL_STRING("Pikachu yawning", activeSlot(page, 17 * 60)->bitmap);
|
||||
TEST_ASSERT_EQUAL_STRING(BUILTIN_PAGE_CLOCK, builtinPageName(0));
|
||||
TEST_ASSERT_FALSE(builtinPage("Pikachu", page));
|
||||
|
||||
// The time where the old firmware printed it: cursor 46, 4 with the classic font
|
||||
FakeResources res;
|
||||
TEST_ASSERT_TRUE(builtinPage(BUILTIN_PAGE_CLOCK, page));
|
||||
uint8_t b[BITMAP_BYTES];
|
||||
renderPage(page, at(12, 34), res, b);
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 48, 4)); // the '1'
|
||||
TEST_ASSERT_TRUE(bitmapPixel(b, 48, 10));
|
||||
TEST_ASSERT_FALSE(bitmapPixel(b, 48, 11));
|
||||
}
|
||||
|
||||
void test_default_page_is_the_builtin_clock()
|
||||
{
|
||||
DynamicPage page;
|
||||
TEST_ASSERT_TRUE(builtinPage(defaults.page, page));
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
@@ -842,7 +1199,6 @@ int main()
|
||||
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);
|
||||
@@ -894,7 +1250,7 @@ int main()
|
||||
RUN_TEST(test_settings_off_page_blank_or_bitmap_name);
|
||||
RUN_TEST(test_changed_off_image_is_redrawn_while_the_display_is_on);
|
||||
RUN_TEST(test_settings_flip_time_and_port_ranges);
|
||||
RUN_TEST(test_settings_page_must_be_clock_or_bitmap_name);
|
||||
RUN_TEST(test_settings_page_must_be_a_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);
|
||||
@@ -903,12 +1259,11 @@ int main()
|
||||
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_dynamic_page_redrawn_when_its_content_changes);
|
||||
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);
|
||||
@@ -917,5 +1272,22 @@ int main()
|
||||
RUN_TEST(test_live_ends_after_timeout);
|
||||
RUN_TEST(test_live_respects_the_power_switch);
|
||||
|
||||
RUN_TEST(test_builtin_fonts_are_valid);
|
||||
RUN_TEST(test_font_sizes);
|
||||
RUN_TEST(test_draw_text_uses_the_classic_glyphs);
|
||||
RUN_TEST(test_draw_text_scaled_and_inverted);
|
||||
RUN_TEST(test_draw_text_is_clipped);
|
||||
RUN_TEST(test_invalid_fonts_are_rejected);
|
||||
RUN_TEST(test_active_slot_follows_the_time);
|
||||
RUN_TEST(test_page_validation);
|
||||
RUN_TEST(test_clock_and_date_texts);
|
||||
RUN_TEST(test_home_assistant_value_texts);
|
||||
RUN_TEST(test_render_background_follows_the_slots);
|
||||
RUN_TEST(test_render_overlays);
|
||||
RUN_TEST(test_render_alignment_and_boxes);
|
||||
RUN_TEST(test_render_unknown_font_uses_normal);
|
||||
RUN_TEST(test_builtin_clock_page_is_the_old_clock);
|
||||
RUN_TEST(test_default_page_is_the_builtin_clock);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in new issue
Block a user