JMRI: Scripting "How To..."
Some hints and examples on how to do basic operations in scripts
- How do I run a saved script?
- How should I name variables in JMRI scripts?
- How do I create JMRI objects (sensors, memories, etc.) in a script?
- How do I access the JMRI application windows?
- How can I limit the priority of a script?
- How do I load a panel file from within a script?
- How do I find a file in the preferences directory?
- How do I wait for something(s) to change on my layout?
- How do I "listen" to a Turnout or Sensor?
- How can I get a script to play a sound?
- How do I invoke another script file from a script?
- How do I communicate between scripts?
See also Python/Jython Language for general FAQ about the language.
See also Jython Scripting "What...Where" for other interesting tidbits.
From the PanelPro main screen, click on "Panels" in the top menu. Or, from the DecoderPro main screen, click on "Actions" in the top menu. Select "Run Script..." from the dropdown.The directory set up in Preferences (which you can change) will appear and you can select a script to run. YOu can also navigate to any other directory that contains stored scripts. [On PCs, Jython script files have a ".py" suffix standing for Python.]
In many of the sample files, turnouts are referred to by names like "to12", signals by names like "si21", and sensors by names like "bo45". These conventions grew out of how some older code was written, and they can make the code clearer. But they are in no way required; the program doesn't care what you call variables.
For example, "self.to12" is just the name of a variable. You can call it anything you want, e.g. self.MyBigFatNameForTheLeftTurnout
The "self" part makes it completely local; "self" refers to "an object of the particular class I'm defining right now". Alternately, you can define a global variable, but that's not recommended. If you have multiple scripts running (and you can have as many as you'd like; we recommend that you put each signal head in a separate one), the variables can get confused if you use the same variable name to mean too different things. Using "self" like this one does makes sure that doesn't happen.
Note that turnouts, etc, do have "System Names" that look like "LT12". You'll see those occasionally, but that's something different from the variable names in a script file.
All of the JMRI input and output objects listed in Common Tools , such as sensors, memories, lights, signals, reporters, etc., can be created within a script (all can also be accessed, of course). A simple line of code typed into the Script Entry window is all it takes:
a=memories.provideMemory("IM" + "XXX") b=sensors.provideSensor("IS" + "XXX")
Using the "provide" method for a specific type of input or output either creates a new object with the system name specified (IMXXX or ISXXX in these examples) or finds a reference to an existing object. Variables within the object can then be set using statements like:
a.userName = "My memory 1" b.userName = "My Sensor 1 at East Yard" a.value = "Something I want to save" b.state = ACTIVE
System names versus user names
System names for sensors, turnouts, lights and others, are connection specific. A LocoNet sensor name begins with "LS" while a CMRI sensor begins with "CS" and an internal sensor with "IS". "provide" methods check the prefix of the passed string to determine if it matches a defined connection or internal. If so, and the rest of the name is valid, the sensor is created. If a match does not exist, it assigns the prefix for the first connection in the currently open connection list.
Example: If your connections are LocoNet and CMRI, a provideSensor request without a prefix of LS, CS or IS, will be assigned LS. If the name value is not one to four numeric digits (a LocoNet requirement), you get a bad format error. Memories are only internal, so provideMemory uses "IM" as the default prefix.
User names are set after input and outputs are created. They can be any character string you want. You can use user name or system name in scripts and Logix. The use of user names is recommended for these purposes if there is the possibility that you will be changing your control system in which case system names will be changing as well.
Click for more information on Names and Naming.
Alternative syntax in Python
The Python language provides alternative ways of addressing objects and methods. It is not the purpose of this FAQ to explain all of these, but it is worth noting that the above examples could have been written as:
No assignment operator (=) is necessary as the "set" methods do that work. Use whichever syntax you prefer, although it is a good practice is to be consistent for future readability.
locates the application's main window, and sets its location to the upper left corner of the screen.
call in the first line returns a list of the existing
windows. The  element of this list is the original
splash screen and the  element is the main window; after
that, they're the various windows in the order they are
created. To find a particular one, you can index through
the list checking e.g. the window's title with the
If the script runs a loop that's supposed to update something, it can't be written to run continuously or else it will just suck up as much computer time as it can. Rather, it should wait.
The best thing to do is to wait for something to change. For example, if your script looks at some sensors to decide what to do, wait for one of those sensors to change (see the sample scripts for examples)
Simpler, but not as efficient, is to just wait for a little while before checking again. For example
waitMsec(1000)causes an automaton or Siglet script to wait for 1000 milliseconds (one second) before continuing.
For just a simple script, something that doesn't use the Automat or Siglet classes, you can sleep by doing
from time import sleep sleep(10)The first line defines the "sleep" routine, and only needs to be done once. The second line then sleeps for 10 seconds. Note that the accuracy of this method is not as good as using one of the special classes.
You can always specify the complete pathname
to a file, e.g.
/Users/mine/.jmri/filename.xml. This is not very
portable from computer to computer, however, and can become a
pain to keep straight.
JMRI provides a routine to convert "portable" names to names your computer will recognize:
fullname = jmri.util.FileUtil.getExternalFilename("preference:filename.xml")
preference:" means to look for that file starting in the preferences directory on the current computer. Other choices are "program:" and "home:", see the documentation.
self.waitChange([self.sensorA, self.sensorB, self.sensorC])where you've previously defined each of those "self.sensorA" things via a line like:
self.sensorA = sensors.provideSensor("21")You can then check for various combinations like:
if self.sensorA.knownState == ACTIVE : print "The plane! The plane!" elif self.sensorB.knownState == INACTIVE : print "Would you believe a really fast bird?" else print "Nothing to see here, move along..."You can also wait for any changes in objects of multiple types:
self.waitChange([self.sensorA, self.turnoutB, self.signalC])Finally, you can specify the maximum time to wait before continuing even though nothing has changed:
self.waitChange([self.sensorA, self.sensorB, self.sensorC], 5000)will wait a maximum of 5000 milliseconds, e.g. 5 seconds. If nothing has changed, the script will continue anyway.
A single routine can listen to one or more Turnout, Sensor, etc.
If you retain a reference to your listener object, you can attach it to more than one object:
m = MyListener() turnouts.provideTurnout("12").addPropertyChangeListener(m) turnouts.provideTurnout("13").addPropertyChangeListener(m) turnouts.provideTurnout("14").addPropertyChangeListener(m)
But how does the listener know what changed?
A listener routine looks like this:
class MyListener(java.beans.PropertyChangeListener): def propertyChange(self, event): print "change",event.propertyName print "from", event.oldValue, "to", event.newValue print "source systemName", event.source.systemName print "source userName", event.source.userName
When the listener is called, it's given an object (called event above) that contains the name of the property that changed, plus the old and new values of that property.
You can also get a reference to the original object that changed via "name", and then do whatever you'd like through that. In the example above, you can retrieve the systemName, userName (or even other types of status).
The jython/SampleSound.py file shows how to play a sound within a script. Briefly, you load a sound into a variable ("snd" in this case), then call "play()" to play it once, etc.
Note that if more than one sound is playing at a time, the program combines them as best it can. Generally, it does a pretty good job.
You can combine the play() call with other logic to play a sound when a Sensor changes, etc. Ron McKinnon provided an example of doing this. It plays a railroad crossing bell when the sensor goes active.
# It listens for changes to a sensor, # and then plays a sound file when sensor active import jarray import jmri # create the sound object by loading a file snd = jmri.jmrit.Sound("resources/sounds/Crossing.wav") class SensndExample(jmri.jmrit.automat.Siglet) : # Modify this to define all of your turnouts, sensors and # signal heads. def defineIO(self): # get the sensor self.Sen1Sensor = sensors.provideSensor("473") # Register the inputs so setOutput will be called when needed. self.setInputs(jarray.array([self.Sen1Sensor], jmri.NamedBean)) return # setOutput is called when one of the inputs changes, and is # responsible for setting the correct output # # Modify this to do your calculation. def setOutput(self): if self.Sen1Sensor.knownState==ACTIVE: snd.play() return # end of class definition # start one of these up SensndExample().start()
def printStatus() : print "x is", x print "y is", y print "z is", z return x = 0 y = 0 z = 0Once that file has been executed, any later script can invoke the
printStatus()routine in the global namespace whenever needed.
zvariables are available to anybody. This can lead to obscure bugs if two different routines are using a variable of the same name, without realizing that they are sharing data with each other. Putting your code into "classes" is a way to avoid that.
Note that scripts imported into another script using
import statements are not in the same namespace
as other scripts and do not share variables or routines. To
share variables from the default namespace with an imported
script, you need to explicitly add the shared variable:
import myImport myImport.x = x # make x available to myImport