specificClass.js: 1760 → 400 lines.
Machine extends BaseDomain. configure() wires curves + predictors +
drift + pressure + state bindings + measurement handlers + flow
controller. ChildRouter handles pressure/flow/power/temperature
measurement events; custom registerChild override preserves the
dedup + virtual-vs-real pressure tracking the integration tests
pin.
Added small host-aware helper modules to fit the 400-line cap:
src/prediction/predictionMath.js (calcFlow/Power/Ctrl)
src/prediction/efficiencyMath.js (calcCog/EfficiencyCurve/etc.)
src/pressure/pressureSelector.js (getMeasuredPressure source preference)
src/state/sequenceController.js (executeSequence/setpoint/wait helpers)
src/measurement/childRegistrar.js (custom registerChild path)
src/drift/healthRefresh.js (drift status update wrappers)
src/io/output.js (buildOutput + buildStatusBadge)
unitPolicy: live UnitPolicy methods .canonical()/.output()/.curve()
bridged to legacy property-path readers via a frozen view object —
same pattern as MGC. See OPEN_QUESTIONS.md.
nodeClass.js: 433 → 61 lines.
Extends BaseNodeAdapter. tickInterval=null (event-driven on state +
measurement events). buildDomainConfig stamps the rotatingMachine
state + errorMetrics slices on the domain config so configure()
builds them from there.
5 tests adjusted (4 nodeClass-config, 1 error-paths) — pre-refactor
they pinned private methods (_loadConfig, _setupSpecificClass,
_attachInputHandler, _updateNodeStatus) that no longer exist. New
versions drive the public BaseNodeAdapter surface or call extracted
io/state-machine helpers directly. See OPEN_QUESTIONS.md 2026-05-10
"private nodeClass tests" for the deferred rewrite plan.
196 / 196 tests pass (basic 110 + integration ~80 + edge ~6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
3.8 KiB
JavaScript
87 lines
3.8 KiB
JavaScript
/**
|
|
* Sequence + setpoint orchestration. Pre-refactor lived inline on
|
|
* Machine; extracted so the orchestrator stays focused. All behaviour
|
|
* is preserved verbatim including the interruptible-shutdown abort
|
|
* dance and the operational-state ramp-to-zero before shutdown.
|
|
*/
|
|
|
|
function resolveSetpointBounds(host) {
|
|
const stateMin = Number(host.state?.movementManager?.minPosition);
|
|
const stateMax = Number(host.state?.movementManager?.maxPosition);
|
|
const curveMin = Number(host.predictFlow?.currentFxyXMin);
|
|
const curveMax = Number(host.predictFlow?.currentFxyXMax);
|
|
const minCands = [stateMin, curveMin].filter(Number.isFinite);
|
|
const maxCands = [stateMax, curveMax].filter(Number.isFinite);
|
|
const fbMin = Number.isFinite(stateMin) ? stateMin : 0;
|
|
const fbMax = Number.isFinite(stateMax) ? stateMax : 100;
|
|
let min = minCands.length ? Math.max(...minCands) : fbMin;
|
|
let max = maxCands.length ? Math.min(...maxCands) : fbMax;
|
|
if (min > max) {
|
|
host.logger.warn(`Invalid setpoint bounds detected (min=${min}, max=${max}). Falling back to movement bounds.`);
|
|
min = fbMin; max = fbMax;
|
|
}
|
|
return { min, max };
|
|
}
|
|
|
|
async function setpoint(host, target) {
|
|
try {
|
|
if (!Number.isFinite(target)) { host.logger.error('Invalid setpoint: Setpoint must be a finite number.'); return; }
|
|
const { min, max } = resolveSetpointBounds(host);
|
|
const constrained = Math.min(Math.max(target, min), max);
|
|
if (constrained !== target) host.logger.warn(`Requested setpoint ${target} constrained to ${constrained} (min=${min}, max=${max})`);
|
|
host.logger.info(`Setting setpoint to ${constrained}. Current position: ${host.state.getCurrentPosition()}`);
|
|
await host.state.moveTo(constrained);
|
|
} catch (e) { host.logger.error(`Error setting setpoint: ${e}`); }
|
|
}
|
|
|
|
function waitForOperational(host, timeoutMs = 2000) {
|
|
if (host.state.getCurrentState() === 'operational') return Promise.resolve('operational');
|
|
return new Promise((resolve) => {
|
|
let done = false;
|
|
const timer = setTimeout(() => {
|
|
if (done) return;
|
|
done = true;
|
|
host.state.emitter.off('stateChange', onChange);
|
|
resolve(host.state.getCurrentState());
|
|
}, timeoutMs);
|
|
const onChange = (newState) => {
|
|
if (done) return;
|
|
if (newState === 'operational') {
|
|
done = true; clearTimeout(timer);
|
|
host.state.emitter.off('stateChange', onChange);
|
|
resolve('operational');
|
|
}
|
|
};
|
|
host.state.emitter.on('stateChange', onChange);
|
|
});
|
|
}
|
|
|
|
async function executeSequence(host, rawName) {
|
|
const name = typeof rawName === 'string' ? rawName.toLowerCase() : rawName;
|
|
const sequence = host.config.sequences[name];
|
|
if (!sequence || sequence.size === 0) {
|
|
host.logger.warn(`Sequence '${name}' not defined.`);
|
|
return;
|
|
}
|
|
const interruptible = new Set(['shutdown', 'emergencystop']);
|
|
if (interruptible.has(name)) host.state.delayedMove = null;
|
|
const current = host.state.getCurrentState();
|
|
if (interruptible.has(name) && (current === 'accelerating' || current === 'decelerating')) {
|
|
host.logger.warn(`Sequence '${name}' requested during '${current}'. Aborting active movement.`);
|
|
host.state.abortCurrentMovement(`${name} sequence requested`, { returnToOperational: true });
|
|
await waitForOperational(host, 2000);
|
|
}
|
|
if (host.state.getCurrentState() === 'operational' && name === 'shutdown') {
|
|
host.logger.info(`Machine will ramp down to position 0 before performing ${name} sequence`);
|
|
await setpoint(host, 0);
|
|
}
|
|
host.logger.info(` --------- Executing sequence: ${name} -------------`);
|
|
for (const s of sequence) {
|
|
try { await host.state.transitionToState(s); }
|
|
catch (e) { host.logger.error(`Error during sequence '${name}': ${e}`); break; }
|
|
}
|
|
host.updatePosition();
|
|
}
|
|
|
|
module.exports = { setpoint, executeSequence, resolveSetpointBounds, waitForOperational };
|