log, boot, wifi, clock, http, filesystem, Display, App, AppMatch [INACTIVE Adafruit_NeoPixel]
This commit is contained in:
commit
65a7f170f9
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/.pio/
|
||||||
|
/.idea/
|
||||||
18
platformio.ini
Normal file
18
platformio.ini
Normal file
@ -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
|
||||||
13
src/INDEX_HTML.cpp
Normal file
13
src/INDEX_HTML.cpp
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#include "INDEX_HTML.h"
|
||||||
|
|
||||||
|
const char *INDEX_HTML = R"(<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||||
|
<title>Sporttafel</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Sporttafel</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)";
|
||||||
6
src/INDEX_HTML.h
Normal file
6
src/INDEX_HTML.h
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#ifndef INDEX_HTML_H
|
||||||
|
#define INDEX_HTML_H
|
||||||
|
|
||||||
|
extern const char *INDEX_HTML;
|
||||||
|
|
||||||
|
#endif
|
||||||
49
src/app/App.cpp
Normal file
49
src/app/App.cpp
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/app/App.h
Normal file
98
src/app/App.h
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
#ifndef APP_H
|
||||||
|
#define APP_H
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
|
||||||
|
class App {
|
||||||
|
|
||||||
|
const char *name;
|
||||||
|
|
||||||
|
JsonDocument configJson;
|
||||||
|
|
||||||
|
JsonObject config = configJson.to<JsonObject>();
|
||||||
|
|
||||||
|
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<typename T>
|
||||||
|
T configRead(const char *key, T fallback) {
|
||||||
|
if (config[key].is<T>()) {
|
||||||
|
return config[key].as<T>();
|
||||||
|
}
|
||||||
|
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<JsonObject>();
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
extern App *app;
|
||||||
|
|
||||||
|
void appStart(const String& name);
|
||||||
|
|
||||||
|
void appStop();
|
||||||
|
|
||||||
|
void appLoop();
|
||||||
|
|
||||||
|
#endif
|
||||||
165
src/app/AppMatch.h
Normal file
165
src/app/AppMatch.h
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
#ifndef APP_MATCH_H
|
||||||
|
#define APP_MATCH_H
|
||||||
|
|
||||||
|
#include <display/Display.h>
|
||||||
|
|
||||||
|
#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<unsigned long>(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
|
||||||
29
src/boot.cpp
Normal file
29
src/boot.cpp
Normal file
@ -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.");
|
||||||
|
}
|
||||||
6
src/boot.h
Normal file
6
src/boot.h
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#ifndef BOOT_H
|
||||||
|
#define BOOT_H
|
||||||
|
|
||||||
|
void bootDelay();
|
||||||
|
|
||||||
|
#endif //BOOT_H
|
||||||
68
src/clock.cpp
Normal file
68
src/clock.cpp
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#include "clock.h"
|
||||||
|
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
18
src/clock.h
Normal file
18
src/clock.h
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#ifndef CLOCK_H
|
||||||
|
#define CLOCK_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
void clockLoop();
|
||||||
|
|
||||||
|
bool isClockSet();
|
||||||
|
|
||||||
|
char * getClockStr();
|
||||||
|
|
||||||
|
bool isCorrectTime(time_t now);
|
||||||
|
|
||||||
|
time_t clockCorrect(time_t t);
|
||||||
|
|
||||||
|
void clockCorrect(time_t *t);
|
||||||
|
|
||||||
|
#endif
|
||||||
34
src/display/Color.cpp
Normal file
34
src/display/Color.cpp
Normal file
@ -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)};
|
||||||
|
}
|
||||||
40
src/display/Color.h
Normal file
40
src/display/Color.h
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#ifndef PIXEL_H
|
||||||
|
#define PIXEL_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
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
|
||||||
3
src/display/Display.cpp
Normal file
3
src/display/Display.cpp
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#include "Display.h"
|
||||||
|
|
||||||
|
Display display;
|
||||||
140
src/display/Display.h
Normal file
140
src/display/Display.h
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
#ifndef DISPLAY_H
|
||||||
|
#define DISPLAY_H
|
||||||
|
|
||||||
|
// #include <Adafruit_NeoPixel.h>
|
||||||
|
|
||||||
|
#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
|
||||||
68
src/display/font.cpp
Normal file
68
src/display/font.cpp
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#include "font.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/display/font.h
Normal file
17
src/display/font.h
Normal file
@ -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
|
||||||
11
src/filesystem.cpp
Normal file
11
src/filesystem.cpp
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#include "filesystem.h"
|
||||||
|
|
||||||
|
#include <LittleFS.h>
|
||||||
|
|
||||||
|
void filesystemMount() {
|
||||||
|
if (LittleFS.begin(true)) {
|
||||||
|
Serial.println("filesystem mounted");
|
||||||
|
} else {
|
||||||
|
Serial.println("failed to mount filesystem");
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/filesystem.h
Normal file
6
src/filesystem.h
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#ifndef FILESYSTEM_H
|
||||||
|
#define FILESYSTEM_H
|
||||||
|
|
||||||
|
void filesystemMount() ;
|
||||||
|
|
||||||
|
#endif
|
||||||
53
src/http.cpp
Normal file
53
src/http.cpp
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#include "http.h"
|
||||||
|
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
10
src/http.h
Normal file
10
src/http.h
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#ifndef HTTP_H
|
||||||
|
#define HTTP_H
|
||||||
|
|
||||||
|
void httpSetup();
|
||||||
|
|
||||||
|
void httpLoop();
|
||||||
|
|
||||||
|
void httpPublish(char *payload);
|
||||||
|
|
||||||
|
#endif
|
||||||
71
src/log.cpp
Normal file
71
src/log.cpp
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdarg>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
21
src/log.h
Normal file
21
src/log.h
Normal file
@ -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
|
||||||
23
src/main.cpp
Normal file
23
src/main.cpp
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#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();
|
||||||
|
}
|
||||||
92
src/wifi.cpp
Normal file
92
src/wifi.cpp
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
#include "wifi.h"
|
||||||
|
|
||||||
|
#include <ArduinoOTA.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
10
src/wifi.h
Normal file
10
src/wifi.h
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#ifndef WIFI_H
|
||||||
|
#define WIFI_H
|
||||||
|
|
||||||
|
void wifiOff();
|
||||||
|
|
||||||
|
void wifiLoop();
|
||||||
|
|
||||||
|
bool isWiFiConnected();
|
||||||
|
|
||||||
|
#endif
|
||||||
Loading…
Reference in New Issue
Block a user