lpcmanager

iec60870: add draft C template files and linking with crossbuilt IEC60870 library.
// widget_historyxygraph.ysl2
widget_desc("HistoryXYGraph") {
longdesc
||
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") {
||
frequency = 1;
init() {
this.params = [null, null];
[this.x_format, this.y_format] = this.args;
this.fetch_error_bound = this.fetch_error.bind(this);
this.loading = false;
this.curves = [];
this.curves_data = [];
this.init_specific();
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 + ")");
}
}
fetch_error(e){
console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id);
}
do_http_request() {
this.abort_controller = new AbortController();
const decoder = new TextDecoder();
let partialChunk = '';
const query = {
startTime: Date.parse(this.params[0]),
endTime: Date.parse(this.params[1]),
variableNames: this.params.slice(2)
};
const options = {
method: 'POST',
body: JSON.stringify(query),
headers: { 'Content-Type': 'application/json' },
signal: this.abort_controller.signal
};
return fetch('/history', options)
.then(response => {
const reader = response.body.getReader();
const read = () => {
return reader.read().then(({ value, done }) => {
if (done) return;
const chunk = decoder.decode(value, { stream: true });
const lines = (partialChunk + chunk).split(String.fromCharCode(10));
partialChunk = lines.pop();
lines.forEach(line => {
if (line.trim()) {
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;
}
}
});
return read();
});
};
return read();
}).catch(this.fetch_error_bound);
}
unsub() {
if (this.abort_controller) {
this.abort_controller.abort();
}
super.unsub();
}
sub(...args){
super.sub(...args);
}
dispatch(value, oldval, index) {
this.params[index] = value;
if (this.params.every((item) => item !== null)) {
if(!this.loading){
this.loading = true;
this.curves_data = [];
for (let curve of this.curves) {
this.curves_data.push([]);
}
this.ymin = Infinity;
this.ymax = -Infinity;
this.do_http_request().finally(() => {
let xmin = Infinity;
let xmax = -Infinity;
let has_data = false;
for (let i = 0; i < this.curves.length; i++) {
const dataLength = this.curves_data[i].length;
if (dataLength > 1) {
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;
has_data = true;
}
}
if (has_data) {
this.xmin = xmin;
this.xmax = xmax;
} else {
this.xmin = Date.parse(this.params[0]);
this.xmax = Date.parse(this.params[1]);
this.ymin = -1;
this.ymax = 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]];
Xrange += 2 * xMargin;
Yrange += 2 * yMargin;
// recompute curves "d" attribute
let [base_point, xvect, yvect] = this.reference.getBaseRef();
this.curves_d_attr =
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;
});
new_d.unshift("M ");
return new_d.join('');
});
// computed curves "d" attr is applied to svg curve during animate();
this.request_animate();
this.loading = false;
});
}
}
}
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)) {
if (d_attr.length > 2)
curve.setAttribute("d", d_attr);
else
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");
| init_specific() {
// 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»
foreach "$curves" {
const "label","@inkscape:label";
const "_id","@id";
const "curve_num", "substring(@inkscape:label, 7)";
| this.curves[«$curve_num»] = id("«@id»"); /* «@inkscape:label» */
}
| }
}