Source code for gcode_reader.emulate.commands

from dataclasses import dataclass, field, asdict
from typing import Tuple
from copy import copy

from ..read import apply_word_abstractions
from ..syntax import flavors
from . import tag_literals

_ATTR_CACHE: dict[type, tuple] = {}


[docs] class Command: """Base class for abstract representation of a G-code line's Words""" TAG = "" ATTRIBUTES = ( "G", "comment", "raw", "settings", ) _COLLECT_WORD_MAP_SETTINGS = True def __init__(self, **kwargs): self._words = {"G": None, "comment": "", "raw": ""} self.processed_by = None self.settings = {} if kwargs: for attr in self._get_all_attributes(): if attr in kwargs: setattr(self, attr, kwargs[attr]) @classmethod def _get_all_attributes(cls) -> tuple: """Walks the class hierarchy (MRO) to collect all ATTRIBUTES. Result is cached per class.""" if cls not in _ATTR_CACHE: all_attrs = set() for base_class in cls.__mro__: if hasattr(base_class, "ATTRIBUTES"): all_attrs.update(base_class.ATTRIBUTES) _ATTR_CACHE[cls] = tuple(all_attrs) return _ATTR_CACHE[cls]
[docs] @classmethod def from_words(cls, word_map=flavors["default"]["word_map"], **words): """Creates a new command from a set of words and word_map""" return cls._from_words_dict(word_map, words)
@classmethod def _from_words_dict(cls, word_map, words): """Internal: same as from_words but takes a pre-built dict to avoid kwargs unpack/repack.""" command = cls() command._words = apply_word_abstractions(word_map, words) attrs_to_set = cls._get_all_attributes() command._set_attributes_from_words(*attrs_to_set) command._collect_settings_from_words(word_map) return command def _collect_settings_from_words(self, word_map: dict): """Moves abstract word_map keys that weren't promoted to ATTRIBUTES into settings. Subclasses can set _COLLECT_WORD_MAP_SETTINGS = False to opt out (e.g. Config, which manages settings via its own __init__ kwargs path). """ if not self._COLLECT_WORD_MAP_SETTINGS: return if self.settings is None: self.settings = {} words = self._words # Intersection avoids iterating word_map keys that aren't in _words. for key in word_map.keys() & words.keys(): self.settings[key] = words.pop(key) def _set_attributes_from_words(self, *attributes: str): """Promotes specified keys from the internal `_words` dictionary to class attributes. This helper method iterates through a given sequence of attribute names. For each name, it removes the corresponding item from the `self._words` dictionary and sets it as a direct attribute on the instance (e.g., `self.location`). This is used to move well-defined parameters out of the generic word bucket and into typed, accessible properties. If an attribute is not found in `_words`, its corresponding instance attribute is set to `None`. Args: *attributes (str): A variable number of attribute names to process. """ for attr in attributes: value = self._words.pop(attr, None) setattr(self, attr, value) @property def raw(self): return getattr(self, "_raw", self._words.get("raw", "")) @raw.setter def raw(self, v): self._raw = str(v) if v is not None else "" @property def comment(self): return getattr(self, "_comment", self._words.get("comment", "")) @comment.setter def comment(self, v): self._comment = str(v) if v is not None else "" @property def process_data_type(self): return self._process_data_type
[docs] def to_dict(self) -> dict: data = {} for attr in self._get_all_attributes(): if hasattr(self, attr): data[attr] = getattr(self, attr) data["words"] = self._words # Add any remaining, unprocessed words return data
def __eq__(self, other) -> bool: if type(self) is not type(other): return False for attr in self._get_all_attributes(): if getattr(self, attr, None) != getattr(other, attr, None): return False return self._words == other._words
[docs] class Comment(Command): TAG = tag_literals.COMMENT
[docs] class Move(Command): """A Linear Movement to a location at a specified feed rate""" TAG = tag_literals.LINEAR_MOVE ATTRIBUTES = ("location", "feed_rate") + Command.ATTRIBUTES def __init__(self, location: list = None, feed_rate: float = None, **kwargs): super().__init__(location=location, feed_rate=feed_rate, **kwargs) @property def location(self): return self._location @location.setter def location(self, v): self._location = tuple(v) if v is not None else (None, None, None) @property def feed_rate(self): return self._feed_rate @feed_rate.setter def feed_rate(self, v): self._feed_rate = float(v) if v is not None else None
[docs] class Arc(Move): """An arced movement to a location at a specified feed rate about a center point""" TAG = tag_literals.ARC_MOVE ATTRIBUTES = ("arc_center",) + Move.ATTRIBUTES def __init__( self, arc_center: list = None, location: list = None, feed_rate: float = None, **kwargs, ): super().__init__( location=location, feed_rate=feed_rate, arc_center=arc_center, **kwargs ) @property def arc_center(self): return self._arc_center @arc_center.setter def arc_center(self, v): self._arc_center = tuple(v) if v is not None else (None, None) def __eq__(self, value): return super().__eq__(value) and self.arc_center == value.arc_center
[docs] class Extrude(Command): """Specifies a deposit amount for a toolhead""" TAG = tag_literals.EXTRUDE ATTRIBUTES = ("deposited_volume", "extrusion_rate") + Command.ATTRIBUTES def __init__( self, deposited_volume: float = None, **kwargs, ): super().__init__( deposited_volume=deposited_volume, **kwargs, ) @property def deposited_volume(self): return self._deposited_volume @deposited_volume.setter def deposited_volume(self, v): self._deposited_volume = float(v) if v is not None else None if v is not None: self._extrusion_rate = None @property def extrusion_rate(self): return self._extrusion_rate @extrusion_rate.setter def extrusion_rate(self, v): self._extrusion_rate = float(v) if v is not None else None if v is not None: self._deposited_volume = None
[docs] class MoveExtrude(Move, Extrude): """Specifies a composition of movement to a location at a feed rate, and an extrusion rate for a toolhead""" TAG = tag_literals.MOVE_EXTRUDE ATTRIBUTES = Move.ATTRIBUTES + Extrude.ATTRIBUTES def __init__( self, location: list = None, feed_rate: float = None, deposited_volume: float = 0.0, **kwargs, ): super().__init__( location=location, feed_rate=feed_rate, deposited_volume=deposited_volume, **kwargs, )
[docs] class ArcExtrude(Arc, Extrude): """Specifies a composition of arc movement to a location at a feed rate about a center point, and an extrusion rate for a toolhead""" TAG = f"{tag_literals.MOVE_EXTRUDE}{tag_literals.ARC_MOVE}" ATTRIBUTES = Arc.ATTRIBUTES + Extrude.ATTRIBUTES def __init__( self, arc_center: list = None, location: list = None, feed_rate: float = None, deposited_volume: float = 0, **kwargs, ): super().__init__( arc_center=arc_center, location=location, feed_rate=feed_rate, deposited_volume=deposited_volume, **kwargs, )
[docs] class Mill(Command): """Specifies a spindle rate for a milling toolhead""" TAG = tag_literals.MILL ATTRIBUTES = ("spindle",) + Command.ATTRIBUTES def __init__(self, spindle: float = None, **kwargs): super().__init__(spindle=spindle, **kwargs) @property def spindle(self): return self._spindle @spindle.setter def spindle(self, v): self._spindle = float(v) if v is not None else None
[docs] class MoveMill(Move, Mill): """Composition of a linear movement and a milling toolhead spindle rate""" TAG = tag_literals.MOVE_MILL ATTRIBUTES = Move.ATTRIBUTES + Mill.ATTRIBUTES def __init__( self, location: list = None, feed_rate: float = None, spindle=None, **kwargs ): super().__init__( location=location, feed_rate=feed_rate, spindle=spindle, **kwargs, )
[docs] class ArcMill(Arc, Mill): """Specifies a composition of arc movement to a location at a feed rate about a center point, and an extrusion rate for a toolhead""" TAG = f"{tag_literals.MOVE_MILL}{tag_literals.ARC_MOVE}" ATTRIBUTES = Arc.ATTRIBUTES + Mill.ATTRIBUTES def __init__( self, arc_center: list = None, location: list = None, feed_rate: float = None, spindle: float = None, **kwargs, ): super().__init__( arc_center=arc_center, location=location, feed_rate=feed_rate, spindle=spindle, **kwargs, )
[docs] class Dwell(Command): """Informs the machine to rest at the most recent location for a specified amount of time""" TAG = tag_literals.DWELL # NOTE: We need both time_s and time_ms attrs to successfully construct via Command.from_words(). Only store time_s though. ATTRIBUTES = ("time_s", "time_ms") + Command.ATTRIBUTES def __init__(self, time_ms: float = None, time_s: float = None, **kwargs): super().__init__(**kwargs) if time_s is not None and time_ms is not None: raise ValueError("Dwell.__init__: Cannot specify both time_s and time_ms.") if time_s is not None: self.time_s = time_s elif time_ms is not None: self.time_ms = time_ms else: self._time_s = None @property def time_ms(self): return self.time_s * 1000.0 if self.time_s is not None else 0.0 @time_ms.setter def time_ms(self, v): # NOTE: Since we store only time_s, we want to only update the underlying attr (_time_s) if there is a value provided to this setter. self._time_s = float(v) / 1000.0 if v is not None else self._time_s @property def time_s(self): return self._time_s if self._time_s is not None else 0.0 @time_s.setter def time_s(self, v): self._time_s = float(v) if v is not None else self._time_s
[docs] class Config(Command): """Class for codes that modify a machine's configuration""" TAG = tag_literals.CONFIG ATTRIBUTES = ("code", "settings") + Command.ATTRIBUTES _COLLECT_WORD_MAP_SETTINGS = False # Manually override promotion of additional settings in .from_words() classmethod def __init__(self, code: int = None, settings: dict = None, **kwargs): super().__init__(**kwargs) self._code = None if code is not None: self.code = code self.settings = {} if settings is not None: self.settings.update(settings) self.settings.update(kwargs) @property def code(self): return self._code @code.setter def code(self, v): if isinstance(v, (list, tuple)): self._code = [int(c) for c in v] elif v is not None: self._code = int(v) # NOTE: Cannot set to None via property setter, because M assigns 'M' property to 'code'. This avoids Command.from_words() race condition @property def is_modal_group(self): return isinstance(self.code, (list, tuple))
[docs] class M(Config): """A M-code configuration command.""" TAG = f"#M{Config.TAG}" ATTRIBUTES = ("M",) + Config.ATTRIBUTES @property def M(self): return self._code @M.setter def M(self, v): self.code = v
[docs] class G(Config): """A G-code configuration command.""" TAG = f"#G{Config.TAG}" @property def G(self): return self._code @G.setter def G(self, v): self.code = v
[docs] class FeedRate(Config): """A feedrate configuration command. Used to only update the feedrate state.""" TAG = f"{tag_literals.FEED_RATE}"
[docs] class Purge(Dwell, Extrude): """Dwells while extruding""" TAG = f"{tag_literals.DWELL}{tag_literals.EXTRUDE}" ATTRIBUTES = ( ("feed_rate",) + Dwell.ATTRIBUTES + Extrude.ATTRIBUTES + Config.ATTRIBUTES ) def __init__(self, time_ms=None, deposited_volume: float = None, **kwargs): super().__init__( time_ms=time_ms, deposited_volume=deposited_volume, **kwargs, )
[docs] class AbsolutePosition(Config): """An Absolute Position configuration command. Typically G90.""" TAG = f"{tag_literals.ABSOLUTE_POS}"
[docs] class IncrementalPosition(Config): """An Incremental Position configuration command. Typically G91.""" TAG = f"{tag_literals.INCREMENTAL_POS}"
[docs] class CoordinateSystemChange(Config): """A coordinate-system selection command that may carry target coordinates. This is a configuration command (not a motion command). It may optionally carry XYZ coordinates — e.g. ``G53 X0 Y0 Z0`` — which are stored in ``location``. It inherits from ``Config``, not ``Move``, so that ``isinstance(cmd, Move)`` correctly returns ``False`` for these commands. """ ATTRIBUTES = ("location",) + Config.ATTRIBUTES TAG = "" def __init__(self, location=None, **kwargs): super().__init__(**kwargs) self.location = location @property def location(self): return self._location @location.setter def location(self, v): self._location = tuple(v) if v is not None else (None, None, None)
[docs] class WorkCoordinates(CoordinateSystemChange): """A Work Coordinate System command. Typically G54.""" TAG = f"{tag_literals.WORK_COORD}"
[docs] class MachineCoordinates(CoordinateSystemChange): """A Machine Coordinate System command. Typically G53.""" TAG = f"{tag_literals.MACHINE_COORD}"