# HG changeset patch # User Edouard Tisserant # Date 1597238642 -7200 # Node ID 0a9f6f29b7ddaaef4a0bee5601ac8c601b46f262 # Parent 407a0205405a64c144550be67da941498cac1ebc# Parent 49799de67540ba934ab0c4cb7ef9421fe0db05ab Merge diff -r 49799de67540 -r 0a9f6f29b7dd svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Mon Aug 10 15:25:57 2020 +0200 +++ b/svghmi/gen_index_xhtml.xslt Wed Aug 12 15:24:02 2020 +0200 @@ -169,14 +169,30 @@ - - - - - - - - + + + + + + + + + + + + + + PAGE_LOCAL + + + + + HMI_LOCAL + + + + + @@ -834,7 +850,7 @@ " - + " , @@ -845,15 +861,35 @@ - - Widget - - id=" - - " : No match for path " - - " in HMI tree - + + + + Widget + + id=" + + " : No match for path " + + " in HMI tree + + + + " + + " + + , + + + + hmi_local_index(" + + ") + + , + + + @@ -905,8 +941,8 @@ - - + + /* @@ -915,6 +951,101 @@ + let hmi_locals = {}; + + var last_remote_index = hmitree_types.length - 1; + + var next_available_index = hmitree_types.length; + + + + const local_defaults = { + + + + + VarInit + + must have only one variable given. + + + + + VarInit + + only applies to HMI variable. + + + + : + + + , + + + + + }; + + var cache = hmitree_types.map(_ignored => undefined); + + + + function page_local_index(varname, pagename){ + + let pagevars = hmi_locals[pagename]; + + let new_index; + + if(pagevars == undefined){ + + new_index = next_available_index++; + + hmi_locals[pagename] = {[varname]:new_index} + + } else { + + let result = pagevars[varname]; + + if(result != undefined) { + + return result; + + } + + + + new_index = next_available_index++; + + pagevars[varname] = new_index; + + } + + return new_index; + + } + + + + function hmi_local_index(varname){ + + return page_local_index(varname, "HMI_LOCAL"); + + } + + + + + + + + + /* + + */ + + + var pending_widget_animates = []; @@ -961,7 +1092,7 @@ index += this.offset; - subscribers[index].delete(this); + subscribers(index).delete(this); } @@ -973,25 +1104,23 @@ - sub(new_offset=0, relativeness){ + sub(new_offset=0, relativeness, container_id){ this.offset = new_offset; this.relativeness = relativeness; + this.container_id = container_id ; + /* add this's subsribers */ if(!this.unsubscribable) for(let i = 0; i < this.indexes.length; i++) { - let index = this.indexes[i]; - - if(relativeness[i]) - - index += new_offset; - - subscribers[index].add(this); + let index = this.get_variable_index(i); + + subscribers(index).add(this); } @@ -1007,7 +1136,7 @@ /* dispatch current cache in newly opened page widgets */ - let realindex = index+this.offset; + let realindex = this.get_variable_index(index); let cached_val = cache[realindex]; @@ -1021,17 +1150,31 @@ - get_idx(index) { - - let orig = this.indexes[index]; - - return this.relativeness[index] ? orig + this.offset : orig; + get_variable_index(varnum) { + + let index = this.indexes[varnum]; + + if(typeof(index) == "string"){ + + index = page_local_index(index, this.container_id); + + } else { + + if(this.relativeness[varnum]){ + + index += this.offset; + + } + + } + + return index; } change_hmi_value(index,opstr) { - return change_hmi_value(this.get_idx(index), opstr); + return change_hmi_value(this.get_variable_index(index), opstr); } @@ -1039,7 +1182,7 @@ apply_hmi_value(index, new_val) { - return apply_hmi_value(this.get_idx(0), new_val); + return apply_hmi_value(this.get_variable_index(0), new_val); } @@ -1053,11 +1196,7 @@ for(let i = 0; i < this.indexes.length; i++) { - let refindex = this.indexes[i]; - - if(this.relativeness[i]) - - refindex += this.offset; + let refindex = this.get_variable_index(i); @@ -1159,8 +1298,8 @@ } - - + + @@ -1173,7 +1312,7 @@ var hmi_widgets = { - + } @@ -1256,11 +1395,9 @@ - - - - + + @@ -1294,77 +1431,75 @@ - t{ - - 5; - - 0; - - d; - - d; - - - - { - - { - - ); - - ); - - } - - ); - - } - - - - { - - { - - ); - - ); - - } - - ); - - } - - - - { - - d; - - d; - - - - { - - ); - - ); - - } - - - - ); - - ); - - } - - } - - || + class ButtonWidget extends Widget{ + + frequency = 5; + + state = 0; + + active_style = undefined; + + inactive_style = undefined; + + + + on_mouse_down(evt) { + + if (this.active_style && this.inactive_style) { + + this.active_elt.setAttribute("style", this.active_style); + + this.inactive_elt.setAttribute("style", "display:none"); + + } + + this.apply_hmi_value(0, 1); + + } + + + + on_mouse_up(evt) { + + if (this.active_style && this.inactive_style) { + + this.active_elt.setAttribute("style", "display:none"); + + this.inactive_elt.setAttribute("style", this.inactive_style); + + } + + this.apply_hmi_value(0, 0); + + } + + + + init() { + + this.active_style = this.active_elt ? this.active_elt.style.cssText : undefined; + + this.inactive_style = this.inactive_elt ? this.inactive_elt.style.cssText : undefined; + + + + if (this.active_style && this.inactive_style) { + + this.active_elt.setAttribute("style", "display:none"); + + this.inactive_elt.setAttribute("style", this.inactive_style); + + } + + + + this.element.setAttribute("onmousedown", "hmi_widgets["+this.element_id+"].on_mouse_down(evt)"); + + this.element.setAttribute("onmouseup", "hmi_widgets["+this.element_id+"].on_mouse_up(evt)"); + + } + + } @@ -1816,7 +1951,22 @@ " is not a svg::text element - fields: [], + + + + + "" + + 0 + + + , + + + + fields: [ + + ], @@ -4616,8 +4766,6 @@ - var cache = hmitree_types.map(_ignored => undefined); - var updates = {}; var need_cache_apply = []; @@ -4628,7 +4776,7 @@ function dispatch_value(index, value) { - let widgets = subscribers[index]; + let widgets = subscribers(index); @@ -4936,19 +5084,65 @@ - // subscription state, as it should be in hmi server - - // hmitree indexed array of integers - - var subscriptions = hmitree_types.map(_ignored => 0); - - - - // subscription state as needed by widget now - - // hmitree indexed array of Sets of widgets objects - - var subscribers = hmitree_types.map(_ignored => new Set()); + 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; + + } + + } @@ -4958,7 +5152,7 @@ // PLC will periodically send variable at given frequency - subscribers[heartbeat_index].add({ + subscribers(heartbeat_index).add({ /* type: "Watchdog", */ @@ -4976,19 +5170,21 @@ + + function update_subscriptions() { let delta = []; - for(let index = 0; index < subscribers.length; index++){ - - let widgets = subscribers[index]; + for(let index in subscriptions){ + + let widgets = subscribers(index); // periods are in ms - let previous_period = subscriptions[index]; + let previous_period = get_subscription_period(index); @@ -5022,15 +5218,19 @@ if(previous_period != new_period) { - subscriptions[index] = new_period; - - delta.push( - - new Uint8Array([2]), /* subscribe = 2 */ - - new Uint32Array([index]), - - new Uint16Array([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])); + + } } @@ -5044,6 +5244,20 @@ function send_hmi_value(index, value) { + if(index > last_remote_index){ + + console.log("updated local variable ",index,value); + + updates[index] = value; + + requestHMIAnimation(); + + return; + + } + + + let iectype = hmitree_types[index]; let tobinary = typedarray_types[iectype]; @@ -5238,7 +5452,13 @@ var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; - new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness)); + + + container_id = page_name + (page_index != undefined ? page_index : ""); + + + + new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); diff -r 49799de67540 -r 0a9f6f29b7dd svghmi/hmi_tree.ysl2 --- a/svghmi/hmi_tree.ysl2 Mon Aug 10 15:25:57 2020 +0200 +++ b/svghmi/hmi_tree.ysl2 Wed Aug 12 15:24:02 2020 +0200 @@ -107,9 +107,21 @@ attrib "value" > «.» const "path", "."; const "item", "$indexed_hmitree/*[@hmipath = $path]"; - if "count($item) = 1" { - attrib "index" > «$item/@index» - attrib "type" > «local-name($item)» + choose { + when "count($item) = 1" { + attrib "index" > «$item/@index» + attrib "type" > «local-name($item)» + } + otherwise { + choose { + when "regexp:test($path,'^\.[a-zA-Z0-9_]+')" { + attrib "type" > PAGE_LOCAL + } + when "regexp:test($path,'^[a-zA-Z0-9_]+')" { + attrib "type" > HMI_LOCAL + } + } + } } } } diff -r 49799de67540 -r 0a9f6f29b7dd svghmi/svghmi.js --- a/svghmi/svghmi.js Mon Aug 10 15:25:57 2020 +0200 +++ b/svghmi/svghmi.js Wed Aug 12 15:24:02 2020 +0200 @@ -1,12 +1,11 @@ // svghmi.js -var cache = hmitree_types.map(_ignored => undefined); var updates = {}; var need_cache_apply = []; function dispatch_value(index, value) { - let widgets = subscribers[index]; + let widgets = subscribers(index); let oldval = cache[index]; cache[index] = value; @@ -160,18 +159,41 @@ send_blob(new Uint8Array([1])); /* reset = 1 */ }; -// subscription state, as it should be in hmi server -// hmitree indexed array of integers -var subscriptions = hmitree_types.map(_ignored => 0); - -// subscription state as needed by widget now -// hmitree indexed array of Sets of widgets objects -var subscribers = hmitree_types.map(_ignored => new Set()); +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; + } +} // 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({ +subscribers(heartbeat_index).add({ /* type: "Watchdog", */ frequency: 1, indexes: [heartbeat_index], @@ -180,13 +202,14 @@ } }); + function update_subscriptions() { let delta = []; - for(let index = 0; index < subscribers.length; index++){ - let widgets = subscribers[index]; + for(let index in subscriptions){ + let widgets = subscribers(index); // periods are in ms - let previous_period = subscriptions[index]; + let previous_period = get_subscription_period(index); // subscribing with a zero period is unsubscribing let new_period = 0; @@ -203,17 +226,26 @@ } if(previous_period != new_period) { - subscriptions[index] = new_period; - delta.push( - new Uint8Array([2]), /* subscribe = 2 */ - new Uint32Array([index]), - new Uint16Array([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){ + console.log("updated local variable ",index,value); + updates[index] = value; + requestHMIAnimation(); + return; + } + let iectype = hmitree_types[index]; let tobinary = typedarray_types[iectype]; send_blob([ @@ -311,7 +343,10 @@ old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); } var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; - new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness)); + + container_id = page_name + (page_index != undefined ? page_index : ""); + + new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); update_subscriptions(); diff -r 49799de67540 -r 0a9f6f29b7dd svghmi/widget_button.ysl2 --- a/svghmi/widget_button.ysl2 Mon Aug 10 15:25:57 2020 +0200 +++ b/svghmi/widget_button.ysl2 Wed Aug 12 15:24:02 2020 +0200 @@ -1,6 +1,6 @@ // widget_button.ysl2 -template "widget[@type='Button']", mode="widget_class" +template "widget[@type='Button']", mode="widget_class"{ || class ButtonWidget extends Widget{ frequency = 5; @@ -38,6 +38,7 @@ } } || +} template "widget[@type='Button']", mode="widget_defs" { diff -r 49799de67540 -r 0a9f6f29b7dd svghmi/widget_display.ysl2 --- a/svghmi/widget_display.ysl2 Mon Aug 10 15:25:57 2020 +0200 +++ b/svghmi/widget_display.ysl2 Wed Aug 12 15:24:02 2020 +0200 @@ -18,7 +18,14 @@ if "$hmi_element[not(self::svg:text)]" error > Display Widget id="«$hmi_element/@id»" is not a svg::text element - | fields: [], + const "field_initializer" foreach "path" { + choose{ + when "@type='HMI_STRING'" > "" + otherwise 0 + } + if "position()!=last()" > , + } + | fields: [«$field_initializer»], } emit "preamble:display" diff -r 49799de67540 -r 0a9f6f29b7dd svghmi/widget_tooglebutton.ysl2 --- a/svghmi/widget_tooglebutton.ysl2 Mon Aug 10 15:25:57 2020 +0200 +++ b/svghmi/widget_tooglebutton.ysl2 Wed Aug 12 15:24:02 2020 +0200 @@ -1,7 +1,7 @@ // widget_tooglebutton.ysl2 -template "widget[@type='ToggleButton']", mode="widget_class" +template "widget[@type='ToggleButton']", mode="widget_class"{ || class ToggleButtonWidget extends Widget{ frequency = 5; @@ -33,6 +33,7 @@ } } || +} template "widget[@type='ToggleButton']", mode="widget_defs" { param "hmi_element"; diff -r 49799de67540 -r 0a9f6f29b7dd svghmi/widgets_common.ysl2 --- a/svghmi/widgets_common.ysl2 Mon Aug 10 15:25:57 2020 +0200 +++ b/svghmi/widgets_common.ysl2 Wed Aug 12 15:24:02 2020 +0200 @@ -24,11 +24,18 @@ template "svg:*", mode="hmi_widgets" { const "widget", "func:widget(@id)"; const "eltid","@id"; - const "args" foreach "$widget/arg" > "«@value»"`if "position()!=last()" > ,` + const "args" foreach "$widget/arg" > "«func:escape_quotes(@value)»"`if "position()!=last()" > ,` const "indexes" foreach "$widget/path" { choose { when "not(@index)" { - warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree + choose { + when "not(@type)" + error > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree + when "@type = 'PAGE_LOCAL'" + > "«substring(@value, 1)»"`if "position()!=last()" > ,` + when "@type = 'HMI_LOCAL'" + > hmi_local_index("«@value»")`if "position()!=last()" > ,` + } } otherwise { > «@index»`if "position()!=last()" > ,` @@ -62,8 +69,40 @@ } } +emit "preamble:local-variable-indexes" { + || + let hmi_locals = {}; + var last_remote_index = hmitree_types.length - 1; + var next_available_index = hmitree_types.length; + + var cache = hmitree_types.map(_ignored => undefined); + + function page_local_index(varname, pagename){ + let pagevars = hmi_locals[pagename]; + let new_index; + if(pagevars == undefined){ + new_index = next_available_index++; + hmi_locals[pagename] = {[varname]:new_index} + } else { + let result = pagevars[varname]; + if(result != undefined) { + return result; + } + + new_index = next_available_index++; + pagevars[varname] = new_index; + } + return new_index; + } + + function hmi_local_index(varname){ + return page_local_index(varname, "HMI_LOCAL"); + } + || +} + emit "preamble:widget-base-class" { - || + || var pending_widget_animates = []; class Widget { @@ -87,22 +126,21 @@ let index = this.indexes[i]; if(this.relativeness[i]) index += this.offset; - subscribers[index].delete(this); + subscribers(index).delete(this); } this.offset = 0; this.relativeness = undefined; } - sub(new_offset=0, relativeness){ + sub(new_offset=0, relativeness, container_id){ this.offset = new_offset; this.relativeness = relativeness; + this.container_id = container_id ; /* add this's subsribers */ if(!this.unsubscribable) for(let i = 0; i < this.indexes.length; i++) { - let index = this.indexes[i]; - if(relativeness[i]) - index += new_offset; - subscribers[index].add(this); + let index = this.get_variable_index(i); + subscribers(index).add(this); } need_cache_apply.push(this); } @@ -110,32 +148,37 @@ apply_cache() { if(!this.unsubscribable) for(let index of this.indexes){ /* dispatch current cache in newly opened page widgets */ - let realindex = index+this.offset; + let realindex = this.get_variable_index(index); let cached_val = cache[realindex]; if(cached_val != undefined) this.new_hmi_value(realindex, cached_val, cached_val); } } - get_idx(index) { - let orig = this.indexes[index]; - return this.relativeness[index] ? orig + this.offset : orig; + get_variable_index(varnum) { + let index = this.indexes[varnum]; + if(typeof(index) == "string"){ + index = page_local_index(index, this.container_id); + } else { + if(this.relativeness[varnum]){ + index += this.offset; + } + } + return index; } change_hmi_value(index,opstr) { - return change_hmi_value(this.get_idx(index), opstr); + return change_hmi_value(this.get_variable_index(index), opstr); } apply_hmi_value(index, new_val) { - return apply_hmi_value(this.get_idx(0), new_val); + return apply_hmi_value(this.get_variable_index(0), new_val); } new_hmi_value(index, value, oldval) { try { // TODO avoid searching, store index at sub() for(let i = 0; i < this.indexes.length; i++) { - let refindex = this.indexes[i]; - if(this.relativeness[i]) - refindex += this.offset; + let refindex = this.get_variable_index(i); if(index == refindex) { let d = this.dispatch; @@ -186,12 +229,12 @@ } || -const "excluded_types", "str:split('Page Lang')"; -const "excluded_ids","$parsed_widgets/widget[not(@type = $excluded_types)]/@id"; +const "excluded_types", "str:split('Page Lang VarInit')"; +const "included_ids","$parsed_widgets/widget[not(@type = $excluded_types)]/@id"; emit "declarations:hmi-elements" { | var hmi_widgets = { - apply "$hmi_elements[@id = $excluded_ids]", mode="hmi_widgets"; + apply "$hmi_elements[@id = $included_ids]", mode="hmi_widgets"; | } } @@ -240,11 +283,10 @@ def "func:escape_quotes" { param "txt"; // have to use a python string to enter escaped quote - const "frst", !"substring-before($txt,'\"')"!; - const "frstln", "string-length($frst)"; + // const "frstln", "string-length($frst)"; choose { - when "$frstln > 0 and string-length($txt) > $frstln" { - result !"concat($frst,'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!; + when !"contains($txt,'\"')"! { + result !"concat(substring-before($txt,'\"'),'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!; } otherwise { result "$txt"; diff -r 49799de67540 -r 0a9f6f29b7dd tests/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/svghmi/svghmi_0@svghmi/svghmi.svg Mon Aug 10 15:25:57 2020 +0200 +++ b/tests/svghmi/svghmi_0@svghmi/svghmi.svg Wed Aug 12 15:24:02 2020 +0200 @@ -16,7 +16,7 @@ version="1.1" id="hmi0" sodipodi:docname="svghmi.svg" - inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"> + inkscape:version="0.92.3 (2405546, 2018-03-11)"> @@ -34,6 +34,21 @@ + + + + transform="matrix(0.35865594,0,0,0.35865594,22.072155,63.074421)"> 8888 SetPoint + x="114.11434" + y="90.742165" + style="stroke-width:0.35865593px">SetPoint Actual + style="stroke-width:0.35865593px">Actual + transform="matrix(0.5,0,0,0.5,230.11026,885.7162)" + style="stroke-width:2"> 8888 + style="stroke-width:2px">8888 + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ff00ff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;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" /> + transform="translate(-416.52022,170.47452)" + style="stroke-width:2"> + 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:none;stroke-width:10;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" /> + transform="translate(-416.52022,170.47452)" + style="stroke-width:2"> -10 + style="stroke-width:1px">-10 + transform="translate(-416.52022,170.47452)" + style="stroke-width:2"> + transform="translate(-416.52022,170.47452)" + style="stroke-width:2"> + transform="translate(-416.52022,170.47452)" + style="stroke-width:2"> +10 + style="stroke-width:1px">+10 - - - + inkscape:label="HMI:JsonTable:/alarms@/ALARMCOUNT" + transform="matrix(0.5,0,0,0.5,635.30409,71.500438)"> @@ -3147,7 +3145,7 @@ sodipodi:role="line">information + transform="translate(1340,-520)"> Multiple variables + 8888 + + 8888 + + + + dhu + + + + plop + + + + mhoo + + + + yodl + + + + mhe + + + HMI_LOCAL variables + + + + + + 8888 + + 8888 + + + + dhu + + + + plop + + + + mhoo + + + + yodl + + + + mhe + + + PAGE_LOCAL variables + + + + 0 + 10000 + 000 + bar + + + 8888 + + + + + + + + 8888 + + 8888 + + + + dhu + + + + plop + + + + mhoo + + + + yodl + + + + mhe + + + HMI_LOCAL variables + 8888 + + 8888 + + + + dhu + + + + plop + + + + mhoo + + + + yodl + + + + mhe + + + PAGE_LOCAL variables + 8888 + + 8888 + + + + dhu + + + + plop + + + + mhoo + + + + yodl + + + + mhe + + + HMI_LOCAL variables + 8888 + + 8888 + + + + dhu + + + + plop + + + + mhoo + + + + yodl + + + + mhe + + + PAGE_LOCAL variables + +