Add HA dropdowns, off image, flip fixes and refresh effects

Home Assistant: remove the Clock switch (its entity is deleted on connect),
add an IP sensor and Mode / Off image dropdowns whose options follow the
saved bitmaps. Remove the built-in "Nintendo border" bitmap.

Off image: a bitmap (or blank) left on the dots when the display switches off.

Nano: pause after each flip pulse and repeat pulses for changed dots, both
stored in EEPROM (G/N commands). Refresh effects (E command): instant,
sparkle, wipe, rain, from the middle out, diagonal, spiral, flip all then
reveal, push and random, optionally only for small changes. Effects run in
steps from loop() so serial input keeps flowing.

Web UI: flip settings, refresh effect and test patterns under Settings; the
System tab shows why the ESP32 last restarted. Settings records from
versions 1 and 2 are migrated. Firmware 2.3.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-15 01:29:01 +02:00
1 parent 4a5f6a8b5d
commit 1d3b084de1
42 files changed
+1631 -230

No files matched your search

+9 -28
View File
@@ -14,6 +14,11 @@ void DisplayController::setSettings(const ScheduleSettings &schedule, const Cloc
_content_revision++; // e.g. evening time changed: redraw
}
void DisplayController::setEffectTime(uint32_t ms)
{
_effect_time = ms;
}
void DisplayController::setPage(Page page)
{
_page = page;
@@ -43,21 +48,11 @@ bool DisplayController::liveActive() const
return _live;
}
void DisplayController::setClockEnabled(bool enabled)
{
_clock_enabled = enabled;
}
void DisplayController::setPowerEnabled(bool enabled)
{
_power_enabled = enabled;
}
bool DisplayController::clockEnabled() const
{
return _clock_enabled;
}
bool DisplayController::powerEnabled() const
{
return _power_enabled;
@@ -92,8 +87,8 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu
{
if (_power.isOn())
{
_showOnce(Screen::Blank);
_off_timer.request(now, CLEAR_BEFORE_OFF_TIME);
_showOnce(Screen::Off);
_off_timer.request(now, CLEAR_BEFORE_OFF_TIME + _effect_time);
}
return;
}
@@ -111,23 +106,12 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu
if (!time_valid)
return;
if (!_clock_enabled)
{
// The logo is only shown while the display is on; it never switches the power on
if (_power.isOn())
{
_showOnce(Screen::Logo);
_off_timer.request(now, POWER_OFF_DELAY);
}
return;
}
switch (scheduleFor(hour, minute, _schedule))
{
case ScheduleMode::Night:
if (_power.isOn())
{
_showOnce(Screen::Blank);
_showOnce(Screen::Off);
_off_timer.request(now, POWER_OFF_DELAY);
}
break;
@@ -186,11 +170,8 @@ void DisplayController::_showOnce(Screen screen)
case Screen::Live:
_frames.live();
break;
case Screen::Logo:
_frames.logo();
break;
default:
_frames.blank();
_frames.off();
break;
}
_screen = screen;
+9 -9
View File
@@ -26,8 +26,8 @@ public:
virtual void page() = 0;
// The image sent from the web UI's live mode
virtual void live() = 0;
virtual void logo() = 0;
virtual void blank() = 0;
// Left on the dots when the display switches off: blank or the off bitmap
virtual void off() = 0;
};
enum class Page : uint8_t
@@ -45,8 +45,12 @@ public:
DisplayController(DisplayPower &power, Frames &frames);
void setSettings(const ScheduleSettings &schedule, const ClockSettings &clock);
// 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);
// What is shown during the day. Call pageChanged() when the bitmap itself changed.
// What is shown during the day. Call pageChanged() when the page or off bitmap
// itself changed.
void setPage(Page page);
void pageChanged();
@@ -56,12 +60,9 @@ public:
void stopLive();
bool liveActive() const;
// Home Assistant "FlipDot Display Clock" switch: off shows the logo instead of the page
void setClockEnabled(bool enabled);
// Home Assistant "FlipDot Display Power" switch: off clears the display and powers it off
void setPowerEnabled(bool enabled);
bool clockEnabled() const;
bool powerEnabled() const;
// Call often (every loop). time_valid is false until the time is synced over NTP.
@@ -76,8 +77,7 @@ private:
NoWifi,
Page,
Live,
Logo,
Blank,
Off, // blank or the off bitmap
};
DisplayPower &_power;
@@ -87,8 +87,8 @@ private:
ScheduleSettings _schedule;
ClockSettings _clock;
Page _page = Page::Clock;
uint32_t _effect_time = 0;
bool _clock_enabled = true;
bool _power_enabled = true;
Screen _screen = Screen::Unknown;
+21
View File
@@ -15,6 +15,8 @@ const char *validateSettings(const Settings &s)
if (!terminated(s.page, sizeof(s.page)) ||
(strcmp(s.page, PAGE_CLOCK) != 0 && !validBitmapName(s.page)))
return "Unknown page";
if (!terminated(s.off_page, sizeof(s.off_page)) || (s.off_page[0] && !validBitmapName(s.off_page)))
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)
@@ -28,6 +30,16 @@ const char *validateSettings(const Settings &s)
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";
const DriveSettings &d = s.drive;
if (d.flip_gap > 5000)
return "The flip gap must be between 0 and 5000 us";
if (d.pulses < 1 || d.pulses > 3)
return "Pulses must be 1, 2 or 3";
if (d.effect >= DRIVE_EFFECT_COUNT)
return "Unknown refresh effect";
if (d.effect_time < 100 || d.effect_time > 10000)
return "The effect time must be between 100 and 10000 ms";
const HomeAssistantSettings &ha = s.home_assistant;
if (!terminated(ha.host, sizeof(ha.host)) || !terminated(ha.user, sizeof(ha.user)) ||
!terminated(ha.password, sizeof(ha.password)))
@@ -38,6 +50,15 @@ const char *validateSettings(const Settings &s)
return nullptr;
}
uint32_t effectTime(const DriveSettings &drive)
{
if (drive.effect == DRIVE_EFFECT_INSTANT)
return 0;
if (drive.effect == DRIVE_EFFECT_PUSH || drive.effect == DRIVE_EFFECT_RANDOM)
return drive.effect_time + DRIVE_PUSH_EXTRA_TIME;
return drive.effect_time;
}
bool parseTime(const char *text, uint16_t &minutes)
{
if (text == nullptr)
+28
View File
@@ -32,9 +32,29 @@ struct HomeAssistantSettings
char password[64] = "";
};
// How the Nano drives the dots (besides the flip time). Sent to the Nano, which keeps
// them in its EEPROM. The defaults match the Nano's (src/nano/config.h).
const uint8_t DRIVE_EFFECT_INSTANT = 0; // numbers: RefreshEffect in lib/BROSE9323/dot_sequence.h
const uint8_t DRIVE_EFFECT_PUSH = 8;
const uint8_t DRIVE_EFFECT_RANDOM = 9;
const uint8_t DRIVE_EFFECT_COUNT = 10;
// Pushing flips many dots per column step, so it can take much longer than effect_time
const uint32_t DRIVE_PUSH_EXTRA_TIME = 20000;
struct DriveSettings
{
uint16_t flip_gap = 200; // pause after each pulse in us (0-5000)
uint8_t pulses = 2; // pulses for each changed dot (1-3)
uint8_t effect = DRIVE_EFFECT_INSTANT;
uint16_t effect_time = 1500; // ms (100-10000)
bool effect_small_only = false; // big changes (a new page) appear instantly
};
// What is shown during the day: "clock" or the name of a bitmap
const int PAGE_NAME_SIZE = 25;
#define PAGE_CLOCK "clock"
// What is left on the dots when the display switches off: empty = blank, or a bitmap name
#define OFF_PAGE_BLANK ""
struct Settings
{
@@ -44,8 +64,16 @@ struct Settings
HomeAssistantSettings home_assistant;
uint16_t flip_time = 225; // flip pulse length in us, sent to the Nano
bool status_led = true;
// Added in settings version 2: keep it last, the fields before it are read from
// version 1 records (system/settings_store.cpp)
char off_page[PAGE_NAME_SIZE] = OFF_PAGE_BLANK;
// Added in version 3
DriveSettings drive;
};
// How long a new frame can take to appear on the dots in ms (0 when instant)
uint32_t effectTime(const DriveSettings &drive);
const uint16_t MINUTES_PER_DAY = 24 * 60;
const uint16_t SETTINGS_FLIP_TIME_MIN = 50;
const uint16_t SETTINGS_FLIP_TIME_MAX = 2000;