# HG changeset patch # User Edouard Tisserant <edouard.tisserant@gmail.com> # Date 1596646169 -7200 # Node ID ff9ae4f4e3be517bd997ca09a356af977517b562 # Parent 705e34c6fe93c813fa027a9261e8e4b61cbd5cb7 SVGHMI: widgets are not anymore binary relative or absolute, but have a "relativeness". Because of allowing multiple variables per widget, we must distinguish if individual variables is relative to page, not the whole widget. diff -r 705e34c6fe93 -r ff9ae4f4e3be svghmi/detachable_pages.ysl2 --- a/svghmi/detachable_pages.ysl2 Tue Aug 04 11:53:39 2020 +0200 +++ b/svghmi/detachable_pages.ysl2 Wed Aug 05 18:49:29 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: [ diff -r 705e34c6fe93 -r ff9ae4f4e3be svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Tue Aug 04 11:53:39 2020 +0200 +++ b/svghmi/gen_index_xhtml.xslt Wed Aug 05 18:49:29 2020 +0200 @@ -497,26 +497,22 @@ <xsl:text>, </xsl:text> </xsl:if> - <xsl:text> relative_widgets: [ -</xsl:text> - <xsl:for-each select="$page_relative_widgets"> - <xsl:text> hmi_widgets["</xsl:text> + <xsl:text> widgets: [ +</xsl:text> + <xsl:for-each select="$page_managed_widgets"> + <xsl:variable name="widget_paths_relativeness"> + <xsl:for-each select="func:widget(@id)/path"> + <xsl:value-of select="func:is_descendant_path(@value, $desc/path/@value)"/> + <xsl:if test="position()!=last()"> + <xsl:text>,</xsl:text> + </xsl:if> + </xsl:for-each> + </xsl:variable> + <xsl:text> [hmi_widgets["</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> absolute_widgets: [ -</xsl:text> - <xsl:for-each select="$page_managed_widgets[not(@id = $page_relative_widgets/@id)]"> - <xsl:text> hmi_widgets["</xsl:text> - <xsl:value-of select="@id"/> - <xsl:text>"]</xsl:text> + <xsl:text>"], [</xsl:text> + <xsl:value-of select="$widget_paths_relativeness"/> + <xsl:text>]]</xsl:text> <xsl:if test="position()!=last()"> <xsl:text>,</xsl:text> </xsl:if> @@ -947,80 +943,152 @@ </xsl:text> <xsl:text> /* remove subsribers */ </xsl:text> + <xsl:text> if(!this.unsubscribable) +</xsl:text> + <xsl:text> for(let i = 0; i < this.indexes.length; i++) { +</xsl:text> + <xsl:text> let index = this.indexes[i]; +</xsl:text> + <xsl:text> if(this.relativeness[i]) +</xsl:text> + <xsl:text> index += this.offset; +</xsl:text> + <xsl:text> subscribers[index].delete(this); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> this.offset = 0; +</xsl:text> + <xsl:text> this.relativeness = undefined; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> sub(new_offset=0, relativeness){ +</xsl:text> + <xsl:text> this.offset = new_offset; +</xsl:text> + <xsl:text> this.relativeness = relativeness; +</xsl:text> + <xsl:text> /* add this's subsribers */ +</xsl:text> + <xsl:text> if(!this.unsubscribable) +</xsl:text> + <xsl:text> for(let i = 0; i < this.indexes.length; i++) { +</xsl:text> + <xsl:text> let index = this.indexes[i]; +</xsl:text> + <xsl:text> if(relativeness[i]) +</xsl:text> + <xsl:text> index += new_offset; +</xsl:text> + <xsl:text> subscribers[index].add(this); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> need_cache_apply.push(this); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> apply_cache() { +</xsl:text> <xsl:text> if(!this.unsubscribable) for(let index of this.indexes){ </xsl:text> - <xsl:text> let idx = index + this.offset; -</xsl:text> - <xsl:text> subscribers[idx].delete(this); + <xsl:text> /* dispatch current cache in newly opened page widgets */ +</xsl:text> + <xsl:text> let realindex = index+this.offset; +</xsl:text> + <xsl:text> let cached_val = cache[realindex]; +</xsl:text> + <xsl:text> if(cached_val != undefined) +</xsl:text> + <xsl:text> this.new_hmi_value(realindex, cached_val, cached_val); </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> this.offset = 0; -</xsl:text> <xsl:text> } </xsl:text> <xsl:text> </xsl:text> - <xsl:text> sub(new_offset=0){ -</xsl:text> - <xsl:text> /* set the offset because relative */ -</xsl:text> - <xsl:text> this.offset = new_offset; -</xsl:text> - <xsl:text> /* add this's subsribers */ -</xsl:text> - <xsl:text> if(!this.unsubscribable) for(let index of this.indexes){ -</xsl:text> - <xsl:text> subscribers[index + new_offset].add(this); + <xsl:text> get_idx(index) { +</xsl:text> + <xsl:text> let orig = this.indexes[index]; +</xsl:text> + <xsl:text> return this.relativeness[index] ? orig + this.offset : orig; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> change_hmi_value(index,opstr) { +</xsl:text> + <xsl:text> return change_hmi_value(this.get_idx(index), opstr); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> apply_hmi_value(index, new_val) { +</xsl:text> + <xsl:text> return apply_hmi_value(this.get_idx(0), new_val); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> new_hmi_value(index, value, oldval) { +</xsl:text> + <xsl:text> try { +</xsl:text> + <xsl:text> // TODO avoid searching, store index at sub() +</xsl:text> + <xsl:text> for(let i = 0; i < this.indexes.length; i++) { +</xsl:text> + <xsl:text> let refindex = this.indexes[i]; +</xsl:text> + <xsl:text> if(this.relativeness[i]) +</xsl:text> + <xsl:text> refindex += this.offset; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(index == refindex) { +</xsl:text> + <xsl:text> let d = this.dispatch; +</xsl:text> + <xsl:text> if(typeof(d) == "function"){ +</xsl:text> + <xsl:text> d.call(this, value, oldval, i); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> else if(typeof(d) == "object"){ +</xsl:text> + <xsl:text> d[i].call(this, 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> break; +</xsl:text> + <xsl:text> } +</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> need_cache_apply.push(this); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> apply_cache() { -</xsl:text> - <xsl:text> if(!this.unsubscribable) for(let index of this.indexes){ -</xsl:text> - <xsl:text> /* dispatch current cache in newly opened page widgets */ -</xsl:text> - <xsl:text> let realindex = index+this.offset; -</xsl:text> - <xsl:text> let cached_val = cache[realindex]; -</xsl:text> - <xsl:text> if(cached_val != undefined) -</xsl:text> - <xsl:text> dispatch_value_to_widget(this, realindex, cached_val, cached_val); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> get_idx(index) { -</xsl:text> - <xsl:text> let orig = this.indexes[index]; -</xsl:text> - <xsl:text> return this.offset ? orig + this.offset : orig; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> change_hmi_value(index,opstr) { -</xsl:text> - <xsl:text> return change_hmi_value(this.get_idx(index), opstr); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> apply_hmi_value(index, new_val) { -</xsl:text> - <xsl:text> return apply_hmi_value(this.get_idx(0), new_val); -</xsl:text> <xsl:text> } </xsl:text> <xsl:text>} @@ -1892,6 +1960,20 @@ </xsl:template> <xsl:template mode="widget_defs" match="widget[@type='ForEach']"> <xsl:param name="hmi_element"/> + <xsl:if test="count(path) != 1"> + <xsl:message terminate="yes"> + <xsl:text>ForEach widget </xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text> must have one HMI path given.</xsl:text> + </xsl:message> + </xsl:if> + <xsl:if test="count(arg) != 1"> + <xsl:message terminate="yes"> + <xsl:text>ForEach widget </xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text> must have one argument given : a class name.</xsl:text> + </xsl:message> + </xsl:if> <xsl:variable name="class" select="arg[1]/@value"/> <xsl:variable name="base_path" select="path/@value"/> <xsl:variable name="hmi_index_base" select="$indexed_hmitree/*[@hmipath = $base_path]"/> @@ -1993,97 +2075,125 @@ <xsl:template mode="widget_class" match="widget[@type='ForEach']"> <xsl:text>class ForEachWidget extends Widget{ </xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> unsub_items(){ +</xsl:text> + <xsl:text> for(let item of this.items){ +</xsl:text> + <xsl:text> for(let widget of item) { +</xsl:text> + <xsl:text> widget.unsub(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> <xsl:text> unsub(){ </xsl:text> - <xsl:text> for(let item of this.items){ + <xsl:text> this.unsub_items(); +</xsl:text> + <xsl:text> this.offset = 0; +</xsl:text> + <xsl:text> this.relativeness = undefined; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> sub_items(){ +</xsl:text> + <xsl:text> for(let i = 0; i < this.items.length; i++) { +</xsl:text> + <xsl:text> let item = this.items[i]; +</xsl:text> + <xsl:text> let orig_item_index = this.index_pool[i]; +</xsl:text> + <xsl:text> let item_index = this.index_pool[i+this.item_offset]; +</xsl:text> + <xsl:text> let item_index_offset = item_index - orig_item_index; +</xsl:text> + <xsl:text> if(this.relativeness[0]) +</xsl:text> + <xsl:text> item_index_offset += this.offset; </xsl:text> <xsl:text> for(let widget of item) { </xsl:text> - <xsl:text> widget.unsub(); + <xsl:text> /* all variables of all widgets in a ForEach are all relative. +</xsl:text> + <xsl:text> Really. +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> TODO: allow absolute variables in ForEach widgets +</xsl:text> + <xsl:text> */ +</xsl:text> + <xsl:text> widget.sub(item_index_offset, widget.indexes.map(_=>true)); </xsl:text> <xsl:text> } </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> this.offset = 0; -</xsl:text> <xsl:text> } </xsl:text> <xsl:text> </xsl:text> - <xsl:text> foreach_widgets_do(todo){ -</xsl:text> - <xsl:text> for(let i = 0; i < this.items.length; i++) { -</xsl:text> - <xsl:text> let item = this.items[i]; -</xsl:text> - <xsl:text> let orig_item_index = this.index_pool[i]; -</xsl:text> - <xsl:text> let item_index = this.index_pool[i+this.item_offset]; -</xsl:text> - <xsl:text> let item_index_offset = item_index - orig_item_index; -</xsl:text> - <xsl:text> for(let widget of item) { -</xsl:text> - <xsl:text> todo(widget).call(widget, this.offset + item_index_offset); -</xsl:text> - <xsl:text> } + <xsl:text> sub(new_offset=0, relativeness=[]){ +</xsl:text> + <xsl:text> this.offset = new_offset; +</xsl:text> + <xsl:text> this.relativeness = relativeness; +</xsl:text> + <xsl:text> this.sub_items(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> apply_cache() { +</xsl:text> + <xsl:text> this.items.forEach(item=>item.forEach(widget=>widget.apply_cache())); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> on_click(opstr, evt) { +</xsl:text> + <xsl:text> let new_item_offset = eval(String(this.item_offset)+opstr); +</xsl:text> + <xsl:text> if(new_item_offset + this.items.length > this.index_pool.length) { +</xsl:text> + <xsl:text> if(this.item_offset + this.items.length == this.index_pool.length) +</xsl:text> + <xsl:text> new_item_offset = 0; +</xsl:text> + <xsl:text> else +</xsl:text> + <xsl:text> new_item_offset = this.index_pool.length - this.items.length; +</xsl:text> + <xsl:text> } else if(new_item_offset < 0) { +</xsl:text> + <xsl:text> if(this.item_offset == 0) +</xsl:text> + <xsl:text> new_item_offset = this.index_pool.length - this.items.length; +</xsl:text> + <xsl:text> else +</xsl:text> + <xsl:text> new_item_offset = 0; </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> sub(new_offset=0){ -</xsl:text> - <xsl:text> this.offset = new_offset; -</xsl:text> - <xsl:text> this.foreach_widgets_do(w=>w.sub); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> apply_cache() { -</xsl:text> - <xsl:text> this.foreach_widgets_do(w=>w.apply_cache); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> on_click(opstr, evt) { -</xsl:text> - <xsl:text> let new_item_offset = eval(String(this.item_offset)+opstr); -</xsl:text> - <xsl:text> if(new_item_offset + this.items.length > this.index_pool.length) { -</xsl:text> - <xsl:text> if(this.item_offset + this.items.length == this.index_pool.length) -</xsl:text> - <xsl:text> new_item_offset = 0; -</xsl:text> - <xsl:text> else -</xsl:text> - <xsl:text> new_item_offset = this.index_pool.length - this.items.length; -</xsl:text> - <xsl:text> } else if(new_item_offset < 0) { -</xsl:text> - <xsl:text> if(this.item_offset == 0) -</xsl:text> - <xsl:text> new_item_offset = this.index_pool.length - this.items.length; -</xsl:text> - <xsl:text> else -</xsl:text> - <xsl:text> new_item_offset = 0; -</xsl:text> - <xsl:text> } -</xsl:text> <xsl:text> this.item_offset = new_item_offset; </xsl:text> - <xsl:text> this.unsub(); -</xsl:text> - <xsl:text> this.sub(this.offset); + <xsl:text> this.unsub_items(); +</xsl:text> + <xsl:text> this.sub_items(); </xsl:text> <xsl:text> update_subscriptions(); </xsl:text> @@ -3049,655 +3159,613 @@ </xsl:text> <xsl:text> </xsl:text> - <xsl:text>function dispatch_value_to_widget(widget, index, value, oldval) { + <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> widget.new_hmi_value(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> NODE: (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> +</xsl:text> + <xsl:text> while(widget = need_cache_apply.pop()){ +</xsl:text> + <xsl:text> widget.apply_cache(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(jumps_need_update) update_jumps(); +</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> 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> if(typeof(d) == "function" && idxidx == 0){ -</xsl:text> - <xsl:text> d.call(widget, value, oldval); + <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> NODE: (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> else if(typeof(d) == "object" && d.length >= idxidx){ -</xsl:text> - <xsl:text> d[idxidx].call(widget, value, oldval); + <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> new_hmi_value: function(index, value, oldval) { +</xsl:text> + <xsl:text> apply_hmi_value(heartbeat_index, value+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> let wf = widget.frequency; +</xsl:text> + <xsl:text> if(wf != undefined && maxfreq < wf) +</xsl:text> + <xsl:text> maxfreq = wf; +</xsl:text> + <xsl:text> } +</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> /* 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> 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> // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf +</xsl:text> + <xsl:text> // cache[index] = value; +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function apply_hmi_value(index, new_val) { +</xsl:text> + <xsl:text> let old_val = cache[index] +</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>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>quotes = {"'":null, '"':null}; +</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; +</xsl:text> + <xsl:text> if(opstr.length < 2) +</xsl:text> + <xsl:text> return undefined; // TODO raise +</xsl:text> + <xsl:text> if(opstr[1] in quotes){ +</xsl:text> + <xsl:text> if(opstr.length < 3) +</xsl:text> + <xsl:text> return undefined; // TODO raise +</xsl:text> + <xsl:text> if(opstr[opstr.length-1] == opstr[1]){ +</xsl:text> + <xsl:text> given_val = opstr.slice(2,opstr.length-1); </xsl:text> <xsl:text> } </xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> given_val = Number(opstr.slice(1)); +</xsl:text> <xsl:text> } </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> new_val = given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "+": +</xsl:text> + <xsl:text> new_val = old_val + given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "-": +</xsl:text> + <xsl:text> new_val = old_val - given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "*": +</xsl:text> + <xsl:text> new_val = old_val * given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "/": +</xsl:text> + <xsl:text> new_val = old_val / given_val; +</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> // TODO else raise +</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>var current_page_index; +</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 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> NODE: (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>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 false; </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> if(page_name == undefined) +</xsl:text> + <xsl:text> page_name = current_subscribed_page; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> +</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 false; </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> 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> while(widget = need_cache_apply.pop()){ -</xsl:text> - <xsl:text> widget.apply_cache(); + <xsl:text> if(old_desc){ +</xsl:text> + <xsl:text> old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(jumps_need_update) update_jumps(); -</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> NODE: (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> apply_hmi_value(heartbeat_index, value+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> let wf = widget.frequency; -</xsl:text> - <xsl:text> if(wf != undefined && maxfreq < wf) -</xsl:text> - <xsl:text> maxfreq = wf; -</xsl:text> - <xsl:text> } -</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> // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf -</xsl:text> - <xsl:text> // cache[index] = value; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function apply_hmi_value(index, new_val) { -</xsl:text> - <xsl:text> let old_val = cache[index] -</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>quotes = {"'":null, '"':null}; -</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; -</xsl:text> - <xsl:text> if(opstr.length < 2) -</xsl:text> - <xsl:text> return undefined; // TODO raise -</xsl:text> - <xsl:text> if(opstr[1] in quotes){ -</xsl:text> - <xsl:text> if(opstr.length < 3) -</xsl:text> - <xsl:text> return undefined; // TODO raise -</xsl:text> - <xsl:text> if(opstr[opstr.length-1] == opstr[1]){ -</xsl:text> - <xsl:text> given_val = opstr.slice(2,opstr.length-1); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> given_val = Number(opstr.slice(1)); -</xsl:text> - <xsl:text> } -</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> new_val = given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "+": -</xsl:text> - <xsl:text> new_val = old_val + given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "-": -</xsl:text> - <xsl:text> new_val = old_val - given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "*": -</xsl:text> - <xsl:text> new_val = old_val * given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "/": -</xsl:text> - <xsl:text> new_val = old_val / given_val; -</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> // TODO else raise -</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>var current_page_index; -</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 false; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(page_name == undefined) -</xsl:text> - <xsl:text> page_name = current_subscribed_page; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> -</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 false; -</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> old_desc.absolute_widgets.map(w=>w.unsub()); -</xsl:text> - <xsl:text> old_desc.relative_widgets.map(w=>w.unsub()); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> new_desc.absolute_widgets.map(w=>w.sub()); -</xsl:text> <xsl:text> var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; </xsl:text> - <xsl:text> new_desc.relative_widgets.map(w=>w.sub(new_offset)); + <xsl:text> new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness)); </xsl:text> <xsl:text> </xsl:text> diff -r 705e34c6fe93 -r ff9ae4f4e3be svghmi/svghmi.js --- a/svghmi/svghmi.js Tue Aug 04 11:53:39 2020 +0200 +++ b/svghmi/svghmi.js Wed Aug 05 18:49:29 2020 +0200 @@ -323,12 +323,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 705e34c6fe93 -r ff9ae4f4e3be svghmi/widget_foreach.ysl2 --- a/svghmi/widget_foreach.ysl2 Tue Aug 04 11:53:39 2020 +0200 +++ b/svghmi/widget_foreach.ysl2 Wed Aug 05 18:49:29 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 705e34c6fe93 -r ff9ae4f4e3be svghmi/widgets_common.ysl2 --- a/svghmi/widgets_common.ysl2 Tue Aug 04 11:53:39 2020 +0200 +++ b/svghmi/widgets_common.ysl2 Wed Aug 05 18:49:29 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); } @@ -107,7 +115,7 @@ 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);