Measuring model train Scale Speed with UNIHIKER


Model Railroad Hobbyists want their model railroad to resemble the appearance and operation of a full-sized real railroad. The hobbyist strives to have the train run at scalespeed on the layout. Scalespeed represents the model speed for what the actual speed would be for a life-size train.


Real trains speeds vary depending on the operation. Model railroaders will configure their layout to work within those speeds on the scaled layout.


The speed of an object the magnitude of the change of its position per unit of time.



 s = _



s = speed

d = distance covered

t = time


We scale the model distance to represent the real world. Scalespeed calculation has to factor this in to the calculation. Time doesn’t scale. One minute in real world time is still one minute in HO scale.


ScaleSpeed with UNIHIKER measures the time it take for the model engine to cover a specific distance and then factors in the scale of the layout (i.e. Z, N, HO..scale) to calculate the speed. This project only focuses on HO scale. Changes in the scale variable within the python code formula for calculating scalespeed can be done to accommodate other layout scales.



We need two data values, distance and time for the calculation. The distance between two sensors A & B is known. Time is determined by triggering sensors and taking clock readings. The engine triggers sensor A to start a timer. The timer stops when the engine triggers sensor B. Subtract the two times and you arrive at the time.


Sensor Selection


IR sensors work by transmitting (TX) a beam of IR light from one LED to another LED that detects or receive (RX) the IR light signal and produce an output.


There are two types of IR sensors. An object breaks the beam in break-beam type of sensor to produce a signal. The reflect type depends on an object reflecting IR light to provide a signal.


Bench testing provided performance results to determine the ideal sensor for the project. The sensors were connected to a bread board to establish power and have access to the output for testing.


The first test was to determine if IR noise impacted the performance of the sensor. Random IR can be produced by a number of devices. LED light bulbs are an example. This IR noise can impact the performance of an IR devices. The noise can be so high the sensor can’t read actual IR. A known IR noisy LED lamp was used to see if any sensors were impacted.


Infrared IR Proximity Sensor for Arduino (10±5mm~80±20mm) & 5V IR Photoelectric Switch (4m) sensors performance were not impacted by the IR noise. The IR Break Beam Sensor (50cm) produced an output when the noisy LED lamp was turned on and did not change state. That eliminated the sensor from further testing.


Next test was to determine would the sensor impacted in a closed environment. For the final project the sensors would be enclosed inside a mountain scene in close proximity to each other. Would this enclosure impact the performance.


The Infrared IR Proximity Sensor for Arduino (10±5mm~80±20mm) did not perform well on the test bench. Vertical surfaces around the sensors and the sensor being in close proximity, resulted in false positive readings.


5V IR Photoelectric Switch (4m) performance was not impacted by vertical surfaces and close proximity. During testing it was determined that the transmitters for both sensor need to be located on the same side to eliminate stray IR reflections.


With hardware finalized it was time to make some connections and write the code.


Development Platform

The sides of a plastic enclosure were cut to create a tunnel. Mounted to the tunnel are the sensor pair. A small bread board on the top of the enclosure was used to support connections for the sensors to power and the Unihiker computers. The tunnel enclosure was placed over the track for testing. The train passing in the tunnel triggered the sensors.


Power for the sensors was provided by an external 5V power source. Each sensor output was connected to a GPIO on the Unihiker.

The IR Break Beam Sensor (50cm) SKU:SEN0503 pinout:
Transmitter: red wire=5V DC, black wire=GND
Receiving: red wire =5V DC, black wire=GND, white wire=OUT (NPN)


Break...Break... Code development

The prototype was to cumbersome for code development. Code was developed to use the two button on the Unihiker as substitutes in the code for the sensors. Once the code was developed, the prototype was used to perform proof of performance testing.


In addition to sensor work a debug routine is inserted to provide text output during testing. This can be disabled when not required.


Proof of Concept

The code provides date and time at the top with speed indication below. The code calculations provide an actual MPH reading and the corresponding scale speed in MPH as well as KPH reading and the scale speed in KPH.


The display readings remains for 10 seconds after which the reading returns to zero.


Proof of performance testing was the final stage of this project. The mountain scenery (shown above) was not complete in order to do a full installation. The tunnel prototype was installed on a test track and the train ran to determine speeds.


Lessons Learned

During testing it was determined some hardware changes are need when the sensors are installed in the mountain.


In the tunnel prototype the sensors are located directly across from each other. This resulted in false positive reading when additional cars were attached to the train engine. The gap between the cars was sufficient to trigger the sensor.


The plan is to offset the sensor pair at an angle to eliminate the gap causing the false positives.


During code development and testing the buttons created the ideal testing scenario that only became evident using the prototype. 


With no car in the tunnel the IR sensor output is HIGH. When the car breaks the beam the output goes LOW. This LOW is ignored in favour of a return to HIGH. Triggering on the HIGH enables a train to have more than one car and still read accurate time.


Another surprising find was taking the absolute value of the time calculation. This simple function enables the train speed calculation to be done with the train moving in either direction.


After examining the photos, it was discovered the time displayed on the Unihiker was wrong. A post to the Unihker forum  https://www.dfrobot.com/forum/topic/329077 provided a solution that resolved the issue.

Thanks to DFRobot for the opportunity to test and post about the Unihiker single board computer.


Update February 10, 2024



The work on the mountain scene that will contain the IR sensor for scalespeed project continues.  A small section of track was installed to permit installation of the sensors and some initial testing.



The image on the left shows a car entering at the top. The two wood sticks point to the pair of IR sensors. The sensor on the right side of the track are the receive. In the upper right of the picture the indicator R & L are draw on the foam to reference the right and left sensor.


The image on the right shows two cars on the track. During prototype testing it was discovered the gap between cars triggered the sensor giving false readings. The problem was eliminated by angling the TX & RX sensor. The gap between train cars doesn’t trigger the sensors.


I am still unable to fully test the project. Not all the track is installed and there is no power yet to run a train engine. As the project progresses, I will continue to update. 

1 Unihiker
2 5V IR Photoelectric Switch (4m)
2 Infrared IR Proximity Sensor for Arduino (10±5mm~80±20mm)
import time
from pinpong.board import Board, Pin #Import libary to control GPIO
from unihiker import GUI  # Import the GUI module from the UniHiker library

gui = GUI()  # Instantiate the GUI class and create an object

deBug = 1               #When 1 additional print to console. Additional print statements are included for troubleshooting.

timer_left = False #initialize to prevent cal before sensor read
timer_right = False  #initialize to prevent cal before sensor read
sensorSpace_inch = 4  # space between sensors in inches
sensorSpace_mm = 101.6  # space between sensor in mm
modelScale = 87

#Create a black background with date and time at top
#Create Title text
#Create four number displays to hold different speeds
bg = gui.fill_rect(x=0, y=0, w=240, h=320,  width=3, color=(0, 0, 0))
DigitalTime=gui.draw_digit(text="0",x=10,y=310,font_size=15, color="white",angle=90)
title=gui.draw_text(text="TRAIN SPEED",x=30,y=240,font_size=20, color="white",angle=90)
MPH = gui.draw_text(x=80, y=263, text="MPH :", origin = "center",color="white",font_size=12,angle=90)
MPH2 = gui.draw_digit(x=120,y=240,text="0",origin = "center",color="white",font_size=33,angle=90)
KPH = gui.draw_text(x=80, y=140, text="KPH :", origin = "center",color="white",font_size=12,angle=90)
KPH2 = gui.draw_digit(x=120,y=100,text="0",origin = "center",color="white",font_size=33,angle=90)
SCALEMPH = gui.draw_text(x=175, y=263, text="ScaleMPH :", origin = "center",color="white",font_size=12,angle=90)
SCALEMPH2 = gui.draw_digit(x=215, y=240, text="0", origin = "center",color="white",font_size=33,angle=90)
SCALEKPH = gui.draw_text(x=175, y=140, text="ScaleKPH :", origin = "center",color="white",font_size=12,angle=90)
SCALEKPH2 = gui.draw_digit(x=215, y=100, text="0", origin = "center",color="white",font_size=33,angle=90)

#sensor23 = Pin(Pin.P27, Pin.IN) #P27 & P28 are button attachement on Unihiker can double as sensor inputs for testing
#sensor24 = Pin(Pin.P28, Pin.IN)

sensor23 = Pin(Pin.P23, Pin.IN) #P23 & P24 are Unihiker sensor inputs
sensor24 = Pin(Pin.P24, Pin.IN)

def sensor23_handler(pin): #This is what happens when pin 23 is triggered
    global deBug
    global timer_left
    global left_count
    if deBug:
        print("\n sensor23--triggered--")
        print("pin = ", pin)
        left_count = time.perf_counter()
        if deBug:
        timer_left = True

def sensor24_handler(pin): #This is what happens when pin 24 is triggered
    global deBug
    global timer_right
    global right_count
    if deBug:   
        print("\n sensor24--triggered--")
        print("pin = ", pin)
        right_count = time.perf_counter()
        if deBug:
        timer_right = True

def counter_reset(): #This resets counters to zero for a restart
    global timer_left
    global timer_right
    if deBug:
        print('System Reset')
    timer_left = False
    timer_right = False      

#Can use Unihiker button to test in absence of sensors.
#sensor23.irq(trigger=Pin.IRQ_FALLING, handler=sensor23_handler) #FALLING sensor trigger HIGH TO LOW
#sensor24.irq(trigger=Pin.IRQ_FALLING, handler=sensor24_handler) # Default used for Button Testing

# Using break beam sensor
# Sensor output is HIGH. Break beam goes LOW
# Use RISING for train with more than one car
sensor23.irq(trigger=Pin.IRQ_RISING, handler=sensor23_handler) #RISING sensor trigger LOW to HIGH 
sensor24.irq(trigger=Pin.IRQ_RISING, handler=sensor24_handler) 

while True:
    DigitalTime.config(text=time.strftime("%Y-%m-%d          %H:%M")) #Keep refreshing date and time

    if timer_left == True and timer_right == True: #Both sensor have counter loaded
        elapsed_time_sec = abs(right_count - left_count) # enables train speed to be calculated from either direction
        miles = sensorSpace_inch/63360
        kilometers = sensorSpace_mm/1000000
        hours = elapsed_time_sec/3600
        mph = miles/hours
        MPHG = mph
        kph = kilometers/hours
        KPHG = kph
        scaleMPH = mph*modelScale
        SMPHG = scaleMPH        
        scaleKPH = kph*modelScale
        SKPHG = scaleKPH
        if deBug:
            print('Elaspsed time in seconds:', elapsed_time_sec)
            print('Miles:', miles)
            print('Kilometers:', kilometers)
            print('Hours', hours)
            print("Speed in MPHG:", MPHG)
            print("Scale Speed in MPH:", scaleMPH)
            print("Speed in KPHG:", KPHG)
            print("Scale Speed in KPH:", scaleKPH)

        counter_reset() #reset the counters for another pass
        #Zero the display for another pass
All Rights