SVGHMI: created widget_*.ysl2. Renamed widget_common in widgets_common, so that it doesn't match globing. svghmi
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Wed, 18 Mar 2020 09:16:14 +0100
branchsvghmi
changeset 2883 8e3d130399b0
parent 2882 ac08a5d15c15
child 2884 50b9832c51fc
SVGHMI: created widget_*.ysl2. Renamed widget_common in widgets_common, so that it doesn't match globing.
svghmi/gen_index_xhtml.xslt
svghmi/gen_index_xhtml.ysl2
svghmi/widget_common.ysl2
svghmi/widget_custom.ysl2
svghmi/widget_display.ysl2
svghmi/widget_input.ysl2
svghmi/widget_jump.ysl2
svghmi/widget_meter.ysl2
svghmi/widget_switch.ysl2
svghmi/widgets_common.ysl2
--- a/svghmi/gen_index_xhtml.xslt	Tue Mar 17 14:11:54 2020 +0100
+++ b/svghmi/gen_index_xhtml.xslt	Wed Mar 18 09:16:14 2020 +0100
@@ -624,998 +624,6 @@
       </xsl:choose>
     </xsl:for-each>
   </xsl:template>
-  <xsl:template match="/">
-    <xsl:comment>
-      <xsl:text>Made with SVGHMI. https://beremiz.org</xsl:text>
-    </xsl:comment>
-    <xsl:comment>
-      <xsl:text>
-</xsl:text>
-      <xsl:text>debug_hmitree:
-</xsl:text>
-      <xsl:call-template name="debug_hmitree"/>
-      <xsl:text>
-</xsl:text>
-    </xsl:comment>
-    <xsl:comment>
-      <xsl:text>
-</xsl:text>
-      <xsl:text>debug_geometry:
-</xsl:text>
-      <xsl:call-template name="debug_geometry"/>
-      <xsl:text>
-</xsl:text>
-    </xsl:comment>
-    <xsl:comment>
-      <xsl:text>
-</xsl:text>
-      <xsl:text>debug_detachables:
-</xsl:text>
-      <xsl:call-template name="debug_detachables"/>
-      <xsl:text>
-</xsl:text>
-    </xsl:comment>
-    <xsl:comment>
-      <xsl:text>
-</xsl:text>
-      <xsl:text>debug_unlink:
-</xsl:text>
-      <xsl:call-template name="debug_unlink"/>
-      <xsl:text>
-</xsl:text>
-    </xsl:comment>
-    <html xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/1999/xhtml">
-      <head/>
-      <body style="margin:0;overflow:hidden;">
-        <xsl:copy-of select="$result_svg"/>
-        <script>
-          <xsl:call-template name="scripts"/>
-        </script>
-      </body>
-    </html>
-  </xsl:template>
-  <xsl:template name="scripts">
-    <xsl:text>//(function(){
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>id = idstr =&gt; document.getElementById(idstr);
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>var hmi_hash = [</xsl:text>
-    <xsl:value-of select="$hmitree/@hash"/>
-    <xsl:text>]; 
-</xsl:text>
-    <xsl:text>var hmi_widgets = {
-</xsl:text>
-    <xsl:apply-templates mode="hmi_elements" select="$hmi_elements"/>
-    <xsl:text>}
-</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/*">
-      <xsl:text>    /* </xsl:text>
-      <xsl:value-of select="@index"/>
-      <xsl:text>  </xsl:text>
-      <xsl:value-of select="@hmipath"/>
-      <xsl:text> */ "</xsl:text>
-      <xsl:value-of select="substring(local-name(), 5)"/>
-      <xsl:text>"</xsl:text>
-      <xsl:if test="position()!=last()">
-        <xsl:text>,</xsl:text>
-      </xsl:if>
-      <xsl:text>
-</xsl:text>
-    </xsl:for-each>
-    <xsl:text>]
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>var detachable_elements = {
-</xsl:text>
-    <xsl:for-each select="$detachable_elements">
-      <xsl:text>    "</xsl:text>
-      <xsl:value-of select="@id"/>
-      <xsl:text>":[id("</xsl:text>
-      <xsl:value-of select="@id"/>
-      <xsl:text>"), id("</xsl:text>
-      <xsl:value-of select="../@id"/>
-      <xsl:text>")]</xsl:text>
-      <xsl:if test="position()!=last()">
-        <xsl:text>,</xsl:text>
-      </xsl:if>
-      <xsl:text>
-</xsl:text>
-    </xsl:for-each>
-    <xsl:text>}
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>var page_desc = {
-</xsl:text>
-    <xsl:apply-templates mode="page_desc" select="$hmi_pages"/>
-    <xsl:text>}
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>var default_page = "</xsl:text>
-    <xsl:value-of select="$default_page"/>
-    <xsl:text>";
-</xsl:text>
-    <xsl:text>var svg_root = id("</xsl:text>
-    <xsl:value-of select="/svg:svg/@id"/>
-    <xsl:text>");
-</xsl:text>
-    <xsl:text>// svghmi.js
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>var cache = hmitree_types.map(_ignored =&gt; undefined);
-</xsl:text>
-    <xsl:text>var updates = {};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function dispatch_value_to_widget(widget, index, value, oldval) {
-</xsl:text>
-    <xsl:text>    try {
-</xsl:text>
-    <xsl:text>        let idx = widget.offset ? index - widget.offset : index;
-</xsl:text>
-    <xsl:text>        let idxidx = widget.indexes.indexOf(idx);
-</xsl:text>
-    <xsl:text>        let d = widget.dispatch;
-</xsl:text>
-    <xsl:text>        console.log(index, idx, idxidx, value);
-</xsl:text>
-    <xsl:text>        if(typeof(d) == "function" &amp;&amp; idxidx == 0){
-</xsl:text>
-    <xsl:text>            d.call(widget, value, oldval);
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>        else if(typeof(d) == "object" &amp;&amp; d.length &gt;= idxidx){
-</xsl:text>
-    <xsl:text>            d[idxidx].call(widget, value, oldval);
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>        /* else dispatch_0, ..., dispatch_n ? */
-</xsl:text>
-    <xsl:text>        /*else {
-</xsl:text>
-    <xsl:text>            throw new Error("Dunno how to dispatch to widget at index = " + index);
-</xsl:text>
-    <xsl:text>        }*/
-</xsl:text>
-    <xsl:text>    } catch(err) {
-</xsl:text>
-    <xsl:text>        console.log(err);
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>}
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function dispatch_value(index, value) {
-</xsl:text>
-    <xsl:text>    let widgets = subscribers[index];
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    let oldval = cache[index];
-</xsl:text>
-    <xsl:text>    cache[index] = value;
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    if(widgets.size &gt; 0) {
-</xsl:text>
-    <xsl:text>        for(let widget of widgets){
-</xsl:text>
-    <xsl:text>            dispatch_value_to_widget(widget, index, value, oldval);
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function init_widgets() {
-</xsl:text>
-    <xsl:text>    Object.keys(hmi_widgets).forEach(function(id) {
-</xsl:text>
-    <xsl:text>        let widget = hmi_widgets[id];
-</xsl:text>
-    <xsl:text>        let init = widget.init;
-</xsl:text>
-    <xsl:text>        if(typeof(init) == "function"){
-</xsl:text>
-    <xsl:text>            try {
-</xsl:text>
-    <xsl:text>                init.call(widget);
-</xsl:text>
-    <xsl:text>            } catch(err) {
-</xsl:text>
-    <xsl:text>                console.log(err);
-</xsl:text>
-    <xsl:text>            }
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    });
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>// Open WebSocket to relative "/ws" address
-</xsl:text>
-    <xsl:text>var ws = new WebSocket(window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws'));
-</xsl:text>
-    <xsl:text>ws.binaryType = 'arraybuffer';
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>const dvgetters = {
-</xsl:text>
-    <xsl:text>    INT: (dv,offset) =&gt; [dv.getInt16(offset, true), 2],
-</xsl:text>
-    <xsl:text>    BOOL: (dv,offset) =&gt; [dv.getInt8(offset, true), 1],
-</xsl:text>
-    <xsl:text>    STRING: (dv, offset) =&gt; {
-</xsl:text>
-    <xsl:text>        size = dv.getInt8(offset);
-</xsl:text>
-    <xsl:text>        return [
-</xsl:text>
-    <xsl:text>            String.fromCharCode.apply(null, new Uint8Array(
-</xsl:text>
-    <xsl:text>                dv.buffer, /* original buffer */
-</xsl:text>
-    <xsl:text>                offset + 1, /* string starts after size*/
-</xsl:text>
-    <xsl:text>                size /* size of string */
-</xsl:text>
-    <xsl:text>            )), size + 1]; /* total increment */
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>// Apply updates recieved through ws.onmessage to subscribed widgets
-</xsl:text>
-    <xsl:text>function apply_updates() {
-</xsl:text>
-    <xsl:text>    for(let index in updates){
-</xsl:text>
-    <xsl:text>        // serving as a key, index becomes a string
-</xsl:text>
-    <xsl:text>        // -&gt; pass Number(index) instead
-</xsl:text>
-    <xsl:text>        dispatch_value(Number(index), updates[index]);
-</xsl:text>
-    <xsl:text>        delete updates[index];
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>}
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>// Called on requestAnimationFrame, modifies DOM
-</xsl:text>
-    <xsl:text>var requestAnimationFrameID = null;
-</xsl:text>
-    <xsl:text>function animate() {
-</xsl:text>
-    <xsl:text>    // Do the page swith if any one pending
-</xsl:text>
-    <xsl:text>    if(current_subscribed_page != current_visible_page){
-</xsl:text>
-    <xsl:text>        switch_visible_page(current_subscribed_page);
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>    apply_updates();
-</xsl:text>
-    <xsl:text>    requestAnimationFrameID = null;
-</xsl:text>
-    <xsl:text>}
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function requestHMIAnimation() {
-</xsl:text>
-    <xsl:text>    if(requestAnimationFrameID == null){
-</xsl:text>
-    <xsl:text>        requestAnimationFrameID = window.requestAnimationFrame(animate);
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>}
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>// Message reception handler
-</xsl:text>
-    <xsl:text>// Hash is verified and HMI values updates resulting from binary parsing
-</xsl:text>
-    <xsl:text>// are stored until browser can compute next frame, DOM is left untouched
-</xsl:text>
-    <xsl:text>ws.onmessage = function (evt) {
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    let data = evt.data;
-</xsl:text>
-    <xsl:text>    let dv = new DataView(data);
-</xsl:text>
-    <xsl:text>    let i = 0;
-</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>
-</xsl:text>
-    <xsl:text>        while(i &lt; data.byteLength){
-</xsl:text>
-    <xsl:text>            let index = dv.getUint32(i, true);
-</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 = dvgetters[iectype];
-</xsl:text>
-    <xsl:text>                let [value, bytesize] = dvgetter(dv,i);
-</xsl:text>
-    <xsl:text>                updates[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>        // register for rendering on next frame, since there are updates
-</xsl:text>
-    <xsl:text>        requestHMIAnimation();
-</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>
-</xsl:text>
-    <xsl:text>const typedarray_types = {
-</xsl:text>
-    <xsl:text>    INT: (number) =&gt; new Int16Array([number]),
-</xsl:text>
-    <xsl:text>    BOOL: (truth) =&gt; new Int16Array([truth]),
-</xsl:text>
-    <xsl:text>    STRING: (str) =&gt; {
-</xsl:text>
-    <xsl:text>        // beremiz default string max size is 128
-</xsl:text>
-    <xsl:text>        str = str.slice(0,128);
-</xsl:text>
-    <xsl:text>        binary = new Uint8Array(str.length + 1);
-</xsl:text>
-    <xsl:text>        binary[0] = str.length;
-</xsl:text>
-    <xsl:text>        for(var i = 0; i &lt; str.length; i++){
-</xsl:text>
-    <xsl:text>            binary[i+1] = str.charCodeAt(i);
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>        return binary;
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>    /* TODO */
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function send_reset() {
-</xsl:text>
-    <xsl:text>    send_blob(new Uint8Array([1])); /* reset = 1 */
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>// subscription state, as it should be in hmi server
-</xsl:text>
-    <xsl:text>// hmitree indexed array of integers
-</xsl:text>
-    <xsl:text>var subscriptions =  hmitree_types.map(_ignored =&gt; 0);
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>// subscription state as needed by widget now
-</xsl:text>
-    <xsl:text>// hmitree indexed array of Sets of widgets objects
-</xsl:text>
-    <xsl:text>var subscribers = hmitree_types.map(_ignored =&gt; new Set());
-</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(heartbeat_index, "+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 = [];
-</xsl:text>
-    <xsl:text>    for(let index = 0; index &lt; subscribers.length; index++){
-</xsl:text>
-    <xsl:text>        let widgets = subscribers[index];
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>        // periods are in ms
-</xsl:text>
-    <xsl:text>        let previous_period = subscriptions[index];
-</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) {
-</xsl:text>
-    <xsl:text>            let maxfreq = 0;
-</xsl:text>
-    <xsl:text>            for(let widget of widgets)
-</xsl:text>
-    <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>        if(previous_period != new_period) {
-</xsl:text>
-    <xsl:text>            subscriptions[index] = new_period;
-</xsl:text>
-    <xsl:text>            delta.push(
-</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>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>    send_blob(delta);
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function send_hmi_value(index, value) {
-</xsl:text>
-    <xsl:text>    let iectype = hmitree_types[index];
-</xsl:text>
-    <xsl:text>    let tobinary = typedarray_types[iectype];
-</xsl:text>
-    <xsl:text>    send_blob([
-</xsl:text>
-    <xsl:text>        new Uint8Array([0]),  /* setval = 0 */
-</xsl:text>
-    <xsl:text>        new Uint32Array([index]),
-</xsl:text>
-    <xsl:text>        tobinary(value)]);
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    cache[index] = value;
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function change_hmi_value(index, opstr) {
-</xsl:text>
-    <xsl:text>    let op = opstr[0];
-</xsl:text>
-    <xsl:text>    let given_val = opstr.slice(1);
-</xsl:text>
-    <xsl:text>    let old_val = cache[index]
-</xsl:text>
-    <xsl:text>    let new_val;
-</xsl:text>
-    <xsl:text>    switch(op){
-</xsl:text>
-    <xsl:text>      case "=":
-</xsl:text>
-    <xsl:text>        eval("new_val"+opstr);
-</xsl:text>
-    <xsl:text>        break;
-</xsl:text>
-    <xsl:text>      case "+":
-</xsl:text>
-    <xsl:text>      case "-":
-</xsl:text>
-    <xsl:text>      case "*":
-</xsl:text>
-    <xsl:text>      case "/":
-</xsl:text>
-    <xsl:text>        if(old_val != undefined)
-</xsl:text>
-    <xsl:text>            new_val = eval("old_val"+opstr);
-</xsl:text>
-    <xsl:text>        break;
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>    if(new_val != undefined &amp;&amp; old_val != new_val)
-</xsl:text>
-    <xsl:text>        send_hmi_value(index, new_val);
-</xsl:text>
-    <xsl:text>    return new_val;
-</xsl:text>
-    <xsl:text>}
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>var current_visible_page;
-</xsl:text>
-    <xsl:text>var current_subscribed_page;
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function prepare_svg() {
-</xsl:text>
-    <xsl:text>    for(let eltid in detachable_elements){
-</xsl:text>
-    <xsl:text>        let [element,parent] = detachable_elements[eltid];
-</xsl:text>
-    <xsl:text>        parent.removeChild(element);
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function switch_page(page_name, page_index) {
-</xsl:text>
-    <xsl:text>    if(current_subscribed_page != current_visible_page){
-</xsl:text>
-    <xsl:text>        /* page switch already going */
-</xsl:text>
-    <xsl:text>        /* TODO LOG ERROR */
-</xsl:text>
-    <xsl:text>        return;
-</xsl:text>
-    <xsl:text>    } else if(page_name == current_visible_page){
-</xsl:text>
-    <xsl:text>        /* already in that page */
-</xsl:text>
-    <xsl:text>        /* TODO LOG ERROR */
-</xsl:text>
-    <xsl:text>        return;
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>    switch_subscribed_page(page_name, page_index);
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function* chain(a,b){
-</xsl:text>
-    <xsl:text>    yield* a;
-</xsl:text>
-    <xsl:text>    yield* b;
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function switch_subscribed_page(page_name, page_index) {
-</xsl:text>
-    <xsl:text>    let old_desc = page_desc[current_subscribed_page];
-</xsl:text>
-    <xsl:text>    let new_desc = page_desc[page_name];
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    if(new_desc == undefined){
-</xsl:text>
-    <xsl:text>        /* TODO LOG ERROR */
-</xsl:text>
-    <xsl:text>        return;
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    if(page_index == undefined){
-</xsl:text>
-    <xsl:text>        page_index = new_desc.page_index;
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    if(old_desc){
-</xsl:text>
-    <xsl:text>        for(let widget of old_desc.absolute_widgets){
-</xsl:text>
-    <xsl:text>            /* remove subsribers */
-</xsl:text>
-    <xsl:text>            for(let index of widget.indexes){
-</xsl:text>
-    <xsl:text>                subscribers[index].delete(widget);
-</xsl:text>
-    <xsl:text>            }
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>        for(let widget of old_desc.relative_widgets){
-</xsl:text>
-    <xsl:text>            /* remove subsribers */
-</xsl:text>
-    <xsl:text>            for(let index of widget.indexes){
-</xsl:text>
-    <xsl:text>                let idx = widget.offset ? index + widget.offset : index;
-</xsl:text>
-    <xsl:text>                subscribers[idx].delete(widget);
-</xsl:text>
-    <xsl:text>            }
-</xsl:text>
-    <xsl:text>            /* lose the offset */
-</xsl:text>
-    <xsl:text>            delete widget.offset;
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>    for(let widget of new_desc.absolute_widgets){
-</xsl:text>
-    <xsl:text>        /* add widget's subsribers */
-</xsl:text>
-    <xsl:text>        for(let index of widget.indexes){
-</xsl:text>
-    <xsl:text>            subscribers[index].add(widget);
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>    var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index;
-</xsl:text>
-    <xsl:text>    for(let widget of new_desc.relative_widgets){
-</xsl:text>
-    <xsl:text>        /* set the offset because relative */
-</xsl:text>
-    <xsl:text>        widget.offset = new_offset;
-</xsl:text>
-    <xsl:text>        /* add widget's subsribers */
-</xsl:text>
-    <xsl:text>        for(let index of widget.indexes){
-</xsl:text>
-    <xsl:text>            subscribers[index + new_offset].add(widget);
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    update_subscriptions();
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    current_subscribed_page = page_name;
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    requestHMIAnimation();
-</xsl:text>
-    <xsl:text>}
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>function switch_visible_page(page_name) {
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    let old_desc = page_desc[current_visible_page];
-</xsl:text>
-    <xsl:text>    let new_desc = page_desc[page_name];
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    if(old_desc){
-</xsl:text>
-    <xsl:text>        for(let eltid in old_desc.required_detachables){
-</xsl:text>
-    <xsl:text>            if(!(eltid in new_desc.required_detachables)){
-</xsl:text>
-    <xsl:text>                let [element, parent] = old_desc.required_detachables[eltid];
-</xsl:text>
-    <xsl:text>                parent.removeChild(element);
-</xsl:text>
-    <xsl:text>            }
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>        for(let eltid in new_desc.required_detachables){
-</xsl:text>
-    <xsl:text>            if(!(eltid in old_desc.required_detachables)){
-</xsl:text>
-    <xsl:text>                let [element, parent] = new_desc.required_detachables[eltid];
-</xsl:text>
-    <xsl:text>                parent.appendChild(element);
-</xsl:text>
-    <xsl:text>            }
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    }else{
-</xsl:text>
-    <xsl:text>        for(let eltid in new_desc.required_detachables){
-</xsl:text>
-    <xsl:text>            let [element, parent] = new_desc.required_detachables[eltid];
-</xsl:text>
-    <xsl:text>            parent.appendChild(element);
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    for(let widget of chain(new_desc.absolute_widgets,new_desc.relative_widgets)){
-</xsl:text>
-    <xsl:text>        for(let index of widget.indexes){
-</xsl:text>
-    <xsl:text>            /* dispatch current cache in newly opened page widgets */
-</xsl:text>
-    <xsl:text>            let cached_val = cache[index];
-</xsl:text>
-    <xsl:text>            if(cached_val != undefined)
-</xsl:text>
-    <xsl:text>                dispatch_value_to_widget(widget, index, cached_val, cached_val);
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    svg_root.setAttribute('viewBox',new_desc.bbox.join(" "));
-</xsl:text>
-    <xsl:text>    current_visible_page = page_name;
-</xsl:text>
-    <xsl:text>};
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>// Once connection established
-</xsl:text>
-    <xsl:text>ws.onopen = function (evt) {
-</xsl:text>
-    <xsl:text>    init_widgets();
-</xsl:text>
-    <xsl:text>    send_reset();
-</xsl:text>
-    <xsl:text>    // show main page
-</xsl:text>
-    <xsl:text>    prepare_svg();
-</xsl:text>
-    <xsl:text>    switch_page(default_page);
-</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="widget_defs" match="widget[@type='Display']">
-    <xsl:param name="hmi_element"/>
-    <xsl:text>    frequency: 5,
-</xsl:text>
-    <xsl:text>    dispatch: function(value) {
-</xsl:text>
-    <xsl:choose>
-      <xsl:when test="$hmi_element[self::svg:text]">
-        <xsl:text>      this.element.textContent = String(value);
-</xsl:text>
-      </xsl:when>
-      <xsl:otherwise>
-        <xsl:message terminate="no">
-          <xsl:text>Display widget as a group not implemented</xsl:text>
-        </xsl:message>
-      </xsl:otherwise>
-    </xsl:choose>
-    <xsl:text>    },
-</xsl:text>
-  </xsl:template>
-  <xsl:template mode="widget_defs" match="widget[@type='Meter']">
-    <xsl:param name="hmi_element"/>
-    <xsl:text>    frequency: 10,
-</xsl:text>
-    <xsl:call-template name="defs_by_labels">
-      <xsl:with-param name="hmi_element" select="$hmi_element"/>
-      <xsl:with-param name="labels">
-        <xsl:text>needle range</xsl:text>
-      </xsl:with-param>
-    </xsl:call-template>
-    <xsl:call-template name="defs_by_labels">
-      <xsl:with-param name="hmi_element" select="$hmi_element"/>
-      <xsl:with-param name="labels">
-        <xsl:text>value min max</xsl:text>
-      </xsl:with-param>
-      <xsl:with-param name="mandatory" select="'no'"/>
-    </xsl:call-template>
-    <xsl:text>    dispatch: function(value) {
-</xsl:text>
-    <xsl:text>        if(this.value_elt)
-</xsl:text>
-    <xsl:text>            this.value_elt.textContent = String(value);
-</xsl:text>
-    <xsl:text>        let [min,max,totallength] = this.range;
-</xsl:text>
-    <xsl:text>        let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min)));
-</xsl:text>
-    <xsl:text>        let tip = this.range_elt.getPointAtLength(length);
-</xsl:text>
-    <xsl:text>        this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y);
-</xsl:text>
-    <xsl:text>    },
-</xsl:text>
-    <xsl:text>    origin: undefined,
-</xsl:text>
-    <xsl:text>    range: undefined,
-</xsl:text>
-    <xsl:text>    init: function() {
-</xsl:text>
-    <xsl:text>        let min = this.min_elt ?
-</xsl:text>
-    <xsl:text>                    Number(this.min_elt.textContent) :
-</xsl:text>
-    <xsl:text>                    this.args.length &gt;= 1 ? this.args[0] : 0;
-</xsl:text>
-    <xsl:text>        let max = this.max_elt ?
-</xsl:text>
-    <xsl:text>                    Number(this.max_elt.textContent) :
-</xsl:text>
-    <xsl:text>                    this.args.length &gt;= 2 ? this.args[1] : 100;
-</xsl:text>
-    <xsl:text>        this.range = [min, max, this.range_elt.getTotalLength()]
-</xsl:text>
-    <xsl:text>        this.origin = this.needle_elt.getPointAtLength(0);
-</xsl:text>
-    <xsl:text>    },
-</xsl:text>
-  </xsl:template>
   <func:function name="func:escape_quotes">
     <xsl:param name="txt"/>
     <xsl:variable name="frst" select="substring-before($txt,'&quot;')"/>
@@ -1629,6 +637,26 @@
       </xsl:otherwise>
     </xsl:choose>
   </func:function>
+  <xsl:template mode="widget_defs" match="widget[@type='Display']">
+    <xsl:param name="hmi_element"/>
+    <xsl:text>    frequency: 5,
+</xsl:text>
+    <xsl:text>    dispatch: function(value) {
+</xsl:text>
+    <xsl:choose>
+      <xsl:when test="$hmi_element[self::svg:text]">
+        <xsl:text>      this.element.textContent = String(value);
+</xsl:text>
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:message terminate="no">
+          <xsl:text>Display widget as a group not implemented</xsl:text>
+        </xsl:message>
+      </xsl:otherwise>
+    </xsl:choose>
+    <xsl:text>    },
+</xsl:text>
+  </xsl:template>
   <xsl:template mode="widget_defs" match="widget[@type='Input']">
     <xsl:param name="hmi_element"/>
     <xsl:variable name="value_elt">
@@ -1688,9 +716,79 @@
     <xsl:text>    },
 </xsl:text>
   </xsl:template>
-  <xsl:template mode="widget_defs" match="widget[@type='Button']"/>
-  <xsl:template mode="widget_defs" match="widget[@type='Toggle']">
-    <xsl:text>    frequency: 5,
+  <xsl:template mode="widget_defs" match="widget[@type='Jump']">
+    <xsl:param name="hmi_element"/>
+    <xsl:text>    on_click: function(evt) {
+</xsl:text>
+    <xsl:text>        switch_page(this.args[0], this.indexes[0]);
+</xsl:text>
+    <xsl:text>    },
+</xsl:text>
+    <xsl:text>    init: function() {
+</xsl:text>
+    <xsl:text>        this.element.setAttribute("onclick", "hmi_widgets['</xsl:text>
+    <xsl:value-of select="$hmi_element/@id"/>
+    <xsl:text>'].on_click(evt)");
+</xsl:text>
+    <xsl:text>    },
+</xsl:text>
+  </xsl:template>
+  <xsl:template mode="widget_defs" match="widget[@type='Meter']">
+    <xsl:param name="hmi_element"/>
+    <xsl:text>    frequency: 10,
+</xsl:text>
+    <xsl:call-template name="defs_by_labels">
+      <xsl:with-param name="hmi_element" select="$hmi_element"/>
+      <xsl:with-param name="labels">
+        <xsl:text>needle range</xsl:text>
+      </xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="defs_by_labels">
+      <xsl:with-param name="hmi_element" select="$hmi_element"/>
+      <xsl:with-param name="labels">
+        <xsl:text>value min max</xsl:text>
+      </xsl:with-param>
+      <xsl:with-param name="mandatory" select="'no'"/>
+    </xsl:call-template>
+    <xsl:text>    dispatch: function(value) {
+</xsl:text>
+    <xsl:text>        if(this.value_elt)
+</xsl:text>
+    <xsl:text>            this.value_elt.textContent = String(value);
+</xsl:text>
+    <xsl:text>        let [min,max,totallength] = this.range;
+</xsl:text>
+    <xsl:text>        let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min)));
+</xsl:text>
+    <xsl:text>        let tip = this.range_elt.getPointAtLength(length);
+</xsl:text>
+    <xsl:text>        this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y);
+</xsl:text>
+    <xsl:text>    },
+</xsl:text>
+    <xsl:text>    origin: undefined,
+</xsl:text>
+    <xsl:text>    range: undefined,
+</xsl:text>
+    <xsl:text>    init: function() {
+</xsl:text>
+    <xsl:text>        let min = this.min_elt ?
+</xsl:text>
+    <xsl:text>                    Number(this.min_elt.textContent) :
+</xsl:text>
+    <xsl:text>                    this.args.length &gt;= 1 ? this.args[0] : 0;
+</xsl:text>
+    <xsl:text>        let max = this.max_elt ?
+</xsl:text>
+    <xsl:text>                    Number(this.max_elt.textContent) :
+</xsl:text>
+    <xsl:text>                    this.args.length &gt;= 2 ? this.args[1] : 100;
+</xsl:text>
+    <xsl:text>        this.range = [min, max, this.range_elt.getTotalLength()]
+</xsl:text>
+    <xsl:text>        this.origin = this.needle_elt.getPointAtLength(0);
+</xsl:text>
+    <xsl:text>    },
 </xsl:text>
   </xsl:template>
   <xsl:template mode="widget_defs" match="widget[@type='Switch']">
@@ -1750,21 +848,914 @@
     <xsl:text>    ],
 </xsl:text>
   </xsl:template>
-  <xsl:template mode="widget_defs" match="widget[@type='Jump']">
-    <xsl:param name="hmi_element"/>
-    <xsl:text>    on_click: function(evt) {
-</xsl:text>
-    <xsl:text>        switch_page(this.args[0], this.indexes[0]);
-</xsl:text>
-    <xsl:text>    },
-</xsl:text>
-    <xsl:text>    init: function() {
-</xsl:text>
-    <xsl:text>        this.element.setAttribute("onclick", "hmi_widgets['</xsl:text>
-    <xsl:value-of select="$hmi_element/@id"/>
-    <xsl:text>'].on_click(evt)");
-</xsl:text>
-    <xsl:text>    },
+  <xsl:template match="/">
+    <xsl:comment>
+      <xsl:text>Made with SVGHMI. https://beremiz.org</xsl:text>
+    </xsl:comment>
+    <xsl:comment>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>debug_hmitree:
+</xsl:text>
+      <xsl:call-template name="debug_hmitree"/>
+      <xsl:text>
+</xsl:text>
+    </xsl:comment>
+    <xsl:comment>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>debug_geometry:
+</xsl:text>
+      <xsl:call-template name="debug_geometry"/>
+      <xsl:text>
+</xsl:text>
+    </xsl:comment>
+    <xsl:comment>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>debug_detachables:
+</xsl:text>
+      <xsl:call-template name="debug_detachables"/>
+      <xsl:text>
+</xsl:text>
+    </xsl:comment>
+    <xsl:comment>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>debug_unlink:
+</xsl:text>
+      <xsl:call-template name="debug_unlink"/>
+      <xsl:text>
+</xsl:text>
+    </xsl:comment>
+    <html xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/1999/xhtml">
+      <head/>
+      <body style="margin:0;overflow:hidden;">
+        <xsl:copy-of select="$result_svg"/>
+        <script>
+          <xsl:call-template name="scripts"/>
+        </script>
+      </body>
+    </html>
+  </xsl:template>
+  <xsl:template name="scripts">
+    <xsl:text>
+</xsl:text>
+    <xsl:text>id = idstr =&gt; document.getElementById(idstr);
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>var hmi_hash = [</xsl:text>
+    <xsl:value-of select="$hmitree/@hash"/>
+    <xsl:text>]; 
+</xsl:text>
+    <xsl:text>var hmi_widgets = {
+</xsl:text>
+    <xsl:apply-templates mode="hmi_elements" select="$hmi_elements"/>
+    <xsl:text>}
+</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/*">
+      <xsl:text>    /* </xsl:text>
+      <xsl:value-of select="@index"/>
+      <xsl:text>  </xsl:text>
+      <xsl:value-of select="@hmipath"/>
+      <xsl:text> */ "</xsl:text>
+      <xsl:value-of select="substring(local-name(), 5)"/>
+      <xsl:text>"</xsl:text>
+      <xsl:if test="position()!=last()">
+        <xsl:text>,</xsl:text>
+      </xsl:if>
+      <xsl:text>
+</xsl:text>
+    </xsl:for-each>
+    <xsl:text>]
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>var detachable_elements = {
+</xsl:text>
+    <xsl:for-each select="$detachable_elements">
+      <xsl:text>    "</xsl:text>
+      <xsl:value-of select="@id"/>
+      <xsl:text>":[id("</xsl:text>
+      <xsl:value-of select="@id"/>
+      <xsl:text>"), id("</xsl:text>
+      <xsl:value-of select="../@id"/>
+      <xsl:text>")]</xsl:text>
+      <xsl:if test="position()!=last()">
+        <xsl:text>,</xsl:text>
+      </xsl:if>
+      <xsl:text>
+</xsl:text>
+    </xsl:for-each>
+    <xsl:text>}
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>var page_desc = {
+</xsl:text>
+    <xsl:apply-templates mode="page_desc" select="$hmi_pages"/>
+    <xsl:text>}
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>var default_page = "</xsl:text>
+    <xsl:value-of select="$default_page"/>
+    <xsl:text>";
+</xsl:text>
+    <xsl:text>var svg_root = id("</xsl:text>
+    <xsl:value-of select="/svg:svg/@id"/>
+    <xsl:text>");
+</xsl:text>
+    <xsl:text>// svghmi.js
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>var cache = hmitree_types.map(_ignored =&gt; undefined);
+</xsl:text>
+    <xsl:text>var updates = {};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function dispatch_value_to_widget(widget, index, value, oldval) {
+</xsl:text>
+    <xsl:text>    try {
+</xsl:text>
+    <xsl:text>        let idx = widget.offset ? index - widget.offset : index;
+</xsl:text>
+    <xsl:text>        let idxidx = widget.indexes.indexOf(idx);
+</xsl:text>
+    <xsl:text>        let d = widget.dispatch;
+</xsl:text>
+    <xsl:text>        console.log(index, idx, idxidx, value);
+</xsl:text>
+    <xsl:text>        if(typeof(d) == "function" &amp;&amp; idxidx == 0){
+</xsl:text>
+    <xsl:text>            d.call(widget, value, oldval);
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>        else if(typeof(d) == "object" &amp;&amp; d.length &gt;= idxidx){
+</xsl:text>
+    <xsl:text>            d[idxidx].call(widget, value, oldval);
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>        /* else dispatch_0, ..., dispatch_n ? */
+</xsl:text>
+    <xsl:text>        /*else {
+</xsl:text>
+    <xsl:text>            throw new Error("Dunno how to dispatch to widget at index = " + index);
+</xsl:text>
+    <xsl:text>        }*/
+</xsl:text>
+    <xsl:text>    } catch(err) {
+</xsl:text>
+    <xsl:text>        console.log(err);
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>}
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function dispatch_value(index, value) {
+</xsl:text>
+    <xsl:text>    let widgets = subscribers[index];
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    let oldval = cache[index];
+</xsl:text>
+    <xsl:text>    cache[index] = value;
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    if(widgets.size &gt; 0) {
+</xsl:text>
+    <xsl:text>        for(let widget of widgets){
+</xsl:text>
+    <xsl:text>            dispatch_value_to_widget(widget, index, value, oldval);
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function init_widgets() {
+</xsl:text>
+    <xsl:text>    Object.keys(hmi_widgets).forEach(function(id) {
+</xsl:text>
+    <xsl:text>        let widget = hmi_widgets[id];
+</xsl:text>
+    <xsl:text>        let init = widget.init;
+</xsl:text>
+    <xsl:text>        if(typeof(init) == "function"){
+</xsl:text>
+    <xsl:text>            try {
+</xsl:text>
+    <xsl:text>                init.call(widget);
+</xsl:text>
+    <xsl:text>            } catch(err) {
+</xsl:text>
+    <xsl:text>                console.log(err);
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    });
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>// Open WebSocket to relative "/ws" address
+</xsl:text>
+    <xsl:text>var ws = new WebSocket(window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws'));
+</xsl:text>
+    <xsl:text>ws.binaryType = 'arraybuffer';
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>const dvgetters = {
+</xsl:text>
+    <xsl:text>    INT: (dv,offset) =&gt; [dv.getInt16(offset, true), 2],
+</xsl:text>
+    <xsl:text>    BOOL: (dv,offset) =&gt; [dv.getInt8(offset, true), 1],
+</xsl:text>
+    <xsl:text>    STRING: (dv, offset) =&gt; {
+</xsl:text>
+    <xsl:text>        size = dv.getInt8(offset);
+</xsl:text>
+    <xsl:text>        return [
+</xsl:text>
+    <xsl:text>            String.fromCharCode.apply(null, new Uint8Array(
+</xsl:text>
+    <xsl:text>                dv.buffer, /* original buffer */
+</xsl:text>
+    <xsl:text>                offset + 1, /* string starts after size*/
+</xsl:text>
+    <xsl:text>                size /* size of string */
+</xsl:text>
+    <xsl:text>            )), size + 1]; /* total increment */
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>// Apply updates recieved through ws.onmessage to subscribed widgets
+</xsl:text>
+    <xsl:text>function apply_updates() {
+</xsl:text>
+    <xsl:text>    for(let index in updates){
+</xsl:text>
+    <xsl:text>        // serving as a key, index becomes a string
+</xsl:text>
+    <xsl:text>        // -&gt; pass Number(index) instead
+</xsl:text>
+    <xsl:text>        dispatch_value(Number(index), updates[index]);
+</xsl:text>
+    <xsl:text>        delete updates[index];
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>}
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>// Called on requestAnimationFrame, modifies DOM
+</xsl:text>
+    <xsl:text>var requestAnimationFrameID = null;
+</xsl:text>
+    <xsl:text>function animate() {
+</xsl:text>
+    <xsl:text>    // Do the page swith if any one pending
+</xsl:text>
+    <xsl:text>    if(current_subscribed_page != current_visible_page){
+</xsl:text>
+    <xsl:text>        switch_visible_page(current_subscribed_page);
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>    apply_updates();
+</xsl:text>
+    <xsl:text>    requestAnimationFrameID = null;
+</xsl:text>
+    <xsl:text>}
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function requestHMIAnimation() {
+</xsl:text>
+    <xsl:text>    if(requestAnimationFrameID == null){
+</xsl:text>
+    <xsl:text>        requestAnimationFrameID = window.requestAnimationFrame(animate);
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>}
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>// Message reception handler
+</xsl:text>
+    <xsl:text>// Hash is verified and HMI values updates resulting from binary parsing
+</xsl:text>
+    <xsl:text>// are stored until browser can compute next frame, DOM is left untouched
+</xsl:text>
+    <xsl:text>ws.onmessage = function (evt) {
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    let data = evt.data;
+</xsl:text>
+    <xsl:text>    let dv = new DataView(data);
+</xsl:text>
+    <xsl:text>    let i = 0;
+</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>
+</xsl:text>
+    <xsl:text>        while(i &lt; data.byteLength){
+</xsl:text>
+    <xsl:text>            let index = dv.getUint32(i, true);
+</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 = dvgetters[iectype];
+</xsl:text>
+    <xsl:text>                let [value, bytesize] = dvgetter(dv,i);
+</xsl:text>
+    <xsl:text>                updates[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>        // register for rendering on next frame, since there are updates
+</xsl:text>
+    <xsl:text>        requestHMIAnimation();
+</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>
+</xsl:text>
+    <xsl:text>const typedarray_types = {
+</xsl:text>
+    <xsl:text>    INT: (number) =&gt; new Int16Array([number]),
+</xsl:text>
+    <xsl:text>    BOOL: (truth) =&gt; new Int16Array([truth]),
+</xsl:text>
+    <xsl:text>    STRING: (str) =&gt; {
+</xsl:text>
+    <xsl:text>        // beremiz default string max size is 128
+</xsl:text>
+    <xsl:text>        str = str.slice(0,128);
+</xsl:text>
+    <xsl:text>        binary = new Uint8Array(str.length + 1);
+</xsl:text>
+    <xsl:text>        binary[0] = str.length;
+</xsl:text>
+    <xsl:text>        for(var i = 0; i &lt; str.length; i++){
+</xsl:text>
+    <xsl:text>            binary[i+1] = str.charCodeAt(i);
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>        return binary;
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>    /* TODO */
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function send_reset() {
+</xsl:text>
+    <xsl:text>    send_blob(new Uint8Array([1])); /* reset = 1 */
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>// subscription state, as it should be in hmi server
+</xsl:text>
+    <xsl:text>// hmitree indexed array of integers
+</xsl:text>
+    <xsl:text>var subscriptions =  hmitree_types.map(_ignored =&gt; 0);
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>// subscription state as needed by widget now
+</xsl:text>
+    <xsl:text>// hmitree indexed array of Sets of widgets objects
+</xsl:text>
+    <xsl:text>var subscribers = hmitree_types.map(_ignored =&gt; new Set());
+</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(heartbeat_index, "+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 = [];
+</xsl:text>
+    <xsl:text>    for(let index = 0; index &lt; subscribers.length; index++){
+</xsl:text>
+    <xsl:text>        let widgets = subscribers[index];
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        // periods are in ms
+</xsl:text>
+    <xsl:text>        let previous_period = subscriptions[index];
+</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) {
+</xsl:text>
+    <xsl:text>            let maxfreq = 0;
+</xsl:text>
+    <xsl:text>            for(let widget of widgets)
+</xsl:text>
+    <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>        if(previous_period != new_period) {
+</xsl:text>
+    <xsl:text>            subscriptions[index] = new_period;
+</xsl:text>
+    <xsl:text>            delta.push(
+</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>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>    send_blob(delta);
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function send_hmi_value(index, value) {
+</xsl:text>
+    <xsl:text>    let iectype = hmitree_types[index];
+</xsl:text>
+    <xsl:text>    let tobinary = typedarray_types[iectype];
+</xsl:text>
+    <xsl:text>    send_blob([
+</xsl:text>
+    <xsl:text>        new Uint8Array([0]),  /* setval = 0 */
+</xsl:text>
+    <xsl:text>        new Uint32Array([index]),
+</xsl:text>
+    <xsl:text>        tobinary(value)]);
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    cache[index] = value;
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function change_hmi_value(index, opstr) {
+</xsl:text>
+    <xsl:text>    let op = opstr[0];
+</xsl:text>
+    <xsl:text>    let given_val = opstr.slice(1);
+</xsl:text>
+    <xsl:text>    let old_val = cache[index]
+</xsl:text>
+    <xsl:text>    let new_val;
+</xsl:text>
+    <xsl:text>    switch(op){
+</xsl:text>
+    <xsl:text>      case "=":
+</xsl:text>
+    <xsl:text>        eval("new_val"+opstr);
+</xsl:text>
+    <xsl:text>        break;
+</xsl:text>
+    <xsl:text>      case "+":
+</xsl:text>
+    <xsl:text>      case "-":
+</xsl:text>
+    <xsl:text>      case "*":
+</xsl:text>
+    <xsl:text>      case "/":
+</xsl:text>
+    <xsl:text>        if(old_val != undefined)
+</xsl:text>
+    <xsl:text>            new_val = eval("old_val"+opstr);
+</xsl:text>
+    <xsl:text>        break;
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>    if(new_val != undefined &amp;&amp; old_val != new_val)
+</xsl:text>
+    <xsl:text>        send_hmi_value(index, new_val);
+</xsl:text>
+    <xsl:text>    return new_val;
+</xsl:text>
+    <xsl:text>}
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>var current_visible_page;
+</xsl:text>
+    <xsl:text>var current_subscribed_page;
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function prepare_svg() {
+</xsl:text>
+    <xsl:text>    for(let eltid in detachable_elements){
+</xsl:text>
+    <xsl:text>        let [element,parent] = detachable_elements[eltid];
+</xsl:text>
+    <xsl:text>        parent.removeChild(element);
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function switch_page(page_name, page_index) {
+</xsl:text>
+    <xsl:text>    if(current_subscribed_page != current_visible_page){
+</xsl:text>
+    <xsl:text>        /* page switch already going */
+</xsl:text>
+    <xsl:text>        /* TODO LOG ERROR */
+</xsl:text>
+    <xsl:text>        return;
+</xsl:text>
+    <xsl:text>    } else if(page_name == current_visible_page){
+</xsl:text>
+    <xsl:text>        /* already in that page */
+</xsl:text>
+    <xsl:text>        /* TODO LOG ERROR */
+</xsl:text>
+    <xsl:text>        return;
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>    switch_subscribed_page(page_name, page_index);
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function* chain(a,b){
+</xsl:text>
+    <xsl:text>    yield* a;
+</xsl:text>
+    <xsl:text>    yield* b;
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function switch_subscribed_page(page_name, page_index) {
+</xsl:text>
+    <xsl:text>    let old_desc = page_desc[current_subscribed_page];
+</xsl:text>
+    <xsl:text>    let new_desc = page_desc[page_name];
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    if(new_desc == undefined){
+</xsl:text>
+    <xsl:text>        /* TODO LOG ERROR */
+</xsl:text>
+    <xsl:text>        return;
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    if(page_index == undefined){
+</xsl:text>
+    <xsl:text>        page_index = new_desc.page_index;
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    if(old_desc){
+</xsl:text>
+    <xsl:text>        for(let widget of old_desc.absolute_widgets){
+</xsl:text>
+    <xsl:text>            /* remove subsribers */
+</xsl:text>
+    <xsl:text>            for(let index of widget.indexes){
+</xsl:text>
+    <xsl:text>                subscribers[index].delete(widget);
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>        for(let widget of old_desc.relative_widgets){
+</xsl:text>
+    <xsl:text>            /* remove subsribers */
+</xsl:text>
+    <xsl:text>            for(let index of widget.indexes){
+</xsl:text>
+    <xsl:text>                let idx = widget.offset ? index + widget.offset : index;
+</xsl:text>
+    <xsl:text>                subscribers[idx].delete(widget);
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>            /* lose the offset */
+</xsl:text>
+    <xsl:text>            delete widget.offset;
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>    for(let widget of new_desc.absolute_widgets){
+</xsl:text>
+    <xsl:text>        /* add widget's subsribers */
+</xsl:text>
+    <xsl:text>        for(let index of widget.indexes){
+</xsl:text>
+    <xsl:text>            subscribers[index].add(widget);
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>    var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index;
+</xsl:text>
+    <xsl:text>    for(let widget of new_desc.relative_widgets){
+</xsl:text>
+    <xsl:text>        /* set the offset because relative */
+</xsl:text>
+    <xsl:text>        widget.offset = new_offset;
+</xsl:text>
+    <xsl:text>        /* add widget's subsribers */
+</xsl:text>
+    <xsl:text>        for(let index of widget.indexes){
+</xsl:text>
+    <xsl:text>            subscribers[index + new_offset].add(widget);
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    update_subscriptions();
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    current_subscribed_page = page_name;
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    requestHMIAnimation();
+</xsl:text>
+    <xsl:text>}
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>function switch_visible_page(page_name) {
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    let old_desc = page_desc[current_visible_page];
+</xsl:text>
+    <xsl:text>    let new_desc = page_desc[page_name];
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    if(old_desc){
+</xsl:text>
+    <xsl:text>        for(let eltid in old_desc.required_detachables){
+</xsl:text>
+    <xsl:text>            if(!(eltid in new_desc.required_detachables)){
+</xsl:text>
+    <xsl:text>                let [element, parent] = old_desc.required_detachables[eltid];
+</xsl:text>
+    <xsl:text>                parent.removeChild(element);
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>        for(let eltid in new_desc.required_detachables){
+</xsl:text>
+    <xsl:text>            if(!(eltid in old_desc.required_detachables)){
+</xsl:text>
+    <xsl:text>                let [element, parent] = new_desc.required_detachables[eltid];
+</xsl:text>
+    <xsl:text>                parent.appendChild(element);
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }else{
+</xsl:text>
+    <xsl:text>        for(let eltid in new_desc.required_detachables){
+</xsl:text>
+    <xsl:text>            let [element, parent] = new_desc.required_detachables[eltid];
+</xsl:text>
+    <xsl:text>            parent.appendChild(element);
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    for(let widget of chain(new_desc.absolute_widgets,new_desc.relative_widgets)){
+</xsl:text>
+    <xsl:text>        for(let index of widget.indexes){
+</xsl:text>
+    <xsl:text>            /* dispatch current cache in newly opened page widgets */
+</xsl:text>
+    <xsl:text>            let cached_val = cache[index];
+</xsl:text>
+    <xsl:text>            if(cached_val != undefined)
+</xsl:text>
+    <xsl:text>                dispatch_value_to_widget(widget, index, cached_val, cached_val);
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    svg_root.setAttribute('viewBox',new_desc.bbox.join(" "));
+</xsl:text>
+    <xsl:text>    current_visible_page = page_name;
+</xsl:text>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>// Once connection established
+</xsl:text>
+    <xsl:text>ws.onopen = function (evt) {
+</xsl:text>
+    <xsl:text>    init_widgets();
+</xsl:text>
+    <xsl:text>    send_reset();
+</xsl:text>
+    <xsl:text>    // show main page
+</xsl:text>
+    <xsl:text>    prepare_svg();
+</xsl:text>
+    <xsl:text>    switch_page(default_page);
+</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:template>
 </xsl:stylesheet>
--- a/svghmi/gen_index_xhtml.ysl2	Tue Mar 17 14:11:54 2020 +0100
+++ b/svghmi/gen_index_xhtml.ysl2	Wed Mar 18 09:16:14 2020 +0100
@@ -60,7 +60,9 @@
 
     include inline_svg.ysl2
 
-    include widget_common.ysl2
+    include widgets_common.ysl2
+
+    include widget_*.ysl2
 
     template "/" {
         comment > Made with SVGHMI. https://beremiz.org
@@ -73,6 +75,7 @@
              xmlns:xlink="http://www.w3.org/1999/xlink" {
             head;
             body style="margin:0;overflow:hidden;" {
+                // Inline SVG
                 copy "$result_svg";
                 script{
                     call "scripts";
@@ -83,30 +86,11 @@
 
     function "scripts"
     {
-        | //(function(){
         |
         | id = idstr => document.getElementById(idstr);
         |
         | var hmi_hash = [«$hmitree/@hash»]; 
 
-        /* TODO re-enable
-        ||
-        function evaluate_js_from_descriptions() {
-            var Page;
-            var Input;
-            var Display;
-            var res = [];
-        ||
-        const "midmark" > \n«$mark»
-        apply """//*[contains(child::svg:desc, $midmark) or \
-                     starts-with(child::svg:desc, $mark)]""",2 
-              mode="code_from_descs";
-        ||
-            return res;
-        }
-        ||
-        */
-
         | var hmi_widgets = {
         apply "$hmi_elements", mode="hmi_elements";
         | }
@@ -137,177 +121,5 @@
         | var default_page = "«$default_page»";
         | var svg_root = id("«/svg:svg/@id»");
         include text svghmi.js
-        | //})();
-    }
-
-    // template "*", mode="code_from_descs" {
-    //     ||
-    //     {
-    //         var path, role, name, priv;
-    //         var id = "«@id»";
-    //     ||
-
-    //     /* if label is used, use it as default name */
-    //     if "@inkscape:label"
-    //         |> name = "«@inkscape:label»";
-
-    //     | /* -------------- */
-
-    //     // this breaks indent, but fixing indent could break string literals
-    //     value "substring-after(svg:desc, $mark)";
-    //     // nobody reads generated code anyhow...
-
-    //     ||
-
-    //         /* -------------- */
-    //         res.push({
-    //             path:path,
-    //             role:role,
-    //             name:name,
-    //             priv:priv
-    //         })
-    //     }
-    //     ||
-    // }
-
-
-    template "widget[@type='Display']", mode="widget_defs" {
-        param "hmi_element";
-        |     frequency: 5,
-        |     dispatch: function(value) {
-        choose {
-            when "$hmi_element[self::svg:text]"{
-            // TODO : care about <tspan> ?
-        |       this.element.textContent = String(value);
-            }
-            otherwise {
-                warning > Display widget as a group not implemented
-            }
-        }
-        |     },
-
-    }
-    template "widget[@type='Meter']", mode="widget_defs" {
-        param "hmi_element";
-        |     frequency: 10,
-        labels("needle range");
-        optional_labels("value min max");
-        |     dispatch: function(value) {
-        |         if(this.value_elt)
-        |             this.value_elt.textContent = String(value);
-        |         let [min,max,totallength] = this.range;
-        |         let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min)));
-        |         let tip = this.range_elt.getPointAtLength(length);
-        |         this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y);
-        |     },
-        |     origin: undefined,
-        |     range: undefined,
-        |     init: function() {
-        |         let min = this.min_elt ?
-        |                     Number(this.min_elt.textContent) :
-        |                     this.args.length >= 1 ? this.args[0] : 0;
-        |         let max = this.max_elt ?
-        |                     Number(this.max_elt.textContent) :
-        |                     this.args.length >= 2 ? this.args[1] : 100;
-        |         this.range = [min, max, this.range_elt.getTotalLength()]
-        |         this.origin = this.needle_elt.getPointAtLength(0);
-        |     },
-    }
-
-    def "func:escape_quotes" {
-        param "txt";
-        // have to use a python string to enter escaped quote
-        const "frst", !"substring-before($txt,'\"')"!;
-        const "frstln", "string-length($frst)";
-        choose {
-            when "$frstln > 0 and string-length($txt) > $frstln" {
-                result !"concat($frst,'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
-            }
-            otherwise {
-                result "$txt";
-            }
-        }
-    }
-
-    template "widget[@type='Input']", mode="widget_defs" {
-        param "hmi_element";
-        const "value_elt" {
-            optional_labels("value");
-        }
-        const "have_value","string-length($value_elt)>0";
-        value "$value_elt";
-        if "$have_value"
-        |     frequency: 5,
-
-        |     dispatch: function(value) {
-
-        if "$have_value"
-        |         this.value_elt.textContent = String(value);
-
-        |     },
-        const "edit_elt_id","$hmi_element/*[@inkscape:label='edit'][1]/@id";
-        |     init: function() {
-        if "$edit_elt_id" {
-        |         id("«$edit_elt_id»").addEventListener(
-        |             "click", 
-        |             evt => alert('XXX TODO : Edit value'));
-        }
-        foreach "$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]" {
-        |         id("«@id»").addEventListener(
-        |             "click", 
-        |             evt => {let new_val = change_hmi_value(this.indexes[0], "«func:escape_quotes(@inkscape:label)»");
-            if "$have_value"{
-        |                     this.value_elt.textContent = String(new_val);
-            }
-        |                    });
-                              /* TODO gray out value until refreshed */
-        }
-        |     },
-    }
-    template "widget[@type='Button']", mode="widget_defs" {
-    }
-    template "widget[@type='Toggle']", mode="widget_defs" {
-        |     frequency: 5,
-    }
-    template "widget[@type='Switch']", mode="widget_defs" {
-        param "hmi_element";
-        |     frequency: 5,
-        |     dispatch: function(value) {
-        |         for(let choice of this.choices){
-        |             if(value != choice.value){
-        |                 choice.elt.setAttribute("style", "display:none");
-        |             } else {
-        |                 choice.elt.setAttribute("style", choice.style);
-        |             }
-        |         }
-        |     },
-        |     init: function() {
-        |         // Hello Switch
-        |     },
-        |     choices: [
-        const "regex",!"'^(\"[^\"].*\"|\-?[0-9]+)(#.*)?$'"!;
-        foreach "$hmi_element/*[regexp:test(@inkscape:label,$regex)]" {
-            const "literal", "regexp:match(@inkscape:label,$regex)[2]";
-        |         {
-        |             elt:id("«@id»"),
-        |             style:"«@style»",
-        |             value:«$literal»
-        |         }`if "position()!=last()" > ,`
-        }
-        |     ],
-    }
-    template "widget[@type='Jump']", mode="widget_defs" {
-        param "hmi_element";
-        |     on_click: function(evt) {
-        |         switch_page(this.args[0], this.indexes[0]);
-        |     },
-        |     init: function() {
-        /* registering event this way doies not "click" through svg:use 
-        |     this.element.onclick = evt => switch_page(this.args[0]);
-        event must be registered by adding attribute to element instead
-        TODO : generalize mouse event handling by global event capture + getElementsAtPoint()
-        */
-        |         this.element.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_click(evt)");
-        |     },
     }
 }
--- a/svghmi/widget_common.ysl2	Tue Mar 17 14:11:54 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-in xsl decl labels(*ptr, name="defs_by_labels") alias call-template {
-    with "hmi_element", "$hmi_element";
-    with "labels"{text *ptr};
-};
-
-in xsl decl optional_labels(*ptr, name="defs_by_labels") alias call-template {
-    with "hmi_element", "$hmi_element";
-    with "labels"{text *ptr};
-    with "mandatory","'no'";
-};
-
-template "svg:*", mode="hmi_elements" {
-    const "widget", "func:parselabel(@inkscape:label)/widget";
-    const "eltid","@id";
-    |   "«@id»": {
-    |     type: "«$widget/@type»",
-    |     args: [
-        foreach "$widget/arg"
-    |         "«@value»"`if "position()!=last()" > ,`
-    |     ],
-    |     indexes: [
-    foreach "$widget/path" {
-        choose {
-            when "not(@index)" {
-                warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree
-            }
-            otherwise {
-    |             «@index»`if "position()!=last()" > ,`
-            }
-        }
-    }
-    |     ],
-    |     element: id("«@id»"),
-    apply "$widget", mode="widget_defs" with "hmi_element",".";
-    |   }`if "position()!=last()" > ,`
-}
-
-
-function "defs_by_labels" {
-    param "labels","''";
-    param "mandatory","'yes'";
-    param "hmi_element";
-    const "widget_type","@type";
-    foreach "str:split($labels)" {
-        const "name",".";
-        const "elt_id","$result_svg_ns//*[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]/@id";
-        choose {
-            when "not($elt_id)" {
-                if "$mandatory='yes'" {
-                    // TODO FIXME error > «$widget_type» widget must have a «$name» element
-                    warning > «$widget_type» widget must have a «$name» element
-                }
-                // otherwise produce nothing
-            }
-            otherwise {
-                |     «$name»_elt: id("«$elt_id»"),
-            }
-        }
-    }
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_custom.ysl2	Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,60 @@
+// widget_custom.ysl2
+//
+// widget entierely defined from JS code in Inkscape description field
+
+// TODO
+
+// a preliminary implementation was initially attempted but disabled
+// code collected around before code refactoring
+
+
+
+        /* TODO re-enable
+        ||
+        function evaluate_js_from_descriptions() {
+            var Page;
+            var Input;
+            var Display;
+            var res = [];
+        ||
+        const "midmark" > \n«$mark»
+        apply """//*[contains(child::svg:desc, $midmark) or \
+                     starts-with(child::svg:desc, $mark)]""",2 
+              mode="code_from_descs";
+        ||
+            return res;
+        }
+        ||
+        */
+
+    // template "*", mode="code_from_descs" {
+    //     ||
+    //     {
+    //         var path, role, name, priv;
+    //         var id = "«@id»";
+    //     ||
+
+    //     /* if label is used, use it as default name */
+    //     if "@inkscape:label"
+    //         |> name = "«@inkscape:label»";
+
+    //     | /* -------------- */
+
+    //     // this breaks indent, but fixing indent could break string literals
+    //     value "substring-after(svg:desc, $mark)";
+    //     // nobody reads generated code anyhow...
+
+    //     ||
+
+    //         /* -------------- */
+    //         res.push({
+    //             path:path,
+    //             role:role,
+    //             name:name,
+    //             priv:priv
+    //         })
+    //     }
+    //     ||
+    // }
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_display.ysl2	Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,19 @@
+// widget_display.ysl2
+
+
+template "widget[@type='Display']", mode="widget_defs" {
+    param "hmi_element";
+    |     frequency: 5,
+    |     dispatch: function(value) {
+    choose {
+        when "$hmi_element[self::svg:text]"{
+        // TODO : care about <tspan> ?
+    |       this.element.textContent = String(value);
+        }
+        otherwise {
+            warning > Display widget as a group not implemented
+        }
+    }
+    |     },
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_input.ysl2	Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,37 @@
+// widget_input.ysl2
+
+template "widget[@type='Input']", mode="widget_defs" {
+    param "hmi_element";
+    const "value_elt" {
+        optional_labels("value");
+    }
+    const "have_value","string-length($value_elt)>0";
+    value "$value_elt";
+    if "$have_value"
+    |     frequency: 5,
+
+    |     dispatch: function(value) {
+
+    if "$have_value"
+    |         this.value_elt.textContent = String(value);
+
+    |     },
+    const "edit_elt_id","$hmi_element/*[@inkscape:label='edit'][1]/@id";
+    |     init: function() {
+    if "$edit_elt_id" {
+    |         id("«$edit_elt_id»").addEventListener(
+    |             "click", 
+    |             evt => alert('XXX TODO : Edit value'));
+    }
+    foreach "$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]" {
+    |         id("«@id»").addEventListener(
+    |             "click", 
+    |             evt => {let new_val = change_hmi_value(this.indexes[0], "«func:escape_quotes(@inkscape:label)»");
+        if "$have_value"{
+    |                     this.value_elt.textContent = String(new_val);
+        }
+    |                    });
+                          /* TODO gray out value until refreshed */
+    }
+    |     },
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_jump.ysl2	Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,16 @@
+// widget_jump.ysl2
+
+template "widget[@type='Jump']", mode="widget_defs" {
+    param "hmi_element";
+    |     on_click: function(evt) {
+    |         switch_page(this.args[0], this.indexes[0]);
+    |     },
+    |     init: function() {
+    /* registering event this way doies not "click" through svg:use 
+    |     this.element.onclick = evt => switch_page(this.args[0]);
+    event must be registered by adding attribute to element instead
+    TODO : generalize mouse event handling by global event capture + getElementsAtPoint()
+    */
+    |         this.element.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_click(evt)");
+    |     },
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_meter.ysl2	Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,31 @@
+// widget_meter.ysl2
+
+
+template "widget[@type='Meter']", mode="widget_defs" {
+    param "hmi_element";
+    |     frequency: 10,
+    labels("needle range");
+    optional_labels("value min max");
+    |     dispatch: function(value) {
+    |         if(this.value_elt)
+    |             this.value_elt.textContent = String(value);
+    |         let [min,max,totallength] = this.range;
+    |         let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min)));
+    |         let tip = this.range_elt.getPointAtLength(length);
+    |         this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y);
+    |     },
+    |     origin: undefined,
+    |     range: undefined,
+    |     init: function() {
+    |         let min = this.min_elt ?
+    |                     Number(this.min_elt.textContent) :
+    |                     this.args.length >= 1 ? this.args[0] : 0;
+    |         let max = this.max_elt ?
+    |                     Number(this.max_elt.textContent) :
+    |                     this.args.length >= 2 ? this.args[1] : 100;
+    |         this.range = [min, max, this.range_elt.getTotalLength()]
+    |         this.origin = this.needle_elt.getPointAtLength(0);
+    |     },
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_switch.ysl2	Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,29 @@
+// widget_switch.ysl2
+
+template "widget[@type='Switch']", mode="widget_defs" {
+    param "hmi_element";
+    |     frequency: 5,
+    |     dispatch: function(value) {
+    |         for(let choice of this.choices){
+    |             if(value != choice.value){
+    |                 choice.elt.setAttribute("style", "display:none");
+    |             } else {
+    |                 choice.elt.setAttribute("style", choice.style);
+    |             }
+    |         }
+    |     },
+    |     init: function() {
+    |         // Hello Switch
+    |     },
+    |     choices: [
+    const "regex",!"'^(\"[^\"].*\"|\-?[0-9]+)(#.*)?$'"!;
+    foreach "$hmi_element/*[regexp:test(@inkscape:label,$regex)]" {
+        const "literal", "regexp:match(@inkscape:label,$regex)[2]";
+    |         {
+    |             elt:id("«@id»"),
+    |             style:"«@style»",
+    |             value:«$literal»
+    |         }`if "position()!=last()" > ,`
+    }
+    |     ],
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widgets_common.ysl2	Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,76 @@
+in xsl decl labels(*ptr, name="defs_by_labels") alias call-template {
+    with "hmi_element", "$hmi_element";
+    with "labels"{text *ptr};
+};
+
+in xsl decl optional_labels(*ptr, name="defs_by_labels") alias call-template {
+    with "hmi_element", "$hmi_element";
+    with "labels"{text *ptr};
+    with "mandatory","'no'";
+};
+
+template "svg:*", mode="hmi_elements" {
+    const "widget", "func:parselabel(@inkscape:label)/widget";
+    const "eltid","@id";
+    |   "«@id»": {
+    |     type: "«$widget/@type»",
+    |     args: [
+        foreach "$widget/arg"
+    |         "«@value»"`if "position()!=last()" > ,`
+    |     ],
+    |     indexes: [
+    foreach "$widget/path" {
+        choose {
+            when "not(@index)" {
+                warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree
+            }
+            otherwise {
+    |             «@index»`if "position()!=last()" > ,`
+            }
+        }
+    }
+    |     ],
+    |     element: id("«@id»"),
+    apply "$widget", mode="widget_defs" with "hmi_element",".";
+    |   }`if "position()!=last()" > ,`
+}
+
+
+function "defs_by_labels" {
+    param "labels","''";
+    param "mandatory","'yes'";
+    param "hmi_element";
+    const "widget_type","@type";
+    foreach "str:split($labels)" {
+        const "name",".";
+        const "elt_id","$result_svg_ns//*[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]/@id";
+        choose {
+            when "not($elt_id)" {
+                if "$mandatory='yes'" {
+                    // TODO FIXME error > «$widget_type» widget must have a «$name» element
+                    warning > «$widget_type» widget must have a «$name» element
+                }
+                // otherwise produce nothing
+            }
+            otherwise {
+                |     «$name»_elt: id("«$elt_id»"),
+            }
+        }
+    }
+}
+
+def "func:escape_quotes" {
+    param "txt";
+    // have to use a python string to enter escaped quote
+    const "frst", !"substring-before($txt,'\"')"!;
+    const "frstln", "string-length($frst)";
+    choose {
+        when "$frstln > 0 and string-length($txt) > $frstln" {
+            result !"concat($frst,'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
+        }
+        otherwise {
+            result "$txt";
+        }
+    }
+}
+