# HG changeset patch # User Edouard Tisserant <edouard.tisserant@gmail.com> # Date 1653322350 -7200 # Node ID f422d3d71f89aab466cd2006ca13249831cf448d # Parent 435259844a644856d57ac32fa19e74df08dfd302 SVGHMI: fix active/inactive being swapped in ToggleButton diff -r 435259844a64 -r f422d3d71f89 svghmi/analyse_widget.xslt --- a/svghmi/analyse_widget.xslt Mon May 23 18:11:31 2022 +0200 +++ b/svghmi/analyse_widget.xslt Mon May 23 18:12:30 2022 +0200 @@ -50,6 +50,16 @@ <xsl:value-of select="$type"/> </xsl:attribute> <xsl:if test="$freq"> + <xsl:if test="not(regexp:test($freq,'^[0-9]*(\.[0-9]+)?[smh]?'))"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$label"/> + <xsl:text> has wrong syntax of frequency forcing </xsl:text> + <xsl:value-of select="$freq"/> + </xsl:message> + </xsl:if> <xsl:attribute name="freq"> <xsl:value-of select="$freq"/> </xsl:attribute> @@ -223,8 +233,6 @@ </xsl:template> <xsl:template name="generated_button_class"> <xsl:param name="fsm"/> - <xsl:text> frequency = 5; -</xsl:text> <xsl:text> display = "inactive"; </xsl:text> <xsl:text> state = "init"; @@ -717,7 +725,7 @@ <xsl:value-of select="@type"/> </type> <longdesc> - <xsl:text>PathSlider - + <xsl:text>PathSlider - </xsl:text> </longdesc> <shortdesc> diff -r 435259844a64 -r f422d3d71f89 svghmi/gen_dnd_widget_svg.xslt --- a/svghmi/gen_dnd_widget_svg.xslt Mon May 23 18:11:31 2022 +0200 +++ b/svghmi/gen_dnd_widget_svg.xslt Mon May 23 18:12:30 2022 +0200 @@ -52,6 +52,16 @@ <xsl:value-of select="$type"/> </xsl:attribute> <xsl:if test="$freq"> + <xsl:if test="not(regexp:test($freq,'^[0-9]*(\.[0-9]+)?[smh]?'))"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$label"/> + <xsl:text> has wrong syntax of frequency forcing </xsl:text> + <xsl:value-of select="$freq"/> + </xsl:message> + </xsl:if> <xsl:attribute name="freq"> <xsl:value-of select="$freq"/> </xsl:attribute> diff -r 435259844a64 -r f422d3d71f89 svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Mon May 23 18:11:31 2022 +0200 +++ b/svghmi/gen_index_xhtml.xslt Mon May 23 18:12:30 2022 +0200 @@ -207,6 +207,16 @@ <xsl:value-of select="$type"/> </xsl:attribute> <xsl:if test="$freq"> + <xsl:if test="not(regexp:test($freq,'^[0-9]*(\.[0-9]+)?[smh]?'))"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$label"/> + <xsl:text> has wrong syntax of frequency forcing </xsl:text> + <xsl:value-of select="$freq"/> + </xsl:message> + </xsl:if> <xsl:attribute name="freq"> <xsl:value-of select="$freq"/> </xsl:attribute> @@ -1248,7 +1258,9 @@ <xsl:variable name="freq"> <xsl:choose> <xsl:when test="$widget/@freq"> + <xsl:text>"</xsl:text> <xsl:value-of select="$widget/@freq"/> + <xsl:text>"</xsl:text> </xsl:when> <xsl:otherwise> <xsl:text>undefined</xsl:text> @@ -1477,6 +1489,68 @@ </xsl:text> <xsl:text> this.forced_frequency = freq; </xsl:text> + <xsl:text> this.clip = true; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> do_init(){ +</xsl:text> + <xsl:text> let forced = this.forced_frequency; +</xsl:text> + <xsl:text> if(forced !== undefined){ +</xsl:text> + <xsl:text> /* +</xsl:text> + <xsl:text> once every 10 seconds : 10s +</xsl:text> + <xsl:text> once per minute : 1m +</xsl:text> + <xsl:text> once per hour : 1h +</xsl:text> + <xsl:text> once per day : 1d +</xsl:text> + <xsl:text> */ +</xsl:text> + <xsl:text> let unit = forced.slice(-1); +</xsl:text> + <xsl:text> let factor = { +</xsl:text> + <xsl:text> "s":1, +</xsl:text> + <xsl:text> "m":60, +</xsl:text> + <xsl:text> "h":3600, +</xsl:text> + <xsl:text> "d":86400}[unit]; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> this.frequency = factor ? 1/(factor * Number(forced.slice(0,-1))) +</xsl:text> + <xsl:text> : Number(forced); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let init = this.init; +</xsl:text> + <xsl:text> if(typeof(init) == "function"){ +</xsl:text> + <xsl:text> try { +</xsl:text> + <xsl:text> init.call(this); +</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> @@ -1649,7 +1723,9 @@ </xsl:text> <xsl:text> let new_val = eval_operation_string(old_val, opstr); </xsl:text> - <xsl:text> new_val = this.clip_min_max(index, new_val); + <xsl:text> if(this.clip) +</xsl:text> + <xsl:text> new_val = this.clip_min_max(index, new_val); </xsl:text> <xsl:text> return apply_hmi_value(realindex, new_val); </xsl:text> @@ -1663,7 +1739,9 @@ </xsl:text> <xsl:text> if(realindex == undefined) return undefined; </xsl:text> - <xsl:text> new_val = this.clip_min_max(index, new_val); + <xsl:text> if(this.clip) +</xsl:text> + <xsl:text> new_val = this.clip_min_max(index, new_val); </xsl:text> <xsl:text> return apply_hmi_value(realindex, new_val); </xsl:text> @@ -1883,8 +1961,10 @@ <xsl:param name="hmi_element"/> <xsl:variable name="widget_type" select="@type"/> <xsl:for-each select="str:split($labels)"> - <xsl:variable name="name" select="."/> - <xsl:variable name="elt" select="$result_widgets[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]"/> + <xsl:variable name="absolute" select="starts-with(., '/')"/> + <xsl:variable name="name" select="substring(.,number($absolute)+1)"/> + <xsl:variable name="widget" select="$result_widgets[@id = $hmi_element/@id]"/> + <xsl:variable name="elt" select="($widget//*[not($absolute) and @inkscape:label=$name] | $widget/*[$absolute and @inkscape:label=$name])[1]"/> <xsl:choose> <xsl:when test="not($elt/@id)"> <xsl:if test="$mandatory='yes'"> @@ -2400,10 +2480,6 @@ <xsl:value-of select="@name"/> <xsl:text>_action(){ </xsl:text> - <xsl:text>console.log("Entering state </xsl:text> - <xsl:value-of select="@name"/> - <xsl:text>", this.frequency); -</xsl:text> <xsl:apply-templates mode="actions" select="*"/> <xsl:text> } </xsl:text> @@ -2424,8 +2500,6 @@ </xsl:template> <xsl:template name="generated_button_class"> <xsl:param name="fsm"/> - <xsl:text> frequency = 5; -</xsl:text> <xsl:text> display = "inactive"; </xsl:text> <xsl:text> state = "init"; @@ -2492,6 +2566,8 @@ <xsl:text>ButtonWidget</xsl:text> <xsl:text> extends Widget{ </xsl:text> + <xsl:text> frequency = 5; +</xsl:text> <xsl:variable name="fsm" select="exsl:node-set($_button_fsm)"/> <xsl:call-template name="generated_button_class"> <xsl:with-param name="fsm" select="$fsm"/> @@ -2514,6 +2590,8 @@ <xsl:text>PushButtonWidget</xsl:text> <xsl:text> extends Widget{ </xsl:text> + <xsl:text> frequency = 20; +</xsl:text> <xsl:variable name="fsm" select="exsl:node-set($_push_button_fsm)"/> <xsl:call-template name="generated_button_class"> <xsl:with-param name="fsm" select="$fsm"/> @@ -6116,7 +6194,7 @@ <xsl:value-of select="@type"/> </type> <longdesc> - <xsl:text>PathSlider - + <xsl:text>PathSlider - </xsl:text> </longdesc> <shortdesc> @@ -6159,7 +6237,7 @@ </xsl:text> <xsl:text> origPt = undefined; </xsl:text> - <xsl:text> + <xsl:text> </xsl:text> <xsl:text> </xsl:text> @@ -6243,7 +6321,7 @@ </xsl:text> <xsl:text> bestDistance = beforeDistance; </xsl:text> - <xsl:text> } else if ((afterLength = bestLength + precision) <= this.pathLength && + <xsl:text> } else if ((afterLength = bestLength + precision) <= this.pathLength && </xsl:text> <xsl:text> (afterDistance = distance2(afterPoint = this.path_elt.getPointAtLength(afterLength))) < bestDistance) { </xsl:text> @@ -7627,7 +7705,7 @@ </xsl:text> <xsl:text> activate(val) { </xsl:text> - <xsl:text> let [active, inactive] = val ? ["none",""] : ["", "none"]; + <xsl:text> let [active, inactive] = val ? ["","none"] : ["none", ""]; </xsl:text> <xsl:text> if (this.active_elt) </xsl:text> @@ -7751,7 +7829,7 @@ </xsl:text> <xsl:text> modulo: /^%{2}/, </xsl:text> - <xsl:text> placeholder: /^%(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/, + <xsl:text> placeholder: /^%(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxXD])/, </xsl:text> <xsl:text> key: /^([a-z_][a-z_\d]*)/i, </xsl:text> @@ -7877,6 +7955,96 @@ </xsl:text> <xsl:text> break </xsl:text> + <xsl:text> case 'D': +</xsl:text> + <xsl:text> /* +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> select date format with width +</xsl:text> + <xsl:text> select time format with precision +</xsl:text> + <xsl:text> %D => 13:31 AM (default) +</xsl:text> + <xsl:text> %1D => 13:31 AM +</xsl:text> + <xsl:text> %.1D => 07/07/20 +</xsl:text> + <xsl:text> %1.1D => 07/07/20, 13:31 AM +</xsl:text> + <xsl:text> %1.2D => 07/07/20, 13:31:55 AM +</xsl:text> + <xsl:text> %2.2D => May 5, 2022, 9:29:16 AM +</xsl:text> + <xsl:text> %3.3D => May 5, 2022 at 9:28:16 AM GMT+2 +</xsl:text> + <xsl:text> %4.4D => Thursday, May 5, 2022 at 9:26:59 AM Central European Summer Time +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> see meaning of DateTimeFormat's options "datestyle" and "timestyle" in MDN +</xsl:text> + <xsl:text> */ +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let [datestyle, timestyle] = [ph.width, ph.precision].map(val => ({ +</xsl:text> + <xsl:text> 1: "short", +</xsl:text> + <xsl:text> 2: "medium", +</xsl:text> + <xsl:text> 3: "long", +</xsl:text> + <xsl:text> 4: "full" +</xsl:text> + <xsl:text> }[val])); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(timestyle === undefined && datestyle === undefined){ +</xsl:text> + <xsl:text> timestyle = "short"; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let options = { +</xsl:text> + <xsl:text> dateStyle: datestyle, +</xsl:text> + <xsl:text> timeStyle: timestyle, +</xsl:text> + <xsl:text> hour12: false +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> /* get lang from globals */ +</xsl:text> + <xsl:text> let lang = get_current_lang_code(); +</xsl:text> + <xsl:text> arg = Date(arg).toLocaleString('en-US', options); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> /* +</xsl:text> + <xsl:text> TODO: select with padding char +</xsl:text> + <xsl:text> a: absolute time and date (default) +</xsl:text> + <xsl:text> r: relative time +</xsl:text> + <xsl:text> */ +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> break +</xsl:text> <xsl:text> case 'j': </xsl:text> <xsl:text> arg = JSON.stringify(arg, null, ph.width ? parseInt(ph.width) : 0) @@ -8221,498 +8389,490 @@ </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> widget.do_init(); +</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 has_watchdog = window.location.hash == "#watchdog"; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var ws_url = +</xsl:text> + <xsl:text> window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') +</xsl:text> + <xsl:text> + '?mode=' + (has_watchdog ? "watchdog" : "multiclient"); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var ws = new WebSocket(ws_url); +</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> REAL: (dv,offset) => [dv.getFloat32(offset, true), 4], +</xsl:text> + <xsl:text> STRING: (dv, offset) => { +</xsl:text> + <xsl:text> const 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> updates.forEach((value, index) => { +</xsl:text> + <xsl:text> dispatch_value(index, value); +</xsl:text> + <xsl:text> }); +</xsl:text> + <xsl:text> updates.clear(); +</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> +</xsl:text> + <xsl:text> pending_widget_animates.forEach(widget => widget._animate()); +</xsl:text> + <xsl:text> pending_widget_animates = []; +</xsl:text> + <xsl:text> +</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.set(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>hmi_hash_u8 = new Uint8Array(hmi_hash); +</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([hmi_hash_u8].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> REAL: (number) => new Float32Array([number]), +</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(let i = 0; i < str.length; i++){ +</xsl:text> + <xsl:text> binary[i+1] = str.charCodeAt(i); +</xsl:text> <xsl:text> } </xsl:text> - <xsl:text> if(widget.forced_frequency !== undefined) -</xsl:text> - <xsl:text> widget.frequency = widget.forced_frequency; + <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>var subscriptions = []; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function subscribers(index) { +</xsl:text> + <xsl:text> let entry = subscriptions[index]; +</xsl:text> + <xsl:text> let res; +</xsl:text> + <xsl:text> if(entry == undefined){ +</xsl:text> + <xsl:text> res = new Set(); +</xsl:text> + <xsl:text> subscriptions[index] = [res,0]; +</xsl:text> + <xsl:text> }else{ +</xsl:text> + <xsl:text> [res, _ign] = entry; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> return res +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function get_subscription_period(index) { +</xsl:text> + <xsl:text> let entry = subscriptions[index]; +</xsl:text> + <xsl:text> if(entry == undefined) +</xsl:text> + <xsl:text> return 0; +</xsl:text> + <xsl:text> let [_ign, period] = entry; +</xsl:text> + <xsl:text> return period; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function set_subscription_period(index, period) { +</xsl:text> + <xsl:text> let entry = subscriptions[index]; +</xsl:text> + <xsl:text> if(entry == undefined){ +</xsl:text> + <xsl:text> subscriptions[index] = [new Set(), period]; +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> entry[1] = period; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>if(has_watchdog){ +</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> -</xsl:text> - <xsl:text>// Open WebSocket to relative "/ws" address -</xsl:text> - <xsl:text>var has_watchdog = window.location.hash == "#watchdog"; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var ws_url = -</xsl:text> - <xsl:text> window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') -</xsl:text> - <xsl:text> + '?mode=' + (has_watchdog ? "watchdog" : "multiclient"); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var ws = new WebSocket(ws_url); -</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> REAL: (dv,offset) => [dv.getFloat32(offset, true), 4], -</xsl:text> - <xsl:text> STRING: (dv, offset) => { -</xsl:text> - <xsl:text> const 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>// subscribe to per instance current page hmi variable +</xsl:text> + <xsl:text>// PLC must prefix page name with "!" for page switch to happen +</xsl:text> + <xsl:text>subscribers(current_page_var_index).add({ +</xsl:text> + <xsl:text> frequency: 1, +</xsl:text> + <xsl:text> indexes: [current_page_var_index], +</xsl:text> + <xsl:text> new_hmi_value: function(index, value, oldval) { +</xsl:text> + <xsl:text> if(value.startsWith("!")) +</xsl:text> + <xsl:text> switch_page(value.slice(1)); </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> updates.forEach((value, index) => { -</xsl:text> - <xsl:text> dispatch_value(index, value); -</xsl:text> - <xsl:text> }); -</xsl:text> - <xsl:text> updates.clear(); + <xsl:text>}); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function svg_text_to_multiline(elt) { +</xsl:text> + <xsl:text> return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); </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>function multiline_to_svg_text(elt, str) { +</xsl:text> + <xsl:text> str.split('\n').map((line,i) => {elt.children[i].textContent = line;}); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function switch_langnum(langnum) { +</xsl:text> + <xsl:text> langnum = Math.max(0, Math.min(langs.length - 1, langnum)); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> for (let translation of translations) { +</xsl:text> + <xsl:text> let [objs, msgs] = translation; +</xsl:text> + <xsl:text> let msg = msgs[langnum]; +</xsl:text> + <xsl:text> for (let obj of objs) { +</xsl:text> + <xsl:text> multiline_to_svg_text(obj, msg); +</xsl:text> + <xsl:text> obj.setAttribute("lang",langnum); +</xsl:text> + <xsl:text> } </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> return langnum; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// backup original texts +</xsl:text> + <xsl:text>for (let translation of translations) { +</xsl:text> + <xsl:text> let [objs, msgs] = translation; +</xsl:text> + <xsl:text> msgs.unshift(svg_text_to_multiline(objs[0])); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var lang_local_index = hmi_local_index("lang"); +</xsl:text> + <xsl:text>var langcode_local_index = hmi_local_index("lang_code"); +</xsl:text> + <xsl:text>var langname_local_index = hmi_local_index("lang_name"); +</xsl:text> + <xsl:text>subscribers(lang_local_index).add({ +</xsl:text> + <xsl:text> indexes: [lang_local_index], +</xsl:text> + <xsl:text> new_hmi_value: function(index, value, oldval) { +</xsl:text> + <xsl:text> let current_lang = switch_langnum(value); +</xsl:text> + <xsl:text> let [langname,langcode] = langs[current_lang]; +</xsl:text> + <xsl:text> apply_hmi_value(langcode_local_index, langcode); +</xsl:text> + <xsl:text> apply_hmi_value(langname_local_index, langname); +</xsl:text> + <xsl:text> switch_page(); </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> -</xsl:text> - <xsl:text> pending_widget_animates.forEach(widget => widget._animate()); -</xsl:text> - <xsl:text> pending_widget_animates = []; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> requestAnimationFrameID = null; + <xsl:text>}); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// returns en_US, fr_FR or en_UK depending on selected language +</xsl:text> + <xsl:text>function get_current_lang_code(){ +</xsl:text> + <xsl:text> return cache[langcode_local_index]; </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.set(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>hmi_hash_u8 = new Uint8Array(hmi_hash); -</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([hmi_hash_u8].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> REAL: (number) => new Float32Array([number]), -</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(let 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>var subscriptions = []; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function subscribers(index) { -</xsl:text> - <xsl:text> let entry = subscriptions[index]; -</xsl:text> - <xsl:text> let res; -</xsl:text> - <xsl:text> if(entry == undefined){ -</xsl:text> - <xsl:text> res = new Set(); -</xsl:text> - <xsl:text> subscriptions[index] = [res,0]; -</xsl:text> - <xsl:text> }else{ -</xsl:text> - <xsl:text> [res, _ign] = entry; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> return res -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function get_subscription_period(index) { -</xsl:text> - <xsl:text> let entry = subscriptions[index]; -</xsl:text> - <xsl:text> if(entry == undefined) -</xsl:text> - <xsl:text> return 0; -</xsl:text> - <xsl:text> let [_ign, period] = entry; -</xsl:text> - <xsl:text> return period; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function set_subscription_period(index, period) { -</xsl:text> - <xsl:text> let entry = subscriptions[index]; -</xsl:text> - <xsl:text> if(entry == undefined){ -</xsl:text> - <xsl:text> subscriptions[index] = [new Set(), period]; -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> entry[1] = period; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>if(has_watchdog){ -</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> -</xsl:text> - <xsl:text>// subscribe to per instance current page hmi variable -</xsl:text> - <xsl:text>// PLC must prefix page name with "!" for page switch to happen -</xsl:text> - <xsl:text>subscribers(current_page_var_index).add({ -</xsl:text> - <xsl:text> frequency: 1, -</xsl:text> - <xsl:text> indexes: [current_page_var_index], -</xsl:text> - <xsl:text> new_hmi_value: function(index, value, oldval) { -</xsl:text> - <xsl:text> if(value.startsWith("!")) -</xsl:text> - <xsl:text> switch_page(value.slice(1)); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>}); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function svg_text_to_multiline(elt) { -</xsl:text> - <xsl:text> return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function multiline_to_svg_text(elt, str) { -</xsl:text> - <xsl:text> str.split('\n').map((line,i) => {elt.children[i].textContent = line;}); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function switch_langnum(langnum) { -</xsl:text> - <xsl:text> langnum = Math.max(0, Math.min(langs.length - 1, langnum)); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> for (let translation of translations) { -</xsl:text> - <xsl:text> let [objs, msgs] = translation; -</xsl:text> - <xsl:text> let msg = msgs[langnum]; -</xsl:text> - <xsl:text> for (let obj of objs) { -</xsl:text> - <xsl:text> multiline_to_svg_text(obj, msg); -</xsl:text> - <xsl:text> obj.setAttribute("lang",langnum); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> return langnum; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>// backup original texts -</xsl:text> - <xsl:text>for (let translation of translations) { -</xsl:text> - <xsl:text> let [objs, msgs] = translation; -</xsl:text> - <xsl:text> msgs.unshift(svg_text_to_multiline(objs[0])); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var lang_local_index = hmi_local_index("lang"); -</xsl:text> - <xsl:text>var langcode_local_index = hmi_local_index("lang_code"); -</xsl:text> - <xsl:text>var langname_local_index = hmi_local_index("lang_name"); -</xsl:text> - <xsl:text>subscribers(lang_local_index).add({ -</xsl:text> - <xsl:text> indexes: [lang_local_index], -</xsl:text> - <xsl:text> new_hmi_value: function(index, value, oldval) { -</xsl:text> - <xsl:text> let current_lang = switch_langnum(value); -</xsl:text> - <xsl:text> let [langname,langcode] = langs[current_lang]; -</xsl:text> - <xsl:text> apply_hmi_value(langcode_local_index, langcode); -</xsl:text> - <xsl:text> apply_hmi_value(langname_local_index, langname); -</xsl:text> - <xsl:text> switch_page(); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>}); -</xsl:text> - <xsl:text> -</xsl:text> <xsl:text>function setup_lang(){ </xsl:text> <xsl:text> let current_lang = cache[lang_local_index]; diff -r 435259844a64 -r f422d3d71f89 svghmi/widget_tooglebutton.ysl2 --- a/svghmi/widget_tooglebutton.ysl2 Mon May 23 18:11:31 2022 +0200 +++ b/svghmi/widget_tooglebutton.ysl2 Mon May 23 18:12:30 2022 +0200 @@ -38,7 +38,7 @@ } activate(val) { - let [active, inactive] = val ? ["none",""] : ["", "none"]; + let [active, inactive] = val ? ["","none"] : ["none", ""]; if (this.active_elt) this.active_elt.style.display = active; if (this.inactive_elt)