1. Allgemein
Im ersten Teil dieses Projekts wurde gezeigt, wie Weichen und Lichtmodule über einen Arduino Mega gesteuert werden: Eine Processing-Oberfläche auf dem PC sendet per USB-Seriell Kommandos an den Mega, der daraufhin über seine Ausgänge die Schaltvorgänge übernimmt . Die gesamte Steuerung lief bislang lokal am angeschlossenen Rechner ab.
Nun kommt der ESP32 als Bindeglied zwischen Netzwerk und Mega hinzu. Er fungiert sowohl als WLAN-Access-Pointals auch als Web-Server. Das bedeutet:
- Autarke Netzwerkanbindung: Der ESP32 stellt ein eigenes WLAN „stellwerk“ bereit, in das sich beliebige Geräte direkt einklinken können – ganz ohne externen Router.
- Geräteunabhängige Bedienung: Über jeden modernen Webbrowser (Smartphone, Tablet, PC) wird die HTML-Oberfläche aufgerufen und dort die Anlage gesteuert.
- Nahtlose Integration: Eingehende HTTP-Anfragen werden in einzelne Steuerzeichen (z. B.
'a'bis'z'oder'1') umgewandelt und über die zweite serielle Schnittstelle (UART2) an den Mega weitergeleitet.
2. Schaltplan
Um den ESP32 sicher mit dem Arduino Mega zu verbinden und damit eine Kommunikation zwischen den beiden Boards zu ermöglichen, ist ein Logik-Level-Konverter unverzichtbar, da die beiden Mikrocontroller unterschiedliche Spannungspegel für ihre UART-Schnittstellen verwenden. Der ESP32 arbeitet mit 3,3 V-Logik, während der Mega seine serielle Kommunikation mit 5 V anlegt. Würde man die Signale direkt verbinden, könnte der höhere Pegel des Mega den ESP32 beschädigen. Der Level-Konverter – etwa ein bidirektionales Modul auf MOSFET- oder TXB0108-Basis – stellt sicher, dass die TX-Leitung des ESP32 auf 5 V erhöht wird, bevor sie zum Mega wandert, und umgekehrt das 5-V-Signal des Mega auf verträgliche 3,3 V für den ESP32 reduziert.

Im einfachen Teilschaltplan ist daher zunächst nur der Mega, der Konverter und der ESP32 aufgeführt. Der ESP32 sendet sein TX-Signal (GPIO 17) an den Eingang LV1 des Konverters, dieser wandelt auf 5 V um und leitet es an den RX-Pin des Mega (Pin 17) weiter. Alle Bausteine teilen sich dabei unbedingt eine gemeinsame Masse, damit die Pegel korrekt „bezogen“ werden und kein Rauschpotenzial entsteht.

Im umfassenden Schaltplan sind zusätzlich die Spannungsversorgung und sämtliche Peripherie-Komponenten dargestellt. Ein 5-V-Netzteil speist den Mega und die Relais-Module zur Ansteuerung der Weichen- und Lichtmodule. Der ESP32 bezieht seine Energie aus demselben Netzteil über seinen On-Board-Regler und teilt sich ebenfalls die Masse. Vom Mega aus führen Digitalleitungen zu den Relaiskarten, die jeweils einen Servo oder LED-Strang schalten. Die Relais werden dabei kurzzeitig auf LOW gezogen, um den Schaltvorgang auszulösen, und danach wieder auf HIGH gelegt, um in die Ruheposition zurückzukehren.
Diese zweistufige Verkabelung – zuerst die reine UART-Verbindung mit Level-Konverter, anschließend die vollständige Verschaltung mit Stromversorgung und Relaistreibern – gewährleistet eine robuste und sichere Kommunikation zwischen ESP32 und Mega sowie eine zuverlässige Versorgung aller Module. Im nächsten Kapitel widmen wir uns der Gestaltung und Ablage der HTML-Oberfläche im SPIFFS des ESP32.

3. Weboberfläche mit HTML, CSS und Javascript
Ihr kennt bereits den Processing-Sketch, in dem alle Gleisgrafiken, Weichenstellungen und Lichtzustände über Canvas-Zeichnungen und Maus- bzw. Tastatur-Events gesteuert wurden. Im Browser wollen wir genau das gleiche erreichen – und zwar ohne Processing, nur mit Standard-Webtechnologien.
Dabei habe ich die gesamte Logik und die exakten Koordinaten aus dem Processing-Sketch direkt übernommen und lediglich statt der Java-Canvas-API das HTML-Canvas genutzt. Anstelle von mousePressed() und keyPressed() fange ich alle Nutzeraktionen über herkömmliche DOM-Elemente (<button>) und JavaScript-Event-Listener ab. Den seriellen Schreibaufruf Serial.write('a') ersetze ich durch eine einfache HTTP-Anfrage über fetch('/cmd?c=a'), sodass der ESP32 die Steuerbefehle über das Netzwerk weiterleitet. Das präzise Positionieren der Buttons über dem Canvas realisiere ich ausschließlich mit CSS – so kann ich die komplette Oberfläche nicht nur pixelgenau abbilden, sondern sie dank flexibler Media-Queries gleichzeitig auch responsive gestalten.
<button id="W12" class="btn">W1/2</button>Im JavaScript binde ich für jeden Button zunächst einen Event-Listener, der beim Klick die interne Zustandvariable umschaltet, das passende Steuerzeichen ermittelt und es direkt per HTTP-Request an den ESP32 schickt. So sieht das etwa für die Weiche 1/2 aus:
function toggleW12() {
// Zustand toggeln
W12State = !W12State;
// Steuerzeichen 'a' (links) oder 'b' (rechts) festlegen
const cmd = W12State ? "a" : "b";
// HTTP-Request an den ESP32-Webserver
fetch('/cmd?c=' + cmd);
// Meldung ins Log schreiben und UI aktualisieren
updateLog("W12 nach " + (W12State ? "links" : "rechts"));
updateMobileHeader();
}
document.getElementById("W12").addEventListener("click", toggleW12);
Das fetch('/cmd?c=' + cmd) ersetzt also das früher im Sketch verwendete Serial.write(cmd) und übergibt das Zeichen als URL-Parameter c. Der Server antwortet kurz mit "OK", ohne die Oberfläche zu blockieren. Für die Lichtmodule nutze ich dieselbe Struktur, nur mit anderen Zeichen:
function toggleLight(index) {
L1State = !L1State; // Beispiel für Modul 1
const cmd = L1State ? 'o' : 'p';
fetch('/cmd?c=' + cmd);
updateLog("Lichtmodul 1 " + (L1State ? "aktiviert" : "deaktiviert"));
btnL1.className = "btn " + (L1State ? "lightOn" : "lightOff");
}
document.getElementById("L1").addEventListener("click", () => toggleLight(1));Auf diese Weise sind alle Klicks und Tastendrücke (über zusätzliche keydown-Listener) nahtlos in HTTP-Kommandos umgewandelt, die der ESP32 per Serial2.write() an den Mega weiterreicht.

Mittels CSS-Code kann die Darstellung responsive gestaltet werden, sodass sich das Layout automatisch an verschiedene Bildschirmgrößen und -auflösungen anpasst und auf allen Geräten (Smartphone, Tablet, Desktop) optimal lesbar und bedienbar bleibt.
4. Webserver am ESP32 einrichten
Beim ESP32 setze ich den Webserver in fünf klaren Schritten auf – jeweils unterlegt von kurzen Code-Snippets aus meinem Sketch:
SPIFFS mounten
Damit der ESP32 eure HTML-, CSS- und JavaScript-Dateien ausliefern kann, mounte ich das interne Flash-Dateisystem. Schlägt das Mounten fehl, halte ich das Programm an und gebe eine Fehlermeldung aus.
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS Mount failed");
while (1) delay(1000);
}WLAN-Access-Point starten
Mit wenigen Zeilen schalte ich den ESP32 in den AP-Modus, vergebe SSID und Passwort und lese mir die zugewiesene IP aus – später auch über mDNS unter stellwerk.local erreichbar.
WiFi.mode(WIFI_AP);
WiFi.softAP(ssid, password);
Serial.print("AP-IP: ");
Serial.println(WiFi.softAPIP());
if (!MDNS.begin("stellwerk")) {
Serial.println("mDNS-Start fehlgeschlagen");
while (1) delay(1000);
}Statische Dateien ausliefern
Jeder Aufruf von http://stellwerk.local liefert die i aus SPIFFS. Dahinter folgt automatisch das Nachladen von CSS und JS.ndex.html
server.on("/", HTTP_GET, []() {
File f = SPIFFS.open("/index.html", "r");
server.streamFile(f, "text/html");
f.close();
});Kommando-Route /cmd
Klicks im Browser mappe ich auf URL-Parameter c. Der Handler prüft, ob ?c= gesetzt ist, wandelt das erste Zeichen in ein char um und schickt es per UART2 an den Mega.
server.on("/cmd", HTTP_GET, []() {
if (server.hasArg("c")) {
char cmd = server.arg("c").charAt(0);
Serial2.write(cmd);
}
server.send(200, "text/plain", "OK");
});Server starten & Anfragen verarbeiten
Mit server.begin() nehme ich den Dienst auf – und in der loop()-Schleife genügt ein einziger Aufruf, um alle anstehenden HTTP-Requests zu bedienen.
server.begin();
Serial.println("HTTP-Server gestartet");
// …
void loop() {
server.handleClient();
}Damit ihr nicht jedes Mal die IP-Adresse eures ESP32 im Browser eingeben müsst, setze ich mDNS (Multicast DNS) ein. Dabei meldet sich der ESP32 unter einem festen Namen im lokalen Netzwerk an und beantwortet Namensanfragen automatisch. Im setup() genügt ein einziger Aufruf. Das Stellwerk wird unter http://stellwerk.local erreichbar.
// mDNS unter "stellwerk.local" registrieren
if (!MDNS.begin("stellwerk")) {
Serial.println("Fehler beim Starten von mDNS");
while (1) { delay(1000); }
}5. Upload der HTML-Seite auf den ESP32
Bevor der ESP32 eure Web-Oberfläche ausliefern kann, müssen alle Frontend-Dateien (HTML, CSS, JavaScript, evtl. Bilder) in ein spezielles Verzeichnis kopiert werden und per Upload-Tool ins SPIFFS übertragen werden. Ich stelle euch hier zwei gängige Methoden vor:
Hochladen mit dem ESP32FS-Plugin für die Arduino-IDE
- Im gleichen Verzeichnis wie eure
*.ino-Datei legt ihr einen Ordner namensdataan. Darin kommen alle Dateien, die im SPIFFS landen sollen. - ESP32FS-Plugin installieren
- Öffnet in der Arduino-IDE Sketch → Bibliothek einbinden → ZIP-Bibliothek hinzufügen und wählt das heruntergeladene
ESP32FS.zip. Diese findet ihr auf Github (link). - Nach einem Neustart der IDE findet ihr unter Werkzeuge → ESP32 Sketch Data Upload den neuen Menüpunkt.
- Öffnet in der Arduino-IDE Sketch → Bibliothek einbinden → ZIP-Bibliothek hinzufügen und wählt das heruntergeladene
- Upload ins SPIFFS
- Wählt in Werkzeuge euer ESP32-Board und den richtigen Port.
- Klickt auf ESP32 Sketch Data Upload.
- Die IDE formatiert das Flash-Dateisystem (falls nötig) und kopiert alles aus
datains SPIFFS. - In der Konsole seht ihr Fortschrittsmeldungen und eine Erfolgsmeldung am Ende.
Hinweis (Mac & aktuelle Arduino-IDE): Auf meinem aktuellen macOS ließ sich das Plugin nicht zuverlässig installieren oder lief nicht durch – in diesem Fall empfehle ich die folgende Alternative über PlatformIO.
Hochladen mit PlatformIO in VS Code
- PlatformIO-Projekt anlegen
- Installiert in VS Code die Erweiterung PlatformIO IDE.
- Legt ein neues Projekt an, wählt Board „ESP32 Dev Module“ (oder euer spezifisches ESP32-Board) und als Framework Arduino.
data-Ordner integrieren- Im Projektverzeichnis legt ihr ebenfalls einen Ordner
data(oderspiffs) an. - Kopiert eure Frontend-Dateien (
index.html,style.css,app.js) hinein.
- Im Projektverzeichnis legt ihr ebenfalls einen Ordner
platformio.inikonfigurieren- Fügt im Root der Datei folgende Zeilen hinzu, damit PlatformIO automatisch das SPIFFS-Dateisystem erkennt und bereitstellt:
- Upload ins SPIFFS
- Öffnet die PlatformIO-Leiste und klickt auf Project Tasks → env:esp32dev → Upload Filesystem Image.
- PlatformIO erstellt ein SPIFFS-Image und lädt es per esptool.py auf euren ESP32.
- Nach Erfolg seht ihr in der Konsole eine entsprechende Meldung.
- Sketch hochladen und testen
- Anschließend noch den regulären Sketch-Upload (Upload) starten, wenn ihr Änderungen im C++-Code vorgenommen habt.
- Nach dem Neustart liefert der ESP32 unter Stellwerk.local eure HTML-Seite aus.
- Sketch hochladen und testen
- Anschließend noch den regulären Sketch-Upload (Upload) starten, wenn ihr Änderungen im C++-Code vorgenommen habt.
- Nach dem Neustart liefert der ESP32 unter
/eure HTML-Seite aus.
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
board_build.filesystem = spiffs6. UART2-Kommunikation zwischen ESP32 und Arduino Mega
Damit ESP32 und Mega Befehle austauschen können, richte ich auf beiden Boards eine zweite serielle Schnittstelle (Serial2) ein – unabhängig von der USB-Debug-Verbindung (Serial). Über diese UART2-Leitung flutschen die einzelnen Steuerzeichen im Halbduplex:
ESP32 als Sender
Im setup() des ESP32 initialisiere ich UART2 mit 9600 Baud und lege RX/TX auf die Pins 16/17 fest:
// ESP32: RX2 = GPIO16, TX2 = GPIO17
Serial2.begin(9600, SERIAL_8N1, RXp2, TXp2);Immer wenn in meinem Web-Handler eine Route wie /cmd?c=a aufgerufen wird, schreibe ich das entsprechende Zeichen direkt in diese Schnittstelle:
// Beispiel aus handleW12():
char cmd = state ? 'a' : 'b';
Serial2.write(cmd);So wird aus dem Klick im Browser binnen weniger Mikrosekunden ein Byte auf TX2 gelegt.
Arduino Mega als Empfänger und Ausführer
Auch auf dem Mega boote ich UART2 mit 9600 Baud:
Serial2.begin(9600); // Hardware-Serial2In der loop()-Funktion checke ich kontinuierlich both Serial (USB) und Serial2 (UART2):
if (Serial2.available()) {
receivedChar = Serial2.read();
}Sobald ein neues Zeichen ankommt, wertet ein switch(receivedChar) es aus und ruft die passende Handler-Funktion auf, z. B.:
case 'a': // W1/2 links
handleW1_2L();
break;
case 'b': // W1/2 rechts
handleW1_2R();
break;
// … ebenso für 'c' bis 'z' und '1' (Reset)7. Weichen-Handler
Auf dem Mega laufen alle Schaltvorgänge „on the fly“, sobald ein Steuerzeichen über UART2 ankommt. Ich habe dafür jede Weichenpaarung in eine eigene Funktion ausgelagert, die kurz den entsprechenden Ausgang auf LOW zieht, um das Relais zu aktivieren, und nach einer definierten Verzögerung wieder auf HIGH setzt. Am Ende muss das gelesene Zeichen unbedingt zurückgesetzt werden, sonst würde der gleiche Befehl in der nächsten Loop-Iteration ungewollt erneut ausgeführt.
// Pins W1R und W2R gemeinsam schalten (Richtungsstellung "rechts")
void handleW1_2R() {
digitalWrite(W1R, LOW);
digitalWrite(W2R, LOW);
delay(schaltZeit);
digitalWrite(W1R, HIGH);
digitalWrite(W2R, HIGH);
}Bei den meisten Relaismodulen ist die Eingangssteuerung active-low ausgeführt: Ein HIGH-Signal lässt den Transistor auf dem Modul sperren, die Relaisspule liegt stromlos und bleibt in Ruhestellung, während ein LOW-Signal den Transistor durchschaltet, die Spule mit Masse verbindet und das Relais anzieht. Durch den internen Pull-up-Widerstand ist der Eingang im Normalzustand automatisch auf HIGH gezogen, sodass nach jedem Reset alle Relais sicher deaktiviert bleiben, und erst ein gezieltes LOW aktiviert das Schalten.
Hier sorgt delay(schaltZeit) (500 ms) dafür, dass das Magnetrelais lange genug Spannung bekommt, um umzulegen, bevor wir wieder in die Ruhestellung zurückkehren. Das ist auch ungefähr die Zeit die ein Weichenantrieb benötigt um zu schalten. Da nicht permanent Strom am Weichenantrieb anliegt entfällt auch die Feinjustierung der Endabschaltungskontakte.
Empfang und Auswertung des Steuerzeichens
Im loop() liest der Mega per Serial2.read() das ankommende Byte ein und speichert es in receivedChar. Danach wertet ein switch-Block das Zeichen aus und ruft die passende Handler-Funktion auf:
void loop() {
// Neues Zeichen aus serieller Schnittstelle holen
if (Serial2.available()) {
receivedChar = Serial2.read();
}
// Sonderfall: auch USB-Serial kann Befehle liefern
if (Serial.available()) {
receivedChar = Serial.read();
}
// Einmalige Ausführung pro neuem Zeichen
switch (receivedChar) {
case 'a': handleW1_2R(); break;
case 'b': handleW1_2L(); break;
case 'c': handleW3_4R(); break;
case 'd': handleW3_4L(); break;
// … weitere Fälle für 'e' bis 'j' …
case '1': handleReset(); break;
// … und 'o' bis 'z' für die Lichtmodule …
default:
// kein bekanntes Kommando – nichts tun
break;
}
// Sehr wichtig: Zeichen zurücksetzen,
// damit der gleiche Befehl nicht erneut abgearbeitet wird
receivedChar = 0;
}Durch das Zurücksetzen von receivedChar auf 0 stellen wir sicher, dass jede Weichenumstellung oder Lichtschaltung genau einmal pro empfangenem Zeichen ausgeführt wird. Ohne dieses Reset würde etwa bei dauerhaft verfügbarem Zeichen der zugeordnete Handler in jeder Schleife erneut anspringen und beispielsweise das Relais dauerhaft betätigen oder ständig in die gleiche Richtung schalten.
8. Fazit
Kompletten Arduino-MEGA Code ansehen
// =====================
// Pin-Definitionen
// =====================
// Weichen
const int W1R = 52;
const int W1L = 53;
const int W2R = 50;
const int W2L = 51;
const int W3R = 48;
const int W3L = 49;
const int W4R = 46;
const int W4L = 47;
const int W5R = 44;
const int W5L = 45;
const int W6R = 42;
const int W6L = 43;
const int W7R = 40;
const int W7L = 41;
// Lichtmodule
const int L1 = 39;
const int L2 = 38;
const int L3 = 37;
const int L4 = 36;
const int L5 = 35;
const int L6 = 34;
// Verzögerung beim Schalten (ms)
const int schaltZeit = 500;
char receivedChar = 0;
// =====================
// Weichen-Handler
// =====================
void handleW1_2R() {
digitalWrite(W1R, LOW);
digitalWrite(W2R, LOW);
delay(schaltZeit);
digitalWrite(W1R, HIGH);
digitalWrite(W2R, HIGH);
}
void handleW1_2L() {
digitalWrite(W1L, LOW);
digitalWrite(W2L, LOW);
delay(schaltZeit);
digitalWrite(W1L, HIGH);
digitalWrite(W2L, HIGH);
}
void handleW3_4R() {
digitalWrite(W3R, LOW);
digitalWrite(W4R, LOW);
delay(schaltZeit);
digitalWrite(W3R, HIGH);
digitalWrite(W4R, HIGH);
}
void handleW3_4L() {
digitalWrite(W3L, LOW);
digitalWrite(W4L, LOW);
delay(schaltZeit);
digitalWrite(W3L, HIGH);
digitalWrite(W4L, HIGH);
}
void handleW5R() {
digitalWrite(W5R, LOW);
delay(schaltZeit);
digitalWrite(W5R, HIGH);
}
void handleW5L() {
digitalWrite(W5L, LOW);
delay(schaltZeit);
digitalWrite(W5L, HIGH);
}
void handleW6R() {
digitalWrite(W6R, LOW);
delay(schaltZeit);
digitalWrite(W6R, HIGH);
}
void handleW6L() {
digitalWrite(W6L, LOW);
delay(schaltZeit);
digitalWrite(W6L, HIGH);
}
void handleW7R() {
digitalWrite(W7R, LOW);
delay(schaltZeit);
digitalWrite(W7R, HIGH);
}
void handleW7L() {
digitalWrite(W7L, LOW);
delay(schaltZeit);
digitalWrite(W7L, HIGH);
}
// Reset aller Weichen in festgelegter Reihenfolge
void handleReset() {
handleW1_2R();
handleW3_4R();
handleW5R();
handleW6R();
handleW7R();
}
// =====================
// Licht-Handler
// =====================
void handleLightOff(int pin) {
digitalWrite(pin, LOW);
}
void handleLightOn(int pin) {
digitalWrite(pin, HIGH);
}
// =====================
// Setup
// =====================
void setup() {
// Alle Weichen-Pins als Ausgang, initial HIGH (Ruhestellung)
pinMode(W1R, OUTPUT); pinMode(W1L, OUTPUT);
pinMode(W2R, OUTPUT); pinMode(W2L, OUTPUT);
pinMode(W3R, OUTPUT); pinMode(W3L, OUTPUT);
pinMode(W4R, OUTPUT); pinMode(W4L, OUTPUT);
pinMode(W5R, OUTPUT); pinMode(W5L, OUTPUT);
pinMode(W6R, OUTPUT); pinMode(W6L, OUTPUT);
pinMode(W7R, OUTPUT); pinMode(W7L, OUTPUT);
digitalWrite(W1R, HIGH); digitalWrite(W1L, HIGH);
digitalWrite(W2R, HIGH); digitalWrite(W2L, HIGH);
digitalWrite(W3R, HIGH); digitalWrite(W3L, HIGH);
digitalWrite(W4R, HIGH); digitalWrite(W4L, HIGH);
digitalWrite(W5R, HIGH); digitalWrite(W5L, HIGH);
digitalWrite(W6R, HIGH); digitalWrite(W6L, HIGH);
digitalWrite(W7R, HIGH); digitalWrite(W7L, HIGH);
// Alle Licht-Pins als Ausgang, initial HIGH (an)
pinMode(L1, OUTPUT); pinMode(L2, OUTPUT);
pinMode(L3, OUTPUT); pinMode(L4, OUTPUT);
pinMode(L5, OUTPUT); pinMode(L6, OUTPUT);
digitalWrite(L1, HIGH); digitalWrite(L2, HIGH);
digitalWrite(L3, HIGH); digitalWrite(L4, HIGH);
digitalWrite(L5, HIGH); digitalWrite(L6, HIGH);
Serial.begin(9600);
Serial2.begin(9600); // ESP32: RX2=17, TX2=16
}
// =====================
// Loop
// =====================
void loop() {
// eingehendes Zeichen einlesen
if (Serial.available()) receivedChar = Serial.read();
if (Serial2.available()) receivedChar = Serial2.read();
// nur einmal pro neuem Zeichen ausführen
switch (receivedChar) {
case 'a': handleW1_2R(); break;
case 'b': handleW1_2L(); break;
case 'c': handleW3_4R(); break;
case 'd': handleW3_4L(); break;
case 'e': handleW5L(); break;
case 'f': handleW5R(); break;
case 'g': handleW6L(); break;
case 'h': handleW6R(); break;
case 'i': handleW7L(); break;
case 'j': handleW7R(); break;
case '1': handleReset(); break;
case 'o': handleLightOff(L1); break;
case 'p': handleLightOn (L1); break;
case 'q': handleLightOff(L2); break;
case 'r': handleLightOn (L2); break;
case 's': handleLightOff(L3); break;
case 't': handleLightOn (L3); break;
case 'u': handleLightOff(L4); break;
case 'v': handleLightOn (L4); break;
case 'w': handleLightOff(L5); break;
case 'x': handleLightOn (L5); break;
case 'y': handleLightOff(L6); break;
case 'z': handleLightOn (L6); break;
default:
// kein neues Kommando
break;
}
// Zeichen zurücksetzen, damit es nicht erneut abgearbeitet wird
receivedChar = 0;
}Kompletten ESP32-Code ansehen
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include "SPIFFS.h"
#define RXp2 16 // ESP32 empfängt hier UNO-TX
#define TXp2 17 // ESP32 sendet hier an UNO-RX
const char* ssid = "stellwerk";
const char* password = "stellwerk_moba";
WebServer server(80);
// Entfernt Zeilen, die mit "\n#line" beginnen (falls du das noch brauchst)
void removeLineDirectives(String& html) {
int index = 0;
while ((index = html.indexOf("\n#line")) != -1) {
int endIndex = html.indexOf('\n', index + 1);
if (endIndex == -1) break;
html.remove(index, endIndex - index);
}
}
void setup() {
Serial2.begin(9600, SERIAL_8N1, RXp2, TXp2); // UART mit UNO
Serial.begin(115200);
// SPIFFS initialisieren (true = formatieren, falls noch kein Image vorhanden)
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS Mount failed");
return;
}
// Kontrolle, ob die Datei wirklich da ist
if (!SPIFFS.exists("/index.html")) {
Serial.println("Datei /index.html existiert nicht!");
}
// WLAN-AP starten
WiFi.mode(WIFI_AP);
WiFi.softAP(ssid, password);
Serial.println("Access Point gestartet!");
Serial.print("IP-Adresse: ");
Serial.println(WiFi.softAPIP());
// mDNS („stellwerk.local“) starten
if (!MDNS.begin("stellwerk")) {
Serial.println("Fehler beim Starten von mDNS");
while (1) { delay(1000); }
}
// HTTP-Handler: Root-Route liefert index.html aus SPIFFS
server.on("/", HTTP_GET, []() {
File file = SPIFFS.open("/index.html", "r");
if (!file) {
server.send(404, "text/plain", "Datei nicht gefunden");
return;
}
server.streamFile(file, "text/html");
file.close();
});
// Handler für Kommandos über /cmd?c=…
server.on("/cmd", HTTP_GET, []() {
if (server.hasArg("c")) {
String cmdStr = server.arg("c");
char cmd = cmdStr.charAt(0);
switch(cmd) {
case 'a': // W12 links
handleW12(true);
break;
case 'b': // W12 rechts
handleW12(false);
break;
case 'c': // W34 links
handleW34(true);
break;
case 'd': // W34 rechts
handleW34(false);
break;
case 'e': // W5 links
handleW5(true);
break;
case 'f': // W5 rechts
handleW5(false);
break;
case 'g': // W6 links
handleW6(true);
break;
case 'h': // W6 rechts
handleW6(false);
break;
case 'i': // W7 links
handleW7(true);
break;
case 'j': // W7 rechts
handleW7(false);
break;
case 'o': // Lichtmodul 1 aktivieren
handleLight(1, true);
break;
case 'p': // Lichtmodul 1 deaktivieren
handleLight(1, false);
break;
case 'q': // Lichtmodul 2 aktivieren
handleLight(2, true);
break;
case 'r': // Lichtmodul 2 deaktivieren
handleLight(2, false);
break;
case 's': // Lichtmodul 3 aktivieren
handleLight(3, true);
break;
case 't': // Lichtmodul 3 deaktivieren
handleLight(3, false);
break;
case 'u': // Lichtmodul 4 aktivieren
handleLight(4, true);
break;
case 'v': // Lichtmodul 4 deaktivieren
handleLight(4, false);
break;
case 'w': // Lichtmodul 5 aktivieren
handleLight(5, true);
break;
case 'x': // Lichtmodul 5 deaktivieren
handleLight(5, false);
break;
case 'y': // Lichtmodul 6 aktivieren
handleLight(6, true);
break;
case 'z': // Lichtmodul 6 deaktivieren
handleLight(6, false);
break;
case 'R': // Reset-Befehl
handleReset();
break;
default:
Serial.println("Unbekannter Befehl empfangen: " + String(cmd));
break;
}
}
server.send(200, "text/plain", "OK");
});
server.begin();
Serial.println("HTTP-Server gestartet");
}
void handleW12(bool state) {
// State-String fürs Log
String stateStr = state ? "links" : "rechts";
Serial.println("W12: Zustand auf " + stateStr);
// Je nach state entweder 'a' (links) oder 'b' (rechts) senden
char cmd = state ? 'a' : 'b';
Serial2.write(cmd);
}
void handleW34(bool state) {
String stateStr = state ? "links" : "rechts";
Serial.println("W34: Zustand auf " + stateStr);
// 'c' = W34 links, 'd' = W34 rechts
char cmd = state ? 'c' : 'd';
Serial2.write(cmd);
}
void handleW5(bool state) {
String stateStr = state ? "links" : "rechts";
Serial.println("W5: Zustand auf " + stateStr);
// 'e' = W5 links, 'f' = W5 rechts
char cmd = state ? 'e' : 'f';
Serial2.write(cmd);
}
void handleW6(bool state) {
String stateStr = state ? "links" : "rechts";
Serial.println("W6: Zustand auf " + stateStr);
// 'g' = W6 links, 'h' = W6 rechts
char cmd = state ? 'g' : 'h';
Serial2.write(cmd);
}
void handleW7(bool state) {
String stateStr = state ? "links" : "rechts";
Serial.println("W7: Zustand auf " + stateStr);
// 'i' = W7 links, 'j' = W7 rechts
char cmd = state ? 'i' : 'j';
Serial2.write(cmd);
}
void handleLight(int module, bool state) {
String stateStr = state ? "aktiviert" : "deaktiviert";
Serial.println("Lichtmodul " + String(module) + " " + stateStr);
// Befehlsbuchstaben für Lichtmodule 1–6:
// Modul 1: o/p, 2: q/r, 3: s/t, 4: u/v, 5: w/x, 6: y/z
const char tableOn[] = { 'o','q','s','u','w','y' };
const char tableOff[] = { 'p','r','t','v','x','z' };
if (module >= 1 && module <= 6) {
char cmd = state ? tableOn[module-1] : tableOff[module-1];
Serial2.write(cmd);
}
}
void handleReset() {
Serial.println("Reset: Zustände wurden zurückgesetzt");
// Reset-Kommando
Serial2.write('1');
// Weitere Reset-Aktionen können hier ergänzt werden.
}
void loop() {
server.handleClient();
}Kompletten HTML-Code ansehen
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<!-- Wichtig für mobiles Rendering -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stellwerk V3</title>
<style>
/* Allgemeine Einstellungen */
* {
font-family: 'Roboto', sans-serif;
}
body {
margin: 0;
padding: 0;
background: rgb(0, 0, 0);
font-family: 'Roboto', sans-serif;
}
/* Desktop-Container: exakte Größe wie im Processing-Sketch */
#desktopContainer {
position: relative;
height: 100vh;
margin: 0 auto;
overflow: hidden;
}
/* Canvas und Buttons im Desktop-Modus */
#desktopContainer canvas {
position: absolute;
top: -40px;
left: 0;
z-index: 1;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
z-index: 1;
}
#desktopContainer .btn {
position: absolute;
z-index: 2;
font-size: 16px;
font-weight: bold;
border: none;
cursor: pointer;
}
/* Positionen der Weichen-Buttons */
#reset {
left: 30px;
top: 690px;
width: 60px;
height: 40px;
}
#desktopContainer canvas {
position: absolute;
left: 50%;
transform: translate(-50%, 0);
z-index: 1;
}
/* Angepasste Positionierung der Weichen-Buttons relativ zum zentrierten Canvas */
#reset {
left: calc(50% - 575px + 30px);
top: 690px;
width: 60px;
height: 40px;
}
#W5 {
left: calc(50% - 575px + 650px);
top: 140px;
width: 60px;
height: 40px;
}
#W6 {
left: calc(50% - 566px + 344px);
top: 310px;
width: 60px;
height: 40px;
}
#W12 {
left: calc(50% - 575px + 450px);
top: 540px;
width: 60px;
height: 40px;
}
#W34 {
left: calc(50% - 575px + 640px);
top: 540px;
width: 60px;
height: 40px;
}
#W7 {
left: calc(50% - 575px + 510px);
top: 290px;
width: 60px;
height: 40px;
}
/* Positionen der Lichtmodul-Buttons */
/* Entferne bisherige Positionsangaben der Lichtmodule */
#L1,
#L2,
#L3,
#L4,
#L5,
#L6 {
position: static;
width: 130px;
/* oder eine feste Breite, wenn gewünscht */
height: 40px;
/* behält die Höhe der Buttons */
}
/* Container für Lichtmodule in der Desktop-Ansicht */
#lightButtons {
position: absolute;
bottom: 20px;
/* Abstand vom unteren Rand des Containers */
left: 50%;
/* Zentriert horizontal */
transform: translateX(-50%);
display: flex;
gap: 10px;
/* Abstand zwischen den Buttons */
padding: 0 10px;
z-index: 2;
}
/* Optional: Style-Anpassungen für die Buttons im Container */
#lightButtons .btn {
/* Hier kannst Du noch weitere Anpassungen vornehmen, z.B.: */
/* flex: 1; */
}
/* Farbklassen für Lichtmodul-Buttons */
.lightOff {
background-color: rgb(255, 0, 0);
color: black;
}
.lightOn {
background-color: rgb(0, 255, 0);
color: black;
}
/* Mobile-Container: standardmäßig ausgeblendet */
#mobileContainer {
display: none;
padding: 15px;
margin: 15px;
border-radius: 10px;
background: linear-gradient(45deg, #3e3e3e, #1a1a1a);
}
/* Mobile Header mit kleinen Weichen-Indikatoren */
#mobileHeader {
display: flex;
justify-content: space-around;
align-items: center;
margin-bottom: 20px;
margin-top: 10px;
}
#mobileHeader .switch {
width: 30px;
height: 30px;
border-radius: 50%;
background: red;
transition: transform 0.3s ease, background-color 0.3s ease;
}
#mobileHeader .switch.active {
background: green;
transform: rotate(45deg);
}
/* Mobile Layout: Zwei Gruppen – Weichen- und Lichtbuttons */
#mobileContainer .weichen-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
#mobileContainer .licht-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
/* Mobile-Buttons: volle Breite */
#mobileContainer .btn {
width: 100%;
height: 40px;
font-size: 16px;
font-weight: bold;
border: none;
height: 6vh;
cursor: pointer;
}
#header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: rgb(0, 0, 0);
position: fixed;
width: 100%;
z-index: 2;
}
#desktopContainer #lightButtons .btn {
position: static;
}
@media (min-width: 60rem) {
#m-h1 {
position: relative;
color: rgb(255, 255, 255);
font-size: 1rem;
text-align: left;
font-weight: lighter;
z-index: 2;
margin: 10px 0px -10px 20px
}
#m-h1 {
color: rgb(255, 255, 255);
font-size: 1.6rem;
text-align: left;
font-weight: normal;
font-weight: lighter;
margin: 10px 0px 0px 20px
}
#logo {
position: absolute;
height: 50px;
top: 10px;
right: 30px;
z-index: 2;
}
}
/* Media Query: Bei Bildschirmbreiten ≤60rem (ca. 960px) */
@media (max-width: 60rem) {
#desktopContainer {
display: block;
width: 100%;
height: auto;
margin: 0;
position: relative;
}
#desktopContainer canvas {
width: 100%;
height: auto;
position: relative;
z-index: 0;
top: 0;
padding: 30px 0 0 0;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.3);
/* Schatten hinzugefügt */
/* Hintergrund ggf. setzen, falls nötig */
}
/* Falls du die Desktop-Buttons nicht in der mobilen Ansicht brauchst */
#desktopContainer .btn {
display: none;
}
#mobileContainer {
display: block;
padding-top: 10px;
}
#m-h1 {
color: rgb(255, 255, 255);
font-size: 1rem;
text-align: left;
font-weight: normal;
font-weight: lighter;
margin: 10px 0px 0px 20px
}
.btn {
border-radius: 5px;
}
#m-reset {
background-color: rgb(137, 137, 137);
color: black;
}
#logo {
position: absolute;
height: 30px;
top: 10px;
right: 20px;
z-index: 1;
}
#canvas {
top: 0;
}
}
</style>
</head>
<body>
<!-- Desktop-Ansicht (wie zuvor) -->
<div id="desktopContainer">
<div id="header">
<h1 id="m-h1">Stellwerk V3</h1>
<img id="logo" src="https://www.sontak.de/Bilder/Logo_s.png" alt="">
</div>
<canvas id="canvas" width="1150" height="768"></canvas>
<!-- Buttons für Weichen -->
<button id="W5" class="btn">W5</button>
<button id="W6" class="btn">W6</button>
<button id="W12" class="btn">W1/2</button>
<button id="W34" class="btn">W3/4</button>
<button id="W7" class="btn">W7</button>
<!-- Container für Lichtmodule -->
<div id="lightButtons">
<button id="reset" class="btn">reset</button>
<button id="L1" class="btn lightOff">Lichtmodul 1</button>
<button id="L2" class="btn lightOff">Lichtmodul 2</button>
<button id="L3" class="btn lightOff">Lichtmodul 3</button>
<button id="L4" class="btn lightOff">Lichtmodul 4</button>
<button id="L5" class="btn lightOff">Lichtmodul 5</button>
<button id="L6" class="btn lightOff">Lichtmodul 6</button>
</div>
</div>
<!-- Mobile-Ansicht: Header mit Weichenstatus + Buttons in neuem Layout -->
<div id="mobileContainer">
<!-- Header: Anzeige der Weichen-Zustände als kleine Kreise -->
<div class="weichen-buttons">
<button id="m-W12" class="btn">W1/2</button>
<button id="m-W34" class="btn">W3/4</button>
<button id="m-W5" class="btn">W5</button>
<button id="m-W6" class="btn">W6</button>
<button id="m-W7" class="btn">W7</button>
<button id="m-reset" class="btn">reset</button>
</div>
</div>
<div id="mobileContainer">
<div class="licht-buttons">
<button id="m-L1" class="btn lightOff">Lichtmodul 1</button>
<button id="m-L2" class="btn lightOff">Lichtmodul 2</button>
<button id="m-L3" class="btn lightOff">Lichtmodul 3</button>
<button id="m-L4" class="btn lightOff">Lichtmodul 4</button>
<button id="m-L5" class="btn lightOff">Lichtmodul 5</button>
<button id="m-L6" class="btn lightOff">Lichtmodul 6</button>
</div>
</div>
<script>
// Zustände (wie im Processing-Sketch)
let W12State = true;
let W34State = true;
let W5State = false;
let W6State = false;
let W7State = false;
let L1State = false, L2State = false, L3State = false, L4State = false, L5State = false, L6State = false;
let latestLog = "Betriebsbereit";
// Farben
const colorOn = "rgb(0,255,0)";
const colorOff = "rgb(255,0,0)";
const greenColor = "rgb(66,161,0)";
const redColor = "rgba(100,0,0,1)";
const nLineColor = "rgb(200,200,200)";
const backColor = "rgb(0, 0, 0)";
// Funktion, um den Header in der mobilen Ansicht zu aktualisieren
function updateMobileHeader() {
// Für jede Weiche wird der entsprechende Statuskreis (z. B. status-W12) aktiviert oder deaktiviert.
document.getElementById("status-W12").classList.toggle("active", W12State);
document.getElementById("status-W34").classList.toggle("active", W34State);
document.getElementById("status-W5").classList.toggle("active", W5State);
document.getElementById("status-W6").classList.toggle("active", W6State);
document.getElementById("status-W7").classList.toggle("active", W7State);
}
// Canvas & Kontext (nur in Desktop-Ansicht vorhanden)
const canvas = document.getElementById("canvas");
if (canvas) {
const ctx = canvas.getContext("2d");
// Mauskoordinaten
let mouseX = 0, mouseY = 0;
/* canvas.addEventListener("mousemove", function (e) {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
}); */
// Log-Funktion
function updateLog(message) {
latestLog = message;
}
// Zustände (wie im Processing-Sketch)
let W12State = true;
let W34State = true;
let W5State = false;
let W6State = false;
let W7State = false;
let L1State = false, L2State = false, L3State = false, L4State = false, L5State = false, L6State = false;
let latestLog = "Betriebsbereit";
// Farben
const colorOn = "rgb(0,255,0)";
const colorOff = "rgb(255,0,0)";
const greenColor = "rgb(66,161,0)";
const redColor = "rgba(100,0,0,1)";
const nLineColor = "rgb(200,200,200)";
const backColor = "rgb(0, 0, 0)";
// Funktion, um den Header in der mobilen Ansicht zu aktualisieren
function updateMobileHeader() {
document.getElementById("status-W12")?.classList.toggle("active", W12State);
document.getElementById("status-W34")?.classList.toggle("active", W34State);
document.getElementById("status-W5")?.classList.toggle("active", W5State);
document.getElementById("status-W6")?.classList.toggle("active", W6State);
document.getElementById("status-W7")?.classList.toggle("active", W7State);
}
// Beispiel: Toggle-Funktion für W12 (Weiche 1/2)
function toggleW12() {
W12State = !W12State;
// Sende hier das entsprechende Zeichen: 'a' wenn true, 'b' wenn false
let cmd = W12State ? "a" : "b";
console.log("Weiche 1/2 schalten, sende das Zeichen " + cmd);
updateLog("W12 nach " + (W12State ? "links" : "rechts"));
updateMobileHeader();
}
function toggleW34() {
W34State = !W34State;
let cmd = W34State ? "c" : "d";
console.log("Weiche 3/4 schalten, sende das Zeichen " + cmd);
updateLog("W34 nach " + (W34State ? "links" : "rechts"));
updateMobileHeader();
}
function toggleW5() {
W5State = !W5State;
let cmd = W5State ? "e" : "f";
console.log("Weiche 5 schalten, sende das Zeichen " + cmd);
updateLog("W5 nach " + (W5State ? "links" : "rechts"));
updateMobileHeader();
}
function toggleW6() {
W6State = !W6State;
let cmd = W6State ? "g" : "h";
console.log("Weiche 6 schalten, sende das Zeichen " + cmd);
updateLog("W6 nach " + (W6State ? "links" : "rechts"));
updateMobileHeader();
}
function toggleW7() {
W7State = !W7State;
let cmd = W7State ? "i" : "j";
console.log("Weiche 7 schalten, sende das Zeichen " + cmd);
updateLog("W7 nach " + (W7State ? "links" : "rechts"));
updateMobileHeader();
}
// Toggle-Funktion für die Lichtmodule (entsprechend der Buchstabenlogik in deiner Processing-Basis)
function toggleLight(index) {
let cmd;
switch (index) {
case 1:
L1State = !L1State;
cmd = L1State ? 'o' : 'p';
console.log("Lichtmodul 1 schalten, sende das Zeichen " + cmd);
updateLog("Lichtmodul 1 " + (L1State ? "aktiviert" : "deaktiviert"));
document.getElementById("L1").className = "btn " + (L1State ? "lightOn" : "lightOff");
if (document.getElementById("m-L1"))
document.getElementById("m-L1").className = "btn " + (L1State ? "lightOn" : "lightOff");
break;
case 2:
L2State = !L2State;
cmd = L2State ? 'q' : 'r';
console.log("Lichtmodul 2 schalten, sende das Zeichen " + cmd);
updateLog("Lichtmodul 2 " + (L2State ? "aktiviert" : "deaktiviert"));
document.getElementById("L2").className = "btn " + (L2State ? "lightOn" : "lightOff");
if (document.getElementById("m-L2"))
document.getElementById("m-L2").className = "btn " + (L2State ? "lightOn" : "lightOff");
break;
case 3:
L3State = !L3State;
cmd = L3State ? 's' : 't';
console.log("Lichtmodul 3 schalten, sende das Zeichen " + cmd);
updateLog("Lichtmodul 3 " + (L3State ? "aktiviert" : "deaktiviert"));
document.getElementById("L3").className = "btn " + (L3State ? "lightOn" : "lightOff");
if (document.getElementById("m-L3"))
document.getElementById("m-L3").className = "btn " + (L3State ? "lightOn" : "lightOff");
break;
case 4:
L4State = !L4State;
cmd = L4State ? 'u' : 'v';
console.log("Lichtmodul 4 schalten, sende das Zeichen " + cmd);
updateLog("Lichtmodul 4 " + (L4State ? "aktiviert" : "deaktiviert"));
document.getElementById("L4").className = "btn " + (L4State ? "lightOn" : "lightOff");
if (document.getElementById("m-L4"))
document.getElementById("m-L4").className = "btn " + (L4State ? "lightOn" : "lightOff");
break;
case 5:
L5State = !L5State;
cmd = L5State ? 'w' : 'x';
console.log("Lichtmodul 5 schalten, sende das Zeichen " + cmd);
updateLog("Lichtmodul 5 " + (L5State ? "aktiviert" : "deaktiviert"));
document.getElementById("L5").className = "btn " + (L5State ? "lightOn" : "lightOff");
if (document.getElementById("m-L5"))
document.getElementById("m-L5").className = "btn " + (L5State ? "lightOn" : "lightOff");
break;
case 6:
L6State = !L6State;
cmd = L6State ? 'y' : 'z';
console.log("Lichtmodul 6 schalten, sende das Zeichen " + cmd);
updateLog("Lichtmodul 6 " + (L6State ? "aktiviert" : "deaktiviert"));
document.getElementById("L6").className = "btn " + (L6State ? "lightOn" : "lightOff");
if (document.getElementById("m-L6"))
document.getElementById("m-L6").className = "btn " + (L6State ? "lightOn" : "lightOff");
break;
}
}
// Reset-Funktion: setzt alle Weichen zurück und loggt den Reset
function resetAll() {
W12State = true;
W34State = true;
W5State = false;
W6State = false;
W7State = false;
console.log("Reset: Zustände zurückgesetzt");
updateLog("Reset durchgeführt");
updateMobileHeader();
}
// Desktop-Button-Events binden
document.getElementById("reset").addEventListener("click", resetAll);
document.getElementById("W5").addEventListener("click", toggleW5);
document.getElementById("W6").addEventListener("click", toggleW6);
document.getElementById("W12").addEventListener("click", toggleW12);
document.getElementById("W34").addEventListener("click", toggleW34);
document.getElementById("W7").addEventListener("click", toggleW7);
document.getElementById("L1").addEventListener("click", function () { toggleLight(1); });
document.getElementById("L2").addEventListener("click", function () { toggleLight(2); });
document.getElementById("L3").addEventListener("click", function () { toggleLight(3); });
document.getElementById("L4").addEventListener("click", function () { toggleLight(4); });
document.getElementById("L5").addEventListener("click", function () { toggleLight(5); });
document.getElementById("L6").addEventListener("click", function () { toggleLight(6); });
// Tastaturkürzel (entspricht keyPressed im Processing)
document.addEventListener("keydown", function (e) {
if (e.key === '1' || e.key === '2') {
toggleW12();
} else if (e.key === '3' || e.key === '4') {
toggleW34();
} else if (e.key === '5') {
toggleW5();
} else if (e.key === '6') {
toggleW6();
} else if (e.key === '7') {
toggleW7();
} else if (e.key === 'r' || e.key === 'R') {
resetAll();
}
});
// Desktop-Zeichnungsfunktion (Canvas)
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = backColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Mauskoordinaten
ctx.fillStyle = "white";
ctx.font = "15px Calibri";
/*
ctx.fillText("Mouse X: " + mouseX, 30, 50);
ctx.fillText("Mouse Y: " + mouseY, 30, 70);
*/
// Log-Bereich
ctx.fillStyle = backColor;
ctx.fillRect(0, 730, 1366, 38);
ctx.fillStyle = "white";
//ctx.fillText(latestLog, 30, 750);
// Obere Leiste und Titel
ctx.fillStyle = "white";
ctx.font = "20px Calibri";
// Stellwerk V3", 30, 23);
let now = new Date();
//let timeStr = ("0" + now.getHours()).slice(-2) + ":" + ("0" + now.getMinutes()).slice(-2) + ":" + ("0" + now.getSeconds()).slice(-2);
//ctx.fillText(timeStr, 1250, 23);
// ctx.fillText("Lichtsteuerung", 1150, 100);
// Zeichnen der Weichen (dicke, abgerundete Linien)
ctx.lineWidth = 15;
ctx.lineCap = "round";
// W12
if (W12State) {
ctx.strokeStyle = redColor;
ctx.beginPath();
ctx.moveTo(350, 600);
ctx.lineTo(430, 600);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(490, 700);
ctx.lineTo(410, 700);
ctx.stroke();
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(350, 600);
ctx.lineTo(420, 650);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(420, 650);
ctx.lineTo(490, 700);
ctx.stroke();
} else {
ctx.strokeStyle = redColor;
ctx.beginPath();
ctx.moveTo(350, 600);
ctx.lineTo(420, 650);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(420, 650);
ctx.lineTo(490, 700);
ctx.stroke();
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(350, 600);
ctx.lineTo(430, 600);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(490, 700);
ctx.lineTo(410, 700);
ctx.stroke();
}
// W34
if (W34State) {
ctx.strokeStyle = redColor;
ctx.beginPath();
ctx.moveTo(800, 600);
ctx.lineTo(720, 600);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(660, 700);
ctx.lineTo(740, 700);
ctx.stroke();
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(800, 600);
ctx.lineTo(730, 650);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(730, 650);
ctx.lineTo(660, 700);
ctx.stroke();
} else {
ctx.strokeStyle = redColor;
ctx.beginPath();
ctx.moveTo(800, 600);
ctx.lineTo(730, 650);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(730, 650);
ctx.lineTo(660, 700);
ctx.stroke();
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(800, 600);
ctx.lineTo(720, 600);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(660, 700);
ctx.lineTo(740, 700);
ctx.stroke();
}
// W5
if (W5State) {
ctx.strokeStyle = redColor;
ctx.beginPath();
ctx.moveTo(550, 200);
ctx.lineTo(630, 200);
ctx.stroke();
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(630, 200);
ctx.lineTo(560, 250);
ctx.stroke();
} else {
ctx.strokeStyle = redColor;
ctx.beginPath();
ctx.moveTo(630, 200);
ctx.lineTo(560, 250);
ctx.stroke();
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(550, 200);
ctx.lineTo(630, 200);
ctx.stroke();
}
// W6
if (W6State) {
ctx.strokeStyle = redColor;
ctx.beginPath();
ctx.moveTo(490, 300);
ctx.lineTo(420, 350);
ctx.stroke();
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(420, 350);
ctx.lineTo(500, 350);
ctx.stroke();
} else {
ctx.strokeStyle = redColor;
ctx.beginPath();
ctx.moveTo(420, 350);
ctx.lineTo(500, 350);
ctx.stroke();
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(490, 300);
ctx.lineTo(420, 350);
ctx.stroke();
}
// W7
if (W7State) {
ctx.strokeStyle = redColor;
ctx.beginPath();
ctx.moveTo(580, 350);
ctx.lineTo(660, 350);
ctx.stroke();
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(580, 350);
ctx.lineTo(650, 400);
ctx.stroke();
} else {
ctx.strokeStyle = redColor;
ctx.beginPath();
ctx.moveTo(580, 350);
ctx.lineTo(650, 400);
ctx.stroke();
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(580, 350);
ctx.lineTo(660, 350);
ctx.stroke();
}
// Gleise (Schienen) zeichnen
ctx.lineWidth = 15;
ctx.lineCap = "square";
ctx.strokeStyle = nLineColor;
ctx.beginPath();
ctx.arc(350, 400, 300, Math.PI / 2, Math.PI * 1.5);
ctx.stroke();
ctx.beginPath();
ctx.arc(800, 400, 300, -Math.PI / 2, Math.PI / 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(350, 100);
ctx.lineTo(800, 100);
ctx.stroke();
ctx.beginPath();
ctx.arc(350, 400, 200, Math.PI / 2, Math.PI * 1.5);
ctx.stroke();
ctx.beginPath();
ctx.arc(800, 400, 200, -Math.PI / 2, Math.PI / 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(350, 200);
ctx.lineTo(550, 200);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(630, 200);
ctx.lineTo(800, 200);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(560, 250);
ctx.lineTo(490, 300);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(350, 700);
ctx.lineTo(410, 700);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(740, 700);
ctx.lineTo(800, 700);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(430, 600);
ctx.lineTo(720, 600);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(490, 700);
ctx.lineTo(660, 700);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(420, 350);
ctx.lineTo(270, 460);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(500, 350);
ctx.lineTo(580, 350);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(660, 350);
ctx.lineTo(850, 350);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(650, 400);
ctx.lineTo(730, 460);
ctx.stroke();
ctx.lineWidth = 1;
requestAnimationFrame(draw);
}
draw();
}
/* Mobile-Button-Events:
Binde dieselben Funktionen an die mobilen Buttons (IDs mit Präfix "m-").
*/
if (document.getElementById("m-reset")) {
document.getElementById("m-reset").addEventListener("click", resetAll);
document.getElementById("m-W5").addEventListener("click", toggleW5);
document.getElementById("m-W6").addEventListener("click", toggleW6);
document.getElementById("m-W12").addEventListener("click", toggleW12);
document.getElementById("m-W34").addEventListener("click", toggleW34);
document.getElementById("m-W7").addEventListener("click", toggleW7);
document.getElementById("m-L1").addEventListener("click", function () { toggleLight(1); });
document.getElementById("m-L2").addEventListener("click", function () { toggleLight(2); });
document.getElementById("m-L3").addEventListener("click", function () { toggleLight(3); });
document.getElementById("m-L4").addEventListener("click", function () { toggleLight(4); });
document.getElementById("m-L5").addEventListener("click", function () { toggleLight(5); });
document.getElementById("m-L6").addEventListener("click", function () { toggleLight(6); });
}
</script>
</body>
</html>Mit der ESP32-Erweiterung wird aus eurer USB-gebundenen Weichensteuerung ein autarkes WLAN-Stellwerk: Der ESP32 liefert als Access Point und Web-Server die HTML5-Oberfläche aus SPIFFS aus, nimmt über mDNS den Namen stellwerk.local an und leitet Klick-Kommandos per UART2 an den Arduino Mega weiter. Die zweistufige Verkabelung mit Logik-Level-Konverter zwischen 3,3 V-ESP32 und 5 V-Mega sorgt für eine sichere Signalübersetzung, während die Portierung der Processing-Canvas-Logik in HTML/CSS/JavaScript mit simplen Fetch-Aufrufen gewohnt vertraute Bedienung und Responsive Design kombiniert.
Darüber hinaus ließe sich die Schaltung leicht um einen Rückkanal erweitern, indem der Mega Status-Bytes (z. B. Endlagen-Schaltzustände oder Sensordaten) per Serial2.write() zurück an den ESP32 sendet. Dort könnten entsprechende HTTP-Routen oder WebSocket-Verbindungen eingerichtet werden, um im Browser in Echtzeit Rückmeldungen anzuzeigen und bidirektionale Kommunikation zu realisieren. So gewinnt euer WLAN-Stellwerk nicht nur Steuer-, sondern auch Überwachungsfunktionen und wird zu einer vollständig vernetzten Modellbahn-Lösung. Außerdem soll ein Videosignal welches durch eine Webcam im Schattenbahnhof angebracht wird angezeigt werden.
