Automatiseret togdrift med SCWarrants i JMRI

På min bane kan indtil flere tog køre samtidig. Styret af min PC.

For at opnå dette anvender jeg JMRI på PC’en. JMRI er forbundet til min ECOS som så igen styrer selve togene og sporskifterne.

Hele min bane er dækket af virtuelle signaler. Dvs. at for enhver blok er der et signal til at dirigere togdriften. Derfor, og fordi jeg bruger signal controlled warrants, sker der ingen sammenstød.

Hvert tog har en hjemme-blok – typisk i en skyggebanegård – hvor den holder, når den ikke er på tur. Hjemme-blokken er reserveret til det tog. Dvs. ingen andre tog kommer nogensinde der.

I JMRI har jeg defineret et antal warrants for hvert tog. En warrant tager toget fra et stop (en station) til den næste. Det er vigtigt, at der er defineret tilstrækkeligt mange warrants til at toget kan komme videre, ligegyldigt hvor den standser, dvs. at der hvor en warrant ender skal der være defineret en anden warrant, der kan bringe toget videre. Og uanset udgangspunktet skal toget kunne komme tilbage dertil vha. en eller flere sammenhængende warrants.

Desuden har jeg lavet et Python script, der starter disse warrants, så togene bevæger sig rundt på banen på en semi-tilfældig måde. Jeg har altså ikke faste køreplaner. Det var tanken i starten, men det gav ikke det samme “liv”.

Udover at køre togene bidrager scriptet også til underholdningen ved at trutte i hornet, styre lyset, afvikle højttalerkald på stationerne m.v.

Scriptet er delt i to: Selve scriptet og et antal tog definitioner – et pr. tog.

Data definition

En tog definition ser f.eks. således ud (forklaring følger):

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

Togdefinitionen består af:
– Sensor: JMRI navnet på en sensor, der styrer, om toget er aktivt eller inaktivt. Jeg bruger en virtuel sensor, der bliver til en “knap” på skærmen. Men det kan også være en fysisk sensor forbundet til en fysisk kontakt.
– Locomotive: Definition af lokomotivet. Se nedenfor.
– Warrants: Listen af warrants for toget.
– DefaultStartBlock: JMRI navnet på hjemme-blokken.
– DefaultDirection: FWD eller REV. Indikerer om toget som default starter med at køre frem eller tilbage.  Kun relevant, hvis toget har en endestation som hjemmeblok, og hvis toget via en vendesløjfe nogen gange kommer “hjem” ved at køre fremad og andre gange ved at bakke.
– StoredStatus: Navnet på en JMRI memory variabel til at gemme status for toget, dvs. hvilken blok toget befinder sig i og om toget kørte fremad eller bakkede ind i blokken.
– DisplayStatus: Navnet på en JMRI memory variabel, der er beregnet til at vise på skærmen, så man kan se status (f.eks. “Waiting” eller “Running”) for toget.
– Place: Navnet på en JMRI memory variabel, der er beregnet til at vise på skærmen, hvilken blok toget befinder sig i.

Lokomotiv definitionen består af:
– Train: JMRI navnet på lokomotivet.
– DCCAddress: Lokomotivets DCC adresse. For kombinationen af  JMRI, ECOS og  Märklin MFX / M4 dekodere er der den specialitet, at eftersom meget i JMRI afhænger af DCC adressen, og eftersom disse dekodere ikke har nogen DCC adresse, så er der i JMRI opfundet en DCC adresse, der er 20.000 + ECOS id’en for pågældende lokomotiv. Jeg ved det, for jeg har selv implementeret det i JMRI.
– SoundDecoder: 0 eller 1 alt efter om lokomotivet er udstyret med lyddekoder. Informationen bruges p.t. ikke til noget.
– E: Definition af “underholdning”, dvs. nogle få linier Python kode, der udføres ved bestemte lejligheder. Det kan være hvad som helst. Men tanken er at aktivere lyde, lys m.v. på toget. I eksemplet ovenfor tændes og slukkes lyset via F0. Det kunne sådan set også være afspilning af en banegårdsmelding i en højttaler på en station eller dæmpning af lyset i rummet eller noget helt syvende.

Underholdningen består af:
– EPE: Enter Platform Entertainment. Udføres hver gang en warrant afsluttes.
– LPE: Leave Platform Entertainment. Udføres hver gang en warrant startes.
– SLE: Start Locomotive Entertainment. Udføres når toget aktiveres (vha. Sensor).
– ELE: End Locomotive Entertainment. Udføres når toget stoppes (vha. Sensor).
– 0 eller flere bloknavne. Udføres når toget kører ind i pågældende blok.

EPE, LPE, SLE og ELE skal være defineret. Bloknavnene er optionelle.

For hver warrant for det pågældende tog er der følgende attributter:
– Warrant: Navnet på JMRI warranten.
– TimeToWait: Det antal millisekunder, som toget skal holde ved perronen, før warranten sættes i gang. I virkeligheden tager scriptet TimeToWait for en tilfældig warrant med startblok hvor toget holder. For den specifikke warrant vælges efter ventetiden. Der kan jo ske ændringer i, hvilke blokke, der er frie, mens toget venter.
– Direction: Enten FWD eller REV. Indikerer om toget kører fremad eller bakker. Skal matche samme attribut i selve warranten.
– StartBlock: Navnet på første blok i warranten.
– EndBlock: Navnet på sidste blok i warranten.
– NextDirection: Enten FWD, REV eller DontCare. Den warrant, der vælges næste gang, skal have denne Direction værdi. DontCare betyder at der ikke er krav til Direction af næste warrant. Denne mekanisme er relevant for tog, der nogen gange vender den ene vej (kører fremad) og andre gange den anden (bakker), når den kører samme vej (f.eks. mod nord) gennem samme blok. Bemærk, at det kræver et dobbelt sæt af warrants med hver direction repræsenteret. Der skal endvidere være en vendesløjfe involveret et eller andet sted på banen, før mekanismen finder anvendelse.
– MayRepeat: Yes eller No. Indikerer hvorvidt samme warrant må bruges igen næste gang toget skal bringes ud af samme blok, også selvom der findes en alternativ warrant.

Scriptet

Scriptet består af forskellige elementer:
– Kode, der indlæser alle tilgængelige togdefinitioner.
– En klasse (RunTrain), der arver fra AbstractAutomation klassen i JMRI. Denne klassedefinition udgør hovedparten af koden.
– Initialiseringskode, der skaber et specifikt object af førnævnte klasse for hvert tog. Herefter lever disse objekter hver sit liv i separate tråde, der intet ved om hinanden.

RunTrain klassen består af:
– Noget initialiseringskode.
– Hjælpefunktion til at tænde og slukke strømmen på banen (for at vække et af mine tog, som kan finde på at gå i baglås).
– Hjælpefunktioner til at persistere status for toget på harddisken og til at genindlæse status som en del af initialiseringen. Før jeg lavede denne funktionalitet skulle alle tog være i deres hjemme-blok og vende rigtigt, før den automatiske togdrift kunne startes. Nu skal de blot deaktiveres og de igangværende warrants afsluttes, før jeg lukker ned. Så kan togdriften genoptages, næste gang jeg starter JMRI og banen op.
– runTrainOnce afvikler en given warrant og udfører underholdningskoden, opdaterer status på skærmen osv.
– handle er hoved-proceduren i alle klasser, der arver fra AbstractAutomation. Den bliver kaldt igen og igen. I alt væsentligt venter den til Sensor bliver aktiv og udvælger derefter den næste warrant og kalder runTrainOnce med den valgte warrant. Warranten bliver valgt ved en kombination af at startblokken er den blok, som toget befinder sig i, at reglerne omkring togretning beskrevet ovenfor er overholdt, at reglen om gentagelse så vidt muligt er overholdt samt endelig med et element af tilfældighed.

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()

Jeg håber dette kan tjene til inspiration. Alle er velkomne til at kopiere og modificere uhæmmet. Så længe det er til privat brug.