SVGHMI: added a watchdog. To ensure that the whole chain is checked, watchdog use a periodic echo of a hearteat variable. JS client code systematically register /HEARTBEAT at 1s update freq, and reacts on updates of /HEARTBEAT by systematically incrementing it. C code catch /HEARTBEAT update and feeds python-implemented watchdog. For now, watchdog does nothing when tiggered svghmi
authorEdouard Tisserant
Fri, 10 Jan 2020 13:15:07 +0100
branchsvghmi
changeset 2822 9101a72a1da0
parent 2821 d92d201d22e1
child 2823 d631f8671c75
SVGHMI: added a watchdog. To ensure that the whole chain is checked, watchdog use a periodic echo of a hearteat variable. JS client code systematically register /HEARTBEAT at 1s update freq, and reacts on updates of /HEARTBEAT by systematically incrementing it. C code catch /HEARTBEAT update and feeds python-implemented watchdog. For now, watchdog does nothing when tiggered
svghmi/gen_index_xhtml.xslt
svghmi/gen_index_xhtml.ysl2
svghmi/svghmi.c
svghmi/svghmi.js
svghmi/svghmi.py
svghmi/svghmi_server.py
--- a/svghmi/gen_index_xhtml.xslt	Wed Dec 18 13:31:22 2019 +0100
+++ b/svghmi/gen_index_xhtml.xslt	Fri Jan 10 13:15:07 2020 +0100
@@ -264,6 +264,12 @@
 </xsl:text>
     <xsl:text>
 </xsl:text>
+    <xsl:text>var heartbeat_index = </xsl:text>
+    <xsl:value-of select="$indexed_hmitree/*[@hmipath = '/HEARTBEAT']/@index"/>
+    <xsl:text>;
+</xsl:text>
+    <xsl:text>
+</xsl:text>
     <xsl:text>var hmitree_types = [
 </xsl:text>
     <xsl:for-each select="$indexed_hmitree/*">
@@ -576,6 +582,32 @@
 </xsl:text>
     <xsl:text>
 </xsl:text>
+    <xsl:text>// artificially subscribe the watchdog widget to "/heartbeat" hmi variable
+</xsl:text>
+    <xsl:text>// Since dispatch directly calls change_hmi_value, 
+</xsl:text>
+    <xsl:text>// PLC will periodically send variable at given frequency
+</xsl:text>
+    <xsl:text>subscribers[heartbeat_index].add({
+</xsl:text>
+    <xsl:text>    /* type: "Watchdog", */
+</xsl:text>
+    <xsl:text>    frequency: 1,
+</xsl:text>
+    <xsl:text>    indexes: [heartbeat_index],
+</xsl:text>
+    <xsl:text>    dispatch: function(value) {
+</xsl:text>
+    <xsl:text>        console.log("Heartbeat" + value);
+</xsl:text>
+    <xsl:text>        change_hmi_value(this.indexes[0], "+1");
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>});
+</xsl:text>
+    <xsl:text>
+</xsl:text>
     <xsl:text>function update_subscriptions() {
 </xsl:text>
     <xsl:text>    let delta = [];
@@ -592,6 +624,8 @@
 </xsl:text>
     <xsl:text>
 </xsl:text>
+    <xsl:text>        // subscribing with a zero period is unsubscribing
+</xsl:text>
     <xsl:text>        let new_period = 0;
 </xsl:text>
     <xsl:text>        if(widgets.size &gt; 0) {
--- a/svghmi/gen_index_xhtml.ysl2	Wed Dec 18 13:31:22 2019 +0100
+++ b/svghmi/gen_index_xhtml.ysl2	Fri Jan 10 13:15:07 2020 +0100
@@ -238,6 +238,8 @@
         }
         | }
         |
+        | var heartbeat_index = «$indexed_hmitree/*[@hmipath = '/HEARTBEAT']/@index»;
+        |
         | var hmitree_types = [
 
         foreach "$indexed_hmitree/*" {
--- a/svghmi/svghmi.c	Wed Dec 18 13:31:22 2019 +0100
+++ b/svghmi/svghmi.c	Fri Jan 10 13:15:07 2020 +0100
@@ -257,16 +257,19 @@
     subscribe = 2
 } cmd_from_JS;
 
+// Returns :
+//   0 is OK, <0 is error, 1 is heartbeat
 int svghmi_recv_dispatch(uint32_t size, const uint8_t *ptr){
     const uint8_t* cursor = ptr + HMI_HASH_SIZE;
     const uint8_t* end = ptr + size;
 
+    int was_hearbeat = 0;
 
     /* match hmitree fingerprint */
     if(size <= HMI_HASH_SIZE || memcmp(ptr, hmi_hash, HMI_HASH_SIZE) != 0)
     {
         printf("svghmi_recv_dispatch MISMATCH !!\n");
-        return EINVAL;
+        return -EINVAL;
     }
 
     while(cursor < end)
@@ -279,6 +282,9 @@
             {
                 uint32_t index = *(uint32_t*)(cursor);
                 uint8_t const *valptr = cursor + sizeof(uint32_t);
+                
+                if(index == heartbeat_index)
+                    was_hearbeat = 1;
 
                 if(index < HMI_ITEM_COUNT)
                 {
@@ -344,6 +350,6 @@
         }
         cursor += progress;
     }
-    return 0;
-}
-
+    return was_hearbeat;
+}
+
--- a/svghmi/svghmi.js	Wed Dec 18 13:31:22 2019 +0100
+++ b/svghmi/svghmi.js	Fri Jan 10 13:15:07 2020 +0100
@@ -115,6 +115,19 @@
 // hmitree indexed array of Sets of widgets objects
 var subscribers = hmitree_types.map(_ignored => new Set());
 
+// 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],
+    dispatch: function(value) {
+        console.log("Heartbeat" + value);
+        change_hmi_value(this.indexes[0], "+1");
+    }
+});
+
 function update_subscriptions() {
     let delta = [];
     for(let index = 0; index < subscribers.length; index++){
@@ -123,6 +136,7 @@
         // periods are in ms
         let previous_period = subscriptions[index];
 
+        // subscribing with a zero period is unsubscribing
         let new_period = 0;
         if(widgets.size > 0) {
             let maxfreq = 0;
--- a/svghmi/svghmi.py	Wed Dec 18 13:31:22 2019 +0100
+++ b/svghmi/svghmi.py	Fri Jan 10 13:15:07 2020 +0100
@@ -10,7 +10,7 @@
 import os
 import shutil
 from itertools import izip, imap
-from pprint import pprint, pformat
+from pprint import pformat
 import hashlib
 import weakref
 
@@ -43,7 +43,7 @@
 ScriptDirectory = paths.AbsDir(__file__)
 
 class HMITreeNode(object):
-    def __init__(self, path, name, nodetype, iectype = None, vartype = None, hmiclass = None):
+    def __init__(self, path, name, nodetype, iectype = None, vartype = None, cpath = None, hmiclass = None):
         self.path = path
         self.name = name
         self.nodetype = nodetype
@@ -52,6 +52,8 @@
         if iectype is not None:
             self.iectype = iectype
             self.vartype = vartype
+            self.cpath = cpath
+
         if nodetype in ["HMI_NODE", "HMI_ROOT"]:
             self.children = []
 
@@ -133,12 +135,15 @@
 
 on_hmitree_update = None
 
+SPECIAL_NODES = [("heartbeat", "HMI_INT")]
+                 # ("current_page", "HMI_STRING")])
+
 class SVGHMILibrary(POULibrary):
     def GetLibraryPath(self):
          return paths.AbsNeighbourFile(__file__, "pous.xml")
 
     def Generate_C(self, buildpath, varlist, IECCFLAGS):
-        global hmi_tree_root, on_hmitree_update, hmi_tree_unique_id
+        global hmi_tree_root, on_hmitree_update
 
         """
         PLC Instance Tree:
@@ -179,25 +184,21 @@
 
         hmi_tree_root = HMITreeNode(None, "/", "HMI_ROOT")
 
-        # add special nodes
-        map(lambda (n,t): hmi_tree_root.children.append(HMITreeNode(None,n,t)), [
-                ("plc_status", "HMI_PLC_STATUS"),
-                ("current_page", "HMI_CURRENT_PAGE")])
-
         # deduce HMI tree from PLC HMI_* instances
         for v in hmi_types_instances:
-            path = v["C_path"].split(".")
+            path = v["IEC_path"].split(".")
             # ignores variables starting with _TMP_
             if path[-1].startswith("_TMP_"):
                 continue
             derived = v["derived"]
             kwargs={}
             if derived == "HMI_NODE":
+                # TODO : make problem if HMI_NODE used in CONFIG or RESOURCE
                 name = path[-2]
                 kwargs['hmiclass'] = path[-1]
             else:
                 name = path[-1]
-            new_node = HMITreeNode(path, name, derived, v["type"], v["vartype"], **kwargs)
+            new_node = HMITreeNode(path, name, derived, v["type"], v["vartype"], v["C_path"], **kwargs)
             hmi_tree_root.place_node(new_node)
 
         if on_hmitree_update is not None:
@@ -207,12 +208,26 @@
         extern_variables_declarations = []
         buf_index = 0
         item_count = 0
+        found_heartbeat = False
+
+        # find heartbeat in hmi tree
+        # it is supposed to come first, but some HMI_* intances 
+        # in config's globals might shift them
+        hearbeat_IEC_path = ['CONFIG', 'HEARTBEAT']
         for node in hmi_tree_root.traverse():
-            if hasattr(node, "iectype") and \
-               node.nodetype not in ["HMI_NODE"]:
+            if node.path == hearbeat_IEC_path:
+                hmi_tree_hearbeat_index = item_count
+                found_heartbeat = True
+                extern_variables_declarations += [
+                    "#define heartbeat_index "+str(hmi_tree_hearbeat_index)
+                ]
+                break;
+
+        for node in hmi_tree_root.traverse():
+            if hasattr(node, "iectype") and node.nodetype != "HMI_NODE":
                 sz = DebugTypesSize.get(node.iectype, 0)
                 variable_decl_array += [
-                    "{&(" + ".".join(node.path) + "), " + node.iectype + {
+                    "{&(" + node.cpath + "), " + node.iectype + {
                         "EXT": "_P_ENUM",
                         "IN":  "_P_ENUM",
                         "MEM": "_O_ENUM",
@@ -226,11 +241,13 @@
                     extern_variables_declarations += [
                         "extern __IEC_" + node.iectype + "_" +
                         "t" if node.vartype is "VAR" else "p"
-                        + ".".join(node.path) + ";"]
+                        + node.cpath + ";"]
+                
+        assert(found_heartbeat)
 
         # TODO : filter only requiered external declarations
         for v in varlist :
-            if v["C_path"].find('.') < 0 and v["vartype"] == "FB" :
+            if v["C_path"].find('.') < 0 :  # and v["vartype"] == "FB" :
                 extern_variables_declarations += [
                     "extern %(type)s %(C_path)s;" % v]
 
@@ -530,3 +547,10 @@
             if not os.path.isfile(svgfile):
                 svgfile = None
             open_svg(svgfile)
+
+    def CTNGlobalInstances(self):
+        # view_name = self.BaseParams.getName()
+        # return [ (view_name + "_" + name, iec_type, "") for name, iec_type in SPECIAL_NODES]
+        # TODO : move to library level for multiple hmi
+        return [(name, iec_type, "") for name, iec_type in SPECIAL_NODES]
+
--- a/svghmi/svghmi_server.py	Wed Dec 18 13:31:22 2019 +0100
+++ b/svghmi/svghmi_server.py	Fri Jan 10 13:15:07 2020 +0100
@@ -7,6 +7,7 @@
 
 from __future__ import absolute_import
 import errno
+from threading import RLock, Timer
 
 from twisted.web.server import Site
 from twisted.web.resource import Resource
@@ -20,8 +21,10 @@
 # TODO multiclient :
 # session list lock
 # svghmi_sessions = []
+# svghmi_watchdogs = []
 
 svghmi_session = None
+svghmi_watchdog = None
 
 svghmi_send_collect = PLCBinary.svghmi_send_collect
 svghmi_send_collect.restype = ctypes.c_int # error or 0
@@ -61,15 +64,45 @@
 
     def onMessage(self, msg):
         # pass message to the C side recieve_message()
-        svghmi_recv_dispatch(len(msg), msg)
+        return svghmi_recv_dispatch(len(msg), msg)
 
         # TODO multiclient : pass client index as well
 
-
     def sendMessage(self, msg):
         self.protocol_instance.sendMessage(msg, True)
         return 0
 
+class Watchdog(object):
+    def __init__(self, initial_timeout, callback):
+        self._callback = callback
+        self.lock = RLock()
+        self.initial_timeout = initial_timeout
+        self.callback = callback
+        with self.lock:
+            self._start()
+
+    def _start(self):
+        self.timer = Timer(self.initial_timeout, self.trigger)
+        self.timer.start()
+
+    def _stop(self):
+        if self.timer is not None:
+            self.timer.cancel()
+            self.timer = None
+
+    def cancel(self):
+        with self.lock:
+            self._stop()
+
+    def feed(self):
+        with self.lock:
+            self._stop()
+            self._start()
+
+    def trigger(self):
+        self._callback()
+        self.feed()
+
 class HMIProtocol(WebSocketServerProtocol):
 
     def __init__(self, *args, **kwargs):
@@ -85,7 +118,11 @@
 
     def onMessage(self, msg, isBinary):
         assert(self._hmi_session is not None)
-        self._hmi_session.onMessage(msg)
+
+        result = self._hmi_session.onMessage(msg)
+        if result == 1 :  # was heartbeat
+            if svghmi_watchdog is not None:
+                svghmi_watchdog.feed()
 
 class HMIWebSocketServerFactory(WebSocketServerFactory):
     protocol = HMIProtocol
@@ -114,11 +151,13 @@
             break
 
 
-
+def watchdog_trigger():
+    print("SVGHMI watchdog trigger")
+    
 
 # Called by PLCObject at start
 def _runtime_svghmi0_start():
-    global svghmi_listener, svghmi_root, svghmi_send_thread
+    global svghmi_listener, svghmi_root, svghmi_send_thread, svghmi_watchdog
 
     svghmi_root = Resource()
     svghmi_root.putChild("ws", WebSocketResource(HMIWebSocketServerFactory()))
@@ -129,10 +168,16 @@
     svghmi_send_thread = Thread(target=SendThreadProc, name="SVGHMI Send")
     svghmi_send_thread.start()
 
+    svghmi_watchdog = Watchdog(5, watchdog_trigger)
 
 # Called by PLCObject at stop
 def _runtime_svghmi0_stop():
-    global svghmi_listener, svghmi_root, svghmi_send_thread, svghmi_session
+    global svghmi_listener, svghmi_root, svghmi_send_thread, svghmi_session, svghmi_watchdog
+
+    if svghmi_watchdog is not None:
+        svghmi_watchdog.cancel()
+        svghmi_watchdog = None
+
     if svghmi_session is not None:
         svghmi_session.close()
     svghmi_root.delEntity("ws")