Signal Modules
The basic signal model in Rail3D is very good at what it does - it keeps the train apart properly - however, it is not sophisticated enough to cope with some of the more complicated routing examples. For example:
- Single line passing loops you can easily create a single line passing loop and have trains passing properly, but without some extra effort you will soon or later get the layout “locked up” with a train in each side of the loop and one on each of the approaches, and hence nothing able to move.
- Bidirectional working it’s not possible to reliably model bidirectional working with the basic signals without sooner or later getting into a lockup situation.
- Single line intermediate signals similar to bidirectional working, sooner or later it will lock up.
- Selective routing you may want to route trains into a yard, or choose a platform depending on other factors - but this cannot be achieved with the basic signal system.
- Routing by priority you may want to give express trains priority over frieght - but this cannot be achieved easily with the basic signal system.
All of these problems can be resolved (to some extent) with scripts, but generally the solutions are complex, and as they need scripts distributed over many signals, it can be difficult to achieve. The new “Signal module” feature makes things a lot simpler and easier to understand.
Signal Modules Concept
The concept is simple: to group a number of signals together and control them from a single script.
Example
For my example I’m going to build a control module for a single line passing loop:
I’m using Swiss metre guage models (because I’ve been working on myh bvz layout recently) and the signals are type “ch_Ltype3″.
In each direction I have a distant (type ch_Ltype2D), home and starter. Note that signals of this type behave like semaphores, and that the home signal are not section signals.
If you build this layout and run it for long enough (with enough trains) you will find a number of problems with the operation:
- You can get the lock-up situation described above where there are two trains sitting in the loop, and two more sitting on the home signals.
- Using signals with semaphore type operation, you will find that when (eg) an up train approaches from the left, all the up signals will clear (including the starter) if possible. Ideally it would be nice if the signal logic was a bit more intelligent and would not lock the next section to early if there is a down train wanting to come through.
- In Swiss operation, both sides of the loop are bi-directional and you would expect the first train to arrive to enter the loop when passing, and the second to run through the straight line. This can be solved with scripts on each signal, but it is complicated to set up, see Passing Loops On Single Lines
Implementation
Firstly, we need to define the signals under control. There are all sorts of ways one could do this, but I’ve decided to use the signal panel as the way to do it. This has the advantage(s) that we already have the signal panel defined as a means of grouping related signals, and it gives me a degree of abstraction in the script I write so that a generic script can be re-used with minimal customisation: the customisation for each specific application is defined by the parent signal panel, and finally it gives us a nice display where we can see what is going on during test and development.
So, the first step is to create a signal panel covering the cluster of signals, you can see my simple panel in the above screenshot. Later, I’m going to need to identify the signals, so I’ve given them signal ids: “ud”, “uh”, “us” for Up Distant, Up Home, Up starter and similarly “dd”,”dh”,”ds”.
In the latest code (later than build 101.8.1) I’ve added some new script functions and the ability to define a script on the panel: On the panel dialog, select “Edit Script” from the “Settings” menu.
A panel script has some extra features:
- Panel scripts receive input from all the signals on the panel
- Panel scripts can modify the action of the signals on the panel.
This is achieved through some new script entry points:
OnSignalOnTrain() : This function is called whenever a train passes one of the signals referenced by the panel.
OnSignalCanClear() : This function is called when a signal on the panel is about to clear - the script may return a flag to cancel the signal clearing (see note below)
OnSignalCanRouteTo() : This function is called when the signalling system is about to set a route to a signal reference by the panel- the script may return a flag to cancel the route setting.
I’ve also added a new function that returns the distance a train is from a signal: Signal.GetDistance(Train);
Note: the OnSignalCanClear() function doesn’t appear to be called for signals that are wanting to set a route to a signal not on the panel.
Testing this out
Some simple script fragments to test these new functions:
OnSignalOnTrain()
{
debug.print(Train.GetRoute());
debug.print(" passing ");
debug.printL(Signal.GetID());
}
This simple prints out a debug statement whenever a train passes one of the signals on the panel. You can also see how it is possible to access the details of the train and the signal.
OnSignalCanClear()
{
return FALSE;
}
This code simply stops any of the panel signals from clearing (returning true) would allow them to clear as normal.
OnSignalCanRouteTo()
{
return FALSE;
}
This code simply stops any routes being set to the panel signals. You can think of this as if a train is being offered from a neighbouring box and refused.
Step One: Stopping the forward routing from happening too soon.
In order to maximise the efficient use of the route, I don’t want trains to lock the route ahead too soon, so I’m going to add a simple section of code
OnSignalCanClear()
{
sigid=Signal.GetID();
float x=Signal.GetDistance(Train);
if(sigid=="UH")
{
if(x>UpAppDist)
return FALSE;
}
if(sigid=="DH")
{
if(x>DnAppDist)
return FALSE;
}
return TRUE;
}
Actually I’m controlling the home signal (because later I’m going to want to for the bi-directional loop problem). When something causes the home signal to clear (ie an approaching train), I don’t want the signal to clear too soon, because this will in turn lock the section ahead. Equally, I would like the signal to be clear before the train gets to the distant signal, so that the train is not checked unduly. So, I use the new Signal.GetDistance function to get the distance the train is from the signal (in metres) and compare this with the approach distance I’ve defined in my script’s init function:
float UpAppDist=650;
float DnAppDist=1050;
If the train is too far away, the signal is prevented from clearing, otherwise it is allowed to clear as normal.
Obviously, this is a fairly simple (though effective) way to achieve what I want, a more refined solution would involve looking at the class of the train, how far away any other trains are and giving priority as appropriate.
Step Two: Preventing the lock up
This is the more interesting problem, but put simply: if we have three trains in the panel area we are alright, four trains means a lock up.
So to solve this: a) we need to keep track of the number of trains in the panel area, and b) we need to prevent the fourth train from entering the panel area.
To achieve this I define a variable: TrainCount to hold the number of trains in the area. Next I write code to determine whether a train can be allowed to approach the loop:
OnSignalCanRouteTo()
{
sigid=Signal.GetID();
if((sigid=="UD") || (sigid=="DD")
{
// Maximum number of trains can cope with is three
if(TrainCount>2)
return FALSE;
TrainCount++;
}
return TRUE;
}
This function is called when the routing logic tries to set a route to any of our signals, and in this case I’m concerned with routes being set up to the distant signals (ud and dd).
So with no trains in the loop area, TrainCount is zero and when a route is set to the distant signal, the function increments the train count and returns true (which allows the route to be set). The same is true for the second train and third trains to approach, but when a fourth train is offered, TrainCount is 3, so the test if(TrainCount>2) evaluates true and the function returns false which prevents the route being set.
This effectively prevents the lockup situation, but it is not quite complete, because we need to decrement our train counter when the trains leave the area - to do this I’m monitoring trains passing the starting signals:
OnSignalOnTrain()
{
sigid=Signal.GetID();
if(TrainCount>0)
{
if(sigid=="US")
TrainCount--;
if(sigid=="DS")
TrainCount--;
}
}
with a check as well to prevent the count going negative.
Making it Generic
A few little changes are needed to make the script generic.
- If
sigid=Signal.GetID(); is changed to sigid=Right(Signal.GetID(),2); then the signal ids can be changed to (eg) London_DS and Glasgow_DS and the script will still work.
- The main functions
OnSignalOnTrain() etc can be saved in a script file, then each time the script is to be used, the panel just needs the init() function and an include for the main script:
Init()
{
persistent int TrainCount;
float UpAppDist=650;
float DnAppDist=1050;
}
INCLUDE PassingLoop.cpp
Note that this defines the approach distances which are the only bits that need to be customised for the specific location.
sig=GetSig(“*ds”); // and for the down signals.
lnk=sig.GetRearLink();
if(lnk.GetLocked())
iTrains++;
if(lnk.GetOccupied())
iTrains++;
sig=GetSig(“*dh”);
lnk=sig.GetRearLink();
if(lnk.GetLocked())
iTrains++;
if(lnk.GetOccupied())
iTrains++;
Now, this code is not perfect - for example it can count trains twice (if the same train has locked both home and starter for example), but it’s better to err on the side of caution.
So now I can use this code to determine if it is safe to allow another train to approach the loop, and my CanRouteTo function looks like this:
OnSignalCanRouteTo()
{
//GetLoopStatus();
sigid=Right(Signal.GetID(),2);
if(sigid=="DS")
return TRUE;
if(sigid=="US")
return TRUE;
if(iTrains>2)
return FALSE;
GetLoopStatus();
return TRUE;
}
in other words, I always allow routes to the starters (from the homes) to set, but I only allow routes to the distants/homes to be set if the count determined by the get status code above is 2 or less.
A couple of gotchas to note:
- Calling
GetLoopStatus at the start of this code is wrong - hence it’s commented out. The reason is that this function is called at the end of the signal’s route setting process, and during the route setting process the signal temporarily locks the sections as part of it’s working. So, if you try to use the GetLocked functions at this time, you may get a false reading due to the temparary locks created as part of the signal working out its route. To get round this I actually put the code to determine the loop status in the panels OnTimer function so it gets called every second.
- The second call to
GetLoopStatus in the above allows for the fact that if the function returns true, the route will be set, and therefore the value of iTrains ought to change. During testing I had instances where an up route would be set, and then before the time function had updated iTrains a down route set as well and I ended up with too many trains. Calling GetLoopStatus at this point resolves this.
As I type this, I’m wondering if the timer call is needed - it might be enough to call GetLoopStatus at the end of CanRouteTo and in OnSignal but then we’d back where we started with code that could get out of sync, so that’s not a good idea.
The current code
Well, the code so far. This works fairly well, it could be refined in several ways, but that’s probably enough for now.
Init()
{
signal SignalTo;
string sigid;
panel Panel;
signal sig;
int iTrains;
link lnk;
float UpAppDist=150;
float DnAppDist=150;
}
////////////////////////////////////////////////////////////////////////////
GetLoopStatus()
{
iTrains=0;
sig=Panel.GetSig("*US");
lnk=sig.GetRearLink();
if(lnk.GetLocked())
iTrains++;
if(lnk.GetOccupied())
iTrains++;
sig=Panel.GetSig("*UH");
lnk=sig.GetRearLink();
if(lnk.GetLocked())
iTrains++;
if(lnk.GetOccupied())
iTrains++;
sig=Panel.GetSig("*DS");
lnk=sig.GetRearLink();
if(lnk.GetLocked())
iTrains++;
if(lnk.GetOccupied())
iTrains++;
sig=Panel.GetSig("*DH");
lnk=sig.GetRearLink();
if(lnk.GetLocked())
iTrains++;
if(lnk.GetOccupied())
iTrains++;
}
///////////////////////////////////////////////////////////////////////////
OnTimer()
{
GetLoopStatus();
}
OnSignalCanRouteTo()
{
//GetLoopStatus();
sigid=Right(Signal.GetID(),2);
if(sigid=="DS")
return TRUE;
if(sigid=="US")
return TRUE;
if(iTrains>2)
return FALSE;
GetLoopStatus();
return TRUE;
}
///////////////////////////////////////////////////////////
OnSignalCanClear()
{
sigid=Right(Signal.GetID(),2);
float x=Signal.GetDistance(Train);
if(sigid=="UH")
{
if(x>UpAppDist)
return FALSE;
}
if(sigid=="DH")
{
if(x>DnAppDist)
return FALSE;
}
return TRUE;
}
Next steps.
So, further developments:
- Prioritisation: it ought to be possible to tweak the code so that it prioritises the trains in order to use the line more efficiently. For example, if there are two trains waiting to pass the same single line section, the panel with the higher train count might send its train first. Alternatly, express trains might be given priority over freights etc.
- Bidirectional routing: European practice is to use the passing loops bi-directionaly - the first train uses the loop when passing, the second train - or a train not passing - uses the through line. See also Passing Loops On Single Lines, where we’ve tried to achieve this before, but I think it can be done much more simply with panel scripts. To do this properly, we might need a new function something lile Test Could ClearTest Could Clear(train) which would test if the signal is able to clear.
The code so far.
You can download the passing loop script so far from http://www.rail3d.net/preview/passingloop.cpp
You need to create a signal panel for each passing loop, and add a script to each panel, like this:
Init()
{
persistent int TrainCount;
signal SignalTo;
string sigid;
panel Panel;
!!Don't panic
If all that looks a bit complicated - don't worry. The script will be available soon, then all you need to do is create the panel and a panel script like this:
Init()
{
signal SignalTo;
string sigid;
panel Panel;
signal sig;
int iTrains;
link lnk;
float UpAppDist=150;
float DnAppDist=150;
}
#include "PassingLoop.cpp"
Close - but no cigar
Well that works for a while, but if you leave it running for long enough, you can still get a deadlock situation. This happens when you get three trains at each of two adjacent loops (allowable under the above code) like this:
So what we need is some way for the panel script to be able to communicate with the adjacent panel and not accept a train if it will result in a dialog situation like the above.
So, this is a new addition to the scripting system - the ability to call a function in another script. At the moment it only applies to panels.
First we define a simple function that returns the panels current train count:
GetLoopCount()
{
return iTrains;
}
Now, we can call this function from one panel and get the train count at the next panel:
panel pUp=Document.GetPanel(sPanelUp);
if(pUp)
iNextUp=pUp.Call("GetLoopCount()");
else
iNextUp=0;
ie, we first try to locate the adjacent panel (string sPanelUp is set in the panel’s init function). Assuming the panel exists, we call that panel’s "GetLoopCount" function to retrieve the train count from that loop.
nb, I apprecate the above code is a bit clunky, you might expect to write pUp.GetLoopCount() but that would need a fairly major rewrite of the scripting system to achieve that. I did consider pUp->GetLoopCount() before I settled on the Call() approach. And as yet I haven’t worked out how to pass parameters, it might be something like pUp.Call("GetLoopCount()","Train=this;Signal=sSig");
So, now we have a mechanism for retrieving the state of adjacent panels, so we cac rewrite the acceptance code like this:
OnSignalCanRouteTo()
{
sigid=Right(Signal.GetID(),2);
if(sigid=="DS")
return TRUE;
if(sigid=="US")
return TRUE;
if(iTrains>2) // Too busy
{
return FALSE;
}
if(iTrains==2) // If we're at two, then check next panel
{
if((sigid=="UH") || (sigid=="UD"))
{
if(iNextUp>=2)
{
return FALSE;
}
}
if((sigid=="DH") || (sigid=="DD"))
{
if(iNextDn>=2)
{
return FALSE;
}
}
}
GetLoopStatus();
return TRUE;
}
So,
- If the count is greater than 2, then reject the train.
- If the count is 2, what is the state of the next panel, if it is also 2, reject the train.
This seems to work, and so far has run more or less indefinitly without deadlock.
string sPanelDn=“Lower Loop”;
float UpAppDist=1150;
float DnAppDist=1150;
}
- include “PassingLoop.cpp”
Note the lines that define the next panels up and down.
Next steps.
MRG 10/12/2013 14:26:45
|