# HG changeset patch # User Edouard Tisserant <edouard.tisserant@gmail.com> # Date 1667579917 -3600 # Node ID e0d6f5f0dcc291b9e2f5ac516d9c56191e69b9d9 # Parent b5c6bb72bfc9c87a3bbbc26461e2850c72c03306# Parent efbc869494678098f2d00a16c70b94ad39034a55 Merged changes from default in wxPython4 branch diff -r b5c6bb72bfc9 -r e0d6f5f0dcc2 controls/PouInstanceVariablesPanel.py --- a/controls/PouInstanceVariablesPanel.py Thu Nov 03 17:43:30 2022 +0100 +++ b/controls/PouInstanceVariablesPanel.py Fri Nov 04 17:38:37 2022 +0100 @@ -174,6 +174,19 @@ self.DebugInstanceImage: _ButtonCallbacks( self.DebugButtonCallback, self.DebugButtonDClickCallback)} + self.FilterCtrl = wx.SearchCtrl(self) + self.FilterCtrl.ShowCancelButton(True) + self.FilterCtrl.Bind(wx.EVT_TEXT, self.OnFilterUpdate) + self.FilterCtrl.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self.OnFilterCancel) + + searchMenu = wx.Menu() + item = searchMenu.AppendCheckItem(-1, _("Match Case")) + self.Bind(wx.EVT_MENU, self.OnSearchMenu, item) + item = searchMenu.AppendCheckItem(-1, _("Whole Words")) + self.Bind(wx.EVT_MENU, self.OnSearchMenu, item) + self.FilterCtrl.SetMenu(searchMenu) + + buttons_sizer = wx.FlexGridSizer(cols=3, hgap=0, rows=1, vgap=0) buttons_sizer.Add(self.ParentButton) buttons_sizer.Add(self.InstanceChoice, flag=wx.GROW) @@ -181,9 +194,10 @@ buttons_sizer.AddGrowableCol(1) buttons_sizer.AddGrowableRow(0) - main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0) + main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=0) main_sizer.Add(buttons_sizer, flag=wx.GROW) main_sizer.Add(self.VariablesList, flag=wx.GROW) + main_sizer.Add(self.FilterCtrl, flag=wx.GROW) main_sizer.AddGrowableCol(0) main_sizer.AddGrowableRow(1) @@ -199,6 +213,11 @@ self.PouInfos = None self.PouInstance = None + self.Filter = None + self.FilterCaseSensitive = False + self.FilterWholeWord = False + + def __del__(self): self.Controller = None @@ -236,6 +255,21 @@ self.RefreshView() + def OnSearchMenu(self, event): + searchMenu = self.FilterCtrl.GetMenu().GetMenuItems() + self.FilterCaseSensitive = searchMenu[0].IsChecked() + self.FilterWholeWord = searchMenu[1].IsChecked() + self.RefreshView() + + def OnFilterUpdate(self, event): + self.Filter = self.FilterCtrl.GetValue() + self.RefreshView() + event.Skip() + + def OnFilterCancel(self, event): + self.FilterCtrl.SetValue('') + event.Skip() + def RefreshView(self): self.Freeze() self.VariablesList.DeleteAllItems() @@ -252,6 +286,15 @@ if self.PouInfos is not None: root = self.VariablesList.AddRoot("", data=self.PouInfos) for var_infos in self.PouInfos.variables: + if self.Filter: + pattern = self.Filter + varname = var_infos.name + if not self.FilterCaseSensitive: + pattern = pattern.upper() + varname = varname.upper() + if ((pattern != varname) if self.FilterWholeWord else + (pattern not in varname)): + continue if var_infos.type is not None: text = "%s (%s)" % (var_infos.name, var_infos.type) else: diff -r b5c6bb72bfc9 -r e0d6f5f0dcc2 exemples/svghmi_references/plc.xml --- a/exemples/svghmi_references/plc.xml Thu Nov 03 17:43:30 2022 +0100 +++ b/exemples/svghmi_references/plc.xml Fri Nov 04 17:38:37 2022 +0100 @@ -1,7 +1,7 @@ <?xml version='1.0' encoding='utf-8'?> <project xmlns:ns1="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.plcopen.org/xml/tc6_0201"> <fileHeader companyName="Unknown" productName="Unnamed" productVersion="1" creationDateTime="2022-09-05T09:02:48"/> - <contentHeader name="Unnamed" modificationDateTime="2022-09-20T11:48:55"> + <contentHeader name="Unnamed" modificationDateTime="2022-10-04T10:59:24"> <coordinateInfo> <fbd> <scaling x="5" y="5"/> @@ -22,12 +22,12 @@ <localVars> <variable name="LocalVar0"> <type> - <DINT/> + <INT/> </type> </variable> - <variable name="LocalVar1"> + <variable name="PLCHMIVAR"> <type> - <DINT/> + <derived name="HMI_INT"/> </type> </variable> </localVars> @@ -39,7 +39,7 @@ <connectionPointOut> <relPosition x="85" y="10"/> </connectionPointOut> - <expression>LocalVar0</expression> + <expression>PLCHMIVAR</expression> </inVariable> <outVariable localId="30" executionOrderId="0" height="25" width="85" negated="false"> <position x="330" y="290"/> @@ -50,7 +50,7 @@ <position x="260" y="300"/> </connection> </connectionPointIn> - <expression>LocalVar1</expression> + <expression>LocalVar0</expression> </outVariable> </FBD> </body> diff -r b5c6bb72bfc9 -r e0d6f5f0dcc2 svghmi/detachable_pages.ysl2 --- a/svghmi/detachable_pages.ysl2 Thu Nov 03 17:43:30 2022 +0100 +++ b/svghmi/detachable_pages.ysl2 Fri Nov 04 17:38:37 2022 +0100 @@ -25,6 +25,17 @@ emit "preamble:default-page" { | | var default_page = "«$default_page»"; + const "screensaverpage", "$hmi_pages_descs[arg[1]/@value = 'ScreenSaver']"; + const "delay" choose { + when "$screensaverpage" { + const "delaystr", "$screensaverpage/arg[2]/@value"; + if "not(regexp:test($delaystr,'^[0-9]+$'))" + error > ScreenSaver page has missing or malformed delay argument. + value "$delaystr"; + } + otherwise > null + } + | var screensaver_delay = «$delay»; } const "keypads_descs", "$parsed_widgets/widget[@type = 'Keypad']"; diff -r b5c6bb72bfc9 -r e0d6f5f0dcc2 svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Thu Nov 03 17:43:30 2022 +0100 +++ b/svghmi/gen_index_xhtml.xslt Fri Nov 04 17:38:37 2022 +0100 @@ -637,6 +637,27 @@ <xsl:value-of select="$default_page"/> <xsl:text>"; </xsl:text> + <xsl:variable name="screensaverpage" select="$hmi_pages_descs[arg[1]/@value = 'ScreenSaver']"/> + <xsl:variable name="delay"> + <xsl:choose> + <xsl:when test="$screensaverpage"> + <xsl:variable name="delaystr" select="$screensaverpage/arg[2]/@value"/> + <xsl:if test="not(regexp:test($delaystr,'^[0-9]+$'))"> + <xsl:message terminate="yes"> + <xsl:text>ScreenSaver page has missing or malformed delay argument.</xsl:text> + </xsl:message> + </xsl:if> + <xsl:value-of select="$delaystr"/> + </xsl:when> + <xsl:otherwise> + <xsl:text>null</xsl:text> + </xsl:otherwise> + </xsl:choose> + </xsl:variable> + <xsl:text>var screensaver_delay = </xsl:text> + <xsl:value-of select="$delay"/> + <xsl:text>; +</xsl:text> <xsl:text> </xsl:text> </xsl:template> @@ -2907,9 +2928,17 @@ </xsl:text> <xsl:text> if(jump_history.length > 1){ </xsl:text> - <xsl:text> jump_history.pop(); -</xsl:text> - <xsl:text> let [page_name, index] = jump_history.pop(); + <xsl:text> let page_name, index; +</xsl:text> + <xsl:text> do { +</xsl:text> + <xsl:text> jump_history.pop(); // forget current page +</xsl:text> + <xsl:text> if(jump_history.length == 0) return; +</xsl:text> + <xsl:text> [page_name, index] = jump_history[jump_history.length-1]; +</xsl:text> + <xsl:text> } while(page_name == "ScreenSaver") // never go back to ScreenSaver </xsl:text> <xsl:text> switch_page(page_name, index); </xsl:text> @@ -2919,7 +2948,7 @@ </xsl:text> <xsl:text> init() { </xsl:text> - <xsl:text> this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); + <xsl:text> this.element.onclick = this.on_click.bind(this); </xsl:text> <xsl:text> } </xsl:text> @@ -11289,7 +11318,1247 @@ </xsl:text> <xsl:text> </xsl:text> - <xsl:text>var ws_url = + <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>// Called on requestAnimationFrame, modifies DOM +</xsl:text> + <xsl:text>var requestAnimationFrameID = null; +</xsl:text> + <xsl:text>function animate() { +</xsl:text> + <xsl:text> let rearm = true; +</xsl:text> + <xsl:text> do{ +</xsl:text> + <xsl:text> if(page_fading == "pending" || page_fading == "forced"){ +</xsl:text> + <xsl:text> if(page_fading == "pending") +</xsl:text> + <xsl:text> svg_root.classList.add("fade-out-page"); +</xsl:text> + <xsl:text> page_fading = "in_progress"; +</xsl:text> + <xsl:text> if(page_fading_args.length) +</xsl:text> + <xsl:text> setTimeout(function(){ +</xsl:text> + <xsl:text> switch_page(...page_fading_args); +</xsl:text> + <xsl:text> },1); +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // Do the page swith if pending +</xsl:text> + <xsl:text> if(page_switch_in_progress){ +</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> page_switch_in_progress = false; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(page_fading == "in_progress"){ +</xsl:text> + <xsl:text> svg_root.classList.remove("fade-out-page"); +</xsl:text> + <xsl:text> page_fading = "off"; +</xsl:text> + <xsl:text> } +</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> +</xsl:text> + <xsl:text> pending_widget_animates.forEach(widget => widget._animate()); +</xsl:text> + <xsl:text> pending_widget_animates = []; +</xsl:text> + <xsl:text> rearm = false; +</xsl:text> + <xsl:text> } while(0); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> requestAnimationFrameID = null; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(rearm) requestHMIAnimation(); +</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>function ws_onmessage(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> dispatch_value(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> +</xsl:text> + <xsl:text> // register for rendering on next frame, since there are updates +</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>var ws = null; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function send_blob(data) { +</xsl:text> + <xsl:text> if(ws && 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 Int8Array([truth]), +</xsl:text> + <xsl:text> NODE: (truth) => new Int8Array([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>function reset_subscription_periods() { +</xsl:text> + <xsl:text> for(let index in subscriptions) +</xsl:text> + <xsl:text> subscriptions[index][1] = 0; +</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> +</xsl:text> + <xsl:text>var page_fading = "off"; +</xsl:text> + <xsl:text>var page_fading_args = "off"; +</xsl:text> + <xsl:text>function fading_page_switch(...args){ +</xsl:text> + <xsl:text> if(page_fading == "in_progress") +</xsl:text> + <xsl:text> page_fading = "forced"; +</xsl:text> + <xsl:text> else +</xsl:text> + <xsl:text> page_fading = "pending"; +</xsl:text> + <xsl:text> page_fading_args = args; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> requestHMIAnimation(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text>document.body.style.backgroundColor = "black"; +</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> fading_page_switch(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, blank) { +</xsl:text> + <xsl:text> str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":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>// 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 setup_lang(){ +</xsl:text> + <xsl:text> let current_lang = cache[lang_local_index]; +</xsl:text> + <xsl:text> let new_lang = switch_langnum(current_lang); +</xsl:text> + <xsl:text> if(current_lang != new_lang){ +</xsl:text> + <xsl:text> apply_hmi_value(lang_local_index, new_lang); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>setup_lang(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function update_subscriptions() { +</xsl:text> + <xsl:text> let delta = []; +</xsl:text> + <xsl:text> if(!ws) +</xsl:text> + <xsl:text> // dont' change subscriptions if not connected +</xsl:text> + <xsl:text> return; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> for(let index in subscriptions){ +</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 = get_subscription_period(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> set_subscription_period(index, new_period); +</xsl:text> + <xsl:text> if(index <= last_remote_index){ +</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> } +</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> if(index > last_remote_index){ +</xsl:text> + <xsl:text> dispatch_value(index, value); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(persistent_indexes.has(index)){ +</xsl:text> + <xsl:text> let varname = persistent_indexes.get(index); +</xsl:text> + <xsl:text> document.cookie = varname+"="+value+"; max-age=3153600000"; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> return; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</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> // Similarly to previous comment, taking decision to update based +</xsl:text> + <xsl:text> // on cache content is bad and can lead to inconsistency +</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>const quotes = {"'":null, '"':null}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function eval_operation_string(old_val, 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; +</xsl:text> + <xsl:text> if(opstr[1] in quotes){ +</xsl:text> + <xsl:text> if(opstr.length < 3) +</xsl:text> + <xsl:text> return undefined; +</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 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> 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>var page_node_local_index = hmi_local_index("page_node"); +</xsl:text> + <xsl:text>var page_switch_in_progress = false; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function toggleFullscreen() { +</xsl:text> + <xsl:text> let elem = document.documentElement; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if (!document.fullscreenElement) { +</xsl:text> + <xsl:text> elem.requestFullscreen().catch(err => { +</xsl:text> + <xsl:text> console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); +</xsl:text> + <xsl:text> }); +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> document.exitFullscreen(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// prevents context menu from appearing on right click and long touch +</xsl:text> + <xsl:text>document.body.addEventListener('contextmenu', e => { +</xsl:text> + <xsl:text> toggleFullscreen(); +</xsl:text> + <xsl:text> e.preventDefault(); +</xsl:text> + <xsl:text>}); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var screensaver_timer = null; +</xsl:text> + <xsl:text>function reset_screensaver_timer() { +</xsl:text> + <xsl:text> if(screensaver_timer){ +</xsl:text> + <xsl:text> window.clearTimeout(screensaver_timer); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> screensaver_timer = window.setTimeout(() => { +</xsl:text> + <xsl:text> switch_page("ScreenSaver"); +</xsl:text> + <xsl:text> screensaver_timer = null; +</xsl:text> + <xsl:text> }, screensaver_delay*1000); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text>if(screensaver_delay) +</xsl:text> + <xsl:text> document.body.addEventListener('pointerdown', reset_screensaver_timer); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function detach_detachables() { +</xsl:text> + <xsl:text> +</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(page_switch_in_progress){ +</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> page_switch_in_progress = true; +</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> else if(page_index == undefined){ +</xsl:text> + <xsl:text> [page_name, page_index] = page_name.split('@') +</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> else if(typeof(page_index) == "string") { +</xsl:text> + <xsl:text> let hmitree_node = hmitree_nodes[page_index]; +</xsl:text> + <xsl:text> if(hmitree_node !== undefined){ +</xsl:text> + <xsl:text> let [int_index, hmiclass] = hmitree_node; +</xsl:text> + <xsl:text> if(hmiclass == new_desc.page_class) +</xsl:text> + <xsl:text> page_index = int_index; +</xsl:text> + <xsl:text> else +</xsl:text> + <xsl:text> page_index = new_desc.page_index; +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> page_index = new_desc.page_index; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <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> const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> const container_id = page_name + (page_index != undefined ? page_index : ""); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> update_subscriptions(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> current_subscribed_page = page_name; +</xsl:text> + <xsl:text> current_page_index = page_index; +</xsl:text> + <xsl:text> let page_node; +</xsl:text> + <xsl:text> if(page_index != undefined){ +</xsl:text> + <xsl:text> page_node = hmitree_paths[page_index]; +</xsl:text> + <xsl:text> }else{ +</xsl:text> + <xsl:text> page_node = ""; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> apply_hmi_value(page_node_local_index, page_node); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> jumps_need_update = true; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> requestHMIAnimation(); +</xsl:text> + <xsl:text> let [last_page_name, last_page_index] = jump_history[jump_history.length-1]; +</xsl:text> + <xsl:text> if(last_page_name != page_name || last_page_index != page_index){ +</xsl:text> + <xsl:text> jump_history.push([page_name, page_index]); +</xsl:text> + <xsl:text> if(jump_history.length > 42) +</xsl:text> + <xsl:text> jump_history.shift(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> apply_hmi_value(current_page_var_index, page_index == undefined +</xsl:text> + <xsl:text> ? page_name +</xsl:text> + <xsl:text> : page_name + "@" + hmitree_paths[page_index]); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> return true; +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function switch_visible_page(page_name) { +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let old_desc = page_desc[current_visible_page]; +</xsl:text> + <xsl:text> let new_desc = page_desc[page_name]; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(old_desc){ +</xsl:text> + <xsl:text> for(let eltid in old_desc.required_detachables){ +</xsl:text> + <xsl:text> if(!(eltid in new_desc.required_detachables)){ +</xsl:text> + <xsl:text> let [element, parent] = old_desc.required_detachables[eltid]; +</xsl:text> + <xsl:text> parent.removeChild(element); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> for(let eltid in new_desc.required_detachables){ +</xsl:text> + <xsl:text> if(!(eltid in old_desc.required_detachables)){ +</xsl:text> + <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; +</xsl:text> + <xsl:text> parent.appendChild(element); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> }else{ +</xsl:text> + <xsl:text> for(let eltid in new_desc.required_detachables){ +</xsl:text> + <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; +</xsl:text> + <xsl:text> parent.appendChild(element); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); +</xsl:text> + <xsl:text> current_visible_page = page_name; +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>/* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */ +</xsl:text> + <xsl:text>function getAbsoluteCTM(element){ +</xsl:text> + <xsl:text> var height = svg_root.height.baseVal.value, +</xsl:text> + <xsl:text> width = svg_root.width.baseVal.value, +</xsl:text> + <xsl:text> viewBoxRect = svg_root.viewBox.baseVal, +</xsl:text> + <xsl:text> vHeight = viewBoxRect.height, +</xsl:text> + <xsl:text> vWidth = viewBoxRect.width; +</xsl:text> + <xsl:text> if(!vWidth || !vHeight){ +</xsl:text> + <xsl:text> return element.getCTM(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> var sH = height/vHeight, +</xsl:text> + <xsl:text> sW = width/vWidth, +</xsl:text> + <xsl:text> matrix = svg_root.createSVGMatrix(); +</xsl:text> + <xsl:text> matrix.a = sW; +</xsl:text> + <xsl:text> matrix.d = sH +</xsl:text> + <xsl:text> var realCTM = element.getCTM().multiply(matrix.inverse()); +</xsl:text> + <xsl:text> realCTM.e = realCTM.e/sW + viewBoxRect.x; +</xsl:text> + <xsl:text> realCTM.f = realCTM.f/sH + viewBoxRect.y; +</xsl:text> + <xsl:text> return realCTM; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function apply_reference_frames(){ +</xsl:text> + <xsl:text> const matches = svg_root.querySelectorAll("g[svghmi_x_offset]"); +</xsl:text> + <xsl:text> matches.forEach((group) => { +</xsl:text> + <xsl:text> let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset"))); +</xsl:text> + <xsl:text> let ctm = getAbsoluteCTM(group); +</xsl:text> + <xsl:text> // zero translation part of CTM +</xsl:text> + <xsl:text> // to only apply rotation/skewing to offset vector +</xsl:text> + <xsl:text> ctm.e = 0; +</xsl:text> + <xsl:text> ctm.f = 0; +</xsl:text> + <xsl:text> let invctm = ctm.inverse(); +</xsl:text> + <xsl:text> let vect = new DOMPoint(x, y); +</xsl:text> + <xsl:text> let newvect = vect.matrixTransform(invctm); +</xsl:text> + <xsl:text> let transform = svg_root.createSVGTransform(); +</xsl:text> + <xsl:text> transform.setTranslate(newvect.x, newvect.y); +</xsl:text> + <xsl:text> group.transform.baseVal.appendItem(transform); +</xsl:text> + <xsl:text> ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset")); +</xsl:text> + <xsl:text> }); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// prepare SVG +</xsl:text> + <xsl:text>apply_reference_frames(); +</xsl:text> + <xsl:text>init_widgets(); +</xsl:text> + <xsl:text>detach_detachables(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// show main page +</xsl:text> + <xsl:text>switch_page(default_page); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// initialize screensaver +</xsl:text> + <xsl:text>reset_screensaver_timer(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var reconnect_delay = 0; +</xsl:text> + <xsl:text>var periodic_reconnect_timer; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// Once connection established +</xsl:text> + <xsl:text>function ws_onopen(evt) { +</xsl:text> + <xsl:text> // Work around memory leak with websocket on QtWebEngine +</xsl:text> + <xsl:text> // reconnect every hour to force deallocate websocket garbage +</xsl:text> + <xsl:text> if(window.navigator.userAgent.includes("QtWebEngine")){ +</xsl:text> + <xsl:text> if(periodic_reconnect_timer){ +</xsl:text> + <xsl:text> window.clearTimeout(periodic_reconnect_timer); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> periodic_reconnect_timer = window.setTimeout(() => { +</xsl:text> + <xsl:text> ws.close(); +</xsl:text> + <xsl:text> periodic_reconnect_timer = null; +</xsl:text> + <xsl:text> }, 3600000); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // forget subscriptions remotely +</xsl:text> + <xsl:text> send_reset(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // forget earlier subscriptions locally +</xsl:text> + <xsl:text> reset_subscription_periods(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // update PLC about subscriptions and current page +</xsl:text> + <xsl:text> switch_page(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // at first try reconnect immediately +</xsl:text> + <xsl:text> reconnect_delay = 1; +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function ws_onclose(evt) { +</xsl:text> + <xsl:text> console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in "+reconnect_delay+"ms."); +</xsl:text> + <xsl:text> ws = null; +</xsl:text> + <xsl:text> // reconect +</xsl:text> + <xsl:text> // TODO : add visible notification while waiting for reload +</xsl:text> + <xsl:text> window.setTimeout(create_ws, reconnect_delay); +</xsl:text> + <xsl:text> reconnect_delay += 500; +</xsl:text> + <xsl:text>}; +</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> @@ -11297,1131 +12566,23 @@ </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>// Called on requestAnimationFrame, modifies DOM -</xsl:text> - <xsl:text>var requestAnimationFrameID = null; -</xsl:text> - <xsl:text>function animate() { -</xsl:text> - <xsl:text> let rearm = true; -</xsl:text> - <xsl:text> do{ -</xsl:text> - <xsl:text> if(page_fading == "pending" || page_fading == "forced"){ -</xsl:text> - <xsl:text> if(page_fading == "pending") -</xsl:text> - <xsl:text> svg_root.classList.add("fade-out-page"); -</xsl:text> - <xsl:text> page_fading = "in_progress"; -</xsl:text> - <xsl:text> if(page_fading_args.length) -</xsl:text> - <xsl:text> setTimeout(function(){ -</xsl:text> - <xsl:text> switch_page(...page_fading_args); -</xsl:text> - <xsl:text> },1); -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // Do the page swith if pending -</xsl:text> - <xsl:text> if(page_switch_in_progress){ -</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> page_switch_in_progress = false; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(page_fading == "in_progress"){ -</xsl:text> - <xsl:text> svg_root.classList.remove("fade-out-page"); -</xsl:text> - <xsl:text> page_fading = "off"; -</xsl:text> - <xsl:text> } -</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> -</xsl:text> - <xsl:text> pending_widget_animates.forEach(widget => widget._animate()); -</xsl:text> - <xsl:text> pending_widget_animates = []; -</xsl:text> - <xsl:text> rearm = false; -</xsl:text> - <xsl:text> } while(0); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> requestAnimationFrameID = null; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(rearm) requestHMIAnimation(); + <xsl:text>function create_ws(){ +</xsl:text> + <xsl:text> ws = new WebSocket(ws_url); +</xsl:text> + <xsl:text> ws.binaryType = 'arraybuffer'; +</xsl:text> + <xsl:text> ws.onmessage = ws_onmessage; +</xsl:text> + <xsl:text> ws.onclose = ws_onclose; +</xsl:text> + <xsl:text> ws.onopen = ws_onopen; </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> dispatch_value(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> -</xsl:text> - <xsl:text> // register for rendering on next frame, since there are updates -</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> -</xsl:text> - <xsl:text>var page_fading = "off"; -</xsl:text> - <xsl:text>var page_fading_args = "off"; -</xsl:text> - <xsl:text>function fading_page_switch(...args){ -</xsl:text> - <xsl:text> if(page_fading == "in_progress") -</xsl:text> - <xsl:text> page_fading = "forced"; -</xsl:text> - <xsl:text> else -</xsl:text> - <xsl:text> page_fading = "pending"; -</xsl:text> - <xsl:text> page_fading_args = args; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> requestHMIAnimation(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text>document.body.style.backgroundColor = "black"; -</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> fading_page_switch(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, blank) { -</xsl:text> - <xsl:text> str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":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>// 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 setup_lang(){ -</xsl:text> - <xsl:text> let current_lang = cache[lang_local_index]; -</xsl:text> - <xsl:text> let new_lang = switch_langnum(current_lang); -</xsl:text> - <xsl:text> if(current_lang != new_lang){ -</xsl:text> - <xsl:text> apply_hmi_value(lang_local_index, new_lang); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>setup_lang(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function update_subscriptions() { -</xsl:text> - <xsl:text> let delta = []; -</xsl:text> - <xsl:text> for(let index in subscriptions){ -</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 = get_subscription_period(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> set_subscription_period(index, new_period); -</xsl:text> - <xsl:text> if(index <= last_remote_index){ -</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> } -</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> if(index > last_remote_index){ -</xsl:text> - <xsl:text> dispatch_value(index, value); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(persistent_indexes.has(index)){ -</xsl:text> - <xsl:text> let varname = persistent_indexes.get(index); -</xsl:text> - <xsl:text> document.cookie = varname+"="+value+"; max-age=3153600000"; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> return; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</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> // Similarly to previous comment, taking decision to update based -</xsl:text> - <xsl:text> // on cache content is bad and can lead to inconsistency -</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>const quotes = {"'":null, '"':null}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function eval_operation_string(old_val, 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; -</xsl:text> - <xsl:text> if(opstr[1] in quotes){ -</xsl:text> - <xsl:text> if(opstr.length < 3) -</xsl:text> - <xsl:text> return undefined; -</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 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> 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>var page_node_local_index = hmi_local_index("page_node"); -</xsl:text> - <xsl:text>var page_switch_in_progress = false; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function toggleFullscreen() { -</xsl:text> - <xsl:text> let elem = document.documentElement; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if (!document.fullscreenElement) { -</xsl:text> - <xsl:text> elem.requestFullscreen().catch(err => { -</xsl:text> - <xsl:text> console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); -</xsl:text> - <xsl:text> }); -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> document.exitFullscreen(); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function prepare_svg() { -</xsl:text> - <xsl:text> // prevents context menu from appearing on right click and long touch -</xsl:text> - <xsl:text> document.body.addEventListener('contextmenu', e => { -</xsl:text> - <xsl:text> toggleFullscreen(); -</xsl:text> - <xsl:text> e.preventDefault(); -</xsl:text> - <xsl:text> }); -</xsl:text> - <xsl:text> -</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(page_switch_in_progress){ -</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> page_switch_in_progress = true; -</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> else if(page_index == undefined){ -</xsl:text> - <xsl:text> [page_name, page_index] = page_name.split('@') -</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> else if(typeof(page_index) == "string") { -</xsl:text> - <xsl:text> let hmitree_node = hmitree_nodes[page_index]; -</xsl:text> - <xsl:text> if(hmitree_node !== undefined){ -</xsl:text> - <xsl:text> let [int_index, hmiclass] = hmitree_node; -</xsl:text> - <xsl:text> if(hmiclass == new_desc.page_class) -</xsl:text> - <xsl:text> page_index = int_index; -</xsl:text> - <xsl:text> else -</xsl:text> - <xsl:text> page_index = new_desc.page_index; -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> page_index = new_desc.page_index; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <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> const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> const container_id = page_name + (page_index != undefined ? page_index : ""); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> update_subscriptions(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> current_subscribed_page = page_name; -</xsl:text> - <xsl:text> current_page_index = page_index; -</xsl:text> - <xsl:text> let page_node; -</xsl:text> - <xsl:text> if(page_index != undefined){ -</xsl:text> - <xsl:text> page_node = hmitree_paths[page_index]; -</xsl:text> - <xsl:text> }else{ -</xsl:text> - <xsl:text> page_node = ""; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> apply_hmi_value(page_node_local_index, page_node); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> jumps_need_update = true; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> requestHMIAnimation(); -</xsl:text> - <xsl:text> jump_history.push([page_name, page_index]); -</xsl:text> - <xsl:text> if(jump_history.length > 42) -</xsl:text> - <xsl:text> jump_history.shift(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> apply_hmi_value(current_page_var_index, page_index == undefined -</xsl:text> - <xsl:text> ? page_name -</xsl:text> - <xsl:text> : page_name + "@" + hmitree_paths[page_index]); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> return true; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function switch_visible_page(page_name) { -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> let old_desc = page_desc[current_visible_page]; -</xsl:text> - <xsl:text> let new_desc = page_desc[page_name]; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(old_desc){ -</xsl:text> - <xsl:text> for(let eltid in old_desc.required_detachables){ -</xsl:text> - <xsl:text> if(!(eltid in new_desc.required_detachables)){ -</xsl:text> - <xsl:text> let [element, parent] = old_desc.required_detachables[eltid]; -</xsl:text> - <xsl:text> parent.removeChild(element); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> for(let eltid in new_desc.required_detachables){ -</xsl:text> - <xsl:text> if(!(eltid in old_desc.required_detachables)){ -</xsl:text> - <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; -</xsl:text> - <xsl:text> parent.appendChild(element); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> }else{ -</xsl:text> - <xsl:text> for(let eltid in new_desc.required_detachables){ -</xsl:text> - <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; -</xsl:text> - <xsl:text> parent.appendChild(element); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); -</xsl:text> - <xsl:text> current_visible_page = page_name; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>/* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */ -</xsl:text> - <xsl:text>function getAbsoluteCTM(element){ -</xsl:text> - <xsl:text> var height = svg_root.height.baseVal.value, -</xsl:text> - <xsl:text> width = svg_root.width.baseVal.value, -</xsl:text> - <xsl:text> viewBoxRect = svg_root.viewBox.baseVal, -</xsl:text> - <xsl:text> vHeight = viewBoxRect.height, -</xsl:text> - <xsl:text> vWidth = viewBoxRect.width; -</xsl:text> - <xsl:text> if(!vWidth || !vHeight){ -</xsl:text> - <xsl:text> return element.getCTM(); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> var sH = height/vHeight, -</xsl:text> - <xsl:text> sW = width/vWidth, -</xsl:text> - <xsl:text> matrix = svg_root.createSVGMatrix(); -</xsl:text> - <xsl:text> matrix.a = sW; -</xsl:text> - <xsl:text> matrix.d = sH -</xsl:text> - <xsl:text> var realCTM = element.getCTM().multiply(matrix.inverse()); -</xsl:text> - <xsl:text> realCTM.e = realCTM.e/sW + viewBoxRect.x; -</xsl:text> - <xsl:text> realCTM.f = realCTM.f/sH + viewBoxRect.y; -</xsl:text> - <xsl:text> return realCTM; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function apply_reference_frames(){ -</xsl:text> - <xsl:text> const matches = svg_root.querySelectorAll("g[svghmi_x_offset]"); -</xsl:text> - <xsl:text> matches.forEach((group) => { -</xsl:text> - <xsl:text> let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset"))); -</xsl:text> - <xsl:text> let ctm = getAbsoluteCTM(group); -</xsl:text> - <xsl:text> // zero translation part of CTM -</xsl:text> - <xsl:text> // to only apply rotation/skewing to offset vector -</xsl:text> - <xsl:text> ctm.e = 0; -</xsl:text> - <xsl:text> ctm.f = 0; -</xsl:text> - <xsl:text> let invctm = ctm.inverse(); -</xsl:text> - <xsl:text> let vect = new DOMPoint(x, y); -</xsl:text> - <xsl:text> let newvect = vect.matrixTransform(invctm); -</xsl:text> - <xsl:text> let transform = svg_root.createSVGTransform(); -</xsl:text> - <xsl:text> transform.setTranslate(newvect.x, newvect.y); -</xsl:text> - <xsl:text> group.transform.baseVal.appendItem(transform); -</xsl:text> - <xsl:text> ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset")); -</xsl:text> - <xsl:text> }); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>// Once connection established -</xsl:text> - <xsl:text>ws.onopen = function (evt) { -</xsl:text> - <xsl:text> apply_reference_frames(); -</xsl:text> - <xsl:text> init_widgets(); -</xsl:text> - <xsl:text> send_reset(); -</xsl:text> - <xsl:text> // show main page -</xsl:text> - <xsl:text> prepare_svg(); -</xsl:text> - <xsl:text> switch_page(default_page); -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>ws.onclose = function (evt) { -</xsl:text> - <xsl:text> // TODO : add visible notification while waiting for reload -</xsl:text> - <xsl:text> console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in 10s."); -</xsl:text> - <xsl:text> // TODO : re-enable auto reload when not in debug -</xsl:text> - <xsl:text> //window.setTimeout(() => location.reload(true), 10000); -</xsl:text> - <xsl:text> alert("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+"."); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>}; + <xsl:text>create_ws() </xsl:text> <xsl:text> </xsl:text> diff -r b5c6bb72bfc9 -r e0d6f5f0dcc2 svghmi/svghmi.js --- a/svghmi/svghmi.js Thu Nov 03 17:43:30 2022 +0100 +++ b/svghmi/svghmi.js Fri Nov 04 17:38:37 2022 +0100 @@ -23,13 +23,6 @@ // Open WebSocket to relative "/ws" address var has_watchdog = window.location.hash == "#watchdog"; -var ws_url = - window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') - + '?mode=' + (has_watchdog ? "watchdog" : "multiclient"); - -var ws = new WebSocket(ws_url); -ws.binaryType = 'arraybuffer'; - const dvgetters = { INT: (dv,offset) => [dv.getInt16(offset, true), 2], BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], @@ -98,7 +91,7 @@ // 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) { +function ws_onmessage(evt) { let data = evt.data; let dv = new DataView(data); @@ -140,16 +133,18 @@ hmi_hash_u8 = new Uint8Array(hmi_hash); +var ws = null; + function send_blob(data) { - if(data.length > 0) { + if(ws && data.length > 0) { ws.send(new Blob([hmi_hash_u8].concat(data))); }; }; const typedarray_types = { INT: (number) => new Int16Array([number]), - BOOL: (truth) => new Int16Array([truth]), - NODE: (truth) => new Int16Array([truth]), + BOOL: (truth) => new Int8Array([truth]), + NODE: (truth) => new Int8Array([truth]), REAL: (number) => new Float32Array([number]), STRING: (str) => { // beremiz default string max size is 128 @@ -199,6 +194,11 @@ } } +function reset_subscription_periods() { + for(let index in subscriptions) + subscriptions[index][1] = 0; +} + if(has_watchdog){ // artificially subscribe the watchdog widget to "/heartbeat" hmi variable // Since dispatch directly calls change_hmi_value, @@ -298,6 +298,10 @@ function update_subscriptions() { let delta = []; + if(!ws) + // dont' change subscriptions if not connected + return; + for(let index in subscriptions){ let widgets = subscribers(index); @@ -418,12 +422,26 @@ } } -function prepare_svg() { - // prevents context menu from appearing on right click and long touch - document.body.addEventListener('contextmenu', e => { - toggleFullscreen(); - e.preventDefault(); - }); +// prevents context menu from appearing on right click and long touch +document.body.addEventListener('contextmenu', e => { + toggleFullscreen(); + e.preventDefault(); +}); + +var screensaver_timer = null; +function reset_screensaver_timer() { + if(screensaver_timer){ + window.clearTimeout(screensaver_timer); + } + screensaver_timer = window.setTimeout(() => { + switch_page("ScreenSaver"); + screensaver_timer = null; + }, screensaver_delay*1000); +} +if(screensaver_delay) + document.body.addEventListener('pointerdown', reset_screensaver_timer); + +function detach_detachables() { for(let eltid in detachable_elements){ let [element,parent] = detachable_elements[eltid]; @@ -492,9 +510,12 @@ jumps_need_update = true; requestHMIAnimation(); - jump_history.push([page_name, page_index]); - if(jump_history.length > 42) - jump_history.shift(); + let [last_page_name, last_page_index] = jump_history[jump_history.length-1]; + if(last_page_name != page_name || last_page_index != page_index){ + jump_history.push([page_name, page_index]); + if(jump_history.length > 42) + jump_history.shift(); + } apply_hmi_value(current_page_var_index, page_index == undefined ? page_name @@ -572,24 +593,69 @@ }); } +// prepare SVG +apply_reference_frames(); +init_widgets(); +detach_detachables(); + +// show main page +switch_page(default_page); + +// initialize screensaver +reset_screensaver_timer(); + +var reconnect_delay = 0; +var periodic_reconnect_timer; + // Once connection established -ws.onopen = function (evt) { - apply_reference_frames(); - init_widgets(); +function ws_onopen(evt) { + // Work around memory leak with websocket on QtWebEngine + // reconnect every hour to force deallocate websocket garbage + if(window.navigator.userAgent.includes("QtWebEngine")){ + if(periodic_reconnect_timer){ + window.clearTimeout(periodic_reconnect_timer); + } + periodic_reconnect_timer = window.setTimeout(() => { + ws.close(); + periodic_reconnect_timer = null; + }, 3600000); + } + + // forget subscriptions remotely send_reset(); - // show main page - prepare_svg(); - switch_page(default_page); -}; - -ws.onclose = function (evt) { + + // forget earlier subscriptions locally + reset_subscription_periods(); + + // update PLC about subscriptions and current page + switch_page(); + + // at first try reconnect immediately + reconnect_delay = 1; +}; + +function ws_onclose(evt) { + console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in "+reconnect_delay+"ms."); + ws = null; + // reconect // TODO : add visible notification while waiting for reload - console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in 10s."); - // TODO : re-enable auto reload when not in debug - //window.setTimeout(() => location.reload(true), 10000); - alert("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+"."); - -}; + window.setTimeout(create_ws, reconnect_delay); + reconnect_delay += 500; +}; + +var ws_url = + window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') + + '?mode=' + (has_watchdog ? "watchdog" : "multiclient"); + +function create_ws(){ + ws = new WebSocket(ws_url); + ws.binaryType = 'arraybuffer'; + ws.onmessage = ws_onmessage; + ws.onclose = ws_onclose; + ws.onopen = ws_onopen; +} + +create_ws() const xmlns = "http://www.w3.org/2000/svg"; var edit_callback; diff -r b5c6bb72bfc9 -r e0d6f5f0dcc2 svghmi/svghmi.py --- a/svghmi/svghmi.py Thu Nov 03 17:43:30 2022 +0100 +++ b/svghmi/svghmi.py Fri Nov 04 17:38:37 2022 +0100 @@ -641,19 +641,29 @@ svghmi_cmds[thing] = ( "Popen(" + repr(shlex.split(given_command.format(**svghmi_options))) + - ")") if given_command else "pass # no command given" + ")") if given_command else "None # no command given" runtimefile_path = os.path.join(buildpath, "runtime_%s_svghmi_.py" % location_str) runtimefile = open(runtimefile_path, 'w') runtimefile.write(""" -# TODO : multiple watchdog (one for each svghmi instance) +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# generated by beremiz/svghmi/svghmi.py + +browser_proc = None + def svghmi_{location}_watchdog_trigger(): - {svghmi_cmds[Watchdog]} + global browser_proc + restart_proc = {svghmi_cmds[Watchdog]} + waitpid_timeout(restart_proc, "SVGHMI watchdog triggered command") + waitpid_timeout(browser_proc, "SVGHMI browser process") + browser_proc = None max_svghmi_sessions = {maxConnections_total} def _runtime_{location}_svghmi_start(): - global svghmi_watchdog, svghmi_servers + global svghmi_watchdog, svghmi_servers, browser_proc srv = svghmi_servers.get("{interface}:{port}", None) if srv is not None: @@ -678,7 +688,7 @@ path_list.append("{path}") - {svghmi_cmds[Start]} + browser_proc = {svghmi_cmds[Start]} if {enable_watchdog}: if svghmi_watchdog is None: @@ -691,7 +701,7 @@ def _runtime_{location}_svghmi_stop(): - global svghmi_watchdog, svghmi_servers + global svghmi_watchdog, svghmi_servers, browser_proc if svghmi_watchdog is not None: svghmi_watchdog.cancel() @@ -707,7 +717,10 @@ svghmi_listener.stopListening() svghmi_servers.pop("{interface}:{port}") - {svghmi_cmds[Stop]} + stop_proc = {svghmi_cmds[Stop]} + waitpid_timeout(stop_proc, "SVGHMI stop command") + waitpid_timeout(browser_proc, "SVGHMI browser process") + browser_proc = None """.format(location=location_str, xhtml=target_fname, diff -r b5c6bb72bfc9 -r e0d6f5f0dcc2 svghmi/svghmi_server.py --- a/svghmi/svghmi_server.py Thu Nov 03 17:43:30 2022 +0100 +++ b/svghmi/svghmi_server.py Fri Nov 04 17:38:37 2022 +0100 @@ -8,6 +8,7 @@ from __future__ import absolute_import import errno from threading import RLock, Timer +import os, time try: from runtime.spawn_subprocess import Popen @@ -23,6 +24,9 @@ from autobahn.websocket.protocol import WebSocketProtocol from autobahn.twisted.resource import WebSocketResource +from runtime.loglevels import LogLevelsDict +from runtime import GetPLCObjectSingleton + max_svghmi_sessions = None svghmi_watchdog = None @@ -299,3 +303,21 @@ render_HEAD = render_GET +def waitpid_timeout(proc, helpstr="", timeout = 3): + if proc is None: + return + def waitpid_timeout_loop(pid=proc.pid, timeout = timeout): + try: + while os.waitpid(pid,os.WNOHANG) == (0,0): + time.sleep(1) + timeout = timeout - 1 + if not timeout: + GetPLCObjectSingleton().LogMessage( + LogLevelsDict["WARNING"], + "Timeout waiting for {} PID: {}".format(helpstr, str(pid))) + break + except OSError: + # workaround exception "OSError: [Errno 10] No child processes" + pass + Thread(target=waitpid_timeout_loop, name="Zombie hunter").start() + diff -r b5c6bb72bfc9 -r e0d6f5f0dcc2 svghmi/widget_back.ysl2 --- a/svghmi/widget_back.ysl2 Thu Nov 03 17:43:30 2022 +0100 +++ b/svghmi/widget_back.ysl2 Fri Nov 04 17:38:37 2022 +0100 @@ -9,17 +9,20 @@ shortdesc > Jump to previous page } -// TODO: use es6 widget_class("Back") || on_click(evt) { if(jump_history.length > 1){ - jump_history.pop(); - let [page_name, index] = jump_history.pop(); + let page_name, index; + do { + jump_history.pop(); // forget current page + if(jump_history.length == 0) return; + [page_name, index] = jump_history[jump_history.length-1]; + } while(page_name == "ScreenSaver") // never go back to ScreenSaver switch_page(page_name, index); } } init() { - this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); + this.element.onclick = this.on_click.bind(this); } || diff -r b5c6bb72bfc9 -r e0d6f5f0dcc2 targets/plc_debug.c --- a/targets/plc_debug.c Thu Nov 03 17:43:30 2022 +0100 +++ b/targets/plc_debug.c Fri Nov 04 17:38:37 2022 +0100 @@ -154,8 +154,6 @@ UnpackVar(dsc, &value_p, NULL, &size); - printf("Reminding %%d %%ld \n", retain_list_collect_cursor, size); - /* if buffer not full */ Remind(retain_offset, size, value_p); /* increment cursor according size*/ @@ -211,8 +209,6 @@ retain_list_collect_cursor++; } - printf("Retain size %%d \n", retain_size); - return retain_size; } diff -r b5c6bb72bfc9 -r e0d6f5f0dcc2 tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg Thu Nov 03 17:43:30 2022 +0100 +++ b/tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg Fri Nov 04 17:38:37 2022 +0100 @@ -137,8 +137,8 @@ showgrid="false" units="px" inkscape:zoom="0.40092403" - inkscape:cx="323.58553" - inkscape:cy="-56.756946" + inkscape:cx="2333.0807" + inkscape:cy="1015.6842" inkscape:window-width="1600" inkscape:window-height="836" inkscape:window-x="0" @@ -6543,6 +6543,31 @@ style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px">Home</tspan></text> </g> </g> + <g + transform="translate(-519.60999,-498.54925)" + id="g1315-6" + inkscape:label="HMI:Back"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:#ff6600;stroke-width:5.20923424;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect943-3-2" + width="114.49817" + height="52.074696" + x="1682.5072" + y="-298.84613" + ry="23.177595" + rx="26.820074" /> + <text + id="text949-6-6" + y="-260.38251" + x="1737.7013" + style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;display:inline;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:1.04184675px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + xml:space="preserve"><tspan + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:1.04184675px" + y="-260.38251" + x="1737.7013" + id="tspan947-2-1" + sodipodi:role="line">Back</tspan></text> + </g> </g> <text xml:space="preserve" @@ -8557,4 +8582,294 @@ sodipodi:role="line">Home</tspan></text> </g> </g> + <rect + y="-800" + x="1480" + height="720" + width="1280" + id="rect1282" + style="color:#000000;fill:#000000" + inkscape:label="HMI:Page:ScreenSaver:12" /> + <g + id="g1280" + transform="matrix(4.3157423,0,0,4.3157423,1737.4823,-785.25938)" + style="stroke-width:0.23170985" + inkscape:label="anim"> + <circle + id="circle1260" + style="fill:#ff6600;stroke-width:0.25819258px;font-variant-east_asian:normal;opacity:1;vector-effect:none;fill-opacity:1;stroke:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none" + r="50" + cy="80" + cx="80" /> + <g + id="g1264" + transform="matrix(0.866,-0.5,0.25,0.433,80,80)" + style="stroke-width:0.23170985"> + <path + id="path1262" + d="M 0,70 A 65,70 0 0 0 65,0 5,5 0 0 1 75,0 75,70 0 0 1 0,70 Z" + inkscape:connector-curvature="0" + style="fill:#ffffff;stroke-width:0.23170985"> + <animateTransform + repeatCount="indefinite" + dur="21s" + to="0 0 0" + from="360 0 0" + type="rotate" + attributeName="transform" /> + </path> + </g> + <path + id="path1266-3" + style="fill:#ff6600;stroke-width:0.25819826px;font-variant-east_asian:normal;opacity:1;vector-effect:none;fill-opacity:1;stroke:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none" + transform="matrix(0.866,-0.5,0.5,0.866,80,80)" + d="M 50,0 A 50,50 0 0 0 -50,0 Z" + inkscape:connector-curvature="0" /> + </g> + <g + id="g1315" + inkscape:label="HMI:Back" + transform="translate(0,80)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:#ff6600;stroke-width:5.20923424;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect943-3" + width="410.06546" + height="95.723877" + x="1536.5942" + y="-322.54138" + ry="23.177595" + rx="26.820074" /> + <text + id="text949-6" + y="-260.38251" + x="1737.7013" + style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;display:inline;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:1.04184675px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + xml:space="preserve"><tspan + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:1.04184675px" + y="-260.38251" + x="1737.7013" + id="tspan947-2" + sodipodi:role="line">Leave ScreenSaver</tspan></text> + </g> + <g + id="g4282-91" + inkscape:label="HMI:Jump:Home" + transform="translate(1466.2292,-1613.0769)"> + <g + id="g4274-2" + inkscape:label="button"> + <path + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:#ff6600;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 1217.4113,1410.4016 -22,24.5657 c -10.7925,12.0511 6.1317,35.5791 -13.5791,35.5791 h -174.2877 c -19.71078,0 -2.7866,-23.528 -13.57905,-35.5791 l -22,-24.5657 127.74845,-48.4334 z" + id="path4272-70" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cssssccc" /> + </g> + <g + id="g4280-9" + inkscape:label="text"> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;display:inline;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + x="1090.7626" + y="1436.9814" + id="text4278-36" + inkscape:label="home_jmp"><tspan + sodipodi:role="line" + id="tspan4276-0" + x="1090.7626" + y="1436.9814" + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px">Home</tspan></text> + </g> + </g> + <g + id="g1077-8" + inkscape:label="HMI:Jump:Conf" + transform="matrix(0.57180538,0,0,0.57180538,1065.1448,-867.17294)"> + <g + id="g1159-7" + inkscape:label="button"> + <rect + rx="35.579063" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:#ff6600;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect1020-9" + width="245.44583" + height="95.723877" + x="971.96545" + y="594.82263" + ry="35.579063" + inkscape:label="button" /> + </g> + <g + id="g1156-2" + inkscape:label="text"> + <text + inkscape:label="setting_jmp" + id="setting_jmp-0" + y="656.98151" + x="1090.7626" + style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;display:inline;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + xml:space="preserve"><tspan + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px" + y="656.98151" + x="1090.7626" + id="tspan1024-2" + sodipodi:role="line">Settings</tspan></text> + </g> + </g> + <g + transform="matrix(0.57180538,0,0,0.57180538,1346.4405,-1101.6314)" + inkscape:label="HMI:Jump:RelativePageTest@/PUMP0" + id="g1458-3"> + <g + inkscape:label="button" + id="g1450-7"> + <rect + rx="35.579063" + inkscape:label="button" + ry="35.579063" + y="594.82263" + x="971.96545" + height="95.723877" + width="245.44583" + id="rect1448-5" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:#ff6600;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + </g> + <g + inkscape:label="text" + id="g1456-9"> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;display:inline;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + x="1090.7626" + y="656.98151" + id="text1454-2" + inkscape:label="setting_jmp"><tspan + sodipodi:role="line" + x="1090.7626" + y="656.98151" + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px" + id="tspan1460-2">Pump 0</tspan></text> + </g> + </g> + <g + id="g1475-8" + inkscape:label="HMI:Jump:RelativePageTest@/PUMP1" + transform="matrix(0.57180538,0,0,0.57180538,1506.4405,-1101.6314)"> + <g + id="g1467-9" + inkscape:label="button"> + <rect + rx="35.579063" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:#ff6600;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect1464-7" + width="245.44583" + height="95.723877" + x="971.96545" + y="594.82263" + ry="35.579063" + inkscape:label="button" /> + </g> + <g + id="g1473-3" + inkscape:label="text"> + <text + inkscape:label="setting_jmp" + id="text1471-6" + y="656.98151" + x="1090.7626" + style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;display:inline;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + xml:space="preserve"><tspan + id="tspan1469-1" + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px" + y="656.98151" + x="1090.7626" + sodipodi:role="line">Pump 1</tspan><tspan + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px" + y="706.98151" + x="1090.7626" + sodipodi:role="line" + id="tspan1477-2" /></text> + </g> + </g> + <g + transform="matrix(0.57180538,0,0,0.57180538,1666.4405,-1101.6314)" + inkscape:label="HMI:Jump:RelativePageTest@/PUMP2" + id="g1491-9"> + <g + inkscape:label="button" + id="g1481-3"> + <rect + rx="35.579063" + inkscape:label="button" + ry="35.579063" + y="594.82263" + x="971.96545" + height="95.723877" + width="245.44583" + id="rect1479-1" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:#ff6600;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + </g> + <g + inkscape:label="text" + id="g1489-9"> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;display:inline;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + x="1090.7626" + y="656.98151" + id="text1487-4" + inkscape:label="setting_jmp"><tspan + sodipodi:role="line" + x="1090.7626" + y="656.98151" + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px" + id="tspan1493-7">Pump 2</tspan><tspan + id="tspan1485-8" + sodipodi:role="line" + x="1090.7626" + y="706.98151" + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px" /></text> + </g> + </g> + <g + id="g1509-4" + inkscape:label="HMI:Jump:RelativePageTest@/PUMP3" + transform="matrix(0.57180538,0,0,0.57180538,1826.4405,-1101.6314)"> + <g + id="g1499-5" + inkscape:label="button"> + <rect + rx="35.579063" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:#ff6600;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect1497-0" + width="245.44583" + height="95.723877" + x="971.96545" + y="594.82263" + ry="35.579063" + inkscape:label="button" /> + </g> + <g + id="g1507-3" + inkscape:label="text"> + <text + inkscape:label="setting_jmp" + id="text1505-6" + y="656.98151" + x="1090.7626" + style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;display:inline;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + xml:space="preserve"><tspan + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px" + y="656.98151" + x="1090.7626" + sodipodi:role="line" + id="tspan1511-1">Pump 3</tspan><tspan + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px" + y="706.98151" + x="1090.7626" + sodipodi:role="line" + id="tspan1503-0" /></text> + </g> + </g> </svg>