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-04-09 - Conductor 2: Virtual Blocks Implementation
Category Rtac
Let’s implement virtual blocks. These have been part of the original design and I now have a need for them.
As a reminder, what is a virtual block and what is it good for?
Let’s look at the Branchline. It’s is a simple straight chain of blocks as shown in yellow on this map:
Because the track is curved and goes through tunnels, it looks complicated, yet it’s really a straight line:
- [ B801 parking ] ⇒ [ B820 station ] ⇒ [ B830 canyon ] ⇒ [ B850 tunnel ] ⇒ [ B860 reversing block ]
One thing not obvious on this map is that B830 has no track sensor associated with it. When I did the original sensor wiring back in 2017, I simply couldn’t identify the corresponding track feeders, and I suspect (or hope) there are actually two blocks in there because it’s a fairly long segment. That’s why I left a B840 in my naming scheme, for potential later usage.
But now the concept behind the Conductor 2 automation is for the engine to track on which block the train is supposed to be, and match that with the reality of the track sensors. That’s a problem in this case since after moving out of block B820 we have no idea… the track appears empty, and that’s a definite error condition. How do we solve that in the script?
In this case, B830 is going to be a “virtual block”: we can add it to our block graph. Since the track is purely linear, we know that an engine coming out of B820 must be on B830. As soon as we see an activation on B850, we know the engine has reached the other side of that virtual block and all is fine. The only thing we can do is use a timeout -- if the engine hasn’t reached the other side in N seconds, then we know there’s a problem.
In the DSL, we currently have:
val B310 = block("NS768") named "B310"
… and everything in the DSL is an “IBlock”.
The current IBlock requires a “systemName” property, so it’s very JMRI-centric. Which is not a real problem since Conductor is naturally JMRI-centric.
However a lot of places, especially the junction with the simulator, expect a block to have a unique system name. This is how the main engine exchanges block data with the simulator whilst trying hard to avoid “leaking” objects. System names are unique, and are consequently an adequate way to uniquely identify a block.
Thus a natural way to implement virtual blocks would be to use IBlock as-is and define a system name. It should be unique and not used by any other block.
It’s worth remembering that a block has 2 different names:
- A block has a “default name”, which is its “JMRI system name”.
- block.systemName or block.defaultName returns that.
- A block is also a “named variable”, which has a more user-friendly name.
- block.name returns the “named” name, and if not set returns the “default name”.
So the real question is whether the virtual block’s “system name” is going to be synthetic (e.g. auto-generated, for example “virtual01”, “virtual02”, etc.) or selected by the user:
val B350 = virtualBlock() named "B350"
val B350 = virtualBlock("vb350") named "B350"
In the current script, once we have blocks defined, we always use the variables, not the string names. The variable names are used only for display. The system names are used for keying and exporting.
Deliberation:
- We support only this syntax:
val B350 = virtualBlock("vb350") named "B350"
- This allows us to create maps with deterministic system names. I can also define virtual blocks that this way become later regular blocks.
- We will not merge the variable name into the system name.
The initial design called for the script to programmatically activate and deactivate the virtual block. E.g. literally a script would have looked like this:
- B820 has become inactive → train must have moved on → set virtual B830 to active.
- B850 has become active → train must have moved on → set virtual B830 to inactive.
I tried that and it quickly proved to be problematic: There is a bit of a conundrum on when and how a virtual block would be activated. The problem is that this is going to be different behavior when under simulation or real JMRI.
- Simulation: the route manager simulator automatically triggers the next block after a timeout to simulate engine movement.
- That naturally activates the virtual block. After another timeout simulating engine movement in the virtual block, the next real block gets activated automatically by the simulator.
- In the script, that block deactivates the virtual block.
- Real JMRI: there’s currently nothing to activate the virtual block.
- The script needs to check, while the block is occupied, that the block sensor is indeed active. When it becomes inactive, we can expect the train to have moved and programmatically activate the virtual block.
- But this basically has the script do more or less what the route sequence manager could be doing. Thus the route sequence manager should be the one activating the virtual block when the occupied block becomes inactive.
There are two obvious red flags here. Once again the simulation is drastically different from the real behavior. We cannot define a mechanism based on “it works properly under simulation but not with the real sensor”.
The other red flag was that the virtual block design relied on the script driving the activation of the block. Instead we should strive for the route sequence manager to take care of it, and do it the same way under simulation or not:
- Route manager detecting the current block is no longer activated:
- Right now it expects one of the outgoing blocks to become active.
- Change: If no outgoing blocks are active yet there’s only one and it’s a virtual block, activate that.
- Potential issue: More than one virtual block ⇒ should not be possible. Cannot choose. It’s an error. Prevented by enforcing there’s only one outgoing block.
- Potential issue: Delay in next block activating. Now two blocks are active (the virtual block + the real block). It’s an error. Prevented by enforcing there’s only one outgoing block.
- Route manager detecting the next (outgoing) block is activated:
- When marking the current block as trailing, if it’s a virtual block then also deactivate the underlying sensor.
- Potential issue: none foreseen.
So that’s the way to go. Let the route manager do what it is supposed to: manage the route. The script shouldn’t have to do what will only turn into boilerplate code.