merged AppMatch.State.MINUTES/SECONDS to RUNNING + web-control for configMillis + re-implemented websocketEvent + configWrite delay

This commit is contained in:
Patrick Haßel 2025-01-10 14:51:33 +01:00
parent 66a8474fca
commit b7c2899f3f
5 changed files with 153 additions and 110 deletions

View File

@ -131,7 +131,7 @@
segment.setAttribute("y", y + "vw"); segment.setAttribute("y", y + "vw");
segment.setAttribute("width", S + "vw"); segment.setAttribute("width", S + "vw");
segment.setAttribute("height", S + "vw"); segment.setAttribute("height", S + "vw");
segment.setAttribute("stroke", "magenta"); segment.setAttribute("stroke", "white");
segment.setAttribute("fill", "none"); segment.setAttribute("fill", "none");
segment.setAttribute("id", "segment" + segments.length); segment.setAttribute("id", "segment" + segments.length);
display.appendChild(segment); display.appendChild(segment);
@ -155,9 +155,7 @@
function connect() { function connect() {
console.log("connecting websocket..."); console.log("connecting websocket...");
const url2 = url("ws", "/ws"); const socket = new WebSocket(url("ws", "/ws"));
console.log(url2);
const socket = new WebSocket(url2);
socket.timeout = 1; socket.timeout = 1;
socket.addEventListener('open', _ => { socket.addEventListener('open', _ => {
console.log('websocket connected'); console.log('websocket connected');
@ -183,14 +181,14 @@
console.log('websocket disconnected'); console.log('websocket disconnected');
segments.forEach(segment => { segments.forEach(segment => {
segment.setAttribute("fill", "none"); segment.setAttribute("fill", "none");
segment.setAttribute("stroke", "magenta"); segment.setAttribute("stroke", "white");
}); });
connect(); setTimeout(connect, 1000);
}); });
} }
drawDisplay(1, 1); drawDisplay(1, 1);
connect(); setTimeout(connect, 1000);
</script> </script>

View File

@ -6,6 +6,8 @@
#include <core/log.h> #include <core/log.h>
#include <display/Display.h> #include <display/Display.h>
#define CONFIG_WRITE_DELAY_MILLIS (30 * 1000)
class App { class App {
const char *name; const char *name;
@ -16,6 +18,8 @@ class App {
JsonObject config = configJsonDoc.to<JsonObject>(); JsonObject config = configJsonDoc.to<JsonObject>();
unsigned long configDirty = 0;
bool dirty = true; bool dirty = true;
bool doForceNextHexBuffer = true; bool doForceNextHexBuffer = true;
@ -36,7 +40,6 @@ public:
void start() { void start() {
configRead(); configRead();
_start(); _start();
markDirty();
} }
void loop() { void loop() {
@ -45,6 +48,10 @@ public:
const auto dtMillis = now - lastMillis; const auto dtMillis = now - lastMillis;
lastMillis = now; lastMillis = now;
_loop(dtMillis); _loop(dtMillis);
if (configDirty != 0 && millis() - configDirty > CONFIG_WRITE_DELAY_MILLIS) {
configDirty = 0;
configWrite();
}
if (dirty) { if (dirty) {
dirty = false; dirty = false;
draw(); draw();
@ -98,6 +105,7 @@ protected:
template<typename T> template<typename T>
void configSet(const char *key, T value) { void configSet(const char *key, T value) {
config[key] = value; config[key] = value;
configDirty = max(1UL, millis());
} }
virtual void _start() { virtual void _start() {
@ -116,7 +124,7 @@ protected:
// //
} }
void markDirty(const bool forceNextHexBuffer = false) { void markDirty(const bool forceNextHexBuffer) {
dirty = true; dirty = true;
doForceNextHexBuffer = forceNextHexBuffer; doForceNextHexBuffer = forceNextHexBuffer;
} }

View File

@ -5,13 +5,23 @@
#include "App.h" #include "App.h"
#define MS_PER_SEC (1000L)
#define APP_MATCH_NAME "match" #define APP_MATCH_NAME "match"
#define MILLIS_STEP_UP_DOWN (60 * MS_PER_SEC)
#define MILLIS_STEP_LEFT_RIGHT (MS_PER_SEC)
#define MILLIS_MIN (MS_PER_SEC)
#define MILLIS_MAX ((99 * 60 + 59) * MS_PER_SEC)
#define CONFIG_SECONDS_KEY "seconds" #define CONFIG_SECONDS_KEY "seconds"
#define CONFIG_SECONDS_DEFAULT (6 * 60) #define CONFIG_SECONDS_DEFAULT (6 * 60)
#define CONFIG_CENTIS_KEY "centis" #define CONFIG_CENTIS_KEY "centis"
#define CONFIG_CENTIS_DEFAULT (6 * 60) #define CONFIG_CENTIS_DEFAULT false
enum State {
INITIAL, RUNNING, PAUSE, END
};
class AppMatch final : public App { class AppMatch final : public App {
@ -39,11 +49,7 @@ class AppMatch final : public App {
unsigned long updateSeconds = totalSeconds; unsigned long updateSeconds = totalSeconds;
enum State { State state = INITIAL;
PAUSE, MINUTES, SECONDS, END
};
State state = PAUSE;
public: public:
@ -55,11 +61,10 @@ public:
bool setConfig(const String& key, const String& valueStr) override { bool setConfig(const String& key, const String& valueStr) override {
if (key.equals(CONFIG_SECONDS_KEY)) { if (key.equals(CONFIG_SECONDS_KEY)) {
const auto seconds = valueStr.toInt(); const auto seconds = valueStr.toInt();
const auto newMillis = seconds * 1000; const auto newMillis = seconds * MS_PER_SEC;
if (seconds > 0 && configMillis != newMillis) { if (seconds > 0 && configMillis != newMillis) {
configMillis = newMillis; configMillis = newMillis;
configSet(CONFIG_SECONDS_KEY, seconds); configSet(CONFIG_SECONDS_KEY, seconds);
configWrite();
return true; return true;
} }
} else if (key.equals(CONFIG_CENTIS_KEY)) { } else if (key.equals(CONFIG_CENTIS_KEY)) {
@ -67,50 +72,71 @@ public:
if (configCentis != newCentis) { if (configCentis != newCentis) {
configCentis = newCentis; configCentis = newCentis;
configSet(CONFIG_CENTIS_KEY, newCentis); configSet(CONFIG_CENTIS_KEY, newCentis);
configWrite();
return true; return true;
} }
} }
return false; return false;
} }
void up() override {
addConfigMillis(MILLIS_STEP_UP_DOWN);
}
void down() override {
addConfigMillis(-MILLIS_STEP_UP_DOWN);
}
void right() override {
addConfigMillis(MILLIS_STEP_LEFT_RIGHT);
}
void left() override {
addConfigMillis(-MILLIS_STEP_LEFT_RIGHT);
}
void confirm() override {
if (state == END) {
return;
}
switch (state) {
case INITIAL:
case PAUSE:
setState(RUNNING);
break;
case RUNNING:
setState(PAUSE);
break;
case END:
break;
}
}
void cancel() override {
if (state == PAUSE || state == END) {
_start();
}
}
protected: protected:
void _start() override { void _start() override {
configMillis = configGet<unsigned long>(CONFIG_SECONDS_KEY, CONFIG_SECONDS_DEFAULT) * 1000; configMillis = configGet<unsigned long>(CONFIG_SECONDS_KEY, CONFIG_SECONDS_DEFAULT) * MS_PER_SEC;
configCentis = configGet<bool>(CONFIG_CENTIS_KEY, CONFIG_CENTIS_DEFAULT); configCentis = configGet<bool>(CONFIG_CENTIS_KEY, CONFIG_CENTIS_DEFAULT);
info("config:"); info("config:");
info(" seconds = %ld", configMillis / 1000); info(" seconds = %ld", configMillis / MS_PER_SEC);
info(" centis = %s", configCentis ? "true" : "false"); info(" centis = %s", configCentis ? "true" : "false");
totalMillis = configMillis; setTotalMillis(configMillis);
setState(PAUSE, true); setState(INITIAL, true);
} }
void _loop(const unsigned long dtMillis) override { void _loop(const unsigned long dtMillis) override {
if (state != PAUSE) { if (state == RUNNING) {
if (totalMillis <= dtMillis) { if (totalMillis <= dtMillis) {
totalMillis = 0; setTotalMillis(0);
} else { } else {
totalMillis -= dtMillis; setTotalMillis(totalMillis - dtMillis);
}
}
totalCentis = totalMillis / 10;
totalSeconds = totalCentis / 100;
totalMinutes = totalSeconds / 60;
partCentis = totalCentis % 100;
partSeconds = totalSeconds % 60;
if (state != PAUSE) {
if (totalMinutes > 0) {
setState(MINUTES);
} else if (totalMillis > 0) {
setState(SECONDS);
} else {
setState(END);
} }
} }
@ -119,25 +145,31 @@ protected:
if (now - blinkMillis > blinkIntervalMillis) { if (now - blinkMillis > blinkIntervalMillis) {
blinkMillis = now; blinkMillis = now;
blinkState = !blinkState; blinkState = !blinkState;
markDirty(); markDirty(false);
} }
} }
if (state == RUNNING) {
if (totalMinutes > 0 || !configCentis) {
if (updateSeconds != totalSeconds) { if (updateSeconds != totalSeconds) {
updateSeconds = totalSeconds; updateSeconds = totalSeconds;
markDirty(); markDirty(false);
} else if (state == SECONDS && configCentis) { }
markDirty(); } else if (configCentis) {
markDirty(false);
}
} }
} }
void draw() override { void draw() override {
display.clear(); display.clear();
if (state == PAUSE) { if (state == INITIAL) {
display.setColor(WHITE);
} else if (state == PAUSE) {
display.setColor(BLUE); display.setColor(BLUE);
} else if (totalMillis * 2 >= configMillis) { } else if (totalMillis * 2 >= configMillis) {
display.setColor(GREEN); display.setColor(GREEN);
} else if (totalMillis >= 10 * 1000) { } else if (totalMillis > 0) {
display.setColor(YELLOW); display.setColor(YELLOW);
} else { } else {
display.setColor(RED); display.setColor(RED);
@ -145,7 +177,6 @@ protected:
if (blinkIntervalMillis == 0 || blinkState) { if (blinkIntervalMillis == 0 || blinkState) {
if (totalMinutes > 0) { if (totalMinutes > 0) {
display.printf("%2d:%02d", totalMinutes, partSeconds); display.printf("%2d:%02d", totalMinutes, partSeconds);
info("%2d:%02d", totalMinutes, partSeconds);
} else if (totalMillis > 0) { } else if (totalMillis > 0) {
if (configCentis) { if (configCentis) {
display.printf("%2d.%02d", partSeconds, partCentis); display.printf("%2d.%02d", partSeconds, partCentis);
@ -158,60 +189,32 @@ protected:
} }
} }
void confirm() override {
if (state == END) {
return;
}
if (state == PAUSE) {
setState(MINUTES);
} else {
setState(PAUSE);
}
}
void cancel() override {
_start();
}
private: private:
void blinkEnable(const unsigned long intervalMillis) {
blinkState = false;
blinkIntervalMillis = intervalMillis;
blinkMillis = millis();
}
void setState(const State newState, const bool force = false) { void setState(const State newState, const bool force = false) {
if (state == newState && !force) { if (state == newState && !force) {
return; return;
} }
state = newState; state = newState;
switch (state) {
case PAUSE:
blinkEnable(0);
break;
case MINUTES:
updateSeconds = totalSeconds; updateSeconds = totalSeconds;
blinkEnable(0); blinkEnable(0);
case SECONDS: if (state == END) {
blinkEnable(0);
break;
case END:
blinkEnable(500); blinkEnable(500);
break;
} }
info("state changed to %s", getStateName());
markDirty(); info("state = %s", getStateName());
markDirty(true);
} }
const char *getStateName() const { const char *getStateName() const {
switch (state) { switch (state) {
case INITIAL:
return "INITIAL";
case PAUSE: case PAUSE:
return "PAUSE"; return "PAUSE";
case MINUTES: case RUNNING:
return "MINUTES"; return "RUNNING";
case SECONDS:
return "SECONDS";
case END: case END:
return "END"; return "END";
default: default:
@ -219,6 +222,44 @@ private:
} }
} }
void blinkEnable(const unsigned long intervalMillis) {
blinkState = false;
blinkIntervalMillis = intervalMillis;
blinkMillis = millis();
}
void addConfigMillis(const long change) {
if (state != INITIAL) {
return;
}
const auto newConfigMillis = max(MILLIS_MIN, min(static_cast<long>(configMillis) + change, MILLIS_MAX));
if (configMillis != newConfigMillis) {
configMillis = newConfigMillis;
configSet(CONFIG_SECONDS_KEY, configMillis / 1000);
setTotalMillis(configMillis);
markDirty(true);
}
}
void setTotalMillis(const unsigned long newTotalMillis) {
if (totalMillis == newTotalMillis) {
return;
}
totalMillis = newTotalMillis;
if (totalMillis == 0) {
setState(END);
}
totalCentis = totalMillis / 10;
totalSeconds = totalCentis / 100;
totalMinutes = totalSeconds / 60;
partCentis = totalCentis % 100;
partSeconds = totalSeconds % 60;
}
}; };
#endif #endif

View File

@ -81,24 +81,16 @@ void httpNotFound(AsyncWebServerRequest *request) {
} }
} }
const char *getWebsocketTypeName(AwsEventType type) { void websocketEvent(AsyncWebSocket *socket, AsyncWebSocketClient *client, const AwsEventType type, void *arg, unsigned char *message, const unsigned length) {
switch (type) { if (type == WS_EVT_CONNECT) {
case WS_EVT_CONNECT: display.sendHexBuffer(client);
return "CONNECT";
case WS_EVT_DISCONNECT:
return "DISCONNECT";
case WS_EVT_PONG:
return "PONG";
case WS_EVT_ERROR:
return "ERROR";
case WS_EVT_DATA:
return "DATA";
default:
return "[???]";
} }
} }
void httpSetup() { void httpSetup() {
ws.onEvent(websocketEvent);
server.addHandler(&ws);
server.on("/action/left", HTTP_GET, httpActionLeft); server.on("/action/left", HTTP_GET, httpActionLeft);
server.on("/action/up", HTTP_GET, httpActionUp); server.on("/action/up", HTTP_GET, httpActionUp);
server.on("/action/down", HTTP_GET, httpActionDown); server.on("/action/down", HTTP_GET, httpActionDown);
@ -108,7 +100,6 @@ void httpSetup() {
server.on("/app/config", HTTP_GET, httpAppConfig); server.on("/app/config", HTTP_GET, httpAppConfig);
server.serveStatic("/", LittleFS, "/http/", "max-age=86400").setDefaultFile("index.html"); server.serveStatic("/", LittleFS, "/http/", "max-age=86400").setDefaultFile("index.html");
server.onNotFound(httpNotFound); server.onNotFound(httpNotFound);
server.addHandler(&ws);
server.begin(); server.begin();
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");

View File

@ -32,6 +32,8 @@ class Display {
Color color = WHITE; Color color = WHITE;
char hexBuffer[HEX_BUFFER_SIZE] = "";
public: public:
Display() /* : leds(PIXEL_COUNT, GPIO_NUM_13) */ { Display() /* : leds(PIXEL_COUNT, GPIO_NUM_13) */ {
@ -61,7 +63,8 @@ public:
static auto last = now; static auto last = now;
if (now - last >= HEX_BUFFER_MIN_WAIT_MS || forceNextHexBuffer) { if (now - last >= HEX_BUFFER_MIN_WAIT_MS || forceNextHexBuffer) {
last = now; last = now;
sendHexBuffer(); fillHexBuffer();
websocketSendAll(hexBuffer);
} }
} }
@ -143,15 +146,17 @@ public:
} }
} }
void sendHexBuffer(AsyncWebSocketClient *client) {
client->text(hexBuffer);
}
private: private:
void sendHexBuffer() { void fillHexBuffer() {
char hexBuffer[HEX_BUFFER_SIZE] = "";
auto b = hexBuffer; auto b = hexBuffer;
for (const auto& pixel: pixels) { for (const auto& pixel: pixels) {
b += snprintf(b, sizeof hexBuffer - (b - hexBuffer), "%X%X%X", pixel.r / 16, pixel.g / 16, pixel.b / 16); b += snprintf(b, sizeof hexBuffer - (b - hexBuffer), "%X%X%X", pixel.r / 16, pixel.g / 16, pixel.b / 16);
} }
websocketSendAll(hexBuffer);
} }
}; };