2026-10-13 01:52:34 +02:00
|
|
|
#include "app.h"
|
|
|
|
|
|
|
|
|
|
#include <string.h>
|
2026-10-15 21:01:14 +02:00
|
|
|
#include <sys/time.h>
|
2026-10-15 19:43:59 +02:00
|
|
|
#include <time.h>
|
2026-10-13 01:52:34 +02:00
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
#include "display/bitmap_store.h"
|
2026-10-15 19:43:59 +02:00
|
|
|
#include "display/font_store.h"
|
|
|
|
|
#include "display/page_resources.h"
|
|
|
|
|
#include "display/page_store.h"
|
2026-10-15 21:36:56 +02:00
|
|
|
#include "logic/page_selector.h"
|
2026-10-15 19:43:59 +02:00
|
|
|
#include "net/ha_api.h"
|
2026-10-15 21:36:56 +02:00
|
|
|
#include "system/schedule_store.h"
|
2026-10-13 01:52:34 +02:00
|
|
|
#include "system/settings_store.h"
|
|
|
|
|
|
2026-10-15 21:36:56 +02:00
|
|
|
static void copyName(char *out, const char *name)
|
|
|
|
|
{
|
|
|
|
|
strncpy(out, name, PAGE_NAME_SIZE - 1);
|
|
|
|
|
out[PAGE_NAME_SIZE - 1] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
NanoLink nano(Serial2, NANO_RX_PIN, NANO_TX_PIN, RELAY_PIN, NANO_RESET_PIN);
|
|
|
|
|
BROSE9323 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, PANEL_WIDTH);
|
2026-10-15 19:43:59 +02:00
|
|
|
FlipdotFrames frames(display, pageResources);
|
2026-10-13 01:52:34 +02:00
|
|
|
StatusScreen status(display, nano);
|
|
|
|
|
DisplayController controller(nano, frames);
|
2026-10-16 15:20:26 +02:00
|
|
|
LoopStats loopStats;
|
|
|
|
|
NanoMonitor nanoMonitor(nano, display);
|
2026-10-13 01:52:34 +02:00
|
|
|
|
|
|
|
|
static Settings settings;
|
|
|
|
|
static bool restart_pending = false;
|
|
|
|
|
static uint32_t restart_at = 0;
|
2026-10-15 19:43:59 +02:00
|
|
|
// Not on the stack: about 1.7 KB
|
|
|
|
|
static DynamicPage loaded_page;
|
2026-10-15 21:36:56 +02:00
|
|
|
// 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;
|
2026-10-13 01:52:34 +02:00
|
|
|
|
2026-10-15 19:43:59 +02:00
|
|
|
static void showDynamicPage()
|
|
|
|
|
{
|
|
|
|
|
frames.setDynamicPage(loaded_page);
|
|
|
|
|
haApiWatch(&loaded_page);
|
|
|
|
|
controller.setPage(Page::Dynamic);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:36:56 +02:00
|
|
|
// 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)
|
2026-10-13 01:52:34 +02:00
|
|
|
{
|
2026-10-15 21:36:56 +02:00
|
|
|
if (instant)
|
|
|
|
|
frames.showNextInstant();
|
|
|
|
|
if (loadPage(shown, loaded_page))
|
2026-10-13 01:52:34 +02:00
|
|
|
{
|
2026-10-15 19:43:59 +02:00
|
|
|
showDynamicPage();
|
|
|
|
|
return;
|
2026-10-13 01:52:34 +02:00
|
|
|
}
|
2026-10-15 19:43:59 +02:00
|
|
|
uint8_t bitmap[BITMAP_BYTES];
|
2026-10-15 21:36:56 +02:00
|
|
|
if (loadBitmap(shown, bitmap))
|
2026-10-15 19:43:59 +02:00
|
|
|
{
|
|
|
|
|
frames.setPageBitmap(bitmap);
|
|
|
|
|
haApiWatch(nullptr);
|
|
|
|
|
controller.setPage(Page::Bitmap);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-10-15 21:36:56 +02:00
|
|
|
Serial.printf("Page \"%s\" not found, showing the clock\n", shown);
|
2026-10-15 19:43:59 +02:00
|
|
|
builtinPage(DEFAULT_PAGE, loaded_page);
|
|
|
|
|
showDynamicPage();
|
2026-10-13 01:52:34 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-15 01:29:01 +02:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
static void apply()
|
|
|
|
|
{
|
|
|
|
|
controller.setSettings(settings.schedule, settings.clock);
|
2026-10-15 01:29:01 +02:00
|
|
|
controller.setEffectTime(effectTime(settings.drive));
|
2026-10-16 15:20:26 +02:00
|
|
|
status.setDrive(settings.flip_time, settings.drive, settings.refresh.dots);
|
2026-10-15 21:01:14 +02:00
|
|
|
frames.setDrive(settings.flip_time, settings.drive);
|
2026-10-15 19:43:59 +02:00
|
|
|
haApiSetSettings(settings.home_assistant);
|
2026-10-15 01:29:01 +02:00
|
|
|
applyOffPage();
|
2026-10-13 01:52:34 +02:00
|
|
|
applyPage();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 19:43:59 +02:00
|
|
|
// Something the current page may use changed: load it again and redraw
|
|
|
|
|
static void reloadPage()
|
|
|
|
|
{
|
|
|
|
|
applyPage();
|
|
|
|
|
controller.pageChanged();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
const Settings &appSettings()
|
|
|
|
|
{
|
|
|
|
|
return settings;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void appLoadSettings()
|
|
|
|
|
{
|
|
|
|
|
loadSettings(settings);
|
2026-10-15 21:36:56 +02:00
|
|
|
loadSchedule(page_schedule);
|
|
|
|
|
selector.setSchedule(&page_schedule);
|
|
|
|
|
selector.setChosen(settings.page);
|
|
|
|
|
haApiWatchRules(&page_schedule);
|
|
|
|
|
copyName(shown, settings.page); // until the time is known
|
2026-10-13 01:52:34 +02:00
|
|
|
apply();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 19:43:59 +02:00
|
|
|
static bool sameMqtt(const HomeAssistantSettings &a, const HomeAssistantSettings &b)
|
2026-10-13 01:52:34 +02:00
|
|
|
{
|
|
|
|
|
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;
|
2026-10-15 19:43:59 +02:00
|
|
|
if (!pageExists(new_settings.page) && !bitmapExists(new_settings.page))
|
|
|
|
|
return "Page not found";
|
2026-10-15 01:29:01 +02:00
|
|
|
if (new_settings.off_page[0] && !bitmapExists(new_settings.off_page))
|
|
|
|
|
return "Off bitmap not found";
|
2026-10-13 01:52:34 +02:00
|
|
|
|
2026-10-15 19:43:59 +02:00
|
|
|
restart_needed = !sameMqtt(settings.home_assistant, new_settings.home_assistant);
|
2026-10-15 01:29:01 +02:00
|
|
|
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 ||
|
2026-10-16 15:20:26 +02:00
|
|
|
a.effect_small_only != b.effect_small_only ||
|
|
|
|
|
settings.refresh.dots != new_settings.refresh.dots;
|
2026-10-13 01:52:34 +02:00
|
|
|
settings = new_settings;
|
|
|
|
|
saveSettings(settings);
|
2026-10-15 21:36:56 +02:00
|
|
|
selector.setChosen(settings.page);
|
2026-10-13 01:52:34 +02:00
|
|
|
apply();
|
2026-10-15 01:29:01 +02:00
|
|
|
if (drive_changed)
|
|
|
|
|
status.sendDrive();
|
2026-10-13 01:52:34 +02:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:36:56 +02:00
|
|
|
|
|
|
|
|
// The chosen page, without overriding the schedule
|
|
|
|
|
static void setChosen(const char *name)
|
2026-10-15 19:43:59 +02:00
|
|
|
{
|
2026-10-15 21:36:56 +02:00
|
|
|
copyName(settings.page, name);
|
|
|
|
|
saveSettings(settings);
|
|
|
|
|
selector.setChosen(name);
|
2026-10-15 19:43:59 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
const char *appSelectPage(const char *name)
|
|
|
|
|
{
|
2026-10-15 19:43:59 +02:00
|
|
|
if (!pageExists(name) && !bitmapExists(name))
|
|
|
|
|
return "Page not found";
|
2026-10-15 21:36:56 +02:00
|
|
|
setChosen(name);
|
|
|
|
|
selector.pick(name);
|
|
|
|
|
if (!time_known)
|
|
|
|
|
{
|
|
|
|
|
copyName(shown, name);
|
|
|
|
|
applyPage();
|
|
|
|
|
}
|
2026-10-13 01:52:34 +02:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:36:56 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 01:29:01 +02:00
|
|
|
const char *appSelectOffPage(const char *name)
|
|
|
|
|
{
|
|
|
|
|
if (name[0] && !bitmapExists(name))
|
|
|
|
|
return "Bitmap not found";
|
2026-10-15 19:43:59 +02:00
|
|
|
copyName(settings.off_page, name);
|
2026-10-15 01:29:01 +02:00
|
|
|
saveSettings(settings);
|
|
|
|
|
applyOffPage();
|
|
|
|
|
controller.pageChanged();
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 19:43:59 +02:00
|
|
|
const char *appSaveBitmap(const char *name, const uint8_t *bitmap)
|
2026-10-13 01:52:34 +02:00
|
|
|
{
|
2026-10-15 19:43:59 +02:00
|
|
|
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))
|
2026-10-15 01:29:01 +02:00
|
|
|
applyOffPage();
|
2026-10-15 19:43:59 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:36:56 +02:00
|
|
|
static const char *USED_BY_SCHEDULE = "Used by the schedule or a rule: change that first";
|
|
|
|
|
|
2026-10-15 19:43:59 +02:00
|
|
|
const char *appDeleteBitmap(const char *name)
|
|
|
|
|
{
|
2026-10-15 21:36:56 +02:00
|
|
|
if (scheduleUses(page_schedule, name))
|
|
|
|
|
return USED_BY_SCHEDULE;
|
2026-10-15 19:43:59 +02:00
|
|
|
String user = pageUsingBitmap(name);
|
|
|
|
|
if (user.length())
|
|
|
|
|
return usedBy(user);
|
|
|
|
|
const char *error = deleteBitmap(name);
|
|
|
|
|
if (error)
|
|
|
|
|
return error;
|
|
|
|
|
if (sameName(settings.page, name))
|
2026-10-15 21:36:56 +02:00
|
|
|
setChosen(DEFAULT_PAGE);
|
2026-10-15 19:43:59 +02:00
|
|
|
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;
|
2026-10-15 21:36:56 +02:00
|
|
|
if (sameName(shown, name))
|
2026-10-15 19:43:59 +02:00
|
|
|
reloadPage();
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char *appDeletePage(const char *name)
|
|
|
|
|
{
|
2026-10-15 21:36:56 +02:00
|
|
|
if (scheduleUses(page_schedule, name))
|
|
|
|
|
return USED_BY_SCHEDULE;
|
2026-10-15 19:43:59 +02:00
|
|
|
const char *error = deletePage(name);
|
|
|
|
|
if (error)
|
|
|
|
|
return error;
|
|
|
|
|
if (sameName(settings.page, name))
|
2026-10-15 21:36:56 +02:00
|
|
|
setChosen(DEFAULT_PAGE);
|
2026-10-15 19:43:59 +02:00
|
|
|
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);
|
2026-10-13 01:52:34 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
void appStopwatch(StopwatchAction action)
|
|
|
|
|
{
|
|
|
|
|
if (action == StopwatchAction::Start)
|
|
|
|
|
stopwatch.start(millis());
|
|
|
|
|
else if (action == StopwatchAction::Stop)
|
|
|
|
|
stopwatch.stop(millis());
|
|
|
|
|
else
|
|
|
|
|
stopwatch.reset();
|
|
|
|
|
controller.pageChanged();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
void appShowLive(const uint8_t *bitmap)
|
|
|
|
|
{
|
|
|
|
|
frames.setLiveBitmap(bitmap);
|
|
|
|
|
controller.showLive(millis());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void appStopLive()
|
|
|
|
|
{
|
|
|
|
|
controller.stopLive();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 19:43:59 +02:00
|
|
|
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;
|
2026-10-15 21:01:14 +02:00
|
|
|
time.second = now.tm_sec;
|
|
|
|
|
struct timeval tv;
|
|
|
|
|
gettimeofday(&tv, nullptr);
|
|
|
|
|
time.millisecond = tv.tv_usec / 1000;
|
2026-10-15 21:50:19 +02:00
|
|
|
// 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);
|
2026-10-15 19:43:59 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:52:34 +02:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|