# HG changeset patch # User Edouard Tisserant # 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 @@ - + @@ -22,12 +22,12 @@ - + - + - + @@ -39,7 +39,7 @@ - LocalVar0 + PLCHMIVAR @@ -50,7 +50,7 @@ - LocalVar1 + LocalVar0 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 @@ "; + + + + + + + + ScreenSaver page has missing or malformed delay argument. + + + + + + null + + + + var screensaver_delay = + + ; + @@ -2907,9 +2928,17 @@ 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); @@ -2919,7 +2948,7 @@ init() { - this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); + this.element.onclick = this.on_click.bind(this); } @@ -11289,7 +11318,1247 @@ - var ws_url = + const dvgetters = { + + INT: (dv,offset) => [dv.getInt16(offset, true), 2], + + BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], + + NODE: (dv,offset) => [dv.getInt8(offset, true), 1], + + REAL: (dv,offset) => [dv.getFloat32(offset, true), 4], + + STRING: (dv, offset) => { + + const size = dv.getInt8(offset); + + return [ + + String.fromCharCode.apply(null, new Uint8Array( + + dv.buffer, /* original buffer */ + + offset + 1, /* string starts after size*/ + + size /* size of string */ + + )), size + 1]; /* total increment */ + + } + + }; + + + + // Called on requestAnimationFrame, modifies DOM + + var requestAnimationFrameID = null; + + function animate() { + + let rearm = true; + + do{ + + if(page_fading == "pending" || page_fading == "forced"){ + + if(page_fading == "pending") + + svg_root.classList.add("fade-out-page"); + + page_fading = "in_progress"; + + if(page_fading_args.length) + + setTimeout(function(){ + + switch_page(...page_fading_args); + + },1); + + break; + + } + + + + // Do the page swith if pending + + if(page_switch_in_progress){ + + if(current_subscribed_page != current_visible_page){ + + switch_visible_page(current_subscribed_page); + + } + + + + page_switch_in_progress = false; + + + + if(page_fading == "in_progress"){ + + svg_root.classList.remove("fade-out-page"); + + page_fading = "off"; + + } + + } + + + + if(jumps_need_update) update_jumps(); + + + + + + pending_widget_animates.forEach(widget => widget._animate()); + + pending_widget_animates = []; + + rearm = false; + + } while(0); + + + + requestAnimationFrameID = null; + + + + if(rearm) requestHMIAnimation(); + + } + + + + function requestHMIAnimation() { + + if(requestAnimationFrameID == null){ + + requestAnimationFrameID = window.requestAnimationFrame(animate); + + } + + } + + + + // Message reception handler + + // Hash is verified and HMI values updates resulting from binary parsing + + // are stored until browser can compute next frame, DOM is left untouched + + function ws_onmessage(evt) { + + + + let data = evt.data; + + let dv = new DataView(data); + + let i = 0; + + try { + + for(let hash_int of hmi_hash) { + + if(hash_int != dv.getUint8(i)){ + + throw new Error("Hash doesn't match"); + + }; + + i++; + + }; + + + + while(i < data.byteLength){ + + let index = dv.getUint32(i, true); + + i += 4; + + let iectype = hmitree_types[index]; + + if(iectype != undefined){ + + let dvgetter = dvgetters[iectype]; + + let [value, bytesize] = dvgetter(dv,i); + + dispatch_value(index, value); + + i += bytesize; + + } else { + + throw new Error("Unknown index "+index); + + } + + }; + + + + // register for rendering on next frame, since there are updates + + } catch(err) { + + // 1003 is for "Unsupported Data" + + // ws.close(1003, err.message); + + + + // TODO : remove debug alert ? + + alert("Error : "+err.message+"\nHMI will be reloaded."); + + + + // force reload ignoring cache + + location.reload(true); + + } + + }; + + + + hmi_hash_u8 = new Uint8Array(hmi_hash); + + + + var ws = null; + + + + function send_blob(data) { + + 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 Int8Array([truth]), + + NODE: (truth) => new Int8Array([truth]), + + REAL: (number) => new Float32Array([number]), + + STRING: (str) => { + + // beremiz default string max size is 128 + + str = str.slice(0,128); + + binary = new Uint8Array(str.length + 1); + + binary[0] = str.length; + + for(let i = 0; i < str.length; i++){ + + binary[i+1] = str.charCodeAt(i); + + } + + return binary; + + } + + /* TODO */ + + }; + + + + function send_reset() { + + send_blob(new Uint8Array([1])); /* reset = 1 */ + + }; + + + + var subscriptions = []; + + + + function subscribers(index) { + + let entry = subscriptions[index]; + + let res; + + if(entry == undefined){ + + res = new Set(); + + subscriptions[index] = [res,0]; + + }else{ + + [res, _ign] = entry; + + } + + return res + + } + + + + function get_subscription_period(index) { + + let entry = subscriptions[index]; + + if(entry == undefined) + + return 0; + + let [_ign, period] = entry; + + return period; + + } + + + + function set_subscription_period(index, period) { + + let entry = subscriptions[index]; + + if(entry == undefined){ + + subscriptions[index] = [new Set(), period]; + + } else { + + entry[1] = period; + + } + + } + + + + 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, + + // PLC will periodically send variable at given frequency + + subscribers(heartbeat_index).add({ + + /* type: "Watchdog", */ + + frequency: 1, + + indexes: [heartbeat_index], + + new_hmi_value: function(index, value, oldval) { + + apply_hmi_value(heartbeat_index, value+1); + + } + + }); + + } + + + + + + var page_fading = "off"; + + var page_fading_args = "off"; + + function fading_page_switch(...args){ + + if(page_fading == "in_progress") + + page_fading = "forced"; + + else + + page_fading = "pending"; + + page_fading_args = args; + + + + requestHMIAnimation(); + + + + } + + document.body.style.backgroundColor = "black"; + + + + // subscribe to per instance current page hmi variable + + // PLC must prefix page name with "!" for page switch to happen + + subscribers(current_page_var_index).add({ + + frequency: 1, + + indexes: [current_page_var_index], + + new_hmi_value: function(index, value, oldval) { + + if(value.startsWith("!")) + + fading_page_switch(value.slice(1)); + + } + + }); + + + + function svg_text_to_multiline(elt) { + + return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); + + } + + + + function multiline_to_svg_text(elt, str, blank) { + + str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); + + } + + + + function switch_langnum(langnum) { + + langnum = Math.max(0, Math.min(langs.length - 1, langnum)); + + + + for (let translation of translations) { + + let [objs, msgs] = translation; + + let msg = msgs[langnum]; + + for (let obj of objs) { + + multiline_to_svg_text(obj, msg); + + obj.setAttribute("lang",langnum); + + } + + } + + return langnum; + + } + + + + // backup original texts + + for (let translation of translations) { + + let [objs, msgs] = translation; + + msgs.unshift(svg_text_to_multiline(objs[0])); + + } + + + + var lang_local_index = hmi_local_index("lang"); + + var langcode_local_index = hmi_local_index("lang_code"); + + var langname_local_index = hmi_local_index("lang_name"); + + subscribers(lang_local_index).add({ + + indexes: [lang_local_index], + + new_hmi_value: function(index, value, oldval) { + + let current_lang = switch_langnum(value); + + let [langname,langcode] = langs[current_lang]; + + apply_hmi_value(langcode_local_index, langcode); + + apply_hmi_value(langname_local_index, langname); + + switch_page(); + + } + + }); + + + + // returns en_US, fr_FR or en_UK depending on selected language + + function get_current_lang_code(){ + + return cache[langcode_local_index]; + + } + + + + function setup_lang(){ + + let current_lang = cache[lang_local_index]; + + let new_lang = switch_langnum(current_lang); + + if(current_lang != new_lang){ + + apply_hmi_value(lang_local_index, new_lang); + + } + + } + + + + setup_lang(); + + + + function update_subscriptions() { + + let delta = []; + + if(!ws) + + // dont' change subscriptions if not connected + + return; + + + + for(let index in subscriptions){ + + let widgets = subscribers(index); + + + + // periods are in ms + + let previous_period = get_subscription_period(index); + + + + // subscribing with a zero period is unsubscribing + + let new_period = 0; + + if(widgets.size > 0) { + + let maxfreq = 0; + + for(let widget of widgets){ + + let wf = widget.frequency; + + if(wf != undefined && maxfreq < wf) + + maxfreq = wf; + + } + + + + if(maxfreq != 0) + + new_period = 1000/maxfreq; + + } + + + + if(previous_period != new_period) { + + set_subscription_period(index, new_period); + + if(index <= last_remote_index){ + + delta.push( + + new Uint8Array([2]), /* subscribe = 2 */ + + new Uint32Array([index]), + + new Uint16Array([new_period])); + + } + + } + + } + + send_blob(delta); + + }; + + + + function send_hmi_value(index, value) { + + if(index > last_remote_index){ + + dispatch_value(index, value); + + + + if(persistent_indexes.has(index)){ + + let varname = persistent_indexes.get(index); + + document.cookie = varname+"="+value+"; max-age=3153600000"; + + } + + + + return; + + } + + + + let iectype = hmitree_types[index]; + + let tobinary = typedarray_types[iectype]; + + send_blob([ + + new Uint8Array([0]), /* setval = 0 */ + + new Uint32Array([index]), + + tobinary(value)]); + + + + // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf + + // cache[index] = value; + + }; + + + + function apply_hmi_value(index, new_val) { + + // Similarly to previous comment, taking decision to update based + + // on cache content is bad and can lead to inconsistency + + /*let old_val = cache[index];*/ + + if(new_val != undefined /*&& old_val != new_val*/) + + send_hmi_value(index, new_val); + + return new_val; + + } + + + + const quotes = {"'":null, '"':null}; + + + + function eval_operation_string(old_val, opstr) { + + let op = opstr[0]; + + let given_val; + + if(opstr.length < 2) + + return undefined; + + if(opstr[1] in quotes){ + + if(opstr.length < 3) + + return undefined; + + if(opstr[opstr.length-1] == opstr[1]){ + + given_val = opstr.slice(2,opstr.length-1); + + } + + } else { + + given_val = Number(opstr.slice(1)); + + } + + let new_val; + + switch(op){ + + case "=": + + new_val = given_val; + + break; + + case "+": + + new_val = old_val + given_val; + + break; + + case "-": + + new_val = old_val - given_val; + + break; + + case "*": + + new_val = old_val * given_val; + + break; + + case "/": + + new_val = old_val / given_val; + + break; + + } + + return new_val; + + } + + + + var current_visible_page; + + var current_subscribed_page; + + var current_page_index; + + var page_node_local_index = hmi_local_index("page_node"); + + var page_switch_in_progress = false; + + + + function toggleFullscreen() { + + let elem = document.documentElement; + + + + if (!document.fullscreenElement) { + + elem.requestFullscreen().catch(err => { + + console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); + + }); + + } else { + + document.exitFullscreen(); + + } + + } + + + + // 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]; + + parent.removeChild(element); + + } + + }; + + + + function switch_page(page_name, page_index) { + + if(page_switch_in_progress){ + + /* page switch already going */ + + /* TODO LOG ERROR */ + + return false; + + } + + page_switch_in_progress = true; + + + + if(page_name == undefined) + + page_name = current_subscribed_page; + + else if(page_index == undefined){ + + [page_name, page_index] = page_name.split('@') + + } + + + + let old_desc = page_desc[current_subscribed_page]; + + let new_desc = page_desc[page_name]; + + + + if(new_desc == undefined){ + + /* TODO LOG ERROR */ + + return false; + + } + + + + if(page_index == undefined) + + page_index = new_desc.page_index; + + else if(typeof(page_index) == "string") { + + let hmitree_node = hmitree_nodes[page_index]; + + if(hmitree_node !== undefined){ + + let [int_index, hmiclass] = hmitree_node; + + if(hmiclass == new_desc.page_class) + + page_index = int_index; + + else + + page_index = new_desc.page_index; + + } else { + + page_index = new_desc.page_index; + + } + + } + + + + if(old_desc){ + + old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); + + } + + const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; + + + + const container_id = page_name + (page_index != undefined ? page_index : ""); + + + + new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); + + + + update_subscriptions(); + + + + current_subscribed_page = page_name; + + current_page_index = page_index; + + let page_node; + + if(page_index != undefined){ + + page_node = hmitree_paths[page_index]; + + }else{ + + page_node = ""; + + } + + apply_hmi_value(page_node_local_index, page_node); + + + + jumps_need_update = true; + + + + requestHMIAnimation(); + + 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 + + : page_name + "@" + hmitree_paths[page_index]); + + + + return true; + + }; + + + + function switch_visible_page(page_name) { + + + + let old_desc = page_desc[current_visible_page]; + + let new_desc = page_desc[page_name]; + + + + if(old_desc){ + + for(let eltid in old_desc.required_detachables){ + + if(!(eltid in new_desc.required_detachables)){ + + let [element, parent] = old_desc.required_detachables[eltid]; + + parent.removeChild(element); + + } + + } + + for(let eltid in new_desc.required_detachables){ + + if(!(eltid in old_desc.required_detachables)){ + + let [element, parent] = new_desc.required_detachables[eltid]; + + parent.appendChild(element); + + } + + } + + }else{ + + for(let eltid in new_desc.required_detachables){ + + let [element, parent] = new_desc.required_detachables[eltid]; + + parent.appendChild(element); + + } + + } + + + + svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); + + current_visible_page = page_name; + + }; + + + + /* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */ + + function getAbsoluteCTM(element){ + + var height = svg_root.height.baseVal.value, + + width = svg_root.width.baseVal.value, + + viewBoxRect = svg_root.viewBox.baseVal, + + vHeight = viewBoxRect.height, + + vWidth = viewBoxRect.width; + + if(!vWidth || !vHeight){ + + return element.getCTM(); + + } + + var sH = height/vHeight, + + sW = width/vWidth, + + matrix = svg_root.createSVGMatrix(); + + matrix.a = sW; + + matrix.d = sH + + var realCTM = element.getCTM().multiply(matrix.inverse()); + + realCTM.e = realCTM.e/sW + viewBoxRect.x; + + realCTM.f = realCTM.f/sH + viewBoxRect.y; + + return realCTM; + + } + + + + function apply_reference_frames(){ + + const matches = svg_root.querySelectorAll("g[svghmi_x_offset]"); + + matches.forEach((group) => { + + let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset"))); + + let ctm = getAbsoluteCTM(group); + + // zero translation part of CTM + + // to only apply rotation/skewing to offset vector + + ctm.e = 0; + + ctm.f = 0; + + let invctm = ctm.inverse(); + + let vect = new DOMPoint(x, y); + + let newvect = vect.matrixTransform(invctm); + + let transform = svg_root.createSVGTransform(); + + transform.setTranslate(newvect.x, newvect.y); + + group.transform.baseVal.appendItem(transform); + + ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset")); + + }); + + } + + + + // 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 + + 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(); + + + + // 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 + + window.setTimeout(create_ws, reconnect_delay); + + reconnect_delay += 500; + + }; + + + + var ws_url = window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') @@ -11297,1131 +12566,23 @@ - var ws = new WebSocket(ws_url); - - ws.binaryType = 'arraybuffer'; - - - - const dvgetters = { - - INT: (dv,offset) => [dv.getInt16(offset, true), 2], - - BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], - - NODE: (dv,offset) => [dv.getInt8(offset, true), 1], - - REAL: (dv,offset) => [dv.getFloat32(offset, true), 4], - - STRING: (dv, offset) => { - - const size = dv.getInt8(offset); - - return [ - - String.fromCharCode.apply(null, new Uint8Array( - - dv.buffer, /* original buffer */ - - offset + 1, /* string starts after size*/ - - size /* size of string */ - - )), size + 1]; /* total increment */ - - } - - }; - - - - // Called on requestAnimationFrame, modifies DOM - - var requestAnimationFrameID = null; - - function animate() { - - let rearm = true; - - do{ - - if(page_fading == "pending" || page_fading == "forced"){ - - if(page_fading == "pending") - - svg_root.classList.add("fade-out-page"); - - page_fading = "in_progress"; - - if(page_fading_args.length) - - setTimeout(function(){ - - switch_page(...page_fading_args); - - },1); - - break; - - } - - - - // Do the page swith if pending - - if(page_switch_in_progress){ - - if(current_subscribed_page != current_visible_page){ - - switch_visible_page(current_subscribed_page); - - } - - - - page_switch_in_progress = false; - - - - if(page_fading == "in_progress"){ - - svg_root.classList.remove("fade-out-page"); - - page_fading = "off"; - - } - - } - - - - if(jumps_need_update) update_jumps(); - - - - - - pending_widget_animates.forEach(widget => widget._animate()); - - pending_widget_animates = []; - - rearm = false; - - } while(0); - - - - requestAnimationFrameID = null; - - - - if(rearm) requestHMIAnimation(); + function create_ws(){ + + ws = new WebSocket(ws_url); + + ws.binaryType = 'arraybuffer'; + + ws.onmessage = ws_onmessage; + + ws.onclose = ws_onclose; + + ws.onopen = ws_onopen; } - function requestHMIAnimation() { - - if(requestAnimationFrameID == null){ - - requestAnimationFrameID = window.requestAnimationFrame(animate); - - } - - } - - - - // Message reception handler - - // Hash is verified and HMI values updates resulting from binary parsing - - // are stored until browser can compute next frame, DOM is left untouched - - ws.onmessage = function (evt) { - - - - let data = evt.data; - - let dv = new DataView(data); - - let i = 0; - - try { - - for(let hash_int of hmi_hash) { - - if(hash_int != dv.getUint8(i)){ - - throw new Error("Hash doesn't match"); - - }; - - i++; - - }; - - - - while(i < data.byteLength){ - - let index = dv.getUint32(i, true); - - i += 4; - - let iectype = hmitree_types[index]; - - if(iectype != undefined){ - - let dvgetter = dvgetters[iectype]; - - let [value, bytesize] = dvgetter(dv,i); - - dispatch_value(index, value); - - i += bytesize; - - } else { - - throw new Error("Unknown index "+index); - - } - - }; - - - - // register for rendering on next frame, since there are updates - - } catch(err) { - - // 1003 is for "Unsupported Data" - - // ws.close(1003, err.message); - - - - // TODO : remove debug alert ? - - alert("Error : "+err.message+"\nHMI will be reloaded."); - - - - // force reload ignoring cache - - location.reload(true); - - } - - }; - - - - hmi_hash_u8 = new Uint8Array(hmi_hash); - - - - function send_blob(data) { - - if(data.length > 0) { - - ws.send(new Blob([hmi_hash_u8].concat(data))); - - }; - - }; - - - - const typedarray_types = { - - INT: (number) => new Int16Array([number]), - - BOOL: (truth) => new Int16Array([truth]), - - NODE: (truth) => new Int16Array([truth]), - - REAL: (number) => new Float32Array([number]), - - STRING: (str) => { - - // beremiz default string max size is 128 - - str = str.slice(0,128); - - binary = new Uint8Array(str.length + 1); - - binary[0] = str.length; - - for(let i = 0; i < str.length; i++){ - - binary[i+1] = str.charCodeAt(i); - - } - - return binary; - - } - - /* TODO */ - - }; - - - - function send_reset() { - - send_blob(new Uint8Array([1])); /* reset = 1 */ - - }; - - - - var subscriptions = []; - - - - function subscribers(index) { - - let entry = subscriptions[index]; - - let res; - - if(entry == undefined){ - - res = new Set(); - - subscriptions[index] = [res,0]; - - }else{ - - [res, _ign] = entry; - - } - - return res - - } - - - - function get_subscription_period(index) { - - let entry = subscriptions[index]; - - if(entry == undefined) - - return 0; - - let [_ign, period] = entry; - - return period; - - } - - - - function set_subscription_period(index, period) { - - let entry = subscriptions[index]; - - if(entry == undefined){ - - subscriptions[index] = [new Set(), period]; - - } else { - - entry[1] = period; - - } - - } - - - - if(has_watchdog){ - - // artificially subscribe the watchdog widget to "/heartbeat" hmi variable - - // Since dispatch directly calls change_hmi_value, - - // PLC will periodically send variable at given frequency - - subscribers(heartbeat_index).add({ - - /* type: "Watchdog", */ - - frequency: 1, - - indexes: [heartbeat_index], - - new_hmi_value: function(index, value, oldval) { - - apply_hmi_value(heartbeat_index, value+1); - - } - - }); - - } - - - - - - var page_fading = "off"; - - var page_fading_args = "off"; - - function fading_page_switch(...args){ - - if(page_fading == "in_progress") - - page_fading = "forced"; - - else - - page_fading = "pending"; - - page_fading_args = args; - - - - requestHMIAnimation(); - - - - } - - document.body.style.backgroundColor = "black"; - - - - // subscribe to per instance current page hmi variable - - // PLC must prefix page name with "!" for page switch to happen - - subscribers(current_page_var_index).add({ - - frequency: 1, - - indexes: [current_page_var_index], - - new_hmi_value: function(index, value, oldval) { - - if(value.startsWith("!")) - - fading_page_switch(value.slice(1)); - - } - - }); - - - - function svg_text_to_multiline(elt) { - - return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); - - } - - - - function multiline_to_svg_text(elt, str, blank) { - - str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); - - } - - - - function switch_langnum(langnum) { - - langnum = Math.max(0, Math.min(langs.length - 1, langnum)); - - - - for (let translation of translations) { - - let [objs, msgs] = translation; - - let msg = msgs[langnum]; - - for (let obj of objs) { - - multiline_to_svg_text(obj, msg); - - obj.setAttribute("lang",langnum); - - } - - } - - return langnum; - - } - - - - // backup original texts - - for (let translation of translations) { - - let [objs, msgs] = translation; - - msgs.unshift(svg_text_to_multiline(objs[0])); - - } - - - - var lang_local_index = hmi_local_index("lang"); - - var langcode_local_index = hmi_local_index("lang_code"); - - var langname_local_index = hmi_local_index("lang_name"); - - subscribers(lang_local_index).add({ - - indexes: [lang_local_index], - - new_hmi_value: function(index, value, oldval) { - - let current_lang = switch_langnum(value); - - let [langname,langcode] = langs[current_lang]; - - apply_hmi_value(langcode_local_index, langcode); - - apply_hmi_value(langname_local_index, langname); - - switch_page(); - - } - - }); - - - - // returns en_US, fr_FR or en_UK depending on selected language - - function get_current_lang_code(){ - - return cache[langcode_local_index]; - - } - - - - function setup_lang(){ - - let current_lang = cache[lang_local_index]; - - let new_lang = switch_langnum(current_lang); - - if(current_lang != new_lang){ - - apply_hmi_value(lang_local_index, new_lang); - - } - - } - - - - setup_lang(); - - - - function update_subscriptions() { - - let delta = []; - - for(let index in subscriptions){ - - let widgets = subscribers(index); - - - - // periods are in ms - - let previous_period = get_subscription_period(index); - - - - // subscribing with a zero period is unsubscribing - - let new_period = 0; - - if(widgets.size > 0) { - - let maxfreq = 0; - - for(let widget of widgets){ - - let wf = widget.frequency; - - if(wf != undefined && maxfreq < wf) - - maxfreq = wf; - - } - - - - if(maxfreq != 0) - - new_period = 1000/maxfreq; - - } - - - - if(previous_period != new_period) { - - set_subscription_period(index, new_period); - - if(index <= last_remote_index){ - - delta.push( - - new Uint8Array([2]), /* subscribe = 2 */ - - new Uint32Array([index]), - - new Uint16Array([new_period])); - - } - - } - - } - - send_blob(delta); - - }; - - - - function send_hmi_value(index, value) { - - if(index > last_remote_index){ - - dispatch_value(index, value); - - - - if(persistent_indexes.has(index)){ - - let varname = persistent_indexes.get(index); - - document.cookie = varname+"="+value+"; max-age=3153600000"; - - } - - - - return; - - } - - - - let iectype = hmitree_types[index]; - - let tobinary = typedarray_types[iectype]; - - send_blob([ - - new Uint8Array([0]), /* setval = 0 */ - - new Uint32Array([index]), - - tobinary(value)]); - - - - // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf - - // cache[index] = value; - - }; - - - - function apply_hmi_value(index, new_val) { - - // Similarly to previous comment, taking decision to update based - - // on cache content is bad and can lead to inconsistency - - /*let old_val = cache[index];*/ - - if(new_val != undefined /*&& old_val != new_val*/) - - send_hmi_value(index, new_val); - - return new_val; - - } - - - - const quotes = {"'":null, '"':null}; - - - - function eval_operation_string(old_val, opstr) { - - let op = opstr[0]; - - let given_val; - - if(opstr.length < 2) - - return undefined; - - if(opstr[1] in quotes){ - - if(opstr.length < 3) - - return undefined; - - if(opstr[opstr.length-1] == opstr[1]){ - - given_val = opstr.slice(2,opstr.length-1); - - } - - } else { - - given_val = Number(opstr.slice(1)); - - } - - let new_val; - - switch(op){ - - case "=": - - new_val = given_val; - - break; - - case "+": - - new_val = old_val + given_val; - - break; - - case "-": - - new_val = old_val - given_val; - - break; - - case "*": - - new_val = old_val * given_val; - - break; - - case "/": - - new_val = old_val / given_val; - - break; - - } - - return new_val; - - } - - - - var current_visible_page; - - var current_subscribed_page; - - var current_page_index; - - var page_node_local_index = hmi_local_index("page_node"); - - var page_switch_in_progress = false; - - - - function toggleFullscreen() { - - let elem = document.documentElement; - - - - if (!document.fullscreenElement) { - - elem.requestFullscreen().catch(err => { - - console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); - - }); - - } else { - - document.exitFullscreen(); - - } - - } - - - - function prepare_svg() { - - // prevents context menu from appearing on right click and long touch - - document.body.addEventListener('contextmenu', e => { - - toggleFullscreen(); - - e.preventDefault(); - - }); - - - - for(let eltid in detachable_elements){ - - let [element,parent] = detachable_elements[eltid]; - - parent.removeChild(element); - - } - - }; - - - - function switch_page(page_name, page_index) { - - if(page_switch_in_progress){ - - /* page switch already going */ - - /* TODO LOG ERROR */ - - return false; - - } - - page_switch_in_progress = true; - - - - if(page_name == undefined) - - page_name = current_subscribed_page; - - else if(page_index == undefined){ - - [page_name, page_index] = page_name.split('@') - - } - - - - let old_desc = page_desc[current_subscribed_page]; - - let new_desc = page_desc[page_name]; - - - - if(new_desc == undefined){ - - /* TODO LOG ERROR */ - - return false; - - } - - - - if(page_index == undefined) - - page_index = new_desc.page_index; - - else if(typeof(page_index) == "string") { - - let hmitree_node = hmitree_nodes[page_index]; - - if(hmitree_node !== undefined){ - - let [int_index, hmiclass] = hmitree_node; - - if(hmiclass == new_desc.page_class) - - page_index = int_index; - - else - - page_index = new_desc.page_index; - - } else { - - page_index = new_desc.page_index; - - } - - } - - - - if(old_desc){ - - old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); - - } - - const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; - - - - const container_id = page_name + (page_index != undefined ? page_index : ""); - - - - new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); - - - - update_subscriptions(); - - - - current_subscribed_page = page_name; - - current_page_index = page_index; - - let page_node; - - if(page_index != undefined){ - - page_node = hmitree_paths[page_index]; - - }else{ - - page_node = ""; - - } - - apply_hmi_value(page_node_local_index, page_node); - - - - jumps_need_update = true; - - - - requestHMIAnimation(); - - 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 - - : page_name + "@" + hmitree_paths[page_index]); - - - - return true; - - }; - - - - function switch_visible_page(page_name) { - - - - let old_desc = page_desc[current_visible_page]; - - let new_desc = page_desc[page_name]; - - - - if(old_desc){ - - for(let eltid in old_desc.required_detachables){ - - if(!(eltid in new_desc.required_detachables)){ - - let [element, parent] = old_desc.required_detachables[eltid]; - - parent.removeChild(element); - - } - - } - - for(let eltid in new_desc.required_detachables){ - - if(!(eltid in old_desc.required_detachables)){ - - let [element, parent] = new_desc.required_detachables[eltid]; - - parent.appendChild(element); - - } - - } - - }else{ - - for(let eltid in new_desc.required_detachables){ - - let [element, parent] = new_desc.required_detachables[eltid]; - - parent.appendChild(element); - - } - - } - - - - svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); - - current_visible_page = page_name; - - }; - - - - /* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */ - - function getAbsoluteCTM(element){ - - var height = svg_root.height.baseVal.value, - - width = svg_root.width.baseVal.value, - - viewBoxRect = svg_root.viewBox.baseVal, - - vHeight = viewBoxRect.height, - - vWidth = viewBoxRect.width; - - if(!vWidth || !vHeight){ - - return element.getCTM(); - - } - - var sH = height/vHeight, - - sW = width/vWidth, - - matrix = svg_root.createSVGMatrix(); - - matrix.a = sW; - - matrix.d = sH - - var realCTM = element.getCTM().multiply(matrix.inverse()); - - realCTM.e = realCTM.e/sW + viewBoxRect.x; - - realCTM.f = realCTM.f/sH + viewBoxRect.y; - - return realCTM; - - } - - - - function apply_reference_frames(){ - - const matches = svg_root.querySelectorAll("g[svghmi_x_offset]"); - - matches.forEach((group) => { - - let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset"))); - - let ctm = getAbsoluteCTM(group); - - // zero translation part of CTM - - // to only apply rotation/skewing to offset vector - - ctm.e = 0; - - ctm.f = 0; - - let invctm = ctm.inverse(); - - let vect = new DOMPoint(x, y); - - let newvect = vect.matrixTransform(invctm); - - let transform = svg_root.createSVGTransform(); - - transform.setTranslate(newvect.x, newvect.y); - - group.transform.baseVal.appendItem(transform); - - ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset")); - - }); - - } - - - - // Once connection established - - ws.onopen = function (evt) { - - apply_reference_frames(); - - init_widgets(); - - send_reset(); - - // show main page - - prepare_svg(); - - switch_page(default_page); - - }; - - - - ws.onclose = function (evt) { - - // 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+"."); - - - - }; + create_ws() 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 + + + Back + Home + + + + + + + + + + + + + Leave ScreenSaver + + + + + + + Home + + + + + + + + Settings + + + + + + + + Pump 0 + + + + + + + + Pump 1 + + + + + + + + Pump 2 + + + + + + + + Pump 3 + +