plccodesysstructured-textiec61131-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


Solution

  • 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.