mirror of
https://git.mirrors.martin98.com/https://github.com/petaflot/pygcode
synced 2025-08-14 05:15:56 +08:00
initial content (24hrs later)
This commit is contained in:
parent
8220baa837
commit
2d125028b1
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# python cache / compilations
|
||||||
|
*__pycache__
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# editor backups
|
||||||
|
*.swp
|
27
README.md
27
README.md
@ -16,16 +16,19 @@ Just brainstorming here...
|
|||||||
import math
|
import math
|
||||||
import euclid
|
import euclid
|
||||||
|
|
||||||
gfile_in = pygcode.Parser('part.gcode')
|
gfile_in = pygcode.parse('part1.gcode') #
|
||||||
gfile_out = pygcode.Writer('part2.gcode')
|
gfile_out = pygcode.GCodeFile('part2.gcode')
|
||||||
|
|
||||||
total_travel = 0
|
total_travel = 0
|
||||||
total_time = 0
|
total_time = 0
|
||||||
|
|
||||||
for (state, block) in gfile_in.iterstate():
|
machine = pygcode.Machine()
|
||||||
# where:
|
|
||||||
# state = CNC's state before the block is executed
|
for line in gfile_in.iterlines():
|
||||||
# block = the gcode to be executed next
|
|
||||||
|
block = line.block
|
||||||
|
if block is None:
|
||||||
|
continue
|
||||||
|
|
||||||
# validation
|
# validation
|
||||||
if isinstance(block, pygcode.GCodeArc):
|
if isinstance(block, pygcode.GCodeArc):
|
||||||
@ -36,13 +39,13 @@ Just brainstorming here...
|
|||||||
block.set_precision(0.0005, method=pygcode.GCodeArc.EFFECT_RADIUS)
|
block.set_precision(0.0005, method=pygcode.GCodeArc.EFFECT_RADIUS)
|
||||||
|
|
||||||
# random metrics
|
# random metrics
|
||||||
travel_vector = block.position - state.position # euclid.Vector3 instance
|
travel_vector = block.position - machine.state.position # euclid.Vector3 instance
|
||||||
distance = travel_vector.magnitude()
|
distance = travel_vector.magnitude()
|
||||||
travel = block.travel_distance(position=state.position) # eg: distance != travel for G02 & G03
|
travel = block.travel_distance(position=machine.state.position) # eg: distance != travel for G02 & G03
|
||||||
|
|
||||||
total_travel += travel
|
total_travel += travel
|
||||||
#total_time += block.time(feed_rate=state.feed_rate) # doesn't consider the feedrate being changed in this block
|
#total_time += block.time(feed_rate=machine.state.feed_rate) # doesn't consider the feedrate being changed in this block
|
||||||
total_time += block.time(state=state)
|
total_time += block.time(state=machine.state)
|
||||||
|
|
||||||
# rotate : entire file 90deg CCW
|
# rotate : entire file 90deg CCW
|
||||||
block.rotate(euclid.Quaternion.new_rotate_axis(
|
block.rotate(euclid.Quaternion.new_rotate_axis(
|
||||||
@ -51,6 +54,8 @@ Just brainstorming here...
|
|||||||
# translate : entire file x += 1, y += 2 mm (after rotation)
|
# translate : entire file x += 1, y += 2 mm (after rotation)
|
||||||
block.translate(euclid.Vector3(1, 2, 0), unit=pygcode.UNIT_MM)
|
block.translate(euclid.Vector3(1, 2, 0), unit=pygcode.UNIT_MM)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: then do something like write it to another file
|
# TODO: then do something like write it to another file
|
||||||
gfile_out.write(block)
|
gfile_out.write(block)
|
||||||
|
|
||||||
@ -59,7 +64,7 @@ Just brainstorming here...
|
|||||||
|
|
||||||
|
|
||||||
## Supported G-Codes
|
## Supported G-Codes
|
||||||
GCode support is planned to follow that of [GRBL](https://github.com/gnea/grbl).
|
GCode support is planned to follow that of [GRBL](https://github.com/gnea/grbl) which follows [LinuxCNC](http://linuxcnc.org) (list of gcodes documented [here](http://linuxcnc.org/docs/html/gcode.html)).
|
||||||
|
|
||||||
But anything pre v1.0 will be a sub-set, focusing on the issues I'm having... I'm selfish that way.
|
But anything pre v1.0 will be a sub-set, focusing on the issues I'm having... I'm selfish that way.
|
||||||
|
|
||||||
|
0
pygcode/__init__.py
Normal file
0
pygcode/__init__.py
Normal file
36
pygcode/block.py
Normal file
36
pygcode/block.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import re
|
||||||
|
from .words import iter_words, WORD_MAP
|
||||||
|
|
||||||
|
class Block(object):
|
||||||
|
"""GCode block (effectively any gcode file line that defines any <word><value>)"""
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
"""
|
||||||
|
Block Constructor
|
||||||
|
:param A-Z: gcode parameter values
|
||||||
|
:param comment: comment text
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._raw_text = text # unaltered block content (before alteration)
|
||||||
|
|
||||||
|
# clean up block string
|
||||||
|
text = re.sub(r'(^\s+|\s+$)', '', text) # remove whitespace padding
|
||||||
|
text = re.sub(r'\s+', ' ', text) # remove duplicate whitespace with ' '
|
||||||
|
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
self.words = list(iter_words(self.text))
|
||||||
|
|
||||||
|
def __getattr__(self, k):
|
||||||
|
if k in WORD_MAP:
|
||||||
|
for w in self.words:
|
||||||
|
if w.letter == k:
|
||||||
|
return w
|
||||||
|
# if word is not in this block:
|
||||||
|
return None
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise AttributeError("'{cls}' object has no attribute '{key}'".format(
|
||||||
|
cls=self.__class__.__name__,
|
||||||
|
key=k
|
||||||
|
))
|
61
pygcode/comment.py
Normal file
61
pygcode/comment.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class CommentBase(object):
|
||||||
|
ORDER = 0
|
||||||
|
MULTICOMMENT_JOINER = ". " # joiner if multiple comments are found on the same line
|
||||||
|
def __init__(self, text):
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
|
||||||
|
class CommentSemicolon(CommentBase):
|
||||||
|
"Comments of the format: 'G00 X1 Y2 ; something profound'"
|
||||||
|
ORDER = 1
|
||||||
|
AUTO_REGEX = re.compile(r'\s*;\s*(?P<text>.*)$')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "; {text}".format(text=self.text)
|
||||||
|
|
||||||
|
|
||||||
|
class CommentBrackets(CommentBase):
|
||||||
|
"Comments of the format: 'G00 X1 Y2 (something profound)"
|
||||||
|
ORDER = 2
|
||||||
|
AUTO_REGEX = re.compile(r'\((?P<text>[^\)]*)\)')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "({text})".format(text=self.text)
|
||||||
|
|
||||||
|
|
||||||
|
Comment = CommentBrackets # default comment type
|
||||||
|
|
||||||
|
|
||||||
|
def split_line(line_text):
|
||||||
|
"""
|
||||||
|
Split functional block content from comments
|
||||||
|
:param line_text: line from gcode file
|
||||||
|
:return: tuple of (str(<functional block code>), CommentBase(<comment(s)>))
|
||||||
|
"""
|
||||||
|
comments_class = None
|
||||||
|
|
||||||
|
# Auto-detect comment type if I can
|
||||||
|
comments = []
|
||||||
|
block_str = line_text.rstrip("\n") # to remove potential return carriage from comment body
|
||||||
|
|
||||||
|
for cls in sorted(CommentBase.__subclasses__(), key=lambda c: c.ORDER):
|
||||||
|
matches = list(cls.AUTO_REGEX.finditer(block_str))
|
||||||
|
if matches:
|
||||||
|
for match in reversed(matches):
|
||||||
|
# Build list of comment text
|
||||||
|
comments.insert(0, match.group('text')) # prepend
|
||||||
|
# Remove comments from given block_str
|
||||||
|
block_str = block_str[:match.start()] + block_str[match.end():]
|
||||||
|
comments_class = cls
|
||||||
|
break
|
||||||
|
|
||||||
|
# Create comment instance if content was found
|
||||||
|
comment_obj = None
|
||||||
|
if comments_class:
|
||||||
|
comment_text = comments_class.MULTICOMMENT_JOINER.join(comments)
|
||||||
|
comment_obj = comments_class(comment_text)
|
||||||
|
|
||||||
|
return (block_str, comment_obj)
|
7
pygcode/exceptions.py
Normal file
7
pygcode/exceptions.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
# ===================== Parsing Exceptions =====================
|
||||||
|
class GCodeBlockFormatError(Exception):
|
||||||
|
"""Raised when errors encountered while parsing block text"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ===================== Parsing Exceptions =====================
|
23
pygcode/file.py
Normal file
23
pygcode/file.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from .line import Line
|
||||||
|
|
||||||
|
|
||||||
|
class GCodeFile(object):
|
||||||
|
def __init__(self, filename=None):
|
||||||
|
self.filename = filename
|
||||||
|
|
||||||
|
# Initialize
|
||||||
|
self.lines = []
|
||||||
|
|
||||||
|
def append(self, line):
|
||||||
|
assert isinstance(line, Line), "invalid line type"
|
||||||
|
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)
|
||||||
|
file.append(line_obj)
|
||||||
|
return file
|
28
pygcode/line.py
Normal file
28
pygcode/line.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from .comment import split_line
|
||||||
|
from .block import Block
|
||||||
|
|
||||||
|
class Line(object):
|
||||||
|
def __init__(self, text=None):
|
||||||
|
self._text = text
|
||||||
|
|
||||||
|
# Initialize
|
||||||
|
self.block = None
|
||||||
|
self.comment = None
|
||||||
|
|
||||||
|
# Split line into block text, and comments
|
||||||
|
if text is not None:
|
||||||
|
(block_str, comment) = split_line(text)
|
||||||
|
if block_str:
|
||||||
|
self.block = Block(block_str)
|
||||||
|
if comment:
|
||||||
|
self.comment = comment
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
if self._text is None:
|
||||||
|
return self.build_line_text()
|
||||||
|
return self._text
|
||||||
|
|
||||||
|
|
||||||
|
def build_line_text(self):
|
||||||
|
return ' '.join([str(x) for x in [self.block, self.comment] if x]) + '\n'
|
36
pygcode/machine.py
Normal file
36
pygcode/machine.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
|
||||||
|
class MachineState(object):
|
||||||
|
def __init__(self, axes=('x', 'y', 'z')):
|
||||||
|
self.axes = axes
|
||||||
|
|
||||||
|
# initialize
|
||||||
|
self.position = {}
|
||||||
|
for axis in self.axes:
|
||||||
|
self.position[axis] = 0
|
||||||
|
|
||||||
|
self.time = 0
|
||||||
|
|
||||||
|
class Machine(object):
|
||||||
|
""""""
|
||||||
|
def __init__(self, **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)
|
||||||
|
|
||||||
|
def process_line(self, line):
|
||||||
|
"""Change machine's state based on the given gcode line"""
|
||||||
|
pass # TODO
|
321
pygcode/words.py
Normal file
321
pygcode/words.py
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
import re
|
||||||
|
import itertools
|
||||||
|
import six
|
||||||
|
|
||||||
|
from .exceptions import GCodeBlockFormatError
|
||||||
|
|
||||||
|
FLOAT_REGEX = re.compile(r'^-?\d+(\.\d+)?')
|
||||||
|
INT_REGEX = re.compile(r'^-?\d+')
|
||||||
|
POSITIVEINT_REGEX = re.compile(r'^\d+')
|
||||||
|
|
||||||
|
WORD_MAP = {
|
||||||
|
# Descriptions copied from wikipedia:
|
||||||
|
# https://en.wikipedia.org/wiki/G-code#Letter_addresses
|
||||||
|
|
||||||
|
# Rotational Axes
|
||||||
|
'A': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Absolute or incremental position of A axis (rotational axis around X axis)",
|
||||||
|
},
|
||||||
|
'B': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Absolute or incremental position of B axis (rotational axis around Y axis)",
|
||||||
|
},
|
||||||
|
'C': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Absolute or incremental position of C axis (rotational axis around Z axis)",
|
||||||
|
},
|
||||||
|
'D': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Defines diameter or radial offset used for cutter compensation. D is used for depth of cut on lathes. It is used for aperture selection and commands on photoplotters.",
|
||||||
|
},
|
||||||
|
# Feed Rates
|
||||||
|
'E': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Precision feedrate for threading on lathes",
|
||||||
|
},
|
||||||
|
'F': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Feedrate",
|
||||||
|
},
|
||||||
|
# G-Codes
|
||||||
|
'G': {
|
||||||
|
'class': str,
|
||||||
|
'value_regex': re.compile(r'^\d+(\.\d+)?'),
|
||||||
|
'description': "Address for preparatory commands",
|
||||||
|
},
|
||||||
|
# Tool Offsets
|
||||||
|
'H': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Defines tool length offset; Incremental axis corresponding to C axis (e.g., on a turn-mill)",
|
||||||
|
},
|
||||||
|
# Arc radius center coords
|
||||||
|
'I': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Defines arc center in X axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.",
|
||||||
|
},
|
||||||
|
'J': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Defines arc center in Y axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.",
|
||||||
|
},
|
||||||
|
'K': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Defines arc center in Z axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles, equal to L address.",
|
||||||
|
},
|
||||||
|
# Loop Count
|
||||||
|
'L': {
|
||||||
|
'class': int,
|
||||||
|
'value_regex': POSITIVEINT_REGEX,
|
||||||
|
'description': "Fixed cycle loop count; Specification of what register to edit using G10",
|
||||||
|
},
|
||||||
|
# Miscellaneous Function
|
||||||
|
'M': {
|
||||||
|
'class': str,
|
||||||
|
'value_regex': re.compile(r'^\d+(\.\d+)?'),
|
||||||
|
'description': "Miscellaneous function",
|
||||||
|
},
|
||||||
|
# Line Number
|
||||||
|
'N': {
|
||||||
|
'class': int,
|
||||||
|
'value_regex': POSITIVEINT_REGEX,
|
||||||
|
'description': "Line (block) number in program; System parameter number to change using G10",
|
||||||
|
},
|
||||||
|
# Program Name
|
||||||
|
'O': {
|
||||||
|
'class': str,
|
||||||
|
'value_regex': re.compile(r'^.+$'), # all the way to the end
|
||||||
|
'description': "Program name",
|
||||||
|
},
|
||||||
|
# Parameter (arbitrary parameter)
|
||||||
|
'P': {
|
||||||
|
'class': float, # parameter is often an integer, but can be a float
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Serves as parameter address for various G and M codes",
|
||||||
|
},
|
||||||
|
# Peck increment
|
||||||
|
'Q': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Depth to increase on each peck; Peck increment in canned cycles",
|
||||||
|
},
|
||||||
|
# Arc Radius
|
||||||
|
'R': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Defines size of arc radius, or defines retract height in milling canned cycles",
|
||||||
|
},
|
||||||
|
# Spindle speed
|
||||||
|
'S': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Defines speed, either spindle speed or surface speed depending on mode",
|
||||||
|
},
|
||||||
|
# Tool Selecton
|
||||||
|
'T': {
|
||||||
|
'class': str,
|
||||||
|
'value_regex': POSITIVEINT_REGEX, # tool string may have leading '0's, but is effectively an index (integer)
|
||||||
|
'description': "Tool selection",
|
||||||
|
},
|
||||||
|
# Incremental axes
|
||||||
|
'U': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Incremental axis corresponding to X axis (typically only lathe group A controls) Also defines dwell time on some machines (instead of 'P' or 'X').",
|
||||||
|
},
|
||||||
|
'V': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Incremental axis corresponding to Y axis",
|
||||||
|
},
|
||||||
|
'W': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Incremental axis corresponding to Z axis (typically only lathe group A controls)",
|
||||||
|
},
|
||||||
|
# Linear Axes
|
||||||
|
'X': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Absolute or incremental position of X axis.",
|
||||||
|
},
|
||||||
|
'Y': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Absolute or incremental position of Y axis.",
|
||||||
|
},
|
||||||
|
'Z': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': FLOAT_REGEX,
|
||||||
|
'description': "Absolute or incremental position of Z axis.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ORDER_LINUXCNC_LETTER_MAP = {
|
||||||
|
'O': 10,
|
||||||
|
'F': 40,
|
||||||
|
'S': 50,
|
||||||
|
'T': 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
_v_csv = lambda v, ks: [(k, v) for k in ks.split(',')]
|
||||||
|
|
||||||
|
ORDER_LINUXCNC_LETTERVALUE_MAP = dict(itertools.chain.from_iterable([
|
||||||
|
_v_csv(30, 'G93,G94'),
|
||||||
|
_v_csv(70, 'M62,M63,M64,M65,M66,M67,M68'),
|
||||||
|
_v_csv(80, 'M6,M61'),
|
||||||
|
_v_csv(90, 'M3,M4,M5'),
|
||||||
|
_v_csv(100, 'M71,M73,M72,M71'),
|
||||||
|
_v_csv(110, 'M7,M8,M9'),
|
||||||
|
_v_csv(120, 'M48,M49,M50,M51,M52,M53'),
|
||||||
|
[('G4', 140)],
|
||||||
|
_v_csv(150, 'G17,G18,G19'),
|
||||||
|
_v_csv(160, 'G20,G21'),
|
||||||
|
_v_csv(170, 'G40,G41,G42'),
|
||||||
|
_v_csv(180, 'G43,G49'),
|
||||||
|
_v_csv(190, 'G54,G55,G56,G57,G58,G59,G59.1,G59.2,G59.3'),
|
||||||
|
_v_csv(200, 'G61,G61.1,G64'),
|
||||||
|
_v_csv(210, 'G90,G91'),
|
||||||
|
_v_csv(220, 'G98,G99'),
|
||||||
|
_v_csv(230, 'G28,G30,G10,G92,G92.1,G92.2,G94'),
|
||||||
|
_v_csv(240, 'G0,G1,G2,G3,G33,G73,G76,G80,G81,G82,G83,G84,G85,G86,G87,G88,G89'),
|
||||||
|
_v_csv(250, 'M0,M1,M2,M30,M60'),
|
||||||
|
]))
|
||||||
|
|
||||||
|
def _word_order_linuxcnc(word):
|
||||||
|
'''
|
||||||
|
Order taken http://linuxcnc.org/docs/html/gcode/overview.html#_g_code_order_of_execution
|
||||||
|
(as of 2017-07-03)
|
||||||
|
010: O-word commands (optionally followed by a comment but no other words allowed on the same line)
|
||||||
|
N/A: Comment (including message)
|
||||||
|
030: Set feed rate mode (G93, G94).
|
||||||
|
040: Set feed rate (F).
|
||||||
|
050: Set spindle speed (S).
|
||||||
|
060: Select tool (T).
|
||||||
|
070: HAL pin I/O (M62-M68).
|
||||||
|
080: Change tool (M6) and Set Tool Number (M61).
|
||||||
|
090: Spindle on or off (M3, M4, M5).
|
||||||
|
100: Save State (M70, M73), Restore State (M72), Invalidate State (M71).
|
||||||
|
110: Coolant on or off (M7, M8, M9).
|
||||||
|
120: Enable or disable overrides (M48, M49,M50,M51,M52,M53).
|
||||||
|
130: User-defined Commands (M100-M199).
|
||||||
|
140: Dwell (G4).
|
||||||
|
150: Set active plane (G17, G18, G19).
|
||||||
|
160: Set length units (G20, G21).
|
||||||
|
170: Cutter radius compensation on or off (G40, G41, G42)
|
||||||
|
180: Cutter length compensation on or off (G43, G49)
|
||||||
|
190: Coordinate system selection (G54, G55, G56, G57, G58, G59, G59.1, G59.2, G59.3).
|
||||||
|
200: Set path control mode (G61, G61.1, G64)
|
||||||
|
210: Set distance mode (G90, G91).
|
||||||
|
220: Set retract mode (G98, G99).
|
||||||
|
230: Go to reference location (G28, G30) or change coordinate system data (G10) or set axis offsets (G92, G92.1, G92.2, G94).
|
||||||
|
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).
|
||||||
|
900 + letter val: (else)
|
||||||
|
'''
|
||||||
|
if word.letter in ORDER_LINUXCNC_LETTER_MAP:
|
||||||
|
return ORDER_LINUXCNC_LETTER_MAP[word.letter]
|
||||||
|
letter_value = str(word)
|
||||||
|
if letter_value in ORDER_LINUXCNC_LETTERVALUE_MAP:
|
||||||
|
return ORDER_LINUXCNC_LETTERVALUE_MAP[letter_value]
|
||||||
|
|
||||||
|
# special cases
|
||||||
|
if (word.letter == 'M') and (100 <= int(word.value) <= 199):
|
||||||
|
return 130
|
||||||
|
if (word.letter == 'G') and (38 < float(word.value) < 39):
|
||||||
|
return 240
|
||||||
|
|
||||||
|
# otherwise, sort last, in alphabetic order
|
||||||
|
return (900 + (ord(word.letter) - ord('A')))
|
||||||
|
|
||||||
|
def by_linuxcnc_order(word):
|
||||||
|
return word.orderval_linuxcnc
|
||||||
|
|
||||||
|
|
||||||
|
class Word(object):
|
||||||
|
def __init__(self, letter, value):
|
||||||
|
self.letter = letter.upper()
|
||||||
|
|
||||||
|
self._value_str = None
|
||||||
|
self._value = None
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
self._value_str = value
|
||||||
|
else:
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
# Sorting Order
|
||||||
|
self._order_linuxcnc = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{letter}{value}".format(
|
||||||
|
letter=self.letter,
|
||||||
|
value=self.value_str,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Value Properties
|
||||||
|
@property
|
||||||
|
def value_str(self):
|
||||||
|
"""Value string, or """
|
||||||
|
if self._value_str is None:
|
||||||
|
return str(self._value)
|
||||||
|
return self._value_str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
if self._value is None:
|
||||||
|
return WORD_MAP[self.letter]['class'](self._value_str)
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
# Order
|
||||||
|
@property
|
||||||
|
def orderval_linuxcnc(self):
|
||||||
|
if self._order_linuxcnc is None:
|
||||||
|
self._order_linuxcnc = _word_order_linuxcnc(self)
|
||||||
|
return self._order_linuxcnc
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
return WORD_MAP[self.letter]['description']
|
||||||
|
|
||||||
|
NEXT_WORD = re.compile(r'^.*?(?P<letter>[%s])' % ''.join(WORD_MAP.keys()), re.IGNORECASE)
|
||||||
|
|
||||||
|
def iter_words(block_text):
|
||||||
|
"""
|
||||||
|
Iterate through block text yielding Word instances
|
||||||
|
:param block_text: text for given block with comments removed
|
||||||
|
"""
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
letter_match = NEXT_WORD.search(block_text[index:])
|
||||||
|
if letter_match:
|
||||||
|
# Letter
|
||||||
|
letter = letter_match.group('letter').upper()
|
||||||
|
index += letter_match.end() # propogate index to start of value
|
||||||
|
|
||||||
|
# Value
|
||||||
|
value_regex = WORD_MAP[letter]['value_regex']
|
||||||
|
value_match = value_regex.search(block_text[index:])
|
||||||
|
if value_match is None:
|
||||||
|
raise GCodeBlockFormatError("word '%s' value invalid" % letter)
|
||||||
|
value = value_match.group() # matched text
|
||||||
|
|
||||||
|
yield Word(letter, value)
|
||||||
|
|
||||||
|
index += value_match.end() # propogate index to end of value
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
remainder = block_text[index:]
|
||||||
|
if remainder and re.search(r'\S', remainder):
|
||||||
|
raise GCodeBlockFormatError("block code remaining '%s'" % remainder)
|
3
tests/runtests.sh
Executable file
3
tests/runtests.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
python -m unittest discover -s . -p 'test_*.py' --verbose
|
26
tests/test-files/vertical-slot.ngc
Normal file
26
tests/test-files/vertical-slot.ngc
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
T5 M06 G43
|
||||||
|
G17 G90 G21
|
||||||
|
G00 Z5 S7000 M03
|
||||||
|
X9.60 Y48.74
|
||||||
|
Z2
|
||||||
|
G01 Z-0.5 F100
|
||||||
|
G02 X10.75 Y47.44 I-0.11 J-1.26 F70
|
||||||
|
G01 X10.75 Y-47.44
|
||||||
|
G02 X9.51 Y-48.69 I-1.25 J0
|
||||||
|
G02 X8.25 Y-47.44 I0 J1.25
|
||||||
|
G01 X8.25 Y-47.44
|
||||||
|
X8.25 Y47.44
|
||||||
|
G02 X9.6 Y48.74 I1.25 J0.05
|
||||||
|
G00 Z5
|
||||||
|
Z1.5
|
||||||
|
G01 Z-1 F100
|
||||||
|
G02 X10.75 Y47.44 I-0.11 J-1.26 F70
|
||||||
|
G01 X10.75 Y-47.44
|
||||||
|
G02 X9.51 Y-48.69 I-1.25 J0
|
||||||
|
G02 X8.25 Y-47.44 I0 J1.25
|
||||||
|
G01 X8.25 Y-47.44
|
||||||
|
X8.25 Y47.44
|
||||||
|
G02 X9.60 Y48.74 I1.25 J0.05
|
||||||
|
G00 Z5
|
||||||
|
T0 M06 M02
|
||||||
|
|
20
tests/test_file.py
Normal file
20
tests/test_file.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
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
|
||||||
|
|
||||||
|
class FileParseTest(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(' '.join(["%s%s" % (w.letter, w.value_str) for w in line.block.words]))
|
27
tests/test_line.py
Normal file
27
tests/test_line.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
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.line import Line
|
||||||
|
|
||||||
|
|
||||||
|
class LineCommentTests(unittest.TestCase):
|
||||||
|
def test_line_comment_semicolon(self):
|
||||||
|
line = Line('G02 X10.75 Y47.44 I-0.11 J-1.26 F70 ; blah blah')
|
||||||
|
self.assertEqual(line.comment.text, 'blah blah')
|
||||||
|
self.assertEqual(len(line.block.words), 6)
|
||||||
|
|
||||||
|
def test_line_comment_brackets(self):
|
||||||
|
line = Line('G02 X10.75 Y47.44 I-0.11 J-1.26 F70 (blah blah)')
|
||||||
|
self.assertEqual(line.comment.text, 'blah blah')
|
||||||
|
self.assertEqual(len(line.block.words), 6)
|
||||||
|
|
||||||
|
def test_line_comment_brackets_multi(self):
|
||||||
|
line = Line('G02 X10.75 (x coord) Y47.44 (y coord) I-0.11 J-1.26 F70 (eol)')
|
||||||
|
self.assertEqual(line.comment.text, 'x coord. y coord. eol')
|
||||||
|
self.assertEqual(len(line.block.words), 6)
|
42
tests/test_words.py
Normal file
42
tests/test_words.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
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, '..'))
|
||||||
|
import pygcode.words as gcode_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(gcode_words.iter_words(block_str))
|
||||||
|
# word length
|
||||||
|
self.assertEqual(len(w), 3)
|
||||||
|
# word values
|
||||||
|
self.assertEqual([w[0].letter, w[0].value], ['G', '01'])
|
||||||
|
self.assertEqual([w[1].letter, w[1].value], ['Z', -0.5])
|
||||||
|
self.assertEqual([w[2].letter, w[2].value], ['F', 100])
|
||||||
|
|
||||||
|
def test_iter2(self):
|
||||||
|
block_str = 'G02 X10.75 Y47.44 I-0.11 J-1.26 F70'
|
||||||
|
w = list(gcode_words.iter_words(block_str))
|
||||||
|
# word length
|
||||||
|
self.assertEqual(len(w), 6)
|
||||||
|
# word values
|
||||||
|
self.assertEqual([w[0].letter, w[0].value], ['G', '02'])
|
||||||
|
self.assertEqual([w[1].letter, w[1].value], ['X', 10.75])
|
||||||
|
self.assertEqual([w[2].letter, w[2].value], ['Y', 47.44])
|
||||||
|
self.assertEqual([w[3].letter, w[3].value], ['I', -0.11])
|
||||||
|
self.assertEqual([w[4].letter, w[4].value], ['J', -1.26])
|
||||||
|
self.assertEqual([w[5].letter, w[5].value], ['F', 70])
|
Loading…
x
Reference in New Issue
Block a user