153 lines
6.2 KiB
JavaScript
153 lines
6.2 KiB
JavaScript
|
|
// Wiring helpers for cross-node end-to-end tests.
|
|||
|
|
//
|
|||
|
|
// Builds a small physical plant in pure JS:
|
|||
|
|
// - 3 rotatingMachine pumps (centrifugal, identical curves)
|
|||
|
|
// - 1 machineGroupControl coordinating them
|
|||
|
|
// - 1 pumpingStation owning a wet-well basin and the MGC
|
|||
|
|
//
|
|||
|
|
// Pumps register as children of the MGC. The MGC registers as a child of
|
|||
|
|
// the PS. This mirrors what Node-RED's registerChild messages do at runtime.
|
|||
|
|
//
|
|||
|
|
// A controllable clock replaces Date.now so _updatePredictedVolume's deltaT
|
|||
|
|
// is exact regardless of wall-clock time.
|
|||
|
|
|
|||
|
|
const PumpingStation = require('../../nodes/pumpingStation/src/specificClass');
|
|||
|
|
const MachineGroup = require('../../nodes/machineGroupControl/src/specificClass');
|
|||
|
|
const Machine = require('../../nodes/rotatingMachine/src/specificClass');
|
|||
|
|
|
|||
|
|
// ---------------- configs (mirror what the demo flow ships) ----------------
|
|||
|
|
|
|||
|
|
function pumpConfig(id) {
|
|||
|
|
return {
|
|||
|
|
general: { id, name: id, unit: 'm3/h',
|
|||
|
|
logging: { enabled: false, logLevel: 'error' } },
|
|||
|
|
functionality: { softwareType: 'machine', role: 'rotationaldevicecontroller',
|
|||
|
|
positionVsParent: 'atEquipment' },
|
|||
|
|
asset: { category: 'pump', type: 'centrifugal',
|
|||
|
|
model: 'hidrostal-H05K-S03R', supplier: 'hidrostal',
|
|||
|
|
curveUnits: { pressure: 'mbar', flow: 'm3/h', power: 'kW', control: '%' } },
|
|||
|
|
mode: {
|
|||
|
|
current: 'auto',
|
|||
|
|
allowedActions: { auto: ['execsequence', 'execmovement', 'flowmovement', 'statuscheck'] },
|
|||
|
|
allowedSources: { auto: ['parent', 'GUI'] },
|
|||
|
|
},
|
|||
|
|
sequences: {
|
|||
|
|
startup: ['starting', 'warmingup', 'operational'],
|
|||
|
|
shutdown: ['stopping', 'coolingdown', 'idle'],
|
|||
|
|
emergencystop: ['emergencystop', 'off'],
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function pumpStateConfig() {
|
|||
|
|
return {
|
|||
|
|
general: { logging: { enabled: false, logLevel: 'error' } },
|
|||
|
|
state: { current: 'idle' },
|
|||
|
|
movement: { mode: 'staticspeed', speed: 1200, maxSpeed: 1800, interval: 10 },
|
|||
|
|
time: { starting: 0, warmingup: 0, stopping: 0, coolingdown: 0 },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mgcConfig() {
|
|||
|
|
return {
|
|||
|
|
general: { name: 'mgc', id: 'mgc', logging: { enabled: false, logLevel: 'error' } },
|
|||
|
|
functionality: { softwareType: 'machinegroup', role: 'groupcontroller',
|
|||
|
|
positionVsParent: 'atEquipment' },
|
|||
|
|
scaling: { current: 'normalized' },
|
|||
|
|
mode: { current: 'optimalcontrol' },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function psConfig(overrides = {}) {
|
|||
|
|
return {
|
|||
|
|
general: { id: 'ps', name: 'ps', unit: 'm3/h',
|
|||
|
|
logging: { enabled: false, logLevel: 'error' },
|
|||
|
|
flowThreshold: 1e-4 },
|
|||
|
|
functionality: { softwareType: 'pumpingstation', role: 'stationcontroller',
|
|||
|
|
positionVsParent: 'atEquipment' },
|
|||
|
|
basin: {
|
|||
|
|
// Sized so the [stopLevel,startLevel] band holds enough water that
|
|||
|
|
// a single pump at min flow (~99 m³/h) drains for ~5 min while
|
|||
|
|
// nominal inflow (~25 m³/h) refills it in ~15 min.
|
|||
|
|
// 0.5 m × 12.5 m² = 6.25 m³ (drain time = 6.25 / (99-25) m³/h ≈ 5 min)
|
|||
|
|
volume: 50, height: 4,
|
|||
|
|
inflowLevel: 2.5, outflowLevel: 0.3, overflowLevel: 3.8,
|
|||
|
|
inletPipeDiameter: 0.4, outletPipeDiameter: 0.3,
|
|||
|
|
},
|
|||
|
|
hydraulics: { refHeight: 'NAP', basinBottomRef: 0, minHeightBasedOn: 'outlet' },
|
|||
|
|
control: {
|
|||
|
|
mode: 'levelbased',
|
|||
|
|
allowedModes: new Set(['levelbased', 'manual']),
|
|||
|
|
levelbased: {
|
|||
|
|
minLevel: 0.5, startLevel: 2.5, stopLevel: 2.0, maxLevel: 3.5,
|
|||
|
|
curveType: 'linear', logCurveFactor: 9,
|
|||
|
|
deadZoneKeepAlivePercent: 1, // % sent to MGC while engaged in [stopLvl, startLevel]
|
|||
|
|
enableShiftedRamp: false, shiftLevel: null, shiftArmPercent: 95,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
safety: {
|
|||
|
|
enableDryRunProtection: true, enableOverfillProtection: true,
|
|||
|
|
dryRunThresholdPercent: 5, highVolumeSafetyThresholdPercent: 95,
|
|||
|
|
overfillThresholdPercent: 95, timeleftToFullOrEmptyThresholdSeconds: 0,
|
|||
|
|
},
|
|||
|
|
...overrides,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------- harness ----------------
|
|||
|
|
|
|||
|
|
function buildPlant({ initialBasinLevel = 2.0 } = {}) {
|
|||
|
|
const ps = new PumpingStation(psConfig());
|
|||
|
|
const mgc = new MachineGroup(mgcConfig());
|
|||
|
|
const pumps = ['pump_a', 'pump_b', 'pump_c'].map(id => new Machine(pumpConfig(id), pumpStateConfig()));
|
|||
|
|
|
|||
|
|
// Inject initial pressure on each pump so predictFlow / predictPower /
|
|||
|
|
// predictCtrl have a real fDimension before MGC starts asking. Real
|
|||
|
|
// values are set every tick by the physics step.
|
|||
|
|
for (const m of pumps) injectPumpPressure(m, /* upstreamPa */ 19620, /* downstreamPa */ 117720);
|
|||
|
|
|
|||
|
|
// Wire pumps → MGC.
|
|||
|
|
for (const m of pumps) mgc.childRegistrationUtils.registerChild(m, m.config.functionality.positionVsParent);
|
|||
|
|
// Wire MGC → PS.
|
|||
|
|
ps.childRegistrationUtils.registerChild(mgc, mgc.config.functionality.positionVsParent);
|
|||
|
|
|
|||
|
|
mgc.calcAbsoluteTotals();
|
|||
|
|
mgc.calcDynamicTotals();
|
|||
|
|
|
|||
|
|
// Calibrate basin level to start point.
|
|||
|
|
ps.calibratePredictedLevel(initialBasinLevel);
|
|||
|
|
|
|||
|
|
// Controllable clock — overrides Date.now ONLY for our process.
|
|||
|
|
let now = Date.now();
|
|||
|
|
const realNow = Date.now;
|
|||
|
|
Date.now = () => now;
|
|||
|
|
ps._predictedFlowState.lastTimestamp = now;
|
|||
|
|
|
|||
|
|
function advance(ms) { now += ms; }
|
|||
|
|
function restore() { Date.now = realNow; }
|
|||
|
|
|
|||
|
|
return { ps, mgc, pumps, advance, restore, get now() { return now; } };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Convert mbar to Pa for the rotatingMachine canonical pressure unit.
|
|||
|
|
function mbarToPa(mbar) { return mbar * 100; }
|
|||
|
|
function paToMbar(Pa) { return Pa / 100; }
|
|||
|
|
|
|||
|
|
// Inject upstream + downstream pressure measurements onto a pump as if a
|
|||
|
|
// pressure-sensor child had emitted them. updateMeasuredPressure is the
|
|||
|
|
// same path the rotatingMachine listens on for sensor children, so this
|
|||
|
|
// fires the pump's "pressure.measured.<position>" emitter — which the MGC
|
|||
|
|
// is also subscribed to, so totals recompute identically.
|
|||
|
|
function injectPumpPressure(pump, upstreamPa, downstreamPa, ts = Date.now()) {
|
|||
|
|
pump.updateMeasuredPressure(paToMbar(upstreamPa), 'upstream',
|
|||
|
|
{ timestamp: ts, unit: 'mbar', childName: 'PT-up', childId: `up-${pump.config.general.id}` });
|
|||
|
|
pump.updateMeasuredPressure(paToMbar(downstreamPa), 'downstream',
|
|||
|
|
{ timestamp: ts, unit: 'mbar', childName: 'PT-dn', childId: `dn-${pump.config.general.id}` });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
buildPlant,
|
|||
|
|
injectPumpPressure,
|
|||
|
|
mbarToPa, paToMbar,
|
|||
|
|
};
|