SVGHMI: Many fixes. Subscriptions to HMItree seems to be working, and dispatch function is called in JS with good data. Bidirectional communication now really working. svghmi
authorEdouard Tisserant
Thu, 17 Oct 2019 15:48:09 +0200
branchsvghmi
changeset 2799 f5da343b9b63
parent 2798 ddb2c4668a6b
child 2800 68cee1366b9c
SVGHMI: Many fixes. Subscriptions to HMItree seems to be working, and dispatch function is called in JS with good data. Bidirectional communication now really working.
svghmi/gen_index_xhtml.xslt
svghmi/gen_index_xhtml.ysl2
svghmi/svghmi.c
svghmi/svghmi.js
svghmi/svghmi_server.py
--- a/svghmi/gen_index_xhtml.xslt	Tue Oct 15 17:14:48 2019 +0200
+++ b/svghmi/gen_index_xhtml.xslt	Thu Oct 17 15:48:09 2019 +0200
@@ -179,7 +179,7 @@
     <func:result select="exsl:node-set($ast)"/>
   </func:function>
   <xsl:template name="scripts">
-    <xsl:text>(function(){
+    <xsl:text>//(function(){
 </xsl:text>
     <xsl:text>
 </xsl:text>
@@ -319,7 +319,7 @@
 </xsl:text>
     <xsl:text>function dispatch_value(index, value) {
 </xsl:text>
-    <xsl:text>    console.log("dispatch_value("+index+value+")");
+    <xsl:text>    console.log("dispatch_value("+index+", "+value+")");
 </xsl:text>
     <xsl:text>};
 </xsl:text>
@@ -357,74 +357,90 @@
 </xsl:text>
     <xsl:text>    let i = 0;
 </xsl:text>
-    <xsl:text>    for(let hash_int of hmi_hash) {
-</xsl:text>
-    <xsl:text>        if(hash_int != dv.getUint8(i)){
-</xsl:text>
-    <xsl:text>            console.log("Recv non maching hash. Reload.");
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>            // 1003 is for "Unsupported Data"
-</xsl:text>
-    <xsl:text>            ws.close(1003,"Hash doesn't match");
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>            // TODO : remove debug alert ?
-</xsl:text>
-    <xsl:text>            alert("HMI will be reloaded.");
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>            // force reload ignoring cache
-</xsl:text>
-    <xsl:text>            location.reload(true);
+    <xsl:text>    console.log("Recv something.");
+</xsl:text>
+    <xsl:text>    try {
+</xsl:text>
+    <xsl:text>        for(let hash_int of hmi_hash) {
+</xsl:text>
+    <xsl:text>            if(hash_int != dv.getUint8(i)){
+</xsl:text>
+    <xsl:text>                throw new Error("Hash doesn't match")
+</xsl:text>
+    <xsl:text>            };
+</xsl:text>
+    <xsl:text>            i++;
 </xsl:text>
     <xsl:text>        };
 </xsl:text>
-    <xsl:text>        i++;
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        console.log("Recv something GOOD.");
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        while(i &lt; data.byteLength){
+</xsl:text>
+    <xsl:text>            let index = dv.getUint32(i, true);
+</xsl:text>
+    <xsl:text>            console.log("Recv something index is "+index);
+</xsl:text>
+    <xsl:text>            i += 4;
+</xsl:text>
+    <xsl:text>            let iectype = hmitree_types[index];
+</xsl:text>
+    <xsl:text>            if(iectype != undefined){
+</xsl:text>
+    <xsl:text>                let [dvgetter, bytesize] = dvgetters[iectype];
+</xsl:text>
+    <xsl:text>                let value = dvgetter.call(dv,i,true);
+</xsl:text>
+    <xsl:text>                dispatch_value(index, value);
+</xsl:text>
+    <xsl:text>                i += bytesize;
+</xsl:text>
+    <xsl:text>            } else {
+</xsl:text>
+    <xsl:text>                throw new Error("Unknown index "+index)
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        };
+</xsl:text>
+    <xsl:text>    } catch(err) {
+</xsl:text>
+    <xsl:text>        // 1003 is for "Unsupported Data"
+</xsl:text>
+    <xsl:text>        // ws.close(1003, err.message);
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        // TODO : remove debug alert ?
+</xsl:text>
+    <xsl:text>        alert("Error : "+err.message+"\nHMI will be reloaded.");
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        // force reload ignoring cache
+</xsl:text>
+    <xsl:text>        location.reload(true);
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function send_blob(data) {
+</xsl:text>
+    <xsl:text>    if(data.length &gt; 0) {
+</xsl:text>
+    <xsl:text>        ws.send(new Blob([new Uint8Array(hmi_hash)].concat(data)));
 </xsl:text>
     <xsl:text>    };
 </xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    while(i &lt; data.length){
-</xsl:text>
-    <xsl:text>        let index = dv.getUint32(i);
-</xsl:text>
-    <xsl:text>        i += 4;
-</xsl:text>
-    <xsl:text>        let iectype = hmitree_types[index];
-</xsl:text>
-    <xsl:text>        let [dvgetter, bytesize] = dvgetters[iectypes];
-</xsl:text>
-    <xsl:text>        value = dvgetter.call(dv,i);
-</xsl:text>
-    <xsl:text>        dispatch_value(index, value);
-</xsl:text>
-    <xsl:text>        i += bytesize;
-</xsl:text>
-    <xsl:text>    };
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function send_blob(data) {
-</xsl:text>
-    <xsl:text>    if(data.length &gt; 0) {
-</xsl:text>
-    <xsl:text>        ws.send(new Blob([
-</xsl:text>
-    <xsl:text>            new Uint8Array(hmi_hash), 
-</xsl:text>
-    <xsl:text>            data]));
-</xsl:text>
-    <xsl:text>    };
-</xsl:text>
     <xsl:text>};
 </xsl:text>
     <xsl:text>
@@ -481,7 +497,7 @@
 </xsl:text>
     <xsl:text>
 </xsl:text>
-    <xsl:text>        let new_period;
+    <xsl:text>        let new_period = 0;
 </xsl:text>
     <xsl:text>        if(widgets.size &gt; 0) {
 </xsl:text>
@@ -489,33 +505,31 @@
 </xsl:text>
     <xsl:text>            for(let widget of widgets)
 </xsl:text>
-    <xsl:text>                if(maxfreq &lt; widgets.frequency)
-</xsl:text>
-    <xsl:text>                    maxfreq = widgets.frequency;
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>            new_period = 1000/maxfreq;
-</xsl:text>
-    <xsl:text>        } else {
-</xsl:text>
-    <xsl:text>            new_period = 0;
+    <xsl:text>                if(maxfreq &lt; widget.frequency)
+</xsl:text>
+    <xsl:text>                    maxfreq = widget.frequency;
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>            if(maxfreq != 0)
+</xsl:text>
+    <xsl:text>                new_period = 1000/maxfreq;
 </xsl:text>
     <xsl:text>        }
 </xsl:text>
-    <xsl:text>
+    <xsl:text>         
 </xsl:text>
     <xsl:text>        if(previous_period != new_period) {
 </xsl:text>
     <xsl:text>            subscriptions[index] = new_period;
 </xsl:text>
-    <xsl:text>            delta.push([
+    <xsl:text>            delta.push(new Blob([
 </xsl:text>
     <xsl:text>                new Uint8Array([2]), /* subscribe = 2 */
 </xsl:text>
     <xsl:text>                new Uint32Array([index]), 
 </xsl:text>
-    <xsl:text>                new Uint16Array([new_period])]);
+    <xsl:text>                new Uint16Array([new_period])]));
 </xsl:text>
     <xsl:text>        }
 </xsl:text>
@@ -613,13 +627,27 @@
 </xsl:text>
     <xsl:text>    switch_page(default_page);
 </xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>})();
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>ws.onclose = function (evt) {
+</xsl:text>
+    <xsl:text>    // TODO : add visible notification while waiting for reload
+</xsl:text>
+    <xsl:text>    console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in 10s.");
+</xsl:text>
+    <xsl:text>    // TODO : re-enable auto reload when not in debug
+</xsl:text>
+    <xsl:text>    //window.setTimeout(() =&gt; location.reload(true), 10000);
+</xsl:text>
+    <xsl:text>    alert("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+".");
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>//})();
 </xsl:text>
   </xsl:template>
   <xsl:template mode="page_desc" match="*"/>
--- a/svghmi/gen_index_xhtml.ysl2	Tue Oct 15 17:14:48 2019 +0200
+++ b/svghmi/gen_index_xhtml.ysl2	Thu Oct 17 15:48:09 2019 +0200
@@ -164,7 +164,7 @@
 
     function "scripts"
     {
-        | (function(){
+        | //(function(){
         |
         | var hmi_hash = [«$hmitree/@hash»]; 
 
@@ -242,7 +242,7 @@
         | var default_page = "«$default_page»";
 
         include text svghmi.js
-        | })();
+        | //})();
     }
 
     /*
--- a/svghmi/svghmi.c	Tue Oct 15 17:14:48 2019 +0200
+++ b/svghmi/svghmi.c	Thu Oct 17 15:48:09 2019 +0200
@@ -65,11 +65,13 @@
 {
     unsigned int i;
     for(i = 0; i < sizeof(hmi_tree_item)/sizeof(hmi_tree_item_t); i++){
-        int res;
         hmi_tree_item_t *dsc = &hmi_tree_item[i];
-        if(res = (*fp)(i, dsc))
+        int res = (*fp)(i, dsc);
+        if(res != 0){
             return res;
-    }
+        }
+    }
+    return 0;
 }
 
 #define __Unpack_desc_type hmi_tree_item_t
@@ -116,16 +118,16 @@
 
 static int send_iterator(uint32_t index, hmi_tree_item_t *dsc)
 {
-    int res = 0;
     while(AtomicCompareExchange(&dsc->wlock, 0, 1)) sched_yield();
 
     if(dsc->wstate == buf_tosend)
     {
         uint32_t sz = __get_type_enum_size(dsc->type);
-        if(sbufidx + sizeof(uint32_t) + sz <  sizeof(sbuf))
+        if(sbufidx + sizeof(uint32_t) + sz <=  sizeof(sbuf))
         {
             void *src_p = &wbuf[dsc->buf_index];
             void *dst_p = &sbuf[sbufidx];
+            /* TODO : force into little endian */
             memcpy(dst_p, &index, sizeof(uint32_t));
             memcpy(dst_p + sizeof(uint32_t), src_p, sz);
             dsc->wstate = buf_free;
@@ -133,12 +135,14 @@
         }
         else
         {
-            res = EOVERFLOW;
+            printf("BUG!!! %%d + %%ld + %%d >  %%ld \n", sbufidx, sizeof(uint32_t), sz,  sizeof(sbuf));
+            AtomicCompareExchange(&dsc->wlock, 1, 0);
+            return EOVERFLOW; 
         }
     }
 
     AtomicCompareExchange(&dsc->wlock, 1, 0);
-    return res; 
+    return 0; 
 }
 
 static int read_iterator(uint32_t index, hmi_tree_item_t *dsc)
@@ -224,13 +228,18 @@
 
     if(do_collect) {
         int res;
-        memcpy(&sbuf[0], &hmi_hash[0], HMI_HASH_SIZE);
         sbufidx = HMI_HASH_SIZE;
         if((res = traverse_hmi_tree(send_iterator)) == 0)
         {
-            *ptr = &sbuf[0];
-            *size = sbufidx;
-        }
+            if(sbufidx > HMI_HASH_SIZE){
+                memcpy(&sbuf[0], &hmi_hash[0], HMI_HASH_SIZE);
+                *ptr = &sbuf[0];
+                *size = sbufidx;
+                return 0;
+            }
+            return ENODATA;
+        }
+        // printf("collected BAD result %%d\n", res);
         return res;
     }
     else
@@ -249,7 +258,6 @@
     const uint8_t* cursor = ptr + HMI_HASH_SIZE;
     const uint8_t* end = ptr + size;
 
-    printf("svghmi_recv_dispatch %%d\n",size);
 
     /* match hmitree fingerprint */
     if(size <= HMI_HASH_SIZE || memcmp(ptr, hmi_hash, HMI_HASH_SIZE) != 0)
@@ -289,9 +297,15 @@
                         AtomicCompareExchange(&dsc->rlock, 1, 0);
                         progress = sz + sizeof(uint32_t) /* index */;
                     }
-                    else return -EINVAL;
-                }
-                else return -EINVAL;
+                    else 
+                    {
+                        return -EINVAL;
+                    }
+                }
+                else 
+                {
+                    return -EINVAL;
+                }
             }
             break;
 
@@ -311,13 +325,19 @@
                 {
                     hmi_tree_item_t *dsc = &hmi_tree_item[index];
                     update_refresh_period(dsc, refresh_period_ms);
-                }
-                else return -EINVAL;
+                    printf("OK\n");
+                }
+                else 
+                {
+                    return -EINVAL;
+                }
 
                 progress = sizeof(uint32_t) /* index */ + 
                            sizeof(uint16_t) /* refresh period */;
             }
             break;
+            default:
+                printf("svghmi_recv_dispatch unknown %%d\n",cmd);
 
         }
         cursor += progress;
--- a/svghmi/svghmi.js	Tue Oct 15 17:14:48 2019 +0200
+++ b/svghmi/svghmi.js	Thu Oct 17 15:48:09 2019 +0200
@@ -1,7 +1,7 @@
 // svghmi.js
 
 function dispatch_value(index, value) {
-    console.log("dispatch_value("+index+value+")");
+    console.log("dispatch_value("+index+", "+value+")");
 };
 
 // Open WebSocket to relative "/ws" address
@@ -20,39 +20,44 @@
     let data = evt.data;
     let dv = new DataView(data);
     let i = 0;
-    for(let hash_int of hmi_hash) {
-        if(hash_int != dv.getUint8(i)){
-            console.log("Recv non maching hash. Reload.");
+    try {
+        for(let hash_int of hmi_hash) {
+            if(hash_int != dv.getUint8(i)){
+                throw new Error("Hash doesn't match")
+            };
+            i++;
+        };
 
-            // 1003 is for "Unsupported Data"
-            ws.close(1003,"Hash doesn't match");
+        while(i < data.byteLength){
+            let index = dv.getUint32(i, true);
+            console.log("Recv something index is "+index);
+            i += 4;
+            let iectype = hmitree_types[index];
+            if(iectype != undefined){
+                let [dvgetter, bytesize] = dvgetters[iectype];
+                let value = dvgetter.call(dv,i,true);
+                dispatch_value(index, value);
+                i += bytesize;
+            } else {
+                throw new Error("Unknown index "+index)
+            }
+        };
+    } catch(err) {
+        // 1003 is for "Unsupported Data"
+        // ws.close(1003, err.message);
 
-            // TODO : remove debug alert ?
-            alert("HMI will be reloaded.");
+        // TODO : remove debug alert ?
+        alert("Error : "+err.message+"\\\\nHMI will be reloaded.");
 
-            // force reload ignoring cache
-            location.reload(true);
-        };
-        i++;
-    };
-
-    while(i < data.length){
-        let index = dv.getUint32(i);
-        i += 4;
-        let iectype = hmitree_types[index];
-        let [dvgetter, bytesize] = dvgetters[iectypes];
-        value = dvgetter.call(dv,i);
-        dispatch_value(index, value);
-        i += bytesize;
-    };
+        // force reload ignoring cache
+        location.reload(true);
+    }
 };
 
 
 function send_blob(data) {
     if(data.length > 0) {
-        ws.send(new Blob([
-            new Uint8Array(hmi_hash), 
-            data]));
+        ws.send(new Blob([new Uint8Array(hmi_hash)].concat(data)));
     };
 };
 
@@ -82,24 +87,23 @@
         // periods are in ms
         let previous_period = subscriptions[index];
 
-        let new_period;
+        let new_period = 0;
         if(widgets.size > 0) {
             let maxfreq = 0;
             for(let widget of widgets)
-                if(maxfreq < widgets.frequency)
-                    maxfreq = widgets.frequency;
+                if(maxfreq < widget.frequency)
+                    maxfreq = widget.frequency;
 
-            new_period = 1000/maxfreq;
-        } else {
-            new_period = 0;
+            if(maxfreq != 0)
+                new_period = 1000/maxfreq;
         }
 
         if(previous_period != new_period) {
             subscriptions[index] = new_period;
-            delta.push([
+            delta.push(new Blob([
                 new Uint8Array([2]), /* subscribe = 2 */
                 new Uint32Array([index]), 
-                new Uint16Array([new_period])]);
+                new Uint16Array([new_period])]));
         }
         
     }
@@ -148,6 +152,13 @@
     send_reset();
     // show main page
     switch_page(default_page);
+};
+
+ws.onclose = function (evt) {
+    // TODO : add visible notification while waiting for reload
+    console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in 10s.");
+    // TODO : re-enable auto reload when not in debug
+    //window.setTimeout(() => location.reload(true), 10000);
+    alert("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+".");
 
 };
-
--- a/svghmi/svghmi_server.py	Tue Oct 15 17:14:48 2019 +0200
+++ b/svghmi/svghmi_server.py	Thu Oct 17 15:48:09 2019 +0200
@@ -6,6 +6,7 @@
 # See COPYING file for copyrights details.
 
 from __future__ import absolute_import
+import errno
 
 from twisted.web.server import Site
 from twisted.web.resource import Resource
@@ -13,6 +14,7 @@
 from twisted.web.static import File
 
 from autobahn.twisted.websocket import WebSocketServerFactory, WebSocketServerProtocol
+from autobahn.websocket.protocol import WebSocketProtocol
 from autobahn.twisted.resource import  WebSocketResource
 
 # TODO multiclient :
@@ -25,7 +27,7 @@
 svghmi_send_collect.restype = ctypes.c_int # error or 0
 svghmi_send_collect.argtypes = [
     ctypes.POINTER(ctypes.c_uint32),  # size
-    ctypes.POINTER(ctypes.c_char_p)]  # data ptr
+    ctypes.POINTER(ctypes.c_void_p)]  # data ptr
 # TODO multiclient : switch to arrays
 
 svghmi_recv_dispatch = PLCBinary.svghmi_recv_dispatch
@@ -38,10 +40,11 @@
 class HMISession(object):
     def __init__(self, protocol_instance):
         global svghmi_session
-
-        # TODO: kill existing session for robustness
-        assert(svghmi_session is None)
-
+        
+        # Single client :
+        # Creating a new HMISession closes pre-existing HMISession
+        if svghmi_session is not None:
+            svghmi_session.close()
         svghmi_session = self
         self.protocol_instance = protocol_instance
 
@@ -50,13 +53,11 @@
         # get a unique bit index amont other svghmi_sessions,
         # so that we can match flags passed by C->python callback
 
-    def __del__(self):
+    def close(self):
         global svghmi_session
-        assert(svghmi_session)
-        svghmi_session = None
-
-        # TODO multiclient :
-        # svghmi_sessions.remove(self)
+        if svghmi_session == self:
+            svghmi_session = None
+        self.protocol_instance.sendClose(WebSocketProtocol.CLOSE_STATUS_CODE_NORMAL)
 
     def onMessage(self, msg):
         # pass message to the C side recieve_message()
@@ -66,7 +67,8 @@
 
 
     def sendMessage(self, msg):
-        self.sendMessage(msg, True)
+        self.protocol_instance.sendMessage(msg, True)
+        return 0
 
 class HMIProtocol(WebSocketServerProtocol):
 
@@ -75,18 +77,15 @@
         WebSocketServerProtocol.__init__(self, *args, **kwargs)
 
     def onOpen(self):
+        assert(self._hmi_session is None)
         self._hmi_session = HMISession(self)
-        print "open"
 
     def onClose(self, wasClean, code, reason):
-        del self._hmi_session
         self._hmi_session = None
-        print "close"
 
     def onMessage(self, msg, isBinary):
+        assert(self._hmi_session is not None)
         self._hmi_session.onMessage(msg)
-        # print msg
-        #self.sendMessage(msg, binary)
 
 class HMIWebSocketServerFactory(WebSocketServerFactory):
     protocol = HMIProtocol
@@ -98,14 +97,17 @@
 def SendThreadProc():
    global svghmi_session
    size = ctypes.c_uint32()
-   ptr = ctypes.c_char_p()
+   ptr = ctypes.c_void_p()
    res = 0
-   while svghmi_send_collect(ctypes.byref(size), ctypes.byref(ptr)) == 0 and \
-         svghmi_session is not None and \
-         svghmi_session.sendMessage(ctypes.string_at(ptr,size)) == 0:
-         pass
+   while True:
+       res=svghmi_send_collect(ctypes.byref(size), ctypes.byref(ptr))
+       if res == 0:
+           # TODO multiclient : dispatch to sessions
+           if svghmi_session is not None:
+               svghmi_session.sendMessage(ctypes.string_at(ptr.value,size.value))
+       elif res not in [errno.EAGAIN, errno.ENODATA]:
+           break
 
-       # TODO multiclient : dispatch to sessions
 
 
 
@@ -125,7 +127,9 @@
 
 # Called by PLCObject at stop
 def _runtime_svghmi0_stop():
-    global svghmi_listener, svghmi_root, svghmi_send_thread
+    global svghmi_listener, svghmi_root, svghmi_send_thread, svghmi_session
+    if svghmi_session is not None:
+        svghmi_session.close()
     svghmi_root.delEntity("ws")
     svghmi_root = None
     svghmi_listener.stopListening()