提交 xyzbc-trt 界面与验证更新
This commit is contained in:
219
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/controlled-user-m-simulation.js
vendored
Normal file
219
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/controlled-user-m-simulation.js
vendored
Normal 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 || [])],
|
||||
};
|
||||
}
|
||||
221
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/execution-timing.js
vendored
Normal file
221
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/execution-timing.js
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
linearUnitsToMillimetersFactor,
|
||||
linearValueToMillimeters,
|
||||
resolveStateLinearUnits,
|
||||
} from "./linear-units.js";
|
||||
|
||||
const LINEAR_AXES = ["x", "y", "z", "u", "v", "w"];
|
||||
const ANGULAR_AXES = ["a", "b", "c"];
|
||||
const ALL_AXES = [...LINEAR_AXES, ...ANGULAR_AXES];
|
||||
|
||||
export function buildProgramExecutionTiming({
|
||||
motion = [],
|
||||
profile = null,
|
||||
feedOverride = 100,
|
||||
rapidOverride = 100,
|
||||
defaultFeedRate = 100,
|
||||
} = {}) {
|
||||
const linearUnits = resolveStateLinearUnits(profile);
|
||||
const limits = buildVelocityLimits(profile, linearUnits);
|
||||
const segments = [];
|
||||
let previousAxes = null;
|
||||
let previousLinearUnits = linearUnits;
|
||||
let elapsedSeconds = 0;
|
||||
let feedRate = Number(defaultFeedRate) > 0 ? Number(defaultFeedRate) : 100;
|
||||
|
||||
for (let index = 0; index < motion.length; index += 1) {
|
||||
const event = motion[index];
|
||||
const eventLinearUnits = event.linearUnits || linearUnits;
|
||||
const axes = normalizeAxes(event.axes, previousAxes);
|
||||
if (Number.isFinite(event.feedRate) && event.feedRate > 0) {
|
||||
feedRate = event.feedRate;
|
||||
}
|
||||
const segment = buildTimingSegment({
|
||||
event,
|
||||
index,
|
||||
axes,
|
||||
previousAxes: previousAxes || axes,
|
||||
limits,
|
||||
feedRate,
|
||||
feedOverride,
|
||||
rapidOverride,
|
||||
elapsedSeconds,
|
||||
linearUnits: eventLinearUnits,
|
||||
previousLinearUnits: previousAxes ? previousLinearUnits : eventLinearUnits,
|
||||
});
|
||||
elapsedSeconds += segment.durationSeconds;
|
||||
segments.push({
|
||||
...segment,
|
||||
elapsedSeconds,
|
||||
});
|
||||
previousAxes = axes;
|
||||
previousLinearUnits = eventLinearUnits;
|
||||
}
|
||||
|
||||
const feedSeconds = segments
|
||||
.filter((segment) => segment.motionClass === "feed")
|
||||
.reduce((total, segment) => total + segment.durationSeconds, 0);
|
||||
const rapidSeconds = segments
|
||||
.filter((segment) => segment.motionClass === "rapid")
|
||||
.reduce((total, segment) => total + segment.durationSeconds, 0);
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-program-execution-timing",
|
||||
semanticBoundary: "linuxcnc_canonical_motion_timing_estimate_not_planner_queue",
|
||||
sourceBasis: "LinuxCNC canonical motion events plus INI/profile velocity limits and feed overrides",
|
||||
totalSeconds: elapsedSeconds,
|
||||
totalMinutes: elapsedSeconds / 60,
|
||||
feedSeconds,
|
||||
rapidSeconds,
|
||||
motionCount: segments.length,
|
||||
segments,
|
||||
limits,
|
||||
linearUnits,
|
||||
};
|
||||
}
|
||||
|
||||
export function timingAtMotionIndex(timing, motionIndex = 0) {
|
||||
const segment = timing?.segments?.[motionIndex] || null;
|
||||
return {
|
||||
elapsedSeconds: segment?.elapsedSeconds || 0,
|
||||
remainingSeconds: Math.max((timing?.totalSeconds || 0) - (segment?.elapsedSeconds || 0), 0),
|
||||
currentVelocity: segment?.velocityMmPerMin || 0,
|
||||
segmentDurationSeconds: segment?.durationSeconds || 0,
|
||||
segmentDistanceMm: segment?.linearDistanceMm || 0,
|
||||
};
|
||||
}
|
||||
|
||||
function buildTimingSegment({
|
||||
event,
|
||||
index,
|
||||
axes,
|
||||
previousAxes,
|
||||
limits,
|
||||
feedRate,
|
||||
feedOverride,
|
||||
rapidOverride,
|
||||
elapsedSeconds,
|
||||
linearUnits,
|
||||
previousLinearUnits,
|
||||
}) {
|
||||
const deltas = Object.fromEntries(ALL_AXES.map((axis) => [axis, axes[axis] - previousAxes[axis]]));
|
||||
const linearDistanceMm = vectorLength(LINEAR_AXES.map((axis) => (
|
||||
linearValueToMillimeters(axes[axis], linearUnits)
|
||||
- linearValueToMillimeters(previousAxes[axis], previousLinearUnits || linearUnits)
|
||||
)));
|
||||
const angularDistanceDeg = vectorLength(ANGULAR_AXES.map((axis) => deltas[axis]));
|
||||
const motionClass = event.type === "STRAIGHT_TRAVERSE" ? "rapid" : "feed";
|
||||
const feedMode = event.feedMode === "inverse-time" ? "inverse-time" : "units-per-minute";
|
||||
const requestedLinearVelocity = motionClass === "rapid"
|
||||
? limits.maxLinearVelocityMmPerMin * percent(rapidOverride)
|
||||
: feedMode === "inverse-time"
|
||||
? inverseTimeVelocityMmPerMin(linearDistanceMm, angularDistanceDeg, feedRate)
|
||||
: linearValueToMillimeters(Math.max(feedRate, 0), linearUnits) * percent(feedOverride);
|
||||
const cappedLinearVelocity = Math.min(
|
||||
requestedLinearVelocity || limits.defaultLinearVelocityMmPerMin,
|
||||
limits.maxLinearVelocityMmPerMin,
|
||||
);
|
||||
const linearSeconds = linearDistanceMm > 0
|
||||
? linearDistanceMm / Math.max(cappedLinearVelocity / 60, 0.000001)
|
||||
: 0;
|
||||
const angularVelocityDegPerMin = motionClass === "rapid"
|
||||
? limits.maxAngularVelocityDegPerMin * percent(rapidOverride)
|
||||
: feedMode === "inverse-time"
|
||||
? inverseTimeAngularVelocityDegPerMin(angularDistanceDeg, feedRate)
|
||||
: Math.min(Math.max(feedRate, 0) * percent(feedOverride), limits.maxAngularVelocityDegPerMin);
|
||||
const angularSeconds = angularDistanceDeg > 0
|
||||
? angularDistanceDeg / Math.max(angularVelocityDegPerMin / 60, 0.000001)
|
||||
: 0;
|
||||
const inverseTimeSeconds = motionClass === "feed" && feedMode === "inverse-time" && feedRate > 0
|
||||
? 60 / feedRate
|
||||
: 0;
|
||||
const durationSeconds = inverseTimeSeconds > 0
|
||||
? inverseTimeSeconds
|
||||
: Math.max(linearSeconds, angularSeconds);
|
||||
|
||||
return {
|
||||
index,
|
||||
line: event.line,
|
||||
type: event.type,
|
||||
motionClass,
|
||||
feedMode,
|
||||
linearDistanceMm,
|
||||
angularDistanceDeg,
|
||||
feedRate,
|
||||
requestedVelocityMmPerMin: requestedLinearVelocity,
|
||||
velocityMmPerMin: cappedLinearVelocity,
|
||||
angularVelocityDegPerMin,
|
||||
durationSeconds,
|
||||
startSeconds: elapsedSeconds,
|
||||
startAxes: previousAxes,
|
||||
endAxes: axes,
|
||||
axes,
|
||||
deltas,
|
||||
linearUnits,
|
||||
};
|
||||
}
|
||||
|
||||
function buildVelocityLimits(profile, linearUnits) {
|
||||
const traj = profile?.traj || {};
|
||||
const axisLimits = profile?.axisLimits || {};
|
||||
const linearVelocityScale = linearUnitsToMillimetersFactor(linearUnits);
|
||||
const maxLinearVelocity = firstFinite(
|
||||
Number(traj.maxLinearVelocity) * linearVelocityScale * 60,
|
||||
...LINEAR_AXES.map((axis) => Number(axisLimits[axis.toUpperCase()]?.maxVelocity) * linearVelocityScale * 60),
|
||||
2100,
|
||||
);
|
||||
const defaultLinearVelocity = firstFinite(
|
||||
Number(traj.defaultLinearVelocity) * linearVelocityScale * 60,
|
||||
maxLinearVelocity,
|
||||
1200,
|
||||
);
|
||||
const maxAngularVelocity = firstFinite(
|
||||
...ANGULAR_AXES.map((axis) => Number(axisLimits[axis.toUpperCase()]?.maxVelocity) * 60),
|
||||
maxLinearVelocity,
|
||||
);
|
||||
return {
|
||||
maxLinearVelocityMmPerMin: maxLinearVelocity,
|
||||
defaultLinearVelocityMmPerMin: defaultLinearVelocity,
|
||||
maxAngularVelocityDegPerMin: maxAngularVelocity,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAxes(axes = {}, fallback = null) {
|
||||
return Object.fromEntries(ALL_AXES.map((axis) => [
|
||||
axis,
|
||||
Number.isFinite(Number(axes[axis]))
|
||||
? Number(axes[axis])
|
||||
: Number(fallback?.[axis] || 0),
|
||||
]));
|
||||
}
|
||||
|
||||
function vectorLength(values) {
|
||||
return Math.sqrt(values.reduce((total, value) => total + value * value, 0));
|
||||
}
|
||||
|
||||
function percent(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? Math.max(number, 0) / 100 : 1;
|
||||
}
|
||||
|
||||
function inverseTimeVelocityMmPerMin(linearDistanceMm, angularDistanceDeg, feedRate) {
|
||||
const durationSeconds = feedRate > 0 ? 60 / feedRate : 0;
|
||||
if (linearDistanceMm > 0 && durationSeconds > 0) {
|
||||
return (linearDistanceMm / durationSeconds) * 60;
|
||||
}
|
||||
if (angularDistanceDeg > 0 && durationSeconds > 0) {
|
||||
return angularDistanceDeg / durationSeconds * 60;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function inverseTimeAngularVelocityDegPerMin(angularDistanceDeg, feedRate) {
|
||||
const durationSeconds = feedRate > 0 ? 60 / feedRate : 0;
|
||||
return angularDistanceDeg > 0 && durationSeconds > 0
|
||||
? angularDistanceDeg / durationSeconds * 60
|
||||
: 0;
|
||||
}
|
||||
|
||||
function firstFinite(...values) {
|
||||
return values.find((value) => Number.isFinite(value) && value > 0) || 1;
|
||||
}
|
||||
356
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/five-axis-session.js
vendored
Normal file
356
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/five-axis-session.js
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
export const FIVE_AXIS_SESSION_FORMAT = "web-rtcp-5axis-session-snapshot";
|
||||
export const FIVE_AXIS_SESSION_VERSION = 1;
|
||||
export const DEFAULT_SESSION_ID = "xyzbc-trt-web-session";
|
||||
export const DEFAULT_SESSION_FILENAME = "web-rtcp-5axis-session.json";
|
||||
|
||||
export function createFiveAxisSessionPayload(state) {
|
||||
return validateFiveAxisSessionPayload({
|
||||
apiName: "web-rtcp-5axis-session-payload",
|
||||
payloadVersion: 1,
|
||||
machineProfile: state.machineProfile,
|
||||
sessionName: state.sessionName,
|
||||
sourceMode: state.sourceMode,
|
||||
machine: state.machine,
|
||||
runState: state.runState,
|
||||
activeProgram: state.activeProgram,
|
||||
programSource: state.programSource,
|
||||
programStartLine: state.programStartLine,
|
||||
activeLine: state.activeLine,
|
||||
lineCount: state.lineCount,
|
||||
fileSizeBytes: state.fileSizeBytes,
|
||||
programLines: state.programLines,
|
||||
axisPose: state.axisPose,
|
||||
jointPose: state.jointPose,
|
||||
tcpPose: state.tcpPose,
|
||||
toolAxisVector: state.toolAxisVector,
|
||||
rtcpState: state.rtcpState,
|
||||
kinsType: state.kinsType,
|
||||
feed: state.feed,
|
||||
spindle: state.spindle,
|
||||
coolant: state.coolant,
|
||||
preview: state.preview,
|
||||
toolPreview: state.toolPreview,
|
||||
programExecutionSourceMode: state.programExecutionSourceMode,
|
||||
programExecutionTiming: state.programExecutionTiming,
|
||||
programElapsedSeconds: state.programElapsedSeconds,
|
||||
programRemainingSeconds: state.programRemainingSeconds,
|
||||
programRuntimeFeedback: state.programRuntimeFeedback,
|
||||
programExecution: state.programExecution
|
||||
? {
|
||||
apiName: state.programExecution.apiName,
|
||||
sourceMode: state.programExecution.sourceMode,
|
||||
semanticBoundary: state.programExecution.semanticBoundary,
|
||||
motion: state.programExecution.motion,
|
||||
plannerTiming: state.programExecution.plannerTiming || null,
|
||||
summary: state.programExecution.summary,
|
||||
}
|
||||
: null,
|
||||
kinematicsRuntimeReadiness: state.kinematicsRuntimeReadiness,
|
||||
interpreterRuntimeReadiness: state.interpreterRuntimeReadiness,
|
||||
linuxCncBoundaryReadiness: state.linuxCncBoundaryReadiness,
|
||||
});
|
||||
}
|
||||
|
||||
export function createFiveAxisSessionSnapshot(sessionId, payload, options = {}) {
|
||||
validateSessionId(sessionId);
|
||||
validateFiveAxisSessionPayload(payload);
|
||||
return {
|
||||
format: FIVE_AXIS_SESSION_FORMAT,
|
||||
version: FIVE_AXIS_SESSION_VERSION,
|
||||
sessionId,
|
||||
createdAt: options.createdAt || new Date().toISOString(),
|
||||
metadata: {
|
||||
source: "web-rtcp-5axis-xyzbc-trt-sim-plan",
|
||||
profile: payload.machineProfile,
|
||||
program: payload.activeProgram,
|
||||
...(options.metadata || {}),
|
||||
},
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function validateFiveAxisSessionSnapshot(snapshot, sessionId) {
|
||||
assertPlainObject(snapshot, "five-axis session snapshot");
|
||||
if (snapshot.format !== FIVE_AXIS_SESSION_FORMAT) {
|
||||
throw new Error(`Unsupported five-axis session snapshot format: ${snapshot.format}`);
|
||||
}
|
||||
if (snapshot.version !== FIVE_AXIS_SESSION_VERSION) {
|
||||
throw new Error(`Unsupported five-axis session snapshot version: ${snapshot.version}`);
|
||||
}
|
||||
if (snapshot.sessionId !== sessionId) {
|
||||
throw new Error(`Five-axis session snapshot id mismatch: ${snapshot.sessionId}`);
|
||||
}
|
||||
assertPlainObject(snapshot.metadata, "five-axis session snapshot metadata");
|
||||
validateFiveAxisSessionPayload(snapshot.payload);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export async function saveFiveAxisSessionSnapshot(sessionId, payload, options = {}) {
|
||||
const snapshot = createFiveAxisSessionSnapshot(sessionId, payload, options);
|
||||
const path = sessionSnapshotPath(sessionId, options.filename);
|
||||
const storage = resolveSessionStorage(options);
|
||||
await saveTextFile(path, `${JSON.stringify(snapshot, null, 2)}\n`, storage.storage);
|
||||
return { snapshot, path, storageMode: storage.mode, storageCapability: storage.capability };
|
||||
}
|
||||
|
||||
export async function loadFiveAxisSessionSnapshot(sessionId, options = {}) {
|
||||
const path = sessionSnapshotPath(sessionId, options.filename);
|
||||
const storage = resolveSessionStorage(options);
|
||||
const text = await loadTextFile(path, storage.storage);
|
||||
let snapshot;
|
||||
try {
|
||||
snapshot = JSON.parse(text);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid five-axis session snapshot JSON: ${error.message}`);
|
||||
}
|
||||
return {
|
||||
snapshot: validateFiveAxisSessionSnapshot(snapshot, sessionId),
|
||||
path,
|
||||
storageMode: storage.mode,
|
||||
storageCapability: storage.capability,
|
||||
};
|
||||
}
|
||||
|
||||
export function restoreFiveAxisSessionState(snapshot) {
|
||||
const payload = validateFiveAxisSessionPayload(snapshot.payload);
|
||||
return {
|
||||
machineProfile: payload.machineProfile,
|
||||
sessionName: payload.sessionName,
|
||||
machine: payload.machine,
|
||||
runState: payload.runState,
|
||||
activeProgram: payload.activeProgram,
|
||||
programSource: payload.programSource,
|
||||
programStartLine: payload.programStartLine,
|
||||
activeLine: payload.activeLine,
|
||||
lineCount: payload.lineCount,
|
||||
fileSizeBytes: payload.fileSizeBytes,
|
||||
programLines: payload.programLines,
|
||||
axisPose: payload.axisPose,
|
||||
rtcpState: payload.rtcpState,
|
||||
kinsType: payload.kinsType,
|
||||
feed: payload.feed,
|
||||
spindle: payload.spindle,
|
||||
coolant: payload.coolant,
|
||||
preview: payload.preview,
|
||||
toolPreview: payload.toolPreview,
|
||||
programExecutionSourceMode: payload.programExecutionSourceMode,
|
||||
programExecutionTiming: payload.programExecutionTiming,
|
||||
programElapsedSeconds: payload.programElapsedSeconds,
|
||||
programRemainingSeconds: payload.programRemainingSeconds,
|
||||
programRuntimeFeedback: payload.programRuntimeFeedback,
|
||||
programExecution: payload.programExecution,
|
||||
};
|
||||
}
|
||||
|
||||
export function createMemorySessionStorage(seed = {}) {
|
||||
const files = new Map(Object.entries(seed));
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-memory-session-storage",
|
||||
files,
|
||||
async getDirectory() {
|
||||
return createDirectoryHandle(files, []);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let browserMemorySessionStorage = null;
|
||||
|
||||
export function detectBrowserStorageCapability(globalScope = globalThis) {
|
||||
const forcedUnavailable = Boolean(globalScope.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__);
|
||||
const isBrowser = typeof globalScope.window === "object" || typeof globalScope.document === "object";
|
||||
const secureContext = !isBrowser || globalScope.isSecureContext !== false;
|
||||
const hasOpfs = typeof globalScope.navigator?.storage?.getDirectory === "function";
|
||||
const opfsAvailable = hasOpfs && secureContext && !forcedUnavailable;
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-storage-capability",
|
||||
opfsAvailable,
|
||||
opfsUnavailable: !opfsAvailable,
|
||||
secureContext,
|
||||
forcedUnavailable,
|
||||
hasNavigatorStorage: Boolean(globalScope.navigator?.storage),
|
||||
hasGetDirectory: hasOpfs,
|
||||
fallbackMode: opfsAvailable ? null : "memory-fallback",
|
||||
reason: opfsAvailable
|
||||
? "opfs_available"
|
||||
: forcedUnavailable
|
||||
? "forced_unavailable"
|
||||
: !secureContext
|
||||
? "non_secure_context"
|
||||
: !hasOpfs
|
||||
? "missing_opfs_get_directory"
|
||||
: "opfs_unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSessionStorage(options = {}) {
|
||||
if (options.storage) {
|
||||
const mode = options.storageMode || storageModeFor(options.storage);
|
||||
if (options.requireOpfs === true && mode !== "opfs") {
|
||||
throw new Error(`OPFS storage required for session snapshots, got ${mode}`);
|
||||
}
|
||||
return {
|
||||
storage: options.storage,
|
||||
mode,
|
||||
capability: {
|
||||
apiName: "web-rtcp-5axis-storage-capability",
|
||||
opfsAvailable: mode === "opfs",
|
||||
opfsUnavailable: mode !== "opfs",
|
||||
secureContext: true,
|
||||
forcedUnavailable: false,
|
||||
hasNavigatorStorage: false,
|
||||
hasGetDirectory: typeof options.storage.getDirectory === "function",
|
||||
fallbackMode: mode === "opfs" ? null : mode || "custom",
|
||||
reason: "explicit_storage",
|
||||
},
|
||||
};
|
||||
}
|
||||
const capability = detectBrowserStorageCapability();
|
||||
const browserStorage = globalThis.navigator?.storage;
|
||||
if (capability.opfsAvailable) {
|
||||
return {
|
||||
storage: browserStorage,
|
||||
mode: "opfs",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
if (options.requireOpfs === true) {
|
||||
throw new Error(`OPFS storage required for session snapshots: ${capability.reason}`);
|
||||
}
|
||||
browserMemorySessionStorage ??= createMemorySessionStorage();
|
||||
return {
|
||||
storage: browserMemorySessionStorage,
|
||||
mode: "memory-fallback",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
|
||||
function storageModeFor(storage) {
|
||||
return storage?.apiName === "web-rtcp-5axis-memory-session-storage"
|
||||
? "memory"
|
||||
: "custom";
|
||||
}
|
||||
|
||||
function validateFiveAxisSessionPayload(payload) {
|
||||
assertPlainObject(payload, "five-axis session payload");
|
||||
if (payload.apiName !== "web-rtcp-5axis-session-payload") {
|
||||
throw new Error(`Unsupported five-axis session payload API: ${payload.apiName}`);
|
||||
}
|
||||
if (payload.payloadVersion !== 1) {
|
||||
throw new Error(`Unsupported five-axis session payload version: ${payload.payloadVersion}`);
|
||||
}
|
||||
if (!["xyzac-trt", "xyzbc-trt"].includes(payload.machineProfile)) {
|
||||
throw new Error(`Unsupported five-axis machine profile: ${payload.machineProfile}`);
|
||||
}
|
||||
if (!Array.isArray(payload.programLines)) {
|
||||
throw new Error("five-axis session programLines must be an array.");
|
||||
}
|
||||
if (payload.programExecution !== null) {
|
||||
assertPlainObject(payload.programExecution, "five-axis session programExecution");
|
||||
if (!Array.isArray(payload.programExecution.motion)) {
|
||||
throw new Error("five-axis session programExecution.motion must be an array.");
|
||||
}
|
||||
assertPlainObject(payload.programExecution.summary, "five-axis session programExecution.summary");
|
||||
}
|
||||
for (const key of ["machine", "axisPose", "feed", "spindle", "coolant", "preview", "toolPreview"]) {
|
||||
assertPlainObject(payload[key], `five-axis session ${key}`);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function sessionSnapshotPath(sessionId, filename = DEFAULT_SESSION_FILENAME) {
|
||||
validateSessionId(sessionId);
|
||||
validateFilename(filename);
|
||||
return `web-rtcp-5axis-xyzbc-trt-sim-plan/sessions/${sessionId}/${filename}`;
|
||||
}
|
||||
|
||||
async function saveTextFile(path, text, storage = globalThis.navigator?.storage) {
|
||||
const root = await getStorageRoot(storage);
|
||||
const dir = await ensureParentDir(root, path);
|
||||
const filename = splitPath(path).at(-1);
|
||||
const fileHandle = await dir.getFileHandle(filename, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(text);
|
||||
await writable.close();
|
||||
}
|
||||
|
||||
async function loadTextFile(path, storage = globalThis.navigator?.storage) {
|
||||
const root = await getStorageRoot(storage);
|
||||
const parts = splitPath(path);
|
||||
let current = root;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
current = await current.getDirectoryHandle(part);
|
||||
}
|
||||
const fileHandle = await current.getFileHandle(parts.at(-1));
|
||||
const file = await fileHandle.getFile();
|
||||
return file.text();
|
||||
}
|
||||
|
||||
async function getStorageRoot(storage) {
|
||||
if (!storage?.getDirectory) {
|
||||
throw new Error("OPFS is not available in this browser.");
|
||||
}
|
||||
return storage.getDirectory();
|
||||
}
|
||||
|
||||
async function ensureParentDir(root, path) {
|
||||
let current = root;
|
||||
for (const part of splitPath(path).slice(0, -1)) {
|
||||
current = await current.getDirectoryHandle(part, { create: true });
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function createDirectoryHandle(files, prefix) {
|
||||
return {
|
||||
async getDirectoryHandle(name) {
|
||||
return createDirectoryHandle(files, [...prefix, name]);
|
||||
},
|
||||
async getFileHandle(name) {
|
||||
const path = [...prefix, name].join("/");
|
||||
return {
|
||||
async createWritable() {
|
||||
let content = "";
|
||||
return {
|
||||
async write(text) {
|
||||
content += String(text);
|
||||
},
|
||||
async close() {
|
||||
files.set(path, content);
|
||||
},
|
||||
};
|
||||
},
|
||||
async getFile() {
|
||||
if (!files.has(path)) throw new Error(`Missing memory session file: ${path}`);
|
||||
return { async text() { return files.get(path); } };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function splitPath(path) {
|
||||
const value = String(path || "").replaceAll("\\", "/");
|
||||
const parts = value.split("/").filter(Boolean);
|
||||
if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
|
||||
throw new Error(`Invalid session path: ${path}`);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
function validateSessionId(sessionId) {
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(String(sessionId || ""))) {
|
||||
throw new Error(`Invalid five-axis session id: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateFilename(filename) {
|
||||
if (!/^[a-zA-Z0-9._-]+\.json$/.test(String(filename || ""))) {
|
||||
throw new Error(`Invalid five-axis session filename: ${filename}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertPlainObject(value, label) {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error(`${label} must be a plain object.`);
|
||||
}
|
||||
}
|
||||
187
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/full-execution-boundary.js
vendored
Normal file
187
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/full-execution-boundary.js
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
const MACHINE_FILE_FLAGS = [
|
||||
"fiveaxis_ini_open=1",
|
||||
"fiveaxis_remaps_ready=1",
|
||||
"fiveaxis_file_reached_exit=1",
|
||||
];
|
||||
|
||||
export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
const adapter = state.linuxCncBoundaryAdapter || {};
|
||||
const frame = state.rtcpFrame || {};
|
||||
const programExecution = state.programExecution || null;
|
||||
const machineFileExecution = state.machineFileExecution || null;
|
||||
const machineFileText = String(machineFileExecution?.resultText || "");
|
||||
|
||||
const kinematicsReady = Boolean(
|
||||
adapter.linuxCncKinematicsReady ||
|
||||
frame.readiness?.linuxCncKinematicsReady,
|
||||
);
|
||||
const interpreterReady = Boolean(
|
||||
adapter.linuxCncInterpreterReady ||
|
||||
state.interpreterRuntimeReadiness?.loaded,
|
||||
);
|
||||
const canonicalProgramReady = Boolean(
|
||||
programExecution?.sourceMode === "linuxcnc-interpreter-wasm" &&
|
||||
programExecution?.summary?.motionEventCount > 0,
|
||||
);
|
||||
const machineFileStagingReady = state.machineFileStaging?.status === "staged"
|
||||
&& state.machineFileStaging?.fileCount > 0;
|
||||
const machineFileRemapReady = Boolean(
|
||||
machineFileExecution?.sourceMode === "linuxcnc-machine-file-remap-wasm" &&
|
||||
machineFileExecution?.summary?.machineFileExecutionReady === true,
|
||||
);
|
||||
const remapRuntimeReady = Boolean(
|
||||
machineFileExecution?.summary?.remapRuntimeReady === true ||
|
||||
MACHINE_FILE_FLAGS.every((flag) => machineFileText.includes(flag)),
|
||||
);
|
||||
const plannerRuntimeReady = Boolean(
|
||||
programExecution?.summary?.plannerRuntimeReady === true &&
|
||||
programExecution?.plannerTiming?.plannerRuntimeReady === true,
|
||||
);
|
||||
const halSwitchkinsEvidenceReady = machineFileText.includes("fiveaxis_hal_switchkins: rc=0 found=1");
|
||||
const taskHalSummary = state.taskHalStatus?.summary || {};
|
||||
const taskRuntimeReady = Boolean(
|
||||
taskHalSummary.taskRuntimeReady === true ||
|
||||
state.taskHalRuntimeReadiness?.taskRuntimeReady === true,
|
||||
);
|
||||
const motionRuntimeReady = Boolean(
|
||||
taskHalSummary.motionRuntimeReady === true ||
|
||||
state.taskHalRuntimeReadiness?.motionRuntimeReady === true,
|
||||
);
|
||||
const halRuntimeReady = Boolean(
|
||||
taskHalSummary.halRuntimeReady === true ||
|
||||
state.taskHalRuntimeReadiness?.halRuntimeReady === true,
|
||||
);
|
||||
const halSyncReady = Boolean(
|
||||
taskHalSummary.halSyncReady === true ||
|
||||
state.taskHalRuntimeReadiness?.halSyncReady === true,
|
||||
);
|
||||
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 &&
|
||||
canonicalProgramReady &&
|
||||
machineFileStagingReady &&
|
||||
machineFileRemapReady &&
|
||||
plannerRuntimeReady &&
|
||||
nativeTaskReady &&
|
||||
nativeHalSyncReady &&
|
||||
taskHalComparisonReady &&
|
||||
toolDbProcessReady &&
|
||||
externalUserMProcessReady
|
||||
);
|
||||
|
||||
const satisfied = [
|
||||
kinematicsReady ? "linuxcnc-kinematics-wasm" : null,
|
||||
interpreterReady ? "linuxcnc-interpreter-wasm" : null,
|
||||
canonicalProgramReady ? "canonical-motion-events" : null,
|
||||
machineFileStagingReady ? "machine-file-staging" : null,
|
||||
machineFileRemapReady ? "fiveaxis-remap-machine-file-run" : null,
|
||||
plannerRuntimeReady ? "linuxcnc-tp-queue-runtime-timing" : null,
|
||||
halSwitchkinsEvidenceReady ? "switchkins-hal-bridge-evidence" : null,
|
||||
taskRuntimeReady ? "linuxcnc-task-runtime" : null,
|
||||
motionRuntimeReady ? "linuxcnc-motion-runtime" : null,
|
||||
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 = [];
|
||||
if (!kinematicsReady) missing.push("linuxcnc kinematics WASM frame");
|
||||
if (!interpreterReady) missing.push("linuxcnc interpreter WASM runtime");
|
||||
if (!canonicalProgramReady) missing.push("linuxcnc canonical motion execution");
|
||||
if (!machineFileStagingReady) missing.push("LinuxCNC machine-file staging");
|
||||
if (!machineFileRemapReady) missing.push("machine-file backed five-axis remap run");
|
||||
if (!plannerRuntimeReady) missing.push("LinuxCNC trajectory planner queue timing runtime");
|
||||
if (!halSwitchkinsEvidenceReady) missing.push("switchkins HAL bridge evidence");
|
||||
if (!taskRuntimeReady) missing.push("LinuxCNC task runtime");
|
||||
if (!motionRuntimeReady) missing.push("LinuxCNC motion runtime");
|
||||
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");
|
||||
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");
|
||||
}
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-full-linuxcnc-execution-boundary",
|
||||
profileId: state.machineProfile || adapter.profileId || "unknown",
|
||||
phase: fullLinuxCncProgramExecutionReady
|
||||
? "linuxcnc-task-motion-hal-simulation-runtime"
|
||||
: machineFileRemapReady
|
||||
? "partial-linuxcnc-remap-boundary"
|
||||
: canonicalProgramReady
|
||||
? "canonical-interpreter-boundary"
|
||||
: "blocked",
|
||||
semanticBoundary: fullLinuxCncProgramExecutionReady
|
||||
? "linuxcnc_task_motion_hal_wasm_simulation_runtime"
|
||||
: machineFileRemapReady
|
||||
? "linuxcnc_machine_file_remap_ready_planner_task_hal_blocked"
|
||||
: canonicalProgramReady
|
||||
? "linuxcnc_interpreter_canonical_ready_planner_task_hal_blocked"
|
||||
: "linuxcnc_full_execution_boundary_blocked",
|
||||
sourceMode: machineFileRemapReady
|
||||
? "linuxcnc-machine-file-remap-wasm"
|
||||
: programExecution?.sourceMode || state.programExecutionSourceMode || "fixture-line-playback",
|
||||
readyForUiSimulation: kinematicsReady && interpreterReady && canonicalProgramReady,
|
||||
machineFileBackedRemapReady: machineFileRemapReady,
|
||||
remapRuntimeReady,
|
||||
halSwitchkinsEvidenceReady,
|
||||
plannerRuntimeReady,
|
||||
taskRuntimeReady,
|
||||
motionRuntimeReady,
|
||||
halRuntimeReady,
|
||||
halSyncReady,
|
||||
taskHalComparisonReady,
|
||||
nativeTaskReady,
|
||||
nativeHalSyncReady,
|
||||
fullLinuxCncProgramExecutionReady,
|
||||
promotionAllowed: fullLinuxCncProgramExecutionReady,
|
||||
hardwareDrive: false,
|
||||
hostRealtimeKernel: 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,
|
||||
evidence: {
|
||||
kinematics: kinematicsReady ? frame.semanticBoundary || adapter.semanticBoundary : null,
|
||||
interpreter: interpreterReady ? state.interpreterRuntimeReadiness?.semanticBoundary || adapter.semanticBoundary : null,
|
||||
canonicalMotionEvents: programExecution?.summary?.motionEventCount || 0,
|
||||
canonicalEventCount: programExecution?.summary?.canonicalEventCount || 0,
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
280
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/gmoccapy-communication-model.js
vendored
Normal file
280
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/gmoccapy-communication-model.js
vendored
Normal file
@@ -0,0 +1,280 @@
|
||||
export const gmoccapyCommunicationModel = {
|
||||
apiName: "web-rtcp-5axis-gmoccapy-communication-model",
|
||||
profileId: "gmoccapy-xyzab",
|
||||
nativeConfigPath: "linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.ini",
|
||||
cycleTimeMs: 100,
|
||||
semanticBoundary: "native_gmoccapy_nml_hal_reference_web_store_runtime_mapping",
|
||||
nativePaths: {
|
||||
command: {
|
||||
label: "NML command",
|
||||
path: ["gmoccapy", "linuxcnc.command()", "NML emcCommand", "milltask"],
|
||||
notes: "GUI commands are written to the NML command buffer and consumed by milltask.",
|
||||
},
|
||||
status: {
|
||||
label: "NML status/error",
|
||||
path: ["milltask", "NML emcStatus/emcError", "linuxcnc.stat()", "linuxcnc.error_channel()", "gmoccapy"],
|
||||
notes: "gmoccapy polls status and error channels at DISPLAY CYCLE_TIME.",
|
||||
},
|
||||
hal: {
|
||||
label: "HAL shared memory",
|
||||
path: ["gmoccapy.* pins", "HAL shared memory", "halui/iocontrol/motmod/spindle_sim"],
|
||||
notes: "postgui HAL connects gmoccapy pins after halcomp.ready().",
|
||||
},
|
||||
hardwareButtons: {
|
||||
label: "HAL hardware buttons",
|
||||
path: ["external HAL bit", "gmoccapy.h-button/v-button pin", "_button_pin_changed", "visible sensitive GTK button", "linuxcnc.command()"],
|
||||
notes: "gmoccapy maps rising-edge hard-button pins to the current visible software button; insensitive or hidden targets are ignored.",
|
||||
},
|
||||
halPins: {
|
||||
label: "HAL input pins",
|
||||
path: ["external HAL value", "gmoccapy.* input pin", "hal_glib.GPin value_changed", "gmoccapy callback", "linuxcnc.command() or GTK widget state"],
|
||||
notes: "jog pins, jog increment pins, settings unlock, ignore-limits, optional-stop/blockdelete, override counts/direct-value, reset and message pins enter gmoccapy through HAL callbacks; tool measurement pins are HAL_OUT diagnostics in XYZAB.",
|
||||
},
|
||||
remap: {
|
||||
label: "Interpreter REMAP",
|
||||
path: ["milltask", "RS274NGC", "Python remap prolog/ngc/epilog"],
|
||||
notes: "M6/M61 remap executes on the task/interpreter side, not inside the GUI event loop.",
|
||||
},
|
||||
filePage: {
|
||||
label: "Native file page",
|
||||
path: ["btn_load", "_show_iconview_tab(True)", "IconFileSelection1", "hal_action_open.load_file(path)", "GStat file-loaded"],
|
||||
notes: "gmoccapy uses Gtk/IconFileSelection and PROGRAM_PREFIX; the Web shell maps this to OPEN_FILE plus staged LinuxCNC G-code source diagnostics.",
|
||||
},
|
||||
macroPage: {
|
||||
label: "Native macro buttons",
|
||||
path: ["[MACROS]", "_make_macro_button", "_on_btn_macro_pressed", "command.mdi(O<name> call [args])", "interp idle/run sensitivity"],
|
||||
notes: "The XYZAB config defines five macros; Web macro dispatch must use MDI gates instead of bypassing task state.",
|
||||
},
|
||||
toolEditorPage: {
|
||||
label: "Native tool editor",
|
||||
path: ["btn_tool", "_show_tooledit_tab(True)", "tooledit1.reload()", "tool.tbl", "M61 Q? or T? M6"],
|
||||
notes: "Native gmoccapy can edit and save tool.tbl; Web keeps writeback diagnostic-only while preserving iocontrol-loopback tool-change semantics.",
|
||||
},
|
||||
nativePages: {
|
||||
label: "Native GTK page matrix",
|
||||
path: ["gmoccapy.glade notebooks", "tbtn_setup/user tabs/tool/touch/load-file", "button callback", "Web implementation matrix", "operator diagnostic"],
|
||||
notes: "Unimplemented native GTK pages are reported as diagnostic-only or native-only and must not be presented as completed Web actions.",
|
||||
},
|
||||
},
|
||||
webPath: {
|
||||
label: "Web simulation",
|
||||
path: ["button", "store.dispatch", "linuxcnc-task-policy", "runtime/status snapshot", "UI/Three.js"],
|
||||
notes: "The browser does not attach to native NML buffers or HAL shared memory.",
|
||||
},
|
||||
startupStages: [
|
||||
{ order: 1, id: "parse-ini", nativeStep: "linuxcnc script parses gmoccapy_XYZAB.ini", webEquivalent: "profile/source map selection" },
|
||||
{ order: 2, id: "linuxcncsvr", nativeStep: "linuxcncsvr -ini <ini>", webEquivalent: "not connected; reference only" },
|
||||
{ order: 3, id: "realtime-hal", nativeStep: "start realtime/RTAPI/HAL", webEquivalent: "task/HAL runtime simulation boundary where available" },
|
||||
{ order: 4, id: "milltask", nativeStep: "halcmd loadusr -Wn inihal milltask -ini <ini>", webEquivalent: "store task policy and optional task/HAL runtime" },
|
||||
{ order: 5, id: "halui", nativeStep: "halcmd loadusr -Wn halui halui -ini <ini>", webEquivalent: "HALUI pins listed as reference diagnostics" },
|
||||
{ order: 6, id: "halfiles", nativeStep: "run core_sim_XYZAB.hal, spindle_sim.hal, simulated_home.hal", webEquivalent: "HAL model diagnostics" },
|
||||
{ order: 7, id: "hal-start", nativeStep: "halcmd start", webEquivalent: "not a browser realtime thread" },
|
||||
{ order: 8, id: "gmoccapy", nativeStep: "gmoccapy -ini <ini>", webEquivalent: "gmoccapy Web shell" },
|
||||
{ order: 9, id: "postgui", nativeStep: "halcomp.ready(), then gmoccapy_postgui.hal", webEquivalent: "postgui model shown after gmoccapy pins exist" },
|
||||
],
|
||||
actionMappings: [
|
||||
{
|
||||
actionId: "estop",
|
||||
webAction: "ESTOP",
|
||||
nativeCommand: "command.state(STATE_ESTOP)",
|
||||
nativeStatus: "task_state STATE_ESTOP",
|
||||
gate: "always available as abortive safety action",
|
||||
},
|
||||
{
|
||||
actionId: "estop-reset",
|
||||
webAction: "RESET",
|
||||
nativeCommand: "command.state(STATE_ESTOP_RESET)",
|
||||
nativeStatus: "task_state STATE_ESTOP_RESET",
|
||||
gate: "allowed from estop",
|
||||
},
|
||||
{
|
||||
actionId: "power-on",
|
||||
webAction: "TOGGLE_POWER",
|
||||
nativeCommand: "command.state(STATE_ON)",
|
||||
nativeStatus: "task_state STATE_ON",
|
||||
gate: "blocked while STATE_ESTOP",
|
||||
},
|
||||
{
|
||||
actionId: "mode-manual",
|
||||
webAction: "SET_MODE manual",
|
||||
nativeCommand: "command.mode(MODE_MANUAL)",
|
||||
nativeStatus: "task_mode MODE_MANUAL",
|
||||
gate: "blocked when AUTO interpreter is not idle",
|
||||
},
|
||||
{
|
||||
actionId: "mode-mdi",
|
||||
webAction: "SET_MODE mdi",
|
||||
nativeCommand: "command.mode(MODE_MDI)",
|
||||
nativeStatus: "task_mode MODE_MDI",
|
||||
gate: "machine on and homed when NO_FORCE_HOMING=0",
|
||||
},
|
||||
{
|
||||
actionId: "mode-auto",
|
||||
webAction: "SET_MODE auto",
|
||||
nativeCommand: "command.mode(MODE_AUTO)",
|
||||
nativeStatus: "task_mode MODE_AUTO",
|
||||
gate: "machine on and homed when NO_FORCE_HOMING=0",
|
||||
},
|
||||
{
|
||||
actionId: "jog",
|
||||
webAction: "JOG",
|
||||
nativeCommand: "command.jog(JOG_CONTINUOUS/JOG_INCREMENT/JOG_STOP)",
|
||||
nativeStatus: "motion/joint feedback",
|
||||
gate: "machine on, manual mode, not running",
|
||||
},
|
||||
{
|
||||
actionId: "home",
|
||||
webAction: "HOME",
|
||||
nativeCommand: "command.home(-1) or command.home(joint)",
|
||||
nativeStatus: "all-homed",
|
||||
gate: "machine on, manual mode, no conflicting homing command",
|
||||
},
|
||||
{
|
||||
actionId: "mdi",
|
||||
webAction: "RUN_MDI",
|
||||
nativeCommand: "command.mdi(command)",
|
||||
nativeStatus: "interp reading/idle",
|
||||
gate: "machine on, MDI mode, homed when NO_FORCE_HOMING=0",
|
||||
},
|
||||
{
|
||||
actionId: "auto-run",
|
||||
webAction: "RUN",
|
||||
nativeCommand: "command.auto(AUTO_RUN, start_line)",
|
||||
nativeStatus: "interp reading/waiting/idle",
|
||||
gate: "INI loaded, program loaded, machine on, homed, AUTO, interpreter idle, runtime ready",
|
||||
},
|
||||
{
|
||||
actionId: "auto-pause",
|
||||
webAction: "PAUSE",
|
||||
nativeCommand: "command.auto(AUTO_PAUSE)",
|
||||
nativeStatus: "interp paused",
|
||||
gate: "AUTO or MDI with machine on",
|
||||
},
|
||||
{
|
||||
actionId: "auto-resume",
|
||||
webAction: "RESUME",
|
||||
nativeCommand: "command.auto(AUTO_RESUME)",
|
||||
nativeStatus: "interp reading",
|
||||
gate: "interpreter paused",
|
||||
},
|
||||
{
|
||||
actionId: "abort",
|
||||
webAction: "STOP/ABORT",
|
||||
nativeCommand: "command.abort()",
|
||||
nativeStatus: "interp idle after abort",
|
||||
gate: "available across most states",
|
||||
},
|
||||
{
|
||||
actionId: "spindle",
|
||||
webAction: "spindle controls",
|
||||
nativeCommand: "command.spindle(direction, rpm)",
|
||||
nativeStatus: "spindle.0.forward/reverse/speed-out/speed-in",
|
||||
gate: "blocked in estop/off and restricted during interpreter reading/waiting",
|
||||
},
|
||||
{
|
||||
actionId: "coolant",
|
||||
webAction: "TOGGLE_COOLANT",
|
||||
nativeCommand: "command.flood()/command.mist()",
|
||||
nativeStatus: "iocontrol.0.coolant-flood/mist",
|
||||
gate: "blocked in estop/off",
|
||||
},
|
||||
{
|
||||
actionId: "hardware-button",
|
||||
webAction: "explicit data-gmoccapy-hal-pin button mapping",
|
||||
nativeCommand: "_button_pin_changed -> GtkButton emit clicked/pressed/toggled",
|
||||
nativeStatus: "same status as the target software button",
|
||||
gate: "rising-edge pin ignored when target button is hidden or insensitive",
|
||||
},
|
||||
{
|
||||
actionId: "hal-pin-input",
|
||||
webAction: "GMOCAPY_HAL_PIN",
|
||||
nativeCommand: "_ignore_limits/_optional_blocks/_blockdelete/_on_counts_changed/_on_analog_value_changed/_reset_override/_del_message_changed",
|
||||
nativeStatus: "ignore-limits, block delete, optional stop, feed/rapid/spindle/jog override widget state and operator message state",
|
||||
gate: "counts require count-enable, direct-value requires analog-enable, reset/delete-message pins are rising-edge only",
|
||||
},
|
||||
{
|
||||
actionId: "hal-settings-unlock",
|
||||
webAction: "GMOCAPY_HAL_PIN unlock-settings",
|
||||
nativeCommand: "_on_unlock_settings_changed",
|
||||
nativeStatus: "tbtn_setup sensitivity when rbt_hal_unlock is active",
|
||||
gate: "level-driven pin only gates setup access when unlock_way/rbt_hal_unlock selects HAL unlock",
|
||||
},
|
||||
{
|
||||
actionId: "hal-jog-pin",
|
||||
webAction: "GMOCAPY_HAL_PIN jog.axis/jog-inc",
|
||||
nativeCommand: "_on_pin_jog_changed/_on_pin_incr_changed",
|
||||
nativeStatus: "manual jog command, active jog increment and gmoccapy.jog.jog-increment HAL OUT",
|
||||
gate: "axis pins use press/release level, task must be on and manual; increment pins are rising-edge only",
|
||||
},
|
||||
{
|
||||
actionId: "hal-tool-measurement",
|
||||
webAction: "GMOCAPY_HAL_PIN tool measurement diagnostics",
|
||||
nativeCommand: "_check_toolmeasurement/on_chk_use_tool_measurement_toggled/on_btn_block_height_clicked",
|
||||
nativeStatus: "probeheight, blockheight, toolmeasurement, searchvel and probevel HAL_OUT pins",
|
||||
gate: "XYZAB has no [TOOLSENSOR], so auto tool measurement controls are disabled and Web records these pins as diagnostics",
|
||||
},
|
||||
{
|
||||
actionId: "hal-user-message",
|
||||
webAction: "GMOCAPY_HAL_PIN messages.* diagnostics",
|
||||
nativeCommand: "_init_user_messages/_show_user_message",
|
||||
nativeStatus: "dynamic messages.<pinname>, waiting and response pins when MESSAGE_* entries exist",
|
||||
gate: "gmoccapy_XYZAB.ini defines no MESSAGE_* entries, so no dynamic user message pins are created",
|
||||
},
|
||||
{
|
||||
actionId: "hal-warning-confirm",
|
||||
webAction: "GMOCAPY_HAL_PIN warning-confirm",
|
||||
nativeCommand: "dialogs.warning_dialog confirm_pin polling",
|
||||
nativeStatus: "active warning dialog response",
|
||||
gate: "level-polled while warning dialog is active",
|
||||
},
|
||||
{
|
||||
actionId: "file-page",
|
||||
webAction: "LOAD_PROGRAM / LOAD_LINUXCNC_GCODE_SOURCE / GMOCAPY_PAGE_ACTION file-load",
|
||||
nativeCommand: "on_btn_load_clicked -> IconFileSelection1 -> hal_action_open.load_file",
|
||||
nativeStatus: "file-loaded signal updates gmoccapy.program.length/current-line/progress",
|
||||
gate: "blocked while interpreter is running; native GTK file chooser is represented as Web file/staged-source diagnostics",
|
||||
},
|
||||
{
|
||||
actionId: "macro-page",
|
||||
webAction: "GMOCAPY_RUN_MACRO",
|
||||
nativeCommand: "_on_btn_macro_pressed -> command.mdi(O<macro> call [args])",
|
||||
nativeStatus: "macro buttons disabled during interpreter run and re-enabled at interp idle",
|
||||
gate: "same as RUN_MDI: machine on, MDI-capable, homed when NO_FORCE_HOMING=0, interpreter idle",
|
||||
},
|
||||
{
|
||||
actionId: "tool-editor-page",
|
||||
webAction: "GMOCAPY_TOOL_EDITOR_ACTION",
|
||||
nativeCommand: "_init_tooleditor/_show_tooledit_tab/on_btn_selected_tool_clicked",
|
||||
nativeStatus: "tool.tbl rows, selected tool, active tool, tool offsets and M6/M61 commands",
|
||||
gate: "Web writeback is disabled; tool-change actions remain diagnostic and preserve iocontrol-loopback",
|
||||
},
|
||||
{
|
||||
actionId: "native-page-diagnostic",
|
||||
webAction: "GMOCAPY_NATIVE_PAGE",
|
||||
nativeCommand: "tbtn_setup/tbtn_user_tabs/btn_touch/btn_tool/tbtn_switch_mode native page callbacks",
|
||||
nativeStatus: "implementation matrix and operator diagnostic message",
|
||||
gate: "diagnostic-only pages do not emit fake GTK page transitions or native file/tool writes",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function createGmoccapyCommunicationSummary(model = gmoccapyCommunicationModel) {
|
||||
return {
|
||||
apiName: `${model.apiName}-summary`,
|
||||
profileId: model.profileId,
|
||||
nativeConfigPath: model.nativeConfigPath,
|
||||
nativePathCount: Object.keys(model.nativePaths).length,
|
||||
startupStageCount: model.startupStages.length,
|
||||
actionCount: model.actionMappings.length,
|
||||
pagePathCount: ["filePage", "macroPage", "toolEditorPage", "nativePages"]
|
||||
.filter((key) => Boolean(model.nativePaths[key])).length,
|
||||
postguiAfterHalcompReady: model.startupStages.findIndex((stage) => stage.id === "postgui") >
|
||||
model.startupStages.findIndex((stage) => stage.id === "gmoccapy"),
|
||||
hasNativeNmlCommandPath: model.nativePaths.command.path.includes("NML emcCommand"),
|
||||
hasNativeHalPath: model.nativePaths.hal.path.includes("HAL shared memory"),
|
||||
hasNativeHardwareButtonPath: model.nativePaths.hardwareButtons.path.includes("_button_pin_changed"),
|
||||
hasNativeHalPinInputPath: model.nativePaths.halPins.path.includes("hal_glib.GPin value_changed"),
|
||||
hasWebStorePath: model.webPath.path.includes("store.dispatch"),
|
||||
semanticBoundary: model.semanticBoundary,
|
||||
};
|
||||
}
|
||||
888
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/gmoccapy-hal-model.js
vendored
Normal file
888
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/gmoccapy-hal-model.js
vendored
Normal file
@@ -0,0 +1,888 @@
|
||||
export const gmoccapyHalModel = {
|
||||
apiName: "web-rtcp-5axis-gmoccapy-hal-model",
|
||||
profileId: "gmoccapy-xyzab",
|
||||
nativeConfigPath: "linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.ini",
|
||||
halFiles: [
|
||||
{
|
||||
path: "linuxcnc/configs/sim/gmoccapy/core_sim_XYZAB.hal",
|
||||
stage: "HALFILE",
|
||||
role: "trivkins and motmod simulation loopback",
|
||||
semanticBoundary: "native_hal_reference",
|
||||
},
|
||||
{
|
||||
path: "linuxcnc/configs/sim/gmoccapy/spindle_sim.hal",
|
||||
stage: "HALFILE",
|
||||
role: "spindle speed encoder and at-speed simulation",
|
||||
semanticBoundary: "native_hal_reference",
|
||||
},
|
||||
{
|
||||
path: "linuxcnc/configs/sim/gmoccapy/simulated_home.hal",
|
||||
stage: "HALFILE",
|
||||
role: "simulated X/Y/Z home switches",
|
||||
semanticBoundary: "native_hal_reference",
|
||||
},
|
||||
{
|
||||
path: "linuxcnc/configs/sim/gmoccapy/gmoccapy_postgui.hal",
|
||||
stage: "POSTGUI_HALFILE",
|
||||
role: "connections that require gmoccapy.* pins",
|
||||
requiresHalcompReady: true,
|
||||
semanticBoundary: "native_postgui_hal_reference",
|
||||
},
|
||||
],
|
||||
nativePins: [
|
||||
{
|
||||
group: "hard-buttons",
|
||||
pins: [
|
||||
"gmoccapy.h-button.button-0",
|
||||
"gmoccapy.h-button.button-1",
|
||||
"gmoccapy.h-button.button-2",
|
||||
"gmoccapy.h-button.button-3",
|
||||
"gmoccapy.h-button.button-4",
|
||||
"gmoccapy.h-button.button-5",
|
||||
"gmoccapy.h-button.button-6",
|
||||
"gmoccapy.h-button.button-7",
|
||||
"gmoccapy.h-button.button-8",
|
||||
"gmoccapy.h-button.button-9",
|
||||
"gmoccapy.v-button.button-0",
|
||||
"gmoccapy.v-button.button-1",
|
||||
"gmoccapy.v-button.button-2",
|
||||
"gmoccapy.v-button.button-3",
|
||||
"gmoccapy.v-button.button-4",
|
||||
"gmoccapy.v-button.button-5",
|
||||
"gmoccapy.v-button.button-6",
|
||||
],
|
||||
webSource: "current visible Web button set",
|
||||
semanticBoundary: "native_hardware_button_pin_reference",
|
||||
},
|
||||
{
|
||||
group: "jog",
|
||||
pins: [
|
||||
"gmoccapy.jog.axis.jog-x-plus",
|
||||
"gmoccapy.jog.axis.jog-x-minus",
|
||||
"gmoccapy.jog.axis.jog-y-plus",
|
||||
"gmoccapy.jog.axis.jog-y-minus",
|
||||
"gmoccapy.jog.axis.jog-z-plus",
|
||||
"gmoccapy.jog.axis.jog-z-minus",
|
||||
"gmoccapy.jog.axis.jog-a-plus",
|
||||
"gmoccapy.jog.axis.jog-a-minus",
|
||||
"gmoccapy.jog.axis.jog-b-plus",
|
||||
"gmoccapy.jog.axis.jog-b-minus",
|
||||
"gmoccapy.jog.jog-inc-0",
|
||||
"gmoccapy.jog.jog-inc-1",
|
||||
"gmoccapy.jog.jog-inc-2",
|
||||
"gmoccapy.jog.jog-inc-3",
|
||||
"gmoccapy.jog.jog-inc-4",
|
||||
"gmoccapy.jog.jog-inc-5",
|
||||
"gmoccapy.jog.jog-increment",
|
||||
"gmoccapy.jog.turtle-jog",
|
||||
],
|
||||
webSource: "JOG dispatch and machine.jogIncrement",
|
||||
semanticBoundary: "native_jog_pin_reference_web_task_policy_gate",
|
||||
},
|
||||
{
|
||||
group: "override",
|
||||
pins: [
|
||||
"gmoccapy.feed.feed-override.counts",
|
||||
"gmoccapy.feed.feed-override.count-enable",
|
||||
"gmoccapy.feed.feed-override.analog-enable",
|
||||
"gmoccapy.feed.feed-override.direct-value",
|
||||
"gmoccapy.feed.reset-feed-override",
|
||||
"gmoccapy.spindle.spindle-override.counts",
|
||||
"gmoccapy.spindle.spindle-override.count-enable",
|
||||
"gmoccapy.spindle.spindle-override.analog-enable",
|
||||
"gmoccapy.spindle.spindle-override.direct-value",
|
||||
"gmoccapy.spindle.reset-spindle-override",
|
||||
"gmoccapy.jog.jog-velocity.counts",
|
||||
"gmoccapy.jog.jog-velocity.count-enable",
|
||||
"gmoccapy.jog.jog-velocity.analog-enable",
|
||||
"gmoccapy.jog.jog-velocity.direct-value",
|
||||
"gmoccapy.rapid.rapid-override.counts",
|
||||
"gmoccapy.rapid.rapid-override.count-enable",
|
||||
"gmoccapy.rapid.rapid-override.analog-enable",
|
||||
"gmoccapy.rapid.rapid-override.direct-value",
|
||||
"gmoccapy.rapid.reset-rapid-override",
|
||||
],
|
||||
webSource: "feed/spindle override state",
|
||||
semanticBoundary: "native_override_pin_reference_web_state_mapping",
|
||||
},
|
||||
{
|
||||
group: "operator-inputs",
|
||||
pins: [
|
||||
"gmoccapy.ignore-limits",
|
||||
"gmoccapy.optional-stop",
|
||||
"gmoccapy.blockdelete",
|
||||
],
|
||||
webSource: "gmoccapyGui ignoreLimits, optionalBlocks, optionalStop",
|
||||
semanticBoundary: "native_ignore_limits_optional_stop_blockdelete_pin_reference_web_state_mapping",
|
||||
},
|
||||
{
|
||||
group: "settings",
|
||||
pins: [
|
||||
"gmoccapy.unlock-settings",
|
||||
],
|
||||
webSource: "gmoccapyGui settingsUnlockPin and setupSensitive",
|
||||
semanticBoundary: "native_settings_unlock_pin_reference_web_diagnostic_state",
|
||||
},
|
||||
{
|
||||
group: "tool",
|
||||
pins: [
|
||||
"gmoccapy.tooloffset-x",
|
||||
"gmoccapy.tooloffset-z",
|
||||
"gmoccapy.tool-diameter",
|
||||
"gmoccapy.probeheight",
|
||||
"gmoccapy.blockheight",
|
||||
"gmoccapy.toolmeasurement",
|
||||
"gmoccapy.searchvel",
|
||||
"gmoccapy.probevel",
|
||||
"gmoccapy.toolchange-change",
|
||||
"gmoccapy.toolchange-changed",
|
||||
"gmoccapy.toolchange-number",
|
||||
"gmoccapy.toolchange-confirm",
|
||||
],
|
||||
webSource: "toolPreview, disconnected manual tool-change diagnostics, and disabled XYZAB tool measurement outputs",
|
||||
semanticBoundary: "native_tool_pin_reference_simulated_tool_loop",
|
||||
},
|
||||
{
|
||||
group: "program",
|
||||
pins: [
|
||||
"gmoccapy.program.length",
|
||||
"gmoccapy.program.current-line",
|
||||
"gmoccapy.program.progress",
|
||||
],
|
||||
webSource: "programLines, activeLine, programRuntimeFeedback",
|
||||
semanticBoundary: "native_program_pin_reference_web_execution_status",
|
||||
},
|
||||
{
|
||||
group: "message",
|
||||
pins: [
|
||||
"gmoccapy.error",
|
||||
"gmoccapy.delete-message",
|
||||
"gmoccapy.warning-confirm",
|
||||
],
|
||||
webSource: "operatorMessage and browser diagnostics",
|
||||
semanticBoundary: "native_message_pin_reference_web_operator_message",
|
||||
},
|
||||
{
|
||||
group: "spindle-postgui",
|
||||
pins: [
|
||||
"gmoccapy.spindle_feedback_bar",
|
||||
"gmoccapy.spindle_at_speed_led",
|
||||
],
|
||||
webSource: "spindle rpm/status panel",
|
||||
semanticBoundary: "postgui_spindle_feedback_reference",
|
||||
},
|
||||
],
|
||||
coreNets: [
|
||||
{
|
||||
signal: "Xpos",
|
||||
source: "joint.0.motor-pos-cmd",
|
||||
target: "joint.0.motor-pos-fb",
|
||||
role: "X command-to-feedback simulation loop",
|
||||
},
|
||||
{
|
||||
signal: "Ypos",
|
||||
source: "joint.1.motor-pos-cmd",
|
||||
target: "joint.1.motor-pos-fb",
|
||||
role: "Y command-to-feedback simulation loop",
|
||||
},
|
||||
{
|
||||
signal: "Zpos",
|
||||
source: "joint.2.motor-pos-cmd",
|
||||
target: "joint.2.motor-pos-fb",
|
||||
role: "Z command-to-feedback simulation loop",
|
||||
},
|
||||
{
|
||||
signal: "Apos",
|
||||
source: "joint.3.motor-pos-cmd",
|
||||
target: "joint.3.motor-pos-fb",
|
||||
role: "A command-to-feedback simulation loop",
|
||||
},
|
||||
{
|
||||
signal: "Bpos",
|
||||
source: "joint.4.motor-pos-cmd",
|
||||
target: "joint.4.motor-pos-fb",
|
||||
role: "B command-to-feedback simulation loop",
|
||||
},
|
||||
{
|
||||
signal: "estop-loop",
|
||||
source: "iocontrol.0.user-enable-out",
|
||||
target: "iocontrol.0.emc-enable-in",
|
||||
role: "simulated estop enable loop",
|
||||
},
|
||||
{
|
||||
signal: "tool-prep-loop",
|
||||
source: "iocontrol.0.tool-prepare",
|
||||
target: "iocontrol.0.tool-prepared",
|
||||
role: "simulated tool prepare completion",
|
||||
},
|
||||
{
|
||||
signal: "tool-change-loop",
|
||||
source: "iocontrol.0.tool-change",
|
||||
target: "iocontrol.0.tool-changed",
|
||||
role: "simulated automatic tool-change completion",
|
||||
},
|
||||
{
|
||||
signal: "flood",
|
||||
source: "iocontrol.0.coolant-flood",
|
||||
target: "gmoccapy coolant display via status",
|
||||
role: "coolant flood state",
|
||||
},
|
||||
{
|
||||
signal: "mist",
|
||||
source: "iocontrol.0.coolant-mist",
|
||||
target: "gmoccapy coolant display via status",
|
||||
role: "coolant mist state",
|
||||
},
|
||||
],
|
||||
spindleNets: [
|
||||
{
|
||||
signal: "spindle-speed-cmd",
|
||||
source: "spindle.0.speed-out",
|
||||
target: "limit_speed.in",
|
||||
role: "commanded spindle speed",
|
||||
},
|
||||
{
|
||||
signal: "spindle-speed-limited",
|
||||
source: "limit_speed.out",
|
||||
target: "sim_encoder_0.speed",
|
||||
role: "simulated spindle inertia",
|
||||
},
|
||||
{
|
||||
signal: "spindle-pos",
|
||||
source: "encoder_0.position",
|
||||
target: "spindle.0.revs",
|
||||
role: "encoder feedback position",
|
||||
},
|
||||
{
|
||||
signal: "spindle-at-speed",
|
||||
source: "near_speed.out",
|
||||
target: "spindle.0.at-speed",
|
||||
role: "at-speed comparator result",
|
||||
},
|
||||
],
|
||||
postguiNets: [
|
||||
{
|
||||
signal: "spindle-abs",
|
||||
source: "abs_spindle_feedback.out",
|
||||
target: "gmoccapy.spindle_feedback_bar",
|
||||
requiresHalcompReady: true,
|
||||
role: "spindle feedback bar",
|
||||
},
|
||||
{
|
||||
signal: "spindle-at-speed",
|
||||
source: "spindle.0.at-speed",
|
||||
target: "gmoccapy.spindle_at_speed_led",
|
||||
requiresHalcompReady: true,
|
||||
role: "spindle at-speed LED",
|
||||
},
|
||||
{
|
||||
signal: "tooloffset-x",
|
||||
source: "motion.tooloffset.x",
|
||||
target: "gmoccapy.tooloffset-x",
|
||||
requiresHalcompReady: true,
|
||||
role: "tool X offset display",
|
||||
},
|
||||
{
|
||||
signal: "tooloffset-z",
|
||||
source: "motion.tooloffset.z",
|
||||
target: "gmoccapy.tooloffset-z",
|
||||
requiresHalcompReady: true,
|
||||
role: "tool Z offset display",
|
||||
},
|
||||
{
|
||||
signal: "tool-change-loop",
|
||||
source: "iocontrol.0.tool-change",
|
||||
target: "iocontrol.0.tool-changed",
|
||||
requiresHalcompReady: true,
|
||||
simulationLoop: true,
|
||||
role: "automatic simulated tool-change completion",
|
||||
},
|
||||
],
|
||||
hardwareButtons: {
|
||||
source: {
|
||||
path: "linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py",
|
||||
methods: ["_button_pin_changed", "_get_child_button", "_make_hal_pins"],
|
||||
},
|
||||
behavior: {
|
||||
edge: "rising-edge-only",
|
||||
hButtonContainer: "current ntb_button page visible children",
|
||||
vButtonContainer: "vbtb_main visible children",
|
||||
insensitiveTarget: "ignored",
|
||||
hiddenLabelsSkipped: true,
|
||||
semanticBoundary: "native_gmoccapy_hardware_button_pin_to_visible_sensitive_button",
|
||||
},
|
||||
verticalMain: [
|
||||
{
|
||||
index: 0,
|
||||
pin: "gmoccapy.v-button.button-0",
|
||||
nativeWidget: "tbtn_estop",
|
||||
webAction: "ESTOP",
|
||||
webDispatch: { type: "ESTOP" },
|
||||
webSelector: '[data-action="estop"]',
|
||||
gate: "always clickable safety action",
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
pin: "gmoccapy.v-button.button-1",
|
||||
nativeWidget: "tbtn_on",
|
||||
webAction: "TOGGLE_POWER",
|
||||
webDispatch: { type: "TOGGLE_POWER" },
|
||||
webSelector: '[data-action="power"]',
|
||||
gate: "blocked while STATE_ESTOP",
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
pin: "gmoccapy.v-button.button-2",
|
||||
nativeWidget: "rbt_manual",
|
||||
webAction: "SET_MODE manual",
|
||||
webDispatch: { type: "SET_MODE", mode: "manual" },
|
||||
webSelector: '[data-action="mode-manual"]',
|
||||
gate: "machine on and AUTO interpreter idle",
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
pin: "gmoccapy.v-button.button-3",
|
||||
nativeWidget: "rbt_mdi",
|
||||
webAction: "SET_MODE mdi",
|
||||
webDispatch: { type: "SET_MODE", mode: "mdi" },
|
||||
webSelector: '[data-action="mode-mdi"]',
|
||||
gate: "machine on and homed when NO_FORCE_HOMING=0",
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
pin: "gmoccapy.v-button.button-4",
|
||||
nativeWidget: "rbt_auto",
|
||||
webAction: "SET_MODE auto",
|
||||
webDispatch: { type: "SET_MODE", mode: "auto" },
|
||||
webSelector: '[data-action="mode-auto"]',
|
||||
gate: "machine on and homed when NO_FORCE_HOMING=0",
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
pin: "gmoccapy.v-button.button-5",
|
||||
nativeWidget: "tbtn_user_tabs",
|
||||
webAction: "GMOCAPY_NATIVE_PAGE user-tabs",
|
||||
webDispatch: { type: "GMOCAPY_NATIVE_PAGE", pageId: "user-tabs", nativeWidget: "tbtn_user_tabs" },
|
||||
webSelector: null,
|
||||
gate: "native user tabs page is diagnostic-only in the Web shell",
|
||||
},
|
||||
{
|
||||
index: 6,
|
||||
pin: "gmoccapy.v-button.button-6",
|
||||
nativeWidget: "tbtn_setup",
|
||||
webAction: "GMOCAPY_NATIVE_PAGE setup",
|
||||
webDispatch: { type: "GMOCAPY_NATIVE_PAGE", pageId: "setup", nativeWidget: "tbtn_setup" },
|
||||
webSelector: null,
|
||||
gate: "native setup page is diagnostic-only in the Web shell",
|
||||
},
|
||||
],
|
||||
bottomMain: [
|
||||
{
|
||||
index: 0,
|
||||
pin: "gmoccapy.h-button.button-0",
|
||||
nativeWidget: "btn_homing",
|
||||
webAction: "HOME",
|
||||
webDispatch: { type: "HOME" },
|
||||
webSelector: '[data-action="HOME"]',
|
||||
gate: "machine on and manual mode",
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
pin: "gmoccapy.h-button.button-1",
|
||||
nativeWidget: "btn_touch",
|
||||
webAction: "GMOCAPY_NATIVE_PAGE touch-off",
|
||||
webDispatch: { type: "GMOCAPY_NATIVE_PAGE", pageId: "touch-off", nativeWidget: "btn_touch" },
|
||||
webSelector: null,
|
||||
gate: "touch-off page not implemented in Web shell",
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
pin: "gmoccapy.h-button.button-3",
|
||||
nativeWidget: "btn_tool",
|
||||
webAction: "GMOCAPY_NATIVE_PAGE tool-editor",
|
||||
webDispatch: { type: "GMOCAPY_NATIVE_PAGE", pageId: "tool-editor", nativeWidget: "btn_tool" },
|
||||
webSelector: null,
|
||||
gate: "tool editor page not implemented in Web shell",
|
||||
},
|
||||
{
|
||||
index: 6,
|
||||
pin: "gmoccapy.h-button.button-6",
|
||||
nativeWidget: "tbtn_switch_mode",
|
||||
webAction: "GMOCAPY_NATIVE_PAGE switchkins-mode",
|
||||
webDispatch: { type: "GMOCAPY_NATIVE_PAGE", pageId: "switchkins-mode", nativeWidget: "tbtn_switch_mode" },
|
||||
webSelector: null,
|
||||
gate: "native joint/world switch page position is diagnostic-only in the Web shell",
|
||||
},
|
||||
],
|
||||
},
|
||||
halPinActions: {
|
||||
source: {
|
||||
path: "linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py",
|
||||
methods: [
|
||||
"_ignore_limits",
|
||||
"_optional_blocks",
|
||||
"_blockdelete",
|
||||
"_on_pin_jog_changed",
|
||||
"_on_pin_incr_changed",
|
||||
"_on_counts_changed",
|
||||
"_on_analog_enable_changed",
|
||||
"_on_analog_value_changed",
|
||||
"_reset_override",
|
||||
"_del_message_changed",
|
||||
"_on_unlock_settings_changed",
|
||||
"_check_toolmeasurement",
|
||||
"on_chk_use_tool_measurement_toggled",
|
||||
"on_btn_block_height_clicked",
|
||||
"_on_blockheight_value_changed",
|
||||
"_init_user_messages",
|
||||
"_show_user_message",
|
||||
],
|
||||
prefPath: "linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.pref",
|
||||
},
|
||||
jogPins: {
|
||||
axes: [
|
||||
{ axis: "x", plus: "gmoccapy.jog.axis.jog-x-plus", minus: "gmoccapy.jog.axis.jog-x-minus" },
|
||||
{ axis: "y", plus: "gmoccapy.jog.axis.jog-y-plus", minus: "gmoccapy.jog.axis.jog-y-minus" },
|
||||
{ axis: "z", plus: "gmoccapy.jog.axis.jog-z-plus", minus: "gmoccapy.jog.axis.jog-z-minus" },
|
||||
{ axis: "a", plus: "gmoccapy.jog.axis.jog-a-plus", minus: "gmoccapy.jog.axis.jog-a-minus" },
|
||||
{ axis: "b", plus: "gmoccapy.jog.axis.jog-b-plus", minus: "gmoccapy.jog.axis.jog-b-minus" },
|
||||
],
|
||||
increments: [
|
||||
{ index: 0, pin: "gmoccapy.jog.jog-inc-0", label: "Continuous", distance: 0 },
|
||||
{ index: 1, pin: "gmoccapy.jog.jog-inc-1", label: "1.000 mm", distance: 1 },
|
||||
{ index: 2, pin: "gmoccapy.jog.jog-inc-2", label: "0.100 mm", distance: 0.1 },
|
||||
{ index: 3, pin: "gmoccapy.jog.jog-inc-3", label: "0.010 mm", distance: 0.01 },
|
||||
{ index: 4, pin: "gmoccapy.jog.jog-inc-4", label: "0.001 mm", distance: 0.001 },
|
||||
{ index: 5, pin: "gmoccapy.jog.jog-inc-5", label: "1.2345 in", distance: 31.3563 },
|
||||
],
|
||||
outputPin: "gmoccapy.jog.jog-increment",
|
||||
turtlePin: "gmoccapy.jog.turtle-jog",
|
||||
nativeCallbacks: {
|
||||
axis: "_on_pin_jog_changed -> _on_btn_jog_pressed/_on_btn_jog_released",
|
||||
increment: "_on_pin_incr_changed -> _jog_increment_changed",
|
||||
turtle: "_on_pin_turtle_jog -> tbtn_turtle_jog.set_active",
|
||||
},
|
||||
},
|
||||
operatorPins: [
|
||||
{
|
||||
pin: "gmoccapy.ignore-limits",
|
||||
nativeCallback: "_ignore_limits -> chk_ignore_limits.set_active",
|
||||
nativeCommand: "on_chk_ignore_limits_toggled -> command.override_limits() when active",
|
||||
webDispatch: { type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.ignore-limits" },
|
||||
webState: "gmoccapyGui.ignoreLimits",
|
||||
},
|
||||
{
|
||||
pin: "gmoccapy.optional-stop",
|
||||
nativeCallback: "_optional_blocks",
|
||||
nativeCommand: "tbtn_optional_blocks -> command.set_block_delete(state)",
|
||||
webDispatch: { type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.optional-stop" },
|
||||
webState: "gmoccapyGui.optionalBlocks",
|
||||
},
|
||||
{
|
||||
pin: "gmoccapy.blockdelete",
|
||||
nativeCallback: "_blockdelete",
|
||||
nativeCommand: "command.set_optional_stop(state)",
|
||||
webDispatch: { type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.blockdelete" },
|
||||
webState: "gmoccapyGui.optionalStop",
|
||||
},
|
||||
],
|
||||
settingsPins: [
|
||||
{
|
||||
pin: "gmoccapy.unlock-settings",
|
||||
nativeCallback: "_on_unlock_settings_changed",
|
||||
nativeCommand: "rbt_hal_unlock active gates tbtn_setup sensitivity from pin level",
|
||||
prefDefault: "unlock_way=use",
|
||||
webDispatch: { type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.unlock-settings" },
|
||||
webState: "gmoccapyGui.settingsUnlockPin",
|
||||
},
|
||||
],
|
||||
overridePins: [
|
||||
{
|
||||
target: "feed",
|
||||
counts: "gmoccapy.feed.feed-override.counts",
|
||||
countEnable: "gmoccapy.feed.feed-override.count-enable",
|
||||
analogEnable: "gmoccapy.feed.feed-override.analog-enable",
|
||||
directValue: "gmoccapy.feed.feed-override.direct-value",
|
||||
reset: "gmoccapy.feed.reset-feed-override",
|
||||
nativeWidget: "spc_feed",
|
||||
scalePref: "scale_feed_override=1",
|
||||
webState: "feed.feedOverride",
|
||||
},
|
||||
{
|
||||
target: "rapid",
|
||||
counts: "gmoccapy.rapid.rapid-override.counts",
|
||||
countEnable: "gmoccapy.rapid.rapid-override.count-enable",
|
||||
analogEnable: "gmoccapy.rapid.rapid-override.analog-enable",
|
||||
directValue: "gmoccapy.rapid.rapid-override.direct-value",
|
||||
reset: "gmoccapy.rapid.reset-rapid-override",
|
||||
nativeWidget: "spc_rapid",
|
||||
scalePref: "scale_rapid_override=1",
|
||||
webState: "feed.rapidOverride",
|
||||
},
|
||||
{
|
||||
target: "spindle",
|
||||
counts: "gmoccapy.spindle.spindle-override.counts",
|
||||
countEnable: "gmoccapy.spindle.spindle-override.count-enable",
|
||||
analogEnable: "gmoccapy.spindle.spindle-override.analog-enable",
|
||||
directValue: "gmoccapy.spindle.spindle-override.direct-value",
|
||||
reset: "gmoccapy.spindle.reset-spindle-override",
|
||||
nativeWidget: "spc_spindle",
|
||||
scalePref: "scale_spindle_override=1",
|
||||
webState: "spindle.override",
|
||||
},
|
||||
{
|
||||
target: "jogVelocity",
|
||||
counts: "gmoccapy.jog.jog-velocity.counts",
|
||||
countEnable: "gmoccapy.jog.jog-velocity.count-enable",
|
||||
analogEnable: "gmoccapy.jog.jog-velocity.analog-enable",
|
||||
directValue: "gmoccapy.jog.jog-velocity.direct-value",
|
||||
reset: null,
|
||||
nativeWidget: "spc_lin_jog_vel",
|
||||
scalePref: "scale_jog_vel=140.4",
|
||||
webState: "gmoccapyGui.jogVelocity",
|
||||
},
|
||||
],
|
||||
messagePins: [
|
||||
{
|
||||
pin: "gmoccapy.delete-message",
|
||||
nativeCallback: "_del_message_changed",
|
||||
nativeCommand: "pin true deletes first alert message when gmoccapy.error is set, otherwise del_last()",
|
||||
webState: "operatorMessage",
|
||||
},
|
||||
{
|
||||
pin: "gmoccapy.warning-confirm",
|
||||
nativeCallback: "dialogs.warning_dialog periodic confirm_pin poll",
|
||||
nativeCommand: "pin true responds OK to active warning dialog",
|
||||
webState: "gmoccapyGui.warningConfirm",
|
||||
},
|
||||
{
|
||||
pin: "gmoccapy.error",
|
||||
nativeCallback: "_show_error/_on_message_deleted",
|
||||
nativeCommand: "HAL OUT bit tracks alert messages",
|
||||
webState: "gmoccapyGui.error",
|
||||
},
|
||||
],
|
||||
toolMeasurementPins: {
|
||||
toolsensorConfigured: false,
|
||||
useToolMeasurementPref: false,
|
||||
prefDefaults: {
|
||||
useToolMeasurement: "use_toolmeasurement=False",
|
||||
blockHeight: "blockheight=0.0",
|
||||
},
|
||||
disabledReason: "gmoccapy_XYZAB.ini has no [TOOLSENSOR] data, so _check_toolmeasurement disables the controls",
|
||||
pins: [
|
||||
{
|
||||
pin: "gmoccapy.probeheight",
|
||||
direction: "HAL_OUT",
|
||||
nativeCallback: "on_spbtn_probe_height_value_changed / on_chk_use_tool_measurement_toggled / on_btn_block_height_clicked",
|
||||
webState: "gmoccapyGui.probeHeight",
|
||||
},
|
||||
{
|
||||
pin: "gmoccapy.blockheight",
|
||||
direction: "HAL_OUT",
|
||||
nativeCallback: "on_btn_block_height_clicked / _on_blockheight_value_changed",
|
||||
webState: "gmoccapyGui.blockHeight",
|
||||
},
|
||||
{
|
||||
pin: "gmoccapy.toolmeasurement",
|
||||
direction: "HAL_OUT",
|
||||
nativeCallback: "on_chk_use_tool_measurement_toggled",
|
||||
webState: "gmoccapyGui.toolMeasurement",
|
||||
},
|
||||
{
|
||||
pin: "gmoccapy.searchvel",
|
||||
direction: "HAL_OUT",
|
||||
nativeCallback: "on_spbtn_search_vel_value_changed / on_chk_use_tool_measurement_toggled",
|
||||
webState: "gmoccapyGui.searchVelocity",
|
||||
},
|
||||
{
|
||||
pin: "gmoccapy.probevel",
|
||||
direction: "HAL_OUT",
|
||||
nativeCallback: "on_spbtn_probe_vel_value_changed / on_chk_use_tool_measurement_toggled",
|
||||
webState: "gmoccapyGui.probeVelocity",
|
||||
},
|
||||
],
|
||||
},
|
||||
userMessages: {
|
||||
configured: false,
|
||||
configuredMessageCount: 0,
|
||||
pins: [],
|
||||
source: "get_ini_info.get_user_messages()",
|
||||
disabledReason: "gmoccapy_XYZAB.ini has no MESSAGE_* entries, so _init_user_messages() returns without creating messages.* pins",
|
||||
dynamicPinPatterns: [
|
||||
"gmoccapy.messages.<pinname>",
|
||||
"gmoccapy.messages.<pinname>-waiting",
|
||||
"gmoccapy.messages.<pinname>-response",
|
||||
],
|
||||
supportedTypes: ["status", "okdialog", "yesnodialog"],
|
||||
},
|
||||
behavior: {
|
||||
optionalStopPinCrossesToBlockDelete: true,
|
||||
blockdeletePinCrossesToOptionalStop: true,
|
||||
unlockSettingsLevelDriven: true,
|
||||
jogAxisPinsPressRelease: true,
|
||||
jogPinsUseTaskPolicyGate: true,
|
||||
jogIncrementPinsRisingEdgeOnly: true,
|
||||
jogIncrementZeroIsContinuous: true,
|
||||
turtleJogLevelDriven: true,
|
||||
countInputRequiresEnable: true,
|
||||
directValueRequiresAnalogEnable: true,
|
||||
resetPinsRisingEdgeOnly: true,
|
||||
deleteMessageRisingEdgeOnly: true,
|
||||
warningConfirmLevelPolled: true,
|
||||
toolMeasurementPinsAreHalOut: true,
|
||||
toolMeasurementDisabledWithoutToolsensor: true,
|
||||
userMessagesDynamicIniOnly: true,
|
||||
directValueRange: "0..1",
|
||||
semanticBoundary: "native_gmoccapy_hal_pin_callbacks_to_web_store_actions",
|
||||
},
|
||||
},
|
||||
nativePages: {
|
||||
source: {
|
||||
path: "linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py",
|
||||
gladePath: "linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.glade",
|
||||
methods: [
|
||||
"_init_IconFileSelection",
|
||||
"_init_file_to_load",
|
||||
"on_btn_load_clicked",
|
||||
"on_IconFileSelection1_selected",
|
||||
"on_hal_status_file_loaded",
|
||||
"_make_macro_button",
|
||||
"_on_btn_macro_pressed",
|
||||
"_init_tooleditor",
|
||||
"_show_tooledit_tab",
|
||||
"on_btn_tool_clicked",
|
||||
"on_btn_selected_tool_clicked",
|
||||
"on_btn_select_tool_by_no_clicked",
|
||||
"on_tbtn_setup_toggled",
|
||||
"on_tbtn_user_tabs_toggled",
|
||||
"on_btn_touch_clicked",
|
||||
],
|
||||
},
|
||||
filePage: {
|
||||
pageId: "file-load",
|
||||
nativeNotebookPage: "_BB_LOAD_FILE / ntb_preview page 3",
|
||||
nativeWidget: "IconFileSelection1",
|
||||
programPrefix: "../../nc_files/",
|
||||
defaultOpenPreference: "open_file",
|
||||
fileExtensions: [
|
||||
"*.ngc",
|
||||
"*.png",
|
||||
"*.gif",
|
||||
"*.jpg",
|
||||
"*.py",
|
||||
],
|
||||
nativeCallbacks: [
|
||||
"on_btn_load_clicked -> _show_iconview_tab(True)",
|
||||
"on_IconFileSelection1_selected -> hal_action_open.load_file(path)",
|
||||
"on_hal_status_file_loaded -> gmoccapy.program.length",
|
||||
"on_hal_status_line_changed -> gmoccapy.program.current-line/progress",
|
||||
],
|
||||
webSurface: "OPEN_FILE input, staged LinuxCNC G-code source selector and program preview",
|
||||
webAction: "GMOCAPY_PAGE_ACTION file-load diagnostics",
|
||||
implementation: "partial",
|
||||
boundary: "native_GtkIconFileSelection_not_embedded_in_browser",
|
||||
},
|
||||
macroPage: {
|
||||
pageId: "mdi-macros",
|
||||
nativeContainer: "hbtb_MDI",
|
||||
iniSection: "[MACROS]",
|
||||
macroLimit: 14,
|
||||
macros: [
|
||||
{ name: "i_am_lost", args: [], file: "macros/i_am_lost.ngc", commandTemplate: "O<i_am_lost> call" },
|
||||
{ name: "halo_world", args: [], file: "macros/halo_world.ngc", commandTemplate: "O<halo_world> call" },
|
||||
{ name: "jog_around", args: [], file: "macros/jog_around.ngc", commandTemplate: "O<jog_around> call" },
|
||||
{ name: "increment", args: ["xinc", "yinc"], file: "macros/increment.ngc", commandTemplate: "O<increment> call [xinc] [yinc]" },
|
||||
{ name: "go_to_position", args: ["X-pos", "Y-pos", "Z-pos"], file: "macros/go_to_position.ngc", commandTemplate: "O<go_to_position> call [X-pos] [Y-pos] [Z-pos]" },
|
||||
],
|
||||
nativeCallbacks: [
|
||||
"_make_macro_button -> get_ini_info.get_macros()",
|
||||
"_on_btn_macro_pressed -> command.mdi(O<name> call [args])",
|
||||
"on_hal_status_interp_idle enables macro buttons",
|
||||
"on_hal_status_interp_run disables macro buttons",
|
||||
],
|
||||
webAction: "GMOCAPY_RUN_MACRO",
|
||||
implementation: "partial",
|
||||
boundary: "macro_buttons_map_to_MDI_command_without_native_entry_dialogs",
|
||||
},
|
||||
toolEditorPage: {
|
||||
pageId: "tool-editor",
|
||||
nativeNotebookPage: "ntb_preview page 2 / _BB_TOOL",
|
||||
nativeWidget: "tooledit1",
|
||||
toolTablePath: "linuxcnc/configs/sim/gmoccapy/tool.tbl",
|
||||
visibleAxes: ["X", "Y", "Z", "A", "B"],
|
||||
toolCount: 17,
|
||||
nativeCallbacks: [
|
||||
"_init_tooleditor -> tooledit1.set_filename(tool.tbl)",
|
||||
"_show_tooledit_tab(True) -> tooledit1.reload()",
|
||||
"on_btn_selected_tool_clicked -> M61 Q? or T? M6",
|
||||
"on_btn_save_tool_changes_clicked -> tooledit1.save(None)",
|
||||
"on_btn_reload_tooltable_clicked -> tooledit1.reload(None)",
|
||||
],
|
||||
webAction: "GMOCAPY_TOOL_EDITOR_ACTION diagnostics",
|
||||
editableInWeb: false,
|
||||
implementation: "diagnostic-only",
|
||||
boundary: "tool_table_writeback_not_promoted_browser_keeps_iocontrol_loopback",
|
||||
},
|
||||
implementationMatrix: [
|
||||
{ pageId: "main", nativeWidget: "ntb_main page 0", implementation: "implemented", webSurface: "DRO/G-code/preview/main controls", hardButton: null },
|
||||
{ pageId: "manual-home", nativeWidget: "btn_homing / _BB_HOME", implementation: "partial", webSurface: "HOME action", hardButton: "gmoccapy.h-button.button-0" },
|
||||
{ pageId: "mdi-macros", nativeWidget: "hbtb_MDI macro buttons", implementation: "partial", webSurface: "MDI macro diagnostics and dispatch", hardButton: null },
|
||||
{ pageId: "auto-run", nativeWidget: "_BB_AUTO buttons", implementation: "partial", webSurface: "RUN/STOP/PAUSE/STEP controls", hardButton: null },
|
||||
{ pageId: "file-load", nativeWidget: "IconFileSelection1 / _BB_LOAD_FILE", implementation: "partial", webSurface: "OPEN_FILE input and staged source selector", hardButton: null },
|
||||
{ pageId: "tool-editor", nativeWidget: "btn_tool / tooledit1", implementation: "diagnostic-only", webSurface: "tool table summary and action diagnostics", hardButton: "gmoccapy.h-button.button-3" },
|
||||
{ pageId: "touch-off", nativeWidget: "btn_touch / offsetpage1", implementation: "diagnostic-only", webSurface: "touch-off boundary diagnostics", hardButton: "gmoccapy.h-button.button-1" },
|
||||
{ pageId: "setup", nativeWidget: "tbtn_setup / ntb_setup", implementation: "diagnostic-only", webSurface: "settings/unlock diagnostics", hardButton: "gmoccapy.v-button.button-6" },
|
||||
{ pageId: "user-tabs", nativeWidget: "tbtn_user_tabs / ntb_user_tabs", implementation: "diagnostic-only", webSurface: "user tab boundary diagnostics", hardButton: "gmoccapy.v-button.button-5" },
|
||||
{ pageId: "switchkins-mode", nativeWidget: "tbtn_switch_mode", implementation: "diagnostic-only", webSurface: "identity-only XYZAB switchkins diagnostic", hardButton: "gmoccapy.h-button.button-6" },
|
||||
{ pageId: "edit", nativeWidget: "_BB_EDIT / gcode_view editable", implementation: "native-only", webSurface: "not promoted", hardButton: null },
|
||||
{ pageId: "offset-editor", nativeWidget: "offsetpage1 edit offsets", implementation: "native-only", webSurface: "not promoted", hardButton: null },
|
||||
],
|
||||
behavior: {
|
||||
unimplementedPagesDiagnosticOnly: true,
|
||||
hardButtonsDoNotFakeNativePages: true,
|
||||
fileChooserNotNativeGtk: true,
|
||||
macroButtonsUseMdiGate: true,
|
||||
toolEditorWritebackDisabled: true,
|
||||
toolEditorKeepsIocontrolLoopback: true,
|
||||
semanticBoundary: "native_gmoccapy_page_matrix_web_diagnostic_boundary",
|
||||
},
|
||||
},
|
||||
toolChange: {
|
||||
strategy: "iocontrol-loopback",
|
||||
manualGmoccapyPinsConnected: false,
|
||||
remapCodes: ["M6", "M61"],
|
||||
postguiUnlinks: [
|
||||
{
|
||||
pin: "iocontrol.0.tool-change",
|
||||
reason: "gmoccapy_postgui.hal removes the core_sim connection before applying the final loopback",
|
||||
},
|
||||
{
|
||||
pin: "iocontrol.0.tool-changed",
|
||||
reason: "gmoccapy_postgui.hal removes the core_sim connection before applying the final loopback",
|
||||
},
|
||||
{
|
||||
pin: "iocontrol.0.tool-prep-number",
|
||||
reason: "manual gmoccapy tool-change number net is not active in this simulation",
|
||||
},
|
||||
],
|
||||
commentedManualGuiNets: [
|
||||
{
|
||||
signal: "tool-change",
|
||||
source: "iocontrol.0.tool-change",
|
||||
target: "gmoccapy.toolchange-change",
|
||||
},
|
||||
{
|
||||
signal: "tool-changed",
|
||||
source: "gmoccapy.toolchange-changed",
|
||||
target: "iocontrol.0.tool-changed",
|
||||
},
|
||||
{
|
||||
signal: "tool-prep-number",
|
||||
source: "iocontrol.0.tool-prep-number",
|
||||
target: "gmoccapy.toolchange-number",
|
||||
},
|
||||
],
|
||||
activeLoop: {
|
||||
signal: "tool-change-loop",
|
||||
source: "iocontrol.0.tool-change",
|
||||
target: "iocontrol.0.tool-changed",
|
||||
stage: "POSTGUI_HALFILE",
|
||||
},
|
||||
semanticBoundary: "gmoccapy_xyzab_iocontrol_tool_change_loop_no_manual_gui_dialog",
|
||||
},
|
||||
semanticBoundary: "gmoccapy_hal_pin_postgui_reference_web_diagnostic_only",
|
||||
};
|
||||
|
||||
export function createGmoccapyHalSummary(model = gmoccapyHalModel) {
|
||||
const pinCount = model.nativePins.reduce((total, group) => total + group.pins.length, 0);
|
||||
const postguiFile = model.halFiles.find((file) => file.stage === "POSTGUI_HALFILE");
|
||||
const mappedHardwareButtonCount = [
|
||||
...(model.hardwareButtons?.verticalMain || []),
|
||||
...(model.hardwareButtons?.bottomMain || []),
|
||||
].filter((button) => button.webSelector).length;
|
||||
return {
|
||||
apiName: `${model.apiName}-summary`,
|
||||
profileId: model.profileId,
|
||||
halFileCount: model.halFiles.length,
|
||||
nativePinGroupCount: model.nativePins.length,
|
||||
nativePinCount: pinCount,
|
||||
coreNetCount: model.coreNets.length,
|
||||
spindleNetCount: model.spindleNets.length,
|
||||
postguiNetCount: model.postguiNets.length,
|
||||
postguiRequiresHalcompReady: Boolean(postguiFile?.requiresHalcompReady) &&
|
||||
model.postguiNets.every((net) => net.requiresHalcompReady),
|
||||
hasToolChangeSimulationLoop: model.postguiNets.some((net) => net.simulationLoop),
|
||||
toolChangeStrategy: model.toolChange.strategy,
|
||||
manualToolChangeGuiConnected: model.toolChange.manualGmoccapyPinsConnected,
|
||||
postguiToolUnlinkCount: model.toolChange.postguiUnlinks.length,
|
||||
commentedManualToolChangeNetCount: model.toolChange.commentedManualGuiNets.length,
|
||||
hardwareButtonVerticalCount: model.hardwareButtons.verticalMain.length,
|
||||
hardwareButtonBottomMainCount: model.hardwareButtons.bottomMain.length,
|
||||
mappedHardwareButtonCount,
|
||||
hardwareButtonEdge: model.hardwareButtons.behavior.edge,
|
||||
hardwareButtonInsensitiveTarget: model.hardwareButtons.behavior.insensitiveTarget,
|
||||
operatorInputPinCount: model.halPinActions.operatorPins.length,
|
||||
settingsInputPinCount: model.halPinActions.settingsPins.length,
|
||||
overridePinTargetCount: model.halPinActions.overridePins.length,
|
||||
jogAxisPinCount: (model.halPinActions.jogPins?.axes || []).length * 2,
|
||||
jogIncrementPinCount: model.halPinActions.jogPins?.increments?.length || 0,
|
||||
messageInputPinCount: model.halPinActions.messagePins.length,
|
||||
toolMeasurementPinCount: model.halPinActions.toolMeasurementPins.pins.length,
|
||||
toolMeasurementEnabled: model.halPinActions.toolMeasurementPins.useToolMeasurementPref,
|
||||
toolsensorConfigured: model.halPinActions.toolMeasurementPins.toolsensorConfigured,
|
||||
userMessagePinCount: model.halPinActions.userMessages.pins.length,
|
||||
userMessagesConfigured: model.halPinActions.userMessages.configured,
|
||||
optionalStopPinCrossesToBlockDelete: model.halPinActions.behavior.optionalStopPinCrossesToBlockDelete,
|
||||
blockdeletePinCrossesToOptionalStop: model.halPinActions.behavior.blockdeletePinCrossesToOptionalStop,
|
||||
unlockSettingsLevelDriven: model.halPinActions.behavior.unlockSettingsLevelDriven,
|
||||
jogAxisPinsPressRelease: model.halPinActions.behavior.jogAxisPinsPressRelease,
|
||||
jogPinsUseTaskPolicyGate: model.halPinActions.behavior.jogPinsUseTaskPolicyGate,
|
||||
jogIncrementPinsRisingEdgeOnly: model.halPinActions.behavior.jogIncrementPinsRisingEdgeOnly,
|
||||
jogIncrementZeroIsContinuous: model.halPinActions.behavior.jogIncrementZeroIsContinuous,
|
||||
turtleJogLevelDriven: model.halPinActions.behavior.turtleJogLevelDriven,
|
||||
countInputRequiresEnable: model.halPinActions.behavior.countInputRequiresEnable,
|
||||
directValueRequiresAnalogEnable: model.halPinActions.behavior.directValueRequiresAnalogEnable,
|
||||
resetPinsRisingEdgeOnly: model.halPinActions.behavior.resetPinsRisingEdgeOnly,
|
||||
deleteMessageRisingEdgeOnly: model.halPinActions.behavior.deleteMessageRisingEdgeOnly,
|
||||
warningConfirmLevelPolled: model.halPinActions.behavior.warningConfirmLevelPolled,
|
||||
toolMeasurementPinsAreHalOut: model.halPinActions.behavior.toolMeasurementPinsAreHalOut,
|
||||
toolMeasurementDisabledWithoutToolsensor: model.halPinActions.behavior.toolMeasurementDisabledWithoutToolsensor,
|
||||
userMessagesDynamicIniOnly: model.halPinActions.behavior.userMessagesDynamicIniOnly,
|
||||
nativePageCount: model.nativePages.implementationMatrix.length,
|
||||
implementedNativePageCount: model.nativePages.implementationMatrix
|
||||
.filter((page) => page.implementation === "implemented").length,
|
||||
partialNativePageCount: model.nativePages.implementationMatrix
|
||||
.filter((page) => page.implementation === "partial").length,
|
||||
diagnosticOnlyNativePageCount: model.nativePages.implementationMatrix
|
||||
.filter((page) => page.implementation === "diagnostic-only").length,
|
||||
nativeOnlyPageCount: model.nativePages.implementationMatrix
|
||||
.filter((page) => page.implementation === "native-only").length,
|
||||
filePageImplementation: model.nativePages.filePage.implementation,
|
||||
filePageBoundary: model.nativePages.filePage.boundary,
|
||||
macroCount: model.nativePages.macroPage.macros.length,
|
||||
macroLimit: model.nativePages.macroPage.macroLimit,
|
||||
macroButtonsUseMdiGate: model.nativePages.behavior.macroButtonsUseMdiGate,
|
||||
toolEditorImplementation: model.nativePages.toolEditorPage.implementation,
|
||||
toolEditorWritebackDisabled: model.nativePages.behavior.toolEditorWritebackDisabled,
|
||||
hardButtonsDoNotFakeNativePages: model.nativePages.behavior.hardButtonsDoNotFakeNativePages,
|
||||
unimplementedPagesDiagnosticOnly: model.nativePages.behavior.unimplementedPagesDiagnosticOnly,
|
||||
semanticBoundary: model.semanticBoundary,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveGmoccapyHardwareButton(input, model = gmoccapyHalModel) {
|
||||
const pin = typeof input === "string" ? input : input?.pin;
|
||||
const location = typeof input === "object" ? input?.location || input?.panel : null;
|
||||
const index = typeof input === "object" && input?.index !== undefined ? Number(input.index) : null;
|
||||
const allButtons = [
|
||||
...(model.hardwareButtons?.verticalMain || []).map((button) => ({ ...button, panel: "right", location: "v" })),
|
||||
...(model.hardwareButtons?.bottomMain || []).map((button) => ({ ...button, panel: "bottom", location: "h" })),
|
||||
];
|
||||
if (pin) return allButtons.find((button) => button.pin === pin) || null;
|
||||
if (!Number.isFinite(index)) return null;
|
||||
const normalizedLocation = location === "right" || location === "v" || location === "vertical" ? "v"
|
||||
: location === "bottom" || location === "h" || location === "horizontal" ? "h"
|
||||
: null;
|
||||
return allButtons.find((button) => button.location === normalizedLocation && button.index === index) || null;
|
||||
}
|
||||
60
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linear-units.js
vendored
Normal file
60
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linear-units.js
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
const UNIT_ALIASES = new Map([
|
||||
["mm", "mm"],
|
||||
["millimeter", "mm"],
|
||||
["millimeters", "mm"],
|
||||
["millimetre", "mm"],
|
||||
["millimetres", "mm"],
|
||||
["metric", "mm"],
|
||||
["inch", "inch"],
|
||||
["inches", "inch"],
|
||||
["in", "inch"],
|
||||
["imperial", "inch"],
|
||||
["m", "m"],
|
||||
["meter", "m"],
|
||||
["meters", "m"],
|
||||
["metre", "m"],
|
||||
["metres", "m"],
|
||||
]);
|
||||
|
||||
const METERS_PER_UNIT = {
|
||||
mm: 0.001,
|
||||
inch: 0.0254,
|
||||
m: 1,
|
||||
};
|
||||
|
||||
export function normalizeLinearUnits(units, fallback = "mm") {
|
||||
const key = String(units || "").trim().toLowerCase();
|
||||
return UNIT_ALIASES.get(key) || UNIT_ALIASES.get(String(fallback || "mm").trim().toLowerCase()) || "mm";
|
||||
}
|
||||
|
||||
export function linearUnitsToMetersFactor(units) {
|
||||
return METERS_PER_UNIT[normalizeLinearUnits(units)] || METERS_PER_UNIT.mm;
|
||||
}
|
||||
|
||||
export function linearUnitsToMillimetersFactor(units) {
|
||||
return linearUnitsToMetersFactor(units) * 1000;
|
||||
}
|
||||
|
||||
export function linearValueToMeters(value, units) {
|
||||
const number = Number(value) || 0;
|
||||
return number * linearUnitsToMetersFactor(units);
|
||||
}
|
||||
|
||||
export function linearValueToMillimeters(value, units) {
|
||||
const number = Number(value) || 0;
|
||||
return number * linearUnitsToMillimetersFactor(units);
|
||||
}
|
||||
|
||||
export function linearUnitsLabel(units) {
|
||||
return normalizeLinearUnits(units);
|
||||
}
|
||||
|
||||
export function resolveStateLinearUnits(stateOrProfile, fallback = "mm") {
|
||||
return normalizeLinearUnits(
|
||||
stateOrProfile?.programExecution?.summary?.linearUnits
|
||||
|| stateOrProfile?.linuxCncIniConfig?.traj?.linearUnits
|
||||
|| stateOrProfile?.profile?.traj?.linearUnits
|
||||
|| stateOrProfile?.traj?.linearUnits,
|
||||
fallback,
|
||||
);
|
||||
}
|
||||
85
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-boundary-adapter.js
vendored
Normal file
85
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-boundary-adapter.js
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
import { xyzacTrtProfile } from "../profiles/xyzac-trt.js";
|
||||
import { createPyvcpHalBindingSummary, xyzacTrtPyvcpPanelSchema } from "../panel-schema/xyzac-trt-pyvcp.js";
|
||||
import { createProfileSourceReferenceSummary } from "../profiles/source-reference-map.js";
|
||||
|
||||
export function createLinuxCncBoundaryAdapter({
|
||||
profile = xyzacTrtProfile,
|
||||
panelSchema = profile.panelSchema || xyzacTrtPyvcpPanelSchema,
|
||||
runtime = null,
|
||||
} = {}) {
|
||||
const sourceSummary = createProfileSourceReferenceSummary(profile.id);
|
||||
const panelSummary = createPyvcpHalBindingSummary(panelSchema);
|
||||
const kinematicsRuntimeReady = Boolean(runtime?.kinematicsWasm?.loaded);
|
||||
const interpreterRuntimeReady = Boolean(runtime?.interpreterWasm?.loaded);
|
||||
const runtimeReady = kinematicsRuntimeReady && interpreterRuntimeReady;
|
||||
const linuxCncKinematicsReady = kinematicsRuntimeReady
|
||||
&& runtime.kinematicsWasm.sourceMode === "source-derived-kinematics-wasm";
|
||||
const linuxCncInterpreterReady = interpreterRuntimeReady
|
||||
&& runtime.interpreterWasm.sourceMode === "linuxcnc-interpreter-wasm";
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-boundary-adapter",
|
||||
profileId: profile.id,
|
||||
profile,
|
||||
sourceSummary,
|
||||
panelSummary,
|
||||
runtimeReady,
|
||||
kinematicsRuntimeReady,
|
||||
interpreterRuntimeReady,
|
||||
linuxCncInterpreterReady,
|
||||
profileSummary: createProfileSummary(profile),
|
||||
linuxCncKinematicsReady,
|
||||
promotionAllowed: linuxCncKinematicsReady,
|
||||
fullLinuxCncProgramExecutionReady: false,
|
||||
semanticBoundary: linuxCncKinematicsReady
|
||||
? linuxCncInterpreterReady
|
||||
? "linuxcnc_kinematics_and_interpreter_wasm_connected_remap_planner_not_promoted"
|
||||
: "linuxcnc_kinematics_wasm_runtime_connected"
|
||||
: "adapter_entrypoint_only_runtime_not_connected",
|
||||
adapterPoints: {
|
||||
kinematicsWasm: runtime?.kinematicsWasm || null,
|
||||
interpreterWasm: runtime?.interpreterWasm || null,
|
||||
halPins: profile.halPins,
|
||||
pyvcpSchemaId: panelSchema.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createProfileSummary(profile) {
|
||||
return {
|
||||
machineName: profile.machineName,
|
||||
coordinates: profile.traj.coordinates,
|
||||
jointCount: profile.jointConfig.length,
|
||||
axisCount: Object.keys(profile.axisLimits).length,
|
||||
mdiCommandCount: profile.halui.mdiCommands.length,
|
||||
remapCount: profile.remaps.length,
|
||||
toolCount: profile.toolTable.toolCount,
|
||||
offsetNetCount: profile.hal.halcmd.offsetNets.length,
|
||||
feedbackNetCount: profile.hal.halcmd.feedbackNets.length,
|
||||
halFileCount: profile.hal.halFiles.length + profile.hal.postguiHalFiles.length,
|
||||
sourceKinds: profile.sourceReferenceObjects.map((reference) => reference.kind),
|
||||
};
|
||||
}
|
||||
|
||||
export function createLinuxCncBoundaryReadiness(adapter = createLinuxCncBoundaryAdapter()) {
|
||||
const missing = [];
|
||||
if (!adapter.kinematicsRuntimeReady) missing.push("kinematics runtime");
|
||||
if (!adapter.interpreterRuntimeReady) missing.push("interpreter/remap runtime");
|
||||
if (!adapter.sourceSummary.referenceCount) missing.push("source reference map");
|
||||
if (!adapter.panelSummary.controlCount) missing.push("PyVCP/HAL panel schema");
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-boundary-readiness",
|
||||
profileId: adapter.profileId,
|
||||
ready: adapter.linuxCncKinematicsReady
|
||||
&& !missing.includes("kinematics runtime")
|
||||
&& !missing.includes("source reference map")
|
||||
&& !missing.includes("PyVCP/HAL panel schema"),
|
||||
missing,
|
||||
linuxCncKinematicsReady: adapter.linuxCncKinematicsReady,
|
||||
linuxCncInterpreterReady: adapter.linuxCncInterpreterReady,
|
||||
promotionAllowed: adapter.promotionAllowed,
|
||||
fullLinuxCncProgramExecutionReady: adapter.fullLinuxCncProgramExecutionReady,
|
||||
semanticBoundary: adapter.semanticBoundary,
|
||||
};
|
||||
}
|
||||
478
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-ini-runtime.js
vendored
Normal file
478
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-ini-runtime.js
vendored
Normal file
@@ -0,0 +1,478 @@
|
||||
const AXIS_SECTION_RE = /^AXIS_([A-Z])$/;
|
||||
const JOINT_SECTION_RE = /^JOINT_(\d+)$/;
|
||||
|
||||
export function parseLinuxCncIni(text, { path = "inline.ini", profileId = "unknown" } = {}) {
|
||||
const sections = parseIniSections(text);
|
||||
const kinsText = getFirstValue(sections, "KINS", "KINEMATICS") || "";
|
||||
const coordinates = getFirstValue(sections, "TRAJ", "COORDINATES") || "";
|
||||
const jointCount = numberOrNull(getFirstValue(sections, "KINS", "JOINTS"));
|
||||
const axisLimits = parseAxisLimits(sections);
|
||||
const jointConfig = parseJointConfig(sections, coordinates);
|
||||
const remaps = parseRemaps(sections);
|
||||
const hal = parseHal(sections);
|
||||
const display = parseDisplay(sections);
|
||||
const emcmot = {
|
||||
module: getFirstValue(sections, "EMCMOT", "EMCMOT") || null,
|
||||
servoPeriodNs: numberOrNull(getFirstValue(sections, "EMCMOT", "SERVO_PERIOD")),
|
||||
};
|
||||
const task = {
|
||||
module: getFirstValue(sections, "TASK", "TASK") || null,
|
||||
cycleTimeSeconds: numberOrNull(getFirstValue(sections, "TASK", "CYCLE_TIME")),
|
||||
};
|
||||
const emcio = {
|
||||
toolTable: getFirstValue(sections, "EMCIO", "TOOL_TABLE") || null,
|
||||
};
|
||||
const halui = {
|
||||
mdiCommands: getValues(sections, "HALUI", "MDI_COMMAND"),
|
||||
};
|
||||
const kinematicsModuleId = inferKinematicsModuleId(kinsText);
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-ini-config",
|
||||
profileId,
|
||||
path,
|
||||
sourceText: text,
|
||||
machineName: getFirstValue(sections, "EMC", "MACHINE") || null,
|
||||
kinematics: parseKinematics(kinsText),
|
||||
kinematicsModuleId,
|
||||
kinematicsParameters: {
|
||||
sparm: parseKinematicsParameter(kinsText, "sparm"),
|
||||
joints: jointCount,
|
||||
switchkinsTypes: inferSwitchkinsTypes({ coordinates, halui, kinematicsModuleId }),
|
||||
},
|
||||
traj: {
|
||||
coordinates,
|
||||
linearUnits: getFirstValue(sections, "TRAJ", "LINEAR_UNITS") || null,
|
||||
angularUnits: getFirstValue(sections, "TRAJ", "ANGULAR_UNITS") || null,
|
||||
defaultLinearVelocity: numberOrNull(getFirstValue(sections, "TRAJ", "DEFAULT_LINEAR_VELOCITY")),
|
||||
maxLinearVelocity: numberOrNull(getFirstValue(sections, "TRAJ", "MAX_LINEAR_VELOCITY")),
|
||||
defaultLinearAcceleration: numberOrNull(getFirstValue(sections, "TRAJ", "DEFAULT_LINEAR_ACCELERATION")),
|
||||
maxLinearAcceleration: numberOrNull(getFirstValue(sections, "TRAJ", "MAX_LINEAR_ACCELERATION")),
|
||||
},
|
||||
display,
|
||||
rs274ngc: {
|
||||
subroutinePath: getFirstValue(sections, "RS274NGC", "SUBROUTINE_PATH") || null,
|
||||
halPinVars: boolFromIni(getFirstValue(sections, "RS274NGC", "HAL_PIN_VARS")),
|
||||
parameterFile: getFirstValue(sections, "RS274NGC", "PARAMETER_FILE") || null,
|
||||
remaps,
|
||||
},
|
||||
hal,
|
||||
halui,
|
||||
axisLimits,
|
||||
jointConfig,
|
||||
emcmot,
|
||||
task,
|
||||
emcio,
|
||||
validation: validateIniConfig({
|
||||
sections,
|
||||
coordinates,
|
||||
jointCount,
|
||||
axisLimits,
|
||||
jointConfig,
|
||||
kinsText,
|
||||
remaps,
|
||||
hal,
|
||||
halui,
|
||||
rs274ngc: {
|
||||
halPinVars: boolFromIni(getFirstValue(sections, "RS274NGC", "HAL_PIN_VARS")),
|
||||
parameterFile: getFirstValue(sections, "RS274NGC", "PARAMETER_FILE") || null,
|
||||
},
|
||||
emcmot,
|
||||
task,
|
||||
emcio,
|
||||
}),
|
||||
semanticBoundary: "linuxcnc_ini_file_browser_parser",
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadLinuxCncIniConfig(profile, { baseUrl = import.meta.url, fetchImpl = globalThis.fetch } = {}) {
|
||||
if (!profile?.iniPath) {
|
||||
throw new Error("profile is missing iniPath");
|
||||
}
|
||||
if (typeof fetchImpl !== "function") {
|
||||
throw new Error("fetch is not available for LinuxCNC INI loading");
|
||||
}
|
||||
|
||||
const candidateUrls = linuxCncIniCandidateUrls(profile.iniPath, baseUrl);
|
||||
const errors = [];
|
||||
let response = null;
|
||||
for (const url of candidateUrls) {
|
||||
try {
|
||||
response = await fetchImpl(url.href);
|
||||
if (response.ok) break;
|
||||
errors.push(`${url.href}: HTTP ${response.status}`);
|
||||
response = null;
|
||||
} catch (error) {
|
||||
errors.push(`${url.href}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
throw new Error(`failed to load LinuxCNC INI ${profile.iniPath}: ${errors.join(" | ")}`);
|
||||
}
|
||||
return parseLinuxCncIni(await response.text(), {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
}
|
||||
|
||||
function linuxCncIniCandidateUrls(iniPath, baseUrl) {
|
||||
const inDist = String(baseUrl).includes("/app/dist/");
|
||||
if (String(iniPath).startsWith("linuxcnc/")) {
|
||||
return inDist
|
||||
? [new URL(`../../${iniPath}`, baseUrl)]
|
||||
: [new URL(`../../../../${iniPath}`, baseUrl)];
|
||||
}
|
||||
return inDist
|
||||
? [new URL(`../../${iniPath}`, baseUrl)]
|
||||
: [new URL(`../../../../wasm-port/vendor/linuxcnc/${iniPath}`, baseUrl)];
|
||||
}
|
||||
|
||||
export function applyIniConfigToProfile(profile, iniConfig) {
|
||||
if (!iniConfig) return profile;
|
||||
return {
|
||||
...profile,
|
||||
machineName: iniConfig.machineName || profile.machineName,
|
||||
kinematics: iniConfig.kinematics.name || profile.kinematics,
|
||||
kinematicsModuleId: iniConfig.kinematicsModuleId || profile.kinematicsModuleId,
|
||||
kinematicsParameters: {
|
||||
...profile.kinematicsParameters,
|
||||
...iniConfig.kinematicsParameters,
|
||||
switchkinsTypes: mergeSwitchkinsTypes(
|
||||
profile.kinematicsParameters?.switchkinsTypes || [],
|
||||
iniConfig.halui.mdiCommands.length > 0
|
||||
? iniConfig.kinematicsParameters.switchkinsTypes
|
||||
: [],
|
||||
),
|
||||
},
|
||||
display: {
|
||||
...profile.display,
|
||||
...iniConfig.display,
|
||||
},
|
||||
rs274ngc: {
|
||||
...profile.rs274ngc,
|
||||
subroutinePath: iniConfig.rs274ngc.subroutinePath || profile.rs274ngc?.subroutinePath,
|
||||
halPinVars: iniConfig.rs274ngc.halPinVars ?? profile.rs274ngc?.halPinVars,
|
||||
parameterFile: iniConfig.rs274ngc.parameterFile || profile.rs274ngc?.parameterFile,
|
||||
},
|
||||
hal: {
|
||||
...profile.hal,
|
||||
halui: iniConfig.hal.halui || profile.hal?.halui,
|
||||
halFiles: iniConfig.hal.halFiles.length > 0 ? iniConfig.hal.halFiles : profile.hal?.halFiles,
|
||||
postguiHalFiles: iniConfig.hal.postguiHalFiles.length > 0
|
||||
? iniConfig.hal.postguiHalFiles
|
||||
: profile.hal?.postguiHalFiles,
|
||||
halcmd: {
|
||||
...profile.hal?.halcmd,
|
||||
raw: iniConfig.hal.halcmd,
|
||||
initialSets: iniConfig.hal.initialSets.length > 0
|
||||
? iniConfig.hal.initialSets
|
||||
: profile.hal?.halcmd?.initialSets,
|
||||
},
|
||||
},
|
||||
halui: iniConfig.halui.mdiCommands.length > 0 ? iniConfig.halui : profile.halui,
|
||||
emcmot: {
|
||||
...profile.emcmot,
|
||||
...dropNullish(iniConfig.emcmot || {}),
|
||||
},
|
||||
task: {
|
||||
...profile.task,
|
||||
...dropNullish(iniConfig.task || {}),
|
||||
},
|
||||
emcio: {
|
||||
...profile.emcio,
|
||||
...dropNullish(iniConfig.emcio || {}),
|
||||
},
|
||||
traj: {
|
||||
...profile.traj,
|
||||
...dropNullish(iniConfig.traj),
|
||||
},
|
||||
axisLimits: Object.keys(iniConfig.axisLimits).length > 0 ? iniConfig.axisLimits : profile.axisLimits,
|
||||
jointConfig: iniConfig.jointConfig.length > 0 ? iniConfig.jointConfig : profile.jointConfig,
|
||||
linuxCncIniConfig: iniConfig,
|
||||
};
|
||||
}
|
||||
|
||||
function parseIniSections(text) {
|
||||
const sections = new Map();
|
||||
let current = null;
|
||||
for (const rawLine of String(text).split(/\r?\n/)) {
|
||||
const line = stripIniComment(rawLine).trim();
|
||||
if (!line) continue;
|
||||
const sectionMatch = line.match(/^\[([^\]]+)]$/);
|
||||
if (sectionMatch) {
|
||||
current = sectionMatch[1].trim().toUpperCase();
|
||||
if (!sections.has(current)) sections.set(current, new Map());
|
||||
continue;
|
||||
}
|
||||
if (!current) continue;
|
||||
const equals = line.indexOf("=");
|
||||
if (equals < 0) continue;
|
||||
const key = line.slice(0, equals).trim().toUpperCase();
|
||||
const value = line.slice(equals + 1).trim();
|
||||
const section = sections.get(current);
|
||||
if (!section.has(key)) section.set(key, []);
|
||||
section.get(key).push(value);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
function stripIniComment(line) {
|
||||
let quote = null;
|
||||
for (let index = 0; index < line.length; index += 1) {
|
||||
const char = line[index];
|
||||
if ((char === "\"" || char === "'") && line[index - 1] !== "\\") {
|
||||
quote = quote === char ? null : quote || char;
|
||||
}
|
||||
if (!quote && (char === "#" || char === ";")) {
|
||||
return line.slice(0, index);
|
||||
}
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
function getValues(sections, sectionName, key) {
|
||||
return sections.get(sectionName.toUpperCase())?.get(key.toUpperCase()) || [];
|
||||
}
|
||||
|
||||
function getFirstValue(sections, sectionName, key) {
|
||||
return getValues(sections, sectionName, key)[0] ?? null;
|
||||
}
|
||||
|
||||
function parseAxisLimits(sections) {
|
||||
const result = {};
|
||||
for (const [sectionName, values] of sections) {
|
||||
const match = sectionName.match(AXIS_SECTION_RE);
|
||||
if (!match) continue;
|
||||
result[match[1]] = {
|
||||
min: numberOrNull(first(values, "MIN_LIMIT")),
|
||||
max: numberOrNull(first(values, "MAX_LIMIT")),
|
||||
maxVelocity: numberOrNull(first(values, "MAX_VELOCITY")),
|
||||
maxAcceleration: numberOrNull(first(values, "MAX_ACCELERATION")),
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseJointConfig(sections, coordinates) {
|
||||
const axisOrder = String(coordinates || "").split("");
|
||||
return [...sections.entries()]
|
||||
.map(([sectionName, values]) => {
|
||||
const match = sectionName.match(JOINT_SECTION_RE);
|
||||
if (!match) return null;
|
||||
const id = Number(match[1]);
|
||||
return {
|
||||
id,
|
||||
axis: axisOrder[id] || null,
|
||||
type: first(values, "TYPE") || null,
|
||||
home: numberOrNull(first(values, "HOME")),
|
||||
min: numberOrNull(first(values, "MIN_LIMIT")),
|
||||
max: numberOrNull(first(values, "MAX_LIMIT")),
|
||||
maxVelocity: numberOrNull(first(values, "MAX_VELOCITY")),
|
||||
maxAcceleration: numberOrNull(first(values, "MAX_ACCELERATION")),
|
||||
homeSearchVelocity: numberOrNull(first(values, "HOME_SEARCH_VEL")),
|
||||
homeSequence: numberOrNull(first(values, "HOME_SEQUENCE")),
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => left.id - right.id);
|
||||
}
|
||||
|
||||
function parseRemaps(sections) {
|
||||
return getValues(sections, "RS274NGC", "REMAP").map((value) => {
|
||||
const code = value.match(/\bM\d+\b/i)?.[0]?.toUpperCase() || null;
|
||||
const ngc = value.match(/\bngc=([^\s]+)/i)?.[1] || null;
|
||||
const modalGroup = numberOrNull(value.match(/\bmodalgroup=(\d+)/i)?.[1]);
|
||||
return { raw: value, code, modalGroup, ngc };
|
||||
});
|
||||
}
|
||||
|
||||
function parseHal(sections) {
|
||||
const halcmd = getValues(sections, "HAL", "HALCMD");
|
||||
return {
|
||||
halui: getFirstValue(sections, "HAL", "HALUI") || null,
|
||||
halFiles: getValues(sections, "HAL", "HALFILE"),
|
||||
postguiHalFiles: getValues(sections, "HAL", "POSTGUI_HALFILE"),
|
||||
halcmd,
|
||||
initialSets: halcmd
|
||||
.map((line) => line.match(/^\s*(sets|setp)\s+:?([^\s]+)\s+([-+0-9.eE]+)/i))
|
||||
.filter(Boolean)
|
||||
.map((match) => ({ op: match[1], pin: match[2], value: Number(match[3]) })),
|
||||
};
|
||||
}
|
||||
|
||||
function parseDisplay(sections) {
|
||||
const jogAxes = getFirstValue(sections, "DISPLAY", "JOG_AXES");
|
||||
return dropNullish({
|
||||
geometry: getFirstValue(sections, "DISPLAY", "GEOMETRY"),
|
||||
display: getFirstValue(sections, "DISPLAY", "DISPLAY"),
|
||||
jogAxes: jogAxes ? jogAxes.split("") : null,
|
||||
pyvcp: getFirstValue(sections, "DISPLAY", "PYVCP"),
|
||||
openFile: getFirstValue(sections, "DISPLAY", "OPEN_FILE"),
|
||||
programPrefix: getFirstValue(sections, "DISPLAY", "PROGRAM_PREFIX"),
|
||||
positionOffset: getFirstValue(sections, "DISPLAY", "POSITION_OFFSET"),
|
||||
positionFeedback: getFirstValue(sections, "DISPLAY", "POSITION_FEEDBACK"),
|
||||
maxFeedOverride: numberOrNull(getFirstValue(sections, "DISPLAY", "MAX_FEED_OVERRIDE")),
|
||||
maxLinearVelocity: numberOrNull(getFirstValue(sections, "DISPLAY", "MAX_LINEAR_VELOCITY")),
|
||||
maxAngularVelocity: numberOrNull(getFirstValue(sections, "DISPLAY", "MAX_ANGULAR_VELOCITY")),
|
||||
});
|
||||
}
|
||||
|
||||
function parseKinematics(text) {
|
||||
const [name, ...parameters] = String(text || "").trim().split(/\s+/).filter(Boolean);
|
||||
return {
|
||||
name: name || null,
|
||||
raw: text,
|
||||
parameters,
|
||||
};
|
||||
}
|
||||
|
||||
function parseKinematicsParameter(text, key) {
|
||||
return String(text || "").match(new RegExp(`\\b${key}=([^\\s]+)`, "i"))?.[1] || null;
|
||||
}
|
||||
|
||||
function inferKinematicsModuleId(kinsText) {
|
||||
const name = parseKinematics(kinsText).name || "";
|
||||
if (name.includes("xyzbc")) return "xyzbc-trt";
|
||||
if (name.includes("xyzac")) return "xyzac-trt";
|
||||
return name.replace(/-kins$/, "") || null;
|
||||
}
|
||||
|
||||
function inferSwitchkinsTypes({ coordinates, halui, kinematicsModuleId }) {
|
||||
const mdiCommands = halui.mdiCommands.length > 0 ? halui.mdiCommands : ["M429", "M428", "M430"];
|
||||
const tcpType = coordinates === "XYZBC" ? "tcp-xyzbc" : "tcp-xyzac";
|
||||
const tcpLabel = `${coordinates || kinematicsModuleId || "TCP"} TCP`;
|
||||
return mdiCommands.map((command, index) => ({
|
||||
value: index === 0 ? 0 : index,
|
||||
label: index === 0 ? "identity" : index === 1 ? tcpLabel : "USERK",
|
||||
mdiCommand: command,
|
||||
webKinsType: index === 0 ? "identity" : index === 1 ? tcpType : "userk",
|
||||
}));
|
||||
}
|
||||
|
||||
function validateIniConfig({
|
||||
sections,
|
||||
coordinates,
|
||||
jointCount,
|
||||
axisLimits,
|
||||
jointConfig,
|
||||
kinsText,
|
||||
remaps,
|
||||
hal,
|
||||
halui,
|
||||
rs274ngc,
|
||||
emcmot,
|
||||
task,
|
||||
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",
|
||||
"TRAJ",
|
||||
"EMCMOT",
|
||||
"TASK",
|
||||
"EMCIO",
|
||||
];
|
||||
if (isSwitchkinsTrt) {
|
||||
requiredSections.push("HALUI");
|
||||
}
|
||||
for (const section of requiredSections) {
|
||||
if (!sections.has(section)) missing.push(`[${section}]`);
|
||||
}
|
||||
if (!["XYZAC", "XYZBC"].includes(coordinates)) missing.push("TRAJ.COORDINATES XYZAC/XYZBC");
|
||||
if (!coordinates) missing.push("TRAJ.COORDINATES");
|
||||
if (!jointCount) missing.push("KINS.JOINTS");
|
||||
if (jointCount !== 5) missing.push("KINS.JOINTS=5");
|
||||
if (!kinsText) missing.push("KINS.KINEMATICS");
|
||||
if (!isSwitchkinsTrt && !isGmoccapyFixedTrt) {
|
||||
missing.push("KINS.KINEMATICS sparm=identityfirst");
|
||||
}
|
||||
for (const axis of String(coordinates || "").split("")) {
|
||||
if (!axisLimits[axis]) missing.push(`AXIS_${axis}`);
|
||||
}
|
||||
if (jointCount && jointConfig.length !== jointCount) {
|
||||
missing.push(`JOINT_ count ${jointConfig.length}/${jointCount}`);
|
||||
}
|
||||
for (let joint = 0; joint < 5; joint += 1) {
|
||||
if (!sections.has(`JOINT_${joint}`)) missing.push(`JOINT_${joint}`);
|
||||
}
|
||||
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.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 (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 (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");
|
||||
if (!task.cycleTimeSeconds) missing.push("TASK.CYCLE_TIME");
|
||||
if (!emcio.toolTable) missing.push("EMCIO.TOOL_TABLE");
|
||||
return {
|
||||
ready: missing.length === 0,
|
||||
missing,
|
||||
axisCount: Object.keys(axisLimits).length,
|
||||
jointCount: jointConfig.length,
|
||||
};
|
||||
}
|
||||
|
||||
function first(values, key) {
|
||||
return values.get(key)?.[0] ?? null;
|
||||
}
|
||||
|
||||
function numberOrNull(value) {
|
||||
if (value === null || value === undefined || value === "") return null;
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
|
||||
function boolFromIni(value) {
|
||||
if (value === null || value === undefined) return null;
|
||||
return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
|
||||
}
|
||||
|
||||
function dropNullish(object) {
|
||||
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== null && value !== undefined));
|
||||
}
|
||||
|
||||
function mergeSwitchkinsTypes(profileTypes, iniTypes) {
|
||||
if (!iniTypes?.length) return profileTypes;
|
||||
return iniTypes.map((iniType) => ({
|
||||
...iniType,
|
||||
...(profileTypes.find((entry) => entry.mdiCommand === iniType.mdiCommand || entry.value === iniType.value) || {}),
|
||||
...iniType,
|
||||
}));
|
||||
}
|
||||
544
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-interpreter-runtime.js
vendored
Normal file
544
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-interpreter-runtime.js
vendored
Normal file
@@ -0,0 +1,544 @@
|
||||
const DEFAULT_SDK_MODULE_URL = "../../../../wasm-port/runtime/sdk/src/linuxcnc-interp.js";
|
||||
const DEFAULT_TP_SDK_MODULE_URL = "../../../../wasm-port/runtime/sdk/src/linuxcnc-tp.js";
|
||||
const SOURCE_MODE = "linuxcnc-interpreter-wasm";
|
||||
const SEMANTIC_BOUNDARY = "linuxcnc_interpreter_wasm_canonical_events";
|
||||
const PLANNER_TIMING_BOUNDARY = "linuxcnc_tp_queue_runtime_timing_from_canonical_motion";
|
||||
const SWITCHKINS_REMAP_BOUNDARY = "linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied";
|
||||
const FIVE_AXIS_REMAP_FLAGS = [
|
||||
"fiveaxis_ini_open=1",
|
||||
"fiveaxis_remaps_ready=1",
|
||||
"fiveaxis_file_reached_exit=1",
|
||||
];
|
||||
|
||||
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
|
||||
const SWITCHKINS_M_CODES = new Map([
|
||||
[428, { switchkinsType: 1, requestedKinsType: "tcp" }],
|
||||
[429, { switchkinsType: 0, requestedKinsType: "identity" }],
|
||||
[430, { switchkinsType: 2, requestedKinsType: "userk" }],
|
||||
]);
|
||||
const PLANE_AXIS_MAP = {
|
||||
170: ["x", "y", "z"],
|
||||
180: ["z", "x", "y"],
|
||||
190: ["y", "z", "x"],
|
||||
};
|
||||
|
||||
export async function createLinuxCncInterpreterRuntime({
|
||||
moduleOptions = null,
|
||||
tpModuleOptions = null,
|
||||
wasmRoot = null,
|
||||
sdkModuleUrl = DEFAULT_SDK_MODULE_URL,
|
||||
tpSdkModuleUrl = DEFAULT_TP_SDK_MODULE_URL,
|
||||
} = {}) {
|
||||
const { createLinuxCncInterpSdk } = await import(sdkModuleUrl);
|
||||
const resolvedModuleOptions = moduleOptions || await createDefaultModuleOptions({ wasmRoot });
|
||||
const sdk = await createLinuxCncInterpSdk(resolvedModuleOptions);
|
||||
const tpRuntime = await createOptionalTpRuntime({ tpModuleOptions, wasmRoot, tpSdkModuleUrl });
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-interpreter-runtime",
|
||||
loaded: true,
|
||||
sourceMode: SOURCE_MODE,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
executionContext: "direct",
|
||||
sdk,
|
||||
tpRuntime,
|
||||
|
||||
readiness() {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-interpreter-runtime-readiness",
|
||||
loaded: true,
|
||||
sourceMode: SOURCE_MODE,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
executionContext: "direct",
|
||||
runProgramReady: typeof sdk.runProgram === "function",
|
||||
remapRuntimeReady: false,
|
||||
plannerRuntimeReady: tpRuntime?.loaded === true,
|
||||
plannerSemanticBoundary: tpRuntime?.semanticBoundary || null,
|
||||
};
|
||||
},
|
||||
|
||||
runProgram(programText) {
|
||||
const prepared = prepareLinuxCncProgramForRuntime(programText);
|
||||
const resultText = sdk.runProgram(prepared.runtimeProgramText);
|
||||
const motion = parseLinuxCncCanonicalMotion(resultText, programText, prepared.switchkinsEvents);
|
||||
return createProgramExecutionResult({
|
||||
programText,
|
||||
resultText: prependRuntimeEvents(resultText, prepared.switchkinsEvents),
|
||||
motion,
|
||||
switchkinsEvents: prepared.switchkinsEvents,
|
||||
runtimeProgramText: prepared.runtimeProgramText,
|
||||
plannerTiming: runPlannerTiming(tpRuntime, motion),
|
||||
});
|
||||
},
|
||||
|
||||
runMachineFileProgram({ plan, files = null, executionMode = "fiveAxisRemap" } = {}) {
|
||||
if (!plan?.wasmIniPath || !plan?.wasmProgramPath) {
|
||||
throw new Error("runMachineFileProgram requires a machine-file staging plan with INI and program paths");
|
||||
}
|
||||
if (typeof sdk.runSimConfigProgram !== "function") {
|
||||
throw new Error("LinuxCNC interpreter SDK missing runSimConfigProgram");
|
||||
}
|
||||
const stagedFiles = files || plan.files;
|
||||
const resultText = sdk.runSimConfigProgram({
|
||||
files: stagedFiles.map((file) => ({
|
||||
path: file.wasmPath || file.path,
|
||||
text: file.text,
|
||||
executable: file.executable,
|
||||
})),
|
||||
programPath: plan.wasmProgramPath,
|
||||
iniPath: plan.wasmIniPath,
|
||||
executionMode,
|
||||
});
|
||||
const programFile = stagedFiles.find((file) => (
|
||||
(file.wasmPath || file.path) === plan.wasmProgramPath
|
||||
));
|
||||
const programText = programFile?.text || "";
|
||||
const motion = parseLinuxCncCanonicalMotion(resultText, programText);
|
||||
return createProgramExecutionResult({
|
||||
programText,
|
||||
resultText,
|
||||
motion,
|
||||
machineFilePlan: plan,
|
||||
sourceMode: "linuxcnc-machine-file-remap-wasm",
|
||||
semanticBoundary: "linuxcnc_fiveaxis_remap_wasm_machine_file_execution",
|
||||
plannerTiming: runPlannerTiming(tpRuntime, motion),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createLinuxCncInterpreterRuntimeDescriptor(runtime) {
|
||||
if (!runtime?.loaded) return null;
|
||||
return {
|
||||
apiName: runtime.apiName,
|
||||
loaded: runtime.loaded,
|
||||
sourceMode: runtime.sourceMode,
|
||||
semanticBoundary: runtime.semanticBoundary,
|
||||
executionContext: runtime.executionContext || "direct",
|
||||
};
|
||||
}
|
||||
|
||||
function createProgramExecutionResult({
|
||||
programText,
|
||||
resultText,
|
||||
motion,
|
||||
switchkinsEvents = [],
|
||||
runtimeProgramText = programText,
|
||||
machineFilePlan = null,
|
||||
sourceMode = SOURCE_MODE,
|
||||
semanticBoundary = SEMANTIC_BOUNDARY,
|
||||
plannerTiming = null,
|
||||
}) {
|
||||
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 {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-interpreter-program-execution",
|
||||
sourceMode,
|
||||
semanticBoundary,
|
||||
resultText,
|
||||
runtimeProgramText,
|
||||
motion,
|
||||
plannerTiming,
|
||||
switchkinsEvents,
|
||||
switchkinsRemapBoundary: switchkinsEvents.length > 0 ? SWITCHKINS_REMAP_BOUNDARY : null,
|
||||
machineFilePlan: machineFilePlan
|
||||
? {
|
||||
apiName: machineFilePlan.apiName,
|
||||
profileId: machineFilePlan.profileId,
|
||||
wasmIniPath: machineFilePlan.wasmIniPath,
|
||||
wasmProgramPath: machineFilePlan.wasmProgramPath,
|
||||
selectedProgramSourceRel: machineFilePlan.selectedProgramSourceRel || null,
|
||||
selectedProgramFilename: machineFilePlan.selectedProgramFilename || null,
|
||||
fileCount: machineFilePlan.files?.length ?? 0,
|
||||
semanticBoundary: machineFilePlan.semanticBoundary,
|
||||
}
|
||||
: null,
|
||||
summary: {
|
||||
ready: motion.length > 0,
|
||||
programLineCount: programText.split(/\r?\n/).filter((line) => line.trim()).length,
|
||||
canonicalEventCount,
|
||||
motionEventCount: motion.length,
|
||||
motionTypes: [...new Set(motion.map((event) => event.type))],
|
||||
finalAxes: motion.at(-1)?.axes || Object.fromEntries(AXES.map((axis) => [axis, 0])),
|
||||
switchkinsEventCount: switchkinsEvents.length,
|
||||
switchkinsCodes: [...new Set(switchkinsEvents.map((event) => event.code))],
|
||||
switchkinsRemapBoundary: switchkinsEvents.length > 0 ? SWITCHKINS_REMAP_BOUNDARY : null,
|
||||
remapRuntimeReady,
|
||||
plannerRuntimeReady,
|
||||
plannerSemanticBoundary: plannerRuntimeReady ? PLANNER_TIMING_BOUNDARY : null,
|
||||
machineFileExecutionReady,
|
||||
fullLinuxCncProgramExecutionReady: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function createOptionalTpRuntime({ tpModuleOptions, wasmRoot, tpSdkModuleUrl }) {
|
||||
try {
|
||||
const { createLinuxCncTpSdk } = await import(tpSdkModuleUrl);
|
||||
const resolvedOptions = tpModuleOptions || await createDefaultTpModuleOptions({ wasmRoot });
|
||||
const sdk = await createLinuxCncTpSdk(resolvedOptions);
|
||||
return {
|
||||
loaded: true,
|
||||
semanticBoundary: PLANNER_TIMING_BOUNDARY,
|
||||
sdk,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
loaded: false,
|
||||
semanticBoundary: null,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function runPlannerTiming(tpRuntime, motion) {
|
||||
if (!tpRuntime?.loaded || typeof tpRuntime.sdk?.runCanonicalMotionTiming !== "function") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const sampleStride = motion.length > 1000 ? Math.ceil(motion.length / 90) : 10;
|
||||
return tpRuntime.sdk.runCanonicalMotionTiming({
|
||||
motion,
|
||||
options: {
|
||||
cycleTime: 0.001,
|
||||
queueSize: 32,
|
||||
maxCycles: 2000000,
|
||||
sampleStride,
|
||||
maxVelocity: 35,
|
||||
maxAcceleration: 500,
|
||||
maxJerk: 1000,
|
||||
tolerance: 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-tp-program-timing",
|
||||
semanticBoundary: PLANNER_TIMING_BOUNDARY,
|
||||
plannerRuntimeReady: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
motionCount: motion.length,
|
||||
totalSeconds: 0,
|
||||
totalMinutes: 0,
|
||||
feedSeconds: 0,
|
||||
rapidSeconds: 0,
|
||||
segments: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function parseLinuxCncCanonicalMotion(resultText, programText = "", switchkinsEvents = []) {
|
||||
const axes = Object.fromEntries(AXES.map((axis) => [axis, 0]));
|
||||
const sourceLines = programLineMap(programText);
|
||||
const feedRatesByLine = feedRatesBySourceLine(programText);
|
||||
const feedModesByLine = feedModesBySourceLine(programText);
|
||||
const linearUnitsByLine = linearUnitsBySourceLine(programText);
|
||||
const switchkinsByLine = switchkinsEventsByLine(switchkinsEvents);
|
||||
const motion = [];
|
||||
let activePlane = 170;
|
||||
let activeSwitchkinsEvent = null;
|
||||
let activeFeedRate = null;
|
||||
let activeFeedMode = "units-per-minute";
|
||||
let activeLinearUnits = "mm";
|
||||
|
||||
for (const line of String(resultText).split("\n")) {
|
||||
const feedRate = readCanonicalFeedRate(line);
|
||||
if (Number.isFinite(feedRate) && feedRate > 0) {
|
||||
activeFeedRate = feedRate;
|
||||
}
|
||||
|
||||
const plane = readCanonicalNumber(line, "plane");
|
||||
if (plane && PLANE_AXIS_MAP[plane]) {
|
||||
activePlane = plane;
|
||||
}
|
||||
|
||||
const event = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/);
|
||||
if (!event) continue;
|
||||
|
||||
if (event[1] === "ARC_FEED") {
|
||||
const [firstAxis, secondAxis, thirdAxis] = PLANE_AXIS_MAP[activePlane] ?? PLANE_AXIS_MAP[170];
|
||||
const firstEnd = readCanonicalNumber(line, "first_end");
|
||||
const secondEnd = readCanonicalNumber(line, "second_end");
|
||||
const axisEndPoint = readCanonicalNumber(line, "axis_end_point");
|
||||
if (Number.isFinite(firstEnd)) axes[firstAxis] = firstEnd;
|
||||
if (Number.isFinite(secondEnd)) axes[secondAxis] = secondEnd;
|
||||
if (Number.isFinite(axisEndPoint)) axes[thirdAxis] = axisEndPoint;
|
||||
axes.arc = {
|
||||
plane: activePlane,
|
||||
firstAxis,
|
||||
secondAxis,
|
||||
thirdAxis,
|
||||
firstEnd,
|
||||
secondEnd,
|
||||
centerFirst: readCanonicalNumber(line, "first_axis"),
|
||||
centerSecond: readCanonicalNumber(line, "second_axis"),
|
||||
rotation: readCanonicalNumber(line, "rotation"),
|
||||
axisEndPoint,
|
||||
};
|
||||
} else {
|
||||
for (const axis of AXES) {
|
||||
const value = readCanonicalNumber(line, axis);
|
||||
if (value !== null && Number.isFinite(value)) axes[axis] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const sourceLine = readCanonicalNumber(line, "line");
|
||||
if (Number.isFinite(sourceLine)) {
|
||||
const event = latestSwitchkinsEventAtOrBeforeLine(switchkinsByLine, sourceLine);
|
||||
if (event) activeSwitchkinsEvent = event;
|
||||
const sourceFeedRate = latestFeedRateAtOrBeforeLine(feedRatesByLine, sourceLine);
|
||||
if ((!Number.isFinite(activeFeedRate) || activeFeedRate <= 0)
|
||||
&& Number.isFinite(sourceFeedRate) && sourceFeedRate > 0) {
|
||||
activeFeedRate = sourceFeedRate;
|
||||
}
|
||||
activeFeedMode = latestFeedModeAtOrBeforeLine(feedModesByLine, sourceLine) || activeFeedMode;
|
||||
activeLinearUnits = latestLinearUnitsAtOrBeforeLine(linearUnitsByLine, sourceLine) || activeLinearUnits;
|
||||
}
|
||||
motion.push({
|
||||
type: event[1],
|
||||
line: Number.isFinite(sourceLine) ? sourceLine : null,
|
||||
statement: Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-",
|
||||
axes: { ...axes },
|
||||
kinsType: activeSwitchkinsEvent?.requestedKinsType || null,
|
||||
switchkinsType: activeSwitchkinsEvent?.switchkinsType ?? null,
|
||||
switchkinsCode: activeSwitchkinsEvent?.code || null,
|
||||
switchkinsRemapBoundary: activeSwitchkinsEvent ? SWITCHKINS_REMAP_BOUNDARY : null,
|
||||
feedRate: activeFeedRate,
|
||||
feedMode: activeFeedMode,
|
||||
linearUnits: activeLinearUnits,
|
||||
raw: line,
|
||||
});
|
||||
}
|
||||
|
||||
return motion;
|
||||
}
|
||||
|
||||
function feedModesBySourceLine(programText) {
|
||||
const modes = [{ line: 0, feedMode: "units-per-minute" }];
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
const codeOnly = stripComments(line);
|
||||
let activeMode = null;
|
||||
for (const match of codeOnly.matchAll(/\bG\s*([0-9]+(?:\.[0-9]+)?)\b/gi)) {
|
||||
const value = Number(match[1]);
|
||||
if (value === 93) activeMode = "inverse-time";
|
||||
if (value === 94) activeMode = "units-per-minute";
|
||||
}
|
||||
if (activeMode) {
|
||||
modes.push({ line: index + 1, feedMode: activeMode });
|
||||
}
|
||||
});
|
||||
return modes;
|
||||
}
|
||||
|
||||
function latestFeedModeAtOrBeforeLine(modes, sourceLine) {
|
||||
let feedMode = "units-per-minute";
|
||||
for (const entry of modes) {
|
||||
if (entry.line <= sourceLine) feedMode = entry.feedMode;
|
||||
}
|
||||
return feedMode;
|
||||
}
|
||||
|
||||
function linearUnitsBySourceLine(programText) {
|
||||
const units = [{ line: 0, linearUnits: "mm" }];
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
const codeOnly = stripComments(line);
|
||||
let activeUnits = null;
|
||||
for (const match of codeOnly.matchAll(/\bG\s*([0-9]+(?:\.[0-9]+)?)\b/gi)) {
|
||||
const value = Number(match[1]);
|
||||
if (value === 20) activeUnits = "inch";
|
||||
if (value === 21) activeUnits = "mm";
|
||||
}
|
||||
if (activeUnits) {
|
||||
units.push({ line: index + 1, linearUnits: activeUnits });
|
||||
}
|
||||
});
|
||||
return units;
|
||||
}
|
||||
|
||||
function latestLinearUnitsAtOrBeforeLine(units, sourceLine) {
|
||||
let linearUnits = "mm";
|
||||
for (const entry of units) {
|
||||
if (entry.line <= sourceLine) linearUnits = entry.linearUnits;
|
||||
}
|
||||
return linearUnits;
|
||||
}
|
||||
|
||||
function feedRatesBySourceLine(programText) {
|
||||
const rates = [];
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
const codeOnly = stripComments(line);
|
||||
let feedRate = null;
|
||||
for (const match of codeOnly.matchAll(/\bF\s*([-+]?\d+(?:\.\d+)?)/gi)) {
|
||||
const value = Number(match[1]);
|
||||
if (Number.isFinite(value) && value > 0) feedRate = value;
|
||||
}
|
||||
if (feedRate !== null) {
|
||||
rates.push({ line: index + 1, feedRate });
|
||||
}
|
||||
});
|
||||
return rates;
|
||||
}
|
||||
|
||||
function latestFeedRateAtOrBeforeLine(rates, sourceLine) {
|
||||
let feedRate = null;
|
||||
for (const entry of rates) {
|
||||
if (entry.line <= sourceLine) feedRate = entry.feedRate;
|
||||
}
|
||||
return feedRate;
|
||||
}
|
||||
|
||||
export function prepareLinuxCncProgramForRuntime(programText) {
|
||||
const switchkinsEvents = [];
|
||||
const runtimeLines = String(programText).split(/\r?\n/).map((line, index) => {
|
||||
const lineNumber = index + 1;
|
||||
if (String(line).trim() === "%") {
|
||||
return "(linuxcnc program delimiter)";
|
||||
}
|
||||
const events = readSwitchkinsEventsFromLine(line, lineNumber);
|
||||
if (events.length === 0) return line;
|
||||
switchkinsEvents.push(...events);
|
||||
return stripSwitchkinsMcodesFromLine(line, events);
|
||||
});
|
||||
|
||||
return {
|
||||
runtimeProgramText: runtimeLines.join("\n"),
|
||||
switchkinsEvents,
|
||||
};
|
||||
}
|
||||
|
||||
function readSwitchkinsEventsFromLine(line, lineNumber) {
|
||||
const codeOnly = stripComments(String(line));
|
||||
const events = [];
|
||||
for (const match of codeOnly.matchAll(/\bM\s*([0-9]+(?:\.[0-9]+)?)\b/gi)) {
|
||||
const value = Number(match[1]);
|
||||
const mCode = Number.isInteger(value) ? value : null;
|
||||
const switchkins = SWITCHKINS_M_CODES.get(mCode);
|
||||
if (!switchkins) continue;
|
||||
events.push({
|
||||
line: lineNumber,
|
||||
code: `M${mCode}`,
|
||||
switchkinsType: switchkins.switchkinsType,
|
||||
requestedKinsType: switchkins.requestedKinsType,
|
||||
semanticBoundary: SWITCHKINS_REMAP_BOUNDARY,
|
||||
});
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
function stripSwitchkinsMcodesFromLine(line, events) {
|
||||
const eventCodes = new Set(events.map((event) => event.code.slice(1)));
|
||||
let nextLine = String(line).replace(/\bM\s*([0-9]+(?:\.[0-9]+)?)\b/gi, (token, value) => {
|
||||
const numericValue = Number(value);
|
||||
if (Number.isInteger(numericValue) && eventCodes.has(String(numericValue))) {
|
||||
return " ";
|
||||
}
|
||||
return token;
|
||||
});
|
||||
const codeOnly = stripComments(nextLine).replace(/\bN\s*[0-9]+\b/gi, "").trim();
|
||||
if (!codeOnly) {
|
||||
nextLine = `(web runtime switchkins ${events.map((event) => event.code).join(" ")})`;
|
||||
}
|
||||
return nextLine;
|
||||
}
|
||||
|
||||
function stripComments(line) {
|
||||
return String(line)
|
||||
.replace(/\([^)]*\)/g, " ")
|
||||
.replace(/;.*$/g, " ");
|
||||
}
|
||||
|
||||
function switchkinsEventsByLine(events) {
|
||||
return [...events]
|
||||
.filter((event) => Number.isFinite(event.line))
|
||||
.sort((left, right) => left.line - right.line);
|
||||
}
|
||||
|
||||
function latestSwitchkinsEventAtOrBeforeLine(events, lineNumber) {
|
||||
let latest = null;
|
||||
for (const event of events) {
|
||||
if (event.line > lineNumber) break;
|
||||
latest = event;
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
function prependRuntimeEvents(resultText, switchkinsEvents) {
|
||||
if (switchkinsEvents.length === 0) return resultText;
|
||||
const eventLines = switchkinsEvents.map((event) => (
|
||||
`web_runtime_event=SWITCHKINS line=${event.line} code=${event.code} switchkins_type=${event.switchkinsType} requested_kins=${event.requestedKinsType}`
|
||||
));
|
||||
return `${eventLines.join("\n")}\n${resultText}`;
|
||||
}
|
||||
|
||||
function programLineMap(programText) {
|
||||
const lines = new Map();
|
||||
programText.split(/\r?\n/).forEach((line, index) => {
|
||||
lines.set(index + 1, line.trim() || "(blank)");
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
|
||||
function readCanonicalNumber(line, field) {
|
||||
const match = String(line).match(new RegExp(`\\b${field}=([-+0-9.eE]+)`));
|
||||
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;
|
||||
|
||||
const [{ readFileSync }, { dirname, resolve }, { fileURLToPath }] = await Promise.all([
|
||||
import("node:fs"),
|
||||
import("node:path"),
|
||||
import("node:url"),
|
||||
]);
|
||||
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||
const resolvedWasmRoot = wasmRoot || resolve(moduleDir, "../../../../wasm-port/build/wasm/core");
|
||||
return {
|
||||
...quietOptions,
|
||||
wasmBinary: readFileSync(resolve(resolvedWasmRoot, "linuxcnc_interp.wasm")),
|
||||
};
|
||||
}
|
||||
|
||||
async function createDefaultTpModuleOptions({ wasmRoot }) {
|
||||
const quietOptions = { print() {}, printErr() {} };
|
||||
if (!isNodeRuntime()) return quietOptions;
|
||||
|
||||
const [{ readFileSync }, { dirname, resolve }, { fileURLToPath }] = await Promise.all([
|
||||
import("node:fs"),
|
||||
import("node:path"),
|
||||
import("node:url"),
|
||||
]);
|
||||
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||
const resolvedWasmRoot = wasmRoot || resolve(moduleDir, "../../../../wasm-port/build/wasm/tp");
|
||||
return {
|
||||
...quietOptions,
|
||||
wasmBinary: readFileSync(resolve(resolvedWasmRoot, "linuxcnc_tp.wasm")),
|
||||
};
|
||||
}
|
||||
|
||||
function isNodeRuntime() {
|
||||
return typeof process === "object"
|
||||
&& typeof process.versions === "object"
|
||||
&& typeof process.versions.node === "string"
|
||||
&& process.type !== "renderer";
|
||||
}
|
||||
80
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-interpreter-worker-client.js
vendored
Normal file
80
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-interpreter-worker-client.js
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
const SOURCE_MODE = "linuxcnc-interpreter-wasm";
|
||||
const SEMANTIC_BOUNDARY = "linuxcnc_interpreter_wasm_canonical_events";
|
||||
|
||||
export async function createLinuxCncInterpreterWorkerRuntime({
|
||||
sdkModuleUrl,
|
||||
workerUrl = new URL("./linuxcnc-interpreter-worker.js", import.meta.url).href,
|
||||
} = {}) {
|
||||
if (typeof Worker !== "function") {
|
||||
throw new Error("Web Worker is not available in this runtime");
|
||||
}
|
||||
|
||||
const worker = new Worker(workerUrl, { type: "module" });
|
||||
const request = createWorkerRequest(worker);
|
||||
const readiness = await request("init", { sdkModuleUrl });
|
||||
|
||||
const runtime = {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-interpreter-worker-runtime",
|
||||
loaded: true,
|
||||
sourceMode: SOURCE_MODE,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
executionContext: "worker",
|
||||
workerUrl,
|
||||
|
||||
readiness() {
|
||||
return {
|
||||
...readiness,
|
||||
apiName: "web-rtcp-5axis-linuxcnc-interpreter-worker-runtime-readiness",
|
||||
executionContext: "worker",
|
||||
workerUrl,
|
||||
};
|
||||
},
|
||||
|
||||
runProgram(programText) {
|
||||
return request("runProgram", { programText });
|
||||
},
|
||||
|
||||
runMachineFileProgram(options = {}) {
|
||||
return request("runMachineFileProgram", options);
|
||||
},
|
||||
|
||||
terminate() {
|
||||
worker.terminate();
|
||||
},
|
||||
};
|
||||
|
||||
return runtime;
|
||||
}
|
||||
|
||||
function createWorkerRequest(worker) {
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
|
||||
worker.addEventListener("message", (event) => {
|
||||
const { id, ok, value, error } = event.data || {};
|
||||
const request = pending.get(id);
|
||||
if (!request) return;
|
||||
pending.delete(id);
|
||||
if (ok) {
|
||||
request.resolve(value);
|
||||
} else {
|
||||
request.reject(new Error(error || "LinuxCNC interpreter worker request failed"));
|
||||
}
|
||||
});
|
||||
|
||||
worker.addEventListener("error", (event) => {
|
||||
const error = new Error(event.message || "LinuxCNC interpreter worker error");
|
||||
for (const request of pending.values()) {
|
||||
request.reject(error);
|
||||
}
|
||||
pending.clear();
|
||||
});
|
||||
|
||||
return function request(type, payload = {}) {
|
||||
const id = nextId++;
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
worker.postMessage({ id, type, payload });
|
||||
});
|
||||
};
|
||||
}
|
||||
49
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-interpreter-worker.js
vendored
Normal file
49
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-interpreter-worker.js
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createLinuxCncInterpreterRuntime } from "./linuxcnc-interpreter-runtime.js";
|
||||
|
||||
let runtime = null;
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
const { id, type, payload = {} } = event.data || {};
|
||||
try {
|
||||
if (type === "init") {
|
||||
runtime = await createLinuxCncInterpreterRuntime({
|
||||
moduleOptions: payload.moduleOptions,
|
||||
wasmRoot: payload.wasmRoot,
|
||||
sdkModuleUrl: payload.sdkModuleUrl,
|
||||
});
|
||||
postSuccess(id, runtime.readiness());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!runtime?.loaded) {
|
||||
throw new Error("LinuxCNC interpreter worker runtime is not initialized");
|
||||
}
|
||||
|
||||
if (type === "readiness") {
|
||||
postSuccess(id, runtime.readiness());
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "runProgram") {
|
||||
postSuccess(id, runtime.runProgram(payload.programText || ""));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "runMachineFileProgram") {
|
||||
postSuccess(id, runtime.runMachineFileProgram(payload));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`unknown LinuxCNC interpreter worker request: ${type}`);
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
id,
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function postSuccess(id, value) {
|
||||
self.postMessage({ id, ok: true, value });
|
||||
}
|
||||
144
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-kinematics-runtime.js
vendored
Normal file
144
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-kinematics-runtime.js
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
const DEFAULT_MODULE_ID = "xyzbc-trt";
|
||||
const DEFAULT_JOINT_COUNT = 5;
|
||||
const SOURCE_MODE = "source-derived-kinematics-wasm";
|
||||
const SEMANTIC_BOUNDARY = "linuxcnc_kinematics_wasm_c_abi";
|
||||
const DEFAULT_SDK_MODULE_URL = "../../../../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js";
|
||||
|
||||
export async function createLinuxCncKinematicsRuntime({
|
||||
moduleId = DEFAULT_MODULE_ID,
|
||||
moduleOptions = null,
|
||||
switchkinsType = 0,
|
||||
jointCount = DEFAULT_JOINT_COUNT,
|
||||
wasmRoot = null,
|
||||
sdkModuleUrl = DEFAULT_SDK_MODULE_URL,
|
||||
} = {}) {
|
||||
const {
|
||||
createLinuxCncKinematicsSdk,
|
||||
linuxCncKinematicsWasmFile,
|
||||
supportedLinuxCncKinematicsModules,
|
||||
} = await import(sdkModuleUrl);
|
||||
const wasmFile = linuxCncKinematicsWasmFile(moduleId);
|
||||
if (!wasmFile) {
|
||||
throw new Error(`unsupported LinuxCNC kinematics module: ${moduleId}`);
|
||||
}
|
||||
|
||||
const resolvedModuleOptions = moduleOptions || await createDefaultModuleOptions({ wasmRoot, wasmFile });
|
||||
const sdk = await createLinuxCncKinematicsSdk({ moduleId, moduleOptions: resolvedModuleOptions });
|
||||
let activeSwitchkinsType = switchkinsType;
|
||||
let activeSwitchRc = typeof sdk.switchKinematics === "function"
|
||||
? sdk.switchKinematics(switchkinsType)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-kinematics-runtime",
|
||||
moduleId,
|
||||
wasmFile,
|
||||
supportedModules: supportedLinuxCncKinematicsModules(),
|
||||
loaded: true,
|
||||
sourceMode: SOURCE_MODE,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
executionContext: "direct",
|
||||
get switchkinsType() {
|
||||
return activeSwitchkinsType;
|
||||
},
|
||||
get switchRc() {
|
||||
return activeSwitchRc;
|
||||
},
|
||||
jointCount,
|
||||
sdk,
|
||||
|
||||
readiness() {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-kinematics-runtime-readiness",
|
||||
moduleId,
|
||||
wasmFile,
|
||||
supportedModules: supportedLinuxCncKinematicsModules(),
|
||||
loaded: true,
|
||||
sourceMode: SOURCE_MODE,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
executionContext: "direct",
|
||||
switchkinsType: activeSwitchkinsType,
|
||||
switchRc: activeSwitchRc,
|
||||
};
|
||||
},
|
||||
|
||||
switchKinematics(nextSwitchkinsType) {
|
||||
const requestedType = Number(nextSwitchkinsType) || 0;
|
||||
activeSwitchRc = typeof sdk.switchKinematics === "function"
|
||||
? sdk.switchKinematics(requestedType)
|
||||
: 0;
|
||||
activeSwitchkinsType = requestedType;
|
||||
return activeSwitchRc;
|
||||
},
|
||||
|
||||
forward(joints, options = {}) {
|
||||
return sdk.forward(joints, options);
|
||||
},
|
||||
|
||||
inverse(pose, count = jointCount, options = {}) {
|
||||
return sdk.inverse(pose, count, options);
|
||||
},
|
||||
|
||||
frameForJoints(joints, options = {}) {
|
||||
const jointValues = Array.from(joints, Number);
|
||||
const forward = sdk.forward(jointValues, options.forwardOptions || {});
|
||||
const inverse = sdk.inverse(
|
||||
forward.pose,
|
||||
options.jointCount || jointCount,
|
||||
{ seedJoints: jointValues, ...(options.inverseOptions || {}) },
|
||||
);
|
||||
return {
|
||||
moduleId,
|
||||
switchkinsType: activeSwitchkinsType,
|
||||
forward,
|
||||
inverse,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function createDefaultModuleOptions({ wasmRoot, wasmFile }) {
|
||||
const quietOptions = {
|
||||
print() {},
|
||||
printErr() {},
|
||||
};
|
||||
if (!isNodeRuntime()) {
|
||||
return quietOptions;
|
||||
}
|
||||
|
||||
const [{ readFileSync }, { dirname, resolve }, { fileURLToPath }] = await Promise.all([
|
||||
import("node:fs"),
|
||||
import("node:path"),
|
||||
import("node:url"),
|
||||
]);
|
||||
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||
const resolvedWasmRoot = wasmRoot || resolve(moduleDir, "../../../../wasm-port/build/wasm/kinematics");
|
||||
return {
|
||||
...quietOptions,
|
||||
wasmBinary: readFileSync(resolve(resolvedWasmRoot, wasmFile)),
|
||||
};
|
||||
}
|
||||
|
||||
function isNodeRuntime() {
|
||||
return typeof process === "object"
|
||||
&& typeof process.versions === "object"
|
||||
&& typeof process.versions.node === "string"
|
||||
&& process.type !== "renderer";
|
||||
}
|
||||
|
||||
export function createLinuxCncKinematicsRuntimeDescriptor(runtime) {
|
||||
if (!runtime?.loaded) return null;
|
||||
return {
|
||||
apiName: runtime.apiName,
|
||||
moduleId: runtime.moduleId,
|
||||
wasmFile: runtime.wasmFile,
|
||||
supportedModules: runtime.supportedModules,
|
||||
loaded: runtime.loaded,
|
||||
sourceMode: runtime.sourceMode,
|
||||
semanticBoundary: runtime.semanticBoundary,
|
||||
executionContext: runtime.executionContext || "direct",
|
||||
workerUrl: runtime.workerUrl || null,
|
||||
switchkinsType: runtime.switchkinsType,
|
||||
switchRc: runtime.switchRc,
|
||||
};
|
||||
}
|
||||
107
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-kinematics-worker-client.js
vendored
Normal file
107
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-kinematics-worker-client.js
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
const DEFAULT_MODULE_ID = "xyzbc-trt";
|
||||
const DEFAULT_JOINT_COUNT = 5;
|
||||
const SOURCE_MODE = "source-derived-kinematics-wasm";
|
||||
const SEMANTIC_BOUNDARY = "linuxcnc_kinematics_wasm_c_abi";
|
||||
|
||||
export async function createLinuxCncKinematicsWorkerRuntime({
|
||||
moduleId = DEFAULT_MODULE_ID,
|
||||
switchkinsType = 0,
|
||||
jointCount = DEFAULT_JOINT_COUNT,
|
||||
sdkModuleUrl,
|
||||
workerUrl = new URL("./linuxcnc-kinematics-worker.js", import.meta.url).href,
|
||||
} = {}) {
|
||||
if (typeof Worker !== "function") {
|
||||
throw new Error("Web Worker is not available in this runtime");
|
||||
}
|
||||
|
||||
const worker = new Worker(workerUrl, { type: "module" });
|
||||
const request = createWorkerRequest(worker);
|
||||
const readiness = await request("init", {
|
||||
moduleId,
|
||||
switchkinsType,
|
||||
jointCount,
|
||||
sdkModuleUrl,
|
||||
});
|
||||
|
||||
const runtime = {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-kinematics-worker-runtime",
|
||||
moduleId,
|
||||
wasmFile: readiness.wasmFile,
|
||||
supportedModules: readiness.supportedModules,
|
||||
loaded: true,
|
||||
sourceMode: SOURCE_MODE,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
executionContext: "worker",
|
||||
workerUrl,
|
||||
switchkinsType,
|
||||
switchRc: readiness.switchRc,
|
||||
jointCount,
|
||||
|
||||
readiness() {
|
||||
return {
|
||||
...readiness,
|
||||
apiName: "web-rtcp-5axis-linuxcnc-kinematics-worker-runtime-readiness",
|
||||
executionContext: "worker",
|
||||
workerUrl,
|
||||
};
|
||||
},
|
||||
|
||||
forward(joints, options = {}) {
|
||||
return request("forward", { joints: Array.from(joints, Number), options });
|
||||
},
|
||||
|
||||
inverse(pose, count = jointCount, options = {}) {
|
||||
return request("inverse", { pose, count, options });
|
||||
},
|
||||
|
||||
async switchKinematics(nextSwitchkinsType) {
|
||||
const result = await request("switchKinematics", { switchkinsType: nextSwitchkinsType });
|
||||
runtime.switchkinsType = result.switchkinsType;
|
||||
runtime.switchRc = result.switchRc;
|
||||
return result.switchRc;
|
||||
},
|
||||
|
||||
frameForJoints(joints, options = {}) {
|
||||
return request("frameForJoints", { joints: Array.from(joints, Number), options });
|
||||
},
|
||||
|
||||
terminate() {
|
||||
worker.terminate();
|
||||
},
|
||||
};
|
||||
|
||||
return runtime;
|
||||
}
|
||||
|
||||
function createWorkerRequest(worker) {
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
|
||||
worker.addEventListener("message", (event) => {
|
||||
const { id, ok, value, error } = event.data || {};
|
||||
const request = pending.get(id);
|
||||
if (!request) return;
|
||||
pending.delete(id);
|
||||
if (ok) {
|
||||
request.resolve(value);
|
||||
} else {
|
||||
request.reject(new Error(error || "LinuxCNC kinematics worker request failed"));
|
||||
}
|
||||
});
|
||||
|
||||
worker.addEventListener("error", (event) => {
|
||||
const error = new Error(event.message || "LinuxCNC kinematics worker error");
|
||||
for (const request of pending.values()) {
|
||||
request.reject(error);
|
||||
}
|
||||
pending.clear();
|
||||
});
|
||||
|
||||
return function request(type, payload = {}) {
|
||||
const id = nextId++;
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
worker.postMessage({ id, type, payload });
|
||||
});
|
||||
};
|
||||
}
|
||||
66
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-kinematics-worker.js
vendored
Normal file
66
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-kinematics-worker.js
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
import { createLinuxCncKinematicsRuntime } from "./linuxcnc-kinematics-runtime.js";
|
||||
|
||||
let runtime = null;
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
const { id, type, payload = {} } = event.data || {};
|
||||
try {
|
||||
if (type === "init") {
|
||||
runtime = await createLinuxCncKinematicsRuntime({
|
||||
moduleId: payload.moduleId,
|
||||
moduleOptions: payload.moduleOptions,
|
||||
switchkinsType: payload.switchkinsType,
|
||||
jointCount: payload.jointCount,
|
||||
wasmRoot: payload.wasmRoot,
|
||||
sdkModuleUrl: payload.sdkModuleUrl,
|
||||
});
|
||||
postSuccess(id, runtime.readiness());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!runtime?.loaded) {
|
||||
throw new Error("LinuxCNC kinematics worker runtime is not initialized");
|
||||
}
|
||||
|
||||
if (type === "readiness") {
|
||||
postSuccess(id, runtime.readiness());
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "forward") {
|
||||
postSuccess(id, runtime.forward(payload.joints, payload.options || {}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "inverse") {
|
||||
postSuccess(id, runtime.inverse(payload.pose, payload.count, payload.options || {}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "switchKinematics") {
|
||||
const switchRc = runtime.switchKinematics(payload.switchkinsType);
|
||||
postSuccess(id, {
|
||||
switchkinsType: runtime.switchkinsType,
|
||||
switchRc,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "frameForJoints") {
|
||||
postSuccess(id, runtime.frameForJoints(payload.joints, payload.options || {}));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`unknown LinuxCNC kinematics worker request: ${type}`);
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
id,
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function postSuccess(id, value) {
|
||||
self.postMessage({ id, ok: true, value });
|
||||
}
|
||||
562
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-machine-file-staging.js
vendored
Normal file
562
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-machine-file-staging.js
vendored
Normal file
@@ -0,0 +1,562 @@
|
||||
const DEFAULT_SDK_MODULE_URL = "../../../../wasm-port/runtime/sdk/src/sim-config-staging.js";
|
||||
const DEFAULT_MANIFEST_URLS = [
|
||||
new URL("../../../../wasm-port/tools/source-manifest.txt", import.meta.url).href,
|
||||
new URL("../../wasm-port/tools/source-manifest.txt", import.meta.url).href,
|
||||
];
|
||||
const DEFAULT_VENDOR_ROOT_URLS = [
|
||||
new URL("../../../../wasm-port/vendor/linuxcnc/", import.meta.url).href,
|
||||
new URL("../../wasm-port/vendor/linuxcnc/", import.meta.url).href,
|
||||
];
|
||||
const DEFAULT_REPO_ROOT_URLS = [
|
||||
new URL("../../../../", import.meta.url).href,
|
||||
new URL("../../", import.meta.url).href,
|
||||
];
|
||||
const DEFAULT_TEST_SOURCE_ROOT_URLS = [
|
||||
new URL("../../../working_run/test_linuxcnc_source/", import.meta.url).href,
|
||||
new URL("../../working_run/test_linuxcnc_source/", import.meta.url).href,
|
||||
new URL("../../../../working_run/test_linuxcnc_source/", import.meta.url).href,
|
||||
];
|
||||
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-xyzbc-trt-sim-plan/machines";
|
||||
let browserMemoryMachineFileStorage = null;
|
||||
|
||||
export async function createMachineFileStagingPlan({
|
||||
profile,
|
||||
iniText = null,
|
||||
manifestText = null,
|
||||
sdkModuleUrl = DEFAULT_SDK_MODULE_URL,
|
||||
manifestUrl = null,
|
||||
wasmDir = null,
|
||||
} = {}) {
|
||||
if (!profile?.iniPath) {
|
||||
throw new Error("machine file staging requires a profile with iniPath");
|
||||
}
|
||||
const { planSimConfigStaging } = await import(sdkModuleUrl);
|
||||
const resolvedManifestText = manifestText ?? await readTextFromCandidateUrls(
|
||||
manifestUrl ? [manifestUrl] : DEFAULT_MANIFEST_URLS,
|
||||
);
|
||||
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,
|
||||
iniFile,
|
||||
iniText: resolvedIniText,
|
||||
wasmDir: wasmDir || profile.machineFileStaging?.wasmDir || `/work/sim/${machineRel}/${profile.id}`,
|
||||
});
|
||||
|
||||
const files = addProfileRuntimeFiles(
|
||||
addVendoredDemoSources(plan.files, resolvedManifestText, plan.wasmDir, {
|
||||
sourcePrefix: sourcePrefixForProfile(profile),
|
||||
demoDirectory,
|
||||
}),
|
||||
profile,
|
||||
plan.wasmDir,
|
||||
);
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-machine-file-staging-plan",
|
||||
profileId: profile.id,
|
||||
machineRel,
|
||||
demoDirectory,
|
||||
iniPath: profile.iniPath,
|
||||
wasmDir: plan.wasmDir,
|
||||
wasmIniPath: plan.iniPath,
|
||||
wasmProgramPath: plan.programPath,
|
||||
files: files.map((file) => ({
|
||||
...file,
|
||||
opfsPath: opfsPathFor(profile.id, file.sourceRel),
|
||||
kind: classifySourceRel(file.sourceRel),
|
||||
})),
|
||||
summary: summarizePlan(files),
|
||||
taskHalSession: createTaskHalSession({
|
||||
profileId: profile.id,
|
||||
wasmDir: plan.wasmDir,
|
||||
iniPath: plan.iniPath,
|
||||
programPath: plan.programPath,
|
||||
files,
|
||||
}),
|
||||
semanticBoundary: "linuxcnc_sim_config_file_staging_plan_only",
|
||||
};
|
||||
}
|
||||
|
||||
export function listLinuxCncGcodeSources(save) {
|
||||
return [...(save?.files || [])]
|
||||
.filter((file) => file.kind === "demo" && isLinuxCncFiveAxisGcodeSourceRel(file.sourceRel, save?.plan || save))
|
||||
.map((file) => ({
|
||||
sourceRel: file.sourceRel,
|
||||
wasmPath: file.wasmPath,
|
||||
opfsPath: file.opfsPath,
|
||||
filename: basename(file.sourceRel),
|
||||
bytes: file.bytes,
|
||||
label: basename(file.sourceRel).replace(/\.ngc$/i, ""),
|
||||
sourceMode: "linuxcnc-vendored-5axis-gcode",
|
||||
semanticBoundary: "linuxcnc_vendored_5axis_gcode_source_file",
|
||||
}))
|
||||
.sort((left, right) => left.filename.localeCompare(right.filename));
|
||||
}
|
||||
|
||||
export function listProjectGcodeFiles(save) {
|
||||
return [...(save?.files || [])]
|
||||
.filter((file) => file.kind === "demo" || file.kind === "remap")
|
||||
.map((file) => ({
|
||||
sourceRel: file.sourceRel,
|
||||
wasmPath: file.wasmPath,
|
||||
opfsPath: file.opfsPath,
|
||||
filename: basename(file.sourceRel),
|
||||
bytes: file.bytes,
|
||||
kind: file.kind,
|
||||
semanticBoundary: file.kind === "demo"
|
||||
? "linuxcnc_vendored_5axis_gcode_source_file"
|
||||
: "linuxcnc_vendored_5axis_remap_subroutine_file",
|
||||
}))
|
||||
.sort((left, right) => left.sourceRel.localeCompare(right.sourceRel));
|
||||
}
|
||||
|
||||
export function selectMachineFileProgram(plan, save, sourceRel) {
|
||||
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel, 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);
|
||||
if (!selectedFile) {
|
||||
throw new Error(`LinuxCNC G-code source not staged: ${sourceRel}`);
|
||||
}
|
||||
return {
|
||||
...plan,
|
||||
wasmProgramPath: selectedFile.wasmPath || selectedFile.path,
|
||||
selectedProgramSourceRel: selectedFile.sourceRel,
|
||||
selectedProgramFilename: basename(selectedFile.sourceRel),
|
||||
selectedProgramBytes: selectedFile.bytes,
|
||||
taskHalSession: {
|
||||
...(plan.taskHalSession || {}),
|
||||
programPath: selectedFile.wasmPath || selectedFile.path,
|
||||
programSourceRel: selectedFile.sourceRel,
|
||||
},
|
||||
semanticBoundary: "linuxcnc_sim_config_file_staging_plan_with_selected_gcode_source",
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||
if (plan?.apiName !== "web-rtcp-5axis-machine-file-staging-plan") {
|
||||
throw new Error("saveMachineFileStagingPlan requires a machine-file staging plan");
|
||||
}
|
||||
const storage = resolveMachineFileStorage(options);
|
||||
const sourceTextOverrides = options.sourceTextOverrides || {};
|
||||
const savedFiles = [];
|
||||
for (const file of plan.files) {
|
||||
const text = typeof sourceTextOverrides[file.sourceRel] === "string"
|
||||
? sourceTextOverrides[file.sourceRel]
|
||||
: await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel));
|
||||
await saveTextFile(file.opfsPath, text, storage.storage);
|
||||
savedFiles.push({
|
||||
sourceRel: file.sourceRel,
|
||||
opfsPath: file.opfsPath,
|
||||
wasmPath: file.wasmPath,
|
||||
path: file.wasmPath,
|
||||
text,
|
||||
kind: file.kind,
|
||||
bytes: text.length,
|
||||
executable: Boolean(file.executable),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-machine-file-staging-save",
|
||||
profileId: plan.profileId,
|
||||
status: "saved",
|
||||
savedAt: new Date().toISOString(),
|
||||
fileCount: savedFiles.length,
|
||||
opfsRoot: `${OPFS_ROOT}/${plan.profileId}`,
|
||||
storageMode: storage.mode,
|
||||
storageCapability: storage.capability,
|
||||
files: savedFiles,
|
||||
plan,
|
||||
gcodeSources: listLinuxCncGcodeSources({ ...plan, files: savedFiles }),
|
||||
gcodeFiles: listProjectGcodeFiles({ files: savedFiles }),
|
||||
summary: summarizeSavedFiles(savedFiles),
|
||||
taskHalSession: {
|
||||
...(plan.taskHalSession || {}),
|
||||
files: savedFiles.map((file) => ({
|
||||
sourceRel: file.sourceRel,
|
||||
wasmPath: file.wasmPath,
|
||||
path: file.wasmPath,
|
||||
kind: file.kind,
|
||||
bytes: file.bytes,
|
||||
})),
|
||||
},
|
||||
semanticBoundary: storage.mode === "opfs"
|
||||
? "opfs_machine_file_text_staging_only"
|
||||
: "memory_machine_file_text_staging_current_page_lifecycle_only",
|
||||
};
|
||||
}
|
||||
|
||||
export async function stageProfileMachineFiles(profile, options = {}) {
|
||||
const plan = await createMachineFileStagingPlan({
|
||||
profile,
|
||||
iniText: options.iniText,
|
||||
manifestText: options.manifestText,
|
||||
sdkModuleUrl: options.sdkModuleUrl,
|
||||
manifestUrl: options.manifestUrl,
|
||||
wasmDir: options.wasmDir,
|
||||
});
|
||||
const save = await saveMachineFileStagingPlan(plan, {
|
||||
storage: options.storage,
|
||||
storageMode: options.storageMode,
|
||||
sourceTextOverrides: options.sourceTextOverrides,
|
||||
});
|
||||
return { plan, save };
|
||||
}
|
||||
|
||||
export function detectMachineFileStorageCapability(globalScope = globalThis) {
|
||||
const forcedUnavailable = Boolean(globalScope.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__);
|
||||
const isBrowser = typeof globalScope.window === "object" || typeof globalScope.document === "object";
|
||||
const secureContext = !isBrowser || globalScope.isSecureContext !== false;
|
||||
const hasOpfs = typeof globalScope.navigator?.storage?.getDirectory === "function";
|
||||
const opfsAvailable = hasOpfs && secureContext && !forcedUnavailable;
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-machine-file-storage-capability",
|
||||
opfsAvailable,
|
||||
opfsUnavailable: !opfsAvailable,
|
||||
secureContext,
|
||||
forcedUnavailable,
|
||||
hasNavigatorStorage: Boolean(globalScope.navigator?.storage),
|
||||
hasGetDirectory: hasOpfs,
|
||||
fallbackMode: opfsAvailable ? null : "memory-fallback",
|
||||
reason: opfsAvailable
|
||||
? "opfs_available"
|
||||
: forcedUnavailable
|
||||
? "forced_unavailable"
|
||||
: !secureContext
|
||||
? "non_secure_context"
|
||||
: !hasOpfs
|
||||
? "missing_opfs_get_directory"
|
||||
: "opfs_unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMachineFileStorage(options = {}) {
|
||||
if (options.storage) {
|
||||
const mode = options.storageMode || storageModeFor(options.storage);
|
||||
if (options.requireOpfs === true && mode !== "opfs") {
|
||||
throw new Error(`OPFS storage required for machine files, got ${mode}`);
|
||||
}
|
||||
return {
|
||||
storage: options.storage,
|
||||
mode,
|
||||
capability: {
|
||||
apiName: "web-rtcp-5axis-machine-file-storage-capability",
|
||||
opfsAvailable: mode === "opfs",
|
||||
opfsUnavailable: mode !== "opfs",
|
||||
secureContext: true,
|
||||
fallbackMode: mode === "opfs" ? null : mode || "custom",
|
||||
reason: "explicit_storage",
|
||||
},
|
||||
};
|
||||
}
|
||||
const capability = detectMachineFileStorageCapability();
|
||||
if (capability.opfsAvailable) {
|
||||
return {
|
||||
storage: globalThis.navigator.storage,
|
||||
mode: "opfs",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
if (options.requireOpfs === true) {
|
||||
throw new Error(`OPFS storage required for machine files: ${capability.reason}`);
|
||||
}
|
||||
browserMemoryMachineFileStorage ??= createMemoryStorage();
|
||||
return {
|
||||
storage: browserMemoryMachineFileStorage,
|
||||
mode: "memory-fallback",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
|
||||
function storageModeFor(storage) {
|
||||
return storage?.apiName === "web-rtcp-5axis-memory-machine-file-storage" ||
|
||||
storage?.apiName === "web-rtcp-5axis-memory-session-storage"
|
||||
? "memory"
|
||||
: "custom";
|
||||
}
|
||||
|
||||
function createMemoryStorage(seed = {}) {
|
||||
const files = new Map(Object.entries(seed));
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-memory-machine-file-storage",
|
||||
files,
|
||||
async getDirectory() {
|
||||
return createDirectoryHandle(files, []);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function summarizePlan(files) {
|
||||
const kinds = countKinds(files.map((file) => classifySourceRel(file.sourceRel)));
|
||||
return {
|
||||
fileCount: files.length,
|
||||
requiredFileCount: files.filter((file) => (
|
||||
file.sourceRel.endsWith(".ini")
|
||||
|| file.sourceRel.includes("/demos/")
|
||||
|| file.sourceRel.includes("/examples/")
|
||||
)).length,
|
||||
gcodeFileCount: (kinds.demo || 0) + (kinds.remap || 0),
|
||||
remapFileCount: kinds.remap || 0,
|
||||
demoFileCount: kinds.demo || 0,
|
||||
toolTableFileCount: kinds.toolTable || 0,
|
||||
halFileCount: kinds.hal || 0,
|
||||
kinds,
|
||||
};
|
||||
}
|
||||
|
||||
function createTaskHalSession({ profileId, wasmDir, iniPath, programPath, files }) {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-session-plan",
|
||||
semanticBoundary: "linuxcnc_machine_file_session_for_task_hal_wasm",
|
||||
profileId,
|
||||
wasmDir,
|
||||
iniPath,
|
||||
programPath,
|
||||
halFiles: files.filter((file) => classifySourceRel(file.sourceRel) === "hal").map((file) => file.wasmPath),
|
||||
remapFiles: files.filter((file) => classifySourceRel(file.sourceRel) === "remap").map((file) => file.wasmPath),
|
||||
toolTableFiles: files.filter((file) => classifySourceRel(file.sourceRel) === "toolTable").map((file) => file.wasmPath),
|
||||
};
|
||||
}
|
||||
|
||||
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, { gcodeSourcePrefix: sourcePrefix })) continue;
|
||||
if (bySourceRel.has(sourceRel)) continue;
|
||||
bySourceRel.set(sourceRel, {
|
||||
sourceRel,
|
||||
wasmPath: `${wasmDir}/${demoDirectory}/${basename(sourceRel)}`,
|
||||
executable: false,
|
||||
});
|
||||
}
|
||||
return [...bySourceRel.values()];
|
||||
}
|
||||
|
||||
function addProfileRuntimeFiles(files, profile, wasmDir) {
|
||||
const bySourceRel = new Map(files.map((file) => [file.sourceRel, file]));
|
||||
for (const sourceRel of [
|
||||
profile.pyvcpXmlPath,
|
||||
]) {
|
||||
if (!sourceRel || bySourceRel.has(sourceRel)) continue;
|
||||
bySourceRel.set(sourceRel, {
|
||||
sourceRel,
|
||||
wasmPath: `${wasmDir}/${basename(sourceRel)}`,
|
||||
executable: false,
|
||||
});
|
||||
}
|
||||
return [...bySourceRel.values()];
|
||||
}
|
||||
|
||||
function isLinuxCncFiveAxisGcodeSourceRel(sourceRel, context = {}) {
|
||||
const value = String(sourceRel || "");
|
||||
const sourcePrefix = gcodeSourcePrefixForContext(context);
|
||||
return value.startsWith(sourcePrefix)
|
||||
&& value.endsWith(".ngc")
|
||||
&& !value.slice(sourcePrefix.length).includes("/");
|
||||
}
|
||||
|
||||
function summarizeSavedFiles(files) {
|
||||
const kinds = countKinds(files.map((file) => file.kind));
|
||||
return {
|
||||
fileCount: files.length,
|
||||
totalBytes: files.reduce((total, file) => total + file.bytes, 0),
|
||||
gcodeFileCount: (kinds.demo || 0) + (kinds.remap || 0),
|
||||
kinds,
|
||||
opfsPaths: files.map((file) => file.opfsPath),
|
||||
};
|
||||
}
|
||||
|
||||
function countKinds(kinds) {
|
||||
return kinds.reduce((counts, kind) => {
|
||||
counts[kind] = (counts[kind] || 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function classifySourceRel(sourceRel) {
|
||||
if (sourceRel.endsWith(".ini")) return "ini";
|
||||
if (sourceRel.endsWith(".tbl")) return "toolTable";
|
||||
if (sourceRel.endsWith(".hal")) return "hal";
|
||||
if (sourceRel.includes("/remap_subs/")) return "remap";
|
||||
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("\\", "/")}`;
|
||||
}
|
||||
|
||||
async function readTextFromCandidateUrls(urls) {
|
||||
const errors = [];
|
||||
for (const url of urls) {
|
||||
try {
|
||||
return await readTextFromUrl(url);
|
||||
} catch (error) {
|
||||
errors.push(`${url}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`failed to read machine staging asset: ${errors.join(" | ")}`);
|
||||
}
|
||||
|
||||
async function readTextFromUrl(url) {
|
||||
if (isNodeRuntime()) {
|
||||
const [{ readFile }, { resolve }, { fileURLToPath }] = await Promise.all([
|
||||
import("node:fs/promises"),
|
||||
import("node:path"),
|
||||
import("node:url"),
|
||||
]);
|
||||
const path = String(url).startsWith("file:")
|
||||
? fileURLToPath(url)
|
||||
: resolve(process.cwd(), url);
|
||||
return readFile(path, "utf8");
|
||||
}
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`failed to fetch machine staging asset ${url}: ${response.status}`);
|
||||
}
|
||||
const text = await response.text();
|
||||
if (/^\s*<!doctype html/i.test(text) || /^\s*<html[\s>]/i.test(text)) {
|
||||
throw new Error(`machine staging asset resolved to HTML fallback ${url}`);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
async function saveTextFile(path, text, storage = globalThis.navigator?.storage) {
|
||||
const root = await getStorageRoot(storage);
|
||||
const dir = await ensureParentDir(root, path);
|
||||
const filename = splitPath(path).at(-1);
|
||||
const fileHandle = await dir.getFileHandle(filename, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(text);
|
||||
await writable.close();
|
||||
}
|
||||
|
||||
async function getStorageRoot(storage) {
|
||||
if (!storage?.getDirectory) {
|
||||
throw new Error("OPFS is not available in this browser.");
|
||||
}
|
||||
return storage.getDirectory();
|
||||
}
|
||||
|
||||
async function ensureParentDir(root, path) {
|
||||
let current = root;
|
||||
for (const part of splitPath(path).slice(0, -1)) {
|
||||
current = await current.getDirectoryHandle(part, { create: true });
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function createDirectoryHandle(files, prefix) {
|
||||
return {
|
||||
async getDirectoryHandle(name) {
|
||||
return createDirectoryHandle(files, [...prefix, name]);
|
||||
},
|
||||
async getFileHandle(name) {
|
||||
const path = [...prefix, name].join("/");
|
||||
return {
|
||||
async createWritable() {
|
||||
let content = "";
|
||||
return {
|
||||
async write(text) {
|
||||
content += String(text);
|
||||
},
|
||||
async close() {
|
||||
files.set(path, content);
|
||||
},
|
||||
};
|
||||
},
|
||||
async getFile() {
|
||||
if (!files.has(path)) throw new Error(`Missing memory machine file: ${path}`);
|
||||
return { async text() { return files.get(path); } };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function splitPath(path) {
|
||||
const parts = String(path || "").replaceAll("\\", "/").split("/").filter(Boolean);
|
||||
if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
|
||||
throw new Error(`Invalid OPFS path: ${path}`);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
function sourceUrlsFor(sourceRel) {
|
||||
if (String(sourceRel).startsWith("linuxcnc/")) {
|
||||
return sourceOverrideUrlsFor(sourceRel).concat(
|
||||
DEFAULT_REPO_ROOT_URLS.map((rootUrl) => new URL(sourceRel, rootUrl).href),
|
||||
);
|
||||
}
|
||||
return sourceOverrideUrlsFor(sourceRel).concat(
|
||||
DEFAULT_VENDOR_ROOT_URLS.map((rootUrl) => new URL(sourceRel, rootUrl).href),
|
||||
);
|
||||
}
|
||||
|
||||
function sourceOverrideUrlsFor(sourceRel) {
|
||||
const basenameValue = basename(sourceRel);
|
||||
if (!basenameValue) return [];
|
||||
|
||||
if (sourceRel === `configs/sim/${TRT_MACHINE_REL}/xyzac-trt.ini`) {
|
||||
return DEFAULT_TEST_SOURCE_ROOT_URLS.map((rootUrl) => new URL("xyzac-trt.ini", rootUrl).href);
|
||||
}
|
||||
if (sourceRel === `${TRT_DEMO_SOURCE_PREFIX}impeller-7bl-xyzac.ngc`) {
|
||||
return DEFAULT_TEST_SOURCE_ROOT_URLS.map((rootUrl) => new URL("impeller-7bl-xyzac.ngc", rootUrl).href);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function basename(path) {
|
||||
return String(path).split("/").filter(Boolean).at(-1);
|
||||
}
|
||||
|
||||
function assertPathSegment(segment) {
|
||||
if (!/^[a-z0-9._-]+$/i.test(String(segment))) {
|
||||
throw new Error(`Invalid OPFS path segment: ${segment}`);
|
||||
}
|
||||
return segment;
|
||||
}
|
||||
|
||||
function isNodeRuntime() {
|
||||
return typeof process === "object"
|
||||
&& typeof process.versions === "object"
|
||||
&& typeof process.versions.node === "string"
|
||||
&& process.type !== "renderer";
|
||||
}
|
||||
460
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-parity-matrix.js
vendored
Normal file
460
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-parity-matrix.js
vendored
Normal file
@@ -0,0 +1,460 @@
|
||||
import { linuxCncSourceReferenceMap } from "../profiles/source-reference-map.js";
|
||||
|
||||
export const RIGHT_SIDEBAR_PARITY_ENTRIES = [
|
||||
"E-STOP",
|
||||
"POWER",
|
||||
"RESET",
|
||||
"AUTO",
|
||||
"MANUAL",
|
||||
"JOG",
|
||||
"MDI",
|
||||
"IDENTITY",
|
||||
"TCP",
|
||||
];
|
||||
|
||||
export const SWITCHKINS_PARITY_CODES = ["M428", "M429", "M430"];
|
||||
|
||||
export const LINUXCNC_PARITY_SOURCE_REFERENCES = [
|
||||
{
|
||||
id: "remap-428",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
|
||||
},
|
||||
{
|
||||
id: "remap-429",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
|
||||
},
|
||||
{
|
||||
id: "remap-430",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-emc-hh",
|
||||
path: "linuxcnc/src/emc/nml_intf/emc.hh",
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-emctaskmain",
|
||||
path: "linuxcnc/src/emc/task/emctaskmain.cc",
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-emctask",
|
||||
path: "linuxcnc/src/emc/task/emctask.cc",
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-motion-control",
|
||||
path: "linuxcnc/src/emc/motion/control.c",
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-hal-lib",
|
||||
path: "linuxcnc/src/hal/hal_lib.c",
|
||||
},
|
||||
{
|
||||
id: "web-machine-project-root",
|
||||
path: "web-rtcp-5axis-xyzbc-trt-sim-plan/machines/<profile>/configs/sim/axis/vismach/5axis/table-rotary-tilting",
|
||||
},
|
||||
];
|
||||
|
||||
export const LINUXCNC_PARITY_ITEMS = [
|
||||
{
|
||||
id: "xyzac-trt-axis-vismach-config",
|
||||
category: "machine-config",
|
||||
label: "XYZAC TRT AXIS/Vismach machine config",
|
||||
linuxCncPaths: [
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.tbl",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
|
||||
],
|
||||
functions: [
|
||||
"INI [KINS] xyzac-trt-kins sparm=identityfirst",
|
||||
"TRAJ XYZAC",
|
||||
"HALUI M429/M428/M430",
|
||||
"motion.switchkins-type",
|
||||
"project-staged INI/HAL/TBL/NGC",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "xyzbc-trt-axis-vismach-config",
|
||||
category: "machine-config",
|
||||
label: "XYZBC TRT AXIS/Vismach machine config",
|
||||
linuxCncPaths: [
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
|
||||
],
|
||||
functions: [
|
||||
"INI [KINS] xyzbc-trt-kins sparm=identityfirst",
|
||||
"TRAJ XYZBC",
|
||||
"HALUI M429/M428/M430",
|
||||
"motion.switchkins-type",
|
||||
"project-staged INI/HAL/TBL/NGC",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-trt-config",
|
||||
category: "machine-config",
|
||||
label: "gmoccapy non-trivial TRT sample config",
|
||||
linuxCncPaths: [
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/postgui.hal",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.tbl",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzac.ngc",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/test-xyzac.ngc",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/test-xyzbc.ngc",
|
||||
],
|
||||
functions: [
|
||||
"DISPLAY gmoccapy",
|
||||
"POSTGUI spindle/tool HAL bindings",
|
||||
"xyzac-trt-gui vismach feedback pins",
|
||||
"gmoccapy TRT example programs",
|
||||
"gmoccapy TRT ngcgui subroutine examples",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-native-operator-ui",
|
||||
category: "operator-ui",
|
||||
label: "gmoccapy native operator workflow",
|
||||
linuxCncPaths: [
|
||||
"linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py",
|
||||
"linuxcnc/share/gmoccapy/gmoccapy.glade",
|
||||
"web-rtcp-5axis-sim-plan/app/src/ui-reference/gmoccapy-button-icons.json",
|
||||
],
|
||||
functions: [
|
||||
"E-STOP/POWER/RESET task-state entry",
|
||||
"AUTO/MANUAL/JOG/MDI task-mode entry",
|
||||
"button active/blocked color rule",
|
||||
"gmoccapy HAL button/icon mapping",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "right-sidebar-task-interlocks",
|
||||
category: "operator-ui",
|
||||
label: "Right vertical machine-state entrances",
|
||||
linuxCncPaths: [
|
||||
"linuxcnc/src/emc/nml_intf/emc.hh",
|
||||
"linuxcnc/src/emc/task/emctaskmain.cc",
|
||||
"linuxcnc/src/emc/task/emctask.cc",
|
||||
],
|
||||
functions: [
|
||||
"EMC_TASK_SET_STATE",
|
||||
"EMC_TASK_SET_MODE",
|
||||
"EMC_JOINT_HOME",
|
||||
"EMC_JOG_INCR",
|
||||
"EMC_TASK_PLAN_RUN",
|
||||
"EMC_TASK_PLAN_EXECUTE",
|
||||
"EMC_TASK_ABORT",
|
||||
"mode and interpreter idle gates",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "vismach-machine-preview",
|
||||
category: "visualization",
|
||||
label: "Vismach TRT preview and feedback model",
|
||||
linuxCncPaths: [
|
||||
"src/hal/user_comps/vismach/xyzac-trt-gui.py",
|
||||
"src/hal/user_comps/vismach/xyzbc-trt-gui.py",
|
||||
],
|
||||
functions: [
|
||||
"table/saddle/spindle feedback pins",
|
||||
"A/B tilt and C rotate feedback pins",
|
||||
"tool-offset/y-offset/x-offset/z-offset visualization",
|
||||
"WebGL toolpath preview and current tool marker",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "source-derived-kinematics-switchkins",
|
||||
category: "runtime",
|
||||
label: "TRT kinematics and switchkins",
|
||||
linuxCncPaths: [
|
||||
"src/emc/kinematics/xyzac-trt-kins.c",
|
||||
"src/emc/kinematics/xyzbc-trt-kins.c",
|
||||
"src/emc/kinematics/trtfuncs.c",
|
||||
"src/emc/kinematics/switchkins.c",
|
||||
],
|
||||
functions: [
|
||||
"forward/inverse kinematics frame",
|
||||
"identity/TCP/userk kinstype mapping",
|
||||
"M428 TCP",
|
||||
"M429 identity",
|
||||
"M430 userk",
|
||||
"DRO TCP axis value refresh",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-interpreter-program-validation",
|
||||
category: "runtime",
|
||||
label: "LinuxCNC interpreter-backed G-code validation",
|
||||
linuxCncPaths: [
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_2.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_3.ngc",
|
||||
],
|
||||
functions: [
|
||||
"real LinuxCNC 5-axis program source guard",
|
||||
"canonical motion extraction",
|
||||
"planner sample timing",
|
||||
"current line and realtime axis feedback",
|
||||
"toolpath preview path verification",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "machine-project-directory",
|
||||
category: "project-files",
|
||||
label: "Machine project directory and INI parity",
|
||||
linuxCncPaths: [
|
||||
"web-rtcp-5axis-xyzbc-trt-sim-plan/machines/<profile>/configs/sim/axis/vismach/5axis/table-rotary-tilting",
|
||||
],
|
||||
functions: [
|
||||
"profile-specific project root",
|
||||
"LinuxCNC INI source text match",
|
||||
"config file count",
|
||||
"G-code/remap file count",
|
||||
"selected program provenance",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "task-hal-status-loop",
|
||||
category: "runtime",
|
||||
label: "Task/HAL run feedback and DRO updates",
|
||||
linuxCncPaths: [
|
||||
"linuxcnc/src/emc/task/emctaskmain.cc",
|
||||
"linuxcnc/src/emc/motion/control.c",
|
||||
"linuxcnc/src/hal/hal_lib.c",
|
||||
],
|
||||
functions: [
|
||||
"task cycle status",
|
||||
"servo cycle status",
|
||||
"HAL changed pin summary",
|
||||
"run/pause/resume/step/stop feedback",
|
||||
"DRO axis value and DTG refresh",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-postgui-tool-spindle",
|
||||
category: "operator-ui",
|
||||
label: "gmoccapy POSTGUI tool/spindle HAL functions",
|
||||
linuxCncPaths: [
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/postgui.hal",
|
||||
"linuxcnc/configs/sim/gmoccapy/gmoccapy_postgui.hal",
|
||||
"linuxcnc/configs/sim/gmoccapy/core_sim_XYZAB.hal",
|
||||
"linuxcnc/configs/sim/gmoccapy/spindle_sim.hal",
|
||||
],
|
||||
functions: [
|
||||
"gmoccapy.spindle_feedback_bar",
|
||||
"gmoccapy.spindle_at_speed_led",
|
||||
"gmoccapy.toolchange-change/changing loop",
|
||||
"gmoccapy.tooloffset-z",
|
||||
"coolant/spindle/operator override gates",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function createLinuxCncParityMatrix(state = {}) {
|
||||
const items = LINUXCNC_PARITY_ITEMS.map((item) => createParityItemState(item, state));
|
||||
const implementedCount = items.filter((item) => item.implemented).length;
|
||||
const activeCount = items.filter((item) => item.active).length;
|
||||
const sourcePaths = unique(items.flatMap((item) => item.linuxCncPaths));
|
||||
const functions = unique(items.flatMap((item) => item.functions));
|
||||
const categories = Object.fromEntries(
|
||||
unique(items.map((item) => item.category)).map((category) => [
|
||||
category,
|
||||
{
|
||||
itemCount: items.filter((item) => item.category === category).length,
|
||||
implementedCount: items.filter((item) => item.category === category && item.implemented).length,
|
||||
activeCount: items.filter((item) => item.category === category && item.active).length,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const sidebarLabels = (state.rightSidebarEntrances || []).map((entry) => entry.label);
|
||||
const switchkinsCodes = state.profile?.kinematicsParameters?.switchkinsTypes
|
||||
?.map((type) => type.mdiCommand)
|
||||
.filter(Boolean) || [];
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-parity-matrix",
|
||||
profileId: state.profile?.id || state.machineProfile || "unknown",
|
||||
status: implementedCount === items.length ? "implemented" : "partial",
|
||||
implementedCount,
|
||||
activeCount,
|
||||
itemCount: items.length,
|
||||
sourcePathCount: sourcePaths.length,
|
||||
linuxCncProgramCount: sourcePaths.filter((path) => path.endsWith(".ngc")).length,
|
||||
functionCount: functions.length,
|
||||
categories,
|
||||
requiredRightSidebarEntries: RIGHT_SIDEBAR_PARITY_ENTRIES,
|
||||
actualRightSidebarEntries: sidebarLabels,
|
||||
rightSidebarComplete: RIGHT_SIDEBAR_PARITY_ENTRIES.every((label) => sidebarLabels.includes(label)),
|
||||
requiredSwitchkinsCodes: SWITCHKINS_PARITY_CODES,
|
||||
activeSwitchkinsCodes: switchkinsCodes,
|
||||
switchkinsComplete: SWITCHKINS_PARITY_CODES.every((code) => switchkinsCodes.includes(code)),
|
||||
items,
|
||||
linuxCncPrograms: sourcePaths,
|
||||
linuxCncFunctions: functions,
|
||||
sourceReferenceIds: unique(sourceReferenceIdsForPaths(sourcePaths)),
|
||||
semanticBoundary: "linuxcnc_source_function_parity_matrix_for_browser_simulation_not_hardware_control",
|
||||
};
|
||||
}
|
||||
|
||||
function createParityItemState(item, state) {
|
||||
const evidence = createItemEvidence(item.id, state);
|
||||
const sourceMapped = item.linuxCncPaths.every((path) => hasSourceReference(path));
|
||||
const implemented = sourceMapped && evidence.implemented !== false;
|
||||
const active = implemented && evidence.active === true;
|
||||
|
||||
return {
|
||||
...item,
|
||||
linuxCncReferenceIds: sourceReferenceIdsForPaths(item.linuxCncPaths),
|
||||
sourceMapped,
|
||||
implemented,
|
||||
active,
|
||||
status: active
|
||||
? "active"
|
||||
: implemented
|
||||
? evidence.status || "implemented"
|
||||
: "missing-source-reference",
|
||||
evidence,
|
||||
};
|
||||
}
|
||||
|
||||
function createItemEvidence(id, state) {
|
||||
switch (id) {
|
||||
case "xyzac-trt-axis-vismach-config":
|
||||
return {
|
||||
status: state.profile?.id === "xyzac-trt" ? "active-profile" : "implemented",
|
||||
active: state.profile?.id === "xyzac-trt",
|
||||
profileAvailable: profileAvailable(state, "xyzac-trt"),
|
||||
iniPath: state.profile?.id === "xyzac-trt" ? state.profile?.iniPath : null,
|
||||
};
|
||||
case "xyzbc-trt-axis-vismach-config":
|
||||
return {
|
||||
status: state.profile?.id === "xyzbc-trt" ? "active-profile" : "implemented",
|
||||
active: state.profile?.id === "xyzbc-trt",
|
||||
profileAvailable: profileAvailable(state, "xyzbc-trt"),
|
||||
iniPath: state.profile?.id === "xyzbc-trt" ? state.profile?.iniPath : null,
|
||||
};
|
||||
case "gmoccapy-trt-config":
|
||||
return {
|
||||
status: "reference-mapped",
|
||||
active: Boolean(state.gmoccapyGui),
|
||||
displayStyle: "gmoccapy-web-shell",
|
||||
};
|
||||
case "gmoccapy-native-operator-ui":
|
||||
return {
|
||||
active: Boolean(state.gmoccapyGui) && rightSidebarComplete(state),
|
||||
rightSidebarComplete: rightSidebarComplete(state),
|
||||
buttonCount: state.rightSidebarEntrances?.length || 0,
|
||||
};
|
||||
case "right-sidebar-task-interlocks":
|
||||
return {
|
||||
active: Boolean(state.linuxCncTaskPolicy) && rightSidebarComplete(state),
|
||||
taskState: state.linuxCncTaskPolicy?.taskState || null,
|
||||
taskMode: state.linuxCncTaskPolicy?.taskMode || null,
|
||||
interpState: state.linuxCncTaskPolicy?.interpState || null,
|
||||
gates: {
|
||||
canJog: state.linuxCncTaskPolicy?.canJog === true,
|
||||
canRunAuto: state.linuxCncTaskPolicy?.canRunAuto === true,
|
||||
canExecuteMdi: state.linuxCncTaskPolicy?.canExecuteMdi === true,
|
||||
},
|
||||
};
|
||||
case "vismach-machine-preview":
|
||||
return {
|
||||
active: Boolean(state.rtcpFrame) && Boolean(state.preview),
|
||||
frameApi: state.rtcpFrame?.apiName || null,
|
||||
previewPoints: state.preview?.pathPoints || 0,
|
||||
profileKinematics: state.profile?.kinematics || null,
|
||||
};
|
||||
case "source-derived-kinematics-switchkins":
|
||||
return {
|
||||
status: state.kinematicsRuntimeReadiness?.loaded ? "runtime-ready" : "implemented",
|
||||
active: Boolean(state.profile?.kinematics) && switchkinsComplete(state),
|
||||
runtimeLoaded: state.kinematicsRuntimeReadiness?.loaded === true,
|
||||
kinsType: state.kinsType || null,
|
||||
rtcpState: state.rtcpState || null,
|
||||
activeSwitchkinsCodes: state.profile?.kinematicsParameters?.switchkinsTypes?.map((type) => type.mdiCommand) || [],
|
||||
};
|
||||
case "linuxcnc-interpreter-program-validation":
|
||||
return {
|
||||
status: state.programValidation?.ready ? "runtime-validated" : "implemented",
|
||||
active: state.programValidation?.ready === true,
|
||||
sourceGuard: state.programValidation?.sourceGuard || null,
|
||||
motionEventCount: state.programValidation?.motionEventCount || 0,
|
||||
plannerSampleCount: state.programValidation?.plannerSampleCount || 0,
|
||||
};
|
||||
case "machine-project-directory":
|
||||
return {
|
||||
status: state.machineProject?.status === "staged" ? "staged" : "implemented",
|
||||
active: state.machineProject?.status === "staged",
|
||||
projectRoot: state.machineProject?.projectRoot || null,
|
||||
iniSourceMatchesProfile: state.machineProject?.ini?.sourceMatchesProfile === true,
|
||||
configFileCount: state.machineProject?.configFileCount || 0,
|
||||
gcodeFileCount: state.machineProject?.gcodeFileCount || 0,
|
||||
};
|
||||
case "task-hal-status-loop":
|
||||
return {
|
||||
status: state.taskHalStatusLoop?.active ? "runtime-active" : "implemented",
|
||||
active: Boolean(state.taskHalRuntimeReadiness?.loaded || state.programRuntimeFeedback),
|
||||
taskHalLoaded: state.taskHalRuntimeReadiness?.loaded === true,
|
||||
statusLoopActive: state.taskHalStatusLoop?.active === true,
|
||||
runtimeFeedbackSource: state.programRuntimeFeedback?.sourceMode || null,
|
||||
};
|
||||
case "gmoccapy-postgui-tool-spindle":
|
||||
return {
|
||||
active: Boolean(state.gmoccapyGui) && Boolean(state.toolPreview) && Boolean(state.spindle) && Boolean(state.coolant),
|
||||
toolNumber: state.toolPreview?.toolNumber || null,
|
||||
spindleDirection: state.spindle?.direction || null,
|
||||
coolantFlood: state.coolant?.flood === true,
|
||||
coolantMist: state.coolant?.mist === true,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
active: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function profileAvailable(state, profileId) {
|
||||
return (state.availableProfiles || []).some((profile) => profile.id === profileId);
|
||||
}
|
||||
|
||||
function rightSidebarComplete(state) {
|
||||
const labels = (state.rightSidebarEntrances || []).map((entry) => entry.label);
|
||||
return RIGHT_SIDEBAR_PARITY_ENTRIES.every((label) => labels.includes(label));
|
||||
}
|
||||
|
||||
function switchkinsComplete(state) {
|
||||
const codes = state.profile?.kinematicsParameters?.switchkinsTypes
|
||||
?.map((type) => type.mdiCommand)
|
||||
.filter(Boolean) || [];
|
||||
return SWITCHKINS_PARITY_CODES.every((code) => codes.includes(code));
|
||||
}
|
||||
|
||||
function hasSourceReference(path) {
|
||||
return allParityReferences().some((reference) => reference.path === path);
|
||||
}
|
||||
|
||||
function sourceReferenceIdsForPaths(paths) {
|
||||
return paths
|
||||
.map((path) => allParityReferences().find((reference) => reference.path === path)?.id)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function allParityReferences() {
|
||||
return [...linuxCncSourceReferenceMap, ...LINUXCNC_PARITY_SOURCE_REFERENCES];
|
||||
}
|
||||
|
||||
function unique(values) {
|
||||
return [...new Set(values.filter(Boolean))];
|
||||
}
|
||||
414
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-task-hal-runtime.js
vendored
Normal file
414
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-task-hal-runtime.js
vendored
Normal file
@@ -0,0 +1,414 @@
|
||||
const DEFAULT_SDK_MODULE_URLS = [
|
||||
new URL("../../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
|
||||
new URL("../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
|
||||
];
|
||||
|
||||
const SEMANTIC_BOUNDARY = "linuxcnc_task_motion_hal_wasm_simulation_runtime";
|
||||
|
||||
export async function createLinuxCncTaskHalRuntime({
|
||||
sdkModuleUrl = null,
|
||||
moduleOptions = {},
|
||||
} = {}) {
|
||||
const errors = [];
|
||||
const candidateUrls = sdkModuleUrl ? [sdkModuleUrl] : DEFAULT_SDK_MODULE_URLS;
|
||||
for (const url of candidateUrls) {
|
||||
try {
|
||||
const { createLinuxCncTaskHalSdk } = await import(url);
|
||||
const sdk = await createLinuxCncTaskHalSdk(moduleOptions);
|
||||
return wrapTaskHalSdk(sdk, {
|
||||
sdkModuleUrl: url,
|
||||
executionContext: "direct",
|
||||
});
|
||||
} catch (error) {
|
||||
errors.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`LinuxCNC task/HAL runtime unavailable: ${errors.join(" | ")}`);
|
||||
}
|
||||
|
||||
export function wrapTaskHalSdk(sdk, {
|
||||
sdkModuleUrl = null,
|
||||
executionContext = "direct",
|
||||
workerUrl = null,
|
||||
} = {}) {
|
||||
if (!sdk || typeof sdk.readiness !== "function") {
|
||||
throw new Error("wrapTaskHalSdk requires a task/HAL SDK");
|
||||
}
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime",
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
executionContext,
|
||||
sdkModuleUrl,
|
||||
workerUrl,
|
||||
loaded: true,
|
||||
|
||||
readiness() {
|
||||
const readiness = sdk.readiness();
|
||||
const taskRuntimeReady = readiness.taskRuntimeReady === true;
|
||||
const motionRuntimeReady = readiness.motionRuntimeReady === true;
|
||||
const halRuntimeReady = readiness.halRuntimeReady === true;
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
|
||||
loaded: true,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
sdkSemanticBoundary: readiness.semanticBoundary,
|
||||
executionContext,
|
||||
workerUrl,
|
||||
taskRuntimeReady,
|
||||
motionRuntimeReady,
|
||||
halRuntimeReady,
|
||||
halSyncReady: taskRuntimeReady && motionRuntimeReady && halRuntimeReady,
|
||||
nativeTaskReady: taskRuntimeReady,
|
||||
nativeHalSyncReady: taskRuntimeReady && motionRuntimeReady && halRuntimeReady,
|
||||
hardwareDrive: false,
|
||||
hostRealtimeKernel: false,
|
||||
externalUserMProcessReady: false,
|
||||
};
|
||||
},
|
||||
|
||||
initSession(session = {}) {
|
||||
return sdk.initSession(session);
|
||||
},
|
||||
|
||||
stageFiles(files = []) {
|
||||
let staged = 0;
|
||||
for (const file of files) {
|
||||
const path = file.wasmPath || file.path;
|
||||
if (!path) continue;
|
||||
const rc = sdk.stageFile(path, file.text || "");
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_stage_file failed for ${path} rc=${rc}`);
|
||||
}
|
||||
staged += 1;
|
||||
}
|
||||
return staged;
|
||||
},
|
||||
|
||||
openProgram(path) {
|
||||
const rc = sdk.openProgram(path);
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_open_program failed for ${path} rc=${rc}`);
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
|
||||
loadProgramMotionPlan(plan = {}) {
|
||||
if (typeof sdk.loadProgramMotionPlan !== "function") {
|
||||
throw new Error("task/HAL SDK missing loadProgramMotionPlan; rebuild wasm-port/tools/build_task_hal_wasm.sh");
|
||||
}
|
||||
const rc = sdk.loadProgramMotionPlan(plan);
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_load_program_motion_plan_json failed rc=${rc}`);
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
|
||||
sendCommand(command) {
|
||||
const rc = sdk.sendCommand(command);
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_send_command_json failed for ${command?.type || "unknown"} rc=${rc}`);
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
|
||||
runCycles(options = {}) {
|
||||
const rc = sdk.runCycles(options);
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_run_cycles failed rc=${rc}`);
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
|
||||
readStatus() {
|
||||
return normalizeTaskHalStatus(sdk.readStatus());
|
||||
},
|
||||
|
||||
readEvents() {
|
||||
return sdk.readEvents();
|
||||
},
|
||||
|
||||
resetSession() {
|
||||
return sdk.resetSession();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTaskHalSessionFromMachineFiles({ profile, plan, save, selectedProgramRel = null } = {}) {
|
||||
const files = save?.files || [];
|
||||
const iniFile = files.find((file) => file.kind === "ini")
|
||||
|| files.find((file) => file.sourceRel === profile?.iniPath)
|
||||
|| null;
|
||||
const selectedFile = selectedProgramRel
|
||||
? files.find((file) => file.sourceRel === selectedProgramRel)
|
||||
: null;
|
||||
const programFile = selectedFile
|
||||
|| files.find((file) => file.wasmPath === plan?.wasmProgramPath)
|
||||
|| files.find((file) => file.kind === "demo")
|
||||
|| null;
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-session",
|
||||
semanticBoundary: "linuxcnc_machine_files_for_task_hal_wasm_runtime",
|
||||
profileId: profile?.id || plan?.profileId || save?.profileId || "unknown",
|
||||
iniPath: iniFile?.wasmPath || plan?.wasmIniPath || plan?.iniPath || profile?.iniPath || null,
|
||||
iniText: iniFile?.text || "",
|
||||
programPath: programFile?.wasmPath || plan?.wasmProgramPath || null,
|
||||
programSourceRel: programFile?.sourceRel || selectedProgramRel || null,
|
||||
halFiles: files.filter((file) => file.kind === "hal").map(sessionFileDescriptor),
|
||||
toolTableFiles: files.filter((file) => file.kind === "toolTable").map(sessionFileDescriptor),
|
||||
remapFiles: files.filter((file) => file.kind === "remap").map(sessionFileDescriptor),
|
||||
files: files.map(sessionFileDescriptor),
|
||||
fileCount: files.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTaskHalProgramMotionPlan({
|
||||
programPath = null,
|
||||
motion = [],
|
||||
timing = null,
|
||||
linearUnits = "mm",
|
||||
programLines = [],
|
||||
} = {}) {
|
||||
const segments = Array.isArray(timing?.segments) ? timing.segments : [];
|
||||
let planSegments = segments.map((segment, index) => {
|
||||
const event = motion[index] || {};
|
||||
const startAxes = normalizePlanAxes(segment.startAxes || motion[index - 1]?.axes || {});
|
||||
const endAxes = normalizePlanAxes(segment.endAxes || segment.axes || event.axes || startAxes);
|
||||
return {
|
||||
line: Number(segment.line ?? event.line ?? index + 1),
|
||||
type: segment.type || event.type || "STRAIGHT_FEED",
|
||||
motionClass: segment.motionClass || (event.type === "STRAIGHT_TRAVERSE" ? "rapid" : "feed"),
|
||||
feedMode: segment.feedMode || event.feedMode || "units-per-minute",
|
||||
startSeconds: Number(segment.startSeconds || 0),
|
||||
durationSeconds: Math.max(Number(segment.durationSeconds || 0), 0),
|
||||
elapsedSeconds: Number(segment.elapsedSeconds || 0),
|
||||
feedRate: Number(segment.feedRate || event.feedRate || 0),
|
||||
linearUnits: segment.linearUnits || event.linearUnits || linearUnits,
|
||||
velocityMmPerMin: Math.max(Number(segment.velocityMmPerMin || 0), 0),
|
||||
requestedVelocityMmPerMin: Math.max(Number(segment.requestedVelocityMmPerMin || segment.velocityMmPerMin || 0), 0),
|
||||
startAxes,
|
||||
endAxes,
|
||||
};
|
||||
}).filter((segment) => segment.durationSeconds > 0 || segment.line > 0);
|
||||
const lineSegments = buildSourceLineMotionSegments({
|
||||
programLines,
|
||||
seedSegments: planSegments,
|
||||
linearUnits,
|
||||
});
|
||||
if (shouldUseSourceLineSegments(planSegments, lineSegments)) {
|
||||
planSegments = lineSegments;
|
||||
}
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-program-motion-plan",
|
||||
semanticBoundary: "linuxcnc_canonical_motion_feed_timed_task_hal_plan",
|
||||
programPath,
|
||||
linearUnits,
|
||||
totalSeconds: Number(timing?.totalSeconds || 0),
|
||||
segmentCount: planSegments.length,
|
||||
segments: planSegments,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeTaskHalStatus(status = {}) {
|
||||
const motion = status.motionStatus?.motion || {};
|
||||
const axis = status.motionStatus?.axis || {};
|
||||
const halPins = status.halSnapshot?.pins || {};
|
||||
const motionProgramLine = Number(motion.programLine || 0);
|
||||
const halProgramLine = Number(halPins["motion.program-line"]?.value || 0);
|
||||
const activeLine = motionProgramLine > 0
|
||||
? motionProgramLine
|
||||
: halProgramLine > 0
|
||||
? halProgramLine
|
||||
: 1;
|
||||
return {
|
||||
...status,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
summary: {
|
||||
taskRuntimeReady: status.taskRuntimeReady === true,
|
||||
motionRuntimeReady: status.motionStatus?.motionHalSyncReady === true || status.taskCommandsDriveMotionRuntime === true,
|
||||
halRuntimeReady: Boolean(status.halSnapshot?.halRuntimeReady ?? status.halSnapshot?.ready ?? true),
|
||||
halSyncReady: status.taskCommandsDriveMotionRuntime === true && Boolean(halPins["motion.program-line"]),
|
||||
taskHalComparisonReady: status.taskRuntimeReady === true && status.taskCommandsDriveMotionRuntime === true,
|
||||
switchkinsRemapHalSync: Boolean(halPins["motion.switchkins-type"]),
|
||||
nativeTaskReady: status.taskRuntimeReady === true,
|
||||
nativeHalSyncReady: status.taskCommandsDriveMotionRuntime === true && Boolean(halPins["motion.program-line"]),
|
||||
fullLinuxCncProgramExecutionReady: false,
|
||||
hardwareDrive: false,
|
||||
hostRealtimeKernel: false,
|
||||
},
|
||||
ui: {
|
||||
taskState: String(status.task?.state || "ESTOP").toLowerCase(),
|
||||
taskMode: String(status.task?.mode || "MANUAL").toLowerCase(),
|
||||
interpState: String(status.task?.interpState || "IDLE").toLowerCase(),
|
||||
execState: String(status.task?.execState || "DONE").toLowerCase(),
|
||||
taskCycle: Number(status.task?.taskCycle ?? status.taskCycle ?? 0),
|
||||
servoCycle: Number(status.motionStatus?.cycle ?? status.task?.servoCycle ?? 0),
|
||||
motionQueueDepth: Number(status.motionStatus?.queueDepth ?? motion.queueDepth ?? 0),
|
||||
halChangedPinCount: Array.isArray(status.halSnapshot?.changedPins)
|
||||
? status.halSnapshot.changedPins.length
|
||||
: Number(status.halSnapshot?.changedPinCount || 0),
|
||||
activeLine,
|
||||
motionProgramLine,
|
||||
halProgramLine,
|
||||
activeLineSource: motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback",
|
||||
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0
|
||||
? motionProgramLine === halProgramLine
|
||||
: false,
|
||||
switchkinsType: Number(motion.switchkinsType ?? halPins["motion.switchkins-type"]?.value ?? 0),
|
||||
axisPose: {
|
||||
x: Number(axis.x ?? halPins["axis.0.pos-cmd"]?.value ?? 0),
|
||||
y: Number(axis.y ?? halPins["axis.1.pos-cmd"]?.value ?? 0),
|
||||
z: Number(axis.z ?? halPins["axis.2.pos-cmd"]?.value ?? 0),
|
||||
a: Number(axis.a ?? halPins["axis.3.pos-cmd"]?.value ?? 0),
|
||||
b: Number(axis.b ?? halPins["axis.4.pos-cmd"]?.value ?? 0),
|
||||
c: Number(axis.c ?? halPins["axis.5.pos-cmd"]?.value ?? 0),
|
||||
},
|
||||
axisPoseFrame: isJogMotion(motion) ? "task-local" : "work",
|
||||
currentVelocity: Number(motion.currentVel || motion.currentVelocity || 0) * 60,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function shouldUseSourceLineSegments(planSegments, lineSegments) {
|
||||
if (lineSegments.length < 3) return false;
|
||||
const plannedLines = new Set(planSegments.map((segment) => Number(segment.line)).filter(Number.isFinite));
|
||||
const lineCount = lineSegments.length;
|
||||
return plannedLines.size <= 2 && lineCount > plannedLines.size;
|
||||
}
|
||||
|
||||
function buildSourceLineMotionSegments({
|
||||
programLines = [],
|
||||
seedSegments = [],
|
||||
linearUnits = "mm",
|
||||
} = {}) {
|
||||
if (!Array.isArray(programLines) || programLines.length === 0) return [];
|
||||
const seedByLine = new Map(seedSegments.map((segment) => [Number(segment.line), segment]));
|
||||
const axes = normalizePlanAxes(seedSegments[0]?.startAxes || {});
|
||||
const segments = [];
|
||||
let elapsedSeconds = 0;
|
||||
let feedRate = firstPositive(seedSegments.map((segment) => segment.feedRate), 100);
|
||||
let rapidVelocity = firstPositive(
|
||||
seedSegments.filter((segment) => segment.motionClass === "rapid").map((segment) => segment.velocityMmPerMin),
|
||||
2100,
|
||||
);
|
||||
|
||||
for (let index = 0; index < programLines.length; index += 1) {
|
||||
const line = index + 1;
|
||||
const code = stripSourceLineComments(programLines[index]);
|
||||
if (!isExecutableGcodeLine(code)) continue;
|
||||
|
||||
const seed = seedByLine.get(line) || null;
|
||||
const startAxes = normalizePlanAxes(seed?.startAxes || axes);
|
||||
const parsed = parseGcodeLineMotion(code, startAxes, feedRate);
|
||||
if (parsed.feedRate > 0) feedRate = parsed.feedRate;
|
||||
const motionClass = seed?.motionClass || parsed.motionClass;
|
||||
const endAxes = normalizePlanAxes(seed?.endAxes || parsed.endAxes);
|
||||
const velocityMmPerMin = Number(seed?.velocityMmPerMin) > 0
|
||||
? Number(seed.velocityMmPerMin)
|
||||
: motionClass === "rapid"
|
||||
? rapidVelocity
|
||||
: Math.max(feedRate, 1);
|
||||
if (motionClass === "rapid" && velocityMmPerMin > 0) {
|
||||
rapidVelocity = velocityMmPerMin;
|
||||
}
|
||||
const durationSeconds = Math.max(
|
||||
Number(seed?.durationSeconds || 0),
|
||||
estimateLineDurationSeconds(startAxes, endAxes, velocityMmPerMin),
|
||||
0.05,
|
||||
);
|
||||
const segment = {
|
||||
line,
|
||||
type: seed?.type || parsed.type,
|
||||
motionClass,
|
||||
feedMode: seed?.feedMode || parsed.feedMode,
|
||||
startSeconds: elapsedSeconds,
|
||||
durationSeconds,
|
||||
elapsedSeconds: elapsedSeconds + durationSeconds,
|
||||
feedRate,
|
||||
linearUnits: seed?.linearUnits || linearUnits,
|
||||
velocityMmPerMin,
|
||||
requestedVelocityMmPerMin: Number(seed?.requestedVelocityMmPerMin || velocityMmPerMin),
|
||||
startAxes,
|
||||
endAxes,
|
||||
};
|
||||
segments.push(segment);
|
||||
Object.assign(axes, endAxes);
|
||||
elapsedSeconds += durationSeconds;
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
function parseGcodeLineMotion(code, startAxes, currentFeedRate) {
|
||||
const numberPattern = "[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)";
|
||||
const gCodes = [...code.matchAll(new RegExp(`\\bG\\s*(${numberPattern})\\b`, "gi"))].map((match) => Number(match[1]));
|
||||
const feedMode = gCodes.includes(93) ? "inverse-time" : "units-per-minute";
|
||||
const rapid = gCodes.includes(0);
|
||||
const feed = gCodes.some((value) => value === 1 || value === 2 || value === 3);
|
||||
const endAxes = { ...startAxes };
|
||||
for (const axis of ["x", "y", "z", "a", "b", "c", "u", "v", "w"]) {
|
||||
const match = code.match(new RegExp(`\\b${axis}\\s*(${numberPattern})`, "i"));
|
||||
if (match) endAxes[axis] = Number(match[1]);
|
||||
}
|
||||
const feedMatch = code.match(new RegExp(`\\bF\\s*(${numberPattern})`, "i"));
|
||||
const feedRate = feedMatch && Number(feedMatch[1]) > 0 ? Number(feedMatch[1]) : Number(currentFeedRate || 0);
|
||||
return {
|
||||
type: rapid ? "STRAIGHT_TRAVERSE" : "STRAIGHT_FEED",
|
||||
motionClass: rapid && !feed ? "rapid" : "feed",
|
||||
feedMode,
|
||||
feedRate,
|
||||
endAxes,
|
||||
};
|
||||
}
|
||||
|
||||
function stripSourceLineComments(line) {
|
||||
return String(line || "")
|
||||
.replace(/\([^)]*\)/g, " ")
|
||||
.replace(/;.*$/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isExecutableGcodeLine(code) {
|
||||
if (!code || code === "%") return false;
|
||||
return /\b[GMTXYZABCUVWF]\s*[-+]?\d/i.test(code);
|
||||
}
|
||||
|
||||
function estimateLineDurationSeconds(startAxes, endAxes, velocityMmPerMin) {
|
||||
const distance = Math.sqrt(["x", "y", "z"].reduce((total, axis) => {
|
||||
const delta = Number(endAxes[axis] || 0) - Number(startAxes[axis] || 0);
|
||||
return total + delta * delta;
|
||||
}, 0));
|
||||
if (distance <= 0 || velocityMmPerMin <= 0) return 0;
|
||||
return distance / Math.max(velocityMmPerMin / 60, 0.000001);
|
||||
}
|
||||
|
||||
function firstPositive(values, fallback) {
|
||||
for (const value of values) {
|
||||
const number = Number(value);
|
||||
if (Number.isFinite(number) && number > 0) return number;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizePlanAxes(axes = {}) {
|
||||
return Object.fromEntries(["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => [
|
||||
axis,
|
||||
Number.isFinite(Number(axes[axis])) ? Number(axes[axis]) : 0,
|
||||
]));
|
||||
}
|
||||
|
||||
function isJogMotion(motion = {}) {
|
||||
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
|
||||
}
|
||||
|
||||
function sessionFileDescriptor(file) {
|
||||
return {
|
||||
sourceRel: file.sourceRel,
|
||||
wasmPath: file.wasmPath || file.path,
|
||||
path: file.wasmPath || file.path,
|
||||
kind: file.kind,
|
||||
bytes: Number(file.bytes || String(file.text || "").length),
|
||||
text: file.text || "",
|
||||
};
|
||||
}
|
||||
71
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-task-hal-worker-client.js
vendored
Normal file
71
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-task-hal-worker-client.js
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
import { wrapTaskHalSdk } from "./linuxcnc-task-hal-runtime.js";
|
||||
|
||||
export async function createLinuxCncTaskHalWorkerRuntime({
|
||||
workerUrl = new URL("./linuxcnc-task-hal-worker.js", import.meta.url).href,
|
||||
sdkModuleUrl = null,
|
||||
moduleOptions = {},
|
||||
} = {}) {
|
||||
if (typeof Worker !== "function") {
|
||||
throw new Error("Worker is not available");
|
||||
}
|
||||
const worker = new Worker(workerUrl, { type: "module" });
|
||||
const client = createWorkerClient(worker);
|
||||
await client.call("init", { sdkModuleUrl, moduleOptions });
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-task-hal-worker-runtime",
|
||||
semanticBoundary: "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
executionContext: "worker",
|
||||
workerUrl,
|
||||
loaded: true,
|
||||
readiness: () => client.call("readiness"),
|
||||
initSession: (session) => client.call("initSession", { session }),
|
||||
stageFiles: (files) => client.call("stageFiles", { files }),
|
||||
openProgram: (path) => client.call("openProgram", { path }),
|
||||
loadProgramMotionPlan: (plan) => client.call("loadProgramMotionPlan", { plan }),
|
||||
sendCommand: (command) => client.call("command", { command }),
|
||||
runCycles: (options) => client.call("runCycles", { options }),
|
||||
readStatus: () => client.call("readStatus"),
|
||||
readEvents: () => client.call("readEvents"),
|
||||
resetSession: () => client.call("reset"),
|
||||
terminate: () => worker.terminate(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDirectTaskHalRuntimeFromSdk(sdk) {
|
||||
return wrapTaskHalSdk(sdk, { executionContext: "direct" });
|
||||
}
|
||||
|
||||
function createWorkerClient(worker) {
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
|
||||
worker.addEventListener("message", (event) => {
|
||||
const { id, ok, result, error } = event.data || {};
|
||||
const request = pending.get(id);
|
||||
if (!request) return;
|
||||
pending.delete(id);
|
||||
if (ok) {
|
||||
request.resolve(result);
|
||||
} else {
|
||||
request.reject(new Error(error || "LinuxCNC task/HAL worker failed"));
|
||||
}
|
||||
});
|
||||
|
||||
worker.addEventListener("error", (event) => {
|
||||
for (const request of pending.values()) {
|
||||
request.reject(new Error(event.message || "LinuxCNC task/HAL worker error"));
|
||||
}
|
||||
pending.clear();
|
||||
});
|
||||
|
||||
return {
|
||||
call(type, payload = {}) {
|
||||
const id = nextId;
|
||||
nextId += 1;
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
worker.postMessage({ id, type, payload });
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
63
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-task-hal-worker.js
vendored
Normal file
63
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/linuxcnc-task-hal-worker.js
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import { createLinuxCncTaskHalRuntime } from "./linuxcnc-task-hal-runtime.js";
|
||||
|
||||
let runtime = null;
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
const { id, type, payload = {} } = event.data || {};
|
||||
try {
|
||||
const result = await handleMessage(type, payload);
|
||||
self.postMessage({ id, ok: true, result });
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
id,
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function handleMessage(type, payload) {
|
||||
switch (type) {
|
||||
case "init":
|
||||
runtime = await createLinuxCncTaskHalRuntime(payload);
|
||||
return runtime.readiness();
|
||||
case "readiness":
|
||||
assertRuntime();
|
||||
return runtime.readiness();
|
||||
case "stageFiles":
|
||||
assertRuntime();
|
||||
return runtime.stageFiles(payload.files || []);
|
||||
case "initSession":
|
||||
assertRuntime();
|
||||
return runtime.initSession(payload.session || {});
|
||||
case "openProgram":
|
||||
assertRuntime();
|
||||
return runtime.openProgram(payload.path);
|
||||
case "loadProgramMotionPlan":
|
||||
assertRuntime();
|
||||
return runtime.loadProgramMotionPlan(payload.plan || {});
|
||||
case "command":
|
||||
assertRuntime();
|
||||
return runtime.sendCommand(payload.command);
|
||||
case "runCycles":
|
||||
assertRuntime();
|
||||
return runtime.runCycles(payload.options || {});
|
||||
case "readStatus":
|
||||
assertRuntime();
|
||||
return runtime.readStatus();
|
||||
case "readEvents":
|
||||
assertRuntime();
|
||||
return runtime.readEvents();
|
||||
case "reset":
|
||||
assertRuntime();
|
||||
return runtime.resetSession();
|
||||
default:
|
||||
throw new Error(`Unknown task/HAL worker message: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertRuntime() {
|
||||
if (!runtime) {
|
||||
throw new Error("LinuxCNC task/HAL worker runtime is not initialized");
|
||||
}
|
||||
}
|
||||
181
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/native-task-hal-audit.js
vendored
Normal file
181
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/native-task-hal-audit.js
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
const WEB_SIMULATION_BOUNDARY = "linuxcnc_task_motion_hal_wasm_simulation_runtime";
|
||||
const ALLOWED_NATIVE_PROBE_STATUSES = new Set([
|
||||
"passed",
|
||||
"ready_disabled_by_default",
|
||||
"skipped_missing_host_runtime",
|
||||
]);
|
||||
|
||||
export function createNativeTaskHalReadinessAudit({
|
||||
sourceManifest = {},
|
||||
nativeProbe = {},
|
||||
fullExecutionBoundary = {},
|
||||
taskHalRuntimeReadiness = {},
|
||||
taskHalStatus = {},
|
||||
generatedAt = new Date().toISOString(),
|
||||
artifactPaths = {},
|
||||
} = {}) {
|
||||
const taskSourceCount = numberFrom(sourceManifest.task_source_count);
|
||||
const halSourceCount = numberFrom(sourceManifest.hal_source_count);
|
||||
const motionSourceCount = numberFrom(sourceManifest.motion_source_count);
|
||||
const sourceManifestReady = truthy(sourceManifest.task_hal_source_manifest_ready)
|
||||
&& truthy(sourceManifest.task_hal_reference_source_ready)
|
||||
&& taskSourceCount > 0
|
||||
&& halSourceCount > 0
|
||||
&& motionSourceCount > 0;
|
||||
|
||||
const nativeProbeStatus = String(
|
||||
nativeProbe.native_probe_status
|
||||
|| nativeProbe.trt_task_hal_runtime_probe_status
|
||||
|| "missing",
|
||||
);
|
||||
const nativeProbeOk = String(nativeProbe.native_task_hal_probe || "") === "ok"
|
||||
&& ALLOWED_NATIVE_PROBE_STATUSES.has(nativeProbeStatus);
|
||||
const nativePromotionBlocked = !truthy(nativeProbe.trt_task_hal_promotion_allowed)
|
||||
&& String(nativeProbe.nativeTaskReady) !== "true"
|
||||
&& String(nativeProbe.nativeHalSyncReady) !== "true";
|
||||
|
||||
const taskReady = Boolean(
|
||||
fullExecutionBoundary.taskRuntimeReady
|
||||
|| taskHalRuntimeReadiness.taskRuntimeReady
|
||||
|| taskHalStatus.summary?.taskRuntimeReady,
|
||||
);
|
||||
const motionReady = Boolean(
|
||||
fullExecutionBoundary.motionRuntimeReady
|
||||
|| taskHalRuntimeReadiness.motionRuntimeReady
|
||||
|| taskHalStatus.summary?.motionRuntimeReady,
|
||||
);
|
||||
const halReady = Boolean(
|
||||
fullExecutionBoundary.halRuntimeReady
|
||||
|| taskHalRuntimeReadiness.halRuntimeReady
|
||||
|| taskHalStatus.summary?.halRuntimeReady,
|
||||
);
|
||||
const halSyncReady = Boolean(
|
||||
fullExecutionBoundary.halSyncReady
|
||||
|| taskHalRuntimeReadiness.halSyncReady
|
||||
|| taskHalStatus.summary?.halSyncReady,
|
||||
);
|
||||
|
||||
const fullBoundarySimulationReady = Boolean(
|
||||
fullExecutionBoundary.semanticBoundary === WEB_SIMULATION_BOUNDARY
|
||||
&& fullExecutionBoundary.fullLinuxCncProgramExecutionReady === true
|
||||
&& fullExecutionBoundary.promotionAllowed === true,
|
||||
);
|
||||
const hardwareBlocked = fullExecutionBoundary.hardwareDrive === false
|
||||
&& fullExecutionBoundary.hostRealtimeKernel === false
|
||||
&& fullExecutionBoundary.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
|
||||
&& taskReady
|
||||
&& motionReady
|
||||
&& halReady
|
||||
&& halSyncReady
|
||||
&& fullBoundarySimulationReady
|
||||
&& toolUserWebSimulationReady
|
||||
&& hardwareBlocked;
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-native-task-hal-readiness-audit",
|
||||
batch: "M18-native-task-hal-source-and-artifact-audit",
|
||||
generatedAt,
|
||||
status: webSimulationConsistent ? "ok" : "blocked",
|
||||
semanticBoundary: WEB_SIMULATION_BOUNDARY,
|
||||
promotionScope: "web_simulation_only",
|
||||
taskHalWebSimulationBoundaryConsistent: webSimulationConsistent,
|
||||
webSimulation: {
|
||||
promoted: fullBoundarySimulationReady,
|
||||
taskRuntimeReady: taskReady,
|
||||
motionRuntimeReady: motionReady,
|
||||
halRuntimeReady: halReady,
|
||||
nativeTaskReady: fullExecutionBoundary.nativeTaskReady === true,
|
||||
nativeHalSyncReady: fullExecutionBoundary.nativeHalSyncReady === true,
|
||||
fullLinuxCncProgramExecutionReady:
|
||||
fullExecutionBoundary.fullLinuxCncProgramExecutionReady === true,
|
||||
promotionAllowed: fullExecutionBoundary.promotionAllowed === true,
|
||||
},
|
||||
nativeHostAndHardware: {
|
||||
nativeProbe: nativeProbeOk ? "ok" : "blocked",
|
||||
nativeProbeStatus,
|
||||
nativePromotionAllowed: truthy(nativeProbe.trt_task_hal_promotion_allowed),
|
||||
hardwareDrive: false,
|
||||
hostRealtimeKernel: false,
|
||||
externalUserMProcessReady: false,
|
||||
toolDbProcessReady: false,
|
||||
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,
|
||||
taskSourceCount,
|
||||
halSourceCount,
|
||||
motionSourceCount,
|
||||
nmlSourceCount: numberFrom(sourceManifest.nml_source_count),
|
||||
libnmlSourceCount: numberFrom(sourceManifest.libnml_source_count),
|
||||
referenceSourceReady: truthy(sourceManifest.task_hal_reference_source_ready),
|
||||
vendorSourceReady: truthy(sourceManifest.task_hal_vendor_source_ready),
|
||||
vendorHashMatchReady: truthy(sourceManifest.task_hal_vendor_hash_match_ready),
|
||||
},
|
||||
gates: {
|
||||
task_hal_web_simulation_boundary_consistent: webSimulationConsistent ? 1 : 0,
|
||||
native_task_hal_host_probe_status: nativeProbeStatus,
|
||||
hardware_drive: 0,
|
||||
host_realtime_kernel: 0,
|
||||
external_user_m_process_ready: 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,
|
||||
blockers: webSimulationConsistent ? [] : buildBlockers({
|
||||
sourceManifestReady,
|
||||
nativeProbeOk,
|
||||
nativePromotionBlocked,
|
||||
taskReady,
|
||||
motionReady,
|
||||
halReady,
|
||||
halSyncReady,
|
||||
fullBoundarySimulationReady,
|
||||
toolUserWebSimulationReady,
|
||||
hardwareBlocked,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildBlockers(checks) {
|
||||
const blockers = [];
|
||||
if (!checks.sourceManifestReady) blockers.push("task/HAL source manifest proof is incomplete");
|
||||
if (!checks.nativeProbeOk) blockers.push("native host probe did not produce an accepted default status");
|
||||
if (!checks.nativePromotionBlocked) blockers.push("native probe unexpectedly allowed promotion");
|
||||
if (!checks.taskReady) blockers.push("task runtime is not ready in the Web simulation boundary");
|
||||
if (!checks.motionReady) blockers.push("motion runtime is not ready in the Web simulation boundary");
|
||||
if (!checks.halReady) blockers.push("HAL runtime is not ready in the Web simulation boundary");
|
||||
if (!checks.halSyncReady) blockers.push("task/motion/HAL sync is not ready in the Web simulation boundary");
|
||||
if (!checks.fullBoundarySimulationReady) blockers.push("full execution boundary is not promoted for Web simulation");
|
||||
if (!checks.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;
|
||||
}
|
||||
|
||||
function truthy(value) {
|
||||
return value === true || value === 1 || value === "1" || value === "true";
|
||||
}
|
||||
|
||||
function numberFrom(value) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : 0;
|
||||
}
|
||||
214
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/rtcp-frame.js
vendored
Normal file
214
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/rtcp-frame.js
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
import { xyzacTrtProfile } from "../profiles/xyzac-trt.js";
|
||||
|
||||
const DEG_TO_RAD = Math.PI / 180;
|
||||
|
||||
export function buildRtcpFrame({
|
||||
axisPose,
|
||||
activeLine,
|
||||
kinsType = "identity",
|
||||
rtcpEnabled = false,
|
||||
sourceMode = "fixture-ui-only",
|
||||
profile = xyzacTrtProfile,
|
||||
linuxCncKinematicsResult = null,
|
||||
}) {
|
||||
if (linuxCncKinematicsResult) {
|
||||
return buildLinuxCncKinematicsFrame({
|
||||
axisPose,
|
||||
activeLine,
|
||||
kinsType,
|
||||
rtcpEnabled,
|
||||
sourceMode,
|
||||
profile,
|
||||
linuxCncKinematicsResult,
|
||||
});
|
||||
}
|
||||
|
||||
const pose = normalizeAxisPose(axisPose);
|
||||
const toolLength = 84.019;
|
||||
const toolAxisVector = computeToolAxisVector(pose, profile);
|
||||
const compensation = rtcpEnabled
|
||||
? {
|
||||
x: -toolAxisVector.x * toolLength,
|
||||
y: -toolAxisVector.y * toolLength,
|
||||
z: -toolAxisVector.z * toolLength,
|
||||
}
|
||||
: { x: 0, y: 0, z: 0 };
|
||||
const tcpPose = {
|
||||
x: pose.x + compensation.x,
|
||||
y: pose.y + compensation.y,
|
||||
z: pose.z + compensation.z,
|
||||
a: pose.a,
|
||||
b: pose.b,
|
||||
c: pose.c,
|
||||
};
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-motion-frame",
|
||||
profileId: profile.id,
|
||||
sourceMode,
|
||||
semanticBoundary:
|
||||
sourceMode === "fixture-ui-only"
|
||||
? "fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof"
|
||||
: "linuxcnc_or_source_derived_runtime_required",
|
||||
activeLine,
|
||||
kinsType,
|
||||
rtcpEnabled,
|
||||
rtcpState: rtcpEnabled ? "on" : "off",
|
||||
axisPose: pose,
|
||||
jointPose: buildJointPose(pose, profile),
|
||||
tcpPose,
|
||||
toolAxisVector,
|
||||
compensation,
|
||||
toolLength,
|
||||
readiness: {
|
||||
frameReady: true,
|
||||
uiReady: true,
|
||||
profileReady: true,
|
||||
linuxCncKinematicsReady: false,
|
||||
promotionAllowed: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildLinuxCncKinematicsFrame({
|
||||
axisPose,
|
||||
activeLine,
|
||||
kinsType,
|
||||
rtcpEnabled,
|
||||
profile,
|
||||
linuxCncKinematicsResult,
|
||||
}) {
|
||||
const forward = linuxCncKinematicsResult.forward || {};
|
||||
const inverse = linuxCncKinematicsResult.inverse || {};
|
||||
const wasmPose = normalizeLinuxCncPose(forward.pose);
|
||||
const pose = normalizeAxisPose({
|
||||
...axisPose,
|
||||
...wasmPose,
|
||||
});
|
||||
const jointValues = Array.isArray(inverse.joints) ? inverse.joints : [];
|
||||
const toolAxisVector = computeToolAxisVector(pose, profile);
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-motion-frame",
|
||||
profileId: profile.id,
|
||||
sourceMode: "source-derived-kinematics-wasm",
|
||||
semanticBoundary: "linuxcnc_kinematics_wasm_c_abi",
|
||||
activeLine,
|
||||
kinsType,
|
||||
rtcpEnabled,
|
||||
rtcpState: rtcpEnabled ? "on" : "off",
|
||||
axisPose: pose,
|
||||
jointPose: buildJointPoseFromLinuxCncJoints(jointValues, pose, profile),
|
||||
tcpPose: {
|
||||
x: pose.x,
|
||||
y: pose.y,
|
||||
z: pose.z,
|
||||
a: pose.a,
|
||||
b: pose.b,
|
||||
c: pose.c,
|
||||
},
|
||||
toolAxisVector,
|
||||
compensation: { x: 0, y: 0, z: 0 },
|
||||
toolLength: 84.019,
|
||||
kinematicsModuleId: linuxCncKinematicsResult.moduleId,
|
||||
kinematicsSwitchkinsType: linuxCncKinematicsResult.switchkinsType,
|
||||
kinematicsForwardRc: forward.rc,
|
||||
kinematicsInverseRc: inverse.rc,
|
||||
kinematicsFlags: {
|
||||
fflags: forward.fflags,
|
||||
iflags: forward.iflags,
|
||||
inverseFflags: inverse.fflags,
|
||||
inverseIflags: inverse.iflags,
|
||||
},
|
||||
readiness: {
|
||||
frameReady: true,
|
||||
uiReady: true,
|
||||
profileReady: true,
|
||||
linuxCncKinematicsReady: forward.rc === 0 && inverse.rc === 0,
|
||||
promotionAllowed: forward.rc === 0 && inverse.rc === 0,
|
||||
fullLinuxCncProgramExecutionReady: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAxisPose(axisPose) {
|
||||
return {
|
||||
x: Number(axisPose?.x ?? 0),
|
||||
y: Number(axisPose?.y ?? 0),
|
||||
z: Number(axisPose?.z ?? 0),
|
||||
a: Number(axisPose?.a ?? 0),
|
||||
b: Number(axisPose?.b ?? 0),
|
||||
c: Number(axisPose?.c ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
function buildJointPose(pose, profile = xyzacTrtProfile) {
|
||||
return axesForProfile(profile).map((axis, joint) => ({
|
||||
joint,
|
||||
axis,
|
||||
value: pose[axis.toLowerCase()] ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
function buildJointPoseFromLinuxCncJoints(joints, fallbackPose, profile = xyzacTrtProfile) {
|
||||
const axes = axesForProfile(profile);
|
||||
const fallbackValues = axes.map((axis) => fallbackPose[axis.toLowerCase()] ?? 0);
|
||||
return axes.map((axis, joint) => ({
|
||||
joint,
|
||||
axis,
|
||||
value: Number(joints[joint] ?? fallbackValues[joint] ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
function axesForProfile(profile = xyzacTrtProfile) {
|
||||
const coordinates = profile?.coordinates?.length
|
||||
? profile.coordinates
|
||||
: String(profile?.traj?.coordinates || "XYZAC").split("");
|
||||
return coordinates.map((axis) => String(axis).toUpperCase()).filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeLinuxCncPose(pose = {}) {
|
||||
return {
|
||||
x: Number(pose.x ?? pose.tran?.x ?? 0),
|
||||
y: Number(pose.y ?? pose.tran?.y ?? 0),
|
||||
z: Number(pose.z ?? pose.tran?.z ?? 0),
|
||||
a: Number(pose.a ?? 0),
|
||||
b: Number(pose.b ?? 0),
|
||||
c: Number(pose.c ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
function computeToolAxisVector(pose, profile = xyzacTrtProfile) {
|
||||
const coordinates = profile?.traj?.coordinates || "XYZAC";
|
||||
const tiltDegrees = coordinates.includes("B") ? pose.b : pose.a;
|
||||
const cDegrees = pose.c;
|
||||
const tilt = tiltDegrees * DEG_TO_RAD;
|
||||
const c = cDegrees * DEG_TO_RAD;
|
||||
const sinTilt = Math.sin(tilt);
|
||||
const cosTilt = Math.cos(tilt);
|
||||
const sinC = Math.sin(c);
|
||||
const cosC = Math.cos(c);
|
||||
|
||||
if (coordinates.includes("B")) {
|
||||
return normalizeVector({
|
||||
x: sinTilt * cosC,
|
||||
y: sinTilt * sinC,
|
||||
z: cosTilt,
|
||||
});
|
||||
}
|
||||
|
||||
return normalizeVector({
|
||||
x: sinTilt * sinC,
|
||||
y: -sinTilt * cosC,
|
||||
z: cosTilt,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeVector(vector) {
|
||||
const length = Math.hypot(vector.x, vector.y, vector.z) || 1;
|
||||
return {
|
||||
x: vector.x / length,
|
||||
y: vector.y / length,
|
||||
z: vector.z / length,
|
||||
};
|
||||
}
|
||||
1093
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/tool-db-simulation.js
vendored
Normal file
1093
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/tool-db-simulation.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
111
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/vismach-model-state.js
vendored
Normal file
111
web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/runtime/vismach-model-state.js
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
const DEFAULT_LINEAR_UNITS = "mm";
|
||||
|
||||
export const XYZBC_TRT_VISMACH_PINS = [
|
||||
"table-x",
|
||||
"saddle-y",
|
||||
"spindle-z",
|
||||
"tilt-b",
|
||||
"rotate-c",
|
||||
"tool-offset",
|
||||
"x-offset",
|
||||
"z-offset",
|
||||
];
|
||||
|
||||
export function buildVismachModelState(state = {}) {
|
||||
const profile = state.profile || {};
|
||||
const axisPose = resolveAxisPose(state);
|
||||
const offsets = profile.offsets || {};
|
||||
const toolOffset = resolveToolOffset(state);
|
||||
const xOffset = finiteNumber(offsets.x, -20);
|
||||
const zOffset = finiteNumber(offsets.z, -15);
|
||||
const linearUnits = resolveLinearUnits(state);
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-vismach-model-state",
|
||||
profileId: profile.id || state.machineProfile || null,
|
||||
sourceGui: "src/hal/user_comps/vismach/xyzbc-trt-gui.py",
|
||||
webModel: "app/src/visualization/five-axis-scene.js",
|
||||
linearUnits,
|
||||
pins: {
|
||||
"table-x": finiteNumber(axisPose.x, 0),
|
||||
"saddle-y": finiteNumber(axisPose.y, 0),
|
||||
"spindle-z": finiteNumber(axisPose.z, 0),
|
||||
"tilt-b": finiteNumber(axisPose.b, 0),
|
||||
"rotate-c": finiteNumber(axisPose.c, 0),
|
||||
"tool-offset": toolOffset,
|
||||
"x-offset": xOffset,
|
||||
"z-offset": zOffset,
|
||||
},
|
||||
transforms: {
|
||||
table: {
|
||||
translate: { x: finiteNumber(axisPose.x, 0), y: 0, z: 0 },
|
||||
sourcePins: ["table-x"],
|
||||
},
|
||||
saddle: {
|
||||
translate: { x: 0, y: finiteNumber(axisPose.y, 0), z: 0 },
|
||||
sourcePins: ["saddle-y"],
|
||||
},
|
||||
spindle: {
|
||||
translate: { x: 0, y: 0, z: finiteNumber(axisPose.z, 0) },
|
||||
sourcePins: ["spindle-z"],
|
||||
},
|
||||
tilt: {
|
||||
rotateDeg: { x: 0, y: finiteNumber(axisPose.b, 0), z: 0 },
|
||||
sourcePins: ["tilt-b"],
|
||||
},
|
||||
rotary: {
|
||||
rotateDeg: { x: 0, y: 0, z: finiteNumber(axisPose.c, 0) },
|
||||
sourcePins: ["rotate-c"],
|
||||
},
|
||||
tool: {
|
||||
translate: { x: xOffset, y: 0, z: zOffset - toolOffset },
|
||||
sourcePins: ["tool-offset", "x-offset", "z-offset"],
|
||||
},
|
||||
},
|
||||
halNets: [
|
||||
{ signal: "table-x", source: "joint.0.pos-fb", target: "xyzbc-trt-gui.table-x" },
|
||||
{ signal: "saddle-y", source: "joint.1.pos-fb", target: "xyzbc-trt-gui.saddle-y" },
|
||||
{ signal: "spindle-z", source: "joint.2.pos-fb", target: "xyzbc-trt-gui.spindle-z" },
|
||||
{ signal: "tilt-b", source: "joint.3.pos-fb", target: "xyzbc-trt-gui.tilt-b" },
|
||||
{ signal: "rotate-c", source: "joint.4.pos-fb", target: "xyzbc-trt-gui.rotate-c" },
|
||||
{ signal: "tool-offset", source: "motion.tooloffset.z", target: "xyzbc-trt-kins.tool-offset" },
|
||||
{ signal: "tool-offset", source: "xyzbc-trt-kins.tool-offset", target: "xyzbc-trt-gui.tool-offset" },
|
||||
{ signal: "x-offset", source: "xyzbc-trt-kins.x-offset", target: "xyzbc-trt-gui.x-offset" },
|
||||
{ signal: "z-offset", source: "xyzbc-trt-kins.z-offset", target: "xyzbc-trt-gui.z-offset" },
|
||||
],
|
||||
clearTraceSignal: "pyvcp.vismach-clear => vismach.plotclear",
|
||||
semanticBoundary: "web_threejs_vismach_equivalent_driven_by_xyzbc_trt_hal_pins",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAxisPose(state) {
|
||||
if (state.programRuntimeFeedback?.axisPose) return state.programRuntimeFeedback.axisPose;
|
||||
if (state.taskHalStatus?.ui?.axisPose) return state.taskHalStatus.ui.axisPose;
|
||||
return state.axisPose || {};
|
||||
}
|
||||
|
||||
function resolveToolOffset(state) {
|
||||
if (Number.isFinite(Number(state.toolRuntimeState?.vismach?.toolOffset))) {
|
||||
return Number(state.toolRuntimeState.vismach.toolOffset);
|
||||
}
|
||||
return finiteNumber(
|
||||
state.toolDbSimulation?.activeToolOffset?.offset?.z,
|
||||
state.toolPreview?.length,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveLinearUnits(state) {
|
||||
return state.linuxCncIniConfig?.traj?.linearUnits
|
||||
|| state.programRuntimeFeedback?.linearUnits
|
||||
|| state.programExecutionTiming?.linearUnits
|
||||
|| DEFAULT_LINEAR_UNITS;
|
||||
}
|
||||
|
||||
function finiteNumber(...values) {
|
||||
for (const value of values) {
|
||||
const number = Number(value);
|
||||
if (Number.isFinite(number)) return number;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user