feat(pumpingStation): realistic defaults, ramp-foot visual fix, manual-mode visibility, dashboard example
Editor + schema defaults - pumpingStation.html: drag-in defaults now reflect a realistic basin (volume=50 m³, height=4 m, inflowLevel=1.5, outflowLevel=0.2, overflowLevel=3.8, startLevel=1, stopLevel=0.5, minLevel=0.3, maxLevel=3.8). Old defaults left every level field null. Visual bug fix - src/editor/mode-preview.js: the level-based ramp curve in the editor was being drawn with foot=startLevel via buildPath(start, start, max). The runtime in control/levelBased.js has always used inflowLevel as the ramp foot. Pass buildPath(start, upFoot, max) where upFoot falls back to start when inflowLevel is missing, matching the runtime. Manual mode observability - src/specificClass.js: store last forwarded demand on this._manualDemand; surface as `mode` and `manualDemand` in getOutput(); call notifyOutputChanged() on forwardDemandToChildren and on changeMode so Port 0/1 emit even with no children registered. Status badge compacted to `mode | dir% | net m³/h` + `Qd=X m³/h` in manual mode. Examples cleanup - Drop stale 02-Integration.json, 03-Dashboard.json, basic-dashboard.flow.json, standalone-demo.js. - 01-Basic.json: numbered driver groups (1. Control mode … 4. Calibration), Debug-outputs group, fixed typos and HOW-TO-USE; Port 1 debug now active. - New 02-Dashboard.json: FlowFuse Dashboard 2.0 with Controls (7 buttons), Status (7 ui-text rows), Trends (4 ui-charts: level / volume / volume% / flow in-out-net), Raw output (ui-template dumping every Port 0 field). Fan-out function pattern-matches the 4-segment measurement keys by prefix instead of hardcoding childId, converts flow m³/s → m³/h, and caches last-known values so deltas never blank a row. - examples/README.md realigned to the two-file set. Wiki - Home.md: 5 image placeholders replaced with the provided screenshots (01-node-and-editor, 02-basic-flow, 03-wiring-standalone, 04-wiring-integrated) and the demo GIF (01-basic-demo). - Reference-Examples.md: shipped-files table reduced to 01-Basic + 02-Dashboard, Example-01 section uses the screenshot + GIF, Example-02 rewritten as Dashboard (kept screenshot/GIF callouts open for those captures), Example-03/Integration sections + their debug-recipes row removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,12 @@ class PumpingStation extends BaseDomain {
|
||||
this.controlState = { percControl: 0 };
|
||||
this.state = { direction: 'steady', netFlow: 0, flowSource: null, seconds: null, remainingSource: null };
|
||||
|
||||
// Last operator demand from set.demand in manual mode. Stored on the
|
||||
// host so getOutput()/status reflect it even when no children are
|
||||
// registered yet (otherwise forwardDemand is invisible on Port 0/1).
|
||||
// Cleared on mode change away from manual.
|
||||
this._manualDemand = null;
|
||||
|
||||
// Level-armed hysteresis state — ported from basin-docs `_controlLevelBased`.
|
||||
// Exposed as instance fields because the e2e/basic tests assert on them
|
||||
// directly. levelBased strategy reads/writes via the same names.
|
||||
@@ -172,6 +178,8 @@ class PumpingStation extends BaseDomain {
|
||||
if (this.config.control.allowedModes?.has?.(newMode)) {
|
||||
this.logger.info(`Control mode changing from ${this.mode} to ${newMode}`);
|
||||
this.mode = newMode;
|
||||
if (newMode !== 'manual') this._manualDemand = null;
|
||||
this.notifyOutputChanged();
|
||||
} else {
|
||||
this.logger.warn(`Attempted to change to unsupported control mode: ${newMode}`);
|
||||
}
|
||||
@@ -183,7 +191,11 @@ class PumpingStation extends BaseDomain {
|
||||
setManualInflow(value, ts = Date.now(), unit) { calibration.setManualInflow(this, value, ts, unit); }
|
||||
setManualOutflow(value, ts = Date.now(), unit) { calibration.setManualOutflow(this, value, ts, unit); }
|
||||
|
||||
forwardDemandToChildren(demand) { return control.manual.forwardDemand(this.context(), demand); }
|
||||
forwardDemandToChildren(demand) {
|
||||
this._manualDemand = Number.isFinite(demand) ? demand : null;
|
||||
this.notifyOutputChanged();
|
||||
return control.manual.forwardDemand(this.context(), demand);
|
||||
}
|
||||
|
||||
// Direct delegations preserved so existing tests can drive the strategy
|
||||
// without re-mocking the dispatch layer.
|
||||
@@ -220,6 +232,8 @@ class PumpingStation extends BaseDomain {
|
||||
out.flowSource = this.state.flowSource;
|
||||
out.timeleft = this.state.seconds;
|
||||
out.percControl = this.controlState.percControl;
|
||||
out.mode = this.mode;
|
||||
out.manualDemand = this._manualDemand;
|
||||
|
||||
// Derived safety thresholds — exposed so editor + dashboards can show
|
||||
// the dryRunLevel and highVolumeSafetyLevel without recomputing.
|
||||
@@ -247,15 +261,14 @@ class PumpingStation extends BaseDomain {
|
||||
steady: { arrow: '⏸️', fill: 'green' },
|
||||
};
|
||||
const { arrow = '❔', fill = 'grey' } = STYLES[this.state?.direction] || {};
|
||||
const vol = this.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3') ?? 0;
|
||||
const pct = this.measurements.type('volumePercent').variant('predicted').position('atequipment').getCurrentValue() ?? 0;
|
||||
const maxVol = this.basin?.maxVolAtOverflow ?? 0;
|
||||
const netFlowM3h = (this.state?.netFlow ?? 0) * 3600;
|
||||
const seconds = this.state?.seconds;
|
||||
const tStr = seconds != null ? `t≈${Math.round(seconds / 60)} min` : null;
|
||||
const mode = this.mode || '?';
|
||||
const manualPart = this.mode === 'manual' && Number.isFinite(this._manualDemand)
|
||||
? `Qd=${this._manualDemand.toFixed(0)} m³/h` : null;
|
||||
|
||||
return statusBadge.compose(
|
||||
[`${arrow} ${pct.toFixed(1)}%`, `V=${vol.toFixed(2)} / ${maxVol.toFixed(2)} m³`, `net: ${netFlowM3h.toFixed(0)} m³/h`, tStr],
|
||||
[mode, `${arrow} ${pct.toFixed(1)}%`, `net: ${netFlowM3h.toFixed(0)} m³/h`, manualPart],
|
||||
{ fill, shape: 'dot' }
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user