- Published on
Automatic Instance Path for Error and Log Identification
- Authors
- Name
- Kassym Dorsel
All POUs can have their device tree path automatically initialized into a variable. This can used as a unique string identification for the part of the code and can be used for easy identification of errors, logs, counters, etc.
Introduction
When logging any information, be it to a database, file or just hmi, there needs to be an easy way to identify where it comes from. This can machine errors, counters, cycle time, anything. Every device, station, item needs its own unique id. This can be as simple as an integer, but that does not convey any extra useful information. Ideally the id would be able to convey the location or enough information to be able to easily find the physical associate device on a machine for easy trouble shooting.
Of course all of this can be manually hardcoded, for example using an integer device id to a location with a lookup table. Why do something manully when there are functions to automatically do it for us.
Instance Path
Beckhoff offers a pragma that will initialize a string variable to its device tree path. This will be a unique string id and can also convey additional location information. Beckhoff documentation about instance-path.
Let's create a base FB with this instance path. All of the code used in the project can extend this base FB and automatically have their instance path initilizated in the _sid
variable. Short for String ID.
{attribute 'reflection'}
FUNCTION_BLOCK FB_BaseDevice
VAR
{attribute 'instance-path'}
{attribute 'noinit'}
_sid : STRING(255);
END_VAR
Create a project and define an instance.
PROGRAM MAIN
VAR
base : FB_BaseDevice;
END_VAR
After starting it all up looking at the _sid
it is initilzed to Project_Name.PLC_Name.MAIN.base
. We now have a unique id that can easily be used to identify what and where it is. When designing the PLC code layout it should match the physical makeup of the machine. That way all instance paths will perfectly match the physical machine layout.
Real World Example
Let's look at a more reasonable example.
PROGRAM MAIN
VAR
stn1 : FB_Station;
stn2 : FB_Station;
stn3 : FB_Station;
END_VAR
FUNCTION_BLOCK FB_Station EXTENDS FB_BaseDevice
VAR
cylinder : FB_Pneumatic // Also extends FB_BaseDevice;
END_VAR
This would give the following instance paths _sid
.
POU | _sid |
---|---|
stn1 | Project_Name.PLC_Name.MAIN.stn1 |
stn2 | Project_Name.PLC_Name.MAIN.stn2 |
stn3 | Project_Name.PLC_Name.MAIN.stn3 |
stn1 cylinder | Project_Name.PLC_Name.MAIN.stn1.cylinder |
stn2 cylinder | Project_Name.PLC_Name.MAIN.stn2.cylinder |
stn3 cylinder | Project_Name.PLC_Name.MAIN.stn3.cylinder |
Now sending a log to the HMI from any of these and have it display information is much easier. I would even add a log method to FB_BaseDevice
that would automatically use this _sid
.
{attribute 'reflection'}
FUNCTION_BLOCK FB_BaseDevice
VAR
{attribute 'instance-path'}
{attribute 'noinit'}
_sid : STRING(255);
END_VAR
METHOD log : BOOL:
VAR_INPUT
msg : STRING(255);
END_VAR
// Implementation
log := F_SendToHMI(_sid, msg);
This can now be used in FB_Cylinder
to notify of a failed extension/retraction.
IF failedToExtend THEN
log('Failed to Extend');
END_IF
Even in FB_Station
if it expects a part and the part sensor does not turn on.
IF partExpected AND NOT partSensor THEN
log('Part mismatch');
END_IF
This could then easily be displayed.
Device | Message |
---|---|
Project_Name.PLC_Name.MAIN.stn1.cylinder | Failed to Extend |
Project_Name.PLC_Name.MAIN.stn1 | Part mismatch |
Cleaning Up
We now have automatic id for any POU in the PLC. However the _sid
is verbose and repetitive. Sepcifically the Project_Name.PLC_Name.
part. This will never change (unless you change the project or PLC name, which you probably shouldn't) and adds no value.
The MAIN.
part of the _sid
can be useful if more than one PLC Task is used. For example if a secondary faster PLC task was used it could show up as MAIN_FAST.
.
It would be nice to automatically remove the project and plc name from the _sid
. The project name is available in a global variable automatically defined by Beckhoff.
FUNCTION F_RemoveProjectName : STRING(255)
VAR_INPUT
path : STRING(255);
END_VAR
// Implementation
F_RemoveProjectName := DELETE(path, LEN(TwinCAT_SystemInfoVarList._AppInfo.ProjectName) + FIND(path, TwinCAT_SystemInfoVarList._AppInfo.ProjectName), 1);
F_RemoveProjectName := F_ToLCase(F_RemoveProjectName);
This function will remove everythin upto the task name from the instance path. This helps clean it and also makes it even less likely to reach the 255 string length limit. Now let's add it to the FB_init
method of FB_BaseDevice
. As this only needs to be computed once, this is the best place to put it.
{attribute 'reflection'}
FUNCTION_BLOCK FB_BaseDevice
VAR
{attribute 'instance-path'}
{attribute 'noinit'}
_sid : STRING(255);
_id : WORD;
END_VAR
METHOD FB_init : BOOL
VAR_INPUT
bInitRetains : BOOL; // if TRUE, the retain variables are initialized (warm start / cold start)
bInCopyCode : BOOL; // if TRUE, the instance afterwards gets moved into the copy code (online change)
END_VAR
// Implementation
_sid := F_RemoveProjectName(_sid);
_id := F_DATA_TO_CRC16_CCITT(ADR(_sid), TO_UDINT(len(_sid)), 0);
Going back to the example the _sid
will now be much shorter and cleaner.
POU | _sid |
---|---|
stn1 | MAIN.stn1 |
stn2 | MAIN.stn2 |
stn3 | MAIN.stn3 |
stn1 cylinder | MAIN.stn1.cylinder |
stn2 cylinder | MAIN.stn2.cylinder |
stn3 cylinder | MAIN.stn3.cylinder |
Looking closely notice the _id
variable. In your project sometimes a string based id will not work for you and an integer is needed. In FB_init
set the _id
to a unique integer based on the string using the F_DATA_TO_CRC16_CCITT function.
Using a hashing algorithm comes with the chance of collision where the
_id
for two or more_sid
will be the same. Albeit a very small chance this needs to be taken into account if used and verified.
Conclusion
There is no need to hard code a description string or id for all the different POUs in a project. Use the tools from Beckhoff and have it done automatically. Using OO principles makes implementing this easy by using a base FB with this already implemented. Ideally, even if it's empty, all FBs should extend a base class. This makes any future modifications fast and easy.
This can be used as the basis for creating a global string based counter for any device. See how using a persistent hash table.