From 542c90dc923ce68731d34b1892ec66ef0d87ee33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Thu, 9 Jan 2025 16:39:07 +0100 Subject: [PATCH] Display digits/dots fix + config setting via http [PERSISTENCE NOT WORKING] --- data/index.html | 18 ++++++-- src/app/App.h | 63 +++++++++++++++++++++++--- src/app/AppMatch.h | 53 +++++++++++++++++----- src/core/filesystem.cpp | 5 ++- src/core/http.cpp | 33 +++++++++++--- src/core/log.cpp | 20 ++++++--- src/display/Display.h | 98 ++++++++++++++++++++--------------------- 7 files changed, 204 insertions(+), 86 deletions(-) diff --git a/data/index.html b/data/index.html index bc2e1ec..18b42d4 100644 --- a/data/index.html +++ b/data/index.html @@ -37,7 +37,7 @@ - + @@ -121,7 +121,8 @@ segment.setAttribute("y", y + "vw"); segment.setAttribute("width", S + "vw"); segment.setAttribute("height", S + "vw"); - segment.setAttribute("fill", "#555"); + segment.setAttribute("stroke", "magenta"); + segment.setAttribute("fill", "none"); segment.setAttribute("id", "segment" + segments.length); display.appendChild(segment); segments.push(segment); @@ -152,7 +153,18 @@ socket.timeout = 1; socket.addEventListener('open', _ => console.log('websocket connected')); socket.addEventListener('message', event => { - event.data.split(',').filter((color, index) => color !== '').forEach((color, index) => segments[index].setAttribute("fill", "#" + color)); + const len = event.data.length; + let index = 0; + let start = 0; + while (start < len) { + const end = start + 3; + const color = event.data.substring(start, end); + const segment = segments[index]; + segment.setAttribute("fill", "#" + color) + segment.setAttribute("stroke", "none"); + index++; + start = end; + } }); socket.addEventListener('close', _ => { console.log('websocket disconnected'); diff --git a/src/app/App.h b/src/app/App.h index c38594b..d8323a5 100644 --- a/src/app/App.h +++ b/src/app/App.h @@ -9,6 +9,8 @@ class App { const char *name; + const String path; + JsonDocument configJson; JsonObject config = configJson.to(); @@ -17,8 +19,9 @@ class App { public: - explicit App(const char *name) - : name(name) { + explicit App(const char *name) : + name(name), + path(String("/") + name + ".json") { // } @@ -74,16 +77,25 @@ public: // } + virtual bool setConfig(const String& key, const String& valueStr) { + return false; + } + protected: template - T configRead(const char *key, T fallback) { + T configGet(const char *key, T fallback) { if (config[key].is()) { return config[key].as(); } return fallback; } + template + void configSet(const char *key, T value) { + config[key] = value; + } + virtual void _start() { // } @@ -104,14 +116,51 @@ protected: dirty = true; } -private: - void configLoad() { - const auto path = String("/apps/") + name + ".json"; configJson.clear(); auto file = LittleFS.open(path, "r"); - deserializeJson(configJson, file); + if (!file) { + error("failed to open file for config read: %s", path.c_str()); + return; + } + + if (deserializeJson(configJson, file)) { + info("config loaded: %s", path.c_str()); + } else { + error("failed to load config: %s", path.c_str()); + } + file.close(); + config = configJson.to(); + + char buffer[256]; + serializeJsonPretty(configJson, buffer, sizeof buffer); + Serial.println(buffer); + } + + void configSave() { + auto file = LittleFS.open(path, "w"); + if (!file) { + error("failed to open file for config write: %s", path.c_str()); + return; + } + + if (serializeJson(configJson, file)) { + info("config written: %s", path.c_str()); + } else { + error("failed to write config: %s", path.c_str()); + } + file.close(); + + auto file2 = LittleFS.open(path, "r"); + if (file2) { + while (file2.available()) { + Serial.write(file2.read()); + } + file2.close(); + } else { + Serial.println("Failed to open file2 for reading"); + } } }; diff --git a/src/app/AppMatch.h b/src/app/AppMatch.h index b3563cc..fc83c77 100644 --- a/src/app/AppMatch.h +++ b/src/app/AppMatch.h @@ -43,14 +43,28 @@ class AppMatch final : public App { public: explicit AppMatch() - : App(APP_MATCH_NAME) { + : App(APP_MATCH_NAME) { // } + bool setConfig(const String& key, const String& valueStr) override { + if (key.equals(CONFIG_SECONDS_KEY)) { + const auto seconds = valueStr.toInt(); + const auto newMillis = seconds * 1000; + if (seconds > 0 && configMillis != newMillis) { + configMillis = newMillis; + configSet(CONFIG_SECONDS_KEY, seconds); + configSave(); + return true; + } + } + return false; + } + protected: void _start() override { - configMillis = configRead(CONFIG_SECONDS_KEY,CONFIG_SECONDS_DEFAULT) * 1000; + configMillis = configGet(CONFIG_SECONDS_KEY, CONFIG_SECONDS_DEFAULT) * 1000; totalMillis = configMillis; setState(PAUSE); } @@ -83,7 +97,7 @@ protected: if (blinkIntervalMillis > 0) { const auto now = millis(); - if (now - blinkMillis > 500) { + if (now - blinkMillis > blinkIntervalMillis) { blinkMillis = now; blinkState = !blinkState; markDirty(); @@ -104,24 +118,34 @@ protected: void draw() override { display.clear(); + if (state == PAUSE) { + display.setColor(BLUE); + } else if (totalMillis * 2 >= configMillis) { + display.setColor(GREEN); + } else if (totalMillis >= 10 * 1000) { + display.setColor(YELLOW); + } else { + display.setColor(RED); + } if (blinkIntervalMillis == 0 || blinkState) { if (totalMinutes > 0) { - display.setColor(totalMillis < configMillis / 2 ? YELLOW : GREEN); display.printf("%2d:%02d", totalMinutes, partSeconds); info("%2d:%02d", totalMinutes, partSeconds); } else if (totalMillis > 0) { - display.setColor(RED); display.printf("%2d.%02d", partSeconds, partCentis); } else { display.printf("00:00"); } } else if (state == PAUSE) { - info("PAUS", totalMinutes, partSeconds); + display.printf("PAUS"); } display.flush(); } void confirm() override { + if (state == END) { + return; + } if (state == PAUSE) { setState(MINUTES); } else { @@ -157,7 +181,7 @@ private: blinkEnable(0); break; case END: - blinkEnable(100); + blinkEnable(500); break; } info("state changed to %s", getStateName()); @@ -166,11 +190,16 @@ private: const char *getStateName() const { switch (state) { - case PAUSE: return "PAUSE"; - case MINUTES: return "MINUTES"; - case SECONDS: return "SECONDS"; - case END: return "END"; - default: return "[???]"; + case PAUSE: + return "PAUSE"; + case MINUTES: + return "MINUTES"; + case SECONDS: + return "SECONDS"; + case END: + return "END"; + default: + return "[???]"; } } diff --git a/src/core/filesystem.cpp b/src/core/filesystem.cpp index 24c9afe..611520c 100644 --- a/src/core/filesystem.cpp +++ b/src/core/filesystem.cpp @@ -1,11 +1,12 @@ #include "filesystem.h" +#include #include #include "log.h" void filesystemMount() { - if (LittleFS.begin(true)) { - info("filesystem mounted"); + if (LittleFS.begin()) { + info("filesystem mounted: %3d%% used", (int) round(100.0 * (LittleFS.usedBytes()) / LittleFS.totalBytes())); } else { error("failed to mount filesystem"); } diff --git a/src/core/http.cpp b/src/core/http.cpp index 9c9983b..dec7cb6 100644 --- a/src/core/http.cpp +++ b/src/core/http.cpp @@ -16,45 +16,65 @@ void httpIndex(AsyncWebServerRequest *request) { } void httpActionLeft(AsyncWebServerRequest *request) { - request->send(200); if (app != nullptr) { app->left(); } + request->send(200); } void httpActionUp(AsyncWebServerRequest *request) { - request->send(200); if (app != nullptr) { app->up(); } + request->send(200); } void httpActionDown(AsyncWebServerRequest *request) { - request->send(200); if (app != nullptr) { app->down(); } + request->send(200); } void httpActionRight(AsyncWebServerRequest *request) { - request->send(200); if (app != nullptr) { app->right(); } + request->send(200); } void httpActionCancel(AsyncWebServerRequest *request) { - request->send(200); if (app != nullptr) { app->cancel(); } + request->send(200); } void httpActionConfirm(AsyncWebServerRequest *request) { - request->send(200); if (app != nullptr) { app->confirm(); } + request->send(200); +} + +void httpAppConfig(AsyncWebServerRequest *request) { + if (!request->hasArg("key")) { + error("missing parameter 'key'"); + request->send(400); + return; + } + if (!request->hasArg("value")) { + error("missing parameter 'value'"); + request->send(400); + return; + } + if (app != nullptr) { + if (app->setConfig(request->arg("key"), request->arg("value"))) { + request->send(200); + return; + } + } + request->send(400); } void httpSetup() { @@ -91,6 +111,7 @@ void httpSetup() { server.on("/action/right", HTTP_GET, httpActionRight); server.on("/action/cancel", HTTP_GET, httpActionCancel); server.on("/action/confirm", HTTP_GET, httpActionConfirm); + server.on("/app/config", HTTP_GET, httpAppConfig); server.begin(); websocketStarted = true; } diff --git a/src/core/log.cpp b/src/core/log.cpp index d77ba06..edd04e0 100644 --- a/src/core/log.cpp +++ b/src/core/log.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "clock.h" @@ -23,6 +24,7 @@ void execute(const char *cmd) { info(" reboot"); info(" reconnect"); info(" time"); + info(" ls"); } else if (strcmp(cmd, "network") == 0) { info("NETWORK"); info(" %-10s %s", "mac:", WiFi.macAddress().c_str()); @@ -47,27 +49,30 @@ void execute(const char *cmd) { info(" %-10s %s", "real:", getClockStr()); info(" %-10s %s", "startup:", getStartupStr()); info(" %-10s %s", "uptime:", getUptimeStr()); + } else if (strcmp(cmd, "ls") == 0) { + info("TODO"); } else { info("UNKNOWN COMMAND: %s", cmd); } } void logLoop() { - static char cmd[64]; - static auto write = cmd; + static char buffer[64]; + static auto write = buffer; if (Serial.available() > 0) { const auto c = static_cast(Serial.read()); switch (c) { + case 10: + break; case 13: Serial.print(c); - execute(cmd); - write = cmd; - *write = 0; + execute(buffer); + write = buffer; break; case 8: - if (write > cmd) { + if (write > buffer) { Serial.print(c); - *write-- = 0; + write--; } break; default: @@ -76,6 +81,7 @@ void logLoop() { write++; break; } + *write = 0; } } diff --git a/src/display/Display.h b/src/display/Display.h index 3806b08..b8ac173 100644 --- a/src/display/Display.h +++ b/src/display/Display.h @@ -12,20 +12,19 @@ #define PIXELS_PER_SEGMENT 3 #define SEGMENTS_PER_DIGIT 7 #define PIXELS_PER_DOT 1 -#define DOTS_PER_DIGIT 4 +#define DOT_COUNT 4 #define DIGITS 4 -#define MAX_STR_LEN (2 * DIGITS - 1) -#define PIXELS_PER_DIGIT_SEGMENTS (PIXELS_PER_SEGMENT * SEGMENTS_PER_DIGIT) -#define PIXELS_PER_DIGIT_DOTS (PIXELS_PER_DOT * DOTS_PER_DIGIT) -#define PIXELS_PER_DIGIT_AND_DOTS (PIXELS_PER_DIGIT_SEGMENTS + PIXELS_PER_DIGIT_DOTS) -#define PIXEL_COUNT (DIGITS * PIXELS_PER_DIGIT_AND_DOTS - PIXELS_PER_DIGIT_DOTS) -#define PIXEL_BYTE_COUNT (PIXEL_COUNT * sizeof(Color)) + +#define PIXELS_PER_DIGIT (PIXELS_PER_SEGMENT * SEGMENTS_PER_DIGIT) +#define TOTAL_DOT_PIXEL_COUNT (PIXELS_PER_DOT * DOT_COUNT) +#define TOTAL_PIXEL_COUNT (DIGITS * PIXELS_PER_DIGIT + TOTAL_DOT_PIXEL_COUNT) +#define TOTAL_PIXEL_BYTE_COUNT (TOTAL_PIXEL_COUNT * sizeof(Color)) class Display { // Adafruit_NeoPixel leds; - Color pixels[PIXEL_COUNT] = {}; + Color pixels[TOTAL_PIXEL_COUNT] = {}; Color color = WHITE; @@ -38,8 +37,8 @@ public: flush(); } - void setColor(const Color color) { - this->color = color; + void setColor(const Color newColor) { + this->color = newColor; } void setBrightness(const int brightness) { @@ -47,7 +46,7 @@ public: } void clear() { - memset(pixels, 0, PIXEL_BYTE_COUNT); + memset(pixels, 0, TOTAL_PIXEL_BYTE_COUNT); } void flush() { @@ -66,55 +65,58 @@ public: char buffer[512]; char *b = buffer; for (const auto& pixel: pixels) { - b += snprintf(b, sizeof buffer - (b - buffer), "%02X%02X%02X,", pixel.r, pixel.g, pixel.b); + b += snprintf(b, sizeof buffer - (b - buffer), "%X%X%X", pixel.r / 16, pixel.g / 16, pixel.b / 16); } websocketSend(buffer); } - int printf(const char *format, ...) { - char buffer[MAX_STR_LEN + 1]; + void printf(const char *format, ...) { + char buffer[64]; va_list args; va_start(args, format); vsnprintf(buffer, sizeof buffer, format, args); va_end(args); - return print(buffer); + print(buffer); } - int print(const char *str) { - auto pixel = 0; - for (auto character = str; *character != 0 && character - str < MAX_STR_LEN; character++) { - pixel = print(pixel, *character); - } - return pixel; - } - - int print(const int pixel, const char character) { - switch (character) { - case '\'': - case '"': - case '`': - case '.': - return printDots(pixel, false, false, false, true); - case ',': - return printDots(pixel, false, false, true, true); - case ':': - return printDots(pixel, false, true, true, false); - case ';': - return printDots(pixel, false, true, true, true); - case '|': - return printDots(pixel, true, true, true, true); - default: - return printCharacter(pixel, character); + void print(const char *str) { + auto digit = 0; + for (auto c = str; *c != 0 && digit < 4; c++) { + switch (*c) { + case '\'': + case '"': + case '`': + case '.': + printDots(false, false, false, true); + digit = 2; + break; + case ',': + printDots(false, false, true, true); + digit = 2; + break; + case ':': + printDots(false, true, true, false); + digit = 2; + break; + case ';': + printDots(false, true, true, true); + digit = 2; + break; + case '|': + printDots(true, true, true, true); + digit = 2; + break; + default: + printCharacter(digit++, *c); + break; + } } } - int printDots(int pixel, const bool dot0, const bool dot1, const bool dot2, const bool dot3) { - pixel = pixel / PIXELS_PER_DIGIT_AND_DOTS - PIXELS_PER_DIGIT_DOTS; - if (pixel < 0) { - return 0; - } + void printDots(const bool dot0, const bool dot1, const bool dot2, const bool dot3) { + auto pixel = PIXELS_PER_DIGIT * 2; if (dot0) { pixels[pixel] = color; } @@ -130,11 +132,10 @@ public: if (dot3) { pixels[pixel] = color; } - return pixel; } - int printCharacter(int pixel, const char character) { - pixel = pixel / PIXELS_PER_DIGIT_AND_DOTS; + void printCharacter(int digit, const char character) { + auto pixel = digit * PIXELS_PER_DIGIT + (digit >= 2 ? TOTAL_DOT_PIXEL_COUNT : 0); const auto symbol = getSymbol(character); for (auto s = *symbol; s < *symbol + SYMBOL_SIZE; s++) { if (*s) { @@ -145,7 +146,6 @@ public: pixel += 3; } } - return pixel; } };