Coming soon: cinnamon – das neue Automatisierungs-Framework von ekvip

Nur das entwickeln, was sich ändert

Die Grundidee unseres Frameworks ist einfach: Es soll den Entwickler einer einzelnen Maschine so weit wie möglich entlasten. Das erleichtert nicht nur die Arbeit des Entwicklers, sondern spart auch Zeit und Geld. Sondermaschinen unterscheiden sich in ihrer Funktion in vielerlei Hinsicht, aber für uns Entwickler gibt es im Wesentlichen zwei Kategorien: die anzusteuernde Hardware und die Ablauflogik. Unser Framework abstrahiert diese beiden Aspekte.

Jedes Gerät, sei es ein einfacher digitaler Sensor oder ein bis zum Anschlag mit Sonderfunktionen ausgestatteter Antrieb, verfügt über eine Auswahl von ausführbaren Kommandos. Ein langfristiges Ziel von cinnamon ist es, eine möglichst breite Palette an Hardwaretreibern bereitzustellen, damit man aus einem vorgefertigten Satz von Komponenten auswählen kann.

Der zweite Aspekt ist die Sequenzlogik eines Systems. Eine Sequenz ist eine Reihe von Prozessschritten, die wiederum in kleinere Sequenzen unterteilt werden, bis man die Hardwareebene erreicht. Hier kommt die Abstraktion von der Hardware zu Kommandos ins Spiel: In cinnamon ist eine Sequenz eine Kette von Kommandos. Ob es nun darum geht, einen Sensorwert zu lesen, einen Aktuator zu steuern oder einfach nur Daten zu verarbeiten, es sind immer Kommandos, die man startet und deren Ausführung man abwartet.

Stabil, aber flexibel

Die Bibliotheken des cinnamon Frameworks übernehmen im Hintergrund alle anderen wichtigen Aspekte der Maschinen-Software. Sie liefern vorgefertigte Funktionen für die Betriebsartenverwaltung (Operation Mode Handling), Device Monitoring und Fehlermanagement, Datenerfassung und ein weitgehend selbstständig aufbauendes HMI. Die Bibliotheken sind modular aufgebaut, sodass nur das eingebunden werden muss, was wirklich benötigt wird.

Im Gegensatz zu anderen Software-Standards in der Automatisierungsindustrie verzichten wir bewusst auf die automatisierte Code-Erzeugung und zusätzliche externe Design-Tools, da diese sehr starke Abhängigkeiten zwischen den generierten Software-Komponenten schaffen. Dies reduziert die Wartbarkeit des Codes und kompliziert die Zusammenarbeit von Teams an einem Projekt, da es das Zusammenführen von Projektdateien erschwert.

Open source

Wir haben uns für einen Open-Source-Ansatz entschieden, damit jeder selbst sehen kann, was er oder sie bekommt. Wir möchten unser Wissen teilen und jedem die Möglichkeit geben, das Framework oder einzelne Komponenten in bestehende Umgebungen zu integrieren.

Das Grundkonzept

Alles ist ein Objekt

Alle Klassen und Komponenten in unserem Framework sind vom CNM AbstractObject abgeleitet. Das Objekt selbst liefert den Klassennamen der konkreten Klasse, den Variablenpfadnamen der Instanz, einen eindeutigen Hashcode für jede Instanz und Methoden zum Klonen und Zerstören eines Objekts. Alle diese Methoden und Eigenschaften sind durch die IObject-Schnittstelle definiert. Dadurch können wir jede Klasse als Objekt behandeln. Unsere Collections arbeiten mit Interface Referenzen vom Typ IObject. Da in unserem Framework alles ein Objekt ist, können wir jede generische Instanz jeder Klasse zu jeder Liste oder jedem Baum hinzufügen. Interface Casts können problemlos für typisierte Collections verwendet werden (__QUERYINTERFACE).

Fast alle Designmuster für Software lassen sich mithilfe der generischen Objektklasse implementieren. Designmuster sind wie Werkzeuge in einem Werkzeugkasten – man verwendet sie, wenn sie zum Problem passen, um das Ziel schneller, sicherer und eleganter zu erreichen.

Der cinnamon Werkzeugkasten

Das cinnamon Framework wird standardmäßig mit einer Reihe nützlicher Werkzeuge ausgeliefert. Dazu gehören verlinkte Listen, Array-Listen, balancierte binäre Suchbäume und Sets. Hash Tables und Dictionaries sind in Planung. Die Assertions bieten umfangreiche datentypabhängige Prüfungen, um Fehler im Code frühzeitig zu erkennen und eigene Komponenten zu testen.

Die CNM Unicode Utilities können Unicode verwalten, normalisieren oder als UTF-8 oder UTF-16 formatieren, was für internationale Projekte nützlich ist.

Produktionskennzahlen wie Taktzeiterfassungen oder Teiledatenverfolgung werden bereitgestellt, ebenso wie eine benutzerfreundliche Verwaltung für Rezepturen und Maschineneinstellungen.

Am Anfang steht ein Baum

Jedes Automatisierungssystem lässt sich in einer Baumstruktur darstellen, wobei die Kapselung der Komponenten entscheidend ist. Stellen wir uns als Beispiel eine einfache Maschine vor, die über eine Zuführung (Infeed), eine Handling-Einheit zum Platzieren der Teile, einen Klebstoffspender (Dispenser), einen Ofen zum Aushärten und einen Auslauf (Outfeed) verfügt.

Machine

Die Handling-Einheit besteht aus einem Greifer (Gripper) mit vier pneumatischen Einzelgreifern und einer Lichtschranke, einem XYZ-Portal (XYZ-Gantry) aus X-, Y- und Z-Achse, einem Stopper, einem Indexierzylinder (Indexing Unit) und einem Zuführer (Feeder).

HandlingUnitSubunits

Das Gleiche können wir für die anderen Funktionseinheiten tun, indem wir sie in immer kleinere Komponenten zerlegen, bis wir die Hardwareebene erreichen. Anstelle der gekapselten Ansicht können wir sie als Baum darstellen:

completetree

Angesichts dieser Systemarchitektur können wir das 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.

Ein weiterer Vorteil dieser Architektur besteht darin, dass jeder Knoten im Baum seine eigenen Kinderknoten (Child Node) kennt. Dadurch können wir die Struktur des Baums auslesen und rekonstruieren. Wir nutzen dies, um unser HMI automatisch und fast wie von Zauberhand dynamisch aufzubauen.

Das sich selbst erstellende HMI

Wir empfehlen die Verwendung des TwinCAT HMI, da dieses am besten in die Beckhoff-Umgebung integriert werden kann. Technisch sind aber auch viele andere HMI-Lösungen möglich, da der Großteil der Logik auf der ADS-Schnittstelle basiert.

Wir haben eine Servererweiterung und Clientpakete für das TwinCAT HMI entwickelt, die über den NuGet Manager eingebunden werden können. Die Servererweiterung übernimmt die Struktur des SPS-Baums: Nur der Root-Knoten, also der Maschinen-Knoten in der obigen Abbildung, muss für die Erweiterung konfiguriert werden. Der ADS-Pfad, der Name und die Klasse bzw. das HMI-Template für jeden Kinderknoten werden dann rekursiv über ADS mittels Remote Procedure Calls ausgelesen und die Symbole auf dem HMI-Server generiert. Um die Auslastung der Aux-Tasks zu reduzieren, wird der Baum nach einem Neustart, d. h. nach der Initialisierung der SPS, nur einmal gelesen. Die Struktur und die Symbole stehen dann auf dem Server zur Verfügung. Wenn sich ein HMI-Client beim Server anmeldet, ruft er automatisch die Mappings für den Baum ab. Die Klassen-Templates werden verwendet, um Steuerelemente aus dem Standard-Baukasten sowie projektspezifische benutzerdefinierte Ansichten für einen ausgewählten Knoten automatisch zu laden.

Gesteuerte Produktionszyklen

Um sequenzielle Produktionslogik in Structured Text zu implementieren, werden häufig CASE- Anweisungen in Kombination mit einer Schrittvariablen verwendet. Wir haben dieses einfache Konzept aufgegriffen und einen CNM CycleManager dafür entwickelt. Diese Klasse verwaltet die Flankenauswertung des Execute-Eingangs, den Schrittwechsel, den Rückgabewert einer Betriebsmodus-Kette oder von Befehlen und kann selbständig Alarme verwalten. Außerdem kann der CycleManager Kommandos ausführen und auswerten und bietet vielfältige Konfigurationsmöglichkeiten für die Abbildung von Schrittbetrieben oder Stop Requests. Damit lassen sich Betriebsarten bequem und schnell implementieren.

Anwendungen in der Praxis

noun-information-6735183-FFFFFF_edited

Das cinnamon Framework befindet sich noch in der Pre-Release-Phase, daher können sich die hier gezeigten Codebeispiele gegebenenfalls ändern. Außerdem sind die Beispiele nicht vollständig oder für eine konkrete Maschine gedacht, sondern sollen lediglich die Grundidee und Funktionalität des Frameworks veranschaulichen.

Knoten erstellen

Kehren wir zum obigen Baumbeispiel zurück, um die beschriebene Handling-Einheit zu implementieren. Während sich das strukturelle Design vom Root-Knoten zur Hardware hin entwickelt, erfolgt die Implementierung von unten nach oben. Beginnen wir also mit der Greifereinheit:

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

Die Einheiten werden mit THIS^ initialisiert, da es sich um Knoten handelt. Wenn man einem Knoten bei der Initialisierung einen Elternknoten (Parent Node) übergibt, registriert er sich selbst als Kinderknoten. Er wird in die Hierarchiebaumstruktur eingefügt.

Die HandlingGripperUnit erbt von der Klasse AbstractCyclicNode. Dadurch erhält sie eine Run-Methode, die zyklisch aufgerufen wird und für verschiedene Monitoringzwecke verwendet werden kann. Sie erbt nicht von der Klasse AbstractModeNode, da sie keine eigene Prozesslogik besitzt, sondern nur Befehle. Dasselbe gilt für den 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

Die Indizierungseinheit ist lediglich ein Zylinder, während der Zuführer und der Stopper der Einfachheit halber als bereits implementierte Klassen angenommen werden. Die Handling-Einheit selbst sieht wie folgt aus:

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

Es ist zu beachten, dass die Handling-Einheit von der AbstractModeNode abgeleitet ist, da sie die Prozesslogik für diesen Prozessabschnitt enthält. Unter der Annahme, dass alle anderen Einheiten auf die gleiche Weise erstellt wurden, können wir die Maschineneinheit erstellen:

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

Zusätzlich zu einer Instanz der Maschineneinheit muss das MAIN-Programm eine OpmodeHandler-Instanz, eine HMI-Instanz und eine SafetyUnit deklarieren. Dies sind gebrauchsfertige Klassen aus unserer CNM-Bibliothek. Die Instanz der Maschine wird als RootNode an den OpmodeHandler übergeben:

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();

Obwohl im Hauptteil von MAIN nur Safety, HMI und OpModeHandler aufgerufen werden, stellt die Architektur des Frameworks sicher, dass alle Knoten zyklisch aufgerufen werden.

Mit dem HMI verbinden

Auch wenn noch keine Logik implementiert ist, können wir ein HMI-Projekt erstellen und den Root Node Pfad für die CNM Server-Erweiterung konfigurieren. Sobald das SPS-Projekt heruntergeladen und gestartet ist, ruft der HMI-Server die Baumstruktur ab:

Screenshot_HMI

Die CNM-HMI-Pakete enthalten Controls für gängige Komponenten wie Pneumatikzylinder, Achsen, Sensoren und vieles mehr. Wenn die I/Os gemappt wurden, könnte nun bereits ein I/O-Check durchgeführt werden. Die angeschlossen Geräte können angesteuert und ausgelesen werden.

Sie können eigene HMI-Bedienelemente für maschinenspezifische Logikeinheiten erstellen, wie im Beispiel für die Maschine oder die Handling-Einheit gezeigt. Dabei handelt es sich um normale TwinCAT-HMI-Controls mit festem Mapping, die jedoch wie die CNM-Controls durch Auswahl des entsprechenden Knotens auf dem HMI dynamisch geladen werden.

Das CNM-HMI erstellt Lokalisierungsschlüssel für alle Knoten entsprechend ihrer Variablenpfade in der SPS. Es müssen lediglich die gewünschten Projektsprachen hinzugefügt und die Übersetzungen eingefügt werden. Das HMI versucht dann, Texte über einen Lokalisierungsschlüssel zu laden.

Die Einstellung, welche Controls für welchen Knoten geladen werden soll, wird in der HMI-Konfigurationsdatei vorgenommen. Zum Beispiel ist es möglich, eigene HMI-Ansichten für einen CNM-Zylinder zu erstellen.

Modussequenzen hinzufügen

Der CNM AbstractModeNode definiert abstrakte Methoden für Betriebsarten: Für den automatischen Modus runAutomatic, für die Grundstellungsfahrt runHoming, für den manuellen Modus runManual. Es gibt weitere Betriebsmodi, und es können auch eigene hinzugefügt werden. Diese Methoden werden je nach ausgewähltem Betriebsmodus aufgerufen, und abgeleitete Klassen müssen diese implementieren.

Betrachten wir einen einfachen Prozess, bei dem 4 Teile aus dem Zuführer auf den Carrier gelegt werden:

handling-process

Der Code für diesen Ablauf kann wie folgt lauten:

(* 
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;

Der CycleManager verwaltet die Schnittstellen ICommand und ISingleAttempt zur Evaluierung der Rückgabewerte. Ein Kommando kann über mehrere Zyklen laufen. Kommandos werden mit einer steigenden Flanke des Eingangs execute gestartet. Ein SingleAttempt hingegen wird genau einmal aufgerufen und wird für enter() und leave() des CycleManagers verwendet. Wie die Namen schon vermuten lassen, wird enter() genau einmal aufgerufen, wenn ein neuer Schritt betreten wird, und leave() wird genau einmal aufgerufen, wenn ein Schritt verlassen wird. Der Schritt wird verlassen, wenn alle Auswertungsergebnisse, z. B. eines Kommandos, SUCCESS zurückgeben. Mit den Methoden proceed() und proceedWith(step :DINT) ist es auch möglich, einen neuen Schritt zu erzwingen.

Der CycleManager kann auch das Fehlermanagement einer Sequenz übernehmen.

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);

Wenn der Extend-Befehl des Zylinders ein ERROR zurückgibt, fährt der CycleManager mit einem vordefinierten Error-Schritt fort. Die Methode handle() löst den Alarm aus und setzt automatisch das Error-Flag für diesen Knoten. Danach wartet sie auf die Quittierung des Alarms. Nach der Quittierung wird das Error-Flag gelöscht und der CycleManager kehrt zum letzten Schritt zurück.

Wir verwenden den TwinCAT EventLogger für unser Fehlermanagement und haben einen Mechanismus aufgebaut, der automatisch auf Fehler reagiert. Wenn beispielsweise ein Fehler mit dem Schweregrad „Kritisch“ auftritt, stoppt der Betriebsmodus-Handler die Maschine sofort. Auf dem HMI werden Knoten mit einem aktiven Alarm rot markiert.

Möchten Sie mehr über unser Framework cinnamon erfahren?