Add RTCP simulation QA updates

This commit is contained in:
2026-06-22 09:30:27 -04:00
parent 61e2fe8441
commit ea8e10031b
51 changed files with 10008 additions and 178 deletions

View File

@@ -82,6 +82,7 @@ const initialState = {
},
sourceMode: "fixture-ui-only",
frameSourceMode: "fixture-ui-only",
desiredFrameSourceMode: "fixture-ui-only",
machine: {
powerOn: false,
estopActive: false,
@@ -142,6 +143,7 @@ const initialState = {
taskHalExecutionPending: false,
taskHalExecutionSequence: 0,
taskHalFallbackReason: null,
pendingJogCommand: null,
interpreterExecutionPending: false,
interpreterExecutionSequence: 0,
machineFileExecution: null,
@@ -230,7 +232,7 @@ export function createSimulationStore(seed = {}) {
activeLine: seed.activeLine || initialState.activeLine,
kinsType: seedKinsType,
rtcpEnabled: seedRtcpState === "on" || seedKinsType === "tcp-xyzac",
sourceMode: seed.sourceMode || seed.frameSourceMode || initialState.sourceMode,
sourceMode: seed.desiredFrameSourceMode || seed.sourceMode || seed.frameSourceMode || initialState.sourceMode,
});
let state = {
...initialState,
@@ -268,6 +270,7 @@ export function createSimulationStore(seed = {}) {
...next,
sourceMode: frame.sourceMode,
frameSourceMode: frame.sourceMode,
desiredFrameSourceMode: next.desiredFrameSourceMode || frame.sourceMode,
axisPose: frame.axisPose,
jointPose: frame.jointPose,
tcpPose: frame.tcpPose,
@@ -321,8 +324,7 @@ export function createSimulationStore(seed = {}) {
kinematicsExecutionContext: runtime?.executionContext || (runtime?.loaded ? "direct" : "none"),
linuxCncBoundaryAdapter: adapter,
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
sourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
frameSourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
desiredFrameSourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
operatorMessage: runtime?.loaded
? `LinuxCNC kinematics ${runtime.moduleId} ready`
: "LinuxCNC kinematics runtime missing",
@@ -803,6 +805,7 @@ export function createSimulationStore(seed = {}) {
setState({
taskHalFallbackReason: action.error,
taskHalExecutionPending: false,
pendingJogCommand: null,
operatorMessage: `task/HAL fallback: ${action.error}`,
});
break;
@@ -833,6 +836,7 @@ export function createSimulationStore(seed = {}) {
setState({
sourceMode: action.sourceMode,
frameSourceMode: action.sourceMode,
desiredFrameSourceMode: action.sourceMode,
operatorMessage: `frame source ${action.sourceMode}`,
});
break;
@@ -971,6 +975,13 @@ export function createSimulationStore(seed = {}) {
const direction = Number(action.direction || 1);
const increment = Number(action.increment || state.machine.jogIncrement);
if (state.taskHalRuntime?.loaded) {
const pendingJogCommand = {
axis,
direction,
increment,
basePose: { ...state.axisPose },
createdAtLine: state.activeLine,
};
runTaskHalCommandSequence([
{
type: "EMC_JOG_INCR",
@@ -978,7 +989,10 @@ export function createSimulationStore(seed = {}) {
distance: direction * increment,
velocity: Number(action.velocity || 60),
},
], { operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}` }).catch(() => {});
], {
pendingJogCommand,
operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}`,
}).catch(() => {});
break;
}
setState({
@@ -1371,6 +1385,9 @@ export function createSimulationStore(seed = {}) {
};
const refreshAsyncKinematicsFrame = async ({ operatorMessage = state.operatorMessage } = {}) => {
if (state.desiredFrameSourceMode !== "source-derived-kinematics-wasm") {
return state.rtcpFrame;
}
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
return state.rtcpFrame;
}
@@ -1381,44 +1398,58 @@ export function createSimulationStore(seed = {}) {
asyncFrameRefreshSequence: sequence,
};
notify();
await switchKinematicsRuntimeForState(state);
const frameSource = await state.kinematicsRuntime.frameForJoints(
jointsFromAxisPose(state.axisPose, state.profile),
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
);
if (state.asyncFrameRefreshSequence !== sequence) {
try {
await switchKinematicsRuntimeForState(state);
const frameSource = await state.kinematicsRuntime.frameForJoints(
jointsFromAxisPose(state.axisPose, state.profile),
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
);
if (state.asyncFrameRefreshSequence !== sequence) {
return state.rtcpFrame;
}
const frame = buildRtcpFrame({
axisPose: state.axisPose,
activeLine: state.activeLine,
kinsType: state.kinsType,
rtcpEnabled: state.rtcpState === "on" || state.kinsType.startsWith("tcp-"),
sourceMode: "source-derived-kinematics-wasm",
profile: state.profile,
linuxCncKinematicsResult: frameSource,
});
const nextState = {
...state,
sourceMode: "source-derived-kinematics-wasm",
frameSourceMode: "source-derived-kinematics-wasm",
desiredFrameSourceMode: "source-derived-kinematics-wasm",
axisPose: frame.axisPose,
jointPose: frame.jointPose,
tcpPose: frame.tcpPose,
toolAxisVector: frame.toolAxisVector,
rtcpState: frame.rtcpState,
rtcpFrame: frame,
lastKinematicsResult: frameSource,
dro: buildDroFromFrame(frame, state.programRuntimeFeedback),
asyncFrameRefreshPending: false,
operatorMessage,
};
state = {
...nextState,
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(nextState),
};
notify();
return frame;
} catch (error) {
if (state.asyncFrameRefreshSequence !== sequence) {
return state.rtcpFrame;
}
state = {
...state,
asyncFrameRefreshPending: false,
operatorMessage: `LinuxCNC kinematics refresh failed: ${error instanceof Error ? error.message : String(error)}`,
};
notify();
return state.rtcpFrame;
}
const frame = buildRtcpFrame({
axisPose: state.axisPose,
activeLine: state.activeLine,
kinsType: state.kinsType,
rtcpEnabled: state.rtcpState === "on" || state.kinsType.startsWith("tcp-"),
sourceMode: "source-derived-kinematics-wasm",
profile: state.profile,
linuxCncKinematicsResult: frameSource,
});
const nextState = {
...state,
sourceMode: frame.sourceMode,
frameSourceMode: frame.sourceMode,
axisPose: frame.axisPose,
jointPose: frame.jointPose,
tcpPose: frame.tcpPose,
toolAxisVector: frame.toolAxisVector,
rtcpState: frame.rtcpState,
rtcpFrame: frame,
lastKinematicsResult: frameSource,
dro: buildDroFromFrame(frame, state.programRuntimeFeedback),
asyncFrameRefreshPending: false,
operatorMessage,
};
state = {
...nextState,
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(nextState),
};
notify();
return frame;
};
const saveSession = async (options = {}) => {
@@ -1543,6 +1574,7 @@ export function createSimulationStore(seed = {}) {
taskPeriodNs = 10000000,
servoPeriodNs = 1000000,
operatorMessage = "task/HAL command complete",
pendingJogCommand = null,
} = {}) => {
if (!state.taskHalRuntime?.loaded) {
throw new Error("LinuxCNC task/HAL runtime not attached");
@@ -1551,6 +1583,7 @@ export function createSimulationStore(seed = {}) {
setState({
taskHalExecutionPending: true,
taskHalExecutionSequence: sequence,
pendingJogCommand,
operatorMessage: "LinuxCNC task/HAL command running",
});
try {
@@ -1614,13 +1647,21 @@ export function createSimulationStore(seed = {}) {
};
const scheduleAsyncKinematicsRefresh = () => {
if (state.asyncFrameRefreshPending) return null;
if (state.desiredFrameSourceMode !== "source-derived-kinematics-wasm") return null;
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) return null;
if (state.frameSourceMode !== "source-derived-kinematics-wasm") return null;
const frame = state.rtcpFrame;
if (
frame?.sourceMode === "source-derived-kinematics-wasm" &&
frame.readiness?.linuxCncKinematicsReady === true &&
frame.activeLine === state.activeLine
frame.activeLine === state.activeLine &&
frame.kinsType === state.kinsType &&
frame.axisPose?.x === state.axisPose.x &&
frame.axisPose?.y === state.axisPose.y &&
frame.axisPose?.z === state.axisPose.z &&
frame.axisPose?.a === state.axisPose.a &&
frame.axisPose?.b === state.axisPose.b &&
frame.axisPose?.c === state.axisPose.c
) {
return null;
}
@@ -1645,10 +1686,9 @@ export function createSimulationStore(seed = {}) {
}
function buildFrameForState(state, patch = {}) {
const requestedSourceMode = state.frameSourceMode || state.sourceMode;
const requestedSourceMode = state.desiredFrameSourceMode || state.frameSourceMode || state.sourceMode;
let linuxCncKinematicsResult = patch.lastKinematicsResult || null;
let sourceMode = requestedSourceMode;
let operatorMessage = state.operatorMessage;
if (requestedSourceMode === "source-derived-kinematics-wasm") {
if (state.kinematicsRuntime?.loaded && !isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
@@ -1657,10 +1697,12 @@ function buildFrameForState(state, patch = {}) {
jointsFromAxisPose(state.axisPose, state.profile),
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
);
} else if (state.kinematicsRuntime?.loaded && isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
sourceMode = "fixture-ui-only";
linuxCncKinematicsResult = null;
} else {
sourceMode = "fixture-ui-only";
linuxCncKinematicsResult = null;
operatorMessage = "LinuxCNC kinematics runtime missing; using fixture frame";
}
}
@@ -1674,10 +1716,6 @@ function buildFrameForState(state, patch = {}) {
linuxCncKinematicsResult,
});
if (operatorMessage !== state.operatorMessage) {
state.operatorMessage = operatorMessage;
}
return {
frame,
lastKinematicsResult: linuxCncKinematicsResult,
@@ -1743,11 +1781,8 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine.mode);
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
const kinsType = kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
const axisPose = clampAxisPoseToProfile({
...state.axisPose,
...ui.axisPose,
}, state.profile);
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
const axisPose = resolveTaskHalAxisPose(state, status);
const currentVelocity = Number.isFinite(ui.currentVelocity) && ui.currentVelocity > 0
? ui.currentVelocity
: state.feed.currentVelocity;
@@ -1772,11 +1807,14 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
taskHalStatus: status,
taskHalExecutionPending: false,
taskHalFallbackReason: null,
pendingJogCommand: null,
activeLine,
axisPose,
kinsType,
rtcpState: rtcpStateFromKinsType(kinsType),
programExecutionSourceMode: "linuxcnc-task-motion-hal-wasm",
programExecutionSourceMode: state.programExecution
? state.programExecutionSourceMode
: "linuxcnc-task-motion-hal-wasm",
machine: {
...state.machine,
powerOn: taskState === "on",
@@ -1797,6 +1835,93 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
};
}
function resolveTaskHalKinsType(state, status, activeLine) {
const ui = status?.ui || {};
const numeric = Number(ui.switchkinsType);
if (Number.isFinite(numeric) && numeric !== 0) {
return kinsTypeFromSwitchkinsTypeValue(state, numeric);
}
const programKinsType = kinsTypeFromProgramActiveLine(state, activeLine);
if (programKinsType) {
return programKinsType;
}
return kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
}
function kinsTypeFromProgramActiveLine(state, activeLine) {
const motion = programMotionAtOrBeforeLine(state, activeLine)
|| state.programExecution?.motion?.[clampMotionIndex(state, state.programExecutionMotionIndex)];
return kinsTypeFromProgramMotion(state, motion);
}
function programMotionAtOrBeforeLine(state, activeLine) {
const motion = state.programExecution?.motion;
if (!Array.isArray(motion) || motion.length === 0) return null;
const line = Number(activeLine);
if (!Number.isFinite(line)) return null;
let candidate = null;
for (const item of motion) {
const itemLine = Number(item?.line);
if (!Number.isFinite(itemLine)) continue;
if (itemLine > line) break;
candidate = item;
}
return candidate;
}
function resolveTaskHalAxisPose(state, status) {
const ui = status?.ui || {};
const axisPose = ui.axisPose;
if (!axisPose || typeof axisPose !== "object") {
return state.axisPose;
}
if (ui.axisPoseFrame === "work") {
return clampAxisPoseToProfile({ ...state.axisPose, ...axisPose }, state.profile);
}
if (ui.axisPoseDelta && typeof ui.axisPoseDelta === "object") {
return addAxisDelta(state.axisPose, ui.axisPoseDelta, state.profile);
}
if (state.pendingJogCommand && isJogStatus(status)) {
const { axis, direction, increment, basePose } = state.pendingJogCommand;
return clampAxisPoseToProfile({
...basePose,
[axis]: Number(basePose?.[axis] || 0) + direction * increment,
}, state.profile);
}
if (!ui.axisPoseFrame && wouldResetNonZeroPoseToLocalZero(state.axisPose, axisPose)) {
return state.axisPose;
}
return clampAxisPoseToProfile({ ...state.axisPose, ...axisPose }, state.profile);
}
function addAxisDelta(axisPose, delta, profile) {
const next = { ...axisPose };
for (const axis of ["x", "y", "z", "a", "b", "c"]) {
if (!Number.isFinite(Number(delta[axis]))) continue;
next[axis] = Number(next[axis] || 0) + Number(delta[axis]);
}
return clampAxisPoseToProfile(next, profile);
}
function isJogStatus(status) {
const motion = status?.motionStatus?.motion || {};
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
}
function wouldResetNonZeroPoseToLocalZero(currentPose = {}, nextPose = {}) {
const axes = ["x", "y", "z", "a", "b", "c"];
const currentHasNonZero = axes.some((axis) => Math.abs(Number(currentPose[axis] || 0)) > 0.001);
const nextIsNearZero = axes.every((axis) => Math.abs(Number(nextPose[axis] || 0)) <= 0.001);
return currentHasNonZero && nextIsNearZero;
}
function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
const ui = status?.ui || {};
const motion = status?.motionStatus?.motion || {};