Over the past year, I have made a number of logic boards for controlling retired railroad signals for display purposes. I am a dedicated member of my union (Brotherhood of Locomotive Engineers and Trainman), and a local officer of my Division (188). In our office we have a retired 3 Color Light Signal that is on loan from a fellow officer.

The signal runs off of 12VDC provided by a small external power supply. Originally, when it was first put on display, the three lights were illuminated at the same time. This would never be the case in the real world of course. So, we decided it should have a more prototypical sequence, as if it was still in the field.
Each bulb has its own ground and a positive wire. The positive wire goes through an interesting variable resistor, before going to the bulb. This was obviously a feature to control the brilliance of the light.

The ground wire attaches to the bracket that holds the bulb in place.

These signals, with only one “head” and three different (red, yellow and green) lenses have a limited amount of indications that they can display. Most railroads have similar rules or definitions as to what the different colors mean. As to what the those indications mean – there can be slight differences, but most roads share the same basic progression. You can learn more about railroad signaling and systems from this wonderful Trains Magazine article.
It was decided that the signal would display the different color sequences found below:
- Red = Stop Indication – Stop
- Flashing Red = Restricting Indication – Proceed at restricted speed
- Yellow = Approach Indication – Be prepared to stop at next signal
- Flashing Yellow = Advance Approach Indication – Be prepared to stop at second signal
- Green – Clear = Proceed
The base of the system is an Arduino ESP8266, with three 3VDC relay boards. Each relay was labeled for ease of identification.

As you can see, the relay boards are powered directly from the ESP8266. The terminal side of the relays have a daisy chained terminal block connector for the 12VDC + line. That is the only line the relays contact as the ground is directly wired to each of the bulbs as shown above.
When I first got into working with Arduino ESP8266 controllers, I used the the ESPHome system for programing and controlling the devices. Instead of programming in C++ from scratch, it uses the ESPHome firmware and then reads a Yaml configuration file. So, the code I am going to post for this project is from the Yaml for the ESP8266 and will not work without using the ESPHome platform.
The program is a simple loop, with set time frames for cycling of the selected relays. Then every four minutes the script starts back over causing the signal to go from green to red.
esphome:
name: blet188signal
platform: ESP8266
board: nodemcuv2
wifi:
ssid: "WIFINAME"
password: "WIFIPASSWORD"
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "NAMEOFWIFINETWORK"
password: "WIFIPASSWORD"
captive_portal:
# Enable logging
logger:
# Enable Home Assistant API
api:
password: "APIPASSWORD"
reboot_timeout: 0s
ota:
password: "PASSWORD"
web_server:
port: 80
#relay control
switch:
- platform: gpio
pin: D1
name: "green"
id: "green"
restore_mode: ALWAYS_OFF
- platform: gpio
pin: D2
name: "yellow"
id: "yellow"
restore_mode: ALWAYS_OFF
- platform: gpio
pin: D3
name: "red"
id: "red"
restore_mode: ALWAYS_OFF
script:
- id: signal_cycle
then:
#stop indication
- switch.turn_off: green
- switch.turn_off: yellow
- switch.turn_on: red
- delay: 20s
#restricting indication
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_on: red
- delay: 750ms
- switch.turn_off: red
#approach indicaton
- switch.turn_off: red
- switch.turn_on: yellow
- delay: 30s
#advance approach indication
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
- switch.turn_off: yellow
- delay: 750ms
- switch.turn_on: yellow
- delay: 750ms
#clear indication
- switch.turn_off: yellow
- delay: 500ms
- switch.turn_on: green
interval:
#executes signal cycle
- interval: 4min
then:
- script.execute: signal_cycle
ESPHome does greatly simplify things, and makes it all fairly intuitive. After the initial setup of wifi networks and API info, comes the basic relay setup.
#relay control
switch:
- platform: gpio
pin: D1
name: "green"
id: "green"
restore_mode: ALWAYS_OFF
- platform: gpio
pin: D2
name: "yellow"
id: "yellow"
restore_mode: ALWAYS_OFF
- platform: gpio
pin: D3
name: "red"
id: "red"
restore_mode: ALWAYS_OFF
Platform sets the control to GPIO. Pin sets the GPIO pin that is used. Name and Id are self explanatory, and the restore mode sets the default start up of the pin. In this case, they are all set to “ALWAYS_OFF” and wait for the script to run before they turn on.
Then comes the script, which is named “signal_cycle” and runs the cycle of the signal as previously discussed.
script:
- id: signal_cycle
then:
#stop indication
- switch.turn_off: green
- switch.turn_off: yellow
- switch.turn_on: red
- delay: 20s
Finally, the script is ran every four minutes. The length of time it takes for the full cycle to run is slightly less then 4 minutes, so it creates a perfect loop.
interval:
#executes signal cycle
- interval: 4min
then:
- script.execute: signal_cycle
Then came the day to install it into the signal.

I took my Pi3B+ with its screen mounted into the office for the install session. I ran an extra long micro-USB cable up the signal mast, and started mounting up the controller. I pulled new positive lines to each bulbs resistor and hooked them up to the relay boards.



In closing, the project turned out to be success and welcomes visitors to our office in the sitting area. So far, it has always worked without any issues what so ever. I have also made a number of other signal controllers that I will post about in the future as well.