Model Train-related Notes Blog -- these are personal notes and musings on the subject of model train control, automation, electronics, or whatever I find interesting. I also have more posts in a blog dedicated to the maintenance of the Randall Museum Model Railroad.

2018-12-14 - Conductor 2: Route / Events Programming

Category Rtac

Just to keep some perspective, here is how the current Conductor 1 script is architectured:

  • There are 3 routes, 2 for passenger and 1 for branchline, with the passenger ones alternating.
    • Where appropriate, the routes define variables for speed & timers upfront so that they can be adjusted for new engines / train configuration.
    • They lack profiles but these are essentially an extension of the current setup.
  • Within each route are sequences. Since the script is a parallel execution script, the sequences are implemented using state keeping, but that’s mostly what the states are for.
    • That’s actually a bit of an issue as it makes the states hard to figure and keep track of, creating some unforeseen complications sometimes.
  • There are events not associated with any route.
    • Main example is the Sonora switch in non-automated mode.
    • A lesser example is the RTAC display especially for stopped/error modes.

Having changed the trains for automation recently, I can see where the system works and where it breaks:

  • Mostly I changed the passenger trains just by adjusting the speeds.
  • I did alter 1 or 2 timers, for example the Freight’s run time before stopping at the mid station.
  • It broke when I tried to use an MTH engine as its functions usage is vastly different.
  • Same goes for the Rapido which has more fancy functions. In that case changing the engine in a route would require customizing the script for different functions.

Function differentiation could be done in the script by having, for example, an integer variable for a grade crossing sound for example and then making sure the command that triggers that can use that variable. An invalid value such as -1 could be used to disable the command.

Another suggestion is to allow a route script to be exactly that, a condition sequence of tests/actions. So for example we could write “if address = 1234 then { on (condition) → (events) }”.  This is important -- it means the route events closure is executed at each iteration, whereas right now it would be executed once during startup to define all the events with delayed condition closures. There’s some benefit in doing that as it would allow the events closure to be fully scripted with large if-block conditionals, loops, etc. Switching on states could actually using a switch/case instruction with an enum.

We still need to address having events not associated with a route. It’s possible to make a default “no-automated-route-active” route, but that seems a bit clumsy. Unless I just call it “default” and it’s not too bad. Rather than see these as synthetic routes, we could see them as special triggers: startup {}, loop {}, shutdown {}. The startup would happen once before any route is executed. The loop would run at the end of each iteration after the routes, and the shutdown would happen after the last route executes.

One part to elaborate is the sequencing with a route and the sequence management in a route.

The goal is to do reservation and as indicated above for the sake of granularity we could do either sub-routes or automatic freeing. In the case of ABS on a continuous loop, we can do “on the fly” reservation using  an N+2 window. For our use case it makes sense to start with 3 modes: global reservation, global reservation with automatic freeing, ABS window reservation.

Implementation wise that means we’ll have different reservation strategies and plug the proper engine based on the route’s definition, with a sensible default (simplest case).

If we exclude the harder case of sub-routes, it makes sense to define the route as an ordered set of blocks.

Originally I wanted to add turnouts in the routes. However given the way the layout is wired at Randall, turnouts are always part of a specific block. They do not have a sensor of their own, and never have their own block. One reason to add them is so that they can be locked by a route reservation. This is good if we describe the rails as a graph structure, as the turnouts are naturally the nodes linking the paths, and it could allow automatic re-routing. However here we’re not going to do that. Consequently listing turnouts provide no benefit in terms of reservation.

We can however add the turnouts if we also add normal/reverse direction. The info is not just the turnout but its desired state. That would allow the script to automatically set them for the required route, and when would depend on the route strategy, e.g. an ABS window strategy could just align them 2 blocks ahead and keep them reserved for 2 extra blocks past the train.

In the simplest case the automation should look like:

  • An ordered list of blocks and turnouts (with their thrown state).
  • A default speed for the blocks, plus per-block speed override.
  • A startup sequence, using timers and/or blocks.
  • An end-route stop sequence, equally using timers and/or blocks.
  • En-route pauses (e.g. for shuttle pause).
  • Special events (horn, etc.) based on timers and/or blocks.

If we are using automated reservation, we may want to customize the start/pause of the train. Ideally it should just be whatever the DCC momentum does.

There are two ways to organize the route’s sequence:

  • Define an array of blocks / turnouts.
  • Separately define events which, like in Conductor v1, have conditions on the block occupancy and engine directions.

Or:

  • Define an array of blocks.
  • For each block, add an events closure.
  • Keep track of the position of the train in the block sequence (aka “the current block) and only execute events for that block.

Or:

  • All both styles. Each block can have its events closure, and there’s a “generic” events closure that can get executed at each iteration (similar to the “default” closure indicated above).

In summary we have script structure that looks like this:

Startup {

  On (condition) then { actions }

  On (condition) then { actions }

}

 

Route 1 {

  Mode ABS # or Global-Lock

  Blocks [

    B311 eval {

      On (condition) then { actions }

    },

    T311 Normal,

    B321 eval {

      After 5.seconds then { horn grade-crossing }

    },

    T330 Reverse,

    B330 speed(cross-over),

    B340,

    B350 virtual,

    B360,

    B370 eval {

      On (condition) then { actions }

    },

    B360,

    B350 virtual,

    B340,

    B330 eval { … } speed(cross-over),

    T330 Reverse,

    B321 eval { … },

    T311 Normal,

    B311 eval { … },

  ]

  Eval {

    if () { }; switch(state) { case … }

    On (condition) then { actions }

  }

}

 

Loop {

  On (condition) then { actions }

}

 

Shutdown {

  On (condition) then { actions }

}

The syntax doesn’t quite work with groovy so it will need some work.

The eval blocks probably need a specific syntax to create timers “on the fly” -- this could be implemented by having the current block being tracked with the time spent in the block.

This shows a potential example of “virtual” blocks. These would be declared as such to know they are not associated with any sensor. In this case the idea is that going up, B340 is activated. When the block is seen going off, the train is assumed to be in B350 so it is “virtually occupied”. Once B360 is activated, the virtual block is turned off and it is not an error. There are few issues with this as there is no guarantee that it is the expected train that shows up in B360. The other issue is how to handle flaky sensors, of which B340 is a good example.

Next we need to detail how error conditions are handled, for example when a block suddenly turns occupied and is not predicted to be the next one. Are we stopping everything, are there cases where we need to be lenient, etc.


 Generated on 2024-04-19 by Rig4j 0.1-Exp-e7e2d73