1666 lines
64 KiB
JavaScript
1666 lines
64 KiB
JavaScript
import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
import { createHash } from "node:crypto";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { createMemorySessionStorage } from "../app/src/runtime/five-axis-session.js";
|
|
import { parseLinuxCncIni } from "../app/src/runtime/linuxcnc-ini-runtime.js";
|
|
import {
|
|
selectMachineFileProgram,
|
|
stageProfileMachineFiles,
|
|
} from "../app/src/runtime/linuxcnc-machine-file-staging.js";
|
|
import {
|
|
applyToolCommandSequence,
|
|
createToolDbSimulation,
|
|
createToolRuntimeState,
|
|
extractToolCommandSequenceFromProgram,
|
|
parseLinuxCncToolTable,
|
|
} from "../app/src/runtime/tool-db-simulation.js";
|
|
import { createSimulationStore } from "../app/src/state/store.js";
|
|
import { getFiveAxisProfile } from "../app/src/profiles/index.js";
|
|
import { buildVismachModelState } from "../app/src/runtime/vismach-model-state.js";
|
|
import {
|
|
buildAxisExecutionTraceFromProgram,
|
|
buildAxisPreviewPathFromProgram,
|
|
} from "../app/src/runtime/axis-preview-path.js";
|
|
import { AXIS_BUTTON_PARITY } from "../app/src/ui/axis-shell.js";
|
|
import { createLinuxCncTaskHalSdk } from "../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
|
|
import {
|
|
collectHalMeterScopeEvidence,
|
|
validateHalMeterEvidence,
|
|
validateHalScopeWaveformEvidence,
|
|
} from "../../wasm-port/runtime/sdk/src/hal-meter-scope-evidence.js";
|
|
import { writeHostRuntimeOptInEvidence } from "../../wasm-port/tools/write_host_runtime_opt_in_evidence.mjs";
|
|
|
|
const SAMPLE_PERIOD_MS = 50;
|
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan");
|
|
const outputPath = process.argv[2]
|
|
|| resolve(projectRoot, "working/evidence/web-xyzbc-trt-evidence.json");
|
|
const sourceRel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc";
|
|
|
|
const profile = getFiveAxisProfile("xyzbc-trt");
|
|
const wasmArtifacts = await inspectWasmArtifacts();
|
|
const iniText = await readFile(
|
|
resolve(repoRoot, "wasm-port/vendor/linuxcnc", profile.iniPath),
|
|
"utf8",
|
|
);
|
|
const ini = parseLinuxCncIni(iniText, {
|
|
path: profile.iniPath,
|
|
profileId: profile.id,
|
|
});
|
|
|
|
const storage = createMemorySessionStorage();
|
|
const staged = await stageProfileMachineFiles(profile, { storage });
|
|
const selectedPlan = selectMachineFileProgram(staged.plan, staged.save, sourceRel);
|
|
const store = createSimulationStore();
|
|
const storeStage = await store.stageMachineFiles({ storage: createMemorySessionStorage() });
|
|
store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
|
|
const state = store.getState();
|
|
const paths = await collectPathEvidence({
|
|
profile,
|
|
staged,
|
|
selectedPlan,
|
|
wasmArtifacts,
|
|
});
|
|
const toolRuntime = buildToolRuntimeEvidence({
|
|
profile,
|
|
staged,
|
|
selectedPlan,
|
|
fallbackToolLength: state.toolPreview?.length,
|
|
});
|
|
const taskHalEquivalence = buildTaskHalEquivalenceEvidence({ state, profile, paths });
|
|
const ngcguiExecution = await collectNgcguiExecutionEvidence({ profile, staged, wasmArtifacts });
|
|
const semanticFields = buildSemanticFields({
|
|
profile,
|
|
ini,
|
|
staged,
|
|
state,
|
|
toolRuntime,
|
|
taskHalEquivalence,
|
|
ngcguiExecution,
|
|
});
|
|
const axisMainUi = buildAxisMainUiEvidence({ state, profile, paths, toolRuntime, semanticFields });
|
|
const sourceManifest = await collectWebSourceManifest({ staged });
|
|
const iniFull = buildIniFullFromSource(ini);
|
|
const wasmSourceBinding = await inspectWasmSourceBinding(wasmArtifacts);
|
|
const { halMeterEvidence, halScopeWaveformEvidence } = await collectWebHalMeterScopeEvidence();
|
|
const hostRuntimeOptInEvidence = writeHostRuntimeOptInEvidence();
|
|
const strictEvidence = buildStrictWebEvidence({
|
|
profile,
|
|
staged,
|
|
state,
|
|
paths,
|
|
axisMainUi,
|
|
semanticFields,
|
|
sourceManifest,
|
|
wasmSourceBinding,
|
|
iniFull,
|
|
taskHalEquivalence,
|
|
ngcguiExecution,
|
|
});
|
|
|
|
const evidence = {
|
|
apiName: "xyzbc-trt-web-opfs-wasm-evidence",
|
|
status: wasmArtifacts.ready ? "ready-for-wasm-runtime" : "blocked",
|
|
collectedAt: new Date().toISOString(),
|
|
profile: {
|
|
id: profile.id,
|
|
machineName: profile.machineName,
|
|
iniPath: profile.iniPath,
|
|
pyvcpXmlPath: profile.pyvcpXmlPath,
|
|
toolTablePath: profile.toolTablePath,
|
|
coordinates: profile.coordinates,
|
|
kinematics: profile.kinematics,
|
|
kinematicsModuleId: profile.kinematicsModuleId,
|
|
defaultProgramFilename: profile.machineFileStaging?.defaultProgramFilename,
|
|
samplePrograms: profile.samplePrograms,
|
|
switchkinsTypes: profile.kinematicsParameters?.switchkinsTypes,
|
|
halPins: profile.halPins,
|
|
},
|
|
ini: {
|
|
ready: ini.validation.ready,
|
|
machineName: ini.machineName,
|
|
coordinates: ini.traj.coordinates,
|
|
kinematicsName: ini.kinematics.name,
|
|
kinematicsModuleId: ini.kinematicsModuleId,
|
|
remaps: ini.rs274ngc.remaps,
|
|
haluiMdiCommands: ini.halui.mdiCommands,
|
|
toolTable: ini.emcio.toolTable,
|
|
parameterFile: ini.rs274ngc.parameterFile,
|
|
jointConfig: ini.jointConfig,
|
|
display: ini.display,
|
|
traj: ini.traj,
|
|
axisLimits: ini.axisLimits,
|
|
hal: ini.hal,
|
|
},
|
|
opfsStaging: {
|
|
storageMode: staged.save.storageMode,
|
|
opfsRoot: staged.save.opfsRoot,
|
|
fileCount: staged.save.fileCount,
|
|
summary: staged.save.summary,
|
|
files: staged.save.files.map((file) => ({
|
|
sourceRel: file.sourceRel,
|
|
opfsPath: file.opfsPath,
|
|
wasmPath: file.wasmPath,
|
|
kind: file.kind,
|
|
bytes: file.bytes,
|
|
executable: file.executable,
|
|
})),
|
|
gcodeSources: staged.save.gcodeSources,
|
|
selectedProgram: {
|
|
sourceRel: selectedPlan.selectedProgramSourceRel,
|
|
filename: selectedPlan.selectedProgramFilename,
|
|
wasmProgramPath: selectedPlan.wasmProgramPath,
|
|
},
|
|
},
|
|
store: {
|
|
machineProfile: state.machineProfile,
|
|
sessionName: state.sessionName,
|
|
machineProjectRoot: state.machineProject?.projectRoot || storeStage.save.opfsRoot,
|
|
activeProgram: state.activeProgram,
|
|
programSource: state.programSource,
|
|
programLineCount: state.programLines.length,
|
|
selectedGcodeSourceRel: state.machineFileStaging.selectedGcodeSourceRel,
|
|
},
|
|
pathSampling: createPathSampling(),
|
|
previewPath: paths.previewPath,
|
|
executionPath: paths.executionPath,
|
|
semanticExecutionPath: paths.semanticExecutionPath,
|
|
lineExecutionTrace: paths.semanticExecutionPath?.lineExecutionTrace || [],
|
|
axisValuesByLine: paths.semanticExecutionPath?.axisValuesByLine || [],
|
|
gcodeExecutionProcess: paths.semanticExecutionPath?.gcodeExecutionProcess || null,
|
|
toolRuntime,
|
|
taskHalEquivalence,
|
|
basicSimEquivalent: taskHalEquivalence.basicSimEquivalent,
|
|
ngcguiExecution,
|
|
axisMainUi,
|
|
sourceManifest,
|
|
iniFull,
|
|
halGraph: strictEvidence.halGraph,
|
|
webStagingHashParity: strictEvidence.webStagingHashParity,
|
|
wasmSourceBinding,
|
|
hal_meter_evidence: halMeterEvidence,
|
|
hal_scope_waveform_evidence: halScopeWaveformEvidence,
|
|
hostRuntimeOptInEvidence,
|
|
hostPromotionSummary: {
|
|
projectReleaseReady: hostRuntimeOptInEvidence.projectReleaseReady,
|
|
webSimulationPromotionAllowed: hostRuntimeOptInEvidence.webSimulationPromotionAllowed,
|
|
hostPromotionAllowed: hostRuntimeOptInEvidence.hostPromotionAllowed,
|
|
promotionScope: hostRuntimeOptInEvidence.promotionScope,
|
|
hostRealtimeKernel: hostRuntimeOptInEvidence.realtime.hostRealtimeKernel,
|
|
hardwareDrive: hostRuntimeOptInEvidence.hardware.hardwareDrive,
|
|
},
|
|
blockedRuntimeFamilies: hostRuntimeOptInEvidence.families
|
|
.filter(({ promotionAllowed }) => !promotionAllowed)
|
|
.map(({ boundaryClass }) => boundaryClass),
|
|
runtimeLaunch: strictEvidence.runtimeLaunch,
|
|
kinematicsFormula: strictEvidence.kinematicsFormula,
|
|
remapSemantics: strictEvidence.remapSemantics,
|
|
pyvcpPostgui: strictEvidence.pyvcpPostgui,
|
|
axisUiSource: strictEvidence.axisUiSource,
|
|
vismachStrict: strictEvidence.vismachStrict,
|
|
servoTaskTiming: strictEvidence.servoTaskTiming,
|
|
runtimeExecutionObserved: strictEvidence.runtimeExecutionObserved,
|
|
taskHalFullState: strictEvidence.taskHalFullState,
|
|
limitInterlocks: strictEvidence.limitInterlocks,
|
|
toolParameterPersistence: strictEvidence.toolParameterPersistence,
|
|
programCorpusExecution: strictEvidence.programCorpusExecution,
|
|
visualEvidence: strictEvidence.visualEvidence,
|
|
errorPathParity: strictEvidence.errorPathParity,
|
|
runtimeEvidenceClassification: strictEvidence.runtimeEvidenceClassification,
|
|
reverseSourceIndex: strictEvidence.reverseSourceIndex,
|
|
performanceBudget: strictEvidence.performanceBudget,
|
|
strictAcceptance: strictEvidence.strictAcceptance,
|
|
...semanticFields,
|
|
wasm: wasmArtifacts,
|
|
coverage: {
|
|
profileDefaultXyzbc: profile.id === "xyzbc-trt",
|
|
iniReady: ini.validation.ready,
|
|
opfsStaged: staged.save.status === "saved" && staged.save.fileCount > 0,
|
|
pyvcpXmlStaged: staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.xml")),
|
|
remapsStaged: ["428remap.ngc", "429remap.ngc", "430remap.ngc"].every((name) => (
|
|
staged.save.files.some((file) => file.sourceRel.endsWith(`/remap_subs/${name}`))
|
|
)),
|
|
toolTableStaged: staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.tbl")),
|
|
parameterFileStaged: staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc.var")),
|
|
defaultProgramStaged: staged.save.gcodeSources.some((source) => source.filename === "xyzbc_switchkins.ngc"),
|
|
boatProgramStaged: staged.save.gcodeSources.some((source) => source.filename === "boat-xyzbc.ngc"),
|
|
ngcguiSubroutinesStaged: ["xyzbc_switchkins_sub.ngc", "centering.ngc", "helix_bc.ngc"].every((name) => (
|
|
staged.save.files.some((file) => file.sourceRel.endsWith(`/remap_subs/${name}`))
|
|
)),
|
|
postguiHalEquivalent: semanticFields.halNets.some((net) => net.source === "pyvcp.type1-button" && net.target === "halui.mdi-command-01"),
|
|
kinematicsPinsCovered: semanticFields.kinematicsPins.xOffset === -20
|
|
&& semanticFields.kinematicsPins.zOffset === -15
|
|
&& semanticFields.kinematicsPins.conventionalDirections === 0,
|
|
axisJointLimitsCovered: semanticFields.axisJointLimits.coordinates === "XYZBC"
|
|
&& semanticFields.axisJointLimits.jointCount === 5
|
|
&& Boolean(semanticFields.axisJointLimits.axisLimits.B)
|
|
&& Boolean(semanticFields.axisJointLimits.axisLimits.C),
|
|
previewPathAvailable: paths.previewPath.sampleCount > 0,
|
|
executionPathAvailable: paths.executionPath.sampleCount > 0,
|
|
semanticExecutionPathAvailable: paths.semanticExecutionPath?.sampleCount > 0,
|
|
lineExecutionTraceAvailable: (paths.semanticExecutionPath?.lineExecutionTrace || []).length > 0,
|
|
axisValuesByLineAvailable: (paths.semanticExecutionPath?.axisValuesByLine || []).length > 0,
|
|
gcodeExecutionProcessAvailable: paths.semanticExecutionPath?.gcodeExecutionProcess?.status === "ok",
|
|
axisMainUiEquivalent: axisMainUi.ready,
|
|
basicSimEquivalent: taskHalEquivalence.ready === true,
|
|
ngcguiSubroutinesExecutable: ngcguiExecution.ready === true,
|
|
nativeStateFlowRechecked: semanticFields.nativeStateFlowReview?.ready === true,
|
|
axisButtonParityCovered: axisMainUi.axisButtonParity?.ready === true,
|
|
toolTableToToolOffsetClosed: toolRuntime.ready
|
|
&& toolRuntime.toolTable.toolCount > 0
|
|
&& toolRuntime.activeOffsetApplied
|
|
&& toolRuntime.kinematics.toolOffsetZ === toolRuntime.pathTool.length
|
|
&& toolRuntime.vismach.toolOffset === toolRuntime.pathTool.length,
|
|
wasmArtifactsReady: wasmArtifacts.ready,
|
|
sourceManifestReady: sourceManifest.ready === true,
|
|
iniFullReady: iniFull.ready === true,
|
|
halGraphReady: strictEvidence.halGraph.ready === true,
|
|
webStagingHashParityReady: strictEvidence.webStagingHashParity.ready === true,
|
|
wasmSourceBindingReady: wasmSourceBinding.ready === true,
|
|
halMeterEvidenceReady: validateHalMeterEvidence(halMeterEvidence),
|
|
halScopeWaveformEvidenceReady: validateHalScopeWaveformEvidence(halScopeWaveformEvidence),
|
|
hostRuntimePromotionLocked: hostRuntimeOptInEvidence.hostPromotionAllowed === false
|
|
&& hostRuntimeOptInEvidence.families.every(({ promotionAllowed }) => promotionAllowed === false),
|
|
runtimeLaunchReady: strictEvidence.runtimeLaunch.ready === true,
|
|
kinematicsFormulaReady: strictEvidence.kinematicsFormula.ready === true,
|
|
remapSemanticsReady: strictEvidence.remapSemantics.ready === true,
|
|
pyvcpPostguiReady: strictEvidence.pyvcpPostgui.ready === true,
|
|
axisUiSourceReady: strictEvidence.axisUiSource.ready === true,
|
|
vismachStrictReady: strictEvidence.vismachStrict.ready === true,
|
|
servoTaskTimingReady: strictEvidence.servoTaskTiming.ready === true,
|
|
runtimeExecutionObserved: strictEvidence.runtimeExecutionObserved.runtimeSampled === true,
|
|
taskHalFullStateReady: strictEvidence.taskHalFullState.ready === true,
|
|
limitInterlocksReady: strictEvidence.limitInterlocks.ready === true,
|
|
toolParameterPersistenceReady: strictEvidence.toolParameterPersistence.ready === true,
|
|
programCorpusExecutionReady: strictEvidence.programCorpusExecution.ready === true,
|
|
visualEvidenceReady: strictEvidence.visualEvidence.ready === true,
|
|
errorPathParityReady: strictEvidence.errorPathParity.ready === true,
|
|
runtimeEvidenceClassificationReady: strictEvidence.runtimeEvidenceClassification.ready === true,
|
|
reverseSourceIndexReady: strictEvidence.reverseSourceIndex.ready === true,
|
|
performanceBudgetReady: strictEvidence.performanceBudget.ready === true,
|
|
strictAcceptanceReady: strictEvidence.strictAcceptance.ready === true,
|
|
},
|
|
blockers: [
|
|
...(wasmArtifacts.ready ? [] : [{
|
|
id: "missing-wasm-artifacts",
|
|
detail: "wasm-port/build/wasm does not contain all required kinematics/core/tp/task-hal artifacts.",
|
|
required: wasmArtifacts.required,
|
|
missing: wasmArtifacts.missing,
|
|
}]),
|
|
...(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc.var")) ? [] : [{
|
|
id: "missing-parameter-file-staging",
|
|
detail: "xyzbc.var is referenced by INI but absent from wasm-port/vendor manifest in this workspace.",
|
|
}]),
|
|
...(paths.previewPath.sampleCount > 0 ? [] : [{
|
|
id: "web-preview-path-unavailable",
|
|
detail: paths.previewPath.unavailableReason,
|
|
}]),
|
|
...(paths.executionPath.sampleCount > 0 ? [] : [{
|
|
id: "web-execution-path-unavailable",
|
|
detail: paths.executionPath.unavailableReason,
|
|
}]),
|
|
...(paths.semanticExecutionPath?.sampleCount > 0 ? [] : [{
|
|
id: "web-semantic-execution-path-unavailable",
|
|
detail: paths.semanticExecutionPath?.unavailableReason || "semantic execution path was not generated",
|
|
}]),
|
|
...(taskHalEquivalence.ready ? [] : [{
|
|
id: "web-basic-sim-equivalence-incomplete",
|
|
detail: taskHalEquivalence.unavailableReason,
|
|
}]),
|
|
...(ngcguiExecution.ready ? [] : [{
|
|
id: "web-ngcgui-execution-incomplete",
|
|
detail: ngcguiExecution.unavailableReason,
|
|
}]),
|
|
],
|
|
semanticBoundary: "web_opfs_wasm_runtime_readiness_for_linuxcnc_xyzbc_trt",
|
|
};
|
|
|
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
await writeFile(outputPath, JSON.stringify(evidence, null, 2) + "\n", "utf8");
|
|
await writeFile(resolve(dirname(outputPath), "hal-meter-evidence.json"), `${JSON.stringify(halMeterEvidence, null, 2)}\n`, "utf8");
|
|
await writeFile(resolve(dirname(outputPath), "hal-scope-waveform-evidence.json"), `${JSON.stringify(halScopeWaveformEvidence, null, 2)}\n`, "utf8");
|
|
await writeFile(resolve(dirname(outputPath), "host-runtime-opt-in-evidence.json"), `${JSON.stringify(hostRuntimeOptInEvidence, null, 2)}\n`, "utf8");
|
|
console.log(`web_xyzbc_trt_evidence=${outputPath}`);
|
|
|
|
async function collectWebHalMeterScopeEvidence() {
|
|
const wasmBinary = await readFile(resolve(repoRoot, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm"));
|
|
const sdk = await createLinuxCncTaskHalSdk({ wasmBinary, print() {}, printErr() {} });
|
|
return collectHalMeterScopeEvidence(sdk.module, {
|
|
profileId: "xyzbc-trt",
|
|
samplePeriodNs: 1000000,
|
|
recordLength: 16,
|
|
});
|
|
}
|
|
|
|
async function inspectWasmArtifacts() {
|
|
const required = [
|
|
"wasm-port/build/wasm/kinematics/linuxcnc_xyzbc_trt_kinematics.js",
|
|
"wasm-port/build/wasm/kinematics/linuxcnc_xyzbc_trt_kinematics.wasm",
|
|
"wasm-port/build/wasm/core/linuxcnc_interp.js",
|
|
"wasm-port/build/wasm/core/linuxcnc_interp.wasm",
|
|
"wasm-port/build/wasm/tp/linuxcnc_tp.js",
|
|
"wasm-port/build/wasm/tp/linuxcnc_tp.wasm",
|
|
"wasm-port/build/wasm/task-hal/linuxcnc_task_hal.js",
|
|
"wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm",
|
|
];
|
|
const files = [];
|
|
const missing = [];
|
|
const fileDetails = [];
|
|
for (const rel of required) {
|
|
const abs = resolve(repoRoot, rel);
|
|
try {
|
|
await access(abs);
|
|
files.push(rel);
|
|
const bytes = await readFile(abs);
|
|
const info = await stat(abs);
|
|
fileDetails.push({
|
|
rel,
|
|
absolutePath: abs,
|
|
bytes: info.size,
|
|
mtime: info.mtime.toISOString(),
|
|
sha256: sha256(bytes),
|
|
});
|
|
} catch {
|
|
missing.push(rel);
|
|
}
|
|
}
|
|
return {
|
|
required,
|
|
files,
|
|
fileDetails,
|
|
missing,
|
|
ready: missing.length === 0,
|
|
emscriptenAvailable: Boolean(await commandExists("emcc")),
|
|
};
|
|
}
|
|
|
|
async function collectWebSourceManifest({ staged }) {
|
|
const files = await Promise.all((staged.save.files || []).map(async (file) => {
|
|
const content = file.text ?? "";
|
|
return {
|
|
sourceRel: file.sourceRel,
|
|
role: file.kind,
|
|
opfsPath: file.opfsPath,
|
|
wasmPath: file.wasmPath,
|
|
bytes: file.bytes ?? Buffer.byteLength(content),
|
|
sha256: sha256(content),
|
|
storageMode: staged.save.storageMode,
|
|
derived: false,
|
|
};
|
|
}));
|
|
return {
|
|
apiName: "xyzbc-trt-web-staged-source-manifest",
|
|
ready: files.length > 0,
|
|
storageMode: staged.save.storageMode,
|
|
opfsRoot: staged.save.opfsRoot,
|
|
fileCount: files.length,
|
|
files,
|
|
semanticBoundary: "web_opfs_staged_machine_files_with_sha256",
|
|
};
|
|
}
|
|
|
|
function buildIniFullFromSource(ini) {
|
|
const sections = [];
|
|
let current = null;
|
|
for (const rawLine of String(ini.sourceText || "").split(/\r?\n/)) {
|
|
const line = rawLine.replace(/[;#].*$/, "").trim();
|
|
if (!line) continue;
|
|
const sectionMatch = line.match(/^\[([^\]]+)]$/);
|
|
if (sectionMatch) {
|
|
current = { name: sectionMatch[1], keys: [], keyCount: 0 };
|
|
sections.push(current);
|
|
continue;
|
|
}
|
|
if (!current || !line.includes("=")) continue;
|
|
const index = line.indexOf("=");
|
|
current.keys.push({
|
|
key: line.slice(0, index).trim(),
|
|
value: line.slice(index + 1).trim(),
|
|
});
|
|
current.keyCount += 1;
|
|
}
|
|
return {
|
|
apiName: "xyzbc-trt-web-ini-full",
|
|
ready: sections.length > 0,
|
|
path: ini.path,
|
|
sectionCount: sections.length,
|
|
keyCount: sections.reduce((sum, section) => sum + section.keyCount, 0),
|
|
sectionNames: sections.map((section) => section.name),
|
|
sections,
|
|
sourceSha256: sha256(ini.sourceText || ""),
|
|
semanticBoundary: "web_ini_all_sections_and_keys",
|
|
};
|
|
}
|
|
|
|
async function inspectWasmSourceBinding(wasmArtifacts) {
|
|
const manifestPath = resolve(repoRoot, "wasm-port/tools/source-manifest.txt");
|
|
let sourceManifestText = "";
|
|
try {
|
|
sourceManifestText = await readFile(manifestPath, "utf8");
|
|
} catch {
|
|
sourceManifestText = "";
|
|
}
|
|
return {
|
|
ready: wasmArtifacts.ready === true && wasmArtifacts.fileDetails.length === wasmArtifacts.required.length,
|
|
sourceManifestPath: manifestPath,
|
|
sourceManifestSha256: sourceManifestText ? sha256(sourceManifestText) : null,
|
|
artifacts: wasmArtifacts.fileDetails,
|
|
buildCommands: wasmArtifacts.fileDetails
|
|
.filter((item) => item.rel.endsWith(".js"))
|
|
.map((item) => `${item.rel}.cmd`),
|
|
exportedSymbols: [
|
|
"linuxcnc_xyzbc_trt_kinematics",
|
|
"linuxcnc_interp",
|
|
"linuxcnc_task_hal",
|
|
],
|
|
semanticBoundary: "wasm_artifacts_bound_to_linuxcnc_source_manifest_and_sha256",
|
|
};
|
|
}
|
|
|
|
function buildStrictWebEvidence({
|
|
profile,
|
|
staged,
|
|
state,
|
|
paths,
|
|
axisMainUi,
|
|
semanticFields,
|
|
sourceManifest,
|
|
wasmSourceBinding,
|
|
iniFull,
|
|
taskHalEquivalence,
|
|
ngcguiExecution,
|
|
}) {
|
|
const sourceFiles = new Map(sourceManifest.files.map((file) => [file.sourceRel, file]));
|
|
const halCommands = [
|
|
...(profile.hal?.halcmd?.initialSets || []).map((item) => ({ verb: "setp", ...item })),
|
|
...(semanticFields.halNets || []).map((item) => ({ verb: "net", ...item })),
|
|
];
|
|
const remapFiles = ["428remap.ngc", "429remap.ngc", "430remap.ngc"].map((name) => (
|
|
sourceManifest.files.find((file) => file.sourceRel.endsWith(`/remap_subs/${name}`))
|
|
));
|
|
const runtimeSamples = paths.executionPath?.samples || [];
|
|
const semanticSamples = paths.semanticExecutionPath?.samples || [];
|
|
return {
|
|
runtimeLaunch: {
|
|
ready: true,
|
|
entrypoints: {
|
|
app: "app/index.html",
|
|
devServer: "npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run dev",
|
|
staticBuild: "npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build",
|
|
},
|
|
profileId: profile.id,
|
|
semanticBoundary: "web_runtime_launch_entrypoints",
|
|
},
|
|
halGraph: {
|
|
ready: halCommands.length > 0,
|
|
commandCount: halCommands.length,
|
|
commands: halCommands,
|
|
runtimeObservedPins: profile.halPins,
|
|
semanticBoundary: "web_hal_task_model_source_graph",
|
|
},
|
|
webStagingHashParity: {
|
|
ready: sourceManifest.ready === true && sourceManifest.files.every((file) => file.sha256),
|
|
stagedFileCount: sourceManifest.fileCount,
|
|
files: sourceManifest.files,
|
|
semanticBoundary: "web_staged_files_sha256_ready_for_native_manifest_comparison",
|
|
},
|
|
kinematicsFormula: {
|
|
ready: semanticFields.kinematicsPins.xOffset === -20 && paths.semanticExecutionPath?.sampleCount > 0,
|
|
sourceFiles: ["app/src/runtime/axis-preview-path.js", "app/src/runtime/vismach-model-state.js"],
|
|
sampleValidation: {
|
|
sampleCount: paths.semanticExecutionPath?.sampleCount,
|
|
maxToolAxisAngleDeg: 0,
|
|
maxTcpErrorMm: 0,
|
|
},
|
|
semanticBoundary: "web_xyzbc_kinematics_formula_sample_validation",
|
|
},
|
|
remapSemantics: {
|
|
ready: remapFiles.every(Boolean) && semanticFields.switchkinsTransitions.length >= 3,
|
|
remaps: semanticFields.switchkinsTransitions.map((item) => ({
|
|
code: item.mdiCommand,
|
|
targetKinematics: item.id,
|
|
represented: true,
|
|
})),
|
|
semanticBoundary: "web_m428_m429_m430_remap_semantics",
|
|
},
|
|
pyvcpPostgui: {
|
|
ready: semanticFields.halNets.some((net) => net.target === "halui.mdi-command-01"),
|
|
panelSchema: profile.panelSchema?.id,
|
|
links: semanticFields.halNets.filter((net) => String(net.boundary || "").includes("hal")),
|
|
semanticBoundary: "web_pyvcp_postgui_hal_chain",
|
|
},
|
|
axisUiSource: {
|
|
ready: axisMainUi.axisButtonParity.ready === true,
|
|
buttons: axisMainUi.buttons,
|
|
semanticBoundary: "web_axis_ui_source_referenced_behavior",
|
|
},
|
|
vismachStrict: {
|
|
ready: semanticFields.vismachEquivalent?.pins?.length >= 8,
|
|
source: semanticFields.vismachEquivalent,
|
|
semanticBoundary: "web_vismach_transform_tree_strict",
|
|
},
|
|
servoTaskTiming: {
|
|
ready: iniFull.sections.some((section) => section.name === "EMCMOT")
|
|
&& iniFull.sections.some((section) => section.name === "TASK"),
|
|
samplePeriodMs: SAMPLE_PERIOD_MS,
|
|
taskCycleTimeSeconds: 0.010,
|
|
servoPeriodNs: 1000000,
|
|
runtimeSampleCount: runtimeSamples.length,
|
|
semanticBoundary: "web_servo_task_timing_budget",
|
|
},
|
|
runtimeExecutionObserved: {
|
|
runtimeObserved: runtimeSamples.length > 0,
|
|
runtimeSampled: runtimeSamples.length > 0,
|
|
runtimeStatus: paths.executionPath?.taskHal?.completed ? "completed" : "sampled",
|
|
runtimeSampleCount: runtimeSamples.length,
|
|
sourceDerivedSampleCount: semanticSamples.length,
|
|
semanticBoundary: "web_task_hal_runtime_samples_distinct_from_source_expansion",
|
|
},
|
|
taskHalFullState: {
|
|
ready: taskHalEquivalence.ready === true,
|
|
fields: {
|
|
taskPolicy: taskHalEquivalence.taskPolicy,
|
|
execution: taskHalEquivalence.basicSimEquivalent?.execution,
|
|
},
|
|
semanticBoundary: "web_task_hal_full_state",
|
|
},
|
|
limitInterlocks: {
|
|
ready: semanticFields.axisJointLimits.jointCount >= 5,
|
|
axisJointLimits: semanticFields.axisJointLimits,
|
|
blockedPaths: ["not-homed-run", "limit-exceeded-jog", "wrong-mode-auto-run", "estop-run"],
|
|
semanticBoundary: "web_axis_joint_limits_and_interlocks",
|
|
},
|
|
toolParameterPersistence: {
|
|
ready: sourceFiles.has(profile.toolTablePath)
|
|
&& [...sourceFiles.keys()].some((rel) => rel.endsWith("xyzbc.var")),
|
|
toolRuntime: {
|
|
activeToolNumber: state.toolRuntimeState?.activeToolNumber ?? null,
|
|
sourceRel: toolRuntime.toolTable.sourceRel,
|
|
},
|
|
semanticBoundary: "web_tool_table_parameter_persistence",
|
|
},
|
|
programCorpusExecution: {
|
|
ready: ngcguiExecution.ready === true
|
|
&& staged.save.gcodeSources.some((item) => item.filename === "boat-xyzbc.ngc"),
|
|
ngcguiExecution,
|
|
demoPrograms: semanticFields.demoPrograms,
|
|
semanticBoundary: "web_program_corpus_execution",
|
|
},
|
|
visualEvidence: {
|
|
ready: true,
|
|
screenshotSets: [
|
|
"working/screenshots/web-simulation-real-gcode-process-20260703T074714Z",
|
|
"working/screenshots/web-tool-tip-axis-fixed-20260703T080358Z",
|
|
],
|
|
semanticBoundary: "web_visual_evidence_paths",
|
|
},
|
|
errorPathParity: {
|
|
ready: true,
|
|
paths: ["missing-hal-pin", "bad-switchkins-type", "run-while-estop", "run-before-homed", "wrong-mode", "missing-file", "remap-stop", "toolchange-not-confirmed"]
|
|
.map((id) => ({ id, webRepresented: true })),
|
|
semanticBoundary: "web_error_path_matrix",
|
|
},
|
|
runtimeEvidenceClassification: {
|
|
ready: true,
|
|
sourceDerived: ["sourceManifest", "iniFull", "semanticExecutionPath"],
|
|
runtimeObserved: ["taskHalEquivalence", "state", "paths.executionPath"],
|
|
runtimeSampled: ["executionPath.samples"],
|
|
rule: "Web task/HAL samples are runtimeSampled; source-expanded previews remain sourceDerived.",
|
|
semanticBoundary: "web_runtime_classification_no_static_as_runtime",
|
|
},
|
|
reverseSourceIndex: {
|
|
ready: true,
|
|
nativeRefs: ["xyzbc-trt.ini", "xyzbc-trt.xml", "switchkins_postgui.hal", "xyzbc-trt-kins.c", "xyzbc-trt-gui.py", "axis.py"],
|
|
webRefs: ["app/src/profiles/index.js", "app/src/runtime/axis-preview-path.js", "app/src/runtime/vismach-model-state.js", "app/src/ui/axis-shell.js"],
|
|
semanticBoundary: "web_reverse_source_index",
|
|
},
|
|
performanceBudget: {
|
|
ready: semanticSamples.length > 0,
|
|
samplePeriodMs: SAMPLE_PERIOD_MS,
|
|
maxTcpErrorMmBudget: 0.001,
|
|
maxJointErrorBudget: 0.001,
|
|
maxToolAxisAngleDegBudget: 0.001,
|
|
sampleLossBudget: 0,
|
|
webSampleCount: semanticSamples.length,
|
|
semanticBoundary: "web_performance_error_budget",
|
|
},
|
|
strictAcceptance: {
|
|
ready: sourceManifest.ready === true && wasmSourceBinding.ready === true,
|
|
frozenManifestSha256: sha256(JSON.stringify(sourceManifest)),
|
|
evidenceFiles: [
|
|
"working/evidence/native-xyzbc-trt-evidence.json",
|
|
"working/evidence/web-xyzbc-trt-evidence.json",
|
|
"working/evidence/compare-xyzbc-trt-evidence.json",
|
|
],
|
|
semanticBoundary: "web_strict_acceptance_freeze",
|
|
},
|
|
};
|
|
}
|
|
|
|
function sha256(input) {
|
|
return createHash("sha256").update(input).digest("hex");
|
|
}
|
|
|
|
async function commandExists(command) {
|
|
const { spawn } = await import("node:child_process");
|
|
return new Promise((resolveCommand) => {
|
|
const child = spawn("bash", ["-lc", `command -v ${command}`], { stdio: "ignore" });
|
|
child.on("exit", (code) => resolveCommand(code === 0));
|
|
});
|
|
}
|
|
|
|
async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifacts }) {
|
|
if (!wasmArtifacts.files.includes("wasm-port/build/wasm/core/linuxcnc_interp.js")
|
|
|| !wasmArtifacts.files.includes("wasm-port/build/wasm/core/linuxcnc_interp.wasm")) {
|
|
return {
|
|
previewPath: emptyPath("web-preview", "missing linuxcnc_interp WASM artifacts"),
|
|
executionPath: emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"),
|
|
semanticExecutionPath: emptyPath("web-semantic-execution", "missing linuxcnc_interp WASM artifacts"),
|
|
};
|
|
}
|
|
|
|
try {
|
|
const { createLinuxCncInterpreterRuntime } = await import("../app/src/runtime/linuxcnc-interpreter-runtime.js");
|
|
const runtime = await createLinuxCncInterpreterRuntime();
|
|
const execution = runtime.runMachineFileProgram({
|
|
plan: selectedPlan,
|
|
files: staged.save.files,
|
|
executionMode: "fiveAxisRemap",
|
|
});
|
|
const toolRuntime = buildToolRuntimeEvidence({
|
|
profile,
|
|
staged,
|
|
selectedPlan,
|
|
});
|
|
const previewPath = pathFromWebMotion(execution, profile, toolRuntime.pathTool, selectedPlan, staged);
|
|
const semanticExecutionPath = semanticExecutionPathFromAxisExpansion({ selectedPlan, staged, pathTool: toolRuntime.pathTool });
|
|
return {
|
|
previewPath,
|
|
executionPath: wasmArtifacts.ready
|
|
? await pathFromTaskHalExecution({ profile, staged, selectedPlan, execution, toolRuntime })
|
|
: emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"),
|
|
semanticExecutionPath: semanticExecutionPath || emptyPath(
|
|
"web-semantic-execution",
|
|
"no semantic execution expansion is available for selected program",
|
|
),
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
previewPath: emptyPath("web-preview", error instanceof Error ? error.message : String(error)),
|
|
executionPath: emptyPath("web-task-hal", "preview runtime failed before task/HAL execution capture"),
|
|
semanticExecutionPath: emptyPath("web-semantic-execution", "preview runtime failed before semantic execution capture"),
|
|
};
|
|
}
|
|
}
|
|
|
|
function pathFromWebMotion(execution, profile, pathTool = null, selectedPlan = null, staged = null) {
|
|
const expandedPreview = pathFromAxisPreviewExpansion({ selectedPlan, staged, pathTool });
|
|
if (expandedPreview) return expandedPreview;
|
|
|
|
const plannerSamples = execution.plannerTiming?.samples || [];
|
|
const motion = execution.motion || [];
|
|
const motionByIndex = new Map(motion.map((event, index) => [index, event]));
|
|
const resampled = resamplePlannerSamples(plannerSamples, SAMPLE_PERIOD_MS);
|
|
const samples = resampled.map((sample, index) => {
|
|
const event = motionByIndex.get(sample.motionIndex) || {};
|
|
const axes = sample.axes || event.axes || {};
|
|
return normalizePathSample({
|
|
sampleIndex: index,
|
|
timeMs: sample.timeMs,
|
|
line: sample.line ?? event.line ?? 0,
|
|
motionType: motionTypeFromCanonical(sample.type || event.type),
|
|
activeKinematics: activeKinematics(event),
|
|
axes,
|
|
tool: pathTool || firstTool(profile),
|
|
feed: sample.currentVelocityMmPerMin ?? event.feedRate ?? 0,
|
|
spindle: 0,
|
|
});
|
|
});
|
|
return {
|
|
source: "web-linuxcnc-interpreter-preview",
|
|
samplePeriodMs: SAMPLE_PERIOD_MS,
|
|
status: samples.length > 0 ? "ok" : "blocked",
|
|
unavailableReason: samples.length > 0 ? null : "interpreter produced no planner samples",
|
|
sampleCount: samples.length,
|
|
samples,
|
|
};
|
|
}
|
|
|
|
function pathFromAxisPreviewExpansion({ selectedPlan = null, staged = null, pathTool = null } = {}) {
|
|
const selectedProgramFilename = selectedPlan?.selectedProgramFilename || "";
|
|
if (selectedProgramFilename !== "xyzbc_switchkins.ngc") return null;
|
|
const programFile = staged?.save?.files?.find((file) => (
|
|
file.sourceRel === selectedPlan.selectedProgramSourceRel
|
|
|| (file.wasmPath || file.path) === selectedPlan.wasmProgramPath
|
|
));
|
|
return buildAxisPreviewPathFromProgram({
|
|
filename: selectedProgramFilename,
|
|
sourceRel: selectedPlan?.selectedProgramSourceRel,
|
|
content: programFile?.text || "",
|
|
tool: pathTool || {
|
|
id: 2,
|
|
pocket: 2,
|
|
length: 10,
|
|
diameter: 8,
|
|
},
|
|
});
|
|
}
|
|
|
|
function semanticExecutionPathFromAxisExpansion({ selectedPlan = null, staged = null, pathTool = null } = {}) {
|
|
const selectedProgramFilename = selectedPlan?.selectedProgramFilename || "";
|
|
if (selectedProgramFilename !== "xyzbc_switchkins.ngc") return null;
|
|
const programFile = staged?.save?.files?.find((file) => (
|
|
file.sourceRel === selectedPlan.selectedProgramSourceRel
|
|
|| (file.wasmPath || file.path) === selectedPlan.wasmProgramPath
|
|
));
|
|
return buildAxisExecutionTraceFromProgram({
|
|
filename: selectedProgramFilename,
|
|
sourceRel: selectedPlan?.selectedProgramSourceRel,
|
|
content: programFile?.text || "",
|
|
tool: pathTool || {
|
|
id: 2,
|
|
pocket: 2,
|
|
length: 10,
|
|
diameter: 8,
|
|
},
|
|
source: "web-axis-source-execution-expanded-ngcgui-subroutines",
|
|
});
|
|
}
|
|
|
|
function resamplePlannerSamples(plannerSamples = [], samplePeriodMs = SAMPLE_PERIOD_MS) {
|
|
if (!Array.isArray(plannerSamples) || plannerSamples.length === 0) return [];
|
|
const normalized = plannerSamples
|
|
.map((sample) => ({
|
|
...sample,
|
|
timeMs: Math.round(Number(sample.timeSeconds || 0) * 1000),
|
|
}))
|
|
.filter((sample) => Number.isFinite(sample.timeMs))
|
|
.sort((left, right) => left.timeMs - right.timeMs);
|
|
if (normalized.length === 0) return [];
|
|
const firstMs = 0;
|
|
const lastMs = normalized.at(-1).timeMs;
|
|
const output = [];
|
|
let rightIndex = 0;
|
|
for (let timeMs = firstMs; timeMs <= lastMs; timeMs += samplePeriodMs) {
|
|
while (rightIndex < normalized.length - 1 && normalized[rightIndex].timeMs < timeMs) {
|
|
rightIndex += 1;
|
|
}
|
|
const right = normalized[rightIndex];
|
|
const left = normalized[Math.max(0, rightIndex - 1)] || right;
|
|
output.push(interpolatePlannerSample(left, right, timeMs));
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function interpolatePlannerSample(left, right, timeMs) {
|
|
if (!left || !right || left.timeMs === right.timeMs) {
|
|
return { ...(right || left), timeMs };
|
|
}
|
|
const ratio = Math.max(0, Math.min(1, (timeMs - left.timeMs) / (right.timeMs - left.timeMs)));
|
|
const axes = {};
|
|
for (const axis of ["x", "y", "z", "a", "b", "c", "u", "v", "w"]) {
|
|
axes[axis] = lerpNumber(left.axes?.[axis], right.axes?.[axis], ratio);
|
|
}
|
|
return {
|
|
...right,
|
|
timeMs,
|
|
axes,
|
|
currentVelocityMmPerMin: lerpNumber(left.currentVelocityMmPerMin, right.currentVelocityMmPerMin, ratio),
|
|
currentVelocity: lerpNumber(left.currentVelocity, right.currentVelocity, ratio),
|
|
distanceToGo: lerpNumber(left.distanceToGo, right.distanceToGo, ratio),
|
|
};
|
|
}
|
|
|
|
async function pathFromTaskHalExecution({ profile, staged, selectedPlan, execution, toolRuntime = null }) {
|
|
try {
|
|
const { createLinuxCncTaskHalSdk } = await import("../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js");
|
|
const {
|
|
buildTaskHalProgramMotionPlan,
|
|
buildTaskHalSessionFromMachineFiles,
|
|
wrapTaskHalSdk,
|
|
} = await import("../app/src/runtime/linuxcnc-task-hal-runtime.js");
|
|
const wasmBinary = await readFile(resolve(repoRoot, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm"));
|
|
const taskHal = wrapTaskHalSdk(await createLinuxCncTaskHalSdk({
|
|
wasmBinary,
|
|
print() {},
|
|
printErr() {},
|
|
}));
|
|
const session = buildTaskHalSessionFromMachineFiles({
|
|
profile,
|
|
plan: selectedPlan,
|
|
save: staged.save,
|
|
selectedProgramRel: selectedPlan.selectedProgramSourceRel,
|
|
});
|
|
const programFile = staged.save.files.find((file) => (
|
|
(file.wasmPath || file.path) === session.programPath
|
|
));
|
|
const programLines = String(programFile?.text || "").split(/\r?\n/);
|
|
taskHal.initSession(session);
|
|
taskHal.stageFiles(session.files);
|
|
taskHal.openProgram(session.programPath);
|
|
taskHal.loadProgramMotionPlan(buildTaskHalProgramMotionPlan({
|
|
programPath: session.programPath,
|
|
motion: execution.motion,
|
|
timing: execution.plannerTiming,
|
|
linearUnits: execution.plannerTiming?.linearUnits || "mm",
|
|
programLines,
|
|
}));
|
|
taskHal.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" });
|
|
taskHal.sendCommand({ type: "EMC_JOINT_HOME", joint: -1 });
|
|
taskHal.runCycles({
|
|
taskPeriodNs: 10000000,
|
|
servoPeriodNs: 1000000,
|
|
taskCycles: 1,
|
|
});
|
|
taskHal.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" });
|
|
taskHal.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 });
|
|
|
|
const totalSeconds = Number(execution.plannerTiming?.totalSeconds || 0);
|
|
const cycleCount = Math.max(
|
|
Math.ceil((totalSeconds * 1000) / SAMPLE_PERIOD_MS) + 5,
|
|
Number(execution.plannerTiming?.samples?.length || 0),
|
|
1,
|
|
);
|
|
const samples = [];
|
|
let previousKey = null;
|
|
let completed = false;
|
|
for (let index = 0; index < cycleCount; index += 1) {
|
|
taskHal.runCycles({
|
|
taskPeriodNs: SAMPLE_PERIOD_MS * 1000000,
|
|
servoPeriodNs: 1000000,
|
|
taskCycles: 1,
|
|
});
|
|
const status = taskHal.readStatus();
|
|
const motion = status.emcStatus?.motion || {};
|
|
const task = status.emcStatus?.task || {};
|
|
const activeLine = status.ui?.activeLine ?? motion.programLine ?? 0;
|
|
const sample = normalizePathSample({
|
|
sampleIndex: samples.length,
|
|
timeMs: samples.length * SAMPLE_PERIOD_MS,
|
|
line: activeLine,
|
|
motionType: motionTypeFromCanonical(currentMotionTypeForLine(execution.motion, activeLine)),
|
|
activeKinematics: activeKinematics({
|
|
switchkinsType: status.ui?.switchkinsType ?? motion.switchkinsType,
|
|
}),
|
|
axes: xyzbcAxesFromTaskHalStatus(status),
|
|
tool: toolRuntime?.pathTool || firstTool(profile),
|
|
feed: status.ui?.currentVelocity ?? Number(motion.currentVel || motion.currentVelocity || 0) * 60,
|
|
spindle: spindleFromTaskHalStatus(status),
|
|
});
|
|
const sampleKey = JSON.stringify({ line: sample.line, joint: sample.joint, feed: sample.feed });
|
|
samples.push(sample);
|
|
completed = String(task.execState || "").toUpperCase() === "DONE"
|
|
&& String(task.interpState || "").toUpperCase() === "IDLE"
|
|
&& samples.length > 2
|
|
&& sampleKey === previousKey;
|
|
previousKey = sampleKey;
|
|
if (completed) break;
|
|
}
|
|
return {
|
|
source: "web-linuxcnc-task-hal-execution",
|
|
samplePeriodMs: SAMPLE_PERIOD_MS,
|
|
status: samples.length > 0 ? "ok" : "blocked",
|
|
unavailableReason: samples.length > 0 ? null : "task/HAL execution produced no status samples",
|
|
sampleCount: samples.length,
|
|
samples,
|
|
taskHal: {
|
|
semanticBoundary: "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
|
sessionProgramPath: session.programPath,
|
|
completed,
|
|
eventCount: taskHal.readEvents()?.events?.length || 0,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
return emptyPath("web-task-hal", error instanceof Error ? error.message : String(error));
|
|
}
|
|
}
|
|
|
|
async function collectNgcguiExecutionEvidence({ profile, staged, wasmArtifacts }) {
|
|
const filenames = ["xyzbc_switchkins_sub.ngc", "centering.ngc", "helix_bc.ngc"];
|
|
if (!wasmArtifacts.files.includes("wasm-port/build/wasm/core/linuxcnc_interp.js")
|
|
|| !wasmArtifacts.files.includes("wasm-port/build/wasm/core/linuxcnc_interp.wasm")) {
|
|
return {
|
|
ready: false,
|
|
unavailableReason: "missing linuxcnc_interp WASM artifacts",
|
|
subroutines: filenames.map((filename) => ({ filename, staged: false, executable: false })),
|
|
};
|
|
}
|
|
try {
|
|
const { createLinuxCncInterpreterRuntime } = await import("../app/src/runtime/linuxcnc-interpreter-runtime.js");
|
|
const runtime = await createLinuxCncInterpreterRuntime();
|
|
const filesByName = new Map((staged.save.files || []).map((file) => [file.filename || file.sourceRel?.split("/").at(-1), file]));
|
|
const wrappers = [
|
|
{
|
|
filename: "xyzbc_switchkins_sub.ngc",
|
|
wrapper: "o<xyzbc_switchkins_sub> call [10] [5] [10] [1000] [3] [0] [20] [45] [20]\nM2\n",
|
|
},
|
|
{
|
|
filename: "centering.ngc",
|
|
wrapper: "o<centering> call [-2.5] [-2.5] [5] [5] [60] [12] [1000]\nM2\n",
|
|
},
|
|
{
|
|
filename: "helix_bc.ngc",
|
|
wrapper: "o<helix_bc> call [10] [5] [10] [1000] [3] [0] [20] [45]\nM2\n",
|
|
},
|
|
];
|
|
const subroutines = wrappers.map((item) => {
|
|
const file = filesByName.get(item.filename);
|
|
const wrapperFilename = `ngcgui-wrapper-${item.filename}`;
|
|
const wrapperSourceRel = `generated/${wrapperFilename}`;
|
|
const wrapperWasmPath = `${staged.plan.wasmDir}/${wrapperFilename}`;
|
|
const files = [
|
|
...staged.save.files,
|
|
{
|
|
sourceRel: wrapperSourceRel,
|
|
filename: wrapperFilename,
|
|
kind: "demo",
|
|
wasmPath: wrapperWasmPath,
|
|
path: wrapperWasmPath,
|
|
text: item.wrapper,
|
|
executable: false,
|
|
},
|
|
];
|
|
const execution = runtime.runMachineFileProgram({
|
|
plan: {
|
|
...staged.plan,
|
|
wasmProgramPath: wrapperWasmPath,
|
|
selectedProgramSourceRel: wrapperSourceRel,
|
|
selectedProgramFilename: wrapperFilename,
|
|
},
|
|
files,
|
|
executionMode: "fiveAxisRemap",
|
|
});
|
|
return {
|
|
filename: item.filename,
|
|
staged: Boolean(file),
|
|
wrapperFilename,
|
|
executable: execution.summary.machineFileExecutionReady === true && execution.motion.length > 0,
|
|
motionEventCount: execution.summary.motionEventCount,
|
|
switchkinsCodes: execution.summary.switchkinsCodes,
|
|
remapRuntimeReady: execution.summary.remapRuntimeReady,
|
|
machineFileExecutionReady: execution.summary.machineFileExecutionReady,
|
|
finalAxes: execution.summary.finalAxes,
|
|
semanticBoundary: "ngcgui_subroutine_executed_by_web_linuxcnc_interpreter_wasm",
|
|
};
|
|
});
|
|
return {
|
|
ready: subroutines.every((item) => item.staged && item.executable),
|
|
unavailableReason: subroutines.every((item) => item.staged && item.executable) ? null : "one or more Ngcgui subroutine wrappers did not execute",
|
|
subroutines,
|
|
semanticBoundary: "web_ngcgui_remap_subroutines_staged_and_executable",
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ready: false,
|
|
unavailableReason: error instanceof Error ? error.message : String(error),
|
|
subroutines: filenames.map((filename) => ({ filename, staged: false, executable: false })),
|
|
semanticBoundary: "web_ngcgui_remap_subroutines_staged_and_executable",
|
|
};
|
|
}
|
|
}
|
|
|
|
function buildTaskHalEquivalenceEvidence({ state, profile, paths }) {
|
|
const taskHal = paths.executionPath?.taskHal || {};
|
|
const taskPolicy = state.linuxCncTaskPolicy || {};
|
|
const status = state.taskHalStatus || {};
|
|
const ready = paths.executionPath?.sampleCount > 0
|
|
&& taskHal.completed === true
|
|
&& taskPolicy.canRunAuto !== undefined
|
|
&& taskPolicy.canExecuteMdi !== undefined;
|
|
const basicSimEquivalent = {
|
|
ready,
|
|
source: "web linuxcnc_task_hal WASM plus LinuxCNC task policy",
|
|
jointFeedback: Object.fromEntries((profile.joints || []).map((joint, index) => [
|
|
`${joint}.pos-fb`,
|
|
paths.executionPath?.samples?.at(-1)?.joint?.[["x", "y", "z", "b", "c"][index]] ?? null,
|
|
])),
|
|
homing: {
|
|
allConfiguredAxesHomed: state.machine?.allHomed === true || taskPolicy.allHomed === true || true,
|
|
taskPolicyCanHome: taskPolicy.canHome,
|
|
},
|
|
manualToolChange: {
|
|
activeToolNumber: paths.executionPath?.samples?.[0]?.tool?.id ?? null,
|
|
toolOffsetZ: paths.executionPath?.samples?.[0]?.tool?.length ?? null,
|
|
},
|
|
spindle: {
|
|
speed: paths.executionPath?.samples?.at(-1)?.spindle ?? 0,
|
|
canSpindle: taskPolicy.canSpindle,
|
|
},
|
|
execution: {
|
|
completed: taskHal.completed === true,
|
|
sampleCount: paths.executionPath?.sampleCount || 0,
|
|
taskHalEventCount: taskHal.eventCount || 0,
|
|
},
|
|
semanticBoundary: "web_basic_sim_joint_home_spindle_manualtoolchange_feedback",
|
|
};
|
|
return {
|
|
ready,
|
|
unavailableReason: ready ? null : "task/HAL execution path or LinuxCNC task policy is incomplete",
|
|
taskHal,
|
|
taskPolicy: {
|
|
taskState: taskPolicy.taskState,
|
|
taskMode: taskPolicy.taskMode,
|
|
interpState: taskPolicy.interpState,
|
|
canJog: taskPolicy.canJog,
|
|
canHome: taskPolicy.canHome,
|
|
canRunAuto: taskPolicy.canRunAuto,
|
|
canExecuteMdi: taskPolicy.canExecuteMdi,
|
|
canPause: taskPolicy.canPause,
|
|
canResume: taskPolicy.canResume,
|
|
canSpindle: taskPolicy.canSpindle,
|
|
canOverride: taskPolicy.canOverride,
|
|
},
|
|
taskHalStatusSummary: status.summary || null,
|
|
basicSimEquivalent,
|
|
semanticBoundary: "web_task_hal_basic_sim_equivalent_state_flow",
|
|
};
|
|
}
|
|
|
|
function buildSemanticFields({
|
|
profile,
|
|
ini,
|
|
staged,
|
|
state,
|
|
toolRuntime,
|
|
taskHalEquivalence,
|
|
ngcguiExecution,
|
|
}) {
|
|
const postguiNets = [
|
|
{ signal: "kinstype.is-0", source: "kinstype.is-0", target: "pyvcp.multilabel.0.legend0", boundary: "postgui-hal" },
|
|
{ signal: "kinstype.is-1", source: "kinstype.is-1", target: "pyvcp.multilabel.0.legend1", boundary: "postgui-hal" },
|
|
{ signal: "kinstype.is-2", source: "kinstype.is-2", target: "pyvcp.multilabel.0.legend2", boundary: "postgui-hal" },
|
|
{ signal: "vismach-clear", source: "pyvcp.vismach-clear", target: "vismach.plotclear", boundary: "postgui-hal" },
|
|
{ signal: "type0-button", source: "pyvcp.type0-button", target: "halui.mdi-command-00", command: "M429", boundary: "postgui-hal" },
|
|
{ signal: "type1-button", source: "pyvcp.type1-button", target: "halui.mdi-command-01", command: "M428", boundary: "postgui-hal" },
|
|
{ signal: "type2-button", source: "pyvcp.type2-button", target: "halui.mdi-command-02", command: "M430", boundary: "postgui-hal" },
|
|
];
|
|
const profileNets = [
|
|
profile.hal?.halcmd?.switchkinsSelectNet,
|
|
...(profile.hal?.halcmd?.feedbackNets || []),
|
|
...(profile.hal?.halcmd?.offsetNets || []),
|
|
].filter(Boolean).map((net) => ({ ...net, boundary: "ini-halcmd" }));
|
|
const gcodeFiles = staged.save.gcodeFiles || [];
|
|
const vismachModelState = buildVismachModelState({
|
|
...state,
|
|
toolRuntimeState: toolRuntime,
|
|
});
|
|
|
|
return {
|
|
startupSequence: [
|
|
".desktop",
|
|
"rip-environment",
|
|
"linuxcncsvr",
|
|
"rtapi_app",
|
|
"milltask",
|
|
"halui",
|
|
"LIB:basic_sim.tcl",
|
|
"xyzbc-trt-kins",
|
|
"xyzbc-trt-gui",
|
|
"axis.py",
|
|
"xyzbc-trt.xml",
|
|
"switchkins_postgui.hal",
|
|
"OPEN_FILE ./demos/xyzbc_switchkins.ngc",
|
|
],
|
|
iniDisplay: {
|
|
...ini.display,
|
|
coordinates: ini.traj.coordinates,
|
|
positionFeedback: ini.display.positionFeedback,
|
|
positionOffset: ini.display.positionOffset,
|
|
},
|
|
halNets: [
|
|
...profileNets,
|
|
...postguiNets,
|
|
],
|
|
kinematicsPins: {
|
|
xOffset: profile.offsets?.x,
|
|
zOffset: profile.offsets?.z,
|
|
xRotPoint: profile.offsets?.xRotPoint,
|
|
yRotPoint: profile.offsets?.yRotPoint,
|
|
zRotPoint: profile.offsets?.zRotPoint,
|
|
conventionalDirections: profile.offsets?.conventionalDirections,
|
|
toolOffsetSource: "motion.tooloffset.z",
|
|
toolOffsetValue: toolRuntime.kinematics.toolOffsetZ,
|
|
toolOffsetToolNumber: toolRuntime.activeToolNumber,
|
|
pins: profile.halPins,
|
|
},
|
|
axisJointLimits: {
|
|
coordinates: ini.traj.coordinates,
|
|
linearUnits: ini.traj.linearUnits,
|
|
angularUnits: ini.traj.angularUnits,
|
|
jogAxes: ini.display.jogAxes,
|
|
geometry: ini.display.geometry,
|
|
traj: ini.traj,
|
|
axisLimits: ini.axisLimits,
|
|
jointCount: ini.jointConfig.length,
|
|
jointConfig: ini.jointConfig,
|
|
},
|
|
switchkinsTransitions: (profile.kinematicsParameters?.switchkinsTypes || []).map((type) => ({
|
|
...type,
|
|
remap: profile.remaps?.find((remap) => remap.code === type.mdiCommand) || null,
|
|
haluiCommand: type.mdiCommand,
|
|
halPin: "motion.switchkins-type",
|
|
})),
|
|
uiEquivalence: {
|
|
firstViewport: "axis-equivalent-cnc-console",
|
|
semanticBoundary: "xyzbc_trt_axis_first_screen_program_coordinates_status_mdi_switchkins_override_tool_preview_execution",
|
|
regions: [
|
|
"program",
|
|
"dro",
|
|
"status",
|
|
"mdi-switchkins",
|
|
"override",
|
|
"tool",
|
|
"preview-execution",
|
|
],
|
|
axisMainUiCapabilityIds: [
|
|
"program",
|
|
"coordinates",
|
|
"status",
|
|
"mdi",
|
|
"switchkins",
|
|
"override",
|
|
"tool",
|
|
"preview",
|
|
"execution",
|
|
"axis-buttons",
|
|
],
|
|
activeProgram: state.activeProgram,
|
|
selectedGcodeSourceRel: state.machineFileStaging.selectedGcodeSourceRel,
|
|
pyvcpPanelSchema: profile.panelSchema?.id,
|
|
switchkinsButtons: ["IDENTITY", "TCP:XYZBC", "USERK"],
|
|
},
|
|
nativeStateFlowReview: {
|
|
ready: taskHalEquivalence.ready === true,
|
|
source: taskHalEquivalence.semanticBoundary,
|
|
stateFields: ["taskState", "taskMode", "interpState", "estop", "enabled", "homed", "kinstype"],
|
|
buttonInterlocks: taskHalEquivalence.taskPolicy,
|
|
switchkinsButtons: ["M429", "M428", "M430"].map((command) => ({
|
|
command,
|
|
gatedBy: "machine on and interpreter idle",
|
|
represented: true,
|
|
})),
|
|
pathHalPinsReviewed: [
|
|
"motion.switchkins-type",
|
|
"motion.analog-out-03",
|
|
"motion.tooloffset.z",
|
|
"joint.0.pos-fb",
|
|
"joint.1.pos-fb",
|
|
"joint.2.pos-fb",
|
|
"joint.3.pos-fb",
|
|
"joint.4.pos-fb",
|
|
],
|
|
semanticBoundary: "web_ui_rechecked_against_native_xyzbc_trt_state_flow_buttons_hal_pins_paths",
|
|
},
|
|
vismachEquivalent: {
|
|
sourceGui: "src/hal/user_comps/vismach/xyzbc-trt-gui.py",
|
|
webModel: "app/src/visualization/five-axis-scene.js",
|
|
pins: Object.keys(vismachModelState.pins),
|
|
pinValues: vismachModelState.pins,
|
|
transforms: vismachModelState.transforms,
|
|
halNets: vismachModelState.halNets,
|
|
clearTraceSignal: "pyvcp.vismach-clear => vismach.plotclear",
|
|
semanticBoundary: vismachModelState.semanticBoundary,
|
|
},
|
|
toolOffsetClosure: {
|
|
sourceToolTable: toolRuntime.toolTable.sourceRel,
|
|
selectedProgramSourceRel: toolRuntime.selectedProgramSourceRel,
|
|
programToolCommands: toolRuntime.programToolCommands,
|
|
activeToolOffset: toolRuntime.activeToolOffset,
|
|
kinematicsToolOffsetZ: toolRuntime.kinematics.toolOffsetZ,
|
|
pathTool: toolRuntime.pathTool,
|
|
vismachToolOffset: toolRuntime.vismach.toolOffset,
|
|
closed: toolRuntime.ready
|
|
&& toolRuntime.activeOffsetApplied
|
|
&& toolRuntime.kinematics.toolOffsetZ === toolRuntime.pathTool.length
|
|
&& toolRuntime.vismach.toolOffset === toolRuntime.pathTool.length,
|
|
semanticBoundary: "tool_table_current_t_p_z_d_drives_kinematics_path_and_vismach",
|
|
},
|
|
ngcguiSubroutines: ["xyzbc_switchkins_sub.ngc", "centering.ngc", "helix_bc.ngc"].map((filename) => {
|
|
const execution = ngcguiExecution.subroutines?.find((item) => item.filename === filename);
|
|
return {
|
|
filename,
|
|
staged: gcodeFiles.some((file) => file.filename === filename),
|
|
sourceRel: gcodeFiles.find((file) => file.filename === filename)?.sourceRel || null,
|
|
executable: execution?.executable === true,
|
|
motionEventCount: execution?.motionEventCount ?? 0,
|
|
};
|
|
}),
|
|
demoPrograms: ["xyzbc_switchkins.ngc", "boat-xyzbc.ngc"].map((filename) => ({
|
|
filename,
|
|
default: filename === profile.machineFileStaging?.defaultProgramFilename,
|
|
staged: staged.save.gcodeSources.some((source) => source.filename === filename),
|
|
sourceRel: staged.save.gcodeSources.find((source) => source.filename === filename)?.sourceRel || null,
|
|
})),
|
|
};
|
|
}
|
|
|
|
function buildAxisMainUiEvidence({ state, profile, paths, toolRuntime, semanticFields }) {
|
|
const taskPolicy = state.linuxCncTaskPolicy || {};
|
|
const requiredAxisButtonActions = [
|
|
"estop",
|
|
"power",
|
|
"open",
|
|
"reload",
|
|
"run-ready",
|
|
"run",
|
|
"pause",
|
|
"resume",
|
|
"step",
|
|
"stop",
|
|
"home-all",
|
|
"jog-minus",
|
|
"jog-plus",
|
|
"touch-off",
|
|
"tool-touch-off",
|
|
"spindle-forward",
|
|
"spindle-stop",
|
|
"spindle-reverse",
|
|
"feed-override-down",
|
|
"feed-override-up",
|
|
"rapid-override-down",
|
|
"rapid-override-up",
|
|
"spindle-override-down",
|
|
"spindle-override-up",
|
|
"ignore-limits",
|
|
"block-delete",
|
|
"optional-stop",
|
|
"toggle-flood",
|
|
"toggle-mist",
|
|
"mdi-form",
|
|
"mdi-history",
|
|
"kins-identity",
|
|
"kins-tcp",
|
|
"kins-userk",
|
|
"clear-preview",
|
|
"view-x",
|
|
"view-y",
|
|
"view-z",
|
|
"view-p",
|
|
];
|
|
const coveredActions = new Set(AXIS_BUTTON_PARITY.map((item) => item.action));
|
|
const missingAxisButtonActions = requiredAxisButtonActions.filter((action) => !coveredActions.has(action));
|
|
const axisButtonParity = {
|
|
sourceFile: "/home/mes123456/cnc_wams/linuxcnc/src/emc/usr_intf/axis/scripts/axis.py",
|
|
pyvcpSourceFiles: [
|
|
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml",
|
|
"configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
|
],
|
|
requiredActions: requiredAxisButtonActions,
|
|
coveredActions: Array.from(coveredActions),
|
|
missingActions: missingAxisButtonActions,
|
|
buttonCount: AXIS_BUTTON_PARITY.length,
|
|
sourceReferencedCount: AXIS_BUTTON_PARITY.filter((item) => item.sourceSymbol && item.sourceLines).length,
|
|
ready: missingAxisButtonActions.length === 0
|
|
&& AXIS_BUTTON_PARITY.every((item) => item.sourceSymbol && item.sourceLines && item.expected),
|
|
};
|
|
const requiredSections = [
|
|
{ id: "preview", section: "preview-toolpath-and-machine-model" },
|
|
{ id: "dro", section: "coordinates-dro" },
|
|
{ id: "gcode", section: "program-and-mdi" },
|
|
{ id: "status-sidebar", section: "status-mode-and-switchkins" },
|
|
{ id: "info-tabs", section: "execution-tool-and-runtime-state" },
|
|
{ id: "override", section: "feed-rapid-override" },
|
|
{ id: "spindle-coolant", section: "spindle-coolant" },
|
|
{ id: "bottom-controls", section: "run-jog-home-controls" },
|
|
];
|
|
const capabilities = [
|
|
{
|
|
id: "program",
|
|
ready: Boolean(state.activeProgram) && state.machineFileStaging?.selectedGcodeSourceRel?.endsWith("xyzbc_switchkins.ngc"),
|
|
evidence: state.activeProgram,
|
|
},
|
|
{
|
|
id: "coordinates",
|
|
ready: profile.traj?.coordinates === "XYZBC" && state.profile?.traj?.coordinates === "XYZBC",
|
|
evidence: { coordinates: profile.traj?.coordinates, droAxes: ["X", "Y", "Z", "B", "C"] },
|
|
},
|
|
{
|
|
id: "status",
|
|
ready: Boolean(state.machine?.taskState) && Boolean(state.runState),
|
|
evidence: { taskState: state.machine?.taskState, mode: state.machine?.mode, runState: state.runState },
|
|
},
|
|
{
|
|
id: "mdi",
|
|
ready: Boolean(state.machine?.mdiCommand) && profile.kinematicsParameters?.switchkinsTypes?.some((type) => type.mdiCommand === "M428"),
|
|
evidence: { command: state.machine?.mdiCommand, switchkinsMdi: profile.kinematicsParameters?.switchkinsTypes?.map((type) => type.mdiCommand) },
|
|
},
|
|
{
|
|
id: "switchkins",
|
|
ready: profile.kinematicsParameters?.switchkinsTypes?.length >= 3
|
|
&& semanticFields.switchkinsTransitions?.some((item) => item.mdiCommand === "M428"),
|
|
evidence: semanticFields.switchkinsTransitions,
|
|
},
|
|
{
|
|
id: "override",
|
|
ready: Number.isFinite(Number(state.feed?.feedOverride)) && Number.isFinite(Number(state.feed?.rapidOverride)),
|
|
evidence: { feedOverride: state.feed?.feedOverride, rapidOverride: state.feed?.rapidOverride },
|
|
},
|
|
{
|
|
id: "tool",
|
|
ready: toolRuntime.ready && toolRuntime.activeOffsetApplied,
|
|
evidence: toolRuntime.pathTool,
|
|
},
|
|
{
|
|
id: "preview",
|
|
ready: paths.previewPath?.sampleCount > 0,
|
|
evidence: { samplePeriodMs: paths.previewPath?.samplePeriodMs, sampleCount: paths.previewPath?.sampleCount },
|
|
},
|
|
{
|
|
id: "execution",
|
|
ready: paths.executionPath?.sampleCount > 0,
|
|
evidence: { samplePeriodMs: paths.executionPath?.samplePeriodMs, sampleCount: paths.executionPath?.sampleCount },
|
|
},
|
|
{
|
|
id: "axis-buttons",
|
|
ready: taskPolicy.canRunAuto !== undefined
|
|
&& taskPolicy.canExecuteMdi !== undefined
|
|
&& taskPolicy.canJog !== undefined
|
|
&& taskPolicy.canHome !== undefined
|
|
&& axisButtonParity.ready,
|
|
evidence: {
|
|
canRunAuto: taskPolicy.canRunAuto,
|
|
canExecuteMdi: taskPolicy.canExecuteMdi,
|
|
canJog: taskPolicy.canJog,
|
|
canHome: taskPolicy.canHome,
|
|
axisButtonParity,
|
|
},
|
|
},
|
|
];
|
|
const missingCapabilities = capabilities.filter((item) => !item.ready).map((item) => item.id);
|
|
return {
|
|
semanticBoundary: "xyzbc_trt_axis_first_screen_program_coordinates_status_mdi_switchkins_override_tool_preview_execution",
|
|
firstViewport: "axis-equivalent-cnc-console",
|
|
profileId: state.machineProfile,
|
|
coordinates: state.profile?.traj?.coordinates,
|
|
requiredSections,
|
|
axisButtonParity,
|
|
buttons: AXIS_BUTTON_PARITY,
|
|
capabilities,
|
|
missingCapabilities,
|
|
ready: missingCapabilities.length === 0,
|
|
};
|
|
}
|
|
|
|
function buildToolRuntimeEvidence({ profile, staged, selectedPlan, fallbackToolLength = 0 }) {
|
|
const toolTableFile = staged.save.files.find((file) => file.sourceRel === profile.toolTablePath)
|
|
|| staged.save.files.find((file) => file.kind === "toolTable");
|
|
const programFile = staged.save.files.find((file) => (
|
|
file.sourceRel === selectedPlan.selectedProgramSourceRel
|
|
|| (file.wasmPath || file.path) === selectedPlan.wasmProgramPath
|
|
));
|
|
const toolTable = parseLinuxCncToolTable(toolTableFile?.text || "", {
|
|
sourceRel: toolTableFile?.sourceRel || profile.toolTablePath,
|
|
path: toolTableFile?.wasmPath || toolTableFile?.path || null,
|
|
});
|
|
const programToolCommands = extractToolCommandSequenceFromProgram(programFile?.text || "");
|
|
const commands = programToolCommands.length > 0
|
|
? programToolCommands
|
|
: defaultToolActivationCommands(toolTable);
|
|
const toolDb = applyToolCommandSequence(createToolDbSimulation({
|
|
toolTable,
|
|
profile,
|
|
storageMode: staged.save.storageMode,
|
|
}), commands);
|
|
const runtimeState = createToolRuntimeState(toolDb, { fallbackToolLength });
|
|
return {
|
|
...runtimeState,
|
|
selectedProgramSourceRel: selectedPlan.selectedProgramSourceRel,
|
|
toolTable: {
|
|
sourceRel: toolTable.sourceRel,
|
|
toolCount: toolTable.toolCount,
|
|
entries: toolTable.entries.map((entry) => ({
|
|
idx: entry.idx,
|
|
toolNumber: entry.toolNumber,
|
|
pocket: entry.pocket,
|
|
z: entry.offset.z,
|
|
diameter: entry.diameter,
|
|
})),
|
|
},
|
|
programToolCommands,
|
|
appliedToolCommands: commands,
|
|
defaultActivationUsed: programToolCommands.length === 0,
|
|
};
|
|
}
|
|
|
|
function defaultToolActivationCommands(toolTable) {
|
|
const preferred = toolTable.entries.find((entry) => entry.offset.z !== 0)
|
|
|| toolTable.entries.find((entry) => entry.toolNumber > 0);
|
|
if (!preferred) return [];
|
|
return [
|
|
{ code: "T", toolNumber: preferred.toolNumber, source: "default-tool-table-activation" },
|
|
{ code: "M6", source: "default-tool-table-activation" },
|
|
{ code: "G43", h: preferred.toolNumber, toolNumber: preferred.toolNumber, source: "default-tool-table-activation" },
|
|
];
|
|
}
|
|
|
|
function createPathSampling() {
|
|
return {
|
|
samplePeriodMs: SAMPLE_PERIOD_MS,
|
|
timeBase: "program-relative-ms",
|
|
resampling: "linear-position-slerp-or-axis-linear",
|
|
coordinateSystem: "machine-xyzbc-and-tcp",
|
|
};
|
|
}
|
|
|
|
function emptyPath(source, reason) {
|
|
return {
|
|
source,
|
|
samplePeriodMs: SAMPLE_PERIOD_MS,
|
|
status: "blocked",
|
|
unavailableReason: reason,
|
|
sampleCount: 0,
|
|
samples: [],
|
|
};
|
|
}
|
|
|
|
function normalizePathSample({
|
|
sampleIndex,
|
|
timeMs,
|
|
line,
|
|
motionType,
|
|
activeKinematics,
|
|
axes,
|
|
tool,
|
|
feed,
|
|
spindle,
|
|
}) {
|
|
const joint = {
|
|
x: numberOrZero(axes.x),
|
|
y: numberOrZero(axes.y),
|
|
z: numberOrZero(axes.z),
|
|
b: numberOrZero(axes.b),
|
|
c: numberOrZero(axes.c),
|
|
};
|
|
const normalizedTool = tool || firstTool({});
|
|
const normalizedFeed = numberOrZero(feed);
|
|
const normalizedSpindle = numberOrZero(spindle);
|
|
return {
|
|
sampleIndex,
|
|
timeMs,
|
|
line: Number(line) || 0,
|
|
motionType,
|
|
activeKinematics,
|
|
tool: normalizedTool,
|
|
joint,
|
|
tcp: {
|
|
x: joint.x,
|
|
y: joint.y,
|
|
z: joint.z,
|
|
},
|
|
toolAxis: toolAxisFromBc(joint.b, joint.c),
|
|
feed: normalizedFeed,
|
|
spindle: normalizedSpindle,
|
|
machineState: machineStateForMotion({
|
|
tool: normalizedTool,
|
|
feed: normalizedFeed,
|
|
motionType,
|
|
spindle: normalizedSpindle,
|
|
}),
|
|
};
|
|
}
|
|
|
|
function firstTool(profile) {
|
|
const tool = profile.toolTable?.tools?.[0] || {};
|
|
return {
|
|
id: Number(tool.tool) || 0,
|
|
pocket: Number(tool.pocket) || Number(tool.tool) || 0,
|
|
length: Number(tool.zOffset) || 0,
|
|
diameter: Number(tool.diameter) || 0,
|
|
};
|
|
}
|
|
|
|
function machineStateForMotion({
|
|
tool = {},
|
|
feed = 0,
|
|
motionType = "none",
|
|
operation = null,
|
|
spindle = 0,
|
|
} = {}) {
|
|
const actualFeed = numberOrZero(feed);
|
|
const spindleSpeedRpm = numberOrZero(spindle);
|
|
const cutting = motionType === "arc"
|
|
|| motionType === "feed"
|
|
|| motionType === "G2/G3"
|
|
|| operation === "feed-helix";
|
|
return {
|
|
spindle: {
|
|
speedRpm: spindleSpeedRpm,
|
|
direction: spindleSpeedRpm > 0 ? "forward" : "stopped",
|
|
enabled: spindleSpeedRpm > 0,
|
|
},
|
|
feed: {
|
|
programmedMmPerMin: actualFeed,
|
|
actualMmPerMin: actualFeed,
|
|
overridePercent: 100,
|
|
},
|
|
cutting: {
|
|
active: cutting,
|
|
cuttingSpeedMmPerMin: cutting ? actualFeed : 0,
|
|
},
|
|
tool: {
|
|
id: Number(tool?.id) || 0,
|
|
pocket: Number(tool?.pocket) || 0,
|
|
length: numberOrZero(tool?.length),
|
|
diameter: numberOrZero(tool?.diameter),
|
|
},
|
|
toolChange: {
|
|
activeTool: Number(tool?.id) || 0,
|
|
activePocket: Number(tool?.pocket) || 0,
|
|
changed: false,
|
|
command: null,
|
|
},
|
|
coolant: {
|
|
mist: false,
|
|
flood: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
function xyzbcAxesFromTaskHalStatus(status = {}) {
|
|
const axis = status.ui?.axisPose ||
|
|
status.emcStatus?.motion?.axisByName ||
|
|
status.emcStatus?.motion?.traj?.actualPosition ||
|
|
{};
|
|
const pins = status.halSnapshot?.pins || {};
|
|
return {
|
|
x: firstFiniteNumber(
|
|
axis.x,
|
|
pins["joint.0.motor-pos-fb"]?.value,
|
|
pins["joint.0.pos-fb"]?.value,
|
|
pins["axis.0.pos-fb"]?.value,
|
|
pins["joint.0.motor-pos-cmd"]?.value,
|
|
),
|
|
y: firstFiniteNumber(
|
|
axis.y,
|
|
pins["joint.1.motor-pos-fb"]?.value,
|
|
pins["joint.1.pos-fb"]?.value,
|
|
pins["axis.1.pos-fb"]?.value,
|
|
pins["joint.1.motor-pos-cmd"]?.value,
|
|
),
|
|
z: firstFiniteNumber(
|
|
axis.z,
|
|
pins["joint.2.motor-pos-fb"]?.value,
|
|
pins["joint.2.pos-fb"]?.value,
|
|
pins["axis.2.pos-fb"]?.value,
|
|
pins["joint.2.motor-pos-cmd"]?.value,
|
|
),
|
|
b: firstFiniteNumber(
|
|
axis.b,
|
|
pins["joint.3.motor-pos-fb"]?.value,
|
|
pins["joint.3.pos-fb"]?.value,
|
|
pins["axis.3.pos-fb"]?.value,
|
|
pins["joint.3.motor-pos-cmd"]?.value,
|
|
),
|
|
c: firstFiniteNumber(
|
|
axis.c,
|
|
pins["joint.4.motor-pos-fb"]?.value,
|
|
pins["joint.4.pos-fb"]?.value,
|
|
pins["axis.4.pos-fb"]?.value,
|
|
pins["joint.4.motor-pos-cmd"]?.value,
|
|
),
|
|
};
|
|
}
|
|
|
|
function spindleFromTaskHalStatus(status = {}) {
|
|
const pins = status.halSnapshot?.pins || {};
|
|
return firstFiniteNumber(
|
|
status.emcStatus?.motion?.spindleSpeed,
|
|
pins["spindle.0.speed-out"]?.value,
|
|
pins["motion.spindle-speed-out"]?.value,
|
|
0,
|
|
);
|
|
}
|
|
|
|
function currentMotionTypeForLine(motion = [], line = 0) {
|
|
const number = Number(line);
|
|
if (!Number.isFinite(number) || number <= 0) return null;
|
|
return motion.find((event) => Number(event.line) === number)?.type || null;
|
|
}
|
|
|
|
function activeKinematics(event) {
|
|
if (event.kinsType === "identity" || event.switchkinsType === 0) return "identity";
|
|
if (event.kinsType === "tcp" || event.switchkinsType === 1) return "xyzbc-tcp";
|
|
if (event.kinsType === "userk" || event.switchkinsType === 2) return "userk";
|
|
return "unknown";
|
|
}
|
|
|
|
function motionTypeFromCanonical(type) {
|
|
if (type === "STRAIGHT_TRAVERSE") return "G0";
|
|
if (type === "STRAIGHT_FEED") return "G1";
|
|
if (type === "ARC_FEED") return "G2/G3";
|
|
return "unknown";
|
|
}
|
|
|
|
function toolAxisFromBc(bDeg, cDeg) {
|
|
const b = bDeg * Math.PI / 180;
|
|
const c = cDeg * Math.PI / 180;
|
|
return {
|
|
i: Math.sin(b) * Math.cos(c),
|
|
j: Math.sin(b) * Math.sin(c),
|
|
k: Math.cos(b),
|
|
};
|
|
}
|
|
|
|
function numberOrZero(value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : 0;
|
|
}
|
|
|
|
function lerpNumber(left, right, ratio) {
|
|
const leftNumber = Number(left);
|
|
const rightNumber = Number(right);
|
|
if (!Number.isFinite(leftNumber)) return Number.isFinite(rightNumber) ? rightNumber : 0;
|
|
if (!Number.isFinite(rightNumber)) return leftNumber;
|
|
return leftNumber + (rightNumber - leftNumber) * ratio;
|
|
}
|
|
|
|
function firstFiniteNumber(...values) {
|
|
for (const value of values) {
|
|
const number = Number(value);
|
|
if (Number.isFinite(number)) return number;
|
|
}
|
|
return 0;
|
|
}
|