接入 LinuxCNC TP 运行反馈

This commit is contained in:
2026-06-21 23:29:56 +08:00
parent 626bcfe8e3
commit 3771b9eafe
44 changed files with 7881 additions and 326 deletions

View File

@@ -0,0 +1,228 @@
import createLinuxCncTpModule from "../../../build/wasm/tp/linuxcnc_tp.js";
const SEMANTIC_BOUNDARY = "linuxcnc_tp_queue_runtime_timing_from_canonical_motion";
function allocCString(mod, value) {
const bytes = mod.lengthBytesUTF8(value) + 1;
const ptr = mod._malloc(bytes);
mod.stringToUTF8(value, ptr, bytes);
return ptr;
}
function callTiming(mod, payload) {
const inputPtr = allocCString(mod, JSON.stringify(payload));
let resultPtr = 0;
try {
resultPtr = mod._lctp_run_canonical_motion_timing(inputPtr);
if (!resultPtr) {
throw new Error("lctp_run_canonical_motion_timing returned null");
}
const text = mod.UTF8ToString(resultPtr);
const result = JSON.parse(text);
if (result.ok !== true) {
throw new Error(result.error || "LinuxCNC TP queue timing failed");
}
return result;
} finally {
if (resultPtr) mod._lctp_free_string(resultPtr);
mod._free(inputPtr);
}
}
export async function createLinuxCncTpSdk(moduleOptions = {}) {
const mod = await createLinuxCncTpModule(moduleOptions);
return {
apiName: "linuxcnc-tp-wasm-sdk",
semanticBoundary: SEMANTIC_BOUNDARY,
module: mod,
readiness() {
return {
apiName: "linuxcnc-tp-wasm-sdk-readiness",
loaded: true,
semanticBoundary: SEMANTIC_BOUNDARY,
runCanonicalMotionTimingReady: typeof mod._lctp_run_canonical_motion_timing === "function",
};
},
runCanonicalMotionTiming({ motion = [], options = {} } = {}) {
const result = callTiming(mod, {
...options,
motion: motion.map((event, index) => ({
index,
line: Number.isFinite(Number(event.line)) ? Number(event.line) : -1,
type: event.type,
feedRate: Number.isFinite(Number(event.feedRate)) ? Number(event.feedRate) : 0,
x: numberOrZero(event.axes?.x),
y: numberOrZero(event.axes?.y),
z: numberOrZero(event.axes?.z),
a: numberOrZero(event.axes?.a),
b: numberOrZero(event.axes?.b),
c: numberOrZero(event.axes?.c),
u: numberOrZero(event.axes?.u),
v: numberOrZero(event.axes?.v),
w: numberOrZero(event.axes?.w),
plane: numberOrZero(event.axes?.arc?.plane),
centerFirst: numberOrZero(event.axes?.arc?.centerFirst),
centerSecond: numberOrZero(event.axes?.arc?.centerSecond),
rotation: numberOrZero(event.axes?.arc?.rotation),
axisEndPoint: numberOrZero(event.axes?.arc?.axisEndPoint),
})),
});
return normalizeTimingResult(result, motion);
},
};
}
function normalizeTimingResult(result, motion) {
const rawSegmentsByIndex = new Map((result.segments || []).map((segment) => [segment.index, segment]));
let elapsedSeconds = 0;
const segments = motion.map((event, index) => {
const raw = rawSegmentsByIndex.get(index);
const durationSeconds = Math.max(Number(raw?.durationSeconds) || 0, 0);
elapsedSeconds += durationSeconds;
return {
index,
line: event.line ?? null,
type: event.type,
motionClass: event.type === "STRAIGHT_TRAVERSE" ? "rapid" : "feed",
durationSeconds,
elapsedSeconds,
currentVelocity: Number(raw?.velocity) || 0,
velocityMmPerMin: (Number(raw?.velocity) || 0) * 60,
queueDepth: Number(raw?.queueDepth) || 0,
activeDepth: Number(raw?.activeDepth) || 0,
cycles: Number(raw?.cycles) || 0,
axes: event.axes || {},
runtimeAxes: axesFromRuntimeRecord(raw, event.axes || {}),
tp: raw || null,
};
});
const rawSamples = Array.isArray(result.samples) && result.samples.length > 0
? result.samples
: synthesizeSamplesFromSegments(result.segments || []);
const samples = normalizeSamples(rawSamples);
const feedSeconds = segments
.filter((segment) => segment.motionClass === "feed")
.reduce((total, segment) => total + segment.durationSeconds, 0);
const rapidSeconds = segments
.filter((segment) => segment.motionClass === "rapid")
.reduce((total, segment) => total + segment.durationSeconds, 0);
return {
apiName: "web-rtcp-5axis-linuxcnc-tp-program-timing",
semanticBoundary: SEMANTIC_BOUNDARY,
sourceBasis: "LinuxCNC interpreter canonical motion events queued through LinuxCNC src/emc/tp WASM",
plannerRuntimeReady: result.ok === true
&& result.addFailures === 0
&& result.tpDone === 1
&& segments.length === motion.length,
sampleCount: Number(result.sampleCount) || samples.length,
samples,
totalSeconds: elapsedSeconds,
totalMinutes: elapsedSeconds / 60,
feedSeconds,
rapidSeconds,
motionCount: motion.length,
emittedMotionCount: result.emittedMotionCount || 0,
acceptedMotionCount: result.acceptedMotionCount || 0,
totalCycles: result.totalCycles || 0,
cycleTime: result.cycleTime || 0.001,
addFailures: result.addFailures || 0,
tpDone: result.tpDone === 1,
finalQueueDepth: result.finalQueueDepth || 0,
segments,
raw: result,
};
}
function normalizeSamples(samples) {
return samples.map((sample, index) => ({
sampleIndex: Number(sample.sampleIndex ?? index),
cycle: Number(sample.cycle) || 0,
timeSeconds: Number(sample.timeSeconds) || 0,
motionIndex: Number(sample.motionIndex) || 0,
line: Number.isFinite(Number(sample.line)) ? Number(sample.line) : null,
type: sample.type || "-",
axes: {
x: numberOrZero(sample.x),
y: numberOrZero(sample.y),
z: numberOrZero(sample.z),
a: numberOrZero(sample.a),
b: numberOrZero(sample.b),
c: numberOrZero(sample.c),
u: numberOrZero(sample.u),
v: numberOrZero(sample.v),
w: numberOrZero(sample.w),
},
currentVelocity: numberOrZero(sample.currentVelocity),
currentVelocityMmPerMin: numberOrZero(sample.currentVelocity) * 60,
requestedVelocity: numberOrZero(sample.requestedVelocity),
requestedVelocityMmPerMin: numberOrZero(sample.requestedVelocity) * 60,
distanceToGo: numberOrZero(sample.distanceToGo),
dtg: {
x: numberOrZero(sample.dtgX),
y: numberOrZero(sample.dtgY),
z: numberOrZero(sample.dtgZ),
},
queueDepth: Number(sample.queueDepth) || 0,
activeDepth: Number(sample.activeDepth) || 0,
}));
}
function synthesizeSamplesFromSegments(segments) {
return segments.map((segment, index) => ({
sampleIndex: index,
cycle: Number(segment.cycles) || 0,
timeSeconds: Number(segment.elapsedSeconds) || 0,
motionIndex: Number(segment.index) || index,
line: segment.line,
type: segment.type,
x: segment.x,
y: segment.y,
z: segment.z,
a: segment.a,
b: segment.b,
c: segment.c,
u: segment.u,
v: segment.v,
w: segment.w,
currentVelocity: segment.velocity,
requestedVelocity: segment.velocity,
distanceToGo: segment.distanceToGo ?? 0,
dtgX: segment.dtgX ?? 0,
dtgY: segment.dtgY ?? 0,
dtgZ: segment.dtgZ ?? 0,
queueDepth: segment.queueDepth,
activeDepth: segment.activeDepth,
}));
}
function axesFromRuntimeRecord(record, fallback = {}) {
return {
x: numberOrFallback(record?.x, fallback.x, 0),
y: numberOrFallback(record?.y, fallback.y, 0),
z: numberOrFallback(record?.z, fallback.z, 0),
a: numberOrFallback(record?.a, fallback.a, 0),
b: numberOrFallback(record?.b, fallback.b, 0),
c: numberOrFallback(record?.c, fallback.c, 0),
u: numberOrFallback(record?.u, fallback.u, 0),
v: numberOrFallback(record?.v, fallback.v, 0),
w: numberOrFallback(record?.w, fallback.w, 0),
};
}
function numberOrFallback(...values) {
for (const value of values) {
const number = Number(value);
if (Number.isFinite(number)) return number;
}
return 0;
}
function numberOrZero(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
}