mirror of
https://git.mirrors.martin98.com/https://github.com/petaflot/pygcode
synced 2025-07-04 12:15:11 +08:00
commit
35a1dab19f
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# python cache / compilations
|
||||||
|
*__pycache__
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# editor backups
|
||||||
|
*.swp
|
@ -1,31 +1,44 @@
|
|||||||
# pygcode
|
pygcode
|
||||||
|
=======
|
||||||
|
|
||||||
GCODE Parser for Python
|
GCODE Parser for Python
|
||||||
|
|
||||||
Currently in development, this is planned to be a pythonic interpreter and encoder for g-code.
|
Currently in development, this is planned to be a pythonic interpreter
|
||||||
I'll be learning along the way, but the plan is to follow the lead of [GRBL](https://github.com/gnea/grbl).
|
and encoder for g-code. I'll be learning along the way, but the plan is
|
||||||
|
to follow the lead of `GRBL <https://github.com/gnea/grbl>`__.
|
||||||
|
|
||||||
## Installation
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
`pip install pygcode`
|
``pip install pygcode``
|
||||||
|
|
||||||
## Usage
|
FIXME: well, that's the plan... give me some time to get it going
|
||||||
|
though.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
Just brainstorming here...
|
Just brainstorming here...
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
import pygcode
|
import pygcode
|
||||||
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 +49,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,16 +64,23 @@ 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)
|
||||||
|
|
||||||
gfile_in.close()
|
gfile_in.close()
|
||||||
gfile_out.close()
|
gfile_out.close()
|
||||||
|
|
||||||
|
Supported G-Codes
|
||||||
|
-----------------
|
||||||
|
|
||||||
## Supported G-Codes
|
GCode support is planned to follow that of
|
||||||
GCode support is planned to follow that of [GRBL](https://github.com/gnea/grbl).
|
`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.
|
||||||
|
|
||||||
TBD: list of gcodes (also as a TODO list)
|
TBD: list of gcodes (also as a TODO list)
|
226
scripts/pygcode-normalize.py
Executable file
226
scripts/pygcode-normalize.py
Executable file
@ -0,0 +1,226 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Script to take (theoretically) any g-code file as input, and output a
|
||||||
|
# normalized version of it.
|
||||||
|
#
|
||||||
|
# Script outcome can have cursory verification with:
|
||||||
|
# https://nraynaud.github.io/webgcode/
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
for pygcode_lib_type in ('installed_lib', 'relative_lib'):
|
||||||
|
try:
|
||||||
|
# pygcode
|
||||||
|
from pygcode import Word
|
||||||
|
from pygcode import Machine, Mode, Line
|
||||||
|
from pygcode import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW
|
||||||
|
from pygcode import GCodeCannedCycle
|
||||||
|
from pygcode import split_gcodes
|
||||||
|
from pygcode import Comment
|
||||||
|
from pygcode.transform import linearize_arc, simplify_canned_cycle
|
||||||
|
from pygcode.transform import ArcLinearizeInside, ArcLinearizeOutside, ArcLinearizeMid
|
||||||
|
from pygcode.gcodes import _subclasses
|
||||||
|
from pygcode.utils import omit_redundant_modes
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
import sys, os, inspect
|
||||||
|
# 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, '..'))
|
||||||
|
if pygcode_lib_type == 'installed_lib':
|
||||||
|
continue # import was attempted before sys.path addition. retry import
|
||||||
|
raise # otherwise the raised ImportError is a genuine problem
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# =================== Command Line Arguments ===================
|
||||||
|
# --- Defaults
|
||||||
|
DEFAULT_PRECISION = 0.005 # mm
|
||||||
|
DEFAULT_MACHINE_MODE = 'G0 G54 G17 G21 G90 G94 M5 M9 T0 F0 S0'
|
||||||
|
DEFAULT_ARC_LIN_METHOD = 'm'
|
||||||
|
DEFAULT_CANNED_CODES = ','.join(str(w) for w in sorted(c.word_key for c in _subclasses(GCodeCannedCycle) if c.word_key))
|
||||||
|
|
||||||
|
# --- Create Parser
|
||||||
|
parser = argparse.ArgumentParser(description='Normalize gcode for machine consistency using different CAM software')
|
||||||
|
parser.add_argument(
|
||||||
|
'infile', type=argparse.FileType('r'), nargs=1,
|
||||||
|
help="gcode file to normalize",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--precision', '-p', dest='precision', type=float, default=DEFAULT_PRECISION,
|
||||||
|
help="maximum positional error when generating gcodes (eg: arcs to lines) "
|
||||||
|
"(default: %g)" % DEFAULT_PRECISION,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Machine
|
||||||
|
parser.add_argument(
|
||||||
|
'--machine_mode', '-mm', dest='machine_mode', default=DEFAULT_MACHINE_MODE,
|
||||||
|
help="Machine's startup mode as gcode (default: '%s')" % DEFAULT_MACHINE_MODE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Arc Linearizing
|
||||||
|
group = parser.add_argument_group(
|
||||||
|
"Arc Linearizing",
|
||||||
|
"Converting arcs (G2/G3 codes) into linear interpolations (G1 codes) to "
|
||||||
|
"aproximate the original arc. Indistinguishable from an original arc when "
|
||||||
|
"--precision is set low enough."
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
'--arc_linearize', '-al', dest='arc_linearize',
|
||||||
|
action='store_const', const=True, default=False,
|
||||||
|
help="convert G2,G3 commands to a series of linear interpolations (G1 codes)",
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
'--arc_lin_method', '-alm', dest='arc_lin_method', default=DEFAULT_ARC_LIN_METHOD,
|
||||||
|
help="Method of linearizing arcs, i=inner, o=outer, m=mid. List 2 "
|
||||||
|
"for <ccw>,<cw>, eg 'i,o'. 'i' is equivalent to 'i,i'. "
|
||||||
|
"(default: '%s')" % DEFAULT_ARC_LIN_METHOD,
|
||||||
|
metavar='{i,o,m}[,{i,o,m}]',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Canned Cycles
|
||||||
|
group = parser.add_argument_group(
|
||||||
|
"Canned Cycle Simplification",
|
||||||
|
"Convert canned cycles into basic linear or scalar codes, such as linear "
|
||||||
|
"interpolation (G1), and pauses (or 'dwells', G4)"
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
'--canned_expand', '-ce', dest='canned_expand',
|
||||||
|
action='store_const', const=True, default=False,
|
||||||
|
help="Expand canned cycles into basic linear movements, and pauses",
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
'---canned_codes', '-cc', dest='canned_codes', default=DEFAULT_CANNED_CODES,
|
||||||
|
help="List of canned gcodes to expand, (default is '%s')" % DEFAULT_CANNED_CODES,
|
||||||
|
)
|
||||||
|
|
||||||
|
#parser.add_argument(
|
||||||
|
# '--arc_alignment', '-aa', dest='arc_alignment', type=str, choices=('XYZ','IJK','R'),
|
||||||
|
# default=None,
|
||||||
|
# help="enforce precision on arcs, if XYZ the destination is altered to match the radius"
|
||||||
|
# "if IJK or R then the arc'c centre point is moved to assure precision",
|
||||||
|
#)
|
||||||
|
|
||||||
|
# --- Parse Arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Manually Parsing : Arc Linearizing Method
|
||||||
|
# args.arc_lin_method = {Word('G2'): <linearize method class>, ... }
|
||||||
|
ARC_LIN_CLASS_MAP = {
|
||||||
|
'i': ArcLinearizeInside,
|
||||||
|
'o': ArcLinearizeOutside,
|
||||||
|
'm': ArcLinearizeMid,
|
||||||
|
}
|
||||||
|
|
||||||
|
arc_lin_method_regex = re.compile(r'^(?P<g2>[iom])(,(?P<g3>[iom]))?$', re.I)
|
||||||
|
if args.arc_lin_method:
|
||||||
|
match = arc_lin_method_regex.search(args.arc_lin_method)
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError("parameter for --arc_lin_method is invalid: '%s'" % args.arc_lin_method)
|
||||||
|
|
||||||
|
# changing args.arc_lin_method (because I'm a fiend)
|
||||||
|
args.arc_lin_method = {}
|
||||||
|
args.arc_lin_method[Word('g2')] = ARC_LIN_CLASS_MAP[match.group('g2')]
|
||||||
|
if match.group('g3'):
|
||||||
|
args.arc_lin_method[Word('g3')] = ARC_LIN_CLASS_MAP[match.group('g3')]
|
||||||
|
else:
|
||||||
|
args.arc_lin_method[Word('g3')] = args.arc_lin_method[Word('g2')]
|
||||||
|
else:
|
||||||
|
# FIXME: change default to ArcLinearizeMid (when it's working)
|
||||||
|
args.arc_lin_method = defaultdict(lambda: ArcLinearizeMid) # just to be sure
|
||||||
|
|
||||||
|
|
||||||
|
# --- Manually Parsing : Canned Codes
|
||||||
|
# args.canned_codes = [Word('G73'), Word('G89'), ... ]
|
||||||
|
canned_code_words = set()
|
||||||
|
for word_str in re.split(r'\s*,\s*', args.canned_codes):
|
||||||
|
canned_code_words.add(Word(word_str))
|
||||||
|
|
||||||
|
args.canned_codes = canned_code_words
|
||||||
|
|
||||||
|
|
||||||
|
# =================== Create Virtual CNC Machine ===================
|
||||||
|
class MyMode(Mode):
|
||||||
|
default_mode = args.machine_mode
|
||||||
|
|
||||||
|
class MyMachine(Machine):
|
||||||
|
MODE_CLASS = MyMode
|
||||||
|
|
||||||
|
machine = MyMachine()
|
||||||
|
|
||||||
|
# =================== Utility Functions ===================
|
||||||
|
def gcodes2str(gcodes):
|
||||||
|
return ' '.join("%s" % g for g in gcodes)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def split_and_process(gcode_list, gcode_class, comment):
|
||||||
|
"""
|
||||||
|
Split gcodes by given class, yields given class instance
|
||||||
|
:param gcode_list: list of GCode instances
|
||||||
|
:param gcode_class: class inheriting from GCode (directly, or indirectly)
|
||||||
|
:param comment: Comment instance, or None
|
||||||
|
"""
|
||||||
|
(befores, (g,), afters) = split_gcodes(gcode_list, gcode_class)
|
||||||
|
# print & process those before gcode_class instance
|
||||||
|
if befores:
|
||||||
|
print(gcodes2str(befores))
|
||||||
|
machine.process_gcodes(*befores)
|
||||||
|
# yield, then process gcode_class instance
|
||||||
|
yield g
|
||||||
|
machine.process_gcodes(g)
|
||||||
|
# print & process those after gcode_class instance
|
||||||
|
if afters:
|
||||||
|
print(gcodes2str(afters))
|
||||||
|
machine.process_gcodes(*afters)
|
||||||
|
# print comment (if given)
|
||||||
|
if comment:
|
||||||
|
print(str(line.comment))
|
||||||
|
|
||||||
|
|
||||||
|
# =================== Process File ===================
|
||||||
|
|
||||||
|
for line_str in args.infile[0].readlines():
|
||||||
|
line = Line(line_str)
|
||||||
|
|
||||||
|
# Effective G-Codes:
|
||||||
|
# fills in missing motion modal gcodes (using machine's current motion mode).
|
||||||
|
effective_gcodes = machine.block_modal_gcodes(line.block)
|
||||||
|
|
||||||
|
if args.arc_linearize and any(isinstance(g, GCodeArcMove) for g in effective_gcodes):
|
||||||
|
with split_and_process(effective_gcodes, GCodeArcMove, line.comment) as arc:
|
||||||
|
print(Comment("linearized arc: %r" % arc))
|
||||||
|
linearize_params = {
|
||||||
|
'arc_gcode': arc,
|
||||||
|
'start_pos': machine.pos,
|
||||||
|
'plane': machine.mode.plane_selection,
|
||||||
|
'method_class': args.arc_lin_method[arc.word],
|
||||||
|
'dist_mode': machine.mode.distance,
|
||||||
|
'arc_dist_mode': machine.mode.arc_ijk_distance,
|
||||||
|
'max_error': args.precision,
|
||||||
|
'decimal_places': 3,
|
||||||
|
}
|
||||||
|
for linear_gcode in omit_redundant_modes(linearize_arc(**linearize_params)):
|
||||||
|
print(linear_gcode)
|
||||||
|
|
||||||
|
elif args.canned_expand and any((g.word in args.canned_codes) for g in effective_gcodes):
|
||||||
|
with split_and_process(effective_gcodes, GCodeCannedCycle, line.comment) as canned:
|
||||||
|
print(Comment("expanded: %r" % canned))
|
||||||
|
simplify_canned_params = {
|
||||||
|
'canned_gcode': canned,
|
||||||
|
'start_pos': machine.pos,
|
||||||
|
'plane': machine.mode.plane_selection,
|
||||||
|
'dist_mode': machine.mode.distance,
|
||||||
|
'axes': machine.axes,
|
||||||
|
}
|
||||||
|
for simplified_gcode in omit_redundant_modes(simplify_canned_cycle(**simplify_canned_params)):
|
||||||
|
print(simplified_gcode)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(str(line))
|
||||||
|
machine.process_block(line.block)
|
6
setup.cfg
Normal file
6
setup.cfg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[bdist_wheel]
|
||||||
|
universal = 1
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
description-file = README.md
|
||||||
|
license_file = LICENSE
|
96
setup.py
Normal file
96
setup.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import codecs
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
|
||||||
|
NAME = "attrs"
|
||||||
|
PACKAGES = find_packages(where="src")
|
||||||
|
META_PATH = os.path.join("src", "attr", "__init__.py")
|
||||||
|
KEYWORDS = ["class", "attribute", "boilerplate"]
|
||||||
|
CLASSIFIERS = [
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Natural Language :: English",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Programming Language :: Python :: 2",
|
||||||
|
"Programming Language :: Python :: 2.7",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.3",
|
||||||
|
"Programming Language :: Python :: 3.4",
|
||||||
|
"Programming Language :: Python :: 3.5",
|
||||||
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
]
|
||||||
|
INSTALL_REQUIRES = []
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
|
||||||
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def read(*parts):
|
||||||
|
"""
|
||||||
|
Build an absolute path from *parts* and and return the contents of the
|
||||||
|
resulting file. Assume UTF-8 encoding.
|
||||||
|
"""
|
||||||
|
with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
META_FILE = read(META_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def find_meta(meta):
|
||||||
|
"""
|
||||||
|
Extract __*meta*__ from META_FILE.
|
||||||
|
"""
|
||||||
|
meta_match = re.search(
|
||||||
|
r"^(?P<name>__{meta}__)\s*=\s*['\"](?P<value>[^'\"]*)['\"]".format(meta=meta),
|
||||||
|
META_FILE, re.M
|
||||||
|
)
|
||||||
|
if meta_match:
|
||||||
|
return meta_match.group('value')
|
||||||
|
raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
setup(
|
||||||
|
name=NAME,
|
||||||
|
description=find_meta("description"),
|
||||||
|
license=find_meta("license"),
|
||||||
|
url=find_meta("uri"),
|
||||||
|
version=find_meta("version"),
|
||||||
|
author=find_meta("author"),
|
||||||
|
author_email=find_meta("email"),
|
||||||
|
maintainer=find_meta("author"),
|
||||||
|
maintainer_email=find_meta("email"),
|
||||||
|
keywords=KEYWORDS,
|
||||||
|
long_description=read("README.rst"),
|
||||||
|
packages=PACKAGES,
|
||||||
|
package_dir={"": "src"},
|
||||||
|
zip_safe=False,
|
||||||
|
classifiers=CLASSIFIERS,
|
||||||
|
install_requires=INSTALL_REQUIRES,
|
||||||
|
)
|
||||||
|
|
||||||
|
#VERSION = '0.1.dev' # *.dev = release candidate
|
||||||
|
#
|
||||||
|
#setup(
|
||||||
|
# name = 'pygcode',
|
||||||
|
# packages = ['pygcode'],
|
||||||
|
# version = VERSION,
|
||||||
|
# description = 'basic g-code parser, interpreter, and writer library',
|
||||||
|
# author = 'Peter Boin',
|
||||||
|
# author_email = 'peter.boin@gmail.com',
|
||||||
|
# url = 'https://github.com/fragmuffin/pygcode',
|
||||||
|
# download_url = 'https://github.com/fragmuffin/pygcode/archive/%s.tar.gz' % VERSION,
|
||||||
|
# keywords = ['gcode', 'cnc', 'parser', 'interpreter'],
|
||||||
|
# classifiers = [],
|
||||||
|
#)
|
413
src/pygcode/__init__.py
Normal file
413
src/pygcode/__init__.py
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
__all__ = [
|
||||||
|
# Machine
|
||||||
|
'Machine', 'Position', 'CoordinateSystem', 'State', 'Mode',
|
||||||
|
# Line
|
||||||
|
'Line',
|
||||||
|
# Block
|
||||||
|
'Block',
|
||||||
|
# Comment
|
||||||
|
'Comment', 'split_line',
|
||||||
|
# Word
|
||||||
|
'Word', 'text2words', 'str2word', 'words2dict',
|
||||||
|
|
||||||
|
# GCodes
|
||||||
|
'words2gcodes', 'text2gcodes', 'split_gcodes',
|
||||||
|
# $ python -c "from pygcode.gcodes import GCode, _subclasses as sc; print(',\\n '.join(sorted('\\'%s\\'' % g.__name__ for g in sc(GCode))))"python -c "from pygcode.gcodes import GCode, _subclasses as sc; print(',\\n '.join(sorted('\\'%s\\'' % g.__name__ for g in sc(GCode))))"
|
||||||
|
'GCode',
|
||||||
|
'GCodeAbsoluteArcDistanceMode',
|
||||||
|
'GCodeAbsoluteDistanceMode',
|
||||||
|
'GCodeAdaptiveFeed',
|
||||||
|
'GCodeAddToolLengthOffset',
|
||||||
|
'GCodeAnalogOutput',
|
||||||
|
'GCodeAnalogOutputImmediate',
|
||||||
|
'GCodeAnalogOutputSyncd',
|
||||||
|
'GCodeArcMove',
|
||||||
|
'GCodeArcMoveCCW',
|
||||||
|
'GCodeArcMoveCW',
|
||||||
|
'GCodeBoringCycleDwellFeedOut',
|
||||||
|
'GCodeBoringCycleFeedOut',
|
||||||
|
'GCodeCancelCannedCycle',
|
||||||
|
'GCodeCancelToolLengthOffset',
|
||||||
|
'GCodeCannedCycle',
|
||||||
|
'GCodeCannedCycleReturnLevel',
|
||||||
|
'GCodeCannedReturnMode',
|
||||||
|
'GCodeCoolant',
|
||||||
|
'GCodeCoolantFloodOn',
|
||||||
|
'GCodeCoolantMistOn',
|
||||||
|
'GCodeCoolantOff',
|
||||||
|
'GCodeCoordSystemOffset',
|
||||||
|
'GCodeCublcSpline',
|
||||||
|
'GCodeCutterCompLeft',
|
||||||
|
'GCodeCutterCompRight',
|
||||||
|
'GCodeCutterRadiusComp',
|
||||||
|
'GCodeCutterRadiusCompOff',
|
||||||
|
'GCodeDigitalOutput',
|
||||||
|
'GCodeDigitalOutputOff',
|
||||||
|
'GCodeDigitalOutputOffSyncd',
|
||||||
|
'GCodeDigitalOutputOn',
|
||||||
|
'GCodeDigitalOutputOnSyncd',
|
||||||
|
'GCodeDistanceMode',
|
||||||
|
'GCodeDrillingCycle',
|
||||||
|
'GCodeDrillingCycleChipBreaking',
|
||||||
|
'GCodeDrillingCycleDwell',
|
||||||
|
'GCodeDrillingCyclePeck',
|
||||||
|
'GCodeDwell',
|
||||||
|
'GCodeDynamicCutterCompLeft',
|
||||||
|
'GCodeDynamicCutterCompRight',
|
||||||
|
'GCodeDynamicToolLengthOffset',
|
||||||
|
'GCodeEndProgram',
|
||||||
|
'GCodeEndProgramPalletShuttle',
|
||||||
|
'GCodeExactPathMode',
|
||||||
|
'GCodeExactStopMode',
|
||||||
|
'GCodeFeedOverride',
|
||||||
|
'GCodeFeedRate',
|
||||||
|
'GCodeFeedRateMode',
|
||||||
|
'GCodeFeedStop',
|
||||||
|
'GCodeGotoPredefinedPosition',
|
||||||
|
'GCodeIO',
|
||||||
|
'GCodeIncrementalArcDistanceMode',
|
||||||
|
'GCodeIncrementalDistanceMode',
|
||||||
|
'GCodeInverseTimeMode',
|
||||||
|
'GCodeLatheDiameterMode',
|
||||||
|
'GCodeLatheRadiusMode',
|
||||||
|
'GCodeLinearMove',
|
||||||
|
'GCodeMotion',
|
||||||
|
'GCodeMoveInMachineCoords',
|
||||||
|
'GCodeNURBS',
|
||||||
|
'GCodeNURBSEnd',
|
||||||
|
'GCodeNonModal',
|
||||||
|
'GCodeOrientSpindle',
|
||||||
|
'GCodeOtherModal',
|
||||||
|
'GCodePalletChangePause',
|
||||||
|
'GCodePathBlendingMode',
|
||||||
|
'GCodePathControlMode',
|
||||||
|
'GCodePauseProgram',
|
||||||
|
'GCodePauseProgramOptional',
|
||||||
|
'GCodePlaneSelect',
|
||||||
|
'GCodeProgramControl',
|
||||||
|
'GCodeQuadraticSpline',
|
||||||
|
'GCodeRapidMove',
|
||||||
|
'GCodeResetCoordSystemOffset',
|
||||||
|
'GCodeRestoreCoordSystemOffset',
|
||||||
|
'GCodeRigidTapping',
|
||||||
|
'GCodeSelectCoordinateSystem',
|
||||||
|
'GCodeSelectCoordinateSystem1',
|
||||||
|
'GCodeSelectCoordinateSystem2',
|
||||||
|
'GCodeSelectCoordinateSystem3',
|
||||||
|
'GCodeSelectCoordinateSystem4',
|
||||||
|
'GCodeSelectCoordinateSystem5',
|
||||||
|
'GCodeSelectCoordinateSystem6',
|
||||||
|
'GCodeSelectCoordinateSystem7',
|
||||||
|
'GCodeSelectCoordinateSystem8',
|
||||||
|
'GCodeSelectCoordinateSystem9',
|
||||||
|
'GCodeSelectTool',
|
||||||
|
'GCodeSelectUVPlane',
|
||||||
|
'GCodeSelectVWPlane',
|
||||||
|
'GCodeSelectWUPlane',
|
||||||
|
'GCodeSelectXYPlane',
|
||||||
|
'GCodeSelectYZPlane',
|
||||||
|
'GCodeSelectZXPlane',
|
||||||
|
'GCodeSet',
|
||||||
|
'GCodeSetPredefinedPosition',
|
||||||
|
'GCodeSpeedAndFeedOverrideOff',
|
||||||
|
'GCodeSpeedAndFeedOverrideOn',
|
||||||
|
'GCodeSpindle',
|
||||||
|
'GCodeSpindleConstantSurfaceSpeedMode',
|
||||||
|
'GCodeSpindleRPMMode',
|
||||||
|
'GCodeSpindleSpeed',
|
||||||
|
'GCodeSpindleSpeedMode',
|
||||||
|
'GCodeSpindleSpeedOverride',
|
||||||
|
'GCodeSpindleSyncMotion',
|
||||||
|
'GCodeStartSpindle',
|
||||||
|
'GCodeStartSpindleCCW',
|
||||||
|
'GCodeStartSpindleCW',
|
||||||
|
'GCodeStopSpindle',
|
||||||
|
'GCodeStraightProbe',
|
||||||
|
'GCodeThreadingCycle',
|
||||||
|
'GCodeToolChange',
|
||||||
|
'GCodeToolLength',
|
||||||
|
'GCodeToolLengthOffset',
|
||||||
|
'GCodeToolSetCurrent',
|
||||||
|
'GCodeUnit',
|
||||||
|
'GCodeUnitsPerMinuteMode',
|
||||||
|
'GCodeUnitsPerRevolution',
|
||||||
|
'GCodeUseInches',
|
||||||
|
'GCodeUseMillimeters',
|
||||||
|
'GCodeUserDefined',
|
||||||
|
'GCodeWaitOnInput'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Machine
|
||||||
|
from .machine import (
|
||||||
|
Position, CoordinateSystem,
|
||||||
|
State, Mode,
|
||||||
|
Machine,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Line
|
||||||
|
from .line import Line
|
||||||
|
|
||||||
|
# Block
|
||||||
|
from .block import Block
|
||||||
|
|
||||||
|
# Comment
|
||||||
|
from .comment import Comment, split_line
|
||||||
|
|
||||||
|
# Word
|
||||||
|
from .words import (
|
||||||
|
Word,
|
||||||
|
text2words, str2word, words2dict,
|
||||||
|
)
|
||||||
|
|
||||||
|
# GCode
|
||||||
|
from .gcodes import (
|
||||||
|
words2gcodes, text2gcodes, split_gcodes,
|
||||||
|
|
||||||
|
# $ python -c "from pygcode.gcodes import _gcode_class_infostr as x; print(x(prefix=' # '))"
|
||||||
|
# - GCode:
|
||||||
|
# - GCodeCannedCycle:
|
||||||
|
# G89 - GCodeBoringCycleDwellFeedOut: G89: Boring Cycle, Dwell, Feed Out
|
||||||
|
# G85 - GCodeBoringCycleFeedOut: G85: Boring Cycle, Feed Out
|
||||||
|
# G81 - GCodeDrillingCycle: G81: Drilling Cycle
|
||||||
|
# G73 - GCodeDrillingCycleChipBreaking: G73: Drilling Cycle, ChipBreaking
|
||||||
|
# G82 - GCodeDrillingCycleDwell: G82: Drilling Cycle, Dwell
|
||||||
|
# G83 - GCodeDrillingCyclePeck: G83: Drilling Cycle, Peck
|
||||||
|
# G76 - GCodeThreadingCycle: G76: Threading Cycle
|
||||||
|
# - GCodeCannedReturnMode:
|
||||||
|
# G98 - GCodeCannedCycleReturnLevel: G98: Canned Cycle Return Level
|
||||||
|
# - GCodeCoolant:
|
||||||
|
# M08 - GCodeCoolantFloodOn: M8: turn flood coolant on
|
||||||
|
# M07 - GCodeCoolantMistOn: M7: turn mist coolant on
|
||||||
|
# M09 - GCodeCoolantOff: M9: turn all coolant off
|
||||||
|
# - GCodeCutterRadiusComp:
|
||||||
|
# G41 - GCodeCutterCompLeft: G41: Cutter Radius Compensation (left)
|
||||||
|
# G42 - GCodeCutterCompRight: G42: Cutter Radius Compensation (right)
|
||||||
|
# G40 - GCodeCutterRadiusCompOff: G40: Cutter Radius Compensation Off
|
||||||
|
# G41.1 - GCodeDynamicCutterCompLeft: G41.1: Dynamic Cutter Radius Compensation (left)
|
||||||
|
# G42.1 - GCodeDynamicCutterCompRight: G42.1: Dynamic Cutter Radius Compensation (right)
|
||||||
|
# - GCodeDistanceMode:
|
||||||
|
# G90.1 - GCodeAbsoluteArcDistanceMode: G90.1: Absolute Distance Mode for Arc IJK Parameters
|
||||||
|
# G90 - GCodeAbsoluteDistanceMode: G90: Absolute Distance Mode
|
||||||
|
# G91.1 - GCodeIncrementalArcDistanceMode: G91.1: Incremental Distance Mode for Arc IJK Parameters
|
||||||
|
# G91 - GCodeIncrementalDistanceMode: G91: Incremental Distance Mode
|
||||||
|
# G07 - GCodeLatheDiameterMode: G7: Lathe Diameter Mode
|
||||||
|
# G08 - GCodeLatheRadiusMode: G8: Lathe Radius Mode
|
||||||
|
# - GCodeFeedRateMode:
|
||||||
|
# G93 - GCodeInverseTimeMode: G93: Inverse Time Mode
|
||||||
|
# G94 - GCodeUnitsPerMinuteMode: G94: Units Per MinuteMode
|
||||||
|
# G95 - GCodeUnitsPerRevolution: G95: Units Per Revolution
|
||||||
|
# - GCodeIO:
|
||||||
|
# - GCodeAnalogOutput: Analog Output
|
||||||
|
# M68 - GCodeAnalogOutputImmediate: M68: Analog Output, Immediate
|
||||||
|
# M67 - GCodeAnalogOutputSyncd: M67: Analog Output, Synchronized
|
||||||
|
# - GCodeDigitalOutput: Digital Output Control
|
||||||
|
# M65 - GCodeDigitalOutputOff: M65: turn off digital output immediately
|
||||||
|
# M63 - GCodeDigitalOutputOffSyncd: M63: turn off digital output synchronized with motion
|
||||||
|
# M64 - GCodeDigitalOutputOn: M64: turn on digital output immediately
|
||||||
|
# M62 - GCodeDigitalOutputOnSyncd: M62: turn on digital output synchronized with motion
|
||||||
|
# M66 - GCodeWaitOnInput: M66: Wait on Input
|
||||||
|
# - GCodeMotion:
|
||||||
|
# - GCodeArcMove: Arc Move
|
||||||
|
# G03 - GCodeArcMoveCCW: G3: Arc Move (counter-clockwise)
|
||||||
|
# G02 - GCodeArcMoveCW: G2: Arc Move (clockwise)
|
||||||
|
# G80 - GCodeCancelCannedCycle: G80: Cancel Canned Cycle
|
||||||
|
# G05 - GCodeCublcSpline: G5: Cubic Spline
|
||||||
|
# G04 - GCodeDwell: G4: Dwell
|
||||||
|
# G01 - GCodeLinearMove: G1: Linear Move
|
||||||
|
# G05.2 - GCodeNURBS: G5.2: Non-uniform rational basis spline (NURBS)
|
||||||
|
# G05.3 - GCodeNURBSEnd: G5.3: end NURBS mode
|
||||||
|
# G05.1 - GCodeQuadraticSpline: G5.1: Quadratic Spline
|
||||||
|
# G00 - GCodeRapidMove: G0: Rapid Move
|
||||||
|
# G33.1 - GCodeRigidTapping: G33.1: Rigid Tapping
|
||||||
|
# G33 - GCodeSpindleSyncMotion: G33: Spindle Synchronized Motion
|
||||||
|
# - GCodeStraightProbe: G38.2-G38.5: Straight Probe
|
||||||
|
# - GCodeNonModal:
|
||||||
|
# G92 - GCodeCoordSystemOffset: G92: Coordinate System Offset
|
||||||
|
# - GCodeGotoPredefinedPosition: G28,G30: Goto Predefined Position (rapid movement)
|
||||||
|
# G53 - GCodeMoveInMachineCoords: G53: Move in Machine Coordinates
|
||||||
|
# - GCodeResetCoordSystemOffset: G92.1,G92.2: Reset Coordinate System Offset
|
||||||
|
# G92.3 - GCodeRestoreCoordSystemOffset: G92.3: Restore Coordinate System Offset
|
||||||
|
# G10 - GCodeSet: G10: Set stuff
|
||||||
|
# - GCodeSetPredefinedPosition: G28.1,G30.1: Set Predefined Position
|
||||||
|
# M06 - GCodeToolChange: M6: Tool Change
|
||||||
|
# M61 - GCodeToolSetCurrent: M61: Set Current Tool
|
||||||
|
# - GCodeUserDefined: M101-M199: User Defined Commands
|
||||||
|
# - GCodeOtherModal:
|
||||||
|
# M52 - GCodeAdaptiveFeed: M52: Adaptive Feed Control
|
||||||
|
# M50 - GCodeFeedOverride: M50: Feed Override Control
|
||||||
|
# - GCodeFeedRate: F: Set Feed Rate
|
||||||
|
# M53 - GCodeFeedStop: M53: Feed Stop Control
|
||||||
|
# - GCodeSelectCoordinateSystem: Select Coordinate System
|
||||||
|
# G54 - GCodeSelectCoordinateSystem1: Select Coordinate System 1
|
||||||
|
# G55 - GCodeSelectCoordinateSystem2: Select Coordinate System 2
|
||||||
|
# G56 - GCodeSelectCoordinateSystem3: Select Coordinate System 3
|
||||||
|
# G57 - GCodeSelectCoordinateSystem4: Select Coordinate System 4
|
||||||
|
# G58 - GCodeSelectCoordinateSystem5: Select Coordinate System 5
|
||||||
|
# G59 - GCodeSelectCoordinateSystem6: Select Coordinate System 6
|
||||||
|
# G59.1 - GCodeSelectCoordinateSystem7: Select Coordinate System 7
|
||||||
|
# G59.2 - GCodeSelectCoordinateSystem8: Select Coordinate System 8
|
||||||
|
# G59.3 - GCodeSelectCoordinateSystem9: Select Coordinate System 9
|
||||||
|
# - GCodeSelectTool: T: Select Tool
|
||||||
|
# M49 - GCodeSpeedAndFeedOverrideOff: M49: Speed and Feed Override Control Off
|
||||||
|
# M48 - GCodeSpeedAndFeedOverrideOn: M48: Speed and Feed Override Control On
|
||||||
|
# - GCodeSpindleSpeed: S: Set Spindle Speed
|
||||||
|
# M51 - GCodeSpindleSpeedOverride: M51: Spindle Speed Override Control
|
||||||
|
# - GCodePathControlMode:
|
||||||
|
# G61 - GCodeExactPathMode: G61: Exact path mode
|
||||||
|
# G61.1 - GCodeExactStopMode: G61.1: Exact stop mode
|
||||||
|
# G64 - GCodePathBlendingMode: G64: Path Blending
|
||||||
|
# - GCodePlaneSelect:
|
||||||
|
# G17.1 - GCodeSelectUVPlane: G17.1: select UV plane
|
||||||
|
# G19.1 - GCodeSelectVWPlane: G19.1: select VW plane
|
||||||
|
# G18.1 - GCodeSelectWUPlane: G18.1: select WU plane
|
||||||
|
# G17 - GCodeSelectXYPlane: G17: select XY plane (default)
|
||||||
|
# G19 - GCodeSelectYZPlane: G19: select YZ plane
|
||||||
|
# G18 - GCodeSelectZXPlane: G18: select ZX plane
|
||||||
|
# - GCodeProgramControl:
|
||||||
|
# M02 - GCodeEndProgram: M2: Program End
|
||||||
|
# M30 - GCodeEndProgramPalletShuttle: M30: exchange pallet shuttles and end the program
|
||||||
|
# M60 - GCodePalletChangePause: M60: Pallet Change Pause
|
||||||
|
# M00 - GCodePauseProgram: M0: Program Pause
|
||||||
|
# M01 - GCodePauseProgramOptional: M1: Program Pause (optional)
|
||||||
|
# - GCodeSpindle:
|
||||||
|
# M19 - GCodeOrientSpindle: M19: Orient Spindle
|
||||||
|
# - GCodeSpindleSpeedMode:
|
||||||
|
# G96 - GCodeSpindleConstantSurfaceSpeedMode: G96: Spindle Constant Surface Speed
|
||||||
|
# G97 - GCodeSpindleRPMMode: G97: Spindle RPM Speed
|
||||||
|
# - GCodeStartSpindle: M3,M4: Start Spindle Clockwise
|
||||||
|
# M04 - GCodeStartSpindleCCW: M4: Start Spindle Counter-Clockwise
|
||||||
|
# M03 - GCodeStartSpindleCW: M3: Start Spindle Clockwise
|
||||||
|
# M05 - GCodeStopSpindle: M5: Stop Spindle
|
||||||
|
# - GCodeToolLength:
|
||||||
|
# G43.2 - GCodeAddToolLengthOffset: G43.2: Appkly Additional Tool Length Offset
|
||||||
|
# G49 - GCodeCancelToolLengthOffset: G49: Cancel Tool Length Compensation
|
||||||
|
# G43.1 - GCodeDynamicToolLengthOffset: G43.1: Dynamic Tool Length Offset
|
||||||
|
# G43 - GCodeToolLengthOffset: G43: Tool Length Offset
|
||||||
|
# - GCodeUnit:
|
||||||
|
# G20 - GCodeUseInches: G20: use inches for length units
|
||||||
|
# G21 - GCodeUseMillimeters: G21: use millimeters for length units
|
||||||
|
|
||||||
|
# $ python -c "from pygcode.gcodes import GCode, _subclasses as sc; print(',\\n '.join(sorted(g.__name__ for g in sc(GCode))))"
|
||||||
|
GCode,
|
||||||
|
GCodeAbsoluteArcDistanceMode,
|
||||||
|
GCodeAbsoluteDistanceMode,
|
||||||
|
GCodeAdaptiveFeed,
|
||||||
|
GCodeAddToolLengthOffset,
|
||||||
|
GCodeAnalogOutput,
|
||||||
|
GCodeAnalogOutputImmediate,
|
||||||
|
GCodeAnalogOutputSyncd,
|
||||||
|
GCodeArcMove,
|
||||||
|
GCodeArcMoveCCW,
|
||||||
|
GCodeArcMoveCW,
|
||||||
|
GCodeBoringCycleDwellFeedOut,
|
||||||
|
GCodeBoringCycleFeedOut,
|
||||||
|
GCodeCancelCannedCycle,
|
||||||
|
GCodeCancelToolLengthOffset,
|
||||||
|
GCodeCannedCycle,
|
||||||
|
GCodeCannedCycleReturnLevel,
|
||||||
|
GCodeCannedReturnMode,
|
||||||
|
GCodeCoolant,
|
||||||
|
GCodeCoolantFloodOn,
|
||||||
|
GCodeCoolantMistOn,
|
||||||
|
GCodeCoolantOff,
|
||||||
|
GCodeCoordSystemOffset,
|
||||||
|
GCodeCublcSpline,
|
||||||
|
GCodeCutterCompLeft,
|
||||||
|
GCodeCutterCompRight,
|
||||||
|
GCodeCutterRadiusComp,
|
||||||
|
GCodeCutterRadiusCompOff,
|
||||||
|
GCodeDigitalOutput,
|
||||||
|
GCodeDigitalOutputOff,
|
||||||
|
GCodeDigitalOutputOffSyncd,
|
||||||
|
GCodeDigitalOutputOn,
|
||||||
|
GCodeDigitalOutputOnSyncd,
|
||||||
|
GCodeDistanceMode,
|
||||||
|
GCodeDrillingCycle,
|
||||||
|
GCodeDrillingCycleChipBreaking,
|
||||||
|
GCodeDrillingCycleDwell,
|
||||||
|
GCodeDrillingCyclePeck,
|
||||||
|
GCodeDwell,
|
||||||
|
GCodeDynamicCutterCompLeft,
|
||||||
|
GCodeDynamicCutterCompRight,
|
||||||
|
GCodeDynamicToolLengthOffset,
|
||||||
|
GCodeEndProgram,
|
||||||
|
GCodeEndProgramPalletShuttle,
|
||||||
|
GCodeExactPathMode,
|
||||||
|
GCodeExactStopMode,
|
||||||
|
GCodeFeedOverride,
|
||||||
|
GCodeFeedRate,
|
||||||
|
GCodeFeedRateMode,
|
||||||
|
GCodeFeedStop,
|
||||||
|
GCodeGotoPredefinedPosition,
|
||||||
|
GCodeIO,
|
||||||
|
GCodeIncrementalArcDistanceMode,
|
||||||
|
GCodeIncrementalDistanceMode,
|
||||||
|
GCodeInverseTimeMode,
|
||||||
|
GCodeLatheDiameterMode,
|
||||||
|
GCodeLatheRadiusMode,
|
||||||
|
GCodeLinearMove,
|
||||||
|
GCodeMotion,
|
||||||
|
GCodeMoveInMachineCoords,
|
||||||
|
GCodeNURBS,
|
||||||
|
GCodeNURBSEnd,
|
||||||
|
GCodeNonModal,
|
||||||
|
GCodeOrientSpindle,
|
||||||
|
GCodeOtherModal,
|
||||||
|
GCodePalletChangePause,
|
||||||
|
GCodePathBlendingMode,
|
||||||
|
GCodePathControlMode,
|
||||||
|
GCodePauseProgram,
|
||||||
|
GCodePauseProgramOptional,
|
||||||
|
GCodePlaneSelect,
|
||||||
|
GCodeProgramControl,
|
||||||
|
GCodeQuadraticSpline,
|
||||||
|
GCodeRapidMove,
|
||||||
|
GCodeResetCoordSystemOffset,
|
||||||
|
GCodeRestoreCoordSystemOffset,
|
||||||
|
GCodeRigidTapping,
|
||||||
|
GCodeSelectCoordinateSystem,
|
||||||
|
GCodeSelectCoordinateSystem1,
|
||||||
|
GCodeSelectCoordinateSystem2,
|
||||||
|
GCodeSelectCoordinateSystem3,
|
||||||
|
GCodeSelectCoordinateSystem4,
|
||||||
|
GCodeSelectCoordinateSystem5,
|
||||||
|
GCodeSelectCoordinateSystem6,
|
||||||
|
GCodeSelectCoordinateSystem7,
|
||||||
|
GCodeSelectCoordinateSystem8,
|
||||||
|
GCodeSelectCoordinateSystem9,
|
||||||
|
GCodeSelectTool,
|
||||||
|
GCodeSelectUVPlane,
|
||||||
|
GCodeSelectVWPlane,
|
||||||
|
GCodeSelectWUPlane,
|
||||||
|
GCodeSelectXYPlane,
|
||||||
|
GCodeSelectYZPlane,
|
||||||
|
GCodeSelectZXPlane,
|
||||||
|
GCodeSet,
|
||||||
|
GCodeSetPredefinedPosition,
|
||||||
|
GCodeSpeedAndFeedOverrideOff,
|
||||||
|
GCodeSpeedAndFeedOverrideOn,
|
||||||
|
GCodeSpindle,
|
||||||
|
GCodeSpindleConstantSurfaceSpeedMode,
|
||||||
|
GCodeSpindleRPMMode,
|
||||||
|
GCodeSpindleSpeed,
|
||||||
|
GCodeSpindleSpeedMode,
|
||||||
|
GCodeSpindleSpeedOverride,
|
||||||
|
GCodeSpindleSyncMotion,
|
||||||
|
GCodeStartSpindle,
|
||||||
|
GCodeStartSpindleCCW,
|
||||||
|
GCodeStartSpindleCW,
|
||||||
|
GCodeStopSpindle,
|
||||||
|
GCodeStraightProbe,
|
||||||
|
GCodeThreadingCycle,
|
||||||
|
GCodeToolChange,
|
||||||
|
GCodeToolLength,
|
||||||
|
GCodeToolLengthOffset,
|
||||||
|
GCodeToolSetCurrent,
|
||||||
|
GCodeUnit,
|
||||||
|
GCodeUnitsPerMinuteMode,
|
||||||
|
GCodeUnitsPerRevolution,
|
||||||
|
GCodeUseInches,
|
||||||
|
GCodeUseMillimeters,
|
||||||
|
GCodeUserDefined,
|
||||||
|
GCodeWaitOnInput
|
||||||
|
)
|
85
src/pygcode/block.py
Normal file
85
src/pygcode/block.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import re
|
||||||
|
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=None, verify=True):
|
||||||
|
"""
|
||||||
|
Block Constructor
|
||||||
|
:param A-Z: gcode parameter values
|
||||||
|
:param comment: comment text
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._raw_text = None
|
||||||
|
self._text = None
|
||||||
|
self.words = []
|
||||||
|
self.gcodes = []
|
||||||
|
self.modal_params = []
|
||||||
|
|
||||||
|
# clean up block string
|
||||||
|
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
|
||||||
|
|
||||||
|
# Get words from text, and group into gcodes
|
||||||
|
self.words = list(text2words(self._text))
|
||||||
|
(self.gcodes, self.modal_params) = words2gcodes(self.words)
|
||||||
|
|
||||||
|
# Verification
|
||||||
|
if verify:
|
||||||
|
self._assert_gcodes()
|
||||||
|
|
||||||
|
@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" % ([
|
||||||
|
x for x in self.gcodes
|
||||||
|
if x.modal_group == gc.modal_group
|
||||||
|
]))
|
||||||
|
code_words.add(gc.word)
|
||||||
|
|
||||||
|
# Assert all gcodes are from different modal groups
|
||||||
|
if gc.modal_group is not None:
|
||||||
|
if gc.modal_group in modal_groups:
|
||||||
|
raise AssertionError("%s cannot be in the same block" % ([
|
||||||
|
x for x in self.gcodes
|
||||||
|
if x.modal_group == gc.modal_group
|
||||||
|
]))
|
||||||
|
modal_groups.add(gc.modal_group)
|
||||||
|
|
||||||
|
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
|
||||||
|
))
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(self.words)
|
||||||
|
|
||||||
|
__nonzero__ = __bool__ # python < 3 compatability
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ' '.join(str(x) for x in (self.gcodes + self.modal_params))
|
67
src/pygcode/comment.py
Normal file
67
src/pygcode/comment.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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'"
|
||||||
|
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)
|
18
src/pygcode/exceptions.py
Normal file
18
src/pygcode/exceptions.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
# ===================== Parsing Exceptions =====================
|
||||||
|
class GCodeBlockFormatError(Exception):
|
||||||
|
"""Raised when errors encountered while parsing block text"""
|
||||||
|
|
||||||
|
class GCodeParameterError(Exception):
|
||||||
|
"""Raised for conflicting / invalid / badly formed parameters"""
|
||||||
|
|
||||||
|
class GCodeWordStrError(Exception):
|
||||||
|
"""Raised when issues found while parsing a word string"""
|
||||||
|
|
||||||
|
# ===================== Machine Exceptions =====================
|
||||||
|
class MachineInvalidAxis(Exception):
|
||||||
|
"""Raised if an axis is invalid"""
|
||||||
|
# For example: for axes X/Y/Z, set the value of "Q"; wtf?
|
||||||
|
|
||||||
|
class MachineInvalidState(Exception):
|
||||||
|
"""Raised if a machine state is set incorrectly, or in conflict"""
|
1533
src/pygcode/gcodes.py
Normal file
1533
src/pygcode/gcodes.py
Normal file
File diff suppressed because it is too large
Load Diff
31
src/pygcode/line.py
Normal file
31
src/pygcode/line.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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)
|
||||||
|
self.block = Block(block_str)
|
||||||
|
if comment:
|
||||||
|
self.comment = comment
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
if self._text is None:
|
||||||
|
return str(self)
|
||||||
|
return self._text
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gcodes(self):
|
||||||
|
"""self.block.gcodes passthrough"""
|
||||||
|
return self.block.gcodes
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ' '.join([str(x) for x in [self.block, self.comment] if x])
|
443
src/pygcode/machine.py
Normal file
443
src/pygcode/machine.py
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
from copy import copy, deepcopy
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from .gcodes import (
|
||||||
|
MODAL_GROUP_MAP, GCode,
|
||||||
|
# Modal GCodes
|
||||||
|
GCodeIncrementalDistanceMode,
|
||||||
|
GCodeUseInches, GCodeUseMillimeters,
|
||||||
|
# Utilities
|
||||||
|
words2gcodes,
|
||||||
|
)
|
||||||
|
from .block import Block
|
||||||
|
from .line import Line
|
||||||
|
from .words import Word
|
||||||
|
from .utils import Vector3, Quaternion
|
||||||
|
|
||||||
|
from .exceptions import MachineInvalidAxis, MachineInvalidState
|
||||||
|
|
||||||
|
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
|
||||||
|
if invalid_axes:
|
||||||
|
raise MachineInvalidAxis("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)
|
||||||
|
|
||||||
|
def update(self, **coords):
|
||||||
|
for (k, v) in coords.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
# 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 MachineInvalidAxis("'%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):
|
||||||
|
if self.axes ^ other.axes:
|
||||||
|
raise MachineInvalidAxis("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):
|
||||||
|
if other.axes - self.axes:
|
||||||
|
raise MachineInvalidAxis("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)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vector(self):
|
||||||
|
return Vector3(self._value['X'], self._value['Y'], self._value['Z'])
|
||||||
|
|
||||||
|
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.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# 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 = '''
|
||||||
|
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:
|
||||||
|
# see modal_group in gcode.py module for details
|
||||||
|
def __init__(self):
|
||||||
|
self.modal_groups = defaultdict(lambda: None)
|
||||||
|
|
||||||
|
# Initialize
|
||||||
|
self.set_mode(*Line(self.default_mode).block.gcodes)
|
||||||
|
|
||||||
|
def set_mode(self, *gcode_list):
|
||||||
|
"""
|
||||||
|
Set machine mode from given gcodes (will not be processed)
|
||||||
|
: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:
|
||||||
|
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, not advisable
|
||||||
|
# (recommended to use self.set_mode(value) instead)
|
||||||
|
if not isinstance(value, GCode):
|
||||||
|
raise MachineInvalidState("invalid mode value: %r" % value)
|
||||||
|
if value.modal_group != MODAL_GROUP_MAP[key]:
|
||||||
|
raise MachineInvalidState("cannot set '%s' mode as %r, wrong group" % (key, value))
|
||||||
|
self.modal_groups[MODAL_GROUP_MAP[key]] = value.modal_copy()
|
||||||
|
else:
|
||||||
|
self.__dict__[key] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gcodes(self):
|
||||||
|
"""List of modal gcodes"""
|
||||||
|
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 gcode_list
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ' '.join(str(g) for g in self.gcodes)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<{class_name}: {gcodes}>".format(
|
||||||
|
class_name=self.__class__.__name__, gcodes=str(self)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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.mode = self.MODE_CLASS()
|
||||||
|
self.state = self.STATE_CLASS(axes=self.axes)
|
||||||
|
|
||||||
|
# Position type (with default axes the same as this machine)
|
||||||
|
units_mode = getattr(self.mode, 'units', None)
|
||||||
|
self.Position = type('Position', (Position,), {
|
||||||
|
'default_axes': self.axes,
|
||||||
|
'default_unit': units_mode.unit_id if units_mode else UNIT_METRIC,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 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 modal_gcode(self, modal_params):
|
||||||
|
|
||||||
|
if not modal_params:
|
||||||
|
return None
|
||||||
|
if self.mode.motion is None:
|
||||||
|
raise MachineInvalidState("unable to assign modal parameters when no motion mode is set")
|
||||||
|
params = copy(self.mode.motion.params) # dict
|
||||||
|
params.update(dict((w.letter, w) for w in modal_params)) # override retained modal parameters
|
||||||
|
(modal_gcodes, unasigned_words) = words2gcodes([self.mode.motion.word] + params.values())
|
||||||
|
if unasigned_words:
|
||||||
|
raise MachineInvalidState("modal parameters '%s' cannot be assigned when in mode: %r" % (
|
||||||
|
' '.join(str(x) for x in unasigned_words), self.mode
|
||||||
|
))
|
||||||
|
if modal_gcodes:
|
||||||
|
assert len(modal_gcodes) == 1, "more than 1 modal code found"
|
||||||
|
return modal_gcodes[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def block_modal_gcodes(self, block):
|
||||||
|
"""
|
||||||
|
Block's GCode list in current machine mode
|
||||||
|
:param block: Block instance
|
||||||
|
:return: list of gcodes, block.gcodes + <modal gcode, if there is one>
|
||||||
|
"""
|
||||||
|
assert isinstance(block, Block), "invalid parameter"
|
||||||
|
gcodes = copy(block.gcodes)
|
||||||
|
modal_gcode = self.modal_gcode(block.modal_params)
|
||||||
|
if modal_gcode:
|
||||||
|
gcodes.append(modal_gcode)
|
||||||
|
return sorted(gcodes)
|
||||||
|
|
||||||
|
def process_gcodes(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
|
||||||
|
"""
|
||||||
|
gcode_list = list(gcode_list) # make appendable
|
||||||
|
# Add modal gcode to list of given gcodes
|
||||||
|
modal_params = kwargs.get('modal_params', [])
|
||||||
|
if modal_params:
|
||||||
|
modal_gcode = self.modal_gcode(modal_params)
|
||||||
|
if modal_gcode:
|
||||||
|
gcode_list.append(modal_gcode)
|
||||||
|
|
||||||
|
for gcode in sorted(gcode_list):
|
||||||
|
gcode.process(self) # shifts ownership of what happens now to GCode class
|
||||||
|
|
||||||
|
# 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_gcodes(*block.gcodes, modal_params=block.modal_params)
|
||||||
|
|
||||||
|
def process_str(self, block_str):
|
||||||
|
line = Line(block_str)
|
||||||
|
self.process_block(line.block)
|
||||||
|
|
||||||
|
@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"""
|
||||||
|
if isinstance(self.mode.distance, GCodeIncrementalDistanceMode):
|
||||||
|
pos_delta = Position(axes=self.axes, **coords)
|
||||||
|
self.pos += pos_delta
|
||||||
|
else: # assumed: GCodeAbsoluteDistanceMode
|
||||||
|
new_pos = self.pos
|
||||||
|
new_pos.update(**coords) # only change given coordinates
|
||||||
|
self.pos = new_pos
|
485
src/pygcode/transform.py
Normal file
485
src/pygcode/transform.py
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
from math import sin, cos, tan, asin, acos, atan2, pi, sqrt, ceil
|
||||||
|
|
||||||
|
from .gcodes import GCodeLinearMove, GCodeRapidMove
|
||||||
|
from .gcodes import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW
|
||||||
|
from .gcodes import GCodePlaneSelect, GCodeSelectXYPlane, GCodeSelectYZPlane, GCodeSelectZXPlane
|
||||||
|
from .gcodes import GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode
|
||||||
|
from .gcodes import GCodeAbsoluteArcDistanceMode, GCodeIncrementalArcDistanceMode
|
||||||
|
from .gcodes import GCodeCannedCycle
|
||||||
|
from .gcodes import GCodeDrillingCyclePeck, GCodeDrillingCycleDwell, GCodeDrillingCycleChipBreaking
|
||||||
|
from .gcodes import GCodeCannedReturnMode, GCodeCannedCycleReturnLevel, GCodeCannedCycleReturnToR
|
||||||
|
from .gcodes import _gcodes_abs2rel
|
||||||
|
|
||||||
|
from .machine import Position
|
||||||
|
from .exceptions import GCodeParameterError
|
||||||
|
from .utils import Vector3, Quaternion, plane_projection
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Arcs (G2,G3) --> Linear Motion (G1) ====================
|
||||||
|
|
||||||
|
class ArcLinearizeMethod(object):
|
||||||
|
# Chord Phase Offest:
|
||||||
|
# False : each line will span an equal portion of the arc
|
||||||
|
# True : the first & last chord will span 1/2 the angular distance of all other chords
|
||||||
|
chord_phase_offset = False
|
||||||
|
|
||||||
|
def __init__(self, max_error, plane_normal,
|
||||||
|
arc_p_start, arc_p_end, arc_p_center,
|
||||||
|
arc_radius, arc_angle, helical_start, helical_end):
|
||||||
|
self.max_error = max_error
|
||||||
|
self.plane_normal = plane_normal
|
||||||
|
self.arc_p_start = arc_p_start
|
||||||
|
self.arc_p_end = arc_p_end
|
||||||
|
self.arc_p_center = arc_p_center
|
||||||
|
self.arc_radius = arc_radius
|
||||||
|
self.arc_angle = arc_angle
|
||||||
|
self.helical_start = helical_start
|
||||||
|
self.helical_end = helical_end
|
||||||
|
|
||||||
|
if self.max_error > self.arc_radius:
|
||||||
|
self.max_error = self.arc_radius
|
||||||
|
|
||||||
|
# Initializing
|
||||||
|
self._max_wedge_angle = None
|
||||||
|
self._wedge_count = None
|
||||||
|
self._wedge_angle = None
|
||||||
|
self._inner_radius = None
|
||||||
|
self._outer_radius = None
|
||||||
|
|
||||||
|
# Overridden Functions
|
||||||
|
def get_max_wedge_angle(self):
|
||||||
|
"""Calculate angular coverage of a single line reaching maximum allowable error"""
|
||||||
|
raise NotImplementedError("not overridden")
|
||||||
|
|
||||||
|
def get_inner_radius(self):
|
||||||
|
"""Radius each line is tangential to"""
|
||||||
|
# IMPORTANT: when overriding, calculate this using self.wedge_angle,
|
||||||
|
# (self.wedge_angle will almost always be < self.max_wedge_angle)
|
||||||
|
raise NotImplementedError("not overridden")
|
||||||
|
|
||||||
|
def get_outer_radius(self):
|
||||||
|
"""Radius from which each line forms a chord"""
|
||||||
|
# IMPORTANT: when overriding, calculate this using self.wedge_angle,
|
||||||
|
# (self.wedge_angle will almost always be < self.max_wedge_angle)
|
||||||
|
raise NotImplementedError("not overridden")
|
||||||
|
|
||||||
|
# Properties
|
||||||
|
@property
|
||||||
|
def max_wedge_angle(self):
|
||||||
|
if self._max_wedge_angle is None:
|
||||||
|
self._max_wedge_angle = self.get_max_wedge_angle()
|
||||||
|
return self._max_wedge_angle
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wedge_count(self):
|
||||||
|
"""
|
||||||
|
Number of full wedges covered across the arc.
|
||||||
|
NB: if there is phase offset, then the actual number of linearized lines
|
||||||
|
is this + 1, because the first and last are considered to be the
|
||||||
|
same 'wedge'.
|
||||||
|
"""
|
||||||
|
if self._wedge_count is None:
|
||||||
|
self._wedge_count = int(ceil(abs(self.arc_angle) / self.max_wedge_angle))
|
||||||
|
return self._wedge_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wedge_angle(self):
|
||||||
|
"""Angle each major chord stretches across the original arc"""
|
||||||
|
if self._wedge_angle is None:
|
||||||
|
self._wedge_angle = self.arc_angle / self.wedge_count
|
||||||
|
return self._wedge_angle
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner_radius(self):
|
||||||
|
if self._inner_radius is None:
|
||||||
|
self._inner_radius = self.get_inner_radius()
|
||||||
|
return self._inner_radius
|
||||||
|
|
||||||
|
@property
|
||||||
|
def outer_radius(self):
|
||||||
|
if self._outer_radius is None:
|
||||||
|
self._outer_radius = self.get_outer_radius()
|
||||||
|
return self._outer_radius
|
||||||
|
|
||||||
|
# Vertex Generator
|
||||||
|
def iter_vertices(self):
|
||||||
|
"""Yield absolute (<start vertex>, <end vertex>) for each line for the arc"""
|
||||||
|
start_vertex = self.arc_p_start - self.arc_p_center
|
||||||
|
outer_vertex = start_vertex.normalized() * self.outer_radius
|
||||||
|
d_helical = self.helical_end - self.helical_start
|
||||||
|
|
||||||
|
l_p_start = self.arc_p_center + start_vertex
|
||||||
|
l_start = l_p_start + self.helical_start
|
||||||
|
|
||||||
|
for i in range(self.wedge_count):
|
||||||
|
wedge_number = i + 1
|
||||||
|
# Current angle
|
||||||
|
cur_angle = self.wedge_angle * wedge_number
|
||||||
|
if self.chord_phase_offset:
|
||||||
|
cur_angle -= self.wedge_angle / 2.
|
||||||
|
elif wedge_number >= self.wedge_count:
|
||||||
|
break # stop 1 iteration short
|
||||||
|
# alow last arc to simply span across:
|
||||||
|
# <the end of the last line> -> <circle's end point>
|
||||||
|
|
||||||
|
# Next end point as projected on selected plane
|
||||||
|
q_end = Quaternion.new_rotate_axis(angle=cur_angle, axis=-self.plane_normal)
|
||||||
|
l_p_end = (q_end * outer_vertex) + self.arc_p_center
|
||||||
|
# += helical displacement (difference along plane's normal)
|
||||||
|
helical_displacement = self.helical_start + (d_helical * (cur_angle / self.arc_angle))
|
||||||
|
l_end = l_p_end + helical_displacement
|
||||||
|
|
||||||
|
yield (l_start, l_end)
|
||||||
|
|
||||||
|
# start of next line is the end of this one
|
||||||
|
l_start = l_end
|
||||||
|
|
||||||
|
# Last line always ends at the circle's end
|
||||||
|
yield (l_start, self.arc_p_end + self.helical_end)
|
||||||
|
|
||||||
|
|
||||||
|
class ArcLinearizeInside(ArcLinearizeMethod):
|
||||||
|
"""Start and end points of each line are on the original arc"""
|
||||||
|
# Attributes / Trade-offs:
|
||||||
|
# - Errors cause arc to be slightly smaller
|
||||||
|
# - pocket milling action will remove less material
|
||||||
|
# - perimeter milling action will remove more material
|
||||||
|
# - Each line is the same length
|
||||||
|
# - Simplest maths, easiest to explain & visually verify
|
||||||
|
|
||||||
|
chord_phase_offset = False
|
||||||
|
|
||||||
|
def get_max_wedge_angle(self):
|
||||||
|
"""Calculate angular coverage of a single line reaching maximum allowable error"""
|
||||||
|
return abs(2 * acos((self.arc_radius - self.max_error) / self.arc_radius))
|
||||||
|
|
||||||
|
def get_inner_radius(self):
|
||||||
|
"""Radius each line is tangential to"""
|
||||||
|
return abs(cos(self.wedge_angle / 2.) * self.arc_radius)
|
||||||
|
|
||||||
|
def get_outer_radius(self):
|
||||||
|
"""Radius from which each line forms a chord"""
|
||||||
|
return self.arc_radius
|
||||||
|
|
||||||
|
|
||||||
|
class ArcLinearizeOutside(ArcLinearizeMethod):
|
||||||
|
"""Mid-points of each line are on the original arc, first and last lines are 1/2 length"""
|
||||||
|
# Attributes / Trade-offs:
|
||||||
|
# - Errors cause arc to be slightly larger
|
||||||
|
# - pocket milling action will remove more material
|
||||||
|
# - perimeter milling action will remove less material
|
||||||
|
# - 1st and last lines are 1/2 length of the others
|
||||||
|
|
||||||
|
chord_phase_offset = True
|
||||||
|
|
||||||
|
def get_max_wedge_angle(self):
|
||||||
|
"""Calculate angular coverage of a single line reaching maximum allowable error"""
|
||||||
|
return abs(2 * acos(self.arc_radius / (self.arc_radius + self.max_error)))
|
||||||
|
|
||||||
|
def get_inner_radius(self):
|
||||||
|
"""Radius each line is tangential to"""
|
||||||
|
return self.arc_radius
|
||||||
|
|
||||||
|
def get_outer_radius(self):
|
||||||
|
"""Radius from which each line forms a chord"""
|
||||||
|
return abs(self.arc_radius / cos(self.wedge_angle / 2.))
|
||||||
|
|
||||||
|
|
||||||
|
class ArcLinearizeMid(ArcLinearizeMethod):
|
||||||
|
"""Lines cross original arc from tangent of arc radius - precision/2, until it reaches arc radius + precision/2"""
|
||||||
|
# Attributes / Trade-offs:
|
||||||
|
# - Closest to original arc (error distances are equal inside and outside the arc)
|
||||||
|
# - Most complex to calculate (but who cares, that's only done once)
|
||||||
|
# - default linearizing method as it's probably the best
|
||||||
|
|
||||||
|
chord_phase_offset = True
|
||||||
|
|
||||||
|
def get_max_wedge_angle(self):
|
||||||
|
"""Calculate angular coverage of a single line reaching maximum allowable error"""
|
||||||
|
d_radius = self.max_error / 2.
|
||||||
|
return abs(2. * acos((self.arc_radius - d_radius) / (self.arc_radius + d_radius)))
|
||||||
|
|
||||||
|
def get_inner_radius(self):
|
||||||
|
"""Radius each line is tangential to"""
|
||||||
|
d_radius = self.arc_radius * (tan(self.wedge_angle / 4.) ** 2)
|
||||||
|
return self.arc_radius - d_radius
|
||||||
|
|
||||||
|
def get_outer_radius(self):
|
||||||
|
"""Radius from which each line forms a chord"""
|
||||||
|
d_radius = self.arc_radius * (tan(self.wedge_angle / 4.) ** 2)
|
||||||
|
return self.arc_radius + d_radius
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_LA_METHOD = ArcLinearizeMid
|
||||||
|
DEFAULT_LA_PLANE = GCodeSelectXYPlane
|
||||||
|
DEFAULT_LA_DISTMODE = GCodeAbsoluteDistanceMode
|
||||||
|
DEFAULT_LA_ARCDISTMODE = GCodeIncrementalArcDistanceMode
|
||||||
|
|
||||||
|
def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None,
|
||||||
|
dist_mode=None, arc_dist_mode=None,
|
||||||
|
max_error=0.01, decimal_places=3):
|
||||||
|
"""
|
||||||
|
Convert a G2,G3 arc into a series of approsimation G1 codes
|
||||||
|
:param arc_gcode: arc gcode to approximate (GCodeArcMove)
|
||||||
|
:param start_pos: current machine position (Position)
|
||||||
|
:param plane: machine's active plane (GCodePlaneSelect)
|
||||||
|
:param method_class: method of linear approximation (ArcLinearizeMethod)
|
||||||
|
:param dist_mode: machine's distance mode (GCodeAbsoluteDistanceMode or GCodeIncrementalDistanceMode)
|
||||||
|
:param arc_dist_mode: machine's arc distance mode (GCodeAbsoluteArcDistanceMode or GCodeIncrementalArcDistanceMode)
|
||||||
|
:param max_error: maximum distance approximation arcs can stray from original arc (float)
|
||||||
|
:param decimal_places: number of decimal places gocde will be rounded to, used to mitigate risks of accumulated eror when in incremental distance mode (int)
|
||||||
|
"""
|
||||||
|
# set defaults
|
||||||
|
if method_class is None:
|
||||||
|
method_class = DEFAULT_LA_method_class
|
||||||
|
if plane is None:
|
||||||
|
plane = DEFAULT_LA_PLANE()
|
||||||
|
if dist_mode is None:
|
||||||
|
dist_mode = DEFAULT_LA_DISTMODE()
|
||||||
|
if arc_dist_mode is None:
|
||||||
|
arc_dist_mode = DEFAULT_LA_ARCDISTMODE()
|
||||||
|
|
||||||
|
# Parameter Type Assertions
|
||||||
|
assert isinstance(arc_gcode, GCodeArcMove), "bad arc_gcode type: %r" % arc_gcode
|
||||||
|
assert isinstance(start_pos, Position), "bad start_pos type: %r" % start_pos
|
||||||
|
assert isinstance(plane, GCodePlaneSelect), "bad plane type: %r" % plane
|
||||||
|
assert issubclass(method_class, ArcLinearizeMethod), "bad method_class type: %r" % method_class
|
||||||
|
assert isinstance(dist_mode, (GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode)), "bad dist_mode type: %r" % dist_mode
|
||||||
|
assert isinstance(arc_dist_mode, (GCodeAbsoluteArcDistanceMode, GCodeIncrementalArcDistanceMode)), "bad arc_dist_mode type: %r" % arc_dist_mode
|
||||||
|
assert max_error > 0, "max_error must be > 0"
|
||||||
|
|
||||||
|
# Arc Start
|
||||||
|
arc_start = start_pos.vector
|
||||||
|
# Arc End
|
||||||
|
if isinstance(dist_mode, GCodeAbsoluteDistanceMode):
|
||||||
|
# given coordinates override those already defined
|
||||||
|
arc_end_coords = dict(zip('xyz', arc_start.xyz))
|
||||||
|
arc_end_coords.update(arc_gcode.get_param_dict('XYZ', lc=True))
|
||||||
|
arc_end = Vector3(**arc_end_coords)
|
||||||
|
else:
|
||||||
|
# given coordinates are += to arc's start coords
|
||||||
|
arc_end = arc_start + Vector3(**arc_gcode.get_param_dict('XYZ', lc=True))
|
||||||
|
|
||||||
|
# Planar Projections
|
||||||
|
arc_p_start = plane_projection(arc_start, plane.normal)
|
||||||
|
arc_p_end = plane_projection(arc_end, plane.normal)
|
||||||
|
|
||||||
|
# Arc radius, calcualted one of 2 ways:
|
||||||
|
# - R: arc radius is provided
|
||||||
|
# - IJK: arc's center-point is given, errors mitigated
|
||||||
|
arc_gcode.assert_params()
|
||||||
|
if 'R' in arc_gcode.params:
|
||||||
|
# R: radius magnitude specified
|
||||||
|
if abs(arc_p_start - arc_p_end) < max_error:
|
||||||
|
raise GCodeParameterError(
|
||||||
|
"arc starts and finishes in the same spot; cannot "
|
||||||
|
"speculate where circle's center is: %r" % arc_gcode
|
||||||
|
)
|
||||||
|
|
||||||
|
arc_radius = abs(arc_gcode.R) # arc radius (magnitude)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# IJK: radius vertex specified
|
||||||
|
arc_center_ijk = dict((l, 0.) for l in 'IJK')
|
||||||
|
arc_center_ijk.update(arc_gcode.get_param_dict('IJK'))
|
||||||
|
arc_center_coords = dict(({'I':'x','J':'y','K':'z'}[k], v) for (k, v) in arc_center_ijk.items())
|
||||||
|
arc_center = Vector3(**arc_center_coords)
|
||||||
|
if isinstance(arc_dist_mode, GCodeIncrementalArcDistanceMode):
|
||||||
|
arc_center += start_pos.vector
|
||||||
|
|
||||||
|
# planar projection
|
||||||
|
arc_p_center = plane_projection(arc_center, plane.normal)
|
||||||
|
|
||||||
|
# Radii
|
||||||
|
r1 = arc_p_start - arc_p_center
|
||||||
|
r2 = arc_p_end - arc_p_center
|
||||||
|
|
||||||
|
# average the 2 radii to get the most accurate radius
|
||||||
|
arc_radius = (abs(r1) + abs(r2)) / 2.
|
||||||
|
|
||||||
|
# Find Circle's Center (given radius)
|
||||||
|
arc_span = arc_p_end - arc_p_start # vector spanning from start -> end
|
||||||
|
arc_span_mid = arc_span * 0.5 # arc_span's midpoint
|
||||||
|
if arc_radius < abs(arc_span_mid):
|
||||||
|
raise GCodeParameterError("circle cannot reach endpoint at this radius: %r" % arc_gcode)
|
||||||
|
# vector from arc_span midpoint -> circle's centre
|
||||||
|
radius_mid_vect = arc_span_mid.normalized().cross(plane.normal) * sqrt(arc_radius**2 - abs(arc_span_mid)**2)
|
||||||
|
|
||||||
|
if 'R' in arc_gcode.params:
|
||||||
|
# R: radius magnitude specified
|
||||||
|
if isinstance(arc_gcode, GCodeArcMoveCW) == (arc_gcode.R < 0):
|
||||||
|
arc_p_center = arc_p_start + arc_span_mid - radius_mid_vect
|
||||||
|
else:
|
||||||
|
arc_p_center = arc_p_start + arc_span_mid + radius_mid_vect
|
||||||
|
else:
|
||||||
|
# IJK: radius vertex specified
|
||||||
|
# arc_p_center is defined as per IJK params, this is an adjustment
|
||||||
|
arc_p_center_options = [
|
||||||
|
arc_p_start + arc_span_mid - radius_mid_vect,
|
||||||
|
arc_p_start + arc_span_mid + radius_mid_vect
|
||||||
|
]
|
||||||
|
if abs(arc_p_center_options[0] - arc_p_center) < abs(arc_p_center_options[1] - arc_p_center):
|
||||||
|
arc_p_center = arc_p_center_options[0]
|
||||||
|
else:
|
||||||
|
arc_p_center = arc_p_center_options[1]
|
||||||
|
|
||||||
|
# Arc's angle (first rotated back to xy plane)
|
||||||
|
xy_c2start = plane.quat * (arc_p_start - arc_p_center)
|
||||||
|
xy_c2end = plane.quat * (arc_p_end - arc_p_center)
|
||||||
|
(a1, a2) = (atan2(*xy_c2start.yx), atan2(*xy_c2end.yx))
|
||||||
|
if isinstance(arc_gcode, GCodeArcMoveCW):
|
||||||
|
arc_angle = (a1 - a2) % (2 * pi)
|
||||||
|
else:
|
||||||
|
arc_angle = -((a2 - a1) % (2 * pi))
|
||||||
|
|
||||||
|
# Helical interpolation
|
||||||
|
helical_start = plane.normal * arc_start.dot(plane.normal)
|
||||||
|
helical_end = plane.normal * arc_end.dot(plane.normal)
|
||||||
|
|
||||||
|
# Parameters determined above:
|
||||||
|
# - arc_p_start arc start point
|
||||||
|
# - arc_p_end arc end point
|
||||||
|
# - arc_p_center arc center
|
||||||
|
# - arc_angle angle between start & end (>0 is ccw, <0 is cw) (radians)
|
||||||
|
# - helical_start distance along plane.normal of arc start
|
||||||
|
# - helical_disp distance along plane.normal of arc end
|
||||||
|
|
||||||
|
method_class_params = {
|
||||||
|
'max_error': max_error,
|
||||||
|
'plane_normal': plane.normal,
|
||||||
|
'arc_p_start': arc_p_start,
|
||||||
|
'arc_p_end': arc_p_end,
|
||||||
|
'arc_p_center': arc_p_center,
|
||||||
|
'arc_radius': arc_radius,
|
||||||
|
'arc_angle': arc_angle,
|
||||||
|
'helical_start': helical_start,
|
||||||
|
'helical_end': helical_end,
|
||||||
|
}
|
||||||
|
method = method_class(**method_class_params)
|
||||||
|
|
||||||
|
# Iterate & yield each linear line (start, end) vertices
|
||||||
|
if isinstance(dist_mode, GCodeAbsoluteDistanceMode):
|
||||||
|
# Absolute coordinates
|
||||||
|
for line_vertices in method.iter_vertices():
|
||||||
|
(l_start, l_end) = line_vertices
|
||||||
|
yield GCodeLinearMove(**dict(zip('XYZ', l_end.xyz)))
|
||||||
|
else:
|
||||||
|
# Incremental coordinates (beware cumulative errors)
|
||||||
|
cur_pos = arc_start
|
||||||
|
for line_vertices in method.iter_vertices():
|
||||||
|
(l_start, l_end) = line_vertices
|
||||||
|
l_delta = l_end - cur_pos
|
||||||
|
|
||||||
|
# round delta coordinates (introduces errors)
|
||||||
|
for axis in 'xyz':
|
||||||
|
setattr(l_delta, axis, round(getattr(l_delta, axis), decimal_places))
|
||||||
|
yield GCodeLinearMove(**dict(zip('XYZ', l_delta.xyz)))
|
||||||
|
cur_pos += l_delta # mitigate errors by also adding them the accumulated cur_pos
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Un-Canning ====================
|
||||||
|
|
||||||
|
DEFAULT_SCC_PLANE = GCodeSelectXYPlane
|
||||||
|
DEFAULT_SCC_DISTMODE = GCodeAbsoluteDistanceMode
|
||||||
|
DEFAULT_SCC_RETRACTMODE = GCodeCannedCycleReturnLevel
|
||||||
|
|
||||||
|
def simplify_canned_cycle(canned_gcode, start_pos,
|
||||||
|
plane=None, dist_mode=None, retract_mode=None,
|
||||||
|
axes='XYZ'):
|
||||||
|
"""
|
||||||
|
Simplify canned cycle into it's basic linear components
|
||||||
|
:param canned_gcode: canned gcode to be simplified (GCodeCannedCycle)
|
||||||
|
:param start_pos: current machine position (Position)
|
||||||
|
:param plane: machine's active plane (GCodePlaneSelect)
|
||||||
|
:param dist_mode: machine's distance mode (GCodeAbsoluteDistanceMode or GCodeIncrementalDistanceMode)
|
||||||
|
:param axes: axes machine accepts (set)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# set defaults
|
||||||
|
if plane is None:
|
||||||
|
plane = DEFAULT_SCC_PLANE()
|
||||||
|
if dist_mode is None:
|
||||||
|
dist_mode = DEFAULT_SCC_DISTMODE()
|
||||||
|
if retract_mode is None:
|
||||||
|
retract_mode = DEFAULT_SCC_RETRACTMODE()
|
||||||
|
|
||||||
|
# Parameter Type Assertions
|
||||||
|
assert isinstance(canned_gcode, GCodeCannedCycle), "bad canned_gcode type: %r" % canned_gcode
|
||||||
|
assert isinstance(start_pos, Position), "bad start_pos type: %r" % start_pos
|
||||||
|
assert isinstance(plane, GCodePlaneSelect), "bad plane type: %r" % plane
|
||||||
|
assert isinstance(dist_mode, (GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode)), "bad dist_mode type: %r" % dist_mode
|
||||||
|
assert isinstance(retract_mode, GCodeCannedReturnMode), "bad retract_mode type: %r" % retract_mode
|
||||||
|
|
||||||
|
# TODO: implement for planes other than XY
|
||||||
|
if not isinstance(plane, GCodeSelectXYPlane):
|
||||||
|
raise NotImplementedError("simplifying canned cycles for planes other than X/Y has not been implemented")
|
||||||
|
|
||||||
|
@_gcodes_abs2rel(start_pos=start_pos, dist_mode=dist_mode, axes=axes)
|
||||||
|
def inner():
|
||||||
|
cycle_count = 1 if (canned_gcode.L is None) else canned_gcode.L
|
||||||
|
cur_hole_p_axis = start_pos.vector
|
||||||
|
for i in range(cycle_count):
|
||||||
|
# Calculate Depths
|
||||||
|
if isinstance(dist_mode, GCodeAbsoluteDistanceMode):
|
||||||
|
retract_depth = canned_gcode.R
|
||||||
|
drill_depth = canned_gcode.Z
|
||||||
|
cur_hole_p_axis = Vector3(x=canned_gcode.X, y=canned_gcode.Y)
|
||||||
|
else: # incremental
|
||||||
|
retract_depth = start_pos.Z + canned_gcode.R
|
||||||
|
drill_depth = retract_depth + canned_gcode.Z
|
||||||
|
cur_hole_p_axis += Vector3(x=canned_gcode.X, y=canned_gcode.Y)
|
||||||
|
|
||||||
|
if retract_depth < drill_depth:
|
||||||
|
raise NotImplementedError("drilling upward is not supported")
|
||||||
|
|
||||||
|
if isinstance(retract_mode, GCodeCannedCycleReturnToR):
|
||||||
|
final_depth = retract_depth
|
||||||
|
else:
|
||||||
|
final_depth = start_pos.Z
|
||||||
|
|
||||||
|
# Move above hole (height of retract_depth)
|
||||||
|
if retract_depth > start_pos.Z:
|
||||||
|
yield GCodeRapidMove(Z=retract_depth)
|
||||||
|
yield GCodeRapidMove(X=cur_hole_p_axis.x, Y=cur_hole_p_axis.y)
|
||||||
|
if retract_depth < start_pos.Z:
|
||||||
|
yield GCodeRapidMove(Z=retract_depth)
|
||||||
|
|
||||||
|
# Drill hole
|
||||||
|
delta = drill_depth - retract_depth # full depth
|
||||||
|
if isinstance(canned_gcode, (GCodeDrillingCyclePeck, GCodeDrillingCycleChipBreaking)):
|
||||||
|
delta = -abs(canned_gcode.Q)
|
||||||
|
|
||||||
|
cur_depth = retract_depth
|
||||||
|
last_depth = cur_depth
|
||||||
|
while True:
|
||||||
|
# Determine new depth
|
||||||
|
cur_depth += delta
|
||||||
|
if cur_depth < drill_depth:
|
||||||
|
cur_depth = drill_depth
|
||||||
|
|
||||||
|
# Rapid to just above, then slowly drill through delta
|
||||||
|
just_above_base = last_depth + 0.1
|
||||||
|
if just_above_base < retract_depth:
|
||||||
|
yield GCodeRapidMove(Z=just_above_base)
|
||||||
|
yield GCodeLinearMove(Z=cur_depth)
|
||||||
|
if cur_depth <= drill_depth:
|
||||||
|
break # loop stops at the bottom of the hole
|
||||||
|
else:
|
||||||
|
# back up
|
||||||
|
if isinstance(canned_gcode, GCodeDrillingCycleChipBreaking):
|
||||||
|
# retract "a bit"
|
||||||
|
yield GCodeRapidMove(Z=cur_depth + 0.5) # TODO: configurable retraction
|
||||||
|
else:
|
||||||
|
# default behaviour: GCodeDrillingCyclePeck
|
||||||
|
yield GCodeRapidMove(Z=retract_depth)
|
||||||
|
|
||||||
|
last_depth = cur_depth
|
||||||
|
|
||||||
|
# Dwell
|
||||||
|
if isinstance(canned_gcode, GCodeDrillingCycleDwell):
|
||||||
|
yield GCodeDwell(P=0.5) # TODO: configurable pause
|
||||||
|
|
||||||
|
# Return
|
||||||
|
yield GCodeRapidMove(Z=final_depth)
|
||||||
|
|
||||||
|
return inner()
|
96
src/pygcode/utils.py
Normal file
96
src/pygcode/utils.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import sys
|
||||||
|
from copy import copy, deepcopy
|
||||||
|
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
from euclid import Vector3, Quaternion
|
||||||
|
else:
|
||||||
|
from euclid3 import Vector3, Quaternion
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Geometric Utilities ====================
|
||||||
|
def quat2align(to_align, with_this, normalize=True):
|
||||||
|
"""
|
||||||
|
Calculate Quaternion that will rotate a given vector to align with another
|
||||||
|
can accumulate with perpendicular alignemnt vectors like so:
|
||||||
|
(x_axis, z_axis) = (Vector3(1, 0, 0), Vector3(0, 0, 1))
|
||||||
|
q1 = quat2align(v1, z_axis)
|
||||||
|
q2 = quat2align(q1 * v2, x_axis)
|
||||||
|
# assuming v1 is perpendicular to v2
|
||||||
|
q3 = q2 * q1 # application of q3 to any vector will re-orient it to the
|
||||||
|
# coordinate system defined by (v1,v2), as (z,x) respectively.
|
||||||
|
:param to_align: Vector3 instance to be rotated
|
||||||
|
:param with_this: Vector3 instance as target for alignment
|
||||||
|
:result: Quaternion such that: q * to_align == with_this
|
||||||
|
"""
|
||||||
|
# Normalize Vectors
|
||||||
|
if normalize:
|
||||||
|
to_align = to_align.normalized()
|
||||||
|
with_this = with_this.normalized()
|
||||||
|
# Calculate Quaternion
|
||||||
|
return Quaternion.new_rotate_axis(
|
||||||
|
angle=to_align.angle(with_this),
|
||||||
|
axis=to_align.cross(with_this),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def quat2coord_system(origin1, origin2, align1, align2):
|
||||||
|
"""
|
||||||
|
Calculate Quaternion to apply to any vector to re-orientate it to another
|
||||||
|
(target) coordinate system.
|
||||||
|
(note: both origin and align coordinate systems must use right-hand-rule)
|
||||||
|
:param origin1: origin(1|2) are perpendicular vectors in the original coordinate system
|
||||||
|
:param origin2: see origin1
|
||||||
|
:param align1: align(1|2) are 2 perpendicular vectors in the target coordinate system
|
||||||
|
:param align2: see align1
|
||||||
|
:return: Quaternion such that q * origin1 = align1, and q * origin2 = align2
|
||||||
|
"""
|
||||||
|
# Normalize Vectors
|
||||||
|
origin1 = origin1.normalized()
|
||||||
|
origin2 = origin2.normalized()
|
||||||
|
align1 = align1.normalized()
|
||||||
|
align2 = align2.normalized()
|
||||||
|
# Calculate Quaternion
|
||||||
|
q1 = quat2align(origin1, align1, normalize=False)
|
||||||
|
q2 = quat2align(q1 * origin2, align2, normalize=False)
|
||||||
|
return q2 * q1
|
||||||
|
|
||||||
|
|
||||||
|
def plane_projection(vect, normal):
|
||||||
|
"""
|
||||||
|
Project vect to a plane represented by normal
|
||||||
|
:param vect: vector to be projected (Vector3)
|
||||||
|
:param normal: normal of plane to project on to (Vector3)
|
||||||
|
:return: vect projected onto plane represented by normal
|
||||||
|
"""
|
||||||
|
# ref: https://en.wikipedia.org/wiki/Vector_projection
|
||||||
|
n = normal.normalized()
|
||||||
|
return vect - (n * vect.dot(n))
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== GCode Utilities ====================
|
||||||
|
def omit_redundant_modes(gcode_iter):
|
||||||
|
"""
|
||||||
|
Replace redundant machine motion modes with whitespace,
|
||||||
|
:param gcode_iter: iterable to return with modifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .machine import Machine, Mode
|
||||||
|
from .gcodes import MODAL_GROUP_MAP
|
||||||
|
class NullModeMachine(Machine):
|
||||||
|
MODE_CLASS = type('NullMode', (Mode,), {'default_mode': ''})
|
||||||
|
m = NullModeMachine()
|
||||||
|
|
||||||
|
for g in gcode_iter:
|
||||||
|
if (g.modal_group is not None) and (m.mode.modal_groups[g.modal_group] is not None):
|
||||||
|
# g-code has a modal groups, and the machine's mode
|
||||||
|
# (of the same modal group) is not None
|
||||||
|
if m.mode.modal_groups[g.modal_group].word == g.word:
|
||||||
|
# machine's mode & g-code's mode match (no machine change)
|
||||||
|
if g.modal_group == MODAL_GROUP_MAP['motion']:
|
||||||
|
# finally: g-code sets a motion mode in the machine
|
||||||
|
g = copy(g) # duplicate gcode object
|
||||||
|
# stop redundant g-code word from being printed
|
||||||
|
g._whitespace_prefix = True
|
||||||
|
|
||||||
|
m.process_gcodes(g)
|
||||||
|
yield g
|
330
src/pygcode/words.py
Normal file
330
src/pygcode/words.py
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
import re
|
||||||
|
import itertools
|
||||||
|
import six
|
||||||
|
|
||||||
|
from .exceptions import GCodeBlockFormatError, GCodeWordStrError
|
||||||
|
|
||||||
|
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+')
|
||||||
|
REGEX_CODE = re.compile(r'^\d+(\.\d)?') # similar
|
||||||
|
|
||||||
|
# Value cleaning functions
|
||||||
|
def _clean_codestr(value):
|
||||||
|
if value < 10:
|
||||||
|
return "0%g" % value
|
||||||
|
return "%g" % value
|
||||||
|
|
||||||
|
CLEAN_NONE = lambda v: v
|
||||||
|
CLEAN_FLOAT = lambda v: "{0:g}".format(round(v, 3))
|
||||||
|
CLEAN_CODE = _clean_codestr
|
||||||
|
CLEAN_INT = lambda v: "%g" % v
|
||||||
|
|
||||||
|
WORD_MAP = {
|
||||||
|
# Descriptions copied from wikipedia:
|
||||||
|
# https://en.wikipedia.org/wiki/G-code#Letter_addresses
|
||||||
|
|
||||||
|
# Rotational Axes
|
||||||
|
'A': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Absolute or incremental position of A axis (rotational axis around X axis)",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
'B': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Absolute or incremental position of B axis (rotational axis around Y axis)",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
'C': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Absolute or incremental position of C axis (rotational axis around Z axis)",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
'D': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'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.",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
# Feed Rates
|
||||||
|
'E': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Precision feedrate for threading on lathes",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
'F': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Feedrate",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
# G-Codes
|
||||||
|
'G': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_CODE,
|
||||||
|
'description': "Address for preparatory commands",
|
||||||
|
'clean_value': CLEAN_CODE,
|
||||||
|
},
|
||||||
|
# Tool Offsets
|
||||||
|
'H': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Defines tool length offset; Incremental axis corresponding to C axis (e.g., on a turn-mill)",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
# Arc radius center coords
|
||||||
|
'I': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Defines arc center in X axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
'J': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Defines arc center in Y axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
'K': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'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.",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
# Loop Count
|
||||||
|
'L': {
|
||||||
|
'class': int,
|
||||||
|
'value_regex': REGEX_POSITIVEINT,
|
||||||
|
'description': "Fixed cycle loop count; Specification of what register to edit using G10",
|
||||||
|
'clean_value': CLEAN_INT,
|
||||||
|
},
|
||||||
|
# Miscellaneous Function
|
||||||
|
'M': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_CODE,
|
||||||
|
'description': "Miscellaneous function",
|
||||||
|
'clean_value': CLEAN_CODE,
|
||||||
|
},
|
||||||
|
# Line Number
|
||||||
|
'N': {
|
||||||
|
'class': int,
|
||||||
|
'value_regex': REGEX_POSITIVEINT,
|
||||||
|
'description': "Line (block) number in program; System parameter number to change using G10",
|
||||||
|
'clean_value': CLEAN_INT,
|
||||||
|
},
|
||||||
|
# Program Name
|
||||||
|
'O': {
|
||||||
|
'class': str,
|
||||||
|
'value_regex': re.compile(r'^.+$'), # all the way to the end
|
||||||
|
'description': "Program name",
|
||||||
|
'clean_value': CLEAN_NONE,
|
||||||
|
},
|
||||||
|
# Parameter (arbitrary parameter)
|
||||||
|
'P': {
|
||||||
|
'class': float, # parameter is often an integer, but can be a float
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Serves as parameter address for various G and M codes",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
# Peck increment
|
||||||
|
'Q': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Depth to increase on each peck; Peck increment in canned cycles",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
# Arc Radius
|
||||||
|
'R': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Defines size of arc radius, or defines retract height in milling canned cycles",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
# Spindle speed
|
||||||
|
'S': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Defines speed, either spindle speed or surface speed depending on mode",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
# Tool Selecton
|
||||||
|
'T': {
|
||||||
|
'class': str,
|
||||||
|
'value_regex': REGEX_POSITIVEINT, # tool string may have leading '0's, but is effectively an index (integer)
|
||||||
|
'description': "Tool selection",
|
||||||
|
'clean_value': CLEAN_NONE,
|
||||||
|
},
|
||||||
|
# Incremental axes
|
||||||
|
'U': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'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').",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
'V': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Incremental axis corresponding to Y axis",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
'W': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Incremental axis corresponding to Z axis (typically only lathe group A controls)",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
# Linear Axes
|
||||||
|
'X': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Absolute or incremental position of X axis.",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
'Y': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Absolute or incremental position of Y axis.",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
'Z': {
|
||||||
|
'class': float,
|
||||||
|
'value_regex': REGEX_FLOAT,
|
||||||
|
'description': "Absolute or incremental position of Z axis.",
|
||||||
|
'clean_value': CLEAN_FLOAT,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Word(object):
|
||||||
|
def __init__(self, *args):
|
||||||
|
if len(args) not in (1, 2):
|
||||||
|
raise AssertionError("input arguments either: (letter, value) or (word_str)")
|
||||||
|
if len(args) == 2:
|
||||||
|
# Word('G', 90)
|
||||||
|
(letter, value) = args
|
||||||
|
else:
|
||||||
|
# Word('G90')
|
||||||
|
letter = args[0][0] # first letter
|
||||||
|
value = args[0][1:] # rest of string
|
||||||
|
letter = letter.upper()
|
||||||
|
|
||||||
|
self._value_class = WORD_MAP[letter]['class']
|
||||||
|
self._value_clean = WORD_MAP[letter]['clean_value']
|
||||||
|
|
||||||
|
self.letter = letter
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{letter}{value}".format(
|
||||||
|
letter=self.letter,
|
||||||
|
value=self.value_str,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<{class_name}: {string}>".format(
|
||||||
|
class_name=self.__class__.__name__,
|
||||||
|
string=str(self),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sorting
|
||||||
|
def __lt__(self, other):
|
||||||
|
return (self.letter, self.value) < (other.letter, other.value)
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
return (self.letter, self.value) > (other.letter, other.value)
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return (self.letter, self.value) <= (other.letter, other.value)
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return (self.letter, self.value) >= (other.letter, other.value)
|
||||||
|
|
||||||
|
# Equality
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, six.string_types):
|
||||||
|
other = str2word(other)
|
||||||
|
return (self.letter == other.letter) and (self.value == other.value)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
# Hashing
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.letter, self.value))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value_str(self):
|
||||||
|
"""Clean string representation, for consistent file output"""
|
||||||
|
return self._value_clean(self.value)
|
||||||
|
|
||||||
|
# Value Properties
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, new_value):
|
||||||
|
self._value = self._value_class(new_value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
return "%s: %s" % (self.letter, WORD_MAP[self.letter]['description'])
|
||||||
|
|
||||||
|
|
||||||
|
def text2words(block_text):
|
||||||
|
"""
|
||||||
|
Iterate through block text yielding Word instances
|
||||||
|
:param block_text: text for given block with comments removed
|
||||||
|
"""
|
||||||
|
next_word = re.compile(r'^.*?(?P<letter>[%s])' % ''.join(WORD_MAP.keys()), re.IGNORECASE)
|
||||||
|
|
||||||
|
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 GCodeWordStrError("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 GCodeWordStrError("block code remaining '%s'" % remainder)
|
||||||
|
|
||||||
|
|
||||||
|
def str2word(word_str):
|
||||||
|
words = list(text2words(word_str))
|
||||||
|
if words:
|
||||||
|
if len(words) > 1:
|
||||||
|
raise GCodeWordStrError("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)
|
||||||
|
)
|
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
|
||||||
|
|
24
tests/test_file.py
Normal file
24
tests/test_file.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
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.file import GCodeFile, GCodeParser
|
||||||
|
|
||||||
|
class GCodeParserTest(unittest.TestCase):
|
||||||
|
FILENAME = 'test-files/vertical-slot.ngc'
|
||||||
|
|
||||||
|
def test_parser(self):
|
||||||
|
parser = GCodeParser(self.FILENAME)
|
||||||
|
# count lines
|
||||||
|
line_count = 0
|
||||||
|
for line in parser.iterlines():
|
||||||
|
line_count += 1
|
||||||
|
self.assertEqual(line_count, 26)
|
||||||
|
parser.close()
|
193
tests/test_gcodes.py
Normal file
193
tests/test_gcodes.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import inspect
|
||||||
|
import re
|
||||||
|
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 import gcodes
|
||||||
|
from pygcode import words
|
||||||
|
from pygcode import machine
|
||||||
|
|
||||||
|
from pygcode.exceptions import GCodeWordStrError
|
||||||
|
|
||||||
|
class GCodeWordMappingTests(unittest.TestCase):
|
||||||
|
def test_word_map_integrity(self):
|
||||||
|
|
||||||
|
gcodes.build_maps()
|
||||||
|
for (word_maches, fn_class) in gcodes._gcode_function_list:
|
||||||
|
for (word, key_class) in gcodes._gcode_word_map.items():
|
||||||
|
# Verify that no mapped word will yield a True result
|
||||||
|
# from any of the 'word_maches' functions
|
||||||
|
self.assertFalse(
|
||||||
|
word_maches(word),
|
||||||
|
"conflict with %s and %s" % (fn_class, key_class)
|
||||||
|
)
|
||||||
|
|
||||||
|
class GCodeModalGroupTests(unittest.TestCase):
|
||||||
|
def test_modal_groups(self):
|
||||||
|
# Modal groups taken (and slightly modified) from LinuxCNC documentation:
|
||||||
|
# link: http://linuxcnc.org/docs/html/gcode/overview.html#_modal_groups
|
||||||
|
table_rows = ''
|
||||||
|
# Table 5. G-Code Modal Groups
|
||||||
|
# MODAL GROUP MEANING MEMBER WORDS
|
||||||
|
table_rows += '''
|
||||||
|
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.2,G38.3,G38.4
|
||||||
|
Motion (Group 1) G38.5,G73,G76,G80,G81,G82,G83,G85,G89
|
||||||
|
Plane selection (Group 2) G17, G18, G19, G17.1, G18.1, G19.1
|
||||||
|
Distance Mode (Group 3) G90, G91
|
||||||
|
Arc IJK Distance Mode (Group 4) G90.1, G91.1
|
||||||
|
Feed Rate Mode (Group 5) G93, G94, G95
|
||||||
|
Units (Group 6) G20, G21
|
||||||
|
Cutter Diameter Compensation (Group 7) G40, G41, G42, G41.1, G42.1
|
||||||
|
Tool Length Offset (Group 8) G43, G43.1, G49
|
||||||
|
Canned Cycles Return Mode (Group 10) G98
|
||||||
|
Coordinate System (Group 12) G54,G55,G56,G57,G58,G59,G59.1,G59.2,G59.3
|
||||||
|
Control Mode (Group 13) G61, G61.1, G64
|
||||||
|
Spindle Speed Mode (Group 14) G96, G97
|
||||||
|
Lathe Diameter Mode (Group 15) G7,G8
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Table 6. M-Code Modal Groups
|
||||||
|
# MODAL GROUP MEANING MEMBER WORDS
|
||||||
|
table_rows += re.sub(r'\(Group (\d+)\)', r'(Group 10\1)', '''
|
||||||
|
Stopping (Group 4) M0, M1, M2, M30, M60
|
||||||
|
Spindle (Group 7) M3, M4, M5
|
||||||
|
Coolant (Group 8) M7, M8, M9
|
||||||
|
Override Switches (Group 9) M48, M49
|
||||||
|
''') # groups += 100 (to distinguish "M" GCodes from "G" GCodes)
|
||||||
|
|
||||||
|
for row in table_rows.split('\n'):
|
||||||
|
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.text2words(word_str))[0]
|
||||||
|
gcode_class = gcodes.word_gcode_class(word)
|
||||||
|
# GCode class found for each word in the table
|
||||||
|
self.assertIsNotNone(gcode_class)
|
||||||
|
# GCode's modal group equals that defined in the table
|
||||||
|
expected_group = int(match.group('group'))
|
||||||
|
if expected_group == 0:
|
||||||
|
self.assertIsNone(
|
||||||
|
gcode_class.modal_group,
|
||||||
|
"%s modal_group: %s is not None" % (gcode_class, gcode_class.modal_group)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertEqual(
|
||||||
|
gcode_class.modal_group, expected_group,
|
||||||
|
"%s != %s (%r)" % (gcode_class.modal_group, expected_group, word)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Words2GCodesTests(unittest.TestCase):
|
||||||
|
def test_stuff(self): # FIXME: function name
|
||||||
|
line = 'G1 X82.6892 Y-38.6339 F1500'
|
||||||
|
word_list = list(words.text2words(line))
|
||||||
|
result = gcodes.words2gcodes(word_list)
|
||||||
|
# result form
|
||||||
|
self.assertIsInstance(result, tuple)
|
||||||
|
self.assertEqual(len(result), 2)
|
||||||
|
# result content
|
||||||
|
(gcode_list, unused_words) = result
|
||||||
|
self.assertEqual(len(gcode_list), 2)
|
||||||
|
self.assertEqual(unused_words, [])
|
||||||
|
# Parsed GCodes
|
||||||
|
# G1
|
||||||
|
self.assertEqual(gcode_list[0].word, words.Word('G', 1))
|
||||||
|
self.assertEqual(gcode_list[0].X, 82.6892)
|
||||||
|
self.assertEqual(gcode_list[0].Y, -38.6339)
|
||||||
|
# F1500
|
||||||
|
self.assertEqual(gcode_list[1].word, words.Word('F', 1500))
|
||||||
|
|
||||||
|
|
||||||
|
class Text2GCodesTests(unittest.TestCase):
|
||||||
|
def test_basic(self):
|
||||||
|
gcs = gcodes.text2gcodes('G1 X1 Y2 G90')
|
||||||
|
self.assertEqual(len(gcs), 2)
|
||||||
|
# G1 X1 Y2
|
||||||
|
self.assertEqual(gcs[0].word, words.Word('G', 1))
|
||||||
|
self.assertEqual(gcs[0].X, 1)
|
||||||
|
self.assertEqual(gcs[0].Y, 2)
|
||||||
|
# G90
|
||||||
|
self.assertEqual(gcs[1].word, words.Word('G', 90))
|
||||||
|
|
||||||
|
def test_modal_params(self):
|
||||||
|
with self.assertRaises(GCodeWordStrError):
|
||||||
|
gcodes.text2gcodes('X1 Y2')
|
||||||
|
|
||||||
|
|
||||||
|
class GCodeSplitTests(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_split(self):
|
||||||
|
g_list = gcodes.text2gcodes('G91 S1000 G1 X1 Y2 M3')
|
||||||
|
split = gcodes.split_gcodes(g_list, gcodes.GCodeStartSpindle)
|
||||||
|
self.assertEqual([len(x) for x in split], [1, 1, 2])
|
||||||
|
self.assertTrue(any(isinstance(g, gcodes.GCodeSpindleSpeed) for g in split[0]))
|
||||||
|
self.assertTrue(isinstance(split[1][0], gcodes.GCodeStartSpindle))
|
||||||
|
self.assertTrue(any(isinstance(g, gcodes.GCodeDistanceMode) for g in split[2]))
|
||||||
|
self.assertTrue(any(isinstance(g, gcodes.GCodeMotion) for g in split[2]))
|
||||||
|
|
||||||
|
def test_split_unsorted(self):
|
||||||
|
g_list = gcodes.text2gcodes('G91 G1 X1 Y2 M3 S1000')
|
||||||
|
split = gcodes.split_gcodes(g_list, gcodes.GCodeStartSpindle, sort_list=False)
|
||||||
|
self.assertEqual([len(x) for x in split], [2, 1, 1])
|
||||||
|
self.assertTrue(any(isinstance(g, gcodes.GCodeDistanceMode) for g in split[0]))
|
||||||
|
self.assertTrue(any(isinstance(g, gcodes.GCodeMotion) for g in split[0]))
|
||||||
|
self.assertTrue(isinstance(split[1][0], gcodes.GCodeStartSpindle))
|
||||||
|
self.assertTrue(any(isinstance(g, gcodes.GCodeSpindleSpeed) for g in split[2]))
|
||||||
|
|
||||||
|
|
||||||
|
class GCodeAbsoluteToRelativeDecoratorTests(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_gcodes_abs2rel(self):
|
||||||
|
# setup gcode testlist
|
||||||
|
L = gcodes.GCodeLinearMove
|
||||||
|
R = gcodes.GCodeRapidMove
|
||||||
|
args = lambda x, y, z: dict(a for a in zip('XYZ', [x,y,z]) if a[1] is not None)
|
||||||
|
gcode_list = [
|
||||||
|
# GCode instances Expected incremental output
|
||||||
|
(L(**args(0, 0, 0)), L(**args(-10, -20, -30))),
|
||||||
|
(L(**args(1, 2, 0)), L(**args(1, 2, None))),
|
||||||
|
(L(**args(3, 4, 0)), L(**args(2, 2, None))),
|
||||||
|
(R(**args(1, 2, 0)), R(**args(-2, -2, None))),
|
||||||
|
(R(**args(3, 4, 0)), R(**args(2, 2, None))),
|
||||||
|
(L(**args(3, 4, 0)), None),
|
||||||
|
(L(**args(3, 4, 8)), L(**args(None, None, 8))),
|
||||||
|
]
|
||||||
|
|
||||||
|
m = machine.Machine()
|
||||||
|
|
||||||
|
# Incremental Output
|
||||||
|
m.set_mode(gcodes.GCodeAbsoluteDistanceMode())
|
||||||
|
m.move_to(X=10, Y=20, Z=30) # initial position (absolute)
|
||||||
|
m.set_mode(gcodes.GCodeIncrementalDistanceMode())
|
||||||
|
|
||||||
|
@gcodes._gcodes_abs2rel(m.pos, dist_mode=m.mode.distance, axes=m.axes)
|
||||||
|
def expecting_rel():
|
||||||
|
return [g[0] for g in gcode_list]
|
||||||
|
|
||||||
|
trimmed_expecting_list = [x[1] for x in gcode_list if x[1] is not None]
|
||||||
|
for (i, g) in enumerate(expecting_rel()):
|
||||||
|
expected = trimmed_expecting_list[i]
|
||||||
|
self.assertEqual(type(g), type(expected))
|
||||||
|
self.assertEqual(g.word, expected.word)
|
||||||
|
self.assertEqual(g.params, expected.params)
|
||||||
|
|
||||||
|
# Absolute Output
|
||||||
|
m.set_mode(gcodes.GCodeAbsoluteDistanceMode())
|
||||||
|
m.move_to(X=10, Y=20, Z=30) # initial position
|
||||||
|
|
||||||
|
@gcodes._gcodes_abs2rel(m.pos, dist_mode=m.mode.distance, axes=m.axes)
|
||||||
|
def expecting_abs():
|
||||||
|
return [g[0] for g in gcode_list]
|
||||||
|
|
||||||
|
for (i, g) in enumerate(expecting_abs()):
|
||||||
|
expected = gcode_list[i][0] # expecting passthrough
|
||||||
|
self.assertEqual(type(g), type(expected))
|
||||||
|
self.assertEqual(g.word, expected.word)
|
||||||
|
self.assertEqual(g.params, expected.params)
|
29
tests/test_line.py
Normal file
29
tests/test_line.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
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.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)
|
123
tests/test_machine.py
Normal file
123
tests/test_machine.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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
|
||||||
|
from pygcode.exceptions import MachineInvalidAxis
|
||||||
|
|
||||||
|
|
||||||
|
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(MachineInvalidAxis):
|
||||||
|
p1 + p3 # mismatched axes
|
||||||
|
with self.assertRaises(MachineInvalidAxis):
|
||||||
|
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(MachineInvalidAxis):
|
||||||
|
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
|
33
tests/test_utils.py
Normal file
33
tests/test_utils.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import unittest
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Add relative pygcode to path
|
||||||
|
from testutils import add_pygcode_to_path, str_lines
|
||||||
|
add_pygcode_to_path()
|
||||||
|
|
||||||
|
# Units under test
|
||||||
|
from pygcode.utils import omit_redundant_modes
|
||||||
|
from pygcode import text2gcodes, Line
|
||||||
|
|
||||||
|
class UtilityTests(unittest.TestCase):
|
||||||
|
def test_omit_redundant_modes(self):
|
||||||
|
lines = [
|
||||||
|
Line(line_str)
|
||||||
|
for line_str in re.split(r'\s*\n\s*', '''
|
||||||
|
g1 x0 y0 ; yes
|
||||||
|
g1 x10 y-20 ; no
|
||||||
|
g0 x-3 y2 ; yes
|
||||||
|
g0 x0 y0 ; no
|
||||||
|
g0 x1 y1 ; no
|
||||||
|
g1 x20 y20 z5 ; yes
|
||||||
|
''')
|
||||||
|
if line_str
|
||||||
|
]
|
||||||
|
gcodes = [l.gcodes[0] for l in lines]
|
||||||
|
comments = [l.comment for l in lines]
|
||||||
|
for (i, g) in enumerate(omit_redundant_modes(gcodes)):
|
||||||
|
comment = comments[i].text if comments[i] else None
|
||||||
|
if comment == 'no':
|
||||||
|
self.assertIsNotNone(re.search(r'^\s', str(g)))
|
||||||
|
elif comment == 'yes':
|
||||||
|
self.assertIsNone(re.search(r'^\s', str(g)))
|
74
tests/test_words.py
Normal file
74
tests/test_words.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
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 import words
|
||||||
|
|
||||||
|
|
||||||
|
class WordIterTests(unittest.TestCase):
|
||||||
|
def test_iter1(self):
|
||||||
|
block_str = 'G01 Z-0.5 F100'
|
||||||
|
w = list(words.text2words(block_str))
|
||||||
|
# word length
|
||||||
|
self.assertEqual(len(w), 3)
|
||||||
|
# word values
|
||||||
|
self.assertEqual(w[0], words.Word('G', 1))
|
||||||
|
self.assertEqual(w[1], words.Word('Z', -0.5))
|
||||||
|
self.assertEqual(w[2], words.Word('F', 100))
|
||||||
|
|
||||||
|
def test_iter2(self):
|
||||||
|
block_str = 'G02 X10.75 Y47.44 I-0.11 J-1.26 F70'
|
||||||
|
w = list(words.text2words(block_str))
|
||||||
|
# word length
|
||||||
|
self.assertEqual(len(w), 6)
|
||||||
|
# word values
|
||||||
|
self.assertEqual([w[0].letter, w[0].value], ['G', 2])
|
||||||
|
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])
|
||||||
|
|
||||||
|
|
||||||
|
class WordValueMatchTests(unittest.TestCase):
|
||||||
|
|
||||||
|
def regex_assertions(self, regex, positive_list, negative_list):
|
||||||
|
# Assert all elements of positive_list match regex
|
||||||
|
for (value_str, expected_match) in positive_list:
|
||||||
|
match = regex.search(value_str)
|
||||||
|
self.assertIsNotNone(match, "failed to match '%s'" % value_str)
|
||||||
|
self.assertEqual(match.group(), expected_match)
|
||||||
|
|
||||||
|
# Asesrt all elements of negative_list do not match regex
|
||||||
|
for value_str in negative_list:
|
||||||
|
match = regex.search(value_str)
|
||||||
|
self.assertIsNone(match, "matched for '%s'" % value_str)
|
||||||
|
|
||||||
|
def test_float(self):
|
||||||
|
self.regex_assertions(
|
||||||
|
regex=words.REGEX_FLOAT,
|
||||||
|
positive_list=[
|
||||||
|
('1.2', '1.2'), ('1', '1'), ('200', '200'), ('0092', '0092'),
|
||||||
|
('1.', '1.'), ('.2', '.2'), ('-1.234', '-1.234'),
|
||||||
|
('-1.', '-1.'), ('-.289', '-.289'),
|
||||||
|
# error cases (only detectable in gcode context)
|
||||||
|
('1.2e3', '1.2'),
|
||||||
|
],
|
||||||
|
negative_list=['.', ' 1.2']
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_code(self):
|
||||||
|
self.regex_assertions(
|
||||||
|
regex=words.REGEX_CODE,
|
||||||
|
positive_list=[
|
||||||
|
('1.2', '1.2'), ('1', '1'), ('10', '10'),
|
||||||
|
('02', '02'), ('02.3', '02.3'),
|
||||||
|
('1.', '1'), ('03 ', '03'),
|
||||||
|
# error cases (only detectable in gcode context)
|
||||||
|
('30.12', '30.1'),
|
||||||
|
],
|
||||||
|
negative_list=['.2', '.', ' 2']
|
||||||
|
)
|
26
tests/testutils.py
Normal file
26
tests/testutils.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# utilities for the testing suite (as opposed to the tests for utils.py)
|
||||||
|
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')
|
Loading…
x
Reference in New Issue
Block a user