# HG changeset patch # User Edouard Tisserant # Date 1653322350 -7200 # Node ID f422d3d71f89aab466cd2006ca13249831cf448d # Parent 435259844a644856d57ac32fa19e74df08dfd302 SVGHMI: fix active/inactive being swapped in ToggleButton diff -r 435259844a64 -r f422d3d71f89 svghmi/analyse_widget.xslt --- a/svghmi/analyse_widget.xslt Mon May 23 18:11:31 2022 +0200 +++ b/svghmi/analyse_widget.xslt Mon May 23 18:12:30 2022 +0200 @@ -50,6 +50,16 @@ + + + Widget id: + + label: + + has wrong syntax of frequency forcing + + + @@ -223,8 +233,6 @@ - frequency = 5; - display = "inactive"; state = "init"; @@ -717,7 +725,7 @@ - PathSlider - + PathSlider - diff -r 435259844a64 -r f422d3d71f89 svghmi/gen_dnd_widget_svg.xslt --- a/svghmi/gen_dnd_widget_svg.xslt Mon May 23 18:11:31 2022 +0200 +++ b/svghmi/gen_dnd_widget_svg.xslt Mon May 23 18:12:30 2022 +0200 @@ -52,6 +52,16 @@ + + + Widget id: + + label: + + has wrong syntax of frequency forcing + + + diff -r 435259844a64 -r f422d3d71f89 svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Mon May 23 18:11:31 2022 +0200 +++ b/svghmi/gen_index_xhtml.xslt Mon May 23 18:12:30 2022 +0200 @@ -207,6 +207,16 @@ + + + Widget id: + + label: + + has wrong syntax of frequency forcing + + + @@ -1248,7 +1258,9 @@ + " + " undefined @@ -1477,6 +1489,68 @@ this.forced_frequency = freq; + this.clip = true; + + } + + + + do_init(){ + + let forced = this.forced_frequency; + + if(forced !== undefined){ + + /* + + once every 10 seconds : 10s + + once per minute : 1m + + once per hour : 1h + + once per day : 1d + + */ + + let unit = forced.slice(-1); + + let factor = { + + "s":1, + + "m":60, + + "h":3600, + + "d":86400}[unit]; + + + + this.frequency = factor ? 1/(factor * Number(forced.slice(0,-1))) + + : Number(forced); + + } + + + + let init = this.init; + + if(typeof(init) == "function"){ + + try { + + init.call(this); + + } catch(err) { + + console.log(err); + + } + + } + } @@ -1649,7 +1723,9 @@ let new_val = eval_operation_string(old_val, opstr); - new_val = this.clip_min_max(index, new_val); + if(this.clip) + + new_val = this.clip_min_max(index, new_val); return apply_hmi_value(realindex, new_val); @@ -1663,7 +1739,9 @@ if(realindex == undefined) return undefined; - new_val = this.clip_min_max(index, new_val); + if(this.clip) + + new_val = this.clip_min_max(index, new_val); return apply_hmi_value(realindex, new_val); @@ -1883,8 +1961,10 @@ - - + + + + @@ -2400,10 +2480,6 @@ _action(){ - console.log("Entering state - - ", this.frequency); - } @@ -2424,8 +2500,6 @@ - frequency = 5; - display = "inactive"; state = "init"; @@ -2492,6 +2566,8 @@ ButtonWidget extends Widget{ + frequency = 5; + @@ -2514,6 +2590,8 @@ PushButtonWidget extends Widget{ + frequency = 20; + @@ -6116,7 +6194,7 @@ - PathSlider - + PathSlider - @@ -6159,7 +6237,7 @@ origPt = undefined; - + @@ -6243,7 +6321,7 @@ bestDistance = beforeDistance; - } else if ((afterLength = bestLength + precision) <= this.pathLength && + } else if ((afterLength = bestLength + precision) <= this.pathLength && (afterDistance = distance2(afterPoint = this.path_elt.getPointAtLength(afterLength))) < bestDistance) { @@ -7627,7 +7705,7 @@ activate(val) { - let [active, inactive] = val ? ["none",""] : ["", "none"]; + let [active, inactive] = val ? ["","none"] : ["none", ""]; if (this.active_elt) @@ -7751,7 +7829,7 @@ modulo: /^%{2}/, - placeholder: /^%(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/, + placeholder: /^%(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxXD])/, key: /^([a-z_][a-z_\d]*)/i, @@ -7877,6 +7955,96 @@ break + case 'D': + + /* + + + + select date format with width + + select time format with precision + + %D => 13:31 AM (default) + + %1D => 13:31 AM + + %.1D => 07/07/20 + + %1.1D => 07/07/20, 13:31 AM + + %1.2D => 07/07/20, 13:31:55 AM + + %2.2D => May 5, 2022, 9:29:16 AM + + %3.3D => May 5, 2022 at 9:28:16 AM GMT+2 + + %4.4D => Thursday, May 5, 2022 at 9:26:59 AM Central European Summer Time + + + + see meaning of DateTimeFormat's options "datestyle" and "timestyle" in MDN + + */ + + + + let [datestyle, timestyle] = [ph.width, ph.precision].map(val => ({ + + 1: "short", + + 2: "medium", + + 3: "long", + + 4: "full" + + }[val])); + + + + if(timestyle === undefined && datestyle === undefined){ + + timestyle = "short"; + + } + + + + let options = { + + dateStyle: datestyle, + + timeStyle: timestyle, + + hour12: false + + } + + + + /* get lang from globals */ + + let lang = get_current_lang_code(); + + arg = Date(arg).toLocaleString('en-US', options); + + + + /* + + TODO: select with padding char + + a: absolute time and date (default) + + r: relative time + + */ + + + + break + case 'j': arg = JSON.stringify(arg, null, ph.width ? parseInt(ph.width) : 0) @@ -8221,498 +8389,490 @@ let widget = hmi_widgets[id]; - let init = widget.init; - - if(typeof(init) == "function"){ - - try { - - init.call(widget); - - } catch(err) { - - console.log(err); + widget.do_init(); + + }); + + }; + + + + // Open WebSocket to relative "/ws" address + + var has_watchdog = window.location.hash == "#watchdog"; + + + + var ws_url = + + window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') + + + '?mode=' + (has_watchdog ? "watchdog" : "multiclient"); + + + + var ws = new WebSocket(ws_url); + + ws.binaryType = 'arraybuffer'; + + + + const dvgetters = { + + INT: (dv,offset) => [dv.getInt16(offset, true), 2], + + BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], + + NODE: (dv,offset) => [dv.getInt8(offset, true), 1], + + REAL: (dv,offset) => [dv.getFloat32(offset, true), 4], + + STRING: (dv, offset) => { + + const size = dv.getInt8(offset); + + return [ + + String.fromCharCode.apply(null, new Uint8Array( + + dv.buffer, /* original buffer */ + + offset + 1, /* string starts after size*/ + + size /* size of string */ + + )), size + 1]; /* total increment */ + + } + + }; + + + + // Apply updates recieved through ws.onmessage to subscribed widgets + + function apply_updates() { + + updates.forEach((value, index) => { + + dispatch_value(index, value); + + }); + + updates.clear(); + + } + + + + // Called on requestAnimationFrame, modifies DOM + + var requestAnimationFrameID = null; + + function animate() { + + // Do the page swith if any one pending + + if(current_subscribed_page != current_visible_page){ + + switch_visible_page(current_subscribed_page); + + } + + + + while(widget = need_cache_apply.pop()){ + + widget.apply_cache(); + + } + + + + if(jumps_need_update) update_jumps(); + + + + apply_updates(); + + + + pending_widget_animates.forEach(widget => widget._animate()); + + pending_widget_animates = []; + + + + requestAnimationFrameID = null; + + } + + + + function requestHMIAnimation() { + + if(requestAnimationFrameID == null){ + + requestAnimationFrameID = window.requestAnimationFrame(animate); + + } + + } + + + + // Message reception handler + + // Hash is verified and HMI values updates resulting from binary parsing + + // are stored until browser can compute next frame, DOM is left untouched + + ws.onmessage = function (evt) { + + + + let data = evt.data; + + let dv = new DataView(data); + + let i = 0; + + try { + + for(let hash_int of hmi_hash) { + + if(hash_int != dv.getUint8(i)){ + + throw new Error("Hash doesn't match"); + + }; + + i++; + + }; + + + + while(i < data.byteLength){ + + let index = dv.getUint32(i, true); + + i += 4; + + let iectype = hmitree_types[index]; + + if(iectype != undefined){ + + let dvgetter = dvgetters[iectype]; + + let [value, bytesize] = dvgetter(dv,i); + + updates.set(index, value); + + i += bytesize; + + } else { + + throw new Error("Unknown index "+index); } + }; + + // register for rendering on next frame, since there are updates + + requestHMIAnimation(); + + } catch(err) { + + // 1003 is for "Unsupported Data" + + // ws.close(1003, err.message); + + + + // TODO : remove debug alert ? + + alert("Error : "+err.message+"\nHMI will be reloaded."); + + + + // force reload ignoring cache + + location.reload(true); + + } + + }; + + + + hmi_hash_u8 = new Uint8Array(hmi_hash); + + + + function send_blob(data) { + + if(data.length > 0) { + + ws.send(new Blob([hmi_hash_u8].concat(data))); + + }; + + }; + + + + const typedarray_types = { + + INT: (number) => new Int16Array([number]), + + BOOL: (truth) => new Int16Array([truth]), + + NODE: (truth) => new Int16Array([truth]), + + REAL: (number) => new Float32Array([number]), + + STRING: (str) => { + + // beremiz default string max size is 128 + + str = str.slice(0,128); + + binary = new Uint8Array(str.length + 1); + + binary[0] = str.length; + + for(let i = 0; i < str.length; i++){ + + binary[i+1] = str.charCodeAt(i); + } - if(widget.forced_frequency !== undefined) - - widget.frequency = widget.forced_frequency; + return binary; + + } + + /* TODO */ + + }; + + + + function send_reset() { + + send_blob(new Uint8Array([1])); /* reset = 1 */ + + }; + + + + var subscriptions = []; + + + + function subscribers(index) { + + let entry = subscriptions[index]; + + let res; + + if(entry == undefined){ + + res = new Set(); + + subscriptions[index] = [res,0]; + + }else{ + + [res, _ign] = entry; + + } + + return res + + } + + + + function get_subscription_period(index) { + + let entry = subscriptions[index]; + + if(entry == undefined) + + return 0; + + let [_ign, period] = entry; + + return period; + + } + + + + function set_subscription_period(index, period) { + + let entry = subscriptions[index]; + + if(entry == undefined){ + + subscriptions[index] = [new Set(), period]; + + } else { + + entry[1] = period; + + } + + } + + + + if(has_watchdog){ + + // artificially subscribe the watchdog widget to "/heartbeat" hmi variable + + // Since dispatch directly calls change_hmi_value, + + // PLC will periodically send variable at given frequency + + subscribers(heartbeat_index).add({ + + /* type: "Watchdog", */ + + frequency: 1, + + indexes: [heartbeat_index], + + new_hmi_value: function(index, value, oldval) { + + apply_hmi_value(heartbeat_index, value+1); + + } }); - }; - - - - // Open WebSocket to relative "/ws" address - - var has_watchdog = window.location.hash == "#watchdog"; - - - - var ws_url = - - window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') - - + '?mode=' + (has_watchdog ? "watchdog" : "multiclient"); - - - - var ws = new WebSocket(ws_url); - - ws.binaryType = 'arraybuffer'; - - - - const dvgetters = { - - INT: (dv,offset) => [dv.getInt16(offset, true), 2], - - BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], - - NODE: (dv,offset) => [dv.getInt8(offset, true), 1], - - REAL: (dv,offset) => [dv.getFloat32(offset, true), 4], - - STRING: (dv, offset) => { - - const size = dv.getInt8(offset); - - return [ - - String.fromCharCode.apply(null, new Uint8Array( - - dv.buffer, /* original buffer */ - - offset + 1, /* string starts after size*/ - - size /* size of string */ - - )), size + 1]; /* total increment */ + } + + + + // subscribe to per instance current page hmi variable + + // PLC must prefix page name with "!" for page switch to happen + + subscribers(current_page_var_index).add({ + + frequency: 1, + + indexes: [current_page_var_index], + + new_hmi_value: function(index, value, oldval) { + + if(value.startsWith("!")) + + switch_page(value.slice(1)); } - }; - - - - // Apply updates recieved through ws.onmessage to subscribed widgets - - function apply_updates() { - - updates.forEach((value, index) => { - - dispatch_value(index, value); - - }); - - updates.clear(); + }); + + + + function svg_text_to_multiline(elt) { + + return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); } - // Called on requestAnimationFrame, modifies DOM - - var requestAnimationFrameID = null; - - function animate() { - - // Do the page swith if any one pending - - if(current_subscribed_page != current_visible_page){ - - switch_visible_page(current_subscribed_page); + function multiline_to_svg_text(elt, str) { + + str.split('\n').map((line,i) => {elt.children[i].textContent = line;}); + + } + + + + function switch_langnum(langnum) { + + langnum = Math.max(0, Math.min(langs.length - 1, langnum)); + + + + for (let translation of translations) { + + let [objs, msgs] = translation; + + let msg = msgs[langnum]; + + for (let obj of objs) { + + multiline_to_svg_text(obj, msg); + + obj.setAttribute("lang",langnum); + + } } - - - while(widget = need_cache_apply.pop()){ - - widget.apply_cache(); + return langnum; + + } + + + + // backup original texts + + for (let translation of translations) { + + let [objs, msgs] = translation; + + msgs.unshift(svg_text_to_multiline(objs[0])); + + } + + + + var lang_local_index = hmi_local_index("lang"); + + var langcode_local_index = hmi_local_index("lang_code"); + + var langname_local_index = hmi_local_index("lang_name"); + + subscribers(lang_local_index).add({ + + indexes: [lang_local_index], + + new_hmi_value: function(index, value, oldval) { + + let current_lang = switch_langnum(value); + + let [langname,langcode] = langs[current_lang]; + + apply_hmi_value(langcode_local_index, langcode); + + apply_hmi_value(langname_local_index, langname); + + switch_page(); } - - - if(jumps_need_update) update_jumps(); - - - - apply_updates(); - - - - pending_widget_animates.forEach(widget => widget._animate()); - - pending_widget_animates = []; - - - - requestAnimationFrameID = null; + }); + + + + // returns en_US, fr_FR or en_UK depending on selected language + + function get_current_lang_code(){ + + return cache[langcode_local_index]; } - function requestHMIAnimation() { - - if(requestAnimationFrameID == null){ - - requestAnimationFrameID = window.requestAnimationFrame(animate); - - } - - } - - - - // Message reception handler - - // Hash is verified and HMI values updates resulting from binary parsing - - // are stored until browser can compute next frame, DOM is left untouched - - ws.onmessage = function (evt) { - - - - let data = evt.data; - - let dv = new DataView(data); - - let i = 0; - - try { - - for(let hash_int of hmi_hash) { - - if(hash_int != dv.getUint8(i)){ - - throw new Error("Hash doesn't match"); - - }; - - i++; - - }; - - - - while(i < data.byteLength){ - - let index = dv.getUint32(i, true); - - i += 4; - - let iectype = hmitree_types[index]; - - if(iectype != undefined){ - - let dvgetter = dvgetters[iectype]; - - let [value, bytesize] = dvgetter(dv,i); - - updates.set(index, value); - - i += bytesize; - - } else { - - throw new Error("Unknown index "+index); - - } - - }; - - // register for rendering on next frame, since there are updates - - requestHMIAnimation(); - - } catch(err) { - - // 1003 is for "Unsupported Data" - - // ws.close(1003, err.message); - - - - // TODO : remove debug alert ? - - alert("Error : "+err.message+"\nHMI will be reloaded."); - - - - // force reload ignoring cache - - location.reload(true); - - } - - }; - - - - hmi_hash_u8 = new Uint8Array(hmi_hash); - - - - function send_blob(data) { - - if(data.length > 0) { - - ws.send(new Blob([hmi_hash_u8].concat(data))); - - }; - - }; - - - - const typedarray_types = { - - INT: (number) => new Int16Array([number]), - - BOOL: (truth) => new Int16Array([truth]), - - NODE: (truth) => new Int16Array([truth]), - - REAL: (number) => new Float32Array([number]), - - STRING: (str) => { - - // beremiz default string max size is 128 - - str = str.slice(0,128); - - binary = new Uint8Array(str.length + 1); - - binary[0] = str.length; - - for(let i = 0; i < str.length; i++){ - - binary[i+1] = str.charCodeAt(i); - - } - - return binary; - - } - - /* TODO */ - - }; - - - - function send_reset() { - - send_blob(new Uint8Array([1])); /* reset = 1 */ - - }; - - - - var subscriptions = []; - - - - function subscribers(index) { - - let entry = subscriptions[index]; - - let res; - - if(entry == undefined){ - - res = new Set(); - - subscriptions[index] = [res,0]; - - }else{ - - [res, _ign] = entry; - - } - - return res - - } - - - - function get_subscription_period(index) { - - let entry = subscriptions[index]; - - if(entry == undefined) - - return 0; - - let [_ign, period] = entry; - - return period; - - } - - - - function set_subscription_period(index, period) { - - let entry = subscriptions[index]; - - if(entry == undefined){ - - subscriptions[index] = [new Set(), period]; - - } else { - - entry[1] = period; - - } - - } - - - - if(has_watchdog){ - - // artificially subscribe the watchdog widget to "/heartbeat" hmi variable - - // Since dispatch directly calls change_hmi_value, - - // PLC will periodically send variable at given frequency - - subscribers(heartbeat_index).add({ - - /* type: "Watchdog", */ - - frequency: 1, - - indexes: [heartbeat_index], - - new_hmi_value: function(index, value, oldval) { - - apply_hmi_value(heartbeat_index, value+1); - - } - - }); - - } - - - - // subscribe to per instance current page hmi variable - - // PLC must prefix page name with "!" for page switch to happen - - subscribers(current_page_var_index).add({ - - frequency: 1, - - indexes: [current_page_var_index], - - new_hmi_value: function(index, value, oldval) { - - if(value.startsWith("!")) - - switch_page(value.slice(1)); - - } - - }); - - - - function svg_text_to_multiline(elt) { - - return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); - - } - - - - function multiline_to_svg_text(elt, str) { - - str.split('\n').map((line,i) => {elt.children[i].textContent = line;}); - - } - - - - function switch_langnum(langnum) { - - langnum = Math.max(0, Math.min(langs.length - 1, langnum)); - - - - for (let translation of translations) { - - let [objs, msgs] = translation; - - let msg = msgs[langnum]; - - for (let obj of objs) { - - multiline_to_svg_text(obj, msg); - - obj.setAttribute("lang",langnum); - - } - - } - - return langnum; - - } - - - - // backup original texts - - for (let translation of translations) { - - let [objs, msgs] = translation; - - msgs.unshift(svg_text_to_multiline(objs[0])); - - } - - - - var lang_local_index = hmi_local_index("lang"); - - var langcode_local_index = hmi_local_index("lang_code"); - - var langname_local_index = hmi_local_index("lang_name"); - - subscribers(lang_local_index).add({ - - indexes: [lang_local_index], - - new_hmi_value: function(index, value, oldval) { - - let current_lang = switch_langnum(value); - - let [langname,langcode] = langs[current_lang]; - - apply_hmi_value(langcode_local_index, langcode); - - apply_hmi_value(langname_local_index, langname); - - switch_page(); - - } - - }); - - - function setup_lang(){ let current_lang = cache[lang_local_index]; diff -r 435259844a64 -r f422d3d71f89 svghmi/widget_tooglebutton.ysl2 --- a/svghmi/widget_tooglebutton.ysl2 Mon May 23 18:11:31 2022 +0200 +++ b/svghmi/widget_tooglebutton.ysl2 Mon May 23 18:12:30 2022 +0200 @@ -38,7 +38,7 @@ } activate(val) { - let [active, inactive] = val ? ["none",""] : ["", "none"]; + let [active, inactive] = val ? ["","none"] : ["none", ""]; if (this.active_elt) this.active_elt.style.display = active; if (this.inactive_elt)