It was already available in CODESYS, but with the release of TwinCAT 4024 it’s now available in TwinCAT as well: the ABSTRACT keyword. Abstraction and the use of the abstract keyword is common practice in OOP and many higher level languages as C# support this. It’s often considered as the fourth pillar of OOP. In this post i’ll explain how to use the ABSTRACT keyword in TwinCAT with some practical examples.
Why do we need abstraction
To understand why abstraction is of such an importance in OOP let’s quickly go back to the definition of abstraction. Abstraction is about hiding unnecessary implementation details from the user and focus on functionality.
Consider a function block which implements a basic load cell functionality. To use this all we need to know is that it needs an raw input signal and scale factor, and it will provide us with an output value in Newton. We don’t need to know how the output value is converted, filtered and scaled. Let someone else bother about that. It’s not of influence on our program. We will just work with a simple interface of a load cell.
It is good to know that using abstractions is closely related to the dependency inversion principle one of the SOLID principles. This becomes especially imported when you start working unit tests. In future posts we will go deeper into this subject.
The ABSTRACT keyword
The new ABTRACT keyword in TwinCAT helps to create an abstract function block, method or property. A function block can be made abstract with the following syntax:
FUNCTION_BLOCK ABSTRACT AbstractLoadCell
The abstract function block may, or may not have abstract properties and methods. Where you define how the method interface will look like. But you do not write the concrete implementation. For example:
METHOD PUBLIC ABSTRACT GetActualValue : LREAL VAR_INPUT END_VAR
Alright sounds good.. But didn’t already have such a feature called an interface ? Yes. And they are closely related. However, contrary to an interface an abstract function block can contain concrete methods and properties! Abstract function blocks can of course still implement an interface.
Now to make an concrete implementation out of an abstraction you create another function blocks which inherits the abstract function block:
FUNCTION_BLOCK PUBLIC ConcreteLoadCell EXTENDS AbstractLoadCell
For more info on inheritance in TwinCAT have look at this post. Notice that our new function block ‘ConcreteLoadCell ‘ must implement the method GetActualValue defined in ‘AbstractLoadCell’.
To use a concrete function block, without depending on it, you could use a reference to the abstract function block. For example:
PROGRAM MAIN VAR_INPUT fbLoadCell:REFERENCE TO AbstractLoadCell; END_VAR
The abstract kewords comes with some constrains:
- Abstract function blocks cannot be instantiated.
- Abstract properties or methods have no implementation.
- Concrete function blocks inherited from an abstraction must implement the all abstract methods and properties of its base or it must also be defined as abstract.
For the full list of rules regarding the ABSTRACT keyword please refer to the TwinCAT infosys.
A real life example:
Now we know the theory let’s make a real life example with the ABSTRACT keyword. Consider two event logger function blocks with the followings functionalities:
Logger1: ADS Logger
- Has a log message method which logs an given string (input).
- Gets current system time and date.
- Adds system time and date string to the input string.
- Writes combined output to the ADS.
- Has a get only busy property.
Logger2: CSV Logger
- Has a log message method which logs an given string (input).
- Gets current system time and date.
- Adds system time and date string to the input string.
- Writes combined output to a CSV file.
- Has a get only busy property.
We notice of course immediately that part of the loggers functionality is the same. To keep our code DRY we will catch this functionality in an abstract class and write two inherited concrete loggers with only their specific logging functionality. So we can redivide the functionalities as follows:
Abstract logger:
- Has a log message method which logs an given string (input).
- Gets current system time and date.
- Adds system time and date string to the input string.
- Calls the abstract internal log method with the combined string.
- Has a get only busy property.
Logger1: ADS Logger inherits from abstract logger
- Implements a internal log method which writes a string to the ADS
Logger2: CSV Logger inherits from abstract logger
- Implements a internal log method which writes a string to a CSV file.
Example implementation:
In ST the abstract logger function block could look as follows:
FUNCTION_BLOCK ABSTRACT AbstractLogger VAR _Busy:BOOL; //Backing variable for Busy property. END_VAR
{attribute 'monitoring' := 'variable'} PROPERTY PUBLIC Busy : BOOL
VAR END_VAR
Busy:=_Busy;
METHOD PROTECTED ABSTRACT InternalLog VAR_INPUT Message:STRING; END_VAR
METHOD PUBLIC LogMessage VAR_INPUT Message:string; END_VAR VAR GetSystemTime : NT_GetTime:=(NetID:='xxx.xxx.xxx.1.1.1'); END_VAR
// Retrieve a the current system time and combine it with the input message. GetSystemTime(START:=FALSE); REPEAT GetSystemTime(START:=TRUE); UNTIL (NOT GetSystemTime.BUSY) END_REPEAT //Call abstract method InternalLog(Message:=CONCAT(CONCAT(Message,', '),SYSTEMTIME_TO_STRING(GetSystemTime.TIMESTR)));
On this base functionality we can write our concrete loggers which MUST implement the protected abstract method ‘InternalLog’. The ADS logger is the most straight forward:
FUNCTION_BLOCK PUBLIC FINAL ConcreteADSLogger EXTENDS AbstractLogger
METHOD PROTECTED InternalLog VAR_INPUT Message:STRING; END_VAR
_Busy:=TRUE; ADSLOGSTR( msgCtrlMask:=ADSLOG_MSGTYPE_LOG, msgFmtStr:=Message, strArg:=''); _Busy:=FALSE;
The CSV logger takes a little bit more code. As not all steps can be handled in one cycle, it uses a state machine to go cyclic through the steps:
{attribute 'hide_all_locals'} FUNCTION_BLOCK FINAL ConcreteCSVLogger EXTENDS AbstractLogger VAR fbFileOpen:FB_FileOpen; fbFilePuts:FB_FilePuts; fbFileClose:FB_FileClose; State:INT; Message:STRING; END_VAR
CASE State OF 0: IF _Busy THEN State:=State+1; END_IF 1: fbFileOpen(bExecute:=FALSE); fbFileOpen(sNetId:='xxx.xx.xxx.1.1.1', sPathName:='C:\Log.csv', nMode:=FOPEN_MODEAPPEND, bExecute:=TRUE); State:=State+1; 2: fbFileOpen(bExecute:=FALSE); IF NOT fbFileOpen.bBusy THEN State:=State+1; END_IF 3: fbFilePuts(bExecute :=FALSE); fbFilePuts( bExecute:=TRUE, sNetId:='xxx.xx.xxx.1.1.1', hFile:=fbFileOpen.hFile, sLine:=Message); State:=State+1; 4: fbFilePuts(bExecute:=FALSE); IF NOT fbFilePuts.bBusy THEN State:=State+1; END_IF 5: fbFileClose(bExecute :=FALSE); fbFileClose(sNetId:='xxx.xx.xxx.1.1.1', bExecute:=TRUE, hFile:= fbFileOpen.hFile); State:=State+1; 6: fbFileClose(bExecute:=FALSE); IF NOT fbFileClose.bBusy THEN State:=0; _Busy:=FALSE; END_IF END_CASE
METHOD PROTECTED InternalLog VAR_INPUT Message:STRING; END_VAR
THIS^.Message:=CONCAT(Message,'$n'); _Busy:=TRUE;
Now our two loggers are finished we will create a small demonstration program to test them. Our test program consists of two processes: Process 1 assigns a logger to a pointer to our abstract logger. It then starts process 2. Process 2 uses the pointer to log a message. Notice that process is completely unaware of the used logger.
PROGRAM MAIN VAR ConcreteCSVLogger:ConcreteCSVLogger; ConcreteAdsLogger:ConcreteADSLogger; AbstractLogger:POINTER TO AbstractLogger; LogNow:BOOL; Process1:INT; Process2:INT; END_VAR
// Process 1 assigns a logger and starts task 2. CASE Process1 OF 0: IF LogNow THEN LogNow:=FALSE; AbstractLogger:= ADR(ConcreteAdsLogger); Process2 :=1; Process1:=Process1+1; END_IF 1: IF Process2 = 0 THEN AbstractLogger:= ADR(ConcreteCSVLogger); Process2:=1; Process1:=Process1+1; END_IF 2:IF Process2 = 0 THEN AbstractLogger:= ADR(ConcreteAdsLogger); Process2:=1; Process1:=Process1+1; END_IF 3: IF Process2 = 0 THEN AbstractLogger:= ADR(ConcreteCSVLogger); Process2:=1; Process1:=Process1+1; END_IF 4:IF Process2 = 0 THEN Process1:=0; END_IF END_CASE //Process 2 does its task indepently of the assigned type of logger. CASE process2 OF 1: AbstractLogger^.LogMessage(CONCAT('Important message ',INT_TO_STRING(Process1))); process2:=process2+1; 2: AbstractLogger^(); IF NOT AbstractLogger^.Busy THEN process2:=0; END_IF END_CASE
Running the process by setting variable ‘LogNow’ to true, will yield to following results:
Please note that however the loggers are working, this code is meant as example for the ABSTRACT keyword. This code is NOT production ready in terms of unexpected error handling and checking for null reference pointers.
Conclusion
The ABSTRACT keyword is a new feature in TwinCAT. The keyword helps us to hide unnecessary implementation details and focus on functionality. We have seen a practical example of two loggers with combined functionality in an abstract function block.
Thank you for reading, let me know your thoughts and remarks here.
Happy coding!