Add OPFS G-code staging and browser simulation checks
This commit is contained in:
@@ -510,7 +510,7 @@ static void append_runtime_sample(
|
|||||||
EMSCRIPTEN_KEEPALIVE
|
EMSCRIPTEN_KEEPALIVE
|
||||||
char *lctp_run_canonical_motion_timing(const char *json)
|
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);
|
char *out = (char *)malloc(out_size);
|
||||||
if (!out) {
|
if (!out) {
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -532,7 +532,7 @@ char *lctp_run_canonical_motion_timing(const char *json)
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum { MAX_EVENTS = 4096 };
|
enum { MAX_EVENTS = 12000 };
|
||||||
LctpMotionEvent *events = (LctpMotionEvent *)calloc(MAX_EVENTS, sizeof(LctpMotionEvent));
|
LctpMotionEvent *events = (LctpMotionEvent *)calloc(MAX_EVENTS, sizeof(LctpMotionEvent));
|
||||||
if (!events) {
|
if (!events) {
|
||||||
append(out, out_size, &offset, "{\"ok\":false,\"error\":\"failed to allocate motion events\"}\n");
|
append(out, out_size, &offset, "{\"ok\":false,\"error\":\"failed to allocate motion events\"}\n");
|
||||||
|
|||||||
1
web-rtcp-5axis-sim-plan/.gitignore
vendored
1
web-rtcp-5axis-sim-plan/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
app/dist/
|
app/dist/
|
||||||
|
build/
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"build": "node scripts/build-static.mjs",
|
"build": "node scripts/build-static.mjs",
|
||||||
"dev": "python3 -m http.server 4173",
|
"dev": "python3 -m http.server 4173",
|
||||||
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh",
|
"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": {},
|
"dependencies": {},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
|
|||||||
@@ -61,8 +61,11 @@ console.log("gmoccapy_static_build=ok");
|
|||||||
async function copyLinuxCncConfigAssets() {
|
async function copyLinuxCncConfigAssets() {
|
||||||
const configSrcDir = join(repoRoot, "wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting");
|
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 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(configDistDir, { recursive: true });
|
||||||
|
await mkdir(vendorConfigDistDir, { recursive: true });
|
||||||
await cp(configSrcDir, configDistDir, { recursive: true });
|
await cp(configSrcDir, configDistDir, { recursive: true });
|
||||||
|
await cp(configSrcDir, vendorConfigDistDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyLinuxCncManifest() {
|
async function copyLinuxCncManifest() {
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ const iniConfigReady = attachProfileIniConfig(store, store.getState().profile);
|
|||||||
const kinematicsRuntimeReady = attachDefaultKinematicsRuntime(store, store.getState().profile.kinematicsModuleId || store.getState().machineProfile);
|
const kinematicsRuntimeReady = attachDefaultKinematicsRuntime(store, store.getState().profile.kinematicsModuleId || store.getState().machineProfile);
|
||||||
const interpreterRuntimeReady = attachDefaultInterpreterRuntime(store);
|
const interpreterRuntimeReady = attachDefaultInterpreterRuntime(store);
|
||||||
const taskHalRuntimeReady = attachDefaultTaskHalRuntime(store);
|
const taskHalRuntimeReady = attachDefaultTaskHalRuntime(store);
|
||||||
|
const machineFileSeedReady = ensureMachineFilesForProfile(store);
|
||||||
let attachedKinematicsProfile = store.getState().machineProfile;
|
let attachedKinematicsProfile = store.getState().machineProfile;
|
||||||
let attachedIniProfile = store.getState().machineProfile;
|
let attachedIniProfile = store.getState().machineProfile;
|
||||||
|
let attachedMachineFileProfile = store.getState().machineProfile;
|
||||||
|
let machineFileSeedPromise = machineFileSeedReady;
|
||||||
store.subscribe((state) => {
|
store.subscribe((state) => {
|
||||||
if (state.machineProfile !== attachedIniProfile) {
|
if (state.machineProfile !== attachedIniProfile) {
|
||||||
attachedIniProfile = state.machineProfile;
|
attachedIniProfile = state.machineProfile;
|
||||||
@@ -31,6 +34,11 @@ store.subscribe((state) => {
|
|||||||
attachedKinematicsProfile = state.machineProfile;
|
attachedKinematicsProfile = state.machineProfile;
|
||||||
attachDefaultKinematicsRuntime(store, state.profile.kinematicsModuleId || state.machineProfile).catch(() => {});
|
attachDefaultKinematicsRuntime(store, state.profile.kinematicsModuleId || state.machineProfile).catch(() => {});
|
||||||
}
|
}
|
||||||
|
if (state.machineProfile !== attachedMachineFileProfile) {
|
||||||
|
attachedMachineFileProfile = state.machineProfile;
|
||||||
|
machineFileSeedPromise = ensureMachineFilesForProfile(store);
|
||||||
|
window.webRtcp5AxisSimulation.machineFileSeedReady = machineFileSeedPromise;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.webRtcp5AxisSimulation = {
|
window.webRtcp5AxisSimulation = {
|
||||||
@@ -46,6 +54,7 @@ window.webRtcp5AxisSimulation = {
|
|||||||
kinematicsRuntimeReady,
|
kinematicsRuntimeReady,
|
||||||
interpreterRuntimeReady,
|
interpreterRuntimeReady,
|
||||||
taskHalRuntimeReady,
|
taskHalRuntimeReady,
|
||||||
|
machineFileSeedReady,
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch({ type: "BOOT_READY" });
|
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") {
|
async function attachDefaultKinematicsRuntime(store, moduleId = "xyzac-trt") {
|
||||||
const sdkModuleUrls = [
|
const sdkModuleUrls = [
|
||||||
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js", import.meta.url).href,
|
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js", import.meta.url).href,
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export async function saveFiveAxisSessionSnapshot(sessionId, payload, options =
|
|||||||
const path = sessionSnapshotPath(sessionId, options.filename);
|
const path = sessionSnapshotPath(sessionId, options.filename);
|
||||||
const storage = resolveSessionStorage(options);
|
const storage = resolveSessionStorage(options);
|
||||||
await saveTextFile(path, `${JSON.stringify(snapshot, null, 2)}\n`, storage.storage);
|
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 = {}) {
|
export async function loadFiveAxisSessionSnapshot(sessionId, options = {}) {
|
||||||
@@ -103,7 +103,12 @@ export async function loadFiveAxisSessionSnapshot(sessionId, options = {}) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Invalid five-axis session snapshot JSON: ${error.message}`);
|
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) {
|
export function restoreFiveAxisSessionState(snapshot) {
|
||||||
@@ -150,24 +155,65 @@ export function createMemorySessionStorage(seed = {}) {
|
|||||||
|
|
||||||
let browserMemorySessionStorage = null;
|
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 = {}) {
|
function resolveSessionStorage(options = {}) {
|
||||||
if (options.storage) {
|
if (options.storage) {
|
||||||
return {
|
return {
|
||||||
storage: options.storage,
|
storage: options.storage,
|
||||||
mode: options.storageMode || storageModeFor(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;
|
const browserStorage = globalThis.navigator?.storage;
|
||||||
if (browserStorage?.getDirectory) {
|
if (capability.opfsAvailable) {
|
||||||
return {
|
return {
|
||||||
storage: browserStorage,
|
storage: browserStorage,
|
||||||
mode: "opfs",
|
mode: "opfs",
|
||||||
|
capability,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
browserMemorySessionStorage ??= createMemorySessionStorage();
|
browserMemorySessionStorage ??= createMemorySessionStorage();
|
||||||
return {
|
return {
|
||||||
storage: browserMemorySessionStorage,
|
storage: browserMemorySessionStorage,
|
||||||
mode: "memory-fallback",
|
mode: "memory-fallback",
|
||||||
|
capability,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,13 +200,14 @@ function runPlannerTiming(tpRuntime, motion) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const sampleStride = motion.length > 1000 ? Math.ceil(motion.length / 90) : 10;
|
||||||
return tpRuntime.sdk.runCanonicalMotionTiming({
|
return tpRuntime.sdk.runCanonicalMotionTiming({
|
||||||
motion,
|
motion,
|
||||||
options: {
|
options: {
|
||||||
cycleTime: 0.001,
|
cycleTime: 0.001,
|
||||||
queueSize: 32,
|
queueSize: 32,
|
||||||
maxCycles: 2000000,
|
maxCycles: 2000000,
|
||||||
sampleStride: 10,
|
sampleStride,
|
||||||
maxVelocity: 35,
|
maxVelocity: 35,
|
||||||
maxAcceleration: 500,
|
maxAcceleration: 500,
|
||||||
maxJerk: 1000,
|
maxJerk: 1000,
|
||||||
@@ -334,6 +335,9 @@ export function prepareLinuxCncProgramForRuntime(programText) {
|
|||||||
const switchkinsEvents = [];
|
const switchkinsEvents = [];
|
||||||
const runtimeLines = String(programText).split(/\r?\n/).map((line, index) => {
|
const runtimeLines = String(programText).split(/\r?\n/).map((line, index) => {
|
||||||
const lineNumber = index + 1;
|
const lineNumber = index + 1;
|
||||||
|
if (String(line).trim() === "%") {
|
||||||
|
return "(linuxcnc program delimiter)";
|
||||||
|
}
|
||||||
const events = readSwitchkinsEventsFromLine(line, lineNumber);
|
const events = readSwitchkinsEventsFromLine(line, lineNumber);
|
||||||
if (events.length === 0) return line;
|
if (events.length === 0) return line;
|
||||||
switchkinsEvents.push(...events);
|
switchkinsEvents.push(...events);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const DEFAULT_VENDOR_ROOT_URLS = [
|
|||||||
const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting";
|
const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting";
|
||||||
const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`;
|
const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`;
|
||||||
const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
|
const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
|
||||||
|
let browserMemoryMachineFileStorage = null;
|
||||||
|
|
||||||
export async function createMachineFileStagingPlan({
|
export async function createMachineFileStagingPlan({
|
||||||
profile,
|
profile,
|
||||||
@@ -79,6 +80,23 @@ export function listLinuxCncGcodeSources(save) {
|
|||||||
.sort((left, right) => left.filename.localeCompare(right.filename));
|
.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) {
|
export function selectMachineFileProgram(plan, save, sourceRel) {
|
||||||
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) {
|
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) {
|
||||||
throw new Error(`5-axis G-code source must come from LinuxCNC source demos: ${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") {
|
if (plan?.apiName !== "web-rtcp-5axis-machine-file-staging-plan") {
|
||||||
throw new Error("saveMachineFileStagingPlan requires a machine-file staging plan");
|
throw new Error("saveMachineFileStagingPlan requires a machine-file staging plan");
|
||||||
}
|
}
|
||||||
|
const storage = resolveMachineFileStorage(options);
|
||||||
const savedFiles = [];
|
const savedFiles = [];
|
||||||
for (const file of plan.files) {
|
for (const file of plan.files) {
|
||||||
const text = await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel));
|
const text = await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel));
|
||||||
await saveTextFile(file.opfsPath, text, options.storage);
|
await saveTextFile(file.opfsPath, text, storage.storage);
|
||||||
savedFiles.push({
|
savedFiles.push({
|
||||||
sourceRel: file.sourceRel,
|
sourceRel: file.sourceRel,
|
||||||
opfsPath: file.opfsPath,
|
opfsPath: file.opfsPath,
|
||||||
@@ -129,8 +148,11 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
|||||||
savedAt: new Date().toISOString(),
|
savedAt: new Date().toISOString(),
|
||||||
fileCount: savedFiles.length,
|
fileCount: savedFiles.length,
|
||||||
opfsRoot: `${OPFS_ROOT}/${plan.profileId}`,
|
opfsRoot: `${OPFS_ROOT}/${plan.profileId}`,
|
||||||
|
storageMode: storage.mode,
|
||||||
|
storageCapability: storage.capability,
|
||||||
files: savedFiles,
|
files: savedFiles,
|
||||||
gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }),
|
gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }),
|
||||||
|
gcodeFiles: listProjectGcodeFiles({ files: savedFiles }),
|
||||||
summary: summarizeSavedFiles(savedFiles),
|
summary: summarizeSavedFiles(savedFiles),
|
||||||
taskHalSession: {
|
taskHalSession: {
|
||||||
...(plan.taskHalSession || {}),
|
...(plan.taskHalSession || {}),
|
||||||
@@ -142,7 +164,9 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
|||||||
bytes: file.bytes,
|
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,
|
manifestUrl: options.manifestUrl,
|
||||||
wasmDir: options.wasmDir,
|
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 };
|
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) {
|
function summarizePlan(files) {
|
||||||
const kinds = countKinds(files.map((file) => classifySourceRel(file.sourceRel)));
|
const kinds = countKinds(files.map((file) => classifySourceRel(file.sourceRel)));
|
||||||
return {
|
return {
|
||||||
fileCount: files.length,
|
fileCount: files.length,
|
||||||
requiredFileCount: files.filter((file) => file.sourceRel.endsWith(".ini") || file.sourceRel.includes("/demos/")).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,
|
remapFileCount: kinds.remap || 0,
|
||||||
demoFileCount: kinds.demo || 0,
|
demoFileCount: kinds.demo || 0,
|
||||||
toolTableFileCount: kinds.toolTable || 0,
|
toolTableFileCount: kinds.toolTable || 0,
|
||||||
@@ -208,10 +312,12 @@ function isLinuxCncFiveAxisGcodeSourceRel(sourceRel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function summarizeSavedFiles(files) {
|
function summarizeSavedFiles(files) {
|
||||||
|
const kinds = countKinds(files.map((file) => file.kind));
|
||||||
return {
|
return {
|
||||||
fileCount: files.length,
|
fileCount: files.length,
|
||||||
totalBytes: files.reduce((total, file) => total + file.bytes, 0),
|
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),
|
opfsPaths: files.map((file) => file.opfsPath),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -266,7 +372,11 @@ async function readTextFromUrl(url) {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`failed to fetch machine staging asset ${url}: ${response.status}`);
|
throw new Error(`failed to fetch machine staging asset ${url}: ${response.status}`);
|
||||||
}
|
}
|
||||||
return response.text();
|
const text = await response.text();
|
||||||
|
if (/^\s*<!doctype html/i.test(text) || /^\s*<html[\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) {
|
async function saveTextFile(path, text, storage = globalThis.navigator?.storage) {
|
||||||
@@ -294,6 +404,34 @@ async function ensureParentDir(root, path) {
|
|||||||
return current;
|
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) {
|
function splitPath(path) {
|
||||||
const parts = String(path || "").replaceAll("\\", "/").split("/").filter(Boolean);
|
const parts = String(path || "").replaceAll("\\", "/").split("/").filter(Boolean);
|
||||||
if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
|
if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
|
||||||
|
|||||||
161
web-rtcp-5axis-sim-plan/app/src/runtime/native-task-hal-audit.js
Normal file
161
web-rtcp-5axis-sim-plan/app/src/runtime/native-task-hal-audit.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -60,6 +60,7 @@ const initialState = {
|
|||||||
status: "not-saved",
|
status: "not-saved",
|
||||||
path: null,
|
path: null,
|
||||||
storageMode: null,
|
storageMode: null,
|
||||||
|
storageCapability: null,
|
||||||
savedAt: null,
|
savedAt: null,
|
||||||
restoredAt: null,
|
restoredAt: null,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
@@ -70,6 +71,8 @@ const initialState = {
|
|||||||
profileId: null,
|
profileId: null,
|
||||||
fileCount: 0,
|
fileCount: 0,
|
||||||
opfsRoot: null,
|
opfsRoot: null,
|
||||||
|
storageMode: null,
|
||||||
|
storageCapability: null,
|
||||||
savedAt: null,
|
savedAt: null,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
plan: null,
|
plan: null,
|
||||||
@@ -632,6 +635,7 @@ export function createSimulationStore(seed = {}) {
|
|||||||
status: "saved",
|
status: "saved",
|
||||||
path: action.path,
|
path: action.path,
|
||||||
storageMode: action.storageMode || null,
|
storageMode: action.storageMode || null,
|
||||||
|
storageCapability: action.storageCapability || null,
|
||||||
savedAt: action.savedAt,
|
savedAt: action.savedAt,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
},
|
},
|
||||||
@@ -661,6 +665,19 @@ export function createSimulationStore(seed = {}) {
|
|||||||
setState({
|
setState({
|
||||||
...action.restoredState,
|
...action.restoredState,
|
||||||
profile: restoredProfile,
|
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,
|
linuxCncBoundaryAdapter: adapter,
|
||||||
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
|
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
|
||||||
sessionPersistence: {
|
sessionPersistence: {
|
||||||
@@ -668,6 +685,7 @@ export function createSimulationStore(seed = {}) {
|
|||||||
status: "restored",
|
status: "restored",
|
||||||
path: action.path,
|
path: action.path,
|
||||||
storageMode: action.storageMode || null,
|
storageMode: action.storageMode || null,
|
||||||
|
storageCapability: action.storageCapability || null,
|
||||||
restoredAt: action.restoredAt,
|
restoredAt: action.restoredAt,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
},
|
},
|
||||||
@@ -704,6 +722,8 @@ export function createSimulationStore(seed = {}) {
|
|||||||
profileId: action.plan.profileId,
|
profileId: action.plan.profileId,
|
||||||
fileCount: action.save.fileCount,
|
fileCount: action.save.fileCount,
|
||||||
opfsRoot: action.save.opfsRoot,
|
opfsRoot: action.save.opfsRoot,
|
||||||
|
storageMode: action.save.storageMode || null,
|
||||||
|
storageCapability: action.save.storageCapability || null,
|
||||||
savedAt: action.save.savedAt,
|
savedAt: action.save.savedAt,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
plan: action.plan,
|
plan: action.plan,
|
||||||
@@ -1407,7 +1427,7 @@ export function createSimulationStore(seed = {}) {
|
|||||||
const sessionId = options.sessionId || state.sessionPersistence.sessionId;
|
const sessionId = options.sessionId || state.sessionPersistence.sessionId;
|
||||||
const filename = options.filename || state.sessionPersistence.filename;
|
const filename = options.filename || state.sessionPersistence.filename;
|
||||||
const payload = createFiveAxisSessionPayload(state);
|
const payload = createFiveAxisSessionPayload(state);
|
||||||
const { snapshot, path, storageMode } = await saveFiveAxisSessionSnapshot(sessionId, payload, {
|
const { snapshot, path, storageMode, storageCapability } = await saveFiveAxisSessionSnapshot(sessionId, payload, {
|
||||||
filename,
|
filename,
|
||||||
storage: options.storage,
|
storage: options.storage,
|
||||||
storageMode: options.storageMode,
|
storageMode: options.storageMode,
|
||||||
@@ -1417,9 +1437,10 @@ export function createSimulationStore(seed = {}) {
|
|||||||
type: "SESSION_SAVE_COMPLETE",
|
type: "SESSION_SAVE_COMPLETE",
|
||||||
path,
|
path,
|
||||||
storageMode,
|
storageMode,
|
||||||
|
storageCapability,
|
||||||
savedAt: snapshot.createdAt,
|
savedAt: snapshot.createdAt,
|
||||||
});
|
});
|
||||||
return { snapshot, path, storageMode };
|
return { snapshot, path, storageMode, storageCapability };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({ type: "SESSION_PERSISTENCE_FAILED", error: error.message });
|
dispatch({ type: "SESSION_PERSISTENCE_FAILED", error: error.message });
|
||||||
throw error;
|
throw error;
|
||||||
@@ -1431,7 +1452,7 @@ export function createSimulationStore(seed = {}) {
|
|||||||
try {
|
try {
|
||||||
const sessionId = options.sessionId || state.sessionPersistence.sessionId;
|
const sessionId = options.sessionId || state.sessionPersistence.sessionId;
|
||||||
const filename = options.filename || state.sessionPersistence.filename;
|
const filename = options.filename || state.sessionPersistence.filename;
|
||||||
const { snapshot, path, storageMode } = await loadFiveAxisSessionSnapshot(sessionId, {
|
const { snapshot, path, storageMode, storageCapability } = await loadFiveAxisSessionSnapshot(sessionId, {
|
||||||
filename,
|
filename,
|
||||||
storage: options.storage,
|
storage: options.storage,
|
||||||
storageMode: options.storageMode,
|
storageMode: options.storageMode,
|
||||||
@@ -1441,10 +1462,11 @@ export function createSimulationStore(seed = {}) {
|
|||||||
restoredState: restoreFiveAxisSessionState(snapshot),
|
restoredState: restoreFiveAxisSessionState(snapshot),
|
||||||
path,
|
path,
|
||||||
storageMode,
|
storageMode,
|
||||||
|
storageCapability,
|
||||||
restoredAt: new Date().toISOString(),
|
restoredAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
await refreshAsyncKinematicsFrame({ operatorMessage: `5-axis session restored ${path}` });
|
await refreshAsyncKinematicsFrame({ operatorMessage: `5-axis session restored ${path}` });
|
||||||
return { snapshot, path, storageMode };
|
return { snapshot, path, storageMode, storageCapability };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({ type: "SESSION_PERSISTENCE_FAILED", error: error.message });
|
dispatch({ type: "SESSION_PERSISTENCE_FAILED", error: error.message });
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ function renderInfoTabs(element, state) {
|
|||||||
<dt>Canonical:</dt><dd data-program-execution-summary="${state.programExecution?.summary?.motionEventCount ?? 0}">${state.programExecution?.summary?.motionEventCount ?? 0} motion / ${state.programExecution?.summary?.canonicalEventCount ?? 0} events</dd>
|
<dt>Canonical:</dt><dd data-program-execution-summary="${state.programExecution?.summary?.motionEventCount ?? 0}">${state.programExecution?.summary?.motionEventCount ?? 0} motion / ${state.programExecution?.summary?.canonicalEventCount ?? 0} events</dd>
|
||||||
<dt>Switchkins:</dt><dd data-program-switchkins-summary="${state.programExecution?.summary?.switchkinsEventCount ?? 0}">${formatSwitchkinsSummary(state.programExecution)}</dd>
|
<dt>Switchkins:</dt><dd data-program-switchkins-summary="${state.programExecution?.summary?.switchkinsEventCount ?? 0}">${formatSwitchkinsSummary(state.programExecution)}</dd>
|
||||||
<dt>Machine run:</dt><dd data-machine-file-execution="status">${formatMachineFileExecution(state.machineFileExecution)}</dd>
|
<dt>Machine run:</dt><dd data-machine-file-execution="status">${formatMachineFileExecution(state.machineFileExecution)}</dd>
|
||||||
<dt>Session:</dt><dd data-session-persistence="status">${state.sessionPersistence.status} / ${state.sessionPersistence.storageMode ?? "-"} / ${state.sessionPersistence.path ?? "-"}</dd>
|
<dt>Session:</dt><dd data-session-persistence="status">${formatSessionPersistence(state.sessionPersistence)}</dd>
|
||||||
<dt>Tool preview:</dt><dd data-tool-preview="detail">T${state.toolPreview.toolNumber} D${formatNumber(state.toolPreview.diameter, 2)} L${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}</dd>
|
<dt>Tool preview:</dt><dd data-tool-preview="detail">T${state.toolPreview.toolNumber} D${formatNumber(state.toolPreview.diameter, 2)} L${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}</dd>
|
||||||
<dt>Program time:</dt><dd data-program-timing="summary">${formatProgramTiming(state)}</dd>
|
<dt>Program time:</dt><dd data-program-timing="summary">${formatProgramTiming(state)}</dd>
|
||||||
<dt>Segment time:</dt><dd data-program-timing="segment">${formatProgramTimingSegment(state)}</dd>
|
<dt>Segment time:</dt><dd data-program-timing="segment">${formatProgramTimingSegment(state)}</dd>
|
||||||
@@ -348,6 +348,7 @@ function renderInfoTabs(element, state) {
|
|||||||
<dt>Full boundary:</dt><dd data-full-execution-boundary="status">${formatFullExecutionBoundary(state.fullExecutionBoundary)}</dd>
|
<dt>Full boundary:</dt><dd data-full-execution-boundary="status">${formatFullExecutionBoundary(state.fullExecutionBoundary)}</dd>
|
||||||
<dt>Planner/task:</dt><dd data-full-execution-boundary="blockers">${formatFullExecutionBlockers(state.fullExecutionBoundary)}</dd>
|
<dt>Planner/task:</dt><dd data-full-execution-boundary="blockers">${formatFullExecutionBlockers(state.fullExecutionBoundary)}</dd>
|
||||||
<dt>Boundary evidence:</dt><dd data-full-execution-boundary="evidence">${formatFullExecutionEvidence(state.fullExecutionBoundary)}</dd>
|
<dt>Boundary evidence:</dt><dd data-full-execution-boundary="evidence">${formatFullExecutionEvidence(state.fullExecutionBoundary)}</dd>
|
||||||
|
<dt>Host/native:</dt><dd data-full-execution-boundary="host-native">${formatHostNativeBoundary(state.fullExecutionBoundary)}</dd>
|
||||||
<dt>LinuxCNC kins:</dt><dd data-rtcp-diagnostic="kinematics-ready">${frame.readiness.linuxCncKinematicsReady ? "ready" : "pending"}</dd>
|
<dt>LinuxCNC kins:</dt><dd data-rtcp-diagnostic="kinematics-ready">${frame.readiness.linuxCncKinematicsReady ? "ready" : "pending"}</dd>
|
||||||
<dt>Kins context:</dt><dd data-rtcp-diagnostic="execution-context">${state.kinematicsExecutionContext}</dd>
|
<dt>Kins context:</dt><dd data-rtcp-diagnostic="execution-context">${state.kinematicsExecutionContext}</dd>
|
||||||
<dt>Interpreter:</dt><dd data-linuxcnc-boundary="interpreter">${state.interpreterRuntimeReadiness?.loaded ? state.interpreterRuntimeReadiness.semanticBoundary : "pending"}</dd>
|
<dt>Interpreter:</dt><dd data-linuxcnc-boundary="interpreter">${state.interpreterRuntimeReadiness?.loaded ? state.interpreterRuntimeReadiness.semanticBoundary : "pending"}</dd>
|
||||||
@@ -427,7 +428,21 @@ function formatMachineFileStaging(machineFileStaging) {
|
|||||||
if (machineFileStaging.status === "error") {
|
if (machineFileStaging.status === "error") {
|
||||||
return `error ${machineFileStaging.lastError || "-"}`;
|
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) {
|
function formatMachineFileExecution(machineFileExecution) {
|
||||||
@@ -471,6 +486,16 @@ function formatFullExecutionEvidence(boundary) {
|
|||||||
return `${boundary.satisfied.length} satisfied / ${boundary.missing.length} missing / ${boundary.semanticBoundary}`;
|
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) {
|
function renderOverride(element, state, dispatch) {
|
||||||
element.innerHTML = `
|
element.innerHTML = `
|
||||||
<section class="meter-card">
|
<section class="meter-card">
|
||||||
|
|||||||
@@ -38,12 +38,17 @@ export function renderFiveAxisScene(canvas, state) {
|
|||||||
exposePreviewDataset(canvas, state, {
|
exposePreviewDataset(canvas, state, {
|
||||||
pointCount,
|
pointCount,
|
||||||
executedPointCount,
|
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),
|
sceneObjectCount: countSceneObjects(preview.scene),
|
||||||
toolhead: preview.currentToolhead,
|
toolhead: preview.currentToolhead,
|
||||||
renderer: "webgl",
|
renderer: "webgl",
|
||||||
sceneMode: "program-preview-and-tool-execution",
|
sceneMode: "program-preview-and-tool-execution",
|
||||||
cameraControls: preview.controls.enabled,
|
cameraControls: preview.controls.enabled,
|
||||||
toolExecutionMarker: preview.toolMarker.visible,
|
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 camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
||||||
|
|
||||||
const previewPath = createLine(0x808892, 0.56);
|
const previewPath = createLine(0x808892, 0.56);
|
||||||
|
const feedPath = createLine(0x4fb3ff, 0.92);
|
||||||
const executedPath = createLine(0x1ffff4, 1);
|
const executedPath = createLine(0x1ffff4, 1);
|
||||||
const rapidPath = createLine(0xffb13b, 0.82);
|
const rapidPath = createLine(0xffb13b, 0.82);
|
||||||
|
const arcPath = createLine(0xd7ff62, 0.95);
|
||||||
|
const currentSegmentPath = createLine(0xff4fd8, 1);
|
||||||
const toolMarker = new THREE.Mesh(
|
const toolMarker = new THREE.Mesh(
|
||||||
new THREE.SphereGeometry(0.065, 18, 12),
|
new THREE.SphereGeometry(0.065, 18, 12),
|
||||||
new THREE.MeshBasicMaterial({ color: 0x1ffff4 }),
|
new THREE.MeshBasicMaterial({ color: 0x1ffff4 }),
|
||||||
@@ -78,7 +86,7 @@ function createScene(canvas) {
|
|||||||
EMPTY_GEOMETRY.clone(),
|
EMPTY_GEOMETRY.clone(),
|
||||||
new THREE.LineBasicMaterial({ color: 0x1ffff4, transparent: true, opacity: 0.9 }),
|
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, () => {
|
const controls = createToolpathCameraControls(canvas, camera, () => {
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
@@ -90,13 +98,18 @@ function createScene(canvas) {
|
|||||||
camera,
|
camera,
|
||||||
controls,
|
controls,
|
||||||
previewPath,
|
previewPath,
|
||||||
|
feedPath,
|
||||||
executedPath,
|
executedPath,
|
||||||
rapidPath,
|
rapidPath,
|
||||||
|
arcPath,
|
||||||
|
currentSegmentPath,
|
||||||
toolMarker,
|
toolMarker,
|
||||||
toolAxis,
|
toolAxis,
|
||||||
currentToolhead: new THREE.Vector3(),
|
currentToolhead: new THREE.Vector3(),
|
||||||
lastSelectedView: null,
|
lastSelectedView: null,
|
||||||
lastCameraRevision: null,
|
lastCameraRevision: null,
|
||||||
|
lastFitKey: null,
|
||||||
|
pathFitBoundsReady: false,
|
||||||
};
|
};
|
||||||
resizeRenderer(preview);
|
resizeRenderer(preview);
|
||||||
resetCamera(preview, "iso");
|
resetCamera(preview, "iso");
|
||||||
@@ -133,6 +146,10 @@ function renderFallbackPreview(preview, state) {
|
|||||||
|
|
||||||
const previewPoints = buildProgramPreviewPoints(state);
|
const previewPoints = buildProgramPreviewPoints(state);
|
||||||
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
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 pointCount = previewPoints.length;
|
||||||
const executedPointCount = executedPoints.length;
|
const executedPointCount = executedPoints.length;
|
||||||
if (pointCount > 0) {
|
if (pointCount > 0) {
|
||||||
@@ -150,8 +167,24 @@ function renderFallbackPreview(preview, state) {
|
|||||||
drawFallbackPolyline(ctx, executedPoints, cx, cy, scale);
|
drawFallbackPolyline(ctx, executedPoints, cx, cy, scale);
|
||||||
ctx.stroke();
|
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 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) {
|
if (toolPosition) {
|
||||||
const toolX = cx + toolPosition.x * scale;
|
const toolX = cx + toolPosition.x * scale;
|
||||||
const toolY = cy - toolPosition.y * scale;
|
const toolY = cy - toolPosition.y * scale;
|
||||||
@@ -174,6 +207,11 @@ function renderFallbackPreview(preview, state) {
|
|||||||
sceneMode: "program-preview-and-tool-execution",
|
sceneMode: "program-preview-and-tool-execution",
|
||||||
cameraControls: false,
|
cameraControls: false,
|
||||||
toolExecutionMarker: Boolean(toolPosition),
|
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;
|
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.threeCameraControls = preview.cameraControls ? "orbit-pan-zoom" : "none";
|
||||||
canvas.dataset.threeProgramPreviewSource = previewSourceMode(state);
|
canvas.dataset.threeProgramPreviewSource = previewSourceMode(state);
|
||||||
canvas.dataset.threeToolExecutionMarker = preview.toolExecutionMarker ? "true" : "false";
|
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) {
|
function createLine(color, opacity) {
|
||||||
@@ -212,21 +261,29 @@ function updateToolpathPreview(preview, state) {
|
|||||||
const previewPoints = buildProgramPreviewPoints(state);
|
const previewPoints = buildProgramPreviewPoints(state);
|
||||||
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
||||||
const rapidPoints = buildRapidPreviewPoints(state);
|
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);
|
const toolPosition = executionToolPosition(state, previewPoints);
|
||||||
|
|
||||||
updateLineGeometry(preview.previewPath, previewPoints);
|
updateLineGeometry(preview.previewPath, previewPoints);
|
||||||
|
updateLineGeometry(preview.feedPath, feedPoints);
|
||||||
updateLineGeometry(preview.executedPath, executedPoints);
|
updateLineGeometry(preview.executedPath, executedPoints);
|
||||||
updateLineGeometry(preview.rapidPath, rapidPoints);
|
updateLineGeometry(preview.rapidPath, rapidPoints);
|
||||||
|
updateLineGeometry(preview.arcPath, arcPoints);
|
||||||
|
updateLineGeometry(preview.currentSegmentPath, currentSegmentPoints);
|
||||||
updateToolExecutionMarker(preview, state, toolPosition);
|
updateToolExecutionMarker(preview, state, toolPosition);
|
||||||
|
|
||||||
const cameraRevision = state.preview.cameraRevision ?? 0;
|
const cameraRevision = state.preview.cameraRevision ?? 0;
|
||||||
if (
|
if (
|
||||||
preview.lastSelectedView !== state.preview.selectedView ||
|
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.lastSelectedView = state.preview.selectedView;
|
||||||
preview.lastCameraRevision = cameraRevision;
|
preview.lastCameraRevision = cameraRevision;
|
||||||
|
preview.lastFitKey = fitKey;
|
||||||
} else {
|
} else {
|
||||||
applyCameraControls(preview.controls);
|
applyCameraControls(preview.controls);
|
||||||
}
|
}
|
||||||
@@ -285,15 +342,32 @@ function buildExecutedProgramPoints(state, previewPoints) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildRapidPreviewPoints(state) {
|
function buildRapidPreviewPoints(state) {
|
||||||
|
return buildTypedPreviewPoints(state, "STRAIGHT_TRAVERSE");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTypedPreviewPoints(state, type) {
|
||||||
const motion = state.programExecution?.motion;
|
const motion = state.programExecution?.motion;
|
||||||
if (!Array.isArray(motion) || state.preview.pathPoints === 0) return [];
|
if (!Array.isArray(motion) || state.preview.pathPoints === 0) return [];
|
||||||
return limitPoints(
|
return limitPoints(
|
||||||
motion
|
motion
|
||||||
.filter((event) => event.type === "STRAIGHT_TRAVERSE")
|
.filter((event) => event.type === type)
|
||||||
.map((event) => vectorFromAxes(event.axes)),
|
.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) {
|
function buildFixturePreviewPoints(pointCount, tcpPosition) {
|
||||||
const points = [];
|
const points = [];
|
||||||
for (let index = 0; index < pointCount; index += 1) {
|
for (let index = 0; index < pointCount; index += 1) {
|
||||||
@@ -367,6 +441,60 @@ function previewSourceMode(state) {
|
|||||||
return state.programExecutionSourceMode || "fixture-line-playback";
|
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) {
|
function drawFallbackPolyline(ctx, points, cx, cy, scale) {
|
||||||
for (let index = 0; index < points.length; index += 1) {
|
for (let index = 0; index < points.length; index += 1) {
|
||||||
const point = points[index];
|
const point = points[index];
|
||||||
@@ -513,15 +641,30 @@ function panCamera(controls, dx, dy, canvas) {
|
|||||||
controls.target.addScaledVector(up, dy * speed);
|
controls.target.addScaledVector(up, dy * speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetCamera(preview, selectedView) {
|
function resetCamera(preview, selectedView, fitPoints = []) {
|
||||||
const preset = CAMERA_PRESETS[selectedView] || CAMERA_PRESETS.iso;
|
const preset = CAMERA_PRESETS[selectedView] || CAMERA_PRESETS.iso;
|
||||||
preview.controls.theta = preset.theta;
|
preview.controls.theta = preset.theta;
|
||||||
preview.controls.phi = preset.phi;
|
preview.controls.phi = preset.phi;
|
||||||
preview.controls.radius = preset.radius;
|
preview.controls.radius = preset.radius;
|
||||||
preview.controls.target.copy(preset.target);
|
preview.controls.target.copy(preset.target);
|
||||||
|
preview.pathFitBoundsReady = applyFitBounds(preview.controls, selectedView, fitPoints);
|
||||||
applyCameraControls(preview.controls);
|
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) {
|
function applyCameraControls(controls) {
|
||||||
const sinPhiRadius = Math.sin(controls.phi) * controls.radius;
|
const sinPhiRadius = Math.sin(controls.phi) * controls.radius;
|
||||||
controls.camera.position.set(
|
controls.camera.position.set(
|
||||||
|
|||||||
@@ -333,39 +333,42 @@ promotionAllowed=false
|
|||||||
## 9. 当前状态
|
## 9. 当前状态
|
||||||
|
|
||||||
```text
|
```text
|
||||||
status=M17_linuxcnc_5axis_gcode_source_ingest_complete
|
status=M22_host_native_boundary_visibility_complete
|
||||||
active_style=gmoccapy_5_axis
|
active_style=gmoccapy_5_axis
|
||||||
frontend_framework=none
|
frontend_framework=none
|
||||||
ui_stack=html_css_typescript_es_modules
|
ui_stack=html_css_typescript_es_modules
|
||||||
preview_stack=threejs
|
preview_stack=threejs
|
||||||
semantic_boundary=linuxcnc_owned
|
semantic_boundary=linuxcnc_owned
|
||||||
latest_batch=M17-linuxcnc-5axis-gcode-source-ingest
|
latest_batch=M22-host-native-boundary-visibility
|
||||||
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_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
|
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
|
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
|
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_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
|
program_current_line=implemented_linuxcnc_canonical_motion_highlight_with_fixture_fallback
|
||||||
tool_preview=implemented_tool_card_and_threejs_marker
|
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_source_map=implemented_xyzac_trt_and_xyzbc_trt
|
||||||
profile_switching=implemented_xyzac_trt_and_xyzbc_trt_with_runtime_reload
|
profile_switching=implemented_xyzac_trt_and_xyzbc_trt_with_runtime_reload
|
||||||
pyvcp_hal_schema=implemented_xyzac_trt_switchkins
|
pyvcp_hal_schema=implemented_xyzac_trt_switchkins
|
||||||
linuxcnc_boundary_adapter=kinematics_and_interpreter_runtime_connected_remap_planner_missing
|
linuxcnc_boundary_adapter=kinematics_and_interpreter_runtime_connected_remap_planner_missing
|
||||||
full_execution_boundary=implemented_machine_file_remap_ready_with_planner_task_hal_blockers
|
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
|
linuxcnc_kinematics_wasm=browser_and_node_proof_ready_xyzac_trt
|
||||||
browser_kinematics_wasm=worker_connected_source_and_dist_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
|
linuxcnc_interpreter_wasm=browser_worker_and_node_direct_canonical_program_execution_ready_with_switchkins_mcode_preservation
|
||||||
browser_interpreter_wasm=worker_connected_source_and_dist
|
browser_interpreter_wasm=worker_connected_source_and_dist
|
||||||
switchkins_rtcp_program_execution=implemented_m428_m429_program_events_drive_rtcp_and_kinematics_switch
|
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
|
full_program_execution=partial_interpreter_canonical_and_switchkins_event_ready_remap_planner_not_promoted
|
||||||
session_persistence=implemented_opfs_save_restore_for_5axis_session
|
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
|
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
|
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
|
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_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
|
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
|
||||||
next_batch=hardware_realtime_external_processes_if_required
|
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 接续状态:
|
当前 native task/HAL 接续状态:
|
||||||
@@ -419,6 +422,433 @@ plannerRuntimeReady=false
|
|||||||
fullLinuxCncProgramExecutionReady=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<sub> 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 任务
|
## 14. M16 任务
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -294,10 +294,27 @@ toolAxisVector
|
|||||||
rtcpState
|
rtcpState
|
||||||
rtcpFrame.apiName
|
rtcpFrame.apiName
|
||||||
preview.selectedView
|
preview.selectedView
|
||||||
|
programExecution.motion
|
||||||
|
programExecutionTiming.samples
|
||||||
|
programRuntimeFeedback
|
||||||
```
|
```
|
||||||
|
|
||||||
browser smoke 已检查 canvas nonblank、scene objects、path points、RTCP on/off 同步和 STEP 后 TCP pose 更新。
|
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
|
### Step 5:profile 和 panel schema
|
||||||
|
|
||||||
目标:
|
目标:
|
||||||
@@ -493,6 +510,96 @@ hostRealtimeKernel=false
|
|||||||
externalUserMProcessReady=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<sub> 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 语义;
|
- 用 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`。
|
当前已完成 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 产物。
|
注意:`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 证明;当前完成范围仍限定为浏览器仿真。
|
后续如果要越过 Web simulation boundary,必须另行实现 host realtime kernel / hardware IO / external user-M / tool DB process 证明;当前完成范围仍限定为浏览器仿真。
|
||||||
|
|||||||
@@ -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 |
|
| 程序运行/暂停/继续/停止 | `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 |
|
| 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 轴源程序选择 | `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<sub> 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 |
|
| 程序执行当前行显示 | `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 |
|
| 程序执行速度/时间 | `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 |
|
| 刀具预览 | `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 |
|
| 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` 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 |
|
| `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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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. 源文件追溯清单
|
## 3. 源文件追溯清单
|
||||||
|
|
||||||
@@ -1192,3 +1195,257 @@ Remaining risk:
|
|||||||
Next:
|
Next:
|
||||||
interpreter_worker_or_opfs_machine_file_staging_or_remap_planner_boundary
|
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
|
||||||
|
```
|
||||||
|
|||||||
@@ -34,6 +34,16 @@
|
|||||||
}
|
}
|
||||||
return win.webRtcp5AxisSimulation.getState();
|
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() {
|
async function runSmoke() {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
@@ -44,6 +54,14 @@
|
|||||||
|
|
||||||
const doc = frame.contentDocument;
|
const doc = frame.contentDocument;
|
||||||
const win = frame.contentWindow;
|
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 = [
|
const regions = [
|
||||||
"titlebar",
|
"titlebar",
|
||||||
"preview",
|
"preview",
|
||||||
@@ -301,6 +319,20 @@
|
|||||||
if (!doc.querySelector('[data-program-switchkins-summary]')?.textContent.includes("0 events")) {
|
if (!doc.querySelector('[data-program-switchkins-summary]')?.textContent.includes("0 events")) {
|
||||||
throw new Error("program switchkins summary DOM did not render zero-event state");
|
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({
|
win.webRtcp5AxisSimulation.dispatch({
|
||||||
type: "LOAD_PROGRAM",
|
type: "LOAD_PROGRAM",
|
||||||
filename: "operator-arc-demo.ngc",
|
filename: "operator-arc-demo.ngc",
|
||||||
@@ -328,12 +360,27 @@
|
|||||||
if (stagedMachineFiles.save.status !== "saved" || stagedMachineFiles.save.fileCount < 5) {
|
if (stagedMachineFiles.save.status !== "saved" || stagedMachineFiles.save.fileCount < 5) {
|
||||||
throw new Error(`machine file staging did not save files: ${JSON.stringify(stagedMachineFiles.save)}`);
|
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"))) {
|
if (!stagedMachineFiles.save.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc"))) {
|
||||||
throw new Error("machine file staging did not include M428 remap");
|
throw new Error("machine file staging did not include M428 remap");
|
||||||
}
|
}
|
||||||
if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) {
|
if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) {
|
||||||
throw new Error("machine file staging status did not render");
|
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"]');
|
const sourceSelect = doc.querySelector('[data-action="select-linuxcnc-gcode-source"]');
|
||||||
if (!sourceSelect || sourceSelect.options.length < 4) {
|
if (!sourceSelect || sourceSelect.options.length < 4) {
|
||||||
throw new Error("LinuxCNC 5-axis G-code source selector did not render staged demos");
|
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")) {
|
if (!doc.querySelector('[data-program-source]')?.textContent.includes("linuxcnc-vendored-5axis-gcode")) {
|
||||||
throw new Error("program source DOM did not show LinuxCNC vendored source");
|
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" });
|
win.webRtcp5AxisSimulation.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
|
||||||
const machineFileRunState = await waitForMachineFileExecution(win);
|
const machineFileRunState = await waitForMachineFileExecution(win);
|
||||||
if (machineFileRunState.machineFileExecution?.summary?.machineFileExecutionReady !== true) {
|
if (machineFileRunState.machineFileExecution?.summary?.machineFileExecutionReady !== true) {
|
||||||
@@ -383,12 +469,18 @@
|
|||||||
if (!["opfs", "memory-fallback"].includes(savedSession.storageMode)) {
|
if (!["opfs", "memory-fallback"].includes(savedSession.storageMode)) {
|
||||||
throw new Error(`saveSession used unexpected storage mode: ${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")) {
|
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("saved")) {
|
||||||
throw new Error("session save status did not render");
|
throw new Error("session save status did not render");
|
||||||
}
|
}
|
||||||
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes(savedSession.storageMode)) {
|
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes(savedSession.storageMode)) {
|
||||||
throw new Error("session save status did not render storage mode");
|
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") {
|
if (doc.querySelector('[data-active-program-line]')?.textContent !== "Current line 2") {
|
||||||
throw new Error("loaded program did not render first LinuxCNC motion line");
|
throw new Error("loaded program did not render first LinuxCNC motion line");
|
||||||
}
|
}
|
||||||
@@ -413,10 +505,22 @@
|
|||||||
canvas = doc.querySelector("[data-five-axis-canvas]");
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
||||||
if (
|
if (
|
||||||
Number(canvas.dataset.threeExecutedPathPoints ?? 0) < 1 ||
|
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)}`);
|
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();
|
const taskHalRunState = win.webRtcp5AxisSimulation.getState();
|
||||||
if (taskHalRunState.fullExecutionBoundary?.semanticBoundary !== "linuxcnc_task_motion_hal_wasm_simulation_runtime") {
|
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)}`);
|
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")) {
|
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");
|
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")) {
|
if (!doc.querySelector('[data-task-hal-runtime="readiness"]')?.textContent.includes("sync")) {
|
||||||
throw new Error("task/HAL readiness diagnostic did not render sync");
|
throw new Error("task/HAL readiness diagnostic did not render sync");
|
||||||
}
|
}
|
||||||
@@ -545,6 +670,9 @@
|
|||||||
if (uiRestoredSession.status !== "restored" || uiRestoredSession.storageMode !== uiSavedSession.storageMode) {
|
if (uiRestoredSession.status !== "restored" || uiRestoredSession.storageMode !== uiSavedSession.storageMode) {
|
||||||
throw new Error(`UI Restore Session did not restore from default storage fallback: ${JSON.stringify(uiRestoredSession)}`);
|
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) {
|
if (win.webRtcp5AxisSimulation.getState().machine.allHomed !== true) {
|
||||||
doc.querySelector('[data-action="mode-manual"]').click();
|
doc.querySelector('[data-action="mode-manual"]').click();
|
||||||
await wait(50);
|
await wait(50);
|
||||||
@@ -673,6 +801,9 @@
|
|||||||
if (win.webRtcp5AxisSimulation.getState().runState !== "estopped") {
|
if (win.webRtcp5AxisSimulation.getState().runState !== "estopped") {
|
||||||
throw new Error("E-STOP did not update run state");
|
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";
|
result.textContent = "gmoccapy_shell_smoke=ok";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/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("remap_subs/429remap.ngc")), true);
|
||||||
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("demos/xyzac_switchkins.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.remapFileCount >= 3, true);
|
||||||
assert.equal(plan.summary.demoFileCount >= 1, 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.apiName, "web-rtcp-5axis-machine-file-staging-save");
|
||||||
assert.equal(staged.save.status, "saved");
|
assert.equal(staged.save.status, "saved");
|
||||||
assert.equal(staged.save.fileCount, staged.plan.files.length);
|
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.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 === "impeller-7bl-xyzac.ngc"), true);
|
||||||
assert.equal(staged.save.gcodeSources.some((source) => source.filename === "boat-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.equal(staged.save.files.every((file) => file.opfsPath.startsWith("web-rtcp-5axis-sim-plan/machines/xyzac-trt/")), true);
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => selectMachineFileProgram(staged.plan, staged.save, "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc"),
|
() => 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.status, "staged");
|
||||||
assert.equal(state.machineFileStaging.profileId, "xyzac-trt");
|
assert.equal(state.machineFileStaging.profileId, "xyzac-trt");
|
||||||
assert.equal(state.machineFileStaging.fileCount, staged.save.fileCount);
|
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.save.summary.kinds.remap >= 3, true);
|
||||||
assert.equal(state.machineFileStaging.gcodeSources.length >= 4, true);
|
assert.equal(state.machineFileStaging.gcodeSources.length >= 4, true);
|
||||||
assert.equal(state.machineFileStaging.selectedGcodeSourceRel, null);
|
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.satisfied.includes("switchkins-hal-bridge-evidence"), true);
|
||||||
assert.equal(runState.fullExecutionBoundary.blockers.some((blocker) => blocker.includes("trajectory planner")), 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");
|
console.log("machine_file_staging_smoke=ok");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
Reference in New Issue
Block a user