feat(mgc): rendezvous lock + emergency bypass (no re-plan mid-rendezvous)
Once a rendezvous plan is committed it now runs to completion untouched: an ordinary new setpoint arriving while the group is 'working' is remembered (latest wins) and dispatched sequentially when the group reaches 'ready', instead of aborting + re-planning. A re-plan mid-flight dropped the in-flight schedule and re-deferred a pump that was mid-sequence, parking starting pumps at minimum flow. Only an EMERGENCY pre-empts the lock: a stop (≤0) or a pressure excursion. _isUrgentDemand (which pre-empted on any large step) is replaced by _isEmergencyDemand; the large-step pre-emption is gone — large operator steps now defer like any other setpoint. _pressureEmergency() reads planner.emergencyPressurePa and is INERT until that threshold is configured; handlePressureChange fires a latched bypass dispatch when it breaches. Verified live on the E2E Isolated MGC rig: a 1→2 pump staging transition ramps the added pump straight through (no wait-at-minimum, no start-then-stop) and the group total climbs monotonically. (The Pump-tab node's hunting is a separate demand-feedback-loop issue in that flow's wiring, not the rendezvous.) Integration tests now settle to 'ready' between demands (waitReady) since the lock defers setpoints arriving mid-move. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,11 +78,16 @@ class MachineGroup extends BaseDomain {
|
|||||||
// Demand held by the movement gate while the group is 'working'. Latest
|
// Demand held by the movement gate while the group is 'working'. Latest
|
||||||
// wins; flushed by _maybeFlushPendingDemand once the group is 'ready'.
|
// wins; flushed by _maybeFlushPendingDemand once the group is 'ready'.
|
||||||
this._pendingDemand = null;
|
this._pendingDemand = null;
|
||||||
// Intent of the last dispatch that actually proceeded — used by the
|
// Intent of the last dispatch that actually proceeded — recorded so a
|
||||||
// movement gate to treat a mode/priority change as urgent (a new
|
// pressure-emergency re-dispatch can re-plan the SAME intent against
|
||||||
// intent), not a hold-worthy nudge.
|
// the new envelope without inventing a setpoint.
|
||||||
this._lastDispatchedMode = null;
|
this._lastDispatchedMode = null;
|
||||||
this._lastPriorityKey = JSON.stringify(null);
|
this._lastPriorityKey = JSON.stringify(null);
|
||||||
|
this._lastPriorityList = null;
|
||||||
|
// Pressure-emergency latch. Set when handlePressureChange fires a
|
||||||
|
// bypass dispatch; cleared once pressure falls back below threshold,
|
||||||
|
// so the (several-times-a-second) handler doesn't re-fire every tick.
|
||||||
|
this._emergencyLatched = false;
|
||||||
this.dynamicTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 }, NCog: 0 };
|
this.dynamicTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 }, NCog: 0 };
|
||||||
this.absoluteTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } };
|
this.absoluteTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } };
|
||||||
|
|
||||||
@@ -91,7 +96,7 @@ class MachineGroup extends BaseDomain {
|
|||||||
// call that is later superseded resolves with { superseded: true }.
|
// call that is later superseded resolves with { superseded: true }.
|
||||||
this._demandDispatcher = new DemandDispatcher(
|
this._demandDispatcher = new DemandDispatcher(
|
||||||
{ logger: this.logger },
|
{ logger: this.logger },
|
||||||
(payload) => this._runDispatch(payload.source, payload.demand, payload.powerCap, payload.priorityList),
|
(payload) => this._runDispatch(payload.source, payload.demand, payload.powerCap, payload.priorityList, { emergency: payload.emergency === true }),
|
||||||
);
|
);
|
||||||
this._shutdownInFlight = new Set();
|
this._shutdownInFlight = new Set();
|
||||||
|
|
||||||
@@ -233,7 +238,27 @@ class MachineGroup extends BaseDomain {
|
|||||||
const eff = this.measurements.type('efficiency').variant('predicted').position(POSITIONS.AT_EQUIPMENT).getCurrentValue() ?? null;
|
const eff = this.measurements.type('efficiency').variant('predicted').position(POSITIONS.AT_EQUIPMENT).getCurrentValue() ?? null;
|
||||||
this.calcDistanceBEP(eff, maxEfficiency, lowestEfficiency);
|
this.calcDistanceBEP(eff, maxEfficiency, lowestEfficiency);
|
||||||
this.notifyOutputChanged();
|
this.notifyOutputChanged();
|
||||||
// Group may have just settled — release any demand the gate is holding.
|
// Emergency bypass: a pressure excursion pre-empts the rendezvous lock
|
||||||
|
// and re-plans the last intent against the new envelope immediately.
|
||||||
|
// Inert until planner.emergencyPressurePa is configured (see
|
||||||
|
// _pressureEmergency). Latched so we fire once per excursion, not every
|
||||||
|
// tick; the latch clears when pressure falls back below threshold.
|
||||||
|
if (this._pressureEmergency()) {
|
||||||
|
if (!this._emergencyLatched && Number.isFinite(this._lastDemand?.canonical)) {
|
||||||
|
this._emergencyLatched = true;
|
||||||
|
this.logger.warn(`Pressure emergency — pre-empting rendezvous, re-planning last demand ${this._lastDemand.canonical.toFixed(3)}.`);
|
||||||
|
Promise.resolve(this._demandDispatcher.fireAndWait({
|
||||||
|
source: 'pressure-emergency',
|
||||||
|
demand: this._lastDemand.canonical,
|
||||||
|
powerCap: Infinity,
|
||||||
|
priorityList: this._lastPriorityList,
|
||||||
|
emergency: true,
|
||||||
|
})).catch((e) => this.logger?.error?.(`emergency dispatch failed: ${e?.message || e}`));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._emergencyLatched = false;
|
||||||
|
}
|
||||||
|
// Group may have just settled — release any demand the lock is holding.
|
||||||
this._maybeFlushPendingDemand();
|
this._maybeFlushPendingDemand();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,25 +287,34 @@ class MachineGroup extends BaseDomain {
|
|||||||
return 'ready';
|
return 'ready';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this demand urgent enough to pre-empt an in-flight group movement?
|
// May this demand pre-empt an in-flight rendezvous? Only an EMERGENCY may —
|
||||||
// • a stop (≤0) is always urgent — never make the operator wait to stop;
|
// a committed rendezvous is otherwise locked, and ordinary new setpoints
|
||||||
// • the first demand (no prior) dispatches immediately;
|
// (any size, mode/priority changes included) are deferred and dispatched
|
||||||
// • a control-mode switch or a changed priority order is a new intent,
|
// sequentially once the group is 'ready' (_maybeFlushPendingDemand). This
|
||||||
// not a nudge — dispatch it now rather than holding it;
|
// is what stops a re-plan from re-deferring a pump that's mid-sequence
|
||||||
// • otherwise a step larger than `planner.urgentDemandFraction` of the
|
// (which parked starting pumps at minimum flow → the staging bump).
|
||||||
// capacity envelope (default 25%) pre-empts; smaller nudges wait for
|
// • a stop (≤0) is always an emergency — never make the operator wait;
|
||||||
// the group to be 'ready' so they don't thrash the current ramp.
|
// • the first demand (no prior intent) must proceed or nothing ever runs;
|
||||||
_isUrgentDemand(demandQ, priorityList) {
|
// • a pressure excursion (opts.emergency, raised by handlePressureChange)
|
||||||
|
// pre-empts so rising discharge pressure is actioned immediately.
|
||||||
|
// Everything else returns false → defer.
|
||||||
|
_isEmergencyDemand(demandQ, opts = {}) {
|
||||||
if (!(demandQ > 0)) return true;
|
if (!(demandQ > 0)) return true;
|
||||||
if (this._lastDemand?.canonical == null) return true;
|
if (this._lastDemand?.canonical == null) return true;
|
||||||
if (this.mode !== this._lastDispatchedMode) return true;
|
return opts.emergency === true;
|
||||||
if (JSON.stringify(priorityList ?? null) !== this._lastPriorityKey) return true;
|
}
|
||||||
const dt = (typeof this.calcDynamicTotals === 'function' ? this.calcDynamicTotals() : this.dynamicTotals) || {};
|
|
||||||
const span = Number(dt?.flow?.max) || 0;
|
// Pressure-excursion detector for the emergency bypass. Returns true when
|
||||||
if (span <= 0) return true;
|
// the resolved header pressure breaches a configured safety threshold.
|
||||||
const frac = Math.abs(demandQ - this._lastDemand.canonical) / span;
|
// INERT BY DEFAULT: with no `planner.emergencyPressurePa` set, this always
|
||||||
const thr = Number(this.config?.planner?.urgentDemandFraction);
|
// returns false — the bypass mechanism is wired and tested but never fires
|
||||||
return frac >= (Number.isFinite(thr) ? thr : 0.25);
|
// until a real threshold is configured. (Rate-of-rise can be added here
|
||||||
|
// later behind its own config key without touching the call sites.)
|
||||||
|
_pressureEmergency() {
|
||||||
|
const absPa = Number(this.config?.planner?.emergencyPressurePa);
|
||||||
|
if (!Number.isFinite(absPa) || absPa <= 0) return false;
|
||||||
|
const p = this.operatingPoint?.headerDiffPa;
|
||||||
|
return Number.isFinite(p) && p >= absPa;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch a demand held by the movement gate, once the group has settled.
|
// Dispatch a demand held by the movement gate, once the group has settled.
|
||||||
@@ -474,7 +508,7 @@ class MachineGroup extends BaseDomain {
|
|||||||
return this.handleInput('parent', canonical);
|
return this.handleInput('parent', canonical);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _runDispatch(source, demand, powerCap, priorityList) {
|
async _runDispatch(source, demand, powerCap, priorityList, opts = {}) {
|
||||||
const demandQ = parseFloat(demand);
|
const demandQ = parseFloat(demand);
|
||||||
if (!Number.isFinite(demandQ)) {
|
if (!Number.isFinite(demandQ)) {
|
||||||
this.logger.error(`Invalid flow demand input: ${demand}.`);
|
this.logger.error(`Invalid flow demand input: ${demand}.`);
|
||||||
@@ -485,24 +519,25 @@ class MachineGroup extends BaseDomain {
|
|||||||
// keep a defensive check in case turnOff-state arrives some other way.
|
// keep a defensive check in case turnOff-state arrives some other way.
|
||||||
if (demandQ <= 0) { await this.turnOffAllMachines(); return; }
|
if (demandQ <= 0) { await this.turnOffAllMachines(); return; }
|
||||||
|
|
||||||
// Movement gate. If the group is still converging on its previous
|
// Rendezvous lock. While the group is still converging on its committed
|
||||||
// intent ('working') and this demand is NOT urgent, hold it instead of
|
// plan ('working'), an ordinary new setpoint is NOT applied — it is
|
||||||
// aborting the in-flight ramps. The held demand (latest wins) is
|
// remembered (latest wins) and dispatched sequentially once the group
|
||||||
// dispatched the moment the group reports 'ready'
|
// reports 'ready' (_maybeFlushPendingDemand, off handlePressureChange).
|
||||||
// (_maybeFlushPendingDemand, off handlePressureChange). This is what
|
// This keeps a re-plan from dropping the in-flight schedule and
|
||||||
// stops a fast-re-commanding parent from freezing pumps at 0 by
|
// re-deferring a pump that's mid-sequence — which parked starting pumps
|
||||||
// aborting every ramp before it can progress. Urgent demand (shutdown,
|
// at minimum flow (the staging bump). Only an EMERGENCY (stop, or a
|
||||||
// or a large step) still pre-empts and dispatches immediately.
|
// pressure excursion flagged via opts.emergency) pre-empts.
|
||||||
if (this.getMovementState() === 'working' && !this._isUrgentDemand(demandQ, priorityList)) {
|
if (this.getMovementState() === 'working' && !this._isEmergencyDemand(demandQ, opts)) {
|
||||||
this._pendingDemand = { source, demand: demandQ, powerCap, priorityList };
|
this._pendingDemand = { source, demand: demandQ, powerCap, priorityList };
|
||||||
this.logger.debug(`Demand ${demandQ.toFixed(3)} held — group 'working'; will dispatch when 'ready'.`);
|
this.logger.debug(`Demand ${demandQ.toFixed(3)} held — rendezvous locked ('working'); will dispatch when 'ready'.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._pendingDemand = null;
|
this._pendingDemand = null;
|
||||||
// Record the intent now driving the group, so a later same-magnitude
|
// Record the intent now driving the group, so a pressure-emergency
|
||||||
// demand in the same mode/priority is correctly seen as a nudge.
|
// re-dispatch can re-plan the same intent against the new envelope.
|
||||||
this._lastDispatchedMode = this.mode;
|
this._lastDispatchedMode = this.mode;
|
||||||
this._lastPriorityKey = JSON.stringify(priorityList ?? null);
|
this._lastPriorityKey = JSON.stringify(priorityList ?? null);
|
||||||
|
this._lastPriorityList = priorityList ?? null;
|
||||||
|
|
||||||
await this.abortActiveMovements('new demand received');
|
await this.abortActiveMovements('new demand received');
|
||||||
const dt = this.calcDynamicTotals();
|
const dt = this.calcDynamicTotals();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Unit tests for the MGC movement state + dispatch-gate helpers
|
// Unit tests for the MGC movement state + rendezvous-lock helpers
|
||||||
// (getMovementState / _isUrgentDemand). Exercised via prototype.call with a
|
// (getMovementState / _isEmergencyDemand / _pressureEmergency). Exercised via
|
||||||
|
// prototype.call with a
|
||||||
// minimal fake `this` so no Node-RED runtime or full MachineGroup boot is
|
// minimal fake `this` so no Node-RED runtime or full MachineGroup boot is
|
||||||
// needed. See project rule .claude/rules/testing.md (basic = pure logic).
|
// needed. See project rule .claude/rules/testing.md (basic = pure logic).
|
||||||
|
|
||||||
@@ -40,38 +41,46 @@ test('movementState: working when the executor still has scheduled commands', ()
|
|||||||
assert.equal(movementStateOf({ a: machine('operational') }, 2), 'working');
|
assert.equal(movementStateOf({ a: machine('operational') }, 2), 'working');
|
||||||
});
|
});
|
||||||
|
|
||||||
function urgent(demandQ, {
|
// Rendezvous lock: only an EMERGENCY pre-empts an in-flight rendezvous; every
|
||||||
mode = 'optimalControl', lastMode = 'optimalControl',
|
// ordinary setpoint (any size, mode/priority change included) defers.
|
||||||
last = 10, priorityList = null, lastPriorityKey = 'null', span = 100, thr,
|
function emergency(demandQ, { last = 10, emergency = false } = {}) {
|
||||||
} = {}) {
|
return MachineGroup.prototype._isEmergencyDemand.call({
|
||||||
return MachineGroup.prototype._isUrgentDemand.call({
|
|
||||||
_lastDemand: last == null ? null : { canonical: last },
|
_lastDemand: last == null ? null : { canonical: last },
|
||||||
mode, _lastDispatchedMode: lastMode, _lastPriorityKey: lastPriorityKey,
|
}, demandQ, { emergency });
|
||||||
calcDynamicTotals: () => ({ flow: { max: span } }),
|
|
||||||
config: { planner: thr == null ? {} : { urgentDemandFraction: thr } },
|
|
||||||
}, demandQ, priorityList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test('urgent: a stop (≤0) always pre-empts', () => {
|
test('emergency: a stop (≤0) always pre-empts', () => {
|
||||||
assert.equal(urgent(0), true);
|
assert.equal(emergency(0), true);
|
||||||
assert.equal(urgent(-5), true);
|
assert.equal(emergency(-5), true);
|
||||||
});
|
});
|
||||||
test('urgent: the first demand (no prior) dispatches immediately', () => {
|
test('emergency: the first demand (no prior) dispatches immediately', () => {
|
||||||
assert.equal(urgent(50, { last: null }), true);
|
assert.equal(emergency(50, { last: null }), true);
|
||||||
});
|
});
|
||||||
test('urgent: a control-mode switch is a new intent', () => {
|
test('emergency: an explicit emergency flag pre-empts', () => {
|
||||||
assert.equal(urgent(10, { mode: 'priorityControl', lastMode: 'optimalControl' }), true);
|
assert.equal(emergency(60, { last: 10, emergency: true }), true);
|
||||||
});
|
});
|
||||||
test('urgent: a changed priority order is a new intent', () => {
|
test('emergency: an ordinary same-mode step defers (large or small)', () => {
|
||||||
assert.equal(urgent(10, { priorityList: ['eff', 'std'], lastPriorityKey: 'null' }), true);
|
assert.equal(emergency(12, { last: 10 }), false); // small nudge — defer
|
||||||
|
assert.equal(emergency(60, { last: 10 }), false); // large step — also defers now
|
||||||
});
|
});
|
||||||
test('urgent: a small same-mode nudge is held (not urgent)', () => {
|
|
||||||
assert.equal(urgent(12, { last: 10, span: 100 }), false); // 2% of span < 25%
|
// Pressure-excursion detector — inert until planner.emergencyPressurePa is set.
|
||||||
|
function pressureEmergency({ thr, headerPa } = {}) {
|
||||||
|
return MachineGroup.prototype._pressureEmergency.call({
|
||||||
|
config: { planner: thr == null ? {} : { emergencyPressurePa: thr } },
|
||||||
|
operatingPoint: { headerDiffPa: headerPa },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('pressureEmergency: inert (false) when no threshold is configured', () => {
|
||||||
|
assert.equal(pressureEmergency({ headerPa: 999999 }), false);
|
||||||
});
|
});
|
||||||
test('urgent: a large same-mode step pre-empts', () => {
|
test('pressureEmergency: false when header is below the configured threshold', () => {
|
||||||
assert.equal(urgent(60, { last: 10, span: 100 }), true); // 50% of span ≥ 25%
|
assert.equal(pressureEmergency({ thr: 200000, headerPa: 150000 }), false);
|
||||||
});
|
});
|
||||||
test('urgent: threshold is configurable via planner.urgentDemandFraction', () => {
|
test('pressureEmergency: true when header breaches the configured threshold', () => {
|
||||||
assert.equal(urgent(15, { last: 10, span: 100, thr: 0.02 }), true); // 5% ≥ 2%
|
assert.equal(pressureEmergency({ thr: 200000, headerPa: 210000 }), true);
|
||||||
assert.equal(urgent(15, { last: 10, span: 100, thr: 0.5 }), false); // 5% < 50%
|
});
|
||||||
|
test('pressureEmergency: false when header pressure is unknown', () => {
|
||||||
|
assert.equal(pressureEmergency({ thr: 200000, headerPa: undefined }), false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,13 +48,26 @@ async function buildGroupWithPressure() {
|
|||||||
return { mgc, pumps };
|
return { mgc, pumps };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settle to 'ready' between demands. The rendezvous lock defers a new setpoint
|
||||||
|
// that arrives while the group is still 'working', so each sweep step must wait
|
||||||
|
// for the previous move to land before issuing (and reading) the next.
|
||||||
|
async function waitReady(mgc, timeoutMs = 6000) {
|
||||||
|
const t0 = Date.now();
|
||||||
|
while (Date.now() - t0 < timeoutMs) {
|
||||||
|
if (mgc.getMovementState?.() === 'ready') return true;
|
||||||
|
try { await mgc.movementExecutor?.tick?.(); } catch { /* ignore */ }
|
||||||
|
await new Promise(r => setTimeout(r, 40));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function sweepDemand(mgc, demands_m3h) {
|
async function sweepDemand(mgc, demands_m3h) {
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const Qd_m3h of demands_m3h) {
|
for (const Qd_m3h of demands_m3h) {
|
||||||
const Qd = Qd_m3h / 3600; // m3/h → m3/s
|
const Qd = Qd_m3h / 3600; // m3/h → m3/s
|
||||||
try { await mgc.handleInput('parent', Qd); }
|
try { await mgc.handleInput('parent', Qd); }
|
||||||
catch (e) { /* turnOff or no-combination paths are part of the contract */ }
|
catch (e) { /* turnOff or no-combination paths are part of the contract */ }
|
||||||
await new Promise(r => setTimeout(r, 30));
|
await waitReady(mgc);
|
||||||
const out = getOutput(mgc);
|
const out = getOutput(mgc);
|
||||||
rows.push({
|
rows.push({
|
||||||
demand: Qd_m3h,
|
demand: Qd_m3h,
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ const baseCurve = require('../../../generalFunctions/datasets/assetData/curves/h
|
|||||||
|
|
||||||
/* ---- helpers ---- */
|
/* ---- helpers ---- */
|
||||||
|
|
||||||
|
// Settle the group to 'ready'. The rendezvous lock defers a setpoint arriving
|
||||||
|
// while the group is still 'working', so a full-MGC test must wait for each
|
||||||
|
// move to land before reading steady state or issuing the next demand.
|
||||||
|
async function waitReady(mgc, timeoutMs = 6000) {
|
||||||
|
const t0 = Date.now();
|
||||||
|
while (Date.now() - t0 < timeoutMs) {
|
||||||
|
if (mgc.getMovementState?.() === 'ready') return true;
|
||||||
|
try { await mgc.movementExecutor?.tick?.(); } catch { /* ignore */ }
|
||||||
|
await new Promise(r => setTimeout(r, 40));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }
|
function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }
|
||||||
|
|
||||||
function distortSeries(series, scale = 1, tilt = 0) {
|
function distortSeries(series, scale = 1, tilt = 0) {
|
||||||
@@ -414,6 +427,7 @@ test('full MGC optimalControl uses ≤ power than priorityControl for mixed pump
|
|||||||
return mgc.interpolation.interpolate_lin_single_point(pct, 0, 100, dt.flow.min, dt.flow.max);
|
return mgc.interpolation.interpolate_lin_single_point(pct, 0, 100, dt.flow.min, dt.flow.max);
|
||||||
}
|
}
|
||||||
await mg.handleInput('parent', pctCanonical(mg, 50), Infinity);
|
await mg.handleInput('parent', pctCanonical(mg, 50), Infinity);
|
||||||
|
await waitReady(mg); // rendezvous lock — let the move land before reading steady state
|
||||||
const optPower = mg.measurements.type('power').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
const optPower = mg.measurements.type('power').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
||||||
const optFlow = mg.measurements.type('flow').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
const optFlow = mg.measurements.type('flow').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
||||||
|
|
||||||
@@ -422,10 +436,12 @@ test('full MGC optimalControl uses ≤ power than priorityControl for mixed pump
|
|||||||
await m.handleInput('parent', 'execSequence', 'shutdown');
|
await m.handleInput('parent', 'execSequence', 'shutdown');
|
||||||
await m.handleInput('parent', 'execSequence', 'startup');
|
await m.handleInput('parent', 'execSequence', 'startup');
|
||||||
}
|
}
|
||||||
|
await waitReady(mg); // ensure the group is settled so the next demand isn't deferred
|
||||||
|
|
||||||
// Run priorityControl
|
// Run priorityControl
|
||||||
mg.setMode('prioritycontrol');
|
mg.setMode('prioritycontrol');
|
||||||
await mg.handleInput('parent', pctCanonical(mg, 50), Infinity, ['eff', 'std', 'weak']);
|
await mg.handleInput('parent', pctCanonical(mg, 50), Infinity, ['eff', 'std', 'weak']);
|
||||||
|
await waitReady(mg);
|
||||||
const prioPower = mg.measurements.type('power').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
const prioPower = mg.measurements.type('power').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
||||||
const prioFlow = mg.measurements.type('flow').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
const prioFlow = mg.measurements.type('flow').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user