完善五轴 RTCP 仿真与验证资料

This commit is contained in:
2026-07-01 21:49:58 -04:00
parent ac4e855b2b
commit d0d58998ac
159 changed files with 6771594 additions and 339 deletions

View File

@@ -62,6 +62,9 @@ window.webRtcp5AxisSimulation = {
saveSession: store.saveSession,
restoreSession: store.restoreSession,
stageMachineFiles: store.stageMachineFiles,
queryToolDb: store.queryToolDb,
editToolDb: store.editToolDb,
saveToolDb: store.saveToolDb,
runFullBoundaryAudit: store.runFullBoundaryAudit,
getRegions: shell.getRegions,
iniConfigReady,
@@ -126,6 +129,11 @@ async function ensureDefaultLinuxCncProgramPreview(store) {
function selectDefaultLinuxCncSource(state) {
const sources = state.machineFileStaging?.gcodeSources || [];
const profileDefault = state.profile?.machineFileStaging?.defaultProgramFilename;
if (profileDefault) {
const match = sources.find((source) => source.filename === profileDefault);
if (match) return match;
}
const preferredFilename = `${state.machineProfile}_switchkins.ngc`;
return sources.find((source) => source.filename === preferredFilename)
|| sources.find((source) => source.filename.includes(state.machineProfile))

View File

@@ -1,8 +1,14 @@
import { xyzacTrtProfile } from "./xyzac-trt.js";
import { xyzbcTrtProfile } from "./xyzbc-trt.js";
import { gmoccapyXyzacTrtProfile } from "./gmoccapy-xyzac-trt.js";
import { gmoccapyXyzabProfile } from "./gmoccapy-xyzab.js";
export const fiveAxisProfiles = [xyzacTrtProfile, xyzbcTrtProfile, gmoccapyXyzabProfile];
export const fiveAxisProfiles = [
xyzacTrtProfile,
xyzbcTrtProfile,
gmoccapyXyzacTrtProfile,
gmoccapyXyzabProfile,
];
export function getFiveAxisProfile(profileId = "xyzac-trt") {
const profile = fiveAxisProfiles.find((entry) => entry.id === profileId);

View File

@@ -31,6 +31,7 @@ export const xyzacTrtProfile = {
coordinates: ["X", "Y", "Z", "A", "C"],
joints: ["joint.0", "joint.1", "joint.2", "joint.3", "joint.4"],
kinematics: "xyzac-trt-kins",
kinematicsModuleId: "xyzac-trt",
kinematicsParameters: {
sparm: "identityfirst",
defaultSwitchkinsType: 0,

View File

@@ -0,0 +1,219 @@
const DEFAULT_ALLOWED_USER_M_CODES = {
M428: {
code: "M428",
label: "TCP kinematics",
kinsType: "tcp",
switchkinsType: 1,
rtcpState: "on",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
},
M429: {
code: "M429",
label: "Identity kinematics",
kinsType: "identity",
switchkinsType: 0,
rtcpState: "off",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
},
M430: {
code: "M430",
label: "User kinematics",
kinsType: "userk",
switchkinsType: 2,
rtcpState: "on",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
},
M128: {
code: "M128",
label: "controlled millturn mill-mode user-M",
kinsType: "mill",
switchkinsType: 0,
rtcpState: "off",
sourceRel: "linuxcnc millturn user-M reference",
},
M129: {
code: "M129",
label: "controlled millturn turn-mode user-M",
kinsType: "turn",
switchkinsType: 1,
rtcpState: "off",
sourceRel: "linuxcnc millturn user-M reference",
},
};
export function createControlledUserMSimulation({ allowedCodes = DEFAULT_ALLOWED_USER_M_CODES } = {}) {
return {
apiName: "web-rtcp-5axis-controlled-user-m-simulation",
ready: true,
processReady: true,
processScope: "web_simulation_only",
hostProcessReady: false,
hostProcessExecution: false,
arbitraryUserMExecution: false,
allowedCodes: Object.fromEntries(
Object.entries(allowedCodes).map(([code, definition]) => [normalizeMCode(code), {
...definition,
code: normalizeMCode(definition.code || code),
}]),
),
events: [],
blockedEvents: [],
semanticBoundary: "controlled_user_m_web_simulation_whitelist_not_host_process",
linuxCncReferences: [
"linuxcnc/src/emc/rs274ngc",
"linuxcnc/src/emc/task/emctask.cc",
"linuxcnc/src/emc/task/emccanon.cc",
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
],
};
}
export function runControlledUserM(simulation, code, context = {}) {
const state = cloneSimulation(simulation);
const normalizedCode = normalizeMCode(code);
const definition = state.allowedCodes[normalizedCode] || null;
if (!definition) {
const event = createUserMEvent({
code: normalizedCode,
allowed: false,
reason: "not_in_controlled_user_m_whitelist",
context,
});
state.blockedEvents.push(event);
return {
simulation: state,
event,
statePatch: {},
halPatch: {},
};
}
const profileKinsType = definition.kinsType === "tcp"
? tcpKinsTypeForProfile(context.profile)
: definition.kinsType;
const halPatch = {
"motion.switchkins-type": definition.switchkinsType,
"motion.analog-out-03": definition.switchkinsType,
};
const statePatch = {
kinsType: profileKinsType,
rtcpState: definition.rtcpState,
};
const event = createUserMEvent({
code: normalizedCode,
allowed: true,
definition,
context,
halPatch,
statePatch,
});
state.events.push(event);
return {
simulation: state,
event,
statePatch,
halPatch,
};
}
export function createControlledUserMReadiness(simulation) {
const ready = simulation?.processReady === true && simulation?.processScope === "web_simulation_only";
return {
apiName: "web-rtcp-5axis-controlled-user-m-readiness",
ready,
externalUserMProcessReady: ready,
externalUserMProcessScope: ready ? "web_simulation_only" : "not_ready",
hostExternalUserMProcessReady: false,
hostProcessExecution: false,
arbitraryUserMExecution: false,
allowedCodeCount: Object.keys(simulation?.allowedCodes || {}).length,
blockedEventCount: simulation?.blockedEvents?.length || 0,
semanticBoundary: "external_user_m_process_ready_for_web_simulation_only",
};
}
export function extractControlledUserMCodesFromProgram(programText = "") {
const matches = [];
const lines = String(programText).split(/\r?\n/);
lines.forEach((line, index) => {
const stripped = line.replace(/\([^)]*\)/g, " ");
for (const match of stripped.matchAll(/\bM\s*(\d{2,3})\b/gi)) {
const code = normalizeMCode(`M${match[1]}`);
if (Number(match[1]) >= 100 || ["M428", "M429", "M430"].includes(code)) {
matches.push({
code,
line: index + 1,
rawLine: line,
});
}
}
});
return matches;
}
export function runControlledUserMProgramScan(simulation, programText = "", context = {}) {
let state = simulation;
const events = [];
for (const occurrence of extractControlledUserMCodesFromProgram(programText)) {
const result = runControlledUserM(state, occurrence.code, {
...context,
line: occurrence.line,
rawLine: occurrence.rawLine,
});
state = result.simulation;
events.push(result.event);
}
return { simulation: state, events };
}
function createUserMEvent({
code,
allowed,
definition = null,
context = {},
halPatch = {},
statePatch = {},
reason = null,
}) {
return {
apiName: "web-rtcp-5axis-controlled-user-m-event",
code,
allowed,
reason,
label: definition?.label || null,
sourceRel: definition?.sourceRel || context.sourceRel || null,
line: context.line || null,
halPatch,
statePatch,
createdAt: new Date().toISOString(),
semanticBoundary: allowed
? "vendored_or_whitelisted_user_m_web_simulation_event"
: "arbitrary_external_user_m_blocked",
promotionScope: "web_simulation_only",
hostProcessExecution: false,
arbitraryUserMExecution: false,
};
}
function tcpKinsTypeForProfile(profile) {
if (profile?.id === "xyzbc-trt") return "tcp-xyzbc";
return "tcp-xyzac";
}
function normalizeMCode(code) {
const normalized = String(code || "").trim().toUpperCase().replace(/\s+/g, "");
const match = normalized.match(/^M0*(\d+)$/);
if (!match) return normalized;
return `M${Number(match[1])}`;
}
function cloneSimulation(simulation) {
return {
...simulation,
allowedCodes: { ...(simulation.allowedCodes || {}) },
events: [...(simulation.events || [])],
blockedEvents: [...(simulation.blockedEvents || [])],
};
}

View File

@@ -27,7 +27,10 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
&& state.machineFileStaging?.fileCount > 0;
const machineFileRemapReady = Boolean(
machineFileExecution?.sourceMode === "linuxcnc-machine-file-remap-wasm" &&
machineFileExecution?.summary?.machineFileExecutionReady === true &&
machineFileExecution?.summary?.machineFileExecutionReady === true,
);
const remapRuntimeReady = Boolean(
machineFileExecution?.summary?.remapRuntimeReady === true ||
MACHINE_FILE_FLAGS.every((flag) => machineFileText.includes(flag)),
);
const plannerRuntimeReady = Boolean(
@@ -55,6 +58,10 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
const taskHalComparisonReady = Boolean(taskHalSummary.taskHalComparisonReady === true);
const nativeTaskReady = taskRuntimeReady;
const nativeHalSyncReady = halRuntimeReady && motionRuntimeReady && halSyncReady;
const toolDbReadiness = state.toolDbReadiness || {};
const controlledUserMReadiness = state.controlledUserMReadiness || {};
const toolDbProcessReady = toolDbReadiness.toolDbProcessReady === true;
const externalUserMProcessReady = controlledUserMReadiness.externalUserMProcessReady === true;
const fullLinuxCncProgramExecutionReady = Boolean(
kinematicsReady &&
interpreterReady &&
@@ -64,7 +71,9 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
plannerRuntimeReady &&
nativeTaskReady &&
nativeHalSyncReady &&
taskHalComparisonReady
taskHalComparisonReady &&
toolDbProcessReady &&
externalUserMProcessReady
);
const satisfied = [
@@ -80,6 +89,8 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
halRuntimeReady ? "linuxcnc-hal-runtime" : null,
halSyncReady ? "task-motion-hal-sync" : null,
taskHalComparisonReady ? "task-hal-cycle-artifact" : null,
toolDbProcessReady ? "tool-db-web-simulation" : null,
externalUserMProcessReady ? "controlled-user-m-web-simulation" : null,
].filter(Boolean);
const missing = [];
@@ -95,11 +106,15 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
if (!halRuntimeReady) missing.push("LinuxCNC HAL runtime");
if (!halSyncReady) missing.push("task/motion/HAL synchronization");
if (!taskHalComparisonReady) missing.push("task cycle and HAL servo cycle artifact");
if (!toolDbProcessReady) missing.push("tool DB Web/WASM simulation process");
if (!externalUserMProcessReady) missing.push("controlled user-M Web/WASM simulation process");
const blockers = [];
if (!nativeTaskReady) blockers.push("LinuxCNC task runtime is not promoted");
if (!nativeHalSyncReady) blockers.push("realtime HAL synchronization is not promoted");
blockers.push("external user-M process and full tool DB process are not promoted");
if (!toolDbProcessReady) blockers.push("tool DB Web simulation process is not ready");
if (!externalUserMProcessReady) blockers.push("controlled user-M Web simulation process is not ready");
blockers.push("host external user-M process and host tool DB process remain disabled; Web simulation boundary only");
if (!plannerRuntimeReady) {
blockers.push("LinuxCNC trajectory planner queue is not promoted as browser runtime");
}
@@ -126,7 +141,7 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
: programExecution?.sourceMode || state.programExecutionSourceMode || "fixture-line-playback",
readyForUiSimulation: kinematicsReady && interpreterReady && canonicalProgramReady,
machineFileBackedRemapReady: machineFileRemapReady,
remapRuntimeReady: machineFileRemapReady,
remapRuntimeReady,
halSwitchkinsEvidenceReady,
plannerRuntimeReady,
taskRuntimeReady,
@@ -140,8 +155,13 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
promotionAllowed: fullLinuxCncProgramExecutionReady,
hardwareDrive: false,
hostRealtimeKernel: false,
externalUserMProcessReady: false,
toolDbProcessReady: false,
externalUserMProcessReady,
externalUserMProcessScope: externalUserMProcessReady ? "web_simulation_only" : "not_ready",
toolDbProcessReady,
toolDbProcessScope: toolDbProcessReady ? "web_simulation_only" : "not_ready",
hostExternalUserMProcessReady: false,
hostToolDbProcessReady: false,
arbitraryUserMExecution: false,
satisfied,
missing,
blockers,
@@ -153,12 +173,15 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
plannerTiming: plannerRuntimeReady ? programExecution.plannerTiming?.semanticBoundary : null,
machineFileFlags: MACHINE_FILE_FLAGS.filter((flag) => machineFileText.includes(flag)),
machineFileExecutionReady: machineFileExecution?.summary?.machineFileExecutionReady === true,
remapRuntimeReady,
stagedFileCount: state.machineFileStaging?.fileCount || 0,
taskHal: taskHalSummary,
taskCycle: state.taskHalStatus?.ui?.taskCycle || 0,
servoCycle: state.taskHalStatus?.ui?.servoCycle || 0,
motionQueueDepth: state.taskHalStatus?.ui?.motionQueueDepth || 0,
halChangedPinCount: state.taskHalStatus?.ui?.halChangedPinCount || 0,
toolDb: toolDbReadiness,
controlledUserM: controlledUserMReadiness,
},
};
}

View File

@@ -139,7 +139,9 @@ export function applyIniConfigToProfile(profile, iniConfig) {
...iniConfig.kinematicsParameters,
switchkinsTypes: mergeSwitchkinsTypes(
profile.kinematicsParameters?.switchkinsTypes || [],
iniConfig.kinematicsParameters.switchkinsTypes,
iniConfig.halui.mdiCommands.length > 0
? iniConfig.kinematicsParameters.switchkinsTypes
: [],
),
},
display: {
@@ -363,18 +365,25 @@ function validateIniConfig({
emcio,
}) {
const missing = [];
const displayName = getFirstValue(sections, "DISPLAY", "DISPLAY") || "";
const isSwitchkinsTrt = String(kinsText || "").includes("sparm=identityfirst");
const isGmoccapyFixedTrt = String(displayName).toLowerCase() === "gmoccapy"
&& /^xyz[ab]c-trt-kins\b/i.test(String(kinsText || ""))
&& !isSwitchkinsTrt;
const requiredSections = [
"EMC",
"DISPLAY",
"RS274NGC",
"KINS",
"HAL",
"HALUI",
"TRAJ",
"EMCMOT",
"TASK",
"EMCIO",
];
if (isSwitchkinsTrt) {
requiredSections.push("HALUI");
}
for (const section of requiredSections) {
if (!sections.has(section)) missing.push(`[${section}]`);
}
@@ -383,7 +392,7 @@ function validateIniConfig({
if (!jointCount) missing.push("KINS.JOINTS");
if (jointCount !== 5) missing.push("KINS.JOINTS=5");
if (!kinsText) missing.push("KINS.KINEMATICS");
if (!String(kinsText || "").includes("sparm=identityfirst")) {
if (!isSwitchkinsTrt && !isGmoccapyFixedTrt) {
missing.push("KINS.KINEMATICS sparm=identityfirst");
}
for (const axis of String(coordinates || "").split("")) {
@@ -395,20 +404,38 @@ function validateIniConfig({
for (let joint = 0; joint < 5; joint += 1) {
if (!sections.has(`JOINT_${joint}`)) missing.push(`JOINT_${joint}`);
}
for (const code of ["M428", "M429", "M430"]) {
if (!remaps.some((remap) => remap.code === code && remap.ngc)) {
missing.push(`RS274NGC.REMAP ${code}`);
if (isSwitchkinsTrt) {
for (const code of ["M428", "M429", "M430"]) {
if (!remaps.some((remap) => remap.code === code && remap.ngc)) {
missing.push(`RS274NGC.REMAP ${code}`);
}
}
if (rs274ngc.halPinVars !== true) missing.push("RS274NGC.HAL_PIN_VARS=1");
} else if (isGmoccapyFixedTrt) {
for (const code of ["M6", "M61"]) {
if (!remaps.some((remap) => remap.code === code && remap.ngc)) {
missing.push(`RS274NGC.REMAP ${code}`);
}
}
} else {
if (rs274ngc.halPinVars !== true) missing.push("RS274NGC.HAL_PIN_VARS=1");
}
if (rs274ngc.halPinVars !== true) missing.push("RS274NGC.HAL_PIN_VARS=1");
if (!rs274ngc.parameterFile) missing.push("RS274NGC.PARAMETER_FILE");
if (!hal.halui) missing.push("HAL.HALUI");
if (!hal.halFiles.length) missing.push("HAL.HALFILE");
if (!hal.postguiHalFiles.length) missing.push("HAL.POSTGUI_HALFILE");
if (!hal.halcmd.some((line) => line.includes("motion.analog-out-03") && line.includes("motion.switchkins-type"))) {
if (isSwitchkinsTrt && !hal.halcmd.some((line) => line.includes("motion.analog-out-03") && line.includes("motion.switchkins-type"))) {
missing.push("HAL.HALCMD motion.analog-out-03=>motion.switchkins-type");
}
if (halui.mdiCommands.length < 3) missing.push("HALUI.MDI_COMMAND M428/M429/M430");
if (isGmoccapyFixedTrt) {
for (const token of ["xyzac-trt-gui", "xyzac-trt-kins.tool-offset", "xyzac-trt-kins.y-offset", "xyzac-trt-kins.z-offset"]) {
if (!hal.halcmd.some((line) => line.includes(token))) {
missing.push(`HAL.HALCMD ${token}`);
}
}
} else if (isSwitchkinsTrt && halui.mdiCommands.length < 3) {
missing.push("HALUI.MDI_COMMAND M428/M429/M430");
}
if (!emcmot.module) missing.push("EMCMOT.EMCMOT");
if (!emcmot.servoPeriodNs) missing.push("EMCMOT.SERVO_PERIOD");
if (!task.module) missing.push("TASK.TASK");

View File

@@ -129,10 +129,15 @@ function createProgramExecutionResult({
semanticBoundary = SEMANTIC_BOUNDARY,
plannerTiming = null,
}) {
const canonicalEventCount = String(resultText).split("\n").filter((line) => line.startsWith("canon_event=")).length;
const machineFileExecutionReady = Boolean(
machineFilePlan && FIVE_AXIS_REMAP_FLAGS.every((flag) => String(resultText).includes(flag)),
);
const resultTextString = String(resultText);
const canonicalEventCount = resultTextString.split("\n").filter((line) => line.startsWith("canon_event=")).length;
const remapRuntimeReady = Boolean(machineFilePlan && resultTextString.includes("fiveaxis_remaps_ready=1"));
const machineFileRunCompleted = Boolean(machineFilePlan && (
FIVE_AXIS_REMAP_FLAGS.every((flag) => resultTextString.includes(flag)) ||
resultTextString.includes("fiveaxis_file_reached_exit=1") ||
resultTextString.includes("fiveaxis_linuxcnc_remap_file_execute=0")
));
const machineFileExecutionReady = Boolean(machineFileRunCompleted && motion.length > 0);
const plannerRuntimeReady = plannerTiming?.plannerRuntimeReady === true
&& plannerTiming.motionCount === motion.length;
return {
@@ -167,7 +172,7 @@ function createProgramExecutionResult({
switchkinsEventCount: switchkinsEvents.length,
switchkinsCodes: [...new Set(switchkinsEvents.map((event) => event.code))],
switchkinsRemapBoundary: switchkinsEvents.length > 0 ? SWITCHKINS_REMAP_BOUNDARY : null,
remapRuntimeReady: machineFileExecutionReady,
remapRuntimeReady,
plannerRuntimeReady,
plannerSemanticBoundary: plannerRuntimeReady ? PLANNER_TIMING_BOUNDARY : null,
machineFileExecutionReady,
@@ -245,7 +250,7 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
let activeLinearUnits = "mm";
for (const line of String(resultText).split("\n")) {
const feedRate = readCanonicalNumber(line, "feed_rate");
const feedRate = readCanonicalFeedRate(line);
if (Number.isFinite(feedRate) && feedRate > 0) {
activeFeedRate = feedRate;
}
@@ -290,7 +295,8 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
const event = latestSwitchkinsEventAtOrBeforeLine(switchkinsByLine, sourceLine);
if (event) activeSwitchkinsEvent = event;
const sourceFeedRate = latestFeedRateAtOrBeforeLine(feedRatesByLine, sourceLine);
if (Number.isFinite(sourceFeedRate) && sourceFeedRate > 0) {
if ((!Number.isFinite(activeFeedRate) || activeFeedRate <= 0)
&& Number.isFinite(sourceFeedRate) && sourceFeedRate > 0) {
activeFeedRate = sourceFeedRate;
}
activeFeedMode = latestFeedModeAtOrBeforeLine(feedModesByLine, sourceLine) || activeFeedMode;
@@ -485,6 +491,17 @@ function readCanonicalNumber(line, field) {
return match ? Number(match[1]) : null;
}
function readCanonicalFeedRate(line) {
const text = String(line);
if (text.startsWith("canon_event=SET_FEED_RATE")) {
return readCanonicalNumber(text, "rate");
}
if (text.startsWith("canon_event=UPDATE_TAG")) {
return readCanonicalNumber(text, "feed");
}
return readCanonicalNumber(text, "feed_rate");
}
async function createDefaultModuleOptions({ wasmRoot }) {
const quietOptions = { print() {}, printErr() {} };
if (!isNodeRuntime()) return quietOptions;

View File

@@ -18,6 +18,8 @@ const DEFAULT_TEST_SOURCE_ROOT_URLS = [
];
const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting";
const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`;
const GMOCAPY_TRT_MACHINE_REL = "gmoccapy/non_trivial_kinematics/table-rotary-tilting";
const GMOCAPY_TRT_EXAMPLE_SOURCE_PREFIX = `configs/sim/${GMOCAPY_TRT_MACHINE_REL}/examples/`;
const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
let browserMemoryMachineFileStorage = null;
@@ -38,20 +40,26 @@ export async function createMachineFileStagingPlan({
);
const resolvedIniText = iniText ?? await readTextFromCandidateUrls(sourceUrlsFor(profile.iniPath));
const iniFile = basename(profile.iniPath);
const machineRel = machineRelForProfile(profile);
const demoDirectory = demoDirectoryForProfile(profile);
const plan = planSimConfigStaging({
manifestText: resolvedManifestText,
machineRel: TRT_MACHINE_REL,
machineRel,
iniFile,
iniText: resolvedIniText,
wasmDir: wasmDir || `/work/sim/${TRT_MACHINE_REL}/${profile.id}`,
wasmDir: wasmDir || profile.machineFileStaging?.wasmDir || `/work/sim/${machineRel}/${profile.id}`,
});
const files = addVendoredDemoSources(plan.files, resolvedManifestText, plan.wasmDir);
const files = addVendoredDemoSources(plan.files, resolvedManifestText, plan.wasmDir, {
sourcePrefix: sourcePrefixForProfile(profile),
demoDirectory,
});
return {
apiName: "web-rtcp-5axis-machine-file-staging-plan",
profileId: profile.id,
machineRel: TRT_MACHINE_REL,
machineRel,
demoDirectory,
iniPath: profile.iniPath,
wasmDir: plan.wasmDir,
wasmIniPath: plan.iniPath,
@@ -75,7 +83,7 @@ export async function createMachineFileStagingPlan({
export function listLinuxCncGcodeSources(save) {
return [...(save?.files || [])]
.filter((file) => file.kind === "demo" && isLinuxCncFiveAxisGcodeSourceRel(file.sourceRel))
.filter((file) => file.kind === "demo" && isLinuxCncFiveAxisGcodeSourceRel(file.sourceRel, save?.plan || save))
.map((file) => ({
sourceRel: file.sourceRel,
wasmPath: file.wasmPath,
@@ -107,7 +115,7 @@ export function listProjectGcodeFiles(save) {
}
export function selectMachineFileProgram(plan, save, sourceRel) {
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) {
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel, plan)) {
throw new Error(`5-axis G-code source must come from LinuxCNC source demos: ${sourceRel}`);
}
const selectedFile = (save?.files || []).find((file) => file.sourceRel === sourceRel);
@@ -163,7 +171,8 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
storageMode: storage.mode,
storageCapability: storage.capability,
files: savedFiles,
gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }),
plan,
gcodeSources: listLinuxCncGcodeSources({ ...plan, files: savedFiles }),
gcodeFiles: listProjectGcodeFiles({ files: savedFiles }),
summary: summarizeSavedFiles(savedFiles),
taskHalSession: {
@@ -279,7 +288,11 @@ 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,
requiredFileCount: files.filter((file) => (
file.sourceRel.endsWith(".ini")
|| file.sourceRel.includes("/demos/")
|| file.sourceRel.includes("/examples/")
)).length,
gcodeFileCount: (kinds.demo || 0) + (kinds.remap || 0),
remapFileCount: kinds.remap || 0,
demoFileCount: kinds.demo || 0,
@@ -303,25 +316,29 @@ function createTaskHalSession({ profileId, wasmDir, iniPath, programPath, files
};
}
function addVendoredDemoSources(files, manifestText, wasmDir) {
function addVendoredDemoSources(files, manifestText, wasmDir, {
sourcePrefix = TRT_DEMO_SOURCE_PREFIX,
demoDirectory = "demos",
} = {}) {
const bySourceRel = new Map(files.map((file) => [file.sourceRel, file]));
for (const sourceRel of String(manifestText).split(/\r?\n/)) {
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) continue;
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel, { gcodeSourcePrefix: sourcePrefix })) continue;
if (bySourceRel.has(sourceRel)) continue;
bySourceRel.set(sourceRel, {
sourceRel,
wasmPath: `${wasmDir}/demos/${basename(sourceRel)}`,
wasmPath: `${wasmDir}/${demoDirectory}/${basename(sourceRel)}`,
executable: false,
});
}
return [...bySourceRel.values()];
}
function isLinuxCncFiveAxisGcodeSourceRel(sourceRel) {
function isLinuxCncFiveAxisGcodeSourceRel(sourceRel, context = {}) {
const value = String(sourceRel || "");
return value.startsWith(TRT_DEMO_SOURCE_PREFIX)
const sourcePrefix = gcodeSourcePrefixForContext(context);
return value.startsWith(sourcePrefix)
&& value.endsWith(".ngc")
&& !value.slice(TRT_DEMO_SOURCE_PREFIX.length).includes("/");
&& !value.slice(sourcePrefix.length).includes("/");
}
function summarizeSavedFiles(files) {
@@ -347,12 +364,35 @@ function classifySourceRel(sourceRel) {
if (sourceRel.endsWith(".tbl")) return "toolTable";
if (sourceRel.endsWith(".hal")) return "hal";
if (sourceRel.includes("/remap_subs/")) return "remap";
if (sourceRel.includes("/demos/")) return "demo";
if (sourceRel.includes("/demos/") || sourceRel.includes("/examples/")) return "demo";
if (sourceRel.endsWith(".xml")) return "pyvcp";
if (sourceRel.endsWith(".var")) return "parameters";
return "asset";
}
function machineRelForProfile(profile = {}) {
return profile.machineFileStaging?.machineRel || TRT_MACHINE_REL;
}
function demoDirectoryForProfile(profile = {}) {
return profile.machineFileStaging?.demoDirectory || "demos";
}
function sourcePrefixForProfile(profile = {}) {
if (machineRelForProfile(profile) === GMOCAPY_TRT_MACHINE_REL) {
return GMOCAPY_TRT_EXAMPLE_SOURCE_PREFIX;
}
return `configs/sim/${machineRelForProfile(profile)}/${demoDirectoryForProfile(profile)}/`;
}
function gcodeSourcePrefixForContext(context = {}) {
if (context.gcodeSourcePrefix) return context.gcodeSourcePrefix;
if (context.machineRel === GMOCAPY_TRT_MACHINE_REL || context.demoDirectory === "examples") {
return GMOCAPY_TRT_EXAMPLE_SOURCE_PREFIX;
}
return TRT_DEMO_SOURCE_PREFIX;
}
function opfsPathFor(profileId, sourceRel) {
return `${OPFS_ROOT}/${assertPathSegment(profileId)}/${String(sourceRel).replaceAll("\\", "/")}`;
}

View File

@@ -62,8 +62,13 @@ export function createNativeTaskHalReadinessAudit({
);
const hardwareBlocked = fullExecutionBoundary.hardwareDrive === false
&& fullExecutionBoundary.hostRealtimeKernel === false
&& fullExecutionBoundary.externalUserMProcessReady === false
&& fullExecutionBoundary.toolDbProcessReady === false;
&& fullExecutionBoundary.hostExternalUserMProcessReady === false
&& fullExecutionBoundary.hostToolDbProcessReady === false
&& fullExecutionBoundary.arbitraryUserMExecution === false;
const toolUserWebSimulationReady = fullExecutionBoundary.externalUserMProcessReady === true
&& fullExecutionBoundary.externalUserMProcessScope === "web_simulation_only"
&& fullExecutionBoundary.toolDbProcessReady === true
&& fullExecutionBoundary.toolDbProcessScope === "web_simulation_only";
const webSimulationConsistent = sourceManifestReady
&& nativeProbeOk
&& nativePromotionBlocked
@@ -72,6 +77,7 @@ export function createNativeTaskHalReadinessAudit({
&& halReady
&& halSyncReady
&& fullBoundarySimulationReady
&& toolUserWebSimulationReady
&& hardwareBlocked;
return {
@@ -101,6 +107,16 @@ export function createNativeTaskHalReadinessAudit({
hostRealtimeKernel: false,
externalUserMProcessReady: false,
toolDbProcessReady: false,
hostExternalUserMProcessReady: false,
hostToolDbProcessReady: false,
arbitraryUserMExecution: false,
},
toolUserWebSimulation: {
externalUserMProcessReady: fullExecutionBoundary.externalUserMProcessReady === true,
externalUserMProcessScope: fullExecutionBoundary.externalUserMProcessScope || "not_ready",
toolDbProcessReady: fullExecutionBoundary.toolDbProcessReady === true,
toolDbProcessScope: fullExecutionBoundary.toolDbProcessScope || "not_ready",
ready: toolUserWebSimulationReady,
},
sourceManifest: {
ready: sourceManifestReady,
@@ -118,8 +134,10 @@ export function createNativeTaskHalReadinessAudit({
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,
external_user_m_process_ready: toolUserWebSimulationReady ? 1 : 0,
tool_db_process_ready: toolUserWebSimulationReady ? 1 : 0,
host_external_user_m_process_ready: 0,
host_tool_db_process_ready: 0,
promotion_scope: "web_simulation_only",
},
artifacts: artifactPaths,
@@ -132,6 +150,7 @@ export function createNativeTaskHalReadinessAudit({
halReady,
halSyncReady,
fullBoundarySimulationReady,
toolUserWebSimulationReady,
hardwareBlocked,
}),
};
@@ -147,6 +166,7 @@ function buildBlockers(checks) {
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.toolUserWebSimulationReady) blockers.push("tool DB or controlled user-M Web simulation is not ready");
if (!checks.hardwareBlocked) blockers.push("hardware/native process blocked fields are not explicitly false");
return blockers;
}

View File

@@ -38,6 +38,7 @@ export function buildRtcpFrame({
y: pose.y + compensation.y,
z: pose.z + compensation.z,
a: pose.a,
b: pose.b,
c: pose.c,
};
@@ -103,6 +104,7 @@ function buildLinuxCncKinematicsFrame({
y: pose.y,
z: pose.z,
a: pose.a,
b: pose.b,
c: pose.c,
},
toolAxisVector,

View File

@@ -0,0 +1,997 @@
const TOOL_WORD_PATTERN = /([A-Z])\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+))/gi;
const TOOL_DB_STORAGE_ROOT = "web-rtcp-5axis-sim-plan/tool-db";
const TOOL_OFFSET_AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
const TOOL_PARAMETER_KEYS = {
toolNumber: 5400,
x: 5401,
y: 5402,
z: 5403,
a: 5404,
b: 5405,
c: 5406,
u: 5407,
v: 5408,
w: 5409,
diameter: 5410,
frontAngle: 5411,
backAngle: 5412,
orientation: 5413,
};
let browserMemoryToolDbStorage = null;
export function parseLinuxCncToolTable(text = "", options = {}) {
const randomToolChanger = Boolean(options.randomToolChanger);
const pockets = [createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 })];
const diagnostics = [];
const lines = String(text).split(/\r?\n/);
let nonRandomIdx = 1;
lines.forEach((rawLine, index) => {
const lineNumber = index + 1;
const [bodyPart, ...commentParts] = rawLine.split(";");
const body = bodyPart.trim();
const comment = commentParts.join(";").trim();
if (!body) return;
const words = {};
for (const match of body.matchAll(TOOL_WORD_PATTERN)) {
words[match[1].toUpperCase()] = Number(match[2]);
}
if (!Number.isFinite(words.T)) {
diagnostics.push({
line: lineNumber,
severity: "warning",
message: "tool table row missing T word",
rawLine,
});
return;
}
if (!Number.isFinite(words.P)) {
diagnostics.push({
line: lineNumber,
severity: "warning",
message: "tool table row missing P word",
rawLine,
});
}
const realPocket = Number.isFinite(words.P) ? words.P : words.T;
const idx = randomToolChanger ? realPocket : nonRandomIdx;
if (!randomToolChanger) nonRandomIdx += 1;
const entry = normalizeToolEntry({
idx,
line: lineNumber,
rawLine,
comment,
toolNumber: words.T,
pocket: realPocket,
diameter: Number.isFinite(words.D) ? words.D : 0,
orientation: Number.isFinite(words.Q) ? words.Q : 0,
frontAngle: Number.isFinite(words.I) ? words.I : 0,
backAngle: Number.isFinite(words.J) ? words.J : 0,
offset: offsetFromWords(words),
});
if (pockets[idx]?.toolNumber > 0) {
diagnostics.push({
line: lineNumber,
severity: "warning",
message: `multiple tools assigned to internal pocket/index ${idx}`,
rawLine,
});
}
pockets[idx] = entry;
});
const entries = compactToolEntries(pockets);
return {
apiName: "web-rtcp-5axis-tool-table",
sourceRel: options.sourceRel || null,
path: options.path || null,
randomToolChanger,
entries,
pockets: normalizePockets(pockets),
toolCount: entries.length,
diagnostics,
rawText: String(text),
semanticBoundary: "linuxcnc_tool_table_text_parsed_for_web_simulation",
linuxCncReferences: [
"linuxcnc/src/emc/tooldata/tooldata_common.cc:70",
"linuxcnc/src/emc/tooldata/tooldata_common.cc:365",
"linuxcnc/src/emc/nml_intf/emctool.h:29",
],
};
}
export function serializeLinuxCncToolTable(toolTable) {
const table = normalizeToolTable(toolTable);
const startIdx = table.randomToolChanger ? 0 : 1;
const lines = [];
for (let idx = startIdx; idx < table.pockets.length; idx += 1) {
const entry = table.pockets[idx];
if (!entry || entry.toolNumber < 0) continue;
lines.push(formatLinuxCncToolLine(entry, table.randomToolChanger));
}
return `${lines.join("\n")}${lines.length ? "\n" : ""}`;
}
export function createToolDbSimulation({ toolTable = null, profile = null, storageMode = null } = {}) {
const normalizedTable = normalizeToolTable(toolTable?.apiName === "web-rtcp-5axis-tool-table"
? toolTable
: parseLinuxCncToolTable("", { sourceRel: profile?.toolTablePath || null }));
const spindleEntry = normalizedTable.pockets[0] || createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 });
return {
apiName: "web-rtcp-5axis-tool-db-simulation",
ready: true,
processReady: true,
processScope: "web_simulation_only",
hostProcessReady: false,
hostProcessExecution: false,
dbProgramReady: false,
storageMode: storageMode || null,
storageCapability: null,
profileId: profile?.id || null,
toolTablePath: normalizedTable.sourceRel || profile?.toolTablePath || null,
randomToolChanger: normalizedTable.randomToolChanger,
toolTable: normalizedTable,
toolInSpindle: spindleEntry.toolNumber > 0 ? spindleEntry.toolNumber : 0,
toolFromPocket: spindleEntry.toolNumber > 0 ? spindleEntry.pocket : 0,
pocketPrepped: -1,
preparedTool: null,
preparedPocket: null,
preparedIndex: null,
currentPocket: spindleEntry.toolNumber > 0 ? spindleEntry.idx : 0,
activeToolOffset: null,
toolTableCurrent: cloneToolEntry(spindleEntry),
iocontrol: createInitialIoControlPins(spindleEntry),
emcioStatus: createInitialEmcioStatus(spindleEntry),
interpreterParameters: createToolParameters(null),
events: [],
lastSavedText: null,
lastSavedPath: null,
semanticBoundary: "linuxcnc_tool_db_web_simulation_not_native_db_program",
linuxCncReferences: [
"linuxcnc/src/emc/task/taskclass.cc:281",
"linuxcnc/src/emc/task/taskclass.cc:425",
"linuxcnc/src/emc/task/taskclass.cc:482",
"linuxcnc/src/emc/task/taskclass.cc:614",
"linuxcnc/src/emc/rs274ngc/interp_convert.cc:6236",
"linuxcnc/src/emc/task/emccanon.cc:3075",
],
};
}
export function listToolEntries(toolDb) {
return compactToolEntries(normalizeToolTable(toolDb?.toolTable).pockets).map(cloneToolEntry);
}
export function queryToolEntry(toolDb, selector = {}) {
const table = normalizeToolTable(toolDb?.toolTable);
const entry = resolveToolEntry(table, selector);
return entry ? cloneToolEntry(entry) : null;
}
export function prepareTool(toolDb, toolNumber, options = {}) {
let state = cloneToolDb(toolDb);
const idx = findToolIndexForTool(state.toolTable, toolNumber, state.randomToolChanger);
const entry = idx >= 0 ? state.toolTable.pockets[idx] : null;
state.preparedTool = Number(toolNumber);
state.preparedIndex = idx >= 0 ? idx : null;
state.preparedPocket = entry?.pocket ?? null;
if (idx === -1) {
state.preparedTool = null;
state.pocketPrepped = -1;
state.iocontrol.toolPrepIndex = -1;
state.iocontrol.toolPrepNumber = 0;
state.iocontrol.toolPrepPocket = 0;
state.iocontrol.toolPrepare = false;
state.iocontrol.toolPrepared = false;
state.emcioStatus.status = "ERROR";
state.events.push(createToolDbEvent("T", {
toolNumber: Number(toolNumber),
idx,
pocket: null,
found: false,
error: "tool_number_not_found",
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:425",
}));
return state;
} else {
state.iocontrol.toolPrepIndex = idx;
if (state.randomToolChanger || idx !== 0) {
state.iocontrol.toolPrepNumber = entry?.toolNumber ?? 0;
state.iocontrol.toolPrepPocket = entry?.pocket ?? 0;
} else {
state.iocontrol.toolPrepNumber = 0;
state.iocontrol.toolPrepPocket = 0;
}
}
state.iocontrol.toolPrepare = true;
state.iocontrol.toolPrepared = options.toolPrepared ?? true;
state.emcioStatus.status = "EXEC";
state.events.push(createToolDbEvent("T", {
toolNumber: Number(toolNumber),
idx,
pocket: entry?.pocket ?? null,
found: Boolean(entry),
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:425",
}));
if (options.autoReadToolInputs !== false) {
state = readToolInputs(state);
}
return state;
}
export function loadPreparedTool(toolDb, options = {}) {
let state = cloneToolDb(toolDb);
if (state.pocketPrepped === -1 && state.iocontrol.toolPrepare && state.iocontrol.toolPrepared) {
state = readToolInputs(state);
}
const idx = state.pocketPrepped;
const entry = idx >= 0 ? state.toolTable.pockets[idx] : null;
if (state.randomToolChanger && idx === 0) {
state.events.push(createToolDbEvent("M6", {
idx,
noOp: true,
reason: "random_toolchanger_spindle_pocket",
}));
return state;
}
if (!state.randomToolChanger && idx > 0 && state.toolInSpindle === entry?.toolNumber) {
state.events.push(createToolDbEvent("M6", {
idx,
toolNumber: state.toolInSpindle,
noOp: true,
reason: "tool_already_in_spindle",
}));
return state;
}
if (idx !== -1) {
state.iocontrol.toolChange = true;
state.iocontrol.toolChanged = options.toolChanged ?? true;
state.emcioStatus.status = "EXEC";
}
state.events.push(createToolDbEvent("M6", {
idx,
toolNumber: entry?.toolNumber ?? 0,
pocket: entry?.pocket ?? 0,
found: Boolean(entry),
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:482",
}));
if (options.autoReadToolInputs !== false) {
state = readToolInputs(state);
}
return state;
}
export function readToolInputs(toolDb) {
let state = cloneToolDb(toolDb);
if (state.iocontrol.toolPrepare && state.iocontrol.toolPrepared) {
state.pocketPrepped = state.iocontrol.toolPrepIndex;
state.emcioStatus.tool.pocketPrepped = state.pocketPrepped;
state.iocontrol.toolPrepare = false;
state.emcioStatus.status = "DONE";
state.events.push(createToolDbEvent("READ_TOOL_INPUTS_PREPARED", {
pocketPrepped: state.pocketPrepped,
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:616",
}));
}
if (state.iocontrol.toolChange && state.iocontrol.toolChanged) {
const idx = state.pocketPrepped;
let loaded = createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 });
if (!state.randomToolChanger && idx === 0) {
state.toolTable.pockets[0] = loaded;
} else {
const entry = state.toolTable.pockets[idx] || createEmptyToolEntry({ idx, pocket: 0, toolNumber: 0 });
loaded = cloneToolEntry({
...entry,
idx: 0,
});
if (state.randomToolChanger) {
loaded.pocket = 0;
const previousSpindle = state.toolTable.pockets[0] || createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 });
state.toolTable.pockets[idx] = normalizeToolEntry({
...previousSpindle,
idx,
pocket: entry.pocket,
});
}
state.toolTable.pockets[0] = loaded;
}
state.toolTable = refreshToolTable(state.toolTable);
state.toolInSpindle = loaded.toolNumber > 0 ? loaded.toolNumber : 0;
state.toolFromPocket = state.toolInSpindle > 0 ? loaded.pocket : 0;
state.currentPocket = state.toolInSpindle > 0 ? idx : 0;
state.toolTableCurrent = cloneToolEntry(loaded);
state.iocontrol.toolNumber = state.toolInSpindle;
state.iocontrol.toolFromPocket = state.toolFromPocket;
state.emcioStatus.tool.toolInSpindle = state.toolInSpindle;
state.emcioStatus.tool.toolFromPocket = state.toolFromPocket;
state.emcioStatus.tool.pocketPrepped = -1;
state.pocketPrepped = -1;
state.preparedTool = null;
state.preparedPocket = null;
state.preparedIndex = null;
state.iocontrol.toolPrepNumber = 0;
state.iocontrol.toolPrepPocket = 0;
state.iocontrol.toolPrepIndex = 0;
state.iocontrol.toolChange = false;
state.emcioStatus.status = "DONE";
state.events.push(createToolDbEvent("READ_TOOL_INPUTS_CHANGED", {
toolNumber: state.toolInSpindle,
toolFromPocket: state.toolFromPocket,
currentPocket: state.currentPocket,
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:623",
}));
}
return state;
}
export function applyToolLengthOffset(toolDb, toolNumber = null) {
const state = cloneToolDb(toolDb);
const table = normalizeToolTable(state.toolTable);
const idx = Number.isFinite(Number(toolNumber))
? findToolIndexForTool(table, Number(toolNumber), state.randomToolChanger)
: 0;
const entry = idx >= 0 ? table.pockets[idx] : null;
state.activeToolOffset = entry
? createActiveToolOffset(entry, idx)
: null;
state.interpreterParameters = createToolParameters(state.activeToolOffset);
state.events.push(createToolDbEvent("G43", {
idx,
toolNumber: toolNumber ?? state.toolInSpindle,
activeToolOffset: state.activeToolOffset,
found: Boolean(entry),
linuxCncReference: "linuxcnc/src/emc/rs274ngc/interp_convert.cc:6236",
}));
return state;
}
export function setToolNumber(toolDb, toolNumber) {
let state = cloneToolDb(toolDb);
if (!state.randomToolChanger && Number(toolNumber) === 0) {
const unloaded = createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 });
state.toolTable.pockets[0] = unloaded;
state.toolTable = refreshToolTable(state.toolTable);
state.toolInSpindle = 0;
state.toolFromPocket = 0;
state.currentPocket = 0;
state.toolTableCurrent = cloneToolEntry(unloaded);
state.iocontrol.toolNumber = 0;
state.iocontrol.toolFromPocket = 0;
state.emcioStatus.tool.toolInSpindle = 0;
state.emcioStatus.tool.toolFromPocket = 0;
state.events.push(createToolDbEvent("M61", {
toolNumber: 0,
idx: 0,
pocket: 0,
found: true,
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:572",
}));
return state;
}
const idx = findToolIndexForTool(state.toolTable, toolNumber, state.randomToolChanger);
const entry = idx >= 0 ? state.toolTable.pockets[idx] : null;
if (!entry) {
state.events.push(createToolDbEvent("M61", {
toolNumber: Number(toolNumber),
idx,
found: false,
}));
return state;
}
state.toolTable.pockets[0] = cloneToolEntry({ ...entry, idx: 0 });
state.toolTable = refreshToolTable(state.toolTable);
state.toolInSpindle = entry.toolNumber;
state.toolFromPocket = entry.toolNumber > 0 ? entry.pocket : 0;
state.currentPocket = idx;
state.toolTableCurrent = cloneToolEntry(state.toolTable.pockets[0]);
state.iocontrol.toolNumber = state.toolInSpindle;
state.iocontrol.toolFromPocket = state.toolFromPocket;
state.emcioStatus.tool.toolInSpindle = state.toolInSpindle;
state.emcioStatus.tool.toolFromPocket = state.toolFromPocket;
state.events.push(createToolDbEvent("M61", {
toolNumber: state.toolInSpindle,
idx,
pocket: state.toolFromPocket,
found: true,
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:572",
}));
return state;
}
export function editToolEntry(toolDb, patch = {}) {
const state = cloneToolDb(toolDb);
const selector = {
idx: patch.idx,
toolNumber: patch.toolNumber ?? patch.toolno,
pocket: patch.pocket ?? patch.pocketno,
};
const current = resolveToolEntry(state.toolTable, selector);
const idx = Number.isFinite(Number(patch.idx))
? Number(patch.idx)
: current?.idx ?? nextAvailableToolIndex(state.toolTable);
const nextEntry = normalizeToolEntry({
...(current || createEmptyToolEntry({ idx, pocket: patch.pocket ?? idx, toolNumber: patch.toolNumber ?? idx })),
...patch,
idx,
toolNumber: patch.toolNumber ?? patch.toolno ?? current?.toolNumber ?? idx,
pocket: patch.pocket ?? patch.pocketno ?? current?.pocket ?? idx,
diameter: patch.diameter ?? current?.diameter ?? 0,
frontAngle: patch.frontAngle ?? patch.frontangle ?? current?.frontAngle ?? 0,
backAngle: patch.backAngle ?? patch.backangle ?? current?.backAngle ?? 0,
orientation: patch.orientation ?? current?.orientation ?? 0,
offset: {
...(current?.offset || createZeroOffset()),
...(patch.offset || {}),
...(Number.isFinite(Number(patch.length)) ? { z: Number(patch.length) } : {}),
...(Number.isFinite(Number(patch.toolLength)) ? { z: Number(patch.toolLength) } : {}),
...(Number.isFinite(Number(patch.zOffset)) ? { z: Number(patch.zOffset) } : {}),
},
});
state.toolTable.pockets[idx] = nextEntry;
if (state.currentPocket === idx || state.toolInSpindle === nextEntry.toolNumber) {
state.toolTable.pockets[0] = cloneToolEntry({ ...nextEntry, idx: 0 });
state.toolTableCurrent = cloneToolEntry(state.toolTable.pockets[0]);
state.toolInSpindle = nextEntry.toolNumber;
state.toolFromPocket = nextEntry.pocket;
state.iocontrol.toolNumber = state.toolInSpindle;
state.iocontrol.toolFromPocket = state.toolFromPocket;
state.emcioStatus.tool.toolInSpindle = state.toolInSpindle;
state.emcioStatus.tool.toolFromPocket = state.toolFromPocket;
}
if (state.activeToolOffset?.idx === idx || state.activeToolOffset?.toolNumber === nextEntry.toolNumber) {
state.activeToolOffset = createActiveToolOffset(nextEntry, idx);
state.interpreterParameters = createToolParameters(state.activeToolOffset);
}
state.toolTable = refreshToolTable(state.toolTable);
state.events.push(createToolDbEvent("EDIT_TOOL", {
idx,
toolNumber: nextEntry.toolNumber,
pocket: nextEntry.pocket,
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:527",
}));
return state;
}
export async function saveToolDbSimulation(toolDb, options = {}) {
const state = cloneToolDb(toolDb);
const text = serializeLinuxCncToolTable(state.toolTable);
const path = options.path || toolDbStoragePath(state);
const storageInfo = resolveToolDbStorage(options);
await saveTextFile(path, text, storageInfo.storage);
state.lastSavedText = text;
state.lastSavedPath = path;
state.storageMode = options.storageMode || storageInfo.mode;
state.storageCapability = storageInfo.capability;
state.events.push(createToolDbEvent("SAVE_TOOL_TABLE", {
path,
bytes: text.length,
storageMode: state.storageMode,
linuxCncReference: "linuxcnc/src/emc/tooldata/tooldata_common.cc:383",
}));
return {
toolDb: state,
path,
text,
storageMode: state.storageMode,
storageCapability: storageInfo.capability,
semanticBoundary: "tool_table_saved_in_web_storage_boundary",
};
}
export function applyToolCommandSequence(toolDb, commands = []) {
return commands.reduce((state, command) => {
const code = String(command.code || command).toUpperCase();
if (code === "T") return prepareTool(state, command.toolNumber);
if (code === "M6") return loadPreparedTool(state);
if (code === "G43") return applyToolLengthOffset(state, command.toolNumber ?? command.h ?? null);
if (code === "M61") return setToolNumber(state, command.toolNumber ?? command.q);
throw new Error(`unsupported tool DB simulation command: ${code}`);
}, toolDb);
}
export function extractToolCommandSequenceFromProgram(programText = "") {
const commands = [];
const lines = String(programText).split(/\r?\n/);
lines.forEach((rawLine, index) => {
const line = rawLine
.replace(/\([^)]*\)/g, " ")
.split(";")[0];
const words = [...line.matchAll(/([GMTQH])\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+))/gi)]
.map((match) => ({
letter: match[1].toUpperCase(),
value: Number(match[2]),
index: match.index ?? 0,
}))
.filter((word) => Number.isFinite(word.value));
if (!words.length) return;
const hWord = words.find((word) => word.letter === "H");
const qWord = words.find((word) => word.letter === "Q");
for (const word of words) {
if (word.letter === "T") {
commands.push(createProgramToolCommand("T", index + 1, rawLine, {
toolNumber: word.value,
}));
} else if (word.letter === "M" && word.value === 6) {
commands.push(createProgramToolCommand("M6", index + 1, rawLine));
} else if (word.letter === "M" && word.value === 61 && Number.isFinite(qWord?.value)) {
commands.push(createProgramToolCommand("M61", index + 1, rawLine, {
q: qWord?.value,
toolNumber: qWord?.value,
}));
} else if (word.letter === "G" && word.value === 43) {
commands.push(createProgramToolCommand("G43", index + 1, rawLine, {
h: hWord?.value,
toolNumber: hWord?.value,
}));
}
}
});
return commands;
}
export function createToolDbReadiness(toolDb) {
const ready = toolDb?.processReady === true && toolDb?.processScope === "web_simulation_only";
return {
apiName: "web-rtcp-5axis-tool-db-readiness",
ready,
toolDbProcessReady: ready,
toolDbProcessScope: ready ? "web_simulation_only" : "not_ready",
hostToolDbProcessReady: false,
hostProcessExecution: false,
dbProgramReady: false,
toolCount: toolDb?.toolTable?.toolCount || 0,
toolTablePath: toolDb?.toolTablePath || null,
storageMode: toolDb?.storageMode || null,
semanticBoundary: "tool_db_process_ready_for_web_simulation_only",
};
}
export function detectToolDbStorageCapability(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-tool-db-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",
};
}
export function createMemoryToolDbStorage(seed = {}) {
const files = new Map(Object.entries(seed));
return {
apiName: "web-rtcp-5axis-memory-tool-db-storage",
files,
async getDirectory() {
return createDirectoryHandle(files, []);
},
};
}
function createProgramToolCommand(code, line, rawLine, fields = {}) {
return {
code,
line,
rawLine,
...fields,
semanticBoundary: "linuxcnc_tool_command_scanned_for_web_simulation",
};
}
function normalizeToolTable(toolTable = {}) {
const randomToolChanger = Boolean(toolTable.randomToolChanger);
const rawPockets = Array.isArray(toolTable.pockets)
? toolTable.pockets
: pocketsFromEntries(toolTable.entries || [], randomToolChanger);
const pockets = normalizePockets(rawPockets);
const entries = compactToolEntries(pockets);
return {
apiName: "web-rtcp-5axis-tool-table",
sourceRel: toolTable.sourceRel || null,
path: toolTable.path || null,
randomToolChanger,
entries,
pockets,
toolCount: entries.length,
diagnostics: [...(toolTable.diagnostics || [])],
rawText: typeof toolTable.rawText === "string" ? toolTable.rawText : "",
semanticBoundary: toolTable.semanticBoundary || "linuxcnc_tool_table_text_parsed_for_web_simulation",
linuxCncReferences: toolTable.linuxCncReferences || [],
};
}
function refreshToolTable(toolTable) {
const table = normalizeToolTable(toolTable);
return {
...table,
rawText: serializeLinuxCncToolTable(table),
};
}
function pocketsFromEntries(entries = [], randomToolChanger = false) {
const pockets = [createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 })];
let nextIdx = 1;
for (const entry of entries) {
const idx = Number.isFinite(Number(entry.idx))
? Number(entry.idx)
: randomToolChanger
? Number(entry.pocket ?? entry.pocketno ?? entry.P ?? nextIdx)
: nextIdx;
pockets[idx] = normalizeToolEntry({ ...entry, idx });
if (!randomToolChanger && idx >= nextIdx) nextIdx = idx + 1;
}
return pockets;
}
function normalizePockets(rawPockets = []) {
const pockets = [...rawPockets];
pockets[0] = normalizeToolEntry(pockets[0] || createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 }));
for (let idx = 1; idx < pockets.length; idx += 1) {
if (pockets[idx]) pockets[idx] = normalizeToolEntry({ ...pockets[idx], idx });
}
return pockets;
}
function compactToolEntries(pockets = []) {
return pockets
.slice(1)
.filter((entry) => entry && entry.toolNumber >= 0)
.map(cloneToolEntry)
.sort((left, right) => left.idx - right.idx);
}
function normalizeToolEntry(entry = {}) {
const toolNumber = Number(entry.toolNumber ?? entry.toolno ?? entry.T ?? 0);
const idx = Number(entry.idx ?? entry.index ?? entry.P ?? entry.pocket ?? 0);
return {
idx,
line: entry.line || null,
rawLine: entry.rawLine || null,
comment: entry.comment || "",
toolNumber,
toolno: toolNumber,
pocket: Number(entry.pocket ?? entry.pocketno ?? entry.P ?? idx),
pocketno: Number(entry.pocket ?? entry.pocketno ?? entry.P ?? idx),
diameter: Number(entry.diameter ?? entry.D ?? 0),
orientation: Number(entry.orientation ?? entry.Q ?? 0),
frontAngle: Number(entry.frontAngle ?? entry.frontangle ?? entry.I ?? 0),
backAngle: Number(entry.backAngle ?? entry.backangle ?? entry.J ?? 0),
offset: {
x: Number(entry.offset?.x ?? entry.x ?? entry.X ?? 0),
y: Number(entry.offset?.y ?? entry.y ?? entry.Y ?? 0),
z: Number(entry.offset?.z ?? entry.z ?? entry.Z ?? entry.length ?? entry.toolLength ?? 0),
a: Number(entry.offset?.a ?? entry.a ?? entry.A ?? 0),
b: Number(entry.offset?.b ?? entry.b ?? entry.B ?? 0),
c: Number(entry.offset?.c ?? entry.c ?? entry.C ?? 0),
u: Number(entry.offset?.u ?? entry.u ?? entry.U ?? 0),
v: Number(entry.offset?.v ?? entry.v ?? entry.V ?? 0),
w: Number(entry.offset?.w ?? entry.w ?? entry.W ?? 0),
},
};
}
function createEmptyToolEntry({ idx = 0, pocket = -1, toolNumber = -1 } = {}) {
return normalizeToolEntry({
idx,
pocket,
toolNumber,
diameter: 0,
frontAngle: 0,
backAngle: 0,
orientation: 0,
offset: createZeroOffset(),
});
}
function cloneToolEntry(entry) {
const normalized = normalizeToolEntry(entry);
return {
...normalized,
offset: { ...normalized.offset },
};
}
function createZeroOffset() {
return {
x: 0,
y: 0,
z: 0,
a: 0,
b: 0,
c: 0,
u: 0,
v: 0,
w: 0,
};
}
function offsetFromWords(words) {
return Object.fromEntries(TOOL_OFFSET_AXES.map((axis) => {
const letter = axis.toUpperCase();
return [axis, Number.isFinite(words[letter]) ? words[letter] : 0];
}));
}
function resolveToolEntry(toolTable, selector = {}) {
const table = normalizeToolTable(toolTable);
if (Number.isFinite(Number(selector.idx))) {
return table.pockets[Number(selector.idx)] || null;
}
if (Number.isFinite(Number(selector.toolNumber ?? selector.toolno))) {
const idx = findToolIndexForTool(table, Number(selector.toolNumber ?? selector.toolno), table.randomToolChanger);
return idx >= 0 ? table.pockets[idx] : null;
}
if (Number.isFinite(Number(selector.pocket ?? selector.pocketno))) {
const pocket = Number(selector.pocket ?? selector.pocketno);
return table.pockets.find((entry, idx) => idx > 0 && entry?.pocket === pocket) ||
table.pockets.find((entry) => entry?.pocket === pocket) ||
null;
}
return null;
}
function findToolIndexForTool(toolTable, toolNumber, randomToolChanger = false) {
const number = Number(toolNumber);
if (!randomToolChanger && number === 0) return 0;
const table = normalizeToolTable(toolTable);
const nonSpindleIndex = table.pockets.findIndex((entry, idx) => idx > 0 && entry?.toolNumber === number);
if (nonSpindleIndex >= 0) return nonSpindleIndex;
const index = table.pockets.findIndex((entry) => entry?.toolNumber === number);
return index >= 0 ? index : -1;
}
function nextAvailableToolIndex(toolTable) {
const table = normalizeToolTable(toolTable);
for (let idx = 1; idx < table.pockets.length; idx += 1) {
if (!table.pockets[idx] || table.pockets[idx].toolNumber < 0) return idx;
}
return Math.max(table.pockets.length, 1);
}
function createInitialIoControlPins(spindleEntry = null) {
const loaded = spindleEntry?.toolNumber > 0 ? spindleEntry : null;
return {
toolPrepare: false,
toolPrepared: false,
toolPrepNumber: 0,
toolPrepPocket: 0,
toolPrepIndex: 0,
toolChange: false,
toolChanged: false,
toolNumber: loaded?.toolNumber || 0,
toolFromPocket: loaded?.pocket || 0,
};
}
function createInitialEmcioStatus(spindleEntry = null) {
const loaded = spindleEntry?.toolNumber > 0 ? spindleEntry : null;
return {
status: "DONE",
tool: {
toolInSpindle: loaded?.toolNumber || 0,
toolFromPocket: loaded?.pocket || 0,
pocketPrepped: -1,
},
};
}
function cloneToolDb(toolDb) {
const toolTable = normalizeToolTable(toolDb?.toolTable);
const spindleEntry = toolTable.pockets[0] || createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 });
return {
...toolDb,
randomToolChanger: Boolean(toolDb?.randomToolChanger ?? toolTable.randomToolChanger),
toolTable,
toolTableCurrent: toolDb?.toolTableCurrent
? cloneToolEntry(toolDb.toolTableCurrent)
: cloneToolEntry(spindleEntry),
iocontrol: { ...createInitialIoControlPins(spindleEntry), ...(toolDb?.iocontrol || {}) },
emcioStatus: {
...createInitialEmcioStatus(spindleEntry),
...(toolDb?.emcioStatus || {}),
tool: {
...createInitialEmcioStatus(spindleEntry).tool,
...(toolDb?.emcioStatus?.tool || {}),
},
},
activeToolOffset: toolDb?.activeToolOffset
? {
...toolDb.activeToolOffset,
offset: { ...toolDb.activeToolOffset.offset },
}
: null,
interpreterParameters: { ...(toolDb?.interpreterParameters || createToolParameters(null)) },
events: [...(toolDb?.events || [])],
};
}
function createActiveToolOffset(entry, idx) {
return {
idx,
toolNumber: entry.toolNumber,
pocket: entry.pocket,
offset: { ...entry.offset },
diameter: entry.diameter,
frontAngle: entry.frontAngle,
backAngle: entry.backAngle,
orientation: entry.orientation,
};
}
function createToolParameters(activeOffset) {
const parameters = {
[TOOL_PARAMETER_KEYS.toolNumber]: activeOffset?.toolNumber || 0,
};
for (const axis of TOOL_OFFSET_AXES) {
parameters[TOOL_PARAMETER_KEYS[axis]] = activeOffset?.offset?.[axis] || 0;
}
parameters[TOOL_PARAMETER_KEYS.diameter] = activeOffset?.diameter || 0;
parameters[TOOL_PARAMETER_KEYS.frontAngle] = activeOffset?.frontAngle || 0;
parameters[TOOL_PARAMETER_KEYS.backAngle] = activeOffset?.backAngle || 0;
parameters[TOOL_PARAMETER_KEYS.orientation] = activeOffset?.orientation || 0;
return parameters;
}
function createToolDbEvent(code, payload = {}) {
return {
apiName: "web-rtcp-5axis-tool-db-event",
code,
payload,
createdAt: new Date().toISOString(),
semanticBoundary: "linuxcnc_tool_task_semantics_mirrored_for_web_simulation",
promotionScope: "web_simulation_only",
hostProcessExecution: false,
};
}
function formatLinuxCncToolLine(entry, randomToolChanger) {
const normalized = normalizeToolEntry(entry);
const pocketWord = randomToolChanger ? normalized.idx : normalized.pocket;
const words = [
`T${String(formatInteger(normalized.toolNumber)).padEnd(3, " ")}`,
`P${String(formatInteger(pocketWord)).padEnd(3, " ")}`,
];
if (normalized.diameter !== 0) words.push(`D${formatLinuxCncFloat(normalized.diameter)}`);
for (const axis of TOOL_OFFSET_AXES) {
const value = normalized.offset[axis];
if (value !== 0) words.push(`${axis.toUpperCase()}${formatLinuxCncFloat(value)}`);
}
if (normalized.frontAngle !== 0) words.push(`I${formatLinuxCncFloat(normalized.frontAngle)}`);
if (normalized.backAngle !== 0) words.push(`J${formatLinuxCncFloat(normalized.backAngle)}`);
if (normalized.orientation !== 0) words.push(`Q${formatInteger(normalized.orientation)}`);
return `${words.join(" ")}${normalized.comment ? ` ;${normalized.comment}` : ""}`;
}
function formatInteger(value) {
const number = Number(value);
if (!Number.isFinite(number)) return 0;
return Math.trunc(number);
}
function formatLinuxCncFloat(value) {
const number = Number(value);
if (!Number.isFinite(number)) return "+0.000000";
return `${number >= 0 ? "+" : ""}${number.toFixed(6)}`;
}
function toolDbStoragePath(toolDb) {
const profileId = toolDb.profileId || "unknown-profile";
const filename = basename(toolDb.toolTablePath || "tool.tbl");
return `${TOOL_DB_STORAGE_ROOT}/${profileId}/${filename}`;
}
function basename(path = "") {
return String(path).split("/").filter(Boolean).pop() || "";
}
function resolveToolDbStorage(options = {}) {
if (options.storage) {
return {
storage: options.storage,
mode: options.storageMode || storageModeFor(options.storage),
capability: {
apiName: "web-rtcp-5axis-tool-db-storage-capability",
opfsAvailable: options.storageMode === "opfs",
opfsUnavailable: options.storageMode !== "opfs",
fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom",
reason: "explicit_storage",
},
};
}
const capability = detectToolDbStorageCapability();
if (capability.opfsAvailable) {
return {
storage: globalThis.navigator.storage,
mode: "opfs",
capability,
};
}
browserMemoryToolDbStorage ??= createMemoryToolDbStorage();
return {
storage: browserMemoryToolDbStorage,
mode: "memory-fallback",
capability,
};
}
function storageModeFor(storage) {
return storage?.apiName === "web-rtcp-5axis-memory-tool-db-storage" ||
storage?.apiName === "web-rtcp-5axis-memory-session-storage"
? "memory"
: "custom";
}
async function saveTextFile(path, text, storage) {
const root = await storage.getDirectory();
const parts = path.split("/").filter(Boolean);
let directory = root;
for (const part of parts.slice(0, -1)) {
directory = await directory.getDirectoryHandle(part, { create: true });
}
const handle = await directory.getFileHandle(parts[parts.length - 1], { create: true });
const writable = await handle.createWritable();
await writable.write(text);
await writable.close();
}
function createDirectoryHandle(files, prefix) {
return {
async getDirectoryHandle(name, options = {}) {
const nextPrefix = [...prefix, name];
if (!options.create) {
const hasChild = [...files.keys()].some((path) => path.startsWith(`${nextPrefix.join("/")}/`));
if (!hasChild) throw new Error(`directory not found: ${nextPrefix.join("/")}`);
}
return createDirectoryHandle(files, nextPrefix);
},
async getFileHandle(name, options = {}) {
const path = [...prefix, name].join("/");
if (!options.create && !files.has(path)) {
throw new Error(`file not found: ${path}`);
}
return {
async createWritable() {
let buffer = "";
return {
async write(chunk) {
buffer += String(chunk);
},
async close() {
files.set(path, buffer);
},
};
},
async getFile() {
return {
async text() {
return files.get(path) || "";
},
};
},
};
},
};
}

View File

@@ -21,6 +21,23 @@ import {
buildTaskHalProgramMotionPlan,
buildTaskHalSessionFromMachineFiles,
} from "../runtime/linuxcnc-task-hal-runtime.js";
import {
applyToolCommandSequence,
createToolDbReadiness,
createToolDbSimulation,
editToolEntry,
extractToolCommandSequenceFromProgram,
listToolEntries,
parseLinuxCncToolTable,
queryToolEntry,
saveToolDbSimulation,
} from "../runtime/tool-db-simulation.js";
import {
createControlledUserMReadiness,
createControlledUserMSimulation,
runControlledUserM,
runControlledUserMProgramScan,
} from "../runtime/controlled-user-m-simulation.js";
import { createLinuxCncParityMatrix } from "../runtime/linuxcnc-parity-matrix.js";
import { gmoccapyHalModel, resolveGmoccapyHardwareButton } from "../runtime/gmoccapy-hal-model.js";
import {
@@ -36,6 +53,7 @@ const initialLinuxCncBoundaryAdapter = createLinuxCncBoundaryAdapter({
profile: defaultProfile,
});
const initialLinuxCncBoundaryReadiness = createLinuxCncBoundaryReadiness(initialLinuxCncBoundaryAdapter);
const initialControlledUserMSimulation = createControlledUserMSimulation();
const MACHINE_PROJECT_OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
const initialAxisPose = {
@@ -195,6 +213,10 @@ const initialState = {
interpreterExecutionPending: false,
interpreterExecutionSequence: 0,
machineFileExecution: null,
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
controlledUserMSimulation: initialControlledUserMSimulation,
controlledUserMReadiness: createControlledUserMReadiness(initialControlledUserMSimulation),
fullExecutionBoundary: null,
linuxCncTaskPolicy: null,
asyncFrameRefreshPending: false,
@@ -510,6 +532,7 @@ export function createSimulationStore(seed = {}) {
case "SET_PROFILE":
{
const profile = getFiveAxisProfile(action.profileId);
const defaultKinsType = defaultKinsTypeForProfile(profile);
const adapter = createLinuxCncBoundaryAdapter({
profile,
runtime: {
@@ -521,11 +544,14 @@ export function createSimulationStore(seed = {}) {
machineProfile: profile.id,
profile,
activeProgram: profile.samplePrograms[0] || state.activeProgram,
kinsType: "identity",
rtcpState: "off",
kinsType: defaultKinsType,
rtcpState: rtcpStateFromKinsType(defaultKinsType),
kinematicsRuntime: null,
kinematicsRuntimeReadiness: null,
kinematicsExecutionContext: "none",
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
...createInitialControlledUserMState(),
linuxCncIniConfig: null,
iniConfigReadiness: initialState.iniConfigReadiness,
linuxCncBoundaryAdapter: adapter,
@@ -881,6 +907,10 @@ export function createSimulationStore(seed = {}) {
|| action.plan.selectedProgramSourceRel
|| null,
},
...createToolDbStatePatchFromStagedFiles({
profile: state.profile,
save: action.save,
}),
operatorMessage: `LinuxCNC machine files staged ${action.save.fileCount}`,
});
if (state.taskHalRuntime?.loaded) {
@@ -909,8 +939,14 @@ export function createSimulationStore(seed = {}) {
sourceRel: selectedFile.sourceRel,
wasmPath: selectedFile.wasmPath,
});
const toolUserPatch = createProgramToolUserSimulationPatch({
state,
programText: selectedFile.text,
sourceRel: selectedFile.sourceRel,
});
setState({
...loadedProgram,
...toolUserPatch,
machineFileStaging: {
...state.machineFileStaging,
plan: selectedPlan,
@@ -938,6 +974,27 @@ export function createSimulationStore(seed = {}) {
}
}
break;
case "RUN_CONTROLLED_USER_M":
{
const result = runControlledUserM(
state.controlledUserMSimulation || createControlledUserMSimulation(),
action.code,
{
profile: state.profile,
sourceRel: action.sourceRel || state.programSourceRel || null,
line: action.line || null,
},
);
setState({
controlledUserMSimulation: result.simulation,
controlledUserMReadiness: createControlledUserMReadiness(result.simulation),
...result.statePatch,
operatorMessage: result.event.allowed
? `controlled user-M ${result.event.code} simulated`
: `controlled user-M ${result.event.code} blocked`,
});
}
break;
case "TASK_HAL_SESSION_READY":
if (action.session?.programSourceRel && action.session.programSourceRel !== state.machineFileStaging?.selectedGcodeSourceRel) {
break;
@@ -1373,8 +1430,14 @@ export function createSimulationStore(seed = {}) {
case "LOAD_PROGRAM":
{
const loadedProgram = buildLoadedProgram(action);
const toolCommands = extractToolCommandSequenceFromProgram(loadedProgram.programLines.join("\n"));
const toolDbSimulation = state.toolDbSimulation && toolCommands.length > 0
? applyToolCommandSequence(state.toolDbSimulation, toolCommands)
: state.toolDbSimulation;
setState({
...loadedProgram,
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
machine: {
...state.machine,
mode: "auto",
@@ -2065,6 +2128,40 @@ export function createSimulationStore(seed = {}) {
}
};
const queryToolDb = (selector = {}) => {
if (!state.toolDbSimulation) return null;
if (selector.all === true || selector.list === true) {
return listToolEntries(state.toolDbSimulation);
}
return queryToolEntry(state.toolDbSimulation, selector);
};
const editToolDb = (patch = {}) => {
if (!state.toolDbSimulation) {
throw new Error("tool DB simulation is not ready");
}
const toolDbSimulation = editToolEntry(state.toolDbSimulation, patch);
setState({
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
operatorMessage: `tool DB edited T${patch.toolNumber ?? patch.toolno ?? "-"}`,
});
return state.toolDbSimulation;
};
const saveToolDb = async (options = {}) => {
if (!state.toolDbSimulation) {
throw new Error("tool DB simulation is not ready");
}
const save = await saveToolDbSimulation(state.toolDbSimulation, options);
setState({
toolDbSimulation: save.toolDb,
toolDbReadiness: createToolDbReadiness(save.toolDb),
operatorMessage: `tool DB saved ${save.path} (${save.storageMode})`,
});
return save;
};
const runFullBoundaryAudit = async (options = {}) => {
if (state.machineFileStaging?.status !== "staged" || options.restage === true) {
await stageMachineFiles(options);
@@ -2480,6 +2577,9 @@ export function createSimulationStore(seed = {}) {
saveSession,
restoreSession,
stageMachineFiles,
queryToolDb,
editToolDb,
saveToolDb,
runFullBoundaryAudit,
initializeTaskHalSession,
};
@@ -2562,6 +2662,11 @@ function selectMachineFileProgramForState(state) {
function defaultLinuxCncGcodeSourceForState(state) {
const sources = state.machineFileStaging?.gcodeSources || [];
const defaultProgramFilename = state.profile?.machineFileStaging?.defaultProgramFilename;
if (defaultProgramFilename) {
const match = sources.find((source) => source.filename === defaultProgramFilename);
if (match) return match;
}
const preferred = `${state.machineProfile || "xyzac-trt"}_switchkins.ngc`;
return sources.find((source) => source.filename === preferred)
|| sources.find((source) => source.filename.includes(state.machineProfile || "xyzac"))
@@ -2569,6 +2674,66 @@ function defaultLinuxCncGcodeSourceForState(state) {
|| null;
}
function createInitialControlledUserMState() {
const simulation = createControlledUserMSimulation();
return {
controlledUserMSimulation: simulation,
controlledUserMReadiness: createControlledUserMReadiness(simulation),
};
}
function createToolDbStatePatchFromStagedFiles({ profile, save }) {
const toolTableFile = findStagedToolTableFile(profile, save);
if (!toolTableFile) {
return {
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
};
}
const toolTable = parseLinuxCncToolTable(toolTableFile.text, {
sourceRel: toolTableFile.sourceRel,
path: toolTableFile.wasmPath || toolTableFile.path || null,
});
const toolDbSimulation = createToolDbSimulation({
toolTable,
profile,
storageMode: save?.storageMode || null,
});
return {
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
};
}
function createProgramToolUserSimulationPatch({ state, programText, sourceRel }) {
const toolCommands = extractToolCommandSequenceFromProgram(programText);
const toolDbSimulation = state.toolDbSimulation && toolCommands.length > 0
? applyToolCommandSequence(state.toolDbSimulation, toolCommands)
: state.toolDbSimulation;
const userMScan = runControlledUserMProgramScan(
state.controlledUserMSimulation || createControlledUserMSimulation(),
programText,
{
profile: state.profile,
sourceRel,
},
);
return {
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
controlledUserMSimulation: userMScan.simulation,
controlledUserMReadiness: createControlledUserMReadiness(userMScan.simulation),
};
}
function findStagedToolTableFile(profile, save) {
const files = Array.isArray(save?.files) ? save.files : [];
return files.find((file) => file.sourceRel === profile?.toolTablePath)
|| files.find((file) => file.kind === "toolTable" && file.sourceRel?.endsWith(`${profile?.id}.tbl`))
|| files.find((file) => file.kind === "toolTable")
|| null;
}
function createMachineProjectState(state = {}) {
const profile = state.profile || {};
const profileId = profile.id || state.machineProfile || "unknown";
@@ -2589,6 +2754,8 @@ function createMachineProjectState(state = {}) {
const selectedProgram = gcodeFiles.find((file) => file.sourceRel === staging.selectedGcodeSourceRel)
|| null;
const projectRoot = staging.opfsRoot || `${MACHINE_PROJECT_OPFS_ROOT}/${profileId}`;
const machineRel = staging.plan?.machineRel || profile.machineFileStaging?.machineRel || "axis/vismach/5axis/table-rotary-tilting";
const demoDirectory = staging.plan?.demoDirectory || profile.machineFileStaging?.demoDirectory || "demos";
return {
apiName: "web-rtcp-5axis-machine-project",
@@ -2611,7 +2778,7 @@ function createMachineProjectState(state = {}) {
: null,
configFiles,
configFileCount: configFiles.length,
gcodeDirectory: `${projectRoot}/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos`,
gcodeDirectory: `${projectRoot}/configs/sim/${machineRel}/${demoDirectory}`,
gcodeFiles: gcodeFiles.map(projectFileDescriptor),
gcodeFileCount: gcodeFiles.length,
selectedProgram: selectedProgram ? projectFileDescriptor(selectedProgram) : null,
@@ -2791,8 +2958,11 @@ function countProjectFileKinds(files = []) {
}
function isLinuxCncFiveAxisDemoSource(sourceRel) {
return String(sourceRel || "").startsWith("configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/")
&& String(sourceRel || "").endsWith(".ngc");
const value = String(sourceRel || "");
return (
value.startsWith("configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/")
|| value.startsWith("configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/")
) && value.endsWith(".ngc");
}
function sourceBasename(path) {
@@ -2809,7 +2979,7 @@ export function validateRunPreconditions(state = {}, {
} = {}) {
const profile = state.profile || {};
const profileId = profile.id || state.machineProfile || "unknown";
const supportedProfiles = new Set(["xyzac-trt", "xyzbc-trt"]);
const supportedProfile = profile.rtcpProof !== false && Boolean(profile.kinematicsModuleId);
const fail = (operatorMessage, detail = {}) => ({
apiName: "web-rtcp-5axis-run-preconditions",
ok: false,
@@ -2821,7 +2991,7 @@ export function validateRunPreconditions(state = {}, {
...detail,
});
if (!supportedProfiles.has(profileId)) {
if (!supportedProfile) {
return fail(`run blocked: unsupported five-axis profile ${profileId}`);
}
@@ -3335,9 +3505,18 @@ function profileSupportsTcp(profile) {
}
function tcpKinsTypeForProfile(profile) {
return profile?.kinematicsParameters?.switchkinsTypes
?.find((type) => type.value === 1 && String(type.webKinsType || "").startsWith("tcp-"))
?.webKinsType || null;
const types = profile?.kinematicsParameters?.switchkinsTypes || [];
return types.find((type) => type.value === 1 && String(type.webKinsType || "").startsWith("tcp-"))
?.webKinsType
|| types.find((type) => String(type.webKinsType || "").startsWith("tcp-"))
?.webKinsType
|| null;
}
function defaultKinsTypeForProfile(profile) {
return profile?.kinematicsParameters?.fixedTrtDefault
? tcpKinsTypeForProfile(profile) || "identity"
: "identity";
}
function clampAxisPoseToProfile(axisPose, profile = defaultProfile) {

View File

@@ -651,6 +651,7 @@ function renderInfoTabs(element, state) {
<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>Host/native:</dt><dd data-full-execution-boundary="host-native">${formatHostNativeBoundary(state.fullExecutionBoundary)}</dd>
<dt>Tool/User process:</dt><dd data-full-execution-boundary="tool-user-process">${formatToolUserProcessBoundary(state)}</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>Interpreter:</dt><dd data-linuxcnc-boundary="interpreter">${state.interpreterRuntimeReadiness?.loaded ? state.interpreterRuntimeReadiness.semanticBoundary : "pending"}</dd>
@@ -817,8 +818,26 @@ function formatHostNativeBoundary(boundary) {
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",
boundary.externalUserMProcessReady ? "external user-M web simulation only" : "external user-M false",
boundary.toolDbProcessReady ? "tool DB web simulation only" : "tool DB false",
boundary.hostExternalUserMProcessReady ? "host external user-M ready" : "host external user-M false",
boundary.hostToolDbProcessReady ? "host tool DB ready" : "host tool DB false",
boundary.arbitraryUserMExecution ? "arbitrary user-M enabled" : "arbitrary user-M false",
].join(" / ");
}
function formatToolUserProcessBoundary(state) {
const toolReadiness = state.toolDbReadiness || {};
const userMReadiness = state.controlledUserMReadiness || {};
const toolDb = state.toolDbSimulation || {};
const userM = state.controlledUserMSimulation || {};
return [
`toolDbProcessReady=${toolReadiness.toolDbProcessReady === true ? "true web_simulation_only" : "false"}`,
`externalUserMProcessReady=${userMReadiness.externalUserMProcessReady === true ? "true web_simulation_only" : "false"}`,
`tools ${toolReadiness.toolCount || 0}`,
`tool events ${(toolDb.events || []).length}`,
`user-M events ${(userM.events || []).length}`,
`blocked ${(userM.blockedEvents || []).length}`,
].join(" / ");
}

View File

@@ -916,6 +916,7 @@ function toRoundedPose(pose) {
y: round(pose.y),
z: round(pose.z),
a: round(pose.a),
b: round(pose.b),
c: round(pose.c),
};
}