fix: interruptible shutdown/emergencystop + dual-curve test coverage

Runtime:
- executeSequence now normalizes sequenceName to lowercase so parent
  orchestrators that use 'emergencyStop' (capital S) route correctly to
  the 'emergencystop' sequence key. Closes the "Sequence 'emergencyStop'
  not defined" warn seen when commands reach the node during accelerating.
- When a shutdown or emergencystop sequence is requested while the FSM is
  in accelerating/decelerating, the active movement is aborted via
  state.abortCurrentMovement() and the sequence waits (up to 2s) for the
  FSM to return to 'operational' before proceeding. New helper
  _waitForOperational listens on the state emitter for the transition.
- Single-side pressure warning: fix "acurate" typo and make the message
  actionable.

Tests (+15, now 91/91 passing):
- test/integration/interruptible-movement.integration.test.js (+3):
  shutdown during accelerating -> idle; emergencystop during accelerating
  -> off; mixed-case sequence-name normalization.
- test/integration/curve-prediction.integration.test.js (+12):
  parametrized across both shipped pump curves (hidrostal-H05K-S03R and
  hidrostal-C5-D03R-SHN1). Verifies loader integrity, mid-range prediction
  sanity, flow monotonicity in ctrl, inverse-pressure monotonicity, CoG
  finiteness, and reverse-predictor round-trip.

E2E:
- test/e2e/curve-prediction-benchmark.py: live Dockerized Node-RED
  benchmark that deploys one rotatingMachine per curve and runs a per-pump
  (pressure x ctrl) sweep inside each curve's envelope. Reports envelope
  compliance and monotonicity.
- test/e2e/README.md documents the benchmark and a known limitation:
  pressure below the curve's minimum slice extrapolates wildly
  (defended by upstream measurement-node clamping in production).

UX:
- rotatingMachine.html: added placeholders and descriptions for Reaction
  Speed / Startup / Warmup / Shutdown / Cooldown. Expanded the Node-RED
  help panel with a topic reference, port documentation, state diagram,
  and prediction rules.

Docs:
- README.md rewritten (was a single line) with install, quick start,
  topic/port reference, state machine, predictions, testing, production
  status.

Depends on generalFunctions commit 75d16c6 (state.js abort recovery and
rotatingMachine schema additions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-04-13 13:21:48 +02:00
parent 07af7cef40
commit 17b88870bb
7 changed files with 984 additions and 13 deletions

View File

@@ -128,23 +128,28 @@
<!-- 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%;" />
<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 (0100% 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%;" />
<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%;" />
<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%;" />
<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%;" />
<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>
@@ -184,11 +189,40 @@
</script>
<script type="text/html" data-help-name="rotatingMachine">
<p><b>Rotating Machine Node</b>: Configure a rotatingmachine asset.</p>
<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, Startup, Warmup, Shutdown, Cooldown:</b> timing parameters.</li>
<li><b>Supplier / SubType / Model / Unit:</b> choose via Asset menu.</li>
<li><b>Enable Log / Log Level:</b> toggle via Logger menu.</li>
<li><b>Position:</b> set Upstream / At Equipment / Downstream via Position menu.</li>
<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&nbsp;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: &lt;flow in configured unit&gt;}</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>