state.moveTo: unpark post-abort residue on new setpoint

When MGC's per-tick abortActiveMovements parks the FSM in
'accelerating'/'decelerating' to avoid a bounce loop, a subsequent
moveTo previously fell into the early-return path and saved the new
setpoint to delayedMove — which never fired because nothing transitioned
back to 'operational'. Now distinguish residue states from genuine
non-operational states (starting/warmingup/...) and force-transition
out of residue so the new setpoint actually executes. Also picks up
in-flight predict shareInputsFrom plumbing and pumpingStation.json
stopLevel doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rene De Ren
2026-05-08 11:19:34 +02:00
parent 94bcc90b4b
commit 9a998191cd
3 changed files with 102 additions and 11 deletions

View File

@@ -68,6 +68,13 @@ const Interpolation = require('./interpolation');
class Predict {
constructor(config = {}) {
// Capture share-source BEFORE config validation strips it (ConfigUtils
// mutates the input config to drop unknown keys, which would remove
// shareInputsFrom because it's not in predictConfig.json's schema).
const _sharedSource = (config && config.shareInputsFrom instanceof Predict)
? config.shareInputsFrom
: null;
// Initialize dependencies
this.emitter = new EventEmitter(); // Own EventEmitter
this.configUtils = new ConfigUtils(defaultConfig);
@@ -107,8 +114,29 @@ class Predict {
this.calculationPoints = this.config.normalization.parameters.curvePoints;
this.interpolationType = this.config.interpolation.type;
// Load curve if provided
if (config.curve) {
// Load curve if provided.
// shareInputsFrom: an existing Predict instance whose pre-built input
// curves and splines we adopt by reference. Used to create a parallel
// "view" of the same source curves (e.g. an MGC group-scope predict
// that mirrors a pump's individual predict). Per-instance state —
// currentF / currentX / currentFxyCurve / currentFxySplines /
// currentFxyY/X Min/Max / outputY — stays freshly initialised so the
// two views have independent operating points. Curve mutations on the
// source via updateCurve() are propagated through the source's
// "curveUpdated" emitter (see updateCurve below).
if (_sharedSource) {
this._adoptInputsFrom(_sharedSource);
this._sharedInputsSource = _sharedSource;
this._sharedInputsHandler = (newCurve) => {
this._adoptInputsFrom(this._sharedInputsSource);
// Keep our currentF in range; constrain re-uses the new fValues.
this.fDimension = this.constrain(this.currentF, this.fValues.min, this.fValues.max);
};
this._sharedInputsSource.emitter.on('curveUpdated', this._sharedInputsHandler);
// Initialise our own operating point to the source's min, same as
// the standard buildAllFxyCurves flow does at end of curve load.
this.fDimension = this.fValues.min;
} else if (config.curve) {
this.inputCurveData = config.curve;
} else {
this.logger.warn("No curve data provided. Please set curve data using setCurveData method. Using default");
@@ -116,6 +144,31 @@ class Predict {
}
}
// Adopt another Predict's input curves and splines by reference. Used by
// the shareInputsFrom constructor option and by the curveUpdated emitter
// handler to re-sync after the source's curves change. Does NOT touch
// per-instance state (currentF, currentX, currentFxy* etc.).
//
// Also copies the scalar parameters (calculationPoints, normMin/Max,
// interpolationType) so the clone uses the SAME pointsCount the source
// built fSplines with — otherwise buildSingleFxyCurve can iterate past
// the end of the shared fSplines.
_adoptInputsFrom(source) {
this.inputCurve = source.inputCurve;
this.normalizedCurve = source.normalizedCurve;
this.calculatedCurve = source.calculatedCurve;
this.fCurve = source.fCurve;
this.fSplines = source.fSplines;
this.normalizedSplines = source.normalizedSplines;
this.xValues = source.xValues;
this.fValues = source.fValues;
this.yValues = source.yValues;
this.calculationPoints = source.calculationPoints;
this.normMin = source.normMin;
this.normMax = source.normMax;
this.interpolationType = source.interpolationType;
}
// Improved function to get a local peak in an array by starting in the middle.
// It also handles the case of a tie by preferring the left side (arbitrary choice)
@@ -348,6 +401,9 @@ class Predict {
this.buildAllFxyCurves(validatedCurve);
// Notify shared-input clones (see shareInputsFrom in the constructor).
// They re-adopt our inputs and clamp their own operating point.
this.emitter.emit('curveUpdated', validatedCurve);
}
constrain(value,min,max) {