Scripting error due to binding evaluation delay

Hi everyone,

I am still fairly new to Ignition and I was writing a very basic component property script when I came across this issue:
On the Vision Client the Template I use works well, but usually (not always) gives an error. Looking into the details, and reading various forum topics here, it seems to me that it is a problem that occurs when the script runs before the template parameter has evaluated its binding to the tag (memory or OPC tag). I tried to resolve the problem by utilizing system.util.invokeLater, but it does not solve the problem. I even tried adding a manual delay using the system.util.invokeLater, but it doesn’t always solve the problem (I tried 500mSec which is already a substantial delay, I believe).

First, here is the script:
For reference: ALM_Box_One (TESTING2) --> template, Alarm1_Value --> template parameter bound to a Tag name inside the main window, Alarm1 --> label component

print event.source
print event.propertyName

if event.propertyName in ['Alarm1_Value']:
	print 'correct event type'
	def Alarm_Box():
		# Choice of text and value for "Alarm 1"

		if '_50' in event.source.Alarm1_TagName:
			event.source.getComponent('Alarm1').text='SHORT CIRCUIT'
		elif '_51' in event.source.Alarm1_TagName:
			event.source.getComponent('Alarm1').text='OVERCURRENT'
		elif '_NA' in event.source.Alarm1_TagName:
			event.source.getComponent('Alarm1').text='UNAVAILABLE'
		elif '_27' in event.source.Alarm1_TagName:
			event.source.getComponent('Alarm1').text='UNDERVOLTAGE'
		else:
			event.source.getComponent('Alarm1').text='TYPE MISMATCH'
            
		if event.source.Alarm1_Value:
			event.source.getComponent('Alarm1').foreground=(255,0,0)
		else:
			event.source.getComponent('Alarm1').foreground=(255,255,255)
	system.util.invokeLater(Alarm_Box)
	print 'elaborated if condition'

I will copy here the Vision console. Note that after I launched the project in the Vision Client, it didn’t give any errors. Then when I switched the window and came back, it gave an error almost immediately.

17:55:41.075 [Thread-0] INFO com.inductiveautomation.ignition.client.gateway_interface.GatewayConnectionManager - Updated login state. Logged in? true, Username: TTC, Roles: [Administrator], Security Zones: null
17:55:41.381 [Thread-4] INFO tags.manager.gwinterface - Tag poll rate changed to 250 ms
17:55:41.480 [ClientExecEngine-2] INFO tags.subscriptions - Changing connected quality to 'Good'
17:55:43.273 [Thread-4] INFO com.inductiveautomation.reporting.client.ReportingClientHook - Starting up Reporting Module. Mode: Trial
17:55:43.512 [Thread-4] ERROR Scripting.ScriptManager.OBOB_PMS - Warning: collision at system.util.initialize
17:55:43.904 [AWT-EventQueue-0] INFO vision.App - Starting Up...
ALM_Box_One (TESTING2)
Alarm1_TagName
ALM_Box_One (TESTING2)
Alarm1_Value
correct event type
elaborated if condition
ALM_Box_One (TESTING2)
componentRunning
ALM_Box_One (TESTING2)
instanceName
ALM_Box_One (TESTING2)
vision.bounds2d
ALM_Box_One (TESTING2)
componentRunning
ALM_Box_One (TESTING2)
ancestor
ALM_Box_One (TESTING2)
graphicsConfiguration
ALM_Box_One (TESTING2)
graphicsConfiguration
ALM_Box_One (TESTING2)
ancestor
ALM_Box_One (TESTING2)
componentRunning
ALM_Box_One (TESTING2)
Alarm1_Value
correct event type
elaborated if condition
ALM_Box_One (TESTING2)
Alarm1_TagName
17:56:28.823 [AWT-EventQueue-0] ERROR com.inductiveautomation.factorypmi.application.script.builtin.ClientSystemUtilities - <HTML>Error running function from <code>fpmi.system.invokeLater</code>
com.inductiveautomation.ignition.common.script.JythonExecException: Traceback (most recent call last):
  File "<event:propertyChange>", line 16, in Alarm_Box
TypeError: 'NoneType' object is not iterable

	at org.python.core.Py.TypeError(Py.java:265)
	at org.python.core.PyObject.__iter__(PyObject.java:900)
	at org.python.core.PyObject$1.iterator(PyObject.java:912)
	at org.python.core.PyObject.object___contains__(PyObject.java:1786)
	at org.python.core.PyObject.__contains__(PyObject.java:1782)
	at org.python.core.PyObject._in(PyObject.java:1762)
	at org.python.pycode._pyx0.Alarm_Box$1(<event:propertyChange>:30)
	at org.python.pycode._pyx0.call_function(<event:propertyChange>)
	at org.python.core.PyTableCode.call(PyTableCode.java:171)
	at org.python.core.PyBaseCode.call(PyBaseCode.java:308)
	at org.python.core.PyFunction.function___call__(PyFunction.java:471)
	at org.python.core.PyFunction.__call__(PyFunction.java:466)
	at org.python.core.PyFunction.__call__(PyFunction.java:456)
	at org.python.core.PyFunction.__call__(PyFunction.java:451)
	at com.inductiveautomation.ignition.common.script.ScriptManager.runFunction(ScriptManager.java:788)
	at com.inductiveautomation.factorypmi.application.script.builtin.ClientSystemUtilities.lambda$invokeLater$1(ClientSystemUtilities.java:276)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.run(Unknown Source)
Caused by: org.python.core.PyException: Traceback (most recent call last):
  File "<event:propertyChange>", line 16, in Alarm_Box
TypeError: 'NoneType' object is not iterable

	... 29 common frames omitted
ALM_Box_One (TESTING2)
Alarm1_Value
correct event type
elaborated if condition
ALM_Box_One (TESTING2)
Alarm1_TagName

Wrap it in a Try/Except and it will ignore the early binding error.

if event.propertyName in ['Alarm1_Value']:
	print 'correct event type'
	def Alarm_Box():
		# Choice of text and value for "Alarm 1"
            try:
		if '_50' in event.source.Alarm1_TagName:
			event.source.getComponent('Alarm1').text='SHORT CIRCUIT'
		elif '_51' in event.source.Alarm1_TagName:
			event.source.getComponent('Alarm1').text='OVERCURRENT'
		elif '_NA' in event.source.Alarm1_TagName:
			event.source.getComponent('Alarm1').text='UNAVAILABLE'
		elif '_27' in event.source.Alarm1_TagName:
			event.source.getComponent('Alarm1').text='UNDERVOLTAGE'
		else:
			event.source.getComponent('Alarm1').text='TYPE MISMATCH'
            
		if event.source.Alarm1_Value:
			event.source.getComponent('Alarm1').foreground=(255,0,0)
		else:
			event.source.getComponent('Alarm1').foreground=(255,255,255)
	except:
               pass
        system.util.invokeLater(Alarm_Box)
	print 'elaborated if condition'
1 Like

Thank you for your prompt response.
I tried your solution and it works. However, I feel it’s more like a workaround than a real solution, basically just ignoring the error :))

My main question now is: is there anyway to delay the running of the script until all the bindings and template parameters have been evaluated?
(system.util.invokeLater() doesn’t seem to do the trick, maybe I misunderstand its precise purpose)

I have a template where I actually have to run the original code for 6 different components instead of just one. So, I developed it further, using your advice, and it works now without any errors, but only with a forced delay in the invokeLater I use.
If I don’t use that delay, the event triggers the script before the new changed value is transferred from the tag provider (memory tag) to the template parameters, and that results in an Alarm Box that is always one alarm change behind.

Any further ideas?

For further reference, I will attach my code here: (N.B.: “Alarms” is an internal property of type dataset that I had to define and bind to the template properties to enable me to use the for loop)

if event.propertyName in ['Alarm1_Value', 'Alarm2_Value', 'Alarm3_Value', 'Alarm4_Value','Alarm5_Value', 'Alarm6_Value']:
	import timeit
	starttime=timeit.default_timer()
#	print 'correct event type; ', event.propertyName    #for diagnostics 
	def Alarm_Box():
#		# Choice of text for "Alarm#"
		for i in range(event.source.Alarms.getRowCount()):
			try:    #to sidestep the early binding error issue
#				print event.source.Alarms.getValueAt(i, 0)  #for diagnostics 
				if '_50' in event.source.Alarms.getValueAt(i, 0):
					event.source.getComponent('Alarm'+str(i+1)).text='SHORT CIRCUIT'
				elif '_51' in event.source.Alarms.getValueAt(i, 0):
					event.source.getComponent('Alarm'+str(i+1)).text='OVERCURRENT'
				elif '_NA' in event.source.Alarms.getValueAt(i, 0):
					event.source.getComponent('Alarm'+str(i+1)).text='UNAVAILABLE'
				elif '_27' in event.source.Alarms.getValueAt(i, 0):
					event.source.getComponent('Alarm'+str(i+1)).text='UNDERVOLTAGE'
				else:
					event.source.getComponent('Alarm'+str(i+1)).text='TYPE MISMATCH'
			except:
				pass
#		# Choice of colour for "Alarm#"
		for j in range(event.source.Alarms.getRowCount()):
#			try:
#				print event.source.Alarms.getValueAt(j, 1)  #for diagnostics 
				if event.source.Alarms.getValueAt(j, 1):
					event.source.getComponent('Alarm'+str(j+1)).foreground=(255,0,0)
				else:
					event.source.getComponent('Alarm'+str(j+1)).foreground=(255,255,255)
#			except:
#				pass
	system.util.invokeLater(Alarm_Box, 1000) #to resolve the early binding error, must manually add enough delay to give time for new values to be evaluated inside the template before the script runs
	print 'The elaborated time is: ', timeit.default_timer() - starttime    #performance benchmark
	print 'elaborated if condition for event: ', event.propertyName, 'for template', event.source #for diagnostics 
#

I would use a python set() at the top level of a script module to act as a memory of the property changes you’ve encountered. At each property change within your list, add the property name to the set. When the set length matches your list of properties, you’ve received values for all of them.

(FWIW: Tag and query bindings run on asynchronous background threads because their round-trip to the gateway can be disruptive to a gui. That puts their data delivery outside the scope of system.util.invokeLater(), which runs the given function when the GUI thread is available.)

@pturmel
First of all, thank you for your prompt reply.
Sorry for the delay in my response, only now have I been able to read your suggestion. I think I didn’t explain my target well, as I actually need to push any change in any of the tags bound to the template to be visualized right away on the Vision Window. I cannot wait for all of them to happen before I visualize this change.

I have actually found out the root cause of my problem. When I implemented the for loop to go through all label components inside the template, I still checked for the individual “property change” events, which are all template parameters. My script with for loop actually depends on an internal parameter, a dataset, that is filled by the template parameters.
So, when I changed the property change check at the head of the script to look for changes in the internal parameter instead of the template parameters, I could actually remove the manual delay in system.util.invokeLater(), and the script worked perfectly.

So, my conclusion is that internal parameters are refreshed with a time delay compared to the bindings of the template parameters. Is my conclusion correct? Also, just out of curiosity, do they run on different threads?

Thank you all again for you help.

I will include my final script here for reference:

if event.propertyName in ['Alarms']: # Property Change trigger for script: 'Alarms' Dataset internal property is used, which includes the alarms TagNames and Values
#	import timeit	#performance benchmark
#	starttime=timeit.default_timer()	#performance benchmark
#	print 'correct event type; ', event.propertyName	#for diagnostics 
	def Alarm_Box():
#		# Choice of text for "Alarm#"
		for i in range(event.source.Alarms.getRowCount()):
			try:	#to sidestep the early binding error issue
#				print event.source.Alarms.getValueAt(i, 0)	#for diagnostics 
				if '_50' in event.source.Alarms.getValueAt(i, 0):
					event.source.getComponent('Alarm'+str(i+1)).text='SHORT CIRCUIT'
				elif '_51' in event.source.Alarms.getValueAt(i, 0):
					event.source.getComponent('Alarm'+str(i+1)).text='OVERCURRENT'
				elif '_NA' in event.source.Alarms.getValueAt(i, 0):
					event.source.getComponent('Alarm'+str(i+1)).text='UNAVAILABLE'
				elif '_27' in event.source.Alarms.getValueAt(i, 0):
					event.source.getComponent('Alarm'+str(i+1)).text='UNDERVOLTAGE'
				else:
					event.source.getComponent('Alarm'+str(i+1)).text='TYPE MISMATCH'
			except:
				pass
#		# Choice of colour for "Alarm#"
		for j in range(event.source.Alarms.getRowCount()):
#			try:
#				print event.source.Alarms.getValueAt(j, 1)	#for diagnostics 
				if event.source.Alarms.getValueAt(j, 1):
					event.source.getComponent('Alarm'+str(j+1)).foreground=(255,0,0)
				else:
					event.source.getComponent('Alarm'+str(j+1)).foreground=(255,255,255)
#			except:
#				pass
	system.util.invokeLater(Alarm_Box) #to resolve the early binding error, can manually add delay if needed
#	print 'The elaborated time is: ', timeit.default_timer() - starttime	#performance benchmark
	print 'elaborated if condition for event: ', event.propertyName, 'for template', event.source	#for diagnostics 

Not to throw a big wrench in the works here, but have you looked at the Style Customizer functionality? It seems to me that for what you’re wanting to do (pivoting on a value and driving .text and `.foreground), the Style Customizer would be the first stop and allow you to avoid all of the scripting. Something to at least consider if you weren’t aware of this functionality.

@Kevin.Collins
When I started writing the script I didn’t know if the possibility. But then when I checked the Styles inside the Label component, I couldn’t find a way to change the text of a Label component based on a certain substring in the bound Tag Name. As far as I understand, you cannot write an expression inside the cell bindings of the “Cell Update” tab inside Styles.
For changing colors only, sure, it is handy. But since I was considering the text as well, I thought to include everything inside the script, for comfort and ease of possible future modifications.

No, there’s no extra delay, beyond the queuing of changes from async (tag/query bindings) to foreground (gui thread). Your internal dataset parameter presumably references other properties in its binding–when the referenced properties change, the binding is triggered. If a query binding, that will run in a background thread until its result arrives from the gateway, triggering its own property change.

In other words, it isn’t whether a property is internal to the template or not, but whether its binding executes asynchronously. Gateway round-trips running asynchronous is good, in case it isn’t clear, as the GUI would freeze during the delay otherwise. Even very short freezes make the GUI “choppy”.

@pturmel
Thank you very much for your valuable feedback. It’s always nice to understand how the system works behind the scenes.