mirror of
https://git.mirrors.martin98.com/https://github.com/petaflot/pygcode
synced 2025-06-04 11:25:20 +08:00
commit
24e92a8bc3
221
README.rst
221
README.rst
@ -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
|
|
||||||
|
102
deployment.md
102
deployment.md
@ -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
190
deployment/README.md
Normal 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
227
deployment/deploy.sh
Executable 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
|
BIN
dist/pygcode-0.1.0-py2.py3-none-any.whl
vendored
BIN
dist/pygcode-0.1.0-py2.py3-none-any.whl
vendored
Binary file not shown.
BIN
dist/pygcode-0.1.0.tar.gz
vendored
BIN
dist/pygcode-0.1.0.tar.gz
vendored
Binary file not shown.
BIN
dist/pygcode-0.1.1-py2.py3-none-any.whl
vendored
Normal file
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
BIN
dist/pygcode-0.1.1.tar.gz
vendored
Normal file
Binary file not shown.
6
media/README.md
Normal file
6
media/README.md
Normal 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/)
|
BIN
media/arc-linearize-method-inside.png
Normal file
BIN
media/arc-linearize-method-inside.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
media/arc-linearize-method-mid.png
Normal file
BIN
media/arc-linearize-method-mid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
media/arc-linearize-method-mixed.png
Normal file
BIN
media/arc-linearize-method-mixed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
media/arc-linearize-method-outside.png
Normal file
BIN
media/arc-linearize-method-outside.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
245
scripts/pygcode-crop
Executable file
245
scripts/pygcode-crop
Executable 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)
|
@ -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)
|
6
setup.py
6
setup.py
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
|
argparse
|
||||||
euclid3
|
euclid3
|
||||||
six
|
six
|
||||||
|
@ -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,
|
||||||
|
@ -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,10 +497,18 @@ 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)
|
||||||
|
|
||||||
machine.move_to(**moveto_coords)
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
class GCodeDrillingCycle(GCodeCannedCycle):
|
class GCodeDrillingCycle(GCodeCannedCycle):
|
||||||
@ -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)
|
||||||
|
@ -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]
|
|
||||||
for k in [k for (k, v) in self._value.items() if v is not None]:
|
|
||||||
self._value[k] *= factor
|
|
||||||
self._unit = 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]:
|
||||||
|
self._value[k] *= factor
|
||||||
|
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,11 +287,17 @@ 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
|
||||||
self.set_mode(*Line(self.default_mode).block.gcodes)
|
if set_default:
|
||||||
|
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):
|
||||||
"""
|
"""
|
||||||
@ -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):
|
||||||
|
@ -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,
|
||||||
|
@ -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'
|
||||||
|
@ -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()
|
|
@ -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
|
if expected_pos is not None:
|
||||||
comment = line.comment.text
|
p1 = machine.pos
|
||||||
if comment in expected_pos:
|
p2 = machine.Position(**expected_pos)
|
||||||
self.assertEqual(m.pos, expected_pos[comment])
|
self.assertEqual(p1, p2, "index:%i '%s': %r != %r" % (i, line_str, p1, p2))
|
||||||
#print("%r\n%r\npos=%r" % (m.mode, m.state, m.pos))
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
@ -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:
|
||||||
# Add pygcode (relative to this test-path) to the system path
|
if os.environ.get('PYGCODE_TESTSCOPE', 'local') == 'installed':
|
||||||
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
# Run tests on the `pip install` installed libray
|
||||||
sys.path.insert(0, os.path.join(_this_path, '..'))
|
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
|
||||||
|
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
||||||
|
sys.path.insert(0, os.path.join(_this_path, '..', 'src'))
|
||||||
|
|
||||||
_pygcode_in_path = True
|
_pygcode_in_path = True
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user