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
+143
-32
@@ -1,36 +1,54 @@
|
||||
#include "app.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "display/bitmap_store.h"
|
||||
#include "display/font_store.h"
|
||||
#include "display/page_resources.h"
|
||||
#include "display/page_store.h"
|
||||
#include "net/ha_api.h"
|
||||
#include "system/settings_store.h"
|
||||
|
||||
NanoLink nano(Serial2, NANO_RX_PIN, NANO_TX_PIN, RELAY_PIN, NANO_RESET_PIN);
|
||||
BROSE9323 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, PANEL_WIDTH);
|
||||
FlipdotFrames frames(display);
|
||||
FlipdotFrames frames(display, pageResources);
|
||||
StatusScreen status(display, nano);
|
||||
DisplayController controller(nano, frames);
|
||||
|
||||
static Settings settings;
|
||||
static bool restart_pending = false;
|
||||
static uint32_t restart_at = 0;
|
||||
// Not on the stack: about 1.7 KB
|
||||
static DynamicPage loaded_page;
|
||||
|
||||
// Load the page bitmap into the frames; falls back to the clock when it is gone
|
||||
static void showDynamicPage()
|
||||
{
|
||||
frames.setDynamicPage(loaded_page);
|
||||
haApiWatch(&loaded_page);
|
||||
controller.setPage(Page::Dynamic);
|
||||
}
|
||||
|
||||
// Load the page into the frames: a dynamic page or a bitmap; the Clock page when it is gone
|
||||
static void applyPage()
|
||||
{
|
||||
if (strcmp(settings.page, PAGE_CLOCK) != 0)
|
||||
if (loadPage(settings.page, loaded_page))
|
||||
{
|
||||
uint8_t bitmap[BITMAP_BYTES];
|
||||
if (loadBitmap(settings.page, bitmap))
|
||||
{
|
||||
frames.setPageBitmap(bitmap);
|
||||
controller.setPage(Page::Bitmap);
|
||||
return;
|
||||
}
|
||||
Serial.printf("Page bitmap \"%s\" not found, showing the clock\n", settings.page);
|
||||
showDynamicPage();
|
||||
return;
|
||||
}
|
||||
controller.setPage(Page::Clock);
|
||||
uint8_t bitmap[BITMAP_BYTES];
|
||||
if (loadBitmap(settings.page, bitmap))
|
||||
{
|
||||
frames.setPageBitmap(bitmap);
|
||||
haApiWatch(nullptr);
|
||||
controller.setPage(Page::Bitmap);
|
||||
return;
|
||||
}
|
||||
Serial.printf("Page \"%s\" not found, showing the clock\n", settings.page);
|
||||
builtinPage(DEFAULT_PAGE, loaded_page);
|
||||
showDynamicPage();
|
||||
}
|
||||
|
||||
// Load the off bitmap into the frames; blank when it is gone
|
||||
@@ -52,10 +70,18 @@ static void apply()
|
||||
controller.setSettings(settings.schedule, settings.clock);
|
||||
controller.setEffectTime(effectTime(settings.drive));
|
||||
status.setDrive(settings.flip_time, settings.drive);
|
||||
haApiSetSettings(settings.home_assistant);
|
||||
applyOffPage();
|
||||
applyPage();
|
||||
}
|
||||
|
||||
// Something the current page may use changed: load it again and redraw
|
||||
static void reloadPage()
|
||||
{
|
||||
applyPage();
|
||||
controller.pageChanged();
|
||||
}
|
||||
|
||||
const Settings &appSettings()
|
||||
{
|
||||
return settings;
|
||||
@@ -67,7 +93,7 @@ void appLoadSettings()
|
||||
apply();
|
||||
}
|
||||
|
||||
static bool sameHomeAssistant(const HomeAssistantSettings &a, const HomeAssistantSettings &b)
|
||||
static bool sameMqtt(const HomeAssistantSettings &a, const HomeAssistantSettings &b)
|
||||
{
|
||||
return a.enabled == b.enabled && a.port == b.port && strcmp(a.host, b.host) == 0 &&
|
||||
strcmp(a.user, b.user) == 0 && strcmp(a.password, b.password) == 0;
|
||||
@@ -78,12 +104,12 @@ const char *appSaveSettings(const Settings &new_settings, bool &restart_needed)
|
||||
const char *error = validateSettings(new_settings);
|
||||
if (error)
|
||||
return error;
|
||||
if (strcmp(new_settings.page, PAGE_CLOCK) != 0 && !bitmapExists(new_settings.page))
|
||||
return "Bitmap not found";
|
||||
if (!pageExists(new_settings.page) && !bitmapExists(new_settings.page))
|
||||
return "Page not found";
|
||||
if (new_settings.off_page[0] && !bitmapExists(new_settings.off_page))
|
||||
return "Off bitmap not found";
|
||||
|
||||
restart_needed = !sameHomeAssistant(settings.home_assistant, new_settings.home_assistant);
|
||||
restart_needed = !sameMqtt(settings.home_assistant, new_settings.home_assistant);
|
||||
const DriveSettings &a = settings.drive, &b = new_settings.drive;
|
||||
bool drive_changed = settings.flip_time != new_settings.flip_time || a.flip_gap != b.flip_gap ||
|
||||
a.pulses != b.pulses || a.effect != b.effect || a.effect_time != b.effect_time ||
|
||||
@@ -96,12 +122,17 @@ const char *appSaveSettings(const Settings &new_settings, bool &restart_needed)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void copyName(char *out, const char *name)
|
||||
{
|
||||
strncpy(out, name, PAGE_NAME_SIZE - 1);
|
||||
out[PAGE_NAME_SIZE - 1] = '\0';
|
||||
}
|
||||
|
||||
const char *appSelectPage(const char *name)
|
||||
{
|
||||
if (strcmp(name, PAGE_CLOCK) != 0 && !bitmapExists(name))
|
||||
return "Bitmap not found";
|
||||
strncpy(settings.page, name, sizeof(settings.page) - 1);
|
||||
settings.page[sizeof(settings.page) - 1] = '\0';
|
||||
if (!pageExists(name) && !bitmapExists(name))
|
||||
return "Page not found";
|
||||
copyName(settings.page, name);
|
||||
saveSettings(settings);
|
||||
applyPage();
|
||||
return nullptr;
|
||||
@@ -111,26 +142,92 @@ const char *appSelectOffPage(const char *name)
|
||||
{
|
||||
if (name[0] && !bitmapExists(name))
|
||||
return "Bitmap not found";
|
||||
strncpy(settings.off_page, name, sizeof(settings.off_page) - 1);
|
||||
settings.off_page[sizeof(settings.off_page) - 1] = '\0';
|
||||
copyName(settings.off_page, name);
|
||||
saveSettings(settings);
|
||||
applyOffPage();
|
||||
controller.pageChanged();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void appBitmapChanged(const char *name)
|
||||
const char *appSaveBitmap(const char *name, const uint8_t *bitmap)
|
||||
{
|
||||
if (strcmp(settings.off_page, name) == 0)
|
||||
{
|
||||
if (pageExists(name))
|
||||
return "A page has this name";
|
||||
const char *error = saveBitmap(name, bitmap);
|
||||
if (error)
|
||||
return error;
|
||||
if (sameName(settings.off_page, name))
|
||||
applyOffPage();
|
||||
controller.pageChanged();
|
||||
}
|
||||
if (strcmp(settings.page, name) == 0)
|
||||
{
|
||||
applyPage();
|
||||
controller.pageChanged();
|
||||
}
|
||||
reloadPage(); // the page or its background
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// "Used by page X ..." (the returned text stays valid until the next call)
|
||||
static const char *usedBy(const String &page)
|
||||
{
|
||||
static char message[80];
|
||||
snprintf(message, sizeof(message), "Used by page \"%s\": change that page first", page.c_str());
|
||||
return message;
|
||||
}
|
||||
|
||||
const char *appDeleteBitmap(const char *name)
|
||||
{
|
||||
String user = pageUsingBitmap(name);
|
||||
if (user.length())
|
||||
return usedBy(user);
|
||||
const char *error = deleteBitmap(name);
|
||||
if (error)
|
||||
return error;
|
||||
if (sameName(settings.page, name))
|
||||
appSelectPage(DEFAULT_PAGE);
|
||||
if (sameName(settings.off_page, name))
|
||||
appSelectOffPage(OFF_PAGE_BLANK);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *appSavePage(const char *name, const DynamicPage &page)
|
||||
{
|
||||
if (bitmapExists(name))
|
||||
return "A bitmap has this name";
|
||||
for (int i = 0; i < page.slot_count; i++)
|
||||
if (page.slots[i].bitmap[0] && !bitmapExists(page.slots[i].bitmap))
|
||||
return "Background bitmap not found";
|
||||
for (int i = 0; i < page.element_count; i++)
|
||||
if (!fontExists(page.elements[i].font))
|
||||
return "Font not found";
|
||||
const char *error = savePage(name, page);
|
||||
if (error)
|
||||
return error;
|
||||
if (sameName(settings.page, name))
|
||||
reloadPage();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *appDeletePage(const char *name)
|
||||
{
|
||||
const char *error = deletePage(name);
|
||||
if (error)
|
||||
return error;
|
||||
if (sameName(settings.page, name))
|
||||
appSelectPage(DEFAULT_PAGE);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *appSaveFont(const char *name, const uint8_t *data, size_t size)
|
||||
{
|
||||
const char *error = saveFont(name, data, size);
|
||||
if (error)
|
||||
return error;
|
||||
controller.pageChanged(); // the page may use it
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *appDeleteFont(const char *name)
|
||||
{
|
||||
String user = pageUsingFont(name);
|
||||
if (user.length())
|
||||
return usedBy(user);
|
||||
return deleteFont(name);
|
||||
}
|
||||
|
||||
void appShowLive(const uint8_t *bitmap)
|
||||
@@ -144,6 +241,20 @@ void appStopLive()
|
||||
controller.stopLive();
|
||||
}
|
||||
|
||||
bool appLocalTime(LocalTime &time)
|
||||
{
|
||||
struct tm now = {};
|
||||
if (!getLocalTime(&now, 0))
|
||||
return false;
|
||||
time.year = now.tm_year + 1900;
|
||||
time.month = now.tm_mon + 1;
|
||||
time.day = now.tm_mday;
|
||||
time.weekday = now.tm_wday;
|
||||
time.hour = now.tm_hour;
|
||||
time.minute = now.tm_min;
|
||||
return true;
|
||||
}
|
||||
|
||||
void appRestartLater(uint32_t delay_ms)
|
||||
{
|
||||
restart_pending = true;
|
||||
|
||||
Reference in new issue
Block a user