From 65a7f170f9e44df0b6dfe0bd9d6369310cdbf589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Wed, 8 Jan 2025 15:48:29 +0100 Subject: [PATCH] log, boot, wifi, clock, http, filesystem, Display, App, AppMatch [INACTIVE Adafruit_NeoPixel] --- .gitignore | 2 + platformio.ini | 18 +++++ src/INDEX_HTML.cpp | 13 ++++ src/INDEX_HTML.h | 6 ++ src/app/App.cpp | 49 ++++++++++++ src/app/App.h | 98 ++++++++++++++++++++++++ src/app/AppMatch.h | 165 ++++++++++++++++++++++++++++++++++++++++ src/boot.cpp | 29 +++++++ src/boot.h | 6 ++ src/clock.cpp | 68 +++++++++++++++++ src/clock.h | 18 +++++ src/display/Color.cpp | 34 +++++++++ src/display/Color.h | 40 ++++++++++ src/display/Display.cpp | 3 + src/display/Display.h | 140 ++++++++++++++++++++++++++++++++++ src/display/font.cpp | 68 +++++++++++++++++ src/display/font.h | 17 +++++ src/filesystem.cpp | 11 +++ src/filesystem.h | 6 ++ src/http.cpp | 53 +++++++++++++ src/http.h | 10 +++ src/log.cpp | 71 +++++++++++++++++ src/log.h | 21 +++++ src/main.cpp | 23 ++++++ src/wifi.cpp | 92 ++++++++++++++++++++++ src/wifi.h | 10 +++ 26 files changed, 1071 insertions(+) create mode 100644 .gitignore create mode 100644 platformio.ini create mode 100644 src/INDEX_HTML.cpp create mode 100644 src/INDEX_HTML.h create mode 100644 src/app/App.cpp create mode 100644 src/app/App.h create mode 100644 src/app/AppMatch.h create mode 100644 src/boot.cpp create mode 100644 src/boot.h create mode 100644 src/clock.cpp create mode 100644 src/clock.h create mode 100644 src/display/Color.cpp create mode 100644 src/display/Color.h create mode 100644 src/display/Display.cpp create mode 100644 src/display/Display.h create mode 100644 src/display/font.cpp create mode 100644 src/display/font.h create mode 100644 src/filesystem.cpp create mode 100644 src/filesystem.h create mode 100644 src/http.cpp create mode 100644 src/http.h create mode 100644 src/log.cpp create mode 100644 src/log.h create mode 100644 src/main.cpp create mode 100644 src/wifi.cpp create mode 100644 src/wifi.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..938b6ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.pio/ +/.idea/ diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..7a49482 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,18 @@ +[env:Sporttafel] +platform = espressif32 +board = esp32dev +framework = arduino +board_build.filesystem = littlefs +lib_deps = bblanchon/ArduinoJson +; https://github.com/adafruit/Adafruit_NeoPixel + https://github.com/me-no-dev/ESPAsyncWebServer.git +build_flags = -DWIFI_SSID=\"HappyNet\" + -DWIFI_PKEY=\"1Grausame!Sackratte7\" + -DWIFI_HOST=\"Sporttafel\" +;upload_port = 10.0.0.119 +;upload_protocol = espota +upload_port = /dev/ttyUSB0 +upload_speed = 921600 +monitor_port = /dev/ttyUSB0 +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder diff --git a/src/INDEX_HTML.cpp b/src/INDEX_HTML.cpp new file mode 100644 index 0000000..fc29e38 --- /dev/null +++ b/src/INDEX_HTML.cpp @@ -0,0 +1,13 @@ +#include "INDEX_HTML.h" + +const char *INDEX_HTML = R"( + + + + Sporttafel + + +

Sporttafel

+ + +)"; diff --git a/src/INDEX_HTML.h b/src/INDEX_HTML.h new file mode 100644 index 0000000..6f974b3 --- /dev/null +++ b/src/INDEX_HTML.h @@ -0,0 +1,6 @@ +#ifndef INDEX_HTML_H +#define INDEX_HTML_H + +extern const char *INDEX_HTML; + +#endif diff --git a/src/app/App.cpp b/src/app/App.cpp new file mode 100644 index 0000000..00055fc --- /dev/null +++ b/src/app/App.cpp @@ -0,0 +1,49 @@ +#include "App.h" + +#include "AppMatch.h" + +App *app = nullptr; + +uint8_t getHeapUsagePercent() { + return (ESP.getHeapSize() - ESP.getFreeHeap()) / ESP.getHeapSize(); +} + +void appStart(const String& name) { + appStop(); + Serial.printf("Loading app: \"%s\" (heap usage: %d%%)\n", name.c_str(), getHeapUsagePercent()); + + if (name.equals(APP_MATCH_NAME)) { + app = new AppMatch(); + } else { + Serial.printf("No such app: \"%s\"\n", name.c_str()); + return; + } + Serial.printf("App instantiated: \"%s\" (heap usage: %d%%)\n", app->getName(), getHeapUsagePercent()); + + app->start(); + Serial.printf("App started: \"%s\" (heap usage: %d%%)\n", app->getName(), getHeapUsagePercent()); +} + +void appStop() { + if (app == nullptr) { + return; + } + const auto name = app->getName(); + Serial.printf("Stopping app: \"%s\" (heap usage: %d%%)\n", name, getHeapUsagePercent()); + + app->stop(); + Serial.printf("App stopped: \"%s\" (heap usage: %d%%)\n", name, getHeapUsagePercent()); + + delete app; + app = nullptr; + Serial.printf("App unloaded: \"%s\" (heap usage: %d%%)\n", name, getHeapUsagePercent()); + + display.clear(); + display.flush(); +} + +void appLoop() { + if (app != nullptr) { + app->loop(); + } +} diff --git a/src/app/App.h b/src/app/App.h new file mode 100644 index 0000000..0b9f38e --- /dev/null +++ b/src/app/App.h @@ -0,0 +1,98 @@ +#ifndef APP_H +#define APP_H + +#include +#include + +class App { + + const char *name; + + JsonDocument configJson; + + JsonObject config = configJson.to(); + + bool dirty = true; + +public: + + explicit App(const char *name) + : name(name) { + // + } + + virtual ~App() = default; + + const char *getName() const { + return name; + } + + void start() { + configLoad(); + _start(); + markDirty(); + } + + void loop() { + static auto lastMillis = millis(); + const auto dtMillis = millis() - lastMillis; + _loop(dtMillis); + if (dirty) { + dirty = false; + _draw(); + } + } + + void stop() { + _stop(); + } + +protected: + + template + T configRead(const char *key, T fallback) { + if (config[key].is()) { + return config[key].as(); + } + return fallback; + } + + virtual void _start() { + // + } + + virtual void _loop(unsigned long dtMillis) { + // + } + + virtual void _draw() { + // + } + + virtual void _stop() { + // + } + + void markDirty() { + dirty = true; + } + +private: + + void configLoad() { + auto file = LittleFS.open(String("/apps/") + name + ".json", "r"); + deserializeJson(configJson, file); + config = configJson.to(); + } + +}; + +extern App *app; + +void appStart(const String& name); + +void appStop(); + +void appLoop(); + +#endif diff --git a/src/app/AppMatch.h b/src/app/AppMatch.h new file mode 100644 index 0000000..32d2d6d --- /dev/null +++ b/src/app/AppMatch.h @@ -0,0 +1,165 @@ +#ifndef APP_MATCH_H +#define APP_MATCH_H + +#include + +#include "App.h" + +#define APP_MATCH_NAME "match" + +#define CONFIG_SECONDS_KEY "seconds" +#define CONFIG_SECONDS_DEFAULT (6 * 60) + +class AppMatch final : public App { + + unsigned long configMillis = 0; + + unsigned long totalMillis = 0; + + unsigned long totalCentis = 0; + + unsigned long totalSeconds = 0; + + unsigned long totalMinutes = 0; + + unsigned long partCentis = 0; + + unsigned long partSeconds = 0; + + bool blinkState = false; + + unsigned long blinkIntervalMillis = 0; + + unsigned long blinkMillis = 0; + + unsigned long updateSeconds = totalSeconds; + + enum State { + PAUSE, MINUTES, SECONDS, END + }; + + State state = PAUSE; + +public: + + explicit AppMatch() + : App(APP_MATCH_NAME) { + // + } + +protected: + + void _start() override { + configMillis = configRead(CONFIG_SECONDS_KEY,CONFIG_SECONDS_DEFAULT) * 1000; + totalMillis = configMillis; + setState(PAUSE); + } + + void _loop(const unsigned long dtMillis) override { + if (state != PAUSE) { + if (totalMillis <= dtMillis) { + totalMillis = 0; + } else { + 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); + } + } + + if (blinkIntervalMillis > 0) { + const auto now = millis(); + if (blinkMillis - now > 500) { + blinkMillis = now; + blinkState = !blinkState; + markDirty(); + } + } + + if (state == MINUTES) { + if (updateSeconds != totalSeconds) { + updateSeconds = totalSeconds; + markDirty(); + } + } + + if (state == SECONDS) { + markDirty(); + } + } + + void _draw() override { + display.clear(); + if (blinkIntervalMillis == 0 || blinkState) { + if (totalMinutes > 0) { + display.setColor(totalMillis < configMillis / 2 ? YELLOW : GREEN); + display.printf("%2d:%02d", totalMinutes, partSeconds); + Serial.printf("%2d:%02d", totalMinutes, partSeconds); + } else if (totalMillis > 0) { + display.setColor(RED); + display.printf("%2d.%02d", partSeconds, partCentis); + } else { + display.printf("00:00"); + } + } + display.flush(); + } + +private: + + void blinkEnable(const unsigned long intervalMillis) { + blinkState = true; + blinkIntervalMillis = intervalMillis; + blinkMillis = millis(); + } + + void setState(const State newState) { + if (state == newState) { + return; + } + state = newState; + switch (state) { + case PAUSE: + blinkEnable(500); + break; + case MINUTES: + updateSeconds = totalSeconds; + blinkEnable(0); + case SECONDS: + blinkEnable(0); + break; + case END: + blinkEnable(100); + break; + } + Serial.printf("state changed to %s", getStateName()); + markDirty(); + } + + const char *getStateName() const { + switch (state) { + case PAUSE: return "PAUSE"; + case MINUTES: return "MINUTES"; + case SECONDS: return "SECONDS"; + case END: return "END"; + default: return "[???]"; + } + } + +}; + +#endif diff --git a/src/boot.cpp b/src/boot.cpp new file mode 100644 index 0000000..68c92bd --- /dev/null +++ b/src/boot.cpp @@ -0,0 +1,29 @@ +#include "boot.h" + +#include "wifi.h" +#include "clock.h" +#include "log.h" + +void bootDelay() { + info("Waiting for WiFi..."); + while (!isWiFiConnected()) { + wifiLoop(); + yield(); + } + + info("Waiting for clock to be set..."); + while (!isClockSet()) { + wifiLoop(); + clockLoop(); + yield(); + } + + info("Waiting 5 seconds for OTA update..."); + const auto start = millis(); + while (millis() - start < 5000) { + wifiLoop(); + yield(); + } + + info("Boot delay complete."); +} diff --git a/src/boot.h b/src/boot.h new file mode 100644 index 0000000..5bf26fc --- /dev/null +++ b/src/boot.h @@ -0,0 +1,6 @@ +#ifndef BOOT_H +#define BOOT_H + +void bootDelay(); + +#endif //BOOT_H diff --git a/src/clock.cpp b/src/clock.cpp new file mode 100644 index 0000000..9941ee5 --- /dev/null +++ b/src/clock.cpp @@ -0,0 +1,68 @@ +#include "clock.h" + +#include + +#include "log.h" +#include "wifi.h" + +#define CLOCK_GMT_OFFSET_SECONDS 3600 +#define CLOCK_DST_OFFSET_SECONDS 3600 +#define CLOCK_NTP_SERVER2_URL "de.pool.ntp.org" +#define CLOCK_EPOCH_SECONDS_MIN 1735686000 + +time_t clockOffset = 0; + +time_t startupTime = 0; + +auto ntpSet = false; + +void clockLoop() { + if (isClockSet()) { + return; + } + + if (!ntpSet && isWiFiConnected()) { + configTime(CLOCK_GMT_OFFSET_SECONDS, CLOCK_DST_OFFSET_SECONDS, WiFi.gatewayIP().toString().c_str(), CLOCK_NTP_SERVER2_URL); + ntpSet = true; + } + + const auto now = time(nullptr); + if (isCorrectTime(now)) { + clockOffset = now; + } else { + startupTime = now - clockOffset; + info("clock set after %ld seconds! So startup was at %s", clockOffset, getClockStr()); + } +} + +bool isClockSet() { + return startupTime != 0; +} + +char buffer[20]; + +char *getClockStr() { + const auto now = time(nullptr); + tm t{0}; + localtime_r(&now, &t); + snprintf(buffer, sizeof buffer, "%04d-%02d-%02d %02d:%02d:%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec); + return buffer; +} + +bool isCorrectTime(const time_t now) { + return now < CLOCK_EPOCH_SECONDS_MIN; +} + +time_t clockCorrect(const time_t t) { + if (!isClockSet() || isCorrectTime(t)) { + return t; + } + return t + clockOffset; +} + +void clockCorrect(time_t *t) { + if (!isClockSet() || isCorrectTime(*t)) { + return; + } + *t += clockOffset; +} diff --git a/src/clock.h b/src/clock.h new file mode 100644 index 0000000..f3d992a --- /dev/null +++ b/src/clock.h @@ -0,0 +1,18 @@ +#ifndef CLOCK_H +#define CLOCK_H + +#include + +void clockLoop(); + +bool isClockSet(); + +char * getClockStr(); + +bool isCorrectTime(time_t now); + +time_t clockCorrect(time_t t); + +void clockCorrect(time_t *t); + +#endif diff --git a/src/display/Color.cpp b/src/display/Color.cpp new file mode 100644 index 0000000..ca41cf2 --- /dev/null +++ b/src/display/Color.cpp @@ -0,0 +1,34 @@ +#include "Color.h" + +#define ____ 0 +#define QUAR 64 +#define HALF 128 +#define FULL 255 + +const Color BLACK = {____, ____, ____}; + +const Color WHITE = {FULL, FULL, FULL}; + +const Color RED = {FULL, ____, ____}; + +const Color GREEN = {____, FULL, ____}; + +const Color ORANGE = {FULL, QUAR, ____}; + +const Color BLUE = {____, ____, FULL}; + +const Color YELLOW = {FULL, FULL, ____}; + +const Color MAGENTA = {FULL, ____, FULL}; + +const Color VIOLET = {HALF, ____, FULL}; + +const Color TURQUOISE = {____, FULL, FULL}; + +Color gray(uint8_t brightness) { + return {brightness, brightness, brightness}; +} + +Color randomColor() { + return {(uint8_t) random(255), (uint8_t) random(255), (uint8_t) random(255)}; +} diff --git a/src/display/Color.h b/src/display/Color.h new file mode 100644 index 0000000..dd2bc4a --- /dev/null +++ b/src/display/Color.h @@ -0,0 +1,40 @@ +#ifndef PIXEL_H +#define PIXEL_H + +#include + +struct Color { + + uint8_t r; + + uint8_t g; + + uint8_t b; + +}; + +Color gray(uint8_t brightness); + +Color randomColor(); + +extern const Color BLACK; + +extern const Color WHITE; + +extern const Color RED; + +extern const Color GREEN; + +extern const Color ORANGE; + +extern const Color BLUE; + +extern const Color YELLOW; + +extern const Color MAGENTA; + +extern const Color VIOLET; + +extern const Color TURQUOISE; + +#endif diff --git a/src/display/Display.cpp b/src/display/Display.cpp new file mode 100644 index 0000000..44bed1d --- /dev/null +++ b/src/display/Display.cpp @@ -0,0 +1,3 @@ +#include "Display.h" + +Display display; diff --git a/src/display/Display.h b/src/display/Display.h new file mode 100644 index 0000000..2bca767 --- /dev/null +++ b/src/display/Display.h @@ -0,0 +1,140 @@ +#ifndef DISPLAY_H +#define DISPLAY_H + +// #include + +#include "Color.h" +#include "font.h" + +#define PIXELS_PER_SEGMENT 3 +#define SEGMENTS_PER_DIGIT 7 +#define PIXELS_PER_DOT 1 +#define DOTS_PER_DIGIT 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)) + +class Display { + + // Adafruit_NeoPixel leds; + + Color buffer[PIXEL_COUNT] = {}; + + Color color = WHITE; + +public: + + Display() /* : leds(PIXEL_COUNT, GPIO_NUM_13) */ { + Serial.printf("%20s = %d\n", "PIXELS_PER_SEGMENT", PIXELS_PER_SEGMENT); + Serial.printf("%20s = %d\n", "PIXELS_PER_DOT", PIXELS_PER_DOT); + Serial.printf("%20s = %d\n", "SEGMENTS_PER_DIGIT", SEGMENTS_PER_DIGIT); + Serial.printf("%20s = %d\n", "DOTS_PER_DIGIT", DOTS_PER_DIGIT); + Serial.printf("%20s = %d\n", "DIGITS", DIGITS); + Serial.printf("%20s = %d\n", "PIXELS_PER_DIGIT_SEGMENTS", PIXELS_PER_DIGIT_SEGMENTS); + Serial.printf("%20s = %d\n", "PIXELS_PER_DIGIT_DOTS", PIXELS_PER_DIGIT_DOTS); + Serial.printf("%20s = %d\n", "PIXELS_PER_DIGIT_AND_DOTS", PIXELS_PER_DIGIT_AND_DOTS); + Serial.printf("%20s = %d\n", "PIXEL_COUNT", PIXEL_COUNT); + Serial.printf("%20s = %d\n", "PIXEL_BYTE_COUNT", PIXEL_BYTE_COUNT); + // leds.begin(); + setBrightness(6); + clear(); + flush(); + } + + void setColor(const Color color) { + this->color = color; + } + + void setBrightness(const int brightness) { + // leds.setBrightness(brightness); + } + + void clear() { + memset(buffer, 0, PIXEL_BYTE_COUNT); + } + + void flush() { + // memcpy(leds.getPixels(), buffer, PIXEL_BYTE_COUNT); + // leds.show(); + } + + int printf(const char *format, ...) { + char buffer[MAX_STR_LEN + 1]; + + va_list args; + va_start(args, format); + vsnprintf(buffer, sizeof buffer, format, args); + va_end(args); + + return 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); + } + } + + 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; + } + if (dot0) { + buffer[pixel] = color; + } + pixel++; + if (dot1) { + buffer[pixel] = color; + } + pixel++; + if (dot2) { + buffer[pixel] = color; + } + pixel++; + if (dot3) { + buffer[pixel] = color; + } + return pixel; + } + + int printCharacter(int pixel, const char character) { + pixel = pixel / PIXELS_PER_DIGIT_AND_DOTS; + const auto symbol = getSymbol(character); + for (auto s = *symbol; s < *symbol + SYMBOL_SIZE; s++) { + if (*s) { + buffer[pixel++] = color; + buffer[pixel++] = color; + buffer[pixel++] = color; + } else { + pixel += 3; + } + } + return pixel; + } + +}; + +extern Display display; + +#endif diff --git a/src/display/font.cpp b/src/display/font.cpp new file mode 100644 index 0000000..3ded170 --- /dev/null +++ b/src/display/font.cpp @@ -0,0 +1,68 @@ +#include "font.h" + +#include + +SYMBOL SYMBOLS[][SYMBOL_SIZE] = { + {X,X,X,X,X,X,_}, // 0 + {_,_,X,X,_,_,_}, // 1 + {_,X,X,_,X,X,X}, // 2 + {_,X,X,X,X,_,X}, // 3 + {X,_,X,X,_,_,X}, // 4 + {X,X,_,X,X,_,X}, // 5 + {X,X,_,X,X,X,X}, // 6 + {_,X,X,X,_,_,_}, // 7 + {X,X,X,X,X,X,X}, // 8 + {X,X,X,X,X,_,X}, // 9 + {X,X,X,X,_,X,X}, // A + {X,_,_,X,X,X,X}, // B + {X,X,_,_,X,X,_}, // C + {_,_,X,X,X,X,X}, // D + {X,X,_,_,X,X,X}, // E + {X,X,_,_,_,X,X}, // F + {X,X,_,X,X,X,_}, // G + {X,_,X,X,_,X,X}, // H + {_,_,_,X,_,_,_}, // I + {_,_,X,X,X,_,_}, // J + {X,X,_,X,_,X,X}, // K + {X,_,_,X,X,_,_}, // L + {_,X,_,X,_,X,X}, // M + {_,_,_,X,_,X,X}, // N + {_,_,_,X,X,X,X}, // O + {X,X,X,_,_,X,X}, // P + {X,X,X,X,_,_,X}, // Q + {_,_,_,_,_,X,X}, // R + {X,X,_,X,X,_,X}, // S + {X,_,_,_,X,X,X}, // T + {_,_,_,X,X,X,_}, // U + {X,_,X,_,X,_,_}, // V + {X,_,X,_,X,_,X}, // W + {_,_,_,X,_,X,_}, // X + {X,_,X,X,X,_,X}, // Y + {_,X,X,_,X,X,X}, // Z + {_,_,_,_,_,_,X}, // - + {_,_,_,_,X,_,_}, // _ + {X,X,X,_,_,_,X}, // ° + {_,_,_,_,_,_,_}, // +}; + +SYMBOL *getSymbol(const char character) { + if (character >= '0' && character <= '9') { + return SYMBOLS[character - '0']; + } + if (character >= 'a' && character <= 'z') { + return SYMBOLS[character - 'a' + 10]; + } + if (character >= 'A' && character <= 'Z') { + return SYMBOLS[character - 'A' + 10]; + } + switch (character) { + case '-': return SYMBOLS[36]; + case '_': return SYMBOLS[37]; + case '^': return SYMBOLS[38]; + case ' ': return SYMBOLS[39]; + default: { + Serial.printf("[ERROR] NO SYMBOL MAPPING FOR CHARACTER \"%c\" = #%d\n", character, character); + return SYMBOLS[SYMBOL_SIZE - 1]; + } + } +} diff --git a/src/display/font.h b/src/display/font.h new file mode 100644 index 0000000..d806d1a --- /dev/null +++ b/src/display/font.h @@ -0,0 +1,17 @@ +#ifndef FONT_H +#define FONT_H + +#define X true +#define _ false + +constexpr auto SYMBOL_COUNT = 39; + +constexpr auto SYMBOL_SIZE = 7; + +typedef const bool SYMBOL[SYMBOL_SIZE]; + +extern SYMBOL SYMBOLS[][SYMBOL_SIZE]; + +SYMBOL *getSymbol(char character); + +#endif diff --git a/src/filesystem.cpp b/src/filesystem.cpp new file mode 100644 index 0000000..3cdba09 --- /dev/null +++ b/src/filesystem.cpp @@ -0,0 +1,11 @@ +#include "filesystem.h" + +#include + +void filesystemMount() { + if (LittleFS.begin(true)) { + Serial.println("filesystem mounted"); + } else { + Serial.println("failed to mount filesystem"); + } +} diff --git a/src/filesystem.h b/src/filesystem.h new file mode 100644 index 0000000..dc08a30 --- /dev/null +++ b/src/filesystem.h @@ -0,0 +1,6 @@ +#ifndef FILESYSTEM_H +#define FILESYSTEM_H + +void filesystemMount() ; + +#endif diff --git a/src/http.cpp b/src/http.cpp new file mode 100644 index 0000000..eaaef69 --- /dev/null +++ b/src/http.cpp @@ -0,0 +1,53 @@ +#include "http.h" + +#include + +#include "INDEX_HTML.h" +#include "log.h" + +AsyncWebServer server(80); + +AsyncWebSocket ws("/ws"); + +void httpIndex(AsyncWebServerRequest *request) { + request->send(200, "text/html", INDEX_HTML); +} + +void httpSetup() { + ws.onEvent([](AsyncWebSocket *socket, AsyncWebSocketClient *client, AwsEventType type, void *arg, unsigned char *message, unsigned length) { + const char *t; + switch (type) { + case WS_EVT_CONNECT: + t = "CONNECT"; + break; + case WS_EVT_DISCONNECT: + t = "DISCONNECT"; + break; + case WS_EVT_PONG: + t = "PONG"; + break; + case WS_EVT_ERROR: + t = "ERROR"; + break; + case WS_EVT_DATA: + t = "DATA"; + break; + default: + t = "[???]"; + break; + } + debug("%s: %s (%d bytes)", client->remoteIP().toString().c_str(), t, length); + }); + server.addHandler(&ws); + + server.on("/", HTTP_GET, httpIndex); + server.begin(); +} + +void httpLoop() { + ws.cleanupClients(); +} + +void httpPublish(char *payload) { + ws.textAll(payload); +} diff --git a/src/http.h b/src/http.h new file mode 100644 index 0000000..e4c0a70 --- /dev/null +++ b/src/http.h @@ -0,0 +1,10 @@ +#ifndef HTTP_H +#define HTTP_H + +void httpSetup(); + +void httpLoop(); + +void httpPublish(char *payload); + +#endif diff --git a/src/log.cpp b/src/log.cpp new file mode 100644 index 0000000..58e1874 --- /dev/null +++ b/src/log.cpp @@ -0,0 +1,71 @@ +#include "log.h" + +#include +#include + +#include "clock.h" + +auto logLevel = DEBUG; + +void log(const LogLevel level, const char *format, const va_list args) { + if (level > logLevel) { + return; + } + Serial.print(getClockStr()); + switch (level) { + case ERROR: + Serial.print(" [ERROR] "); + break; + case WARN: + Serial.print(" [WARN ] "); + break; + case INFO: + Serial.print(" [INFO ] "); + break; + case DEBUG: + Serial.print(" [DEBUG] "); + break; + } + + char message[256]; + vsnprintf(message, sizeof message, format, args); + Serial.print(message); + Serial.print("\n"); + + yield(); +} + +void log(const LogLevel level, const char *format, ...) { + va_list args; + va_start(args, format); + log(level, format, args); + va_end(args); +} + +void error(const char *format, ...) { + va_list args; + va_start(args, format); + log(ERROR, format, args); + va_end(args); +} + +void warn(const char *format, ...) { + va_list args; + va_start(args, format); + log(WARN, format, args); + va_end(args); +} + +void info(const char *format, ...) { + va_list args; + va_start(args, format); + log(INFO, format, args); + va_end(args); +} + +void debug(const char *format, ...) { + va_list args; + va_start(args, format); + log(DEBUG, format, args); + va_end(args); +} diff --git a/src/log.h b/src/log.h new file mode 100644 index 0000000..f088acc --- /dev/null +++ b/src/log.h @@ -0,0 +1,21 @@ +#ifndef LOG_H +#define LOG_H + +enum LogLevel { + ERROR = 0, + WARN = 1, + INFO = 2, + DEBUG = 3 +}; + +void log(LogLevel level, const char *format, ...); + +void error(const char *format, ...); + +void warn(const char *format, ...); + +void info(const char *format, ...); + +void debug(const char *format, ...); + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..93741b2 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,23 @@ +#include + +#include "boot.h" +#include "filesystem.h" +#include "http.h" +#include "wifi.h" +#include "app/AppMatch.h" + +void setup() { + delay(500); + Serial.begin(115200); + Serial.print("Startup\n"); + bootDelay(); + filesystemMount(); + httpSetup(); + appStart(APP_MATCH_NAME); +} + +void loop() { + wifiLoop(); + httpLoop(); + appLoop(); +} diff --git a/src/wifi.cpp b/src/wifi.cpp new file mode 100644 index 0000000..200593e --- /dev/null +++ b/src/wifi.cpp @@ -0,0 +1,92 @@ +#include "wifi.h" + +#include +#include "log.h" + +#define WIFI_TIMEOUT_MILLIS 10000 + +auto wifiEnabled = true; + +auto wifiConnected = false; + +auto wifiTryMillis = 0UL; + +auto wifiSsid = WIFI_SSID; + +auto wifiPkey = WIFI_PKEY; + +auto wifiHost = WIFI_HOST; + +void wifiSetupOTA() { + ArduinoOTA.onStart([] { + info("beginning ota update..."); + Serial.print("OTA-UPDATE: 0%"); + }); + ArduinoOTA.onProgress([](const unsigned done, const unsigned total) { + Serial.printf("\rOTA-UPDATE: %3.0f%%", 100.0 * done / total); + }); + ArduinoOTA.onEnd([] { + Serial.print("\rOTA-UPDATE: COMPLETE\n"); + info("OTA update complete"); + }); + ArduinoOTA.onError([](const ota_error_t errorCode) { + auto name = "[???]"; + switch (errorCode) { + case OTA_AUTH_ERROR: + name = "AUTH"; + break; + case OTA_BEGIN_ERROR: + name = "BEGIN"; + break; + case OTA_CONNECT_ERROR: + name = "CONNECT"; + break; + case OTA_RECEIVE_ERROR: + name = "RECEIVE"; + break; + case OTA_END_ERROR: + name = "END"; + break; + } + Serial.printf("\nOTA-UPDATE: ERROR #%d=%s\n", errorCode, name); + error("OTA update failed: #%d=%s", errorCode, name); + }); + ArduinoOTA.begin(); +} + +void wifiOff() { + info("wifi disabled"); + wifiEnabled = false; +} + +void wifiLoop() { + const auto currentState = WiFi.localIP() != 0; + if (wifiConnected != currentState) { + wifiConnected = currentState; + if (wifiConnected) { + info("wifi connected: %s", WiFi.localIP().toString().c_str()); + wifiSetupOTA(); + } else { + warn("wifi disconnected"); + ArduinoOTA.end(); + WiFi.disconnect(); + } + } else if (!wifiConnected) { + if (!wifiEnabled) { + return; + } + if (wifiTryMillis == 0 || millis() - wifiTryMillis >= WIFI_TIMEOUT_MILLIS) { + wifiTryMillis = millis(); + WiFiClass::hostname(wifiHost); + WiFi.setAutoReconnect(true); + info(R"(connecting to SSID "%s" with hostname "%s")", wifiSsid, wifiHost); + WiFi.begin(wifiSsid, wifiPkey); + } + } else { + ArduinoOTA.handle(); + } +} + +bool isWiFiConnected() { + return wifiConnected; +} diff --git a/src/wifi.h b/src/wifi.h new file mode 100644 index 0000000..074d5c9 --- /dev/null +++ b/src/wifi.h @@ -0,0 +1,10 @@ +#ifndef WIFI_H +#define WIFI_H + +void wifiOff(); + +void wifiLoop(); + +bool isWiFiConnected(); + +#endif