Slow mode replaces the late-evening quarter hours

Slow mode (Home Assistant switch "FlipDot Display Slow mode", dropdown
"FlipDot Display Slow mode interval", web UI Settings) makes the display
change only every 1, 2, 5, 10, 15, 20, 30 or 60 minutes, on the clock, and
powers it off in between: less noise. Nothing else is drawn meanwhile, also
no page changes. It updates at once after boot, the night, the Power switch
and live mode, and when it is switched off. Rules can override it ("show at
once in slow mode"): while their page is shown the display works normally.

Settings layout 7 and schedule layout 4 (older records are converted; they
are stored again once the firmware is marked good, so a rollback still
finds the old ones). Pages in older formats are rewritten in the current
one, in time-boxed steps. /api/status reports old_data_left.

Version 2.15.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-17 21:16:41 +02:00
1 parent a904615c51
commit 968492ca35
39 files changed
+726 -126

No files matched your search

+55 -6
View File
@@ -7,13 +7,28 @@ DisplayController::DisplayController(DisplayPower &power, Frames &frames) : _pow
{
}
void DisplayController::setSettings(const ScheduleSettings &schedule, const ClockSettings &clock)
void DisplayController::setSettings(const ScheduleSettings &schedule, const ClockSettings &clock, const SlowModeSettings &slow)
{
if (slow.enabled && !_slow.enabled)
_slow_keep = true;
_schedule = schedule;
_clock = clock;
_slow = slow;
_content_revision++; // e.g. the No WiFi setting changed: redraw
}
void DisplayController::setSlowModeOverride(bool active)
{
if (_slow_override && !active)
_slow_last = -1;
_slow_override = active;
}
bool DisplayController::slowModeActive() const
{
return _slow.enabled && !_slow_override;
}
void DisplayController::setEffectTime(uint32_t ms)
{
_effect_time = ms;
@@ -40,6 +55,7 @@ void DisplayController::showLive(uint32_t now)
void DisplayController::stopLive()
{
_live = false;
_slow_last = -1;
_content_revision++;
}
@@ -50,6 +66,8 @@ bool DisplayController::liveActive() const
void DisplayController::setPowerEnabled(bool enabled)
{
if (enabled && !_power_enabled)
_slow_last = -1;
_power_enabled = enabled;
}
@@ -109,6 +127,8 @@ void DisplayController::update(uint32_t now, bool time_valid, const LocalTime &t
switch (scheduleFor(time.hour, time.minute, _schedule))
{
case ScheduleMode::Night:
_slow_last = -1; // the morning starts with an update
_slow_keep = false;
if (_power.isOn())
{
_showOnce(Screen::Off);
@@ -116,6 +136,11 @@ void DisplayController::update(uint32_t now, bool time_valid, const LocalTime &t
}
break;
case ScheduleMode::Clock:
if (slowModeActive())
{
_slowClock(now, time, show_no_wifi);
break;
}
_off_timer.cancel();
_ensureOn();
if (_page == Page::Dynamic)
@@ -123,14 +148,38 @@ void DisplayController::update(uint32_t now, bool time_valid, const LocalTime &t
else
_showOnce(Screen::Page);
break;
case ScheduleMode::Standby:
// Flipdots keep their image without power, so the last image stays visible
if (_power.isOn())
_off_timer.request(now, POWER_OFF_DELAY);
break;
}
}
// Slow mode: one frame in each update minute, then the power goes off. Flipdots keep their
// image without power, so it stays visible until the next update.
void DisplayController::_slowClock(uint32_t now, const LocalTime &time, bool no_wifi)
{
int minute = time.hour * 60 + time.minute;
if (_slow_keep)
{
_slow_keep = false;
// Not the boot screen or an off screen: that is replaced right away
if (_power.isOn() && (_screen == Screen::Dynamic || _screen == Screen::NoWifi || _screen == Screen::Page))
_slow_last = minute;
}
bool due = _slow_last < 0 || (minute != _slow_last && slowModeUpdate(time.hour, time.minute, _slow.minutes));
if (due)
{
_slow_last = minute;
_off_timer.cancel();
_ensureOn();
_screen = Screen::Unknown; // the whole image, also when it looks the same
if (_page == Page::Dynamic)
_showDynamic(now, time, no_wifi);
else
_showOnce(Screen::Page);
_dynamic_moving = false; // moving pages stand still until the next update
}
if (_power.isOn())
_off_timer.request(now, POWER_OFF_DELAY + _effect_time);
}
void DisplayController::_ensureOn()
{
if (_power.isOn())
+14 -1
View File
@@ -49,7 +49,15 @@ class DisplayController
public:
DisplayController(DisplayPower &power, Frames &frames);
void setSettings(const ScheduleSettings &schedule, const ClockSettings &clock);
// Slow mode: the display only changes in the minutes of slowModeUpdate() (once, also
// when the page changed in between) and is powered off in between. Switching it on keeps
// the image until then; switching it off updates right away.
void setSettings(const ScheduleSettings &schedule, const ClockSettings &clock, const SlowModeSettings &slow);
// A rule that overrides slow mode shows its page: the display works normally meanwhile,
// and is updated right away when it ends
void setSlowModeOverride(bool active);
// Slow mode is switched on and no rule overrides it
bool slowModeActive() const;
// How long the Nano takes to show a frame with the refresh effect (ms, 0 = instant).
// The power stays on that much longer after the last frame.
void setEffectTime(uint32_t ms);
@@ -91,6 +99,10 @@ private:
ScheduleSettings _schedule;
ClockSettings _clock;
SlowModeSettings _slow;
int _slow_last = -1; // slow mode: minute of the day of the last update, -1 = update now
bool _slow_keep = false; // slow mode was switched on: the current image counts as updated
bool _slow_override = false;
Page _page = Page::Dynamic;
uint32_t _effect_time = 0;
@@ -110,6 +122,7 @@ private:
bool _wifi_lost = false;
uint32_t _wifi_lost_since = 0;
void _slowClock(uint32_t now, const LocalTime &time, bool no_wifi);
void _ensureOn();
void _showDynamic(uint32_t now, const LocalTime &time, bool no_wifi);
void _showOnce(Screen screen);
+1
View File
@@ -65,6 +65,7 @@ struct TriggerRule
uint16_t interval = 10; // read the entity this often (s)
uint8_t blocks = 0; // bit i: active during block i; 0: always (also without a schedule)
bool over_manual = true; // also over a page picked by hand (false: it waits until the pick ends)
bool over_slow_mode = false; // while its page is shown the display works normally (slow mode waits)
};
struct PageSchedule
+1
View File
@@ -159,6 +159,7 @@ PageChoice PageSelector::update(int minute, uint32_t now)
}
choice.changed = !sameName(choice.page, _shown);
choice.over_slow_mode = choice.rule >= 0 && choice.rule < rule_count && _schedule->rules[choice.rule].over_slow_mode;
// Rules bring their page at once, and leave at once; the schedule as set per block
choice.instant = choice.changed && (choice.reason == ShowReason::Trigger || _shown_reason == ShowReason::Trigger ||
!effect);
+1
View File
@@ -29,6 +29,7 @@ struct PageChoice
int rule = -1; // the rule (Rotation, Trigger)
bool changed = false; // another page than at the last update()
bool instant = false; // when changed: switch without the refresh effect
bool over_slow_mode = false; // shown by a rule that overrides slow mode
};
// The chosen page also rotates with AddToRotation pages, this long
+6 -3
View File
@@ -8,7 +8,10 @@ ScheduleMode scheduleFor(int hour, int minute, const ScheduleSettings &s)
int now = hour * 60 + minute;
if (now < s.day_start || now >= s.night_start)
return ScheduleMode::Night;
if (now < s.late_start || !s.quarter_hours || minute % 15 == 0)
return ScheduleMode::Clock;
return ScheduleMode::Standby;
return ScheduleMode::Clock;
}
bool slowModeUpdate(int hour, int minute, int interval)
{
return interval <= 1 || (hour * 60 + minute) % interval == 0;
}
+8 -4
View File
@@ -6,16 +6,20 @@
// settings:
//
// 00:00 - 07:59 Night screen cleared, power off
// 08:00 - 21:59 Clock page shown (a dynamic page is updated every minute)
// 22:00 - 23:30 Clock only during the minutes :00, :15, :30 and :45
// Standby in between: power off, the last image stays on the dots
// 08:00 - 23:30 Clock page shown (a dynamic page is updated every minute)
// 23:31 - 23:59 Night
//
// Slow mode (SlowModeSettings, handled by DisplayController) only changes how often the
// page is drawn during Clock.
enum class ScheduleMode
{
Night,
Clock,
Standby,
};
ScheduleMode scheduleFor(int hour, int minute, const ScheduleSettings &settings);
// Slow mode: true in the minutes when the display is updated (interval: one of
// SLOW_MODE_MINUTES, counted from midnight, so 15 = :00 :15 :30 :45)
bool slowModeUpdate(int hour, int minute, int interval);
+13 -3
View File
@@ -20,10 +20,12 @@ const char *validateSettings(const Settings &s)
return "Unknown off image";
const ScheduleSettings &t = s.schedule;
if (t.day_start >= MINUTES_PER_DAY || t.late_start > MINUTES_PER_DAY || t.night_start > MINUTES_PER_DAY)
if (t.day_start >= MINUTES_PER_DAY || t.night_start > MINUTES_PER_DAY)
return "Times must be between 00:00 and 24:00";
if (t.day_start > t.late_start || t.late_start > t.night_start)
return "The times must be in order: day start, late evening, night";
if (t.day_start > t.night_start)
return "The night must start after the day";
if (!validSlowModeMinutes(s.slow_mode.minutes))
return "Unknown slow mode interval";
if (s.flip_time < SETTINGS_FLIP_TIME_MIN || s.flip_time > SETTINGS_FLIP_TIME_MAX)
return "The flip time must be between 50 and 2000 us";
@@ -53,6 +55,14 @@ const char *validateSettings(const Settings &s)
return nullptr;
}
bool validSlowModeMinutes(int minutes)
{
for (uint8_t m : SLOW_MODE_MINUTES)
if (m == minutes)
return true;
return false;
}
uint32_t effectTime(const DriveSettings &drive)
{
if (drive.effect == DRIVE_EFFECT_INSTANT)
+13 -2
View File
@@ -14,11 +14,21 @@ struct ScheduleSettings
{
bool enabled = true; // false: the display is always on
uint16_t day_start = 8 * 60; // display on from
uint16_t late_start = 22 * 60; // from here only at the quarter hours (see quarter_hours)
bool quarter_hours = true; // false: on all the time until night_start
uint16_t night_start = 23 * 60 + 31; // display off from (up to 24:00 = never)
};
// Slow mode (Home Assistant switch "FlipDot Display Slow mode", web UI): the display only
// changes every `minutes` minutes, on the clock (15: at :00 :15 :30 :45), and is powered
// off in between. Fewer flips, so less noise.
const uint8_t SLOW_MODE_MINUTES[] = {1, 2, 5, 10, 15, 20, 30, 60}; // all divide an hour
const int SLOW_MODE_MINUTES_COUNT = sizeof(SLOW_MODE_MINUTES) / sizeof(SLOW_MODE_MINUTES[0]);
struct SlowModeSettings
{
bool enabled = false;
uint8_t minutes = 15; // one of SLOW_MODE_MINUTES
};
bool validSlowModeMinutes(int minutes);
struct ClockSettings
{
bool no_wifi_screen = true; // "No WiFi" instead of a dynamic page when WiFi is down
@@ -85,6 +95,7 @@ struct Settings
RefreshSettings refresh;
// Plex user name for {plex_user} in entity ids (Tautulli's sensors of that user)
char plex_user[PLEX_USER_SIZE] = "";
SlowModeSettings slow_mode;
// New fields go here, at the end (see system/settings_store.cpp)
};