I was using direct tag bindings before and had the same issue. I can't recall exactly why I decided to test it with indirect tag bindings (something I saw on the forum led me to try this). Thanks for suggesting using expression bindings - I'll use now() from now on.
Here's the binding on ViewCanvas.props.instances[12].viewParams.xy (station instance):
station_number = 5
plc = 'Engine'
line_number = 1
POS_End = system.tag.readBlocking(["{}/Line/Line {}/Station {}/POS_End".format(plc, line_number, station_number)])[0].value
POS_Start = system.tag.readBlocking(["{}/Line/Line {}/Station {}/POS_Start".format(plc, line_number, station_number)])[0].value
POS_Center = (POS_End - POS_Start)/2 + POS_Start
agvMap = system.tag.readBlocking(["[default]{}/MCP/TrackSection/path".format(plc)])[0].value
# Convert keys to integers and sort them
sorted_keys = sorted(agvMap.keys(), key=lambda x: int(x))
# Linear search to find the two closest keys
def linear_search(sorted_keys, value):
previous_key = sorted_keys[0]
for key in sorted_keys[1:]:
if int(key) >= int(value):
# Interpolate between the two keys
lower_key = str(previous_key)
upper_key = str(key)
lower_x, lower_y = agvMap[lower_key]["x"], agvMap[lower_key]["y"]
upper_x, upper_y = agvMap[upper_key]["x"], agvMap[upper_key]["y"]
ratio = (value - int(lower_key)) / float((int(upper_key) - int(lower_key)))
interpolated_x = round(float(str(lower_x)) + ratio * (float(str(upper_x)) - float(str(lower_x))), 4)
interpolated_y = round(float(str(lower_y)) + ratio * (float(str(upper_y)) - float(str(lower_y))), 4)
return [interpolated_x, interpolated_y]
previous_key = key
# If value is greater than all keys, use the last key
if value > int(sorted_keys[-1]):
return [agvMap[sorted_keys[-1]]["x"], agvMap[sorted_keys[-1]]["y"]]
interpolated_coordinates = linear_search(sorted_keys, POS_Center)
return interpolated_coordinates
And here's the script on an AGV instance (ViewCanvas.props.instances[2].viewParams.xy):
line_number = str(system.tag.readBlocking(["[default]Engine/AGV/AGV 3/POS_Sta"])[0].value)
if len(line_number) == 1:
line_number = "00{}".format(line_number)
line_number = line_number[0]
agvMap = system.tag.readBlocking(["[default]Engine/MCP/TrackSection/path"])[0].value
# Convert keys to integers and sort them
sorted_keys = sorted(agvMap.keys(), key=lambda x: int(x))
# Linear search to find the two closest keys
def linear_search(sorted_keys, value):
previous_key = sorted_keys[0]
for key in sorted_keys[1:]:
if int(key) >= value:
# Interpolate between the two keys
lower_key = str(previous_key)
upper_key = str(key)
lower_x, lower_y = agvMap[lower_key]["x"], agvMap[lower_key]["y"]
upper_x, upper_y = agvMap[upper_key]["x"], agvMap[upper_key]["y"]
ratio = (value - int(lower_key)) / float((int(upper_key) - int(lower_key)))
interpolated_x = round(float(str(lower_x)) + ratio * (float(str(upper_x)) - float(str(lower_x))), 4)
interpolated_y = round(float(str(lower_y)) + ratio * (float(str(upper_y)) - float(str(lower_y))), 4)
return [interpolated_x, interpolated_y]
previous_key = key
# If value is greater than all keys, use the last key
if value > int(sorted_keys[-1]):
return [agvMap[sorted_keys[-1]]["x"], agvMap[sorted_keys[-1]]["y"]]
interpolated_coordinates = linear_search(sorted_keys, value)
return interpolated_coordinates
EDIT: I updated my binding creation script so that the xy value of the Station instances update based on a boolean memory tag "Update All Displays". The problem still exists however, since it's an issue of the binding not even evaluating the tag value until I click on the binding to update it. I'll also post my binding creation scripts below.
def build_canvas_instances(self):
if self.session.props.device.type == "designer":
height = 800
width = 1200
else:
height = self.page.props.dimensions.viewport.height
width = self.page.props.dimensions.viewport.width
canvas_instances = []
plcs = []
folders = system.tag.browse("[default]", filter={"tagType" : "Folder"})
for folder in folders:
if folder["name"] != "_types_":
plcs.append({
"name" : folder["name"],
"fullPath" : folder["fullPath"]
})
for plc in plcs:
agv_list = system.tag.browse('''{}/AGV'''.format(plc["fullPath"]), filter={})
for agv in agv_list:
agv_instance = {
"position": "absolute",
"top": "0px",
"left": "0px",
"bottom": "auto",
"right": "auto",
"width": "24px",
"height": "24px",
"zIndex": "auto",
"viewPath": "Overview/Subviews/AGV Instance",
"viewParams": {
"agv_number": agv["name"][agv["name"].find(" ")+1:],
"height": "24px",
"plc": plc["name"],
"width": "24px",
"position" : {
"y_min": 297.28,
"x_max": 555.72,
"y_max": 377.28,
"x_min": 579.72
}
},
"style": {
"classes": ""
}
}
canvas_instances.append(agv_instance)
line_tags = system.tag.browse('''{}/Line'''.format(plc["fullPath"]), filter={})
line_list = []
for line in line_tags:
line_list.append(line["fullPath"])
stations = []
for line_path in line_list:
for item in system.tag.browse(line_path, filter={}):
if item["name"].find("Station") != -1:
stations.append({
"station" : item["name"][item["name"].find(' '):].strip(' '),
"line" : str(line_path)[str(line_path).find(" "):].strip(" ")
})
for item in stations:
station_instance = {
"position": "absolute",
"top": "0px",
"left": "0px",
"bottom": "auto",
"right": "auto",
"zIndex": "auto",
"width": width,
"height": height,
"viewPath": "Overview/Subviews/Station Instance",
"viewParams": {
"height": height,
"line_number": item["line"],
"plc": plc["name"],
"station_number": item["station"],
"width": width,
"position" : {
"y_min": 297.28,
"x_max": 555.72,
"y_max": 377.28,
"x_min": 579.72
}
},
"style": {
"classes": ""
}
}
canvas_instances.append(station_instance)
return canvas_instances
def buildStationBinding(i, station_number, plc, line_number):
xyCode = "\t# Read ending and starting positions\n\tstation_number = "+station_number+"\n\tplc = "+"""'{}'""".format(plc)+"\n\tline_number = "+line_number+"\n\tPOS_End = system.tag.readBlocking([\"{}/Line/Line {}/Station {}/POS_End\".format(plc, line_number, station_number)])[0].value\n\tPOS_Start = system.tag.readBlocking([\"{}/Line/Line {}/Station {}/POS_Start\".format(plc, line_number, station_number)])[0].value\n\tPOS_Center = (POS_End - POS_Start)/2 + POS_Start\n\n\tagvMap = system.tag.readBlocking([\"[default]{}/MCP/TrackSection/path\".format(plc)])[0].value\n\t\n\t# Convert keys to integers and sort them\n\tsorted_keys = sorted(agvMap.keys(), key=lambda x: int(x))\n\t\n\t# Linear search to find the two closest keys\n\tdef linear_search(sorted_keys, value):\n\t\tprevious_key = sorted_keys[0]\n\t\tfor key in sorted_keys[1:]:\n\t\t\tif int(key) >= int(value):\n\t\t\t\t# Interpolate between the two keys\n\t\t\t\tlower_key = str(previous_key)\n\t\t\t\tupper_key = str(key)\n\t\t\t\tlower_x, lower_y = agvMap[lower_key][\"x\"], agvMap[lower_key][\"y\"]\n\t\t\t\tupper_x, upper_y = agvMap[upper_key][\"x\"], agvMap[upper_key][\"y\"]\n\t\t\t\tratio = (value - int(lower_key)) / float((int(upper_key) - int(lower_key)))\n\t\t\t\tinterpolated_x = round(float(str(lower_x)) + ratio * (float(str(upper_x)) - float(str(lower_x))), 4)\n\t\t\t\tinterpolated_y = round(float(str(lower_y)) + ratio * (float(str(upper_y)) - float(str(lower_y))), 4)\n\t\t\t\treturn [interpolated_x, interpolated_y]\n\t\t\tprevious_key = key\n\t\n\t\t# If value is greater than all keys, use the last key\n\t\tif value > int(sorted_keys[-1]):\n\t\t\treturn [agvMap[sorted_keys[-1]][\"x\"], agvMap[sorted_keys[-1]][\"y\"]]\n\t\n\t# Perform linear search to find the closest \u0027mm_pos\u0027 that does not exceed the \u0027value\u0027\n\tinterpolated_coordinates = linear_search(sorted_keys, POS_Center)\n\treturn interpolated_coordinates",
binding_data = {
"props.instances[{}].height".format(i): {
"binding": {
"config": {
"path": "view.custom.station_height"
},
"type": "property"
}
},
"props.instances[{}].left".format(i): {
"binding": {
"config": {
"path": "this.props.instances[{}].viewParams.xy[0]".format(i)
},
"overlayOptOut": True,
"transforms": [
{
"code": "\tx = float(str(value))*100 - ((self.props.instances["+str(i)+"].width/self.view.props.defaultSize.width)/2)*100\n\treturn \"{}%\".format(x)",
"type": "script"
}
],
"type": "property"
}
},
"props.instances[{}].top".format(i): {
"binding": {
"config": {
"path": "this.props.instances[{}].viewParams.xy[1]".format(i)
},
"transforms": [
{
"code": "\ty = float(str(value))*100 - ((self.props.instances["+str(i)+"].height/self.view.props.defaultSize.height)/2)*100\n\treturn \"{}%\".format(y)",
"type": "script"
}
],
"type": "property"
}
},
"props.instances[{}].viewParams.xy".format(i): {
"binding": {
"type": "tag",
"config": {
"mode": "direct",
"tagPath": "[default]Update All Displays",
"fallbackDelay": 2.5
},
"transforms": [
{
"code": xyCode,
"type": "script"
}
]
}
},
"props.instances[{}].width".format(i): {
"binding": {
"config": {
"path": "view.custom.station_width"
},
"overlayOptOut": True,
"type": "property"
}
}
}
return binding_data
def buildAGVBinding(i, agv_num, plc):
code = "\tline_number = str(system.tag.readBlocking([\"[default]"+str(plc)+"/AGV/AGV "+str(agv_num)+"/POS_Sta\"])[0].value)\n\tif len(line_number) == 1:\n\t\tline_number = \"00{}\".format(line_number)\n\tline_number = line_number[0]\n\tagvMap = system.tag.readBlocking([\"[default]"+str(plc)+"/MCP/TrackSection/path\"])[0].value\n\t\n\t\n\t# Convert keys to integers and sort them\n\tsorted_keys = sorted(agvMap.keys(), key=lambda x: int(x))\n\t\n\t# Linear search to find the two closest keys\n\tdef linear_search(sorted_keys, value):\n\t\tprevious_key = sorted_keys[0]\n\t\tfor key in sorted_keys[1:]:\n\t\t\tif int(key) >= value:\n\t\t\t\t# Interpolate between the two keys\n\t\t\t\tlower_key = str(previous_key)\n\t\t\t\tupper_key = str(key)\n\t\t\t\tlower_x, lower_y = agvMap[lower_key][\"x\"], agvMap[lower_key][\"y\"]\n\t\t\t\tupper_x, upper_y = agvMap[upper_key][\"x\"], agvMap[upper_key][\"y\"]\n\t\t\t\tratio = (value - int(lower_key)) / float((int(upper_key) - int(lower_key)))\n\t\t\t\tinterpolated_x = round(float(str(lower_x)) + ratio * (float(str(upper_x)) - float(str(lower_x))), 4)\n\t\t\t\tinterpolated_y = round(float(str(lower_y)) + ratio * (float(str(upper_y)) - float(str(lower_y))), 4)\n\t\t\t\treturn [interpolated_x, interpolated_y]\n\t\t\tprevious_key = key\n\t\n\t\t# If value is greater than all keys, use the last key\n\t\tif value > int(sorted_keys[-1]):\n\t\t\treturn [agvMap[sorted_keys[-1]][\"x\"], agvMap[sorted_keys[-1]][\"y\"]]\n\t\n\t# Perform linear search to find the closest \u0027mm_pos\u0027 that does not exceed the \u0027value\u0027\n\tinterpolated_coordinates = linear_search(sorted_keys, value)\n\treturn interpolated_coordinates"
binding_data = {
"props.instances[{}].left".format(i): {
"binding": {
"config": {
"path": "this.props.instances[{}].viewParams.xy[0]".format(i)
},
"transforms": [
{
"code": "\treturn value * self.view.props.defaultSize.width",
"type": "script"
}
],
"type": "property"
}
},
"props.instances[{}].top".format(i): {
"binding": {
"config": {
"path": "this.props.instances[{}].viewParams.xy[1]".format(i)
},
"transforms": [
{
"code": "\treturn value * self.view.props.defaultSize.height",
"type": "script"
}
],
"type": "property"
}
},
"props.instances[{}].viewParams.xy".format(i): {
"binding": {
"config": {
"fallbackDelay": 2.5,
"mode": "indirect",
"references": {},
"tagPath": "[default]{}/AGV/AGV {}/POS_mm".format(plc, agv_num)
},
"transforms": [
{
"code": code,
"type": "script"
}
],
"type": "tag"
},
}
}
return binding_data
def readFile(filePath="/var/lib/ignition/data/projects/Display/com.inductiveautomation.perspective/views/Overview/Canvas/view.json"):
import os
import system
# Check if the file exists
if os.path.exists(filePath):
# If the file exists, read its contents
with open(filePath, 'r') as file:
fileContents = file.read()
logger = system.util.getLogger("FileContentLogger")
# Log the contents using the Ignition logger
logger.info("Contents of {}: \n{}".format(filePath, fileContents))
return {"filePath":filePath, "fileContents":fileContents}
else:
# Log a message if the file does not exist
logger = system.util.getLogger("FileContentLogger")
logger.warn("File not found: {}".format(filePath))
return -1
def build_canvas_bindings(i, file_path, file_contents, binding):
logger = system.util.getLogger("build_canvas_bindings")
file_contents = system.util.jsonDecode(file_contents)
for k,v in binding.items():
logger.info("Key in pyContents.items(): {}".format(k))
logger.info("Value in pyContents.items(): {}".format(v))
file_contents["root"]["children"][1]["propConfig"][k] = v
return system.util.jsonEncode(file_contents)
def write_view_json(file_path, view_json):
system.file.writeFile(file_path, view_json)
system.project.requestScan()
Here's the script on the onClick event of my "Create Bindings" button:
LOGGER = system.util.getLogger("binding_string")
view_json_path = "/var/lib/ignition/data/projects/Display/com.inductiveautomation.perspective/views/Overview/Canvas/view.json"
view_json = build_canvas.readFile(view_json_path)
new_view_json = view_json["fileContents"]
for i, instance in enumerate(self.getSibling("ViewCanvas").props.instances):
try:
line_number = instance.viewParams.line_number
instance_type = "station"
except:
instance_type = "agv"
if instance_type == "agv":
system.perspective.print("THE INSTANCE TYPE IS AGV")
agv_num = instance.viewParams.agv_number
plc = instance.viewParams.plc
binding_data = build_canvas.buildAGVBinding(i, agv_num, plc)
elif instance_type == "station":
station_number = instance.viewParams.station_number
plc = instance.viewParams.plc
line_number = instance.viewParams.line_number
binding_data = build_canvas.buildStationBinding(i, station_number, plc, line_number)
system.perspective.print("instance_type: {} ||| i: {}".format(instance_type, i))
self.getSibling("Label").props.text = new_view_json
system.perspective.print("||||||||||||| {} |||||||||||||".format(type(new_view_json)))
new_view_json = build_canvas.build_canvas_bindings(i, file_path=view_json_path, file_contents=new_view_json, binding=binding_data)
build_canvas.write_view_json(file_path=view_json["filePath"], view_json=new_view_json)