This blog post is about a specialized composite pattern. It allows us to work with a hierarchy of objects (like an n-tree structure) in a uniform way. By designing a tree node that can execute operation modes and manage any number of child nodes, we can traverse all tree elements in one call.
The machines we program all have a hierarchical structure (see Figure 1).
Figure 1
A tiresome topic with the machines are the operating modes, therefore these will now serve as an example. The example is very simplified and incomplete because it is only meant to explain a principle, and a complete example would go beyond the scope of the blog post.
Our Machine
The machine has three stations: one infeed, one outfeed and one press station.
Since a picture is worth a thousand words, take a look at Figure 2. I have only drawn the structure of the inlet to outline the tree structure. The other stations have been left out.
Figure 2
The Idea
We build a tree node that can execute an operation mode and can have any number of children. Furthermore, the node should call the corresponding operation mode for all children nodes, so that we get a traverse call over all tree elements. The calls of the corresponding operation mode should be done via an interface that a client can call (operation mode handler and parent node).
When calling the constructor (FB_init), the parent node should be passed, the child node calls the addChildNode method with itself. This way we save attachment methods, and the declaration structure also reflects the tree structure (ease of use and good readability). The class structure is shown in Figure 3, and the idea of communication is indicated in Figure 4.
Figure 3
Figure 4
In the introduction, I talked about how this is a specialized form of the composite pattern. The classic composite pattern distinguishes between nodes and leaves, i.e. like a folder structure there is the folder (root node) which contains files (leaves) and subfolders (nodes). A folder cannot be a file at the same time. In our example, however, each node can be a leaf and can also contain nodes and leaves.
Node Implementation
For the implementation of the example, our cinnamon framework libraries are needed. They will be published soon here on our website.
The example uses CNM_AbstractObject, CNM_RetrunTypes, CNM_Collections, CNM_State-Machine-Lib, CNM_CollectionInterfaces, and CNM_CycleManagerInterface.
Interfaces
As always, we start with the interfaces. We need the three interfaces INodeApi, INode and INodeBehavior. The structure of the interfaces can be seen in Figure 5.
INodeApi
INTERFACE INodeApi EXTENDS CNM_AbstractObject.IObject
METHOD executeAutomaticMode :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
END_VAR
END_METHOD
METHOD executeHoming : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
END_VAR
END_METHOD
METHOD executeManualMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
END_VAR
END_METHOD
PROPERTY stopRequest : BOOL
SET
END_PROPERTY
INode
INTERFACE INode EXTENDS INodeApi
METHOD addChildNode
VAR_INPUT
child :INode;
END_VAR
END_METHOD
METHOD runAutomatic :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute : BOOL;
END_VAR
END_METHOD
METHOD runHoming :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute : BOOL;
END_VAR
END_METHOD
METHOD runManual : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute : BOOL;
END_VAR
END_METHOD
INodeBehavior
INTERFACE INodeBehavior EXTENDS CNM_AbstractObject.IObject
METHOD executeMode :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
node :INodeApi;
END_VAR
END_METHOD
METHOD runMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
node :INode;
END_VAR
END_METHOD
Figure 5
Classes
Delegates
First, we implement the three delegates for Automatic, Homing and Manual, because these are needed for our AbstractNode. The structure of the delegates can be seen in Figure 6.
AutomaticBehavior
FUNCTION_BLOCK AutomaticBehavior EXTENDS CNM_AbstractObject.Object IMPLEMENTS INodeBehavior
METHOD executeMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
node :INodeApi;
END_VAR
IF (THIS^.isObjectValid(node)) THEN
executeMode := node.executeAutomaticMode(execute := execute);
ELSE
executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
END_IF
END_METHOD
METHOD runMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
node :INode;
END_VAR
IF (THIS^.isObjectValid(node)) THEN
runMode := node.runAutomatic(execute := execute);
ELSE
executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
END_IF
END_METHOD
PROPERTY className :CNM_AbstractObject.ClassName
GET
className := 'AutomaticBehavior';
END_GET
END_PROPERTY
HomingBehavior
FUNCTION_BLOCK HomingBehavior EXTENDS CNM_AbstractObject.Object IMPLEMENTS INodeBehavior
METHOD executeMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
node :INodeApi;
END_VAR
IF (THIS^.isObjectValid(node)) THEN
executeMode := node.executeHoming(execute := execute);
ELSE
executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
END_IF
END_METHOD
METHOD runMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
node :INode;
END_VAR
IF (THIS^.isObjectValid(node)) THEN
runMode := node.runHoming(execute := execute);
ELSE
executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
END_IF
END_METHOD
PROPERTY className :CNM_AbstractObject.ClassName
GET
className := 'HomingBehavior';
END_GET
END_PROPERTY
ManualBehavior
FUNCTION_BLOCK ManualBehavior EXTENDS CNM_AbstractObject.Object IMPLEMENTS INodeBehavior
METHOD executeMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
node :INodeApi;
END_VAR
IF (THIS^.isObjectValid(node)) THEN
executeMode := node.executeManualMode(execute := execute);
ELSE
executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
END_IF
END_METHOD
METHOD runMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
node :INode;
END_VAR
IF (THIS^.isObjectValid(node)) THEN
node := node.runManual(execute := execute);
ELSE
executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
END_IF
END_METHOD
PROPERTY className :CNM_AbstractObject.ClassName
GET
className := 'ManualBehavior';
END_GET
END_PROPERTY
Figure 6
Abstract Node
Now the abstract node can finally be implemented, and with it the core of our concept. There are actually three methods that are important to understand.
- The constructor (FB_init), because here our tree structure is built.
- The protected method handleMode, because it manages the execution and return values of the own node and the child nodes.
- The property stopRequest, because it shares a stop request with all the children.
FUNCTION_BLOCK ABSTRACT AbstractNode EXTENDS CNM_AbstractObject.Object IMPLEMENTS INode
VAR
childNodes :CNM_Collections.ArrayList(initialSize := 15);
stopRequestIsPending :BOOL;
END_VAR
METHOD PROTECTED iterateChilds :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
END_VAR
VAR_OUTPUT
child :INodeApi;
END_VAR
VAR
object :CNM_AbstractObject.IObject;
END_VAR
iterateChilds := THIS^.childNodes.iterate(
execute := execute,
object => object
);
IF (NOT __QUERYINTERFACE(object, child)) THEN
child := 0;
END_IF
END_METHOD
METHOD PROTECTED handleMode :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute :BOOL;
behavaior :INodeBehavior;
END_VAR
VAR_INST
cycleManger :CNM_State_Machine_Lib.SingleExecuteCycleManager();
everthingIsOk :BOOL;
END_VAR
VAR
childNode :INodeApi;
allChildsFinished :BOOL;
myState :CNM_ReturnTypes.SingleExecutionState;
errorCount :__UXINT;
END_VAR
VAR CONSTANT
EXECUTION_STEP :DINT := 2;
EVALUATION_STEP :DINT := 3;
END_VAR
cycleManger.execute := execute;
CASE cycleManger.currentStep OF
CNM_ReturnTypes.DefaultSteps.STEP.INIT:
everthingIsOk := TRUE;
behavaior.runMode(execute := FALSE, node := THIS^);
THIS^.iterateChilds(execute:=FALSE);
WHILE (THIS^.iterateChilds(execute:=TRUE,child => childNode) <> CNM_ReturnTypes.SingleExecutionState.BUSY) DO
IF (childNode <> 0) THEN
behavaior.executeMode(execute := FALSE, node := childNode);
END_IF
END_WHILE
THIS^.stopRequest := FALSE;
cycleManger.transitions.nextStep.noCondition.now();
EXECUTION_STEP:
THIS^.iterateChilds(execute:=FALSE);
allChildsFinished := TRUE;
errorCount := 0;
WHILE (THIS^.iterateChilds(execute:=TRUE,child => childNode) <> CNM_ReturnTypes.SingleExecutionState.BUSY) DO
IF (childNode <> 0) THEN
CASE behavaior.executeMode(execute := TRUE, node := childNode) OF
CNM_ReturnTypes.SingleExecutionState.BUSY: allChildsFinished := FALSE;
CNM_ReturnTypes.SingleExecutionState.ERROR:
errorCount := errorCount+1;
ELSE;//nothing to do
END_CASE
END_IF
END_WHILE
myState := behavaior.runMode(execute := TRUE, node := THIS^);
cycleManger.transitions.nextStep.ifTrue(
(myState <> CNM_ReturnTypes.SingleExecutionState.BUSY)
AND allChildsFinished
).now();
everthingIsOk := ((myState <> CNM_ReturnTypes.SingleExecutionState.ERROR) AND (errorCount = 0));
IF (NOT everthingIsOk) THEN
THIS^.stopRequest := TRUE;
END_IF
EVALUATION_STEP:
cycleManger.transitions.setSuccess.noCondition.now();
cycleManger.assert.boolean.valueIsTrue(everthingIsOk, 'something went wrong');
ELSE;//whatever
END_CASE
executeAutomaticMode := cycleManger.executionState;
END_METHOD
METHOD executeAutomaticMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute : BOOL;
END_VAR
VAR_INST
modeBehavior :AutomaticBehavior();
END_VAR
executeAutomaticMode := THIS^.handleMode(execute, modeBehavior);
END_METHOD
METHOD executeHoming : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute : BOOL;
END_VAR
VAR_INST
modeBehavior :HomingBehavior();
END_VAR
executeHoming := THIS^.handleMode(execute, modeBehavior);
END_METHOD
METHOD executeManualMode :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute : BOOL;
END_VAR
VAR_INST
modeBehavior :ManualBehavior();
END_VAR
executeManualMode := THIS^.handleMode(execute, modeBehavior);
END_METHOD
PROPERTY stopRequest :BOOL
SET
VAR
childNode :INodeApi;
END_VAR
IF (THIS^.stopRequestIsPending <> stopRequest) THEN
THIS^.iterateChilds(execute:=FALSE);
WHILE (THIS^.iterateChilds(execute:=TRUE,child => childNode) <> CNM_ReturnTypes.SingleExecutionState.BUSY) DO
IF (childNode <> 0) THEN
childNode.stopRequest := stopRequest;
END_IF
END_WHILE
THIS^.iterateChilds(execute:=FALSE);
END_IF
THIS^.stopRequestIsPending := stopRequest;
END_SET
END_PROPERTY
METHOD addChildNode
VAR_INPUT
child : INode;
END_VAR
THIS^.childNodes.append(child);
END_METHOD
METHOD runAutomatic :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute : BOOL;
END_VAR
runAutomatic := CNM_ReturnTypes.SingleExecutionState.SUCCESS;
END_METHOD
METHOD runHoming :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute : BOOL;
END_VAR
runHoming := CNM_ReturnTypes.SingleExecutionState.SUCCESS;
END_METHOD
METHOD runManual :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute : BOOL;
END_VAR
runManual := CNM_ReturnTypes.SingleExecutionState.SUCCESS;
END_METHOD
METHOD FB_init
VAR_INPUT
(* if TRUE, the retain variables are initialized (warm start / cold start)*)
bInitRetains :BOOL;
(* if TRUE, the instance afterwards gets moved into the copy code (online change) *)
bInCopyCode :BOOL;
parentNode :INode;
END_VAR
VAR
{attribute 'hide'}
newHashstate3 :Hashcode;
END_VAR
VAR CONSTANT
{attribute 'hide'}
NUMBER_OF_LEFT_SHIFTS :UINT := 17;
{attribute 'hide'}
NUMBER_OF_LEFT_ROTATIONS :UINT := 45;
END_VAR
IF (THIS^.isObjectValid(parentNode)) THEN
parentNode.addChildNode(THIS^);
END_IF
END_METHOD
PROPERTY className :CNM_AbstractObject.ClassName
GET
className := 'AbstractNode';
END_GET
END_PROPERTY
Now if I want to create a class for the machine, a station, a function group or a device, that class just needs to inherit from AbstractNode. Then I can just add the class to our hierarchy. For functionality of the node, the methods runAutomatic, runHoming, runManual are overridden, if needed. Figure 7 summarizes the structure of AbstractNode again.
Figure 7
Operation Mode Handling
Now we need a client that tells the tree what to do. This is our operation mode handler. In our example, we assume that the opmode is an unsigned short integer that comes by magic from the HMI. As said before, the example is incomplete. The operation mode handler should get a node and make the node do what needs to be done.
For readability, we build an enumeration which represents the different operation modes.
OpMode enumeration
{attribute 'qualified_only'}
TYPE OpMode :
(
IDLE := 0,
HOMING := 1,
AUTOMATIC := 2,
MANUAL := 3
)USINT;
END_TYPE
Class OperationMode
FUNCTION_BLOCK OperationMode EXTENDS CNM_AbstractObject.Object
VAR
hmiButtonOperationMode :OpMode;
rootNode :INodeApi;
END_VAR
METHOD FB_init
VAR_INPUT
(* if TRUE, the retain variables are initialized (warm start / cold start)*)
bInitRetains :BOOL;
(* if TRUE, the instance afterwards gets moved into the copy code (online change) *)
bInCopyCode :BOOL;
rootNode :INodeApi;
END_VAR
VAR
{attribute 'hide'}
newHashstate3 :Hashcode;
END_VAR
VAR CONSTANT
{attribute 'hide'}
NUMBER_OF_LEFT_SHIFTS :UINT := 17;
{attribute 'hide'}
NUMBER_OF_LEFT_ROTATIONS :UINT := 45;
END_VAR
THIS^.rootNode := rootNode;
END_METHOD
METHOD run
VAR_INST
opModeChanged :BOOL;
activeOpMode :OpMode := OpMode.IDLE;
lastOpMode :OpMode := OpMode.IDLE;
END_VAR
IF (THIS^.isObjectNull(THIS^.rootNode)) THEN
RETURN;
END_IF
IF (THIS^.hmiButtonOperationMode <> lastOpMode) THEN
THIS^.rootNode.stopRequest := TRUE;
lastOpMode := THIS^.hmiButtonOperationMode;
END_IF
CASE activeOpMode OF
OpMode.IDLE:
THIS^.rootNode.executeAutomaticMode(FALSE);
THIS^.rootNode.executeHoming(FALSE);
THIS^.rootNode.executeManualMode(FALSE);
activeOpMode := THIS^.hmiButtonOperationMode;
OpMode.HOMING:
activeOpMode := SEL(
THIS^.rootNode.executeHoming(TRUE) = CNM_ReturnTypes.SingleExecutionState.BUSY,
OpMode.IDLE,
OpMode.HOMING
);
OpMode.AUTOMATIC:
activeOpMode := SEL(
THIS^.rootNode.executeAutomaticMode(TRUE) = CNM_ReturnTypes.SingleExecutionState.BUSY,
OpMode.IDLE,
OpMode.AUTOMATIC
);
OpMode.MANUAL:
activeOpMode := SEL(
THIS^.rootNode.executeManualMode(TRUE) = CNM_ReturnTypes.SingleExecutionState.BUSY,
OpMode.IDLE,
OpMode.MANUAL
);
ELSE
activeOpMode := OpMode.IDLE;
END_CASE
END_METHOD
PROPERTY className :CNM_AbstractObject.ClassName
GET
className := 'OperationMode';
END_GET
END_PROPERTY
In summary, you can find our client in Figure 8.
Figure 8
Our Machine Implementation
Don’t worry, I’m not going to implement a machine here, I’m just going to show you what it might look like.
Infeed
FUNCTION_BLOCK InfeedConveyor EXTENDS AbstractNode
VAR
infeedConveyor :Conveyor(THIS^);
nokConveyor :Conveyor(THIS^);
jamStopperinfeed :Stopper(THIS^, infeedConveyor);
preStopperStu :Stopper(THIS^, infeedConveyor);
nokLift :LiftUnit(THIS^, nokConveyor);
stu :StrokeTransverseUnit(THIS^, infeedConveyor, nokLift);
END_VAR
Machine
FUNCTION_BLOCK Machine EXTENDS AbstractNode
VAR
infeed :InfeedConveyor(THIS^);
press :PressStation(THIS^);
outfeed :OutfeedConveyor(THIS^);
END_VAR
Main
PROGRAM MAIN
VAR
machine :Machine(parentNode := 0);
modeHandler :OperationMode(rootNode := machine);
END_VAR
modeHandler.run();
END_PROGRAM
Summary
The example shows very well how easy it is to build and use hierarchical compositions, if they provide the same methods/operations. The calls do not take longer than if you put each call in the main, just so it reads nicely because it reflects the structure of our machine.