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,81 @@
|
||||
#include "logic/bitmap_format.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
|
||||
bool bitmapPixel(const uint8_t *bitmap, int x, int y)
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= BITMAP_WIDTH || y >= BITMAP_HEIGHT)
|
||||
return false;
|
||||
return bitmap[y * BITMAP_ROW_BYTES + x / 8] & (0x80 >> (x & 7));
|
||||
}
|
||||
|
||||
void setBitmapPixel(uint8_t *bitmap, int x, int y, bool on)
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= BITMAP_WIDTH || y >= BITMAP_HEIGHT)
|
||||
return;
|
||||
uint8_t &byte = bitmap[y * BITMAP_ROW_BYTES + x / 8];
|
||||
if (on)
|
||||
byte |= 0x80 >> (x & 7);
|
||||
else
|
||||
byte &= ~(0x80 >> (x & 7));
|
||||
}
|
||||
|
||||
static int hexValue(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool bitmapFromHex(const char *hex, uint8_t *bitmap)
|
||||
{
|
||||
if (hex == nullptr || strlen(hex) != (size_t)BITMAP_HEX_LENGTH)
|
||||
return false;
|
||||
for (int i = 0; i < BITMAP_BYTES; i++)
|
||||
{
|
||||
int high = hexValue(hex[2 * i]);
|
||||
int low = hexValue(hex[2 * i + 1]);
|
||||
if (high < 0 || low < 0)
|
||||
return false;
|
||||
bitmap[i] = (high << 4) | low;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void bitmapToHex(const uint8_t *bitmap, char *out)
|
||||
{
|
||||
static const char digits[] = "0123456789abcdef";
|
||||
for (int i = 0; i < BITMAP_BYTES; i++)
|
||||
{
|
||||
out[2 * i] = digits[bitmap[i] >> 4];
|
||||
out[2 * i + 1] = digits[bitmap[i] & 0x0F];
|
||||
}
|
||||
out[BITMAP_HEX_LENGTH] = '\0';
|
||||
}
|
||||
|
||||
bool validBitmapName(const char *name)
|
||||
{
|
||||
if (name == nullptr)
|
||||
return false;
|
||||
size_t len = strlen(name);
|
||||
if (len == 0 || len > (size_t)BITMAP_NAME_MAX)
|
||||
return false;
|
||||
if (name[0] == ' ' || name[len - 1] == ' ')
|
||||
return false;
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
char c = name[i];
|
||||
if (!isalnum((unsigned char)c) && c != ' ' && c != '-' && c != '_')
|
||||
return false;
|
||||
}
|
||||
// "clock" is the clock page
|
||||
char lower[BITMAP_NAME_MAX + 1];
|
||||
for (size_t i = 0; i <= len; i++)
|
||||
lower[i] = tolower((unsigned char)name[i]);
|
||||
return strcmp(lower, "clock") != 0;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Full screen bitmaps (84 x 19), stored and sent to the web UI in the format of
|
||||
// Adafruit_GFX::drawBitmap: rows from top to bottom, 11 bytes per row, the most
|
||||
// significant bit is the leftmost dot. Pure logic, tested in test/test_esp32.
|
||||
|
||||
const int BITMAP_WIDTH = 84;
|
||||
const int BITMAP_HEIGHT = 19;
|
||||
const int BITMAP_ROW_BYTES = (BITMAP_WIDTH + 7) / 8;
|
||||
const int BITMAP_BYTES = BITMAP_ROW_BYTES * BITMAP_HEIGHT; // 209
|
||||
const int BITMAP_HEX_LENGTH = BITMAP_BYTES * 2; // 418
|
||||
|
||||
bool bitmapPixel(const uint8_t *bitmap, int x, int y);
|
||||
void setBitmapPixel(uint8_t *bitmap, int x, int y, bool on);
|
||||
|
||||
// Exactly BITMAP_HEX_LENGTH hex digits. Returns false on anything else.
|
||||
bool bitmapFromHex(const char *hex, uint8_t *bitmap);
|
||||
// out must hold BITMAP_HEX_LENGTH + 1 characters
|
||||
void bitmapToHex(const uint8_t *bitmap, char *out);
|
||||
|
||||
// Bitmap names: 1-24 characters, letters, digits, space, '-' and '_', no space at the
|
||||
// start or end, not "clock" (that is the clock page)
|
||||
const int BITMAP_NAME_MAX = 24;
|
||||
bool validBitmapName(const char *name);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#include "logic/power_timer.h"
|
||||
#include "logic/settings.h"
|
||||
|
||||
// Power for the panels and the Nano (implemented by NanoLink)
|
||||
class DisplayPower
|
||||
@@ -18,22 +19,44 @@ public:
|
||||
class Frames
|
||||
{
|
||||
public:
|
||||
virtual void clock(int hour, int minute) = 0;
|
||||
virtual void clock(int hour, int minute, bool evening) = 0;
|
||||
// Shown instead of the clock while WiFi is down (the time keeps running without it)
|
||||
virtual void noWifi(int hour, int minute) = 0;
|
||||
// The bitmap selected as page
|
||||
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;
|
||||
};
|
||||
|
||||
// Decides what the display shows and when it is powered, based on the time and the two
|
||||
// Home Assistant switches. A frame is only sent when it changes. Pure logic, tested in
|
||||
// test/test_esp32.
|
||||
enum class Page : uint8_t
|
||||
{
|
||||
Clock, // Pikachu with the time
|
||||
Bitmap, // a bitmap (FlipdotFrames holds which one)
|
||||
};
|
||||
|
||||
// Decides what the display shows and when it is powered, based on the time, the settings,
|
||||
// the Home Assistant switches and the web UI. A frame is only sent when it changes. Pure
|
||||
// logic, tested in test/test_esp32.
|
||||
class DisplayController
|
||||
{
|
||||
public:
|
||||
DisplayController(DisplayPower &power, Frames &frames);
|
||||
|
||||
// Home Assistant "FlipDot Display Clock" switch: off shows the logo instead of the clock
|
||||
void setSettings(const ScheduleSettings &schedule, const ClockSettings &clock);
|
||||
|
||||
// What is shown during the day. Call pageChanged() when the bitmap itself changed.
|
||||
void setPage(Page page);
|
||||
void pageChanged();
|
||||
|
||||
// Live mode: show Frames::live() now, whatever the time, until stopLive() or
|
||||
// LIVE_TIMEOUT after the last showLive()
|
||||
void showLive(uint32_t now);
|
||||
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);
|
||||
@@ -51,6 +74,8 @@ private:
|
||||
Unknown, // nothing sent since the last power on
|
||||
Clock,
|
||||
NoWifi,
|
||||
Page,
|
||||
Live,
|
||||
Logo,
|
||||
Blank,
|
||||
};
|
||||
@@ -59,17 +84,27 @@ private:
|
||||
Frames &_frames;
|
||||
PowerTimer _off_timer;
|
||||
|
||||
ScheduleSettings _schedule;
|
||||
ClockSettings _clock;
|
||||
Page _page = Page::Clock;
|
||||
|
||||
bool _clock_enabled = true;
|
||||
bool _power_enabled = true;
|
||||
|
||||
Screen _screen = Screen::Unknown;
|
||||
int _shown_hour = -1;
|
||||
int _shown_minute = -1;
|
||||
bool _shown_evening = false;
|
||||
uint32_t _content_revision = 0; // changed page, bitmap or live image
|
||||
uint32_t _shown_revision = 0;
|
||||
|
||||
bool _live = false;
|
||||
uint32_t _live_since = 0;
|
||||
|
||||
bool _wifi_lost = false;
|
||||
uint32_t _wifi_lost_since = 0;
|
||||
|
||||
void _ensureOn();
|
||||
void _showClock(int hour, int minute, bool no_wifi);
|
||||
void _showLogo();
|
||||
void _showBlank();
|
||||
void _showOnce(Screen screen);
|
||||
};
|
||||
@@ -1,21 +1,19 @@
|
||||
#include "logic/schedule.h"
|
||||
|
||||
static const int DAY_START_HOUR = 8;
|
||||
static const int LATE_START_HOUR = 22;
|
||||
static const int NIGHT_START_HOUR = 23;
|
||||
static const int NIGHT_START_MINUTE = 31;
|
||||
static const int EVENING_START_HOUR = 17;
|
||||
|
||||
ScheduleMode scheduleFor(int hour, int minute)
|
||||
ScheduleMode scheduleFor(int hour, int minute, const ScheduleSettings &s)
|
||||
{
|
||||
if (hour < DAY_START_HOUR || (hour >= NIGHT_START_HOUR && minute >= NIGHT_START_MINUTE))
|
||||
if (!s.enabled)
|
||||
return ScheduleMode::Clock;
|
||||
|
||||
int now = hour * 60 + minute;
|
||||
if (now < s.day_start || now >= s.night_start)
|
||||
return ScheduleMode::Night;
|
||||
if (hour < LATE_START_HOUR || minute % 15 == 0)
|
||||
if (now < s.late_start || !s.quarter_hours || minute % 15 == 0)
|
||||
return ScheduleMode::Clock;
|
||||
return ScheduleMode::Standby;
|
||||
}
|
||||
|
||||
bool isEvening(int hour)
|
||||
bool isEvening(int hour, int minute, const ClockSettings &s)
|
||||
{
|
||||
return hour >= EVENING_START_HOUR;
|
||||
return s.evening_enabled && hour * 60 + minute >= s.evening_start;
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
// When the clock is shown. Pure logic, tested in test/test_esp32.
|
||||
#include "logic/settings.h"
|
||||
|
||||
// When the display is on. Pure logic, tested in test/test_esp32. With the default
|
||||
// settings:
|
||||
//
|
||||
// 00:00 - 07:59 Night screen cleared, power off
|
||||
// 08:00 - 21:59 Clock clock updated every minute
|
||||
// 08:00 - 21:59 Clock page shown (the clock 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 time stays on the dots
|
||||
// Standby in between: power off, the last image stays on the dots
|
||||
// 23:31 - 23:59 Night
|
||||
|
||||
enum class ScheduleMode
|
||||
@@ -15,7 +18,7 @@ enum class ScheduleMode
|
||||
Standby,
|
||||
};
|
||||
|
||||
ScheduleMode scheduleFor(int hour, int minute);
|
||||
ScheduleMode scheduleFor(int hour, int minute, const ScheduleSettings &settings);
|
||||
|
||||
// Evening: the yawning Pikachu is shown
|
||||
bool isEvening(int hour);
|
||||
bool isEvening(int hour, int minute, const ClockSettings &settings);
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "logic/settings.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "logic/bitmap_format.h"
|
||||
|
||||
static bool terminated(const char *text, int size)
|
||||
{
|
||||
return memchr(text, '\0', size) != nullptr;
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
const ScheduleSettings &t = s.schedule;
|
||||
if (t.day_start >= MINUTES_PER_DAY || t.late_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 (s.clock.evening_start >= MINUTES_PER_DAY)
|
||||
return "The evening must start before 24:00";
|
||||
|
||||
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 HomeAssistantSettings &ha = s.home_assistant;
|
||||
if (!terminated(ha.host, sizeof(ha.host)) || !terminated(ha.user, sizeof(ha.user)) ||
|
||||
!terminated(ha.password, sizeof(ha.password)))
|
||||
return "Home Assistant text too long";
|
||||
if (ha.port == 0)
|
||||
return "Invalid MQTT port";
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool parseTime(const char *text, uint16_t &minutes)
|
||||
{
|
||||
if (text == nullptr)
|
||||
return false;
|
||||
int hour = 0, minute = 0;
|
||||
char extra;
|
||||
if (sscanf(text, "%d:%d%c", &hour, &minute, &extra) != 2)
|
||||
return false;
|
||||
if (hour < 0 || minute < 0 || minute > 59 || hour * 60 + minute > MINUTES_PER_DAY)
|
||||
return false;
|
||||
minutes = hour * 60 + minute;
|
||||
return true;
|
||||
}
|
||||
|
||||
void formatTime(uint16_t minutes, char *out)
|
||||
{
|
||||
snprintf(out, 6, "%02u:%02u", (unsigned)(minutes / 60) % 100, (unsigned)(minutes % 60));
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Everything that can be changed in the web UI. Stored in NVS (system/settings_store),
|
||||
// the defaults are the original behaviour. Pure logic, tested in test/test_esp32.
|
||||
//
|
||||
// Times are minutes since midnight (8:00 = 480).
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
struct ClockSettings
|
||||
{
|
||||
bool evening_enabled = true; // yawning Pikachu in the evening
|
||||
uint16_t evening_start = 17 * 60;
|
||||
bool no_wifi_screen = true; // "No WiFi" instead of the clock when WiFi is down
|
||||
};
|
||||
|
||||
struct HomeAssistantSettings
|
||||
{
|
||||
bool enabled = true;
|
||||
char host[64] = ""; // empty: BROKER_ADDR from secrets.h
|
||||
uint16_t port = 1883;
|
||||
char user[32] = "";
|
||||
char password[64] = "";
|
||||
};
|
||||
|
||||
// What is shown during the day: "clock" or the name of a bitmap
|
||||
const int PAGE_NAME_SIZE = 25;
|
||||
#define PAGE_CLOCK "clock"
|
||||
|
||||
struct Settings
|
||||
{
|
||||
char page[PAGE_NAME_SIZE] = PAGE_CLOCK;
|
||||
ScheduleSettings schedule;
|
||||
ClockSettings clock;
|
||||
HomeAssistantSettings home_assistant;
|
||||
uint16_t flip_time = 225; // flip pulse length in us, sent to the Nano
|
||||
bool status_led = true;
|
||||
};
|
||||
|
||||
const uint16_t MINUTES_PER_DAY = 24 * 60;
|
||||
const uint16_t SETTINGS_FLIP_TIME_MIN = 50;
|
||||
const uint16_t SETTINGS_FLIP_TIME_MAX = 2000;
|
||||
|
||||
// Checks the values. Returns nullptr when they are fine, otherwise a message for the user.
|
||||
const char *validateSettings(const Settings &settings);
|
||||
|
||||
// "HH:MM" (00:00 - 24:00) <-> minutes since midnight
|
||||
bool parseTime(const char *text, uint16_t &minutes);
|
||||
// out must hold 6 characters
|
||||
void formatTime(uint16_t minutes, char *out);
|
||||
Reference in new issue
Block a user