Numeric entry with unit conversion (bidirectional) question

I’m working on numeric entry template that will automatically switch between metric and imperial, but only write imperial values to the PLC. Here are the template’s custom properties and parameters:

the key property here is the “value”. It is an indirect binding to the tag specified in the tagPath parameter. The processType parameter is an enumeration that gets passed into my conversion functions to determine the correct math to use. So for instance, we’ve standardized on using psi for pressure in imperial units and mbar in metric units. So when you pass “pressure” to my function, it multiplies or divides by 68.9476. If you pass “level” it does the math with the 0.3048 factor between feet and meters. Here are the functions for reference:

def convertToMetric(value, processType):
	# Converts an input value to metric units according to the process type.
	# Typically used for reading values FROM the PLC and converting for vizualization
	# Acceptable process types: pressure, flow, level, temp
	if processType == 'pressure':
		newValue = value * 68.9476
	elif processType == 'flow':
		newValue = value * 0.227
	elif processType == 'level':
		newValue = value * 0.3048
	elif processType == 'temp':
		newValue = (value - 32) * 0.55556
	else:
		newValue = 'Invalid process type'
	return newValue

def convertToImperial(value, processType):
	# Converts an input value to imperial units according to the process type.
	# Typically used for converting visualized values before writing TO the PLC.
	# Acceptable process types: pressure, flow, level, temp
	if processType == 'pressure':
		newValue = value / 68.9476
	elif processType == 'flow':
		newValue = value / 0.227
	elif processType == 'level':
		newValue = value / 0.3048
	elif processType == 'temp':
		newValue = (value * 1.8 ) + 32
	else:
		newValue = 'Invalid process type'
	return newValue

What I want is a numeric entry that, in metric mode, displays and accepts input in metric units, but converts those to imperial before writing to the PLC, while keeping the displayed value in metric. So it seems like a binding between my numeric entry’s value property and the custom property is out.

What I’ve done so far is first, create a script on the startup event for my numeric entry field that will read the value of the view.custom.value which is bound to the tag, and then, if necessary, do the conversion and write it to the object’s value.

That should get things initialized at least.

On my numeric entry’s value property, I have a change script attached:

It’s basically doing the other conversion script (to Imperial) if I have metric selected, and if not, just passing the raw value to it.

But, I have an issue.

If the value of the tag changes by any other means, the value in my input box will not change! I need to keep that synchronized, but without using a binding of some kind, the only other method I know is by using property change scripts. But if I put a property change script on view.custom.value as well as the value property on my object that writes to it with ITS property change script, what I’m worried about is the following sequence:

  1. Object’s property change script writes value to view.custom.value
  2. view.custom.value registers the write as a change
  3. view.custom.value executes its change script.
  4. Object’s value property is updated.
  5. Loop to step 1

Would having two property change scripts create a race condition where they are constantly executing because they are constantly recognizing their “writes” as a change even if the value doesn’t actually change? I’m only worried because I’m going to be dealing with floating point numbers and I’m afraid I’ll run into a situation where it detects a changed value because this time the floating point math had a result that was 0.00000000000000000000001 different than the last value.

Am I worried about nothing? Does simply writing to a property automatically trigger any property change scripts attached to it? If the value does actually have to be different to register as changed, does it handle floating point weirdness where you can’t get perfect precision with floating point numbers?

Edit:

I have confirmed there is a limited amount of “bouncing” that happens between the two scripts. It seems like every time I change a value, the scripting is executed 3-5 times:

The other thing I found is that when the tag changes, the conversion math is done both ways, which results in the value property being slightly different than what the user entered. For instance, I entered in 7312.5, but after the dueling scripts, the value in the numeric entry is 7312.500227234. It doesn’t change in the display until I actually click on the numeric entry.

I think what needs to happen is I only need to run my write-back on my view custom property if the source of that change was NOT my other property change script. I’m not sure how to accomplish that yet, though.

OK. I think I figured it out:

First thing I had to do was create a boolean view custom property called “userChange.” I use this as a handshake between numeric entry and the bound “value” property in my view.

In my object value property change script, everything is only if the origin is “Browser.” So the script will only execute if the change comes from someone typing a value in. If the tag value changes by some other means, it will have a different origin, and will not execute the property change script. Finally, I set my handshaking property to True

For the View custom property, It’s a little more complicated. I only want to execute this script if the Binding changes the value. The problem is, if I update it with my property change script, I will actually get TWO executions, one with an origin of Script, and then another with an origin of Binding. So I still have to differentiate if my origin is Binding, because it could just be the binding being updated after I wrote to the tag. That’s where the handshaking property comes in. I wait for my binding update, and when I get it, I check to see if the userChange property is True. If it is, don’t execute the rest of the script, and just set it to off.

I’m counting on always getting that Binding origin update, and always getting it AFTER the script origin update. That’s the only thing I’m not sure about now. It’s working well in my test environment, but if that doesn’t always happen in that order, I may have to go back to square one.

start your scripts off with if currentValue.value != previousValue.value: so that it doesn't run if the actual value didn't change.

Just reread your 2prior post that says there is a slight difference because of rounding... So maybe you'd need to add round the 2 values prior to compare to get them "equal" as the display shows..

I haven’t tried this before, but my instinct would be to always do this at the tag level, not the display level, using a derived tag. That will handle the reading and writing from SI unit inputs (ie metric) to imperial. I think I'd have a session prop to store the displayed units.. But I can't think of how to switch the binding display that isn't a bit gross…

There are several issues with that. We did that originally, but since we’re transmitting these tags over MQTT, sending double tags eats up unnecessary bandwidth.

I’ve read in several places that unit conversions should always be done at the visualization level and not with tags. I know @pturmel is a big advocate of this but I don’t know the exact technical reasoning other than the difficulties we’re running into: performance concerns, bloated tag providers, redundant data being historized for trending, etc.

  • Avoids conversion bugs in the path from PLC to Ignition live tag to Ignition Historian.
  • Avoids duplicate data transmission and storage, as you noted.
  • Avoids possible inconsistencies between data in different units (particularly in storage)
  • Avoids data races when writeback has two possible paths.
  • Combined with effective use of .engUnit tag property, enables UI-level implementation of unit conversions with no dependency on any other tag or tag path.
2 Likes

I have a few thoughts / suggestions:

  1. I'm optimistic that all of this can be done without script transforms. A simple expression binding should suffice for client-rendering of live values in units of their choosing.
  2. Try to grab as much data from the tagPath as possible, optionally, provide a means to override as necessary:
  • Instead of passing in minEntry/maxEntry, bind a custom property to engLow/engHigh on the tag, use those limits by default.
  • Instead of roundingDecimalPlaces, bind a custom property to formatString on the tag.
  • Instead of processType, bind a custom property to engUnit on the tag.
  • If any of these need to be adjusted / overridden at runtime, consider adding input parameters (e.g. ovrd_minEntry) to override individual properties, only if the input parameter contains a value that is not null.

Regarding "bind a custom property to engUnit on the tag" above, the goal would be to allow conversion from any unit to another of the operator's choosing. The conversion would be a two-step process, but straightforward. Ex: "R" --> "degF" --> "degC" (where 'degF' is selected as your 'standard' for temperature conversion). Consider building a table (as a tag and/or session prop?) which contains everything needed for any unit conversion to another. Something resembling:

Units Type Factor Offset
psig Pressure 1 0
psia Pressure 1 -14.6959
inHg Pressure 0.491154 0
inH2O Pressure 0.0360912 0
kPa Pressure 0.14503774 -14.6959
°F Temperature 1 0
°R Temperature 1 -459.67
°C Temperature 1.8 32
gpm Rate 1 0
m³/s Rate 15,850.30 0
ft³/min Rate 7.48052 0
  • Optionally, add an additional column for an adjustment to formatString on the tag.
  • The slope (Factor) & intercept (Offset) columns would contain values to convert to a standard unit of your choosing.
  • Disclaimer, don't trust any numbers in the table above, or my math below. :nerd_face:
  • An operator could select their desired units (from a config screen), whereby session props would contain the desired scaling (via lookup() function) to convert from their selected unit to a standard unit. Inside of your view, custom properties would contain the same logic - scaling to get from units on the tag to standard units. Combine the session props w/ view props to provide the resultant scaling, all in one expression binding (extra parenthesis added):
ClientValue = (((TagValue * TagFactor) + TagOffset) - ClientOffset) / ClientFactor

(I suppose you could also come up with resultant scaling values as additional custom props).