接入 task HAL Web 仿真运行时
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"build": "node scripts/build-static.mjs",
|
||||
"dev": "python3 -m http.server 4173",
|
||||
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh",
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
|
||||
@@ -15,6 +15,7 @@ await copyLinuxCncConfigAssets();
|
||||
await copyKinematicsRuntimeAssets();
|
||||
await copyInterpreterRuntimeAssets();
|
||||
await copyTpRuntimeAssets();
|
||||
await copyTaskHalRuntimeAssets();
|
||||
|
||||
const packageJson = JSON.parse(await readFile(join(appRoot, "package.json"), "utf8"));
|
||||
const forbiddenDependencies = ["react", "vue", "@angular/core", "svelte"];
|
||||
@@ -46,6 +47,15 @@ async function copyTpRuntimeAssets() {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyTaskHalRuntimeAssets() {
|
||||
const taskHalSrcDir = join(repoRoot, "wasm-port/build/wasm/task-hal");
|
||||
const taskHalDistDir = join(distDir, "wasm-port/build/wasm/task-hal");
|
||||
await mkdir(taskHalDistDir, { recursive: true });
|
||||
for (const entry of ["linuxcnc_task_hal.js", "linuxcnc_task_hal.wasm"]) {
|
||||
await cp(join(taskHalSrcDir, entry), join(taskHalDistDir, entry));
|
||||
}
|
||||
}
|
||||
|
||||
console.log("gmoccapy_static_build=ok");
|
||||
|
||||
async function copyLinuxCncConfigAssets() {
|
||||
@@ -72,7 +82,7 @@ async function copyKinematicsRuntimeAssets() {
|
||||
join(sdkSrcDir, "linuxcnc-kinematics.js"),
|
||||
join(sdkDistDir, "linuxcnc-kinematics.js"),
|
||||
);
|
||||
for (const entry of ["linuxcnc-interp.js", "linuxcnc-hal.js", "linuxcnc-tp.js"]) {
|
||||
for (const entry of ["linuxcnc-interp.js", "linuxcnc-hal.js", "linuxcnc-tp.js", "linuxcnc-task-hal.js"]) {
|
||||
await cp(join(sdkSrcDir, entry), join(sdkDistDir, entry));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { createLinuxCncInterpreterWorkerRuntime } from "./runtime/linuxcnc-inter
|
||||
import { createLinuxCncKinematicsRuntime } from "./runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { createLinuxCncKinematicsWorkerRuntime } from "./runtime/linuxcnc-kinematics-worker-client.js";
|
||||
import { loadLinuxCncIniConfig } from "./runtime/linuxcnc-ini-runtime.js";
|
||||
import { createLinuxCncTaskHalRuntime } from "./runtime/linuxcnc-task-hal-runtime.js";
|
||||
import { createLinuxCncTaskHalWorkerRuntime } from "./runtime/linuxcnc-task-hal-worker-client.js";
|
||||
|
||||
const app = document.querySelector("#app");
|
||||
|
||||
@@ -17,6 +19,7 @@ const shell = mountGmoccapyShell(app, store);
|
||||
const iniConfigReady = attachProfileIniConfig(store, store.getState().profile);
|
||||
const kinematicsRuntimeReady = attachDefaultKinematicsRuntime(store, store.getState().profile.kinematicsModuleId || store.getState().machineProfile);
|
||||
const interpreterRuntimeReady = attachDefaultInterpreterRuntime(store);
|
||||
const taskHalRuntimeReady = attachDefaultTaskHalRuntime(store);
|
||||
let attachedKinematicsProfile = store.getState().machineProfile;
|
||||
let attachedIniProfile = store.getState().machineProfile;
|
||||
store.subscribe((state) => {
|
||||
@@ -42,6 +45,7 @@ window.webRtcp5AxisSimulation = {
|
||||
iniConfigReady,
|
||||
kinematicsRuntimeReady,
|
||||
interpreterRuntimeReady,
|
||||
taskHalRuntimeReady,
|
||||
};
|
||||
|
||||
store.dispatch({ type: "BOOT_READY" });
|
||||
@@ -145,3 +149,41 @@ async function attachDefaultInterpreterRuntime(store) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function attachDefaultTaskHalRuntime(store) {
|
||||
const sdkModuleUrls = [
|
||||
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
|
||||
new URL("../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
|
||||
];
|
||||
const errors = [];
|
||||
for (const sdkModuleUrl of sdkModuleUrls) {
|
||||
if (typeof Worker === "function") {
|
||||
try {
|
||||
const runtime = await createLinuxCncTaskHalWorkerRuntime({ sdkModuleUrl });
|
||||
const readiness = await runtime.readiness();
|
||||
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime, readiness });
|
||||
return readiness;
|
||||
} catch (error) {
|
||||
errors.push(`${sdkModuleUrl} worker: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const runtime = await createLinuxCncTaskHalRuntime({ sdkModuleUrl });
|
||||
const readiness = runtime.readiness();
|
||||
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime, readiness });
|
||||
return readiness;
|
||||
} catch (error) {
|
||||
errors.push(`${sdkModuleUrl}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
store.dispatch({ type: "TASK_HAL_RUNTIME_FAILED", error: errors.join(" | ") });
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
|
||||
loaded: false,
|
||||
taskRuntimeReady: false,
|
||||
motionRuntimeReady: false,
|
||||
halRuntimeReady: false,
|
||||
error: errors.join(" | "),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,37 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
programExecution?.plannerTiming?.plannerRuntimeReady === true,
|
||||
);
|
||||
const halSwitchkinsEvidenceReady = machineFileText.includes("fiveaxis_hal_switchkins: rc=0 found=1");
|
||||
const taskHalSummary = state.taskHalStatus?.summary || {};
|
||||
const taskRuntimeReady = Boolean(
|
||||
taskHalSummary.taskRuntimeReady === true ||
|
||||
state.taskHalRuntimeReadiness?.taskRuntimeReady === true,
|
||||
);
|
||||
const motionRuntimeReady = Boolean(
|
||||
taskHalSummary.motionRuntimeReady === true ||
|
||||
state.taskHalRuntimeReadiness?.motionRuntimeReady === true,
|
||||
);
|
||||
const halRuntimeReady = Boolean(
|
||||
taskHalSummary.halRuntimeReady === true ||
|
||||
state.taskHalRuntimeReadiness?.halRuntimeReady === true,
|
||||
);
|
||||
const halSyncReady = Boolean(
|
||||
taskHalSummary.halSyncReady === true ||
|
||||
state.taskHalRuntimeReadiness?.halSyncReady === true,
|
||||
);
|
||||
const taskHalComparisonReady = Boolean(taskHalSummary.taskHalComparisonReady === true);
|
||||
const nativeTaskReady = taskRuntimeReady;
|
||||
const nativeHalSyncReady = halRuntimeReady && motionRuntimeReady && halSyncReady;
|
||||
const fullLinuxCncProgramExecutionReady = Boolean(
|
||||
kinematicsReady &&
|
||||
interpreterReady &&
|
||||
canonicalProgramReady &&
|
||||
machineFileStagingReady &&
|
||||
machineFileRemapReady &&
|
||||
plannerRuntimeReady &&
|
||||
nativeTaskReady &&
|
||||
nativeHalSyncReady &&
|
||||
taskHalComparisonReady
|
||||
);
|
||||
|
||||
const satisfied = [
|
||||
kinematicsReady ? "linuxcnc-kinematics-wasm" : null,
|
||||
@@ -44,6 +75,11 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
machineFileRemapReady ? "fiveaxis-remap-machine-file-run" : null,
|
||||
plannerRuntimeReady ? "linuxcnc-tp-queue-runtime-timing" : null,
|
||||
halSwitchkinsEvidenceReady ? "switchkins-hal-bridge-evidence" : null,
|
||||
taskRuntimeReady ? "linuxcnc-task-runtime" : null,
|
||||
motionRuntimeReady ? "linuxcnc-motion-runtime" : null,
|
||||
halRuntimeReady ? "linuxcnc-hal-runtime" : null,
|
||||
halSyncReady ? "task-motion-hal-sync" : null,
|
||||
taskHalComparisonReady ? "task-hal-cycle-artifact" : null,
|
||||
].filter(Boolean);
|
||||
|
||||
const missing = [];
|
||||
@@ -54,12 +90,16 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
if (!machineFileRemapReady) missing.push("machine-file backed five-axis remap run");
|
||||
if (!plannerRuntimeReady) missing.push("LinuxCNC trajectory planner queue timing runtime");
|
||||
if (!halSwitchkinsEvidenceReady) missing.push("switchkins HAL bridge evidence");
|
||||
if (!taskRuntimeReady) missing.push("LinuxCNC task runtime");
|
||||
if (!motionRuntimeReady) missing.push("LinuxCNC motion runtime");
|
||||
if (!halRuntimeReady) missing.push("LinuxCNC HAL runtime");
|
||||
if (!halSyncReady) missing.push("task/motion/HAL synchronization");
|
||||
if (!taskHalComparisonReady) missing.push("task cycle and HAL servo cycle artifact");
|
||||
|
||||
const blockers = [
|
||||
"native LinuxCNC task/NML process is not ported",
|
||||
"native realtime HAL thread synchronization is not ported",
|
||||
"external user-M process and full tool DB process are not promoted",
|
||||
];
|
||||
const blockers = [];
|
||||
if (!nativeTaskReady) blockers.push("LinuxCNC task runtime is not promoted");
|
||||
if (!nativeHalSyncReady) blockers.push("realtime HAL synchronization is not promoted");
|
||||
blockers.push("external user-M process and full tool DB process are not promoted");
|
||||
if (!plannerRuntimeReady) {
|
||||
blockers.push("LinuxCNC trajectory planner queue is not promoted as browser runtime");
|
||||
}
|
||||
@@ -67,12 +107,16 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-full-linuxcnc-execution-boundary",
|
||||
profileId: state.machineProfile || adapter.profileId || "unknown",
|
||||
phase: machineFileRemapReady
|
||||
phase: fullLinuxCncProgramExecutionReady
|
||||
? "linuxcnc-task-motion-hal-simulation-runtime"
|
||||
: machineFileRemapReady
|
||||
? "partial-linuxcnc-remap-boundary"
|
||||
: canonicalProgramReady
|
||||
? "canonical-interpreter-boundary"
|
||||
: "blocked",
|
||||
semanticBoundary: machineFileRemapReady
|
||||
semanticBoundary: fullLinuxCncProgramExecutionReady
|
||||
? "linuxcnc_task_motion_hal_wasm_simulation_runtime"
|
||||
: machineFileRemapReady
|
||||
? "linuxcnc_machine_file_remap_ready_planner_task_hal_blocked"
|
||||
: canonicalProgramReady
|
||||
? "linuxcnc_interpreter_canonical_ready_planner_task_hal_blocked"
|
||||
@@ -85,10 +129,19 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
remapRuntimeReady: machineFileRemapReady,
|
||||
halSwitchkinsEvidenceReady,
|
||||
plannerRuntimeReady,
|
||||
nativeTaskReady: false,
|
||||
nativeHalSyncReady: false,
|
||||
fullLinuxCncProgramExecutionReady: false,
|
||||
promotionAllowed: false,
|
||||
taskRuntimeReady,
|
||||
motionRuntimeReady,
|
||||
halRuntimeReady,
|
||||
halSyncReady,
|
||||
taskHalComparisonReady,
|
||||
nativeTaskReady,
|
||||
nativeHalSyncReady,
|
||||
fullLinuxCncProgramExecutionReady,
|
||||
promotionAllowed: fullLinuxCncProgramExecutionReady,
|
||||
hardwareDrive: false,
|
||||
hostRealtimeKernel: false,
|
||||
externalUserMProcessReady: false,
|
||||
toolDbProcessReady: false,
|
||||
satisfied,
|
||||
missing,
|
||||
blockers,
|
||||
@@ -101,6 +154,11 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
machineFileFlags: MACHINE_FILE_FLAGS.filter((flag) => machineFileText.includes(flag)),
|
||||
machineFileExecutionReady: machineFileExecution?.summary?.machineFileExecutionReady === true,
|
||||
stagedFileCount: state.machineFileStaging?.fileCount || 0,
|
||||
taskHal: taskHalSummary,
|
||||
taskCycle: state.taskHalStatus?.ui?.taskCycle || 0,
|
||||
servoCycle: state.taskHalStatus?.ui?.servoCycle || 0,
|
||||
motionQueueDepth: state.taskHalStatus?.ui?.motionQueueDepth || 0,
|
||||
halChangedPinCount: state.taskHalStatus?.ui?.halChangedPinCount || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,6 +52,13 @@ export async function createMachineFileStagingPlan({
|
||||
kind: classifySourceRel(file.sourceRel),
|
||||
})),
|
||||
summary: summarizePlan(files),
|
||||
taskHalSession: createTaskHalSession({
|
||||
profileId: profile.id,
|
||||
wasmDir: plan.wasmDir,
|
||||
iniPath: plan.iniPath,
|
||||
programPath: plan.programPath,
|
||||
files,
|
||||
}),
|
||||
semanticBoundary: "linuxcnc_sim_config_file_staging_plan_only",
|
||||
};
|
||||
}
|
||||
@@ -86,6 +93,11 @@ export function selectMachineFileProgram(plan, save, sourceRel) {
|
||||
selectedProgramSourceRel: selectedFile.sourceRel,
|
||||
selectedProgramFilename: basename(selectedFile.sourceRel),
|
||||
selectedProgramBytes: selectedFile.bytes,
|
||||
taskHalSession: {
|
||||
...(plan.taskHalSession || {}),
|
||||
programPath: selectedFile.wasmPath || selectedFile.path,
|
||||
programSourceRel: selectedFile.sourceRel,
|
||||
},
|
||||
semanticBoundary: "linuxcnc_sim_config_file_staging_plan_with_selected_gcode_source",
|
||||
};
|
||||
}
|
||||
@@ -120,6 +132,16 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||
files: savedFiles,
|
||||
gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }),
|
||||
summary: summarizeSavedFiles(savedFiles),
|
||||
taskHalSession: {
|
||||
...(plan.taskHalSession || {}),
|
||||
files: savedFiles.map((file) => ({
|
||||
sourceRel: file.sourceRel,
|
||||
wasmPath: file.wasmPath,
|
||||
path: file.wasmPath,
|
||||
kind: file.kind,
|
||||
bytes: file.bytes,
|
||||
})),
|
||||
},
|
||||
semanticBoundary: "opfs_machine_file_text_staging_only",
|
||||
};
|
||||
}
|
||||
@@ -150,6 +172,20 @@ function summarizePlan(files) {
|
||||
};
|
||||
}
|
||||
|
||||
function createTaskHalSession({ profileId, wasmDir, iniPath, programPath, files }) {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-session-plan",
|
||||
semanticBoundary: "linuxcnc_machine_file_session_for_task_hal_wasm",
|
||||
profileId,
|
||||
wasmDir,
|
||||
iniPath,
|
||||
programPath,
|
||||
halFiles: files.filter((file) => classifySourceRel(file.sourceRel) === "hal").map((file) => file.wasmPath),
|
||||
remapFiles: files.filter((file) => classifySourceRel(file.sourceRel) === "remap").map((file) => file.wasmPath),
|
||||
toolTableFiles: files.filter((file) => classifySourceRel(file.sourceRel) === "toolTable").map((file) => file.wasmPath),
|
||||
};
|
||||
}
|
||||
|
||||
function addVendoredDemoSources(files, manifestText, wasmDir) {
|
||||
const bySourceRel = new Map(files.map((file) => [file.sourceRel, file]));
|
||||
for (const sourceRel of String(manifestText).split(/\r?\n/)) {
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
const DEFAULT_SDK_MODULE_URLS = [
|
||||
new URL("../../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
|
||||
new URL("../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
|
||||
];
|
||||
|
||||
const SEMANTIC_BOUNDARY = "linuxcnc_task_motion_hal_wasm_simulation_runtime";
|
||||
|
||||
export async function createLinuxCncTaskHalRuntime({
|
||||
sdkModuleUrl = null,
|
||||
moduleOptions = {},
|
||||
} = {}) {
|
||||
const errors = [];
|
||||
const candidateUrls = sdkModuleUrl ? [sdkModuleUrl] : DEFAULT_SDK_MODULE_URLS;
|
||||
for (const url of candidateUrls) {
|
||||
try {
|
||||
const { createLinuxCncTaskHalSdk } = await import(url);
|
||||
const sdk = await createLinuxCncTaskHalSdk(moduleOptions);
|
||||
return wrapTaskHalSdk(sdk, {
|
||||
sdkModuleUrl: url,
|
||||
executionContext: "direct",
|
||||
});
|
||||
} catch (error) {
|
||||
errors.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`LinuxCNC task/HAL runtime unavailable: ${errors.join(" | ")}`);
|
||||
}
|
||||
|
||||
export function wrapTaskHalSdk(sdk, {
|
||||
sdkModuleUrl = null,
|
||||
executionContext = "direct",
|
||||
workerUrl = null,
|
||||
} = {}) {
|
||||
if (!sdk || typeof sdk.readiness !== "function") {
|
||||
throw new Error("wrapTaskHalSdk requires a task/HAL SDK");
|
||||
}
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime",
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
executionContext,
|
||||
sdkModuleUrl,
|
||||
workerUrl,
|
||||
loaded: true,
|
||||
|
||||
readiness() {
|
||||
const readiness = sdk.readiness();
|
||||
const taskRuntimeReady = readiness.taskRuntimeReady === true;
|
||||
const motionRuntimeReady = readiness.motionRuntimeReady === true;
|
||||
const halRuntimeReady = readiness.halRuntimeReady === true;
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
|
||||
loaded: true,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
sdkSemanticBoundary: readiness.semanticBoundary,
|
||||
executionContext,
|
||||
workerUrl,
|
||||
taskRuntimeReady,
|
||||
motionRuntimeReady,
|
||||
halRuntimeReady,
|
||||
halSyncReady: taskRuntimeReady && motionRuntimeReady && halRuntimeReady,
|
||||
nativeTaskReady: taskRuntimeReady,
|
||||
nativeHalSyncReady: taskRuntimeReady && motionRuntimeReady && halRuntimeReady,
|
||||
hardwareDrive: false,
|
||||
hostRealtimeKernel: false,
|
||||
externalUserMProcessReady: false,
|
||||
};
|
||||
},
|
||||
|
||||
initSession(session = {}) {
|
||||
return sdk.initSession(session);
|
||||
},
|
||||
|
||||
stageFiles(files = []) {
|
||||
let staged = 0;
|
||||
for (const file of files) {
|
||||
const path = file.wasmPath || file.path;
|
||||
if (!path) continue;
|
||||
const rc = sdk.stageFile(path, file.text || "");
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_stage_file failed for ${path} rc=${rc}`);
|
||||
}
|
||||
staged += 1;
|
||||
}
|
||||
return staged;
|
||||
},
|
||||
|
||||
openProgram(path) {
|
||||
const rc = sdk.openProgram(path);
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_open_program failed for ${path} rc=${rc}`);
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
|
||||
sendCommand(command) {
|
||||
const rc = sdk.sendCommand(command);
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_send_command_json failed for ${command?.type || "unknown"} rc=${rc}`);
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
|
||||
runCycles(options = {}) {
|
||||
const rc = sdk.runCycles(options);
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_run_cycles failed rc=${rc}`);
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
|
||||
readStatus() {
|
||||
return normalizeTaskHalStatus(sdk.readStatus());
|
||||
},
|
||||
|
||||
readEvents() {
|
||||
return sdk.readEvents();
|
||||
},
|
||||
|
||||
resetSession() {
|
||||
return sdk.resetSession();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTaskHalSessionFromMachineFiles({ profile, plan, save, selectedProgramRel = null } = {}) {
|
||||
const files = save?.files || [];
|
||||
const iniFile = files.find((file) => file.kind === "ini")
|
||||
|| files.find((file) => file.sourceRel === profile?.iniPath)
|
||||
|| null;
|
||||
const selectedFile = selectedProgramRel
|
||||
? files.find((file) => file.sourceRel === selectedProgramRel)
|
||||
: null;
|
||||
const programFile = selectedFile
|
||||
|| files.find((file) => file.wasmPath === plan?.wasmProgramPath)
|
||||
|| files.find((file) => file.kind === "demo")
|
||||
|| null;
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-session",
|
||||
semanticBoundary: "linuxcnc_machine_files_for_task_hal_wasm_runtime",
|
||||
profileId: profile?.id || plan?.profileId || save?.profileId || "unknown",
|
||||
iniPath: iniFile?.wasmPath || plan?.wasmIniPath || plan?.iniPath || profile?.iniPath || null,
|
||||
iniText: iniFile?.text || "",
|
||||
programPath: programFile?.wasmPath || plan?.wasmProgramPath || null,
|
||||
programSourceRel: programFile?.sourceRel || selectedProgramRel || null,
|
||||
halFiles: files.filter((file) => file.kind === "hal").map(sessionFileDescriptor),
|
||||
toolTableFiles: files.filter((file) => file.kind === "toolTable").map(sessionFileDescriptor),
|
||||
remapFiles: files.filter((file) => file.kind === "remap").map(sessionFileDescriptor),
|
||||
files: files.map(sessionFileDescriptor),
|
||||
fileCount: files.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeTaskHalStatus(status = {}) {
|
||||
const motion = status.motionStatus?.motion || {};
|
||||
const axis = status.motionStatus?.axis || {};
|
||||
const halPins = status.halSnapshot?.pins || {};
|
||||
return {
|
||||
...status,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
summary: {
|
||||
taskRuntimeReady: status.taskRuntimeReady === true,
|
||||
motionRuntimeReady: status.motionStatus?.motionHalSyncReady === true || status.taskCommandsDriveMotionRuntime === true,
|
||||
halRuntimeReady: Boolean(status.halSnapshot?.halRuntimeReady ?? status.halSnapshot?.ready ?? true),
|
||||
halSyncReady: status.taskCommandsDriveMotionRuntime === true && Boolean(halPins["motion.program-line"]),
|
||||
taskHalComparisonReady: status.taskRuntimeReady === true && status.taskCommandsDriveMotionRuntime === true,
|
||||
switchkinsRemapHalSync: Boolean(halPins["motion.switchkins-type"]),
|
||||
nativeTaskReady: status.taskRuntimeReady === true,
|
||||
nativeHalSyncReady: status.taskCommandsDriveMotionRuntime === true && Boolean(halPins["motion.program-line"]),
|
||||
fullLinuxCncProgramExecutionReady: false,
|
||||
hardwareDrive: false,
|
||||
hostRealtimeKernel: false,
|
||||
},
|
||||
ui: {
|
||||
taskState: String(status.task?.state || "ESTOP").toLowerCase(),
|
||||
taskMode: String(status.task?.mode || "MANUAL").toLowerCase(),
|
||||
interpState: String(status.task?.interpState || "IDLE").toLowerCase(),
|
||||
execState: String(status.task?.execState || "DONE").toLowerCase(),
|
||||
taskCycle: Number(status.task?.taskCycle ?? status.taskCycle ?? 0),
|
||||
servoCycle: Number(status.motionStatus?.cycle ?? status.task?.servoCycle ?? 0),
|
||||
motionQueueDepth: Number(status.motionStatus?.queueDepth ?? motion.queueDepth ?? 0),
|
||||
halChangedPinCount: Array.isArray(status.halSnapshot?.changedPins)
|
||||
? status.halSnapshot.changedPins.length
|
||||
: Number(status.halSnapshot?.changedPinCount || 0),
|
||||
activeLine: Number(motion.programLine || halPins["motion.program-line"]?.value || 1),
|
||||
switchkinsType: Number(motion.switchkinsType ?? halPins["motion.switchkins-type"]?.value ?? 0),
|
||||
axisPose: {
|
||||
x: Number(axis.x ?? halPins["axis.0.pos-cmd"]?.value ?? 0),
|
||||
y: Number(axis.y ?? halPins["axis.1.pos-cmd"]?.value ?? 0),
|
||||
z: Number(axis.z ?? halPins["axis.2.pos-cmd"]?.value ?? 0),
|
||||
a: Number(axis.a ?? halPins["axis.3.pos-cmd"]?.value ?? 0),
|
||||
b: Number(axis.b ?? halPins["axis.4.pos-cmd"]?.value ?? 0),
|
||||
c: Number(axis.c ?? halPins["axis.5.pos-cmd"]?.value ?? 0),
|
||||
},
|
||||
currentVelocity: Number(motion.currentVel || motion.currentVelocity || 0) * 60,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sessionFileDescriptor(file) {
|
||||
return {
|
||||
sourceRel: file.sourceRel,
|
||||
wasmPath: file.wasmPath || file.path,
|
||||
path: file.wasmPath || file.path,
|
||||
kind: file.kind,
|
||||
bytes: Number(file.bytes || String(file.text || "").length),
|
||||
text: file.text || "",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { wrapTaskHalSdk } from "./linuxcnc-task-hal-runtime.js";
|
||||
|
||||
export async function createLinuxCncTaskHalWorkerRuntime({
|
||||
workerUrl = new URL("./linuxcnc-task-hal-worker.js", import.meta.url).href,
|
||||
sdkModuleUrl = null,
|
||||
moduleOptions = {},
|
||||
} = {}) {
|
||||
if (typeof Worker !== "function") {
|
||||
throw new Error("Worker is not available");
|
||||
}
|
||||
const worker = new Worker(workerUrl, { type: "module" });
|
||||
const client = createWorkerClient(worker);
|
||||
await client.call("init", { sdkModuleUrl, moduleOptions });
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-task-hal-worker-runtime",
|
||||
semanticBoundary: "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
executionContext: "worker",
|
||||
workerUrl,
|
||||
loaded: true,
|
||||
readiness: () => client.call("readiness"),
|
||||
initSession: (session) => client.call("initSession", { session }),
|
||||
stageFiles: (files) => client.call("stageFiles", { files }),
|
||||
openProgram: (path) => client.call("openProgram", { path }),
|
||||
sendCommand: (command) => client.call("command", { command }),
|
||||
runCycles: (options) => client.call("runCycles", { options }),
|
||||
readStatus: () => client.call("readStatus"),
|
||||
readEvents: () => client.call("readEvents"),
|
||||
resetSession: () => client.call("reset"),
|
||||
terminate: () => worker.terminate(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDirectTaskHalRuntimeFromSdk(sdk) {
|
||||
return wrapTaskHalSdk(sdk, { executionContext: "direct" });
|
||||
}
|
||||
|
||||
function createWorkerClient(worker) {
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
|
||||
worker.addEventListener("message", (event) => {
|
||||
const { id, ok, result, error } = event.data || {};
|
||||
const request = pending.get(id);
|
||||
if (!request) return;
|
||||
pending.delete(id);
|
||||
if (ok) {
|
||||
request.resolve(result);
|
||||
} else {
|
||||
request.reject(new Error(error || "LinuxCNC task/HAL worker failed"));
|
||||
}
|
||||
});
|
||||
|
||||
worker.addEventListener("error", (event) => {
|
||||
for (const request of pending.values()) {
|
||||
request.reject(new Error(event.message || "LinuxCNC task/HAL worker error"));
|
||||
}
|
||||
pending.clear();
|
||||
});
|
||||
|
||||
return {
|
||||
call(type, payload = {}) {
|
||||
const id = nextId;
|
||||
nextId += 1;
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
worker.postMessage({ id, type, payload });
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createLinuxCncTaskHalRuntime } from "./linuxcnc-task-hal-runtime.js";
|
||||
|
||||
let runtime = null;
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
const { id, type, payload = {} } = event.data || {};
|
||||
try {
|
||||
const result = await handleMessage(type, payload);
|
||||
self.postMessage({ id, ok: true, result });
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
id,
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function handleMessage(type, payload) {
|
||||
switch (type) {
|
||||
case "init":
|
||||
runtime = await createLinuxCncTaskHalRuntime(payload);
|
||||
return runtime.readiness();
|
||||
case "readiness":
|
||||
assertRuntime();
|
||||
return runtime.readiness();
|
||||
case "stageFiles":
|
||||
assertRuntime();
|
||||
return runtime.stageFiles(payload.files || []);
|
||||
case "initSession":
|
||||
assertRuntime();
|
||||
return runtime.initSession(payload.session || {});
|
||||
case "openProgram":
|
||||
assertRuntime();
|
||||
return runtime.openProgram(payload.path);
|
||||
case "command":
|
||||
assertRuntime();
|
||||
return runtime.sendCommand(payload.command);
|
||||
case "runCycles":
|
||||
assertRuntime();
|
||||
return runtime.runCycles(payload.options || {});
|
||||
case "readStatus":
|
||||
assertRuntime();
|
||||
return runtime.readStatus();
|
||||
case "readEvents":
|
||||
assertRuntime();
|
||||
return runtime.readEvents();
|
||||
case "reset":
|
||||
assertRuntime();
|
||||
return runtime.resetSession();
|
||||
default:
|
||||
throw new Error(`Unknown task/HAL worker message: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertRuntime() {
|
||||
if (!runtime) {
|
||||
throw new Error("LinuxCNC task/HAL worker runtime is not initialized");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -343,6 +343,8 @@ function renderInfoTabs(element, state) {
|
||||
<dt>INI joints:</dt><dd data-linuxcnc-ini="joints">${state.iniConfigReadiness.jointCount ?? 0} joints / ${state.iniConfigReadiness.axisCount ?? 0} axes</dd>
|
||||
<dt>Machine files:</dt><dd data-machine-file-staging="status">${formatMachineFileStaging(state.machineFileStaging)}</dd>
|
||||
<dt>LinuxCNC G-code:</dt><dd data-linuxcnc-gcode-source="selected">${state.machineFileStaging.selectedGcodeSourceRel || state.programSourceRel || "-"}</dd>
|
||||
<dt>Task/HAL:</dt><dd data-task-hal-runtime="readiness">${formatTaskHalReadiness(state)}</dd>
|
||||
<dt>Task/HAL cycles:</dt><dd data-task-hal-runtime="cycles">${formatTaskHalCycles(state)}</dd>
|
||||
<dt>Full boundary:</dt><dd data-full-execution-boundary="status">${formatFullExecutionBoundary(state.fullExecutionBoundary)}</dd>
|
||||
<dt>Planner/task:</dt><dd data-full-execution-boundary="blockers">${formatFullExecutionBlockers(state.fullExecutionBoundary)}</dd>
|
||||
<dt>Boundary evidence:</dt><dd data-full-execution-boundary="evidence">${formatFullExecutionEvidence(state.fullExecutionBoundary)}</dd>
|
||||
@@ -434,6 +436,24 @@ function formatMachineFileExecution(machineFileExecution) {
|
||||
return `${summary.machineFileExecutionReady ? "ready" : "ran"} ${summary.motionEventCount || 0} motion ${machineFileExecution.machineFilePlan.profileId}`;
|
||||
}
|
||||
|
||||
function formatTaskHalReadiness(state) {
|
||||
const readiness = state.taskHalRuntimeReadiness;
|
||||
if (!readiness?.loaded && !state.taskHalStatus) return "pending";
|
||||
return [
|
||||
readiness?.taskRuntimeReady ? "task" : "task pending",
|
||||
readiness?.motionRuntimeReady ? "motion" : "motion pending",
|
||||
readiness?.halRuntimeReady ? "hal" : "hal pending",
|
||||
state.taskHalStatus?.summary?.halSyncReady ? "sync" : "sync pending",
|
||||
state.taskHalFallbackReason ? `fallback ${state.taskHalFallbackReason}` : null,
|
||||
].filter(Boolean).join(" / ");
|
||||
}
|
||||
|
||||
function formatTaskHalCycles(state) {
|
||||
const status = state.taskHalStatus;
|
||||
if (!status?.ui) return "pending";
|
||||
return `task ${status.ui.taskCycle} / servo ${status.ui.servoCycle} / queue ${status.ui.motionQueueDepth} / HAL changed ${status.ui.halChangedPinCount}`;
|
||||
}
|
||||
|
||||
function formatFullExecutionBoundary(boundary) {
|
||||
if (!boundary) return "pending";
|
||||
const remap = boundary.machineFileBackedRemapReady ? "remap ready" : "remap pending";
|
||||
|
||||
Reference in New Issue
Block a user