接入 task HAL Web 仿真运行时

This commit is contained in:
2026-06-22 06:11:55 +08:00
parent 3771b9eafe
commit bd11a5f8d6
42 changed files with 5574 additions and 50 deletions

View File

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

View File

@@ -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/)) {

View File

@@ -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 || "",
};
}

View File

@@ -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 });
});
},
};
}

View File

@@ -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");
}
}