# HG changeset patch # User Edouard Tisserant <edouard.tisserant@gmail.com> # Date 1584519374 -3600 # Node ID 8e3d130399b035cf980b4eb810f402d6db17a61c # Parent ac08a5d15c157ae83e93c23f116815ee2d1c48e7 SVGHMI: created widget_*.ysl2. Renamed widget_common in widgets_common, so that it doesn't match globing. diff -r ac08a5d15c15 -r 8e3d130399b0 svghmi/gen_index_xhtml.xslt --- 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 => 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 => 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" && idxidx == 0){ -</xsl:text> - <xsl:text> d.call(widget, value, oldval); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> else if(typeof(d) == "object" && d.length >= 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 > 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) => [dv.getInt16(offset, true), 2], -</xsl:text> - <xsl:text> BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], -</xsl:text> - <xsl:text> STRING: (dv, offset) => { -</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> // -> 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 < 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 > 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) => new Int16Array([number]), -</xsl:text> - <xsl:text> BOOL: (truth) => new Int16Array([truth]), -</xsl:text> - <xsl:text> STRING: (str) => { -</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 < 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 => 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 => 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 < 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 > 0) { -</xsl:text> - <xsl:text> let maxfreq = 0; -</xsl:text> - <xsl:text> for(let widget of widgets) -</xsl:text> - <xsl:text> if(maxfreq < 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 && 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(() => 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 >= 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 >= 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,'"')"/> @@ -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 >= 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 >= 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 => 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 => 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" && idxidx == 0){ +</xsl:text> + <xsl:text> d.call(widget, value, oldval); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> else if(typeof(d) == "object" && d.length >= 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 > 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) => [dv.getInt16(offset, true), 2], +</xsl:text> + <xsl:text> BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], +</xsl:text> + <xsl:text> STRING: (dv, offset) => { +</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> // -> 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 < 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 > 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) => new Int16Array([number]), +</xsl:text> + <xsl:text> BOOL: (truth) => new Int16Array([truth]), +</xsl:text> + <xsl:text> STRING: (str) => { +</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 < 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 => 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 => 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 < 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 > 0) { +</xsl:text> + <xsl:text> let maxfreq = 0; +</xsl:text> + <xsl:text> for(let widget of widgets) +</xsl:text> + <xsl:text> if(maxfreq < 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 && 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(() => 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> diff -r ac08a5d15c15 -r 8e3d130399b0 svghmi/gen_index_xhtml.ysl2 --- 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)"); - | }, } } diff -r ac08a5d15c15 -r 8e3d130399b0 svghmi/widget_common.ysl2 --- 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»"), - } - } - } -} - diff -r ac08a5d15c15 -r 8e3d130399b0 svghmi/widget_custom.ysl2 --- /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 + // }) + // } + // || + // } + + diff -r ac08a5d15c15 -r 8e3d130399b0 svghmi/widget_display.ysl2 --- /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 + } + } + | }, + +} diff -r ac08a5d15c15 -r 8e3d130399b0 svghmi/widget_input.ysl2 --- /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 */ + } + | }, +} diff -r ac08a5d15c15 -r 8e3d130399b0 svghmi/widget_jump.ysl2 --- /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)"); + | }, +} diff -r ac08a5d15c15 -r 8e3d130399b0 svghmi/widget_meter.ysl2 --- /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); + | }, +} + + diff -r ac08a5d15c15 -r 8e3d130399b0 svghmi/widget_switch.ysl2 --- /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()" > ,` + } + | ], +} diff -r ac08a5d15c15 -r 8e3d130399b0 svghmi/widgets_common.ysl2 --- /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"; + } + } +} +