diff --git a/pygcode/file.py b/pygcode/file.py index ed8d780..44fab3f 100644 --- a/pygcode/file.py +++ b/pygcode/file.py @@ -1,6 +1,6 @@ from .line import Line -from .machine import AbstractMachine +#from .machine import AbstractMachine class GCodeFile(object): def __init__(self, filename=None): @@ -14,10 +14,6 @@ class GCodeFile(object): self.lines.append(line) -class GCodeWriterMachine(AbstractMachine): - def machine_init(self, *args, **kwargs): - pass - def parse(filename): # FIXME: should be an iterator, and also not terrible file = GCodeFile() diff --git a/pygcode/machine.py b/pygcode/machine.py index 91c82a0..646aa0d 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -1,70 +1,24 @@ +from collections import defaultdict + +from .gcodes import MODAL_GROUP_MAP, GCode +from .line import Line + +from .machinestate import MachineState + -class MachineState(object): - def __init__(self, axes=('x', 'y', 'z')): - self.axes = axes +class Machine(object): + def __init__(self): + self.state = MachineState() - # initialize - self.position = {} - for axis in self.axes: - self.position[axis] = 0 + def process(self, *gcode_list, **kwargs): + """ + Process gcodes + :param gcode_list: list of GCode instances + :param modal_params: list of Word instances to be applied to current movement mode + """ + modal_params = kwargs.get('modal_params', []) + for gcode in sorted(gcode_list): + self.state.set_mode(gcode) # if gcode is not modal, it's ignored - self.time = 0 - - - -class AbstractMachine(object): - """Basis for a real / virtualized machine to process gcode""" - def __init__(self, *args, **kwargs): - self.axes = kwargs.get('axes', ('x', 'y', 'z')) - self.max_rate = kwargs.get('max_rate', { - 'x': 500, # mm/min - 'y': 500, # mm/min - 'z': 500, # mm/min - }) - self.max_travel = kwargs.get('max_travel', { - 'x': 200, # mm - 'y': 200, # mm - 'z': 200, # mm - }) - self.max_spindle_speed = kwargs.get('max_spindle_speed', 1000) # rpm - self.min_spindle_speed = kwargs.get('max_spindle_speed', 0) # rpm - - # initialize - self.state = MachineState(self.axes) - - # machine-specific initialization - self.machine_init(*args, **kwargs) - - def machine_init(self, *args, **kwargs): - # Executed last in instances' __init__ call. - # Parameters are identical to that of __init__ - pass - - def process_line(self, line): - """Change machine's state based on the given gcode line""" - pass # TODO - - - - -""" -class Axes(object): - pass - -class MyMachineState(MachineState): - axes_state_class = AxesState - pass - -class MyMachine(AbstractMachine): - available_axes = set('xyz') - state_class = MyMachineState - - -m = MyMachine( - state=MyMachineState( - absolute_position= - ), -) - -""" + # TODO: gcode instance to change machine's state diff --git a/pygcode/machinestate.py b/pygcode/machinestate.py new file mode 100644 index 0000000..1e550bf --- /dev/null +++ b/pygcode/machinestate.py @@ -0,0 +1,97 @@ +from .gcodes import GCode +from .line import Line + + +class State(object): + """State of a Machine""" + # State is very forgiving: + # Anything possible in a machine's state may be changed & fetched. + # For example: x, y, z, a, b, c may all be set & requested. + # However, the machine for which this state is stored probably doesn't + # have all possible 6 axes. + # It is also possible to set an axis to an impossibly large distance. + # It is the responsibility of the Machine using this class to be + # discerning in these respects. + def __init__(self): + self.axes = defaultdict(lambda: 0.0) # aka: "machine coordinates" + + self.work_offset = defaultdict(lambda: 0.0) + # TODO: how to manage work offsets? (probs not like the above) + # read up on: + # - G92: coordinate system offset + # - G54-G59: select coordinate system (offsets from machine coordinates set by G10 L2) + + # TODO: Move this class into MachineState + + +class MachineState(object): + """Machine's state, and mode""" + # Mode is defined by gcodes set by processed blocks: + # see modal_group in gcode.py module for details + def __init__(self): + self.modal_groups = defaultdict(lambda: None) + # populate with all groups + for modal_group in MODAL_GROUP_MAP.values(): + self.modal_groups[modal_group] = None + + # Default mode: + self.set_mode(*Line(''' + G17 (plane_selection: X/Y plane) + G90 (distance: absolute position. ie: not "turtle" mode) + G91.1 (arc_ijk_distance: IJK sets arc center vertex relative to current position) + G94 (feed_rate_mode: feed-rate defined in units/min) + G21 (units: mm) + G40 (cutter_diameter_comp: no compensation) + G49 (tool_length_offset: no offset) + G61 (control_mode: exact path mode) + G97 (spindle_speed_mode: RPM Mode) + M0 (stopping: program paused) + M5 (spindle: off) + M9 (coolant: off) + F100 (feed_rate: 100 mm/min) + S1000 (tool: 1000 rpm, when it's switched on) + ''').block.gcodes) # note: although this is not a single line + # '\n' is just treated like any other whitespace, + # so it behaves like a single line. + + def set_mode(self, *gcode_list): + for g in sorted(gcode_list): # sorted by execution order + if g.modal_group is not None: + self.modal_groups[g.modal_group] = g + + def __getattr__(self, key): + if key in MODAL_GROUP_MAP: + return self.modal_groups[MODAL_GROUP_MAP[key]] + + raise AttributeError("'{cls}' object has no attribute '{key}'".format( + cls=self.__class__.__name__, + key=key + )) + + def __setattr__(self, key, value): + if key in MODAL_GROUP_MAP: + # Set/Clear modal group gcode + if value is None: + # clear mode group + self.modal_groups[MODAL_GROUP_MAP[key]] = None + else: + # set mode group explicitly + # (recommended to use self.set_mode(value) instead) + assert isinstance(value, GCode), "invalid value type: %r" % value + assert value.modal_group == MODAL_GROUP_MAP[key], \ + "cannot set '%s' mode as %r, wrong group" % (key, value) + self.modal_groups[MODAL_GROUP_MAP[key]] = value + else: + self.__dict__[key] = value + + def __str__(self): + gcode_list = [] + for modal_group in sorted(MODAL_GROUP_MAP.values()): + if self.modal_groups[modal_group]: + gcode_list.append(self.modal_groups[modal_group]) + return ' '.join(str(g) for g in gcode_list) + + def __repr__(self): + return "<{class_name}: {gcodes}>".format( + class_name=self.__class__.__name__, gcodes=str(self) + ) diff --git a/pygcode/words.py b/pygcode/words.py index 47a6bbe..5ced857 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -4,7 +4,6 @@ import six from .exceptions import GCodeBlockFormatError - REGEX_FLOAT = re.compile(r'^-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float REGEX_INT = re.compile(r'^-?\d+') REGEX_POSITIVEINT = re.compile(r'^\d+') @@ -201,7 +200,15 @@ WORD_MAP = { class Word(object): - def __init__(self, letter, value): + def __init__(self, *args): + assert len(args) in [1, 2], "input arguments either: (letter, value) or (word_str)" + if len(args) == 2: + (letter, value) = args + else: + word_str = args[0] + letter = word_str[0] # first letter + value = word_str[1:] # rest of string + self.letter = letter.upper() self._value_str = None