Menzelenerheide 26

46519 Alpen

Michael Arthen

Ihr Ansprechpartner

0176/34048100

immer für Sie erreichbar

MQTT-LCD Display mit ESP8266 (Wemos D1 mini)

Dieses Projekt zeigt, wie man mit einem ESP8266 (D1 mini) und einem I²C-LCD-Display (2004A, 20×4 Zeichen) beliebige Informationen aus dem Smart Home (z. B. ioBroker) per MQTT darstellen kann.

Die Firmware wurde so entwickelt, dass sie komplett ohne feste Vorkonfiguration funktioniert und bequem über ein Webinterface eingestellt werden kann.


Funktionen

  • Automatische WLAN-Einrichtung
    Beim ersten Start öffnet das Modul einen eigenen Access Point („LCD-Setup-xxxxxx“).
    Über eine Weboberfläche können WLAN-Zugangsdaten und MQTT-Server eingestellt werden.

  • MQTT-Integration

    • Verbindung zu einem beliebigen MQTT-Broker (Host, Port, Benutzer, Passwort frei konfigurierbar).

    • Alle LCD-Zeilen lassen sich über Topics beschreiben.

    • Zwei Modi pro Zeile:

      • Links/Rechts-Aufteilung → links ein fester Bereich (z. B. „Netz:“), rechts Werte rechtsbündig (z. B. „+1500W“).

      • Vollzeile → ganzer Text ohne Aufteilung.

    • Steuerung über JSON möglich: alle vier Zeilen in einem Befehl.

  • Besonderheiten

    • Rechtsbündige Zahlenwerte: Zahlen (z. B. Wattangaben) stehen immer exakt am rechten Rand, unabhängig von der Länge.

    • Flexibler „Left Width“: Breite des linken Bereichs ist frei einstellbar (Standard: 12 Zeichen).

    • Backlight steuerbar über MQTT oder Webinterface.

    • Display löschen über MQTT-Topic oder Button im Webinterface.

    • Mehrseitige Anzeigen möglich (z. B. zyklisch wechselnde Seiten via ioBroker Blockly).

  • Webinterface

    • Statusseite mit IP-Adresse und aktuellem MQTT-Broker.

    • Testeingabe: Links/Rechts je Zeile können manuell ausprobiert werden.

    • Konfigurationsseite für MQTT-Server, Topic-Prefix, LCD-Größe und Left-Width.

    • Zurücksetzen der WLAN-Einstellungen per Button.

  • Persistente Speicherung
    Alle Konfigurationen (MQTT-Server, Displayeinstellungen usw.) werden im Flash des ESP8266 (LittleFS) gespeichert und bleiben nach einem Neustart erhalten.

  • Robustheit

    • Automatischer Reconnect zu WLAN und MQTT.

    • Statusmeldung („alive“) wird regelmäßig an den Broker gesendet.


Verwendete Libraries

  • ESP8266WiFi – WLAN-Funktionalität

  • PubSubClient – MQTT-Client

  • WiFiManager – komfortable Einrichtung über Captive Portal

  • ArduinoJson – JSON-Verarbeitung

  • hd44780 / hd44780_I2Cexp – Display-Ansteuerung für I²C-LCDs

  • LittleFS – Speichern der Konfiguration im Flash


Hardware

  • ESP8266 Wemos D1 mini

  • LCD 2004A (20×4, grün) mit I²C-Adapter

  • I²C-Anschlüsse:

    • SDA → D2 (GPIO4)

    • SCL → D1 (GPIO5)

  • Stromversorgung über Micro-USB oder 5 V-Pin

				
					#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <Wire.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <FS.h>
#include <LittleFS.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h> // tzapu

// hd44780 (statt LiquidCrystal_I2C)
#include <hd44780.h>
#include <hd44780ioClass/hd44780_I2Cexp.h>

// ---------- I2C Pins (D1 mini) ----------
const int I2C_SDA_PIN = D2; // GPIO4
const int I2C_SCL_PIN = D1; // GPIO5

// ---------- Defaults ----------
struct AppConfig {
  char mqtt_host[64]    = "IP.Adresse";
  uint16_t mqtt_port    = 1885;
  char mqtt_user[32]    = "Benutzername";
  char mqtt_pass[64]    = "Passwort";
  char topic_prefix[48] = "LCD/lcd";     // z. B. "LCD/lcd"
  uint8_t lcd_cols      = 20;            // 16 oder 20
  uint8_t lcd_rows      = 4;             // meist 4
  uint8_t left_width    = 12;            // feste Breite links
} cfg;

const char* CONFIG_PATH = "/config.json";

// ---------- Topics (aus Prefix) ----------
String T_L1L, T_L1R, T_L2L, T_L2R, T_L3L, T_L3R, T_L4L, T_L4R;
String T_LINE1, T_LINE2, T_LINE3, T_LINE4; // Full-Line
String T_ALL, T_CLEAR, T_BACKLIGHT, T_STATUS;

WiFiClient espClient;
PubSubClient mqtt(espClient);
ESP8266WebServer http(80);

// hd44780: Auto-Scan der I2C-Adresse
hd44780_I2Cexp lcd;

// Zeilen-Darstellung
enum RowMode { MODE_SPLIT, MODE_FULL };
RowMode rowMode[4] = { MODE_SPLIT, MODE_SPLIT, MODE_SPLIT, MODE_SPLIT };

String leftPart[4]  = {"","","",""};
String rightPart[4] = {"","","",""};
String fullLine[4]  = {"","","",""};

bool shouldSaveConfig = false;
bool firstRenderDone = false;

// ---------- Utils ----------
String translit(String s){
  s.replace("ä","ae"); s.replace("Ä","Ae");
  s.replace("ö","oe"); s.replace("Ö","Oe");
  s.replace("ü","ue"); s.replace("Ü","Ue");
  s.replace("ß","ss");
  return s;
}

String padRightFixed(String s, uint8_t width){
  s = translit(s);
  if ((int)s.length() > width) return s.substring(0, width);
  while ((int)s.length() < width) s += ' ';
  return s;
}

String padLeftToWidth(String s, uint8_t width){
  s = translit(s);
  if ((int)s.length() > width) s = s.substring(0, width);
  int spaces = width - s.length();
  String pad = "";
  for (int i=0;i<spaces;i++) pad += ' ';
  return pad + s;
}

void renderRow(uint8_t row){
  if (row >= cfg.lcd_rows) return;

  lcd.setCursor(0, row);

  if (rowMode[row] == MODE_FULL) {
    // komplette Zeile wie gesendet anzeigen
    String t = translit(fullLine[row]);
    if ((int)t.length() > cfg.lcd_cols) t = t.substring(0,cfg.lcd_cols);
    while ((int)t.length() < cfg.lcd_cols) t += ' ';
    lcd.print(t);
  } else {
    // Links/Rechts-Layout
    uint8_t rightWidth = (cfg.lcd_cols > cfg.left_width) ? (cfg.lcd_cols - cfg.left_width) : 0;
    String leftFixed  = padRightFixed(leftPart[row], cfg.left_width);
    String rightFixed = (rightWidth > 0) ? padLeftToWidth(rightPart[row], rightWidth) : "";
    lcd.print(leftFixed + rightFixed);
  }
}

void renderAll(){
  for (uint8_t r=0; r<cfg.lcd_rows; r++) renderRow(r);
  firstRenderDone = true;
}

void lcdClear(){
  for (uint8_t r=0; r<cfg.lcd_rows; r++){
    leftPart[r]=""; rightPart[r]=""; fullLine[r]="";
    rowMode[r] = MODE_SPLIT;
  }
  lcd.clear();
  renderAll();
}

void setBacklight(bool on){
  if (on) lcd.backlight(); else lcd.noBacklight();
}

// ---------- Config (LittleFS) ----------
bool loadConfig(){
  if (!LittleFS.exists(CONFIG_PATH)) return false;
  File f = LittleFS.open(CONFIG_PATH, "r");
  if (!f) return false;
  StaticJsonDocument<512> doc;
  auto err = deserializeJson(doc, f);
  f.close();
  if (err) return false;

  strlcpy(cfg.mqtt_host,   doc["mqtt_host"]   | cfg.mqtt_host,   sizeof(cfg.mqtt_host));
  cfg.mqtt_port  = doc["mqtt_port"]  | cfg.mqtt_port;
  strlcpy(cfg.mqtt_user,   doc["mqtt_user"]   | cfg.mqtt_user,   sizeof(cfg.mqtt_user));
  strlcpy(cfg.mqtt_pass,   doc["mqtt_pass"]   | cfg.mqtt_pass,   sizeof(cfg.mqtt_pass));
  strlcpy(cfg.topic_prefix,doc["topic_prefix"]| cfg.topic_prefix,sizeof(cfg.topic_prefix));
  cfg.lcd_cols   = doc["lcd_cols"]   | cfg.lcd_cols;
  cfg.lcd_rows   = doc["lcd_rows"]   | cfg.lcd_rows;
  cfg.left_width = doc["left_width"] | cfg.left_width;
  return true;
}

bool saveConfig(){
  StaticJsonDocument<512> doc;
  doc["mqtt_host"] = cfg.mqtt_host;
  doc["mqtt_port"] = cfg.mqtt_port;
  doc["mqtt_user"] = cfg.mqtt_user;
  doc["mqtt_pass"] = cfg.mqtt_pass;
  doc["topic_prefix"] = cfg.topic_prefix;
  doc["lcd_cols"] = cfg.lcd_cols;
  doc["lcd_rows"] = cfg.lcd_rows;
  doc["left_width"] = cfg.left_width;

  File f = LittleFS.open(CONFIG_PATH, "w");
  if (!f) return false;
  serializeJson(doc, f);
  f.close();
  return true;
}

void buildTopics(){
  String p = String(cfg.topic_prefix);
  if (p.endsWith("/")) p.remove(p.length()-1);

  // Left/Right
  T_L1L = p + "/line1_left";  T_L1R = p + "/line1_right";
  T_L2L = p + "/line2_left";  T_L2R = p + "/line2_right";
  T_L3L = p + "/line3_left";  T_L3R = p + "/line3_right";
  T_L4L = p + "/line4_left";  T_L4R = p + "/line4_right";

  // Full-Line (schaltet MODE_FULL)
  T_LINE1 = p + "/line1";
  T_LINE2 = p + "/line2";
  T_LINE3 = p + "/line3";
  T_LINE4 = p + "/line4";

  T_ALL       = p + "/all";
  T_CLEAR     = p + "/clear";
  T_BACKLIGHT = p + "/backlight";
  T_STATUS    = p + "/status";
}

// ---------- MQTT ----------
String jsonValue(const String& json, const String& key){
  String k = "\"" + key + "\"";
  int p = json.indexOf(k); if (p < 0) return "";
  p = json.indexOf(':', p); if (p < 0) return "";
  p++;
  while (p < (int)json.length() && json[p]==' ') p++;
  String v="";
  if (p < (int)json.length() && json[p]=='\"'){
    p++;
    while (p < (int)json.length() && json[p] != '\"') v += json[p++];
  } else {
    while (p < (int)json.length()){
      char c = json[p];
      if (c==',' || c=='}' || c==' ' || c=='\n' || c=='\r' || c=='\t') break;
      v += c; p++;
    }
  }
  v.trim(); return v;
}

void handleAllJson(const String& payload){
  // left_width optional
  String v;
  if ((v = jsonValue(payload,"left_width")).length()){
    int lw = v.toInt();
    if (lw > 0 && lw < cfg.lcd_cols) { cfg.left_width = (uint8_t)lw; saveConfig(); }
  }

  // Links/Rechts je Zeile – aktiviert MODE_SPLIT
  String l1L = jsonValue(payload,"l1L"); if (l1L.length()) { leftPart[0] = l1L; rowMode[0] = MODE_SPLIT; }
  String l1R = jsonValue(payload,"l1R"); if (l1R.length()) { rightPart[0]= l1R; rowMode[0] = MODE_SPLIT; }
  String l2L = jsonValue(payload,"l2L"); if (l2L.length()) { leftPart[1] = l2L; rowMode[1] = MODE_SPLIT; }
  String l2R = jsonValue(payload,"l2R"); if (l2R.length()) { rightPart[1]= l2R; rowMode[1] = MODE_SPLIT; }
  String l3L = jsonValue(payload,"l3L"); if (l3L.length()) { leftPart[2] = l3L; rowMode[2] = MODE_SPLIT; }
  String l3R = jsonValue(payload,"l3R"); if (l3R.length()) { rightPart[2]= l3R; rowMode[2] = MODE_SPLIT; }
  String l4L = jsonValue(payload,"l4L"); if (l4L.length()) { leftPart[3] = l4L; rowMode[3] = MODE_SPLIT; }
  String l4R = jsonValue(payload,"l4R"); if (l4R.length()) { rightPart[3]= l4R; rowMode[3] = MODE_SPLIT; }

  // Optional komplette Zeilen im JSON: line1..line4 → MODE_FULL
  String f1 = jsonValue(payload,"line1"); if (f1.length()) { fullLine[0]=f1; rowMode[0]=MODE_FULL; }
  String f2 = jsonValue(payload,"line2"); if (f2.length()) { fullLine[1]=f2; rowMode[1]=MODE_FULL; }
  String f3 = jsonValue(payload,"line3"); if (f3.length()) { fullLine[2]=f3; rowMode[2]=MODE_FULL; }
  String f4 = jsonValue(payload,"line4"); if (f4.length()) { fullLine[3]=f4; rowMode[3]=MODE_FULL; }

  renderAll();
}

void mqttCallback(char* topic, byte* payload, unsigned int length){
  String t = String(topic);
  String msg; msg.reserve(length+1);
  for (unsigned int i=0;i<length;i++) msg += (char)payload[i];
  msg.trim();

  if (t == T_CLEAR){
    if (msg=="1" || msg.equalsIgnoreCase("true")) lcdClear();

  } else if (t == T_BACKLIGHT){
    if (msg.equalsIgnoreCase("on") || msg=="1" || msg.equalsIgnoreCase("true")) setBacklight(true);
    else if (msg.equalsIgnoreCase("off") || msg=="0" || msg.equalsIgnoreCase("false")) setBacklight(false);

  } else if (t == T_ALL){
    handleAllJson(msg);

  // Left/Right → MODE_SPLIT
  } else if (t == T_L1L){ leftPart[0] = msg; rowMode[0]=MODE_SPLIT; renderRow(0); }
    else if (t == T_L1R){ rightPart[0]= msg; rowMode[0]=MODE_SPLIT; renderRow(0); }
    else if (t == T_L2L){ leftPart[1] = msg; rowMode[1]=MODE_SPLIT; renderRow(1); }
    else if (t == T_L2R){ rightPart[1]= msg; rowMode[1]=MODE_SPLIT; renderRow(1); }
    else if (t == T_L3L){ leftPart[2] = msg; rowMode[2]=MODE_SPLIT; renderRow(2); }
    else if (t == T_L3R){ rightPart[2]= msg; rowMode[2]=MODE_SPLIT; renderRow(2); }
    else if (t == T_L4L){ leftPart[3] = msg; rowMode[3]=MODE_SPLIT; renderRow(3); }
    else if (t == T_L4R){ rightPart[3]= msg; rowMode[3]=MODE_SPLIT; renderRow(3); }

  // Full-Line → MODE_FULL (ändert nur diese Zeile)
  else if (t == T_LINE1){ fullLine[0] = msg; rowMode[0]=MODE_FULL; renderRow(0); }
  else if (t == T_LINE2){ fullLine[1] = msg; rowMode[1]=MODE_FULL; renderRow(1); }
  else if (t == T_LINE3){ fullLine[2] = msg; rowMode[2]=MODE_FULL; renderRow(2); }
  else if (t == T_LINE4){ fullLine[3] = msg; rowMode[3]=MODE_FULL; renderRow(3); }
}

void subscribeTopics(){
  // Left/Right
  mqtt.subscribe(T_L1L.c_str()); mqtt.subscribe(T_L1R.c_str());
  mqtt.subscribe(T_L2L.c_str()); mqtt.subscribe(T_L2R.c_str());
  mqtt.subscribe(T_L3L.c_str()); mqtt.subscribe(T_L3R.c_str());
  mqtt.subscribe(T_L4L.c_str()); mqtt.subscribe(T_L4R.c_str());

  // Full-Line
  mqtt.subscribe(T_LINE1.c_str()); mqtt.subscribe(T_LINE2.c_str());
  mqtt.subscribe(T_LINE3.c_str()); mqtt.subscribe(T_LINE4.c_str());

  mqtt.subscribe(T_ALL.c_str());
  mqtt.subscribe(T_CLEAR.c_str());
  mqtt.subscribe(T_BACKLIGHT.c_str());
}

void connectMQTT(){
  if (mqtt.connected()) return;
  mqtt.setServer(cfg.mqtt_host, cfg.mqtt_port);
  mqtt.setCallback(mqttCallback);

  String clientId = "LCD-" + String(ESP.getChipId(), HEX);
  while (!mqtt.connected()){
    if (strlen(cfg.mqtt_user)==0){
      if (mqtt.connect(clientId.c_str())){
        subscribeTopics();
        mqtt.publish(T_STATUS.c_str(),"online");
        break;
      }
    } else {
      if (mqtt.connect(clientId.c_str(), cfg.mqtt_user, cfg.mqtt_pass)){
        subscribeTopics();
        mqtt.publish(T_STATUS.c_str(),"online");
        break;
      }
    }
    delay(1500);
  }
}

// ---------- Web UI ----------
String htmlHeader(const String& title){
  return String(F("<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"))
    + "<title>"+title+"</title>"
    + F("<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;margin:16px}input,select{padding:8px;margin:4px 0;width:100%;box-sizing:border-box}button{padding:10px 14px;border:0;border-radius:8px;cursor:pointer}form{max-width:680px}code{background:#f2f2f2;padding:2px 6px;border-radius:4px}hr{margin:20px 0}</style></head><body>");
}
String htmlFooter(){ return F("<p style='margin-top:24px;color:#777'>Micha's LCD-Firmware 1.0</p><script src="https://ma-multiservice.de/wp-content/cache/min/1/ff03109257ed727b9d04b74f4c23411d.js" data-minify="1"></script></body></html>"); }

void handleRoot(){
  String ip = WiFi.localIP().toString();
  String s = htmlHeader("LCD – Test & Status");
  s += "<h2>LCD – Test & Status</h2>";
  s += "<p><b>IP:</b> "+ip+" &nbsp; <b>MQTT:</b> "+String(cfg.mqtt_host)+":"+String(cfg.mqtt_port)+"</p>";
  s += "<p><b>Topics:</b> Full <code>"+T_LINE1+"</code> / Split <code>"+T_L1L+"</code>, <code>"+T_L1R+"</code> …</p>";
  s += "<hr><h3>Test: Full-Line</h3>";
  s += F("<form method='POST' action='/testfull'>"
         "<label>Zeile 1 (full)</label><input name='f1'>"
         "<label>Zeile 2 (full)</label><input name='f2'>"
         "<label>Zeile 3 (full)</label><input name='f3'>"
         "<label>Zeile 4 (full)</label><input name='f4'>"
         "<div style='margin-top:8px'><button type='submit'>Anzeigen (full)</button></div></form>");
  s += "<hr><h3>Test: Links/Rechts je Zeile</h3>";
  s += F("<form method='POST' action='/testpairs'>"
         "<label>Left width</label><input name='left_width' value='' placeholder='leer = unverändert'>"
         "<div style='display:grid;grid-template-columns:1fr 1fr;gap:8px'>"
         "<div><b>Z1 links</b><input name='l1l'></div><div><b>Z1 rechts</b><input name='l1r'></div>"
         "<div><b>Z2 links</b><input name='l2l'></div><div><b>Z2 rechts</b><input name='l2r'></div>"
         "<div><b>Z3 links</b><input name='l3l'></div><div><b>Z3 rechts</b><input name='l3r'></div>"
         "<div><b>Z4 links</b><input name='l4l'></div><div><b>Z4 rechts</b><input name='l4r'></div>"
         "</div><div style='margin-top:8px'><button type='submit'>Anzeigen (split)</button></div></form>");
  s += "<form method='POST' action='/clear' style='margin-top:12px'><button>Display löschen</button></form>";
  s += "<form method='POST' action='/backlight?on=1' style='display:inline-block;margin-top:12px;margin-right:8px'><button>Backlight EIN</button></form>";
  s += "<form method='POST' action='/backlight?on=0' style='display:inline-block;margin-top:12px'><button>Backlight AUS</button></form>";
  s += "<hr><h3>Konfiguration</h3><p><a href='/config'>MQTT & Display konfigurieren</a></p>";
  s += "<p><a href='/wifireset'>WLAN-Einstellungen zurücksetzen (neustarten)</a></p>";
  s += htmlFooter();
  http.send(200, "text/html; charset=utf-8", s);
}

void handleTestFull(){
  bool changed=false;
  if (http.hasArg("f1")) { fullLine[0]=http.arg("f1"); rowMode[0]=MODE_FULL; changed=true; }
  if (http.hasArg("f2")) { fullLine[1]=http.arg("f2"); rowMode[1]=MODE_FULL; changed=true; }
  if (http.hasArg("f3")) { fullLine[2]=http.arg("f3"); rowMode[2]=MODE_FULL; changed=true; }
  if (http.hasArg("f4")) { fullLine[3]=http.arg("f4"); rowMode[3]=MODE_FULL; changed=true; }
  if (changed) renderAll();
  http.sendHeader("Location","/");
  http.send(303);
}

void handleTestPairs(){
  if (http.hasArg("left_width")){
    String lw = http.arg("left_width"); lw.trim();
    if (lw.length()){
      int v = lw.toInt();
      if (v > 0 && v < cfg.lcd_cols){ cfg.left_width = (uint8_t)v; saveConfig(); }
    }
  }
  if (http.hasArg("l1l")) { leftPart[0]  = http.arg("l1l"); rowMode[0]=MODE_SPLIT; }
  if (http.hasArg("l1r")) { rightPart[0] = http.arg("l1r"); rowMode[0]=MODE_SPLIT; }
  if (http.hasArg("l2l")) { leftPart[1]  = http.arg("l2l"); rowMode[1]=MODE_SPLIT; }
  if (http.hasArg("l2r")) { rightPart[1] = http.arg("l2r"); rowMode[1]=MODE_SPLIT; }
  if (http.hasArg("l3l")) { leftPart[2]  = http.arg("l3l"); rowMode[2]=MODE_SPLIT; }
  if (http.hasArg("l3r")) { rightPart[2] = http.arg("l3r"); rowMode[2]=MODE_SPLIT; }
  if (http.hasArg("l4l")) { leftPart[3]  = http.arg("l4l"); rowMode[3]=MODE_SPLIT; }
  if (http.hasArg("l4r")) { rightPart[3] = http.arg("l4r"); rowMode[3]=MODE_SPLIT; }

  renderAll();
  http.sendHeader("Location","/");
  http.send(303);
}

void handleClear(){
  lcdClear();
  http.sendHeader("Location","/");
  http.send(303);
}

void handleBacklight(){
  bool on = http.arg("on") == "1";
  setBacklight(on);
  http.sendHeader("Location","/");
  http.send(303);
}

void handleConfigGet(){
  String s = htmlHeader("Konfiguration");
  s += "<h2>Konfiguration</h2>";
  s += F("<form method='POST' action='/config'>");
  s += "<label>MQTT Host</label><input name='mqtt_host' value='"+String(cfg.mqtt_host)+"'>";
  s += "<label>MQTT Port</label><input name='mqtt_port' value='"+String(cfg.mqtt_port)+"'>";
  s += "<label>MQTT User</label><input name='mqtt_user' value='"+String(cfg.mqtt_user)+"'>";
  s += "<label>MQTT Passwort</label><input name='mqtt_pass' value='"+String(cfg.mqtt_pass)+"'>";
  s += "<label>Topic-Präfix</label><input name='topic_prefix' value='"+String(cfg.topic_prefix)+"'>";
  s += "<label>LCD Spalten (16/20)</label><input name='lcd_cols' value='"+String(cfg.lcd_cols)+"'>";
  s += "<label>LCD Zeilen (4)</label><input name='lcd_rows' value='"+String(cfg.lcd_rows)+"'>";
  s += "<label>Left-Width (links, Zeichen)</label><input name='left_width' value='"+String(cfg.left_width)+"'>";
  s += "<div style='margin-top:8px'><button type='submit'>Speichern & Neustarten</button></div></form>";
  s += htmlFooter();
  http.send(200, "text/html; charset=utf-8", s);
}

void handleConfigPost(){
  if (http.hasArg("mqtt_host")) strlcpy(cfg.mqtt_host, http.arg("mqtt_host").c_str(), sizeof(cfg.mqtt_host));
  if (http.hasArg("mqtt_port")) cfg.mqtt_port = (uint16_t) http.arg("mqtt_port").toInt();
  if (http.hasArg("mqtt_user")) strlcpy(cfg.mqtt_user, http.arg("mqtt_user").c_str(), sizeof(cfg.mqtt_user));
  if (http.hasArg("mqtt_pass")) strlcpy(cfg.mqtt_pass, http.arg("mqtt_pass").c_str(), sizeof(cfg.mqtt_pass));
  if (http.hasArg("topic_prefix")) strlcpy(cfg.topic_prefix, http.arg("topic_prefix").c_str(), sizeof(cfg.topic_prefix));
  if (http.hasArg("lcd_cols")) cfg.lcd_cols = (uint8_t) http.arg("lcd_cols").toInt();
  if (http.hasArg("lcd_rows")) cfg.lcd_rows = (uint8_t) http.arg("lcd_rows").toInt();
  if (http.hasArg("left_width")) {
    int v = http.arg("left_width").toInt();
    if (v > 0 && v < cfg.lcd_cols) cfg.left_width = (uint8_t)v;
  }

  saveConfig();
  String s = htmlHeader("Gespeichert");
  s += "<h2>Gespeichert</h2><p>Neustart in 2 Sekunden…</p>";
  s += htmlFooter();
  http.send(200,"text/html; charset=utf-8",s);
  delay(2000);
  ESP.restart();
}

void handleWiFiReset(){
  String s = htmlHeader("WLAN Reset");
  s += "<h2>WLAN-Einstellungen gelöscht</h2><p>Gerät startet neu und öffnet das Setup-WLAN.</p>";
  s += htmlFooter();
  http.send(200,"text/html; charset=utf-8",s);
  delay(500);
  WiFiManager wm;
  wm.resetSettings(); // löscht nur WLAN-Creds
  delay(500);
  ESP.restart();
}

// ---------- WiFiManager ----------
void saveConfigCallback() { shouldSaveConfig = true; }

void startConfigPortalIfNeeded(){
  WiFiManager wm;
  wm.setSaveConfigCallback(saveConfigCallback);

  WiFiManagerParameter p_mhost("mqtt_host", "MQTT Host", cfg.mqtt_host, 64);
  char portBuf[8]; snprintf(portBuf, sizeof(portBuf), "%u", cfg.mqtt_port);
  WiFiManagerParameter p_mport("mqtt_port", "MQTT Port", portBuf, 6);
  WiFiManagerParameter p_muser("mqtt_user", "MQTT User", cfg.mqtt_user, 32);
  WiFiManagerParameter p_mpass("mqtt_pass", "MQTT Passwort", cfg.mqtt_pass, 64);
  WiFiManagerParameter p_prefix("topic_prefix", "Topic-Präfix", cfg.topic_prefix, 48);
  char colsBuf[4]; snprintf(colsBuf, sizeof(colsBuf), "%u", cfg.lcd_cols);
  WiFiManagerParameter p_lcols("lcd_cols", "LCD Spalten (16/20)", colsBuf, 3);
  char rowsBuf[4]; snprintf(rowsBuf, sizeof(rowsBuf), "%u", cfg.lcd_rows);
  WiFiManagerParameter p_lrows("lcd_rows", "LCD Zeilen (4)", rowsBuf, 3);
  char lwBuf[4]; snprintf(lwBuf, sizeof(lwBuf), "%u", cfg.left_width);
  WiFiManagerParameter p_lw("left_width", "Left-Width (links, Zeichen)", lwBuf, 3);

  wm.addParameter(&p_mhost); wm.addParameter(&p_mport);
  wm.addParameter(&p_muser); wm.addParameter(&p_mpass);
  wm.addParameter(&p_prefix);
  wm.addParameter(&p_lcols); wm.addParameter(&p_lrows);
  wm.addParameter(&p_lw);

  String apName = "LCD-Setup-" + String(ESP.getChipId(), HEX);
  wm.setConfigPortalBlocking(true);
  wm.setBreakAfterConfig(true);
  wm.setMinimumSignalQuality(8);
  wm.setConnectTimeout(20);

  if (!wm.autoConnect(apName.c_str())) { ESP.restart(); }

  // Übernahme
  strlcpy(cfg.mqtt_host, p_mhost.getValue(), sizeof(cfg.mqtt_host));
  cfg.mqtt_port = (uint16_t) String(p_mport.getValue()).toInt();
  strlcpy(cfg.mqtt_user, p_muser.getValue(), sizeof(cfg.mqtt_user));
  strlcpy(cfg.mqtt_pass, p_mpass.getValue(), sizeof(cfg.mqtt_pass));
  strlcpy(cfg.topic_prefix, p_prefix.getValue(), sizeof(cfg.topic_prefix));
  cfg.lcd_cols = (uint8_t) String(p_lcols.getValue()).toInt();
  cfg.lcd_rows = (uint8_t) String(p_lrows.getValue()).toInt();
  cfg.left_width = (uint8_t) String(p_lw.getValue()).toInt();
  if (cfg.left_width == 0 || cfg.left_width >= cfg.lcd_cols) cfg.left_width = 12;

  if (shouldSaveConfig || !LittleFS.exists(CONFIG_PATH)) saveConfig();
}

// ---------- Setup/Loop ----------
unsigned long lastPing=0;

void setup(){
  Serial.begin(115200);
  delay(100);
  LittleFS.begin();

  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);

  loadConfig();

  // LCD initialisieren
  lcd.begin(cfg.lcd_cols, cfg.lcd_rows);
  setBacklight(true);
  lcd.clear();

  // Bootanzeige (full mode)
  fullLine[0] = "MQTT LCD bereit"; rowMode[0]=MODE_FULL;
  fullLine[1] = WiFi.localIP().toString(); rowMode[1]=MODE_FULL; // wird nach connect neu gesetzt
  fullLine[2] = "MQTT-Broker"; rowMode[2]=MODE_FULL;
  fullLine[3] = String(cfg.mqtt_host) + ":" + String(cfg.mqtt_port); rowMode[3]=MODE_FULL;
  renderAll();

  // WiFiManager-Portal (bei Erststart / wenn no connect)
  startConfigPortalIfNeeded();

  // IP erneuern
  fullLine[1] = WiFi.localIP().toString(); rowMode[1]=MODE_FULL;
  renderRow(1);

  buildTopics();
  mqtt.setCallback(mqttCallback);
  connectMQTT();

  // Webserver
  http.on("/", HTTP_GET, handleRoot);
  http.on("/testfull",  HTTP_POST, handleTestFull);
  http.on("/testpairs", HTTP_POST, handleTestPairs);
  http.on("/clear",     HTTP_POST, handleClear);
  http.on("/backlight", HTTP_POST, handleBacklight);
  http.on("/config",    HTTP_GET,  handleConfigGet);
  http.on("/config",    HTTP_POST, handleConfigPost);
  http.on("/wifireset", HTTP_GET,  handleWiFiReset);
  http.begin();
}

void loop(){
  if (!mqtt.connected()) connectMQTT();
  mqtt.loop();
  http.handleClient();

  unsigned long now = millis();
  if (now - lastPing > 60000UL){
    lastPing = now;
    mqtt.publish(T_STATUS.c_str(), "alive");
    if (!firstRenderDone) renderAll();
  }
}

				
			
Nach oben scrollen