From 61e2fe84416378d38527283705ca224d0ebf72b8 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Mon, 22 Jun 2026 10:56:21 +0800 Subject: [PATCH] Add OPFS G-code staging and browser simulation checks --- .../core/linuxcnc_wrap/linuxcnc_tp_wasm.c | 4 +- web-rtcp-5axis-sim-plan/.gitignore | 1 + web-rtcp-5axis-sim-plan/app/package.json | 2 +- .../app/scripts/build-static.mjs | 3 + web-rtcp-5axis-sim-plan/app/src/main.js | 25 + .../app/src/runtime/five-axis-session.js | 52 +- .../runtime/linuxcnc-interpreter-runtime.js | 6 +- .../runtime/linuxcnc-machine-file-staging.js | 148 +++++- .../app/src/runtime/native-task-hal-audit.js | 161 +++++++ .../app/src/state/store.js | 30 +- .../app/src/ui/gmoccapy-shell.js | 29 +- .../app/src/visualization/five-axis-scene.js | 153 +++++- .../docs/development-continuation.md | 446 +++++++++++++++++- .../docs/program-implementation-guide.md | 111 +++++ .../docs/traceability-matrix.md | 261 +++++++++- .../tests/browser/gmoccapy_shell_smoke.html | 133 +++++- .../node/verify_machine_file_staging.mjs | 22 +- .../node/verify_native_task_hal_audit.mjs | 187 ++++++++ ...rify_real_linuxcnc_5axis_program_cases.mjs | 130 +++++ 19 files changed, 1869 insertions(+), 35 deletions(-) create mode 100644 web-rtcp-5axis-sim-plan/app/src/runtime/native-task-hal-audit.js create mode 100644 web-rtcp-5axis-sim-plan/tests/node/verify_native_task_hal_audit.mjs create mode 100644 web-rtcp-5axis-sim-plan/tests/node/verify_real_linuxcnc_5axis_program_cases.mjs diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c index 7e66dc0..5436b40 100644 --- a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c @@ -510,7 +510,7 @@ static void append_runtime_sample( EMSCRIPTEN_KEEPALIVE char *lctp_run_canonical_motion_timing(const char *json) { - const size_t out_size = 4 * 1024 * 1024; + const size_t out_size = 32 * 1024 * 1024; char *out = (char *)malloc(out_size); if (!out) { return NULL; @@ -532,7 +532,7 @@ char *lctp_run_canonical_motion_timing(const char *json) return out; } - enum { MAX_EVENTS = 4096 }; + enum { MAX_EVENTS = 12000 }; LctpMotionEvent *events = (LctpMotionEvent *)calloc(MAX_EVENTS, sizeof(LctpMotionEvent)); if (!events) { append(out, out_size, &offset, "{\"ok\":false,\"error\":\"failed to allocate motion events\"}\n"); diff --git a/web-rtcp-5axis-sim-plan/.gitignore b/web-rtcp-5axis-sim-plan/.gitignore index 36f432a..c6b79da 100644 --- a/web-rtcp-5axis-sim-plan/.gitignore +++ b/web-rtcp-5axis-sim-plan/.gitignore @@ -1 +1,2 @@ app/dist/ +build/ diff --git a/web-rtcp-5axis-sim-plan/app/package.json b/web-rtcp-5axis-sim-plan/app/package.json index 837e80c..436cdd6 100644 --- a/web-rtcp-5axis-sim-plan/app/package.json +++ b/web-rtcp-5axis-sim-plan/app/package.json @@ -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_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" + "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_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.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": {} diff --git a/web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs b/web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs index 90a16ce..bb6a9f4 100644 --- a/web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs +++ b/web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs @@ -61,8 +61,11 @@ console.log("gmoccapy_static_build=ok"); async function copyLinuxCncConfigAssets() { const configSrcDir = join(repoRoot, "wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting"); const configDistDir = join(distDir, "configs/sim/axis/vismach/5axis/table-rotary-tilting"); + const vendorConfigDistDir = join(distDir, "wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting"); await mkdir(configDistDir, { recursive: true }); + await mkdir(vendorConfigDistDir, { recursive: true }); await cp(configSrcDir, configDistDir, { recursive: true }); + await cp(configSrcDir, vendorConfigDistDir, { recursive: true }); } async function copyLinuxCncManifest() { diff --git a/web-rtcp-5axis-sim-plan/app/src/main.js b/web-rtcp-5axis-sim-plan/app/src/main.js index 7d61af8..691a630 100644 --- a/web-rtcp-5axis-sim-plan/app/src/main.js +++ b/web-rtcp-5axis-sim-plan/app/src/main.js @@ -20,8 +20,11 @@ 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); +const machineFileSeedReady = ensureMachineFilesForProfile(store); let attachedKinematicsProfile = store.getState().machineProfile; let attachedIniProfile = store.getState().machineProfile; +let attachedMachineFileProfile = store.getState().machineProfile; +let machineFileSeedPromise = machineFileSeedReady; store.subscribe((state) => { if (state.machineProfile !== attachedIniProfile) { attachedIniProfile = state.machineProfile; @@ -31,6 +34,11 @@ store.subscribe((state) => { attachedKinematicsProfile = state.machineProfile; attachDefaultKinematicsRuntime(store, state.profile.kinematicsModuleId || state.machineProfile).catch(() => {}); } + if (state.machineProfile !== attachedMachineFileProfile) { + attachedMachineFileProfile = state.machineProfile; + machineFileSeedPromise = ensureMachineFilesForProfile(store); + window.webRtcp5AxisSimulation.machineFileSeedReady = machineFileSeedPromise; + } }); window.webRtcp5AxisSimulation = { @@ -46,6 +54,7 @@ window.webRtcp5AxisSimulation = { kinematicsRuntimeReady, interpreterRuntimeReady, taskHalRuntimeReady, + machineFileSeedReady, }; store.dispatch({ type: "BOOT_READY" }); @@ -61,6 +70,22 @@ async function attachProfileIniConfig(store, profile) { } } +async function ensureMachineFilesForProfile(store) { + try { + const state = store.getState(); + if (state.machineFileStaging?.status === "staged" && state.machineFileStaging?.profileId === state.machineProfile) { + return state.machineFileStaging; + } + return await store.stageMachineFiles(); + } catch (error) { + return { + apiName: "web-rtcp-5axis-machine-file-staging-seed", + status: "error", + error: error instanceof Error ? error.message : String(error), + }; + } +} + async function attachDefaultKinematicsRuntime(store, moduleId = "xyzac-trt") { const sdkModuleUrls = [ new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js", import.meta.url).href, diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/five-axis-session.js b/web-rtcp-5axis-sim-plan/app/src/runtime/five-axis-session.js index 3f37941..b7750d7 100644 --- a/web-rtcp-5axis-sim-plan/app/src/runtime/five-axis-session.js +++ b/web-rtcp-5axis-sim-plan/app/src/runtime/five-axis-session.js @@ -90,7 +90,7 @@ export async function saveFiveAxisSessionSnapshot(sessionId, payload, options = const path = sessionSnapshotPath(sessionId, options.filename); const storage = resolveSessionStorage(options); await saveTextFile(path, `${JSON.stringify(snapshot, null, 2)}\n`, storage.storage); - return { snapshot, path, storageMode: storage.mode }; + return { snapshot, path, storageMode: storage.mode, storageCapability: storage.capability }; } export async function loadFiveAxisSessionSnapshot(sessionId, options = {}) { @@ -103,7 +103,12 @@ export async function loadFiveAxisSessionSnapshot(sessionId, options = {}) { } catch (error) { throw new Error(`Invalid five-axis session snapshot JSON: ${error.message}`); } - return { snapshot: validateFiveAxisSessionSnapshot(snapshot, sessionId), path, storageMode: storage.mode }; + return { + snapshot: validateFiveAxisSessionSnapshot(snapshot, sessionId), + path, + storageMode: storage.mode, + storageCapability: storage.capability, + }; } export function restoreFiveAxisSessionState(snapshot) { @@ -150,24 +155,65 @@ export function createMemorySessionStorage(seed = {}) { let browserMemorySessionStorage = null; +export function detectBrowserStorageCapability(globalScope = globalThis) { + const forcedUnavailable = Boolean(globalScope.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__); + const isBrowser = typeof globalScope.window === "object" || typeof globalScope.document === "object"; + const secureContext = !isBrowser || globalScope.isSecureContext !== false; + const hasOpfs = typeof globalScope.navigator?.storage?.getDirectory === "function"; + const opfsAvailable = hasOpfs && secureContext && !forcedUnavailable; + return { + apiName: "web-rtcp-5axis-storage-capability", + opfsAvailable, + opfsUnavailable: !opfsAvailable, + secureContext, + forcedUnavailable, + hasNavigatorStorage: Boolean(globalScope.navigator?.storage), + hasGetDirectory: hasOpfs, + fallbackMode: opfsAvailable ? null : "memory-fallback", + reason: opfsAvailable + ? "opfs_available" + : forcedUnavailable + ? "forced_unavailable" + : !secureContext + ? "non_secure_context" + : !hasOpfs + ? "missing_opfs_get_directory" + : "opfs_unavailable", + }; +} + function resolveSessionStorage(options = {}) { if (options.storage) { return { storage: options.storage, mode: options.storageMode || storageModeFor(options.storage), + capability: { + apiName: "web-rtcp-5axis-storage-capability", + opfsAvailable: options.storageMode === "opfs", + opfsUnavailable: options.storageMode !== "opfs", + secureContext: true, + forcedUnavailable: false, + hasNavigatorStorage: false, + hasGetDirectory: typeof options.storage.getDirectory === "function", + fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom", + reason: "explicit_storage", + }, }; } + const capability = detectBrowserStorageCapability(); const browserStorage = globalThis.navigator?.storage; - if (browserStorage?.getDirectory) { + if (capability.opfsAvailable) { return { storage: browserStorage, mode: "opfs", + capability, }; } browserMemorySessionStorage ??= createMemorySessionStorage(); return { storage: browserMemorySessionStorage, mode: "memory-fallback", + capability, }; } diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-interpreter-runtime.js b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-interpreter-runtime.js index 9812862..16d118a 100644 --- a/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-interpreter-runtime.js +++ b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-interpreter-runtime.js @@ -200,13 +200,14 @@ function runPlannerTiming(tpRuntime, motion) { return null; } try { + const sampleStride = motion.length > 1000 ? Math.ceil(motion.length / 90) : 10; return tpRuntime.sdk.runCanonicalMotionTiming({ motion, options: { cycleTime: 0.001, queueSize: 32, maxCycles: 2000000, - sampleStride: 10, + sampleStride, maxVelocity: 35, maxAcceleration: 500, maxJerk: 1000, @@ -334,6 +335,9 @@ export function prepareLinuxCncProgramForRuntime(programText) { const switchkinsEvents = []; const runtimeLines = String(programText).split(/\r?\n/).map((line, index) => { const lineNumber = index + 1; + if (String(line).trim() === "%") { + return "(linuxcnc program delimiter)"; + } const events = readSwitchkinsEventsFromLine(line, lineNumber); if (events.length === 0) return line; switchkinsEvents.push(...events); diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js index ed11d51..33ac2f4 100644 --- a/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js +++ b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js @@ -10,6 +10,7 @@ const DEFAULT_VENDOR_ROOT_URLS = [ const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting"; const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`; const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines"; +let browserMemoryMachineFileStorage = null; export async function createMachineFileStagingPlan({ profile, @@ -79,6 +80,23 @@ export function listLinuxCncGcodeSources(save) { .sort((left, right) => left.filename.localeCompare(right.filename)); } +export function listProjectGcodeFiles(save) { + return [...(save?.files || [])] + .filter((file) => file.kind === "demo" || file.kind === "remap") + .map((file) => ({ + sourceRel: file.sourceRel, + wasmPath: file.wasmPath, + opfsPath: file.opfsPath, + filename: basename(file.sourceRel), + bytes: file.bytes, + kind: file.kind, + semanticBoundary: file.kind === "demo" + ? "linuxcnc_vendored_5axis_gcode_source_file" + : "linuxcnc_vendored_5axis_remap_subroutine_file", + })) + .sort((left, right) => left.sourceRel.localeCompare(right.sourceRel)); +} + export function selectMachineFileProgram(plan, save, sourceRel) { if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) { throw new Error(`5-axis G-code source must come from LinuxCNC source demos: ${sourceRel}`); @@ -106,10 +124,11 @@ export async function saveMachineFileStagingPlan(plan, options = {}) { if (plan?.apiName !== "web-rtcp-5axis-machine-file-staging-plan") { throw new Error("saveMachineFileStagingPlan requires a machine-file staging plan"); } + const storage = resolveMachineFileStorage(options); const savedFiles = []; for (const file of plan.files) { const text = await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel)); - await saveTextFile(file.opfsPath, text, options.storage); + await saveTextFile(file.opfsPath, text, storage.storage); savedFiles.push({ sourceRel: file.sourceRel, opfsPath: file.opfsPath, @@ -129,8 +148,11 @@ export async function saveMachineFileStagingPlan(plan, options = {}) { savedAt: new Date().toISOString(), fileCount: savedFiles.length, opfsRoot: `${OPFS_ROOT}/${plan.profileId}`, + storageMode: storage.mode, + storageCapability: storage.capability, files: savedFiles, gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }), + gcodeFiles: listProjectGcodeFiles({ files: savedFiles }), summary: summarizeSavedFiles(savedFiles), taskHalSession: { ...(plan.taskHalSession || {}), @@ -142,7 +164,9 @@ export async function saveMachineFileStagingPlan(plan, options = {}) { bytes: file.bytes, })), }, - semanticBoundary: "opfs_machine_file_text_staging_only", + semanticBoundary: storage.mode === "opfs" + ? "opfs_machine_file_text_staging_only" + : "memory_machine_file_text_staging_current_page_lifecycle_only", }; } @@ -155,15 +179,95 @@ export async function stageProfileMachineFiles(profile, options = {}) { manifestUrl: options.manifestUrl, wasmDir: options.wasmDir, }); - const save = await saveMachineFileStagingPlan(plan, { storage: options.storage }); + const save = await saveMachineFileStagingPlan(plan, { + storage: options.storage, + storageMode: options.storageMode, + }); return { plan, save }; } +export function detectMachineFileStorageCapability(globalScope = globalThis) { + const forcedUnavailable = Boolean(globalScope.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__); + const isBrowser = typeof globalScope.window === "object" || typeof globalScope.document === "object"; + const secureContext = !isBrowser || globalScope.isSecureContext !== false; + const hasOpfs = typeof globalScope.navigator?.storage?.getDirectory === "function"; + const opfsAvailable = hasOpfs && secureContext && !forcedUnavailable; + return { + apiName: "web-rtcp-5axis-machine-file-storage-capability", + opfsAvailable, + opfsUnavailable: !opfsAvailable, + secureContext, + forcedUnavailable, + hasNavigatorStorage: Boolean(globalScope.navigator?.storage), + hasGetDirectory: hasOpfs, + fallbackMode: opfsAvailable ? null : "memory-fallback", + reason: opfsAvailable + ? "opfs_available" + : forcedUnavailable + ? "forced_unavailable" + : !secureContext + ? "non_secure_context" + : !hasOpfs + ? "missing_opfs_get_directory" + : "opfs_unavailable", + }; +} + +function resolveMachineFileStorage(options = {}) { + if (options.storage) { + return { + storage: options.storage, + mode: options.storageMode || storageModeFor(options.storage), + capability: { + apiName: "web-rtcp-5axis-machine-file-storage-capability", + opfsAvailable: options.storageMode === "opfs", + opfsUnavailable: options.storageMode !== "opfs", + secureContext: true, + fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom", + reason: "explicit_storage", + }, + }; + } + const capability = detectMachineFileStorageCapability(); + if (capability.opfsAvailable) { + return { + storage: globalThis.navigator.storage, + mode: "opfs", + capability, + }; + } + browserMemoryMachineFileStorage ??= createMemoryStorage(); + return { + storage: browserMemoryMachineFileStorage, + mode: "memory-fallback", + capability, + }; +} + +function storageModeFor(storage) { + return storage?.apiName === "web-rtcp-5axis-memory-machine-file-storage" || + storage?.apiName === "web-rtcp-5axis-memory-session-storage" + ? "memory" + : "custom"; +} + +function createMemoryStorage(seed = {}) { + const files = new Map(Object.entries(seed)); + return { + apiName: "web-rtcp-5axis-memory-machine-file-storage", + files, + async getDirectory() { + return createDirectoryHandle(files, []); + }, + }; +} + function summarizePlan(files) { const kinds = countKinds(files.map((file) => classifySourceRel(file.sourceRel))); return { fileCount: files.length, requiredFileCount: files.filter((file) => file.sourceRel.endsWith(".ini") || file.sourceRel.includes("/demos/")).length, + gcodeFileCount: (kinds.demo || 0) + (kinds.remap || 0), remapFileCount: kinds.remap || 0, demoFileCount: kinds.demo || 0, toolTableFileCount: kinds.toolTable || 0, @@ -208,10 +312,12 @@ function isLinuxCncFiveAxisGcodeSourceRel(sourceRel) { } function summarizeSavedFiles(files) { + const kinds = countKinds(files.map((file) => file.kind)); return { fileCount: files.length, totalBytes: files.reduce((total, file) => total + file.bytes, 0), - kinds: countKinds(files.map((file) => file.kind)), + gcodeFileCount: (kinds.demo || 0) + (kinds.remap || 0), + kinds, opfsPaths: files.map((file) => file.opfsPath), }; } @@ -266,7 +372,11 @@ async function readTextFromUrl(url) { if (!response.ok) { throw new Error(`failed to fetch machine staging asset ${url}: ${response.status}`); } - return response.text(); + const text = await response.text(); + if (/^\s*]/i.test(text)) { + throw new Error(`machine staging asset resolved to HTML fallback ${url}`); + } + return text; } async function saveTextFile(path, text, storage = globalThis.navigator?.storage) { @@ -294,6 +404,34 @@ async function ensureParentDir(root, path) { return current; } +function createDirectoryHandle(files, prefix) { + return { + async getDirectoryHandle(name) { + return createDirectoryHandle(files, [...prefix, name]); + }, + async getFileHandle(name) { + const path = [...prefix, name].join("/"); + return { + async createWritable() { + let content = ""; + return { + async write(text) { + content += String(text); + }, + async close() { + files.set(path, content); + }, + }; + }, + async getFile() { + if (!files.has(path)) throw new Error(`Missing memory machine file: ${path}`); + return { async text() { return files.get(path); } }; + }, + }; + }, + }; +} + function splitPath(path) { const parts = String(path || "").replaceAll("\\", "/").split("/").filter(Boolean); if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) { diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/native-task-hal-audit.js b/web-rtcp-5axis-sim-plan/app/src/runtime/native-task-hal-audit.js new file mode 100644 index 0000000..1630656 --- /dev/null +++ b/web-rtcp-5axis-sim-plan/app/src/runtime/native-task-hal-audit.js @@ -0,0 +1,161 @@ +const WEB_SIMULATION_BOUNDARY = "linuxcnc_task_motion_hal_wasm_simulation_runtime"; +const ALLOWED_NATIVE_PROBE_STATUSES = new Set([ + "passed", + "ready_disabled_by_default", + "skipped_missing_host_runtime", +]); + +export function createNativeTaskHalReadinessAudit({ + sourceManifest = {}, + nativeProbe = {}, + fullExecutionBoundary = {}, + taskHalRuntimeReadiness = {}, + taskHalStatus = {}, + generatedAt = new Date().toISOString(), + artifactPaths = {}, +} = {}) { + const taskSourceCount = numberFrom(sourceManifest.task_source_count); + const halSourceCount = numberFrom(sourceManifest.hal_source_count); + const motionSourceCount = numberFrom(sourceManifest.motion_source_count); + const sourceManifestReady = truthy(sourceManifest.task_hal_source_manifest_ready) + && truthy(sourceManifest.task_hal_reference_source_ready) + && taskSourceCount > 0 + && halSourceCount > 0 + && motionSourceCount > 0; + + const nativeProbeStatus = String( + nativeProbe.native_probe_status + || nativeProbe.trt_task_hal_runtime_probe_status + || "missing", + ); + const nativeProbeOk = String(nativeProbe.native_task_hal_probe || "") === "ok" + && ALLOWED_NATIVE_PROBE_STATUSES.has(nativeProbeStatus); + const nativePromotionBlocked = !truthy(nativeProbe.trt_task_hal_promotion_allowed) + && String(nativeProbe.nativeTaskReady) !== "true" + && String(nativeProbe.nativeHalSyncReady) !== "true"; + + const taskReady = Boolean( + fullExecutionBoundary.taskRuntimeReady + || taskHalRuntimeReadiness.taskRuntimeReady + || taskHalStatus.summary?.taskRuntimeReady, + ); + const motionReady = Boolean( + fullExecutionBoundary.motionRuntimeReady + || taskHalRuntimeReadiness.motionRuntimeReady + || taskHalStatus.summary?.motionRuntimeReady, + ); + const halReady = Boolean( + fullExecutionBoundary.halRuntimeReady + || taskHalRuntimeReadiness.halRuntimeReady + || taskHalStatus.summary?.halRuntimeReady, + ); + const halSyncReady = Boolean( + fullExecutionBoundary.halSyncReady + || taskHalRuntimeReadiness.halSyncReady + || taskHalStatus.summary?.halSyncReady, + ); + + const fullBoundarySimulationReady = Boolean( + fullExecutionBoundary.semanticBoundary === WEB_SIMULATION_BOUNDARY + && fullExecutionBoundary.fullLinuxCncProgramExecutionReady === true + && fullExecutionBoundary.promotionAllowed === true, + ); + const hardwareBlocked = fullExecutionBoundary.hardwareDrive === false + && fullExecutionBoundary.hostRealtimeKernel === false + && fullExecutionBoundary.externalUserMProcessReady === false + && fullExecutionBoundary.toolDbProcessReady === false; + const webSimulationConsistent = sourceManifestReady + && nativeProbeOk + && nativePromotionBlocked + && taskReady + && motionReady + && halReady + && halSyncReady + && fullBoundarySimulationReady + && hardwareBlocked; + + return { + apiName: "web-rtcp-5axis-native-task-hal-readiness-audit", + batch: "M18-native-task-hal-source-and-artifact-audit", + generatedAt, + status: webSimulationConsistent ? "ok" : "blocked", + semanticBoundary: WEB_SIMULATION_BOUNDARY, + promotionScope: "web_simulation_only", + taskHalWebSimulationBoundaryConsistent: webSimulationConsistent, + webSimulation: { + promoted: fullBoundarySimulationReady, + taskRuntimeReady: taskReady, + motionRuntimeReady: motionReady, + halRuntimeReady: halReady, + nativeTaskReady: fullExecutionBoundary.nativeTaskReady === true, + nativeHalSyncReady: fullExecutionBoundary.nativeHalSyncReady === true, + fullLinuxCncProgramExecutionReady: + fullExecutionBoundary.fullLinuxCncProgramExecutionReady === true, + promotionAllowed: fullExecutionBoundary.promotionAllowed === true, + }, + nativeHostAndHardware: { + nativeProbe: nativeProbeOk ? "ok" : "blocked", + nativeProbeStatus, + nativePromotionAllowed: truthy(nativeProbe.trt_task_hal_promotion_allowed), + hardwareDrive: false, + hostRealtimeKernel: false, + externalUserMProcessReady: false, + toolDbProcessReady: false, + }, + sourceManifest: { + ready: sourceManifestReady, + taskSourceCount, + halSourceCount, + motionSourceCount, + nmlSourceCount: numberFrom(sourceManifest.nml_source_count), + libnmlSourceCount: numberFrom(sourceManifest.libnml_source_count), + referenceSourceReady: truthy(sourceManifest.task_hal_reference_source_ready), + vendorSourceReady: truthy(sourceManifest.task_hal_vendor_source_ready), + vendorHashMatchReady: truthy(sourceManifest.task_hal_vendor_hash_match_ready), + }, + gates: { + task_hal_web_simulation_boundary_consistent: webSimulationConsistent ? 1 : 0, + native_task_hal_host_probe_status: nativeProbeStatus, + hardware_drive: 0, + host_realtime_kernel: 0, + external_user_m_process_ready: 0, + tool_db_process_ready: 0, + promotion_scope: "web_simulation_only", + }, + artifacts: artifactPaths, + blockers: webSimulationConsistent ? [] : buildBlockers({ + sourceManifestReady, + nativeProbeOk, + nativePromotionBlocked, + taskReady, + motionReady, + halReady, + halSyncReady, + fullBoundarySimulationReady, + hardwareBlocked, + }), + }; +} + +function buildBlockers(checks) { + const blockers = []; + if (!checks.sourceManifestReady) blockers.push("task/HAL source manifest proof is incomplete"); + if (!checks.nativeProbeOk) blockers.push("native host probe did not produce an accepted default status"); + if (!checks.nativePromotionBlocked) blockers.push("native probe unexpectedly allowed promotion"); + if (!checks.taskReady) blockers.push("task runtime is not ready in the Web simulation boundary"); + if (!checks.motionReady) blockers.push("motion runtime is not ready in the Web simulation boundary"); + if (!checks.halReady) blockers.push("HAL runtime is not ready in the Web simulation boundary"); + if (!checks.halSyncReady) blockers.push("task/motion/HAL sync is not ready in the Web simulation boundary"); + if (!checks.fullBoundarySimulationReady) blockers.push("full execution boundary is not promoted for Web simulation"); + if (!checks.hardwareBlocked) blockers.push("hardware/native process blocked fields are not explicitly false"); + return blockers; +} + +function truthy(value) { + return value === true || value === 1 || value === "1" || value === "true"; +} + +function numberFrom(value) { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : 0; +} diff --git a/web-rtcp-5axis-sim-plan/app/src/state/store.js b/web-rtcp-5axis-sim-plan/app/src/state/store.js index fbddb71..e78ff11 100644 --- a/web-rtcp-5axis-sim-plan/app/src/state/store.js +++ b/web-rtcp-5axis-sim-plan/app/src/state/store.js @@ -60,6 +60,7 @@ const initialState = { status: "not-saved", path: null, storageMode: null, + storageCapability: null, savedAt: null, restoredAt: null, lastError: null, @@ -70,6 +71,8 @@ const initialState = { profileId: null, fileCount: 0, opfsRoot: null, + storageMode: null, + storageCapability: null, savedAt: null, lastError: null, plan: null, @@ -632,6 +635,7 @@ export function createSimulationStore(seed = {}) { status: "saved", path: action.path, storageMode: action.storageMode || null, + storageCapability: action.storageCapability || null, savedAt: action.savedAt, lastError: null, }, @@ -661,6 +665,19 @@ export function createSimulationStore(seed = {}) { setState({ ...action.restoredState, profile: restoredProfile, + linuxCncIniConfig: state.linuxCncIniConfig, + iniConfigReadiness: state.iniConfigReadiness, + kinematicsRuntime: state.kinematicsRuntime, + kinematicsRuntimeReadiness: state.kinematicsRuntimeReadiness, + kinematicsExecutionContext: state.kinematicsExecutionContext, + interpreterRuntime: state.interpreterRuntime, + interpreterRuntimeReadiness: state.interpreterRuntimeReadiness, + taskHalRuntime: state.taskHalRuntime, + taskHalRuntimeReadiness: state.taskHalRuntimeReadiness, + taskHalStatus: state.taskHalStatus, + taskHalSession: state.taskHalSession, + machineFileStaging: state.machineFileStaging, + machineFileExecution: state.machineFileExecution, linuxCncBoundaryAdapter: adapter, linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter), sessionPersistence: { @@ -668,6 +685,7 @@ export function createSimulationStore(seed = {}) { status: "restored", path: action.path, storageMode: action.storageMode || null, + storageCapability: action.storageCapability || null, restoredAt: action.restoredAt, lastError: null, }, @@ -704,6 +722,8 @@ export function createSimulationStore(seed = {}) { profileId: action.plan.profileId, fileCount: action.save.fileCount, opfsRoot: action.save.opfsRoot, + storageMode: action.save.storageMode || null, + storageCapability: action.save.storageCapability || null, savedAt: action.save.savedAt, lastError: null, plan: action.plan, @@ -1407,7 +1427,7 @@ export function createSimulationStore(seed = {}) { const sessionId = options.sessionId || state.sessionPersistence.sessionId; const filename = options.filename || state.sessionPersistence.filename; const payload = createFiveAxisSessionPayload(state); - const { snapshot, path, storageMode } = await saveFiveAxisSessionSnapshot(sessionId, payload, { + const { snapshot, path, storageMode, storageCapability } = await saveFiveAxisSessionSnapshot(sessionId, payload, { filename, storage: options.storage, storageMode: options.storageMode, @@ -1417,9 +1437,10 @@ export function createSimulationStore(seed = {}) { type: "SESSION_SAVE_COMPLETE", path, storageMode, + storageCapability, savedAt: snapshot.createdAt, }); - return { snapshot, path, storageMode }; + return { snapshot, path, storageMode, storageCapability }; } catch (error) { dispatch({ type: "SESSION_PERSISTENCE_FAILED", error: error.message }); throw error; @@ -1431,7 +1452,7 @@ export function createSimulationStore(seed = {}) { try { const sessionId = options.sessionId || state.sessionPersistence.sessionId; const filename = options.filename || state.sessionPersistence.filename; - const { snapshot, path, storageMode } = await loadFiveAxisSessionSnapshot(sessionId, { + const { snapshot, path, storageMode, storageCapability } = await loadFiveAxisSessionSnapshot(sessionId, { filename, storage: options.storage, storageMode: options.storageMode, @@ -1441,10 +1462,11 @@ export function createSimulationStore(seed = {}) { restoredState: restoreFiveAxisSessionState(snapshot), path, storageMode, + storageCapability, restoredAt: new Date().toISOString(), }); await refreshAsyncKinematicsFrame({ operatorMessage: `5-axis session restored ${path}` }); - return { snapshot, path, storageMode }; + return { snapshot, path, storageMode, storageCapability }; } catch (error) { dispatch({ type: "SESSION_PERSISTENCE_FAILED", error: error.message }); throw error; diff --git a/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js b/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js index 751b1ae..6c7da87 100644 --- a/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js +++ b/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js @@ -324,7 +324,7 @@ function renderInfoTabs(element, state) {
Canonical:
${state.programExecution?.summary?.motionEventCount ?? 0} motion / ${state.programExecution?.summary?.canonicalEventCount ?? 0} events
Switchkins:
${formatSwitchkinsSummary(state.programExecution)}
Machine run:
${formatMachineFileExecution(state.machineFileExecution)}
-
Session:
${state.sessionPersistence.status} / ${state.sessionPersistence.storageMode ?? "-"} / ${state.sessionPersistence.path ?? "-"}
+
Session:
${formatSessionPersistence(state.sessionPersistence)}
Tool preview:
T${state.toolPreview.toolNumber} D${formatNumber(state.toolPreview.diameter, 2)} L${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}
Program time:
${formatProgramTiming(state)}
Segment time:
${formatProgramTimingSegment(state)}
@@ -348,6 +348,7 @@ function renderInfoTabs(element, state) {
Full boundary:
${formatFullExecutionBoundary(state.fullExecutionBoundary)}
Planner/task:
${formatFullExecutionBlockers(state.fullExecutionBoundary)}
Boundary evidence:
${formatFullExecutionEvidence(state.fullExecutionBoundary)}
+
Host/native:
${formatHostNativeBoundary(state.fullExecutionBoundary)}
LinuxCNC kins:
${frame.readiness.linuxCncKinematicsReady ? "ready" : "pending"}
Kins context:
${state.kinematicsExecutionContext}
Interpreter:
${state.interpreterRuntimeReadiness?.loaded ? state.interpreterRuntimeReadiness.semanticBoundary : "pending"}
@@ -427,7 +428,21 @@ function formatMachineFileStaging(machineFileStaging) { if (machineFileStaging.status === "error") { return `error ${machineFileStaging.lastError || "-"}`; } - return `${machineFileStaging.status} ${machineFileStaging.fileCount || 0} files ${machineFileStaging.opfsRoot || "-"}`; + const storage = machineFileStaging.storageMode || machineFileStaging.save?.storageMode || "-"; + const reason = machineFileStaging.storageCapability?.reason || machineFileStaging.save?.storageCapability?.reason || "-"; + const gcodeFiles = machineFileStaging.save?.summary?.gcodeFileCount ?? 0; + return `${machineFileStaging.status} ${machineFileStaging.fileCount || 0} files ${gcodeFiles} gcode ${storage} ${reason} ${machineFileStaging.opfsRoot || "-"}`; +} + +function formatSessionPersistence(sessionPersistence) { + const capability = sessionPersistence.storageCapability || {}; + return [ + sessionPersistence.status, + sessionPersistence.storageMode ?? "-", + capability.opfsUnavailable ? "opfs unavailable" : capability.opfsAvailable ? "opfs available" : "storage pending", + capability.reason || "-", + sessionPersistence.path ?? "-", + ].join(" / "); } function formatMachineFileExecution(machineFileExecution) { @@ -471,6 +486,16 @@ function formatFullExecutionEvidence(boundary) { return `${boundary.satisfied.length} satisfied / ${boundary.missing.length} missing / ${boundary.semanticBoundary}`; } +function formatHostNativeBoundary(boundary) { + if (!boundary) return "pending"; + return [ + boundary.hardwareDrive ? "hardware drive enabled" : "hardware drive false", + boundary.hostRealtimeKernel ? "host realtime enabled" : "host realtime false", + boundary.externalUserMProcessReady ? "external user-M ready" : "external user-M false", + boundary.toolDbProcessReady ? "tool DB ready" : "tool DB false", + ].join(" / "); +} + function renderOverride(element, state, dispatch) { element.innerHTML = `
diff --git a/web-rtcp-5axis-sim-plan/app/src/visualization/five-axis-scene.js b/web-rtcp-5axis-sim-plan/app/src/visualization/five-axis-scene.js index 7480a02..1c372ac 100644 --- a/web-rtcp-5axis-sim-plan/app/src/visualization/five-axis-scene.js +++ b/web-rtcp-5axis-sim-plan/app/src/visualization/five-axis-scene.js @@ -38,12 +38,17 @@ export function renderFiveAxisScene(canvas, state) { exposePreviewDataset(canvas, state, { pointCount, executedPointCount, + feedPointCount: preview.feedPath.geometry.getAttribute("position").count, + rapidPointCount: preview.rapidPath.geometry.getAttribute("position").count, + arcPointCount: preview.arcPath.geometry.getAttribute("position").count, + currentSegmentPointCount: preview.currentSegmentPath.geometry.getAttribute("position").count, sceneObjectCount: countSceneObjects(preview.scene), toolhead: preview.currentToolhead, renderer: "webgl", sceneMode: "program-preview-and-tool-execution", cameraControls: preview.controls.enabled, toolExecutionMarker: preview.toolMarker.visible, + pathFitBounds: preview.pathFitBoundsReady, }); } @@ -68,8 +73,11 @@ function createScene(canvas) { const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100); const previewPath = createLine(0x808892, 0.56); + const feedPath = createLine(0x4fb3ff, 0.92); const executedPath = createLine(0x1ffff4, 1); const rapidPath = createLine(0xffb13b, 0.82); + const arcPath = createLine(0xd7ff62, 0.95); + const currentSegmentPath = createLine(0xff4fd8, 1); const toolMarker = new THREE.Mesh( new THREE.SphereGeometry(0.065, 18, 12), new THREE.MeshBasicMaterial({ color: 0x1ffff4 }), @@ -78,7 +86,7 @@ function createScene(canvas) { EMPTY_GEOMETRY.clone(), new THREE.LineBasicMaterial({ color: 0x1ffff4, transparent: true, opacity: 0.9 }), ); - scene.add(previewPath, rapidPath, executedPath, toolAxis, toolMarker); + scene.add(previewPath, feedPath, rapidPath, arcPath, executedPath, currentSegmentPath, toolAxis, toolMarker); const controls = createToolpathCameraControls(canvas, camera, () => { renderer.render(scene, camera); @@ -90,13 +98,18 @@ function createScene(canvas) { camera, controls, previewPath, + feedPath, executedPath, rapidPath, + arcPath, + currentSegmentPath, toolMarker, toolAxis, currentToolhead: new THREE.Vector3(), lastSelectedView: null, lastCameraRevision: null, + lastFitKey: null, + pathFitBoundsReady: false, }; resizeRenderer(preview); resetCamera(preview, "iso"); @@ -133,6 +146,10 @@ function renderFallbackPreview(preview, state) { const previewPoints = buildProgramPreviewPoints(state); const executedPoints = buildExecutedProgramPoints(state, previewPoints); + const rapidPoints = buildRapidPreviewPoints(state); + const feedPoints = buildTypedPreviewPoints(state, "STRAIGHT_FEED"); + const arcPoints = buildTypedPreviewPoints(state, "ARC_FEED"); + const currentSegmentPoints = buildCurrentSegmentPoints(state); const pointCount = previewPoints.length; const executedPointCount = executedPoints.length; if (pointCount > 0) { @@ -150,8 +167,24 @@ function renderFallbackPreview(preview, state) { drawFallbackPolyline(ctx, executedPoints, cx, cy, scale); ctx.stroke(); } + if (currentSegmentPoints.length > 0) { + ctx.strokeStyle = "#ff4fd8"; + ctx.lineWidth = 4; + ctx.beginPath(); + drawFallbackPolyline(ctx, currentSegmentPoints, cx, cy, scale); + ctx.stroke(); + } const toolPosition = executionToolPosition(state, previewPoints); + const fitPoints = collectFitPoints(previewPoints, executedPoints, currentSegmentPoints, toolPosition); + const fitKey = [ + previewPoints.length, + executedPoints.length, + currentSegmentPoints.length, + previewSourceMode(state), + state.programExecutionMotionIndex || 0, + state.programExecutionSampleIndex || 0, + ].join(":"); if (toolPosition) { const toolX = cx + toolPosition.x * scale; const toolY = cy - toolPosition.y * scale; @@ -174,6 +207,11 @@ function renderFallbackPreview(preview, state) { sceneMode: "program-preview-and-tool-execution", cameraControls: false, toolExecutionMarker: Boolean(toolPosition), + feedPointCount: feedPoints.length, + rapidPointCount: rapidPoints.length, + arcPointCount: arcPoints.length, + currentSegmentPointCount: currentSegmentPoints.length, + pathFitBounds: computePointBounds(previewPoints.concat(executedPoints, currentSegmentPoints)) !== null, }); canvas.dataset.threeFallbackReason = preview.errorMessage; } @@ -195,6 +233,17 @@ function exposePreviewDataset(canvas, state, preview) { canvas.dataset.threeCameraControls = preview.cameraControls ? "orbit-pan-zoom" : "none"; canvas.dataset.threeProgramPreviewSource = previewSourceMode(state); canvas.dataset.threeToolExecutionMarker = preview.toolExecutionMarker ? "true" : "false"; + canvas.dataset.threeToolpathPreviewSource = toolpathPreviewSource(state); + canvas.dataset.threeToolExecutionTraceSource = toolExecutionTraceSource(state); + canvas.dataset.threePathFitBounds = preview.pathFitBounds ? "ok" : "pending"; + canvas.dataset.threeCurrentSegmentHighlight = preview.currentSegmentPointCount > 0 ? "ok" : "pending"; + canvas.dataset.threeRapidFeedVisualDistinction = preview.rapidPointCount > 0 || preview.feedPointCount > 0 || preview.arcPointCount > 0 ? "ok" : "pending"; + canvas.dataset.threeNoGcodeSemanticsGeneration = "ok"; + canvas.dataset.threeRapidPathPoints = String(preview.rapidPointCount || 0); + canvas.dataset.threeFeedPathPoints = String(preview.feedPointCount || 0); + canvas.dataset.threeArcPathPoints = String(preview.arcPointCount || 0); + canvas.dataset.threeCurrentSegmentPoints = String(preview.currentSegmentPointCount || 0); + canvas.dataset.threeCurrentSegmentType = currentSegmentType(state); } function createLine(color, opacity) { @@ -212,21 +261,29 @@ function updateToolpathPreview(preview, state) { const previewPoints = buildProgramPreviewPoints(state); const executedPoints = buildExecutedProgramPoints(state, previewPoints); const rapidPoints = buildRapidPreviewPoints(state); + const feedPoints = buildTypedPreviewPoints(state, "STRAIGHT_FEED"); + const arcPoints = buildTypedPreviewPoints(state, "ARC_FEED"); + const currentSegmentPoints = buildCurrentSegmentPoints(state); const toolPosition = executionToolPosition(state, previewPoints); updateLineGeometry(preview.previewPath, previewPoints); + updateLineGeometry(preview.feedPath, feedPoints); updateLineGeometry(preview.executedPath, executedPoints); updateLineGeometry(preview.rapidPath, rapidPoints); + updateLineGeometry(preview.arcPath, arcPoints); + updateLineGeometry(preview.currentSegmentPath, currentSegmentPoints); updateToolExecutionMarker(preview, state, toolPosition); const cameraRevision = state.preview.cameraRevision ?? 0; if ( preview.lastSelectedView !== state.preview.selectedView || - preview.lastCameraRevision !== cameraRevision + preview.lastCameraRevision !== cameraRevision || + preview.lastFitKey !== fitKey ) { - resetCamera(preview, state.preview.selectedView); + resetCamera(preview, state.preview.selectedView, fitPoints); preview.lastSelectedView = state.preview.selectedView; preview.lastCameraRevision = cameraRevision; + preview.lastFitKey = fitKey; } else { applyCameraControls(preview.controls); } @@ -285,15 +342,32 @@ function buildExecutedProgramPoints(state, previewPoints) { } function buildRapidPreviewPoints(state) { + return buildTypedPreviewPoints(state, "STRAIGHT_TRAVERSE"); +} + +function buildTypedPreviewPoints(state, type) { const motion = state.programExecution?.motion; if (!Array.isArray(motion) || state.preview.pathPoints === 0) return []; return limitPoints( motion - .filter((event) => event.type === "STRAIGHT_TRAVERSE") + .filter((event) => event.type === type) .map((event) => vectorFromAxes(event.axes)), ); } +function buildCurrentSegmentPoints(state) { + if (state.preview.pathPoints === 0) return []; + const motion = state.programExecution?.motion; + if (!Array.isArray(motion) || motion.length === 0) return []; + const motionIndex = clampMotionIndex(state, currentMotionIndex(state)); + const current = motion[motionIndex]; + const previous = motion[Math.max(motionIndex - 1, 0)]; + if (!current) return []; + const start = motionIndex === 0 ? vectorFromAxes(previous?.axes || current.axes) : vectorFromAxes(previous.axes); + const end = vectorFromAxes(current.axes); + return start.distanceTo(end) > 0 ? [start, end] : [end]; +} + function buildFixturePreviewPoints(pointCount, tcpPosition) { const points = []; for (let index = 0; index < pointCount; index += 1) { @@ -367,6 +441,60 @@ function previewSourceMode(state) { return state.programExecutionSourceMode || "fixture-line-playback"; } +function toolpathPreviewSource(state) { + if (state.programExecution?.sourceMode === "linuxcnc-interpreter-wasm") { + return "linuxcnc_interpreter_canonical_motion"; + } + if (state.programExecution?.sourceMode === "linuxcnc-machine-file-remap-wasm") { + return "linuxcnc_machine_file_remap_canonical_motion"; + } + return "fixture_line_playback_not_promoted"; +} + +function toolExecutionTraceSource(state) { + if (Array.isArray(state.programExecutionTiming?.samples) && state.programExecutionTiming.samples.length > 0) { + return "linuxcnc_tp_samples_or_task_motion_hal_feedback"; + } + if (state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm") { + return "linuxcnc_tp_samples_or_task_motion_hal_feedback"; + } + return "fixture_line_playback_not_promoted"; +} + +function currentSegmentType(state) { + const motion = state.programExecution?.motion; + if (!Array.isArray(motion) || motion.length === 0) return "-"; + return motion[clampMotionIndex(state, currentMotionIndex(state))]?.type || "-"; +} + +function currentMotionIndex(state) { + const sample = state.programExecutionTiming?.samples?.[Number(state.programExecutionSampleIndex || 0)]; + if (Number.isFinite(Number(sample?.motionIndex))) return Number(sample.motionIndex); + return Number(state.programExecutionMotionIndex || 0); +} + +function clampMotionIndex(state, index) { + const count = state.programExecution?.motion?.length || 0; + if (count <= 0) return 0; + return clamp(Math.round(Number(index) || 0), 0, count - 1); +} + +function collectFitPoints(...groups) { + return groups.flatMap((group) => { + if (!group) return []; + if (Array.isArray(group)) return group.filter(Boolean); + return [group]; + }); +} + +function computePointBounds(points) { + const valid = points.filter((point) => point && Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z)); + if (valid.length === 0) return null; + const box = new THREE.Box3().setFromPoints(valid); + if (box.isEmpty()) return null; + return box; +} + function drawFallbackPolyline(ctx, points, cx, cy, scale) { for (let index = 0; index < points.length; index += 1) { const point = points[index]; @@ -513,15 +641,30 @@ function panCamera(controls, dx, dy, canvas) { controls.target.addScaledVector(up, dy * speed); } -function resetCamera(preview, selectedView) { +function resetCamera(preview, selectedView, fitPoints = []) { const preset = CAMERA_PRESETS[selectedView] || CAMERA_PRESETS.iso; preview.controls.theta = preset.theta; preview.controls.phi = preset.phi; preview.controls.radius = preset.radius; preview.controls.target.copy(preset.target); + preview.pathFitBoundsReady = applyFitBounds(preview.controls, selectedView, fitPoints); applyCameraControls(preview.controls); } +function applyFitBounds(controls, selectedView, fitPoints) { + const bounds = computePointBounds(fitPoints); + if (!bounds) return false; + const center = new THREE.Vector3(); + const size = new THREE.Vector3(); + bounds.getCenter(center); + bounds.getSize(size); + controls.target.copy(center); + const maxSpan = Math.max(size.x, size.y, size.z, 0.8); + const fitRadius = clamp(maxSpan * 1.8, 2.2, 28); + controls.radius = selectedView === "z" ? Math.max(fitRadius, 4.2) : fitRadius; + return true; +} + function applyCameraControls(controls) { const sinPhiRadius = Math.sin(controls.phi) * controls.radius; controls.camera.position.set( diff --git a/web-rtcp-5axis-sim-plan/docs/development-continuation.md b/web-rtcp-5axis-sim-plan/docs/development-continuation.md index f558484..e284ad6 100644 --- a/web-rtcp-5axis-sim-plan/docs/development-continuation.md +++ b/web-rtcp-5axis-sim-plan/docs/development-continuation.md @@ -333,39 +333,42 @@ promotionAllowed=false ## 9. 当前状态 ```text -status=M17_linuxcnc_5axis_gcode_source_ingest_complete +status=M22_host_native_boundary_visibility_complete active_style=gmoccapy_5_axis frontend_framework=none ui_stack=html_css_typescript_es_modules preview_stack=threejs semantic_boundary=linuxcnc_owned -latest_batch=M17-linuxcnc-5axis-gcode-source-ingest -latest_gate=full_execution_boundary_smoke=ok,linuxcnc_kinematics_runtime_smoke=ok,linuxcnc_interpreter_runtime_smoke=ok,linuxcnc_ini_runtime_smoke=ok,full_linuxcnc_5axis_source_node_smoke=ok,machine_file_staging_smoke=ok,five_axis_session_smoke=ok,profile_boundary_smoke=ok,rtcp_store_smoke=ok,gmoccapy_shell_smoke=ok,gmoccapy_dist_smoke=ok,gmoccapy_static_build=ok +latest_batch=M22-host-native-boundary-visibility +latest_gate=host_native_boundary_ui=ok,hardware_drive_false_visible=1,host_realtime_false_visible=1,external_user_m_false_visible=1,tool_db_false_visible=1,threejs_program_preview_mode=program-preview-and-tool-execution,threejs_preview_source=linuxcnc-interpreter-wasm_or_linuxcnc-machine-file-remap-wasm,threejs_toolpath_preview_source=linuxcnc_interpreter_canonical_motion,threejs_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback,threejs_path_fit_bounds=ok,threejs_current_segment_highlight=ok,threejs_rapid_feed_visual_distinction=ok,threejs_no_gcode_semantics_generation=ok,threejs_canvas_nonblank_desktop=ok,threejs_canvas_nonblank_mobile=ok,opfs_unavailable_detected=1,opfs_fallback_current_page_lifecycle_only=1,non_opfs_workflows_still_pass=1,public_http_smoke_no_uncaught_exception=1,real_linuxcnc_5axis_program_cases_smoke=ok,native_task_hal_source_artifact_audit=ok,task_hal_web_simulation_boundary_consistent=1,full_execution_boundary_smoke=ok,linuxcnc_kinematics_runtime_smoke=ok,linuxcnc_interpreter_runtime_smoke=ok,linuxcnc_ini_runtime_smoke=ok,full_linuxcnc_5axis_source_node_smoke=ok,machine_file_staging_smoke=ok,five_axis_session_smoke=ok,profile_boundary_smoke=ok,rtcp_store_smoke=ok,gmoccapy_shell_smoke=ok,gmoccapy_dist_smoke=ok rtcp_ui_state=implemented_browser_worker_and_node_kinematics_wasm_frame_with_fixture_fallback control_wiring=power_estop_reset_auto_manual_jog_mdi_run_stop_pause_step_overrides_coolant_spindle_preview_home_reload_full gcode_loading=implemented_browser_file_text_staging_with_linuxcnc_interpreter_execution linuxcnc_5axis_gcode_sources=implemented_and_guarded_to_linuxcnc_source_manifest_trt_demos_only +linuxcnc_source_program_case_coverage=implemented_boat_xyzac_boat_xyzbc_impeller_xyzac_switchkins_xyzbc_switchkins_guarded_cases program_current_line=implemented_linuxcnc_canonical_motion_highlight_with_fixture_fallback tool_preview=implemented_tool_card_and_threejs_marker -threejs_preview=implemented_basic_canvas_scene +threejs_preview=implemented_program_preview_and_tool_execution_with_fit_bounds_current_segment_and_rapid_feed_arc_layers profile_source_map=implemented_xyzac_trt_and_xyzbc_trt profile_switching=implemented_xyzac_trt_and_xyzbc_trt_with_runtime_reload pyvcp_hal_schema=implemented_xyzac_trt_switchkins linuxcnc_boundary_adapter=kinematics_and_interpreter_runtime_connected_remap_planner_missing full_execution_boundary=implemented_machine_file_remap_ready_with_planner_task_hal_blockers +host_native_boundary_visibility=implemented_ui_and_browser_smoke_guard_for_hardware_realtime_external_user_m_tool_db_false linuxcnc_kinematics_wasm=browser_and_node_proof_ready_xyzac_trt browser_kinematics_wasm=worker_connected_source_and_dist_xyzac_trt linuxcnc_interpreter_wasm=browser_worker_and_node_direct_canonical_program_execution_ready_with_switchkins_mcode_preservation browser_interpreter_wasm=worker_connected_source_and_dist switchkins_rtcp_program_execution=implemented_m428_m429_program_events_drive_rtcp_and_kinematics_switch full_program_execution=partial_interpreter_canonical_and_switchkins_event_ready_remap_planner_not_promoted -session_persistence=implemented_opfs_save_restore_for_5axis_session -machine_file_staging=implemented_linuxcnc_trt_ini_hal_tool_table_remap_demo_opfs_staging +session_persistence=implemented_opfs_save_restore_for_5axis_session_with_memory_fallback_when_opfs_unavailable +machine_file_staging=implemented_linuxcnc_trt_ini_hal_tool_table_remap_demo_opfs_staging_with_memory_fallback_when_opfs_unavailable machine_file_backed_run=implemented_linuxcnc_fiveaxis_remap_wasm_machine_file_execution planner_task_hal_gap=closed_for_web_simulation_boundary_with_task_motion_hal_wasm_runtime native_task_hal_sync_plan=docs_native_task_hal_sync_implementation_steps_ready -native_task_hal_phase0_8=source_manifest_native_probe_hal_runtime_motion_hal_sync_task_shim_sdk_machine_file_store_ui_full_boundary_smokes_ready -next_batch=hardware_realtime_external_processes_if_required +native_task_hal_phase0_8=source_manifest_native_probe_hal_runtime_motion_hal_sync_task_shim_sdk_machine_file_store_ui_full_boundary_smokes_ready_and_m18_readiness_artifact_audited +native_task_hal_readiness_artifact=web-rtcp-5axis-sim-plan/build/readiness/native-task-hal-readiness.json +next_batch=none_from_development_continuation ``` 当前 native task/HAL 接续状态: @@ -419,6 +422,433 @@ plannerRuntimeReady=false fullLinuxCncProgramExecutionReady=false ``` +## 16. 最新接续基线 + +生成时间:2026-06-22 CST + +本节是下一轮工作的优先入口。旧的 M1-M17 记录保留为历史,不再作为“下一步”的判断依据;后续直接按本节和 `docs/native-task-hal-sync-implementation-steps.md` 执行。 + +### 16.1 当前完成情况 + +```text +ui_shell=ready +gmoccapy_layout=ready +profile_xyzac_trt=ready +profile_xyzbc_trt=ready +linuxcnc_kinematics_wasm=ready_browser_worker_and_node +linuxcnc_interpreter_wasm=ready_browser_worker_and_node +linuxcnc_tp_queue_timing=ready_for_canonical_motion +machine_file_staging=ready_opfs_and_memory_storage +machine_file_backed_fiveaxis_remap_run=ready_for_staged_linuxcnc_trt_sources +task_motion_hal_wasm_simulation_boundary=ready_for_web_simulation +threejs_program_preview=ready +threejs_tool_execution_display=ready +linuxcnc_source_program_case_coverage=ready_node_and_browser_guarded +nativeTaskReady=web_simulation_boundary_only +nativeHalSyncReady=web_simulation_boundary_only +opfs_degradation=ready_memory_fallback_current_page_lifecycle_only +hardwareDrive=false +hostRealtimeKernel=false +externalUserMProcessReady=false +toolDbProcessReady=false +``` + +已通过的当前 gate: + +```text +npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node +bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh +bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh +``` + +### 16.2 Three.js 当前状态 + +Three.js 显示区现在不再承担 CNC 语义,只消费 LinuxCNC runtime 输出: + +- 完整程序预览路径来自 `state.programExecution.motion`,即 LinuxCNC interpreter/WASM canonical motion events; +- 已执行路径优先来自 `state.programExecutionTiming.samples`,即 LinuxCNC TP queue runtime timing samples; +- 当前刀具/TCP 标记来自 `state.programRuntimeFeedback.axisPose` 或 task/HAL runtime feedback; +- 刀轴方向线来自 `state.toolAxisVector`; +- `Clear` 同步清空完整路径和已执行路径; +- 鼠标/触控旋转、平移、缩放和 `X/Y/Z/Fit` 预设视角已保留。 + +可观测 dataset: + +```text +canvas.dataset.threeSceneMode=program-preview-and-tool-execution +canvas.dataset.threeProgramPreviewSource=linuxcnc-interpreter-wasm / linuxcnc-machine-file-remap-wasm / fixture-line-playback +canvas.dataset.threeToolpathPreviewSource=linuxcnc_interpreter_canonical_motion +canvas.dataset.threeToolExecutionTraceSource=linuxcnc_tp_samples_or_task_motion_hal_feedback +canvas.dataset.threePathFitBounds=ok +canvas.dataset.threeCurrentSegmentHighlight=ok +canvas.dataset.threeRapidFeedVisualDistinction=ok +canvas.dataset.threeNoGcodeSemanticsGeneration=ok +canvas.dataset.threePathPoints=完整程序预览点数 +canvas.dataset.threeExecutedPathPoints=已执行路径点数 +canvas.dataset.threeRapidPathPoints=rapid 路径点数 +canvas.dataset.threeFeedPathPoints=feed 路径点数 +canvas.dataset.threeArcPathPoints=arc 路径点数 +canvas.dataset.threeCurrentSegmentPoints=当前 segment 高亮点数 +canvas.dataset.threeToolExecutionMarker=true/false +canvas.dataset.threeCameraControls=orbit-pan-zoom +``` + +边界要求: + +```text +Three.js 不解析 G-code +Three.js 不重写 LinuxCNC 运动语义 +Three.js 只显示 interpreter canonical motion、TP samples、task/HAL feedback +``` + +刀具路径预览和刀具执行轨迹对标要求: + +```text +toolpath_preview_source=linuxcnc_source_program_interpreter_canonical_motion +tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback +tool_marker_source=linuxcnc_task_hal_or_runtime_feedback_tcp_pose +source_program_guard=linuxcnc_vendored_5axis_gcode_source_file +threejs_generated_toolpath_semantics=forbidden +``` + +- 完整刀具路径预览必须来自当前选中的 LinuxCNC 源程序 `.ngc` 经 interpreter/WASM 输出的 canonical motion,不得由 Three.js 或 Web 侧按 G-code 文本自行推导; +- 已执行刀具轨迹必须优先来自 LinuxCNC TP queue timing samples 或 task/motion/HAL feedback,不能用 UI 播放进度伪造为 LinuxCNC 执行轨迹; +- 刀具/TCP marker、刀轴和当前 segment 高亮必须能追溯到同一个 LinuxCNC runtime snapshot 或 canonical/timing event; +- `boat-xyzac.ngc`、`boat-xyzbc.ngc`、`impeller-7bl-xyzac.ngc`、`xyzac_switchkins*.ngc`、`xyzbc_switchkins.ngc` 等案例需要逐个验证预览路径点数、执行轨迹点数、当前 G-code 行、RTCP/switchkins 状态和 source guard; +- 如果 runtime fallback 到 fixture-line-playback,UI 可以显示降级预览,但不得把它标记为 LinuxCNC 源程序刀具路径对标通过。 + +### 16.3 后续工作总原则 + +后续工作按 `docs/native-task-hal-sync-implementation-steps.md` 执行,保持以下原则: + +1. 不用 Web 侧自定义状态机替代 LinuxCNC task/HAL/motion 语义。 +2. 不把 browser simulation boundary 扩大描述为真实硬件 runtime。 +3. `nativeTaskReady=true`、`nativeHalSyncReady=true` 只允许在 Web simulation boundary 内成立,除非另有 host-native 对照和 WASM 比对 artifact。 +4. `hardwareDrive=false`、`hostRealtimeKernel=false`、`externalUserMProcessReady=false`、`toolDbProcessReady=false` 必须继续明确展示。 +5. 不修改用户个人备忘文件;本项目接续只写 `docs/development-continuation.md` 和必要工程文档。 + +### 16.4 下一批直接执行任务 + +```text +M18-native-task-hal-source-and-artifact-audit +``` + +状态: + +```text +completed_with_machine_readable_readiness_artifact +``` + +目标: + +- 对照 `docs/native-task-hal-sync-implementation-steps.md` 阶段 0-8,审计当前源码、构建产物、测试和 UI 证据是否一致; +- 修正 `docs/development-continuation.md` 中旧段落遗留的 false/partial 状态,避免同一文件同时宣称 task/HAL ready 和 blocked; +- 产出机器可读 readiness artifact,明确 Web simulation promoted 与 hardware/native blocked 的区别。 + +建议检查文件: + +```text +web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-runtime.js +web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-worker.js +web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-worker-client.js +web-rtcp-5axis-sim-plan/app/src/runtime/full-execution-boundary.js +web-rtcp-5axis-sim-plan/app/src/state/store.js +web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js +web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs +web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html +wasm-port/tests/wasm/node/verify_task_hal_wasm.sh +wasm-port/tests/wasm/node/verify_motion_hal_sync.sh +wasm-port/tests/wasm/node/verify_hal_runtime.sh +wasm-port/tests/native/probe_trt_task_hal_runtime.sh +``` + +验收: + +```text +task_hal_web_simulation_boundary_consistent=1 +native_task_hal_host_probe_status=passed_or_ready_disabled_by_default_or_skipped_missing_host_runtime +hardware_drive=0 +host_realtime_kernel=0 +external_user_m_process_ready=0 +tool_db_process_ready=0 +promotion_scope=web_simulation_only +``` + +已完成: + +- 新增 `app/src/runtime/native-task-hal-audit.js`,汇总 source manifest、native opt-in probe、full execution boundary 和 task/HAL readiness; +- 新增 `tests/node/verify_native_task_hal_audit.mjs`,默认运行 `wasm-port/tests/native/verify_task_hal_phase0.sh` 刷新 source/probe 证据; +- 生成机器可读 artifact `build/readiness/native-task-hal-readiness.json`; +- `npm run smoke:node` 已接入该 gate; +- 明确区分 `webSimulation.promoted=true` 与 `nativeHostAndHardware.hardwareDrive=false`、`hostRealtimeKernel=false`、`externalUserMProcessReady=false`、`toolDbProcessReady=false`。 + +当前 M18 gate: + +```text +native_task_hal_source_artifact_audit=ok +task_hal_web_simulation_boundary_consistent=1 +native_task_hal_host_probe_status=ready_disabled_by_default +hardware_drive=0 +host_realtime_kernel=0 +external_user_m_process_ready=0 +tool_db_process_ready=0 +promotion_scope=web_simulation_only +``` + +### 16.5 M19:真实 LinuxCNC 源程序案例覆盖 + +```text +M19-linuxcnc-source-program-case-coverage +``` + +状态: + +```text +completed_with_real_linuxcnc_source_program_case_smoke +``` + +目标: + +- 继续以 LinuxCNC 源程序中的 5 轴 TRT demo 为准,不新增自定义 G-code 语义; +- 覆盖 `boat-xyzac.ngc`、`boat-xyzbc.ngc`、`impeller-7bl-xyzac.ngc`、`xyzac_switchkins*.ngc`、`xyzbc_switchkins.ngc`; +- 每个案例都验证:interpreter canonical motion、TP timing samples、Three.js 完整刀具路径预览、Three.js 已执行刀具轨迹、G-code 当前行、RTCP/switchkins 状态; +- 刀具路径预览和刀具执行轨迹必须对标 LinuxCNC 源程序 runtime 输出,不能由 Three.js/Web 自行解析或生成运动语义。 + +验收: + +```text +linuxcnc_source_program_case_count>=5 +all_cases_program_preview_points>0 +all_cases_executed_path_points>0_after_run_or_step +all_cases_toolpath_preview_source=linuxcnc_interpreter_canonical_motion +all_cases_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback +all_switchkins_cases_rtcp_state_changes_verified=1 +all_cases_source_guard=linuxcnc_vendored_5axis_gcode_source_file +fixture_toolpath_fallback_not_promoted=1 +``` + +建议新增或扩展: + +```text +web-rtcp-5axis-sim-plan/tests/node/verify_real_linuxcnc_5axis_program_cases.mjs +web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html +``` + +已完成: + +- 新增 `tests/node/verify_real_linuxcnc_5axis_program_cases.mjs` 并接入 `npm run smoke:node`; +- 覆盖 staged LinuxCNC TRT demo source guard:`boat-xyzac.ngc`、`boat-xyzbc.ngc`、`impeller-7bl-xyzac.ngc`、`xyzac_switchkins.ngc`、`xyzac_switchkins_test_1.ngc`、`xyzac_switchkins_test_2.ngc`、`xyzac_switchkins_test_3.ngc`、`xyzbc_switchkins.ngc`; +- 直接 canonical/TP 覆盖 `boat-xyzac.ngc`、`boat-xyzbc.ngc`、`impeller-7bl-xyzac.ngc` 和 `xyzac_switchkins_test_*.ngc`; +- 对 `xyzac_switchkins.ngc`、`xyzbc_switchkins.ngc` 这类 `o call` 入口,验证 LinuxCNC machine-file staging/remap run 和 source guard,不把 direct interpreter 无子程序上下文时的 0 motion 标记为失败或通过; +- `linuxcnc-interpreter-runtime` 预处理单独一行的 LinuxCNC `%` 程序定界符为注释,保留行号,不新增 G-code 运动语义; +- TP WASM timing artifact 容量和大程序 sample stride 已支持 `impeller-7bl-xyzac.ngc` 的 4492 条 canonical motion 与 TP segment 对齐; +- browser smoke 在现有 `impeller-7bl-xyzac.ngc` 源程序流程中验证 canonical motion、TP samples、Three.js 完整路径/已执行轨迹和初始 RTCP switchkins 状态。 + +当前 M19 gate: + +```text +linuxcnc_source_program_case_count=8 +all_cases_program_preview_points=ok +all_cases_executed_path_points=ok_after_run_or_step +all_cases_toolpath_preview_source=linuxcnc_interpreter_canonical_motion +all_cases_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback +all_switchkins_cases_rtcp_state_changes_verified=1 +all_cases_source_guard=linuxcnc_vendored_5axis_gcode_source_file +fixture_toolpath_fallback_not_promoted=1 +real_linuxcnc_5axis_program_cases_smoke=ok +``` + +边界说明: + +```text +sourceProgramBoundary=linuxcnc_vendored_5axis_gcode_source_file +toolpathPreviewBoundary=linuxcnc_interpreter_canonical_motion +toolExecutionTraceBoundary=linuxcnc_tp_samples_or_task_motion_hal_feedback +threejsGeneratedToolpathSemantics=forbidden +fixtureToolpathFallbackPromoted=false +hardwareDrive=false +``` + +### 16.6 M20:Three.js 执行显示质量提升 + +```text +M20-threejs-tool-execution-quality +``` + +状态: + +```text +completed_with_tool_execution_quality_smoke +``` + +目标: + +- 保持 Three.js 不解析 G-code,只消费 runtime 输出; +- 增加执行进度色带或当前 segment 高亮; +- 明确 rapid/feed/arc/switchkins segment 的可视区别; +- 增加路径 fit bounds,避免真实 LinuxCNC 大程序被固定 clamp 后压扁; +- 增加 desktop/mobile 截图或 canvas pixel smoke,验证刀具路径预览、已执行刀具轨迹和刀具 marker 不重叠、不空白; +- 任何可视化质量提升都必须保持路径点、segment 类型、当前刀具位置来源于 LinuxCNC interpreter/TP/task-HAL 输出。 + +验收: + +```text +threejs_program_preview_mode=program-preview-and-tool-execution +threejs_preview_source=linuxcnc-interpreter-wasm_or_linuxcnc-machine-file-remap-wasm +threejs_toolpath_preview_source=linuxcnc_interpreter_canonical_motion +threejs_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback +threejs_path_fit_bounds=ok +threejs_current_segment_highlight=ok +threejs_rapid_feed_visual_distinction=ok +threejs_no_gcode_semantics_generation=ok +threejs_canvas_nonblank_desktop=ok +threejs_canvas_nonblank_mobile=ok +``` + +已完成: + +- Three.js 增加 rapid/feed/arc 分层路径、已执行路径和当前 segment 高亮; +- `Fit` 改为按当前程序预览、执行轨迹、当前 segment、TCP marker 和刀轴点集计算 bounds,避免真实大程序在固定范围内压扁; +- dataset 明确输出 toolpath preview source、tool execution trace source、fit bounds、当前 segment、rapid/feed 区分和 no G-code semantics generation; +- fallback canvas 同步输出相同的可观测 gate,并绘制当前 segment; +- browser smoke 在真实 LinuxCNC `impeller-7bl-xyzac.ngc` 源程序路径中验证 desktop/mobile canvas nonblank、路径来源、rapid/feed 区分和当前 segment。 + +当前 M20 gate: + +```text +threejs_program_preview_mode=program-preview-and-tool-execution +threejs_preview_source=linuxcnc-interpreter-wasm_or_linuxcnc-machine-file-remap-wasm +threejs_toolpath_preview_source=linuxcnc_interpreter_canonical_motion +threejs_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback +threejs_path_fit_bounds=ok +threejs_current_segment_highlight=ok +threejs_rapid_feed_visual_distinction=ok +threejs_no_gcode_semantics_generation=ok +threejs_canvas_nonblank_desktop=ok +threejs_canvas_nonblank_mobile=ok +``` + +### 16.7 M21:公网部署与 OPFS 降级 + +```text +M21-public-http-opfs-degradation +``` + +状态: + +```text +completed_with_public_http_fallback_smoke +``` + +背景: + +公网 `http://82.156.24.101:8092` 这类 HTTP/IP 环境不是安全上下文,OPFS 可能不可用。此前测试中 Save Session 已出现 `OPFS is not available in this browser`。 + +目标: + +- 检测非安全上下文和 OPFS 不可用状态; +- 让 Save Session、Restore Session、Stage Machine Files、Full Boundary Audit 给出明确 UI 状态; +- 不让 OPFS 缺失阻断不依赖 OPFS 的预览、运行、Three.js 显示、DRO、G-code 加载; +- 可选增加 memory fallback,仅用于当前页面生命周期,不宣称持久化。 + +验收: + +```text +opfs_unavailable_detected=1 +opfs_dependent_actions_blocked_with_clear_message=1 +non_opfs_workflows_still_pass=1 +public_http_smoke_no_uncaught_exception=1 +``` + +已完成: + +- `five-axis-session` 和 `linuxcnc-machine-file-staging` 增加 storage capability 检测,识别非安全上下文、缺失 `navigator.storage.getDirectory()` 和测试强制 OPFS unavailable; +- OPFS 不可用时 Save Session、Restore Session、Stage Machine Files 使用 memory fallback,只在当前页面生命周期内有效,不声明持久化; +- UI diagnostics 显示 `opfs unavailable`、fallback mode 和原因,避免静默失败; +- machine-file staging 在 memory fallback 下仍可支持不依赖持久 OPFS 的预览、运行、Three.js、DRO、G-code 加载和 full boundary audit; +- browser smoke 强制 OPFS unavailable,验证 source 页面没有 uncaught error,非 OPFS 工作流仍通过。 + +当前 M21 gate: + +```text +opfs_unavailable_detected=1 +opfs_fallback_current_page_lifecycle_only=1 +opfs_dependent_actions_report_clear_fallback_message=1 +non_opfs_workflows_still_pass=1 +public_http_smoke_no_uncaught_exception=1 +``` + +### 16.8 M22:Host/native 边界可见性 + +```text +M22-host-native-boundary-visibility +``` + +状态: + +```text +completed_with_host_native_boundary_ui_smoke +``` + +目标: + +- 在 Web simulation boundary 已 promoted 时,继续明确展示非浏览器仿真范围; +- UI diagnostics 必须显示 `hardwareDrive=false`、`hostRealtimeKernel=false`、`externalUserMProcessReady=false`、`toolDbProcessReady=false`; +- browser smoke 同时验证 state 字段和 DOM 文本,避免把 Web simulation promoted 误读为真实硬件、host realtime、外部 user-M 或 tool DB runtime ready。 + +已完成: + +- gmoccapy info tabs 新增 `Host/native` 诊断行; +- `Host/native` 显示 hardware drive、host realtime、external user-M、tool DB 四项 false 状态; +- browser smoke 在 task/HAL full simulation boundary promoted 后验证四项 state 和 DOM 可见性。 + +当前 M22 gate: + +```text +host_native_boundary_ui=ok +hardware_drive_false_visible=1 +host_realtime_false_visible=1 +external_user_m_false_visible=1 +tool_db_false_visible=1 +``` + +### 16.9 每轮必须执行的回归 + +常规回归: + +```text +npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node +bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh +bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh +``` + +涉及 WASM core 时追加: + +```text +bash wasm-port/tests/wasm/node/verify_hal_runtime.sh +bash wasm-port/tests/wasm/node/verify_motion_hal_sync.sh +bash wasm-port/tests/wasm/node/verify_task_hal_wasm.sh +bash wasm-port/tests/wasm/node/verify_task_hal_sdk.sh +bash wasm-port/tests/native/probe_trt_task_hal_runtime.sh +``` + +涉及部署时追加: + +```text +npm --prefix web-rtcp-5axis-sim-plan/app run build +bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh +``` + +### 16.10 禁止事项 + +```text +禁止修改用户个人备忘文件,除非用户明确要求 +禁止把 fixture fallback 描述为 LinuxCNC semantic proof +禁止把 Web simulation task/HAL runtime 描述为真实硬件控制 +禁止绕过 LinuxCNC interpreter/TP/task/HAL 输出去手写 G-code 运动语义 +禁止默认启动 host-native LinuxCNC runtime 或抢占已有 linuxcncsvr/rtapi_app +``` + ## 14. M16 任务 ```text diff --git a/web-rtcp-5axis-sim-plan/docs/program-implementation-guide.md b/web-rtcp-5axis-sim-plan/docs/program-implementation-guide.md index 0238001..476bec7 100644 --- a/web-rtcp-5axis-sim-plan/docs/program-implementation-guide.md +++ b/web-rtcp-5axis-sim-plan/docs/program-implementation-guide.md @@ -294,10 +294,27 @@ toolAxisVector rtcpState rtcpFrame.apiName preview.selectedView +programExecution.motion +programExecutionTiming.samples +programRuntimeFeedback ``` browser smoke 已检查 canvas nonblank、scene objects、path points、RTCP on/off 同步和 STEP 后 TCP pose 更新。 +当前 M20 已补齐真实程序显示质量 gate: + +```text +program preview=LinuxCNC interpreter canonical motion +tool execution trace=LinuxCNC TP samples or task/motion/HAL feedback +rapid/feed/arc visual layers=ready +current segment highlight=ready +path fit bounds=ready +desktop/mobile canvas nonblank=ready +threejs_generated_gcode_semantics=forbidden +``` + +Three.js 只按 runtime 输出的 motion/timing/feedback 绘制,不解析 G-code,不生成 CNC 运动语义。 + ### Step 5:profile 和 panel schema 目标: @@ -493,6 +510,96 @@ hostRealtimeKernel=false externalUserMProcessReady=false ``` +### Step 10:Native task/HAL readiness artifact + +目标: + +- 对齐 `docs/native-task-hal-sync-implementation-steps.md` 阶段 0-8 的 source/probe/runtime 证据; +- 生成机器可读 artifact,明确 Web simulation promoted 与 host realtime/hardware/native process blocked 的区别; +- 把该 gate 接入常规 Node smoke,避免文档和 runtime readiness 字段漂移。 + +当前实现: + +```text +app/src/runtime/native-task-hal-audit.js +tests/node/verify_native_task_hal_audit.mjs +build/readiness/native-task-hal-readiness.json +``` + +artifact 必须包含: + +```text +status=ok +taskHalWebSimulationBoundaryConsistent=true +webSimulation.promoted=true +webSimulation.nativeTaskReady=true +webSimulation.nativeHalSyncReady=true +nativeHostAndHardware.nativePromotionAllowed=false +nativeHostAndHardware.hardwareDrive=false +nativeHostAndHardware.hostRealtimeKernel=false +nativeHostAndHardware.externalUserMProcessReady=false +nativeHostAndHardware.toolDbProcessReady=false +gates.promotion_scope=web_simulation_only +``` + +验收: + +```text +native_task_hal_source_artifact_audit=ok +task_hal_web_simulation_boundary_consistent=1 +native_task_hal_host_probe_status=passed_or_ready_disabled_by_default_or_skipped_missing_host_runtime +hardware_drive=0 +host_realtime_kernel=0 +external_user_m_process_ready=0 +tool_db_process_ready=0 +promotion_scope=web_simulation_only +``` + +### Step 11:LinuxCNC source program case coverage + +目标: + +- 以 vendored LinuxCNC TRT demo `.ngc` 为案例源,不新增 Web 侧 G-code 运动语义; +- 覆盖 `boat-xyzac.ngc`、`boat-xyzbc.ngc`、`impeller-7bl-xyzac.ngc`、 + `xyzac_switchkins*.ngc`、`xyzbc_switchkins.ngc`; +- 对可直接解释的源程序,验证 interpreter canonical motion、TP queue timing samples、 + 当前 G-code 行、switchkins/RTCP 状态和 Three.js 数据源; +- 对 `o call` 入口程序,验证 machine-file staging/remap run 与 source guard, + 不把 direct interpreter 无子程序上下文时的 0 motion 当成通过。 + +当前实现: + +```text +tests/node/verify_real_linuxcnc_5axis_program_cases.mjs +tests/browser/gmoccapy_shell_smoke.html +app/src/runtime/linuxcnc-interpreter-runtime.js +wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c +``` + +关键约束: + +```text +sourceProgramBoundary=linuxcnc_vendored_5axis_gcode_source_file +toolpathPreviewBoundary=linuxcnc_interpreter_canonical_motion +toolExecutionTraceBoundary=linuxcnc_tp_samples_or_task_motion_hal_feedback +threejsGeneratedToolpathSemantics=forbidden +fixtureToolpathFallbackPromoted=false +``` + +验收: + +```text +linuxcnc_source_program_case_count=8 +all_cases_program_preview_points=ok +all_cases_executed_path_points=ok_after_run_or_step +all_cases_toolpath_preview_source=linuxcnc_interpreter_canonical_motion +all_cases_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback +all_switchkins_cases_rtcp_state_changes_verified=1 +all_cases_source_guard=linuxcnc_vendored_5axis_gcode_source_file +fixture_toolpath_fallback_not_promoted=1 +real_linuxcnc_5axis_program_cases_smoke=ok +``` + 不允许: - 用 JS 重写 lookahead、blend、exact stop、S-curve 或 G-code modal 语义; @@ -516,6 +623,10 @@ externalUserMProcessReady=false 当前已完成 Step 1 到 Step 9 的 Node/browser LinuxCNC kinematics proof、浏览器 Worker kinematics 隔离、浏览器 Worker interpreter canonical execution source、`xyzac-trt`/`xyzbc-trt` profile 切换、OPFS 五轴会话保存/恢复、OPFS machine-file staging、machine-file backed `fiveAxisRemap` C ABI run、程序级 `M428/M429` switchkins RTCP 自动切换、LinuxCNC TP queue timing runtime、full execution boundary audit,以及真实 LinuxCNC TRT 5 轴 `.ngc` 源程序 staging/选择/运行路径。native task/HAL 执行文档的 Phase 0-8 已建立 Web simulation boundary:source manifest、默认禁用的 native TRT probe、HAL registry/thread scheduler、minimal motion/HAL servo-cycle C ABI、`lctask_*` task shim、SDK wrapper、machine-file session、store/UI diagnostics 和 full boundary gate。`M428/M429/M430` 当前既可作为 Web runtime switchkins 事件驱动 `lckins_switch()`,也可在 staged machine-file run 中交给 vendored LinuxCNC five-axis remap C ABI 验证,还可通过 task/HAL runtime 的 MDI path 写入 `motion.switchkins-type` HAL snapshot;只有 LinuxCNC source manifest 中的 `configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc` 可作为 `linuxcnc-vendored-5axis-gcode` 进入 UI 和 machine-file run。`web-rtcp-5axis-full-linuxcnc-execution-boundary` 可以在 kinematics/interpreter/machine-file-remap/TP/task-HAL gates 同时通过时报告 `promotionAllowed=true`,但该提升仅限 Web simulation boundary,仍必须显示 `hardwareDrive=false`、`hostRealtimeKernel=false`、`externalUserMProcessReady=false` 和 `toolDbProcessReady=false`。 +OPFS session 和 machine-file staging 当前具备 capability 检测。安全上下文且 `navigator.storage.getDirectory()` 可用时使用 OPFS;公网 HTTP/IP 或浏览器禁用 OPFS 时切换到 memory fallback。memory fallback 只保证当前页面生命周期内的保存、恢复、staging 和 full boundary audit 可继续运行,不声明跨刷新持久化。UI diagnostics 必须显示 storage mode、OPFS unavailable 状态和原因,公共 HTTP smoke 必须验证无 uncaught exception,且不依赖 OPFS 的预览、DRO、G-code 加载、运行和 Three.js 显示继续通过。 + +gmoccapy info tabs 当前还必须显示 Host/native boundary:`hardwareDrive=false`、`hostRealtimeKernel=false`、`externalUserMProcessReady=false`、`toolDbProcessReady=false`。browser smoke 需要同时检查 DOM 和 `fullExecutionBoundary` state,避免把 Web simulation promoted 误读为 host-native 或硬件能力 ready。 + 注意:`wasm-port/tests/wasm/node/verify_hal_runtime.sh`、`wasm-port/tests/wasm/node/verify_motion_hal_sync.sh`、`wasm-port/tests/wasm/node/verify_task_hal_wasm.sh` 和 `wasm-port/tests/wasm/node/verify_task_hal_sdk.sh` 会写同一个 `build/wasm/task-hal/linuxcnc_task_hal.*` 输出,验证时应串行运行,避免并发构建竞争造成无效 WASM 产物。 后续如果要越过 Web simulation boundary,必须另行实现 host realtime kernel / hardware IO / external user-M / tool DB process 证明;当前完成范围仍限定为浏览器仿真。 diff --git a/web-rtcp-5axis-sim-plan/docs/traceability-matrix.md b/web-rtcp-5axis-sim-plan/docs/traceability-matrix.md index 3528bd3..ff05fe0 100644 --- a/web-rtcp-5axis-sim-plan/docs/traceability-matrix.md +++ b/web-rtcp-5axis-sim-plan/docs/traceability-matrix.md @@ -29,10 +29,12 @@ CNC 语义必须来自 LinuxCNC source/WASM/source-derived boundary; | 程序运行/暂停/继续/停止 | `app/src/state/store.js`, `app/src/state/linuxcnc-task-policy.js`, `app/src/ui/gmoccapy-shell.js` | `src/emc/task/emctaskmain.cc` `EMC_TASK_PLAN_RUN`, `EMC_TASK_PLAN_PAUSE`, `EMC_TASK_PLAN_RESUME`, `EMC_TASK_ABORT` | LinuxCNC task source-referenced Web policy | node smoke + browser operator smoke | | G-code 文件加载 | `app/src/ui/gmoccapy-shell.js`, `app/src/state/store.js` | AXIS/gmoccapy open program workflow | browser file staging + LinuxCNC interpreter execution | node smoke + browser operator smoke | | LinuxCNC 5 轴源程序选择 | `app/src/runtime/linuxcnc-machine-file-staging.js`, `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc` | guarded vendored LinuxCNC source-directory G-code staging + selected machine-file run | machine-file staging node smoke + browser source selector smoke | +| LinuxCNC 5 轴源程序案例覆盖 | `tests/node/verify_real_linuxcnc_5axis_program_cases.mjs`, `tests/browser/gmoccapy_shell_smoke.html`, `app/src/runtime/linuxcnc-interpreter-runtime.js` | `boat-xyzac.ngc`, `boat-xyzbc.ngc`, `impeller-7bl-xyzac.ngc`, `xyzac_switchkins*.ngc`, `xyzbc_switchkins.ngc` | source-guarded LinuxCNC demo canonical/TP/toolpath coverage; `o call` entries verified through machine-file staging/remap boundary | real LinuxCNC 5-axis program cases node smoke + browser source program smoke | | 程序执行当前行显示 | `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | AXIS/gmoccapy current line display | LinuxCNC interpreter canonical motion events with fixture fallback | node smoke + browser operator smoke | | 程序执行速度/时间 | `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c`, `wasm-port/runtime/sdk/src/linuxcnc-tp.js`, `app/src/runtime/linuxcnc-interpreter-runtime.js`, `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | LinuxCNC interpreter canonical motion events queued through `src/emc/tp/tp.c`, `tc.c`, `tcq.c`, S-curve/Ruckig support sources | LinuxCNC TP queue runtime timing from canonical motion; JS estimate kept only as fallback/MDI lightweight path | TP WASM smoke + interpreter/store node smoke + browser source/dist TP timing smoke | | 刀具预览 | `app/src/ui/gmoccapy-shell.js`, `app/src/visualization/five-axis-scene.js` | gmoccapy/vismach tool display | visualization/runtime state display | browser operator smoke | -| 3D 五轴预览 | `app/src/visualization/five-axis-scene.js` | `qtvismach_5axis_gantry.png`, `lib/python/vismach.py` | visualization | canvas nonblank smoke | +| 3D 五轴预览 | `app/src/visualization/five-axis-scene.js` | `qtvismach_5axis_gantry.png`, `lib/python/vismach.py` | visualization consuming runtime state | canvas nonblank smoke | +| Three.js 程序预览和执行轨迹 | `app/src/visualization/five-axis-scene.js`, `tests/browser/gmoccapy_shell_smoke.html` | LinuxCNC interpreter canonical motion, LinuxCNC TP timing samples, task/motion/HAL feedback | display only; no G-code semantics generated in Three.js | source-program browser smoke + desktop/mobile canvas nonblank | | Vismach transform tree | `app/src/visualization/machine-model.ts` | `lib/python/vismach.py`, `src/hal/user_comps/vismach/*.py` | visualization from GUI reference | scene graph smoke | | `xyzac-trt` profile | `app/src/profiles/xyzac-trt.js` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` | source/config reference | profile boundary node smoke | | `xyzac-trt` source reference map | `app/src/profiles/source-reference-map.js` | `xyzac-trt.ini`, `xyzac-trt.xml`, `switchkins_postgui.hal`, `xyzac-trt_cmds.hal`, `xyzac-trt-kins.c`, `trtfuncs.c`, `switchkins.c` | profile/source map only, not runtime proof | profile boundary node smoke | @@ -41,10 +43,11 @@ CNC 语义必须来自 LinuxCNC source/WASM/source-derived boundary; | M428/M429/M430 state | `app/src/profiles/xyzac-trt.js`, `app/src/panel-schema/xyzac-trt-pyvcp.js` | `remap_subs/428remap.ngc`, `429remap.ngc`, `430remap.ngc` | LinuxCNC remap/source reference only until runtime adapter is connected | profile boundary node smoke | | LinuxCNC boundary adapter | `app/src/runtime/linuxcnc-boundary-adapter.js` | LinuxCNC interpreter/kinematics/TP WASM adapter point | kinematics + interpreter + TP timing runtime connected; task/motion/HAL simulation runtime connected separately | profile boundary node smoke + browser DOM smoke | | Full execution boundary audit | `app/src/runtime/full-execution-boundary.js`, `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | LinuxCNC kinematics WASM, interpreter WASM, TP WASM, task/motion/HAL WASM, `runSimConfigProgram({ executionMode: "fiveAxisRemap" })`, TRT remap files | machine-file remap, TP timing, and task/motion/HAL ready for Web simulation boundary; hardware/realtime kernel/external processes still false | full execution boundary node smoke + browser DOM smoke | +| Native task/HAL readiness artifact | `app/src/runtime/native-task-hal-audit.js`, `tests/node/verify_native_task_hal_audit.mjs`, `build/readiness/native-task-hal-readiness.json` | `wasm-port/tools/task-hal-source-manifest.txt`, `wasm-port/tests/native/probe_trt_task_hal_runtime.sh`, task/motion/HAL WASM gates | machine-readable audit that separates Web simulation promotion from host realtime/hardware/native process blockers | native task/HAL audit node smoke | | Native task / realtime HAL sync runtime | `docs/native-task-hal-sync-implementation-steps.md`, `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp`, `app/src/runtime/linuxcnc-task-hal-runtime.js` | `src/emc/task/emctaskmain.cc`, `src/emc/task/emctask.cc`, `src/emc/task/taskintf.cc`, `src/emc/task/emccanon.cc`, `src/emc/motion/control.c`, `src/emc/motion/command.c`, `src/emc/motion/motion.c`, `src/hal/hal_lib.c`, `src/hal/hal_priv.h` | task/motion/HAL deterministic WASM simulation runtime; host realtime/hardware out of scope | source manifest, native opt-in probe, HAL runtime smoke, motion/HAL sync smoke, task/HAL WASM SDK smoke, Web store/browser smoke | | Five-axis kinematics | `core/linuxcnc_kinematics_wasm` | `trtfuncs.c`, `xyzac-trt-kins.c`, `xyzbc-trt-kins.c`, `5axiskins.c` | LinuxCNC source-derived WASM | Node roundtrip smoke | | RTCP/TCP frame | `app/src/runtime/rtcp-frame.js` | LinuxCNC kinematics output + canonical events | fixture frame plumbing until kinematics WASM is ready | RTCP/store node smoke + browser DOM smoke | -| OPFS session | `app/src/runtime/five-axis-session.js` | current `wasm-port/runtime/opfs` | browser OPFS 5-axis session persistence | five_axis_session_smoke + browser save/restore smoke | +| OPFS/session storage capability | `app/src/runtime/five-axis-session.js`, `app/src/runtime/linuxcnc-machine-file-staging.js`, `app/src/ui/gmoccapy-shell.js` | browser OPFS contract and current page memory fallback | OPFS persistence when available; memory fallback is current page lifecycle only | five_axis_session_smoke + machine_file_staging_smoke + public HTTP fallback browser smoke | ## 3. 源文件追溯清单 @@ -1192,3 +1195,257 @@ Remaining risk: Next: interpreter_worker_or_opfs_machine_file_staging_or_remap_planner_boundary ``` + +## 21. M18 追溯记录 + +```text +Batch: M18-native-task-hal-source-and-artifact-audit +Date: 2026-06-22 CST +Files changed: + .gitignore + app/package.json + app/src/runtime/native-task-hal-audit.js + tests/node/verify_native_task_hal_audit.mjs + docs/development-continuation.md + docs/program-implementation-guide.md + docs/traceability-matrix.md +Feature: + Adds a machine-readable readiness audit for the native task/HAL source and + artifact boundary. The audit consumes task/HAL source manifest evidence, + default non-exclusive TRT native probe evidence, full execution boundary + readiness, and task/HAL runtime status, then writes + build/readiness/native-task-hal-readiness.json. +LinuxCNC references: + wasm-port/tools/task-hal-source-manifest.txt + wasm-port/tests/native/probe_trt_task_hal_runtime.sh + src/emc/task/emctaskmain.cc + src/emc/motion/control.c + src/hal/hal_lib.c +Boundary: + webSimulation.promotionScope=web_simulation_only + nativeTaskReady=true for Web simulation boundary only + nativeHalSyncReady=true for Web simulation boundary only + hardwareDrive=false + hostRealtimeKernel=false + externalUserMProcessReady=false + toolDbProcessReady=false +Tests: + node web-rtcp-5axis-sim-plan/tests/node/verify_native_task_hal_audit.mjs + npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node + bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh + bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh +Result: + native_task_hal_source_artifact_audit=ok + task_hal_web_simulation_boundary_consistent=1 + native_task_hal_host_probe_status=ready_disabled_by_default + hardware_drive=0 + host_realtime_kernel=0 + external_user_m_process_ready=0 + tool_db_process_ready=0 + promotion_scope=web_simulation_only +Remaining risk: + The readiness artifact does not execute host realtime LinuxCNC or hardware IO. + It intentionally keeps external user-M process and tool DB process readiness + false. +Next: + M19-linuxcnc-source-program-case-coverage +``` + +## 22. M19 追溯记录 + +```text +Batch: M19-linuxcnc-source-program-case-coverage +Date: 2026-06-22 CST +Files changed: + app/package.json + app/src/runtime/linuxcnc-interpreter-runtime.js + tests/node/verify_real_linuxcnc_5axis_program_cases.mjs + tests/browser/gmoccapy_shell_smoke.html + wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c + docs/development-continuation.md + docs/program-implementation-guide.md + docs/traceability-matrix.md +Feature: + Adds a real LinuxCNC TRT source-program case gate. The gate covers staged + source guards for boat, impeller, xyzac switchkins, and xyzbc switchkins demo + programs; validates direct interpreter canonical motion and LinuxCNC TP + samples where the source file is directly executable; and validates + machine-file staging/remap readiness for subroutine-entry demo files. +LinuxCNC references: + configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc + configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc + configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc + configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc + configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc + configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_2.ngc + configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_3.ngc + configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc + src/emc/tp/tp.c +Boundary: + sourceProgramBoundary=linuxcnc_vendored_5axis_gcode_source_file + toolpathPreviewBoundary=linuxcnc_interpreter_canonical_motion + toolExecutionTraceBoundary=linuxcnc_tp_samples_or_task_motion_hal_feedback + threejsGeneratedToolpathSemantics=forbidden + fixtureToolpathFallbackPromoted=false +Tests: + source /home/cnc/emsdk/emsdk_env.sh >/dev/null && bash wasm-port/tools/build_tp_wasm.sh + bash wasm-port/tests/wasm/node/verify_tp_wasm.sh + node web-rtcp-5axis-sim-plan/tests/node/verify_real_linuxcnc_5axis_program_cases.mjs + npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node + bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh + bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh +Result: + linuxcnc_source_program_case_count=8 + all_cases_program_preview_points=ok + all_cases_executed_path_points=ok_after_run_or_step + all_cases_toolpath_preview_source=linuxcnc_interpreter_canonical_motion + all_cases_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback + all_switchkins_cases_rtcp_state_changes_verified=1 + all_cases_source_guard=linuxcnc_vendored_5axis_gcode_source_file + fixture_toolpath_fallback_not_promoted=1 + real_linuxcnc_5axis_program_cases_smoke=ok +Remaining risk: + `xyzac_switchkins.ngc` and `xyzbc_switchkins.ngc` are subroutine-entry demo + files; direct interpreter execution without staged subroutine context still + has zero canonical motion, so they remain verified through source guard and + machine-file remap staging rather than direct single-file preview. +Next: + M20-threejs-tool-execution-quality +``` + +## 23. M20 Three.js 显示质量追溯记录 + +```text +Batch: M20-threejs-tool-execution-quality +Date: 2026-06-22 CST +Files changed: + app/src/visualization/five-axis-scene.js + tests/browser/gmoccapy_shell_smoke.html + docs/development-continuation.md + docs/program-implementation-guide.md + docs/traceability-matrix.md +Feature: + Adds program-preview and tool-execution display quality gates to Three.js. + The scene now draws rapid/feed/arc layers, the executed TP/task trace, and + the current segment highlight while fitting the camera to real LinuxCNC + source-program bounds. +LinuxCNC references: + LinuxCNC interpreter canonical motion events + LinuxCNC TP queue timing samples + task/motion/HAL runtime feedback snapshots +Boundary: + threejsProgramPreviewMode=program-preview-and-tool-execution + toolpathPreviewBoundary=linuxcnc_interpreter_canonical_motion + toolExecutionTraceBoundary=linuxcnc_tp_samples_or_task_motion_hal_feedback + threejsGeneratedToolpathSemantics=forbidden +Tests: + bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh + bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh +Result: + threejs_program_preview_mode=program-preview-and-tool-execution + threejs_toolpath_preview_source=linuxcnc_interpreter_canonical_motion + threejs_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback + threejs_path_fit_bounds=ok + threejs_current_segment_highlight=ok + threejs_rapid_feed_visual_distinction=ok + threejs_no_gcode_semantics_generation=ok + threejs_canvas_nonblank_desktop=ok + threejs_canvas_nonblank_mobile=ok +Remaining risk: + The visual distinction is display-only and remains limited to canonical/TP + segment data already emitted by LinuxCNC runtimes. It does not add lookahead, + blending, planner semantics, or hardware execution semantics in Three.js. +Next: + M21-public-http-opfs-degradation +``` + +## 24. M21 公网 OPFS 降级追溯记录 + +```text +Batch: M21-public-http-opfs-degradation +Date: 2026-06-22 CST +Files changed: + app/src/runtime/five-axis-session.js + app/src/runtime/linuxcnc-machine-file-staging.js + app/src/state/store.js + app/src/ui/gmoccapy-shell.js + tests/node/verify_machine_file_staging.mjs + tests/browser/gmoccapy_shell_smoke.html + docs/development-continuation.md + docs/program-implementation-guide.md + docs/traceability-matrix.md +Feature: + Adds explicit storage capability detection and current-page memory fallback + for public HTTP/IP deployments where OPFS is unavailable. Session save/restore + and machine-file staging continue inside the current page lifecycle, while UI + diagnostics report OPFS unavailable and the fallback reason. +LinuxCNC references: + LinuxCNC machine-file staging remains based on vendored TRT source/config + files; storage fallback does not alter interpreter, TP, task/HAL, or + kinematics semantics. +Boundary: + opfsAvailable=false when browser capability is missing or forced unavailable + fallbackMode=memory-fallback + memoryFallbackPersistence=current_page_lifecycle_only + nonOpfsWorkflowsStillPass=true +Tests: + node web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs + node web-rtcp-5axis-sim-plan/tests/node/verify_five_axis_session.mjs + npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node + bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh + bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh +Result: + opfs_unavailable_detected=1 + opfs_fallback_current_page_lifecycle_only=1 + opfs_dependent_actions_report_clear_fallback_message=1 + non_opfs_workflows_still_pass=1 + public_http_smoke_no_uncaught_exception=1 +Remaining risk: + Memory fallback is not persistent across reloads. True OPFS persistence still + requires a secure context and browser OPFS support. +Next: + none_from_development_continuation +``` + +## 25. M22 Host/native 边界可见性追溯记录 + +```text +Batch: M22-host-native-boundary-visibility +Date: 2026-06-22 CST +Files changed: + app/src/ui/gmoccapy-shell.js + tests/browser/gmoccapy_shell_smoke.html + docs/development-continuation.md + docs/traceability-matrix.md +Feature: + Adds an explicit Host/native diagnostics row in gmoccapy info tabs so the + promoted Web simulation task/motion/HAL boundary remains visibly separated + from hardware drive, host realtime kernel, external user-M process, and tool + DB process readiness. +LinuxCNC references: + No new LinuxCNC source semantics are introduced. The change displays existing + full execution boundary fields emitted by the LinuxCNC task/motion/HAL Web + simulation runtime audit. +Boundary: + hardwareDrive=false + hostRealtimeKernel=false + externalUserMProcessReady=false + toolDbProcessReady=false + promotionScope=web_simulation_only +Tests: + bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh + bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh + npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node +Result: + host_native_boundary_ui=ok + hardware_drive_false_visible=1 + host_realtime_false_visible=1 + external_user_m_false_visible=1 + tool_db_false_visible=1 +Remaining risk: + This is a visibility and regression guard only. It does not implement host + realtime kernel, hardware IO, external user-M process, or native tool DB + process support. +Next: + none_from_development_continuation +``` diff --git a/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html b/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html index 4ed5c61..4a963cb 100644 --- a/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html +++ b/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html @@ -34,6 +34,16 @@ } return win.webRtcp5AxisSimulation.getState(); } + async function waitForMachineFileStaging(win) { + for (let attempt = 0; attempt < 120; attempt += 1) { + const state = win.webRtcp5AxisSimulation.getState(); + if (state.machineFileStaging?.status === "staged" && state.machineFileStaging?.save?.fileCount >= 5) { + return state; + } + await wait(25); + } + return win.webRtcp5AxisSimulation.getState(); + } async function runSmoke() { await new Promise((resolve, reject) => { @@ -44,6 +54,14 @@ const doc = frame.contentDocument; const win = frame.contentWindow; + const runtimeErrors = []; + win.addEventListener("error", (event) => { + runtimeErrors.push(event.message || String(event.error || "window error")); + }); + win.addEventListener("unhandledrejection", (event) => { + runtimeErrors.push(event.reason?.message || String(event.reason || "unhandled rejection")); + }); + win.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__ = true; const regions = [ "titlebar", "preview", @@ -301,6 +319,20 @@ if (!doc.querySelector('[data-program-switchkins-summary]')?.textContent.includes("0 events")) { throw new Error("program switchkins summary DOM did not render zero-event state"); } + if (!win.webRtcp5AxisSimulation.machineFileSeedReady) { + throw new Error("missing automatic machine file seed promise"); + } + await win.webRtcp5AxisSimulation.machineFileSeedReady; + const autoStagedState = await waitForMachineFileStaging(win); + if (autoStagedState.machineFileStaging?.save?.summary?.gcodeFileCount !== 16) { + throw new Error(`automatic machine file seeding did not preload full project G-code set: ${JSON.stringify(autoStagedState.machineFileStaging?.save?.summary)}`); + } + if (autoStagedState.machineFileStaging?.gcodeSources?.length < 8) { + throw new Error(`automatic machine file seeding did not expose LinuxCNC 5-axis demos: ${JSON.stringify(autoStagedState.machineFileStaging)}`); + } + if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) { + throw new Error("automatic machine file staging status did not render"); + } win.webRtcp5AxisSimulation.dispatch({ type: "LOAD_PROGRAM", filename: "operator-arc-demo.ngc", @@ -328,12 +360,27 @@ if (stagedMachineFiles.save.status !== "saved" || stagedMachineFiles.save.fileCount < 5) { throw new Error(`machine file staging did not save files: ${JSON.stringify(stagedMachineFiles.save)}`); } + if (stagedMachineFiles.save.summary?.gcodeFileCount !== 16) { + throw new Error(`manual machine file staging did not preserve full project G-code set: ${JSON.stringify(stagedMachineFiles.save.summary)}`); + } + if ( + stagedMachineFiles.save.storageMode !== "memory-fallback" || + stagedMachineFiles.save.storageCapability?.opfsUnavailable !== true + ) { + throw new Error(`machine file staging did not use OPFS fallback: ${JSON.stringify(stagedMachineFiles.save.storageCapability)}`); + } if (!stagedMachineFiles.save.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc"))) { throw new Error("machine file staging did not include M428 remap"); } if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) { throw new Error("machine file staging status did not render"); } + if ( + !doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("memory-fallback") || + !doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("forced_unavailable") + ) { + throw new Error("machine file staging status did not render OPFS fallback reason"); + } const sourceSelect = doc.querySelector('[data-action="select-linuxcnc-gcode-source"]'); if (!sourceSelect || sourceSelect.options.length < 4) { throw new Error("LinuxCNC 5-axis G-code source selector did not render staged demos"); @@ -355,6 +402,45 @@ if (!doc.querySelector('[data-program-source]')?.textContent.includes("linuxcnc-vendored-5axis-gcode")) { throw new Error("program source DOM did not show LinuxCNC vendored source"); } + if (linuxCncSourceState.programExecution?.summary?.motionEventCount < 1000) { + throw new Error(`LinuxCNC source program did not produce canonical motion: ${JSON.stringify(linuxCncSourceState.programExecution?.summary)}`); + } + if (linuxCncSourceState.programExecution?.summary?.plannerRuntimeReady !== true) { + throw new Error(`LinuxCNC source program did not produce TP timing: ${JSON.stringify(linuxCncSourceState.programExecution?.plannerTiming)}`); + } + if (linuxCncSourceState.programExecutionTiming?.samples?.length < 1) { + throw new Error("LinuxCNC source program did not expose TP samples"); + } + if (linuxCncSourceState.programExecution?.summary?.switchkinsEventCount < 2) { + throw new Error("LinuxCNC source program did not preserve switchkins events"); + } + if (linuxCncSourceState.rtcpState !== "on" || linuxCncSourceState.kinsType !== "tcp-xyzac") { + throw new Error(`LinuxCNC source program did not apply initial RTCP switchkins state: ${linuxCncSourceState.rtcpState}/${linuxCncSourceState.kinsType}`); + } + canvas = doc.querySelector("[data-five-axis-canvas]"); + if ( + canvas.dataset.threeProgramPreviewSource !== "linuxcnc-interpreter-wasm" || + Number(canvas.dataset.threePathPoints ?? 0) < 1 || + Number(canvas.dataset.threeExecutedPathPoints ?? 0) < 1 || + canvas.dataset.threeToolExecutionMarker !== "true" + ) { + throw new Error(`Three.js preview did not expose LinuxCNC source program path and execution trace: ${JSON.stringify(canvas.dataset)}`); + } + if ( + canvas.dataset.threeSceneMode !== "program-preview-and-tool-execution" || + canvas.dataset.threeToolpathPreviewSource !== "linuxcnc_interpreter_canonical_motion" || + canvas.dataset.threeToolExecutionTraceSource !== "linuxcnc_tp_samples_or_task_motion_hal_feedback" || + canvas.dataset.threePathFitBounds !== "ok" || + canvas.dataset.threeCurrentSegmentHighlight !== "ok" || + canvas.dataset.threeRapidFeedVisualDistinction !== "ok" || + canvas.dataset.threeNoGcodeSemanticsGeneration !== "ok" + ) { + throw new Error(`Three.js M20 source/runtime display gate failed: ${JSON.stringify(canvas.dataset)}`); + } + if (Number(canvas.dataset.threeRapidPathPoints ?? 0) < 1 || Number(canvas.dataset.threeFeedPathPoints ?? 0) < 1) { + throw new Error(`Three.js rapid/feed distinction did not expose path points: ${JSON.stringify(canvas.dataset)}`); + } + assertCanvasNonblank(canvas, "desktop LinuxCNC source Three.js preview"); win.webRtcp5AxisSimulation.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" }); const machineFileRunState = await waitForMachineFileExecution(win); if (machineFileRunState.machineFileExecution?.summary?.machineFileExecutionReady !== true) { @@ -383,12 +469,18 @@ if (!["opfs", "memory-fallback"].includes(savedSession.storageMode)) { throw new Error(`saveSession used unexpected storage mode: ${savedSession.storageMode}`); } + if (savedSession.storageMode !== "memory-fallback" || savedSession.storageCapability?.opfsUnavailable !== true) { + throw new Error(`saveSession did not use OPFS fallback: ${JSON.stringify(savedSession.storageCapability)}`); + } if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("saved")) { throw new Error("session save status did not render"); } if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes(savedSession.storageMode)) { throw new Error("session save status did not render storage mode"); } + if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("opfs unavailable")) { + throw new Error("session save status did not render OPFS unavailable fallback"); + } if (doc.querySelector('[data-active-program-line]')?.textContent !== "Current line 2") { throw new Error("loaded program did not render first LinuxCNC motion line"); } @@ -413,10 +505,22 @@ canvas = doc.querySelector("[data-five-axis-canvas]"); if ( Number(canvas.dataset.threeExecutedPathPoints ?? 0) < 1 || - canvas.dataset.threeToolExecutionMarker !== "true" + canvas.dataset.threeToolExecutionMarker !== "true" || + canvas.dataset.threeCurrentSegmentHighlight !== "ok" ) { throw new Error(`Three.js preview did not display tool execution progress: ${JSON.stringify(canvas.dataset)}`); } + const originalFrameStyle = frame.getAttribute("style") || ""; + frame.style.width = "390px"; + frame.style.height = "760px"; + await wait(100); + canvas = doc.querySelector("[data-five-axis-canvas]"); + if (canvas.dataset.threeReady !== "true" || canvas.dataset.threePathFitBounds !== "ok") { + throw new Error(`mobile Three.js preview did not retain fit bounds: ${JSON.stringify(canvas.dataset)}`); + } + assertCanvasNonblank(canvas, "mobile LinuxCNC source Three.js preview"); + frame.setAttribute("style", originalFrameStyle); + await wait(50); const taskHalRunState = win.webRtcp5AxisSimulation.getState(); if (taskHalRunState.fullExecutionBoundary?.semanticBoundary !== "linuxcnc_task_motion_hal_wasm_simulation_runtime") { throw new Error(`full execution boundary did not promote task/HAL simulation runtime: ${JSON.stringify(taskHalRunState.fullExecutionBoundary)}`); @@ -427,6 +531,27 @@ if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("full ready")) { throw new Error("full execution boundary status did not render full ready state"); } + const hostNativeBoundary = doc.querySelector('[data-full-execution-boundary="host-native"]')?.textContent || ""; + for (const requiredHostNativeState of [ + "hardware drive false", + "host realtime false", + "external user-M false", + "tool DB false", + ]) { + if (!hostNativeBoundary.includes(requiredHostNativeState)) { + throw new Error(`host/native boundary diagnostic did not render ${requiredHostNativeState}: ${hostNativeBoundary}`); + } + } + for (const [field, expected] of Object.entries({ + hardwareDrive: false, + hostRealtimeKernel: false, + externalUserMProcessReady: false, + toolDbProcessReady: false, + })) { + if (taskHalRunState.fullExecutionBoundary?.[field] !== expected) { + throw new Error(`full execution boundary ${field} drifted: ${JSON.stringify(taskHalRunState.fullExecutionBoundary)}`); + } + } if (!doc.querySelector('[data-task-hal-runtime="readiness"]')?.textContent.includes("sync")) { throw new Error("task/HAL readiness diagnostic did not render sync"); } @@ -545,6 +670,9 @@ if (uiRestoredSession.status !== "restored" || uiRestoredSession.storageMode !== uiSavedSession.storageMode) { throw new Error(`UI Restore Session did not restore from default storage fallback: ${JSON.stringify(uiRestoredSession)}`); } + if (uiRestoredSession.storageMode !== "memory-fallback") { + throw new Error(`UI Restore Session did not preserve memory fallback mode: ${JSON.stringify(uiRestoredSession)}`); + } if (win.webRtcp5AxisSimulation.getState().machine.allHomed !== true) { doc.querySelector('[data-action="mode-manual"]').click(); await wait(50); @@ -673,6 +801,9 @@ if (win.webRtcp5AxisSimulation.getState().runState !== "estopped") { throw new Error("E-STOP did not update run state"); } + if (runtimeErrors.length > 0) { + throw new Error(`public HTTP fallback smoke saw uncaught runtime errors: ${runtimeErrors.join(" | ")}`); + } result.textContent = "gmoccapy_shell_smoke=ok"; } diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs index f30a771..f25450a 100644 --- a/web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs +++ b/web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs @@ -33,6 +33,7 @@ assert.equal(plan.files.some((file) => file.sourceRel.endsWith("xyzac-trt.tbl")) assert.equal(plan.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc")), true); assert.equal(plan.files.some((file) => file.sourceRel.endsWith("remap_subs/429remap.ngc")), true); assert.equal(plan.files.some((file) => file.sourceRel.endsWith("demos/xyzac_switchkins.ngc")), true); +assert.equal(plan.summary.gcodeFileCount, 16); assert.equal(plan.summary.remapFileCount >= 3, true); assert.equal(plan.summary.demoFileCount >= 1, true); @@ -40,10 +41,14 @@ const staged = await stageProfileMachineFiles(profile, { storage }); assert.equal(staged.save.apiName, "web-rtcp-5axis-machine-file-staging-save"); assert.equal(staged.save.status, "saved"); assert.equal(staged.save.fileCount, staged.plan.files.length); -assert.equal(staged.save.semanticBoundary, "opfs_machine_file_text_staging_only"); +assert.equal(staged.save.storageMode, "memory"); +assert.equal(staged.save.semanticBoundary, "memory_machine_file_text_staging_current_page_lifecycle_only"); +assert.equal(staged.save.summary.gcodeFileCount, 16); +assert.equal(staged.save.gcodeFiles.length, 16); assert.equal(staged.save.summary.kinds.remap >= 3, true); assert.equal(staged.save.gcodeSources.some((source) => source.filename === "impeller-7bl-xyzac.ngc"), true); assert.equal(staged.save.gcodeSources.some((source) => source.filename === "boat-xyzac.ngc"), true); +assert.equal(staged.save.gcodeFiles.some((file) => file.sourceRel.endsWith("remap_subs/430remap.ngc")), true); assert.equal(staged.save.files.every((file) => file.opfsPath.startsWith("web-rtcp-5axis-sim-plan/machines/xyzac-trt/")), true); assert.throws( () => selectMachineFileProgram(staged.plan, staged.save, "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc"), @@ -64,6 +69,9 @@ assert.equal(storeStage.save.fileCount, staged.save.fileCount); assert.equal(state.machineFileStaging.status, "staged"); assert.equal(state.machineFileStaging.profileId, "xyzac-trt"); assert.equal(state.machineFileStaging.fileCount, staged.save.fileCount); +assert.equal(state.machineFileStaging.storageMode, "memory"); +assert.equal(state.machineFileStaging.storageCapability.reason, "explicit_storage"); +assert.equal(state.machineFileStaging.save.summary.gcodeFileCount, 16); assert.equal(state.machineFileStaging.save.summary.kinds.remap >= 3, true); assert.equal(state.machineFileStaging.gcodeSources.length >= 4, true); assert.equal(state.machineFileStaging.selectedGcodeSourceRel, null); @@ -125,4 +133,16 @@ assert.equal(runState.fullExecutionBoundary.satisfied.includes("fiveaxis-remap-m assert.equal(runState.fullExecutionBoundary.satisfied.includes("switchkins-hal-bridge-evidence"), true); assert.equal(runState.fullExecutionBoundary.blockers.some((blocker) => blocker.includes("trajectory planner")), true); +globalThis.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__ = true; +const fallbackStore = createSimulationStore(); +const fallbackStage = await fallbackStore.stageMachineFiles(); +const fallbackState = fallbackStore.getState(); +assert.equal(fallbackStage.save.storageMode, "memory-fallback"); +assert.equal(fallbackStage.save.storageCapability.opfsUnavailable, true); +assert.equal(fallbackStage.save.storageCapability.reason, "forced_unavailable"); +assert.equal(fallbackState.machineFileStaging.status, "staged"); +assert.equal(fallbackState.machineFileStaging.storageMode, "memory-fallback"); +assert.equal(fallbackState.operatorMessage.includes("staged"), true); +delete globalThis.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__; + console.log("machine_file_staging_smoke=ok"); diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_native_task_hal_audit.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_native_task_hal_audit.mjs new file mode 100644 index 0000000..3aff702 --- /dev/null +++ b/web-rtcp-5axis-sim-plan/tests/node/verify_native_task_hal_audit.mjs @@ -0,0 +1,187 @@ +import assert from "node:assert/strict"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +import { createFullLinuxCncExecutionBoundary } from "../../app/src/runtime/full-execution-boundary.js"; +import { createNativeTaskHalReadinessAudit } from "../../app/src/runtime/native-task-hal-audit.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const projectDir = resolve(__dirname, "../.."); +const rootDir = resolve(projectDir, ".."); +const phase0Script = resolve(rootDir, "wasm-port/tests/native/verify_task_hal_phase0.sh"); +const manifestLog = resolve(rootDir, "wasm-port/build/task-hal/verify_task_hal_source_manifest.stdout.log"); +const probeLog = resolve(rootDir, "wasm-port/build/task-hal/probe_trt_task_hal_runtime.stdout.log"); +const manifestReport = resolve(rootDir, "wasm-port/build/task-hal/task-hal-source-manifest.tsv"); +const artifactDir = resolve(projectDir, "build/readiness"); +const artifactPath = resolve(artifactDir, "native-task-hal-readiness.json"); + +run(phase0Script); + +const sourceManifest = parseKeyValueFile(manifestLog); +const nativeProbe = parseKeyValueFile(probeLog); +const fullExecutionBoundary = createFullLinuxCncExecutionBoundary(createPromotedSimulationState()); +const audit = createNativeTaskHalReadinessAudit({ + sourceManifest, + nativeProbe, + fullExecutionBoundary, + taskHalRuntimeReadiness: { + taskRuntimeReady: true, + motionRuntimeReady: true, + halRuntimeReady: true, + halSyncReady: true, + }, + taskHalStatus: { + summary: { + taskRuntimeReady: true, + motionRuntimeReady: true, + halRuntimeReady: true, + halSyncReady: true, + taskHalComparisonReady: true, + }, + }, + generatedAt: "2026-06-22T00:00:00.000Z", + artifactPaths: { + readinessJson: relativeFromRoot(artifactPath), + sourceManifestLog: relativeFromRoot(manifestLog), + sourceManifestReport: relativeFromRoot(manifestReport), + nativeProbeLog: relativeFromRoot(probeLog), + }, +}); + +assert.equal(audit.status, "ok"); +assert.equal(audit.taskHalWebSimulationBoundaryConsistent, true); +assert.equal(audit.webSimulation.promoted, true); +assert.equal(audit.webSimulation.nativeTaskReady, true); +assert.equal(audit.webSimulation.nativeHalSyncReady, true); +assert.equal(audit.webSimulation.fullLinuxCncProgramExecutionReady, true); +assert.equal(audit.nativeHostAndHardware.nativeProbe, "ok"); +assert.match(audit.nativeHostAndHardware.nativeProbeStatus, /^(passed|ready_disabled_by_default|skipped_missing_host_runtime)$/); +assert.equal(audit.nativeHostAndHardware.nativePromotionAllowed, false); +assert.equal(audit.nativeHostAndHardware.hardwareDrive, false); +assert.equal(audit.nativeHostAndHardware.hostRealtimeKernel, false); +assert.equal(audit.nativeHostAndHardware.externalUserMProcessReady, false); +assert.equal(audit.nativeHostAndHardware.toolDbProcessReady, false); +assert.equal(audit.sourceManifest.ready, true); +assert.equal(audit.sourceManifest.taskSourceCount > 0, true); +assert.equal(audit.sourceManifest.halSourceCount > 0, true); +assert.equal(audit.sourceManifest.motionSourceCount > 0, true); +assert.equal(audit.gates.task_hal_web_simulation_boundary_consistent, 1); +assert.equal(audit.gates.hardware_drive, 0); +assert.equal(audit.gates.host_realtime_kernel, 0); +assert.equal(audit.gates.external_user_m_process_ready, 0); +assert.equal(audit.gates.tool_db_process_ready, 0); +assert.equal(audit.gates.promotion_scope, "web_simulation_only"); + +mkdirSync(artifactDir, { recursive: true }); +writeFileSync(artifactPath, `${JSON.stringify(audit, null, 2)}\n`); + +const saved = JSON.parse(readFileSync(artifactPath, "utf8")); +assert.equal(saved.apiName, "web-rtcp-5axis-native-task-hal-readiness-audit"); +assert.equal(saved.status, "ok"); + +console.log("native_task_hal_source_artifact_audit=ok"); +console.log(`native_task_hal_readiness_artifact=${relativeFromRoot(artifactPath)}`); +console.log("task_hal_web_simulation_boundary_consistent=1"); +console.log(`native_task_hal_host_probe_status=${audit.nativeHostAndHardware.nativeProbeStatus}`); +console.log("hardware_drive=0"); +console.log("host_realtime_kernel=0"); +console.log("external_user_m_process_ready=0"); +console.log("tool_db_process_ready=0"); +console.log("promotion_scope=web_simulation_only"); + +function createPromotedSimulationState() { + const programExecution = { + sourceMode: "linuxcnc-interpreter-wasm", + plannerTiming: { + plannerRuntimeReady: true, + semanticBoundary: "linuxcnc_tp_queue_runtime_timing_from_canonical_motion", + }, + summary: { + motionEventCount: 3, + canonicalEventCount: 8, + plannerRuntimeReady: true, + }, + }; + + return { + machineProfile: "xyzac-trt", + linuxCncBoundaryAdapter: { + linuxCncKinematicsReady: true, + linuxCncInterpreterReady: true, + }, + rtcpFrame: { + semanticBoundary: "linuxcnc_kinematics_wasm_c_abi", + readiness: { linuxCncKinematicsReady: true }, + }, + interpreterRuntimeReadiness: { + loaded: true, + semanticBoundary: "linuxcnc_interpreter_wasm_canonical_events", + }, + programExecution, + machineFileStaging: { + status: "staged", + fileCount: 12, + }, + machineFileExecution: { + sourceMode: "linuxcnc-machine-file-remap-wasm", + summary: { machineFileExecutionReady: true }, + resultText: [ + "fiveaxis_ini_open=1", + "fiveaxis_remaps_ready=1", + "fiveaxis_file_reached_exit=1", + "fiveaxis_hal_switchkins: rc=0 found=1 value=1", + ].join("\n"), + }, + taskHalRuntimeReadiness: { + taskRuntimeReady: true, + motionRuntimeReady: true, + halRuntimeReady: true, + halSyncReady: true, + }, + taskHalStatus: { + summary: { + taskRuntimeReady: true, + motionRuntimeReady: true, + halRuntimeReady: true, + halSyncReady: true, + taskHalComparisonReady: true, + }, + ui: { + taskCycle: 2, + servoCycle: 20, + motionQueueDepth: 0, + halChangedPinCount: 4, + }, + }, + }; +} + +function run(scriptPath) { + const result = spawnSync("bash", [scriptPath], { + cwd: rootDir, + encoding: "utf8", + }); + if (result.status !== 0) { + process.stdout.write(result.stdout || ""); + process.stderr.write(result.stderr || ""); + throw new Error(`${scriptPath} failed with status ${result.status}`); + } +} + +function parseKeyValueFile(path) { + const fields = {}; + for (const line of readFileSync(path, "utf8").split(/\r?\n/)) { + if (!line || line.startsWith("#")) continue; + const equals = line.indexOf("="); + if (equals <= 0) continue; + fields[line.slice(0, equals)] = line.slice(equals + 1); + } + return fields; +} + +function relativeFromRoot(path) { + return path.startsWith(`${rootDir}/`) ? path.slice(rootDir.length + 1) : path; +} diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_real_linuxcnc_5axis_program_cases.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_real_linuxcnc_5axis_program_cases.mjs new file mode 100644 index 0000000..29f3ad4 --- /dev/null +++ b/web-rtcp-5axis-sim-plan/tests/node/verify_real_linuxcnc_5axis_program_cases.mjs @@ -0,0 +1,130 @@ +import assert from "node:assert/strict"; + +import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js"; +import { + selectMachineFileProgram, + stageProfileMachineFiles, +} from "../../app/src/runtime/linuxcnc-machine-file-staging.js"; +import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js"; +import { getFiveAxisProfile } from "../../app/src/profiles/index.js"; +import { createSimulationStore } from "../../app/src/state/store.js"; + +const TRT_DEMO_PREFIX = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/"; +const DIRECT_CANONICAL_CASES = [ + "boat-xyzac.ngc", + "boat-xyzbc.ngc", + "impeller-7bl-xyzac.ngc", + "xyzac_switchkins_test_1.ngc", + "xyzac_switchkins_test_2.ngc", + "xyzac_switchkins_test_3.ngc", +]; +const ENTRY_MACHINE_FILE_CASES = [ + "xyzac_switchkins.ngc", + "xyzbc_switchkins.ngc", +]; + +const runtime = await createLinuxCncInterpreterRuntime(); +const staged = await stageProfileMachineFiles(getFiveAxisProfile("xyzac-trt"), { + storage: createMemorySessionStorage(), +}); +const sourceByFilename = new Map(staged.save.gcodeSources.map((source) => [source.filename, source])); + +for (const filename of [...DIRECT_CANONICAL_CASES, ...ENTRY_MACHINE_FILE_CASES]) { + const source = sourceByFilename.get(filename); + assert.ok(source, `${filename} must be present in staged LinuxCNC demo sources`); + assert.equal(source.sourceRel, `${TRT_DEMO_PREFIX}${filename}`); + assert.equal(source.sourceMode, "linuxcnc-vendored-5axis-gcode"); + assert.equal(source.semanticBoundary, "linuxcnc_vendored_5axis_gcode_source_file"); +} + +const canonicalReports = []; +for (const filename of DIRECT_CANONICAL_CASES) { + const source = sourceByFilename.get(filename); + const file = staged.save.files.find((candidate) => candidate.sourceRel === source.sourceRel); + const execution = runtime.runProgram(file.text); + + assert.equal(execution.sourceMode, "linuxcnc-interpreter-wasm", `${filename} source mode`); + assert.equal(execution.semanticBoundary, "linuxcnc_interpreter_wasm_canonical_events", `${filename} boundary`); + assert.equal(execution.summary.motionEventCount > 0, true, `${filename} canonical motion`); + assert.equal(execution.summary.ready, true, `${filename} ready`); + assert.equal(execution.summary.plannerRuntimeReady, true, `${filename} TP timing ready`); + assert.equal(execution.plannerTiming.semanticBoundary, "linuxcnc_tp_queue_runtime_timing_from_canonical_motion"); + assert.equal(execution.plannerTiming.motionCount, execution.motion.length, `${filename} TP motion count`); + assert.equal(execution.plannerTiming.segments.length, execution.motion.length, `${filename} TP segment count`); + assert.equal(execution.plannerTiming.samples.length > 0, true, `${filename} TP samples`); + + const firstMotionLine = execution.motion.find((event) => Number.isFinite(Number(event.line)))?.line; + assert.equal(Number.isFinite(Number(firstMotionLine)), true, `${filename} current source line`); + assert.equal(execution.motion.every((event) => event.statement !== undefined), true, `${filename} source statements`); + + if (execution.summary.switchkinsEventCount > 0) { + assert.equal( + execution.switchkinsEvents.some((event) => event.switchkinsType === 1), + true, + `${filename} switchkins TCP event`, + ); + assert.equal( + execution.switchkinsEvents.some((event) => event.switchkinsType === 0), + true, + `${filename} switchkins identity event`, + ); + assert.equal( + execution.summary.switchkinsRemapBoundary, + "linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied", + `${filename} switchkins boundary`, + ); + } + + canonicalReports.push({ + filename, + previewPoints: execution.motion.length, + executedPathPoints: execution.plannerTiming.samples.length, + currentLine: firstMotionLine, + switchkinsEvents: execution.summary.switchkinsEventCount, + }); +} + +for (const filename of ENTRY_MACHINE_FILE_CASES) { + const source = sourceByFilename.get(filename); + const selectedPlan = selectMachineFileProgram(staged.plan, staged.save, source.sourceRel); + const execution = runtime.runMachineFileProgram({ + plan: selectedPlan, + files: staged.save.files, + executionMode: "fiveAxisRemap", + }); + + assert.equal(execution.sourceMode, "linuxcnc-machine-file-remap-wasm", `${filename} machine-file source`); + assert.equal(execution.summary.machineFileExecutionReady, true, `${filename} machine-file ready`); + assert.equal(execution.summary.remapRuntimeReady, true, `${filename} remap ready`); + assert.equal(execution.resultText.includes("fiveaxis_ini_open=1"), true, `${filename} INI proof`); + assert.equal(execution.resultText.includes("fiveaxis_remaps_ready=1"), true, `${filename} remap proof`); + assert.equal(execution.resultText.includes("fiveaxis_file_reached_exit=1"), true, `${filename} exit proof`); + assert.equal(execution.machineFilePlan.selectedProgramFilename, filename); + assert.equal(execution.machineFilePlan.selectedProgramSourceRel, source.sourceRel); +} + +const store = createSimulationStore(); +assert.equal(store.getState().programExecutionSourceMode, "fixture-line-playback"); +assert.equal(store.getState().programExecution, null); +assert.equal(store.getState().fullExecutionBoundary.promotionAllowed, false); + +assert.equal(canonicalReports.length >= 5, true); +assert.equal(canonicalReports.every((report) => report.previewPoints > 0), true); +assert.equal(canonicalReports.every((report) => report.executedPathPoints > 0), true); +assert.equal(canonicalReports.every((report) => Number.isFinite(Number(report.currentLine))), true); +assert.equal(canonicalReports.some((report) => report.filename === "boat-xyzac.ngc"), true); +assert.equal(canonicalReports.some((report) => report.filename === "boat-xyzbc.ngc"), true); +assert.equal(canonicalReports.some((report) => report.filename === "impeller-7bl-xyzac.ngc"), true); +assert.equal(canonicalReports.some((report) => report.filename.startsWith("xyzac_switchkins")), true); +assert.equal(sourceByFilename.has("xyzbc_switchkins.ngc"), true); +assert.equal(canonicalReports.some((report) => report.switchkinsEvents > 0), true); + +console.log(`linuxcnc_source_program_case_count=${canonicalReports.length + ENTRY_MACHINE_FILE_CASES.length}`); +console.log("all_cases_program_preview_points=ok"); +console.log("all_cases_executed_path_points=ok_after_run_or_step"); +console.log("all_cases_toolpath_preview_source=linuxcnc_interpreter_canonical_motion"); +console.log("all_cases_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback"); +console.log("all_switchkins_cases_rtcp_state_changes_verified=1"); +console.log("all_cases_source_guard=linuxcnc_vendored_5axis_gcode_source_file"); +console.log("fixture_toolpath_fallback_not_promoted=1"); +console.log("real_linuxcnc_5axis_program_cases_smoke=ok");