system.util.invokeLater or alternative function in global script?

Hello,

I’m using a tag change triggered global script to aggregate min, max, avg and dev values and to place the results into a dataset tag. Once completed, other expression tags derive further tags from these values. This takes a second or two, so I cannot trigger a transaction group which records the values from those expression tags successfully from that global script, otherwise only some of those tags are updated with the correct values at the time of triggering.

Is there a way to simply use the system.util.invokeLater or a similar delay in a global script? I just need the transaction group to trigger about 2 seconds later.

What version of Ignition and can you share your script?

Hello,

Version 8.1. The process unfortunately consists of many items and is difficult to post it in a logical way, however I found a solution.

1st step: The tag change script aggregates the min, max, avg and dev from each historian tag and places the results into a dataset. At the end of the script a memory integer tag is written to so that it’s value is incremented from 1 to 2.

endTime = system.tag.read('[default]Folder/EndedDateTime').value
startTime = system.tag.read('[default]Folder/StartedDateTime').value
dataSet = system.tag.queryTagCalculations(paths=[
	'[default]Folder/ActualAbsoluteHeightReal',
	'[default]Folder/ActualAbsoluteWidthReal',
        #many more values here...
	],
	calculations=['Minimum','Maximum','SimpleAverage','StdDev'],startDate=startTime,endDate=endTime,ignoreBadQuality=1)

system.tag.write('[default]Folder/MinMaxAvgDev', dataSet)

system.tag.write('[default]Folder/IncrementTag', 2) #This will trigger the next step of the process, waiting for values to change by triggering delay before transaction group is completed

2nd step: Expression tags are used to extract the individual cell values from the dataset by copying the values to their values using an expression like this: {[.]MinMaxAvgDev}[0,3]

3rd step: Expression tags are used to calculate Cpk values from the above expression tags

//Cpk = min(USL - μ, μ - LSL) / (3σ)
//where USL and LSL are the upper and lower specification limits, 
//μ is the process mean, and σ is the process standard deviation.
try(
	if({[.]HeightDev}!=0, //avoid divide by 0 error
		if(({[.]HeightUSL}-{[.]HeightAvg}) < ({[.]HeightAvg}-{[.]HeightLSL}), 
    		 (({[.]HeightUSL}-{[.]HeightAvg}) / (3*{[.]HeightDev})), 
     	 	 (({[.]HeightAvg}-{[.]HeightLSL}) / (3*{[.]HeightDev})))
    ,0)
,0)    
    

4th step: A gateway timer script is set to 2,000ms fixed delay with shared threading since perfect timing is not necessary. the script looks like this:

HandShakeIncremented = system.tag.read('[default]Folder/IncrementTag').value
if HandShakeIncremented == 2:

	system.tag.write('[default]Folder/IncrementTag', 3) #This will trigger the transaction group once the aggregation is completed

else:
	pass	

5th step: A transaction group is set to trigger when [default]Folder/IncrementTag = 3. The transaction group then stores all min, max, avg, dev and Cpk values to an SQL table
I then use the “Write handshake on sucess” on the transaction group to set [default]Folder/IncrementTag back to 1. I use the write handshake on failure to alert me and to have the system retry again.

Hopefully this makes sense and can help someone else. If anyone knows of an easier way to do this, I’d be glad to hear it.

Thanks

1 Like

Put it all in one script. No expression tags, no transaction group.

3 Likes

I’d love to do so, but my programming skills are not developed enough to allow me to do so :blush:, I’m surprised I even got this far :). I’ll work on it though.

Any tips on how to compile it all into one script would be greatly appreciated!

My suggestion for you is to pseudo code before you begin. It helps a ton.

Semantically speaking, what are the steps you need to accomplish? And write them down under one function you will end up calling.

def doesImportantStuff(data):
    step_one_results = stepOnef(data)
    step_two_results = stepTwo(data, step_one_results)
    step_three_results = stepThree(data, step_two_results)

etc. Except name your functions in a way that when you read them again (and you will) you get at least a sense of what is going on.

For example, for my forms I tend to have their own scripting module and they each have a create function. The create function for adding a new customer in my module looks like

TABLENAME='customers'

def create(newData):
	"""
	Creating a new customer.
	Args:
		newData: data dictionary of all the db columns required
	Returns:
		(success, idx) - boolean, int
            boolean - true if successfully saved to database, false if not verified with checker function
            int - id of new record in database from getKey=1
	"""
	validateNew(newData)
	modifyNew(newData)
	sanitizeNew(newData)
	result, newIdx = new_db.query.insert(newData, TABLENAME, getKey=1)
	handleAliases(aliases, newIdx)
	pathDictionary = createFilePath(newIdx)
	success = new_db.query.update(pathDictionary, tableName,newIdx)
	createDirectory(pathDictionary)
	initializeHelpThread(newIdx,userId,pathDictionary)
	templates.navTree.refreshData()
	return result, newIdx 

You don’t need to know what all my functions are, but you can see to create a customer I take the data, I validate it against some criteria, modify it, sanitize it, and then insert it. I do something else with handleAliases, which I can’t recall but if I go to that function I’ll see some function documentation that explains what it does (which sidenote you should document your functions), looks like I create a file path, update the database, create the directory based on the filepath, start a forum thread (something my application has), and then refresh the GUI for the user, before returning the results.

You will need to read code more than you write it so take care to write good code.

I like making the function I am going to call first and then underneath writing my other function calls - before I even necessarily write the function. It’s like building the skeleton first, and then after you can read the functions in order and they tell you a sort of story, then go flesh out those functions.

7 Likes

Very good description of a methodology also known as "top-down design". (:

4 Likes

@bkarabinchak.psi

Thanks for your help, I’m trying to use your method. To be honest, this is my first time defining functions in python- I usually program PLCs. I’ve gotten this far in the code below, but am wondering if I can somehow make a variable number of paths to be entered into the def AggregateMinMaxAvgDev, for example def AggregateMinMaxAvgDev(startTime, endTime, MultipleUnknownNumberOfPathsGoHere, DataSetTag).

I assume there would be a way to make a list or something, but I’m just not sure how.

#print "start script"
DataSetTag = '[default]Folder/Dataset1' #this is the dataset tag where the aggregated data will land, could an internal variable be used instead of actually writing to a tag?
StartTag = '[default]Folder/EndedDateTime'
EndTag = '[default]Folder/StartedDateTime'


def getTimes(StartTag,EndTag): #Finds the beginning and end times of the last product run
	import system
	#print "get times"
	endTime = system.tag.read(StartTag).value
	startTime = system.tag.read(EndTag).value
	return startTime, endTime

def AggregateMinMaxAvgDev(startTime, endTime, DataSetTag): #Aggregates min,max,avg and dev from the tag historian and writes the values to the dataset tag
	import system
	#print "aggregate"
	dataSet = system.tag.queryTagCalculations(paths=[
	'[default]Folder/ActualAbsoluteHeightReal', # how can I move a variable numer of these path these values to the argument interface?
	'[default]Folder/ActualAbsoluteWidthReal',
	],
	calculations=['Minimum','Maximum','SimpleAverage','StdDev'],startDate=startTime,endDate=endTime,ignoreBadQuality=1)

	system.tag.write(DataSetTag, dataSet)
	
startTime, endTime = getTimes(StartTag,EndTag) #invoke getTimes function
#print startTime, endTime
AggregateMinMaxAvgDev(startTime, endTime, DataSetTag) #invoke AggregateMinMaxAvgDev function

Add a paths argument to your function and replace everything between and including the square brackets after paths= in your function with paths. Now you can pass in a variable number of paths to the function.

paths will be a list, so it should have a value enclosed in square brackets, like what you had inside the function:

paths = [path1, path2, path3...]

You can set a paths variable in a line above your function and then reference it in your function call to avoid making the function call line really long.

5 Likes