If TagX = 1 and TagY = 1 Then 3

I am trying to make a template for a valve, and the valve has two switches (ZSO, ZSC), which combined can be 4 different states, Opened, Closed, Both Sensors, No Sensors. Each state would make the valve show a different color. I get that I need to use a style customizer for this, and have to make a custom property (called ValvePosition) on the item I want to change the color on (the valve body).

I have gotten this to work successfully using the binEnc function, which is basically converting my two separate tags into a 2 digit binary number. Each value of the 2 digit binary number would drive a separate color scheme on the style customizer.

binEnc ({[default]ROV_V402_100/ZSO.value},{[default]ROV_V402_100/ZSC.value})

The problem is I don’t want this hard coded directly to a tag, so I need to add some indirect tags. So I am trying to use the following code:

binEnc ({[default]{Valve.ValveTag::Meta.TagName}/ZSO.value},{[default]{Valve.ValveTag::Meta.TagName}/ZSC.value})

This wont work because of the the following error message “Scan Error: Nested Paths Not Allowed (Line 1, Char 19)”.

I think I have a solution for this (haven’t tested it yet) by moving the logic into a memory tag as part of my UDT and using the original binEnc code to drive the value of the memory tag and then use that memory tag in the template, but I feel there should be a way to do this with script language inside of a template. The script language seems very limited when it comes to evaluating different tags.

As it says, you can’t nest paths. Use two additional custom properties, booleans, for your two sensors. Use indirect tag binding on each of them to subscribe to the correct sensors. Encode those bit properties into your style integer.

You can also create an expression tag in your udt to combine the two inputs into your status integer value, and then use that in any of your expressions. That way you have one source of the truth, not multiple. I also like to use client tags to hold colours as strings, such as device running, stopped, faulted, etc. Status colours. Then I use an expression binding on the colour properties to refer to the client colour tags. You can then also synchronise the flashing of fault condition colours. If you use a blink colour in the style customiser, the flashes will not be synchronised across components which in my opinion looks unprofessional, a bit like watching the indicators of cars lined up to turn :smile:

I like the idea of doing it in the UDT too, but the only advantage I can think of for putting it in the template like pturmel suggested would be that it would only be evaluated when the template is being used. Correct me if im wrong, but when its in the UDT it would get evaluated every tag update even if its not being displayed on the screen. If I had 1000 or 2000 valve UDT’s, I don’t know if that little bit extra for each tag would make a noticeable difference on performance though.

To make it blink with the vision client tag color strings like you suggested, are you using a script to write different colors values into your color string based on a timer? If that’s how you are doing it, where are you locating that script?

Sorry in advance for the essay…
For the colours, I use an expression binding with a case statement. However it wouldn’t be a bad idea to create a python function to return the colour as well. Then you could make it a global script and call with runScript.
Update: I’ve just tested this and it seems to work really well. I have 350 objects copied and pasted that all use the same binding on the background colour, and there are no client performance issues whatsoever.

I would be interested in your opinions @pturmel and @PGriffith?

runScript function:

def getDeviceStatusColour(valueString, pulseValue, isTextBox=0):
	colour = ""
	textBoxSuffix = ""
	# I use a slightly lighter grey if the colour is used beneath text, otherwise the text is drowned out
	if isTextBox: textBoxSuffix = " Text"
	
	if valueString == 'Stopped':
		colour = system.tag.read('[Client]Styles/Colours/Devices/Stopped' + textBoxSuffix).value
	if valueString == 'Stopping':
		if pulseValue:
			colour = system.tag.read('[Client]Styles/Colours/Devices/Stopped' + textBoxSuffix).value
		else:
			colour = system.tag.read('[Client]Styles/Colours/Devices/Stopped Flash').value
	if valueString == 'Running':
		colour = system.tag.read('[Client]Styles/Colours/Devices/Running').value
	if valueString == 'Starting':
		if pulseValue:
			colour = system.tag.read('[Client]Styles/Colours/Devices/Running').value
		else:
			colour = system.tag.read('[Client]Styles/Colours/Devices/Running Flash').value
	if valueString == 'Faulted':
		if pulseValue:
			colour = system.tag.read('[Client]Styles/Colours/Devices/Faulted').value
		else:
			colour = system.tag.read('[Client]Styles/Colours/Devices/Faulted Flash').value
	
	if colour == "":
		colour = system.tag.read('[Client]Styles/Colours/Devices/Invalid State').value
	
	return colour

runScript expression binding on background colour:

runScript('shared.TEST.getDeviceStatusColour',0, {_Testing/Device Status}, {[Client]System/Pulses/500ms})

And my client tag colour definitions (might be relevant):
image
Remember to turn on combine repaints when you have lots of potentially flashing objects, to synchronise the repaints together.


Regarding the expression tags. I actually use string expression tags for my UDT Status and Mode tags to translate the values to their corresponding string description. This is so that I can use the same device template for many different versions of the UDT, even if they’re not using the same mode or status translations. You simply modify the translation in the UDT or at the instance, and the template will work. Ideally there should only ever be the one standard… but in reality, other unscrupulous contractors perform work and use their own standards who show little interest in using the standards already onsite :unamused: E.g.

case({[.]Status}
    ,0,'Stopped'
    ,1,'Running'
    ,2,'Faulted'
    ,'INVALID'
)

Initially I thought this might have a performance impact, and it probably does, however my status and mode tags don’t change every scan, they’re more likely to change once an hour, if that. So even with 1000’s of them having to update, the impact is relatively low.


For the pulses to control the flashing of colours, I use client tags and a client script running in a dedicated thread, fixed rate @ 125ms. I usually use the 500ms pulse, however I have a number of other values for just in case:

# Set this to the script execution time period in ms. This affects the minimum time of pulses that may be generated.
timeInterval = 125
# Time to count up to in ms. This affects the maximum time of pulses that may be generated
counterPeriod = 2000

tag = "[Client]System/Pulses/125ms Counter"
counter = system.tag.read(tag).value

# Increment the 125ms counter tag if the time is within the counterPeriod time, otherwise reset to 0
if counter > (counterPeriod/timeInterval - 1):
	system.tag.write(tag, 0)
else:
	system.tag.write(tag, system.tag.read(tag).value + 1)

# Write the pulse values to the client tags
system.tag.write("[Client]System/Pulses/125ms", int(counter*timeInterval / 125) % 2)
system.tag.write("[Client]System/Pulses/250ms", int(counter*timeInterval / 250) % 2)
system.tag.write("[Client]System/Pulses/500ms", int(counter*timeInterval / 500) % 2)
system.tag.write("[Client]System/Pulses/750ms", int(counter*timeInterval / 750) % 2)
system.tag.write("[Client]System/Pulses/1000ms", int(counter*timeInterval / 1000) % 2)

The approach is good. I’d optimize the getDeviceStatusColour function to cache the color combinations in a script module scoped dictionary. Use a client tag change script monitoring the colors to update the cache. The function called from runscript would be just:

def getDeviceStatusColour(valueString, pulseValue, isTextBox=0):
    return colorCache.get((valueString, bool(pulseValue), bool(isTextBox), colorCache[("Invalid State", False, False)])
1 Like

Thanks for taking the time, its very helpful.

I just want to make sure I understand what the last lines of code are doing in the in your pulse script.

system.tag.write("[Client]System/Pulses/125ms", int(counter*timeInterval / 125) % 2)
system.tag.write("[Client]System/Pulses/250ms", int(counter*timeInterval / 250) % 2)
system.tag.write("[Client]System/Pulses/500ms", int(counter*timeInterval / 500) % 2)
system.tag.write("[Client]System/Pulses/750ms", int(counter*timeInterval / 750) % 2)
system.tag.write("[Client]System/Pulses/1000ms", int(counter*timeInterval / 1000) % 2)

How are you getting a pulse bit out of this? You are taking the remainder of the math (counter*timeInterval / 250) % 2) and then converting it to an integer. If there is no remainder this should write a zero I believe. If there is a remainder then wouldn’t it just get truncated when you convert it to an integer, or does it round up?

Your first assumption is correct, int() will basically floor/truncate the result
See if this helps:

Awesome, thanks Phil! I'll get on it

Edit:

Actually I might need some help.. how do I define this in the right scope so that both the global script and the tag script can access it?

I have this in one of the colour client tag's value change event:

if 'colourCache' not in globals(): globals()['colourCache'] = {}
keyName = tagPath.split("/")[-1]
globals()['colourCache'][keyName] = currentValue.value

I got it working using system.util.getGlobals()['ColourCache'], but understand this is a legacy function and probably shouldn't be using it for new projects.

Not global scope. Script module scope. This, unindented, above your function def:

colorCache = {}

Hmm, I’ve replaced with using the script-scoped variable, but my test component script can’t seem to access it. The result of clicking the button below is “INVALID”

Global script library ([shared]devices.displayColour):

colourCache = {}

def getDeviceStatusColour(valueString, pulseValue, isTextBox=0):
	return colourCache.get(valueString, colourCache.get('Invalid State','INVALID')) # last INVALID there only in case the dictionary Invalid State isn't in the dict

Vision client tag change value event handler:

keyName = tagPath.split("/")[-1]#.replace(" ","_").lower()
colourCache[keyName] = currentValue.value

Component Button script:
print shared.devices.displayColour.getDeviceStatusColour(system.tag.read("_Testing/Device Status").value, 0, isTextBox=0)

Also, my Client tag change script events don’t fire on first load which I thought they would

Your event handler(s) should delegate to a function in the same script module as the colorCache dictionary. Then the can access the dictionary unqualified.

Also consider putting code in the top level of the script module to pre-populate the dictionary. Keep in mind that I show the lookup using a tuple of string,boolean,boolean. You’ll have to populate with the same scheme.

1 Like

Can I auto-populate the dictionary without manually adding each key to it? i.e. can I browse the client tags like you can with standard tags? system.tag.browseTags doesn’t work for client tags… but I guess these must be accessible via scripting somehow?

Edited: meant browseTags, not read

You might find some inspiration in my old shared.tag script module.

1 Like