// Trace recorder — hooks into every emitter and timer-driven path on a // wired plant and records ALL events into a flat list with timestamps. // // Captures: // - Per-pump state transitions (state.emitter on 'state-change' or via // polling getCurrentState() before/after each tick). // - Per-pump pressure events (measurements.emitter on // 'pressure.measured.{upstream,downstream,differential}'). // - Per-pump flow / power / ctrl events (predicted variants). // - MGC dynamic totals (after each calcDynamicTotals). // - PS percControl + level + volume + safetyState (after each tick). // - MGC bestCombination (instrument by wrapping optimalControl). // - Pump operating points: individual predictFlow.currentF and // groupPredictFlow.currentF (per tick, post-equalization). const POSITIONS = ['upstream', 'downstream', 'differential']; function attachRecorder({ ps, mgc, pumps }) { const events = []; const push = (kind, data) => events.push({ t: Date.now(), kind, ...data }); // --- pump-level: pressure events --- for (const pump of pumps) { const id = pump.config.general.id; for (const pos of POSITIONS) { const ev = `pressure.measured.${pos}`; pump.measurements.emitter.on(ev, (e) => push('pump.pressure', { pump: id, pos, value: e?.value, unit: e?.unit, })); } // flow / power predicted (rotatingMachine emits these on state changes // and movement updates). pump.measurements.emitter.on('flow.predicted.downstream', (e) => push('pump.flow.predicted', { pump: id, value: e?.value, unit: e?.unit, })); pump.measurements.emitter.on('power.predicted.atequipment', (e) => push('pump.power.predicted', { pump: id, value: e?.value, unit: e?.unit, })); pump.measurements.emitter.on('ctrl.predicted.atequipment', (e) => push('pump.ctrl.predicted', { pump: id, value: e?.value, unit: e?.unit, })); } // --- MGC bestCombination: wrap optimalControl --- const origOptimal = mgc.optimalControl.bind(mgc); mgc.optimalControl = async function (Qd, powerCap = Infinity) { push('mgc.optimalControl.in', { Qd, powerCap }); const before = snapshotMachineState(pumps); const result = await origOptimal(Qd, powerCap); const after = snapshotMachineState(pumps); push('mgc.optimalControl.out', { Qd, headerDiffPa: pumps[0]?.groupPredictFlow?.currentF, indivDiffPaPerPump: Object.fromEntries(pumps.map(p => [p.config.general.id, p.predictFlow?.currentF])), groupDiffPaPerPump: Object.fromEntries(pumps.map(p => [p.config.general.id, p.groupPredictFlow?.currentF])), // capture state before/after to spot transitions caused by this optimal stateBefore: before, stateAfter: after, }); return result; }; return { events, push }; } function snapshotMachineState(pumps) { return Object.fromEntries(pumps.map(p => [ p.config.general.id, p.state?.getCurrentState?.() ?? '?' ])); } function snapshotFull(ps, mgc, pumps) { const level = ps.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m'); const volume = ps.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3'); return { psLevel: round3(level), psVolume: round3(volume), psPercControl: round3(ps.percControl), psSafety: ps.safetyControllerActive, psDirection: ps.state?.direction, psNetFlow_m3h: round3((ps.state?.netFlow ?? 0) * 3600), pumps: Object.fromEntries(pumps.map(p => { const id = p.config.general.id; const flowPred = p.measurements.type('flow').variant('predicted').position('downstream').getCurrentValue('m3/h'); const powerPred = p.measurements.type('power').variant('predicted').position('atEquipment').getCurrentValue('kW'); const ctrlPred = p.measurements.type('ctrl').variant('predicted').position('atEquipment').getCurrentValue(); const upPred = p.measurements.type('pressure').variant('measured').position('upstream').getCurrentValue('mbar'); const dnPred = p.measurements.type('pressure').variant('measured').position('downstream').getCurrentValue('mbar'); return [id, { state: p.state?.getCurrentState?.(), ctrl_pct: round3(ctrlPred), flow_m3h: round3(flowPred), power_kW: round3(powerPred), pUp_mbar: round3(upPred), pDn_mbar: round3(dnPred), indivDiff_mbar: round3((p.predictFlow?.currentF ?? 0) / 100), groupDiff_mbar: round3((p.groupPredictFlow?.currentF ?? 0) / 100), NCog: round3(p.NCog), groupNCog: round3(p.groupNCog), }]; })), mgc: { scaling: mgc.scaling, mode: mgc.mode, dynamicMin_m3h: round3((mgc.dynamicTotals?.flow?.min ?? 0) * 3600), dynamicMax_m3h: round3((mgc.dynamicTotals?.flow?.max ?? 0) * 3600), }, }; } function round3(v) { if (typeof v !== 'number' || !Number.isFinite(v)) return v; return Math.round(v * 1000) / 1000; } module.exports = { attachRecorder, snapshotFull, snapshotMachineState };