- rotatingMachine.html: add default name:{value:""} to the editor
defaults block (standard Node-RED pattern; was missing).
- nodeClass.js: clear node status badge on close — matches the
pattern already in other EVOLV node close handlers.
- specificClass.js: remove the `(x <= 0) ? 0 : ...` guard in the
flow and power prediction methods. The guard was redundant:
predictions only run while the FSM is in an active state
(operational / starting / warmingup / accelerating / decelerating),
none of which produce x=0. Math.max(0, rawFlow) still clamps
negative extrapolation. Net: same behaviour in production, less
dead code.
All 10 basic tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
230 lines
11 KiB
HTML
230 lines
11 KiB
HTML
<!--
|
||
| S88-niveau | Primair (blokkleur) | Tekstkleur |
|
||
| ---------------------- | ------------------- | ---------- |
|
||
| **Area** | `#0f52a5` | wit |
|
||
| **Process Cell** | `#0c99d9` | wit |
|
||
| **Unit** | `#50a8d9` | zwart |
|
||
| **Equipment (Module)** | `#86bbdd` | zwart |
|
||
| **Control Module** | `#a9daee` | zwart |
|
||
|
||
-->
|
||
<!-- Load the dynamic menu & config endpoints -->
|
||
<script src="/rotatingMachine/menu.js"></script>
|
||
<script src="/rotatingMachine/configData.js"></script>
|
||
|
||
<script>
|
||
RED.nodes.registerType("rotatingMachine", {
|
||
category: "EVOLV",
|
||
color: "#86bbdd",
|
||
defaults: {
|
||
name: { value: "" },
|
||
|
||
// Define specific properties
|
||
speed: { value: 1, required: true },
|
||
startup: { value: 0 },
|
||
warmup: { value: 0 },
|
||
shutdown: { value: 0 },
|
||
cooldown: { value: 0 },
|
||
movementMode : { value: "staticspeed" }, // static or dynamic
|
||
machineCurve : { value: {}},
|
||
processOutputFormat: { value: "process" },
|
||
dbaseOutputFormat: { value: "influxdb" },
|
||
|
||
//define asset properties
|
||
uuid: { value: "" },
|
||
assetTagNumber: { value: "" },
|
||
supplier: { value: "" },
|
||
category: { value: "" },
|
||
assetType: { value: "" },
|
||
model: { value: "" },
|
||
unit: { value: "" },
|
||
curvePressureUnit: { value: "mbar" },
|
||
curveFlowUnit: { value: "" },
|
||
curvePowerUnit: { value: "kW" },
|
||
curveControlUnit: { value: "%" },
|
||
|
||
//logger properties
|
||
enableLog: { value: false },
|
||
logLevel: { value: "error" },
|
||
|
||
//physicalAspect
|
||
positionVsParent: { value: "" },
|
||
positionIcon: { value: "" },
|
||
hasDistance: { value: false },
|
||
distance: { value: 0 },
|
||
distanceUnit: { value: "m" },
|
||
distanceDescription: { value: "" }
|
||
|
||
},
|
||
inputs: 1,
|
||
outputs: 3,
|
||
inputLabels: ["Input"],
|
||
outputLabels: ["process", "dbase", "parent"],
|
||
icon: "font-awesome/fa-cog",
|
||
|
||
label: function () {
|
||
return (this.positionIcon || "") + " " + (this.category || "Machine");
|
||
},
|
||
|
||
oneditprepare: function() {
|
||
// wait for the menu scripts to load
|
||
let menuRetries = 0;
|
||
const maxMenuRetries = 100; // 5 seconds at 50ms intervals
|
||
const waitForMenuData = () => {
|
||
if (window.EVOLV?.nodes?.rotatingMachine?.initEditor) {
|
||
window.EVOLV.nodes.rotatingMachine.initEditor(this);
|
||
} else if (++menuRetries < maxMenuRetries) {
|
||
setTimeout(waitForMenuData, 50);
|
||
} else {
|
||
console.warn("rotatingMachine: menu scripts failed to load within 5 seconds");
|
||
}
|
||
};
|
||
waitForMenuData();
|
||
|
||
// your existing project‐settings & asset dropdown logic can remain here
|
||
document.getElementById("node-input-speed");
|
||
document.getElementById("node-input-startup");
|
||
document.getElementById("node-input-warmup");
|
||
document.getElementById("node-input-shutdown");
|
||
document.getElementById("node-input-cooldown");
|
||
const movementMode = document.getElementById("node-input-movementMode");
|
||
if (movementMode) {
|
||
movementMode.value = this.movementMode || "staticspeed";
|
||
}
|
||
|
||
},
|
||
oneditsave: function() {
|
||
const node = this;
|
||
|
||
// save asset fields
|
||
if (window.EVOLV?.nodes?.rotatingMachine?.assetMenu?.saveEditor) {
|
||
window.EVOLV.nodes.rotatingMachine.assetMenu.saveEditor(this);
|
||
}
|
||
// save logger fields
|
||
if (window.EVOLV?.nodes?.rotatingMachine?.loggerMenu?.saveEditor) {
|
||
window.EVOLV.nodes.rotatingMachine.loggerMenu.saveEditor(this);
|
||
}
|
||
// save position field
|
||
if (window.EVOLV?.nodes?.rotatingMachine?.positionMenu?.saveEditor) {
|
||
window.EVOLV.nodes.rotatingMachine.positionMenu.saveEditor(this);
|
||
}
|
||
|
||
["speed", "startup", "warmup", "shutdown", "cooldown"].forEach((field) => {
|
||
const element = document.getElementById(`node-input-${field}`);
|
||
const value = parseFloat(element?.value) || 0;
|
||
console.log(`----------------> Saving ${field}: ${value}`);
|
||
node[field] = value;
|
||
});
|
||
|
||
node.movementMode = document.getElementById("node-input-movementMode").value;
|
||
console.log(`----------------> Saving movementMode: ${node.movementMode}`);
|
||
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<!-- Main UI Template -->
|
||
<script type="text/html" data-template-name="rotatingMachine">
|
||
|
||
<!-- Machine-specific controls -->
|
||
<div class="form-row">
|
||
<label for="node-input-speed"><i class="fa fa-clock-o"></i> Reaction Speed</label>
|
||
<input type="number" id="node-input-speed" style="width:60%;" placeholder="position units / second" />
|
||
<div style="font-size:11px;color:#666;margin-left:160px;">Ramp rate of the controller position in units per second (0–100% controller range; e.g. 1 = 1%/s).</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="node-input-startup"><i class="fa fa-clock-o"></i> Startup Time</label>
|
||
<input type="number" id="node-input-startup" style="width:60%;" placeholder="seconds" />
|
||
<div style="font-size:11px;color:#666;margin-left:160px;">Seconds spent in the <code>starting</code> state before moving to <code>warmingup</code>.</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="node-input-warmup"><i class="fa fa-clock-o"></i> Warmup Time</label>
|
||
<input type="number" id="node-input-warmup" style="width:60%;" placeholder="seconds" />
|
||
<div style="font-size:11px;color:#666;margin-left:160px;">Seconds spent in the protected <code>warmingup</code> state before reaching <code>operational</code>.</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="node-input-shutdown"><i class="fa fa-clock-o"></i> Shutdown Time</label>
|
||
<input type="number" id="node-input-shutdown" style="width:60%;" placeholder="seconds" />
|
||
<div style="font-size:11px;color:#666;margin-left:160px;">Seconds spent in the <code>stopping</code> state before moving to <code>coolingdown</code>.</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="node-input-cooldown"><i class="fa fa-clock-o"></i> Cooldown Time</label>
|
||
<input type="number" id="node-input-cooldown" style="width:60%;" placeholder="seconds" />
|
||
<div style="font-size:11px;color:#666;margin-left:160px;">Seconds spent in the protected <code>coolingdown</code> state before returning to <code>idle</code>.</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="node-input-movementMode"><i class="fa fa-exchange"></i> Movement Mode</label>
|
||
<select id="node-input-movementMode" style="width:60%;">
|
||
<option value="staticspeed">Static</option>
|
||
<option value="dynspeed">Dynamic</option>
|
||
</select>
|
||
</div>
|
||
|
||
<h3>Output Formats</h3>
|
||
<div class="form-row">
|
||
<label for="node-input-processOutputFormat"><i class="fa fa-random"></i> Process Output</label>
|
||
<select id="node-input-processOutputFormat" style="width:60%;">
|
||
<option value="process">process</option>
|
||
<option value="json">json</option>
|
||
<option value="csv">csv</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label>
|
||
<select id="node-input-dbaseOutputFormat" style="width:60%;">
|
||
<option value="influxdb">influxdb</option>
|
||
<option value="json">json</option>
|
||
<option value="csv">csv</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Asset fields injected here -->
|
||
<div id="asset-fields-placeholder"></div>
|
||
|
||
<!-- Logger fields injected here -->
|
||
<div id="logger-fields-placeholder"></div>
|
||
|
||
<!-- Position fields injected here -->
|
||
<div id="position-fields-placeholder"></div>
|
||
|
||
</script>
|
||
|
||
<script type="text/html" data-help-name="rotatingMachine">
|
||
<p><b>Rotating Machine</b>: individual pump / compressor / blower control module. Runs a 10-state S88 sequence, predicts flow and power from a supplier curve, and publishes process + telemetry outputs each second.</p>
|
||
|
||
<h3>Configuration</h3>
|
||
<ul>
|
||
<li><b>Reaction Speed</b>: controller ramp rate (position units / second). E.g. <code>1</code> = 1%/s, so Set 60% from idle reaches 60% in ~60 s.</li>
|
||
<li><b>Startup / Warmup / Shutdown / Cooldown</b>: seconds per FSM phase. Warmup and Cooldown are <i>protected</i> — they cannot be aborted by a new command.</li>
|
||
<li><b>Movement Mode</b>: <code>staticspeed</code> = linear ramp; <code>dynspeed</code> = ease-in/out.</li>
|
||
<li><b>Asset</b> (menu): supplier, category, model (must match a curve in <code>generalFunctions</code>), flow unit (e.g. m³/h), curve units.</li>
|
||
<li><b>Output Formats</b>: <code>process</code>/<code>json</code>/<code>csv</code> on port 0; <code>influxdb</code>/<code>json</code>/<code>csv</code> on port 1.</li>
|
||
<li><b>Position</b> (menu): <code>upstream</code> / <code>atEquipment</code> / <code>downstream</code> relative to a parent group/station.</li>
|
||
</ul>
|
||
|
||
<h3>Input topics (<code>msg.topic</code>)</h3>
|
||
<ul>
|
||
<li><code>setMode</code> — <code>payload</code> = <code>auto</code> | <code>virtualControl</code> | <code>fysicalControl</code></li>
|
||
<li><code>execSequence</code> — <code>payload</code> = <code>{source, action:"execSequence", parameter: "startup"|"shutdown"|"entermaintenance"|"exitmaintenance"}</code></li>
|
||
<li><code>execMovement</code> — <code>payload</code> = <code>{source, action:"execMovement", setpoint: 0..100}</code> (controller %)</li>
|
||
<li><code>flowMovement</code> — <code>payload</code> = <code>{source, action:"flowMovement", setpoint: <flow in configured unit>}</code></li>
|
||
<li><code>emergencystop</code> — <code>payload</code> = <code>{source, action:"emergencystop"}</code>. Aborts any active movement.</li>
|
||
<li><code>simulateMeasurement</code> — <code>payload</code> = <code>{type:"pressure"|"flow"|"temperature"|"power", position, value, unit}</code>. Injects dashboard-side measurement.</li>
|
||
<li><code>showWorkingCurves</code>, <code>CoG</code> — diagnostics, reply arrives on port 0.</li>
|
||
</ul>
|
||
|
||
<h3>Output ports</h3>
|
||
<ol>
|
||
<li><b>process</b> — delta-compressed process payload. Consumers must cache and merge each tick. Keys use 4-segment format <code>type.variant.position.childId</code> (e.g. <code>flow.predicted.downstream.default</code>).</li>
|
||
<li><b>dbase</b> — InfluxDB telemetry.</li>
|
||
<li><b>parent</b> — <code>registerChild</code> handshake for a parent <code>machineGroupControl</code> / <code>pumpingStation</code>.</li>
|
||
</ol>
|
||
|
||
<h3>State machine</h3>
|
||
<p>States: <code>idle → starting → warmingup → operational → (accelerating ⇄ decelerating) → operational → stopping → coolingdown → idle</code>. <code>emergencystop → off</code> is reachable from every active state.</p>
|
||
<p>If a <code>shutdown</code> or <code>emergencystop</code> sequence is requested while a setpoint move is in flight (<code>accelerating</code> / <code>decelerating</code>), the move is aborted automatically and the sequence proceeds once the FSM returns to <code>operational</code>.</p>
|
||
|
||
<h3>Predictions</h3>
|
||
<p>Flow and power predictions only produce meaningful values once at least one pressure child is reporting (or a <code>simulateMeasurement</code> pressure is injected). Inject BOTH upstream and downstream for best accuracy.</p>
|
||
</script>
|