Start stop of async process?

Hi,

I would like to implement a vision client window with a start button, stop button and a text area.

When the start button is clicked a long running process is started which also uses time.sleep().

How would I start the long runing process without freezing the vision client?

While it's running it needs to update the text area component with its status.

If the stop button is clicked the long running process should break.

How would I go about implementing this?

I've been looking at this example, but cannot get it working:
system.util.invokeAsynchronous | Ignition User Manual (inductiveautomation.com)

Thanks

That's usually a clue that you're doing something wrong.

Please describe the real problem you are trying to solve rather than your solution.

There is a bug in the current version where alarms can get a wrong alarm prioity after a GW restart.

I'm running a script disabling all alarms and then enabling them again to fix the bug and have the GW assign the correct alarm priority to the alarms.

I don't want to disable all alarms at once and then enable them all again at once. I want to do it in chunks and give the GW some time between the operations thus the time.sleep().

You want to use the techniques described in this topic:

(And follow the links in that topic to related discussions.)

Always keep in mind that the only safe thing to do with a Java Swing object in a background thread is to carry it around, to pass back to a short foreground function.

Thank you Phil, almost got it working now with your later.py script.
Only thing I'm missing is the ability to stop the thread?
At the moment i have a window with a 'is_running' bool custom property. I can tooogle it with a start and stop button, but can I get/read it in the thread and make the thread end if it's false?

This is the code of the 'Start' button:

def gw_restart_alarm_prio_fix(text_area, is_running):		
	
	import time
	
	path = '[SCADA]'
	
	folders = []
	res = system.tag.browse(path, filter={'tagType':'Folder'})
	for r in res.getResults():
		fp = str(r['fullPath'])
		folders.append(fp)
	
	ust_bloker_alarmer_list = []
	for folder in folders:
		res = system.tag.browse(folder, filter={})
		for r in res.getResults():
			fp = str(r['fullPath'])
			if 'USTBlokerAlarmer' in fp and fp.split('/')[-1] != 'USTBlokerAlarmer' and 'Puls' not in fp:
				ust_bloker_alarmer_list.append(fp)
	
	anl_list = []
	for ust in ust_bloker_alarmer_list:
		anl = ust.split('_')[0].split(']')[-1]
		if anl not in anl_list:
			anl_list.append(anl)

	delay = 10 # sek
	text = ''
	for anl in anl_list:
		if is_running:  # <= this check doens't seem to work
			ust_list = []
			for ust in ust_bloker_alarmer_list:
				if ust.split('_')[0].split(']')[-1] == anl:
					ust_list.append(ust)
			text += ust_list[0].split('/')[0] + '\n'
			project.later.assignLater(text_area, 'text' , text)
			system.tag.writeBlocking(ust_list, [True]*len(ust_list))
			time.sleep(delay)
			system.tag.writeBlocking(ust_list, [False]*len(ust_list))
			time.sleep(delay)
	
	text += 'done'
	project.later.assignLater(text_area, 'text' , text)
	

# start button
event.source.parent.run = True
event.source.parent.getComponent('TA1_Log').text = 'Running' + '\n'

text_area = event.source.parent.getComponent('TA1_Log')
is_running = event.source.parent.is_running
project.later.callAsync(gw_restart_alarm_prio_fix, text_area, is_running)

Your is_running is a function argument--it is snapshotted when you start the thread. To get an update, you will have to use of the ..Later() functions to run a short script in the UI to retrieve the current value and return it. The future from my function will be completed with that value for you to .get().

Hm, I understand the problem, but I don't understand the solution.
Would you mind explaining it? Possibly using my previously posted code?

I've tried this approach, where <root_containter> is passed as an argument, but still not the right way :

...
def get_is_running():
		return root_container.is_running

delay = 10 # sek
text = ''
for anl in anl_list:
	#if is_running:
	if project.later.callLater(get_is_running, None).get():
		ust_list = []
        ...

That looks close to correct. Show your whole script.

Here is the code in the start button

def gw_restart_alarm_prio_fix(text_area, root_container):		
	
	import time
	
	path = '[SCADA]'
	
	folders = []
	res = system.tag.browse(path, filter={'tagType':'Folder'})
	for r in res.getResults():
		fp = str(r['fullPath'])
		folders.append(fp)
	
	ust_bloker_alarmer_list = []
	for folder in folders:
		res = system.tag.browse(folder, filter={})
		for r in res.getResults():
			fp = str(r['fullPath'])
			if 'USTBlokerAlarmer' in fp and fp.split('/')[-1] != 'USTBlokerAlarmer' and 'Puls' not in fp:
				ust_bloker_alarmer_list.append(fp)
	
	anl_list = []
	for ust in ust_bloker_alarmer_list:
		anl = ust.split('_')[0].split(']')[-1]
		if anl not in anl_list:
			anl_list.append(anl)

	def get_is_running():
		return root_container.is_running

	delay = 10 # sek
	text = ''
	for anl in anl_list:
		#if is_running:
		if project.later.callLater(get_is_running, None).get():
			ust_list = []
			for ust in ust_bloker_alarmer_list:
				if ust.split('_')[0].split(']')[-1] == anl:
					ust_list.append(ust)
			text += ust_list[0].split('/')[0] + '\n'
			project.later.assignLater(text_area, 'text' , text)
			system.tag.writeBlocking(ust_list, [True]*len(ust_list))
			time.sleep(delay)
			system.tag.writeBlocking(ust_list, [False]*len(ust_list))
			time.sleep(delay)
	
	text += 'done'
	project.later.assignLater(text_area, 'text' , text)
	

# start button
event.source.parent.is_running = True
text_area = event.source.parent.getComponent('TA1_Log')
text_area.text = 'Running' + '\n'
root_containter = event.source.parent.getComponent('Root Container')
project.later.callAsync(gw_restart_alarm_prio_fix, text_area, root_containter)

When I run it nothing seems ot happen.

There's probably some typo or logic error that is throwing an exception. Try something like this (moving to a project library script):

from java.lang import Throwable
logger = system.util.getLogger(system.util.getProjectName() + '.' + __name__)

def gw_restart_alarm_prio_fix(text_area, root_container):		
	try:
		_gw_restart_alarm_prio_fix(text_area, root_container)
	except Throwable, t:
		logger.warn("Java error in gw_restart_alarm_prio_fix()", t)
	except Exception, e:
		logger.warn("Java error in gw_restart_alarm_prio_fix()", project.later.PythonAsJavaException(e))

def _gw_restart_alarm_prio_fix(text_area, root_container):		

	import time
...

Your button's actionPerformed would change to just:

event.source.parent.is_running = True
text_area = event.source.parent.getComponent('TA1_Log')
text_area.text = 'Running' + '\n'
root_container = event.source.parent.getComponent('Root Container')
project.later.callAsync(project.something.gw_restart_alarm_prio_fix, text_area, root_container)

You should also get rid import time, and any other use of the jython standard library that doesn't have java alternative. Use the various sleep methods of java.lang.Thread instead of time.sleep().

Ok so I've been tinkering a bit with and it seems to be working now.

For some reason to get values in the async function from passed UI components you need to make a small 'get' function and call that whenever you want the value.
Simply calling <UI_component.value> ie. <root_container.is_running> doesn't work.

This is the working code in the start button :

def gw_restart_alarm_prio_fix(text_area, root_container):		
	
	import time
	
	path = '[SCADA]'
	
	folders = []
	res = system.tag.browse(path, filter={'tagType':'Folder'})
	for r in res.getResults():
		fp = str(r['fullPath'])
		folders.append(fp)
	
	ust_bloker_alarmer_list = []
	for folder in folders:
		res = system.tag.browse(folder, filter={})
		for r in res.getResults():
			fp = str(r['fullPath'])
			if 'USTBlokerAlarmer' in fp and fp.split('/')[-1] != 'USTBlokerAlarmer' and 'Puls' not in fp:
				ust_bloker_alarmer_list.append(fp)
	
	anl_list = []
	for ust in ust_bloker_alarmer_list:
		anl = ust.split('_')[0].split(']')[-1]
		if anl not in anl_list:
			anl_list.append(anl)

	def get_is_running():
		return root_container.is_running

	delay = 10 # sek
	text = ''
	for anl in anl_list:
		if get_is_running():
			ust_list = []
			for ust in ust_bloker_alarmer_list:
				if ust.split('_')[0].split(']')[-1] == anl:
					ust_list.append(ust)
			text += ust_list[0].split('/')[0] + '\n'
			project.later.assignLater(text_area, 'text' , text)
			system.tag.writeBlocking(ust_list, [True]*len(ust_list))
			time.sleep(delay)
			system.tag.writeBlocking(ust_list, [False]*len(ust_list))
			time.sleep(delay)
	
	text += 'done'
	project.later.assignLater(text_area, 'text' , text)
	

# start button
event.source.parent.is_running = True
text_area = event.source.parent.getComponent('TA1_Log')
text_area.text = 'Running' + '\n'
root_containter = event.source.parent
project.later.callAsync(gw_restart_alarm_prio_fix, text_area, root_containter)

The stop button merely sets the root_containers custom property 'is_running' to false which ends the main for loop in the async function causing it to finish quickly.

I've yet to replace the import time and time.sleep() calls, but so far so good.

Not "for some reason", but "not safe to do otherwise". (Some objects will give you property values, until one day your session locks up hard.)

Well I'm no expert at this stuff. I wish I understood it better.

Consider this :

def my_func(ui_component):
	value = ui_component.value # 1
	
	def get_ui_component_value():
		return ui_component.value
	
	value = get_ui_component_value() # 2

To me there is no real difference between #1 and #2. They both ultimately get ui_component.value, #2 is just comming from a get function where #1 is read directly from the component. I don't understand how that can make a difference in the async function?

And earlier in the post you mentioned that passed parameters to the async function could be 'snapshotted' and thus unchanged in the async function - why doesn't that happen with the 'root_container' and 'text_area' too?

You don't need to answer all this, I guess I'm just venting my confusion about asynchronous programming.

Really appreciate your input and efforts :slight_smile:

There isn't a difference in that example. You aren't delegating to a "Later" function that completes a future for you. You absolutely need the later.callLater(...).get() to make the property retrieval happen in the foreground where it is safe.

Calling system.util.invokeLater() without wrapping the callable in a mechanism for a completable future simply gets you a None in value without any wait for the foreground. system.util.invokeLater() doesn't return a value.

Go look at later.callLater() in my later.py script.

1 Like

I've been testing some more with your logger suggestion and it turns out that the error was in this part

...
def get_is_running():
		return root_container.is_running

delay = 10 # sek
text = ''
for anl in anl_list:
	#if is_running:
	if project.later.callLater(get_is_running, None).get():
		ust_list = []
        ...

Where "None" is passed as an argument to a function that takes no arguments.
After fixing this with "if project.later.callLater(get_is_running).get():" it works.

Here is the working code

def gw_restart_alarm_prio_fix(root_container, text_area, delay):
	
	import time
	
	path = '[SCADA]'
	
	folders = []
	res = system.tag.browse(path, filter={'tagType':'Folder'})
	for r in res.getResults():
		fp = str(r['fullPath'])
		folders.append(fp)
	

	ust_bloker_alarmer_list = []
	for folder in folders:
		res = system.tag.browse(folder, filter={})
		for r in res.getResults():
			fp = str(r['fullPath'])
			if 'Puls' in fp:
				ust_bloker_alarmer_list.append(fp)
	
	anl_list = []
	for ust in ust_bloker_alarmer_list:
		anl = ust.split('_')[0].split(']')[-1]
		if anl not in anl_list:
			anl_list.append(anl)

	def get_is_running():
		return root_container.is_running
	def get_text_area_text():
		return text_area.text
	def get_delay():
		return delay.intValue

	text = project.later.callLater(get_text_area_text).get()
	for anl in anl_list:
		if project.later.callLater(get_is_running).get():
			ust_list = []
			for ust in ust_bloker_alarmer_list:
				if ust.split('_')[0].split(']')[-1] == anl:
					ust_list.append(ust)
			text = ust_list[0].split('/')[0] + '\n' + text
			project.later.assignLater(text_area, 'text' , text)
			system.tag.writeBlocking(ust_list, [True]*len(ust_list))
			time.sleep(project.later.callLater(get_delay).get())
			system.tag.writeBlocking(ust_list, [False]*len(ust_list))
			time.sleep(project.later.callLater(get_delay).get())
	
	text = 'Done' + '\n' + project.later.callLater(get_text_area_text).get()
	project.later.assignLater(text_area, 'text' , text)


# start button
event.source.parent.is_running = True
text_area = event.source.parent.getComponent('TA1_Log')
text_area.text = 'Start'
root_containter = event.source.parent
delay = event.source.parent.getComponent('NTF1_Delay')
project.later.callAsync(gw_restart_alarm_prio_fix, root_containter, text_area, delay)
1 Like

It's good that it runs, but it seems you haven't taken the advice to split the bulk into a project library script. Use def something directly in an event script may work in this specific case, but is hazardous in many places.

Ideally, even the last six lines would be in another project library script function. Make your events one-liners that delegate to the library for best long-term maintainability.

1 Like