boot, clock, http, log, mqtt, system, wifi, Node, demo, NodeTest

This commit is contained in:
Patrick Haßel 2025-01-22 14:00:13 +01:00
commit a6fb6af1a9
24 changed files with 757 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/.pio/
/.idea/
/src/patrix/library.json
/*.tar*

29
library.json Normal file
View File

@ -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"
]
}

9
platformio.ini Normal file
View File

@ -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\"

21
src/demo/NodeTest.h Normal file
View File

@ -0,0 +1,21 @@
#ifndef NODE_TEST_H
#define NODE_TEST_H
#include <public/node/Node.h>
#include <patrix/node/Node.h>
class NodeTest final : public Node {
public:
explicit NodeTest() : Node(true, true, true) {
//
}
void setup() override {
display.printf("Test");
}
};
#endif

7
src/demo/main.cpp Normal file
View File

@ -0,0 +1,7 @@
#include "NodeTest.h"
auto node = NodeTest();
Node &patrixGetNode() {
return node;
}

47
src/library.json Normal file
View File

@ -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"
]
}

16
src/patrix/Patrix.cpp Normal file
View File

@ -0,0 +1,16 @@
#include <patrix/Patrix.h>
void setup() {
logSetup();
bootDelay();
patrixNode.setup();
httpSetup();
}
void loop() {
wifiLoop();
clockLoop();
mqttLoop();
patrixNode.loop();
httpLoop();
}

17
src/patrix/Patrix.h Normal file
View File

@ -0,0 +1,17 @@
#ifndef PATRIX_H
#define PATRIX_H
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <PubSubClient.h>
#include <patrix/core/boot.h>
#include <patrix/core/clock.h>
#include <patrix/core/http.h>
#include <patrix/core/log.h>
#include <patrix/core/mqtt.h>
#include <patrix/core/system.h>
#include <patrix/core/wifi.h>
#include <patrix/node/Node.h>
#endif

95
src/patrix/core/boot.cpp Normal file
View File

@ -0,0 +1,95 @@
#include <patrix/core/boot.h>
#include <patrix/core/clock.h>
#include <patrix/core/log.h>
#include <patrix/core/wifi.h>
#include <patrix/node/Node.h>
#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.");
}

6
src/patrix/core/boot.h Normal file
View File

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

94
src/patrix/core/clock.cpp Normal file
View File

@ -0,0 +1,94 @@
#include <WiFi.h>
#include <patrix/core/clock.h>
#include <patrix/core/log.h>
#include <patrix/core/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, 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;
}

24
src/patrix/core/clock.h Normal file
View File

@ -0,0 +1,24 @@
#ifndef PATRIX_CLOCK_H
#define PATRIX_CLOCK_H
#include <Arduino.h>
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

58
src/patrix/core/http.cpp Normal file
View File

@ -0,0 +1,58 @@
#include <ESPAsyncWebServer.h>
#include <patrix/core/log.h>
#include <patrix/core/system.h>
#include <patrix/node/Node.h>
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);
}

12
src/patrix/core/http.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef PATRIX_HTTP_H
#define PATRIX_HTTP_H
#include <Arduino.h>
void httpSetup();
void httpLoop();
void websocketSendAll(const String& payload);
#endif

75
src/patrix/core/log.cpp Normal file
View File

@ -0,0 +1,75 @@
#include <patrix/core/clock.h>
#include <patrix/core/log.h>
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();
}

23
src/patrix/core/log.h Normal file
View File

@ -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

49
src/patrix/core/mqtt.cpp Normal file
View File

@ -0,0 +1,49 @@
#include <PubSubClient.h>
#include <WiFi.h>
#include <patrix/core/log.h>
#include <patrix/core/mqtt.h>
#include <patrix/core/wifi.h>
#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);
}

14
src/patrix/core/mqtt.h Normal file
View File

@ -0,0 +1,14 @@
#ifndef PATRIX_MQTT_H
#define PATRIX_MQTT_H
#include <Arduino.h>
enum Retain {
NO_RETAIN, RETAIN
};
void mqttLoop();
bool mqttPublish(const String& topic, const String& payload, Retain retain);
#endif

View File

@ -0,0 +1,8 @@
#include <Esp.h>
#include <patrix/core/system.h>
#include <patrix/core/wifi.h>
void restart() {
wifiOff();
ESP.restart();
}

6
src/patrix/core/system.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef PATRIX_SYSTEM_H
#define PATRIX_SYSTEM_H
void restart();
#endif

92
src/patrix/core/wifi.cpp Normal file
View File

@ -0,0 +1,92 @@
#include <ArduinoOTA.h>
#include <patrix/core/log.h>
#include <patrix/core/wifi.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;
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;
}

10
src/patrix/core/wifi.h Normal file
View File

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

4
src/patrix/node/Node.cpp Normal file
View File

@ -0,0 +1,4 @@
#include <patrix/node/Node.h>
// ReSharper disable once CppUseAuto
Node patrixNode = patrixGetNode();

37
src/patrix/node/Node.h Normal file
View File

@ -0,0 +1,37 @@
#ifndef NODE_H
#define NODE_H
#include <ESPAsyncWebServer.h>
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