346 lines
13 KiB
JavaScript
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,
|
|
}));
|
|
}
|