接入 LinuxCNC TP 运行反馈
This commit is contained in:
345
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-ini-runtime.js
Normal file
345
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-ini-runtime.js
Normal file
@@ -0,0 +1,345 @@
|
||||
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,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user