Bild in lightbox öffnen

How To cinnamon Teil 2: CycleManager

1 Einleitung

Speicherprogrammierbare Steuerungen (SPSen) basieren grundsätzlich auf Zustandsmaschinen und schrittweisen, sequenziellen Anweisungen. In der industriellen Automatisierung ist es deshalb für ein vorhersehbares Verhalten und wartbare Systeme unerlässlich, diese Abläufe klar und deterministisch darzustellen.

cinnamon setzt bewusst auf ein rein textbasiertes Programmiermodell nach dem IEC-Standard „Structured Text“ und verzichtet auf grafische Notationen wie das Sequential Function Chart (SFC). Textbasierter Code lässt sich leichter überprüfen, vergleichen und zusammenführen. Er passt zu modernen Entwicklungspraktiken und ermöglicht eine unkomplizierte CI/CD-Automatisierung ohne die Kompatibilitätsprobleme, die bei diagrammbasierten Artefakten auftreten können.

Das Herzstück von cinnamon sind die CycleManager. Sie koordinieren die Ablaufsteuerung, Übergänge und die Zustandsverwaltung, sodass du Schrittfolgen und Prozessabläufe mit minimalem Boilerplate-Code implementieren kannst. Anstatt die Steuerungslogik manuell zu programmieren, definierst du die Absicht und lässt die CycleManager die Ausführung zuverlässig von Zustand zu Zustand steuern.

CycleManager lassen sich sowohl auf Schritt- als auch auf Sequenzebene konfigurieren. So können Teams das Timing, die Übergangskriterien, die Fehlerbehandlung und das Wiederherstellungsverhalten pro Schritt feinabstimmen und gleichzeitig die allgemeine Ausführungsstrategie für den gesamten Prozess festlegen.

Zustände und Ergebnisse

Die Bibliothek CNM_ReturnTypes definiert zwei Standardtypen für Ein- und Mehrzyklus-Methoden/Zustandsmaschinen/Schrittketten, die auf PLCopen-konforme Funktionsblöcke abgestimmt sind. SingleExecutionState wird für flankengesteuerte Mehrzyklus-Operationen verwendet, während SingleExecutionResult für Einzyklus-Ausführungen gilt. Die gemeinsamen Felder ERROR, SUCCESS und ABORTED verwenden bewusst dieselben numerischen Werte. Da die beiden Enums nicht streng sind, sind sie durch diesen Aufbau bedingt kompatibel.

TYPE SingleExecutionResult :
(
	(* Ausführung wurde mit einem Fehler beendet *)
	ERROR	:= -1,
	(* Ausführung wurde erfolgreich abgeschlossen *)
	SUCCESS	:= 0,
	(* Ausführung wurde abgebrochen *) 
	ABORTED	:= 3 
)INT;
END_TYPE

TYPE SingleExecutionState :
(
	(* Ausführung wurde mit einem Fehler beendet *)
	ERROR	:= -1,
	(* Ausführung wurde erfolgreich abgeschlossen *)
	SUCCESS	:= 0,
	(* Keine Ausführung *) 
	IDLE	:= 1,
	(* Ausführung läuft noch *)
	BUSY	:= 2,
	(* Ausführung wurde abgebrochen *) 
	ABORTED	:= 3,
	(* Ausführung wurde angehalten *)
	PAUSED	:= 4
)INT;
END_TYPE

Dementsprechend definiert dieselbe Bibliothek auch die DefaultSteps für Zustandsmaschinen, die cinnamon-CycleManager standardmäßig implementieren und aus denen sie ihren aktuellen Zustand ableiten:

DefaultSteps

VAR_GLOBAL CONSTANT
	(* Schritt für: Ausführung der Zustandsmaschine wurde abgebrochen *)
	ABORT	:DINT := 16#8000_0000;
	(* Schritt für: Zustandsmaschine befindet sich im Fehlerzustand *)
	ERROR	:DINT := -1;
	(* Schritt für: Zustandsmaschine befindet sich im Ruhezustand/Leerlauf *)
	IDLE	:DINT := 0;
	(* erster Ausführungsschritt der Zustandsmaschine *)
	INIT	:DINT := 1;
	(* Schritt für: Zustandsmaschine kann angehalten werden *) 
	PAUSE	:DINT := 16#7FFF_FFFE;
	(* Ausführung der Zustandsmaschine war erfolgreich *)
	SUCCESS	:DINT := 16#7FFF_FFFF;
END_VAR

2 Verwendung

cinnamon bietet zwei Arten von CycleManagern. Der SimpleCycleManager kann überall im Code unabhängig vom Rest des Frameworks verwendet werden, erfordert jedoch eine zyklische Zuweisung von Steuersignalen (wie z. B. Stoppbefehl oder Schrittbetrieb, falls erforderlich). Er befindet sich in der Bibliothek CNM_CycleManager. Der NodeCycleManager hingegen benötigt zur Initialisierung einen Node, kann aber Signale wie Stoppanforderung oder Step-Befehl selbstständig vom Node abfragen und reduziert so die zyklische Signalzuweisung. Er befindet sich in der Bibliothek CNM_OpmodeHandling und ist die empfohlene Wahl für Modus-Schritt-Sequenzen.

Beide CycleManager müssen zyklisch mit einem execute-Eingang aufgerufen werden; ein pause-Eingang ist vorhanden, aber optional. Es ist entscheidend, dass sie genau einmal pro Zyklus aufgerufen werden, da viele interne Funktionen auf flankenbasiertem Verhalten beruhen und bei mehr als einem Aufruf Fehlfunktionen verursachen.

Eine steigende Flanke am execute-Eingang startet die Sequenz: Der Schritt wechselt von IDLE zu INIT (Schritt 1) und cycleManager.state wechselt zu BUSY. Eine fallende Flanke am execute-Eingang bewirkt, dass cycleManager.state auf ABORTED wechselt, wenn die Sequenz noch nicht den Zustand SUCCESS erreicht hat – in diesem Fall wird auch der aktuelle Schritt für einen Zyklus auf ABORTED gesetzt. Wenn die Sequenz bereits erfolgreich abgeschlossen wurde, bleibt cycleManager.state auf SUCCESS und der Schritt kehrt zu IDLE zurück.

Eine steigende Flanke am pause-Eingang versetzt den aktuell aktiven Schritt (cycleManager.step.current) in den Zustand PAUSE, während die Ausführung aktiv ist. Eine fallende Flanke am pause-Eingang löst einen Übergang zurück zum letzten aktiven Schritt aus, wodurch die Ausführung effektiv an der Stelle fortgesetzt wird, an der sie unterbrochen wurde.

2.1 Deklaration

Der SimpleCycleManager wird als einfache Instanz ohne Konstruktorargumente deklariert:

cycleManager :CNM_CycleManager.SimpleCycleManager();

Der NodeCycleManager benötigt einen Schnittstellenzeiger auf die zugehörige Node-Instanz – typischerweise THIS^, wobei THIS auf die aktuelle Instanz einer von AbstractNode abgeleiteten Klasse verweist:

cycleManager :CNM_OpmodeHandling.NodeCycleManager(THIS^);

INPUTs für SimpleCycleManager

FUNCTION_BLOCK SimpleCycleManager EXTENDS AbstractCycleManager
VAR_INPUT
	(* Steuerbit zum Starten oder Abbrechen des Zyklusmanagers *)
	execute :BOOL;
	(* wird bei der Auswertung von StopRequest verwendet, sofern konfiguriert *)
	stopRequest :BOOL := FALSE;
	(* Pause auslösen (vorausgesetzt, Pause aktivieren ist aktiviert) *)
	pause :BOOL := FALSE;
	(* wenn wahr, wird der Fehler bestätigt und zum vorherigen Schritt zurückgekehrt *)
	resume :BOOL := FALSE;
	(* muss TRUE sein, wenn das Stepping aktiviert ist, damit die Schritte automatisch ausgeführt werden *)
	stepControl :BOOL := TRUE;
END_VAR

INPUTs für NodeCycleManager

FUNCTION_BLOCK NodeCycleManager EXTENDS CNM_CycleManager.AbstractCycleManager
VAR_INPUT
	(* Steuerbit zum Starten oder Abbrechen des Zyklusmanagers *)
	execute :BOOL;
	(* Pause auslösen (vorausgesetzt, Pause aktivieren ist aktiviert) *)
	pause :BOOL := FALSE;
	(* wenn wahr, wird der Fehler bestätigt und zum vorherigen Schritt zurückgekehrt *)
	resume :BOOL := FALSE;
END_VAR

2.2 Übergänge

CycleManager bieten übersichtliche Übergangsprimitive, sodass du Schritte oder Zustände nicht manuell verwalten musst. Die Abläufe bleiben schlank und konzentrieren sich auf das Wesentliche.

  • proceed() beendet den aktuellen Schritt und springt bedingungslos zum nächsten (standardmäßig current + 1).

  • proceedWith(step) beendet den aktuellen Schritt und springt bedingungslos zum angegebenen Schritt für Verzweigung.

  • evaluate(state) prüft einen SingleExecutionState: Bei SUCCESS springt die Sequenz zum nächsten Schritt; bei ERROR wechselt sie zum ERROR-Schritt (oder zu einem angegebenen Fehlerschritt mit evaluate(state, errorStep)). Solange der Zustand BUSY, IDLE oder ABORTED ist, bleibt die Sequenz im aktuellen Schritt.

    Hinweis: evaluate() ignoriert den Zustand im ersten Zyklus, in dem es aufgerufen wird, da SingleExecutionState-Maschinen den letzten Zustand aus einer vorherigen Ausführung beibehalten können. Um eine SingleExecutionState-Maschine zurückzusetzen und neu zu starten, kann das boolesche Flag cycleManager.executeStep verwendet werden, das im ersten Zyklus eines Schritts FALSE ist und danach bis zum Ende des Schritts TRUE bleibt. Nutze es, um verschachtelte Schrittketten zu steuern, zum Beispiel:

    cycleManager.evaluate(state := pusher.extend(execute := cycleManager.executeStep));.

  • waitFor(value) springt zum nächsten Schritt, sobald der boolesche Eingabewert TRUE wird; andernfalls bleibt die Kette im aktuellen Schritt.

  • executeCommand(ICommand) führt einen Befehl aus und interpretiert dessen Rückgabestatus. Im ersten Zyklus wird der Befehl mit execute := FALSE aufgerufen (z. B. für Vorabprüfungen). In den folgenden Zyklen wird er mit execute := TRUE aufgerufen. Die ICommand-Schnittstelle stellt die Methode executeCommand(execute) : SingleExecutionState bereit, welche der CycleManager mit der beschriebenen Kantenbehandlung aufruft.

  • Außerdem stehen zwei Helper für einzelne Zyklen zur Verfügung: enter(ISingleAttempt) wird nur im ersten Zyklus eines Schritts ausgeführt, und leave(ISingleAttempt) wird einmalig ausgeführt, sobald die Übergangsbedingungen zum nächsten Schritt erfüllt sind. ISingleAttempt stellt die Methode invoke() : SingleExecutionResult bereit, welche der CycleManager zum richtigen Zeitpunkt ausführt.

  • Für Fehlerszenarien stehen zwei spezielle Methoden zur Verfügung: Mit acknowledge( ) kannst du von einem Fehlerschritt zurück zum vorherigen Schritt springen, allerdings nur, wenn dieser Schritt über einen Fehlerübergang erreicht wurde. handle(IMessage) wurde bereits in Teil 1 erläutert. Die Methode löst einen Fehler aus, wartet, bis dieser behoben ist, und kann dann entweder zum vorherigen Schritt zurückkehren – wenn der Schritt über einen Fehler mit handle erreicht wurde – oder zum nächsten Schritt übergehen, wenn der optionale Parameter resumeWithLastStep auf false gesetzt ist.

Du kannst diese Methoden innerhalb eines Schritts beliebig kombinieren; ein Schrittwechsel findet erst statt, wenn alle verwendeten Methoden den Status SUCCESS melden.

Wenn sie im selben Schritt verwendet werden, sollte die Reihenfolge der Methodenaufrufe wie folgt sein:

  • configurations

  • enter

  • executeCommand

  • evaluate

  • waitFor

  • proceed

  • proceedWith

  • acknowledge

  • leave

2.3 Allgemeine Eigenschaften

  • cycleManager.step.current ist eine schreibgeschützte Eigenschaft, die den aktiven Schritt der Ausführung zurückgibt.

  • cycleManager.step.last ist eine schreibgeschützte Eigenschaft, die den vorherigen Schritt zurückgibt.

  • cycleManager.step.next kann verwendet werden, um den nächsten Schritt festzulegen, nachdem der aktuelle erfolgreich abgeschlossen wurde.

  • cycleManager.executeStep ist eine boolesche Eigenschaft, die im ersten Zyklus eines neuen Schritts den Wert false hat und dann bis zum nächsten Schrittwechsel den Wert true beibehält. Sie ist dazu gedacht, kantenausgelöste Operationen innerhalb eines Schritts zu starten.

  • cycleManager.errors.isInErrorStep ist true, wenn der aktuelle Schritt durch die Auswertung eines Fehlers erreicht wurde.

  • cycleManager.state gibt den aktuellen Zustand der Ausführung zurück.

2.4 Konfiguration

cycleManager.configuration bietet eine Fluent API, die die Auffindbarkeit und die IntelliSense-Unterstützung verbessert.

Du kannst entweder einen einzelnen Schritt oder die gesamte Abfolge konfigurieren:

  • cycleManager.configuration.step gilt nur für den aktuellen Schritt und wird zurückgesetzt, wenn der Schritt wechselt.

  • cycleManager.configuration.sequence gilt für alle Schritte, kann aber durch Einstellungen auf Schritt-Ebene überschrieben werden.

Die folgenden Optionen stehen sowohl für die Sequenz als auch für den Schritt zur Verfügung:

  • stepWidth legt die Schrittweite für den nächsten Schritt fest, wenn keine angegeben ist. Der Standardwert ist 1.

  • errorStep definiert den Schritt, zu dem gesprungen wird, wenn ein ERROR-Zustand auftritt.

  • timeout begrenzt die maximale Ausführungszeit eines Schritts. Wenn diese abgelaufen ist, springt der CycleManager zu timeoutStep.

  • timeoutStep legt den Schritt fest, zu dem bei Ablauf der Zeit gesprungen wird.

  • pause.enable() ermöglicht das Anhalten. pause.disable() verhindert das Anhalten (zum Beispiel bei nicht umkehrbaren Prozessen).

  • stepping.enable() aktiviert das schrittweise Voranschreiten, sodass der CycleManager wartet, bis stepControl TRUE ist, bevor er die Schritte wechselt. stepping.disable() ermöglicht automatische Schrittwechsel.

  • stopRequest legt fest, wie sich das System verhält, wenn der stopRequest-Eingang auf TRUE gesetzt wird:

    • stopRequest.ignore() setzt die Ausführung der Sequenz fort.

    • stopRequest.immediate() bricht die Sequenz sofort ab, indem der aktuelle Schritt auf SUCCESS gesetzt wird.

    • stopRequest.afterSuccessfulStep() wartet, bis der aktuelle Schritt abgeschlossen ist, und fährt dann mit SUCCESS fort, anstatt zum nächsten Schritt überzugehen.

    • stopRequest.onBusy() beendet die Sequenz, wenn der aktuelle Schritt noch nicht erfolgreich war (zum Beispiel während des Wartens auf einen Handshake mit einer anderen Einheit). Es wird nicht gestoppt, wenn der Handshake erfolgreich ist.

  • Die folgende Option gilt nur für die Sequenzkonfiguration: requireSuccessStep ist ein boolescher Wert. Ist er auf TRUE gesetzt, garantiert der CycleManager die Ausführung von Operationen innerhalb des SUCCESS-Schritts. Der Status des CycleManagers wechselt nur dann zu SUCCESS, wenn der SUCCESS-Schritt erfolgreich ist, zum Beispiel mit proceed oder evaluate(SUCCESS).

2.5 Beispiele

Mit dem CycleManager mehrere Befehle parallel ausführen

METHOD moveToPos : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
	(* eine steigende Flanke löst die Ausführung aus *)
	execute :BOOL;
	(* der Index der Zielposition xyzr *)
	position :SupplyPortalPositions;
END_VAR
VAR_INST
	(* Wir müssen eine Instanz des CycleManager deklarieren, da zyklische Knoten keinen nodeCycleManager haben (im Gegensatz zu den ModeNodes). *)
	cycleManager :CNM_CycleManager.SimpleCycleManager;
END_VAR

cycleManager(execute := execute);
CASE cycleManager.step.current OF
CNM_ReturnTypes.DefaultSteps.INIT:
	cycleManager.step.next := CNM_ReturnTypes.DefaultSteps.SUCCESS;
	(* Der cycleManager bleibt in diesem Schritt, bis alle Befehle den Status SUCCESS zurückgeben oder mindestens ein Befehl den Status ERROR zurückgibt.
          Sobald alle Befehle ausgeführt sind, geht es automatisch zum nächsten Schritt über. *)
	cycleManager.executeCommand( THIS^.driveX.commands.moveAbsolute(THIS^.settings.portalPositions[position].X ));
	cycleManager.executeCommand( THIS^.driveY.commands.moveAbsolute(THIS^.settings.portalPositions[position].Y ));
	cycleManager.executeCommand( THIS^.driveZ.commands.moveAbsolute(THIS^.settings.portalPositions[position].Z ));
	cycleManager.executeCommand( THIS^.driveR.commands.moveAbsolute(THIS^.settings.portalPositions[position].Rot ));
END_CASE

moveToPos := cycleManager.state;

Mit enter() und leave() einen Motor für eine bestimmte Zeit laufen lassen

METHOD runBrushing : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
	execute :BOOL;
END_VAR
VAR_INST
	cycleManager :CNM_CycleManager.SimpleCycleManager;
	brushTimer :CNM_CycleManager.WaitCommand;
END_VAR
VAR CONSTANT	
	START_BRUSHING :DINT := CNM_ReturnTypes.DefaultSteps.INIT + 1;
END_VAR

cycleManager(execute := execute);
CASE cycleManager.step.current OF
CNM_ReturnTypes.DefaultSteps.INIT:
    (* Lass den Motor nur laufen, wenn ein Teil vorhanden ist, sonst nimm den schnellsten Weg zu SUCCESS *)
	cycleManager.step.next := SEL(
		THIS^.glazingSensor.occupied,
		CNM_ReturnTypes.DefaultSteps.SUCCESS,
		START_BRUSHING
	);
	cycleManager.proceed();
START_BRUSHING:
    cycleManager.step.next := CNM_ReturnTypes.DefaultSteps.SUCCESS;
	cycleManager.enter(THIS^.creamBrusher.attempts.start);
	brushTimer.waitTime := T#3S;
	cycleManager.executeCommand(brushTimer);
	cycleManager.leave(THIS^.creamBrusher.attempts.stop);
END_CASE

runBrushing := cycleManager.state;

Eine Schleifenfolge mit Datenverarbeitung, Aufbereitung und Handshakes

METHOD runOven :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
	execute :BOOL;
END_VAR
VAR_INST
	cycleManager :CNM_OpModeHandling.NodeCycleManager(THIS^);
	ovenTimer :CNM_CupcakeDevices.WaitCommand;
END_VAR
VAR CONSTANT
	CHECK_SAUCER_UNLOADED :DINT := CNM_ReturnTypes.DefaultSteps.INIT + 1;
	REQUEST_LOADING :DINT := CHECK_SAUCER_UNLOADED + 1;
	WAIT_SAUCER_LOADED :DINT := REQUEST_LOADING +1;
	OPEN_OVEN :DINT := WAIT_SAUCER_LOADED + 1;
	MOVE_TO_OVEN :DINT := OPEN_OVEN + 1;
	CLOSE_OVEN :DINT := MOVE_TO_OVEN + 1;
	WAIT_OVEN :DINT := CLOSE_OVEN + 1;
	OPEN_OVEN_MOVEOUT :DINT := WAIT_OVEN + 1;
	MOVE_TO_LOADING :DINT := OPEN_OVEN_MOVEOUT + 1;
	REQUEST_UNLOAD :DINT := MOVE_TO_LOADING + 1;
END_VAR

cycleManager(execute := execute);
CASE cycleManager.step.current OF
CNM_ReturnTypes.DefaultSteps.INIT:
	cycleManager.configuration.sequence.stepping.enable();
	cycleManager.proceed();
CHECK_SAUCER_UNLOADED:
	cycleManager.configuration.step.stopRequest.afterSuccessfulStep();
	cycleManager.step.next := SEL(THIS^.dropOffSensor.free, OPEN_OVEN, REQUEST_LOADING);
	cycleManager.proceed();
REQUEST_LOADING:
	cycleManager.evaluate(THIS^.cupcakeSupply.requestDrop(execute := cycleManager.executeStep));
WAIT_SAUCER_LOADED:
	cycleManager.configuration.step.stopRequest.immediate();
	IF THIS^.dropOffSensor.occupied THEN
		cycleManager.proceed();
	END_IF
OPEN_OVEN:
	cycleManager.configuration.step.stopRequest.afterSuccessfulStep();
	cycleManager.executeCommand( THIS^.ovenDoor.commands.Extend );
MOVE_TO_OVEN:
	cycleManager.executeCommand(THIS^.flyingSaucer.commands.moveAbsolute(THIS^.settings.saucerPositions[BakingSaucerPosition.OVEN]));
CLOSE_OVEN:
	cycleManager.executeCommand( THIS^.ovenDoor.commands.Retract );
WAIT_OVEN:
	ovenTimer.waitTime := T#5S;
	cycleManager.executeCommand(ovenTimer);
OPEN_OVEN_MOVEOUT:
	cycleManager.executeCommand( THIS^.ovenDoor.commands.Extend );
MOVE_TO_LOADING:
	cycleManager.configuration.step.stopRequest.afterSuccessfulStep();
	cycleManager.executeCommand(THIS^.flyingSaucer.commands.moveAbsolute(THIS^.settings.saucerPositions[BakingSaucerPosition.HOME]));
REQUEST_UNLOAD:
	cycleManager.configuration.step.stopRequest.afterSuccessfulStep();
	(* Indem wir den nächsten Schritt so festlegen, erzeugen wir eine sich wiederholende Abfolge, bis ein Stoppbefehl ausgelöst wird. *)
	cycleManager.step.next := CHECK_SAUCER_UNLOADED;
	cycleManager.evaluate(THIS^.requestPickup(cycleManager.executeStep));
END_CASE

runOven := cycleManager.state;

cinnamon ist verfügbar auf GitHub