Merge pull request #3 from fragmuffin/develop

0.1.1 release
This commit is contained in:
Peter Boin 2017-07-30 21:03:39 +10:00 committed by GitHub
commit 24e92a8bc3
27 changed files with 1128 additions and 707 deletions

View File

@ -7,227 +7,18 @@ GCODE Parser for Python
Currently in development, ``pygcode`` is a low-level GCode interpreter Currently in development, ``pygcode`` is a low-level GCode interpreter
for python. for python.
Installation Installation
============ ============
Using `PyPi <https://pypi.python.org/pypi/pydemia>`__: Install using ``pip``
``pip install pygcode`` ``pip install pygcode``
Usage or `download directly from PyPi <https://pypi.python.org/pypi/pygcode>`__
=====
Just brainstorming here...
Writing GCode
-------------
Writing gcode from python object instances to text
::
>>> from pygcode import *
>>> gcodes = [
... GCodeRapidMove(Z=5),
... GCodeStartSpindleCW(),
... GCodeRapidMove(X=10, Y=20),
... GCodeFeedRate(200),
... GCodeLinearMove(Z=-1.5),
... GCodeRapidMove(Z=5),
... GCodeStopSpindle(),
... ]
>>> print('\n'.join(str(g) for g in gcodes))
G00 Z5
M03
G00 X10 Y20
F200
G01 Z-1.5
G00 Z5
M05
To plot along a lines of vectors, you could write... Documentation
=============
:: `Check out the wiki <https://github.com/fragmuffin/pygcode/wiki>`__ for documentation.
>>> from pygcode import *
>>> from euclid import Vector3
>>> vectors = [
... Vector3(0, 0, 0),
... Vector3(10, 0, 0),
... Vector3(10, 20, 0),
... Vector3(10, 20, 3),
... Vector3(0, 20, 3),
... Vector3(0, 0, 3),
... Vector3(0, 0, 0)
... ]
>>> to_coords = lambda v: {'X': v.x, 'Y': v.y, 'Z': v.z}
>>> for v in vectors:
... print("%s" % GCodeLinearMove(**to_coords(v)))
G01 X0 Y0 Z0
G01 X10 Y0 Z0
G01 X10 Y20 Z0
G01 X10 Y20 Z3
G01 X0 Y20 Z3
G01 X0 Y0 Z3
G01 X0 Y0 Z0
Reading / Interpreting GCode
----------------------------
To read gcode from a file, utilise the ``Line`` class.
Each ``Line`` instance contains a ``Block`` and an optional ``Comment``.
The ``Block`` contains a list of gcodes you're after.
::
from pygcode import Line
with open('part.gcode', 'r') as fh:
for line_text in fh.readlines():
line = Line(line_text)
print(line) # will print the line (with cosmetic changes)
line.block.gcodes # is your list of gcodes
line.block.modal_params # are all parameters not assigned to a gcode, assumed to be motion modal parameters
if line.comment:
line.comment.text # your comment text
To elaborate, here are some line examples
::
>>> from pygcode import Line
>>> line = Line('G01 x1 y2 f100 s1000 ; blah')
>>> print(line)
G01 X1 Y2 F100 S1000 ; blah
>>> print(line.block)
G01 X1 Y2 F100 S1000
>>> print(line.comment)
; blah
>>> line = Line('G0 x1 y2 (foo) f100 (bar) s1000')
>>> print(line)
G00 X1 Y2 F100 S1000 (foo. bar)
>>> print(line.comment)
(foo. bar)
Interpreting what a line of gcode does depends on the machine it's running on,
and also that machine's state (or 'mode')
The simple line of a rapid move to ``x=10, y=10`` may be ``G00 X10 Y10``.
However, if the machine in question is in "Incremental Motion" mode ``G91`` then
the machine will only end up at ``x=10, y=10`` if it started at ``x=0, y=0``
So, GCode interpretation is done via a virtual machine:
::
>>> from pygcode import Machine, GCodeRapidMove
>>> m = Machine()
>>> m.pos
<Position: X0 Y0 Z0>
>>> g = GCodeRapidMove(X=10, Y=20)
>>> m.process_gcodes(g)
>>> m.pos
<Position: X10 Y20 Z0>
>>> m.process_gcodes(g)
>>> m.pos
<Position: X10 Y20 Z0> # same position; machine in absolute mode
>>> m.mode.distance
<GCodeAbsoluteDistanceMode: G90> # see
>>> m.process_gcodes(GCodeIncrementalDistanceMode())
>>> m.process_gcodes(g) # same gcode as above
>>> m.pos
<Position: X20 Y40 Z0>
all valid ``m.mode`` attributes can be found with ``from pygcode.gcodes import MODAL_GROUP_MAP; MODAL_GROUP_MAP.keys()``
Also note that the order codes are interpreted is important.
For example, the following code is WRONG
::
from pygcode import Machine, Line
m = Machine()
line = Line('G0 x10 y10 G91')
m.process_gcodes(*line.block.gcodes) # WRONG!
This will process the movement to ``x=10, y=10``, and **then** it will change the
distance mode to *Incremental*... there are 2 ways to do this correctly.
- ``m.process_gcodes(*sorted(line.block.gcodes))``, or simply
- ``m.process_block(line.block)``
sorting a list of gcodes will sort them in execution order (as specified by
`LinuxCNC's order of execution <http://linuxcnc.org/docs/html/gcode/overview.html#_g_code_order_of_execution>`__).
``process_block`` does this automatically.
If you need to process & change one type of gcode (usually a movement),
you must split a list of gcodes into those executed before, and after the one
in question.
::
from pygcode import GCodeRapidMove, GCodeLinearMove
from pygcode import Machine, Line, split_gcodes
m = Machine()
line = Line('M0 G0 x10 y10 G91')
(befores, (g,), afters) = split_gcodes(line.block.gcodes, (GCodeRapidMove, GCodeLinearMove))
m.process_gcodes(*sorted(befores))
if g.X is not None:
g.X += 100 # shift linear movements (rapid or otherwise)
m.process_gcodes(g)
m.process_gcodes(*sorted(afters))
For a more practical use of machines & interpreting gcode, have a look at
`pygcode-normalize.py <https://github.com/fragmuffin/pygcode/blob/master/scripts/pygcode-normalize.py>`__
At the time of writing this, that script converts arcs to linear codes, and
expands drilling cycles to basic movements (so my
`GRBL <https://github.com/gnea/grbl>`__ machine can understand them)
Development
===========
This library came from my own needs to interpret and convert erroneous
arcs to linear segments, and to expand canned drilling cycles, but also
as a means to *learn* GCode.
As such there is no direct plan for further development, however I'm
interested in what you'd like to use it for, and cater for that.
Generally, in terms of what to support, I'm following the lead of:
- `GRBL <https://github.com/gnea/grbl>`__ and
- `LinuxCNC <http://linuxcnc.org/>`__
More support will come with increased interest.
So that is... if you don't like what it does, or how it's documented, make some
noise in the `issue section <https://github.com/fragmuffin/pygcode/issues>`__.
if you get in early, you may get some free labour out of me ;)
Supported G-Codes
-----------------
All GCodes supported by `LinuxCNC <http://linuxcnc.org>`__ can be written, and
parsed by ``pygcode``.
Few GCodes are accurately interpreted by a virtual CNC ``Machine`` instance.
Supported movements are currently;
- linear movements
- arc movements
- canned drilling cycles

View File

@ -1,102 +0,0 @@
# Notes on deployment
How I deployed this package (mainly just notes for myself)
Method based on the articles:
* http://peterdowns.com/posts/first-time-with-pypi.html and
* https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/
## Pre-requisites
```
pip install -U "pip>=1.4" "setuptools>=0.9" "wheel>=0.21" twine
```
## PyPi rc
`cat ~/.pypirc`
```
[distutils]
index-servers =
prod
test
[prod]
repository = https://upload.pypi.org/legacy/
username=FraggaMuffin
password=secret
[test]
repository=https://test.pypi.org/legacy/
username=FraggaMuffin
password=secret
```
`chmod 600 ~/.pypirc`
## Building
```
rm -rf build/
python setup.py sdist bdist_wheel
```
### Test Build (sdist)
**Python 2.x**
```
rmvirtualenv 27-test
mkvirtualenv 27-test
$WORKON_HOME/27-test/bin/pip install dist/pygcode-0.1.0.tar.gz
$WORKON_HOME/27-test/bin/python
>>> import pygcode
>>> pygcode.Line('g1 x2 y3 m3 s1000 f100').block.gcodes # or whatever
```
**Python 3.x**
```
rmvirtualenv 35-test
mkvirtualenv -p $(which python3) 35-test
$WORKON_HOME/35-test/bin/pip install dist/pygcode-0.1.0.tar.gz
$WORKON_HOME/35-test/bin/python
>>> import pygcode
>>> pygcode.Line('g1 x2 y3 m3 s1000 f100').block.gcodes # or whatever
```
### Test Build (wheel)
similar to above, but the `pip` call references `pygcode-0.1.0-py2.py3-none-any.whl` instead
make sure to `rmvirtualenv` to ensure `pygcode` is uninstalled from virtual environment
## Upload to PyPi Test server
`twine upload -r test dist/pygcode-0.1.0*`
Then another round of testing, where `pip` call is:
`$WORKON_HOME/<envname>/bin/pip install -i https://testpypi.python.org/pypi pygcode`
## Upload to PyPy server
all good!? sweet :+1: time to upload to 'production'
`twine upload -r prod dist/pygcode-0.1.0*`
and final tests with simply:
`$WORKON_HOME/<envname>/bin/pip install pygcode`

190
deployment/README.md Normal file
View File

@ -0,0 +1,190 @@
# Notes on deployment
How I deploy this package.
For anyone reading, this readme and all files in this folder are mainly just
notes for myself; they have little to do with pygcode itself.
However, if you're interested in deploying your own PyPi package, then hopefully
this can help.
Method based on the articles:
* http://peterdowns.com/posts/first-time-with-pypi.html and
* https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/
Deployment also heavily uses the `./deploy.sh` script in this folder.
At this time, running `./deploy.sh --help` displays:
```
Usage: ./deploy.sh {build|test|and so on ...}
This script is to maintain a consistent method of deployment and testing.
Deployment target: pygcode 0.1.1.dev0
Arguments:
Setup:
setup: Installs packages & sets up environment (requires sudo)
Compiling:
build: Execute setup to build packages (populates ../dist/)
creates both 'sdist' and 'wheel' distrobutions.
Virtual Environments:
rmenv py# Remove virtual environment
remkenv py# Remove, then create re-create virtual environment
envprereq py# install environment prerequisites (official PyPi)
Deploy:
deploy test Upload to PyPi test server
deploy prod Upload to PyPi (official)
Install:
install sdist py# Install from local sdist
install wheel py# Install from local wheel
install pypitest py# Install from PyPi test server
install pypi py# Install from PyPi (official)
Testing:
test dev py# Run tests on local dev in a virtual env
test installed py# Run tests on installed library in virtual env
Help:
-h | --help display this help message
py#: when referenced above means
'py2' for Python 2.7.12
'py3' for Python 3.5.2
```
# PyPi deployment
## Install Required Tools
`./deploy.sh setup`
## PyPi rc
`cat ~/.pypirc`
```
[distutils]
index-servers =
prod
test
[prod]
repository = https://upload.pypi.org/legacy/
username=FraggaMuffin
password=secret
[test]
repository=https://test.pypi.org/legacy/
username=FraggaMuffin
password=secret
```
`chmod 600 ~/.pypirc`
## Build and Test `sdist` and `wheel`
**Build**
```
./deploy.sh build
```
**Test `sdist`**
```
# Python 2.x
./deploy.sh remkenv py2
./deploy.sh envprereq py2
./deploy.sh install sdist py2
./deploy.sh test installed py2
# Python 3.x
./deploy.sh remkenv py3
./deploy.sh envprereq py3
./deploy.sh install sdist py3
./deploy.sh test installed py3
```
**Test `wheel`**
```
# Python 2.x
./deploy.sh remkenv py2
./deploy.sh install wheel py2
./deploy.sh test installed py2
# Python 3.x
./deploy.sh remkenv py3
./deploy.sh install wheel py3
./deploy.sh test installed py3
```
## Upload to PyPi Test server
```
./deploy.sh deploy test
```
**Test**
```
# Python 2.x
./deploy.sh remkenv py2
./deploy.sh envprereq py2
./deploy.sh install pypitest py2
./deploy.sh test installed py2
# Python 3.x
./deploy.sh remkenv py3
./deploy.sh envprereq py3
./deploy.sh install pypitest py3
./deploy.sh test installed py3
```
have a look at:
https://testpypi.python.org/pypi/pygcode
to make sure it's sane
## Upload to PyPy server
all good!? sweet :+1: time to upload to 'production'
```
./deploy.sh deploy prod
```
**Test**
```
# Python 2.x
./deploy.sh remkenv py2
./deploy.sh install pypi py2
./deploy.sh test installed py2
# Python 3.x
./deploy.sh remkenv py3
./deploy.sh install pypi py3
./deploy.sh test installed py3
```
have a look at:
https://pypi.python.org/pypi/pygcode
to make sure it's sane
# Deployment in Git
merge deployed branch to `master`
```
git tag ${version} -m "<change description>"
git push --tags origin master
```
have a look at the [releases page](https://github.com/fragmuffin/pygcode/releases) and it should be there.
tadaaaaaa!... go to sleep; it's probably late

227
deployment/deploy.sh Executable file
View File

@ -0,0 +1,227 @@
#!/usr/bin/env bash
source $(which virtualenvwrapper.sh)
function pyver() {
# write python version to stdout
$1 -c 'import sys; print("{}.{}.{}".format(*sys.version_info))'
}
function lib_ver() {
# Get library vesion number
pushd ../src > /dev/null
python -c 'import pygcode; print(pygcode.__version__)'
popd > /dev/null
}
function lib_prereq() {
pushd .. > /dev/null
python -c 'from setup import INSTALL_REQUIRES ; print(" ".join(INSTALL_REQUIRES))'
popd > /dev/null
}
# ============ Local Parameters
LIB_NAME=pygcode
LIB_VER=$(lib_ver)
# ============ Help Text
function show_help() {
[ "$@" ] && echo "$@"
cat << EOF
Usage: ./${0##*/} {build|test|and so on ...}
This script is to maintain a consistent method of deployment and testing.
Deployment target: ${LIB_NAME} ${LIB_VER}
Arguments:
Setup:
setup: Installs packages & sets up environment (requires sudo)
Compiling:
build: Execute setup to build packages (populates ../dist/)
creates both 'sdist' and 'wheel' distrobutions.
Virtual Environments:
rmenv py# Remove virtual environment
remkenv py# Remove, then create re-create virtual environment
envprereq py# install environment prerequisites (official PyPi)
Deploy:
deploy test Upload to PyPi test server
deploy prod Upload to PyPi (official)
Install:
install sdist py# Install from local sdist
install wheel py# Install from local wheel
install pypitest py# Install from PyPi test server
install pypi py# Install from PyPi (official)
Testing:
test dev py# Run tests on local dev in a virtual env
test installed py# Run tests on installed library in virtual env
Help:
-h | --help display this help message
py#: when referenced above means
'py2' for Python $(pyver python2)
'py3' for Python $(pyver python3)
EOF
}
# ============ Commands
function setup() {
# Install pre-requisite tooling
sudo pip install -U "pip>=1.4" "setuptools>=0.9" "wheel>=0.21" twine
}
function build() {
# Run setup.py to build sdist and wheel distrobutions
pushd ..
rm -rf build/
python setup.py sdist bdist_wheel
popd
}
function rmenv() {
# Remove virtual environment
set_venv_variables $1
rmvirtualenv ${env_name}
}
function remkenv() {
# Remove virtual environment, then re-create it from scratch
set_venv_variables $1
rmvirtualenv ${env_name}
mkvirtualenv --python=${python_bin} ${env_name}
}
function envprereq() {
set_venv_variables $1
workon ${env_name}
${env_pip_bin} install $(lib_prereq)
deactivate
}
function deploy() {
# Deploy compiled distributions to the test|prod server
_deployment_env=$1
pushd ..
twine upload -r ${_deployment_env} dist/${LIB_NAME}-${LIB_VER}*
popd
}
function install() {
# Install library from a variety of sources
_install_type=$1
_env_key=$2
set_venv_variables ${_env_key}
workon ${env_name}
case "${_install_type}" in
sdist)
# Install from locally compiled 'sdist' file
${env_pip_bin} install ../dist/${LIB_NAME}-${LIB_VER}.tar.gz
;;
wheel)
# Install from locally compiled 'wheel' file
${env_pip_bin} install ../dist/${LIB_NAME}-${LIB_VER}-py2.py3-none-any.whl
;;
pypitest)
# Install from PyPi test server
${env_pip_bin} install -i https://testpypi.python.org/pypi ${LIB_NAME}
;;
pypi)
# Install from official PyPi server
${env_pip_bin} install ${LIB_NAME}
;;
*)
echo invalid install type: \"${_install_type}\" >&2
exit 1
;;
esac
deactivate
}
function run_test() {
# Run tests
_test_scope=$1
_env_key=$2
set_venv_variables ${_env_key}
case "${_test_scope}" in
dev)
export PYGCODE_TESTSCOPE=local
;;
installed)
export PYGCODE_TESTSCOPE=installed
;;
*)
echo invalid test scope: \"${_test_scope}\" >&2
exit 1
;;
esac
pushd ../tests
workon ${env_name}
${env_python_bin} -m unittest discover -s . -p 'test_*.py'
deactivate
popd
}
# ============ Utilities
function set_venv_variables() {
# on successful exit, environment variables are set:
# env_name = virtual environment's name
# env_pip_bin = environment's pip binary
# env_python_bin = environment's python binary
# python_bin = python binary in host environment
_env_key=$1
env_name=${LIB_NAME}_${_env_key}
env_pip_bin=${WORKON_HOME}/${env_name}/bin/pip
env_python_bin=${WORKON_HOME}/${env_name}/bin/python
case "${_env_key}" in
py2)
python_bin=$(which python2)
;;
py3)
python_bin=$(which python3)
;;
*)
echo invalid environment key: \"${_env_key}\" >&2
exit 1
;;
esac
}
# ============ Option parsing
case "$1" in
# Show help on request
-h|--help)
show_help
exit 0
;;
# Valid Commands
setup) setup ;;
build) build ;;
rmenv) rmenv $2 ;;
remkenv) remkenv $2 ;;
envprereq) envprereq $2 ;;
deploy) deploy $2 ;;
install) install $2 $3 ;;
test) run_test $2 $3 ;;
# otherwise... show help
*)
show_help >&2
exit 1
;;
esac
echo ./${0##*/} completed successfully

Binary file not shown.

Binary file not shown.

BIN
dist/pygcode-0.1.1-py2.py3-none-any.whl vendored Normal file

Binary file not shown.

BIN
dist/pygcode-0.1.1.tar.gz vendored Normal file

Binary file not shown.

6
media/README.md Normal file
View File

@ -0,0 +1,6 @@
## `arc-linearize-method-*.png`
Screenshots of arc linearizing methods for the `pygcode-norm` script.
credit to _nraynaud_ for [this awesome online gcode interpreter](https://nraynaud.github.io/webgcode/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

245
scripts/pygcode-crop Executable file
View File

@ -0,0 +1,245 @@
#!/usr/bin/env python
# Script to remove commands of a gcode file before and after the given range.
# All gcodes before the given line will be replaced with equivalent rapid
# movements to get to the right position. Initial rapid movement will first move
# to the maximum Z value used in the removed portion, then move to XY,
# then move down to the correct Z.
import argparse
import re
from copy import copy
for pygcode_lib_type in ('installed_lib', 'relative_lib'):
try:
# pygcode
from pygcode import Machine, Mode
from pygcode import Line, Comment
from pygcode import GCodePlaneSelect, GCodeSelectXYPlane
from pygcode import GCodeRapidMove
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, '..', 'src'))
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 ===================
# --- Types
def range_type(value):
"""
Return (is_first, is_last) such that is_first(n, pos) will return True if
the gcode's current line is the first to be cropped, and similarly for the
last line.
:param value: string given as argument
:return: (is_first, is_last) callables
"""
# All files are cropped from one line, to another, identifying these lines is
# done via the [first] and [last] cropping criteria.
# - Line numbers (parameters: n)
# - Machine position (parameters: a,b,c,x,y,z)
# Comparisons, all with <letter><comparison><number>
# - = (or ==) equal to
# - != not equal to
# - < less than
# - <= less than or equal to
# - > greater than
# - >= greater than or equal to
match = re.search(r'^(?P<first>[^:]*):(?P<last>[^:]*)$', value)
if not match:
raise argparse.ArgumentTypeError("'%s' range invalid format" % value)
def _cmp(cmp_str):
"""
Convert strings like
'x>10.3'
into a callable equivalent to:
lambda n, pos: pos.X > 10.3
where:
n is the file's line number
pos is the machine's position (Position) instance
:param cmp_str: comparison string of the form: '<param><cmp><value>'
:return: callable
"""
CMP_MAP = {
'=': lambda a, b: a == b,
'==': lambda a, b: a == b,
'!=': lambda a, b: a != b,
'<': lambda a, b: a < b,
'<=': lambda a, b: a <= b,
'>': lambda a, b: a > b,
'>=': lambda a, b: a >= b,
}
# split comparison into (param, cmp, value)
m = re.search(
r'''^\s*
(
(?P<param>[abcnxyz])?\s* # parameter
(?P<cmp>(==?|!=|<=?|>=?)) # comparison
)?\s* # parameter & comparison defaults to "n="
(?P<value>-?\d+(\.\d+)?)\s*
$''',
cmp_str, re.IGNORECASE | re.MULTILINE | re.VERBOSE
)
if not m:
raise argparse.ArgumentTypeError("'%s' range comparison invalid" % cmp_str)
(param, cmp, val) = (
(m.group('param') or 'N').upper(), # default to 'N'
m.group('cmp') or '=', # default to '='
m.group('value')
)
# convert to lambda
if param == 'N':
if float(val) % 1:
raise argparse.ArgumentTypeError("'%s' line number must be an integer" % cmp_str)
return lambda n, pos: CMP_MAP[cmp](n, float(val))
else:
return lambda n, pos: CMP_MAP[cmp](getattr(pos, param), float(val))
def _cmp_group(group_str, default):
"""
Split given group_str by ',' and return callable that will return True
only if all comparisons are true.
So if group_str is:
x>=10.4,z>1
return will be a callable equivalent to:
lambda n, pos: (pos.X >= 10.4) and (pos.Z > 1)
(see _cmp for more detail)
:param group_str: string of _cmp valid strings delimited by ','s
:param default: default callable if group_str is falsey
:return: callable that returns True if all cmp's are true
"""
if not group_str:
return default
cmp_list = []
for cmp_str in group_str.split(','):
cmp_list.append(_cmp(cmp_str))
return lambda n, pos: all(x(n, pos) for x in cmp_list)
is_first = _cmp_group(match.group('first'), lambda n, pos: True)
is_last = _cmp_group(match.group('last'), lambda n, pos: False)
return (is_first, is_last)
# --- Defaults
# --- Create Parser
parser = argparse.ArgumentParser(
description="Remove gcode before and after given 'from' and 'to' conditions.",
epilog="Range Format:"
"""
range must be of the format:
[condition[,condition...]]:[condition[,condition...]]
the first condition(s) are true for the first line included in the cropped area
the second set are true for the first line excluded after the cropped area
Conditions:
each condition is of the format:
{variable}{operation}{number}
or, more specifically:
[[{a,b,c,n,x,y,z}]{=,!=,<,<=,>,>=}]{number}
Condition Variables:
n - file's line number
a|b|c - machine's angular axes
x|y|z - machine's linear axes
Example Ranges:
"100:200" will crop lines 100-199 (inclusive)
"z<=-2:" will isolate everything after the machine crosses z=-2
"x>10,y>10:n>=123" starts cropped area where both x and y exceed 10,
but only before line 123
Limitations:
Only takes points from start and finish of a gcode operation, so a line
through a condition region, or an arc that crosses a barrier will NOT
trigger the start or stop of cropping.
Probe alignment operations will not change virtual machine's position.
""",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
'infile', type=argparse.FileType('r'),
help="gcode file to crop",
)
parser.add_argument(
'range', type=range_type,
help="file range to crop, format [from]:[to] (details below)",
)
# --- Parse Arguments
args = parser.parse_args()
# =================== Cropping File ===================
# --- Machine
class NullMachine(Machine):
MODE_CLASS = type('NullMode', (Mode,), {'default_mode': ''})
machine = NullMachine()
pre_crop = True
post_crop = False
(is_first, is_last) = args.range
for (i, line_str) in enumerate(args.infile.readlines()):
line = Line(line_str)
# remember machine's state before processing the current line
old_machine = copy(machine)
machine.process_block(line.block)
if pre_crop:
if is_first(i + 1, machine.pos):
# First line inside cropping range
pre_crop = False
# Set machine's accumulated mode (from everything that's been cut)
mode_str = str(old_machine.mode)
if mode_str:
print(Comment("machine mode before cropping"))
print(mode_str)
# Getting machine's current (modal) selected plane
plane = old_machine.mode.plane_selection
if not isinstance(plane, GCodePlaneSelect):
plane = GCodeSelectXYPlane() # default to XY plane
# --- position machine before first cropped line
print(Comment("traverse into position, up, over, and down"))
# rapid move to Z (maximum Z the machine has experienced thus far)
print(GCodeRapidMove(**{
plane.normal_axis: getattr(old_machine.abs_range_max, plane.normal_axis),
}))
# rapid move to X,Y
print(GCodeRapidMove(**dict(
(k, v) for (k, v) in old_machine.pos.values.items()
if k in plane.plane_axes
)))
# rapid move to Z (machine.pos.Z)
print(GCodeRapidMove(**{
plane.normal_axis: getattr(old_machine.pos, plane.normal_axis),
}))
print('')
if (pre_crop, post_crop) == (False, False):
if is_last(i + 1, machine.pos):
# First line **outside** the area being cropped
# (ie: this line won't be output)
post_crop = True # although, irrelevant because...
break
else:
# inside cropping area
print(line)

View File

@ -23,13 +23,13 @@ for pygcode_lib_type in ('installed_lib', 'relative_lib'):
from pygcode.transform import linearize_arc, simplify_canned_cycle from pygcode.transform import linearize_arc, simplify_canned_cycle
from pygcode.transform import ArcLinearizeInside, ArcLinearizeOutside, ArcLinearizeMid from pygcode.transform import ArcLinearizeInside, ArcLinearizeOutside, ArcLinearizeMid
from pygcode.gcodes import _subclasses from pygcode.gcodes import _subclasses
from pygcode.utils import omit_redundant_modes from pygcode import utils
except ImportError: except ImportError:
import sys, os, inspect import sys, os, inspect
# Add pygcode (relative to this test-path) to the system path # Add pygcode (relative to this test-path) to the system path
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) _this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, os.path.join(_this_path, '..')) sys.path.insert(0, os.path.join(_this_path, '..', 'src'))
if pygcode_lib_type == 'installed_lib': if pygcode_lib_type == 'installed_lib':
continue # import was attempted before sys.path addition. retry import continue # import was attempted before sys.path addition. retry import
raise # otherwise the raised ImportError is a genuine problem raise # otherwise the raised ImportError is a genuine problem
@ -37,6 +37,43 @@ for pygcode_lib_type in ('installed_lib', 'relative_lib'):
# =================== Command Line Arguments =================== # =================== Command Line Arguments ===================
# --- Types
def arc_lin_method_type(value):
"""
:return: {Word('G2'): <linearize method class>, ... }
"""
ARC_LIN_CLASS_MAP = {
'i': ArcLinearizeInside,
'o': ArcLinearizeOutside,
'm': ArcLinearizeMid,
}
value_dict = defaultdict(lambda: ArcLinearizeMid)
if value:
match = re.search(r'^(?P<g2>[iom])(,(?P<g3>[iom]))?$', value, re.IGNORECASE)
if not match:
raise argparse.ArgumentTypeError("invalid format '%s'" % value)
value_dict = {
Word('g2'): ARC_LIN_CLASS_MAP[match.group('g2')],
Word('g3'): ARC_LIN_CLASS_MAP[match.group('g2')],
}
if match.group('g3'):
value_dict[Word('g3')] = ARC_LIN_CLASS_MAP[match.group('g3')]
return value_dict
def word_list_type(value):
"""
:return: [Word('G73'), Word('G89'), ... ]
"""
canned_code_words = set()
for word_str in re.split(r'\s*,\s*', value):
canned_code_words.add(Word(word_str))
return canned_code_words
# --- Defaults # --- Defaults
DEFAULT_PRECISION = 0.005 # mm DEFAULT_PRECISION = 0.005 # mm
DEFAULT_MACHINE_MODE = 'G0 G54 G17 G21 G90 G94 M5 M9 T0 F0 S0' DEFAULT_MACHINE_MODE = 'G0 G54 G17 G21 G90 G94 M5 M9 T0 F0 S0'
@ -44,16 +81,24 @@ 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)) DEFAULT_CANNED_CODES = ','.join(str(w) for w in sorted(c.word_key for c in _subclasses(GCodeCannedCycle) if c.word_key))
# --- Create Parser # --- Create Parser
parser = argparse.ArgumentParser(description='Normalize gcode for machine consistency using different CAM software') parser = argparse.ArgumentParser(
description="Normalize gcode for machine consistency when using different CAM software"
)
parser.add_argument( parser.add_argument(
'infile', type=argparse.FileType('r'), nargs=1, 'infile', type=argparse.FileType('r'),
help="gcode file to normalize", help="gcode file to normalize",
) )
parser.add_argument( parser.add_argument(
'--precision', '-p', dest='precision', type=float, default=DEFAULT_PRECISION, '--singles', '-s', dest='singles',
help="maximum positional error when generating gcodes (eg: arcs to lines) " action='store_const', const=True, default=False,
"(default: %g)" % DEFAULT_PRECISION, help="only output one command per gcode line",
)
parser.add_argument(
'--full', '-f', dest='full',
action='store_const', const=True, default=False,
help="output full commands, any modal parameters will be acompanied with "
"the fully qualified gcode command",
) )
# Machine # Machine
@ -67,7 +112,7 @@ group = parser.add_argument_group(
"Arc Linearizing", "Arc Linearizing",
"Converting arcs (G2/G3 codes) into linear interpolations (G1 codes) to " "Converting arcs (G2/G3 codes) into linear interpolations (G1 codes) to "
"aproximate the original arc. Indistinguishable from an original arc when " "aproximate the original arc. Indistinguishable from an original arc when "
"--precision is set low enough." "--arc_precision is set low enough."
) )
group.add_argument( group.add_argument(
'--arc_linearize', '-al', dest='arc_linearize', '--arc_linearize', '-al', dest='arc_linearize',
@ -75,12 +120,25 @@ group.add_argument(
help="convert G2,G3 commands to a series of linear interpolations (G1 codes)", help="convert G2,G3 commands to a series of linear interpolations (G1 codes)",
) )
group.add_argument( group.add_argument(
'--arc_lin_method', '-alm', dest='arc_lin_method', default=DEFAULT_ARC_LIN_METHOD, '--arc_lin_method', '-alm', dest='arc_lin_method',
type=arc_lin_method_type, default=DEFAULT_ARC_LIN_METHOD,
help="Method of linearizing arcs, i=inner, o=outer, m=mid. List 2 " 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'. " "for <cw>,<ccw>, eg 'i,o'. 'i' is equivalent to 'i,i'. "
"(default: '%s')" % DEFAULT_ARC_LIN_METHOD, "(default: '%s')" % DEFAULT_ARC_LIN_METHOD,
metavar='{i,o,m}[,{i,o,m}]', metavar='{i,o,m}[,{i,o,m}]',
) )
group.add_argument(
'--arc_precision', '-alp', dest='arc_precision', type=float, default=DEFAULT_PRECISION,
help="maximum positional error when creating linear interpolation codes "
"(default: %g)" % DEFAULT_PRECISION,
)
#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",
#)
# Canned Cycles # Canned Cycles
group = parser.add_argument_group( group = parser.add_argument_group(
@ -94,56 +152,43 @@ group.add_argument(
help="Expand canned cycles into basic linear movements, and pauses", help="Expand canned cycles into basic linear movements, and pauses",
) )
group.add_argument( group.add_argument(
'---canned_codes', '-cc', dest='canned_codes', default=DEFAULT_CANNED_CODES, '--canned_codes', '-cc', dest='canned_codes',
type=word_list_type, default=DEFAULT_CANNED_CODES,
help="List of canned gcodes to expand, (default is '%s')" % DEFAULT_CANNED_CODES, help="List of canned gcodes to expand, (default is '%s')" % DEFAULT_CANNED_CODES,
) )
#parser.add_argument( # Removing non-functional content
# '--arc_alignment', '-aa', dest='arc_alignment', type=str, choices=('XYZ','IJK','R'), group = parser.add_argument_group(
# default=None, "Removing Content",
# help="enforce precision on arcs, if XYZ the destination is altered to match the radius" "options for the removal of content"
# "if IJK or R then the arc'c centre point is moved to assure precision", )
#) group.add_argument(
'--rm_comments', '-rc', dest='rm_comments',
action='store_const', const=True, default=False,
help="remove all comments (non-functional)",
)
group.add_argument(
'--rm_blanks', '-rb', dest='rm_blanks',
action='store_const', const=True, default=False,
help="remove all empty lines (non-functional)",
)
group.add_argument(
'--rm_whitespace', '-rws', dest='rm_whitespace',
action='store_const', const=True, default=False,
help="remove all whitespace from gcode blocks (non-functional)",
)
group.add_argument(
'--rm_gcodes', '-rmg', dest='rm_gcodes',
type=word_list_type, default=[],
help="remove gcode (and it's parameters) with words in the given list "
"(eg: M6,G43) (note: only works for modal params with --full)",
)
# --- Parse Arguments # --- Parse Arguments
args = parser.parse_args() 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 =================== # =================== Create Virtual CNC Machine ===================
class MyMode(Mode): class MyMode(Mode):
default_mode = args.machine_mode default_mode = args.machine_mode
@ -154,6 +199,46 @@ class MyMachine(Machine):
machine = MyMachine() machine = MyMachine()
# =================== Utility Functions =================== # =================== Utility Functions ===================
omit_redundant_modes = utils.omit_redundant_modes
if args.full:
omit_redundant_modes = lambda gcode_iter: gcode_iter # bypass
def write(gcodes, modal_params=tuple(), comment=None):
"""
Write to output, while enforcing the flags:
args.singles
args.rm_comments
args.rm_blanks
args.rm_whitespace
:param obj: Line, Block, GCode or Comment instance
"""
assert not(args.full and modal_params), "with full specified, this should never be called with modal_params"
if args.singles and len(gcodes) > 1:
for g in sorted(gcodes):
write([g], comment=comment)
else:
# remove comments
if args.rm_comments:
comment = None
# remove particular gcodes
if args.rm_gcodes:
gcodes = [g for g in gcodes if g.word not in args.rm_gcodes]
# Convert to string & write to file (or stdout)
block_str = ' '.join(str(x) for x in (list(gcodes) + list(modal_params)))
if args.rm_whitespace:
block_str = re.sub(r'\s', '', block_str)
line_list = []
if block_str:
line_list.append(block_str)
if comment:
line_list.append(str(comment))
line_str = ' '.join(line_list)
if line_str or not args.rm_blanks:
print(line_str)
def gcodes2str(gcodes): def gcodes2str(gcodes):
return ' '.join("%s" % g for g in gcodes) return ' '.join("%s" % g for g in gcodes)
@ -167,25 +252,26 @@ def split_and_process(gcode_list, gcode_class, comment):
:param comment: Comment instance, or None :param comment: Comment instance, or None
""" """
(befores, (g,), afters) = split_gcodes(gcode_list, gcode_class) (befores, (g,), afters) = split_gcodes(gcode_list, gcode_class)
# print & process those before gcode_class instance # write & process those before gcode_class instance
if befores: if befores:
print(gcodes2str(befores)) write(befores)
machine.process_gcodes(*befores) machine.process_gcodes(*befores)
# yield, then process gcode_class instance # yield, then process gcode_class instance
yield g yield g
machine.process_gcodes(g) machine.process_gcodes(g)
# print & process those after gcode_class instance # write & process those after gcode_class instance
if afters: if afters:
print(gcodes2str(afters)) write(afters)
machine.process_gcodes(*afters) machine.process_gcodes(*afters)
# print comment (if given) # write comment (if given)
if comment: if comment:
print(str(line.comment)) write([], comment=line.comment)
# =================== Process File =================== # =================== Process File ===================
for line_str in args.infile[0].readlines(): for line_str in args.infile.readlines():
line = Line(line_str) line = Line(line_str)
# Effective G-Codes: # Effective G-Codes:
@ -194,7 +280,7 @@ for line_str in args.infile[0].readlines():
if args.arc_linearize and any(isinstance(g, GCodeArcMove) for g in effective_gcodes): 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: with split_and_process(effective_gcodes, GCodeArcMove, line.comment) as arc:
print(Comment("linearized arc: %r" % arc)) write([], comment=Comment("linearized arc: %r" % arc))
linearize_params = { linearize_params = {
'arc_gcode': arc, 'arc_gcode': arc,
'start_pos': machine.pos, 'start_pos': machine.pos,
@ -202,15 +288,15 @@ for line_str in args.infile[0].readlines():
'method_class': args.arc_lin_method[arc.word], 'method_class': args.arc_lin_method[arc.word],
'dist_mode': machine.mode.distance, 'dist_mode': machine.mode.distance,
'arc_dist_mode': machine.mode.arc_ijk_distance, 'arc_dist_mode': machine.mode.arc_ijk_distance,
'max_error': args.precision, 'max_error': args.arc_precision,
'decimal_places': 3, 'decimal_places': 3,
} }
for linear_gcode in omit_redundant_modes(linearize_arc(**linearize_params)): for linear_gcode in omit_redundant_modes(linearize_arc(**linearize_params)):
print(linear_gcode) write([linear_gcode])
elif args.canned_expand and any((g.word in args.canned_codes) for g in effective_gcodes): 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: with split_and_process(effective_gcodes, GCodeCannedCycle, line.comment) as canned:
print(Comment("expanded: %r" % canned)) write([], comment=Comment("expanded: %r" % canned))
simplify_canned_params = { simplify_canned_params = {
'canned_gcode': canned, 'canned_gcode': canned,
'start_pos': machine.pos, 'start_pos': machine.pos,
@ -219,8 +305,11 @@ for line_str in args.infile[0].readlines():
'axes': machine.axes, 'axes': machine.axes,
} }
for simplified_gcode in omit_redundant_modes(simplify_canned_cycle(**simplify_canned_params)): for simplified_gcode in omit_redundant_modes(simplify_canned_cycle(**simplify_canned_params)):
print(simplified_gcode) write([simplified_gcode])
else: else:
print(str(line)) if args.full:
write(effective_gcodes, comment=line.comment)
else:
write(line.block.gcodes, modal_params=line.block.modal_params, comment=line.comment)
machine.process_block(line.block) machine.process_block(line.block)

View File

@ -26,9 +26,14 @@ CLASSIFIERS = [
"Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering",
] ]
INSTALL_REQUIRES = [ INSTALL_REQUIRES = [
'argparse', # Python command-line parsing library
'euclid3', # 2D and 3D vector, matrix, quaternion and geometry module. 'euclid3', # 2D and 3D vector, matrix, quaternion and geometry module.
'six', # Python 2 and 3 compatibility utilities 'six', # Python 2 and 3 compatibility utilities
] ]
SCRIPTS = [
'scripts/pygcode-norm',
'scripts/pygcode-crop',
]
################################################################### ###################################################################
@ -78,4 +83,5 @@ if __name__ == "__main__":
zip_safe=False, zip_safe=False,
classifiers=CLASSIFIERS, classifiers=CLASSIFIERS,
install_requires=INSTALL_REQUIRES, install_requires=INSTALL_REQUIRES,
scripts=SCRIPTS,
) )

View File

@ -1,6 +1,6 @@
Metadata-Version: 1.1 Metadata-Version: 1.1
Name: pygcode Name: pygcode
Version: 0.1.0 Version: 0.1.1
Summary: Basic g-code parser, interpreter, and encoder library. Summary: Basic g-code parser, interpreter, and encoder library.
Home-page: https://github.com/fragmuffin/pygcode Home-page: https://github.com/fragmuffin/pygcode
Author: Peter Boin Author: Peter Boin
@ -15,230 +15,21 @@ Description: =======
Currently in development, ``pygcode`` is a low-level GCode interpreter Currently in development, ``pygcode`` is a low-level GCode interpreter
for python. for python.
Installation Installation
============ ============
Using `PyPi <https://pypi.python.org/pypi/pydemia>`__: Install using ``pip``
``pip install pygcode`` ``pip install pygcode``
Usage or `download directly from PyPi <https://pypi.python.org/pypi/pygcode>`__
=====
Just brainstorming here...
Writing GCode
-------------
Writing gcode from python object instances to text
::
>>> from pygcode import *
>>> gcodes = [
... GCodeRapidMove(Z=5),
... GCodeStartSpindleCW(),
... GCodeRapidMove(X=10, Y=20),
... GCodeFeedRate(200),
... GCodeLinearMove(Z=-1.5),
... GCodeRapidMove(Z=5),
... GCodeStopSpindle(),
... ]
>>> print('\n'.join(str(g) for g in gcodes))
G00 Z5
M03
G00 X10 Y20
F200
G01 Z-1.5
G00 Z5
M05
To plot along a lines of vectors, you could write... Documentation
=============
:: `Check out the wiki <https://github.com/fragmuffin/pygcode/wiki>`__ for documentation.
>>> from pygcode import *
>>> from euclid import Vector3
>>> vectors = [
... Vector3(0, 0, 0),
... Vector3(10, 0, 0),
... Vector3(10, 20, 0),
... Vector3(10, 20, 3),
... Vector3(0, 20, 3),
... Vector3(0, 0, 3),
... Vector3(0, 0, 0)
... ]
>>> to_coords = lambda v: {'X': v.x, 'Y': v.y, 'Z': v.z}
>>> for v in vectors:
... print("%s" % GCodeLinearMove(**to_coords(v)))
G01 X0 Y0 Z0
G01 X10 Y0 Z0
G01 X10 Y20 Z0
G01 X10 Y20 Z3
G01 X0 Y20 Z3
G01 X0 Y0 Z3
G01 X0 Y0 Z0
Reading / Interpreting GCode
----------------------------
To read gcode from a file, utilise the ``Line`` class.
Each ``Line`` instance contains a ``Block`` and an optional ``Comment``.
The ``Block`` contains a list of gcodes you're after.
::
from pygcode import Line
with open('part.gcode', 'r') as fh:
for line_text in fh.readlines():
line = Line(line_text)
print(line) # will print the line (with cosmetic changes)
line.block.gcodes # is your list of gcodes
line.block.modal_params # are all parameters not assigned to a gcode, assumed to be motion modal parameters
if line.comment:
line.comment.text # your comment text
To elaborate, here are some line examples
::
>>> from pygcode import Line
>>> line = Line('G01 x1 y2 f100 s1000 ; blah')
>>> print(line)
G01 X1 Y2 F100 S1000 ; blah
>>> print(line.block)
G01 X1 Y2 F100 S1000
>>> print(line.comment)
; blah
>>> line = Line('G0 x1 y2 (foo) f100 (bar) s1000')
>>> print(line)
G00 X1 Y2 F100 S1000 (foo. bar)
>>> print(line.comment)
(foo. bar)
Interpreting what a line of gcode does depends on the machine it's running on,
and also that machine's state (or 'mode')
The simple line of a rapid move to ``x=10, y=10`` may be ``G00 X10 Y10``.
However, if the machine in question is in "Incremental Motion" mode ``G91`` then
the machine will only end up at ``x=10, y=10`` if it started at ``x=0, y=0``
So, GCode interpretation is done via a virtual machine:
::
>>> from pygcode import Machine, GCodeRapidMove
>>> m = Machine()
>>> m.pos
<Position: X0 Y0 Z0>
>>> g = GCodeRapidMove(X=10, Y=20)
>>> m.process_gcodes(g)
>>> m.pos
<Position: X10 Y20 Z0>
>>> m.process_gcodes(g)
>>> m.pos
<Position: X10 Y20 Z0> # same position; machine in absolute mode
>>> m.mode.distance
<GCodeAbsoluteDistanceMode: G90> # see
>>> m.process_gcodes(GCodeIncrementalDistanceMode())
>>> m.process_gcodes(g) # same gcode as above
>>> m.pos
<Position: X20 Y40 Z0>
all valid ``m.mode`` attributes can be found with ``from pygcode.gcodes import MODAL_GROUP_MAP; MODAL_GROUP_MAP.keys()``
Also note that the order codes are interpreted is important.
For example, the following code is WRONG
::
from pygcode import Machine, Line
m = Machine()
line = Line('G0 x10 y10 G91')
m.process_gcodes(*line.block.gcodes) # WRONG!
This will process the movement to ``x=10, y=10``, and **then** it will change the
distance mode to *Incremental*... there are 2 ways to do this correctly.
- ``m.process_gcodes(*sorted(line.block.gcodes))``, or simply
- ``m.process_block(line.block)``
sorting a list of gcodes will sort them in execution order (as specified by
`LinuxCNC's order of execution <http://linuxcnc.org/docs/html/gcode/overview.html#_g_code_order_of_execution>`__).
``process_block`` does this automatically.
If you need to process & change one type of gcode (usually a movement),
you must split a list of gcodes into those executed before, and after the one
in question.
::
from pygcode import GCodeRapidMove, GCodeLinearMove
from pygcode import Machine, Line, split_gcodes
m = Machine()
line = Line('M0 G0 x10 y10 G91')
(befores, (g,), afters) = split_gcodes(line.block.gcodes, (GCodeRapidMove, GCodeLinearMove))
m.process_gcodes(*sorted(befores))
if g.X is not None:
g.X += 100 # shift linear movements (rapid or otherwise)
m.process_gcodes(g)
m.process_gcodes(*sorted(afters))
For a more practical use of machines & interpreting gcode, have a look at
`pygcode-normalize.py <https://github.com/fragmuffin/pygcode/blob/master/scripts/pygcode-normalize.py>`__
At the time of writing this, that script converts arcs to linear codes, and
expands drilling cycles to basic movements (so my
`GRBL <https://github.com/gnea/grbl>`__ machine can understand them)
Development
===========
This library came from my own needs to interpret and convert erroneous
arcs to linear segments, and to expand canned drilling cycles, but also
as a means to *learn* GCode.
As such there is no direct plan for further development, however I'm
interested in what you'd like to use it for, and cater for that.
Generally, in terms of what to support, I'm following the lead of:
- `GRBL <https://github.com/gnea/grbl>`__ and
- `LinuxCNC <http://linuxcnc.org/>`__
More support will come with increased interest.
So that is... if you don't like what it does, or how it's documented, make some
noise in the `issue section <https://github.com/fragmuffin/pygcode/issues>`__.
if you get in early, you may get some free labour out of me ;)
Supported G-Codes
-----------------
All GCodes supported by `LinuxCNC <http://linuxcnc.org>`__ can be written, and
parsed by ``pygcode``.
Few GCodes are accurately interpreted by a virtual CNC ``Machine`` instance.
Supported movements are currently;
- linear movements
- arc movements
- canned drilling cycles
Keywords: gcode,cnc,parser,interpreter Keywords: gcode,cnc,parser,interpreter
Platform: UNKNOWN Platform: UNKNOWN

View File

@ -1,6 +1,8 @@
README.rst README.rst
setup.cfg setup.cfg
setup.py setup.py
scripts/pygcode-crop
scripts/pygcode-norm
src/pygcode/__init__.py src/pygcode/__init__.py
src/pygcode/block.py src/pygcode/block.py
src/pygcode/comment.py src/pygcode/comment.py

View File

@ -1,2 +1,3 @@
argparse
euclid3 euclid3
six six

View File

@ -6,7 +6,7 @@
# 1.x - Development Status :: 5 - Production/Stable # 1.x - Development Status :: 5 - Production/Stable
# <any above>.y - developments on that version (pre-release) # <any above>.y - developments on that version (pre-release)
# <any above>*.dev* - development release (intended purely to test deployment) # <any above>*.dev* - development release (intended purely to test deployment)
__version__ = "0.1.0" __version__ = "0.1.1"
__title__ = "pygcode" __title__ = "pygcode"
__description__ = "Basic g-code parser, interpreter, and encoder library." __description__ = "Basic g-code parser, interpreter, and encoder library."
@ -53,7 +53,7 @@ __all__ = [
'GCodeCancelCannedCycle', 'GCodeCancelCannedCycle',
'GCodeCancelToolLengthOffset', 'GCodeCancelToolLengthOffset',
'GCodeCannedCycle', 'GCodeCannedCycle',
'GCodeCannedCycleReturnLevel', 'GCodeCannedCycleReturnPrevLevel',
'GCodeCannedCycleReturnToR', 'GCodeCannedCycleReturnToR',
'GCodeCannedReturnMode', 'GCodeCannedReturnMode',
'GCodeCoolant', 'GCodeCoolant',
@ -199,7 +199,7 @@ from .gcodes import (
# G83 - GCodeDrillingCyclePeck: G83: Drilling Cycle, Peck # G83 - GCodeDrillingCyclePeck: G83: Drilling Cycle, Peck
# G76 - GCodeThreadingCycle: G76: Threading Cycle # G76 - GCodeThreadingCycle: G76: Threading Cycle
# - GCodeCannedReturnMode: # - GCodeCannedReturnMode:
# G98 - GCodeCannedCycleReturnLevel: G98: Canned Cycle Return to the level set prior to cycle start # G98 - GCodeCannedCycleReturnPrevLevel: G98: Canned Cycle Return to the level set prior to cycle start
# G99 - GCodeCannedCycleReturnToR: G99: Canned Cycle Return to the level set by R # G99 - GCodeCannedCycleReturnToR: G99: Canned Cycle Return to the level set by R
# - GCodeCoolant: # - GCodeCoolant:
# M08 - GCodeCoolantFloodOn: M8: turn flood coolant on # M08 - GCodeCoolantFloodOn: M8: turn flood coolant on
@ -330,7 +330,7 @@ from .gcodes import (
GCodeCancelCannedCycle, GCodeCancelCannedCycle,
GCodeCancelToolLengthOffset, GCodeCancelToolLengthOffset,
GCodeCannedCycle, GCodeCannedCycle,
GCodeCannedCycleReturnLevel, GCodeCannedCycleReturnPrevLevel,
GCodeCannedCycleReturnToR, GCodeCannedCycleReturnToR,
GCodeCannedReturnMode, GCodeCannedReturnMode,
GCodeCoolant, GCodeCoolant,

View File

@ -96,7 +96,7 @@ MODAL_GROUP_MAP = {
# Traditionally Non-grouped: # Traditionally Non-grouped:
# Although these GCodes set the machine's mode, there are no other GCodes to # Although these GCodes set the machine's mode, there are no other GCodes to
# group with them. So although they're modal, they doesn't have a defined # group with them. So although they're modal, they don't have a defined
# modal group. # modal group.
# However, having a modal group assists with: # However, having a modal group assists with:
# - validating gcode blocks for conflicting commands # - validating gcode blocks for conflicting commands
@ -218,15 +218,29 @@ class GCode(object):
return copy(self.word_key) return copy(self.word_key)
raise AssertionError("class %r has no default word" % self.__class__) raise AssertionError("class %r has no default word" % self.__class__)
# Comparisons # Equality
def __eq__(self, other):
return (
(self.word == other.word) and
(self.params == other.params)
)
def __ne__(self, other):
return not self.__eq__(other)
# Sort by execution order
def __lt__(self, other): def __lt__(self, other):
"""Sort by execution order"""
return self.exec_order < other.exec_order return self.exec_order < other.exec_order
def __le__(self, other):
return self.exec_order <= other.exec_order
def __gt__(self, other): def __gt__(self, other):
"""Sort by execution order"""
return self.exec_order > other.exec_order return self.exec_order > other.exec_order
def __ge__(self, other):
return self.exec_order >= other.exec_order
# Parameters # Parameters
def add_parameter(self, word): def add_parameter(self, word):
""" """
@ -483,9 +497,17 @@ class GCodeCannedCycle(GCode):
if isinstance(machine.mode.canned_cycles_return, GCodeCannedCycleReturnToR): if isinstance(machine.mode.canned_cycles_return, GCodeCannedCycleReturnToR):
# canned return is to this.R, not this.Z (plane dependent) # canned return is to this.R, not this.Z (plane dependent)
moveto_coords.update({ moveto_coords.update({
machine.mode.plane_selection.normal_axis: this.R, machine.mode.plane_selection.normal_axis: self.R,
}) })
else: # default: GCodeCannedCycleReturnPrevLevel
# Remove this.Z (plane dependent) value (ie: no machine movement on this axis)
moveto_coords.pop(machine.mode.plane_selection.normal_axis, None)
# Process action 'L' times
loop_count = self.L
if (loop_count is None) or (loop_count <= 0):
loop_count = 1
for i in range(loop_count):
machine.move_to(**moveto_coords) machine.move_to(**moveto_coords)
@ -802,9 +824,11 @@ class GCodePlaneSelect(GCode):
# vectorYZ = GCodeSelectYZPlane.quat * (GCodeSelectZXPlane.quat.conjugate() * vectorZX) # vectorYZ = GCodeSelectYZPlane.quat * (GCodeSelectZXPlane.quat.conjugate() * vectorZX)
quat = None # Quaternion quat = None # Quaternion
# -- Plane Normal # -- Plane Axis Information
# Vector normal to plane (such that XYZ axes follow the right-hand rule) # Vector normal to plane (such that XYZ axes follow the right-hand rule)
normal_axis = None # Letter of normal axis (upper case) normal_axis = None # Letter of normal axis (upper case)
# Axes of plane
plane_axes = set()
normal = None # Vector3 normal = None # Vector3
@ -813,6 +837,7 @@ class GCodeSelectXYPlane(GCodePlaneSelect):
word_key = Word('G', 17) word_key = Word('G', 17)
quat = Quaternion() # no effect quat = Quaternion() # no effect
normal_axis = 'Z' normal_axis = 'Z'
plane_axes = set('XY')
normal = Vector3(0., 0., 1.) normal = Vector3(0., 0., 1.)
@ -824,6 +849,7 @@ class GCodeSelectZXPlane(GCodePlaneSelect):
Vector3(0., 0., 1.), Vector3(1., 0., 0.) Vector3(0., 0., 1.), Vector3(1., 0., 0.)
) )
normal_axis = 'Y' normal_axis = 'Y'
plane_axes = set('ZX')
normal = Vector3(0., 1., 0.) normal = Vector3(0., 1., 0.)
@ -835,6 +861,7 @@ class GCodeSelectYZPlane(GCodePlaneSelect):
Vector3(0., 1., 0.), Vector3(0., 0., 1.) Vector3(0., 1., 0.), Vector3(0., 0., 1.)
) )
normal_axis = 'X' normal_axis = 'X'
plane_axes = set('YZ')
normal = Vector3(1., 0., 0.) normal = Vector3(1., 0., 0.)
@ -929,7 +956,7 @@ class GCodeCannedReturnMode(GCode):
exec_order = 220 exec_order = 220
class GCodeCannedCycleReturnLevel(GCodeCannedReturnMode): class GCodeCannedCycleReturnPrevLevel(GCodeCannedReturnMode):
"""G98: Canned Cycle Return to the level set prior to cycle start""" """G98: Canned Cycle Return to the level set prior to cycle start"""
# "retract to the position that axis was in just before this series of one or more contiguous canned cycles was started" # "retract to the position that axis was in just before this series of one or more contiguous canned cycles was started"
word_key = Word('G', 98) word_key = Word('G', 98)

View File

@ -52,6 +52,9 @@ class Position(object):
self._value = defaultdict(lambda: 0.0, dict((k, 0.0) for k in self.axes)) self._value = defaultdict(lambda: 0.0, dict((k, 0.0) for k in self.axes))
self._value.update(kwargs) self._value.update(kwargs)
def __copy__(self):
return self.__class__(axes=copy(self.axes), unit=self._unit, **self.values)
def update(self, **coords): def update(self, **coords):
for (k, v) in coords.items(): for (k, v) in coords.items():
setattr(self, k, v) setattr(self, k, v)
@ -74,10 +77,6 @@ class Position(object):
else: else:
self.__dict__[key] = value self.__dict__[key] = value
# Copy
def __copy__(self):
return self.__class__(axes=copy(self.axes), unit=self._unit, **self.values)
# Equality # Equality
def __eq__(self, other): def __eq__(self, other):
if self.axes ^ other.axes: if self.axes ^ other.axes:
@ -87,7 +86,7 @@ class Position(object):
return self._value == other._value return self._value == other._value
else: else:
x = copy(other) x = copy(other)
x.set_unit(self._unit) x.unit = self._unit
return self._value == x._value return self._value == x._value
def __ne__(self, other): def __ne__(self, other):
@ -125,14 +124,48 @@ class Position(object):
__truediv__ = __div__ # Python 3 division __truediv__ = __div__ # Python 3 division
# Conversion # Conversion
def set_unit(self, unit): @property
if unit == self._unit: def unit(self):
return return self._unit
factor = UNIT_MAP[self._unit]['conversion_factor'][unit]
@unit.setter
def unit(self, value):
if value != self._unit:
factor = UNIT_MAP[self._unit]['conversion_factor'][value]
for k in [k for (k, v) in self._value.items() if v is not None]: for k in [k for (k, v) in self._value.items() if v is not None]:
self._value[k] *= factor self._value[k] *= factor
self._unit = unit self._unit = value
# Min/Max
@classmethod
def _cmp(cls, p1, p2, key):
"""
Returns a position of the combined min/max values for each axis
(eg: key=min for)
note: the result is not necessarily equal to either p1 or p2.
:param p1: Position instance
:param p2: Position instance
:return: Position instance with the highest/lowest value per axis
"""
if p2.unit != p1.unit:
p2 = copy(p2)
p2.unit = p1.unit
return cls(
unit=p1.unit,
**dict(
(k, key(getattr(p1, k), getattr(p2, k))) for k in p1.values
)
)
@classmethod
def min(cls, a, b):
return cls._cmp(a, b, key=min)
@classmethod
def max(cls, a, b):
return cls._cmp(a, b, key=max)
# Words & Values
@property @property
def words(self): def words(self):
return sorted(Word(k, self._value[k]) for k in self.axes) return sorted(Word(k, self._value[k]) for k in self.axes)
@ -145,6 +178,7 @@ class Position(object):
def vector(self): def vector(self):
return Vector3(self._value['X'], self._value['Y'], self._value['Z']) return Vector3(self._value['X'], self._value['Y'], self._value['Z'])
# String representation(s)
def __repr__(self): def __repr__(self):
return "<{class_name}: {coordinates}>".format( return "<{class_name}: {coordinates}>".format(
class_name=self.__class__.__name__, class_name=self.__class__.__name__,
@ -153,12 +187,13 @@ class Position(object):
class CoordinateSystem(object): class CoordinateSystem(object):
def __init__(self, axes): def __init__(self, axes=None):
self.offset = Position(axes) self.offset = Position(axes=axes)
def __add__(self, other): def __copy__(self):
if isinstance(other, CoordinateSystem): obj = self.__class__()
pass obj.offset = copy(self.offset)
return obj
def __repr__(self): def __repr__(self):
return "<{class_name}: offset={offset}>".format( return "<{class_name}: offset={offset}>".format(
@ -174,6 +209,7 @@ class State(object):
# AFAIK: this is everything needed to remember a machine's state that isn't # AFAIK: this is everything needed to remember a machine's state that isn't
# handled by modal gcodes. # handled by modal gcodes.
def __init__(self, axes=None): def __init__(self, axes=None):
self._axes = axes
# Coordinate Systems # Coordinate Systems
self.coord_systems = {} self.coord_systems = {}
for i in range(1, 10): # G54-G59.3 for i in range(1, 10): # G54-G59.3
@ -202,7 +238,11 @@ class State(object):
# Coordinate System selection: # Coordinate System selection:
# - G54-G59: select coordinate system (offsets from machine coordinates set by G10 L2) # - G54-G59: select coordinate system (offsets from machine coordinates set by G10 L2)
# TODO: Move this class into MachineState def __copy__(self):
obj = self.__class__(axes=self._axes)
obj.coord_systems = [copy(cs) for cs in self.coord_systems]
obj.offset = copy(self.offset)
return obj
@property @property
def coord_sys(self): def coord_sys(self):
@ -221,15 +261,6 @@ class State(object):
class Mode(object): class Mode(object):
"""Machine's mode""" """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 # Default Mode
# for a Grbl controller this can be obtained with the `$G` command, eg: # for a Grbl controller this can be obtained with the `$G` command, eg:
# > $G # > $G
@ -256,12 +287,18 @@ class Mode(object):
# Mode is defined by gcodes set by processed blocks: # Mode is defined by gcodes set by processed blocks:
# see modal_group in gcode.py module for details # see modal_group in gcode.py module for details
def __init__(self): def __init__(self, set_default=True):
self.modal_groups = defaultdict(lambda: None) self.modal_groups = defaultdict(lambda: None)
# Initialize # Initialize
if set_default:
self.set_mode(*Line(self.default_mode).block.gcodes) self.set_mode(*Line(self.default_mode).block.gcodes)
def __copy__(self):
obj = self.__class__(set_default=False)
obj.modal_groups = deepcopy(self.modal_groups)
return obj
def set_mode(self, *gcode_list): def set_mode(self, *gcode_list):
""" """
Set machine mode from given gcodes (will not be processed) Set machine mode from given gcodes (will not be processed)
@ -337,11 +374,23 @@ class Machine(object):
units_mode = getattr(self.mode, 'units', None) units_mode = getattr(self.mode, 'units', None)
self.Position = type('Position', (Position,), { self.Position = type('Position', (Position,), {
'default_axes': self.axes, 'default_axes': self.axes,
'default_unit': units_mode.unit_id if units_mode else UNIT_METRIC, 'default_unit': units_mode.unit_id if units_mode else Position.default_unit,
}) })
# Absolute machine position # Absolute machine position
self.abs_pos = self.Position() self.abs_pos = self.Position()
# Machine's motion range (min/max corners of a bounding box)
self.abs_range_min = copy(self.abs_pos)
self.abs_range_max = copy(self.abs_pos)
def __copy__(self):
obj = self.__class__()
obj.mode = copy(self.mode)
obj.state = copy(self.state)
obj.abs_pos = copy(self.abs_pos)
obj.abs_range_min = copy(self.abs_range_min)
obj.abs_range_max = copy(self.abs_range_max)
return obj
def set_mode(self, *gcode_list): def set_mode(self, *gcode_list):
self.mode.set_mode(*gcode_list) # passthrough self.mode.set_mode(*gcode_list) # passthrough
@ -351,6 +400,8 @@ class Machine(object):
if coord_sys_mode: if coord_sys_mode:
self.state.cur_coord_sys = coord_sys_mode.coord_system_id self.state.cur_coord_sys = coord_sys_mode.coord_system_id
# TODO: convert coord systems between inches/mm, G20/G21 respectively
def modal_gcode(self, modal_params): def modal_gcode(self, modal_params):
if not modal_params: if not modal_params:
@ -359,7 +410,9 @@ class Machine(object):
raise MachineInvalidState("unable to assign modal parameters when no motion mode is set") raise MachineInvalidState("unable to assign modal parameters when no motion mode is set")
params = copy(self.mode.motion.params) # dict params = copy(self.mode.motion.params) # dict
params.update(dict((w.letter, w) for w in modal_params)) # override retained modal parameters 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()) (modal_gcodes, unasigned_words) = words2gcodes(
[self.mode.motion.word] + list(params.values())
)
if unasigned_words: if unasigned_words:
raise MachineInvalidState("modal parameters '%s' cannot be assigned when in mode: %r" % ( raise MachineInvalidState("modal parameters '%s' cannot be assigned when in mode: %r" % (
' '.join(str(x) for x in unasigned_words), self.mode ' '.join(str(x) for x in unasigned_words), self.mode
@ -430,6 +483,11 @@ class Machine(object):
coord_sys_offset = getattr(self.state.coord_sys, 'offset', Position(axes=self.axes)) coord_sys_offset = getattr(self.state.coord_sys, 'offset', Position(axes=self.axes))
temp_offset = self.state.offset temp_offset = self.state.offset
self.abs_pos = (value + temp_offset) + coord_sys_offset self.abs_pos = (value + temp_offset) + coord_sys_offset
self._update_abs_range(self.abs_pos)
def _update_abs_range(self, pos):
self.abs_range_min = Position.min(pos, self.abs_range_min)
self.abs_range_max = Position.max(pos, self.abs_range_max)
# =================== Machine Actions =================== # =================== Machine Actions ===================
def move_to(self, rapid=False, **coords): def move_to(self, rapid=False, **coords):

View File

@ -7,7 +7,7 @@ from .gcodes import GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode
from .gcodes import GCodeAbsoluteArcDistanceMode, GCodeIncrementalArcDistanceMode from .gcodes import GCodeAbsoluteArcDistanceMode, GCodeIncrementalArcDistanceMode
from .gcodes import GCodeCannedCycle from .gcodes import GCodeCannedCycle
from .gcodes import GCodeDrillingCyclePeck, GCodeDrillingCycleDwell, GCodeDrillingCycleChipBreaking from .gcodes import GCodeDrillingCyclePeck, GCodeDrillingCycleDwell, GCodeDrillingCycleChipBreaking
from .gcodes import GCodeCannedReturnMode, GCodeCannedCycleReturnLevel, GCodeCannedCycleReturnToR from .gcodes import GCodeCannedReturnMode, GCodeCannedCycleReturnPrevLevel, GCodeCannedCycleReturnToR
from .gcodes import _gcodes_abs2rel from .gcodes import _gcodes_abs2rel
from .machine import Position from .machine import Position
@ -381,7 +381,7 @@ def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None,
DEFAULT_SCC_PLANE = GCodeSelectXYPlane DEFAULT_SCC_PLANE = GCodeSelectXYPlane
DEFAULT_SCC_DISTMODE = GCodeAbsoluteDistanceMode DEFAULT_SCC_DISTMODE = GCodeAbsoluteDistanceMode
DEFAULT_SCC_RETRACTMODE = GCodeCannedCycleReturnLevel DEFAULT_SCC_RETRACTMODE = GCodeCannedCycleReturnPrevLevel
def simplify_canned_cycle(canned_gcode, start_pos, def simplify_canned_cycle(canned_gcode, start_pos,
plane=None, dist_mode=None, retract_mode=None, plane=None, dist_mode=None, retract_mode=None,

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/usr/bin/env bash
python -m unittest discover -s . -p 'test_*.py' --verbose python -m unittest discover -s . -p 'test_*.py'

View File

@ -1,24 +0,0 @@
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()

View File

@ -8,6 +8,11 @@ add_pygcode_to_path()
from pygcode.machine import Position, Machine from pygcode.machine import Position, Machine
from pygcode.line import Line from pygcode.line import Line
from pygcode.exceptions import MachineInvalidAxis from pygcode.exceptions import MachineInvalidAxis
from pygcode.gcodes import (
GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode,
GCodeAbsoluteArcDistanceMode, GCodeIncrementalArcDistanceMode,
GCodeCannedCycleReturnPrevLevel, GCodeCannedCycleReturnToR,
)
class PositionTests(unittest.TestCase): class PositionTests(unittest.TestCase):
@ -81,43 +86,146 @@ class PositionTests(unittest.TestCase):
self.assertEqual(p / 2, Position(axes='XYZ', X=1, Y=5)) self.assertEqual(p / 2, Position(axes='XYZ', X=1, Y=5))
class MachineGCodeProcessingTests(unittest.TestCase): class MachineGCodeProcessingTests(unittest.TestCase):
def test_linear_movement(self): def assert_processed_lines(self, line_data, machine):
m = Machine() """
test_str = '''; move in a 10mm square Process lines & assert machine's position
F100 M3 S1000 ; 0 :param line_data: list of tuples [('g1 x2', {'X':2}), ... ]
g1 x0 y10 ; 1 """
g1 x10 y10 ; 2 for (i, (line_str, expected_pos)) in enumerate(line_data):
g1 x10 y0 ; 3 line = Line(line_str)
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: if line.block:
#print("\n%s" % line.block) machine.process_block(line.block)
m.process_block(line.block)
# Assert possition change correct # Assert possition change correct
comment = line.comment.text if expected_pos is not None:
if comment in expected_pos: p1 = machine.pos
self.assertEqual(m.pos, expected_pos[comment]) p2 = machine.Position(**expected_pos)
#print("%r\n%r\npos=%r" % (m.mode, m.state, m.pos)) self.assertEqual(p1, p2, "index:%i '%s': %r != %r" % (i, line_str, p1, p2))
# Rapid Movement
def test_rapid_abs(self):
m = Machine()
m.process_gcodes(GCodeAbsoluteDistanceMode())
line_data = [
('', {}), # start @ 0,0,0
('g0 x0 y10', {'X':0, 'Y':10}),
(' x10 y10', {'X':10, 'Y':10}),
(' x10 y0', {'X':10, 'Y':0}),
(' x0 y0', {'X':0, 'Y':0}),
]
self.assert_processed_lines(line_data, m)
#m = Machine() def test_rapid_inc(self):
# m = Machine()
#file = GCodeParser('part1.gcode') m.process_gcodes(GCodeIncrementalDistanceMode())
#for line in file.iterlines(): line_data = [
# for (i, gcode) in enumerate(line.block.gcode): ('', {}), # start @ 0,0,0
# if isinstance(gcode, GCodeArcMove): ('g0 y10', {'X':0, 'Y':10}),
# arc = gcode (' x10', {'X':10, 'Y':10}),
# line_params = arc.line_segments(precision=0.0005) (' y-10', {'X':10, 'Y':0}),
# for (' x-10', {'X':0, 'Y':0}),
]
self.assert_processed_lines(line_data, m)
# Linearly Interpolated Movement
def test_linear_abs(self):
m = Machine()
m.process_gcodes(GCodeAbsoluteDistanceMode())
line_data = [
('g1 x0 y10', {'X':0, 'Y':10}),
(' x10 y10', {'X':10, 'Y':10}),
(' x10 y0', {'X':10, 'Y':0}),
(' x0 y0', {'X':0, 'Y':0}),
]
self.assert_processed_lines(line_data, m)
def test_linear_inc(self):
m = Machine()
m.process_gcodes(GCodeIncrementalDistanceMode())
line_data = [
('g1 y10', {'X':0, 'Y':10}),
(' x10', {'X':10, 'Y':10}),
(' y-10', {'X':10, 'Y':0}),
(' x-10', {'X':0, 'Y':0}),
]
self.assert_processed_lines(line_data, m)
# Arc Movement
def test_arc_abs(self):
m = Machine()
m.process_gcodes(
GCodeAbsoluteDistanceMode(),
GCodeIncrementalArcDistanceMode(),
)
line_data = [
# Clockwise circle in 4 segments
('g2 x0 y10 i5 j5', {'X':0, 'Y':10}),
(' x10 y10 i5 j-5', {'X':10, 'Y':10}),
(' x10 y0 i-5 j-5', {'X':10, 'Y':0}),
(' x0 y0 i-5 j5', {'X':0, 'Y':0}),
# Counter-clockwise circle in 4 segments
('g3 x10 y0 i5 j5', {'X':10, 'Y':0}),
(' x10 y10 i-5 j5', {'X':10, 'Y':10}),
(' x0 y10 i-5 j-5', {'X':0, 'Y':10}),
(' x0 y0 i5 j-5', {'X':0, 'Y':0}),
]
self.assert_processed_lines(line_data, m)
def test_arc_inc(self):
m = Machine()
m.process_gcodes(
GCodeIncrementalDistanceMode(),
GCodeIncrementalArcDistanceMode(),
)
line_data = [
# Clockwise circle in 4 segments
('g2 y10 i5 j5', {'X':0, 'Y':10}),
(' x10 i5 j-5', {'X':10, 'Y':10}),
(' y-10 i-5 j-5', {'X':10, 'Y':0}),
(' x-10 i-5 j5', {'X':0, 'Y':0}),
# Counter-clockwise circle in 4 segments
('g3 x10 i5 j5', {'X':10, 'Y':0}),
(' y10 i-5 j5', {'X':10, 'Y':10}),
(' x-10 i-5 j-5', {'X':0, 'Y':10}),
(' y-10 i5 j-5', {'X':0, 'Y':0}),
]
self.assert_processed_lines(line_data, m)
# Canned Drilling Cycles
def test_canned_return2oldz(self):
m = Machine()
m.process_gcodes(
GCodeAbsoluteDistanceMode(),
GCodeCannedCycleReturnPrevLevel(),
)
line_data = [
('g0 z5', {'Z':5}),
('g81 x10 y20 z-2 r1', {'X':10, 'Y':20, 'Z':5}),
]
self.assert_processed_lines(line_data, m)
def test_canned_return2r(self):
m = Machine()
m.process_gcodes(
GCodeAbsoluteDistanceMode(),
GCodeCannedCycleReturnToR(),
)
line_data = [
('g0 z5', {'Z':5}),
('g81 x10 y20 z-2 r1', {'X':10, 'Y':20, 'Z':1}),
]
self.assert_processed_lines(line_data, m)
def test_canned_loops(self):
m = Machine()
m.process_gcodes(
GCodeAbsoluteDistanceMode(),
GCodeCannedCycleReturnPrevLevel(),
)
line_data = [
('g0 z5', None),
('g81 x10 y20 z-2 r1 l2', {'X':10, 'Y':20, 'Z':5}),
('g91', None), # switch to incremental mode
('g81 x10 y20 z-2 r1 l2', {'X':30, 'Y':60, 'Z':5}),
]
self.assert_processed_lines(line_data, m)

View File

@ -5,14 +5,20 @@ import inspect
import re import re
# Units Under Test # Units Under Test
_pygcode_in_path = False _pygcode_in_path = False
def add_pygcode_to_path(): def add_pygcode_to_path():
global _pygcode_in_path global _pygcode_in_path
if not _pygcode_in_path: if not _pygcode_in_path:
if os.environ.get('PYGCODE_TESTSCOPE', 'local') == 'installed':
# Run tests on the `pip install` installed libray
pass # nothing to be done
else:
# Run tests explicitly on files in ../src (ignore any installed libs)
# Add pygcode (relative to this test-path) to the system path # Add pygcode (relative to this test-path) to the system path
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) _this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, os.path.join(_this_path, '..')) sys.path.insert(0, os.path.join(_this_path, '..', 'src'))
_pygcode_in_path = True _pygcode_in_path = True