Scripting via script?

So, a while back, I needed to create an alarming module for Ignition, to replace an older software package. The older version read off a very roughly formatted HTML file. So, since I needed to write a script to parse and separate the valuable information from the huge amount of HTML cruft anyway, I extended the script to create Ignition alarms and write all the parsed information to their relevant properties, Name, Notes, Priority, setpoints, etc. Ran it as a gateway event script off a timer, worked like a charm.

Iā€™m currently copying out a page from another piece of software we present as a large table. We have many different thermocouples, and weā€™d like to keep the functionality whereby when a particular temperature gets out of range, the background for that TCā€™s cell goes red. At the moment, this requires me to create each text field:

  1. Write a simple script in its Text property to read and then format the relevant tag value.
  2. Write a script for the background color, setting its value to red if certain conditions are met, or transparent if not.

Even with copy/pasting, this is hours of brainless work, and Iā€™m setting myself up to have to go bug hunting for typos in the bindings when itā€™s done.

Iā€™m wondering if itā€™s possible write a python script to access individual components and their properties, which Iā€™ve only so far managed with expressions and event scripting. Then, could I have my code access the binding associated with a components property and write the expression script automatically? The script itself is very simple, one line in each case, with the only change being the name of the tags, which could be provided via a .csv list or some such.

Essentially, I can see a way to do all this (looks like 1 day of work based on how its going) with about 10 lines of pseudocode, that would probably execute in a second or two, given how quickly the alarms updated. The secret sauce is being able to access and modify components properties, and scripts, outside of the usual GUI. Is that possible?

Iā€™m thinking something like how system.tag.editTag has the AlarmConfig keyword argument that allowed me to write alarms to tags from a script.

And though Iā€™ll probably be done brute forcing this by the time I get an answer, I see this coming up again, so itā€™s of general interest to me

I donā€™t fully understand your application, but have you considered using the Template Repeater component?
That way you can create one template text field with the script you need and just pass in on each instance the tag path you are referencing.
Just a thought.

1 Like

That sounds like that might be the solution. Havenā€™t been exposed to that particular component yet. Iā€™ll check out the help page you linked, thanks

I would script creation of memory tags for the alarms and then just use your script to write to those tags.
This way you can do the alarming right in the actual Ignition tags and bind the controls to the tags.
Using a template repeater for this would be the best way if the amount of inputs varies.

Ok, so having looked into templates, I donā€™t see how they would help me.

They seem primarily for re-creating the graphical look of components. Iā€™m doing a simple grid of bordered text fields. 8 CTRL+C/CTRL+V and I have my 100 some fields. Lining them up is also very easy with the alignment tools, though perhaps a flow-based template repeater would be a bit faster.

The issue is, the tags that need linking are not simply motor_1, motor_2 as in the Inductive university tutorials, making indirect referencing not particularly useful. Each thermocouple has its own, distinct name, and though there are a few groups with multiple members, they only include 4-10 or so members. There are also a significant amount of singletons.

I need to bind properties of ~100 components to ~100 distinct names. The binding Expressions are as follows:
text - numberFormat({Tag_Path},"##0.0")
color - ({Tag_Path}<900) && ({Tag_Path}>100)

With Templates, I still would have to add each of those expressions to each text field by hand, which is where the wasted time is coming from.

Perhaps I should not have mentioned the alarms. The alarms are completed, and I had no problem with them. I mentioned it as an example where pythonā€™s capabilities were extensible to my problem. It seems that Python is really only useable in event scripts, and mostly interacts with tags and their properties, but has no access to components or their properties? Is this correct?

For instance, how can I, from an event script run off a timer, access the background color of a component in my project? Can I do that or not? And if I can access it, can I only set the background color to be a static value, or can I access the bindings related to that property via a script, and modify them?

Sorry guys, I run at the mouth. In my attempt to explain everything, I bury my thesis in noise.

In all examples of the template I have seen, the user must type in the tag names or bindings themselves. This typing is the problem, and is where I am wasting time. I canā€™t use indirect scripting to get around this, due to the names of the tags, which I can not change.

I need a way to run a single script (or set up a single component) that will run down the list of tags and automatically populate itself with bindings relating to formatting the data and changing color based on the data.

I could do this very easily if I could navigate around components and their properties in Python the way you can in expressions a la event.parent.component.property. But it seems like I canā€™t.

Is it the case that I canā€™t?

Look into the template repeater. It works with templates to generate and provide the tags/bindings in each template with info from the list of tags you provide.

It might help to see the actual structure of your tag tree. Are you only using the one one tag for each template? Or does each template refer to a related tag that is consistently named? All related tags for one thermocouple in a folder with consistent names or parts of names?

Thereā€™s not enough information to really give a specific example for you. Basically, as long as you can create a template that you can feed a base tag path or other string that you can use to build a tag path and have it display properly, you should be able to use the template repeater to do the legwork of creating copies of the template and populating the tag paths.

Also note that you can populate the template repeater using a dataset, which you can build through scripting - it doesnā€™t have to be a simple list of incrementing indexes.

1 Like

Hereā€™s an example of something that I have running in a project here.

I have a screen that I display on a large tv here. Part of the screen is dedicated to something Iā€™m calling a ā€œWhatā€™s Nextā€ list. Itā€™s a list of upcoming events, coming from different pieces of equipment and how long is left until those events occur. Some events have an extra data point displayed in the template as well. The background of the event changes color based on the amount of time left until the event occurs.

Hereā€™s what the event item template looks like:

You can see that I have three Template Parameters that the template expects to be provided. You can set yours up with more or fewer pieces of information, as needed by your templates.

The equipment name text field has a binding that looks like this:

tag({WhatsNextItem.itemTagPath}+"/EquipmentName")+if({WhatsNextItem.itemType} = "Quench"," Quench","")

The tag path I am putting in the template parameter is actually a path to a folder where tags related to a piece of equipment are kept. One of the tags in that folder will be a memory tag that holds a human readable name for the piece of equipment. You could also just display the last part of a standard tag path using scripting, or, if you want to provide a name as part of the template parameter dataset, you could simply bind a text field to that too.

The time left display in the template is filled in a similar manner. I append the appropriate string to the base tag path to get at the time left string in my folder. I had to use a switch statement as different classes of equipment ended up with the time left string being named differently. Oops on my part when setting up the naming, but itā€™s not too bad to work around. Thatā€™s another use for the itemType template parameter, actually. I use that to determine the appropriate string to add to the base tag path.

I use the style customizer to change the background color of the template. Iā€™m using a custom property I added to the template to drive the customizer:

The custom property itself is bound to an expression:

if({WhatsNextItem.timeLeftInt} <= 0, 2, //if timer done, set code 2
if({WhatsNextItem.timeLeftInt} < 300, 1, 0)) //if timer less than 5 minutes, set code 1, else set code 0

Itā€™s looking at the third template parameter that Iā€™m providing, but you could also do some sort of calculation here too.

To use the template in a dynamic fashion, you use the template repeater. Hereā€™s how mine looks in the designer:

The repeater is bound to a dataset tag in my tag tree. That dataset tag is populated by a script that runs each minute, but you could just provide a static dataset with the tags you need for your displays. Mine are calculated by the script because I wanted my list to be dynamic. The script finds pieces of equipment with active timers, gets their base tag paths, merges them into a common list, sorts the list and populates the dataset with it.

When the dataset changes, the template repeater notices the change and updates the templates as needed. Removing, adding, sorting as needed. The templates themselves will update the time displays. They could even update the backgrounds if I wanted, but I was fine with waiting on the background color change until the next dataset update (Iā€™m pulling the color code info from the dataset, not building it from the tag path. It was simpler.)

This looks very promising. And similar to what Iā€™m trying to do. Thanks.

The issue Iā€™d run into was it was easy to populate the template fields that dealt with the Value of the dropped-in tag. But getting the name of the tag into one column of the table, that was not working for me. Building a dataset, populated by a script might be the way to do it. By linking the frequently changing values direct to the tag, it should save overhead on running the script.

All Iā€™d need to do is set it to scrape the tag names out of the folders containing thermocouples and populate a single column dataset. Plug the ā€˜nameā€™ field into the dataset in one template, plug the value and good/bad fields into the tags on a second, compose the two templates into one and I should be off to the races, yes?

Iā€™m guessing you should be able to do what you want with one template, and one dataset feeding it.

You can either put the good/bad fields into extra columns on the dataset, or let your template find them based on the tag path provided for the actual reading. This method works if your naming system and/or folder structure is consistent. If itā€™s not, itā€™s probably easier to just provide the tag path directly to the dataset.

Unless your list of tag names is dynamic, you should only have to populate the list once and store it somewhere. In a dataset tag, or database table. Your template repeater can then be bound to that tag/table.

Hi Brian.

Would running the script every time a value changes (given the numbers of thermocouples, that would be basically every polling cycle) add more overhead than simply linking the values to the tag in a more standard way? If not, thatā€™s fine, but it has me concerned that it might be. Iā€™m sure Ignitions underlying infrastructure is more efficient than whatever I could hack together.

And again, the only reason I need to script is to fill in the names, which are much more static than the values.

BTW, this project is done, but I am still interested in figuring out how to get it done faster next time.

I guess Iā€™m still a bit fuzzy on exactly how your screen is laid out. It kind of sounds like you just manually copied/pasted a bunch of objects (maybe templates?) onto your screen, and were using scripting to modify the bindings of each individual object.

My suggestion was to create one template that you can feed key bits of information (thermocouple tag path, and maybe good/bad values or tag paths to those values) and have it display that one thermocoupleā€™s info properly. Note that you donā€™t actually place this template on your window!

Once you have a template working, you use a template repeater to do the work of rubber stamping as many templates as you need. It uses a dataset to both know how many templates to create, and to know what values to supply to each template.

I think Iā€™d have to see screenshots of what your screen looks like right now and/or your tag layouts to give much more advice. Itā€™s hard to say what you could do better if I donā€™t fully understand what it was you actually did.

1 Like

Got it.

Thanks for bearing with me, Brian. Trying to explain what I need as I move into this new field, with itā€™s own conventions and jargon, can seem like being a pre-verbal child trying to communicate. I know exactly what I want, but I canā€™t phrase it in such a way that others can help :slight_smile:

For posterity, my solution is:

Generate three side-by-side text fields in one template. The template has two properties, ā€˜Valueā€™, the drop-in property and ā€˜TagNameā€™. I use a template repeater linked to a database I generate with the following code.

def main():
    tags = system.tag.browseTags(parentPath="Thermocouples/OpsTC1")

    ds = system.dataset.toDataSet([], [])
    ds = system.dataset.addColumn(ds, [], 'TagName', str)
    ds = system.dataset.addColumn(ds, [], 'Value', float)

    tagList = []
    nameList = []

    for tag in tags:
	tagList.append(tag.path)
	nameList.append(tag.name)

    valList = system.tag.readAll(tagList)

    for i,name in enumerate(nameList):
	ds = system.dataset.addRow(ds,[name,float(valList[i].value)])
	
    system.tag.write("TC_DB",ds)

And it gives me exactly what I need. I do conditional formatting of the final text field in the component properties of the textfield, not in the code, simply because Iā€™m more familiar with doing it that way. Not sure if itā€™s more efficient to do it otherwise.

No problem.

Are you running the code to generate the database each time you want to update the Value? And that is the value displayed for the user to look at? If so, you can definitely do this more efficiently.

First, I would modify your code to generate only the list of tag paths. Donā€™t include the tag value in your dataset. Then, modify your template to have only one parameter, the tag path. For displaying the actual value, bind the text field to an expression that uses the tag path parameter on the template to build the path to the value that you want to display. You should still be able to do any conditional formatting here as well (thatā€™s the proper place for that code, IMO).

Now, you should only need to run the function to generate the list of tags you want displayed. If thatā€™s fairly stable, you would only need to run it when you actually add a new thermocouple to ignition. The templates will take care of subscribing to updates of the tag by themselves. This will avoid the overhead of browsing the tag tree and creating a new dataset every time you want to refresh the readings displayed in the templates.

As a further example of how the text field binding, I have a text field in my project that has something like this:

toInt(tag({WhatsNextItem.itemTagPath}+"/BasicInfo/Temperature"))+" Ā°F"

Thereā€™s actually more to it, but thatā€™s the relevant part. Basically, Iā€™m using the tag expression function to dynamically build the tag path, instead of hard coding it. Iā€™m taking the base tag path provided to my template and tacking on the rest of the path to the tag I want to display. The tag expression executes that tag path and retrieves the value. The toInt is used because I just wanted to display the integer portion of the reading for this overview screen. Iā€™m also appending some text for the units.

Your expression might look something like:

tag({templateName.tagPath}

Iā€™d have to look at the docs for the system.tag.browseTags function to see if the path it returns when searching with a parent path given as a variable includes the full path or not. You might need to prepend the expression with the parent path variable you gave the browseTags function. Or, the return dataset from that function might have a fullPath result as well as the path result that you are using. I donā€™t remember off the top of my head.