接入 task HAL Web 仿真运行时
This commit is contained in:
@@ -16,6 +16,9 @@ import {
|
||||
selectMachineFileProgram,
|
||||
stageProfileMachineFiles,
|
||||
} from "../runtime/linuxcnc-machine-file-staging.js";
|
||||
import {
|
||||
buildTaskHalSessionFromMachineFiles,
|
||||
} from "../runtime/linuxcnc-task-hal-runtime.js";
|
||||
import {
|
||||
createLinuxCncTaskPolicyStatus,
|
||||
gateLinuxCncTaskAction,
|
||||
@@ -128,6 +131,13 @@ const initialState = {
|
||||
programExecutionMotionIndex: 0,
|
||||
programExecutionSampleIndex: 0,
|
||||
programRuntimeFeedback: null,
|
||||
taskHalRuntime: null,
|
||||
taskHalRuntimeReadiness: null,
|
||||
taskHalStatus: null,
|
||||
taskHalSession: null,
|
||||
taskHalExecutionPending: false,
|
||||
taskHalExecutionSequence: 0,
|
||||
taskHalFallbackReason: null,
|
||||
interpreterExecutionPending: false,
|
||||
interpreterExecutionSequence: 0,
|
||||
machineFileExecution: null,
|
||||
@@ -404,6 +414,50 @@ export function createSimulationStore(seed = {}) {
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "ATTACH_TASK_HAL_RUNTIME":
|
||||
{
|
||||
const runtime = action.runtime || null;
|
||||
const maybeReadiness = action.readiness || (runtime?.readiness ? runtime.readiness() : null);
|
||||
const readiness = typeof maybeReadiness?.then === "function"
|
||||
? {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
|
||||
loaded: Boolean(runtime?.loaded),
|
||||
taskRuntimeReady: false,
|
||||
motionRuntimeReady: false,
|
||||
halRuntimeReady: false,
|
||||
pending: true,
|
||||
}
|
||||
: maybeReadiness;
|
||||
setState({
|
||||
taskHalRuntime: runtime,
|
||||
taskHalRuntimeReadiness: readiness,
|
||||
taskHalFallbackReason: runtime?.loaded ? null : "LinuxCNC task/HAL runtime missing",
|
||||
operatorMessage: runtime?.loaded
|
||||
? "LinuxCNC task/HAL runtime ready"
|
||||
: "LinuxCNC task/HAL runtime missing",
|
||||
});
|
||||
if (runtime?.loaded && state.machineFileStaging?.status === "staged") {
|
||||
initializeTaskHalSession().catch(() => {});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "TASK_HAL_RUNTIME_FAILED":
|
||||
setState({
|
||||
taskHalRuntime: null,
|
||||
taskHalRuntimeReadiness: {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
|
||||
loaded: false,
|
||||
taskRuntimeReady: false,
|
||||
motionRuntimeReady: false,
|
||||
halRuntimeReady: false,
|
||||
nativeTaskReady: false,
|
||||
nativeHalSyncReady: false,
|
||||
error: action.error,
|
||||
},
|
||||
taskHalFallbackReason: action.error,
|
||||
operatorMessage: `LinuxCNC task/HAL runtime blocked: ${action.error}`,
|
||||
});
|
||||
break;
|
||||
case "RUN_INTERPRETER_PROGRAM":
|
||||
if (!state.interpreterRuntime?.loaded) {
|
||||
setState({
|
||||
@@ -657,6 +711,9 @@ export function createSimulationStore(seed = {}) {
|
||||
},
|
||||
operatorMessage: `LinuxCNC machine files staged ${action.save.fileCount}`,
|
||||
});
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
initializeTaskHalSession().catch(() => {});
|
||||
}
|
||||
break;
|
||||
case "LOAD_LINUXCNC_GCODE_SOURCE":
|
||||
{
|
||||
@@ -700,11 +757,31 @@ export function createSimulationStore(seed = {}) {
|
||||
},
|
||||
operatorMessage: `loaded LinuxCNC 5-axis source ${selectedFile.sourceRel}`,
|
||||
});
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
initializeTaskHalSession({ openProgram: true }).catch(() => {});
|
||||
}
|
||||
if (state.interpreterRuntime?.loaded) {
|
||||
dispatch({ type: "RUN_INTERPRETER_PROGRAM" });
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "TASK_HAL_SESSION_READY":
|
||||
setState({
|
||||
taskHalSession: action.session,
|
||||
taskHalFallbackReason: null,
|
||||
operatorMessage: `LinuxCNC task/HAL session ready ${action.session.programPath || "-"}`,
|
||||
});
|
||||
break;
|
||||
case "TASK_HAL_STATUS_APPLIED":
|
||||
setState(applyTaskHalStatusPatch(state, action.status, action.operatorMessage));
|
||||
break;
|
||||
case "TASK_HAL_COMMAND_FAILED":
|
||||
setState({
|
||||
taskHalFallbackReason: action.error,
|
||||
taskHalExecutionPending: false,
|
||||
operatorMessage: `task/HAL fallback: ${action.error}`,
|
||||
});
|
||||
break;
|
||||
case "MACHINE_FILE_STAGING_FAILED":
|
||||
setState({
|
||||
machineFileStaging: {
|
||||
@@ -745,6 +822,12 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_STATE", state: state.machine.powerOn ? "ESTOP_RESET" : "ON" },
|
||||
], { operatorMessage: state.machine.powerOn ? "task/HAL machine power off" : "task/HAL machine power on" }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const turningOff = state.machine.taskState === "on" || state.machine.powerOn;
|
||||
setState({
|
||||
machine: {
|
||||
@@ -825,6 +908,12 @@ export function createSimulationStore(seed = {}) {
|
||||
break;
|
||||
}
|
||||
const mode = normalizeLinuxCncTaskMode(action.mode);
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_MODE", mode: mode.toUpperCase() },
|
||||
], { operatorMessage: `task/HAL mode ${mode}` }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -857,6 +946,17 @@ export function createSimulationStore(seed = {}) {
|
||||
const axis = action.axis || state.machine.jogAxis;
|
||||
const direction = Number(action.direction || 1);
|
||||
const increment = Number(action.increment || state.machine.jogIncrement);
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{
|
||||
type: "EMC_JOG_INCR",
|
||||
axis: axis.toUpperCase(),
|
||||
distance: direction * increment,
|
||||
velocity: Number(action.velocity || 60),
|
||||
},
|
||||
], { operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}` }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -880,6 +980,14 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
const command = normalizeMdiCommand(action.command ?? state.machine.mdiCommand);
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_MODE", mode: "MDI" },
|
||||
{ type: "EMC_TASK_PLAN_EXECUTE", mdi: command },
|
||||
], { operatorMessage: `task/HAL MDI ${command}` }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const mdiResult = executeMdiCommand(state, action.command ?? state.machine.mdiCommand);
|
||||
setState(mdiResult.patch);
|
||||
}
|
||||
@@ -917,6 +1025,14 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_STATE", state: "ON" },
|
||||
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
|
||||
{ type: "EMC_TASK_PLAN_RUN", line: Math.max(Number(state.activeLine || 1) - 1, 0) },
|
||||
], { taskCycles: 5, operatorMessage: "task/HAL program run" }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const playback = nextProgramRuntimeSamplePlayback(state, 5);
|
||||
setState({
|
||||
machine: {
|
||||
@@ -952,6 +1068,12 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_ABORT" },
|
||||
], { operatorMessage: action.type === "ABORT" ? "task/HAL abort complete" : "task/HAL program stopped" }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -975,6 +1097,12 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_PLAN_PAUSE" },
|
||||
], { operatorMessage: "task/HAL program paused" }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -996,6 +1124,12 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_PLAN_RESUME" },
|
||||
], { operatorMessage: "task/HAL program resumed" }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const resumeState = state.machine.interpResumeState === "idle"
|
||||
? "reading"
|
||||
: state.machine.interpResumeState;
|
||||
@@ -1333,6 +1467,114 @@ export function createSimulationStore(seed = {}) {
|
||||
dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
|
||||
};
|
||||
|
||||
const initializeTaskHalSession = async ({ openProgram = true } = {}) => {
|
||||
if (!state.taskHalRuntime?.loaded || !state.machineFileStaging?.save?.files?.length) {
|
||||
return null;
|
||||
}
|
||||
const selectedPlan = selectMachineFileProgramForState(state);
|
||||
const session = buildTaskHalSessionFromMachineFiles({
|
||||
profile: state.profile,
|
||||
plan: selectedPlan,
|
||||
save: state.machineFileStaging.save,
|
||||
selectedProgramRel: state.machineFileStaging.selectedGcodeSourceRel,
|
||||
});
|
||||
await state.taskHalRuntime.resetSession?.();
|
||||
await state.taskHalRuntime.initSession({
|
||||
profileId: session.profileId,
|
||||
iniPath: session.iniPath,
|
||||
iniText: session.iniText,
|
||||
programPath: session.programPath,
|
||||
semanticBoundary: session.semanticBoundary,
|
||||
});
|
||||
await state.taskHalRuntime.stageFiles(session.files);
|
||||
if (openProgram && session.programPath) {
|
||||
await state.taskHalRuntime.openProgram(session.programPath);
|
||||
}
|
||||
dispatch({ type: "TASK_HAL_SESSION_READY", session });
|
||||
const status = await state.taskHalRuntime.readStatus();
|
||||
dispatch({
|
||||
type: "TASK_HAL_STATUS_APPLIED",
|
||||
status,
|
||||
operatorMessage: `LinuxCNC task/HAL session ready ${session.programPath || "-"}`,
|
||||
});
|
||||
return session;
|
||||
};
|
||||
|
||||
const runTaskHalCommandSequence = async (commands, {
|
||||
taskCycles = 1,
|
||||
taskPeriodNs = 10000000,
|
||||
servoPeriodNs = 1000000,
|
||||
operatorMessage = "task/HAL command complete",
|
||||
} = {}) => {
|
||||
if (!state.taskHalRuntime?.loaded) {
|
||||
throw new Error("LinuxCNC task/HAL runtime not attached");
|
||||
}
|
||||
const sequence = state.taskHalExecutionSequence + 1;
|
||||
setState({
|
||||
taskHalExecutionPending: true,
|
||||
taskHalExecutionSequence: sequence,
|
||||
operatorMessage: "LinuxCNC task/HAL command running",
|
||||
});
|
||||
try {
|
||||
if (!state.taskHalSession && state.machineFileStaging?.save?.files?.length) {
|
||||
await initializeTaskHalSession({ openProgram: true });
|
||||
} else if (!state.taskHalSession && state.programLines?.length) {
|
||||
await initializeFixtureTaskHalSessionForState();
|
||||
}
|
||||
for (const command of commands) {
|
||||
await state.taskHalRuntime.sendCommand(command);
|
||||
}
|
||||
await state.taskHalRuntime.runCycles({ taskPeriodNs, servoPeriodNs, taskCycles });
|
||||
const status = await state.taskHalRuntime.readStatus();
|
||||
if (state.taskHalExecutionSequence !== sequence) {
|
||||
return status;
|
||||
}
|
||||
dispatch({ type: "TASK_HAL_STATUS_APPLIED", status, operatorMessage });
|
||||
return status;
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: "TASK_HAL_COMMAND_FAILED",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const initializeFixtureTaskHalSessionForState = async () => {
|
||||
if (!state.taskHalRuntime?.loaded) return null;
|
||||
const programPath = "web-ui/current-program.ngc";
|
||||
const iniText = state.linuxCncIniConfig?.sourceText || `[TRAJ]\nCOORDINATES = ${state.profile.traj.coordinates.split("").join(" ")}\n`;
|
||||
await state.taskHalRuntime.resetSession?.();
|
||||
await state.taskHalRuntime.initSession({
|
||||
profileId: state.machineProfile,
|
||||
iniPath: state.profile.iniPath,
|
||||
iniText,
|
||||
programPath,
|
||||
semanticBoundary: "linuxcnc_task_hal_fixture_program_session",
|
||||
});
|
||||
await state.taskHalRuntime.stageFiles([
|
||||
{
|
||||
sourceRel: state.programSourceRel || state.activeProgram,
|
||||
wasmPath: programPath,
|
||||
path: programPath,
|
||||
kind: "demo",
|
||||
text: state.programLines.join("\n"),
|
||||
bytes: state.programLines.join("\n").length,
|
||||
},
|
||||
]);
|
||||
await state.taskHalRuntime.openProgram(programPath);
|
||||
const session = {
|
||||
apiName: "web-rtcp-5axis-task-hal-fixture-session",
|
||||
semanticBoundary: "linuxcnc_task_hal_fixture_program_session",
|
||||
profileId: state.machineProfile,
|
||||
iniPath: state.profile.iniPath,
|
||||
programPath,
|
||||
fileCount: 1,
|
||||
};
|
||||
dispatch({ type: "TASK_HAL_SESSION_READY", session });
|
||||
return session;
|
||||
};
|
||||
|
||||
const scheduleAsyncKinematicsRefresh = () => {
|
||||
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) return null;
|
||||
if (state.frameSourceMode !== "source-derived-kinematics-wasm") return null;
|
||||
@@ -1360,6 +1602,7 @@ export function createSimulationStore(seed = {}) {
|
||||
restoreSession,
|
||||
stageMachineFiles,
|
||||
runFullBoundaryAudit,
|
||||
initializeTaskHalSession,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1454,6 +1697,115 @@ function isAsyncKinematicsRuntime(runtime) {
|
||||
return runtime?.executionContext === "worker";
|
||||
}
|
||||
|
||||
function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
const ui = status?.ui || {};
|
||||
const task = status?.task || {};
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
const taskState = normalizeTaskHalTaskState(ui.taskState || task.state);
|
||||
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine.mode);
|
||||
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
|
||||
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
|
||||
const kinsType = kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
|
||||
const axisPose = clampAxisPoseToProfile({
|
||||
...state.axisPose,
|
||||
...ui.axisPose,
|
||||
}, state.profile);
|
||||
const currentVelocity = Number.isFinite(ui.currentVelocity) && ui.currentVelocity > 0
|
||||
? ui.currentVelocity
|
||||
: state.feed.currentVelocity;
|
||||
const paused = interpState === "paused" || motion.paused === true;
|
||||
const aborted = motion.aborted === true;
|
||||
const programComplete = interpState === "idle" && Number(task.nextProgramLine || 0) >= Number(task.openedLineCount || 1);
|
||||
const runState = aborted
|
||||
? "stopped"
|
||||
: paused
|
||||
? "paused"
|
||||
: taskMode === "mdi"
|
||||
? "mdi"
|
||||
: interpState === "reading"
|
||||
? "running"
|
||||
: programComplete
|
||||
? "complete"
|
||||
: state.runState === "jogging"
|
||||
? "jogging"
|
||||
: "idle";
|
||||
|
||||
return {
|
||||
taskHalStatus: status,
|
||||
taskHalExecutionPending: false,
|
||||
taskHalFallbackReason: null,
|
||||
activeLine,
|
||||
axisPose,
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
programExecutionSourceMode: "linuxcnc-task-motion-hal-wasm",
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: taskState === "on",
|
||||
estopActive: taskState === "estop",
|
||||
taskState,
|
||||
mode: taskMode,
|
||||
interpState,
|
||||
interpResumeState: paused ? state.machine.interpResumeState || "reading" : interpState,
|
||||
taskPaused: paused,
|
||||
},
|
||||
runState,
|
||||
feed: {
|
||||
...state.feed,
|
||||
currentVelocity,
|
||||
},
|
||||
programRuntimeFeedback: createTaskHalRuntimeFeedback(state, status, axisPose, activeLine),
|
||||
operatorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
|
||||
const ui = status?.ui || {};
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-program-runtime-feedback",
|
||||
sourceMode: "linuxcnc-task-motion-hal-wasm",
|
||||
semanticBoundary: status?.semanticBoundary || "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
sampleIndex: Number(ui.servoCycle || 0),
|
||||
motionIndex: Math.max(Number(ui.activeLine || 1) - 1, 0),
|
||||
line: activeLine,
|
||||
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
|
||||
timeSeconds: Number(ui.taskCycle || 0) * 0.01,
|
||||
axisPose,
|
||||
currentVelocityMmPerMin: Number(ui.currentVelocity || 0),
|
||||
requestedVelocityMmPerMin: Number(motion.requestedVel || 0) * 60,
|
||||
distanceToGo: motion.inPosition === true ? 0 : 1,
|
||||
dtg: { x: 0, y: 0, z: 0 },
|
||||
queueDepth: Number(ui.motionQueueDepth || status?.motionStatus?.commandQueueDepth || 0),
|
||||
activeDepth: motion.inPosition === true ? 0 : 1,
|
||||
cycle: Number(ui.servoCycle || status?.servoCycle || 0),
|
||||
taskCycle: Number(ui.taskCycle || status?.task?.cycle || 0),
|
||||
halChangedPinCount: Number(ui.halChangedPinCount || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTaskHalTaskState(value) {
|
||||
const state = String(value || "").toLowerCase().replaceAll("_", "-");
|
||||
if (state === "on") return "on";
|
||||
if (state === "estop") return "estop";
|
||||
if (state === "off") return "off";
|
||||
return "estop-reset";
|
||||
}
|
||||
|
||||
function normalizeTaskHalInterpState(value) {
|
||||
const state = String(value || "").toLowerCase();
|
||||
if (state === "paused") return "paused";
|
||||
if (state === "reading") return "reading";
|
||||
return "idle";
|
||||
}
|
||||
|
||||
function kinsTypeFromSwitchkinsTypeValue(state, value) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric) || numeric === 0) return "identity";
|
||||
return state.profile.kinematicsParameters.switchkinsTypes
|
||||
.find((type) => Number(type.value) === numeric)?.webKinsType || state.kinsType;
|
||||
}
|
||||
|
||||
function canMoveMachine(state) {
|
||||
return createLinuxCncTaskPolicyStatus(state).canMove;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user