# HG changeset patch # User Edouard Tisserant # Date 1578658507 -3600 # Node ID 9101a72a1da0e06ff0aba9336952963a6eec3b38 # Parent d92d201d22e15f9e80f09db8a84d1efa286d6cc7 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 diff -r d92d201d22e1 -r 9101a72a1da0 svghmi/gen_index_xhtml.xslt --- 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 @@ + var heartbeat_index = + + ; + + + var hmitree_types = [ @@ -576,6 +582,32 @@ + // 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 = []; @@ -592,6 +624,8 @@ + // subscribing with a zero period is unsubscribing + let new_period = 0; if(widgets.size > 0) { diff -r d92d201d22e1 -r 9101a72a1da0 svghmi/gen_index_xhtml.ysl2 --- 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/*" { diff -r d92d201d22e1 -r 9101a72a1da0 svghmi/svghmi.c --- 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; +} + diff -r d92d201d22e1 -r 9101a72a1da0 svghmi/svghmi.js --- 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; diff -r d92d201d22e1 -r 9101a72a1da0 svghmi/svghmi.py --- 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] + diff -r d92d201d22e1 -r 9101a72a1da0 svghmi/svghmi_server.py --- 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")