From 69f68adffea1fb93d35a75d3f88adf0cb68ed6e6 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:55:48 +0100 Subject: [PATCH] testing codex --- src/specificClass.js | 37 ++- src/specificClass2.js | 681 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 702 insertions(+), 16 deletions(-) create mode 100644 src/specificClass2.js diff --git a/src/specificClass.js b/src/specificClass.js index 85a6833..b5671c4 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -85,7 +85,6 @@ class pumpingStation { }); break; - case("upstream"): //check for predicted outgoing flow at the connected child pumpingsation child.measurements.emitter.on("flow.predicted.downstream", (eventData) => { @@ -152,8 +151,8 @@ class pumpingStation { const prevFlow = series.getLaggedValue(1, "m3/s"); // { value, timestamp, unit } if (!currFLow || !prevFlow) return; - - this.logger.debug(`currDownflow = ${currFLow.value} , prevDownFlow = ${prevFlow.value}`); + + this.logger.debug(`Flowdir ${flowDir} => currFlow ${currFLow.value} , prevflow = ${prevFlow.value}`); // calc difference in time const deltaT = currFLow.timestamp - prevFlow.timestamp; @@ -222,7 +221,7 @@ class pumpingStation { //this._calcNetFlow(); const {time:timeleft, source:variant} = this._calcTimeRemaining(); - this.logger.debug(`Remaining time ${Math.round(timeleft/60/60*100)/100} h, based on variant ${variant} `); + this.logger.debug(`Remaining time ~${Math.round(timeleft/60/60*10)/10} h, based on variant ${variant} `); } _calcTimeRemaining(){ @@ -624,7 +623,7 @@ const PumpingStation = require("./specificClass"); const RotatingMachine = require("../../rotatingMachine/src/specificClass"); const Measurement = require("../../measurement/src/specificClass"); -/** Helpers ******************************************************************/ +//Helpers function createPumpingStationConfig(name) { return { general: { @@ -754,10 +753,11 @@ function pushSample(measurement, type, value, unit) { .value(value, Date.now(), unit); } -/** Demo *********************************************************************/ +// Demo (async function demoStationWithPump() { const station = new PumpingStation(createPumpingStationConfig("PumpingStationDemo")); - const pump = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig()); + const pump1 = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig()); + const pump2 = new RotatingMachine(createMachineConfig("Pump2"), createMachineStateConfig()); const levelSensor = new Measurement(createLevelMeasurementConfig("WetWellLevel")); const upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", "upstream")); @@ -765,15 +765,16 @@ function pushSample(measurement, type, value, unit) { // station uses the sensors - /* + station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType); station.childRegistrationUtils.registerChild(upstreamFlow, upstreamFlow.config.functionality.softwareType); station.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.softwareType); - */ + // pump owns the downstream flow sensor - pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent); - station.childRegistrationUtils.registerChild(pump,"downstream"); + //pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent); + station.childRegistrationUtils.registerChild(pump1,"downstream"); + station.childRegistrationUtils.registerChild(pump2,"upstream"); setInterval(() => station.tick(), 1000); @@ -782,7 +783,7 @@ function pushSample(measurement, type, value, unit) { pushSample(levelSensor, "level", 1.8, "m"); pushSample(upstreamFlow, "flow", 0.35, "m3/s"); pushSample(downstreamFlow, "flow", 0.20, "m3/s"); - */ + //*/ await new Promise(resolve => setTimeout(resolve, 20)); @@ -791,13 +792,17 @@ function pushSample(measurement, type, value, unit) { pushSample(downstreamFlow, "flow", 0.28, "m3/s"); pushSample(upstreamFlow, "flow", 0.40, "m3/s"); pushSample(levelSensor, "level", 1.85, "m"); - */ + //*/ + console.log("Station output:", station.getOutput()); -await pump.handleInput("parent", "execSequence", "startup"); -await pump.handleInput("parent", "execMovement", 50); +await pump1.handleInput("parent", "execSequence", "startup"); +await pump2.handleInput("parent", "execSequence", "startup"); +await pump1.handleInput("parent", "execMovement", 5); +await pump2.handleInput("parent", "execMovement", 5); console.log("Station state:", station.state); console.log("Station output:", station.getOutput()); - console.log("Pump state:", pump.state.getCurrentState()); + console.log("Pump state:", pump1.state.getCurrentState()); + console.log("Pump state:", pump2.state.getCurrentState()); })(); diff --git a/src/specificClass2.js b/src/specificClass2.js new file mode 100644 index 0000000..f771840 --- /dev/null +++ b/src/specificClass2.js @@ -0,0 +1,681 @@ +const EventEmitter = require('events'); +const { + logger, + configUtils, + configManager, + childRegistrationUtils, + MeasurementContainer, + coolprop, + interpolation +} = require('generalFunctions'); + +const FLOW_VARIANTS = ['measured', 'predicted']; +const LEVEL_VARIANTS = ['measured', 'predicted']; +const FLOW_POSITIONS = { + inflow: ['in', 'upstream'], + outflow: ['out', 'downstream'] +}; + +class PumpingStationV2 { + 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 }); + this.measurements.setPreferredUnit('flow', 'm3/s'); + this.measurements.setPreferredUnit('level', 'm'); + this.measurements.setPreferredUnit('volume', 'm3'); + this.childRegistrationUtils = new childRegistrationUtils(this); + this.machines = {}; + this.stations = {}; + + this.basin = {}; + 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('PumpingStationV2 initialized'); + } + + registerChild(child, softwareType) { + this.logger.debug(`Registering child (${softwareType}) "${child.config.general.name}"`); + + if (softwareType === 'measurement') { + this._registerMeasurementChild(child); + return; + } + + if (softwareType === 'machine' || softwareType === 'pumpingStation') { + this._registerPredictedFlowChild(child); + return; + } + + this.logger.warn(`Unsupported child software type: ${softwareType}`); + } + + tick() { + const snapshot = this._takeMeasurementSnapshot(); + + this._updatePredictedVolume(snapshot); + + const netFlow = this._selectBestNetFlow(snapshot); + const remaining = this._computeRemainingTime(snapshot, netFlow); + + this.state = { + direction: netFlow.direction, + netFlow: netFlow.value, + flowSource: netFlow.source, + seconds: remaining.seconds, + remainingSource: remaining.source + }; + + this.logger.debug( + `Remaining time (${remaining.source ?? 'n/a'}): ${ + remaining.seconds != null ? `${Math.round((remaining.seconds / 60 / 60) * 10) / 10} h` : 'n/a' + }` + ); + } + + /* ------------------------------------------------------------------ */ + /* Helpers */ + /* ------------------------------------------------------------------ */ + + _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}: ${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; + const childName = child.config.general.name; + + const listener = (eventName, posKey) => { + child.measurements.emitter.on(eventName, (eventData) => { + this.logger.debug( + `Predicted flow update from ${childName} (${position}) -> ${eventData.value} ${eventData.unit}` + ); + this.measurements + .type('flow') + .variant('predicted') + .position(posKey) + .value(eventData.value, eventData.timestamp, eventData.unit); + }); + }; + + if (position === 'downstream' || position === 'atEquipment' || position === 'out') { + listener('flow.predicted.downstream', 'out'); + } else if (position === 'upstream' || position === 'in') { + listener('flow.predicted.downstream', 'in'); + } else { + this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); + } + } + + _handleMeasurement(measurementType, value, position, context) { + switch (measurementType) { + case 'level': + this._onLevelMeasurement(position, value, context); + break; + case 'pressure': + this._onPressureMeasurement(position, value, context); + break; + case 'flow': + // Additional flow-specific logic could go here if needed + break; + default: + this.logger.debug(`Unhandled measurement type "${measurementType}", storing only.`); + break; + } + } + + _onLevelMeasurement(position, value, context = {}) { + 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('volume') + .variant('percent') + .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'); + } + + _takeMeasurementSnapshot() { + const snapshot = { + flows: {}, + levels: {}, + levelRates: {} + }; + + for (const variant of FLOW_VARIANTS) { + snapshot.flows[variant] = this._snapshotFlowsForVariant(variant); + } + + for (const variant of LEVEL_VARIANTS) { + snapshot.levels[variant] = this._snapshotLevelForVariant(variant); + snapshot.levelRates[variant] = this._estimateLevelRate(snapshot.levels[variant]); + } + + return snapshot; + } + + _snapshotFlowsForVariant(variant) { + const inflowSeries = this._locateSeries('flow', variant, FLOW_POSITIONS.inflow); + const outflowSeries = this._locateSeries('flow', variant, FLOW_POSITIONS.outflow); + + return { + variant, + inflow: this._seriesSamples(inflowSeries), + outflow: this._seriesSamples(outflowSeries) + }; + } + + _snapshotLevelForVariant(variant) { + const levelSeries = this._locateSeries('level', variant, ['atEquipment']); + return { + variant, + samples: this._seriesSamples(levelSeries) + }; + } + + _seriesSamples(seriesInfo) { + if (!seriesInfo) { + return { + exists: false, + series: null, + current: null, + previous: null + }; + } + + try { + const current = seriesInfo.series.getLaggedSample(0); + const previous = seriesInfo.series.getLaggedSample(1); + return { + exists: Boolean(current), + series: seriesInfo.series, + current, + previous + }; + } catch (err) { + this.logger.debug( + `Failed to read samples for ${seriesInfo.type}.${seriesInfo.variant}.${seriesInfo.position}: ${err.message}` + ); + return { + exists: false, + series: seriesInfo.series, + current: null, + previous: null + }; + } + } + + _locateSeries(type, variant, positions) { + for (const position of positions) { + try { + this.measurements.type(type).variant(variant); + const exists = this.measurements.exists({ position, requireValues: true }); + if (!exists) continue; + const series = this.measurements.type(type).variant(variant).position(position); + return { type, variant, position, series }; + } catch (err) { + // ignore missing combinations + } + } + return null; + } + + _estimateLevelRate(levelSnapshot) { + if (!levelSnapshot.samples.exists) return null; + const { current, previous } = levelSnapshot.samples; + if (!current || !previous || previous.timestamp == null) return null; + + const deltaT = (current.timestamp - previous.timestamp) / 1000; + if (!Number.isFinite(deltaT) || deltaT <= 0) return null; + + const deltaLevel = current.value - previous.value; + return deltaLevel / deltaT; + } + + _selectBestNetFlow(snapshot) { + for (const variant of FLOW_VARIANTS) { + const flow = snapshot.flows[variant]; + if (!flow.inflow.exists || !flow.outflow.exists) continue; + + const inflow = flow.inflow.current?.value ?? null; + const outflow = flow.outflow.current?.value ?? null; + if (!Number.isFinite(inflow) || !Number.isFinite(outflow)) continue; + + const net = inflow - outflow; // positive => filling + if (!Number.isFinite(net)) continue; + + return { + value: net, + source: variant, + direction: this._deriveDirection(net) + }; + } + + // fallback using level trend + for (const variant of LEVEL_VARIANTS) { + const levelRate = snapshot.levelRates[variant]; + if (!Number.isFinite(levelRate)) continue; + + const netFlow = levelRate * 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(snapshot, 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) { + this.logger.warn('Invalid basin surface area.'); + return { seconds: null, source: null }; + } + + for (const variant of LEVEL_VARIANTS) { + const levelSnap = snapshot.levels[variant]; + const current = levelSnap.samples.current?.value ?? null; + if (!Number.isFinite(current)) continue; + + const remainingHeight = + netFlow.value > 0 + ? Math.max(heightOverflow - current, 0) + : Math.max(current - heightOutlet, 0); + + const seconds = (remainingHeight * surfaceArea) / Math.abs(netFlow.value); + if (!Number.isFinite(seconds)) continue; + + return { seconds, source: `${netFlow.source}/${variant}` }; + } + + this.logger.warn('No level data available to compute remaining time.'); + return { seconds: null, source: netFlow.source }; + } + + _updatePredictedVolume(snapshot) { + const predicted = snapshot.flows.predicted; + if (!predicted) return; + + const inflowCur = predicted.inflow.current; + const inflowPrev = predicted.inflow.previous ?? inflowCur; + const outflowCur = predicted.outflow.current; + const outflowPrev = predicted.outflow.previous ?? outflowCur; + + const timestampNow = + inflowCur?.timestamp ?? outflowCur?.timestamp ?? inflowPrev?.timestamp ?? outflowPrev?.timestamp; + const timestampPrev = inflowPrev?.timestamp ?? outflowPrev?.timestamp ?? timestampNow; + + if (!Number.isFinite(timestampNow) || !Number.isFinite(timestampPrev)) return; + + const deltaSeconds = (timestampNow - timestampPrev) / 1000; + if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0) return; + + const avgInflow = this._averageSampleValues(inflowCur, inflowPrev); + const avgOutflow = this._averageSampleValues(outflowCur, outflowPrev); + + const netVolumeChange = (avgInflow - avgOutflow) * deltaSeconds; + if (!Number.isFinite(netVolumeChange) || netVolumeChange === 0) return; + + const volumeSeries = this.measurements + .type('volume') + .variant('predicted') + .position('atEquipment'); + + const currentVolume = volumeSeries.getCurrentValue('m3') ?? this.basin.minVol; + const nextVolume = currentVolume + netVolumeChange; + + const writeTimestamp = Number.isFinite(timestampNow) ? timestampNow : Date.now(); + + volumeSeries.value(nextVolume, writeTimestamp, 'm3').unit('m3'); + + const nextLevel = this._calcLevelFromVolume(nextVolume); + this.measurements + .type('level') + .variant('predicted') + .position('atEquipment') + .value(nextLevel, writeTimestamp, 'm') + .unit('m'); + } + + _averageSampleValues(sampleA, sampleB) { + const values = [sampleA?.value, sampleB?.value].filter((v) => Number.isFinite(v)); + if (!values.length) return 0; + return values.reduce((acc, val) => acc + val, 0) / values.length; + } + + _deriveDirection(netFlow) { + if (netFlow > this.flowThreshold) return 'filling'; + if (netFlow < -this.flowThreshold) return 'draining'; + return 'steady'; + } + + /* ------------------------------------------------------------------ */ + /* Basin Calculations */ + /* ------------------------------------------------------------------ */ + + initBasinProperties() { + 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 minVol = heightOutlet * surfaceArea; + const minVolOut = heightInlet * surfaceArea; + + this.basin = { + volEmptyBasin, + heightBasin, + heightInlet, + heightOutlet, + heightOverflow, + surfaceArea, + maxVol, + maxVolOverflow, + minVol, + minVolOut + }; + + this.measurements + .type('volume') + .variant('predicted') + .position('atEquipment') + .value(minVol) + .unit('m3'); + + this.logger.debug( + `Basin initialized | area=${surfaceArea.toFixed(2)} m2, max=${maxVol.toFixed(2)} m3, overflow=${maxVolOverflow.toFixed(2)} 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 = {}; + Object.entries(this.measurements.measurements).forEach(([type, variants]) => { + Object.entries(variants).forEach(([variant, positions]) => { + Object.entries(positions).forEach(([position, measurement]) => { + output[`${type}.${variant}.${position}`] = measurement.getCurrentValue(); + }); + }); + }); + + output.state = this.state; + output.basin = this.basin; + return output; + } +} + +module.exports = PumpingStationV2; + +/* ------------------------------------------------------------------------- */ +/* 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: 3.5, + heightInlet: 0.3, + heightOutlet: 0.2, + heightOverflow: 3.0 + }, + hydraulics: { + refHeight: 'NAP', + basinBottomRef: 0 + } + }; + } + + 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) { + return { + general: { + name, + logging: { enabled: true, logLevel: 'debug' } + }, + functionality: { + positionVsParent: 'downstream' + }, + 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 PumpingStationV2(createPumpingStationConfig('PumpingStationDemo')); + const pump = new RotatingMachine(createMachineConfig('Pump1'), 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(pump, '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)); + + station.tick(); + console.log('Initial state:', station.state); + + await pump.handleInput('parent', 'execSequence', 'startup'); + await pump.handleInput('parent', 'execMovement', 50); + + console.log('Station state:', station.state); + console.log('Station output:', station.getOutput()); + })().catch((err) => { + console.error('Demo failed:', err); + }); +}