How Composite Patterns Simplify Hierarchical Machine Structures

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_Hirarchical_Compositions

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_Hirarchical_Compositions

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_Hirarchical_Compositions

Figure 3

Figure_4_Hirarchical_Compositions

Figure 4

noun-information-6735183-FFFFFF_edited

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

noun-information-6735183-FFFFFF_edited

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_Hirarchical_Compositions

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_Hirarchical_Compositions

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_Hirarchical_Compositions

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_Hirarchical_Compositions

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.