Public Access
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:
1 parent
c9e0800e36
commit
2b1ac12ed7
34 files changed
+1989
-117
No files matched your search
@@ -0,0 +1,131 @@
|
||||
#include "display/bitmap_store.h"
|
||||
|
||||
#include <LittleFS.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "display/bitmaps.h"
|
||||
|
||||
static const char *DIR = "/bitmaps";
|
||||
|
||||
struct BuiltinBitmap
|
||||
{
|
||||
const char *name;
|
||||
const uint8_t *data; // PROGMEM, BITMAP_BYTES
|
||||
};
|
||||
|
||||
static const BuiltinBitmap BUILTIN[] = {
|
||||
{"Nintendo", nintendo_bitmap_Nintendo_new},
|
||||
{"Nintendo border", nintendo_bitmap_nintendo_border},
|
||||
};
|
||||
|
||||
static bool mounted = false;
|
||||
|
||||
static const BuiltinBitmap *builtin(const char *name)
|
||||
{
|
||||
for (const BuiltinBitmap &b : BUILTIN)
|
||||
if (strcasecmp(b.name, name) == 0)
|
||||
return &b;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static String path(const char *name)
|
||||
{
|
||||
return String(DIR) + "/" + name + ".bin";
|
||||
}
|
||||
|
||||
bool bitmapStoreBegin()
|
||||
{
|
||||
mounted = LittleFS.begin(true);
|
||||
if (mounted && !LittleFS.exists(DIR))
|
||||
LittleFS.mkdir(DIR);
|
||||
Serial.printf("Bitmap storage %s\n", mounted ? "ready" : "not available (old flash layout?)");
|
||||
return mounted;
|
||||
}
|
||||
|
||||
bool bitmapStorageAvailable()
|
||||
{
|
||||
return mounted;
|
||||
}
|
||||
|
||||
void bitmapStorageUsage(size_t &used, size_t &total)
|
||||
{
|
||||
used = mounted ? LittleFS.usedBytes() : 0;
|
||||
total = mounted ? LittleFS.totalBytes() : 0;
|
||||
}
|
||||
|
||||
std::vector<BitmapInfo> listBitmaps()
|
||||
{
|
||||
std::vector<BitmapInfo> list;
|
||||
for (const BuiltinBitmap &b : BUILTIN)
|
||||
list.push_back({b.name, true});
|
||||
if (!mounted)
|
||||
return list;
|
||||
|
||||
File dir = LittleFS.open(DIR);
|
||||
for (File file = dir.openNextFile(); file; file = dir.openNextFile())
|
||||
{
|
||||
String name = file.name();
|
||||
int slash = name.lastIndexOf('/');
|
||||
if (slash >= 0)
|
||||
name = name.substring(slash + 1);
|
||||
if (!name.endsWith(".bin"))
|
||||
continue;
|
||||
name.remove(name.length() - 4);
|
||||
if (validBitmapName(name.c_str()) && !builtin(name.c_str()))
|
||||
list.push_back({name, false});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
bool bitmapExists(const char *name)
|
||||
{
|
||||
if (builtin(name))
|
||||
return true;
|
||||
return mounted && validBitmapName(name) && LittleFS.exists(path(name));
|
||||
}
|
||||
|
||||
bool loadBitmap(const char *name, uint8_t *bitmap)
|
||||
{
|
||||
const BuiltinBitmap *b = builtin(name);
|
||||
if (b)
|
||||
{
|
||||
memcpy_P(bitmap, b->data, BITMAP_BYTES);
|
||||
return true;
|
||||
}
|
||||
if (!mounted || !validBitmapName(name))
|
||||
return false;
|
||||
File file = LittleFS.open(path(name), "r");
|
||||
if (!file)
|
||||
return false;
|
||||
bool ok = file.read(bitmap, BITMAP_BYTES) == (size_t)BITMAP_BYTES;
|
||||
file.close();
|
||||
return ok;
|
||||
}
|
||||
|
||||
const char *saveBitmap(const char *name, const uint8_t *bitmap)
|
||||
{
|
||||
if (!validBitmapName(name))
|
||||
return "Invalid name: use 1-24 letters, digits, spaces, - or _";
|
||||
if (builtin(name))
|
||||
return "A built-in bitmap has this name";
|
||||
if (!mounted)
|
||||
return "No bitmap storage (install the new flash layout over USB, see README)";
|
||||
if (!LittleFS.exists(path(name)) && listBitmaps().size() >= (size_t)MAX_BITMAPS + (sizeof(BUILTIN) / sizeof(BUILTIN[0])))
|
||||
return "Too many bitmaps, delete some first";
|
||||
|
||||
File file = LittleFS.open(path(name), "w");
|
||||
if (!file)
|
||||
return "Could not write the file";
|
||||
bool ok = file.write(bitmap, BITMAP_BYTES) == (size_t)BITMAP_BYTES;
|
||||
file.close();
|
||||
return ok ? nullptr : "Storage full";
|
||||
}
|
||||
|
||||
const char *deleteBitmap(const char *name)
|
||||
{
|
||||
if (builtin(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";
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
#include "logic/bitmap_format.h"
|
||||
|
||||
// Full screen bitmaps (84 x 19, see logic/bitmap_format.h): built-in ones from
|
||||
// display/bitmaps.h and the ones uploaded in the web UI, stored on LittleFS
|
||||
// (partition "spiffs" in partitions.csv) as /bitmaps/<name>.bin.
|
||||
|
||||
struct BitmapInfo
|
||||
{
|
||||
String name;
|
||||
bool builtin;
|
||||
};
|
||||
|
||||
const int MAX_BITMAPS = 200;
|
||||
|
||||
// Mounts LittleFS (formats it the first time). False without the partition (old flash
|
||||
// layout): only the built-in bitmaps are available then.
|
||||
bool bitmapStoreBegin();
|
||||
bool bitmapStorageAvailable();
|
||||
void bitmapStorageUsage(size_t &used, size_t &total);
|
||||
|
||||
std::vector<BitmapInfo> listBitmaps();
|
||||
bool bitmapExists(const char *name);
|
||||
bool loadBitmap(const char *name, uint8_t *bitmap);
|
||||
|
||||
// Return nullptr on success, otherwise a message for the user
|
||||
const char *saveBitmap(const char *name, const uint8_t *bitmap);
|
||||
const char *deleteBitmap(const char *name);
|
||||
@@ -1,10 +1,11 @@
|
||||
#include "display/frames.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "display/bitmap_store.h"
|
||||
#include "display/bitmaps.h"
|
||||
#include "display/text.h"
|
||||
#include "logic/schedule.h"
|
||||
|
||||
// Horizontal offset that centers the 81 pixel wide clock on the 84 pixel display
|
||||
static const int CLOCK_X = 3;
|
||||
@@ -13,13 +14,13 @@ FlipdotFrames::FlipdotFrames(BROSE9323 &display) : _display(display)
|
||||
{
|
||||
}
|
||||
|
||||
void FlipdotFrames::clock(int hour, int minute)
|
||||
void FlipdotFrames::clock(int hour, int minute, bool evening)
|
||||
{
|
||||
char time[6];
|
||||
snprintf(time, sizeof(time), "%02d:%02d", hour, minute);
|
||||
|
||||
_display.fillScreen(0);
|
||||
if (isEvening(hour))
|
||||
if (evening)
|
||||
_display.drawBitmap(CLOCK_X, 0, yawning_pikachu_bitmap_yawning_pikachu_black_border, 35, 19, 1);
|
||||
else
|
||||
_display.drawBitmap(CLOCK_X, 0, pikachu_bitmap_pikachu_black_border, 35, 19, 1);
|
||||
@@ -40,11 +41,21 @@ void FlipdotFrames::noWifi(int hour, int minute)
|
||||
_display.display();
|
||||
}
|
||||
|
||||
void FlipdotFrames::page()
|
||||
{
|
||||
_drawFullScreen(_page);
|
||||
}
|
||||
|
||||
void FlipdotFrames::live()
|
||||
{
|
||||
_drawFullScreen(_live);
|
||||
}
|
||||
|
||||
void FlipdotFrames::logo()
|
||||
{
|
||||
_display.fillScreen(0);
|
||||
_display.drawBitmap(0, 0, nintendo_bitmap_Nintendo_new, 84, 19, 1);
|
||||
_display.display();
|
||||
uint8_t logo[BITMAP_BYTES];
|
||||
if (loadBitmap("Nintendo", logo))
|
||||
_drawFullScreen(logo);
|
||||
}
|
||||
|
||||
void FlipdotFrames::blank()
|
||||
@@ -52,3 +63,20 @@ void FlipdotFrames::blank()
|
||||
_display.fillScreen(0);
|
||||
_display.display();
|
||||
}
|
||||
|
||||
void FlipdotFrames::setPageBitmap(const uint8_t *bitmap)
|
||||
{
|
||||
memcpy(_page, bitmap, BITMAP_BYTES);
|
||||
}
|
||||
|
||||
void FlipdotFrames::setLiveBitmap(const uint8_t *bitmap)
|
||||
{
|
||||
memcpy(_live, bitmap, BITMAP_BYTES);
|
||||
}
|
||||
|
||||
void FlipdotFrames::_drawFullScreen(const uint8_t *bitmap)
|
||||
{
|
||||
_display.fillScreen(0);
|
||||
_display.drawBitmap(0, 0, bitmap, BITMAP_WIDTH, BITMAP_HEIGHT, 1);
|
||||
_display.display();
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "BROSE9323.h"
|
||||
#include "logic/bitmap_format.h"
|
||||
#include "logic/display_controller.h"
|
||||
|
||||
// Draws the screens on the flipdot display. Each method sends one complete frame.
|
||||
@@ -10,13 +11,24 @@ public:
|
||||
explicit FlipdotFrames(BROSE9323 &display);
|
||||
|
||||
// Pikachu with a thought bubble showing HH:MM (yawning in the evening)
|
||||
void clock(int hour, int minute) override;
|
||||
void clock(int hour, int minute, bool evening) override;
|
||||
// "No WiFi" with the time below it
|
||||
void noWifi(int hour, int minute) override;
|
||||
// The bitmap set with setPageBitmap()
|
||||
void page() override;
|
||||
// The image set with setLiveBitmap()
|
||||
void live() override;
|
||||
// Nintendo logo
|
||||
void logo() override;
|
||||
void blank() override;
|
||||
|
||||
void setPageBitmap(const uint8_t *bitmap);
|
||||
void setLiveBitmap(const uint8_t *bitmap);
|
||||
|
||||
private:
|
||||
BROSE9323 &_display;
|
||||
uint8_t _page[BITMAP_BYTES] = {};
|
||||
uint8_t _live[BITMAP_BYTES] = {};
|
||||
|
||||
void _drawFullScreen(const uint8_t *bitmap);
|
||||
};
|
||||
@@ -5,10 +5,15 @@
|
||||
#include "config.h"
|
||||
#include "display/text.h"
|
||||
|
||||
StatusScreen::StatusScreen(BROSE9323 &display, NanoLink &nano) : _display(display), _nano(nano)
|
||||
StatusScreen::StatusScreen(BROSE9323 &display, NanoLink &nano) : _display(display), _nano(nano), _flip_time(FLIP_TIME)
|
||||
{
|
||||
}
|
||||
|
||||
void StatusScreen::setFlipTime(uint16_t flip_time)
|
||||
{
|
||||
_flip_time = flip_time;
|
||||
}
|
||||
|
||||
void StatusScreen::show(const char *line1, const char *line2, bool keep_on)
|
||||
{
|
||||
Serial.printf("Display: %s / %s\n", line1, line2 ? line2 : "");
|
||||
@@ -16,7 +21,7 @@ void StatusScreen::show(const char *line1, const char *line2, bool keep_on)
|
||||
if (!_started)
|
||||
{
|
||||
_display.begin(&_nano.stream());
|
||||
_display.setTiming(FLIP_TIME);
|
||||
_display.setTiming(_flip_time);
|
||||
_started = true;
|
||||
}
|
||||
drawTextLines(_display, line1, line2);
|
||||
|
||||
@@ -14,8 +14,12 @@ 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);
|
||||
|
||||
private:
|
||||
BROSE9323 &_display;
|
||||
NanoLink &_nano;
|
||||
bool _started = false;
|
||||
uint16_t _flip_time;
|
||||
};
|
||||
Reference in new issue
Block a user