# HG changeset patch # User Edouard Tisserant # Date 1607183967 -3600 # Node ID 6b1b23971960c2cc487fb07385e5348af97452a1 # Parent 1ae4a871b6f9142e5f3c20cd86f284c8d68c756e SVGHMI: Rewrote button widget. diff -r 1ae4a871b6f9 -r 6b1b23971960 svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Wed Dec 02 14:33:24 2020 +0100 +++ b/svghmi/gen_index_xhtml.xslt Sat Dec 05 16:59:27 2020 +0100 @@ -1675,134 +1675,219 @@ <xsl:text>} </xsl:text> </xsl:template> + <xsl:variable name="_button_fsm"> + <fsm> + <state name="init"> + <on-dispatch value="false"> + <jump state="released"/> + </on-dispatch> + <on-dispatch value="true"> + <jump state="pressed"/> + </on-dispatch> + </state> + <state name="pressing"> + <hmi-value value="true"/> + <on-dispatch value="true"> + <jump state="pressed"/> + </on-dispatch> + <on-mouse position="up"> + <jump state="shortpress"/> + </on-mouse> + </state> + <state name="pressed"> + <show eltname="active"/> + <on-mouse position="up"> + <jump state="releasing"/> + </on-mouse> + <on-dispatch value="false"> + <jump state="released"/> + </on-dispatch> + </state> + <state name="shortpress"> + <on-dispatch value="true"> + <jump state="releasing"/> + </on-dispatch> + <on-mouse position="down"> + <jump state="pressing"/> + </on-mouse> + </state> + <state name="releasing"> + <hmi-value value="false"/> + <on-dispatch value="false"> + <jump state="released"/> + </on-dispatch> + <on-mouse position="down"> + <jump state="shortrelease"/> + </on-mouse> + </state> + <state name="released"> + <show eltname="inactive"/> + <on-mouse position="down"> + <jump state="pressing"/> + </on-mouse> + <on-dispatch value="true"> + <jump state="pressed"/> + </on-dispatch> + </state> + <state name="shortrelease"> + <on-dispatch value="false"> + <jump state="pressing"/> + </on-dispatch> + <on-mouse position="up"> + <jump state="releasing"/> + </on-mouse> + </state> + </fsm> + </xsl:variable> + <xsl:template mode="dispatch_transition" match="fsm"> + <xsl:text> switch (this.state) { +</xsl:text> + <xsl:apply-templates mode="dispatch_transition" select="state"/> + <xsl:text> } +</xsl:text> + </xsl:template> + <xsl:template mode="dispatch_transition" match="state"> + <xsl:text> case "</xsl:text> + <xsl:value-of select="@name"/> + <xsl:text>": +</xsl:text> + <xsl:apply-templates select="on-dispatch"/> + <xsl:text> break; +</xsl:text> + </xsl:template> + <xsl:template match="on-dispatch"> + <xsl:text> if(value == </xsl:text> + <xsl:value-of select="@value"/> + <xsl:text>) { +</xsl:text> + <xsl:apply-templates mode="transition" select="jump"/> + <xsl:text> } +</xsl:text> + </xsl:template> + <xsl:template mode="mouse_transition" match="fsm"> + <xsl:param name="position"/> + <xsl:text> switch (this.state) { +</xsl:text> + <xsl:apply-templates mode="mouse_transition" select="state"> + <xsl:with-param name="position" select="$position"/> + </xsl:apply-templates> + <xsl:text> } +</xsl:text> + </xsl:template> + <xsl:template mode="mouse_transition" match="state"> + <xsl:param name="position"/> + <xsl:text> case "</xsl:text> + <xsl:value-of select="@name"/> + <xsl:text>": +</xsl:text> + <xsl:apply-templates select="on-mouse[@position = $position]"/> + <xsl:text> break; +</xsl:text> + </xsl:template> + <xsl:template match="on-mouse"> + <xsl:apply-templates mode="transition" select="jump"/> + </xsl:template> + <xsl:template mode="transition" match="jump"> + <xsl:text> this.state = "</xsl:text> + <xsl:value-of select="@state"/> + <xsl:text>"; +</xsl:text> + <xsl:text> this.</xsl:text> + <xsl:value-of select="@state"/> + <xsl:text>_action(); +</xsl:text> + </xsl:template> + <xsl:template mode="actions" match="fsm"> + <xsl:apply-templates mode="actions" select="state"/> + </xsl:template> + <xsl:template mode="actions" match="state"> + <xsl:text> </xsl:text> + <xsl:value-of select="@name"/> + <xsl:text>_action(){ +</xsl:text> + <xsl:apply-templates mode="actions" select="*"/> + <xsl:text> } +</xsl:text> + </xsl:template> + <xsl:template mode="actions" match="show"> + <xsl:text> this.display = "</xsl:text> + <xsl:value-of select="@eltname"/> + <xsl:text>"; +</xsl:text> + <xsl:text> this.request_animate(); +</xsl:text> + </xsl:template> + <xsl:template mode="actions" match="hmi-value"> + <xsl:text> this.apply_hmi_value(0, </xsl:text> + <xsl:value-of select="@value"/> + <xsl:text>); +</xsl:text> + </xsl:template> <xsl:template mode="widget_class" match="widget[@type='Button']"> + <xsl:variable name="fsm" select="exsl:node-set($_button_fsm)"/> <xsl:text>class ButtonWidget extends Widget{ </xsl:text> <xsl:text> frequency = 5; </xsl:text> - <xsl:text> state_plc = 0; -</xsl:text> - <xsl:text> state_hmi = 0; -</xsl:text> - <xsl:text> plc_lock = false; -</xsl:text> - <xsl:text> active_style = undefined; -</xsl:text> - <xsl:text> inactive_style = undefined; -</xsl:text> - <xsl:text> + <xsl:text> display = "inactive"; +</xsl:text> + <xsl:text> state = "init"; </xsl:text> <xsl:text> dispatch(value) { </xsl:text> - <xsl:text> this.state_plc = value; -</xsl:text> - <xsl:text> if(this.plc_lock){ -</xsl:text> - <xsl:text> if(this.state_plc == 1){ -</xsl:text> - <xsl:text> this.apply_hmi_value(0, 0); -</xsl:text> - <xsl:text> this.plc_lock = false; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> //redraw button -</xsl:text> - <xsl:text> this.state_hmi = this.state_plc; -</xsl:text> - <xsl:text> this.request_animate(); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> + <xsl:apply-templates mode="dispatch_transition" select="$fsm"/> + <xsl:text> } +</xsl:text> + <xsl:text> onmouseup(evt) { +</xsl:text> + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); +</xsl:text> + <xsl:apply-templates mode="mouse_transition" select="$fsm"> + <xsl:with-param name="position" select="'up'"/> + </xsl:apply-templates> + <xsl:text> } +</xsl:text> + <xsl:text> onmousedown(evt) { +</xsl:text> + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); +</xsl:text> + <xsl:apply-templates mode="mouse_transition" select="$fsm"> + <xsl:with-param name="position" select="'down'"/> + </xsl:apply-templates> + <xsl:text> } +</xsl:text> + <xsl:apply-templates mode="actions" select="$fsm"/> <xsl:text> animate(){ </xsl:text> - <xsl:text> if (this.active_style && this.inactive_style) { -</xsl:text> - <xsl:text> // redraw button on screen refresh -</xsl:text> - <xsl:text> if (this.state_hmi) { -</xsl:text> - <xsl:text> this.active_elt.setAttribute("style", this.active_style); -</xsl:text> - <xsl:text> this.inactive_elt.setAttribute("style", "display:none"); -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> this.inactive_elt.setAttribute("style", this.inactive_style); -</xsl:text> - <xsl:text> this.active_elt.setAttribute("style", "display:none"); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> on_click(evt) { -</xsl:text> - <xsl:text> //set state and apply if plc is 0 -</xsl:text> - <xsl:text> this.plc_lock = true; -</xsl:text> - <xsl:text> if(this.state_plc == 0){ -</xsl:text> - <xsl:text> this.apply_hmi_value(0, 1); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> //redraw button -</xsl:text> - <xsl:text> this.request_animate(); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> on_press(evt) { -</xsl:text> - <xsl:text> //set graphic -</xsl:text> - <xsl:text> this.state_hmi = 1; -</xsl:text> - <xsl:text> //redraw button -</xsl:text> - <xsl:text> this.request_animate(); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> init() { -</xsl:text> - <xsl:text> this.active_style = this.active_elt ? this.active_elt.style.cssText : undefined; -</xsl:text> - <xsl:text> this.inactive_style = this.inactive_elt ? this.inactive_elt.style.cssText : undefined; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if (this.active_style && this.inactive_style) { -</xsl:text> - <xsl:text> this.active_elt.setAttribute("style", "display:none"); -</xsl:text> - <xsl:text> this.inactive_elt.setAttribute("style", this.inactive_style); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); -</xsl:text> - <xsl:text> this.element.setAttribute("onmousedown", "hmi_widgets['"+this.element_id+"'].on_press(evt)"); -</xsl:text> - <xsl:text> } + <xsl:text> if (this.active_elt && this.inactive_elt) { +</xsl:text> + <xsl:for-each select="str:split('active inactive')"> + <xsl:text> if(this.display == "</xsl:text> + <xsl:value-of select="."/> + <xsl:text>") +</xsl:text> + <xsl:text> this.</xsl:text> + <xsl:value-of select="."/> + <xsl:text>_elt.style.display = ""; +</xsl:text> + <xsl:text> else +</xsl:text> + <xsl:text> this.</xsl:text> + <xsl:value-of select="."/> + <xsl:text>_elt.style.display = "none"; +</xsl:text> + </xsl:for-each> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> init() { +</xsl:text> + <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); +</xsl:text> + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); +</xsl:text> + <xsl:text> } </xsl:text> <xsl:text>} </xsl:text> @@ -6645,12 +6730,16 @@ </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> 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]; diff -r 1ae4a871b6f9 -r 6b1b23971960 svghmi/widget_button.ysl2 --- a/svghmi/widget_button.ysl2 Wed Dec 02 14:33:24 2020 +0100 +++ b/svghmi/widget_button.ysl2 Sat Dec 05 16:59:27 2020 +0100 @@ -1,73 +1,152 @@ // widget_button.ysl2 +// Finite state machine +decl fsm(name); +decl state(name); +decl on_mouse(position); +decl on_dispatch(value); +decl jump(state); +decl show(eltname); +decl hmi_value(value); + +// State machine to drive HMI_BOOL on a potentially laggy connection +// TODO: make more robust in case other widget or PLC change value on their own +const "_button_fsm" fsm { + state "init" { + on_dispatch "false" jump "released"; + on_dispatch "true" jump "pressed"; + } + + state "pressing" { + // show "waitactive"; + hmi_value "true"; + on_dispatch "true" jump "pressed"; + on_mouse "up" jump "shortpress"; + } + state "pressed" { + show "active"; + on_mouse "up" jump "releasing"; + on_dispatch "false" jump "released"; + } + state "shortpress" { + on_dispatch "true" jump "releasing"; + on_mouse "down" jump "pressing"; + } + + state "releasing" { + // show "waitinactive"; + hmi_value "false"; + on_dispatch "false" jump "released"; + on_mouse "down" jump "shortrelease"; + } + state "released" { + show "inactive"; + on_mouse "down" jump "pressing"; + on_dispatch "true" jump "pressed"; + } + state "shortrelease" { + on_dispatch "false" jump "pressing"; + on_mouse "up" jump "releasing"; + } +} + +template "fsm", mode="dispatch_transition" { + | switch (this.state) { + apply "state", mode="dispatch_transition"; + | } +} +template "state", mode="dispatch_transition" { + | case "«@name»": + apply "on-dispatch"; + | break; +} +template "on-dispatch" { + | if(value == «@value») { + apply "jump", mode="transition"; + | } +} + +template "fsm", mode="mouse_transition" { + param "position"; + | switch (this.state) { + apply "state", mode="mouse_transition" with "position", "$position"; + | } +} +template "state", mode="mouse_transition" { + param "position"; + | case "«@name»": + apply "on-mouse[@position = $position]"; + | break; +} +template "on-mouse" { + // up or down state is already assumed because apply statement filters it + apply "jump", mode="transition"; +} + +template "jump", mode="transition" { + | this.state = "«@state»"; + | this.«@state»_action(); +} + +template "fsm", mode="actions" { + apply "state", mode="actions"; +} +template "state", mode="actions" { + | «@name»_action(){ + //| console.log("Entering state «@name»"); + apply "*", mode="actions"; + | } +} +template "show", mode="actions" { + | this.display = "«@eltname»"; + | this.request_animate(); +} +template "hmi-value", mode="actions" { + | this.apply_hmi_value(0, «@value»); +} + template "widget[@type='Button']", mode="widget_class"{ - || - class ButtonWidget extends Widget{ - frequency = 5; - state_plc = 0; - state_hmi = 0; - plc_lock = false; - active_style = undefined; - inactive_style = undefined; + const "fsm","exsl:node-set($_button_fsm)"; + | class ButtonWidget extends Widget{ + | frequency = 5; - dispatch(value) { - this.state_plc = value; - if(this.plc_lock){ - if(this.state_plc == 1){ - this.apply_hmi_value(0, 0); - this.plc_lock = false; - } - } + | display = "inactive"; + | state = "init"; - //redraw button - this.state_hmi = this.state_plc; - this.request_animate(); - } + | dispatch(value) { + // | console.log("dispatch"+value); + apply "$fsm", mode="dispatch_transition"; + | } - animate(){ - if (this.active_style && this.inactive_style) { - // redraw button on screen refresh - if (this.state_hmi) { - this.active_elt.setAttribute("style", this.active_style); - this.inactive_elt.setAttribute("style", "display:none"); - } else { - this.inactive_elt.setAttribute("style", this.inactive_style); - this.active_elt.setAttribute("style", "display:none"); - } - } - } + | onmouseup(evt) { + | svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + // | console.log("onmouseup"); + apply "$fsm", mode="mouse_transition" with "position", "'up'"; + | } + | onmousedown(evt) { + | svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + // | console.log("onmousedown"); + apply "$fsm", mode="mouse_transition" with "position", "'down'"; + | } - on_click(evt) { - //set state and apply if plc is 0 - this.plc_lock = true; - if(this.state_plc == 0){ - this.apply_hmi_value(0, 1); - } - //redraw button - this.request_animate(); - } + apply "$fsm", mode="actions"; - on_press(evt) { - //set graphic - this.state_hmi = 1; - //redraw button - this.request_animate(); - } + | animate(){ + | if (this.active_elt && this.inactive_elt) { + foreach "str:split('active inactive')" { + | if(this.display == "«.»") + | this.«.»_elt.style.display = ""; + | else + | this.«.»_elt.style.display = "none"; + } + | } + | } - 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("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); - this.element.setAttribute("onmousedown", "hmi_widgets['"+this.element_id+"'].on_press(evt)"); - } - } - || + | init() { + | this.bound_onmouseup = this.onmouseup.bind(this); + | this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + | } + | } } diff -r 1ae4a871b6f9 -r 6b1b23971960 tests/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/svghmi/svghmi_0@svghmi/svghmi.svg Wed Dec 02 14:33:24 2020 +0100 +++ b/tests/svghmi/svghmi_0@svghmi/svghmi.svg Sat Dec 05 16:59:27 2020 +0100 @@ -197,12 +197,12 @@ inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:document-units="px" - inkscape:current-layer="hmi0" + inkscape:current-layer="g443" showgrid="false" units="px" - inkscape:zoom="0.27282898" - inkscape:cx="646.17826" - inkscape:cy="652.93449" + inkscape:zoom="0.77167689" + inkscape:cx="379.87087" + inkscape:cy="462.91635" inkscape:window-width="1848" inkscape:window-height="1016" inkscape:window-x="72" @@ -2442,19 +2442,19 @@ <g inkscape:label="HMI:Input@/SELECTION" id="g446" - transform="matrix(0.28590269,0,0,0.28590269,487.38811,348.87609)"> + transform="matrix(0.28590269,0,0,0.28590269,1047.3881,408.87609)"> <text xml:space="preserve" - style="font-style:normal;font-weight:normal;font-size:160px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - x="136.32812" + style="font-style:normal;font-weight:normal;font-size:160px;line-height:125%;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + x="216.32812" y="218.24219" id="text432" inkscape:label="value"><tspan sodipodi:role="line" id="tspan430" - x="136.32812" + x="216.32812" y="218.24219" - style="stroke-width:1px">8</tspan></text> + style="text-align:end;text-anchor:end;stroke-width:1px">8</tspan></text> <path transform="scale(1,-1)" sodipodi:type="star" @@ -2477,7 +2477,7 @@ inkscape:label="edit" onclick="" y="95.40741" - x="139.85185" + x="-174.94055" height="128" width="407.7037" id="rect438" @@ -2519,41 +2519,46 @@ inkscape:transform-center-x="1.0089177e-06" /> </g> <g - transform="matrix(0.28590269,0,0,0.28590269,170.16209,215.31977)" + transform="matrix(0.57180538,0,0,0.57180538,417.18774,31.574523)" id="g443" inkscape:label="HMI:Button@/SELECTION" - style="stroke-width:2"> - <g - id="g435" - inkscape:label="bg" - style="stroke-width:2"> - <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: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" - id="rect433" - width="245.44583" - height="95.723877" - x="971.96545" - y="594.82263" - ry="47.861938" - inkscape:label="button" - rx="47.861938" /> - </g> - <g - id="g441" + style="stroke-width:1"> + <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;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="rect5492" + width="245.44583" + height="95.723877" + x="971.96545" + y="594.82263" + ry="23.930969" + inkscape:label="inactive" + rx="23.930969" /> + <rect + rx="23.930969" + inkscape:label="active" + ry="23.930969" + y="594.82263" + x="971.96545" + height="95.723877" + width="245.44583" + id="rect433" + 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:#fdfdfd;fill-opacity:1;fill-rule:nonzero;stroke:#ffd0b2;stroke-width:28.60938356;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 + style="stroke-width:1" inkscape:label="text" - style="stroke-width:2"> - <text - inkscape:label="setting_jmp" - id="text439" + id="g952"> + <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" - 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:1.99999988px;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.99999988px" + id="text950" + inkscape:label="setting_jmp"><tspan + sodipodi:role="line" + id="tspan948" + x="1090.7626" y="656.98151" - x="1090.7626" - id="tspan437" - sodipodi:role="line">up</tspan></text> + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px">up</tspan></text> </g> </g> <g