继续完成 web-rtcp-5axis-sim-plan

结论:完成 LinuxCNC kinematics WASM ABI 覆盖,并将 web-rtcp-5axis-sim-plan 的 RTCP frame/boundary adapter 接到 xyzac-trt kinematics SDK;Node、build、browser smoke 验证通过。
This commit is contained in:
2026-06-21 16:44:29 +08:00
parent a6eda3fbff
commit 626bcfe8e3
101 changed files with 101586 additions and 770 deletions

View File

@@ -0,0 +1,616 @@
import { buildRtcpFrame } from "../runtime/rtcp-frame.js";
import { createLinuxCncBoundaryAdapter, createLinuxCncBoundaryReadiness } from "../runtime/linuxcnc-boundary-adapter.js";
import { xyzacTrtProfile } from "../profiles/xyzac-trt.js";
const initialLinuxCncBoundaryAdapter = createLinuxCncBoundaryAdapter({
profile: xyzacTrtProfile,
});
const initialLinuxCncBoundaryReadiness = createLinuxCncBoundaryReadiness(initialLinuxCncBoundaryAdapter);
const initialAxisPose = {
x: 43.0,
y: -32.15,
z: -11.306,
a: 0.0,
b: 0.0,
c: 0.0,
};
const initialState = {
machineProfile: "xyzac-trt",
profile: xyzacTrtProfile,
sessionName: "gmoccapy-web-session",
sourceMode: "fixture-ui-only",
frameSourceMode: "fixture-ui-only",
machine: {
powerOn: false,
estopActive: false,
mode: "manual",
jogAxis: "x",
jogIncrement: 1,
mdiCommand: "G0 X0 Y0 Z0",
resetCount: 0,
},
runState: "idle",
activeProgram: "../../../linuxcnc/nc_files/3D_Chips.ngc",
programSource: "fixture",
programStartLine: 496,
activeLine: 501,
lineCount: 4711,
fileSizeBytes: 200509,
kinsType: "identity",
rtcpState: "off",
axisPose: initialAxisPose,
jointPose: [],
tcpPose: {
x: 43.0,
y: -32.15,
z: -11.306,
a: 0.0,
c: 0.0,
},
toolAxisVector: {
x: 0.0,
y: 0.0,
z: 1.0,
},
rtcpFrame: null,
kinematicsRuntime: null,
kinematicsRuntimeReadiness: null,
lastKinematicsResult: null,
linuxCncBoundaryAdapter: initialLinuxCncBoundaryAdapter,
linuxCncBoundaryReadiness: initialLinuxCncBoundaryReadiness,
dro: {
x: 43.0,
y: -32.15,
z: -11.306,
a: 0.0,
b: 0.0,
c: 0.0,
tcpX: 43.0,
tcpY: -32.15,
tcpZ: -11.306,
dtgX: 0.0,
dtgY: 0.01,
dtgZ: 2.25,
},
feed: {
currentVelocity: 2621,
rapidOverride: 100,
feedRate: 4500000,
feedOverride: 100,
},
spindle: {
rpm: 1600,
override: 100,
enabled: true,
},
coolant: {
flood: true,
mist: false,
},
preview: {
pathPoints: 64,
selectedView: "iso",
fullscreen: false,
},
toolPreview: {
toolNumber: 1,
diameter: 6,
length: 84.019,
units: "mm",
holder: "CAT40",
},
operatorMessage: "ready",
};
const programLines = [
"N4860 Y[#<yscale>*-39.009]",
"N4870 Y[#<yscale>*-32.524]",
"N4880 Y[#<yscale>*-32.384]",
"N4890 Y[#<yscale>*-32.267]",
"N4900 Y[#<yscale>*-32.235] Z[#<zscale>*-11.306]",
"N4910 Y[#<yscale>*-32.118] Z[#<zscale>*-11.312]",
"N4920 Y[#<yscale>*-32.103] Z[#<zscale>*-11.314]",
"N4930 Y[#<yscale>*-32.071] Z[#<zscale>*-11.316]",
"N4940 Y[#<yscale>*-31.972] Z[#<zscale>*-11.318]",
"N4950 Y[#<yscale>*-31.759] Z[#<zscale>*-11.320]",
"N4960 Y[#<yscale>*-31.509] Z[#<zscale>*-11.324]",
];
export function createSimulationStore(seed = {}) {
const seedAxisPose = seed.axisPose || initialAxisPose;
const seedKinsType = seed.kinsType || initialState.kinsType;
const seedRtcpState = seed.rtcpState || initialState.rtcpState;
const seedFrame = buildRtcpFrame({
axisPose: seedAxisPose,
activeLine: seed.activeLine || initialState.activeLine,
kinsType: seedKinsType,
rtcpEnabled: seedRtcpState === "on" || seedKinsType === "tcp-xyzac",
sourceMode: seed.sourceMode || seed.frameSourceMode || initialState.sourceMode,
});
let state = {
...initialState,
...seed,
axisPose: seedFrame.axisPose,
jointPose: seedFrame.jointPose,
tcpPose: seedFrame.tcpPose,
toolAxisVector: seedFrame.toolAxisVector,
rtcpState: seedFrame.rtcpState,
rtcpFrame: seedFrame,
dro: buildDroFromFrame(seedFrame),
programLines: seed.programLines || programLines,
};
const listeners = new Set();
const notify = () => {
for (const listener of listeners) {
listener(state);
}
};
const setState = (patch) => {
const next = { ...state, ...patch };
const frameState = buildFrameForState(next, patch);
const frame = patch.rtcpFrame || frameState.frame;
state = {
...next,
sourceMode: frame.sourceMode,
frameSourceMode: frame.sourceMode,
axisPose: frame.axisPose,
jointPose: frame.jointPose,
tcpPose: frame.tcpPose,
toolAxisVector: frame.toolAxisVector,
rtcpState: frame.rtcpState,
rtcpFrame: frame,
lastKinematicsResult: frameState.lastKinematicsResult,
dro: buildDroFromFrame(frame),
};
notify();
};
const dispatch = (action) => {
switch (action.type) {
case "BOOT_READY":
setState({ bootReady: true });
break;
case "ATTACH_KINEMATICS_RUNTIME":
{
const runtime = action.runtime || null;
const readiness = runtime?.readiness ? runtime.readiness() : null;
const runtimeDescriptor = runtime?.loaded
? {
apiName: runtime.apiName,
moduleId: runtime.moduleId,
wasmFile: runtime.wasmFile,
supportedModules: runtime.supportedModules,
loaded: runtime.loaded,
sourceMode: runtime.sourceMode,
semanticBoundary: runtime.semanticBoundary,
switchkinsType: runtime.switchkinsType,
switchRc: runtime.switchRc,
}
: null;
const adapter = createLinuxCncBoundaryAdapter({
profile: state.profile,
runtime: {
kinematicsWasm: runtimeDescriptor,
interpreterWasm: null,
},
});
setState({
kinematicsRuntime: runtime,
kinematicsRuntimeReadiness: readiness,
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",
operatorMessage: runtime?.loaded
? `LinuxCNC kinematics ${runtime.moduleId} ready`
: "LinuxCNC kinematics runtime missing",
});
}
break;
case "SET_FRAME_SOURCE":
setState({
sourceMode: action.sourceMode,
frameSourceMode: action.sourceMode,
operatorMessage: `frame source ${action.sourceMode}`,
});
break;
case "REFRESH_KINEMATICS_FRAME":
setState({
sourceMode: "source-derived-kinematics-wasm",
frameSourceMode: "source-derived-kinematics-wasm",
operatorMessage: "LinuxCNC kinematics frame refreshed",
});
break;
case "TOGGLE_POWER":
setState({
machine: {
...state.machine,
powerOn: !state.machine.powerOn,
},
runState: state.machine.powerOn ? "idle" : "powered-off",
operatorMessage: state.machine.powerOn ? "machine power off" : "machine power on",
});
break;
case "ESTOP":
setState({
machine: {
...state.machine,
powerOn: false,
estopActive: true,
},
runState: "estopped",
feed: {
...state.feed,
currentVelocity: 0,
},
operatorMessage: "emergency stop active",
});
break;
case "RESET":
setState({
machine: {
...state.machine,
estopActive: false,
resetCount: state.machine.resetCount + 1,
},
runState: "idle",
operatorMessage: "machine reset complete",
});
break;
case "SET_MODE":
setState({
machine: {
...state.machine,
mode: action.mode,
},
runState: state.runState === "running" ? "paused" : state.runState,
operatorMessage: `mode ${action.mode}`,
});
break;
case "JOG":
if (!canMoveMachine(state)) {
setState({ operatorMessage: "jog blocked: power or estop state" });
break;
}
{
const axis = action.axis || state.machine.jogAxis;
const direction = Number(action.direction || 1);
const increment = Number(action.increment || state.machine.jogIncrement);
setState({
machine: {
...state.machine,
mode: "jog",
jogAxis: axis,
jogIncrement: increment,
},
axisPose: {
...state.axisPose,
[axis]: Number(state.axisPose[axis] || 0) + direction * increment,
},
runState: "jogging",
operatorMessage: `jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}`,
});
}
break;
case "RUN_MDI":
if (!canMoveMachine(state)) {
setState({ operatorMessage: "MDI blocked: power or estop state" });
break;
}
setState({
machine: {
...state.machine,
mode: "mdi",
mdiCommand: action.command || state.machine.mdiCommand,
},
runState: "mdi",
operatorMessage: `MDI ${action.command || state.machine.mdiCommand}`,
});
break;
case "LOAD_PROGRAM":
{
const loadedProgram = buildLoadedProgram(action);
setState({
...loadedProgram,
machine: {
...state.machine,
mode: "auto",
},
axisPose: initialAxisPose,
runState: "idle",
preview: {
...state.preview,
pathPoints: Math.max(loadedProgram.programLines.length, 1),
},
operatorMessage: `loaded ${loadedProgram.activeProgram}`,
});
}
break;
case "RUN":
if (!canMoveMachine(state)) {
setState({ operatorMessage: "run blocked: power or estop state" });
break;
}
{
const nextLine = getNextProgramLine(state, 5);
const nextAxisPose = buildFixtureAxisPoseForLine(state.axisPose, nextLine);
setState({
machine: {
...state.machine,
mode: "auto",
},
runState: nextLine >= getProgramEndLine(state) ? "complete" : "running",
activeLine: nextLine,
axisPose: nextAxisPose,
operatorMessage: `executing line ${nextLine}`,
});
}
break;
case "STOP":
setState({
runState: "stopped",
operatorMessage: "program stopped",
});
break;
case "PAUSE":
setState({ runState: "paused", operatorMessage: "program paused" });
break;
case "STEP":
if (!canMoveMachine(state)) {
setState({ operatorMessage: "step blocked: power or estop state" });
break;
}
{
const nextLine = getNextProgramLine(state, 1);
const nextAxisPose = buildFixtureAxisPoseForLine(state.axisPose, nextLine);
setState({
machine: {
...state.machine,
mode: "auto",
},
runState: "stepping",
activeLine: nextLine,
axisPose: nextAxisPose,
operatorMessage: `stepped to line ${nextLine}`,
});
}
break;
case "RUN_FRAME":
if (!canMoveMachine(state)) {
setState({ operatorMessage: "run frame blocked: power or estop state" });
break;
}
{
const nextLine = getNextProgramLine(state, 5);
const nextAxisPose = buildFixtureAxisPoseForLine(state.axisPose, nextLine);
setState({
runState: "running",
activeLine: nextLine,
axisPose: nextAxisPose,
});
}
break;
case "SET_RTCP":
setState({
kinsType: action.enabled ? "tcp-xyzac" : "identity",
rtcpState: action.enabled ? "on" : "off",
});
break;
case "RESET_VIEW":
setState({
preview: { ...state.preview, selectedView: "iso" },
operatorMessage: "preview fit to program",
});
break;
case "CLEAR_PREVIEW":
setState({
preview: { ...state.preview, pathPoints: 0 },
operatorMessage: "preview path cleared",
});
break;
case "SET_VIEW":
setState({
preview: { ...state.preview, selectedView: action.view },
operatorMessage: `preview view ${action.view}`,
});
break;
case "TOGGLE_FULLSCREEN":
setState({
preview: { ...state.preview, fullscreen: !state.preview.fullscreen },
operatorMessage: state.preview.fullscreen ? "fullscreen preview off" : "fullscreen preview on",
});
break;
case "SET_KINS_TYPE":
setState({
kinsType: action.kinsType,
rtcpState: action.kinsType === "tcp-xyzac" ? "on" : "off",
operatorMessage: `kinematics ${action.kinsType}`,
});
break;
case "ADJUST_OVERRIDE":
setState({
feed: {
...state.feed,
[`${action.target}Override`]: clampPercent(
state.feed[`${action.target}Override`] + action.delta,
0,
200,
),
},
operatorMessage: `${action.target} override adjusted`,
});
break;
case "ADJUST_SPINDLE_OVERRIDE":
setState({
spindle: {
...state.spindle,
override: clampPercent(state.spindle.override + action.delta, 0, 150),
},
operatorMessage: "spindle override adjusted",
});
break;
case "TOGGLE_COOLANT":
setState({
coolant: {
...state.coolant,
[action.kind]: !state.coolant[action.kind],
},
operatorMessage: `${action.kind} coolant toggled`,
});
break;
case "HOME":
if (!canMoveMachine(state)) {
setState({ operatorMessage: "home blocked: power or estop state" });
break;
}
setState({
runState: "idle",
axisPose: initialAxisPose,
operatorMessage: "machine homed to fixture origin",
});
break;
case "RELOAD_PROGRAM":
setState({
runState: "idle",
activeLine: state.programStartLine === 496 ? 501 : state.programStartLine,
axisPose: initialAxisPose,
preview: { ...state.preview, pathPoints: Math.max(state.programLines.length, 1) },
operatorMessage: "program reloaded",
});
break;
default:
throw new Error(`Unknown action type: ${action.type}`);
}
};
return {
getState: () => state,
subscribe(listener) {
listeners.add(listener);
listener(state);
return () => listeners.delete(listener);
},
dispatch,
};
}
function buildFrameForState(state, patch = {}) {
const requestedSourceMode = 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) {
linuxCncKinematicsResult = state.kinematicsRuntime.frameForJoints(
jointsFromAxisPose(state.axisPose),
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
);
} else {
sourceMode = "fixture-ui-only";
linuxCncKinematicsResult = null;
operatorMessage = "LinuxCNC kinematics runtime missing; using fixture frame";
}
}
const frame = buildRtcpFrame({
axisPose: state.axisPose,
activeLine: state.activeLine,
kinsType: state.kinsType,
rtcpEnabled: state.rtcpState === "on" || state.kinsType === "tcp-xyzac",
sourceMode,
profile: state.profile,
linuxCncKinematicsResult,
});
if (operatorMessage !== state.operatorMessage) {
state.operatorMessage = operatorMessage;
}
return {
frame,
lastKinematicsResult: linuxCncKinematicsResult,
};
}
function canMoveMachine(state) {
return state.machine.powerOn && !state.machine.estopActive;
}
function getProgramEndLine(state) {
return state.programStartLine + Math.max(state.programLines.length - 1, 0);
}
function getNextProgramLine(state, step) {
return Math.min(state.activeLine + step, getProgramEndLine(state));
}
function buildLoadedProgram(action) {
const content = String(action.content || "");
const lines = parseProgramLines(content);
const filename = action.filename || "operator-program.ngc";
return {
activeProgram: filename,
programSource: "operator-file",
programStartLine: 1,
activeLine: 1,
lineCount: lines.length,
fileSizeBytes: content.length,
programLines: lines,
};
}
function parseProgramLines(content) {
const lines = content
.split(/\r?\n/)
.map((line) => line.trimEnd())
.filter((line) => line.trim().length > 0);
return lines.length > 0 ? lines : ["(empty program)"];
}
function clampPercent(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function buildDroFromFrame(frame) {
return {
x: frame.axisPose.x,
y: frame.axisPose.y,
z: frame.axisPose.z,
a: frame.axisPose.a,
b: frame.axisPose.b,
c: frame.axisPose.c,
tcpX: frame.tcpPose.x,
tcpY: frame.tcpPose.y,
tcpZ: frame.tcpPose.z,
dtgX: frame.rtcpEnabled ? Math.abs(frame.compensation.x) : 0,
dtgY: frame.rtcpEnabled ? Math.abs(frame.compensation.y) : 0.01,
dtgZ: frame.rtcpEnabled ? Math.abs(frame.compensation.z) : 2.25,
};
}
function jointsFromAxisPose(axisPose) {
return [
Number(axisPose.x || 0),
Number(axisPose.y || 0),
Number(axisPose.z || 0),
Number(axisPose.a || 0),
Number(axisPose.c || 0),
];
}
function buildFixtureAxisPoseForLine(axisPose, line) {
const phase = (line - 496) * 0.17;
return {
...axisPose,
x: 43 + Math.sin(phase) * 4,
y: -32.15 + Math.cos(phase) * 2.5,
z: -11.306 + Math.sin(phase * 0.7) * 1.2,
a: Math.sin(phase * 0.45) * 18,
c: Math.cos(phase * 0.33) * 32,
};
}