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