Update GUI via Asynchronous thread

Opened the file with a tiny hope in my heart that there would be an invokeLater that can be awaited inside an async thread :wink:

jython2/python2 doesn’t have await. Asyncio and general threading don’t play well together. When it becomes availably in jython (if it ever gets to 3.x), don’t be surprised to find it forbidden in any foreground thread.

I don’t want to await in the UI thread, I wan’t to be able to await a function called from and inside an async thread.

Example:

def asyncInvoked():
	
	def laterInvoked():
		system.gui.messageBox("Please press OK")

	system.util.invokeLater(laterInvoked).await()

	# Do more stuff after Ok is pressed.

system.util.invokeAsynchronous(asyncInvoked)

Coroutines run in the same thread they are called from, so even if asyncio was available, you aren’t going to be able to await in a background thread for a coroutine to run in a gui thread.

If you absolutely have to pass something back from the UI thread, supply a future for the GUI thread to complete and the async thread to .get().

OK. I’ve poked myself with the futures idea. Grab the latest version of later.py, then try this:

def asyncInvoked():
	# do something in the background
	shared.later.callLater(system.gui.messageBox, "Please Press OK").get()
	# do something after OK is pressed

system.util.invokeAsynchronous(asyncInvoked)

Note the use of .get() to wait for system.gui.messageBox to complete.

This updated later.py includes an emulation of invokeLater for gateway scope. Untested and unsupported!

Edit: Realized the invokeLater emulation should only use one thread. Fixed.

3 Likes

Raises an error: AttributeError: 'NoneType' object has no attribute 'get'
Think the later.py file is the same as the later_old.py file
`

Refresh your cache. They are definitely different files.

That was actually the problem. Thank you

The callLater function works great. There is just a minor bug when a Python exception is thrown.
I made the following workaround (and for all the other functions in the later.py file aswell), don’t know if you can come up with something better.

def callLater(func, *args, **kwargs):
	future = CompletableFuture()
	def callLaterInvoked(f=func, a=args, kw=kwargs):
		try:
			future.complete(f(*a, **kw))
		except java.lang.Exception, e:
			future.completeExceptionally(e)
		except Exception, e:
			future.completeExceptionally(java.lang.Exception(str(e)))
	invokeLater(callLaterInvoked)
	return future

Hmm. Will play with this later. PyException extends java's RuntimeException, so I thought it would work. /-:

The linked version of later.py has been updated to wrap python exceptions to (mostly) retain their traceback when captured by the utility functions and delivered to a future.

1 Like

I apologize for my gross ignorance of this topic. I am self-taught in Python due to Ignition. What should I do with later.py?

I have downloaded it (from the Automation Pros website), created a script called “later” in the shared script library and copied and pasted the contents of later.py into it. Two things are confusing me:

  1. Phil’s comment of “refresh your cache”. This implies (to me at least) that later.py is NOT just a shared script in the script library since presumably any changes to it would be available as soon as I hit the commit button.

  2. The syntax of Phil’s earlier post:

# In a script module in your project:
import shutil
def fileTransfer(label):
	path = system.file.openFile()
	newPath = "F:\NewFolder"
	shutil.copy2(path, newPath)
	shared.assignLater(label, "text", "Step one done")

# In your button event 
label = event.source.parent.getComponent("Label")
shared.later.callAsync(project.module.fileTransfer, label)

I would have thought that the syntax would be:

shared.later.assignLater(label, "text", "Step one done")

Again, I apologize for my ignorance.

  1. That comment was directed solely to Claus, in regards to him downloading an updated version of later.py from my public website.

  2. You are correct. That was a typo.

  3. (You didn’t ask, but…) The usage of system.file.openFile() should be in the event, prior to the callAsync(), as it opens a dialog for picking a file.

2 Likes

Ah. The comment about system.file.openFile() makes sense. So the example should read something more like this?

# In a script module in your project:
import shutil
path = system.file.openFile()
def fileTransfer(label,path):
	newPath = "F:\NewFolder"
	shutil.copy2(path, newPath)
	shared.later.assignLater(label, "text", "Step one done")

# In your button event 
label = event.source.parent.getComponent("Label")
shared.later.callAsync(project.module.fileTransfer, label,path)

Just in case any of my fellow noobs need something to copy and paste.

1 Like

I’m not sure I understand asynchronous threads correctly. I have the following in the action performed event of a button on a GUI.

def overwrite_database():
    try:
	    #Record event.
	    backend.messageHandler('HMI', 'info', 'HMI.overwrite_database()')
	
	    tbl = event.source.parent.parent.getComponent('tbl_offline')
	    data = tbl.data
	    backend.overwrite_database(data)
    #--------------------ERROR HANDLING--------------------
    except java.lang.Exception:
	    backend.messageHandler('HMI', 'info', 'Error location: overwrite_database()' + \
								 		      '\nError type: ' + str(sys.exc_info()[0]) + \
								 		      '\nError message : ' + str(sys.exc_info()[1]))
    except Exception:
	    backend.messageHandler('HMI', 'info', 'Error location: overwrite_database()' + \
							 		  	      '\nError type: ' + str(sys.exc_info()[0]) + \
							 		  	      '\nError message : ' + str(sys.exc_info()[1]))
    #--------------------ERROR HANDLING--------------------

system.util.invokeAsynchronous(overwrite_database)'

And then the function backend.overwrite_database(data) is called below from a script:

def overwrite_database(data):
    try:
	    #Record event.
	    backend.messageHandler('backend', 'info', 'backend.overwrite_database(data[...])')
	
	    ##########
	    #do some database related stuff
	    ##########

	    if i % 100 == 0:
		    strVal = strVal.rstrip(', ')
		    sql = strQry + strVal + ';'
		    args = []
		    system.db.runPrepUpdate(sql, args, db)	
				
		if (lastRow - 100) < 1:
			pass
		else:
			lastRow = lastRow - 100
			strQry = "INSERT INTO SCADA.flatFile (line, verName, modelDescrip, menGrp, staNo, seq, workCell, t_Sec, mCode, description, key_Point, quality_Chk, partNo, partName, scan, scan_req, pic, videoFileName) VALUES "
			strVal = ""	
				
		    new_Progress = ((float(max_new) - float(min_new))/(float(max_old) - float(min_old)))*(float(i)-float(max_old))+float(max_new)
		    future = InductionAutomation.assignLater(window.getRootContainer().getComponent('progressBar'), "value", int(new_Progress))
	    new_Progress = 100
	    future = InductionAutomation.assignLater(window.getRootContainer().getComponent('progressBar'), "value", int(new_Progress))

    #--------------------ERROR HANDLING--------------------
    except java.lang.Exception:
	    backend.messageHandler('backend', 'info', 'Error location: backend.overwrite_database()' + \
											      '\nError type: ' + str(sys.exc_info()[0]) + \
											      '\nError message : ' + str(sys.exc_info()[1]))
    except Exception:
	    backend.messageHandler('backend', 'info', 'Error location: backend.overwrite_database()' + \
										 	      '\nError type: ' + str(sys.exc_info()[0]) + \
										 	      '\nError message : ' + str(sys.exc_info()[1]))
    #--------------------ERROR HANDLING--------------------

And this thread, of which ive edited some parts out, calls a function I believe you created pturmel.

def assignLater(comp, prop, val, ms = 0):
    future = CompletableFuture()
    def assignLaterInvoked():
        try:
            try:
                setattr(comp, prop, val)
            except AttributeError:
                comp.setPropertyValue(prop, val)
            future.complete(None)
        except java.lang.Exception, e:
            future.completeExceptionally(e)
        except Exception, e:
            future.completeExceptionally(PythonAsJavaException(e))
    invokeLater(assignLaterInvoked, ms)
    return future

So is the last function sending my GUI calls back to the GUI from the asynchronous thread? In my original action performed event I call components on the GUI from an asynchronous thread. Am I leaving myself open for a world of hurt at some point? I’ve heard you mention to never ever do that but I’ve found no problems with the above code. It actually works really well and looks like a proper progress bar updating from 0 to 100.

Any more thoughts are appreciated.

Updating properties from an asynchronous thread will work. Until it doesn’t, and leaves your client deadlocked. That’s why you always have to call from the main thread.

2 Likes

Yes. And it isn't enough to just avoid writes. You must also avoid property reads. Property reads are really method calls and can have side effects. Only component methods and properties that are carefully designed to be accessible concurrently (written in java, not in jython) are safe in an asynchronous thread. The only thing you can do with a component in a background thread is to hold onto it for later passing back to the UI thread.

You risk client crashes and data corruption.

1 Like

You have peaked my curiosity. If interacting with the GUI from an asynchronous thread is so bad then why allow it?

I originally made the function asynchronous because it took longer than 1/10th of a second of GUI time (which you recommended somewhere else pturmel as a rule of thumb). And the function took so long I wanted to add a progress bar for the user. So is it impossible to update the progress of an asynchronous thread at this time? Any future plans for Ignition to allow that behavior? I’ve seen this functionality in other software so i don’t think it’s a novel idea.

Thanks for your thoughts. Always a pleasure to hear them.

1 Like

Besides getting the data from the table I think i'm following all processes correctly in my above example. Instead of using system.util.invokeLater() I use the assignLater you mention from later.py.