Add a web UI to the clock firmware

- Web UI on port 80: live view of the display, choose the page (clock or
  a bitmap), bitmaps (list, show, live, edit, delete), draw / image upload
  with conversion in the browser, live mode, settings, Home Assistant
- Live view from a shadow buffer in the BROSE9323 driver (shownPixel)
- Settings in NVS with the original behaviour as defaults: schedule times,
  quarter hours, yawning Pikachu, No WiFi screen, flip time, status LED,
  Home Assistant broker (restart on change)
- Bitmaps on LittleFS; new 320 KB partition, ota slots now 1.31 MB
- DisplayController: pages, live mode, settings; app.cpp holds the objects
- README for the recovery app; docs for the web UI
- Tests: settings, time parsing, bitmap format and names, schedule with
  settings, pages and live mode (95 total)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-13 01:52:34 +02:00
1 parent c9e0800e36
commit 2b1ac12ed7
34 files changed
+1989 -117

No files matched your search

+93 -26
View File
@@ -7,6 +7,42 @@ DisplayController::DisplayController(DisplayPower &power, Frames &frames) : _pow
{
}
void DisplayController::setSettings(const ScheduleSettings &schedule, const ClockSettings &clock)
{
_schedule = schedule;
_clock = clock;
_content_revision++; // e.g. evening time changed: redraw
}
void DisplayController::setPage(Page page)
{
_page = page;
_content_revision++;
}
void DisplayController::pageChanged()
{
_content_revision++;
}
void DisplayController::showLive(uint32_t now)
{
_live = true;
_live_since = now;
_content_revision++;
}
void DisplayController::stopLive()
{
_live = false;
_content_revision++;
}
bool DisplayController::liveActive() const
{
return _live;
}
void DisplayController::setClockEnabled(bool enabled)
{
_clock_enabled = enabled;
@@ -36,7 +72,10 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu
_wifi_lost = true;
_wifi_lost_since = now;
}
bool show_no_wifi = _wifi_lost && now - _wifi_lost_since >= NO_WIFI_SHOW_TIME;
bool show_no_wifi = _clock.no_wifi_screen && _wifi_lost && now - _wifi_lost_since >= NO_WIFI_SHOW_TIME;
if (_live && now - _live_since >= LIVE_TIMEOUT)
stopLive();
// Switched off elsewhere (power timer, Nano flashing): the Nano lost its frame
if (!_power.isOn())
@@ -53,12 +92,21 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu
{
if (_power.isOn())
{
_showBlank();
_showOnce(Screen::Blank);
_off_timer.request(now, CLEAR_BEFORE_OFF_TIME);
}
return;
}
// Live mode from the web UI: shown right away, whatever the time
if (_live)
{
_off_timer.cancel();
_ensureOn();
_showOnce(Screen::Live);
return;
}
// Keep the boot screen until the time is known
if (!time_valid)
return;
@@ -68,64 +116,83 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu
// The logo is only shown while the display is on; it never switches the power on
if (_power.isOn())
{
_showLogo();
_showOnce(Screen::Logo);
_off_timer.request(now, POWER_OFF_DELAY);
}
return;
}
switch (scheduleFor(hour, minute))
switch (scheduleFor(hour, minute, _schedule))
{
case ScheduleMode::Night:
if (_power.isOn())
{
_showBlank();
_showOnce(Screen::Blank);
_off_timer.request(now, POWER_OFF_DELAY);
}
break;
case ScheduleMode::Clock:
_off_timer.cancel();
if (!_power.isOn())
{
_power.on();
_screen = Screen::Unknown;
}
_showClock(hour, minute, show_no_wifi);
_ensureOn();
if (_page == Page::Clock)
_showClock(hour, minute, show_no_wifi);
else
_showOnce(Screen::Page);
break;
case ScheduleMode::Standby:
// Flipdots keep their image without power, so the last time stays visible
// Flipdots keep their image without power, so the last image stays visible
if (_power.isOn())
_off_timer.request(now, POWER_OFF_DELAY);
break;
}
}
void DisplayController::_ensureOn()
{
if (_power.isOn())
return;
_power.on();
_screen = Screen::Unknown;
}
void DisplayController::_showClock(int hour, int minute, bool no_wifi)
{
Screen screen = no_wifi ? Screen::NoWifi : Screen::Clock;
if (_screen == screen && _shown_hour == hour && _shown_minute == minute)
bool evening = isEvening(hour, minute, _clock);
if (_screen == screen && _shown_hour == hour && _shown_minute == minute && _shown_evening == evening &&
_shown_revision == _content_revision)
return;
if (no_wifi)
_frames.noWifi(hour, minute);
else
_frames.clock(hour, minute);
_frames.clock(hour, minute, evening);
_screen = screen;
_shown_hour = hour;
_shown_minute = minute;
_shown_evening = evening;
_shown_revision = _content_revision;
}
void DisplayController::_showLogo()
// Screens without the time: only sent again when they or their content changed
void DisplayController::_showOnce(Screen screen)
{
if (_screen == Screen::Logo)
if (_screen == screen && _shown_revision == _content_revision)
return;
_frames.logo();
_screen = Screen::Logo;
}
void DisplayController::_showBlank()
{
if (_screen == Screen::Blank)
return;
_frames.blank();
_screen = Screen::Blank;
switch (screen)
{
case Screen::Page:
_frames.page();
break;
case Screen::Live:
_frames.live();
break;
case Screen::Logo:
_frames.logo();
break;
default:
_frames.blank();
break;
}
_screen = screen;
_shown_revision = _content_revision;
}