提交 xyzbc-trt 界面与验证更新

This commit is contained in:
mes123456
2026-07-02 20:25:37 -04:00
parent 68ecd05353
commit 370c344b96
868 changed files with 275426 additions and 39640 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,228 @@
import createLinuxCncInterpModule from "../../../build/wasm/core/linuxcnc_interp.js";
import { createVirtualHalWasmBridgeSnapshot } from "./linuxcnc-hal.js";
function allocCString(mod, value) {
const bytes = mod.lengthBytesUTF8(value) + 1;
const ptr = mod._malloc(bytes);
mod.stringToUTF8(value, ptr, bytes);
return ptr;
}
function ensureParentPath(mod, path) {
const parts = path.split("/").filter(Boolean);
let current = "";
for (const part of parts.slice(0, -1)) {
current += `/${part}`;
try {
mod.FS.mkdir(current);
} catch {
// Directory already exists.
}
}
}
function copyResultString(mod, resultPtr, functionName) {
if (!resultPtr) {
throw new Error(`${functionName} returned null`);
}
return mod.UTF8ToString(resultPtr);
}
function callStringResult(mod, functionName, ...values) {
const ptrs = values.map((value) => allocCString(mod, value));
let resultPtr = 0;
try {
resultPtr = mod[`_${functionName}`](...ptrs);
return copyResultString(mod, resultPtr, functionName);
} finally {
if (resultPtr) {
mod._lcinterp_free_string(resultPtr);
}
for (const ptr of ptrs) {
mod._free(ptr);
}
}
}
function requireWasmFunction(mod, functionName) {
const fn = mod[`_${functionName}`];
if (typeof fn !== "function") {
throw new Error(`linuxcnc interpreter WASM missing ${functionName}; rebuild wasm-port/tools/build_wasm_core.sh`);
}
return fn;
}
function callVoidWithStringsAndNumbers(mod, functionName, strings, numbers = []) {
const fn = requireWasmFunction(mod, functionName);
const ptrs = strings.map((value) => allocCString(mod, value));
try {
return fn(...ptrs, ...numbers);
} finally {
for (const ptr of ptrs) {
mod._free(ptr);
}
}
}
export async function createLinuxCncInterpSdk(moduleOptions = {}) {
const mod = await createLinuxCncInterpModule(moduleOptions);
return {
module: mod,
hasWasmFunction(functionName) {
return typeof mod[`_${functionName}`] === "function";
},
writeTextFile(path, text) {
ensureParentPath(mod, path);
mod.FS.writeFile(path, text, { encoding: "utf8" });
},
readTextFile(path) {
return mod.FS.readFile(path, { encoding: "utf8" });
},
runSimConfigProgram({
iniPath,
programPath,
files = [],
executionMode = "fileWithIni",
}) {
for (const file of files) {
ensureParentPath(mod, file.path);
mod.FS.writeFile(file.path, file.text, { encoding: "utf8" });
if (file.executable) {
mod.FS.chmod(file.path, 0o755);
}
}
if (executionMode === "fiveAxisRemap") {
return callStringResult(
mod,
"lcinterp_run_fiveaxis_remap_file",
programPath,
iniPath,
);
}
if (executionMode === "fileWithIni") {
return callStringResult(mod, "lcinterp_run_file_with_ini", programPath, iniPath);
}
throw new Error(`unsupported sim config execution mode: ${executionMode}`);
},
runProgram(programText) {
return callStringResult(mod, "lcinterp_run_program", programText);
},
runProgramWithIni(programText, iniPath) {
return callStringResult(mod, "lcinterp_run_program_with_ini", programText, iniPath);
},
runFile(path) {
return callStringResult(mod, "lcinterp_run_file", path);
},
runFileWithIni(path, iniPath) {
return callStringResult(mod, "lcinterp_run_file_with_ini", path, iniPath);
},
runFileWithIniContinueOnError(path, iniPath) {
return callStringResult(
mod,
"lcinterp_run_file_with_ini_continue_on_error",
path,
iniPath,
);
},
runFiveAxisRemapFile(path, iniPath) {
return callStringResult(mod, "lcinterp_run_fiveaxis_remap_file", path, iniPath);
},
runRemapFile(path, iniPath) {
return callStringResult(mod, "lcinterp_run_remap_file", path, iniPath);
},
runRemapFileContinueOnError(path, iniPath) {
return callStringResult(mod, "lcinterp_run_remap_file_continue_on_error", path, iniPath);
},
runRemapIoMdiSequence(iniPath) {
return callStringResult(mod, "lcinterp_run_remap_io_mdi_sequence", iniPath);
},
restoreParameters(path) {
return callStringResult(mod, "lcinterp_restore_parameters", path);
},
saveParameters(path, values) {
const assignments = Object.entries(values)
.map(([parameter, value]) => `${parameter} ${value}`)
.join("\n");
return callStringResult(mod, "lcinterp_save_parameters", path, assignments);
},
loadToolTable(path, options = {}) {
const functionName = options.randomToolChanger
? "lcinterp_load_tool_table_random"
: "lcinterp_load_tool_table";
return callStringResult(mod, functionName, path);
},
saveToolTable(path) {
return callStringResult(mod, "lcinterp_save_tool_table", path);
},
probeInitAndSynch() {
return callStringResult(mod, "lcinterp_probe_init_and_synch");
},
probeIndexer() {
return callStringResult(mod, "lcinterp_probe_indexer");
},
probeNamedParameters(iniPath) {
return callStringResult(mod, "lcinterp_probe_named_parameters", iniPath);
},
resetHal() {
requireWasmFunction(mod, "lcinterp_hal_reset")();
},
setHalValue({ kind = "pin", name, type = "HAL_FLOAT", value = 0, connected = true }) {
if (!name) {
throw new Error("setHalValue requires a HAL name");
}
return callVoidWithStringsAndNumbers(
mod,
"lcinterp_hal_set_value",
[kind, name, type],
[Number(value) || 0, connected === false ? 0 : 1],
);
},
applyVirtualHalSnapshot(snapshot, options = {}) {
if (options.reset !== false) {
this.resetHal();
}
for (const value of snapshot?.values ?? []) {
this.setHalValue(value);
}
return {
apiName: "linuxcnc-wasm-hal-apply-result",
applied: snapshot?.values?.length ?? 0,
reset: options.reset !== false,
source: snapshot?.source ?? "unknown",
};
},
applyVirtualHalState(halState, options = {}) {
return this.applyVirtualHalSnapshot(createVirtualHalWasmBridgeSnapshot(halState), options);
},
probeHalNamed(name) {
return callStringResult(mod, "lcinterp_probe_hal_named", name);
},
};
}

View File

@@ -0,0 +1,206 @@
import createLinuxCncTrivkinsKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_trivkins_kinematics.js";
import createLinuxCnc5axiskinsKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_5axiskins_kinematics.js";
import createLinuxCncXyzacTrtKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_xyzac_trt_kinematics.js";
import createLinuxCncXyzbcTrtKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_xyzbc_trt_kinematics.js";
import createLinuxCncCorexyKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_corexy_kinematics.js";
import createLinuxCncRotateKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_rotate_kinematics.js";
import createLinuxCncRoseKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_rose_kinematics.js";
import createLinuxCncMaxKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_max_kinematics.js";
import createLinuxCncLineardeltaKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_lineardelta_kinematics.js";
import createLinuxCncRotarydeltaKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_rotarydelta_kinematics.js";
import createLinuxCncScorbotKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_scorbot_kinematics.js";
import createLinuxCncTripodKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_tripod_kinematics.js";
import createLinuxCncScaraKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_scara_kinematics.js";
import createLinuxCncPumaKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_puma_kinematics.js";
import createLinuxCncGenserKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_genser_kinematics.js";
import createLinuxCncGenhexKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_genhex_kinematics.js";
import createLinuxCncPentakinsKinematicsModule from "../../../build/wasm/kinematics/linuxcnc_pentakins_kinematics.js";
export const LINUXCNC_KINEMATICS_MODULES = [
{ id: "trivkins", wasmFile: "linuxcnc_trivkins_kinematics.wasm", factory: createLinuxCncTrivkinsKinematicsModule },
{ id: "5axiskins", wasmFile: "linuxcnc_5axiskins_kinematics.wasm", factory: createLinuxCnc5axiskinsKinematicsModule },
{ id: "xyzac-trt", wasmFile: "linuxcnc_xyzac_trt_kinematics.wasm", factory: createLinuxCncXyzacTrtKinematicsModule },
{ id: "xyzbc-trt", wasmFile: "linuxcnc_xyzbc_trt_kinematics.wasm", factory: createLinuxCncXyzbcTrtKinematicsModule },
{ id: "corexy", wasmFile: "linuxcnc_corexy_kinematics.wasm", factory: createLinuxCncCorexyKinematicsModule },
{ id: "rotate", wasmFile: "linuxcnc_rotate_kinematics.wasm", factory: createLinuxCncRotateKinematicsModule },
{ id: "rose", wasmFile: "linuxcnc_rose_kinematics.wasm", factory: createLinuxCncRoseKinematicsModule },
{ id: "max", wasmFile: "linuxcnc_max_kinematics.wasm", factory: createLinuxCncMaxKinematicsModule },
{ id: "lineardelta", wasmFile: "linuxcnc_lineardelta_kinematics.wasm", factory: createLinuxCncLineardeltaKinematicsModule },
{ id: "rotarydelta", wasmFile: "linuxcnc_rotarydelta_kinematics.wasm", factory: createLinuxCncRotarydeltaKinematicsModule },
{ id: "scorbot", wasmFile: "linuxcnc_scorbot_kinematics.wasm", factory: createLinuxCncScorbotKinematicsModule },
{ id: "tripod", wasmFile: "linuxcnc_tripod_kinematics.wasm", factory: createLinuxCncTripodKinematicsModule },
{ id: "scara", wasmFile: "linuxcnc_scara_kinematics.wasm", factory: createLinuxCncScaraKinematicsModule },
{ id: "puma", wasmFile: "linuxcnc_puma_kinematics.wasm", factory: createLinuxCncPumaKinematicsModule },
{ id: "genser", wasmFile: "linuxcnc_genser_kinematics.wasm", factory: createLinuxCncGenserKinematicsModule },
{ id: "genhex", wasmFile: "linuxcnc_genhex_kinematics.wasm", factory: createLinuxCncGenhexKinematicsModule },
{ id: "pentakins", wasmFile: "linuxcnc_pentakins_kinematics.wasm", factory: createLinuxCncPentakinsKinematicsModule },
];
const MODULES_BY_ID = new Map(LINUXCNC_KINEMATICS_MODULES.map((entry) => [entry.id, entry]));
function requireWasmFunction(mod, functionName) {
const fn = mod[`_${functionName}`];
if (typeof fn !== "function") {
throw new Error(`linuxcnc kinematics WASM missing ${functionName}; rebuild wasm-port/tools/build_kinematics_wasm.sh`);
}
return fn;
}
function writeDoubleArray(mod, values) {
const bytes = values.length * 8;
const ptr = mod._malloc(bytes);
mod.HEAPF64.set(values, ptr / 8);
return ptr;
}
function readDoubleArray(mod, ptr, length) {
return Array.from(mod.HEAPF64.subarray(ptr / 8, ptr / 8 + length));
}
function poseValuesFromObject(pose = {}) {
return [
Number(pose.x ?? pose.tran?.x ?? 0),
Number(pose.y ?? pose.tran?.y ?? 0),
Number(pose.z ?? pose.tran?.z ?? 0),
Number(pose.a ?? 0),
Number(pose.b ?? 0),
Number(pose.c ?? 0),
Number(pose.u ?? 0),
Number(pose.v ?? 0),
Number(pose.w ?? 0),
];
}
function poseObjectFromValues(values) {
return {
x: values[0],
y: values[1],
z: values[2],
a: values[3],
b: values[4],
c: values[5],
u: values[6],
v: values[7],
w: values[8],
};
}
export function supportedLinuxCncKinematicsModules() {
return LINUXCNC_KINEMATICS_MODULES.map((entry) => entry.id);
}
export function linuxCncKinematicsWasmFile(moduleId) {
return MODULES_BY_ID.get(moduleId)?.wasmFile || null;
}
export async function createLinuxCncKinematicsSdk({
moduleId = "xyzac-trt",
moduleOptions = {},
} = {}) {
const entry = MODULES_BY_ID.get(moduleId);
if (!entry) {
throw new Error(`unsupported LinuxCNC kinematics module: ${moduleId}`);
}
const mod = await entry.factory(moduleOptions);
requireWasmFunction(mod, "lckins_init")();
return {
apiName: "linuxcnc-kinematics-wasm-sdk",
moduleId,
module: mod,
wasmFile: entry.wasmFile,
hasWasmFunction(functionName) {
return typeof mod[`_${functionName}`] === "function";
},
type() {
return requireWasmFunction(mod, "lckins_type")();
},
switchable() {
return requireWasmFunction(mod, "lckins_switchable")();
},
switchKinematics(switchkinsType) {
return requireWasmFunction(mod, "lckins_switch")(Number(switchkinsType) || 0);
},
forward(joints, options = {}) {
const jointValues = Array.from(joints, Number);
const jointsPtr = writeDoubleArray(mod, jointValues);
const posePtr = writeDoubleArray(mod, poseValuesFromObject(options.seedPose));
const fflagsPtr = mod._malloc(8);
const iflagsPtr = mod._malloc(8);
mod.HEAPU32[fflagsPtr / 4] = 0;
mod.HEAPU32[iflagsPtr / 4] = 0;
try {
const rc = requireWasmFunction(mod, "lckins_forward")(
jointsPtr,
jointValues.length,
posePtr,
fflagsPtr,
iflagsPtr,
);
return {
rc,
pose: poseObjectFromValues(readDoubleArray(mod, posePtr, 9)),
fflags: mod.HEAPU32[fflagsPtr / 4],
iflags: mod.HEAPU32[iflagsPtr / 4],
};
} finally {
mod._free(jointsPtr);
mod._free(posePtr);
mod._free(fflagsPtr);
mod._free(iflagsPtr);
}
},
inverse(pose, jointCount = 5, options = {}) {
const posePtr = writeDoubleArray(mod, poseValuesFromObject(pose));
const jointsPtr = mod._malloc(jointCount * 8);
const iflagsPtr = mod._malloc(8);
const fflagsPtr = mod._malloc(8);
const seedJoints = Array.isArray(options.seedJoints)
? options.seedJoints.map(Number)
: [];
mod.HEAPF64.fill(0, jointsPtr / 8, jointsPtr / 8 + jointCount);
mod.HEAPF64.set(seedJoints.slice(0, jointCount), jointsPtr / 8);
mod.HEAPU32[iflagsPtr / 4] = 0;
mod.HEAPU32[fflagsPtr / 4] = 0;
try {
const rc = requireWasmFunction(mod, "lckins_inverse")(
posePtr,
jointsPtr,
jointCount,
iflagsPtr,
fflagsPtr,
);
return {
rc,
joints: readDoubleArray(mod, jointsPtr, jointCount),
iflags: mod.HEAPU32[iflagsPtr / 4],
fflags: mod.HEAPU32[fflagsPtr / 4],
};
} finally {
mod._free(posePtr);
mod._free(jointsPtr);
mod._free(iflagsPtr);
mod._free(fflagsPtr);
}
},
runProbe() {
const resultPtr = requireWasmFunction(mod, "lckins_run_probe")();
if (!resultPtr) {
throw new Error("lckins_run_probe returned null");
}
try {
return mod.UTF8ToString(resultPtr);
} finally {
requireWasmFunction(mod, "lckins_free_string")(resultPtr);
}
},
};
}

View File

@@ -0,0 +1,132 @@
import createLinuxCncTaskHalModule from "../../../build/wasm/task-hal/linuxcnc_task_hal.js";
const SEMANTIC_BOUNDARY = "linuxcnc_task_motion_hal_wasm_phase4_minimal";
function allocCString(mod, value) {
const text = String(value ?? "");
const bytes = mod.lengthBytesUTF8(text) + 1;
const ptr = mod._malloc(bytes);
mod.stringToUTF8(text, ptr, bytes);
return ptr;
}
function withCString(mod, value, fn) {
const ptr = allocCString(mod, value);
try {
return fn(ptr);
} finally {
mod._free(ptr);
}
}
function requireWasmFunction(mod, functionName) {
const fn = mod[`_${functionName}`];
if (typeof fn !== "function") {
throw new Error(`linuxcnc task/HAL WASM missing ${functionName}; rebuild wasm-port/tools/build_task_hal_wasm.sh`);
}
return fn;
}
function readJson(mod, functionName) {
const fn = requireWasmFunction(mod, functionName);
let bytes = 131072;
for (let attempt = 0; attempt < 2; attempt += 1) {
const ptr = mod._malloc(bytes);
try {
const rc = fn(ptr, bytes);
if (rc === 0) {
return JSON.parse(mod.UTF8ToString(ptr));
}
if (rc > bytes) {
bytes = rc;
continue;
}
throw new Error(`${functionName} failed with rc=${rc}`);
} finally {
mod._free(ptr);
}
}
throw new Error(`${functionName} output exceeded buffer`);
}
function callWithJson(mod, functionName, payload) {
const fn = requireWasmFunction(mod, functionName);
return withCString(mod, JSON.stringify(payload ?? {}), (ptr) => fn(ptr));
}
export async function createLinuxCncTaskHalSdk(moduleOptions = {}) {
const mod = await createLinuxCncTaskHalModule(moduleOptions);
return {
apiName: "linuxcnc-task-hal-wasm-sdk",
semanticBoundary: SEMANTIC_BOUNDARY,
module: mod,
readiness() {
return {
apiName: "linuxcnc-task-hal-wasm-sdk-readiness",
loaded: true,
semanticBoundary: SEMANTIC_BOUNDARY,
taskRuntimeReady: typeof mod._lctask_init_session === "function",
motionRuntimeReady: typeof mod._lcmot_step_servo === "function",
halRuntimeReady: typeof mod._lchal_get_snapshot_json === "function",
nativeTaskReady: false,
nativeHalSyncReady: false,
};
},
initSession(session = {}) {
const rc = callWithJson(mod, "lctask_init_session", session);
if (rc !== 0) {
throw new Error(`lctask_init_session failed with rc=${rc}`);
}
return rc;
},
stageFile(path, text) {
return withCString(mod, path, (pathPtr) =>
withCString(mod, text, (textPtr) =>
requireWasmFunction(mod, "lctask_stage_file")(pathPtr, textPtr),
),
);
},
openProgram(path) {
return withCString(mod, path, (pathPtr) =>
requireWasmFunction(mod, "lctask_open_program")(pathPtr),
);
},
loadProgramMotionPlan(plan) {
return callWithJson(mod, "lctask_load_program_motion_plan_json", plan);
},
sendCommand(command) {
return callWithJson(mod, "lctask_send_command_json", command);
},
runCycles({
taskPeriodNs = 10000000,
servoPeriodNs = 1000000,
taskCycles = 1,
} = {}) {
return requireWasmFunction(mod, "lctask_run_cycles")(
Number(taskPeriodNs) || 10000000,
Number(servoPeriodNs) || 1000000,
Number(taskCycles) || 0,
);
},
readStatus() {
return readJson(mod, "lctask_read_status_json");
},
readEvents() {
return readJson(mod, "lctask_read_events_json");
},
resetSession() {
return requireWasmFunction(mod, "lctask_reset_session")();
},
};
}

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

View File

@@ -0,0 +1,486 @@
function cleanManifest(manifestText) {
return manifestText
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"));
}
function normalizeRel(path) {
const parts = [];
for (const part of path.split("/")) {
if (!part || part === ".") {
continue;
}
if (part === "..") {
parts.pop();
continue;
}
parts.push(part);
}
return parts.join("/");
}
function dirname(path) {
const clean = normalizeRel(path);
const index = clean.lastIndexOf("/");
return index === -1 ? "" : clean.slice(0, index);
}
function basename(path) {
const clean = normalizeRel(path);
const index = clean.lastIndexOf("/");
return index === -1 ? clean : clean.slice(index + 1);
}
function stripIniComment(line) {
const hash = line.indexOf("#");
const semicolon = line.indexOf(";");
const indexes = [hash, semicolon].filter((index) => index >= 0);
if (indexes.length === 0) {
return line;
}
return line.slice(0, Math.min(...indexes));
}
function parseIni(iniText) {
const values = new Map();
let section = "";
for (const rawLine of iniText.split("\n")) {
const sectionMatch = rawLine.match(/^\s*\[([^\]]+)\]/);
if (sectionMatch) {
section = sectionMatch[1].trim().toUpperCase();
continue;
}
const line = stripIniComment(rawLine);
const equals = line.indexOf("=");
if (equals === -1 || !section) {
continue;
}
const key = line.slice(0, equals).trim().toUpperCase();
const value = line.slice(equals + 1).trim();
if (!key || !value) {
continue;
}
const mapKey = `${section}.${key}`;
const existing = values.get(mapKey) ?? [];
existing.push(value);
values.set(mapKey, existing);
}
return values;
}
function firstIniValue(values, section, key) {
return values.get(`${section.toUpperCase()}.${key.toUpperCase()}`)?.[0] ?? null;
}
function allIniValues(values, section, key) {
return values.get(`${section.toUpperCase()}.${key.toUpperCase()}`) ?? [];
}
function splitSearchPath(value) {
return value.split(":").map((entry) => entry.trim()).filter(Boolean);
}
function sourceRelFor(machineRel, path) {
return normalizeRel(`configs/sim/${machineRel}/${path}`);
}
function targetPathFor(wasmDir, targetRel) {
return `${wasmDir}/${normalizeRel(targetRel)}`;
}
function isUserMCodePath(path) {
return /^M1\d\d$/i.test(basename(path));
}
function isSubroutinePath(path) {
return path.toLowerCase().endsWith(".ngc");
}
function findUpwardByBasename(manifestSet, sourceDir, fileName, searchRootRel) {
let current = sourceDir;
while (!searchRootRel || current === searchRootRel || current.startsWith(`${searchRootRel}/`)) {
const candidate = normalizeRel(`${current}/${fileName}`);
if (manifestSet.has(candidate)) {
return candidate;
}
if (!current || current === searchRootRel) {
break;
}
current = dirname(current);
}
return null;
}
function sourceForReference(manifestSet, sourceDir, reference, searchRootRel) {
if (!reference || reference.startsWith("/")) {
return null;
}
const sourceRel = normalizeRel(`${sourceDir}/${reference}`);
if (manifestSet.has(sourceRel)) {
return sourceRel;
}
return findUpwardByBasename(manifestSet, sourceDir, basename(reference), searchRootRel);
}
function addPlannedFile(plan, manifestSet, sourceRel, targetRel, options = {}) {
if (!sourceRel || !manifestSet.has(sourceRel)) {
if (options.required) {
throw new Error(`missing vendored sim-config file: ${sourceRel}`);
}
return;
}
const existing = plan.get(sourceRel);
const executable = options.executable || isUserMCodePath(sourceRel);
if (existing) {
existing.executable ||= executable;
return;
}
plan.set(sourceRel, {
sourceRel,
wasmPath: targetPathFor(options.wasmDir, targetRel),
path: targetPathFor(options.wasmDir, targetRel),
executable,
});
}
function addDirectoryFiles(plan, manifestEntries, manifestSet, sourceDir, targetDir, predicate, options) {
const prefix = sourceDir ? `${sourceDir}/` : "";
for (const sourceRel of manifestEntries) {
if (!sourceRel.startsWith(prefix)) {
continue;
}
const childRel = sourceRel.slice(prefix.length);
if (childRel.includes("/") || !predicate(sourceRel)) {
continue;
}
addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${targetDir}/${childRel}`), options);
}
}
function remapNgcNames(iniValues) {
const names = [];
for (const remap of allIniValues(iniValues, "RS274NGC", "REMAP")) {
const match = remap.match(/(?:^|\s)ngc=([^\s]+)/i);
if (match) {
names.push(`${match[1]}.ngc`);
}
}
return names;
}
function sectionValues(iniValues, section) {
const prefix = `${section.toUpperCase()}.`;
const values = [];
for (const [key, entries] of iniValues.entries()) {
if (!key.startsWith(prefix)) {
continue;
}
for (const value of entries) {
values.push({ key: key.slice(prefix.length), value });
}
}
return values;
}
function valuesMatching(iniValues, section, keys) {
const wanted = new Set(keys.map((key) => key.toUpperCase()));
return sectionValues(iniValues, section)
.filter((entry) => wanted.has(entry.key))
.map((entry) => entry.value);
}
function looksLikePythonReference(value) {
return /(^|\s|=|:)["']?[^"'\s]*\.py(["'\s]|$)/i.test(value);
}
function userMCodesInText(text) {
const codes = new Set();
for (const match of text.matchAll(/(?<![A-Za-z0-9_])M\s*(1\d\d)(?![0-9])/gi)) {
codes.add(`M${match[1]}`);
}
return [...codes].sort();
}
function vendoredUserMCodes({
manifestEntries,
sourceDir,
normalizedSearchRoot,
userMPathValues,
}) {
const manifestSet = new Set(manifestEntries);
const executableCodes = new Set();
for (const dirEntry of userMPathValues.flatMap(splitSearchPath)) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
const prefix = sourceUserMDir ? `${sourceUserMDir}/` : "";
for (const sourceRel of manifestEntries) {
if (
sourceRel.startsWith(prefix) &&
!sourceRel.slice(prefix.length).includes("/") &&
isUserMCodePath(sourceRel)
) {
executableCodes.add(basename(sourceRel).toUpperCase());
}
}
}
for (const dirEntry of userMPathValues.flatMap(splitSearchPath)) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
for (let code = 100; code <= 199; code += 1) {
const candidate = findUpwardByBasename(
manifestSet,
sourceUserMDir,
`M${code}`,
normalizedSearchRoot,
);
if (candidate) {
executableCodes.add(`M${code}`);
}
}
}
return [...executableCodes].sort();
}
export function analyzeIniRuntimeBoundaries({
manifestText = "",
sourceRootRel,
sourceSearchRootRel = sourceRootRel,
iniFile = "",
iniText,
executionTexts = [],
}) {
const manifestEntries = cleanManifest(manifestText);
const iniValues = parseIni(iniText);
const normalizedSourceRoot = normalizeRel(sourceRootRel);
const normalizedSearchRoot = normalizeRel(sourceSearchRootRel);
const sourceDir = normalizeRel(`${normalizedSourceRoot}/${dirname(iniFile)}`);
const userMPathValues = allIniValues(iniValues, "RS274NGC", "USER_M_PATH");
const halValues = valuesMatching(iniValues, "HAL", [
"HALFILE",
"HALCMD",
"POSTGUI_HALFILE",
"HALUI",
]);
const displayValues = valuesMatching(iniValues, "DISPLAY", [
"DISPLAY",
"PYVCP",
"GLADEVCP",
"EMBED_TAB_COMMAND",
]);
const halUiMdiCommands = allIniValues(iniValues, "HALUI", "MDI_COMMAND");
const dbProgram = firstIniValue(iniValues, "EMCIO", "DB_PROGRAM");
const remapValues = allIniValues(iniValues, "RS274NGC", "REMAP");
const pythonRemapReferences = [
...sectionValues(iniValues, "PYTHON").map((entry) => entry.value),
...remapValues.filter((value) => /(?:^|\s)python=/i.test(value)),
];
const pythonUiReferences = [
...displayValues.filter(looksLikePythonReference),
...(dbProgram && looksLikePythonReference(dbProgram) ? [dbProgram] : []),
];
const pythonReferences = [...pythonRemapReferences, ...pythonUiReferences];
const vendoredUserMCodeList = vendoredUserMCodes({
manifestEntries,
sourceDir,
normalizedSearchRoot,
userMPathValues,
});
const vendoredUserMCodeSet = new Set(vendoredUserMCodeList);
const hasUserMPath = userMPathValues.length > 0;
const executionUserMCodes = [...new Set(executionTexts.flatMap(userMCodesInText))].sort();
const unstagedExecutionUserMCodes = executionUserMCodes
.filter((code) => !vendoredUserMCodeSet.has(code));
const hasExternalUserMUse = hasUserMPath && unstagedExecutionUserMCodes.length > 0;
const dependencies = [];
if (dbProgram) {
dependencies.push("tool_database_process");
}
if (halValues.length > 0) {
dependencies.push("hal_process");
}
if (displayValues.some((value) => !/^axis$/i.test(value))) {
dependencies.push("ui_process");
}
if (halUiMdiCommands.length > 0) {
dependencies.push("halui_mdi_process");
}
if (pythonReferences.length > 0) {
dependencies.push("python_runtime");
}
if (hasExternalUserMUse) {
dependencies.push("external_user_m_process");
}
let recommendedBlockedKind = "-";
if (dbProgram && dbProgram !== "./db_nonran.py") {
recommendedBlockedKind = "L4-TOOL-DB";
} else if (hasExternalUserMUse) {
recommendedBlockedKind = "L4-USER-M-PROCESS";
} else if (pythonRemapReferences.length > 0) {
recommendedBlockedKind = "L4-PYTHON-REMAP";
}
return {
dependencies: [...new Set(dependencies)].sort(),
recommendedBlockedKind,
toolDatabaseProgram: dbProgram ?? "",
halRuntime: {
values: halValues,
requiresProcess: halValues.length > 0,
},
uiRuntime: {
values: displayValues,
requiresProcess: displayValues.some((value) => !/^axis$/i.test(value)),
},
haluiRuntime: {
mdiCommands: halUiMdiCommands,
requiresProcess: halUiMdiCommands.length > 0,
},
userMRuntime: {
paths: userMPathValues,
vendoredExecutableCount: vendoredUserMCodeList.length,
executionCodes: executionUserMCodes,
unstagedExecutionCodes: unstagedExecutionUserMCodes,
requiresExternalProcess: hasExternalUserMUse,
},
pythonRuntime: {
references: pythonReferences,
remapReferences: pythonRemapReferences,
uiReferences: pythonUiReferences,
requiresProcess: pythonReferences.length > 0,
},
};
}
export function planSimConfigStaging({
manifestText,
machineRel,
iniFile,
iniText,
programFile = null,
wasmDir = `/work/sim/${machineRel}`,
}) {
return planIniFileContextStaging({
manifestText,
sourceRootRel: `configs/sim/${machineRel}`,
sourceSearchRootRel: "configs/sim",
iniFile,
iniText,
programFile,
wasmDir,
});
}
export function planIniFileContextStaging({
manifestText,
sourceRootRel,
iniFile,
iniText,
programFile = null,
wasmDir,
sourceSearchRootRel = sourceRootRel,
}) {
const manifestEntries = cleanManifest(manifestText);
const manifestSet = new Set(manifestEntries);
const iniValues = parseIni(iniText);
const plan = new Map();
const normalizedSourceRoot = normalizeRel(sourceRootRel);
const normalizedSearchRoot = normalizeRel(sourceSearchRootRel);
const sourceDir = normalizeRel(`${normalizedSourceRoot}/${dirname(iniFile)}`);
const iniSourceRel = normalizeRel(`${normalizedSourceRoot}/${iniFile}`);
const programReference = programFile ?? firstIniValue(iniValues, "DISPLAY", "OPEN_FILE");
addPlannedFile(plan, manifestSet, iniSourceRel, iniFile, { wasmDir, required: true });
if (programReference) {
const programSourceRel = sourceForReference(
manifestSet,
sourceDir,
programReference,
normalizedSearchRoot,
);
addPlannedFile(plan, manifestSet, programSourceRel, programReference, {
wasmDir,
required: true,
});
}
for (const [section, key] of [
["EMCIO", "TOOL_TABLE"],
["RS274NGC", "PARAMETER_FILE"],
]) {
const reference = firstIniValue(iniValues, section, key);
if (!reference) {
continue;
}
const sourceRel = sourceForReference(manifestSet, sourceDir, reference, normalizedSearchRoot);
addPlannedFile(plan, manifestSet, sourceRel, reference, { wasmDir });
}
const subroutineDirs = allIniValues(iniValues, "RS274NGC", "SUBROUTINE_PATH")
.flatMap(splitSearchPath);
for (const dirEntry of subroutineDirs) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceSubdir = normalizeRel(`${sourceDir}/${dirEntry}`);
const targetSubdir = normalizeRel(dirEntry);
addDirectoryFiles(
plan,
manifestEntries,
manifestSet,
sourceSubdir,
targetSubdir,
isSubroutinePath,
{ wasmDir },
);
}
for (const dirEntry of allIniValues(iniValues, "RS274NGC", "USER_M_PATH").flatMap(splitSearchPath)) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
const targetUserMDir = normalizeRel(dirEntry);
addDirectoryFiles(
plan,
manifestEntries,
manifestSet,
sourceUserMDir,
targetUserMDir,
isUserMCodePath,
{ wasmDir, executable: true },
);
}
for (const remapName of remapNgcNames(iniValues)) {
for (const dirEntry of subroutineDirs) {
const sourceRel = sourceForReference(
manifestSet,
normalizeRel(`${sourceDir}/${dirEntry}`),
remapName,
normalizedSearchRoot,
);
addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${dirEntry}/${remapName}`), {
wasmDir,
});
}
}
return {
wasmDir,
iniPath: targetPathFor(wasmDir, iniFile),
programPath: programReference ? targetPathFor(wasmDir, programReference) : null,
files: [...plan.values()],
};
}