SVGHMI: added widget_common.ysl2 svghmi
authorEdouard Tisserant
Tue, 17 Mar 2020 14:01:37 +0100
branchsvghmi
changeset 2881 3bb49f93d48c
parent 2880 9da4ac0c9add
child 2882 ac08a5d15c15
SVGHMI: added widget_common.ysl2
svghmi/gen_index_xhtml.xslt
svghmi/gen_index_xhtml.ysl2
svghmi/widget_common.ysl2
--- a/svghmi/gen_index_xhtml.xslt	Tue Mar 17 13:43:19 2020 +0100
+++ b/svghmi/gen_index_xhtml.xslt	Tue Mar 17 14:01:37 2020 +0100
@@ -527,157 +527,22 @@
 </xsl:text>
     </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:for-each select="$hmi_elements">
-      <xsl:variable name="widget" select="func:parselabel(@inkscape:label)/widget"/>
-      <xsl:variable name="eltid" select="@id"/>
-      <xsl:text>  "</xsl:text>
-      <xsl:value-of select="@id"/>
-      <xsl:text>": {
-</xsl:text>
-      <xsl:text>    type: "</xsl:text>
-      <xsl:value-of select="$widget/@type"/>
-      <xsl:text>",
-</xsl:text>
-      <xsl:text>    args: [
-</xsl:text>
-      <xsl:for-each select="$widget/arg">
-        <xsl:text>        "</xsl:text>
-        <xsl:value-of select="@value"/>
-        <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>    indexes: [
-</xsl:text>
-      <xsl:for-each select="$widget/path">
-        <xsl:choose>
-          <xsl:when test="not(@index)">
-            <xsl:message terminate="no">
-              <xsl:text>Widget </xsl:text>
-              <xsl:value-of select="$widget/@type"/>
-              <xsl:text> id="</xsl:text>
-              <xsl:value-of select="$eltid"/>
-              <xsl:text>" : No match for path "</xsl:text>
-              <xsl:value-of select="@value"/>
-              <xsl:text>" in HMI tree</xsl:text>
-            </xsl:message>
-          </xsl:when>
-          <xsl:otherwise>
-            <xsl:text>            </xsl:text>
-            <xsl:value-of select="@index"/>
-            <xsl:if test="position()!=last()">
-              <xsl:text>,</xsl:text>
-            </xsl:if>
-            <xsl:text>
-</xsl:text>
-          </xsl:otherwise>
-        </xsl:choose>
-      </xsl:for-each>
-      <xsl:text>    ],
-</xsl:text>
-      <xsl:text>    element: id("</xsl:text>
-      <xsl:value-of select="@id"/>
-      <xsl:text>"),
-</xsl:text>
-      <xsl:apply-templates mode="widget_defs" select="$widget">
-        <xsl:with-param name="hmi_element" select="."/>
-      </xsl:apply-templates>
-      <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 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:template mode="hmi_elements" match="svg:*">
+    <xsl:variable name="widget" select="func:parselabel(@inkscape:label)/widget"/>
+    <xsl:variable name="eltid" select="@id"/>
+    <xsl:text>  "</xsl:text>
+    <xsl:value-of select="@id"/>
+    <xsl:text>": {
+</xsl:text>
+    <xsl:text>    type: "</xsl:text>
+    <xsl:value-of select="$widget/@type"/>
+    <xsl:text>",
+</xsl:text>
+    <xsl:text>    args: [
+</xsl:text>
+    <xsl:for-each select="$widget/arg">
+      <xsl:text>        "</xsl:text>
+      <xsl:value-of select="@value"/>
       <xsl:text>"</xsl:text>
       <xsl:if test="position()!=last()">
         <xsl:text>,</xsl:text>
@@ -685,826 +550,48 @@
       <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:text>    ],
+</xsl:text>
+    <xsl:text>    indexes: [
+</xsl:text>
+    <xsl:for-each select="$widget/path">
+      <xsl:choose>
+        <xsl:when test="not(@index)">
+          <xsl:message terminate="no">
+            <xsl:text>Widget </xsl:text>
+            <xsl:value-of select="$widget/@type"/>
+            <xsl:text> id="</xsl:text>
+            <xsl:value-of select="$eltid"/>
+            <xsl:text>" : No match for path "</xsl:text>
+            <xsl:value-of select="@value"/>
+            <xsl:text>" in HMI tree</xsl:text>
+          </xsl:message>
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:text>            </xsl:text>
+          <xsl:value-of select="@index"/>
+          <xsl:if test="position()!=last()">
+            <xsl:text>,</xsl:text>
+          </xsl:if>
+          <xsl:text>
+</xsl:text>
+        </xsl:otherwise>
+      </xsl:choose>
     </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:text>
+    <xsl:text>    element: id("</xsl:text>
+    <xsl:value-of select="@id"/>
+    <xsl:text>"),
+</xsl:text>
+    <xsl:apply-templates mode="widget_defs" select="$widget">
+      <xsl:with-param name="hmi_element" select="."/>
+    </xsl:apply-templates>
+    <xsl:text>  }</xsl:text>
+    <xsl:if test="position()!=last()">
+      <xsl:text>,</xsl:text>
+    </xsl:if>
+    <xsl:text>
 </xsl:text>
   </xsl:template>
   <xsl:template name="defs_by_labels">
@@ -1537,6 +624,920 @@
       </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,
--- a/svghmi/gen_index_xhtml.ysl2	Tue Mar 17 13:43:19 2020 +0100
+++ b/svghmi/gen_index_xhtml.ysl2	Tue Mar 17 14:01:37 2020 +0100
@@ -60,6 +60,8 @@
 
     include inline_svg.ysl2
 
+    include widget_common.ysl2
+
     template "/" {
         comment > Made with SVGHMI. https://beremiz.org
 
@@ -120,31 +122,7 @@
         */
 
         | var hmi_widgets = {
-        foreach "$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()" > ,`
-        }
+        apply "$hmi_elements", mode="hmi_elements";
         | }
         |
         | var heartbeat_index = «$indexed_hmitree/*[@hmipath = '/HEARTBEAT']/@index»;
@@ -207,31 +185,6 @@
     // }
 
 
-
-    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»"),
-                }
-            }
-        }
-    }
-
-
     template "widget[@type='Display']", mode="widget_defs" {
         param "hmi_element";
         |     frequency: 5,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_common.ysl2	Tue Mar 17 14:01:37 2020 +0100
@@ -0,0 +1,61 @@
+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»"),
+            }
+        }
+    }
+}
+