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.

2023-05-10 - Conductor 2: Crossing Block Boundaries

Category Rtac

Now I understand the issue that was affecting the automated freight train; it's quite interesting and worth elaborating.

First, as a recap, the freight route is a shuttle composed of 2 blocks. The train starts on say block A, travels to the consecutive block B, stops, and reverses to block A. The sequence is very simple:

Step / State Block A                  State Block B                Description
1-
[ A  active   occupied  ]   [ B inactive   empty    ]        Start from A
2- [ A inactive  trailing  ]   [ B  active   occupied  ]        Crosses from A to B
2- [ A inactive  trailing  ]   [ B  active   occupied  ]        Stops & Reverses
3- [ A  active   occupied  ]   [ B inactive  trailing  ]        Crosses from B to A
4- [ A  active   occupied  ]   [ B inactive   empty    ]        Stops back on A and resets

Note how although we have 5 logical steps, there are only 4 different steps from the sensor/block occupancy point of view -- there is no difference whether the engine is running on block B or stopped and waiting till it reverses.

As the train moves forward from A towards B, at some point it crosses the boundary: block A’s sensor changes from “active” to “inactive”, and the logical occupancy status changes from “occupied” to “trailing” -- a trailing block is the last one the train occupied before the currently occupied block. Since this route has 2 blocks, they alternate between trailing & occupied because as the train is running the other block is always the “last block used”.

Now we need to look more closely at the train crossing the boundary between blocks A to B. We need to remember that these types of block detectors sense current. In this case they sense the engine and that's it. The following cars are not detected.

First the engine bridges blocks A and B, making them briefly look both occupied -- the front and rear wheels are electrically connected so they temporarily bridge rails from both blocks A and B, thus triggering both sensors as the train crosses the boundary.

Once the engine is fully on block B, block A should be detected as empty -- it changes state from occupied to trailing.

But as each car crosses the trail junction, the metal wheels briefly touch both rails and thus also briefly connect blocks A and B, making block A "blink" on and off quickly on the sensor.

Now that scenario is already handled, that’s exactly why the “last occupied” block is marked as trailing: we know that the sensor can go on/off quickly on a trailing block, and we can ignore that in the Conductor logic.

Now let’s add “flaky sensors” to this mix. These sensors detect current and have a limited sensitivity range. If an engine does not use enough current, the sensor may not detect the train at all, or it could be borderline. In a borderline case, the sensor detects the running train on and off as we oscillate around the sensor’s detection threshold.

So again, Conductor 2 already handles that: if a block is marked trailing or occupied and its sensor “blinks”, we simply ignore it. That works great for the common case of a train traversing a series of blocks, say X → Y → Z. If we know the train occupied block Y and the sensor “blinks”, it’s fine: we can ignore it because we know the train must still be on block Y so it’s still the occupied block -- until the sensor trigger for block Z, in which case Z becomes the occupied block, and Y becomes the trailing one.

In my case, the sensor for block B has poor sensitivity and is flaky. And even though this is well handled in the case of a train going through, it fails very subtly for the reversing shuttle case. Let’s reexamine the steps from before, but let’s add a “flaky” sensor B that can become inactive even when the train is running:

Step / State Block A                  State Block B                Description
1-
[ A  active   occupied  ]   [ B inactive   empty    ]        Start from A
2- [ A inactive  trailing  ]   [ B  active   occupied  ]        Crosses from A to B
Sensor B is flaky with this engine, and sometimes fails to detect the engine (making block B seem inactive). At the same time, the train is crossing the boundary and the wheels can short block A and B, making A seem active. This looks like this from the view of the Conductor logic:
?-
[ A  active      ??     ]   [ B inactive     ??     ]        What is this step?
⇒ This doesn’t look like Step 2 anymore, it looks like step 3!

2- [ A inactive  trailing  ]   [ B  active   occupied  ]        Stops & Reverses
3- [ A  active   occupied  ]   [ B inactive  trailing  ]        Crosses from B to A
3- [ A inactive  occupied  ]   [ B inactive  trailing  ]        Stops back on A
4- [ A inactive  occupied  ]   [ B inactive   empty    ]        Stops back on A and resets

As indicated above, once a block becomes occupied, we stop ignoring its sensor -- it’s fine if it “blinks” and oscillates between active and inactive states. Thus the two steps 3 above are perfectly “valid” and the same state from the automation’s logic point of view.

The consequence is that as soon as the sensor B flakes while the train is barely reaching block B, the automation thinks the train is… back to block A (with a flaky sensor that is permanently inactive) and has finished its shuttle route! It then triggers the corresponding timers to perform whatever operation is needed to stop the train and end the route.

Then when the automation tries to restart, it enters error mode because it realizes the train is detected neither on block A nor B. It’s as if it had vanished, and thus this situation cannot be recovered from.

So how can we fix this?

At first glance, an obvious issue is that the Conductor engine can instantly change from one state in the route sequence to the next one. But the physical train cannot be moving that fast. Thus a naive approach could be to add a timer: we know that a train crossing the block boundary must take at least N seconds to do so, so let’s come up with the time it takes to cross and ignore any sensor changes during that period. Unfortunately, that time is impossible to guess. It depends on the train’s length and its speed. However, we don’t have to guess -- we could allow (or force) the script writer to set that value in the script.

Although it’s less than perfect, this idea has some merit, so let’s add support for it:

  • We’ll add support for an “enter block” time, with some kind of default (say 10 seconds).
  • Let the script override that at the route level, and at the node level (per block).
  • The “enter block time” is then used when a new block becomes occupied, and during that time we ignore changes to both the newly occupied and the newly trailing block sensors.

The other issue is that the whole point of Conductor 2 is to base the shuttle route management on the automatic trigger of blocks. And as pointed above, the current block state management is “instant”: as soon as the next block in the sequence is activated, we consider it must mean the train has reached it. If we go back to the example of a linear route such as X → Y → Z, as soon as Y becomes active after X, we assume the train is on Y. And as soon as Z becomes active after Y, we assume the train is on Z, even if that happened a few milliseconds after!

Presented that way, one could immediately see that such a quick activation must be an error case. We currently have a timeout of the maximum time a train must spend on a block, but we don’t have a notion of a minimum time that the train must be on that block.

And that’s where that suggestion of an ”enter block” time start to makes sense: it can mean the minimum of time we expect to be on that block, and during that time any spurious activation of the next block must be an error for a linear route.

A sequence route currently has a “timeout” field in its script definition, and that’s the max time it can take to cross the block, so we’ll want to rename that field to avoid confusion. Instead we’ll have two fields:

  • maxSecondsOnBlock: The maximum time spent moving on the block.
  • minSecondsOnBlock: The minimum time once entered before we can reach the next block. It must cover at least the time needed to initially fully enter the block.

That will help clarify what the “timeout” means. We’ll also want to have a value for the route, and a possible override for each node. Our A/B shuttle could be rewritten this way:

val Shuttle_Route = MyRoute.sequence {

    name = "Shuttle"

    throttle = MyThrottle

    minSecondsOnBlock = 10        // default if not specified in a node

    maxSecondsOnBlock = 120        // 2 minutes per block max by default

    val A_fwd = node(A) {

        onEnter { … }

    }

    val B_fwd = node(B) {

        minSecondsOnBlock = 60  // custom min time for this block/node

        onEnter { … }

    }

    val A_rev = node(A) {

        minSecondsOnBlock = 30

        onEnter {

            after (40.seconds) then {

                BL.stop()

            } and_after (100.seconds) then {        // see below

                BL.sound(false)

            }

        }

    }

    sequence = listOf(A_fwd, B_fwd, A_rev)

}

It may also seem helpful to assert that any “after” rule used has a time that is less than the max time allocated on a block. We’re not currently doing that, for a good reason: in the example above, we have 2 timers when reaching back to block A which time (40+100 seconds) is greater than the maxSecondsOnBlock (120 seconds in that example). But the problem is that the max time is with the train moving -- we reset the timer when the train stops. That allows a train to stop at a station and have that time not count against the max time spent on the block. Thus that example above is perfectly valid -- the train should have spent 40 seconds moving on that block before stopping, well under 2 minutes.


 Generated on 2024-08-06 by Rig4j 0.1-Exp-e7e2d73