Splits pumpingStation/src/ into focused concern modules. specificClass.js
will be slimmed to an orchestrator in P2.9 (integration); for now both
the inlined logic AND the new modules coexist so tests stay green
throughout.
src/basin/ BasinGeometry + thresholdValidator (pure)
src/measurement/ flowAggregator + measurementRouter + calibration
src/control/ levelBased + flowBased(stub) + manual + index dispatcher
src/safety/ safetyController split into dryRun + overfill rules
src/commands/ registry array + handlers (canonical names from start)
src/editor.js 260 lines of SVG basin-diagram redraw, was inline in .html
examples/standalone-demo.js was if(require.main===module) at bottom of specificClass.js
CONTRACT.md canonical inputs + outputs + emitted events
Modified:
src/specificClass.js removed the 170-line standalone demo block
pumpingStation.html oneditprepare/oneditsave delegate to editor.{init,save}
pumpingStation.js added admin endpoint serving src/editor.js
102 basic tests pass (60 new + 42 existing).
specificClass.js itself is unchanged in behaviour — integration is P2.9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 lines
3.1 KiB
JavaScript
83 lines
3.1 KiB
JavaScript
// MeasurementRouter — dispatches incoming measurement updates by type and
|
|
// derives downstream measurements (volume from level, predicted level from
|
|
// pressure). Pure domain over a context bag; no Node-RED dependency.
|
|
|
|
const { coolprop, interpolation } = require('generalFunctions');
|
|
|
|
const G = 9.80665;
|
|
const ASSUMED_TEMPERATURE_C = 15;
|
|
const ATMOSPHERIC_PRESSURE_PA = 101325;
|
|
|
|
class MeasurementRouter {
|
|
constructor(ctx = {}) {
|
|
if (!ctx.measurements) throw new Error('MeasurementRouter: ctx.measurements is required');
|
|
if (!ctx.basin) throw new Error('MeasurementRouter: ctx.basin is required');
|
|
|
|
this.measurements = ctx.measurements;
|
|
this.basin = ctx.basin;
|
|
this.logger = ctx.logger || null;
|
|
this._interp = ctx.interpolation || new interpolation();
|
|
}
|
|
|
|
route(measurementType, value, position, eventData = {}) {
|
|
switch (measurementType) {
|
|
case 'level':
|
|
this.onLevelMeasurement(position, value, eventData);
|
|
return true;
|
|
case 'pressure':
|
|
this.onPressureMeasurement(position, value, eventData);
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
onLevelMeasurement(position, value, context = {}) {
|
|
this.measurements.type('level').variant('measured').position(position)
|
|
.value(value).unit(context.unit);
|
|
|
|
const series = this.measurements.type('level').variant('measured').position(position);
|
|
const levelMeters = series.getCurrentValue('m');
|
|
if (levelMeters == null) return;
|
|
|
|
const surfaceArea = this.basin.surfaceArea;
|
|
const volume = surfaceArea > 0 ? Math.max(levelMeters, 0) * surfaceArea : 0;
|
|
const percent = this._interp.interpolate_lin_single_point(
|
|
volume, this.basin.minVol, this.basin.maxVolAtOverflow, 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 kelvin = this.measurements
|
|
.type('temperature').variant('measured').position('atequipment')
|
|
.getCurrentValue('K') ?? null;
|
|
|
|
if (kelvin === null) {
|
|
if (this.logger) {
|
|
this.logger.warn('No temperature measurement; assuming 15C for pressure to level conversion.');
|
|
}
|
|
this.measurements.type('temperature').variant('assumed').position('atequipment')
|
|
.value(ASSUMED_TEMPERATURE_C, Date.now(), 'C');
|
|
kelvin = this.measurements.type('temperature').variant('assumed').position('atequipment')
|
|
.getCurrentValue('K');
|
|
}
|
|
if (kelvin == null) return;
|
|
|
|
const density = coolprop.PropsSI('D', 'T', kelvin, 'P', ATMOSPHERIC_PRESSURE_PA, 'Water');
|
|
const pressurePa = this.measurements.type('pressure').variant('measured').position(position)
|
|
.getCurrentValue('Pa');
|
|
if (!Number.isFinite(pressurePa) || !Number.isFinite(density)) return;
|
|
|
|
const level = pressurePa / (density * G);
|
|
this.measurements.type('level').variant('predicted').position(position)
|
|
.value(level, context.timestamp, 'm');
|
|
}
|
|
}
|
|
|
|
module.exports = MeasurementRouter;
|