Consumer half of the abort-token mechanism added in generalFunctions
state.js. executeSequence captures host.state.sequenceAbortToken at
entry, then re-checks before every state transition and after the
optional ramp-down. If MGC (or any external caller) bumps the token
mid-sequence, the loop bails out cleanly — no more barge-through where
a pre-empted shutdown advances through stopping → coolingdown after a
fresh demand has already engaged the pump.
Without this the MGC rendezvous planner can't reliably re-dispatch a
pump that's mid-shutdown: the new flowmovement claims the gate, but
the old shutdown's for-loop keeps running on microtasks and steps the
FSM into idle/off underneath it.
Also: wiki regen following the same visual-first 14-section template as
the other EVOLV nodes — Reference-{Architecture,Contracts,Examples,
Limitations}.md split with _Sidebar.md index.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
4.7 KiB
JavaScript
105 lines
4.7 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;
|
|
}
|
|
// Snapshot the sequence-abort token at entry, BEFORE any awaits. If an
|
|
// external abort advances the counter while we're inside this call
|
|
// (setpoint ramp-down, waitForOperational, or the state transition
|
|
// loop), every check below sees the mismatch and breaks out so the
|
|
// new dispatch can claim the FSM. Capturing later would conflate the
|
|
// abort that fired during setpoint(0) with the initial entry state.
|
|
const startToken = host.state.sequenceAbortToken ?? 0;
|
|
const aborted = () => (host.state.sequenceAbortToken ?? 0) !== startToken;
|
|
|
|
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);
|
|
if (aborted()) {
|
|
host.logger.warn(`Sequence '${name}' interrupted during ramp-down by external abort; not entering shutdown loop.`);
|
|
host.updatePosition();
|
|
return;
|
|
}
|
|
}
|
|
host.logger.info(` --------- Executing sequence: ${name} -------------`);
|
|
for (const s of sequence) {
|
|
if (aborted()) {
|
|
host.logger.warn(`Sequence '${name}' interrupted at step '${s}' by external abort; stopping further transitions.`);
|
|
break;
|
|
}
|
|
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 };
|