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; }