Spur G Lokschuppen Teil 2 – ESP8266-12F als Türsteuerung

Veröffentlicht von Torben am

Moin… Was habe ich getan… Das war alles so gar nicht geplant, es sollte nur ein einfacher Lokschuppen werden. Nun habe ich ein voll in der Länge modulares Gebäude mit abnehmbarem Dach, Beleuchtung und Torsteuerung über Reedkontakte…

26-01-10-11-04-44-8028

Bild 3 von 3

Es fing mit einer einfachen Idee an: Wir wollten einen Lokschuppen für die Gartenbahn, so einfach wie möglich. Das Tor soll aber automatisch auf und zu gehen und unabhängig sein vom Fahrstrom, da zwischen analog und digital gewechselt wird. Es gibt auch „fertige“ Mechaniken zu kaufen. Die sind aber so „einfach“ aufgebaut, dass man zum Aufbau mindestens einen Architekten und Ingenieur vom Raumschiff Enterprise benötigt – und nen Statiker nicht zu vergessen.

Folgende Ideen standen im Raum:

  • Der Zug fährt einfach gegen die Türen, drückt sie auf und ein Federmechanismus (Drehfeder am Scharnier der Tür) drückt sie wieder zu.
    Der Nachteil: Es ist sehr wahrscheinlich, dass nach einiger Zeit an der Lok oder den Türen Kratzer entstehen, die nicht sonderlich gut aussehen.
  • Der Zug fährt auf eine „Rampe“ / „Wippe“, die einen Zug- oder Stangenmechanismus bewegt, über den die Türen aufgedrückt werden. Vergleichbar mit dem einfachen System einer Schranke am Bahnübergang z.B. bei H0.
    Nachteil: Mechanik verdreckt zu schnell – vor allem dann, wenn sie sich unter den Schienen befindet.
  • Ein im Lokschuppen abgehängter Stangenmechanismus, der durch einen Motor, Stellmotor oder Servo angetrieben wird. Als Auslöser dienen z.B. Mikroschalter, Fahrkontakte oder Reedkontakte.
    Nachteil: Beliebig kompliziert umsetzbar, oft nicht allwettertauglich
  • Selbes Stangensystem, aber mit digital gesteuertem Servo, der auch über die Roco Maus gesteuert werden könnte.
    Nachteil: Im Analogbetrieb nervig, da man trotzdem die Maus in der Hand haben muss. Außerdem sind die nötigen Dekoder gerne teuer und auch nicht allwettertauglich.

Nach diversem hin und her kristallisierten sich zwei mögliche Systeme heraus, beide mit dem abgehängten Stangenmechanismus:

  1. Ansteuerung eines einfachen Weichenstellantriebs mittels Bistabilen Relais und zwei Zeitrelais und zwei Reedkontakten. Der Motor hat zwei Eingänge – einen für auf und einen für zu. Je Eingang sollte ein Zeitrelais dienen.

    Dabei sollte es so sein, dass durch das bistabile Relais, das den beiden Zeitrelais vorgeschaltet war, folgendes erreicht wird:
    • Das bistabile Relais sollte sich den Zustand auf / zu merken
    • Ich könnte mit zwei Reedkontakten (1x vor und 1x hinter der Tür) die Türen sowohl öffnen, als auch schließen
    • Mit den Zeitrelais könnte ich den Motor für einen Zeitraum x ansteuern, um die Türen zu öffnen oder zu schließen
  2. Ansteuerung eines einfachen Servos (SG90) mittels ESP8266-12F und zwei Reedkontakten. Der Servo hat Plus/Minus und eine „Datenleitung“, über die ein einfaches PWM-Signal angelegt wird. Mit dem wird dem Servo die Zielposition mitgeteilt (also wie viel Grad auf / zu).

    Dadurch wäre folgendes zu erreichen:
    • Der ESP merkt sich auf dem Flash-Speicher die aktuelle Position des Servos (auf / zu)
    • Wie bei dem Beispiel mit den Relais, könnte man mit zwei Reedkontakten für auf / zu auskommen
    • Mit dem ESP kann man zielgenau die Soll-Position des Servos ansteuern und auch eine realistische, langsame „Fahrt“ realisieren (also Tür geht langsam auf und zu).
    • Die Feineinstellung ist einfacher, da der Mechanismus nur grob ausgerichtet werden muss. Der Rest kann auf dem ESP erfolgen.

Die Variante mit den Relais hatte für mich einige Nachteile:

  • Die Platinen sind recht groß und hoch. Man muss dafür schon Platz haben.
  • Das Entprellen der Reedkontakte gestaltete sich schwierig – bzw. ich hatte die Auswirkungen unterschätzt, die das „Flattern“ der Reedkontakte auslösten
  • Die Relais sollten eigentlich in ihrer Position stehen bleiben, fielen dann aber trotzdem immer wieder zurück. Das konnte bewirken, dass die Türen sich schließen, wenn die Lok noch nicht durchgefahren ist.

26-01-18-14-28-23-8038

Bild 1 von 5

Damit fiel diese Möglichkeit nach diversen Tests raus. Nach genauer Betrachtung und Code-Entwicklung mit Hilfe meines Freundes ChatGPT, bot sich aber mit dem ESP eine super Lösung. Es kamen noch Features hinzu, die ich damit realisieren konnte:

  • Kleine Webseite zum Öffnen, Schließen, Einsehen des Status (auf/zu), Kalibrieren und Einstellen des Servos
  • Wie schon geschrieben, langsames Öffnen und Schließen der Türen
  • Merken des Zustandes auch nach Powerloss
  • Problemloses Entprellen der Reedkontakte (keine Reaktion auf mehrfachen Kontakt innerhalb von x Sekunden)
  • Nahezu beliebig erweiterbar und integrierbar

Screenshot-2026-02-09-175007

Bild 1 von 3

Für interessierte, der Code sah später so aus – die ersten Tests damit waren auch erfolgreich:

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <Servo.h>
#include <EEPROM.h>

// ================= Konfiguration & Speicher =================

#define EEPROM_SIZE 512
#define SERVO_PIN D1   
#define REED_A D5
#define REED_B D6

struct Settings {
  char ssid[32];
  char pass[32];
  char hostname[32];
  int openPos;
  int closePos;
  int stepDelay;
  int lastState; // 1 = Offen, 0 = Zu
};

Settings cfg;
Servo doorServo;
ESP8266WebServer server(80);

bool doorOpen = false;
bool isMoving = false;
int currentPos = 90;
int targetPos = 90;
unsigned long lastStepTime = 0;
unsigned long lastTriggerA = 0;
unsigned long lastTriggerB = 0;
#define DEBOUNCE_MS 500

void loadConfig() {
  EEPROM.begin(EEPROM_SIZE);
  EEPROM.get(0, cfg);
  
  // Validierung für Erststart
  if (cfg.openPos < 0 || cfg.openPos > 180 || cfg.stepDelay < 1 || cfg.stepDelay > 500) {
    strncpy(cfg.ssid, "MeinWLAN", 31);
    strncpy(cfg.pass, "KenntKeiner", 31);
    strncpy(cfg.hostname, "Lokschuppen", 31);
    cfg.openPos = 160; cfg.closePos = 20; cfg.stepDelay = 25; cfg.lastState = 0;
    EEPROM.put(0, cfg); EEPROM.commit();
  }
  
  // Letzten Zustand wiederherstellen
  doorOpen = (cfg.lastState == 1);
  currentPos = doorOpen ? cfg.openPos : cfg.closePos;
  targetPos = currentPos;
}

void saveState(bool open) {
  cfg.lastState = open ? 1 : 0;
  EEPROM.put(0, cfg);
  EEPROM.commit();
}

// ================= Steuerungs-Funktionen =================

void moveInstant(int pos) {
  if(!doorServo.attached()) doorServo.attach(SERVO_PIN, 500, 2500);
  currentPos = pos;
  targetPos = pos;
  doorServo.write(pos);
  delay(150); 
}

void openDoor() {
  if (isMoving || doorOpen) return;
  targetPos = cfg.openPos;
  isMoving = true;
  if(!doorServo.attached()) doorServo.attach(SERVO_PIN, 500, 2500);
  saveState(true);
}

void closeDoor() {
  if (isMoving || !doorOpen) return;
  targetPos = cfg.closePos;
  isMoving = true;
  if(!doorServo.attached()) doorServo.attach(SERVO_PIN, 500, 2500);
  saveState(false);
}

// ================= Webserver Handler =================

void handleRoot() {
  String page = "<html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'>";
  if (isMoving) page += "<meta http-equiv='refresh' content='2; URL=/'>";
  page += "<style>button{padding:15px; margin:5px; width:45%; font-size:18px;}</style></head><body>";
  page += "<h1>" + String(cfg.hostname) + "</h1>";
  page += "<p>Status: <b>" + String(doorOpen ? "OFFEN" : "GESCHLOSSEN") + "</b> (" + String(currentPos) + "°)</p>";
  page += "<a href='/open'><button>AUF</button></a><a href='/close'><button>ZU</button></a>";
  page += "<hr><a href='/config'>WLAN & Werte</a> | <a href='/cal'>Kalibrierung</a>";
  page += "</body></html>";
  server.send(200, "text/html", page);
}

void handleCal() {
  String page = "<html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'>";
  page += "<style>button{padding:10px; margin:5px; min-width:60px;}</style></head><body>";
  page += "<h2>Kalibrierung</h2><p>Aktuell: <b>" + String(currentPos) + "°</b></p>";
  page += "<a href='/adj?v=-10'><button>-10°</button></a><a href='/adj?v=-1'><button>-1°</button></a>";
  page += "<a href='/adj?v=1'><button>+1°</button></a><a href='/adj?v=10'><button>+10°</button></a><br><br>";
  page += "<a href='/set?t=open'><button style='background:#dfd'>Als OFFEN (" + String(cfg.openPos) + "°) speichern</button></a><br>";
  page += "<a href='/set?t=close'><button style='background:#fdd'>Als ZU (" + String(cfg.closePos) + "°) speichern</button></a>";
  page += "<hr><a href='/'>Zurück</a></body></html>";
  server.send(200, "text/html", page);
}

void handleAdjust() {
  moveInstant(constrain(currentPos + server.arg("v").toInt(), 0, 180));
  handleCal();
}

void handleSet() {
  if (server.arg("t") == "open") cfg.openPos = currentPos;
  else cfg.closePos = currentPos;
  EEPROM.put(0, cfg); EEPROM.commit();
  handleCal();
}

void handleConfig() {
  String page = "<html><head><meta charset='UTF-8'></head><body><h2>Einstellungen</h2><form action='/save'>";
  page += "Name: <input name='host' value='" + String(cfg.hostname) + "'><br>";
  page += "Grad OFFEN: <input name='open' type='number' value='" + String(cfg.openPos) + "'><br>";
  page += "Grad ZU: <input name='close' type='number' value='" + String(cfg.closePos) + "'><br>";
  page += "Speed (ms): <input name='speed' type='number' value='" + String(cfg.stepDelay) + "'><br><br>";
  page += "SSID: <input name='ssid' value='" + String(cfg.ssid) + "'><br>";
  page += "Pass: <input name='pass' value='" + String(cfg.pass) + "'><br>";
  page += "<input type='submit' value='Speichern & Neustart'></form></body></html>";
  server.send(200, "text/html", page);
}

void handleSave() {
  strncpy(cfg.ssid, server.arg("ssid").c_str(), 31);
  strncpy(cfg.pass, server.arg("pass").c_str(), 31);
  strncpy(cfg.hostname, server.arg("host").c_str(), 31);
  cfg.openPos = server.arg("open").toInt();
  cfg.closePos = server.arg("close").toInt();
  cfg.stepDelay = server.arg("speed").toInt();
  EEPROM.put(0, cfg); EEPROM.commit();
  server.send(200, "text/html", "Speichere... Neustart.");
  delay(1000); ESP.restart();
}

// ================= Setup & Loop =================

void setup() {
  Serial.begin(115200);
  loadConfig();
  pinMode(REED_A, INPUT_PULLUP);
  pinMode(REED_B, INPUT_PULLUP);

  // Ruckfreier Start auf letzter Position
  doorServo.write(currentPos); 
  doorServo.attach(SERVO_PIN, 500, 2500);
  delay(200);
  doorServo.detach(); 

  WiFi.begin(cfg.ssid, cfg.pass);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }

  server.on("/", handleRoot);
  server.on("/cal", handleCal);
  server.on("/config", handleConfig);
  server.on("/save", handleSave);
  server.on("/adj", handleAdjust);
  server.on("/set", handleSet);
  server.on("/open", []() { openDoor(); handleRoot(); });
  server.on("/close", []() { closeDoor(); handleRoot(); });
  server.begin();

  // Nach Start immer öffnen, falls noch zu (jetzt sanft via Loop)
  if(!doorOpen) {
    delay(500);
    openDoor();
  }
}

void loop() {
  server.handleClient();
  yield();

  // Schrittweise Bewegung
  if (isMoving && millis() - lastStepTime >= (unsigned long)cfg.stepDelay) {
    lastStepTime = millis();
    if (currentPos < targetPos) currentPos++;
    else if (currentPos > targetPos) currentPos--;
    doorServo.write(currentPos);
    if (currentPos == targetPos) {
      isMoving = false;
      doorOpen = (currentPos == cfg.openPos);
      delay(200); doorServo.detach();
    }
  }

  // Reed-Kontakte (Toggle)
  if (digitalRead(REED_A) == LOW && (millis() - lastTriggerA > DEBOUNCE_MS)) {
    lastTriggerA = millis();
    doorOpen ? closeDoor() : openDoor();
  }
  if (digitalRead(REED_B) == LOW && (millis() - lastTriggerB > DEBOUNCE_MS)) {
    lastTriggerB = millis();
    doorOpen ? closeDoor() : openDoor();
  }
}

Der Aufbau sah dann also folgendermaßen aus:

  • Ich rechne mit einer gemeinsamen Eingangsspannung von 12 – 16V (Lichtstrom)
  • Je ein Stepdown-Modul auf 5V für Servo und ESP
  • Zwei Reedkontakte zum Schalten
  • ESP8266-12F mit einfacher Programmierung via Arduino IDE

26-01-17-12-53-27-8037

Bild 1 von 6

Zwei Stepdown-Module habe ich verbaut, weil ich bei ersten Tests Phänomene hatte, die ich nicht ganz zuordnen konnte. Der Servo lief unruhig und der ESP stürzte einfach im Betrieb ab – vor allem dann, wenn der Servo Leistung zog. Ich hatte da den Servo und den ESP noch am Breadboard angeschlossen. Ich vermute, dass da das Problem lag. Nachdem ich die Spannungsversorgung der beiden getrennt hatte (zwei Zuleitungen zum selben Netzteil), waren die Probleme weg. Mit zwei Stepdown-Modulen konnte ich sicher sein, dass sie sich nicht gegenseitig stören.

Der erste Test sah dann auch schon vielversprechend aus:

Und wann kommt Teil 3 – Aufbau und Inbetriebnahme im Garten? Tja, da muss es erstmal wärmer werden. Noch sind die Loks, Schienen und Co. im Winterschlaf 😉


0 Kommentare

Schreibe einen Kommentar

Avatar-Platzhalter

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert