Client Tag Color Bindings

I’m running into a bit of a mental block with using client tags to hold colors that are then used throughout the vision windows/templates. On initial client startup I get the same issue as others have pointed out here where the initial value for the tags is -1 causing errors on any bindings that use this color tag. I could go through and use the toColor() function everywhere with a failover color in order to suppress the errors, but I would really like to avoid this as there are a lot of different locations where I need to make a color binding, and some are conditional so it’s not as simple as copy and paste everywhere…

My initial thought was to create custom properties on the root container of the window/template where the toColor() function would reside, allowing me to bind component colors to these properties directly. But I quickly realized the pain this would cause due to how many custom properties I would have to create on every window/template and would have to be done manually, let alone adding additional colors in the future. If template internal properties were bindable from outside the template, I could create a “Color Palette” template and just plop that into each screen and bind to it, and that would make adding colors to all windows/templates just a matter of updating this one template…

Anyone have any ideas or something I may be missing?

Ignition 7.9.3.

I have started using runScript with a python script library for most of my colour bindings.

The library first stores all client tag colours into a dictionary that I can then use instead of referring to the client tags (suggestion from @pturmel).

The library then has functions defined to return the correct colour for a given ‘thing’ (device status, device mode, etc). You could handle invalid colour values in here

Here’s a snippet:

'''
Library: [shared].devices.colour
Version: 1.1
Description:
	Use these functions within colour expression bindings within a runScript function to show the state of devices/equipment.

Examples
Textbox background:
runScript('shared.devices.colour.getDeviceStatusColour', 0, {[Client]System/Pulses/500ms}, 1)

Device graphic background:
runScript('shared.devices.colour.getDeviceStatusColour', 0, {[Client]System/Pulses/500ms}, 0)
'''
''' # REMOVE THIS, fix for forum formatting only

pipeColourCache = {}
pipeColourTagFolderPath = '[Client]Styles/Colours/Pipework'
pipeContents = ['Water', 'CIP']
for cT in pipeContents:
	pipeColourCache[cT + ' - Flow'] = system.tag.read(pipeColourTagFolderPath + '/' + cT + ' - Flow').value
	pipeColourCache[cT + ' - No Flow'] = system.tag.read(pipeColourTagFolderPath + '/' + cT + ' - No Flow').value
	
colourCache = {}
colourTagFolderPath = '[Client]Styles/Colours/Devices'
# unfortunately you can't browse client tags, so have to manually type these out...
colourTags = ['Faulted','Faulted Flash','FOI','Invalid State','Paused','Running','Running Flash','Stopped','Stopped Flash','Stopped Text','Device Mode - Manual Abnormal']
for cT in colourTags: colourCache[cT] = system.tag.read(colourTagFolderPath + '/' + cT).value

stateToColourTranslation = {}
stateToColourTranslation['Starting'] = 'Running'
stateToColourTranslation['Stopping'] = 'Stopped'
stateToColourTranslation['Off'] = 'Stopped'
stateToColourTranslation['Auto'] = 'Running'
stateToColourTranslation['Manual'] = 'Device Mode - Manual Abnormal'

def getDeviceStatusColour(valueString, pulseValue, isTextBox=0):
	colour = ""
	textBoxSuffix = ""
	if isTextBox: textBoxSuffix = " Text"
	
	# Non-flashing states
	if valueString in ['Stopped','Running']:
		colour = _getColourFromState(valueString, isTextBox)
	
	# Flashing states
	if valueString in ['Stopping','Starting','Faulted']:
		if pulseValue:
			colour = _getColourFromState(valueString, isTextBox, 0)
		else:
			colour = _getColourFromState(valueString, isTextBox, 1)
	
	if colour == "":
		colour = _getColourFromState('Invalid State')
	
	return colour

def _getColourFromState(valueString, isTextBox=0, flashColour=0):
	textBoxSuffix = ""
	flashSuffix = ""
	if isTextBox: textBoxSuffix = " Text"
	if flashColour: flashSuffix = " Flash"
	
	colour = colourCache.get(stateToColourTranslation.get(valueString, valueString) + textBoxSuffix + flashSuffix, colourCache[stateToColourTranslation.get(valueString, valueString) + flashSuffix])
	return colour

and some of my colours:
image

On each client tag colour tagChange event, to update the cache if the tag changes:

	keyName = tagPath.split("/")[-1]
	shared.devices.colour.colourCache[keyName] = currentValue.value
5 Likes

Hi nick! Great implementation. One nit: I can’t endorse the misspelling of “color” all over the place. :stuck_out_tongue_closed_eyes:

2 Likes

Haha, you seem to have made a typo there, Phil :grin:

This is great! Thanks @nminchin (and it sounds like by extension, @pturmel). I have a couple - hopefully minor - questions:

  1. Looking at the screenshot of your tags, it looks like you only have the tag event script on a few of the colo(u)r tags. Just curious as to why that is?
  2. In your code block, the runScript examples in the comment look like they leave out the valueString variable. Is that just a typo in the examples or am I missing something?
  1. Yep these scripts should be on all the colours, I just hadn’t finished adding to the rest. I actually finished them after posting.
  2. Also yep, good catch. The status (as a description*) should be the 3rd argument.

*Within my device UDTs, for the status and mode tags, which are integers that represent states, I have a corresponding description tag which is a memory expression tag. This translates the integer value into its corresponding description, e.g. for Motor Status: 0-Unknown, 1-Starting, 2-Running, 3-Stopping, 4-Stopped, 5-Faulted.
This helps in being able to use the same device Template (graphic) and sometimes popup across different similar devices, that may or may not (hopefully not) use the same integer value standards. It’s also easier to work with

1 Like

Thanks @nminchin, that’s what I figured but wanted to double check. I’ve been able to get this solution implemented and it fixes a lot of issues I had before.

Only drawback I’m running into is my project has a lot of unique components that can’t be templated (mainly custom shaped piping components) so each binding has to be manually adjusted. Would be awesome if we could relative path component properties in a binding. Even just being able to pass in the component name with something like {source.name} would be awesome. But until something like that’s possible I think I’m stuck with manually adjusting each binding as necessary…

Anyways, enough of my rambling. Thanks again for the help!

You can access self from runScript(), or binding.target in objectScript(). Either gives you the ability to include component-relative logic in your bindings.

1 Like

I'm not sure if this is relevant at all..
But if it helps, this is what I use for 'pipes' (line shapes). Maybe this could be made into a template itself, but I find using templates with components that need to be aligned perfectly sometimes doesn't work so well, which is why I've steered clear with them for pipes. That being said, if the template and components within the template are an integer size, and everything is placed at integer positions on the canvas, it should be ok. I know the pipe component itself is super dodgy when it comes to alignment in the client. I never use those!

def getPipeColour(flow, contents):
	colour = ""
	colourName = contents + ' - ' + {0:'No Flow',1:'Flow'}[flow]
	colour = pipeColourCache.get(colourName, colourCache['Invalid State'])
	return colour

RE a {source.name} reference, see Phil's post, but heed Paul's warning:

That being said though, I have tested using runScript within an expression binding on about 350 objects on the page, all updating at a 500ms rate (flashing colours), and there was no noticeable delay/lag/slowness at the client whatsoever

1 Like

Wow, @pturmel you just blew my mind with being able to pass in “self”, thank you! And @nminchin I actually saw that same post and was going to ask the two of you about the potential issue with performance when using “self” in runScript().

@nminchin the testing you did with runScript(), did that include a reference to “self” in the binding? If not, I’ll try running a test to see what kind of load something like this would cause:

runScript(
     "shared.colors.getColor",
     0,
     runScript("self.pipeContents")
)

I didn’t test using self, but I imagine the performance impact would be much the same as any using runScript in any shape or form.
Nested runScript calls, as in your example, is probably where I would cut the line though. I would definitely avoid them. Although it would be interesting to see what the impact is like.
I should also say that when I tested the 350 components, they all had the same runScript expression, so I dont know if, like other identical bindings, there’s a single source which they all attach to. I really need to test it again using different expressions

Good point. I forgot you can structure the runScript() binding with the variables embedded in the script call, which is probably the better way to pass in self:

runScript(
     "shared.colors.getColor(self.pipeContents)",
     0
)

Just remember, the reason I pass in my arguments as bindings instead of “self.x” is so that the runScript updates whenever the binding value changes. Passing in self.parameter will not cause the runScript to run, as it isn’t subscribed to any events.

1 Like

Oh interesting, I didn’t think of that…that makes my reason for using “self” pointless in many cases. I really only wanted to use “self” so I could stick with component-relative logic and not have to update the binding for every component. Especially the ones that I can’t turn into templates. I guess this method gets me part of the way there and would only need one binding per component to get this to fire when necessary. Would have loved that 100% option though…

Thanks for the help!