Add OPFS G-code staging and browser simulation checks
This commit is contained in:
@@ -90,7 +90,7 @@ export async function saveFiveAxisSessionSnapshot(sessionId, payload, options =
|
||||
const path = sessionSnapshotPath(sessionId, options.filename);
|
||||
const storage = resolveSessionStorage(options);
|
||||
await saveTextFile(path, `${JSON.stringify(snapshot, null, 2)}\n`, storage.storage);
|
||||
return { snapshot, path, storageMode: storage.mode };
|
||||
return { snapshot, path, storageMode: storage.mode, storageCapability: storage.capability };
|
||||
}
|
||||
|
||||
export async function loadFiveAxisSessionSnapshot(sessionId, options = {}) {
|
||||
@@ -103,7 +103,12 @@ export async function loadFiveAxisSessionSnapshot(sessionId, options = {}) {
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid five-axis session snapshot JSON: ${error.message}`);
|
||||
}
|
||||
return { snapshot: validateFiveAxisSessionSnapshot(snapshot, sessionId), path, storageMode: storage.mode };
|
||||
return {
|
||||
snapshot: validateFiveAxisSessionSnapshot(snapshot, sessionId),
|
||||
path,
|
||||
storageMode: storage.mode,
|
||||
storageCapability: storage.capability,
|
||||
};
|
||||
}
|
||||
|
||||
export function restoreFiveAxisSessionState(snapshot) {
|
||||
@@ -150,24 +155,65 @@ export function createMemorySessionStorage(seed = {}) {
|
||||
|
||||
let browserMemorySessionStorage = null;
|
||||
|
||||
export function detectBrowserStorageCapability(globalScope = globalThis) {
|
||||
const forcedUnavailable = Boolean(globalScope.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__);
|
||||
const isBrowser = typeof globalScope.window === "object" || typeof globalScope.document === "object";
|
||||
const secureContext = !isBrowser || globalScope.isSecureContext !== false;
|
||||
const hasOpfs = typeof globalScope.navigator?.storage?.getDirectory === "function";
|
||||
const opfsAvailable = hasOpfs && secureContext && !forcedUnavailable;
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-storage-capability",
|
||||
opfsAvailable,
|
||||
opfsUnavailable: !opfsAvailable,
|
||||
secureContext,
|
||||
forcedUnavailable,
|
||||
hasNavigatorStorage: Boolean(globalScope.navigator?.storage),
|
||||
hasGetDirectory: hasOpfs,
|
||||
fallbackMode: opfsAvailable ? null : "memory-fallback",
|
||||
reason: opfsAvailable
|
||||
? "opfs_available"
|
||||
: forcedUnavailable
|
||||
? "forced_unavailable"
|
||||
: !secureContext
|
||||
? "non_secure_context"
|
||||
: !hasOpfs
|
||||
? "missing_opfs_get_directory"
|
||||
: "opfs_unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSessionStorage(options = {}) {
|
||||
if (options.storage) {
|
||||
return {
|
||||
storage: options.storage,
|
||||
mode: options.storageMode || storageModeFor(options.storage),
|
||||
capability: {
|
||||
apiName: "web-rtcp-5axis-storage-capability",
|
||||
opfsAvailable: options.storageMode === "opfs",
|
||||
opfsUnavailable: options.storageMode !== "opfs",
|
||||
secureContext: true,
|
||||
forcedUnavailable: false,
|
||||
hasNavigatorStorage: false,
|
||||
hasGetDirectory: typeof options.storage.getDirectory === "function",
|
||||
fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom",
|
||||
reason: "explicit_storage",
|
||||
},
|
||||
};
|
||||
}
|
||||
const capability = detectBrowserStorageCapability();
|
||||
const browserStorage = globalThis.navigator?.storage;
|
||||
if (browserStorage?.getDirectory) {
|
||||
if (capability.opfsAvailable) {
|
||||
return {
|
||||
storage: browserStorage,
|
||||
mode: "opfs",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
browserMemorySessionStorage ??= createMemorySessionStorage();
|
||||
return {
|
||||
storage: browserMemorySessionStorage,
|
||||
mode: "memory-fallback",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -200,13 +200,14 @@ function runPlannerTiming(tpRuntime, motion) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const sampleStride = motion.length > 1000 ? Math.ceil(motion.length / 90) : 10;
|
||||
return tpRuntime.sdk.runCanonicalMotionTiming({
|
||||
motion,
|
||||
options: {
|
||||
cycleTime: 0.001,
|
||||
queueSize: 32,
|
||||
maxCycles: 2000000,
|
||||
sampleStride: 10,
|
||||
sampleStride,
|
||||
maxVelocity: 35,
|
||||
maxAcceleration: 500,
|
||||
maxJerk: 1000,
|
||||
@@ -334,6 +335,9 @@ export function prepareLinuxCncProgramForRuntime(programText) {
|
||||
const switchkinsEvents = [];
|
||||
const runtimeLines = String(programText).split(/\r?\n/).map((line, index) => {
|
||||
const lineNumber = index + 1;
|
||||
if (String(line).trim() === "%") {
|
||||
return "(linuxcnc program delimiter)";
|
||||
}
|
||||
const events = readSwitchkinsEventsFromLine(line, lineNumber);
|
||||
if (events.length === 0) return line;
|
||||
switchkinsEvents.push(...events);
|
||||
|
||||
@@ -10,6 +10,7 @@ const DEFAULT_VENDOR_ROOT_URLS = [
|
||||
const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting";
|
||||
const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`;
|
||||
const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
|
||||
let browserMemoryMachineFileStorage = null;
|
||||
|
||||
export async function createMachineFileStagingPlan({
|
||||
profile,
|
||||
@@ -79,6 +80,23 @@ export function listLinuxCncGcodeSources(save) {
|
||||
.sort((left, right) => left.filename.localeCompare(right.filename));
|
||||
}
|
||||
|
||||
export function listProjectGcodeFiles(save) {
|
||||
return [...(save?.files || [])]
|
||||
.filter((file) => file.kind === "demo" || file.kind === "remap")
|
||||
.map((file) => ({
|
||||
sourceRel: file.sourceRel,
|
||||
wasmPath: file.wasmPath,
|
||||
opfsPath: file.opfsPath,
|
||||
filename: basename(file.sourceRel),
|
||||
bytes: file.bytes,
|
||||
kind: file.kind,
|
||||
semanticBoundary: file.kind === "demo"
|
||||
? "linuxcnc_vendored_5axis_gcode_source_file"
|
||||
: "linuxcnc_vendored_5axis_remap_subroutine_file",
|
||||
}))
|
||||
.sort((left, right) => left.sourceRel.localeCompare(right.sourceRel));
|
||||
}
|
||||
|
||||
export function selectMachineFileProgram(plan, save, sourceRel) {
|
||||
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) {
|
||||
throw new Error(`5-axis G-code source must come from LinuxCNC source demos: ${sourceRel}`);
|
||||
@@ -106,10 +124,11 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||
if (plan?.apiName !== "web-rtcp-5axis-machine-file-staging-plan") {
|
||||
throw new Error("saveMachineFileStagingPlan requires a machine-file staging plan");
|
||||
}
|
||||
const storage = resolveMachineFileStorage(options);
|
||||
const savedFiles = [];
|
||||
for (const file of plan.files) {
|
||||
const text = await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel));
|
||||
await saveTextFile(file.opfsPath, text, options.storage);
|
||||
await saveTextFile(file.opfsPath, text, storage.storage);
|
||||
savedFiles.push({
|
||||
sourceRel: file.sourceRel,
|
||||
opfsPath: file.opfsPath,
|
||||
@@ -129,8 +148,11 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||
savedAt: new Date().toISOString(),
|
||||
fileCount: savedFiles.length,
|
||||
opfsRoot: `${OPFS_ROOT}/${plan.profileId}`,
|
||||
storageMode: storage.mode,
|
||||
storageCapability: storage.capability,
|
||||
files: savedFiles,
|
||||
gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }),
|
||||
gcodeFiles: listProjectGcodeFiles({ files: savedFiles }),
|
||||
summary: summarizeSavedFiles(savedFiles),
|
||||
taskHalSession: {
|
||||
...(plan.taskHalSession || {}),
|
||||
@@ -142,7 +164,9 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||
bytes: file.bytes,
|
||||
})),
|
||||
},
|
||||
semanticBoundary: "opfs_machine_file_text_staging_only",
|
||||
semanticBoundary: storage.mode === "opfs"
|
||||
? "opfs_machine_file_text_staging_only"
|
||||
: "memory_machine_file_text_staging_current_page_lifecycle_only",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,15 +179,95 @@ export async function stageProfileMachineFiles(profile, options = {}) {
|
||||
manifestUrl: options.manifestUrl,
|
||||
wasmDir: options.wasmDir,
|
||||
});
|
||||
const save = await saveMachineFileStagingPlan(plan, { storage: options.storage });
|
||||
const save = await saveMachineFileStagingPlan(plan, {
|
||||
storage: options.storage,
|
||||
storageMode: options.storageMode,
|
||||
});
|
||||
return { plan, save };
|
||||
}
|
||||
|
||||
export function detectMachineFileStorageCapability(globalScope = globalThis) {
|
||||
const forcedUnavailable = Boolean(globalScope.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__);
|
||||
const isBrowser = typeof globalScope.window === "object" || typeof globalScope.document === "object";
|
||||
const secureContext = !isBrowser || globalScope.isSecureContext !== false;
|
||||
const hasOpfs = typeof globalScope.navigator?.storage?.getDirectory === "function";
|
||||
const opfsAvailable = hasOpfs && secureContext && !forcedUnavailable;
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-machine-file-storage-capability",
|
||||
opfsAvailable,
|
||||
opfsUnavailable: !opfsAvailable,
|
||||
secureContext,
|
||||
forcedUnavailable,
|
||||
hasNavigatorStorage: Boolean(globalScope.navigator?.storage),
|
||||
hasGetDirectory: hasOpfs,
|
||||
fallbackMode: opfsAvailable ? null : "memory-fallback",
|
||||
reason: opfsAvailable
|
||||
? "opfs_available"
|
||||
: forcedUnavailable
|
||||
? "forced_unavailable"
|
||||
: !secureContext
|
||||
? "non_secure_context"
|
||||
: !hasOpfs
|
||||
? "missing_opfs_get_directory"
|
||||
: "opfs_unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMachineFileStorage(options = {}) {
|
||||
if (options.storage) {
|
||||
return {
|
||||
storage: options.storage,
|
||||
mode: options.storageMode || storageModeFor(options.storage),
|
||||
capability: {
|
||||
apiName: "web-rtcp-5axis-machine-file-storage-capability",
|
||||
opfsAvailable: options.storageMode === "opfs",
|
||||
opfsUnavailable: options.storageMode !== "opfs",
|
||||
secureContext: true,
|
||||
fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom",
|
||||
reason: "explicit_storage",
|
||||
},
|
||||
};
|
||||
}
|
||||
const capability = detectMachineFileStorageCapability();
|
||||
if (capability.opfsAvailable) {
|
||||
return {
|
||||
storage: globalThis.navigator.storage,
|
||||
mode: "opfs",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
browserMemoryMachineFileStorage ??= createMemoryStorage();
|
||||
return {
|
||||
storage: browserMemoryMachineFileStorage,
|
||||
mode: "memory-fallback",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
|
||||
function storageModeFor(storage) {
|
||||
return storage?.apiName === "web-rtcp-5axis-memory-machine-file-storage" ||
|
||||
storage?.apiName === "web-rtcp-5axis-memory-session-storage"
|
||||
? "memory"
|
||||
: "custom";
|
||||
}
|
||||
|
||||
function createMemoryStorage(seed = {}) {
|
||||
const files = new Map(Object.entries(seed));
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-memory-machine-file-storage",
|
||||
files,
|
||||
async getDirectory() {
|
||||
return createDirectoryHandle(files, []);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function summarizePlan(files) {
|
||||
const kinds = countKinds(files.map((file) => classifySourceRel(file.sourceRel)));
|
||||
return {
|
||||
fileCount: files.length,
|
||||
requiredFileCount: files.filter((file) => file.sourceRel.endsWith(".ini") || file.sourceRel.includes("/demos/")).length,
|
||||
gcodeFileCount: (kinds.demo || 0) + (kinds.remap || 0),
|
||||
remapFileCount: kinds.remap || 0,
|
||||
demoFileCount: kinds.demo || 0,
|
||||
toolTableFileCount: kinds.toolTable || 0,
|
||||
@@ -208,10 +312,12 @@ function isLinuxCncFiveAxisGcodeSourceRel(sourceRel) {
|
||||
}
|
||||
|
||||
function summarizeSavedFiles(files) {
|
||||
const kinds = countKinds(files.map((file) => file.kind));
|
||||
return {
|
||||
fileCount: files.length,
|
||||
totalBytes: files.reduce((total, file) => total + file.bytes, 0),
|
||||
kinds: countKinds(files.map((file) => file.kind)),
|
||||
gcodeFileCount: (kinds.demo || 0) + (kinds.remap || 0),
|
||||
kinds,
|
||||
opfsPaths: files.map((file) => file.opfsPath),
|
||||
};
|
||||
}
|
||||
@@ -266,7 +372,11 @@ async function readTextFromUrl(url) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`failed to fetch machine staging asset ${url}: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
const text = await response.text();
|
||||
if (/^\s*<!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) {
|
||||
@@ -294,6 +404,34 @@ async function ensureParentDir(root, path) {
|
||||
return current;
|
||||
}
|
||||
|
||||
function createDirectoryHandle(files, prefix) {
|
||||
return {
|
||||
async getDirectoryHandle(name) {
|
||||
return createDirectoryHandle(files, [...prefix, name]);
|
||||
},
|
||||
async getFileHandle(name) {
|
||||
const path = [...prefix, name].join("/");
|
||||
return {
|
||||
async createWritable() {
|
||||
let content = "";
|
||||
return {
|
||||
async write(text) {
|
||||
content += String(text);
|
||||
},
|
||||
async close() {
|
||||
files.set(path, content);
|
||||
},
|
||||
};
|
||||
},
|
||||
async getFile() {
|
||||
if (!files.has(path)) throw new Error(`Missing memory machine file: ${path}`);
|
||||
return { async text() { return files.get(path); } };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function splitPath(path) {
|
||||
const parts = String(path || "").replaceAll("\\", "/").split("/").filter(Boolean);
|
||||
if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user