Compare commits
1 Commits
6ab585bcc2
...
e2ebb31816
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2ebb31816 |
@@ -86,6 +86,7 @@
|
|||||||
shiftLevel: { value: 0 },
|
shiftLevel: { value: 0 },
|
||||||
shiftArmPercent: { value: 95 },
|
shiftArmPercent: { value: 95 },
|
||||||
startLevel: { value: null },
|
startLevel: { value: null },
|
||||||
|
stopLevel: { value: null },
|
||||||
minLevel: { value: null },
|
minLevel: { value: null },
|
||||||
maxLevel: { value: null },
|
maxLevel: { value: null },
|
||||||
flowSetpoint: { value: null },
|
flowSetpoint: { value: null },
|
||||||
@@ -413,6 +414,11 @@
|
|||||||
<input type="number" id="node-input-startLevel" min="0" step="0.01" />
|
<input type="number" id="node-input-startLevel" min="0" step="0.01" />
|
||||||
<span class="ps-unit">m</span>
|
<span class="ps-unit">m</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ps-row" data-stroke="#7D3C98" data-couples-line="ps-mode-line-stopLevel">
|
||||||
|
<div><label>stopLevel</label><div class="ps-sub">pump-off threshold (optional, ≤ startLevel)</div></div>
|
||||||
|
<input type="number" id="node-input-stopLevel" min="0" step="0.01" />
|
||||||
|
<span class="ps-unit">m</span>
|
||||||
|
</div>
|
||||||
<div class="ps-row ps-readonly" data-stroke="#1F4E79" data-couples-line="ps-mode-line-inflowLevel">
|
<div class="ps-row ps-readonly" data-stroke="#1F4E79" data-couples-line="ps-mode-line-inflowLevel">
|
||||||
<div><label>inflowLevel</label><div class="ps-sub">from basin above</div></div>
|
<div><label>inflowLevel</label><div class="ps-sub">from basin above</div></div>
|
||||||
<span id="ps-mode-readout-inflow" class="ps-readonly-val">— m</span>
|
<span id="ps-mode-readout-inflow" class="ps-readonly-val">— m</span>
|
||||||
@@ -469,6 +475,7 @@
|
|||||||
<!-- Vertical level-marker lines — span y=24..140 (top to baseline only, NOT into OFF tier). x set dynamically. -->
|
<!-- Vertical level-marker lines — span y=24..140 (top to baseline only, NOT into OFF tier). x set dynamically. -->
|
||||||
<line id="ps-mode-line-dryRunLevel" y1="24" y2="140" stroke="#C0392B" stroke-dasharray="2 2" />
|
<line id="ps-mode-line-dryRunLevel" y1="24" y2="140" stroke="#C0392B" stroke-dasharray="2 2" />
|
||||||
<line id="ps-mode-line-startLevel" y1="24" y2="140" stroke="#1E8449" stroke-dasharray="2 2" />
|
<line id="ps-mode-line-startLevel" y1="24" y2="140" stroke="#1E8449" stroke-dasharray="2 2" />
|
||||||
|
<line id="ps-mode-line-stopLevel" y1="24" y2="140" stroke="#7D3C98" stroke-dasharray="2 2" />
|
||||||
<line id="ps-mode-line-inflowLevel" y1="24" y2="140" stroke="#1F4E79" stroke-dasharray="2 2" />
|
<line id="ps-mode-line-inflowLevel" y1="24" y2="140" stroke="#1F4E79" stroke-dasharray="2 2" />
|
||||||
<line id="ps-mode-line-maxLevel" y1="24" y2="140" stroke="#D68910" stroke-dasharray="2 2" />
|
<line id="ps-mode-line-maxLevel" y1="24" y2="140" stroke="#D68910" stroke-dasharray="2 2" />
|
||||||
<line id="ps-mode-line-overflowLevel" y1="24" y2="140" stroke="#C0392B" stroke-dasharray="2 2" />
|
<line id="ps-mode-line-overflowLevel" y1="24" y2="140" stroke="#C0392B" stroke-dasharray="2 2" />
|
||||||
@@ -484,6 +491,7 @@
|
|||||||
(cheaper than guarding each one). They're hidden via display:none. -->
|
(cheaper than guarding each one). They're hidden via display:none. -->
|
||||||
<text id="ps-mode-label-dryRunLevel" style="display:none;"></text>
|
<text id="ps-mode-label-dryRunLevel" style="display:none;"></text>
|
||||||
<text id="ps-mode-label-startLevel" style="display:none;"></text>
|
<text id="ps-mode-label-startLevel" style="display:none;"></text>
|
||||||
|
<text id="ps-mode-label-stopLevel" style="display:none;"></text>
|
||||||
<text id="ps-mode-label-inflowLevel" style="display:none;"></text>
|
<text id="ps-mode-label-inflowLevel" style="display:none;"></text>
|
||||||
<text id="ps-mode-label-maxLevel" style="display:none;"></text>
|
<text id="ps-mode-label-maxLevel" style="display:none;"></text>
|
||||||
<text id="ps-mode-label-overflowLevel" style="display:none;"></text>
|
<text id="ps-mode-label-overflowLevel" style="display:none;"></text>
|
||||||
|
|||||||
@@ -66,6 +66,13 @@
|
|||||||
max ?? inlet ?? start ?? EPS,
|
max ?? inlet ?? start ?? EPS,
|
||||||
basinHeight);
|
basinHeight);
|
||||||
|
|
||||||
|
// stopLevel — explicit pump-off threshold. Must sit between
|
||||||
|
// dryRunLevel and startLevel (so it can be reached during draining
|
||||||
|
// before pumps re-engage).
|
||||||
|
setBounds('stopLevel',
|
||||||
|
Number.isFinite(dryRun) ? dryRun + EPS : EPS,
|
||||||
|
start ?? inlet ?? max ?? overflow ?? basinHeight);
|
||||||
|
|
||||||
// Shift inputs (only relevant when shifted ramp enabled).
|
// Shift inputs (only relevant when shifted ramp enabled).
|
||||||
if (shiftEnabled) {
|
if (shiftEnabled) {
|
||||||
setBounds('shiftLevel',
|
setBounds('shiftLevel',
|
||||||
|
|||||||
@@ -25,6 +25,11 @@
|
|||||||
const start = fNum('startLevel');
|
const start = fNum('startLevel');
|
||||||
const inlet = fNum('inflowLevel');
|
const inlet = fNum('inflowLevel');
|
||||||
const max = fNum('maxLevel');
|
const max = fNum('maxLevel');
|
||||||
|
// Optional stopLevel — explicit pump-off threshold. Drawn as its
|
||||||
|
// own marker line; does NOT shift the ramp foot. Must be < startLevel
|
||||||
|
// for the marker to render.
|
||||||
|
const stopRaw = fNum('stopLevel');
|
||||||
|
const stop = Number.isFinite(stopRaw) && stopRaw >= 0 && Number.isFinite(start) && stopRaw < start ? stopRaw : null;
|
||||||
// dryRunLevel is derived from the basin's outflowLevel + dryRun%
|
// dryRunLevel is derived from the basin's outflowLevel + dryRun%
|
||||||
// (no separate input). Below dryRunLevel the runtime hard-stops;
|
// (no separate input). Below dryRunLevel the runtime hard-stops;
|
||||||
// we draw it as the leftmost vertical marker so the user sees
|
// we draw it as the leftmost vertical marker so the user sees
|
||||||
@@ -85,11 +90,14 @@
|
|||||||
return pts.join(' ');
|
return pts.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Up curve: same as before.
|
// Up curve. Foot is startLevel (the configured pump-on threshold and
|
||||||
|
// ramp foot per the runtime in _controlLevelBased). The OFF baseline
|
||||||
|
// is drawn for level < startLevel; at startLevel demand jumps from
|
||||||
|
// OFF to 0 % and ramps up to 100 % at maxLevel.
|
||||||
const up = document.getElementById('ps-mode-curve-up');
|
const up = document.getElementById('ps-mode-curve-up');
|
||||||
const down = document.getElementById('ps-mode-curve-down');
|
const down = document.getElementById('ps-mode-curve-down');
|
||||||
const downLabel = document.getElementById('ps-mode-curve-down-label');
|
const downLabel = document.getElementById('ps-mode-curve-down-label');
|
||||||
if (up) up.setAttribute('points', buildPath(start, inlet, max));
|
if (up) up.setAttribute('points', buildPath(start, start, max));
|
||||||
|
|
||||||
// Shifted-DOWN curve (only when shift enabled): represents the
|
// Shifted-DOWN curve (only when shift enabled): represents the
|
||||||
// worst-case held-then-ramp path drawn for hold=100 % (the SVG
|
// worst-case held-then-ramp path drawn for hold=100 % (the SVG
|
||||||
@@ -152,6 +160,7 @@
|
|||||||
[
|
[
|
||||||
['dryRunLevel', dryRun],
|
['dryRunLevel', dryRun],
|
||||||
['startLevel', start],
|
['startLevel', start],
|
||||||
|
['stopLevel', stop],
|
||||||
['inflowLevel', inlet],
|
['inflowLevel', inlet],
|
||||||
['maxLevel', max],
|
['maxLevel', max],
|
||||||
['overflowLevel', overflow],
|
['overflowLevel', overflow],
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class nodeClass {
|
|||||||
levelbased:{
|
levelbased:{
|
||||||
minLevel:uiConfig.minLevel,
|
minLevel:uiConfig.minLevel,
|
||||||
startLevel:uiConfig.startLevel,
|
startLevel:uiConfig.startLevel,
|
||||||
|
stopLevel: uiConfig.stopLevel,
|
||||||
maxLevel:uiConfig.maxLevel,
|
maxLevel:uiConfig.maxLevel,
|
||||||
curveType: uiConfig.levelCurveType || uiConfig.curveType,
|
curveType: uiConfig.levelCurveType || uiConfig.curveType,
|
||||||
logCurveFactor: uiConfig.logCurveFactor,
|
logCurveFactor: uiConfig.logCurveFactor,
|
||||||
|
|||||||
@@ -119,6 +119,28 @@ class PumpingStation {
|
|||||||
this._shiftHoldValue = null;
|
this._shiftHoldValue = null;
|
||||||
this._lastDirection = null;
|
this._lastDirection = null;
|
||||||
|
|
||||||
|
// --- stopLevel hysteresis (Schmitt trigger) ---
|
||||||
|
// Levelbased control uses two thresholds:
|
||||||
|
// - startLevel: ramp foot AND rising-edge engage point. Demand
|
||||||
|
// scales 0..100 % over [startLevel, maxLevel].
|
||||||
|
// - stopLevel: falling-edge disengage point. Pumps stay engaged
|
||||||
|
// (running at minimum flow) while level drains through
|
||||||
|
// [stopLevel, startLevel]; below stopLevel they're turned off.
|
||||||
|
//
|
||||||
|
// _stopHystRunning is the engaged-state flag: flips TRUE when level
|
||||||
|
// crosses startLevel on the way up, FALSE when level crosses stopLevel
|
||||||
|
// on the way down. While engaged AND level < startLevel (i.e. the
|
||||||
|
// basin is draining through the dead band) the controller emits a
|
||||||
|
// small keep-alive percControl so MGC keeps a single pump running
|
||||||
|
// until level reaches stopLevel. Without this, percControl=0 in the
|
||||||
|
// dead band would let MGC turn the pump off, the basin would refill,
|
||||||
|
// and the pump would oscillate at startLevel instead of running for
|
||||||
|
// a full drain stroke.
|
||||||
|
//
|
||||||
|
// Editor preview also reads _stopHystRunning to shade the hysteresis
|
||||||
|
// band; runtime semantics are now explicit (no longer "bookkeeping").
|
||||||
|
this._stopHystRunning = false;
|
||||||
|
|
||||||
// --- Flow dead-band ---
|
// --- Flow dead-band ---
|
||||||
// flowThreshold (m3/s) prevents control actions on noise.
|
// flowThreshold (m3/s) prevents control actions on noise.
|
||||||
// Default 1e-4 m3/s ≈ 0.36 m3/h — below this, net flow is
|
// Default 1e-4 m3/s ≈ 0.36 m3/h — below this, net flow is
|
||||||
@@ -379,13 +401,45 @@ class PumpingStation {
|
|||||||
this.percControl = 0;
|
this.percControl = 0;
|
||||||
this._shiftHoldValue = null;
|
this._shiftHoldValue = null;
|
||||||
this._shiftArmed = false;
|
this._shiftArmed = false;
|
||||||
|
this._stopHystRunning = false;
|
||||||
this._lastDirection = direction;
|
this._lastDirection = direction;
|
||||||
Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines());
|
Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Up-curve value (always defined; foot=inflowLevel, top=maxLevel).
|
// stopLevel hysteresis (Schmitt trigger).
|
||||||
const upPct = this._scaleLevelToFlowPercent(level, this.basin?.inflowLevel ?? startLevel, cfg.maxLevel);
|
// _stopHystRunning becomes TRUE on rising edge at startLevel
|
||||||
|
// FALSE on falling edge at stopLevel
|
||||||
|
// While engaged AND level < startLevel (basin draining through the
|
||||||
|
// dead band), the controller emits a small keep-alive percControl so
|
||||||
|
// a single pump keeps running until level reaches stopLevel. Without
|
||||||
|
// hysteresis the pump would oscillate at startLevel because the
|
||||||
|
// up-curve goes through 0 there.
|
||||||
|
const stopLvl = Number(cfg.stopLevel);
|
||||||
|
const stopThresholdActive = Number.isFinite(stopLvl) && stopLvl >= 0 && stopLvl < cfg.maxLevel;
|
||||||
|
|
||||||
|
if (stopThresholdActive && level <= stopLvl) {
|
||||||
|
// Hard off: drained past stopLevel.
|
||||||
|
this.percControl = 0;
|
||||||
|
this._stopHystRunning = false;
|
||||||
|
this._lastDirection = direction;
|
||||||
|
Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Update Schmitt-trigger engaged state.
|
||||||
|
if (stopThresholdActive) {
|
||||||
|
if (!this._stopHystRunning && level >= startLevel) this._stopHystRunning = true;
|
||||||
|
// disengage on falling edge is handled by the `level <= stopLvl` block above.
|
||||||
|
} else {
|
||||||
|
// No stopLevel configured → no hysteresis; engaged only while level >= startLevel.
|
||||||
|
this._stopHystRunning = level >= startLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up-curve value. Foot stays at startLevel (per the user-set demand
|
||||||
|
// ramp), top is maxLevel. Below startLevel the curve gives 0 %; above
|
||||||
|
// maxLevel it saturates at 100 %.
|
||||||
|
const rampFoot = startLevel;
|
||||||
|
const upPct = this._scaleLevelToFlowPercent(level, rampFoot, cfg.maxLevel);
|
||||||
|
|
||||||
// Update arming flag.
|
// Update arming flag.
|
||||||
if (cfg.enableShiftedRamp) {
|
if (cfg.enableShiftedRamp) {
|
||||||
@@ -423,9 +477,23 @@ class PumpingStation {
|
|||||||
&& direction === 'draining' && this._shiftHoldValue != null;
|
&& direction === 'draining' && this._shiftHoldValue != null;
|
||||||
|
|
||||||
if (!inDrainingHold) {
|
if (!inDrainingHold) {
|
||||||
// Up curve: 0 % below inflow, scaled inflow..max → 0..100, saturates above max.
|
// Up curve: 0 % below the ramp foot (startLevel), scaled
|
||||||
if (level < (this.basin?.inflowLevel ?? startLevel)) {
|
// startLevel..maxLevel → 0..100 %, saturates above maxLevel.
|
||||||
percControl = 0;
|
// While engaged via the stopLevel Schmitt trigger AND level is
|
||||||
|
// inside the dead band [stopLevel, startLevel], emit a small
|
||||||
|
// keep-alive value so MGC's normalized scaling resolves to flow.min
|
||||||
|
// (a single pump at minimum stable speed) and the basin actually
|
||||||
|
// drains. Configurable via levelbased.deadZoneKeepAlivePercent
|
||||||
|
// (default 1%). Ramp foot stays at startLevel — keep-alive is a
|
||||||
|
// separate "engaged in dead band" signal, not a shifted ramp.
|
||||||
|
if (level < rampFoot) {
|
||||||
|
if (stopThresholdActive && this._stopHystRunning) {
|
||||||
|
const keepAlive = Number.isFinite(Number(cfg.deadZoneKeepAlivePercent))
|
||||||
|
? Number(cfg.deadZoneKeepAlivePercent) : 1;
|
||||||
|
percControl = Math.max(0, keepAlive);
|
||||||
|
} else {
|
||||||
|
percControl = 0;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
percControl = Math.max(0, upPct);
|
percControl = Math.max(0, upPct);
|
||||||
}
|
}
|
||||||
@@ -484,6 +552,16 @@ class PumpingStation {
|
|||||||
*/
|
*/
|
||||||
async forwardDemandToChildren(demand) {
|
async forwardDemandToChildren(demand) {
|
||||||
this.logger.info(`Manual demand forwarded: ${demand}`);
|
this.logger.info(`Manual demand forwarded: ${demand}`);
|
||||||
|
// Manual-mode explicit stop: MGC's handleInput now treats demand=0 as
|
||||||
|
// "hold current pump states" so the levelbased stopLevel hysteresis
|
||||||
|
// works. In manual mode the operator setting Qd=0 should still mean
|
||||||
|
// "stop now", so we issue an explicit turnOff and short-circuit.
|
||||||
|
if (Number(demand) <= 0) {
|
||||||
|
if (this.machineGroups && Object.keys(this.machineGroups).length > 0) {
|
||||||
|
Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Forward to machine groups (MGC)
|
// Forward to machine groups (MGC)
|
||||||
if (this.machineGroups && Object.keys(this.machineGroups).length > 0) {
|
if (this.machineGroups && Object.keys(this.machineGroups).length > 0) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
|||||||
Reference in New Issue
Block a user