diff --git a/PythonicGcodeMachine/Gcode/Rs274/Coordinate.py b/PythonicGcodeMachine/Gcode/Rs274/Coordinate.py
new file mode 100644
index 0000000000000000000000000000000000000000..260979aa63bb1855806a0fc9658d7922c452a104
--- /dev/null
+++ b/PythonicGcodeMachine/Gcode/Rs274/Coordinate.py
@@ -0,0 +1,92 @@
+####################################################################################################
+#
+# PythonicGcodeMachine - A Python G-code Toolkit
+# Copyright (C) 2018 Fabrice Salvaire
+#
+# 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 3 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, see .
+#
+####################################################################################################
+
+"""Module to implement a machine coordinate.
+"""
+
+####################################################################################################
+
+__all__ = [
+ 'Coordinate',
+]
+
+####################################################################################################
+
+import numpy as np
+
+####################################################################################################
+
+class Coordinate:
+
+ ##############################################
+
+ def __init__(self, *args, dimension=None):
+
+ if args:
+ self._v = numpy.array(args)
+ else:
+ self._v = numpy.zeros(dimension)
+
+ ##############################################
+
+ def clone(self):
+ return self.__class__(self._v)
+
+ ##############################################
+
+ @property
+ def dimension(self):
+ return self._v.shape[0]
+
+ ##############################################
+
+ def __len__(self):
+ return self.dimension
+
+ def __iter__(self):
+ return iter(self._v)
+
+ def __getitem__(self, slice_):
+ return self._v[slice_]
+
+ ##############################################
+
+ def set(self, v):
+ if isintance(self, Coordinate):
+ self._v = self._v
+ else:
+ self._v = v
+
+ ##############################################
+
+ def __eq__(self, v):
+ return self._v == self._v
+
+ ##############################################
+
+ def __iadd__(self, v):
+ self._v += self._v
+ return self
+
+ ##############################################
+
+ def __isub__(self, v):
+ self._v -= self._v
+ return self
diff --git a/PythonicGcodeMachine/Gcode/Rs274/MachineState.py b/PythonicGcodeMachine/Gcode/Rs274/MachineState.py
new file mode 100644
index 0000000000000000000000000000000000000000..18154bbc67b8aa8d15981f494caebbbf391aed7a
--- /dev/null
+++ b/PythonicGcodeMachine/Gcode/Rs274/MachineState.py
@@ -0,0 +1,199 @@
+####################################################################################################
+#
+# PythonicGcodeMachine - A Python G-code Toolkit
+# Copyright (C) 2018 Fabrice Salvaire
+#
+# 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 3 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, see .
+#
+####################################################################################################
+
+"""Module to implement a basic G-code machine.
+"""
+
+####################################################################################################
+
+__all__ = [
+ 'MachineState',
+]
+
+####################################################################################################
+
+from enum import Enum, auto
+
+from .Coordinate import Coordinate
+from .ToolSet import ToolSet
+
+####################################################################################################
+
+class PlaneSelection:
+ XY = auto()
+ XZ = auto()
+ YZ = auto()
+
+class FeedRateMode:
+ UNITS_PER_MINUTE = auto()
+ INVERSE_TIME = auto()
+
+####################################################################################################
+
+class MachineState:
+
+ NUMBER_OF_COORDINATE_SYSTEMS = 9
+
+ ##############################################
+
+ def __init__(self,
+ number_of_axes,
+ is_metric=True,
+ ):
+
+ self._number_of_axes = int(number_of_axes)
+ self._coordinate = Coordinate(dimension=self._number_of_axes)
+
+ self._tool_set = ToolSet()
+ self._tool = None # T
+
+ self._is_metric = bool(is_metric)
+ self._use_metric = self._is_metric # G20 inch / G21 mm
+ self._use_absolut = True # G90 absolut / G91 incremental
+
+ # G17 XY-plane selection
+ # G18 XZ-plane selection
+ # G19 YZ-plane selection
+ self._plane = None
+
+ # G54 G55 G56 G57 G58 G59 G59.1 G59.2 G59.3
+ self._coordinate_system = None
+
+ self._feed_rate = 0 # F
+ # G93 units per minute
+ # G94 inverse time
+ self._feed_rate_mode = FeedRateMode.UNITS_PER_MINUTE
+
+ self._spindle_rate = 0 # S
+
+ ### 4 M0 M1 M2 M30 M60 stopping
+ ### 6 M6 tool change
+ ### 7 M3 M4 M5 spindle turning
+ ### 8 M7 M8 M9 coolant (special case: M7 and M8 may be active at the same time)
+ ### 9 M48 M49 enable/disable feed and speed override switches
+ ### 10 G98 G99 return mode in canned cycles
+ ### 13 G61 G61.1 G64 path control mode
+
+ ##############################################
+
+ @property
+ def number_of_axes(self):
+ return self._number_of_axes
+
+ @property
+ def coordinate(self):
+ return self._coordinate
+
+ ##############################################
+
+ @property
+ def is_metric(self):
+ return self._is_metric
+
+ @property
+ def use_metric(self):
+ return self._use_metric
+
+ @use_metric.setter
+ def use_metric(self, value):
+ self._use_metric = bool(value)
+
+ ##############################################
+
+ @property
+ def use_absolut(self):
+ return self._use_absolut
+
+ @use_absolut.setter
+ def use_absolut(self, value):
+ self._use_absolut = bool(value)
+
+ ##############################################
+
+ @property
+ def plane(self):
+ return self._plane
+
+ @plane.setter
+ def plane(self, value):
+ self._plane = PlaneSelection(value)
+
+ ##############################################
+
+ @property
+ def coordinate_system(self):
+ return self._coordinate_system
+
+ @coordinate_system.setter
+ def coordinate_system(self, value):
+ _value = int(value)
+ if not(1 <= _value <= self.NUMBER_OF_COORDINATE_SYSTEMS):
+ raise ValueError('Invalid coordinate system {}'.format(value))
+ self._coordinate_system = _value
+
+ ##############################################
+
+ @property
+ def tool_set(self):
+ return self._tool_set
+
+ def load_tool(self, pocket):
+ """Load the tool at the given carousel pocket.
+
+ Raise ValueError if KeyError.
+ """
+ self._tool.toggle_loaded()
+ try:
+ self._tool = self._tool_set[pocket].toggle_loaded()
+ except KeyError:
+ raise ValueError('Invalid carousel pocket {}'.format(pocket))
+
+ ##############################################
+
+ @property
+ def feed_rate(self):
+ return self._feed_rate
+
+ @feed_rate.setter
+ def feed_rate(self, value):
+ # negative feedback ?
+ self._feed_rate = float(value)
+
+ @property
+ def feed_rate_mode(self):
+ return self._feed_rate_mode
+
+ @feed_rate_mode.setter
+ def feed_rate_mode(self, value):
+ self._feed_rate_mode = FeedRateMode(value)
+
+ ##############################################
+
+ @property
+ def spindle_rate(self):
+ return self._spindle_rate
+
+ @spindle_rate.setter
+ def spindle_rate(self, value):
+ _value = float(value)
+ if _value < 0:
+ raise ValueError('Negative spindle rate {}'.format(value))
+ self._spindle_rate = _value
+
diff --git a/PythonicGcodeMachine/Gcode/Rs274/Tool.py b/PythonicGcodeMachine/Gcode/Rs274/Tool.py
new file mode 100644
index 0000000000000000000000000000000000000000..88aa837b19232600a278a72bcaa3f7a7fda6451d
--- /dev/null
+++ b/PythonicGcodeMachine/Gcode/Rs274/Tool.py
@@ -0,0 +1,268 @@
+####################################################################################################
+#
+# PythonicGcodeMachine - A Python G-code Toolkit
+# Copyright (C) 2018 Fabrice Salvaire
+#
+# 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 3 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, see .
+#
+####################################################################################################
+
+"""Module to implement a basic tool set.
+"""
+
+####################################################################################################
+
+__all__ = [
+ 'Tool',
+ 'LatheTool',
+ 'ToolSet',
+]
+
+####################################################################################################
+
+import yaml
+
+####################################################################################################
+
+class Tool:
+
+ """Class to define a tool"""
+
+ ##############################################
+
+ def __init__(self, tool_id, offset, diameter=None, comment=None):
+
+ self._id = tool_id
+ self._tool_offset = offset
+ self._tool_diameter = diameter
+ self._comment = comment
+
+ self._tool_set = None
+ self._pocket = None
+
+ self._loaded = False
+
+ ##############################################
+
+ @property
+ def id(self):
+ return self._id
+
+ @tool_id.setter
+ def id(self, value):
+ self._id = str(value)
+
+ @property
+ def offset(self):
+ return self._offset
+
+ @offset.setter
+ def offset(self, value):
+ self._offset = value
+
+ @property
+ def diameter(self):
+ return self._diameter
+
+ @diameter.setter
+ def diameter(self, value):
+ self._diameter = float(value)
+
+ @property
+ def comment(self):
+ return self._comment
+
+ @comment.setter
+ def comment(self, value):
+ self._comment = str(value)
+
+ ##############################################
+
+ @property
+ def tool_set(self):
+ return self._tool_set
+
+ @tool_set.setter
+ def tool_set(self, value):
+ self._tool_set = value
+
+ @property
+ def pocket(self):
+ return self._pocket
+
+ @pocket.setter
+ def pocket(self, value):
+ self._pocket = int(value)
+
+ ##############################################
+
+ @property
+ def loaded(self):
+ return self._loaded
+
+ @loaded.setter
+ def loaded(self, value):
+ self._loaded = bool(value)
+
+ def toggle_loaded(self):
+ self._loaded = not self._loaded
+ return self
+
+ ##############################################
+
+ def _to_dict(self, d, keys):
+ for key in keys:
+ d[key] = geattr(self, key)
+
+ ##############################################
+
+ def to_dict(self):
+
+ keys = (
+ 'id',
+ 'tool_offset',
+ 'tool_diameter',
+ 'comment',
+ 'pocket',
+ )
+
+ return self._to_dict({}, keys)
+
+####################################################################################################
+
+class LatheTool(Tool):
+
+ """Class to define a lathe tool"""
+
+ ##############################################
+
+ def __init__(self, tool_id, offset, front_angle, back_angle, orientation,
+ diameter=None, comment=None):
+
+ super(). __init__(tool_id, offset, diameter, comment)
+
+ self._front_angle = front_angle
+ self._back_angle = back_angle
+ self._orientation = orientation
+
+ ##############################################
+
+ @property
+ def front_angle(self):
+ return self._front_angle
+
+ @front_angle.setter
+ def front_angle(self, value):
+ self._front_angle = float(value)
+
+ @property
+ def back_angle(self):
+ return self._back_angle
+
+ @back_angle.setter
+ def back_angle(self, value):
+ self._back_angle = float(value)
+
+ @property
+ def orientation(self):
+ return self._orientation
+
+ @orientation.setter
+ def orientation(self, value):
+ self._orientation = int(value)
+
+ ##############################################
+
+ def to_dict(self):
+
+ d = super().to_dict()
+ keys = (
+ 'front_angle',
+ 'back_angle',
+ 'orientation',
+ )
+ self._to_dict(d, keys)
+
+ return d
+
+####################################################################################################
+
+class ToolSet:
+
+ """Class to define a tool set"""
+
+ ##############################################
+
+ def __init__(self):
+
+ self._tools = {} # Free pocket implementation
+
+ ##############################################
+
+ def __len__(self):
+ return len(self._tools)
+
+ def __iter__(self):
+ return iter(self._tools.values())
+
+ def __getitem__(self, pocket):
+ return self._tools[pocket]
+
+ ##############################################
+
+ def remove_tool(self, pocket):
+ if isinstance(pocket, Tool):
+ pocket = pocket.pocket
+ tool = self._tools.pop(pocket)
+ tool.tool_set = None
+ tool.pocket = None
+ return tool
+
+ ##############################################
+
+ def add_tool(self, tool, pocket):
+ old_tool = self.remove_tool(pocket)
+ self._tools[pocket] = tool
+ tool.tool_set = self
+ tool.pocket = pocket
+ return old_tool
+
+ ##############################################
+
+ def load_yaml(self, path):
+
+ with (open(path, 'r')) as fh:
+ yaml_data = yaml.load(fh.read())
+
+ for pocket, d in yaml_data.items():
+ if 'front_angle' in d:
+ cls = LatheTool
+ else:
+ cls = Tool
+ tool = cls(*d)
+ self.add_tool(tool, pocket)
+
+ ##############################################
+
+ def write_yaml(self, path):
+
+ data = {}
+ for tool in self:
+ d = tool.to_dict()
+ del d['pocket']
+ data[tool.pocket] = d
+
+ yaml_data = yaml.dump(data, default_flow_style=False)
+ with (open(path, 'w')) as fh:
+ fh.write(yaml_data)
diff --git a/requirements.txt b/requirements.txt
index 6e4516b3dc77fc40edd23c647522fb412356bb1e..85b64e2fc5178850a7395fc98b73cd4919cdf9e9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
ansicolors
+numpy
ply>=3.11
PyYAML>=3.13