Logging: added HMI:HistoryXYGraph (accepts streamed data from log and plots XYGraph) and log_append POU (logs data up to maximum number of records defined in config file).
--- a/LPCSVGHMI/analyse_widget.xslt Fri Jan 16 11:50:30 2026 +0100
+++ b/LPCSVGHMI/analyse_widget.xslt Mon Jan 26 11:30:15 2026 +0100
@@ -1162,6 +1162,53 @@
<xsl:text>Image display</xsl:text>
+ <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>HistoryXYGraph draws a cartesian trend graph reusing styles given for axis, + <xsl:text>grid/marks, legends and curves. + <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containing: + <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels. + <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be + <xsl:text> duplicated along axis line to form intervals marks. + <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their + <xsl:text> bounding box is the chart wall. + <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used + <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths. + <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored. + <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text> + <path name="value" count="1+" accepts="HMI_INT,HMI_REAL"> + <xsl:text>value</xsl:text> + <arg name="xformat" count="optional" accepts="string"> + <xsl:text>format string for X label</xsl:text> + <arg name="yformat" count="optional" accepts="string"> + <xsl:text>format string for Y label</xsl:text> <xsl:template match="widget[@type='Swipe']" mode="widget_desc">
<xsl:value-of select="@type"/>
--- a/LPCSVGHMI/gen_index_xhtml.xslt Fri Jan 16 11:50:30 2026 +0100
+++ b/LPCSVGHMI/gen_index_xhtml.xslt Mon Jan 26 11:30:15 2026 +0100
@@ -9649,6 +9649,516 @@
<xsl:apply-templates mode="inline_svg" select="@*[not(contains(name(), 'href'))] | node()"/>
+ <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>HistoryXYGraph draws a cartesian trend graph reusing styles given for axis, + <xsl:text>grid/marks, legends and curves. + <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containing: + <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels. + <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be + <xsl:text> duplicated along axis line to form intervals marks. + <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their + <xsl:text> bounding box is the chart wall. + <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used + <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths. + <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored. + <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text> + <path name="value" count="1+" accepts="HMI_INT,HMI_REAL"> + <xsl:text>value</xsl:text> + <arg name="xformat" count="optional" accepts="string"> + <xsl:text>format string for X label</xsl:text> + <arg name="yformat" count="optional" accepts="string"> + <xsl:text>format string for Y label</xsl:text> + <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>HistoryXYGraphWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 1; + <xsl:text> this.params = [null, null]; + <xsl:text> [this.x_format, this.y_format] = this.args; + <xsl:text> this.fetch_error_bound = this.fetch_error.bind(this); + <xsl:text> this.loading = false; + <xsl:text> this.curves = []; + <xsl:text> this.curves_data = []; + <xsl:text> this.init_specific(); + <xsl:text> this.reference = new ReferenceFrame( + <xsl:text> [[this.x_interval_minor_mark_elt, this.x_interval_major_mark_elt], + <xsl:text> [this.y_interval_minor_mark_elt, this.y_interval_major_mark_elt]], + <xsl:text> [this.x_axis_label_elt, this.y_axis_label_elt], + <xsl:text> [this.x_axis_line_elt, this.y_axis_line_elt], + <xsl:text> [this.x_format, this.y_format]); + <xsl:text> let max_stroke_width = 0; + <xsl:text> for (let curve of this.curves) { + <xsl:text> if (curve.style.strokeWidth > max_stroke_width) { + <xsl:text> max_stroke_width = curve.style.strokeWidth; + <xsl:text> this.curves_data.push([]); + <xsl:text> this.params.push(null); + <xsl:text> this.Margins = this.reference.getLengths().map(length => max_stroke_width / length); + <xsl:text> // create <clipPath> path and attach it to widget + <xsl:text> let clipPath = document.createElementNS(xmlns, "clipPath"); + <xsl:text> let clipPathPath = document.createElementNS(xmlns, "path"); + <xsl:text> let clipPathPathDattr = document.createAttribute("d"); + <xsl:text> clipPathPathDattr.value = this.reference.getClipPathPathDattr(); + <xsl:text> clipPathPath.setAttributeNode(clipPathPathDattr); + <xsl:text> clipPath.appendChild(clipPathPath); + <xsl:text> clipPath.id = randomId(); + <xsl:text> this.element.appendChild(clipPath); + <xsl:text> // assign created clipPath to clip-path property of curves + <xsl:text> for(let curve of this.curves) { + <xsl:text> curve.setAttribute("clip-path", "url(#" + clipPath.id + ")"); + <xsl:text> fetch_error(e){ + <xsl:text> console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id); + <xsl:text> do_http_request() { + <xsl:text> this.abort_controller = new AbortController(); + <xsl:text> const decoder = new TextDecoder(); + <xsl:text> let partialChunk = ''; + <xsl:text> const query = { + <xsl:text> startTime: Date.parse(this.params[0]), + <xsl:text> endTime: Date.parse(this.params[1]), + <xsl:text> variableNames: this.params.slice(2) + <xsl:text> const options = { + <xsl:text> method: 'POST', + <xsl:text> body: JSON.stringify(query), + <xsl:text> headers: { 'Content-Type': 'application/json' }, + <xsl:text> signal: this.abort_controller.signal + <xsl:text> return fetch('/history', options) + <xsl:text> .then(response => { + <xsl:text> const reader = response.body.getReader(); + <xsl:text> const read = () => { + <xsl:text> return reader.read().then(({ value, done }) => { + <xsl:text> if (done) return; + <xsl:text> const chunk = decoder.decode(value, { stream: true }); + <xsl:text> const lines = (partialChunk + chunk).split(String.fromCharCode(10)); + <xsl:text> partialChunk = lines.pop(); + <xsl:text> lines.forEach(line => { + <xsl:text> if (line.trim()) { + <xsl:text> const row = JSON.parse(line); + <xsl:text> const vi = query.variableNames.findIndex(v => v === row.varname); + <xsl:text> if (vi !== -1 && this.curves_data[vi]) { + <xsl:text> this.curves_data[vi].push([row.timestamp, row.value]); + <xsl:text> if (row.value > this.ymax) this.ymax = row.value; + <xsl:text> if (row.value < this.ymin) this.ymin = row.value; + <xsl:text> return read(); + <xsl:text> return read(); + <xsl:text> }).catch(this.fetch_error_bound); + <xsl:text> if (this.abort_controller) { + <xsl:text> this.abort_controller.abort(); + <xsl:text> super.unsub(); + <xsl:text> sub(...args){ + <xsl:text> super.sub(...args); + <xsl:text> dispatch(value, oldval, index) { + <xsl:text> this.params[index] = value; + <xsl:text> if (this.params.every((item) => item !== null)) { + <xsl:text> if(!this.loading){ + <xsl:text> this.loading = true; + <xsl:text> this.curves_data = []; + <xsl:text> for (let curve of this.curves) { + <xsl:text> this.curves_data.push([]); + <xsl:text> this.ymin = Infinity; + <xsl:text> this.ymax = -Infinity; + <xsl:text> this.do_http_request().finally(() => { + <xsl:text> let xmin = Infinity; + <xsl:text> let xmax = -Infinity; + <xsl:text> let has_data = false; + <xsl:text> for (let i = 0; i < this.curves.length; i++) { + <xsl:text> const dataLength = this.curves_data[i].length; + <xsl:text> if (dataLength > 1) { + <xsl:text> const ximin = this.curves_data[i][0][0]; + <xsl:text> const ximax = this.curves_data[i][dataLength - 1][0]; + <xsl:text> if (ximin < xmin) xmin = ximin; + <xsl:text> if (ximax > xmax) xmax = ximax; + <xsl:text> has_data = true; + <xsl:text> if (has_data) { + <xsl:text> this.xmin = xmin; + <xsl:text> this.xmax = xmax; + <xsl:text> this.xmin = Date.parse(this.params[0]); + <xsl:text> this.xmax = Date.parse(this.params[1]); + <xsl:text> this.ymin = -1; + <xsl:text> this.ymax = 1; + <xsl:text> let Xrange = this.xmax - this.xmin; + <xsl:text> let Yrange = this.ymax - this.ymin; + <xsl:text> // apply margin by moving min and max to enlarge range + <xsl:text> let [xMargin, yMargin] = zip(this.Margins, [Xrange, Yrange]).map(([m, l]) => m * l); + <xsl:text> [[this.dxmin, this.dxmax], [this.dymin, this.dymax]] = + <xsl:text> [[this.xmin - xMargin, this.xmax + xMargin], + <xsl:text> [this.ymin - yMargin, this.ymax + yMargin]]; + <xsl:text> Xrange += 2 * xMargin; + <xsl:text> Yrange += 2 * yMargin; + <xsl:text> // recompute curves "d" attribute + <xsl:text> let [base_point, xvect, yvect] = this.reference.getBaseRef(); + <xsl:text> this.curves_d_attr = + <xsl:text> zip(this.curves_data, this.curves).map(([data, curve]) => { + <xsl:text> let new_d = data.map(([x, y], i) => { + <xsl:text> // compute curve point from data, ranges, and base_ref + <xsl:text> let xv = vectorscale(xvect, (x - this.dxmin) / Xrange); + <xsl:text> let yv = vectorscale(yvect, (y - this.dymin) / Yrange); + <xsl:text> let px = base_point.x + xv.x + yv.x; + <xsl:text> let py = base_point.y + xv.y + yv.y; + <xsl:text> return " " + px + "," + py; + <xsl:text> new_d.unshift("M "); + <xsl:text> return new_d.join(''); + <xsl:text> // computed curves "d" attr is applied to svg curve during animate(); + <xsl:text> this.request_animate(); + <xsl:text> this.loading = false; + <xsl:text> this.reference.applyRanges([[this.dxmin, this.dxmax], + <xsl:text> [this.dymin, this.dymax]]); + <xsl:text> // apply computed curves "d" attributes + <xsl:text> for (let [curve, d_attr] of zip(this.curves, this.curves_d_attr)) { + <xsl:text> if (d_attr.length > 2) + <xsl:text> curve.setAttribute("d", d_attr); + <xsl:text> curve.setAttribute("d", "M 0 0"); + <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_defs"> + <xsl:param name="hmi_element"/> + <xsl:variable name="disability"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/disabled</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/x_interval_minor_mark /x_axis_line /x_interval_major_mark /x_axis_label</xsl:text> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/y_interval_minor_mark /y_axis_line /y_interval_major_mark /y_axis_label</xsl:text> + <xsl:text> init_specific() { + <xsl:variable name="curves" select="$hmi_element/*[regexp:test(@inkscape:label,'^curve_[0-9]+$')]"/> + <xsl:variable name="curves_error" select="func:check_curves_label_consistency($curves,count($curves)-1)"/> + <xsl:if test="string-length($curves_error)"> + <xsl:message terminate="yes"> + <xsl:text>HistoryXYGraph id="</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>", label="</xsl:text> + <xsl:value-of select="@inkscape:label"/> + <xsl:text>" : </xsl:text> + <xsl:value-of select="$curves_error"/> + <xsl:for-each select="$curves"> + <xsl:variable name="label" select="@inkscape:label"/> + <xsl:variable name="_id" select="@id"/> + <xsl:variable name="curve_num" select="substring(@inkscape:label, 7)"/> + <xsl:text> this.curves[</xsl:text> + <xsl:value-of select="$curve_num"/> + <xsl:text>] = id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>"); /* </xsl:text> + <xsl:value-of select="@inkscape:label"/> <xsl:template match="widget[@type='Swipe']" mode="widget_desc">
<xsl:value-of select="@type"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LPCSVGHMI/widget_historyxygraph.ysl2 Mon Jan 26 11:30:15 2026 +0100
@@ -0,0 +1,254 @@
+// widget_historyxygraph.ysl2 +widget_desc("HistoryXYGraph") { + HistoryXYGraph draws a cartesian trend graph reusing styles given for axis, + grid/marks, legends and curves. + Elements labeled "x_axis" and "y_axis" are svg:groups containing: + - "axis_label" svg:text gives style an alignment for axis labels. + - "interval_major_mark" and "interval_minor_mark" are svg elements to be + duplicated along axis line to form intervals marks. + - "axis_line" svg:path is the axis line. Paths must be intersect and their + bounding box is the chart wall. + Elements labeled "curve_0", "curve_1", ... are paths whose styles are used + to draw curves corresponding to data from variables passed as HMI tree paths. + "curve_0" is mandatory. HMI variables outnumbering given curves are ignored. + shortdesc > Cartesian trend graph showing values of given variables over time + path name="value" count="1+" accepts="HMI_INT,HMI_REAL" > value + arg name="xformat" count="optional" accepts="string" > format string for X label + arg name="yformat" count="optional" accepts="string" > format string for Y label +widget_class("HistoryXYGraph") { + this.params = [null, null]; + [this.x_format, this.y_format] = this.args; + this.fetch_error_bound = this.fetch_error.bind(this); + this.reference = new ReferenceFrame( + [[this.x_interval_minor_mark_elt, this.x_interval_major_mark_elt], + [this.y_interval_minor_mark_elt, this.y_interval_major_mark_elt]], + [this.x_axis_label_elt, this.y_axis_label_elt], + [this.x_axis_line_elt, this.y_axis_line_elt], + [this.x_format, this.y_format]); + let max_stroke_width = 0; + for (let curve of this.curves) { + if (curve.style.strokeWidth > max_stroke_width) { + max_stroke_width = curve.style.strokeWidth; + this.curves_data.push([]); + this.params.push(null); + this.Margins = this.reference.getLengths().map(length => max_stroke_width / length); + // create <clipPath> path and attach it to widget + let clipPath = document.createElementNS(xmlns, "clipPath"); + let clipPathPath = document.createElementNS(xmlns, "path"); + let clipPathPathDattr = document.createAttribute("d"); + clipPathPathDattr.value = this.reference.getClipPathPathDattr(); + clipPathPath.setAttributeNode(clipPathPathDattr); + clipPath.appendChild(clipPathPath); + clipPath.id = randomId(); + this.element.appendChild(clipPath); + // assign created clipPath to clip-path property of curves + for(let curve of this.curves) { + curve.setAttribute("clip-path", "url(#" + clipPath.id + ")"); + console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id); + this.abort_controller = new AbortController(); + const decoder = new TextDecoder(); + startTime: Date.parse(this.params[0]), + endTime: Date.parse(this.params[1]), + variableNames: this.params.slice(2) + body: JSON.stringify(query), + headers: { 'Content-Type': 'application/json' }, + signal: this.abort_controller.signal + return fetch('/history', options) + const reader = response.body.getReader(); + return reader.read().then(({ value, done }) => { + const chunk = decoder.decode(value, { stream: true }); + const lines = (partialChunk + chunk).split(String.fromCharCode(10)); + partialChunk = lines.pop(); + lines.forEach(line => { + const row = JSON.parse(line); + const vi = query.variableNames.findIndex(v => v === row.varname); + if (vi !== -1 && this.curves_data[vi]) { + this.curves_data[vi].push([row.timestamp, row.value]); + if (row.value > this.ymax) this.ymax = row.value; + if (row.value < this.ymin) this.ymin = row.value; + }).catch(this.fetch_error_bound); + if (this.abort_controller) { + this.abort_controller.abort(); + dispatch(value, oldval, index) { + this.params[index] = value; + if (this.params.every((item) => item !== null)) { + for (let curve of this.curves) { + this.curves_data.push([]); + this.do_http_request().finally(() => { + for (let i = 0; i < this.curves.length; i++) { + const dataLength = this.curves_data[i].length; + const ximin = this.curves_data[i][0][0]; + const ximax = this.curves_data[i][dataLength - 1][0]; + if (ximin < xmin) xmin = ximin; + if (ximax > xmax) xmax = ximax; + this.xmin = Date.parse(this.params[0]); + this.xmax = Date.parse(this.params[1]); + let Xrange = this.xmax - this.xmin; + let Yrange = this.ymax - this.ymin; + // apply margin by moving min and max to enlarge range + let [xMargin, yMargin] = zip(this.Margins, [Xrange, Yrange]).map(([m, l]) => m * l); + [[this.dxmin, this.dxmax], [this.dymin, this.dymax]] = + [[this.xmin - xMargin, this.xmax + xMargin], + [this.ymin - yMargin, this.ymax + yMargin]]; + // recompute curves "d" attribute + let [base_point, xvect, yvect] = this.reference.getBaseRef(); + zip(this.curves_data, this.curves).map(([data, curve]) => { + let new_d = data.map(([x, y], i) => { + // compute curve point from data, ranges, and base_ref + let xv = vectorscale(xvect, (x - this.dxmin) / Xrange); + let yv = vectorscale(yvect, (y - this.dymin) / Yrange); + let px = base_point.x + xv.x + yv.x; + let py = base_point.y + xv.y + yv.y; + return " " + px + "," + py; + // computed curves "d" attr is applied to svg curve during animate(); + this.request_animate(); + this.reference.applyRanges([[this.dxmin, this.dxmax], + [this.dymin, this.dymax]]); + // apply computed curves "d" attributes + for (let [curve, d_attr] of zip(this.curves, this.curves_d_attr)) { + curve.setAttribute("d", d_attr); + curve.setAttribute("d", "M 0 0"); +widget_defs("HistoryXYGraph") { + labels("/x_interval_minor_mark /x_axis_line /x_interval_major_mark /x_axis_label"); + labels("/y_interval_minor_mark /y_axis_line /y_interval_major_mark /y_axis_label"); + // collect all curve_n labelled children + const "curves","$hmi_element/*[regexp:test(@inkscape:label,'^curve_[0-9]+$')]"; + const "curves_error", "func:check_curves_label_consistency($curves,count($curves)-1)"; + if "string-length($curves_error)" + error > HistoryXYGraph id="«@id»", label="«@inkscape:label»" : «$curves_error» + const "label","@inkscape:label"; + const "curve_num", "substring(@inkscape:label, 7)"; + | this.curves[«$curve_num»] = id("«@id»"); /* «@inkscape:label» */ --- a/Pous/pousCommon.xml Fri Jan 16 11:50:30 2026 +0100
+++ b/Pous/pousCommon.xml Mon Jan 26 11:30:15 2026 +0100
@@ -1663,6 +1663,353 @@
+ <pou name="log_append" pouType="functionBlock"> + <variable name="ERROR"> + <variable name="RESULT"> + <variable name="VARNAME"> + <variable name="VALUE"> + <variable name="py_eval0"> + <derived name="python_eval"/> + <variable name="R_TRIG1"> + <derived name="R_TRIG"/> + <inVariable localId="8" executionOrderId="0" height="30" width="117" negated="false"> + <position x="432" y="128"/> + <relPosition x="117" y="16"/> + <expression>'LogAppend("'</expression> + <comment localId="29" height="40" width="232"> + <position x="64" y="32"/> + <xhtml:p><![CDATA[Generate python code line]]></xhtml:p> + <block localId="40" width="104" height="80" typeName="python_eval" instanceName="py_eval0" executionOrderId="0"> + <position x="392" y="416"/> + <variable formalParameter="TRIG"> + <relPosition x="0" y="32"/> + <connection refLocalId="46" formalParameter="Q"> + <position x="392" y="448"/> + <position x="200" y="448"/> + <variable formalParameter="CODE"> + <relPosition x="0" y="64"/> + <connection refLocalId="41"> + <position x="392" y="480"/> + <position x="360" y="480"/> + <variable formalParameter="ACK"> + <relPosition x="104" y="32"/> + <variable formalParameter="RESULT"> + <relPosition x="104" y="64"/> + <continuation name="Code" localId="41" height="28" width="128"> + <position x="232" y="464"/> + <relPosition x="128" y="16"/> + <inVariable localId="42" height="27" width="64" executionOrderId="0" negated="false"> + <position x="48" y="432"/> + <relPosition x="64" y="16"/> + <expression>TRIG</expression> + <outVariable localId="43" height="32" width="40" executionOrderId="0" negated="false"> + <position x="840" y="432"/> + <relPosition x="0" y="16"/> + <connection refLocalId="40" formalParameter="ACK"> + <position x="840" y="448"/> + <position x="496" y="448"/> + <expression>ACK</expression> + <outVariable localId="44" height="27" width="64" executionOrderId="0" negated="false"> + <position x="840" y="488"/> + <relPosition x="0" y="16"/> + <connection refLocalId="40" formalParameter="RESULT"> + <position x="840" y="504"/> + <position x="668" y="504"/> + <position x="668" y="480"/> + <position x="496" y="480"/> + <expression>RESULT</expression> + <block localId="46" typeName="R_TRIG" instanceName="R_TRIG1" executionOrderId="0" height="48" width="64"> + <position x="136" y="416"/> + <variable formalParameter="CLK"> + <relPosition x="0" y="32"/> + <connection refLocalId="42"> + <position x="136" y="448"/> + <position x="112" y="448"/> + <variable formalParameter="Q"> + <relPosition x="64" y="32"/> + <block localId="33" typeName="LEFT" executionOrderId="0" height="64" width="56"> + <position x="592" y="520"/> + <variable formalParameter="IN"> + <relPosition x="0" y="32"/> + <connection refLocalId="40" formalParameter="RESULT"> + <position x="592" y="552"/> + <position x="540" y="552"/> + <position x="540" y="480"/> + <position x="496" y="480"/> + <variable formalParameter="L"> + <relPosition x="0" y="56"/> + <connection refLocalId="35"> + <position x="592" y="576"/> + <position x="568" y="576"/> + <variable formalParameter="OUT"> + <relPosition x="56" y="32"/> + <block localId="34" typeName="EQ" executionOrderId="0" height="72" width="64"> + <position x="736" y="520"/> + <variable formalParameter="IN1"> + <relPosition x="0" y="32"/> + <connection refLocalId="33" formalParameter="OUT"> + <position x="736" y="552"/> + <position x="648" y="552"/> + <variable formalParameter="IN2"> + <relPosition x="0" y="56"/> + <connection refLocalId="36"> + <position x="736" y="576"/> + <position x="704" y="576"/> + <variable formalParameter="OUT"> + <relPosition x="64" y="32"/> + <inVariable localId="35" executionOrderId="0" height="27" width="24" negated="false"> + <position x="544" y="560"/> + <relPosition x="24" y="16"/> + <expression>1</expression> + <inVariable localId="36" executionOrderId="0" height="32" width="40" negated="false"> + <position x="664" y="560"/> + <relPosition x="40" y="16"/> + <expression>'#'</expression> + <outVariable localId="54" executionOrderId="0" width="56" height="32" negated="false"> + <position x="840" y="536"/> + <relPosition x="0" y="16"/> + <connection refLocalId="34" formalParameter="OUT"> + <position x="840" y="552"/> + <position x="800" y="552"/> + <expression>ERROR</expression> + <block localId="7" typeName="CONCAT" executionOrderId="0" height="246" width="78"> + <position x="592" y="104"/> + <variable formalParameter="IN1"> + <relPosition x="0" y="40"/> + <connection refLocalId="8"> + <position x="592" y="144"/> + <position x="549" y="144"/> + <variable formalParameter="IN2"> + <relPosition x="0" y="88"/> + <connection refLocalId="2"> + <position x="592" y="192"/> + <position x="352" y="192"/> + <variable formalParameter="IN3"> + <relPosition x="0" y="136"/> + <connection refLocalId="10"> + <position x="592" y="240"/> + <position x="552" y="240"/> + <variable formalParameter="IN4"> + <relPosition x="0" y="176"/> + <connection refLocalId="3"> + <position x="592" y="280"/> + <position x="352" y="280"/> + <variable formalParameter="IN5"> + <relPosition x="0" y="224"/> + <connection refLocalId="1"> + <position x="592" y="328"/> + <position x="550" y="328"/> + <variable formalParameter="OUT"> + <relPosition x="78" y="40"/> + <inVariable localId="2" executionOrderId="0" height="32" width="112" negated="false"> + <position x="240" y="176"/> + <relPosition x="112" y="16"/> + <expression>VARNAME</expression> + <inVariable localId="10" executionOrderId="0" height="27" width="112" negated="false"> + <position x="440" y="224"/> + <relPosition x="112" y="16"/> + <expression>'","'</expression> + <inVariable localId="3" executionOrderId="0" height="32" width="112" negated="false"> + <position x="240" y="264"/> + <relPosition x="112" y="16"/> + <expression>VALUE</expression> + <connector name="Code" localId="19" height="28" width="128"> + <position x="728" y="128"/> + <relPosition x="0" y="16"/> + <connection refLocalId="7" formalParameter="OUT"> + <position x="728" y="144"/> + <position x="670" y="144"/> + <inVariable localId="1" executionOrderId="0" height="33" width="118" negated="false"> + <position x="432" y="312"/> + <relPosition x="118" y="16"/> + <expression>'")'</expression>