log, boot, wifi, clock, http, filesystem, Display, App, AppMatch [INACTIVE Adafruit_NeoPixel]

This commit is contained in:
Patrick Haßel 2025-01-08 15:48:29 +01:00
commit 65a7f170f9
26 changed files with 1071 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.pio/
/.idea/

18
platformio.ini Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
#ifndef BOOT_H
#define BOOT_H
void bootDelay();
#endif //BOOT_H

68
src/clock.cpp Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
#include "Display.h"
Display display;

140
src/display/Display.h Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
#ifndef FILESYSTEM_H
#define FILESYSTEM_H
void filesystemMount() ;
#endif

53
src/http.cpp Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,10 @@
#ifndef WIFI_H
#define WIFI_H
void wifiOff();
void wifiLoop();
bool isWiFiConnected();
#endif