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:
2026-04-06 13:11:15 +02:00
co-authored by Claude Sonnet 4.6
parent a50ef96eb7
commit a52f164299
6 changed files with 892 additions and 0 deletions
+175
View 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
}