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
--- 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 > 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")