--- a/svghmi/detachable_pages.ysl2 Fri Oct 29 11:49:22 2021 +0200
+++ b/svghmi/detachable_pages.ysl2 Thu Nov 04 12:00:50 2021 +0100
@@ -169,6 +169,7 @@
if "count($desc/path/@index)=0"
warning > Page id="«$page/@id»" : No match for path "«$desc/path/@value»" in HMI tree
| page_index: «$desc/path/@index»,
+ | page_class: "«$indexed_hmitree/*[@hmipath = $desc/path/@value]/@class»", foreach "$page_managed_widgets" {
--- a/svghmi/hmi_tree.py Fri Oct 29 11:49:22 2021 +0200
+++ b/svghmi/hmi_tree.py Thu Nov 04 12:00:50 2021 +0100
@@ -160,5 +160,4 @@
SPECIAL_NODES = [("HMI_ROOT", "HMI_NODE"),
("heartbeat", "HMI_INT")]
- # ("current_page", "HMI_STRING")])
--- a/svghmi/hmi_tree.ysl2 Fri Oct 29 11:49:22 2021 +0200
+++ b/svghmi/hmi_tree.ysl2 Thu Nov 04 12:00:50 2021 +0100
@@ -1,5 +1,7 @@
+// Location identifies uniquely SVGHMI instance // HMI Tree computed from VARIABLES.CSV in svghmi.py
const "hmitree", "ns:GetHMITree()";
@@ -19,20 +21,28 @@
| var heartbeat_index = «$indexed_hmitree/*[@hmipath = '/HEARTBEAT']/@index»;
+ | var current_page_var_index = «$indexed_hmitree/*[@hmipath = concat('/CURRENT_PAGE_', $instance_name)]/@index»; foreach "$indexed_hmitree/*"
- | /* «@index» */ "«substring(local-name(), 5)»"`if "position()!=last()" > ,`
+ | "«substring(local-name(), 5)»"`if "position()!=last()" > ,` foreach "$indexed_hmitree/*"
- | /* «@index» */ "«@hmipath»"`if "position()!=last()" > ,`
+ | "«@hmipath»"`if "position()!=last()" > ,` + | var hmitree_nodes = { + foreach "$indexed_hmitree/*[local-name() = 'HMI_NODE']" + | "«@hmipath»" : [«@index», "«@class»"]`if "position()!=last()" > ,` template "*", mode="index" {
--- a/svghmi/svghmi.js Fri Oct 29 11:49:22 2021 +0200
+++ b/svghmi/svghmi.js Thu Nov 04 12:00:50 2021 +0100
@@ -30,12 +30,12 @@
// Open WebSocket to relative "/ws" address
+var has_watchdog = window.location.hash == "#watchdog"; window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws')
- + '?mode=' + (window.location.hash == "#watchdog"
+ + '?mode=' + (has_watchdog ? "watchdog" : "multiclient"); var ws = new WebSocket(ws_url);
ws.binaryType = 'arraybuffer';
@@ -195,15 +195,26 @@
-// artificially subscribe the watchdog widget to "/heartbeat" hmi variable
-// Since dispatch directly calls change_hmi_value,
-// PLC will periodically send variable at given frequency
-subscribers(heartbeat_index).add({
- /* type: "Watchdog", */
+ // artificially subscribe the watchdog widget to "/heartbeat" hmi variable + // Since dispatch directly calls change_hmi_value, + // PLC will periodically send variable at given frequency + subscribers(heartbeat_index).add({ + /* type: "Watchdog", */ + indexes: [heartbeat_index], + new_hmi_value: function(index, value, oldval) { + apply_hmi_value(heartbeat_index, value+1); +// subscribe to per instance current page hmi variable +subscribers(current_page_var_index).add({ - indexes: [heartbeat_index],
+ indexes: [current_page_var_index], new_hmi_value: function(index, value, oldval) {
- apply_hmi_value(heartbeat_index, value+1);
@@ -401,7 +412,9 @@
if(page_name == undefined)
page_name = current_subscribed_page;
+ else if(page_index == undefined){ + [page_name, page_index] = page_name.split('@') let old_desc = page_desc[current_subscribed_page];
let new_desc = page_desc[page_name];
@@ -411,8 +424,19 @@
- if(page_index == undefined){
+ if(page_index == undefined) page_index = new_desc.page_index;
+ else if(typeof(page_index) == "string") { + let hmitree_node = hmitree_nodes[page_index]; + if(hmitree_node !== undefined){ + let [int_index, hmiclass] = hmitree_node; + if(hmiclass == new_desc.page_class) + page_index = int_index; + page_index = new_desc.page_index; + page_index = new_desc.page_index; @@ -443,6 +467,11 @@
if(jump_history.length > 42)
+ apply_hmi_value(current_page_var_index, + page_index == undefined + : page_name + "@" + hmitree_paths[page_index]); --- a/svghmi/svghmi.py Fri Oct 29 11:49:22 2021 +0200
+++ b/svghmi/svghmi.py Thu Nov 04 12:00:50 2021 +0100
@@ -130,6 +130,10 @@
# ignores variables starting with _TMP_
if path[-1].startswith("_TMP_"):
+ # ignores external variables if derived == "HMI_NODE":
@@ -138,7 +142,7 @@
kwargs['hmiclass'] = path[-1]
- new_node = HMITreeNode(path, name, derived, v["type"], v["vartype"], v["C_path"], **kwargs)
+ new_node = HMITreeNode(path, name, derived, v["type"], vartype, v["C_path"], **kwargs) placement_result = hmi_tree_root.place_node(new_node)
if placement_result is not None:
cause, problematic_node = placement_result
@@ -148,10 +152,10 @@
- if v["vartype"] == "FB":
- if v["C_path"] == problematic_node:
+ if _v["vartype"] == "FB": + if _v["C_path"] == problematic_node: failing_parent = last_FB["type"]
@@ -572,7 +576,8 @@
# call xslt transform on Inkscape's SVG to generate XHTML
self.ProgressStart("xslt", "XSLT transform")
- result = transform.transform(svgdom) # , profile_run=True)
+ result = transform.transform( + svgdom, instance_name=location_str) # , profile_run=True) except XSLTApplyError as e:
self.FatalError("SVGHMI " + svghmi_options["name"] + ": " + e.message)
@@ -826,8 +831,11 @@
self.GetCTRoot().logger.write_error(
_("Font file does not exist: %s\n") % fontfile)
+ def CTNGlobalInstances(self): + location_str = "_".join(map(str, self.GetCurrentLocation())) + return [("CURRENT_PAGE_"+location_str, "HMI_STRING", "")] ## In case one day we support more than one heartbeat
- # def CTNGlobalInstances(self):
# view_name = self.BaseParams.getName()
# return [(view_name + "_HEARTBEAT", "HMI_INT", "")]
--- a/tests/svghmi/plc.xml Fri Oct 29 11:49:22 2021 +0200
+++ b/tests/svghmi/plc.xml Thu Nov 04 12:00:50 2021 +0100
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns:ns1="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.plcopen.org/xml/tc6_0201">
<fileHeader companyName="Unknown" productName="Unnamed" productVersion="1" creationDateTime="2019-08-06T14:23:42"/>
- <contentHeader name="Unnamed" modificationDateTime="2021-10-03T20:43:39">
+ <contentHeader name="Unnamed" modificationDateTime="2021-11-04T11:35:21"> @@ -71,6 +71,25 @@
+ <variable name="CURRENT_PAGE_0"> + <derived name="HMI_STRING"/> + <variable name="PAGESWITCH"> + <variable name="R_TRIG0"> + <derived name="R_TRIG"/> @@ -287,6 +306,100 @@
<expression>0</expression>
+ <inOutVariable localId="12" executionOrderId="0" height="25" width="125" negatedOut="false" negatedIn="false"> + <position x="410" y="205"/> + <relPosition x="0" y="10"/> + <connection refLocalId="13" formalParameter="OUT"> + <position x="410" y="215"/> + <position x="385" y="215"/> + <relPosition x="125" y="10"/> + <expression>CURRENT_PAGE_0</expression> + <block localId="13" typeName="SEL" executionOrderId="0" height="80" width="65"> + <position x="320" y="185"/> + <variable formalParameter="G"> + <relPosition x="0" y="30"/> + <connection refLocalId="17" formalParameter="Q"> + <position x="320" y="215"/> + <position x="280" y="215"/> + <variable formalParameter="IN0"> + <relPosition x="0" y="50"/> + <connection refLocalId="12"> + <position x="320" y="235"/> + <position x="60" y="235"/> + <position x="60" y="155"/> + <position x="550" y="155"/> + <position x="550" y="215"/> + <position x="535" y="215"/> + <variable formalParameter="IN1"> + <relPosition x="0" y="70"/> + <connection refLocalId="16"> + <position x="320" y="255"/> + <position x="290" y="255"/> + <variable formalParameter="OUT"> + <relPosition x="65" y="30"/> + <inVariable localId="15" executionOrderId="0" height="25" width="90" negated="false"> + <position x="100" y="205"/> + <relPosition x="90" y="10"/> + <expression>PAGESWITCH</expression> + <inVariable localId="16" executionOrderId="0" height="25" width="220" negated="false"> + <position x="70" y="245"/> + <relPosition x="220" y="10"/> + <expression>'RelativePageTest@/TRUMP2'</expression> + <block localId="17" typeName="R_TRIG" instanceName="R_TRIG0" executionOrderId="0" height="40" width="60"> + <position x="220" y="185"/> + <variable formalParameter="CLK"> + <relPosition x="0" y="30"/> + <connection refLocalId="15"> + <position x="220" y="215"/> + <position x="190" y="215"/> + <variable formalParameter="Q"> + <relPosition x="60" y="30"/>