RaylFX – Train Station

Arduino Train Station RaylFX OLED

This module of the RaylFX system controls an Arduino train station for the model railway. The functions include:

  • OLED-Departure display with changing content
  • Sound-module with verbal announcements
  • Train station lighting with fluorescent tube switch on effect

Wiring Diagram

Arduino Train Station RaylFX OLED Wiring Diagram
  • D2, D3: DFPlayer MP3 module
  • D4 – D12: LEDs as fluorescent tubes
  • A3: connection of the time signal from the conrol module
  • A4, A5: I2C connection to the OLED-display

Attention: At pin A3 (normally A4, but in this case A3) the control signal of the RaylFX control module is applied. It controls the time of day and is mandatory.

Components

Code Settings

This module also offers a number of adjustment options. The lighting of the train station can flicker like fluorescent tubes when switched on. The time that all lamps take to switch on can be set with the variable flickerTimeLights. The higher the value, the longer the lights flicker. The intensity of flickering can be set with the variable flickerSpeed.

The announcements on the OLED display change randomly. The minimum duration per announcements can be set with the variable announcementChangeMin. A time between 0 and the value set in the variable announcementChangeVariableTime is added to it randomly.

The texts on the display are defined in the textSegments array. It can be extended by further texts. For this purpose, the dimensions in the array and the variable announcementCount must be adjusted.

The display automatically shows the current model railway time when switching. The follow-up trains are displayed at 30-minute intervals. So if the current time is 17:21, the departure times of the following trains are displayed 17:51 and 18:21.

The display will also randomly show a message. The text can be adjusted in the variable message.

The DFPlayer Mini is used for the sound output. It plays MP3 and WAV files from an SD card. The SD card must be formatted in the FAT16 or FAT32 file system. In addition, the files must be in the folder “01” and the file names must be sequentially named 001.mp3, 002.mp3, 003.mp3, and so on. As mentioned, WAV files may also be used: 001.mp3, 002.wav, 003.mp3 …

Sample files can be downloaded here:

Further info about the DF-Player on this page: DFPlayer Mini-MP3-Player for Arduino.

int flickerTimeLights = 1000;                   // the higher the value, the more the fluorescent tubes flicker when switched on
int flickerSpeed = 100;                         // flickering speed of the fluorescent tubes
int announcementChangeMin = 2000;               // announcement change every x milliseconds
int announcementChangeVariableTime  = 3000;     // the announcement change varies by this value (milliseconds)

int mp3Count  = 10;                             // amount of MP3 files on the SD card
int mp3Duration[] = {16,16,15,17,17,18,19,18,19,9}; // duration of MP3 files in seconds
int mp3Likelihood = 20;                         // likelihood of MP3s being played, 10 often, 100 rarely
int volume = 10;                                // volume of the DFPlayer (0 – 30);

/* Announcements */
int numberOfAnnouncements = 6;                  // Must be as large as the number of announcement texts from the array textSegments

const char* textSegments[6][7] = {
  {"12", "", "Train passing through!", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"},
  {"12", "Leipzig Central - Halle (Saale)", "Hannover Central", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"},
  {"12", "Leipzig Central - Halle (Saale)", "Berlin Central", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"},
  {"12", "Leipzig Central - Halle (Saale)", "Dresden Central", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"},
  {"12", "Leipzig Central - Halle (Saale)", "Leipzig Central", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"},
  {"12", "Leipzig Central - Halle (Saale)", "Hamburg Central", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"}
};

const char* message[] = {
  "Due to a signal fault ",
  "the ICE100 will unfortunately have approx. 25 minutes delay. ",
  "We appreciate your understanding."
};

Code for the RaylFX Train Station Module

Libraries

This project requires the SSD1306Ascii library from Bill Greiman. You can add it by searching for “SSD1306Ascii” in the Arduino menu > Sketch > Include Library > Manage Libraries and installing the current version..

In addition, the library DFRobotDFPlayerMini from DFRobot is used. You can also install this library via Manage Libraries. Here you can simply search for dfplayer.

When uploading you have to make sure that the correct board is selected in the Arduino menu. To do this, “ATmega328P (Old Bootlaoder)” must also be selected in the Processor subitem of the Tools menu. Otherwise you can simply copy the following program code with the above mentioned changes and load it onto the Arduino nano.

#include <Wire.h>
#include "SSD1306Ascii.h"
#include "SSD1306AsciiWire.h"
#include "SoftwareSerial.h"              // required for the DFPlayer
#include "DFRobotDFPlayerMini.h"         // required for the DFPlayer

/*
     Rayl-FX Train Station
     StartHardware.org/en
*/

/* ***** ***** Settings ***** ***** ***** *****  ***** ***** ***** *****  ***** ***** ***** ***** */

int flickerTimeLights = 1000;                   // the higher the value, the more the fluorescent tubes flicker when switched on
int flickerSpeed = 100;                         // flickering speed of the fluorescent tubes
int announcementChangeMin = 2000;               // announcement change every x milliseconds
int announcementChangeVariableTime  = 3000;     // the announcement change varies by this value (milliseconds)

int mp3Count  = 10;                             // amount of MP3 files on the SD card
int mp3Duration[] = {16,16,15,17,17,18,19,18,19,9}; // duration of MP3 files in seconds
int mp3Likelihood = 20;                         // likelihood of MP3s being played, 10 often, 100 rarely
int volume = 10;                                // volume of the DFPlayer (0 – 30);

/* Announcements */
int numberOfAnnouncements = 6;                  // Must be as large as the number of announcement texts from the array textSegments

const char* textSegments[6][7] = {
  {"12", "", "Train passing through!", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"},
  {"12", "Leipzig Central - Halle (Saale)", "Hannover Central", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"},
  {"12", "Leipzig Central - Halle (Saale)", "Berlin Central", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"},
  {"12", "Leipzig Central - Halle (Saale)", "Dresden Central", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"},
  {"12", "Leipzig Central - Halle (Saale)", "Leipzig Central", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"},
  {"12", "Leipzig Central - Halle (Saale)", "Hamburg Central", "RB31", "RB31", "Elsterw-Biehal", "from Coswig (b. Dresden)"}
};

const char* message[] = {
  "Due to a signal fault ",
  "the ICE100 will unfortunately have approx. 25 minutes delay. ",
  "We appreciate your understanding."
};

/* ***** ***** From here begins the program code, which does not need to be adjusted ***** ***** ***** ***** */
// 0X3C+SA0 - 0x3C or 0x3D
#define I2C_ADDRESS 0x3C
uint32_t tickTime = 0;
int tickerPosition = 0;
#define RTN_CHECK 1

int fluorescentTubesPins[9] = {4, 5, 6, 7, 8, 9, 10, 11, 12};

/* Memory Variables */
int fluorescentTubesState[9] = {1, 1, 1, 1, 1, 1, 1, 1, 1};
TickerState state;          // stores the current text pointer of the OLED ticker
int thereIsDelay = 0;       // is determined at random; 1 = delay, 0 = no delay
int announcementChangeVariableTimeAuxiliaryVariable = 0;

/* Timer Variables */
long fluorescentTubesTimer[9];
long announcementTimer;

/* Variables for the OLED */
SSD1306AsciiWire oled;

/* Variables for the MP3 Player*/
long soundTimer = 0;                     // timer of the DFPlayer
long soundTimeout = 0;                   // stores the playing time of the current MP3
int soundState = 0;                      // state of the DFPlayers

int soundRandom;
int theSound;
int soundPlaying = false;

SoftwareSerial mySoftwareSerial(3, 2);   // RX, TX for the DFPlayer
DFRobotDFPlayerMini myDFPlayer;          // DFPlayer object

/* Variables from the control module to determine the time of day */
boolean receive = false;
boolean receiveStarted = false;
int receiveTimeout = 10;
long receiveTimer = 0;
int receivedTime = 0;
int receivePulse = 0;
int lastReceivePulse = 0;
int receivePin = 17;
int myTime = 8;
int myMinutes = 0;
long lastTimeSignal;
int timeToHour;

#define PAYLOAD_SIZE 2                          // mandatory for communication with master-module
int timeOfDay = 0;                              // stores timeOfDay of master-module (0 and 255)
byte nodePayload[PAYLOAD_SIZE];                 // temporarily stores data of master-module

void setup() {
  Serial.begin(115200);                         // starts the serial communication
  Wire.begin();                                 // I2C connection to OLED
  Wire.setClock(400000L);                       // I2C connection to OLED
  mySoftwareSerial.begin(9600);                 // starts the serial communication for the DFPlayer
  oled.begin(&Adafruit128x64, I2C_ADDRESS);     // start of the OLED



  for (int i = 0; i < 9; i++) {
    pinMode(fluorescentTubesPins[i], OUTPUT);
    digitalWrite(fluorescentTubesPins[i], HIGH);
  }
  randomSeed(A2);
  oled.tickerInit(&state, Adafruit5x7, 0, false, 27, 100);    // ticker for the departure display
  drawInfo(1);

  /* DFPlayer Setup */
  Serial.println(F("Initializing DFPlayer ... "));
  if (!myDFPlayer.begin(mySoftwareSerial)) {  // use softwareSerial to communicate with the DFPlayer
    Serial.println(F("Fehler: Prüfe Verbindung zum DFPlayer und SD-Karte"));
    /*while (true) {
      delay(0); // Code to compatible with ESP8266 watch dog.
      }*/
  }
  Serial.println(F("DFPlayer Mini online."));
  myDFPlayer.volume(volume);       // volume is assigned
}

void loop() {

  receiveFunction();                // execute instructions for reception
  
  if (receiveStarted == false) {      // if no data is currently being received:
    
    if (myTime > 22) {                // ***** late evening *****
      fluorescentTubesOn();           // turn on fluorescent tubes
      soundOn();                      // turn on sound
      
    } else if (myTime > 18) {         // ***** evening *****
      fluorescentTubesOn();           // turn on fluorescent tubes
      soundOn();                      // turn on sound
      
    } else if (myTime > 12) {         // ***** noon *****
      fluorescentTubesOff();          // turn off fluorescent tubes
      soundOn();                      // turn on sound
      
    } else if (myTime > 9) {          // ***** forenoon *****
      fluorescentTubesOff();          // turn off fluorescent tubes
      soundOn();                      // turn on sound
      
    } else if (myTime > 7) {          // ***** morning *****
      fluorescentTubesOff();          // turn on fluorescent tubes
      soundOn();                      // turn on sound
      
    } else if (myTime > 0) {          // ***** night *****
      fluorescentTubesOn();           // turn on fluorescent tubes
      soundOff();                     // turn off sound
    }
    if (myMinutes < 40) departureDisplay();
  }
}

void departureDisplay() {
  int i;
  if (announcementTimer + announcementChangeMin + announcementChangeVariableTimeAuxiliaryVariable < millis()) { // the second part prevents that a display change blocks the reception of the time signal
    announcementTimer = millis();
    i = random(numberOfAnnouncements);
    drawInfo(i);
    thereIsDelay = random(2);
    announcementChangeVariableTimeAuxiliaryVariable = random(announcementChangeVariableTime );
  }
  if ((thereIsDelay > 0) && (i != 0)) {
    if (tickTime <= millis()) {
      tickTime = millis() + 30;
      oled.setInvertMode(1);
      int8_t rtn = oled.tickerTick(&state);
      if (rtn <= RTN_CHECK) {

        oled.tickerText(&state, message[(tickerPosition++) % 3]);
      }
    }
  }
}

void drawInfo(int i) {      // OLED output
  oled.clear();
  oled.setInvertMode(0);

  oled.setCursor(0 , 0);
  oled.setFont(Callibri15);
  oled.println(textSegments[i][0]);
  oled.clearToEOL();

  oled.setCursor(104 , 0);
  oled.setFont(Callibri10);
  oled.println(makeTime());
  oled.clearToEOL();

  oled.setFont(Callibri10);
  oled.setCursor(27 , 1);
  oled.println(textSegments[i][1]);
  oled.setFont(Callibri14);
  oled.setCursor(27 , 2);
  oled.println(textSegments[i][2]);

  oled.setFont(Callibri10);
  oled.setCursor(0 , 5);
  oled.println(makeTime1());
  oled.setCursor(0 , 6);
  oled.println(makeTime2());

  oled.setCursor(27 , 5);
  oled.println(textSegments[i][3]);
  oled.setCursor(27 , 6);
  oled.println(textSegments[i][4]);

  oled.setCursor(54 , 5);
  oled.println(textSegments[i][5]);
  oled.setCursor(54 , 6);
  oled.println(textSegments[i][6]);
}

String makeTime() {
  String myTimeString = "";
  int myHoursSub = myTime;
  int myMinutesSub = myMinutes;
  if (myHoursSub < 10) myTimeString = " " + String(myHoursSub);
  else myTimeString = String(myHoursSub);
  if (myMinutesSub < 10) myTimeString += ":0" + String(myMinutesSub);
  else myTimeString += ":" + String(myMinutesSub);
  return myTimeString;
}


String makeTime1() {
  // next train
  String myTimeString = "";
  int myHoursSub = myTime;
  int myMinutesSub = myMinutes;

  // int myMinutesSub1=random(30);
  if (myMinutesSub > 29) {
    if (myHoursSub > 22) myHoursSub = 0;
    else myHoursSub++;
  } else {
    myMinutesSub += 30;
  }

  if (myHoursSub < 10) myTimeString = " " + String(myHoursSub);
  else myTimeString = String(myHoursSub);
  if (myMinutesSub < 10) myTimeString += ":0" + String(myMinutesSub);
  else myTimeString += ":" + String(myMinutesSub);
  return myTimeString;
}


String makeTime2() {
  // train after the next train
  String myTimeString = "";
  int myHoursSub = myTime;
  int myMinutesSub = myMinutes;

  if (myHoursSub >= 23) myHoursSub = 0;
  else myHoursSub++;

  if (myHoursSub < 10) myTimeString = " " + String(myHoursSub);
  else myTimeString = String(myHoursSub);
  if (myMinutesSub < 10) myTimeString += ":0" + String(myMinutesSub);
  else myTimeString += ":" + String(myMinutesSub);
  return myTimeString;
}

void fluorescentTubesOn() {

  for (int i = 0; i < 9; i++) {
    if (random(flickerTimeLights) == 0) fluorescentTubesState[i] = 0;
    if (fluorescentTubesState[i] != 0) {                        // when the fluorescent tube is not switched on
      if (fluorescentTubesState[i] + fluorescentTubesTimer[i] < millis()) {
        fluorescentTubesState[i] = random(flickerSpeed) + 20;   // creates a timeout variable
        fluorescentTubesTimer[i] = millis();
        int myOutput = random(2);
        digitalWrite(fluorescentTubesPins[i], myOutput);
      }
    } else {
      digitalWrite(fluorescentTubesPins[i], 0);
    }
  }
}

void fluorescentTubesOff() {
  for (int i = 0; i < 9; i++) {
      fluorescentTubesState[i]=1;
      digitalWrite(fluorescentTubesPins[i], HIGH);
  }
  
  /*if (random(switchOffProbability) == 1) {   //  switching on the fluorescent tubes at random
    int thePin = random(10);
    digitalWrite(fluorescentTubesPins[thePin], HIGH);
    fluorescentTubesState[thePin] = 0;
  }*/
}

void soundOn() {
  switch (soundState) {
    case 0:
      soundRandom = random(mp3Likelihood);
      if (soundRandom < 1) {
        soundState = 1;
        theSound = random(mp3Count ) + 1;
        soundTimer = millis();
        soundTimeout = mp3Duration[theSound - 1] * 1000;
        Serial.print("Playsound: \t\t\t"); Serial.print(theSound); Serial.print("\t\t\t"); Serial.println(soundTimeout);
        myDFPlayer.playFolder(1, theSound); //play specific mp3 in SD:/15/004.mp3; Folder Name(1~99); File Name(1~255)
        soundPlaying = true;
      } else {
        soundTimer = millis();
        soundTimeout = 500;
        soundState = 1;
      }
      break;
    case 1:
      if (soundTimer + soundTimeout < millis()) {
        myDFPlayer.pause();
        soundPlaying = false;
        soundState = 0;
      }
      break;
  }
}


void soundOff() {
  if (soundPlaying == true) {
    Serial.println(soundPlaying);
    myDFPlayer.pause();
    soundPlaying = false;
  }
}

void receiveFunction() {                      // receives time of day from control module
  receivePulse = digitalRead(receivePin);     // read out the receive pin

  if ((receiveTimer + receiveTimeout < millis()) && (receiveStarted == true)) {
    // on timeout and active reception
    receiveStarted = false;                   // end active reception
    myTime = receivedTime - 1;                // store received time
    receivedTime = 0;                         // reset the auxiliary variable for time reception

    timeToHour = millis() - lastTimeSignal;
    lastTimeSignal = millis();

  }

  // if a pulse is detected at the receive pin that was not there before
  if ((receivePulse == 0) && (lastReceivePulse == 1)) {
    receiveTimer = millis();                              // restart timer
    if (receiveStarted == false) receiveStarted = true;   // start active reception, if not already done
    receivedTime++;                                       // there was a pulse, so increase the auxiliary variable to receive time
  }
  lastReceivePulse = receivePulse;                        // remember current state at pin for next pass

  if (receiveStarted == false) {
    if (millis() % 100 < 2) {
      int myNewMinutes = map(millis() - lastTimeSignal, 0, timeToHour, 0, 59);
      myNewMinutes = constrain(myNewMinutes, 0, 59);

      if (myNewMinutes != myMinutes) {
        Serial.print(myTime); Serial.print(":"); Serial.print(myMinutes);
        Serial.print("\t"); Serial.println(myTime);
      }

      myMinutes = myNewMinutes;
    }
  }
}

    Leave a Reply

    Your email address will not be published. Required fields are marked *