A command can be run on a function block by calling a method. Function block A calls a method of function block B. So far, so good, but how can such commands be exchanged flexibly between several function blocks. The command pattern provides an interesting approach.
A small example from the home automation should help us at this. Suppose we have several FBs which represent each a device or an actuator. Each device has an individual set of commands which provide different functions.
A further function block should display 8-button keypad. This keypad contains 8 buttons which correspond to single functionalities (commands) of the devices. The necessary commands are called in the devices via positive edge at the corresponding input.
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(CLK := arrSwitch); IF arrRtrig.Q THEN fbSocket.On(); END_IF arrRtrig(CLK := arrSwitch); IF arrRtrig.Q THEN fbSocket.Off(); END_IF arrRtrig(CLK := arrSwitch); IF arrRtrig.Q THEN fbLamp.SetLevel(100); END_IF arrRtrig(CLK := arrSwitch); IF arrRtrig.Q THEN fbLamp.SetLevel(0); END_IF arrRtrig(CLK := arrSwitch); IF arrRtrig.Q THEN fbAirConditioning.Activate(); fbAirConditioning.SetTemperature(20.0); END_IF arrRtrig(CLK := arrSwitch); IF arrRtrig.Q THEN fbAirConditioning.Activate(); fbAirConditioning.SetTemperature(17.5); END_IF arrRtrig(CLK := arrSwitch); IF arrRtrig.Q THEN fbCDPlayer.SetVolume(40); fbCDPlayer.SetTrack(1); fbCDPlayer.Start(); END_IF arrRtrig(CLK := arrSwitch); IF arrRtrig.Q THEN fbCDPlayer.Stop(); END_IF
Regarding flexibility, this outline is rather suboptimal. Why?
Unflexible. There is a fixed assignment between FB_SwitchPanel and the single devices (FB_Lamp, FB_Socket, FB_CDPlayer und FB_AirConditioning). If, for example, FB_Socket has to be replaced with a second FB_Lamp, it is necessary to adapt the implementation of FB_SwitchPanel.
Missing reusability. If the buttons 3 and 4 have to serve for controlling the CD player, the necessary sequence of methods calls has to be programmed anew.
Undynamic. The 8-button keypad was implemented as a function block. As long as all key fields have the same key assignments, this approach is executable. But what if the key assignments have to be different? An individual function block has to be programmed or, instead of function blocks, programs should be used.
Definition of the Command Pattern
The solution of the problem is to introduce a software layer which is inserted between the keypad and the devices. This layer encapsulates each single command (with a command FB) and contains all relevant method calls to perform an action on the device. Thus, the 8-button keypad sees only these commands and contains no further references to the corresponding devices.
You will find the exact definition of the command pattern in the book (Amazon Advertising Link *) Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph E. Johnson and John Vlissides.
The command pattern defines three layers:
|Invoker||FBs of this layer trigger the required command. The invoker, the 8-button keypad FB_SwitchPanel in our example, doesn’t know the command receiver. But it knows how a command is started.|
|Receiver||These are the FBs which represent the corresponding receiver of the commands: FB_Socket, FB_Lamp, …|
|Commands||Each command is represented by a FB. This FB contains a reference to the receiver. Furthermore, these commands have a method to activate the command. If this method is called, the command FB knows, which methods have to be executed on the receiver to achieve the desired effect.|
Let us take a close look at the command FB.
A command FB encapsulates an assignment by containing a set of actions for a certain receiver. For this purpose, the actions and the reference of the receiver are combined into one FB. Via any method (e.g., Execute()), the command FB ensures that the proper actions are executed on the receiver. The invoker doesn’t see from the exterior, which actions these actually are. It only knows, that when it calls the method Invoke(), all required steps are performed.
Below is the implementation for the command FB to run the ON command on the FB_Socket:
FUNCTION_BLOCK PUBLIC FB_SocketOnCommand VAR refSocket : REFERENCE TO FB_Socket; END_VAR
The variable refSocket contains the reference to the instance of the block FB_Socket, i.e. the receiver of the command. The reference is set via the method 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
The method Execute() runs the required action on FB_Socket:
METHOD Execute IF (__ISVALIDREF(THIS^.refSocket)) THEN THIS^.refSocket.On(); END_IF
Depending on the receiver, this method can be also composed of several actions. Thus, the following method calls are necessary to begin playing a CD:
METHOD Execute IF (__ISVALIDREF(THIS^.refCDPlayer)) THEN THIS^.refCDPlayer.SetVolume(40); THIS^.refCDPlayer.SetTrack(1); THIS^.refCDPlayer.Start(); END_IF
Since all command FBs in our example provide the method Execute() to run the command, this method is standardised by the interface I_Command. Each command FB has to implement this interface.
The 8-button keypad (FB_SwitchPanel) should only get the information, which command FBs have to be used. The details of the command FBs doesn’t have to be known. FB_SwitchPanel knows only 8 variables that are of type I_Command. If a positive edge on a button is detected, the command FB calls the method Invoke() via the interface I_Command.
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
Prior to this, the required command FB is assigned to the single buttons using the method SetCommand(). Thus, FB_SwitchPanel is universally applicable.
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
The invoker FB_SwitchPanel doesn’t know the receiver. It sees only the interface I_Command with its method Execute() 8 times.
The FBs which map a receiver doesn’t have to be adapted. A command FB can execute the required actions with the help of the corresponding methods or inputs.
Below is a small example of a program, which combines an application from the three software layers shown above:
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
An instance of FB_SwitchPanel is created for 8 keys (invoker).
Likewise, an instance is declared per each device (receiver). Moreover, a reference of each instance is required.
When declaring the command FBs, the created references are passed to FB_init. If necessary, further parameters can be also transferred here. Thus, the command to set lighting has a parameter for the control value variable.
In this example, the single command FBs can be assigned to the 8 buttons with the method SetCommand(). The method expects the key numbers (1…8) as a first parameter, and an FB, which implements the interface I_Command, as a second parameter.
The resulting benefits are quite convincing:
Decoupling. Invoker and receiver are decoupled from each other. As a consequence, FB_SwitchPanel can be designed generically. The method SetCommand() provides possibility to adapt the assignment of the keys during runtime.
Expandability. Any command FB can be added. Even if FB_SwitchPanel is provided by a library, a programmer can define any command FB and use it with FB_SwitchPanel, without the need for adapting the library. Because the additional command FBs implement the interface I_Command, they can be used by FB_SwitchPanel.
UML class diagram
All the commandos implement the interface I_Command.
The invoker has references to the commands via the interface I_Command and executes these commands if required.
One command calls all the required methods of the receiver.
MAIN sets out a link between the single commands and the receiver.
The command pattern can be very easily expanded with further functions.
Especially interesting is the possibility to combine any commands with one another and encapsulate in a command object. One speaks here of so-called macro commands.
A macro command has an array of commands. Up to four command FBs can be defined in this example. Since each command FB implements the interface I_Command, the commands can be stored in an array of type ARRAY [1…4] OF I_Command.
FUNCTION_BLOCK PUBLIC FB_RoomOffCommand IMPLEMENTS I_Command VAR aiCommands : ARRAY [1..4] OF I_Command; END_VAR
The single command FBs are piped to the macro command via the method FB_init().
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 := 0; THIS^.aiCommand := 0; THIS^.aiCommand := 0; THIS^.aiCommand := 0; IF (iCommand01 <> 0) THEN THIS^.aiCommand := iCommand01; END_IF IF (iCommand02 <> 0) THEN THIS^.aiCommand := iCommand02; END_IF IF (iCommand03 <> 0) THEN THIS^.aiCommand := iCommand03; END_IF IF (iCommand04 <> 0) THEN THIS^.aiCommand := iCommand04; END_IF
When executing the method Execute(), iteration over the array is performed and Execute() is called for each command. Thus, several commands can be executed all at once with the single Execute() of the macro command.
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
When declaring the macro command, the four command FBs are passed in MAIN. Since the macro command is a command FB itself (it implements the interface I_Command), it can be assigned to a button in the 8-button keypad.
PROGRAM MAIN VAR ... fbRoomOffCommand : FB_RoomOffCommand(fbSocketOffCommand, fbLampSetLevel0Command, fbAirConStandbyCommand, fbMusicStopCommand); ... END_VAR IF (NOT bInit) THEN ... fbSwitchPanel.SetCommand(8, fbRoomOffCommand); ... ELSE fbSwitchPanel(); END_IF
Another option would be a method which passes single command FBs to the macro command. The implementation would be comparable with the method SetCommand() of the 8-button keypad. Thus, a scene controller could be implemented, with which the user can himself allocate the commands to a scene over an interactive user interface.
A further possible feature is a cancellation function. The 8-button keypad gets a further input which undoes the last executed command. For this purpose, the interface I_Command is extended by the method Undo().
This method contains the inversion of the execute method. If a socket is switched on with the execute method, it is switched off again in the same command FB with the undo method.
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
The implementation of the undo method for setting the lamp brightness is somewhat more complex. In this case, the command FB has to be expanded with a memory. Before setting a new regulating variable via the method Execute(), the previous regulating variable is stored. When calling, the undo method uses this value to restore the previous regulating variable.
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
After the command FBs were expanded with a undo method, the 8-button keypad must also be adjusted.
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 line 19, the last executed command is stored temporarily. When activating the undo function, this command can be used and the undo method is executed (line 27). The details of the inversion of a command are implemented in the command FB. The 8-button keypad refers to the single commands only via the interface I_Command.
Logging of commands
Since each command FB implements the interface I_Command, each command can be stored in a variable of type I_Command. For example, the undo functionality would make use of it. If this variable is replaced with a buffer, we get an opportunity to log the commands. The analysis and the diagnosis of an equipment can be facilitated in this way.
The central idea of the command pattern is decoupling of the invoker and the receiver by means of command objects.
• A developer can add new command FBs without adapting the code of the invoker (8-button keypad).
• The assignment of the commands to the invoker can be performed dynamically during runtime.
• Command FBs can be reused at various points. Thus, code redundancy is prevented.
• Commands can be made “intelligent”. In this way, macro commands and undo commands can be implemented.