1. Allgemein
In Teil 1 und Teil 2 Teilen ging es zunächst um die eigentliche Weichensteuerung mit einem Arduino Mega und anschließend um die WLAN-Erweiterung mit dem ESP32. Die Weboberfläche konnte bereits Kommandos an den Mega senden, der daraufhin die Weichen und Lichtmodule schaltet.
Im praktischen Betrieb zeigte sich jedoch ein Nachteil: Der ESP32 wusste nach einem Neustart zunächst nicht, in welchem Zustand sich die Anlage aktuell befindet. Die Weboberfläche zeigte also unter Umständen nicht den echten Ist-Zustand, sondern nur den zuletzt lokal angenommenen Zustand. Genau dieses Problem löst der Rückkanal.
In diesem dritten Teil sendet der Arduino Mega seinen aktuellen Zustand regelmäßig und zusätzlich nach jeder Änderung an den ESP32 zurück. Dadurch kennt die Weboberfläche jederzeit die echte Stellung der Weichen und den tatsächlichen Zustand der Lichtmodule.
2. Warum ein Rückkanal sinnvoll ist
Ohne Rückkanal arbeitet die Weboberfläche im Grunde nur „blind“: Ein Tastendruck löst zwar ein Kommando aus, aber der ESP32 weiß nicht sicher, ob der Befehl tatsächlich ausgeführt wurde oder welcher Zustand aktuell auf der Anlage anliegt.
Mit einem Rückkanal ergeben sich mehrere Vorteile:
- Der ESP32 kennt jederzeit den tatsächlichen Zustand der Anlage
- Die Weboberfläche zeigt den bestätigten Ist-Zustand statt nur einer Annahme
- Nach einem Neustart des ESP32 kann der Zustand automatisch wieder übernommen werden
- Verbindungsabbrüche zum Mega lassen sich erkennen
- Die Benutzeroberfläche wirkt deutlich robuster und nachvollziehbarer
Gerade bei mehreren Weichen und Lichtmodulen verbessert das die Bedienung erheblich.
3. UART-Rückkanal zwischen Mega und ESP32
Die Kommunikation läuft weiterhin über die zweite serielle Schnittstelle beider Boards.
ESP32
- GPIO17 = TX2 zum Mega
- GPIO16 = RX2 vom Mega
Arduino Mega
- Pin 17 = RX2 vom ESP32
- Pin 16 = TX2 zum ESP32
Die eigentlichen Steuerkommandos bleiben weiterhin einzelne Zeichen wie a, b, c oder o, p usw.
Neu ist, dass der Mega zusätzlich komplette Status-Telegramme an den ESP32 zurücksendet.
4. Wichtiger Praxishinweis zur Pegelanpassung
In der Theorie liegt es nahe, für beide Richtungen einen Logik-Level-Konverter zu verwenden, da der ESP32 mit 3,3 V und der Mega mit 5 V arbeitet. In meinem Aufbau hat sich jedoch gezeigt, dass ein üblicher bidirektionaler Levelshifter für UART nicht zuverlässig funktioniert hat. Sobald der Rückkanal vollständig über das Modul geführt wurde, kamen die Kommandos vom ESP32 zum Mega nicht mehr sauber an.
Die funktionierende Lösung war am Ende:
- ESP32 TX2 -> Mega RX2 direkt verbinden
- Mega TX2 -> ESP32 RX2 nur in dieser Richtung absenken
Das bedeutet praktisch:
- Die Leitung vom ESP32 zum Mega braucht in meinem Aufbau keinen Pegelwandler
- Die Leitung vom Mega zum ESP32 sollte hingegen auf 3,3 V angepasst werden
Ein einfacher Spannungsteiler oder ein geeigneter unidirektionaler Pegelwandler reicht hier aus. Der entscheidende Punkt ist: Die Pegelanpassung muss für den Rückkanal nur in Richtung Mega -> ESP32 erfolgen.
Gemeinsame Masse (GND) zwischen beiden Boards ist selbstverständlich weiterhin Pflicht.
5. Status-Telegramm vom Mega
Der Arduino Mega hält intern den Zustand aller steuerbaren Elemente vor und sendet diesen als vollständigen Snapshot zurück. Verwendet wird dabei ein einfaches Telegramm mit Start- und Endzeichen:
<ST;W1=A;W2=A;W3=C;W4=C;W5=F;W6=H;W7=J;L1=0;L2=0;L3=0;L4=0;L5=0;L6=0>Dabei steht:
- ST für Status
- W1 bis W7 für die Weichen
- L1 bis L6 für die Lichtmodule
- 0 bzw. 1 für Aus/Ein
Der Mega sendet dieses Telegramm:
- regelmäßig im festen Zeitintervall
- sofort nach jeder Zustandsänderung
Ein gekürzter Ausschnitt aus dem Mega-Code sieht so aus:
void sendStatusTelegram() {
String telegram = "<ST";
for (int i = 0; i < 7; i++) {
telegram += ";W";
telegram += String(i + 1);
telegram += "=";
telegram += weichenState[i];
}
for (int i = 0; i < 6; i++) {
telegram += ";L";
telegram += String(i + 1);
telegram += "=";
telegram += lightState[i] ? "1" : "0";
}
telegram += ">";
Serial2.println(telegram);
}
Wichtig ist, dass immer der komplette Zustand übertragen wird und nicht nur einzelne Ereignisse. Damit der Mega den aktuellen Zustand zuverlässig zurückmelden kann, wird die Stellung aller Weichen im Programm zusätzlich in einem Array weichenState[] gespeichert.
6. Auswertung auf dem ESP32
Der ESP32 liest den Rückkanal zeichenweise ein. Erst wenn ein vollständiges Telegramm zwischen < und > empfangen wurde, wird es ausgewertet. Unvollständige oder fehlerhafte Daten werden verworfen.
Der grundsätzliche Ablauf sieht so aus:
while (Serial2.available()) {
char c = (char)Serial2.read();
if (c == '<') {
megaTelegramActive = true;
megaRxBuffer = "";
} else if (c == '>' && megaTelegramActive) {
processMegaTelegram(megaRxBuffer);
megaTelegramActive = false;
megaRxBuffer = "";
} else if (megaTelegramActive) {
megaRxBuffer += c;
}
}Was passiert dabei genau?
Der Ablauf lässt sich folgendermaßen beschreiben:
- Solange keine Nachricht begonnen hat, werden eingehende Zeichen ignoriert.
- Sobald < empfangen wird, setzt der ESP32 die Variable megaTelegramActive auf true. Damit weiß das Programm: Ab jetzt gehören die folgenden Zeichen zu einem Telegramm.
- Gleichzeitig wird der Puffer megaRxBuffer geleert, damit keine alten Reste erhalten bleiben.
- Jedes weitere Zeichen wird an diesen Puffer angehängt.
- Sobald schließlich > eintrifft, wird der komplette Inhalt des Puffers an die Funktion processMegaTelegram() übergeben.
- Danach wird der Empfangszustand wieder zurückgesetzt.
Auf diese Weise wird verhindert, dass halbe oder unvollständige Nachrichten sofort ausgewertet werden. Der ESP32 reagiert also nur auf vollständige Telegramme.
Warum ist das wichtig?
Serielle Kommunikation ist ein Datenstrom. Es gibt keine Garantie, dass ein komplettes Telegramm genau in einem einzigen Moment oder in einem einzigen Leseschritt verfügbar ist. Häufig liegen beim ersten Zugriff nur wenige Zeichen vor, der Rest trifft erst Millisekunden später ein. Würde man sofort mit dem Parsen beginnen, entstünden leicht Fehler oder unvollständige Zustände.
Der Rahmen mit < und > sorgt also dafür, dass der ESP32 genau erkennen kann:
- wann eine Nachricht beginnt
- wann sie vollständig ist
- wann sie ausgewertet werden darf
Weitergabe an den Parser
Sobald der Puffer vollständig ist, übernimmt eine weitere Funktion die eigentliche inhaltliche Auswertung. Diese Funktion prüft zunächst, ob das Telegramm überhaupt mit ST; beginnt und damit ein gültiges Status-Telegramm ist:
bool parseStatusTelegram(const String& telegram) {
if (!telegram.startsWith("ST;")) return false;
// weitere Verarbeitung ...
}Ein gültiges Telegramm besteht also im Kern aus mehreren Einträgen, die durch Semikolons getrennt sind:
ST;W1=A;W2=A;W3=C;...;L1=0;L2=0Zerlegen in einzelne Einträge
Im nächsten Schritt wird das Telegramm Stück für Stück zerlegt. Jeder Abschnitt zwischen zwei Semikolons wird einzeln betrachtet. Dadurch entstehen Teilstrings wie:
- W1=A
- W2=A
- W5=F
- L1=0
- L6=1
Dazu wird das Telegramm in einer Schleife durchsucht:
int start = 3;
while (start < telegram.length()) {
int end = telegram.indexOf(';', start);
if (end < 0) end = telegram.length();
String part = telegram.substring(start, end);
int equals = part.indexOf('=');
// part enthält nun z. B. "W1=A" oder "L3=0"
start = end + 1;
}(start = 3 deshalb, weil die ersten drei Zeichen bereits ST; sind und übersprungen werden können.)
Auslesen von Typ, Nummer und Wert
Jeder dieser Teile wird nun weiter analysiert.
Aus W1=A werden beispielsweise drei Informationen gewonnen:
- Typ: W → es handelt sich um eine Weiche
- Nummer: 1
- Wert: A
Aus L3=0 wird entsprechend:
- Typ: L → es handelt sich um ein Lichtmodul
- Nummer: 3
- Wert: 0
Der Code dazu sieht etwa so aus:
char type = part.charAt(0);
int number = part.substring(1, equals).toInt();
char value = part.charAt(equals + 1);Anschließend wird geprüft, ob der Eintrag gültig ist:
if (type == 'W' && number >= 1 && number <= 7 && value >= 'A' && value <= 'Z') {
nextWeichen[number - 1] = value;
} else if (type == 'L' && number >= 1 && number <= 6 && (value == '0' || value == '1')) {
nextLicht[number - 1] = (value == '1');
} else {
return false;
}Übernahme in den internen Zustand
Wenn alle Teile korrekt gelesen wurden, übernimmt der ESP32 die Werte in seine zentrale Zustandsstruktur. Dort liegen sie anschließend nicht mehr nur als Text vor, sondern als echter Programmzustand, mit dem die Weboberfläche arbeiten kann.
Dazu gehören beispielsweise:
- die aktuelle Stellung aller Weichen
- der Zustand aller Lichtmodule
- der Zeitstempel der letzten gültigen Mega-Nachricht
- der Verbindungsstatus megaOnline
Ein vereinfachter Ausschnitt sieht so aus:
stellwerkState.weichen[i] = nextWeichen[i];
stellwerkState.licht[i] = nextLicht[i];
stellwerkState.lastMegaMessageMillis = millis();
stellwerkState.megaOnline = true;
Damit ist nach jedem gültigen Telegramm klar:
- welche Stellung wirklich auf der Anlage aktiv ist
- ob der Mega noch erreichbar ist
- welche Informationen an die Weboberfläche weitergereicht werden müssen
Vorteile dieses Verfahrens
Der beschriebene Aufbau bringt mehrere Vorteile mit sich:
- Es werden nur vollständige Telegramme ausgewertet
- Störzeichen oder unvollständige Daten können leicht verworfen werden
- Der ESP32 bleibt unabhängig von der Übertragungsdauer einzelner Zeichen stabil
- Die Zustandsdaten liegen nach dem Parsen in einer gut nutzbaren internen Struktur vor
- Die Weboberfläche kann auf bestätigte Ist-Zustände statt auf bloße Annahmen reagieren
Gerade bei der seriellen Kommunikation zwischen zwei Mikrocontrollern ist diese saubere Trennung aus Empfang, Pufferung, Validierung und Parsing sehr hilfreich.
7. Weboberfläche mit Rückmeldung
Mit dem Rückkanal kennt der ESP32 nicht nur die eingehenden Schaltbefehle aus dem Browser, sondern auch den vom Arduino Mega bestätigten tatsächlichen Anlagenzustand. Genau diese Information kann er wiederum an die Weboberfläche zurückgeben. Dadurch wird aus einer reinen Fernbedienung eine Oberfläche mit echter Rückmeldung.
Ohne Rückkanal wäre die HTML-Seite gezwungen, den Zustand lokal nur zu schätzen. Klickt man etwa auf eine Weiche, könnte das JavaScript sofort die Darstellung umschalten, obwohl der Mega den Befehl vielleicht noch gar nicht verarbeitet hat oder die Kommunikation gestört ist. Mit der Rückmeldung arbeitet die Oberfläche dagegen wesentlich sauberer: Sichtbar wird nur der Zustand, den der Mega tatsächlich gemeldet hat.
Der Status-Endpunkt /status
Damit der Browser den aktuellen Zustand abfragen kann, stellt der ESP32 einen eigenen HTTP-Endpunkt bereit. Dieser ist unter /status erreichbar und liefert die Daten als JSON zurück.
Im ESP32-Code sieht der Handler dafür so aus:
server.on("/status", HTTP_GET, []() {
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.send(200, "application/json", statusJson());
});Sobald der Browser also die Adresse /status anfragt, erzeugt der ESP32 daraus eine JSON-Antwort und schickt sie zurück.
Aufbau der JSON-Antwort
Die Rückgabe enthält alle wichtigen Zustände der Anlage in kompakter Form. Ein Beispiel:
{
"w1": "A",
"w2": "A",
"w3": "C",
"w4": "C",
"w5": "F",
"w6": "H",
"w7": "J",
"l1": 0,
"l2": 0,
"l3": 0,
"l4": 0,
"l5": 0,
"l6": 0,
"megaOnline": true,
"lastMegaMessageMillis": 15342
}
Die Bedeutung der einzelnen Felder ist dabei:
- w1 bis w7: aktuelle Stellungen der Weichen
- l1 bis l6: Status der Lichtmodule (0 = aus, 1 = ein)
- megaOnline: zeigt an, ob der ESP32 den Mega als erreichbar betrachtet
- lastMegaMessageMillis: Zeitpunkt der letzten gültigen Nachricht vom Mega
Die Werte stammen nicht aus einer lokalen Vermutung der Webseite, sondern aus dem internen Zustand, den der ESP32 zuvor aus dem Status-Telegramm des Mega aufgebaut hat.
Erzeugen des JSON auf dem ESP32
Im ESP32-Programm werden diese Daten in einer Funktion als String zusammengesetzt. Ein vereinfachter Ausschnitt sieht so aus:
String statusJson() {
String json = "{";
json += "\"w1\":\"" + String(stellwerkState.weichen[0]) + "\",";
json += "\"w2\":\"" + String(stellwerkState.weichen[1]) + "\",";
// weitere Weichen ...
json += "\"l1\":" + String(stellwerkState.licht[0] ? "1" : "0") + ",";
json += "\"l2\":" + String(stellwerkState.licht[1] ? "1" : "0") + ",";
// weitere Lichtmodule ...
json += "\"megaOnline\":";
json += stellwerkState.megaOnline ? "true" : "false";
json += "}";
return json;
}
Diese Funktion greift also direkt auf die zuvor geparsten Statusdaten zu und formt daraus eine standardisierte JSON-Struktur, die jeder Browser problemlos verarbeiten kann.
Abruf im Browser
Auf der HTML-Seite erfolgt der Abruf ganz normal per JavaScript über fetch(). Die Funktion fragt /status beim ESP32 an, liest die Antwort als JSON ein und übergibt sie anschließend an eine weitere Funktion zur Darstellung.
Beispiel:
function loadInitialStatus() {
fetch("/status")
.then(resp => {
if (!resp.ok) throw new Error("Status nicht erreichbar");
return resp.json();
})
.then(applyMegaStatus)
.catch(err => {
console.error("Status-Fehler:", err);
});
}Was passiert dabei genau?
Der Ablauf ist wie folgt:
- Die Webseite sendet eine HTTP-Anfrage an den ESP32:
GET /status - Der ESP32 beantwortet diese Anfrage mit einem JSON-Dokument.
- Im Browser wandelt resp.json() diese Textantwort automatisch in ein JavaScript-Objekt um.
- Dieses Objekt wird dann an applyMegaStatus(status) übergeben.
Das heißt: Aus der Rückgabe
{
"w1": "A",
"l1": 0,
"megaOnline": true
}status.w1
status.l1
status.megaOnlineÜbernahme in die Oberfläche
In der Funktion applyMegaStatus() wird anschließend der vom Mega bestätigte Zustand auf die gesamte Weboberfläche übertragen. Dabei werden sowohl die internen Zustandsvariablen als auch die sichtbaren Bedienelemente aktualisiert.
Ein vereinfachter Ausschnitt:
function applyMegaStatus(status) {
W12State = status.w1 === "A";
W34State = status.w3 === "C";
W5State = status.w5 === "E";
W6State = status.w6 === "G";
W7State = status.w7 === "I";
L1State = !!status.l1;
L2State = !!status.l2;
L3State = !!status.l3;
L4State = !!status.l4;
L5State = !!status.l5;
L6State = !!status.l6;
setLightButtonState(6, L1State);
setLightButtonState(5, L2State);
// weitere Lichtmodule ...
setLoadingVisible(!status.megaOnline);
}Hier sieht man gut, dass die Webseite nicht mehr einfach beim Klick irgendeine Farbe wechselt, sondern stattdessen immer zuerst den bestätigten Zustand vom ESP32 übernimmt.
Darstellung von Weichen und Lichtmodulen
Die Zustandsvariablen steuern anschließend die grafische Darstellung:
- der Canvas-Gleisplan zeigt die aktuelle Weichenstellung
- die Lichtbuttons werden rot oder grün eingefärbt
- bei fehlender Verbindung kann ein Overlay oder eine Statusmeldung eingeblendet werden
Für die Lichtmodule geschieht die optische Aktualisierung beispielsweise über CSS-Klassen:
function setLightButtonState(buttonIndex, isOn) {
const desktopButton = document.getElementById("L" + buttonIndex);
if (desktopButton) {
desktopButton.className = "btn " + (isOn ? "lightOn" : "lightOff");
}
}Die Klassen lightOn und lightOff definieren dann in CSS die jeweilige Farbe.
Regelmäßige Aktualisierung
Damit Änderungen auf der Anlage auch dann sichtbar werden, wenn nicht gerade geklickt wurde, fragt die Weboberfläche den Status regelmäßig erneut ab. Das geschieht über einen Timer:
loadInitialStatus();
setInterval(loadInitialStatus, 300);Damit wird alle 300 Millisekunden eine neue Statusanfrage an den ESP32 gesendet. So bleibt die Oberfläche praktisch in Echtzeit aktuell.
Zusätzlich kann nach einem Benutzerklick noch kurzfristig besonders schnell nachgefragt werden, damit die Bestätigung des Mega zügiger sichtbar wird. Genau das sorgt dafür, dass die Oberfläche nicht träge wirkt, obwohl sie immer auf bestätigte Zustände wartet.
Vorteil gegenüber rein lokaler Anzeige
Der große Unterschied zur alten Lösung liegt darin, dass die Oberfläche nicht mehr „rät“, sondern sich am tatsächlichen Zustand orientiert:
- Klick im Browser erzeugt nur einen Befehl
- Mega schaltet real
- Mega sendet seinen neuen Zustand an den ESP32
- ESP32 liefert diesen Zustand als JSON an die Webseite
- erst dann wird die Darstellung aktualisiert
Damit ist sichergestellt, dass Canvas, Lichtbutton und Verbindungsanzeige nicht nur optisch schön aussehen, sondern wirklich den Zustand der Anlage widerspiegeln.
Verbindungsstatus in der Oberfläche
Zusätzlich wird über megaOnline sichtbar, ob der Rückkanal aktuell funktioniert. Wenn längere Zeit keine gültige Nachricht vom Mega eingeht, setzt der ESP32 diesen Wert auf false. Die HTML-Seite kann darauf sofort reagieren, beispielsweise mit einem Lade-Overlay:
setLoadingVisible(!status.megaOnline);Dadurch erkennt man direkt, ob die Steuerung zwar noch geöffnet ist, aber die eigentliche Verbindung zum Mega gerade fehlt.
Zusammenfassung
Die Weboberfläche arbeitet mit Rückmeldung also in mehreren Schritten:
- Der Mega sendet seinen bestätigten Zustand an den ESP32
- Der ESP32 speichert diesen Zustand intern
- Über /status stellt er ihn als JSON bereit
- Die Webseite fragt diesen Endpunkt regelmäßig per fetch() ab
- Das empfangene JSON wird in sichtbare Zustände für Canvas, Buttons und Verbindungsanzeige übersetzt
Gerade dieser Ablauf macht die gesamte Steuerung wesentlich robuster und nachvollziehbarer als eine rein lokale Button-Logik ohne Rückmeldung.
8. Fazit
Mit dem Rückkanal ist aus der reinen Fernsteuerung nun ein deutlich vollständigeres System geworden. Der ESP32 übernimmt nicht mehr nur das Weiterleiten von Befehlen, sondern kennt auch den tatsächlichen Zustand der Anlage. Gerade nach Neustarts oder bei längerer Nutzung ist das ein großer Vorteil.
Besonders hilfreich war in der Praxis:
- vollständige Status-Telegramme statt Einzelmeldungen
- sofortige Rückmeldung nach jedem Schaltvorgang
- zusätzliche Offline-Erkennung
- klare Trennung zwischen Steuerkanal und Rückkanal
- Pegelanpassung nur in der Richtung Mega -> ESP32, da die direkte Verbindung ESP32 -> Mega in meinem Aufbau deutlich zuverlässiger funktionierte
Damit ist die Steuerung nicht nur komfortabler, sondern auch robuster geworden. Als nächste Ausbaustufe wären z. B. Rückmeldekontakte, Sensordaten oder sogar eine Kameraansicht des Schattenbahnhofs denkbar.
Danke fürs Lesen!
