Files
cnc_wams/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-ini-runtime.js

346 lines
13 KiB
JavaScript

const AXIS_SECTION_RE = /^AXIS_([A-Z])$/;
const JOINT_SECTION_RE = /^JOINT_(\d+)$/;
export function parseLinuxCncIni(text, { path = "inline.ini", profileId = "unknown" } = {}) {
const sections = parseIniSections(text);
const kinsText = getFirstValue(sections, "KINS", "KINEMATICS") || "";
const coordinates = getFirstValue(sections, "TRAJ", "COORDINATES") || "";
const jointCount = numberOrNull(getFirstValue(sections, "KINS", "JOINTS"));
const axisLimits = parseAxisLimits(sections);
const jointConfig = parseJointConfig(sections, coordinates);
const remaps = parseRemaps(sections);
const hal = parseHal(sections);
const display = parseDisplay(sections);
const halui = {
mdiCommands: getValues(sections, "HALUI", "MDI_COMMAND"),
};
const kinematicsModuleId = inferKinematicsModuleId(kinsText);
return {
apiName: "web-rtcp-5axis-linuxcnc-ini-config",
profileId,
path,
machineName: getFirstValue(sections, "EMC", "MACHINE") || null,
kinematics: parseKinematics(kinsText),
kinematicsModuleId,
kinematicsParameters: {
sparm: parseKinematicsParameter(kinsText, "sparm"),
joints: jointCount,
switchkinsTypes: inferSwitchkinsTypes({ coordinates, halui, kinematicsModuleId }),
},
traj: {
coordinates,
linearUnits: getFirstValue(sections, "TRAJ", "LINEAR_UNITS") || null,
angularUnits: getFirstValue(sections, "TRAJ", "ANGULAR_UNITS") || null,
defaultLinearVelocity: numberOrNull(getFirstValue(sections, "TRAJ", "DEFAULT_LINEAR_VELOCITY")),
maxLinearVelocity: numberOrNull(getFirstValue(sections, "TRAJ", "MAX_LINEAR_VELOCITY")),
defaultLinearAcceleration: numberOrNull(getFirstValue(sections, "TRAJ", "DEFAULT_LINEAR_ACCELERATION")),
maxLinearAcceleration: numberOrNull(getFirstValue(sections, "TRAJ", "MAX_LINEAR_ACCELERATION")),
},
display,
rs274ngc: {
subroutinePath: getFirstValue(sections, "RS274NGC", "SUBROUTINE_PATH") || null,
halPinVars: boolFromIni(getFirstValue(sections, "RS274NGC", "HAL_PIN_VARS")),
parameterFile: getFirstValue(sections, "RS274NGC", "PARAMETER_FILE") || null,
remaps,
},
hal,
halui,
axisLimits,
jointConfig,
emcio: {
toolTable: getFirstValue(sections, "EMCIO", "TOOL_TABLE") || null,
},
validation: validateIniConfig({ coordinates, jointCount, axisLimits, jointConfig, kinsText }),
semanticBoundary: "linuxcnc_ini_file_browser_parser",
};
}
export async function loadLinuxCncIniConfig(profile, { baseUrl = import.meta.url, fetchImpl = globalThis.fetch } = {}) {
if (!profile?.iniPath) {
throw new Error("profile is missing iniPath");
}
if (typeof fetchImpl !== "function") {
throw new Error("fetch is not available for LinuxCNC INI loading");
}
const candidateUrls = [
new URL(`../../${profile.iniPath}`, baseUrl),
new URL(`../../../../wasm-port/vendor/linuxcnc/${profile.iniPath}`, baseUrl),
];
const errors = [];
let response = null;
for (const url of candidateUrls) {
try {
response = await fetchImpl(url.href);
if (response.ok) break;
errors.push(`${url.href}: HTTP ${response.status}`);
response = null;
} catch (error) {
errors.push(`${url.href}: ${error.message}`);
}
}
if (!response) {
throw new Error(`failed to load LinuxCNC INI ${profile.iniPath}: ${errors.join(" | ")}`);
}
return parseLinuxCncIni(await response.text(), {
path: profile.iniPath,
profileId: profile.id,
});
}
export function applyIniConfigToProfile(profile, iniConfig) {
if (!iniConfig) return profile;
return {
...profile,
machineName: iniConfig.machineName || profile.machineName,
kinematics: iniConfig.kinematics.name || profile.kinematics,
kinematicsModuleId: iniConfig.kinematicsModuleId || profile.kinematicsModuleId,
kinematicsParameters: {
...profile.kinematicsParameters,
...iniConfig.kinematicsParameters,
switchkinsTypes: mergeSwitchkinsTypes(
profile.kinematicsParameters?.switchkinsTypes || [],
iniConfig.kinematicsParameters.switchkinsTypes,
),
},
display: {
...profile.display,
...iniConfig.display,
},
rs274ngc: {
...profile.rs274ngc,
subroutinePath: iniConfig.rs274ngc.subroutinePath || profile.rs274ngc?.subroutinePath,
halPinVars: iniConfig.rs274ngc.halPinVars ?? profile.rs274ngc?.halPinVars,
parameterFile: iniConfig.rs274ngc.parameterFile || profile.rs274ngc?.parameterFile,
},
hal: {
...profile.hal,
halui: iniConfig.hal.halui || profile.hal?.halui,
halFiles: iniConfig.hal.halFiles.length > 0 ? iniConfig.hal.halFiles : profile.hal?.halFiles,
postguiHalFiles: iniConfig.hal.postguiHalFiles.length > 0
? iniConfig.hal.postguiHalFiles
: profile.hal?.postguiHalFiles,
halcmd: {
...profile.hal?.halcmd,
raw: iniConfig.hal.halcmd,
initialSets: iniConfig.hal.initialSets.length > 0
? iniConfig.hal.initialSets
: profile.hal?.halcmd?.initialSets,
},
},
halui: iniConfig.halui.mdiCommands.length > 0 ? iniConfig.halui : profile.halui,
traj: {
...profile.traj,
...dropNullish(iniConfig.traj),
},
axisLimits: Object.keys(iniConfig.axisLimits).length > 0 ? iniConfig.axisLimits : profile.axisLimits,
jointConfig: iniConfig.jointConfig.length > 0 ? iniConfig.jointConfig : profile.jointConfig,
linuxCncIniConfig: iniConfig,
};
}
function parseIniSections(text) {
const sections = new Map();
let current = null;
for (const rawLine of String(text).split(/\r?\n/)) {
const line = stripIniComment(rawLine).trim();
if (!line) continue;
const sectionMatch = line.match(/^\[([^\]]+)]$/);
if (sectionMatch) {
current = sectionMatch[1].trim().toUpperCase();
if (!sections.has(current)) sections.set(current, new Map());
continue;
}
if (!current) continue;
const equals = line.indexOf("=");
if (equals < 0) continue;
const key = line.slice(0, equals).trim().toUpperCase();
const value = line.slice(equals + 1).trim();
const section = sections.get(current);
if (!section.has(key)) section.set(key, []);
section.get(key).push(value);
}
return sections;
}
function stripIniComment(line) {
let quote = null;
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
if ((char === "\"" || char === "'") && line[index - 1] !== "\\") {
quote = quote === char ? null : quote || char;
}
if (!quote && (char === "#" || char === ";")) {
return line.slice(0, index);
}
}
return line;
}
function getValues(sections, sectionName, key) {
return sections.get(sectionName.toUpperCase())?.get(key.toUpperCase()) || [];
}
function getFirstValue(sections, sectionName, key) {
return getValues(sections, sectionName, key)[0] ?? null;
}
function parseAxisLimits(sections) {
const result = {};
for (const [sectionName, values] of sections) {
const match = sectionName.match(AXIS_SECTION_RE);
if (!match) continue;
result[match[1]] = {
min: numberOrNull(first(values, "MIN_LIMIT")),
max: numberOrNull(first(values, "MAX_LIMIT")),
maxVelocity: numberOrNull(first(values, "MAX_VELOCITY")),
maxAcceleration: numberOrNull(first(values, "MAX_ACCELERATION")),
};
}
return result;
}
function parseJointConfig(sections, coordinates) {
const axisOrder = String(coordinates || "").split("");
return [...sections.entries()]
.map(([sectionName, values]) => {
const match = sectionName.match(JOINT_SECTION_RE);
if (!match) return null;
const id = Number(match[1]);
return {
id,
axis: axisOrder[id] || null,
type: first(values, "TYPE") || null,
home: numberOrNull(first(values, "HOME")),
min: numberOrNull(first(values, "MIN_LIMIT")),
max: numberOrNull(first(values, "MAX_LIMIT")),
maxVelocity: numberOrNull(first(values, "MAX_VELOCITY")),
maxAcceleration: numberOrNull(first(values, "MAX_ACCELERATION")),
homeSearchVelocity: numberOrNull(first(values, "HOME_SEARCH_VEL")),
homeSequence: numberOrNull(first(values, "HOME_SEQUENCE")),
};
})
.filter(Boolean)
.sort((left, right) => left.id - right.id);
}
function parseRemaps(sections) {
return getValues(sections, "RS274NGC", "REMAP").map((value) => {
const code = value.match(/\bM\d+\b/i)?.[0]?.toUpperCase() || null;
const ngc = value.match(/\bngc=([^\s]+)/i)?.[1] || null;
const modalGroup = numberOrNull(value.match(/\bmodalgroup=(\d+)/i)?.[1]);
return { raw: value, code, modalGroup, ngc };
});
}
function parseHal(sections) {
const halcmd = getValues(sections, "HAL", "HALCMD");
return {
halui: getFirstValue(sections, "HAL", "HALUI") || null,
halFiles: getValues(sections, "HAL", "HALFILE"),
postguiHalFiles: getValues(sections, "HAL", "POSTGUI_HALFILE"),
halcmd,
initialSets: halcmd
.map((line) => line.match(/^\s*(sets|setp)\s+:?([^\s]+)\s+([-+0-9.eE]+)/i))
.filter(Boolean)
.map((match) => ({ op: match[1], pin: match[2], value: Number(match[3]) })),
};
}
function parseDisplay(sections) {
const jogAxes = getFirstValue(sections, "DISPLAY", "JOG_AXES");
return dropNullish({
geometry: getFirstValue(sections, "DISPLAY", "GEOMETRY"),
display: getFirstValue(sections, "DISPLAY", "DISPLAY"),
jogAxes: jogAxes ? jogAxes.split("") : null,
pyvcp: getFirstValue(sections, "DISPLAY", "PYVCP"),
openFile: getFirstValue(sections, "DISPLAY", "OPEN_FILE"),
programPrefix: getFirstValue(sections, "DISPLAY", "PROGRAM_PREFIX"),
positionOffset: getFirstValue(sections, "DISPLAY", "POSITION_OFFSET"),
positionFeedback: getFirstValue(sections, "DISPLAY", "POSITION_FEEDBACK"),
maxFeedOverride: numberOrNull(getFirstValue(sections, "DISPLAY", "MAX_FEED_OVERRIDE")),
maxLinearVelocity: numberOrNull(getFirstValue(sections, "DISPLAY", "MAX_LINEAR_VELOCITY")),
maxAngularVelocity: numberOrNull(getFirstValue(sections, "DISPLAY", "MAX_ANGULAR_VELOCITY")),
});
}
function parseKinematics(text) {
const [name, ...parameters] = String(text || "").trim().split(/\s+/).filter(Boolean);
return {
name: name || null,
raw: text,
parameters,
};
}
function parseKinematicsParameter(text, key) {
return String(text || "").match(new RegExp(`\\b${key}=([^\\s]+)`, "i"))?.[1] || null;
}
function inferKinematicsModuleId(kinsText) {
const name = parseKinematics(kinsText).name || "";
if (name.includes("xyzbc")) return "xyzbc-trt";
if (name.includes("xyzac")) return "xyzac-trt";
return name.replace(/-kins$/, "") || null;
}
function inferSwitchkinsTypes({ coordinates, halui, kinematicsModuleId }) {
const mdiCommands = halui.mdiCommands.length > 0 ? halui.mdiCommands : ["M429", "M428", "M430"];
const tcpType = coordinates === "XYZBC" ? "tcp-xyzbc" : "tcp-xyzac";
const tcpLabel = `${coordinates || kinematicsModuleId || "TCP"} TCP`;
return mdiCommands.map((command, index) => ({
value: index === 0 ? 0 : index,
label: index === 0 ? "identity" : index === 1 ? tcpLabel : "USERK",
mdiCommand: command,
webKinsType: index === 0 ? "identity" : index === 1 ? tcpType : "userk",
}));
}
function validateIniConfig({ coordinates, jointCount, axisLimits, jointConfig, kinsText }) {
const missing = [];
if (!coordinates) missing.push("TRAJ.COORDINATES");
if (!jointCount) missing.push("KINS.JOINTS");
if (!kinsText) missing.push("KINS.KINEMATICS");
for (const axis of String(coordinates || "").split("")) {
if (!axisLimits[axis]) missing.push(`AXIS_${axis}`);
}
if (jointCount && jointConfig.length !== jointCount) {
missing.push(`JOINT_ count ${jointConfig.length}/${jointCount}`);
}
return {
ready: missing.length === 0,
missing,
axisCount: Object.keys(axisLimits).length,
jointCount: jointConfig.length,
};
}
function first(values, key) {
return values.get(key)?.[0] ?? null;
}
function numberOrNull(value) {
if (value === null || value === undefined || value === "") return null;
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
function boolFromIni(value) {
if (value === null || value === undefined) return null;
return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
}
function dropNullish(object) {
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== null && value !== undefined));
}
function mergeSwitchkinsTypes(profileTypes, iniTypes) {
if (!iniTypes?.length) return profileTypes;
return iniTypes.map((iniType) => ({
...iniType,
...(profileTypes.find((entry) => entry.mdiCommand === iniType.mdiCommand || entry.value === iniType.value) || {}),
...iniType,
}));
}