Oriol Galceran
Oriol Galceran

Reputation: 3

Iterating through variable list

I'm writing an App on CODESYS that has a set of alarms in a variable list whose state I'd like to check in a program. Right now I'm just referencing those variables individually in the program, but I was wondering if there's a way to iterate through every variable in that list, in such a way that I could add new alarms without having to modify the program.

Any ideas?

Thanks

Upvotes: 0

Views: 815

Answers (2)

dwpessoa
dwpessoa

Reputation: 632

As a possible workaround...

Have you thought about using it as a recipe, directly in an Array? This would allow you to easily iterate through the alarm index, but this is only valid if there is a limit on the number of possible alarms and if you can handle some limitations.

First create a struct that models your Alarm, for example:

TYPE MYALARM :
STRUCT
    Description : STRING;
    State : BOOL;
    IsEnable : BOOL;
END_STRUCT
END_TYPE

Then declare it in a GVL (it could be of the retentive type, in case you don't want to lose the settings when the PLC is restarted). Adjust the array size to the maximum number of alarms you can have in this application.

//GVL
VAR_GLOBAL RETAIN
    ListOfAlarms : ARRAY [0..100] OF MYALARM;
END_VAR

Now you can create a routine to program the alarms in the list, like a setup function (for example, use a function block that checks if the alarms already exist and adds them to the list), or manually add them at runtime.

//Add a new alarm to the list
FOR i := 0 TO 100 BY 1 DO
    IF NOT GVL.ListOfAlarms[i].IsEnable THEN
        GVL.ListOfAlarms[i].IsEnable := TRUE;
        GVL.ListOfAlarms[i].Description := 'New Alarm';
    END_IF
END_FOR

This can come with an inconvenience of making the program less readable, as the alarms must be referenced by the Array index, including in the HMI. A solution to the code can be to create a function that looks for the alarm by the string and returns the index, but in this case, the description cannot be repeated.

//Simple example of function to return the index
FUNCTION AlarmDescrToIndex : INT
VAR_INPUT
    Descr : STRING;
END_VAR
VAR
    i : INT;
END_VAR

//It is necessary to handle the return if not find the description
FOR i := 0 TO 100 BY 1 DO
    IF GVL.ListOfAlarms[i].Description = Descr THEN
        EXIT;
    END_IF
END_FOR

AlarmDescrToIndex := i;
//How to use
IF GVL.ListOfAlarms[AlarmDescrToIndex('Alarm 2')].State = TRUE THEN
    //bla bla bla...
END_IF

Another problem to deal with is time if there are many alarms in your list, as it will be necessary to search the index multiple times, I recommend keeping an eye on them.

Upvotes: 0

Guiorgy
Guiorgy

Reputation: 1734

You can't. You may think that taking the address of the first variable and looping until you reach the address of the last would work, but here's an experiment:

{attribute 'qualified_only'}
// GVL_ALARMS
VAR_GLOBAL
    START_MARKER: BYTE := 254;
    
    alarm1: BYTE;
    alarm2: BYTE;
    alarm3: BYTE;
    
    END_MARKER: BYTE := 255;
END_VAR
PROGRAM Main
VAR
    ptr: POINTER TO BYTE;
    alarm_ptr: POINTER TO BOOL;
    alarm: BOOL;
    
    activeAlarmsCount: DINT;
END_VAR

activeAlarmsCount := 0;

ptr := ADR(GVL_ALARMS.START_MARKER);
ptr := ADR(ptr[1]);
WHILE (ptr^ <> GVL_ALARMS.END_MARKER) DO
    alarm_ptr := ptr;
    alarm := alarm_ptr^;
    
    // do whatever with alarm
    
    IF (alarm) THEN
        activeAlarmsCount := activeAlarmsCount + 1;
    END_IF
    
    ptr := ADR(ptr[1]);
END_WHILE

However the above does not work at all, at least not on my machine. And that's the problem, CODESYS gives no guarantees how the variables will be arranged in memory. I run the code in a simulator, but maybe if I run on some PLC it could work, you can't rely on that.

difference between START_MARKER and END_MARKER

A possible alternative would be to define your alarms in a structure and loop over that instead:

TYPE _ALARMS :
STRUCT
    alarm1: BOOL;
    alarm2: BOOL;
    alarm3: BOOL;
END_STRUCT
END_TYPE
// GVL_ALARMS
VAR_GLOBAL
    alarms: _ALARMS;
END_VAR
PROGRAM Main
VAR
    alarms_siz: DINT := SIZEOF(alarms) - 1;
    i: DINT;
    
    alarm_ptr: POINTER TO BOOL;
    alarm: BOOL;
    
    activeAlarmsCount: DINT;
END_VAR

activeAlarmsCount := 0;

alarm_ptr := ADR(alarms);
FOR i := 0 TO alarms_siz DO
    alarm := alarm_ptr[i];
    
    // do whatever with alarm
    
    IF (alarm) THEN
        activeAlarmsCount := activeAlarmsCount + 1;
    END_IF
END_FOR

Results of the simulation:

simulation results

This works since structures generally occupy a continuous chunk of memory, however again, I can't guarantee that some PLC won't add some padding to the struct.

Personally I'd use the Python ScriptEngine API that was added to CODESYS to check for changes in the alarms and generate new functions on every save of the project. However, this requires some knowledge of Python and the API. Here's a simple demonstration of the use of the API, which finds all variables of type BOOL in a given GVL and generates a function that counts how many of them are TRUE:

from __future__ import print_function
import sys
import re

# replace GVL_ALARMS with the name of the GVL in your project
gvl_name = 'GVL_ALARMS'
gvls = projects.primary.find(gvl_name, recursive=True)

if len(gvls) == 0:
    print("GVL doesn't exist")
    sys.exit()
elif len(gvls) > 1:
    print("more than 1 GVL found")
    sys.exit();

gvl = gvls[0]
bool_variables = []

# loop through all lines, and find all bool variables defined
for i in range(gvl.textual_declaration.linecount):
    line = gvl.textual_declaration.get_line(i)
    
    # regex pattern that searches for a BOOL variable declaration, that may have an initial value 
    match = re.search('\\s*(.+)\\s*:\\s*BOOL(?:\\s*:=\\s*.+)?;', line)
    if match and match.group(1):
        bool_variables.append(match.group(1))

# print("found bool variables: ", bool_variables)

# replace CountActiveAlarms with the name of the desired function in your project
count_true_function_name = 'CountActiveAlarms'
count_true_functions = projects.primary.find(count_true_function_name, recursive=True)

if len(count_true_functions) > 1:
    print("more than 1 function found")
    sys.exit();
elif len(count_true_functions) == 0:
    count_true_function = projects.primary.create_pou(
        name=count_true_function_name,
        type=PouType.Function,
        language=ImplementationLanguages.st,
        return_type='UINT'
    )
    count_true_function.textual_declaration.insert(0, '(* AUTO GENERATED, DO NOT MODIFY *)\n')
else:
    count_true_function = count_true_functions[0]

# remove old code to replace with newly generated
count_true_function.textual_implementation.remove(0, count_true_function.textual_implementation.length)

if_statement_template = \
"""IF ({0}.{1}) THEN
\t{2} := {2} + 1;
END_IF\n"""

code = ''

# loop through all found variables and generate function code
for bool_variable in bool_variables:
    code += if_statement_template.format(gvl_name, bool_variable, count_true_function_name)

# insert the generated code
count_true_function.textual_implementation.insert(0, code)

The results of running the above script is the following function:

(* AUTO GENERATED, DO NOT MODIFY *)
FUNCTION CountActiveAlarms : UINT
VAR_INPUT
END_VAR
VAR
END_VAR

IF (GVL_ALARMS.alarm1) THEN
    CountActiveAlarms := CountActiveAlarms + 1;
END_IF
IF (GVL_ALARMS.alarm2) THEN
    CountActiveAlarms := CountActiveAlarms + 1;
END_IF
IF (GVL_ALARMS.alarm3) THEN
    CountActiveAlarms := CountActiveAlarms + 1;
END_IF

If at any point you make any changes to the GVL, running the above script will regenerate the function accordingly, so you don't need to change any part of the code that calls the function.

Upvotes: 1

Related Questions