# HG changeset patch
# User Edouard Tisserant
# Date 1597051806 -7200
# Node ID 22b969b409b033ab0f6862f865dcfd11f39edfcc
# Parent dabad70db1bfec5b4afdeef1ee821d974f1d0163# Parent 085a678715d04247f413d33392aebee63d418279
Merge
diff -r 085a678715d0 -r 22b969b409b0 svghmi/detachable_pages.ysl2
--- a/svghmi/detachable_pages.ysl2 Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/detachable_pages.ysl2 Mon Aug 10 11:30:06 2020 +0200
@@ -138,14 +138,14 @@
warning > Page id="«$page/@id»" : No match for path "«$desc/path/@value»" in HMI tree
| page_index: «$desc/path/@index»,
}
- | relative_widgets: [
- foreach "$page_relative_widgets" {
- | hmi_widgets["«@id»"]`if "position()!=last()" > ,`
- }
- | ],
- | absolute_widgets: [
- foreach "$page_managed_widgets[not(@id = $page_relative_widgets/@id)]" {
- | hmi_widgets["«@id»"]`if "position()!=last()" > ,`
+ | widgets: [
+ foreach "$page_managed_widgets" {
+ const "widget_paths_relativeness"
+ foreach "func:widget(@id)/path" {
+ value "func:is_descendant_path(@value, $desc/path/@value)";
+ if "position()!=last()" > ,
+ }
+ | [hmi_widgets["«@id»"], [«$widget_paths_relativeness»]]`if "position()!=last()" > ,`
}
| ],
| jumps: [
@@ -168,7 +168,7 @@
| }`if "position()!=last()" > ,`
}
-emit "declarations:page-desc" {
+emit "definitions:page-desc" {
|
| var page_desc = {
apply "$hmi_pages", mode="page_desc";
diff -r 085a678715d0 -r 22b969b409b0 svghmi/gen_index_xhtml.xslt
--- a/svghmi/gen_index_xhtml.xslt Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/gen_index_xhtml.xslt Mon Aug 10 11:30:06 2020 +0200
@@ -497,26 +497,22 @@
,
- relative_widgets: [
-
-
- hmi_widgets["
+ widgets: [
+
+
+
+
+
+
+ ,
+
+
+
+ [hmi_widgets["
- "]
-
- ,
-
-
-
-
- ],
-
- absolute_widgets: [
-
-
- hmi_widgets["
-
- "]
+ "], [
+
+ ]]
,
@@ -573,8 +569,8 @@
-
-
+
+
/*
@@ -947,89 +943,161 @@
/* remove subsribers */
+ if(!this.unsubscribable)
+
+ for(let i = 0; i < this.indexes.length; i++) {
+
+ let index = this.indexes[i];
+
+ if(this.relativeness[i])
+
+ index += this.offset;
+
+ subscribers[index].delete(this);
+
+ }
+
+ this.offset = 0;
+
+ this.relativeness = undefined;
+
+ }
+
+
+
+ sub(new_offset=0, relativeness){
+
+ this.offset = new_offset;
+
+ this.relativeness = relativeness;
+
+ /* add this's subsribers */
+
+ if(!this.unsubscribable)
+
+ for(let i = 0; i < this.indexes.length; i++) {
+
+ let index = this.indexes[i];
+
+ if(relativeness[i])
+
+ index += new_offset;
+
+ subscribers[index].add(this);
+
+ }
+
+ need_cache_apply.push(this);
+
+ }
+
+
+
+ apply_cache() {
+
if(!this.unsubscribable) for(let index of this.indexes){
- let idx = index + this.offset;
-
- subscribers[idx].delete(this);
+ /* dispatch current cache in newly opened page widgets */
+
+ let realindex = index+this.offset;
+
+ let cached_val = cache[realindex];
+
+ if(cached_val != undefined)
+
+ this.new_hmi_value(realindex, cached_val, cached_val);
}
- this.offset = 0;
-
}
- sub(new_offset=0){
-
- /* set the offset because relative */
-
- this.offset = new_offset;
-
- /* add this's subsribers */
-
- if(!this.unsubscribable) for(let index of this.indexes){
-
- subscribers[index + new_offset].add(this);
+ get_idx(index) {
+
+ let orig = this.indexes[index];
+
+ return this.relativeness[index] ? orig + this.offset : orig;
+
+ }
+
+ change_hmi_value(index,opstr) {
+
+ return change_hmi_value(this.get_idx(index), opstr);
+
+ }
+
+
+
+ apply_hmi_value(index, new_val) {
+
+ return apply_hmi_value(this.get_idx(0), new_val);
+
+ }
+
+
+
+ new_hmi_value(index, value, oldval) {
+
+ try {
+
+ // TODO avoid searching, store index at sub()
+
+ for(let i = 0; i < this.indexes.length; i++) {
+
+ let refindex = this.indexes[i];
+
+ if(this.relativeness[i])
+
+ refindex += this.offset;
+
+
+
+ if(index == refindex) {
+
+ let d = this.dispatch;
+
+ if(typeof(d) == "function"){
+
+ d.call(this, value, oldval, i);
+
+ }
+
+ else if(typeof(d) == "object"){
+
+ d[i].call(this, value, oldval);
+
+ }
+
+ /* else dispatch_0, ..., dispatch_n ? */
+
+ /*else {
+
+ throw new Error("Dunno how to dispatch to widget at index = " + index);
+
+ }*/
+
+ break;
+
+ }
+
+ }
+
+ } catch(err) {
+
+ console.log(err);
}
- need_cache_apply.push(this);
-
}
-
-
- apply_cache() {
-
- if(!this.unsubscribable) for(let index of this.indexes){
-
- /* dispatch current cache in newly opened page widgets */
-
- let realindex = index+this.offset;
-
- let cached_val = cache[realindex];
-
- if(cached_val != undefined)
-
- dispatch_value_to_widget(this, realindex, cached_val, cached_val);
-
- }
-
- }
-
-
-
- get_idx(index) {
-
- let orig = this.indexes[index];
-
- return this.offset ? orig + this.offset : orig;
-
- }
-
- change_hmi_value(index,opstr) {
-
- return change_hmi_value(this.get_idx(index), opstr);
-
- }
-
-
-
- apply_hmi_value(index, new_val) {
-
- return apply_hmi_value(this.get_idx(0), new_val);
-
- }
-
}
-
-
+
+
/*
@@ -1057,8 +1125,8 @@
-
-
+
+
/*
@@ -1690,9 +1758,13 @@
frequency = 5;
- dispatch(value) {
-
- this.element.textContent = String(value);
+ dispatch(value, oldval, index) {
+
+ this.fields[index] = value;
+
+ console.log(value, index);
+
+ this.element.textContent = this.args.length == 1 ? vsprintf(this.args[0],this.fields) : this.fields.join(' ');
}
@@ -1708,6 +1780,487 @@
" is not a svg::text element
+ fields: [],
+
+
+
+
+
+
+ /*
+
+ */
+
+
+
+ /* https://github.com/alexei/sprintf.js/blob/master/src/sprintf.js */
+
+ /* global window, exports, define */
+
+
+
+ !function() {
+
+ 'use strict'
+
+
+
+ var re = {
+
+ not_string: /[^s]/,
+
+ not_bool: /[^t]/,
+
+ not_type: /[^T]/,
+
+ not_primitive: /[^v]/,
+
+ number: /[diefg]/,
+
+ numeric_arg: /[bcdiefguxX]/,
+
+ json: /[j]/,
+
+ not_json: /[^j]/,
+
+ text: /^[^%]+/,
+
+ modulo: /^%{2}/,
+
+ placeholder: /^%(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,
+
+ key: /^([a-z_][a-z_\d]*)/i,
+
+ key_access: /^\.([a-z_][a-z_\d]*)/i,
+
+ index_access: /^\[(\d+)\]/,
+
+ sign: /^[+-]/
+
+ }
+
+
+
+ function sprintf(key) {
+
+ //
+
+ is not an array, but should be fine for this call
+
+ return sprintf_format(sprintf_parse(key), arguments)
+
+ }
+
+
+
+ function vsprintf(fmt, argv) {
+
+ return sprintf.apply(null, [fmt].concat(argv || []))
+
+ }
+
+
+
+ function sprintf_format(parse_tree, argv) {
+
+ var cursor = 1, tree_length = parse_tree.length, arg, output = '', i, k, ph, pad, pad_character, pad_length, is_positive, sign
+
+ for (i = 0; i < tree_length; i++) {
+
+ if (typeof parse_tree[i] === 'string') {
+
+ output += parse_tree[i]
+
+ }
+
+ else if (typeof parse_tree[i] === 'object') {
+
+ ph = parse_tree[i] // convenience purposes only
+
+ if (ph.keys) { // keyword argument
+
+ arg = argv[cursor]
+
+ for (k = 0; k < ph.keys.length; k++) {
+
+ if (arg == undefined) {
+
+ throw new Error(sprintf('[sprintf] Cannot access property "%s" of undefined value "%s"', ph.keys[k], ph.keys[k-1]))
+
+ }
+
+ arg = arg[ph.keys[k]]
+
+ }
+
+ }
+
+ else if (ph.param_no) { // positional argument (explicit)
+
+ arg = argv[ph.param_no]
+
+ }
+
+ else { // positional argument (implicit)
+
+ arg = argv[cursor++]
+
+ }
+
+
+
+ if (re.not_type.test(ph.type) && re.not_primitive.test(ph.type) && arg instanceof Function) {
+
+ arg = arg()
+
+ }
+
+
+
+ if (re.numeric_arg.test(ph.type) && (typeof arg !== 'number' && isNaN(arg))) {
+
+ throw new TypeError(sprintf('[sprintf] expecting number but found %T', arg))
+
+ }
+
+
+
+ if (re.number.test(ph.type)) {
+
+ is_positive = arg >= 0
+
+ }
+
+
+
+ switch (ph.type) {
+
+ case 'b':
+
+ arg = parseInt(arg, 10).toString(2)
+
+ break
+
+ case 'c':
+
+ arg = String.fromCharCode(parseInt(arg, 10))
+
+ break
+
+ case 'd':
+
+ case 'i':
+
+ arg = parseInt(arg, 10)
+
+ break
+
+ case 'j':
+
+ arg = JSON.stringify(arg, null, ph.width ? parseInt(ph.width) : 0)
+
+ break
+
+ case 'e':
+
+ arg = ph.precision ? parseFloat(arg).toExponential(ph.precision) : parseFloat(arg).toExponential()
+
+ break
+
+ case 'f':
+
+ arg = ph.precision ? parseFloat(arg).toFixed(ph.precision) : parseFloat(arg)
+
+ break
+
+ case 'g':
+
+ arg = ph.precision ? String(Number(arg.toPrecision(ph.precision))) : parseFloat(arg)
+
+ break
+
+ case 'o':
+
+ arg = (parseInt(arg, 10) >>> 0).toString(8)
+
+ break
+
+ case 's':
+
+ arg = String(arg)
+
+ arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+
+ break
+
+ case 't':
+
+ arg = String(!!arg)
+
+ arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+
+ break
+
+ case 'T':
+
+ arg = Object.prototype.toString.call(arg).slice(8, -1).toLowerCase()
+
+ arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+
+ break
+
+ case 'u':
+
+ arg = parseInt(arg, 10) >>> 0
+
+ break
+
+ case 'v':
+
+ arg = arg.valueOf()
+
+ arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+
+ break
+
+ case 'x':
+
+ arg = (parseInt(arg, 10) >>> 0).toString(16)
+
+ break
+
+ case 'X':
+
+ arg = (parseInt(arg, 10) >>> 0).toString(16).toUpperCase()
+
+ break
+
+ }
+
+ if (re.json.test(ph.type)) {
+
+ output += arg
+
+ }
+
+ else {
+
+ if (re.number.test(ph.type) && (!is_positive || ph.sign)) {
+
+ sign = is_positive ? '+' : '-'
+
+ arg = arg.toString().replace(re.sign, '')
+
+ }
+
+ else {
+
+ sign = ''
+
+ }
+
+ pad_character = ph.pad_char ? ph.pad_char === '0' ? '0' : ph.pad_char.charAt(1) : ' '
+
+ pad_length = ph.width - (sign + arg).length
+
+ pad = ph.width ? (pad_length > 0 ? pad_character.repeat(pad_length) : '') : ''
+
+ output += ph.align ? sign + arg + pad : (pad_character === '0' ? sign + pad + arg : pad + sign + arg)
+
+ }
+
+ }
+
+ }
+
+ return output
+
+ }
+
+
+
+ var sprintf_cache = Object.create(null)
+
+
+
+ function sprintf_parse(fmt) {
+
+ if (sprintf_cache[fmt]) {
+
+ return sprintf_cache[fmt]
+
+ }
+
+
+
+ var _fmt = fmt, match, parse_tree = [], arg_names = 0
+
+ while (_fmt) {
+
+ if ((match = re.text.exec(_fmt)) !== null) {
+
+ parse_tree.push(match[0])
+
+ }
+
+ else if ((match = re.modulo.exec(_fmt)) !== null) {
+
+ parse_tree.push('%')
+
+ }
+
+ else if ((match = re.placeholder.exec(_fmt)) !== null) {
+
+ if (match[2]) {
+
+ arg_names |= 1
+
+ var field_list = [], replacement_field = match[2], field_match = []
+
+ if ((field_match = re.key.exec(replacement_field)) !== null) {
+
+ field_list.push(field_match[1])
+
+ while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
+
+ if ((field_match = re.key_access.exec(replacement_field)) !== null) {
+
+ field_list.push(field_match[1])
+
+ }
+
+ else if ((field_match = re.index_access.exec(replacement_field)) !== null) {
+
+ field_list.push(field_match[1])
+
+ }
+
+ else {
+
+ throw new SyntaxError('[sprintf] failed to parse named argument key')
+
+ }
+
+ }
+
+ }
+
+ else {
+
+ throw new SyntaxError('[sprintf] failed to parse named argument key')
+
+ }
+
+ match[2] = field_list
+
+ }
+
+ else {
+
+ arg_names |= 2
+
+ }
+
+ if (arg_names === 3) {
+
+ throw new Error('[sprintf] mixing positional and named placeholders is not (yet) supported')
+
+ }
+
+
+
+ parse_tree.push(
+
+ {
+
+ placeholder: match[0],
+
+ param_no: match[1],
+
+ keys: match[2],
+
+ sign: match[3],
+
+ pad_char: match[4],
+
+ align: match[5],
+
+ width: match[6],
+
+ precision: match[7],
+
+ type: match[8]
+
+ }
+
+ )
+
+ }
+
+ else {
+
+ throw new SyntaxError('[sprintf] unexpected placeholder')
+
+ }
+
+ _fmt = _fmt.substring(match[0].length)
+
+ }
+
+ return sprintf_cache[fmt] = parse_tree
+
+ }
+
+
+
+ /**
+
+ * export to either browser or node.js
+
+ */
+
+ /* eslint-disable quote-props */
+
+ if (typeof exports !== 'undefined') {
+
+ exports['sprintf'] = sprintf
+
+ exports['vsprintf'] = vsprintf
+
+ }
+
+ if (typeof window !== 'undefined') {
+
+ window['sprintf'] = sprintf
+
+ window['vsprintf'] = vsprintf
+
+
+
+ if (typeof define === 'function' && define['amd']) {
+
+ define(function() {
+
+ return {
+
+ 'sprintf': sprintf,
+
+ 'vsprintf': vsprintf
+
+ }
+
+ })
+
+ }
+
+ }
+
+ /* eslint-enable quote-props */
+
+ }(); // eslint-disable-line
+
+
+
@@ -2224,6 +2777,20 @@
+
+
+ ForEach widget
+
+ must have one HMI path given.
+
+
+
+
+ ForEach widget
+
+ must have one argument given : a class name.
+
+
@@ -2325,97 +2892,125 @@
class ForEachWidget extends Widget{
+
+
+ unsub_items(){
+
+ for(let item of this.items){
+
+ for(let widget of item) {
+
+ widget.unsub();
+
+ }
+
+ }
+
+ }
+
+
+
unsub(){
- for(let item of this.items){
+ this.unsub_items();
+
+ this.offset = 0;
+
+ this.relativeness = undefined;
+
+ }
+
+
+
+ sub_items(){
+
+ for(let i = 0; i < this.items.length; i++) {
+
+ let item = this.items[i];
+
+ let orig_item_index = this.index_pool[i];
+
+ let item_index = this.index_pool[i+this.item_offset];
+
+ let item_index_offset = item_index - orig_item_index;
+
+ if(this.relativeness[0])
+
+ item_index_offset += this.offset;
for(let widget of item) {
- widget.unsub();
+ /* all variables of all widgets in a ForEach are all relative.
+
+ Really.
+
+
+
+ TODO: allow absolute variables in ForEach widgets
+
+ */
+
+ widget.sub(item_index_offset, widget.indexes.map(_=>true));
}
}
- this.offset = 0;
-
}
- foreach_widgets_do(todo){
-
- for(let i = 0; i < this.items.length; i++) {
-
- let item = this.items[i];
-
- let orig_item_index = this.index_pool[i];
-
- let item_index = this.index_pool[i+this.item_offset];
-
- let item_index_offset = item_index - orig_item_index;
-
- for(let widget of item) {
-
- todo(widget).call(widget, this.offset + item_index_offset);
-
- }
+ sub(new_offset=0, relativeness=[]){
+
+ this.offset = new_offset;
+
+ this.relativeness = relativeness;
+
+ this.sub_items();
+
+ }
+
+
+
+ apply_cache() {
+
+ this.items.forEach(item=>item.forEach(widget=>widget.apply_cache()));
+
+ }
+
+
+
+ on_click(opstr, evt) {
+
+ let new_item_offset = eval(String(this.item_offset)+opstr);
+
+ if(new_item_offset + this.items.length > this.index_pool.length) {
+
+ if(this.item_offset + this.items.length == this.index_pool.length)
+
+ new_item_offset = 0;
+
+ else
+
+ new_item_offset = this.index_pool.length - this.items.length;
+
+ } else if(new_item_offset < 0) {
+
+ if(this.item_offset == 0)
+
+ new_item_offset = this.index_pool.length - this.items.length;
+
+ else
+
+ new_item_offset = 0;
}
- }
-
-
-
- sub(new_offset=0){
-
- this.offset = new_offset;
-
- this.foreach_widgets_do(w=>w.sub);
-
- }
-
-
-
- apply_cache() {
-
- this.foreach_widgets_do(w=>w.apply_cache);
-
- }
-
-
-
- on_click(opstr, evt) {
-
- let new_item_offset = eval(String(this.item_offset)+opstr);
-
- if(new_item_offset + this.items.length > this.index_pool.length) {
-
- if(this.item_offset + this.items.length == this.index_pool.length)
-
- new_item_offset = 0;
-
- else
-
- new_item_offset = this.index_pool.length - this.items.length;
-
- } else if(new_item_offset < 0) {
-
- if(this.item_offset == 0)
-
- new_item_offset = this.index_pool.length - this.items.length;
-
- else
-
- new_item_offset = 0;
-
- }
-
this.item_offset = new_item_offset;
- this.unsub();
-
- this.sub(this.offset);
+ this.unsub_items();
+
+ this.sub_items();
update_subscriptions();
@@ -3917,655 +4512,613 @@
- function dispatch_value_to_widget(widget, index, value, oldval) {
+
+
+ function dispatch_value(index, value) {
+
+ let widgets = subscribers[index];
+
+
+
+ let oldval = cache[index];
+
+ cache[index] = value;
+
+
+
+ if(widgets.size > 0) {
+
+ for(let widget of widgets){
+
+ widget.new_hmi_value(index, value, oldval);
+
+ }
+
+ }
+
+ };
+
+
+
+ function init_widgets() {
+
+ Object.keys(hmi_widgets).forEach(function(id) {
+
+ let widget = hmi_widgets[id];
+
+ let init = widget.init;
+
+ if(typeof(init) == "function"){
+
+ try {
+
+ init.call(widget);
+
+ } catch(err) {
+
+ console.log(err);
+
+ }
+
+ }
+
+ });
+
+ };
+
+
+
+ // Open WebSocket to relative "/ws" address
+
+ var ws = new WebSocket(window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws'));
+
+ 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],
+
+ STRING: (dv, offset) => {
+
+ 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() {
+
+ for(let index in updates){
+
+ // serving as a key, index becomes a string
+
+ // -> pass Number(index) instead
+
+ dispatch_value(Number(index), updates[index]);
+
+ delete updates[index];
+
+ }
+
+ }
+
+
+
+ // 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();
+
+ 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 {
- let idx = widget.offset ? index - widget.offset : index;
-
- let idxidx = widget.indexes.indexOf(idx);
-
- let d = widget.dispatch;
-
- if(typeof(d) == "function" && idxidx == 0){
-
- d.call(widget, value, oldval);
+ 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[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);
+
+ }
+
+ };
+
+
+
+
+
+ function send_blob(data) {
+
+ if(data.length > 0) {
+
+ ws.send(new Blob([new Uint8Array(hmi_hash)].concat(data)));
+
+ };
+
+ };
+
+
+
+ const typedarray_types = {
+
+ INT: (number) => new Int16Array([number]),
+
+ BOOL: (truth) => new Int16Array([truth]),
+
+ NODE: (truth) => new Int16Array([truth]),
+
+ 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(var i = 0; i < str.length; i++){
+
+ binary[i+1] = str.charCodeAt(i);
}
- else if(typeof(d) == "object" && d.length >= idxidx){
-
- d[idxidx].call(widget, value, oldval);
+ return binary;
+
+ }
+
+ /* TODO */
+
+ };
+
+
+
+ function send_reset() {
+
+ send_blob(new Uint8Array([1])); /* reset = 1 */
+
+ };
+
+
+
+ // subscription state, as it should be in hmi server
+
+ // hmitree indexed array of integers
+
+ var subscriptions = hmitree_types.map(_ignored => 0);
+
+
+
+ // subscription state as needed by widget now
+
+ // hmitree indexed array of Sets of widgets objects
+
+ var subscribers = hmitree_types.map(_ignored => new Set());
+
+
+
+ // 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);
+
+ }
+
+ });
+
+
+
+ function update_subscriptions() {
+
+ let delta = [];
+
+ for(let index = 0; index < subscribers.length; index++){
+
+ let widgets = subscribers[index];
+
+
+
+ // periods are in ms
+
+ let previous_period = subscriptions[index];
+
+
+
+ // subscribing with a zero period is unsubscribing
+
+ let new_period = 0;
+
+ if(widgets.size > 0) {
+
+ let maxfreq = 0;
+
+ for(let widget of widgets){
+
+ let wf = widget.frequency;
+
+ if(wf != undefined && maxfreq < wf)
+
+ maxfreq = wf;
+
+ }
+
+
+
+ if(maxfreq != 0)
+
+ new_period = 1000/maxfreq;
}
- /* else dispatch_0, ..., dispatch_n ? */
-
- /*else {
-
- throw new Error("Dunno how to dispatch to widget at index = " + index);
-
- }*/
-
- } catch(err) {
-
- console.log(err);
+
+
+ if(previous_period != new_period) {
+
+ subscriptions[index] = new_period;
+
+ delta.push(
+
+ new Uint8Array([2]), /* subscribe = 2 */
+
+ new Uint32Array([index]),
+
+ new Uint16Array([new_period]));
+
+ }
}
+ send_blob(delta);
+
+ };
+
+
+
+ function send_hmi_value(index, value) {
+
+ let iectype = hmitree_types[index];
+
+ let tobinary = typedarray_types[iectype];
+
+ send_blob([
+
+ new Uint8Array([0]), /* setval = 0 */
+
+ new Uint32Array([index]),
+
+ tobinary(value)]);
+
+
+
+ // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf
+
+ // cache[index] = value;
+
+ };
+
+
+
+ function apply_hmi_value(index, new_val) {
+
+ let old_val = cache[index]
+
+ if(new_val != undefined && old_val != new_val)
+
+ send_hmi_value(index, new_val);
+
+ return new_val;
+
}
- function dispatch_value(index, value) {
-
- let widgets = subscribers[index];
-
-
-
- let oldval = cache[index];
-
- cache[index] = value;
-
-
-
- if(widgets.size > 0) {
-
- for(let widget of widgets){
-
- dispatch_value_to_widget(widget, index, value, oldval);
+ quotes = {"'":null, '"':null};
+
+
+
+ function change_hmi_value(index, opstr) {
+
+ let op = opstr[0];
+
+ let given_val;
+
+ if(opstr.length < 2)
+
+ return undefined; // TODO raise
+
+ if(opstr[1] in quotes){
+
+ if(opstr.length < 3)
+
+ return undefined; // TODO raise
+
+ if(opstr[opstr.length-1] == opstr[1]){
+
+ given_val = opstr.slice(2,opstr.length-1);
}
+ } else {
+
+ given_val = Number(opstr.slice(1));
+
}
+ let old_val = cache[index];
+
+ let new_val;
+
+ switch(op){
+
+ case "=":
+
+ new_val = given_val;
+
+ break;
+
+ case "+":
+
+ new_val = old_val + given_val;
+
+ break;
+
+ case "-":
+
+ new_val = old_val - given_val;
+
+ break;
+
+ case "*":
+
+ new_val = old_val * given_val;
+
+ break;
+
+ case "/":
+
+ new_val = old_val / given_val;
+
+ break;
+
+ }
+
+ if(new_val != undefined && old_val != new_val)
+
+ send_hmi_value(index, new_val);
+
+ // TODO else raise
+
+ return new_val;
+
+ }
+
+
+
+ var current_visible_page;
+
+ var current_subscribed_page;
+
+ var current_page_index;
+
+
+
+ function prepare_svg() {
+
+ for(let eltid in detachable_elements){
+
+ let [element,parent] = detachable_elements[eltid];
+
+ parent.removeChild(element);
+
+ }
+
};
- function init_widgets() {
-
- Object.keys(hmi_widgets).forEach(function(id) {
-
- let widget = hmi_widgets[id];
-
- let init = widget.init;
-
- if(typeof(init) == "function"){
-
- try {
-
- init.call(widget);
-
- } catch(err) {
-
- console.log(err);
-
- }
-
- }
-
- });
-
- };
-
-
-
- // Open WebSocket to relative "/ws" address
-
- var ws = new WebSocket(window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws'));
-
- 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],
-
- STRING: (dv, offset) => {
-
- 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 */
+ function switch_page(page_name, page_index) {
+
+ if(current_subscribed_page != current_visible_page){
+
+ /* page switch already going */
+
+ /* TODO LOG ERROR */
+
+ return false;
}
- };
-
-
-
- // Apply updates recieved through ws.onmessage to subscribed widgets
-
- function apply_updates() {
-
- for(let index in updates){
-
- // serving as a key, index becomes a string
-
- // -> pass Number(index) instead
-
- dispatch_value(Number(index), updates[index]);
-
- delete updates[index];
+
+
+ if(page_name == undefined)
+
+ page_name = current_subscribed_page;
+
+
+
+
+
+ let old_desc = page_desc[current_subscribed_page];
+
+ let new_desc = page_desc[page_name];
+
+
+
+ if(new_desc == undefined){
+
+ /* TODO LOG ERROR */
+
+ return false;
}
- }
-
-
-
- // 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);
+
+
+ if(page_index == undefined){
+
+ page_index = new_desc.page_index;
}
- while(widget = need_cache_apply.pop()){
-
- widget.apply_cache();
+ if(old_desc){
+
+ old_desc.widgets.map(([widget,relativeness])=>widget.unsub());
}
-
-
- if(jumps_need_update) update_jumps();
-
-
-
- apply_updates();
-
- 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[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);
-
- }
-
- };
-
-
-
-
-
- function send_blob(data) {
-
- if(data.length > 0) {
-
- ws.send(new Blob([new Uint8Array(hmi_hash)].concat(data)));
-
- };
-
- };
-
-
-
- const typedarray_types = {
-
- INT: (number) => new Int16Array([number]),
-
- BOOL: (truth) => new Int16Array([truth]),
-
- NODE: (truth) => new Int16Array([truth]),
-
- 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(var 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 */
-
- };
-
-
-
- // subscription state, as it should be in hmi server
-
- // hmitree indexed array of integers
-
- var subscriptions = hmitree_types.map(_ignored => 0);
-
-
-
- // subscription state as needed by widget now
-
- // hmitree indexed array of Sets of widgets objects
-
- var subscribers = hmitree_types.map(_ignored => new Set());
-
-
-
- // 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],
-
- dispatch: function(value) {
-
- apply_hmi_value(heartbeat_index, value+1);
-
- }
-
- });
-
-
-
- function update_subscriptions() {
-
- let delta = [];
-
- for(let index = 0; index < subscribers.length; index++){
-
- let widgets = subscribers[index];
-
-
-
- // periods are in ms
-
- let previous_period = subscriptions[index];
-
-
-
- // subscribing with a zero period is unsubscribing
-
- let new_period = 0;
-
- if(widgets.size > 0) {
-
- let maxfreq = 0;
-
- for(let widget of widgets){
-
- let wf = widget.frequency;
-
- if(wf != undefined && maxfreq < wf)
-
- maxfreq = wf;
-
- }
-
-
-
- if(maxfreq != 0)
-
- new_period = 1000/maxfreq;
-
- }
-
-
-
- if(previous_period != new_period) {
-
- subscriptions[index] = new_period;
-
- delta.push(
-
- new Uint8Array([2]), /* subscribe = 2 */
-
- new Uint32Array([index]),
-
- new Uint16Array([new_period]));
-
- }
-
- }
-
- send_blob(delta);
-
- };
-
-
-
- function send_hmi_value(index, value) {
-
- let iectype = hmitree_types[index];
-
- let tobinary = typedarray_types[iectype];
-
- send_blob([
-
- new Uint8Array([0]), /* setval = 0 */
-
- new Uint32Array([index]),
-
- tobinary(value)]);
-
-
-
- // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf
-
- // cache[index] = value;
-
- };
-
-
-
- function apply_hmi_value(index, new_val) {
-
- let old_val = cache[index]
-
- if(new_val != undefined && old_val != new_val)
-
- send_hmi_value(index, new_val);
-
- return new_val;
-
- }
-
-
-
- quotes = {"'":null, '"':null};
-
-
-
- function change_hmi_value(index, opstr) {
-
- let op = opstr[0];
-
- let given_val;
-
- if(opstr.length < 2)
-
- return undefined; // TODO raise
-
- if(opstr[1] in quotes){
-
- if(opstr.length < 3)
-
- return undefined; // TODO raise
-
- if(opstr[opstr.length-1] == opstr[1]){
-
- given_val = opstr.slice(2,opstr.length-1);
-
- }
-
- } else {
-
- given_val = Number(opstr.slice(1));
-
- }
-
- let old_val = cache[index];
-
- let new_val;
-
- switch(op){
-
- case "=":
-
- new_val = given_val;
-
- break;
-
- case "+":
-
- new_val = old_val + given_val;
-
- break;
-
- case "-":
-
- new_val = old_val - given_val;
-
- break;
-
- case "*":
-
- new_val = old_val * given_val;
-
- break;
-
- case "/":
-
- new_val = old_val / given_val;
-
- break;
-
- }
-
- if(new_val != undefined && old_val != new_val)
-
- send_hmi_value(index, new_val);
-
- // TODO else raise
-
- return new_val;
-
- }
-
-
-
- var current_visible_page;
-
- var current_subscribed_page;
-
- var current_page_index;
-
-
-
- function prepare_svg() {
-
- for(let eltid in detachable_elements){
-
- let [element,parent] = detachable_elements[eltid];
-
- parent.removeChild(element);
-
- }
-
- };
-
-
-
- function switch_page(page_name, page_index) {
-
- if(current_subscribed_page != current_visible_page){
-
- /* page switch already going */
-
- /* TODO LOG ERROR */
-
- return false;
-
- }
-
-
-
- if(page_name == undefined)
-
- page_name = current_subscribed_page;
-
-
-
-
-
- let old_desc = page_desc[current_subscribed_page];
-
- let new_desc = page_desc[page_name];
-
-
-
- if(new_desc == undefined){
-
- /* TODO LOG ERROR */
-
- return false;
-
- }
-
-
-
- if(page_index == undefined){
-
- page_index = new_desc.page_index;
-
- }
-
-
-
- if(old_desc){
-
- old_desc.absolute_widgets.map(w=>w.unsub());
-
- old_desc.relative_widgets.map(w=>w.unsub());
-
- }
-
- new_desc.absolute_widgets.map(w=>w.sub());
-
var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index;
- new_desc.relative_widgets.map(w=>w.sub(new_offset));
+ new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness));
diff -r 085a678715d0 -r 22b969b409b0 svghmi/svghmi.js
--- a/svghmi/svghmi.js Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/svghmi.js Mon Aug 10 11:30:06 2020 +0200
@@ -4,25 +4,6 @@
var updates = {};
var need_cache_apply = [];
-function dispatch_value_to_widget(widget, index, value, oldval) {
- try {
- let idx = widget.offset ? index - widget.offset : index;
- let idxidx = widget.indexes.indexOf(idx);
- let d = widget.dispatch;
- if(typeof(d) == "function" && idxidx == 0){
- d.call(widget, value, oldval);
- }
- else if(typeof(d) == "object" && d.length >= idxidx){
- d[idxidx].call(widget, value, oldval);
- }
- /* else dispatch_0, ..., dispatch_n ? */
- /*else {
- throw new Error("Dunno how to dispatch to widget at index = " + index);
- }*/
- } catch(err) {
- console.log(err);
- }
-}
function dispatch_value(index, value) {
let widgets = subscribers[index];
@@ -32,7 +13,7 @@
if(widgets.size > 0) {
for(let widget of widgets){
- dispatch_value_to_widget(widget, index, value, oldval);
+ widget.new_hmi_value(index, value, oldval);
}
}
};
@@ -190,7 +171,7 @@
/* type: "Watchdog", */
frequency: 1,
indexes: [heartbeat_index],
- dispatch: function(value) {
+ new_hmi_value: function(index, value, oldval) {
apply_hmi_value(heartbeat_index, value+1);
}
});
@@ -323,12 +304,10 @@
}
if(old_desc){
- old_desc.absolute_widgets.map(w=>w.unsub());
- old_desc.relative_widgets.map(w=>w.unsub());
- }
- new_desc.absolute_widgets.map(w=>w.sub());
+ old_desc.widgets.map(([widget,relativeness])=>widget.unsub());
+ }
var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index;
- new_desc.relative_widgets.map(w=>w.sub(new_offset));
+ new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness));
update_subscriptions();
diff -r 085a678715d0 -r 22b969b409b0 svghmi/widget_button.ysl2
--- a/svghmi/widget_button.ysl2 Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_button.ysl2 Mon Aug 10 11:30:06 2020 +0200
@@ -13,7 +13,7 @@
this.active_elt.setAttribute("style", this.active_style);
this.inactive_elt.setAttribute("style", "display:none");
}
- change_hmi_value(this.indexes[0], "=1");
+ this.apply_hmi_value(0, 1);
}
on_mouse_up(evt) {
@@ -21,7 +21,7 @@
this.active_elt.setAttribute("style", "display:none");
this.inactive_elt.setAttribute("style", this.inactive_style);
}
- change_hmi_value(this.indexes[0], "=0");
+ this.apply_hmi_value(0, 0);
}
init() {
@@ -33,8 +33,8 @@
this.inactive_elt.setAttribute("style", this.inactive_style);
}
- this.element.setAttribute("onmousedown", "hmi_widgets['«$hmi_element/@id»'].on_mouse_down(evt)");
- this.element.setAttribute("onmouseup", "hmi_widgets['«$hmi_element/@id»'].on_mouse_up(evt)");
+ this.element.setAttribute("onmousedown", "hmi_widgets["+this.element_id+"].on_mouse_down(evt)");
+ this.element.setAttribute("onmouseup", "hmi_widgets["+this.element_id+"].on_mouse_up(evt)");
}
}
||
diff -r 085a678715d0 -r 22b969b409b0 svghmi/widget_circularslider.ysl2
--- a/svghmi/widget_circularslider.ysl2 Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_circularslider.ysl2 Mon Aug 10 11:30:06 2020 +0200
@@ -89,7 +89,7 @@
this.handle_position(svg_dist);
if(this.value_elt)
this.value_elt.textContent = String(Math.ceil(svg_dist));
- change_hmi_value(this.indexes[0], "="+Math.ceil(svg_dist));
+ this.apply_hmi_value(0, Math.ceil(svg_dist));
//reset timer
this.enTimer = false;
@@ -156,4 +156,4 @@
labels("handle range");
optional_labels("value min max");
|,
-}
\ No newline at end of file
+}
diff -r 085a678715d0 -r 22b969b409b0 svghmi/widget_display.ysl2
--- a/svghmi/widget_display.ysl2 Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_display.ysl2 Mon Aug 10 11:30:06 2020 +0200
@@ -5,8 +5,10 @@
||
class DisplayWidget extends Widget{
frequency = 5;
- dispatch(value) {
- this.element.textContent = String(value);
+ dispatch(value, oldval, index) {
+ this.fields[index] = value;
+ console.log(value, index);
+ this.element.textContent = this.args.length == 1 ? vsprintf(this.args[0],this.fields) : this.fields.join(' ');
}
}
||
@@ -15,4 +17,242 @@
param "hmi_element";
if "$hmi_element[not(self::svg:text)]"
error > Display Widget id="«$hmi_element/@id»" is not a svg::text element
+
+ | fields: [],
}
+
+emit "preamble:display"
+||
+/* https://github.com/alexei/sprintf.js/blob/master/src/sprintf.js */
+/* global window, exports, define */
+
+!function() {
+ 'use strict'
+
+ var re = {
+ not_string: /[^s]/,
+ not_bool: /[^t]/,
+ not_type: /[^T]/,
+ not_primitive: /[^v]/,
+ number: /[diefg]/,
+ numeric_arg: /[bcdiefguxX]/,
+ json: /[j]/,
+ not_json: /[^j]/,
+ text: /^[^\x25]+/,
+ modulo: /^\x25{2}/,
+ placeholder: /^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,
+ key: /^([a-z_][a-z_\d]*)/i,
+ key_access: /^\.([a-z_][a-z_\d]*)/i,
+ index_access: /^\[(\d+)\]/,
+ sign: /^[+-]/
+ }
+
+ function sprintf(key) {
+ // `arguments` is not an array, but should be fine for this call
+ return sprintf_format(sprintf_parse(key), arguments)
+ }
+
+ function vsprintf(fmt, argv) {
+ return sprintf.apply(null, [fmt].concat(argv || []))
+ }
+
+ function sprintf_format(parse_tree, argv) {
+ var cursor = 1, tree_length = parse_tree.length, arg, output = '', i, k, ph, pad, pad_character, pad_length, is_positive, sign
+ for (i = 0; i < tree_length; i++) {
+ if (typeof parse_tree[i] === 'string') {
+ output += parse_tree[i]
+ }
+ else if (typeof parse_tree[i] === 'object') {
+ ph = parse_tree[i] // convenience purposes only
+ if (ph.keys) { // keyword argument
+ arg = argv[cursor]
+ for (k = 0; k < ph.keys.length; k++) {
+ if (arg == undefined) {
+ throw new Error(sprintf('[sprintf] Cannot access property "%s" of undefined value "%s"', ph.keys[k], ph.keys[k-1]))
+ }
+ arg = arg[ph.keys[k]]
+ }
+ }
+ else if (ph.param_no) { // positional argument (explicit)
+ arg = argv[ph.param_no]
+ }
+ else { // positional argument (implicit)
+ arg = argv[cursor++]
+ }
+
+ if (re.not_type.test(ph.type) && re.not_primitive.test(ph.type) && arg instanceof Function) {
+ arg = arg()
+ }
+
+ if (re.numeric_arg.test(ph.type) && (typeof arg !== 'number' && isNaN(arg))) {
+ throw new TypeError(sprintf('[sprintf] expecting number but found %T', arg))
+ }
+
+ if (re.number.test(ph.type)) {
+ is_positive = arg >= 0
+ }
+
+ switch (ph.type) {
+ case 'b':
+ arg = parseInt(arg, 10).toString(2)
+ break
+ case 'c':
+ arg = String.fromCharCode(parseInt(arg, 10))
+ break
+ case 'd':
+ case 'i':
+ arg = parseInt(arg, 10)
+ break
+ case 'j':
+ arg = JSON.stringify(arg, null, ph.width ? parseInt(ph.width) : 0)
+ break
+ case 'e':
+ arg = ph.precision ? parseFloat(arg).toExponential(ph.precision) : parseFloat(arg).toExponential()
+ break
+ case 'f':
+ arg = ph.precision ? parseFloat(arg).toFixed(ph.precision) : parseFloat(arg)
+ break
+ case 'g':
+ arg = ph.precision ? String(Number(arg.toPrecision(ph.precision))) : parseFloat(arg)
+ break
+ case 'o':
+ arg = (parseInt(arg, 10) >>> 0).toString(8)
+ break
+ case 's':
+ arg = String(arg)
+ arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+ break
+ case 't':
+ arg = String(!!arg)
+ arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+ break
+ case 'T':
+ arg = Object.prototype.toString.call(arg).slice(8, -1).toLowerCase()
+ arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+ break
+ case 'u':
+ arg = parseInt(arg, 10) >>> 0
+ break
+ case 'v':
+ arg = arg.valueOf()
+ arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+ break
+ case 'x':
+ arg = (parseInt(arg, 10) >>> 0).toString(16)
+ break
+ case 'X':
+ arg = (parseInt(arg, 10) >>> 0).toString(16).toUpperCase()
+ break
+ }
+ if (re.json.test(ph.type)) {
+ output += arg
+ }
+ else {
+ if (re.number.test(ph.type) && (!is_positive || ph.sign)) {
+ sign = is_positive ? '+' : '-'
+ arg = arg.toString().replace(re.sign, '')
+ }
+ else {
+ sign = ''
+ }
+ pad_character = ph.pad_char ? ph.pad_char === '0' ? '0' : ph.pad_char.charAt(1) : ' '
+ pad_length = ph.width - (sign + arg).length
+ pad = ph.width ? (pad_length > 0 ? pad_character.repeat(pad_length) : '') : ''
+ output += ph.align ? sign + arg + pad : (pad_character === '0' ? sign + pad + arg : pad + sign + arg)
+ }
+ }
+ }
+ return output
+ }
+
+ var sprintf_cache = Object.create(null)
+
+ function sprintf_parse(fmt) {
+ if (sprintf_cache[fmt]) {
+ return sprintf_cache[fmt]
+ }
+
+ var _fmt = fmt, match, parse_tree = [], arg_names = 0
+ while (_fmt) {
+ if ((match = re.text.exec(_fmt)) !== null) {
+ parse_tree.push(match[0])
+ }
+ else if ((match = re.modulo.exec(_fmt)) !== null) {
+ parse_tree.push('%')
+ }
+ else if ((match = re.placeholder.exec(_fmt)) !== null) {
+ if (match[2]) {
+ arg_names |= 1
+ var field_list = [], replacement_field = match[2], field_match = []
+ if ((field_match = re.key.exec(replacement_field)) !== null) {
+ field_list.push(field_match[1])
+ while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
+ if ((field_match = re.key_access.exec(replacement_field)) !== null) {
+ field_list.push(field_match[1])
+ }
+ else if ((field_match = re.index_access.exec(replacement_field)) !== null) {
+ field_list.push(field_match[1])
+ }
+ else {
+ throw new SyntaxError('[sprintf] failed to parse named argument key')
+ }
+ }
+ }
+ else {
+ throw new SyntaxError('[sprintf] failed to parse named argument key')
+ }
+ match[2] = field_list
+ }
+ else {
+ arg_names |= 2
+ }
+ if (arg_names === 3) {
+ throw new Error('[sprintf] mixing positional and named placeholders is not (yet) supported')
+ }
+
+ parse_tree.push(
+ {
+ placeholder: match[0],
+ param_no: match[1],
+ keys: match[2],
+ sign: match[3],
+ pad_char: match[4],
+ align: match[5],
+ width: match[6],
+ precision: match[7],
+ type: match[8]
+ }
+ )
+ }
+ else {
+ throw new SyntaxError('[sprintf] unexpected placeholder')
+ }
+ _fmt = _fmt.substring(match[0].length)
+ }
+ return sprintf_cache[fmt] = parse_tree
+ }
+
+ /**
+ * export to either browser or node.js
+ */
+ /* eslint-disable quote-props */
+ if (typeof exports !== 'undefined') {
+ exports['sprintf'] = sprintf
+ exports['vsprintf'] = vsprintf
+ }
+ if (typeof window !== 'undefined') {
+ window['sprintf'] = sprintf
+ window['vsprintf'] = vsprintf
+
+ if (typeof define === 'function' && define['amd']) {
+ define(function() {
+ return {
+ 'sprintf': sprintf,
+ 'vsprintf': vsprintf
+ }
+ })
+ }
+ }
+ /* eslint-enable quote-props */
+}(); // eslint-disable-line
+||
diff -r 085a678715d0 -r 22b969b409b0 svghmi/widget_foreach.ysl2
--- a/svghmi/widget_foreach.ysl2 Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_foreach.ysl2 Mon Aug 10 11:30:06 2020 +0200
@@ -2,6 +2,9 @@
template "widget[@type='ForEach']", mode="widget_defs" {
param "hmi_element";
+ if "count(path) != 1" error > ForEach widget «$hmi_element/@id» must have one HMI path given.
+ if "count(arg) != 1" error > ForEach widget «$hmi_element/@id» must have one argument given : a class name.
+
const "class","arg[1]/@value";
const "base_path","path/@value";
@@ -49,34 +52,48 @@
template "widget[@type='ForEach']", mode="widget_class"
||
class ForEachWidget extends Widget{
- unsub(){
+
+ unsub_items(){
for(let item of this.items){
for(let widget of item) {
widget.unsub();
}
}
- this.offset = 0;
}
- foreach_widgets_do(todo){
+ unsub(){
+ this.unsub_items();
+ this.offset = 0;
+ this.relativeness = undefined;
+ }
+
+ sub_items(){
for(let i = 0; i < this.items.length; i++) {
let item = this.items[i];
let orig_item_index = this.index_pool[i];
let item_index = this.index_pool[i+this.item_offset];
let item_index_offset = item_index - orig_item_index;
+ if(this.relativeness[0])
+ item_index_offset += this.offset;
for(let widget of item) {
- todo(widget).call(widget, this.offset + item_index_offset);
+ /* all variables of all widgets in a ForEach are all relative.
+ Really.
+
+ TODO: allow absolute variables in ForEach widgets
+ */
+ widget.sub(item_index_offset, widget.indexes.map(_=>true));
}
}
}
- sub(new_offset=0){
+ sub(new_offset=0, relativeness=[]){
this.offset = new_offset;
- this.foreach_widgets_do(w=>w.sub);
+ this.relativeness = relativeness;
+ this.sub_items();
}
apply_cache() {
- this.foreach_widgets_do(w=>w.apply_cache);
+ this.items.forEach(item=>item.forEach(widget=>widget.apply_cache()));
}
on_click(opstr, evt) {
@@ -93,8 +110,8 @@
new_item_offset = 0;
}
this.item_offset = new_item_offset;
- this.unsub();
- this.sub(this.offset);
+ this.unsub_items();
+ this.sub_items();
update_subscriptions();
need_cache_apply.push(this);
jumps_need_update = true;
diff -r 085a678715d0 -r 22b969b409b0 svghmi/widget_multistate.ysl2
--- a/svghmi/widget_multistate.ysl2 Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_multistate.ysl2 Mon Aug 10 11:30:06 2020 +0200
@@ -35,7 +35,7 @@
}
//post value to plc
- change_hmi_value(this.indexes[0], "="+this.state);
+ this.apply_hmi_value(0, this.state);
}
init() {
@@ -57,4 +57,4 @@
| }`if "position()!=last()" > ,`
}
| ],
-}
\ No newline at end of file
+}
diff -r 085a678715d0 -r 22b969b409b0 svghmi/widget_slider.ysl2
--- a/svghmi/widget_slider.ysl2 Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_slider.ysl2 Mon Aug 10 11:30:06 2020 +0200
@@ -80,7 +80,7 @@
//redraw handle
this.handle_position(svg_dist=(html_dist/range_length)*this.range[1]);
this.value_elt.textContent = String(Math.ceil(svg_dist));
- change_hmi_value(this.indexes[0], "="+Math.ceil(svg_dist));
+ this.apply_hmi_value(0, Math.ceil(svg_dist));
//reset timer
this.enTimer = false;
setTimeout("{hmi_widgets['"+this.element_id+"'].enTimer = true;}", 100);
@@ -128,4 +128,4 @@
labels("handle range");
optional_labels("value min max");
|,
-}
\ No newline at end of file
+}
diff -r 085a678715d0 -r 22b969b409b0 svghmi/widget_tooglebutton.ysl2
--- a/svghmi/widget_tooglebutton.ysl2 Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_tooglebutton.ysl2 Mon Aug 10 11:30:06 2020 +0200
@@ -23,7 +23,7 @@
}
on_click(evt) {
- change_hmi_value(this.indexes[0], "="+this.state);
+ this.apply_hmi_value(0, this.state);
}
init() {
diff -r 085a678715d0 -r 22b969b409b0 svghmi/widgets_common.ysl2
--- a/svghmi/widgets_common.ysl2 Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widgets_common.ysl2 Mon Aug 10 11:30:06 2020 +0200
@@ -78,20 +78,28 @@
unsub(){
/* remove subsribers */
- if(!this.unsubscribable) for(let index of this.indexes){
- let idx = index + this.offset;
- subscribers[idx].delete(this);
- }
+ if(!this.unsubscribable)
+ for(let i = 0; i < this.indexes.length; i++) {
+ let index = this.indexes[i];
+ if(this.relativeness[i])
+ index += this.offset;
+ subscribers[index].delete(this);
+ }
this.offset = 0;
- }
-
- sub(new_offset=0){
- /* set the offset because relative */
+ this.relativeness = undefined;
+ }
+
+ sub(new_offset=0, relativeness){
this.offset = new_offset;
+ this.relativeness = relativeness;
/* add this's subsribers */
- if(!this.unsubscribable) for(let index of this.indexes){
- subscribers[index + new_offset].add(this);
- }
+ if(!this.unsubscribable)
+ for(let i = 0; i < this.indexes.length; i++) {
+ let index = this.indexes[i];
+ if(relativeness[i])
+ index += new_offset;
+ subscribers[index].add(this);
+ }
need_cache_apply.push(this);
}
@@ -101,13 +109,13 @@
let realindex = index+this.offset;
let cached_val = cache[realindex];
if(cached_val != undefined)
- dispatch_value_to_widget(this, realindex, cached_val, cached_val);
+ this.new_hmi_value(realindex, cached_val, cached_val);
}
}
get_idx(index) {
let orig = this.indexes[index];
- return this.offset ? orig + this.offset : orig;
+ return this.relativeness[index] ? orig + this.offset : orig;
}
change_hmi_value(index,opstr) {
return change_hmi_value(this.get_idx(index), opstr);
@@ -116,11 +124,39 @@
apply_hmi_value(index, new_val) {
return apply_hmi_value(this.get_idx(0), new_val);
}
+
+ new_hmi_value(index, value, oldval) {
+ try {
+ // TODO avoid searching, store index at sub()
+ for(let i = 0; i < this.indexes.length; i++) {
+ let refindex = this.indexes[i];
+ if(this.relativeness[i])
+ refindex += this.offset;
+
+ if(index == refindex) {
+ let d = this.dispatch;
+ if(typeof(d) == "function"){
+ d.call(this, value, oldval, i);
+ }
+ else if(typeof(d) == "object"){
+ d[i].call(this, value, oldval);
+ }
+ /* else dispatch_0, ..., dispatch_n ? */
+ /*else {
+ throw new Error("Dunno how to dispatch to widget at index = " + index);
+ }*/
+ break;
+ }
+ }
+ } catch(err) {
+ console.log(err);
+ }
+ }
}
||
}
-emit "preamble:hmi-classes" {
+emit "declarations:hmi-classes" {
const "used_widget_types", "func:unique_types($parsed_widgets/widget)";
apply "$used_widget_types", mode="widget_class";
}
@@ -135,7 +171,7 @@
const "excluded_types", "str:split('Page Lang')";
const "excluded_ids","$parsed_widgets/widget[not(@type = $excluded_types)]/@id";
-emit "preamble:hmi-elements" {
+emit "declarations:hmi-elements" {
| var hmi_widgets = {
apply "$hmi_elements[@id = $excluded_ids]", mode="hmi_widgets";
| }
diff -r 085a678715d0 -r 22b969b409b0 tests/svghmi/svghmi_0@svghmi/svghmi.svg
--- a/tests/svghmi/svghmi_0@svghmi/svghmi.svg Wed Aug 05 15:20:10 2020 +0200
+++ b/tests/svghmi/svghmi_0@svghmi/svghmi.svg Mon Aug 10 11:30:06 2020 +0200
@@ -167,16 +167,16 @@
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:document-units="px"
- inkscape:current-layer="g6077"
+ inkscape:current-layer="hmi0"
showgrid="false"
units="px"
- inkscape:zoom="1.4142136"
- inkscape:cx="1970.3359"
- inkscape:cy="368.15797"
+ inkscape:zoom="0.7071068"
+ inkscape:cx="543.82641"
+ inkscape:cy="218.7845"
inkscape:window-width="2419"
inkscape:window-height="1266"
- inkscape:window-x="1197"
- inkscape:window-y="563"
+ inkscape:window-x="1405"
+ inkscape:window-y="37"
inkscape:window-maximized="0"
showguides="true"
inkscape:guide-bbox="true" />
@@ -4200,4 +4200,28 @@
sodipodi:role="line">-1
+ 8888
+ Multiple variables