src/groupOps/ groupOperatingPoint + groupCurves (pure functions)
src/totals/ totalsCalculator (dynamic + absolute + active)
src/combinatorics/ pumpCombinations (validPumpCombinations + checkSpecialCases)
src/optimizer/ bestCombination (CoG) + bepGravitation (BEP-G + marginal-cost)
src/efficiency/ groupEfficiency (calc + distance helpers)
src/dispatch/ demandDispatcher (LatestWinsGate-based; replaces
_dispatchInFlight + _delayedCall)
src/commands/ canonical names from start (set.mode/scaling/demand,
child.register) + legacy aliases
CONTRACT.md inputs/outputs/events surface
53 basic tests pass (52 new + 1 pre-existing).
specificClass.js / nodeClass.js untouched — integration in P4 wave 2.
Findings flagged via agents (TODO append to OPEN_QUESTIONS.md):
- calcGroupEfficiency.maxEfficiency is actually the mean (misleading name)
- checkSpecialCases has a no-op `return false` inside forEach
- MGC doesn't route cmd.startup/shutdown/estop — confirm if station broadcasts need it
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
1.9 KiB
JavaScript
59 lines
1.9 KiB
JavaScript
'use strict';
|
|
|
|
// Handler functions for machineGroupControl commands. Each handler receives:
|
|
// source: the domain (specificClass) instance — exposes setMode, setScaling,
|
|
// handleInput, childRegistrationUtils.registerChild, logger,
|
|
// config.general.name.
|
|
// msg: the Node-RED input message.
|
|
// ctx: { node, RED, send, logger } — provided by BaseNodeAdapter.
|
|
//
|
|
// Pure functions: no module-level state. The registry already enforces the
|
|
// typeof-check ladder; per-topic semantic validation lives here.
|
|
|
|
function _logger(source, ctx) {
|
|
return ctx?.logger || source?.logger || null;
|
|
}
|
|
|
|
exports.setMode = (source, msg) => {
|
|
source.setMode(msg.payload);
|
|
};
|
|
|
|
exports.setScaling = (source, msg) => {
|
|
source.setScaling(msg.payload);
|
|
};
|
|
|
|
exports.registerChild = (source, msg, ctx) => {
|
|
const log = _logger(source, ctx);
|
|
const childId = msg.payload;
|
|
const childObj = ctx?.RED?.nodes?.getNode?.(childId);
|
|
if (!childObj || !childObj.source) {
|
|
log?.warn?.(`registerChild: child '${childId}' not found or has no .source`);
|
|
return;
|
|
}
|
|
source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
|
|
};
|
|
|
|
exports.setDemand = async (source, msg, ctx) => {
|
|
const log = _logger(source, ctx);
|
|
const demand = parseFloat(msg.payload);
|
|
if (Number.isNaN(demand)) {
|
|
log?.error?.(`set.demand: invalid Qd value '${msg.payload}'`);
|
|
return;
|
|
}
|
|
try {
|
|
await source.handleInput('parent', demand);
|
|
} catch (err) {
|
|
log?.error?.(`set.demand: failed to process Qd: ${err && err.message}`);
|
|
return;
|
|
}
|
|
// Reply on Port 0 with the configured node name as topic — preserves the
|
|
// legacy "done" handshake some downstream flows rely on.
|
|
if (typeof ctx?.send === 'function') {
|
|
const reply = Object.assign({}, msg, {
|
|
topic: source?.config?.general?.name,
|
|
payload: 'done',
|
|
});
|
|
ctx.send(reply);
|
|
}
|
|
};
|