190 lines
6.9 KiB
Python
190 lines
6.9 KiB
Python
# Copyright (C) 2026 LinuxCNC contributors
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
# Build a kinematic description of a LinuxCNC machine from its INI file.
|
|
#
|
|
# The model captures everything a downstream consumer (e.g. a FreeCAD Path
|
|
# plugin) needs to auto-configure a machine: axis list and type, travel limits,
|
|
# home positions, kinematics module/type, and the joint <-> axis mapping.
|
|
#
|
|
# LinuxCNC distinguishes AXES (Cartesian DOF, letters X Y Z A B C U V W,
|
|
# configured in [AXIS_*]) from JOINTS (physical motors, configured in
|
|
# [JOINT_n]). The kins module's coordinates= string ties joint N to an axis
|
|
# letter; for identity/trivial kinematics this mirrors [TRAJ]COORDINATES. The
|
|
# joint->axis mapping here follows map_coordinates_to_jnumbers() in
|
|
# src/emc/kinematics/kins_util.c.
|
|
#
|
|
# The axis set, limits, home data and kins module name are reported for every
|
|
# machine. The joint<->axis map and per-axis direction vectors are only trusted
|
|
# for serial, orthogonal-axis kinematics (SERIAL_ORTHOGONAL_KINS); for parallel,
|
|
# articulated or non-orthogonal machines that geometry is not INI-derivable and
|
|
# is omitted rather than guessed (see is_serial).
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
AXIS_LETTERS = "XYZABCUVW"
|
|
LINEAR_LETTERS = "XYZUVW"
|
|
ANGULAR_LETTERS = "ABC"
|
|
|
|
# Unit direction vector for each axis in the machine coordinate frame.
|
|
# Linear axes translate along the vector; rotary axes rotate about it.
|
|
AXIS_VECTORS = {
|
|
"X": (1.0, 0.0, 0.0), "Y": (0.0, 1.0, 0.0), "Z": (0.0, 0.0, 1.0),
|
|
"A": (1.0, 0.0, 0.0), "B": (0.0, 1.0, 0.0), "C": (0.0, 0.0, 1.0),
|
|
"U": (1.0, 0.0, 0.0), "V": (0.0, 1.0, 0.0), "W": (0.0, 0.0, 1.0),
|
|
}
|
|
|
|
# Human-readable name for LinuxCNC's KINEMATICS_TYPE enum
|
|
# (src/emc/kinematics/kinematics.h).
|
|
KINEMATICS_TYPE_NAMES = {
|
|
1: "IDENTITY",
|
|
2: "FORWARD_ONLY",
|
|
3: "INVERSE_ONLY",
|
|
4: "BOTH",
|
|
}
|
|
|
|
# Kins modules whose joints map 1:1 to orthogonal Cartesian axes. For these the
|
|
# joint<->axis map and the canonical AXIS_VECTORS are correct. For anything else
|
|
# -- CoreXY, parallel (hexapod/delta/tripod/penta), articulated (scara/puma/
|
|
# genser), non-orthogonal or runtime-switched kinematics -- the true geometry
|
|
# lives in the compiled kins module, not the INI, so it is omitted rather than
|
|
# guessed (the module name still travels in x:Kinematics for consumers to branch
|
|
# on).
|
|
SERIAL_ORTHOGONAL_KINS = frozenset({
|
|
"trivkins", "5axiskins", "xyzac-trt-kins", "xyzbc-trt-kins",
|
|
})
|
|
|
|
|
|
@dataclass
|
|
class Axis:
|
|
letter: str # X..W
|
|
kind: str # "LINEAR" or "ANGULAR"
|
|
vector: tuple # unit direction / rotation axis
|
|
min_limit: float = None # soft travel limit (machine units / deg)
|
|
max_limit: float = None
|
|
|
|
|
|
@dataclass
|
|
class Joint:
|
|
number: int
|
|
kind: str # "LINEAR" or "ANGULAR"
|
|
axis: str = None # mapped axis letter, if known
|
|
min_limit: float = None
|
|
max_limit: float = None
|
|
home: float = None
|
|
home_offset: float = None
|
|
|
|
|
|
@dataclass
|
|
class KinematicModel:
|
|
kins_module: str # e.g. "trivkins", "xyzac-trt-kins"
|
|
kins_params: str # remainder of the [KINS]KINEMATICS line
|
|
coordinates: str # packed axis letters, e.g. "XYZAC"
|
|
joints_count: int
|
|
axes: list = field(default_factory=list)
|
|
joints: list = field(default_factory=list)
|
|
kinematics_type: str = None # IDENTITY/BOTH/... filled from stat when live
|
|
|
|
@property
|
|
def is_serial(self):
|
|
"""True when joints map 1:1 to orthogonal Cartesian axes, so the
|
|
joint<->axis map and the canonical axis vectors are trustworthy."""
|
|
return self.kins_module in SERIAL_ORTHOGONAL_KINS
|
|
|
|
def joint_axis_map(self):
|
|
"""Return {joint_number: axis_letter} for mapped joints."""
|
|
return {j.number: j.axis for j in self.joints if j.axis}
|
|
|
|
|
|
def _dedupe(seq):
|
|
seen = set()
|
|
out = []
|
|
for item in seq:
|
|
if item not in seen:
|
|
seen.add(item)
|
|
out.append(item)
|
|
return out
|
|
|
|
|
|
def _axis_kind(letter):
|
|
return "ANGULAR" if letter in ANGULAR_LETTERS else "LINEAR"
|
|
|
|
|
|
def _packed_coordinates(raw):
|
|
"""Normalize a COORDINATES value ('X Y Z' or 'XYZAC') to 'XYZ...'."""
|
|
if not raw:
|
|
return ""
|
|
return "".join(ch for ch in raw.upper() if ch in AXIS_LETTERS)
|
|
|
|
|
|
def build_model(ini):
|
|
"""Build a KinematicModel from an IniReader."""
|
|
kins_line = (ini.find("KINS", "KINEMATICS", "trivkins") or "trivkins").strip()
|
|
parts = kins_line.split()
|
|
kins_module = parts[0] if parts else "trivkins"
|
|
kins_params = " ".join(parts[1:])
|
|
|
|
traj_coords = _packed_coordinates(ini.find("TRAJ", "COORDINATES", "XYZ"))
|
|
|
|
# The joint mapping is driven by the kins coordinates= param when present
|
|
# (e.g. gantry "XYZZ"); otherwise it mirrors the trajectory coordinates.
|
|
coord_map = _coordinates_param(kins_params) or traj_coords
|
|
|
|
joints_count = ini.find_int("KINS", "JOINTS", len(traj_coords)) or len(traj_coords)
|
|
|
|
axes = []
|
|
for letter in _dedupe(traj_coords):
|
|
section = "AXIS_%s" % letter
|
|
axes.append(Axis(
|
|
letter=letter,
|
|
kind=_axis_kind(letter),
|
|
vector=AXIS_VECTORS.get(letter, (0.0, 0.0, 0.0)),
|
|
min_limit=ini.find_float(section, "MIN_LIMIT"),
|
|
max_limit=ini.find_float(section, "MAX_LIMIT"),
|
|
))
|
|
|
|
joints = []
|
|
for jnum in range(joints_count):
|
|
section = "JOINT_%d" % jnum
|
|
letter = coord_map[jnum] if jnum < len(coord_map) else None
|
|
kind = (ini.find(section, "TYPE", "LINEAR") or "LINEAR").strip().upper()
|
|
joints.append(Joint(
|
|
number=jnum,
|
|
kind=kind,
|
|
axis=letter,
|
|
min_limit=ini.find_float(section, "MIN_LIMIT"),
|
|
max_limit=ini.find_float(section, "MAX_LIMIT"),
|
|
home=ini.find_float(section, "HOME"),
|
|
home_offset=ini.find_float(section, "HOME_OFFSET"),
|
|
))
|
|
|
|
return KinematicModel(
|
|
kins_module=kins_module,
|
|
kins_params=kins_params,
|
|
coordinates=coord_map,
|
|
joints_count=joints_count,
|
|
axes=axes,
|
|
joints=joints,
|
|
)
|
|
|
|
|
|
def _coordinates_param(params):
|
|
"""Extract a 'coordinates=XYZ' value from a kins parameter string."""
|
|
for token in params.split():
|
|
if token.lower().startswith("coordinates="):
|
|
return _packed_coordinates(token.split("=", 1)[1])
|
|
return ""
|