Gateway Event Script - Tag Value Change

The purpose of this script is to record the values of specific components as they were 5 seconds before an unintentional shut down. The script includes some conditions that must be met before writing to the CSV file. This is to ensure the script only writes when an unexpected shut down occurs, rather than a planned shutdown. Also, to make sure the script only writes once per shutdown event.

What the script should do is write to the CSV file one time per shutdown event. What the script is actually doing is writing to the CSV file every few minutes. This includes times when the first trigger component value is not zero. First of all, it shouldn't be writing very few minutes and second it should never write unless the value of the first trigger component is exactly zero.

Can anyone tell me where I've gone wrong with the code? If needed, I'm happy to provide more details for what I'm trying to achieve.

Thank you

Link to script: GitHub - PeteGietl/Ignition-Pub at 04a3c2d1b8b1b99e00afb9f53c1711f8510197f4

I should also add that this script is being used in Ignition 7.9 (Jython). Upgrading is unfortunately not likely to happen and it's out of my control.

I've also replaced potentially sensitive data with an 'x'.

Tip: You can post the code here using the </> formatting button and, if you want, use the gear-icon, Hide Details option to collapse the selected code.

Code in here
for i in range(10):
    print("Syntax highlighting and preserved indentation.")

Got it, thanks. Here's the code:

Gateway Event Script
import system
import csv
import time

# Define the list of triggering components
triggerComponentPath = [
    {"path": "x/GVBED1/F19/F19:35"},
    {"path": "x/GVBED1/F53/F53:245"},
]

# Define the list of components to monitor with corresponding column headers
componentPaths = [
    {"path": "x/GVBED1/F53/F53:60", "header": "D1 inlet temp"},
    {"path": "x/GVBED1/F53/F53:65", "header": "D1 outlet temp"},
    {"path": "x/GVBED1/F53/F53:70", "header": "D2 inlet temp"},
    {"path": "x/GVBED1/F53/F53:75", "header": "D2 outlet temp"},
    {"path": "x/GVBED1/F53/F53:80", "header": "D3 inlet temp"},
    {"path": "x/GVBED1/F53/F53:85", "header": "D3 outlet temp"},
    {"path": "x/GVBED1/F53/F53:115", "header": "Burn Chamber temp"},
    {"path": "x/GVBED1/F53/F53:120", "header": "Plenum temp"},
    {"path": "x/GVBED1/PD90/F554/F554:14", "header": "TCV-101 D1 Damper pos"},
    {"path": "x/GVBED1/N:108/TCV102", "header": "TCV-102 D2 Damper pos"},
    {"path": "x/GVBED1/PD90/F554/F554:16", "header": "TCV-103 D3 Damper pos"},
    {"path": "x/GVBED4/B10_Damper_Scaled_pos_Ing", "header": "B10 Damper pos"},
    {"path": "x/GVBED3/ACO_D4_Dry_Scaled", "header": "ACO moisture %"},
    {"path": "x/GVBED1/N110/TCV104", "header": "TCV-104 D3 Cold Damper pos"},
    {"path": "x/GVBED4/TCV107", "header": "TCV-107 D1 Cold Damper pos"},
    {"path": "x/GVBED4/TCV108", "header": "TCV-108 D2 Cold Damper pos"},
    {"path": "x/GVBED3/M15_Press_Fiber_Lbs_Hour", "header": "Lbs per hour"},
    {"path": "x/GVBED4/TE_Tank_Farm", "header": "Outside Temp"}
]

# Define the path of the CSV file to store the state information
csvDirectoryPath = "C:\\Users\\x\\Documents\\Ignition Fire Report\\"
csvFilePath = csvDirectoryPath + "output.csv"

# Initialize a flag variable to False
eventTriggered = False

# Initialize a variable to store the previous state of the triggering conditions
previousState = False

while True:
    ## Get the current timestamp
    currentTimestamp = system.date.now()

    # Calculate the timestamp 5 seconds before the current time
    captureStartTimestamp = system.date.addSeconds(currentTimestamp, -5)

    # Read the current value of the triggering components
    triggerComponentValues = [system.tag.read(trigger["path"]).value for trigger in triggerComponentPath]

    # Check if the values of both triggering components reach zero
    currentState = triggerComponentValues[0] == 0 and triggerComponentValues[1] <= 8

    # Check if the state has changed from False to True
    if currentState and not previousState:
        print("Trigger Component Values:", triggerComponentValues)

        # Open the file in append mode using the csv module
        csvfile = open(csvFilePath, mode='a')
        writer = csv.writer(csvfile)

        # Check if the file is empty
        if csvfile.tell() == 0:
            # Write column headers if the file is empty
            columnHeaders = ["Timestamp"]
            columnHeaders.extend(component["header"] for component in componentPaths)
            writer.writerow(columnHeaders)

        # Capture component states
        states = [system.tag.read(component["path"]).value for component in componentPaths]

        # Write captured states to CSV file
        row = [captureStartTimestamp]
        row.extend(states)
        writer.writerow(row)

        # Close the CSV file
        csvfile.close()

        print("Data written to CSV at", captureStartTimestamp)

    # Update the previousState variable
    previousState = currentState

    # Sleep for a short duration before checking again
    time.sleep(5)
1 Like

You should not be setting up an infinite while loop in an event script (or, generally speaking, anywhere in scripting).

You should rely on the existing facilities (such as a timer script) and use tags or system.util.globals to manage state between executions. What you've written is prone to memory leaks and is consuming one of a limited thread pool indefinitely.

4 Likes

Thanks for the feedback. I understand your point about memory consumption/leaks. A timer won't be appropriate for what I'm trying to achieve, but I'll see what else I can do to manage the state between executions.

Why not?

while True:

combined with

time.sleep(5)

Is for all intents and purposes a script that executes every 5 seconds (or near enough).

A Timer Script set to a Fixed Delay of 5000ms will execute it's script with a 5 second delay between each invocation.

Everything above the while loop can be safely moved to Top Level project variables, and everything inside of the loop can be executed in the script.

In fact I would say that if you replaced the while True with def checkState():, removed the timer.sleep(5) and put all of this in a project script and then called checkState() from a Timer Event you would get exactly the same execution as you are expecting now.

6 Likes

Also, this part here is extremely inefficient:

triggerComponentValues = [system.tag.read(trigger["path"]).value for trigger in triggerComponentPath]

if you're back in v7, the use system.tag.readAll to read a list of tagPaths. Reading them 1-by-1 is very slow and inefficient.
Eg

triggerComponentValues = [qval.value for qval in system.tag.readAll(trigger)] 
1 Like

The intent is for the script to execute any time the conditions for the trigger components are met (i.e., when an event occurs that causes an unexpected shutdown). Producing output from the script at timed intervals would not provide the necessary data.

Thank you for your feedback.

I see, thank you.

Okay, but that isn't what the script you provided would do. Once triggered your script as written will run indefinitely with a 5 second interval between executions (until it is stopped by some outside force).

If you only want the script to execute once, then the while True needs to be removed as well as the time.sleep().

Any time you have a long-running task in Jython, it effectively becomes a memory leak as soon as anyone edits and saves the project it comes from (or any of the projects from which it inherits), because the event infrastructure will likely start another one in parallel. Then that task might very well be duplicating the work it is designed for.

Two pretty simple indicators that you are screwing up:

  • Unbounded loop (while True: and friends)

  • Any form of .sleep(), including CPU-burning spin loops that check a condition.

Perspective has some tolerance for short .sleep() operations, entirely due to its unbounded thread pool. The rest of Ignition, not so much.

2 Likes

I understand, thank you. My current script is not using while loops or time.sleep.

I understand. My current script is not using unbounded loops or any form of .sleep(). Thanks for providing further detail on the effects of my previous choices.

I appreciate your feedback and I've applied your advice. I don't want to miss the forest for the trees though, as my script is still producing unexpected output. This is the core issue I'm seeking help with. I've gone through several iterations with the same results: The script writes to the CSV file every few minutes even while the values for 'triggerComponent[0]' and 'previousValue0' remain unchanged at a value of 0 for each. I've been watching the tag values live as the script runs.

Ignition 7.9

The purpose of the script:

  • To write the states of specific components to a file when an unexpected shutdown occurs
  • This shutdown is caused by a type of event that occurs on average about 5 times per week
  • Only one new line should be written per each event

What I expect the script to do:

  • When the value of 'triggerComponent[0]' changes, the script runs
  • Then it checks if the value for 'previousValue0' (this is a memory tag) is greater than or equal to 10
    (this is to ensure the script writes only when the event occurs) & 'triggerComponentValue[1] is less
    than or equal to 1
  • If both are true, the script writes to the CSV file

What the script is doing:

  • Writing to the CSV file every few minutes while the values that it checks remain at 0

Additional Information

  • The only print statement that shows up in the 'wrapper.log' is "Data written to CSV at..."
  • In the 'Gateway Tag Change Scripts' interface, I originally used both 'triggerComponent' tags in the
    'Tag Path(s)' window. I now have only triggerComponent[0].
  • I've replace potentially sensitive info with an 'x'

Here is the current script. I will update lines as necessary for efficiency, but I would love to work out the core functionality first.

Thank you

Gateway Tag Change Script
import system
import csv
import os

# Define the list of triggering components
triggerComponentPath = [
    "x/GVBED1/F19/F19:35",
    "x/GVBED1/F53/F53:245",
]

# Define the list of components to monitor with corresponding column headers
componentPaths = [
    {"path": "x/GVBED1/F53/F53:60", "header": "D1 inlet temp"},
    {"path": "x/GVBED1/F53/F53:65", "header": "D1 outlet temp"},
    {"path": "x/GVBED1/F53/F53:70", "header": "D2 inlet temp"},
    {"path": "x/GVBED1/F53/F53:75", "header": "D2 outlet temp"},
    {"path": "x/GVBED1/F53/F53:80", "header": "D3 inlet temp"},
    {"path": "x/GVBED1/F53/F53:85", "header": "D3 outlet temp"},
    {"path": "x/GVBED1/F53/F53:115", "header": "Burn Chamber temp"},
    {"path": "x/GVBED1/F53/F53:120", "header": "Plenum temp"},
    {"path": "x/GVBED1/PD90/F554/F554:14", "header": "TCV-101 D1 Damper pos"},
    {"path": "x/GVBED1/N:108/TCV102", "header": "TCV-102 D2 Damper pos"},
    {"path": "x/GVBED1/PD90/F554/F554:16", "header": "TCV-103 D3 Damper pos"},
    {"path": "x/GVBED4/B10_Damper_Scaled_pos_Ing", "header": "B10 Damper pos"},
    {"path": "x/GVBED3/ACO_D4_Dry_Scaled", "header": "ACO moisture %"},
    {"path": "x/GVBED1/N110/TCV104", "header": "TCV-104 D3 Cold Damper pos"},
    {"path": "x/GVBED4/TCV107", "header": "TCV-107 D1 Cold Damper pos"},
    {"path": "x/GVBED4/TCV108", "header": "TCV-108 D2 Cold Damper pos"},
    {"path": "x/GVBED3/M15_Press_Fiber_Lbs_Hour", "header": "Lbs per hour"},
    {"path": "x/GVBED4/TE_Tank_Farm", "header": "Outside Temp"}
]

# Define the path of the CSV file to store the state information
csvDirectoryPath = "C:\\Users\\x\\Documents\\Ignition Fire Report\\"
csvFilePath = csvDirectoryPath + "output.csv"

# Define the path of the Memory Tag to store the previous state of triggerComponentValues[0]
previousValue0TagPath = "x/GVBED1/F19/F19:35_previous"

# Initialize fire event flag
fire_event_detected = False

# Event handler for Gateway Tag Change Event
def tagChangeEvent(event):
    global fire_event_detected  # Declare the flag as global to modify it

    # Check if the event is due to the initial subscription
    if not event.initialChange:
        # Print when the tagChangeEvent function is called
        print "tagChangeEvent called at", system.date.now()

        # Check if the tag that changed is F19:35
        if event.getTagPath() == "x/GVBED1/F19/F19:35":
            # Read the current value of the triggering components
            triggerComponentValues = [system.tag.read(tagPath).value for tagPath in triggerComponentPath]

            # Read the previous value from the Memory Tag
            previousValue0 = system.tag.read(previousValue0TagPath).value

            # Print the values of F19:35 and F19:35_previous
            print "F19:35 value:", triggerComponentValues[0]
            print "F19:35_previous value:", previousValue0

            # Check if the previous value of triggering component[0] was at least 10 and the current value of triggerComponent[1] is 1 or less
            if previousValue0 >= 10 and triggerComponentValues[1] <= 1:
                if not fire_event_detected:
                    # Fire event detected, set flag
                    fire_event_detected = True
                    print "Fire event detected, setting flag to True"
                    
                    # Get the current timestamp
                    currentTimestamp = system.date.now()

                    # Open the file in append mode using the csv module
                    csvfile = open(csvFilePath, "a")
                    writer = csv.writer(csvfile)

                    # Check if the file is empty
                    if os.path.isfile(csvFilePath) and os.path.getsize(csvFilePath) == 0:
                        # Write column headers if the file is empty
                        columnHeaders = ["Timestamp"]
                        columnHeaders.extend(component["header"] for component in componentPaths)
                        writer.writerow(columnHeaders)

                    # Capture component states
                    states = [system.tag.read(component["path"]).value for component in componentPaths]

                    # Write captured states to CSV file
                    row = [currentTimestamp]
                    row.extend(states)
                    writer.writerow(row)

                    csvfile.close()

                    print "Data written to CSV at", currentTimestamp

            elif fire_event_detected and not (previousValue0 >= 10 and triggerComponentValues[1] <= 1):
                # Conditions for a fire event no longer met, reset flag
                fire_event_detected = False
                print "Fire event conditions no longer met, resetting flag to False"
            
            # Update the previous value in the Memory Tag
            system.tag.write(previousValue0TagPath, triggerComponentValues[0])

If this is the whole script you have within the Gateway Tag Change Events Script window, then you have scoped the majority of your logic within the function tagChangeEvent() but that function is never called in the script.

Are you referring to, in the context of Ignition's Gateway Tag Change Scripts, the function that gets automatically called when a tag change event occurs is named tagChange(event) , not tagChangeEvent(event)?

Update

Before my post 2 hours ago, I disabled this script in Designer. When I came back after 2 hours, I checked my CSV file and there were several new lines written for this duration. So, I restarted the gateway. Now, after re-enabling the script, and after about 30 minutes, there are no new lines. This is good, but I assume this means some iteration of my script was lingering.

Is it considered best practice to restart the gateway after saving changes in Designer (this is only an option during planned downtime or emergency)? Are lingering processes associated with 7.9? Is there some other reason that may cause processes to linger?

I don't believe gateway tag change scripts have an initial change. I've been checking if the event.previousValue is not None

You should check the logs for any errors running your script

Generally no. The script engine will restart whenever you save changes. Although I'm not sure if currently running threads are affected or not. If they aren't, then your "while true" script would have still kept running, hence why you were still seeing values being inserted. Only a gw restart would fix that

No, I’m referring to the function that is defined in the script you provided.

Unless you call the function after it is defined, any code in that scope will not be executed.

I don't believe gateway tag change scripts have an initial change. I've been checking if the event.previousValue is not None

You should check the logs for any errors running your script

I added this condition based on info from the 7.9 manual. it's in there for previous troubleshooting and probably isn't needed anymore:

  • initialChange - a variable that is a flag (0 or 1) which indicates whether or not the event is due to the initial subscription or not. This is useful as you can filter out the event that is the initial subscription, preventing a script from running when the values haven't actually changed.

As for the logs, there have been no errors.

your "while true" script would have still kept running

I think you're probably right about that :cry:

1 Like