Files
rotatingMachine/src/state/sequenceController.js
znetsixe 5ea0b0bda6 feat(state): honor sequenceAbortToken so external aborts cleanly break sequences
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>
2026-05-17 19:44:48 +02:00

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 };