SVGHMI: created widget_*.ysl2. Renamed widget_common in widgets_common, so that it doesn't match globing.
--- 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>
--- a/svghmi/gen_index_xhtml.ysl2 Tue Mar 17 14:11:54 2020 +0100
+++ b/svghmi/gen_index_xhtml.ysl2 Wed Mar 18 09:16:14 2020 +0100
@@ -60,7 +60,9 @@
include inline_svg.ysl2
- include widget_common.ysl2
+ include widgets_common.ysl2
+
+ include widget_*.ysl2
template "/" {
comment > Made with SVGHMI. https://beremiz.org
@@ -73,6 +75,7 @@
xmlns:xlink="http://www.w3.org/1999/xlink" {
head;
body style="margin:0;overflow:hidden;" {
+ // Inline SVG
copy "$result_svg";
script{
call "scripts";
@@ -83,30 +86,11 @@
function "scripts"
{
- | //(function(){
|
| id = idstr => document.getElementById(idstr);
|
| var hmi_hash = [«$hmitree/@hash»];
- /* TODO re-enable
- ||
- function evaluate_js_from_descriptions() {
- var Page;
- var Input;
- var Display;
- var res = [];
- ||
- const "midmark" > \n«$mark»
- apply """//*[contains(child::svg:desc, $midmark) or \
- starts-with(child::svg:desc, $mark)]""",2
- mode="code_from_descs";
- ||
- return res;
- }
- ||
- */
-
| var hmi_widgets = {
apply "$hmi_elements", mode="hmi_elements";
| }
@@ -137,177 +121,5 @@
| var default_page = "«$default_page»";
| var svg_root = id("«/svg:svg/@id»");
include text svghmi.js
- | //})();
- }
-
- // template "*", mode="code_from_descs" {
- // ||
- // {
- // var path, role, name, priv;
- // var id = "«@id»";
- // ||
-
- // /* if label is used, use it as default name */
- // if "@inkscape:label"
- // |> name = "«@inkscape:label»";
-
- // | /* -------------- */
-
- // // this breaks indent, but fixing indent could break string literals
- // value "substring-after(svg:desc, $mark)";
- // // nobody reads generated code anyhow...
-
- // ||
-
- // /* -------------- */
- // res.push({
- // path:path,
- // role:role,
- // name:name,
- // priv:priv
- // })
- // }
- // ||
- // }
-
-
- template "widget[@type='Display']", mode="widget_defs" {
- param "hmi_element";
- | frequency: 5,
- | dispatch: function(value) {
- choose {
- when "$hmi_element[self::svg:text]"{
- // TODO : care about <tspan> ?
- | this.element.textContent = String(value);
- }
- otherwise {
- warning > Display widget as a group not implemented
- }
- }
- | },
-
- }
- template "widget[@type='Meter']", mode="widget_defs" {
- param "hmi_element";
- | frequency: 10,
- labels("needle range");
- optional_labels("value min max");
- | dispatch: function(value) {
- | if(this.value_elt)
- | this.value_elt.textContent = String(value);
- | let [min,max,totallength] = this.range;
- | let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min)));
- | let tip = this.range_elt.getPointAtLength(length);
- | this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y);
- | },
- | origin: undefined,
- | range: undefined,
- | init: function() {
- | let min = this.min_elt ?
- | Number(this.min_elt.textContent) :
- | this.args.length >= 1 ? this.args[0] : 0;
- | let max = this.max_elt ?
- | Number(this.max_elt.textContent) :
- | this.args.length >= 2 ? this.args[1] : 100;
- | this.range = [min, max, this.range_elt.getTotalLength()]
- | this.origin = this.needle_elt.getPointAtLength(0);
- | },
- }
-
- def "func:escape_quotes" {
- param "txt";
- // have to use a python string to enter escaped quote
- const "frst", !"substring-before($txt,'\"')"!;
- const "frstln", "string-length($frst)";
- choose {
- when "$frstln > 0 and string-length($txt) > $frstln" {
- result !"concat($frst,'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
- }
- otherwise {
- result "$txt";
- }
- }
- }
-
- template "widget[@type='Input']", mode="widget_defs" {
- param "hmi_element";
- const "value_elt" {
- optional_labels("value");
- }
- const "have_value","string-length($value_elt)>0";
- value "$value_elt";
- if "$have_value"
- | frequency: 5,
-
- | dispatch: function(value) {
-
- if "$have_value"
- | this.value_elt.textContent = String(value);
-
- | },
- const "edit_elt_id","$hmi_element/*[@inkscape:label='edit'][1]/@id";
- | init: function() {
- if "$edit_elt_id" {
- | id("«$edit_elt_id»").addEventListener(
- | "click",
- | evt => alert('XXX TODO : Edit value'));
- }
- foreach "$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]" {
- | id("«@id»").addEventListener(
- | "click",
- | evt => {let new_val = change_hmi_value(this.indexes[0], "«func:escape_quotes(@inkscape:label)»");
- if "$have_value"{
- | this.value_elt.textContent = String(new_val);
- }
- | });
- /* TODO gray out value until refreshed */
- }
- | },
- }
- template "widget[@type='Button']", mode="widget_defs" {
- }
- template "widget[@type='Toggle']", mode="widget_defs" {
- | frequency: 5,
- }
- template "widget[@type='Switch']", mode="widget_defs" {
- param "hmi_element";
- | frequency: 5,
- | dispatch: function(value) {
- | for(let choice of this.choices){
- | if(value != choice.value){
- | choice.elt.setAttribute("style", "display:none");
- | } else {
- | choice.elt.setAttribute("style", choice.style);
- | }
- | }
- | },
- | init: function() {
- | // Hello Switch
- | },
- | choices: [
- const "regex",!"'^(\"[^\"].*\"|\-?[0-9]+)(#.*)?$'"!;
- foreach "$hmi_element/*[regexp:test(@inkscape:label,$regex)]" {
- const "literal", "regexp:match(@inkscape:label,$regex)[2]";
- | {
- | elt:id("«@id»"),
- | style:"«@style»",
- | value:«$literal»
- | }`if "position()!=last()" > ,`
- }
- | ],
- }
- template "widget[@type='Jump']", mode="widget_defs" {
- param "hmi_element";
- | on_click: function(evt) {
- | switch_page(this.args[0], this.indexes[0]);
- | },
- | init: function() {
- /* registering event this way doies not "click" through svg:use
- | this.element.onclick = evt => switch_page(this.args[0]);
- event must be registered by adding attribute to element instead
- TODO : generalize mouse event handling by global event capture + getElementsAtPoint()
- */
- | this.element.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_click(evt)");
- | },
}
}
--- a/svghmi/widget_common.ysl2 Tue Mar 17 14:11:54 2020 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-in xsl decl labels(*ptr, name="defs_by_labels") alias call-template {
- with "hmi_element", "$hmi_element";
- with "labels"{text *ptr};
-};
-
-in xsl decl optional_labels(*ptr, name="defs_by_labels") alias call-template {
- with "hmi_element", "$hmi_element";
- with "labels"{text *ptr};
- with "mandatory","'no'";
-};
-
-template "svg:*", mode="hmi_elements" {
- const "widget", "func:parselabel(@inkscape:label)/widget";
- const "eltid","@id";
- | "«@id»": {
- | type: "«$widget/@type»",
- | args: [
- foreach "$widget/arg"
- | "«@value»"`if "position()!=last()" > ,`
- | ],
- | indexes: [
- foreach "$widget/path" {
- choose {
- when "not(@index)" {
- warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree
- }
- otherwise {
- | «@index»`if "position()!=last()" > ,`
- }
- }
- }
- | ],
- | element: id("«@id»"),
- apply "$widget", mode="widget_defs" with "hmi_element",".";
- | }`if "position()!=last()" > ,`
-}
-
-
-function "defs_by_labels" {
- param "labels","''";
- param "mandatory","'yes'";
- param "hmi_element";
- const "widget_type","@type";
- foreach "str:split($labels)" {
- const "name",".";
- const "elt_id","$result_svg_ns//*[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]/@id";
- choose {
- when "not($elt_id)" {
- if "$mandatory='yes'" {
- // TODO FIXME error > «$widget_type» widget must have a «$name» element
- warning > «$widget_type» widget must have a «$name» element
- }
- // otherwise produce nothing
- }
- otherwise {
- | «$name»_elt: id("«$elt_id»"),
- }
- }
- }
-}
-
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_custom.ysl2 Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,60 @@
+// widget_custom.ysl2
+//
+// widget entierely defined from JS code in Inkscape description field
+
+// TODO
+
+// a preliminary implementation was initially attempted but disabled
+// code collected around before code refactoring
+
+
+
+ /* TODO re-enable
+ ||
+ function evaluate_js_from_descriptions() {
+ var Page;
+ var Input;
+ var Display;
+ var res = [];
+ ||
+ const "midmark" > \n«$mark»
+ apply """//*[contains(child::svg:desc, $midmark) or \
+ starts-with(child::svg:desc, $mark)]""",2
+ mode="code_from_descs";
+ ||
+ return res;
+ }
+ ||
+ */
+
+ // template "*", mode="code_from_descs" {
+ // ||
+ // {
+ // var path, role, name, priv;
+ // var id = "«@id»";
+ // ||
+
+ // /* if label is used, use it as default name */
+ // if "@inkscape:label"
+ // |> name = "«@inkscape:label»";
+
+ // | /* -------------- */
+
+ // // this breaks indent, but fixing indent could break string literals
+ // value "substring-after(svg:desc, $mark)";
+ // // nobody reads generated code anyhow...
+
+ // ||
+
+ // /* -------------- */
+ // res.push({
+ // path:path,
+ // role:role,
+ // name:name,
+ // priv:priv
+ // })
+ // }
+ // ||
+ // }
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_display.ysl2 Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,19 @@
+// widget_display.ysl2
+
+
+template "widget[@type='Display']", mode="widget_defs" {
+ param "hmi_element";
+ | frequency: 5,
+ | dispatch: function(value) {
+ choose {
+ when "$hmi_element[self::svg:text]"{
+ // TODO : care about <tspan> ?
+ | this.element.textContent = String(value);
+ }
+ otherwise {
+ warning > Display widget as a group not implemented
+ }
+ }
+ | },
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_input.ysl2 Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,37 @@
+// widget_input.ysl2
+
+template "widget[@type='Input']", mode="widget_defs" {
+ param "hmi_element";
+ const "value_elt" {
+ optional_labels("value");
+ }
+ const "have_value","string-length($value_elt)>0";
+ value "$value_elt";
+ if "$have_value"
+ | frequency: 5,
+
+ | dispatch: function(value) {
+
+ if "$have_value"
+ | this.value_elt.textContent = String(value);
+
+ | },
+ const "edit_elt_id","$hmi_element/*[@inkscape:label='edit'][1]/@id";
+ | init: function() {
+ if "$edit_elt_id" {
+ | id("«$edit_elt_id»").addEventListener(
+ | "click",
+ | evt => alert('XXX TODO : Edit value'));
+ }
+ foreach "$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]" {
+ | id("«@id»").addEventListener(
+ | "click",
+ | evt => {let new_val = change_hmi_value(this.indexes[0], "«func:escape_quotes(@inkscape:label)»");
+ if "$have_value"{
+ | this.value_elt.textContent = String(new_val);
+ }
+ | });
+ /* TODO gray out value until refreshed */
+ }
+ | },
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_jump.ysl2 Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,16 @@
+// widget_jump.ysl2
+
+template "widget[@type='Jump']", mode="widget_defs" {
+ param "hmi_element";
+ | on_click: function(evt) {
+ | switch_page(this.args[0], this.indexes[0]);
+ | },
+ | init: function() {
+ /* registering event this way doies not "click" through svg:use
+ | this.element.onclick = evt => switch_page(this.args[0]);
+ event must be registered by adding attribute to element instead
+ TODO : generalize mouse event handling by global event capture + getElementsAtPoint()
+ */
+ | this.element.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_click(evt)");
+ | },
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_meter.ysl2 Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,31 @@
+// widget_meter.ysl2
+
+
+template "widget[@type='Meter']", mode="widget_defs" {
+ param "hmi_element";
+ | frequency: 10,
+ labels("needle range");
+ optional_labels("value min max");
+ | dispatch: function(value) {
+ | if(this.value_elt)
+ | this.value_elt.textContent = String(value);
+ | let [min,max,totallength] = this.range;
+ | let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min)));
+ | let tip = this.range_elt.getPointAtLength(length);
+ | this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y);
+ | },
+ | origin: undefined,
+ | range: undefined,
+ | init: function() {
+ | let min = this.min_elt ?
+ | Number(this.min_elt.textContent) :
+ | this.args.length >= 1 ? this.args[0] : 0;
+ | let max = this.max_elt ?
+ | Number(this.max_elt.textContent) :
+ | this.args.length >= 2 ? this.args[1] : 100;
+ | this.range = [min, max, this.range_elt.getTotalLength()]
+ | this.origin = this.needle_elt.getPointAtLength(0);
+ | },
+}
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_switch.ysl2 Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,29 @@
+// widget_switch.ysl2
+
+template "widget[@type='Switch']", mode="widget_defs" {
+ param "hmi_element";
+ | frequency: 5,
+ | dispatch: function(value) {
+ | for(let choice of this.choices){
+ | if(value != choice.value){
+ | choice.elt.setAttribute("style", "display:none");
+ | } else {
+ | choice.elt.setAttribute("style", choice.style);
+ | }
+ | }
+ | },
+ | init: function() {
+ | // Hello Switch
+ | },
+ | choices: [
+ const "regex",!"'^(\"[^\"].*\"|\-?[0-9]+)(#.*)?$'"!;
+ foreach "$hmi_element/*[regexp:test(@inkscape:label,$regex)]" {
+ const "literal", "regexp:match(@inkscape:label,$regex)[2]";
+ | {
+ | elt:id("«@id»"),
+ | style:"«@style»",
+ | value:«$literal»
+ | }`if "position()!=last()" > ,`
+ }
+ | ],
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widgets_common.ysl2 Wed Mar 18 09:16:14 2020 +0100
@@ -0,0 +1,76 @@
+in xsl decl labels(*ptr, name="defs_by_labels") alias call-template {
+ with "hmi_element", "$hmi_element";
+ with "labels"{text *ptr};
+};
+
+in xsl decl optional_labels(*ptr, name="defs_by_labels") alias call-template {
+ with "hmi_element", "$hmi_element";
+ with "labels"{text *ptr};
+ with "mandatory","'no'";
+};
+
+template "svg:*", mode="hmi_elements" {
+ const "widget", "func:parselabel(@inkscape:label)/widget";
+ const "eltid","@id";
+ | "«@id»": {
+ | type: "«$widget/@type»",
+ | args: [
+ foreach "$widget/arg"
+ | "«@value»"`if "position()!=last()" > ,`
+ | ],
+ | indexes: [
+ foreach "$widget/path" {
+ choose {
+ when "not(@index)" {
+ warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree
+ }
+ otherwise {
+ | «@index»`if "position()!=last()" > ,`
+ }
+ }
+ }
+ | ],
+ | element: id("«@id»"),
+ apply "$widget", mode="widget_defs" with "hmi_element",".";
+ | }`if "position()!=last()" > ,`
+}
+
+
+function "defs_by_labels" {
+ param "labels","''";
+ param "mandatory","'yes'";
+ param "hmi_element";
+ const "widget_type","@type";
+ foreach "str:split($labels)" {
+ const "name",".";
+ const "elt_id","$result_svg_ns//*[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]/@id";
+ choose {
+ when "not($elt_id)" {
+ if "$mandatory='yes'" {
+ // TODO FIXME error > «$widget_type» widget must have a «$name» element
+ warning > «$widget_type» widget must have a «$name» element
+ }
+ // otherwise produce nothing
+ }
+ otherwise {
+ | «$name»_elt: id("«$elt_id»"),
+ }
+ }
+ }
+}
+
+def "func:escape_quotes" {
+ param "txt";
+ // have to use a python string to enter escaped quote
+ const "frst", !"substring-before($txt,'\"')"!;
+ const "frstln", "string-length($frst)";
+ choose {
+ when "$frstln > 0 and string-length($txt) > $frstln" {
+ result !"concat($frst,'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
+ }
+ otherwise {
+ result "$txt";
+ }
+ }
+}
+