machine processing underway

This commit is contained in:
Peter Boin 2017-07-08 22:18:58 +10:00
parent 3674f9d6aa
commit ec6ebb2870
12 changed files with 661 additions and 105 deletions

View File

@ -1,39 +1,50 @@
import re
from .words import iter_words, WORD_MAP
from .words import text2words, WORD_MAP
from .gcodes import words2gcodes
class Block(object):
"""GCode block (effectively any gcode file line that defines any <word><value>)"""
def __init__(self, text):
def __init__(self, text=None, verify=True):
"""
Block Constructor
:param A-Z: gcode parameter values
:param comment: comment text
"""
self._raw_text = text # unaltered block content (before alteration)
self._raw_text = None
self._text = None
self.words = []
self.gcodes = []
self.modal_params = []
# clean up block string
text = re.sub(r'(^\s+|\s+$)', '', text) # remove whitespace padding
text = re.sub(r'\s+', ' ', text) # remove duplicate whitespace with ' '
if text:
self._raw_text = text # unaltered block content (before alteration)
text = re.sub(r'(^\s+|\s+$)', '', text) # remove whitespace padding
text = re.sub(r'\s+', ' ', text) # remove duplicate whitespace with ' '
self._text = text # cleaned up block content
self.text = text
# Get words from text, and group into gcodes
self.words = list(text2words(self._text))
(self.gcodes, self.modal_params) = words2gcodes(self.words)
self.words = list(iter_words(self.text))
(self.gcodes, self.modal_params) = words2gcodes(self.words)
# Verification
if verify:
self._assert_gcodes()
self._assert_gcodes()
# TODO: gcode verification
# - gcodes in the same modal_group raises exception
@property
def text(self):
if self._text:
return self._text
return str(self)
def _assert_gcodes(self):
modal_groups = set()
code_words = set()
for gc in self.gcodes:
# Assert all gcodes are not repeated in the same block
if gc.word in code_words:
raise AssertionError("%s cannot be in the same block" % ([

View File

@ -7,6 +7,12 @@ class CommentBase(object):
def __init__(self, text):
self.text = text
def __repr__(self):
return "<{class_name}: '{comment}'>".format(
class_name=self.__class__.__name__,
comment=str(self),
)
class CommentSemicolon(CommentBase):
"Comments of the format: 'G00 X1 Y2 ; something profound'"

View File

@ -14,12 +14,15 @@ class GCodeFile(object):
self.lines.append(line)
def parse(filename):
# FIXME: should be an iterator, and also not terrible
file = GCodeFile()
with open(filename, 'r') as fh:
for line in fh.readlines():
line_obj = Line(line)
# FIXME: don't dump entire file into RAM; change to generator model
file.append(line_obj)
return file
class GCodeParser(object):
"""Parse a gocde file"""
def __init__(self, filename):
self.filename = filename
self._fh = open(filename, 'r')
def iterlines(self):
for line in self._fh.readlines():
yield Line(line)
def close(self):
self._fh.close()

View File

@ -40,7 +40,7 @@ from .words import Word
#
# Table 5. G-Code Modal Groups
# MODAL GROUP MEANING MEMBER WORDS
# Non-modal codes (Group 0) G4, G10 G28, G30, G53 G92, G92.1, G92.2, G92.3,
# Non-modal codes (Group 0) G4, G10 G28, G30, G53, G92, G92.1, G92.2, G92.3,
# Motion (Group 1) G0, G1, G2, G3, G33, G38.x, G73, G76, G80, G81
# G82, G83, G84, G85, G86, G87, G88,G89
# Plane selection (Group 2) G17, G18, G19, G17.1, G18.1, G19.1
@ -132,6 +132,12 @@ MODAL_GROUP_MAP = {
# 240: Perform motion (G0 to G3, G33, G38.x, G73, G76, G80 to G89), as modified (possibly) by G53.
# 250: Stop (M0, M1, M2, M30, M60).
class GCodeEffect(object):
"""Effect a gcode has on a machine upon processing"""
def __init__(self, **kwargs):
self.mode = {}
self.dt = 0.0
class GCode(object):
# Defining Word
@ -149,6 +155,10 @@ class GCode(object):
exec_order = 999 # if not otherwise specified, run last
def __init__(self, word, *params):
"""
:param word: Word instance defining gcode (eg: Word('G0') for rapid movement)
:param params: list of Word instances (eg: Word('X-1.2') as x-coordinate)
"""
assert isinstance(word, Word), "invalid gcode word %r" % code_word
self.word = word
self.params = {}
@ -188,6 +198,10 @@ class GCode(object):
return self.exec_order < other.exec_order
def add_parameter(self, word):
"""
Add given word as a parameter for this gcode
:param word: Word instance
"""
assert isinstance(word, Word), "invalid parameter class: %r" % word
assert word.letter in self.param_letters, "invalid parameter for %s: %s" % (self.__class__.__name__, str(word))
assert word.letter not in self.params, "parameter defined twice: %s -> %s" % (self.params[word.letter], word)
@ -217,6 +231,44 @@ class GCode(object):
if l in self.modal_param_letters
])
def get_param_dict(self, letters=None):
"""
Get gcode parameters as a dict
gcode parameter like "X3.1, Y-2" would return {'X': 3.1, 'Y': -2}
:param letters: iterable whitelist of letters to include as dict keys
:return: dict of gcode parameters' (letter, value) pairs
"""
return dict(
(w.letter, w.value) for w in self.params.values()
if (letters is None) or (w.letter in letters)
)
# Process GCode
def process(self, machine):
"""
Process a GCode on the given Machine
:param machine: Machine instance, to change state
:return: GCodeEffect instance; effect the gcode just had on machine
"""
from .machine import Machine # importing high-level state
assert isinstance(machine, Machine), "invalid parameter"
effect = GCodeEffect()
# Set mode
self._process_mode(machine)
# GCode specific
self._process(machine)
def _process_mode(self, machine):
"""Set machine's state"""
machine.set_mode(self)
def _process(self, machine):
"""Process this GCode (to be overridden)"""
pass
# ======================= Motion =======================
# (X Y Z A B C U V W apply to all motions)
@ -238,10 +290,17 @@ class GCodeMotion(GCode):
modal_group = MODAL_GROUP_MAP['motion']
exec_order = 241
def _process(self, machine):
machine.move_to(**self.get_param_dict(letters=machine.axes))
class GCodeRapidMove(GCodeMotion):
"""G0: Rapid Move"""
word_key = Word('G', 0)
def _process(self, machine):
machine.move_to(rapid=True, **self.get_param_dict(letters=machine.axes))
class GCodeLinearMove(GCodeMotion):
"""G1: Linear Move"""
@ -334,6 +393,7 @@ class GCodeCannedCycle(GCode):
modal_group = MODAL_GROUP_MAP['motion']
exec_order = 241
class GCodeDrillingCycle(GCodeCannedCycle):
"""G81: Drilling Cycle"""
param_letters = GCodeCannedCycle.param_letters | set('RLP')
@ -600,11 +660,13 @@ class GCodeUnit(GCode):
class GCodeUseInches(GCodeUnit):
"""G20: use inches for length units"""
word_key = Word('G', 20)
unit_id = 0
class GCodeUseMillimeters(GCodeUnit):
"""G21: use millimeters for length units"""
word_key = Word('G', 21)
unit_id = 1
# ======================= Plane Selection =======================
@ -815,22 +877,64 @@ class GCodeFeedStop(GCodeOtherModal):
class GCodeSelectCoordinateSystem(GCodeOtherModal):
"""
G54 - select coordinate system 1
G55 - select coordinate system 2
G56 - select coordinate system 3
G57 - select coordinate system 4
G58 - select coordinate system 5
G59 - select coordinate system 6
G59.1 - select coordinate system 7
G59.2 - select coordinate system 8
G59.3 - select coordinate system 9
"""
@classmethod
def word_matches(cls, w):
return (w.letter == 'G') and (w.value in [54, 55, 56, 57, 58, 59, 59.1, 59.2, 59.3])
"""Select Coordinate System"""
modal_group = MODAL_GROUP_MAP['coordinate_system']
exec_order = 190
coord_system_id = None # overridden in inheriting classes
class GCodeSelectCoordinateSystem1(GCodeSelectCoordinateSystem):
"""Select Coordinate System 1"""
word_key = Word('G', 54)
coord_system_id = 1
class GCodeSelectCoordinateSystem2(GCodeSelectCoordinateSystem):
"""Select Coordinate System 2"""
word_key = Word('G', 55)
coord_system_id = 2
class GCodeSelectCoordinateSystem3(GCodeSelectCoordinateSystem):
"""Select Coordinate System 3"""
word_key = Word('G', 56)
coord_system_id = 3
class GCodeSelectCoordinateSystem4(GCodeSelectCoordinateSystem):
"""Select Coordinate System 4"""
word_key = Word('G', 57)
coord_system_id = 4
class GCodeSelectCoordinateSystem5(GCodeSelectCoordinateSystem):
"""Select Coordinate System 5"""
word_key = Word('G', 58)
coord_system_id = 5
class GCodeSelectCoordinateSystem6(GCodeSelectCoordinateSystem):
"""Select Coordinate System 6"""
word_key = Word('G', 59)
coord_system_id = 6
class GCodeSelectCoordinateSystem7(GCodeSelectCoordinateSystem):
"""Select Coordinate System 7"""
coord_system_id = 7
word_key = Word('G', 59.1)
class GCodeSelectCoordinateSystem8(GCodeSelectCoordinateSystem):
"""Select Coordinate System 8"""
word_key = Word('G', 59.2)
coord_system_id = 8
class GCodeSelectCoordinateSystem9(GCodeSelectCoordinateSystem):
"""Select Coordinate System 9"""
word_key = Word('G', 59.3)
coord_system_id = 9
# ======================= Flow-control Codes =======================
@ -981,6 +1085,8 @@ class GCodeResetCoordSystemOffset(GCodeNonModal):
return (w.letter == 'G') and (w.value in [92.1, 92.2])
exec_order = 230
# TODO: machine.state.offset *= 0
class GCodeRestoreCoordSystemOffset(GCodeNonModal):
"""G92.3: Restore Coordinate System Offset"""
@ -1009,7 +1115,7 @@ def _subclasses_level(root_class, recursion_level=0):
:param
"""
yield (root_class, recursion_level)
for cls in root_class.__subclasses__():
for cls in sorted(root_class.__subclasses__(), key=lambda c: c.__name__):
for (sub, level) in _subclasses_level(cls, recursion_level+1):
yield (sub, level)
@ -1028,7 +1134,11 @@ def _gcode_class_infostr(base_class=GCode):
"""
info_str = ""
for (cls, level) in _subclasses_level(base_class):
info_str += "{indent}- {name}: {description}\n".format(
word_str = ''
if cls.word_key:
word_str = cls.word_key.clean_str
info_str += "{word} {indent}- {name}: {description}\n".format(
word="%-5s" % word_str,
indent=(level * " "),
name=cls.__name__,
description=cls.__doc__ or "",

View File

@ -1,12 +1,211 @@
from copy import copy, deepcopy
from collections import defaultdict
from .gcodes import MODAL_GROUP_MAP, GCode
from .gcodes import (
MODAL_GROUP_MAP, GCode,
# Modal GCodes
GCodeIncrementalDistanceMode,
GCodeUseInches, GCodeUseMillimeters,
)
from .block import Block
from .line import Line
from .words import Word
UNIT_IMPERIAL = GCodeUseInches.unit_id # G20
UNIT_METRIC = GCodeUseMillimeters.unit_id # G21
UNIT_MAP = {
UNIT_IMPERIAL: {
'name': 'inches',
'conversion_factor': { UNIT_METRIC: 25.4 },
},
UNIT_METRIC: {
'name': 'millimeters',
'conversion_factor': { UNIT_IMPERIAL: 1. / 25.4 },
},
}
class Position(object):
default_axes = 'XYZABCUVW'
default_unit = UNIT_METRIC
POSSIBLE_AXES = set('XYZABCUVW')
def __init__(self, axes=None, **kwargs):
# Set axes (note: usage in __getattr__ and __setattr__)
if axes is None:
axes = self.__class__.default_axes
else:
invalid_axes = set(axes) - self.POSSIBLE_AXES
assert not invalid_axes, "invalid axes proposed %s" % invalid_axes
self.__dict__['axes'] = set(axes) & self.POSSIBLE_AXES
# Unit
self._unit = kwargs.pop('unit', self.default_unit)
# Initial Values
self._value = defaultdict(lambda: 0.0, dict((k, 0.0) for k in self.axes))
self._value.update(kwargs)
# Attributes Get/Set
def __getattr__(self, key):
if key in self.axes:
return self._value[key]
raise AttributeError("'{cls}' object has no attribute '{key}'".format(
cls=self.__class__.__name__,
key=key
))
def __setattr__(self, key, value):
if key in self.axes:
self._value[key] = value
elif key in self.POSSIBLE_AXES:
raise AssertionError("'%s' axis is not defined to be set" % key)
else:
self.__dict__[key] = value
# Copy
def __copy__(self):
return self.__class__(axes=copy(self.axes), unit=self._unit, **self.values)
# Equality
def __eq__(self, other):
if self.axes ^ other.axes:
return False
else:
if self._unit == other._unit:
return self._value == other._value
else:
x = copy(other)
x.set_unit(self._unit)
return self._value == x._value
def __ne__(self, other):
return not self.__eq__(other)
# Arithmetic
def __add__(self, other):
assert not (self.axes ^ other.axes), "axes: %r != %r" % (self.axes, other.axes)
new_obj = copy(self)
for k in new_obj._value:
new_obj._value[k] += other._value[k]
return new_obj
def __sub__(self, other):
assert not (other.axes - self.axes), "for a - b: axes in b, that are not in a: %r" % (other.axes - self.axes)
new_obj = copy(self)
for k in other._value:
new_obj._value[k] -= other._value[k]
return new_obj
def __mul__(self, scalar):
new_obj = copy(self)
for k in self._value:
new_obj._value[k] = self._value[k] * scalar
return new_obj
def __div__(self, scalar):
new_obj = copy(self)
for k in self._value:
new_obj._value[k] = self._value[k] / scalar
return new_obj
__truediv__ = __div__ # Python 3 division
# Conversion
def set_unit(self, unit):
if unit == self._unit:
return
factor = UNIT_MAP[self._unit]['conversion_factor'][unit]
for k in [k for (k, v) in self._value.items() if v is not None]:
self._value[k] *= factor
self._unit = unit
@property
def words(self):
return sorted(Word(k, self._value[k]) for k in self.axes)
@property
def values(self):
return dict(self._value)
def __repr__(self):
return "<{class_name}: {coordinates}>".format(
class_name=self.__class__.__name__,
coordinates=' '.join(str(w) for w in self.words)
)
class CoordinateSystem(object):
def __init__(self, axes):
self.offset = Position(axes)
def __add__(self, other):
if isinstance(other, CoordinateSystem):
pass
def __repr__(self):
return "<{class_name}: offset={offset}>".format(
class_name=self.__class__.__name__,
offset=repr(self.offset),
)
class State(object):
"""State of a Machine"""
# LinuxCNC documentation lists parameters for a machine's state:
# http://linuxcnc.org/docs/html/gcode/overview.html#sub:numbered-parameters
# AFAIK: this is everything needed to remember a machine's state that isn't
# handled by modal gcodes.
def __init__(self, axes=None):
# Coordinate Systems
self.coord_systems = {}
for i in range(1, 10): # G54-G59.3
self.coord_systems[i] = CoordinateSystem(axes)
self.cur_coord_sys = 1 # default to coord system 1 (G54)
# Temporary Offset
self.offset = Position(axes) # G92 offset (reset by G92.x)
# Missing from state (according to LinuxCNC's state variables):
# - G38.2 probe result (Position())
# - G38 probe result (bool)
# - M66: result (bool)
# - Tool offsets (Position())
# - Tool info (number, diameter, front angle, back angle, orientation)
#self.work_offset = defaultdict(lambda: 0.0)
# TODO: how to manage work offsets? (probs not like the above)
# read up on:
# Coordinate System config:
# - G92: set machine coordinate system value (no movement, effects all coordinate systems)
# - G10 L2: offsets the origin of the axes in the coordinate system specified to the value of the axis word
# - G10 L20: makes the current machine coordinates the coordinate system's offset
# Coordinate System selection:
# - G54-G59: select coordinate system (offsets from machine coordinates set by G10 L2)
# TODO: Move this class into MachineState
@property
def coord_sys(self):
"""Current equivalent coordinate system, including all """
if self.cur_coord_sys in self.coord_systems:
return self.coord_systems[self.cur_coord_sys]
return None
def __repr__(self):
return "<{class_name}: coord_sys[{coord_index}]; offset={offset}>".format(
class_name=self.__class__.__name__,
coord_index=self.cur_coord_sys,
offset=repr(self.offset),
)
class Mode(object):
"""Machine's mode"""
# 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.
@ -15,36 +214,29 @@ class State(object):
# 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"""
# Default Mode
# for a Grbl controller this can be obtained with the `$G` command, eg:
# > $G
# > [GC:G0 G54 G17 G21 G90 G94 M5 M9 T0 F0 S0]
# ref: https://github.com/gnea/grbl/wiki/Grbl-v1.1-Commands#g---view-gcode-parser-state
default_mode = '''
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)
G0 (movement: rapid)
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)
G54 (coordinate_system: 1)
G61 (control_mode: exact path mode)
G97 (spindle_speed_mode: RPM Mode)
M5 (spindle: off)
M9 (coolant: off)
F0 (feed_rate: 0)
S0 (spindle_speed: 0)
T0 (tool: 0)
'''
# Mode is defined by gcodes set by processed blocks:
@ -56,9 +248,18 @@ class MachineState(object):
self.set_mode(*Line(self.default_mode).block.gcodes)
def set_mode(self, *gcode_list):
"""
Set machine mode from given gcodes
:param gcode_list: list of GCode instances (given as individual parameters)
:return: dict of form: {<modal group>: <new mode GCode>, ...}
"""
modal_gcodes = {}
for g in sorted(gcode_list): # sorted by execution order
if g.modal_group is not None:
self.modal_groups[g.modal_group] = g.modal_copy()
modal_gcodes[g.modal_group] = self.modal_groups[g.modal_group]
# assumption: no 2 gcodes are in the same modal_group
return modal_gcodes
def __getattr__(self, key):
if key in MODAL_GROUP_MAP:
@ -99,8 +300,34 @@ class MachineState(object):
class Machine(object):
"""Machine to process gcodes, enforce axis limits, keep track of time, etc"""
# Class types
MODE_CLASS = Mode
STATE_CLASS = State
axes = set('XYZ')
def __init__(self):
self.state = MachineState()
self.mode = self.MODE_CLASS()
self.state = self.STATE_CLASS(axes=self.axes)
# Position type (with default axes the same as this machine)
self.Position = type('Position', (Position,), {
'default_axes': self.axes,
'default_unit': self.mode.units.unit_id,
})
# Absolute machine position
self.abs_pos = self.Position()
def set_mode(self, *gcode_list):
self.mode.set_mode(*gcode_list) # passthrough
# Act on mode changes
coord_sys_mode = self.mode.coordinate_system
if coord_sys_mode:
self.state.cur_coord_sys = coord_sys_mode.coord_system_id
def process(self, *gcode_list, **kwargs):
"""
@ -108,16 +335,52 @@ class Machine(object):
:param gcode_list: list of GCode instances
:param modal_params: list of Word instances to be applied to current movement mode
"""
# Add modal gcode to list of given gcodes
modal_params = kwargs.get('modal_params', [])
#process_gcodes =
if modal_params:
assert self.mode.motion is not None, "unable to assign modal parameters when no motion mode is set"
(modal_gcodes, unasigned_words) = words2gcodes([self.mode.motion.word] + modal_params)
assert unasigned_words == [], "modal parameters '%s' unknown when in mode: %r" % (
' '.join(str(x) for x in unasigned_words), self.mode
)
gcode_list += modal_gcodes
for gcode in sorted(gcode_list):
self.state.set_mode(gcode) # if gcode is not modal, it's ignored
gcode.process(self) # shifts ownership of what happens now to GCode class
gcode.process(self.state)
# TODO: gcode instance to change machine's state
# Questions to drive design:
# - how much time did the command take?
# - what was the tool's distance / displacement
# - did the tool travel outside machine boundaries?
# Use-cases
# - Transform / rotate coordinate system in given gcode
# - Convert arcs to linear segments (visa versa?)
# - Correct precision errors
# - Crop a file (eg: resume half way through)
def process_block(self, block):
self.process(*block.gcodes, modal_params=block.modal_params)
@property
def pos(self):
"""Return current position in current coordinate system"""
coord_sys_offset = getattr(self.state.coord_sys, 'offset', Position(axes=self.axes))
temp_offset = self.state.offset
return (self.abs_pos - coord_sys_offset) - temp_offset
@pos.setter
def pos(self, value):
"""Set absolute position given current position and coordinate system"""
coord_sys_offset = getattr(self.state.coord_sys, 'offset', Position(axes=self.axes))
temp_offset = self.state.offset
self.abs_pos = (value + temp_offset) + coord_sys_offset
# =================== Machine Actions ===================
def move_to(self, rapid=False, **coords):
"""Move machine to given position"""
given_position = Position(axes=self.axes, **coords)
if isinstance(self.mode.distance, GCodeIncrementalDistanceMode):
self.pos += given_position
else: # assumed: GCodeAbsoluteDistanceMode
self.pos = given_position

View File

@ -281,7 +281,7 @@ class Word(object):
return "%s: %s" % (self.letter, WORD_MAP[self.letter]['description'])
def iter_words(block_text):
def text2words(block_text):
"""
Iterate through block text yielding Word instances
:param block_text: text for given block with comments removed
@ -315,8 +315,21 @@ def iter_words(block_text):
def str2word(word_str):
words = list(iter_words(word_str))
words = list(text2words(word_str))
if words:
assert len(words) <= 1, "more than one word given"
return words[0]
return None
def words2dict(word_list, limit_word_letters=None):
"""
Represent a list of words as a dict
:param limit_word_letters: iterable containing a white-list of word letters (None allows all)
:return: dict of the form: {<letter>: <value>, ... }
"""
# Remember: duplicate word letters cannot be represented as a dict
return dict(
(w.letter, w.value) for w in word_list
if (limit_word_letters is None) or (w.letter in limit_word_letters)
)

View File

@ -4,17 +4,21 @@ import inspect
import unittest
# Units Under Test
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, os.path.join(_this_path, '..'))
from pygcode.file import parse, GCodeFile
# Add relative pygcode to path
from testutils import add_pygcode_to_path, str_lines
add_pygcode_to_path()
class FileParseTest(unittest.TestCase):
# Units under test
from pygcode.file import GCodeFile, GCodeParser
class GCodeParserTest(unittest.TestCase):
FILENAME = 'test-files/vertical-slot.ngc'
def test_parser(self):
file = parse(self.FILENAME)
self.assertEqual(len(file.lines), 26)
# FIXME: just verifying content visually
#for line in file.lines:
# print(line)
parser = GCodeParser(self.FILENAME)
# count lines
line_count = 0
for line in parser.iterlines():
line_count += 1
self.assertEqual(line_count, 26)
parser.close()

View File

@ -4,9 +4,11 @@ import inspect
import re
import unittest
# Units Under Test
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, os.path.join(_this_path, '..'))
# Add relative pygcode to path
from testutils import add_pygcode_to_path, str_lines
add_pygcode_to_path()
# Units under test
from pygcode import gcodes
from pygcode import words
class TestGCodeWordMapping(unittest.TestCase):
@ -60,7 +62,7 @@ class TestGCodeModalGroups(unittest.TestCase):
match = re.search(r'^\s*(?P<title>.*)\s*\(Group (?P<group>\d+)\)\s*(?P<words>.*)$', row, re.I)
if match:
for word_str in re.split(r'\s*,\s*', match.group('words')):
word = list(words.iter_words(word_str))[0]
word = list(words.text2words(word_str))[0]
gcode_class = gcodes.word_gcode_class(word)
# GCode class found for each word in the table
self.assertIsNotNone(gcode_class)
@ -81,7 +83,7 @@ class TestGCodeModalGroups(unittest.TestCase):
class TestWordsToGCodes(unittest.TestCase):
def test_stuff(self): # FIXME: function name
line = 'G1 X82.6892 Y-38.6339 F1500'
word_list = list(words.iter_words(line))
word_list = list(words.text2words(line))
result = gcodes.words2gcodes(word_list)
# result form
self.assertIsInstance(result, tuple)

View File

@ -4,9 +4,11 @@ import inspect
import unittest
# Units Under Test
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, os.path.join(_this_path, '..'))
# Add relative pygcode to path
from testutils import add_pygcode_to_path, str_lines
add_pygcode_to_path()
# Units under test
from pygcode.line import Line

122
tests/test_machine.py Normal file
View File

@ -0,0 +1,122 @@
import unittest
# Add relative pygcode to path
from testutils import add_pygcode_to_path, str_lines
add_pygcode_to_path()
# Units under test
from pygcode.machine import Position, Machine
from pygcode.line import Line
class PositionTests(unittest.TestCase):
def test_basics(self):
p = Position()
#
def test_default_axes(self):
p = Position() # no instantiation parameters
# all initialized to zero
for axis in 'XYZABCUVW':
self.assertEqual(getattr(p, axis), 0)
for axis in 'XYZABCUVW':
# set to 100
setattr(p, axis, 100)
self.assertEqual(getattr(p, axis), 100)
for inner_axis in set('XYZABCUVW') - {axis}: # no other axis has changed
self.assertEqual(getattr(p, inner_axis), 0), "axis '%s'" % inner_axis
# revert back to zero
setattr(p, axis, 0)
self.assertEqual(getattr(p, axis), 0)
# Equality
def test_equality(self):
p1 = Position(axes='XYZ', X=1, Y=2)
p2 = Position(axes='XYZ', X=1, Y=2, Z=0)
p3 = Position(axes='XYZ', X=1, Y=2, Z=1000)
p4 = Position(axes='XYZA', X=1, Y=2, Z=0)
# p1 <--> p2
self.assertTrue(p1 == p2)
self.assertFalse(p1 != p2) # negative case
# p2 <--> p3
self.assertTrue(p2 != p3)
self.assertFalse(p2 == p3) # negative case
# p2 <--> p4
self.assertTrue(p2 != p4)
self.assertFalse(p2 == p4) # negative case
# Arithmetic
def test_arithmetic_add(self):
p1 = Position(axes='XYZ', X=1, Y=2)
p2 = Position(axes='XYZ', Y=10, Z=-20)
self.assertEqual(p1 + p2, Position(axes='XYZ', X=1, Y=12, Z=-20))
p3 = Position(axes='XYZA')
with self.assertRaises(AssertionError):
p1 + p3 # mismatched axes
with self.assertRaises(AssertionError):
p3 + p1 # mismatched axes
def test_arithmetic_sub(self):
p1 = Position(axes='XYZ', X=1, Y=2)
p2 = Position(axes='XYZ', Y=10, Z=-20)
self.assertEqual(p1 - p2, Position(axes='XYZ', X=1, Y=-8, Z=20))
p3 = Position(axes='XYZA')
p3 - p1 # fine
with self.assertRaises(AssertionError):
p1 - p3 # mismatched axes
def test_arithmetic_multiply(self):
p = Position(axes='XYZ', X=2, Y=10)
self.assertEqual(p * 2, Position(axes='XYZ', X=4, Y=20))
def test_arithmetic_divide(self):
p = Position(axes='XYZ', X=2, Y=10)
self.assertEqual(p / 2, Position(axes='XYZ', X=1, Y=5))
class MachineGCodeProcessingTests(unittest.TestCase):
def test_linear_movement(self):
m = Machine()
test_str = '''; move in a 10mm square
F100 M3 S1000 ; 0
g1 x0 y10 ; 1
g1 x10 y10 ; 2
g1 x10 y0 ; 3
g1 x0 y0 ; 4
'''
expected_pos = {
'0': m.Position(),
'1': m.Position(X=0, Y=10),
'2': m.Position(X=10, Y=10),
'3': m.Position(X=10, Y=0),
'4': m.Position(X=0, Y=0),
}
#print("\n%r\n%r" % (m.mode, m.state))
for line_text in str_lines(test_str):
line = Line(line_text)
if line.block:
#print("\n%s" % line.block)
m.process_block(line.block)
# Assert possition change correct
comment = line.comment.text
if comment in expected_pos:
self.assertEqual(m.pos, expected_pos[comment])
#print("%r\n%r\npos=%r" % (m.mode, m.state, m.pos))
#m = Machine()
#
#file = GCodeParser('part1.gcode')
#for line in file.iterlines():
# for (i, gcode) in enumerate(line.block.gcode):
# if isinstance(gcode, GCodeArcMove):
# arc = gcode
# line_params = arc.line_segments(precision=0.0005)
# for

View File

@ -4,23 +4,18 @@ import inspect
import unittest
# Units Under Test
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, os.path.join(_this_path, '..'))
# Add relative pygcode to path
from testutils import add_pygcode_to_path, str_lines
add_pygcode_to_path()
# Units under test
from pygcode import words
#words.iter_words
class WordTests(unittest.TestCase):
def test_O(self):
pass # TODO
class WordIterTests(unittest.TestCase):
def test_iter1(self):
block_str = 'G01 Z-0.5 F100'
w = list(words.iter_words(block_str))
w = list(words.text2words(block_str))
# word length
self.assertEqual(len(w), 3)
# word values
@ -30,7 +25,7 @@ class WordIterTests(unittest.TestCase):
def test_iter2(self):
block_str = 'G02 X10.75 Y47.44 I-0.11 J-1.26 F70'
w = list(words.iter_words(block_str))
w = list(words.text2words(block_str))
# word length
self.assertEqual(len(w), 6)
# word values

25
tests/testutils.py Normal file
View File

@ -0,0 +1,25 @@
import sys
import os
import inspect
import re
# Units Under Test
_pygcode_in_path = False
def add_pygcode_to_path():
global _pygcode_in_path
if not _pygcode_in_path:
# Add pygcode (relative to this test-path) to the system path
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, os.path.join(_this_path, '..'))
_pygcode_in_path = True
add_pygcode_to_path()
# String Utilities
def str_lines(text):
"""Split given string into lines (ignore blank lines, and automagically strip)"""
for match in re.finditer(r'\s*(?P<content>.*?)\s*\n', text):
yield match.group('content')