commit a6fb6af1a953acc253083f044ce1e69280d49a54 Author: Patrick Haßel Date: Wed Jan 22 14:00:13 2025 +0100 boot, clock, http, log, mqtt, system, wifi, Node, demo, NodeTest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24d87e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.pio/ +/.idea/ +/src/patrix/library.json +/*.tar* \ No newline at end of file diff --git a/library.json b/library.json new file mode 100644 index 0000000..10c8459 --- /dev/null +++ b/library.json @@ -0,0 +1,29 @@ +{ + "name": "Patrix", + "version": "0.1.0", + "description": "Patrix library", + "keywords": "patrix", + "authors": [ + { + "name": "Patrick Haßel", + "email": "development@patrick-hassel.de", + "url": "https://patrick-hassel.de/", + "maintainer": true + } + ], + "license": "MIT", + "homepage": "https://patrick-hassel.de/", + "dependencies": { + "knolleary/pubsubclient": "2.8", + "bblanchon/ArduinoJson": "7.3.0", + "me-no-dev/ESPAsyncWebServer": "1.2.4" + }, + "export": { + "include": "src/patrix" + }, + "frameworks": "arduino", + "platforms": [ + "espressif8266", + "espressif32" + ] +} \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..60c714e --- /dev/null +++ b/platformio.ini @@ -0,0 +1,9 @@ +[env:Patrix] +platform = espressif32 +board = esp32dev +framework = arduino +lib_deps = + knolleary/pubsubclient@2.8 + bblanchon/ArduinoJson@7.3.0 + me-no-dev/ESPAsyncWebServer@1.2.4 +build_flags = -DWIFI_SSID=\"HappyNet\" -DWIFI_PKEY=\"1Grausame!Sackratte7\" -DWIFI_HOST=\"PatrixTest\" \ No newline at end of file diff --git a/src/demo/NodeTest.h b/src/demo/NodeTest.h new file mode 100644 index 0000000..eff3ed1 --- /dev/null +++ b/src/demo/NodeTest.h @@ -0,0 +1,21 @@ +#ifndef NODE_TEST_H +#define NODE_TEST_H + +#include +#include + +class NodeTest final : public Node { + +public: + + explicit NodeTest() : Node(true, true, true) { + // + } + + void setup() override { + display.printf("Test"); + } + +}; + +#endif diff --git a/src/demo/main.cpp b/src/demo/main.cpp new file mode 100644 index 0000000..d91b6dc --- /dev/null +++ b/src/demo/main.cpp @@ -0,0 +1,7 @@ +#include "NodeTest.h" + +auto node = NodeTest(); + +Node &patrixGetNode() { + return node; +} diff --git a/src/library.json b/src/library.json new file mode 100644 index 0000000..771753c --- /dev/null +++ b/src/library.json @@ -0,0 +1,47 @@ +{ + "name": "Patrix", + "version": "0.1.0", + "authors": [ + { + "name": "Patrick Haßel", + "email": "development@patrick-hassel.de", + "maintainer": true, + "url": "https://patrick-hassel.de/" + } + ], + "description": "Patrix library", + "homepage": "https://patrick-hassel.de/", + "license": "MIT", + "dependencies": [ + { + "owner": "knolleary", + "name": "pubsubclient", + "version": "2.8" + }, + { + "owner": "bblanchon", + "name": "ArduinoJson", + "version": "7.3.0" + }, + { + "owner": "me-no-dev", + "name": "ESPAsyncWebServer", + "version": "1.2.4" + } + ], + "export": { + "exclude": [ + "src/demo/**" + ] + }, + "keywords": [ + "patrix" + ], + "platforms": [ + "espressif8266", + "espressif32" + ], + "frameworks": [ + "arduino" + ] +} \ No newline at end of file diff --git a/src/patrix/Patrix.cpp b/src/patrix/Patrix.cpp new file mode 100644 index 0000000..49fe697 --- /dev/null +++ b/src/patrix/Patrix.cpp @@ -0,0 +1,16 @@ +#include + +void setup() { + logSetup(); + bootDelay(); + patrixNode.setup(); + httpSetup(); +} + +void loop() { + wifiLoop(); + clockLoop(); + mqttLoop(); + patrixNode.loop(); + httpLoop(); +} diff --git a/src/patrix/Patrix.h b/src/patrix/Patrix.h new file mode 100644 index 0000000..e090cd0 --- /dev/null +++ b/src/patrix/Patrix.h @@ -0,0 +1,17 @@ +#ifndef PATRIX_H +#define PATRIX_H + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#endif diff --git a/src/patrix/core/boot.cpp b/src/patrix/core/boot.cpp new file mode 100644 index 0000000..ff21fc9 --- /dev/null +++ b/src/patrix/core/boot.cpp @@ -0,0 +1,95 @@ +#include +#include +#include +#include +#include + +#include "esp32/rom/rtc.h" + +#define BOOT_DELAY_SECONDS 3 + +auto failureReset = false; + +const char *getResetReason(const RESET_REASON reason) { + switch (reason) { + case NO_MEAN: return "NO_MEAN"; + case POWERON_RESET: return "POWERON_RESET"; + case SW_RESET: return "SW_RESET"; + case OWDT_RESET: return "OWDT_RESET"; + case DEEPSLEEP_RESET: return "DEEPSLEEP_RESET"; + case SDIO_RESET: return "SDIO_RESET"; + case TG0WDT_SYS_RESET: return "TG0WDT_SYS_RESET"; + case TG1WDT_SYS_RESET: return "TG1WDT_SYS_RESET"; + case RTCWDT_SYS_RESET: return "RTCWDT_SYS_RESET"; + case INTRUSION_RESET: return "INTRUSION_RESET"; + case TGWDT_CPU_RESET: return "TGWDT_CPU_RESET"; + case SW_CPU_RESET: return "SW_CPU_RESET"; + case RTCWDT_CPU_RESET: return "RTCWDT_CPU_RESET"; + case EXT_CPU_RESET: return "EXT_CPU_RESET"; + case RTCWDT_BROWN_OUT_RESET: return "RTCWDT_BROWN_OUT_RESET"; + case RTCWDT_RTC_RESET: return "RTCWDT_RTC_RESET"; + default: return "[???]"; + } +} + +void bootReasonLoad() { + const auto r0 = rtc_get_reset_reason(0); + const auto r1 = rtc_get_reset_reason(1); + failureReset = (r0 != POWERON_RESET && r0 != DEEPSLEEP_RESET) || (r1 != POWERON_RESET && r1 != DEEPSLEEP_RESET); + if (failureReset) { + warn("Forcing OTA delay because of failure-reset: r0=%s, r1=%s", getResetReason(r0), getResetReason(r1)); + } +} + +void boot_waitForWifi() { + info("Waiting for WiFi..."); + while (!isWiFiConnected()) { + wifiLoop(); + yield(); + } +} + +void boot_waitForOTA() { + info("Waiting %d seconds for OTA update...", BOOT_DELAY_SECONDS); + const auto start = millis(); + while (millis() - start < BOOT_DELAY_SECONDS * 1000) { + wifiLoop(); + yield(); + } +} + +void boot_waitForClock() { + info("Waiting for system time..."); + while (!isClockSet()) { + wifiLoop(); + clockLoop(); + yield(); + } +} + +void bootDelay() { + bootReasonLoad(); + + if (!failureReset && !patrixNode.waitForWiFi && !patrixNode.waitForOTA && !patrixNode.waitForClock) { + info("NOT delaying boot."); + return; + } + + info("Boot delay active..."); + + boot_waitForWifi(); + + if (failureReset || patrixNode.waitForOTA) { + boot_waitForOTA(); + } else { + info("NOT waiting for OTA."); + } + + if (patrixNode.waitForClock) { + boot_waitForClock(); + } else { + info("NOT waiting for system time."); + } + + info("Boot delay complete."); +} diff --git a/src/patrix/core/boot.h b/src/patrix/core/boot.h new file mode 100644 index 0000000..46b0238 --- /dev/null +++ b/src/patrix/core/boot.h @@ -0,0 +1,6 @@ +#ifndef PATRIX_BOOT_H +#define PATRIX_BOOT_H + +void bootDelay(); + +#endif diff --git a/src/patrix/core/clock.cpp b/src/patrix/core/clock.cpp new file mode 100644 index 0000000..fdd112e --- /dev/null +++ b/src/patrix/core/clock.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include + +#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, CLOCK_NTP_SERVER2_URL, WiFi.gatewayIP().toString().c_str()); + ntpSet = true; + } + + const auto now = time(nullptr); + if (isCorrectTime(now)) { + startupTime = now - clockOffset; + info("clock set after %ld seconds! So startup was at %s", clockOffset, getStartupStr()); + } else { + clockOffset = now; + } +} + +bool isClockSet() { + return startupTime != 0; +} + + +char *getClockStr(const time_t epoch) { + auto now = epoch; + if (now == 0) { + now = time(nullptr); + } + + tm t{}; + localtime_r(&now, &t); + + static char buffer[20]; + 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; +} + +char *getStartupStr() { + return getClockStr(startupTime); +} + +time_t getUptimeSeconds() { + return time(nullptr) - startupTime; +} + +char *getUptimeStr() { + const auto totalSeconds = getUptimeSeconds(); + const auto totalMinutes = totalSeconds / 60; + const auto totalHours = totalMinutes / 60; + + const auto partSeconds = totalSeconds % 60; + const auto partMinutes = totalMinutes % 60; + const auto partHours = totalHours % 24; + const auto partDays = totalHours / 24; + + static char buffer[20]; + snprintf(buffer, sizeof buffer, "%ldd %2ldh %2ldm %2lds", partDays, partHours, partMinutes, partSeconds); + 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/patrix/core/clock.h b/src/patrix/core/clock.h new file mode 100644 index 0000000..0c7ecb6 --- /dev/null +++ b/src/patrix/core/clock.h @@ -0,0 +1,24 @@ +#ifndef PATRIX_CLOCK_H +#define PATRIX_CLOCK_H + +#include + +void clockLoop(); + +bool isClockSet(); + +char *getClockStr(time_t epoch = 0); + +char *getStartupStr(); + +time_t getUptimeSeconds(); + +char *getUptimeStr(); + +bool isCorrectTime(time_t now); + +time_t clockCorrect(time_t t); + +void clockCorrect(time_t *t); + +#endif diff --git a/src/patrix/core/http.cpp b/src/patrix/core/http.cpp new file mode 100644 index 0000000..8d8394f --- /dev/null +++ b/src/patrix/core/http.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include + +AsyncWebServer server(80); + +AsyncWebSocket ws("/ws"); + +void httpReboot(AsyncWebServerRequest *request) { + info("Rebooting..."); + request->redirect("/"); + request->client()->close(); + yield(); + delay(500); + + restart(); +} + +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"; + patrixNode.websocketEvent(socket, client, type, arg, message, length); + 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("/reboot", HTTP_GET, httpReboot); + server.begin(); +} + +void httpLoop() { + ws.cleanupClients(); +} + +void websocketSendAll(const String& payload) { + ws.textAll(payload); +} diff --git a/src/patrix/core/http.h b/src/patrix/core/http.h new file mode 100644 index 0000000..fdedc67 --- /dev/null +++ b/src/patrix/core/http.h @@ -0,0 +1,12 @@ +#ifndef PATRIX_HTTP_H +#define PATRIX_HTTP_H + +#include + +void httpSetup(); + +void httpLoop(); + +void websocketSendAll(const String& payload); + +#endif diff --git a/src/patrix/core/log.cpp b/src/patrix/core/log.cpp new file mode 100644 index 0000000..1a5fa1c --- /dev/null +++ b/src/patrix/core/log.cpp @@ -0,0 +1,75 @@ +#include +#include + +void doLog(LogLevel level, const char *format, va_list args); + +auto logLevel = DEBUG; + +void logSetup() { + delay(500); + Serial.begin(115200); + info("Startup"); +} + +void log(const LogLevel level, const char *format, ...) { + va_list args; + va_start(args, format); + doLog(level, format, args); + va_end(args); +} + +void error(const char *format, ...) { + va_list args; + va_start(args, format); + doLog(ERROR, format, args); + va_end(args); +} + +void warn(const char *format, ...) { + va_list args; + va_start(args, format); + doLog(WARN, format, args); + va_end(args); +} + +void info(const char *format, ...) { + va_list args; + va_start(args, format); + doLog(INFO, format, args); + va_end(args); +} + +void debug(const char *format, ...) { + va_list args; + va_start(args, format); + doLog(DEBUG, format, args); + va_end(args); +} + +void doLog(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(); +} diff --git a/src/patrix/core/log.h b/src/patrix/core/log.h new file mode 100644 index 0000000..b23c27e --- /dev/null +++ b/src/patrix/core/log.h @@ -0,0 +1,23 @@ +#ifndef PATRIX_LOG_H +#define PATRIX_LOG_H + +enum LogLevel { + ERROR = 0, + WARN = 1, + INFO = 2, + DEBUG = 3 +}; + +void logSetup(); + +void error(const char *format, ...); + +void warn(const char *format, ...); + +void info(const char *format, ...); + +void debug(const char *format, ...); + +void log(LogLevel level, const char *format, ...); + +#endif diff --git a/src/patrix/core/mqtt.cpp b/src/patrix/core/mqtt.cpp new file mode 100644 index 0000000..67b40b1 --- /dev/null +++ b/src/patrix/core/mqtt.cpp @@ -0,0 +1,49 @@ +#include +#include +#include +#include +#include + +#define MQTT_RETRY_DELAY_MILLIS 3000 + +WiFiClient wifiClient; + +PubSubClient mqtt(wifiClient); + +auto mqttHost = "10.0.0.50"; + +auto mqttPort = 1883; + +auto mqttLastConnectMillis = 0UL; + +char mqttWillTopic[128]; + +void mqttLoop() { + if (mqtt.loop()) { + return; + } + if (!isWiFiConnected()) { + mqttLastConnectMillis = 0; + return; + } + if (mqttLastConnectMillis == 0 || millis() - mqttLastConnectMillis > MQTT_RETRY_DELAY_MILLIS) { + snprintf(mqttWillTopic, sizeof(mqttWillTopic), "%s/online", WiFiClass::getHostname()); + mqtt.setServer(mqttHost, mqttPort); + mqtt.setKeepAlive(10); + mqtt.setBufferSize(512); + mqttLastConnectMillis = millis(); + info("mqtt connecting: host=%s, port=%d", mqttHost, mqttPort); + if (mqtt.connect(WiFiClass::getHostname(), mqttWillTopic, 1, true, "false")) { + info("mqtt connected"); + yield(); + mqtt.publish(mqttWillTopic, "true", true); + yield(); + } else { + error("mqtt failed to connect"); + } + } +} + +bool mqttPublish(const String& topic, const String& payload, const Retain retain) { + return mqtt.publish(topic.c_str(), payload.c_str(), retain == RETAIN); +} diff --git a/src/patrix/core/mqtt.h b/src/patrix/core/mqtt.h new file mode 100644 index 0000000..dc7920d --- /dev/null +++ b/src/patrix/core/mqtt.h @@ -0,0 +1,14 @@ +#ifndef PATRIX_MQTT_H +#define PATRIX_MQTT_H + +#include + +enum Retain { + NO_RETAIN, RETAIN +}; + +void mqttLoop(); + +bool mqttPublish(const String& topic, const String& payload, Retain retain); + +#endif diff --git a/src/patrix/core/system.cpp b/src/patrix/core/system.cpp new file mode 100644 index 0000000..40ca26c --- /dev/null +++ b/src/patrix/core/system.cpp @@ -0,0 +1,8 @@ +#include +#include +#include + +void restart() { + wifiOff(); + ESP.restart(); +} diff --git a/src/patrix/core/system.h b/src/patrix/core/system.h new file mode 100644 index 0000000..639478f --- /dev/null +++ b/src/patrix/core/system.h @@ -0,0 +1,6 @@ +#ifndef PATRIX_SYSTEM_H +#define PATRIX_SYSTEM_H + +void restart(); + +#endif diff --git a/src/patrix/core/wifi.cpp b/src/patrix/core/wifi.cpp new file mode 100644 index 0000000..cb4228d --- /dev/null +++ b/src/patrix/core/wifi.cpp @@ -0,0 +1,92 @@ +#include +#include +#include + +#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; + WiFi.disconnect(); +} + +void wifiLoop() { + const auto currentState = WiFi.localIP() != 0; + if (wifiConnected != currentState) { + wifiConnected = currentState; + if (wifiConnected) { + info("wifi connected as %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/patrix/core/wifi.h b/src/patrix/core/wifi.h new file mode 100644 index 0000000..fac3831 --- /dev/null +++ b/src/patrix/core/wifi.h @@ -0,0 +1,10 @@ +#ifndef PATRIX_WIFI_H +#define PATRIX_WIFI_H + +void wifiOff(); + +void wifiLoop(); + +bool isWiFiConnected(); + +#endif diff --git a/src/patrix/node/Node.cpp b/src/patrix/node/Node.cpp new file mode 100644 index 0000000..d899cf7 --- /dev/null +++ b/src/patrix/node/Node.cpp @@ -0,0 +1,4 @@ +#include + +// ReSharper disable once CppUseAuto +Node patrixNode = patrixGetNode(); diff --git a/src/patrix/node/Node.h b/src/patrix/node/Node.h new file mode 100644 index 0000000..d010788 --- /dev/null +++ b/src/patrix/node/Node.h @@ -0,0 +1,37 @@ +#ifndef NODE_H +#define NODE_H + +#include + +class Node { + +public: + + const bool waitForWiFi; + + const bool waitForOTA; + + const bool waitForClock; + + explicit Node(const bool waitForWiFi, const bool waitForOTA, const bool waitForClock) + : waitForWiFi(waitForWiFi), + waitForOTA(waitForOTA), + waitForClock(waitForClock) { + // + } + + virtual ~Node() = default; + + virtual void setup() {} + + virtual void loop() {} + + virtual void websocketEvent(AsyncWebSocket *socket, AsyncWebSocketClient *client, AwsEventType type, void *arg, unsigned char *message, unsigned length) {} + +}; + +extern Node patrixNode; + +Node& patrixGetNode(); + +#endif