完善五轴 RTCP 仿真与验证资料
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || [])],
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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("\\", "/")}`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
997
web-rtcp-5axis-sim-plan/app/src/runtime/tool-db-simulation.js
Normal file
997
web-rtcp-5axis-sim-plan/app/src/runtime/tool-db-simulation.js
Normal 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) || "";
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(" / ");
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user