feat: add ESP32 firmware with WiFi, API fetch, e-paper rendering, and captive portal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a50ef96eb7
commit
a52f164299
127
display/esp32/include/captive_portal.h
Normal file
127
display/esp32/include/captive_portal.h
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include <DNSServer.h>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
static const char CAPTIVE_PORTAL_HTML[] PROGMEM = R"rawhtml(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>EPaper Setup</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; max-width: 480px; margin: 40px auto; padding: 16px; }
|
||||||
|
h1 { font-size: 1.4em; }
|
||||||
|
label { display: block; margin-top: 12px; font-weight: bold; }
|
||||||
|
input { width: 100%; padding: 8px; box-sizing: border-box; margin-top: 4px; }
|
||||||
|
button { margin-top: 20px; padding: 10px 24px; background: #0078d4; color: #fff;
|
||||||
|
border: none; border-radius: 4px; font-size: 1em; cursor: pointer; }
|
||||||
|
button:hover { background: #005a9e; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>EPaper Display Setup</h1>
|
||||||
|
<form method="POST" action="/save">
|
||||||
|
<label for="ssid">WiFi SSID</label>
|
||||||
|
<input id="ssid" name="ssid" type="text" placeholder="Netzwerkname" required>
|
||||||
|
|
||||||
|
<label for="pass">WiFi Password</label>
|
||||||
|
<input id="pass" name="pass" type="password" placeholder="Passwort (leer lassen wenn kein Passwort)">
|
||||||
|
|
||||||
|
<label for="api_url">API URL</label>
|
||||||
|
<input id="api_url" name="api_url" type="url" placeholder="http://192.168.1.100:3000" required>
|
||||||
|
|
||||||
|
<label for="display_id">Display ID</label>
|
||||||
|
<input id="display_id" name="display_id" type="text" placeholder="1" value="1">
|
||||||
|
|
||||||
|
<label for="api_token">API Token (optional)</label>
|
||||||
|
<input id="api_token" name="api_token" type="text" placeholder="Bearer Token">
|
||||||
|
|
||||||
|
<button type="submit">Speichern & Neustart</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)rawhtml";
|
||||||
|
|
||||||
|
static const char CAPTIVE_PORTAL_SAVED_HTML[] PROGMEM = R"rawhtml(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Gespeichert</title>
|
||||||
|
<style>body{font-family:Arial,sans-serif;text-align:center;padding:40px;}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Konfiguration gespeichert!</h1>
|
||||||
|
<p>Das Gerät startet jetzt neu...</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)rawhtml";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a captive WiFi access point for initial configuration.
|
||||||
|
* Creates AP "EPaper-Setup", serves a config form on port 80.
|
||||||
|
* POST /save persists the config and reboots.
|
||||||
|
* This function never returns.
|
||||||
|
*/
|
||||||
|
inline void startCaptivePortal(Config& config) {
|
||||||
|
const byte DNS_PORT = 53;
|
||||||
|
DNSServer dnsServer;
|
||||||
|
WebServer server(80);
|
||||||
|
|
||||||
|
// ── Start AP ──────────────────────────────────────────────────────────────
|
||||||
|
WiFi.mode(WIFI_AP);
|
||||||
|
WiFi.softAP("EPaper-Setup"); // open network, no password
|
||||||
|
|
||||||
|
IPAddress apIP = WiFi.softAPIP();
|
||||||
|
Serial.print("[Portal] AP started, IP: ");
|
||||||
|
Serial.println(apIP);
|
||||||
|
|
||||||
|
// ── DNS: redirect all queries to us ───────────────────────────────────────
|
||||||
|
dnsServer.start(DNS_PORT, "*", apIP);
|
||||||
|
|
||||||
|
// ── Routes ────────────────────────────────────────────────────────────────
|
||||||
|
server.on("/", HTTP_GET, [&server]() {
|
||||||
|
server.send_P(200, "text/html", CAPTIVE_PORTAL_HTML);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("/save", HTTP_POST, [&server, &config]() {
|
||||||
|
if (server.hasArg("ssid") && server.hasArg("api_url")) {
|
||||||
|
config.wifi_ssid = server.arg("ssid");
|
||||||
|
config.wifi_password = server.arg("pass");
|
||||||
|
config.api_url = server.arg("api_url");
|
||||||
|
config.display_id = server.arg("display_id").length() > 0
|
||||||
|
? server.arg("display_id")
|
||||||
|
: "1";
|
||||||
|
config.api_token = server.arg("api_token");
|
||||||
|
config.save();
|
||||||
|
|
||||||
|
server.send_P(200, "text/html", CAPTIVE_PORTAL_SAVED_HTML);
|
||||||
|
delay(2000);
|
||||||
|
ESP.restart();
|
||||||
|
} else {
|
||||||
|
server.send(400, "text/plain", "Fehlende Felder: ssid und api_url sind Pflichtfelder.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Captive portal redirect (all unknown URLs → /) ────────────────────────
|
||||||
|
server.onNotFound([&server, &apIP]() {
|
||||||
|
// Some OSes check a specific URL; redirect them all to our form
|
||||||
|
server.sendHeader("Location", String("http://") + apIP.toString() + "/", true);
|
||||||
|
server.send(302, "text/plain", "");
|
||||||
|
});
|
||||||
|
|
||||||
|
server.begin();
|
||||||
|
Serial.println("[Portal] HTTP server started");
|
||||||
|
|
||||||
|
// ── Main loop (blocks forever) ────────────────────────────────────────────
|
||||||
|
for (;;) {
|
||||||
|
dnsServer.processNextRequest();
|
||||||
|
server.handleClient();
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
41
display/esp32/include/config.h
Normal file
41
display/esp32/include/config.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
String wifi_ssid;
|
||||||
|
String wifi_password;
|
||||||
|
String api_url;
|
||||||
|
String display_id;
|
||||||
|
String api_token;
|
||||||
|
int refresh_sec;
|
||||||
|
|
||||||
|
void load() {
|
||||||
|
Preferences prefs;
|
||||||
|
prefs.begin("epaper", true); // read-only
|
||||||
|
wifi_ssid = prefs.getString("wifi_ssid", "");
|
||||||
|
wifi_password = prefs.getString("wifi_password", "");
|
||||||
|
api_url = prefs.getString("api_url", "");
|
||||||
|
display_id = prefs.getString("display_id", "1");
|
||||||
|
api_token = prefs.getString("api_token", "");
|
||||||
|
refresh_sec = prefs.getInt ("refresh_sec", 300);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void save() const {
|
||||||
|
Preferences prefs;
|
||||||
|
prefs.begin("epaper", false); // read-write
|
||||||
|
prefs.putString("wifi_ssid", wifi_ssid);
|
||||||
|
prefs.putString("wifi_password", wifi_password);
|
||||||
|
prefs.putString("api_url", api_url);
|
||||||
|
prefs.putString("display_id", display_id);
|
||||||
|
prefs.putString("api_token", api_token);
|
||||||
|
prefs.putInt ("refresh_sec", refresh_sec);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isConfigured() const {
|
||||||
|
return wifi_ssid.length() > 0 && api_url.length() > 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
385
display/esp32/include/icons.h
Normal file
385
display/esp32/include/icons.h
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <pgmspace.h>
|
||||||
|
|
||||||
|
// 24x24 monochrome bitmaps, 1 bit per pixel, stored row-major (24 bits = 3 bytes per row, 24 rows = 72 bytes each)
|
||||||
|
// Pixels are MSB-first. A 0 bit = black, 1 bit = white (GxEPD2 convention for drawBitmap).
|
||||||
|
// For use with display.drawBitmap(x, y, data, 24, 24, GxEPD_BLACK)
|
||||||
|
|
||||||
|
// ── Thermometer ─────────────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_THERMOMETER[] PROGMEM = {
|
||||||
|
0xFF, 0xE7, 0xFF, // row 0
|
||||||
|
0xFF, 0xC3, 0xFF, // row 1
|
||||||
|
0xFF, 0xC3, 0xFF, // row 2
|
||||||
|
0xFF, 0xDB, 0xFF, // row 3
|
||||||
|
0xFF, 0xDB, 0xFF, // row 4
|
||||||
|
0xFF, 0xDB, 0xFF, // row 5
|
||||||
|
0xFF, 0xDB, 0xFF, // row 6
|
||||||
|
0xFF, 0xDB, 0xFF, // row 7
|
||||||
|
0xFF, 0xDB, 0xFF, // row 8
|
||||||
|
0xFF, 0xDB, 0xFF, // row 9
|
||||||
|
0xFF, 0xDB, 0xFF, // row 10
|
||||||
|
0xFF, 0xDB, 0xFF, // row 11
|
||||||
|
0xFF, 0xDB, 0xFF, // row 12
|
||||||
|
0xFF, 0xDB, 0xFF, // row 13
|
||||||
|
0xFF, 0xDB, 0xFF, // row 14
|
||||||
|
0xFE, 0x01, 0x7F, // row 15
|
||||||
|
0xFC, 0x00, 0x3F, // row 16
|
||||||
|
0xFC, 0x00, 0x3F, // row 17
|
||||||
|
0xFC, 0x00, 0x3F, // row 18
|
||||||
|
0xFC, 0x00, 0x3F, // row 19
|
||||||
|
0xFE, 0x01, 0x7F, // row 20
|
||||||
|
0xFF, 0x03, 0xFF, // row 21
|
||||||
|
0xFF, 0x03, 0xFF, // row 22
|
||||||
|
0xFF, 0xE7, 0xFF, // row 23
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Droplet / Humidity ───────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_DROPLET[] PROGMEM = {
|
||||||
|
0xFF, 0xFF, 0xFF, // row 0
|
||||||
|
0xFF, 0xE7, 0xFF, // row 1
|
||||||
|
0xFF, 0xC3, 0xFF, // row 2
|
||||||
|
0xFF, 0x81, 0xFF, // row 3
|
||||||
|
0xFF, 0x00, 0xFF, // row 4
|
||||||
|
0xFE, 0x00, 0x7F, // row 5
|
||||||
|
0xFE, 0x00, 0x7F, // row 6
|
||||||
|
0xFC, 0x00, 0x3F, // row 7
|
||||||
|
0xFC, 0x00, 0x3F, // row 8
|
||||||
|
0xFC, 0x00, 0x3F, // row 9
|
||||||
|
0xFC, 0x00, 0x3F, // row 10
|
||||||
|
0xFC, 0x00, 0x3F, // row 11
|
||||||
|
0xFC, 0x00, 0x3F, // row 12
|
||||||
|
0xFE, 0x00, 0x7F, // row 13
|
||||||
|
0xFE, 0x00, 0x7F, // row 14
|
||||||
|
0xFF, 0x00, 0xFF, // row 15
|
||||||
|
0xFF, 0x81, 0xFF, // row 16
|
||||||
|
0xFF, 0xC3, 0xFF, // row 17
|
||||||
|
0xFF, 0xFF, 0xFF, // row 18
|
||||||
|
0xFF, 0xFF, 0xFF, // row 19
|
||||||
|
0xFF, 0xFF, 0xFF, // row 20
|
||||||
|
0xFF, 0xFF, 0xFF, // row 21
|
||||||
|
0xFF, 0xFF, 0xFF, // row 22
|
||||||
|
0xFF, 0xFF, 0xFF, // row 23
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Wind ─────────────────────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_WIND[] PROGMEM = {
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xC0, 0x00, 0xFF,
|
||||||
|
0x9F, 0xFF, 0x7F,
|
||||||
|
0xFF, 0xFE, 0x7F,
|
||||||
|
0x80, 0x00, 0x7F,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0x80, 0x00, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Pressure / Barometer ─────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_PRESSURE[] PROGMEM = {
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xFE, 0x00, 0x7F,
|
||||||
|
0xFC, 0x00, 0x3F,
|
||||||
|
0xF8, 0x00, 0x1F,
|
||||||
|
0xF3, 0xFF, 0xCF,
|
||||||
|
0xE7, 0xFF, 0xE7,
|
||||||
|
0xEF, 0xFF, 0xF7,
|
||||||
|
0xEF, 0x03, 0xF7,
|
||||||
|
0xEF, 0xFD, 0xF7,
|
||||||
|
0xEF, 0xFD, 0xF7,
|
||||||
|
0xEF, 0xFF, 0xF7,
|
||||||
|
0xE7, 0xFF, 0xE7,
|
||||||
|
0xF3, 0xFF, 0xCF,
|
||||||
|
0xF8, 0x00, 0x1F,
|
||||||
|
0xFC, 0x00, 0x3F,
|
||||||
|
0xFE, 0x00, 0x7F,
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Sun ──────────────────────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_SUN[] PROGMEM = {
|
||||||
|
0xFF, 0xE7, 0xFF,
|
||||||
|
0xFF, 0xE7, 0xFF,
|
||||||
|
0xFB, 0xE7, 0xDF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xCF, 0x00, 0xF3,
|
||||||
|
0xFE, 0x00, 0x7F,
|
||||||
|
0xFC, 0x00, 0x3F,
|
||||||
|
0x80, 0x00, 0x01,
|
||||||
|
0x80, 0x00, 0x01,
|
||||||
|
0xFC, 0x00, 0x3F,
|
||||||
|
0xFE, 0x00, 0x7F,
|
||||||
|
0xCF, 0x00, 0xF3,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFB, 0xE7, 0xDF,
|
||||||
|
0xFF, 0xE7, 0xFF,
|
||||||
|
0xFF, 0xE7, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Bolt / Lightning ─────────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_BOLT[] PROGMEM = {
|
||||||
|
0xFF, 0x87, 0xFF,
|
||||||
|
0xFF, 0x0F, 0xFF,
|
||||||
|
0xFE, 0x1F, 0xFF,
|
||||||
|
0xFC, 0x3F, 0xFF,
|
||||||
|
0xF8, 0x00, 0xFF,
|
||||||
|
0xF0, 0x01, 0xFF,
|
||||||
|
0xE0, 0x03, 0xFF,
|
||||||
|
0xC0, 0x07, 0xFF,
|
||||||
|
0xFF, 0x80, 0x1F,
|
||||||
|
0xFF, 0x00, 0x3F,
|
||||||
|
0xFE, 0x01, 0x7F,
|
||||||
|
0xFC, 0x03, 0xFF,
|
||||||
|
0xF8, 0x07, 0xFF,
|
||||||
|
0xF0, 0x0F, 0xFF,
|
||||||
|
0xFF, 0x1F, 0xFF,
|
||||||
|
0xFF, 0x3F, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Gauge / Speedometer ───────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_GAUGE[] PROGMEM = {
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xFE, 0x00, 0x7F,
|
||||||
|
0xF8, 0x00, 0x1F,
|
||||||
|
0xF3, 0xFF, 0xCF,
|
||||||
|
0xEF, 0xFF, 0xF7,
|
||||||
|
0xEF, 0x07, 0xF7,
|
||||||
|
0xEF, 0x03, 0xF7,
|
||||||
|
0xEF, 0x81, 0xF7,
|
||||||
|
0xEF, 0xC0, 0xF7,
|
||||||
|
0xEF, 0xFF, 0xF7,
|
||||||
|
0xF3, 0xFF, 0xCF,
|
||||||
|
0xF8, 0x00, 0x1F,
|
||||||
|
0xFE, 0x00, 0x7F,
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── WiFi ─────────────────────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_WIFI[] PROGMEM = {
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xC0, 0x00, 0x03,
|
||||||
|
0x3F, 0xFF, 0xFC,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0x00, 0xFF,
|
||||||
|
0xFC, 0x00, 0x3F,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xE7, 0xFF,
|
||||||
|
0xFF, 0x81, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Battery ───────────────────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_BATTERY[] PROGMEM = {
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xE0, 0x00, 0x07,
|
||||||
|
0xDF, 0xFF, 0xFB,
|
||||||
|
0xDF, 0x00, 0xFB,
|
||||||
|
0xDF, 0x00, 0xFB,
|
||||||
|
0xDF, 0x00, 0xFB,
|
||||||
|
0xDF, 0xFF, 0xFB,
|
||||||
|
0xE0, 0x00, 0x07,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Check / Checkmark ────────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_CHECK[] PROGMEM = {
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xF9,
|
||||||
|
0xFF, 0xFF, 0xF3,
|
||||||
|
0xFF, 0xFF, 0xE7,
|
||||||
|
0xFF, 0xFF, 0xCF,
|
||||||
|
0xFF, 0xFF, 0x9F,
|
||||||
|
0xE7, 0xFF, 0x3F,
|
||||||
|
0xCF, 0xFE, 0x7F,
|
||||||
|
0x9F, 0xFC, 0xFF,
|
||||||
|
0x3F, 0xF9, 0xFF,
|
||||||
|
0x7F, 0xF3, 0xFF,
|
||||||
|
0xFF, 0xE7, 0xFF,
|
||||||
|
0xFF, 0xCF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Warning / Exclamation ─────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_WARNING[] PROGMEM = {
|
||||||
|
0xFF, 0xE7, 0xFF,
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xFF, 0x81, 0xFF,
|
||||||
|
0xFF, 0x81, 0xFF,
|
||||||
|
0xFF, 0x00, 0xFF,
|
||||||
|
0xFE, 0x18, 0x7F,
|
||||||
|
0xFE, 0x18, 0x7F,
|
||||||
|
0xFE, 0x18, 0x7F,
|
||||||
|
0xFC, 0x18, 0x3F,
|
||||||
|
0xFC, 0x18, 0x3F,
|
||||||
|
0xFC, 0x18, 0x3F,
|
||||||
|
0xFC, 0x00, 0x3F,
|
||||||
|
0xFC, 0x18, 0x3F,
|
||||||
|
0xFC, 0x18, 0x3F,
|
||||||
|
0xFE, 0x18, 0x7F,
|
||||||
|
0xFF, 0x00, 0xFF,
|
||||||
|
0xFF, 0x81, 0xFF,
|
||||||
|
0xFF, 0x81, 0xFF,
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xFF, 0xE7, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Clock ─────────────────────────────────────────────────────────────────────
|
||||||
|
static const unsigned char ICON_CLOCK[] PROGMEM = {
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xFE, 0x00, 0x7F,
|
||||||
|
0xFC, 0x00, 0x3F,
|
||||||
|
0xF8, 0x00, 0x1F,
|
||||||
|
0xF3, 0xFF, 0xCF,
|
||||||
|
0xE7, 0xE7, 0xE7,
|
||||||
|
0xEF, 0xE7, 0xF7,
|
||||||
|
0xEF, 0xE7, 0xF7,
|
||||||
|
0xEF, 0xE0, 0xF7,
|
||||||
|
0xEF, 0xE0, 0x07,
|
||||||
|
0xEF, 0xFF, 0xF7,
|
||||||
|
0xE7, 0xFF, 0xE7,
|
||||||
|
0xF3, 0xFF, 0xCF,
|
||||||
|
0xF8, 0x00, 0x1F,
|
||||||
|
0xFC, 0x00, 0x3F,
|
||||||
|
0xFE, 0x00, 0x7F,
|
||||||
|
0xFF, 0xC3, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Icon registry ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct IconEntry {
|
||||||
|
const char* name;
|
||||||
|
const unsigned char* data;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const IconEntry ICON_TABLE[] = {
|
||||||
|
{ "thermometer", ICON_THERMOMETER },
|
||||||
|
{ "droplet", ICON_DROPLET },
|
||||||
|
{ "wind", ICON_WIND },
|
||||||
|
{ "pressure", ICON_PRESSURE },
|
||||||
|
{ "sun", ICON_SUN },
|
||||||
|
{ "bolt", ICON_BOLT },
|
||||||
|
{ "gauge", ICON_GAUGE },
|
||||||
|
{ "wifi", ICON_WIFI },
|
||||||
|
{ "battery", ICON_BATTERY },
|
||||||
|
{ "check", ICON_CHECK },
|
||||||
|
{ "warning", ICON_WARNING },
|
||||||
|
{ "clock", ICON_CLOCK },
|
||||||
|
};
|
||||||
|
|
||||||
|
static const size_t ICON_TABLE_SIZE = sizeof(ICON_TABLE) / sizeof(ICON_TABLE[0]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up an icon bitmap by name.
|
||||||
|
* Returns nullptr if the name is not found.
|
||||||
|
*/
|
||||||
|
inline const unsigned char* getIcon(const char* name) {
|
||||||
|
for (size_t i = 0; i < ICON_TABLE_SIZE; ++i) {
|
||||||
|
if (strcmp(name, ICON_TABLE[i].name) == 0) {
|
||||||
|
return ICON_TABLE[i].data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const unsigned char* getIcon(const String& name) {
|
||||||
|
return getIcon(name.c_str());
|
||||||
|
}
|
||||||
152
display/esp32/include/renderer.h
Normal file
152
display/esp32/include/renderer.h
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <GxEPD2_BW.h>
|
||||||
|
#include "icons.h"
|
||||||
|
|
||||||
|
// ── Font size helper ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map an API font_size value (typically 8–40) to GFX setTextSize(1–5).
|
||||||
|
* font_size / 8, clamped to [1, 5].
|
||||||
|
*/
|
||||||
|
static inline uint8_t mapFontSize(int fontSize) {
|
||||||
|
int s = fontSize / 8;
|
||||||
|
if (s < 1) s = 1;
|
||||||
|
if (s > 5) s = 5;
|
||||||
|
return static_cast<uint8_t>(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Render all elements from JSON array ───────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates every element in the JsonArray and draws it onto `display`.
|
||||||
|
* Must be called inside a firstPage/nextPage loop so that display is
|
||||||
|
* already prepared for drawing.
|
||||||
|
*/
|
||||||
|
template <typename GxEPD2_Type>
|
||||||
|
void renderElements(GxEPD2_Type& display, JsonArray elements) {
|
||||||
|
for (JsonObject el : elements) {
|
||||||
|
const char* type = el["type"] | "";
|
||||||
|
int x = el["x"] | 0;
|
||||||
|
int y = el["y"] | 0;
|
||||||
|
int w = el["w"] | 0;
|
||||||
|
int h = el["h"] | 0;
|
||||||
|
|
||||||
|
// ── line ─────────────────────────────────────────────────────────────
|
||||||
|
if (strcmp(type, "line") == 0) {
|
||||||
|
// Use fillRect: height==1 → horizontal, width==1 → vertical
|
||||||
|
int thickness = el["thickness"] | 1;
|
||||||
|
if (h <= 1 && w > 1) {
|
||||||
|
// horizontal line
|
||||||
|
display.fillRect(x, y, w, (thickness > 0 ? thickness : 1), GxEPD_BLACK);
|
||||||
|
} else if (w <= 1 && h > 1) {
|
||||||
|
// vertical line
|
||||||
|
display.fillRect(x, y, (thickness > 0 ? thickness : 1), h, GxEPD_BLACK);
|
||||||
|
} else {
|
||||||
|
// generic: draw as thin rect
|
||||||
|
display.fillRect(x, y, w, h, GxEPD_BLACK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ── rect ─────────────────────────────────────────────────────────────
|
||||||
|
else if (strcmp(type, "rect") == 0) {
|
||||||
|
display.drawRect(x, y, w, h, GxEPD_BLACK);
|
||||||
|
}
|
||||||
|
// ── label ────────────────────────────────────────────────────────────
|
||||||
|
else if (strcmp(type, "label") == 0) {
|
||||||
|
const char* text = el["text"] | "";
|
||||||
|
int fontSize = el["font_size"] | 8;
|
||||||
|
const char* textAlign = el["text_align"] | "left";
|
||||||
|
uint8_t sz = mapFontSize(fontSize);
|
||||||
|
|
||||||
|
display.setTextColor(GxEPD_BLACK);
|
||||||
|
display.setTextSize(sz);
|
||||||
|
|
||||||
|
if (strcmp(textAlign, "center") == 0) {
|
||||||
|
// Calculate approximate text width: each char is 6px wide at size 1
|
||||||
|
int charWidth = 6 * sz;
|
||||||
|
int textLen = strlen(text);
|
||||||
|
int textWidth = charWidth * textLen;
|
||||||
|
int textHeight = 8 * sz;
|
||||||
|
display.setCursor(x - textWidth / 2, y - textHeight / 2);
|
||||||
|
} else if (strcmp(textAlign, "right") == 0) {
|
||||||
|
int charWidth = 6 * sz;
|
||||||
|
int textLen = strlen(text);
|
||||||
|
int textWidth = charWidth * textLen;
|
||||||
|
display.setCursor(x - textWidth, y);
|
||||||
|
} else {
|
||||||
|
// left (default)
|
||||||
|
display.setCursor(x, y);
|
||||||
|
}
|
||||||
|
display.print(text);
|
||||||
|
}
|
||||||
|
// ── value ─────────────────────────────────────────────────────────────
|
||||||
|
else if (strcmp(type, "value") == 0) {
|
||||||
|
const char* style = el["style"] | "plain";
|
||||||
|
const char* label = el["label"] | "";
|
||||||
|
const char* value = el["value"] | "";
|
||||||
|
const char* unit = el["unit"] | "";
|
||||||
|
const char* iconName = el["icon"] | "";
|
||||||
|
int fontSize = el["font_size"] | 16;
|
||||||
|
|
||||||
|
if (strcmp(style, "card") == 0) {
|
||||||
|
// ── card style ─────────────────────────────────────────────
|
||||||
|
// Border
|
||||||
|
display.drawRoundRect(x, y, w, h, 4, GxEPD_BLACK);
|
||||||
|
|
||||||
|
// Icon top-left (24x24), with small padding
|
||||||
|
const unsigned char* iconData = getIcon(iconName);
|
||||||
|
if (iconData != nullptr) {
|
||||||
|
display.drawBitmap(x + 4, y + 4, iconData, 24, 24, GxEPD_BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label text (small, below icon)
|
||||||
|
display.setTextColor(GxEPD_BLACK);
|
||||||
|
display.setTextSize(1);
|
||||||
|
display.setCursor(x + 4, y + 32);
|
||||||
|
display.print(label);
|
||||||
|
|
||||||
|
// Value + unit (large, bottom-right)
|
||||||
|
uint8_t valSize = mapFontSize(fontSize);
|
||||||
|
display.setTextSize(valSize);
|
||||||
|
|
||||||
|
// Build value string with unit
|
||||||
|
String valStr = String(value);
|
||||||
|
if (strlen(unit) > 0) {
|
||||||
|
valStr += " ";
|
||||||
|
valStr += unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-align inside card
|
||||||
|
int charWidth = 6 * valSize;
|
||||||
|
int valWidth = charWidth * valStr.length();
|
||||||
|
int valX = x + w - valWidth - 4;
|
||||||
|
int valY = y + h - 8 * valSize - 4;
|
||||||
|
if (valX < x + 4) valX = x + 4;
|
||||||
|
display.setCursor(valX, valY);
|
||||||
|
display.print(valStr);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// ── plain style ─────────────────────────────────────────────
|
||||||
|
uint8_t sz = mapFontSize(fontSize);
|
||||||
|
display.setTextColor(GxEPD_BLACK);
|
||||||
|
display.setTextSize(sz);
|
||||||
|
display.setCursor(x, y);
|
||||||
|
|
||||||
|
// "Label: value unit"
|
||||||
|
String line;
|
||||||
|
if (strlen(label) > 0) {
|
||||||
|
line += label;
|
||||||
|
line += ": ";
|
||||||
|
}
|
||||||
|
line += value;
|
||||||
|
if (strlen(unit) > 0) {
|
||||||
|
line += " ";
|
||||||
|
line += unit;
|
||||||
|
}
|
||||||
|
display.print(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
display/esp32/platformio.ini
Normal file
12
display/esp32/platformio.ini
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[env:esp32]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
lib_deps =
|
||||||
|
zinggjm/GxEPD2@^1.5.8
|
||||||
|
bblanchon/ArduinoJson@^7.2.0
|
||||||
|
adafruit/Adafruit GFX Library@^1.11.10
|
||||||
|
adafruit/Adafruit BusIO@^1.16.1
|
||||||
|
board_build.partitions = default.csv
|
||||||
|
build_flags = -DBOARD_HAS_PSRAM
|
||||||
175
display/esp32/src/main.cpp
Normal file
175
display/esp32/src/main.cpp
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
// GxEPD2 for Waveshare 7.5" V2 (800x480, GxEPD2_750_T7)
|
||||||
|
#include <GxEPD2_BW.h>
|
||||||
|
#include <GxEPD2_750_T7.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "renderer.h"
|
||||||
|
#include "captive_portal.h"
|
||||||
|
|
||||||
|
// ── Pin mapping for Waveshare ESP32 Driver Board ──────────────────────────────
|
||||||
|
#define EPD_CS 15
|
||||||
|
#define EPD_DC 27
|
||||||
|
#define EPD_RST 26
|
||||||
|
#define EPD_BUSY 25
|
||||||
|
|
||||||
|
// ── Display instance ──────────────────────────────────────────────────────────
|
||||||
|
GxEPD2_BW<GxEPD2_750_T7, GxEPD2_750_T7::HEIGHT> display(
|
||||||
|
GxEPD2_750_T7(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Deep sleep helper ─────────────────────────────────────────────────────────
|
||||||
|
static void goDeepSleep(int seconds) {
|
||||||
|
Serial.printf("[Sleep] Entering deep sleep for %d seconds\n", seconds);
|
||||||
|
display.hibernate();
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
esp_sleep_enable_timer_wakeup((uint64_t)seconds * 1000000ULL);
|
||||||
|
esp_deep_sleep_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── showError: white screen with centered error message ───────────────────────
|
||||||
|
static void showError(const char* message) {
|
||||||
|
Serial.printf("[Error] %s\n", message);
|
||||||
|
display.setFullWindow();
|
||||||
|
display.firstPage();
|
||||||
|
do {
|
||||||
|
display.fillScreen(GxEPD_WHITE);
|
||||||
|
display.setTextColor(GxEPD_BLACK);
|
||||||
|
display.setTextSize(2);
|
||||||
|
|
||||||
|
// Print "FEHLER:" header
|
||||||
|
const char* header = "FEHLER:";
|
||||||
|
int headerW = 6 * 2 * strlen(header);
|
||||||
|
display.setCursor((display.width() - headerW) / 2, display.height() / 2 - 24);
|
||||||
|
display.print(header);
|
||||||
|
|
||||||
|
// Print message below
|
||||||
|
display.setTextSize(1);
|
||||||
|
int msgW = 6 * strlen(message);
|
||||||
|
display.setCursor((display.width() - msgW) / 2, display.height() / 2 + 4);
|
||||||
|
display.print(message);
|
||||||
|
} while (display.nextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── setup ─────────────────────────────────────────────────────────────────────
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.println("[Boot] EPaper Display starting...");
|
||||||
|
|
||||||
|
// 1. Init display
|
||||||
|
display.init(115200);
|
||||||
|
display.setRotation(0);
|
||||||
|
|
||||||
|
// 2. Load config from NVS
|
||||||
|
Config config;
|
||||||
|
config.load();
|
||||||
|
|
||||||
|
// 3. If not configured → captive portal
|
||||||
|
if (!config.isConfigured()) {
|
||||||
|
Serial.println("[Boot] Not configured, starting captive portal");
|
||||||
|
display.setFullWindow();
|
||||||
|
display.firstPage();
|
||||||
|
do {
|
||||||
|
display.fillScreen(GxEPD_WHITE);
|
||||||
|
display.setTextColor(GxEPD_BLACK);
|
||||||
|
display.setTextSize(2);
|
||||||
|
const char* line1 = "Mit 'EPaper-Setup' WLAN verbinden";
|
||||||
|
const char* line2 = "und http://192.168.4.1 oeffnen";
|
||||||
|
int w1 = 6 * 2 * strlen(line1);
|
||||||
|
int w2 = 6 * 2 * strlen(line2);
|
||||||
|
display.setCursor((display.width() - w1) / 2, display.height() / 2 - 20);
|
||||||
|
display.print(line1);
|
||||||
|
display.setCursor((display.width() - w2) / 2, display.height() / 2 + 10);
|
||||||
|
display.print(line2);
|
||||||
|
} while (display.nextPage());
|
||||||
|
|
||||||
|
startCaptivePortal(config); // never returns
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Connect to WiFi (30 attempts, 500 ms each)
|
||||||
|
Serial.printf("[WiFi] Connecting to %s\n", config.wifi_ssid.c_str());
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.begin(config.wifi_ssid.c_str(), config.wifi_password.c_str());
|
||||||
|
|
||||||
|
int attempts = 0;
|
||||||
|
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
|
||||||
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
|
++attempts;
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
showError("WiFi failed");
|
||||||
|
goDeepSleep(60);
|
||||||
|
return; // not reached
|
||||||
|
}
|
||||||
|
Serial.printf("[WiFi] Connected, IP: %s\n", WiFi.localIP().toString().c_str());
|
||||||
|
|
||||||
|
// 5. Build request URL
|
||||||
|
String url = config.api_url + "/api/display/" + config.display_id;
|
||||||
|
if (config.api_token.length() > 0) {
|
||||||
|
url += "?token=" + config.api_token;
|
||||||
|
}
|
||||||
|
Serial.printf("[HTTP] GET %s\n", url.c_str());
|
||||||
|
|
||||||
|
// 6. HTTP GET
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin(url);
|
||||||
|
http.setTimeout(10000);
|
||||||
|
if (config.api_token.length() > 0) {
|
||||||
|
http.addHeader("Authorization", "Bearer " + config.api_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
int httpCode = http.GET();
|
||||||
|
if (httpCode <= 0 || httpCode >= 400) {
|
||||||
|
Serial.printf("[HTTP] Error: %d\n", httpCode);
|
||||||
|
http.end();
|
||||||
|
showError(("HTTP " + String(httpCode)).c_str());
|
||||||
|
goDeepSleep(60);
|
||||||
|
return; // not reached
|
||||||
|
}
|
||||||
|
|
||||||
|
String payload = http.getString();
|
||||||
|
http.end();
|
||||||
|
Serial.printf("[HTTP] Payload length: %d\n", payload.length());
|
||||||
|
|
||||||
|
// 7. Parse JSON (ArduinoJson v7)
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError err = deserializeJson(doc, payload);
|
||||||
|
if (err) {
|
||||||
|
Serial.printf("[JSON] Parse error: %s\n", err.c_str());
|
||||||
|
showError(("JSON: " + String(err.c_str())).c_str());
|
||||||
|
goDeepSleep(60);
|
||||||
|
return; // not reached
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Extract refresh_sec (prefer from JSON, fall back to config)
|
||||||
|
int refreshSec = doc["refresh_sec"] | config.refresh_sec;
|
||||||
|
if (refreshSec < 10) refreshSec = 60; // safety minimum
|
||||||
|
|
||||||
|
// 9. Render elements
|
||||||
|
JsonArray elements = doc["elements"].as<JsonArray>();
|
||||||
|
|
||||||
|
display.setFullWindow();
|
||||||
|
display.firstPage();
|
||||||
|
do {
|
||||||
|
display.fillScreen(GxEPD_WHITE);
|
||||||
|
renderElements(display, elements);
|
||||||
|
} while (display.nextPage());
|
||||||
|
|
||||||
|
Serial.println("[Render] Done");
|
||||||
|
|
||||||
|
// 10. Disconnect WiFi and deep sleep
|
||||||
|
goDeepSleep(refreshSec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── loop ─────────────────────────────────────────────────────────────────────
|
||||||
|
void loop() {
|
||||||
|
// Never reached: setup() ends in deep sleep
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user