Zum Inhalt springen
RailFX Zugzielanzeiger ESP32 Live

RailFX: Arduino Zugzielanzeiger mit Live-Daten der Deutschen Bahn

    RailFX: Arduino Zugzielanzeiger – Dieses Modul des RailFX-Systems zeigt Live-Daten eines einstellbaren Bahnhofes auf OLED-Displays als Zielzuganzeiger dar. Es bezieht die Daten per Wi-Fi von der API der Deutschen Bahn und rechnet die Dauer bis zur Abfahrt aus. Bei einer API (Application Programming Interface) handelt es sich um eine Programmierschnittstelle, über die wir Daten der Bahn empfangen können.

    Eines vorweg: Dieses Modul ist nur durch die Vorarbeit von Arduino Hannover und der leicht zugänglichen API der Deutschen Bahn möglich geworden. Die Idee hab ich von Klaus zugeschickt bekommen – danke dafür :-)

    Im Gegensatz zu den meisten anderen Modulen des RailFX-Systems basiert dieses auf dem ESP32-NodeMCU. Es verfügt über Wi-Fi und lässt sich über die Arduino-IDE wie ein normales Arduino programmieren. Ein weiterer Unterschied besteht darin, dass wir keine Verbindung zum Control-Modul benötigen. Dieses stellt normalerweise die Synchronisation aller Module sicher, indem es ein Modellbau-Zeitsignal sendet. Da wir für dieses Projekt aber die »echte« Uhrzeit verwenden, ist dieses Signal hier unnötig.


    Sieh dir jetzt meinen neuen Arduino-Videokurs an: Jetzt ansehen!


    Sehen wir uns aber zuerst den elektronischen Aufbau an.

    Schaltplan

    RailFX Zugzielanzeiger ESP32 Arduino Schaltplan Schaltbild Schaltung

    Das RailFX Zugzielanzeiger-Modul verwendet bis zu acht 0,91 Zoll OLED Displays. Sie werden per I2C-Schnittstelle angesprochen. Da sie aber leider alle die gleiche feste I2C-Adresse haben, müssen wir einen I2C-Multiplexer (TCA9548A) verwenden, der die I2C-Daten auf die jeweiligen Displays verteilt. Die I2C-Pins des NodeMCUs sind an den Eingangspins des Multiplexers angeschlossen. Dieser hat acht Kanäle mit jeweils zwei Anschlüssen für I2C-Geräte (SD0, SC0 – SD7, SC7). Hier sind die Displays verbunden. Darüber hinaus werden sowohl die Displays, als auch der Multiplexer über den GND und den 3.3V-Pin des NodeMCUs versorgt.

    Der Aufbau auf dem Breadboard stelle eine kleine Herausforderung dar, weil das NodeMCU mit ESP32 nicht auf ein normales Breadboard passt. Breadboards lassen sich aber in der Regel zerlegen und so habe ich von einem eine der seitlichen Verteilungen entfernt (muss man unten das Klebepad einschneiden) und mit einem anderen Breadboard zusammengesteckt.

    Bauteile

    Natürlich kann man das ganze auch mit anderen Bauteilen umsetzen, aber das bedarf wahrscheinlich größerer Änderungen im Code.

    RailFX Zugzielanzeiger ESP32 Arduino IDE Ansicht von oben

    Vorbereitung

    Dieses Projekt ist nicht ganz trivial, da man einiges an Vorbereitungen treffen muss. Also legen wir Schritt für Schritt los.

    1. Installation des ESP32-Boards in der Arduino-IDE

    Als Erstes müssen wir das ESP32 zur Arduino-IDE hinzufügen. Öffne die Arduino-Software und klicke im Hauptmenü auf Arduino>Voreinstellungen. Trage in die Zeile zusätzliche Boardverwalter-URLs folgendes ein:

    https://dl.espressif.com/dl/package_esp32_index.json, http://arduino.esp8266.com/stable/package_esp8266com_index.json
    RailFX Zugzielanzeiger ESP32 Arduino IDE Boardverwalter URLs hinzufügen

    Bestätige mit OK und starte die Arduino-Software neu.

    Klicke jetzt im Arduino-Menü auf Werkzeuge>Board>Boardverwalter und gib im Suchfeld »esp32« ein. Installiere die Board-Software von Expressif-Systems in der aktuellen Version.

    RailFX Zugzielanzeiger ESP32 Arduino IDE Boardverwalter von Expressif hinzufügen

    Starte die Arduino-Software noch einmal neu.

    2. Installation der SSD1306Ascii-Bibliothek

    Nun müssen wir ein paar Libraries installieren. Gehe dafür im Arduino-Menü auf Sketch>Bibliotheken einbinden>Bibliotheken verwalten und suche nach »SSD1306Ascii« und installiere die aktuelle Version.

    RailFX Zugzielanzeiger ESP32 Arduino IDE SSD1306 Ascii Bibliothek

    3. Installation der DBAPI-Bibliothek

    Um die Bibliothek für die Deutsche Bahn API einzubinden, gehe auf die folgende Website: https://github.com/ArduinoHannover/DBAPI, klicke auf die grüne Code-Schaltfläche und wähle Download Zip.

    RailFX Zugzielanzeiger ESP32 Arduino IDE Deutsche Bahn API Bibliothek hinzufügen DB

    Nun wird die Bibliothek heruntergeladen. Entpacke sie und benenne den Ordner von DBAPI-master in DBAPI um. Kopiere den ganzen Ordner in deinen Arduino-Sketchbook-Library-Ordner (z.B. C:\<Benutzername>\Arduino\libraries). Starte die Arduino-Software neu.

    4. Board auswählen

    Wähle nun im Arduino-Menü unter Werkzeuge>Board den Eintrag ESP32 Dev-Module aus. Unter Werkzeuge>Port muss nun noch die Verbindung ausgewählt werden. Bei mir ist das /dev/cu.usbserial-0001.

    5. Neuen Sketch anlegen und einrichten

    Kopiere den Programmtext aus dem Codefenster in einen neuen Sketch und passe den WiFi-Namen und das WiFi-Passwort an.

    const char* ssid = "Wifi-Name";               // WiFi-Name
    const char* password = "Wifi-Passwort";       // WiFi-Passwort

    Jetzt kannst du das Programm auf das NodeMCU-Board übertragen.

    Starte den seriellen Monitor (Baud-Rate 115200). Wenn alles geklappt hat, sollte der Sketch starten und es sollten Zugdaten auf den Zugzielanzeigern erscheinen. Bis die korrekten Zeiten angezeigt werden, kann es 30 Sekunden dauern.

    Programm-Code – RailFX: Arduino Zugzielanzeiger

    Hier ist erstmal der gesamte Programm-Text. Erklärungen und Einstellungen erkläre ich weiter unten.

    #include <Wire.h>                             // Bibliothek für die I2C Funktionalität
    #include "SSD1306Ascii.h"                     // Bibliothek für die Displays
    #include "SSD1306AsciiWire.h"                 // Bibliothek für die Displays
    #include <dummy.h>                            // Bibliothek für ESP32
    #include <DBAPI.h>                            // Bibliothek um die Deutsche Bahn API abzufragen
    
    /*
         Rail-FX Live-Zugzielanzeiger der Deutschen Bahn
         StartHardware.org
    */
    
    /* ***** ***** Einstellungen ***** ***** ***** *****  ***** ***** ***** *****  ***** ***** ***** ***** */
    
    const char* ssid = "Wifi-Name";               // WiFi-Name
    const char* password = "Wifi-Passwort";       // WiFi-Passwort
    const char* bahnhofsName = "Berlin Hbf";      // Eine Liste findet man hier: https://data.deutschebahn.com/dataset/data-haltestellen.html
    String removeString1 = "Berlin ";             // Dieser String wird vom Bahnhofsnamen entfernt, z.B. "Berlin " von "Berlin Warschauer Straße"
    String removeString2 = "Berlin-";             // Dieser String wird vom Bahnhofsnamen entfernt, z.B. "Berlin-" von "Berlin-Westkreuz"
    int showPlatform[8] = {15, 16, 1, 2, 5, 6, 7, 8};  // Zuordnung der Displays zu den Gleisen (Display 1 = Gleis 15, Display 2 = Gleis 16 ... Display 8 = Gleis 8
    
    long anzeigeTimeout = 20000;                  // Anzeigewechsel alle x Millisekunden
    int apiCallTimeout = 20000;                   // Pause in Millisekunden zwischen den API Calls der DB API – Abrufen der Abfahrtszeiten alle x Millisekunden
    
    int maxPlatforms = 20;
    
    /* ***** ***** Ab hier beginnt der Programmcode, der nicht oder wenig angepasst werden muss ***** ***** ***** ***** */
    
    DBAPI db;                                      // Bahn API Objekt
    #define I2C_ADDRESS 0x3C                       // Adresse der OLEDs
    
    /* Speicher Variablen */
    char* theDates[250];                           // Array, um Abfahrtsdaten zu speichern
    char* theTimes[250];                           // Array, um Abfahrtsdaten zu speichern
    char* theProducts[250];                        // Array, um Abfahrtsdaten zu speichern
    String theTargets[250];                        // Array, um Abfahrtsdaten zu speichern
    String thePlatforms[250];                      // Array, um Abfahrtsdaten zu speichern
    char* theTextdelays[250];                      // Array, um Abfahrtsdaten zu speichern
    int myIndex = 0;                               // Anzahl der Einträge im Array
    
    const char* ntpServer = "pool.ntp.org";        // Network Time Protokol Server-Adresse
    const long  gmtOffset_sec = 3600;             // Offset für Zeitzone (3600 = CET)
    const int   daylightOffset_sec = 3600;        // Offset für Sommerzeit (3600 = Sommerzeit)
    
    
    int myDay;                                    // Aktueller Tag
    int myMonth;                                  // Aktueller Monat
    int myYear;                                   // Aktuelles Jahr
    int myHour;                                   // Aktuelle Stunde
    int myMinute;                                 // Aktuelle Minute
    
    /* Timer Variablen */
    long anzeigeTimer = 0;
    long apiCallTime;
    
    /* Variablen für das OLED */
    SSD1306AsciiWire oled;
    
    void setup() {
      Wire.begin();                              // I2C Verbindung zum OLED
      Wire.setClock(400000L);                    // I2C Verbindung zum OLED
      for (int i = 0; i < 7; i++) {
        TCA9548A(i);
        oled.begin(&Adafruit128x32, I2C_ADDRESS);  // Start des OLEDs
      }
    
      Serial.begin(115200);                      // started die serielle Kommunikation
      WiFi.mode(WIFI_STA);                       // Setzt den WIFI-Modus
      WiFi.begin(ssid, password);                // Startet die WIFI Verbindung
      while (WiFi.status() != WL_CONNECTED) {
        Serial.write('.');
        delay(500);
      }
    
      drawTest(0);   // Tafel an I2C Adresse 0 des TCA9548A, Test
      drawTest(1);   // Tafel an I2C Adresse 0 des TCA9548A, Test
      drawTest(2);   // Tafel an I2C Adresse 0 des TCA9548A, Test
      drawTest(3);   // Tafel an I2C Adresse 0 des TCA9548A, Test
    
      DBstation* station = db.getStation(bahnhofsName); // Setzt den Bahnhof für die DB Api
    
      //yield();
      if (station != NULL) {
        Serial.println();
        Serial.print("Name:      ");
        Serial.println(station->name);
        Serial.print("ID:        ");
        Serial.println(station->stationId);
        Serial.print("Latitude:  ");
        Serial.println(station->latitude);
        Serial.print("Longitude: ");
        Serial.println(station->longitude);
      }
      callDBApi();
      anzeigeTimer -= anzeigeTimeout;
    }
    
    void loop() {
      //yield();
      if (apiCallTime + apiCallTimeout < millis()) {
        configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
        getLocalTime();
        //delay(100);
        callDBApi();
        apiCallTime = millis();
      }
    
      anzeigeTafel();
    }
    
    void anzeigeTafel() {
      if (anzeigeTimer + anzeigeTimeout < millis()) { // der zweite Teil verhindert, dass ein Anzeigewechsel dem Empfang des Zeitsignals blockiert
        anzeigeTimer = millis();
        for (int i = 0; i < 8; i++) {
          drawInfo(i, showPlatform[i]); // Tafeln an I2C Adressen 0 - 7 des TCA9548A
        }
      }
    }
    
    void drawInfo(uint8_t displayNr, int platformToDisplay) {     // OLED Ausgabe
      Serial.println("Draw: "); Serial.println(displayNr);
      TCA9548A(displayNr);
      int theEntryIndexes[] = {-1,-1,-1,-1};
      int theEntryIndexesIndex = 0;
    
      for (int j = 0; j < myIndex; j++) {
        if (theEntryIndexesIndex < 4) {
          if (thePlatforms[j].toInt() == platformToDisplay) {
            theEntryIndexes[theEntryIndexesIndex] = j;
            Serial.print(displayNr); Serial.print(" <- Display \t ");Serial.print(j); Serial.print(" <- Index \t ");Serial.println(thePlatforms[j].toInt());
            theEntryIndexesIndex++;
          }
        }
      }
    
      oled.clear();
      //oled.setInvertMode(0);
      oled.setFont(Adafruit5x7);
      for (int i = 0; i < 4; i++) {
        if (theEntryIndexes[i] != -1) {
          oled.setCursor(0 , i); oled.println(theTimeDifference(theTimes[theEntryIndexes[i]], theDates[theEntryIndexes[i]]));
          oled.setCursor(20 , i); oled.println(theTargets[theEntryIndexes[i]]);
    
        }
      }
      delay(100);
    }
    
    void drawTest(uint8_t displayNr) {     // OLED Ausgabe
      Serial.println("DrawTest: "); Serial.println(displayNr);
      TCA9548A(displayNr);
      oled.clear();
      //oled.setInvertMode(0);
      oled.setFont(Adafruit5x7);
      oled.setCursor(0 , 0); oled.println("RailFX");
      oled.setCursor(0 , 1); oled.println("RailFX");
      oled.setCursor(0 , 2); oled.println("RailFX");
      oled.setCursor(0 , 3); oled.println("RailFX");
      oled.setCursor(30 , 0); oled.println("StartHardware");
      oled.setCursor(30 , 1); oled.println("StartHardware");
      oled.setCursor(30 , 2); oled.println("StartHardware");
      oled.setCursor(30 , 3); oled.println("StartHardware");
    }
    
    void TCA9548A(uint8_t bus) {
      if (bus > 7) return;           // Falls Input zu groß, abbrechen
      Wire.beginTransmission(0x70);  // TCA9548A address is 0x70
      Wire.write(1 << bus);          // send byte to select bus
      Wire.endTransmission();
    }
    
    void getLocalTime() {                         // Aktuelle Zeit abfragen
      time_t now = time(NULL);
      struct tm *tm_struct = localtime(&now);
      myDay = tm_struct->tm_mday;
      myMonth = tm_struct->tm_mon + 1;
      myYear = tm_struct->tm_year + 1900;
      myHour = tm_struct->tm_hour;
      myMinute = tm_struct->tm_min;
    }
    
    int theTimeDifference(char* theDepartureTime, char* theDepartureDate) {
      int timeToTrain = 0;
    
      int theDepartureHour, theDepartureMinute;
      sscanf(theDepartureTime, "%d:%d", &theDepartureHour, &theDepartureMinute);
      int theDepartureYear, theDepartureMonth, theDepartureDay;
      sscanf(theDepartureDate, "%d.%d.%d", &theDepartureDay, &theDepartureMonth, &theDepartureYear);
    
      boolean departureTomorrow = false;
      if (theDepartureYear - 2000 > myYear) {
        departureTomorrow = true;
      } else if (theDepartureMonth > myMonth) {
        departureTomorrow = true;
      } else if (theDepartureDay > myDay) {
        departureTomorrow = true;
      }
    
      if (departureTomorrow == true) {
        timeToTrain = ((theDepartureHour + 24) * 60 + theDepartureMinute) - (myHour * 60 + myMinute);
      } else {
        timeToTrain = (theDepartureHour * 60 + theDepartureMinute) - (myHour * 60 + myMinute);
      }
      return timeToTrain;
    }
    
    void callDBApi() {
      Serial.println("Call DB API");
      DBstation* station = db.getStation(bahnhofsName);
      myIndex = 0;
      DBdeparr* da = db.getDepatures(station->stationId, NULL, NULL, NULL, 0, PROD_ICE | PROD_IC_EC | PROD_IR | PROD_RE | PROD_S);
      char myDate;
      while (da != NULL) {
        yield();
        theDates[myIndex] = da->date;
        theTimes[myIndex] = da->time;
        theProducts[myIndex] = da->product;
        theTargets[myIndex] = da->target;
        thePlatforms[myIndex] = da->platform;
        theTextdelays[myIndex] = da->textdelay;
        da = da->next;
        myIndex++;
      }
    
      for (int platform = 0; platform < maxPlatforms; platform++) {
        for (int i = 0; i < myIndex; i++) {
          if ( thePlatforms[i].toInt() == platform) {
            //Serial.print(theDates[i]); Serial.print("\t");
            Serial.print(i); Serial.print("\t");
            Serial.print(theTimes[i]); Serial.print("\t");
            Serial.print(theProducts[i]); Serial.print("\t");
            Serial.print(thePlatforms[i]); Serial.print("\t");
            Serial.print(theTextdelays[i]); Serial.print("\t");
            theTargets[i].replace(removeString1, "");
            theTargets[i].replace(removeString2, "");
            Serial.print(theTargets[i]); Serial.println("");
          }
        }
      }
      Serial.println(""); Serial.println("");
      Serial.println("");
    }

    Wenn das Programm erstmal läuft, hast du die schwierigsten Schritte hinter dir. Herzlichen Glückwunsch! Jetzt gucken wir uns an, was wir im Code einstellen können.

    Einstellungen im Code

    Im Code muss man erstmal die WiFi-Einstellungen treffen. Das haben wir ja schon weiter oben angesehen.

    Des Weiteren kann man sich nun einen Bahnhof aussuchen, von dem man die Zugzieldaten anzeigen möchte. Im Beispiel habe ich »Berlin Hbf« gewählt, aber die API der Deutschen Bahn bietet eine Übersicht mit allen Bahnhöfen an.

    Da in Berlin viele Ziele mit Berlin oder Berlin- anfangen, habe ich zwei Variablen eingefügt, mit denen eben diese Wörter aus dem Ziel entfernt werden können. Aus »Berlin-Wannsee« wird also z.B. »Wannsee«.

    Über die Variable showPlatform kann man nun entscheiden, welche Bahnsteige auf den OLED-Displays angezeigt werden sollen. OLED1 zeigt in meinem Fall Gleis 15 an. Natürlich kann man auf mehreren OLEDs das gleiche Gleis anzeigen lassen, falls man z.B. zwei Zugzielanzeiger auf einem Bahnsteig hat.

    Über anzeigeTimeout kann man einstellen, wie oft die Displays aktualisiert werden. Die Zeit wird in Millisekunden angegeben.

    Die Variable apiCallTimeout gibt an, wie groß der Abstand zwischen den Aufrufen der API sein soll, also wie oft neue Daten abgefragt werden sollen.

    Die Variable maxPlatforms gibt an, wie viele Bahnsteige es höchstens gibt. Normalerweise sollte man sie nicht ändern müssen.

    /* ***** ***** Einstellungen ***** ***** ***** *****  ***** ***** ***** *****  ***** ***** ***** ***** */
    
    const char* ssid = "Wifi-Name";               // WiFi-Name
    const char* password = "Wifi-Passwort";       // WiFi-Passwort
    const char* bahnhofsName = "Berlin Hbf";      // Eine Liste findet man hier: https://data.deutschebahn.com/dataset/data-haltestellen.html
    String removeString1 = "Berlin ";             // Dieser String wird vom Bahnhofsnamen entfernt, z.B. "Berlin " von "Berlin Warschauer Straße"
    String removeString2 = "Berlin-";             // Dieser String wird vom Bahnhofsnamen entfernt, z.B. "Berlin-" von "Berlin-Westkreuz"
    int showPlatform[8] = {15, 16, 1, 2, 5, 6, 7, 8};  // Zuordnung der Displays zu den Gleisen (Display 1 = Gleis 15, Display 2 = Gleis 16 ... Display 8 = Gleis 8
    
    long anzeigeTimeout = 20000;                  // Anzeigewechsel alle x Millisekunden
    int apiCallTimeout = 20000;                   // Pause in Millisekunden zwischen den API Calls der DB API – Abrufen der Abfahrtszeiten alle x Millisekunden
    
    int maxPlatforms = 20;

    Noch ein Hinweis zum Code: Hier gibt es mit Sicherheit noch einiges an Optimierungspotential. Ich bin für Vorschläge offen :-)


    Wenn dir das Projekt gefallen hat und du von weiteren interessanten Projekten inspiriert werden willst, sieh dir doch mal mein neues E-Book »Arduino Projekte Volume 1« an!

    • Die beliebtesten Arduino-Projekte von StartHardware
    • Inklusive Schaltplan, Beschreibung und Code
    • Arduino-Schnellstart-Kapitel
    • Kompakter Programmierkurs


    Ein Gedanke zu „RailFX: Arduino Zugzielanzeiger mit Live-Daten der Deutschen Bahn“

    1. Rothenpieler, Peter

      Hi,
      das ist ein Superprojekt und für mich der passende Einstieg.
      Anfangs habe ich mir die Fahrplandaten immer abgetippt, wieder im ESP32 eingelesen und auf einem OLED 0,91 und für die Bahnhofstafel mit einem TFT SPI 1,8″ ausgegeben.
      Jetzt habe ich online Daten.
      Allerdings ist die Verbindung zur API noch nicht stabil genug.
      Öfters bekomme ich keine Daten zur Bahnhofstafel.
      Ich habe den Sketch ein wenig für meine Bedürfnisse passend gemacht.
      Was ich auch noch nicht ganz hinbekomme: Kann man für einen Bahnhof den gesamten Fahrplan einlesen oder geht das wie in Deinem Sketch nur stückweise?

      Auf jeden Fall ist es eine Super Arbeit von Dir .
      Bin sehr begeistert.

      Viele Grüße

      Peter

    Schreibe einen Kommentar

    Deine E-Mail-Adresse wird nicht veröffentlicht.

     

    Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.