| 

.NET C# Java Javascript Exception

9
State machines are used regularly, especially in automation technology. The state pattern provides an object-oriented approach that offers important advantages especially for larger state machines. Most developers have already implemented state machines in IEC 61131-3: one consciously, the other one perhaps unconsciously. The following is a simple example of three different approaches: CASE statement State […]

State machines are used regularly, especially in automation technology. The state pattern provides an object-oriented approach that offers important advantages especially for larger state machines.

Most developers have already implemented state machines in IEC 61131-3: one consciously, the other one perhaps unconsciously. The following is a simple example of three different approaches:

CASE statement State transitions in methods The ‘state’ pattern

Our example describes a vending machine that dispenses a product after inserting a coin and pressing a button. The number of products is limited. If a coin is inserted and the button is pressed although the machine is empty, the coin is returned.

The vending machine shall be mapped by the function block FB_Machine. Inputs accept the events and the current state and the number of still available products are read out via outputs. The declaration of the FB defines the maximum number of products.

FUNCTION_BLOCK PUBLIC FB_Machine
VAR_INPUT
 bButton : BOOL;
 bInsertCoin : BOOL;
 bTakeProduct : BOOL;
 bTakeCoin : BOOL;
END_VAR
VAR_OUTPUT
 eState : E_States;
 nProducts : UINT;
END_VAR

UML state diagram

State machines can be very well represented as a UML state diagram.

A UML state diagram describes an automaton that is in exactly one state of a finite set of states at any given time.

The states in a UML state diagram are represented by rectangles with rounded corners (vertices) (in other diagram forms also often as a circle). States can execute activities, e.g. when entering the state (entry) or when leaving the state (exit). With entry / n = n – 1, the variable n is decremented when entering the state.

The arrows between the states symbolize possible state transitions. They are labeled with the events that lead to the respective state transition. A state transition occurs when the event occurs and an optional condition (guard) is fulfilled. Conditions are specified in square brackets. This allows decision trees to be implemented.

First variant: CASE statement

You will often find CASE statements for the conversion of state machines. The CASE statement queries every possible state. The conditions are queried for the individual states within the respective areas. If the condition is fulfilled, the action is executed and the state variable is adapted. To increase readability, the state variable is often mapped as ENUM.

TYPE E_States :
(
 eWaiting := 0,
 eHasCoin,
 eProductEjected,
 eCoinEjected
);
END_TYPE

Thus, the first variant of the state machine looks like this:

FUNCTION_BLOCK PUBLIC FB_Machine
VAR_INPUT
 bButton : BOOL;
 bInsertCoin : BOOL;
 bTakeProduct : BOOL;
 bTakeCoin : BOOL;
END_VAR
VAR_OUTPUT
 eState : E_States;
 nProducts : UINT;
END_VAR
VAR
 rtrigButton : R_TRIG;
 rtrigInsertCoin : R_TRIG;
 rtrigTakeProduct : R_TRIG;
 rtrigTakeCoin : R_TRIG;
END_VAR

rtrigButton(CLK := bButton);
rtrigInsertCoin(CLK := bInsertCoin);
rtrigTakeProduct(CLK := bTakeProduct);
rtrigTakeCoin(CLK := bTakeCoin);
 
CASE eState OF
 E_States.eWaiting:
 IF (rtrigButton.Q) THEN
 ; // keep in the state
 END_IF
 IF (rtrigInsertCoin.Q) THEN
 ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has insert a coin.', '');
 eState := E_States.eHasCoin;
 END_IF
 
 E_States.eHasCoin:
 IF (rtrigButton.Q) THEN
 IF (nProducts > 0) THEN
 nProducts := nProducts - 1;
 ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has pressed the button. Output product.', '');
 eState := E_States.eProductEjected;
 ELSE
 ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has pressed the button. No more products. Return coin.', '');
 eState := E_States.eCoinEjected;
 END_IF
 END_IF
 
 E_States.eProductEjected:
 IF (rtrigTakeProduct.Q) THEN
 ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has taken the product.', '');
 eState := E_States.eWaiting;
 END_IF
 
 E_States.eCoinEjected:
 IF (rtrigTakeCoin.Q) THEN
 ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has taken the coin.', '');
 eState := E_States.eWaiting;
 END_IF
 
 ELSE
 ADSLOGSTR(ADSLOG_MSGTYPE_ERROR, 'Invalid state', '');
 eState := E_States.eWaiting;
END_CASE

A quick test shows that the FB does what it is supposed to do:

However, it quickly becomes clear that larger applications cannot be implemented in this way. The clarity is completely lost after a few states.

Sample 1 (TwinCAT 3.1.4022) on GitHub

Second variant: State transitions in methods

The problem can be reduced if all state transitions are implemented as methods.

If a particular event occurs, the respective method is called.

FUNCTION_BLOCK PUBLIC FB_Machine
VAR_INPUT
 bButton : BOOL;
 bInsertCoin : BOOL;
 bTakeProduct : BOOL;
 bTakeCoin : BOOL;
END_VAR
VAR_OUTPUT
 eState : E_States;
 nProducts : UINT;
END_VAR
VAR
 rtrigButton : R_TRIG;
 rtrigInsertCoin : R_TRIG;
 rtrigTakeProduct : R_TRIG;
 rtrigTakeCoin : R_TRIG;
END_VAR

rtrigButton(CLK := bButton);
rtrigInsertCoin(CLK := bInsertCoin);
rtrigTakeProduct(CLK := bTakeProduct);
rtrigTakeCoin(CLK := bTakeCoin);
 
IF (rtrigButton.Q) THEN
 THIS^.PressButton();
END_IF
IF (rtrigInsertCoin.Q) THEN
 THIS^.InsertCoin();
END_IF
IF (rtrigTakeProduct.Q) THEN
 THIS^.CustomerTakesProduct();
END_IF
IF (rtrigTakeCoin.Q) THEN
 THIS^.CustomerTakesCoin();
END_IF

Depending on the current state, the desired state transition is executed in the methods and the state variable is adapted:

METHOD INTERNAL CustomerTakesCoin : BOOL
IF (THIS^.eState = E_States.eCoinEjected) THEN
 ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has taken the coin.', '');
 eState := E_States.eWaiting;
END_IF
 
METHOD INTERNAL CustomerTakesProduct : BOOL
IF (THIS^.eState = E_States.eProductEjected) THEN
 ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has taken the product.', '');
 eState := E_States.eWaiting;
END_IF
 
METHOD INTERNAL InsertCoin : BOOL
IF (THIS^.eState = E_States.eWaiting) THEN
 ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has insert a coin.', '');
 THIS^.eState := E_States.eHasCoin;
END_IF
 
METHOD INTERNAL PressButton : BOOL
IF (THIS^.eState = E_States.eHasCoin) THEN
 IF (THIS^.nProducts > 0) THEN
 THIS^.nProducts := THIS^.nProducts - 1;
 ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has pressed the button. Output product.', '');
 THIS^.eState := E_States.eProductEjected;
 ELSE 
 ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has pressed the button. No more products. Return coin.', '');
 THIS^.eState := E_States.eCoinEjected;
 END_IF
END_IF

This approach also works perfectly. However, the state machine remains in only one function block. Although the state transitions are shifted to methods, this is a solution approach of structured programming. This still ignores the possibilities of object orientation. This leads to the result that the source code is still difficult to extend and is illegible.

Sample 2 (TwinCAT 3.1.4022) on GitHub

Third variant: The state pattern

Some OO design principles are helpful for the implementation of the State Pattern:

Cohesion (= degree to which a class has a single concentrated purpose) and delegation

Encapsulate each responsibility into a separate object and delegate calls to these objects. One class, one responsibility!

Identify those aspects that change and separate them from those that remain constant

How are the objects split so that extensions to the state machine are necessary in as few places as possible? Previously, FB_Machine had to be adapted for each extension. This is a major disadvantage, especially for large state machines on which several developers are working.

Let’s look again at the methods CustomerTakesCoin(), CustomerTakesProduct(), InsertCoin() and PressButton(). They all have a similar structure. In IF statements, the current state is queried and the desired actions are executed. If necessary, the current state is also adjusted. However, this approach does not scale. Each time a new state is added, several methods have to be adjusted.

The state pattern scatters the status to several objects. Each possible status is represented by a FB. These status FBs contain the entire behavior for the respective state. Thus, a new status can be introduced without having to change the source code of the original blocks.

Every action (CustomerTakesCoin(), CustomerTakesProduct(), InsertCoin(), and PressButton()) can be executed on any state. Thus, all status FBs have the same interface. For this reason, one interface is introduced for all status FBs:

FB_Machine aggregates this interface (line 9), which delegates the method calls to the respective status FBs (lines 30, 34, 38 and 42).

FUNCTION_BLOCK PUBLIC FB_Machine
VAR_INPUT
 bButton : BOOL;
 bInsertCoin : BOOL;
 bTakeProduct : BOOL;
 bTakeCoin : BOOL;
END_VAR
VAR_OUTPUT
 ipState : I_State := fbWaitingState;
 nProducts : UINT;
END_VAR
VAR
 fbCoinEjectedState : FB_CoinEjectedState(THIS);
 fbHasCoinState : FB_HasCoinState(THIS);
 fbProductEjectedState : FB_ProductEjectedState(THIS);
 fbWaitingState : FB_WaitingState(THIS);
 
 rtrigButton : R_TRIG;
 rtrigInsertCoin : R_TRIG;
 rtrigTakeProduct : R_TRIG;
 rtrigTakeCoin : R_TRIG;
END_VAR
 
rtrigButton(CLK := bButton);
rtrigInsertCoin(CLK := bInsertCoin);
rtrigTakeProduct(CLK := bTakeProduct);
rtrigTakeCoin(CLK := bTakeCoin);
 
IF (rtrigButton.Q) THEN
 ipState.PressButton();
END_IF
 
IF (rtrigInsertCoin.Q) THEN
 ipState.InsertCoin();
END_IF
 
IF (rtrigTakeProduct.Q) THEN
 ipState.CustomerTakesProduct();
END_IF
 
IF (rtrigTakeCoin.Q) THEN
 ipState.CustomerTakesCoin();
END_IF

But how can the status be changed in the respective methods, the individual status FBs?

First of all, an instance within FB_Machine is declared by each status FB. Via FB_init(), a pointer to FB_Machine is transferred to each status FB (lines 13 – 16).

Each single instance can be read by property from FB_Machine. Each time an interface pointer to I_State is returned.

Furthermore, FB_Machine receives a method for setting the status,

METHOD INTERNAL SetState : BOOL
VAR_INPUT
 newState : I_State;
END_VAR
THIS^.ipState := newState;

and a method for changing the current number of products:

METHOD INTERNAL SetProducts : BOOL
VAR_INPUT
 newProducts : UINT;
END_VAR
THIS^.nProducts := newProducts;

FB_init() receives another input variable, so that the maximum number of products can be specified in the declaration.

Since the user of the state machine only needs FB_Machine and I_State, the four properties (CoinEjectedState, HasCoinState, ProductEjectedState and WaitingState), the two methods (SetState() and SetProducts()) and the four status FBs (FB_CoinEjectedState(), FB_HasCoinState(), FB_ProductEjectedState() and FB_WaitingState()) were declared as INTERNAL. If the FBs of the state machine are in a compiled library, they are not visible from the outside. These are also not present in the library repository. The same applies to elements that are declared as PRIVATE. FBs, interfaces, methods and properties that are only used within a library, can thus be hidden from the user of the library.

The test of the state machine is the same in all three variants:

PROGRAM MAIN
VAR
 fbMachine : FB_Machine(3);
 sState : STRING;
 bButton : BOOL;
 bInsertCoin : BOOL;
 bTakeProduct : BOOL;
 bTakeCoin : BOOL;
END_VAR
 
fbMachine(bButton := bButton,
 bInsertCoin := bInsertCoin,
 bTakeProduct := bTakeProduct,
 bTakeCoin := bTakeCoin);
sState := fbMachine.ipState.Description;
 
bButton := FALSE;
bInsertCoin := FALSE;
bTakeProduct := FALSE;
bTakeCoin := FALSE;

The statement in line 15 is intended to simplify testing, since a readable text is displayed for each state.

Sample 3 (TwinCAT 3.1.4022) on GitHub

This variant seems quite complex at first sight, since considerably more FBs are needed. But the distribution of responsibilities to single FBs makes this approach very flexible and much more robust for extensions.

This becomes clear when the individual status FBs become very extensive. For example, a state machine could control a complex process in which each status FB contains further subprocesses. A division into several FBs makes such a program maintainable in the first place, especially if several developers are involved.

For very small state machines, the use of the state pattern is not necessarily the most optimal variant. I myself also like to fall back on the solution with the CASE statement.

Alternatively, IEC 61131-3 offers a further option for implementing state machines with the Sequential Function Chart (SFC). But that is another story.

Definition

In the book “Design patterns: elements of reusable object-oriented software” by Gamma, Helm, Johnson and Vlissides, this is expressed as follows:

Allow an object to change its behavior when its internal state changes. It will look as if the object has changed its class.

Implementation

A common interface (State) is defined, which contains a method for each state transition. For each state, a class is created that implements this interface (State1, State2, …). As all states have the same interface, they are interchangeable.

Such a state object is aggregated (encapsulated) by the object whose behavior has to be changed depending on the state (Context). This object represents the current internal state (currentState) and encapsulates the state-dependent behavior. The context delegates calls to the currently set status object.

The state changes can be performed by the specific state objects themselves. To do this, each status object requires a reference to the context (Context). The context must also provide a method for changing the state (setState()). The subsequent state is passed to the method setState() as a parameter. For this purpose, the context offers all possible states as properties.

UML Diagram

 

Based on the example above, the following assignment results:

ContextFB_Machine
StateI_State
State1, State2, …

FB_CoinEjectedState, FB_HasCoinState,
FB_ProductEjectedState, FB_WaitingState

Handle()

CustomerTakesCoin(), CustomerTakesProduct(),
InsertCoin(), PressButton()

GetState1, GetState2, …

CoinEjectedState, HasCoinState,
ProductEjectedState, WaitingState

currentStateipState
setState()SetState()
contextpMachine

Application examples

A TCP communication stack is a good example of using the state pattern. Each state of a connection socket can be represented by corresponding state classes (TCPOpen, TCPClosed, TCPListen, …). Each of these classes implements the same interface (TCPState). The context (TCPConnection) contains the current state object. All actions are transferred to the respective state class via this state object. This class processes the actions and changes to a new state if necessary.

Text parsers are also state-based. For example, the meaning of a character usually depends on the previously read characters.

iec-61131-3 codesys-v3 oop twincat plc interfaces inheritance iec-61131-3-(english) methods
Schreibe einen Kommentar:
Themen:
methods iec-61131-3-(english) inheritance interfaces plc twincat oop codesys-v3 iec-61131-3
Entweder einloggen... ...oder ohne Wartezeit registrieren
Benutzername
Passwort
Passwort wiederholen
E-Mail