#include "app.h" #include #include #include #include "config.h" #include "display/bitmap_store.h" #include "display/font_store.h" #include "display/page_resources.h" #include "display/page_store.h" #include "logic/page_selector.h" #include "net/ha_api.h" #include "system/schedule_store.h" #include "system/settings_store.h" static void copyName(char *out, const char *name) { strncpy(out, name, PAGE_NAME_SIZE - 1); out[PAGE_NAME_SIZE - 1] = '\0'; } 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, pageResources); StatusScreen status(display, nano); DisplayController controller(nano, frames); LoopStats loopStats; NanoMonitor nanoMonitor(nano, display); 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; // What is shown when (about 2.7 KB), and which page that is now static PageSchedule page_schedule; static PageSelector selector; static char shown[PAGE_NAME_SIZE] = DEFAULT_PAGE; static PageChoice shown_choice; static bool time_known = false; static void showDynamicPage() { frames.setDynamicPage(loaded_page); haApiWatch(&loaded_page); controller.setPage(Page::Dynamic); } // Load the shown page into the frames: a dynamic page or a bitmap; the Clock page when it is // gone. instant: the next frame without the refresh effect. static void applyPage(bool instant = false) { if (instant) frames.showNextInstant(); if (loadPage(shown, loaded_page)) { showDynamicPage(); return; } uint8_t bitmap[BITMAP_BYTES]; if (loadBitmap(shown, bitmap)) { frames.setPageBitmap(bitmap); haApiWatch(nullptr); controller.setPage(Page::Bitmap); return; } Serial.printf("Page \"%s\" not found, showing the clock\n", shown); builtinPage(DEFAULT_PAGE, loaded_page); showDynamicPage(); } // Load the off bitmap into the frames; blank when it is gone static void applyOffPage() { uint8_t bitmap[BITMAP_BYTES]; if (settings.off_page[0] && loadBitmap(settings.off_page, bitmap)) frames.setOffBitmap(bitmap); else { if (settings.off_page[0]) Serial.printf("Off bitmap \"%s\" not found, the display is cleared instead\n", settings.off_page); frames.setOffBitmap(nullptr); } } static void apply() { controller.setSettings(settings.schedule, settings.clock); controller.setEffectTime(effectTime(settings.drive)); status.setDrive(settings.flip_time, settings.drive, settings.refresh.dots); frames.setDrive(settings.flip_time, settings.drive); char user[PLEX_USER_SIZE]; plexUserSlug(settings.plex_user, user, sizeof(user)); haApiSetSettings(settings.home_assistant, user); haApiWatchRules(&page_schedule); // {plex_user} may have changed 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; } void appLoadSettings() { loadSettings(settings); loadSchedule(page_schedule); selector.setSchedule(&page_schedule); selector.setChosen(settings.page); haApiWatchRules(&page_schedule); copyName(shown, settings.page); // until the time is known apply(); } 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; } const char *appSaveSettings(const Settings &new_settings, bool &restart_needed) { const char *error = validateSettings(new_settings); if (error) return error; 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 = !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 || a.effect_small_only != b.effect_small_only || settings.refresh.dots != new_settings.refresh.dots; settings = new_settings; saveSettings(settings); selector.setChosen(settings.page); apply(); if (drive_changed) status.sendDrive(); return nullptr; } // The chosen page, without overriding the schedule static void setChosen(const char *name) { copyName(settings.page, name); saveSettings(settings); selector.setChosen(name); } const char *appSelectPage(const char *name) { if (!pageExists(name) && !bitmapExists(name)) return "Page not found"; setChosen(name); selector.pick(name); if (!time_known) { copyName(shown, name); applyPage(); } return nullptr; } const char *appSaveSchedule(const PageSchedule &schedule) { const char *error = validateSchedule(schedule); if (error) return error; for (int i = 0; i < schedule.block_count; i++) for (int j = 0; j < schedule.blocks[i].item_count; j++) { const char *page = schedule.blocks[i].items[j].page; if (!pageExists(page) && !bitmapExists(page)) return "A page of the schedule was not found"; } for (int i = 0; i < schedule.rule_count; i++) if (!pageExists(schedule.rules[i].page) && !bitmapExists(schedule.rules[i].page)) return "The page of a rule was not found"; page_schedule = schedule; saveSchedule(page_schedule); selector.setSchedule(&page_schedule); haApiWatchRules(&page_schedule); return nullptr; } const PageSchedule &appSchedule() { return page_schedule; } void appBackToSchedule() { selector.backToSchedule(); } const char *appShownPage() { return shown; } void appShowStatus(char *out, size_t size) { static const char *const MODES[] = {"while true", "in rotation", "for a while"}; switch (shown_choice.reason) { case ShowReason::Trigger: case ShowReason::Rotation: if (shown_choice.rule >= 0 && shown_choice.rule < page_schedule.rule_count) { const TriggerRule &r = page_schedule.rules[shown_choice.rule]; snprintf(out, size, "rule %d (%s, %s)", shown_choice.rule + 1, r.entity, MODES[(int)r.mode]); return; } break; case ShowReason::Schedule: snprintf(out, size, "schedule"); return; default: break; } int until = selector.pickedUntil(); if (until >= 0) snprintf(out, size, "chosen, until %02d:%02d", until / 60, until % 60); else snprintf(out, size, selector.scheduleActive() ? "chosen" : "chosen (no schedule)"); } // Which page now: the rules (from Home Assistant), the schedule, the chosen page void appPageLoop(bool time_valid, const LocalTime &time) { uint32_t now = millis(); for (int i = 0; i < page_schedule.rule_count; i++) { const TriggerRule &r = page_schedule.rules[i]; selector.setRuleState(i, ruleMatches(r, haApiValue(r.entity, r.attribute)), now); } time_known = time_valid; if (!time_valid) return; shown_choice = selector.update(time.hour * 60 + time.minute, now); if (!shown_choice.changed && sameName(shown, shown_choice.page)) return; copyName(shown, shown_choice.page); applyPage(shown_choice.instant); } const char *appSelectOffPage(const char *name) { if (name[0] && !bitmapExists(name)) return "Bitmap not found"; copyName(settings.off_page, name); saveSettings(settings); applyOffPage(); controller.pageChanged(); return nullptr; } const char *appSaveBitmap(const char *name, const uint8_t *bitmap) { 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(); 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; } static const char *USED_BY_SCHEDULE = "Used by the schedule or a rule: change that first"; const char *appDeleteBitmap(const char *name) { if (scheduleUses(page_schedule, name)) return USED_BY_SCHEDULE; String user = pageUsingBitmap(name); if (user.length()) return usedBy(user); const char *error = deleteBitmap(name); if (error) return error; if (sameName(settings.page, name)) setChosen(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(shown, name)) reloadPage(); return nullptr; } const char *appDeletePage(const char *name) { if (scheduleUses(page_schedule, name)) return USED_BY_SCHEDULE; const char *error = deletePage(name); if (error) return error; if (sameName(settings.page, name)) setChosen(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 appStopwatch(StopwatchAction action) { if (action == StopwatchAction::Start) stopwatch.start(millis()); else if (action == StopwatchAction::Stop) stopwatch.stop(millis()); else stopwatch.reset(); controller.pageChanged(); } void appShowLive(const uint8_t *bitmap) { frames.setLiveBitmap(bitmap); controller.showLive(millis()); } 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; time.second = now.tm_sec; struct timeval tv; gettimeofday(&tv, nullptr); time.millisecond = tv.tv_usec / 1000; // Local time - UTC (summer time included), for Home Assistant's UTC timestamps struct tm utc = {}; time_t seconds = tv.tv_sec; gmtime_r(&seconds, &utc); int day_diff = now.tm_yday - utc.tm_yday; if (day_diff > 1) // around new year day_diff = -1; else if (day_diff < -1) day_diff = 1; time.utc_offset = day_diff * 24 * 60 + (now.tm_hour - utc.tm_hour) * 60 + (now.tm_min - utc.tm_min); return true; } void appRestartLater(uint32_t delay_ms) { restart_pending = true; restart_at = millis() + delay_ms; } void appLoop() { if (restart_pending && (int32_t)(millis() - restart_at) >= 0) { Serial.println("Restarting for the new settings"); ESP.restart(); } }