Note that in the previous example there is no way to get back to the default state. That may be a good thing!
Also (and
this is not to consider in detail right now) imagine you had two of these sets of lights and buttons.
You might think that you would have double the number of states
Code:
State | Designated name
======+===========================
0 | Default#1 (DEF1)
1 | Down Train Awaiting Dispatch#1 (DTAD1)
2 | Down Train Accepted#1 (DTA1)
3 | Ready Next Service#1 (RNS1)
4 | Default#2 (DEF2)
5 | Down Train Awaiting Dispatch#2 (DTAD2)
6 | Down Train Accepted#2 (DTA2)
7 | Ready Next Service#2 (RNS2)
And that you just progress from there.
However it's not that simple. You might want one to be in DTAD (DTAD1) and the next in DTA (DTA2). There is no single state that allows us to do that.
This is a major complication, and there are three ways to resolve it.
First solution
The first way is to set up all the combination states. that means there will be 16 states (not 4 + 4, but 4 * 4)
The States will be something like
Code:
State | Designated name
======+===========================
0 | Default#1, Default#2 (DEF1/DEF2)
1 | Default#1, Down Train Awaiting Dispatch#2 (DEF1/DTAD2)
2 | Default#1, Down Train Accepted#2 (DEF1/DTA2)
3 | Default#1, Ready Next Service#2 (DEF1/RNS2)
4 | Down Train Awaiting Dispatch#1, Default2 (DTAD1/DEF2)
5 | Down Train Awaiting Dispatch#1, Down Train Awaiting Dispatch#2 (DTAD1/DTAD2)
6 | Down Train Awaiting Dispatch#1, Down Train Accepted#2 (DTAD1/DTA2)
7 | Down Train Awaiting Dispatch#1, Ready Next Service#2 (DTAD1/RNS2)
8 | Down Train Accepted#1, Default2 (DTA1/DEF2)
9 | Down Train Accepted#1, Down Train Awaiting Dispatch#2 (DTA1/DTAD2)
10 | Down Train Accepted#1, Down Train Accepted#2 (DTA1/DTA2)
11 | Down Train Accepted#1, Ready Next Service#2 (DTA1/RNS2)
12 | Ready Next Service#1, Default2 (RNS1/DEF2)
13 | Ready Next Service#1, Down Train Awaiting Dispatch#2 (RNS1/DTAD2)
14 | Ready Next Service#1, Down Train Accepted#2 (RNS1/DTA2)
15 | Ready Next Service#1, Ready Next Service#2 (RNS1/RNS2)
Knowing what these mean, you might realise that some of these states are actually invalid. For the moment, we'll retain them, but understand that an invalid state is one in which there are no valid ways to get there.
Then you write the other table showing for each of these 16 states, what things move you to the other states.
The key thing to note is that if a state is invalid (let's say state 10 is invalid) then there will be no action which results in state 10 being the next state. Normally, for example ButtonB2 might signal a move from RNS to DTA -- so we would have perhaps Button B2-1 to change all of the states containing RNS1 (e.g. RNS1/RNS2) to the corresponding state with DTA1 (in this case DTA1/RNS2). BUT we would NOT encode the rule for RNS1/DTA2, because the "next state" would be the invalid DTA1/DTA2 state.
This can get very complex quickly. For example, having 4 of these sets of switches and buttons gives us a potential 256 states. In practice we will have fewer, because if a state is actually invalid, we can remove it completely. This will help ensure we never change to it. But we still have to consider all 256 states to decide which to encode and which not to.
However, there is an up-side. It is relatively certain that once you have decided on all of the states and transitions that everything will work. If you have 10 of these, it will take you quite some time to get all of the million states defined though.
Second Solution
The second solution is almost too simple. All we do is duplicate the state machine.
So we have one state machine for each of these things.
int iState1; // for state machine 1
int iState2; // for state machine 2
And now we simply have one set of states
Code:
State | Designated name
======+===========================
0 | Default#1 (DEF)
1 | Down Train Awaiting Dispatch#1 (DTAD)
2 | Down Train Accepted#1 (DTA)
3 | Ready Next Service#1 (RNS)
And one set of rules
Code:
This | Next |
State | State | Action
======+=======+====================
DEF | DTAD | BtnA1Edge == true
------+-------+--------------------
DTAD | DTA | BtnB2Edge == true
| RNS | BtnB2Edge == true
------+-------+--------------------
DTA | DTAD | BtnA1Edge == true
| RNS | BtnB2Edge == true
------+-------+--------------------
RNS | DTA | BtnB2Edge == true
| DTAD | BtnA1Edge == true
(Well, not exactly 1 set of rules, because we have 2 sets of buttons, but the rules are exactly the same for each state machine, we just refer to the correct buttons.)
And then we have two machineState() functions, and two changeState() functions, each of which deals with it's own iSate variable.
Effectively we have 2 state machines. and we would code things like this:
Code:
void loop() {
getInputs(); // gets all inputs
changeState1(); // handle the first state machine
changeState2(); // handle the second state machine
writeOutputs(); // write all the outputs
}
This handles both totally independently. If there are no illegal states, then 10 state machines will have a total of 40 states (better than a million)
But remember in the previous example that there were invalid states? An invalid state means there is some interaction between the state machines. And with a train setup, I expect that there will be many of these.
So the upside of this is that the number of states is reduced to a minimum. In fact, it is the number we might naively have thought we needed. But the downside is that it can't handle invalid combinations of states, so we'll crash a lot of trains. (And that's bad, right?)
Third Solution
There's got to be a middle ground, right? Well yeah.
All we do is modify the second solution slightly so that the rules take into account the other state machines.
Code:
This | Next |
State | State | Action
======+=======+====================
DEF | DTAD | BtnA1Edge == true
------+-------+--------------------
DTAD | DTA | BtnB2Edge == true AND other state machine is not DTA
| RNS | BtnB2Edge == true
------+-------+--------------------
DTA | DTAD | BtnA1Edge == true
| RNS | BtnB2Edge == true
------+-------+--------------------
RNS | DTA | BtnB2Edge == true AND other state machine is not DTA
| DTAD | BtnA1Edge == true
And that's all there is to it. We just make sure that before we make a transition (in this case to DTA) that the other machine is not in the DTA state (because DTA1/DTA2 was invalid)
These rules will probably change from state machine to state machine, so the rules are likely to be different for each state machine, but those differences will be in which other state machine(s) we need to check.
Note that the states DTA for machine 1 and DTA for machine 2 still exist, and it is only the rules we have that prevents both from being set.
So the upside here is that we can have fewer states, AND not get into any invalid states. We need to be careful, but each of the tasks is relatively small.
Solution 4
Did I say 3?
Imagine that things get really big. You have 100 of these things, each with 34 buttons and 3 LEDs. That's a lot of buttons and LEDs. Whilst you can probably drive all that I/O, at some point your arduino is going to say "Too much code! I can't take any more!!!". What then?
Well, the answer then is to put some of your state machines into another arduino.
And how do they communicate?
1) They could have some communications method to send machine states.
2) you could monitor the condition of the LEDs driven by the other arduinos
3) Some state machines might be totally independent from the rest and not need communication.
Things get a little more complex because you can't ensure that the state machines are operated one at a time any more, so 2 state machines might change state simultaneously leading to an invalid state. More coding, or perhaps additional states may be needed to either prevent this or to deal with it in a sensible manner.