Best Practice for Aggregating Multiple OPC Tags into a Single Memory Tag Without Asynchronous Update Conflicts using a Mitsubishi Driver

Hey all — first-time post here, hoping to get some insight into a workflow challenge I'm facing with data aggregation in Ignition Edge.

I'm working on an Ignition Edge Gateway with a local Ignition OPC UA server subscribed to a Mitsubishi iQ-R PLC. The OPC connection is stable, and I've successfully created individual OPC tags from the device registers. These tags represent serial identifiers and a variety of measurement values. I’m trying to combine them into a structured JSON memory tag and store that value once per cycle when the PLC updates the relevant memory block.

The PLC executes a block move to simultaneously populate the memory area for all values. However, in Ignition, I’m observing asynchronous updates across tags. Even though the data is loaded into the PLC memory space at once, the tags in Ignition are updating with delays between them. This results in my JSON expression tag updating multiple times per cycle, with partial or duplicated data, as each OPC tag refreshes individually.

I initially created a string expression tag to format the data into a JSON object:

"{" +
"\"Tag1\":\"" + {[.]../Tag1} + "\"," +
"\"Tag2\":\"" + {[.]../Tag2} + "\"," +
...
"\"Tag_n\":\"" + {[.]../Tag_n} + "\"" +
"}"

This worked structurally, but it updated every time any one of the source tags changed, leading to partial entries or duplicates when historized.

To control write timing:

  • I added an onChange script to each source tag that increments a memory tag (Collection Trigger).
  • When the trigger reaches the expected count (i.e., number of JSON fields), a script reads all tags and writes a final JSON string to the output tag.

This reduced the frequency of writes but failed when one or more tag values didn't actually change (i.e., no onChange fired), causing the count to never reach its threshold and blocking output entirely, so I scrapped this setting.

To mitigate duplicates and race conditions:

  • I used a delayed onChange approach where the expression tag only writes to the output memory tag if the serial number in the current value is different from the previous value.

This method improved consistency but still led to occasional mismatches or stale values depending on tag refresh timing.

Is there a best-practice approach in Ignition (e.g., scripting, tag group settings, or gateway logic) to ensure all tags are fully updated before writing? Could it be on the PLC-side or a driver setting?

Thanks in advance — happy to share further examples or testing results if helpful.

Example Settings



Use scripting to issue a single system.opc.readValues for all the tags you're interested in.

3 Likes

One way to set this up:

  • have a "trigger" tag (looks like you may have this already).
  • have a "reset" tag ("Resultant Tag"? not sure if that's what this is for)
  • use a gateway timer script to periodically read the trigger tag
    • when it's set, issue the system.opc.readValues
    • assemble your JSON object and write it to a document tag
    • write to the "reset" tag to tell the PLC you're done. it should either increment or reset the trigger tag. Never write to the trigger tag from Ignition.
1 Like

This could also be a gateway tag change event script (not tag event script), instead of a timer script, and then the trigger just needs to be toggling or incrementing.

The point was to avoid a tag event script, in which you shouldn't do long-ish running blocking work like a system.opc.readValues call.

3 Likes

Didn’t fully understand the advice since I'm new to the platform, but after digging into the scripting docs this morning, it finally clicked. Just wanted to follow up and share how I got it working in case it helps someone else.

I went with an onChange script since the serial number tag update is my trigger. When that happens, I use system.opc.readValues inside the Collection Trigger tag to pull raw data from the PLC.

Here’s an example of the solution I implemented using Kevin's advice:

def valueChanged(tag, tagPath, previousValue, currentValue, initialChange, missedEvents):
	from com.inductiveautomation.ignition.common.model.values import QualityCode
	
	try:
		opc_server = "Ignition OPC UA Server"
		base_address = 0
		end_address = 100
		word_count = end_address - base_address + 1
	
		# Build address list
		addresses = ["ns=1;s=[Mitsubishi_iQR_Ethernet]D{}".format(base_address + i) for i in range(word_count)]
		raw_data = system.opc.readValues(opc_server, addresses)
	
		# Convert to byte array
		byte_array = []
		for val in raw_data:
			if val.quality.isGood():
				word = int(val.value)
				byte_array.append(word & 0xFF)
				byte_array.append((word >> 8) & 0xFF)
	
		# Calculate byte offset from D register
		def word_offset(reg): return (reg - base_address) * 2
	
		# Define fields with offsets and lengths
		fields = [
			("A", word_offset(0), 5),
			("B", word_offset(5), 1),
			("C", word_offset(6), 12)
		]
	
		# Build dictionary to convert to JSON
		result = {}
		for label, offset, length in fields:
			chars = []
			for i in range(length):
				byte_index = offset + i
				if byte_index >= len(byte_array):
					break
				char = chr(byte_array[byte_index])
				if char == '\x00':
					break
				chars.append(char)
			result[label] = ''.join(chars)
	
		# Convert to JSON string
		json_blob = system.util.jsonEncode(result)
	
		# Write to memory tag
		system.tag.writeAsync(["[.]Dataset"], [json_blob])
	
	except Exception as e:
		system.tag.writeAsync(["[.]Dataset"], ["Error parsing"])

Let me know if you see any areas that could be improved.

Thanks for the support!

As Kevin mentioned before, put this in a Gateway Tag Changed event. Don't have it on the tag's value changed event, you're risking locking your entire tag system.

To make things simpler as well, put the main logic in a function in a project library script, and call that function from the Gateway Tag Change event.

2 Likes

Thanks for the heads up!

I moved the onChange behavior to a Gateway Event Tag Change Script that calls a function from the Scripting Project Library folder now.

Can you elaborate on this?

1 Like

The tag system on Ignition runs on 3 threads by default. This includes distributing new values to tags, running expressions on expression tags, and tag script events.

If you trigger any blocking work, it blocks the assigned thread until it completes, meaning it cannot work on anything else. If you have a lot of tag value change events or manage to queue a blocking script onto all 3 threads, your tag system halts until the blocking work completes on one of those threads and it is available for work again.

Not an issue if you block for a ms or 2, but if your blocking call is waiting on a timeout of 60s or similar, you've just locked your entire tag system for that period of time.

This is why its recommended that any tag value change scripts made should run in <10ms. Gateway Tag Change events run in a separate pool, and can be configured to spawn their own dedicated thread, so they don't risk blocking the tag system.

3 Likes