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

+11 -2
View File
@@ -15,10 +15,10 @@ struct BuiltinBitmap
static const BuiltinBitmap BUILTIN[] = {
{"Nintendo", nintendo_bitmap_Nintendo_new},
{"Nintendo border", nintendo_bitmap_nintendo_border},
};
static bool mounted = false;
static uint32_t revision = 0;
static const BuiltinBitmap *builtin(const char *name)
{
@@ -77,6 +77,11 @@ std::vector<BitmapInfo> listBitmaps()
return list;
}
uint32_t bitmapListRevision()
{
return revision;
}
bool bitmapExists(const char *name)
{
if (builtin(name))
@@ -118,6 +123,7 @@ const char *saveBitmap(const char *name, const uint8_t *bitmap)
return "Could not write the file";
bool ok = file.write(bitmap, BITMAP_BYTES) == (size_t)BITMAP_BYTES;
file.close();
revision++;
return ok ? nullptr : "Storage full";
}
@@ -127,5 +133,8 @@ const char *deleteBitmap(const char *name)
return "Built-in bitmaps can't be deleted";
if (!mounted || !validBitmapName(name) || !LittleFS.exists(path(name)))
return "Bitmap not found";
return LittleFS.remove(path(name)) ? nullptr : "Could not delete the file";
if (!LittleFS.remove(path(name)))
return "Could not delete the file";
revision++;
return nullptr;
}
+2
View File
@@ -24,6 +24,8 @@ bool bitmapStorageAvailable();
void bitmapStorageUsage(size_t &used, size_t &total);
std::vector<BitmapInfo> listBitmaps();
// Changes each time a bitmap is saved or deleted
uint32_t bitmapListRevision();
bool bitmapExists(const char *name);
bool loadBitmap(const char *name, uint8_t *bitmap);
-17
View File
@@ -17,23 +17,6 @@ const unsigned char nintendo_bitmap_Nintendo_new[] PROGMEM = {
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
0x00};
// 'nintendo border', 84x19px
const unsigned char nintendo_bitmap_nintendo_border[] PROGMEM = {
0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x80, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x60, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x60, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xc0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x30, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xc0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x30, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x60,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0,
0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x0f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
0x00};
// 'panda', 17x18px
const unsigned char nintendo_bitmap_panda_inverted[] PROGMEM = {
0xfe, 0x7f, 0x80, 0xfc, 0x3c, 0x80, 0xfc, 0x38, 0x00, 0xdd, 0xfe, 0x00, 0xbb, 0xff, 0x80, 0x73,
+10 -11
View File
@@ -3,7 +3,6 @@
#include <stdio.h>
#include <string.h>
#include "display/bitmap_store.h"
#include "display/bitmaps.h"
#include "display/text.h"
@@ -51,17 +50,9 @@ void FlipdotFrames::live()
_drawFullScreen(_live);
}
void FlipdotFrames::logo()
void FlipdotFrames::off()
{
uint8_t logo[BITMAP_BYTES];
if (loadBitmap("Nintendo", logo))
_drawFullScreen(logo);
}
void FlipdotFrames::blank()
{
_display.fillScreen(0);
_display.display();
_drawFullScreen(_off);
}
void FlipdotFrames::setPageBitmap(const uint8_t *bitmap)
@@ -74,6 +65,14 @@ void FlipdotFrames::setLiveBitmap(const uint8_t *bitmap)
memcpy(_live, bitmap, BITMAP_BYTES);
}
void FlipdotFrames::setOffBitmap(const uint8_t *bitmap)
{
if (bitmap)
memcpy(_off, bitmap, BITMAP_BYTES);
else
memset(_off, 0, BITMAP_BYTES);
}
void FlipdotFrames::_drawFullScreen(const uint8_t *bitmap)
{
_display.fillScreen(0);
+5 -3
View File
@@ -18,17 +18,19 @@ public:
void page() override;
// The image set with setLiveBitmap()
void live() override;
// Nintendo logo
void logo() override;
void blank() override;
// Blank, or the bitmap set with setOffBitmap()
void off() override;
void setPageBitmap(const uint8_t *bitmap);
void setLiveBitmap(const uint8_t *bitmap);
// nullptr: blank
void setOffBitmap(const uint8_t *bitmap);
private:
BROSE9323 &_display;
uint8_t _page[BITMAP_BYTES] = {};
uint8_t _live[BITMAP_BYTES] = {};
uint8_t _off[BITMAP_BYTES] = {}; // all zero: blank
void _drawFullScreen(const uint8_t *bitmap);
};
+23 -3
View File
@@ -9,9 +9,27 @@ StatusScreen::StatusScreen(BROSE9323 &display, NanoLink &nano) : _display(displa
{
}
void StatusScreen::setFlipTime(uint16_t flip_time)
void StatusScreen::setDrive(uint16_t flip_time, const DriveSettings &drive)
{
_flip_time = flip_time;
_drive = drive;
}
void StatusScreen::sendDrive()
{
// Not sent right away: the Nano could be in its bootloader (flashing)
_drive_pending = true;
}
void StatusScreen::loop()
{
if (!_drive_pending || !_started || !_nano.isOn())
return;
_drive_pending = false;
_display.setTiming(_flip_time);
_display.setGap(_drive.flip_gap);
_display.setPulses(_drive.pulses);
_display.setEffect(_drive.effect, _drive.effect_time, _drive.effect_small_only ? EFFECT_FLAG_SMALL_ONLY : 0);
}
void StatusScreen::show(const char *line1, const char *line2, bool keep_on)
@@ -21,8 +39,9 @@ void StatusScreen::show(const char *line1, const char *line2, bool keep_on)
if (!_started)
{
_display.begin(&_nano.stream());
_display.setTiming(_flip_time);
_started = true;
_drive_pending = true;
loop();
}
drawTextLines(_display, line1, line2);
_display.display();
@@ -30,7 +49,8 @@ void StatusScreen::show(const char *line1, const char *line2, bool keep_on)
if (!keep_on)
{
// The first frame after power on flips every dot, give the Nano time for that
delay(STATUS_FLIP_TIME);
// (a push can take longer, but these messages are short: at worst it is cut off)
delay(STATUS_FLIP_TIME + (_drive.effect == DRIVE_EFFECT_INSTANT ? 0 : _drive.effect_time));
_nano.off();
}
}
+10 -2
View File
@@ -1,6 +1,7 @@
#pragma once
#include "BROSE9323.h"
#include "logic/settings.h"
#include "nano_link/nano_link.h"
// Short messages for situations outside the normal clock: booting, crashes, recovery app.
@@ -14,12 +15,19 @@ public:
// power off again once the dots have flipped: flipdots keep their image without power.
void show(const char *line1, const char *line2, bool keep_on = false);
// Flip time sent to the Nano with the first message (default FLIP_TIME)
void setFlipTime(uint16_t flip_time);
// How the Nano drives the dots, sent with the first message (default: FLIP_TIME and
// the DriveSettings defaults, so the recovery app always shows its messages instantly)
void setDrive(uint16_t flip_time, const DriveSettings &drive);
// Send them again (they changed), from loop()
void sendDrive();
// Sends changed drive settings once the display is on. Not while the Nano is flashed.
void loop();
private:
BROSE9323 &_display;
NanoLink &_nano;
bool _started = false;
bool _drive_pending = false;
uint16_t _flip_time;
DriveSettings _drive;
};