const EventEmitter = require('events'); const { logger, configUtils, configManager, childRegistrationUtils, MeasurementContainer, coolprop, interpolation } = require('generalFunctions'); class PumpingStation { constructor(config = {}) { this.emitter = new EventEmitter(); this.configManager = new configManager(); this.defaultConfig = this.configManager.getConfig('pumpingStation'); this.configUtils = new configUtils(this.defaultConfig); this.config = this.configUtils.initConfig(config); this.interpolate = new interpolation(); this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel,this.config.general.name); this.measurements = new MeasurementContainer({ autoConvert: true, preferredUnits: { flow: 'm3/s', netFlowRate: 'm3/s', level: 'm', volume: 'm3' } }); this.childRegistrationUtils = new childRegistrationUtils(this); this.machines = {}; this.stations = {}; this.machineGroups = {}; this.predictedFlowChildren = new Map(); this.flowVariants = ['measured', 'predicted']; this.levelVariants = ['measured', 'predicted']; this.volVariants = ['measured', 'predicted']; this.flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] }; this.mode = this.config.control.mode; this._levelState = { crossed: new Set(), dwellUntil: null }; this.state = { direction: 'steady', netFlow: 0, flowSource: null, seconds: null, remainingSource: null }; const thresholdFromConfig = Number(this.config.general?.flowThreshold); this.flowThreshold = Number.isFinite(thresholdFromConfig) ? thresholdFromConfig : 1e-4; this.initBasinProperties(); this.logger.debug('PumpingStation initialized'); } /* --------------------------- Registration --------------------------- */ registerChild(child, softwareType) { this.logger.debug(`Registering child (${softwareType}) "${child.config.general.name}"`); if (softwareType === 'measurement') { this._registerMeasurementChild(child); return; } if (softwareType === 'machine') { this.machines[child.config.general.id] = child; } else if (softwareType === 'pumpingstation') { this.stations[child.config.general.id] = child; } else if (softwareType === 'machinegroup') { this.machineGroups[child.config.general.id] = child; this._registerPredictedFlowChild(child); } if (softwareType === 'machine' || softwareType === 'pumpingstation' || softwareType === 'machinegroup') { this._registerPredictedFlowChild(child); } } _registerMeasurementChild(child) { const position = child.config.functionality.positionVsParent; const measurementType = child.config.asset.type; const eventName = `${measurementType}.measured.${position}`; child.measurements.emitter.on(eventName, (eventData = {}) => { this.logger.debug( `Measurement update ${eventName} <- ${eventData.childName || child.config.general.name}: ${eventData.value} ${eventData.unit}` ); this.measurements .type(measurementType) .variant('measured') .position(position) .value(eventData.value, eventData.timestamp, eventData.unit); this._handleMeasurement(measurementType, eventData.value, position, eventData); }); } _registerPredictedFlowChild(child) { const position = (child.config.functionality.positionVsParent || '').toLowerCase(); const childName = child.config.general.name; const childId = child.config.general.id ?? childName; let posKey; let eventNames; switch (position) { case 'downstream': case 'out': case 'atequipment': posKey = 'out'; eventNames = ['flow.predicted.downstream', 'flow.predicted.atequipment']; break; case 'upstream': case 'in': posKey = 'in'; eventNames = ['flow.predicted.upstream', 'flow.predicted.atequipment']; break; default: this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); return; } if (!this.predictedFlowChildren.has(childId)) { this.predictedFlowChildren.set(childId, { in: 0, out: 0 }); } const handler = (eventData = {}) => { const unit = eventData.unit || child.config?.general?.unit; const ts = eventData.timestamp || Date.now(); this.logger.debug(`Emitting for child ${unit} `); this.measurements .type('flow') .variant('predicted') .position(posKey) .child(childId) .value(eventData.value, ts, unit); }; eventNames.forEach((ev) => child.measurements.emitter.on(ev, handler)); } /* --------------------------- Calibration --------------------------- */ calibratePredictedVolume(calibratedVol, timestamp = Date.now()) { const volumeChain = this.measurements.type('volume').variant('predicted').position('atequipment'); const levelChain = this.measurements.type('level').variant('predicted').position('atequipment'); const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null; if (volumeMeasurement) { volumeMeasurement.values = []; volumeMeasurement.timestamps = []; } const levelMeasurement = levelChain.exists() ? levelChain.get() : null; if (levelMeasurement) { levelMeasurement.values = []; levelMeasurement.timestamps = []; } volumeChain.value(calibratedVol, timestamp, 'm3').unit('m3'); levelChain.value(this._calcLevelFromVolume(calibratedVol), timestamp, 'm'); this._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: timestamp }; } calibratePredictedLevel(val, timestamp = Date.now(), unit = 'm') { const volumeChain = this.measurements.type('volume').variant('predicted').position('atequipment'); const levelChain = this.measurements.type('level').variant('predicted').position('atequipment'); const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null; if (volumeMeasurement) { volumeMeasurement.values = []; volumeMeasurement.timestamps = []; } const levelMeasurement = levelChain.exists() ? levelChain.get() : null; if (levelMeasurement) { levelMeasurement.values = []; levelMeasurement.timestamps = []; } levelChain.value(val, timestamp).unit(unit); volumeChain.value(this._calcVolumeFromLevel(val), timestamp, 'm3'); this._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: timestamp }; } setManualInflow(value, timestamp = Date.now(), unit) { const num = Number(value); this.measurements.type('flow').variant('predicted').position('in').child('manual-qin').value(num, timestamp, unit); } /* --------------------------- Tick / Control --------------------------- */ tick() { this._updatePredictedVolume(); const netFlow = this._selectBestNetFlow(); const remaining = this._computeRemainingTime(netFlow); this._safetyController(remaining.seconds, netFlow.direction); if (this.safetyControllerActive) return; this._controlLogic(netFlow.direction); this.state = { direction: netFlow.direction, netFlow: netFlow.value, flowSource: netFlow.source, seconds: remaining.seconds, remainingSource: remaining.source }; this.logger.debug(`netflow = ${JSON.stringify(netFlow)}`); this.logger.debug( `Height : ${this.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m')} m` ); } changeMode(newMode){ if ( this.config.control.allowedModes.has(newMode) ){ const currentMode = this.mode; this.logger.info(`Control mode changing from ${currentMode} to ${newMode}`); this.mode = newMode; } else{ this.logger.warn(`Attempted to change to unsupported control mode: ${newMode}`); } } _controlLogic(direction) { switch (this.mode) { case 'levelbased': this._controlLevelBased(direction); break; case 'flowbased': this._controlFlowBased?.(); break; case 'manual': break; default: this.logger.warn(`Unsupported control mode: ${this.mode}`); } } async _controlLevelBased(direction) { const { startLevel, stopLevel } = this.config.control.levelbased; const flowUnit = this.measurements.getUnit('flow'); const levelUnit = this.measurements.getUnit('level'); const level = this._pickVariant('level', this.levelVariants, 'atequipment', levelUnit); if (level == null) { this.logger.warn('No valid level found'); return; } if (level > startLevel && direction === 'filling') { const percControl = this._scaleLevelToFlowPercent(level); this.logger.debug(`Controllevel based => Level ${level} control applying to pump : ${percControl}`); await this._applyMachineLevelControl(percControl); } if (level < stopLevel && direction === 'draining') { Object.values(this.machines).forEach((machine) => { const pos = machine?.config?.functionality?.positionVsParent; if ((pos === 'downstream' || pos === 'atequipment') && machine._isOperationalState()) { machine.handleInput('parent', 'execSequence', 'shutdown'); } }); Object.values(this.stations).forEach((station) => station.handleInput('parent', 'execSequence', 'shutdown')); Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines()); } } _controlFlowBased() { // placeholder for flow-based logic } async _applyMachineGroupLevelControl(percentControl) { if (!this.machineGroups || Object.keys(this.machineGroups).length === 0) return; await Promise.all( Object.values(this.machineGroups).map((group) => group.handleInput('parent', percentControl).catch((err) => { this.logger.error(`Failed to send level control to group "${group.config.general.name}": ${err.message}`); }) ) ); } async _applyMachineLevelControl(percentControl) { const machines = Object.values(this.machines).filter((machine) => { const pos = machine?.config?.functionality?.positionVsParent; return (pos === 'downstream' || pos === 'atequipment'); }); if (!machines.length) return; const perMachine = percentControl / machines.length; for (const machine of machines) { try { await machine.handleInput('parent', 'execSequence', 'startup'); await machine.handleInput('parent', 'execMovement', perMachine); } catch (err) { this.logger.error(`Failed to start machine "${machine.config.general.name}": ${err.message}`); } } } /* --------------------------- Measurements --------------------------- */ _handleMeasurement(measurementType, value, position, context) { switch (measurementType) { case 'level': this._onLevelMeasurement(position, value, context); break; case 'pressure': this._onPressureMeasurement(position, value, context); break; default: break; } } _onLevelMeasurement(position, value, context = {}) { this.measurements.type('level').variant('measured').position(position).value(value).unit(context.unit); const levelSeries = this.measurements.type('level').variant('measured').position(position); const levelMeters = levelSeries.getCurrentValue('m'); if (levelMeters == null) return; const volume = this._calcVolumeFromLevel(levelMeters); const percent = this.interpolate.interpolate_lin_single_point( volume, this.basin.minVol, this.basin.maxVolOverflow, 0, 100 ); this.measurements.type('volume').variant('measured').position('atequipment').value(volume, context.timestamp, 'm3'); this.measurements .type('volumePercent') .variant('measured') .position('atequipment') .value(percent, context.timestamp, '%'); } _onPressureMeasurement(position, value, context = {}) { let kelvinTemp = this.measurements.type('temperature').variant('measured').position('atequipment').getCurrentValue('K') ?? null; if (kelvinTemp === null) { this.logger.warn('No temperature measurement; assuming 15C for pressure to level conversion.'); this.measurements.type('temperature').variant('assumed').position('atequipment').value(15, Date.now(), 'C'); kelvinTemp = this.measurements.type('temperature').variant('assumed').position('atequipment').getCurrentValue('K'); } if (kelvinTemp == null) return; const density = coolprop.PropsSI('D', 'T', kelvinTemp, 'P', 101325, 'Water'); const pressurePa = this.measurements.type('pressure').variant('measured').position(position).getCurrentValue('Pa'); if (!Number.isFinite(pressurePa) || !Number.isFinite(density)) return; const g = 9.80665; const level = pressurePa / (density * g); this.measurements.type('level').variant('predicted').position(position).value(level, context.timestamp, 'm'); } /* --------------------------- Core Calculations --------------------------- */ _pickVariant(type, variants, position, unit) { for (const variant of variants) { const val = this.measurements.type(type).variant(variant).position(position).getCurrentValue(unit); if (!Number.isFinite(val)) continue; return val; } return null; } _scaleLevelToFlowPercent(level) { const { minFlowLevel, maxFlowLevel } = this.config.control.levelbased; this.logger.debug(`Scaling minflow level : ${minFlowLevel} and maxflowLevel : ${maxFlowLevel}`); return this.interpolate.interpolate_lin_single_point(level, minFlowLevel, maxFlowLevel, 0, 100); } _levelRate(variant) { const chain = this.measurements.type('level').variant(variant).position('atequipment'); if (!chain.exists({ requireValues: true })) return null; const m = chain.get(); const current = m?.getLaggedSample?.(0); const previous = m?.getLaggedSample?.(1); if (!current || !previous || previous.timestamp == null) return null; const dt = (current.timestamp - previous.timestamp) / 1000; if (!Number.isFinite(dt) || dt <= 0) return null; return (current.value - previous.value) / dt; } _updatePredictedVolume() { const flowUnit = 'm3/s'; // this has to be in m3/s for the actions below const now = Date.now(); const inflow = this.measurements.sum('flow', 'predicted', this.flowPositions.inflow, flowUnit) || 0; const outflow = this.measurements.sum('flow', 'predicted', this.flowPositions.outflow, flowUnit) || 0; if (!this._predictedFlowState) { this._predictedFlowState = { inflow, outflow, lastTimestamp: now }; } const timestampPrev = this._predictedFlowState.lastTimestamp ?? now; const deltaSeconds = Math.max((now - timestampPrev) / 1000, 0); const netVolumeChange = deltaSeconds > 0 ? (inflow - outflow) * deltaSeconds : 0; const volumeSeries = this.measurements.type('volume').variant('predicted').position('atequipment'); const currentVolume = volumeSeries.getCurrentValue('m3'); const nextVolume = currentVolume + netVolumeChange; const writeTimestamp = timestampPrev + deltaSeconds * 1000; volumeSeries.value(nextVolume, writeTimestamp, 'm3').unit('m3'); //olifant const nextLevel = this._calcLevelFromVolume(nextVolume); this.measurements .type('level') .variant('predicted') .position('atequipment') .value(nextLevel, writeTimestamp, 'm') .unit('m'); const percent = this.interpolate.interpolate_lin_single_point( nextVolume, this.basin.minVol, this.basin.maxVolOverflow, 0, 100 ); this.measurements .type('volumePercent') .variant('predicted') .position('atequipment') .value(percent, writeTimestamp, '%'); this._predictedFlowState = { inflow, outflow, lastTimestamp: writeTimestamp }; } _selectBestNetFlow() { const type = 'flow'; const unit = this.measurements.getUnit(type) || 'm3/s'; for (const variant of this.flowVariants) { const bucket = this.measurements.measurements?.[type]?.[variant]; if (!bucket || Object.keys(bucket).length === 0) continue; const inflow = this.measurements.sum(type, variant, this.flowPositions.inflow, unit) || 0; const outflow = this.measurements.sum(type, variant, this.flowPositions.outflow, unit) || 0; if (Math.abs(inflow) < this.flowThreshold && Math.abs(outflow) < this.flowThreshold) continue; const net = inflow - outflow; this.measurements.type('netFlowRate').variant(variant).position('atequipment').value(net, Date.now(), unit); return { value: net, source: variant, direction: this._deriveDirection(net) }; } // Fallback: level trend for (const variant of this.levelVariants) { const rate = this._levelRate(variant); if (!Number.isFinite(rate)) continue; const netFlow = rate * this.basin.surfaceArea; return { value: netFlow, source: `level:${variant}`, direction: this._deriveDirection(netFlow) }; } this.logger.warn('No usable measurements to compute net flow; assuming steady.'); return { value: 0, source: null, direction: 'steady' }; } _computeRemainingTime(netFlow) { if (!netFlow || Math.abs(netFlow.value) < this.flowThreshold) return { seconds: null, source: null }; const { heightOverflow, heightOutlet, surfaceArea } = this.basin; if (!Number.isFinite(surfaceArea) || surfaceArea <= 0) return { seconds: null, source: null }; for (const variant of this.levelVariants) { const lvl = this.measurements.type('level').variant(variant).position('atequipment').getCurrentValue('m'); if (!Number.isFinite(lvl)) continue; const remainingHeight = netFlow.value > 0 ? Math.max(heightOverflow - lvl, 0) : Math.max(lvl - heightOutlet, 0); const seconds = (remainingHeight * surfaceArea) / Math.abs(netFlow.value); if (!Number.isFinite(seconds)) continue; return { seconds, source: `${netFlow.source}/${variant}` }; } return { seconds: null, source: netFlow.source }; } _deriveDirection(netFlow) { if (netFlow > this.flowThreshold) return 'filling'; if (netFlow < -this.flowThreshold) return 'draining'; return 'steady'; } /* --------------------------- Safety --------------------------- */ _safetyController(remainingTime, direction) { this.safetyControllerActive = false; const volUnit = this.measurements.getUnit('volume'); const vol = this._pickVariant('volume', this.volVariants, 'atequipment', volUnit); if (vol == null) { Object.values(this.machines).forEach((machine) => machine.handleInput('parent', 'execSequence', 'shutdown')); this.logger.warn('No volume data available to safe guard system; shutting down all machines.'); this.safetyControllerActive = true; return; } const { enableDryRunProtection, dryRunThresholdPercent, enableOverfillProtection, overfillThresholdPercent, timeleftToFullOrEmptyThresholdSeconds } = this.config.safety || {}; const dryRunEnabled = Boolean(enableDryRunProtection); const overfillEnabled = Boolean(enableOverfillProtection); const timeProtectionEnabled = timeleftToFullOrEmptyThresholdSeconds > 0; const triggerHighVol = this.basin.maxVolOverflow * ((Number(overfillThresholdPercent) || 0) / 100); const triggerLowVol = this.basin.minVol * (1 + ((Number(dryRunThresholdPercent) || 0) / 100)); if (direction === 'draining') { const timeTriggered = timeProtectionEnabled && remainingTime != null && remainingTime < timeleftToFullOrEmptyThresholdSeconds; const dryRunTriggered = dryRunEnabled && vol < triggerLowVol; if (timeTriggered || dryRunTriggered) { Object.values(this.machines).forEach((machine) => { const pos = machine?.config?.functionality?.positionVsParent; if ((pos === 'downstream' || pos === 'atequipment') && machine._isOperationalState()) { machine.handleInput('parent', 'execSequence', 'shutdown'); } }); Object.values(this.stations).forEach((station) => station.handleInput('parent', 'execSequence', 'shutdown')); Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines()); this.logger.warn( `Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down downstream equipment` ); this.safetyControllerActive = true; } } if (direction === 'filling') { const timeTriggered = timeProtectionEnabled && remainingTime != null && remainingTime < timeleftToFullOrEmptyThresholdSeconds; const overfillTriggered = overfillEnabled && vol > triggerHighVol; if (timeTriggered || overfillTriggered) { Object.values(this.machines).forEach((machine) => { const pos = machine?.config?.functionality?.positionVsParent; if (pos === 'upstream' && machine._isOperationalState()) { machine.handleInput('parent', 'execSequence', 'shutdown'); } }); Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines()); Object.values(this.stations).forEach((station) => station.handleInput('parent', 'execSequence', 'shutdown')); this.logger.warn( `Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down upstream equipment` ); this.safetyControllerActive = true; } } } /* --------------------------- Basin --------------------------- */ initBasinProperties() { const minHeightBasedOn = this.config.hydraulics.minHeightBasedOn; const volEmptyBasin = this.config.basin.volume; const heightBasin = this.config.basin.height; const heightInlet = this.config.basin.heightInlet; const heightOutlet = this.config.basin.heightOutlet; const heightOverflow = this.config.basin.heightOverflow; const surfaceArea = volEmptyBasin / heightBasin; const maxVol = heightBasin * surfaceArea; const maxVolOverflow = heightOverflow * surfaceArea; const minVolOut = heightOutlet * surfaceArea; const minVolIn = heightInlet * surfaceArea; const minVol = minHeightBasedOn === 'inlet' ? minVolIn : minVolOut; this.basin = { volEmptyBasin, heightBasin, heightInlet, heightOutlet, heightOverflow, surfaceArea, maxVol, maxVolOverflow, minVolIn, minVolOut, minVol, minHeightBasedOn }; this.measurements.type('volume').variant('predicted').position('atequipment').value(minVol).unit('m3'); } _calcVolumeFromLevel(level) { return Math.max(level, 0) * this.basin.surfaceArea; } _calcLevelFromVolume(volume) { return Math.max(volume, 0) / this.basin.surfaceArea; } /* --------------------------- Output --------------------------- */ getOutput() { const output = this.measurements.getFlattenedOutput(); output.direction = this.state.direction; output.flowSource = this.state.flowSource; output.timeleft = this.state.seconds; output.volEmptyBasin = this.basin.volEmptyBasin; output.heightInlet = this.basin.heightInlet; output.heightOverflow = this.basin.heightOverflow; output.maxVol = this.basin.maxVol; output.minVol = this.basin.minVol; output.maxVolOverflow = this.basin.maxVolOverflow; output.minVolOut = this.basin.minVolOut; output.minVolIn = this.basin.minVolIn; output.minHeightBasedOn = this.basin.minHeightBasedOn; return output; } } module.exports = PumpingStation; /* ------------------------------------------------------------------------- */ /* Example usage */ /* ------------------------------------------------------------------------- */ /* if (require.main === module) { const Measurement = require('../../measurement/src/specificClass'); const RotatingMachine = require('../../rotatingMachine/src/specificClass'); function createPumpingStationConfig(name) { return { general: { logging: { enabled: true, logLevel: 'debug' }, name, id: `${name}-${Date.now()}`, flowThreshold: 1e-4 }, functionality: { softwareType: 'pumpingStation', role: 'stationcontroller' }, basin: { volume: 43.75, height: 10, heightInlet: 3, heightOutlet: 0.2, heightOverflow: 3.2 }, hydraulics: { refHeight: 'NAP', basinBottomRef: 0 }, safety: { enableDryRunProtection:false, enableOverfillProtection:false } }; } function createLevelMeasurementConfig(name) { return { general: { logging: { enabled: true, logLevel: 'debug' }, name, id: `${name}-${Date.now()}`, unit: 'm' }, functionality: { softwareType: 'measurement', role: 'sensor', positionVsParent: 'atequipment' }, asset: { category: 'sensor', type: 'level', model: 'demo-level', supplier: 'demoCo', unit: 'm' }, scaling: { enabled: false }, smoothing: { smoothWindow: 5, smoothMethod: 'none' } }; } function createFlowMeasurementConfig(name, position) { return { general: { logging: { enabled: true, logLevel: 'debug' }, name, id: `${name}-${Date.now()}`, unit: 'm3/s' }, functionality: { softwareType: 'measurement', role: 'sensor', positionVsParent: position }, asset: { category: 'sensor', type: 'flow', model: 'demo-flow', supplier: 'demoCo', unit: 'm3/s' }, scaling: { enabled: false }, smoothing: { smoothWindow: 5, smoothMethod: 'none' } }; } function createMachineConfig(name,position) { return { general: { name, logging: { enabled: false, logLevel: 'debug' } }, functionality: { softwareType: "machine", positionVsParent: position }, asset: { supplier: 'Hydrostal', type: 'pump', category: 'centrifugal', model: 'hidrostal-H05K-S03R' } }; } function createMachineStateConfig() { return { general: { logging: { enabled: true, logLevel: 'debug' } }, movement: { speed: 1 }, time: { starting: 2, warmingup: 3, stopping: 2, coolingdown: 3 } }; } function seedSample(measurement, type, value, unit) { const pos = measurement.config.functionality.positionVsParent; measurement.measurements.type(type).variant('measured').position(pos).value(value, Date.now(), unit); } (async function demo() { const station = new PumpingStation(createPumpingStationConfig('PumpingStationDemo')); const pump1 = new RotatingMachine(createMachineConfig('Pump1','downstream'), createMachineStateConfig()); //const pump2 = new RotatingMachine(createMachineConfig('Pump2','upstream'), createMachineStateConfig()); //const levelSensor = new Measurement(createLevelMeasurementConfig('WetWellLevel')); //const inflowSensor = new Measurement(createFlowMeasurementConfig('InfluentFlow', 'in')); //const outflowSensor = new Measurement(createFlowMeasurementConfig('PumpDischargeFlow', 'out')); //station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType); //station.childRegistrationUtils.registerChild(inflowSensor, inflowSensor.config.functionality.softwareType); //station.childRegistrationUtils.registerChild(outflowSensor, outflowSensor.config.functionality.softwareType); station.childRegistrationUtils.registerChild(pump1, 'machine'); //station.childRegistrationUtils.registerChild(pump2, 'machine'); // Seed initial measurements //seedSample(levelSensor, 'level', 1.8, 'm'); //seedSample(inflowSensor, 'flow', 0.35, 'm3/s'); //seedSample(outflowSensor, 'flow', 0.20, 'm3/s'); setInterval( () => station.tick(), 1000); await new Promise((resolve) => setTimeout(resolve, 10)); console.log('Initial state:', station.state); station.setManualInflow(300,Date.now(),'l/s'); //await pump1.handleInput('parent', 'execSequence', 'startup'); //await pump1.handleInput('parent', 'execMovement', 10); // //await pump2.handleInput('parent', 'execSequence', 'startup'); //await pump2.handleInput('parent', 'execMovement', 10); console.log('Station state:', station.state); console.log('Station output:', station.getOutput()); })().catch((err) => { console.error('Demo failed:', err); }); } //*/