Using my PC, it is possible to have several trains running automatically on my layout.
To achieve that, I am using JMRI on the PC. JMRI is connected to my ECOS. And ECOS is controlling the trains as well as the turnouts on the layout.
My entire layout is covered by virtual signals. I.e. for any block there is a signal to control the running of trains. Because of that and because I am using signal controlled warrants in JMRI, no collision of trains will occur.
Every train is having it’s own home block – typically in a shadow yard – where it is parked when not on tour. The home block is reserved to that particular train. I.e. no other train will ever enter that block.
I have defined several warrants in JMRI for every train. A warrant will take the train from one stop (or station) to the next. It is important to have a sufficient number of warrants to ensure the train can travel onwards no matter where it stops, i.e. that wherever one warrant ends there must be another warrant that starts. And no matter the starting point, the train must be able to return via one or more consecutive warrants.
In addition, I have made a Python script that is starting the warrants so that the trains are moving around on the layout in a semi-random fashion. I.e., I am not having any timetables. I tried that in an earlier stage, but it did not provide as much “life” on the layout.
In addition to running trains, the script is also contributing to the entertainment by blowing the horn, controlling the lights, making station announcements etc.
The script is divided in two parts: The script itself and a number of train definitions – one for each train.
Data definition
The following is an example train definition (explanation below):
IC3_Warrants = []
IC3_Warrants.append({ 'Warrant': 'IC3_3-1', 'TimeToWait': 10000, 'Direction': 'FWD', 'StartBlock': 'OB3', 'EndBlock': 'OB1', 'NextDirection': 'FWD', 'MayRepeat': 'Yes' })
IC3_Warrants.append({ 'Warrant': 'IC3_1-3', 'TimeToWait': 10000, 'Direction': 'FWD', 'StartBlock': 'OB1', 'EndBlock': 'OB3', 'NextDirection': 'FWD', 'MayRepeat': 'Yes' })
IC3EPE = """
print "IC3 entering platform"
"""
IC3LPE = """
print "IC3 leaving platform"
"""
IC3SLE = """
print "IC3 starting"
self.Throttle.setF0(True)
"""
IC3ELE = """
print "IC3 ending"
self.Throttle.setF0(False)
"""
IC3OB26 = """
print "IC3 entering main station"
self.Throttle.setF3(True)
self.waitMsec(1000)
self.Throttle.setF3(False)
"""
IC3Entertainment = { 'EPE': IC3EPE, 'LPE': IC3LPE, 'SLE': IC3SLE, 'ELE': IC3ELE, 'OB26': IC3OB26 }
IC3Train = { 'Train': 'IC3', 'DCCAddress': 5003, 'SoundDecoder': 0, 'E': IC3Entertainment }
IC3Definition = { 'Sensor': 'RunIC3', 'Locomotive': IC3Train, 'Warrants': IC3_Warrants, 'DefaultStartBlock': 'OB3', 'DefaultDirection': 'FWD', 'StoredStatus': 'IC3_StoredStatus', 'DisplayStatus': 'IC3_Status', 'Place': 'IC3_Place' }
TrainsAndTrips['IC3'] = IC3Definition
The train definition consists of:
– Sensor: The JMRI name of a sensor controlling whether the train is active or inactive. I am using a virtual, which becomes a “button” on the PC screen. But it can also be a physical sensor connected to a physical switch.
– Locomotive: Definition of the locomotive. See below.
– Warrants: A list of all warrats for the train.
– DefaultStartBlock: The JMRI name for the home block.
– DefaultDirection: FWD or REV. Indicates if the train by default starts by running forward or reverse. Only relevant if the trains home block is a terminal station and if the train via a reversing loop can return home running either backward or forward.
– StoredStatus: The name of a JMRI memory variable for storing status for the train, i.e. information about which block the train currently occupies and which direction of travel it used to get there.
– DisplayStatus: The name of a JMRI memory variable for displaying status on the PC screen such as “Waiting” or “Running”.
– Place: The name of a JMRI memory variable for displaying on the PC screen which block the train currently occupies.
The locomotive definition consists of:
– Train: The JMRI name of the locomotive.
– DCCAddress: The locomotives DCC adress. Note that the combination JMRI, ECOS and Märklin MFX / M4 decoders is having the speciality that since a lot of functionality in JMRI is depending on the DCC address and since these decoders do not have one, a fake DCC address is invented in JMRI by adding 20.000 + the ECOS id for the train. I know, since I implemented it myself in JMRI.
– SoundDecoder: 0 or 1 depending if the locomotive is equipped with a sound decoder or not. The information is presently not used.
– E: “Entertainment” definition, i.e. a few lines of Python code to be executed at certain occasions. It can be anything. The intention is to activate sounds, light etc. on the train. In the above example, the light on the train is controlled via F0. It could also be station announcements or something entirely different.
The entertainment consists of:
– EPE: Enter Platform Entertainment. Executed when a warrant finishes.
– LPE: Leave Platform Entertainment. Executed when a warrant starts.
– SLE: Start Locomotive Entertainment. Executed when the train is activated (via Sensor).
– ELE: End Locomotive Entertainment. Executed when the train is deactivated (via Sensor).
– 0 or more block names. Executed when the train enters the named block.
EPE, LPE, SLE and ELE must be defined. The block names are optional.
Each warrant has the following attributes:
– Warrant: The name of the JMRI warrant.
– TimeToWait: Number of miliseconds the train should wait before the warrant is started. The script actually takes TimeToWait from a random warrant with starting block where the train is situated. That is so because the actual warrant is not selected until the waiting time has passed. This is because the pattern of free and reserved/occupied blocks may change while the train is waiting.
– Direction: Either FWD or REV. Indicates if the train is going forward or reverse. Must match the same attribute in the JMRI warrant.
– StartBlock: The JMRI name of the first block of the warrant.
– EndBlock: The JMRI name of the last block of the warrant.
– NextDirection: Either FWD, REV or DontCare. The next warrant to be selected must have this Direction. DontCare means no requirement to the direction of the next warrant. This mechanism is relevant for trains that sometimes goes forward and sometimes reverses through the same block while travelling in the same direction (such as north). Note that it requires a double set of warrants with each Direction represented. In addition, there must be a reversing loop involved somewhere on the layout for this mechanism to be relevant.
– MayRepeat: Yes or No. Indicates whether the same warrant may be used again next time the train shall start from the same block, even if there is an alternative warrant.
The Script
The script consists of different elements:
– Code that reads/executes all available train definitions.
– A class (RunTrain) inheriting from the JMRI AbstractAutomation class. This class definition constitutes the main part of the code.
– Initilization code that creates an instance of the RunTrain class for each train. Each of these objects then lives in it’s own thread without knowing anything of each other.
The RunTrain class consists of:
– Initialization code.
– Utility function to switch track power off and on again. (To wake up one of my trains which tends to lock up.)
– Utility functions for persisting and restoring the train status. Before I made this functionality, I needed all trains to always be in their home blocks and pointing in the right direction, when starting JMRI. Now the only requirement is that no warrant is active when closing JMRI and the layout.
– runTrainOnce is starting a given warrant and keeping status on the screen updated as well as executing the entertainment code while the warrant is active.
– handle is the main procedure in any class inheriting from AbstractAutomation. It is being called again and again. It waits for Sensor to be active. Then it selects a warrant and calls runTrainOnce with the selected warrant. The warrant is selected so that it has the correct start block, the correct direction (as described above) and if possible so that the rules for repeating the same warrant are fulfilled. If more than one warrant fulfills this, a random warrant among them is selected.
import jarray
import jmri
import pickle
import java.util
import threading
oblocks = jmri.InstanceManager.getDefault(jmri.jmrit.logix.OBlockManager)
###################################################################################################
###################################################################################################
#
# Each locomotive is defined by:
# - the parameters that are needed to start an SCWarrant
# - Entertainment in terms of a few lines of code to execute in these cases:
# - EPE = Enter Platform Entertainment
# - LPE = Leave Platform Entertainment
# - SLE = Start Locomotive Entertainment
# - ELE = End Locomotive Entertainment
# - A set of alternative warrants for the locomotive out of which one is chosen at random.
# However, only if the destination block of the warrant is free.
#
# The following code imports all available locomotive definitions by executing any file matching
# *_lokdef.py in any subdirectory. Each of those file should define a locomotive and add that
# definition to the global variable (Dictionary) TrainsAndTrips.
#
###################################################################################################
###################################################################################################
#First a few methods to traverse the file system
import os
import fnmatch
def yield_files_with_extensions(folder_path, file_match):
for root, dirs, files in os.walk(folder_path):
for file in files:
if fnmatch.fnmatch(file.lower(),file_match.lower()):
yield file
break # without this line it traverses the subfolders too
def yield_files_in_subfolders(folder_path, file_match):
yield [folder_path, [f for f in yield_files_with_extensions(folder_path, file_match)]]
for root, dirs, files in os.walk(folder_path):
for d in dirs:
subfolder = os.path.join(root, d)
my_files = [subfolder, [f for f in yield_files_with_extensions(subfolder, file_match)]]
yield my_files
#Start in current working directory and look through all subdirectories
the_path = r'.'
def_files = [f for f in yield_files_in_subfolders(the_path, "*_lokdef.py")]
def_files1 = []
#For each subdirectory
for d in def_files:
dir = str(d[0])
for f in d[1]:
#Add each filename matching the pattern including its relative directory to a list
def_files1.append(dir+'/'+str(f))
#Traverse the list of files and execute each of them to define all trains / locomotives
TrainsAndTrips = {}
for t in def_files1:
with open(t) as f: exec(f.read())
# Initialize the random number generator. We will use it to find random warrants for trains
Randomizer = java.util.Random()
###################################################################################################
###################################################################################################
#
# CLASS IMPLEMENTING A THREAD PER LOCOMOTIVE TO RUN THAT LOCOMOTIVE
#
###################################################################################################
###################################################################################################
class RunTrain(jmri.jmrit.automat.AbstractAutomaton) :
global TrainsAndTrips
global Randomizer
def powerOffOn(self):
print "powerOffOn ", self.name
# Flip the power off and on again - but not if it is already off by purpose
if (powermanager.getPower() == jmri.PowerManager.ON):
powermanager.setPower(jmri.PowerManager.OFF)
self.waitMsec(3000)
powermanager.setPower(jmri.PowerManager.ON)
def init(self):
# init() is called exactly once at the beginning to do
# any necessary configuration.
print "Inside init()", self.name
self.setLocomotive(TrainsAndTrips[self.name]['Locomotive'])
self.runSensor = sensors.provideSensor(TrainsAndTrips[self.name]['Sensor'])
print 'sensor', TrainsAndTrips[self.name]['Sensor']
self.runSensor.setKnownState(INACTIVE)
self.StoredStatusVariable = memories.provideMemory(TrainsAndTrips[self.name]['StoredStatus'])
self.DisplayStatusVariable = memories.provideMemory(TrainsAndTrips[self.name]['DisplayStatus'])
self.PlaceVariable = memories.provideMemory(TrainsAndTrips[self.name]['Place'])
self.restoreStatus()
print "Init ",self.name
self.MODE_RUN = 2
self.oldRunSensorState = False
self.setPlace(self.status['Block'])
self.setDisplayStatus(self.status['Direction'])
return
def statusFileName(self):
return self.name+".status"
def chosenWarrantFileName(self):
return self.name+".usedwarrants"
def restoreStatus(self):
try:
f = open(self.statusFileName(),"r")
self.status = pickle.load(f)
f.close()
except:
self.status = { 'Block': TrainsAndTrips[self.name]['DefaultStartBlock'], 'Direction': TrainsAndTrips[self.name]['DefaultDirection'] }
try:
f = open(self.chosenWarrantFileName(),"r")
self.previousWarrantFromBlock = pickle.load(f)
f.close()
except:
self.previousWarrantFromBlock = { 'noBlock': 'NoWarrant' }
return
def storeStatus(self):
print "storeStatus 1 ",self.status
f = open(self.statusFileName(),"w")
print "storeStatus 2"
pickle.dump(self.status,f)
print "storeStatus 3"
f.close()
print "storeStatus 4"
f = open(self.chosenWarrantFileName(),"w")
print "storeStatus 5"
pickle.dump(self.previousWarrantFromBlock,f)
print "storeStatus 6"
f.close()
self.StoredStatusVariable.setValue(self.status)
print "storeStatus 7"
return
def updateStatus(self,block,direction):
self.status = { 'Block': block, 'Direction': direction }
self.storeStatus()
return
def setDisplayStatus(self,m):
self.DisplayStatusVariable.setValue(m)
return
def setPlace(self,p):
self.PlaceVariable.setValue(p)
def setLocomotive(self, Loc):
print "setLocomotive", Loc
self.DCCAddress = Loc['DCCAddress']
self.Train = Loc['Train']
self.SoundDecoder = Loc['SoundDecoder']
E = Loc['E']
self.EPE = E['EPE']
self.LPE = E['LPE']
self.SLE = E['SLE']
self.ELE = E['ELE']
return
def runTrainOnce(self, Route):
print "runTrainOnce ",self.name
Warrant = Route['Warrant']
print "runTrainOnce 3 ",self.name
w = warrants.getWarrant(Warrant)
print "runTrainOnce 4 ",self.name
self.RosterEntry = w.getSpeedUtil().getRosterEntry()
print "runTrainOnce 5 ",self.name
self.Throttle = self.getThrottle(self.RosterEntry)
print "runTrainOnce 6 ",self.name
self.setPlace(self.status['Block'])
print "runTrainOnce 10 ",self.name
self.setDisplayStatus('Ready')
exec self.LPE
while (not w.isRouteFree()):
print "ROUTE STOLEN ", self.name
self.waitMsec(100)
self.setDisplayStatus('Running')
w.runWarrant(self.MODE_RUN)
self.waitWarrantRunState(w, self.MODE_RUN)
print "running ",self.name
block = "Start block"
# Some Märklin locomotives locks after a certain period and needs to be reset by power off/on
timer = threading.Timer(40.0, self.powerOffOn)
timer.start()
while (block != None):
self.setPlace(block)
block = self.waitWarrantBlockChange(w)
print "ude af waitWarrantBlockChange ",block
timer.cancel()
if (self.Entertainment.keys().count(block) > 0):
exec self.Entertainment[block]
print "ude af while loop"
self.updateStatus(Route['EndBlock'],Route['NextDirection'])
self.setPlace(self.status['Block'])
self.setDisplayStatus("Entering")
# Re-acquire the throttle. The Warrant has probably taken it away from us.
self.Throttle = self.getThrottle(self.RosterEntry)
exec self.EPE
return
def handle(self):
# handle() is called repeatedly until it returns false.
print "handle ", self.name, "waiting for sensor to run train"
self.setDisplayStatus('Push Run')
self.setPlace(self.status['Block'])
if (self.DCCAddress > 127):
self.Throttle = self.getThrottle(self.DCCAddress, True)
else:
self.Throttle = self.getThrottle(self.DCCAddress, False)
if (self.runSensor.getKnownState() == INACTIVE):
if (self.oldRunSensorState == True):
# The train has been running, but shall now stop - shut off sounds etc.
exec self.ELE
self.oldRunSensorState = False
self.waitSensorActive(self.runSensor)
if (self.oldRunSensorState == False):
# The train is about to start running - turn on sounds etc.
exec self.SLE
self.oldRunSensorState = True
print self.name, 'sensor active'
#Wait and then pick a random warrant
WarrantCandidates = TrainsAndTrips[self.name]['Warrants']
print 'WarrantCandidates',WarrantCandidates
print 'len',len(WarrantCandidates)
print 'WarrantCandidates 0',WarrantCandidates[0]
print 'random',Randomizer.nextInt(7)
self.setDisplayStatus('Waiting')
print "wait at platform ",self.name
TimeToWait = 1
for Warrant in WarrantCandidates:
if (Warrant['StartBlock'] == self.status['Block']):
TimeToWait = Warrant['TimeToWait']
break
self.waitMsec(TimeToWait)
FilteredWarrantCandidates = []
for Warrant in WarrantCandidates:
WarrantName = Warrant['Warrant']
WarrantMayRepeat = Warrant['MayRepeat']
StartBlockName = Warrant['StartBlock']
WarrantDirection = Warrant['Direction']
if (self.status['Block'] in self.previousWarrantFromBlock.keys()):
prevWFromBlock = self.previousWarrantFromBlock[self.status['Block']]['Warrant']
else:
prevWFromBlock = 'None'
print "StartBlock: ",StartBlockName
if (StartBlockName == self.status['Block'] and
(WarrantDirection == self.status['Direction'] or self.status['Direction'] == 'DontCare') and
(WarrantName != prevWFromBlock or WarrantMayRepeat == 'Yes')):
EndBlockName = Warrant['EndBlock']
print "Warrant: ", Warrant['Warrant'], " StartBlock: ",StartBlockName, " EndBlock: ", EndBlockName
StartBlock = oblocks.getBySystemName(StartBlockName)
EndBlock = oblocks.getBySystemName(EndBlockName)
w = warrants.getWarrant(WarrantName)
if (w.isRouteFree()):
FilteredWarrantCandidates.append(Warrant)
if (len(FilteredWarrantCandidates) == 0 and self.status['Block'] in self.previousWarrantFromBlock.keys()):
prevWarrant = self.previousWarrantFromBlock[self.status['Block']]
prevWarrantName = prevWarrant['Warrant']
w = warrants.getWarrant(prevWarrantName)
prevWarrantDirection = prevWarrant['Direction']
if (w.isRouteFree() and (prevWarrantDirection == self.status['Direction'] or self.status['Direction'] == 'DontCare')):
FilteredWarrantCandidates.append(prevWarrant)
if (len(FilteredWarrantCandidates) == 0):
self.waitMsec(3000)
return 1
ChosenIndex = Randomizer.nextInt(len(FilteredWarrantCandidates))
print 'ChosenIndex',ChosenIndex
Warrant = FilteredWarrantCandidates[ChosenIndex]
self.previousWarrantFromBlock[self.status['Block']] = Warrant
print "CHOSEN Warrant: ",Warrant
self.runTrainOnce(Warrant)
return 1
###################################################################################################
###################################################################################################
#
# START A THREAD PER LOCOMOTIVE TO RUN THAT LOCOMOTIVE
#
###################################################################################################
###################################################################################################
for Train in TrainsAndTrips.iterkeys():
print 'start tog',Train
NextTrain = RunTrain(Train)
NextTrain.start()
I hope this can be used as inspiration. Feel free to copy and modify as you please. As long as you do so for non-profit private purposes.