SVGHMI: add CURRENT_PAGE_{location} global variable to reflect currently visible page. If PLC wites some valid page reference in that variable, it triggers page switch. Additionally, fixed /HEARTBEAT being subscribed systematically by JS code even when wtchdog is not enabled.
authorEdouard Tisserant
Thu, 04 Nov 2021 12:00:50 +0100
changeset 3381 3a0908b0319d
parent 3376 83ed4ea362db
child 3382 589abe084e57
SVGHMI: add CURRENT_PAGE_{location} global variable to reflect currently visible page. If PLC wites some valid page reference in that variable, it triggers page switch. Additionally, fixed /HEARTBEAT being subscribed systematically by JS code even when wtchdog is not enabled.
svghmi/detachable_pages.ysl2
svghmi/hmi_tree.py
svghmi/hmi_tree.ysl2
svghmi/svghmi.js
svghmi/svghmi.py
tests/svghmi/plc.xml
--- 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»",
     }
     |     widgets: [
     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 @@
 // hmi_tree.ysl2
 
+// Location identifies uniquely SVGHMI instance
+param "instance_name";
 
 // 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»;
+    |
     | var hmitree_types = [
 
     foreach "$indexed_hmitree/*" 
-    |     /* «@index» */ "«substring(local-name(), 5)»"`if "position()!=last()" > ,`
+    |     "«substring(local-name(), 5)»"`if "position()!=last()" > ,`
 
     | ];
     |
     | var hmitree_paths = [
 
     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";
 
 var ws_url = 
     window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws')
-    + '?mode=' + (window.location.hash == "#watchdog" 
-                  ? "watchdog"
-                  : "multiclient");
+    + '?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", */
+if(has_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", */
+        frequency: 1,
+        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({
     frequency: 1,
-    indexes: [heartbeat_index],
+    indexes: [current_page_var_index],
     new_hmi_value: function(index, value, oldval) {
-        apply_hmi_value(heartbeat_index, value+1);
+        switch_page(value);
     }
 });
 
@@ -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 @@
         return false;
     }
 
-    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;
+            else
+                page_index = new_desc.page_index;
+        } else {
+            page_index = new_desc.page_index;
+        }
     }
 
     if(old_desc){
@@ -443,6 +467,11 @@
     if(jump_history.length > 42)
         jump_history.shift();
 
+    apply_hmi_value(current_page_var_index,
+                    page_index == undefined
+                        ? page_name
+                        : page_name + "@" + hmitree_paths[page_index]);
+
     return true;
 };
 
--- 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_"):
                 continue
+            vartype = v["vartype"]
+            # ignores external variables
+            if vartype == "EXT":
+                continue
             derived = v["derived"]
             kwargs={}
             if derived == "HMI_NODE":
@@ -138,7 +142,7 @@
                 kwargs['hmiclass'] = path[-1]
             else:
                 name = 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 @@
                         ".".join(new_node.path))
 
                     last_FB = None 
-                    for v in varlist:
-                        if v["vartype"] == "FB":
-                            last_FB = v 
-                        if v["C_path"] == problematic_node:
+                    for _v in varlist:
+                        if _v["vartype"] == "FB":
+                            last_FB = _v 
+                        if _v["C_path"] == problematic_node:
                             break
                     if last_FB is not None:
                         failing_parent = last_FB["type"]
@@ -572,7 +576,8 @@
                 # call xslt transform on Inkscape's SVG to generate XHTML
                 try: 
                     self.ProgressStart("xslt", "XSLT transform")
-                    result = transform.transform(svgdom)  # , profile_run=True)
+                    result = transform.transform(
+                        svgdom, instance_name=location_str)  # , profile_run=True)
                     self.ProgressEnd("xslt")
                 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">
     <coordinateInfo>
       <fbd>
         <scaling x="5" y="5"/>
@@ -71,6 +71,25 @@
               </type>
             </variable>
           </localVars>
+          <externalVars>
+            <variable name="CURRENT_PAGE_0">
+              <type>
+                <derived name="HMI_STRING"/>
+              </type>
+            </variable>
+          </externalVars>
+          <localVars>
+            <variable name="PAGESWITCH">
+              <type>
+                <BOOL/>
+              </type>
+            </variable>
+            <variable name="R_TRIG0">
+              <type>
+                <derived name="R_TRIG"/>
+              </type>
+            </variable>
+          </localVars>
         </interface>
         <body>
           <FBD>
@@ -287,6 +306,100 @@
               </connectionPointOut>
               <expression>0</expression>
             </inVariable>
+            <inOutVariable localId="12" executionOrderId="0" height="25" width="125" negatedOut="false" negatedIn="false">
+              <position x="410" y="205"/>
+              <connectionPointIn>
+                <relPosition x="0" y="10"/>
+                <connection refLocalId="13" formalParameter="OUT">
+                  <position x="410" y="215"/>
+                  <position x="385" y="215"/>
+                </connection>
+              </connectionPointIn>
+              <connectionPointOut>
+                <relPosition x="125" y="10"/>
+              </connectionPointOut>
+              <expression>CURRENT_PAGE_0</expression>
+            </inOutVariable>
+            <block localId="13" typeName="SEL" executionOrderId="0" height="80" width="65">
+              <position x="320" y="185"/>
+              <inputVariables>
+                <variable formalParameter="G">
+                  <connectionPointIn>
+                    <relPosition x="0" y="30"/>
+                    <connection refLocalId="17" formalParameter="Q">
+                      <position x="320" y="215"/>
+                      <position x="280" y="215"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN0">
+                  <connectionPointIn>
+                    <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"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN1">
+                  <connectionPointIn>
+                    <relPosition x="0" y="70"/>
+                    <connection refLocalId="16">
+                      <position x="320" y="255"/>
+                      <position x="290" y="255"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="OUT">
+                  <connectionPointOut>
+                    <relPosition x="65" y="30"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <inVariable localId="15" executionOrderId="0" height="25" width="90" negated="false">
+              <position x="100" y="205"/>
+              <connectionPointOut>
+                <relPosition x="90" y="10"/>
+              </connectionPointOut>
+              <expression>PAGESWITCH</expression>
+            </inVariable>
+            <inVariable localId="16" executionOrderId="0" height="25" width="220" negated="false">
+              <position x="70" y="245"/>
+              <connectionPointOut>
+                <relPosition x="220" y="10"/>
+              </connectionPointOut>
+              <expression>'RelativePageTest@/TRUMP2'</expression>
+            </inVariable>
+            <block localId="17" typeName="R_TRIG" instanceName="R_TRIG0" executionOrderId="0" height="40" width="60">
+              <position x="220" y="185"/>
+              <inputVariables>
+                <variable formalParameter="CLK">
+                  <connectionPointIn>
+                    <relPosition x="0" y="30"/>
+                    <connection refLocalId="15">
+                      <position x="220" y="215"/>
+                      <position x="190" y="215"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="Q">
+                  <connectionPointOut>
+                    <relPosition x="60" y="30"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
           </FBD>
         </body>
       </pou>