Compare commits
1 Commits
6ab585bcc2
...
e2ebb31816
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2ebb31816 |
@@ -86,6 +86,7 @@
|
||||
shiftLevel: { value: 0 },
|
||||
shiftArmPercent: { value: 95 },
|
||||
startLevel: { value: null },
|
||||
stopLevel: { value: null },
|
||||
minLevel: { value: null },
|
||||
maxLevel: { value: null },
|
||||
flowSetpoint: { value: null },
|
||||
@@ -413,6 +414,11 @@
|
||||
<input type="number" id="node-input-startLevel" min="0" step="0.01" />
|
||||
<span class="ps-unit">m</span>
|
||||
</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><label>inflowLevel</label><div class="ps-sub">from basin above</div></div>
|
||||
<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. -->
|
||||
<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-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-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" />
|
||||
@@ -484,6 +491,7 @@
|
||||
(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-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-maxLevel" style="display:none;"></text>
|
||||
<text id="ps-mode-label-overflowLevel" style="display:none;"></text>
|
||||
|
||||
@@ -66,6 +66,13 @@
|
||||
max ?? inlet ?? start ?? EPS,
|
||||
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).
|
||||
if (shiftEnabled) {
|
||||
setBounds('shiftLevel',
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
const start = fNum('startLevel');
|
||||
const inlet = fNum('inflowLevel');
|
||||
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%
|
||||
// (no separate input). Below dryRunLevel the runtime hard-stops;
|
||||
// we draw it as the leftmost vertical marker so the user sees
|
||||
@@ -85,11 +90,14 @@
|
||||
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 down = document.getElementById('ps-mode-curve-down');
|
||||
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
|
||||
// worst-case held-then-ramp path drawn for hold=100 % (the SVG
|
||||
@@ -152,6 +160,7 @@
|
||||
[
|
||||
['dryRunLevel', dryRun],
|
||||
['startLevel', start],
|
||||
['stopLevel', stop],
|
||||
['inflowLevel', inlet],
|
||||
['maxLevel', max],
|
||||
['overflowLevel', overflow],
|
||||
|
||||
@@ -66,6 +66,7 @@ class nodeClass {
|
||||
levelbased:{
|
||||
minLevel:uiConfig.minLevel,
|
||||
startLevel:uiConfig.startLevel,
|
||||
stopLevel: uiConfig.stopLevel,
|
||||
maxLevel:uiConfig.maxLevel,
|
||||
curveType: uiConfig.levelCurveType || uiConfig.curveType,
|
||||
logCurveFactor: uiConfig.logCurveFactor,
|
||||
|
||||
@@ -119,6 +119,28 @@ class PumpingStation {
|
||||
this._shiftHoldValue = 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 ---
|
||||
// flowThreshold (m3/s) prevents control actions on noise.
|
||||
// Default 1e-4 m3/s ≈ 0.36 m3/h — below this, net flow is
|
||||
@@ -379,13 +401,45 @@ class PumpingStation {
|
||||
this.percControl = 0;
|
||||
this._shiftHoldValue = null;
|
||||
this._shiftArmed = false;
|
||||
this._stopHystRunning = false;
|
||||
this._lastDirection = direction;
|
||||
Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines());
|
||||
return;
|
||||
}
|
||||
|
||||
// Up-curve value (always defined; foot=inflowLevel, top=maxLevel).
|
||||
const upPct = this._scaleLevelToFlowPercent(level, this.basin?.inflowLevel ?? startLevel, cfg.maxLevel);
|
||||
// stopLevel hysteresis (Schmitt trigger).
|
||||
// _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.
|
||||
if (cfg.enableShiftedRamp) {
|
||||
@@ -423,9 +477,23 @@ class PumpingStation {
|
||||
&& direction === 'draining' && this._shiftHoldValue != null;
|
||||
|
||||
if (!inDrainingHold) {
|
||||
// Up curve: 0 % below inflow, scaled inflow..max → 0..100, saturates above max.
|
||||
if (level < (this.basin?.inflowLevel ?? startLevel)) {
|
||||
// Up curve: 0 % below the ramp foot (startLevel), scaled
|
||||
// startLevel..maxLevel → 0..100 %, saturates above maxLevel.
|
||||
// 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 {
|
||||
percControl = Math.max(0, upPct);
|
||||
}
|
||||
@@ -484,6 +552,16 @@ class PumpingStation {
|
||||
*/
|
||||
async forwardDemandToChildren(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)
|
||||
if (this.machineGroups && Object.keys(this.machineGroups).length > 0) {
|
||||
await Promise.all(
|
||||
|
||||
Reference in New Issue
Block a user