Randall Train Automation Controller:
“Conductor” and “RTAC” software
2023-07
RTAC is free software for model railroad computer control with the main goal of running trains automatically and performing other event-based automation. It uses JMRI to interface with a model railroad’s command station. |
Starting in 2016, I worked on automating the Randall Museum's model railroad, built by the Golden Gate Model Railroad Club.
The model railroad is an old HO DC model railroad that has been converted to DCC using an NCE command station and boosters. I wrote a fairly extensive description of the layout, which can be found here:
http://ralf.alfray.com/trains/randall_layout.html
The mainline turnouts are controlled using rotary toggles (non-momentary) that are directly connected to their corresponding Tortoise or PFM Fulgurex slow-motion turnout motors, as I explained here.
The model railroad is fairly large, and to get started we decided to only automate two parts of the layout: there are two specific automated routes, called the “passenger” and “branchline” routes.
Video of the Passenger Automation |
Video of the Branchline Automation |
I proposed several other automated lines and as this initial phase works well, we might expand it later.
The automation is designed to be used by the museum staff and consequently the whole system is designed to be as seamless as possible.
3.2- Route Scripting Language for Conductor v2
4.2- Event Language for Conductor v1
1- Automation Overview
Before digging into the details of how the automation software is implemented, this section is mostly educational and gives some background on the kind of equipment selected for this automation project.
Automating a model railroad requires three different aspects: sensors to detect the train, control of turnouts, and a way to connect all these to a computer running the control software. Here's a schematic overview of the system used on this layout:
The core of the automation is a Linux computer running JMRI, which interfaces with the NCE command station. Detectors are connected to the block panel to monitor which blocks draw power. DCC accessory decoders control the turnouts. JMRI provides a way to read data from the block sensors and control turnouts.
The system is not using any of the routes and logix modules of JMRI. Their system is in my opinion fairly limited in capabilities, not very flexible, yet at the same time extremely complex and obscure to set up for the benefit offered. Their UI model is a click-o-drome where everything is configured using a confusing bazillion of poorly designed dialog boxes and it’s fairly impossible to get an overview at a glance of the programmed system.
Instead Conductor is programmed using a single text file that describes all the events and actions in a very simple manner using a rich and descriptive custom programming language. The language design was inspired by my experience with ladder logic (such as Grafcet) as used in industrial programmable logic controllers.
One of the main goals of the changes made to the layout is to be actually as unobtrusive as possible. Most notably, the model railroad must be able to function in DCC without the aid of the computer (e.g. in case of malfunction).
This design and the choice made imply there are some compromises:
- Block detection is limited to pure basic “something is drawing power” detection.
- We don’t use Railcom and the system does not know which locomotive is on a given block.
- Such block detection only detects engines (which motors draw power from the track) and lighted cars. They do not detect cars which are electrically isolated from the track. This is not a problem in the automation we are currently running but it’s something everyone who has to deal with block detection must keep in mind -- for example how does one detect that part of a train has detached.
- The typical work-around for this is to add resistors on cars’ trucks.
- Originally, the NCE Switch-8 was chosen because it can control the Tortoise turnouts from the mainline and due to its advertised capability of being driven by non-momentary toggles.
- It so turned out that I quickly realized not all the mainline turnouts had been converted to Tortoise. There are still a number of old Fulgurex turnout motors on the layout. As explained on the page NCE Switch-8 and PFM Fulgurex, this can be solved by extra relays or DS64.
- It also turned out that the NCE Switch-8 did not actually work with non-momentary toggles, which was solved by reprogramming them as explained in NCE Button Board and non-momentary contacts.
- Finally, due to cost and time constraints, not all the blocks and not all the turnouts are equipped with sensors and DCC accessory control. A strategic number of them have been equipped as required by the first phase of the automation, with the goal to add more later as the system gets developed.
There are two automated routes with two different trains. Each route works in a similar principle: a train is stopped and waiting at a starting point. When an activation button is pressed, the train moves up the route till another specific point where it stops and reverses course towards the starting point. This is a point-to-point back-and-forth travel.
I do get a few comments that this is a “simple” point-to-point automation. This kind of “shuttle” scheme has indeed been done repeatedly in DC using relays and there are readily available commercial modules doing just that.
The system I created allows us to do much more than just run the trains -- we have the same fine control over DCC functions as operators do. The trains are programmed to accelerate and decelerate depending on where they are in their route, as well as use all the DCC functions such as lights, bell, and horn. Moreover, speed and functions can be easily customized for each specific engine, a feature very useful to us as we routinely rotate our trains every few months to avoid excessive tear.
However my plan all along was to offer more than just a shuttle-type automation. Eventually what I want is to do route management where a given train runs along a predetermined route and only enters it when it is safe to enter. For example, right now we have a single passenger train on the mainline. What I envision later is multiple trains on the mainline, waiting for critical sections of the routes being free and automatically stopping and using sidings to pass each other.
Even with the current “shuttle” automation design, the flexible automation has proven useful to automate tasks beyond pure animation -- see section 3.2 below about the Conductor Event Language for an example.
The current system is in its second incarnation and offers a fairly good flexibility.
2- Conductor v1 & v2
The automation is controlled by a Linux computer running JMRI, a custom Java program named “Conductor” and an Android application named “RTAC”, the “Randall Train Automation Controller”. I built Conductor and RTAC specifically for the Randall automation, yet they should be flexible enough to be reused for other train automation projects.
Features of Conductor:
- Conductor is a Java app that integrates with JMRI. It can run on all platforms supported by JMRI (Windows, Mac, and Linux).
- It has access to the turnouts, sensors and throttles defined in JMRI.
- Conductor is controlled using an automation script with a custom language that I developed for that purpose.
- The script responds to sensor events and controls DCC turnouts and DCC engines via JMRI.
- This allows the automation to be easily modified without recompiling the program.
There are two versions of Conductor:
- Conductor v1 was written in 2017 in Java and using a custom event language.
- Conductor v2 is a complete rewrite in 2022, using Kotlin Scripting Language with a custom DSL.
3- Conductor v2
Conductor v2 is a 2022 rewrite of the main server automation software. It’s based around the Kotlin Scripting Language with a custom DSL and offers a cleaner way to describe the train routes used for automation than the first iteration of the software.
3.1- Screenshots & Behavior
Conductor v2 runs as an extension to JMRI. The interface is a full-screen “kiosk” display of the track with the state of the sensors and the controlled trains:
The entire behavior is controlled using a script that defines the train routes and the events and their actions. The scripting language is detailed in the next section.
The Conductor app is invoked from JMRI using its Jython extension bridge. Conductor’s main role is to run a script that drives the automation. It uses an event-based language created for that purpose that updates turnouts and engines in reaction to a combination of sensors and timer inputs. Sensors can be either activation buttons or track occupancy sensors defined in JMRI. Depending on the state of the automation and the location of the engines, the script can change their speed, change the lights, blow the horn, set turnouts as required. It is essentially a fully automated DCC cab.
The Randall museum uses an NCE command station and boosters. Sensors are detected using NCE AIU01 modules. Turnouts are controlled using NCE Switch-8 or Digitrax DS64 modules. Since Conductor interfaces with JMRI, it is not specifically tied to NCE hardware. It can interface with any throttle, sensor or turnout that can be controlled via JMRI.
One key feature of the automation script is that it is both timer-based and sensor-based. Model train motors are analog and their speed and running is not precise enough for automation. The only way for an automation to be reliable is to physically know the location of the train on the track, which is typically done using block occupancy sensors. The track is divided into blocks and electrical sensors detect when a train enters and leaves specific blocks. These events are precise as they are directly related to specific locations on the track that are known and never change. Such location events are ideal to control the speed of the train, to tell it to start, accelerate, stop, etc. Timer events are good enough for less critical tasks like blowing the train horns. Usually both techniques are combined, which results in excellent control, for example a train can be set to stop after entering a block with a delay so that it actually stops in front of a station when taking momentum into account and can then leave after a specific time.
One of the main improvements of Conductor v2 over the previous version is that the script is designed around the concept of “routes”: a route is a series of blocks traveled by one train. A route is active when a train is traveling on it, and the engine monitors all the blocks on the route, keeping track of where the train is, and ensuring the train actually moves from block to block in the expected fashion and time. It also ensures that no unexpected block shows occupancy -- we don’t want unexpected engines or cars to be present where a train is expected to run.
This allows the script to now have “recovery routes”: when trains are not at the expected location, the script tries to guess where the train is located on a route and how to bring it back to its original location. The current script implementation only handles the most trivial recovery scenarios. There are a few ambiguous cases which are explicitly not covered for now.
Another main improvements of Conductor v2 over the previous version is a simulator:
The simulator is a standalone version of Conductor running without JMRI -- it does not need to connect to any command station. The simulator simulates the track sensors and the train positions by examining the current script and figuring the train routes encoded in the script.
3.2- Route Scripting Language for Conductor v2
The scripting language for Conductor v2 has two possible paradigms:
- Logical train routes, with managed block occupancy.
- Isolated events.
3.2.1- Route Logic
In the automation, a train is always associated with a route. The route defines where this train can go, and which blocks it will occupy to get there. Multiple trains can be active at the same time as long as their routes do not intersect, that is they do not use the same blocks.
There currently are 3 types of routes in the scripting language:
- “Sequence” routes are defined as a sequence of blocks that a train must occupy to go from a point A to a point B.
- Sequence routes can define “shuttle” animation (when a train goes to a place then drives back to the starting point), or point-to-point animations, or even “circular” animations where a train performs a full loop and then comes back to its starting position.
- “Idle” routes are, as their name suggests, routes that do nothing. This is used when a train is idle, for example white waiting to be activated and start.
Some other types of routes will be added later.
A train can only be associated with one route at a time. Typically a train is associated with an “idle route” while it is waiting for activation. When the automation starts, the train is then associated with a corresponding “sequence route”.
Each sequence route is defined as a non-cyclic graph composed of the blocks that the train must traverse in order to reach its destination.
Each node in the graph represents one block with all the associated actions that must happen when the train enters or leaves the block.
For example, the Brancheline automation at Randall has a simple linear sequence with a graph like this:
Since this is a “shuttle” sequence, the train goes through each block twice, once in forward and then later in reverse. Thus the sequence graph has two nodes for each block -- the script actions are attached to the nodes, and are thus different when the train first goes through a block and later comes back through that same block.
Here’s an excerpt from the corresponding script:
val B801 = block("NS752") named "BLParked" val B820 = block("NS753") named "BLStation" val B830 = block("B830v") named "B830" val B850 = block("NS754") named "BLTunnel" val B860 = block("NS755") named "BLReverse"
val BL = throttle(204) { name = "BL" onBell { on -> throttle.f1(on) } onSound { on -> throttle.f8(on) } }
val BL_Route = routes { name = "Branchline" toggle = BL_Toggle }
val BL_Idle_Route = BL_Route.idle { // The idle route is where we check whether a Branchline train should start. name = "Ready"
onIdle { if (BL_Toggle.active && AIU_Motion.active) { BL_Shuttle_Route.activate() } } }
val BL_Shuttle_Route = BL_Route.sequence { // The normal "shuttle sequence" for the branchline train. name = "Shuttle" throttle = BL maxSecondsOnBlock = 120 // 2 minutes per block max
val BL801_fwd = node(B801) { onEnter { BL.sound(On) BL.horn() BL.bell(On) after (8.seconds) then { BL.bell(Off) BL.horn() BL.light(On) BL.forward(4.speed) } } }
val BL820_fwd = node(B820) { onEnter { BL.bell(Off) BL.forward(4.speed) after (5.seconds) then { BL.forward(6.speed) } } } …
val BL860 = node(B860) { onEnter { BL.horn() BL.bell(On) after (8.seconds) then { BL.stop() } and_after (25.seconds) then { BL.reverse(6.speed) } } }
… val BL820_rev = node(B820) { onEnter { BL.bell(On) T324.normal() BL.reverse(4.speed) after (6.seconds) then { BL.stop() BL.horn() } and_after (3.seconds) then { BL.horn() BL.reverse(4.speed) } } }
val BL801_rev = node(B801) { onEnter { after (10.seconds) then { BL.stop() } and_after (3.seconds) then { BL.bell(Off) } } }
onActivate { ... }
onError { BL_Recovery_Route.activate() }
sequence = listOf( BL801_fwd, BL820_fwd, B830_fwd, BL850_fwd, BL860, BL850_rev, B830_rev, BL820_rev, BL801_rev) } |
Let’s look at some parts of this script.
The script first defines which blocks it can interact with. These map sensor names in JMRI. “NS752” is a JMRI name for an NCE block sensor, whereas “LS01” would be a JMRI name for a LocoNet sensor. No matter what the JMRI name, the script variable can be named anything relevant, and the block can be given a “friendly” name to make it easier to understand by the user:
val NceBlock = block("NS752") named "BLParked" val LocoNetBlock = block("LS01") named "BLStation" |
A “throttle” defines a DCC throttle, a.k.a. an engine, that can be controlled:
val UP = throttle(8330) name = "BL" onBell { on -> throttle.f1(on) } onSound { on -> throttle.f8(on) } } |
The lines above would declare that we can control engine DCC #8330 (likely a Union Pacific train), and engine DCC #204 is the one on the Brancheline (a.k.a. “BL”). Moreover, we can redefine the DCC functions used for the engine’s bell, sound, or horn.
A “routes” collection is a group of routes, of which only one can be active at the same time. In this case, our branchline train can be idle, running normally, be in recovery, or be in error. Each state is associated with a specific route.
val BL_Route = routes { name = "Branchline" toggle = BL_Toggle }
val BL_Idle_Route = BL_Route.idle { // The idle route is where we check whether a Branchline train should start. name = "Ready"
onIdle { if (BL_Toggle.active && AIU_Motion.active) { BL_Shuttle_Route.activate() } } } |
This block above creates one “routes” collection (named “BL_Route”), to which an idle route is added. The idle route is the default one, and its on-idle callback monitors the activation sensor -- when the sensor becomes active, the shuttle route is activated and becomes the default for that route collection.
The shuttle route is a sequence, which is added to the routes collection:
val BL_Shuttle_Route = BL_Route.sequence { // The normal "shuttle sequence" for the branchline train. name = "Shuttle" throttle = BL maxSecondsOnBlock = 120 // 2 minutes per block max
val BL801_fwd = node(B801) { … } val BL820_fwd = node(B820) { … } val BL830_fwd = node(B830) { … } val BL850_fwd = node(B850) { … } val BL860 = node(B860) { … } val BL850_rev = node(B850) { … } val BL830_rev = node(B830) { … } val BL820_rev = node(B820) { … } val BL801_rev = node(B801) { … }
onActivate { ... }
onError { BL_Recovery_Route.activate() }
sequence = listOf( BL801_fwd, BL820_fwd, B830_fwd, BL850_fwd, BL860, BL850_rev, B830_rev, BL820_rev, BL801_rev) } |
Since this route is a sequence, the train runs twice through most of the blocks, in different directions. The graph of the sequence encodes the blocks which are visited by the train -- this allows Conductor to know which block is next in the sequence.
The sequence also defines which DCC throttle is manipulated by the script. maxSecondsOnBlock is setting that allows the scripting engine to automatically ensure that the train is not stuck somewhere -- in this case, we tell Conductor that the train always takes less than 2 minutes to cross any block. Any longer would result in an error condition.
The “onError” callback allows us to specify what to do when an error occurs -- in this case, to activate the special recovery route (not shown here).
Each node in the graph has its own action blocks. Most of the time, what we want is to define what happens when the train enters a block. For example, at the reversal point, we want the engine to blow the horn, then stop, and later to reverse after a specific amount of time. This is expressed as such:
val BL860 = node(B860) { onEnter { BL.horn() BL.bell(On) after (8.seconds) then { BL.stop() } and_after (25.seconds) then { BL.reverse(6.speed) } } } |
The node has various callbacks that can be used -- on enter, on exit, while occupied, etc.
The “after … then … and_after” are specific commands that trigger delayed actions as long as the train is located on this block.
One particularity of the script above is that the train has very different behaviors on block B820 depending on its direction:
val BL820_fwd = node(B820) { onEnter { BL.bell(Off) BL.forward(4.speed) after (5.seconds) then { BL.forward(6.speed) } } } … val BL820_rev = node(B820) { onEnter { BL.bell(On) T324.normal() BL.reverse(4.speed) after (6.seconds) then { BL.stop() BL.horn() } and_after (3.seconds) then { BL.horn() BL.reverse(4.speed) } } } |
In the forward direction, the engine simply continues forward with a speed change after 5 seconds.
However in the reverse direction, when coming back, the engine actually stops as there’s a little sub-station at this location. It stops for 3 seconds before continuing back towards the parking location.
There are a few more advanced features to the route logic than can fit in this simple introduction.
3.2.2- Events
The event language from Conductor v1 is still available in Conductor v2, albeit with a different syntax: events are simple “conditions → actions”. The script engine evaluates all instructions in parallel in a 30 Hz loop and then executes instructions when their conditions match. This is directly inspired by ladder logic as used in industrial programmable logic controllers (such as Grafcet).
This allows any kind of event to be programmed, even if they are not directly connected to a specific train and its route.
Here’s an example of an event script:
val Block320 = block ("NS770") named "B320" val Block321 = block ("NS771") named "B321" val Block330 = block ("NS773") named "B330"
val T330 = turnout("NT330")
on { !Block330 && Block320.active && !Block321 } then { T330.normal() } on { !Block330 && !Block320 && Block321.active } then { T330.reverse() } |
It’s important to remember that all the “conditions → actions” lines are evaluated in parallel, all at the same time. Once they have all been evaluated, the actions will be carried out in the order they are defined in the script.
The “block” line registers a sensor, which must be declared in JMRI. “NS784” is a JMRI sensor for an NCE AIU01 on address 50, bit 1. On a LocoNet bus, a similar line would look like:
val MyBlock = block(“LS01”) as NAME |
The sensor name is used in the script and is independent from the sensor “username” used in JMRI.
Similarly, the “turnout” line defines a turnout which must have been declared in JMRI. In this case, “NT330” is a turnout at DCC address 330 on an NCE bus in JMRI.
val T330 = turnout("NT330") |
In JMRI these are the “system names” and they are found in the Sensors and Turnouts tables.
Below are actual conditions → actions lines.
on { !Block330 && Block320.active && !Block321 } then { T330.normal() } on { !Block330 && !Block320 && Block321.active } then { T330.reverse() } |
These two lines mean that:
- If the block B330 is off, and block B320 turns on, then set the turnout T330 to Normal.
- Similarly, if the block B330 is off, and block B321 turns on, then set the turnout T330 to Reverse.
- If both blocks B320 and B321 turn on, the program does not alter the turnout.
On the Randall Model Railroad, this corresponds to this piece of track:
On the layout, operators typically run from right to left on this section of the track. A very common error is for operators to forget to align their turnouts and then be surprised when their train shorts or derails. These two event lines fix that: when a train is on block B320 (right) and block B330 after the turnout is empty, the Sonora turnout can be automatically aligned towards the incoming train on B320. Same pattern for a train coming from B321.
This shows how the script does not have to deal with just the “train automation” of the layout. It can also be used to automate parts of the layout that have simple usage patterns.
4- Conductor v1
The initial version of Conductor is architectured around a custom event-based language. The scripting engine is still available, and is activated when the input script is detected to be using the v1 language.
4.1- Screenshots & Behavior
Conductor v1 only runs as an extension to JMRI. The interface is a simple text-based status window that opens when the entry-point Conductor.py is loaded as a Jython script in JMRI:
That screenshot looks complex and probably intimidating. This is only seen on the server, and end-users (layout operators, museum staff) do not have to interact with it.
The Conductor app is invoked from JMRI using its Jython extension bridge. Conductor’s main role is to run a script that drives the automation. It uses an event-based language I created for that purpose that updates turnouts and engines in reaction to a combination of sensors and timer inputs. Sensors can be either activation buttons or track occupancy sensors defined in JMRI. Depending on the state of the automation and the location of the engines, the script can change their speed, change the lights, blow the horn, set turnouts as required. It is essentially a fully automated DCC cab.
For ease of developing and testing, the Conductor app also has a second custom language to simulate the automation. This simulates the progression of the train through the layout, simulating the activation of track occupancy sensors as if the actual engines were moving along.
Here’s an example of the event-based script on the left and the simulation script on the right:
In this example the piece of script on the left above indicates that when the train is stopped on block B503b (the main passenger station) and the activation button is pressed, turn on the train sound & lights and start moving at a reduced station speed. After 2 seconds, blow the horn. Once the train reaches block B503a, accelerate to full speed.
On the right side, the simulation script waits for the same activation button to be pressed and once pressed simulates the train moving from block B503b to B503a after 8 seconds. The simulation script can then be used to test the automation script even when the computer is not connected to the layout and no train is actually moving. Once the simulation is perfected, the system can be connected to the layout to try with actual trains.
Contrary to Conductor v2, the script has no knowledge of routes. It is essentially a fairly large state machine. Keeping track of which train is running and in which direction is done by using a variety of enum state variables.
4.2- Event Language for Conductor v1
The event language uses a simple “conditions → actions” model. The script engine evaluates all instructions in parallel in a 30 Hz loop and then executes instructions when their conditions match. This is directly inspired by ladder logic as used in industrial programmable logic controllers (such as Grafcet).
Here’s an example of an event script:
Sensor B320 = NS784 Sensor B321 = NS785 Sensor B330 = NS786
Turnout T330 = NT330
!PA-Toggle & !B330 & B320 -> T330 Normal !PA-Toggle & !B330 & B321 -> T330 Reverse |
It’s important to remember that all the “conditions → actions” lines are evaluated in parallel, all at the same time. Once they have all been evaluated, the actions will be carried out in the order they are defined in the script.
The “Sensor” line registers a sensor, which must be declared in JMRI. “NS784” is a JMRI sensor for an NCE AIU01 on address 50, bit 1. On a LocoNet bus, a similar line would look like:
Sensor NAME = LS01 |
The sensor name is used in the script and is independent from the sensor “username” used in JMRI.
Similarly, the “Turnout” line defines a turnout which must have been declared in JMRI. In this case, “NT330” is a turnout at DCC address 330 on an NCE bus in JMRI.
Turnout T330 = NT330 |
In JMRI these are the “system names” and they are found in the Sensors and Turnouts tables.
Below are actual conditions → actions lines.
!B330 & B320 & !B321 -> T330 Normal !B330 & B321 & !B320 -> T330 Reverse |
These two lines mean that:
- If the block B330 is off, and block B320 turns on, then set the turnout T330 to Normal.
- Similarly, if the block B330 is off, and block B321 turns on, then set the turnout T330 to Reverse.
- If both blocks B320 and B321 turn on, the program does not alter the turnout.
On the Randall Model Railroad, this corresponds to this piece of track:
On the layout, operators typically run from right to left on this section of the track. A very common error is for operators to forget to align their turnouts and then be surprised when their train shorts or derails. These two event lines fix that: when a train is on block B320 (right) and block B330 after the turnout is empty, the Sonora turnout can be automatically aligned towards the incoming train on B320. Same pattern for a train coming from B321.
This shows how the script does not have to deal with just the “animation” of the layout. It can also be used to automate parts of the layout that have simple usage patterns.
Let’s take another example, which this time is a simplified version the Passenger Automation train as can be seen in the video above:
# Define which DCC engine to control Throttle Loco = 204
# Define various states for the automation Enum State = Idle Station Up Summit Down
# Define some speeds to use Int Station-Speed = 8 Int Summit-Speed = 12
# Define some timers Timer Timer-Acquire-Route = 1 Timer Timer-Release-Route = 1
# --- Train starts in "Idle" state. # Once the main automation toggle is turned on, turn on the sound and light, # and set the train in the "Station" state.
State == Idle & Toggle -> Loco Sound = 1; Loco Light = 1; Loco Stop ; State = Station
# --- State: Station # Departure from Station (going up) when users press the activation button
State == Station & B503b & Toggle & Loco Stopped & Run3 -> State = Up ; Reset Timers
# --- State: Going Up # Start the train at station speed and align all turnouts as desired.
State == Up & B503b & Loco Stopped -> Loco Light = 1 ; Loco Sound = 1 ; Loco Forward = Station-Speed ; Loco Horn ; Timer-Acquire-Route Start
Timer-Acquire-Route -> T311 Reverse; T320 Reverse ; T321 Normal ; T330 Normal
# Once we reach the next block, go to full track speed
State == Up & B503a -> Loco Forward = Full-Speed
# Mid-Station doppler sound on the way up
State == Up & B320 + 27 -> Loco F3 = 1 ; Loco F3 = 0
# Reduce speed when reaching the Sonora bridge # Speed up again after the tunnel on the way up
State == Up & B330 -> Loco Forward = Sonora-Speed State == Up & B330 + 12 -> Loco Forward = Full-Speed ; Loco Horn
# Reaching the Summit, change the state of the automation
State == Up & B360 -> State = Summit
# --- State: Summit
# At the top first reduce a bit the speed
State == Summit & B370 + 9 -> Loco Forward = Summit-Speed
# Then stop and after a delay and a few horn effects, reverse direction.
State == Summit & B370 + 14 -> Loco Stop ; Loco Horn
State == Summit & B370 + 22 -> Loco Horn; Loco Reverse = Summit-Speed
# Once the train reaches the next block, change to the "going down" state of the automation # and make sure all turnouts are still aligned as desired.
State == Summit & !B370 & B360 & Loco Reverse -> State = Down ; Timer-Acquire-Route Start ... |
There are a few interesting things to point out here.
First, this line defines which DCC address (a.k.a. throttle) to use:
Throttle Loco = 204 |
Many throttles can be defined, each with a specific name. Each throttle represent one or more DCC addresses, so it’s easy to create “soft consists”.
These are then used in the script as actions to stop the engine, change the speed or act on functions. For example the following line defines multiple actions to be done on that DCC address when the conditions are met, namely turn the lights on (F0), set the sound (F8), set the seed to forward, blow the horn (F2) and turn on function F3:
conditions -> Loco Light = 1 ; Loco Sound = 1 ; Loco Forward = Station-Speed ; Loco Horn ; Loco F3 = 1 |
The speed can be set using direct values or using named integer constants. The advantage of using the named constants is that they can be easily changed in a script and cary their own semantics:
Throttle Loco = 204
Int Diverging-Speed = 8 Int Full-Speed = 24
condition-1 -> Loco Forward = Full-Speed condition-2 -> Loco Forward = 28 condition-3 -> Loco Reverse = Diverging-Speed condition-4 -> Loco Stop |
The engine’s direction can be used as a condition, so it’s easy to have different behavior when the engines are going forward or reverse; for example the following lines would set different speed on a given block depending on the direction of the engine, or would blow the horn when it stops:
Block-1 & Loco Forward -> Loco Forward = Full-Speed Block-1 & Loco Reverse -> Loco Reverse = Diverging-Speed Block-1 & Loco Stopped -> Loco Horn |
An integral part of the automation is having actions vary with time. For this, timers can be defined with a predetermined time in seconds. An action starts a timer and when the timer goes off, it can execute one or more actions. Example:
Timer Delayed-Horn = 2 Timer Delayed-Speed-Reduction = 4 Int Diverging-Speed = 8
Block-1 & Loco Forward -> Delayed-Horn Start ; Delayed-Speed-Reduction Start
Delayed-Horn --> Loco Horn Delayed-Speed-Reduction & Loco Forward -> Loco Forward = Diverging-Speed |
Since a lot of the program has to deal with performing an action a number of seconds after a train enters a specific block, there is a special shortcut syntax for this:
Int Diverging-Speed = 8
Block-1 + 2 & Loco Forward -> Loco Horn Block-1 + 4 & Loco Forward -> Loco Forward = Diverging-Speed |
Both examples above do the same thing: the train enters block-1 and this starts a timer. 2 seconds in the block the horn is blown, and 4 seconds in the block, the speed is changed. However these actions only happen if the train is moving forward when the timer expires, so there could be a different behavior if the train is going in reverse or stopped.
Finally, an important part of the automation program is defining “states”. States are free-form keywords defined for whatever makes sense in the current program. For example the Randall Passenger automation has states for the train being idle (automation is turned off), waiting at the station, going up, reaching the summit and then going down. This allows the program to easily have different behaviors depending on the state of the automation.
Enum State = Idle Station Up Summit Down
State == Station & Block-Station & Button-Pushed & Loco Stopped -> State = Up; Loco Forward = 8 State == Up & Block-1 -> do something State == Down & Block-1 -> do something else |
In this made-up example, the first line indicates if the train is in state “station” and on the station block, then pushing the automation button starts the train and changes it to the “going up” state. This makes the program much easier to understand and follow.
The states can then be used to differentiate the behavior of the train on a given block depending on whether it’s going in one direction or another. This is similar to using the engine’s direction, only with an easier to understand semantic.
5- RTAC, the Android Software
Once the system is installed on the Randall Model Railroad, the staff in charge does not have to access the computer. It is essentially running as a “headless” computer hidden away under the layout, set to automatically turn on when the layout’s power is turned on and then to automatically shutdown when the layout power is turned off.
To be able to easily interact and monitor the state of the automation, two tablets are provided that run a custom Android app named “RTAC”, which is the visible part of the “Randall Train Automation Controller”.
Features of RTAC:
- RTAC is a custom Android app that runs on two tablets.
- It’s synchronized with Conductor to display the automation status and the track occupancy.
- RTAC automatically connects to the Conductor server using the Zeroconf protocol.
- RTAC features an “Emergency Stop” which users can use to stop the automation, for example if a train derails. Once the Conductor software goes in emergency stop, the tablet can be used to reset the system with some clear indication on screen of what to do.
- Both tablets are synchronized so that the display is the same on both and either can be used to trigger the emergency stop or reset the automation.
The Conductor app is also a server. It communicates with the two tablets to display the status of the automation and the track occupancy. The tablets are connected over wifi with the same private network as the computer driving the automation. The Conductor app and the tablets find each other using Zeroconf; no configuration is needed on the tablet side.
The tablet software is designed to operate in “kiosk mode” meaning that as soon as the tablet is turned on, the RTAC software takes over and prevents users from doing anything else with the tablet. Normally only the museum staff should have access to these tablets, and they should not be able to close or dismiss the app by mistake.
Pressing the “E-Stop” (Emergency Stop) button on either tablet asks for a confirmation dialog. Once the staff confirms the order, the Conductor app stops the train immediately using the NCE/DCC emergency stop command (which means trains stop immediately, regardless of their decelerating momentum). The staff is then instructed to bring back the trains to their expected origin location and then reset the automation.
Although Conductor and RTAC are specific to the Randall Museum's model railroad for now, they are designed to be flexible and be reused to automate other model train layouts. The source is available here; it is a standard Android application built using Android Studio and Gradle; it uses Dagger, Mockito, Robolectric, JSON; it relies on my own custom RX library, and my own KV network protocol. Please contact me if interested in details.
RTAC is also available for download on the Google Play store.
6- Privacy Policy for RTAC
Alf-Labs Android App Privacy Policy:
- The application complies with the privacy and security policies set by the Android Play Store.
- The application does not collect nor transmit personal or sensitive user data.
The following non-exclusive list explains Android permissions usage:
- Internet: This application accesses your local network to perform automatic discovery of Conductor servers via the ZeroConf protocol and communicate with the selected Conductor server.
- Access Wifi State: This application queries the state of the WiFi. If wifi is not enabled, the application does not try to connect to servers. The application only queries the wifi state, it cannot change it.
- Wake Lock: This application maintains a “wake lock” to prevent the device from sleeping once connected to a Conductor server. The lock is released when the app is disconnected from the server. This prevents the tablet from entering sleep mode while the automation is running.
- Foreground Service: A “foreground service” is used to maintain a permanent connection to a Conductor server once connected. When the service is running, the app icon is clearly visible in the notification bar to remind you that the app is still running. Tap the notification icon to return to the app and use it or disconnect from the server if you are done using it.
~~
~~