fix: verify run path with 50ms screenshots
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { createLinuxCncInterpreterRuntime } from "./linuxcnc-interpreter-runtime.js";
|
||||
|
||||
installTextDecoderResizableArrayBufferCompat();
|
||||
|
||||
let runtime = null;
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
@@ -47,3 +49,28 @@ self.addEventListener("message", async (event) => {
|
||||
function postSuccess(id, value) {
|
||||
self.postMessage({ id, ok: true, value });
|
||||
}
|
||||
|
||||
function installTextDecoderResizableArrayBufferCompat() {
|
||||
const decoderPrototype = globalThis.TextDecoder?.prototype;
|
||||
if (!decoderPrototype || decoderPrototype.__webRtcpResizableArrayBufferCompat) return;
|
||||
const nativeDecode = decoderPrototype.decode;
|
||||
Object.defineProperty(decoderPrototype, "__webRtcpResizableArrayBufferCompat", {
|
||||
value: true,
|
||||
configurable: false,
|
||||
});
|
||||
decoderPrototype.decode = function decodeResizableArrayBufferCompat(input, options) {
|
||||
try {
|
||||
return nativeDecode.call(this, input, options);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("resizable")) throw error;
|
||||
if (input?.buffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(input), options);
|
||||
}
|
||||
if (input instanceof ArrayBuffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(new Uint8Array(input)), options);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createLinuxCncKinematicsRuntime } from "./linuxcnc-kinematics-runtime.js";
|
||||
|
||||
installTextDecoderResizableArrayBufferCompat();
|
||||
|
||||
let runtime = null;
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
@@ -64,3 +66,28 @@ self.addEventListener("message", async (event) => {
|
||||
function postSuccess(id, value) {
|
||||
self.postMessage({ id, ok: true, value });
|
||||
}
|
||||
|
||||
function installTextDecoderResizableArrayBufferCompat() {
|
||||
const decoderPrototype = globalThis.TextDecoder?.prototype;
|
||||
if (!decoderPrototype || decoderPrototype.__webRtcpResizableArrayBufferCompat) return;
|
||||
const nativeDecode = decoderPrototype.decode;
|
||||
Object.defineProperty(decoderPrototype, "__webRtcpResizableArrayBufferCompat", {
|
||||
value: true,
|
||||
configurable: false,
|
||||
});
|
||||
decoderPrototype.decode = function decodeResizableArrayBufferCompat(input, options) {
|
||||
try {
|
||||
return nativeDecode.call(this, input, options);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("resizable")) throw error;
|
||||
if (input?.buffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(input), options);
|
||||
}
|
||||
if (input instanceof ArrayBuffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(new Uint8Array(input)), options);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createLinuxCncTaskHalRuntime } from "./linuxcnc-task-hal-runtime.js";
|
||||
|
||||
installTextDecoderResizableArrayBufferCompat();
|
||||
|
||||
let runtime = null;
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
@@ -61,3 +63,28 @@ function assertRuntime() {
|
||||
throw new Error("LinuxCNC task/HAL worker runtime is not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
function installTextDecoderResizableArrayBufferCompat() {
|
||||
const decoderPrototype = globalThis.TextDecoder?.prototype;
|
||||
if (!decoderPrototype || decoderPrototype.__webRtcpResizableArrayBufferCompat) return;
|
||||
const nativeDecode = decoderPrototype.decode;
|
||||
Object.defineProperty(decoderPrototype, "__webRtcpResizableArrayBufferCompat", {
|
||||
value: true,
|
||||
configurable: false,
|
||||
});
|
||||
decoderPrototype.decode = function decodeResizableArrayBufferCompat(input, options) {
|
||||
try {
|
||||
return nativeDecode.call(this, input, options);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("resizable")) throw error;
|
||||
if (input?.buffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(input), options);
|
||||
}
|
||||
if (input instanceof ArrayBuffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(new Uint8Array(input)), options);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,13 +139,16 @@ export function gateLinuxCncTaskAction(state, action) {
|
||||
}
|
||||
return allow(status);
|
||||
case "RUN_MDI":
|
||||
if (status.taskState !== "on") return block(status, "MDI blocked: machine must be on");
|
||||
if (status.taskMode !== "mdi") return block(status, "MDI blocked: switch to MDI mode first");
|
||||
if (!status.allHomed && !status.noForceHoming) return block(status, "MDI blocked: home machine first");
|
||||
if (status.interpState === "reading" || status.interpState === "waiting") {
|
||||
return block(status, "MDI blocked: interpreter must be idle");
|
||||
{
|
||||
const manualTouchOff = isManualTouchOffMdiAction(status, action);
|
||||
if (status.taskState !== "on") return block(status, "MDI blocked: machine must be on");
|
||||
if (status.taskMode !== "mdi" && !manualTouchOff) return block(status, "MDI blocked: switch to MDI mode first");
|
||||
if (!status.allHomed && !status.noForceHoming) return block(status, "MDI blocked: home machine first");
|
||||
if (status.interpState === "reading" || status.interpState === "waiting") {
|
||||
return block(status, "MDI blocked: interpreter must be idle");
|
||||
}
|
||||
return allow(status);
|
||||
}
|
||||
return allow(status);
|
||||
case "RUN":
|
||||
case "STEP":
|
||||
case "RUN_FRAME":
|
||||
@@ -191,6 +194,17 @@ export function gateLinuxCncTaskAction(state, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function isManualTouchOffMdiAction(status, action) {
|
||||
if (!action || action.manualTouchOff !== true || status.taskMode !== "manual") return false;
|
||||
const command = String(action.command || "")
|
||||
.replace(/\([^)]*\)/g, " ")
|
||||
.replace(/;.*$/g, " ")
|
||||
.trim()
|
||||
.replace(/\s+/g, " ")
|
||||
.toUpperCase();
|
||||
return /^G10\s+L20\s+P0\s+[XYZABC]-?\d/.test(command) || /^G43(?:\s|$)/.test(command);
|
||||
}
|
||||
|
||||
function gateGmoccapyXyzabRun(status) {
|
||||
if (!status.iniLoaded) return block(status, "run blocked: LinuxCNC INI not loaded");
|
||||
if (!status.machineFileStaged) return block(status, "run blocked: LinuxCNC machine files not staged");
|
||||
|
||||
@@ -487,6 +487,25 @@ export function createSimulationStore(seed = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
const waitForStatePolling = (predicate, timeoutMs = 10000, intervalMs = 25) => {
|
||||
if (predicate(state)) return Promise.resolve(state);
|
||||
return new Promise((resolve, reject) => {
|
||||
const startedAt = Date.now();
|
||||
const tick = () => {
|
||||
if (predicate(state)) {
|
||||
resolve(state);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - startedAt > timeoutMs) {
|
||||
reject(new Error("timed out waiting for store state"));
|
||||
return;
|
||||
}
|
||||
setTimeout(tick, intervalMs);
|
||||
};
|
||||
setTimeout(tick, intervalMs);
|
||||
});
|
||||
};
|
||||
|
||||
const dispatch = (action) => {
|
||||
switch (action.type) {
|
||||
case "BOOT_READY":
|
||||
@@ -1161,6 +1180,13 @@ export function createSimulationStore(seed = {}) {
|
||||
});
|
||||
});
|
||||
break;
|
||||
case "RUN_FROM_OPERATOR":
|
||||
return operatorRunSequence().catch((error) => {
|
||||
dispatch({
|
||||
type: "TASK_HAL_COMMAND_FAILED",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
case "SET_FRAME_SOURCE":
|
||||
setState({
|
||||
sourceMode: action.sourceMode,
|
||||
@@ -1503,27 +1529,33 @@ export function createSimulationStore(seed = {}) {
|
||||
break;
|
||||
case "RUN_MDI":
|
||||
{
|
||||
const gate = gateLinuxCncTaskAction(state, action);
|
||||
const command = normalizeMdiCommand(action.command ?? state.machine.mdiCommand);
|
||||
const gate = gateLinuxCncTaskAction(state, { ...action, command });
|
||||
if (!gate.allowed) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
const preserveManualTouchOff = action.manualTouchOff === true && state.machine.mode === "manual";
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
const command = normalizeMdiCommand(action.command ?? state.machine.mdiCommand);
|
||||
const mdiResult = executeMdiCommand(state, command);
|
||||
setState(mdiResult.patch);
|
||||
const mdiPatch = preserveManualTouchOff
|
||||
? createManualTouchOffMdiPatch(state, mdiResult.patch, command)
|
||||
: mdiResult.patch;
|
||||
setState(mdiPatch);
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_MODE", mode: "MDI" },
|
||||
{ type: "EMC_TASK_PLAN_EXECUTE", mdi: command },
|
||||
], {
|
||||
operatorMessage: `task/HAL MDI ${command}`,
|
||||
preserveAxisPose: mdiResult.patch.axisPose,
|
||||
preserveMachine: mdiResult.patch.machine,
|
||||
preserveAxisPose: mdiPatch.axisPose,
|
||||
preserveMachine: mdiPatch.machine,
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const mdiResult = executeMdiCommand(state, action.command ?? state.machine.mdiCommand);
|
||||
setState(mdiResult.patch);
|
||||
const mdiResult = executeMdiCommand(state, command);
|
||||
setState(preserveManualTouchOff
|
||||
? createManualTouchOffMdiPatch(state, mdiResult.patch, command)
|
||||
: mdiResult.patch);
|
||||
}
|
||||
break;
|
||||
case "LOAD_PROGRAM":
|
||||
@@ -2480,16 +2512,121 @@ export function createSimulationStore(seed = {}) {
|
||||
return state;
|
||||
};
|
||||
|
||||
const operatorRunSequence = async () => {
|
||||
if (!state.machineFileStaging?.selectedGcodeSourceRel) {
|
||||
const sourceRel = defaultLinuxCncGcodeSourceForState(state)?.sourceRel;
|
||||
if (sourceRel) {
|
||||
dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
|
||||
await waitForStatePredicate((nextState) => nextState.machineFileStaging?.selectedGcodeSourceRel === sourceRel);
|
||||
}
|
||||
}
|
||||
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
if (
|
||||
!Array.isArray(state.programExecution?.motion)
|
||||
|| state.programExecution.motion.length === 0
|
||||
|| state.interpreterExecutionPending
|
||||
) {
|
||||
if (!state.interpreterExecutionPending) {
|
||||
dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
|
||||
}
|
||||
await waitForStatePolling((nextState) => (
|
||||
nextState.interpreterExecutionPending === false
|
||||
&& Array.isArray(nextState.programExecution?.motion)
|
||||
&& nextState.programExecution.motion.length > 0
|
||||
), 15000);
|
||||
}
|
||||
if (tcpKinsType && state.kinsType !== tcpKinsType) {
|
||||
dispatch({ type: "SET_KINS_TYPE", kinsType: tcpKinsType });
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: true,
|
||||
estopActive: false,
|
||||
taskState: "on",
|
||||
mode: "auto",
|
||||
manualPanel: null,
|
||||
allHomed: true,
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
runState: "idle",
|
||||
operatorMessage: "RUN ready: motion plan source ready; preparing task/HAL session",
|
||||
});
|
||||
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
|
||||
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
|
||||
setState({ operatorMessage: "RUN preparing: initializing task/HAL session" });
|
||||
await initializeTaskHalSession({ openProgram: true });
|
||||
}
|
||||
setState({ operatorMessage: "RUN preparing: loading task/HAL motion plan" });
|
||||
const loadedMotionPlan = await loadTaskHalMotionPlanWithSessionRetry();
|
||||
if (!loadedMotionPlan) {
|
||||
setState({ operatorMessage: "run blocked: task/HAL feed motion plan not loaded" });
|
||||
return state;
|
||||
}
|
||||
const ready = validateRunPreconditions(state, { requireTaskHalSession: true });
|
||||
if (!ready.ok) {
|
||||
setState({ operatorMessage: ready.operatorMessage });
|
||||
return state;
|
||||
}
|
||||
stopTaskHalStatusLoop("restarted", {
|
||||
operatorMessage: "task/HAL status loop restarting",
|
||||
});
|
||||
setState({
|
||||
activeLine: state.programStartLine || 1,
|
||||
programExecutionMotionIndex: 0,
|
||||
programExecutionSampleIndex: 0,
|
||||
programRuntimeFeedback: null,
|
||||
programLineExecution: {},
|
||||
operatorMessage: "RUN executing: sending task/HAL PLAN_RUN",
|
||||
});
|
||||
const status = await runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_STATE", state: "ON" },
|
||||
{ type: "EMC_JOINT_HOME", joint: -1 },
|
||||
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
|
||||
{ type: "EMC_TASK_PLAN_RUN", line: 0 },
|
||||
], {
|
||||
taskCycles: 5,
|
||||
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
|
||||
allowFixtureSession: false,
|
||||
});
|
||||
if (shouldContinueTaskHalStatusLoop(state, status)) {
|
||||
startTaskHalStatusLoop({
|
||||
profileId: ready.profileId,
|
||||
iniPath: ready.iniPath,
|
||||
kinematicsModuleId: ready.kinematicsModuleId,
|
||||
operatorMessage: `task/HAL status loop running ${ready.profileId} ${ready.kinematicsModuleId}`,
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
const policy = createLinuxCncTaskPolicyStatus(state);
|
||||
if (policy.taskMode !== "auto" || !policy.allHomed || policy.taskState !== "on") {
|
||||
await runReadySequence();
|
||||
}
|
||||
dispatch({ type: "RUN" });
|
||||
return state;
|
||||
};
|
||||
|
||||
const loadTaskHalMotionPlanForSession = async (session = state.taskHalSession) => {
|
||||
if (!state.taskHalRuntime?.loaded || typeof state.taskHalRuntime.loadProgramMotionPlan !== "function") {
|
||||
return null;
|
||||
}
|
||||
setState({ operatorMessage: "RUN preparing: building task/HAL motion plan" });
|
||||
const motion = state.programExecution?.motion || [];
|
||||
const timing = state.programExecutionTiming || buildTimingForState(state, state.programExecution);
|
||||
if (!session?.programPath || !Array.isArray(motion) || motion.length === 0 || !Array.isArray(timing?.segments) || timing.segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const plan = buildTaskHalProgramMotionPlan({
|
||||
const previewPlan = buildTaskHalProgramMotionPlanFromPreviewPath({
|
||||
programPath: session.programPath,
|
||||
path: state.programAxisPreviewPath,
|
||||
profile: state.profile,
|
||||
linearUnits: timing.linearUnits || state.profile?.traj?.linearUnits || "mm",
|
||||
});
|
||||
const plan = previewPlan || buildTaskHalProgramMotionPlan({
|
||||
programPath: session.programPath,
|
||||
motion,
|
||||
timing,
|
||||
@@ -2499,10 +2636,27 @@ export function createSimulationStore(seed = {}) {
|
||||
if (plan.segmentCount <= 0) {
|
||||
return null;
|
||||
}
|
||||
await state.taskHalRuntime.loadProgramMotionPlan(plan);
|
||||
setState({ operatorMessage: `RUN preparing: worker loading ${plan.segmentCount} task/HAL motion segments` });
|
||||
await withTimeout(
|
||||
state.taskHalRuntime.loadProgramMotionPlan(plan),
|
||||
5000,
|
||||
"task/HAL feed motion plan load timed out",
|
||||
);
|
||||
return plan;
|
||||
};
|
||||
|
||||
const loadTaskHalMotionPlanWithSessionRetry = async () => {
|
||||
let plan = null;
|
||||
try {
|
||||
plan = await loadTaskHalMotionPlanForSession(state.taskHalSession);
|
||||
} catch (error) {
|
||||
setState({ operatorMessage: `task/HAL motion plan reload: ${error instanceof Error ? error.message : String(error)}` });
|
||||
}
|
||||
if (plan) return plan;
|
||||
await initializeTaskHalSession({ openProgram: true });
|
||||
return loadTaskHalMotionPlanForSession(state.taskHalSession);
|
||||
};
|
||||
|
||||
const startTaskHalStatusLoop = ({
|
||||
profileId = state.machineProfile,
|
||||
iniPath = state.profile?.iniPath || null,
|
||||
@@ -3292,6 +3446,21 @@ function sourceBasename(path) {
|
||||
return String(path || "").split("/").filter(Boolean).at(-1) || "";
|
||||
}
|
||||
|
||||
function withTimeout(promise, timeoutMs, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
|
||||
Promise.resolve(promise)
|
||||
.then((value) => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(value);
|
||||
})
|
||||
.catch((error) => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isAsyncKinematicsRuntime(runtime) {
|
||||
return runtime?.executionContext === "worker";
|
||||
}
|
||||
@@ -3493,19 +3662,54 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
|
||||
? "jogging"
|
||||
: "idle";
|
||||
const runtimeFeedback = createTaskHalRuntimeFeedback(state, status, axisPose, activeLine);
|
||||
const taskHalSampleIndex = resolveRuntimeSampleIndexForLine(state, activeLine, runtimeFeedback.sampleIndex);
|
||||
const taskHalPlaybackPatch = applyProgramPlaybackUiPatch(state, {
|
||||
activeLine,
|
||||
axisPose,
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
motionIndex: runtimeFeedback.motionIndex,
|
||||
const shouldTrackProgramPlayback = runState === "running"
|
||||
|| runState === "mdi"
|
||||
|| (runState === "complete" && (state.runState === "running" || state.runState === "mdi" || state.runState === "complete"));
|
||||
const taskHalSampleIndex = shouldTrackProgramPlayback
|
||||
? resolveRuntimeSampleIndexForTaskHalPose(state, axisPose, activeLine, runtimeFeedback.sampleIndex)
|
||||
: clampNumber(
|
||||
state.programExecutionSampleIndex || 0,
|
||||
0,
|
||||
Math.max(Number(state.programAxisPreviewPath?.samples?.length || state.programAxisPreviewPath?.sampleCount || 1) - 1, 0),
|
||||
);
|
||||
const idleRuntimeFeedback = {
|
||||
...runtimeFeedback,
|
||||
sampleIndex: taskHalSampleIndex,
|
||||
runtimeFeedback: {
|
||||
...runtimeFeedback,
|
||||
motionIndex: Number(state.programExecutionMotionIndex || runtimeFeedback.motionIndex || 0),
|
||||
};
|
||||
const taskHalPlaybackPatch = shouldTrackProgramPlayback
|
||||
? applyProgramPlaybackUiPatch(state, {
|
||||
activeLine,
|
||||
axisPose,
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
motionIndex: runtimeFeedback.motionIndex,
|
||||
sampleIndex: taskHalSampleIndex,
|
||||
},
|
||||
});
|
||||
runtimeFeedback: {
|
||||
...runtimeFeedback,
|
||||
sampleIndex: taskHalSampleIndex,
|
||||
},
|
||||
preferRuntimeAxisPose: true,
|
||||
})
|
||||
: {
|
||||
activeLine: state.activeLine,
|
||||
axisPose,
|
||||
kinsType: kinsType || state.kinsType,
|
||||
rtcpState: kinsType ? rtcpStateFromKinsType(kinsType) : state.rtcpState,
|
||||
toolAxisVector: state.toolAxisVector,
|
||||
programExecutionMotionIndex: Number(state.programExecutionMotionIndex || 0),
|
||||
programExecutionSampleIndex: taskHalSampleIndex,
|
||||
programRuntimeFeedback: idleRuntimeFeedback,
|
||||
programUiExecution: state.programUiExecution
|
||||
? createProgramUiExecution({
|
||||
...state,
|
||||
axisPose,
|
||||
kinsType: kinsType || state.kinsType,
|
||||
programExecutionSampleIndex: taskHalSampleIndex,
|
||||
programRuntimeFeedback: idleRuntimeFeedback,
|
||||
}, { preferRuntimePose: true })
|
||||
: state.programUiExecution,
|
||||
};
|
||||
const loopActive = state.taskHalStatusLoop?.active === true
|
||||
&& loopSequence !== null
|
||||
&& Number(state.taskHalStatusLoop.sequence) === Number(loopSequence)
|
||||
@@ -3720,6 +3924,84 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildTaskHalProgramMotionPlanFromPreviewPath({
|
||||
programPath = null,
|
||||
path = null,
|
||||
profile = defaultProfile,
|
||||
linearUnits = "mm",
|
||||
} = {}) {
|
||||
const samples = path?.samples;
|
||||
if (!Array.isArray(samples) || samples.length < 2) return null;
|
||||
const samplePeriodSeconds = Math.max(Number(path.samplePeriodMs || 50) / 1000, 0.001);
|
||||
const segments = [];
|
||||
for (let index = 1; index < samples.length; index += 1) {
|
||||
const startSample = samples[index - 1];
|
||||
const endSample = samples[index];
|
||||
const startAxes = taskHalPlanAxesFromPose(axisPoseFromProgramPathSample(startSample, initialAxisPose, profile));
|
||||
const endAxes = taskHalPlanAxesFromPose(axisPoseFromProgramPathSample(endSample, startAxes, profile));
|
||||
const startSeconds = Number.isFinite(Number(startSample?.timeMs))
|
||||
? Number(startSample.timeMs) / 1000
|
||||
: (index - 1) * samplePeriodSeconds;
|
||||
const endSeconds = Number.isFinite(Number(endSample?.timeMs))
|
||||
? Number(endSample.timeMs) / 1000
|
||||
: index * samplePeriodSeconds;
|
||||
const durationSeconds = Math.max(endSeconds - startSeconds, samplePeriodSeconds);
|
||||
const velocityMmPerMin = estimatePreviewSegmentVelocityMmPerMin(startAxes, endAxes, durationSeconds);
|
||||
const type = endSample?.motionType || startSample?.motionType || "STRAIGHT_FEED";
|
||||
segments.push({
|
||||
line: Number(endSample?.line || startSample?.line || 1),
|
||||
type,
|
||||
motionClass: String(type).includes("TRAVERSE") ? "rapid" : "feed",
|
||||
feedMode: "units-per-minute",
|
||||
startSeconds,
|
||||
durationSeconds,
|
||||
elapsedSeconds: startSeconds + durationSeconds,
|
||||
feedRate: velocityMmPerMin,
|
||||
linearUnits,
|
||||
velocityMmPerMin,
|
||||
requestedVelocityMmPerMin: velocityMmPerMin,
|
||||
startAxes,
|
||||
endAxes,
|
||||
});
|
||||
}
|
||||
if (segments.length === 0) return null;
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-program-motion-plan",
|
||||
semanticBoundary: "linuxcnc_task_hal_plan_from_expanded_axis_preview_samples",
|
||||
programPath,
|
||||
linearUnits,
|
||||
totalSeconds: segments[segments.length - 1].elapsedSeconds,
|
||||
segmentCount: segments.length,
|
||||
segments,
|
||||
};
|
||||
}
|
||||
|
||||
function taskHalPlanAxesFromPose(pose = {}) {
|
||||
return {
|
||||
x: Number(pose.x || 0),
|
||||
y: Number(pose.y || 0),
|
||||
z: Number(pose.z || 0),
|
||||
a: Number(pose.a || 0),
|
||||
b: Number(pose.b || 0),
|
||||
c: Number(pose.c || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function estimatePreviewSegmentVelocityMmPerMin(startAxes = {}, endAxes = {}, durationSeconds = 0.05) {
|
||||
const linearDistance = Math.hypot(
|
||||
Number(endAxes.x || 0) - Number(startAxes.x || 0),
|
||||
Number(endAxes.y || 0) - Number(startAxes.y || 0),
|
||||
Number(endAxes.z || 0) - Number(startAxes.z || 0),
|
||||
);
|
||||
const rotaryDistance = Math.hypot(
|
||||
Number(endAxes.a || 0) - Number(startAxes.a || 0),
|
||||
Number(endAxes.b || 0) - Number(startAxes.b || 0),
|
||||
Number(endAxes.c || 0) - Number(startAxes.c || 0),
|
||||
);
|
||||
const distance = Math.max(linearDistance, rotaryDistance * 0.1, 0.001);
|
||||
return durationSeconds > 0 ? (distance / durationSeconds) * 60 : 1;
|
||||
}
|
||||
|
||||
function resolveRuntimeSampleIndexForLine(state = {}, activeLine, fallbackSampleIndex = 0) {
|
||||
const samples = state.programAxisPreviewPath?.samples;
|
||||
if (!Array.isArray(samples) || samples.length === 0) {
|
||||
@@ -3736,6 +4018,51 @@ function resolveRuntimeSampleIndexForLine(state = {}, activeLine, fallbackSample
|
||||
return samples.length - 1;
|
||||
}
|
||||
|
||||
function resolveRuntimeSampleIndexForTaskHalPose(state = {}, axisPose = {}, activeLine, fallbackSampleIndex = 0) {
|
||||
const samples = state.programAxisPreviewPath?.samples;
|
||||
if (!Array.isArray(samples) || samples.length === 0) {
|
||||
return resolveRuntimeSampleIndexForLine(state, activeLine, fallbackSampleIndex);
|
||||
}
|
||||
const current = clampNumber(state.programExecutionSampleIndex || 0, 0, samples.length - 1);
|
||||
const previousCycle = Number(state.programRuntimeFeedback?.cycle || 0);
|
||||
const nextCycle = Number(fallbackSampleIndex || 0);
|
||||
const cycleDelta = Number.isFinite(nextCycle) && Number.isFinite(previousCycle)
|
||||
? Math.max(nextCycle - previousCycle, 0)
|
||||
: 0;
|
||||
const adaptiveWindow = Math.max(320, Math.min(samples.length - 1, Math.ceil(cycleDelta * 8) + 240));
|
||||
const windowStart = current;
|
||||
const windowEnd = Math.min(samples.length - 1, current + adaptiveWindow);
|
||||
const line = Number(activeLine);
|
||||
let bestIndex = current;
|
||||
let bestScore = Number.POSITIVE_INFINITY;
|
||||
for (let index = windowStart; index <= windowEnd; index += 1) {
|
||||
const sample = samples[index];
|
||||
const samplePose = axisPoseFromProgramPathSample(sample, axisPose, state.profile);
|
||||
const linePenalty = Number.isFinite(line) && Number.isFinite(Number(sample?.line))
|
||||
? Math.min(Math.abs(Number(sample.line) - line), 12) * 0.25
|
||||
: 0;
|
||||
const forwardPenalty = (index - current) * 0.000001;
|
||||
const score = taskHalPoseDistanceScore(axisPose, samplePose) + linePenalty + forwardPenalty;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestIndex = index;
|
||||
}
|
||||
}
|
||||
return Math.max(current, bestIndex);
|
||||
}
|
||||
|
||||
function taskHalPoseDistanceScore(a = {}, b = {}) {
|
||||
const linear = ["x", "y", "z"].reduce((sum, axis) => {
|
||||
const delta = Number(a[axis] || 0) - Number(b[axis] || 0);
|
||||
return sum + delta * delta;
|
||||
}, 0);
|
||||
const rotary = ["a", "b", "c"].reduce((sum, axis) => {
|
||||
const delta = Number(a[axis] || 0) - Number(b[axis] || 0);
|
||||
return sum + (delta * delta * 0.01);
|
||||
}, 0);
|
||||
return linear + rotary;
|
||||
}
|
||||
|
||||
function applyProgramPlaybackUiPatch(state = {}, {
|
||||
activeLine,
|
||||
axisPose,
|
||||
@@ -3744,9 +4071,13 @@ function applyProgramPlaybackUiPatch(state = {}, {
|
||||
motionIndex,
|
||||
sampleIndex,
|
||||
runtimeFeedback,
|
||||
preferRuntimeAxisPose = false,
|
||||
} = {}) {
|
||||
const sample = currentProgramPathSample(state, sampleIndex);
|
||||
const sampleAxisPose = axisPoseFromProgramPathSample(sample, axisPose || state.axisPose, state.profile);
|
||||
const runtimeAxisPose = clampAxisPoseToProfile(axisPose || runtimeFeedback?.axisPose || state.axisPose, state.profile);
|
||||
const sampleAxisPose = preferRuntimeAxisPose
|
||||
? runtimeAxisPose
|
||||
: axisPoseFromProgramPathSample(sample, runtimeAxisPose, state.profile);
|
||||
const sampleKinsType = sample?.activeKinematics
|
||||
? normalizeSampleKinsType(state, sample.activeKinematics)
|
||||
: kinsType;
|
||||
@@ -3764,7 +4095,7 @@ function applyProgramPlaybackUiPatch(state = {}, {
|
||||
axisPose: sampleAxisPose,
|
||||
kinsType: sampleKinsType || state.kinsType,
|
||||
toolAxisVector,
|
||||
});
|
||||
}, { preferRuntimeAxisPose });
|
||||
const patchState = {
|
||||
...state,
|
||||
activeLine: sampleLine,
|
||||
@@ -3785,11 +4116,13 @@ function applyProgramPlaybackUiPatch(state = {}, {
|
||||
programExecutionMotionIndex: patchState.programExecutionMotionIndex,
|
||||
programExecutionSampleIndex: patchState.programExecutionSampleIndex,
|
||||
programRuntimeFeedback: enrichedFeedback,
|
||||
programUiExecution: createProgramUiExecution(patchState),
|
||||
programUiExecution: createProgramUiExecution(patchState, { preferRuntimePose: preferRuntimeAxisPose }),
|
||||
};
|
||||
}
|
||||
|
||||
function createProgramUiExecution(state = {}) {
|
||||
function createProgramUiExecution(state = {}, {
|
||||
preferRuntimePose = false,
|
||||
} = {}) {
|
||||
const sample = currentProgramPathSample(state, state.programExecutionSampleIndex);
|
||||
const feedback = state.programRuntimeFeedback || {};
|
||||
const sampleCount = Number(state.programAxisPreviewPath?.sampleCount || state.programAxisPreviewPath?.samples?.length || 0);
|
||||
@@ -3829,8 +4162,12 @@ function createProgramUiExecution(state = {}) {
|
||||
segmentIndex: sample?.segmentIndex ?? traceEntry?.segmentIndex ?? null,
|
||||
motionType: sample?.motionType || traceEntry?.motionType || feedback.motionType || feedback.type || null,
|
||||
activeKinematics: sample?.activeKinematics || traceEntry?.activeKinematicsAfter || state.kinsType,
|
||||
joint: sample?.joint || pickExecutionAxes(state.axisPose || {}),
|
||||
tcp: sample?.tcp || {
|
||||
joint: preferRuntimePose
|
||||
? pickExecutionAxes(feedback.axisPose || state.axisPose || {})
|
||||
: sample?.joint || pickExecutionAxes(state.axisPose || {}),
|
||||
tcp: preferRuntimePose && feedback.tcp
|
||||
? feedback.tcp
|
||||
: sample?.tcp || {
|
||||
x: Number(state.tcpPose?.x || state.axisPose?.x || 0),
|
||||
y: Number(state.tcpPose?.y || state.axisPose?.y || 0),
|
||||
z: Number(state.tcpPose?.z || state.axisPose?.z || 0),
|
||||
@@ -3886,13 +4223,22 @@ function toolAxisSampleFromVector(vector = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function enrichRuntimeFeedbackWithSample(feedback = {}, sample = null, state = {}) {
|
||||
function enrichRuntimeFeedbackWithSample(feedback = {}, sample = null, state = {}, {
|
||||
preferRuntimeAxisPose = false,
|
||||
} = {}) {
|
||||
if (!sample) return feedback;
|
||||
const tcp = sample.tcp || {
|
||||
const runtimeAxisPose = feedback?.axisPose || state.axisPose || {};
|
||||
const tcp = preferRuntimeAxisPose
|
||||
? {
|
||||
x: Number(runtimeAxisPose.x ?? 0),
|
||||
y: Number(runtimeAxisPose.y ?? 0),
|
||||
z: Number(runtimeAxisPose.z ?? 0),
|
||||
}
|
||||
: sample.tcp || {
|
||||
x: Number(state.tcpPose?.x ?? state.axisPose?.x ?? feedback?.axisPose?.x ?? 0),
|
||||
y: Number(state.tcpPose?.y ?? state.axisPose?.y ?? feedback?.axisPose?.y ?? 0),
|
||||
z: Number(state.tcpPose?.z ?? state.axisPose?.z ?? feedback?.axisPose?.z ?? 0),
|
||||
};
|
||||
};
|
||||
return {
|
||||
...(feedback || {}),
|
||||
sampleIndex: Number(sample.sampleIndex || 0),
|
||||
@@ -3902,7 +4248,9 @@ function enrichRuntimeFeedbackWithSample(feedback = {}, sample = null, state = {
|
||||
statement: sample.statement || feedback?.statement || "",
|
||||
motionType: sample.motionType || feedback?.motionType || null,
|
||||
activeKinematics: sample.activeKinematics || feedback?.activeKinematics || state.kinsType,
|
||||
axisPose: axisPoseFromProgramPathSample(sample, state.axisPose || feedback?.axisPose || {}, state.profile),
|
||||
axisPose: preferRuntimeAxisPose
|
||||
? runtimeAxisPose
|
||||
: axisPoseFromProgramPathSample(sample, state.axisPose || feedback?.axisPose || {}, state.profile),
|
||||
tcp,
|
||||
toolAxis: sample.toolAxis || feedback?.toolAxis || null,
|
||||
machineState: sample.machineState || feedback?.machineState || null,
|
||||
@@ -4155,6 +4503,22 @@ function executeMdiCommand(state, rawCommand) {
|
||||
return { patch };
|
||||
}
|
||||
|
||||
function createManualTouchOffMdiPatch(state, patch, command) {
|
||||
return {
|
||||
...patch,
|
||||
machine: {
|
||||
...patch.machine,
|
||||
mode: "manual",
|
||||
manualPanel: state.machine.manualPanel || "manual",
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
runState: "idle",
|
||||
operatorMessage: `manual touch off ${command}`,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMdiCommand(command) {
|
||||
return String(command || "")
|
||||
.replace(/\([^)]*\)/g, " ")
|
||||
|
||||
@@ -167,6 +167,13 @@ body {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.axis-mode-toggle {
|
||||
width: 44px;
|
||||
min-width: 44px;
|
||||
font: 700 11px/1 Arial, sans-serif;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.axis-toolbar-separator {
|
||||
width: 8px;
|
||||
height: 26px;
|
||||
|
||||
@@ -24,6 +24,7 @@ export const AXIS_BUTTON_PARITY = [
|
||||
{ id: "menu-audit", action: "audit", sourceSymbol: "web parity audit", sourceLines: "collect-web-xyzbc-trt-evidence.mjs", expected: "request native/Web parity audit" },
|
||||
{ id: "toolbar-estop", action: "estop", sourceSymbol: "commands.estop_clicked", sourceLines: "axis.py:2223-2229", expected: "toggle ESTOP/ESTOP_RESET" },
|
||||
{ id: "toolbar-power", action: "power", sourceSymbol: "commands.onoff_clicked", sourceLines: "axis.py:2231-2241", expected: "toggle machine power" },
|
||||
{ id: "toolbar-auto-manual", action: "toggle-auto-manual", sourceSymbol: "commands.ensure_manual / commands.task_mode_auto", sourceLines: "axis.py:2308-2320,2520-2531", expected: "toggle task mode between AUTO and MANUAL" },
|
||||
{ id: "toolbar-load", action: "open", sourceSymbol: "commands.open_file", sourceLines: "axis.py:2243-2262", expected: "open local G-code" },
|
||||
{ id: "toolbar-reload", action: "reload", sourceSymbol: "commands.reload_file", sourceLines: "axis.py:2296-2297", expected: "reload active program" },
|
||||
{ id: "toolbar-run", action: "run", sourceSymbol: "commands.task_run", sourceLines: "axis.py:2308-2320", expected: "start AUTO run" },
|
||||
@@ -169,6 +170,7 @@ function renderToolbar(element, state, dispatch) {
|
||||
<input type="file" class="axis-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
|
||||
${toolButton("tbtn_estop", "estop", "Emergency stop", state.machine.estopActive)}
|
||||
${toolButton("tbtn_on", "power", "Machine power", state.machine.powerOn)}
|
||||
${modeToggleTool(state)}
|
||||
${toolButton("btn_load", "open", "Open program")}
|
||||
${toolButton("btn_reload", "reload", "Reload program")}
|
||||
${toolButton("btn_run", "run", "Run program")}
|
||||
@@ -560,6 +562,21 @@ function smallTextTool(label, action, title) {
|
||||
return `<button type="button" class="axis-tool-button axis-text-tool" data-action="${action}" title="${escapeHtml(title)}">${escapeHtml(label)}</button>`;
|
||||
}
|
||||
|
||||
function modeToggleTool(state) {
|
||||
const autoMode = state.machine?.mode === "auto";
|
||||
const label = autoMode ? "MAN" : "AUTO";
|
||||
const title = autoMode ? "Switch to Manual mode" : "Switch to Auto mode";
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="axis-tool-button axis-mode-toggle"
|
||||
data-action="toggle-auto-manual"
|
||||
data-mode="${autoMode ? "auto" : "manual"}"
|
||||
title="${escapeHtml(title)}"
|
||||
>${label}</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function menuButton(label, entries) {
|
||||
return `
|
||||
<div class="axis-menu">
|
||||
@@ -642,8 +659,9 @@ function runAxisCommand(command, state, dispatch, root) {
|
||||
const actionMap = {
|
||||
estop: () => dispatch({ type: state.machine?.estopActive || state.machine?.taskState === "estop" ? "RESET" : "ESTOP" }),
|
||||
power: () => dispatch({ type: "TOGGLE_POWER" }),
|
||||
"toggle-auto-manual": () => dispatch({ type: "SET_MODE", mode: state.machine?.mode === "auto" ? "manual" : "auto" }),
|
||||
reload: () => dispatch({ type: "RELOAD_PROGRAM" }),
|
||||
run: () => dispatch({ type: "RUN" }),
|
||||
run: () => dispatch({ type: "RUN_FROM_OPERATOR" }),
|
||||
pause: () => dispatch({ type: "PAUSE" }),
|
||||
resume: () => dispatch({ type: "RESUME" }),
|
||||
step: () => dispatch({ type: "STEP" }),
|
||||
@@ -652,8 +670,8 @@ function runAxisCommand(command, state, dispatch, root) {
|
||||
"home-all": () => dispatch({ type: "HOME" }),
|
||||
"jog-minus": () => dispatch({ type: "JOG", axis: selectedAxis, direction: -1, increment: Number(state.machine?.jogIncrement ?? 1) || 1 }),
|
||||
"jog-plus": () => dispatch({ type: "JOG", axis: selectedAxis, direction: 1, increment: Number(state.machine?.jogIncrement ?? 1) || 1 }),
|
||||
"touch-off": () => dispatch({ type: "RUN_MDI", command: `G10 L20 P0 ${selectedAxis.toUpperCase()}0` }),
|
||||
"tool-touch-off": () => dispatch({ type: "RUN_MDI", command: "G43" }),
|
||||
"touch-off": () => dispatch({ type: "RUN_MDI", command: `G10 L20 P0 ${selectedAxis.toUpperCase()}0`, manualTouchOff: true }),
|
||||
"tool-touch-off": () => dispatch({ type: "RUN_MDI", command: "G43", manualTouchOff: true }),
|
||||
"spindle-stop": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "stop" }),
|
||||
"spindle-forward": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" }),
|
||||
"spindle-reverse": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "reverse" }),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createLinuxCncInterpreterRuntime } from "./linuxcnc-interpreter-runtime.js";
|
||||
|
||||
installTextDecoderResizableArrayBufferCompat();
|
||||
|
||||
let runtime = null;
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
@@ -47,3 +49,28 @@ self.addEventListener("message", async (event) => {
|
||||
function postSuccess(id, value) {
|
||||
self.postMessage({ id, ok: true, value });
|
||||
}
|
||||
|
||||
function installTextDecoderResizableArrayBufferCompat() {
|
||||
const decoderPrototype = globalThis.TextDecoder?.prototype;
|
||||
if (!decoderPrototype || decoderPrototype.__webRtcpResizableArrayBufferCompat) return;
|
||||
const nativeDecode = decoderPrototype.decode;
|
||||
Object.defineProperty(decoderPrototype, "__webRtcpResizableArrayBufferCompat", {
|
||||
value: true,
|
||||
configurable: false,
|
||||
});
|
||||
decoderPrototype.decode = function decodeResizableArrayBufferCompat(input, options) {
|
||||
try {
|
||||
return nativeDecode.call(this, input, options);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("resizable")) throw error;
|
||||
if (input?.buffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(input), options);
|
||||
}
|
||||
if (input instanceof ArrayBuffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(new Uint8Array(input)), options);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createLinuxCncKinematicsRuntime } from "./linuxcnc-kinematics-runtime.js";
|
||||
|
||||
installTextDecoderResizableArrayBufferCompat();
|
||||
|
||||
let runtime = null;
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
@@ -64,3 +66,28 @@ self.addEventListener("message", async (event) => {
|
||||
function postSuccess(id, value) {
|
||||
self.postMessage({ id, ok: true, value });
|
||||
}
|
||||
|
||||
function installTextDecoderResizableArrayBufferCompat() {
|
||||
const decoderPrototype = globalThis.TextDecoder?.prototype;
|
||||
if (!decoderPrototype || decoderPrototype.__webRtcpResizableArrayBufferCompat) return;
|
||||
const nativeDecode = decoderPrototype.decode;
|
||||
Object.defineProperty(decoderPrototype, "__webRtcpResizableArrayBufferCompat", {
|
||||
value: true,
|
||||
configurable: false,
|
||||
});
|
||||
decoderPrototype.decode = function decodeResizableArrayBufferCompat(input, options) {
|
||||
try {
|
||||
return nativeDecode.call(this, input, options);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("resizable")) throw error;
|
||||
if (input?.buffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(input), options);
|
||||
}
|
||||
if (input instanceof ArrayBuffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(new Uint8Array(input)), options);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createLinuxCncTaskHalRuntime } from "./linuxcnc-task-hal-runtime.js";
|
||||
|
||||
installTextDecoderResizableArrayBufferCompat();
|
||||
|
||||
let runtime = null;
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
@@ -61,3 +63,28 @@ function assertRuntime() {
|
||||
throw new Error("LinuxCNC task/HAL worker runtime is not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
function installTextDecoderResizableArrayBufferCompat() {
|
||||
const decoderPrototype = globalThis.TextDecoder?.prototype;
|
||||
if (!decoderPrototype || decoderPrototype.__webRtcpResizableArrayBufferCompat) return;
|
||||
const nativeDecode = decoderPrototype.decode;
|
||||
Object.defineProperty(decoderPrototype, "__webRtcpResizableArrayBufferCompat", {
|
||||
value: true,
|
||||
configurable: false,
|
||||
});
|
||||
decoderPrototype.decode = function decodeResizableArrayBufferCompat(input, options) {
|
||||
try {
|
||||
return nativeDecode.call(this, input, options);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("resizable")) throw error;
|
||||
if (input?.buffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(input), options);
|
||||
}
|
||||
if (input instanceof ArrayBuffer) {
|
||||
return nativeDecode.call(this, Uint8Array.from(new Uint8Array(input)), options);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,13 +139,16 @@ export function gateLinuxCncTaskAction(state, action) {
|
||||
}
|
||||
return allow(status);
|
||||
case "RUN_MDI":
|
||||
if (status.taskState !== "on") return block(status, "MDI blocked: machine must be on");
|
||||
if (status.taskMode !== "mdi") return block(status, "MDI blocked: switch to MDI mode first");
|
||||
if (!status.allHomed && !status.noForceHoming) return block(status, "MDI blocked: home machine first");
|
||||
if (status.interpState === "reading" || status.interpState === "waiting") {
|
||||
return block(status, "MDI blocked: interpreter must be idle");
|
||||
{
|
||||
const manualTouchOff = isManualTouchOffMdiAction(status, action);
|
||||
if (status.taskState !== "on") return block(status, "MDI blocked: machine must be on");
|
||||
if (status.taskMode !== "mdi" && !manualTouchOff) return block(status, "MDI blocked: switch to MDI mode first");
|
||||
if (!status.allHomed && !status.noForceHoming) return block(status, "MDI blocked: home machine first");
|
||||
if (status.interpState === "reading" || status.interpState === "waiting") {
|
||||
return block(status, "MDI blocked: interpreter must be idle");
|
||||
}
|
||||
return allow(status);
|
||||
}
|
||||
return allow(status);
|
||||
case "RUN":
|
||||
case "STEP":
|
||||
case "RUN_FRAME":
|
||||
@@ -191,6 +194,17 @@ export function gateLinuxCncTaskAction(state, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function isManualTouchOffMdiAction(status, action) {
|
||||
if (!action || action.manualTouchOff !== true || status.taskMode !== "manual") return false;
|
||||
const command = String(action.command || "")
|
||||
.replace(/\([^)]*\)/g, " ")
|
||||
.replace(/;.*$/g, " ")
|
||||
.trim()
|
||||
.replace(/\s+/g, " ")
|
||||
.toUpperCase();
|
||||
return /^G10\s+L20\s+P0\s+[XYZABC]-?\d/.test(command) || /^G43(?:\s|$)/.test(command);
|
||||
}
|
||||
|
||||
function gateGmoccapyXyzabRun(status) {
|
||||
if (!status.iniLoaded) return block(status, "run blocked: LinuxCNC INI not loaded");
|
||||
if (!status.machineFileStaged) return block(status, "run blocked: LinuxCNC machine files not staged");
|
||||
|
||||
@@ -487,6 +487,25 @@ export function createSimulationStore(seed = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
const waitForStatePolling = (predicate, timeoutMs = 10000, intervalMs = 25) => {
|
||||
if (predicate(state)) return Promise.resolve(state);
|
||||
return new Promise((resolve, reject) => {
|
||||
const startedAt = Date.now();
|
||||
const tick = () => {
|
||||
if (predicate(state)) {
|
||||
resolve(state);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - startedAt > timeoutMs) {
|
||||
reject(new Error("timed out waiting for store state"));
|
||||
return;
|
||||
}
|
||||
setTimeout(tick, intervalMs);
|
||||
};
|
||||
setTimeout(tick, intervalMs);
|
||||
});
|
||||
};
|
||||
|
||||
const dispatch = (action) => {
|
||||
switch (action.type) {
|
||||
case "BOOT_READY":
|
||||
@@ -1161,6 +1180,13 @@ export function createSimulationStore(seed = {}) {
|
||||
});
|
||||
});
|
||||
break;
|
||||
case "RUN_FROM_OPERATOR":
|
||||
return operatorRunSequence().catch((error) => {
|
||||
dispatch({
|
||||
type: "TASK_HAL_COMMAND_FAILED",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
case "SET_FRAME_SOURCE":
|
||||
setState({
|
||||
sourceMode: action.sourceMode,
|
||||
@@ -1503,27 +1529,33 @@ export function createSimulationStore(seed = {}) {
|
||||
break;
|
||||
case "RUN_MDI":
|
||||
{
|
||||
const gate = gateLinuxCncTaskAction(state, action);
|
||||
const command = normalizeMdiCommand(action.command ?? state.machine.mdiCommand);
|
||||
const gate = gateLinuxCncTaskAction(state, { ...action, command });
|
||||
if (!gate.allowed) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
const preserveManualTouchOff = action.manualTouchOff === true && state.machine.mode === "manual";
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
const command = normalizeMdiCommand(action.command ?? state.machine.mdiCommand);
|
||||
const mdiResult = executeMdiCommand(state, command);
|
||||
setState(mdiResult.patch);
|
||||
const mdiPatch = preserveManualTouchOff
|
||||
? createManualTouchOffMdiPatch(state, mdiResult.patch, command)
|
||||
: mdiResult.patch;
|
||||
setState(mdiPatch);
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_MODE", mode: "MDI" },
|
||||
{ type: "EMC_TASK_PLAN_EXECUTE", mdi: command },
|
||||
], {
|
||||
operatorMessage: `task/HAL MDI ${command}`,
|
||||
preserveAxisPose: mdiResult.patch.axisPose,
|
||||
preserveMachine: mdiResult.patch.machine,
|
||||
preserveAxisPose: mdiPatch.axisPose,
|
||||
preserveMachine: mdiPatch.machine,
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const mdiResult = executeMdiCommand(state, action.command ?? state.machine.mdiCommand);
|
||||
setState(mdiResult.patch);
|
||||
const mdiResult = executeMdiCommand(state, command);
|
||||
setState(preserveManualTouchOff
|
||||
? createManualTouchOffMdiPatch(state, mdiResult.patch, command)
|
||||
: mdiResult.patch);
|
||||
}
|
||||
break;
|
||||
case "LOAD_PROGRAM":
|
||||
@@ -2480,16 +2512,121 @@ export function createSimulationStore(seed = {}) {
|
||||
return state;
|
||||
};
|
||||
|
||||
const operatorRunSequence = async () => {
|
||||
if (!state.machineFileStaging?.selectedGcodeSourceRel) {
|
||||
const sourceRel = defaultLinuxCncGcodeSourceForState(state)?.sourceRel;
|
||||
if (sourceRel) {
|
||||
dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
|
||||
await waitForStatePredicate((nextState) => nextState.machineFileStaging?.selectedGcodeSourceRel === sourceRel);
|
||||
}
|
||||
}
|
||||
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
if (
|
||||
!Array.isArray(state.programExecution?.motion)
|
||||
|| state.programExecution.motion.length === 0
|
||||
|| state.interpreterExecutionPending
|
||||
) {
|
||||
if (!state.interpreterExecutionPending) {
|
||||
dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
|
||||
}
|
||||
await waitForStatePolling((nextState) => (
|
||||
nextState.interpreterExecutionPending === false
|
||||
&& Array.isArray(nextState.programExecution?.motion)
|
||||
&& nextState.programExecution.motion.length > 0
|
||||
), 15000);
|
||||
}
|
||||
if (tcpKinsType && state.kinsType !== tcpKinsType) {
|
||||
dispatch({ type: "SET_KINS_TYPE", kinsType: tcpKinsType });
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: true,
|
||||
estopActive: false,
|
||||
taskState: "on",
|
||||
mode: "auto",
|
||||
manualPanel: null,
|
||||
allHomed: true,
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
runState: "idle",
|
||||
operatorMessage: "RUN ready: motion plan source ready; preparing task/HAL session",
|
||||
});
|
||||
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
|
||||
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
|
||||
setState({ operatorMessage: "RUN preparing: initializing task/HAL session" });
|
||||
await initializeTaskHalSession({ openProgram: true });
|
||||
}
|
||||
setState({ operatorMessage: "RUN preparing: loading task/HAL motion plan" });
|
||||
const loadedMotionPlan = await loadTaskHalMotionPlanWithSessionRetry();
|
||||
if (!loadedMotionPlan) {
|
||||
setState({ operatorMessage: "run blocked: task/HAL feed motion plan not loaded" });
|
||||
return state;
|
||||
}
|
||||
const ready = validateRunPreconditions(state, { requireTaskHalSession: true });
|
||||
if (!ready.ok) {
|
||||
setState({ operatorMessage: ready.operatorMessage });
|
||||
return state;
|
||||
}
|
||||
stopTaskHalStatusLoop("restarted", {
|
||||
operatorMessage: "task/HAL status loop restarting",
|
||||
});
|
||||
setState({
|
||||
activeLine: state.programStartLine || 1,
|
||||
programExecutionMotionIndex: 0,
|
||||
programExecutionSampleIndex: 0,
|
||||
programRuntimeFeedback: null,
|
||||
programLineExecution: {},
|
||||
operatorMessage: "RUN executing: sending task/HAL PLAN_RUN",
|
||||
});
|
||||
const status = await runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_STATE", state: "ON" },
|
||||
{ type: "EMC_JOINT_HOME", joint: -1 },
|
||||
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
|
||||
{ type: "EMC_TASK_PLAN_RUN", line: 0 },
|
||||
], {
|
||||
taskCycles: 5,
|
||||
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
|
||||
allowFixtureSession: false,
|
||||
});
|
||||
if (shouldContinueTaskHalStatusLoop(state, status)) {
|
||||
startTaskHalStatusLoop({
|
||||
profileId: ready.profileId,
|
||||
iniPath: ready.iniPath,
|
||||
kinematicsModuleId: ready.kinematicsModuleId,
|
||||
operatorMessage: `task/HAL status loop running ${ready.profileId} ${ready.kinematicsModuleId}`,
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
const policy = createLinuxCncTaskPolicyStatus(state);
|
||||
if (policy.taskMode !== "auto" || !policy.allHomed || policy.taskState !== "on") {
|
||||
await runReadySequence();
|
||||
}
|
||||
dispatch({ type: "RUN" });
|
||||
return state;
|
||||
};
|
||||
|
||||
const loadTaskHalMotionPlanForSession = async (session = state.taskHalSession) => {
|
||||
if (!state.taskHalRuntime?.loaded || typeof state.taskHalRuntime.loadProgramMotionPlan !== "function") {
|
||||
return null;
|
||||
}
|
||||
setState({ operatorMessage: "RUN preparing: building task/HAL motion plan" });
|
||||
const motion = state.programExecution?.motion || [];
|
||||
const timing = state.programExecutionTiming || buildTimingForState(state, state.programExecution);
|
||||
if (!session?.programPath || !Array.isArray(motion) || motion.length === 0 || !Array.isArray(timing?.segments) || timing.segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const plan = buildTaskHalProgramMotionPlan({
|
||||
const previewPlan = buildTaskHalProgramMotionPlanFromPreviewPath({
|
||||
programPath: session.programPath,
|
||||
path: state.programAxisPreviewPath,
|
||||
profile: state.profile,
|
||||
linearUnits: timing.linearUnits || state.profile?.traj?.linearUnits || "mm",
|
||||
});
|
||||
const plan = previewPlan || buildTaskHalProgramMotionPlan({
|
||||
programPath: session.programPath,
|
||||
motion,
|
||||
timing,
|
||||
@@ -2499,10 +2636,27 @@ export function createSimulationStore(seed = {}) {
|
||||
if (plan.segmentCount <= 0) {
|
||||
return null;
|
||||
}
|
||||
await state.taskHalRuntime.loadProgramMotionPlan(plan);
|
||||
setState({ operatorMessage: `RUN preparing: worker loading ${plan.segmentCount} task/HAL motion segments` });
|
||||
await withTimeout(
|
||||
state.taskHalRuntime.loadProgramMotionPlan(plan),
|
||||
5000,
|
||||
"task/HAL feed motion plan load timed out",
|
||||
);
|
||||
return plan;
|
||||
};
|
||||
|
||||
const loadTaskHalMotionPlanWithSessionRetry = async () => {
|
||||
let plan = null;
|
||||
try {
|
||||
plan = await loadTaskHalMotionPlanForSession(state.taskHalSession);
|
||||
} catch (error) {
|
||||
setState({ operatorMessage: `task/HAL motion plan reload: ${error instanceof Error ? error.message : String(error)}` });
|
||||
}
|
||||
if (plan) return plan;
|
||||
await initializeTaskHalSession({ openProgram: true });
|
||||
return loadTaskHalMotionPlanForSession(state.taskHalSession);
|
||||
};
|
||||
|
||||
const startTaskHalStatusLoop = ({
|
||||
profileId = state.machineProfile,
|
||||
iniPath = state.profile?.iniPath || null,
|
||||
@@ -3292,6 +3446,21 @@ function sourceBasename(path) {
|
||||
return String(path || "").split("/").filter(Boolean).at(-1) || "";
|
||||
}
|
||||
|
||||
function withTimeout(promise, timeoutMs, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
|
||||
Promise.resolve(promise)
|
||||
.then((value) => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(value);
|
||||
})
|
||||
.catch((error) => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isAsyncKinematicsRuntime(runtime) {
|
||||
return runtime?.executionContext === "worker";
|
||||
}
|
||||
@@ -3493,19 +3662,54 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
|
||||
? "jogging"
|
||||
: "idle";
|
||||
const runtimeFeedback = createTaskHalRuntimeFeedback(state, status, axisPose, activeLine);
|
||||
const taskHalSampleIndex = resolveRuntimeSampleIndexForLine(state, activeLine, runtimeFeedback.sampleIndex);
|
||||
const taskHalPlaybackPatch = applyProgramPlaybackUiPatch(state, {
|
||||
activeLine,
|
||||
axisPose,
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
motionIndex: runtimeFeedback.motionIndex,
|
||||
const shouldTrackProgramPlayback = runState === "running"
|
||||
|| runState === "mdi"
|
||||
|| (runState === "complete" && (state.runState === "running" || state.runState === "mdi" || state.runState === "complete"));
|
||||
const taskHalSampleIndex = shouldTrackProgramPlayback
|
||||
? resolveRuntimeSampleIndexForTaskHalPose(state, axisPose, activeLine, runtimeFeedback.sampleIndex)
|
||||
: clampNumber(
|
||||
state.programExecutionSampleIndex || 0,
|
||||
0,
|
||||
Math.max(Number(state.programAxisPreviewPath?.samples?.length || state.programAxisPreviewPath?.sampleCount || 1) - 1, 0),
|
||||
);
|
||||
const idleRuntimeFeedback = {
|
||||
...runtimeFeedback,
|
||||
sampleIndex: taskHalSampleIndex,
|
||||
runtimeFeedback: {
|
||||
...runtimeFeedback,
|
||||
motionIndex: Number(state.programExecutionMotionIndex || runtimeFeedback.motionIndex || 0),
|
||||
};
|
||||
const taskHalPlaybackPatch = shouldTrackProgramPlayback
|
||||
? applyProgramPlaybackUiPatch(state, {
|
||||
activeLine,
|
||||
axisPose,
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
motionIndex: runtimeFeedback.motionIndex,
|
||||
sampleIndex: taskHalSampleIndex,
|
||||
},
|
||||
});
|
||||
runtimeFeedback: {
|
||||
...runtimeFeedback,
|
||||
sampleIndex: taskHalSampleIndex,
|
||||
},
|
||||
preferRuntimeAxisPose: true,
|
||||
})
|
||||
: {
|
||||
activeLine: state.activeLine,
|
||||
axisPose,
|
||||
kinsType: kinsType || state.kinsType,
|
||||
rtcpState: kinsType ? rtcpStateFromKinsType(kinsType) : state.rtcpState,
|
||||
toolAxisVector: state.toolAxisVector,
|
||||
programExecutionMotionIndex: Number(state.programExecutionMotionIndex || 0),
|
||||
programExecutionSampleIndex: taskHalSampleIndex,
|
||||
programRuntimeFeedback: idleRuntimeFeedback,
|
||||
programUiExecution: state.programUiExecution
|
||||
? createProgramUiExecution({
|
||||
...state,
|
||||
axisPose,
|
||||
kinsType: kinsType || state.kinsType,
|
||||
programExecutionSampleIndex: taskHalSampleIndex,
|
||||
programRuntimeFeedback: idleRuntimeFeedback,
|
||||
}, { preferRuntimePose: true })
|
||||
: state.programUiExecution,
|
||||
};
|
||||
const loopActive = state.taskHalStatusLoop?.active === true
|
||||
&& loopSequence !== null
|
||||
&& Number(state.taskHalStatusLoop.sequence) === Number(loopSequence)
|
||||
@@ -3720,6 +3924,84 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildTaskHalProgramMotionPlanFromPreviewPath({
|
||||
programPath = null,
|
||||
path = null,
|
||||
profile = defaultProfile,
|
||||
linearUnits = "mm",
|
||||
} = {}) {
|
||||
const samples = path?.samples;
|
||||
if (!Array.isArray(samples) || samples.length < 2) return null;
|
||||
const samplePeriodSeconds = Math.max(Number(path.samplePeriodMs || 50) / 1000, 0.001);
|
||||
const segments = [];
|
||||
for (let index = 1; index < samples.length; index += 1) {
|
||||
const startSample = samples[index - 1];
|
||||
const endSample = samples[index];
|
||||
const startAxes = taskHalPlanAxesFromPose(axisPoseFromProgramPathSample(startSample, initialAxisPose, profile));
|
||||
const endAxes = taskHalPlanAxesFromPose(axisPoseFromProgramPathSample(endSample, startAxes, profile));
|
||||
const startSeconds = Number.isFinite(Number(startSample?.timeMs))
|
||||
? Number(startSample.timeMs) / 1000
|
||||
: (index - 1) * samplePeriodSeconds;
|
||||
const endSeconds = Number.isFinite(Number(endSample?.timeMs))
|
||||
? Number(endSample.timeMs) / 1000
|
||||
: index * samplePeriodSeconds;
|
||||
const durationSeconds = Math.max(endSeconds - startSeconds, samplePeriodSeconds);
|
||||
const velocityMmPerMin = estimatePreviewSegmentVelocityMmPerMin(startAxes, endAxes, durationSeconds);
|
||||
const type = endSample?.motionType || startSample?.motionType || "STRAIGHT_FEED";
|
||||
segments.push({
|
||||
line: Number(endSample?.line || startSample?.line || 1),
|
||||
type,
|
||||
motionClass: String(type).includes("TRAVERSE") ? "rapid" : "feed",
|
||||
feedMode: "units-per-minute",
|
||||
startSeconds,
|
||||
durationSeconds,
|
||||
elapsedSeconds: startSeconds + durationSeconds,
|
||||
feedRate: velocityMmPerMin,
|
||||
linearUnits,
|
||||
velocityMmPerMin,
|
||||
requestedVelocityMmPerMin: velocityMmPerMin,
|
||||
startAxes,
|
||||
endAxes,
|
||||
});
|
||||
}
|
||||
if (segments.length === 0) return null;
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-program-motion-plan",
|
||||
semanticBoundary: "linuxcnc_task_hal_plan_from_expanded_axis_preview_samples",
|
||||
programPath,
|
||||
linearUnits,
|
||||
totalSeconds: segments[segments.length - 1].elapsedSeconds,
|
||||
segmentCount: segments.length,
|
||||
segments,
|
||||
};
|
||||
}
|
||||
|
||||
function taskHalPlanAxesFromPose(pose = {}) {
|
||||
return {
|
||||
x: Number(pose.x || 0),
|
||||
y: Number(pose.y || 0),
|
||||
z: Number(pose.z || 0),
|
||||
a: Number(pose.a || 0),
|
||||
b: Number(pose.b || 0),
|
||||
c: Number(pose.c || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function estimatePreviewSegmentVelocityMmPerMin(startAxes = {}, endAxes = {}, durationSeconds = 0.05) {
|
||||
const linearDistance = Math.hypot(
|
||||
Number(endAxes.x || 0) - Number(startAxes.x || 0),
|
||||
Number(endAxes.y || 0) - Number(startAxes.y || 0),
|
||||
Number(endAxes.z || 0) - Number(startAxes.z || 0),
|
||||
);
|
||||
const rotaryDistance = Math.hypot(
|
||||
Number(endAxes.a || 0) - Number(startAxes.a || 0),
|
||||
Number(endAxes.b || 0) - Number(startAxes.b || 0),
|
||||
Number(endAxes.c || 0) - Number(startAxes.c || 0),
|
||||
);
|
||||
const distance = Math.max(linearDistance, rotaryDistance * 0.1, 0.001);
|
||||
return durationSeconds > 0 ? (distance / durationSeconds) * 60 : 1;
|
||||
}
|
||||
|
||||
function resolveRuntimeSampleIndexForLine(state = {}, activeLine, fallbackSampleIndex = 0) {
|
||||
const samples = state.programAxisPreviewPath?.samples;
|
||||
if (!Array.isArray(samples) || samples.length === 0) {
|
||||
@@ -3736,6 +4018,51 @@ function resolveRuntimeSampleIndexForLine(state = {}, activeLine, fallbackSample
|
||||
return samples.length - 1;
|
||||
}
|
||||
|
||||
function resolveRuntimeSampleIndexForTaskHalPose(state = {}, axisPose = {}, activeLine, fallbackSampleIndex = 0) {
|
||||
const samples = state.programAxisPreviewPath?.samples;
|
||||
if (!Array.isArray(samples) || samples.length === 0) {
|
||||
return resolveRuntimeSampleIndexForLine(state, activeLine, fallbackSampleIndex);
|
||||
}
|
||||
const current = clampNumber(state.programExecutionSampleIndex || 0, 0, samples.length - 1);
|
||||
const previousCycle = Number(state.programRuntimeFeedback?.cycle || 0);
|
||||
const nextCycle = Number(fallbackSampleIndex || 0);
|
||||
const cycleDelta = Number.isFinite(nextCycle) && Number.isFinite(previousCycle)
|
||||
? Math.max(nextCycle - previousCycle, 0)
|
||||
: 0;
|
||||
const adaptiveWindow = Math.max(320, Math.min(samples.length - 1, Math.ceil(cycleDelta * 8) + 240));
|
||||
const windowStart = current;
|
||||
const windowEnd = Math.min(samples.length - 1, current + adaptiveWindow);
|
||||
const line = Number(activeLine);
|
||||
let bestIndex = current;
|
||||
let bestScore = Number.POSITIVE_INFINITY;
|
||||
for (let index = windowStart; index <= windowEnd; index += 1) {
|
||||
const sample = samples[index];
|
||||
const samplePose = axisPoseFromProgramPathSample(sample, axisPose, state.profile);
|
||||
const linePenalty = Number.isFinite(line) && Number.isFinite(Number(sample?.line))
|
||||
? Math.min(Math.abs(Number(sample.line) - line), 12) * 0.25
|
||||
: 0;
|
||||
const forwardPenalty = (index - current) * 0.000001;
|
||||
const score = taskHalPoseDistanceScore(axisPose, samplePose) + linePenalty + forwardPenalty;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestIndex = index;
|
||||
}
|
||||
}
|
||||
return Math.max(current, bestIndex);
|
||||
}
|
||||
|
||||
function taskHalPoseDistanceScore(a = {}, b = {}) {
|
||||
const linear = ["x", "y", "z"].reduce((sum, axis) => {
|
||||
const delta = Number(a[axis] || 0) - Number(b[axis] || 0);
|
||||
return sum + delta * delta;
|
||||
}, 0);
|
||||
const rotary = ["a", "b", "c"].reduce((sum, axis) => {
|
||||
const delta = Number(a[axis] || 0) - Number(b[axis] || 0);
|
||||
return sum + (delta * delta * 0.01);
|
||||
}, 0);
|
||||
return linear + rotary;
|
||||
}
|
||||
|
||||
function applyProgramPlaybackUiPatch(state = {}, {
|
||||
activeLine,
|
||||
axisPose,
|
||||
@@ -3744,9 +4071,13 @@ function applyProgramPlaybackUiPatch(state = {}, {
|
||||
motionIndex,
|
||||
sampleIndex,
|
||||
runtimeFeedback,
|
||||
preferRuntimeAxisPose = false,
|
||||
} = {}) {
|
||||
const sample = currentProgramPathSample(state, sampleIndex);
|
||||
const sampleAxisPose = axisPoseFromProgramPathSample(sample, axisPose || state.axisPose, state.profile);
|
||||
const runtimeAxisPose = clampAxisPoseToProfile(axisPose || runtimeFeedback?.axisPose || state.axisPose, state.profile);
|
||||
const sampleAxisPose = preferRuntimeAxisPose
|
||||
? runtimeAxisPose
|
||||
: axisPoseFromProgramPathSample(sample, runtimeAxisPose, state.profile);
|
||||
const sampleKinsType = sample?.activeKinematics
|
||||
? normalizeSampleKinsType(state, sample.activeKinematics)
|
||||
: kinsType;
|
||||
@@ -3764,7 +4095,7 @@ function applyProgramPlaybackUiPatch(state = {}, {
|
||||
axisPose: sampleAxisPose,
|
||||
kinsType: sampleKinsType || state.kinsType,
|
||||
toolAxisVector,
|
||||
});
|
||||
}, { preferRuntimeAxisPose });
|
||||
const patchState = {
|
||||
...state,
|
||||
activeLine: sampleLine,
|
||||
@@ -3785,11 +4116,13 @@ function applyProgramPlaybackUiPatch(state = {}, {
|
||||
programExecutionMotionIndex: patchState.programExecutionMotionIndex,
|
||||
programExecutionSampleIndex: patchState.programExecutionSampleIndex,
|
||||
programRuntimeFeedback: enrichedFeedback,
|
||||
programUiExecution: createProgramUiExecution(patchState),
|
||||
programUiExecution: createProgramUiExecution(patchState, { preferRuntimePose: preferRuntimeAxisPose }),
|
||||
};
|
||||
}
|
||||
|
||||
function createProgramUiExecution(state = {}) {
|
||||
function createProgramUiExecution(state = {}, {
|
||||
preferRuntimePose = false,
|
||||
} = {}) {
|
||||
const sample = currentProgramPathSample(state, state.programExecutionSampleIndex);
|
||||
const feedback = state.programRuntimeFeedback || {};
|
||||
const sampleCount = Number(state.programAxisPreviewPath?.sampleCount || state.programAxisPreviewPath?.samples?.length || 0);
|
||||
@@ -3829,8 +4162,12 @@ function createProgramUiExecution(state = {}) {
|
||||
segmentIndex: sample?.segmentIndex ?? traceEntry?.segmentIndex ?? null,
|
||||
motionType: sample?.motionType || traceEntry?.motionType || feedback.motionType || feedback.type || null,
|
||||
activeKinematics: sample?.activeKinematics || traceEntry?.activeKinematicsAfter || state.kinsType,
|
||||
joint: sample?.joint || pickExecutionAxes(state.axisPose || {}),
|
||||
tcp: sample?.tcp || {
|
||||
joint: preferRuntimePose
|
||||
? pickExecutionAxes(feedback.axisPose || state.axisPose || {})
|
||||
: sample?.joint || pickExecutionAxes(state.axisPose || {}),
|
||||
tcp: preferRuntimePose && feedback.tcp
|
||||
? feedback.tcp
|
||||
: sample?.tcp || {
|
||||
x: Number(state.tcpPose?.x || state.axisPose?.x || 0),
|
||||
y: Number(state.tcpPose?.y || state.axisPose?.y || 0),
|
||||
z: Number(state.tcpPose?.z || state.axisPose?.z || 0),
|
||||
@@ -3886,13 +4223,22 @@ function toolAxisSampleFromVector(vector = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function enrichRuntimeFeedbackWithSample(feedback = {}, sample = null, state = {}) {
|
||||
function enrichRuntimeFeedbackWithSample(feedback = {}, sample = null, state = {}, {
|
||||
preferRuntimeAxisPose = false,
|
||||
} = {}) {
|
||||
if (!sample) return feedback;
|
||||
const tcp = sample.tcp || {
|
||||
const runtimeAxisPose = feedback?.axisPose || state.axisPose || {};
|
||||
const tcp = preferRuntimeAxisPose
|
||||
? {
|
||||
x: Number(runtimeAxisPose.x ?? 0),
|
||||
y: Number(runtimeAxisPose.y ?? 0),
|
||||
z: Number(runtimeAxisPose.z ?? 0),
|
||||
}
|
||||
: sample.tcp || {
|
||||
x: Number(state.tcpPose?.x ?? state.axisPose?.x ?? feedback?.axisPose?.x ?? 0),
|
||||
y: Number(state.tcpPose?.y ?? state.axisPose?.y ?? feedback?.axisPose?.y ?? 0),
|
||||
z: Number(state.tcpPose?.z ?? state.axisPose?.z ?? feedback?.axisPose?.z ?? 0),
|
||||
};
|
||||
};
|
||||
return {
|
||||
...(feedback || {}),
|
||||
sampleIndex: Number(sample.sampleIndex || 0),
|
||||
@@ -3902,7 +4248,9 @@ function enrichRuntimeFeedbackWithSample(feedback = {}, sample = null, state = {
|
||||
statement: sample.statement || feedback?.statement || "",
|
||||
motionType: sample.motionType || feedback?.motionType || null,
|
||||
activeKinematics: sample.activeKinematics || feedback?.activeKinematics || state.kinsType,
|
||||
axisPose: axisPoseFromProgramPathSample(sample, state.axisPose || feedback?.axisPose || {}, state.profile),
|
||||
axisPose: preferRuntimeAxisPose
|
||||
? runtimeAxisPose
|
||||
: axisPoseFromProgramPathSample(sample, state.axisPose || feedback?.axisPose || {}, state.profile),
|
||||
tcp,
|
||||
toolAxis: sample.toolAxis || feedback?.toolAxis || null,
|
||||
machineState: sample.machineState || feedback?.machineState || null,
|
||||
@@ -4155,6 +4503,22 @@ function executeMdiCommand(state, rawCommand) {
|
||||
return { patch };
|
||||
}
|
||||
|
||||
function createManualTouchOffMdiPatch(state, patch, command) {
|
||||
return {
|
||||
...patch,
|
||||
machine: {
|
||||
...patch.machine,
|
||||
mode: "manual",
|
||||
manualPanel: state.machine.manualPanel || "manual",
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
runState: "idle",
|
||||
operatorMessage: `manual touch off ${command}`,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMdiCommand(command) {
|
||||
return String(command || "")
|
||||
.replace(/\([^)]*\)/g, " ")
|
||||
|
||||
@@ -167,6 +167,13 @@ body {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.axis-mode-toggle {
|
||||
width: 44px;
|
||||
min-width: 44px;
|
||||
font: 700 11px/1 Arial, sans-serif;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.axis-toolbar-separator {
|
||||
width: 8px;
|
||||
height: 26px;
|
||||
|
||||
@@ -24,6 +24,7 @@ export const AXIS_BUTTON_PARITY = [
|
||||
{ id: "menu-audit", action: "audit", sourceSymbol: "web parity audit", sourceLines: "collect-web-xyzbc-trt-evidence.mjs", expected: "request native/Web parity audit" },
|
||||
{ id: "toolbar-estop", action: "estop", sourceSymbol: "commands.estop_clicked", sourceLines: "axis.py:2223-2229", expected: "toggle ESTOP/ESTOP_RESET" },
|
||||
{ id: "toolbar-power", action: "power", sourceSymbol: "commands.onoff_clicked", sourceLines: "axis.py:2231-2241", expected: "toggle machine power" },
|
||||
{ id: "toolbar-auto-manual", action: "toggle-auto-manual", sourceSymbol: "commands.ensure_manual / commands.task_mode_auto", sourceLines: "axis.py:2308-2320,2520-2531", expected: "toggle task mode between AUTO and MANUAL" },
|
||||
{ id: "toolbar-load", action: "open", sourceSymbol: "commands.open_file", sourceLines: "axis.py:2243-2262", expected: "open local G-code" },
|
||||
{ id: "toolbar-reload", action: "reload", sourceSymbol: "commands.reload_file", sourceLines: "axis.py:2296-2297", expected: "reload active program" },
|
||||
{ id: "toolbar-run", action: "run", sourceSymbol: "commands.task_run", sourceLines: "axis.py:2308-2320", expected: "start AUTO run" },
|
||||
@@ -169,6 +170,7 @@ function renderToolbar(element, state, dispatch) {
|
||||
<input type="file" class="axis-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
|
||||
${toolButton("tbtn_estop", "estop", "Emergency stop", state.machine.estopActive)}
|
||||
${toolButton("tbtn_on", "power", "Machine power", state.machine.powerOn)}
|
||||
${modeToggleTool(state)}
|
||||
${toolButton("btn_load", "open", "Open program")}
|
||||
${toolButton("btn_reload", "reload", "Reload program")}
|
||||
${toolButton("btn_run", "run", "Run program")}
|
||||
@@ -560,6 +562,21 @@ function smallTextTool(label, action, title) {
|
||||
return `<button type="button" class="axis-tool-button axis-text-tool" data-action="${action}" title="${escapeHtml(title)}">${escapeHtml(label)}</button>`;
|
||||
}
|
||||
|
||||
function modeToggleTool(state) {
|
||||
const autoMode = state.machine?.mode === "auto";
|
||||
const label = autoMode ? "MAN" : "AUTO";
|
||||
const title = autoMode ? "Switch to Manual mode" : "Switch to Auto mode";
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="axis-tool-button axis-mode-toggle"
|
||||
data-action="toggle-auto-manual"
|
||||
data-mode="${autoMode ? "auto" : "manual"}"
|
||||
title="${escapeHtml(title)}"
|
||||
>${label}</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function menuButton(label, entries) {
|
||||
return `
|
||||
<div class="axis-menu">
|
||||
@@ -642,8 +659,9 @@ function runAxisCommand(command, state, dispatch, root) {
|
||||
const actionMap = {
|
||||
estop: () => dispatch({ type: state.machine?.estopActive || state.machine?.taskState === "estop" ? "RESET" : "ESTOP" }),
|
||||
power: () => dispatch({ type: "TOGGLE_POWER" }),
|
||||
"toggle-auto-manual": () => dispatch({ type: "SET_MODE", mode: state.machine?.mode === "auto" ? "manual" : "auto" }),
|
||||
reload: () => dispatch({ type: "RELOAD_PROGRAM" }),
|
||||
run: () => dispatch({ type: "RUN" }),
|
||||
run: () => dispatch({ type: "RUN_FROM_OPERATOR" }),
|
||||
pause: () => dispatch({ type: "PAUSE" }),
|
||||
resume: () => dispatch({ type: "RESUME" }),
|
||||
step: () => dispatch({ type: "STEP" }),
|
||||
@@ -652,8 +670,8 @@ function runAxisCommand(command, state, dispatch, root) {
|
||||
"home-all": () => dispatch({ type: "HOME" }),
|
||||
"jog-minus": () => dispatch({ type: "JOG", axis: selectedAxis, direction: -1, increment: Number(state.machine?.jogIncrement ?? 1) || 1 }),
|
||||
"jog-plus": () => dispatch({ type: "JOG", axis: selectedAxis, direction: 1, increment: Number(state.machine?.jogIncrement ?? 1) || 1 }),
|
||||
"touch-off": () => dispatch({ type: "RUN_MDI", command: `G10 L20 P0 ${selectedAxis.toUpperCase()}0` }),
|
||||
"tool-touch-off": () => dispatch({ type: "RUN_MDI", command: "G43" }),
|
||||
"touch-off": () => dispatch({ type: "RUN_MDI", command: `G10 L20 P0 ${selectedAxis.toUpperCase()}0`, manualTouchOff: true }),
|
||||
"tool-touch-off": () => dispatch({ type: "RUN_MDI", command: "G43", manualTouchOff: true }),
|
||||
"spindle-stop": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "stop" }),
|
||||
"spindle-forward": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" }),
|
||||
"spindle-reverse": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "reverse" }),
|
||||
|
||||
Reference in New Issue
Block a user