Model Railway Series – 3 – Dynamic Departures

This series is looking at how to use a SSD1306 128×64 pixel OLED display module as a passenger information display on a model railway. The first in the series created a train departures display. The second a clock to keep time. We are going to combine these two to create a dynamic departures board that updates each time a train has departed our station.

Our station has what is know as a clock-face timetable. This means that the same trains depart at the same minutes each hour. This makes our life much easier and more importantly keeps the demand on our Nano’s memory lower.

So far the variables in the departures object have remained the same. We are now going to change the hours value depending on the time of day.

Dynamic Display

We want to add a clock to the departures board we created in Part 1. Our clock is positioned at the top of the screen, in the same location as our departures appear. Before we can add the clock to the departures board we need to alter its size and position to place it in the bottom right corner of the display.

We need to tweak displayClock() to change its font to the tiny one we are using for departures. Change its coordinates and remove the border.

void displayClock()
{
  byte col = 98;
  byte row = 58;
  HCuOLED.SetFont(sharpsharp_6pt);
  
  /* display hours   */
  HCuOLED.Cursor(col, row);
  if (h>9)
  {
    HCuOLED.Print(h);
  } else if (h==0)
  {
    HCuOLED.Print("00");
  } else {
    HCuOLED.Print("0");
    HCuOLED.Cursor(col+4, row);
    HCuOLED.Print(h);
  }
  HCuOLED.Cursor(col+10, row);
  HCuOLED.Print(":");
  
  /* display minutes */
  HCuOLED.Cursor(col+12, row);
  if (m>9)
  {
    HCuOLED.Print(m);
  } else if (m==0)
  {
    HCuOLED.Print("00");
  } else {
    HCuOLED.Print("0");
    HCuOLED.Cursor(col+16, row);
    HCuOLED.Print(m);
  }

  /* display seconds */
  HCuOLED.SetFont(sharpsharp_5pt);
  HCuOLED.Cursor(col+22, row+2);
  if (s>9)
  {
    HCuOLED.Print(s);
  } else if (s==0)
  {
    HCuOLED.Print("00");
  } else {
    HCuOLED.Print("0");
    HCuOLED.Cursor(col+26, row+2);
    HCuOLED.Print(s);
  } 
}

With the clock in a suitable position we can merge the code from our departures and clocks sketches to give us a departure board with a working clock.

#include "HCuOLED.h"
#include "SPI.h"

 // These connect to the CS, DC, and RST pins on the display.

#define CS_DI 10
#define DC_DI 9
#define RST_DI 8

const byte NUMBER_OF_DEPARTURES = 6;

/* Create an instance of the library for the display */
HCuOLED HCuOLED(SSD1306, CS_DI, DC_DI, RST_DI);

/* clock stuff */
byte h=11;
byte m=12;
byte s=13;

struct TrainDeparture
{
  byte hours;
  byte minutes;
  byte platform;
  char *destination;
};

TrainDeparture departures[NUMBER_OF_DEPARTURES] = {
  {11, 11, 1, "Sheffield"},
  {11, 14, 3, "Nottingham"},
  {11, 28, 1, "Amblethorpe"},
  {11, 40, 2, "Newcastle"},
  {11, 49, 4, "Blyth"},
  {11, 58, 1, "Amblethorpe"}
};

void displayDeparture(byte row)
{
  const byte ROW_HEIGHT = 8;
  byte train = row;
  byte row = row * ROW_HEIGHT;
  HCuOLED.SetFont(sharpsharp_6pt);

  HCuOLED.Cursor(0, row);
  HCuOLED.Print(departures[train].hours);
  
  HCuOLED.Cursor(10);
  HCuOLED.Print(":");
  
  HCuOLED.Cursor(12, row);
  HCuOLED.Print(departures[train].minutes);
  
  HCuOLED.Cursor(25, row);
  HCuOLED.Print(departures[train].destination);
  
  HCuOLED.Cursor(91, row); 
  HCuOLED.Print(departures[train].platform);
  
  HCuOLED.Cursor(100, row);
  HCuOLED.Print("On Time");
}

void displayClock()
{
  byte col = 98;
  byte row = 58;
  HCuOLED.SetFont(sharpsharp_6pt);
  
  /* display hours   */
  HCuOLED.Cursor(col);
  if (h>9)
  {
    HCuOLED.Print(h);
  } else if (h==0)
  {
    HCuOLED.Print("00");
  } else {
    HCuOLED.Print("0");
    HCuOLED.Cursor(col+4, row);
    HCuOLED.Print(h);
  }
  HCuOLED.Cursor(col+10, row);
  HCuOLED.Print(":");
  
  /* display minutes */
  HCuOLED.Cursor(col+12, row);
  if (m>9)
  {
    HCuOLED.Print(m);
  } else if (m==0)
  {
    HCuOLED.Print("00");
  } else {
    HCuOLED.Print("0");
    HCuOLED.Cursor(col+16, row);
    HCuOLED.Print(m);
  }

  /* display minutes */
  HCuOLED.SetFont(sharpsharp_5pt);
  HCuOLED.Cursor(col+22, row+1);
  if (s>9)
  {
    HCuOLED.Print(s);
  } else if (s==0)
  {
    HCuOLED.Print("00");
  } else {
    HCuOLED.Print("0");
    HCuOLED.Cursor(col+26, row+1);
    HCuOLED.Print(s);
  } 
}

void setup() 
{
  /* Reset the display */
  HCuOLED.Reset();
}
void loop() 
{ 
  HCuOLED.ClearBuffer();
  for (int i = 0; i < NUMBER_OF_DEPARTURES; i++)
  {
    displayDeparture(i);
  }
  displayClock();
  HCuOLED.Refresh();

  s=s+1;
  if(s==60)
  {
    s=0;
    m=m+1; 
  }
  if(m==60)
  {
    m=0;
    h=h+1;
  }
  if(h==24) h=0;
 
 delay(1000);
}

Dynamic Information

Our display has a working clock, but our departures are fixed. We want our display to keep up to date with the time. The top departure on our display will be the next scheduled departure according to the current time.

Trains are scheduled to leave our station at the same minutes passed each hour to the same destinations from the same platform. This means that hours is the only departures data that changes. We will no longer set the hours value for each departure, it will be calculated using the current time.

We currently set the hours values when we create our departures objects.

struct TrainDeparture
{
  byte hours;
  byte minutes;
  byte platform;
  char *destination;
};

TrainDeparture departures[NUMBER_OF_DEPARTURES] = {
  {11, 11, 1, "Sheffield"},
  {11, 14, 3, "Nottingham"},
  {11, 28, 1, "Amblethorpe"},
  {11, 40, 2, "Newcastle"},
  {11, 49, 4, "Blyth"},
  {11, 58, 1, "Amblethorpe"}
};

hours is the first value we specify. We can remove this.

TrainDeparture departures[NUMBER_OF_DEPARTURES] = {
  {11, 1, "Sheffield"},
  {14, 3, "Nottingham"},
  {28, 1, "Amblethorpe"},
  {40, 2, "Newcastle"},
  {49, 4, "Blyth"},
  {58, 1, "Amblethorpe"}
};

We also need to remove it from our struct. But we are still going to be using hours. Rather than removing it we will simply move it from first to last in our list.

struct TrainDeparture
{
  byte minutes;
  byte platform;
  char *destination;
  byte hours;
};

When the data is loaded into our departures objects it will be placed in the correct member fields and hours will be left empty. Actually, because it is a byte, it will be set to 0, but we don’t need to worry about that.

We need to calculate the hours time of the next departure for each of our services. This needs to happen at start up. It will then need to be updated each time a train passes its departure time.

The start up value of hours can be calculated in our struct. We will add in a setup() function that will set the hours to a value in the next 60 minutes by comparing the minutes value of the departures with the m value of our clock.

struct TrainDeparture
{
  byte minutes;
  byte platform;
  char *destination;
  byte hours;
  
  void setup()
  {
    if (minutes >= m) hours == h;
    else              hours == h+1;
    if (hours==24)    hours=0;
  }
};

To trigger this function for all our departures we need to call it from our main setup() function.

void setup() 
{
  /* Reset the display */
  HCuOLED.Reset();
  
  for (int i = 0; i < NUMBER_OF_DEPARTURES; i++)
  {
    departures[i].setup();
  }
}

These initial values for hours will be incremented by 1 each time our clock passes the departure time and they are removed from the top of the departure board. Before we can put that functionality into our code we need to work out how to get our trains displayed in the correct order.

The first train on our screen will be the one with the time nearest the current time, we will store this train in a global variable called nextDeparture.

byte nextDeparture;

On power up we need to find the next scheduled departure.

for (int i = 0; i < NUMBER_OF_DEPARTURES; i++)
{
  if (departures[i].minutes > m) 
  {
    nextDeparture = i;
    break;
  }  
}

This added to the main setup() function.

Our display can now show the trains in the correct order of departure starting with the next scheduled train at the top. They will remain this way even as the clock ticks past their departure time. Our display requires an update function, and our displayDeparture() function also requires a small modification.

We wrote the displayDeparture() function to display the trains in the order they were entered with train 0 on row 0, train 1 on row 1 and so on. We want to maintain their sequence, while cycling the trains through the rows.

The value of nextDeparture will determine which train appears on which row.

byte train;
train = row + nextDeparture;
if (train >= NUMBER_OF_DEPARTURES) 
{
  train = train - NUMBER_OF_DEPARTURES;
}

The if statement ensures that we don’t try to display a train which doesn’t exist.

To keep our display up to date, each time the clock ticks past a minute we will check to see if the time has passed the time of the nextDeparture. When it has, nextDeparture will be incremented which will ensure our display updates. We will create an update() function within our struct to achieve this.

void update()
{
  if (minutes == m) 
  {
    nextDeparture += 1;
    if (nextDeparture == 6) nextDeparture = 0;
  }
}

We can also use this update() function to change the trains hours value ready for the next service in one hours time.

  void update()
  {
    if (minutes == m) {
      hours += 1;
      if(hours == 24) hours = 0;
      nextDeparture += 1;
      if (nextDeparture == 6) nextDeparture = 0;
    }
  }

The update() function is called for the nextDeparture using this code:

departures[nextDeparture].update();

We add this to the loop() when the s value equals 60.

void loop() 
{ 
  HCuOLED.ClearBuffer();
  for (int i = 0; i < NUMBER_OF_DEPARTURES; i++)
  {
    displayDeparture(i);
  }
  displayClock();
  HCuOLED.Refresh();

  s=s+1;
  if(s==60)
  {
    s=0;
    m=m+1;
    departures[nextDeparture].update();
  }
  if(m==60)
  {
    m=0;
    h=h+1;
  }
  if(h==24) h=0;
 
 delay(1000);
}

We now have a fully functioning departures board with trains appearing and disappearing in time with the clock. The sketch can be uploaded to a nano. If you haven’t followed along and written your own sketch you can download a copy from Github here.

https://github.com/SharpSharp/ModelRailwayPID/blob/main/HCuOLED_Model_Railway_Information_Display.ino


A SSD1306 128×64 Pixel OLED Display Module with updating train departures

If you want to run the clock fast then try changing the delay() value at the end from delay(1000); to delay(100); and it will run 10 times faster.

If you leave your display running for a long time, when it ticks past midnight you may spot a problem. Because the value of departures hours is zero it will be left blank off the screen. We resolved this problem with our clock. We do the same with our displayDeparture() function. Note that we will do the same for minutes. Technically this isn’t needed in this example. However, it ensures the minutes will display correctly if you choose to change the departure times to have a train leaving the station before 10 passed the hour.

void displayDeparture(byte row)
{
  const byte ROW_HEIGHT = 8;
  byte train;
  train = row + nextDeparture;
  if (train >= NUMBER_OF_DEPARTURES) 
  {
    train = train - NUMBER_OF_DEPARTURES;
  }
  row = row * ROW_HEIGHT;  
  HCuOLED.SetFont(sharpsharp_6pt);

  HCuOLED.Cursor(0, row);
  if (departures[train].hours>9)
  {
    HCuOLED.Print(departures[train].hours);
  } else if (departures[train].hours==0)
  {
    HCuOLED.Print("00");
  } else {
    HCuOLED.Print("0");
    HCuOLED.Cursor(4, row);
    HCuOLED.Print(departures[train].hours);
  }

  HCuOLED.Cursor(9, row);
  HCuOLED.Print(":");
  
  HCuOLED.Cursor(11, row);
  if (departures[train].minutes>9)
  {
    HCuOLED.Print(departures[train].minutes);
  } else if (departures[train].minutes==0)
  {
    HCuOLED.Print("00");
  } else {
    HCuOLED.Print("0");
    HCuOLED.Cursor(16, row);
    HCuOLED.Print(departures[train].minutes);
  }

  HCuOLED.Cursor(24, row);
  HCuOLED.Print(departures[train].destination);
  
  HCuOLED.Cursor(91, row); 
  HCuOLED.Print(departures[train].platform);

  HCuOLED.Cursor(100, row);
  HCuOLED.Print("On Time");
}

With our departures display now updating after a train leaves the station, we can turn our attention to the arriving trains. In part 4 of this series we will create an arrivals board.

Files relevant to this series

https://github.com/SharpSharp/ModelRailwayPID/blob/main/HCuOLED_Model_Railway_Departures_Display.ino

https://github.com/SharpSharp/ModelRailwayPID/blob/main/HCuOLED_Model_Railway_Clock_Display.ino

https://github.com/SharpSharp/ModelRailwayPID/blob/main/HCuOLED_Model_Railway_Information_Display.ino

https://github.com/SharpSharp/ModelRailwayPID/blob/main/HCuOLED_Model_Railway_Arrivals_Display.ino

https://github.com/SharpSharp/ModelRailwayPID/blob/main/HCuOLED_Model_Railway_Detailed_Departures.ino

https://github.com/SharpSharp/ModelRailwayPID/blob/main/HCuOLED_Model_Railway_Station_Depature_Board.ino

Library:

https://github.com/HobbyComponents/HCuOLEDhttp://forum.hobbycomponents.com/viewto … =58&t=1817

OR

https://forum.hobbycomponents.com/viewtopic.php?f=58&t=1817



A Note on Electronics

Development of the code for this project was completed using a Nano.

However,  the final installation was completed using a Pro Mini.

The Uno would also be suitable, but obviously has a bigger footprint so use of this will be determined by your chosen installation area.

The OLED used was a white backlit module, however, Hobby Components also sell a blue backlit OLED which would also work well.

Leave a Reply

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