Compare commits
2 Commits
a516c2b2b6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a998191cd | ||
|
|
94bcc90b4b |
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Local stub generated by `npm install` in the submodule directory.
|
||||||
|
# generalFunctions has no production deps of its own.
|
||||||
|
package-lock.json
|
||||||
@@ -498,7 +498,16 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"min": 0,
|
"min": 0,
|
||||||
"description": "Lower shifted ramp point used while draining. While filling, demand is held at 0 % from startLevel through inletLevel, then scales from inletLevel to maxLevel."
|
"description": "Pump-on threshold and ramp foot. Below this level demand is 0 %; at or above it demand scales 0 → 100 % across [startLevel, maxLevel] using the configured curve (linear or log). When enableShiftedRamp is on, this also serves as the bottom of the held-then-ramp curve during draining."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stopLevel": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true,
|
||||||
|
"min": 0,
|
||||||
|
"description": "Optional pump-off threshold. When set, PS sends an explicit turnOffAllMachines command to MGC the moment level drops below stopLevel. Independent of the ramp scaling — does NOT shift where the ramp starts. Pair with a startLevel above stopLevel to get hysteresis (pumps engage at startLevel rising, disengage at stopLevel falling). Must be ≥ minLevel and ≤ startLevel."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"maxLevel": {
|
"maxLevel": {
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ const Interpolation = require('./interpolation');
|
|||||||
class Predict {
|
class Predict {
|
||||||
constructor(config = {}) {
|
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
|
// Initialize dependencies
|
||||||
this.emitter = new EventEmitter(); // Own EventEmitter
|
this.emitter = new EventEmitter(); // Own EventEmitter
|
||||||
this.configUtils = new ConfigUtils(defaultConfig);
|
this.configUtils = new ConfigUtils(defaultConfig);
|
||||||
@@ -107,8 +114,29 @@ class Predict {
|
|||||||
this.calculationPoints = this.config.normalization.parameters.curvePoints;
|
this.calculationPoints = this.config.normalization.parameters.curvePoints;
|
||||||
this.interpolationType = this.config.interpolation.type;
|
this.interpolationType = this.config.interpolation.type;
|
||||||
|
|
||||||
// Load curve if provided
|
// Load curve if provided.
|
||||||
if (config.curve) {
|
// 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;
|
this.inputCurveData = config.curve;
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn("No curve data provided. Please set curve data using setCurveData method. Using default");
|
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.
|
// 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)
|
// It also handles the case of a tie by preferring the left side (arbitrary choice)
|
||||||
@@ -348,6 +401,9 @@ class Predict {
|
|||||||
|
|
||||||
this.buildAllFxyCurves(validatedCurve);
|
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) {
|
constrain(value,min,max) {
|
||||||
|
|||||||
@@ -66,15 +66,41 @@ class state{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.stateManager.getCurrentState() !== "operational") {
|
if (this.stateManager.getCurrentState() !== "operational") {
|
||||||
if (this.config.mode.current === "auto") {
|
// 'accelerating' / 'decelerating' here is post-abort residue —
|
||||||
this.delayedMove = targetPosition;
|
// the previous moveTo was aborted (e.g. MGC's per-tick
|
||||||
this.logger.warn(`Saving setpoint=${targetPosition} to execute once back in 'operational' state.`);
|
// abortActiveMovements) and the catch block intentionally
|
||||||
|
// doesn't auto-return to operational (avoids a bounce loop).
|
||||||
|
// BUT a new setpoint just arrived, so there's nothing for the
|
||||||
|
// anti-bounce policy to protect: the caller IS asking for a
|
||||||
|
// move. Fall through to operational and execute it. Without
|
||||||
|
// this the FSM gets parked, all subsequent setpoints land in
|
||||||
|
// delayedMove which never fires, and currentPosition freezes —
|
||||||
|
// see test/integration/abort-deadlock.integration.test.js for
|
||||||
|
// the exact deadlock scenario.
|
||||||
|
const movementResidueStates = ['accelerating', 'decelerating'];
|
||||||
|
if (movementResidueStates.includes(this.stateManager.getCurrentState())) {
|
||||||
|
this.logger.debug(`moveTo(${targetPosition}) arrived while parked in '${this.stateManager.getCurrentState()}' (post-abort). Returning to operational to service the new setpoint.`);
|
||||||
|
try {
|
||||||
|
await this.transitionToState("operational");
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`Could not transition out of '${this.stateManager.getCurrentState()}': ${e?.message || e}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fall through — state is now operational, proceed with new move.
|
||||||
|
} else {
|
||||||
|
// Genuine non-operational state (starting, warmingup, stopping,
|
||||||
|
// coolingdown, idle, off, emergencystop, maintenance) — these
|
||||||
|
// are sequence steps the caller can't legitimately interrupt
|
||||||
|
// with a setpoint. Save for later, exactly as before.
|
||||||
|
if (this.config.mode.current === "auto") {
|
||||||
|
this.delayedMove = targetPosition;
|
||||||
|
this.logger.warn(`Saving setpoint=${targetPosition} to execute once back in 'operational' state.`);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.logger.warn(`Not able to accept setpoint=${targetPosition} while not in ${this.stateManager.getCurrentState()} state`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else{
|
|
||||||
this.logger.warn(`Not able to accept setpoint=${targetPosition} while not in ${this.stateManager.getCurrentState()} state`);
|
|
||||||
}
|
|
||||||
//return early
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
const { signal } = this.abortController;
|
const { signal } = this.abortController;
|
||||||
|
|||||||
Reference in New Issue
Block a user