Develop only what changes
The basic idea behind our framework is simple: it is designed to take as much work off the developer of an individual machine as possible. This not only makes the developer’s work easier, but also saves time and money. Special purpose machines differ functionally in many ways, but for us developers there are basically two categories: the integrated hardware to be controlled and the sequence logic. Our framework abstracts these two aspects.
Each device, whether it is a simple digital sensor or a hard drive packed to the brim with functionality, provides its own commands. A long-term goal of cinnamon is to provide the widest possible range of hardware drivers, so that you have a ready-made set of components to choose from.
The second aspect is the sequence logic of a system. A sequence is a series of process steps, which in turn are subdivided into finer sequences until you reach the hardware level. And this is where we use the abstraction from hardware to commands: In cinnamon, a sequence is a chain of commands. Whether it is reading a sensor value, controlling an actuator, or just handling data, it is still commands that you start and wait until they are finished.
Stable but flexible
The cinnamon framework libraries take care of all other key aspects of the machine software in the background. It comes with pre-built operation mode handling, device monitoring and error management, data acquisition, and a mostly self- building HMI. The libraries are modular, so that you only have to integrate what you really need.
In contrast to other software standards in the automation industry, we deliberately avoid code generation and additional external design tools, as these create very strict dependencies between the generated software components, reduce the maintainability of the code, and make it difficult for teams to work on the same project due to non-mergeable project files.
Open source
We have chosen an open source approach so that everyone can see for themselves what they are getting. We want to share our knowledge and allow anyone to integrate the framework or individual components into existing environments.
The basic concept
Everything is an object
All classes and components in our framework are derived from the CNM AbstractObject. The object itself provides the class name of the concrete class, the variable pathname of the instance, a unique hashcode for each instance, and methods for cloning and destructing an object. All of these methods and properties are defined by the IObject interface. This allows us to treat each class as an object. Our collections work with interface references of type IObject. Since everything in our framework is an object, we can add any generic instance of any class to any list or tree. Interface casts can easily be used for typed collections (__QUERYINTERFACE).
Almost all software design patterns can be implemented using the generic object class. Design patterns are like tools in a toolbox – you use them when they fit the problem to achieve your goal faster, safer, and more elegantly.
The cinnamon toolset
The cinnamon framework comes standard with a number of useful tools. These include linked lists, array lists, balanced binary search trees and sets. Hashtables and dictionaries are planned. The assertions provide extensive data type dependent checks to detect errors in the code early and to test your own components.
The CNM Unicode Utilities can manage, normalize or format Unicode as UTF-8 or UTF-16, which is useful for international projects.
Production metrics such as cycle time measurement or part data tracking are provided, as well as easy-to-use management for recipes and machine settings.
It all starts with a tree
Any automation system can be represented in a tree structure, where the decisive point is the encapsulation of the components. As an example, let’s imagine a simple machine that has an infeed, a handling unit for placing the parts to be fed, an adhesive dispenser, an oven for curing, and an outfeed:
The handling unit consists of a gripper with four pneumatic individual grippers and a light barrier, an XYZ gantry consisting of X, Y and Z axes, a stopper, an indexing cylinder and a feeder.
We can do the same for the other functional units, breaking them down into smaller and smaller components until we reach the hardware level. Instead of the encapsulated view, we can display it as a tree:
Given this system architecture, we can use the Hierarchical Composition Pattern (hierarchische Kompositionsmuster), das wir in einem anderen Blogbeitrag erläutert haben, verwenden, um den aktuellen Betriebsmodus über den Baum an jeden Knoten (Node) zu verteilen. So wird auch sichergestellt, dass jeder Knoten zyklisch vom Pattern aufgerufen wird. Dazu müssen die Softwarekomponenten lediglich den CNM AbstractCyclicNode (für Geräte und überwachte Komponenten) oder den CNM AbstractModeNode (für Funktionseinheiten mit Moduslogik) erweitern. Ein Implementierungsbeispiel ist unten dargestellt.
Another advantage of this architecture is that each node in the tree has knowledge of its own child nodes. This allows us to read out and reconstruct the structure of the tree. We use this to automatically build our HMI dynamically, almost as if by magic.
The self-building HMI
We recommend the use of the TwinCAT HMI, as this can be best integrated into the Beckhoff environment. Technically, however, many other HMI solutions would also work, since most of the logic is based on the ADS interface.
We have developed a server extension and client packages for TwinCAT HMI, which can be integrated via the NuGet Manager. The server extension adopts the structure of the PLC tree: only the root node, i.e. the machine node in the figure above, has to be configured for the extension. The ADS path, name and class or HMI template for each child node are then read recursively via ADS using Remote Procedure Calls, and symbols are generated on the HMI server. To reduce the load on the PLC’s auxiliary task, the tree is read only once after a restart, i.e., after the PLC has been initialized. The structure and symbols are then available on the server. When an HMI client logs on to the server, it automatically retrieves the mappings for the tree. The class templates are used to automatically load controls from the standard kit as well as project-specific custom views for a selected node.
Managed production cycles
To implement sequential production logic in Structured Text, CASE branches are often used in combination with a step variable. We have taken this simple concept and developed a CNM CycleManager for it. This class manages the edge evaluation of the execute input, the step change, the return value of an operation mode chain or commands, and can handle errors automatically. It can also execute and evaluate commands and offers a variety of configuration options for mapping step operation or stop requests. This makes the implementation of mode sequences convenient and fast.
Hands On
The cinnamon framework is still in the pre-release phase, so the code examples shown here are subject to change. Also, the examples are not intended to be complete or for a specific machine, but only to illustrate the basic idea and functionality of the framework.
Creating Nodes
Let’s return to the tree example from above to implement the described handling unit. While the structural design works its way from the root node to the devices, the implementation is done from the bottom up. So let us start with the gripper unit:
FUNCTION_BLOCK HandlingGripperUnit EXTENDS CNM_OpModeHandler.AbstractCyclicNode
VAR
(* The four individuell gripper cylinder *)
gripper1 :Cylinder(THIS^);
gripper2 :Cylinder(THIS^);
gripper3 :Cylinder(THIS^);
gripper4 :Cylinder(THIS^);
(* light barrier to check if the gripper is occupied *)
partPresentSensor :DigitalSensor(THIS^);
END_VAR
The devices are initialized with THIS^ because they are nodes. If you declare a node with another parent node, it registers itself in the parent node as a child node. It joins the hierarchy tree.
The HandlingGripperUnit inherits from the AbstractCyclicNode class. This gives it a run method that is called cyclically and can be used for various monitoring purposes. It does not inherit from the AbstractModeNode because it does not have its own process logic, only commands. The same applies to the XYZ gantry:
FUNCTION_BLOCK XYZGantryUnit EXTENDS CNM_OpModeHandler.AbstractCyclicNode
VAR
(* The three linear absolute positioning axes *)
axisX :AbsoluteAxis(THIS^);
axisY :AbsoluteAxis(THIS^);
axisZ :AbsoluteAxis(THIS^);
END_VAR
The indexing unit is just a cylinder, while the feeder and the stopper are assumed to be already implemented classes to keep it simple. The handling unit itself will look like this:
FUNCTION_BLOCK HandlingUnit EXTENDS CNM_OpModeHandler.AbstractModeNode
VAR
handlingGripper :HandlingGripperUnit(THIS^);
xyzGantry :XYZGantryUnit(THIS^);
stopper :StopperUnit(THIS^);
indexing :Cylinder(THIS^);
feeder :FeederUnit(THIS^);
END_VAR
Note that the handling unit inherits from the AbstractModeNode because it contains the process logic for this process section. Assuming that all other units are done in the same way, we can create the machine unit:
FUNCTION_BLOCK MachineUnit EXTENDS CNM_OpModeHandler.AbstractModeNode
VAR
infeed :InfeedUnit(THIS^);
handling :HandlingUnit(THIS^);
dispenser :DispenserUnit(THIS^);
oven :OvenUnit(THIS^);
outfeed :OutfeedUnit(THIS^);
END_VAR
In addition to an instance of the machine unit, the MAIN program must declare an OpmodeHandler instance, an HMI instance, and a SafetyUnit. These are ready-to-use classes from our CNM library. The instance of the machine is passed to the OpmodeHandler as RootNode:
PROGRAM MAIN
VAR
myStation :MachineUnit(0);
safetyHandler :SafetyUnit;
handler :CNM_OpModeHandler.OpModeHandler(rootNode := myStation, safety := safetyHandler);
hmi :CNM_OpModeHandler.BaseHmi(handler);
END_VAR
safetyHandler();
hmi.run();
handler.run();
Although only Safety, HMI and OpModeHandler are called in the body of the MAIN, the architecture of the framework ensures that all nodes are called cyclically.
Connect to the HMI
Even if there is no logic implemented yet, we can create an HMI project and configure the root node path for the CNM Server extension. Once the PLC project is downloaded and started, the HMI Server will retrieve the tree structure:
Standard controls are provided with the CNM HMI packages for common devices such as pneumatic cylinders, axes, sensors, and more. If the I/Os are already mapped in the PLC, you can now perform an I/O check in manual mode and control or read devices.
You can create your own HMI views for custom controllers, as shown in the example for the machine or the handling unit. These are normal TwinCAT HMI controls with fixed mapping, but they are loaded dynamically by selecting the corresponding node, just like the CNM controls.
The CNM HMI creates localization keys for all nodes according to their variable paths in the PLC. Simply add the desired project languages and insert the translations. The HMI will then attempt to request texts via a localization key.
The setting for which control to load for which node is made in the HMI configuration file. For example, it is possible to create your own custom controls for a CNM cylinder.
Adding mode sequences
The CNM AbstractModeNode defines abstract mode methods: For automatic mode runAutomatic, for home position runHoming, for manual mode runManual. There are more operating modes and you can also add your own. These methods are called depending on the selected operating mode and derived classes must implement them.
Let us consider a simple process for handling, where the handling puts 4 for parts from the feeder to the carrier:
The code for this sequence can be as follows:
(*
short summary
=================
This method contains instructions and sequences for the automatic mode. It has to be overwritten for every node.
If the node should do nothing, return a 'SUCCESS'.
It should run in an endless loop until the mode is stopped immedtiately or a stop request is raised (THIS^.modeControl.stopRequest).
This method is called automatically by the operation mode handler.
.. attention: If this method returns the state 'ERROR', the operation mode handler automatically disables the whole machine!
..
legal notes
=================
| SPDX-FileCopyrightText: © 2024 ekvip automation GmbH
| SPDX-License-Identifier: Apache-2.0
| For details check: Apache-2.0_
.. _Apache-2.0: https://www.apache.org/licenses/LICENSE-2.0
..
*)
METHOD runAutomatic : CNM_OpModeHandlerInterfaces.CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
execute : BOOL;
pause : BOOL;
END_VAR
VAR CONSTANT
WAIT_FOR_CARRIER :DINT := 10;
INDEXING :DINT := WAIT_FOR_CARRIER + 1;
FEED_PARTS :DINT := INDEXING + 1;
MOVE_TO_FEEDER :DINT := FEED_PARTS + 1;
CLOSE_GRIPPER :DINT := MOVE_TO_FEEDER + 1;
MOVE_TO_PLACE :DINT := CLOSE_GRIPPER + 1;
OPEN_GRIPPER :DINT := MOVE_TO_PLACE + 1;
RELEASE_INDEX :DINT := OPEN_GRIPPER + 1;
SEND_CARRIER :DINT := RELEASE_INDEX + 1;
END_VAR
CASE THIS^.cycleManager.step.current OF
CNM_ReturnTypes.DefaultSteps.STEP.INIT:
THIS^.cycleManager.proceedWith(WAIT_FOR_CARRIER);
WAIT_FOR_CARRIER:
THIS^.cycleManager.executeCommand(
THIS^.stopper.commands.waitForPart
);
THIS^.cycleManager.leave(
THIS^.cyleTimeRecorder.commands.startCycle()
)
INDEXING:
THIS^.cycleManager.step.next := SEL(THIS^.feeder.hasPart, FEED_PARTS, MOVE_TO_FEEDER)
THIS^.cycleManager.executeCommand(
THIS^.indexing.commands.extend
);
FEED_PARTS:
THIS^.cycleManager.executeCommand(
THIS^.feeder.commands.feedParts
);
MOVE_TO_FEEDER:
THIS^.cycleManager.executeCommand(
THIS^.xyzGantry.commands.moveToPos(
x := 3.14,
y := 42.0,
z := 2.0
)
);
CLOSE_GRIPPER:
THIS^.cycleManager.executeCommand(
THIS^.handlingGripper.commands.closeAll
);
MOVE_TO_PLACE:
THIS^.cycleManager.executeCommand(
THIS^.xyzGantry.commands.moveToPos(
x := 0.0,
y := 0.0,
z := 0.0
)
);
OPEN_GRIPPER:
THIS^.cycleManager.executeCommand(
THIS^.handlingGripper.commands.openAll
);
RELEASE_INDEX:
THIS^.cycleManager.executeCommand(
THIS^.indexing.commands.retract
);
SEND_CARRIER:
THIS^.cycleManager.configuration.step.stopRequest.afterSuccess();
THIS^.cycleManager.step.next := WAIT_FOR_CARRIER;
THIS^.cycleManager.executeCommand(
THIS^.stopper.commands.send
);
THIS^.cycleManager.leave(
THIS^.cyleTimeRecorder.commands.captureCurrentCycle()
);
END_CASE
runAutomatic := THIS^.cycleManager.state;
The CycleManager expects only the ICommand and ISingleAttempt interfaces for evaluation. A command can run over several cycles. Commands are started with a rising edge of the execute input. The SingleAttempt, on the other hand, is called exactly once and is used for enter() and leave() of the CycleManager. As the names suggest, enter() is called exactly once when a new step is entered and leave() is called exactly once when a step is exited. The step is exited when all evaluation results, e.g. of a command, return SUCCESS. You can also force a new step with the methods proceed() and proceedWith(step :DINT).
The CycleManager can also handle the error management of a sequence.
myAlarm :CNM_ConcreteMessages.TcError(
event := Global.TC_EVENTS.MyEvents.something ,
injectedService := THIS^.messageService
);
RUN_COMMAND:
THIS^.cycleManager.executeCommand(
command := THIS^.cylinder.commands.extend,
errorStep := MY_ERROR
);
MY_ERROR:
THIS^.cycleManager.handle(myAlarm);
If the extend command of the cylinder returns an ERROR result, the CycleManager will proceed with a given error step. The handle() method raises the alarm and automatically sets the error flag for this node. It then waits for the alarm to be acknowledged. When it’s acknowledged, the error flag is cleared and the CycleManager returns to the last step.
We use the TwinCAT EventLogger for our error management and build a mechanism around it to react automatically to errors. For example, if a raised error has the severity ‚Critical‘, the operation mode handler will stop the machine immediately. On the HMI, nodes with an active alarm are highlighted in red.