This actionPerformed button script causes the next rightmost component to be set active after it is clicked, and I can't figure out why

I tried this on both a button and momentary button:

def async():
	# mode == relative movement, value == output pulses, field == output pulses
	value = event.source.parent.getComponent('ntf_relative_move').intValue
	mode = 2  # relative running
	field = 313  # set output pulse
	
	# Get to "SET" mode
	state = 40
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_cmdTrig'], [False])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_setState'], [state])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_cmdTrig'], [True])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_cmdTrig'], [False])
	
	# Write output pulses
	print("Changing output pulse:", field, value)
	system.tag.writeBlocking([
	    '[PROJ]TB0/dmoco0_setPField',
	    '[PROJ]TB0/dmoco0_setPValue'],
	    [field, value])
	
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_setPTrig'], [False])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_setPTrig'], [True])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_setPTrig'], [False])
	
	# Get back to "READY"
	state = 30
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_cmdTrig'], [False])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_setState'], [state])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_cmdTrig'], [True])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_cmdTrig'], [False])
	
	# Trigger movement
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_setCMode'], [mode])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_setCTrig'], [False])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_setCTrig'], [True])
	system.tag.writeBlocking(['[PROJ]TB0/dmoco0_setCTrig'], [False])
	
	def reEnable():
		event.source.enabled = True
	system.util.invokeLater(reEnable)

event.source.enabled = False
system.util.invokeAsynchronous(async)

I can't for the life of me figure out why this is the case. I have moved the text field to the right of this button away and it then engages with the next textfield below where the prior one was. This behavior persists even if the text fields are disabled (uneditable), and it stops them from updating their contents (they are bound to some tags that update after this butto is pressed).

Interacting with the client from machine A shows proper behavior on machine B but not proper behavior on machine A.

Any help would be appreciated.

Before clicking:
image

After clicking (expected behavior would be to see Position -14000, which I can see on another device):
image

After rearranging the text fields (to demonstrate it is related to proximity / scan order on the screen) and clicking Move:
image

The Move button unblocks shortly after due to the async nature of my request, this is working as intended. The tag that the field next to the Position label is tied to is not referenced in the script attached to the button. Neither is the one below it, which triggers when I move that box.

I'm not sure what else is going on, but you are accessing event.source's methods and then component properties within your asynchronous script. Not safe, not even just to read. They only thing that is safe to do with Swing objects in the background is pass them around.

It seems fairly straightforward to me?
You're disabling the current focused component, which therefore can't be focused, so the focus traversal system is moving focus to the 'next' element in line.

You could just call requestFocusInWindow() on the button (on the EDT) after you're done doing whatever you need to do.

Also, for the sake of performance, you really should batch your writes.

I have to do this in an async function so that it doesn't lock the GUI, because it needs to confirm writes in order to an external system.

Not safe, not even just to read. They only thing that is safe to do with Swing objects in the background is pass them around.

I don't know what any of this means, but I'd like to

That does sound straightforward -- I was previously entirely unfamiliar with focus traversal system and any of its functionality. However, I am not sure why it is allowed to apply focus to an explicitly disabled component. Anyway, I will try this. Do you suggest I put it at the very bottom of the async() method or immediately after the system.util.invokeAsyncronous(async)?

I tried batching my writes, it seemed to misbehave -- I was not getting the behavior that I expected. I think its actually a problem with the device that I am trying to control, specifically. I can try again, it is quite slow as is.

Retrieve the .intValue from the sibling component before calling the async function--pass it as an argument to your async function. (.invokeAsynchronous() supports that now-a-days, or you can use a default function argument.)

Everything else is safe. event (and therefore event.source, too) is included in the closure, but you aren't touching that until you are back on the EDT.)

??

You aren't checking any return values from system.tag.writeBlocking(). So you aren't confirming anything. :man_shrugging:

Sheesh. You are writing different values to the same tags back-to-back. What makes you think the logic in the target device will see the changed value in its own program scan in such a brief window?

Yeah, I was in the middle of doing some rewriting when I saw that pattern and stopped.

@sherman, this 'jogging' of a particular tag for a short period of time should probably be done in the PLC; as is, what happens if the Vision client disconnects from the gateway while setCTrig is True? The False write that's sequential in the script won't make it to the PLC and you'll end up stuck on. The # Trigger movement comment has me very concerned about safety implications. Consider searching this forum for the (many) discussions on the pitfalls of momentary buttons in any HMI/SCADA system.

1 Like

Retrieve the .intValue from the sibling component before calling the async function

I can do that, thanks for the pointer.

You aren't checking any return values from system.tag.writeBlocking() . So you aren't confirming anything. :man_shrugging:

Sorry, I meant confirm the order of the writes. I was told that I can't trust the order of the writes if I make multiple asyncronous calls. So I made many individual calls inside a single asynchronous function.

Sheesh. You are writing different values to the same tags back-to-back. What makes you think the logic in the target device will see the changed value in its own program scan in such a brief window?

Most of the evidence that I have indicates that the PLC is faster than Ignition, and individual calls to writeBlocking seems to be sufficiently slow that writing sequentially is caught in each scan.

This was a development iteration of an exceptionally stubborn motor controller, that is not likely to be kept through to production. It is not the final state of the device, for sure. The setCTrig register was a workaround in place to make up for the fact that in some places the developers of the motor controller use an entire holding register to set a parameter, and in others they use a single field with enumerated options and latch settings. I consolidated all of this into a logic block in the PLC that executes on the leading edge of this trigger. Since the device was so problematic, I am doing the jogging in ignition so that I can iterate on its functionality faster (the documentation was sufficiently poor that we had to literally test its capabilities to find out what the various functions do) and I am a much faster and more competent python programmer than I am a PLC programmer. This pattern gave me the most consistent behavior during this phase, the hiccup being the original reason for this post which was to try to understand how Ignition's GUI was moving around.

I am not worried about Ignition disconnecting during this phase of testing because if it does so we will know immediately and stop testing. This device does not have a jog function, the movement is absolute/relative pulse count based and once it has a command to move it does not require a command to stop. It is also not in a position where it could harm anyone, it is a very small linear actuator that is moving an electrode up and down inside of a double walled chamber. I do appreciate the concern, safety is at the top of our priority list.

2 Likes

Also, this script is the way that it is because I originally wrote it without asynchronicity in mind, but because it takes a second or so to complete it was locking my gui thread. I was instructed to invokeAsyncronous, so I patched it in.

When you say "safe" everywhere, do you mean threadsafe? Memory safe? Order-of-operations safe? I am learning that there are definitely different dangerous things to do (running an unlimited SQL query will crash the entire gateway, for instance)

These, and "crash your client JVM" safe. Java swing is not thread safe at all and many "read" operations make multiple accesses of internal fields, that might be changed by another thread if not singulated. With catastrophic consequences for the UI. Rare race conditions make for headscratchers when the crash comes.

I see. I wish I knew more about Java so that I could program around it's peculiarities better.

I think java is relatively simple--if it isn't documented to be thread-safe, it isn't. And then for some things, like java Swing, it is emphasized by explicitly declaration to not be thread-safe.

I think there is the additional issue that I do not know how Ignition distributes itself across threads. And I have only just learned that Java Swing is their GUI toolkit -- like I said I don't normally exist in a java-heavy ecosystem.

While this would be nice to know, it is probably enough to know that any time you need to touch something in the gui from an asynchronous thread, it needs to be done with system.util.invokeLater. That said, there is no possible way that the scan time on your PLC is fast enough to pick up these tag write pulses.

I see nothing in the code sample provided that is related to changing the position field.

That's probably not entirely true. Message processing request-response round trip varies greatly by processor models and workload--if the round trip is longer than the scan time, it will likely be seen. If the r/t time is much less than the scan, probably not. With random success for variations in between.

(Look at a device connection's diagnostics to get a feel for typical round-trip times in the situation at hand.)

I'm talking about scan time. With this code, the bit is likely going to change three or four times within the same scan, so what is that supposed to accomplish? I don't see how that could be reliable.

The bits will only change as fast as the round-trip message time to that PLC. (writeBlocking() blocks until the PLC acknowledges the write.) Which is why it is a maybe.