IEC 61131-3: Das Command Pattern

Durch den Aufruf einer Methode kann an einem Funktionsblock ein Befehl ausgeführt werden. Funktionsblock A ruft eine Methode von Funktionsblock B auf. So weit so gut. Doch wie lässt sich der Austausch solcher Befehle zwischen mehreren Funktionsblöcken flexibel gestalten? Das Command Pattern liefert hier einen interessanten Ansatz.

Ein kleines Beispiel aus der Heimautomation soll uns hierbei behilflich sein. Angenommen wir haben mehrere FBs die jeweils ein Gerät bzw. Aktor repräsentieren. Jedes Gerät hat einen individuellen Satz an Befehlen, welche unterschiedliche Funktionen zur Verfügung stellen.

Pic01

Ein weiterer Funktionsblock soll ein 8fach-Tastenfeld abbilden. Dieses Tastenfeld enthält 8 Taster, die den einzelnen Funktionalitäten (Befehlen) der Geräte zugeordnet werden. Durch eine positive Flanke an den jeweiligen Eingang werden die notwendigen Befehle bei den Geräten aufgerufen.

FUNCTION_BLOCK PUBLIC FB_SwitchPanel
VAR_INPUT
  arrSwitch           : ARRAY[1..8] OF BOOL;
END_VAR
VAR
  fbLamp              : FB_Lamp;
  fbSocket            : FB_Socket;
  fbAirConditioning   : FB_AirConditioning;
  fbCDPlayer          : FB_CDPlayer;
  arrRtrig            : ARRAY[1..8] OF R_TRIG;
END_VAR

arrRtrig[1](CLK := arrSwitch[1]);
IF arrRtrig[1].Q THEN
  fbSocket.On();
END_IF

arrRtrig[2](CLK := arrSwitch[2]);
IF arrRtrig[2].Q THEN
  fbSocket.Off();
END_IF

arrRtrig[3](CLK := arrSwitch[3]);
IF arrRtrig[3].Q THEN
  fbLamp.SetLevel(100);
END_IF

arrRtrig[4](CLK := arrSwitch[4]);
IF arrRtrig[4].Q THEN
  fbLamp.SetLevel(0);
END_IF

arrRtrig[5](CLK := arrSwitch[5]);
IF arrRtrig[5].Q THEN
  fbAirConditioning.Activate();
  fbAirConditioning.SetTemperature(20.0);
END_IF

arrRtrig[6](CLK := arrSwitch[6]);
IF arrRtrig[6].Q THEN
  fbAirConditioning.Activate();
  fbAirConditioning.SetTemperature(17.5);
END_IF

arrRtrig[7](CLK := arrSwitch[7]);
IF arrRtrig[7].Q THEN
  fbCDPlayer.SetVolume(40);
  fbCDPlayer.SetTrack(1);
  fbCDPlayer.Start();
END_IF

arrRtrig[8](CLK := arrSwitch[8]);
IF arrRtrig[8].Q THEN
  fbCDPlayer.Stop();
END_IF

In Punkto Flexibilität ist dieser Entwurf eher suboptimal. Warum?

Unflexibel. Es besteht eine feste Zuordnung zwischen FB_SwitchPanel und den einzelnen Geräten (FB_Lamp, FB_Socket, FB_CDPlayer und FB_AirConditioning). Soll z.B. FB_Socket durch ein zweites FB_Lamp ersetzt werden, so ist es notwendig die Implementierung von FB_SwitchPanel anzupassen.

Fehlende Wiederverwendbarkeit. Sollen auch die Taster 3 und 4 zum Ansteuern des CD-Players dienen, so muss die notwendige Abfolge von Methodenaufrufen erneut programmiert werden.

Undynamisch. Das 8fach-Tastenfeld wurde als Funktionsblock implementiert. Solange alle Tastenfelder die gleiche Tastenbelegung haben, ist dieser Ansatz lauffähig. Was aber, wenn die Tastenbelegung unterschiedlich sein soll? Entweder müsste für jede Tastenbelegung ein individueller Funktionsblock programmiert werden oder es müssen Programme statt Funktionsblöcke zum Einsatz kommen.

Definition des Command Pattern

Die Lösung des Problems liegt in der Einführung einer Softwareschicht, die sich zwischen dem Tastenfeld und den Geräten einfügt. Diese Schicht kapselt jeden einzelnen Befehl (mit einem Command-FB) und enthält alle relevanten Methodenaufrufe um eine Aktion an einem Gerät auszuführen. Der 8fach-Taster sieht somit nur noch diese Befehle und enthält keine weiteren Referenzen zu den jeweiligen Geräten.

Die genaue Definition des Command Pattern ist in dem Buch (Amazon-Werbelink *) Design Patterns: Entwurfsmuster als Elemente wiederverwendbarer objektorientierter Software von Erich Gamma, Richard Helm, Ralph E. Johnson und John Vlissides zu finden.

Das Command Pattern definiert drei Schichten:

InvokerFBs dieser Schicht lösen an den Command-FBs den gewünschten Befehl aus. Der Aufrufer (Invoker), in unseren Beispiel das 8fach-Tastenfeld (FB_SwitchPanel), kennt nicht die Empfänger der Befehle. Es weiß aber, wie ein Befehl gestartet wird.
ReceiverDieses sind die FBs, die die jeweiligen Empfänger (Receiver) der Befehle repräsentieren; also FB_Socket, FB_Lamp, …
CommandsJeder Befehl wird durch einen FB abgebildet. Dieser FB enthält eine Referenz auf dem Empfänger. Des Weiteren besitzen diese Befehle eine Methode um den Befehl zu aktivieren. Wird diese Methode aufgerufen, so ist dem Command-FB bekannt, welche Methoden am Empfänger ausgeführt werden müssen, um den gewünschten Effekt zu erzielen.

Schauen wir uns die Command-FBs genauer an.

Ein Command-FB kapselt einen “Auftrag”, indem es einen Satz von Aktionen für einen bestimmten Empfänger enthält. Hierzu werden die Aktionen und die Referenz beim Empfänger zu einem FB zusammengefasst. Über eine Methode (z.B. Execute()) sorgt der Command-FB dafür, das am Empfänger die richtigen Aktionen ausgeführt werden. Der Aufrufer sieht von Außen nicht, welche Aktionen das genau sind. Dieser weiß nur, wenn er die Methode Invoke() aufruft, dass alle notwendigen Schritte ausgeführt werden.

Commands

Hier die Implementierung für den Command-FB um an FB_Socket den On-Befehl auszuführen:

FUNCTION_BLOCK PUBLIC FB_SocketOnCommand
VAR
  refSocket : REFERENCE TO FB_Socket;
END_VAR

Die Variable refSocket enthält die Referenz auf eine Instanz des Bausteins FB_Socket, also den Empfänger des Befehls. Gesetzt wird diese Referenz über die Methode FB_init.

METHOD FB_init : BOOL
VAR_INPUT
  bInitRetains  : BOOL;
  bInCopyCode   : BOOL;
  refNewSocket  : REFERENCE TO FB_Socket;
END_VAR

IF (__ISVALIDREF(refNewSocket)) THEN
  THIS^.refSocket REF= refNewSocket;
ELSE
  THIS^.refSocket REF= 0;
END_IF

Die Methode Execute() führt die notwendige Aktion an FB_Socket aus:

METHOD Execute
IF (__ISVALIDREF(THIS^.refSocket)) THEN
  THIS^.refSocket.On();
END_IF

Je nach Empfänger kann diese Methode aber auch aus mehreren Aktionen bestehen. So sind die folgenden Methodenaufrufe notwendig, um das Abspielen einer CD zu starten:

METHOD Execute
IF (__ISVALIDREF(THIS^.refCDPlayer)) THEN
  THIS^.refCDPlayer.SetVolume(40);
  THIS^.refCDPlayer.SetTrack(1);
  THIS^.refCDPlayer.Start();
END_IF

Da in unserem Bespiel alle Command-FBs die Methode Execute() zum Ausführen des Befehls anbieten, wird diese Methode durch das Interface I_Command vereinheitlicht. Jeder Command-FB muss dieses Interface implementieren.

Invoker

Dem 8fach-Tastenfeld (FB_SwitchPanel) muss jetzt nur noch mitgeteilt werden, welche Command-FBs es nutzen soll. Die Details der Command-FBs müssen nicht bekannt sein. FB_SwitchPanel kennt nur 8 Variablen, die vom Typ I_Command sind. Wird eine positive Flanke an einen der Taster erkannt, so wird über das Interface I_Commnd von dem Command-FB die Methode Invoke() aufgerufen.

FUNCTION_BLOCK PUBLIC FB_SwitchPanel
VAR_INPUT
  arrSwitch    : ARRAY[1..8] OF BOOL;
END_VAR
VAR
  aiCommand    : ARRAY[1..8] OF I_Command;
  arrRtrig     : ARRAY[1..8] OF R_TRIG;
  nIndex       : INT;
END_VAR

FOR nIndex := 1 TO 8 DO
  arrRtrig[nIndex](CLK := arrSwitch[nIndex]);
  IF arrRtrig[nIndex].Q THEN
    IF (aiCommand[nIndex] <> 0) THEN
      aiCommand[nIndex].Execute();
    END_IF
  END_IF
END_FOR

Zuvor wird durch die Methode SetCommand() das gewünschte Command-FB den einzelnen Tastern zugeordnet. Dadurch ist FB_SwitchPanel universal einsetzbar.

METHOD PUBLIC SetCommand : BOOL
VAR_INPUT
  nPosition   : INT;
  iCommand    : I_Command;
END_VAR

IF ((nPosition >= 1) AND (nPosition <= 8) AND (iCommand <> 0)) THEN
  THIS^.aiCommand[nPosition] := iCommand;
END_IF

Der Invoker FB_SwitchPanel kennt nicht den Empfänger (Receiver). Es sieht nur 8mal das Interface I_Command mit seiner Methode Execute().

Receiver

An den FBs, die einen Empfänger abbilden, brauchen keine Anpassungen durchgeführt werden. Über entsprechende Methoden oder Eingänge kann ein Command-FB die notwendigen Aktionen ausführen.

Anwendung

Hier ein kleines Beispielprogramm, das aus den drei oben gezeigten Softwareschichten eine Anwendung zusammenstellt:

PROGRAM MAIN
VAR
  // Invoker
  fbSwitchPanel                 : FB_SwitchPanel;

  // Receiver
  fbSocket                      : FB_Socket;
  refSocket                     : REFERENCE TO FB_Socket := fbSocket;
  fbLamp                        : FB_Lamp;
  refLamp                       : REFERENCE TO FB_Lamp := fbLamp;
  fbAirConditioning             : FB_AirConditioning;
  refAirConditioning            : REFERENCE TO FB_AirConditioning := fbAirConditioning;
  fbCDPlayer                    : FB_CDPlayer;
  refCDPlayer                   : REFERENCE TO FB_CDPlayer := fbCDPlayer;

  // Commands
  fbSocketOnCommand             : FB_SocketOnCommand(refSocket);
  fbSocketOffCommand            : FB_SocketOffCommand(refSocket);
  fbLampSetLevel100Command      : FB_LampSetLevelCommand(refLamp, 100);
  fbLampSetLevel0Command        : FB_LampSetLevelCommand(refLamp, 0);
  fbAirConComfortCommand        : FB_AirConComfortCommand(refAirConditioning);
  fbAirConStandbyCommand        : FB_AirConStandbyCommand(refAirConditioning);
  fbMusicPlayCommand            : FB_MusicPlayCommand(refCDPlayer);
  fbMusicStopCommand            : FB_MusicStopCommand(refCDPlayer);

  bInit                         : BOOL;
END_VAR

IF (NOT bInit) THEN
  fbSwitchPanel.SetCommand(1, fbSocketOnCommand);
  fbSwitchPanel.SetCommand(2, fbSocketOffCommand);
  fbSwitchPanel.SetCommand(3, fbLampSetLevel100Command);
  fbSwitchPanel.SetCommand(4, fbLampSetLevel0Command);
  fbSwitchPanel.SetCommand(5, fbAirConComfortCommand);
  fbSwitchPanel.SetCommand(6, fbAirConStandbyCommand);
  fbSwitchPanel.SetCommand(7, fbMusicPlayCommand);
  fbSwitchPanel.SetCommand(8, fbMusicStopCommand);
  bInit := TRUE;
ELSE
  fbSwitchPanel();
END_IF

Pro 8fach-Taster (Invoker) wird eine Instanz von FB_SwitchPanel angelegt.

Von jedem Gerät (Receiver) wird ebenfalls je eine Instanz deklariert. Des Weiteren wird von jeder Instanz noch eine Referenz benötigt.

Die erzeugten Referenzen werden bei der Deklaration der Command-FBs (Commands) mit übergeben (an FB_init). Falls notwendig, können hier noch weitere Parameter übergeben werden. So besitzt das Kommando zum Setzen der Beleuchtung noch einen Parameter für die gewünschte Stellgröße.

In diesem Beispiel können mit der Methode SetCommand() die einzelnen Command-FBs den 8 Tastern zugeordnet werden. Die Methode erwartet als ersten Parameter die Tastennummer (1…8) und als zweiten Parameter einen FB, der das Interface I_Command implementiert.

Die erreichten Vorteile sind recht überzeugend:

Entkopplung. Sender (Invoker) und Empfänger (Receiver) sind voneinander entkoppelt. Dadurch kann FB_SwitchPanel generisch entworfen werden. Durch die Methode SetCommand() besteht die Möglichkeit die Zuordnung der Taster zur Laufzeit anzupassen.

Erweiterbarkeit. Es können beliebige Command-FBs hinzugefügt werden. Selbst wenn FB_SwitchPanel durch eine Bibliothek zur Verfügung gestellt wird, kann ein Programmierer beliebige Command-FBs erstellen und mit FB_SwitchPanel verwenden, ohne das Anpassungen an der Bibliothek notwendig werden. Dadurch, dass die zusätzlichen Command-FBs das Interface I_Command implementieren, können diese von FB_SwitchPanel genutzt werden.

Beispiel 1 (TwinCAT 3.1.4020) auf GitHub

UML-Klassendiagramm

UML

Das Interface I_Command wird von allen Kommandos implementiert.

Der Invoker besitzt über das Interface I_Command Verweise auf Commands und führt diese bei Bedarf aus.

Ein Command ruft alle notwendigen Methoden des Receivers auf.

MAIN stellt die Verbindungen zwischen den einzelnen Commands und dem Receiver her.

Erweiterungen

Das Command Pattern lässt sich sehr einfach um weitere Funktionen erweitern.

Makro-Befehle

Besonders interessant ist die Möglichkeit, die Befehle beliebig miteinander zu kombinieren und in einem neuen Befehlsobjekt zu kapseln. Man spricht hierbei von sogenannten Makro-Befehlen.

Ein Makro-Befehl hält ein Array von Befehlen. In diesem Beispiel können bis zu vier Command-FBs hinterlegt werden. Da jeder Command-FB das Interface I_Command implementiert, können die Befehle in ein Array vom Typ ARRAY[1..4] OF I_Command gespeichert werden.

FUNCTION_BLOCK PUBLIC FB_RoomOffCommand IMPLEMENTS I_Command
VAR
  aiCommands    : ARRAY[1..4] OF I_Command;
END_VAR

Über die Methode FB_init() werden die einzelnen Command-FBs an den Makro-Befehl übergeben.

METHOD FB_init : BOOL
VAR_INPUT
  bInitRetains  : BOOL;
  bInCopyCode   : BOOL;
  iCommand01    : I_Command;
  iCommand02    : I_Command;
  iCommand03    : I_Command;
 iCommand04     : I_Command;
END_VAR

THIS^.aiCommand[1] := 0;
THIS^.aiCommand[2] := 0;
THIS^.aiCommand[3] := 0;
THIS^.aiCommand[4] := 0;
IF (iCommand01 <> 0) THEN
  THIS^.aiCommand[1] := iCommand01;
END_IF
IF (iCommand02 <> 0) THEN
  THIS^.aiCommand[2] := iCommand02;
END_IF
IF (iCommand03 <> 0) THEN
  THIS^.aiCommand[3] := iCommand03;
END_IF
IF (iCommand04 <> 0) THEN
  THIS^.aiCommand[4] := iCommand04;
END_IF

Bei Ausführung der Methode Execute() wird über das Array iteriert und bei jedem Befehl Execute() aufgerufen. Somit können mit einem einzigen Execute() des Makro-Befehls gleich mehrere Befehle auf einmal ausgeführt werden.

METHOD Execute
VAR
  nIndex  : INT;
END_VAR

FOR nIndex := 1 TO 4 DO
  IF (THIS^.aiCommands[nIndex] <> 0) THEN
    THIS^.aiCommands[nIndex].Execute();
  END_IF
END_FOR

In MAIN werden bei der Deklaration des Makro-Befehls die 4 Command-FBs übergeben. Da der Makro-Befehl selbst ein Command-FB ist (er implementiert das Interface I_Command), kann dieser bei dem 8fach-Tasten einem Taster zugeordnet werden.

PROGRAM MAIN
VAR
  ...
  fbRoomOffCommand     : FB_RoomOffCommand(fbSocketOffCommand,
                                           fbLampSetLevel0Command,
                                           fbAirConStandbyCommand,
                                           fbMusicStopCommand);
  ...
END_VAR

IF (NOT bInit) THEN
  ...
  fbSwitchPanel.SetCommand(8, fbRoomOffCommand);
  ...
ELSE
  fbSwitchPanel();
END_IF

Vorstellbar wäre auch eine Methode, welche zur Laufzeit dem Makro-Befehl die einzelnen Command-FBs übergibt. Die Implementierung wäre vergleichbar mit der Methode SetCommand() vom 8fach-Tastenfeld. Somit könnte z.B. ein Szenenbaustein realisiert werden, bei dem der Anwender die Befehle über eine Bedieneroberfläche selbst einer Szene zuordnen kann.

Beispiel 2 (TwinCAT 3.1.4020) auf GitHub

Undo-Funktionalität

Ein weiteres mögliches Feature ist die eine Rückgängigkeits-Funktion. Das 8fach-Tastenfeld erhält einen weiteren Eingang, mit dem der zuletzt ausgeführte Befehl rückgängig gemacht wird. Dazu wird das Interface I_Command um die Methode Undo() erweitert.

UML2

Diese Methode enthält die Umkehrung der Execute-Methode. Wird mit der Execute-Methode die Steckdose eingeschaltet, so wird im gleichen Command-FB mit der Undo-Methode diese wieder ausgeschaltet.

FUNCTION_BLOCK PUBLIC FB_SocketOffCommand IMPLEMENTS I_Command
VAR
  refSocket : REFERENCE TO FB_Socket;
END_VAR

METHOD Execute
IF (__ISVALIDREF(THIS^.refSocket)) THEN
  THIS^.refSocket.Off();
END_IF

METHOD Undo
IF (__ISVALIDREF(THIS^.refSocket)) THEN
  THIS^.refSocket.On();
END_IF

Etwas aufwendiger ist die Implementierung der Undo-Methode für das Setzen der Lampenhelligkeit. In diesen Fall muss der Command-FB um ein “Gedächnis” erweitert werden. Hier wird vor dem Setzen einer neuen Stellgröße, durch die Methode Execute(), die vorherige Stellgröße abgelegt. Die Undo-Methode verwendet beim Aufruf diesen Wert, um die vorherige Stellgröße wiederherzustellen.

FUNCTION_BLOCK PUBLIC FB_LampSetLevelCommand IMPLEMENTS I_Command
VAR
  refLamp        : REFERENCE TO FB_Lamp;
  byNewLevel     : BYTE;
  byLastLevel    : BYTE := 255;
END_VAR

METHOD Execute
IF (__ISVALIDREF(THIS^.refLamp)) THEN
  THIS^.byLastLevel := THIS^.refLamp.Level;
  THIS^.refLamp.SetLevel(THIS^.byNewLevel);
END_IF

METHOD Undo
IF (__ISVALIDREF(THIS^.refLamp)) THEN
  IF (THIS^.byLastLevel <> 255) THEN
    THIS^.refLamp.SetLevel(THIS^.byLastLevel);
  END_IF
END_IF

Nachdem die Command-FBs um eine Undo-Methode erweitert wurden, muss auch das 8fach-Tastenfeld angepasst werden.

FUNCTION_BLOCK PUBLIC FB_SwitchPanel
VAR_INPUT
  bUndo         : BOOL;
  arrSwitch     : ARRAY[1..8] OF BOOL;
END_VAR
VAR
  aiCommand     : ARRAY[1..8] OF I_Command;
  arrRtrig      : ARRAY[1..8] OF R_TRIG;
  iLastCommand  : I_Command;
  fbRtrigUndo   : R_TRIG;
  nIndex        : INT;
END_VAR

FOR nIndex := 1 TO 8 DO
  arrRtrig[nIndex](CLK := arrSwitch[nIndex]);
  IF arrRtrig[nIndex].Q THEN
    IF (aiCommand[nIndex] <> 0) THEN
      aiCommand[nIndex].Execute();
      iLastCommand := aiCommand[nIndex];
    END_IF
  END_IF
END_FOR

fbRtrigUndo(CLK := bUndo);
IF fbRtrigUndo.Q THEN
  IF (iLastCommand <> 0) THEN
    iLastCommand.Undo();
  END_IF
END_IF

In Zeile 19 wird der zuletzt ausgeführte Befehl zwischengespeichert. Bei der Aktivierung der Undo-Funktion wird auf diesen Befehl zurückgegriffen und die Undo-Methode ausgeführt (Zeile 27). Die Details für die Umkehrung eines Befehls sind im Command-FB implementiert. Das 8fach-Tastenfeld greift nur über das Interface I_Command auf die einzelnen Befehle zu.

Beispiel 3 (TwinCAT 3.1.4020) auf GitHub

Befehle protokollieren

Da jeder Command-FB das Interface I_Command implementiert, kann jeder Befehl in eine Variable vom Typ I_Command gespeichert werden. Hiervon wurde z.B. bei der Undo-Funktionalität Gebrauch gemacht. Wird diese Variable durch einen Puffer ersetzt, so erhalten wir die Möglichkeit, Befehle zu protokollieren. Die Analyse und die Diagnose einer Anlage kann dadurch erleichtert werden.

Zusammenfassung

Der zentrale Gedanke des Command Pattern ist das Entkoppeln von Invoker und Receiver durch Befehlsobjekte.

  • Ein Entwickler kann neue Command-FBs hinzufügen, ohne das der Code am Invoker (8fach-Taster) geändert werden muss.
  • Die Zuweisung der Befehle an den Invoker kann dynamisch zur Laufzeit erfolgen.
  • Command-FBs können an verschiedenen Stellen wiederverwendet werden. Coderedundanz wird hierdurch vermieden.
  • Befehle können “intelligent” gemacht werden. Somit lassen sich z.B. Makrobefehle und Undo-Befehle realisieren.

Author: Stefan Henneken

I’m Stefan Henneken, a software developer based in Germany. This blog is just a collection of various articles I want to share, mostly related to Software Development.

6 thoughts on “IEC 61131-3: Das Command Pattern”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: