--- 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>var heartbeat_index = </xsl:text> + <xsl:value-of select="$indexed_hmitree/*[@hmipath = '/HEARTBEAT']/@index"/> <xsl:text>var hmitree_types = [
<xsl:for-each select="$indexed_hmitree/*">
@@ -576,6 +582,32 @@
+ <xsl:text>// artificially subscribe the watchdog widget to "/heartbeat" hmi variable + <xsl:text>// Since dispatch directly calls change_hmi_value, + <xsl:text>// PLC will periodically send variable at given frequency + <xsl:text>subscribers[heartbeat_index].add({ + <xsl:text> /* type: "Watchdog", */ + <xsl:text> frequency: 1, + <xsl:text> indexes: [heartbeat_index], + <xsl:text> dispatch: function(value) { + <xsl:text> console.log("Heartbeat" + value); + <xsl:text> change_hmi_value(this.indexes[0], "+1"); <xsl:text>function update_subscriptions() {
<xsl:text> let delta = [];
@@ -592,6 +624,8 @@
+ <xsl:text> // subscribing with a zero period is unsubscribing <xsl:text> let new_period = 0;
<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»; 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 @@
+// 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;
/* match hmitree fingerprint */
if(size <= HMI_HASH_SIZE || memcmp(ptr, hmi_hash, HMI_HASH_SIZE) != 0)
printf("svghmi_recv_dispatch MISMATCH !!\n");
@@ -279,6 +282,9 @@
uint32_t index = *(uint32_t*)(cursor);
uint8_t const *valptr = cursor + sizeof(uint32_t);
+ if(index == heartbeat_index) if(index < HMI_ITEM_COUNT)
@@ -344,6 +350,6 @@
--- 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", */ + indexes: [heartbeat_index], + dispatch: function(value) { + console.log("Heartbeat" + value); + change_hmi_value(this.indexes[0], "+1"); function update_subscriptions() {
for(let index = 0; index < subscribers.length; index++){
@@ -123,6 +136,7 @@
let previous_period = subscriptions[index];
+ // subscribing with a zero period is unsubscribing --- 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 @@
from itertools import izip, imap
-from pprint import pprint, pformat
+from pprint import pformat @@ -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): @@ -52,6 +52,8 @@
if nodetype in ["HMI_NODE", "HMI_ROOT"]:
@@ -133,12 +135,15 @@
+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 @@ -179,25 +184,21 @@
hmi_tree_root = HMITreeNode(None, "/", "HMI_ROOT")
- 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_"):
if derived == "HMI_NODE":
+ # TODO : make problem if HMI_NODE used in CONFIG or RESOURCE kwargs['hmiclass'] = 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 = []
+ 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 + extern_variables_declarations += [ + "#define heartbeat_index "+str(hmi_tree_hearbeat_index) + for node in hmi_tree_root.traverse(): + if hasattr(node, "iectype") and node.nodetype != "HMI_NODE": sz = DebugTypesSize.get(node.iectype, 0)
- "{&(" + ".".join(node.path) + "), " + node.iectype + {
+ "{&(" + node.cpath + "), " + node.iectype + { @@ -226,11 +241,13 @@
extern_variables_declarations += [
"extern __IEC_" + node.iectype + "_" +
"t" if node.vartype is "VAR" else "p"
- + ".".join(node.path) + ";"]
+ assert(found_heartbeat) # TODO : filter only requiered external declarations
- 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):
+ 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
+from threading import RLock, Timer from twisted.web.server import Site
from twisted.web.resource import Resource
@@ -20,8 +21,10 @@
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)
+ def __init__(self, initial_timeout, callback): + self._callback = callback + self.initial_timeout = initial_timeout + self.callback = callback + self.timer = Timer(self.initial_timeout, self.trigger) + if self.timer is not None: 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: class HMIWebSocketServerFactory(WebSocketServerFactory):
@@ -114,11 +151,13 @@
+ 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.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() if svghmi_session is not None:
svghmi_root.delEntity("ws")