diff -r 9ec5ecf9f589 -r c04c6db09eff 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];