Source code for layerview.gcode.commands

"""G-code commands and command parsing logic."""
from __future__ import annotations

from abc import ABC, abstractmethod
from functools import lru_cache
from typing import Any, Callable, Dict, List, Optional, Type

import layerview.gcode.tokens as gcode_tokens
from layerview.gcode import utils
from layerview.gcode.lexemes import LETTERS_COMMAND, LETTERS_PARAM
from layerview.gcode.tokens import GcodeToken

# Exceptions


[docs]class CommandParseError(Exception): pass
[docs]class CommandTokenError(CommandParseError): pass
[docs]class CommandMissingParamError(CommandParseError): pass
[docs]class CommandInvalidParamError(CommandParseError): pass
# Commands
[docs]class Command(ABC): """Generic G-code command.""" __slots__ = ("line_number", "__dict__") def __init__(self, line_number: int = None): self.line_number: int = line_number @abstractmethod @lru_cache def __repr__(self): pass
[docs]class CommandWithParams(Command, ABC): """Generic G-code command with params.""" @property @abstractmethod def param_letters(self) -> List[str]: pass
[docs]class CommandWithoutParams(Command): """Generic G-code command without params.""" def __repr__(self): return f"{self.__class__.__name__}(line_number={self.line_number})"
[docs]class Move(CommandWithParams, ABC): """Move command base class.""" __slots__ = utils.str_to_chars("xyef") def __init__( self, x: Optional[float] = None, y: Optional[float] = None, e: Optional[float] = None, f: Optional[float] = None, line_number: int = None, ): super().__init__(line_number) self.x: Optional[float] = x self.y: Optional[float] = y self.e: Optional[float] = e self.f: Optional[float] = f
[docs]class LineMove(Move): """Generic Line Move""" param_letters = utils.str_to_chars("xyzefhrs") __slots__ = utils.str_to_chars("zhrs") def __init__( self, x: float = None, y: float = None, z: float = None, e: float = None, f: float = None, h: float = None, r: float = None, s: float = None, line_number: int = None, ): if not any([elem is not None for elem in [x, y, z, e, f, h, r, s]]): raise CommandMissingParamError( "At least one G-code parameter must be specified." ) super().__init__(x=x, y=y, e=e, f=f, line_number=line_number) self.z = z self.h = h self.r = r self.s = s def __repr__(self): return ( f"{self.__class__.__name__}" f"(line_number={self.line_number}, " f"x={self.x}, y={self.y}, z={self.z}, e={self.e}, " f"f={self.f}, h={self.h}, r={self.r}, s={self.s})" )
[docs]class G0(LineMove): """Rapid Move.""" pass
[docs]class G1(LineMove): """Linear Move.""" pass
[docs]class ArcMove(Move): """Generic Arc Move.""" param_letters = utils.str_to_chars("xyijef") def __init__( self, x: float = None, y: float = None, i: float = None, j: float = None, e: float = None, f: float = None, line_number: int = None, ): params_mandatory = {"x": x, "y": y, "i": i, "j": j, "e": e} if not all([elem is not None for elem in params_mandatory.values()]): raise CommandMissingParamError( f"The following parameters must be specified: " f"{', '.join(params_mandatory.keys())}." ) super().__init__(x=x, y=y, e=e, f=f, line_number=line_number) self.i = i self.j = j def __repr__(self): return ( f"{self.__class__.__name__}" f"(line_number={self.line_number}, " f"x={self.x}, y={self.y}, i={self.i}, " f"j={self.j}, e={self.e}, f={self.f})" )
[docs]class G2(ArcMove): """Clockwise Arc.""" pass
[docs]class G3(ArcMove): """Counter-Clockwise Arc.""" pass
[docs]class SetTemperatureExtruder(CommandWithParams, ABC): """Generic set temperature command.""" def __init__(self, s: float, r: float = None, line_number: int = None): super().__init__(line_number=line_number) self.s = s self.r = r
[docs]class M104(SetTemperatureExtruder): """Set Extruder Temperature""" param_letters = utils.str_to_chars("srd") def __init__( self, s: float, r: float = None, d: float = None, line_number: int = None ): super().__init__(s=s, r=r, line_number=line_number) self.d = d def __repr__(self): return ( f"{self.__class__.__name__}" f"(line_number={self.line_number}, " f"s={self.s}, r={self.r}, d={self.d})" )
[docs]class M109(SetTemperatureExtruder): """Set Extruder Temperature and Wait""" param_letters = utils.str_to_chars("srt") def __init__( self, s: float, r: float = None, t: int = None, line_number: int = None ): super().__init__(s=s, r=r, line_number=line_number) self.t = t def __repr__(self): return ( f"{self.__class__.__name__}" f"(line_number={self.line_number}, " f"s={self.s}, r={self.r}, t={self.t})" )
[docs]class G28(CommandWithParams): """Move to origin (home).""" param_letters = utils.str_to_chars("xyz") def __init__(self, x: bool, y: bool, z: bool, line_number: int = None): super().__init__(line_number=line_number) self.x = x self.y = y self.z = z def __repr__(self): return ( f"{self.__class__.__name__}" f"(line_number={self.line_number}, " f"x={self.x}, y={self.y}, z={self.z})" )
[docs]class G92(CommandWithParams): """Set position.""" param_letters = utils.str_to_chars("xyze") def __init__( self, x: float = None, y: float = None, z: float = None, e: float = None, line_number: int = None, ): if not any([elem is not None for elem in [x, y, z, e]]): raise CommandMissingParamError("At least one axis must be specified.") super().__init__(line_number=line_number) self.x = x self.y = y self.z = z self.e = e def __repr__(self): return ( f"{self.__class__.__name__}" f"(line_number={self.line_number}, " f"x={self.x}, y={self.y}, z={self.z})" )
# Commands without parameters
[docs]class G20(CommandWithoutParams): """Set units to inches.""" pass
[docs]class G21(CommandWithoutParams): """Set units to millis.""" pass
[docs]class G90(CommandWithoutParams): """Set absolute positioning.""" pass
[docs]class G91(CommandWithoutParams): """Set relative positioning.""" pass
[docs]class M82(CommandWithoutParams): """Set extruder absolute mode.""" pass
[docs]class M83(CommandWithoutParams): """Set extruder relative mode.""" pass
COMMANDS_LINE_MOVE = (G0, G1) COMMANDS_ARC_MOVE = (G2, G3) COMMANDS_MOVE = (*COMMANDS_LINE_MOVE, *COMMANDS_ARC_MOVE) COMMANDS_SET_TEMPERATURE = [M104, M109]
[docs]class CommandParser: def __init__(self, skip_unknown: Optional[bool] = False): """ Parameters ---------- skip_unknown : Optional[bool] If True, parsing unknown commands will not raise an error. """ self._skip_unknown: bool = skip_unknown self._command_token_to_handler: Dict[GcodeToken, Callable] = { gcode_tokens.G0_RAPID_MOVE: CommandParser._create_g0, gcode_tokens.G1_LINEAR_MOVE: CommandParser._create_g1, gcode_tokens.G2_CLOCKWISE_ARC_MOVE: CommandParser._create_g2, gcode_tokens.G3_COUNTERCLOCKWISE_ARC_MOVE: CommandParser._create_g3, gcode_tokens.G28_MOVE_TO_ORIGIN: CommandParser._create_g28, gcode_tokens.G92_SET_POSITION: CommandParser._create_g92, gcode_tokens.M104_SET_EXTRUDER_TEMP: CommandParser._create_m104, gcode_tokens.M109_SET_EXTRUDER_TEMP_AND_WAIT: CommandParser._create_m109, gcode_tokens.G20_SET_UNITS_TO_INCHES: CommandParser._create_g20, gcode_tokens.G21_SET_UNITS_TO_MILLIS: CommandParser._create_g21, gcode_tokens.G90_SET_ABS_POSITIONING: CommandParser._create_g90, gcode_tokens.G91_SET_REL_POSITIONING: CommandParser._create_g91, gcode_tokens.M82_SET_EXTRUDER_ABS_MODE: CommandParser._create_m82, gcode_tokens.M83_SET_EXTRUDER_REL_MODE: CommandParser._create_m83, }
[docs] def parse(self, tokens: List[GcodeToken]) -> List[Command]: """ Parameters ---------- tokens : List[GcodeToken] List of G-code tokens, representing G-code fields. Expecting [(param, value), (param, value)]. E.g. [('g', '1'), ('x', '10.25'), ('y', '22')]. Returns ------- commands : List[Command] Parsed commands. May be empty if no tokens are present. Raises ------ CommandParseError If tokens contain unknown commands and skip_unknown is False. """ tokens_grouped: List[List[GcodeToken]] = CommandParser._group_tokens_by_command( tokens=tokens ) commands: List[Command] = [] for token_group in tokens_grouped: command_token: GcodeToken = token_group[0] handler = self._get_handler(command_token) if handler: commands.append(handler(token_group)) elif not self._skip_unknown: raise CommandParseError(f"Unsupported command: {token_group}") return commands
[docs] def _get_handler(self, command_token: GcodeToken) -> Callable: """Get factory method for G-code command. Parameters ---------- command_token : GcodeToken Command GcodeToken for which the factory method is to be returned. Returns ------- Callable Factory method for specific G-code command. """ return self._command_token_to_handler.get(command_token)
[docs] @staticmethod def _group_tokens_by_command(tokens: List[GcodeToken]) -> List[List[GcodeToken]]: """Groups tokens by the command they correspond to. Parameters ---------- tokens : List[GcodeToken] List of G-code tokens to group by the command they correspond to. Returns ------- groups : List[List[GcodeToken]] List of lists of G-code tokens. """ groups: List[List[GcodeToken]] = [] current_group: List[GcodeToken] = [] for field in tokens: letter = field[0] if letter in LETTERS_COMMAND: # Start of a new command if current_group: groups.append(current_group) current_group = [] elif letter in LETTERS_PARAM: if not current_group: raise CommandTokenError( "Parameter field specified without a previous command field." ) else: raise CommandTokenError( f"Unrecognized G-code field letter {repr(letter)}." ) current_group.append(field) # Command finished with line end if current_group: groups.append(current_group) return groups
[docs] @staticmethod def _create_from_float_params(tokens: List[GcodeToken], command_type: Type) -> Any: param_to_value = {} param_tokens = tokens[1:] for token_letter, value_str in param_tokens: if token_letter not in command_type.param_letters: # Unrecognized parameter for current command raise CommandInvalidParamError( f"Unrecognized parameter letter for " f"{command_type}: {repr(token_letter)}" ) param_to_value[token_letter] = float(value_str) return command_type(**param_to_value)
[docs] @staticmethod def _create_g0(tokens: List[GcodeToken]) -> G0: return CommandParser._create_from_float_params(tokens, G0)
[docs] @staticmethod def _create_g1(tokens: List[GcodeToken]) -> G1: return CommandParser._create_from_float_params(tokens, G1)
[docs] @staticmethod def _create_g2(tokens: List[GcodeToken]) -> G2: return CommandParser._create_from_float_params(tokens, G2)
[docs] @staticmethod def _create_g3(tokens: List[GcodeToken]) -> G3: return CommandParser._create_from_float_params(tokens, G3)
[docs] @staticmethod def _create_g28(tokens: List[GcodeToken]) -> G28: param_to_value: Dict[str, bool] = {} param_to_value.update(dict.fromkeys(G28.param_letters, False)) for param_token, _ in tokens: if param_token in G28.param_letters: param_to_value[param_token] = True return G28(**param_to_value)
[docs] @staticmethod def _create_g92(tokens: List[GcodeToken]) -> G92: return CommandParser._create_from_float_params(tokens, G92)
[docs] @staticmethod def _create_m104(tokens: List[GcodeToken]) -> M104: return CommandParser._create_from_float_params(tokens, M104)
[docs] @staticmethod def _create_m109(tokens: List[GcodeToken]) -> M109: return CommandParser._create_from_float_params(tokens, M109)
# Commands without params
[docs] @staticmethod def _create_g20(_: List[GcodeToken]) -> G20: return G20()
[docs] @staticmethod def _create_g21(_: List[GcodeToken]) -> G21: return G21()
[docs] @staticmethod def _create_g90(_: List[GcodeToken]) -> G90: return G90()
[docs] @staticmethod def _create_g91(_: List[GcodeToken]) -> G91: return G91()
[docs] @staticmethod def _create_m82(_: List[GcodeToken]) -> M82: return M82()
[docs] @staticmethod def _create_m83(_: List[GcodeToken]) -> M83: return M83()